From da22d22cf4e0d824e5730cc11014fc7a54e22123 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:02:26 +0800 Subject: [PATCH] fix(tab-strip): keep the strip inside the client-side frame (#1184) Under Linux client-side decorations the viewport includes the transparent shadow inset, but the title bar row sits inside the frame padding. Sizing the strip off the raw viewport overflowed the row by both paddings and clipped the window's close button. Subtract `window_paddings` before reserving the lead and window controls; the paddings are zero under server-side decorations and on maximized or tiled windows, so other platforms are unchanged. Fixes #1169 --- src/ui/tab_strip.rs | 58 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/src/ui/tab_strip.rs b/src/ui/tab_strip.rs index 2bd41af0..92e5c237 100644 --- a/src/ui/tab_strip.rs +++ b/src/ui/tab_strip.rs @@ -1,7 +1,7 @@ use gpui::{ - Animation, AnimationExt as _, AnyElement, App, Axis, Bounds, Context, FontWeight, MouseButton, - Pixels, SharedString, Window, canvas, deferred, div, ease_out_quint, linear_color_stop, - linear_gradient, prelude::*, px, + Animation, AnimationExt as _, AnyElement, App, Axis, Bounds, Context, Edges, FontWeight, + MouseButton, Pixels, SharedString, Window, canvas, deferred, div, ease_out_quint, + linear_color_stop, linear_gradient, prelude::*, px, }; use gpui_component::button::{Button, ButtonCustomVariant, ButtonVariants as _}; use gpui_component::input::Input; @@ -633,6 +633,21 @@ pub(crate) fn hit_target(button: Button) -> Button { /// The narrowest a chip gets: its `min_w`, which flex-shrink cannot go under. const CHIP_MIN_W: f32 = 100.; +/// How wide the tab strip is drawn: the content width less what the row +/// reserves beside it (the lead, plus the window buttons or a docked column), +/// never below `floor`. +/// +/// Under Linux client-side decorations `viewport_w` measures the whole surface, +/// shadow included, while the title bar row sits inside the frame padding — +/// so both paddings come off before anything else does. Sizing the strip to +/// the raw viewport overran the frame by exactly that much and clipped the +/// close button (#1169). `window_paddings` answers `Edges::all(0)` under +/// server-side decorations and on a maximized or tiled CSD window, so +/// everywhere else this is the width the strip always had. +fn strip_width(viewport_w: Pixels, pad: Edges, reserved: f32, floor: f32) -> Pixels { + (viewport_w - pad.left - pad.right - px(reserved)).max(px(floor)) +} + /// The run of chips to draw when they cannot all fit. /// /// The row clips what overflows, so past a certain tab count the chips at the @@ -2318,13 +2333,21 @@ impl Tty7App { // under it showed through the file name while staying clickable. let document_w = self.document_dock_px(window, cx).unwrap_or(0.); let controls_w = crate::ui::app::window_controls_w(window.is_fullscreen()); + let pad = gpui_component::window_paddings(window); let strip_w = if cfg!(target_os = "macos") { - (window.viewport_size().width - - px(crate::ui::app::TITLE_BAR_LEAD + panel_w + document_w)) - .max(px(160.)) + strip_width( + window.viewport_size().width, + pad, + crate::ui::app::TITLE_BAR_LEAD + panel_w + document_w, + 160., + ) } else { - (window.viewport_size().width - px(crate::ui::app::TITLE_BAR_LEAD + controls_w)) - .max(px(140.)) + strip_width( + window.viewport_size().width, + pad, + crate::ui::app::TITLE_BAR_LEAD + controls_w, + 140., + ) }; let chrome_band_w = (!cfg!(target_os = "macos") && self.right_panel_open(cx)) .then(|| (self.right_panel_px(window, cx) - controls_w - 1.).max(0.)); @@ -3120,6 +3143,25 @@ mod tests { use std::path::Path; use unicode_segmentation::UnicodeSegmentation; + /// #1169: under Linux client-side decorations the viewport measures the + /// whole surface, shadow included, while the title bar row sits inside the + /// frame padding. A strip sized off the raw viewport overran the frame by + /// both paddings and pushed the close button out past the right edge. + #[test] + fn the_strip_and_window_buttons_fit_inside_the_csd_frame() { + let (lead, controls) = (12., 102.); + let pad = Edges::all(px(12.)); + let strip = strip_width(px(1200.), pad, lead + controls, 140.); + assert!( + px(lead) + strip + px(controls) <= px(1200. - 12. - 12.), + "strip {strip:?} leaves the window buttons outside the frame" + ); + // No frame padding (server-side decorations, macOS, Windows, or a + // maximized/tiled CSD window): the strip keeps the width it always had. + let bare = strip_width(px(1200.), Edges::all(px(0.)), lead + controls, 140.); + assert_eq!(bare, px(1200. - lead - controls)); + } + /// Where a band lands in the terminal column, for a strip that starts /// `lead` in and ends `strip_end` from the column's left edge. fn band_in_column(band: SearchBand, lead: f32, strip_end: f32) -> (f32, f32) {