srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/core/src/manager/windows.rs
blob: 7e2046dcef8a34415e40391a93da3a8e38ee8bf5 (plain) (blame)
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
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
//! Window registration/removal, lookup, and stacking-order operations (raise, lower, pin).
//! 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 {
    // ---- Windows -------------------------------------------------------

    pub fn alloc_window_id(&mut self) -> WindowId {
        let id = self.next_window_id;
        self.next_window_id += 1;
        id
    }

    /// Registers a window that a backend has already created. If the current
    /// workspace's layout doesn't auto-tile ("dynamic"/"floating"), the
    /// window's initial geometry is chosen via [`SmartPlacement`]; otherwise
    /// it's left for the next `arrange_workspace` call to place.
    pub fn add_window(&mut self, mut window: Window) -> WindowId {
        let id = window.id;
        // Applied before rule matching below, which still wins when a rule
        // sets its own `border_color`/`border_width` - this only replaces
        // whatever a backend's `Window::new` happened to hardcode.
        window.border_color = self.theme.default_border_color;
        window.border_width = self.theme.default_border_width;
        window.corner_radius = self.theme.default_corner_radius;
        window.decorated = self.theme.default_decorated && !likely_draws_own_titlebar(&window.app_id);
        let actions = self.rules.iter().find(|r| r.matcher.matches(&window)).map(|r| r.actions.clone());
        // See `Window::rules_applied`'s doc comment: a native Wayland window
        // still has empty title/app_id at this point, so a real (if
        // inconclusive) match attempt needs to wait for `reapply_rules_if_pending`.
        window.rules_applied = actions.is_some() || !(window.title.is_empty() && window.app_id.is_empty());

        let workspace = actions.as_ref().and_then(|a| a.workspace).unwrap_or(self.current_workspace);
        window.workspace = workspace;
        if let Some(a) = &actions {
            if let Some(floating) = a.floating {
                window.floating = floating;
            }
            if let Some(decorated) = a.decorated {
                window.decorated = decorated;
            }
            if let Some(color) = a.border_color {
                window.border_color = color;
            }
            if let Some(width) = a.border_width {
                window.border_width = width;
            }
            if let Some(radius) = a.corner_radius {
                window.corner_radius = radius;
            }
            if let Some(pinned) = a.pinned {
                window.always_on_top = pinned;
            }
            if let Some(opacity) = a.opacity {
                window.opacity = opacity.clamp(0.0, 1.0);
            }
            if let Some(margin) = a.resize_margin {
                window.resize_margin = Some(margin);
            }
        }

        // A remembered size (`remembered_sizes`' own doc comment) wins over
        // whatever fixed default a backend hardcoded into `window.geometry`
        // before calling this - but a rule's explicit `geometry` action
        // below still wins over *this*, since that's a deliberate per-app
        // override, more specific than "whatever I last resized this app
        // to". Clamped to the same minimums a live resize itself can never
        // go below, so a corrupted/stale entry can't hand a new window a
        // degenerate size.
        if !window.app_id.is_empty() {
            if let Some(&(w, h)) = self.remembered_sizes.get(&window.app_id) {
                window.geometry.width = w.max(MIN_WINDOW_WIDTH);
                window.geometry.height = h.max(MIN_WINDOW_HEIGHT);
            }
        }
        // Every new window used to land on the *primary* monitor
        // unconditionally, regardless of which monitor the user was
        // actually working on - reported live as "why do all windows only
        // open on the first monitor" once a second, non-primary monitor
        // was actually in use. Placing on the *focused* window's monitor
        // instead matches every mainstream desktop's own convention (a new
        // window opens where you're currently working, not wherever
        // "primary" happens to be), and needs no new state: `self.focused`
        // already exists for exactly this kind of "what's the user looking
        // at right now" question.
        //
        // Falling all the way back to the primary monitor whenever nothing
        // is focused was still wrong for a second, later-reported case:
        // nothing focused *on the monitor the user is actually at* - an
        // empty desktop there, or the last-focused window happening to sit
        // on a different monitor than the one just clicked/hovered before
        // launching something new - landed the new window on primary
        // regardless of which monitor was genuinely in use. `pointer_
        // monitor` (see its own doc comment) is a second, better fallback
        // for exactly that gap, checked before giving up to primary
        // entirely - which stays the last resort for the one case neither
        // signal can answer, a fresh session's very first window before any
        // pointer motion has been reported at all.
        let target_monitor = self
            .focused
            .and_then(|id| self.windows.get(&id))
            .and_then(|w| self.monitors.iter().find(|m| m.id == w.monitor))
            .or_else(|| self.pointer_monitor.and_then(|id| self.monitors.iter().find(|m| m.id == id)))
            .or_else(|| self.primary_monitor());
        if let Some(monitor) = target_monitor {
            window.monitor = monitor.id;
            let layout_name = self.workspace(workspace).map(|w| w.layout.clone()).unwrap_or_default();
            if layout_name != "tiling" {
                let existing: Vec<Rect> = self.windows_on_workspace(workspace).map(|w| w.geometry).collect();
                let size = (window.geometry.width, window.geometry.height);
                window.geometry = SmartPlacement::place(monitor, &existing, size, &self.placement);
            }
        }
        if let Some(geometry) = actions.as_ref().and_then(|a| a.geometry) {
            window.geometry = geometry;
        }
        let maximize = actions.as_ref().and_then(|a| a.maximized).unwrap_or(false);

        self.windows.insert(id, window);
        self.order.push(id);
        self.focused = Some(id);
        // A new window goes on top, but must not cover a pinned one.
        self.restack_pinned();
        if maximize {
            self.toggle_maximize(id);
        }
        id
    }

    /// Retries rule matching for a window `add_window` couldn't conclusively
    /// match yet (see `Window::rules_applied`'s doc comment) - a backend
    /// calls this once a native Wayland window's real `title`/`app_id`
    /// become known, typically on its first real commit. A no-op once
    /// `rules_applied` is already `true`, so this is safe to call on every
    /// subsequent metadata change without rules re-applying repeatedly.
    ///
    /// Returns whether a rule actually matched and was applied - distinct
    /// from simply "ran" (this is a no-op past the first call regardless).
    /// A backend uses this to decide whether a follow-up geometry/decoration
    /// sync is warranted: `sync_geometry` re-stacks the window to the top
    /// via smithay's `Space::map_element` as a side effect of updating its
    /// tracked position (`map_element` always does this, `activate` or
    /// not - there is no "move without restacking" in this smithay
    /// version), so calling it on *every* title/app_id change - which
    /// happens constantly for perfectly ordinary reasons (a browser tab
    /// finishing a page load) long after the window's own creation - would
    /// silently yank an unfocused, unrelated window back to the front any
    /// time its title happened to update. Reported live as exactly that:
    /// an older window jumping in front of a newer, focused one with no
    /// user action to explain it.
    pub fn reapply_rules_if_pending(&mut self, id: WindowId) -> bool {
        let Some(window) = self.windows.get(&id) else { return false };
        if window.rules_applied || (window.title.is_empty() && window.app_id.is_empty()) {
            return false;
        }
        let actions = self.rules.iter().find(|r| r.matcher.matches(window)).map(|r| r.actions.clone());
        let Some(window) = self.windows.get_mut(&id) else { return false };
        window.rules_applied = true;
        // `add_window`'s matching fallback only ever sees this once
        // `app_id` is actually known - for a native Wayland window that's
        // usually after creation (`set_app_id` lands later), which is
        // exactly why this needs its own check here too, not just there.
        // A rule's own `decorated` action, if any, still wins below.
        let mut heuristic_changed = false;
        if actions.as_ref().and_then(|a| a.decorated).is_none() && window.decorated && likely_draws_own_titlebar(&window.app_id) {
            window.decorated = false;
            heuristic_changed = true;
        }
        let Some(actions) = actions else { return heuristic_changed };
        if let Some(floating) = actions.floating {
            window.floating = floating;
        }
        if let Some(decorated) = actions.decorated {
            window.decorated = decorated;
        }
        if let Some(color) = actions.border_color {
            window.border_color = color;
        }
        if let Some(width) = actions.border_width {
            window.border_width = width;
        }
        if let Some(pinned) = actions.pinned {
            window.always_on_top = pinned;
        }
        if let Some(opacity) = actions.opacity {
            window.opacity = opacity.clamp(0.0, 1.0);
        }
        if let Some(margin) = actions.resize_margin {
            window.resize_margin = Some(margin);
        }
        if let Some(geometry) = actions.geometry {
            window.geometry = geometry;
        }
        if let Some(workspace) = actions.workspace {
            self.move_window_to_workspace(id, workspace);
        }
        if actions.maximized.unwrap_or(false) {
            self.toggle_maximize(id);
        }
        true
    }

    pub fn remove_window(&mut self, id: WindowId) -> Option<Window> {
        self.order.retain(|&w| w != id);
        if self.focused == Some(id) {
            self.focused = self.order.last().copied();
        }
        self.windows.remove(&id)
    }

    pub fn window(&self, id: WindowId) -> Option<&Window> {
        self.windows.get(&id)
    }

    pub fn window_mut(&mut self, id: WindowId) -> Option<&mut Window> {
        self.windows.get_mut(&id)
    }

    pub fn windows(&self) -> impl Iterator<Item = &Window> {
        self.windows.values()
    }

    /// Windows in stacking order, topmost (most recently raised) last.
    pub fn stacking_order(&self) -> impl Iterator<Item = &Window> {
        self.order.iter().filter_map(|id| self.windows.get(id))
    }

    pub(super) fn windows_on_workspace(&self, workspace: WorkspaceId) -> impl Iterator<Item = &Window> {
        self.windows.values().filter(move |w| w.workspace == workspace)
    }

    pub fn raise_window(&mut self, id: WindowId) {
        if let Some(pos) = self.order.iter().position(|&w| w == id) {
            let id = self.order.remove(pos);
            self.order.push(id);
        }
        self.restack_pinned();
    }

    /// Sends a window to the back of the stack - the middle-click-titlebar
    /// convention most X11 WMs (twm, fvwm, IceWM) have always had and this
    /// one never did. Doesn't touch focus: lowering the window you're
    /// currently looking at out from under the pointer without also moving
    /// keyboard focus elsewhere would leave input going to a window that's
    /// no longer visible under the cursor, which is more surprising than
    /// useful. `restack_pinned` still runs afterward so a pinned window
    /// can't accidentally end up buried by this either.
    pub fn lower_window(&mut self, id: WindowId) {
        if let Some(pos) = self.order.iter().position(|&w| w == id) {
            let id = self.order.remove(pos);
            self.order.insert(0, id);
        }
        self.restack_pinned();
    }

    /// Toggles "always on top" for a window (Hyprland's `pin`), used for
    /// picture-in-picture and small HUD overlays that must stay visible
    /// while you work in something else.
    pub fn toggle_always_on_top(&mut self, id: WindowId) {
        if let Some(w) = self.windows.get_mut(&id) {
            w.always_on_top = !w.always_on_top;
        }
        self.restack_pinned();
    }

    pub fn is_always_on_top(&self, id: WindowId) -> bool {
        self.windows.get(&id).map(|w| w.always_on_top).unwrap_or(false)
    }

    /// Moves every always-on-top window to the top of the stack, keeping
    /// their relative order.
    ///
    /// `order` is the stacking order (last = topmost), so pinning is not a
    /// property the renderer checks - it is maintained here, which means
    /// every existing consumer of `stacking_order` gets it for free and
    /// cannot forget to honour it.
    fn restack_pinned(&mut self) {
        if !self.windows.values().any(|w| w.always_on_top) {
            return;
        }
        let (pinned, rest): (Vec<_>, Vec<_>) = self
            .order
            .iter()
            .partition(|id| self.windows.get(id).is_some_and(|w| w.always_on_top));
        self.order = rest.into_iter().chain(pinned).collect();
    }

}