use std::cmp::{max, min}; use niri_config::utils::MergeWith as _; use niri_config::window_rule::{Match, WindowRule}; use niri_config::{ BlockOutFrom, BorderRule, CornerRadius, FloatingPosition, PresetSize, ShadowRule, TabIndicatorRule, }; use niri_ipc::ColumnDisplay; use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel; use smithay::utils::{Logical, Size}; use smithay::wayland::compositor::with_states; use smithay::wayland::shell::xdg::{ SurfaceCachedState, ToplevelSurface, XdgToplevelSurfaceRoleAttributes, }; use crate::utils::with_toplevel_role; pub mod mapped; pub use mapped::Mapped; pub mod unmapped; pub use unmapped::{InitialConfigureState, Unmapped}; /// Reference to a mapped or unmapped window. #[derive(Debug, Clone, Copy)] pub enum WindowRef<'a> { Unmapped(&'a Unmapped), Mapped(&'a Mapped), } /// Rules fully resolved for a window. #[derive(Debug, Default, PartialEq, Clone)] pub struct ResolvedWindowRules { /// Default width for this window. /// /// - `None`: unset (global default should be used). /// - `Some(None)`: set to empty (window picks its own width). /// - `Some(Some(width))`: set to a particular width. pub default_width: Option>, /// Default height for this window. /// /// - `None`: unset (global default should be used). /// - `Some(None)`: set to empty (window picks its own height). /// - `Some(Some(height))`: set to a particular height. pub default_height: Option>, /// Default column display for this window. pub default_column_display: Option, /// Default floating position for this window. pub default_floating_position: Option, /// Output to open this window on. pub open_on_output: Option, /// Workspace to open this window on. pub open_on_workspace: Option, /// Whether the window should open full-width. pub open_maximized: Option, /// Whether the window should open maximized to edges (true maximized). pub open_maximized_to_edges: Option, /// Whether the window should open fullscreen. pub open_fullscreen: Option, /// Whether the window should open floating. pub open_floating: Option, /// Whether the window should open focused. pub open_focused: Option, /// Extra bound on the minimum window width. pub min_width: Option, /// Extra bound on the minimum window height. pub min_height: Option, /// Extra bound on the maximum window width. pub max_width: Option, /// Extra bound on the maximum window height. pub max_height: Option, /// Focus ring overrides. pub focus_ring: BorderRule, /// Window border overrides. pub border: BorderRule, /// Shadow overrides. pub shadow: ShadowRule, /// Tab indicator overrides. pub tab_indicator: TabIndicatorRule, /// Whether or not to draw the border with a solid background. /// /// `None` means using the SSD heuristic. pub draw_border_with_background: Option, /// Extra opacity to draw this window with. pub opacity: Option, /// Corner radius to assume this window has. pub geometry_corner_radius: Option, /// Whether to clip this window to its geometry, including the corner radius. pub clip_to_geometry: Option, /// Whether to bob this window up and down. pub baba_is_float: Option, /// Whether to block out this window from certain render targets. pub block_out_from: Option, /// Whether to enable VRR on this window's primary output if it is on-demand. pub variable_refresh_rate: Option, /// Multiplier for all scroll events sent to this window. pub scroll_factor: Option, /// Override whether to set the Tiled xdg-toplevel state on the window. pub tiled_state: Option, } impl<'a> WindowRef<'a> { pub fn toplevel(self) -> &'a ToplevelSurface { match self { WindowRef::Unmapped(unmapped) => unmapped.toplevel(), WindowRef::Mapped(mapped) => mapped.toplevel(), } } pub fn is_focused(self) -> bool { match self { WindowRef::Unmapped(_) => false, WindowRef::Mapped(mapped) => mapped.is_focused(), } } pub fn is_urgent(self) -> bool { match self { WindowRef::Unmapped(_) => false, WindowRef::Mapped(mapped) => mapped.is_urgent(), } } pub fn is_active_in_column(self) -> bool { match self { WindowRef::Unmapped(_) => true, WindowRef::Mapped(mapped) => mapped.is_active_in_column(), } } pub fn is_floating(self) -> bool { match self { // FIXME: This means you cannot set initial configure rules based on is-floating. I'm // not sure there's a good way to support it, since this matcher makes a cycle with the // open-floating rule. // // That said, I don't think there are a lot of useful initial configure properties you // may want to set through an is-floating matcher? Like, if you're configuring a // specific window to open as floating, you can also set those properties in that same // window rule, rather than relying on a different is-floating rule. WindowRef::Unmapped(_) => false, WindowRef::Mapped(mapped) => mapped.is_floating(), } } pub fn is_window_cast_target(self) -> bool { match self { WindowRef::Unmapped(_) => false, WindowRef::Mapped(mapped) => mapped.is_window_cast_target(), } } } impl ResolvedWindowRules { pub fn compute(rules: &[WindowRule], window: WindowRef, is_at_startup: bool) -> Self { let _span = tracy_client::span!("ResolvedWindowRules::compute"); let mut resolved = ResolvedWindowRules::default(); with_toplevel_role(window.toplevel(), |role| { // 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; let mut open_on_workspace = None; for rule in rules { let matches = |m: &Match| { if let Some(at_startup) = m.at_startup { if at_startup != is_at_startup { return false; } } window_matches(window, role, m) }; if !(rule.matches.is_empty() || rule.matches.iter().any(matches)) { continue; } if rule.excludes.iter().any(matches) { continue; } if let Some(x) = rule.default_column_width { resolved.default_width = Some(x.0); } if let Some(x) = rule.default_window_height { resolved.default_height = Some(x.0); } if let Some(x) = rule.default_column_display { resolved.default_column_display = Some(x); } if let Some(x) = rule.default_floating_position { resolved.default_floating_position = Some(x); } if let Some(x) = rule.open_on_output.as_deref() { open_on_output = Some(x); } if let Some(x) = rule.open_on_workspace.as_deref() { open_on_workspace = Some(x); } if let Some(x) = rule.open_maximized { resolved.open_maximized = Some(x); } if let Some(x) = rule.open_maximized_to_edges { resolved.open_maximized_to_edges = Some(x); } if let Some(x) = rule.open_fullscreen { resolved.open_fullscreen = Some(x); } if let Some(x) = rule.open_floating { resolved.open_floating = Some(x); } if let Some(x) = rule.open_focused { resolved.open_focused = Some(x); } if let Some(x) = rule.min_width { resolved.min_width = Some(x); } if let Some(x) = rule.min_height { resolved.min_height = Some(x); } if let Some(x) = rule.max_width { resolved.max_width = Some(x); } if let Some(x) = rule.max_height { resolved.max_height = Some(x); } resolved.focus_ring.merge_with(&rule.focus_ring); resolved.border.merge_with(&rule.border); resolved.shadow.merge_with(&rule.shadow); resolved.tab_indicator.merge_with(&rule.tab_indicator); if let Some(x) = rule.draw_border_with_background { resolved.draw_border_with_background = Some(x); } if let Some(x) = rule.opacity { resolved.opacity = Some(x); } if let Some(x) = rule.geometry_corner_radius { resolved.geometry_corner_radius = Some(x); } if let Some(x) = rule.clip_to_geometry { resolved.clip_to_geometry = Some(x); } if let Some(x) = rule.baba_is_float { resolved.baba_is_float = Some(x); } if let Some(x) = rule.block_out_from { resolved.block_out_from = Some(x); } if let Some(x) = rule.variable_refresh_rate { resolved.variable_refresh_rate = Some(x); } if let Some(x) = rule.scroll_factor { resolved.scroll_factor = Some(x.0); } if let Some(x) = rule.tiled_state { resolved.tiled_state = Some(x); } } resolved.open_on_output = open_on_output.map(|x| x.to_owned()); resolved.open_on_workspace = open_on_workspace.map(|x| x.to_owned()); }); resolved } pub fn apply_min_size(&self, min_size: Size) -> Size { let mut size = min_size; if let Some(x) = self.min_width { size.w = max(size.w, i32::from(x)); } if let Some(x) = self.min_height { size.h = max(size.h, i32::from(x)); } size } pub fn apply_max_size(&self, max_size: Size) -> Size { let mut size = max_size; if let Some(x) = self.max_width { if size.w == 0 { size.w = i32::from(x); } else if x > 0 { size.w = min(size.w, i32::from(x)); } } if let Some(x) = self.max_height { if size.h == 0 { size.h = i32::from(x); } else if x > 0 { size.h = min(size.h, i32::from(x)); } } size } pub fn apply_min_max_size( &self, min_size: Size, max_size: Size, ) -> (Size, Size) { let min_size = self.apply_min_size(min_size); let max_size = self.apply_max_size(max_size); (min_size, max_size) } pub fn compute_open_floating(&self, toplevel: &ToplevelSurface) -> bool { if let Some(res) = self.open_floating { return res; } // Windows with a parent (usually dialogs) open as floating by default. if toplevel.parent().is_some() { return true; } let (min_size, max_size) = with_states(toplevel.wl_surface(), |state| { let mut guard = state.cached_state.get::(); let current = guard.current(); (current.min_size, current.max_size) }); let (min_size, max_size) = self.apply_min_max_size(min_size, max_size); // We open fixed-height windows as floating. min_size.h > 0 && min_size.h == max_size.h } } fn window_matches(window: WindowRef, role: &XdgToplevelSurfaceRoleAttributes, m: &Match) -> bool { // Must be ensured by the caller. let server_pending = role.server_pending.as_ref().unwrap(); if let Some(is_focused) = m.is_focused { if window.is_focused() != is_focused { return false; } } if let Some(is_urgent) = m.is_urgent { if window.is_urgent() != is_urgent { return false; } } 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; }; if !app_id_re.0.is_match(app_id) { return false; } } if let Some(title_re) = &m.title { let Some(title) = &role.title else { return false; }; if !title_re.0.is_match(title) { return false; } } if let Some(is_active_in_column) = m.is_active_in_column { if window.is_active_in_column() != is_active_in_column { return false; } } if let Some(is_floating) = m.is_floating { if window.is_floating() != is_floating { return false; } } if let Some(is_window_cast_target) = m.is_window_cast_target { if window.is_window_cast_target() != is_window_cast_target { return false; } } true }