atagen

monolith

public
Star0
Watch0

Commit 754ca2132943

tarball

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

6 files changed+170-42
windows: ask a window that becomes floating for its own size

A toggled tiled window took the layout's default float rect, about 60 percent of the output, which dwarfs dialog-like content. Layout::Window::float_default now marks such rects. apply_layout asks xdg toplevels to pick a size with a configure that has none and no tiled states, centres them on the first commit after the ack, and uses the size hints of fixed-size windows directly. Fixed-size xdg windows use their committed geometry because GTK reports min and max sizes that disagree with it.
Modified file: crates/layout/src/lib.rs+6 additions
@@ -61,6 +61,9 @@ pub struct Window {
6161 pub tag: TagId,
6262 pub floating: bool,
6363 pub float_rect: Option<Rect>,
64+ /// `float_rect` is the layout's default, not a size the window or the
65+ /// user chose.
66+ pub float_default: bool,
6467 pub fullscreen: bool,
6568 pub sticky: bool,
6669 pub scratchpad: Option<usize>,
@@ -325,6 +328,7 @@ impl Layout {
325328 tag,
326329 floating: floating.is_some(),
327330 float_rect: floating,
331+ float_default: false,
328332 fullscreen: false,
329333 sticky: false,
330334 scratchpad: None,
@@ -364,6 +368,7 @@ impl Layout {
364368 if win.float_rect.is_none() {
365369 let rect = centred(self.outputs[o].area, 0.6);
366370 self.windows[w].float_rect = Some(rect);
371+ self.windows[w].float_default = true;
367372 }
368373 self.outputs[o].floating.push(w);
369374 return;
@@ -629,6 +634,7 @@ impl Layout {
629634 pub fn set_float_rect(&mut self, w: WindowId, rect: Rect) {
630635 if let Some(win) = self.windows.get_mut(w) {
631636 win.float_rect = Some(rect);
637+ win.float_default = false;
632638 }
633639 }
634640
Modified file: crates/monolith/src/state.rs+3 additions−2 deletions
@@ -247,6 +247,7 @@ impl State {
247247
248248 /// Pushes the layout's target geometry into the space and to clients.
249249 pub fn apply_layout(&mut self) {
250+ self.size_new_floats();
250251 let now = now_ns();
251252 let mut shown = Vec::new();
252253 let mut tracks = Vec::new();
@@ -364,9 +365,9 @@ impl State {
364365
365366 let mut mapped = Vec::new();
366367 for (id, rect, kind, _) in shown {
368+ let free = kind == Kind::Floating && self.wm.get(id).is_some_and(|s| s.natural_size);
367369 if let Some(window) = self.wm.window(id).cloned()
368- && !self.wm.get(id).is_some_and(|s| s.natural_size)
369- && configure(&window, rect, kind, self.wm.layout.maximized(id))
370+ && (free || configure(&window, rect, kind, self.wm.layout.maximized(id)))
370371 {
371372 let loc = (rect.x.round() as i32, rect.y.round() as i32);
372373 let loc = Point::from(loc) - window.geometry().loc;
Modified file: crates/monolith/src/windows.rs+90 additions−2 deletions
@@ -9,7 +9,9 @@ use smithay::desktop::Window;
99 use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
1010 use smithay::reexports::wayland_server::Resource;
1111 use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
12-use smithay::utils::{Logical, Point, Rectangle};
12+use smithay::utils::{Logical, Point, Rectangle, Serial};
13+use smithay::wayland::compositor::with_states;
14+use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
1315 use std::time::Duration;
1416
1517 impl State {
@@ -108,7 +110,11 @@ impl State {
108110 let Some(w) = self.wm.get_mut(id) else {
109111 return;
110112 };
111- let natural = std::mem::take(&mut w.natural_size);
113+ let natural = w.natural_size && w.natural_after.is_none_or(|s| acked(&w.window, s));
114+ if natural {
115+ w.natural_size = false;
116+ w.natural_after = None;
117+ }
112118 let reclassify = std::mem::take(&mut w.reclassify);
113119 let size = w.window.geometry().size;
114120 let sized = natural || (reclassify && self.float_if_hinted(id));
@@ -159,6 +165,74 @@ impl State {
159165 true
160166 }
161167
168+ /// Gives floats that landed on the layout's default rect their own size,
169+ /// as when a tiled window is toggled. The client is told to pick one, and
170+ /// its next commit after acking says which.
171+ pub fn size_new_floats(&mut self) {
172+ let stale: Vec<WindowId> = self
173+ .wm
174+ .iter()
175+ .filter(|(id, s)| {
176+ s.natural_size && !self.wm.layout.window(*id).is_some_and(|w| w.floating)
177+ })
178+ .map(|(id, _)| id)
179+ .collect();
180+ for id in stale {
181+ if let Some(s) = self.wm.get_mut(id) {
182+ s.natural_size = false;
183+ s.natural_after = None;
184+ }
185+ }
186+ let defaults: Vec<WindowId> = self
187+ .wm
188+ .layout
189+ .windows()
190+ .filter(|(_, w)| w.floating && w.float_default && w.scratchpad.is_none())
191+ .map(|(id, _)| id)
192+ .collect();
193+ for id in defaults {
194+ self.ask_natural_size(id);
195+ }
196+ }
197+
198+ fn ask_natural_size(&mut self, id: WindowId) {
199+ let Some(window) = self
200+ .wm
201+ .get(id)
202+ .filter(|s| !s.natural_size)
203+ .map(|s| s.window.clone())
204+ else {
205+ return;
206+ };
207+ let geometry = window.geometry().size;
208+ let current = (geometry.w > 0 && geometry.h > 0).then_some(geometry);
209+ let size = match (window.x11_surface(), window.toplevel()) {
210+ (Some(x), _) => crate::xwayland::fixed_size(x),
211+ (_, Some(t)) if crate::xdg::fixed_size(t).is_some() => current,
212+ (_, Some(t)) => {
213+ let serial = crate::wm::configure_free(t);
214+ if let Some(w) = self.wm.get_mut(id)
215+ && serial.is_some()
216+ {
217+ w.natural_size = true;
218+ w.natural_after = serial;
219+ return;
220+ }
221+ current
222+ }
223+ _ => None,
224+ };
225+ match size {
226+ Some(size) => self.float_at_natural_size(id, f64::from(size.w), f64::from(size.h)),
227+ None => {
228+ let rect = self.wm.layout.window(id).and_then(|w| w.float_rect);
229+ if let Some(rect) = rect {
230+ self.wm.layout.set_float_rect(id, rect);
231+ }
232+ }
233+ }
234+ }
235+
162236 /// Centres a window over its parent, or else its output.
163237 fn float_at_natural_size(&mut self, id: WindowId, w: f64, h: f64) {
164238 let parent = self
@@ -322,3 +396,17 @@ impl State {
322396 self.wm.id_of(surface)
323397 }
324398 }
399+
400+/// Whether the client has acknowledged the configure with `serial` or a later
401+/// one.
402+fn acked(window: &Window, serial: Serial) -> bool {
403+ let Some(t) = window.toplevel() else {
404+ return true;
405+ };
406+ with_states(t.wl_surface(), |s| {
407+ let data = s.data_map.get::<XdgToplevelSurfaceData>()?.lock().ok()?;
408+ let last = data.last_acked.as_ref()?;
409+ Some(last.serial.is_no_older_than(&serial))
410+ })
411+ .unwrap_or(false)
412+}
Modified file: crates/monolith/src/wm.rs+45 additions−25 deletions
@@ -10,11 +10,11 @@ use smithay::backend::renderer::element::solid::SolidColorBuffer;
1010 use smithay::desktop::{PopupManager, Space, Window};
1111 use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State as XdgState;
1212 use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
13-use smithay::utils::{Logical, Rectangle, Transform};
13+use smithay::utils::{Logical, Rectangle, Serial, Transform};
1414 use smithay::wayland::compositor::with_states;
1515 use smithay::wayland::foreign_toplevel_list::ForeignToplevelHandle;
1616 use smithay::wayland::seat::WaylandFocus;
17-use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
17+use smithay::wayland::shell::xdg::{ToplevelState, ToplevelSurface, XdgToplevelSurfaceData};
1818 use std::collections::HashMap;
1919
2020 pub struct Wm {
@@ -58,6 +58,9 @@ pub struct WindowState {
5858 /// Configured without a size until its first buffer shows the one it
5959 /// picked.
6060 pub natural_size: bool,
61+ /// The configure that gave it free choice of size, whose ack the buffer
62+ /// has to follow to show that choice.
63+ pub natural_after: Option<Serial>,
6164 /// Tiled by default, but its size hints are only reliable at its first
6265 /// buffer and may turn it into a float.
6366 pub reclassify: bool,
@@ -117,6 +120,7 @@ impl Wm {
117120 placed: None,
118121 awaiting_buffer: true,
119122 natural_size: false,
123+ natural_after: None,
120124 reclassify: false,
121125 placed_by_rule,
122126 list_handle: None,
@@ -231,29 +235,7 @@ pub fn configure(window: &Window, rect: Rect, kind: Kind, maximized: bool) -> bo
231235 if let Some(t) = window.toplevel() {
232236 t.with_pending_state(|s| {
233237 s.size = Some((w, h).into());
234- let tiled = [
235- XdgState::TiledLeft,
236- XdgState::TiledRight,
237- XdgState::TiledTop,
238- XdgState::TiledBottom,
239- ];
240- for st in tiled {
241- if kind == Kind::Tiled {
242- s.states.set(st);
243- } else {
244- s.states.unset(st);
245- }
246- }
247- if kind == Kind::Fullscreen {
248- s.states.set(XdgState::Fullscreen);
249- } else {
250- s.states.unset(XdgState::Fullscreen);
251- }
252- if maximized && kind == Kind::Tiled {
253- s.states.set(XdgState::Maximized);
254- } else {
255- s.states.unset(XdgState::Maximized);
256- }
238+ set_states(s, kind, maximized);
257239 });
258240 if t.is_initial_configure_sent() {
259241 t.send_pending_configure();
@@ -262,6 +244,44 @@ pub fn configure(window: &Window, rect: Rect, kind: Kind, maximized: bool) -> bo
262244 true
263245 }
264246
247+/// Lets a floating toplevel pick its own size, returning the serial of the
248+/// configure that says so. None if it was sent nothing.
249+pub fn configure_free(t: &ToplevelSurface) -> Option<Serial> {
250+ t.with_pending_state(|s| {
251+ s.size = None;
252+ set_states(s, Kind::Floating, false);
253+ });
254+ t.is_initial_configure_sent()
255+ .then(|| t.send_pending_configure())
256+ .flatten()
257+}
258+
259+fn set_states(s: &mut ToplevelState, kind: Kind, maximized: bool) {
260+ let tiled = [
261+ XdgState::TiledLeft,
262+ XdgState::TiledRight,
263+ XdgState::TiledTop,
264+ XdgState::TiledBottom,
265+ ];
266+ for st in tiled {
267+ if kind == Kind::Tiled {
268+ s.states.set(st);
269+ } else {
270+ s.states.unset(st);
271+ }
272+ }
273+ if kind == Kind::Fullscreen {
274+ s.states.set(XdgState::Fullscreen);
275+ } else {
276+ s.states.unset(XdgState::Fullscreen);
277+ }
278+ if maximized && kind == Kind::Tiled {
279+ s.states.set(XdgState::Maximized);
280+ } else {
281+ s.states.unset(XdgState::Maximized);
282+ }
283+}
284+
265285 /// A window's surface, for xdg toplevels and X11 windows alike.
266286 pub fn root(window: &Window) -> Option<WlSurface> {
267287 window.wl_surface().map(|s| s.into_owned())
Modified file: crates/monolith/src/xdg.rs+16 additions−7 deletions
@@ -4,7 +4,7 @@ use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_to
44 use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
55 use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
66 use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
7-use smithay::utils::Serial;
7+use smithay::utils::{Logical, Serial, Size};
88 use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
99 use smithay::wayland::compositor::with_states;
1010 use smithay::wayland::shell::xdg::{
@@ -14,15 +14,24 @@ use smithay::wayland::shell::xdg::{
1414
1515 /// Dialogs, and windows that cap their size and so could never fill a tile.
1616 pub fn floats(toplevel: &ToplevelSurface) -> bool {
17- let max = with_states(toplevel.wl_surface(), |s| {
18- s.cached_state
19- .get::<SurfaceCachedState>()
20- .current()
21- .max_size
22- });
17+ let (_, max) = size_hints(toplevel);
2318 toplevel.parent().is_some() || (max.w > 0 && max.h > 0)
2419 }
2520
21+/// The size of a window that asks for exactly one.
22+pub fn fixed_size(toplevel: &ToplevelSurface) -> Option<Size<i32, Logical>> {
23+ let (min, max) = size_hints(toplevel);
24+ (min == max && min.w > 0 && min.h > 0).then_some(min)
25+}
26+
27+fn size_hints(toplevel: &ToplevelSurface) -> (Size<i32, Logical>, Size<i32, Logical>) {
28+ with_states(toplevel.wl_surface(), |s| {
29+ let mut cached = s.cached_state.get::<SurfaceCachedState>();
30+ let current = cached.current();
31+ (current.min_size, current.max_size)
32+ })
33+}
34+
2635 impl XdgShellHandler for State {
2736 fn xdg_shell_state(&mut self) -> &mut XdgShellState {
2837 &mut self.protocols.xdg_shell_state
Modified file: crates/monolith/src/xwayland.rs+10 additions−6 deletions
@@ -9,7 +9,7 @@ use smithay::reexports::calloop::generic::Generic;
99 use smithay::reexports::calloop::{Interest, Mode, PostAction, RegistrationToken};
1010 use smithay::reexports::rustix::io::{FdFlags, fcntl_getfd, fcntl_setfd};
1111 use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
12-use smithay::utils::{Logical, Rectangle};
12+use smithay::utils::{Logical, Rectangle, Size};
1313 use smithay::wayland::xwayland_shell::{XWaylandShellHandler, XWaylandShellState};
1414 use smithay::xwayland::xwm::{Reorder, ResizeEdge, WmWindowProperty, WmWindowType, XwmId};
1515 use smithay::xwayland::{X11Surface, X11Wm, XWayland, XWaylandEvent, XwmHandler};
@@ -358,11 +358,15 @@ fn floats(window: &X11Surface) -> bool {
358358 | WmWindowType::Menu
359359 )
360360 );
361- let fixed = matches!(
362- (window.min_size(), window.max_size()),
363- (Some(min), Some(max)) if min == max && min.w > 0
364- );
365- kind || fixed || window.is_transient_for().is_some()
361+ kind || fixed_size(window).is_some() || window.is_transient_for().is_some()
362+}
363+
364+/// The size of a window that asks for exactly one.
365+pub fn fixed_size(window: &X11Surface) -> Option<Size<i32, Logical>> {
366+ match (window.min_size(), window.max_size()) {
367+ (Some(min), Some(max)) if min == max && min.w > 0 && min.h > 0 => Some(min),
368+ _ => None,
369+ }
366370 }
367371
368372 impl State {