diff options
| author | srdusr <[email protected]> | 2024-05-30 16:10:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2024-05-30 16:10:00 +0200 |
| commit | 1c175642d073689ca11b9252411ea5f8446007d0 (patch) | |
| tree | 00b8f9062cf1261d2d2642c059a14ff5790522e5 /crates/core/src/rules.rs | |
| parent | 3d3057ae384ef7389284af8988410889e99c6bb9 (diff) | |
| download | srdwm-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/rules.rs')
| -rw-r--r-- | crates/core/src/rules.rs | 110 |
1 files changed, 104 insertions, 6 deletions
diff --git a/crates/core/src/rules.rs b/crates/core/src/rules.rs index 642179d..148b627 100644 --- a/crates/core/src/rules.rs +++ b/crates/core/src/rules.rs @@ -1,22 +1,46 @@ +use regex::Regex; + use crate::geometry::Rect; use crate::window::Window; use crate::workspace::WorkspaceId; /// Match criteria for a [`WindowRule`]. A matcher with every field `None` /// matches nothing (an accidental `srd.rule({}, {...})` in config should be a -/// silent no-op, not "apply to every window"). +/// silent no-op, not "apply to every window"). Every field that is `Some` +/// must match (AND semantics) - matching the convention i3's multi-criteria +/// rules and bspwm's `class:instance:title` rules both already use. +/// +/// `title_contains`/`class` (plain substring/exact match) are kept +/// alongside the regex fields below rather than folded into them: they +/// cover the large majority of real rules (`srd.rule({ class = "firefox" }, +/// ...)`) with no regex syntax to get right, and are cheaper to evaluate. +/// `title_regex`/`class_regex`/`instance` exist for the cases that need +/// more precision - disambiguating a specific dialog by title while +/// leaving an app's main window alone, the concrete example that motivated +/// adding these - without forcing every simple rule to write one. #[derive(Debug, Clone, Default)] pub struct WindowMatch { /// Case-insensitive substring match against `Window::title`. pub title_contains: Option<String>, - /// Case-insensitive exact match against `Window::app_id` (X11 `WM_CLASS` - /// / Wayland `app_id`). + /// Case-insensitive exact match against `Window::app_id` (X11 `WM_CLASS`'s + /// *class* half / Wayland `app_id`). pub class: Option<String>, + /// Regex match against `Window::title`. Case-sensitive by default -- + /// write `(?i)` at the start of the pattern for case-insensitive, + /// the same convention i3's own criteria use. + pub title_regex: Option<Regex>, + /// Regex match against `Window::app_id`. + pub class_regex: Option<Regex>, + /// Case-insensitive exact match against `Window::instance` (X11 + /// `WM_CLASS`'s *instance* half). Always fails to match on Wayland-native + /// windows, which have no equivalent (`Window::instance` is always + /// empty there) - an X11/XWayland-only criterion, same as bspwm's. + pub instance: Option<String>, } impl WindowMatch { pub fn is_empty(&self) -> bool { - self.title_contains.is_none() && self.class.is_none() + self.title_contains.is_none() && self.class.is_none() && self.title_regex.is_none() && self.class_regex.is_none() && self.instance.is_none() } pub fn matches(&self, window: &Window) -> bool { @@ -33,6 +57,21 @@ impl WindowMatch { return false; } } + if let Some(re) = &self.title_regex { + if !re.is_match(&window.title) { + return false; + } + } + if let Some(re) = &self.class_regex { + if !re.is_match(&window.app_id) { + return false; + } + } + if let Some(i) = &self.instance { + if !window.instance.eq_ignore_ascii_case(i) { + return false; + } + } true } } @@ -70,7 +109,7 @@ mod tests { #[test] fn title_match_is_case_insensitive_substring() { let w = Window::new(1, "Mozilla Firefox"); - let m = WindowMatch { title_contains: Some("firefox".into()), class: None }; + let m = WindowMatch { title_contains: Some("firefox".into()), ..Default::default() }; assert!(m.matches(&w)); } @@ -78,10 +117,69 @@ mod tests { fn class_match_is_case_insensitive_exact() { let mut w = Window::new(1, ""); w.app_id = "Firefox".into(); - let m = WindowMatch { title_contains: None, class: Some("firefox".into()) }; + let m = WindowMatch { class: Some("firefox".into()), ..Default::default() }; assert!(m.matches(&w)); let mut w2 = Window::new(2, ""); w2.app_id = "firefoxx".into(); assert!(!m.matches(&w2)); } + + #[test] + fn title_regex_matches_a_specific_dialog_without_matching_the_main_window() { + // The concrete case that motivated adding regex support at all: + // disambiguating a specific dialog by title while leaving an app's + // main window alone - not reliably possible with substring-only + // matching if the dialog's title is a substring-superset situation + // (or vice versa) that plain `contains` can't express. + let m = WindowMatch { title_regex: Some(Regex::new(r"^Save File$").unwrap()), ..Default::default() }; + let dialog = Window::new(1, "Save File"); + let main = Window::new(2, "Save File - GNU Image Manipulation Program"); + assert!(m.matches(&dialog)); + assert!(!m.matches(&main)); + } + + #[test] + fn class_regex_is_case_sensitive_unless_the_pattern_opts_in() { + let mut w = Window::new(1, ""); + w.app_id = "firefox".into(); + let sensitive = WindowMatch { class_regex: Some(Regex::new(r"^Firefox$").unwrap()), ..Default::default() }; + assert!(!sensitive.matches(&w)); + let insensitive = WindowMatch { class_regex: Some(Regex::new(r"(?i)^Firefox$").unwrap()), ..Default::default() }; + assert!(insensitive.matches(&w)); + } + + #[test] + fn instance_match_is_case_insensitive_exact_and_independent_of_class() { + let mut w = Window::new(1, ""); + w.app_id = "Navigator".into(); + w.instance = "firefox".into(); + let m = WindowMatch { instance: Some("Firefox".into()), ..Default::default() }; + assert!(m.matches(&w)); + let mut w2 = Window::new(2, ""); + w2.instance = "firefoxdeveloperedition".into(); + assert!(!m.matches(&w2)); + } + + #[test] + fn multiple_criteria_are_combined_with_and() { + let mut w = Window::new(1, "Preferences"); + w.app_id = "firefox".into(); + let m = WindowMatch { class: Some("firefox".into()), title_contains: Some("preferences".into()), ..Default::default() }; + assert!(m.matches(&w)); + // Same class, different title - must not match once title is + // also a criterion. + let mut w2 = Window::new(2, "Mozilla Firefox"); + w2.app_id = "firefox".into(); + assert!(!m.matches(&w2)); + } + + #[test] + fn a_nonempty_instance_criterion_does_not_match_a_wayland_native_window() { + // `Window::instance` is always empty on Wayland (no equivalent + // concept), so a real `instance` rule (never an empty-string one -- + // nobody writes `instance = ""`) must not match there. + let w = Window::new(1, ""); + let m = WindowMatch { instance: Some("firefox".into()), ..Default::default() }; + assert!(!m.matches(&w)); + } } |