# ltk cookbook Concrete recipes for patterns that come up often when building real applications and shells with `ltk`. Each recipe pairs a sketch of the problem with a copy-pasteable shape of the solution and pointers to the relevant APIs. If you are looking for the mental model, read [`docs/onboarding.md`](./onboarding.md) and [`docs/architecture.md`](./architecture.md) first. For per-widget reference, see [`docs/widgets.md`](./widgets.md). For theme JSON, see [`docs/theming.md`](./theming.md). ## Table of contents - [Responsive sizing across mobile / tablet / desktop](#responsive-sizing-across-mobile--tablet--desktop) - [Slide-in panel](#slide-in-panel) - [Password field with PAM submit](#password-field-with-pam-submit) - [Swipe-to-dismiss overlay](#swipe-to-dismiss-overlay) - [Runtime light / dark theme toggle](#runtime-light--dark-theme-toggle) - [Icon launcher with WrapGrid](#icon-launcher-with-wrapgrid) - [Channel-driven external state](#channel-driven-external-state) - [Toast / OSD with auto-expiry](#toast--osd-with-auto-expiry) - [Tab navigation between widgets](#tab-navigation-between-widgets) - [Multi-screen app via sub-state pattern](#multi-screen-app-via-sub-state-pattern) - [Embedding ltk without `ltk::run`](#embedding-ltk-without-ltkrun) - [Custom CPU drawing and path clipping](#custom-cpu-drawing-and-path-clipping) - [Projecting an externally-laid-out view tree](#projecting-an-externally-laid-out-view-tree) --- ## Responsive sizing across mobile / tablet / desktop Size everything as a fraction of the surface so one view reads coherently from a portrait phone to a landscape desktop window, without per-target forks. The rule is *fluid but clamped*: a `vmin` percentage tracks the surface's smaller side, and a px `clamp` keeps it from collapsing on a tiny screen or ballooning on a 4K monitor. Use `Length::orient( portrait, landscape )` when the *proportion* itself should differ between orientations, and `Image::short_side` to size a logo along the screen's short side while preserving its aspect ratio. ```rust,no_run # use std::sync::Arc; # use ltk::{ column, img_widget, text, Length, Element }; # #[ derive( Clone ) ] enum Msg {} # fn _ex( logo: Arc>, lw: u32, lh: u32 ) -> Element { column::() .padding( Length::vmin( 4.0 ).clamp( 16.0, 48.0 ) ) .spacing( Length::vmin( 2.0 ).clamp( 8.0, 24.0 ) ) // Logo: 40 % of the width in portrait, 5 % of the height in landscape. .push( img_widget( logo, lw, lh ).short_side( Length::orient( 40.0, 5.0 ) ) ) // Heading: fluid, but never below 20 px nor above 44 px. .push( text( "Welcome" ).size( Length::vmin( 6.0 ).clamp( 20.0, 44.0 ) ) ) .into() # } ``` Fluid units scale with the screen's pixels, not real-world millimetres. For body text that must stay physically legible and honour the user's font-size preference across an open-ended device set, prefer `Length::dp` or `Length::em` over raw `vmin`; the [`typography`](../src/theme/typography.rs) scale (`h0`…`body_xs`) gives clamped-`vmin` sizes tuned for running text. Reserve `view()`-level branching on `surface_width` / `surface_height` for genuine layout restructuring (sidebar → bottom tabs), not for sizing. --- ## Slide-in panel A quick-settings or notification panel that slides down from the top of the screen and dissolves into transparency at its leading edge so the edge does not knife-cut against the layer below. ```rust,no_run # use std::time::Instant; # use ltk::{ container, text, viewport, Anchor, Element, Layer, Length, OverlayId, OverlaySpec }; # const OVERLAY_QS: OverlayId = OverlayId( 1 ); # const SLIDE_DURATION: f32 = 0.25; # #[ derive( Clone ) ] enum Msg { CloseQs } # struct App { qs_started: Option, surface_width: u32, surface_height: u32 } # impl App { # fn quick_settings_view( &self ) -> Element { text( "qs" ).into() } fn build_quick_settings_overlay( &self ) -> OverlaySpec { // Compute the slide progress based on a stored start instant. While // animating, `is_animating()` returns `true` so the runtime redraws // at ~60 Hz and reads the new progress every frame. let progress = match self.qs_started { Some( t ) => ( t.elapsed().as_secs_f32() / SLIDE_DURATION ).min( 1.0 ), None => 1.0, }; let panel_height = self.surface_height as f32 * 0.85; let visible_h = panel_height * progress; // Feather the bottom edge during the slide; drop the fade once the // panel is fully open so the bottom of a settled panel is hard. let fade_px = if progress < 1.0 { 16.0 } else { 0.0 }; let panel: Element = container( self.quick_settings_view() ) .surface( "surface-card" ) .padding( 24.0 ) .into(); OverlaySpec { id: OVERLAY_QS, layer: Layer::Overlay, anchor: Anchor::TOP, size: ( Length::px( self.surface_width as f32 ), Length::px( visible_h ) ), exclusive_zone: 0, keyboard_exclusive: false, input_region: None, view: viewport( panel ) .height( panel_height ) .fade_bottom( fade_px ) .into(), on_dismiss: Some( Msg::CloseQs ), anchor_widget_id: None, } } fn is_animating( &self ) -> bool { self.qs_started .map( |t| t.elapsed().as_secs_f32() < SLIDE_DURATION ) .unwrap_or( false ) } # } ``` The `fade_bottom( px )` builder is a GLES-only effect; the software backend renders a hard edge. If your shell must look identical on both backends, branch on [`ltk::is_software_render()`] and skip the fade when the software path is active. **See also**: [`Viewport`](./widgets.md#viewport), [`OverlaySpec`](../src/app.rs). --- ## Password field with PAM submit A login screen where the user types a password, presses Enter, and the app forwards the submission to a background thread that runs PAM. ```rust,no_run # use ltk::{ button, column, text, text_edit, App, ChannelSender, Element }; # #[ derive( Clone ) ] enum Msg { # UsernameChanged( String ), PasswordChanged( String ), # Submit, AuthResult( bool ), # } # fn pam_authenticate( _user: &str, _pass: &str ) -> bool { true } struct LoginApp { username: String, password: String, sender: Option>, } impl App for LoginApp { type Message = Msg; fn view( &self ) -> Element { column() .padding( 32.0 ) .spacing( 16.0 ) .push( text( "Sign in" ).size( 28.0 ) ) .push( text_edit( "Username", &self.username ) .on_change( |s| Msg::UsernameChanged( s ) ), ) .push( text_edit( "Password", &self.password ) .secure( true ) // mask glyphs + zeroize on drop .on_change( |s| Msg::PasswordChanged( s ) ) .on_submit( Msg::Submit ), // Enter fires this ) .push( button( "Log in" ).on_press( Msg::Submit ) ) .into() } fn set_channel_sender( &mut self, s: ChannelSender ) { // Saved once at startup; cloned into worker threads so they can // wake the loop without polling. self.sender = Some( s ); } fn update( &mut self, msg: Msg ) { match msg { Msg::UsernameChanged( s ) => self.username = s, Msg::PasswordChanged( s ) => self.password = s, Msg::Submit => { let username = self.username.clone(); let password = self.password.clone(); let sender = self.sender.clone().unwrap(); std::thread::spawn( move || { let result = pam_authenticate( &username, &password ); let _ = sender.send( Msg::AuthResult( result ) ); } ); // Clear the visible field so the user has feedback; // `secure( true )` zeroizes the buffer when the next // view() rebuild drops the old TextEdit. self.password.clear(); } Msg::AuthResult( true ) => std::process::exit( 0 ), Msg::AuthResult( false ) => { /* show error */ } } } } ``` `secure( true )` does two things: 1. Renders bullets instead of the actual characters. 2. Wipes the underlying byte buffer with zeroes when the [`TextEdit`](./widgets.md#text_edit) (and the per-frame handler snapshot) is dropped — the heap allocation that used to hold the credential is overwritten before it returns to the allocator. `text_edit::on_submit` fires when the user presses Enter inside the field; it is wired to the same `Msg::Submit` the explicit button uses so both keyboard and pointer paths converge on the same `update` arm. The cleanup of `self.password` is the application's responsibility once authentication completes. `ltk` cannot guarantee a clean memory image on its own — for the strongest guarantees clear long-lived state in your `Drop` impl too. **See also**: [`SECURITY.md`](../SECURITY.md), the [`text_edit`](./widgets.md#text_edit) widget, and [`crate::ChannelSender`](../src/app.rs). --- ## Swipe-to-dismiss overlay A modal panel that closes when the user swipes down past a threshold or taps outside the panel. ```rust,no_run # use ltk::{ column, container, spacer, text, Anchor, Element, Layer, Length, OverlayId, OverlaySpec }; # const OVERLAY_MODAL: OverlayId = OverlayId( 2 ); # #[ derive( Clone ) ] enum Msg { CloseModal } # struct App { modal_open: bool, modal_drag_progress: f32 } # impl App { # fn modal_body( &self ) -> Element { text( "modal" ).into() } fn overlays( &self ) -> Vec> { if !self.modal_open { return vec![]; } // The modal body sits inside a column capped at 400 px so it stays // legible on wide displays; the outer column with two spacers // centres it vertically. let modal: Element = column() .max_width( 400.0 ) .push( container( self.modal_body() ) .surface( "surface-card" ) .padding( 24.0 ), ) .into(); vec![ OverlaySpec { id: OVERLAY_MODAL, layer: Layer::Overlay, anchor: Anchor::ALL, size: ( Length::px( 0.0 ), Length::px( 0.0 ) ), exclusive_zone: 0, keyboard_exclusive: false, input_region: None, // accept input view: column() .center_y( true ) .push( spacer() ) .push( modal ) .push( spacer() ) .into(), on_dismiss: Some( Msg::CloseModal ), // tap outside dismisses anchor_widget_id: None, }, ] } // Swipe-down gesture (only fires inside the overlay because the main // surface does not declare a down-swipe target). fn on_swipe_down( &mut self ) -> Option { Some( Msg::CloseModal ) } fn on_swipe_down_progress( &mut self, progress: f32 ) { // Optional follow-the-finger feedback: store the in-progress value // and use it in view() to translate or fade the modal contents. self.modal_drag_progress = progress; } # } ``` `on_dismiss` is the runtime's "tap outside the panel" hook — it fires for any release on the overlay surface that did not land on an interactive widget. The runtime also fires it on a press on the main surface that does not hit the trigger (relevant for xdg-popup overlays under compositors that do not break the popup grab on parent-surface clicks) and on Escape with an xdg-popup open. Pair with `on_swipe_down` for a follow-the-finger gesture, and `on_swipe_down_progress` if you want the modal to track the finger before commit. **See also**: [`OverlaySpec`](../src/app.rs), [`docs/architecture.md`](./architecture.md#surface-composition). --- ## Runtime light / dark theme toggle A "switch theme" affordance that flips the active mode without a restart. ```rust,no_run # use ltk::{ button, Element, ThemeMode }; # #[ derive( Clone ) ] enum Msg { ToggleTheme } # struct App; # impl App { fn view( &self ) -> Element { let label = match ltk::active_mode() { ThemeMode::Light => "Switch to dark", ThemeMode::Dark => "Switch to light", }; button( label ).on_press( Msg::ToggleTheme ).into() } fn update( &mut self, msg: Msg ) { match msg { Msg::ToggleTheme => { let new = match ltk::active_mode() { ThemeMode::Light => ThemeMode::Dark, ThemeMode::Dark => ThemeMode::Light, }; ltk::set_active_mode( new ); // No further action — the next render reads the new mode // through the slot helpers and recomposes the surface. } } } # } ``` `set_active_mode` mutates a process-global cell; the next `view()` rebuild reads the new mode through the per-slot helpers ([`theme_palette`], [`theme_surface`], [`theme_paint`], etc.) and renders against the new colours. There is no manual invalidation step. For a full theme swap, load a different `ThemeDocument` and apply it: ```rust,no_run # fn _ex() { let doc = ltk::ThemeDocument::find( "midnight" ) .expect( "midnight theme not installed" ); ltk::set_active_document( doc ); # } ``` **See also**: [`docs/theming.md`](./theming.md#using-the-theme-from-app-code). --- ## Icon launcher with WrapGrid An app drawer that displays an N-column grid of icon buttons, scrolls when content overflows, and caches decoded icons across frames. ```rust,no_run # use std::cell::RefCell; # use std::collections::HashMap; # use std::sync::Arc; # use ltk::{ grid, icon_button, scroll, App, Element }; # #[ derive( Clone ) ] enum Msg { Launch( String ) } # struct DesktopEntry { id: String, icon_path: String } # fn decode_icon( _path: &str ) -> ( Arc>, u32, u32 ) { # ( Arc::new( vec![ 0; 4 ] ), 1, 1 ) # } struct LauncherApp { apps: Vec, icon_cache: RefCell>, u32, u32 )>>, } impl App for LauncherApp { type Message = Msg; fn view( &self ) -> Element { let mut grid = grid::( 4 ) .padding( 16.0 ) .spacing( 12.0 ); for app in &self.apps { // Decode icons once on first reference; reuse the Arc on // every subsequent frame for a pointer copy. let ( bytes, w, h ) = self .icon_cache .borrow_mut() .entry( app.id.clone() ) .or_insert_with( || decode_icon( &app.icon_path ) ) .clone(); grid = grid.push( icon_button( bytes, w, h ) .on_press( Msg::Launch( app.id.clone() ) ), ); } scroll( grid ).into() } fn update( &mut self, _msg: Msg ) { /* ... */ } } ``` The `RefCell>` cache is single-threaded — the runtime is single-threaded, so `Mutex` would only add lock overhead. Decoded icons are kept as `Arc>` so building the widget is a pointer clone instead of a full byte clone. **See also**: [`grid`](./widgets.md#grid), [`scroll`](./widgets.md#scroll), [`docs/architecture.md`](./architecture.md#larger-state-patterns). --- ## Channel-driven external state Surface external events (D-Bus signals, file watches, timers) into the event loop without busy-polling. ```rust,no_run # use std::time::{ Duration, Instant }; # use ltk::ChannelSender; # #[ derive( Clone ) ] struct BatteryEvent; # fn wait_for_battery_event() -> BatteryEvent { BatteryEvent } # struct OsdToast { expires_at: Instant } # #[ derive( Clone ) ] enum Msg { BatteryChanged( BatteryEvent ), HideToast } # struct App { sender: Option>, toast: Option } # impl App { fn set_channel_sender( &mut self, sender: ChannelSender ) { // Saved once and never changed. self.sender = Some( sender.clone() ); // Spawn the worker that watches for external events and forwards // them as messages. Substitute `wait_for_battery_event` with whatever // blocks for your real source — a D-Bus signal, a file watch, a // socket read, a timer, etc. std::thread::spawn( move || { loop { // Block until the external source produces an event. When it // arrives, post a message into the loop. let event = wait_for_battery_event(); let _ = sender.send( Msg::BatteryChanged( event ) ); } } ); } fn poll_external( &mut self ) -> Vec { // For state that doesn't need a dedicated thread (file mtime checks, // expiry sweeps), drain it here. Called after every Wayland event // and every poll_interval tick. let mut msgs = vec![]; if let Some( osd ) = self.toast.as_ref() { if osd.expires_at <= Instant::now() { msgs.push( Msg::HideToast ); } } msgs } fn poll_interval( &self ) -> Option { // Wake every minute to re-check the clock display. Keep this `None` // unless you actually need a wall-clock tick — it costs battery // life on mobile targets. Some( Duration::from_secs( 60 ) ) } # } ``` The `set_channel_sender` hook gives you a clone-able sender that wakes the event loop from another thread without busy-waiting. `poll_external` runs in the loop's own thread after each tick, so anything that costs CPU but does not block (TTL checks, RefCell snapshot diffs) is fine there. **See also**: [`docs/architecture.md`](./architecture.md#larger-state-patterns). --- ## Toast / OSD with auto-expiry A short-lived overlay that fades out after a fixed duration without blocking other UI. ```rust,no_run # use std::time::Instant; # use ltk::{ container, text, App, Anchor, Color, Element, Layer, Length, OverlayId, OverlaySpec }; # const OVERLAY_TOAST: OverlayId = OverlayId( 3 ); # #[ derive( Clone ) ] enum Msg {} struct AppState { toast: Option, // ... } struct Toast { text: String, started: Instant, } const TOAST_DURATION: f32 = 2.0; const TOAST_FADE: f32 = 0.25; impl AppState { # fn main_view( &self ) -> Element { text( "main" ).into() } // ... } impl App for AppState { type Message = Msg; fn view( &self ) -> Element { self.main_view() } fn overlays( &self ) -> Vec> { let toast = match &self.toast { Some( t ) => t, None => return vec![], }; let elapsed = toast.started.elapsed().as_secs_f32(); let alpha = if elapsed >= TOAST_DURATION { // Fade-out window: 0.25 s after expiry the alpha hits 0. ( 1.0 - ( elapsed - TOAST_DURATION ) / TOAST_FADE ).clamp( 0.0, 1.0 ) } else { 1.0 }; vec![ OverlaySpec { id: OVERLAY_TOAST, layer: Layer::Overlay, anchor: Anchor::BOTTOM, size: ( Length::px( 0.0 ), Length::px( 0.0 ) ), exclusive_zone: 0, keyboard_exclusive: false, input_region: Some( vec![] ), // pass-through view: container( text( &toast.text ).color( Color::WHITE ) ) .surface( "surface-panel" ) .padding( 12.0 ) .opacity( alpha ) .into(), on_dismiss: None, anchor_widget_id: None, }, ] } fn update( &mut self, _msg: Msg ) {} fn is_animating( &self ) -> bool { // Redraw at 60 Hz while the toast is visible or fading. self.toast.is_some() } fn poll_external( &mut self ) -> Vec { // Drop the toast once the fade window completes. if let Some( t ) = &self.toast { if t.started.elapsed().as_secs_f32() >= TOAST_DURATION + TOAST_FADE { self.toast = None; } } vec![] } } ``` Two ideas worth noting: - `input_region: Some( vec![] )` makes the overlay pass-through: pointer events fall through to whatever surface is below. The toast does not steal taps from the main UI. - The fade-out cleanup belongs in `poll_external`, not `view()`. `view` must stay pure — read state, build a tree, no mutation. **See also**: [`docs/architecture.md`](./architecture.md#animations). --- ## Tab navigation between widgets The runtime ships Tab / Shift+Tab traversal automatically; you only need to set up programmatic focus when an external event should land focus on a specific widget. ```rust,no_run # use ltk::{ column, text_edit, App, Element, WidgetId }; # #[ derive( Clone ) ] enum Msg { # UsernameChanged( String ), PasswordChanged( String ), AuthFailed, # } const FIELD_USERNAME: WidgetId = WidgetId( "username" ); const FIELD_PASSWORD: WidgetId = WidgetId( "password" ); struct LoginApp { username: String, password: String, pending_focus: Option, // ... } impl App for LoginApp { type Message = Msg; fn view( &self ) -> Element { column() .push( text_edit( "Username", &self.username ) .id( FIELD_USERNAME ) .on_change( |s| Msg::UsernameChanged( s ) ), ) .push( text_edit( "Password", &self.password ) .id( FIELD_PASSWORD ) .secure( true ) .on_change( |s| Msg::PasswordChanged( s ) ), ) .into() } fn take_focus_request( &mut self ) -> Option { // Returned once; the runtime focuses that widget on the next // frame. Subsequent calls return None. self.pending_focus.take() } fn update( &mut self, msg: Msg ) { if matches!( msg, Msg::AuthFailed ) { // Clear the password and put focus back on the field so the // user can retype without a click. self.password.clear(); self.pending_focus = Some( FIELD_PASSWORD ); } } } ``` `take_focus_request` is consumed once: the runtime dispatches focus to the returned id and the next call returns `None` because the slot was drained. The application owns "when should focus move". Tab and Shift+Tab traverse focusable widgets in declaration order — no opt-in needed beyond the widget being interactive. **See also**: [`WidgetId`](../src/types.rs), [`tests/tab_navigation.rs`](../tests/tab_navigation.rs). --- ## Multi-screen app via sub-state pattern When the app has more than ~30 message variants it is time to split by screen. Each screen owns its sub-state and sub-message, and the top-level enum wraps them. ```rust,no_run # use ltk::{ column, text, App, Element }; # #[ derive( Clone, Copy ) ] enum Screen { Home, Settings, About } # #[ derive( Clone ) ] enum HomeMsg {} # #[ derive( Clone ) ] enum SettingsMsg {} # struct HomeState; # struct SettingsState; # fn home_view( _: &HomeState ) -> Element { text( "home" ).into() } # fn home_update( _: &mut HomeState, _: HomeMsg ) {} # fn settings_view( _: &SettingsState ) -> Element { text( "settings" ).into() } # fn settings_update( _: &mut SettingsState, _: SettingsMsg ) {} # fn about_view() -> Element { text( "about" ).into() } # fn nav_bar( _: Screen ) -> Element { text( "nav" ).into() } #[derive(Clone)] enum AppMsg { Nav( Screen ), Home( HomeMsg ), Settings( SettingsMsg ), } struct AppState { current: Screen, home: HomeState, settings: SettingsState, } impl App for AppState { type Message = AppMsg; fn view( &self ) -> Element { let body = match self.current { Screen::Home => home_view( &self.home ).map( AppMsg::Home ), Screen::Settings => settings_view( &self.settings ).map( AppMsg::Settings ), Screen::About => about_view(), }; column() .push( nav_bar( self.current ) ) .push( body ) .into() } fn update( &mut self, msg: AppMsg ) { match msg { AppMsg::Nav( s ) => self.current = s, AppMsg::Home( m ) => home_update( &mut self.home, m ), AppMsg::Settings( m ) => settings_update( &mut self.settings, m ), } } } ``` Each screen owns its `view( state ) -> Element` and `update( state, msg )` functions; the parent wraps the sub-message in the outer enum on the way out and unwraps it on the way in. There is no runtime-level "screen" abstraction in `ltk` because the widget tree is already cheap to rebuild every frame — the dispatch above is just three function calls. In a real project each screen lives in its own file (`src/home.rs`, `src/settings.rs`, `src/about.rs`) declared as `pub mod home;` etc. in `lib.rs`, and the call sites become `home::view( &self.home )` and `home::update( &mut self.home, m )` instead of the flat `home_view` / `home_update` shape used above. The snippet is flat so that `doctest-md` can typecheck the dispatch without needing one file per screen. **See also**: [`docs/architecture.md`](./architecture.md#larger-state-patterns). --- ## Embedding ltk without `ltk::run` A compositor or embedder that already owns the Wayland connection and just wants ltk's layout, rendering and hit-testing. ```rust,no_run # use ltk::{ button, Color, Element, Rect }; # use ltk::core::{ Canvas, RenderOptions, UiSurface }; # #[ derive( Clone ) ] enum Msg { Tick } # struct App; # impl App { fn view( &self ) -> Element { button( "x" ).into() } # fn update( &mut self, _: Msg ) {} } # struct Event; # impl Event { fn into_msg( self ) -> Msg { Msg::Tick } } # struct Queue( Vec ); # impl Queue { fn drain( &mut self ) -> std::vec::Drain<'_, Event> { self.0.drain( .. ) } } # fn present_argb8888( _: &[u8] ) {} # fn wl_damage( _: &Rect ) {} # fn _ex( width: u32, height: u32, mut app: App, mut input_queue: Queue, pos_x: f32, pos_y: f32 ) { let mut surface = UiSurface::::new( width, height ); loop { // 1. Drain pending app events from your own input source. for ev in input_queue.drain() { app.update( ev.into_msg() ); } // 2. Build the tree and render. let view = app.view(); let out = surface.render( &view, RenderOptions::full_canvas( width, height ) .background( Color::TRANSPARENT ), ); // 3. Pull pixels (software backend) or present the FBO (GLES). match surface.canvas() { Canvas::Software( _ ) => { let mut buf = vec![ 0u8; ( width * height * 4 ) as usize ]; surface.canvas().write_to_wayland_buf( &mut buf, false ); present_argb8888( &buf ); } Canvas::Gles( _ ) => { // Already drawn into the FBO the embedder owns; commit // through your own EGL context. } } // 4. Use damage rects to feed wl_surface.damage_buffer if you are // on the software path. for rect in &out.damage_rects { wl_damage( rect ); } // Pointer dispatch: turn a screen-space point into the widget under it. let hit = surface.hit_test( ltk::Point { x: pos_x, y: pos_y } ); if let Some( idx ) = hit { if let Some( msg ) = surface.handlers( idx ).and_then( |h| h.press_msg() ) { app.update( msg ); } } # break; } # } ``` `UiSurface` keeps the focus / hover / pressed state, the cursor map, the scroll-offset table, and the per-frame widget rects. Set `set_focused( Some( idx ) )` etc. from your own input handler and the next render automatically uses the partial-damage path when only interaction state changed. **See also**: [`tests/core_surface.rs`](../tests/core_surface.rs) for the full set of supported operations, [`docs/onboarding.md`](./onboarding.md#when-to-use-coreuisurface). --- ## Custom CPU drawing and path clipping Painting something the widget set does not cover — a gauge, a `VectorDrawable`, a Lottie frame, a shaped avatar — straight onto the canvas, identically on the GLES and software backends. `External::cpu` gives you a closure invoked once per frame with the `Canvas` and the widget's laid-out rect; `set_clip_path` then clips arbitrary draws to a vector path with an anti-aliased edge. ```rust,no_run # use ltk::{ External, Element, Color, PathCmd }; # #[ derive( Clone ) ] enum Msg {} # fn _ex() -> Element { External::cpu( 96.0, 96.0, |canvas, rect| { // Background, unclipped. canvas.fill_rect( rect, Color::rgb( 0.10, 0.10, 0.12 ), 0.0 ); // Snapshot the outer clip so the path clip does not leak into the // rest of the frame, then clip the foreground to a circle. let saved = canvas.clip_bounds(); let r = rect.width.min( rect.height ) / 2.0; let ( cx, cy ) = ( rect.x + rect.width / 2.0, rect.y + rect.height / 2.0 ); let k = r * 0.5523; canvas.set_clip_path( &[ PathCmd::MoveTo( cx, cy - r ), PathCmd::CubicTo( cx + k, cy - r, cx + r, cy - k, cx + r, cy ), PathCmd::CubicTo( cx + r, cy + k, cx + k, cy + r, cx, cy + r ), PathCmd::CubicTo( cx - k, cy + r, cx - r, cy + k, cx - r, cy ), PathCmd::CubicTo( cx - r, cy - k, cx - k, cy - r, cx, cy - r ), PathCmd::Close, ] ); canvas.fill_rect( rect, Color::rgb( 0.20, 0.50, 0.95 ), 0.0 ); canvas.set_clip_rects( &saved ); } ).into() # } ``` The path clip is bracketed: `set_clip_path` installs it and `set_clip_rects( &saved )` flushes it (on GLES this is when the offscreen layer is composited). `clip_bounds` snapshots the prior clip first so an outer clip is restored rather than dropped. `fill_path` / `stroke_path` take the same `PathCmd` list to paint a path instead of clipping to it. **See also**: [`examples/clip_path.rs`](../examples/clip_path.rs) (rounded rect, circle, triangle — same smooth result on both backends). --- ## Projecting an externally-laid-out view tree Hosting a widget tree whose geometry is computed elsewhere — an Android measure/layout pass, say, which yields one absolute rect per view — projected onto an ltk `stack` in paint order. `measure_text` gives the external layout the same metrics the renderer will use, without a live `Canvas`; `push_placed` drops each child at its exact rect; and `push_placed_clipped` clips a child's overflow like Android's `clipChildren`. ```rust,no_run # use ltk::{ measure_text, stack, text, Element, Rect }; # #[ derive( Clone ) ] enum Msg {} # struct View { label: String, rect: Rect, clip: Option } # fn external_layout_pass() -> Vec { Vec::new() } # fn _ex() -> Element { // The external layout pass measures text with the renderer's own metrics. let ( w, line_h ) = measure_text( "Inbox", 16.0 ); let _ = ( w, line_h ); let views = external_layout_pass(); let mut s = stack(); for v in views { let child = text( v.label ); s = match v.clip { Some( clip ) => s.push_placed_clipped( child, v.rect, clip ), None => s.push_placed( child, v.rect ), }; } s.into() # } ``` To rasterise the result into a caller-owned buffer instead of presenting it, render through a [`core::UiSurface`](#embedding-ltk-without-ltkrun) and call `Canvas::read_rgba_pixels( &mut buf )` — it returns tightly packed straight-alpha RGBA8 (top-left row first) from either backend (the software path un-premultiplies for you). Branch on `Canvas::is_software()` when a draw must honour a real path clip on software but only a bounding rect on GLES.