srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/core/src/manager/winops.rs
blob: e656298cc9f15f922d890b699b15656173950f0e (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
//! Per-window lifecycle: close, minimize, restore, scratchpad, maximize, fullscreen, floating, and plain move/resize.
//! 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 {
    // ---- Window operations ----------------------------------------------

    pub fn close_window(&mut self, id: WindowId) {
        log::info!("close_window({id})");
        self.close_requests.push(id);
    }

    /// Drains windows queued by `close_window` since the last call. Core
    /// has no way to reach a client itself - the caller (`main.rs`) is
    /// expected to forward each id to `Platform::close`.
    pub fn take_close_requests(&mut self) -> Vec<WindowId> {
        std::mem::take(&mut self.close_requests)
    }

    pub fn minimize_window(&mut self, id: WindowId) {
        if let Some(w) = self.windows.get_mut(&id) {
            w.minimized = true;
        }
        if self.focused == Some(id) {
            self.cycle_focus(true);
        }
    }

    pub fn restore_window(&mut self, id: WindowId) {
        if let Some(w) = self.windows.get_mut(&id) {
            w.minimized = false;
        }
    }

    /// Moves a window into the scratchpad pool, hiding it immediately --
    /// sway's `move scratchpad`. The single most-used "quick terminal"
    /// pattern in tiling window managers, and srdwm had no equivalent at
    /// all before this.
    ///
    /// Also floats the window: tiling something that's meant to pop in and
    /// out on demand doesn't make sense, and would otherwise fight
    /// `arrange_workspace` every time it's shown. Reuses `minimized` for
    /// the actual show/hide gating rather than introducing a second
    /// visibility flag - `scratchpad` here is purely a marker of *pool
    /// membership*, kept separate so `scratchpad_show` knows which hidden
    /// windows are its own to bring back, as opposed to an ordinarily
    /// minimized one.
    pub fn scratchpad_add(&mut self, id: WindowId) {
        if let Some(w) = self.windows.get_mut(&id) {
            w.scratchpad = true;
            w.floating = true;
        }
        self.minimize_window(id);
    }

    /// Removes a window from the scratchpad pool without changing its
    /// current visibility - for a rule or script that wants to opt a
    /// window back into ordinary window management.
    pub fn scratchpad_remove(&mut self, id: WindowId) {
        if let Some(w) = self.windows.get_mut(&id) {
            w.scratchpad = false;
        }
    }

    /// Toggles the scratchpad - sway's `scratchpad show`, meant for one
    /// keybinding a user presses repeatedly. If the focused window is
    /// itself a currently-shown scratchpad window, hides it; otherwise
    /// shows (and focuses) the most recently added hidden scratchpad
    /// window, if any, moving it onto whichever workspace is current so it
    /// follows the user rather than staying pinned to wherever it was
    /// added from - sway's own behavior. "Most recently added" is `id`
    /// order, since ids are allocated monotonically and no separate
    /// timestamp is tracked; only ever one window is shown/hidden per
    /// call, deliberately not sway's full multi-window cycling, which
    /// needs its own remembered order and is a rarer need than a single
    /// scratchpad window covers.
    pub fn scratchpad_show(&mut self) {
        if let Some(id) = self.focused {
            if self.windows.get(&id).is_some_and(|w| w.scratchpad && !w.minimized) {
                self.minimize_window(id);
                return;
            }
        }
        let Some(id) = self.windows.values().filter(|w| w.scratchpad && w.minimized).map(|w| w.id).max() else { return };
        if let Some(w) = self.windows.get_mut(&id) {
            w.workspace = self.current_workspace;
        }
        self.restore_window(id);
        self.focus_window(id);
    }

    pub fn toggle_maximize(&mut self, id: WindowId) {
        let monitor_geom = self.windows.get(&id).and_then(|w| self.monitor_for(w.monitor)).map(|m| m.geometry);
        let animations_enabled = self.animations_enabled;
        let Some(w) = self.windows.get_mut(&id) else { return };
        let from = w.geometry;
        if w.maximized {
            if let Some(restore) = w.restore_geometry.take() {
                w.geometry = restore;
            }
            w.maximized = false;
        } else if let Some(geom) = monitor_geom {
            w.restore_geometry = Some(w.geometry);
            w.geometry = geom;
            w.maximized = true;
        }
        if animations_enabled && w.geometry != from {
            w.anim_from = Some(from);
        }
    }

    /// Fullscreen: the window covers its whole monitor with no decoration.
    ///
    /// Distinct from [`Self::toggle_maximize`], which keeps the titlebar (and
    /// is what a maximise button does). Both share `restore_geometry`, so
    /// they are mutually exclusive - toggling one off restores whatever the
    /// window's geometry was before *either* was applied, and entering
    /// fullscreen from a maximised window doesn't lose the original size.
    ///
    /// `decorated` is saved and restored the same way, via
    /// `restore_decorated` - exiting used to hardcode `w.decorated = true`
    /// unconditionally, which is only correct for a window that was
    /// decorated to begin with. Any window a rule sets `decorated = false`
    /// for (client-side-decorated apps like Firefox, matched via
    /// `srd.rule({ class = "firefox" }, { decorated = false })`) that ever
    /// goes fullscreen - an HTML5 video, a PDF presentation, plain F11 --
    /// came back from it permanently `decorated = true`, with no further
    /// event to ever set it back. Since border/titlebar redraw fresh from
    /// live `Window.decorated` every frame but the *hit-testing* band this
    /// wrongly turned on doesn't correspond to anything the client is
    /// actually drawing there, every click in what srdwm now (incorrectly)
    /// treats as the titlebar band got swallowed as a drag/button hit
    /// instead of ever reaching the client - reported live as a click on
    /// Firefox's back button minimizing the window instead.
    pub fn toggle_fullscreen(&mut self, id: WindowId) {
        // Unlike `toggle_maximize`, fullscreen uses the monitor's true
        // full rect, not the exclusive-zone-shrunk usable area - a
        // fullscreen window should cover (or go under) a bar/dock like
        // everywhere else, not stop short of it. See `Monitor::
        // full_geometry`'s doc comment.
        let monitor_geom = self.windows.get(&id).and_then(|w| self.monitor_for(w.monitor)).map(|m| m.full_geometry);
        let animations_enabled = self.animations_enabled;
        let Some(w) = self.windows.get_mut(&id) else { return };
        let from = w.geometry;
        if w.fullscreen {
            if let Some(restore) = w.restore_geometry.take() {
                w.geometry = restore;
            }
            w.fullscreen = false;
            w.decorated = w.restore_decorated.take().unwrap_or(true);
        } else if let Some(geom) = monitor_geom {
            // Only remember the pre-fullscreen geometry if we aren't already
            // maximised, otherwise the monitor rect would overwrite the real
            // restore point and the window could never get its size back.
            if !w.maximized {
                w.restore_geometry = Some(w.geometry);
            }
            w.maximized = false;
            w.geometry = geom;
            w.fullscreen = true;
            w.restore_decorated = Some(w.decorated);
            w.decorated = false;
        }
        if animations_enabled && w.geometry != from {
            w.anim_from = Some(from);
        }
    }

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

    pub fn toggle_floating(&mut self, id: WindowId) {
        if let Some(w) = self.windows.get_mut(&id) {
            w.floating = !w.floating;
        }
    }

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

    pub fn move_window(&mut self, id: WindowId, x: i32, y: i32) {
        if let Some(w) = self.windows.get_mut(&id) {
            w.geometry.x = x;
            w.geometry.y = y;
        }
    }

    pub fn resize_window(&mut self, id: WindowId, width: u32, height: u32) {
        if let Some(w) = self.windows.get_mut(&id) {
            w.geometry.width = width.max(MIN_WINDOW_WIDTH);
            w.geometry.height = height.max(MIN_WINDOW_HEIGHT);
        }
    }

}