atagen

monolith

public
Star0
Watch0

Commit abe5e42a95d6

tarball

atagen <boss@atagen.co>2026-10-19 08:01 UTC

4 files changed+260-16
input: resize from the nearest edge on Mod+right-drag

Mod+right-drag always grew a floating window from its bottom-right corner and ignored tiled windows. The pointer's cell in a 3x3 grid over the window now picks the edges, and tiled windows move the split next to those edges through the new Tree::move_split, which changes only the two tiles meeting at the split so it tracks the pointer.
Modified file: crates/layout/src/lib.rs+21 additions
@@ -593,6 +593,27 @@ impl Layout {
593593 done
594594 }
595595
596+ /// Whether `w` is tiled with a visible window across the split on side `dir`.
597+ pub fn has_split(&self, w: WindowId, dir: Dir) -> bool {
598+ let Some(o) = self.windows.get(w).and_then(|w| w.output) else {
599+ return false;
600+ };
601+ self.outputs[o].tree.has_split(w, dir, self.vis_on(o))
602+ }
603+
604+ /// Moves the split on side `dir` of the tiled window `w` by `px`, positive
605+ /// toward the right or bottom.
606+ pub fn move_split(&mut self, w: WindowId, dir: Dir, px: f64) -> bool {
607+ let Some(o) = self.windows.get(w).and_then(|w| w.output) else {
608+ return false;
609+ };
610+ let area = inset(self.outputs[o].usable(), self.gaps.outer);
611+ let mut tree = std::mem::take(&mut self.outputs[o].tree);
612+ let moved = tree.move_split(w, dir, px, area, self.gaps.inner, self.vis_on(o));
613+ self.outputs[o].tree = tree;
614+ moved
615+ }
616+
596617 pub fn expand(&mut self, scope: ExpandScope) -> bool {
597618 let Some((w, o)) = self.focused_tiled() else {
598619 return false;
Modified file: crates/layout/src/tests.rs+36 additions
@@ -677,3 +677,39 @@ fn floats_shrink_into_a_smaller_output_they_are_evicted_to() {
677677 assert!(r.x >= usable.x && r.right() <= usable.right());
678678 assert!(r.y >= usable.y && r.bottom() <= usable.bottom());
679679 }
680+
681+/// Dragging a split moves only the two tiles that meet at it, whichever of
682+/// them the drag started from, and stops where one would vanish.
683+#[test]
684+fn dragging_a_split_moves_just_that_split() {
685+ let mut l = Layout::default();
686+ let o = l.add_output("a", AREA);
687+ let a = l.add_window(Some(o), None, None);
688+ let b = l.add_window(Some(o), None, None);
689+ let c = l.add_window(Some(o), None, None);
690+ let rect = |l: &Layout, w| {
691+ l.arrange(o)
692+ .tiled
693+ .iter()
694+ .find(|p| p.window == w)
695+ .unwrap()
696+ .rect
697+ };
698+ let before = [rect(&l, a), rect(&l, b), rect(&l, c)];
699+ assert!(!l.has_split(a, Dir::Up));
700+ assert!(!l.move_split(a, Dir::Up, 50.0));
701+ assert!(l.has_split(a, Dir::Down));
702+
703+ assert!(l.move_split(a, Dir::Down, 40.0));
704+ let (ra, rb) = (rect(&l, a), rect(&l, b));
705+ assert!((ra.h - before[0].h - 40.0).abs() < 1e-6);
706+ assert!((rb.h - before[1].h + 40.0).abs() < 1e-6);
707+ assert_eq!(rect(&l, c).h, before[2].h);
708+
709+ assert!(l.move_split(b, Dir::Up, -40.0));
710+ assert!((rect(&l, a).h - before[0].h).abs() < 1e-6);
711+
712+ l.move_split(a, Dir::Down, 1e6);
713+ assert!(rect(&l, b).h > 1.0);
714+ l.check();
715+}
Modified file: crates/layout/src/tree.rs+94 additions
@@ -426,6 +426,100 @@ impl<W: Copy + Eq + Hash> Tree<W> {
426426 false
427427 }
428428
429+ /// The lane a window's split on side `dir` lives in, as the child holding
430+ /// the window, its parent and the index of the visible sibling across
431+ /// the split.
432+ fn split_across(
433+ &self,
434+ w: W,
435+ dir: Dir,
436+ vis: &impl Fn(W) -> bool,
437+ ) -> Option<(NodeId, NodeId, usize)> {
438+ let mut cur = *self.leaves.get(&w)?;
439+ if !self.visible(cur, vis) {
440+ return None;
441+ }
442+ while let Some((p, i)) = self.position(cur) {
443+ if self.axis(p) == Some(dir.axis())
444+ && let Some(j) = self.next_shown(p, i, dir, vis)
445+ {
446+ return Some((cur, p, j));
447+ }
448+ cur = p;
449+ }
450+ None
451+ }
452+
453+ /// Whether the split on side `dir` of `w` has a visible window across it.
454+ pub fn has_split(&self, w: W, dir: Dir, vis: impl Fn(W) -> bool) -> bool {
455+ self.split_across(w, dir, &vis).is_some()
456+ }
457+
458+ /// Moves the split on side `dir` of `w` by `px` along that side's axis,
459+ /// positive toward the right or bottom. Only the two children meeting at
460+ /// the split change, so the split follows the pointer.
461+ pub fn move_split(
462+ &mut self,
463+ w: W,
464+ dir: Dir,
465+ px: f64,
466+ area: Rect,
467+ gap: f64,
468+ vis: impl Fn(W) -> bool,
469+ ) -> bool {
470+ let Some((cur, p, j)) = self.split_across(w, dir, &vis) else {
471+ return false;
472+ };
473+ let axis = dir.axis();
474+ let other = self.children(p)[j];
475+ let (first, second) = if dir.forward() {
476+ (cur, other)
477+ } else {
478+ (other, cur)
479+ };
480+ let mut leaves = Vec::new();
481+ self.shown_leaves(first, &vis, &mut leaves);
482+ let (start, end) = self
483+ .arrange(area, gap, &vis)
484+ .iter()
485+ .filter(|(l, _)| leaves.contains(l))
486+ .map(|(_, r)| span(*r, axis))
487+ .fold((f64::MAX, f64::MIN), |(s, e), (rs, rl)| {
488+ (s.min(rs), e.max(rs + rl))
489+ });
490+ let ef = self.extent(first, axis, &vis);
491+ let es = self.extent(second, axis, &vis);
492+ if end <= start || ef <= 0.0 || es <= 0.0 {
493+ return true;
494+ }
495+ let sum: f64 = self
496+ .children(p)
497+ .iter()
498+ .filter(|&&c| self.visible(c, &vis))
499+ .map(|&c| self.extent(c, axis, &vis))
500+ .sum();
501+ let min = MIN_SHARE * sum;
502+ let du = (px * ef / (end - start)).clamp(-(ef - min).max(0.0), (es - min).max(0.0));
503+ self.scale(first, axis, (ef + du) / ef);
504+ self.scale(second, axis, (es - du) / es);
505+ true
506+ }
507+
508+ fn shown_leaves(&self, n: NodeId, vis: &impl Fn(W) -> bool, out: &mut Vec<W>) {
509+ match &self.nodes[n].kind {
510+ Kind::Leaf(w, _) => {
511+ if vis(*w) {
512+ out.push(*w);
513+ }
514+ }
515+ Kind::Container { children, .. } => {
516+ for &c in children {
517+ self.shown_leaves(c, vis, out);
518+ }
519+ }
520+ }
521+ }
522+
429523 /// Scales every window under `n` along `axis`, hidden ones too, so they
430524 /// keep their proportions for when they are shown again.
431525 fn scale(&mut self, n: NodeId, axis: Axis, by: f64) {
Modified file: crates/monolith/src/input.rs+109 additions−16 deletions
@@ -2,7 +2,7 @@
22 //! into these calls, with pointer positions in global logical coordinates.
33
44 use crate::state::State;
5-use monolith_layout::{MIN_FLOAT_SIZE, Rect, WindowId, clamp_float};
5+use monolith_layout::{Axis, Dir, Layout, MIN_FLOAT_SIZE, Rect, WindowId, clamp_float};
66 use smithay::backend::input::{ButtonState, InputTime, KeyState};
77 use smithay::backend::session::Session;
88 use smithay::input::keyboard::{FilterResult, Keycode, xkb};
@@ -246,7 +246,7 @@ const BTN_LEFT: u32 = 0x110;
246246 const BTN_RIGHT: u32 = 0x111;
247247
248248 /// The sides of a window a resize moves.
249-#[derive(Clone, Copy, Default)]
249+#[derive(Clone, Copy, Default, PartialEq, Eq)]
250250 pub struct Edges {
251251 left: bool,
252252 right: bool,
@@ -255,12 +255,52 @@ pub struct Edges {
255255 }
256256
257257 impl Edges {
258- const BOTTOM_RIGHT: Self = Self {
259- left: false,
260- right: true,
261- top: false,
262- bottom: true,
263- };
258+ /// The edges a drag from pos resizes, from the cell of a 3x3 grid over
259+ /// the rect. The centre cell takes the nearest corner.
260+ fn grid(r: Rect, pos: Point<f64, Logical>) -> Self {
261+ let (fx, fy) = ((pos.x - r.x) / r.w, (pos.y - r.y) / r.h);
262+ let third = |f: f64| match f {
263+ f if f < 1.0 / 3.0 => -1,
264+ f if f > 2.0 / 3.0 => 1,
265+ _ => 0,
266+ };
267+ let (col, row) = match (third(fx), third(fy)) {
268+ (0, 0) => (if fx < 0.5 { -1 } else { 1 }, if fy < 0.5 { -1 } else { 1 }),
269+ cell => cell,
270+ };
271+ Self {
272+ left: col < 0,
273+ right: col > 0,
274+ top: row < 0,
275+ bottom: row > 0,
276+ }
277+ }
278+
279+ fn dirs(self) -> impl Iterator<Item = Dir> {
280+ [
281+ (Dir::Left, self.left),
282+ (Dir::Right, self.right),
283+ (Dir::Up, self.top),
284+ (Dir::Down, self.bottom),
285+ ]
286+ .into_iter()
287+ .filter_map(|(dir, on)| on.then_some(dir))
288+ }
289+
290+ /// Drops the edges of a tiled window that have no split to move.
291+ fn movable(self, layout: &Layout, id: WindowId) -> Self {
292+ let has = |dir, on: bool| on && layout.has_split(id, dir);
293+ Self {
294+ left: has(Dir::Left, self.left),
295+ right: has(Dir::Right, self.right),
296+ top: has(Dir::Up, self.top),
297+ bottom: has(Dir::Down, self.bottom),
298+ }
299+ }
300+
301+ fn any(self) -> bool {
302+ self.left || self.right || self.top || self.bottom
303+ }
264304
265305 pub fn from_xdg(edge: xdg_toplevel::ResizeEdge) -> Option<Self> {
266306 use xdg_toplevel::ResizeEdge as E;
@@ -310,6 +350,8 @@ pub struct Drag {
310350 from: Point<f64, Logical>,
311351 rect: Rect,
312352 bounds: Rect,
353+ /// A tiled window, whose splits follow the pointer instead of a rect.
354+ tiled: bool,
313355 /// None moves the window.
314356 edges: Option<Edges>,
315357 /// The client's button, whose release is still the client's to see.
@@ -332,32 +374,67 @@ fn start_drag(state: &mut State, button: u32) -> bool {
332374 if !held {
333375 return false;
334376 }
335- let edges = (button == BTN_RIGHT).then_some(Edges::BOTTOM_RIGHT);
377+ let edges = match button {
378+ BTN_RIGHT => {
379+ let Some(rect) = window_rect(&state.wm.layout, id) else {
380+ return false;
381+ };
382+ Some(Edges::grid(rect, state.input.pointer_pos))
383+ }
384+ _ => None,
385+ };
336386 begin_drag(state, id, edges, None)
337387 }
338388
389+fn window_rect(layout: &Layout, id: WindowId) -> Option<Rect> {
390+ let win = layout.window(id)?;
391+ if win.floating {
392+ return win.float_rect;
393+ }
394+ layout
395+ .arrange(win.output?)
396+ .tiled
397+ .iter()
398+ .find(|p| p.window == id && p.visible)
399+ .map(|p| p.rect)
400+}
401+
339402 impl State {
340403 /// Starts moving, or with `edges` resizing, a floating window at a
341404 /// client's request. Tiled windows are left alone.
342405 pub fn client_drag(&mut self, id: WindowId, edges: Option<Edges>, button: u32) {
343- begin_drag(self, id, edges, Some(button));
406+ if self.wm.layout.window(id).is_some_and(|w| w.floating) {
407+ begin_drag(self, id, edges, Some(button));
408+ }
344409 }
345410 }
346411
347412 fn begin_drag(state: &mut State, id: WindowId, edges: Option<Edges>, button: Option<u32>) -> bool {
348413 let layout = &state.wm.layout;
349- let Some((rect, bounds)) = layout
350- .window(id)
351- .filter(|w| w.floating && !w.fullscreen)
352- .and_then(|w| Some((w.float_rect?, layout.output(w.output?)?.usable())))
353- else {
414+ let Some(win) = layout.window(id).filter(|w| !w.fullscreen) else {
415+ return false;
416+ };
417+ let tiled = !win.floating;
418+ let edges = if tiled {
419+ edges.map(|e| e.movable(layout, id)).filter(|e| e.any())
420+ } else {
421+ edges
422+ };
423+ if tiled && edges.is_none() {
424+ return false;
425+ }
426+ let (Some(rect), Some(out)) = (
427+ window_rect(layout, id),
428+ win.output.and_then(|o| layout.output(o)),
429+ ) else {
354430 return false;
355431 };
356432 state.input.drag = Some(Drag {
357433 window: id,
358434 from: state.input.pointer_pos,
359435 rect,
360- bounds,
436+ bounds: out.usable(),
437+ tiled,
361438 edges,
362439 button,
363440 });
@@ -392,6 +469,22 @@ fn drag_to(state: &mut State, pos: Point<f64, Logical>) -> bool {
392469 return false;
393470 };
394471 let (dx, dy) = (pos.x - d.from.x, pos.y - d.from.y);
472+ if let (true, Some(e)) = (d.tiled, d.edges) {
473+ let id = d.window;
474+ for dir in e.dirs() {
475+ let px = if dir.axis() == Axis::Horizontal {
476+ dx
477+ } else {
478+ dy
479+ };
480+ state.wm.layout.move_split(id, dir, px);
481+ }
482+ if let Some(d) = state.input.drag.as_mut() {
483+ d.from = pos;
484+ }
485+ state.apply_layout();
486+ return true;
487+ }
395488 let r = d.rect;
396489 let rect = match d.edges {
397490 Some(e) => {