use std::any::TypeId;
use accesskit::{Node, Role};
use masonry_core::core::HasProperty;
use tracing::{Span, trace_span};
use vello::Scene;
use vello::kurbo::{Affine, Point, Rect, Size};
use vello::peniko::Color;
use crate::core::{
AccessCtx, ArcStr, BoxConstraints, ChildrenIds, LayoutCtx, NewWidget, NoAction, PaintCtx,
Properties, PropertiesMut, PropertiesRef, RegisterCtx, Update, UpdateCtx, Widget, WidgetId,
WidgetMut, WidgetPod,
};
use crate::properties::{
Background, BorderColor, BorderWidth, BoxShadow, CaretColor, ContentColor, CornerRadius,
DisabledBackground, Padding, PlaceholderColor, SelectionColor, UnfocusedSelectionColor,
};
use crate::util::{fill, stroke};
use crate::widgets::{Label, TextArea};
pub struct TextInput {
text: WidgetPod<TextArea<true>>,
placeholder: WidgetPod<Label>,
placeholder_text: ArcStr,
clip: bool,
}
impl TextInput {
pub fn new(text: &str) -> Self {
Self::from_text_area(TextArea::new_editable(text).with_auto_id())
}
pub fn from_text_area(text: NewWidget<TextArea<true>>) -> Self {
Self {
text: text.to_pod(),
placeholder: NewWidget::new_with_props(Label::new(""), Properties::new()).to_pod(),
placeholder_text: "".into(),
clip: false,
}
}
pub fn with_placeholder(mut self, placeholder_text: impl Into<ArcStr>) -> Self {
let placeholder_text = placeholder_text.into();
self.placeholder =
NewWidget::new_with_props(Label::new(placeholder_text.clone()), Properties::new())
.to_pod();
self.placeholder_text = placeholder_text;
self
}
pub fn with_clip(mut self, clip: bool) -> Self {
self.clip = clip;
self
}
pub fn area_pod(&self) -> &WidgetPod<TextArea<true>> {
&self.text
}
}
impl TextInput {
pub fn text_mut<'t>(this: &'t mut WidgetMut<'_, Self>) -> WidgetMut<'t, TextArea<true>> {
this.ctx.get_mut(&mut this.widget.text)
}
pub fn placeholder_mut<'t>(this: &'t mut WidgetMut<'_, Self>) -> WidgetMut<'t, Label> {
this.ctx.get_mut(&mut this.widget.placeholder)
}
pub fn set_placeholder(this: &mut WidgetMut<'_, Self>, placeholder_text: impl Into<ArcStr>) {
Label::set_text(&mut Self::placeholder_mut(this), placeholder_text);
}
pub fn set_clip(this: &mut WidgetMut<'_, Self>, clip: bool) {
this.widget.clip = clip;
this.ctx.request_layout();
}
}
impl HasProperty<Background> for TextInput {}
impl HasProperty<CaretColor> for TextInput {}
impl HasProperty<DisabledBackground> for TextInput {}
impl HasProperty<BorderColor> for TextInput {}
impl HasProperty<BorderWidth> for TextInput {}
impl HasProperty<BoxShadow> for TextInput {}
impl HasProperty<CornerRadius> for TextInput {}
impl HasProperty<Padding> for TextInput {}
impl HasProperty<PlaceholderColor> for TextInput {}
impl HasProperty<SelectionColor> for TextInput {}
impl HasProperty<UnfocusedSelectionColor> for TextInput {}
impl Widget for TextInput {
type Action = NoAction;
fn register_children(&mut self, ctx: &mut RegisterCtx<'_>) {
ctx.register_child(&mut self.text);
ctx.register_child(&mut self.placeholder);
}
fn property_changed(&mut self, ctx: &mut UpdateCtx<'_>, property_type: TypeId) {
DisabledBackground::prop_changed(ctx, property_type);
Background::prop_changed(ctx, property_type);
BorderColor::prop_changed(ctx, property_type);
BorderWidth::prop_changed(ctx, property_type);
CornerRadius::prop_changed(ctx, property_type);
Padding::prop_changed(ctx, property_type);
BoxShadow::prop_changed(ctx, property_type);
if property_type == TypeId::of::<CaretColor>() {
ctx.mutate_self_later(|mut input| {
let mut input = input.downcast::<Self>();
let color = *input.get_prop::<CaretColor>();
let mut text_area = Self::text_mut(&mut input);
text_area.insert_prop(color);
});
} else if property_type == TypeId::of::<SelectionColor>() {
ctx.mutate_self_later(|mut input| {
let mut input = input.downcast::<Self>();
let color = *input.get_prop::<SelectionColor>();
let mut text_area = Self::text_mut(&mut input);
text_area.insert_prop(color);
});
} else if property_type == TypeId::of::<UnfocusedSelectionColor>() {
ctx.mutate_self_later(|mut input| {
let mut input = input.downcast::<Self>();
let color = *input.get_prop::<UnfocusedSelectionColor>();
let mut text_area = Self::text_mut(&mut input);
text_area.insert_prop(color);
});
} else if property_type == TypeId::of::<PlaceholderColor>() {
ctx.mutate_self_later(|mut input| {
let mut input = input.downcast::<Self>();
let color = input.get_prop::<PlaceholderColor>().color;
let mut label = Self::placeholder_mut(&mut input);
label.insert_prop(ContentColor::new(color));
});
}
}
fn update(&mut self, ctx: &mut UpdateCtx<'_>, _props: &mut PropertiesMut<'_>, event: &Update) {
match event {
Update::WidgetAdded => {
ctx.mutate_self_later(|mut input| {
let mut input = input.downcast::<Self>();
let color = *input.get_prop::<CaretColor>();
let mut text_area = Self::text_mut(&mut input);
text_area.insert_prop(color);
});
ctx.mutate_self_later(|mut input| {
let mut input = input.downcast::<Self>();
let color = *input.get_prop::<SelectionColor>();
let mut text_area = Self::text_mut(&mut input);
text_area.insert_prop(color);
});
ctx.mutate_self_later(|mut input| {
let mut input = input.downcast::<Self>();
let color = *input.get_prop::<UnfocusedSelectionColor>();
let mut text_area = Self::text_mut(&mut input);
text_area.insert_prop(color);
});
ctx.mutate_self_later(|mut input| {
let mut input = input.downcast::<Self>();
let color = input.get_prop::<PlaceholderColor>().color;
let mut label = Self::placeholder_mut(&mut input);
label.insert_prop(ContentColor::new(color));
});
}
Update::ChildFocusChanged(_) => {
ctx.request_paint_only();
}
_ => {}
}
}
fn layout(
&mut self,
ctx: &mut LayoutCtx<'_>,
props: &mut PropertiesMut<'_>,
bc: &BoxConstraints,
) -> Size {
let border = props.get::<BorderWidth>();
let padding = props.get::<Padding>();
let shadow = props.get::<BoxShadow>();
let bc = *bc;
let bc = border.layout_down(bc);
let bc = padding.layout_down(bc);
let size = ctx.run_layout(&mut self.text, &bc);
let baseline = ctx.child_baseline_offset(&self.text);
let (size, baseline) = padding.layout_up(size, baseline);
let (size, baseline) = border.layout_up(size, baseline);
let pos = Point::ORIGIN;
let pos = border.place_down(pos);
let pos = padding.place_down(pos);
ctx.place_child(&mut self.text, pos);
let text_is_empty = ctx.get_raw(&mut self.text).0.is_empty();
ctx.set_stashed(&mut self.placeholder, !text_is_empty);
if text_is_empty {
let _ = ctx.run_layout(&mut self.placeholder, &bc);
ctx.place_child(&mut self.placeholder, pos);
}
if shadow.is_visible() {
ctx.set_paint_insets(shadow.get_insets());
}
if self.clip {
ctx.set_clip_path(Rect::from_origin_size(Point::ORIGIN, size));
}
ctx.set_baseline_offset(baseline);
size
}
fn paint(&mut self, ctx: &mut PaintCtx<'_>, props: &PropertiesRef<'_>, scene: &mut Scene) {
let size = ctx.size();
let border_width = props.get::<BorderWidth>();
let border_radius = props.get::<CornerRadius>();
let shadow = props.get::<BoxShadow>();
let mut border_color = props.get::<BorderColor>();
let bg = if ctx.is_disabled() {
&props.get::<DisabledBackground>().0
} else {
props.get::<Background>()
};
let bg_rect = border_width.bg_rect(size, border_radius);
let border_rect = border_width.border_rect(size, border_radius);
if ctx.has_focus_target() {
border_color = &BorderColor {
color: Color::WHITE,
};
}
shadow.paint(scene, Affine::IDENTITY, bg_rect);
let brush = bg.get_peniko_brush_for_rect(bg_rect.rect());
fill(scene, &bg_rect, &brush);
stroke(scene, &border_rect, border_color.color, border_width.width);
}
fn accessibility_role(&self) -> Role {
Role::GenericContainer
}
fn accessibility(
&mut self,
_ctx: &mut AccessCtx<'_>,
_props: &PropertiesRef<'_>,
node: &mut Node,
) {
node.set_placeholder(self.placeholder_text.to_string());
}
fn children_ids(&self) -> ChildrenIds {
ChildrenIds::from_slice(&[self.text.id(), self.placeholder.id()])
}
fn make_trace_span(&self, id: WidgetId) -> Span {
trace_span!("Prose", id = id.trace())
}
fn get_debug_text(&self) -> Option<String> {
self.clip.then(|| "(clip)".into())
}
}
#[cfg(test)]
mod tests {
use masonry_core::core::TextEvent;
use vello::kurbo::Size;
use super::*;
use crate::core::StyleProperty;
use crate::testing::{TestHarness, assert_render_snapshot};
use crate::theme::default_property_set;
use crate::widgets::TextArea;
#[test]
fn text_input_outline() {
let text_input = NewWidget::new(TextInput::from_text_area(
TextArea::new_editable("TextInput contents")
.with_style(StyleProperty::FontSize(14.0))
.with_auto_id(),
));
let mut harness = TestHarness::create_with_size(
default_property_set(),
text_input,
Size::new(150.0, 40.0),
);
assert_render_snapshot!(harness, "text_input_outline");
let mut text_area_id = None;
harness.edit_root_widget(|mut text_input| {
let mut text_input = TextInput::text_mut(&mut text_input);
text_area_id = Some(text_input.ctx.widget_id());
TextArea::select_text(&mut text_input, "contents");
});
harness.focus_on(text_area_id);
assert_render_snapshot!(harness, "text_input_selection");
harness.process_text_event(TextEvent::WindowFocusChange(false));
assert_render_snapshot!(harness, "text_input_selection_unfocused");
harness.process_text_event(TextEvent::WindowFocusChange(true));
harness.animate_ms(500 + 1);
assert_render_snapshot!(harness, "text_input_cursor_blink");
}
#[test]
fn placeholder() {
let text_input = NewWidget::new(
TextInput::from_text_area(
TextArea::new_editable("")
.with_style(StyleProperty::FontSize(14.0))
.with_auto_id(),
)
.with_placeholder("HELLO WORLD"),
);
let mut harness = TestHarness::create_with_size(
default_property_set(),
text_input,
Size::new(150.0, 40.0),
);
assert_render_snapshot!(harness, "text_input_placeholder");
}
#[test]
fn text_input_clips() {
let text_input = NewWidget::new(
TextInput::from_text_area(
TextArea::new_editable("TextInput contents")
.with_style(StyleProperty::FontSize(14.0))
.with_word_wrap(false)
.with_auto_id(),
)
.with_clip(true),
);
let mut harness = TestHarness::create_with_size(
default_property_set(),
text_input,
Size::new(80.0, 30.0),
);
assert_render_snapshot!(harness, "text_input_clip");
}
}