Add up, down, enter, esc keybinding for Dropdown.

This commit is contained in:
Jason Lee 2024-06-27 23:53:46 +08:00
parent 720672b0e2
commit 63076725f2
4 changed files with 70 additions and 15 deletions

View file

@ -1,18 +1,30 @@
use std::rc::Rc; use std::rc::Rc;
use gpui::{ use gpui::{
div, prelude::FluentBuilder as _, px, AppContext, Div, ElementId, FocusHandle, FocusableView, actions, div, prelude::FluentBuilder as _, px, AppContext, Div, ElementId, FocusHandle,
InteractiveElement, IntoElement, ParentElement as _, Render, RenderOnce, SharedString, FocusableView, InteractiveElement, IntoElement, KeyBinding, ParentElement as _, Render,
Stateful, StatefulInteractiveElement as _, Styled as _, View, ViewContext, VisualContext as _, RenderOnce, SharedString, Stateful, StatefulInteractiveElement as _, Styled as _, View,
WeakView, ViewContext, VisualContext as _, WeakView,
}; };
actions!(dropdown, [Up, Down, Enter, Escape]);
pub fn init(cx: &mut AppContext) {
let context = Some("Dropdown");
cx.bind_keys([
KeyBinding::new("up", Up, context),
KeyBinding::new("down", Down, context),
KeyBinding::new("enter", Enter, context),
KeyBinding::new("escape", Escape, context),
])
}
use crate::{ use crate::{
h_flex, h_flex,
list::ListItem, list::ListItem,
picker::{Picker, PickerDelegate}, picker::{self, Picker, PickerDelegate},
theme::ActiveTheme, theme::ActiveTheme,
v_flex, IconName, StyledExt, v_flex, IconName,
}; };
/// A trait for items that can be displayed in a dropdown. /// A trait for items that can be displayed in a dropdown.
@ -98,6 +110,7 @@ where
view.value = Some(item.value().to_string().into()); view.value = Some(item.value().to_string().into());
} }
view.open = false; view.open = false;
view.focus_handle.focus(cx);
}); });
} }
} }
@ -139,6 +152,35 @@ where
self.value = Some(value.into()); self.value = Some(value.into());
cx.notify(); cx.notify();
} }
fn up(&mut self, _: &Up, cx: &mut ViewContext<Self>) {
if !self.open {
return;
}
self.picker.focus_handle(cx).focus(cx);
cx.dispatch_action(Box::new(picker::SelectPrev));
}
fn down(&mut self, _: &Down, cx: &mut ViewContext<Self>) {
if !self.open {
self.open = true;
}
self.picker.focus_handle(cx).focus(cx);
cx.dispatch_action(Box::new(picker::SelectNext));
}
fn enter(&mut self, _: &Enter, cx: &mut ViewContext<Self>) {
if !self.open {
self.open = true;
cx.notify();
}
}
fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
self.open = false;
cx.notify();
}
} }
impl<D> FocusableView for Dropdown<D> impl<D> FocusableView for Dropdown<D>
@ -155,11 +197,18 @@ where
D: DropdownDelegate + 'static, D: DropdownDelegate + 'static,
{ {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let group_id = format!("dropdown-group:{}", self.id);
let title = self.title.clone().unwrap_or_else(|| "Select...".into()); let title = self.title.clone().unwrap_or_else(|| "Select...".into());
let is_focused = self.focus_handle.is_focused(cx); let focused = self.focus_handle.is_focused(cx);
div() div()
.key_context("Dropdown") .key_context("Dropdown")
.group(group_id.clone())
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::up))
.on_action(cx.listener(Self::down))
.on_action(cx.listener(Self::enter))
.on_action(cx.listener(Self::escape))
.size_full() .size_full()
.relative() .relative()
.child( .child(
@ -177,7 +226,7 @@ where
.shadow_sm() .shadow_sm()
.px_3() .px_3()
.py_2() .py_2()
.when(is_focused, |this| this.border_color(cx.theme().ring)) .when(focused, |this| this.border_color(cx.theme().ring))
.on_click(cx.listener(|this, _, cx| { .on_click(cx.listener(|this, _, cx| {
this.open = !this.open; this.open = !this.open;
cx.notify(); cx.notify();
@ -204,7 +253,11 @@ where
.rounded(px(cx.theme().radius)) .rounded(px(cx.theme().radius))
.shadow_md() .shadow_md()
.track_focus(&self.picker.focus_handle(cx)) .track_focus(&self.picker.focus_handle(cx))
.child(self.picker.clone()), .child(self.picker.clone())
.on_mouse_down_out(cx.listener(|view, _, cx| {
view.open = false;
cx.notify();
})),
) )
}) })
} }

View file

@ -541,7 +541,7 @@ impl Element for TextElement {
cx: &mut WindowContext, cx: &mut WindowContext,
) { ) {
let focus_handle = self.input.read(cx).focus_handle.clone(); let focus_handle = self.input.read(cx).focus_handle.clone();
let is_focused = focus_handle.is_focused(cx); let focused = focus_handle.is_focused(cx);
cx.handle_input( cx.handle_input(
&focus_handle, &focus_handle,
@ -553,7 +553,7 @@ impl Element for TextElement {
let line = prepaint.line.take().unwrap(); let line = prepaint.line.take().unwrap();
line.paint(bounds.origin, cx.line_height(), cx).unwrap(); line.paint(bounds.origin, cx.line_height(), cx).unwrap();
if is_focused { if focused {
if let Some(cursor) = prepaint.cursor.take() { if let Some(cursor) = prepaint.cursor.take() {
cx.paint_quad(cursor); cx.paint_quad(cursor);
} }

View file

@ -35,4 +35,5 @@ pub use stock::*;
pub fn init(cx: &mut gpui::AppContext) { pub fn init(cx: &mut gpui::AppContext) {
input::init(cx); input::init(cx);
picker::init(cx); picker::init(cx);
dropdown::init(cx);
} }

View file

@ -24,11 +24,12 @@ actions!(
); );
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
let context = Some("Picker");
cx.bind_keys([ cx.bind_keys([
KeyBinding::new("enter", Confirm, None), KeyBinding::new("enter", Confirm, context),
KeyBinding::new("escape", Cancel, None), KeyBinding::new("escape", Cancel, context),
KeyBinding::new("up", SelectPrev, None), KeyBinding::new("up", SelectPrev, context),
KeyBinding::new("down", SelectNext, None), KeyBinding::new("down", SelectNext, context),
]); ]);
} }