1use std::rc::Rc;
130
131use frust::authoring::Role;
132use frust::authoring::text::{TextContext, TextLayout, TextStyle};
133use frust::authoring::{
134 AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, CursorIcon, EventCtx, EventResult,
135 InputEvent, LayoutCtx, PaintCtx, PaintScene, PointerPhase, SemanticsCtx, View, Widget, any,
136};
137use frust::{AnimationController, Theme, text_input};
138use kurbo::{Point, RoundedRect, Shape, Size};
139use peniko::{Brush, Color};
140
141use super::interaction::{DISABLED_CONTAINER_OPACITY, DISABLED_CONTENT_OPACITY};
142use crate::tokens::{MaterialDimensions, MaterialMotion};
143
144const DECORATION_HEIGHT: f64 = 56.0;
148const HPAD: f64 = 16.0;
151const ICON_SIZE: f64 = 24.0;
156const ICON_GAP: f64 = 12.0;
159const LABEL_FLOAT_TOP: f64 = 8.0;
162const TEXT_TOP_WITH_LABEL: f64 = 24.0;
168const SUPPORTING_GAP: f64 = 4.0;
171const RADIUS: f64 = MaterialDimensions::RADIUS_EXTRA_SMALL;
174const STROKE_REST: f64 = 1.0;
176const STROKE_FOCUSED: f64 = 2.0;
178const STROKE_TOLERANCE: f64 = 0.1;
181const UNBOUNDED_WIDTH: f64 = 200.0;
185
186const PRIMARY: Color = Color::from_rgb8(0x67, 0x50, 0xA4);
188const ERROR: Color = Color::from_rgb8(0xB3, 0x26, 0x1E);
190const ON_SURFACE: Color = Color::from_rgb8(0x1D, 0x1B, 0x20);
192const ON_SURFACE_VARIANT: Color = Color::from_rgb8(0x49, 0x45, 0x4F);
195const OUTLINE: Color = Color::from_rgb8(0x79, 0x74, 0x7E);
197
198const BODY_LARGE_SIZE: f32 = 16.0;
201const BODY_SMALL_SIZE: f32 = 12.0;
204
205const SHAPING_INK: Color = Color::BLACK;
209
210#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
213pub enum TextFieldVariant {
214 #[default]
217 Filled,
218 Outlined,
221}
222
223type OnText<State> = Rc<dyn Fn(&mut State, String)>;
226
227pub struct TextFieldView<State: 'static> {
229 value: String,
230 label: Option<String>,
231 supporting_text: Option<String>,
232 error_text: Option<String>,
233 leading: Option<AnyView<State>>,
234 trailing: Option<AnyView<State>>,
235 variant: TextFieldVariant,
236 obscured: bool,
237 enabled: bool,
238 on_change: OnText<State>,
239 on_submit: Option<OnText<State>>,
240}
241
242pub fn text_field<State: 'static, F: Fn(&mut State, String) + 'static>(
249 value: impl Into<String>,
250 on_change: F,
251) -> TextFieldView<State> {
252 TextFieldView {
253 value: value.into(),
254 label: None,
255 supporting_text: None,
256 error_text: None,
257 leading: None,
258 trailing: None,
259 variant: TextFieldVariant::Filled,
260 obscured: false,
261 enabled: true,
262 on_change: Rc::new(on_change),
263 on_submit: None,
264 }
265}
266
267#[allow(non_snake_case)]
269pub fn TextField<State: 'static, F: Fn(&mut State, String) + 'static>(
270 value: impl Into<String>,
271 on_change: F,
272) -> TextFieldView<State> {
273 text_field(value, on_change)
274}
275
276impl<State: 'static> TextFieldView<State> {
277 pub fn label(mut self, label: impl Into<String>) -> Self {
279 self.label = Some(label.into());
280 self
281 }
282
283 pub fn supporting_text(mut self, text: impl Into<String>) -> Self {
286 self.supporting_text = Some(text.into());
287 self
288 }
289
290 pub fn error_text(mut self, text: impl Into<String>) -> Self {
294 self.error_text = Some(text.into());
295 self
296 }
297
298 pub fn leading(mut self, view: impl View<State>) -> Self {
302 self.leading = Some(AnyView::new(view));
303 self
304 }
305
306 pub fn trailing(mut self, view: impl View<State>) -> Self {
309 self.trailing = Some(AnyView::new(view));
310 self
311 }
312
313 pub fn variant(mut self, variant: TextFieldVariant) -> Self {
315 self.variant = variant;
316 self
317 }
318
319 pub fn obscured(mut self, obscured: bool) -> Self {
322 self.obscured = obscured;
323 self
324 }
325
326 pub fn enabled(mut self, enabled: bool) -> Self {
330 self.enabled = enabled;
331 self
332 }
333
334 pub fn on_submit<F: Fn(&mut State, String) + 'static>(mut self, on_submit: F) -> Self {
336 self.on_submit = Some(Rc::new(on_submit));
337 self
338 }
339
340 fn has_error(&self) -> bool {
342 self.error_text.is_some()
343 }
344
345 fn supporting_line(&self) -> Option<&str> {
347 self.error_text
348 .as_deref()
349 .or(self.supporting_text.as_deref())
350 }
351
352 fn text_left(&self) -> f64 {
354 if self.leading.is_some() {
355 HPAD + ICON_SIZE + ICON_GAP
356 } else {
357 HPAD
358 }
359 }
360
361 fn text_top(&self) -> f64 {
364 if self.label.is_some() {
365 TEXT_TOP_WITH_LABEL
366 } else {
367 0.0
368 }
369 }
370
371 fn shape(&self) -> (bool, bool) {
373 (self.leading.is_some(), self.trailing.is_some())
374 }
375
376 fn field_view(&self) -> AnyView<State> {
380 let on_change = self.on_change.clone();
381 let mut field = text_input(self.value.clone(), move |state: &mut State, text| {
382 on_change(state, text)
383 })
384 .enabled(self.enabled)
385 .obscured(self.obscured)
386 .padding(self.text_left(), self.text_top())
387 .border_width(0.0)
388 .focus_ring_width(0.0)
389 .corner_radius(RADIUS);
390 if let Some(on_submit) = self.on_submit.clone() {
391 field = field.on_submit(move |state: &mut State, text| on_submit(state, text));
392 }
393 any(field)
394 }
395}
396
397struct Run {
404 content: String,
405 layout: Option<TextLayout>,
406 shaped_style: Option<TextStyle>,
407}
408
409impl Run {
410 fn new(content: impl Into<String>) -> Self {
412 Self {
413 content: content.into(),
414 layout: None,
415 shaped_style: None,
416 }
417 }
418
419 fn set_content(&mut self, content: &str) {
422 if self.content != content {
423 self.content = content.to_string();
424 self.layout = None;
425 }
426 }
427
428 fn content(&self) -> &str {
431 &self.content
432 }
433
434 fn size(&self) -> Size {
437 self.layout.as_ref().map_or(Size::ZERO, |l| l.size())
438 }
439
440 fn layout(&mut self, ctx: &mut LayoutCtx, style: &TextStyle) -> Size {
442 if let Some(cached) = &self.layout
443 && self.shaped_style.as_ref() == Some(style)
444 {
445 return cached.size();
446 }
447 let text_ctx = ctx.text_context::<TextContext>();
448 let laid = text_ctx.layout(&self.content, style, None);
449 let size = laid.size();
450 self.layout = Some(laid);
451 self.shaped_style = Some(style.clone());
452 size
453 }
454
455 fn paint(&self, origin: Point, color: Color, scene: &mut dyn PaintScene) {
458 let Some(layout) = &self.layout else {
459 return;
460 };
461 for mut run in layout.to_scene_runs(origin) {
462 run.brush = Brush::Solid(color);
463 scene.draw_glyph_run(run);
464 }
465 }
466}
467
468fn body_large(theme: Option<&Theme>) -> TextStyle {
472 let mut style = match theme {
473 Some(theme) => theme.type_scale.body_large.clone(),
474 None => TextStyle::new(BODY_LARGE_SIZE, SHAPING_INK),
475 };
476 style.color = SHAPING_INK;
477 style
478}
479
480fn body_small(theme: Option<&Theme>) -> TextStyle {
483 let mut style = match theme {
484 Some(theme) => theme.type_scale.body_small.clone(),
485 None => TextStyle::new(BODY_SMALL_SIZE, SHAPING_INK),
486 };
487 style.color = SHAPING_INK;
488 style
489}
490
491#[derive(Clone, Copy, Debug, PartialEq)]
493struct Colors {
494 stroke: Color,
496 label_float: Color,
498 label_rest: Color,
500 supporting: Color,
502}
503
504fn resolve_colors(
520 theme: Option<&Theme>,
521 variant: TextFieldVariant,
522 enabled: bool,
523 has_error: bool,
524 focused: bool,
525) -> Colors {
526 let (primary, error, on_surface, on_surface_variant, outline) = match theme {
527 Some(theme) => {
528 let s = theme.scheme();
529 (
530 s.primary,
531 s.error,
532 s.on_surface,
533 s.on_surface_variant,
534 s.outline,
535 )
536 }
537 None => (PRIMARY, ERROR, ON_SURFACE, ON_SURFACE_VARIANT, OUTLINE),
538 };
539 if !enabled {
540 let container = on_surface.multiply_alpha(DISABLED_CONTAINER_OPACITY);
541 let content = on_surface.multiply_alpha(DISABLED_CONTENT_OPACITY);
542 return Colors {
543 stroke: container,
544 label_float: content,
545 label_rest: content,
546 supporting: content,
547 };
548 }
549 let accent = if has_error { error } else { primary };
550 let idle = match variant {
551 TextFieldVariant::Filled => on_surface_variant,
552 TextFieldVariant::Outlined => outline,
553 };
554 Colors {
555 stroke: if focused || has_error { accent } else { idle },
556 label_float: accent,
557 label_rest: on_surface_variant,
558 supporting: if has_error { error } else { on_surface_variant },
559 }
560}
561
562#[derive(Clone, Copy)]
568struct Slots {
569 field: usize,
570 leading: Option<usize>,
571 trailing: Option<usize>,
572}
573
574pub struct TextFieldWidget {
576 children: Vec<ChildPod>,
577 slots: Slots,
578 variant: TextFieldVariant,
579 enabled: bool,
580 has_error: bool,
581 value_empty: bool,
584 text_left: f64,
586 label_rest: Option<Run>,
589 label_float: Option<Run>,
590 supporting: Option<Run>,
592 supporting_height: f64,
594 float: AnimationController,
596 float_target: bool,
598 float_priming: bool,
606}
607
608fn build_children<State: 'static>(
613 view: &TextFieldView<State>,
614 ctx: &mut BuildCtx<'_>,
615) -> (Vec<ChildPod>, Slots) {
616 let mut children = vec![frust::authoring::build_child(&view.field_view(), ctx)];
617 let field = children.len() - 1;
618 let leading = view.leading.as_ref().map(|v| {
619 children.push(frust::authoring::build_child(v, ctx));
620 children.len() - 1
621 });
622 let trailing = view.trailing.as_ref().map(|v| {
623 children.push(frust::authoring::build_child(v, ctx));
624 children.len() - 1
625 });
626 (
627 children,
628 Slots {
629 field,
630 leading,
631 trailing,
632 },
633 )
634}
635
636fn teardown_children<State: 'static>(
639 view: &TextFieldView<State>,
640 slots: Slots,
641 children: &mut [ChildPod],
642 ctx: &mut BuildCtx<'_>,
643) {
644 for (index, pod) in children.iter_mut().enumerate() {
645 if index == slots.field {
646 frust::authoring::teardown_child(&view.field_view(), pod, ctx);
647 } else if Some(index) == slots.leading
648 && let Some(v) = view.leading.as_ref()
649 {
650 frust::authoring::teardown_child(v, pod, ctx);
651 } else if Some(index) == slots.trailing
652 && let Some(v) = view.trailing.as_ref()
653 {
654 frust::authoring::teardown_child(v, pod, ctx);
655 }
656 }
657}
658
659fn sync_run(slot: &mut Option<Run>, content: Option<&str>) -> bool {
662 match (slot.as_mut(), content) {
663 (Some(run), Some(text)) => {
664 if run.content() == text {
665 return false;
666 }
667 run.set_content(text);
668 true
669 }
670 (None, Some(text)) => {
671 *slot = Some(Run::new(text));
672 true
673 }
674 (Some(_), None) => {
675 *slot = None;
676 true
677 }
678 (None, None) => false,
679 }
680}
681
682fn inside(pos: Point, size: Size) -> bool {
683 pos.x >= 0.0 && pos.y >= 0.0 && pos.x < size.width && pos.y < size.height
684}
685
686impl<State: 'static> View<State> for TextFieldView<State> {
687 type Element = TextFieldWidget;
688
689 fn build(&self, ctx: &mut BuildCtx<'_>) -> TextFieldWidget {
690 let (children, slots) = build_children(self, ctx);
691 let float_target = !self.value.is_empty();
692 let mut float =
696 AnimationController::new(MaterialMotion::SHORT_3).with_curve(MaterialMotion::STANDARD);
697 if float_target {
698 float.forward();
699 }
700 TextFieldWidget {
701 children,
702 slots,
703 variant: self.variant,
704 enabled: self.enabled,
705 has_error: self.has_error(),
706 value_empty: self.value.is_empty(),
707 text_left: self.text_left(),
708 label_rest: self.label.as_deref().map(Run::new),
709 label_float: self.label.as_deref().map(Run::new),
710 supporting: self.supporting_line().map(Run::new),
711 supporting_height: 0.0,
712 float,
713 float_target,
714 float_priming: true,
715 }
716 }
717
718 fn rebuild(
719 &self,
720 prev: &Self,
721 element: &mut TextFieldWidget,
722 ctx: &mut BuildCtx<'_>,
723 ) -> ChangeFlags {
724 let mut flags = ChangeFlags::NONE;
725
726 if self.shape() != prev.shape() {
727 teardown_children(prev, element.slots, &mut element.children, ctx);
730 let (children, slots) = build_children(self, ctx);
731 element.children = children;
732 element.slots = slots;
733 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
734 } else {
735 flags |= frust::authoring::rebuild_child(
736 &prev.field_view(),
737 &self.field_view(),
738 &mut element.children[element.slots.field],
739 ctx,
740 );
741 if let (Some(pv), Some(nv), Some(idx)) = (
742 prev.leading.as_ref(),
743 self.leading.as_ref(),
744 element.slots.leading,
745 ) {
746 flags |= frust::authoring::rebuild_child(pv, nv, &mut element.children[idx], ctx);
747 }
748 if let (Some(pv), Some(nv), Some(idx)) = (
749 prev.trailing.as_ref(),
750 self.trailing.as_ref(),
751 element.slots.trailing,
752 ) {
753 flags |= frust::authoring::rebuild_child(pv, nv, &mut element.children[idx], ctx);
754 }
755 }
756
757 if sync_run(&mut element.label_rest, self.label.as_deref())
760 | sync_run(&mut element.label_float, self.label.as_deref())
761 | sync_run(&mut element.supporting, self.supporting_line())
762 {
763 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
764 }
765 if element.text_left != self.text_left() {
766 element.text_left = self.text_left();
767 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
768 }
769 if element.variant != self.variant {
770 element.variant = self.variant;
771 flags |= ChangeFlags::PAINT;
772 }
773 if element.enabled != self.enabled {
774 element.enabled = self.enabled;
775 flags |= ChangeFlags::PAINT;
776 }
777 if element.has_error != self.has_error() {
778 element.has_error = self.has_error();
779 flags |= ChangeFlags::PAINT;
780 }
781 if element.value_empty != self.value.is_empty() {
782 element.value_empty = self.value.is_empty();
783 flags |= ChangeFlags::PAINT;
786 }
787 flags
788 }
789
790 fn teardown(&self, element: &mut TextFieldWidget, ctx: &mut BuildCtx<'_>) {
791 teardown_children(self, element.slots, &mut element.children, ctx);
792 }
793}
794
795impl Widget for TextFieldWidget {
796 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
797 let width = if bc.max().width.is_finite() {
798 bc.max().width
799 } else {
800 UNBOUNDED_WIDTH
801 };
802
803 let (rest_style, small_style) = {
806 let theme = Theme::from_layout_ctx(ctx);
807 (body_large(theme), body_small(theme))
808 };
809 if let Some(run) = &mut self.label_rest {
810 run.layout(ctx, &rest_style);
811 }
812 if let Some(run) = &mut self.label_float {
813 run.layout(ctx, &small_style);
814 }
815 self.supporting_height = match &mut self.supporting {
816 Some(run) => SUPPORTING_GAP + run.layout(ctx, &small_style).height,
817 None => 0.0,
818 };
819
820 let field = self.slots.field;
824 self.children[field].layout_child(
825 ctx,
826 &BoxConstraints::tight(Size::new(width, DECORATION_HEIGHT)),
827 );
828 self.children[field].set_origin(Point::ORIGIN);
829
830 let slot_bc = BoxConstraints::new(Size::ZERO, Size::new(ICON_SIZE, ICON_SIZE));
831 if let Some(idx) = self.slots.leading {
832 let size = self.children[idx].layout_child(ctx, &slot_bc);
833 self.children[idx].set_origin(icon_origin(HPAD, size));
834 }
835 if let Some(idx) = self.slots.trailing {
836 let size = self.children[idx].layout_child(ctx, &slot_bc);
837 self.children[idx].set_origin(icon_origin(width - HPAD - ICON_SIZE, size));
838 }
839
840 bc.constrain(Size::new(width, DECORATION_HEIGHT + self.supporting_height))
841 }
842
843 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
844 let focused = ctx.has_focus() && self.enabled;
847 let frame_time = ctx.frame_time();
848 let origin = ctx.origin();
849 let width = ctx.size().width;
850 let (colors, reduce_motion) = {
853 let theme = Theme::from_paint_ctx(ctx);
854 (
855 resolve_colors(theme, self.variant, self.enabled, self.has_error, focused),
856 theme.is_some_and(|t| t.motion.reduce_motion),
857 )
858 };
859
860 let floated = focused || !self.value_empty;
861 if self.float_target != floated {
862 self.float_target = floated;
863 if floated {
864 self.float.forward();
865 } else {
866 self.float.reverse();
867 }
868 }
869 let float_t = if reduce_motion {
870 if floated { 1.0 } else { 0.0 }
874 } else {
875 if self.float.advance(frame_time) {
876 ctx.request_frame();
877 }
878 if self.float_priming {
879 if !self.float.is_animating() {
882 self.float_priming = false;
883 }
884 if floated { 1.0 } else { 0.0 }
885 } else {
886 self.float.value_clamped()
887 }
888 };
889
890 self.children[self.slots.field].paint_child(ctx, scene);
893 if let Some(idx) = self.slots.leading {
894 self.children[idx].paint_child(ctx, scene);
895 }
896 if let Some(idx) = self.slots.trailing {
897 self.children[idx].paint_child(ctx, scene);
898 }
899
900 let stroke_w = if focused { STROKE_FOCUSED } else { STROKE_REST };
901 match self.variant {
902 TextFieldVariant::Filled => scene.fill_rect(
903 Point::new(origin.x, origin.y + DECORATION_HEIGHT - stroke_w),
904 Size::new(width, stroke_w),
905 colors.stroke,
906 ),
907 TextFieldVariant::Outlined => {
908 let half = stroke_w / 2.0;
912 let rr = RoundedRect::new(
913 half,
914 half,
915 width - half,
916 DECORATION_HEIGHT - half,
917 (RADIUS - half).max(0.0),
918 );
919 scene.stroke_path(
920 origin,
921 &rr.to_path(STROKE_TOLERANCE),
922 stroke_w,
923 &Brush::Solid(colors.stroke),
924 );
925 }
926 }
927
928 if let (Some(rest), Some(float)) = (&self.label_rest, &self.label_float) {
929 let x = origin.x + self.text_left;
930 let (rest_h, float_h) = (rest.size().height, float.size().height);
931 let rest_center = origin.y + DECORATION_HEIGHT / 2.0;
932 let float_center = origin.y + LABEL_FLOAT_TOP + float_h / 2.0;
933 let center = rest_center + (float_center - rest_center) * float_t;
934 if float_t < 1.0 {
935 rest.paint(
936 Point::new(x, center - rest_h / 2.0),
937 colors.label_rest.multiply_alpha(1.0 - float_t as f32),
938 scene,
939 );
940 }
941 if float_t > 0.0 {
942 float.paint(
943 Point::new(x, center - float_h / 2.0),
944 colors.label_float.multiply_alpha(float_t as f32),
945 scene,
946 );
947 }
948 }
949
950 if let Some(run) = &self.supporting {
951 run.paint(
952 Point::new(
953 origin.x + HPAD,
954 origin.y + DECORATION_HEIGHT + SUPPORTING_GAP,
955 ),
956 colors.supporting,
957 scene,
958 );
959 }
960 }
961
962 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
963 let result = frust::authoring::route_event(&mut self.children, ctx, event);
964 if let InputEvent::Pointer(p) = event
968 && matches!(p.phase, PointerPhase::Move)
969 && inside(p.position, Size::new(ctx.size().width, DECORATION_HEIGHT))
970 {
971 ctx.set_cursor(if self.enabled {
972 CursorIcon::Text
973 } else {
974 CursorIcon::NotAllowed
975 });
976 }
977 result
978 }
979
980 fn semantics(&self, ctx: &mut SemanticsCtx) {
981 ctx.push_container(
985 Role::Group,
986 |node| {
987 if let Some(label) = &self.label_rest {
988 node.set_label(label.content());
989 }
990 if let Some(supporting) = &self.supporting {
991 node.set_description(supporting.content());
992 }
993 },
994 |ctx| {
995 for pod in &self.children {
996 pod.semantics_child(ctx);
997 }
998 },
999 );
1000 }
1001
1002 frust::authoring::visit_children!(children);
1003}
1004
1005fn icon_origin(x: f64, measured: Size) -> Point {
1009 Point::new(
1010 x + ((ICON_SIZE - measured.width) / 2.0).max(0.0),
1011 ((DECORATION_HEIGHT - measured.height) / 2.0).max(0.0),
1012 )
1013}
1014
1015#[cfg(test)]
1016mod tests {
1017 use super::*;
1018 use frust::authoring::scene::GlyphRun;
1019 use frust::authoring::{BezPath, Key, KeyEvent, Modifiers, PointerButton, PointerEvent, Rect};
1020 use frust::{Brightness, FrameTime};
1021 use frust_core::RenderRoot;
1022 use std::any::Any;
1023
1024 const WINDOW: Size = Size::new(320.0, 160.0);
1026 const IN_FIELD: Point = Point::new(120.0, 28.0);
1028
1029 #[derive(Default)]
1034 struct Recorder {
1035 rrects: Vec<(Point, Size, f64, Color)>,
1036 rects: Vec<(Point, Size, Color)>,
1037 strokes: Vec<(Rect, f64, Color)>,
1038 runs: Vec<(f32, Point, Color)>,
1039 chrome_start: usize,
1047 }
1048
1049 impl PaintScene for Recorder {
1050 fn fill_rect(&mut self, origin: Point, size: Size, color: Color) {
1051 self.rects.push((origin, size, color));
1052 self.chrome_start = self.runs.len();
1053 }
1054 fn draw_text(&mut self, _origin: Point, _text: &str) {}
1055 fn fill_rounded_rect(&mut self, origin: Point, size: Size, radius: f64, color: Color) {
1056 self.rrects.push((origin, size, radius, color));
1057 }
1058 fn stroke_path(&mut self, origin: Point, path: &BezPath, width: f64, brush: &Brush) {
1059 let bbox = path.bounding_box() + origin.to_vec2();
1060 let color = match brush {
1061 Brush::Solid(c) => *c,
1062 _ => Color::TRANSPARENT,
1063 };
1064 self.strokes.push((bbox, width, color));
1065 self.chrome_start = self.runs.len();
1066 }
1067 fn draw_glyph_run(&mut self, run: GlyphRun) {
1068 let color = match run.brush {
1069 Brush::Solid(c) => c,
1070 _ => Color::TRANSPARENT,
1071 };
1072 let t = run.transform.translation();
1073 self.runs.push((run.font_size, Point::new(t.x, t.y), color));
1074 }
1075 }
1076
1077 impl Recorder {
1078 fn indicator(&self) -> (Point, Size, Color) {
1081 *self
1082 .rects
1083 .last()
1084 .expect("a filled field paints an indicator")
1085 }
1086
1087 fn outline(&self) -> (Rect, f64, Color) {
1089 *self
1090 .strokes
1091 .last()
1092 .expect("an outlined field strokes an outline")
1093 }
1094
1095 fn runs_at(&self, font_size: f32) -> Vec<(Point, Color)> {
1098 self.runs[self.chrome_start..]
1099 .iter()
1100 .filter(|(size, _, _)| (*size - font_size).abs() < 1e-3)
1101 .map(|(_, origin, color)| (*origin, *color))
1102 .collect()
1103 }
1104 }
1105
1106 struct Harness {
1110 root: RenderRoot<String, TextFieldView<String>>,
1111 state: String,
1112 tcx: TextContext,
1113 variant: TextFieldVariant,
1114 label: Option<String>,
1115 supporting: Option<String>,
1116 error: Option<String>,
1117 enabled: bool,
1118 }
1119
1120 impl Harness {
1121 fn new(variant: TextFieldVariant) -> Self {
1122 let mut h = Harness {
1123 root: RenderRoot::new(),
1124 state: String::new(),
1125 tcx: TextContext::new(),
1126 variant,
1127 label: Some("Email".to_string()),
1128 supporting: None,
1129 error: None,
1130 enabled: true,
1131 };
1132 h.pass();
1133 h
1134 }
1135
1136 fn theme(&mut self, brightness: Brightness) -> Theme {
1137 let theme = crate::baseline().with_brightness(brightness);
1138 self.root.set_theme(Box::new(theme.clone()));
1139 self.pass();
1140 theme
1141 }
1142
1143 fn pass(&mut self) {
1144 let (variant, enabled) = (self.variant, self.enabled);
1145 let (label, supporting, error) = (
1146 self.label.clone(),
1147 self.supporting.clone(),
1148 self.error.clone(),
1149 );
1150 let mut logic = move |state: &mut String| {
1151 let mut view = text_field::<String, _>(state.clone(), |s: &mut String, t| *s = t)
1152 .variant(variant)
1153 .enabled(enabled);
1154 if let Some(label) = &label {
1155 view = view.label(label.clone());
1156 }
1157 if let Some(supporting) = &supporting {
1158 view = view.supporting_text(supporting.clone());
1159 }
1160 if let Some(error) = &error {
1161 view = view.error_text(error.clone());
1162 }
1163 view
1164 };
1165 self.root.rebuild(&mut logic, &mut self.state);
1166 self.root
1167 .layout_with_text(WINDOW, &mut self.tcx as &mut dyn Any);
1168 }
1169
1170 fn paint(&mut self) -> Recorder {
1171 self.paint_at(FrameTime::ZERO)
1172 }
1173
1174 fn paint_at(&mut self, now: FrameTime) -> Recorder {
1175 let mut rec = Recorder::default();
1176 self.root.paint(&mut rec, now);
1177 rec
1178 }
1179
1180 fn pointer(&mut self, phase: PointerPhase, at: Point) {
1181 self.root.event(
1182 &mut self.state,
1183 &InputEvent::Pointer(PointerEvent {
1184 phase,
1185 position: at,
1186 button: PointerButton::Primary,
1187 }),
1188 );
1189 }
1190
1191 fn focus(&mut self) {
1193 self.pointer(PointerPhase::Down, IN_FIELD);
1194 self.pointer(PointerPhase::Up, IN_FIELD);
1195 }
1196
1197 fn blur(&mut self) {
1200 self.pointer(PointerPhase::Down, Point::new(160.0, 140.0));
1201 self.pointer(PointerPhase::Up, Point::new(160.0, 140.0));
1202 }
1203
1204 fn key(&mut self, ch: char) {
1205 self.root.event(
1206 &mut self.state,
1207 &InputEvent::Key(KeyEvent {
1208 key: Key::Character(ch.to_string()),
1209 modifiers: Modifiers::default(),
1210 repeat: false,
1211 }),
1212 );
1213 }
1214 }
1215
1216 fn secs(s: f64) -> FrameTime {
1217 FrameTime::from_nanos((s * 1_000_000_000.0) as u64)
1218 }
1219
1220 fn build<S: 'static>(view: &TextFieldView<S>) -> TextFieldWidget {
1221 let mut counter = 0u64;
1222 View::<S>::build(view, &mut BuildCtx::new(&mut counter))
1223 }
1224
1225 fn layout_widget(w: &mut TextFieldWidget, width: f64) -> Size {
1226 let mut tcx = TextContext::new();
1227 let mut lctx = LayoutCtx::with_text_context(&mut tcx as &mut dyn Any);
1228 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(width, 400.0)))
1229 }
1230
1231 #[test]
1234 fn the_wrapped_field_covers_the_whole_decoration_box() {
1235 let view: TextFieldView<String> = text_field("", |_s: &mut String, _t| {}).label("Email");
1236 let mut w = build(&view);
1237 let size = layout_widget(&mut w, 300.0);
1238 assert_eq!(
1239 size,
1240 Size::new(300.0, DECORATION_HEIGHT),
1241 "no supporting line"
1242 );
1243 let field = &w.children[w.slots.field];
1244 assert_eq!(field.origin(), Point::ORIGIN);
1245 assert_eq!(
1246 field.size(),
1247 Size::new(300.0, DECORATION_HEIGHT),
1248 "tight constraints make the baseline's background the container fill"
1249 );
1250 }
1251
1252 #[test]
1253 fn a_supporting_line_grows_the_widget_below_the_decoration_box() {
1254 let view: TextFieldView<String> =
1255 text_field("", |_s: &mut String, _t| {}).supporting_text("Required");
1256 let mut w = build(&view);
1257 let size = layout_widget(&mut w, 300.0);
1258 assert!(size.height > DECORATION_HEIGHT);
1259 assert_eq!(size.height, DECORATION_HEIGHT + w.supporting_height);
1260 assert!(
1261 w.supporting_height > SUPPORTING_GAP,
1262 "gap plus a shaped line"
1263 );
1264 assert_eq!(
1267 w.children[w.slots.field].size(),
1268 Size::new(300.0, DECORATION_HEIGHT)
1269 );
1270 }
1271
1272 #[test]
1273 fn an_unbounded_width_falls_back_to_the_baseline_default() {
1274 let view: TextFieldView<String> = text_field("", |_s: &mut String, _t| {});
1275 let mut w = build(&view);
1276 let mut tcx = TextContext::new();
1277 let mut lctx = LayoutCtx::with_text_context(&mut tcx as &mut dyn Any);
1278 let size = w.layout(
1279 &mut lctx,
1280 &BoxConstraints::loose(Size::new(f64::INFINITY, 400.0)),
1281 );
1282 assert_eq!(size.width, UNBOUNDED_WIDTH);
1283 }
1284
1285 #[test]
1286 fn a_leading_slot_insets_the_text_and_both_slots_are_placed_and_centered() {
1287 let plain: TextFieldView<String> = text_field("", |_s: &mut String, _t| {});
1288 assert_eq!(plain.text_left(), HPAD);
1289 assert_eq!(plain.text_top(), 0.0, "no label leaves the field centered");
1290
1291 let view: TextFieldView<String> = text_field("", |_s: &mut String, _t| {})
1292 .label("Email")
1293 .leading(frust::SizedBox(Some(20.0), Some(20.0)))
1294 .trailing(frust::SizedBox(Some(20.0), Some(20.0)));
1295 assert_eq!(view.text_left(), HPAD + ICON_SIZE + ICON_GAP);
1296 assert_eq!(view.text_top(), TEXT_TOP_WITH_LABEL);
1297
1298 let mut w = build(&view);
1299 layout_widget(&mut w, 300.0);
1300 let leading = &w.children[w.slots.leading.expect("leading slot")];
1301 let trailing = &w.children[w.slots.trailing.expect("trailing slot")];
1302 assert_eq!(leading.origin(), Point::new(HPAD + 2.0, 18.0));
1303 assert_eq!(
1304 trailing.origin(),
1305 Point::new(300.0 - HPAD - ICON_SIZE + 2.0, 18.0)
1306 );
1307 }
1308
1309 #[test]
1312 fn a_filled_field_paints_an_indicator_and_an_outlined_one_an_outline() {
1313 let mut filled = Harness::new(TextFieldVariant::Filled);
1314 let theme = filled.theme(Brightness::Light);
1315 let rec = filled.paint();
1316 let (origin, size, color) = rec.indicator();
1317 assert_eq!(origin, Point::new(0.0, DECORATION_HEIGHT - STROKE_REST));
1318 assert_eq!(size, Size::new(WINDOW.width, STROKE_REST));
1319 assert_eq!(
1320 color,
1321 theme.scheme().on_surface_variant,
1322 "an idle filled indicator is onSurfaceVariant"
1323 );
1324 assert!(rec.strokes.is_empty(), "the filled variant strokes nothing");
1325 assert!(
1327 rec.rrects
1328 .iter()
1329 .any(|(_, _, _, c)| *c == theme.scheme().surface),
1330 "the wrapped field painted its background"
1331 );
1332
1333 let mut outlined = Harness::new(TextFieldVariant::Outlined);
1334 let theme = outlined.theme(Brightness::Light);
1335 let rec = outlined.paint();
1336 let (bbox, width, color) = rec.outline();
1337 assert_eq!(width, STROKE_REST);
1338 assert_eq!(
1339 color,
1340 theme.scheme().outline,
1341 "an idle outline is `outline`"
1342 );
1343 assert_eq!(bbox.width(), WINDOW.width - STROKE_REST);
1344 assert_eq!(bbox.height(), DECORATION_HEIGHT - STROKE_REST);
1345 assert!(
1346 rec.rects.is_empty(),
1347 "the outlined variant fills no indicator"
1348 );
1349 }
1350
1351 #[test]
1352 fn focus_thickens_and_accents_both_variants() {
1353 for variant in [TextFieldVariant::Filled, TextFieldVariant::Outlined] {
1354 let mut h = Harness::new(variant);
1355 let theme = h.theme(Brightness::Light);
1356 h.focus();
1357 let rec = h.paint();
1358 let (width, color) = match variant {
1359 TextFieldVariant::Filled => {
1360 let (origin, size, color) = rec.indicator();
1361 assert_eq!(origin.y, DECORATION_HEIGHT - STROKE_FOCUSED);
1362 (size.height, color)
1363 }
1364 TextFieldVariant::Outlined => {
1365 let (_, width, color) = rec.outline();
1366 (width, color)
1367 }
1368 };
1369 assert_eq!(width, STROKE_FOCUSED, "{variant:?} thickens on focus");
1370 assert_eq!(
1371 color,
1372 theme.scheme().primary,
1373 "{variant:?} takes the accent"
1374 );
1375 }
1376 }
1377
1378 #[test]
1379 fn an_error_field_accents_the_stroke_even_unfocused() {
1380 for variant in [TextFieldVariant::Filled, TextFieldVariant::Outlined] {
1381 let mut h = Harness::new(variant);
1382 h.error = Some("Enter a valid address".to_string());
1383 let theme = h.theme(Brightness::Light);
1384 let rec = h.paint();
1385 let (width, color) = match variant {
1386 TextFieldVariant::Filled => {
1387 let (_, size, color) = rec.indicator();
1388 (size.height, color)
1389 }
1390 TextFieldVariant::Outlined => {
1391 let (_, width, color) = rec.outline();
1392 (width, color)
1393 }
1394 };
1395 assert_eq!(color, theme.scheme().error, "{variant:?} strokes `error`");
1396 assert_eq!(
1397 width, STROKE_REST,
1398 "error alone does not thicken the stroke"
1399 );
1400 }
1401 }
1402
1403 #[test]
1404 fn a_disabled_field_dims_container_and_content_and_refuses_focus() {
1405 for variant in [TextFieldVariant::Filled, TextFieldVariant::Outlined] {
1406 let mut h = Harness::new(variant);
1407 h.enabled = false;
1408 h.supporting = Some("Required".to_string());
1409 let theme = h.theme(Brightness::Light);
1410 let on_surface = theme.scheme().on_surface;
1411 let rec = h.paint();
1412 let stroke = match variant {
1413 TextFieldVariant::Filled => rec.indicator().2,
1414 TextFieldVariant::Outlined => rec.outline().2,
1415 };
1416 assert_eq!(
1417 stroke,
1418 on_surface.multiply_alpha(DISABLED_CONTAINER_OPACITY),
1419 "{variant:?} dims its container ink to 12%"
1420 );
1421 let content = on_surface.multiply_alpha(DISABLED_CONTENT_OPACITY);
1422 assert!(
1423 rec.runs.iter().any(|(_, _, c)| *c == content),
1424 "{variant:?} dims its label/supporting ink to 38%"
1425 );
1426
1427 h.focus();
1429 let rec = h.paint();
1430 let width = match variant {
1431 TextFieldVariant::Filled => rec.indicator().1.height,
1432 TextFieldVariant::Outlined => rec.outline().1,
1433 };
1434 assert_eq!(width, STROKE_REST);
1435 }
1436 }
1437
1438 #[test]
1439 fn an_unthemed_pass_paints_the_fallback_roles() {
1440 let view: TextFieldView<String> = text_field("", |_s: &mut String, _t| {}).label("Email");
1441 let mut w = build(&view);
1442 let size = layout_widget(&mut w, 300.0);
1443 let mut rec = Recorder::default();
1444 let mut ctx = PaintCtx::new(Point::ORIGIN, size);
1445 w.paint(&mut ctx, &mut rec);
1446 assert_eq!(rec.indicator().2, ON_SURFACE_VARIANT);
1447 assert_eq!(
1448 rec.runs_at(BODY_LARGE_SIZE)[0].1,
1449 ON_SURFACE_VARIANT,
1450 "a resting label is onSurfaceVariant"
1451 );
1452 }
1453
1454 #[test]
1458 fn the_label_floats_on_focus_or_content_and_rests_only_when_neither() {
1459 let mut h = Harness::new(TextFieldVariant::Filled);
1461 h.theme(Brightness::Light);
1462 let rec = h.paint();
1463 assert_eq!(rec.runs_at(BODY_SMALL_SIZE).len(), 0, "no floated run");
1464 assert_eq!(rec.runs_at(BODY_LARGE_SIZE).len(), 1, "the resting run");
1465
1466 h.focus();
1468 h.paint_at(secs(0.0));
1469 let rec = h.paint_at(secs(1.0));
1470 assert_eq!(rec.runs_at(BODY_LARGE_SIZE).len(), 0);
1471 assert_eq!(rec.runs_at(BODY_SMALL_SIZE).len(), 1);
1472
1473 h.key('a');
1475 h.pass();
1476 assert_eq!(h.state, "a");
1477 let rec = h.paint_at(secs(2.0));
1478 assert_eq!(rec.runs_at(BODY_SMALL_SIZE).len(), 1);
1479
1480 h.blur();
1482 let rec = h.paint_at(secs(3.0));
1483 assert_eq!(
1484 rec.runs_at(BODY_SMALL_SIZE).len(),
1485 1,
1486 "content alone keeps the label up"
1487 );
1488 assert_eq!(rec.runs_at(BODY_LARGE_SIZE).len(), 0);
1489 }
1490
1491 #[test]
1492 fn the_float_transition_crossfades_and_travels_upward() {
1493 let mut h = Harness::new(TextFieldVariant::Filled);
1494 h.theme(Brightness::Light);
1495 let resting = h.paint_at(secs(0.0)).runs_at(BODY_LARGE_SIZE)[0].0;
1496
1497 h.focus();
1498 h.paint_at(secs(10.0));
1500 let mid = h.paint_at(secs(10.0 + MaterialMotion::SHORT_3.as_secs_f64() / 2.0));
1501 let rest_run = mid.runs_at(BODY_LARGE_SIZE);
1502 let float_run = mid.runs_at(BODY_SMALL_SIZE);
1503 assert_eq!(rest_run.len(), 1, "both layers are live mid-crossfade");
1504 assert_eq!(float_run.len(), 1);
1505 let rest_alpha = rest_run[0].1.components[3];
1506 let float_alpha = float_run[0].1.components[3];
1507 assert!(rest_alpha > 0.0 && rest_alpha < 1.0, "{rest_alpha}");
1508 assert!(float_alpha > 0.0 && float_alpha < 1.0, "{float_alpha}");
1509 assert!(
1510 (rest_alpha + float_alpha - 1.0).abs() < 1e-3,
1511 "the crossfade is complementary"
1512 );
1513 assert!(
1514 rest_run[0].0.y < resting.y,
1515 "the label has started travelling"
1516 );
1517
1518 let done = h.paint_at(secs(11.0));
1520 let floated = done.runs_at(BODY_SMALL_SIZE);
1521 assert_eq!(floated.len(), 1);
1522 assert!(done.runs_at(BODY_LARGE_SIZE).is_empty());
1523 assert!(floated[0].0.y >= LABEL_FLOAT_TOP);
1524 assert!(floated[0].0.y < resting.y);
1525 assert_eq!(floated[0].1.components[3], 1.0, "fully opaque once settled");
1526 }
1527
1528 #[test]
1529 fn a_floated_label_takes_the_accent_and_a_resting_one_does_not() {
1530 let mut h = Harness::new(TextFieldVariant::Filled);
1531 let theme = h.theme(Brightness::Light);
1532 let resting = h.paint().runs_at(BODY_LARGE_SIZE)[0].1;
1533 assert_eq!(resting, theme.scheme().on_surface_variant);
1534
1535 h.focus();
1536 h.paint_at(secs(0.0));
1537 let floated = h.paint_at(secs(1.0)).runs_at(BODY_SMALL_SIZE)[0].1;
1538 assert_eq!(floated, theme.scheme().primary);
1539
1540 h.error = Some("Bad".to_string());
1542 h.pass();
1543 let floated = h.paint_at(secs(2.0)).runs_at(BODY_SMALL_SIZE)[0].1;
1544 assert_eq!(floated, theme.scheme().error);
1545 }
1546
1547 #[test]
1548 fn reduce_motion_collapses_the_float_and_asks_for_no_frames() {
1549 let mut theme = crate::baseline().with_brightness(Brightness::Light);
1550 theme.motion.reduce_motion = true;
1551 let view: TextFieldView<String> = text_field("", |_s: &mut String, _t| {}).label("Email");
1552 let mut w = build(&view);
1553 let size = layout_widget(&mut w, 300.0);
1554
1555 let mut rec = Recorder::default();
1557 let mut ctx = PaintCtx::new(Point::ORIGIN, size).with_theme(&theme as &dyn Any);
1558 w.paint(&mut ctx, &mut rec);
1559 assert_eq!(rec.runs_at(BODY_LARGE_SIZE).len(), 1);
1560 assert!(!ctx.needs_frame(), "a collapsed float asks for no frame");
1561
1562 w.value_empty = false;
1565 let mut rec = Recorder::default();
1566 let mut ctx = PaintCtx::new(Point::ORIGIN, size).with_theme(&theme as &dyn Any);
1567 w.paint(&mut ctx, &mut rec);
1568 let floated = rec.runs_at(BODY_SMALL_SIZE);
1569 assert_eq!(floated.len(), 1);
1570 assert_eq!(floated[0].1.components[3], 1.0);
1571 assert!(rec.runs_at(BODY_LARGE_SIZE).is_empty());
1572 assert!(!ctx.needs_frame());
1573 }
1574
1575 #[test]
1576 fn a_field_built_with_content_starts_floated() {
1577 let view: TextFieldView<String> =
1578 text_field("already here", |_s: &mut String, _t| {}).label("Email");
1579 let mut w = build(&view);
1580 let size = layout_widget(&mut w, 300.0);
1581 let mut rec = Recorder::default();
1582 let mut ctx = PaintCtx::new(Point::ORIGIN, size);
1583 w.paint(&mut ctx, &mut rec);
1584 assert_eq!(rec.runs_at(BODY_SMALL_SIZE).len(), 1, "no fade-in on mount");
1585 assert!(rec.runs_at(BODY_LARGE_SIZE).is_empty());
1586 }
1587
1588 #[test]
1591 fn the_error_text_outranks_the_helper_text() {
1592 let helper: TextFieldView<String> =
1593 text_field("", |_s: &mut String, _t| {}).supporting_text("Helper");
1594 assert_eq!(helper.supporting_line(), Some("Helper"));
1595
1596 let both: TextFieldView<String> = text_field("", |_s: &mut String, _t| {})
1597 .supporting_text("Helper")
1598 .error_text("Wrong");
1599 assert_eq!(both.supporting_line(), Some("Wrong"), "error wins");
1600 assert!(both.has_error());
1601
1602 let neither: TextFieldView<String> = text_field("", |_s: &mut String, _t| {});
1603 assert_eq!(neither.supporting_line(), None);
1604
1605 let mut h = Harness::new(TextFieldVariant::Filled);
1607 h.label = None;
1608 h.supporting = Some("Helper".to_string());
1609 let theme = h.theme(Brightness::Light);
1610 let helper_run = h.paint().runs_at(BODY_SMALL_SIZE);
1611 assert_eq!(helper_run.len(), 1);
1612 assert_eq!(helper_run[0].1, theme.scheme().on_surface_variant);
1613
1614 h.error = Some("Wrong".to_string());
1615 h.pass();
1616 let error_run = h.paint().runs_at(BODY_SMALL_SIZE);
1617 assert_eq!(error_run.len(), 1, "one line, not two");
1618 assert_eq!(error_run[0].1, theme.scheme().error);
1619 assert_eq!(
1620 error_run[0].0,
1621 Point::new(HPAD, DECORATION_HEIGHT + SUPPORTING_GAP),
1622 "placed below the decoration box"
1623 );
1624 }
1625
1626 #[test]
1629 fn typing_round_trips_through_the_callback_and_never_self_mutates() {
1630 let mut h = Harness::new(TextFieldVariant::Filled);
1631 h.focus();
1632 h.key('h');
1633 h.key('i');
1634 assert_eq!(h.state, "hi", "each edit reported through on_change");
1635
1636 h.state = "frozen".to_string();
1639 h.pass();
1640 assert_eq!(h.state, "frozen");
1641 }
1642
1643 #[test]
1644 fn the_cursor_is_text_over_the_field_and_not_allowed_when_disabled() {
1645 let mut h = Harness::new(TextFieldVariant::Filled);
1646 h.supporting = Some("Required".to_string());
1647 h.pass();
1648 h.pointer(PointerPhase::Move, IN_FIELD);
1649 assert_eq!(h.root.cursor(), CursorIcon::Text);
1650 h.pointer(
1652 PointerPhase::Move,
1653 Point::new(120.0, DECORATION_HEIGHT + 6.0),
1654 );
1655 assert_eq!(h.root.cursor(), CursorIcon::Default);
1656
1657 h.enabled = false;
1658 h.pass();
1659 h.pointer(PointerPhase::Move, IN_FIELD);
1660 assert_eq!(h.root.cursor(), CursorIcon::NotAllowed);
1661 }
1662
1663 #[test]
1664 fn visit_children_publishes_every_mounted_pod() {
1665 let bare: TextFieldView<String> = text_field("", |_s: &mut String, _t| {});
1666 let w = build(&bare);
1667 let mut seen = 0usize;
1668 Widget::visit_children(&w, &mut |_pod| seen += 1);
1669 assert_eq!(seen, 1, "the wrapped field alone");
1670
1671 let slotted: TextFieldView<String> = text_field("", |_s: &mut String, _t| {})
1672 .leading(frust::SizedBox(Some(20.0), Some(20.0)))
1673 .trailing(frust::SizedBox(Some(20.0), Some(20.0)));
1674 let w = build(&slotted);
1675 let mut seen = 0usize;
1676 Widget::visit_children(&w, &mut |_pod| seen += 1);
1677 assert_eq!(seen, 3);
1678 }
1679
1680 #[test]
1681 fn semantics_is_a_labelled_group_around_the_editables_own_node() {
1682 let mut logic = |state: &mut String| {
1683 text_field(state.clone(), |s: &mut String, t| *s = t)
1684 .label("Email")
1685 .error_text("Enter a valid address")
1686 };
1687 let mut root: RenderRoot<String, TextFieldView<String>> = RenderRoot::new();
1688 let mut state = String::new();
1689 root.rebuild(&mut logic, &mut state);
1690 let mut tcx = TextContext::new();
1691 root.layout_with_text(WINDOW, &mut tcx as &mut dyn Any);
1692 let update = root.semantics();
1693 let group = update
1694 .nodes
1695 .iter()
1696 .find(|(_, n)| n.role() == Role::Group)
1697 .expect("a group node");
1698 assert_eq!(group.1.label(), Some("Email"));
1699 assert_eq!(group.1.description(), Some("Enter a valid address"));
1700 assert!(
1701 update
1702 .nodes
1703 .iter()
1704 .any(|(_, n)| n.role() == frust::authoring::Role::TextInput),
1705 "the wrapped editable's node reaches the tree through the chrome"
1706 );
1707 }
1708
1709 #[test]
1710 fn adding_a_slot_rebuilds_the_pod_set() {
1711 let mut root: RenderRoot<bool, TextFieldView<bool>> = RenderRoot::new();
1712 let mut state = false;
1713 let mut logic = |with_slot: &mut bool| {
1714 let view: TextFieldView<bool> = text_field("", |_s: &mut bool, _t| {});
1715 if *with_slot {
1716 view.leading(frust::SizedBox(Some(20.0), Some(20.0)))
1717 } else {
1718 view
1719 }
1720 };
1721 root.rebuild(&mut logic, &mut state);
1722 let mut tcx = TextContext::new();
1723 root.layout_with_text(WINDOW, &mut tcx as &mut dyn Any);
1724 state = true;
1725 root.rebuild(&mut logic, &mut state);
1726 root.layout_with_text(WINDOW, &mut tcx as &mut dyn Any);
1727 state = false;
1730 root.rebuild(&mut logic, &mut state);
1731 root.layout_with_text(WINDOW, &mut tcx as &mut dyn Any);
1732 }
1733}