From e09eeb7446a71ba56c751fc7cf92859a2e4a5cd0 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 11 Apr 2025 15:33:36 +0200 Subject: [PATCH] debugger: Style debugger tabs (#28572) ![image](https://github.com/user-attachments/assets/a88b1897-cb96-4c6c-b602-396a91ef4de8) Release Notes: - N/A --- crates/debugger_ui/src/session/running.rs | 85 +++++++- .../src/session/running/console.rs | 1 - crates/workspace/src/item.rs | 2 +- crates/workspace/src/pane.rs | 182 +++++++++--------- 4 files changed, 177 insertions(+), 93 deletions(-) diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 57bd8f605d..b836a05db9 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -26,13 +26,15 @@ use rpc::proto::ViewId; use settings::Settings; use stack_frame_list::StackFrameList; use ui::{ - AnyElement, App, Context, ContextMenu, DropdownMenu, InteractiveElement, IntoElement, Label, - LabelCommon as _, ParentElement, Render, SharedString, Styled, Window, div, h_flex, v_flex, + ActiveTheme, AnyElement, App, Context, ContextMenu, DropdownMenu, FluentBuilder, + InteractiveElement, IntoElement, Label, LabelCommon as _, ParentElement, Render, SharedString, + StatefulInteractiveElement, Styled, Tab, Window, div, h_flex, v_flex, }; use util::ResultExt; use variable_list::VariableList; use workspace::{ - ActivePaneDecorator, DraggedTab, Item, Pane, PaneGroup, Workspace, move_item, pane::Event, + ActivePaneDecorator, DraggedTab, Item, Pane, PaneGroup, Workspace, item::TabContentParams, + move_item, pane::Event, }; pub struct RunningState { @@ -121,8 +123,9 @@ impl Item for SubView { cx: &App, ) -> AnyElement { let label = Label::new(self.tab_name.clone()) + .size(ui::LabelSize::Small) .color(params.text_color()) - .into_any_element(); + .line_height_style(ui::LineHeightStyle::UiLabel); if !params.selected && self.show_indicator.as_ref()(cx) { return h_flex() @@ -133,7 +136,7 @@ impl Item for SubView { .into_any_element(); } - label + label.into_any_element() } } @@ -266,7 +269,79 @@ fn new_debugger_pane( }))); pane.display_nav_history_buttons(None); pane.set_custom_drop_handle(cx, custom_drop_handle); + pane.set_should_display_tab_bar(|_, _| true); pane.set_render_tab_bar_buttons(cx, |_, _, _| (None, None)); + pane.set_render_tab_bar(cx, |pane, window, cx| { + let active_pane_item = pane.active_item(); + h_flex() + .w_full() + .h(Tab::container_height(cx)) + .drag_over::(|bar, _, _, cx| { + bar.bg(cx.theme().colors().drop_target_background) + }) + .on_drop( + cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| { + this.drag_split_direction = None; + this.handle_tab_drop(dragged_tab, this.items_len(), window, cx) + }), + ) + .bg(cx.theme().colors().tab_bar_background) + .border_b_1() + .border_color(cx.theme().colors().border) + .children(pane.items().enumerate().map(|(ix, item)| { + let selected = active_pane_item + .as_ref() + .map_or(false, |active| active.item_id() == item.item_id()); + let item_ = item.boxed_clone(); + div() + .id(SharedString::from(format!( + "debugger_tab_{}", + item.item_id().as_u64() + ))) + .p_1() + .rounded_md() + .cursor_pointer() + .map(|this| { + if selected { + this.bg(cx.theme().colors().tab_active_background) + } else { + let hover_color = cx.theme().colors().element_hover; + this.hover(|style| style.bg(hover_color)) + } + }) + .on_click(cx.listener(move |this, _, window, cx| { + let index = this.index_for_item(&*item_); + if let Some(index) = index { + this.activate_item(index, true, true, window, cx); + } + })) + .child(item.tab_content( + TabContentParams { + selected, + ..Default::default() + }, + window, + cx, + )) + .on_drop( + cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| { + this.drag_split_direction = None; + this.handle_tab_drop(dragged_tab, ix, window, cx) + }), + ) + .on_drag( + DraggedTab { + item: item.boxed_clone(), + pane: cx.entity().clone(), + detail: 0, + is_active: selected, + ix, + }, + |tab, _, _, cx| cx.new(|_| tab.clone()), + ) + })) + .into_any_element() + }); pane }); diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index d799708827..ad765496c2 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -235,7 +235,6 @@ impl Render for Console { .when(self.is_local(cx), |this| { this.child(Divider::horizontal()) .child(self.render_query_bar(cx)) - .pt(DynamicSpacing::Base04.rems(cx)) }) .border_2() } diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 0cf241ca8c..594d7eeca3 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -168,7 +168,7 @@ pub struct BreadcrumbText { pub font: Option, } -#[derive(Debug, Clone, Copy)] +#[derive(Clone, Copy, Default, Debug)] pub struct TabContentParams { pub detail: Option, pub selected: bool, diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 71c03d8998..a3090e5383 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -294,7 +294,7 @@ pub struct Pane { toolbar: Entity, pub(crate) workspace: WeakEntity, project: WeakEntity, - drag_split_direction: Option, + pub drag_split_direction: Option, can_drop_predicate: Option bool>>, custom_drop_handle: Option< Arc) -> ControlFlow<(), ()>>, @@ -309,6 +309,7 @@ pub struct Pane { &mut Context, ) -> (Option, Option), >, + render_tab_bar: Rc) -> AnyElement>, show_tab_bar_buttons: bool, _subscriptions: Vec, tab_bar_scroll_handle: ScrollHandle, @@ -435,88 +436,8 @@ impl Pane { custom_drop_handle: None, can_split_predicate: None, should_display_tab_bar: Rc::new(|_, cx| TabBarSettings::get_global(cx).show), - render_tab_bar_buttons: Rc::new(move |pane, window, cx| { - if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) { - return (None, None); - } - // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s - // `end_slot`, but due to needing a view here that isn't possible. - let right_children = h_flex() - // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here. - .gap(DynamicSpacing::Base04.rems(cx)) - .child( - PopoverMenu::new("pane-tab-bar-popover-menu") - .trigger_with_tooltip( - IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small), - Tooltip::text("New..."), - ) - .anchor(Corner::TopRight) - .with_handle(pane.new_item_context_menu_handle.clone()) - .menu(move |window, cx| { - Some(ContextMenu::build(window, cx, |menu, _, _| { - menu.action("New File", NewFile.boxed_clone()) - .action( - "Open File", - ToggleFileFinder::default().boxed_clone(), - ) - .separator() - .action( - "Search Project", - DeploySearch { - replace_enabled: false, - } - .boxed_clone(), - ) - .action( - "Search Symbols", - ToggleProjectSymbols.boxed_clone(), - ) - .separator() - .action("New Terminal", NewTerminal.boxed_clone()) - })) - }), - ) - .child( - PopoverMenu::new("pane-tab-bar-split") - .trigger_with_tooltip( - IconButton::new("split", IconName::Split) - .icon_size(IconSize::Small), - Tooltip::text("Split Pane"), - ) - .anchor(Corner::TopRight) - .with_handle(pane.split_item_context_menu_handle.clone()) - .menu(move |window, cx| { - ContextMenu::build(window, cx, |menu, _, _| { - menu.action("Split Right", SplitRight.boxed_clone()) - .action("Split Left", SplitLeft.boxed_clone()) - .action("Split Up", SplitUp.boxed_clone()) - .action("Split Down", SplitDown.boxed_clone()) - }) - .into() - }), - ) - .child({ - let zoomed = pane.is_zoomed(); - IconButton::new("toggle_zoom", IconName::Maximize) - .icon_size(IconSize::Small) - .toggle_state(zoomed) - .selected_icon(IconName::Minimize) - .on_click(cx.listener(|pane, _, window, cx| { - pane.toggle_zoom(&crate::ToggleZoom, window, cx); - })) - .tooltip(move |window, cx| { - Tooltip::for_action( - if zoomed { "Zoom Out" } else { "Zoom In" }, - &ToggleZoom, - window, - cx, - ) - }) - }) - .into_any_element() - .into(); - (None, right_children) - }), + render_tab_bar_buttons: Rc::new(default_render_tab_bar_buttons), + render_tab_bar: Rc::new(Self::render_tab_bar), show_tab_bar_buttons: TabBarSettings::get_global(cx).show_tab_bar_buttons, display_nav_history_buttons: Some( TabBarSettings::get_global(cx).show_nav_history_buttons, @@ -725,6 +646,14 @@ impl Pane { cx.notify(); } + pub fn set_render_tab_bar(&mut self, cx: &mut Context, render: F) + where + F: 'static + Fn(&mut Pane, &mut Window, &mut Context) -> AnyElement, + { + self.render_tab_bar = Rc::new(render); + cx.notify(); + } + pub fn set_render_tab_bar_buttons(&mut self, cx: &mut Context, render: F) where F: 'static @@ -2668,7 +2597,7 @@ impl Pane { }) } - fn render_tab_bar(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render_tab_bar(&mut self, window: &mut Window, cx: &mut Context) -> AnyElement { let focus_handle = self.focus_handle.clone(); let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft) .icon_size(IconSize::Small) @@ -2791,6 +2720,7 @@ impl Pane { })), ), ) + .into_any_element() } pub fn render_menu_overlay(menu: &Entity) -> Div { @@ -2864,7 +2794,7 @@ impl Pane { } } - fn handle_tab_drop( + pub fn handle_tab_drop( &mut self, dragged_tab: &DraggedTab, ix: usize, @@ -3137,6 +3067,86 @@ impl Pane { } } +fn default_render_tab_bar_buttons( + pane: &mut Pane, + window: &mut Window, + cx: &mut Context, +) -> (Option, Option) { + if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) { + return (None, None); + } + // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s + // `end_slot`, but due to needing a view here that isn't possible. + let right_children = h_flex() + // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here. + .gap(DynamicSpacing::Base04.rems(cx)) + .child( + PopoverMenu::new("pane-tab-bar-popover-menu") + .trigger_with_tooltip( + IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small), + Tooltip::text("New..."), + ) + .anchor(Corner::TopRight) + .with_handle(pane.new_item_context_menu_handle.clone()) + .menu(move |window, cx| { + Some(ContextMenu::build(window, cx, |menu, _, _| { + menu.action("New File", NewFile.boxed_clone()) + .action("Open File", ToggleFileFinder::default().boxed_clone()) + .separator() + .action( + "Search Project", + DeploySearch { + replace_enabled: false, + } + .boxed_clone(), + ) + .action("Search Symbols", ToggleProjectSymbols.boxed_clone()) + .separator() + .action("New Terminal", NewTerminal.boxed_clone()) + })) + }), + ) + .child( + PopoverMenu::new("pane-tab-bar-split") + .trigger_with_tooltip( + IconButton::new("split", IconName::Split).icon_size(IconSize::Small), + Tooltip::text("Split Pane"), + ) + .anchor(Corner::TopRight) + .with_handle(pane.split_item_context_menu_handle.clone()) + .menu(move |window, cx| { + ContextMenu::build(window, cx, |menu, _, _| { + menu.action("Split Right", SplitRight.boxed_clone()) + .action("Split Left", SplitLeft.boxed_clone()) + .action("Split Up", SplitUp.boxed_clone()) + .action("Split Down", SplitDown.boxed_clone()) + }) + .into() + }), + ) + .child({ + let zoomed = pane.is_zoomed(); + IconButton::new("toggle_zoom", IconName::Maximize) + .icon_size(IconSize::Small) + .toggle_state(zoomed) + .selected_icon(IconName::Minimize) + .on_click(cx.listener(|pane, _, window, cx| { + pane.toggle_zoom(&crate::ToggleZoom, window, cx); + })) + .tooltip(move |window, cx| { + Tooltip::for_action( + if zoomed { "Zoom Out" } else { "Zoom In" }, + &ToggleZoom, + window, + cx, + ) + }) + }) + .into_any_element() + .into(); + (None, right_children) +} + impl Focusable for Pane { fn focus_handle(&self, _cx: &App) -> FocusHandle { self.focus_handle.clone() @@ -3301,7 +3311,7 @@ impl Render for Pane { }), ) .when(self.active_item().is_some() && display_tab_bar, |pane| { - pane.child(self.render_tab_bar(window, cx)) + pane.child((self.render_tab_bar.clone())(self, window, cx)) }) .child({ let has_worktrees = project.read(cx).visible_worktrees(cx).next().is_some();