atagen

monolith

public
Star0
Watch0

Commit c1b89405f8fd

tarball

atagen <boss@atagen.co>2026-10-19 09:32 UTC

5 files changed+52-3
input: stretch windows during a mouse resize and configure on release

Every motion event of a resize drag sent a configure, and a split drag sent one to each neighbouring tile, so clients redrew dozens of times a second at sizes nobody kept. The windows a drag reshapes now hold their size and are drawn stretched from their last buffer, and the final size goes out once when the button is released.
Modified file: crates/monolith/src/input.rs+12 additions
@@ -2,6 +2,7 @@
22 //! into these calls, with pointer positions in global logical coordinates.
33
44 use crate::state::{Input, State};
5+use crate::wm::Resizing;
56 use monolith_layout::{Axis, Dir, Layout, MIN_FLOAT_SIZE, Rect, WindowId, clamp_float};
67 use smithay::backend::input::{ButtonState, InputTime, KeyState};
78 use smithay::backend::session::Session;
@@ -55,6 +56,9 @@ impl State {
5556 .drag
5657 .take_if(|d| d.button.is_none_or(|b| b == button));
5758 if ended.is_some() {
59+ if self.wm.resizing.take().is_some() {
60+ self.apply_layout();
61+ }
5862 let pos = self.input.pointer_pos;
5963 let over = self.surface_under(pos).is_some();
6064 self.input.hover = self.edge_at(pos, over).map(|(_, e)| e);
@@ -551,6 +555,14 @@ fn begin_drag(state: &mut State, id: WindowId, edges: Option<Edges>, button: Opt
551555 ) else {
552556 return false;
553557 };
558+ state.wm.resizing = win
559+ .output
560+ .filter(|_| edges.is_some())
561+ .map(|output| Resizing {
562+ output,
563+ window: id,
564+ tiled,
565+ });
554566 state.input.drag = Some(Drag {
555567 window: id,
556568 from: state.input.pointer_pos,
Modified file: crates/monolith/src/lock.rs+1 additions
@@ -78,6 +78,7 @@ impl SessionLockHandler for State {
7878 self.services.lock.pending = Some(confirmation);
7979 self.services.lock.blanked.clear();
8080 self.input.drag = None;
81+ self.wm.resizing = None;
8182 self.sync_focus();
8283 self.dirty.redraw = true;
8384 }
Modified file: crates/monolith/src/scene.rs+9 additions−2 deletions
@@ -47,7 +47,9 @@ pub fn build(
4747 anim,
4848 windows,
4949 last_frames,
50+ resizing,
5051 } = wm.parts();
52+ let stretched = |w, tiled| resizing.is_some_and(|r| r.holds(w, tiled, o));
5153 let Some(area) = layout.output(o).map(|out| out.area) else {
5254 return Vec::new();
5355 };
@@ -82,13 +84,18 @@ pub fn build(
8284 Some(r) => above.push((p.window, local(r), true, true)),
8385 None => {
8486 let r = anim.tiled(o, p.window, now).unwrap_or(p.rect);
85- tiled.push((p.window, local(r), true, false));
87+ tiled.push((p.window, local(r), true, stretched(p.window, true)));
8688 }
8789 }
8890 }
8991 let float = |&(w, r): &(WindowId, Rect)| {
9092 let glide = anim.gliding(w, now);
91- (w, local(glide.unwrap_or(r)), true, glide.is_some())
93+ (
94+ w,
95+ local(glide.unwrap_or(r)),
96+ true,
97+ glide.is_some() || stretched(w, false),
98+ )
9299 };
93100 let (over, under): (Vec<_>, Vec<_>) =
94101 arr.floating.iter().partition(|f| arr.over.contains(&f.0));
Modified file: crates/monolith/src/state.rs+9 additions−1 deletions
@@ -396,12 +396,20 @@ impl State {
396396 let mut mapped = Vec::new();
397397 for (id, rect, kind, o) in shown {
398398 let free = kind == Kind::Floating && self.wm.get(id).is_some_and(|s| s.natural_size);
399+ let held = self
400+ .wm
401+ .resizing
402+ .is_some_and(|r| r.holds(id, kind == Kind::Tiled, o))
403+ && rect.w >= 1.0
404+ && rect.h >= 1.0;
399405 let scale = self
400406 .backend
401407 .output(o)
402408 .map_or(1.0, |o| o.current_scale().fractional_scale());
403409 if let Some(window) = self.wm.window(id).cloned()
404- && (free || configure(&window, rect, kind, self.wm.layout.maximized(id), scale))
410+ && (free
411+ || held
412+ || configure(&window, rect, kind, self.wm.layout.maximized(id), scale))
405413 {
406414 let loc = (rect.x.round() as i32, rect.y.round() as i32);
407415 self.wm.space.map_element(window, loc, false);
Modified file: crates/monolith/src/wm.rs+21 additions
@@ -33,6 +33,24 @@ pub struct Wm {
3333 pub layer_focus: Option<WlSurface>,
3434 /// What closed windows still show while their ghost animates.
3535 pub last_frames: HashMap<WindowId, LastFrame>,
36+ pub resizing: Option<Resizing>,
37+}
38+
39+/// A mouse resize in progress. The windows it reshapes keep their size and
40+/// are drawn stretched until the button is released, rather than being sent
41+/// a configure for every motion event.
42+#[derive(Clone, Copy)]
43+pub struct Resizing {
44+ pub output: OutputId,
45+ pub window: WindowId,
46+ /// Moving a split reshapes every tile on the output, not just one.
47+ pub tiled: bool,
48+}
49+
50+impl Resizing {
51+ pub fn holds(&self, id: WindowId, tiled: bool, o: OutputId) -> bool {
52+ self.output == o && if self.tiled { tiled } else { id == self.window }
53+ }
3654 }
3755
3856 pub struct LastFrame {
@@ -51,6 +69,7 @@ pub struct Parts<'a> {
5169 pub anim: &'a Animator,
5270 pub windows: &'a mut SecondaryMap<WindowId, WindowState>,
5371 pub last_frames: &'a HashMap<WindowId, LastFrame>,
72+ pub resizing: Option<Resizing>,
5473 }
5574
5675 pub struct WindowState {
@@ -120,6 +139,7 @@ impl Wm {
120139 anim,
121140 layer_focus: None,
122141 last_frames: HashMap::new(),
142+ resizing: None,
123143 }
124144 }
125145
@@ -211,6 +231,7 @@ impl Wm {
211231 anim: &self.anim,
212232 windows: &mut self.windows,
213233 last_frames: &self.last_frames,
234+ resizing: self.resizing,
214235 }
215236 }