Add Dropdown.

This commit is contained in:
Jason Lee 2024-06-27 20:13:29 +08:00
parent b3a32bc02c
commit 910f27a66a
7 changed files with 221 additions and 131 deletions

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down">
<path d="m6 9 6 6 6-6"/>
</svg>

After

Width:  |  Height:  |  Size: 250 B

View file

@ -1,13 +1,13 @@
use std::{rc::Rc, vec}; use std::{rc::Rc, vec};
use gpui::{ use gpui::{
div, ClickEvent, IntoElement, ParentElement, Render, RenderOnce, SharedString, Styled, div, px, ClickEvent, IntoElement, ParentElement, Render, RenderOnce, SharedString, Styled,
ViewContext, WindowContext, View, ViewContext, VisualContext, WindowContext,
}; };
use ui::{ use ui::{
checkbox::Checkbox, checkbox::Checkbox,
dropdown::{Dropdown, DropdownItem}, dropdown::{Dropdown, DropdownDelegate, DropdownItem},
h_flex, v_flex, Disableable as _, Selection, h_flex, v_flex, Disableable as _, Selection,
}; };
@ -25,25 +25,54 @@ impl Country {
} }
impl DropdownItem for Country { impl DropdownItem for Country {
fn title(&self) -> SharedString { fn title(&self) -> &str {
self.name.into() self.name
} }
fn value(&self) -> SharedString { fn value(&self) -> &str {
self.code.into() self.code
} }
} }
#[derive(IntoElement)] struct CounterDelegate(Vec<Country>);
struct FuritDelegate(Vec<String>);
pub struct DropdownStory { pub struct DropdownStory {
dropdown1: Dropdown, country_dropdown: View<Dropdown<CounterDelegate>>,
dropdown2: Dropdown, furit_dropdown: View<Dropdown<FuritDelegate>>,
countries: Vec<Country>, }
impl DropdownDelegate for CounterDelegate {
fn len(&self) -> usize {
self.0.len()
}
fn get(&self, ix: usize) -> Option<&dyn DropdownItem> {
if let Some(item) = self.0.get(ix) {
Some(item)
} else {
None
}
}
}
impl DropdownDelegate for FuritDelegate {
fn len(&self) -> usize {
self.0.len()
}
fn get(&self, ix: usize) -> Option<&dyn DropdownItem> {
if let Some(item) = self.0.get(ix) {
Some(item)
} else {
None
}
}
} }
impl DropdownStory { impl DropdownStory {
pub(crate) fn new(cx: &mut WindowContext) -> Self { pub(crate) fn new(cx: &mut ViewContext<Self>) -> Self {
let countries = vec![ let countries = CounterDelegate(vec![
Country::new("United States", "US"), Country::new("United States", "US"),
Country::new("Canada", "CA"), Country::new("Canada", "CA"),
Country::new("Mexico", "MX"), Country::new("Mexico", "MX"),
@ -55,22 +84,29 @@ impl DropdownStory {
Country::new("Colombia", "CO"), Country::new("Colombia", "CO"),
Country::new("Venezuela", "VE"), Country::new("Venezuela", "VE"),
Country::new("Ecuador", "EC"), Country::new("Ecuador", "EC"),
]; ]);
let items2 = vec![ let country_dropdown = cx.new_view(|cx| Dropdown::new("dropdown-country", countries, cx));
"Apple",
"Orange", let furits = FuritDelegate(
"Banana", [
"Grape", "Apple",
"Pineapple", "Orange",
"Watermelon", "Banana",
"Avocado", "Grape",
]; "Pineapple",
"Watermelon",
"Avocado",
]
.iter()
.map(|s| s.to_string())
.collect(),
);
let furit_dropdown = cx.new_view(|cx| Dropdown::new("dropdown-furits", furits, cx));
Self { Self {
countries, country_dropdown,
dropdown1: Dropdown::new("dropdown-country", Rc::new(countries), cx), furit_dropdown,
dropdown2: Dropdown::new("dropdown-fruit", Rc::new(items2), cx),
} }
} }
@ -80,73 +116,34 @@ impl DropdownStory {
} }
} }
impl RenderOnce for CheckboxStory { impl Render for DropdownStory {
fn render(self, cx: &mut WindowContext) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
story_case( story_case(
"Checkbox", "Dropdown",
"A control that allows the user to toggle between checked and not checked.", "Displays a list of options for the user to pick from—triggered by a button.",
) )
.child( .child(
v_flex().items_start().justify_start().gap_4().child( v_flex()
h_flex() .size_full()
.items_center() .items_start()
.gap_4() .justify_start()
.child(self.check1)
.child(self.check1_1)
.child(
Checkbox::new("check1_2", cx)
.checked(Selection::Selected)
.on_click(Self::on_click),
),
),
)
.child(
h_flex()
.items_center()
.gap_4() .gap_4()
.child( .child(
Checkbox::new("check2", cx) h_flex()
.checked(Selection::Unselected) .w_full()
.label("With label (Unchecked)") .max_w(px(640.))
.on_click(Self::on_click), .items_center()
.gap_4()
.child(self.country_dropdown.clone())
.child(self.furit_dropdown.clone()),
) )
.child( .child(
Checkbox::new("check2_1", cx) h_flex()
.label("With Label (Indeterminate)") .w_full()
.checked(Selection::Indeterminate) .items_center()
.on_click(Self::on_click), .gap_4()
) .child("This is other text."),
.child(
Checkbox::new("check2_2", cx)
.label("With Label (Checked)")
.checked(Selection::Selected)
.on_click(Self::on_click),
), ),
) )
.child(
h_flex().items_center().gap_4().child(
h_flex()
.items_center()
.gap_4()
.child(
Checkbox::new("check3", cx)
.label("Disabled Checked")
.checked(Selection::Selected)
.disabled(true),
)
.child(
Checkbox::new("check3_1", cx)
.label("Disabled Unchecked")
.checked(Selection::Unselected)
.disabled(true),
)
.child(
Checkbox::new("check3_2", cx)
.label("Disabled Indeterminate")
.checked(Selection::Indeterminate)
.disabled(true),
),
),
)
} }
} }

View file

@ -9,7 +9,7 @@ use gpui::{
mod button_story; mod button_story;
mod checkbox_story; mod checkbox_story;
// mod dropdown_story; mod dropdown_story;
mod input_story; mod input_story;
mod picker_story; mod picker_story;
mod switch_story; mod switch_story;
@ -21,6 +21,7 @@ use ui::{
}; };
use button_story::ButtonStory; use button_story::ButtonStory;
use dropdown_story::DropdownStory;
use input_story::InputStory; use input_story::InputStory;
use picker_story::PickerStory; use picker_story::PickerStory;
use switch_story::SwitchStory; use switch_story::SwitchStory;
@ -77,6 +78,7 @@ enum StoryType {
Checkbox, Checkbox,
Switch, Switch,
Picker, Picker,
Dropdown,
} }
impl Display for StoryType { impl Display for StoryType {
@ -87,6 +89,7 @@ impl Display for StoryType {
Self::Checkbox => write!(f, "Checkbox"), Self::Checkbox => write!(f, "Checkbox"),
Self::Switch => write!(f, "Switch"), Self::Switch => write!(f, "Switch"),
Self::Picker => write!(f, "Picker"), Self::Picker => write!(f, "Picker"),
Self::Dropdown => write!(f, "Dropdown"),
} }
} }
} }
@ -98,6 +101,7 @@ pub struct Stories {
input_story: View<InputStory>, input_story: View<InputStory>,
switch_story: View<SwitchStory>, switch_story: View<SwitchStory>,
picker_story: View<PickerStory>, picker_story: View<PickerStory>,
dropdown_story: View<DropdownStory>,
} }
impl Stories { impl Stories {
@ -108,6 +112,7 @@ impl Stories {
input_story: cx.new_view(|cx| InputStory::new(cx)), input_story: cx.new_view(|cx| InputStory::new(cx)),
switch_story: cx.new_view(|cx| SwitchStory::new(cx)), switch_story: cx.new_view(|cx| SwitchStory::new(cx)),
picker_story: cx.new_view(|cx| PickerStory::new(cx)), picker_story: cx.new_view(|cx| PickerStory::new(cx)),
dropdown_story: cx.new_view(|cx| DropdownStory::new(cx)),
} }
} }
@ -120,27 +125,23 @@ impl Stories {
cx.notify(); cx.notify();
} }
fn render_story_buttons(&self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn tabs(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div() div()
.flex() .flex()
.items_center() .items_center()
.gap_4() .gap_4()
.w_full() .w_full()
.child(TabBar::new("story-tabs").children(vec![ .child(TabBar::new("story-tabs").children(vec![
self.swith_button("story-button", StoryType::Button, cx), self.tab("story-button", StoryType::Button, cx),
self.swith_button("story-input", StoryType::Input, cx), self.tab("story-input", StoryType::Input, cx),
self.swith_button("story-checkbox", StoryType::Checkbox, cx), self.tab("story-checkbox", StoryType::Checkbox, cx),
self.swith_button("story-switch", StoryType::Switch, cx), self.tab("story-switch", StoryType::Switch, cx),
self.swith_button("story-picker", StoryType::Picker, cx), self.tab("story-picker", StoryType::Picker, cx),
self.tab("story-dropdown", StoryType::Dropdown, cx),
])) ]))
} }
fn swith_button( fn tab(&self, id: &str, ty: StoryType, cx: &mut ViewContext<Self>) -> impl IntoElement {
&self,
id: &str,
ty: StoryType,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
let name = format!("{}", ty); let name = format!("{}", ty);
let is_active = ty == self.active; let is_active = ty == self.active;
@ -159,13 +160,14 @@ impl Render for Stories {
.flex() .flex()
.flex_col() .flex_col()
.gap_4() .gap_4()
.child(self.render_story_buttons(cx)) .child(self.tabs(cx))
.map(|this| match self.active { .map(|this| match self.active {
StoryType::Button => this.child(self.button_story.clone()), StoryType::Button => this.child(self.button_story.clone()),
StoryType::Input => this.child(self.input_story.clone()), StoryType::Input => this.child(self.input_story.clone()),
StoryType::Checkbox => this.child(CheckboxStory::new(cx).into_any_element()), StoryType::Checkbox => this.child(CheckboxStory::new(cx).into_any_element()),
StoryType::Switch => this.child(self.switch_story.clone()), StoryType::Switch => this.child(self.switch_story.clone()),
StoryType::Picker => this.child(self.picker_story.clone()), StoryType::Picker => this.child(self.picker_story.clone()),
StoryType::Dropdown => this.child(self.dropdown_story.clone()),
}) })
} }
} }

View file

@ -1,15 +1,18 @@
use std::rc::Rc; use std::rc::Rc;
use gpui::{ use gpui::{
div, prelude::FluentBuilder as _, Div, ElementId, InteractiveElement, IntoElement, div, prelude::FluentBuilder as _, px, AppContext, Div, ElementId, FocusHandle, FocusableView,
ParentElement as _, Render, RenderOnce, SharedString, Stateful, Styled as _, View, ViewContext, InteractiveElement, IntoElement, ParentElement as _, Render, RenderOnce, SharedString,
VisualContext as _, WeakView, Stateful, StatefulInteractiveElement as _, Styled as _, View, ViewContext, VisualContext as _,
WeakView,
}; };
use crate::{ use crate::{
h_flex,
list::ListItem, list::ListItem,
picker::{Picker, PickerDelegate}, picker::{Picker, PickerDelegate},
IconName, theme::ActiveTheme,
v_flex, IconName, StyledExt,
}; };
/// A trait for items that can be displayed in a dropdown. /// A trait for items that can be displayed in a dropdown.
@ -18,19 +21,34 @@ pub trait DropdownItem {
fn value(&self) -> &str; fn value(&self) -> &str;
} }
pub trait DropdownDelegate { impl DropdownItem for String {
type Item: DropdownItem; fn title(&self) -> &str {
self
}
fn len(&self) -> usize; fn value(&self) -> &str {
fn get(&self, index: usize) -> Option<&Self::Item>; self
}
} }
struct DropdownPickerDelegate<D: DropdownDelegate> { pub trait DropdownDelegate {
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
fn get(&self, ix: usize) -> Option<&dyn DropdownItem>;
}
struct DropdownPickerDelegate<D: DropdownDelegate + 'static> {
delegate: D,
dropdown: WeakView<Dropdown<D>>, dropdown: WeakView<Dropdown<D>>,
selected_index: usize, selected_index: usize,
} }
impl<D> PickerDelegate for DropdownPickerDelegate<D: PickerDelegate> { impl<D> PickerDelegate for DropdownPickerDelegate<D>
where
D: DropdownDelegate + 'static,
{
type ListItem = ListItem; type ListItem = ListItem;
fn match_count(&self) -> usize { fn match_count(&self) -> usize {
@ -57,7 +75,7 @@ impl<D> PickerDelegate for DropdownPickerDelegate<D: PickerDelegate> {
.selected(selected) .selected(selected)
.py_1() .py_1()
.px_3() .px_3()
.child(item.title()); .child(item.title().to_string());
Some(list_item) Some(list_item)
} else { } else {
None None
@ -68,7 +86,6 @@ impl<D> PickerDelegate for DropdownPickerDelegate<D: PickerDelegate> {
if let Some(view) = self.dropdown.upgrade() { if let Some(view) = self.dropdown.upgrade() {
cx.update_view(&view, |view, cx| { cx.update_view(&view, |view, cx| {
view.open = false; view.open = false;
cx.notify();
}); });
} }
} }
@ -77,47 +94,117 @@ impl<D> PickerDelegate for DropdownPickerDelegate<D: PickerDelegate> {
if let Some(view) = self.dropdown.upgrade() { if let Some(view) = self.dropdown.upgrade() {
cx.update_view(&view, |view, cx| { cx.update_view(&view, |view, cx| {
if let Some(item) = self.delegate.get(self.selected_index) { if let Some(item) = self.delegate.get(self.selected_index) {
view.value = Some(item.value()); view.title = Some(item.title().to_string().into());
view.value = Some(item.value().to_string().into());
} }
view.open = false; view.open = false;
cx.notify();
}); });
} }
} }
} }
pub struct Dropdown<D: DropdownDelegate> { pub struct Dropdown<D: DropdownDelegate + 'static> {
base: Stateful<Div>, id: ElementId,
delegate: D, focus_handle: FocusHandle,
picker: View<Picker<DropdownPickerDelegate>>, picker: View<Picker<DropdownPickerDelegate<D>>>,
open: bool, open: bool,
/// The value of the selected item. /// The value of the selected item.
value: Option<SharedString>, value: Option<SharedString>,
title: Option<SharedString>,
} }
impl<D: DropdownDelegate> Dropdown<D> { impl<D> Dropdown<D>
where
D: DropdownDelegate + 'static,
{
pub fn new(id: impl Into<ElementId>, delegate: D, cx: &mut ViewContext<Self>) -> Self { pub fn new(id: impl Into<ElementId>, delegate: D, cx: &mut ViewContext<Self>) -> Self {
let delegate = DropdownPickerDelegate { let picker_delegate = DropdownPickerDelegate {
// delegate, delegate,
dropdown: cx.view().downgrade(), dropdown: cx.view().downgrade(),
selected_index: 0, selected_index: 0,
}; };
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx)); let picker = cx.new_view(|cx| Picker::uniform_list(picker_delegate, cx));
Self { Self {
delegate, id: id.into(),
base: div().id(id.into()), focus_handle: cx.focus_handle(),
picker, picker,
open: false, open: false,
title: None,
value: None, value: None,
} }
} }
}
impl RenderOnce for Dropdown { pub fn set_value(&mut self, value: impl Into<SharedString>, cx: &mut ViewContext<Self>) {
fn render(self, cx: &mut gpui::WindowContext) -> impl IntoElement { self.value = Some(value.into());
self.base cx.notify();
.child(self.value.unwrap_or_else(|| "Select...".into())) }
.when(self.open, |this| this.child(self.picker)) }
impl<D> FocusableView for Dropdown<D>
where
D: DropdownDelegate + 'static,
{
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl<D> Render for Dropdown<D>
where
D: DropdownDelegate + 'static,
{
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let title = self.title.clone().unwrap_or_else(|| "Select...".into());
let is_focused = self.focus_handle.is_focused(cx);
div()
.key_context("Dropdown")
.size_full()
.relative()
.child(
div()
.id(self.id.clone())
.relative()
.flex()
.w_full()
.items_center()
.justify_between()
.bg(cx.theme().background)
.border_1()
.border_color(cx.theme().input)
.rounded_sm()
.shadow_sm()
.px_3()
.py_2()
.when(is_focused, |this| this.border_color(cx.theme().ring))
.on_click(cx.listener(|this, _, cx| {
this.open = !this.open;
cx.notify();
}))
.child(
v_flex()
.items_center()
.justify_between()
.child(title)
.child(IconName::ChevronDown),
),
)
.when(self.open, |this| {
this.child(
div()
.absolute()
// Top is the dropdown input height + border
.top(px(50.))
.left_0()
.bg(cx.theme().background)
.border_1()
.border_color(cx.theme().input)
.rounded_sm()
.shadow_md()
.track_focus(&self.picker.focus_handle(cx))
.child(self.picker.clone()),
)
})
} }
} }

View file

@ -8,6 +8,7 @@ pub enum IconName {
Maximize, Maximize,
Minimize, Minimize,
Close, Close,
ChevronDown,
} }
impl IconName { impl IconName {
@ -19,6 +20,7 @@ impl IconName {
IconName::Maximize => "icons/maximize.svg", IconName::Maximize => "icons/maximize.svg",
IconName::Minimize => "icons/minimize.svg", IconName::Minimize => "icons/minimize.svg",
IconName::Close => "icons/close.svg", IconName::Close => "icons/close.svg",
IconName::ChevronDown => "icons/chevron-down.svg",
} }
.into() .into()
} }

View file

@ -16,7 +16,7 @@ pub mod theme;
pub mod title_bar; pub mod title_bar;
pub use styled_ext::StyledExt; pub use styled_ext::StyledExt;
pub mod divider; pub mod divider;
// pub mod dropdown; pub mod dropdown;
pub mod input; pub mod input;
pub mod list; pub mod list;
pub mod picker; pub mod picker;

View file

@ -204,7 +204,6 @@ impl<D: PickerDelegate> Picker<D> {
pub fn uniform_list(delegate: D, cx: &mut ViewContext<Self>) -> Self { pub fn uniform_list(delegate: D, cx: &mut ViewContext<Self>) -> Self {
let query_input = Self::new_query_input("Search...", cx); let query_input = Self::new_query_input("Search...", cx);
Self::new(delegate, ContainerKind::UniformList, Some(query_input), cx) Self::new(delegate, ContainerKind::UniformList, Some(query_input), cx)
} }