aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2024-03-23 14:38:07 +0400
committerIvan Molodetskikh <yalterz@gmail.com>2024-03-23 15:45:44 +0400
commitb7ed2fb82a19afe73e3e51ef2331ac6ad9c175a0 (patch)
tree8757c39889958bd6a8f87ac9089d8e5a7fd43a76
parentf3f02aca2058dd7adc4d75707ded2b5d8887a258 (diff)
downloadniri-b7ed2fb82a19afe73e3e51ef2331ac6ad9c175a0.tar.gz
niri-b7ed2fb82a19afe73e3e51ef2331ac6ad9c175a0.tar.bz2
niri-b7ed2fb82a19afe73e3e51ef2331ac6ad9c175a0.zip
Add is-active window rule matcher
-rw-r--r--niri-config/src/lib.rs24
-rw-r--r--niri-visual-tests/src/test_window.rs2
-rw-r--r--resources/default-config.kdl4
-rw-r--r--src/handlers/xdg_shell.rs8
-rw-r--r--src/layout/mod.rs4
-rw-r--r--src/layout/workspace.rs8
-rw-r--r--src/niri.rs34
-rw-r--r--src/window/mapped.rs47
-rw-r--r--src/window/mod.rs21
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;