diff --git a/Cargo.lock b/Cargo.lock index d7154857a3..91c14858c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5680,7 +5680,6 @@ dependencies = [ "serde_derive", "serde_json", "settings", - "smallvec", "strum", "telemetry", "theme", diff --git a/crates/debugger_ui/src/session/inert.rs b/crates/debugger_ui/src/session/inert.rs index 829f4c5060..8447ee84e2 100644 --- a/crates/debugger_ui/src/session/inert.rs +++ b/crates/debugger_ui/src/session/inert.rs @@ -7,20 +7,39 @@ use settings::Settings as _; use task::TCPHost; use theme::ThemeSettings; use ui::{ - h_flex, relative, v_flex, ActiveTheme as _, Button, ButtonCommon, ButtonStyle, Clickable, - Context, ContextMenu, Disableable, DropdownMenu, InteractiveElement, IntoElement, - ParentElement, Render, SharedString, Styled, Window, + h_flex, relative, v_flex, ActiveTheme as _, ButtonLike, Clickable, Context, ContextMenu, + Disableable, Disclosure, DropdownMenu, FluentBuilder, InteractiveElement, IntoElement, Label, + LabelCommon, LabelSize, ParentElement, PopoverMenu, PopoverMenuHandle, Render, SharedString, + SplitButton, Styled, Window, }; use workspace::Workspace; use crate::attach_modal::AttachModal; +#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)] +enum SpawnMode { + #[default] + Launch, + Attach, +} + +impl SpawnMode { + fn label(&self) -> &'static str { + match self { + SpawnMode::Launch => "Launch", + SpawnMode::Attach => "Attach", + } + } +} + pub(crate) struct InertState { focus_handle: FocusHandle, selected_debugger: Option, program_editor: Entity, cwd_editor: Entity, workspace: WeakEntity, + spawn_mode: SpawnMode, + popover_handle: PopoverMenuHandle, } impl InertState { @@ -47,6 +66,8 @@ impl InertState { program_editor, selected_debugger: None, focus_handle: cx.focus_handle(), + spawn_mode: SpawnMode::default(), + popover_handle: Default::default(), } } } @@ -72,19 +93,46 @@ impl Render for InertState { ) -> impl ui::IntoElement { let weak = cx.weak_entity(); let disable_buttons = self.selected_debugger.is_none(); + let spawn_button = ButtonLike::new_rounded_left("spawn-debug-session") + .child(Label::new(self.spawn_mode.label()).size(LabelSize::Small)) + .on_click(cx.listener(|this, _, window, cx| { + if this.spawn_mode == SpawnMode::Launch { + let program = this.program_editor.read(cx).text(cx); + let cwd = PathBuf::from(this.cwd_editor.read(cx).text(cx)); + let kind = + kind_for_label(this.selected_debugger.as_deref().unwrap_or_else(|| { + unimplemented!( + "Automatic selection of a debugger based on users project" + ) + })); + cx.emit(InertEvent::Spawned { + config: DebugAdapterConfig { + label: "hard coded".into(), + kind, + request: DebugRequestType::Launch, + program: Some(program), + cwd: Some(cwd), + initialize_args: None, + supports_attach: false, + }, + }); + } else { + this.attach(window, cx) + } + })) + .disabled(disable_buttons); v_flex() .track_focus(&self.focus_handle) .size_full() .gap_1() .p_2() - .child( - v_flex().gap_1() + v_flex() + .gap_1() .child( h_flex() .w_full() .gap_2() - .child(Self::render_editor(&self.program_editor, cx)) .child( h_flex().child(DropdownMenu::new( @@ -109,45 +157,63 @@ impl Render for InertState { .entry("Delve", None, setter_for_name("Delve")) .entry("LLDB", None, setter_for_name("LLDB")) .entry("PHP", None, setter_for_name("PHP")) - .entry("JavaScript", None, setter_for_name("JavaScript")) + .entry( + "JavaScript", + None, + setter_for_name("JavaScript"), + ) .entry("Debugpy", None, setter_for_name("Debugpy")) }), )), - ) + ), ) .child( - h_flex().gap_2().child( - Self::render_editor(&self.cwd_editor, cx), - ).child(h_flex() - .gap_4() - .pl_2() - .child( - Button::new("launch-dap", "Launch") - .style(ButtonStyle::Filled) - .disabled(disable_buttons) - .on_click(cx.listener(|this, _, _, cx| { - let program = this.program_editor.read(cx).text(cx); - let cwd = PathBuf::from(this.cwd_editor.read(cx).text(cx)); - let kind = kind_for_label(this.selected_debugger.as_deref().unwrap_or_else(|| unimplemented!("Automatic selection of a debugger based on users project"))); - cx.emit(InertEvent::Spawned { - config: DebugAdapterConfig { - label: "hard coded".into(), - kind, - request: DebugRequestType::Launch, - program: Some(program), - cwd: Some(cwd), - initialize_args: None, - supports_attach: false, - }, - }); - })), - ) - .child(Button::new("attach-dap", "Attach") - .style(ButtonStyle::Filled) - .disabled(disable_buttons) - .on_click(cx.listener(|this, _, window, cx| this.attach(window, cx))) - )) - ) + h_flex() + .gap_2() + .child(Self::render_editor(&self.cwd_editor, cx)) + .map(|this| { + let entity = cx.weak_entity(); + this.child(SplitButton { + left: spawn_button, + right: PopoverMenu::new("debugger-select-spawn-mode") + .trigger(Disclosure::new( + "debugger-spawn-button-disclosure", + self.popover_handle.is_deployed(), + )) + .menu(move |window, cx| { + Some(ContextMenu::build(window, cx, { + let entity = entity.clone(); + move |this, _, _| { + this.entry("Launch", None, { + let entity = entity.clone(); + move |_, cx| { + let _ = + entity.update(cx, |this, cx| { + this.spawn_mode = + SpawnMode::Launch; + cx.notify(); + }); + } + }) + .entry("Attach", None, { + let entity = entity.clone(); + move |_, cx| { + let _ = + entity.update(cx, |this, cx| { + this.spawn_mode = + SpawnMode::Attach; + cx.notify(); + }); + } + }) + } + })) + }) + .with_handle(self.popover_handle.clone()) + .into_any_element(), + }) + }), + ), ) } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a31733823d..65e0e849bd 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5918,11 +5918,7 @@ impl Editor { breakpoint: Option<&(Anchor, Breakpoint)>, cx: &mut Context, ) -> Option { - let color = if breakpoint.is_some() { - Color::Debugger - } else { - Color::Muted - }; + let color = Color::Muted; let position = breakpoint.as_ref().map(|(anchor, _)| *anchor); let bp_kind = Arc::new( diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index e0be28e20b..c6e0746178 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -49,7 +49,6 @@ serde.workspace = true serde_derive.workspace = true serde_json.workspace = true settings.workspace = true -smallvec.workspace = true strum.workspace = true telemetry.workspace = true theme.workspace = true diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 07256d030e..39569218d0 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -163,19 +163,18 @@ fn render_remote_button( } mod remote_button { - use gpui::{hsla, point, Action, AnyView, BoxShadow, ClickEvent, Corner, FocusHandle}; + use gpui::{Action, AnyView, ClickEvent, Corner, FocusHandle}; use ui::{ - div, h_flex, px, rems, ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, Clickable, - ContextMenu, ElementId, ElevationIndex, FluentBuilder, Icon, IconName, IconSize, - IntoElement, Label, LabelCommon, LabelSize, LineHeightStyle, ParentElement, PopoverMenu, - RenderOnce, SharedString, Styled, Tooltip, Window, + div, h_flex, rems, App, ButtonCommon, Clickable, ContextMenu, ElementId, FluentBuilder, + Icon, IconName, IconSize, IntoElement, Label, LabelCommon, LabelSize, LineHeightStyle, + ParentElement, PopoverMenu, SharedString, SplitButton, Styled, Tooltip, Window, }; pub fn render_fetch_button( keybinding_target: Option, id: SharedString, ) -> SplitButton { - SplitButton::new( + split_button( id, "Fetch", 0, @@ -203,7 +202,7 @@ mod remote_button { id: SharedString, ahead: u32, ) -> SplitButton { - SplitButton::new( + split_button( id, "Push", ahead as usize, @@ -232,7 +231,7 @@ mod remote_button { ahead: u32, behind: u32, ) -> SplitButton { - SplitButton::new( + split_button( id, "Pull", ahead as usize, @@ -259,7 +258,7 @@ mod remote_button { keybinding_target: Option, id: SharedString, ) -> SplitButton { - SplitButton::new( + split_button( id, "Publish", 0, @@ -286,7 +285,7 @@ mod remote_button { keybinding_target: Option, id: SharedString, ) -> SplitButton { - SplitButton::new( + split_button( id, "Republish", 0, @@ -364,111 +363,76 @@ mod remote_button { }) .anchor(Corner::TopRight) } - - #[derive(IntoElement)] - pub struct SplitButton { - pub left: ButtonLike, - pub right: AnyElement, - } - - impl SplitButton { - #[allow(clippy::too_many_arguments)] - fn new( - id: impl Into, - left_label: impl Into, - ahead_count: usize, - behind_count: usize, - left_icon: Option, - keybinding_target: Option, - left_on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static, - ) -> Self { - let id = id.into(); - - fn count(count: usize) -> impl IntoElement { - h_flex() - .ml_neg_px() - .h(rems(0.875)) - .items_center() - .overflow_hidden() - .px_0p5() - .child( - Label::new(count.to_string()) - .size(LabelSize::XSmall) - .line_height_style(LineHeightStyle::UiLabel), - ) - } - - let should_render_counts = left_icon.is_none() && (ahead_count > 0 || behind_count > 0); - - let left = ui::ButtonLike::new_rounded_left(ElementId::Name( - format!("split-button-left-{}", id).into(), - )) - .layer(ui::ElevationIndex::ModalSurface) - .size(ui::ButtonSize::Compact) - .when(should_render_counts, |this| { - this.child( - h_flex() - .ml_neg_0p5() - .mr_1() - .when(behind_count > 0, |this| { - this.child(Icon::new(IconName::ArrowDown).size(IconSize::XSmall)) - .child(count(behind_count)) - }) - .when(ahead_count > 0, |this| { - this.child(Icon::new(IconName::ArrowUp).size(IconSize::XSmall)) - .child(count(ahead_count)) - }), - ) - }) - .when_some(left_icon, |this, left_icon| { - this.child( - h_flex() - .ml_neg_0p5() - .mr_1() - .child(Icon::new(left_icon).size(IconSize::XSmall)), - ) - }) - .child( - div() - .child(Label::new(left_label).size(LabelSize::Small)) - .mr_0p5(), - ) - .on_click(left_on_click) - .tooltip(tooltip); - - let right = render_git_action_menu( - ElementId::Name(format!("split-button-right-{}", id).into()), - keybinding_target, - ) - .into_any_element(); - - Self { left, right } - } - } - - impl RenderOnce for SplitButton { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + #[allow(clippy::too_many_arguments)] + fn split_button( + id: SharedString, + left_label: impl Into, + ahead_count: usize, + behind_count: usize, + left_icon: Option, + keybinding_target: Option, + left_on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static, + ) -> SplitButton { + fn count(count: usize) -> impl IntoElement { h_flex() - .rounded_sm() - .border_1() - .border_color(cx.theme().colors().text_muted.alpha(0.12)) - .child(div().flex_grow().child(self.left)) + .ml_neg_px() + .h(rems(0.875)) + .items_center() + .overflow_hidden() + .px_0p5() .child( - div() - .h_full() - .w_px() - .bg(cx.theme().colors().text_muted.alpha(0.16)), + Label::new(count.to_string()) + .size(LabelSize::XSmall) + .line_height_style(LineHeightStyle::UiLabel), ) - .child(self.right) - .bg(ElevationIndex::Surface.on_elevation_bg(cx)) - .shadow(smallvec::smallvec![BoxShadow { - color: hsla(0.0, 0.0, 0.0, 0.16), - offset: point(px(0.), px(1.)), - blur_radius: px(0.), - spread_radius: px(0.), - }]) } + + let should_render_counts = left_icon.is_none() && (ahead_count > 0 || behind_count > 0); + + let left = ui::ButtonLike::new_rounded_left(ElementId::Name( + format!("split-button-left-{}", id).into(), + )) + .layer(ui::ElevationIndex::ModalSurface) + .size(ui::ButtonSize::Compact) + .when(should_render_counts, |this| { + this.child( + h_flex() + .ml_neg_0p5() + .mr_1() + .when(behind_count > 0, |this| { + this.child(Icon::new(IconName::ArrowDown).size(IconSize::XSmall)) + .child(count(behind_count)) + }) + .when(ahead_count > 0, |this| { + this.child(Icon::new(IconName::ArrowUp).size(IconSize::XSmall)) + .child(count(ahead_count)) + }), + ) + }) + .when_some(left_icon, |this, left_icon| { + this.child( + h_flex() + .ml_neg_0p5() + .mr_1() + .child(Icon::new(left_icon).size(IconSize::XSmall)), + ) + }) + .child( + div() + .child(Label::new(left_label).size(LabelSize::Small)) + .mr_0p5(), + ) + .on_click(left_on_click) + .tooltip(tooltip); + + let right = render_git_action_menu( + ElementId::Name(format!("split-button-right-{}", id).into()), + keybinding_target, + ) + .into_any_element(); + + SplitButton { left, right } } } diff --git a/crates/ui/src/components/button.rs b/crates/ui/src/components/button.rs index 68e72253a5..23e7702f62 100644 --- a/crates/ui/src/components/button.rs +++ b/crates/ui/src/components/button.rs @@ -2,9 +2,11 @@ mod button; mod button_icon; mod button_like; mod icon_button; +mod split_button; mod toggle_button; pub use button::*; pub use button_like::*; pub use icon_button::*; +pub use split_button::*; pub use toggle_button::*; diff --git a/crates/ui/src/components/button/split_button.rs b/crates/ui/src/components/button/split_button.rs new file mode 100644 index 0000000000..84b381768d --- /dev/null +++ b/crates/ui/src/components/button/split_button.rs @@ -0,0 +1,45 @@ +use gpui::{ + div, hsla, point, px, AnyElement, App, BoxShadow, IntoElement, ParentElement, RenderOnce, + Styled, Window, +}; +use theme::ActiveTheme; + +use crate::{h_flex, ElevationIndex}; + +use super::ButtonLike; + +/// /// A button with two parts: a primary action on the left and a secondary action on the right. +/// +/// The left side is a [`ButtonLike`] with the main action, while the right side can contain +/// any element (typically a dropdown trigger or similar). +/// +/// The two sections are visually separated by a divider, but presented as a unified control. +#[derive(IntoElement)] +pub struct SplitButton { + pub left: ButtonLike, + pub right: AnyElement, +} + +impl RenderOnce for SplitButton { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + h_flex() + .rounded_sm() + .border_1() + .border_color(cx.theme().colors().text_muted.alpha(0.12)) + .child(div().flex_grow().child(self.left)) + .child( + div() + .h_full() + .w_px() + .bg(cx.theme().colors().text_muted.alpha(0.16)), + ) + .child(self.right) + .bg(ElevationIndex::Surface.on_elevation_bg(cx)) + .shadow(smallvec::smallvec![BoxShadow { + color: hsla(0.0, 0.0, 0.0, 0.16), + offset: point(px(0.), px(1.)), + blur_radius: px(0.), + spread_radius: px(0.), + }]) + } +}