debugger: Tidy up dropdown menus (#30679)

Before
![CleanShot 2025-05-14 at 13 22
44@2x](https://github.com/user-attachments/assets/c6c06c5c-571d-4913-a691-161f44bba27c)

After
![CleanShot 2025-05-14 at 13 22
17@2x](https://github.com/user-attachments/assets/0a25a053-81a3-4b96-8963-4b770b1e5b45)

Release Notes:

- N/A
This commit is contained in:
Nate Butler 2025-05-14 13:32:51 +02:00 committed by GitHub
parent 4280bff10a
commit dce6e96c16
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 329 additions and 169 deletions

View file

@ -1,5 +1,6 @@
use crate::persistence::DebuggerPaneItem; use crate::persistence::DebuggerPaneItem;
use crate::session::DebugSession; use crate::session::DebugSession;
use crate::session::running::RunningState;
use crate::{ use crate::{
ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames, ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames,
FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart, FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart,
@ -30,7 +31,7 @@ use settings::Settings;
use std::any::TypeId; use std::any::TypeId;
use std::sync::Arc; use std::sync::Arc;
use task::{DebugScenario, TaskContext}; use task::{DebugScenario, TaskContext};
use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*}; use ui::{ContextMenu, Divider, Tooltip, prelude::*};
use workspace::SplitDirection; use workspace::SplitDirection;
use workspace::{ use workspace::{
Pane, Workspace, Pane, Workspace,
@ -87,7 +88,20 @@ impl DebugPanel {
}) })
} }
fn filter_action_types(&self, cx: &mut App) { pub(crate) fn sessions(&self) -> Vec<Entity<DebugSession>> {
self.sessions.clone()
}
pub fn active_session(&self) -> Option<Entity<DebugSession>> {
self.active_session.clone()
}
pub(crate) fn running_state(&self, cx: &mut App) -> Option<Entity<RunningState>> {
self.active_session()
.map(|session| session.read(cx).running_state().clone())
}
pub(crate) fn filter_action_types(&self, cx: &mut App) {
let (has_active_session, supports_restart, support_step_back, status) = self let (has_active_session, supports_restart, support_step_back, status) = self
.active_session() .active_session()
.map(|item| { .map(|item| {
@ -273,7 +287,7 @@ impl DebugPanel {
.detach_and_log_err(cx); .detach_and_log_err(cx);
} }
async fn register_session( pub(crate) async fn register_session(
this: WeakEntity<Self>, this: WeakEntity<Self>,
session: Entity<Session>, session: Entity<Session>,
cx: &mut AsyncWindowContext, cx: &mut AsyncWindowContext,
@ -342,7 +356,7 @@ impl DebugPanel {
Ok(debug_session) Ok(debug_session)
} }
fn handle_restart_request( pub(crate) fn handle_restart_request(
&mut self, &mut self,
mut curr_session: Entity<Session>, mut curr_session: Entity<Session>,
window: &mut Window, window: &mut Window,
@ -416,11 +430,12 @@ impl DebugPanel {
.detach_and_log_err(cx); .detach_and_log_err(cx);
} }
pub fn active_session(&self) -> Option<Entity<DebugSession>> { pub(crate) fn close_session(
self.active_session.clone() &mut self,
} entity_id: EntityId,
window: &mut Window,
fn close_session(&mut self, entity_id: EntityId, window: &mut Window, cx: &mut Context<Self>) { cx: &mut Context<Self>,
) {
let Some(session) = self let Some(session) = self
.sessions .sessions
.iter() .iter()
@ -474,93 +489,8 @@ impl DebugPanel {
}) })
.detach(); .detach();
} }
fn sessions_drop_down_menu(
&self,
active_session: &Entity<DebugSession>,
window: &mut Window,
cx: &mut Context<Self>,
) -> DropdownMenu {
let sessions = self.sessions.clone();
let weak = cx.weak_entity();
let label = active_session.read(cx).label_element(cx);
DropdownMenu::new_with_element( pub(crate) fn deploy_context_menu(
"debugger-session-list",
label,
ContextMenu::build(window, cx, move |mut this, _, cx| {
let context_menu = cx.weak_entity();
for session in sessions.into_iter() {
let weak_session = session.downgrade();
let weak_session_id = weak_session.entity_id();
this = this.custom_entry(
{
let weak = weak.clone();
let context_menu = context_menu.clone();
move |_, cx| {
weak_session
.read_with(cx, |session, cx| {
let context_menu = context_menu.clone();
let id: SharedString =
format!("debug-session-{}", session.session_id(cx).0)
.into();
h_flex()
.w_full()
.group(id.clone())
.justify_between()
.child(session.label_element(cx))
.child(
IconButton::new(
"close-debug-session",
IconName::Close,
)
.visible_on_hover(id.clone())
.icon_size(IconSize::Small)
.on_click({
let weak = weak.clone();
move |_, window, cx| {
weak.update(cx, |panel, cx| {
panel.close_session(
weak_session_id,
window,
cx,
);
})
.ok();
context_menu
.update(cx, |this, cx| {
this.cancel(
&Default::default(),
window,
cx,
);
})
.ok();
}
}),
)
.into_any_element()
})
.unwrap_or_else(|_| div().into_any_element())
}
},
{
let weak = weak.clone();
move |window, cx| {
weak.update(cx, |panel, cx| {
panel.activate_session(session.clone(), window, cx);
})
.ok();
}
},
);
}
this
}),
)
}
fn deploy_context_menu(
&mut self, &mut self,
position: Point<Pixels>, position: Point<Pixels>,
window: &mut Window, window: &mut Window,
@ -611,7 +541,11 @@ impl DebugPanel {
} }
} }
fn top_controls_strip(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> { pub(crate) fn top_controls_strip(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Div> {
let active_session = self.active_session.clone(); let active_session = self.active_session.clone();
let focus_handle = self.focus_handle.clone(); let focus_handle = self.focus_handle.clone();
let is_side = self.position(window, cx).axis() == gpui::Axis::Horizontal; let is_side = self.position(window, cx).axis() == gpui::Axis::Horizontal;
@ -651,12 +585,12 @@ impl DebugPanel {
active_session active_session
.as_ref() .as_ref()
.map(|session| session.read(cx).running_state()), .map(|session| session.read(cx).running_state()),
|this, running_session| { |this, running_state| {
let thread_status = let thread_status =
running_session.read(cx).thread_status(cx).unwrap_or( running_state.read(cx).thread_status(cx).unwrap_or(
project::debugger::session::ThreadStatus::Exited, project::debugger::session::ThreadStatus::Exited,
); );
let capabilities = running_session.read(cx).capabilities(cx); let capabilities = running_state.read(cx).capabilities(cx);
this.map(|this| { this.map(|this| {
if thread_status == ThreadStatus::Running { if thread_status == ThreadStatus::Running {
this.child( this.child(
@ -667,7 +601,7 @@ impl DebugPanel {
.icon_size(IconSize::XSmall) .icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square) .shape(ui::IconButtonShape::Square)
.on_click(window.listener_for( .on_click(window.listener_for(
&running_session, &running_state,
|this, _, _window, cx| { |this, _, _window, cx| {
this.pause_thread(cx); this.pause_thread(cx);
}, },
@ -694,7 +628,7 @@ impl DebugPanel {
.icon_size(IconSize::XSmall) .icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square) .shape(ui::IconButtonShape::Square)
.on_click(window.listener_for( .on_click(window.listener_for(
&running_session, &running_state,
|this, _, _window, cx| this.continue_thread(cx), |this, _, _window, cx| this.continue_thread(cx),
)) ))
.disabled(thread_status != ThreadStatus::Stopped) .disabled(thread_status != ThreadStatus::Stopped)
@ -718,7 +652,7 @@ impl DebugPanel {
.icon_size(IconSize::XSmall) .icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square) .shape(ui::IconButtonShape::Square)
.on_click(window.listener_for( .on_click(window.listener_for(
&running_session, &running_state,
|this, _, _window, cx| { |this, _, _window, cx| {
this.step_over(cx); this.step_over(cx);
}, },
@ -742,7 +676,7 @@ impl DebugPanel {
.icon_size(IconSize::XSmall) .icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square) .shape(ui::IconButtonShape::Square)
.on_click(window.listener_for( .on_click(window.listener_for(
&running_session, &running_state,
|this, _, _window, cx| { |this, _, _window, cx| {
this.step_out(cx); this.step_out(cx);
}, },
@ -769,7 +703,7 @@ impl DebugPanel {
.icon_size(IconSize::XSmall) .icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square) .shape(ui::IconButtonShape::Square)
.on_click(window.listener_for( .on_click(window.listener_for(
&running_session, &running_state,
|this, _, _window, cx| { |this, _, _window, cx| {
this.step_in(cx); this.step_in(cx);
}, },
@ -819,7 +753,7 @@ impl DebugPanel {
|| thread_status == ThreadStatus::Ended, || thread_status == ThreadStatus::Ended,
) )
.on_click(window.listener_for( .on_click(window.listener_for(
&running_session, &running_state,
|this, _, _window, cx| { |this, _, _window, cx| {
this.toggle_ignore_breakpoints(cx); this.toggle_ignore_breakpoints(cx);
}, },
@ -842,7 +776,7 @@ impl DebugPanel {
IconButton::new("debug-restart", IconName::DebugRestart) IconButton::new("debug-restart", IconName::DebugRestart)
.icon_size(IconSize::XSmall) .icon_size(IconSize::XSmall)
.on_click(window.listener_for( .on_click(window.listener_for(
&running_session, &running_state,
|this, _, _window, cx| { |this, _, _window, cx| {
this.restart_session(cx); this.restart_session(cx);
}, },
@ -864,7 +798,7 @@ impl DebugPanel {
IconButton::new("debug-stop", IconName::Power) IconButton::new("debug-stop", IconName::Power)
.icon_size(IconSize::XSmall) .icon_size(IconSize::XSmall)
.on_click(window.listener_for( .on_click(window.listener_for(
&running_session, &running_state,
|this, _, _window, cx| { |this, _, _window, cx| {
this.stop_thread(cx); this.stop_thread(cx);
}, },
@ -898,7 +832,7 @@ impl DebugPanel {
IconButton::new("debug-disconnect", IconName::DebugDetach) IconButton::new("debug-disconnect", IconName::DebugDetach)
.icon_size(IconSize::XSmall) .icon_size(IconSize::XSmall)
.on_click(window.listener_for( .on_click(window.listener_for(
&running_session, &running_state,
|this, _, _, cx| { |this, _, _, cx| {
this.detach_client(cx); this.detach_client(cx);
}, },
@ -932,30 +866,42 @@ impl DebugPanel {
.as_ref() .as_ref()
.map(|session| session.read(cx).running_state()) .map(|session| session.read(cx).running_state())
.cloned(), .cloned(),
|this, session| { |this, running_state| {
this.child( this.children({
session.update(cx, |this, cx| { let running_state = running_state.clone();
this.thread_dropdown(window, cx) let threads =
}), running_state.update(cx, |running_state, cx| {
) let session = running_state.session();
session
.update(cx, |session, cx| session.threads(cx))
});
self.render_thread_dropdown(
&running_state,
threads,
window,
cx,
)
})
.when(!is_side, |this| this.gap_2().child(Divider::vertical())) .when(!is_side, |this| this.gap_2().child(Divider::vertical()))
}, },
), ),
) )
.child( .child(
h_flex() h_flex()
.when_some(active_session.as_ref(), |this, session| { .children(self.render_session_menu(
let context_menu = self.active_session(),
self.sessions_drop_down_menu(session, window, cx); self.running_state(cx),
this.child(context_menu).gap_2().child(Divider::vertical()) window,
}) cx,
))
.when(!is_side, |this| this.child(new_session_button())), .when(!is_side, |this| this.child(new_session_button())),
), ),
), ),
) )
} }
fn activate_pane_in_direction( pub(crate) fn activate_pane_in_direction(
&mut self, &mut self,
direction: SplitDirection, direction: SplitDirection,
window: &mut Window, window: &mut Window,
@ -970,7 +916,7 @@ impl DebugPanel {
} }
} }
fn activate_item( pub(crate) fn activate_item(
&mut self, &mut self,
item: DebuggerPaneItem, item: DebuggerPaneItem,
window: &mut Window, window: &mut Window,
@ -985,7 +931,7 @@ impl DebugPanel {
} }
} }
fn activate_session( pub(crate) fn activate_session(
&mut self, &mut self,
session_item: Entity<DebugSession>, session_item: Entity<DebugSession>,
window: &mut Window, window: &mut Window,

View file

@ -13,6 +13,7 @@ use workspace::{ItemHandle, ShutdownDebugAdapters, Workspace};
pub mod attach_modal; pub mod attach_modal;
pub mod debugger_panel; pub mod debugger_panel;
mod dropdown_menus;
mod new_session_modal; mod new_session_modal;
mod persistence; mod persistence;
pub(crate) mod session; pub(crate) mod session;

View file

@ -0,0 +1,186 @@
use gpui::Entity;
use project::debugger::session::{ThreadId, ThreadStatus};
use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
use crate::{
debugger_panel::DebugPanel,
session::{DebugSession, running::RunningState},
};
impl DebugPanel {
fn dropdown_label(label: impl Into<SharedString>) -> Label {
Label::new(label).size(LabelSize::Small)
}
pub fn render_session_menu(
&mut self,
active_session: Option<Entity<DebugSession>>,
running_state: Option<Entity<RunningState>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<impl IntoElement> {
if let Some(running_state) = running_state {
let sessions = self.sessions().clone();
let weak = cx.weak_entity();
let running_state = running_state.read(cx);
let label = if let Some(active_session) = active_session {
active_session.read(cx).session(cx).read(cx).label()
} else {
SharedString::new_static("Unknown Session")
};
let is_terminated = running_state.session().read(cx).is_terminated();
let session_state_indicator = {
if is_terminated {
Some(Indicator::dot().color(Color::Error))
} else {
match running_state.thread_status(cx).unwrap_or_default() {
project::debugger::session::ThreadStatus::Stopped => {
Some(Indicator::dot().color(Color::Conflict))
}
_ => Some(Indicator::dot().color(Color::Success)),
}
}
};
let trigger = h_flex()
.gap_2()
.when_some(session_state_indicator, |this, indicator| {
this.child(indicator)
})
.justify_between()
.child(
DebugPanel::dropdown_label(label)
.when(is_terminated, |this| this.strikethrough()),
)
.into_any_element();
Some(
DropdownMenu::new_with_element(
"debugger-session-list",
trigger,
ContextMenu::build(window, cx, move |mut this, _, cx| {
let context_menu = cx.weak_entity();
for session in sessions.into_iter() {
let weak_session = session.downgrade();
let weak_session_id = weak_session.entity_id();
this = this.custom_entry(
{
let weak = weak.clone();
let context_menu = context_menu.clone();
move |_, cx| {
weak_session
.read_with(cx, |session, cx| {
let context_menu = context_menu.clone();
let id: SharedString = format!(
"debug-session-{}",
session.session_id(cx).0
)
.into();
h_flex()
.w_full()
.group(id.clone())
.justify_between()
.child(session.label_element(cx))
.child(
IconButton::new(
"close-debug-session",
IconName::Close,
)
.visible_on_hover(id.clone())
.icon_size(IconSize::Small)
.on_click({
let weak = weak.clone();
move |_, window, cx| {
weak.update(cx, |panel, cx| {
panel.close_session(
weak_session_id,
window,
cx,
);
})
.ok();
context_menu
.update(cx, |this, cx| {
this.cancel(
&Default::default(),
window,
cx,
);
})
.ok();
}
}),
)
.into_any_element()
})
.unwrap_or_else(|_| div().into_any_element())
}
},
{
let weak = weak.clone();
move |window, cx| {
weak.update(cx, |panel, cx| {
panel.activate_session(session.clone(), window, cx);
})
.ok();
}
},
);
}
this
}),
)
.style(DropdownStyle::Ghost),
)
} else {
None
}
}
pub(crate) fn render_thread_dropdown(
&self,
running_state: &Entity<RunningState>,
threads: Vec<(dap::Thread, ThreadStatus)>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<DropdownMenu> {
let running_state = running_state.clone();
let running_state_read = running_state.read(cx);
let thread_id = running_state_read.thread_id();
let session = running_state_read.session();
let session_id = session.read(cx).session_id();
let session_terminated = session.read(cx).is_terminated();
let selected_thread_name = threads
.iter()
.find(|(thread, _)| thread_id.map(|id| id.0) == Some(thread.id))
.map(|(thread, _)| thread.name.clone());
if let Some(selected_thread_name) = selected_thread_name {
let trigger = DebugPanel::dropdown_label(selected_thread_name).into_any_element();
Some(
DropdownMenu::new_with_element(
("thread-list", session_id.0),
trigger,
ContextMenu::build_eager(window, cx, move |mut this, _, _| {
for (thread, _) in threads {
let running_state = running_state.clone();
let thread_id = thread.id;
this = this.entry(thread.name, None, move |window, cx| {
running_state.update(cx, |running_state, cx| {
running_state.select_thread(ThreadId(thread_id), window, cx);
});
});
}
this
}),
)
.disabled(session_terminated)
.style(DropdownStyle::Ghost),
)
} else {
None
}
}
}

View file

@ -1,7 +1,6 @@
pub mod running; pub mod running;
use std::{cell::OnceCell, sync::OnceLock}; use crate::{StackTraceView, debugger_panel::DebugPanel, persistence::SerializedLayout};
use dap::client::SessionId; use dap::client::SessionId;
use gpui::{ use gpui::{
App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
@ -11,14 +10,13 @@ use project::debugger::session::Session;
use project::worktree_store::WorktreeStore; use project::worktree_store::WorktreeStore;
use rpc::proto; use rpc::proto;
use running::RunningState; use running::RunningState;
use std::{cell::OnceCell, sync::OnceLock};
use ui::{Indicator, prelude::*}; use ui::{Indicator, prelude::*};
use workspace::{ use workspace::{
CollaboratorId, FollowableItem, ViewId, Workspace, CollaboratorId, FollowableItem, ViewId, Workspace,
item::{self, Item}, item::{self, Item},
}; };
use crate::{StackTraceView, debugger_panel::DebugPanel, persistence::SerializedLayout};
pub struct DebugSession { pub struct DebugSession {
remote_id: Option<workspace::ViewId>, remote_id: Option<workspace::ViewId>,
running_state: Entity<RunningState>, running_state: Entity<RunningState>,
@ -159,7 +157,11 @@ impl DebugSession {
.gap_2() .gap_2()
.when_some(icon, |this, indicator| this.child(indicator)) .when_some(icon, |this, indicator| this.child(indicator))
.justify_between() .justify_between()
.child(Label::new(label).when(is_terminated, |this| this.strikethrough())) .child(
Label::new(label)
.size(LabelSize::Small)
.when(is_terminated, |this| this.strikethrough()),
)
.into_any_element() .into_any_element()
} }
} }

View file

@ -43,11 +43,10 @@ use task::{
}; };
use terminal_view::TerminalView; use terminal_view::TerminalView;
use ui::{ use ui::{
ActiveTheme, AnyElement, App, ButtonCommon as _, Clickable as _, Context, ContextMenu, ActiveTheme, AnyElement, App, ButtonCommon as _, Clickable as _, Context, FluentBuilder,
Disableable, DropdownMenu, FluentBuilder, IconButton, IconName, IconSize, InteractiveElement, IconButton, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon as _,
IntoElement, Label, LabelCommon as _, ParentElement, Render, SharedString, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Tab, Tooltip,
StatefulInteractiveElement, Styled, Tab, Tooltip, VisibleOnHover, VisualContext, Window, div, VisibleOnHover, VisualContext, Window, div, h_flex, v_flex,
h_flex, v_flex,
}; };
use util::ResultExt; use util::ResultExt;
use variable_list::VariableList; use variable_list::VariableList;
@ -78,6 +77,12 @@ pub struct RunningState {
_schedule_serialize: Option<Task<()>>, _schedule_serialize: Option<Task<()>>,
} }
impl RunningState {
pub(crate) fn thread_id(&self) -> Option<ThreadId> {
self.thread_id
}
}
impl Render for RunningState { impl Render for RunningState {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let zoomed_pane = self let zoomed_pane = self
@ -515,7 +520,7 @@ impl Focusable for DebugTerminal {
} }
impl RunningState { impl RunningState {
pub fn new( pub(crate) fn new(
session: Entity<Session>, session: Entity<Session>,
project: Entity<Project>, project: Entity<Project>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
@ -1311,7 +1316,12 @@ impl RunningState {
.map(|id| self.session().read(cx).thread_status(id)) .map(|id| self.session().read(cx).thread_status(id))
} }
fn select_thread(&mut self, thread_id: ThreadId, window: &mut Window, cx: &mut Context<Self>) { pub(crate) fn select_thread(
&mut self,
thread_id: ThreadId,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.thread_id.is_some_and(|id| id == thread_id) { if self.thread_id.is_some_and(|id| id == thread_id) {
return; return;
} }
@ -1448,38 +1458,6 @@ impl RunningState {
}); });
} }
pub(crate) fn thread_dropdown(
&self,
window: &mut Window,
cx: &mut Context<'_, RunningState>,
) -> DropdownMenu {
let state = cx.entity();
let session_terminated = self.session.read(cx).is_terminated();
let threads = self.session.update(cx, |this, cx| this.threads(cx));
let selected_thread_name = threads
.iter()
.find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id))
.map(|(thread, _)| thread.name.clone())
.unwrap_or("Threads".to_owned());
DropdownMenu::new(
("thread-list", self.session_id.0),
selected_thread_name,
ContextMenu::build_eager(window, cx, move |mut this, _, _| {
for (thread, _) in threads {
let state = state.clone();
let thread_id = thread.id;
this = this.entry(thread.name, None, move |window, cx| {
state.update(cx, |state, cx| {
state.select_thread(ThreadId(thread_id), window, cx);
});
});
}
this
}),
)
.disabled(session_terminated)
}
fn default_pane_layout( fn default_pane_layout(
project: Entity<Project>, project: Entity<Project>,
workspace: &WeakEntity<Workspace>, workspace: &WeakEntity<Workspace>,

View file

@ -1,7 +1,14 @@
use gpui::{ClickEvent, Corner, CursorStyle, Entity, MouseButton}; use gpui::{ClickEvent, Corner, CursorStyle, Entity, Hsla, MouseButton};
use crate::{ContextMenu, PopoverMenu, prelude::*}; use crate::{ContextMenu, PopoverMenu, prelude::*};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DropdownStyle {
#[default]
Solid,
Ghost,
}
enum LabelKind { enum LabelKind {
Text(SharedString), Text(SharedString),
Element(AnyElement), Element(AnyElement),
@ -11,6 +18,7 @@ enum LabelKind {
pub struct DropdownMenu { pub struct DropdownMenu {
id: ElementId, id: ElementId,
label: LabelKind, label: LabelKind,
style: DropdownStyle,
menu: Entity<ContextMenu>, menu: Entity<ContextMenu>,
full_width: bool, full_width: bool,
disabled: bool, disabled: bool,
@ -25,6 +33,7 @@ impl DropdownMenu {
Self { Self {
id: id.into(), id: id.into(),
label: LabelKind::Text(label.into()), label: LabelKind::Text(label.into()),
style: DropdownStyle::default(),
menu, menu,
full_width: false, full_width: false,
disabled: false, disabled: false,
@ -39,12 +48,18 @@ impl DropdownMenu {
Self { Self {
id: id.into(), id: id.into(),
label: LabelKind::Element(label), label: LabelKind::Element(label),
style: DropdownStyle::default(),
menu, menu,
full_width: false, full_width: false,
disabled: false, disabled: false,
} }
} }
pub fn style(mut self, style: DropdownStyle) -> Self {
self.style = style;
self
}
pub fn full_width(mut self, full_width: bool) -> Self { pub fn full_width(mut self, full_width: bool) -> Self {
self.full_width = full_width; self.full_width = full_width;
self self
@ -66,7 +81,8 @@ impl RenderOnce for DropdownMenu {
.trigger( .trigger(
DropdownMenuTrigger::new(self.label) DropdownMenuTrigger::new(self.label)
.full_width(self.full_width) .full_width(self.full_width)
.disabled(self.disabled), .disabled(self.disabled)
.style(self.style),
) )
.attach(Corner::BottomLeft) .attach(Corner::BottomLeft)
} }
@ -135,12 +151,35 @@ impl Component for DropdownMenu {
} }
} }
#[derive(Debug, Clone, Copy)]
pub struct DropdownTriggerStyle {
pub bg: Hsla,
}
impl DropdownTriggerStyle {
pub fn for_style(style: DropdownStyle, cx: &App) -> Self {
let colors = cx.theme().colors();
if style == DropdownStyle::Solid {
Self {
// why is this editor_background?
bg: colors.editor_background,
}
} else {
Self {
bg: colors.ghost_element_background,
}
}
}
}
#[derive(IntoElement)] #[derive(IntoElement)]
struct DropdownMenuTrigger { struct DropdownMenuTrigger {
label: LabelKind, label: LabelKind,
full_width: bool, full_width: bool,
selected: bool, selected: bool,
disabled: bool, disabled: bool,
style: DropdownStyle,
cursor_style: CursorStyle, cursor_style: CursorStyle,
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>, on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
} }
@ -152,6 +191,7 @@ impl DropdownMenuTrigger {
full_width: false, full_width: false,
selected: false, selected: false,
disabled: false, disabled: false,
style: DropdownStyle::default(),
cursor_style: CursorStyle::default(), cursor_style: CursorStyle::default(),
on_click: None, on_click: None,
} }
@ -161,6 +201,11 @@ impl DropdownMenuTrigger {
self.full_width = full_width; self.full_width = full_width;
self self
} }
pub fn style(mut self, style: DropdownStyle) -> Self {
self.style = style;
self
}
} }
impl Disableable for DropdownMenuTrigger { impl Disableable for DropdownMenuTrigger {
@ -193,11 +238,13 @@ impl RenderOnce for DropdownMenuTrigger {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let disabled = self.disabled; let disabled = self.disabled;
let style = DropdownTriggerStyle::for_style(self.style, cx);
h_flex() h_flex()
.id("dropdown-menu-trigger") .id("dropdown-menu-trigger")
.justify_between() .justify_between()
.rounded_sm() .rounded_sm()
.bg(cx.theme().colors().editor_background) .bg(style.bg)
.pl_2() .pl_2()
.pr_1p5() .pr_1p5()
.py_0p5() .py_0p5()