srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/core/src/event.rs
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-05-30 16:10:00 +0200
committersrdusr <[email protected]>2024-05-30 16:10:00 +0200
commit1c175642d073689ca11b9252411ea5f8446007d0 (patch)
tree00b8f9062cf1261d2d2642c059a14ff5790522e5 /crates/core/src/event.rs
parent3d3057ae384ef7389284af8988410889e99c6bb9 (diff)
downloadsrdwm-1c175642d073689ca11b9252411ea5f8446007d0.tar.gz
srdwm-1c175642d073689ca11b9252411ea5f8446007d0.zip
Fix decoration drift during animated transitions; checkpoint IPC/global-menu/output-management work
Border/titlebar decoration was built from Window.geometry (the animation's final target) in both wayland backends' render loops, while sync_geometry already draws a window's actual content at window_anims' interpolated rect during any maximize/fullscreen/open-slide tween. Border and content read two different rectangles for the whole transition, so the border visibly detached from the window it was outlining - reported as "borders aren't flush." Both udev.rs and winit.rs now read the same animated rect for titlebar placement, border-strip placement, and the occlusion test against later windows in stacking order. Verified: cargo build --workspace, cargo clippy (0 new warnings), cargo test -p srdwm-core (111/111). Also checkpoints substantial protocol/IPC work from prior sessions that had accumulated uncommitted: gtk-shell1 support (gtk_shell.rs, the vendored gtk-shell.xml, xwayland.rs's X11-side mirror) backing the global app menu; zwlr_foreign_toplevel_manager_v1 (foreign_toplevel.rs) broadcasting distinct maximized/minimized/fullscreen/activated state per window; output_management (ext-output-management + layer-shell exclusive-zone reservation tracking); workspace.rs and context_menu.rs; a Unix-socket IPC crate (platform/src/ ipc.rs) and a `srd` control-CLI crate (crates/ctl); xkb_config.rs; and a theme module (core/src/theme.rs). A peer session working the AGS shell concurrently verified several of these live against a running srdwm: the global menu rendering a real app's File/Edit menu over gtk-shell1, and foreign-toplevel correctly reporting maximized and fullscreen as independent, non-simultaneous states with the geometry each implies (maximize stops at a reserved top bar and past a dock; fullscreen reaches the true monitor edge).
Diffstat (limited to 'crates/core/src/event.rs')
-rw-r--r--crates/core/src/event.rs110
1 files changed, 110 insertions, 0 deletions
diff --git a/crates/core/src/event.rs b/crates/core/src/event.rs
index eb2d8aa..5822d3f 100644
--- a/crates/core/src/event.rs
+++ b/crates/core/src/event.rs
@@ -44,6 +44,53 @@ pub fn key_combo_string(modifiers: Modifiers, key_name: &str) -> String {
format!("{modifiers}{key_name}")
}
+/// Parses a `"Mod4+Shift+Return"`-style combo string into modifiers plus the
+/// bare key name, accepting the modifier tokens in *any* order.
+///
+/// This matters because [`key_combo_string`]/[`Modifiers`]'s `Display` only
+/// ever produce one fixed order (Ctrl, Shift, Alt, Mod4) - but every
+/// shipped keybinding is written the conventional "Mod4+Shift+x" way
+/// (Super first, matching Hyprland's own `SUPER, SHIFT, x` convention).
+/// `srd.bind` used to store the combo string exactly as the Lua config
+/// wrote it, and dispatch always looked it up by the canonical
+/// Ctrl/Shift/Alt/Mod4 order built from the real keypress - so any
+/// binding combining more than one modifier in a different order than that
+/// fixed one could never fire: X11 grabbed the physical key correctly
+/// (`grab_keybindings` already parsed order-independently, duplicating
+/// this logic) but dispatch found nothing to run, and on Wayland the combo
+/// was not even recognized as bound at all, so the keypress was forwarded
+/// straight to the focused client instead of reaching srdwm. Confirmed
+/// against the shipped `keybindings.lua`: every multi-modifier binding
+/// there (`Mod4+Shift+*`, `Mod4+Ctrl+k`, `Alt+Shift+Tab`, ...) is written
+/// Super/Alt-first, which never matched the canonical order. Returns
+/// `None` for an empty combo (no key name at all).
+pub fn parse_key_combo(combo: &str) -> Option<(Modifiers, &str)> {
+ let parts: Vec<&str> = combo.split('+').collect();
+ let (key_name, mod_parts) = parts.split_last()?;
+ let mut modifiers = Modifiers::empty();
+ for m in mod_parts {
+ modifiers |= match *m {
+ "Ctrl" => Modifiers::CTRL,
+ "Shift" => Modifiers::SHIFT,
+ "Alt" => Modifiers::ALT,
+ "Mod4" | "Super" => Modifiers::SUPER,
+ _ => Modifiers::empty(),
+ };
+ }
+ Some((modifiers, key_name))
+}
+
+/// Re-orders a combo string into the canonical form [`key_combo_string`]
+/// produces, regardless of what order its modifiers were written in.
+/// Unparseable input (empty string) is returned unchanged, so a caller that
+/// can't do anything better with it still has *something* to store/log.
+pub fn canonicalize_key_combo(combo: &str) -> String {
+ match parse_key_combo(combo) {
+ Some((modifiers, key_name)) => key_combo_string(modifiers, key_name),
+ None => combo.to_string(),
+ }
+}
+
#[derive(Debug, Clone)]
pub enum Event {
WindowCreated(WindowId),
@@ -67,4 +114,67 @@ pub enum Event {
/// can lock and suspend, which is otherwise impossible: a laptop that
/// does nothing on lid-close is a real problem, not a nicety.
LidSwitch { closed: bool },
+ /// `WindowManager::current_workspace` changed. Carries no id: every
+ /// consumer that cares (`main.rs`'s `sync()`) re-reads whichever
+ /// workspace is current now rather than trusting a stale snapshot from
+ /// whenever this event was queued.
+ ///
+ /// Exists purely so `sync()` actually runs after a switch - without a
+ /// `dirty`-setting event, `WindowManager::switch_workspace` alone only
+ /// changes core's own bookkeeping; nothing shows or hides a single
+ /// window for the new workspace until `sync()` runs, which only
+ /// happens when a polled event sets `dirty`. Every switch path (a
+ /// keybinding, `SUPER`+scroll, the `ext_workspace_v1` protocol's
+ /// `activate` request) needs this pushed after it changes
+ /// `current_workspace`, or the switch is invisible.
+ WorkspaceChanged,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn super_first_combo_canonicalizes_to_dispatch_order() {
+ // The shipped keybindings.lua writes every combo Super-first
+ // ("Mod4+Shift+m"), matching Hyprland's own convention - but
+ // `key_combo_string`'s Display order is fixed Ctrl/Shift/Alt/Mod4.
+ // A binding registered under its literal Lua string could never be
+ // found by a real keypress, which always dispatches through the
+ // canonical order. This is exactly the bug `canonicalize_key_combo`
+ // exists to close.
+ assert_eq!(canonicalize_key_combo("Mod4+Shift+m"), "Shift+Mod4+m");
+ assert_eq!(canonicalize_key_combo("Mod4+Ctrl+k"), "Ctrl+Mod4+k");
+ assert_eq!(canonicalize_key_combo("Alt+Shift+Tab"), "Shift+Alt+Tab");
+ }
+
+ #[test]
+ fn already_canonical_combo_is_unchanged() {
+ assert_eq!(canonicalize_key_combo("Ctrl+Mod4+k"), "Ctrl+Mod4+k");
+ }
+
+ #[test]
+ fn single_modifier_combo_is_unaffected() {
+ // No ordering ambiguity with one modifier - this case always
+ // worked, before and after the fix.
+ assert_eq!(canonicalize_key_combo("Mod4+Return"), "Mod4+Return");
+ }
+
+ #[test]
+ fn parse_key_combo_accepts_modifiers_in_any_order() {
+ let (mods, key) = parse_key_combo("Mod4+Shift+m").unwrap();
+ assert_eq!(key, "m");
+ assert!(mods.contains(Modifiers::SUPER) && mods.contains(Modifiers::SHIFT));
+
+ let (mods2, key2) = parse_key_combo("Shift+Mod4+m").unwrap();
+ assert_eq!(key2, "m");
+ assert_eq!(mods, mods2);
+ }
+
+ #[test]
+ fn parse_key_combo_with_no_modifiers_is_bare_key() {
+ let (mods, key) = parse_key_combo("Return").unwrap();
+ assert_eq!(key, "Return");
+ assert_eq!(mods, Modifiers::empty());
+ }
}