1use crate::env::TextSelectionHandleKind;
2use crate::lowering::{InternalIrBuilder, InternalLoweringCx};
3use crate::ui::{
4 traits::InternalLower, Button, ButtonContentAlign, ButtonVariant, Container, Positioned, Row,
5 Spacer, Text, TextContent, TextFontStyle, Widget,
6};
7use crate::ActionEnvelope;
8use fission_ir::{
9 op::{
10 Color as IrColor, Fill, LayoutOp, Op, PaintOp, Stroke, TextAlign as IrTextAlign,
11 TextParagraphStyle,
12 },
13 semantics::{
14 InputFormatter, MaxLengthEnforcement, MouseCursor as SemanticsMouseCursor,
15 TextCapitalization, TextInputAction, TextInputType,
16 },
17 AnyRenderObject, FlexDirection, FlexWrap, Role, Semantics, WidgetId,
18};
19use fission_theme::{ComponentSize, ComponentState};
20use serde::{Deserialize, Serialize};
21use std::sync::Arc;
22use unicode_segmentation::UnicodeSegmentation;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
25pub enum TextAlignVertical {
26 Top,
27 #[default]
28 Center,
29 Bottom,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
33pub enum DragStartBehavior {
34 #[default]
35 Start,
36 Down,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct TextUndoController {
41 pub capacity: usize,
42}
43
44impl Default for TextUndoController {
45 fn default() -> Self {
46 Self { capacity: 100 }
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51pub struct SpellCheckConfiguration {
52 pub enabled: bool,
53 pub underline_color: Option<IrColor>,
54 pub show_suggestions: bool,
55}
56
57impl Default for SpellCheckConfiguration {
58 fn default() -> Self {
59 Self {
60 enabled: true,
61 underline_color: Some(IrColor {
62 r: 255,
63 g: 59,
64 b: 48,
65 a: 255,
66 }),
67 show_suggestions: true,
68 }
69 }
70}
71
72#[doc(hidden)]
73#[derive(Debug, Clone, PartialEq)]
74pub struct TextInputRuntimeConfig {
75 pub drag_start_behavior: DragStartBehavior,
76 pub undo_controller: Option<TextUndoController>,
77 pub restoration_id: Option<String>,
78 pub spell_check_configuration: Option<SpellCheckConfiguration>,
79}
80
81#[doc(hidden)]
82pub fn downcast_text_input_runtime_config(
83 any: &AnyRenderObject,
84) -> Option<&TextInputRuntimeConfig> {
85 any.downcast_ref::<TextInputRuntimeConfig>()
86}
87
88impl TextAlignVertical {
89 fn justify_content(self) -> fission_ir::op::JustifyContent {
90 match self {
91 Self::Top => fission_ir::op::JustifyContent::Start,
92 Self::Center => fission_ir::op::JustifyContent::Center,
93 Self::Bottom => fission_ir::op::JustifyContent::End,
94 }
95 }
96
97 fn align_items(self) -> fission_ir::op::AlignItems {
98 match self {
99 Self::Top => fission_ir::op::AlignItems::Start,
100 Self::Center => fission_ir::op::AlignItems::Center,
101 Self::Bottom => fission_ir::op::AlignItems::End,
102 }
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
107pub enum TextContextMenuAction {
108 Copy,
109 Cut,
110 Paste,
111 SelectAll,
112}
113
114impl TextContextMenuAction {
115 pub fn label(self) -> &'static str {
116 match self {
117 Self::Copy => "Copy",
118 Self::Cut => "Cut",
119 Self::Paste => "Paste",
120 Self::SelectAll => "Select All",
121 }
122 }
123}
124
125#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
126pub struct TextContextMenuConfig {
127 pub enabled: bool,
128 pub actions: Vec<TextContextMenuAction>,
129 pub padding: [f32; 4],
130 pub gap: f32,
131 pub border_radius: f32,
132}
133
134impl Default for TextContextMenuConfig {
135 fn default() -> Self {
136 Self {
137 enabled: true,
138 actions: vec![
139 TextContextMenuAction::Copy,
140 TextContextMenuAction::Cut,
141 TextContextMenuAction::Paste,
142 TextContextMenuAction::SelectAll,
143 ],
144 padding: [10.0, 10.0, 8.0, 8.0],
145 gap: 6.0,
146 border_radius: 12.0,
147 }
148 }
149}
150
151#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
152pub struct TextSelectionControls {
153 pub show_collapsed_handle: bool,
154 pub handle_radius: f32,
155 pub handle_fill: IrColor,
156 pub handle_stroke: Option<IrColor>,
157 pub handle_stroke_width: f32,
158}
159
160impl Default for TextSelectionControls {
161 fn default() -> Self {
162 Self {
163 show_collapsed_handle: true,
164 handle_radius: 7.0,
165 handle_fill: IrColor {
166 r: 0,
167 g: 122,
168 b: 255,
169 a: 255,
170 },
171 handle_stroke: Some(IrColor {
172 r: 255,
173 g: 255,
174 b: 255,
175 a: 255,
176 }),
177 handle_stroke_width: 1.0,
178 }
179 }
180}
181
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
183pub struct TextMagnifierConfiguration {
184 pub enabled: bool,
185 pub diameter: f32,
186 pub scale: f32,
187 pub border_radius: f32,
188 pub border_color: Option<IrColor>,
189 pub border_width: f32,
190}
191
192impl Default for TextMagnifierConfiguration {
193 fn default() -> Self {
194 Self {
195 enabled: true,
196 diameter: 84.0,
197 scale: 1.4,
198 border_radius: 18.0,
199 border_color: Some(IrColor {
200 r: 210,
201 g: 214,
202 b: 224,
203 a: 255,
204 }),
205 border_width: 1.0,
206 }
207 }
208}
209
210pub(crate) fn text_input_selection_handle_id(
211 input_id: WidgetId,
212 kind: TextSelectionHandleKind,
213) -> WidgetId {
214 let suffix = match kind {
215 TextSelectionHandleKind::Caret => 0,
216 TextSelectionHandleKind::Start => 1,
217 TextSelectionHandleKind::End => 2,
218 };
219 WidgetId::derived(input_id.as_u128(), &[900, suffix])
220}
221
222pub(crate) fn text_input_toolbar_button_id(
223 input_id: WidgetId,
224 action: TextContextMenuAction,
225) -> WidgetId {
226 let suffix = match action {
227 TextContextMenuAction::Copy => 0,
228 TextContextMenuAction::Cut => 1,
229 TextContextMenuAction::Paste => 2,
230 TextContextMenuAction::SelectAll => 3,
231 };
232 WidgetId::derived(input_id.as_u128(), &[901, suffix])
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct TextInput {
273 pub id: Option<WidgetId>,
275 pub value: String,
277 pub label: Option<TextContent>,
279 pub placeholder: Option<TextContent>,
281 pub helper_text: Option<TextContent>,
283 pub error_text: Option<TextContent>,
285 pub counter_text: Option<TextContent>,
287 pub on_change: Option<ActionEnvelope>,
289 pub on_submit: Option<ActionEnvelope>,
292 pub on_editing_complete: Option<ActionEnvelope>,
294 pub on_tap_outside: Option<ActionEnvelope>,
296 pub width: Option<f32>,
298 pub height: Option<f32>,
300 #[serde(default)]
302 pub size: ComponentSize,
303 pub padding: Option<[f32; 4]>,
305 pub multiline: bool,
307 pub autofocus: bool,
309 pub enabled: bool,
311 pub read_only: bool,
313 pub min_lines: Option<usize>,
315 pub max_lines: Option<usize>,
317 pub obscure_text: bool,
319 pub obscuring_character: char,
321 pub mask: Option<fission_ir::semantics::InputMask>,
323 pub styled_runs: Option<Vec<fission_ir::op::TextRun>>,
329 pub borderless: bool,
332 pub capture_tab: bool,
334 pub auto_indent: bool,
337 pub on_cursor_change: Option<ActionEnvelope>,
339 pub highlight_ranges: Vec<(usize, usize, IrColor)>,
343 pub background_fill: Option<Fill>,
345 pub border_color: Option<IrColor>,
347 pub focus_border_color: Option<IrColor>,
349 pub border_width: Option<f32>,
351 pub focus_border_width: Option<f32>,
353 pub border_radius: Option<f32>,
355 pub font_size: Option<f32>,
357 pub text_color: Option<IrColor>,
359 pub placeholder_color: Option<IrColor>,
361 pub label_color: Option<IrColor>,
363 pub helper_color: Option<IrColor>,
365 pub error_color: Option<IrColor>,
367 pub counter_color: Option<IrColor>,
369 pub selection_color: Option<IrColor>,
371 pub selection_text_color: Option<IrColor>,
373 pub text_align: fission_ir::op::TextAlign,
375 pub text_align_vertical: TextAlignVertical,
377 pub expands: bool,
379 pub cursor_color: Option<IrColor>,
381 pub cursor_width: Option<f32>,
383 pub cursor_height: Option<f32>,
385 pub cursor_radius: Option<f32>,
387 pub font_family: Option<String>,
389 pub locale: Option<String>,
391 pub font_weight: Option<u16>,
393 pub font_style: TextFontStyle,
395 pub text_scale: Option<f32>,
397 pub line_height: Option<f32>,
399 pub letter_spacing: Option<f32>,
401 pub text_direction: fission_ir::op::TextDirection,
403 pub strut_line_height: Option<f32>,
405 pub text_height_behavior: fission_ir::op::TextHeightBehavior,
407 pub prefix: Option<Widget>,
409 pub suffix: Option<Widget>,
411 pub mouse_cursor: Option<SemanticsMouseCursor>,
413 pub keyboard_type: TextInputType,
415 pub text_input_action: TextInputAction,
417 pub text_capitalization: TextCapitalization,
419 pub max_length: Option<usize>,
421 pub max_length_enforcement: MaxLengthEnforcement,
423 pub input_formatters: Vec<InputFormatter>,
425 pub autocorrect: bool,
427 pub enable_suggestions: bool,
429 pub spell_check: bool,
431 pub smart_dashes: bool,
433 pub smart_quotes: bool,
435 pub autofill_hints: Vec<String>,
437 pub scroll_padding: Option<[f32; 4]>,
439 pub drag_start_behavior: DragStartBehavior,
441 pub context_menu: TextContextMenuConfig,
443 pub selection_controls: TextSelectionControls,
445 pub magnifier_configuration: TextMagnifierConfiguration,
447 pub undo_controller: Option<TextUndoController>,
449 pub spell_check_configuration: Option<SpellCheckConfiguration>,
451 pub restoration_id: Option<String>,
453}
454
455impl TextInput {
456 pub fn value(mut self, v: impl Into<String>) -> Self {
457 self.value = v.into();
458 self
459 }
460
461 pub fn label(mut self, label: impl Into<TextContent>) -> Self {
462 self.label = Some(label.into());
463 self
464 }
465
466 pub fn padding(mut self, padding: [f32; 4]) -> Self {
467 self.padding = Some(padding);
468 self
469 }
470
471 pub fn background_fill(mut self, fill: Fill) -> Self {
472 self.background_fill = Some(fill);
473 self
474 }
475
476 pub fn text_color(mut self, color: IrColor) -> Self {
477 self.text_color = Some(color);
478 self
479 }
480
481 pub fn placeholder_color(mut self, color: IrColor) -> Self {
482 self.placeholder_color = Some(color);
483 self
484 }
485
486 pub fn helper_text(mut self, helper_text: impl Into<TextContent>) -> Self {
487 self.helper_text = Some(helper_text.into());
488 self
489 }
490
491 pub fn error_text(mut self, error_text: impl Into<TextContent>) -> Self {
492 self.error_text = Some(error_text.into());
493 self
494 }
495
496 pub fn counter_text(mut self, counter_text: impl Into<TextContent>) -> Self {
497 self.counter_text = Some(counter_text.into());
498 self
499 }
500
501 pub fn label_color(mut self, color: IrColor) -> Self {
502 self.label_color = Some(color);
503 self
504 }
505
506 pub fn helper_color(mut self, color: IrColor) -> Self {
507 self.helper_color = Some(color);
508 self
509 }
510
511 pub fn error_color(mut self, color: IrColor) -> Self {
512 self.error_color = Some(color);
513 self
514 }
515
516 pub fn counter_color(mut self, color: IrColor) -> Self {
517 self.counter_color = Some(color);
518 self
519 }
520
521 pub fn selection_color(mut self, color: IrColor) -> Self {
522 self.selection_color = Some(color);
523 self
524 }
525
526 pub fn selection_text_color(mut self, color: IrColor) -> Self {
527 self.selection_text_color = Some(color);
528 self
529 }
530
531 pub fn text_align(mut self, text_align: fission_ir::op::TextAlign) -> Self {
532 self.text_align = text_align;
533 self
534 }
535
536 pub fn text_align_vertical(mut self, text_align_vertical: TextAlignVertical) -> Self {
537 self.text_align_vertical = text_align_vertical;
538 self
539 }
540
541 pub fn expands(mut self, expands: bool) -> Self {
542 self.expands = expands;
543 self
544 }
545
546 pub fn cursor_color(mut self, color: IrColor) -> Self {
547 self.cursor_color = Some(color);
548 self
549 }
550
551 pub fn cursor_width(mut self, width: f32) -> Self {
552 self.cursor_width = Some(width);
553 self
554 }
555
556 pub fn cursor_height(mut self, height: f32) -> Self {
557 self.cursor_height = Some(height);
558 self
559 }
560
561 pub fn cursor_radius(mut self, radius: f32) -> Self {
562 self.cursor_radius = Some(radius);
563 self
564 }
565
566 pub fn enabled(mut self, enabled: bool) -> Self {
567 self.enabled = enabled;
568 self
569 }
570
571 pub fn autofocus(mut self, autofocus: bool) -> Self {
572 self.autofocus = autofocus;
573 self
574 }
575
576 pub fn read_only(mut self, read_only: bool) -> Self {
577 self.read_only = read_only;
578 self
579 }
580
581 pub fn keyboard_type(mut self, keyboard_type: TextInputType) -> Self {
582 self.keyboard_type = keyboard_type;
583 self
584 }
585
586 pub fn text_input_action(mut self, action: TextInputAction) -> Self {
587 self.text_input_action = action;
588 self
589 }
590
591 pub fn text_capitalization(mut self, capitalization: TextCapitalization) -> Self {
592 self.text_capitalization = capitalization;
593 self
594 }
595
596 pub fn max_length(mut self, max_length: usize) -> Self {
597 self.max_length = Some(max_length);
598 self
599 }
600
601 pub fn max_length_enforcement(mut self, enforcement: MaxLengthEnforcement) -> Self {
602 self.max_length_enforcement = enforcement;
603 self
604 }
605
606 pub fn input_formatters(mut self, input_formatters: Vec<InputFormatter>) -> Self {
607 self.input_formatters = input_formatters;
608 self
609 }
610
611 pub fn autocorrect(mut self, autocorrect: bool) -> Self {
612 self.autocorrect = autocorrect;
613 self
614 }
615
616 pub fn enable_suggestions(mut self, enable_suggestions: bool) -> Self {
617 self.enable_suggestions = enable_suggestions;
618 self
619 }
620
621 pub fn spell_check(mut self, spell_check: bool) -> Self {
622 self.spell_check = spell_check;
623 self
624 }
625
626 pub fn smart_dashes(mut self, smart_dashes: bool) -> Self {
627 self.smart_dashes = smart_dashes;
628 self
629 }
630
631 pub fn smart_quotes(mut self, smart_quotes: bool) -> Self {
632 self.smart_quotes = smart_quotes;
633 self
634 }
635
636 pub fn autofill_hints(mut self, autofill_hints: Vec<String>) -> Self {
637 self.autofill_hints = autofill_hints;
638 self
639 }
640
641 pub fn context_menu(mut self, context_menu: TextContextMenuConfig) -> Self {
642 self.context_menu = context_menu;
643 self
644 }
645
646 pub fn drag_start_behavior(mut self, drag_start_behavior: DragStartBehavior) -> Self {
647 self.drag_start_behavior = drag_start_behavior;
648 self
649 }
650
651 pub fn selection_controls(mut self, selection_controls: TextSelectionControls) -> Self {
652 self.selection_controls = selection_controls;
653 self
654 }
655
656 pub fn magnifier_configuration(
657 mut self,
658 magnifier_configuration: TextMagnifierConfiguration,
659 ) -> Self {
660 self.magnifier_configuration = magnifier_configuration;
661 self
662 }
663
664 pub fn on_tap_outside(mut self, action: ActionEnvelope) -> Self {
665 self.on_tap_outside = Some(action);
666 self
667 }
668
669 pub fn undo_controller(mut self, undo_controller: TextUndoController) -> Self {
670 self.undo_controller = Some(undo_controller);
671 self
672 }
673
674 pub fn spell_check_configuration(
675 mut self,
676 spell_check_configuration: SpellCheckConfiguration,
677 ) -> Self {
678 self.spell_check_configuration = Some(spell_check_configuration);
679 self
680 }
681
682 pub fn restoration_id(mut self, restoration_id: impl Into<String>) -> Self {
683 self.restoration_id = Some(restoration_id.into());
684 self
685 }
686
687 pub fn family(mut self, family: impl Into<String>) -> Self {
688 self.font_family = Some(family.into());
689 self
690 }
691
692 pub fn locale(mut self, locale: impl Into<String>) -> Self {
693 self.locale = Some(locale.into());
694 self
695 }
696
697 pub fn weight(mut self, weight: u16) -> Self {
698 self.font_weight = Some(weight);
699 self
700 }
701
702 pub fn italic(mut self, italic: bool) -> Self {
703 self.font_style = if italic {
704 TextFontStyle::Italic
705 } else {
706 TextFontStyle::Normal
707 };
708 self
709 }
710
711 pub fn font_size(mut self, size: f32) -> Self {
712 self.font_size = Some(size);
713 self
714 }
715
716 pub fn text_scale(mut self, text_scale: f32) -> Self {
717 self.text_scale = Some(text_scale);
718 self
719 }
720
721 pub fn line_height(mut self, line_height: f32) -> Self {
722 self.line_height = Some(line_height);
723 self
724 }
725
726 pub fn letter_spacing(mut self, letter_spacing: f32) -> Self {
727 self.letter_spacing = Some(letter_spacing);
728 self
729 }
730
731 pub fn text_direction(mut self, text_direction: fission_ir::op::TextDirection) -> Self {
732 self.text_direction = text_direction;
733 self
734 }
735
736 pub fn strut_line_height(mut self, strut_line_height: f32) -> Self {
737 self.strut_line_height = Some(strut_line_height);
738 self
739 }
740
741 pub fn text_height_behavior(
742 mut self,
743 text_height_behavior: fission_ir::op::TextHeightBehavior,
744 ) -> Self {
745 self.text_height_behavior = text_height_behavior;
746 self
747 }
748
749 pub fn prefix(mut self, node: impl Into<Widget>) -> Self {
750 self.prefix = Some(node.into());
751 self
752 }
753
754 pub fn suffix(mut self, node: impl Into<Widget>) -> Self {
755 self.suffix = Some(node.into());
756 self
757 }
758
759 pub fn mouse_cursor(mut self, mouse_cursor: SemanticsMouseCursor) -> Self {
760 self.mouse_cursor = Some(mouse_cursor);
761 self
762 }
763
764 pub fn scroll_padding(mut self, scroll_padding: [f32; 4]) -> Self {
765 self.scroll_padding = Some(scroll_padding);
766 self
767 }
768}
769
770impl Default for TextInput {
771 fn default() -> Self {
772 Self {
773 id: None,
774 value: String::new(),
775 label: None,
776 placeholder: None,
777 helper_text: None,
778 error_text: None,
779 counter_text: None,
780 on_change: None,
781 on_submit: None,
782 on_editing_complete: None,
783 on_tap_outside: None,
784 width: None,
785 height: None,
786 size: ComponentSize::Md,
787 padding: None,
788 multiline: false,
789 autofocus: false,
790 enabled: true,
791 read_only: false,
792 min_lines: None,
793 max_lines: None,
794 obscure_text: false,
795 obscuring_character: '•',
796 mask: None,
797 styled_runs: None,
798 borderless: false,
799 capture_tab: false,
800 auto_indent: false,
801 on_cursor_change: None,
802 highlight_ranges: Vec::new(),
803 background_fill: None,
804 border_color: None,
805 focus_border_color: None,
806 border_width: None,
807 focus_border_width: None,
808 border_radius: None,
809 font_size: None,
810 text_color: None,
811 placeholder_color: None,
812 label_color: None,
813 helper_color: None,
814 error_color: None,
815 counter_color: None,
816 selection_color: None,
817 selection_text_color: None,
818 text_align: fission_ir::op::TextAlign::Start,
819 text_align_vertical: TextAlignVertical::Center,
820 expands: false,
821 cursor_color: None,
822 cursor_width: None,
823 cursor_height: None,
824 cursor_radius: None,
825 font_family: None,
826 locale: None,
827 font_weight: None,
828 font_style: TextFontStyle::Normal,
829 text_scale: None,
830 line_height: None,
831 letter_spacing: None,
832 text_direction: fission_ir::op::TextDirection::Auto,
833 strut_line_height: None,
834 text_height_behavior: fission_ir::op::TextHeightBehavior::default(),
835 prefix: None,
836 suffix: None,
837 mouse_cursor: None,
838 keyboard_type: TextInputType::Text,
839 text_input_action: TextInputAction::Done,
840 text_capitalization: TextCapitalization::None,
841 max_length: None,
842 max_length_enforcement: MaxLengthEnforcement::Enforced,
843 input_formatters: Vec::new(),
844 autocorrect: true,
845 enable_suggestions: true,
846 spell_check: true,
847 smart_dashes: true,
848 smart_quotes: true,
849 autofill_hints: Vec::new(),
850 scroll_padding: None,
851 drag_start_behavior: DragStartBehavior::Start,
852 context_menu: TextContextMenuConfig::default(),
853 selection_controls: TextSelectionControls::default(),
854 magnifier_configuration: TextMagnifierConfiguration::default(),
855 undo_controller: None,
856 spell_check_configuration: None,
857 restoration_id: None,
858 }
859 }
860}
861
862impl TextInput {
863 fn resolve_text_content(content: &TextContent, cx: &InternalLoweringCx<'_>) -> String {
864 match content {
865 TextContent::Literal(s) => s.clone(),
866 TextContent::Key(key) => cx
867 .env
868 .i18n
869 .get(&cx.env.locale, key)
870 .map(|s| s.to_string())
871 .unwrap_or_else(|| format!("MISSING:{}", key)),
872 }
873 }
874
875 fn mask_text(text: &str, obscuring_character: char) -> String {
876 let mut masked = String::new();
877 for _ in text.graphemes(true) {
878 masked.push(obscuring_character);
879 }
880 masked
881 }
882
883 fn masked_byte_offset(source: &str, masked: &str, source_byte_offset: usize) -> usize {
884 let clamped = source_byte_offset.min(source.len());
885 let grapheme_count = source[..clamped].graphemes(true).count();
886 masked
887 .grapheme_indices(true)
888 .nth(grapheme_count)
889 .map(|(idx, _)| idx)
890 .unwrap_or(masked.len())
891 }
892
893 fn supporting_counter_text(
894 &self,
895 cx: &InternalLoweringCx<'_>,
896 current_text: &str,
897 ) -> Option<String> {
898 self.counter_text
899 .as_ref()
900 .map(|content| Self::resolve_text_content(content, cx))
901 .or_else(|| {
902 self.max_length
903 .map(|max_length| format!("{}/{}", current_text.chars().count(), max_length))
904 })
905 }
906
907 fn build_selection_handle_overlay(
908 &self,
909 cx: &mut InternalLoweringCx,
910 input_id: WidgetId,
911 kind: TextSelectionHandleKind,
912 point: fission_layout::LayoutPoint,
913 ) -> WidgetId {
914 let controls = &self.selection_controls;
915 let diameter = controls.handle_radius * 2.0;
916 let handle_node = Button {
917 id: Some(text_input_selection_handle_id(input_id, kind).into()),
918 child: Some(
919 Container::new(Spacer {
920 width: Some(diameter),
921 height: Some(diameter),
922 ..Default::default()
923 })
924 .bg_fill(Fill::Solid(controls.handle_fill))
925 .border(
926 controls.handle_stroke.unwrap_or(IrColor {
927 r: 0,
928 g: 0,
929 b: 0,
930 a: 0,
931 }),
932 controls.handle_stroke_width,
933 )
934 .border_radius(controls.handle_radius)
935 .into(),
936 ),
937 width: Some(diameter),
938 height: Some(diameter),
939 padding: Some([0.0; 4]),
940 content_align: ButtonContentAlign::Center,
941 variant: ButtonVariant::Ghost,
942 ..Default::default()
943 }
944 .into();
945
946 Positioned {
947 left: Some((point.x - controls.handle_radius).max(0.0)),
948 top: Some((point.y - controls.handle_radius).max(0.0)),
949 width: Some(diameter),
950 height: Some(diameter),
951 child: Some(handle_node),
952 ..Default::default()
953 }
954 .lower(cx)
955 }
956
957 fn build_toolbar_overlay(
958 &self,
959 cx: &mut InternalLoweringCx,
960 input_id: WidgetId,
961 anchor: fission_layout::LayoutPoint,
962 ) -> WidgetId {
963 let tokens = &cx.env.theme.tokens;
964 let mut row = Row::default().gap(self.context_menu.gap);
965 for action in &self.context_menu.actions {
966 row.children.push(
967 Button {
968 id: Some(text_input_toolbar_button_id(input_id, *action).into()),
969 child: Some(
970 Text::new(action.label())
971 .size(tokens.typography.label_large_size)
972 .color(tokens.colors.text_primary)
973 .into(),
974 ),
975 padding: Some([10.0, 10.0, 6.0, 6.0]),
976 content_align: ButtonContentAlign::Center,
977 variant: ButtonVariant::Ghost,
978 ..Default::default()
979 }
980 .into(),
981 );
982 }
983
984 let toolbar: Widget = Container::new(row)
985 .bg_fill(Fill::Solid(tokens.colors.surface))
986 .border(tokens.colors.border, 1.0)
987 .border_radius(self.context_menu.border_radius)
988 .padding(self.context_menu.padding)
989 .into();
990
991 Positioned {
992 left: Some(anchor.x.max(0.0)),
993 top: Some((anchor.y - 44.0).max(0.0)),
994 child: Some(toolbar),
995 ..Default::default()
996 }
997 .lower(cx)
998 }
999
1000 fn magnifier_snippet(display_text: &str, caret: usize) -> String {
1001 let mut graphemes = Vec::new();
1002 for (idx, grapheme) in display_text.grapheme_indices(true) {
1003 graphemes.push((idx, grapheme));
1004 }
1005 if graphemes.is_empty() {
1006 return String::new();
1007 }
1008
1009 let caret_grapheme = graphemes
1010 .iter()
1011 .position(|(idx, _)| *idx >= caret.min(display_text.len()))
1012 .unwrap_or(graphemes.len().saturating_sub(1));
1013 let start = caret_grapheme.saturating_sub(4);
1014 let end = (caret_grapheme + 5).min(graphemes.len());
1015 graphemes[start..end]
1016 .iter()
1017 .map(|(_, grapheme)| *grapheme)
1018 .collect::<String>()
1019 }
1020
1021 fn build_magnifier_overlay(
1022 &self,
1023 cx: &mut InternalLoweringCx,
1024 anchor: fission_layout::LayoutPoint,
1025 display_text: &str,
1026 caret: usize,
1027 base_text_style: &fission_ir::op::TextStyle,
1028 ) -> WidgetId {
1029 let cfg = &self.magnifier_configuration;
1030 let tokens = &cx.env.theme.tokens;
1031 let preview = Self::magnifier_snippet(display_text, caret);
1032 let preview_text = Text::new(preview)
1033 .size(base_text_style.font_size * cfg.scale)
1034 .color(base_text_style.color)
1035 .family(
1036 base_text_style
1037 .font_family
1038 .clone()
1039 .unwrap_or_else(|| "system-ui".to_string()),
1040 )
1041 .weight(base_text_style.font_weight)
1042 .italic(base_text_style.font_style == fission_ir::op::FontStyle::Italic)
1043 .line_height(
1044 base_text_style
1045 .line_height
1046 .unwrap_or(base_text_style.font_size * 1.25)
1047 * cfg.scale,
1048 )
1049 .letter_spacing(base_text_style.letter_spacing * cfg.scale);
1050
1051 let magnifier: Widget = Container::new(preview_text)
1052 .width(cfg.diameter)
1053 .height(cfg.diameter)
1054 .bg_fill(Fill::Solid(tokens.colors.surface))
1055 .border(
1056 cfg.border_color.unwrap_or(tokens.colors.border),
1057 cfg.border_width,
1058 )
1059 .border_radius(cfg.border_radius)
1060 .padding_all(8.0)
1061 .into();
1062
1063 Positioned {
1064 left: Some((anchor.x - cfg.diameter * 0.5).max(0.0)),
1065 top: Some((anchor.y - cfg.diameter - 18.0).max(0.0)),
1066 width: Some(cfg.diameter),
1067 height: Some(cfg.diameter),
1068 child: Some(magnifier),
1069 ..Default::default()
1070 }
1071 .lower(cx)
1072 }
1073}
1074
1075impl InternalLower for TextInput {
1076 fn lower(&self, cx: &mut InternalLoweringCx) -> WidgetId {
1077 let input_id = self.id.map(Into::into).unwrap_or_else(|| cx.next_node_id());
1078 let is_focused = cx.runtime_state.interaction.is_focused(input_id);
1079
1080 let theme = &cx.env.theme.components.text_input;
1081 let tokens = &cx.env.theme.tokens;
1082 let component_state = if !self.enabled {
1083 ComponentState::Disabled
1084 } else if self.error_text.is_some() {
1085 ComponentState::Error
1086 } else if is_focused {
1087 ComponentState::Focus
1088 } else {
1089 ComponentState::Default
1090 };
1091 let component_style = theme.resolve(self.size, component_state);
1092
1093 let text_scale = self.text_scale.unwrap_or(1.0).max(0.0);
1094 let font_size = self
1095 .font_size
1096 .unwrap_or(component_style.font_size.unwrap_or(theme.font_size))
1097 * text_scale;
1098 let text_color = self
1099 .text_color
1100 .unwrap_or(component_style.text_color.unwrap_or(theme.text_color));
1101 let selection_color = self
1102 .selection_color
1103 .unwrap_or(tokens.colors.primary.with_alpha(52));
1104 let selection_text_color = self.selection_text_color.unwrap_or(text_color);
1105 let placeholder_color = self.placeholder_color.unwrap_or(
1106 theme
1107 .placeholder_style
1108 .text_color
1109 .unwrap_or(theme.placeholder_color),
1110 );
1111 let cursor_color = self.cursor_color.unwrap_or(theme.focus_color);
1112 let cursor_width = self.cursor_width.unwrap_or(2.0);
1113 let font_weight = self
1114 .font_weight
1115 .unwrap_or(component_style.font_weight.unwrap_or(theme.font_weight));
1116 let line_height = self
1117 .line_height
1118 .or(component_style.line_height)
1119 .map(|value| value * text_scale);
1120 let letter_spacing = self.letter_spacing.unwrap_or(0.0) * text_scale;
1121 let style_border = component_style.border.clone();
1122 let border_color = if is_focused {
1123 self.focus_border_color.unwrap_or_else(|| {
1124 style_border
1125 .as_ref()
1126 .and_then(|border| match &border.fill {
1127 Fill::Solid(color) => Some(*color),
1128 _ => None,
1129 })
1130 .unwrap_or(theme.focus_color)
1131 })
1132 } else {
1133 self.border_color.unwrap_or_else(|| {
1134 style_border
1135 .as_ref()
1136 .and_then(|border| match &border.fill {
1137 Fill::Solid(color) => Some(*color),
1138 _ => None,
1139 })
1140 .unwrap_or(theme.border_color)
1141 })
1142 };
1143 let border_width = if is_focused {
1144 self.focus_border_width.unwrap_or(
1145 style_border
1146 .as_ref()
1147 .map(|border| border.width)
1148 .unwrap_or(2.0),
1149 )
1150 } else {
1151 self.border_width.unwrap_or(
1152 style_border
1153 .as_ref()
1154 .map(|border| border.width)
1155 .unwrap_or(theme.border_width),
1156 )
1157 };
1158 let border_radius = self
1159 .border_radius
1160 .unwrap_or(component_style.radius.unwrap_or(theme.radius));
1161 let content_padding = self.padding.unwrap_or(component_style.padding_box(
1162 component_style.padding_x.unwrap_or(theme.padding_h),
1163 component_style.padding_y.unwrap_or(4.0),
1164 ));
1165 let base_text_style = fission_ir::op::TextStyle {
1166 font_size,
1167 color: text_color,
1168 underline: false,
1169 font_family: self.font_family.clone(),
1170 locale: self.locale.clone(),
1171 font_weight,
1172 font_style: self.font_style.into(),
1173 line_height,
1174 letter_spacing,
1175 background_color: None,
1176 };
1177
1178 let resolved_label = self
1179 .label
1180 .as_ref()
1181 .map(|label| Self::resolve_text_content(label, cx));
1182 let resolved_placeholder = self
1183 .placeholder
1184 .as_ref()
1185 .map(|placeholder| Self::resolve_text_content(placeholder, cx));
1186
1187 let background_id = if self.borderless {
1189 None
1190 } else {
1191 Some(
1192 InternalIrBuilder::new(
1193 cx.next_node_id(),
1194 Op::Paint(PaintOp::DrawRect {
1195 fill: Some(
1196 self.background_fill
1197 .clone()
1198 .or_else(|| component_style.background.clone())
1199 .unwrap_or(Fill::Solid(tokens.colors.background)),
1200 ),
1201 stroke: Some(Stroke {
1202 fill: Fill::Solid(border_color),
1203 width: border_width,
1204 dash_array: None,
1205 line_cap: fission_ir::op::LineCap::Butt,
1206 line_join: fission_ir::op::LineJoin::Miter,
1207 }),
1208 corner_radius: border_radius,
1209 shadow: component_style.outer_shadows().first().copied(),
1210 }),
1211 )
1212 .build(cx),
1213 )
1214 };
1215
1216 let session = cx.runtime_state.text_edit.get(input_id);
1218 let session_display = if is_focused {
1219 session.map(|st| st.display_text())
1220 } else {
1221 None
1222 };
1223
1224 let (display_text, preedit_range, caret, anchor) = if self.obscure_text {
1225 let mut combined = self.value.clone();
1226 if let Some((display, _)) = &session_display {
1227 combined = display.clone();
1228 }
1229 let (caret, anchor) = session.map(|st| (st.caret, st.anchor)).unwrap_or((0, 0));
1230 let masked = Self::mask_text(&combined, self.obscuring_character);
1231 let mapped_caret = Self::masked_byte_offset(&combined, &masked, caret);
1232 let mapped_anchor = Self::masked_byte_offset(&combined, &masked, anchor);
1233 (masked, None, mapped_caret, mapped_anchor)
1234 } else {
1235 match session_display {
1236 Some((combined, preedit_range)) => {
1237 let (caret, anchor) = session.map(|st| (st.caret, st.anchor)).unwrap_or((0, 0));
1238 (combined, preedit_range, caret, anchor)
1239 }
1240 None => {
1241 let (caret, anchor) = session.map(|st| (st.caret, st.anchor)).unwrap_or((0, 0));
1242 (self.value.clone(), None, caret, anchor)
1243 }
1244 }
1245 };
1246
1247 let mut runs = Vec::new();
1249 if is_focused && caret != anchor {
1250 let (s, e) = if caret < anchor {
1251 (caret, anchor)
1252 } else {
1253 (anchor, caret)
1254 };
1255 let s = s.min(display_text.len());
1256 let e = e.min(display_text.len());
1257
1258 if s > 0 {
1259 runs.push(fission_ir::op::TextRun {
1260 text: display_text[..s].to_string(),
1261 style: base_text_style.clone(),
1262 });
1263 }
1264 if s < e {
1265 runs.push(fission_ir::op::TextRun {
1266 text: display_text[s..e].to_string(),
1267 style: fission_ir::op::TextStyle {
1268 color: selection_text_color,
1269 background_color: Some(selection_color),
1270 ..base_text_style.clone()
1271 },
1272 });
1273 }
1274 if e < display_text.len() {
1275 runs.push(fission_ir::op::TextRun {
1276 text: display_text[e..].to_string(),
1277 style: base_text_style.clone(),
1278 });
1279 }
1280 } else if let Some(styled) = &self.styled_runs {
1281 runs = styled
1284 .iter()
1285 .cloned()
1286 .map(|mut run| {
1287 if run.style.font_family.is_none() {
1288 run.style.font_family = base_text_style.font_family.clone();
1289 }
1290 if run.style.font_weight == 400 {
1291 run.style.font_weight = base_text_style.font_weight;
1292 }
1293 if run.style.font_style == fission_ir::op::FontStyle::Normal {
1294 run.style.font_style = base_text_style.font_style;
1295 }
1296 if run.style.line_height.is_none() {
1297 run.style.line_height = base_text_style.line_height;
1298 }
1299 if run.style.letter_spacing == 0.0 {
1300 run.style.letter_spacing = base_text_style.letter_spacing;
1301 }
1302 run
1303 })
1304 .collect();
1305 } else {
1306 runs.push(fission_ir::op::TextRun {
1307 text: display_text.clone(),
1308 style: base_text_style.clone(),
1309 });
1310 }
1311
1312 if !self.highlight_ranges.is_empty() && !runs.is_empty() {
1314 let mut final_runs = Vec::new();
1315 let mut run_start_byte: usize = 0;
1316
1317 for run in runs {
1318 let run_end_byte = run_start_byte + run.text.len();
1319 let mut cuts = Vec::new();
1320
1321 for &(hs, he, color) in &self.highlight_ranges {
1322 let overlap_start = hs.max(run_start_byte);
1323 let overlap_end = he.min(run_end_byte);
1324 if overlap_start < overlap_end {
1325 cuts.push((
1326 overlap_start - run_start_byte,
1327 overlap_end - run_start_byte,
1328 color,
1329 ));
1330 }
1331 }
1332
1333 if cuts.is_empty() {
1334 final_runs.push(run);
1335 } else {
1336 cuts.sort_by_key(|c| c.0);
1337 let mut pos = 0usize;
1338 for (cs, ce, bg_color) in cuts {
1339 if cs > pos {
1340 final_runs.push(fission_ir::op::TextRun {
1341 text: run.text[pos..cs].to_string(),
1342 style: run.style.clone(),
1343 });
1344 }
1345 let mut hl_style = run.style.clone();
1346 hl_style.background_color = Some(bg_color);
1347 final_runs.push(fission_ir::op::TextRun {
1348 text: run.text[cs..ce].to_string(),
1349 style: hl_style,
1350 });
1351 pos = ce;
1352 }
1353 if pos < run.text.len() {
1354 final_runs.push(fission_ir::op::TextRun {
1355 text: run.text[pos..].to_string(),
1356 style: run.style.clone(),
1357 });
1358 }
1359 }
1360 run_start_byte = run_end_byte;
1361 }
1362 runs = final_runs;
1363 }
1364
1365 if display_text.is_empty() && resolved_placeholder.is_some() {
1366 runs = vec![fission_ir::op::TextRun {
1367 text: resolved_placeholder.clone().unwrap(),
1368 style: fission_ir::op::TextStyle {
1369 color: placeholder_color,
1370 ..base_text_style.clone()
1371 },
1372 }];
1373 }
1374
1375 let caret_idx = if is_focused {
1376 let show = cx
1377 .runtime_state
1378 .caret_visible
1379 .get(&input_id)
1380 .copied()
1381 .unwrap_or(true);
1382 if show {
1383 Some(
1384 preedit_range
1385 .map(|(_, end)| end)
1386 .unwrap_or(caret)
1387 .min(display_text.len()),
1388 )
1389 } else {
1390 None
1391 }
1392 } else {
1393 None
1394 };
1395
1396 let paragraph_overflow = if self.multiline {
1397 fission_ir::op::TextOverflow::Clip
1398 } else {
1399 fission_ir::op::TextOverflow::Visible
1400 };
1401 let paragraph_style = Some(TextParagraphStyle {
1402 text_align: self.text_align,
1403 max_lines: None,
1404 overflow: paragraph_overflow,
1405 text_direction: self.text_direction,
1406 text_width_basis: fission_ir::op::TextWidthBasis::Parent,
1407 strut_line_height: self.strut_line_height,
1408 text_height_behavior: self.text_height_behavior,
1409 })
1410 .filter(|style| {
1411 *style
1412 != TextParagraphStyle {
1413 text_align: IrTextAlign::Start,
1414 max_lines: None,
1415 overflow: paragraph_overflow,
1416 text_direction: self.text_direction,
1417 text_width_basis: fission_ir::op::TextWidthBasis::Parent,
1418 strut_line_height: self.strut_line_height,
1419 text_height_behavior: self.text_height_behavior,
1420 }
1421 });
1422
1423 let text_id = InternalIrBuilder::new(
1424 cx.next_node_id(),
1425 Op::Paint(PaintOp::DrawRichText {
1426 runs,
1427 wrap: self.multiline,
1428 caret_index: caret_idx,
1429 caret_color: Some(cursor_color),
1430 caret_width: Some(cursor_width),
1431 caret_height: self.cursor_height,
1432 caret_radius: self.cursor_radius,
1433 paragraph_style,
1434 }),
1435 )
1436 .build(cx);
1437
1438 let mut text_box = InternalIrBuilder::new(
1439 cx.next_node_id(),
1440 Op::Layout(LayoutOp::Box {
1441 width: None,
1442 height: None,
1443 min_width: None,
1444 max_width: None,
1445 min_height: None,
1446 max_height: None,
1447 padding: [0.0; 4],
1448 flex_grow: 0.0,
1449 flex_shrink: 0.0,
1450 aspect_ratio: None,
1451 }),
1452 );
1453 text_box.add_child(text_id);
1454 let text_layout_id = text_box.build(cx);
1455
1456 let mut scroll = InternalIrBuilder::new(
1458 cx.next_node_id(),
1459 Op::Layout(LayoutOp::Scroll {
1460 direction: if self.multiline {
1461 FlexDirection::Column
1462 } else {
1463 FlexDirection::Row
1464 },
1465 show_scrollbar: false,
1466 width: None, height: None,
1468 min_width: None,
1469 max_width: None,
1470 min_height: None,
1471 max_height: None,
1472 padding: [0.0; 4],
1473 flex_grow: 1.0,
1474 flex_shrink: 1.0,
1475 }),
1476 );
1477 scroll.add_child(text_layout_id);
1478 let scroll_id = scroll.build(cx);
1479
1480 let mut content_row = InternalIrBuilder::new(
1482 cx.next_node_id(),
1483 Op::Layout(LayoutOp::Flex {
1484 direction: FlexDirection::Row,
1485 wrap: FlexWrap::NoWrap,
1486 flex_grow: if self.expands { 1.0 } else { 0.0 },
1487 flex_shrink: 1.0,
1488 padding: [0.0; 4],
1489 gap: if self.prefix.is_some() || self.suffix.is_some() {
1490 Some(theme.padding_h * 0.75)
1491 } else {
1492 None
1493 },
1494 align_items: self.text_align_vertical.align_items(),
1495 justify_content: fission_ir::op::JustifyContent::Start,
1496 }),
1497 );
1498 if let Some(prefix) = &self.prefix {
1499 content_row.add_child(prefix.lower(cx));
1500 }
1501 content_row.add_child(scroll_id);
1502 if let Some(suffix) = &self.suffix {
1503 content_row.add_child(suffix.lower(cx));
1504 }
1505 let content_row_id = content_row.build(cx);
1506
1507 let mut content_alignment = InternalIrBuilder::new(
1508 cx.next_node_id(),
1509 Op::Layout(LayoutOp::Flex {
1510 direction: FlexDirection::Column,
1511 wrap: FlexWrap::NoWrap,
1512 flex_grow: 1.0,
1513 flex_shrink: 1.0,
1514 padding: [0.0; 4],
1515 gap: None,
1516 align_items: fission_ir::op::AlignItems::Stretch,
1517 justify_content: self.text_align_vertical.justify_content(),
1518 }),
1519 );
1520 content_alignment.add_child(content_row_id);
1521 let content_id = content_alignment.build(cx);
1522
1523 let effective_line_height = line_height.unwrap_or((font_size * 1.35).max(font_size + 4.0));
1524 let min_height = if self.height.is_some() || self.expands {
1525 None
1526 } else if self.multiline {
1527 Some(
1528 content_padding[2]
1529 + content_padding[3]
1530 + effective_line_height * self.min_lines.unwrap_or(1) as f32,
1531 )
1532 } else {
1533 Some(
1534 theme
1535 .height
1536 .max(content_padding[2] + content_padding[3] + effective_line_height),
1537 )
1538 };
1539 let max_height = if self.height.is_some() || !self.multiline || self.expands {
1540 None
1541 } else {
1542 self.max_lines.map(|lines| {
1543 content_padding[2] + content_padding[3] + effective_line_height * lines as f32
1544 })
1545 };
1546
1547 let wrapper_id = cx.next_node_id();
1549 let mut wrapper = InternalIrBuilder::new(
1550 wrapper_id,
1551 Op::Layout(LayoutOp::Box {
1552 width: self.width,
1553 height: self.height.or(if self.multiline || self.expands {
1554 None
1555 } else {
1556 Some(theme.height)
1557 }),
1558 min_width: None,
1559 max_width: None,
1560 min_height,
1561 max_height,
1562 padding: content_padding,
1563 flex_grow: if self.width.is_none() || self.expands {
1564 1.0
1565 } else {
1566 0.0
1567 },
1568 flex_shrink: 1.0,
1569 aspect_ratio: None,
1570 }),
1571 );
1572 if let Some(bg_id) = background_id {
1573 wrapper.add_child(bg_id); }
1575 wrapper.add_child(content_id); let wrapper_visual_id = wrapper.build(cx);
1578 let mut final_visual_id = wrapper_visual_id;
1579
1580 if is_focused && self.enabled {
1581 if let Some(session_state) = session {
1582 let affordances = &session_state.affordances;
1583 let mut overlay_children = Vec::new();
1584
1585 if caret == anchor {
1586 if self.selection_controls.show_collapsed_handle {
1587 if let Some(point) = affordances.caret_handle {
1588 overlay_children.push(self.build_selection_handle_overlay(
1589 cx,
1590 input_id,
1591 TextSelectionHandleKind::Caret,
1592 point,
1593 ));
1594 }
1595 }
1596 } else {
1597 if let Some(point) = affordances.selection_start_handle {
1598 overlay_children.push(self.build_selection_handle_overlay(
1599 cx,
1600 input_id,
1601 TextSelectionHandleKind::Start,
1602 point,
1603 ));
1604 }
1605 if let Some(point) = affordances.selection_end_handle {
1606 overlay_children.push(self.build_selection_handle_overlay(
1607 cx,
1608 input_id,
1609 TextSelectionHandleKind::End,
1610 point,
1611 ));
1612 }
1613 }
1614
1615 if self.context_menu.enabled && affordances.toolbar_visible {
1616 if let Some(anchor_point) = affordances.toolbar_anchor {
1617 overlay_children.push(self.build_toolbar_overlay(
1618 cx,
1619 input_id,
1620 anchor_point,
1621 ));
1622 }
1623 }
1624
1625 if self.magnifier_configuration.enabled && affordances.magnifier_visible {
1626 if let Some(anchor_point) = affordances.magnifier_anchor {
1627 overlay_children.push(self.build_magnifier_overlay(
1628 cx,
1629 anchor_point,
1630 &display_text,
1631 caret.max(anchor),
1632 &base_text_style,
1633 ));
1634 }
1635 }
1636
1637 if !overlay_children.is_empty() {
1638 let mut stack =
1639 InternalIrBuilder::new(cx.next_node_id(), Op::Layout(LayoutOp::ZStack));
1640 stack.add_child(wrapper_visual_id);
1641 for child in overlay_children {
1642 stack.add_child(child);
1643 }
1644 final_visual_id = stack.build(cx);
1645 }
1646 }
1647 }
1648
1649 let supporting_text = self
1650 .error_text
1651 .as_ref()
1652 .map(|text| Self::resolve_text_content(text, cx))
1653 .or_else(|| {
1654 self.helper_text
1655 .as_ref()
1656 .map(|text| Self::resolve_text_content(text, cx))
1657 });
1658 let counter_text = self.supporting_counter_text(cx, &self.value);
1659
1660 let field_body_id =
1661 if resolved_label.is_some() || supporting_text.is_some() || counter_text.is_some() {
1662 let label_color = self.label_color.unwrap_or(if is_focused {
1663 theme.focus_color
1664 } else {
1665 theme
1666 .label_style
1667 .text_color
1668 .unwrap_or(tokens.colors.text_secondary)
1669 });
1670 let supporting_color = if self.error_text.is_some() {
1671 self.error_color.unwrap_or(tokens.colors.error)
1672 } else {
1673 self.helper_color.unwrap_or(
1674 theme
1675 .helper_style
1676 .text_color
1677 .unwrap_or(tokens.colors.text_secondary),
1678 )
1679 };
1680 let counter_color = self.counter_color.unwrap_or(
1681 theme
1682 .helper_style
1683 .text_color
1684 .unwrap_or(tokens.colors.text_secondary),
1685 );
1686 let mut column = InternalIrBuilder::new(
1687 cx.next_node_id(),
1688 Op::Layout(LayoutOp::Flex {
1689 direction: FlexDirection::Column,
1690 wrap: FlexWrap::NoWrap,
1691 flex_grow: 0.0,
1692 flex_shrink: 1.0,
1693 padding: [0.0; 4],
1694 gap: Some(6.0),
1695 align_items: fission_ir::op::AlignItems::Stretch,
1696 justify_content: fission_ir::op::JustifyContent::Start,
1697 }),
1698 );
1699
1700 if let Some(label) = &resolved_label {
1701 column.add_child(
1702 Text::new(label.clone())
1703 .size(
1704 theme
1705 .label_style
1706 .font_size
1707 .unwrap_or(tokens.typography.label_large_size),
1708 )
1709 .weight(
1710 theme
1711 .label_style
1712 .font_weight
1713 .unwrap_or(tokens.typography.font_weight_medium),
1714 )
1715 .color(label_color)
1716 .lower(cx),
1717 );
1718 }
1719
1720 column.add_child(final_visual_id);
1721
1722 if supporting_text.is_some() || counter_text.is_some() {
1723 let mut row = Row::default().gap(8.0);
1724 if let Some(supporting_text) = supporting_text {
1725 row.children.push(
1726 Text::new(supporting_text)
1727 .size(
1728 theme
1729 .helper_style
1730 .font_size
1731 .unwrap_or(tokens.typography.label_large_size),
1732 )
1733 .color(supporting_color)
1734 .into(),
1735 );
1736 }
1737 row.children.push(
1738 Spacer {
1739 flex_grow: 1.0,
1740 ..Default::default()
1741 }
1742 .into(),
1743 );
1744 if let Some(counter_text) = counter_text {
1745 row.children.push(
1746 Text::new(counter_text)
1747 .size(
1748 theme
1749 .helper_style
1750 .font_size
1751 .unwrap_or(tokens.typography.label_large_size),
1752 )
1753 .color(counter_color)
1754 .into(),
1755 );
1756 }
1757 column.add_child(row.lower(cx));
1758 }
1759
1760 column.build(cx)
1761 } else {
1762 final_visual_id
1763 };
1764
1765 let spell_check_enabled = self
1767 .spell_check_configuration
1768 .as_ref()
1769 .map_or(self.spell_check, |cfg| cfg.enabled);
1770 let suggestions_enabled = self
1771 .spell_check_configuration
1772 .as_ref()
1773 .map_or(self.enable_suggestions, |cfg| {
1774 self.enable_suggestions && cfg.show_suggestions
1775 });
1776
1777 let mut semantics = Semantics {
1778 role: Role::TextInput,
1779 label: resolved_label.clone().or(resolved_placeholder.clone()),
1780 identifier: None,
1781 value: Some(self.value.clone()),
1782 actions: Default::default(),
1783 action_scope_id: None,
1784 focusable: self.enabled,
1785 multiline: self.multiline,
1786 masked: self.obscure_text,
1787 input_mask: self.mask.clone(),
1788 ime_preedit_range: preedit_range,
1789 checked: None,
1790 disabled: !self.enabled,
1791 read_only: self.read_only,
1792 autofocus: self.autofocus,
1793 draggable: false,
1794 scrollable_x: false,
1795 scrollable_y: false,
1796 min_value: None,
1797 max_value: None,
1798 current_value: None,
1799 is_focus_scope: false,
1800 is_focus_barrier: false,
1801 drag_payload: None,
1802 hero_tag: None,
1803 focus_index: None,
1804 text_input_type: if self.multiline {
1805 TextInputType::Multiline
1806 } else {
1807 self.keyboard_type
1808 },
1809 text_input_action: self.text_input_action,
1810 text_capitalization: self.text_capitalization,
1811 max_length: self.max_length,
1812 max_length_enforcement: self.max_length_enforcement,
1813 input_formatters: self.input_formatters.clone(),
1814 autocorrect: self.autocorrect,
1815 enable_suggestions: suggestions_enabled,
1816 spell_check: spell_check_enabled,
1817 smart_dashes: self.smart_dashes,
1818 smart_quotes: self.smart_quotes,
1819 autofill_hints: self.autofill_hints.clone(),
1820 scroll_padding: self.scroll_padding,
1821 capture_tab: self.capture_tab,
1822 auto_indent: self.auto_indent,
1823 };
1824 if let Some(env) = &self.on_change {
1825 semantics.actions.entries.push(fission_ir::ActionEntry {
1826 trigger: fission_ir::semantics::ActionTrigger::Change,
1827 action_id: env.id.as_u128(),
1828 payload_data: None,
1829 });
1830 }
1831 if let Some(env) = &self.on_cursor_change {
1832 semantics.actions.entries.push(fission_ir::ActionEntry {
1833 trigger: fission_ir::semantics::ActionTrigger::CursorChange,
1834 action_id: env.id.as_u128(),
1835 payload_data: None,
1836 });
1837 }
1838 if let Some(env) = &self.on_submit {
1839 semantics.actions.entries.push(fission_ir::ActionEntry {
1840 trigger: fission_ir::semantics::ActionTrigger::Submit,
1841 action_id: env.id.as_u128(),
1842 payload_data: Some(env.payload.clone()),
1843 });
1844 }
1845 if let Some(env) = &self.on_editing_complete {
1846 semantics.actions.entries.push(fission_ir::ActionEntry {
1847 trigger: fission_ir::semantics::ActionTrigger::EditingComplete,
1848 action_id: env.id.as_u128(),
1849 payload_data: Some(env.payload.clone()),
1850 });
1851 }
1852 if let Some(env) = &self.on_tap_outside {
1853 semantics.actions.entries.push(fission_ir::ActionEntry {
1854 trigger: fission_ir::semantics::ActionTrigger::TapOutside,
1855 action_id: env.id.as_u128(),
1856 payload_data: Some(env.payload.clone()),
1857 });
1858 }
1859 if let Some(mouse_cursor) = self.mouse_cursor {
1860 semantics
1861 .actions
1862 .entries
1863 .push(fission_ir::ActionEntry::hover_cursor(mouse_cursor));
1864 }
1865 let mut semantics_builder = InternalIrBuilder::new(input_id, Op::Semantics(semantics));
1866 semantics_builder.add_child(field_body_id);
1867 let semantics_id = semantics_builder.build(cx);
1868 cx.ir.custom_render_objects.insert(
1869 semantics_id,
1870 Arc::new(TextInputRuntimeConfig {
1871 drag_start_behavior: self.drag_start_behavior,
1872 undo_controller: self.undo_controller.clone(),
1873 restoration_id: self.restoration_id.clone(),
1874 spell_check_configuration: self.spell_check_configuration.clone(),
1875 }),
1876 );
1877 semantics_id
1878 }
1879}