use std::iter::zip; use std::mem; use niri_config::{CornerRadius, Gradient, GradientRelativeTo, TabIndicatorPosition}; 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>, shaders: Vec, 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, tile_view_rect: Rectangle, tabs: impl Iterator + 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; } let count = tabs.clone().count(); if self.config.hide_when_single_tab && count == 1 { 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 gaps_between = round(self.config.gaps_between_tabs.0); let side = match self.config.position { TabIndicatorPosition::Left | TabIndicatorPosition::Right => tile_size.h, TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => tile_size.w, }; let total_prop = self.config.length.total_proportion.unwrap_or(0.5); let min_length = round(side * total_prop.clamp(0., 2.)); 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 + gaps_between) - gaps_between; let length = f64::max(min_length, shortest_length); let px_per_tab = (length + gaps_between) / count as f64 - gaps_between; let px_per_tab = floor_logical_in_physical_max1(scale, px_per_tab); let floored_length = count as f64 * (px_per_tab + gaps_between) - gaps_between; let mut ones_left = ((length - floored_length) / pixel).max(0.).round() as usize; let mut shader_loc = Point::from((-gap - width, round((side - length) / 2.))); match self.config.position { TabIndicatorPosition::Left => (), TabIndicatorPosition::Right => shader_loc.x = tile_size.w + gap, TabIndicatorPosition::Top => mem::swap(&mut shader_loc.x, &mut shader_loc.y), TabIndicatorPosition::Bottom => { shader_loc.x = shader_loc.y; shader_loc.y = tile_size.h + gap; } } 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; } match self.config.position { TabIndicatorPosition::Left | TabIndicatorPosition::Right => { shader_loc.y += px_per_tab + gaps_between } TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => { shader_loc.x += px_per_tab + gaps_between } } let shader_size = match self.config.position { TabIndicatorPosition::Left | TabIndicatorPosition::Right => { Size::from((width, px_per_tab)) } TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => { Size::from((px_per_tab, width)) } }; 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, ) -> impl Iterator + '_ { 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() } /// Extra size occupied by the tab indicator. pub fn extra_size(&self, tab_count: usize, scale: f64) -> Size { if self.config.off || !self.config.place_within_column || (self.config.hide_when_single_tab && tab_count == 1) { return Size::from((0., 0.)); } let round = |logical: f64| round_logical_in_physical(scale, logical); let width = round(self.config.width.0); let gap = round(self.config.gap.0); // No, I am *not* falling into the rabbit hole of "what if the tab indicator is wide enough // that it peeks from the other side of the window". let size = f64::max(0., width + gap); match self.config.position { TabIndicatorPosition::Left | TabIndicatorPosition::Right => Size::from((size, 0.)), TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => Size::from((0., size)), } } /// Offset of the tabbed content due to space occupied by the tab indicator. pub fn content_offset(&self, tab_count: usize, scale: f64) -> Point { match self.config.position { TabIndicatorPosition::Left | TabIndicatorPosition::Top => { self.extra_size(tab_count, scale).to_point() } TabIndicatorPosition::Right | TabIndicatorPosition::Bottom => Point::from((0., 0.)), } } pub fn config(&self) -> niri_config::TabIndicator { self.config } } impl TabInfo { pub fn from_tile( tile: &Tile, 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 } } }