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 gpui::{
div, ClickEvent, IntoElement, ParentElement, Render, RenderOnce, SharedString, Styled,
ViewContext, WindowContext,
div, px, ClickEvent, IntoElement, ParentElement, Render, RenderOnce, SharedString, Styled,
View, ViewContext, VisualContext, WindowContext,
};
use ui::{
checkbox::Checkbox,
dropdown::{Dropdown, DropdownItem},
dropdown::{Dropdown, DropdownDelegate, DropdownItem},
h_flex, v_flex, Disableable as _, Selection,
};
@ -25,25 +25,54 @@ impl Country {
}
impl DropdownItem for Country {
fn title(&self) -> SharedString {
self.name.into()
fn title(&self) -> &str {
self.name
}
fn value(&self) -> SharedString {
self.code.into()
fn value(&self) -> &str {
self.code
}
}
#[derive(IntoElement)]
struct CounterDelegate(Vec<Country>);
struct FuritDelegate(Vec<String>);
pub struct DropdownStory {
dropdown1: Dropdown,
dropdown2: Dropdown,
countries: Vec<Country>,
country_dropdown: View<Dropdown<CounterDelegate>>,
furit_dropdown: View<Dropdown<FuritDelegate>>,
}
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 {
pub(crate) fn new(cx: &mut WindowContext) -> Self {
let countries = vec![
pub(crate) fn new(cx: &mut ViewContext<Self>) -> Self {
let countries = CounterDelegate(vec![
Country::new("United States", "US"),
Country::new("Canada", "CA"),
Country::new("Mexico", "MX"),
@ -55,22 +84,29 @@ impl DropdownStory {
Country::new("Colombia", "CO"),
Country::new("Venezuela", "VE"),
Country::new("Ecuador", "EC"),
];
]);
let items2 = vec![
"Apple",
"Orange",
"Banana",
"Grape",
"Pineapple",
"Watermelon",
"Avocado",
];
let country_dropdown = cx.new_view(|cx| Dropdown::new("dropdown-country", countries, cx));
let furits = FuritDelegate(
[
"Apple",
"Orange",
"Banana",
"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 {
countries,
dropdown1: Dropdown::new("dropdown-country", Rc::new(countries), cx),
dropdown2: Dropdown::new("dropdown-fruit", Rc::new(items2), cx),
country_dropdown,
furit_dropdown,
}
}
@ -80,73 +116,34 @@ impl DropdownStory {
}
}
impl RenderOnce for CheckboxStory {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
impl Render for DropdownStory {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
story_case(
"Checkbox",
"A control that allows the user to toggle between checked and not checked.",
"Dropdown",
"Displays a list of options for the user to pick from—triggered by a button.",
)
.child(
v_flex().items_start().justify_start().gap_4().child(
h_flex()
.items_center()
.gap_4()
.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()
v_flex()
.size_full()
.items_start()
.justify_start()
.gap_4()
.child(
Checkbox::new("check2", cx)
.checked(Selection::Unselected)
.label("With label (Unchecked)")
.on_click(Self::on_click),
h_flex()
.w_full()
.max_w(px(640.))
.items_center()
.gap_4()
.child(self.country_dropdown.clone())
.child(self.furit_dropdown.clone()),
)
.child(
Checkbox::new("check2_1", cx)
.label("With Label (Indeterminate)")
.checked(Selection::Indeterminate)
.on_click(Self::on_click),
)
.child(
Checkbox::new("check2_2", cx)
.label("With Label (Checked)")
.checked(Selection::Selected)
.on_click(Self::on_click),
h_flex()
.w_full()
.items_center()
.gap_4()
.child("This is other text."),
),
)
.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 checkbox_story;
// mod dropdown_story;
mod dropdown_story;
mod input_story;
mod picker_story;
mod switch_story;
@ -21,6 +21,7 @@ use ui::{
};
use button_story::ButtonStory;
use dropdown_story::DropdownStory;
use input_story::InputStory;
use picker_story::PickerStory;
use switch_story::SwitchStory;
@ -77,6 +78,7 @@ enum StoryType {
Checkbox,
Switch,
Picker,
Dropdown,
}
impl Display for StoryType {
@ -87,6 +89,7 @@ impl Display for StoryType {
Self::Checkbox => write!(f, "Checkbox"),
Self::Switch => write!(f, "Switch"),
Self::Picker => write!(f, "Picker"),
Self::Dropdown => write!(f, "Dropdown"),
}
}
}
@ -98,6 +101,7 @@ pub struct Stories {
input_story: View<InputStory>,
switch_story: View<SwitchStory>,
picker_story: View<PickerStory>,
dropdown_story: View<DropdownStory>,
}
impl Stories {
@ -108,6 +112,7 @@ impl Stories {
input_story: cx.new_view(|cx| InputStory::new(cx)),
switch_story: cx.new_view(|cx| SwitchStory::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();
}
fn render_story_buttons(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn tabs(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.flex()
.items_center()
.gap_4()
.w_full()
.child(TabBar::new("story-tabs").children(vec![
self.swith_button("story-button", StoryType::Button, cx),
self.swith_button("story-input", StoryType::Input, cx),
self.swith_button("story-checkbox", StoryType::Checkbox, cx),
self.swith_button("story-switch", StoryType::Switch, cx),
self.swith_button("story-picker", StoryType::Picker, cx),
self.tab("story-button", StoryType::Button, cx),
self.tab("story-input", StoryType::Input, cx),
self.tab("story-checkbox", StoryType::Checkbox, cx),
self.tab("story-switch", StoryType::Switch, cx),
self.tab("story-picker", StoryType::Picker, cx),
self.tab("story-dropdown", StoryType::Dropdown, cx),
]))
}
fn swith_button(
&self,
id: &str,
ty: StoryType,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
fn tab(&self, id: &str, ty: StoryType, cx: &mut ViewContext<Self>) -> impl IntoElement {
let name = format!("{}", ty);
let is_active = ty == self.active;
@ -159,13 +160,14 @@ impl Render for Stories {
.flex()
.flex_col()
.gap_4()
.child(self.render_story_buttons(cx))
.child(self.tabs(cx))
.map(|this| match self.active {
StoryType::Button => this.child(self.button_story.clone()),
StoryType::Input => this.child(self.input_story.clone()),
StoryType::Checkbox => this.child(CheckboxStory::new(cx).into_any_element()),
StoryType::Switch => this.child(self.switch_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 gpui::{
div, prelude::FluentBuilder as _, Div, ElementId, InteractiveElement, IntoElement,
ParentElement as _, Render, RenderOnce, SharedString, Stateful, Styled as _, View, ViewContext,
VisualContext as _, WeakView,
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,
};
use crate::{
h_flex,
list::ListItem,
picker::{Picker, PickerDelegate},
IconName,
theme::ActiveTheme,
v_flex, IconName, StyledExt,
};
/// A trait for items that can be displayed in a dropdown.
@ -18,19 +21,34 @@ pub trait DropdownItem {
fn value(&self) -> &str;
}
pub trait DropdownDelegate {
type Item: DropdownItem;
impl DropdownItem for String {
fn title(&self) -> &str {
self
}
fn len(&self) -> usize;
fn get(&self, index: usize) -> Option<&Self::Item>;
fn value(&self) -> &str {
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>>,
selected_index: usize,
}
impl<D> PickerDelegate for DropdownPickerDelegate<D: PickerDelegate> {
impl<D> PickerDelegate for DropdownPickerDelegate<D>
where
D: DropdownDelegate + 'static,
{
type ListItem = ListItem;
fn match_count(&self) -> usize {
@ -57,7 +75,7 @@ impl<D> PickerDelegate for DropdownPickerDelegate<D: PickerDelegate> {
.selected(selected)
.py_1()
.px_3()
.child(item.title());
.child(item.title().to_string());
Some(list_item)
} else {
None
@ -68,7 +86,6 @@ impl<D> PickerDelegate for DropdownPickerDelegate<D: PickerDelegate> {
if let Some(view) = self.dropdown.upgrade() {
cx.update_view(&view, |view, cx| {
view.open = false;
cx.notify();
});
}
}
@ -77,47 +94,117 @@ impl<D> PickerDelegate for DropdownPickerDelegate<D: PickerDelegate> {
if let Some(view) = self.dropdown.upgrade() {
cx.update_view(&view, |view, cx| {
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;
cx.notify();
});
}
}
}
pub struct Dropdown<D: DropdownDelegate> {
base: Stateful<Div>,
delegate: D,
picker: View<Picker<DropdownPickerDelegate>>,
pub struct Dropdown<D: DropdownDelegate + 'static> {
id: ElementId,
focus_handle: FocusHandle,
picker: View<Picker<DropdownPickerDelegate<D>>>,
open: bool,
/// The value of the selected item.
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 {
let delegate = DropdownPickerDelegate {
// delegate,
let picker_delegate = DropdownPickerDelegate {
delegate,
dropdown: cx.view().downgrade(),
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 {
delegate,
base: div().id(id.into()),
id: id.into(),
focus_handle: cx.focus_handle(),
picker,
open: false,
title: None,
value: None,
}
}
}
impl RenderOnce for Dropdown {
fn render(self, cx: &mut gpui::WindowContext) -> impl IntoElement {
self.base
.child(self.value.unwrap_or_else(|| "Select...".into()))
.when(self.open, |this| this.child(self.picker))
pub fn set_value(&mut self, value: impl Into<SharedString>, cx: &mut ViewContext<Self>) {
self.value = Some(value.into());
cx.notify();
}
}
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,
Minimize,
Close,
ChevronDown,
}
impl IconName {
@ -19,6 +20,7 @@ impl IconName {
IconName::Maximize => "icons/maximize.svg",
IconName::Minimize => "icons/minimize.svg",
IconName::Close => "icons/close.svg",
IconName::ChevronDown => "icons/chevron-down.svg",
}
.into()
}

View file

@ -16,7 +16,7 @@ pub mod theme;
pub mod title_bar;
pub use styled_ext::StyledExt;
pub mod divider;
// pub mod dropdown;
pub mod dropdown;
pub mod input;
pub mod list;
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 {
let query_input = Self::new_query_input("Search...", cx);
Self::new(delegate, ContainerKind::UniformList, Some(query_input), cx)
}