windows: centralise input focus on a single FocusTarget, raise+focus on every window interaction
Some checks failed
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled

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:
2026-05-29 23:28:48 +02:00
parent 9ca3b60f3a
commit 042652ec73
9 changed files with 407 additions and 2 deletions

View File

@@ -118,6 +118,12 @@ impl Anchor
#[derive( Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord )] #[derive( Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord )]
pub struct OverlayId( pub u32 ); pub struct OverlayId( pub u32 );
/// Stable identifier for a subsurface, used to diff the list returned by
/// [`App::subsurfaces`] between frames the same way [`OverlayId`] diffs
/// overlays.
#[derive( Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord )]
pub struct SubsurfaceId( pub u32 );
/// One of the surfaces an [`App`] can target with an invalidation. Used inside /// One of the surfaces an [`App`] can target with an invalidation. Used inside
/// [`InvalidationScope::Only`] to name the affected surfaces. /// [`InvalidationScope::Only`] to name the affected surfaces.
#[derive( Debug, Clone, Copy, PartialEq, Eq, Hash )] #[derive( Debug, Clone, Copy, PartialEq, Eq, Hash )]
@@ -255,6 +261,42 @@ pub struct OverlaySpec<Message: Clone>
pub anchor_widget_id: Option<crate::types::WidgetId>, pub anchor_widget_id: Option<crate::types::WidgetId>,
} }
/// An input-transparent child surface composited over the main surface and
/// moved by the compositor.
///
/// Returned from [`App::subsurfaces`]. The runtime creates one `wl_subsurface`
/// per active spec, full-size over the main surface, with an **empty input
/// region** — all pointer/touch falls through to the main surface, so the host
/// keeps a single input/gesture model. The win is in the move: the content
/// buffer is rasterised only when [`content_version`](Self::content_version)
/// (or the surface size) changes, while [`x`](Self::x) / [`y`](Self::y)
/// changes only emit `wl_subsurface.set_position` + a parent commit — no
/// re-raster, no buffer re-upload. This makes an animated reveal/slide track
/// the finger at the compositor's compositing cost rather than the client's
/// raster cost.
pub struct SubsurfaceSpec<Message: Clone>
{
/// Stable identifier, used to diff subsurfaces between frames.
pub id: SubsurfaceId,
/// Widget tree for this subsurface. Should paint its own opaque
/// background if it must cover the main surface beneath it.
pub view: Element<Message>,
/// Position of the subsurface relative to the parent's top-left, in
/// layout (physical) pixels — the same space as [`App::on_resize`] and
/// widget rects. The runtime divides by the surface scale to obtain the
/// logical position it hands to the compositor. Animate this for a cheap
/// compositor-side move.
pub x: i32,
pub y: i32,
/// Content revision. Bump it whenever [`view`](Self::view) would paint
/// differently; leave it unchanged for position-only updates so the
/// runtime skips the re-raster and only repositions.
pub content_version: u64,
}
/// Trait that application types must implement to integrate with ltk. /// Trait that application types must implement to integrate with ltk.
pub trait App: 'static pub trait App: 'static
{ {
@@ -295,6 +337,12 @@ pub trait App: 'static
/// IDs that disappear cause the surface to be destroyed. /// IDs that disappear cause the surface to be destroyed.
fn overlays( &self ) -> Vec<OverlaySpec<Self::Message>> { Vec::new() } fn overlays( &self ) -> Vec<OverlaySpec<Self::Message>> { Vec::new() }
/// Describe the input-transparent child surfaces composited over the main
/// surface this frame. Each becomes a `wl_subsurface` the compositor moves
/// by position; see [`SubsurfaceSpec`]. Diffed across frames by
/// [`SubsurfaceSpec::id`]. Default empty.
fn subsurfaces( &self ) -> Vec<SubsurfaceSpec<Self::Message>> { Vec::new() }
/// Return any pending messages from external sources (timers, async, etc.). /// Return any pending messages from external sources (timers, async, etc.).
fn poll_external( &mut self ) -> Vec<Self::Message> { vec![] } fn poll_external( &mut self ) -> Vec<Self::Message> { vec![] }

66
src/chassis.rs Normal file
View File

@@ -0,0 +1,66 @@
//! Scaffolding shared by full-screen ambient surfaces (greeter, lock screen,
//! kiosk): theme bring-up, branding/wallpaper loading and the wallpaper-backed
//! view stack. Thin convenience over [`theme`](crate::theme) and
//! [`WallpaperBundle`](crate::WallpaperBundle) — every app that paints a
//! wallpaper behind centred content repeats this otherwise.
use std::sync::Arc;
use crate::{ Color, Element, ImageData, ThemeMode, WallpaperBundle };
/// Find, install and activate the `default` theme document in `mode`. Returns
/// the failure message instead of exiting; the caller decides how to abort.
pub fn set_default_theme( mode: ThemeMode ) -> Result<(), String>
{
let doc = crate::ThemeDocument::find( "default" ).map_err( |e| format!( "{e}" ) )?;
crate::set_active_document( doc );
crate::set_active_mode( mode );
Ok( () )
}
/// Decode the active theme's horizontal logo to RGBA, `size` px on the longer
/// edge. `None` if the theme ships no logo or the SVG fails to rasterise.
pub fn theme_logo_rgba( size: u32 ) -> Option<ImageData>
{
let path = crate::theme_logo_horizontal()?;
let bytes = std::fs::read( &path ).ok()?;
crate::decode_svg_bytes( &bytes, size )
}
/// Load a symbolic theme icon to RGBA, tinted with `tint`. `None` if missing.
pub fn theme_icon_tinted( name: &str, size: u32, tint: Color ) -> Option<ImageData>
{
let ( rgba, w, h ) = crate::theme_icon_rgba( name, size )?;
Some( ( Arc::new( crate::tint_symbolic( &rgba, tint ) ), w, h ) )
}
/// The active theme's branding image `name` (e.g. `"wallpaper"`,
/// `"lockscreen"`) as a [`WallpaperBundle`], falling back to a solid fill of
/// the palette background when the theme ships none.
pub fn branding_bundle_or_solid( name: &str ) -> WallpaperBundle
{
let path = crate::theme_branding_image( name, 0, 0 );
let bg = crate::theme_palette().bg;
WallpaperBundle::from_path_or_solid(
path.as_deref(),
( bg.r * 255.0 ) as u8,
( bg.g * 255.0 ) as u8,
( bg.b * 255.0 ) as u8,
)
}
/// Convenience for [`branding_bundle_or_solid`] with `"wallpaper"`.
pub fn wallpaper_bundle_or_solid() -> WallpaperBundle
{
branding_bundle_or_solid( "wallpaper" )
}
/// Stack `content` over `wallpaper` resolved for the given surface size.
pub fn backdrop<M: Clone>( content: Element<M>, wallpaper: &WallpaperBundle, w: u32, h: u32 ) -> Element<M>
{
let ( rgba, iw, ih ) = wallpaper.for_size( w, h );
crate::stack::<M>()
.push( crate::img_widget( rgba, iw, ih ) )
.push( content )
.into()
}

View File

@@ -10,6 +10,7 @@ use smithay_client_toolkit::
shell::{ wlr_layer::LayerShell, xdg::XdgShell }, shell::{ wlr_layer::LayerShell, xdg::XdgShell },
shm::Shm, shm::Shm,
session_lock::SessionLock, session_lock::SessionLock,
subcompositor::SubcompositorState,
}; };
use smithay_client_toolkit::reexports::client:: use smithay_client_toolkit::reexports::client::
{ {
@@ -30,11 +31,12 @@ use wayland_protocols::wp::text_input::zv3::client::
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use crate::app::{ App, OverlayId }; use crate::app::{ App, OverlayId, SubsurfaceId };
use crate::egl_context::EglContext; use crate::egl_context::EglContext;
use crate::types::Point; use crate::types::Point;
use super::repeat::{ ButtonRepeatState, KeyRepeatState }; use super::repeat::{ ButtonRepeatState, KeyRepeatState };
use super::subsurface::SubsurfaceSlot;
use super::surface::{ SurfaceFocus, SurfaceState }; use super::surface::{ SurfaceFocus, SurfaceState };
use super::tooltip::{ TooltipPending, TooltipVisible }; use super::tooltip::{ TooltipPending, TooltipVisible };
@@ -45,6 +47,9 @@ pub struct AppData<A: App>
pub seat_state: SeatState, pub seat_state: SeatState,
pub output_state: OutputState, pub output_state: OutputState,
pub compositor_state: CompositorState, pub compositor_state: CompositorState,
/// `wl_subcompositor` binding for [`crate::app::App::subsurfaces`].
/// `None` when the compositor does not advertise the protocol.
pub subcompositor: Option<SubcompositorState>,
pub shm: Shm, pub shm: Shm,
pub session_lock: Option<SessionLock>, pub session_lock: Option<SessionLock>,
/// Process-wide EGL context (display + GLES context). `None` when EGL /// Process-wide EGL context (display + GLES context). `None` when EGL
@@ -213,6 +218,9 @@ pub struct AppData<A: App>
/// Populated and reconciled by the run loop from [`App::overlays`]. Always /// Populated and reconciled by the run loop from [`App::overlays`]. Always
/// empty until overlay diffing is wired up. /// empty until overlay diffing is wired up.
pub overlays: HashMap<OverlayId, SurfaceState<A::Message>>, pub overlays: HashMap<OverlayId, SurfaceState<A::Message>>,
/// Input-transparent child surfaces keyed by their stable
/// [`SubsurfaceId`]. Reconciled each frame from [`App::subsurfaces`].
pub subsurfaces: HashMap<SubsurfaceId, SubsurfaceSlot>,
/// Surface currently receiving pointer events (updated on each pointer /// Surface currently receiving pointer events (updated on each pointer
/// event via its `surface` field). /// event via its `surface` field).
pub pointer_focus: SurfaceFocus, pub pointer_focus: SurfaceFocus,
@@ -332,6 +340,11 @@ impl<A: App> AppData<A>
{ {
self.main.gesture.long_press_fired = true; self.main.gesture.long_press_fired = true;
self.main.gesture.long_press_origin = ss.gesture.long_press_origin; self.main.gesture.long_press_origin = ss.gesture.long_press_origin;
// Keep the primary touch slot with the migrated drag so motion / release still route through the gesture machine.
if self.main.primary_touch_id.is_none()
{
self.main.primary_touch_id = ss.primary_touch_id;
}
} }
} }
if let SurfaceFocus::Overlay( fid ) = self.pointer_focus if let SurfaceFocus::Overlay( fid ) = self.pointer_focus

View File

@@ -4,7 +4,7 @@
use smithay_client_toolkit:: use smithay_client_toolkit::
{ {
compositor::CompositorHandler, compositor::CompositorHandler,
delegate_compositor, delegate_foreign_toplevel_list, delegate_layer, delegate_compositor, delegate_subcompositor, delegate_foreign_toplevel_list, delegate_layer,
delegate_output, delegate_registry, delegate_seat, delegate_keyboard, delegate_output, delegate_registry, delegate_seat, delegate_keyboard,
delegate_pointer, delegate_touch, delegate_shm, delegate_xdg_popup, delegate_pointer, delegate_touch, delegate_shm, delegate_xdg_popup,
delegate_xdg_shell, delegate_xdg_window, delegate_session_lock, delegate_xdg_shell, delegate_xdg_window, delegate_session_lock,
@@ -431,6 +431,7 @@ impl<A: App> SessionLockHandler for AppData<A>
} }
delegate_compositor!( @<A: App> AppData<A> ); delegate_compositor!( @<A: App> AppData<A> );
delegate_subcompositor!( @<A: App> AppData<A> );
delegate_output!( @<A: App> AppData<A> ); delegate_output!( @<A: App> AppData<A> );
delegate_shm!( @<A: App> AppData<A> ); delegate_shm!( @<A: App> AppData<A> );
delegate_seat!( @<A: App> AppData<A> ); delegate_seat!( @<A: App> AppData<A> );

View File

@@ -10,6 +10,7 @@ pub( crate ) mod drag;
pub( crate ) mod focus; pub( crate ) mod focus;
mod handlers; mod handlers;
pub( crate ) mod repeat; pub( crate ) mod repeat;
pub( crate ) mod subsurface;
pub( crate ) mod surface; pub( crate ) mod surface;
pub( crate ) mod text_editing; pub( crate ) mod text_editing;
pub( crate ) mod tooltip; pub( crate ) mod tooltip;

View File

@@ -56,6 +56,11 @@ pub( super ) fn reconcile_overlays<A: App>( data: &mut AppData<A> )
{ {
data.main.gesture.long_press_fired = true; data.main.gesture.long_press_fired = true;
data.main.gesture.long_press_origin = ss.gesture.long_press_origin; data.main.gesture.long_press_origin = ss.gesture.long_press_origin;
// Keep the primary touch slot with the migrated drag so motion / release still route through the gesture machine.
if data.main.primary_touch_id.is_none()
{
data.main.primary_touch_id = ss.primary_touch_id;
}
} }
} }
} }

View File

@@ -18,6 +18,7 @@ use smithay_client_toolkit::
}, },
}, },
shm::Shm, shm::Shm,
subcompositor::SubcompositorState,
}; };
use smithay_client_toolkit::reexports::client::Connection; use smithay_client_toolkit::reexports::client::Connection;
use smithay_client_toolkit::reexports::calloop::EventLoop; 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 ) let shm = Shm::bind( &globals, &qh )
.map_err( |e| RunError::MissingProtocol { name: "wl_shm", detail: format!( "{e:?}" ) } )?; .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, // Try to bring EGL up. On failure (no libEGL, no compatible config,
// LTK_FORCE_SOFTWARE=1, ES2/ES3 context creation refused…) we log the // LTK_FORCE_SOFTWARE=1, ES2/ES3 context creation refused…) we log the
// reason and every surface falls back to the SHM path. // 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 ), seat_state: SeatState::new( &globals, &qh ),
output_state: OutputState::new( &globals, &qh ), output_state: OutputState::new( &globals, &qh ),
compositor_state: compositor, compositor_state: compositor,
subcompositor,
shm, shm,
session_lock, session_lock,
egl_context, egl_context,
@@ -314,6 +320,7 @@ pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
pending_size_hint_unpin, pending_size_hint_unpin,
main: SurfaceState::<A::Message>::new( surface_kind, titlebar_height, titlebar_title ), main: SurfaceState::<A::Message>::new( surface_kind, titlebar_height, titlebar_title ),
overlays: std::collections::HashMap::new(), overlays: std::collections::HashMap::new(),
subsurfaces: std::collections::HashMap::new(),
pointer_focus: SurfaceFocus::Main, pointer_focus: SurfaceFocus::Main,
keyboard_focus: SurfaceFocus::Main, keyboard_focus: SurfaceFocus::Main,
touch_focus: std::collections::HashMap::new(), 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; 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, // Drain inbound AT-SPI2 actions (Orca pressing a button,
// switch-control focusing a node, etc.) and translate each // switch-control focusing a node, etc.) and translate each
// into a synthetic press / focus on the matching widget. // into a synthetic press / focus on the matching widget.

View File

@@ -0,0 +1,253 @@
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
//! Input-transparent child surfaces ([`crate::app::SubsurfaceSpec`]).
//!
//! Each spec maps to one `wl_subsurface` sized to the main surface, with an
//! empty input region so all input falls through to the parent — the host
//! keeps a single gesture/input model. The content buffer is rasterised only
//! when the size or the spec's `content_version` changes; a position change
//! emits `wl_subsurface.set_position` plus a bare parent commit, so an animated
//! slide costs a compositor recomposite, not a client re-raster.
use std::collections::HashMap;
use std::sync::Arc;
use smithay_client_toolkit::compositor::CompositorState;
use smithay_client_toolkit::shm::Shm;
use smithay_client_toolkit::shm::slot::SlotPool;
use smithay_client_toolkit::reexports::client::protocol::wl_shm;
use smithay_client_toolkit::reexports::client::protocol::wl_subsurface::WlSubsurface;
use smithay_client_toolkit::reexports::client::protocol::wl_surface::WlSurface;
use crate::app::App;
use crate::draw::DrawCtx;
use crate::draw::chrome::apply_input_region;
use crate::draw::layout_and_draw;
use crate::render::Canvas;
use crate::types::Rect;
use crate::widget::Element;
use super::frame::pick_shm_format;
use super::AppData;
/// Convert a layout (physical) position to the logical position the
/// compositor expects for `wl_subsurface.set_position`.
fn to_logical( x: i32, y: i32, scale: u32 ) -> ( i32, i32 )
{
let s = scale.max( 1 ) as i32;
( x / s, y / s )
}
/// Live state for one subsurface: its Wayland objects, its own SHM pool and
/// canvas, and the last (size, version, position) we committed so the per-frame
/// pass can tell a re-raster from a cheap reposition.
pub( crate ) struct SubsurfaceSlot
{
subsurface: WlSubsurface,
surface: WlSurface,
pool: Option<SlotPool>,
canvas: Option<Canvas>,
last_pos: ( i32, i32 ),
last_version: u64,
rastered: bool,
}
impl SubsurfaceSlot
{
fn destroy( self )
{
self.subsurface.destroy();
self.surface.destroy();
}
}
/// Reconcile the live subsurfaces against [`App::subsurfaces`], render content
/// where it changed and reposition where it moved. Cheap when nothing but a
/// position changed. Must run after the parent surface is configured.
pub( crate ) fn reconcile_subsurfaces<A: App>( data: &mut AppData<A> )
{
if data.subcompositor.is_none() { return; }
if !data.main.configured { return; }
let parent = match data.main.surface.try_wl_surface()
{
Some( s ) => s.clone(),
None => return,
};
let scale = data.main.scale_factor.max( 1 ) as u32;
let ( w, h ) = ( data.main.width, data.main.height );
if w == 0 || h == 0 { return; }
let pw = w * scale;
let ph = h * scale;
let ( format, swap_rb ) = pick_shm_format( &data.shm );
let specs: Vec<crate::app::SubsurfaceSpec<A::Message>> = data.app.subsurfaces();
let mut parent_dirty = false;
// Drop subsurfaces whose id disappeared from the spec list.
let present: std::collections::HashSet<crate::app::SubsurfaceId> =
specs.iter().map( |s| s.id ).collect();
let stale: Vec<crate::app::SubsurfaceId> = data.subsurfaces.keys()
.copied()
.filter( |id| !present.contains( id ) )
.collect();
for id in stale
{
if let Some( slot ) = data.subsurfaces.remove( &id )
{
slot.destroy();
parent_dirty = true;
}
}
for spec in &specs
{
// Create on first sight.
if !data.subsurfaces.contains_key( &spec.id )
{
let ( subsurface, surface ) = {
let sc = data.subcompositor.as_ref().unwrap();
sc.create_subsurface( parent.clone(), &data.qh )
};
// Independent buffer commits; placement still applies on the
// parent commit we issue below.
subsurface.set_desync();
let ( lx, ly ) = to_logical( spec.x, spec.y, scale );
subsurface.set_position( lx, ly );
data.subsurfaces.insert( spec.id, SubsurfaceSlot {
subsurface,
surface,
pool: None,
canvas: None,
last_pos: ( spec.x, spec.y ),
last_version: u64::MAX,
rastered: false,
} );
parent_dirty = true;
}
let size_changed = data.subsurfaces.get( &spec.id )
.and_then( |s| s.canvas.as_ref() )
.map( |c| c.size() != ( pw, ph ) )
.unwrap_or( true );
let needs_raster = {
let slot = data.subsurfaces.get( &spec.id ).unwrap();
!slot.rastered || size_changed || slot.last_version != spec.content_version
};
if needs_raster
{
let slot = data.subsurfaces.get_mut( &spec.id ).unwrap();
render_slot::<A::Message>(
slot, &data.shm, &data.compositor_state, &spec.view,
pw, ph, scale, format, swap_rb, size_changed,
);
slot.rastered = true;
slot.last_version = spec.content_version;
parent_dirty = true;
}
let slot = data.subsurfaces.get_mut( &spec.id ).unwrap();
if slot.last_pos != ( spec.x, spec.y )
{
let ( lx, ly ) = to_logical( spec.x, spec.y, scale );
slot.subsurface.set_position( lx, ly );
// Desync subsurface state (the position) is applied on the child
// surface's own commit, not the parent's; commit it here so the
// move actually lands. The parent commit below applies placement.
slot.surface.commit();
slot.last_pos = ( spec.x, spec.y );
parent_dirty = true;
}
}
// Applies pending subsurface placement / mapping without re-attaching the
// parent buffer — the cheap per-frame move.
if parent_dirty
{
parent.commit();
}
}
#[ allow( clippy::too_many_arguments ) ]
fn render_slot<Msg: Clone>(
slot: &mut SubsurfaceSlot,
shm: &Shm,
compositor: &CompositorState,
view: &Element<Msg>,
pw: u32,
ph: u32,
scale: u32,
shm_format: wl_shm::Format,
swap_rb: bool,
size_changed: bool,
)
{
if slot.pool.is_none() || size_changed
{
match SlotPool::new( ( pw * ph * 4 ) as usize, shm )
{
Ok( p ) => slot.pool = Some( p ),
Err( _ ) => return,
}
}
let pool = slot.pool.as_mut().unwrap();
let stride = pw * 4;
let ( buffer, canvas_buf ) = match pool.create_buffer(
pw as i32, ph as i32, stride as i32, shm_format,
)
{
Ok( r ) => r,
Err( _ ) => return,
};
let canvas = slot.canvas.get_or_insert_with( || {
let mut c = Canvas::new( pw, ph );
c.set_dpi_scale( scale as f32 );
if let Some( reg ) = crate::theme::build_font_registry()
{
c.set_font_registry( Arc::new( reg ) );
}
c
} );
if canvas.size() != ( pw, ph )
{
canvas.resize( pw, ph );
canvas.set_dpi_scale( scale as f32 );
}
canvas.clear_clip();
canvas.clear();
let screen_rect = Rect { x: 0.0, y: 0.0, width: pw as f32, height: ph as f32 };
let mut ctx: DrawCtx<Msg> = DrawCtx
{
focused_idx: None,
hovered_idx: None,
pressed_idx: None,
cursor_state: HashMap::new(),
selection_anchor: HashMap::new(),
widget_rects: Vec::new(),
debug_layout: false,
scroll_offsets: HashMap::new(),
scroll_rects: Vec::new(),
scroll_canvases: HashMap::new(),
scroll_navigable_items: HashMap::new(),
previous_widget_rects: Vec::new(),
accessible_extras: Vec::new(),
live_depth: 0,
};
layout_and_draw::<Msg>( view, canvas, screen_rect, &mut ctx, 0 );
canvas.write_to_wayland_buf( canvas_buf, swap_rb );
let wl = &slot.surface;
if buffer.attach_to( wl ).is_err() { return; }
wl.set_buffer_scale( scale as i32 );
wl.damage_buffer( 0, 0, pw as i32, ph as i32 );
// Empty input region: pointer/touch fall through to the parent.
apply_input_region( wl, compositor, Some( &[] ), scale );
wl.commit();
}

View File

@@ -193,6 +193,7 @@ pub( crate ) mod layout;
pub( crate ) mod app; pub( crate ) mod app;
pub mod theme; pub mod theme;
pub mod wallpaper; pub mod wallpaper;
pub mod chassis;
pub( crate ) mod tree; pub( crate ) mod tree;
pub( crate ) mod draw; pub( crate ) mod draw;
@@ -206,6 +207,7 @@ pub mod core;
pub use app:: pub use app::
{ {
Anchor, App, ChannelSender, ShellMode, Layer, OverlayId, OverlaySpec, Anchor, App, ChannelSender, ShellMode, Layer, OverlayId, OverlaySpec,
SubsurfaceId, SubsurfaceSpec,
InvalidationScope, SurfaceTarget, ToplevelEvent, InvalidationScope, SurfaceTarget, ToplevelEvent,
}; };
pub use theme:: pub use theme::
@@ -239,6 +241,7 @@ pub use theme::{ decode_svg_bytes, icon_path as theme_icon_path, icon_rgba as th
pub use gles_render::{ BorrowedGlesTexture, GlesVersion }; pub use gles_render::{ BorrowedGlesTexture, GlesVersion };
pub use render::is_software_render; pub use render::is_software_render;
pub use wallpaper::{ WallpaperBundle, ImageData }; pub use wallpaper::{ WallpaperBundle, ImageData };
pub use chassis::{ set_default_theme, theme_logo_rgba, theme_icon_tinted, branding_bundle_or_solid, wallpaper_bundle_or_solid, backdrop };
pub use types::{ Color, Corners, CursorShape, Length, LengthBase, Point, Rect, Size, WidgetId }; pub use types::{ Color, Corners, CursorShape, Length, LengthBase, Point, Rect, Size, WidgetId };
pub use types::{ design_reference, set_design_reference }; pub use types::{ design_reference, set_design_reference };
pub use widget::{ Element, button, icon_button, text_edit, image as img_widget, text, container }; pub use widget::{ Element, button, icon_button, text_edit, image as img_widget, text, container };