From 3d3057ae384ef7389284af8988410889e99c6bb9 Mon Sep 17 00:00:00 2001 From: srdusr <99972264+srdusr@users.noreply.github.com> Date: Wed, 29 May 2024 14:40:00 +0200 Subject: 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. --- crates/config/src/lib.rs | 70 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) (limited to 'crates/config/src') diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index ec8c7e8..6a0eeea 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -27,6 +27,9 @@ struct SharedState { wm: Rc>, values: HashMap, key_bindings: HashMap, + /// Combos registered with `srd.bind_repeat`, which fire repeatedly while + /// held (Hyprland's `binde`). A subset of `key_bindings`. + repeat_keys: std::collections::HashSet, /// Handlers for non-key events (currently the lid switch), registered /// via `srd.on(...)`. Kept separate from `key_bindings` because the /// backends use that map to decide which *keypresses* to withhold from @@ -65,6 +68,7 @@ impl Engine { wm, values: default_config(), key_bindings: HashMap::new(), + repeat_keys: std::collections::HashSet::new(), event_handlers: HashMap::new(), config_dir: config_dir.into(), log: Vec::new(), @@ -154,6 +158,11 @@ impl Engine { self.state.borrow().key_bindings.keys().cloned().collect() } + /// Combos that should auto-repeat while held. + pub fn repeat_keys(&self) -> Vec { + self.state.borrow().repeat_keys.iter().cloned().collect() + } + fn register_srd_module(&self) -> Result<()> { let lua = &self.lua; let srd = lua.create_table()?; @@ -164,6 +173,7 @@ impl Engine { srd.set("reset_all", self.fn_reset_all()?)?; srd.set("reset_category", self.fn_reset_category()?)?; srd.set("bind", self.fn_bind()?)?; + srd.set("bind_repeat", self.fn_bind_repeat()?)?; srd.set("on", self.fn_on()?)?; srd.set("rule", self.fn_rule()?)?; srd.set("load", self.fn_load()?)?; @@ -186,6 +196,7 @@ impl Engine { window.set("minimize", self.fn_window_action(WindowAction::Minimize)?)?; window.set("maximize", self.fn_window_action(WindowAction::Maximize)?)?; window.set("fullscreen", self.fn_window_action(WindowAction::Fullscreen)?)?; + window.set("toggle_pin", self.fn_window_action(WindowAction::TogglePin)?)?; window.set("focus", self.fn_window_focus_direction()?)?; window.set("move", self.fn_window_move_direction()?)?; window.set("next", self.fn_window_cycle(true)?)?; @@ -316,6 +327,20 @@ impl Engine { })?) } + /// `srd.bind_repeat(combo, fn)` - like `srd.bind`, but keeps firing + /// while the key is held (Hyprland's `binde`). For volume, brightness + /// and window-switcher cycling, where one step per press is unusable. + fn fn_bind_repeat(&self) -> Result> { + let state = self.state.clone(); + Ok(self.lua.create_function(move |lua, (combo, f): (String, mlua::Function)| { + let key = lua.create_registry_value(f)?; + let mut s = state.borrow_mut(); + s.repeat_keys.insert(combo.clone()); + s.key_bindings.insert(combo, key); + Ok(()) + })?) + } + fn fn_bind(&self) -> Result> { let state = self.state.clone(); Ok(self.lua.create_function(move |lua, (combo, f): (String, mlua::Function)| { @@ -363,6 +388,7 @@ impl Engine { decorated: actions.get("decorated")?, border_color, border_width: actions.get("border_width")?, + pinned: actions.get("pinned")?, }, }; state.borrow().wm.borrow_mut().add_rule(rule); @@ -524,6 +550,7 @@ impl Engine { WindowAction::Maximize => wm.toggle_maximize(id), WindowAction::Fullscreen => wm.toggle_fullscreen(id), WindowAction::ToggleFloating => wm.toggle_floating(id), + WindowAction::TogglePin => wm.toggle_always_on_top(id), } } Ok(()) @@ -737,6 +764,7 @@ enum WindowAction { Maximize, Fullscreen, ToggleFloating, + TogglePin, } /// Recursively flattens a Lua table into dotted config keys, e.g. @@ -1148,4 +1176,46 @@ mod tests { engine.load_init().unwrap(); assert_eq!(engine.get("general.window_gap"), Some(ConfigValue::Number(4.0))); } + + #[test] + fn bind_repeat_registers_the_binding_and_marks_it_repeating() { + let dir = tempfile::tempdir().unwrap(); + let engine = engine_in(dir.path()); + engine + .lua + .load(r#" + srd.bind("Mod4+a", function() end) + srd.bind_repeat("XF86AudioRaiseVolume", function() end) + "#) + .exec() + .unwrap(); + + let bound = engine.bound_keys(); + // A repeating bind is still a normal binding - it must be grabbed + // and dispatched like any other, or it would never fire at all. + assert!(bound.contains(&"Mod4+a".to_string())); + assert!(bound.contains(&"XF86AudioRaiseVolume".to_string())); + + let repeat = engine.repeat_keys(); + assert_eq!(repeat, vec!["XF86AudioRaiseVolume".to_string()]); + assert!(!repeat.contains(&"Mod4+a".to_string()), "a plain bind must not repeat"); + } + + #[test] + fn bind_repeat_dispatches_like_a_normal_binding() { + let dir = tempfile::tempdir().unwrap(); + let engine = engine_in(dir.path()); + engine + .lua + .load(r#" + fired = 0 + srd.bind_repeat("Mod4+z", function() fired = fired + 1 end) + "#) + .exec() + .unwrap(); + assert!(engine.dispatch_keybinding("Mod4+z")); + assert!(engine.dispatch_keybinding("Mod4+z")); + let fired: i64 = engine.lua.globals().get("fired").unwrap(); + assert_eq!(fired, 2); + } } -- cgit v1.2.3