Add PopupMenu (#15)

This commit is contained in:
parent
3ccadf6cc3
commit
acbc029eed
12 changed files with 386 additions and 73 deletions
|
|
@ -3,15 +3,22 @@ use std::sync::Arc;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use app_state::AppState;
|
use app_state::AppState;
|
||||||
use assets::Assets;
|
use assets::Assets;
|
||||||
use gpui::{App, AppContext};
|
use gpui::{actions, App, AppContext, KeyBinding, Menu, MenuItem};
|
||||||
|
|
||||||
mod app_state;
|
mod app_state;
|
||||||
mod assets;
|
mod assets;
|
||||||
mod story_workspace;
|
mod story_workspace;
|
||||||
|
|
||||||
|
actions!(main_menu, [Quit, Copy]);
|
||||||
|
|
||||||
fn init(app_state: Arc<AppState>, cx: &mut AppContext) -> Result<()> {
|
fn init(app_state: Arc<AppState>, cx: &mut AppContext) -> Result<()> {
|
||||||
story_workspace::init(app_state.clone(), cx);
|
story_workspace::init(app_state.clone(), cx);
|
||||||
|
|
||||||
|
cx.bind_keys([
|
||||||
|
KeyBinding::new("cmd-q", Quit, None),
|
||||||
|
KeyBinding::new("cmd-c", Copy, None),
|
||||||
|
]);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -28,9 +35,26 @@ fn main() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cx.on_action(quit);
|
||||||
|
|
||||||
|
cx.set_menus(vec![
|
||||||
|
Menu {
|
||||||
|
name: "GPUI App",
|
||||||
|
items: vec![MenuItem::action("Quit", Quit)],
|
||||||
|
},
|
||||||
|
Menu {
|
||||||
|
name: "Edit",
|
||||||
|
items: vec![MenuItem::action("Copy", Copy)],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
story_workspace::open_new(app_state.clone(), cx, |_workspace, _cx| {
|
story_workspace::open_new(app_state.clone(), cx, |_workspace, _cx| {
|
||||||
// do something
|
// do something
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn quit(_: &Quit, _cx: &mut AppContext) {
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use gpui::{
|
||||||
WindowContext,
|
WindowContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
use ui::{button::Button, h_flex, input::TextInput, v_flex, FocusableCycle, IconName};
|
use ui::{button::Button, h_flex, input::TextInput, v_flex, Clickable, FocusableCycle, IconName};
|
||||||
|
|
||||||
use crate::section;
|
use crate::section;
|
||||||
|
|
||||||
|
|
@ -18,7 +18,6 @@ pub fn init(cx: &mut AppContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct InputStory {
|
pub struct InputStory {
|
||||||
focus_handle: FocusHandle,
|
|
||||||
input1: View<TextInput>,
|
input1: View<TextInput>,
|
||||||
input2: View<TextInput>,
|
input2: View<TextInput>,
|
||||||
mash_input: View<TextInput>,
|
mash_input: View<TextInput>,
|
||||||
|
|
@ -65,7 +64,6 @@ impl InputStory {
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
focus_handle: cx.focus_handle(),
|
|
||||||
input1,
|
input1,
|
||||||
input2: cx.new_view(|cx| {
|
input2: cx.new_view(|cx| {
|
||||||
let mut input = TextInput::new(cx);
|
let mut input = TextInput::new(cx);
|
||||||
|
|
@ -116,17 +114,10 @@ impl FocusableCycle for InputStory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FocusableView for InputStory {
|
|
||||||
fn focus_handle(&self, cx: &gpui::AppContext) -> FocusHandle {
|
|
||||||
self.focus_handle.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for InputStory {
|
impl Render for InputStory {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
v_flex()
|
v_flex()
|
||||||
.key_context("InputStory")
|
.key_context("InputStory")
|
||||||
.track_focus(&self.focus_handle)
|
|
||||||
.on_action(cx.listener(Self::tab))
|
.on_action(cx.listener(Self::tab))
|
||||||
.on_action(cx.listener(Self::tab_prev))
|
.on_action(cx.listener(Self::tab_prev))
|
||||||
.size_full()
|
.size_full()
|
||||||
|
|
@ -158,7 +149,8 @@ impl Render for InputStory {
|
||||||
Button::new("btn-submit", cx)
|
Button::new("btn-submit", cx)
|
||||||
.w_full()
|
.w_full()
|
||||||
.style(ui::button::ButtonStyle::Primary)
|
.style(ui::button::ButtonStyle::Primary)
|
||||||
.label("Submit"),
|
.label("Submit")
|
||||||
|
.on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Tab)))),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("btn-cancel", cx)
|
Button::new("btn-cancel", cx)
|
||||||
|
|
|
||||||
|
|
@ -203,7 +203,7 @@ impl StoryContainer {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for StoryContainer {
|
impl Render for StoryContainer {
|
||||||
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
div()
|
div()
|
||||||
.id("story-container")
|
.id("story-container")
|
||||||
.overflow_y_scroll()
|
.overflow_y_scroll()
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,7 @@ impl ListStory {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new(cx: &mut ViewContext<Self>) -> Self {
|
fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||||
let companies = (0..10_000)
|
let companies = (0..1_000)
|
||||||
.map(|_| random_company())
|
.map(|_| random_company())
|
||||||
.collect::<Vec<Company>>();
|
.collect::<Vec<Company>>();
|
||||||
|
|
||||||
|
|
@ -268,10 +268,8 @@ impl Render for ListStory {
|
||||||
v_flex()
|
v_flex()
|
||||||
.h_full()
|
.h_full()
|
||||||
.w_full()
|
.w_full()
|
||||||
.border_1()
|
.border_r_1()
|
||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
.rounded_md()
|
|
||||||
.occlude()
|
|
||||||
.child(self.company_list.clone()),
|
.child(self.company_list.clone()),
|
||||||
)
|
)
|
||||||
// .child(
|
// .child(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, prelude::FluentBuilder as _, px, InteractiveElement as _, IntoElement,
|
deferred, div, hsla, prelude::FluentBuilder as _, px, InteractiveElement as _, IntoElement,
|
||||||
ParentElement, Render, Styled, Task, View, ViewContext, VisualContext as _, WeakView,
|
ParentElement, Render, Styled, Task, View, ViewContext, VisualContext as _, WeakView,
|
||||||
WindowContext,
|
WindowContext,
|
||||||
};
|
};
|
||||||
|
|
@ -9,11 +9,10 @@ use ui::{
|
||||||
h_flex,
|
h_flex,
|
||||||
list::ListItem,
|
list::ListItem,
|
||||||
picker::{Picker, PickerDelegate},
|
picker::{Picker, PickerDelegate},
|
||||||
v_flex, Clickable as _, IconName,
|
theme::hsl,
|
||||||
|
v_flex, Clickable as _, IconName, StyledExt,
|
||||||
};
|
};
|
||||||
|
|
||||||
actions!(picker_story, [DismissPicker]);
|
|
||||||
|
|
||||||
pub struct ListItemDeletegate {
|
pub struct ListItemDeletegate {
|
||||||
story: WeakView<PickerStory>,
|
story: WeakView<PickerStory>,
|
||||||
selected_index: usize,
|
selected_index: usize,
|
||||||
|
|
@ -200,7 +199,6 @@ impl Render for PickerStory {
|
||||||
Button::new("show-picker", cx)
|
Button::new("show-picker", cx)
|
||||||
.label("Show Picker...")
|
.label("Show Picker...")
|
||||||
.icon(IconName::Search)
|
.icon(IconName::Search)
|
||||||
.style(ButtonStyle::Primary)
|
|
||||||
.on_click(cx.listener(|this, _, cx| {
|
.on_click(cx.listener(|this, _, cx| {
|
||||||
this.open = !this.open;
|
this.open = !this.open;
|
||||||
this.picker.focus_handle(cx).focus(cx);
|
this.picker.focus_handle(cx).focus(cx);
|
||||||
|
|
@ -213,21 +211,24 @@ impl Render for PickerStory {
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child("You have selected:")
|
.child("You have selected:")
|
||||||
.child(selected_value),
|
.child(div().child(selected_value).text_color(gpui::red())),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when(self.open, |this| {
|
.when(self.open, |this| {
|
||||||
this.child(
|
this.child(deferred(
|
||||||
div().absolute().size_full().top_0().left_0().child(
|
div().absolute().size_full().top_0().left_0().child(
|
||||||
v_flex().top_10().flex().flex_col().items_center().child(
|
v_flex().flex().flex_col().items_center().child(
|
||||||
div()
|
div()
|
||||||
.w(px(450.))
|
.w(px(450.))
|
||||||
.h(px(350.))
|
.h(px(350.))
|
||||||
.occlude()
|
.child(self.picker.clone())
|
||||||
.child(self.picker.clone()),
|
.on_mouse_down_out(cx.listener(|this, _, cx| {
|
||||||
|
this.open = false;
|
||||||
|
cx.notify();
|
||||||
|
})),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, AnchorCorner, AppContext, DismissEvent, Element, EventEmitter, FocusHandle,
|
actions, div, impl_actions, px, AnchorCorner, AppContext, DismissEvent, Element, EventEmitter,
|
||||||
FocusableView, IntoElement, MouseButton, ParentElement as _, Render, Styled as _, View,
|
FocusHandle, FocusableView, InteractiveElement, IntoElement, MouseButton, MouseDownEvent,
|
||||||
ViewContext, VisualContext, WindowContext,
|
ParentElement as _, Render, Styled as _, View, ViewContext, VisualContext, WindowContext,
|
||||||
};
|
};
|
||||||
use ui::{
|
use ui::{
|
||||||
button::{Button, ButtonSize},
|
button::{Button, ButtonSize},
|
||||||
|
|
@ -9,9 +9,12 @@ use ui::{
|
||||||
h_flex,
|
h_flex,
|
||||||
input::TextInput,
|
input::TextInput,
|
||||||
popover::{Popover, PopoverContent},
|
popover::{Popover, PopoverContent},
|
||||||
v_flex, Clickable,
|
popup_menu::PopupMenu,
|
||||||
|
v_flex, Clickable, IconName,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
actions!(popover_story, [Copy, Paste, Cut, SearchAll]);
|
||||||
|
|
||||||
struct Form {
|
struct Form {
|
||||||
input1: View<TextInput>,
|
input1: View<TextInput>,
|
||||||
}
|
}
|
||||||
|
|
@ -36,6 +39,7 @@ impl Render for Form {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_4()
|
.gap_4()
|
||||||
|
.p_4()
|
||||||
.size_full()
|
.size_full()
|
||||||
.child("This is a form container.")
|
.child("This is a form container.")
|
||||||
.child(self.input1.clone())
|
.child(self.input1.clone())
|
||||||
|
|
@ -47,6 +51,7 @@ impl Render for Form {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PopoverStory {
|
pub struct PopoverStory {
|
||||||
|
focus_handle: FocusHandle,
|
||||||
form: View<Form>,
|
form: View<Form>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,17 +62,56 @@ impl PopoverStory {
|
||||||
|
|
||||||
fn new(cx: &mut ViewContext<Self>) -> Self {
|
fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||||
let form = Form::new(cx);
|
let form = Form::new(cx);
|
||||||
Self { form }
|
Self {
|
||||||
|
form,
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_copy(&mut self, _: &Copy, _: &mut ViewContext<Self>) {
|
||||||
|
println!("You have clicked copy");
|
||||||
|
}
|
||||||
|
fn on_cut(&mut self, _: &Cut, _: &mut ViewContext<Self>) {
|
||||||
|
println!("You have clicked cut");
|
||||||
|
}
|
||||||
|
fn on_paste(&mut self, _: &Paste, _: &mut ViewContext<Self>) {
|
||||||
|
println!("You have clicked paste");
|
||||||
|
}
|
||||||
|
fn on_search_all(&mut self, _: &SearchAll, _: &mut ViewContext<Self>) {
|
||||||
|
println!("You have clicked SearchAll");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FocusableView for PopoverStory {
|
||||||
|
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for PopoverStory {
|
impl Render for PopoverStory {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
let form = self.form.clone();
|
let form = self.form.clone();
|
||||||
|
let _focused = self.focus_handle.is_focused(cx);
|
||||||
|
let focus_handle = self.focus_handle.clone();
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
|
.track_focus(&self.focus_handle)
|
||||||
|
.on_action(cx.listener(Self::on_copy))
|
||||||
|
.on_action(cx.listener(Self::on_cut))
|
||||||
|
.on_action(cx.listener(Self::on_paste))
|
||||||
|
.on_action(cx.listener(Self::on_search_all))
|
||||||
.p_4()
|
.p_4()
|
||||||
.size_full()
|
.size_full()
|
||||||
|
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, cx| {
|
||||||
|
cx.focus(&this.focus_handle);
|
||||||
|
}))
|
||||||
|
.child(
|
||||||
|
Button::new("test1", cx)
|
||||||
|
.label("Hello")
|
||||||
|
.on_click(move |_, cx| {
|
||||||
|
cx.dispatch_action(Box::new(Copy));
|
||||||
|
}),
|
||||||
|
)
|
||||||
.gap_6()
|
.gap_6()
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
|
@ -115,6 +159,29 @@ impl Render for PopoverStory {
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.child(
|
||||||
|
h_flex().child(
|
||||||
|
Popover::new("popup-menu")
|
||||||
|
.trigger(Button::new("popup-menu-1", cx).icon(IconName::Info))
|
||||||
|
.content(move |cx| {
|
||||||
|
let focus_handle = focus_handle.clone();
|
||||||
|
PopupMenu::build(cx, |mut this, _| {
|
||||||
|
this.content(focus_handle)
|
||||||
|
.menu("Copy", Box::new(Copy))
|
||||||
|
.menu("Cut", Box::new(Cut))
|
||||||
|
.menu("Paste", Box::new(Paste))
|
||||||
|
.separator()
|
||||||
|
.menu_with_icon(
|
||||||
|
IconName::Search,
|
||||||
|
"Search",
|
||||||
|
Box::new(SearchAll),
|
||||||
|
);
|
||||||
|
|
||||||
|
this
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
div().absolute().bottom_4().left_0().w_full().h_10().child(
|
div().absolute().bottom_4().left_0().w_full().h_10().child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use gpui::{
|
||||||
SharedString, StyleRefinement, Styled, Svg, View, VisualContext, WindowContext,
|
SharedString, StyleRefinement, Styled, Svg, View, VisualContext, WindowContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement, Clone)]
|
||||||
pub enum IconName {
|
pub enum IconName {
|
||||||
Check,
|
Check,
|
||||||
Minus,
|
Minus,
|
||||||
|
|
@ -77,15 +77,27 @@ pub struct Icon {
|
||||||
size: ButtonSize,
|
size: ButtonSize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Icon {
|
impl Default for Icon {
|
||||||
pub fn new(name: IconName) -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
base: svg().flex_none().size_4(),
|
base: svg().flex_none().size_4(),
|
||||||
path: name.path(),
|
path: "".into(),
|
||||||
text_color: None,
|
text_color: None,
|
||||||
size: ButtonSize::Medium,
|
size: ButtonSize::Medium,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for Icon {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self::default().path(self.path.clone()).size(self.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Icon {
|
||||||
|
pub fn new(name: IconName) -> Self {
|
||||||
|
Self::default().path(name.path())
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the icon path of the Assets bundle
|
/// Set the icon path of the Assets bundle
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ pub mod input;
|
||||||
pub mod list;
|
pub mod list;
|
||||||
pub mod picker;
|
pub mod picker;
|
||||||
pub mod popover;
|
pub mod popover;
|
||||||
|
pub mod popup_menu;
|
||||||
pub mod switch;
|
pub mod switch;
|
||||||
pub mod tab;
|
pub mod tab;
|
||||||
pub mod tooltip;
|
pub mod tooltip;
|
||||||
|
|
@ -42,4 +43,5 @@ pub fn init(cx: &mut gpui::AppContext) {
|
||||||
picker::init(cx);
|
picker::init(cx);
|
||||||
dropdown::init(cx);
|
dropdown::init(cx);
|
||||||
popover::init(cx);
|
popover::init(cx);
|
||||||
|
popup_menu::init(cx);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, prelude::FluentBuilder as _, ClickEvent, Div, ElementId, InteractiveElement, IntoElement,
|
div, prelude::FluentBuilder as _, ClickEvent, Div, ElementId, InteractiveElement, IntoElement,
|
||||||
MouseButton, MouseDownEvent, ParentElement, RenderOnce, Stateful,
|
MouseButton, MouseDownEvent, ParentElement, Pixels, RenderOnce, Stateful,
|
||||||
StatefulInteractiveElement as _, Styled, WindowContext,
|
StatefulInteractiveElement as _, Styled, WindowContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -12,6 +12,7 @@ pub struct ListItem {
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
check_icon: Option<Icon>,
|
check_icon: Option<Icon>,
|
||||||
|
border_radius: Option<Pixels>,
|
||||||
on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
on_click: Option<Box<dyn Fn(&ClickEvent, &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>>,
|
||||||
}
|
}
|
||||||
|
|
@ -25,6 +26,7 @@ impl ListItem {
|
||||||
on_click: None,
|
on_click: None,
|
||||||
on_secondary_mouse_down: None,
|
on_secondary_mouse_down: None,
|
||||||
check_icon: None,
|
check_icon: None,
|
||||||
|
border_radius: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,6 +45,11 @@ impl ListItem {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn rounded(mut self, r: impl Into<Pixels>) -> Self {
|
||||||
|
self.border_radius = Some(r.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
|
pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
|
||||||
self.on_click = Some(Box::new(handler));
|
self.on_click = Some(Box::new(handler));
|
||||||
self
|
self
|
||||||
|
|
@ -93,16 +100,25 @@ impl RenderOnce for ListItem {
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.text_base()
|
.text_base()
|
||||||
.text_color(cx.theme().foreground)
|
.text_color(cx.theme().foreground)
|
||||||
|
.when_some(self.border_radius, |this, r| this.rounded(r))
|
||||||
.when_some(self.on_click, |this, on_click| {
|
.when_some(self.on_click, |this, on_click| {
|
||||||
this.cursor_pointer().on_click(on_click)
|
if !self.disabled {
|
||||||
|
this.cursor_pointer().on_click(on_click)
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.when(self.selected, |this| this.bg(cx.theme().accent))
|
.when(self.selected, |this| this.bg(cx.theme().accent))
|
||||||
.when(!self.selected, |this| {
|
.when(!self.selected && !self.disabled, |this| {
|
||||||
this.hover(|this| this.bg(cx.theme().accent))
|
this.hover(|this| this.bg(cx.theme().accent))
|
||||||
})
|
})
|
||||||
// Right click
|
// Right click
|
||||||
.when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
|
.when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
|
||||||
this.on_mouse_down(MouseButton::Right, move |ev, cx| (on_mouse_down)(ev, cx))
|
if !self.disabled {
|
||||||
|
this.on_mouse_down(MouseButton::Right, move |ev, cx| (on_mouse_down)(ev, cx))
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.child(self.base.w_full())
|
.child(self.base.w_full())
|
||||||
.when(self.selected, |this| {
|
.when(self.selected, |this| {
|
||||||
|
|
|
||||||
|
|
@ -105,10 +105,6 @@ pub trait PickerDelegate: Sized + 'static {
|
||||||
cx: &mut ViewContext<Picker<Self>>,
|
cx: &mut ViewContext<Picker<Self>>,
|
||||||
) -> Option<Self::ListItem>;
|
) -> Option<Self::ListItem>;
|
||||||
|
|
||||||
fn separators_after_indices(&self) -> Vec<usize> {
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_matches(&mut self, _query: &str, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
fn update_matches(&mut self, _query: &str, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||||
Task::ready(())
|
Task::ready(())
|
||||||
}
|
}
|
||||||
|
|
@ -332,15 +328,6 @@ impl<D: PickerDelegate> Picker<D> {
|
||||||
self.delegate
|
self.delegate
|
||||||
.render_item(ix, ix == self.delegate.selected_index(), cx),
|
.render_item(ix, ix == self.delegate.selected_index(), cx),
|
||||||
)
|
)
|
||||||
.when(
|
|
||||||
self.delegate.separators_after_indices().contains(&ix),
|
|
||||||
|picker| {
|
|
||||||
picker
|
|
||||||
.border_color(cx.theme().border)
|
|
||||||
.border_b_1()
|
|
||||||
.pb(px(-1.0))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_scrollbar(&self, cx: &mut ViewContext<Self>) -> Option<Scrollbar> {
|
fn render_scrollbar(&self, cx: &mut ViewContext<Self>) -> Option<Scrollbar> {
|
||||||
|
|
@ -547,8 +534,8 @@ impl<D: PickerDelegate> Render for Picker<D> {
|
||||||
.id("picker")
|
.id("picker")
|
||||||
.key_context("Picker")
|
.key_context("Picker")
|
||||||
.group("picker-group")
|
.group("picker-group")
|
||||||
.size_full()
|
|
||||||
.track_focus(&focus_handle)
|
.track_focus(&focus_handle)
|
||||||
|
.size_full()
|
||||||
.when_some(self.width, |el, width| el.w(width))
|
.when_some(self.width, |el, width| el.w(width))
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.when(self.is_modal, |this| this.elevation_3(cx))
|
.when(self.is_modal, |this| this.elevation_3(cx))
|
||||||
|
|
@ -608,8 +595,5 @@ impl<D: PickerDelegate> Render for Picker<D> {
|
||||||
.child("No matched."),
|
.child("No matched."),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.on_mouse_down_out(cx.listener(|_, _, cx| {
|
|
||||||
cx.dispatch_action(Box::new(Cancel));
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,13 @@ use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
use crate::{theme::ActiveTheme, Selectable, StyledExt as _};
|
use crate::{theme::ActiveTheme, Selectable, StyledExt as _};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, anchored, deferred, div, prelude::FluentBuilder, AnchorCorner, AnyElement, AppContext,
|
anchored, deferred, div, prelude::FluentBuilder, AnchorCorner, AnyElement, AppContext, Bounds,
|
||||||
Bounds, DismissEvent, DispatchPhase, Element, ElementId, EventEmitter, FocusHandle,
|
DismissEvent, DispatchPhase, Element, ElementId, EventEmitter, FocusHandle, FocusableView,
|
||||||
FocusableView, GlobalElementId, Hitbox, InteractiveElement, IntoElement, LayoutId, ManagedView,
|
GlobalElementId, Hitbox, InteractiveElement, IntoElement, LayoutId, ManagedView, MouseButton,
|
||||||
MouseButton, MouseDownEvent, ParentElement as _, Pixels, Point, Render, Style, Styled as _,
|
MouseDownEvent, ParentElement as _, Pixels, Point, Render, Style, Styled as _, View,
|
||||||
View, ViewContext, VisualContext, WindowContext,
|
ViewContext, VisualContext, WindowContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
actions!(popover, [Open, Dismiss]);
|
|
||||||
|
|
||||||
pub fn init(_cx: &AppContext) {}
|
pub fn init(_cx: &AppContext) {}
|
||||||
|
|
||||||
pub struct PopoverContent {
|
pub struct PopoverContent {
|
||||||
|
|
@ -43,7 +41,7 @@ impl FocusableView for PopoverContent {
|
||||||
|
|
||||||
impl Render for PopoverContent {
|
impl Render for PopoverContent {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
div().child(self.content.clone()(cx))
|
div().p_4().max_w_128().child(self.content.clone()(cx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -209,17 +207,14 @@ impl<M: ManagedView> Element for Popover<M> {
|
||||||
anchored.child(
|
anchored.child(
|
||||||
div()
|
div()
|
||||||
.occlude()
|
.occlude()
|
||||||
.map(|d| match this.anchor {
|
|
||||||
AnchorCorner::TopLeft | AnchorCorner::TopRight => d.mt_2(),
|
|
||||||
AnchorCorner::BottomLeft | AnchorCorner::BottomRight => d.mb_2(),
|
|
||||||
})
|
|
||||||
.elevation_2(cx)
|
.elevation_2(cx)
|
||||||
.bg(cx.theme().popover)
|
.bg(cx.theme().popover)
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
.p_4()
|
.map(|d| match this.anchor {
|
||||||
.max_w_128()
|
AnchorCorner::TopLeft | AnchorCorner::TopRight => d.mt_2(),
|
||||||
.occlude()
|
AnchorCorner::BottomLeft | AnchorCorner::BottomRight => d.mb_2(),
|
||||||
|
})
|
||||||
.on_mouse_down_out(move |_, cx| {
|
.on_mouse_down_out(move |_, cx| {
|
||||||
// Update the element_state.content_view to `None`,
|
// Update the element_state.content_view to `None`,
|
||||||
// so that the `paint`` method will not paint it.
|
// so that the `paint`` method will not paint it.
|
||||||
|
|
|
||||||
222
crates/ui/src/popup_menu.rs
Normal file
222
crates/ui/src/popup_menu.rs
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use gpui::{
|
||||||
|
actions, div, prelude::FluentBuilder, px, Action, AppContext, DismissEvent, EventEmitter,
|
||||||
|
FocusHandle, FocusableView, InteractiveElement, KeyBinding, ParentElement, Render,
|
||||||
|
SharedString, Styled as _, View, ViewContext, VisualContext as _, WindowContext,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{h_flex, list::ListItem, theme::ActiveTheme, v_flex, Icon, StyledExt as _};
|
||||||
|
|
||||||
|
actions!(menu, [Confirm, Dismiss, SelectNext, SelectPrev]);
|
||||||
|
|
||||||
|
pub fn init(cx: &mut AppContext) {
|
||||||
|
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),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PopupMenuItem {
|
||||||
|
Separator,
|
||||||
|
Item {
|
||||||
|
icon: Option<Icon>,
|
||||||
|
label: SharedString,
|
||||||
|
action: Option<Box<dyn Action>>,
|
||||||
|
handler: Rc<dyn Fn(Option<&FocusHandle>, &mut WindowContext)>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PopupMenuItem {
|
||||||
|
fn is_clickable(&self) -> bool {
|
||||||
|
!matches!(self, PopupMenuItem::Separator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PopupMenu {
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
action_context: Option<FocusHandle>,
|
||||||
|
menu_items: Vec<PopupMenuItem>,
|
||||||
|
selected_index: Option<usize>,
|
||||||
|
_subscriptions: [gpui::Subscription; 1],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PopupMenu {
|
||||||
|
pub fn build(
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
f: impl FnOnce(Self, &mut WindowContext) -> Self,
|
||||||
|
) -> View<Self> {
|
||||||
|
cx.new_view(|cx| {
|
||||||
|
let focus_handle = cx.focus_handle();
|
||||||
|
let _on_blur_subscription = cx.on_blur(&focus_handle, |this: &mut PopupMenu, cx| {
|
||||||
|
this.dismiss(&Dismiss, cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
let menu = Self {
|
||||||
|
focus_handle,
|
||||||
|
action_context: None,
|
||||||
|
menu_items: Vec::new(),
|
||||||
|
selected_index: None,
|
||||||
|
_subscriptions: [_on_blur_subscription],
|
||||||
|
};
|
||||||
|
cx.refresh();
|
||||||
|
f(menu, cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// You must set content (FocusHandle) with the parent view, if the menu action is listening on the parent view.
|
||||||
|
/// When the Menu Item confirmed, the parent view will be focused again to ensure to receive the action.
|
||||||
|
pub fn content(&mut self, content: FocusHandle) -> &mut Self {
|
||||||
|
self.action_context = Some(content);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add Menu Item
|
||||||
|
pub fn menu(&mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> &mut Self {
|
||||||
|
self.add_menu_item(None, label, action)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add Menu Item with Icon
|
||||||
|
pub fn menu_with_icon(
|
||||||
|
&mut self,
|
||||||
|
icon: impl Into<Icon>,
|
||||||
|
label: impl Into<SharedString>,
|
||||||
|
action: Box<dyn Action>,
|
||||||
|
) -> &mut Self {
|
||||||
|
self.add_menu_item(Some(icon.into()), label, action)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_menu_item(
|
||||||
|
&mut self,
|
||||||
|
icon: Option<Icon>,
|
||||||
|
label: impl Into<SharedString>,
|
||||||
|
action: Box<dyn Action>,
|
||||||
|
) -> &mut Self {
|
||||||
|
self.menu_items.push(PopupMenuItem::Item {
|
||||||
|
icon,
|
||||||
|
label: label.into(),
|
||||||
|
action: Some(action.boxed_clone()),
|
||||||
|
handler: Rc::new(move |content, cx| {
|
||||||
|
if let Some(content) = &content {
|
||||||
|
cx.focus(content);
|
||||||
|
}
|
||||||
|
cx.dispatch_action(action.boxed_clone());
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a separator Menu Item
|
||||||
|
pub fn separator(&mut self) -> &mut Self {
|
||||||
|
self.menu_items.push(PopupMenuItem::Separator);
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
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, cx: &mut ViewContext<Self>) {
|
||||||
|
cx.stop_propagation();
|
||||||
|
cx.prevent_default();
|
||||||
|
self.selected_index = Some(ix);
|
||||||
|
self.confirm(&Confirm, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||||
|
let content = self.action_context.as_ref();
|
||||||
|
match self.selected_index {
|
||||||
|
Some(index) => {
|
||||||
|
let item = self.menu_items.get(index);
|
||||||
|
match item {
|
||||||
|
Some(PopupMenuItem::Item { handler, .. }) => {
|
||||||
|
handler(content, cx);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
|
||||||
|
let count = self.clickable_menu_items().count();
|
||||||
|
if count > 0 {
|
||||||
|
let ix = self
|
||||||
|
.selected_index
|
||||||
|
.map(|index| if index == count - 1 { 0 } else { index + 1 })
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
self.selected_index = Some(ix);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
|
||||||
|
let count = self.clickable_menu_items().count();
|
||||||
|
if count > 0 {
|
||||||
|
let ix = self
|
||||||
|
.selected_index
|
||||||
|
.map(|index| if index == count - 1 { 0 } else { index - 1 })
|
||||||
|
.unwrap_or(count - 1);
|
||||||
|
self.selected_index = Some(ix);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
|
||||||
|
cx.emit(DismissEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FluentBuilder for PopupMenu {}
|
||||||
|
impl EventEmitter<DismissEvent> for PopupMenu {}
|
||||||
|
impl FocusableView for PopupMenu {
|
||||||
|
fn focus_handle(&self, _cx: &gpui::AppContext) -> FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for PopupMenu {
|
||||||
|
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement {
|
||||||
|
v_flex()
|
||||||
|
.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, _, cx| this.dismiss(&Dismiss, cx)))
|
||||||
|
.max_h(px(550.))
|
||||||
|
.min_w(px(230.))
|
||||||
|
.p_1p5()
|
||||||
|
.max_w_128()
|
||||||
|
.gap_y_0p5()
|
||||||
|
.children(self.menu_items.iter_mut().enumerate().map(|(ix, item)| {
|
||||||
|
let this = ListItem::new(("menu-item", ix))
|
||||||
|
.on_click(cx.listener(move |this, _, cx| this.on_click(ix, cx)));
|
||||||
|
match item {
|
||||||
|
PopupMenuItem::Separator => this
|
||||||
|
.disabled(true)
|
||||||
|
.child(div().h(px(1.)).m_1().border_0().bg(cx.theme().border)),
|
||||||
|
PopupMenuItem::Item { icon, label, .. } => {
|
||||||
|
this.py_1().px_2().text_sm().rounded(px(4.)).child(
|
||||||
|
h_flex()
|
||||||
|
.size_full()
|
||||||
|
.gap_2()
|
||||||
|
.items_center()
|
||||||
|
.children(icon.clone())
|
||||||
|
.child(label.clone()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue