1use cranpose_core::{mutableStateOf, MutableState};
20use cranpose_foundation::text::{TextFieldLineLimits, TextFieldState, TextRange};
21use cranpose_foundation::{
22 Constraints, DelegatableNode, DrawModifierNode, DrawScope, InvalidationKind,
23 LayoutModifierNode, Measurable, ModifierNode, ModifierNodeContext, ModifierNodeElement,
24 NodeCapabilities, NodeState, PointerEvent, PointerEventKind, PointerInputNode, PointerSource,
25 SemanticsConfiguration, SemanticsNode, Size,
26};
27use cranpose_ui_graphics::{Brush, Color, Point};
28use std::cell::{Cell, RefCell};
29use std::hash::{Hash, Hasher};
30use std::rc::Rc;
31
32#[derive(Clone, Copy, PartialEq, Debug)]
37pub struct TextFieldHandleMetrics {
38 pub focused: bool,
39 pub touch: bool,
40 pub node_origin: Point,
42 pub padding_left: f32,
43 pub padding_top: f32,
44 pub scroll_offset: f32,
45 pub line_height: f32,
46 pub wrap_width: Option<f32>,
49}
50
51#[derive(Clone)]
56pub struct TextFieldHandleController {
57 inner: Rc<TextFieldHandleControllerInner>,
58}
59
60impl PartialEq for TextFieldHandleController {
61 fn eq(&self, other: &Self) -> bool {
62 Rc::ptr_eq(&self.inner, &other.inner)
63 }
64}
65
66struct TextFieldHandleControllerInner {
67 metrics: Cell<Option<TextFieldHandleMetrics>>,
68 revision: MutableState<u64>,
69}
70
71impl TextFieldHandleController {
72 pub fn new() -> Self {
75 Self {
76 inner: Rc::new(TextFieldHandleControllerInner {
77 metrics: Cell::new(None),
78 revision: mutableStateOf(0u64),
79 }),
80 }
81 }
82
83 pub(crate) fn publish(&self, metrics: TextFieldHandleMetrics) {
86 if self.inner.metrics.get() != Some(metrics) {
87 self.inner.metrics.set(Some(metrics));
88 self.inner
89 .revision
90 .update(|value| *value = value.wrapping_add(1));
91 }
92 }
93
94 pub fn metrics(&self) -> Option<TextFieldHandleMetrics> {
97 let _ = self.inner.revision.value();
98 self.inner.metrics.get()
99 }
100}
101
102impl Default for TextFieldHandleController {
103 fn default() -> Self {
104 Self::new()
105 }
106}
107
108const DEFAULT_CURSOR_COLOR: Color = Color(1.0, 1.0, 1.0, 1.0);
110
111const DEFAULT_SELECTION_COLOR: Color = Color(0.0, 0.5, 1.0, 0.3);
113
114const DEFAULT_LINE_HEIGHT: f32 = 20.0;
116
117const CURSOR_WIDTH: f32 = 2.0;
119
120pub(crate) fn compute_horizontal_scroll_offset(
131 current_offset: f32,
132 cursor_x: f32,
133 text_width: f32,
134 viewport_width: f32,
135) -> f32 {
136 if viewport_width <= 0.0 {
137 return 0.0;
138 }
139 let max_offset = (text_width + CURSOR_WIDTH - viewport_width).max(0.0);
140 let mut offset = current_offset.clamp(0.0, max_offset);
141 let visible_end = offset + viewport_width - CURSOR_WIDTH;
142 if cursor_x > visible_end {
143 offset = cursor_x - viewport_width + CURSOR_WIDTH;
145 } else if cursor_x < offset {
146 offset = cursor_x;
148 }
149 offset.clamp(0.0, max_offset)
150}
151
152pub(crate) fn intersect_rect(
157 rect: cranpose_ui_graphics::Rect,
158 bounds: cranpose_ui_graphics::Rect,
159) -> Option<cranpose_ui_graphics::Rect> {
160 let x0 = rect.x.max(bounds.x);
161 let y0 = rect.y.max(bounds.y);
162 let x1 = (rect.x + rect.width).min(bounds.x + bounds.width);
163 let y1 = (rect.y + rect.height).min(bounds.y + bounds.height);
164 (x1 > x0 && y1 > y0).then_some(cranpose_ui_graphics::Rect {
165 x: x0,
166 y: y0,
167 width: x1 - x0,
168 height: y1 - y0,
169 })
170}
171
172pub type TextPanResolver = Rc<dyn Fn(f32) -> f32>;
175
176pub(crate) fn caret_visual_line_for_offset(
186 text: &str,
187 style: &TextStyle,
188 node_id: Option<cranpose_core::NodeId>,
189 wrap_width: Option<f32>,
190 offset: usize,
191) -> (usize, usize) {
192 let offset = offset.min(text.len());
193 match wrap_width {
194 Some(width) if width.is_finite() && width > 0.0 => {
195 let annotated = crate::text::AnnotatedString::from(text);
196 let ranges = crate::text::wrapped_line_ranges(
197 node_id,
198 &annotated,
199 style,
200 crate::text::TextLayoutOptions::default(),
201 Some(width),
202 );
203 crate::text_selection::caret_visual_line(&ranges, offset)
204 }
205 _ => {
206 let before = &text[..offset];
207 let line_index = before.matches('\n').count();
208 let line_start = before.rfind('\n').map(|i| i + 1).unwrap_or(0);
209 (line_index, line_start)
210 }
211 }
212}
213
214#[allow(clippy::too_many_arguments)]
225pub(crate) fn range_visual_line_rects(
226 text: &str,
227 style: &TextStyle,
228 node_id: Option<cranpose_core::NodeId>,
229 wrap_width: Option<f32>,
230 padding_left: f32,
231 padding_top: f32,
232 pan: f32,
233 line_height: f32,
234 start: usize,
235 end: usize,
236) -> Vec<cranpose_ui_graphics::Rect> {
237 if start >= end {
238 return Vec::new();
239 }
240 let annotated = crate::text::AnnotatedString::from(text);
241 let line_ranges = crate::text::wrapped_line_ranges(
242 node_id,
243 &annotated,
244 style,
245 crate::text::TextLayoutOptions::default(),
246 wrap_width,
247 );
248 let mut rects = Vec::new();
249 for (line_idx, line_range) in line_ranges.iter().enumerate() {
250 let line_start = line_range.start;
251 let line_end = line_range.end;
252 if end <= line_start || start >= line_end {
253 continue;
254 }
255 let seg_start = start.max(line_start);
256 let seg_end = end.min(line_end);
257 let x0 = crate::text::measure_text(
258 &crate::text::AnnotatedString::from(&text[line_start..seg_start]),
259 style,
260 )
261 .width
262 + padding_left
263 - pan;
264 let x1 = crate::text::measure_text(
265 &crate::text::AnnotatedString::from(&text[line_start..seg_end]),
266 style,
267 )
268 .width
269 + padding_left
270 - pan;
271 let width = x1 - x0;
272 if width > 0.0 {
273 rects.push(cranpose_ui_graphics::Rect {
274 x: x0,
275 y: padding_top + line_idx as f32 * line_height,
276 width,
277 height: line_height,
278 });
279 }
280 }
281 rects
282}
283
284#[derive(Clone)]
290pub(crate) struct TextFieldRefs {
291 pub is_focused: Rc<RefCell<bool>>,
293 pub content_offset: Rc<Cell<f32>>,
295 pub content_y_offset: Rc<Cell<f32>>,
297 pub drag_anchor: Rc<Cell<Option<usize>>>,
299 pub last_click_time: Rc<Cell<Option<web_time::Instant>>>,
301 pub last_click_pos: Rc<Cell<Option<(f32, f32)>>>,
303 pub click_count: Rc<Cell<u8>>,
305 pub node_id: Rc<Cell<Option<cranpose_core::NodeId>>>,
307 pub scroll_offset: Rc<Cell<f32>>,
310 pub last_pointer_source: Rc<Cell<PointerSource>>,
314 pub node_origin: Rc<Cell<Point>>,
318}
319
320impl TextFieldRefs {
321 pub fn new() -> Self {
323 Self {
324 is_focused: Rc::new(RefCell::new(false)),
325 content_offset: Rc::new(Cell::new(0.0_f32)),
326 content_y_offset: Rc::new(Cell::new(0.0_f32)),
327 drag_anchor: Rc::new(Cell::new(None::<usize>)),
328 last_click_time: Rc::new(Cell::new(None::<web_time::Instant>)),
329 last_click_pos: Rc::new(Cell::new(None::<(f32, f32)>)),
330 click_count: Rc::new(Cell::new(0_u8)),
331 node_id: Rc::new(Cell::new(None::<cranpose_core::NodeId>)),
332 scroll_offset: Rc::new(Cell::new(0.0_f32)),
333 last_pointer_source: Rc::new(Cell::new(PointerSource::Unknown)),
334 node_origin: Rc::new(Cell::new(Point { x: 0.0, y: 0.0 })),
335 }
336 }
337}
338
339use crate::text::TextStyle; pub struct TextFieldModifierNode {
348 state: TextFieldState,
350 refs: TextFieldRefs,
352 style: TextStyle, cursor_brush: Brush,
356 selection_brush: Brush,
358 line_limits: TextFieldLineLimits,
360 cached_text: String,
362 cached_selection: TextRange,
364 node_state: NodeState,
366 measured_size: Rc<Cell<Size>>,
368 measured_line_height: Rc<Cell<f32>>,
370 measured_wrap_width: Rc<Cell<Option<f32>>>,
376 cached_handler: Rc<dyn Fn(PointerEvent)>,
378 cached_pan_resolver: TextPanResolver,
380 handle_controller: Option<TextFieldHandleController>,
384}
385
386impl std::fmt::Debug for TextFieldModifierNode {
387 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
388 f.debug_struct("TextFieldModifierNode")
389 .field("text", &self.state.text())
390 .field("style", &self.style)
391 .field("is_focused", &*self.refs.is_focused.borrow())
392 .finish()
393 }
394}
395
396use crate::text_field_handler::TextFieldHandler;
398
399impl TextFieldModifierNode {
400 pub fn new(state: TextFieldState, style: TextStyle) -> Self {
402 let value = state.value();
403 let refs = TextFieldRefs::new();
404 let line_limits = TextFieldLineLimits::default();
405 let cached_handler =
406 Self::create_handler(state.clone(), refs.clone(), line_limits, style.clone());
407 let cached_pan_resolver =
408 Self::create_pan_resolver(state.clone(), refs.clone(), line_limits, style.clone());
409
410 Self {
411 state,
412 refs,
413 style,
414 cursor_brush: Brush::solid(DEFAULT_CURSOR_COLOR),
415 selection_brush: Brush::solid(DEFAULT_SELECTION_COLOR),
416 line_limits,
417 cached_text: value.text,
418 cached_selection: value.selection,
419 node_state: NodeState::new(),
420 measured_size: Rc::new(Cell::new(Size {
421 width: 0.0,
422 height: 0.0,
423 })),
424 measured_line_height: Rc::new(Cell::new(DEFAULT_LINE_HEIGHT)),
425 measured_wrap_width: Rc::new(Cell::new(None)),
426 cached_handler,
427 cached_pan_resolver,
428 handle_controller: None,
429 }
430 }
431
432 pub fn with_line_limits(mut self, line_limits: TextFieldLineLimits) -> Self {
434 self.line_limits = line_limits;
435 self.cached_pan_resolver = Self::create_pan_resolver(
436 self.state.clone(),
437 self.refs.clone(),
438 line_limits,
439 self.style.clone(),
440 );
441 self
442 }
443
444 pub fn with_handle_controller(mut self, controller: TextFieldHandleController) -> Self {
446 self.handle_controller = Some(controller);
447 self
448 }
449
450 fn create_pan_resolver(
458 state: TextFieldState,
459 refs: TextFieldRefs,
460 line_limits: TextFieldLineLimits,
461 style: TextStyle,
462 ) -> TextPanResolver {
463 Rc::new(move |viewport_width: f32| {
464 if !line_limits.is_single_line() {
465 refs.scroll_offset.set(0.0);
467 return 0.0;
468 }
469 let text = state.text();
470 let pos = state.selection().start.min(text.len());
471 let text_width = crate::text::measure_text(
472 &crate::text::AnnotatedString::from(text.as_str()),
473 &style,
474 )
475 .width;
476 let cursor_x = crate::text::measure_text(
477 &crate::text::AnnotatedString::from(&text[..pos]),
478 &style,
479 )
480 .width;
481 let offset = compute_horizontal_scroll_offset(
482 refs.scroll_offset.get(),
483 cursor_x,
484 text_width,
485 viewport_width,
486 );
487 refs.scroll_offset.set(offset);
488 offset
489 })
490 }
491
492 pub fn text_pan_resolver(&self) -> Option<TextPanResolver> {
497 self.line_limits
498 .is_single_line()
499 .then(|| self.cached_pan_resolver.clone())
500 }
501
502 pub fn scroll_offset(&self) -> f32 {
504 self.refs.scroll_offset.get()
505 }
506
507 pub fn line_limits(&self) -> TextFieldLineLimits {
509 self.line_limits
510 }
511
512 fn create_handler(
514 state: TextFieldState,
515 refs: TextFieldRefs,
516 line_limits: TextFieldLineLimits,
517 style: TextStyle, ) -> Rc<dyn Fn(PointerEvent)> {
519 use crate::text_selection::{
522 classify_tap_count, find_line_boundaries, find_paragraph_boundaries,
523 resolve_selection_tap_count, tap_selection_granularity, SelectionGranularity,
524 MULTI_TAP_SLOP_PX, MULTI_TAP_TIMEOUT_MS,
525 };
526 use crate::word_boundaries::find_word_boundaries;
527
528 Rc::new(move |event: PointerEvent| {
529 refs.node_origin.set(Point {
536 x: event.global_position.x - event.position.x,
537 y: event.global_position.y - event.position.y,
538 });
539
540 let click_x =
544 (event.position.x - refs.content_offset.get() + refs.scroll_offset.get()).max(0.0);
545 let click_y = (event.position.y - refs.content_y_offset.get()).max(0.0);
546
547 match event.kind {
548 PointerEventKind::Down => {
549 refs.last_pointer_source.set(event.source);
553
554 let handler = TextFieldHandler::new(
559 state.clone(),
560 refs.node_id.get(),
561 line_limits,
562 crate::text_field_handler::CaretGeometryRefs {
563 node_origin: refs.node_origin.clone(),
564 content_offset: refs.content_offset.clone(),
565 content_y_offset: refs.content_y_offset.clone(),
566 scroll_offset: refs.scroll_offset.clone(),
567 style: style.clone(),
568 },
569 );
570 crate::text_field_focus::request_focus(refs.is_focused.clone(), handler);
571
572 let now = web_time::Instant::now();
573 let text = state.text();
574 let pos = crate::text::get_offset_for_position(
575 &crate::text::AnnotatedString::from(text.as_str()),
576 &style,
577 click_x,
578 click_y,
579 );
580
581 let previous = refs.last_click_pos.get().and_then(|(px, py)| {
586 let count = refs.click_count.get();
587 (count > 0).then_some((count, px, py))
588 });
589 let elapsed_ms = refs
590 .last_click_time
591 .get()
592 .map(|last| now.duration_since(last).as_millis())
593 .unwrap_or(u128::MAX);
594 let tap_count = classify_tap_count(
595 previous,
596 elapsed_ms,
597 event.position.x,
598 event.position.y,
599 MULTI_TAP_TIMEOUT_MS,
600 MULTI_TAP_SLOP_PX,
601 );
602
603 let selection = state.selection();
611 let tap_in_selection =
612 !selection.collapsed() && pos >= selection.min() && pos <= selection.max();
613 let repeat_in_place = refs
616 .last_click_pos
617 .get()
618 .map(|(px, py)| {
619 let dx = event.position.x - px;
620 let dy = event.position.y - py;
621 dx * dx + dy * dy <= MULTI_TAP_SLOP_PX * MULTI_TAP_SLOP_PX
622 })
623 .unwrap_or(false);
624 let effective_count = resolve_selection_tap_count(
625 tap_count,
626 refs.click_count.get(),
627 tap_in_selection,
628 repeat_in_place,
629 );
630
631 match tap_selection_granularity(effective_count) {
632 SelectionGranularity::Paragraph => {
633 let (start, end) = find_paragraph_boundaries(&text, pos);
635 state.edit(|buffer| {
636 buffer.select(TextRange::new(start, end));
637 });
638 refs.drag_anchor.set(Some(start));
639 }
640 SelectionGranularity::Line => {
641 let (line_start, line_end) = find_line_boundaries(&text, pos);
643 state.edit(|buffer| {
644 buffer.select(TextRange::new(line_start, line_end));
645 });
646 refs.drag_anchor.set(Some(line_start));
647 }
648 SelectionGranularity::Word => {
649 let (word_start, word_end) = find_word_boundaries(&text, pos);
652 state.edit(|buffer| {
653 buffer.select(TextRange::new(word_start, word_end));
654 });
655 refs.drag_anchor.set(Some(word_start));
656 }
657 SelectionGranularity::Caret => {
658 refs.drag_anchor.set(Some(pos));
660 state.edit(|buffer| {
661 buffer.place_cursor_before_char(pos);
662 });
663 }
664 }
665
666 refs.click_count.set(effective_count);
667 refs.last_click_time.set(Some(now));
668 refs.last_click_pos
669 .set(Some((event.position.x, event.position.y)));
670 event.consume();
671 }
672 PointerEventKind::Move => {
673 if let Some(anchor) = refs.drag_anchor.get() {
675 if *refs.is_focused.borrow() {
676 let text = state.text();
677 let current_pos = crate::text::get_offset_for_position(
678 &crate::text::AnnotatedString::from(text.as_str()),
679 &style,
680 click_x,
681 click_y,
682 );
683
684 state.set_selection(TextRange::new(anchor, current_pos));
686
687 crate::request_render_invalidation();
689
690 event.consume();
691 }
692 }
693 }
694 PointerEventKind::Up => {
695 refs.drag_anchor.set(None);
697 }
698 _ => {}
699 }
700 })
701 }
702
703 pub fn with_cursor_color(mut self, color: Color) -> Self {
705 self.cursor_brush = Brush::solid(color);
706 self
707 }
708
709 pub fn set_focused(&mut self, focused: bool) {
711 let current = *self.refs.is_focused.borrow();
712 if current != focused {
713 *self.refs.is_focused.borrow_mut() = focused;
714 }
715 }
716
717 pub fn is_focused(&self) -> bool {
719 *self.refs.is_focused.borrow()
720 }
721
722 pub fn is_focused_rc(&self) -> Rc<RefCell<bool>> {
724 self.refs.is_focused.clone()
725 }
726
727 pub fn content_offset_rc(&self) -> Rc<Cell<f32>> {
729 self.refs.content_offset.clone()
730 }
731
732 pub fn content_y_offset_rc(&self) -> Rc<Cell<f32>> {
734 self.refs.content_y_offset.clone()
735 }
736
737 pub(crate) fn window_origin_sink(&self) -> Rc<Cell<Point>> {
750 self.refs.node_origin.clone()
751 }
752
753 pub fn text(&self) -> String {
755 self.state.text()
756 }
757
758 pub fn style(&self) -> &TextStyle {
759 &self.style
760 }
761
762 pub fn selection(&self) -> TextRange {
764 self.state.selection()
765 }
766
767 pub fn cursor_brush(&self) -> Brush {
769 self.cursor_brush.clone()
770 }
771
772 pub fn selection_brush(&self) -> Brush {
774 self.selection_brush.clone()
775 }
776
777 pub fn insert_text(&mut self, text: &str) {
779 self.state.edit(|buffer| {
780 buffer.insert(text);
781 });
782 }
783
784 pub fn copy_selection(&self) -> Option<String> {
787 self.state.copy_selection()
788 }
789
790 pub fn cut_selection(&mut self) -> Option<String> {
793 let text = self.copy_selection();
794 if text.is_some() {
795 self.state.edit(|buffer| {
796 buffer.delete(buffer.selection());
797 });
798 }
799 text
800 }
801
802 pub fn get_state(&self) -> cranpose_foundation::text::TextFieldState {
805 self.state.clone()
806 }
807
808 pub fn set_content_offset(&self, offset: f32) {
811 self.refs.content_offset.set(offset);
812 }
813
814 pub fn set_content_y_offset(&self, offset: f32) {
817 self.refs.content_y_offset.set(offset);
818 }
819
820 fn wrap_width(&self, available_width: f32) -> Option<f32> {
827 (!self.line_limits.is_single_line() && available_width.is_finite() && available_width > 0.0)
828 .then_some(available_width)
829 }
830
831 fn measure_text_content(&self, wrap_width: Option<f32>) -> Size {
837 let text = self.state.text();
838 let node_id = self.refs.node_id.get();
839 let annotated = crate::text::AnnotatedString::from(text.as_str());
840 let metrics = match wrap_width {
841 Some(max_width) => crate::text::measure_text_with_options_for_node(
842 node_id,
843 &annotated,
844 &self.style,
845 crate::text::TextLayoutOptions::default(),
846 Some(max_width),
847 ),
848 None => crate::text::measure_text_for_node(node_id, &annotated, &self.style),
849 };
850 self.measured_line_height.set(metrics.line_height);
851 Size {
852 width: metrics.width,
853 height: metrics.height,
854 }
855 }
856
857 fn update_cached_state(&mut self) -> bool {
859 let value = self.state.value();
860 let text_changed = value.text != self.cached_text;
861 let selection_changed = value.selection != self.cached_selection;
862
863 if text_changed {
864 self.cached_text = value.text;
865 }
866 if selection_changed {
867 self.cached_selection = value.selection;
868 }
869
870 text_changed || selection_changed
871 }
872
873 pub fn position_cursor_at_offset(&self, x_offset: f32) {
876 let text = self.state.text();
877 if text.is_empty() {
878 self.state.edit(|buffer| {
879 buffer.place_cursor_at_start();
880 });
881 return;
882 }
883
884 let byte_offset = crate::text::get_offset_for_position(
887 &crate::text::AnnotatedString::from(text.as_str()),
888 &self.style,
889 x_offset + self.refs.scroll_offset.get(),
890 0.0,
891 );
892
893 self.state.edit(|buffer| {
894 buffer.place_cursor_before_char(byte_offset);
895 });
896 }
897
898 }
902
903impl DelegatableNode for TextFieldModifierNode {
904 fn node_state(&self) -> &NodeState {
905 &self.node_state
906 }
907}
908
909impl ModifierNode for TextFieldModifierNode {
910 fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
911 self.refs.node_id.set(context.node_id());
913
914 context.invalidate(InvalidationKind::Layout);
915 context.invalidate(InvalidationKind::Draw);
916 context.invalidate(InvalidationKind::Semantics);
917 }
918
919 fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
920 Some(self)
921 }
922
923 fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
924 Some(self)
925 }
926
927 fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
928 Some(self)
929 }
930
931 fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
932 Some(self)
933 }
934
935 fn as_semantics_node(&self) -> Option<&dyn SemanticsNode> {
936 Some(self)
937 }
938
939 fn as_semantics_node_mut(&mut self) -> Option<&mut dyn SemanticsNode> {
940 Some(self)
941 }
942
943 fn as_pointer_input_node(&self) -> Option<&dyn PointerInputNode> {
944 Some(self)
945 }
946
947 fn as_pointer_input_node_mut(&mut self) -> Option<&mut dyn PointerInputNode> {
948 Some(self)
949 }
950}
951
952impl LayoutModifierNode for TextFieldModifierNode {
953 fn measure(
954 &self,
955 _context: &mut dyn ModifierNodeContext,
956 _measurable: &dyn Measurable,
957 constraints: Constraints,
958 ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
959 let wrap_width = self.wrap_width(constraints.max_width);
963 self.measured_wrap_width.set(wrap_width);
966 let text_size = self.measure_text_content(wrap_width);
967
968 let min_height = if text_size.height < 1.0 {
970 DEFAULT_LINE_HEIGHT
971 } else {
972 text_size.height
973 };
974
975 let width = text_size
977 .width
978 .max(constraints.min_width)
979 .min(constraints.max_width);
980 let height = min_height
981 .max(constraints.min_height)
982 .min(constraints.max_height);
983
984 let size = Size { width, height };
985 self.measured_size.set(size);
986
987 let _ = (self.cached_pan_resolver)(size.width);
990
991 cranpose_ui_layout::LayoutModifierMeasureResult::with_size(size)
992 }
993
994 fn min_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
995 self.measure_text_content(None).width
996 }
997
998 fn max_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
999 self.measure_text_content(None).width
1000 }
1001
1002 fn min_intrinsic_height(&self, _measurable: &dyn Measurable, width: f32) -> f32 {
1003 self.measure_text_content(self.wrap_width(width))
1004 .height
1005 .max(DEFAULT_LINE_HEIGHT)
1006 }
1007
1008 fn max_intrinsic_height(&self, _measurable: &dyn Measurable, width: f32) -> f32 {
1009 self.measure_text_content(self.wrap_width(width))
1010 .height
1011 .max(DEFAULT_LINE_HEIGHT)
1012 }
1013}
1014
1015impl DrawModifierNode for TextFieldModifierNode {
1016 fn draw(&self, _draw_scope: &mut dyn DrawScope) {
1017 }
1021
1022 fn create_draw_closure(
1023 &self,
1024 ) -> Option<Rc<dyn Fn(cranpose_foundation::Size) -> Vec<cranpose_ui_graphics::DrawPrimitive>>>
1025 {
1026 use cranpose_ui_graphics::DrawPrimitive;
1027
1028 let is_focused = self.refs.is_focused.clone();
1030 let state = self.state.clone();
1031 let content_offset = self.refs.content_offset.clone();
1032 let content_y_offset = self.refs.content_y_offset.clone();
1033 let cursor_brush = self.cursor_brush.clone();
1034 let selection_brush = self.selection_brush.clone();
1035 let style = self.style.clone();
1036 let cached_line_height = self.measured_line_height.clone();
1037 let measured_size = self.measured_size.clone();
1038 let measured_wrap_width = self.measured_wrap_width.clone();
1039 let node_id = self.refs.node_id.clone();
1040 let pan_resolver = self.cached_pan_resolver.clone();
1041 let handle_controller = self.handle_controller.clone();
1042 let node_origin = self.refs.node_origin.clone();
1043 let last_pointer_source = self.refs.last_pointer_source.clone();
1044
1045 Some(Rc::new(move |size| {
1046 if !*is_focused.borrow() {
1048 if let Some(controller) = &handle_controller {
1051 controller.publish(TextFieldHandleMetrics {
1052 focused: false,
1053 touch: false,
1054 node_origin: node_origin.get(),
1055 padding_left: 0.0,
1056 padding_top: 0.0,
1057 scroll_offset: 0.0,
1058 line_height: cached_line_height.get(),
1059 wrap_width: measured_wrap_width.get(),
1060 });
1061 }
1062 return vec![];
1063 }
1064
1065 let mut primitives = Vec::new();
1066
1067 let text = state.text();
1068 let selection = state.selection();
1069 let padding_left = content_offset.get();
1070 let padding_top = content_y_offset.get();
1071 let line_height = cached_line_height.get();
1074
1075 let measured = measured_size.get();
1078 let viewport_width = if measured.width > 0.0 {
1079 measured.width
1080 } else {
1081 (size.width - padding_left).max(0.0)
1082 };
1083 let viewport_height = if measured.height > 0.0 {
1084 measured.height
1085 } else {
1086 (size.height - padding_top).max(0.0)
1087 };
1088 let pan = pan_resolver(viewport_width);
1090
1091 if let Some(controller) = &handle_controller {
1094 controller.publish(TextFieldHandleMetrics {
1095 focused: true,
1096 touch: last_pointer_source.get().is_touch_like(),
1097 node_origin: node_origin.get(),
1098 padding_left,
1099 padding_top,
1100 scroll_offset: pan,
1101 line_height,
1102 wrap_width: measured_wrap_width.get(),
1103 });
1104 }
1105 let clip_bounds = cranpose_ui_graphics::Rect {
1109 x: padding_left,
1110 y: padding_top,
1111 width: viewport_width,
1112 height: viewport_height,
1113 };
1114
1115 if !selection.collapsed() {
1117 let sel_start = selection.min();
1118 let sel_end = selection.max();
1119
1120 for sel_rect in range_visual_line_rects(
1123 &text,
1124 &style,
1125 node_id.get(),
1126 measured_wrap_width.get(),
1127 padding_left,
1128 padding_top,
1129 pan,
1130 line_height,
1131 sel_start,
1132 sel_end,
1133 ) {
1134 if let Some(clipped) = intersect_rect(sel_rect, clip_bounds) {
1135 primitives.push(DrawPrimitive::Rect {
1136 rect: clipped,
1137 brush: selection_brush.clone(),
1138 });
1139 }
1140 }
1141 }
1142
1143 if let Some(comp_range) = state.composition() {
1146 let comp_start = comp_range.min();
1147 let comp_end = comp_range.max();
1148
1149 if comp_start < comp_end && comp_end <= text.len() {
1150 let underline_brush = cranpose_ui_graphics::Brush::solid(
1152 cranpose_ui_graphics::Color(0.8, 0.8, 0.8, 0.8),
1153 );
1154 let underline_height: f32 = 2.0;
1155
1156 for line_rect in range_visual_line_rects(
1159 &text,
1160 &style,
1161 node_id.get(),
1162 measured_wrap_width.get(),
1163 padding_left,
1164 padding_top,
1165 pan,
1166 line_height,
1167 comp_start,
1168 comp_end,
1169 ) {
1170 let underline_rect = cranpose_ui_graphics::Rect {
1171 x: line_rect.x,
1172 y: line_rect.y + line_height - underline_height,
1173 width: line_rect.width,
1174 height: underline_height,
1175 };
1176 if let Some(clipped) = intersect_rect(underline_rect, clip_bounds) {
1177 primitives.push(DrawPrimitive::Rect {
1178 rect: clipped,
1179 brush: underline_brush.clone(),
1180 });
1181 }
1182 }
1183 }
1184 }
1185
1186 if crate::cursor_animation::is_cursor_visible() {
1188 let pos = selection.start.min(text.len());
1189 let (line_index, line_start) = caret_visual_line_for_offset(
1195 &text,
1196 &style,
1197 node_id.get(),
1198 measured_wrap_width.get(),
1199 pos,
1200 );
1201 let cursor_x = crate::text::measure_text(
1202 &crate::text::AnnotatedString::from(&text[line_start..pos]),
1203 &style,
1204 )
1205 .width
1206 + padding_left
1207 - pan;
1208 let cursor_y = padding_top + line_index as f32 * line_height;
1209
1210 let cursor_rect = cranpose_ui_graphics::Rect {
1211 x: cursor_x,
1212 y: cursor_y,
1213 width: CURSOR_WIDTH,
1214 height: line_height,
1215 };
1216
1217 if let Some(clipped) = intersect_rect(cursor_rect, clip_bounds) {
1218 primitives.push(DrawPrimitive::Rect {
1219 rect: clipped,
1220 brush: cursor_brush.clone(),
1221 });
1222 }
1223 }
1224
1225 primitives
1226 }))
1227 }
1228}
1229
1230impl SemanticsNode for TextFieldModifierNode {
1231 fn merge_semantics(&self, config: &mut SemanticsConfiguration) {
1232 let text = self.state.text();
1233 config.content_description = Some(text);
1234 config.is_editable_text = true;
1235 config.text_selection = Some(self.state.selection());
1236 }
1237}
1238
1239impl PointerInputNode for TextFieldModifierNode {
1240 fn on_pointer_event(
1241 &mut self,
1242 _context: &mut dyn ModifierNodeContext,
1243 _event: &PointerEvent,
1244 ) -> bool {
1245 false
1256 }
1257
1258 fn hit_test(&self, x: f32, y: f32) -> bool {
1259 let size = self.measured_size.get();
1261 x >= 0.0 && x <= size.width && y >= 0.0 && y <= size.height
1262 }
1263
1264 fn pointer_input_handler(&self) -> Option<Rc<dyn Fn(PointerEvent)>> {
1265 Some(self.cached_handler.clone())
1267 }
1268}
1269
1270#[derive(Clone)]
1281pub struct TextFieldElement {
1282 state: TextFieldState,
1284 style: TextStyle,
1286 cursor_color: Color,
1288 line_limits: TextFieldLineLimits,
1290 handle_controller: Option<TextFieldHandleController>,
1293}
1294
1295impl TextFieldElement {
1296 pub fn new(state: TextFieldState, style: TextStyle) -> Self {
1298 Self {
1299 state,
1300 style,
1301 cursor_color: DEFAULT_CURSOR_COLOR,
1302 line_limits: TextFieldLineLimits::default(),
1303 handle_controller: None,
1304 }
1305 }
1306
1307 pub fn with_cursor_color(mut self, color: Color) -> Self {
1309 self.cursor_color = color;
1310 self
1311 }
1312
1313 pub fn with_line_limits(mut self, line_limits: TextFieldLineLimits) -> Self {
1315 self.line_limits = line_limits;
1316 self
1317 }
1318
1319 pub fn with_handle_controller(mut self, controller: TextFieldHandleController) -> Self {
1321 self.handle_controller = Some(controller);
1322 self
1323 }
1324}
1325
1326impl std::fmt::Debug for TextFieldElement {
1327 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1328 f.debug_struct("TextFieldElement")
1329 .field("text", &self.state.text())
1330 .field("style", &self.style)
1331 .field("cursor_color", &self.cursor_color)
1332 .finish()
1333 }
1334}
1335
1336impl Hash for TextFieldElement {
1337 fn hash<H: Hasher>(&self, state: &mut H) {
1338 std::ptr::hash(std::rc::Rc::as_ptr(&self.state.inner), state);
1341 self.cursor_color.0.to_bits().hash(state);
1343 self.cursor_color.1.to_bits().hash(state);
1344 self.cursor_color.2.to_bits().hash(state);
1345 self.cursor_color.3.to_bits().hash(state);
1346 self.style.render_hash().hash(state);
1347 self.line_limits.hash(state);
1348 }
1349}
1350
1351impl PartialEq for TextFieldElement {
1352 fn eq(&self, other: &Self) -> bool {
1353 self.state == other.state
1357 && self.style == other.style
1358 && self.cursor_color == other.cursor_color
1359 && self.line_limits == other.line_limits
1360 }
1361}
1362
1363impl Eq for TextFieldElement {}
1364
1365impl ModifierNodeElement for TextFieldElement {
1366 type Node = TextFieldModifierNode;
1367
1368 fn create(&self) -> Self::Node {
1369 let mut node = TextFieldModifierNode::new(self.state.clone(), self.style.clone())
1370 .with_cursor_color(self.cursor_color)
1371 .with_line_limits(self.line_limits);
1372 if let Some(controller) = self.handle_controller.clone() {
1373 node = node.with_handle_controller(controller);
1374 }
1375 node
1376 }
1377
1378 fn update(&self, node: &mut Self::Node) {
1379 node.state = self.state.clone();
1381 node.style = self.style.clone();
1382 node.cursor_brush = Brush::solid(self.cursor_color);
1383 node.line_limits = self.line_limits;
1384 node.handle_controller = self.handle_controller.clone();
1385
1386 node.cached_handler = TextFieldModifierNode::create_handler(
1388 node.state.clone(),
1389 node.refs.clone(),
1390 node.line_limits,
1391 self.style.clone(),
1392 );
1393
1394 node.cached_pan_resolver = TextFieldModifierNode::create_pan_resolver(
1396 node.state.clone(),
1397 node.refs.clone(),
1398 node.line_limits,
1399 self.style.clone(),
1400 );
1401
1402 if node.update_cached_state() {
1404 }
1407 }
1408
1409 fn capabilities(&self) -> NodeCapabilities {
1410 NodeCapabilities::LAYOUT
1411 | NodeCapabilities::DRAW
1412 | NodeCapabilities::SEMANTICS
1413 | NodeCapabilities::POINTER_INPUT
1414 }
1415
1416 fn always_update(&self) -> bool {
1417 true
1419 }
1420}
1421
1422#[cfg(test)]
1423mod tests {
1424 use super::*;
1425 use crate::text::TextStyle;
1426 use cranpose_core::{DefaultScheduler, Runtime};
1427 use std::sync::Arc;
1428
1429 fn with_test_runtime<T>(f: impl FnOnce() -> T) -> T {
1431 let _runtime = Runtime::new(Arc::new(DefaultScheduler));
1432 f()
1433 }
1434
1435 #[test]
1436 fn text_field_node_creation() {
1437 let _app_context = crate::render_state::app_context_test_scope();
1438 with_test_runtime(|| {
1439 let state = TextFieldState::new("Hello");
1440 let node = TextFieldModifierNode::new(state, TextStyle::default());
1441 assert_eq!(node.text(), "Hello");
1442 assert!(!node.is_focused());
1443 });
1444 }
1445
1446 #[test]
1451 fn selection_rects_follow_wrapped_visual_lines() {
1452 let _app_context = crate::render_state::app_context_test_scope();
1453 let text = "aaaaa\nbb";
1456 let style = TextStyle::default();
1457 let line_height = 10.0_f32;
1458
1459 let rects = range_visual_line_rects(
1462 text,
1463 &style,
1464 None,
1465 Some(30.0),
1466 0.0,
1467 0.0,
1468 0.0,
1469 line_height,
1470 6,
1471 8,
1472 );
1473 assert_eq!(rects.len(), 1, "one visual line touched, got {rects:?}");
1474 assert_eq!(
1475 rects[0].y,
1476 2.0 * line_height,
1477 "highlight must land on visual line 2, not logical line 1"
1478 );
1479 assert!(rects[0].width > 0.0);
1480
1481 let spanning = range_visual_line_rects(
1484 text,
1485 &style,
1486 None,
1487 Some(30.0),
1488 0.0,
1489 0.0,
1490 0.0,
1491 line_height,
1492 0,
1493 5,
1494 );
1495 assert_eq!(spanning.len(), 2, "wrapped line spans two visual rows");
1496 assert_eq!(spanning[0].y, 0.0);
1497 assert_eq!(spanning[1].y, line_height);
1498 }
1499
1500 #[test]
1501 fn text_field_node_focus() {
1502 let _app_context = crate::render_state::app_context_test_scope();
1503 with_test_runtime(|| {
1504 let state = TextFieldState::new("Test");
1505 let mut node = TextFieldModifierNode::new(state, TextStyle::default());
1506 assert!(!node.is_focused());
1507
1508 node.set_focused(true);
1509 assert!(node.is_focused());
1510
1511 node.set_focused(false);
1512 assert!(!node.is_focused());
1513 });
1514 }
1515
1516 #[test]
1517 fn text_field_element_creates_node() {
1518 let _app_context = crate::render_state::app_context_test_scope();
1519 with_test_runtime(|| {
1520 let state = TextFieldState::new("Hello World");
1521 let element = TextFieldElement::new(state, TextStyle::default());
1522
1523 let node = element.create();
1524 assert_eq!(node.text(), "Hello World");
1525 });
1526 }
1527
1528 #[test]
1537 fn touch_press_publishes_touch_handle_metrics_but_mouse_does_not() {
1538 use cranpose_foundation::{PointerEvent, PointerEventKind, PointerSource};
1539 use cranpose_ui_graphics::Point;
1540
1541 let _app_context = crate::render_state::app_context_test_scope();
1542 with_test_runtime(|| {
1543 let state = TextFieldState::new("hello world");
1544 let controller = TextFieldHandleController::new();
1545 let node = TextFieldModifierNode::new(state.clone(), TextStyle::default())
1546 .with_handle_controller(controller.clone());
1547 node.measured_size.set(Size {
1549 width: 120.0,
1550 height: 20.0,
1551 });
1552
1553 let handler = node
1554 .pointer_input_handler()
1555 .expect("field exposes a pointer handler");
1556 let draw = node
1557 .create_draw_closure()
1558 .expect("field exposes a draw closure");
1559 let at = Point { x: 12.0, y: 8.0 };
1560 let size = Size {
1561 width: 120.0,
1562 height: 20.0,
1563 };
1564
1565 handler(
1568 PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Touch),
1569 );
1570 let _ = draw(size);
1571 let metrics = controller
1572 .metrics()
1573 .expect("focused field publishes handle metrics");
1574 assert!(metrics.focused, "a tap focuses the field");
1575 assert!(
1576 metrics.touch,
1577 "a touch tap must publish touch = true so the finger handles show"
1578 );
1579
1580 handler(
1583 PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Mouse),
1584 );
1585 let _ = draw(size);
1586 let metrics = controller
1587 .metrics()
1588 .expect("focused field publishes handle metrics");
1589 assert!(
1590 !metrics.touch,
1591 "a mouse tap must publish touch = false (clean caret, no finger handle)"
1592 );
1593
1594 crate::text_field_focus::clear_focus();
1595 });
1596 }
1597
1598 #[test]
1606 fn double_tap_selects_the_word_under_the_finger() {
1607 use cranpose_foundation::{PointerEvent, PointerEventKind, PointerSource};
1608 use cranpose_ui_graphics::Point;
1609
1610 let _app_context = crate::render_state::app_context_test_scope();
1611 with_test_runtime(|| {
1612 let state = TextFieldState::new("hello world");
1613 let node = TextFieldModifierNode::new(state.clone(), TextStyle::default());
1614 node.measured_size.set(Size {
1615 width: 200.0,
1616 height: 20.0,
1617 });
1618 let handler = node
1619 .pointer_input_handler()
1620 .expect("field exposes a pointer handler");
1621
1622 let at = Point { x: 2.0, y: 8.0 };
1625 handler(
1626 PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Touch),
1627 );
1628 handler(
1629 PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Touch),
1630 );
1631
1632 let selection = state.selection();
1633 assert!(
1634 !selection.collapsed(),
1635 "a double tap must produce a (word) selection, got {selection:?}"
1636 );
1637 let selected = &state.text()[selection.min()..selection.max()];
1638 assert_eq!(
1639 selected, "hello",
1640 "double tap should select the whole word under the finger"
1641 );
1642
1643 crate::text_field_focus::clear_focus();
1644 });
1645 }
1646
1647 #[test]
1651 fn repeated_taps_escalate_word_line_paragraph_then_cycle() {
1652 use cranpose_foundation::{PointerEvent, PointerEventKind, PointerSource};
1653 use cranpose_ui_graphics::Point;
1654
1655 let _app_context = crate::render_state::app_context_test_scope();
1656 with_test_runtime(|| {
1657 let text = "alpha beta\ngamma delta\n\nsecond para";
1660 let state = TextFieldState::new(text);
1661 let node = TextFieldModifierNode::new(state.clone(), TextStyle::default())
1662 .with_line_limits(TextFieldLineLimits::MultiLine {
1663 min_lines: 1,
1664 max_lines: usize::MAX,
1665 });
1666 node.measured_size.set(Size {
1667 width: 400.0,
1668 height: 80.0,
1669 });
1670 let handler = node
1671 .pointer_input_handler()
1672 .expect("field exposes a pointer handler");
1673
1674 let at = Point { x: 2.0, y: 4.0 };
1676 let tap = || {
1677 handler(
1678 PointerEvent::new(PointerEventKind::Down, at, at)
1679 .with_source(PointerSource::Touch),
1680 );
1681 };
1682 let selected = |state: &TextFieldState| {
1683 let s = state.selection();
1684 state.text()[s.min()..s.max()].to_string()
1685 };
1686
1687 tap(); assert!(state.selection().collapsed(), "first tap places the caret");
1689 tap(); assert_eq!(selected(&state), "alpha", "double tap selects the word");
1691 tap(); assert_eq!(
1693 selected(&state),
1694 "alpha beta",
1695 "triple tap selects the line"
1696 );
1697 tap(); assert_eq!(
1699 selected(&state),
1700 "alpha beta\ngamma delta",
1701 "fourth tap grows to the paragraph"
1702 );
1703 tap(); assert_eq!(
1705 selected(&state),
1706 "alpha",
1707 "fifth tap cycles back to the word"
1708 );
1709
1710 crate::text_field_focus::clear_focus();
1711 });
1712 }
1713
1714 #[test]
1718 fn single_tap_inside_selection_selects_the_word() {
1719 use cranpose_foundation::{PointerEvent, PointerEventKind, PointerSource};
1720 use cranpose_ui_graphics::Point;
1721
1722 let _app_context = crate::render_state::app_context_test_scope();
1723 with_test_runtime(|| {
1724 let state = TextFieldState::new("hello world");
1725 let node = TextFieldModifierNode::new(state.clone(), TextStyle::default());
1726 node.measured_size.set(Size {
1727 width: 200.0,
1728 height: 20.0,
1729 });
1730 let handler = node
1731 .pointer_input_handler()
1732 .expect("field exposes a pointer handler");
1733
1734 state.edit(|buffer| buffer.select(TextRange::new(0, 11)));
1736 assert!(!state.selection().collapsed());
1737
1738 let at = Point { x: 2.0, y: 8.0 };
1741 handler(
1742 PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Touch),
1743 );
1744
1745 let selection = state.selection();
1746 assert!(
1747 !selection.collapsed(),
1748 "a tap inside a selection must not collapse it, got {selection:?}"
1749 );
1750 assert_eq!(
1751 &state.text()[selection.min()..selection.max()],
1752 "hello",
1753 "a tap inside a selection re-selects the word under the finger"
1754 );
1755
1756 crate::text_field_focus::clear_focus();
1757 });
1758 }
1759
1760 #[test]
1768 fn slow_taps_inside_selection_cycle_word_line_paragraph_by_location() {
1769 use cranpose_foundation::{PointerEvent, PointerEventKind, PointerSource};
1770 use cranpose_ui_graphics::Point;
1771
1772 let _app_context = crate::render_state::app_context_test_scope();
1773 with_test_runtime(|| {
1774 let text = "alpha beta\ngamma delta\n\nsecond para";
1775 let state = TextFieldState::new(text);
1776 let node = TextFieldModifierNode::new(state.clone(), TextStyle::default())
1777 .with_line_limits(TextFieldLineLimits::MultiLine {
1778 min_lines: 1,
1779 max_lines: usize::MAX,
1780 });
1781 node.measured_size.set(Size {
1782 width: 400.0,
1783 height: 80.0,
1784 });
1785 let handler = node
1786 .pointer_input_handler()
1787 .expect("field exposes a pointer handler");
1788
1789 state.edit(|buffer| buffer.select(TextRange::new(0, text.len())));
1791
1792 let at = Point { x: 2.0, y: 4.0 };
1793 let selected = |state: &TextFieldState| {
1794 let s = state.selection();
1795 state.text()[s.min()..s.max()].to_string()
1796 };
1797 let slow_tap = || {
1800 node.refs.last_click_time.set(None);
1801 handler(
1802 PointerEvent::new(PointerEventKind::Down, at, at)
1803 .with_source(PointerSource::Touch),
1804 );
1805 };
1806
1807 slow_tap(); assert_eq!(
1809 selected(&state),
1810 "alpha",
1811 "tap inside selection grabs the word"
1812 );
1813 slow_tap(); assert_eq!(
1815 selected(&state),
1816 "alpha beta",
1817 "same-spot tap grows to the line even after the timeout"
1818 );
1819 slow_tap(); assert_eq!(
1821 selected(&state),
1822 "alpha beta\ngamma delta",
1823 "same-spot tap grows to the paragraph"
1824 );
1825 slow_tap(); assert_eq!(
1827 selected(&state),
1828 "alpha",
1829 "same-spot tap cycles back to the word"
1830 );
1831
1832 crate::text_field_focus::clear_focus();
1833 });
1834 }
1835
1836 #[test]
1837 fn text_field_element_equality() {
1838 let _app_context = crate::render_state::app_context_test_scope();
1839 with_test_runtime(|| {
1840 let state1 = TextFieldState::new("Hello");
1841 let state2 = TextFieldState::new("Hello"); let elem1 = TextFieldElement::new(state1.clone(), TextStyle::default());
1844 let elem2 = TextFieldElement::new(state1.clone(), TextStyle::default()); let elem3 = TextFieldElement::new(state2, TextStyle::default()); assert_eq!(elem1, elem2, "Same state should be equal");
1850 assert_ne!(elem1, elem3, "Different states should not be equal");
1851 });
1852 }
1853
1854 #[test]
1855 fn text_field_element_update_refreshes_existing_node_style() {
1856 let _app_context = crate::render_state::app_context_test_scope();
1857 with_test_runtime(|| {
1858 let state = TextFieldState::new("themed text");
1859 let dark_style = TextStyle::from_span_style(crate::text::SpanStyle {
1860 color: Some(Color::from_rgba_u8(228, 240, 252, 255)),
1861 ..crate::text::SpanStyle::default()
1862 });
1863 let light_style = TextStyle::from_span_style(crate::text::SpanStyle {
1864 color: Some(Color::from_rgba_u8(14, 58, 96, 255)),
1865 ..crate::text::SpanStyle::default()
1866 });
1867 let initial = TextFieldElement::new(state.clone(), dark_style);
1868 let updated = TextFieldElement::new(state, light_style.clone());
1869 let mut node = initial.create();
1870
1871 updated.update(&mut node);
1872
1873 assert_eq!(node.text(), "themed text");
1874 assert_eq!(node.style(), &light_style);
1875 });
1876 }
1877
1878 #[test]
1883 fn multiline_field_measures_wrapped_height() {
1884 let _app_context = crate::render_state::app_context_test_scope();
1885 with_test_runtime(|| {
1886 let long = "abcd ".repeat(40); let state = TextFieldState::new(&long);
1888 let node = TextFieldModifierNode::new(state, TextStyle::default());
1889 assert!(
1890 !node.line_limits().is_single_line(),
1891 "default fields are multi-line"
1892 );
1893
1894 let natural = node.measure_text_content(None);
1895 let wrapped = node.measure_text_content(node.wrap_width(20.0));
1896
1897 assert!(
1898 wrapped.height > natural.height,
1899 "wrapped multi-line height {} must exceed the single-line height {}",
1900 wrapped.height,
1901 natural.height
1902 );
1903 });
1904 }
1905
1906 #[test]
1909 fn single_line_field_never_wraps() {
1910 let _app_context = crate::render_state::app_context_test_scope();
1911 with_test_runtime(|| {
1912 let state = TextFieldState::new("abcd ".repeat(40));
1913 let node = TextFieldModifierNode::new(state, TextStyle::default())
1914 .with_line_limits(TextFieldLineLimits::SingleLine);
1915 assert_eq!(
1916 node.wrap_width(20.0),
1917 None,
1918 "single-line fields must not wrap"
1919 );
1920 });
1921 }
1922
1923 #[test]
1929 fn test_cursor_x_position_calculation() {
1930 let _app_context = crate::render_state::app_context_test_scope();
1931 with_test_runtime(|| {
1932 let style = crate::text::TextStyle::default();
1934
1935 let empty_width =
1937 crate::text::measure_text(&crate::text::AnnotatedString::from(""), &style).width;
1938 assert!(
1939 empty_width.abs() < 0.1,
1940 "Empty text should have 0 width, got {}",
1941 empty_width
1942 );
1943
1944 let hi_width =
1946 crate::text::measure_text(&crate::text::AnnotatedString::from("Hi"), &style).width;
1947 assert!(
1948 hi_width > 0.0,
1949 "Text 'Hi' should have positive width: {}",
1950 hi_width
1951 );
1952
1953 let h_width =
1955 crate::text::measure_text(&crate::text::AnnotatedString::from("H"), &style).width;
1956 assert!(h_width > 0.0, "Text 'H' should have positive width");
1957 assert!(
1958 h_width < hi_width,
1959 "'H' width {} should be less than 'Hi' width {}",
1960 h_width,
1961 hi_width
1962 );
1963
1964 let state = TextFieldState::new("Hi");
1966 assert_eq!(
1967 state.selection().start,
1968 2,
1969 "Cursor should be at position 2 (end of 'Hi')"
1970 );
1971
1972 let text = state.text();
1974 let cursor_pos = state.selection().start;
1975 let text_before_cursor = &text[..cursor_pos.min(text.len())];
1976 assert_eq!(text_before_cursor, "Hi");
1977
1978 let cursor_x = crate::text::measure_text(
1980 &crate::text::AnnotatedString::from(text_before_cursor),
1981 &style,
1982 )
1983 .width;
1984 assert!(
1985 (cursor_x - hi_width).abs() < 0.1,
1986 "Cursor x {} should equal 'Hi' width {}",
1987 cursor_x,
1988 hi_width
1989 );
1990 });
1991 }
1992
1993 #[test]
1995 fn test_focused_node_creates_cursor() {
1996 let _app_context = crate::render_state::app_context_test_scope();
1997 with_test_runtime(|| {
1998 let state = TextFieldState::new("Test");
1999 let element = TextFieldElement::new(state.clone(), TextStyle::default());
2000 let node = element.create();
2001
2002 assert!(!node.is_focused());
2004
2005 *node.refs.is_focused.borrow_mut() = true;
2007 assert!(node.is_focused());
2008
2009 assert_eq!(node.text(), "Test");
2011
2012 assert_eq!(node.selection().start, 4);
2014 });
2015 }
2016}