diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2025-02-02 08:41:42 +0300 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2025-02-10 07:29:33 -0800 |
| commit | a451f75917e427a4b4c7d986ce74e5a3999e563d (patch) | |
| tree | 0e0f74e81e85485d5ff07f2ceb5567d4059a06d2 /src/layout | |
| parent | 1515410012842c419e8bc3de717b8a51b1a0b45c (diff) | |
| download | niri-a451f75917e427a4b4c7d986ce74e5a3999e563d.tar.gz niri-a451f75917e427a4b4c7d986ce74e5a3999e563d.tar.bz2 niri-a451f75917e427a4b4c7d986ce74e5a3999e563d.zip | |
Implement tab indicators
Diffstat (limited to 'src/layout')
| -rw-r--r-- | src/layout/focus_ring.rs | 4 | ||||
| -rw-r--r-- | src/layout/mod.rs | 4 | ||||
| -rw-r--r-- | src/layout/scrolling.rs | 77 | ||||
| -rw-r--r-- | src/layout/tab_indicator.rs | 201 | ||||
| -rw-r--r-- | src/layout/tests.rs | 21 | ||||
| -rw-r--r-- | src/layout/tile.rs | 12 |
6 files changed, 306 insertions, 13 deletions
diff --git a/src/layout/focus_ring.rs b/src/layout/focus_ring.rs index 411a73a5..047fc3b7 100644 --- a/src/layout/focus_ring.rs +++ b/src/layout/focus_ring.rs @@ -262,4 +262,8 @@ impl FocusRing { pub fn is_off(&self) -> bool { self.config.off } + + pub fn config(&self) -> &niri_config::FocusRing { + &self.config + } } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index c9c4e72d..a49ec0e6 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -80,6 +80,7 @@ pub mod monitor; pub mod opening_window; pub mod scrolling; pub mod shadow; +pub mod tab_indicator; pub mod tile; pub mod workspace; @@ -312,6 +313,7 @@ pub struct Options { pub focus_ring: niri_config::FocusRing, pub border: niri_config::Border, pub shadow: niri_config::Shadow, + pub tab_indicator: niri_config::TabIndicator, pub insert_hint: niri_config::InsertHint, pub center_focused_column: CenterFocusedColumn, pub always_center_single_column: bool, @@ -337,6 +339,7 @@ impl Default for Options { focus_ring: Default::default(), border: Default::default(), shadow: Default::default(), + tab_indicator: Default::default(), insert_hint: Default::default(), center_focused_column: Default::default(), always_center_single_column: false, @@ -550,6 +553,7 @@ impl Options { focus_ring: layout.focus_ring, border: layout.border, shadow: layout.shadow, + tab_indicator: layout.tab_indicator, insert_hint: layout.insert_hint, center_focused_column: layout.center_focused_column, always_center_single_column: layout.always_center_single_column, diff --git a/src/layout/scrolling.rs b/src/layout/scrolling.rs index b9799ce4..42de18c8 100644 --- a/src/layout/scrolling.rs +++ b/src/layout/scrolling.rs @@ -11,6 +11,7 @@ use smithay::utils::{Logical, Point, Rectangle, Scale, Serial, Size}; use super::closing_window::{ClosingWindow, ClosingWindowRenderElement}; use super::insert_hint_element::{InsertHintElement, InsertHintRenderElement}; +use super::tab_indicator::{TabIndicator, TabIndicatorRenderElement, TabInfo}; use super::tile::{Tile, TileRenderElement, TileRenderSnapshot}; use super::workspace::{InteractiveResize, ResolvedSize}; use super::{ConfigureIntent, InteractiveResizeData, LayoutElement, Options, RemovedTile}; @@ -94,6 +95,7 @@ niri_render_elements! { ScrollingSpaceRenderElement<R> => { Tile = TileRenderElement<R>, ClosingWindow = ClosingWindowRenderElement, + TabIndicator = TabIndicatorRenderElement, InsertHint = InsertHintRenderElement, } } @@ -174,6 +176,9 @@ pub struct Column<W: LayoutElement> { /// How this column displays and arranges windows. display_mode: ColumnDisplay, + /// Tab indicator for the tabbed display mode. + tab_indicator: TabIndicator, + /// Animation of the render offset during window swapping. move_animation: Option<Animation>, @@ -2527,19 +2532,44 @@ impl<W: LayoutElement> ScrollingSpace<W> { } let mut first = true; - for (tile, tile_pos, visible) in self.tiles_with_render_positions() { - // For the active tile (which comes first), draw the focus ring. - let focus_ring = focus_ring && first; - first = false; - if !visible { - continue; + // This matches self.tiles_in_render_order(). + let view_off = Point::from((-self.view_pos(), 0.)); + for (col, col_x) in self.columns_in_render_order() { + let col_off = Point::from((col_x, 0.)); + let col_render_off = col.render_offset(); + + // Draw the tab indicator on top. + { + // This is the "static tile position" so to say: it excludes the tile offset (used + // for e.g. centering smaller tiles in always-center) and the tile render offset + // (used for tile-specific animations). + let pos = view_off + col_off + col_render_off + col.tiles_origin(); + let pos = pos.to_physical_precise_round(scale).to_logical(scale); + rv.extend(col.tab_indicator.render(renderer, pos).map(Into::into)); } - rv.extend( - tile.render(renderer, tile_pos, scale, focus_ring, target) - .map(Into::into), - ); + for (tile, tile_off, visible) in col.tiles_in_render_order() { + let tile_pos = + view_off + col_off + col_render_off + tile_off + tile.render_offset(); + // Round to physical pixels. + let tile_pos = tile_pos.to_physical_precise_round(scale).to_logical(scale); + + // And now the drawing logic. + + // For the active tile (which comes first), draw the focus ring. + let focus_ring = focus_ring && first; + first = false; + + if !visible { + continue; + } + + rv.extend( + tile.render(renderer, tile_pos, scale, focus_ring, target) + .map(Into::into), + ); + } } rv @@ -3268,6 +3298,7 @@ impl<W: LayoutElement> Column<W> { is_full_width, is_fullscreen: false, display_mode, + tab_indicator: TabIndicator::new(options.tab_indicator), move_animation: None, view_size, working_area, @@ -3326,6 +3357,7 @@ impl<W: LayoutElement> Column<W> { data.update(tile); } + self.tab_indicator.update_config(options.tab_indicator); self.view_size = view_size; self.working_area = working_area; self.scale = scale; @@ -3361,6 +3393,31 @@ impl<W: LayoutElement> Column<W> { tile_view_rect.loc -= tile_off + tile.render_offset(); tile.update_render_elements(is_active, tile_view_rect); } + + let (tile, tile_off) = self.tiles().nth(self.active_tile_idx).unwrap(); + let mut tile_view_rect = view_rect; + tile_view_rect.loc -= tile_off + tile.render_offset(); + + let config = self.tab_indicator.config(); + let tabs = self.tiles.iter().enumerate().map(|(tile_idx, tile)| { + let is_active = tile_idx == active_idx; + TabInfo::from_tile(tile, is_active, &config) + }); + + // Hide the tab indicator in fullscreen. If you have it configured to overlap the window, + // you don't want that to happen in fullscreen. Also, laying things out correctly when the + // tab indicator is within the column and the column goes fullscreen, would require too + // many changes to the code for too little benefit (it's mostly invisible anyway). + let enabled = self.display_mode == ColumnDisplay::Tabbed && !self.is_fullscreen; + + self.tab_indicator.update_render_elements( + enabled, + tile.animated_tile_size(), + tile_view_rect, + tabs, + is_active, + self.scale, + ); } pub fn render_offset(&self) -> Point<f64, Logical> { diff --git a/src/layout/tab_indicator.rs b/src/layout/tab_indicator.rs new file mode 100644 index 00000000..114fa810 --- /dev/null +++ b/src/layout/tab_indicator.rs @@ -0,0 +1,201 @@ +use std::iter::zip; + +use niri_config::{CornerRadius, Gradient, GradientRelativeTo}; +use smithay::utils::{Logical, Point, Rectangle, Size}; + +use super::tile::Tile; +use super::LayoutElement; +use crate::niri_render_elements; +use crate::render_helpers::border::BorderRenderElement; +use crate::render_helpers::renderer::NiriRenderer; +use crate::utils::{floor_logical_in_physical_max1, round_logical_in_physical}; + +#[derive(Debug)] +pub struct TabIndicator { + shader_locs: Vec<Point<f64, Logical>>, + shaders: Vec<BorderRenderElement>, + config: niri_config::TabIndicator, +} + +#[derive(Debug)] +pub struct TabInfo { + pub gradient: Gradient, +} + +niri_render_elements! { + TabIndicatorRenderElement => { + Gradient = BorderRenderElement, + } +} + +impl TabIndicator { + pub fn new(config: niri_config::TabIndicator) -> Self { + Self { + shader_locs: Vec::new(), + shaders: Vec::new(), + config, + } + } + + pub fn update_config(&mut self, config: niri_config::TabIndicator) { + self.config = config; + } + + pub fn update_shaders(&mut self) { + for elem in &mut self.shaders { + elem.damage_all(); + } + } + + pub fn update_render_elements( + &mut self, + enabled: bool, + tile_size: Size<f64, Logical>, + tile_view_rect: Rectangle<f64, Logical>, + tabs: impl Iterator<Item = TabInfo> + Clone, + // TODO: do we indicate inactive-but-selected somehow? + _is_active: bool, + scale: f64, + ) { + if !enabled || self.config.off { + self.shader_locs.clear(); + self.shaders.clear(); + return; + } + + // Tab indicators are rendered relative to the tile geometry. + let tile_geo = Rectangle::new(Point::from((0., 0.)), tile_size); + + let round = |logical: f64| round_logical_in_physical(scale, logical); + + let width = round(self.config.width.0); + let gap = round(self.config.gap.0); + + let total_prop = self.config.length.total_proportion.unwrap_or(0.5); + let min_length = round(tile_size.h * total_prop.clamp(0., 2.)); + + let count = tabs.clone().count(); + self.shaders.resize_with(count, Default::default); + self.shader_locs.resize_with(count, Default::default); + + let pixel = 1. / scale; + let shortest_length = count as f64 * pixel; + let length = f64::max(min_length, shortest_length); + let px_per_tab = length / count as f64; + let px_per_tab = floor_logical_in_physical_max1(scale, px_per_tab); + let floored_length = count as f64 * px_per_tab; + let mut ones_left = ((length - floored_length) / pixel).max(0.).round() as usize; + + let mut shader_loc = Point::from((-gap - width, round((tile_size.h - length) / 2.))); + + for ((shader, loc), tab) in zip(&mut self.shaders, &mut self.shader_locs).zip(tabs) { + *loc = shader_loc; + + let mut px_per_tab = px_per_tab; + if ones_left > 0 { + ones_left -= 1; + px_per_tab += pixel; + } + shader_loc.y += px_per_tab; + + let shader_size = Size::from((width, px_per_tab)); + + let mut gradient_area = match tab.gradient.relative_to { + GradientRelativeTo::Window => tile_geo, + GradientRelativeTo::WorkspaceView => tile_view_rect, + }; + gradient_area.loc -= *loc; + + shader.update( + shader_size, + gradient_area, + tab.gradient.in_, + tab.gradient.from, + tab.gradient.to, + ((tab.gradient.angle as f32) - 90.).to_radians(), + Rectangle::from_size(shader_size), + 0., + CornerRadius::default(), + scale as f32, + 1., + ); + } + } + + pub fn render( + &self, + renderer: &mut impl NiriRenderer, + tile_pos: Point<f64, Logical>, + ) -> impl Iterator<Item = TabIndicatorRenderElement> + '_ { + let has_border_shader = BorderRenderElement::has_shader(renderer); + if !has_border_shader { + return None.into_iter().flatten(); + } + + let rv = zip(&self.shaders, &self.shader_locs) + .map(move |(shader, loc)| shader.clone().with_location(tile_pos + *loc)) + .map(TabIndicatorRenderElement::from); + + Some(rv).into_iter().flatten() + } + + pub fn config(&self) -> niri_config::TabIndicator { + self.config + } +} + +impl TabInfo { + pub fn from_tile<W: LayoutElement>( + tile: &Tile<W>, + is_active: bool, + config: &niri_config::TabIndicator, + ) -> Self { + let rules = tile.window().rules(); + let rule = rules.tab_indicator; + + let gradient_from_rule = || { + let (color, gradient) = if is_active { + (rule.active_color, rule.active_gradient) + } else { + (rule.inactive_color, rule.inactive_gradient) + }; + let color = color.map(Gradient::from); + gradient.or(color) + }; + + let gradient_from_config = || { + let (color, gradient) = if is_active { + (config.active_color, config.active_gradient) + } else { + (config.inactive_color, config.inactive_gradient) + }; + let color = color.map(Gradient::from); + gradient.or(color) + }; + + let gradient_from_border = || { + // Come up with tab indicator gradient matching the focus ring or the border, whichever + // one is enabled. + let focus_ring_config = tile.focus_ring().config(); + let border_config = tile.border().config(); + let config = if focus_ring_config.off { + border_config + } else { + focus_ring_config + }; + + let (color, gradient) = if is_active { + (config.active_color, config.active_gradient) + } else { + (config.inactive_color, config.inactive_gradient) + }; + gradient.unwrap_or_else(|| Gradient::from(color)) + }; + + let gradient = gradient_from_rule() + .or_else(gradient_from_config) + .unwrap_or_else(gradient_from_border); + + TabInfo { gradient } + } +} diff --git a/src/layout/tests.rs b/src/layout/tests.rs index 8f3e4d58..5c1749a3 100644 --- a/src/layout/tests.rs +++ b/src/layout/tests.rs @@ -1,6 +1,6 @@ use std::cell::Cell; -use niri_config::{FloatOrInt, OutputName, WorkspaceName, WorkspaceReference}; +use niri_config::{FloatOrInt, OutputName, TabIndicatorLength, WorkspaceName, WorkspaceReference}; use proptest::prelude::*; use proptest_derive::Arbitrary; use smithay::output::{Mode, PhysicalProperties, Subpixel}; @@ -3240,12 +3240,30 @@ prop_compose! { } prop_compose! { + fn arbitrary_tab_indicator()( + off in any::<bool>(), + width in arbitrary_spacing(), + gap in arbitrary_spacing_neg(), + length in (0f64..2f64), + ) -> niri_config::TabIndicator { + niri_config::TabIndicator { + off, + width: FloatOrInt(width), + gap: FloatOrInt(gap), + length: TabIndicatorLength { total_proportion: Some(length) }, + ..Default::default() + } + } +} + +prop_compose! { fn arbitrary_options()( gaps in arbitrary_spacing(), struts in arbitrary_struts(), focus_ring in arbitrary_focus_ring(), border in arbitrary_border(), shadow in arbitrary_shadow(), + tab_indicator in arbitrary_tab_indicator(), center_focused_column in arbitrary_center_focused_column(), always_center_single_column in any::<bool>(), empty_workspace_above_first in any::<bool>(), @@ -3259,6 +3277,7 @@ prop_compose! { focus_ring, border, shadow, + tab_indicator, ..Default::default() } } diff --git a/src/layout/tile.rs b/src/layout/tile.rs index a08f058f..7a49f5ed 100644 --- a/src/layout/tile.rs +++ b/src/layout/tile.rs @@ -563,7 +563,7 @@ impl<W: LayoutElement> Tile<W> { size } - fn animated_window_size(&self) -> Size<f64, Logical> { + pub fn animated_window_size(&self) -> Size<f64, Logical> { let mut size = self.window_size(); if let Some(resize) = &self.resize_animation { @@ -580,7 +580,7 @@ impl<W: LayoutElement> Tile<W> { size } - fn animated_tile_size(&self) -> Size<f64, Logical> { + pub fn animated_tile_size(&self) -> Size<f64, Logical> { let mut size = self.animated_window_size(); if self.is_fullscreen { @@ -1031,6 +1031,14 @@ impl<W: LayoutElement> Tile<W> { self.unmap_snapshot.take() } + pub fn border(&self) -> &FocusRing { + &self.border + } + + pub fn focus_ring(&self) -> &FocusRing { + &self.focus_ring + } + pub fn options(&self) -> &Rc<Options> { &self.options } |
