Add Dropdown.
This commit is contained in:
parent
b3a32bc02c
commit
910f27a66a
7 changed files with 221 additions and 131 deletions
3
assets/icons/chevron-down.svg
Normal file
3
assets/icons/chevron-down.svg
Normal 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 |
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue