atagen

monolith

public
Star0
Watch0

Commit 24fca1aad608

tarball

atagen <boss@atagen.co>2026-10-17 09:57 UTC

4 files changed+68-6
monolith: add focus-follows-mouse and mouse-follows-focus

Both are off by default. Focus changes the pointer made itself are marked as followed, so neither option sets off the other. Warping is skipped nested, where the host owns the pointer.
Modified file: crates/config/default.toml+5 additions
@@ -12,6 +12,11 @@ gaps = { inner = 8, outer = 8 }
1212 # What a window move does when it meets a neighbour. swap trades places,
1313 # merge joins its lane. invert-move does the other one for the next move.
1414 move = "swap"
15+# Focus whatever window the pointer moves onto.
16+focus_follows_mouse = false
17+# Put the pointer in the middle of a window focused from the keyboard, a
18+# rule or a new map. Bare metal only, a nested pointer belongs to the host.
19+mouse_follows_focus = false
1520
1621 [decoration]
1722 # Borders sit in the gap around each window, so keep them under half the inner gap.
Modified file: crates/config/src/lib.rs+4 additions
@@ -31,6 +31,8 @@ pub struct General {
3131 /// What a window move does when it meets a neighbour.
3232 #[serde(rename = "move")]
3333 pub move_mode: MoveMode,
34+ pub focus_follows_mouse: bool,
35+ pub mouse_follows_focus: bool,
3436 }
3537
3638 #[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
@@ -49,6 +51,8 @@ impl Default for General {
4951 mod_key: Modifier::Super,
5052 gaps: Gaps::default(),
5153 move_mode: MoveMode::Swap,
54+ focus_follows_mouse: false,
55+ mouse_follows_focus: false,
5256 }
5357 }
5458 }
Modified file: crates/monolith/src/input.rs+54 additions−6 deletions
@@ -20,6 +20,7 @@ impl State {
2020 let Some(pointer) = self.input.seat.get_pointer() else {
2121 return;
2222 };
23+ focus_on_hover(self, pos);
2324 let under = self.surface_under(pos);
2425 pointer.motion(
2526 self,
@@ -149,20 +150,67 @@ fn focus_under_pointer(state: &mut State) {
149150 if state.wm.layer_focus.take().is_some() {
150151 state.sync_focus();
151152 }
152- let id = state
153- .wm
154- .space
155- .element_under(pos)
156- .and_then(|(w, _)| crate::wm::root(w))
157- .and_then(|s| state.window_id(&s));
153+ let id = window_at(state, pos);
158154 if let Some(id) = id
159155 && state.wm.layout.focused() != Some(id)
160156 {
157+ state.input.followed = Some(id);
161158 state.wm.layout.focus(id);
162159 state.apply_layout();
163160 }
164161 }
165162
163+fn window_at(state: &State, pos: Point<f64, Logical>) -> Option<WindowId> {
164+ let (w, _) = state.wm.space.element_under(pos)?;
165+ state.window_id(&crate::wm::root(w)?)
166+}
167+
168+/// Layers above windows, like a bar, keep the focus where it was.
169+fn focus_on_hover(state: &mut State, pos: Point<f64, Logical>) {
170+ if !state.settings.config.general.focus_follows_mouse
171+ || state.services.lock.active()
172+ || state
173+ .layer_under(pos, &[Layer::Overlay, Layer::Top])
174+ .is_some()
175+ {
176+ return;
177+ }
178+ let Some(id) = window_at(state, pos) else {
179+ return;
180+ };
181+ if state.wm.layout.focused() == Some(id) {
182+ return;
183+ }
184+ state.wm.layer_focus = None;
185+ state.input.followed = Some(id);
186+ state.wm.layout.focus(id);
187+ state.apply_layout();
188+}
189+
190+impl State {
191+ /// Puts the pointer in the middle of a window that gained focus from
192+ /// anything but the pointer.
193+ pub fn follow_focus(&mut self) {
194+ let focused = self.wm.layout.focused();
195+ if focused == std::mem::replace(&mut self.input.followed, focused)
196+ || !self.settings.config.general.mouse_follows_focus
197+ || self.backend.drm().is_none()
198+ || self.services.lock.active()
199+ {
200+ return;
201+ }
202+ let Some((_, r, _)) = focused.and_then(|id| self.wm.get(id)?.placed) else {
203+ return;
204+ };
205+ self.pointer_to(
206+ Point::from((r.x + r.w / 2.0, r.y + r.h / 2.0)),
207+ InputTime::now(),
208+ );
209+ self.pointer_frame();
210+ self.dirty.cursor = true;
211+ }
212+}
213+
166214 const BTN_LEFT: u32 = 0x110;
167215 const BTN_RIGHT: u32 = 0x111;
168216
Modified file: crates/monolith/src/state.rs+5 additions
@@ -49,6 +49,9 @@ pub struct Input {
4949 pub suppressed: Vec<Keycode>,
5050 pub drag: Option<crate::input::Drag>,
5151 pub move_inverted: bool,
52+ /// The focus mouse-follows-focus last saw, so pointer-made focus
53+ /// changes can mark themselves as already followed.
54+ pub followed: Option<WindowId>,
5255 pub cursor: crate::cursor::Cursor,
5356 pub cursor_status: CursorImageStatus,
5457 }
@@ -167,6 +170,7 @@ impl State {
167170 suppressed: Vec::new(),
168171 drag: None,
169172 move_inverted: false,
173+ followed: None,
170174 cursor: crate::cursor::Cursor::load(),
171175 cursor_status: CursorImageStatus::default_named(),
172176 },
@@ -343,6 +347,7 @@ impl State {
343347 }
344348 }
345349 self.sync_focus();
350+ self.follow_focus();
346351 self.update_inhibit();
347352 self.dirty.redraw = true;
348353 self.dirty.snapshot = true;