list: Add secondary confirm support to list. (#769)

This commit is contained in:
Jason Lee 2025-04-02 16:45:27 +08:00 committed by GitHub
parent c91dce3139
commit a60acd7e4d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 187 additions and 134 deletions

View file

@ -249,7 +249,7 @@ impl InputStory {
) { ) {
match event { match event {
InputEvent::Change(text) => println!("Change: {}", text), InputEvent::Change(text) => println!("Change: {}", text),
InputEvent::PressEnter => println!("PressEnter"), InputEvent::PressEnter { secondary } => println!("PressEnter secondary: {}", secondary),
InputEvent::Focus => println!("Focus"), InputEvent::Focus => println!("Focus"),
InputEvent::Blur => println!("Blur"), InputEvent::Blur => println!("Blur"),
}; };
@ -270,7 +270,9 @@ impl InputStory {
} }
println!("Change: {}", text); println!("Change: {}", text);
} }
InputEvent::PressEnter => println!("PressEnter"), InputEvent::PressEnter { secondary } => {
println!("PressEnter secondary: {}", secondary)
}
InputEvent::Focus => println!("Focus"), InputEvent::Focus => println!("Focus"),
InputEvent::Blur => println!("Blur"), InputEvent::Blur => println!("Blur"),
}, },
@ -306,7 +308,9 @@ impl InputStory {
} }
println!("Change: {}", text); println!("Change: {}", text);
} }
InputEvent::PressEnter => println!("PressEnter"), InputEvent::PressEnter { secondary } => {
println!("PressEnter secondary: {}", secondary);
}
InputEvent::Focus => println!("Focus"), InputEvent::Focus => println!("Focus"),
InputEvent::Blur => println!("Blur"), InputEvent::Blur => println!("Blur"),
}, },

View file

@ -174,8 +174,8 @@ impl ListDelegate for CompanyListDelegate {
Task::ready(()) Task::ready(())
} }
fn confirm(&mut self, ix: usize, window: &mut Window, cx: &mut Context<List<Self>>) { fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<List<Self>>) {
self.confirmed_index = Some(ix); println!("Confirmed with secondary: {}", secondary);
window.dispatch_action(Box::new(SelectedCompany), cx); window.dispatch_action(Box::new(SelectedCompany), cx);
} }

View file

@ -368,7 +368,6 @@ impl StoryWorkspace {
Arc::new(StoryContainer::panel::<AccordionStory>(window, cx)), Arc::new(StoryContainer::panel::<AccordionStory>(window, cx)),
Arc::new(StoryContainer::panel::<SidebarStory>(window, cx)), Arc::new(StoryContainer::panel::<SidebarStory>(window, cx)),
Arc::new(StoryContainer::panel::<FormStory>(window, cx)), Arc::new(StoryContainer::panel::<FormStory>(window, cx)),
Arc::new(StoryContainer::panel::<WebViewStory>(window, cx)),
], ],
None, None,
&dock_area, &dock_area,

View file

@ -128,11 +128,13 @@ impl ListDelegate for ListItemDeletegate {
}); });
} }
fn confirm(&mut self, ix: usize, window: &mut Window, cx: &mut Context<List<Self>>) { fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<List<Self>>) {
_ = self.story.update(cx, |this, cx| { _ = self.story.update(cx, |this, cx| {
self.confirmed_index = Some(ix); self.confirmed_index = self.selected_index;
if let Some(item) = self.matches.get(ix) { if let Some(ix) = self.confirmed_index {
this.selected_value = Some(SharedString::from(item.to_string())); if let Some(item) = self.matches.get(ix) {
this.selected_value = Some(SharedString::from(item.to_string()));
}
} }
window.close_drawer(cx); window.close_drawer(cx);

View file

@ -660,7 +660,7 @@ impl TableStory {
) { ) {
match event { match event {
// Update when the user presses Enter or the input loses focus // Update when the user presses Enter or the input loses focus
InputEvent::PressEnter | InputEvent::Blur => { InputEvent::PressEnter { .. } | InputEvent::Blur => {
let text = self.num_stocks_input.read(cx).text().to_string(); let text = self.num_stocks_input.read(cx).text().to_string();
if let Ok(num) = text.parse::<usize>() { if let Ok(num) = text.parse::<usize>() {
self.table.update(cx, |table, _| { self.table.update(cx, |table, _| {

View file

@ -97,7 +97,7 @@ impl WebViewStory {
cx.subscribe( cx.subscribe(
&address_input, &address_input,
|this: &mut Self, input, event: &InputEvent, cx| match event { |this: &mut Self, input, event: &InputEvent, cx| match event {
InputEvent::PressEnter => { InputEvent::PressEnter { .. } => {
let url = input.read(cx).text(); let url = input.read(cx).text();
this.webview.update(cx, |view, _| { this.webview.update(cx, |view, _| {
view.load_url(&url); view.load_url(&url);

11
crates/ui/src/actions.rs Normal file
View file

@ -0,0 +1,11 @@
use gpui::{actions, impl_internal_actions};
use serde::Deserialize;
#[derive(Clone, PartialEq, Eq, Deserialize)]
pub struct Confirm {
/// Is confirm with secondary.
pub secondary: bool,
}
actions!(list, [Cancel, SelectPrev, SelectNext]);
impl_internal_actions!(list, [Confirm]);

View file

@ -6,19 +6,19 @@ use gpui::{
}; };
use crate::{ use crate::{
actions::Cancel,
button::{Button, ButtonVariants}, button::{Button, ButtonVariants},
divider::Divider, divider::Divider,
h_flex, h_flex,
input::{InputEvent, TextInput}, input::{InputEvent, TextInput},
popover::Escape,
tooltip::Tooltip, tooltip::Tooltip,
v_flex, ActiveTheme as _, Colorize as _, Icon, Selectable as _, Sizable, Size, StyleSized, v_flex, ActiveTheme as _, Colorize as _, Icon, Selectable as _, Sizable, Size, StyleSized,
}; };
const KEY_CONTEXT: &'static str = "ColorPicker"; const CONTEXT: &'static str = "ColorPicker";
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
cx.bind_keys([KeyBinding::new("escape", Escape, Some(KEY_CONTEXT))]) cx.bind_keys([KeyBinding::new("escape", Cancel, Some(CONTEXT))])
} }
#[derive(Clone)] #[derive(Clone)]
@ -73,7 +73,7 @@ pub struct ColorPicker {
impl ColorPicker { impl ColorPicker {
pub fn new(id: impl Into<ElementId>, window: &mut Window, cx: &mut Context<Self>) -> Self { pub fn new(id: impl Into<ElementId>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let color_input = cx.new(|cx| TextInput::new(window, cx).xsmall()); let color_input = cx.new(|cx| TextInput::new(window, cx).small());
let _subscriptions = vec![cx.subscribe_in( let _subscriptions = vec![cx.subscribe_in(
&color_input, &color_input,
@ -85,7 +85,7 @@ impl ColorPicker {
this.hovered_color = Some(color); this.hovered_color = Some(color);
} }
} }
InputEvent::PressEnter => { InputEvent::PressEnter { .. } => {
let val = this.color_input.read(cx).text(); let val = this.color_input.read(cx).text();
if let Ok(color) = Hsla::parse_hex(&val) { if let Ok(color) = Hsla::parse_hex(&val) {
this.open = false; this.open = false;
@ -170,8 +170,11 @@ impl ColorPicker {
self self
} }
fn on_escape(&mut self, _: &Escape, _: &mut Window, cx: &mut Context<Self>) { fn on_escape(&mut self, _: &Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.propagate(); if !self.open {
cx.propagate();
}
self.open = false; self.open = false;
cx.notify(); cx.notify();
} }
@ -314,7 +317,7 @@ impl Render for ColorPicker {
div() div()
.id(self.id.clone()) .id(self.id.clone())
.key_context(KEY_CONTEXT) .key_context(CONTEXT)
.track_focus(&self.focus_handle) .track_focus(&self.focus_handle)
.on_action(cx.listener(Self::on_escape)) .on_action(cx.listener(Self::on_escape))
.child( .child(
@ -358,10 +361,6 @@ impl Render for ColorPicker {
}) })
.when_some(self.label.clone(), |this, label| this.child(label)) .when_some(self.label.clone(), |this, label| this.child(label))
.on_click(cx.listener(Self::toggle_picker)) .on_click(cx.listener(Self::toggle_picker))
.on_mouse_up_out(
MouseButton::Left,
cx.listener(|view, _, window, cx| view.on_escape(&Escape, window, cx)),
)
.child( .child(
canvas( canvas(
move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds), move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds),
@ -394,7 +393,13 @@ impl Render for ColorPicker {
.shadow_lg() .shadow_lg()
.rounded(cx.theme().radius) .rounded(cx.theme().radius)
.bg(cx.theme().background) .bg(cx.theme().background)
.child(self.render_colors(window, cx)), .child(self.render_colors(window, cx))
.on_mouse_up_out(
MouseButton::Left,
cx.listener(|view, _, window, cx| {
view.on_escape(&Cancel, window, cx)
}),
),
), ),
) )
.with_priority(1), .with_priority(1),

View file

@ -1,13 +1,14 @@
use std::{rc::Rc, time::Duration}; use std::{rc::Rc, time::Duration};
use gpui::{ use gpui::{
actions, anchored, div, point, prelude::FluentBuilder as _, px, Animation, AnimationExt as _, anchored, div, point, prelude::FluentBuilder as _, px, Animation, AnimationExt as _,
AnyElement, App, ClickEvent, DefiniteLength, DismissEvent, Div, EventEmitter, FocusHandle, AnyElement, App, ClickEvent, DefiniteLength, DismissEvent, Div, EventEmitter, FocusHandle,
InteractiveElement as _, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, InteractiveElement as _, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels,
RenderOnce, Styled, Window, RenderOnce, Styled, Window,
}; };
use crate::{ use crate::{
actions::Cancel,
button::{Button, ButtonVariants as _}, button::{Button, ButtonVariants as _},
h_flex, h_flex,
modal::overlay_color, modal::overlay_color,
@ -17,11 +18,9 @@ use crate::{
v_flex, ActiveTheme, IconName, Placement, Sizable, StyledExt as _, v_flex, ActiveTheme, IconName, Placement, Sizable, StyledExt as _,
}; };
actions!(drawer, [Escape]);
const CONTEXT: &str = "Drawer"; const CONTEXT: &str = "Drawer";
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
cx.bind_keys([KeyBinding::new("escape", Escape, Some(CONTEXT))]) cx.bind_keys([KeyBinding::new("escape", Cancel, Some(CONTEXT))])
} }
#[derive(IntoElement)] #[derive(IntoElement)]
@ -154,7 +153,9 @@ impl RenderOnce for Drawer {
.track_focus(&self.focus_handle) .track_focus(&self.focus_handle)
.on_action({ .on_action({
let on_close = self.on_close.clone(); let on_close = self.on_close.clone();
move |_: &Escape, window, cx| { move |_: &Cancel, window, cx| {
cx.propagate();
on_close(&ClickEvent::default(), window, cx); on_close(&ClickEvent::default(), window, cx);
window.close_drawer(cx); window.close_drawer(cx);
} }

View file

@ -1,21 +1,19 @@
use gpui::{ use gpui::{
actions, anchored, canvas, deferred, div, prelude::FluentBuilder, px, rems, AnyElement, App, anchored, canvas, deferred, div, prelude::FluentBuilder, px, rems, AnyElement, App, AppContext,
AppContext, Bounds, ClickEvent, Context, DismissEvent, ElementId, Entity, EventEmitter, Bounds, ClickEvent, Context, DismissEvent, ElementId, Entity, EventEmitter, FocusHandle,
FocusHandle, Focusable, InteractiveElement, IntoElement, KeyBinding, Length, ParentElement, Focusable, InteractiveElement, IntoElement, KeyBinding, Length, ParentElement, Pixels, Render,
Pixels, Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, Window,
WeakEntity, Window,
}; };
use rust_i18n::t; use rust_i18n::t;
use crate::{ use crate::{
actions::{Cancel, Confirm, SelectNext, SelectPrev},
h_flex, h_flex,
input::clear_button, input::clear_button,
list::{self, List, ListDelegate, ListItem}, list::{List, ListDelegate, ListItem},
v_flex, ActiveTheme, Disableable as _, Icon, IconName, Sizable, Size, StyleSized, StyledExt, v_flex, ActiveTheme, Disableable as _, Icon, IconName, Sizable, Size, StyleSized, StyledExt,
}; };
actions!(dropdown, [Up, Down, Enter, Escape]);
#[derive(Clone)] #[derive(Clone)]
pub enum ListEvent { pub enum ListEvent {
/// Single click or move to selected row. /// Single click or move to selected row.
@ -29,10 +27,15 @@ pub enum ListEvent {
const CONTEXT: &str = "Dropdown"; const CONTEXT: &str = "Dropdown";
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
cx.bind_keys([ cx.bind_keys([
KeyBinding::new("up", Up, Some(CONTEXT)), KeyBinding::new("up", SelectPrev, Some(CONTEXT)),
KeyBinding::new("down", Down, Some(CONTEXT)), KeyBinding::new("down", SelectNext, Some(CONTEXT)),
KeyBinding::new("enter", Enter, Some(CONTEXT)), KeyBinding::new("enter", Confirm { secondary: false }, Some(CONTEXT)),
KeyBinding::new("escape", Escape, Some(CONTEXT)), KeyBinding::new(
"secondary-enter",
Confirm { secondary: true },
Some(CONTEXT),
),
KeyBinding::new("escape", Cancel, Some(CONTEXT)),
]) ])
} }
@ -173,9 +176,7 @@ where
}); });
} }
fn confirm(&mut self, ix: usize, window: &mut Window, cx: &mut Context<List<Self>>) { fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<List<Self>>) {
self.selected_index = Some(ix);
let selected_value = self let selected_value = self
.selected_index .selected_index
.and_then(|ix| self.delegate.get(ix)) .and_then(|ix| self.delegate.get(ix))
@ -495,24 +496,25 @@ where
cx.notify(); cx.notify();
} }
fn up(&mut self, _: &Up, window: &mut Window, cx: &mut Context<Self>) { fn up(&mut self, _: &SelectPrev, window: &mut Window, cx: &mut Context<Self>) {
if !self.open { if !self.open {
return; return;
} }
self.list.focus_handle(cx).focus(window); self.list.focus_handle(cx).focus(window);
cx.dispatch_action(&list::SelectPrev); cx.propagate();
} }
fn down(&mut self, _: &Down, window: &mut Window, cx: &mut Context<Self>) { fn down(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
if !self.open { if !self.open {
self.open = true; self.open = true;
} }
self.list.focus_handle(cx).focus(window); self.list.focus_handle(cx).focus(window);
cx.dispatch_action(&list::SelectNext); cx.propagate();
} }
fn enter(&mut self, _: &Enter, window: &mut Window, cx: &mut Context<Self>) { fn enter(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
// Propagate the event to the parent view, for example to the Modal to support ENTER to confirm. // Propagate the event to the parent view, for example to the Modal to support ENTER to confirm.
cx.propagate(); cx.propagate();
@ -521,7 +523,6 @@ where
cx.notify(); cx.notify();
} else { } else {
self.list.focus_handle(cx).focus(window); self.list.focus_handle(cx).focus(window);
cx.dispatch_action(&list::Confirm);
} }
} }
@ -535,9 +536,10 @@ where
cx.notify(); cx.notify();
} }
fn escape(&mut self, _: &Escape, _: &mut Window, cx: &mut Context<Self>) { fn escape(&mut self, _: &Cancel, _: &mut Window, cx: &mut Context<Self>) {
// Propagate the event to the parent view, for example to the Modal to support ESC to close. if !self.open {
cx.propagate(); cx.propagate();
}
self.open = false; self.open = false;
cx.notify(); cx.notify();
@ -734,13 +736,10 @@ where
.border_color(cx.theme().border) .border_color(cx.theme().border)
.rounded(popup_radius) .rounded(popup_radius)
.shadow_md() .shadow_md()
.on_mouse_down_out(|_, _, cx| {
cx.dispatch_action(&Escape);
})
.child(self.list.clone()), .child(self.list.clone()),
) )
.on_mouse_down_out(cx.listener(|this, _, window, cx| { .on_mouse_down_out(cx.listener(|this, _, window, cx| {
this.escape(&Escape, window, cx); this.escape(&Cancel, window, cx);
})), })),
), ),
) )

View file

@ -3,6 +3,7 @@
//! Based on the `Input` example from the `gpui` crate. //! Based on the `Input` example from the `gpui` crate.
//! https://github.com/zed-industries/zed/blob/main/crates/gpui/examples/input.rs //! https://github.com/zed-industries/zed/blob/main/crates/gpui/examples/input.rs
use serde::Deserialize;
use smallvec::SmallVec; use smallvec::SmallVec;
use std::cell::Cell; use std::cell::Cell;
use std::ops::Range; use std::ops::Range;
@ -11,12 +12,12 @@ use unicode_segmentation::*;
use gpui::prelude::FluentBuilder as _; use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
actions, div, point, px, relative, AnyElement, App, AppContext, Bounds, ClickEvent, actions, div, impl_internal_actions, point, px, relative, AnyElement, App, AppContext, Bounds,
ClipboardItem, Context, DefiniteLength, Entity, EntityInputHandler, EventEmitter, FocusHandle, ClickEvent, ClipboardItem, Context, DefiniteLength, Entity, EntityInputHandler, EventEmitter,
Focusable, InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent, MouseButton, FocusHandle, Focusable, InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Rems, Render, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point,
ScrollHandle, ScrollWheelEvent, SharedString, Styled as _, Subscription, UTF16Selection, Rems, Render, ScrollHandle, ScrollWheelEvent, SharedString, Styled as _, Subscription,
Window, WrappedLine, UTF16Selection, Window, WrappedLine,
}; };
// TODO: // TODO:
@ -37,6 +38,14 @@ use crate::{ActiveTheme, Root};
use crate::{IconName, Size}; use crate::{IconName, Size};
use crate::{Sizable, StyleSized}; use crate::{Sizable, StyleSized};
#[derive(Clone, PartialEq, Eq, Deserialize)]
pub struct Enter {
/// Is confirm with secondary.
pub secondary: bool,
}
impl_internal_actions!(input, [Enter]);
actions!( actions!(
input, input,
[ [
@ -46,7 +55,6 @@ actions!(
DeleteToEndOfLine, DeleteToEndOfLine,
DeleteToPreviousWordStart, DeleteToPreviousWordStart,
DeleteToNextWordEnd, DeleteToNextWordEnd,
Enter,
Up, Up,
Down, Down,
Left, Left,
@ -83,7 +91,7 @@ actions!(
#[derive(Clone)] #[derive(Clone)]
pub enum InputEvent { pub enum InputEvent {
Change(SharedString), Change(SharedString),
PressEnter, PressEnter { secondary: bool },
Focus, Focus,
Blur, Blur,
} }
@ -106,7 +114,8 @@ pub fn init(cx: &mut App) {
KeyBinding::new("alt-delete", DeleteToNextWordEnd, Some(CONTEXT)), KeyBinding::new("alt-delete", DeleteToNextWordEnd, Some(CONTEXT)),
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
KeyBinding::new("ctrl-delete", DeleteToNextWordEnd, Some(CONTEXT)), KeyBinding::new("ctrl-delete", DeleteToNextWordEnd, Some(CONTEXT)),
KeyBinding::new("enter", Enter, Some(CONTEXT)), KeyBinding::new("enter", Enter { secondary: false }, Some(CONTEXT)),
KeyBinding::new("secondary-enter", Enter { secondary: true }, Some(CONTEXT)),
KeyBinding::new("up", Up, Some(CONTEXT)), KeyBinding::new("up", Up, Some(CONTEXT)),
KeyBinding::new("down", Down, Some(CONTEXT)), KeyBinding::new("down", Down, Some(CONTEXT)),
KeyBinding::new("left", Left, Some(CONTEXT)), KeyBinding::new("left", Left, Some(CONTEXT)),
@ -1004,7 +1013,7 @@ impl TextInput {
self.pause_blink_cursor(cx); self.pause_blink_cursor(cx);
} }
fn enter(&mut self, _: &Enter, window: &mut Window, cx: &mut Context<Self>) { fn enter(&mut self, action: &Enter, window: &mut Window, cx: &mut Context<Self>) {
if self.is_multi_line() { if self.is_multi_line() {
let is_eof = self.selected_range.end == self.text.len(); let is_eof = self.selected_range.end == self.text.len();
self.replace_text_in_range(None, "\n", window, cx); self.replace_text_in_range(None, "\n", window, cx);
@ -1017,7 +1026,9 @@ impl TextInput {
self.move_to(new_offset, window, cx); self.move_to(new_offset, window, cx);
} }
cx.emit(InputEvent::PressEnter); cx.emit(InputEvent::PressEnter {
secondary: action.secondary,
});
} }
fn check_to_auto_grow(&mut self, _: &mut Window, cx: &mut Context<Self>) { fn check_to_auto_grow(&mut self, _: &mut Window, cx: &mut Context<Self>) {

View file

@ -11,6 +11,8 @@ mod title_bar;
mod virtual_list; mod virtual_list;
mod window_border; mod window_border;
pub(crate) mod actions;
pub mod accordion; pub mod accordion;
pub mod alert; pub mod alert;
pub mod animation; pub mod animation;

View file

@ -1,6 +1,7 @@
use std::time::Duration; use std::time::Duration;
use std::{cell::Cell, rc::Rc}; use std::{cell::Cell, rc::Rc};
use crate::actions::{Cancel, Confirm, SelectNext, SelectPrev};
use crate::Icon; use crate::Icon;
use crate::{ use crate::{
input::{InputEvent, TextInput}, input::{InputEvent, TextInput},
@ -8,23 +9,22 @@ use crate::{
v_flex, ActiveTheme, IconName, Size, v_flex, ActiveTheme, IconName, Size,
}; };
use gpui::{ use gpui::{
actions, div, prelude::FluentBuilder, uniform_list, AnyElement, AppContext, Entity, div, prelude::FluentBuilder, uniform_list, AnyElement, AppContext, Entity, FocusHandle,
FocusHandle, Focusable, InteractiveElement, IntoElement, KeyBinding, Length, Focusable, InteractiveElement, IntoElement, KeyBinding, Length, ListSizingBehavior,
ListSizingBehavior, MouseButton, ParentElement, Render, SharedString, Styled, Task, MouseButton, ParentElement, Render, SharedString, Styled, Task, UniformListScrollHandle,
UniformListScrollHandle, Window, Window,
}; };
use gpui::{px, App, Context, EventEmitter, ScrollStrategy, Subscription}; use gpui::{px, App, Context, EventEmitter, MouseDownEvent, ScrollStrategy, Subscription};
use smol::Timer; use smol::Timer;
use super::loading::Loading; use super::loading::Loading;
actions!(list, [Cancel, Confirm, SelectPrev, SelectNext]);
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
let context: Option<&str> = Some("List"); let context: Option<&str> = Some("List");
cx.bind_keys([ cx.bind_keys([
KeyBinding::new("escape", Cancel, context), KeyBinding::new("escape", Cancel, context),
KeyBinding::new("enter", Confirm, context), KeyBinding::new("enter", Confirm { secondary: false }, context),
KeyBinding::new("secondary-enter", Confirm { secondary: true }, context),
KeyBinding::new("up", SelectPrev, context), KeyBinding::new("up", SelectPrev, context),
KeyBinding::new("down", SelectNext, context), KeyBinding::new("down", SelectNext, context),
]); ]);
@ -112,7 +112,9 @@ pub trait ListDelegate: Sized + 'static {
); );
/// Set the confirm and give the selected index, this is means user have clicked the item or pressed Enter. /// Set the confirm and give the selected index, this is means user have clicked the item or pressed Enter.
fn confirm(&mut self, ix: usize, window: &mut Window, cx: &mut Context<List<Self>>) {} ///
/// This will always to `set_selected_index` before confirm.
fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<List<Self>>) {}
/// Cancel the selection, e.g.: Pressed ESC. /// Cancel the selection, e.g.: Pressed ESC.
fn cancel(&mut self, window: &mut Window, cx: &mut Context<List<Self>>) {} fn cancel(&mut self, window: &mut Window, cx: &mut Context<List<Self>>) {}
@ -347,7 +349,13 @@ where
}); });
}); });
} }
InputEvent::PressEnter => self.on_action_confirm(&Confirm, window, cx), InputEvent::PressEnter { secondary } => self.on_action_confirm(
&Confirm {
secondary: *secondary,
},
window,
cx,
),
_ => {} _ => {}
} }
} }
@ -389,15 +397,25 @@ where
} }
fn on_action_cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) { fn on_action_cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
if self.selected_index.is_none() {
cx.propagate();
}
if self.reset_on_cancel { if self.reset_on_cancel {
self.set_selected_index(None, window, cx); self.set_selected_index(None, window, cx);
} }
self.delegate.cancel(window, cx); self.delegate.cancel(window, cx);
cx.emit(ListEvent::Cancel); cx.emit(ListEvent::Cancel);
cx.notify(); cx.notify();
} }
fn on_action_confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) { fn on_action_confirm(
&mut self,
confirm: &Confirm,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.delegate.items_count(cx) == 0 { if self.delegate.items_count(cx) == 0 {
return; return;
} }
@ -406,7 +424,9 @@ where
return; return;
}; };
self.delegate.confirm(ix, window, cx); self.delegate
.set_selected_index(self.selected_index, window, cx);
self.delegate.confirm(confirm.secondary, window, cx);
cx.emit(ListEvent::Confirm(ix)); cx.emit(ListEvent::Confirm(ix));
cx.notify(); cx.notify();
} }
@ -496,10 +516,16 @@ where
}) })
.on_mouse_down( .on_mouse_down(
MouseButton::Left, MouseButton::Left,
cx.listener(move |this, _, window, cx| { cx.listener(move |this, ev: &MouseDownEvent, window, cx| {
this.right_clicked_index = None; this.right_clicked_index = None;
this.selected_index = Some(ix); this.selected_index = Some(ix);
this.on_action_confirm(&Confirm, window, cx); this.on_action_confirm(
&Confirm {
secondary: ev.modifiers.secondary(),
},
window,
cx,
);
}), }),
) )
.on_mouse_down( .on_mouse_down(

View file

@ -1,26 +1,25 @@
use std::{rc::Rc, time::Duration}; use std::{rc::Rc, time::Duration};
use gpui::{ use gpui::{
actions, anchored, div, hsla, point, prelude::FluentBuilder, px, relative, Animation, anchored, div, hsla, point, prelude::FluentBuilder, px, relative, Animation, AnimationExt as _,
AnimationExt as _, AnyElement, App, Bounds, ClickEvent, Div, FocusHandle, Hsla, AnyElement, App, Bounds, ClickEvent, Div, FocusHandle, Hsla, InteractiveElement, IntoElement,
InteractiveElement, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point, KeyBinding, MouseButton, ParentElement, Pixels, Point, RenderOnce, SharedString, Styled,
RenderOnce, SharedString, Styled, Window, Window,
}; };
use rust_i18n::t; use rust_i18n::t;
use crate::{ use crate::{
actions::{Cancel, Confirm},
animation::cubic_bezier, animation::cubic_bezier,
button::{Button, ButtonVariant, ButtonVariants as _}, button::{Button, ButtonVariant, ButtonVariants as _},
h_flex, v_flex, ActiveTheme as _, ContextModal, IconName, Sizable as _, StyledExt, h_flex, v_flex, ActiveTheme as _, ContextModal, IconName, Sizable as _, StyledExt,
}; };
actions!(modal, [Escape, Enter]);
const CONTEXT: &str = "Modal"; const CONTEXT: &str = "Modal";
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
cx.bind_keys([ cx.bind_keys([
KeyBinding::new("escape", Escape, Some(CONTEXT)), KeyBinding::new("escape", Cancel, Some(CONTEXT)),
KeyBinding::new("enter", Enter, Some(CONTEXT)), KeyBinding::new("enter", Confirm { secondary: false }, Some(CONTEXT)),
]); ]);
} }
@ -382,7 +381,7 @@ impl RenderOnce for Modal {
this.on_action({ this.on_action({
let on_cancel = on_cancel.clone(); let on_cancel = on_cancel.clone();
let on_close = on_close.clone(); let on_close = on_close.clone();
move |_: &Escape, window, cx| { move |_: &Cancel, window, cx| {
// FIXME: // FIXME:
// //
// Here some Modal have no focus_handle, so it will not work will Escape key. // Here some Modal have no focus_handle, so it will not work will Escape key.
@ -395,7 +394,7 @@ impl RenderOnce for Modal {
.on_action({ .on_action({
let on_ok = on_ok.clone(); let on_ok = on_ok.clone();
let on_close = on_close.clone(); let on_close = on_close.clone();
move |_: &Enter, window, cx| { move |_: &Confirm, window, cx| {
if on_ok(&ClickEvent::default(), window, cx) { if on_ok(&ClickEvent::default(), window, cx) {
on_close(&ClickEvent::default(), window, cx); on_close(&ClickEvent::default(), window, cx);
window.close_modal(cx); window.close_modal(cx);

View file

@ -1,20 +1,18 @@
use gpui::{ use gpui::{
actions, anchored, deferred, div, prelude::FluentBuilder as _, px, AnyElement, App, Bounds, anchored, deferred, div, prelude::FluentBuilder as _, px, AnyElement, App, Bounds, Context,
Context, Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity, EventEmitter, Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity, EventEmitter, FocusHandle,
FocusHandle, Focusable, GlobalElementId, Hitbox, InteractiveElement as _, IntoElement, Focusable, GlobalElementId, Hitbox, InteractiveElement as _, IntoElement, KeyBinding, LayoutId,
KeyBinding, LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render, Style,
Render, Style, StyleRefinement, Styled, Window, StyleRefinement, Styled, Window,
}; };
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
use crate::{Selectable, StyledExt as _}; use crate::{actions::Cancel, Selectable, StyledExt as _};
const CONTEXT: &str = "Popover"; const CONTEXT: &str = "Popover";
actions!(popover, [Escape]);
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
cx.bind_keys([KeyBinding::new("escape", Escape, Some(CONTEXT))]) cx.bind_keys([KeyBinding::new("escape", Cancel, Some(CONTEXT))])
} }
pub struct PopoverContent { pub struct PopoverContent {
@ -55,7 +53,10 @@ impl Render for PopoverContent {
div() div()
.track_focus(&self.focus_handle) .track_focus(&self.focus_handle)
.key_context(CONTEXT) .key_context(CONTEXT)
.on_action(cx.listener(|_, _: &Escape, _, cx| cx.emit(DismissEvent))) .on_action(cx.listener(|_, _: &Cancel, _, cx| {
cx.propagate();
cx.emit(DismissEvent);
}))
.p_2() .p_2()
.when_some(self.max_width, |this, v| this.max_w(v)) .when_some(self.max_width, |this, v| this.max_w(v))
.child(self.content.clone()(window, cx)) .child(self.content.clone()(window, cx))

View file

@ -1,3 +1,4 @@
use crate::actions::{Cancel, Confirm, SelectNext, SelectPrev};
use crate::scroll::{Scrollbar, ScrollbarState}; use crate::scroll::{Scrollbar, ScrollbarState};
use crate::{ use crate::{
button::Button, h_flex, list::ListItem, popover::Popover, v_flex, ActiveTheme, Icon, IconName, button::Button, h_flex, list::ListItem, popover::Popover, v_flex, ActiveTheme, Icon, IconName,
@ -6,24 +7,22 @@ use crate::{
use crate::{Kbd, StyledExt}; use crate::{Kbd, StyledExt};
use gpui::Subscription; use gpui::Subscription;
use gpui::{ use gpui::{
actions, anchored, canvas, div, prelude::FluentBuilder, px, rems, Action, AnyElement, App, anchored, canvas, div, prelude::FluentBuilder, px, rems, Action, AnyElement, App, AppContext,
AppContext, Bounds, Context, Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle, Bounds, Context, Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle, Focusable,
Focusable, InteractiveElement, IntoElement, KeyBinding, ParentElement, Pixels, Render, InteractiveElement, IntoElement, KeyBinding, ParentElement, Pixels, Render, ScrollHandle,
ScrollHandle, SharedString, StatefulInteractiveElement, Styled, WeakEntity, Window, SharedString, StatefulInteractiveElement, Styled, WeakEntity, Window,
}; };
use std::cell::Cell; use std::cell::Cell;
use std::ops::Deref; use std::ops::Deref;
use std::rc::Rc; use std::rc::Rc;
actions!(menu, [Confirm, Dismiss, SelectNext, SelectPrev]);
const ITEM_HEIGHT: Pixels = px(26.); const ITEM_HEIGHT: Pixels = px(26.);
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
let context = Some("PopupMenu"); let context = Some("PopupMenu");
cx.bind_keys([ cx.bind_keys([
KeyBinding::new("enter", Confirm, context), KeyBinding::new("enter", Confirm { secondary: false }, context),
KeyBinding::new("escape", Dismiss, context), KeyBinding::new("escape", Cancel, context),
KeyBinding::new("up", SelectPrev, context), KeyBinding::new("up", SelectPrev, context),
KeyBinding::new("down", SelectNext, context), KeyBinding::new("down", SelectNext, context),
]); ]);
@ -127,7 +126,7 @@ impl PopupMenu {
let _subscriptions = let _subscriptions =
vec![ vec![
cx.on_blur(&focus_handle, window, |this: &mut PopupMenu, window, cx| { cx.on_blur(&focus_handle, window, |this: &mut PopupMenu, window, cx| {
this.dismiss(&Dismiss, window, cx) this.dismiss(&Cancel, window, cx)
}), }),
]; ];
@ -544,7 +543,7 @@ impl PopupMenu {
cx.stop_propagation(); cx.stop_propagation();
window.prevent_default(); window.prevent_default();
self.selected_index = Some(ix); self.selected_index = Some(ix);
self.confirm(&Confirm, window, cx); self.confirm(&Confirm { secondary: false }, window, cx);
} }
fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) { fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
@ -554,11 +553,11 @@ impl PopupMenu {
match item { match item {
Some(PopupMenuItem::Item { handler, .. }) => { Some(PopupMenuItem::Item { handler, .. }) => {
handler(window, cx); handler(window, cx);
self.dismiss(&Dismiss, window, cx) self.dismiss(&Cancel, window, cx)
} }
Some(PopupMenuItem::ElementItem { handler, .. }) => { Some(PopupMenuItem::ElementItem { handler, .. }) => {
handler(window, cx); handler(window, cx);
self.dismiss(&Dismiss, window, cx) self.dismiss(&Cancel, window, cx)
} }
_ => {} _ => {}
} }
@ -601,7 +600,7 @@ impl PopupMenu {
} }
} }
fn dismiss(&mut self, _: &Dismiss, window: &mut Window, cx: &mut Context<Self>) { fn dismiss(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
if self.active_submenu().is_some() { if self.active_submenu().is_some() {
return; return;
} }
@ -620,7 +619,7 @@ impl PopupMenu {
// Dismiss parent menu, when this menu is dismissed // Dismiss parent menu, when this menu is dismissed
_ = parent_menu.update(cx, |view, cx| { _ = parent_menu.update(cx, |view, cx| {
view.hovered_menu_ix = None; view.hovered_menu_ix = None;
view.dismiss(&Dismiss, window, cx); view.dismiss(&Cancel, window, cx);
}); });
} }
@ -883,9 +882,7 @@ impl Render for PopupMenu {
.on_action(cx.listener(Self::select_prev)) .on_action(cx.listener(Self::select_prev))
.on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::dismiss)) .on_action(cx.listener(Self::dismiss))
.on_mouse_down_out( .on_mouse_down_out(cx.listener(|this, _, window, cx| this.dismiss(&Cancel, window, cx)))
cx.listener(|this, _, window, cx| this.dismiss(&Dismiss, window, cx)),
)
.popover_style(cx) .popover_style(cx)
.text_color(cx.theme().popover_foreground) .text_color(cx.theme().popover_foreground)
.relative() .relative()

View file

@ -1,6 +1,7 @@
use std::{cell::Cell, ops::Range, rc::Rc, time::Duration}; use std::{cell::Cell, ops::Range, rc::Rc, time::Duration};
use crate::{ use crate::{
actions::{Cancel, SelectNext, SelectPrev},
context_menu::ContextMenuExt, context_menu::ContextMenuExt,
h_flex, h_flex,
popup_menu::PopupMenu, popup_menu::PopupMenu,
@ -18,16 +19,7 @@ use gpui::{
mod loading; mod loading;
actions!( actions!(table, [SelectPrevColumn, SelectNextColumn]);
table,
[
Cancel,
SelectPrev,
SelectNext,
SelectPrevColumn,
SelectNextColumn
]
);
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
let context = Some("Table"); let context = Some("Table");

View file

@ -8,8 +8,8 @@ use gpui::{
use rust_i18n::t; use rust_i18n::t;
use crate::{ use crate::{
actions::Cancel,
button::{Button, ButtonVariants as _}, button::{Button, ButtonVariants as _},
dropdown::Escape,
h_flex, h_flex,
input::clear_button, input::clear_button,
v_flex, ActiveTheme, Icon, IconName, Sizable, Size, StyleSized as _, StyledExt as _, v_flex, ActiveTheme, Icon, IconName, Sizable, Size, StyleSized as _, StyledExt as _,
@ -19,7 +19,7 @@ use super::calendar::{Calendar, CalendarEvent, Date, Matcher};
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
let context = Some("DatePicker"); let context = Some("DatePicker");
cx.bind_keys([KeyBinding::new("escape", Escape, context)]) cx.bind_keys([KeyBinding::new("escape", Cancel, context)])
} }
#[derive(Clone)] #[derive(Clone)]
@ -208,7 +208,11 @@ impl DatePicker {
cx.notify(); cx.notify();
} }
fn escape(&mut self, _: &Escape, window: &mut Window, cx: &mut Context<Self>) { fn escape(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
if !self.open {
cx.propagate();
}
self.focus_back_if_need(window, cx); self.focus_back_if_need(window, cx);
self.open = false; self.open = false;
@ -364,7 +368,7 @@ impl Render for DatePicker {
.on_mouse_up_out( .on_mouse_up_out(
MouseButton::Left, MouseButton::Left,
cx.listener(|view, _, window, cx| { cx.listener(|view, _, window, cx| {
view.escape(&Escape, window, cx); view.escape(&Cancel, window, cx);
}), }),
) )
.child( .child(