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::{
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<String>);
pub struct DropdownStory {
country_dropdown: View<Dropdown<CounterDelegate>>,
furit_dropdown: View<Dropdown<FuritDelegate>>,
simple_dropdown1: View<Dropdown<StringDropdownDelegate>>,
simple_dropdown2: View<Dropdown<StringDropdownDelegate>>,
}
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<impl DropdownItem> {
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<impl DropdownItem> {
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()),
)
}
}

View file

@ -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<TextInput>,
suffix_input1: View<TextInput>,
both_input1: View<TextInput>,
large_input: View<TextInput>,
small_input: View<TextInput>,
}
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()

View file

@ -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<impl DropdownItem>;
}
struct DropdownListDelegate<D: DropdownDelegate + 'static> {
delegate: D,
dropdown: WeakView<Dropdown<D>>,
selected_index: usize,
selected_index: Option<usize>,
}
impl<D> ListDelegate for DropdownListDelegate<D>
@ -66,7 +79,7 @@ where
}
fn confirmed_index(&self) -> Option<usize> {
Some(self.selected_index)
self.selected_index
}
fn render_item(
@ -74,7 +87,10 @@ where
ix: usize,
_cx: &mut gpui::ViewContext<List<Self>>,
) -> 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) {
let list_item = ListItem::new(("list-item", ix))
.check_icon(IconName::Check)
@ -97,13 +113,15 @@ where
}
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() {
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<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> {
id: ElementId,
focus_handle: FocusHandle,
list: View<List<DropdownListDelegate<D>>>,
size: Size,
open: bool,
/// The value of the selected item.
value: Option<SharedString>,
@ -127,11 +160,30 @@ impl<D> Dropdown<D>
where
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 {
delegate,
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.)));
@ -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<SharedString>, cx: &mut ViewContext<Self>) {
self.value = Some(value.into());
cx.notify();
}
pub fn value(&self) -> Option<SharedString> {
self.value.clone()
}
fn up(&mut self, _: &Up, cx: &mut ViewContext<Self>) {
if !self.open {
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> FocusableView for Dropdown<D>
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),
),
),

View file

@ -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<TextEvent> 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<Size>) -> 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 {

View file

@ -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<Self>) {
@ -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)),
)
}
}

View file

@ -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<Pixels> for 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)),
}
}
}