From a10705fb200b452802a1ba7cd47679536e0ef849 Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Thu, 23 Jan 2025 10:40:52 +0300 Subject: [PATCH] Add toggle-window-rule-opacity action --- niri-config/src/lib.rs | 7 +++++++ niri-ipc/src/lib.rs | 12 ++++++++++++ niri-visual-tests/src/test_window.rs | 4 ++++ src/input/mod.rs | 29 ++++++++++++++++++++++++++++ src/layout/floating.rs | 8 ++++++++ src/layout/mod.rs | 5 +++++ src/layout/scrolling.rs | 9 +++++++++ src/layout/tile.rs | 2 +- src/layout/workspace.rs | 8 ++++++++ src/niri.rs | 2 +- src/window/mapped.rs | 18 +++++++++++++++++ wiki/Configuration:-Key-Bindings.md | 11 +++++++++++ wiki/Configuration:-Window-Rules.md | 2 ++ 13 files changed, 115 insertions(+), 2 deletions(-) diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index db301151e..2fd3d45c0 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -1425,6 +1425,9 @@ pub enum Action { x: PositionChange, y: PositionChange, }, + ToggleWindowRuleOpacity, + #[knuffel(skip)] + ToggleWindowRuleOpacityById(u64), } impl From for Action { @@ -1623,6 +1626,10 @@ impl From for Action { niri_ipc::Action::MoveFloatingWindow { id, x, y } => { Self::MoveFloatingWindowById { id, x, y } } + niri_ipc::Action::ToggleWindowRuleOpacity { id: None } => Self::ToggleWindowRuleOpacity, + niri_ipc::Action::ToggleWindowRuleOpacity { id: Some(id) } => { + Self::ToggleWindowRuleOpacityById(id) + } } } } diff --git a/niri-ipc/src/lib.rs b/niri-ipc/src/lib.rs index f8d393654..513da5780 100644 --- a/niri-ipc/src/lib.rs +++ b/niri-ipc/src/lib.rs @@ -578,6 +578,18 @@ pub enum Action { )] y: PositionChange, }, + /// Toggle the opacity of a window. + #[cfg_attr( + feature = "clap", + clap(about = "Toggle the opacity of the focused window") + )] + ToggleWindowRuleOpacity { + /// Id of the window. + /// + /// If `None`, uses the focused window. + #[cfg_attr(feature = "clap", arg(long))] + id: Option, + }, } /// Change in window or column size. diff --git a/niri-visual-tests/src/test_window.rs b/niri-visual-tests/src/test_window.rs index 51f9d11fe..228701c72 100644 --- a/niri-visual-tests/src/test_window.rs +++ b/niri-visual-tests/src/test_window.rs @@ -224,6 +224,10 @@ impl LayoutElement for TestWindow { fn set_bounds(&self, _bounds: Size) {} + fn is_ignoring_opacity_window_rule(&self) -> bool { + false + } + fn configure_intent(&self) -> ConfigureIntent { ConfigureIntent::CanSend } diff --git a/src/input/mod.rs b/src/input/mod.rs index 8872df780..c9ab0b307 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -39,6 +39,7 @@ use self::move_grab::MoveGrab; use self::resize_grab::ResizeGrab; use self::spatial_movement_grab::SpatialMovementGrab; use crate::layout::scrolling::ScrollDirection; +use crate::layout::LayoutElement as _; use crate::niri::State; use crate::ui::screenshot_ui::ScreenshotUi; use crate::utils::spawning::spawn; @@ -1574,6 +1575,34 @@ impl State { // FIXME: granular self.niri.queue_redraw_all(); } + Action::ToggleWindowRuleOpacity => { + let active_window = self + .niri + .layout + .active_workspace_mut() + .and_then(|ws| ws.active_window_mut()); + if let Some(window) = active_window { + if window.rules().opacity.is_some_and(|o| o != 1.) { + window.toggle_ignore_opacity_window_rule(); + // FIXME: granular + self.niri.queue_redraw_all(); + } + } + } + Action::ToggleWindowRuleOpacityById(id) => { + let window = self + .niri + .layout + .workspaces_mut() + .find_map(|ws| ws.windows_mut().find(|w| w.id().get() == id)); + if let Some(window) = window { + if window.rules().opacity.is_some_and(|o| o != 1.) { + window.toggle_ignore_opacity_window_rule(); + // FIXME: granular + self.niri.queue_redraw_all(); + } + } + } } } diff --git a/src/layout/floating.rs b/src/layout/floating.rs index cb757d67c..c6a17e302 100644 --- a/src/layout/floating.rs +++ b/src/layout/floating.rs @@ -365,6 +365,14 @@ impl FloatingSpace { .map(Tile::window) } + pub fn active_window_mut(&mut self) -> Option<&mut W> { + let id = self.active_window_id.as_ref()?; + self.tiles + .iter_mut() + .find(|tile| tile.window().id() == id) + .map(Tile::window_mut) + } + pub fn has_window(&self, id: &W::Id) -> bool { self.tiles.iter().any(|tile| tile.window().id() == id) } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index eb702c37b..63bedc7f0 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -190,6 +190,7 @@ pub trait LayoutElement { fn set_active_in_column(&mut self, active: bool); fn set_floating(&mut self, floating: bool); fn set_bounds(&self, bounds: Size); + fn is_ignoring_opacity_window_rule(&self) -> bool; fn configure_intent(&self) -> ConfigureIntent; fn send_pending_configure(&mut self); @@ -4347,6 +4348,10 @@ mod tests { fn set_bounds(&self, _bounds: Size) {} + fn is_ignoring_opacity_window_rule(&self) -> bool { + false + } + fn configure_intent(&self) -> ConfigureIntent { ConfigureIntent::CanSend } diff --git a/src/layout/scrolling.rs b/src/layout/scrolling.rs index 6b8bf7602..ffb51acea 100644 --- a/src/layout/scrolling.rs +++ b/src/layout/scrolling.rs @@ -383,6 +383,15 @@ impl ScrollingSpace { Some(col.tiles[col.active_tile_idx].window()) } + pub fn active_window_mut(&mut self) -> Option<&mut W> { + if self.columns.is_empty() { + return None; + } + + let col = &mut self.columns[self.active_column_idx]; + Some(col.tiles[col.active_tile_idx].window_mut()) + } + pub fn active_tile_mut(&mut self) -> Option<&mut Tile> { if self.columns.is_empty() { return None; diff --git a/src/layout/tile.rs b/src/layout/tile.rs index 4a5b61a9b..b8d049b00 100644 --- a/src/layout/tile.rs +++ b/src/layout/tile.rs @@ -720,7 +720,7 @@ impl Tile { ) -> impl Iterator> + 'a { let _span = tracy_client::span!("Tile::render_inner"); - let alpha = if self.is_fullscreen { + let alpha = if self.is_fullscreen || self.window.is_ignoring_opacity_window_rule() { 1. } else { self.window.rules().opacity.unwrap_or(1.).clamp(0., 1.) diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index f4c87342f..4426c2642 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -406,6 +406,14 @@ impl Workspace { } } + pub fn active_window_mut(&mut self) -> Option<&mut W> { + if self.floating_is_active.get() { + self.floating.active_window_mut() + } else { + self.scrolling.active_window_mut() + } + } + pub fn is_active_fullscreen(&self) -> bool { self.scrolling.is_active_fullscreen() } diff --git a/src/niri.rs b/src/niri.rs index a578ced34..687f85393 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -4597,7 +4597,7 @@ impl Niri { let _span = tracy_client::span!("Niri::screenshot_window"); let scale = Scale::from(output.current_scale().fractional_scale()); - let alpha = if mapped.is_fullscreen() { + let alpha = if mapped.is_fullscreen() || mapped.is_ignoring_opacity_window_rule() { 1. } else { mapped.rules().opacity.unwrap_or(1.).clamp(0., 1.) diff --git a/src/window/mapped.rs b/src/window/mapped.rs index 207140c4c..71b6c9da0 100644 --- a/src/window/mapped.rs +++ b/src/window/mapped.rs @@ -69,6 +69,9 @@ pub struct Mapped { /// Whether this window is floating. is_floating: bool, + /// Whether this window should ignore opacity set through window rules. + ignore_opacity_window_rule: bool, + /// Buffer to draw instead of the window when it should be blocked out. block_out_buffer: RefCell, @@ -167,6 +170,7 @@ impl Mapped { is_focused: false, is_active_in_column: true, is_floating: false, + ignore_opacity_window_rule: false, block_out_buffer: RefCell::new(SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.])), animate_next_configure: false, animate_serials: Vec::new(), @@ -192,6 +196,12 @@ impl Mapped { return false; } + // If the opacity window rule no longer makes the window semitransparent, reset the ignore + // flag to reduce surprises down the line. + if !new_rules.opacity.is_some_and(|o| o < 1.) { + self.ignore_opacity_window_rule = false; + } + self.rules = new_rules; true } @@ -228,6 +238,10 @@ impl Mapped { self.is_floating } + pub fn toggle_ignore_opacity_window_rule(&mut self) { + self.ignore_opacity_window_rule = !self.ignore_opacity_window_rule; + } + pub fn set_is_focused(&mut self, is_focused: bool) { if self.is_focused == is_focused { return; @@ -839,6 +853,10 @@ impl LayoutElement for Mapped { .with_pending_state(|state| state.states.contains(xdg_toplevel::State::Fullscreen)) } + fn is_ignoring_opacity_window_rule(&self) -> bool { + self.ignore_opacity_window_rule + } + fn requested_size(&self) -> Option> { self.toplevel().with_pending_state(|state| state.size) } diff --git a/wiki/Configuration:-Key-Bindings.md b/wiki/Configuration:-Key-Bindings.md index 5893d61e4..2331f8a96 100644 --- a/wiki/Configuration:-Key-Bindings.md +++ b/wiki/Configuration:-Key-Bindings.md @@ -253,3 +253,14 @@ Or, in scripts: ```shell niri msg action do-screen-transition --delay-ms 100 ``` + +#### `toggle-window-rule-opacity` + +Toggle the opacity window rule of the focused window. +This only has an effect if the window's opacity window rule is already set to semitransparent. + +```kdl +binds { + Mod+O { toggle-window-rule-opacity; } +} +``` diff --git a/wiki/Configuration:-Window-Rules.md b/wiki/Configuration:-Window-Rules.md index d3fb9068b..cd0a65b22 100644 --- a/wiki/Configuration:-Window-Rules.md +++ b/wiki/Configuration:-Window-Rules.md @@ -492,6 +492,8 @@ This is applied on top of the window's own opacity, so semitransparent windows w Opacity is applied to every surface of the window individually, so subsurfaces and pop-up menus will show window content behind them. +Opacity can be toggled on or off for a window using the [`toggle-window-rule-opacity`](./Configuration:-Key-Bindings.md) action. + ![Screenshot showing Adwaita Demo with a semitransparent pop-up menu.](./img/opacity-popup.png) Also, focus ring and border with background will show through semitransparent windows (see `prefer-no-csd` and the `draw-border-with-background` window rule below).