diff options
Diffstat (limited to 'crates/core/src/manager/winops.rs')
| -rw-r--r-- | crates/core/src/manager/winops.rs | 200 |
1 files changed, 200 insertions, 0 deletions
diff --git a/crates/core/src/manager/winops.rs b/crates/core/src/manager/winops.rs new file mode 100644 index 0000000..e656298 --- /dev/null +++ b/crates/core/src/manager/winops.rs @@ -0,0 +1,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); + } + } + +} |