aboutsummaryrefslogtreecommitdiff
path: root/src/layout
diff options
context:
space:
mode:
Diffstat (limited to 'src/layout')
-rw-r--r--src/layout/focus_ring.rs4
-rw-r--r--src/layout/mod.rs4
-rw-r--r--src/layout/scrolling.rs77
-rw-r--r--src/layout/tab_indicator.rs201
-rw-r--r--src/layout/tests.rs21
-rw-r--r--src/layout/tile.rs12
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
}