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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
|
//! 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.
/// 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<WindowId> {
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<WindowId> {
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),
}
}
}
|