popup_menu: Add submenu support. (#165)
- Add submenu support. - Removed Popover `window` mode, this is not a good design. https://github.com/user-attachments/assets/0e3735f4-2bfb-459f-a24d-e3440e738d4f
This commit is contained in:
parent
fd837565c4
commit
f05d2ebbd2
8 changed files with 302 additions and 386 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -4913,6 +4913,7 @@ dependencies = [
|
||||||
"fake",
|
"fake",
|
||||||
"gpui",
|
"gpui",
|
||||||
"regex",
|
"regex",
|
||||||
|
"serde",
|
||||||
"ui",
|
"ui",
|
||||||
"workspace",
|
"workspace",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ workspace.workspace = true
|
||||||
charts-rs = "0.3"
|
charts-rs = "0.3"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
|
serde = "1"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, px, AnchorCorner, AppContext, DismissEvent, Element, EventEmitter, FocusHandle,
|
actions, div, impl_actions, px, AnchorCorner, AppContext, DismissEvent, Element, EventEmitter,
|
||||||
FocusableView, InteractiveElement, IntoElement, KeyBinding, MouseButton, MouseDownEvent,
|
FocusHandle, FocusableView, InteractiveElement, IntoElement, KeyBinding, MouseButton,
|
||||||
ParentElement as _, Render, Styled as _, View, ViewContext, VisualContext, WindowContext,
|
MouseDownEvent, ParentElement as _, Render, Styled as _, View, ViewContext, VisualContext,
|
||||||
|
WindowContext,
|
||||||
};
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
use ui::{
|
use ui::{
|
||||||
button::Button,
|
button::Button,
|
||||||
context_menu::ContextMenuExt,
|
context_menu::ContextMenuExt,
|
||||||
|
|
@ -11,15 +13,18 @@ use ui::{
|
||||||
input::TextInput,
|
input::TextInput,
|
||||||
popover::{Popover, PopoverContent},
|
popover::{Popover, PopoverContent},
|
||||||
popup_menu::PopupMenuExt,
|
popup_menu::PopupMenuExt,
|
||||||
prelude::FluentBuilder,
|
|
||||||
switch::Switch,
|
switch::Switch,
|
||||||
v_flex, IconName, Sizable,
|
v_flex, IconName, Sizable,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Deserialize)]
|
||||||
|
struct Info(usize);
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
popover_story,
|
popover_story,
|
||||||
[Copy, Paste, Cut, SearchAll, ToggleWindowMode]
|
[Copy, Paste, Cut, SearchAll, ToggleWindowMode]
|
||||||
);
|
);
|
||||||
|
impl_actions!(popover_story, [Info]);
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
cx.bind_keys([
|
cx.bind_keys([
|
||||||
|
|
@ -109,6 +114,10 @@ impl PopupStory {
|
||||||
self.window_mode = !self.window_mode;
|
self.window_mode = !self.window_mode;
|
||||||
cx.notify()
|
cx.notify()
|
||||||
}
|
}
|
||||||
|
fn on_action_info(&mut self, info: &Info, cx: &mut ViewContext<Self>) {
|
||||||
|
self.message = format!("You have clicked info: {}", info.0);
|
||||||
|
cx.notify()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FocusableView for PopupStory {
|
impl FocusableView for PopupStory {
|
||||||
|
|
@ -129,6 +138,7 @@ impl Render for PopupStory {
|
||||||
.on_action(cx.listener(Self::on_paste))
|
.on_action(cx.listener(Self::on_paste))
|
||||||
.on_action(cx.listener(Self::on_search_all))
|
.on_action(cx.listener(Self::on_search_all))
|
||||||
.on_action(cx.listener(Self::on_toggle_window_mode))
|
.on_action(cx.listener(Self::on_toggle_window_mode))
|
||||||
|
.on_action(cx.listener(Self::on_action_info))
|
||||||
.p_4()
|
.p_4()
|
||||||
.mb_5()
|
.mb_5()
|
||||||
.size_full()
|
.size_full()
|
||||||
|
|
@ -137,12 +147,24 @@ impl Render for PopupStory {
|
||||||
cx.focus(&this.focus_handle);
|
cx.focus(&this.focus_handle);
|
||||||
}))
|
}))
|
||||||
.context_menu({
|
.context_menu({
|
||||||
move |this, _cx| {
|
move |this, cx| {
|
||||||
this.menu("Cut", Box::new(Cut))
|
this.menu("Cut", Box::new(Cut))
|
||||||
.menu("Copy", Box::new(Copy))
|
.menu("Copy", Box::new(Copy))
|
||||||
.menu("Paste", Box::new(Paste))
|
.menu("Paste", Box::new(Paste))
|
||||||
.separator()
|
.separator()
|
||||||
.menu("About", Box::new(SearchAll))
|
.submenu("Settings", cx, move |menu, _| {
|
||||||
|
menu.menu_with_check(
|
||||||
|
"Toggle Window Mode",
|
||||||
|
window_mode,
|
||||||
|
Box::new(ToggleWindowMode),
|
||||||
|
)
|
||||||
|
.separator()
|
||||||
|
.menu("Info 0", Box::new(Info(0)))
|
||||||
|
.menu("Item 1", Box::new(Info(1)))
|
||||||
|
.menu("Item 2", Box::new(Info(2)))
|
||||||
|
})
|
||||||
|
.separator()
|
||||||
|
.menu("Search All", Box::new(SearchAll))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.gap_6()
|
.gap_6()
|
||||||
|
|
@ -161,7 +183,6 @@ impl Render for PopupStory {
|
||||||
.child(
|
.child(
|
||||||
v_flex().gap_4().child(
|
v_flex().gap_4().child(
|
||||||
Popover::new("info-top-left")
|
Popover::new("info-top-left")
|
||||||
.when(window_mode, |this| this.window_mode())
|
|
||||||
.trigger(Button::new("info-top-left", cx).label("Top Left"))
|
.trigger(Button::new("info-top-left", cx).label("Top Left"))
|
||||||
.content(|cx| {
|
.content(|cx| {
|
||||||
PopoverContent::new(cx, |cx| {
|
PopoverContent::new(cx, |cx| {
|
||||||
|
|
@ -182,7 +203,6 @@ impl Render for PopupStory {
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Popover::new("info-top-right")
|
Popover::new("info-top-right")
|
||||||
.when(window_mode, |this| this.window_mode())
|
|
||||||
.anchor(AnchorCorner::TopRight)
|
.anchor(AnchorCorner::TopRight)
|
||||||
.trigger(Button::new("info-top-right", cx).label("Top Right"))
|
.trigger(Button::new("info-top-right", cx).label("Top Right"))
|
||||||
.content(|cx| {
|
.content(|cx| {
|
||||||
|
|
@ -209,7 +229,7 @@ impl Render for PopupStory {
|
||||||
.child(
|
.child(
|
||||||
Button::new("popup-menu-1", cx)
|
Button::new("popup-menu-1", cx)
|
||||||
.icon(IconName::Ellipsis)
|
.icon(IconName::Ellipsis)
|
||||||
.popup_menu(move |this, _| {
|
.popup_menu(move |this, cx| {
|
||||||
this.menu("Copy", Box::new(Copy))
|
this.menu("Copy", Box::new(Copy))
|
||||||
.menu("Cut", Box::new(Cut))
|
.menu("Cut", Box::new(Cut))
|
||||||
.menu("Paste", Box::new(Paste))
|
.menu("Paste", Box::new(Paste))
|
||||||
|
|
@ -222,11 +242,16 @@ impl Render for PopupStory {
|
||||||
Box::new(ToggleWindowMode),
|
Box::new(ToggleWindowMode),
|
||||||
)
|
)
|
||||||
.separator()
|
.separator()
|
||||||
.link_with_icon(
|
.submenu("Links", cx, |menu, _| {
|
||||||
"GitHub Repository",
|
menu.link_with_icon(
|
||||||
IconName::GitHub,
|
"GitHub Repository",
|
||||||
"https://github.com/huacnlee/gpui-component",
|
IconName::GitHub,
|
||||||
)
|
"https://github.com/huacnlee/gpui-component",
|
||||||
|
)
|
||||||
|
.separator()
|
||||||
|
.link("GPUI", "https://gpui.rs")
|
||||||
|
.link("Zed", "https://zed.dev")
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(self.message.clone()),
|
.child(self.message.clone()),
|
||||||
|
|
@ -239,7 +264,6 @@ impl Render for PopupStory {
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.child(
|
.child(
|
||||||
Popover::new("info-bottom-left")
|
Popover::new("info-bottom-left")
|
||||||
.when(window_mode, |this| this.window_mode())
|
|
||||||
.anchor(AnchorCorner::BottomLeft)
|
.anchor(AnchorCorner::BottomLeft)
|
||||||
.trigger(
|
.trigger(
|
||||||
Button::new("pop", cx).label("Popup with Form").w(px(300.)),
|
Button::new("pop", cx).label("Popup with Form").w(px(300.)),
|
||||||
|
|
@ -248,7 +272,6 @@ impl Render for PopupStory {
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Popover::new("info-bottom-right")
|
Popover::new("info-bottom-right")
|
||||||
.when(window_mode, |this| this.window_mode())
|
|
||||||
.anchor(AnchorCorner::BottomRight)
|
.anchor(AnchorCorner::BottomRight)
|
||||||
.mouse_button(MouseButton::Right)
|
.mouse_button(MouseButton::Right)
|
||||||
.trigger(
|
.trigger(
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,18 @@ use std::{cell::RefCell, rc::Rc};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
anchored, deferred, div, prelude::FluentBuilder, relative, AnchorCorner, AnyElement,
|
anchored, deferred, div, prelude::FluentBuilder, relative, AnchorCorner, AnyElement,
|
||||||
AppContext, DismissEvent, DispatchPhase, Element, ElementId, Focusable, GlobalElementId,
|
AppContext, DismissEvent, DispatchPhase, Element, ElementId, Focusable, GlobalElementId,
|
||||||
InteractiveElement, IntoElement, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
|
IntoElement, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Position, Stateful,
|
||||||
Position, Stateful, Style, Styled as _, View, WindowContext,
|
Style, View, ViewContext, WindowContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{popup_menu::PopupMenu, theme::ActiveTheme};
|
use crate::popup_menu::PopupMenu;
|
||||||
|
|
||||||
pub fn init(_cx: &mut AppContext) {}
|
pub fn init(_cx: &mut AppContext) {}
|
||||||
|
|
||||||
pub trait ContextMenuExt: ParentElement + Sized {
|
pub trait ContextMenuExt: ParentElement + Sized {
|
||||||
fn context_menu(
|
fn context_menu(
|
||||||
self,
|
self,
|
||||||
f: impl Fn(PopupMenu, &mut WindowContext) -> PopupMenu + 'static,
|
f: impl Fn(PopupMenu, &mut ViewContext<PopupMenu>) -> PopupMenu + 'static,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.child(ContextMenu::new("context_menu").menu(f))
|
self.child(ContextMenu::new("context_menu").menu(f))
|
||||||
}
|
}
|
||||||
|
|
@ -25,7 +25,7 @@ impl<E> ContextMenuExt for Focusable<E> where E: ParentElement {}
|
||||||
|
|
||||||
pub struct ContextMenu {
|
pub struct ContextMenu {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
menu: Option<Box<dyn Fn(PopupMenu, &mut WindowContext) -> PopupMenu + 'static>>,
|
menu: Option<Box<dyn Fn(PopupMenu, &mut ViewContext<PopupMenu>) -> PopupMenu + 'static>>,
|
||||||
anchor: AnchorCorner,
|
anchor: AnchorCorner,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,7 +41,7 @@ impl ContextMenu {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn menu<F>(mut self, builder: F) -> Self
|
pub fn menu<F>(mut self, builder: F) -> Self
|
||||||
where
|
where
|
||||||
F: Fn(PopupMenu, &mut WindowContext) -> PopupMenu + 'static,
|
F: Fn(PopupMenu, &mut ViewContext<PopupMenu>) -> PopupMenu + 'static,
|
||||||
{
|
{
|
||||||
self.menu = Some(Box::new(builder));
|
self.menu = Some(Box::new(builder));
|
||||||
self
|
self
|
||||||
|
|
@ -126,19 +126,7 @@ impl Element for ContextMenu {
|
||||||
// Focus the menu, so that can be handle the action.
|
// Focus the menu, so that can be handle the action.
|
||||||
menu.focus_handle(cx).focus(cx);
|
menu.focus_handle(cx).focus(cx);
|
||||||
|
|
||||||
this.child(
|
this.child(div().child(menu.clone()))
|
||||||
div()
|
|
||||||
.bg(cx.theme().popover)
|
|
||||||
.border_1()
|
|
||||||
.border_color(cx.theme().border)
|
|
||||||
.shadow_lg()
|
|
||||||
.rounded_lg()
|
|
||||||
.child(menu)
|
|
||||||
.on_mouse_down_out(move |_, cx| {
|
|
||||||
*open.borrow_mut() = false;
|
|
||||||
cx.refresh();
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.with_priority(1)
|
.with_priority(1)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, prelude::FluentBuilder as _, AnyElement, ClickEvent, Div, ElementId, InteractiveElement,
|
div, prelude::FluentBuilder as _, AnyElement, ClickEvent, Div, ElementId, InteractiveElement,
|
||||||
IntoElement, MouseButton, MouseDownEvent, ParentElement, RenderOnce, Stateful,
|
IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, RenderOnce,
|
||||||
StatefulInteractiveElement as _, Styled, WindowContext,
|
SharedString, Stateful, StatefulInteractiveElement as _, Styled, WindowContext,
|
||||||
};
|
};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
|
|
@ -14,7 +14,9 @@ pub struct ListItem {
|
||||||
selected: bool,
|
selected: bool,
|
||||||
confirmed: bool,
|
confirmed: bool,
|
||||||
check_icon: Option<Icon>,
|
check_icon: Option<Icon>,
|
||||||
|
group_id: Option<SharedString>,
|
||||||
on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||||
|
on_mouse_enter: Option<Box<dyn Fn(&MouseMoveEvent, &mut WindowContext) + 'static>>,
|
||||||
on_secondary_mouse_down: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
|
on_secondary_mouse_down: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
|
||||||
suffix: Option<Box<dyn Fn(&mut WindowContext) -> AnyElement + 'static>>,
|
suffix: Option<Box<dyn Fn(&mut WindowContext) -> AnyElement + 'static>>,
|
||||||
children: SmallVec<[AnyElement; 2]>,
|
children: SmallVec<[AnyElement; 2]>,
|
||||||
|
|
@ -29,12 +31,20 @@ impl ListItem {
|
||||||
confirmed: false,
|
confirmed: false,
|
||||||
on_click: None,
|
on_click: None,
|
||||||
on_secondary_mouse_down: None,
|
on_secondary_mouse_down: None,
|
||||||
|
on_mouse_enter: None,
|
||||||
check_icon: None,
|
check_icon: None,
|
||||||
suffix: None,
|
suffix: None,
|
||||||
|
group_id: None,
|
||||||
children: SmallVec::new(),
|
children: SmallVec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set group_id
|
||||||
|
pub fn group(mut self, group_id: impl Into<SharedString>) -> Self {
|
||||||
|
self.group_id = Some(group_id.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set to show check icon, default is None.
|
/// Set to show check icon, default is None.
|
||||||
pub fn check_icon(mut self, icon: IconName) -> Self {
|
pub fn check_icon(mut self, icon: IconName) -> Self {
|
||||||
self.check_icon = Some(Icon::new(icon));
|
self.check_icon = Some(Icon::new(icon));
|
||||||
|
|
@ -80,6 +90,14 @@ impl ListItem {
|
||||||
self.on_secondary_mouse_down = Some(Box::new(handler));
|
self.on_secondary_mouse_down = Some(Box::new(handler));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn on_mouse_enter(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(&MouseMoveEvent, &mut WindowContext) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_mouse_enter = Some(Box::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Disableable for ListItem {
|
impl Disableable for ListItem {
|
||||||
|
|
@ -113,6 +131,7 @@ impl RenderOnce for ListItem {
|
||||||
let is_active = self.selected || self.confirmed;
|
let is_active = self.selected || self.confirmed;
|
||||||
|
|
||||||
self.base
|
self.base
|
||||||
|
.when_some(self.group_id, |this, group_id| this.group(group_id))
|
||||||
.text_color(cx.theme().foreground)
|
.text_color(cx.theme().foreground)
|
||||||
.relative()
|
.relative()
|
||||||
.items_center()
|
.items_center()
|
||||||
|
|
@ -136,13 +155,21 @@ impl RenderOnce for ListItem {
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
// Mouse enter
|
||||||
|
.when_some(self.on_mouse_enter, |this, on_mouse_enter| {
|
||||||
|
if !self.disabled {
|
||||||
|
this.on_mouse_move(move |ev, cx| (on_mouse_enter)(ev, cx))
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
})
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.w_full()
|
.w_full()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.gap_x_1()
|
.gap_x_1()
|
||||||
.child(div().w_full().overflow_hidden().children(self.children))
|
.child(div().w_full().children(self.children))
|
||||||
.when_some(self.check_icon, |this, icon| {
|
.when_some(self.check_icon, |this, icon| {
|
||||||
this.child(
|
this.child(
|
||||||
div().w_5().items_center().justify_center().when(
|
div().w_5().items_center().justify_center().when(
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,17 @@
|
||||||
use anyhow::Result;
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, anchored, deferred, div, point, prelude::FluentBuilder as _, px, size, AnchorCorner,
|
actions, anchored, deferred, div, prelude::FluentBuilder as _, AnchorCorner, AnyElement,
|
||||||
AnyElement, AppContext, Bounds, Context, DismissEvent, DispatchPhase, Element, ElementId,
|
AppContext, Bounds, DismissEvent, DispatchPhase, Element, ElementId, EventEmitter, FocusHandle,
|
||||||
EventEmitter, FocusHandle, FocusableView, Global, GlobalElementId, Hitbox,
|
FocusableView, GlobalElementId, Hitbox, InteractiveElement as _, IntoElement, LayoutId,
|
||||||
InteractiveElement as _, IntoElement, LayoutId, ManagedView, MouseButton, MouseDownEvent,
|
ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render, Style, Styled,
|
||||||
ParentElement, Pixels, Point, Render, Style, Styled, Subscription, View, ViewContext,
|
View, ViewContext, VisualContext, WindowContext,
|
||||||
VisualContext, WindowBackgroundAppearance, WindowContext, WindowId, WindowOptions,
|
|
||||||
};
|
};
|
||||||
use std::{cell::RefCell, rc::Rc};
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
use crate::{theme::ActiveTheme, Selectable};
|
use crate::{Selectable, StyledExt as _};
|
||||||
|
|
||||||
actions!(popover, [Open, Dismiss]);
|
actions!(popover, [Open, Dismiss]);
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(_cx: &mut AppContext) {}
|
||||||
cx.set_global(PopoverWindowState { window_id: None });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct PopoverContent {
|
pub struct PopoverContent {
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
|
|
@ -51,19 +47,13 @@ impl Render for PopoverContent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
enum PopupMode {
|
|
||||||
View,
|
|
||||||
Window,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Popover<M: ManagedView> {
|
pub struct Popover<M: ManagedView> {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
anchor: AnchorCorner,
|
anchor: AnchorCorner,
|
||||||
trigger: Option<Box<dyn FnOnce(bool, &WindowContext) -> AnyElement + 'static>>,
|
trigger: Option<Box<dyn FnOnce(bool, &WindowContext) -> AnyElement + 'static>>,
|
||||||
content: Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>,
|
content: Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>,
|
||||||
mouse_button: MouseButton,
|
mouse_button: MouseButton,
|
||||||
mode: PopupMode,
|
no_style: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<M> Popover<M>
|
impl<M> Popover<M>
|
||||||
|
|
@ -78,16 +68,10 @@ where
|
||||||
trigger: None,
|
trigger: None,
|
||||||
content: None,
|
content: None,
|
||||||
mouse_button: MouseButton::Left,
|
mouse_button: MouseButton::Left,
|
||||||
mode: PopupMode::View,
|
no_style: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set Popover to use Window mode
|
|
||||||
pub fn window_mode(mut self) -> Self {
|
|
||||||
self.mode = PopupMode::Window;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
|
pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
|
||||||
self.anchor = anchor;
|
self.anchor = anchor;
|
||||||
self
|
self
|
||||||
|
|
@ -120,6 +104,17 @@ where
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set whether the popover no style, default is `false`.
|
||||||
|
///
|
||||||
|
/// If no style:
|
||||||
|
///
|
||||||
|
/// - The popover will not have a bg, border, shadow, or padding.
|
||||||
|
/// - The click out of the popover will not dismiss it.
|
||||||
|
pub fn no_style(mut self) -> Self {
|
||||||
|
self.no_style = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
fn render_trigger(&mut self, is_open: bool, cx: &mut WindowContext) -> impl IntoElement {
|
fn render_trigger(&mut self, is_open: bool, cx: &mut WindowContext) -> impl IntoElement {
|
||||||
let base = div().id("popover-trigger");
|
let base = div().id("popover-trigger");
|
||||||
|
|
||||||
|
|
@ -197,7 +192,6 @@ pub struct PrepaintState {
|
||||||
hitbox: Hitbox,
|
hitbox: Hitbox,
|
||||||
/// Trigger bounds for limit a rect to handle mouse click.
|
/// Trigger bounds for limit a rect to handle mouse click.
|
||||||
trigger_bounds: Option<Bounds<Pixels>>,
|
trigger_bounds: Option<Bounds<Pixels>>,
|
||||||
popover_bounds: Option<Bounds<Pixels>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<M: ManagedView> Element for Popover<M> {
|
impl<M: ManagedView> Element for Popover<M> {
|
||||||
|
|
@ -213,8 +207,6 @@ impl<M: ManagedView> Element for Popover<M> {
|
||||||
id: Option<&gpui::GlobalElementId>,
|
id: Option<&gpui::GlobalElementId>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
||||||
let mode = self.mode;
|
|
||||||
|
|
||||||
self.with_element_state(id.unwrap(), cx, |view, element_state, cx| {
|
self.with_element_state(id.unwrap(), cx, |view, element_state, cx| {
|
||||||
let mut popover_layout_id = None;
|
let mut popover_layout_id = None;
|
||||||
let mut popover_element = None;
|
let mut popover_element = None;
|
||||||
|
|
@ -228,30 +220,16 @@ impl<M: ManagedView> Element for Popover<M> {
|
||||||
anchored = anchored.position(view.resolved_corner(trigger_bounds));
|
anchored = anchored.position(view.resolved_corner(trigger_bounds));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut element = if mode == PopupMode::Window {
|
let mut element = {
|
||||||
// layout the content view, to let the popover know the size of the content for window size.
|
|
||||||
anchored
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.border_1()
|
|
||||||
.border_color(cx.theme().border)
|
|
||||||
.child(content_view.clone()),
|
|
||||||
)
|
|
||||||
.into_any()
|
|
||||||
} else {
|
|
||||||
let content_view_mut = element_state.content_view.clone();
|
let content_view_mut = element_state.content_view.clone();
|
||||||
let bg_color = cx.theme().popover;
|
|
||||||
let anchor = view.anchor;
|
let anchor = view.anchor;
|
||||||
|
let no_style = view.no_style;
|
||||||
deferred(
|
deferred(
|
||||||
anchored.child(
|
anchored.child(
|
||||||
div()
|
div()
|
||||||
.size_full()
|
.size_full()
|
||||||
.occlude()
|
.occlude()
|
||||||
.border_1()
|
.when(!no_style, |this| this.popover_style(cx))
|
||||||
.border_color(cx.theme().border)
|
|
||||||
.shadow_lg()
|
|
||||||
.rounded_lg()
|
|
||||||
.bg(bg_color)
|
|
||||||
.map(|this| match anchor {
|
.map(|this| match anchor {
|
||||||
AnchorCorner::TopLeft | AnchorCorner::TopRight => this.top_2(),
|
AnchorCorner::TopLeft | AnchorCorner::TopRight => this.top_2(),
|
||||||
AnchorCorner::BottomLeft | AnchorCorner::BottomRight => {
|
AnchorCorner::BottomLeft | AnchorCorner::BottomRight => {
|
||||||
|
|
@ -259,11 +237,13 @@ impl<M: ManagedView> Element for Popover<M> {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.child(content_view.clone())
|
.child(content_view.clone())
|
||||||
.on_mouse_down_out(move |_, cx| {
|
.when(!no_style, |this| {
|
||||||
// Update the element_state.content_view to `None`,
|
this.on_mouse_down_out(move |_, cx| {
|
||||||
// so that the `paint`` method will not paint it.
|
// Update the element_state.content_view to `None`,
|
||||||
*content_view_mut.borrow_mut() = None;
|
// so that the `paint`` method will not paint it.
|
||||||
cx.refresh();
|
*content_view_mut.borrow_mut() = None;
|
||||||
|
cx.refresh();
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -315,7 +295,7 @@ impl<M: ManagedView> Element for Popover<M> {
|
||||||
.map(|id| cx.layout_bounds(id));
|
.map(|id| cx.layout_bounds(id));
|
||||||
|
|
||||||
// Prepare the popover, for get the bounds of it for open window size.
|
// Prepare the popover, for get the bounds of it for open window size.
|
||||||
let popover_bounds = request_layout
|
let _ = request_layout
|
||||||
.popover_layout_id
|
.popover_layout_id
|
||||||
.map(|id| cx.layout_bounds(id));
|
.map(|id| cx.layout_bounds(id));
|
||||||
|
|
||||||
|
|
@ -323,7 +303,6 @@ impl<M: ManagedView> Element for Popover<M> {
|
||||||
|
|
||||||
PrepaintState {
|
PrepaintState {
|
||||||
trigger_bounds,
|
trigger_bounds,
|
||||||
popover_bounds,
|
|
||||||
hitbox,
|
hitbox,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -336,8 +315,6 @@ impl<M: ManagedView> Element for Popover<M> {
|
||||||
prepaint: &mut Self::PrepaintState,
|
prepaint: &mut Self::PrepaintState,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) {
|
) {
|
||||||
let anchor = self.anchor;
|
|
||||||
let mode = self.mode;
|
|
||||||
self.with_element_state(id.unwrap(), cx, |this, element_state, cx| {
|
self.with_element_state(id.unwrap(), cx, |this, element_state, cx| {
|
||||||
element_state.trigger_bounds = prepaint.trigger_bounds;
|
element_state.trigger_bounds = prepaint.trigger_bounds;
|
||||||
|
|
||||||
|
|
@ -345,27 +322,9 @@ impl<M: ManagedView> Element for Popover<M> {
|
||||||
element.paint(cx);
|
element.paint(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
if mode == PopupMode::Window {
|
if let Some(mut element) = request_layout.popover_element.take() {
|
||||||
if let Some(content_view) = element_state.content_view.take() {
|
element.paint(cx);
|
||||||
let popover_bounds = prepaint.popover_bounds.unwrap();
|
return;
|
||||||
let trigger_bounds = prepaint.trigger_bounds.unwrap();
|
|
||||||
|
|
||||||
PopoverWindow::open_popover(
|
|
||||||
content_view,
|
|
||||||
trigger_bounds,
|
|
||||||
popover_bounds,
|
|
||||||
anchor,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.expect("BUG: failed to open popover window.");
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if let Some(mut element) = request_layout.popover_element.take() {
|
|
||||||
element.paint(cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// When mouse click down in the trigger bounds, open the popover.
|
// When mouse click down in the trigger bounds, open the popover.
|
||||||
|
|
@ -387,6 +346,7 @@ impl<M: ManagedView> Element for Popover<M> {
|
||||||
let old_content_view1 = old_content_view.clone();
|
let old_content_view1 = old_content_view.clone();
|
||||||
|
|
||||||
let previous_focus_handle = cx.focused();
|
let previous_focus_handle = cx.focused();
|
||||||
|
|
||||||
cx.subscribe(&new_content_view, move |modal, _: &DismissEvent, cx| {
|
cx.subscribe(&new_content_view, move |modal, _: &DismissEvent, cx| {
|
||||||
if modal.focus_handle(cx).contains_focused(cx) {
|
if modal.focus_handle(cx).contains_focused(cx) {
|
||||||
if let Some(previous_focus_handle) = previous_focus_handle.as_ref() {
|
if let Some(previous_focus_handle) = previous_focus_handle.as_ref() {
|
||||||
|
|
@ -394,7 +354,6 @@ impl<M: ManagedView> Element for Popover<M> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*old_content_view1.borrow_mut() = None;
|
*old_content_view1.borrow_mut() = None;
|
||||||
close_popover(cx);
|
|
||||||
|
|
||||||
cx.refresh();
|
cx.refresh();
|
||||||
})
|
})
|
||||||
|
|
@ -405,242 +364,6 @@ impl<M: ManagedView> Element for Popover<M> {
|
||||||
cx.refresh();
|
cx.refresh();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Click parent window to dimiss popover
|
|
||||||
if mode == PopupMode::Window {
|
|
||||||
let content_view = element_state.content_view.clone();
|
|
||||||
cx.on_mouse_event(move |_: &MouseDownEvent, _, cx| {
|
|
||||||
*content_view.borrow_mut() = None;
|
|
||||||
close_popover(cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PopoverWindowState {
|
|
||||||
window_id: Option<WindowId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Global for PopoverWindowState {}
|
|
||||||
|
|
||||||
impl PopoverWindowState {
|
|
||||||
fn window_id(cx: &AppContext) -> Option<WindowId> {
|
|
||||||
cx.try_global::<Self>().and_then(|state| state.window_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_window_id(window_id: WindowId, cx: &mut WindowContext) {
|
|
||||||
cx.set_global(PopoverWindowState {
|
|
||||||
window_id: Some(window_id),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn close_window(cx: &mut AppContext) {
|
|
||||||
if let Some(window) = cx
|
|
||||||
.windows()
|
|
||||||
.into_iter()
|
|
||||||
.find(|window| Some(window.window_id()) == PopoverWindowState::window_id(cx))
|
|
||||||
{
|
|
||||||
cx.update_window(window, |_, cx| {
|
|
||||||
cx.remove_window();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct PopoverWindow<M: ManagedView> {
|
|
||||||
view: View<M>,
|
|
||||||
anchor: AnchorCorner,
|
|
||||||
close_when_deactivate: bool,
|
|
||||||
_subscriptions: Vec<Subscription>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn close_popover(cx: &mut AppContext) {
|
|
||||||
PopoverWindowState::close_window(cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<M> PopoverWindow<M>
|
|
||||||
where
|
|
||||||
M: ManagedView,
|
|
||||||
{
|
|
||||||
pub fn open_popover(
|
|
||||||
view: View<M>,
|
|
||||||
trigger_bounds: Bounds<Pixels>,
|
|
||||||
bounds: Bounds<Pixels>,
|
|
||||||
anchor: AnchorCorner,
|
|
||||||
cx: &mut WindowContext,
|
|
||||||
) -> Result<()> {
|
|
||||||
// Every open_popover will close the existing one
|
|
||||||
PopoverWindowState::close_window(cx);
|
|
||||||
|
|
||||||
let display = cx.display();
|
|
||||||
let window_bounds = cx.bounds();
|
|
||||||
|
|
||||||
// TODO: avoid out of the screen bounds
|
|
||||||
|
|
||||||
let border_bounds = if cfg!(target_os = "windows") {
|
|
||||||
Bounds {
|
|
||||||
origin: point(px(-8.0), px(5.0)),
|
|
||||||
size: size(px(16.0), px(8.0)),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Bounds {
|
|
||||||
origin: point(px(-8.0), px(0.0)),
|
|
||||||
size: size(px(20.0), px(20.0)),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let trigger_screen_bounds = Bounds {
|
|
||||||
origin: window_bounds.origin + trigger_bounds.origin + border_bounds.origin,
|
|
||||||
size: trigger_bounds.size,
|
|
||||||
};
|
|
||||||
|
|
||||||
let popover_offset = px(2.);
|
|
||||||
let popover_origin = match anchor {
|
|
||||||
AnchorCorner::TopLeft => {
|
|
||||||
trigger_screen_bounds.lower_left() + point(px(0.), popover_offset)
|
|
||||||
}
|
|
||||||
AnchorCorner::TopRight => {
|
|
||||||
trigger_screen_bounds.lower_right() + point(-bounds.size.width, popover_offset)
|
|
||||||
}
|
|
||||||
AnchorCorner::BottomLeft => {
|
|
||||||
trigger_screen_bounds.origin
|
|
||||||
- point(
|
|
||||||
px(0.0),
|
|
||||||
bounds.size.height + border_bounds.size.height + popover_offset,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
AnchorCorner::BottomRight => {
|
|
||||||
trigger_screen_bounds.upper_right()
|
|
||||||
- point(
|
|
||||||
bounds.size.width,
|
|
||||||
bounds.size.height + border_bounds.size.height + popover_offset,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let bounds = Bounds {
|
|
||||||
origin: popover_origin,
|
|
||||||
size: size(
|
|
||||||
bounds.size.width + border_bounds.size.width,
|
|
||||||
bounds.size.height + border_bounds.size.height,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
let view = view.clone();
|
|
||||||
cx.spawn(|mut cx| async move {
|
|
||||||
let window = cx
|
|
||||||
.open_window(
|
|
||||||
WindowOptions {
|
|
||||||
titlebar: None,
|
|
||||||
window_bounds: Some(gpui::WindowBounds::Windowed(bounds)),
|
|
||||||
window_background: WindowBackgroundAppearance::Transparent,
|
|
||||||
// NOTE: on Windows in currently must use PopUp kind, otherwise the window will be sizeable.
|
|
||||||
// And the PopUp kind can fast open.
|
|
||||||
kind: gpui::WindowKind::PopUp,
|
|
||||||
is_movable: false,
|
|
||||||
focus: true,
|
|
||||||
show: true,
|
|
||||||
display_id: display.map(|d| d.id()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
|cx| {
|
|
||||||
let mut _subscriptions = Vec::new();
|
|
||||||
|
|
||||||
let view = cx.new_view(|cx| {
|
|
||||||
// Listen to window diactivation to close window
|
|
||||||
_subscriptions.push(
|
|
||||||
cx.observe_window_activation(Self::window_activation_changed),
|
|
||||||
);
|
|
||||||
|
|
||||||
PopoverWindow {
|
|
||||||
view,
|
|
||||||
anchor,
|
|
||||||
close_when_deactivate: true,
|
|
||||||
_subscriptions,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
view
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("BUG: faild to create a new window.");
|
|
||||||
|
|
||||||
cx.update(|cx| {
|
|
||||||
PopoverWindowState::set_window_id(window.window_id(), cx);
|
|
||||||
})
|
|
||||||
.expect("BUG: failed to set window id.")
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<M> PopoverWindow<M>
|
|
||||||
where
|
|
||||||
M: ManagedView,
|
|
||||||
{
|
|
||||||
fn window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
|
|
||||||
if self.close_when_deactivate {
|
|
||||||
if !cx.is_window_active() {
|
|
||||||
self.dismiss(cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
|
|
||||||
cx.remove_window();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<M> FocusableView for PopoverWindow<M>
|
|
||||||
where
|
|
||||||
M: ManagedView,
|
|
||||||
{
|
|
||||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
|
||||||
self.view.focus_handle(cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<M> Render for PopoverWindow<M>
|
|
||||||
where
|
|
||||||
M: ManagedView,
|
|
||||||
{
|
|
||||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
|
|
||||||
let is_windows = cfg!(target_os = "windows");
|
|
||||||
|
|
||||||
div()
|
|
||||||
.id("PopoverWindow")
|
|
||||||
.size_full()
|
|
||||||
.when(!is_windows, |this| this.p_2())
|
|
||||||
.when(is_windows, |this| this.bg(cx.theme().popover))
|
|
||||||
.text_color(cx.theme().popover_foreground)
|
|
||||||
// Leave margin for show window shadow
|
|
||||||
.map(|d| match self.anchor {
|
|
||||||
AnchorCorner::TopLeft | AnchorCorner::TopRight => d.mt_8(),
|
|
||||||
AnchorCorner::BottomLeft | AnchorCorner::BottomRight => d.mb_8(),
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.when(!is_windows, |this| {
|
|
||||||
this.bg(cx.theme().popover)
|
|
||||||
.border_1()
|
|
||||||
.border_color(cx.theme().border)
|
|
||||||
.shadow_lg()
|
|
||||||
.rounded_lg()
|
|
||||||
})
|
|
||||||
.bg(cx.theme().popover)
|
|
||||||
.child(self.view.clone())
|
|
||||||
.on_mouse_down(
|
|
||||||
gpui::MouseButton::Left,
|
|
||||||
cx.listener(|_, _, cx| {
|
|
||||||
cx.stop_propagation();
|
|
||||||
|
|
||||||
PopoverWindowState::close_window(cx);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,9 @@ use gpui::{
|
||||||
FocusHandle, InteractiveElement, IntoElement, KeyBinding, ParentElement, Pixels, Render,
|
FocusHandle, InteractiveElement, IntoElement, KeyBinding, ParentElement, Pixels, Render,
|
||||||
SharedString, Styled as _, View, ViewContext, VisualContext as _, WindowContext,
|
SharedString, Styled as _, View, ViewContext, VisualContext as _, WindowContext,
|
||||||
};
|
};
|
||||||
use gpui::{rems, FocusableView};
|
use gpui::{anchored, canvas, rems, AnchorCorner, Bounds, FocusableView, WeakView};
|
||||||
|
|
||||||
|
use crate::StyledExt;
|
||||||
use crate::{
|
use crate::{
|
||||||
button::Button, h_flex, list::ListItem, popover::Popover, theme::ActiveTheme, v_flex, Icon,
|
button::Button, h_flex, list::ListItem, popover::Popover, theme::ActiveTheme, v_flex, Icon,
|
||||||
IconName, Selectable, Sizable as _,
|
IconName, Selectable, Sizable as _,
|
||||||
|
|
@ -28,9 +29,10 @@ pub fn init(cx: &mut AppContext) {
|
||||||
pub trait PopupMenuExt: Selectable + IntoElement + 'static {
|
pub trait PopupMenuExt: Selectable + IntoElement + 'static {
|
||||||
fn popup_menu(
|
fn popup_menu(
|
||||||
self,
|
self,
|
||||||
f: impl Fn(PopupMenu, &mut WindowContext) -> PopupMenu + 'static,
|
f: impl Fn(PopupMenu, &mut ViewContext<PopupMenu>) -> PopupMenu + 'static,
|
||||||
) -> Popover<PopupMenu> {
|
) -> Popover<PopupMenu> {
|
||||||
Popover::new("popup-menu")
|
Popover::new("popup-menu")
|
||||||
|
.no_style()
|
||||||
.trigger(self)
|
.trigger(self)
|
||||||
.content(move |cx| PopupMenu::build(cx, |menu, cx| f(menu, cx)))
|
.content(move |cx| PopupMenu::build(cx, |menu, cx| f(menu, cx)))
|
||||||
}
|
}
|
||||||
|
|
@ -45,6 +47,11 @@ enum PopupMenuItem {
|
||||||
action: Option<Box<dyn Action>>,
|
action: Option<Box<dyn Action>>,
|
||||||
handler: Rc<dyn Fn(&mut WindowContext)>,
|
handler: Rc<dyn Fn(&mut WindowContext)>,
|
||||||
},
|
},
|
||||||
|
Submenu {
|
||||||
|
icon: Option<Icon>,
|
||||||
|
label: SharedString,
|
||||||
|
menu: View<PopupMenu>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PopupMenuItem {
|
impl PopupMenuItem {
|
||||||
|
|
@ -58,19 +65,23 @@ impl PopupMenuItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PopupMenu {
|
pub struct PopupMenu {
|
||||||
|
/// The parent menu of this menu, if this is a submenu
|
||||||
|
parent_menu: Option<WeakView<Self>>,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
menu_items: Vec<PopupMenuItem>,
|
menu_items: Vec<PopupMenuItem>,
|
||||||
has_icon: bool,
|
has_icon: bool,
|
||||||
selected_index: Option<usize>,
|
selected_index: Option<usize>,
|
||||||
min_width: Pixels,
|
min_width: Pixels,
|
||||||
max_width: Pixels,
|
max_width: Pixels,
|
||||||
|
hovered_menu_ix: Option<usize>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
_subscriptions: [gpui::Subscription; 1],
|
_subscriptions: [gpui::Subscription; 1],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PopupMenu {
|
impl PopupMenu {
|
||||||
pub fn build(
|
pub fn build(
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
f: impl FnOnce(Self, &mut WindowContext) -> Self,
|
f: impl FnOnce(Self, &mut ViewContext<PopupMenu>) -> Self,
|
||||||
) -> View<Self> {
|
) -> View<Self> {
|
||||||
cx.new_view(|cx| {
|
cx.new_view(|cx| {
|
||||||
let focus_handle = cx.focus_handle();
|
let focus_handle = cx.focus_handle();
|
||||||
|
|
@ -80,11 +91,14 @@ impl PopupMenu {
|
||||||
|
|
||||||
let menu = Self {
|
let menu = Self {
|
||||||
focus_handle,
|
focus_handle,
|
||||||
|
parent_menu: None,
|
||||||
menu_items: Vec::new(),
|
menu_items: Vec::new(),
|
||||||
selected_index: None,
|
selected_index: None,
|
||||||
min_width: px(120.),
|
min_width: px(120.),
|
||||||
max_width: px(500.),
|
max_width: px(500.),
|
||||||
has_icon: false,
|
has_icon: false,
|
||||||
|
hovered_menu_ix: None,
|
||||||
|
bounds: Bounds::default(),
|
||||||
_subscriptions: [_on_blur_subscription],
|
_subscriptions: [_on_blur_subscription],
|
||||||
};
|
};
|
||||||
cx.refresh();
|
cx.refresh();
|
||||||
|
|
@ -138,6 +152,7 @@ impl PopupMenu {
|
||||||
});
|
});
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add Menu Item with Icon
|
/// Add Menu Item with Icon
|
||||||
pub fn menu_with_icon(
|
pub fn menu_with_icon(
|
||||||
mut self,
|
mut self,
|
||||||
|
|
@ -193,6 +208,50 @@ impl PopupMenu {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn submenu(
|
||||||
|
self,
|
||||||
|
label: impl Into<SharedString>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
f: impl Fn(PopupMenu, &mut ViewContext<PopupMenu>) -> PopupMenu + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.submenu_with_icon(None, label, cx, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a Submenu item with icon
|
||||||
|
pub fn submenu_with_icon(
|
||||||
|
mut self,
|
||||||
|
icon: Option<Icon>,
|
||||||
|
label: impl Into<SharedString>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
f: impl Fn(PopupMenu, &mut ViewContext<PopupMenu>) -> PopupMenu + 'static,
|
||||||
|
) -> Self {
|
||||||
|
let submenu = PopupMenu::build(cx, f);
|
||||||
|
let parent_menu = cx.view().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<View<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
|
||||||
|
}
|
||||||
|
|
||||||
fn clickable_menu_items(&self) -> impl Iterator<Item = (usize, &PopupMenuItem)> {
|
fn clickable_menu_items(&self) -> impl Iterator<Item = (usize, &PopupMenuItem)> {
|
||||||
self.menu_items
|
self.menu_items
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -204,7 +263,7 @@ impl PopupMenu {
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
cx.prevent_default();
|
cx.prevent_default();
|
||||||
self.selected_index = Some(ix);
|
self.selected_index = Some(ix);
|
||||||
self.confirm(&Confirm, cx)
|
self.confirm(&Confirm, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||||
|
|
@ -249,7 +308,18 @@ impl PopupMenu {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
|
fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
|
||||||
|
if self.active_submenu().is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
cx.emit(DismissEvent);
|
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, cx);
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_keybinding(
|
fn render_keybinding(
|
||||||
|
|
@ -271,25 +341,50 @@ impl PopupMenu {
|
||||||
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_icon(
|
||||||
|
has_icon: bool,
|
||||||
|
icon: Option<Icon>,
|
||||||
|
_: &ViewContext<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().clone())
|
||||||
|
} else {
|
||||||
|
this.children(icon_placeholder.clone())
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(icon)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FluentBuilder for PopupMenu {}
|
impl FluentBuilder for PopupMenu {}
|
||||||
impl EventEmitter<DismissEvent> for PopupMenu {}
|
impl EventEmitter<DismissEvent> for PopupMenu {}
|
||||||
impl FocusableView for PopupMenu {
|
impl FocusableView for PopupMenu {
|
||||||
fn focus_handle(&self, _cx: &gpui::AppContext) -> FocusHandle {
|
fn focus_handle(&self, _: &gpui::AppContext) -> FocusHandle {
|
||||||
self.focus_handle.clone()
|
self.focus_handle.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for PopupMenu {
|
impl Render for PopupMenu {
|
||||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement {
|
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement {
|
||||||
let icon_placeholder = if self.has_icon {
|
let view = cx.view().clone();
|
||||||
Some(Icon::empty())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let has_icon = self.menu_items.iter().any(|item| item.has_icon());
|
let has_icon = self.menu_items.iter().any(|item| item.has_icon());
|
||||||
|
let max_width = self.max_width;
|
||||||
|
let bounds = self.bounds;
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.key_context("PopupMenu")
|
.key_context("PopupMenu")
|
||||||
|
|
@ -304,14 +399,38 @@ impl Render for PopupMenu {
|
||||||
.p_1()
|
.p_1()
|
||||||
.gap_y_0p5()
|
.gap_y_0p5()
|
||||||
.min_w(rems(8.))
|
.min_w(rems(8.))
|
||||||
|
.popover_style(cx)
|
||||||
.text_color(cx.theme().popover_foreground)
|
.text_color(cx.theme().popover_foreground)
|
||||||
|
.relative()
|
||||||
|
.child({
|
||||||
|
canvas(
|
||||||
|
move |bounds, cx| view.update(cx, |r, _| r.bounds = bounds),
|
||||||
|
|_, _, _| {},
|
||||||
|
)
|
||||||
|
.absolute()
|
||||||
|
.size_full()
|
||||||
|
})
|
||||||
.children(self.menu_items.iter_mut().enumerate().map(|(ix, item)| {
|
.children(self.menu_items.iter_mut().enumerate().map(|(ix, item)| {
|
||||||
|
let group_id = format!("item:{}", ix);
|
||||||
let this = ListItem::new(("menu-item", ix))
|
let this = ListItem::new(("menu-item", ix))
|
||||||
|
.group(group_id.clone())
|
||||||
.p_0()
|
.p_0()
|
||||||
.on_click(cx.listener(move |this, _, cx| this.on_click(ix, cx)));
|
.relative()
|
||||||
|
.py_1p5()
|
||||||
|
.px_2()
|
||||||
|
.rounded_md()
|
||||||
|
.text_sm()
|
||||||
|
.line_height(rems(1.25))
|
||||||
|
.items_center()
|
||||||
|
.on_mouse_enter(cx.listener(move |this, _, cx| {
|
||||||
|
this.hovered_menu_ix = Some(ix);
|
||||||
|
cx.notify();
|
||||||
|
}));
|
||||||
match item {
|
match item {
|
||||||
PopupMenuItem::Separator => this.disabled(true).child(
|
PopupMenuItem::Separator => this.disabled(true).child(
|
||||||
div()
|
div()
|
||||||
|
.p_0()
|
||||||
|
.rounded_none()
|
||||||
.h(px(1.))
|
.h(px(1.))
|
||||||
.mx_neg_1()
|
.mx_neg_1()
|
||||||
.my_px()
|
.my_px()
|
||||||
|
|
@ -327,37 +446,16 @@ impl Render for PopupMenu {
|
||||||
let action = action.as_ref().map(|action| action.boxed_clone());
|
let action = action.as_ref().map(|action| action.boxed_clone());
|
||||||
let key = Self::render_keybinding(action, cx);
|
let key = Self::render_keybinding(action, cx);
|
||||||
|
|
||||||
this.relative()
|
this.on_click(cx.listener(move |this, _, cx| this.on_click(ix, cx)))
|
||||||
.py_1p5()
|
|
||||||
.px_2()
|
|
||||||
.rounded_md()
|
|
||||||
.text_sm()
|
|
||||||
.line_height(rems(1.25))
|
|
||||||
.items_center()
|
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_x_1p5()
|
.gap_x_1p5()
|
||||||
.when(has_icon, |this| {
|
.children(Self::render_icon(has_icon, icon.clone(), cx))
|
||||||
this.child(
|
|
||||||
h_flex()
|
|
||||||
.w_3p5()
|
|
||||||
.h_3p5()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.text_sm()
|
|
||||||
.map(|this| {
|
|
||||||
if let Some(icon) = icon {
|
|
||||||
this.child(icon.clone().small().clone())
|
|
||||||
} else {
|
|
||||||
this.children(icon_placeholder.clone())
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
|
.gap_2()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.child(label.clone())
|
.child(label.clone())
|
||||||
|
|
@ -365,6 +463,52 @@ impl Render for PopupMenu {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
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(), cx))
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.flex_1()
|
||||||
|
.gap_2()
|
||||||
|
.items_center()
|
||||||
|
.justify_between()
|
||||||
|
.child(label.clone())
|
||||||
|
.child(IconName::ChevronRight),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.when_some(self.hovered_menu_ix, |this, hovered_ix| {
|
||||||
|
let (anchor, left) =
|
||||||
|
if cx.bounds().size.width - bounds.origin.x < max_width {
|
||||||
|
(AnchorCorner::TopRight, -px(15.))
|
||||||
|
} else {
|
||||||
|
(AnchorCorner::TopLeft, bounds.size.width - px(10.))
|
||||||
|
};
|
||||||
|
|
||||||
|
let top = if bounds.origin.y + bounds.size.height
|
||||||
|
> cx.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()),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,15 @@ pub trait StyledExt: Styled + Sized {
|
||||||
|
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set as Popover style
|
||||||
|
fn popover_style(self, cx: &mut WindowContext) -> Self {
|
||||||
|
self.bg(cx.theme().popover)
|
||||||
|
.border_1()
|
||||||
|
.border_color(cx.theme().border)
|
||||||
|
.shadow_lg()
|
||||||
|
.rounded_lg()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E: Styled> StyledExt for E {}
|
impl<E: Styled> StyledExt for E {}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue