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