atagen

monolith

public
Star0
Watch0

Commit af095d8ce0fc

tarball

atagen <boss@atagen.co>2026-10-28 04:06 UTC

5 files changed+102-14
layout: keep directional focus on its layer and add focus-layer

Directional focus mixed floating and tiled windows, so a float over a tile could be stepped into by accident and out of only by luck. Floats step by their centres because they overlap, which left the edge-based Layout::focus_dir unable to reach a float overlapping the focused one. focus-layer, on Mod+space, is the way across.
Modified file: crates/config/default.toml+1 additions
@@ -145,6 +145,7 @@ selection_access = "focused"
145145 "Mod+o" = "flip"
146146 "Mod+f" = "fullscreen"
147147 "Mod+Shift+space" = "float"
148+"Mod+space" = "focus-layer"
148149 "Mod+s" = "sticky"
149150
150151 "Mod+comma" = "focus-output left"
Modified file: crates/ipc/src/lib.rs+3 additions
@@ -47,6 +47,8 @@ pub enum Command {
4747 Focus {
4848 dir: Dir,
4949 },
50+ /// Focuses the latest window on the other layer, floating or tiled.
51+ FocusLayer,
5052 Move {
5153 dir: Dir,
5254 },
@@ -173,6 +175,7 @@ impl Command {
173175 "fullscreen" => none(Command::Fullscreen)?,
174176 "sticky" => none(Command::Sticky)?,
175177 "focus" => Command::Focus { dir: dir()? },
178+ "focus-layer" => none(Command::FocusLayer)?,
176179 "move" => Command::Move { dir: dir()? },
177180 "move-container" => Command::MoveContainer { dir: dir()? },
178181 "focus-output" => Command::FocusOutput { dir: dir()? },
Modified file: crates/layout/src/lib.rs+71 additions−14 deletions
@@ -558,26 +558,24 @@ impl Layout {
558558 .map(|w| self.modal_leaf(w))
559559 }
560560
561- /// Focuses the nearest visible window in `dir`, crossing to the next
562- /// output when there is none on this one.
561+ /// Focuses the nearest visible window in `dir` on the focused window's
562+ /// layer, tiled or floating, crossing to the next output when there is
563+ /// none on this one.
563564 pub fn focus_dir(&mut self, dir: Dir) -> Option<WindowId> {
564565 let o = self.active?;
565566 let current = self.focused_on(o);
566567 let from = current.and_then(|f| self.rect_of(f));
567568 let target = from.and_then(|from| {
568569 let arr = self.arrange(o);
569- let reachable = |w| {
570- arr.fullscreen
571- .is_none_or(|f| w == f || arr.over.contains(&w))
572- };
573- let candidates = arr
574- .tiled
575- .iter()
576- .filter(|p| p.visible && !self.eclipsed(p.window))
577- .map(|p| (p.window, p.rect))
578- .chain(arr.floating.iter().copied())
579- .filter(|&(w, _)| Some(self.modal_leaf(w)) != current && reachable(w));
580- nearest(from, dir, candidates)
570+ let floating = current.is_some_and(|f| arr.floating.iter().any(|p| p.0 == f));
571+ let candidates = self
572+ .layer(&arr, floating)
573+ .filter(|&(w, _)| Some(self.modal_leaf(w)) != current);
574+ if floating {
575+ nearest_centre(from, dir, candidates)
576+ } else {
577+ nearest(from, dir, candidates)
578+ }
581579 });
582580 if let Some(w) = target {
583581 return self.focus(w);
@@ -591,6 +589,39 @@ impl Layout {
591589 w
592590 }
593591
592+ /// Focuses the most recently focused window on the other layer, floating
593+ /// when a tiled window has focus and tiled otherwise.
594+ pub fn focus_layer(&mut self) -> Option<WindowId> {
595+ let o = self.active?;
596+ let arr = self.arrange(o);
597+ let floating = self
598+ .focused_on(o)
599+ .is_some_and(|f| arr.floating.iter().any(|p| p.0 == f));
600+ let w = self
601+ .layer(&arr, !floating)
602+ .map(|(w, _)| w)
603+ .max_by_key(|&w| self.history.iter().position(|&h| h == w))?;
604+ self.focus(w)
605+ }
606+
607+ /// The windows focus can reach on one layer of an arrangement.
608+ fn layer<'a>(
609+ &'a self,
610+ arr: &'a Arrangement,
611+ floating: bool,
612+ ) -> impl Iterator<Item = (WindowId, Rect)> + 'a {
613+ let tiled = arr
614+ .tiled
615+ .iter()
616+ .filter(move |p| !floating && p.visible && !self.eclipsed(p.window))
617+ .map(|p| (p.window, p.rect));
618+ let floats = arr.floating.iter().copied().filter(move |_| floating);
619+ tiled.chain(floats).filter(|&(w, _)| {
620+ arr.fullscreen
621+ .is_none_or(|f| w == f || arr.over.contains(&w))
622+ })
623+ }
624+
594625 pub fn output_towards(&self, from: OutputId, dir: Dir) -> Option<OutputId> {
595626 let here = self.outputs.get(from)?.area;
596627 let others = self
@@ -1091,6 +1122,32 @@ fn nearest<K: Copy>(
10911122 .map(|(k, _)| k)
10921123 }
10931124
1125+/// The candidate whose centre is nearest `from`'s within 45 degrees of
1126+/// `dir`. Floating windows overlap freely, so their edges say little about
1127+/// which way one lies from another.
1128+fn nearest_centre<K: Copy>(
1129+ from: Rect,
1130+ dir: Dir,
1131+ candidates: impl Iterator<Item = (K, Rect)>,
1132+) -> Option<K> {
1133+ candidates
1134+ .filter_map(|(k, r)| {
1135+ let (dx, dy) = (
1136+ r.center_x() - from.center_x(),
1137+ r.center_y() - from.center_y(),
1138+ );
1139+ let (along, across) = match dir {
1140+ Dir::Right => (dx, dy),
1141+ Dir::Left => (-dx, dy),
1142+ Dir::Down => (dy, dx),
1143+ Dir::Up => (-dy, dx),
1144+ };
1145+ (along > 0.0 && along >= across.abs()).then_some((k, along.hypot(across)))
1146+ })
1147+ .min_by(|a, b| a.1.total_cmp(&b.1))
1148+ .map(|(k, _)| k)
1149+}
1150+
10941151 fn span_overlap(a: f64, al: f64, b: f64, bl: f64) -> f64 {
10951152 (a + al).min(b + bl) - a.max(b)
10961153 }
Modified file: crates/layout/src/tests.rs+24 additions
@@ -979,3 +979,27 @@ fn new_floats_land_inside_the_bars() {
979979 let r = l.window(plain).and_then(|w| w.float_rect).unwrap();
980980 assert_eq!(r.y + r.h / 2.0, usable.y + usable.h / 2.0);
981981 }
982+
983+/// Overlapping floats have no clear edge between them, so edge-based focus
984+/// could not step from one to another it overlaps.
985+#[test]
986+fn floating_focus_steps_by_centres_and_stays_on_its_layer() {
987+ let mut l = Layout::default();
988+ let o = l.add_output("a", AREA);
989+ let tiled = l.add_window(Some(o), None, None);
990+ let a = l.add_window(Some(o), None, Some(Rect::new(100.0, 100.0, 400.0, 300.0)));
991+ let b = l.add_window(Some(o), None, Some(Rect::new(350.0, 120.0, 300.0, 200.0)));
992+ let c = l.add_window(Some(o), None, Some(Rect::new(250.0, 350.0, 200.0, 200.0)));
993+ l.focus(a);
994+ assert_eq!(l.focus_dir(Dir::Right), Some(b));
995+ l.focus(a);
996+ assert_eq!(l.focus_dir(Dir::Down), Some(c));
997+ assert_eq!(
998+ l.focus_dir(Dir::Down),
999+ None,
1000+ "no float below, and no other output"
1001+ );
1002+ assert_eq!(l.focus_layer(), Some(tiled));
1003+ assert_eq!(l.focus_dir(Dir::Right), None);
1004+ assert_eq!(l.focus_layer(), Some(c));
1005+}
Modified file: crates/monolith/src/commands.rs+3 additions
@@ -80,6 +80,9 @@ impl State {
8080 Command::Focus { dir: d } => {
8181 self.wm.layout.focus_dir(d);
8282 }
83+ Command::FocusLayer => {
84+ self.wm.layout.focus_layer();
85+ }
8386 Command::Move { dir: d } if merge => {
8487 self.wm.layout.merge_window(d);
8588 }