atagen

monolith

public
Star0
Watch0

Commit bc51c38a6fd9

tarball

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

9 files changed+301-12
monolith: support wlr-layer-shell

Exclusive zones are kept as insets on the layout output rather than by shrinking its area, so fullscreen windows still cover the whole output and a resize does not wipe out what bars reserved. The top layer hides under a fullscreen window and the overlay layer does not, matching what bars and notification daemons expect from sway.
Modified file: crates/layout/src/lib.rs+30 additions−1 deletions
@@ -28,6 +28,15 @@ pub enum ExpandScope {
2828 All,
2929 }
3030
31+/// Space taken from an output's edges by bars and other panels.
32+#[derive(Clone, Copy, Debug, PartialEq, Default)]
33+pub struct Insets {
34+ pub top: f64,
35+ pub right: f64,
36+ pub bottom: f64,
37+ pub left: f64,
38+}
39+
3140 #[derive(Clone, Copy, Debug, PartialEq)]
3241 pub struct Gaps {
3342 pub inner: f64,
@@ -65,6 +74,7 @@ pub struct Tag {
6574 pub struct Output {
6675 pub name: String,
6776 pub area: Rect,
77+ pub reserved: Insets,
6878 pub tree: Tree<WindowId>,
6979 /// Viewed tags besides the homeless tag, which is always viewed.
7080 pub viewed: Vec<TagId>,
@@ -216,6 +226,7 @@ impl Layout {
216226 let id = self.outputs.insert(Output {
217227 name: name.into(),
218228 area,
229+ reserved: Insets::default(),
219230 tree: Tree::default(),
220231 viewed: Vec::new(),
221232 floating: Vec::new(),
@@ -331,6 +342,17 @@ impl Layout {
331342 }
332343 }
333344
345+ /// Returns whether anything changed.
346+ pub fn set_reserved(&mut self, o: OutputId, reserved: Insets) -> bool {
347+ match self.outputs.get_mut(o) {
348+ Some(out) if out.reserved != reserved => {
349+ out.reserved = reserved;
350+ true
351+ }
352+ _ => false,
353+ }
354+ }
355+
334356 pub fn is_viewed(&self, o: OutputId, tag: TagId) -> bool {
335357 tag == self.homeless
336358 || self
@@ -821,7 +843,14 @@ impl Layout {
821843 let Some(out) = self.outputs.get(o) else {
822844 return Arrangement::default();
823845 };
824- let area = inset(out.area, self.gaps.outer);
846+ let r = out.reserved;
847+ let usable = Rect::new(
848+ out.area.x + r.left,
849+ out.area.y + r.top,
850+ (out.area.w - r.left - r.right).max(0.0),
851+ (out.area.h - r.top - r.bottom).max(0.0),
852+ );
853+ let area = inset(usable, self.gaps.outer);
825854 let shown = self.vis_on(o);
826855 let vis = &|w: WindowId| shown(w) && !hide.contains(&w);
827856 let tiled = out
Modified file: crates/monolith/src/drm.rs+2 additions
@@ -646,6 +646,7 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
646646 now,
647647 &config.decoration,
648648 borders,
649+ Some(&surface.output),
649650 ));
650651 let scene_ns = scene_began.elapsed().as_nanos() as u64;
651652 let animating = anim.active(now);
@@ -694,6 +695,7 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
694695 Some(output.clone())
695696 });
696697 }
698+ crate::layers::send_frames(&output, elapsed);
697699 animating && !surface.queued
698700 }
699701
Modified file: crates/monolith/src/handlers.rs+1 additions
@@ -94,6 +94,7 @@ impl CompositorHandler for State {
9494 {
9595 let _ = popup.send_configure();
9696 }
97+ self.layer_commit(surface);
9798 self.needs_redraw = true;
9899 }
99100 }
Modified file: crates/monolith/src/input.rs+13 additions
@@ -9,6 +9,7 @@ use smithay::backend::session::Session;
99 use smithay::input::keyboard::{FilterResult, Keycode, xkb};
1010 use smithay::input::pointer::{AxisFrame, ButtonEvent, MotionEvent};
1111 use smithay::utils::{Logical, Point, SERIAL_COUNTER};
12+use smithay::wayland::shell::wlr_layer::Layer;
1213
1314 impl State {
1415 pub fn pointer_to(&mut self, pos: Point<f64, Logical>, time: InputTime) {
@@ -107,6 +108,7 @@ impl State {
107108 );
108109 match action {
109110 Some(Some(Action::Run(cmd))) => {
111+ self.layer_focus = None;
110112 if let Err(e) = self.run(cmd) {
111113 tracing::warn!("bind failed: {e}");
112114 }
@@ -130,6 +132,17 @@ enum Action {
130132
131133 fn focus_under_pointer(state: &mut State) {
132134 let pos = state.pointer_pos;
135+ let layers = [Layer::Overlay, Layer::Top, Layer::Bottom, Layer::Background];
136+ if let Some((layer, surface, _)) = state.layer_under(pos, &layers)
137+ && layer.can_receive_keyboard_focus()
138+ {
139+ state.layer_focus = Some(surface);
140+ state.sync_focus();
141+ return;
142+ }
143+ if state.layer_focus.take().is_some() {
144+ state.sync_focus();
145+ }
133146 let id = state
134147 .space
135148 .element_under(pos)
Added file: crates/monolith/src/layers.rs+195 additions
@@ -0,0 +1,195 @@
1+//! wlr-layer-shell, for bars, launchers, wallpapers and notifications.
2+
3+use crate::scene::Draw;
4+use crate::state::State;
5+use monolith_layout::{Insets, OutputId};
6+use monolith_render_vk::VulkanRenderer;
7+use smithay::backend::renderer::element::AsRenderElements;
8+use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
9+use smithay::desktop::{LayerSurface, PopupKind, WindowSurfaceType, layer_map_for_output};
10+use smithay::output::Output;
11+use smithay::reexports::wayland_server::Resource;
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, Scale};
15+use smithay::wayland::compositor::with_states;
16+use smithay::wayland::shell::wlr_layer::{
17+ KeyboardInteractivity, Layer, LayerSurface as WlrLayerSurface, LayerSurfaceData,
18+ WlrLayerShellHandler, WlrLayerShellState,
19+};
20+use smithay::wayland::shell::xdg::PopupSurface;
21+use std::time::Duration;
22+
23+impl WlrLayerShellHandler for State {
24+ fn shell_state(&mut self) -> &mut WlrLayerShellState {
25+ &mut self.layer_shell_state
26+ }
27+
28+ fn new_layer_surface(
29+ &mut self,
30+ surface: WlrLayerSurface,
31+ output: Option<WlOutput>,
32+ _layer: Layer,
33+ namespace: String,
34+ ) {
35+ let output = output
36+ .as_ref()
37+ .and_then(Output::from_resource)
38+ .or_else(|| self.space.output_under(self.pointer_pos).next().cloned())
39+ .or_else(|| self.space.outputs().next().cloned());
40+ let Some(output) = output else {
41+ surface.send_close();
42+ return;
43+ };
44+ let mut map = layer_map_for_output(&output);
45+ if let Err(e) = map.map_layer(&LayerSurface::new(surface, namespace)) {
46+ tracing::warn!("cannot map layer surface: {e}");
47+ }
48+ }
49+
50+ fn new_popup(&mut self, _parent: WlrLayerSurface, popup: PopupSurface) {
51+ let _ = self.popups.track_popup(PopupKind::Xdg(popup));
52+ }
53+
54+ fn layer_destroyed(&mut self, surface: WlrLayerSurface) {
55+ let outputs: Vec<Output> = self.space.outputs().cloned().collect();
56+ for output in outputs {
57+ let mut map = layer_map_for_output(&output);
58+ let gone = map
59+ .layers()
60+ .find(|l| l.layer_surface() == &surface)
61+ .cloned();
62+ if let Some(layer) = gone {
63+ map.unmap_layer(&layer);
64+ }
65+ }
66+ if self.layer_focus.as_ref() == Some(surface.wl_surface()) {
67+ self.layer_focus = None;
68+ }
69+ self.reserve_layers();
70+ }
71+}
72+
73+impl State {
74+ /// Sends a layer surface its first configure once it has committed its
75+ /// anchors and size, and picks up exclusive zone changes after that.
76+ /// Returns false for surfaces that are not layer surfaces.
77+ pub fn layer_commit(&mut self, surface: &WlSurface) -> bool {
78+ let Some(output) = self
79+ .space
80+ .outputs()
81+ .find(|o| {
82+ layer_map_for_output(o)
83+ .layer_for_surface(surface, WindowSurfaceType::TOPLEVEL)
84+ .is_some()
85+ })
86+ .cloned()
87+ else {
88+ return false;
89+ };
90+ let configured = with_states(surface, |s| {
91+ s.data_map
92+ .get::<LayerSurfaceData>()
93+ .and_then(|d| d.lock().ok())
94+ .is_some_and(|d| d.initial_configure_sent)
95+ });
96+ {
97+ let mut map = layer_map_for_output(&output);
98+ map.arrange();
99+ if !configured
100+ && let Some(layer) = map.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL)
101+ {
102+ layer.layer_surface().send_configure();
103+ }
104+ }
105+ self.reserve_layers();
106+ true
107+ }
108+
109+ /// Keeps tiled windows out of the space bars reserve.
110+ pub fn reserve_layers(&mut self) {
111+ let outputs: Vec<OutputId> = self.layout.outputs().map(|(o, _)| o).collect();
112+ let mut changed = false;
113+ for o in outputs {
114+ let Some(output) = self.smithay_output(o) else {
115+ continue;
116+ };
117+ let Some(area) = self.layout.output(o).map(|out| out.area) else {
118+ continue;
119+ };
120+ let mut map = layer_map_for_output(&output);
121+ map.arrange();
122+ let zone = map.non_exclusive_zone();
123+ let reserved = Insets {
124+ top: f64::from(zone.loc.y),
125+ left: f64::from(zone.loc.x),
126+ bottom: (area.h - f64::from(zone.loc.y + zone.size.h)).max(0.0),
127+ right: (area.w - f64::from(zone.loc.x + zone.size.w)).max(0.0),
128+ };
129+ drop(map);
130+ changed |= self.layout.set_reserved(o, reserved);
131+ }
132+ if changed {
133+ self.apply_layout();
134+ } else {
135+ self.sync_focus();
136+ }
137+ }
138+
139+ /// The layer surface that should hold the keyboard, if any. One asking
140+ /// for exclusive focus above the windows wins over everything.
141+ pub fn layer_keyboard_focus(&self) -> Option<WlSurface> {
142+ for output in self.space.outputs() {
143+ let map = layer_map_for_output(output);
144+ let exclusive = map
145+ .layers_on(Layer::Overlay)
146+ .chain(map.layers_on(Layer::Top))
147+ .find(|l| {
148+ l.cached_state().keyboard_interactivity == KeyboardInteractivity::Exclusive
149+ });
150+ if let Some(l) = exclusive {
151+ return Some(l.wl_surface().clone());
152+ }
153+ }
154+ self.layer_focus.clone().filter(|s| s.is_alive())
155+ }
156+
157+ /// The layer surface under `pos` on `layers`, with its surface origin.
158+ pub fn layer_under(
159+ &self,
160+ pos: Point<f64, Logical>,
161+ layers: &[Layer],
162+ ) -> Option<(LayerSurface, WlSurface, Point<f64, Logical>)> {
163+ let output = self.space.output_under(pos).next()?;
164+ let origin = self.space.output_geometry(output)?.loc;
165+ let local = pos - origin.to_f64();
166+ let map = layer_map_for_output(output);
167+ layers.iter().find_map(|&layer| {
168+ let l = map.layer_under(layer, local)?;
169+ let geo = map.layer_geometry(l)?;
170+ let (s, p) = l.surface_under(local - geo.loc.to_f64(), WindowSurfaceType::ALL)?;
171+ Some((l.clone(), s, (p + geo.loc + origin).to_f64()))
172+ })
173+ }
174+}
175+
176+/// Draws an output's layer surfaces on `layer`, front to back.
177+pub fn elements(out: &mut Vec<Draw>, renderer: &mut VulkanRenderer, output: &Output, layer: Layer) {
178+ let map = layer_map_for_output(output);
179+ for l in map.layers_on(layer).rev() {
180+ let Some(geo) = map.layer_geometry(l) else {
181+ continue;
182+ };
183+ let elems: Vec<WaylandSurfaceRenderElement<VulkanRenderer>> =
184+ l.render_elements(renderer, geo.loc.to_physical(1), Scale::from(1.0), 1.0);
185+ out.extend(elems.into_iter().map(Draw::Surface));
186+ }
187+}
188+
189+pub fn send_frames(output: &Output, elapsed: Duration) {
190+ for l in layer_map_for_output(output).layers() {
191+ l.send_frame(output, elapsed, Some(Duration::ZERO), |_, _| {
192+ Some(output.clone())
193+ });
194+ }
195+}
Modified file: crates/monolith/src/main.rs+1 additions
@@ -6,6 +6,7 @@ mod handlers;
66 mod input;
77 mod ipc;
88 mod launch;
9+mod layers;
910 mod nested;
1011 mod scene;
1112 mod state;
Modified file: crates/monolith/src/nested/mod.rs+2 additions
@@ -232,6 +232,7 @@ pub fn render(state: &mut State) {
232232 now,
233233 &config.decoration,
234234 borders,
235+ Some(&host.output),
235236 );
236237 match host
237238 .damage
@@ -257,6 +258,7 @@ pub fn render(state: &mut State) {
257258 Some(host.output.clone())
258259 });
259260 }
261+ crate::layers::send_frames(&host.output, elapsed);
260262 let Some(damage) = damage else {
261263 if *needs_redraw {
262264 host.surface.frame(&host.qh, ());
Modified file: crates/monolith/src/scene.rs+13 additions
@@ -15,8 +15,10 @@ use smithay::backend::renderer::utils::CommitCounter;
1515 use smithay::backend::renderer::utils::with_renderer_surface_state;
1616 use smithay::backend::renderer::{ContextId, Renderer, Texture};
1717 use smithay::desktop::Window;
18+use smithay::output::Output;
1819 use smithay::utils::user_data::UserDataMap;
1920 use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
21+use smithay::wayland::shell::wlr_layer::Layer;
2022 use std::collections::HashMap;
2123
2224 smithay::backend::renderer::element::render_elements! {
@@ -47,6 +49,7 @@ pub fn build(
4749 now: u64,
4850 deco: &Decoration,
4951 borders: &mut Borders,
52+ output: Option<&Output>,
5053 ) -> Vec<Draw> {
5154 let Some(area) = layout.output(o).map(|out| out.area) else {
5255 return Vec::new();
@@ -87,6 +90,12 @@ pub fn build(
8790 borders.0.retain(|id, _| windows.contains_key(id));
8891 let focused = layout.focused();
8992 let mut out = Vec::new();
93+ if let Some(output) = output {
94+ crate::layers::elements(&mut out, renderer, output, Layer::Overlay);
95+ if !fullscreen_at_rest {
96+ crate::layers::elements(&mut out, renderer, output, Layer::Top);
97+ }
98+ }
9099 for (id, r, framed, gliding) in tiled.into_iter().chain(above).rev() {
91100 let Some(window) = windows.get(&id) else {
92101 continue;
@@ -121,6 +130,10 @@ pub fn build(
121130 anim.ghost_effect(g, now),
122131 )));
123132 }
133+ if let Some(output) = output {
134+ crate::layers::elements(&mut out, renderer, output, Layer::Bottom);
135+ crate::layers::elements(&mut out, renderer, output, Layer::Background);
136+ }
124137 out
125138 }
126139
Modified file: crates/monolith/src/state.rs+44 additions−11 deletions
@@ -26,6 +26,7 @@ use smithay::wayland::compositor::{CompositorClientState, CompositorState, with_
2626 use smithay::wayland::dmabuf::{DmabufGlobal, DmabufState};
2727 use smithay::wayland::output::OutputManagerState;
2828 use smithay::wayland::selection::data_device::DataDeviceState;
29+use smithay::wayland::shell::wlr_layer::{Layer, WlrLayerShellState};
2930 use smithay::wayland::shell::xdg::decoration::XdgDecorationState;
3031 use smithay::wayland::shell::xdg::{XdgShellState, XdgToplevelSurfaceData};
3132 use smithay::wayland::shm::ShmState;
@@ -76,10 +77,13 @@ pub struct State {
7677 /// switches between tiled, floating and fullscreen.
7778 pub last_kind: HashMap<WindowId, (Kind, Rect, OutputId)>,
7879 pub awaiting: HashSet<WindowId>,
80+ /// A layer surface given the keyboard by a click, like a launcher.
81+ pub layer_focus: Option<WlSurface>,
7982 pub handle: LoopHandle<'static, State>,
8083
8184 pub compositor_state: CompositorState,
8285 pub xdg_shell_state: XdgShellState,
86+ pub layer_shell_state: WlrLayerShellState,
8387 pub _xdg_decoration_state: XdgDecorationState,
8488 pub shm_state: ShmState,
8589 pub dmabuf_state: DmabufState,
@@ -171,9 +175,11 @@ impl State {
171175 borders: Default::default(),
172176 last_kind: HashMap::new(),
173177 awaiting: HashSet::new(),
178+ layer_focus: None,
174179 handle: event_loop.handle(),
175180 compositor_state: CompositorState::new::<State>(&dh),
176181 xdg_shell_state: XdgShellState::new::<State>(&dh),
182+ layer_shell_state: WlrLayerShellState::new::<State>(&dh),
177183 _xdg_decoration_state: XdgDecorationState::new::<State>(&dh),
178184 shm_state: ShmState::new::<State>(&dh, vec![]),
179185 dmabuf_state,
@@ -201,6 +207,15 @@ impl State {
201207 if let Some(o) = self.output_id {
202208 let area = Rect::new(0.0, 0.0, f64::from(size.w), f64::from(size.h));
203209 self.layout.set_output_area(o, area);
210+ if let Some(host) = &self.host {
211+ let mode = smithay::output::Mode {
212+ size: (size.w, size.h).into(),
213+ refresh: 60_000,
214+ };
215+ host.output
216+ .change_current_state(Some(mode), None, None, None);
217+ }
218+ self.reserve_layers();
204219 self.apply_layout();
205220 }
206221 }
@@ -455,7 +470,7 @@ impl State {
455470 self.needs_redraw = true;
456471 }
457472
458- fn sync_focus(&mut self) {
473+ pub fn sync_focus(&mut self) {
459474 let focused = self.layout.focused();
460475 for (id, window) in &self.windows {
461476 if window.set_activated(Some(*id) == focused)
@@ -465,10 +480,12 @@ impl State {
465480 t.send_pending_configure();
466481 }
467482 }
468- let target = focused
469- .and_then(|id| self.windows.get(&id))
470- .and_then(|w| w.toplevel())
471- .map(|t| t.wl_surface().clone());
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+ });
472489 if let Some(keyboard) = self.seat.get_keyboard()
473490 && keyboard.current_focus() != target
474491 {
@@ -526,13 +543,29 @@ impl State {
526543 &self,
527544 pos: Point<f64, Logical>,
528545 ) -> Option<(WlSurface, Point<f64, Logical>)> {
529- self.space
530- .element_under(pos)
531- .and_then(|(window, location)| {
532- window
533- .surface_under(pos - location.to_f64(), WindowSurfaceType::ALL)
534- .map(|(s, p)| (s, (p + location).to_f64()))
546+ let fullscreen = self.layout.outputs().any(|(o, out)| {
547+ let a = out.area;
548+ (a.x..a.right()).contains(&pos.x)
549+ && (a.y..a.bottom()).contains(&pos.y)
550+ && self.layout.arrange(o).fullscreen.is_some()
551+ });
552+ let above: &[Layer] = if fullscreen {
553+ &[Layer::Overlay]
554+ } else {
555+ &[Layer::Overlay, Layer::Top]
556+ };
557+ let layer = |layers| self.layer_under(pos, layers).map(|(_, s, p)| (s, p));
558+ layer(above)
559+ .or_else(|| {
560+ self.space
561+ .element_under(pos)
562+ .and_then(|(window, location)| {
563+ window
564+ .surface_under(pos - location.to_f64(), WindowSurfaceType::ALL)
565+ .map(|(s, p)| (s, (p + location).to_f64()))
566+ })
535567 })
568+ .or_else(|| layer(&[Layer::Bottom, Layer::Background]))
536569 }
537570 }