// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. use smithay_client_toolkit:: { compositor::CompositorState, output::OutputState, registry::RegistryState, seat::SeatState, shell:: { WaylandSurface, wlr_layer::LayerShell, xdg:: { XdgShell, window::WindowDecorations, }, }, shm::Shm, subcompositor::SubcompositorState, }; use smithay_client_toolkit::reexports::client::Connection; use smithay_client_toolkit::reexports::calloop::EventLoop; use smithay_client_toolkit::reexports::calloop_wayland_source::WaylandSource; use calloop::timer::{ Timer, TimeoutAction }; use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3; use crate::app::{ App, InvalidationScope }; use crate::types::Point; use super::{ AppData, LayerConfig, SurfaceFocus, SurfaceKind, SurfaceState }; use super::error::RunError; use super::frame::draw_frame; use super::invalidation::apply_invalidation; use super::overlays_reconcile::reconcile_overlays; /// Run the application, panicking on init failure. Thin wrapper over /// [`try_run`] kept for backwards-compatibility — embedders that need /// to recover from a missing compositor or a stripped-down driver /// should call [`try_run`] instead. pub( crate ) fn run( app: A ) { if let Err( e ) = try_run( app ) { panic!( "ltk::run failed during init: {e}" ); } } /// Run the application, returning a typed error on init failure. /// The dispatch loop's runtime errors still panic — they are non- /// recoverable once the surface is on screen, and the surface state /// machine cannot be unwound cleanly from this entry point. pub( crate ) fn try_run( app: A ) -> Result<(), RunError> { let conn = Connection::connect_to_env() .map_err( |e| RunError::NoWaylandConnection( format!( "{e}" ) ) )?; let ( globals, queue ) = smithay_client_toolkit::reexports::client::globals::registry_queue_init( &conn ) .map_err( |e| RunError::RegistryInit( format!( "{e:?}" ) ) )?; let qh = queue.handle(); let mut event_loop: EventLoop> = EventLoop::try_new() .map_err( |e| RunError::EventLoop( format!( "EventLoop::try_new: {e}" ) ) )?; WaylandSource::new( conn.clone(), queue ) .insert( event_loop.handle() ) .map_err( |e| RunError::EventLoop( format!( "WaylandSource::insert: {e:?}" ) ) )?; let compositor = CompositorState::bind( &globals, &qh ) .map_err( |e| RunError::MissingProtocol { name: "wl_compositor", detail: format!( "{e:?}" ) } )?; let shm = Shm::bind( &globals, &qh ) .map_err( |e| RunError::MissingProtocol { name: "wl_shm", detail: format!( "{e:?}" ) } )?; // Optional: compositors lacking wl_subcompositor leave this None and // App::subsurfaces() silently degrades to no subsurfaces. let subcompositor = SubcompositorState::bind( compositor.wl_compositor().clone(), &globals, &qh ).ok(); // Try to bring EGL up. On failure (no libEGL, no compatible config, // LTK_FORCE_SOFTWARE=1, ES2/ES3 context creation refused…) we log the // reason and every surface falls back to the SHM path. let egl_context = match crate::egl_context::EglContext::new( &conn ) { Ok( ctx ) => Some( std::sync::Arc::new( ctx ) ), Err( reason ) => { crate::egl_context::log_software_fallback( &reason ); None } }; crate::render::set_software_render( egl_context.is_none() ); // Bind layer-shell up front. Both the main surface (when requested via // ShellMode::Layer) and every overlay returned by App::overlays() share // this single binding. let layer_shell_opt: Option = LayerShell::bind( &globals, &qh ).ok(); // Backwards compatibility: window_config() overrides shell_mode() let force_window = app.window_config() .map( |( t, id )| ( t.to_string(), id.to_string() ) ); let bind_xdg = |globals: &smithay_client_toolkit::reexports::client::globals::GlobalList, qh: &smithay_client_toolkit::reexports::client::QueueHandle>| -> Result { XdgShell::bind( globals, qh ) .map_err( |e| RunError::MissingProtocol { name: "xdg_wm_base", detail: format!( "{e:?}" ) } ) }; // Skipped when going fullscreen: Mutter rejects the fullscreen // request if a min_size constrains it. let apply_size_hint = |window: &smithay_client_toolkit::shell::xdg::window::Window| { if app.start_fullscreen() { return; } match app.window_size_hint() { Some( ( w, h ) ) => { window.set_min_size( Some( ( w, h ) ) ); window.set_max_size( Some( ( w, h ) ) ); } None => { window.set_min_size( Some( ( 360, 480 ) ) ); } } }; let apply_fullscreen = |window: &smithay_client_toolkit::shell::xdg::window::Window| { if app.start_fullscreen() { window.set_fullscreen( None ); } }; let ( surface_kind, xdg_shell ) = if let Some( ( ref title, ref app_id ) ) = force_window { let xdg = bind_xdg( &globals, &qh )?; let surface = compositor.create_surface( &qh ); let window = xdg.create_window( surface, WindowDecorations::RequestServer, &qh ); window.set_title( title.as_str() ); window.set_app_id( app_id.as_str() ); apply_size_hint( &window ); apply_fullscreen( &window ); window.commit(); ( SurfaceKind::Window( window ), Some( xdg ) ) } else { // Use shell_mode() to determine surface type use crate::app::ShellMode; match app.shell_mode() { ShellMode::SessionLock => { // Lock surface is created in SessionLockHandler::locked once the // compositor grants the lock; until then a placeholder. ( SurfaceKind::PendingLock, None ) } ShellMode::Window => { let xdg = bind_xdg( &globals, &qh )?; let surface = compositor.create_surface( &qh ); let window = xdg.create_window( surface, WindowDecorations::RequestServer, &qh ); window.set_title( "ltk" ); window.set_app_id( "ltk" ); apply_size_hint( &window ); window.commit(); ( SurfaceKind::Window( window ), Some( xdg ) ) } ShellMode::Layer( layer ) => { if layer_shell_opt.is_some() { // Defer surface creation until new_output fires: if we create the layer // surface before the compositor has any output ready (e.g. sway startup), // the compositor cannot assign it and logs an error. let cfg = LayerConfig { layer: layer.to_wlr_layer(), exclusive_zone: app.exclusive_zone(), anchor: app.layer_anchor(), size: app.layer_size(), keyboard_exclusive: app.keyboard_exclusive(), namespace: "ltk-sctk", }; ( SurfaceKind::Pending( cfg ), None ) } else { eprintln!( "ltk: wlr-layer-shell not available, falling back to xdg window" ); let xdg = bind_xdg( &globals, &qh )?; let surface = compositor.create_surface( &qh ); let window = xdg.create_window( surface, WindowDecorations::RequestServer, &qh ); window.set_title( "ltk" ); window.set_app_id( "ltk" ); apply_size_hint( &window ); window.commit(); ( SurfaceKind::Window( window ), Some( xdg ) ) } } } }; // Bind the session-lock manager and request the lock. We don't keep the // `SessionLockState` (the manager) around: it has no `Drop`, so the // manager object persists in the connection, and the lock lifecycle runs // entirely off the returned `SessionLock` + its surfaces. let session_lock = if force_window.is_none() && matches!( app.shell_mode(), crate::app::ShellMode::SessionLock ) { smithay_client_toolkit::session_lock::SessionLockState::new( &globals, &qh ) .lock( &qh ) .ok() } else { None }; let text_input_manager: Option = globals.bind( &qh, 1..=1, () ).ok(); // wl_data_device_manager: optional. Required for cross-application // copy/paste; absence means the clipboard stays process-local. let data_device_manager = smithay_client_toolkit::data_device_manager::DataDeviceManagerState::bind( &globals, &qh ).ok(); let ( clipboard_inbox_tx, clipboard_inbox_rx ) = super::data_device::clipboard_inbox(); let ( drop_inbox_tx, drop_inbox_rx ) = super::data_device::drop_inbox(); // AT-SPI2 / accessibility. `try_new` returns `None` when the // platform adapter cannot be created (no daemon on the bus, // headless CI, etc.) — the runtime then runs with no // accessibility tree, which is the previous behaviour. let a11y_app_name = "ltk-app"; let a11y_app_id = "net.liberux.ltk"; let a11y = crate::a11y::A11yState::try_new( a11y_app_name, a11y_app_id ); // xdg-activation-v1: optional. Compositors that don't carry the // global leave `activation_state` as `None` and the inbound / // outbound activation paths silently degrade to no-ops. let activation_state = smithay_client_toolkit::activation::ActivationState::bind( &globals, &qh ).ok(); // `$XDG_ACTIVATION_TOKEN` is the cross-desktop convention for an // incoming activation token (the launcher that spawned us set it // in the new process's environment). Honoured exactly once, after // the main surface receives its first configure — that is the // earliest moment we can call `xdg_activation_v1.activate`. let activation_token_pending = std::env::var( "XDG_ACTIVATION_TOKEN" ).ok().filter( |t| !t.is_empty() ); // Avoid the token leaking into any child process the app spawns — // it is single-use and would be invalid the second time anyway. if activation_token_pending.is_some() { // SAFETY: removing an env var is sound only when no other thread // is reading the environment concurrently. We are still in the // init phase before `set_channel_sender`, so the app has had // no opportunity to spawn worker threads yet. unsafe { std::env::remove_var( "XDG_ACTIVATION_TOKEN" ); } } // `ext-foreign-toplevel-list-v1`. SCTK's `ForeignToplevelList` handles the // bind + dispatch routing; absence of the global is fine, the inner // `GlobalProxy` then yields no toplevels and `App::on_toplevel_event` never // fires. The list is built unconditionally so apps that override the // callback always see a consistent stream when the compositor does carry // the protocol. let foreign_toplevel_list = smithay_client_toolkit::foreign_toplevel_list::ForeignToplevelList::new( &globals, &qh ); let debug_layout = std::env::var( "LTK_DEBUG_LAYOUT" ).is_ok(); let titlebar_height = if force_window.is_some() { 36.0 } else { 0.0 }; let titlebar_title = force_window.map( |( t, _ )| t ).unwrap_or_default(); let pending_fullscreen = app.start_fullscreen(); let pending_size_hint_unpin = !app.start_fullscreen() && app.window_size_hint().is_some(); let mut data = AppData { app, registry_state: RegistryState::new( &globals ), seat_state: SeatState::new( &globals, &qh ), output_state: OutputState::new( &globals, &qh ), compositor_state: compositor, subcompositor, shm, session_lock, egl_context, xdg_shell, layer_shell: layer_shell_opt, keyboard: None, pointer: None, touch: None, pointer_pos: Point::default(), cursor_shape_manager: smithay_client_toolkit::seat::pointer::cursor_shape::CursorShapeManager::bind( &globals, &qh ).ok(), cursor_shape_device: None, last_pointer_enter_serial: 0, current_cursor_shape: None, text_input_manager, text_input: None, text_input_secure: false, activation_state, activation_token_pending, data_device_manager, data_device: None, clipboard_source: None, clipboard_inbox_tx, clipboard_inbox_rx, drop_position: None, drop_mime: None, drop_inbox_tx, drop_inbox_rx, a11y, foreign_toplevel_list, shift_pressed: false, ctrl_pressed: false, loop_handle: event_loop.handle(), compositor_repeat_rate: 0, compositor_repeat_delay: 0, key_repeat: None, button_repeat: None, clipboard: String::new(), last_press_time: None, last_press_pos: None, debug_layout, pending_msgs: Vec::new(), pending_drag_inits: Vec::new(), tooltip_pending: None, tooltip_visible: None, qh: qh.clone(), last_pointer_serial: 0, last_input_serial: 0, exit_requested: false, pending_fullscreen, pending_size_hint_unpin, main: SurfaceState::::new( surface_kind, titlebar_height, titlebar_title ), overlays: std::collections::HashMap::new(), subsurfaces: std::collections::HashMap::new(), subsurface_gles_canvas: None, pointer_focus: SurfaceFocus::Main, keyboard_focus: SurfaceFocus::Main, touch_focus: std::collections::HashMap::new(), cached_view: None, cached_overlays: None, view_dirty: true, overlays_dirty: true, first_frame_committed: false, focus_retry: None, }; // Register a calloop channel so the app can send messages from any thread. // Messages sent through the Sender wake the event loop immediately. { let ( sender, channel ) = calloop::channel::channel::(); event_loop.handle() .insert_source( channel, |event, _, data: &mut AppData| { if let calloop::channel::Event::Msg( msg ) = event { // Just queue the message — the run loop will run // `App::invalidate_after` and apply the resulting // scope, which is what decides which surfaces (if // any) actually need to redraw. data.pending_msgs.push( msg ); } }, ) .map_err( |e| RunError::EventLoop( format!( "channel insert_source: {e:?}" ) ) )?; data.app.set_channel_sender( sender ); } // Register a periodic timer if the app wants one (e.g. clock tick every second). // The timer fires independently of Wayland events, waking the event loop on schedule. if let Some( dur ) = data.app.poll_interval() { event_loop.handle() .insert_source( Timer::from_duration( dur ), |_, _, data: &mut AppData| { let msgs = data.app.poll_external(); data.pending_msgs.extend( msgs ); let next = data.app.poll_interval() .unwrap_or( std::time::Duration::from_secs( 1 ) ); TimeoutAction::ToDuration( next ) }, ) .map_err( |e| RunError::EventLoop( format!( "poll timer insert_source: {e:?}" ) ) )?; } while !data.exit_requested { // Sleep until something interesting fires: // * Wayland event (input, configure, frame callback, …) // * calloop timer (poll_external) // * calloop channel (App::set_channel_sender) // Pacing is driven by `wl_surface.frame` callbacks, so an idle / // off-screen / VRR display blocks indefinitely instead of polling // at a fixed rate. The one exception is a pending long-press: // cap the wait at its deadline so a perfectly still press still // fires on time. let timeout = match ( data.next_long_press_wakeup(), data.next_tooltip_wakeup() ) { ( Some( a ), Some( b ) ) => Some( a.min( b ) ), ( a, b ) => a.or( b ), }; match event_loop.dispatch( timeout, &mut data ) { Ok( () ) => {}, Err( calloop::Error::IoError( ref e ) ) if matches!( e.kind(), std::io::ErrorKind::BrokenPipe | std::io::ErrorKind::ConnectionReset ) => { // On a fatal `wl_display.error` the compositor closes the // socket; the backend has the typed error stashed but the // dispatch result only carries the resulting `BrokenPipe`. if let Some( pe ) = conn.protocol_error() { eprintln!( "ltk: wayland protocol error — interface={} object_id={} code={}: {}", pe.object_interface, pe.object_id, pe.code, pe.message ); } else { eprintln!( "ltk: wayland connection lost ({e}); exiting" ); } data.exit_requested = true; continue; } Err( calloop::Error::OtherError( ref e ) ) => { // wayland-client surfaces the closed-socket condition as an // OtherError wrapping a WaylandError::Io(BrokenPipe) — one // level deeper than a direct io::Error. Walk the source() // chain so we catch it regardless of how many wrapper types // sit between calloop and the raw io::Error. let mut src: Option<&dyn std::error::Error> = Some( e.as_ref() ); let mut is_closed = false; while let Some( err ) = src { if let Some( io ) = err.downcast_ref::() { if matches!( io.kind(), std::io::ErrorKind::BrokenPipe | std::io::ErrorKind::ConnectionReset ) { is_closed = true; break; } } src = err.source(); } if is_closed { eprintln!( "ltk: wayland connection lost; exiting" ); data.exit_requested = true; continue; } panic!( "dispatch: {e:?}" ); } Err( e ) => panic!( "dispatch: {e:?}" ), } // Any surface whose press has now crossed `long_press_duration` // emits its stored message and flips into drag mode for the rest // of the gesture. data.check_long_press_deadlines(); data.check_tooltip_deadline(); // Poll external messages (immediate async results, e.g. PAM auth channel) let ext: Vec<_> = data.app.poll_external(); data.pending_msgs.extend( ext ); // Issue any xdg-activation tokens the app asked for. The tag rides // in `RequestData::app_id` and comes back through // `ActivationHandler::new_token` (handlers.rs) as the token. When the // compositor never advertised the global, answer with an empty token // so the caller still proceeds (falling back to its own matching). for tag in data.app.take_activation_requests() { match data.activation_state { Some( ref activation ) => { let req = smithay_client_toolkit::activation::RequestData { app_id: Some( tag ), seat_and_serial: None, surface: data.main.surface.try_wl_surface().cloned(), }; activation.request_token::>( &data.qh, req ); } None => { if let Some( msg ) = data.app.on_activation_token( tag, String::new() ) { data.pending_msgs.push( msg ); } } } } // Drain any inbound clipboard payloads delivered by the // data-device worker thread. Latest writer wins; the channel // is unbounded but selection payloads are small (capped at // 16 MiB inside the worker, see `data_device::DataDeviceHandler::selection`). while let Ok( text ) = data.clipboard_inbox_rx.try_recv() { data.clipboard = text; } while let Ok( p ) = data.drop_inbox_rx.try_recv() { data.app.on_drop_received( p.x as f32, p.y as f32, &p.mime, &p.text ); } // Process pending messages, folding their per-message invalidation // scopes into a single decision before applying it. let msgs: Vec<_> = data.pending_msgs.drain( .. ).collect(); let had_msgs = !msgs.is_empty(); if had_msgs { let mut scope = InvalidationScope::Only( Vec::new() ); for msg in msgs { scope = scope.union( data.app.invalidate_after( &msg ) ); data.app.update( msg ); } apply_invalidation( &mut data, scope ); // `update()` may have flipped the busy / loading flag // the app reads from inside `cursor_override`. Re-sync // the pointer cursor so the change propagates without // waiting for the next motion event. let pf = data.pointer_focus; data.dispatch_cursor_shape( pf ); } // App asked to exit (e.g. the lockscreen authenticated). For a // session-lock surface, unlock first — exiting without unlocking // leaves the compositor's lock in place by design. if data.app.requested_exit() { if let Some( lock ) = data.session_lock.take() { lock.unlock(); let _ = conn.roundtrip(); } data.exit_requested = true; } // Seed `on_drag_move` with the long-press origin for any drag that // just started. Must run *after* `update()` so the app's drag state // has already been set up by the paired long-press message — the // coords would otherwise hit a dragging_app=None shell and be lost. let drag_inits: Vec<_> = data.pending_drag_inits.drain( .. ).collect(); if !drag_inits.is_empty() { for origin in drag_inits { data.app.on_drag_move( origin.x, origin.y ); } data.view_dirty = true; data.overlays_dirty = true; data.main.request_redraw(); } // After update() the app state is the source of truth — discard any // pending text values so that the next keystroke reads the fresh state // instead of a stale pre-update buffer (e.g. password cleared on auth failure). if !data.main.pending_text_values.is_empty() { data.main.pending_text_values.clear(); } for ss in data.overlays.values_mut() { ss.pending_text_values.clear(); } // Reconcile the overlay set against the app's current specs: drop any // overlays whose id disappeared, create new ones for ids that just // appeared. Specs are re-queried next frame for drawing. reconcile_overlays( &mut data ); // Draw any surface that's configured, dirty, and not already waiting // on a frame callback. The compositor decides our cadence — when no // surface qualifies we just loop back to `dispatch(None)` and sleep. let any_drawable = ( data.main.configured && data.main.needs_redraw && !data.main.frame_pending ) || data.overlays.values().any( |ss| ss.configured && ss.needs_redraw && !ss.frame_pending ); if any_drawable { // Rebuild while motion is in progress and on the first frame // after it ends, so the settle frame paints at full quality // instead of freezing one step short of the target. // `wants_low_quality_paint` is the source of truth so // finger-tracked drags get the same treatment. Slider / // scroll drags are owned by the runtime gesture machine, // not by `App::update`, so OR with the gesture state to // pick up that motion signal too. let runtime_slider_motion = data.main.gesture.dragging_slider.is_some() || data.overlays.values().any( |ss| ss.gesture.dragging_slider.is_some() ); if runtime_slider_motion { data.view_dirty = true; data.overlays_dirty = true; } if data.view_dirty { data.cached_view = Some( data.app.view() ); data.view_dirty = false; } if data.overlays_dirty { let mut specs = data.app.overlays(); if let Some( ts ) = data.tooltip_overlay() { specs.push( ts ); } data.cached_overlays = Some( specs ); data.overlays_dirty = false; } let first_commit_now = draw_frame( &mut data ); if first_commit_now { data.app.on_first_frame_committed(); } // Push the freshly-laid-out widget tree to AccessKit. The // closure only runs when an AT client is actually // listening (the adapter no-ops `update_if_active` // otherwise), so the build cost is paid on demand. Main // surface only — overlays would require per-surface // adapters, which the AT-SPI2 model is not built for. let mut a11y_taken = data.a11y.take(); if let Some( ref mut a ) = a11y_taken { let main_w = data.main.width as f32; let main_h = data.main.height as f32; let mut overlay_meta: Vec<( u8, crate::app::OverlayId, f32, f32 )> = Vec::new(); let mut next_id: u8 = 1; for ( id, ss ) in data.overlays.iter() { if next_id == 0 { break; } let ( ox, oy ) = data.surface_offset_for( super::SurfaceFocus::Overlay( *id ) ); let ( w, h ) = ( ss.width as f32, ss.height as f32 ); if w <= 0.0 || h <= 0.0 || ( ox + w <= 0.0 ) || ( oy + h <= 0.0 ) || ox >= main_w || oy >= main_h { continue; } overlay_meta.push( ( next_id, *id, ox, oy ) ); next_id = next_id.saturating_add( 1 ); } let kb_focus_id = match data.keyboard_focus { super::SurfaceFocus::Main => 0u8, super::SurfaceFocus::Overlay( id ) => overlay_meta.iter().find( |( _, oid, _, _ )| *oid == id ).map( |( i, _, _, _ )| *i ).unwrap_or( 0 ), }; let mut surfaces: Vec> = Vec::new(); surfaces.push( crate::a11y::tree::SurfaceView { focus_id: 0, is_main: true, label: None, widget_rects: &data.main.widget_rects, extras: &data.main.accessible_extras, focused_idx: data.main.focused_idx, pressed_idx: data.main.gesture.pressed_idx, pending_text_values: &data.main.pending_text_values, cursor_state: &data.main.cursor_state, width: main_w, height: main_h, offset_x: 0.0, offset_y: 0.0, } ); for ( fid, id, ox, oy ) in &overlay_meta { let Some( ss ) = data.overlays.get( id ) else { continue }; surfaces.push( crate::a11y::tree::SurfaceView { focus_id: *fid, is_main: false, label: None, widget_rects: &ss.widget_rects, extras: &ss.accessible_extras, focused_idx: ss.focused_idx, pressed_idx: ss.gesture.pressed_idx, pending_text_values: &ss.pending_text_values, cursor_state: &ss.cursor_state, width: ss.width as f32, height: ss.height as f32, offset_x: *ox, offset_y: *oy, } ); } let app_name = "ltk-app"; a.update( || crate::a11y::tree::build_tree( &surfaces, kb_focus_id, app_name ) ); } data.a11y = a11y_taken; } // Reconcile + reposition input-transparent subsurfaces every // iteration, decoupled from the main redraw cadence: a finger drag // emits motion events faster than frame callbacks clear // `frame_pending`, so gating this on a main draw would pin the // subsurface between frames. A position-only change is just // set_position + a bare parent commit; no-ops when nothing moved. super::subsurface::reconcile_subsurfaces( &mut data ); // Drain inbound AT-SPI2 actions (Orca pressing a button, // switch-control focusing a node, etc.) and translate each // into a synthetic press / focus on the matching widget. // Actions for widgets that disappeared between dispatch and // drain are silently dropped — the action loop is best-effort. let a11y_requests: Vec = if let Some( ref mut a ) = data.a11y { a.action_rx.try_iter().collect() } else { Vec::new() }; if !a11y_requests.is_empty() { let qh = data.qh.clone(); let overlay_keys: Vec = data.overlays.keys().copied().collect(); for req in a11y_requests { let Some( r ) = crate::a11y::tree::parse_id( req.target ) else { continue }; if r.kind != 0 { continue; } let idx = r.idx as usize; let focus_target = if r.focus == 0 { SurfaceFocus::Main } else { let oi = ( r.focus as usize ).saturating_sub( 1 ); let Some( id ) = overlay_keys.get( oi ).copied() else { continue }; SurfaceFocus::Overlay( id ) }; let widget = match focus_target { SurfaceFocus::Main => data.main.widget_rects.iter().find( |w| w.flat_idx == idx ).cloned(), SurfaceFocus::Overlay( id ) => data.overlays.get( &id ).and_then( |ss| ss.widget_rects.iter().find( |w| w.flat_idx == idx ).cloned() ), }; match req.action { accesskit::Action::Click => { if let Some( msg ) = widget.as_ref().and_then( |w| w.handlers.press_msg() ) { data.pending_msgs.push( msg ); } } accesskit::Action::Focus => { data.set_focus( focus_target, Some( idx ), &qh ); } accesskit::Action::SetValue => { if let Some( w ) = widget { match ( &w.handlers, req.data ) { ( crate::widget::WidgetHandlers::Slider { .. }, Some( accesskit::ActionData::NumericValue( v ) ) ) => { if let Some( msg ) = w.handlers.slider_change_msg( v.clamp( 0.0, 1.0 ) as f32 ) { data.pending_msgs.push( msg ); } } ( crate::widget::WidgetHandlers::TextEdit { .. }, Some( accesskit::ActionData::Value( s ) ) ) => { if let Some( msg ) = w.handlers.text_change_msg( &s ) { data.pending_msgs.push( msg ); } } _ => {} } } } accesskit::Action::Increment | accesskit::Action::Decrement => { if let Some( w ) = widget { if let crate::widget::WidgetHandlers::Slider { value, .. } = &w.handlers { let step = 0.05f32; let delta = if matches!( req.action, accesskit::Action::Increment ) { step } else { -step }; let v = ( *value + delta ).clamp( 0.0, 1.0 ); if let Some( msg ) = w.handlers.slider_change_msg( v ) { data.pending_msgs.push( msg ); } } } } accesskit::Action::ScrollUp | accesskit::Action::ScrollDown | accesskit::Action::ScrollLeft | accesskit::Action::ScrollRight => { const STEP: f32 = 80.0; let ( dx, dy ) = match req.action { accesskit::Action::ScrollLeft => ( -STEP, 0.0 ), accesskit::Action::ScrollRight => ( STEP, 0.0 ), accesskit::Action::ScrollUp => ( 0.0, -STEP ), accesskit::Action::ScrollDown => ( 0.0, STEP ), _ => ( 0.0, 0.0 ), }; let ss = match focus_target { SurfaceFocus::Main => Some( &mut data.main ), SurfaceFocus::Overlay( id ) => data.overlays.get_mut( &id ), }; if let Some( ss ) = ss { if let Some( ( _, _, _ ) ) = ss.scroll_rects.last().copied() { let scroll_idx = ss.scroll_rects.last().unwrap().1; let entry = ss.scroll_offsets.entry( scroll_idx ).or_insert( ( 0.0, 0.0 ) ); entry.0 = ( entry.0 + dx ).max( 0.0 ); entry.1 = ( entry.1 + dy ).max( 0.0 ); ss.request_redraw(); } } } accesskit::Action::ScrollIntoView => { let Some( w ) = widget else { continue }; let ss = match focus_target { SurfaceFocus::Main => Some( &mut data.main ), SurfaceFocus::Overlay( id ) => data.overlays.get_mut( &id ), }; if let Some( ss ) = ss { let target = w.rect; let probe = crate::types::Point { x: target.x + 1.0, y: target.y + 1.0 }; let container = ss.scroll_rects.iter().rev() .find( |( r, _, _ )| r.contains( probe ) ) .copied(); if let Some( ( r, idx, _ ) ) = container { let entry = ss.scroll_offsets.entry( idx ).or_insert( ( 0.0, 0.0 ) ); if target.y < r.y { entry.1 -= r.y - target.y; } if target.y + target.height > r.y + r.height { entry.1 += ( target.y + target.height ) - ( r.y + r.height ); } if target.x < r.x { entry.0 -= r.x - target.x; } if target.x + target.width > r.x + r.width { entry.0 += ( target.x + target.width ) - ( r.x + r.width ); } entry.0 = entry.0.max( 0.0 ); entry.1 = entry.1.max( 0.0 ); ss.request_redraw(); } } } _ => {} } } } // Focus the widget with the requested WidgetId if the app requests it. // Walks main + every overlay because the targeted widget can live // on any of them (a search field on a launcher overlay, a text // edit on a dialog modal, …). let focus_id = data.app.take_focus_request().or_else( || data.focus_retry.take() ); if let Some( id ) = focus_id { let mut hit: Option<( SurfaceFocus, usize )> = data.main.widget_rects.iter() .find( |w| w.id == Some( id ) ) .map( |w| ( SurfaceFocus::Main, w.flat_idx ) ); if hit.is_none() { for ( ov_id, surf ) in &data.overlays { if let Some( w ) = surf.widget_rects.iter().find( |w| w.id == Some( id ) ) { hit = Some( ( SurfaceFocus::Overlay( *ov_id ), w.flat_idx ) ); break; } } } if let Some( ( focus, flat_idx ) ) = hit { let qh = data.qh.clone(); data.set_focus( focus, Some( flat_idx ), &qh ); match focus { SurfaceFocus::Main => data.main.request_redraw(), SurfaceFocus::Overlay( id ) => { if let Some( s ) = data.overlays.get_mut( &id ) { s.request_redraw(); } } } } else { data.focus_retry = Some( id ); data.view_dirty = true; data.main.request_redraw(); } } } Ok( () ) }