Add Picker

This commit is contained in:
Jason Lee 2024-06-26 16:16:08 +08:00
parent 1f21469e7b
commit 5c93282e1d
9 changed files with 790 additions and 8 deletions

View file

@ -44,6 +44,7 @@ cargo run
There have a part of UI components from [Zed](https://github.com/zed-industries/zed/tree/main/crates/ui), that are under GPL v3.0 license.
- title_bar
- picker
> I think we can discuss them with Zed team to change the license to MIT or Apache License for share to community.

View file

@ -6,6 +6,7 @@ use gpui::{
RenderOnce, SharedString, StatefulInteractiveElement as _, Styled as _, View, ViewContext,
VisualContext, WindowContext,
};
use picker_story::PickerStory;
use switch_story::SwitchStory;
mod button_story;
@ -74,6 +75,7 @@ enum StoryType {
Input,
Checkbox,
Switch,
Picker,
}
impl Display for StoryType {
@ -83,6 +85,7 @@ impl Display for StoryType {
Self::Input => write!(f, "Input"),
Self::Checkbox => write!(f, "Checkbox"),
Self::Switch => write!(f, "Switch"),
Self::Picker => write!(f, "Picker"),
}
}
}
@ -93,6 +96,7 @@ pub struct Stories {
button_story: View<ButtonStory>,
input_story: View<InputStory>,
switch_story: View<SwitchStory>,
picker_story: View<PickerStory>,
}
impl Stories {
@ -102,6 +106,7 @@ impl Stories {
button_story: cx.new_view(|cx| ButtonStory {}),
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)),
}
}
@ -125,6 +130,7 @@ impl Stories {
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),
]))
}
@ -158,6 +164,7 @@ impl Render for Stories {
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()),
})
}
}

View file

@ -0,0 +1,205 @@
use gpui::{
div, prelude::FluentBuilder as _, px, Div, Empty, Entity, InteractiveElement as _, IntoElement,
ParentElement, Render, SharedString, Styled, Task, View, ViewContext, VisualContext as _,
WindowContext,
};
use ui::{
button::Button,
h_flex,
label::Label,
picker::{Picker, PickerDelegate},
switch::{LabelSide, Switch},
theme::{ActiveTheme, Colorize},
v_flex, Clickable as _, Disableable as _, StyledExt,
};
use super::story_case;
pub struct ListItemDeletegate {
selected_index: usize,
items: Vec<String>,
matches: Vec<String>,
}
impl PickerDelegate for ListItemDeletegate {
type ListItem = Div;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, index: usize, _cx: &mut ViewContext<Picker<Self>>) {
self.selected_index = index
}
fn render_match(
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
if let Some(item) = self.matches.get(ix) {
let list_item = div()
.py_1()
.px_3()
.when(!selected, |this| {
this.hover(|this| this.bg(cx.theme().card))
})
.child(item.clone())
.text_base()
.text_color(cx.theme().foreground)
.when(selected, |this| this.bg(cx.theme().card.lighten(0.1)));
Some(list_item)
} else {
None
}
}
fn update_matches(
&mut self,
query: &str,
cx: &mut ViewContext<Picker<Self>>,
) -> gpui::Task<()> {
let matched_items = self
.items
.iter()
.filter(|item| item.contains(query))
.cloned()
.collect();
self.matches = matched_items;
Task::ready(())
}
}
pub struct PickerStory {
picker: View<Picker<ListItemDeletegate>>,
open: bool,
selected_value: Option<String>,
}
impl PickerStory {
pub(crate) fn new(cx: &mut WindowContext) -> Self {
let items = [
"Baguette (France)",
"Baklava (Turkey)",
"Beef Wellington (UK)",
"Biryani (India)",
"Borscht (Ukraine)",
"Bratwurst (Germany)",
"Bulgogi (Korea)",
"Burrito (USA)",
"Ceviche (Peru)",
"Chicken Tikka Masala (India)",
"Churrasco (Brazil)",
"Couscous (North Africa)",
"Croissant (France)",
"Dim Sum (China)",
"Empanada (Argentina)",
"Fajitas (Mexico)",
"Falafel (Middle East)",
"Feijoada (Brazil)",
"Fish and Chips (UK)",
"Fondue (Switzerland)",
"Goulash (Hungary)",
"Haggis (Scotland)",
"Kebab (Middle East)",
"Kimchi (Korea)",
"Lasagna (Italy)",
"Maple Syrup Pancakes (Canada)",
"Moussaka (Greece)",
"Pad Thai (Thailand)",
"Paella (Spain)",
"Pancakes (USA)",
"Pasta Carbonara (Italy)",
"Pavlova (Australia)",
"Peking Duck (China)",
"Pho (Vietnam)",
"Pierogi (Poland)",
"Pizza (Italy)",
"Poutine (Canada)",
"Pretzel (Germany)",
"Ramen (Japan)",
"Rendang (Indonesia)",
"Sashimi (Japan)",
"Satay (Indonesia)",
"Shepherd's Pie (Ireland)",
"Sushi (Japan)",
"Tacos (Mexico)",
"Tandoori Chicken (India)",
"Tortilla (Spain)",
"Tzatziki (Greece)",
"Wiener Schnitzel (Austria)",
];
let picker = cx.new_view(|cx| {
let items: Vec<String> = items.iter().map(|s| s.to_string()).collect();
let mut picker = Picker::uniform_list(
ListItemDeletegate {
selected_index: 0,
matches: items.clone(),
items,
},
cx,
)
.modal(true)
.max_height(Some(px(350.0).into()));
picker.focus(cx);
picker.set_query("c", cx);
picker
});
Self {
picker,
open: false,
selected_value: None,
}
}
}
impl Render for PickerStory {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
story_case("Picker", "Picker is a list of items that can be selected.")
.child(v_flex().items_start().child(
Button::new("show-picker", "Show Picker...").on_click(cx.listener(
|this, _, cx| {
this.open = !this.open;
cx.notify();
},
)),
))
.when_some(self.selected_value.clone(), |this, selected_value| {
this.child("Selected: ").child(Label::new(selected_value))
})
.when(self.open, |this| {
this.child(
div().absolute().size_full().top_0().left_0().child(
v_flex()
// .h(px(0.0))
.top_10()
.flex()
.flex_col()
.items_center()
.track_focus(&self.picker.focus_handle(cx))
.child(
h_flex()
.w(px(450.))
.occlude()
.child(self.picker.clone())
.on_mouse_down_out(cx.listener(|this, _, cx| {
this.open = false;
cx.notify();
})),
),
),
)
})
}
}

43
crates/ui/src/divider.rs Normal file
View file

@ -0,0 +1,43 @@
use gpui::IntoElement;
use gpui::{div, prelude::FluentBuilder as _, Div, RenderOnce, Styled as _};
use crate::theme::ActiveTheme;
use crate::StyledExt as _;
enum Orientation {
Vertical,
Horizontal,
}
#[derive(IntoElement)]
pub struct Divider {
orientation: Orientation,
}
impl Divider {
pub fn vertical() -> Self {
Self {
orientation: Orientation::Vertical,
}
}
pub fn horizontal() -> Self {
Self {
orientation: Orientation::Horizontal,
}
}
}
impl RenderOnce for Divider {
fn render(self, cx: &mut gpui::WindowContext) -> impl gpui::IntoElement {
let theme = cx.theme();
div()
.map(|this| match self.orientation {
Orientation::Vertical => this.v_flex().w_0().h_full(),
Orientation::Horizontal => this.h_flex().h_0().w_full(),
})
.border_1()
.border_color(theme.border)
}
}

29
crates/ui/src/empty.rs Normal file
View file

@ -0,0 +1,29 @@
use gpui::{
div, AppContext, FocusHandle, FocusableView, InteractiveElement, IntoElement, Render,
ViewContext,
};
/// An invisible element that can hold focus.
pub(crate) struct Empty {
focus_handle: FocusHandle,
}
impl Empty {
pub(crate) fn new(cx: &mut ViewContext<Self>) -> Self {
Self {
focus_handle: cx.focus_handle(),
}
}
}
impl Render for Empty {
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
div().track_focus(&self.focus_handle)
}
}
impl FocusableView for Empty {
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}

View file

@ -16,7 +16,6 @@ pub mod theme;
pub mod title_bar;
pub use styled_ext::StyledExt;
pub mod divider;
pub mod dropdown;
pub mod picker;
pub mod switch;
pub mod tab;

490
crates/ui/src/picker.rs Normal file
View file

@ -0,0 +1,490 @@
use std::time::Duration;
use anyhow::Result;
use gpui::{
actions, div, list, prelude::FluentBuilder as _, px, rems, uniform_list, AppContext,
ClickEvent, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
InteractiveElement as _, IntoElement, Length, ListSizingBehavior, ListState, MouseButton,
MouseUpEvent, ParentElement as _, Render, SharedString, StatefulInteractiveElement as _,
Styled as _, Task, UniformListScrollHandle, View, ViewContext, VisualContext as _,
WindowContext,
};
actions!(
picker,
[
UseSelectedQuery,
Cancel,
Confirm,
SecondaryConfirm,
SelectPrev,
SelectNext,
SelectFirst,
SelectLast,
]
);
use crate::{
divider::Divider, empty::Empty, label::Label, stock::*, text_field::TextField,
theme::ActiveTheme, StyledExt as _,
};
enum ElementContainer {
List(ListState),
UniformList(UniformListScrollHandle),
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
enum ContainerKind {
List,
UniformList,
}
pub trait PickerDelegate: Sized + 'static {
type ListItem: IntoElement;
fn match_count(&self) -> usize;
fn selected_index(&self) -> usize;
fn set_selected_index(&mut self, index: usize, cx: &mut ViewContext<Picker<Self>>);
fn selected_index_changed(
&self,
_ix: usize,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Box<dyn Fn(&mut WindowContext) + 'static>> {
None
}
fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>) {}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {}
fn should_dismiss(&self) -> bool {
return true;
}
fn render_query(&self, input: &View<TextField>, _cx: &mut ViewContext<Picker<Self>>) -> Div {
v_flex()
.child(
h_flex()
.overflow_hidden()
.flex_none()
.h_9()
.px_4()
.child(input.clone()),
)
.child(Divider::horizontal())
}
fn render_match(
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem>;
fn separators_after_indices(&self) -> Vec<usize> {
Vec::new()
}
fn update_matches(&mut self, query: &str, cx: &mut ViewContext<Picker<Self>>) -> Task<()>;
fn confirm_update_query(&mut self, _cx: &mut ViewContext<Picker<Self>>) -> Option<String> {
None
}
fn finalize_update_matches(
&mut self,
_query: String,
_duration: Duration,
_cx: &mut ViewContext<Picker<Self>>,
) -> bool {
false
}
fn selected_as_query(&self) -> Option<String> {
None
}
}
impl<D: PickerDelegate> FocusableView for Picker<D> {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
if let Some(input) = &self.query_input {
input.focus_handle(cx)
} else {
self.head.focus_handle(cx)
}
}
}
impl<D: PickerDelegate> EventEmitter<DismissEvent> for Picker<D> {}
struct PendingUpdateMatches {
delegate_update_matches: Option<Task<()>>,
_task: Task<Result<()>>,
}
pub struct Picker<D: PickerDelegate> {
delegate: D,
element_container: ElementContainer,
query_input: Option<View<TextField>>,
width: Option<Length>,
max_height: Option<Length>,
is_modal: bool,
head: View<Empty>,
pending_update_matches: Option<PendingUpdateMatches>,
confirm_on_update: Option<bool>,
}
impl<D: PickerDelegate> Picker<D> {
fn new(
delegate: D,
kind: ContainerKind,
query_input: Option<View<TextField>>,
cx: &mut ViewContext<Self>,
) -> Self {
let element_container = match kind {
ContainerKind::List => {
let view = cx.view().downgrade();
ElementContainer::List(ListState::new(
0,
gpui::ListAlignment::Top,
px(1000.),
move |ix, cx| {
view.upgrade()
.map(|view| {
view.update(cx, |this, cx| {
this.render_element(cx, ix).into_any_element()
})
})
.unwrap_or_else(|| div().into_any_element())
},
))
}
ContainerKind::UniformList => {
ElementContainer::UniformList(UniformListScrollHandle::new())
}
};
Self {
delegate,
query_input: None,
head: cx.new_view(Empty::new),
width: None,
is_modal: false,
max_height: Some(rems(20.).into()),
element_container,
pending_update_matches: None,
confirm_on_update: None,
}
}
fn new_query_input(
placehoder: impl Into<SharedString>,
cx: &mut ViewContext<Self>,
) -> View<TextField> {
cx.new_view(|cx| {
let mut input = TextField::new(cx);
input.set_placeholder(placehoder, cx);
input
})
}
pub fn list(delegate: D, cx: &mut ViewContext<Self>) -> Self {
let query_input = Self::new_query_input("Search...", cx);
Self::new(delegate, ContainerKind::List, Some(query_input), cx)
}
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)
}
pub fn width(mut self, width: impl Into<gpui::Length>) -> Self {
self.width = Some(width.into());
self
}
pub fn max_height(mut self, max_height: Option<gpui::Length>) -> Self {
self.max_height = max_height;
self
}
pub fn modal(mut self, modal: bool) -> Self {
self.is_modal = modal;
self
}
pub fn focus(&self, cx: &mut WindowContext) {
self.focus_handle(cx).focus(cx);
}
pub fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
if let Some(input) = &self.query_input {
input.update(cx, |this, cx| this.set_text(query, cx));
}
}
/// Return the query input string.
pub fn query(&self, cx: &AppContext) -> String {
if let Some(input) = &self.query_input {
input.read(cx).text(cx)
} else {
String::new()
}
}
fn render_element_container(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let sizing_behavior = if self.max_height.is_some() {
ListSizingBehavior::Infer
} else {
ListSizingBehavior::Auto
};
match &self.element_container {
ElementContainer::UniformList(scroll_handle) => uniform_list(
cx.view().clone(),
"candidates",
self.delegate.match_count(),
move |picker, visible_range, cx| {
visible_range
.map(|ix| picker.render_element(cx, ix))
.collect()
},
)
.with_sizing_behavior(sizing_behavior)
.flex_grow()
.py_2()
.track_scroll(scroll_handle.clone())
.into_any_element(),
ElementContainer::List(state) => list(state.clone())
.with_sizing_behavior(sizing_behavior)
.flex_grow()
.py_2()
.into_any_element(),
}
}
fn render_element(&self, cx: &mut ViewContext<Self>, ix: usize) -> impl IntoElement {
div()
.id(("item", ix))
.cursor_pointer()
.on_click(cx.listener(move |this, event: &ClickEvent, cx| {
this.on_click(ix, event.down.modifiers.secondary(), cx)
}))
// As of this writing, GPUI intercepts `ctrl-[mouse-event]`s on macOS
// and produces right mouse button events. This matches platforms norms
// but means that UIs which depend on holding ctrl down (such as the tab
// switcher) can't be clicked on. Hence, this handler.
.on_mouse_up(
MouseButton::Right,
cx.listener(move |this, event: &MouseUpEvent, cx| {
// We specficially want to use the platform key here, as
// ctrl will already be held down for the tab switcher.
this.on_click(ix, event.modifiers.platform, cx)
}),
)
.children(
self.delegate
.render_match(ix, ix == self.delegate.selected_index(), cx),
)
.when(
self.delegate.separators_after_indices().contains(&ix),
|picker| {
picker
.border_color(cx.theme().border)
.border_b_1()
.pb(px(-1.0))
},
)
}
fn on_click(&mut self, ix: usize, secondary: bool, cx: &mut ViewContext<Self>) {
cx.stop_propagation();
cx.prevent_default();
self.set_selected_index(ix, false, cx);
self.do_confirm(secondary, cx)
}
fn do_confirm(&mut self, secondary: bool, cx: &mut ViewContext<Self>) {
// if let Some(update_query) = self.delegate.confirm_update_query(cx) {
// self.set_query(update_query, cx);
// self.delegate.set_selected_index(0, cx);
// } else {
self.delegate.confirm(secondary, cx)
// }
}
pub fn set_selected_index(
&mut self,
ix: usize,
scroll_to_index: bool,
cx: &mut ViewContext<Self>,
) {
let previous_index = self.delegate.selected_index();
self.delegate.set_selected_index(ix, cx);
let current_index = self.delegate.selected_index();
if previous_index != current_index {
if let Some(action) = self.delegate.selected_index_changed(ix, cx) {
action(cx);
}
if scroll_to_index {
self.scroll_to_item_index(ix);
}
}
}
fn scroll_to_item_index(&mut self, ix: usize) {
match &mut self.element_container {
ElementContainer::List(state) => state.scroll_to_reveal_item(ix),
ElementContainer::UniformList(scroll_handle) => scroll_handle.scroll_to_item(ix),
}
}
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
if self.pending_update_matches.is_some()
&& !self
.delegate
.finalize_update_matches(self.query(cx), Duration::from_millis(16), cx)
{
self.confirm_on_update = Some(false)
} else {
self.pending_update_matches.take();
self.do_confirm(false, cx);
}
}
pub fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
let count = self.delegate.match_count();
if count > 0 {
let index = self.delegate.selected_index();
let ix = if index == count - 1 { 0 } else { index + 1 };
self.set_selected_index(ix, true, cx);
cx.notify();
}
}
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
let count = self.delegate.match_count();
if count > 0 {
let index = self.delegate.selected_index();
let ix = if index == 0 { count - 1 } else { index - 1 };
self.set_selected_index(ix, true, cx);
cx.notify();
}
}
fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
let count = self.delegate.match_count();
if count > 0 {
self.set_selected_index(0, true, cx);
cx.notify();
}
}
fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
let count = self.delegate.match_count();
if count > 0 {
self.delegate.set_selected_index(count - 1, cx);
self.set_selected_index(count - 1, true, cx);
cx.notify();
}
}
pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
if self.delegate.should_dismiss() {
self.delegate.dismissed(cx);
cx.emit(DismissEvent);
}
}
fn use_selected_query(&mut self, _: &UseSelectedQuery, cx: &mut ViewContext<Self>) {
if let Some(new_query) = self.delegate.selected_as_query() {
self.set_query(&new_query, cx);
cx.stop_propagation();
}
}
pub fn refresh(&mut self, cx: &mut ViewContext<Self>) {
let query = self.query(cx);
self.update_matches(&query, cx);
}
pub fn update_matches(&mut self, query: &str, cx: &mut ViewContext<Self>) {
let delegate_pending_update_matches = self.delegate.update_matches(query, cx);
self.matches_updated(cx);
// This struct ensures that we can synchronously drop the task returned by the
// delegate's `update_matches` method and the task that the picker is spawning.
// If we simply capture the delegate's task into the picker's task, when the picker's
// task gets synchronously dropped, the delegate's task would keep running until
// the picker's task has a chance of being scheduled, because dropping a task happens
// asynchronously.
self.pending_update_matches = Some(PendingUpdateMatches {
delegate_update_matches: Some(delegate_pending_update_matches),
_task: cx.spawn(|this, mut cx| async move {
let delegate_pending_update_matches = this.update(&mut cx, |this, _| {
this.pending_update_matches
.as_mut()
.unwrap()
.delegate_update_matches
.take()
.unwrap()
})?;
delegate_pending_update_matches.await;
this.update(&mut cx, |this, cx| {
this.matches_updated(cx);
})
}),
});
}
fn matches_updated(&mut self, cx: &mut ViewContext<Self>) {
if let ElementContainer::List(state) = &mut self.element_container {
state.reset(self.delegate.match_count());
}
let index = self.delegate.selected_index();
self.scroll_to_item_index(index);
// self.pending_update_matches = None;
// if let Some(secondary) = self.confirm_on_update.take() {
// self.confirm(secondary, cx);
// }
cx.notify();
}
}
impl<D: PickerDelegate> Render for Picker<D> {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex()
.key_context("Picker")
.size_full()
.when_some(self.width, |el, width| el.w(width))
.overflow_hidden()
.when(self.is_modal, |this| this.elevation_3(cx))
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_prev))
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::confirm))
// .on_action(cx.listener(Self::secondary_confirm))
.on_action(cx.listener(Self::use_selected_query))
// .on_action(cx.listener(Self::confirm_input))
.child(match self.query_input {
Some(ref input) => self.delegate.render_query(input, cx),
None => div().child(self.head.clone()),
})
.when(self.delegate.match_count() > 0, |el| {
el.child(
v_flex()
.flex_grow()
.when_some(self.max_height, |div, max_h| div.max_h(max_h))
.overflow_hidden()
// .children(self.delegate.render_header(cx))
.child(self.render_element_container(cx)),
)
})
.when(self.delegate.match_count() == 0, |el| {
el.child(
v_flex()
.flex_grow()
.py_2()
.child(div().child(Label::new("No matched.").text_color(cx.theme().muted))),
)
})
}
}

View file

@ -6,7 +6,7 @@ use crate::theme::ActiveTheme;
use gpui::{
div, prelude::FluentBuilder as _, AppContext, ClipboardItem, EventEmitter, FocusHandle,
FocusableView, InteractiveElement, IntoElement, KeyDownEvent, MouseButton, ParentElement,
Render, RenderOnce, Styled, View, ViewContext, WindowContext,
Render, RenderOnce, SharedString, Styled, View, ViewContext, WindowContext,
};
use std::time::Duration;
use text_view::TextView;
@ -31,7 +31,11 @@ impl TextField {
cx.focus(&self.focus_handle);
}
pub fn set_placeholder(&mut self, placeholder: &str, cx: &mut WindowContext) {
pub fn set_placeholder(
&mut self,
placeholder: impl Into<SharedString>,
cx: &mut WindowContext,
) {
self.view.update(cx, |text_view, cx| {
text_view.set_placeholder(placeholder, cx)
});

View file

@ -6,7 +6,7 @@ use super::{
use crate::theme::{ActiveTheme, Colorize as _};
use gpui::{
px, relative, ContentMask, Context, Element, EventEmitter, FocusHandle, HighlightStyle, Hsla,
InteractiveText, IntoElement, Model, Point, Render, Style, StyledText, TextStyle,
InteractiveText, IntoElement, Model, Point, Render, SharedString, Style, StyledText, TextStyle,
TextStyleRefinement, View, ViewContext, VisualContext, WindowContext,
};
@ -19,7 +19,7 @@ pub struct TextFieldStyle {
pub struct TextView {
pub text: String,
pub style: TextFieldStyle,
pub placeholder: String,
pub placeholder: SharedString,
pub word_click: (usize, u16),
pub selection: Range<usize>,
pub disabled: bool,
@ -49,7 +49,7 @@ impl TextView {
let m = Self {
text: String::new(),
style,
placeholder: "".to_string(),
placeholder: "".into(),
word_click: (0, 0),
selection: 0..0,
blink_manager: blink_manager.clone(),
@ -187,8 +187,12 @@ impl TextView {
cx.notify();
}
pub fn set_placeholder(&mut self, placeholder: impl ToString, cx: &mut ViewContext<Self>) {
self.placeholder = placeholder.to_string();
pub fn set_placeholder(
&mut self,
placeholder: impl Into<SharedString>,
cx: &mut ViewContext<Self>,
) {
self.placeholder = placeholder.into();
cx.notify();
}