atagen

monolith

public
Star0
Watch0

Commit 668fd294147a

tarball

atagen <boss@atagen.co>2026-10-19 07:51 UTC

2 files changed+70-13
xdg: keep fullscreen asked for before the first commit

set_fullscreen needs a mapped window id, so mpv --fs, SDL and other clients that request fullscreen before mapping were dropped and started windowed. The request now stays in the toplevel's pending state, together with the wl_output it named, and map_window reads it back, as it does for an X11 window that mapped with _NET_WM_STATE_FULLSCREEN. A request on a mapped window now also honours the output argument.
Modified file: crates/monolith/src/windows.rs+24 additions−1 deletions
@@ -7,7 +7,9 @@ use smithay::backend::renderer::Renderer;
77 use smithay::backend::renderer::utils::{import_surface_tree, with_renderer_surface_state};
88 use smithay::desktop::Window;
99 use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
10+use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State as XdgState;
1011 use smithay::reexports::wayland_server::Resource;
12+use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
1113 use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
1214 use smithay::utils::{Logical, Point, Rectangle, Serial};
1315 use smithay::wayland::compositor::with_states;
@@ -29,6 +31,7 @@ impl State {
2931 .map(|c| c.pid as u32),
3032 };
3133 let launch = pid.and_then(|pid| self.services.launches.find(pid));
34+ let (fullscreen, fullscreen_output) = requested_fullscreen(&window);
3235 let output = rule
3336 .output
3437 .as_deref()
@@ -39,6 +42,7 @@ impl State {
3942 .find(|(_, o)| o.name == name)
4043 .map(|(id, _)| id)
4144 })
45+ .or(fullscreen_output.and_then(|o| self.layout_output(&o)))
4246 .or(launch.and_then(|l| l.0));
4347 let tag = match rule.tag.as_deref() {
4448 Some(t) => Some(self.wm.layout.tag(t)),
@@ -61,6 +65,9 @@ impl State {
6165 if floating != self.wm.layout.window(id).is_some_and(|w| w.floating) {
6266 self.wm.layout.toggle_floating(id);
6367 }
68+ if fullscreen {
69+ self.wm.layout.toggle_fullscreen(id);
70+ }
6471 if rule.sticky == Some(true) {
6572 self.wm.layout.toggle_sticky(id);
6673 }
@@ -69,7 +76,7 @@ impl State {
6976 }
7077 self.view_if_first(id);
7178 let by_rule = rule.tag.is_some() || rule.output.is_some();
72- let natural_size = floating && window.toplevel().is_some();
79+ let natural_size = floating && !fullscreen && window.toplevel().is_some();
7380 let reclassify = !floating && rule.floating.is_none() && window.toplevel().is_some();
7481 self.wm.insert(id, window, by_rule, app_id, title);
7582 if let Some(s) = self.wm.get_mut(id) {
@@ -432,3 +439,19 @@ fn acked(window: &Window, serial: Serial) -> bool {
432439 })
433440 .unwrap_or(false)
434441 }
442+
443+/// What a window asked for before it mapped. X11 windows carry it in their
444+/// state, xdg toplevels in the pending state `set_fullscreen` left.
445+fn requested_fullscreen(window: &Window) -> (bool, Option<WlOutput>) {
446+ if let Some(x) = window.x11_surface() {
447+ return (x.is_fullscreen(), None);
448+ }
449+ window.toplevel().map_or((false, None), |t| {
450+ t.with_pending_state(|s| {
451+ (
452+ s.states.contains(XdgState::Fullscreen),
453+ s.fullscreen_output.clone(),
454+ )
455+ })
456+ })
457+}
Modified file: crates/monolith/src/xdg.rs+46 additions−12 deletions
@@ -1,9 +1,10 @@
11 use crate::state::State;
2+use monolith_layout::OutputId;
23 use smithay::desktop::{PopupKind, Window};
34 use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode;
5+use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State as XdgState;
46 use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
57 use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
6-use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
78 use smithay::utils::{Logical, Serial, Size};
89 use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
910 use smithay::wayland::compositor::with_states;
@@ -45,12 +46,12 @@ impl XdgShellHandler for State {
4546 self.unmap_window(surface.wl_surface());
4647 }
4748
48- fn fullscreen_request(&mut self, surface: ToplevelSurface, _output: Option<WlOutput>) {
49- self.set_fullscreen(surface.wl_surface(), true);
49+ fn fullscreen_request(&mut self, surface: ToplevelSurface, output: Option<WlOutput>) {
50+ self.set_fullscreen(&surface, true, output);
5051 }
5152
5253 fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
53- self.set_fullscreen(surface.wl_surface(), false);
54+ self.set_fullscreen(&surface, false, None);
5455 }
5556
5657 fn new_popup(&mut self, surface: PopupSurface, positioner: PositionerState) {
@@ -97,16 +98,49 @@ fn server_side(toplevel: &ToplevelSurface) {
9798 }
9899
99100 impl State {
100- fn set_fullscreen(&mut self, surface: &WlSurface, on: bool) {
101- if let Some(id) = self.window_id(surface)
102- && self
103- .wm
104- .layout
105- .window(id)
106- .is_some_and(|w| w.fullscreen != on)
107- {
101+ /// A toplevel that has not mapped has no window to change, so its wish is
102+ /// kept in its pending state for `map_window` to find.
103+ fn set_fullscreen(&mut self, toplevel: &ToplevelSurface, on: bool, output: Option<WlOutput>) {
104+ let Some(id) = self.window_id(toplevel.wl_surface()) else {
105+ toplevel.with_pending_state(|s| {
106+ if on {
107+ s.states.set(XdgState::Fullscreen);
108+ } else {
109+ s.states.unset(XdgState::Fullscreen);
110+ }
111+ s.fullscreen_output = output.filter(|_| on);
112+ });
113+ return;
114+ };
115+ let target = output
116+ .as_ref()
117+ .and_then(|o| self.layout_output(o))
118+ .filter(|&o| {
119+ self.wm
120+ .layout
121+ .window(id)
122+ .is_some_and(|w| w.output != Some(o))
123+ });
124+ if let Some(o) = target {
125+ self.wm.layout.send_to_output(id, o);
126+ }
127+ let toggled = self
128+ .wm
129+ .layout
130+ .window(id)
131+ .is_some_and(|w| w.fullscreen != on);
132+ if toggled {
108133 self.wm.layout.toggle_fullscreen(id);
134+ }
135+ if target.is_some() || toggled {
109136 self.apply_layout();
110137 }
111138 }
139+
140+ /// The layout output a client's `wl_output` names.
141+ pub fn layout_output(&self, output: &WlOutput) -> Option<OutputId> {
142+ let ids = self.wm.layout.outputs().map(|(id, _)| id);
143+ ids.into_iter()
144+ .find(|&id| self.backend.output(id).is_some_and(|o| o.owns(output)))
145+ }
112146 }