diff --git a/app/src/tab.rs b/app/src/tab.rs index 7fdeb9b8..855764a3 100644 --- a/app/src/tab.rs +++ b/app/src/tab.rs @@ -1414,7 +1414,9 @@ impl<'a> TabComponent<'a> { Fill::None }; - let border = if is_active { + // Brighten the border on hover (matching the active treatment) so an + // inactive tab gives clear feedback under the cursor. + let border = if is_active || is_hovered { internal_colors::fg_overlay_2(theme) } else { internal_colors::fg_overlay_1(theme) @@ -1470,7 +1472,11 @@ impl<'a> TabComponent<'a> { .finish(), ); Container::new(flex_row.finish()) - .with_horizontal_padding(8.) + .with_horizontal_padding(if FeatureFlag::NewTabStyling.is_enabled() { + 10. + } else { + 8. + }) .finish() }; @@ -1612,22 +1618,56 @@ impl<'a> TabComponent<'a> { .with_vertical_padding(2.) .with_background(background_color); if FeatureFlag::NewTabStyling.is_enabled() { - let is_first_tab = self.tab_index == 0; - tab = tab.with_border( - Border::all(1.) - // We only include a left border on the very first tab to avoid double borders. - .with_sides(false, is_first_tab, false, true) - .with_border_fill(border_fill), - ); + // Floating browser-style card: rounded top corners with a flat bottom + // that merges into the content area below. The active tab's accent + // underline (added below) sits along that flat edge. Tabs are spaced + // by a small gap (added in `build`) rather than abutting separators. + tab = tab.with_corner_radius(CornerRadius::with_top(Radius::Pixels(6.0))); + // Only the active/hovered card draws a subtle outline (top + sides, + // open bottom); idle tabs stay borderless for a calm, uncluttered + // strip. + if is_active || is_hovered { + tab = tab.with_border( + Border::all(1.) + .with_sides(true, true, false, true) + .with_border_fill(border_fill), + ); + } } else { tab = tab .with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.0))) .with_border(Border::all(1.).with_border_fill(border_fill)); } + // Mark the active tab with a full-width accent underline pinned to its + // bottom edge, visually connecting it to the content area below. The bar + // is a pure overlay (`ParentBySize` stretches it to the tab width) so it + // adds no height and never reflows the strip. + let tab_element: Box = if FeatureFlag::NewTabStyling.is_enabled() && is_active + { + let accent_underline = + ConstrainedBox::new(Rect::new().with_background(theme.accent()).finish()) + .with_height(2.) + .finish(); + + let mut accented = Stack::new().with_child(tab.finish()); + accented.add_positioned_overlay_child( + accent_underline, + OffsetPositioning::offset_from_parent( + vec2f(0., 0.), + ParentOffsetBounds::ParentBySize, + ParentAnchor::BottomLeft, + ChildAnchor::BottomLeft, + ), + ); + accented.finish() + } else { + tab.finish() + }; + // If the tab is being dragged, add an opaque background behind it if is_tab_dragging { - Container::new(tab.finish()) + Container::new(tab_element) .with_background_color( self.ui_builder .warp_theme() @@ -1641,10 +1681,10 @@ impl<'a> TabComponent<'a> { // semantically meaningful for an element that follows the cursor, // and because the inner `DropTarget`'s position is unrelated to // any real tab in the target window. - tab.finish() + tab_element } else { DropTarget::new( - tab.finish(), + tab_element, TabBarDropTargetData { tab_bar_location: TabBarLocation::TabIndex(self.tab_index), }, @@ -1858,6 +1898,16 @@ impl UiComponent for TabComponent<'_> { .finish() }; + // Separate the floating cards with a small gap instead of abutting + // separators (new styling only). The first tab keeps a tight left edge. + let constrained_tab = if FeatureFlag::NewTabStyling.is_enabled() && tab_index > 0 { + Container::new(constrained_tab) + .with_margin_left(2.) + .finish() + } else { + constrained_tab + }; + // Skip the `Draggable` and `SavePosition` wrappers when rendering // the tab inside the cross-window drag chip overlay. Wrapping the // chip in another `Draggable` would interfere with the in-flight