//! Focus queries and directional/cyclic focus movement. //! Split out of the original single `manager.rs` - see `super` (`mod.rs`) for //! `WindowManager`'s field definitions; everything here is plain `impl WindowManager` //! methods, unchanged from before the split. use super::*; impl WindowManager { // ---- Focus ---------------------------------------------------------- pub fn focused_window(&self) -> Option<&Window> { self.focused.and_then(|id| self.windows.get(&id)) } pub fn focused_id(&self) -> Option { self.focused } pub fn focus_window(&mut self, id: WindowId) { if let Some(workspace) = self.windows.get(&id).map(|w| w.workspace) { // Switch to the target's workspace first if it isn't already // the active one - without this, focusing a window elsewhere // (Alt-Tab, a dock icon, anything that ends up calling this) // marked it focused while leaving it genuinely off-screen: // `visible_windows`/rendering both gate on `workspace == // current_workspace`, so keyboard focus landed on a window the // user could not see, while whatever was actually on screen // kept looking focused-ish. Reported live (relayed from the // AGS peer session, who measured it directly over IPC): // `srd dispatch focus` on a window from a different workspace // left `current_workspace` unchanged and the target `visible: // false`. Every caller of `focus_window` gets this for free // rather than each one remembering to switch workspaces // itself first - matches the convention the AGS shell was // already built against (Hyprland's `focuswindow` switches // workspace as a side effect of focusing). Guarded on // `w.workspace != self.current_workspace` specifically, not // just always calling `switch_workspace`: that function's own // `auto_back_and_forth` handling treats being asked to // "switch" to the *already*-current workspace as a deliberate // toggle-to-previous gesture, which an ordinary redundant // focus call (the common case: re-focusing whatever is // already focused and already visible) must not trigger as a // surprise workspace jump. if workspace != self.current_workspace { self.switch_workspace(workspace); } // A minimized window's dock icon (or Alt-Tab entry, or any // other `focus_window` caller) has to un-minimize it too, not // just focus it - without this, clicking a minimized app's // dock icon left it focused (keyboard input, `srd clients`' // own `focused: true`) while still `minimized: true`, still // excluded from `visible_windows`/rendering. Reads exactly like // the click did nothing, since the one visible thing "focused" // usually implies (the window coming to the front) never // happens. Every desktop's taskbar/dock has this behavior for // free; this compositor's `Activate` (zwlr-foreign-toplevel) // and `"focus"` IPC handlers both route through here already, // so they get it too rather than each needing their own // explicit restore call. if let Some(w) = self.windows.get_mut(&id) { w.minimized = false; } self.focused = Some(id); self.raise_window(id); } } pub(super) fn cycle_focus(&mut self, forward: bool) { let ids: Vec = self.windows_on_workspace(self.current_workspace).filter(|w| !w.minimized).map(|w| w.id).collect(); if ids.is_empty() { self.focused = None; return; } let cur_pos = self.focused.and_then(|f| ids.iter().position(|&i| i == f)); let next = match cur_pos { None => 0, Some(p) if forward => (p + 1) % ids.len(), Some(p) => (p + ids.len() - 1) % ids.len(), }; self.focus_window(ids[next]); } pub fn focus_next(&mut self) { self.cycle_focus(true); } pub fn focus_previous(&mut self) { self.cycle_focus(false); } /// Vim-style directional focus: picks the nearest window whose center /// lies in `dir` relative to the focused window's center, on the same /// workspace. Returns the newly focused window, if any. /// Nearest window to the focused one in `dir`, by a distance biased /// toward the requested axis so a window that's mostly to the left /// (small |dy|) beats a diagonally-placed one - matching how /// i3/sway-style directional focus feels. /// /// Shared by [`Self::focus_direction`] and [`Self::move_window_direction`] /// so "the window to the left" means the same thing whether you're /// focusing it or swapping with it. pub fn neighbour_in(&self, dir: Direction) -> Option { let (fx, fy, fid) = { let focused = self.focused_window()?; let (fx, fy) = focused.geometry.center(); (fx, fy, focused.id) }; let workspace = self.current_workspace; let mut best: Option<(WindowId, i64)> = None; for w in self.windows_on_workspace(workspace).filter(|w| w.id != fid && !w.minimized) { let (cx, cy) = w.geometry.center(); let (dx, dy) = ((cx - fx) as i64, (cy - fy) as i64); let matches = match dir { Direction::Left => dx < 0, Direction::Right => dx > 0, Direction::Up => dy < 0, Direction::Down => dy > 0, }; if !matches { continue; } let (primary, secondary) = match dir { Direction::Left | Direction::Right => (dx, dy), Direction::Up | Direction::Down => (dy, dx), }; let dist = primary * primary + secondary * secondary * 4; if best.is_none_or(|(_, d)| dist < d) { best = Some((w.id, dist)); } } best.map(|(id, _)| id) } pub fn focus_direction(&mut self, dir: Direction) -> Option { let target = self.neighbour_in(dir); if let Some(id) = target { self.focus_window(id); } target } /// Moves the focused window in `dir` by swapping places with its /// neighbour there - the `movewindow l/r/u/d` gesture. /// /// Swapping (rather than nudging by a fixed step) is what makes this /// useful in both of srdwm's modes: under tiling it reorders the layout, /// and in dynamic/floating mode two windows trade positions, which is /// predictable either way. With no neighbour in that direction the /// window is pushed to the corresponding edge of its monitor instead, so /// the key still does something sensible. /// How far one press moves a window, as a fraction of the monitor. /// /// An eighth crosses the screen in eight presses, which is fine /// control without being tedious, and passes through the middle at the /// fourth - the owner asked for "increments, or one side, middle, /// other side", and stepping gives both. const MOVE_STEP_FRACTION: i32 = 8; pub fn move_window_direction(&mut self, dir: Direction) -> Option { let focused = self.focused_id()?; // Only a tiling layout swaps with the neighbour. Everywhere else a // window has its own geometry and "move it left" means move it, not // trade places with whatever happens to be over there. let tiling = self .windows .get(&focused) .and_then(|w| self.workspace(w.workspace)) .map(|ws| ws.layout == "tiling") .unwrap_or(false); if !tiling { self.nudge_window(focused, dir); return None; } match self.neighbour_in(dir) { Some(other) => { let a = self.windows.get(&focused)?.geometry; let b = self.windows.get(&other)?.geometry; if let Some(w) = self.windows.get_mut(&focused) { w.geometry = b; } if let Some(w) = self.windows.get_mut(&other) { w.geometry = a; } // Keep stacking order in step so a tiling layout, which // assigns slots from `order`, actually reflects the swap. let (ia, ib) = ( self.order.iter().position(|&id| id == focused)?, self.order.iter().position(|&id| id == other)?, ); self.order.swap(ia, ib); Some(other) } None => { let mon = self.windows.get(&focused).and_then(|w| self.monitor_for(w.monitor))?.geometry; let w = self.windows.get_mut(&focused)?; match dir { Direction::Left => w.geometry.x = mon.x, Direction::Right => w.geometry.x = mon.right() - w.geometry.width as i32, Direction::Up => w.geometry.y = mon.y, Direction::Down => w.geometry.y = mon.bottom() - w.geometry.height as i32, } None } } } /// Grows or shrinks the focused window one step along `dir`. /// /// The keyboard counterpart to dragging an edge. srdwm has had /// Super+right-drag since before this, which covers the mouse, but /// nothing resized a window without one - reported as a missing /// keybinding alongside screen lock. /// /// `grow` decides which way: growing extends the window's far edge in /// `dir`, shrinking pulls it back, so Right always affects the right /// edge whichever way it moves. The window's top-left stays put, which /// is what every keyboard resize does and what keeps repeated presses /// predictable. /// /// Bounded by the same minimums a drag is, and by the monitor's usable /// area, so a window can neither be resized to nothing nor grown off /// the screen. pub fn resize_window_direction(&mut self, dir: Direction, grow: bool) -> Option { let focused = self.focused_id()?; let area = self.windows.get(&focused).and_then(|w| self.monitor_for(w.monitor)).map(|m| m.geometry)?; let step_x = (area.width as i32 / Self::MOVE_STEP_FRACTION).max(1); let step_y = (area.height as i32 / Self::MOVE_STEP_FRACTION).max(1); let w = self.windows.get_mut(&focused)?; let (min_w, min_h) = (w.min_size.0.max(crate::placement::MIN_WINDOW_WIDTH) as i32, w.min_size.1.max(crate::placement::MIN_WINDOW_HEIGHT) as i32); let delta_x = if grow { step_x } else { -step_x }; let delta_y = if grow { step_y } else { -step_y }; match dir { Direction::Left | Direction::Right => { let room = area.right() - w.geometry.x; w.geometry.width = (w.geometry.width as i32 + delta_x).clamp(min_w, room.max(min_w)) as u32; } Direction::Up | Direction::Down => { let room = area.bottom() - w.geometry.y; w.geometry.height = (w.geometry.height as i32 + delta_y).clamp(min_h, room.max(min_h)) as u32; } } Some(focused) } /// Moves a window one step in `dir`, stopping flush against the edge of /// its monitor's usable area. /// /// This is what a directional move does outside a tiling layout. It used /// to slam the window all the way to the edge in one press, which the /// owner reported as "it should move in increments, one side, middle, /// other side - not just extreme left/up/right/down". /// /// The step is a fraction of the monitor rather than a pixel count, so /// the same keypress feels the same on a laptop panel and on a 4K /// display. Landing within one step of an edge snaps flush to it, so the /// window can always be put exactly against the side rather than a few /// pixels short of it. fn nudge_window(&mut self, id: WindowId, dir: Direction) { let Some(area) = self.windows.get(&id).and_then(|w| self.monitor_for(w.monitor)).map(|m| m.geometry) else { return }; let step_x = (area.width as i32 / Self::MOVE_STEP_FRACTION).max(1); let step_y = (area.height as i32 / Self::MOVE_STEP_FRACTION).max(1); let Some(w) = self.windows.get_mut(&id) else { return }; let (width, height) = (w.geometry.width as i32, w.geometry.height as i32); let (min_x, max_x) = (area.x, (area.right() - width).max(area.x)); let (min_y, max_y) = (area.y, (area.bottom() - height).max(area.y)); match dir { Direction::Left => w.geometry.x = (w.geometry.x - step_x).max(min_x), Direction::Right => w.geometry.x = (w.geometry.x + step_x).min(max_x), Direction::Up => w.geometry.y = (w.geometry.y - step_y).max(min_y), Direction::Down => w.geometry.y = (w.geometry.y + step_y).min(max_y), } } }