diff options
| author | srdusr <[email protected]> | 2024-05-29 14:40:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2024-05-29 14:40:00 +0200 |
| commit | 3d3057ae384ef7389284af8988410889e99c6bb9 (patch) | |
| tree | 57b05e9660afd8ed51b26e5db1ab541a811ccc16 /crates/core/src | |
| parent | dd31bf5ac2a692617d478478d36c733586b93cf8 (diff) | |
| download | srdwm-3d3057ae384ef7389284af8988410889e99c6bb9.tar.gz srdwm-3d3057ae384ef7389284af8988410889e99c6bb9.zip | |
Config at ~/.config/srd; cursor shapes, key repeat, pin, mouse defaults
Config path drops a level: ~/.config/srd, not ~/.config/srdwm/srd, which
said the same thing twice. No other user-facing path had the same problem --
srdwm reads the config dir and writes nothing else.
Cursor shapes. A client's own cursor surface is now rendered with the
hotspot it declared, so an I-beam over text or a hand over a link shows the
app's image instead of srdwm's arrow. The built-in arrow stays as the
fallback when no client has set one, over decorations and the desktop.
Named shapes still fall back to the arrow; most toolkits set a surface.
Decorations and cursors now share one OverlayElement type, since
render_output takes a single custom-element slice.
Key repeat (srd.bind_repeat, Hyprland's binde). Held volume, brightness and
switcher keys repeat at the seat's own rate rather than firing once. Driven
from the poll loop, not a timer source: the winit backend has no calloop
loop of its own, and poll_events already runs continuously in both backends.
Repeat stops when *that* key is released, not when any key is.
Always-on-top / pin, for the picture-in-picture and HUD rules that used it.
Window::always_on_top was another declared-but-never-read field. Enforced in
WindowManager's stacking order rather than at render time, so every consumer
of stacking_order gets it and none can forget to honour it.
Mouse-only window management, checked end to end: drag the titlebar to move,
drag any edge or corner to resize, titlebar buttons to close/maximise/
minimise, click to focus, drag to a screen edge to snap, and now
double-click the titlebar to maximise. The resize grab band went from 6px to
10px - a hairline is genuinely hard to hit with a mouse, which is why
Hyprland ships extend_border_grab_area.
Also removed the emoji status markers from docs/IMPLEMENTATION_STATUS.md.
Diffstat (limited to 'crates/core/src')
| -rw-r--r-- | crates/core/src/manager.rs | 89 | ||||
| -rw-r--r-- | crates/core/src/rules.rs | 2 | ||||
| -rw-r--r-- | crates/core/src/window.rs | 11 |
3 files changed, 100 insertions, 2 deletions
diff --git a/crates/core/src/manager.rs b/crates/core/src/manager.rs index 23d94be..3856a25 100644 --- a/crates/core/src/manager.rs +++ b/crates/core/src/manager.rs @@ -192,6 +192,9 @@ impl WindowManager { if let Some(width) = a.border_width { window.border_width = width; } + if let Some(pinned) = a.pinned { + window.always_on_top = pinned; + } } if let Some(monitor) = self.primary_monitor() { @@ -211,6 +214,8 @@ impl WindowManager { 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); } @@ -251,6 +256,39 @@ impl WindowManager { let id = self.order.remove(pos); self.order.push(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(); } // ---- Focus ---------------------------------------------------------- @@ -1216,4 +1254,55 @@ mod tests { "the two windows should have traded places in the stack" ); } + + // ---- Always on top --------------------------------------------------- + + #[test] + fn pinned_windows_stay_above_newly_raised_ones() { + let mut wm = wm_with_monitor(); + let pinned = wm.alloc_window_id(); + wm.add_window(Window::new(pinned, "pip")); + let other = wm.alloc_window_id(); + wm.add_window(Window::new(other, "normal")); + + wm.toggle_always_on_top(pinned); + assert!(wm.is_always_on_top(pinned)); + assert_eq!(wm.stacking_order().last().map(|w| w.id), Some(pinned)); + + // Raising a normal window must not bury the pinned one. + wm.raise_window(other); + assert_eq!( + wm.stacking_order().last().map(|w| w.id), + Some(pinned), + "pinned window must remain topmost after another is raised" + ); + } + + #[test] + fn a_new_window_does_not_cover_a_pinned_one() { + let mut wm = wm_with_monitor(); + let pinned = wm.alloc_window_id(); + wm.add_window(Window::new(pinned, "pip")); + wm.toggle_always_on_top(pinned); + + let fresh = wm.alloc_window_id(); + wm.add_window(Window::new(fresh, "just opened")); + + assert_eq!(wm.stacking_order().last().map(|w| w.id), Some(pinned)); + } + + #[test] + fn unpinning_lets_a_window_fall_back_into_the_normal_stack() { + let mut wm = wm_with_monitor(); + let a = wm.alloc_window_id(); + wm.add_window(Window::new(a, "a")); + let b = wm.alloc_window_id(); + wm.add_window(Window::new(b, "b")); + + wm.toggle_always_on_top(a); + assert_eq!(wm.stacking_order().last().map(|w| w.id), Some(a)); + wm.toggle_always_on_top(a); + wm.raise_window(b); + assert_eq!(wm.stacking_order().last().map(|w| w.id), Some(b)); + } } diff --git a/crates/core/src/rules.rs b/crates/core/src/rules.rs index 4d881a5..642179d 100644 --- a/crates/core/src/rules.rs +++ b/crates/core/src/rules.rs @@ -47,6 +47,8 @@ pub struct WindowRuleActions { pub decorated: Option<bool>, pub border_color: Option<(u8, u8, u8)>, pub border_width: Option<u32>, + /// Always-on-top (Hyprland's `pin`). + pub pinned: Option<bool>, } #[derive(Debug, Clone, Default)] diff --git a/crates/core/src/window.rs b/crates/core/src/window.rs index 3970b02..9484709 100644 --- a/crates/core/src/window.rs +++ b/crates/core/src/window.rs @@ -50,8 +50,15 @@ impl Window { /// The height, in pixels, of the drawn title bar. Shared between backends so /// hit-testing and rendering agree on the same band. pub const TITLEBAR_HEIGHT: u32 = 30; -/// Width of a resize grab margin along each window edge. -pub const RESIZE_MARGIN: i32 = 6; +/// Width of the resize grab band along each window edge. +/// +/// 10px rather than a hairline: this is grabbed with a mouse, and a border +/// only a couple of pixels wide is genuinely hard to hit - which is why +/// Hyprland ships `extend_border_grab_area` and why every desktop widens +/// this beyond the visible border. The band is inside the window, so it +/// costs a few pixels of client edge; that is the right trade for making +/// resize reliably grabbable without a keyboard. +pub const RESIZE_MARGIN: i32 = 10; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ResizeEdge { |