From d278d850582c8ea9048b48ea8792f5640d84b667 Mon Sep 17 00:00:00 2001 From: Floyd Wang Date: Fri, 26 Jul 2024 14:31:47 +0800 Subject: [PATCH] Imporve dropdown (#73) * Remove min-height * Provide `render_empty` for custom empty view * Fix crash when delegate is empty --- crates/story/src/dropdown_story.rs | 16 ++++++- crates/ui/src/dropdown.rs | 72 ++++++++++++++++++++---------- crates/ui/src/list/list.rs | 10 ++--- 3 files changed, 67 insertions(+), 31 deletions(-) diff --git a/crates/story/src/dropdown_story.rs b/crates/story/src/dropdown_story.rs index a5b806fd..8517a78e 100644 --- a/crates/story/src/dropdown_story.rs +++ b/crates/story/src/dropdown_story.rs @@ -43,6 +43,7 @@ pub struct DropdownStory { fruit_dropdown: View>>, simple_dropdown1: View>>, simple_dropdown2: View>>, + simple_dropdown3: View>>, } impl DropdownStory { @@ -106,6 +107,18 @@ impl DropdownStory { .placeholder("Language") .title_prefix("Language: ") }), + simple_dropdown3: cx.new_view(|cx| { + Dropdown::string_list("string-list3", Vec::::new(), None, cx) + .size(ui::Size::Small) + .render_empty(|cx| { + h_flex() + .h_24() + .justify_center() + .text_color(cx.theme().muted_foreground) + .child("No Data") + .into_any_element() + }) + }), } }) } @@ -175,7 +188,8 @@ impl Render for DropdownStory { .w_128() .gap_2() .child(self.simple_dropdown1.clone()) - .child(self.simple_dropdown2.clone()), + .child(self.simple_dropdown2.clone()) + .child(self.simple_dropdown3.clone()), ) } } diff --git a/crates/ui/src/dropdown.rs b/crates/ui/src/dropdown.rs index 947a1ba8..aa8bbffe 100644 --- a/crates/ui/src/dropdown.rs +++ b/crates/ui/src/dropdown.rs @@ -1,11 +1,19 @@ -use std::borrow::Cow; +use std::{borrow::Cow, rc::Rc}; use gpui::{ - actions, deferred, div, prelude::FluentBuilder as _, px, rems, AnyElement, AppContext, - ClickEvent, DismissEvent, Element, ElementId, EventEmitter, FocusHandle, FocusableView, - InteractiveElement, IntoElement, KeyBinding, LayoutId, ParentElement as _, Render, - SharedString, StatefulInteractiveElement as _, Styled as _, View, ViewContext, - VisualContext as _, WeakView, WindowContext, + actions, deferred, div, prelude::FluentBuilder, px, rems, AnyElement, AppContext, ClickEvent, + DismissEvent, Div, Element, ElementId, EventEmitter, FocusHandle, Focusable, FocusableView, + InteractiveElement, IntoElement, KeyBinding, LayoutId, ParentElement, Render, SharedString, + StatefulInteractiveElement, Styled, View, ViewContext, VisualContext, WeakView, WindowContext, +}; + +use crate::{ + button::{Button, ButtonStyle}, + h_flex, + list::{self, List, ListDelegate, ListItem}, + styled_ext::StyleSized, + theme::ActiveTheme, + Clickable, Icon, IconName, Size, StyledExt, }; actions!(dropdown, [Up, Down, Enter, Escape]); @@ -20,15 +28,6 @@ pub fn init(cx: &mut AppContext) { ]) } -use crate::{ - button::{Button, ButtonStyle}, - h_flex, - list::{self, List, ListDelegate, ListItem}, - styled_ext::StyleSized, - theme::ActiveTheme as _, - Clickable as _, Icon, IconName, Size, StyledExt, -}; - /// A trait for items that can be displayed in a dropdown. pub trait DropdownItem { type Value: Clone; @@ -185,6 +184,7 @@ pub struct Dropdown { placeholder: SharedString, title_prefix: Option, selected_value: Option<::Value>, + render_empty: Option AnyElement + 'static>>, } impl Dropdown @@ -214,6 +214,7 @@ where open: false, cleanable: false, title_prefix: None, + render_empty: None, }; this.set_selected_index(selected_index, cx); this @@ -246,6 +247,14 @@ where self } + pub fn render_empty(mut self, f: F) -> Self + where + F: Fn(&WindowContext) -> AnyElement + 'static, + { + self.render_empty = Some(Rc::new(f)); + self + } + pub fn set_selected_index( &mut self, selected_index: Option, @@ -326,19 +335,24 @@ where } fn render_menu_content(&self, cx: &WindowContext) -> impl IntoElement { + let is_empty = self.list.read(cx).delegate().delegate.is_empty(); + div() - .absolute() - .mt_1p5() - .bg(cx.theme().background) - .border_1() - .border_color(cx.theme().input) - .rounded(px(cx.theme().radius)) - .shadow_md() .track_focus(&self.list.focus_handle(cx)) - .child(self.list.clone()) .on_mouse_down_out(|_, cx| { cx.dispatch_action(Box::new(Escape)); }) + .map(|this| { + if is_empty { + if let Some(render_empty) = &self.render_empty { + with_style(this, cx).child(render_empty(cx)) + } else { + this + } + } else { + with_style(this, cx).child(self.list.clone()) + } + }) } fn display_title(&self, cx: &WindowContext) -> impl IntoElement { @@ -350,7 +364,7 @@ where .delegate .get(*selected_index) .map(|item| item.title().to_string()) - .unwrap(); + .unwrap_or_default(); h_flex() .children(self.title_prefix.clone().map(|prefix| { @@ -367,6 +381,16 @@ where } } +fn with_style(d: Focusable
, cx: &WindowContext) -> Focusable
{ + d.absolute() + .mt_1p5() + .bg(cx.theme().background) + .border_1() + .border_color(cx.theme().input) + .rounded(px(cx.theme().radius)) + .shadow_md() +} + impl Dropdown> { pub fn string_list( id: impl Into, diff --git a/crates/ui/src/list/list.rs b/crates/ui/src/list/list.rs index 32d00c91..be846cd3 100644 --- a/crates/ui/src/list/list.rs +++ b/crates/ui/src/list/list.rs @@ -3,15 +3,14 @@ use std::{cell::Cell, rc::Rc}; use crate::input::{InputEvent, TextInput}; use crate::scroll::ScrollbarState; -use crate::theme::{ActiveTheme, Colorize as _}; +use crate::theme::{ActiveTheme, Colorize}; use crate::IconName; use crate::{scroll::Scrollbar, v_flex}; use gpui::SharedString; use gpui::{ - actions, div, prelude::FluentBuilder as _, px, uniform_list, AppContext, FocusHandle, - FocusableView, InteractiveElement as _, IntoElement, KeyBinding, Length, ListSizingBehavior, - MouseButton, ParentElement as _, Render, Styled as _, Task, UniformListScrollHandle, View, - ViewContext, VisualContext as _, + actions, div, prelude::FluentBuilder, uniform_list, AppContext, FocusHandle, FocusableView, + InteractiveElement, IntoElement, KeyBinding, Length, ListSizingBehavior, MouseButton, + ParentElement, Render, Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext, }; use smol::Timer; @@ -326,7 +325,6 @@ where v_flex() .flex_grow() .relative() - .min_h(px(100.)) .when_some(self.max_height, |this, h| this.max_h(h)) .overflow_hidden() .when(items_count == 0, |this| {