From 86570e72c75a09c8629fd422e079766edbe29f4a Mon Sep 17 00:00:00 2001 From: "{\"message\":\"Resource not accessible by integration\",\"documentation_url\":\"https://docs.github.com/rest/users/users#get-the-authenticated-user\",\"status\":\"403\"}" <{"message":"Resource not accessible by integration","documentation_url":"https://docs.github.com/rest/users/users#get-the-authenticated-user","status":"403"}> Date: Wed, 29 Apr 2026 03:15:56 +0000 Subject: [PATCH] Improve active tab indication for color-coded tabs (APP-4321) When tabs are color-coded (manually or via Directory tab colors), the active tab can be hard to distinguish from inactive colored tabs because the active fill is only modestly more saturated than the inactive one, and the border is the same generic gray for every tab. This change strengthens the active state in both tab bars: Standard (horizontal) tab bar: - Bumps the active tab's color fill opacity (60 -> 85 with the new tab styling, and 50 -> 80 in the legacy tab bar) so the active colored tab is clearly brighter than hovered/inactive neighbours. The legacy bar previously used the same opacity for active and hovered, which collapsed the two states. - Adds a saturated border in the tab's own color when the active tab is colored. Inactive colored tabs continue to use the existing neutral overlay border so they remain a subtle tint. Vertical tabs: - Splits the previously combined hover/selected fill opacity into three distinct values (inactive: 15, hover: 35, selected: 75) so the active row is clearly brighter than a merely-hovered row. - For colored selected rows, the row border now uses the row's own ANSI color at high saturation instead of the generic neutral overlay, giving the active colored tab a strong same-hue outline. Behavior for tabs without a custom color is unchanged in both bars. The new colors are derived from the existing ANSI palette, so they remain theme-aware and work in light and dark themes. Co-Authored-By: Oz --- app/src/tab.rs | 71 +++++++++++++++---------- app/src/workspace/view/vertical_tabs.rs | 41 +++++++++++--- 2 files changed, 78 insertions(+), 34 deletions(-) diff --git a/app/src/tab.rs b/app/src/tab.rs index b2bc28b2..150b251d 100644 --- a/app/src/tab.rs +++ b/app/src/tab.rs @@ -63,6 +63,20 @@ pub fn uses_vertical_tabs(ctx: &AppContext) -> bool { const WARP_2_TAB_COLOR_OPACITY: Opacity = 25; const WARP_2_HOVERED_TAB_COLOR_OPACITY: Opacity = 50; +/// Opacity applied to the colored fill of the active tab in the legacy +/// (non-`NewTabStyling`) tab bar. Higher than the hover/inactive opacities +/// so the active colored tab is clearly distinguishable. +const WARP_2_ACTIVE_TAB_COLOR_OPACITY: Opacity = 80; +/// Opacity used for the active tab's colored background in the +/// `NewTabStyling` tab bar. Bumped relative to hover/inactive so the active +/// colored tab is clearly brighter than its neighbours. +const NEW_TAB_ACTIVE_COLOR_OPACITY: u8 = 85; +const NEW_TAB_HOVERED_COLOR_OPACITY: u8 = 40; +const NEW_TAB_INACTIVE_COLOR_OPACITY: u8 = 20; +/// Opacity (0..=100) for the saturated colored border drawn around the +/// active tab when the tab has a custom color. Used by both legacy and new +/// tab styling. +const ACTIVE_TAB_COLOR_BORDER_OPACITY: Opacity = 95; const TAB_CLOSE_BUTTON_OPACITY: Opacity = 60; const TAB_CLOSE_BUTTON_WIDTH: f32 = 20.0; const MAX_TOOLTIP_LENGTH: usize = 80; @@ -1195,26 +1209,25 @@ impl<'a> TabComponent<'a> { let theme = self.appearance.theme(); let is_active = self.is_active_tab(); + let custom_background_solid = self.styles.background.map(|fill| match fill { + ThemeFill::Solid(color) => color, + ThemeFill::VerticalGradient(gradient) => gradient.get_most_opaque(), + ThemeFill::HorizontalGradient(gradient) => gradient.get_most_opaque(), + }); + let (background_color, border_fill) = if FeatureFlag::NewTabStyling.is_enabled() { - // If there is a custom tab background, we overlay it with varying opacities. - let bg = if let Some(custom_background) = self.styles.background { + // If there is a custom tab background, we overlay it with varying opacities so the + // active tab pops while inactive colored tabs remain a subtle tint. + let bg = if let Some(color) = custom_background_solid { let base_opacity = if is_active { - 60 + NEW_TAB_ACTIVE_COLOR_OPACITY } else if is_hovered { - 40 + NEW_TAB_HOVERED_COLOR_OPACITY } else { - 20 + NEW_TAB_INACTIVE_COLOR_OPACITY }; let opacity = (base_opacity as f32 * self.background_opacity as f32 / 100.) as u8; - match custom_background { - ThemeFill::Solid(color) => coloru_with_opacity(color, opacity).into(), - ThemeFill::VerticalGradient(gradient) => { - coloru_with_opacity(gradient.get_most_opaque(), opacity).into() - } - ThemeFill::HorizontalGradient(gradient) => { - coloru_with_opacity(gradient.get_most_opaque(), opacity).into() - } - } + coloru_with_opacity(color, opacity).into() } else if is_active { internal_colors::fg_overlay_2(theme).into() } else if is_hovered { @@ -1224,34 +1237,38 @@ impl<'a> TabComponent<'a> { }; let border = if is_active { - internal_colors::fg_overlay_2(theme) + if let Some(color) = custom_background_solid { + ThemeFill::Solid(coloru_with_opacity(color, ACTIVE_TAB_COLOR_BORDER_OPACITY)) + } else { + internal_colors::fg_overlay_2(theme) + } } else { internal_colors::fg_overlay_1(theme) }; (bg, border) } else { - let tab_opacity = if is_active || is_hovered { + let tab_opacity = if is_active { + WARP_2_ACTIVE_TAB_COLOR_OPACITY + } else if is_hovered { WARP_2_HOVERED_TAB_COLOR_OPACITY } else { WARP_2_TAB_COLOR_OPACITY }; - let bg = if let Some(custom_background) = self.styles.background { - match custom_background { - ThemeFill::Solid(color) => coloru_with_opacity(color, tab_opacity).into(), - ThemeFill::VerticalGradient(gradient) => { - coloru_with_opacity(gradient.get_most_opaque(), tab_opacity).into() - } - ThemeFill::HorizontalGradient(gradient) => { - coloru_with_opacity(gradient.get_most_opaque(), tab_opacity).into() - } - } + let bg = if let Some(color) = custom_background_solid { + coloru_with_opacity(color, tab_opacity).into() } else { coloru_with_opacity(theme.surface_3().into(), tab_opacity).into() }; - let border = if is_active || is_hovered { + let border = if is_active { + if let Some(color) = custom_background_solid { + ThemeFill::Solid(coloru_with_opacity(color, ACTIVE_TAB_COLOR_BORDER_OPACITY)) + } else { + internal_colors::fg_overlay_2(theme) + } + } else if is_hovered { internal_colors::fg_overlay_2(theme) } else { internal_colors::fg_overlay_1(theme) diff --git a/app/src/workspace/view/vertical_tabs.rs b/app/src/workspace/view/vertical_tabs.rs index 8b52af66..611a9342 100644 --- a/app/src/workspace/view/vertical_tabs.rs +++ b/app/src/workspace/view/vertical_tabs.rs @@ -103,7 +103,12 @@ const DETAIL_SIDECAR_CORNER_RADIUS: f32 = 4.; /// so the row doesn't resize when badges are toggled. const METADATA_ROW_HEIGHT: f32 = BADGE_ICON_SIZE + 2.; const TAB_COLOR_OPACITY: Opacity = 15; -const TAB_COLOR_HOVER_OPACITY: Opacity = 50; +const TAB_COLOR_HOVER_OPACITY: Opacity = 35; +const TAB_COLOR_SELECTED_OPACITY: Opacity = 75; +/// Opacity applied to the tab color when it is used as the active row's +/// border. Boosts saturation so the colored border reads as a clear active +/// indicator on top of the same color used (more faintly) as the row fill. +const TAB_COLOR_ACTIVE_BORDER_OPACITY: Opacity = 90; // Circular icon constants const ICON_WITH_STATUS_GAP: f32 = 8.; @@ -289,7 +294,11 @@ fn pane_row_background( theme: &WarpTheme, ) -> Option { if let Some(color) = pane_color { - let opacity = if is_selected || is_hovered { + // Distinct opacities for selected vs. hovered so the active colored + // tab is clearly brighter than a merely-hovered one. + let opacity = if is_selected { + TAB_COLOR_SELECTED_OPACITY + } else if is_hovered { TAB_COLOR_HOVER_OPACITY } else { TAB_COLOR_OPACITY @@ -304,6 +313,24 @@ fn pane_row_background( } } +/// Border fill for a vertical tab pane row. When the row is selected and +/// colored, render a saturated border in the same ANSI color so the active +/// colored tab stands out from inactive colored tabs. +fn pane_row_border_fill( + pane_color: Option, + is_selected: bool, + theme: &WarpTheme, +) -> ElementFill { + if !is_selected { + return ElementFill::None; + } + if let Some(color) = pane_color { + color.with_opacity(TAB_COLOR_ACTIVE_BORDER_OPACITY).into() + } else { + internal_colors::fg_overlay_3(theme).into() + } +} + fn render_pane_row_element( props: PaneProps<'_>, padding: Padding, @@ -360,11 +387,11 @@ fn render_pane_row_element( } container - .with_border(Border::all(1.).with_border_fill(if is_selected { - internal_colors::fg_overlay_3(theme).into() - } else { - ElementFill::None - })) + .with_border(Border::all(1.).with_border_fill(pane_row_border_fill( + pane_color, + is_selected, + theme, + ))) .finish() }) .on_click(move |ctx, _, _| {