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/wayland/src/state.rs | |
| 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/wayland/src/state.rs')
| -rw-r--r-- | crates/wayland/src/state.rs | 94 |
1 files changed, 93 insertions, 1 deletions
diff --git a/crates/wayland/src/state.rs b/crates/wayland/src/state.rs index a1704cb..bea3453 100644 --- a/crates/wayland/src/state.rs +++ b/crates/wayland/src/state.rs @@ -14,7 +14,7 @@ use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::rc::Rc; -use std::time::Instant; +use std::time::{Duration, Instant}; use smithay::backend::allocator::Fourcc; use smithay::backend::renderer::element::memory::MemoryRenderBuffer; @@ -127,12 +127,18 @@ pub(crate) struct CompState { /// Bitmap for the built-in arrow, built once at startup rather than /// per frame. pub(crate) cursor_buffer: MemoryRenderBuffer, + /// Last titlebar press, for double-click detection. + pub(crate) last_titlebar_click: Option<(WindowId, u32)>, pub(crate) wm: Rc<RefCell<WindowManager>>, pub(crate) surface_to_id: HashMap<WlSurface, WindowId>, pub(crate) id_to_window: HashMap<WindowId, DWindow>, pub(crate) decorations: HashMap<WindowId, MemoryRenderBuffer>, pub(crate) pending: Rc<RefCell<Vec<CoreEvent>>>, pub(crate) bound_keys: Rc<HashSet<String>>, + /// Combos that repeat while held (`srd.bind_repeat`). + pub(crate) repeat_keys: Rc<HashSet<String>>, + /// The binding currently held down and repeating, if any. + pub(crate) repeat: Option<RepeatState>, pub(crate) start_time: Instant, /// `Some` only for the udev/DRM backend; see `udev.rs` module docs for /// why its runtime state lives here rather than on a separate struct. @@ -147,6 +153,61 @@ pub(crate) struct CompState { pub(crate) xwayland_pending: Vec<smithay::xwayland::X11Surface>, } +/// A held keybinding that is firing repeatedly. +/// +/// Driven from the poll loop rather than a timer source: the winit backend +/// has no `calloop` loop of its own, and `poll_events` already runs +/// continuously in both backends, so this works the same in each. +pub(crate) struct RepeatState { + /// Which physical key is held - repeat stops when *this* key is + /// released, not when any key is. + pub(crate) keycode: smithay::input::keyboard::Keycode, + pub(crate) key_name: String, + pub(crate) modifiers: srdwm_core::Modifiers, + pub(crate) next_fire: Instant, +} + +/// Matches the seat's own repeat settings (`add_keyboard(.., 200, 25)`), so +/// held bindings feel the same as held keys in a text field. +const REPEAT_DELAY: Duration = Duration::from_millis(200); +const REPEAT_INTERVAL: Duration = Duration::from_millis(1000 / 25); + +impl CompState { + /// Starts repeating `combo` if it was registered with `srd.bind_repeat`. + pub(crate) fn begin_repeat(&mut self, keycode: smithay::input::keyboard::Keycode, key_name: &str, modifiers: srdwm_core::Modifiers) { + let combo = srdwm_core::key_combo_string(modifiers, key_name); + if !self.repeat_keys.contains(&combo) { + return; + } + self.repeat = Some(RepeatState { + keycode, + key_name: key_name.to_string(), + modifiers, + next_fire: Instant::now() + REPEAT_DELAY, + }); + } + + /// Stops repeating when the held key is released. + pub(crate) fn end_repeat(&mut self, keycode: smithay::input::keyboard::Keycode) { + if self.repeat.as_ref().is_some_and(|r| r.keycode == keycode) { + self.repeat = None; + } + } + + /// Emits another `KeyPress` if the held binding is due. Called once per + /// poll from both backends. + pub(crate) fn tick_repeat(&mut self) { + let Some(repeat) = self.repeat.as_mut() else { return }; + let now = Instant::now(); + if now < repeat.next_fire { + return; + } + repeat.next_fire = now + REPEAT_INTERVAL; + let (key_name, modifiers) = (repeat.key_name.clone(), repeat.modifiers); + self.pending.borrow_mut().push(CoreEvent::KeyPress { key_name, modifiers }); + } +} + /// Titlebar background is the same regardless of focus (matching the X11 /// backend); only the title text color changes. const TITLEBAR_BG: (u8, u8, u8) = (0x2e, 0x34, 0x40); @@ -219,6 +280,8 @@ impl CompState { // real Wayland keyboard/selection focus. (Same class of bug as the // click-to-focus one fixed earlier; this is the creation path.) self.set_keyboard_focus(Some(surface)); + // A newly-mapped window goes on top, but not over a pinned one. + self.raise_pinned(); self.pending.borrow_mut().push(CoreEvent::WindowCreated(id)); } @@ -339,6 +402,35 @@ impl CompState { keyboard.set_focus(self, surface, serial); } + /// True when this titlebar press is the second of a double-click on the + /// same window. Threshold is the usual 400ms. + pub(crate) fn is_double_click(&mut self, id: WindowId, time: u32) -> bool { + const DOUBLE_CLICK_MS: u32 = 400; + let doubled = match self.last_titlebar_click { + Some((last_id, last_time)) => last_id == id && time.saturating_sub(last_time) <= DOUBLE_CLICK_MS, + None => false, + }; + // Reset after a double, so a third click starts a fresh pair rather + // than counting as another double. + self.last_titlebar_click = if doubled { None } else { Some((id, time)) }; + doubled + } + + /// Re-raises always-on-top windows in the `Space`. + /// + /// `WindowManager` keeps pinned windows last in its own stacking order, + /// but the `Space` has an order of its own that decides what actually + /// draws on top - so pinning is only real once it is pushed here. + /// Called after anything that raises a window. + pub(crate) fn raise_pinned(&mut self) { + let pinned: Vec<WindowId> = self.wm.borrow().stacking_order().filter(|w| w.always_on_top).map(|w| w.id).collect(); + for id in pinned { + if let Some(w) = self.id_to_window.get(&id).cloned() { + self.space.raise_element(&w, false); + } + } + } + pub(crate) fn sync_geometry(&mut self, id: WindowId) { let Some(geom) = self.wm.borrow().window(id).map(|w| w.geometry) else { return }; if let Some(w) = self.id_to_window.get(&id) { |