1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
|
//! 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<WindowId> {
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<WindowId> = 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<WindowId> {
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<WindowId> {
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.
pub fn move_window_direction(&mut self, dir: Direction) -> Option<WindowId> {
let focused = self.focused_id()?;
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
}
}
}
}
|