redesign the sidebar
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 2m0s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m27s

This commit is contained in:
2026-02-11 08:55:42 +07:00
parent a9d2a0a24b
commit 9bee5f2a77
14 changed files with 962 additions and 1167 deletions

View File

@@ -10,7 +10,7 @@ use theme::ActiveTheme;
use crate::indicator::Indicator;
use crate::tooltip::Tooltip;
use crate::{h_flex, Disableable, Icon, Selectable, Sizable, Size, StyledExt};
use crate::{h_flex, Disableable, Icon, IconName, Selectable, Sizable, Size, StyledExt};
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct ButtonCustomVariant {
@@ -20,50 +20,6 @@ pub struct ButtonCustomVariant {
active: Hsla,
}
pub trait ButtonVariants: Sized {
fn with_variant(self, variant: ButtonVariant) -> Self;
/// With the primary style for the Button.
fn primary(self) -> Self {
self.with_variant(ButtonVariant::Primary)
}
/// With the secondary style for the Button.
fn secondary(self) -> Self {
self.with_variant(ButtonVariant::Secondary)
}
/// With the danger style for the Button.
fn danger(self) -> Self {
self.with_variant(ButtonVariant::Danger)
}
/// With the warning style for the Button.
fn warning(self) -> Self {
self.with_variant(ButtonVariant::Warning)
}
/// With the ghost style for the Button.
fn ghost(self) -> Self {
self.with_variant(ButtonVariant::Ghost { alt: false })
}
/// With the ghost style for the Button.
fn ghost_alt(self) -> Self {
self.with_variant(ButtonVariant::Ghost { alt: true })
}
/// With the transparent style for the Button.
fn transparent(self) -> Self {
self.with_variant(ButtonVariant::Transparent)
}
/// With the custom style for the Button.
fn custom(self, style: ButtonCustomVariant) -> Self {
self.with_variant(ButtonVariant::Custom(style))
}
}
impl ButtonCustomVariant {
pub fn new(_window: &Window, cx: &App) -> Self {
Self {
@@ -110,6 +66,50 @@ pub enum ButtonVariant {
Custom(ButtonCustomVariant),
}
pub trait ButtonVariants: Sized {
fn with_variant(self, variant: ButtonVariant) -> Self;
/// With the primary style for the Button.
fn primary(self) -> Self {
self.with_variant(ButtonVariant::Primary)
}
/// With the secondary style for the Button.
fn secondary(self) -> Self {
self.with_variant(ButtonVariant::Secondary)
}
/// With the danger style for the Button.
fn danger(self) -> Self {
self.with_variant(ButtonVariant::Danger)
}
/// With the warning style for the Button.
fn warning(self) -> Self {
self.with_variant(ButtonVariant::Warning)
}
/// With the ghost style for the Button.
fn ghost(self) -> Self {
self.with_variant(ButtonVariant::Ghost { alt: false })
}
/// With the ghost style for the Button.
fn ghost_alt(self) -> Self {
self.with_variant(ButtonVariant::Ghost { alt: true })
}
/// With the transparent style for the Button.
fn transparent(self) -> Self {
self.with_variant(ButtonVariant::Transparent)
}
/// With the custom style for the Button.
fn custom(self, style: ButtonCustomVariant) -> Self {
self.with_variant(ButtonVariant::Custom(style))
}
}
/// A Button element.
#[derive(IntoElement)]
#[allow(clippy::type_complexity)]
@@ -124,17 +124,15 @@ pub struct Button {
children: Vec<AnyElement>,
variant: ButtonVariant,
center: bool,
rounded: bool,
size: Size,
disabled: bool,
reverse: bool,
bold: bool,
cta: bool,
loading: bool,
loading_icon: Option<Icon>,
rounded: bool,
compact: bool,
underline: bool,
caret: bool,
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
on_hover: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
@@ -161,21 +159,19 @@ impl Button {
style: StyleRefinement::default(),
icon: None,
label: None,
variant: ButtonVariant::default(),
disabled: false,
selected: false,
variant: ButtonVariant::default(),
underline: false,
compact: false,
caret: false,
rounded: false,
size: Size::Medium,
tooltip: None,
on_click: None,
on_hover: None,
loading: false,
reverse: false,
center: true,
bold: false,
cta: false,
children: Vec::new(),
loading_icon: None,
tab_index: 0,
tab_stop: true,
}
@@ -211,33 +207,21 @@ impl Button {
self
}
/// Set reverse the position between icon and label.
pub fn reverse(mut self) -> Self {
self.reverse = true;
/// Set true to make the button compact (no padding).
pub fn compact(mut self) -> Self {
self.compact = true;
self
}
/// Set bold the button (label will be use the semi-bold font).
pub fn bold(mut self) -> Self {
self.bold = true;
/// Set true to show the caret indicator.
pub fn caret(mut self) -> Self {
self.caret = true;
self
}
/// Disable centering the button's content.
pub fn no_center(mut self) -> Self {
self.center = false;
self
}
/// Set the cta style of the button.
pub fn cta(mut self) -> Self {
self.cta = true;
self
}
/// Set the loading icon of the button.
pub fn loading_icon(mut self, icon: impl Into<Icon>) -> Self {
self.loading_icon = Some(icon.into());
/// Set true to show the underline indicator.
pub fn underline(mut self) -> Self {
self.underline = true;
self
}
@@ -346,7 +330,7 @@ impl RenderOnce for Button {
};
let focus_handle = window
.use_keyed_state(self.id.clone(), cx, |_, cx| cx.focus_handle())
.use_keyed_state(self.id.clone(), cx, |_window, cx| cx.focus_handle())
.read(cx)
.clone();
@@ -358,10 +342,11 @@ impl RenderOnce for Button {
.tab_stop(self.tab_stop),
)
})
.relative()
.flex_shrink_0()
.flex()
.items_center()
.when(self.center, |this| this.justify_center())
.justify_center()
.cursor_default()
.overflow_hidden()
.refine_style(&self.style)
@@ -369,39 +354,15 @@ impl RenderOnce for Button {
false => this.rounded(cx.theme().radius),
true => this.rounded_full(),
})
.map(|this| {
.when(!self.compact, |this| {
if self.label.is_none() && self.children.is_empty() {
// Icon Button
match self.size {
Size::Size(px) => this.size(px),
Size::XSmall => {
if self.cta {
this.w_10().h_5()
} else {
this.size_5()
}
}
Size::Small => {
if self.cta {
this.w_12().h_6()
} else {
this.size_6()
}
}
Size::Medium => {
if self.cta {
this.w_12().h_7()
} else {
this.size_7()
}
}
_ => {
if self.cta {
this.w_16().h_9()
} else {
this.size_9()
}
}
Size::XSmall => this.size_5(),
Size::Small => this.size_6(),
Size::Medium => this.size_7(),
_ => this.size_9(),
}
} else {
// Normal Button
@@ -410,8 +371,6 @@ impl RenderOnce for Button {
Size::XSmall => {
if self.icon.is_some() {
this.h_6().pl_2().pr_2p5()
} else if self.cta {
this.h_6().px_4()
} else {
this.h_6().px_2()
}
@@ -419,8 +378,6 @@ impl RenderOnce for Button {
Size::Small => {
if self.icon.is_some() {
this.h_7().pl_2().pr_2p5()
} else if self.cta {
this.h_7().px_4()
} else {
this.h_7().px_2()
}
@@ -442,13 +399,27 @@ impl RenderOnce for Button {
}
}
})
.on_mouse_down(gpui::MouseButton::Left, |_, window, _| {
.refine_style(&self.style)
.on_mouse_down(gpui::MouseButton::Left, move |_, window, cx| {
// Stop handle any click event when disabled.
// To avoid handle dropdown menu open when button is disabled.
if self.disabled {
cx.stop_propagation();
return;
}
// Avoid focus on mouse down.
window.prevent_default();
})
.when_some(self.on_click.filter(|_| clickable), |this, on_click| {
.when_some(self.on_click, |this, on_click| {
this.on_click(move |event, window, cx| {
(on_click)(event, window, cx);
// Stop handle any click event when disabled.
// To avoid handle dropdown menu open when button is disabled.
if !clickable {
cx.stop_propagation();
return;
}
on_click(event, window, cx);
})
})
.when_some(self.on_hover.filter(|_| hoverable), |this, on_hover| {
@@ -459,7 +430,6 @@ impl RenderOnce for Button {
.child({
h_flex()
.id("label")
.when(self.reverse, |this| this.flex_row_reverse())
.justify_center()
.map(|this| match self.size {
Size::XSmall => this.text_xs().gap_1(),
@@ -471,22 +441,18 @@ impl RenderOnce for Button {
this.child(icon.with_size(icon_size))
})
})
.when(self.loading, |this| {
this.child(
Indicator::new()
.when_some(self.loading_icon, |this, icon| this.icon(icon)),
)
})
.when(self.loading, |this| this.child(Indicator::new()))
.when_some(self.label, |this, label| {
this.child(
div()
.flex_none()
.line_height(relative(1.))
.child(label)
.when(self.bold, |this| this.font_semibold()),
)
this.child(div().flex_none().line_height(relative(1.)).child(label))
})
.children(self.children)
.when(self.caret, |this| {
this.justify_between().gap_0p5().child(
Icon::new(IconName::ChevronDown)
.small()
.text_color(cx.theme().text_muted),
)
})
})
.text_color(normal_style.fg)
.when(!self.disabled && !self.selected, |this| {
@@ -504,6 +470,17 @@ impl RenderOnce for Button {
let selected_style = style.selected(cx);
this.bg(selected_style.bg).text_color(selected_style.fg)
})
.when(self.selected && self.underline, |this| {
this.child(
div()
.absolute()
.bottom_0()
.left_0()
.h_px()
.w_full()
.bg(cx.theme().element_background),
)
})
.when(self.disabled, |this| {
let disabled_style = style.disabled(cx);
this.cursor_not_allowed()

View File

@@ -61,8 +61,8 @@ impl RenderOnce for Divider {
.absolute()
.rounded_full()
.map(|this| match self.axis {
Axis::Vertical => this.w(px(2.)).h_full(),
Axis::Horizontal => this.h(px(2.)).w_full(),
Axis::Vertical => this.w(px(1.)).h_full(),
Axis::Horizontal => this.h(px(1.)).w_full(),
})
.bg(self.color.unwrap_or(cx.theme().border_variant)),
)

View File

@@ -58,6 +58,7 @@ pub trait PopupMenuExt: Styled + Selectable + InteractiveElement + IntoElement +
})
}
}
impl PopupMenuExt for Button {}
#[allow(clippy::type_complexity)]
@@ -1074,7 +1075,9 @@ impl PopupMenu {
}
impl FluentBuilder for PopupMenu {}
impl EventEmitter<DismissEvent> for PopupMenu {}
impl Focusable for PopupMenu {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()

View File

@@ -1,776 +0,0 @@
use std::ops::Deref;
use std::rc::Rc;
use gpui::prelude::FluentBuilder;
use gpui::{
actions, anchored, canvas, div, px, rems, Action, AnyElement, App, AppContext, AsKeystroke,
Bounds, Context, Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, KeyBinding, Keystroke, ParentElement, Pixels, Render,
ScrollHandle, SharedString, StatefulInteractiveElement, Styled, Subscription, WeakEntity,
Window,
};
use theme::ActiveTheme;
use crate::button::Button;
use crate::list::ListItem;
use crate::popover::Popover;
use crate::scroll::{Scrollbar, ScrollbarState};
use crate::{h_flex, v_flex, Icon, IconName, Selectable, Sizable as _, StyledExt};
actions!(
menu,
[
/// Trigger confirm action when user presses enter button
Confirm,
/// Trigger dismiss action when user presses escape button
Dismiss,
/// Select the next item when user presses up button
SelectNext,
/// Select the previous item when user preses down button
SelectPrev
]
);
const ITEM_HEIGHT: Pixels = px(26.);
pub fn init(cx: &mut App) {
let context = Some("PopupMenu");
cx.bind_keys([
KeyBinding::new("enter", Confirm, context),
KeyBinding::new("escape", Dismiss, context),
KeyBinding::new("up", SelectPrev, context),
KeyBinding::new("down", SelectNext, context),
]);
}
pub trait PopupMenuExt: Styled + Selectable + InteractiveElement + IntoElement + 'static {
/// Create a popup menu with the given items, anchored to the TopLeft corner
fn popup_menu(
self,
f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
) -> Popover<PopupMenu> {
self.popup_menu_with_anchor(Corner::TopLeft, f)
}
/// Create a popup menu with the given items, anchored to the given corner
fn popup_menu_with_anchor(
mut self,
anchor: impl Into<Corner>,
f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
) -> Popover<PopupMenu> {
let style = self.style().clone();
let id = self.interactivity().element_id.clone();
Popover::new(SharedString::from(format!("popup-menu:{id:?}")))
.no_style()
.trigger(self)
.trigger_style(style)
.anchor(anchor.into())
.content(move |window, cx| {
PopupMenu::build(window, cx, |menu, window, cx| f(menu, window, cx))
})
}
}
impl PopupMenuExt for Button {}
enum PopupMenuItem {
Title(SharedString),
Separator,
Item {
icon: Option<Icon>,
label: SharedString,
action: Option<Box<dyn Action>>,
#[allow(clippy::type_complexity)]
handler: Rc<dyn Fn(&mut Window, &mut App)>,
},
ElementItem {
#[allow(clippy::type_complexity)]
render: Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>,
#[allow(clippy::type_complexity)]
handler: Rc<dyn Fn(&mut Window, &mut App)>,
},
Submenu {
icon: Option<Icon>,
label: SharedString,
menu: Entity<PopupMenu>,
},
}
impl PopupMenuItem {
fn is_clickable(&self) -> bool {
!matches!(self, PopupMenuItem::Separator)
}
fn is_separator(&self) -> bool {
matches!(self, PopupMenuItem::Separator)
}
fn has_icon(&self) -> bool {
matches!(self, PopupMenuItem::Item { icon: Some(_), .. })
}
}
pub struct PopupMenu {
/// The parent menu of this menu, if this is a submenu
parent_menu: Option<WeakEntity<Self>>,
focus_handle: FocusHandle,
menu_items: Vec<PopupMenuItem>,
has_icon: bool,
selected_index: Option<usize>,
min_width: Pixels,
max_width: Pixels,
hovered_menu_ix: Option<usize>,
bounds: Bounds<Pixels>,
scrollable: bool,
scroll_handle: ScrollHandle,
scroll_state: ScrollbarState,
action_focus_handle: Option<FocusHandle>,
#[allow(dead_code)]
subscriptions: Vec<Subscription>,
}
impl PopupMenu {
pub fn build(
window: &mut Window,
cx: &mut App,
f: impl FnOnce(Self, &mut Window, &mut Context<PopupMenu>) -> Self,
) -> Entity<Self> {
cx.new(|cx| {
let focus_handle = cx.focus_handle();
let subscriptions =
vec![
cx.on_blur(&focus_handle, window, |this: &mut PopupMenu, window, cx| {
this.dismiss(&Dismiss, window, cx)
}),
];
let menu = Self {
focus_handle,
action_focus_handle: None,
parent_menu: None,
menu_items: Vec::new(),
selected_index: None,
min_width: px(120.),
max_width: px(500.),
has_icon: false,
hovered_menu_ix: None,
bounds: Bounds::default(),
scrollable: false,
scroll_handle: ScrollHandle::default(),
scroll_state: ScrollbarState::default(),
subscriptions,
};
f(menu, window, cx)
})
}
/// Bind the focus handle of the menu, when clicked, it will focus back to this handle and then dispatch the action
pub fn track_focus(mut self, focus_handle: &FocusHandle) -> Self {
self.action_focus_handle = Some(focus_handle.clone());
self
}
/// Set min width of the popup menu, default is 120px
pub fn min_w(mut self, width: impl Into<Pixels>) -> Self {
self.min_width = width.into();
self
}
/// Set max width of the popup menu, default is 500px
pub fn max_w(mut self, width: impl Into<Pixels>) -> Self {
self.max_width = width.into();
self
}
/// Set the menu to be scrollable to show vertical scrollbar.
///
/// NOTE: If this is true, the sub-menus will cannot be support.
pub fn scrollable(mut self) -> Self {
self.scrollable = true;
self
}
/// Add Menu Item
pub fn menu(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
self.add_menu_item(label, None, action);
self
}
/// Add Menu to open link
pub fn link(mut self, label: impl Into<SharedString>, href: impl Into<String>) -> Self {
let href = href.into();
self.menu_items.push(PopupMenuItem::Item {
icon: None,
label: label.into(),
action: None,
handler: Rc::new(move |_window, cx| cx.open_url(&href)),
});
self
}
/// Add Menu to open link
pub fn link_with_icon(
mut self,
label: impl Into<SharedString>,
icon: impl Into<Icon>,
href: impl Into<String>,
) -> Self {
let href = href.into();
self.menu_items.push(PopupMenuItem::Item {
icon: Some(icon.into()),
label: label.into(),
action: None,
handler: Rc::new(move |_window, cx| cx.open_url(&href)),
});
self
}
/// Add Menu Item with Icon
pub fn menu_with_icon(
mut self,
label: impl Into<SharedString>,
icon: impl Into<Icon>,
action: Box<dyn Action>,
) -> Self {
self.add_menu_item(label, Some(icon.into()), action);
self
}
/// Add Menu Item with check icon
pub fn menu_with_check(
mut self,
label: impl Into<SharedString>,
checked: bool,
action: Box<dyn Action>,
) -> Self {
if checked {
self.add_menu_item(label, Some(IconName::Check.into()), action);
} else {
self.add_menu_item(label, None, action);
}
self
}
/// Add Menu Item with custom element render.
pub fn menu_with_element<F, E>(mut self, builder: F, action: Box<dyn Action>) -> Self
where
F: Fn(&mut Window, &mut App) -> E + 'static,
E: IntoElement,
{
self.menu_items.push(PopupMenuItem::ElementItem {
render: Box::new(move |window, cx| builder(window, cx).into_any_element()),
handler: self.wrap_handler(action),
});
self
}
#[allow(clippy::type_complexity)]
fn wrap_handler(&self, action: Box<dyn Action>) -> Rc<dyn Fn(&mut Window, &mut App)> {
let action_focus_handle = self.action_focus_handle.clone();
Rc::new(move |window, cx| {
window.activate_window();
// Focus back to the user expected focus handle
// Then the actions listened on that focus handle can be received
//
// For example:
//
// TabPanel
// |- PopupMenu
// |- PanelContent (actions are listened here)
//
// The `PopupMenu` and `PanelContent` are at the same level in the TabPanel
// If the actions are listened on the `PanelContent`,
// it can't receive the actions from the `PopupMenu`, unless we focus on `PanelContent`.
if let Some(handle) = action_focus_handle.as_ref() {
window.focus(handle);
}
window.dispatch_action(action.boxed_clone(), cx);
})
}
fn add_menu_item(
&mut self,
label: impl Into<SharedString>,
icon: Option<Icon>,
action: Box<dyn Action>,
) -> &mut Self {
if icon.is_some() {
self.has_icon = true;
}
self.menu_items.push(PopupMenuItem::Item {
icon,
label: label.into(),
action: Some(action.boxed_clone()),
handler: self.wrap_handler(action),
});
self
}
/// Add a title menu item
pub fn title(mut self, label: impl Into<SharedString>) -> Self {
if self.menu_items.is_empty() {
return self;
}
if let Some(PopupMenuItem::Title(_)) = self.menu_items.last() {
return self;
}
self.menu_items.push(PopupMenuItem::Title(label.into()));
self
}
/// Add a separator Menu Item
pub fn separator(mut self) -> Self {
if self.menu_items.is_empty() {
return self;
}
if let Some(PopupMenuItem::Separator) = self.menu_items.last() {
return self;
}
self.menu_items.push(PopupMenuItem::Separator);
self
}
pub fn submenu(
self,
label: impl Into<SharedString>,
window: &mut Window,
cx: &mut Context<Self>,
f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
) -> Self {
self.submenu_with_icon(None, label, window, cx, f)
}
/// Add a Submenu item with icon
pub fn submenu_with_icon(
mut self,
icon: Option<Icon>,
label: impl Into<SharedString>,
window: &mut Window,
cx: &mut Context<Self>,
f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
) -> Self {
let submenu = PopupMenu::build(window, cx, f);
let parent_menu = cx.entity().downgrade();
submenu.update(cx, |view, _| {
view.parent_menu = Some(parent_menu);
});
self.menu_items.push(PopupMenuItem::Submenu {
icon,
label: label.into(),
menu: submenu,
});
self
}
pub(crate) fn active_submenu(&self) -> Option<Entity<PopupMenu>> {
if let Some(ix) = self.hovered_menu_ix {
if let Some(item) = self.menu_items.get(ix) {
return match item {
PopupMenuItem::Submenu { menu, .. } => Some(menu.clone()),
_ => None,
};
}
}
None
}
pub fn is_empty(&self) -> bool {
self.menu_items.is_empty()
}
fn clickable_menu_items(&self) -> impl Iterator<Item = (usize, &PopupMenuItem)> {
self.menu_items
.iter()
.enumerate()
.filter(|(_, item)| item.is_clickable())
}
fn on_click(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
cx.stop_propagation();
window.prevent_default();
self.selected_index = Some(ix);
self.confirm(&Confirm, window, cx);
}
fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
if let Some(index) = self.selected_index {
let item = self.menu_items.get(index);
match item {
Some(PopupMenuItem::Item { handler, .. }) => {
handler(window, cx);
self.dismiss(&Dismiss, window, cx)
}
Some(PopupMenuItem::ElementItem { handler, .. }) => {
handler(window, cx);
self.dismiss(&Dismiss, window, cx)
}
_ => {}
}
}
}
fn select_next(&mut self, _: &SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
let count = self.clickable_menu_items().count();
if count > 0 {
let last_ix = count.saturating_sub(1);
let ix = self
.selected_index
.map(|index| if index == last_ix { 0 } else { index + 1 })
.unwrap_or(0);
self.selected_index = Some(ix);
cx.notify();
}
}
fn select_prev(&mut self, _: &SelectPrev, _window: &mut Window, cx: &mut Context<Self>) {
let count = self.clickable_menu_items().count();
if count > 0 {
let last_ix = count.saturating_sub(1);
let ix = self
.selected_index
.map(|index| {
if index == last_ix {
0
} else {
index.saturating_sub(1)
}
})
.unwrap_or(last_ix);
self.selected_index = Some(ix);
cx.notify();
}
}
// TODO: fix this
#[allow(clippy::only_used_in_recursion)]
fn dismiss(&mut self, _: &Dismiss, window: &mut Window, cx: &mut Context<Self>) {
if self.active_submenu().is_some() {
return;
}
cx.emit(DismissEvent);
// Dismiss parent menu, when this menu is dismissed
if let Some(parent_menu) = self.parent_menu.clone().and_then(|menu| menu.upgrade()) {
parent_menu.update(cx, |view, cx| {
view.hovered_menu_ix = None;
view.dismiss(&Dismiss, window, cx);
})
}
}
fn render_keybinding(
action: Option<Box<dyn Action>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<impl IntoElement> {
if let Some(action) = action {
if let Some(keybinding) = window.bindings_for_action(action.deref()).first() {
let el = div().text_color(cx.theme().text_muted).children(
keybinding
.keystrokes()
.iter()
.map(|key| key_shortcut(key.as_keystroke().clone())),
);
return Some(el);
}
}
None
}
fn render_icon(
has_icon: bool,
icon: Option<Icon>,
_window: &Window,
_cx: &Context<Self>,
) -> Option<impl IntoElement> {
let icon_placeholder = if has_icon { Some(Icon::empty()) } else { None };
if !has_icon {
return None;
}
let icon = h_flex()
.w_3p5()
.h_3p5()
.items_center()
.justify_center()
.text_sm()
.map(|this| {
if let Some(icon) = icon {
this.child(icon.clone().small())
} else {
this.children(icon_placeholder.clone())
}
});
Some(icon)
}
}
impl FluentBuilder for PopupMenu {}
impl EventEmitter<DismissEvent> for PopupMenu {}
impl Focusable for PopupMenu {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for PopupMenu {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity().clone();
let has_icon = self.menu_items.iter().any(|item| item.has_icon());
let items_count = self.menu_items.len();
let max_width = self.max_width;
let bounds = self.bounds;
let window_haft_height = window.window_bounds().get_bounds().size.height * 0.5;
let max_height = window_haft_height.min(px(450.));
v_flex()
.id("popup-menu")
.key_context("PopupMenu")
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_prev))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::dismiss))
.on_mouse_down_out(cx.listener(|this, _, window, cx| this.dismiss(&Dismiss, window, cx)))
.popover_style(cx)
.relative()
.p_1()
.child(
div()
.id("popup-menu-items")
.when(self.scrollable, |this| {
this.max_h(max_height)
.overflow_y_scroll()
.track_scroll(&self.scroll_handle)
})
.child(
v_flex()
.gap_y_0p5()
.min_w(self.min_width)
.max_w(self.max_width)
.min_w(rems(8.))
.child({
canvas(
move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds),
|_, _, _, _| {},
)
.absolute()
.size_full()
})
.children(
self.menu_items
.iter_mut()
.enumerate()
// Skip last separator
.filter(|(ix, item)| !(*ix == items_count - 1 && item.is_separator()))
.map(|(ix, item)| {
let this = ListItem::new(("menu-item", ix))
.relative()
.items_center()
.py_0()
.px_2()
.rounded_md()
.text_sm()
.on_mouse_enter(cx.listener(move |this, _, _window, cx| {
this.hovered_menu_ix = Some(ix);
cx.notify();
}));
match item {
PopupMenuItem::Title(label) => {
this.child(
div()
.text_xs()
.font_semibold()
.text_color(cx.theme().text_muted)
.child(label.clone())
)
},
PopupMenuItem::Separator => this.h_auto().p_0().disabled(true).child(
div()
.rounded_none()
.h(px(1.))
.mx_neg_1()
.my_0p5()
.bg(cx.theme().border_disabled),
),
PopupMenuItem::ElementItem { render, .. } => this
.on_click(
cx.listener(move |this, _, window, cx| {
this.on_click(ix, window, cx)
}),
)
.child(
h_flex()
.min_h(ITEM_HEIGHT)
.items_center()
.gap_x_1()
.children(Self::render_icon(has_icon, None, window, cx))
.child((render)(window, cx)),
),
PopupMenuItem::Item {
icon, label, action, ..
} => {
let action = action.as_ref().map(|action| action.boxed_clone());
let key = Self::render_keybinding(action, window, cx);
this.on_click(
cx.listener(move |this, _, window, cx| {
this.on_click(ix, window, cx)
}),
)
.child(
h_flex()
.h(ITEM_HEIGHT)
.items_center()
.gap_x_1p5()
.children(Self::render_icon(has_icon, icon.clone(), window, cx))
.child(
h_flex()
.flex_1()
.gap_2()
.items_center()
.justify_between()
.child(label.clone())
.children(key),
),
)
}
PopupMenuItem::Submenu { icon, label, menu } => this
.when(self.hovered_menu_ix == Some(ix), |this| this.selected(true))
.child(
h_flex()
.items_start()
.child(
h_flex()
.size_full()
.items_center()
.gap_x_1p5()
.children(Self::render_icon(
has_icon,
icon.clone(),
window,
cx,
))
.child(
h_flex()
.flex_1()
.gap_2()
.items_center()
.justify_between()
.child(label.clone())
.child(IconName::CaretRight),
),
)
.when_some(self.hovered_menu_ix, |this, hovered_ix| {
let (anchor, left) = if window.bounds().size.width
- bounds.origin.x
< max_width
{
(Corner::TopRight, -px(15.))
} else {
(Corner::TopLeft, bounds.size.width - px(10.))
};
let top = if bounds.origin.y + bounds.size.height
> window.bounds().size.height
{
px(32.)
} else {
-px(10.)
};
if hovered_ix == ix {
this.child(
anchored()
.anchor(anchor)
.child(
div()
.occlude()
.top(top)
.left(left)
.child(menu.clone()),
)
.snap_to_window_with_margin(Edges::all(px(8.))),
)
} else {
this
}
}),
),
}
}),
),
),
)
.when(self.scrollable, |this| {
// TODO: When the menu is limited by `overflow_y_scroll`, the sub-menu will cannot be displayed.
this.child(
div()
.absolute()
.top_0()
.left_0()
.right_0p5()
.bottom_0()
.child(Scrollbar::vertical(&self.scroll_state, &self.scroll_handle)),
)
})
}
}
/// Return the Platform specific keybinding string by KeyStroke
pub fn key_shortcut(key: Keystroke) -> String {
if cfg!(target_os = "macos") {
return format!("{key}");
}
let mut parts = vec![];
if key.modifiers.control {
parts.push("Ctrl");
}
if key.modifiers.alt {
parts.push("Alt");
}
if key.modifiers.platform {
parts.push("Win");
}
if key.modifiers.shift {
parts.push("Shift");
}
// Capitalize the first letter
let key = if let Some(first_c) = key.key.chars().next() {
format!("{}{}", first_c.to_uppercase(), &key.key[1..])
} else {
key.key.to_string()
};
parts.push(&key);
parts.join("+")
}

View File

@@ -183,39 +183,43 @@ impl<T: Styled> StyleSized<T> for T {
fn input_pl(self, size: Size) -> Self {
match size {
Size::Large => self.pl_5(),
Size::XSmall => self.pl_1(),
Size::Medium => self.pl_3(),
Size::Large => self.pl_5(),
_ => self.pl_2(),
}
}
fn input_pr(self, size: Size) -> Self {
match size {
Size::Large => self.pr_5(),
Size::XSmall => self.pr_1(),
Size::Medium => self.pr_3(),
Size::Large => self.pr_5(),
_ => self.pr_2(),
}
}
fn input_px(self, size: Size) -> Self {
match size {
Size::Large => self.px_5(),
Size::XSmall => self.px_1(),
Size::Medium => self.px_3(),
Size::Large => self.px_5(),
_ => self.px_2(),
}
}
fn input_py(self, size: Size) -> Self {
match size {
Size::Large => self.py_5(),
Size::XSmall => self.py_0p5(),
Size::Medium => self.py_2(),
Size::Large => self.py_5(),
_ => self.py_1(),
}
}
fn input_h(self, size: Size) -> Self {
match size {
Size::XSmall => self.h_7(),
Size::XSmall => self.h_6(),
Size::Small => self.h_8(),
Size::Medium => self.h_9(),
Size::Large => self.h_12(),