From 63076725f25abede24dcecac6e1d9b3e448658a6 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 27 Jun 2024 23:53:46 +0800 Subject: [PATCH] Add up, down, enter, esc keybinding for Dropdown. --- crates/ui/src/dropdown.rs | 71 ++++++++++++++++++++++++++++++++++----- crates/ui/src/input.rs | 4 +-- crates/ui/src/lib.rs | 1 + crates/ui/src/picker.rs | 9 ++--- 4 files changed, 70 insertions(+), 15 deletions(-) diff --git a/crates/ui/src/dropdown.rs b/crates/ui/src/dropdown.rs index 3375a820..6bc6b1ef 100644 --- a/crates/ui/src/dropdown.rs +++ b/crates/ui/src/dropdown.rs @@ -1,18 +1,30 @@ use std::rc::Rc; use gpui::{ - div, prelude::FluentBuilder as _, px, AppContext, Div, ElementId, FocusHandle, FocusableView, - InteractiveElement, IntoElement, ParentElement as _, Render, RenderOnce, SharedString, - Stateful, StatefulInteractiveElement as _, Styled as _, View, ViewContext, VisualContext as _, - WeakView, + actions, div, prelude::FluentBuilder as _, px, AppContext, Div, ElementId, FocusHandle, + FocusableView, InteractiveElement, IntoElement, KeyBinding, ParentElement as _, Render, + RenderOnce, SharedString, Stateful, StatefulInteractiveElement as _, Styled as _, View, + 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::{ h_flex, list::ListItem, - picker::{Picker, PickerDelegate}, + picker::{self, Picker, PickerDelegate}, theme::ActiveTheme, - v_flex, IconName, StyledExt, + v_flex, IconName, }; /// 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.open = false; + view.focus_handle.focus(cx); }); } } @@ -139,6 +152,35 @@ where self.value = Some(value.into()); cx.notify(); } + + fn up(&mut self, _: &Up, cx: &mut ViewContext) { + 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) { + 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) { + if !self.open { + self.open = true; + cx.notify(); + } + } + + fn escape(&mut self, _: &Escape, cx: &mut ViewContext) { + self.open = false; + cx.notify(); + } } impl FocusableView for Dropdown @@ -155,11 +197,18 @@ where D: DropdownDelegate + 'static, { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let group_id = format!("dropdown-group:{}", self.id); 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() .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() .relative() .child( @@ -177,7 +226,7 @@ where .shadow_sm() .px_3() .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| { this.open = !this.open; cx.notify(); @@ -204,7 +253,11 @@ where .rounded(px(cx.theme().radius)) .shadow_md() .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(); + })), ) }) } diff --git a/crates/ui/src/input.rs b/crates/ui/src/input.rs index 50f02b56..cb152510 100644 --- a/crates/ui/src/input.rs +++ b/crates/ui/src/input.rs @@ -541,7 +541,7 @@ impl Element for TextElement { cx: &mut WindowContext, ) { 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( &focus_handle, @@ -553,7 +553,7 @@ impl Element for TextElement { let line = prepaint.line.take().unwrap(); line.paint(bounds.origin, cx.line_height(), cx).unwrap(); - if is_focused { + if focused { if let Some(cursor) = prepaint.cursor.take() { cx.paint_quad(cursor); } diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 75eac377..b28dc30c 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -35,4 +35,5 @@ pub use stock::*; pub fn init(cx: &mut gpui::AppContext) { input::init(cx); picker::init(cx); + dropdown::init(cx); } diff --git a/crates/ui/src/picker.rs b/crates/ui/src/picker.rs index 69ba5321..2a2b8687 100644 --- a/crates/ui/src/picker.rs +++ b/crates/ui/src/picker.rs @@ -24,11 +24,12 @@ actions!( ); pub fn init(cx: &mut AppContext) { + let context = Some("Picker"); cx.bind_keys([ - KeyBinding::new("enter", Confirm, None), - KeyBinding::new("escape", Cancel, None), - KeyBinding::new("up", SelectPrev, None), - KeyBinding::new("down", SelectNext, None), + KeyBinding::new("enter", Confirm, context), + KeyBinding::new("escape", Cancel, context), + KeyBinding::new("up", SelectPrev, context), + KeyBinding::new("down", SelectNext, context), ]); }