windows: centralise input focus on a single FocusTarget, raise+focus on every window interaction
Focus was scattered across three stores — `Layouts::focus`, `Layers::focus` and `Windows::activated` — plus a hack that smuggled X11 surfaces through `Layers::focus`, with the keyboard target derived each frame from a `layers.focus.or(layout)` precedence. As a result "focused", "activated" and "has the keyboard" could drift apart: a window could be raised without taking the keyboard, the recurring "click a console window, then click it again before you can actually type" symptom.
New `windows/focus.rs` with `FocusTarget { Layout(Weak<Window>), Layer(WlSurface, app_id), X11(WlSurface) }`. `Windows` now holds one `focus_target: Option<FocusTarget>` as the single source of truth; `activated` survives only as a reconciliation cache for the xdg `Activated` state and is never set by hand. `set_focus` takes one `Option<FocusTarget>` and is the only entry point for changing focus: it brings the target to the front (`z_promote` + desktop stacking), refreshes the MRU and stores the target, and keyboard focus plus activation are applied from it on the next `focus()`. `focus()` resolves a baseline (the active layout's window, which carries `Activated`) and an overlay (a focused layer/X11 surface that takes the keyboard over the baseline without de-activating it), mirroring the old precedence but from one field, so keyboard focus follows focus by construction. `Layers::focus` is removed and X11 no longer rides inside it; the exclusive-keyboard layer path and `reap_layer` drive `focus_target` directly, and every click/touch/activation site collapses to `set_focus(Some(FocusTarget::…))`.
surface_at popup tie-break. When a layout hit is a popup or subsurface, its `wl_surface` is not tracked in `toplevel_z_order`, so the layout-vs-X11 z compare resolved to `None` and the X window underneath wrongly won — clicking a GTK menu (e.g. Firefox's) over an X11 window pressed the X window instead of the menu item. The tie-break now compares the owning toplevel's root z, taken from the hit's `FocusTarget::Layout` weak, so the popup's parent stacking decides.
Raise and focus on every window interaction, matching standard desktop behaviour. Client-driven (un)maximize, the SSD maximize/minimize/close buttons and the maximize keybind now bring the window to the front in both the render z-order and the hit-test stacking through a new `raise_toplevel` helper; previously only the stacking was updated, so an unmaximised window could stay visually behind another. Starting a move or resize — including a press on the resize edge — now routes through `set_focus`, so the window comes to the front and takes the keyboard before the drag instead of being resized or moved while it stays behind.
Input robustness around grabs and drags. A press under an active pointer grab (Wayland popup, drag-and-drop or explicit client grab) is routed to the grab instead of forge's own surface_at handling, so a click cannot leak to a window underneath a grabbing popup and a click outside the popup dismisses it cleanly. Only the press is blocked, never the release, so an in-flight move/resize drag or topbar swipe always reaches its end handler and can never stay glued to the cursor if a grab appears mid-gesture. `on_touch_up` now ends any active move/resize drag before its other early-return paths (armed SSD button, topbar swipe, app switcher) and drops a stale armed button, fixing a race where pressing a second, overlapping window left the drag running forever after release.
fix(input): keep the primary touch slot when migrating a drag across surfaces
A touch drag started on an overlay that hides mid-gesture (e.g. the app launcher when dropping an icon onto the dock or the homescreen) froze: it neither moved nor dropped. Destroying the origin surface migrated the drag state (long_press_fired / long_press_origin) and touch_focus to the main surface, but not its primary_touch_id; subsequent touch motion/up events then fell through the auxiliary path and never reached the gesture machine, leaving on_drag_move and on_drop uncalled.
reconcile_overlays and discard_overlay now adopt the destroyed overlay's primary_touch_id when a drag is in flight, so the rest of the touch sequence keeps driving the main surface. Touch-only; the mouse already migrated correctly via pointer_focus.
ltk: add a chassis module for full-screen ambient surfaces
New `src/chassis.rs`, re-exported from the crate root, gathers the scaffolding that every full-screen ambient surface — greeter, lock screen, kiosk — otherwise repeats by hand over the existing theme and `WallpaperBundle` primitives. `set_default_theme(mode)` finds, installs and activates the `default` theme document, returning the failure message instead of exiting so the caller decides how to abort. `theme_logo_rgba(size)` decodes the active theme's horizontal logo to RGBA, and `theme_icon_tinted(name, size, tint)` loads a symbolic theme icon and tints it. `branding_bundle_or_solid(name)` resolves a theme branding image (`"wallpaper"`, `"lockscreen"`, …) to a `WallpaperBundle`, falling back to a solid fill of the palette background when the theme ships none; `wallpaper_bundle_or_solid()` is the `"wallpaper"` convenience. `backdrop(content, &wallpaper, w, h)` stacks content over the wallpaper resolved for the surface size. No new capability — thin convenience over what the theme module and `WallpaperBundle` already expose — but it removes the per-application `load_theme_logo` / `build_wallpaper_bundle` / theme-bootstrap duplication.
ltk: animatable, input-transparent child surfaces via App::subsurfaces()
New `App::subsurfaces() -> Vec<SubsurfaceSpec<Msg>>` (default empty) describes input-transparent child surfaces composited over the main surface, with `SubsurfaceSpec { id, view, x, y, content_version }` and the stable `SubsurfaceId`. The motivating use is a slide/reveal that tracks a finger without the per-frame full-screen CPU re-raster a single-surface opacity or translate would cost: the content buffer is rasterised once and the compositor moves it.
`SubcompositorState` is bound in `event_loop/run.rs` from the compositor's `wl_compositor` (absent → `App::subsurfaces` silently degrades to none); `delegate_subcompositor!` is added on `AppData`, which gains a `subcompositor` binding and a `subsurfaces` map. New `event_loop/subsurface.rs` reconciles the live subsurfaces against the specs: each spec becomes a `wl_subsurface` sized to the main surface with an empty input region, so all pointer/touch falls through to the parent and the host keeps a single gesture/input model. The content — its own SHM pool and `Canvas`, drawn through the existing `DrawCtx` / `layout_and_draw` path — is rasterised only when the surface size or the spec's `content_version` changes; a position-only change emits `wl_subsurface.set_position` and commits the child surface, then a bare parent commit for placement. Committing the child is deliberate: desync subsurface state (the position) is applied on the child surface's own commit, not the parent's, so without it the move is queued but never lands. Positions are given in layout (physical) pixels and divided by the surface scale for the logical `set_position`.
The reconcile pass runs on every run-loop iteration rather than being gated behind a main-surface redraw, so a finger-driven move repositions at input-event rate, decoupled from the frame-callback cadence that paces full redraws — without this the subsurface only moved when the main surface happened to redraw, so a drag over a static background froze the panel in place.
This commit is contained in:
@@ -18,6 +18,7 @@ use smithay_client_toolkit::
|
||||
},
|
||||
},
|
||||
shm::Shm,
|
||||
subcompositor::SubcompositorState,
|
||||
};
|
||||
use smithay_client_toolkit::reexports::client::Connection;
|
||||
use smithay_client_toolkit::reexports::calloop::EventLoop;
|
||||
@@ -70,6 +71,10 @@ pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
|
||||
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.
|
||||
@@ -261,6 +266,7 @@ pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
|
||||
seat_state: SeatState::new( &globals, &qh ),
|
||||
output_state: OutputState::new( &globals, &qh ),
|
||||
compositor_state: compositor,
|
||||
subcompositor,
|
||||
shm,
|
||||
session_lock,
|
||||
egl_context,
|
||||
@@ -314,6 +320,7 @@ pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
|
||||
pending_size_hint_unpin,
|
||||
main: SurfaceState::<A::Message>::new( surface_kind, titlebar_height, titlebar_title ),
|
||||
overlays: std::collections::HashMap::new(),
|
||||
subsurfaces: std::collections::HashMap::new(),
|
||||
pointer_focus: SurfaceFocus::Main,
|
||||
keyboard_focus: SurfaceFocus::Main,
|
||||
touch_focus: std::collections::HashMap::new(),
|
||||
@@ -638,6 +645,14 @@ pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user