diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2024-03-23 14:38:07 +0400 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2024-03-23 15:45:44 +0400 |
| commit | b7ed2fb82a19afe73e3e51ef2331ac6ad9c175a0 (patch) | |
| tree | 8757c39889958bd6a8f87ac9089d8e5a7fd43a76 | |
| parent | f3f02aca2058dd7adc4d75707ded2b5d8887a258 (diff) | |
| download | niri-b7ed2fb82a19afe73e3e51ef2331ac6ad9c175a0.tar.gz niri-b7ed2fb82a19afe73e3e51ef2331ac6ad9c175a0.tar.bz2 niri-b7ed2fb82a19afe73e3e51ef2331ac6ad9c175a0.zip | |
Add is-active window rule matcher
| -rw-r--r-- | niri-config/src/lib.rs | 24 | ||||
| -rw-r--r-- | niri-visual-tests/src/test_window.rs | 2 | ||||
| -rw-r--r-- | resources/default-config.kdl | 4 | ||||
| -rw-r--r-- | src/handlers/xdg_shell.rs | 8 | ||||
| -rw-r--r-- | src/layout/mod.rs | 4 | ||||
| -rw-r--r-- | src/layout/workspace.rs | 8 | ||||
| -rw-r--r-- | src/niri.rs | 34 | ||||
| -rw-r--r-- | src/window/mapped.rs | 47 | ||||
| -rw-r--r-- | src/window/mod.rs | 21 |
9 files changed, 126 insertions, 26 deletions
diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index 4e43d795..01cdba70 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -699,17 +699,21 @@ pub struct WindowRule { pub draw_border_with_background: Option<bool>, } +// Remember to update the PartialEq impl when adding fields! #[derive(knuffel::Decode, Debug, Default, Clone)] pub struct Match { #[knuffel(property, str)] pub app_id: Option<Regex>, #[knuffel(property, str)] pub title: Option<Regex>, + #[knuffel(property)] + pub is_active: Option<bool>, } impl PartialEq for Match { fn eq(&self, other: &Self) -> bool { - self.app_id.as_ref().map(Regex::as_str) == other.app_id.as_ref().map(Regex::as_str) + self.is_active == other.is_active + && self.app_id.as_ref().map(Regex::as_str) == other.app_id.as_ref().map(Regex::as_str) && self.title.as_ref().map(Regex::as_str) == other.title.as_ref().map(Regex::as_str) } } @@ -1762,6 +1766,7 @@ mod tests { window-rule { match app-id=".*alacritty" exclude title="~" + exclude is-active=true open-on-output "eDP-1" open-maximized true @@ -1947,11 +1952,20 @@ mod tests { matches: vec![Match { app_id: Some(Regex::new(".*alacritty").unwrap()), title: None, + is_active: None, }], - excludes: vec![Match { - app_id: None, - title: Some(Regex::new("~").unwrap()), - }], + excludes: vec![ + Match { + app_id: None, + title: Some(Regex::new("~").unwrap()), + is_active: None, + }, + Match { + app_id: None, + title: None, + is_active: Some(true), + }, + ], open_on_output: Some("eDP-1".to_owned()), open_maximized: Some(true), open_fullscreen: Some(false), diff --git a/niri-visual-tests/src/test_window.rs b/niri-visual-tests/src/test_window.rs index a0098f73..4f89d15e 100644 --- a/niri-visual-tests/src/test_window.rs +++ b/niri-visual-tests/src/test_window.rs @@ -203,7 +203,7 @@ impl LayoutElement for TestWindow { fn set_offscreen_element_id(&self, _id: Option<Id>) {} - fn set_activated(&self, _active: bool) {} + fn set_activated(&mut self, _active: bool) {} fn set_bounds(&self, _bounds: Size<i32, Logical>) {} diff --git a/resources/default-config.kdl b/resources/default-config.kdl index 6534fe15..55bff24d 100644 --- a/resources/default-config.kdl +++ b/resources/default-config.kdl @@ -364,6 +364,10 @@ animations { // Raw KDL strings are helpful here. exclude app-id=r#"\.unwanted\."# + // One more way to match is by whether the window is active + // (same as when it uses the active border color). + match is-active=true + // Here are the properties that you can set on a window rule. // These properties apply once, when a window first opens. diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs index db629171..c31c6404 100644 --- a/src/handlers/xdg_shell.rs +++ b/src/handlers/xdg_shell.rs @@ -745,12 +745,8 @@ impl State { .layout .find_window_and_output_mut(toplevel.wl_surface()) { - let new_rules = ResolvedWindowRules::compute(window_rules, WindowRef::Mapped(mapped)); - drop(config); - - if mapped.rules != new_rules { - mapped.rules = new_rules; - + if mapped.recompute_window_rules(window_rules) { + drop(config); let output = output.cloned(); let window = mapped.window.clone(); self.niri.layout.update_window(&window); diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 7162c3e3..bc5c8dcf 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -107,7 +107,7 @@ pub trait LayoutElement { fn output_enter(&self, output: &Output); fn output_leave(&self, output: &Output); fn set_offscreen_element_id(&self, id: Option<Id>); - fn set_activated(&self, active: bool); + fn set_activated(&mut self, active: bool); fn set_bounds(&self, bounds: Size<i32, Logical>); fn send_pending_configure(&self); @@ -1893,7 +1893,7 @@ mod tests { fn set_offscreen_element_id(&self, _id: Option<Id>) {} - fn set_activated(&self, _active: bool) {} + fn set_activated(&mut self, _active: bool) {} fn set_bounds(&self, _bounds: Size<i32, Logical>) {} diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index 03868dc0..64be3bcc 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -1550,12 +1550,12 @@ impl<W: LayoutElement> Workspace<W> { true } - pub fn refresh(&self, is_active: bool) { + pub fn refresh(&mut self, is_active: bool) { let bounds = self.toplevel_bounds(); - for (col_idx, col) in self.columns.iter().enumerate() { - for (tile_idx, tile) in col.tiles.iter().enumerate() { - let win = tile.window(); + for (col_idx, col) in self.columns.iter_mut().enumerate() { + for (tile_idx, tile) in col.tiles.iter_mut().enumerate() { + let win = tile.window_mut(); let active = is_active && self.active_column_idx == col_idx && col.active_tile_idx == tile_idx; diff --git a/src/niri.rs b/src/niri.rs index 7984d3c4..5e425396 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -438,6 +438,7 @@ impl State { self.update_keyboard_focus(); self.refresh_pointer_focus(); foreign_toplevel::refresh(self); + self.niri.refresh_window_rules(); self.niri.redraw_queued_outputs(&mut self.backend); { @@ -911,9 +912,9 @@ impl State { let mut windows = vec![]; self.niri.layout.with_windows_mut(|mapped, _| { - mapped.rules = - ResolvedWindowRules::compute(window_rules, WindowRef::Mapped(mapped)); - windows.push(mapped.window.clone()); + if mapped.recompute_window_rules(window_rules) { + windows.push(mapped.window.clone()); + } }); for win in windows { self.niri.layout.update_window(&win); @@ -2149,6 +2150,33 @@ impl Niri { self.idle_notifier_state.set_is_inhibited(is_inhibited); } + pub fn refresh_window_rules(&mut self) { + let _span = tracy_client::span!("Niri::refresh_window_rules"); + + let config = self.config.borrow(); + let window_rules = &config.window_rules; + + let mut windows = vec![]; + let mut outputs = HashSet::new(); + self.layout.with_windows_mut(|mapped, output| { + if mapped.recompute_window_rules_if_needed(window_rules) { + windows.push(mapped.window.clone()); + + if let Some(output) = output { + outputs.insert(output.clone()); + } + } + }); + drop(config); + + for win in windows { + self.layout.update_window(&win); + } + for output in outputs { + self.queue_redraw(&output); + } + } + pub fn render<R: NiriRenderer>( &self, renderer: &mut R, diff --git a/src/window/mapped.rs b/src/window/mapped.rs index b5caf8e2..0d3a3ac1 100644 --- a/src/window/mapped.rs +++ b/src/window/mapped.rs @@ -1,5 +1,6 @@ use std::cmp::{max, min}; +use niri_config::WindowRule; use smithay::backend::renderer::element::{AsRenderElements as _, Id}; use smithay::desktop::space::SpaceElement as _; use smithay::desktop::Window; @@ -11,7 +12,7 @@ use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform}; use smithay::wayland::compositor::{send_surface_state, with_states}; use smithay::wayland::shell::xdg::{SurfaceCachedState, ToplevelSurface}; -use super::ResolvedWindowRules; +use super::{ResolvedWindowRules, WindowRef}; use crate::layout::{LayoutElement, LayoutElementRenderElement}; use crate::niri::WindowOffscreenId; use crate::render_helpers::renderer::NiriRenderer; @@ -22,16 +23,47 @@ pub struct Mapped { /// Up-to-date rules. pub rules: ResolvedWindowRules, + + /// Whether the window rules need to be recomputed. + /// + /// This is not used in all cases; for example, app ID and title changes recompute the rules + /// immediately, rather than setting this flag. + pub need_to_recompute_rules: bool, } impl Mapped { pub fn new(window: Window, rules: ResolvedWindowRules) -> Self { - Self { window, rules } + Self { + window, + rules, + need_to_recompute_rules: false, + } } pub fn toplevel(&self) -> &ToplevelSurface { self.window.toplevel().expect("no X11 support") } + + /// Recomputes the resolved window rules and returns whether they changed. + pub fn recompute_window_rules(&mut self, rules: &[WindowRule]) -> bool { + self.need_to_recompute_rules = false; + + let new_rules = ResolvedWindowRules::compute(rules, WindowRef::Mapped(self)); + if new_rules == self.rules { + return false; + } + + self.rules = new_rules; + true + } + + pub fn recompute_window_rules_if_needed(&mut self, rules: &[WindowRule]) -> bool { + if !self.need_to_recompute_rules { + return false; + } + + self.recompute_window_rules(rules) + } } impl LayoutElement for Mapped { @@ -155,8 +187,15 @@ impl LayoutElement for Mapped { data.0.replace(id); } - fn set_activated(&self, active: bool) { - self.window.set_activated(active); + fn set_activated(&mut self, active: bool) { + let changed = self.toplevel().with_pending_state(|state| { + if active { + state.states.set(xdg_toplevel::State::Activated) + } else { + state.states.unset(xdg_toplevel::State::Activated) + } + }); + self.need_to_recompute_rules |= changed; } fn set_bounds(&self, bounds: Size<i32, Logical>) { diff --git a/src/window/mod.rs b/src/window/mod.rs index 4d225223..4ff943aa 100644 --- a/src/window/mod.rs +++ b/src/window/mod.rs @@ -1,4 +1,5 @@ use niri_config::{Match, WindowRule}; +use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel; use smithay::wayland::compositor::with_states; use smithay::wayland::shell::xdg::{ ToplevelSurface, XdgToplevelSurfaceData, XdgToplevelSurfaceRoleAttributes, @@ -84,13 +85,18 @@ impl ResolvedWindowRules { let toplevel = window.toplevel(); with_states(toplevel.wl_surface(), |states| { - let role = states + let mut role = states .data_map .get::<XdgToplevelSurfaceData>() .unwrap() .lock() .unwrap(); + // Ensure server_pending like in Smithay's with_pending_state(). + if role.server_pending.is_none() { + role.server_pending = Some(role.current_server_state().clone()); + } + let mut open_on_output = None; for rule in rules { @@ -150,6 +156,19 @@ impl ResolvedWindowRules { } fn window_matches(role: &XdgToplevelSurfaceRoleAttributes, m: &Match) -> bool { + // Must be ensured by the caller. + let server_pending = role.server_pending.as_ref().unwrap(); + + if let Some(is_active) = m.is_active { + // Our "is-active" definition corresponds to the window having a pending Activated state. + let pending_activated = server_pending + .states + .contains(xdg_toplevel::State::Activated); + if is_active != pending_activated { + return false; + } + } + if let Some(app_id_re) = &m.app_id { let Some(app_id) = &role.app_id else { return false; |
