atagen

monolith

public
Star0
Watch0

Commit e9ef7cdfe55a

tarball

atagen <boss@atagen.co>2026-10-16 22:24 UTC

8 files changed+329-32
monolith: support session lock, idle notify and idle inhibit

The locker is told it holds the session only after every output has drawn a frame without the windows, so no private content stays on screen after swaylock reports success. A locker that dies leaves the session locked, which is what the protocol requires. Idle inhibitors count only while their surface is shown, so a video left on a hidden tag does not keep the screen awake.
Modified file: crates/monolith/src/drm.rs+24 additions−13 deletions
@@ -589,6 +589,7 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
589589 cursor,
590590 cursor_status,
591591 pointer_pos,
592+ lock,
592593 ..
593594 } = state
594595 else {
@@ -637,17 +638,20 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
637638 elements.extend(cursor.elements(renderer, cursor_status, local));
638639 }
639640 let scene_began = std::time::Instant::now();
640- elements.extend(crate::scene::build(
641- layout,
642- surface.id,
643- windows,
644- anim,
645- renderer,
646- now,
647- &config.decoration,
648- borders,
649- Some(&surface.output),
650- ));
641+ match crate::lock::elements(lock, renderer, &surface.output) {
642+ Some(locked) => elements.extend(locked),
643+ None => elements.extend(crate::scene::build(
644+ layout,
645+ surface.id,
646+ windows,
647+ anim,
648+ renderer,
649+ now,
650+ &config.decoration,
651+ borders,
652+ Some(&surface.output),
653+ )),
654+ }
651655 let scene_ns = scene_began.elapsed().as_nanos() as u64;
652656 let animating = anim.active(now);
653657 anim.prune(now);
@@ -661,12 +665,15 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
661665 Ok(result) if !result.is_empty => {
662666 let queue_began = std::time::Instant::now();
663667 match surface.drm.queue_frame(()) {
664- Ok(()) => surface.queued = true,
668+ Ok(()) => {
669+ surface.queued = true;
670+ lock.shown(&surface.output);
671+ }
665672 Err(e) => tracing::warn!("queueing a frame failed: {e}"),
666673 }
667674 queue_ns = queue_began.elapsed().as_nanos() as u64;
668675 }
669- Ok(_) => {}
676+ Ok(_) => lock.shown(&surface.output),
670677 Err(e) => tracing::warn!("rendering failed: {e}"),
671678 }
672679 let render_cpu_ns = thread_cpu_ns() - cpu_began;
@@ -690,6 +697,10 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
690697 surface.redraw = animating;
691698 let elapsed = start.elapsed();
692699 let output = surface.output.clone();
700+ if lock.active() {
701+ crate::lock::send_frames(lock, &output, elapsed);
702+ return animating && !surface.queued;
703+ }
693704 for window in space.elements_for_output(&output) {
694705 window.send_frame(&output, elapsed, Some(Duration::ZERO), |_, _| {
695706 Some(output.clone())
Added file: crates/monolith/src/idle.rs+54 additions
@@ -0,0 +1,54 @@
1+//! ext-idle-notify and idle-inhibit, so lockers and screen blankers know
2+//! when the user went away and video players can keep the screen on.
3+
4+use crate::state::State;
5+use smithay::reexports::wayland_server::Resource;
6+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
7+use smithay::wayland::compositor::get_parent;
8+use smithay::wayland::idle_inhibit::IdleInhibitHandler;
9+use smithay::wayland::idle_notify::{IdleNotifierHandler, IdleNotifierState};
10+
11+impl IdleNotifierHandler for State {
12+ fn idle_notifier_state(&mut self) -> &mut IdleNotifierState<Self> {
13+ &mut self.idle_notifier
14+ }
15+}
16+
17+impl IdleInhibitHandler for State {
18+ fn inhibit(&mut self, surface: WlSurface) {
19+ self.inhibitors.insert(surface);
20+ self.update_inhibit();
21+ }
22+
23+ fn uninhibit(&mut self, surface: WlSurface) {
24+ self.inhibitors.remove(&surface);
25+ self.update_inhibit();
26+ }
27+}
28+
29+impl State {
30+ pub fn activity(&mut self) {
31+ let seat = self.seat.clone();
32+ self.idle_notifier.notify_activity(&seat);
33+ }
34+
35+ /// An inhibitor only counts while its surface is on screen, as the
36+ /// protocol asks, so a video on a hidden tag lets the screen blank.
37+ pub fn update_inhibit(&mut self) {
38+ self.inhibitors.retain(|s| s.is_alive());
39+ let shown = self.inhibitors.iter().any(|s| {
40+ let mut root = s.clone();
41+ while let Some(parent) = get_parent(&root) {
42+ root = parent;
43+ }
44+ self.space
45+ .elements()
46+ .any(|w| w.toplevel().is_some_and(|t| t.wl_surface() == &root))
47+ || self.layer_maps_contain(&root)
48+ });
49+ if shown != self.inhibited {
50+ self.inhibited = shown;
51+ self.idle_notifier.set_is_inhibited(shown);
52+ }
53+ }
54+}
Modified file: crates/monolith/src/input.rs+8 additions−1 deletions
@@ -14,6 +14,7 @@ use smithay::wayland::shell::wlr_layer::Layer;
1414 impl State {
1515 pub fn pointer_to(&mut self, pos: Point<f64, Logical>, time: InputTime) {
1616 self.pointer_pos = pos;
17+ self.activity();
1718 if drag_to(self, pos) {
1819 return;
1920 }
@@ -33,10 +34,11 @@ impl State {
3334 }
3435
3536 pub fn pointer_button(&mut self, button: u32, pressed: ButtonState, time: InputTime) {
37+ self.activity();
3638 if pressed == ButtonState::Released && self.drag.take().is_some() {
3739 return;
3840 }
39- if pressed == ButtonState::Pressed {
41+ if pressed == ButtonState::Pressed && !self.lock.active() {
4042 focus_under_pointer(self);
4143 if start_drag(self, button) {
4244 return;
@@ -57,6 +59,7 @@ impl State {
5759 }
5860
5961 pub fn pointer_axis(&mut self, frame: AxisFrame) {
62+ self.activity();
6063 if let Some(pointer) = self.seat.get_pointer() {
6164 pointer.axis(self, frame);
6265 }
@@ -69,6 +72,7 @@ impl State {
6972 }
7073
7174 pub fn key(&mut self, code: Keycode, pressed: KeyState, time: InputTime) {
75+ self.activity();
7276 let Some(keyboard) = self.seat.get_keyboard() else {
7377 return;
7478 };
@@ -96,6 +100,9 @@ impl State {
96100 let vt = (sym - xkb::keysyms::KEY_XF86Switch_VT_1 + 1) as i32;
97101 return FilterResult::Intercept(Some(Action::Vt(vt)));
98102 }
103+ if state.lock.active() {
104+ return FilterResult::Forward;
105+ }
99106 match state.binds.find(mods, &handle.raw_syms()) {
100107 Some(cmd) => {
101108 tracing::debug!(?cmd, "bind");
Modified file: crates/monolith/src/layers.rs+8 additions
@@ -154,6 +154,14 @@ impl State {
154154 self.layer_focus.clone().filter(|s| s.is_alive())
155155 }
156156
157+ pub fn layer_maps_contain(&self, surface: &WlSurface) -> bool {
158+ self.space.outputs().any(|o| {
159+ layer_map_for_output(o)
160+ .layer_for_surface(surface, WindowSurfaceType::TOPLEVEL)
161+ .is_some()
162+ })
163+ }
164+
157165 /// The layer surface under `pos` on `layers`, with its surface origin.
158166 pub fn layer_under(
159167 &self,
Added file: crates/monolith/src/lock.rs+185 additions
@@ -0,0 +1,185 @@
1+//! ext-session-lock. While locked only the locker's surfaces are drawn and
2+//! receive input, and no binds run.
3+
4+use crate::scene::Draw;
5+use crate::state::State;
6+use monolith_render_vk::VulkanRenderer;
7+use smithay::backend::renderer::element::Kind;
8+use smithay::backend::renderer::element::surface::render_elements_from_surface_tree;
9+use smithay::desktop::WindowSurfaceType;
10+use smithay::desktop::utils::{send_frames_surface_tree, under_from_surface_tree};
11+use smithay::output::Output;
12+use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
13+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
14+use smithay::utils::{Logical, Point};
15+use smithay::wayland::session_lock::{
16+ LockSurface, SessionLockHandler, SessionLockManagerState, SessionLocker,
17+};
18+use std::collections::HashSet;
19+use std::time::Duration;
20+
21+#[derive(Default)]
22+pub struct Lock {
23+ pending: Option<SessionLocker>,
24+ locked: bool,
25+ /// Outputs that have shown a locked frame since the lock was requested.
26+ blanked: HashSet<String>,
27+ surfaces: Vec<(Output, LockSurface)>,
28+}
29+
30+impl Lock {
31+ pub fn active(&self) -> bool {
32+ self.locked || self.pending.is_some()
33+ }
34+
35+ fn surface_on(&self, output: &Output) -> Option<&LockSurface> {
36+ self.surfaces
37+ .iter()
38+ .find(|(o, s)| o == output && s.alive())
39+ .map(|(_, s)| s)
40+ }
41+
42+ /// Records a frame shown on `output`, which counts towards confirming a
43+ /// pending lock.
44+ pub fn shown(&mut self, output: &Output) {
45+ if self.pending.is_some() {
46+ self.blanked.insert(output.name());
47+ }
48+ }
49+}
50+
51+impl SessionLockHandler for State {
52+ fn lock_state(&mut self) -> &mut SessionLockManagerState {
53+ &mut self.session_lock_state
54+ }
55+
56+ fn lock(&mut self, confirmation: SessionLocker) {
57+ tracing::info!("session lock requested");
58+ self.lock.pending = Some(confirmation);
59+ self.lock.blanked.clear();
60+ self.drag = None;
61+ self.sync_focus();
62+ self.needs_redraw = true;
63+ }
64+
65+ fn unlock(&mut self) {
66+ tracing::info!("session unlocked");
67+ self.lock = Lock::default();
68+ self.sync_focus();
69+ self.needs_redraw = true;
70+ }
71+
72+ fn new_surface(&mut self, surface: LockSurface, output: WlOutput) {
73+ let Some(output) = Output::from_resource(&output) else {
74+ return;
75+ };
76+ configure(&surface, &output);
77+ self.lock
78+ .surfaces
79+ .retain(|(o, s)| o != &output && s.alive());
80+ self.lock.surfaces.push((output, surface));
81+ self.sync_focus();
82+ self.needs_redraw = true;
83+ }
84+}
85+
86+fn configure(surface: &LockSurface, output: &Output) {
87+ let Some(mode) = output.current_mode() else {
88+ return;
89+ };
90+ let size = (mode.size.w as u32, mode.size.h as u32);
91+ surface.with_pending_state(|s| s.size = Some(size.into()));
92+ surface.send_configure();
93+}
94+
95+impl State {
96+ /// Confirms a pending lock once every output has drawn without the
97+ /// windows, so nothing private is left on screen when the locker is told
98+ /// it holds the session.
99+ pub fn confirm_lock(&mut self) {
100+ if self.lock.pending.is_none() {
101+ return;
102+ }
103+ let all = self
104+ .space
105+ .outputs()
106+ .all(|o| self.lock.blanked.contains(&o.name()));
107+ if all && let Some(locker) = self.lock.pending.take() {
108+ locker.lock();
109+ self.lock.locked = true;
110+ tracing::info!("session locked");
111+ }
112+ }
113+
114+ /// Resends sizes after an output changed mode.
115+ pub fn reconfigure_lock(&self) {
116+ for (output, surface) in &self.lock.surfaces {
117+ configure(surface, output);
118+ }
119+ }
120+
121+ pub fn lock_keyboard_focus(&self) -> Option<WlSurface> {
122+ let under = self.space.output_under(self.pointer_pos).next();
123+ under
124+ .and_then(|o| self.lock.surface_on(o))
125+ .or_else(|| {
126+ self.lock
127+ .surfaces
128+ .iter()
129+ .map(|(_, s)| s)
130+ .find(|s| s.alive())
131+ })
132+ .map(|s| s.wl_surface().clone())
133+ }
134+
135+ pub fn lock_surface_under(
136+ &self,
137+ pos: Point<f64, Logical>,
138+ ) -> Option<(WlSurface, Point<f64, Logical>)> {
139+ let output = self.space.output_under(pos).next()?;
140+ let origin = self.space.output_geometry(output)?.loc;
141+ let surface = self.lock.surface_on(output)?;
142+ under_from_surface_tree(
143+ surface.wl_surface(),
144+ pos - origin.to_f64(),
145+ (0, 0),
146+ WindowSurfaceType::ALL,
147+ )
148+ .map(|(s, p)| (s, (p + origin).to_f64()))
149+ }
150+}
151+
152+/// What a locked output draws, or None when the session is not locked.
153+pub fn elements(lock: &Lock, renderer: &mut VulkanRenderer, output: &Output) -> Option<Vec<Draw>> {
154+ if !lock.active() {
155+ return None;
156+ }
157+ let Some(surface) = lock.surface_on(output) else {
158+ return Some(Vec::new());
159+ };
160+ Some(
161+ render_elements_from_surface_tree(
162+ renderer,
163+ surface.wl_surface(),
164+ (0, 0),
165+ 1.0,
166+ 1.0,
167+ Kind::Unspecified,
168+ )
169+ .into_iter()
170+ .map(Draw::Surface)
171+ .collect(),
172+ )
173+}
174+
175+pub fn send_frames(lock: &Lock, output: &Output, elapsed: Duration) {
176+ if let Some(surface) = lock.surface_on(output) {
177+ send_frames_surface_tree(
178+ surface.wl_surface(),
179+ output,
180+ elapsed,
181+ Some(Duration::ZERO),
182+ |_, _| Some(output.clone()),
183+ );
184+ }
185+}
Modified file: crates/monolith/src/main.rs+2 additions
@@ -3,10 +3,12 @@ mod commands;
33 mod cursor;
44 mod drm;
55 mod handlers;
6+mod idle;
67 mod input;
78 mod ipc;
89 mod launch;
910 mod layers;
11+mod lock;
1012 mod nested;
1113 mod scene;
1214 mod state;
Modified file: crates/monolith/src/nested/mod.rs+17 additions−12 deletions
@@ -179,6 +179,7 @@ pub fn render(state: &mut State) {
179179 borders,
180180 config,
181181 output_id,
182+ lock,
182183 ..
183184 } = state;
184185 let Some(host) = host.as_mut() else { return };
@@ -223,17 +224,19 @@ pub fn render(state: &mut State) {
223224 }
224225 };
225226 let Some(o) = *output_id else { return };
226- let elements = crate::scene::build(
227- layout,
228- o,
229- windows,
230- anim,
231- renderer,
232- now,
233- &config.decoration,
234- borders,
235- Some(&host.output),
236- );
227+ let elements = crate::lock::elements(lock, renderer, &host.output).unwrap_or_else(|| {
228+ crate::scene::build(
229+ layout,
230+ o,
231+ windows,
232+ anim,
233+ renderer,
234+ now,
235+ &config.decoration,
236+ borders,
237+ Some(&host.output),
238+ )
239+ });
237240 match host
238241 .damage
239242 .render_output(renderer, &mut fb, slot.age() as usize, &elements, CLEAR)
@@ -252,8 +255,10 @@ pub fn render(state: &mut State) {
252255 };
253256 *needs_redraw = anim.active(now);
254257 anim.prune(now);
258+ lock.shown(&host.output);
255259 let elapsed = start.elapsed();
256- for window in space.elements() {
260+ crate::lock::send_frames(lock, &host.output, elapsed);
261+ for window in space.elements().filter(|_| !lock.active()) {
257262 window.send_frame(&host.output, elapsed, Some(Duration::ZERO), |_, _| {
258263 Some(host.output.clone())
259264 });
Modified file: crates/monolith/src/state.rs+31 additions−6 deletions
@@ -24,8 +24,11 @@ use smithay::reexports::wayland_server::{Display, DisplayHandle, Resource};
2424 use smithay::utils::{Clock, Logical, Monotonic, Point, SERIAL_COUNTER, Size};
2525 use smithay::wayland::compositor::{CompositorClientState, CompositorState, with_states};
2626 use smithay::wayland::dmabuf::{DmabufGlobal, DmabufState};
27+use smithay::wayland::idle_inhibit::IdleInhibitManagerState;
28+use smithay::wayland::idle_notify::IdleNotifierState;
2729 use smithay::wayland::output::OutputManagerState;
2830 use smithay::wayland::selection::data_device::DataDeviceState;
31+use smithay::wayland::session_lock::SessionLockManagerState;
2932 use smithay::wayland::shell::wlr_layer::{Layer, WlrLayerShellState};
3033 use smithay::wayland::shell::xdg::decoration::XdgDecorationState;
3134 use smithay::wayland::shell::xdg::{XdgShellState, XdgToplevelSurfaceData};
@@ -79,11 +82,17 @@ pub struct State {
7982 pub awaiting: HashSet<WindowId>,
8083 /// A layer surface given the keyboard by a click, like a launcher.
8184 pub layer_focus: Option<WlSurface>,
85+ pub lock: crate::lock::Lock,
86+ pub inhibitors: HashSet<WlSurface>,
87+ pub inhibited: bool,
8288 pub handle: LoopHandle<'static, State>,
8389
8490 pub compositor_state: CompositorState,
8591 pub xdg_shell_state: XdgShellState,
8692 pub layer_shell_state: WlrLayerShellState,
93+ pub session_lock_state: SessionLockManagerState,
94+ pub idle_notifier: IdleNotifierState<State>,
95+ pub _idle_inhibit: IdleInhibitManagerState,
8796 pub _xdg_decoration_state: XdgDecorationState,
8897 pub shm_state: ShmState,
8998 pub dmabuf_state: DmabufState,
@@ -176,10 +185,16 @@ impl State {
176185 last_kind: HashMap::new(),
177186 awaiting: HashSet::new(),
178187 layer_focus: None,
188+ lock: Default::default(),
189+ inhibitors: HashSet::new(),
190+ inhibited: false,
179191 handle: event_loop.handle(),
180192 compositor_state: CompositorState::new::<State>(&dh),
181193 xdg_shell_state: XdgShellState::new::<State>(&dh),
182194 layer_shell_state: WlrLayerShellState::new::<State>(&dh),
195+ session_lock_state: SessionLockManagerState::new::<State, _>(&dh, |_| true),
196+ idle_notifier: IdleNotifierState::new(&dh, event_loop.handle()),
197+ _idle_inhibit: IdleInhibitManagerState::new::<State>(&dh),
183198 _xdg_decoration_state: XdgDecorationState::new::<State>(&dh),
184199 shm_state: ShmState::new::<State>(&dh, vec![]),
185200 dmabuf_state,
@@ -198,6 +213,7 @@ impl State {
198213 self.popups.cleanup();
199214 crate::nested::render(self);
200215 crate::drm::render(self);
216+ self.confirm_lock();
201217 crate::ipc::broadcast(self);
202218 self.publish_workspaces();
203219 let _ = self.display.flush_clients();
@@ -216,6 +232,7 @@ impl State {
216232 .change_current_state(Some(mode), None, None, None);
217233 }
218234 self.reserve_layers();
235+ self.reconfigure_lock();
219236 self.apply_layout();
220237 }
221238 }
@@ -467,6 +484,7 @@ impl State {
467484 self.space.raise_element(&self.windows[id], false);
468485 }
469486 self.sync_focus();
487+ self.update_inhibit();
470488 self.needs_redraw = true;
471489 }
472490
@@ -480,12 +498,16 @@ impl State {
480498 t.send_pending_configure();
481499 }
482500 }
483- let target = self.layer_keyboard_focus().or_else(|| {
484- focused
485- .and_then(|id| self.windows.get(&id))
486- .and_then(|w| w.toplevel())
487- .map(|t| t.wl_surface().clone())
488- });
501+ let target = if self.lock.active() {
502+ self.lock_keyboard_focus()
503+ } else {
504+ self.layer_keyboard_focus().or_else(|| {
505+ focused
506+ .and_then(|id| self.windows.get(&id))
507+ .and_then(|w| w.toplevel())
508+ .map(|t| t.wl_surface().clone())
509+ })
510+ };
489511 if let Some(keyboard) = self.seat.get_keyboard()
490512 && keyboard.current_focus() != target
491513 {
@@ -543,6 +565,9 @@ impl State {
543565 &self,
544566 pos: Point<f64, Logical>,
545567 ) -> Option<(WlSurface, Point<f64, Logical>)> {
568+ if self.lock.active() {
569+ return self.lock_surface_under(pos);
570+ }
546571 let fullscreen = self.layout.outputs().any(|(o, out)| {
547572 let a = out.area;
548573 (a.x..a.right()).contains(&pos.x)