diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2024-04-24 21:49:07 +0400 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2024-04-24 22:01:26 +0400 |
| commit | 3e598c565e6e8ad4c34e93aec9a49e60d51d730e (patch) | |
| tree | 95d3f12d04f3a416ba86915ebea01b7e0f66498e | |
| parent | e261b641ed62474676c90aaa4e734cdd1ecde703 (diff) | |
| download | niri-3e598c565e6e8ad4c34e93aec9a49e60d51d730e.tar.gz niri-3e598c565e6e8ad4c34e93aec9a49e60d51d730e.tar.bz2 niri-3e598c565e6e8ad4c34e93aec9a49e60d51d730e.zip | |
Implement border window rule
| -rw-r--r-- | niri-config/src/lib.rs | 80 | ||||
| -rw-r--r-- | niri-visual-tests/src/cases/layout.rs | 4 | ||||
| -rw-r--r-- | src/handlers/xdg_shell.rs | 10 | ||||
| -rw-r--r-- | src/layout/mod.rs | 18 | ||||
| -rw-r--r-- | src/layout/tile.rs | 15 | ||||
| -rw-r--r-- | src/layout/workspace.rs | 66 | ||||
| -rw-r--r-- | src/window/mod.rs | 16 | ||||
| -rw-r--r-- | wiki/Configuration:-Window-Rules.md | 30 |
8 files changed, 203 insertions, 36 deletions
diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index d5982019..26689d09 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -701,6 +701,8 @@ pub struct WindowRule { #[knuffel(child, unwrap(argument))] pub max_height: Option<u16>, + #[knuffel(child, default)] + pub border: BorderRule, #[knuffel(child, unwrap(argument))] pub draw_border_with_background: Option<bool>, #[knuffel(child, unwrap(argument))] @@ -737,6 +739,24 @@ pub enum BlockOutFrom { ScreenCapture, } +#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)] +pub struct BorderRule { + #[knuffel(child)] + pub off: bool, + #[knuffel(child)] + pub on: bool, + #[knuffel(child, unwrap(argument))] + pub width: Option<u16>, + #[knuffel(child)] + pub active_color: Option<Color>, + #[knuffel(child)] + pub inactive_color: Option<Color>, + #[knuffel(child)] + pub active_gradient: Option<Gradient>, + #[knuffel(child)] + pub inactive_gradient: Option<Gradient>, +} + #[derive(Debug, Default, PartialEq)] pub struct Binds(pub Vec<Bind>); @@ -992,6 +1012,56 @@ impl Default for Config { } } +impl BorderRule { + pub fn merge_with(&mut self, other: &Self) { + self.off |= other.off; + self.on |= other.on; + + if let Some(x) = other.width { + self.width = Some(x); + } + if let Some(x) = other.active_color { + self.active_color = Some(x); + } + if let Some(x) = other.inactive_color { + self.inactive_color = Some(x); + } + if let Some(x) = other.active_gradient { + self.active_gradient = Some(x); + } + if let Some(x) = other.inactive_gradient { + self.inactive_gradient = Some(x); + } + } + + pub fn resolve_against(&self, mut config: Border) -> Border { + config.off |= self.off; + if self.on { + config.off = false; + } + + if let Some(x) = self.width { + config.width = x; + } + if let Some(x) = self.active_color { + config.active_color = x; + config.active_gradient = None; + } + if let Some(x) = self.inactive_color { + config.inactive_color = x; + config.inactive_gradient = None; + } + if let Some(x) = self.active_gradient { + config.active_gradient = Some(x); + } + if let Some(x) = self.inactive_gradient { + config.inactive_gradient = Some(x); + } + + config + } +} + impl FromStr for Color { type Err = miette::Error; @@ -1977,6 +2047,11 @@ mod tests { open-on-output "eDP-1" open-maximized true open-fullscreen false + + border { + on + width 8 + } } binds { @@ -2179,6 +2254,11 @@ mod tests { open_on_output: Some("eDP-1".to_owned()), open_maximized: Some(true), open_fullscreen: Some(false), + border: BorderRule { + on: true, + width: Some(8), + ..Default::default() + }, ..Default::default() }], binds: Binds(vec![ diff --git a/niri-visual-tests/src/cases/layout.rs b/niri-visual-tests/src/cases/layout.rs index 4b44fcc6..75a6eaae 100644 --- a/niri-visual-tests/src/cases/layout.rs +++ b/niri-visual-tests/src/cases/layout.rs @@ -147,7 +147,7 @@ impl Layout { fn add_window(&mut self, mut window: TestWindow, width: Option<ColumnWidth>) { let ws = self.layout.active_workspace().unwrap(); - window.request_size(ws.new_window_size(width), false); + window.request_size(ws.new_window_size(width, window.rules()), false); window.communicate(); self.layout.add_window(window.clone(), width, false); @@ -161,7 +161,7 @@ impl Layout { width: Option<ColumnWidth>, ) { let ws = self.layout.active_workspace().unwrap(); - window.request_size(ws.new_window_size(width), false); + window.request_size(ws.new_window_size(width, window.rules()), false); window.communicate(); self.layout diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs index 36033da8..0b0a5a33 100644 --- a/src/handlers/xdg_shell.rs +++ b/src/handlers/xdg_shell.rs @@ -230,7 +230,7 @@ impl XdgShellHandler for State { // The required configure will be the initial configure. } - InitialConfigureState::Configured { output, .. } => { + InitialConfigureState::Configured { rules, output, .. } => { // Figure out the monitor following a similar logic to initial configure. // FIXME: deduplicate. let mon = requested_output @@ -269,7 +269,7 @@ impl XdgShellHandler for State { toplevel.with_pending_state(|state| { state.states.set(xdg_toplevel::State::Fullscreen); }); - ws.configure_new_window(&unmapped.window, None); + ws.configure_new_window(&unmapped.window, None, rules); } // We already sent the initial configure, so we need to reconfigure. @@ -302,10 +302,10 @@ impl XdgShellHandler for State { // The required configure will be the initial configure. } InitialConfigureState::Configured { + rules, width, is_full_width, output, - .. } => { // Figure out the monitor following a similar logic to initial configure. // FIXME: deduplicate. @@ -349,7 +349,7 @@ impl XdgShellHandler for State { } else { *width }; - ws.configure_new_window(&unmapped.window, configure_width); + ws.configure_new_window(&unmapped.window, configure_width, rules); } // We already sent the initial configure, so we need to reconfigure. @@ -580,7 +580,7 @@ impl State { } else { width }; - ws.configure_new_window(window, configure_width); + ws.configure_new_window(window, configure_width, &rules); } // If the user prefers no CSD, it's a reasonable assumption that they would prefer to get diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 4c570ef2..4889e9a7 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -463,8 +463,10 @@ impl<W: LayoutElement> Layout<W> { ) -> Option<&Output> { let mut width = width.unwrap_or_else(|| ColumnWidth::Fixed(window.size().w)); if let ColumnWidth::Fixed(w) = &mut width { - if !self.options.border.off { - *w += self.options.border.width as i32 * 2; + let rules = window.rules(); + let border_config = rules.border.resolve_against(self.options.border); + if !border_config.off { + *w += border_config.width as i32 * 2; } } @@ -519,8 +521,10 @@ impl<W: LayoutElement> Layout<W> { ) -> Option<&Output> { let mut width = width.unwrap_or_else(|| ColumnWidth::Fixed(window.size().w)); if let ColumnWidth::Fixed(w) = &mut width { - if !self.options.border.off { - *w += self.options.border.width as i32 * 2; + let rules = window.rules(); + let border_config = rules.border.resolve_against(self.options.border); + if !border_config.off { + *w += border_config.width as i32 * 2; } } @@ -555,8 +559,10 @@ impl<W: LayoutElement> Layout<W> { ) { let mut width = width.unwrap_or_else(|| ColumnWidth::Fixed(window.size().w)); if let ColumnWidth::Fixed(w) = &mut width { - if !self.options.border.off { - *w += self.options.border.width as i32 * 2; + let rules = window.rules(); + let border_config = rules.border.resolve_against(self.options.border); + if !border_config.off { + *w += border_config.width as i32 * 2; } } diff --git a/src/layout/tile.rs b/src/layout/tile.rs index ed6773da..4778db69 100644 --- a/src/layout/tile.rs +++ b/src/layout/tile.rs @@ -107,9 +107,12 @@ struct MoveAnimation { impl<W: LayoutElement> Tile<W> { pub fn new(window: W, options: Rc<Options>) -> Self { + let rules = window.rules(); + let border_config = rules.border.resolve_against(options.border); + Self { window, - border: FocusRing::new(options.border.into()), + border: FocusRing::new(border_config.into()), focus_ring: FocusRing::new(options.focus_ring), is_fullscreen: false, // FIXME: up-to-date fullscreen right away, but we need size. fullscreen_backdrop: SolidColorBuffer::new((0, 0), [0., 0., 0., 1.]), @@ -123,7 +126,11 @@ impl<W: LayoutElement> Tile<W> { } pub fn update_config(&mut self, options: Rc<Options>) { - self.border.update_config(options.border.into()); + let rules = self.window.rules(); + + let border_config = rules.border.resolve_against(self.options.border); + self.border.update_config(border_config.into()); + self.focus_ring.update_config(options.focus_ring); self.options = options; } @@ -164,6 +171,10 @@ impl<W: LayoutElement> Tile<W> { self.resize_animation = None; } } + + let rules = self.window.rules(); + let border_config = rules.border.resolve_against(self.options.border); + self.border.update_config(border_config.into()); } pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) { diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index b6b38285..fd5662b8 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -23,6 +23,7 @@ use crate::render_helpers::RenderTarget; use crate::swipe_tracker::SwipeTracker; use crate::utils::id::IdCounter; use crate::utils::output_size; +use crate::window::ResolvedWindowRules; /// Amount of touchpad movement to scroll the view for the width of one working area. const VIEW_GESTURE_WORKING_AREA_MOVEMENT: f64 = 1200.; @@ -406,16 +407,9 @@ impl<W: LayoutElement> Workspace<W> { } } - fn toplevel_bounds(&self) -> Size<i32, Logical> { - let mut border = 0; - if !self.options.border.off { - border = self.options.border.width as i32 * 2; - } - - Size::from(( - max(self.working_area.size.w - self.options.gaps * 2 - border, 1), - max(self.working_area.size.h - self.options.gaps * 2 - border, 1), - )) + fn toplevel_bounds(&self, rules: &ResolvedWindowRules) -> Size<i32, Logical> { + let border_config = rules.border.resolve_against(self.options.border); + compute_toplevel_bounds(border_config, self.working_area.size, self.options.gaps) } pub fn resolve_default_width( @@ -429,14 +423,20 @@ impl<W: LayoutElement> Workspace<W> { } } - pub fn new_window_size(&self, width: Option<ColumnWidth>) -> Size<i32, Logical> { + pub fn new_window_size( + &self, + width: Option<ColumnWidth>, + rules: &ResolvedWindowRules, + ) -> Size<i32, Logical> { + let border = rules.border.resolve_against(self.options.border); + let width = if let Some(width) = width { let is_fixed = matches!(width, ColumnWidth::Fixed(_)); let mut width = width.resolve(&self.options, self.working_area.size.w); - if !is_fixed && !self.options.border.off { - width -= self.options.border.width as i32 * 2; + if !is_fixed && !border.off { + width -= border.width as i32 * 2; } max(1, width) @@ -445,14 +445,19 @@ impl<W: LayoutElement> Workspace<W> { }; let mut height = self.working_area.size.h - self.options.gaps * 2; - if !self.options.border.off { - height -= self.options.border.width as i32 * 2; + if !border.off { + height -= border.width as i32 * 2; } Size::from((width, max(height, 1))) } - pub fn configure_new_window(&self, window: &Window, width: Option<ColumnWidth>) { + pub fn configure_new_window( + &self, + window: &Window, + width: Option<ColumnWidth>, + rules: &ResolvedWindowRules, + ) { if let Some(output) = self.output.as_ref() { let scale = output.current_scale().integer_scale(); let transform = output.current_transform(); @@ -468,10 +473,10 @@ impl<W: LayoutElement> Workspace<W> { if state.states.contains(xdg_toplevel::State::Fullscreen) { state.size = Some(self.view_size); } else { - state.size = Some(self.new_window_size(width)); + state.size = Some(self.new_window_size(width, rules)); } - state.bounds = Some(self.toplevel_bounds()); + state.bounds = Some(self.toplevel_bounds(rules)); }); } @@ -2134,8 +2139,6 @@ impl<W: LayoutElement> Workspace<W> { } pub fn refresh(&mut self, is_active: bool) { - let bounds = self.toplevel_bounds(); - 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(); @@ -2144,7 +2147,14 @@ impl<W: LayoutElement> Workspace<W> { && col.active_tile_idx == tile_idx; win.set_activated(active); + let border_config = win.rules().border.resolve_against(self.options.border); + let bounds = compute_toplevel_bounds( + border_config, + self.working_area.size, + self.options.gaps, + ); win.set_bounds(bounds); + win.send_pending_configure(); win.refresh(); } @@ -2824,3 +2834,19 @@ pub fn compute_working_area(output: &Output, struts: Struts) -> Rectangle<i32, L working_area } + +fn compute_toplevel_bounds( + border_config: niri_config::Border, + working_area_size: Size<i32, Logical>, + gaps: i32, +) -> Size<i32, Logical> { + let mut border = 0; + if !border_config.off { + border = border_config.width as i32 * 2; + } + + Size::from(( + max(working_area_size.w - gaps * 2 - border, 1), + max(working_area_size.h - gaps * 2 - border, 1), + )) +} diff --git a/src/window/mod.rs b/src/window/mod.rs index 3c46e9e5..a793cedb 100644 --- a/src/window/mod.rs +++ b/src/window/mod.rs @@ -1,4 +1,4 @@ -use niri_config::{BlockOutFrom, Match, WindowRule}; +use niri_config::{BlockOutFrom, BorderRule, Match, WindowRule}; use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel; use smithay::wayland::compositor::with_states; use smithay::wayland::shell::xdg::{ @@ -48,6 +48,9 @@ pub struct ResolvedWindowRules { /// Extra bound on the maximum window height. pub max_height: Option<u16>, + /// Window border overrides. + pub border: BorderRule, + /// Whether or not to draw the border with a solid background. /// /// `None` means using the SSD heuristic. @@ -87,6 +90,15 @@ impl ResolvedWindowRules { min_height: None, max_width: None, max_height: None, + border: BorderRule { + off: false, + on: false, + width: None, + active_color: None, + inactive_color: None, + active_gradient: None, + inactive_gradient: None, + }, draw_border_with_background: None, opacity: None, block_out_from: None, @@ -158,6 +170,8 @@ impl ResolvedWindowRules { resolved.max_height = Some(x); } + resolved.border.merge_with(&rule.border); + if let Some(x) = rule.draw_border_with_background { resolved.draw_border_with_background = Some(x); } diff --git a/wiki/Configuration:-Window-Rules.md b/wiki/Configuration:-Window-Rules.md index 869350c3..b459f8c0 100644 --- a/wiki/Configuration:-Window-Rules.md +++ b/wiki/Configuration:-Window-Rules.md @@ -45,6 +45,16 @@ window-rule { block-out-from "screencast" // block-out-from "screen-capture" + border { + // off + on + width 4 + active-color "#ffc87f" + inactive-color "#505050" + active-gradient from="#ffbb66" to="#ffc880" angle=45 relative-to="workspace-view" + inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view" + } + min-width 100 max-width 200 min-height 300 @@ -337,6 +347,26 @@ window-rule { } ``` +#### `border` + +<sup>Since: 0.1.6</sup> + +Override the border options for the window. + +This rule has the same options as the normal border config in the [layout](./Configuration:-Layout.md) section, so check the documentation there. + +However, in addition to `off` to disable the border, this window rule has an `on` flag that enables the border for the window even if the border is otherwise disabled. +The `on` flag has precedence over the `off` flag, in case both are set. + +``` +window-rule { + border { + on + width 8 + } +} +``` + #### Size Overrides You can amend the window's minimum and maximum size in logical pixels. |
