diff --git a/crates/story/src/dropdown_story.rs b/crates/story/src/dropdown_story.rs index fadbfaff..7672516f 100644 --- a/crates/story/src/dropdown_story.rs +++ b/crates/story/src/dropdown_story.rs @@ -1,9 +1,11 @@ +use std::rc::Rc; + use gpui::{ px, IntoElement, ParentElement, Render, Styled, View, ViewContext, VisualContext, WindowContext, }; use ui::{ - dropdown::{Dropdown, DropdownDelegate, DropdownItem}, + dropdown::{Dropdown, DropdownDelegate, DropdownItem, StringDropdownDelegate}, h_flex, theme::ActiveTheme, v_flex, Selection, @@ -20,7 +22,7 @@ impl Country { } } -impl DropdownItem for Country { +impl DropdownItem for &Country { fn title(&self) -> &str { self.name } @@ -36,6 +38,8 @@ struct FuritDelegate(Vec); pub struct DropdownStory { country_dropdown: View>, furit_dropdown: View>, + simple_dropdown1: View>, + simple_dropdown2: View>, } impl DropdownDelegate for CounterDelegate { @@ -43,12 +47,8 @@ impl DropdownDelegate for CounterDelegate { self.0.len() } - fn get(&self, ix: usize) -> Option<&dyn DropdownItem> { - if let Some(item) = self.0.get(ix) { - Some(item) - } else { - None - } + fn get(&self, ix: usize) -> Option { + self.0.get(ix) } } @@ -57,12 +57,8 @@ impl DropdownDelegate for FuritDelegate { self.0.len() } - fn get(&self, ix: usize) -> Option<&dyn DropdownItem> { - if let Some(item) = self.0.get(ix) { - Some(item) - } else { - None - } + fn get(&self, ix: usize) -> Option { + self.0.get(ix) } } @@ -82,7 +78,8 @@ impl DropdownStory { Country::new("Ecuador", "EC"), ]); - let country_dropdown = cx.new_view(|cx| Dropdown::new("dropdown-country", countries, cx)); + let country_dropdown = + cx.new_view(|cx| Dropdown::new("dropdown-country", countries, Some(6), cx)); let furits = FuritDelegate( [ @@ -98,11 +95,39 @@ impl DropdownStory { .map(|s| s.to_string()) .collect(), ); - let furit_dropdown = cx.new_view(|cx| Dropdown::new("dropdown-furits", furits, cx)); + let furit_dropdown = cx.new_view(|cx| Dropdown::new("dropdown-furits", furits, None, cx)); - cx.new_view(|_| Self { + cx.new_view(|cx| Self { country_dropdown, furit_dropdown, + simple_dropdown1: cx.new_view(|cx| { + Dropdown::string_list( + "string-list1", + Rc::new(vec![ + "QPUI".into(), + "Iced".into(), + "QT".into(), + "Cocoa".into(), + ]), + Some(0), + cx, + ) + .size(ui::Size::Small) + }), + simple_dropdown2: cx.new_view(|cx| { + Dropdown::string_list( + "string-list2", + Rc::new(vec![ + "Rust".into(), + "Go".into(), + "C++".into(), + "JavaScript".into(), + ]), + None, + cx, + ) + .size(ui::Size::Small) + }), }) } @@ -138,5 +163,13 @@ impl Render for DropdownStory { .gap_4() .child("This is other text."), ) + .child( + h_flex() + .items_center() + .w_128() + .gap_2() + .child(self.simple_dropdown1.clone()) + .child(self.simple_dropdown2.clone()), + ) } } diff --git a/crates/story/src/input_story.rs b/crates/story/src/input_story.rs index 54fa5c98..cbfc66cc 100644 --- a/crates/story/src/input_story.rs +++ b/crates/story/src/input_story.rs @@ -3,7 +3,9 @@ use gpui::{ ParentElement as _, Render, Styled, View, ViewContext, VisualContext, WindowContext, }; -use ui::{button::Button, h_flex, input::TextInput, v_flex, Clickable, FocusableCycle, IconName}; +use ui::{ + button::Button, h_flex, input::TextInput, v_flex, Clickable, FocusableCycle, IconName, Size, +}; use crate::section; @@ -24,6 +26,8 @@ pub struct InputStory { prefix_input1: View, suffix_input1: View, both_input1: View, + large_input: View, + small_input: View, } impl InputStory { @@ -72,6 +76,16 @@ impl InputStory { input.set_disabled(true, cx); input }), + large_input: cx.new_view(|cx| { + TextInput::new(cx) + .size(Size::Large) + .placeholder("Large input") + }), + small_input: cx.new_view(|cx| { + TextInput::new(cx) + .size(Size::Small) + .placeholder("Small input") + }), prefix_input1, suffix_input1, both_input1, @@ -135,6 +149,11 @@ impl Render for InputStory { .child(self.both_input1.clone()) .child(self.suffix_input1.clone()), ) + .child( + section("Input Size", cx) + .child(self.large_input.clone()) + .child(self.small_input.clone()), + ) .child( h_flex() .items_center() diff --git a/crates/ui/src/dropdown.rs b/crates/ui/src/dropdown.rs index d6091ca1..cf606b39 100644 --- a/crates/ui/src/dropdown.rs +++ b/crates/ui/src/dropdown.rs @@ -1,3 +1,5 @@ +use std::rc::Rc; + use gpui::{ actions, deferred, div, prelude::FluentBuilder as _, px, rems, AnyElement, AppContext, DismissEvent, Element, ElementId, EventEmitter, FocusHandle, FocusableView, InteractiveElement, @@ -21,8 +23,9 @@ pub fn init(cx: &mut AppContext) { use crate::{ h_flex, list::{self, List, ListDelegate, ListItem}, + styled_ext::Sizeful, theme::ActiveTheme, - Icon, IconName, StyledExt, + Icon, IconName, Size, StyledExt, }; /// A trait for items that can be displayed in a dropdown. @@ -31,7 +34,7 @@ pub trait DropdownItem { fn value(&self) -> &str; } -impl DropdownItem for String { +impl DropdownItem for &String { fn title(&self) -> &str { self } @@ -41,18 +44,28 @@ impl DropdownItem for String { } } +impl DropdownItem for &SharedString { + fn title(&self) -> &str { + self.as_ref() + } + + fn value(&self) -> &str { + self.as_ref() + } +} + pub trait DropdownDelegate { fn len(&self) -> usize; fn is_empty(&self) -> bool { self.len() == 0 } - fn get(&self, ix: usize) -> Option<&dyn DropdownItem>; + fn get(&self, ix: usize) -> Option; } struct DropdownListDelegate { delegate: D, dropdown: WeakView>, - selected_index: usize, + selected_index: Option, } impl ListDelegate for DropdownListDelegate @@ -66,7 +79,7 @@ where } fn confirmed_index(&self) -> Option { - Some(self.selected_index) + self.selected_index } fn render_item( @@ -74,7 +87,10 @@ where ix: usize, _cx: &mut gpui::ViewContext>, ) -> Option { - let selected = ix == self.selected_index; + let selected = self + .selected_index + .map_or(false, |selected_index| selected_index == ix); + if let Some(item) = self.delegate.get(ix) { let list_item = ListItem::new(("list-item", ix)) .check_icon(IconName::Check) @@ -97,13 +113,15 @@ where } fn confirm(&mut self, ix: Option, cx: &mut ViewContext>) { - self.selected_index = ix.unwrap_or(0); + self.selected_index = ix; if let Some(view) = self.dropdown.upgrade() { cx.update_view(&view, |view, cx| { - if let Some(item) = self.delegate.get(self.selected_index) { - view.title = Some(item.title().to_string().into()); - view.value = Some(item.value().to_string().into()); + if let Some(ix) = self.selected_index { + if let Some(item) = self.delegate.get(ix) { + view.title = Some(item.title().to_string().into()); + view.value = Some(item.value().to_string().into()); + } } view.open = false; @@ -113,10 +131,25 @@ where } } +pub struct StringDropdownDelegate { + items: Rc>, +} + +impl DropdownDelegate for StringDropdownDelegate { + fn len(&self) -> usize { + self.items.len() + } + + fn get(&self, ix: usize) -> Option { + self.items.get(ix) + } +} + pub struct Dropdown { id: ElementId, focus_handle: FocusHandle, list: View>>, + size: Size, open: bool, /// The value of the selected item. value: Option, @@ -127,11 +160,30 @@ impl Dropdown where D: DropdownDelegate + 'static, { - pub fn new(id: impl Into, delegate: D, cx: &mut ViewContext) -> Self { + pub fn new( + id: impl Into, + delegate: D, + selected_index: Option, + cx: &mut ViewContext, + ) -> Self { let delegate = DropdownListDelegate { delegate, dropdown: cx.view().downgrade(), - selected_index: 0, + selected_index, + }; + + let (title, value) = if let Some(selected_index) = selected_index { + let title: Option = delegate + .delegate + .get(selected_index) + .map(|item| item.title().to_string().into()); + let value: Option = delegate + .delegate + .get(selected_index) + .map(|item| item.value().to_string().into()); + (title, value) + } else { + (None, None) }; let list = cx.new_view(|cx| List::new(delegate, cx).no_query().max_h(rems(20.))); @@ -139,17 +191,27 @@ where id: id.into(), focus_handle: cx.focus_handle(), list, + size: Size::Medium, open: false, - title: None, - value: None, + title, + value, } } + pub fn size(mut self, size: Size) -> Self { + self.size = size; + self + } + pub fn set_value(&mut self, value: impl Into, cx: &mut ViewContext) { self.value = Some(value.into()); cx.notify(); } + pub fn value(&self) -> Option { + self.value.clone() + } + fn up(&mut self, _: &Up, cx: &mut ViewContext) { if !self.open { return; @@ -199,6 +261,21 @@ where } } +impl Dropdown { + pub fn string_list( + id: impl Into, + items: Rc>, + selected_index: Option, + cx: &mut ViewContext, + ) -> Self { + let delegate = StringDropdownDelegate { + items: items.clone(), + }; + + Self::new(id, delegate, selected_index, cx) + } +} + impl EventEmitter for Dropdown where D: DropdownDelegate + 'static {} impl FocusableView for Dropdown where @@ -241,8 +318,9 @@ where .rounded(px(cx.theme().radius)) .shadow_sm() .when(focused, |this| this.outline(cx)) - .px_3() - .py_2() + .input_px(self.size) + .input_py(self.size) + .input_h(self.size) .on_click(cx.listener(|this, _, cx| { this.open = !this.open; cx.notify(); @@ -254,8 +332,7 @@ where .justify_between() .child(div().flex_1().child(title)) .child( - Icon::new(IconName::ChevronsUpDown) - .size_4() + Icon::new(IconName::ChevronDown) .text_color(cx.theme().muted_foreground), ), ), diff --git a/crates/ui/src/input.rs b/crates/ui/src/input.rs index 1d553a94..b63d9701 100644 --- a/crates/ui/src/input.rs +++ b/crates/ui/src/input.rs @@ -5,11 +5,19 @@ use std::ops::Range; -use crate::event::InterativeElementExt as _; +use crate::styled_ext::Sizeful; use crate::theme::ActiveTheme; use crate::StyledExt as _; +use crate::{event::InterativeElementExt as _, Size}; use blink_cursor::BlinkCursor; -use gpui::*; +use gpui::{ + actions, div, fill, point, prelude, px, relative, rems, size, AnyView, AppContext, Bounds, + ClipboardItem, Context as _, Element, ElementId, ElementInputHandler, EventEmitter, + FocusHandle, FocusableView, GlobalElementId, InteractiveElement as _, IntoElement, KeyBinding, + KeyDownEvent, LayoutId, Model, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, + PaintQuad, ParentElement as _, Pixels, Point, Render, ShapedLine, SharedString, Style, + Styled as _, TextRun, UnderlineStyle, View, ViewContext, ViewInputHandler, WindowContext, +}; use prelude::FluentBuilder as _; use unicode_segmentation::*; @@ -95,6 +103,7 @@ pub struct TextInput { disabled: bool, masked: bool, appearance: bool, + size: Size, } impl EventEmitter for TextInput {} @@ -119,6 +128,7 @@ impl TextInput { appearance: true, prefix: None, suffix: None, + size: Size::Medium, }; // Observe the blink cursor to repaint the view when it changes. @@ -186,6 +196,12 @@ impl TextInput { self } + /// Set the size of the input field. + pub fn size(mut self, size: impl Into) -> Self { + self.size = size.into(); + self + } + /// Return the text of the input field. pub fn text(&self) -> SharedString { self.text.clone() @@ -633,13 +649,14 @@ impl Element for TextElement { .unwrap(); let cursor_pos = line.x_for_index(cursor); + let inset = px(0.5); let (selection, cursor) = if selected_range.is_empty() && input.show_cursor(cx) { ( None, Some(fill( Bounds::new( - point(bounds.left() + cursor_pos, bounds.top()), - size(px(1.5), bounds.bottom() - bounds.top()), + point(bounds.left() + cursor_pos, bounds.top() + inset), + size(px(1.5), bounds.bottom() - bounds.top() - inset * 2), ), crate::blue_500(), )), @@ -739,8 +756,8 @@ impl Render for TextInput { .size_full() .line_height(rems(1.25)) .text_size(rems(0.875)) - .py_2() - .h_10() + .input_py(self.size) + .input_h(self.size) .when(self.appearance, |this| { this.bg(cx.theme().input) .border_color(cx.theme().input) @@ -748,7 +765,7 @@ impl Render for TextInput { .rounded(px(cx.theme().radius)) .shadow_sm() .when(focused, |this| this.outline(cx)) - .px_3() + .input_px(self.size) .bg(if self.disabled { cx.theme().muted } else { diff --git a/crates/ui/src/list/list.rs b/crates/ui/src/list/list.rs index dc6afb19..e19e8923 100644 --- a/crates/ui/src/list/list.rs +++ b/crates/ui/src/list/list.rs @@ -8,7 +8,7 @@ use crate::theme::{ActiveTheme, Colorize as _}; use crate::{scroll::Scrollbar, v_flex}; use crate::{Icon, IconName}; use gpui::{ - actions, deferred, div, px, uniform_list, AppContext, FocusHandle, FocusableView, + actions, div, px, uniform_list, AppContext, FocusHandle, FocusableView, InteractiveElement as _, IntoElement, KeyBinding, Length, ListSizingBehavior, MouseButton, ParentElement as _, Render, Styled as _, UniformListScrollHandle, View, ViewContext, VisualContext as _, @@ -118,15 +118,12 @@ where return None; } - Some( - deferred(Scrollbar::uniform_scroll( - cx.view().clone(), - self.scrollbar_state.clone(), - self.vertical_scroll_handle.clone(), - self.delegate.items_count(), - )) - .with_priority(2), - ) + Some(Scrollbar::uniform_scroll( + cx.view().clone(), + self.scrollbar_state.clone(), + self.vertical_scroll_handle.clone(), + self.delegate.items_count(), + )) } fn scroll_to_selected_item(&mut self, _cx: &mut ViewContext) { @@ -242,7 +239,6 @@ where .min_h(px(100.)) .when_some(self.max_height, |this, h| this.max_h(h)) .overflow_hidden() - .children(self.render_scrollbar(cx)) .child( uniform_list(view, "uniform-list", items_count, { move |list, visible_range, cx| { @@ -275,7 +271,8 @@ where .with_sizing_behavior(sizing_behavior) .track_scroll(vertical_scroll_handle) .into_any_element(), - ), + ) + .children(self.render_scrollbar(cx)), ) } } diff --git a/crates/ui/src/styled_ext.rs b/crates/ui/src/styled_ext.rs index aad51b1a..ddd5a483 100644 --- a/crates/ui/src/styled_ext.rs +++ b/crates/ui/src/styled_ext.rs @@ -1,5 +1,5 @@ use crate::theme::ActiveTheme; -use gpui::{hsla, point, px, BoxShadow, FocusHandle, Pixels, Styled, WindowContext}; +use gpui::{hsla, point, px, rems, BoxShadow, FocusHandle, Pixels, Styled, WindowContext}; use smallvec::{smallvec, SmallVec}; pub enum ElevationIndex { @@ -149,3 +149,41 @@ impl From for Size { Size::Size(size) } } + +#[allow(unused)] +pub trait Sizeful { + fn input_size(self, size: Size) -> Self; + fn input_px(self, size: Size) -> Self; + fn input_py(self, size: Size) -> Self; + fn input_h(self, size: Size) -> Self; +} + +impl Sizeful for T { + fn input_size(self, size: Size) -> Self { + self.input_px(size).input_py(size).input_h(size) + } + + fn input_px(self, size: Size) -> Self { + match size { + Size::Large => self.px_5(), + Size::Medium => self.px_3(), + _ => self.px_2(), + } + } + + fn input_py(self, size: Size) -> Self { + match size { + Size::Large => self.py_5(), + Size::Medium => self.py_2(), + _ => self.py_1(), + } + } + + fn input_h(self, size: Size) -> Self { + match size { + Size::Large => self.h_11().text_size(rems(1.)), + Size::Medium => self.h_8().text_size(rems(0.85)), + _ => self.h(px(26.)).text_size(rems(0.8)), + } + } +}