use masonry::core::{ArcStr, NewWidget, Properties};
use masonry::properties::{
CaretColor, ContentColor, DisabledContentColor, PlaceholderColor, SelectionColor,
UnfocusedSelectionColor,
};
use masonry::widgets::{self, TextAction};
use vello::peniko::Color;
use crate::core::{MessageContext, Mut, View, ViewMarker};
use crate::view::Prop;
use crate::{InsertNewline, MessageResult, Pod, TextAlign, ViewCtx, WidgetView as _};
type Callback<State, Action> = Box<dyn Fn(&mut State, String) -> Action + Send + Sync + 'static>;
pub fn text_input<F, State, Action>(contents: String, on_changed: F) -> TextInput<State, Action>
where
F: Fn(&mut State, String) -> Action + Send + Sync + 'static,
{
TextInput {
contents,
on_changed: Box::new(on_changed),
on_enter: None,
text_color: None,
disabled_text_color: None,
placeholder: ArcStr::default(),
text_alignment: TextAlign::default(),
insert_newline: InsertNewline::default(),
disabled: false,
clip: true,
}
}
#[must_use = "View values do nothing unless provided to Xilem."]
pub struct TextInput<State, Action> {
contents: String,
on_changed: Callback<State, Action>,
on_enter: Option<Callback<State, Action>>,
text_color: Option<Color>,
disabled_text_color: Option<Color>,
placeholder: ArcStr,
text_alignment: TextAlign,
insert_newline: InsertNewline,
disabled: bool,
clip: bool,
}
impl<State: 'static, Action: 'static> TextInput<State, Action> {
pub fn text_color(mut self, color: Color) -> Self {
self.text_color = Some(color);
self
}
pub fn disabled_text_color(mut self, color: Color) -> Self {
self.disabled_text_color = Some(color);
self
}
pub fn caret_color(self, color: Color) -> Prop<CaretColor, Self, State, Action> {
self.prop(CaretColor { color })
}
pub fn selection_color(self, color: Color) -> Prop<SelectionColor, Self, State, Action> {
self.prop(SelectionColor { color })
}
pub fn unfocused_selection_color(
self,
color: Color,
) -> Prop<UnfocusedSelectionColor, Self, State, Action> {
self.prop(UnfocusedSelectionColor(SelectionColor { color }))
}
pub fn placeholder(mut self, placeholder_text: impl Into<ArcStr>) -> Self {
self.placeholder = placeholder_text.into();
self
}
pub fn placeholder_color(self, color: Color) -> Prop<PlaceholderColor, Self, State, Action> {
self.prop(PlaceholderColor::new(color))
}
pub fn text_alignment(mut self, text_alignment: TextAlign) -> Self {
self.text_alignment = text_alignment;
self
}
pub fn insert_newline(mut self, insert_newline: InsertNewline) -> Self {
self.insert_newline = insert_newline;
self
}
pub fn on_enter<F>(mut self, on_enter: F) -> Self
where
F: Fn(&mut State, String) -> Action + Send + Sync + 'static,
{
self.on_enter = Some(Box::new(on_enter));
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn clip(mut self, clip: bool) -> Self {
self.clip = clip;
self
}
}
impl<State, Action> ViewMarker for TextInput<State, Action> {}
impl<State: 'static, Action: 'static> View<State, Action, ViewCtx> for TextInput<State, Action> {
type Element = Pod<widgets::TextInput>;
type ViewState = ();
fn build(&self, ctx: &mut ViewCtx, _: &mut State) -> (Self::Element, Self::ViewState) {
let text_area = widgets::TextArea::new_editable(&self.contents)
.with_text_alignment(self.text_alignment)
.with_insert_newline(self.insert_newline);
let mut props = Properties::new();
if let Some(color) = self.text_color {
props.insert(ContentColor { color });
}
if let Some(color) = self.disabled_text_color {
props.insert(DisabledContentColor(ContentColor { color }));
}
let text_input =
widgets::TextInput::from_text_area(NewWidget::new_with_props(text_area, props))
.with_clip(self.clip)
.with_placeholder(self.placeholder.clone());
let id = text_input.area_pod().id();
ctx.record_action(id);
let mut pod = ctx.create_pod(text_input);
pod.new_widget.options.disabled = self.disabled;
(pod, ())
}
fn rebuild(
&self,
prev: &Self,
_: &mut Self::ViewState,
_ctx: &mut ViewCtx,
mut element: Mut<'_, Self::Element>,
_: &mut State,
) {
if self.text_color != prev.text_color {
if let Some(color) = self.text_color {
element.insert_prop(ContentColor { color });
} else {
element.remove_prop::<ContentColor>();
}
}
if self.disabled_text_color != prev.disabled_text_color {
if let Some(color) = self.disabled_text_color {
element.insert_prop(DisabledContentColor(ContentColor { color }));
} else {
element.remove_prop::<DisabledContentColor>();
}
}
if self.placeholder != prev.placeholder {
widgets::TextInput::set_placeholder(&mut element, self.placeholder.clone());
}
if prev.disabled != self.disabled {
element.ctx.set_disabled(self.disabled);
}
if self.clip != prev.clip {
widgets::TextInput::set_clip(&mut element, self.clip);
}
let mut text_area = widgets::TextInput::text_mut(&mut element);
if text_area.widget.text() != &self.contents {
widgets::TextArea::reset_text(&mut text_area, &self.contents);
}
if prev.text_alignment != self.text_alignment {
widgets::TextArea::set_text_alignment(&mut text_area, self.text_alignment);
}
if prev.insert_newline != self.insert_newline {
widgets::TextArea::set_insert_newline(&mut text_area, self.insert_newline);
}
}
fn teardown(
&self,
_: &mut Self::ViewState,
ctx: &mut ViewCtx,
element: Mut<'_, Self::Element>,
) {
ctx.teardown_leaf(element);
}
fn message(
&self,
_: &mut Self::ViewState,
message: &mut MessageContext,
_: Mut<'_, Self::Element>,
app_state: &mut State,
) -> MessageResult<Action> {
debug_assert!(
message.remaining_path().is_empty(),
"id path should be empty in TextInput::message"
);
match message.take_message::<TextAction>() {
Some(action) => match *action {
TextAction::Changed(text) => {
MessageResult::Action((self.on_changed)(app_state, text))
}
TextAction::Entered(text) if self.on_enter.is_some() => {
MessageResult::Action((self.on_enter.as_ref().unwrap())(app_state, text))
}
TextAction::Entered(_) => {
tracing::error!("Textbox::message: on_enter is not set");
MessageResult::Stale
}
},
None => {
tracing::error!(?message, "Wrong message type in TextInput::message");
MessageResult::Stale
}
}
}
}