Add Small, Large size support to Input and Dropdown and let them use same height. (#35)

- Add `Dropdown::string_list` to use `Vec<SharedString>` to create
dropdown for easy use.
- Add `input_px`, `input_py`, `input_h` trait methods to `Styled`
element.
This commit is contained in:
Jason Lee 2024-07-16 15:14:12 +08:00 committed by GitHub
parent 2b184ca8d5
commit ce7c1a3a34
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 237 additions and 56 deletions

View file

@ -1,9 +1,11 @@
use std::rc::Rc;
use gpui::{ use gpui::{
px, IntoElement, ParentElement, Render, Styled, View, ViewContext, VisualContext, WindowContext, px, IntoElement, ParentElement, Render, Styled, View, ViewContext, VisualContext, WindowContext,
}; };
use ui::{ use ui::{
dropdown::{Dropdown, DropdownDelegate, DropdownItem}, dropdown::{Dropdown, DropdownDelegate, DropdownItem, StringDropdownDelegate},
h_flex, h_flex,
theme::ActiveTheme, theme::ActiveTheme,
v_flex, Selection, v_flex, Selection,
@ -20,7 +22,7 @@ impl Country {
} }
} }
impl DropdownItem for Country { impl DropdownItem for &Country {
fn title(&self) -> &str { fn title(&self) -> &str {
self.name self.name
} }
@ -36,6 +38,8 @@ struct FuritDelegate(Vec<String>);
pub struct DropdownStory { pub struct DropdownStory {
country_dropdown: View<Dropdown<CounterDelegate>>, country_dropdown: View<Dropdown<CounterDelegate>>,
furit_dropdown: View<Dropdown<FuritDelegate>>, furit_dropdown: View<Dropdown<FuritDelegate>>,
simple_dropdown1: View<Dropdown<StringDropdownDelegate>>,
simple_dropdown2: View<Dropdown<StringDropdownDelegate>>,
} }
impl DropdownDelegate for CounterDelegate { impl DropdownDelegate for CounterDelegate {
@ -43,12 +47,8 @@ impl DropdownDelegate for CounterDelegate {
self.0.len() self.0.len()
} }
fn get(&self, ix: usize) -> Option<&dyn DropdownItem> { fn get(&self, ix: usize) -> Option<impl DropdownItem> {
if let Some(item) = self.0.get(ix) { self.0.get(ix)
Some(item)
} else {
None
}
} }
} }
@ -57,12 +57,8 @@ impl DropdownDelegate for FuritDelegate {
self.0.len() self.0.len()
} }
fn get(&self, ix: usize) -> Option<&dyn DropdownItem> { fn get(&self, ix: usize) -> Option<impl DropdownItem> {
if let Some(item) = self.0.get(ix) { self.0.get(ix)
Some(item)
} else {
None
}
} }
} }
@ -82,7 +78,8 @@ impl DropdownStory {
Country::new("Ecuador", "EC"), 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( let furits = FuritDelegate(
[ [
@ -98,11 +95,39 @@ impl DropdownStory {
.map(|s| s.to_string()) .map(|s| s.to_string())
.collect(), .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, country_dropdown,
furit_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() .gap_4()
.child("This is other text."), .child("This is other text."),
) )
.child(
h_flex()
.items_center()
.w_128()
.gap_2()
.child(self.simple_dropdown1.clone())
.child(self.simple_dropdown2.clone()),
)
} }
} }

View file

@ -3,7 +3,9 @@ use gpui::{
ParentElement as _, Render, Styled, View, ViewContext, VisualContext, WindowContext, 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; use crate::section;
@ -24,6 +26,8 @@ pub struct InputStory {
prefix_input1: View<TextInput>, prefix_input1: View<TextInput>,
suffix_input1: View<TextInput>, suffix_input1: View<TextInput>,
both_input1: View<TextInput>, both_input1: View<TextInput>,
large_input: View<TextInput>,
small_input: View<TextInput>,
} }
impl InputStory { impl InputStory {
@ -72,6 +76,16 @@ impl InputStory {
input.set_disabled(true, cx); input.set_disabled(true, cx);
input 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, prefix_input1,
suffix_input1, suffix_input1,
both_input1, both_input1,
@ -135,6 +149,11 @@ impl Render for InputStory {
.child(self.both_input1.clone()) .child(self.both_input1.clone())
.child(self.suffix_input1.clone()), .child(self.suffix_input1.clone()),
) )
.child(
section("Input Size", cx)
.child(self.large_input.clone())
.child(self.small_input.clone()),
)
.child( .child(
h_flex() h_flex()
.items_center() .items_center()

View file

@ -1,3 +1,5 @@
use std::rc::Rc;
use gpui::{ use gpui::{
actions, deferred, div, prelude::FluentBuilder as _, px, rems, AnyElement, AppContext, actions, deferred, div, prelude::FluentBuilder as _, px, rems, AnyElement, AppContext,
DismissEvent, Element, ElementId, EventEmitter, FocusHandle, FocusableView, InteractiveElement, DismissEvent, Element, ElementId, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
@ -21,8 +23,9 @@ pub fn init(cx: &mut AppContext) {
use crate::{ use crate::{
h_flex, h_flex,
list::{self, List, ListDelegate, ListItem}, list::{self, List, ListDelegate, ListItem},
styled_ext::Sizeful,
theme::ActiveTheme, theme::ActiveTheme,
Icon, IconName, StyledExt, Icon, IconName, Size, StyledExt,
}; };
/// A trait for items that can be displayed in a dropdown. /// A trait for items that can be displayed in a dropdown.
@ -31,7 +34,7 @@ pub trait DropdownItem {
fn value(&self) -> &str; fn value(&self) -> &str;
} }
impl DropdownItem for String { impl DropdownItem for &String {
fn title(&self) -> &str { fn title(&self) -> &str {
self 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 { pub trait DropdownDelegate {
fn len(&self) -> usize; fn len(&self) -> usize;
fn is_empty(&self) -> bool { fn is_empty(&self) -> bool {
self.len() == 0 self.len() == 0
} }
fn get(&self, ix: usize) -> Option<&dyn DropdownItem>; fn get(&self, ix: usize) -> Option<impl DropdownItem>;
} }
struct DropdownListDelegate<D: DropdownDelegate + 'static> { struct DropdownListDelegate<D: DropdownDelegate + 'static> {
delegate: D, delegate: D,
dropdown: WeakView<Dropdown<D>>, dropdown: WeakView<Dropdown<D>>,
selected_index: usize, selected_index: Option<usize>,
} }
impl<D> ListDelegate for DropdownListDelegate<D> impl<D> ListDelegate for DropdownListDelegate<D>
@ -66,7 +79,7 @@ where
} }
fn confirmed_index(&self) -> Option<usize> { fn confirmed_index(&self) -> Option<usize> {
Some(self.selected_index) self.selected_index
} }
fn render_item( fn render_item(
@ -74,7 +87,10 @@ where
ix: usize, ix: usize,
_cx: &mut gpui::ViewContext<List<Self>>, _cx: &mut gpui::ViewContext<List<Self>>,
) -> Option<Self::Item> { ) -> Option<Self::Item> {
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) { if let Some(item) = self.delegate.get(ix) {
let list_item = ListItem::new(("list-item", ix)) let list_item = ListItem::new(("list-item", ix))
.check_icon(IconName::Check) .check_icon(IconName::Check)
@ -97,13 +113,15 @@ where
} }
fn confirm(&mut self, ix: Option<usize>, cx: &mut ViewContext<List<Self>>) { fn confirm(&mut self, ix: Option<usize>, cx: &mut ViewContext<List<Self>>) {
self.selected_index = ix.unwrap_or(0); self.selected_index = ix;
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(ix) = self.selected_index {
view.title = Some(item.title().to_string().into()); if let Some(item) = self.delegate.get(ix) {
view.value = Some(item.value().to_string().into()); view.title = Some(item.title().to_string().into());
view.value = Some(item.value().to_string().into());
}
} }
view.open = false; view.open = false;
@ -113,10 +131,25 @@ where
} }
} }
pub struct StringDropdownDelegate {
items: Rc<Vec<SharedString>>,
}
impl DropdownDelegate for StringDropdownDelegate {
fn len(&self) -> usize {
self.items.len()
}
fn get(&self, ix: usize) -> Option<impl DropdownItem> {
self.items.get(ix)
}
}
pub struct Dropdown<D: DropdownDelegate + 'static> { pub struct Dropdown<D: DropdownDelegate + 'static> {
id: ElementId, id: ElementId,
focus_handle: FocusHandle, focus_handle: FocusHandle,
list: View<List<DropdownListDelegate<D>>>, list: View<List<DropdownListDelegate<D>>>,
size: Size,
open: bool, open: bool,
/// The value of the selected item. /// The value of the selected item.
value: Option<SharedString>, value: Option<SharedString>,
@ -127,11 +160,30 @@ impl<D> Dropdown<D>
where where
D: DropdownDelegate + 'static, 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,
selected_index: Option<usize>,
cx: &mut ViewContext<Self>,
) -> Self {
let delegate = DropdownListDelegate { let delegate = DropdownListDelegate {
delegate, delegate,
dropdown: cx.view().downgrade(), dropdown: cx.view().downgrade(),
selected_index: 0, selected_index,
};
let (title, value) = if let Some(selected_index) = selected_index {
let title: Option<SharedString> = delegate
.delegate
.get(selected_index)
.map(|item| item.title().to_string().into());
let value: Option<SharedString> = 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.))); let list = cx.new_view(|cx| List::new(delegate, cx).no_query().max_h(rems(20.)));
@ -139,17 +191,27 @@ where
id: id.into(), id: id.into(),
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
list, list,
size: Size::Medium,
open: false, open: false,
title: None, title,
value: None, value,
} }
} }
pub fn size(mut self, size: Size) -> Self {
self.size = size;
self
}
pub fn set_value(&mut self, value: impl Into<SharedString>, cx: &mut ViewContext<Self>) { pub fn set_value(&mut self, value: impl Into<SharedString>, cx: &mut ViewContext<Self>) {
self.value = Some(value.into()); self.value = Some(value.into());
cx.notify(); cx.notify();
} }
pub fn value(&self) -> Option<SharedString> {
self.value.clone()
}
fn up(&mut self, _: &Up, cx: &mut ViewContext<Self>) { fn up(&mut self, _: &Up, cx: &mut ViewContext<Self>) {
if !self.open { if !self.open {
return; return;
@ -199,6 +261,21 @@ where
} }
} }
impl Dropdown<StringDropdownDelegate> {
pub fn string_list(
id: impl Into<ElementId>,
items: Rc<Vec<SharedString>>,
selected_index: Option<usize>,
cx: &mut ViewContext<Self>,
) -> Self {
let delegate = StringDropdownDelegate {
items: items.clone(),
};
Self::new(id, delegate, selected_index, cx)
}
}
impl<D> EventEmitter<DismissEvent> for Dropdown<D> where D: DropdownDelegate + 'static {} impl<D> EventEmitter<DismissEvent> for Dropdown<D> where D: DropdownDelegate + 'static {}
impl<D> FocusableView for Dropdown<D> impl<D> FocusableView for Dropdown<D>
where where
@ -241,8 +318,9 @@ where
.rounded(px(cx.theme().radius)) .rounded(px(cx.theme().radius))
.shadow_sm() .shadow_sm()
.when(focused, |this| this.outline(cx)) .when(focused, |this| this.outline(cx))
.px_3() .input_px(self.size)
.py_2() .input_py(self.size)
.input_h(self.size)
.on_click(cx.listener(|this, _, cx| { .on_click(cx.listener(|this, _, cx| {
this.open = !this.open; this.open = !this.open;
cx.notify(); cx.notify();
@ -254,8 +332,7 @@ where
.justify_between() .justify_between()
.child(div().flex_1().child(title)) .child(div().flex_1().child(title))
.child( .child(
Icon::new(IconName::ChevronsUpDown) Icon::new(IconName::ChevronDown)
.size_4()
.text_color(cx.theme().muted_foreground), .text_color(cx.theme().muted_foreground),
), ),
), ),

View file

@ -5,11 +5,19 @@
use std::ops::Range; use std::ops::Range;
use crate::event::InterativeElementExt as _; use crate::styled_ext::Sizeful;
use crate::theme::ActiveTheme; use crate::theme::ActiveTheme;
use crate::StyledExt as _; use crate::StyledExt as _;
use crate::{event::InterativeElementExt as _, Size};
use blink_cursor::BlinkCursor; 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 prelude::FluentBuilder as _;
use unicode_segmentation::*; use unicode_segmentation::*;
@ -95,6 +103,7 @@ pub struct TextInput {
disabled: bool, disabled: bool,
masked: bool, masked: bool,
appearance: bool, appearance: bool,
size: Size,
} }
impl EventEmitter<TextEvent> for TextInput {} impl EventEmitter<TextEvent> for TextInput {}
@ -119,6 +128,7 @@ impl TextInput {
appearance: true, appearance: true,
prefix: None, prefix: None,
suffix: None, suffix: None,
size: Size::Medium,
}; };
// Observe the blink cursor to repaint the view when it changes. // Observe the blink cursor to repaint the view when it changes.
@ -186,6 +196,12 @@ impl TextInput {
self self
} }
/// Set the size of the input field.
pub fn size(mut self, size: impl Into<Size>) -> Self {
self.size = size.into();
self
}
/// Return the text of the input field. /// Return the text of the input field.
pub fn text(&self) -> SharedString { pub fn text(&self) -> SharedString {
self.text.clone() self.text.clone()
@ -633,13 +649,14 @@ impl Element for TextElement {
.unwrap(); .unwrap();
let cursor_pos = line.x_for_index(cursor); 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) { let (selection, cursor) = if selected_range.is_empty() && input.show_cursor(cx) {
( (
None, None,
Some(fill( Some(fill(
Bounds::new( Bounds::new(
point(bounds.left() + cursor_pos, bounds.top()), point(bounds.left() + cursor_pos, bounds.top() + inset),
size(px(1.5), bounds.bottom() - bounds.top()), size(px(1.5), bounds.bottom() - bounds.top() - inset * 2),
), ),
crate::blue_500(), crate::blue_500(),
)), )),
@ -739,8 +756,8 @@ impl Render for TextInput {
.size_full() .size_full()
.line_height(rems(1.25)) .line_height(rems(1.25))
.text_size(rems(0.875)) .text_size(rems(0.875))
.py_2() .input_py(self.size)
.h_10() .input_h(self.size)
.when(self.appearance, |this| { .when(self.appearance, |this| {
this.bg(cx.theme().input) this.bg(cx.theme().input)
.border_color(cx.theme().input) .border_color(cx.theme().input)
@ -748,7 +765,7 @@ impl Render for TextInput {
.rounded(px(cx.theme().radius)) .rounded(px(cx.theme().radius))
.shadow_sm() .shadow_sm()
.when(focused, |this| this.outline(cx)) .when(focused, |this| this.outline(cx))
.px_3() .input_px(self.size)
.bg(if self.disabled { .bg(if self.disabled {
cx.theme().muted cx.theme().muted
} else { } else {

View file

@ -8,7 +8,7 @@ use crate::theme::{ActiveTheme, Colorize as _};
use crate::{scroll::Scrollbar, v_flex}; use crate::{scroll::Scrollbar, v_flex};
use crate::{Icon, IconName}; use crate::{Icon, IconName};
use gpui::{ 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, InteractiveElement as _, IntoElement, KeyBinding, Length, ListSizingBehavior, MouseButton,
ParentElement as _, Render, Styled as _, UniformListScrollHandle, View, ViewContext, ParentElement as _, Render, Styled as _, UniformListScrollHandle, View, ViewContext,
VisualContext as _, VisualContext as _,
@ -118,15 +118,12 @@ where
return None; return None;
} }
Some( Some(Scrollbar::uniform_scroll(
deferred(Scrollbar::uniform_scroll( cx.view().clone(),
cx.view().clone(), self.scrollbar_state.clone(),
self.scrollbar_state.clone(), self.vertical_scroll_handle.clone(),
self.vertical_scroll_handle.clone(), self.delegate.items_count(),
self.delegate.items_count(), ))
))
.with_priority(2),
)
} }
fn scroll_to_selected_item(&mut self, _cx: &mut ViewContext<Self>) { fn scroll_to_selected_item(&mut self, _cx: &mut ViewContext<Self>) {
@ -242,7 +239,6 @@ where
.min_h(px(100.)) .min_h(px(100.))
.when_some(self.max_height, |this, h| this.max_h(h)) .when_some(self.max_height, |this, h| this.max_h(h))
.overflow_hidden() .overflow_hidden()
.children(self.render_scrollbar(cx))
.child( .child(
uniform_list(view, "uniform-list", items_count, { uniform_list(view, "uniform-list", items_count, {
move |list, visible_range, cx| { move |list, visible_range, cx| {
@ -275,7 +271,8 @@ where
.with_sizing_behavior(sizing_behavior) .with_sizing_behavior(sizing_behavior)
.track_scroll(vertical_scroll_handle) .track_scroll(vertical_scroll_handle)
.into_any_element(), .into_any_element(),
), )
.children(self.render_scrollbar(cx)),
) )
} }
} }

View file

@ -1,5 +1,5 @@
use crate::theme::ActiveTheme; 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}; use smallvec::{smallvec, SmallVec};
pub enum ElevationIndex { pub enum ElevationIndex {
@ -149,3 +149,41 @@ impl From<Pixels> for Size {
Size::Size(size) Size::Size(size)
} }
} }
#[allow(unused)]
pub trait Sizeful<T: Styled> {
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<T: Styled> Sizeful<T> 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)),
}
}
}