From 3046d86337d28b67e0942d547442aafba45d5175 Mon Sep 17 00:00:00 2001 From: "Pedro M. de Echanove Pasquin" Date: Sat, 15 Aug 2026 17:56:06 +0200 Subject: [PATCH] =?UTF-8?q?Layer::Window=20overlays:=20an=20OverlaySpec=20?= =?UTF-8?q?can=20now=20be=20an=20ordinary=20xdg=20toplevel,=20named=20thro?= =?UTF-8?q?ugh=20App::overlay=5Ftitle;=20showcase=20persists=20the=20last?= =?UTF-8?q?=20pressed=20button=20too=20A=20shell=20built=20on=20ltk=20had?= =?UTF-8?q?=20no=20way=20to=20show=20something=20as=20a=20regular=20window?= =?UTF-8?q?:=20every=20overlay=20is=20a=20layer-shell=20surface,=20and=20l?= =?UTF-8?q?ayer-shell=20surfaces=20are=20either=20below=20all=20applicatio?= =?UTF-8?q?n=20windows=20or=20above=20all=20of=20them.=20crustace's=20"clo?= =?UTF-8?q?sing=20applications=E2=80=A6"=20card=20ran=20into=20exactly=20t?= =?UTF-8?q?hat=20=E2=80=94=20on=20the=20overlay=20layer=20it=20covered=20t?= =?UTF-8?q?he=20applications'=20own=20"save=20changes=3F"=20prompts,=20on?= =?UTF-8?q?=20the=20bottom=20layer=20it=20vanished=20behind=20their=20wind?= =?UTF-8?q?ows=20=E2=80=94=20and=20what=20that=20card=20wants=20is=20to=20?= =?UTF-8?q?be=20one=20window=20among=20the=20others:=20decorated=20by=20th?= =?UTF-8?q?e=20compositor,=20stackable,=20listed=20in=20the=20switcher=20a?= =?UTF-8?q?nd=20the=20dock.=20`Layer`=20gains=20a=20`Window`=20variant.=20?= =?UTF-8?q?An=20`OverlaySpec`=20carrying=20it=20is=20materialised=20by=20t?= =?UTF-8?q?he=20reconciler=20as=20an=20`xdg=5Ftoplevel`=20(sctk=20`Window`?= =?UTF-8?q?,=20server-side=20decorations)=20instead=20of=20a=20layer=20sur?= =?UTF-8?q?face:=20`size`=20is=20its=20fixed=20size,=20applied=20through?= =?UTF-8?q?=20min/max=20size=20(a=20zero=20component=20falls=20back=20to?= =?UTF-8?q?=20a=20default,=20since=20a=20toplevel=20cannot=20"fill");=20an?= =?UTF-8?q?chor,=20exclusive=20zone=20and=20keyboard=20exclusivity=20are?= =?UTF-8?q?=20ignored.=20The=20window's=20app=5Fid=20is=20`App::app=5Fid()?= =?UTF-8?q?`=20and=20its=20title=20comes=20from=20the=20new,=20defaulted?= =?UTF-8?q?=20`App::overlay=5Ftitle(=20id=20)`,=20so=20the=20compositor's?= =?UTF-8?q?=20window=20lists=20show=20something=20meaningful.=20`WindowHan?= =?UTF-8?q?dler::configure`=20and=20`request=5Fclose`=20now=20resolve=20wh?= =?UTF-8?q?ich=20surface=20a=20window=20belongs=20to:=20the=20main=20windo?= =?UTF-8?q?w=20keeps=20its=20behaviour,=20an=20overlay=20window=20is=20con?= =?UTF-8?q?figured=20through=20its=20own=20`SurfaceState`=20(with=20the=20?= =?UTF-8?q?window=20geometry=20set=20to=20the=20configured=20size)=20and?= =?UTF-8?q?=20the=20compositor's=20close=20button=20delivers=20the=20spec'?= =?UTF-8?q?s=20`on=5Fdismiss`=20instead=20of=20exiting=20the=20app.=20Laye?= =?UTF-8?q?r-shell=20applications=20now=20bind=20`xdg=5Fwm=5Fbase`=20when?= =?UTF-8?q?=20the=20compositor=20offers=20it,=20so=20they=20can=20open=20s?= =?UTF-8?q?uch=20a=20window=20later.=20Adding=20a=20variant=20keeps=20ever?= =?UTF-8?q?y=20existing=20`OverlaySpec`=20literal=20compiling;=20the=20new?= =?UTF-8?q?=20trait=20method=20has=20a=20default.=20The=20showcase=20examp?= =?UTF-8?q?le=20now=20also=20saves=20and=20restores=20`last=5Fpressed`,=20?= =?UTF-8?q?bumping=20its=20private=20state=20format=20to=20version=202=20w?= =?UTF-8?q?ith=20the=20note=20kept=20last=20so=20its=20line=20breaks=20sur?= =?UTF-8?q?vive.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + examples/showcase.rs | 8 +++++--- src/app.rs | 13 +++++++++++++ src/event_loop/handlers.rs | 26 +++++++++++++++++++++++--- src/event_loop/overlays_reconcile.rs | 26 ++++++++++++++++++++++++++ src/event_loop/run.rs | 3 +++ 6 files changed, 71 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9a4e22..c3f5e69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to `ltk` are documented here. The format is based on [Keep a - **`xdg-session-management-v1` (client)** — before the first commit of a `ShellMode::Window` toplevel the runtime binds `xdg_session_manager_v1`, issues `get_session( reason, stored_id )` and `restore_toplevel( toplevel, "main" )`, so a supporting compositor restores window geometry on every launch. Bindings are generated in-tree from the vendored XML under `protocols/` with `wayland-scanner` (`src/protocol/`). - **Runtime session persistence** — `$XDG_STATE_HOME//session.json` (compositor session id, clean-exit marker, pid) plus `state.bin` (the bytes from `App::save_state`), written atomically with mode `0600`; saved every 30 s when the bytes changed, on close and on signal; handed back through `App::restore_state` before the first frame only on a session restore (`LTK_SESSION_RESTORE=1`) or after an unclean exit. Module `src/session_state.rs`. +- **`Layer::Window` overlays** — an `OverlaySpec` with `layer: Layer::Window` is materialised as an ordinary `xdg_toplevel` of the given fixed size instead of a layer-shell surface: decorated, stacked and listed by the compositor like an application window. Anchor, exclusive zone and keyboard exclusivity are ignored; the compositor's close button delivers `on_dismiss`. `App::overlay_title( id )` (defaulted to the app id) names it. Layer-shell apps now bind `xdg_wm_base` when available so they can open one. - **Clean exit on `SIGTERM` / `SIGINT`** — the runtime installs a calloop signal source and leaves the event loop instead of dying, so `save_state` runs and `ltk::run` returns. - **`Slider::on_release` / `VSlider::on_release`** — fired once with the final value when the drag ends, so an app can keep an expensive commit (a subprocess, a D-Bus round trip, a compositor reconfigure) off the per-motion `on_change` path and still move the thumb live. The gesture machine emits it from the slider branch of `on_release`; `on_change` alone behaves exactly as before. diff --git a/examples/showcase.rs b/examples/showcase.rs index 3db5504..d264c4e 100644 --- a/examples/showcase.rs +++ b/examples/showcase.rs @@ -94,17 +94,19 @@ impl App for ShowcaseApp fn app_id( &self ) -> &str { "net.liberux.ltk.example.showcase" } + // One field per line, the note last so its own line breaks survive. fn save_state( &self ) -> Option> { - Some( format!( "1\n{}\n{}\n{}", self.tab, self.slider_value, self.note ).into_bytes() ) + Some( format!( "2\n{}\n{}\n{}\n{}", self.tab, self.last_pressed, self.slider_value, self.note ).into_bytes() ) } fn restore_state( &mut self, state: Vec ) { let Ok( text ) = String::from_utf8( state ) else { return }; - let mut lines = text.splitn( 4, '\n' ); - if lines.next() != Some( "1" ) { return; } + let mut lines = text.splitn( 5, '\n' ); + if lines.next() != Some( "2" ) { return; } if let Some( tab ) = lines.next().and_then( |l| l.parse().ok() ) { self.tab = tab; } + if let Some( pressed ) = lines.next() { self.last_pressed = pressed.to_string(); } if let Some( v ) = lines.next().and_then( |l| l.parse().ok() ) { self.slider_value = v; } if let Some( note ) = lines.next() { self.note = note.to_string(); } } diff --git a/src/app.rs b/src/app.rs index 6c6ca32..6bc2103 100644 --- a/src/app.rs +++ b/src/app.rs @@ -48,6 +48,13 @@ pub enum Layer Background, /// Below normal windows but above background. Bottom, + /// Not a layer-shell surface at all: an ordinary `xdg_toplevel` + /// window stacked among the applications, decorated and listed by + /// the compositor like any other. [`OverlaySpec::size`] is its fixed + /// size (`0` falls back to a default, it cannot "fill"); anchor, + /// exclusive zone and keyboard exclusivity are ignored. The + /// compositor's close button delivers [`OverlaySpec::on_dismiss`]. + Window, /// Above normal windows (panels, docks). Top, /// Above everything (notifications, on-screen displays). @@ -66,6 +73,8 @@ impl Layer Layer::Bottom => WlrLayer::Bottom, Layer::Top => WlrLayer::Top, Layer::Overlay => WlrLayer::Overlay, + // Never reaches layer-shell; the reconciler creates a window first. + Layer::Window => WlrLayer::Top, } } } @@ -520,6 +529,10 @@ pub trait App: 'static /// IDs that disappear cause the surface to be destroyed. fn overlays( &self ) -> Vec> { Vec::new() } + /// Title of a [`Layer::Window`] overlay, shown in its decoration and + /// in the compositor's window lists. Default: the [`app_id`](Self::app_id). + fn overlay_title( &self, _id: OverlayId ) -> Option { None } + /// 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 diff --git a/src/event_loop/handlers.rs b/src/event_loop/handlers.rs index f618d08..67d77e3 100644 --- a/src/event_loop/handlers.rs +++ b/src/event_loop/handlers.rs @@ -237,11 +237,19 @@ impl WindowHandler for AppData { fn request_close( &mut self, - _conn: &Connection, - _qh: &QueueHandle, - _window: &Window, + _conn: &Connection, + _qh: &QueueHandle, + window: &Window, ) { + if let Some( super::SurfaceFocus::Overlay( id ) ) = self.focus_for_surface( window.wl_surface() ) + { + if let Some( msg ) = self.overlay_dismiss_msg( id ) + { + self.pending_msgs.push( msg ); + } + return; + } if self.app.on_close_requested() { self.exit_requested = true; @@ -257,6 +265,18 @@ impl WindowHandler for AppData _serial: u32, ) { + if let Some( super::SurfaceFocus::Overlay( id ) ) = self.focus_for_surface( window.wl_surface() ) + { + let Some( ss ) = self.overlays.get_mut( &id ) else { return }; + let sf = ss.scale_factor.max( 1 ) as f32; + let ( rw, rh ) = ss.last_requested_size; + let fallback = ( ( rw as f32 / sf ).round().max( 1.0 ) as u32, ( rh as f32 / sf ).round().max( 1.0 ) as u32 ); + let w = configure.new_size.0.map( |v| v.get() ).unwrap_or( fallback.0 ); + let h = configure.new_size.1.map( |v| v.get() ).unwrap_or( fallback.1 ); + window.xdg_surface().set_window_geometry( 0, 0, w as i32, h as i32 ); + ss.on_configure( &self.shm, self.egl_context.as_ref(), w, h ); + return; + } // Mutter ignores set_fullscreen sent before the surface is // mapped, so reapply on the first configure. if self.pending_fullscreen diff --git a/src/event_loop/overlays_reconcile.rs b/src/event_loop/overlays_reconcile.rs index 1027561..86f717d 100644 --- a/src/event_loop/overlays_reconcile.rs +++ b/src/event_loop/overlays_reconcile.rs @@ -13,6 +13,7 @@ use smithay_client_toolkit:: { XdgPositioner, XdgSurface, popup::Popup, + window::WindowDecorations, }, }, }; @@ -140,6 +141,7 @@ pub( super ) fn reconcile_overlays( data: &mut AppData ) // `anchor_widget_id` → `Rect` without holding a borrow on `data.main` // across the overlay-mut loop below. let main_widget_rects = &data.main.frame.widget_rects; + let app = &data.app; let overlays_m = &mut data.overlays; for spec in &specs { @@ -308,6 +310,30 @@ pub( super ) fn reconcile_overlays( data: &mut AppData ) overlays_m.insert( spec.id, ss ); continue; } + // xdg-toplevel path: a plain window among the applications. + if spec.layer == crate::app::Layer::Window + { + let Some( xdg_shell ) = xdg_shell_opt else + { + eprintln!( "ltk: ignoring window overlay {:?} — xdg_wm_base not available", spec.id ); + continue; + }; + let ( w, h ) = to_logical_size( resolved_size ); + let ( w, h ) = ( if w == 0 { 480 } else { w }, if h == 0 { 320 } else { h } ); + let surface = cs.create_surface( qh ); + let window = xdg_shell.create_window( surface, WindowDecorations::RequestServer, qh ); + let app_id = app.app_id().to_string(); + window.set_title( app.overlay_title( spec.id ).unwrap_or_else( || app_id.clone() ) ); + window.set_app_id( app_id ); + window.set_min_size( Some( ( w, h ) ) ); + window.set_max_size( Some( ( w, h ) ) ); + window.commit(); + let mut ss = SurfaceState::::new( SurfaceKind::Window( window ), 0.0, String::new() ); + ss.scale_factor = parent_scale_i; + ss.last_requested_size = resolved_size; + overlays_m.insert( spec.id, ss ); + continue; + } // wlr-layer-shell path. let Some( layer_shell ) = layer_shell_opt else { diff --git a/src/event_loop/run.rs b/src/event_loop/run.rs index a0133b5..0703716 100644 --- a/src/event_loop/run.rs +++ b/src/event_loop/run.rs @@ -218,6 +218,9 @@ pub( crate ) fn try_run( mut app: A ) -> Result<(), RunError> } } }; + // Layer-shell apps still get xdg_wm_base when the compositor has it, so + // a `Layer::Window` overlay can be created later. + let xdg_shell = xdg_shell.or_else( || XdgShell::bind( &globals, &qh ).ok() ); // Bind the session-lock manager and request the lock. We don't keep the // `SessionLockState` (the manager) around: it has no `Drop`, so the