# 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

- [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)

---

## 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, OverlayId, OverlaySpec };
# const OVERLAY_QS: OverlayId = OverlayId( 1 );
# const SLIDE_DURATION: f32 = 0.25;
# #[ derive( Clone ) ] enum Msg { CloseQs }
# struct App { qs_started: Option<Instant>, surface_width: u32, surface_height: u32 }
# impl App {
# fn quick_settings_view( &self ) -> Element<Msg> { text( "qs" ).into() }
fn build_quick_settings_overlay( &self ) -> OverlaySpec<Msg>
{
    // 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<Msg> = container( self.quick_settings_view() )
        .surface( "surface-card" )
        .padding( 24.0 )
        .into();

    OverlaySpec
    {
        id:                 OVERLAY_QS,
        layer:              Layer::Overlay,
        anchor:             Anchor::TOP,
        size:               ( self.surface_width, visible_h as u32 ),
        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<ChannelSender<Msg>>,
}

impl App for LoginApp
{
    type Message = Msg;

    fn view( &self ) -> Element<Msg>
    {
        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<Msg> )
    {
        // 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, 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<Msg> { text( "modal" ).into() }
fn overlays( &self ) -> Vec<OverlaySpec<Msg>>
{
    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<Msg> = 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:               ( 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<Msg>
{
    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<Msg>
{
    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<Vec<u8>>, u32, u32 ) {
#     ( Arc::new( vec![ 0; 4 ] ), 1, 1 )
# }
struct LauncherApp
{
    apps:       Vec<DesktopEntry>,
    icon_cache: RefCell<HashMap<String, ( Arc<Vec<u8>>, u32, u32 )>>,
}

impl App for LauncherApp
{
    type Message = Msg;

    fn view( &self ) -> Element<Msg>
    {
        let mut grid = grid::<Msg>( 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<HashMap<…>>` cache is single-threaded — the runtime is
single-threaded, so `Mutex` would only add lock overhead. Decoded
icons are kept as `Arc<Vec<u8>>` 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<ChannelSender<Msg>>, toast: Option<OsdToast> }
# impl App {
fn set_channel_sender( &mut self, sender: ChannelSender<Msg> )
{
    // 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<Msg>
{
    // 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<Duration>
{
    // 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, OverlayId, OverlaySpec };
# const OVERLAY_TOAST: OverlayId = OverlayId( 3 );
# #[ derive( Clone ) ] enum Msg {}
struct AppState
{
    toast: Option<Toast>,
    // ...
}

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<Msg> { text( "main" ).into() }
    // ...
}

impl App for AppState
{
    type Message = Msg;

    fn view( &self ) -> Element<Msg> { self.main_view() }

    fn overlays( &self ) -> Vec<OverlaySpec<Msg>>
    {
        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:               ( 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<Msg>
    {
        // 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<WidgetId>,
    // ...
}

impl App for LoginApp
{
    type Message = Msg;

    fn view( &self ) -> Element<Msg>
    {
        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<WidgetId>
    {
        // 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<HomeMsg>     { text( "home"     ).into() }
# fn home_update( _: &mut HomeState, _: HomeMsg ) {}
# fn settings_view( _: &SettingsState ) -> Element<SettingsMsg> { text( "settings" ).into() }
# fn settings_update( _: &mut SettingsState, _: SettingsMsg ) {}
# fn about_view()                       -> Element<AppMsg>      { text( "about"    ).into() }
# fn nav_bar( _: Screen )               -> Element<AppMsg>      { 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<AppMsg>
    {
        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<SubMsg>` 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<Msg> { button( "x" ).into() }
#            fn update( &mut self, _: Msg ) {} }
# struct Event;
# impl Event { fn into_msg( self ) -> Msg { Msg::Tick } }
# struct Queue( Vec<Event> );
# 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::<Msg>::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).
