atagen

monolith

public
Star0
Watch0

Commit 17d5669ab74b

tarball

atagen <boss@atagen.co>2026-10-17 01:18 UTC

5 files changed+437-0
monolith: support foreign toplevel list and management

Bars and taskbars need the window list to show titles and the focused window. wlr-foreign-toplevel-management is written here, since smithay only ships the read-only ext list, and it diffs snapshots like ext-workspace. Activating a window on a hidden tag views that tag alongside the current ones rather than switching away from them. Minimize requests are ignored, since tags have no minimized state.
Modified file: crates/monolith/src/main.rs+1 additions
@@ -16,6 +16,7 @@ mod lock;
1616 mod nested;
1717 mod scene;
1818 mod state;
19+mod toplevels;
1920 mod xwayland;
2021
2122 #[cfg(feature = "spans")]
Modified file: crates/monolith/src/state.rs+11 additions
@@ -84,6 +84,9 @@ pub struct State {
8484 pub ipc: crate::ipc::Server,
8585 pub launches: crate::launch::Launches,
8686 pub workspaces: WorkspaceState,
87+ pub foreign_toplevels: monolith_protocols::toplevel::ForeignToplevelState,
88+ pub toplevel_list: smithay::wayland::foreign_toplevel_list::ForeignToplevelListState,
89+ pub list_handles: crate::toplevels::ListHandles,
8790 pub anim: Animator,
8891 pub borders: crate::scene::Borders,
8992 /// How each shown window was placed by the last layout pass, to spot
@@ -208,6 +211,13 @@ impl State {
208211 ipc: crate::ipc::Server::default(),
209212 launches: Default::default(),
210213 workspaces: WorkspaceState::new::<State>(&dh),
214+ foreign_toplevels: monolith_protocols::toplevel::ForeignToplevelState::new::<State>(
215+ &dh,
216+ ),
217+ toplevel_list: smithay::wayland::foreign_toplevel_list::ForeignToplevelListState::new::<
218+ State,
219+ >(&dh),
220+ list_handles: Default::default(),
211221 anim: Animator::new(animation),
212222 borders: Default::default(),
213223 last_kind: HashMap::new(),
@@ -269,6 +279,7 @@ impl State {
269279 self.complete_captures();
270280 crate::ipc::broadcast(self);
271281 self.publish_workspaces();
282+ self.publish_toplevels();
272283 let _ = self.display.flush_clients();
273284 }
274285
Added file: crates/monolith/src/toplevels.rs+136 additions
@@ -0,0 +1,136 @@
1+//! Window lists for other clients: ext-foreign-toplevel-list for listing,
2+//! and wlr-foreign-toplevel-management for taskbars that also act on them.
3+
4+use crate::state::{State, identity};
5+use monolith_layout::WindowId;
6+use monolith_protocols::toplevel::{
7+ Action, ForeignToplevelHandler, ForeignToplevelState, Snapshot, Toplevel,
8+};
9+use smithay::wayland::foreign_toplevel_list::{
10+ ForeignToplevelHandle, ForeignToplevelListHandler, ForeignToplevelListState,
11+};
12+use std::collections::HashMap;
13+
14+#[derive(Default)]
15+pub struct ListHandles(HashMap<WindowId, ForeignToplevelHandle>);
16+
17+pub fn key(id: WindowId) -> u64 {
18+ slotmap::Key::data(&id).as_ffi()
19+}
20+
21+impl ForeignToplevelListHandler for State {
22+ fn foreign_toplevel_list_state(&mut self) -> &mut ForeignToplevelListState {
23+ &mut self.toplevel_list
24+ }
25+}
26+
27+impl ForeignToplevelHandler for State {
28+ fn foreign_toplevel_state(&mut self) -> &mut ForeignToplevelState {
29+ &mut self.foreign_toplevels
30+ }
31+
32+ fn foreign_toplevel_action(&mut self, action: Action) {
33+ let id_of = |k: u64| self.windows.keys().copied().find(|&id| key(id) == k);
34+ match action {
35+ Action::Activate(k) => {
36+ let Some(id) = id_of(k) else { return };
37+ if !self.layout.visible(id)
38+ && let Some(w) = self.layout.window(id)
39+ && let Some(o) = w.output
40+ && w.scratchpad.is_none()
41+ {
42+ let tag = w.tag;
43+ self.layout.toggle_view(o, tag);
44+ }
45+ self.layout.focus(id);
46+ }
47+ Action::Close(k) => {
48+ let Some(window) = id_of(k).and_then(|id| self.windows.get(&id)) else {
49+ return;
50+ };
51+ match (window.toplevel(), window.x11_surface()) {
52+ (Some(t), _) => t.send_close(),
53+ (_, Some(x)) => {
54+ let _ = x.close();
55+ }
56+ _ => {}
57+ }
58+ return;
59+ }
60+ Action::Maximize(k, on) => {
61+ let Some(id) = id_of(k) else { return };
62+ if self.layout.maximized(id) != on {
63+ self.layout.focus(id);
64+ self.layout.expand(monolith_layout::ExpandScope::All);
65+ }
66+ }
67+ Action::Fullscreen(k, on) => {
68+ let Some(id) = id_of(k) else { return };
69+ if self.layout.window(id).is_some_and(|w| w.fullscreen != on) {
70+ self.layout.toggle_fullscreen(id);
71+ }
72+ }
73+ }
74+ self.apply_layout();
75+ }
76+}
77+
78+impl State {
79+ /// Brings both window lists in line with the windows.
80+ pub fn publish_toplevels(&mut self) {
81+ let mut handles = std::mem::take(&mut self.list_handles.0);
82+ handles.retain(|id, h| {
83+ let keep = self.windows.contains_key(id);
84+ if !keep {
85+ self.toplevel_list.remove_toplevel(h);
86+ }
87+ keep
88+ });
89+ for (id, window) in &self.windows {
90+ let (app_id, title) = identity(window);
91+ match handles.get(id) {
92+ Some(h) => {
93+ if h.title() != title || h.app_id() != app_id {
94+ h.send_title(&title);
95+ h.send_app_id(&app_id);
96+ h.send_done();
97+ }
98+ }
99+ None => {
100+ let h = self.toplevel_list.new_toplevel::<State>(title, app_id);
101+ handles.insert(*id, h);
102+ }
103+ }
104+ }
105+ self.list_handles.0 = handles;
106+
107+ if !self.foreign_toplevels.has_clients() {
108+ return;
109+ }
110+ let focused = self.layout.focused();
111+ let toplevels = self
112+ .windows
113+ .iter()
114+ .map(|(&id, window)| {
115+ let (app_id, title) = identity(window);
116+ let w = self.layout.window(id);
117+ Toplevel {
118+ key: key(id),
119+ title,
120+ app_id,
121+ outputs: w
122+ .and_then(|w| w.output)
123+ .and_then(|o| self.smithay_output(o))
124+ .into_iter()
125+ .collect(),
126+ activated: focused == Some(id),
127+ maximized: self.layout.maximized(id),
128+ fullscreen: w.is_some_and(|w| w.fullscreen),
129+ }
130+ })
131+ .collect();
132+ let dh = self.display.clone();
133+ self.foreign_toplevels
134+ .update::<State>(&dh, Snapshot { toplevels });
135+ }
136+}
Modified file: crates/protocols/src/lib.rs+1 additions
@@ -1,3 +1,4 @@
11 //! Wayland protocols monolith implements itself.
22
3+pub mod toplevel;
34 pub mod workspace;
Added file: crates/protocols/src/toplevel.rs+288 additions
@@ -0,0 +1,288 @@
1+//! wlr-foreign-toplevel-management-v1, for taskbars and window titles in
2+//! bars. Like ext-workspace, the compositor publishes a snapshot of its
3+//! windows and each client is sent the difference from what it has seen.
4+
5+use smithay::output::Output;
6+use smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::{
7+ zwlr_foreign_toplevel_handle_v1::{self, ZwlrForeignToplevelHandleV1},
8+ zwlr_foreign_toplevel_manager_v1::{self, ZwlrForeignToplevelManagerV1},
9+};
10+use smithay::reexports::wayland_server::backend::{ClientId, GlobalId};
11+use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
12+use smithay::reexports::wayland_server::{
13+ Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
14+};
15+use smithay::wayland::{Dispatch2, GlobalDispatch2};
16+
17+#[derive(Clone, Debug, Default, PartialEq)]
18+pub struct Snapshot {
19+ pub toplevels: Vec<Toplevel>,
20+}
21+
22+#[derive(Clone, Debug, PartialEq)]
23+pub struct Toplevel {
24+ /// Stable for the window's lifetime.
25+ pub key: u64,
26+ pub title: String,
27+ pub app_id: String,
28+ pub outputs: Vec<Output>,
29+ pub activated: bool,
30+ pub maximized: bool,
31+ pub fullscreen: bool,
32+}
33+
34+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35+pub enum Action {
36+ Activate(u64),
37+ Close(u64),
38+ Maximize(u64, bool),
39+ Fullscreen(u64, bool),
40+}
41+
42+pub trait ForeignToplevelHandler {
43+ fn foreign_toplevel_state(&mut self) -> &mut ForeignToplevelState;
44+ fn foreign_toplevel_action(&mut self, action: Action);
45+}
46+
47+struct Sent {
48+ handle: ZwlrForeignToplevelHandleV1,
49+ last: Toplevel,
50+ outputs: Vec<WlOutput>,
51+}
52+
53+struct Manager {
54+ handle: ZwlrForeignToplevelManagerV1,
55+ toplevels: Vec<Sent>,
56+}
57+
58+pub struct ForeignToplevelState {
59+ global: GlobalId,
60+ managers: Vec<Manager>,
61+ snapshot: Snapshot,
62+}
63+
64+pub struct ManagerGlobal;
65+
66+pub struct ManagerData;
67+
68+pub struct HandleData {
69+ key: u64,
70+}
71+
72+impl ForeignToplevelState {
73+ pub fn new<D>(dh: &DisplayHandle) -> Self
74+ where
75+ D: GlobalDispatch<ZwlrForeignToplevelManagerV1, ManagerGlobal> + 'static,
76+ {
77+ Self {
78+ global: dh.create_global::<D, ZwlrForeignToplevelManagerV1, _>(3, ManagerGlobal),
79+ managers: Vec::new(),
80+ snapshot: Snapshot::default(),
81+ }
82+ }
83+
84+ pub fn global(&self) -> GlobalId {
85+ self.global.clone()
86+ }
87+
88+ pub fn has_clients(&self) -> bool {
89+ !self.managers.is_empty()
90+ }
91+
92+ pub fn update<D>(&mut self, dh: &DisplayHandle, snapshot: Snapshot)
93+ where
94+ D: Dispatch<ZwlrForeignToplevelHandleV1, HandleData> + 'static,
95+ {
96+ self.snapshot = snapshot;
97+ self.managers.retain(|m| m.handle.is_alive());
98+ for m in &mut self.managers {
99+ sync::<D>(dh, m, &self.snapshot);
100+ }
101+ }
102+}
103+
104+fn sync<D>(dh: &DisplayHandle, m: &mut Manager, snap: &Snapshot)
105+where
106+ D: Dispatch<ZwlrForeignToplevelHandleV1, HandleData> + 'static,
107+{
108+ let Ok(client) = dh.get_client(m.handle.id()) else {
109+ return;
110+ };
111+ m.toplevels.retain(|t| {
112+ let keep = snap.toplevels.iter().any(|s| s.key == t.last.key);
113+ if !keep {
114+ t.handle.closed();
115+ }
116+ keep
117+ });
118+ for top in &snap.toplevels {
119+ let i = match m.toplevels.iter().position(|t| t.last.key == top.key) {
120+ Some(i) => i,
121+ None => {
122+ let Ok(handle) = client.create_resource::<ZwlrForeignToplevelHandleV1, _, D>(
123+ dh,
124+ m.handle.version(),
125+ HandleData { key: top.key },
126+ ) else {
127+ continue;
128+ };
129+ m.handle.toplevel(&handle);
130+ send_props(&handle, None, top);
131+ m.toplevels.push(Sent {
132+ handle,
133+ last: top.clone(),
134+ outputs: Vec::new(),
135+ });
136+ let sent = m.toplevels.last_mut().expect("just pushed");
137+ sync_outputs(&client, sent, top);
138+ sent.handle.done();
139+ continue;
140+ }
141+ };
142+ let sent = &mut m.toplevels[i];
143+ let mut changed = sync_outputs(&client, sent, top);
144+ if sent.last != *top {
145+ send_props(&sent.handle, Some(&sent.last), top);
146+ sent.last = top.clone();
147+ changed = true;
148+ }
149+ if changed {
150+ sent.handle.done();
151+ }
152+ }
153+}
154+
155+/// Sends output enter and leave for the client's wl_outputs. Returns
156+/// whether anything was sent.
157+fn sync_outputs(client: &Client, sent: &mut Sent, top: &Toplevel) -> bool {
158+ let want: Vec<WlOutput> = top
159+ .outputs
160+ .iter()
161+ .flat_map(|o| o.client_outputs(client))
162+ .collect();
163+ let mut changed = false;
164+ sent.outputs.retain(|o| {
165+ let keep = o.is_alive() && want.contains(o);
166+ if !keep && o.is_alive() {
167+ sent.handle.output_leave(o);
168+ changed = true;
169+ }
170+ keep
171+ });
172+ for wl in want {
173+ if !sent.outputs.contains(&wl) {
174+ sent.handle.output_enter(&wl);
175+ sent.outputs.push(wl);
176+ changed = true;
177+ }
178+ }
179+ changed
180+}
181+
182+fn send_props(h: &ZwlrForeignToplevelHandleV1, old: Option<&Toplevel>, new: &Toplevel) {
183+ if old.is_none_or(|o| o.title != new.title) {
184+ h.title(new.title.clone());
185+ }
186+ if old.is_none_or(|o| o.app_id != new.app_id) {
187+ h.app_id(new.app_id.clone());
188+ }
189+ let states = |t: &Toplevel| {
190+ let mut s: Vec<u32> = Vec::new();
191+ if t.maximized {
192+ s.push(zwlr_foreign_toplevel_handle_v1::State::Maximized as u32);
193+ }
194+ if t.activated {
195+ s.push(zwlr_foreign_toplevel_handle_v1::State::Activated as u32);
196+ }
197+ if t.fullscreen && h.version() >= 2 {
198+ s.push(zwlr_foreign_toplevel_handle_v1::State::Fullscreen as u32);
199+ }
200+ s
201+ };
202+ if old.is_none_or(|o| states(o) != states(new)) {
203+ h.state(states(new).iter().flat_map(|s| s.to_ne_bytes()).collect());
204+ }
205+}
206+
207+impl<D> GlobalDispatch2<ZwlrForeignToplevelManagerV1, D> for ManagerGlobal
208+where
209+ D: ForeignToplevelHandler
210+ + Dispatch<ZwlrForeignToplevelManagerV1, ManagerData>
211+ + Dispatch<ZwlrForeignToplevelHandleV1, HandleData>
212+ + 'static,
213+{
214+ fn bind(
215+ &self,
216+ state: &mut D,
217+ dh: &DisplayHandle,
218+ _client: &Client,
219+ resource: New<ZwlrForeignToplevelManagerV1>,
220+ data_init: &mut DataInit<'_, D>,
221+ ) {
222+ let handle = data_init.init(resource, ManagerData);
223+ let s = state.foreign_toplevel_state();
224+ let mut m = Manager {
225+ handle,
226+ toplevels: Vec::new(),
227+ };
228+ sync::<D>(dh, &mut m, &s.snapshot);
229+ s.managers.push(m);
230+ }
231+}
232+
233+impl<D: ForeignToplevelHandler> Dispatch2<ZwlrForeignToplevelManagerV1, D> for ManagerData {
234+ fn request(
235+ &self,
236+ state: &mut D,
237+ _client: &Client,
238+ resource: &ZwlrForeignToplevelManagerV1,
239+ request: zwlr_foreign_toplevel_manager_v1::Request,
240+ _dh: &DisplayHandle,
241+ _data_init: &mut DataInit<'_, D>,
242+ ) {
243+ if let zwlr_foreign_toplevel_manager_v1::Request::Stop = request {
244+ resource.finished();
245+ state
246+ .foreign_toplevel_state()
247+ .managers
248+ .retain(|m| m.handle != *resource);
249+ }
250+ }
251+
252+ fn destroyed(&self, state: &mut D, _client: ClientId, resource: &ZwlrForeignToplevelManagerV1) {
253+ state
254+ .foreign_toplevel_state()
255+ .managers
256+ .retain(|m| m.handle != *resource);
257+ }
258+}
259+
260+impl<D: ForeignToplevelHandler> Dispatch2<ZwlrForeignToplevelHandleV1, D> for HandleData {
261+ fn request(
262+ &self,
263+ state: &mut D,
264+ _client: &Client,
265+ _resource: &ZwlrForeignToplevelHandleV1,
266+ request: zwlr_foreign_toplevel_handle_v1::Request,
267+ _dh: &DisplayHandle,
268+ _data_init: &mut DataInit<'_, D>,
269+ ) {
270+ use zwlr_foreign_toplevel_handle_v1::Request;
271+ let action = match request {
272+ Request::Activate { .. } => Action::Activate(self.key),
273+ Request::Close => Action::Close(self.key),
274+ Request::SetMaximized => Action::Maximize(self.key, true),
275+ Request::UnsetMaximized => Action::Maximize(self.key, false),
276+ Request::SetFullscreen { .. } => Action::Fullscreen(self.key, true),
277+ Request::UnsetFullscreen => Action::Fullscreen(self.key, false),
278+ _ => return,
279+ };
280+ state.foreign_toplevel_action(action);
281+ }
282+
283+ fn destroyed(&self, state: &mut D, _client: ClientId, resource: &ZwlrForeignToplevelHandleV1) {
284+ for m in &mut state.foreign_toplevel_state().managers {
285+ m.toplevels.retain(|t| t.handle != *resource);
286+ }
287+ }
288+}