From 0ff6740a9ed1c7d0f8fef398ed4d653de729e447 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Fri, 25 Sep 2026 16:49:09 +0700 Subject: [PATCH] feat: add an option to enable navigation buttons --- Cargo.lock | 1 + crates/dock/Cargo.toml | 1 + crates/dock/src/tab_panel.rs | 50 ++++++++++++------- crates/settings/src/settings.rs | 17 +++++++ crates/settings/src/store.rs | 6 +++ .../src/views/sidebar/settings_dialog.rs | 22 ++++++++ 6 files changed, 80 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f29569b..bfac56f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1791,6 +1791,7 @@ dependencies = [ "gpui-base", "gpui-component", "gpui-pre", + "settings", "signed_ui", ] diff --git a/crates/dock/Cargo.toml b/crates/dock/Cargo.toml index b6c512e..b1afc86 100644 --- a/crates/dock/Cargo.toml +++ b/crates/dock/Cargo.toml @@ -9,6 +9,7 @@ publish.workspace = true gpui.workspace = true gpui-component.workspace = true gpui-base.workspace = true +settings = { path = "../settings" } signed_ui = { path = "../signed_ui" } [dev-dependencies] diff --git a/crates/dock/src/tab_panel.rs b/crates/dock/src/tab_panel.rs index 85e8f27..e87cc8a 100644 --- a/crates/dock/src/tab_panel.rs +++ b/crates/dock/src/tab_panel.rs @@ -19,21 +19,23 @@ use gpui_component::animation::{Lerp as _, ease_out_cubic}; use gpui_component::button::{Button, ButtonVariants as _}; use gpui_component::dock::{ClosePanel, PanelControl, PanelHandle, ToggleZoom}; use gpui_component::{ActiveTheme as _, Disableable as _, IconName, Sizable as _, h_flex, v_flex}; +use settings::{SettingsStore, TabBarSettings}; use signed_ui::title_bar_drag_handlers; use crate::dock_area::SkinShared; use crate::{TAB_BAR_HEIGHT, t, window_controls}; -/// A rough size for the drag preview, reported to base so a drop placeholder -/// knows where to fly in from. -/// -/// The preview itself fits its content, so this is an estimate used only to -/// place the placeholder. const DRAG_PREVIEW_SIZE: gpui::Size = size(px(96.), px(30.)); const COLLAPSED_TAB_BAR_SELECTOR: &str = "signed-dock-collapsed-tab-bar"; const ACTIVE_TAB_SELECTOR: &str = "signed-dock-active-tab"; const ACTIVE_TAB_CLOSE_SELECTOR: &str = "signed-dock-active-tab-close"; const TAB_HOVER_GROUP: &str = "signed-dock-tab"; +const NAVIGATION_BUTTONS_TAB_THRESHOLD: usize = 10; + +/// Whether a group's prev/next buttons should be drawn. +fn navigation_buttons_visible(hide_navigation_buttons: bool, panels_len: usize) -> bool { + !hide_navigation_buttons && panels_len > NAVIGATION_BUTTONS_TAB_THRESHOLD +} /// A panel's title, or its registered name when the panel has no handle. pub(crate) fn panel_title( @@ -48,9 +50,6 @@ pub(crate) fn panel_title( } /// The preview that follows the cursor while a panel is dragged. -/// -/// Base's `DragPanel` is the payload and draws nothing, this is the appearance half. -/// It fits its content rather than a fixed width, so a long title is never cut off. struct DragPanelPreview { panel: Arc, } @@ -277,15 +276,22 @@ impl SignedTabGroupSkin { } /// The previous and next tab buttons in the tab bar's leading prefix. - /// Always rendered, disabled at the strip ends or when collapsed. - fn render_prev_next_tab_buttons( - &self, - group: &TabGroupContext, - _cx: &mut App, - ) -> impl IntoElement { + /// + /// Hidden by default, and only offered for crowded strips where the active + /// tab may have scrolled out of view. Disabled at the strip ends or collapsed. + fn render_prev_next_tab_buttons(&self, group: &TabGroupContext, cx: &mut App) -> AnyElement { let collapsed = group.is_collapsed(); let active_ix = group.active_ix(); let panels_len = group.panels().len(); + + let hidden = SettingsStore::try_global(cx) + .map(|store| store.read(cx).settings().tab_bar.hide_navigation_buttons) + .unwrap_or_else(|| TabBarSettings::default().hide_navigation_buttons); + + if !navigation_buttons_visible(hidden, panels_len) { + return Empty.into_any_element(); + } + let prev_enabled = !collapsed && active_ix > 0; let next_enabled = !collapsed && active_ix + 1 < panels_len; @@ -317,11 +323,10 @@ impl SignedTabGroupSkin { move |_, window, cx| group.select_tab(active_ix + 1, window, cx) }), ) + .into_any_element() } /// One tab of the pill strip. - /// While collapsed, tabs lose the active style and all interactions. - /// The strip is also how a closed bottom dock is opened again. #[allow(clippy::too_many_arguments)] fn render_tab( &self, @@ -848,7 +853,7 @@ mod tests { }; use gpui_base::dock::{DockArea, DockLayout, DockPlacement, PanelEvent}; - use super::COLLAPSED_TAB_BAR_SELECTOR; + use super::{COLLAPSED_TAB_BAR_SELECTOR, navigation_buttons_visible}; use crate::{BasePanel, Panel, SignedDockSkin, panel_handle}; struct Probe { @@ -947,4 +952,15 @@ mod tests { reserved_strip, ); } + + #[test] + fn navigation_buttons_follow_the_setting_and_tab_count() { + // Hidden by default, no matter how many tabs there are. + assert!(!navigation_buttons_visible(true, 1)); + assert!(!navigation_buttons_visible(true, 20)); + + // When not hidden they still only appear for a crowded strip. + assert!(!navigation_buttons_visible(false, 10)); + assert!(navigation_buttons_visible(false, 11)); + } } diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 323cd6e..a9a7264 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -54,6 +54,22 @@ impl Default for ThemeSettings { } } +/// Tab bar chrome. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(default)] +pub struct TabBarSettings { + /// Hide the previous and next tab buttons. + pub hide_navigation_buttons: bool, +} + +impl Default for TabBarSettings { + fn default() -> Self { + Self { + hide_navigation_buttons: true, + } + } +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(default)] pub struct GraspServersSettings { @@ -126,6 +142,7 @@ pub struct CreateRepositorySettings { pub struct Settings { pub appearance: AppearanceMode, pub theme: ThemeSettings, + pub tab_bar: TabBarSettings, pub grasp_servers: GraspServersSettings, pub local_repos: LocalReposSettings, pub checkouts: CheckoutsSettings, diff --git a/crates/settings/src/store.rs b/crates/settings/src/store.rs index bca510c..690fbc6 100644 --- a/crates/settings/src/store.rs +++ b/crates/settings/src/store.rs @@ -24,6 +24,12 @@ impl SettingsStore { cx.global::().0.clone() } + /// Retrieve the global settings store if one has been installed. + pub fn try_global(cx: &App) -> Option> { + cx.try_global::() + .map(|store| store.0.clone()) + } + pub fn set_global(entity: Entity, cx: &mut App) { cx.set_global(GlobalSettingsStore(entity)); } diff --git a/crates/workspace/src/views/sidebar/settings_dialog.rs b/crates/workspace/src/views/sidebar/settings_dialog.rs index 51e4e9c..52a129b 100644 --- a/crates/workspace/src/views/sidebar/settings_dialog.rs +++ b/crates/workspace/src/views/sidebar/settings_dialog.rs @@ -290,6 +290,8 @@ fn settings_view(controls: &SettingsControls, cx: &mut App) -> impl IntoElement .child(Separator::horizontal()) .child(theme_section(&settings, controls, cx)) .child(Separator::horizontal()) + .child(tab_bar_section(&settings, cx)) + .child(Separator::horizontal()) .child(grasp_servers_section(&settings, controls, cx)) .child(Separator::horizontal()) .child(repositories_section(&settings, controls, cx)) @@ -374,6 +376,26 @@ fn theme_section(settings: &Settings, controls: &SettingsControls, cx: &App) -> )) } +/// Tab bar chrome, such as its next/previous tab buttons. +fn tab_bar_section(settings: &Settings, cx: &App) -> impl IntoElement { + v_flex().w_full().gap_3().child(setting_row( + cx, + "Hide Tab Navigation Buttons", + "Enable to show navigation buttons when more than 10 tabs.", + Switch::new("hide-tab-navigation") + .checked(settings.tab_bar.hide_navigation_buttons) + .on_click(move |checked: &bool, _window, cx| { + let store = SettingsStore::global(cx); + store.update(cx, |store, cx| { + store.edit( + |settings| settings.tab_bar.hide_navigation_buttons = *checked, + cx, + ); + }); + }), + )) +} + /// Default grasp servers, used until the user's kind `10317` grasp list loads. fn grasp_servers_section( settings: &Settings,