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#[derive(Clone)]
220pub(crate) struct TextFieldRefs {
221 pub is_focused: Rc<RefCell<bool>>,
223 pub content_offset: Rc<Cell<f32>>,
225 pub content_y_offset: Rc<Cell<f32>>,
227 pub drag_anchor: Rc<Cell<Option<usize>>>,
229 pub last_click_time: Rc<Cell<Option<web_time::Instant>>>,
231 pub last_click_pos: Rc<Cell<Option<(f32, f32)>>>,
233 pub click_count: Rc<Cell<u8>>,
235 pub node_id: Rc<Cell<Option<cranpose_core::NodeId>>>,
237 pub scroll_offset: Rc<Cell<f32>>,
240 pub last_pointer_source: Rc<Cell<PointerSource>>,
244 pub node_origin: Rc<Cell<Point>>,
248}
249
250impl TextFieldRefs {
251 pub fn new() -> Self {
253 Self {
254 is_focused: Rc::new(RefCell::new(false)),
255 content_offset: Rc::new(Cell::new(0.0_f32)),
256 content_y_offset: Rc::new(Cell::new(0.0_f32)),
257 drag_anchor: Rc::new(Cell::new(None::<usize>)),
258 last_click_time: Rc::new(Cell::new(None::<web_time::Instant>)),
259 last_click_pos: Rc::new(Cell::new(None::<(f32, f32)>)),
260 click_count: Rc::new(Cell::new(0_u8)),
261 node_id: Rc::new(Cell::new(None::<cranpose_core::NodeId>)),
262 scroll_offset: Rc::new(Cell::new(0.0_f32)),
263 last_pointer_source: Rc::new(Cell::new(PointerSource::Unknown)),
264 node_origin: Rc::new(Cell::new(Point { x: 0.0, y: 0.0 })),
265 }
266 }
267}
268
269use crate::text::TextStyle; pub struct TextFieldModifierNode {
278 state: TextFieldState,
280 refs: TextFieldRefs,
282 style: TextStyle, cursor_brush: Brush,
286 selection_brush: Brush,
288 line_limits: TextFieldLineLimits,
290 cached_text: String,
292 cached_selection: TextRange,
294 node_state: NodeState,
296 measured_size: Rc<Cell<Size>>,
298 measured_line_height: Rc<Cell<f32>>,
300 measured_wrap_width: Rc<Cell<Option<f32>>>,
306 cached_handler: Rc<dyn Fn(PointerEvent)>,
308 cached_pan_resolver: TextPanResolver,
310 handle_controller: Option<TextFieldHandleController>,
314}
315
316impl std::fmt::Debug for TextFieldModifierNode {
317 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
318 f.debug_struct("TextFieldModifierNode")
319 .field("text", &self.state.text())
320 .field("style", &self.style)
321 .field("is_focused", &*self.refs.is_focused.borrow())
322 .finish()
323 }
324}
325
326use crate::text_field_handler::TextFieldHandler;
328
329impl TextFieldModifierNode {
330 pub fn new(state: TextFieldState, style: TextStyle) -> Self {
332 let value = state.value();
333 let refs = TextFieldRefs::new();
334 let line_limits = TextFieldLineLimits::default();
335 let cached_handler =
336 Self::create_handler(state.clone(), refs.clone(), line_limits, style.clone());
337 let cached_pan_resolver =
338 Self::create_pan_resolver(state.clone(), refs.clone(), line_limits, style.clone());
339
340 Self {
341 state,
342 refs,
343 style,
344 cursor_brush: Brush::solid(DEFAULT_CURSOR_COLOR),
345 selection_brush: Brush::solid(DEFAULT_SELECTION_COLOR),
346 line_limits,
347 cached_text: value.text,
348 cached_selection: value.selection,
349 node_state: NodeState::new(),
350 measured_size: Rc::new(Cell::new(Size {
351 width: 0.0,
352 height: 0.0,
353 })),
354 measured_line_height: Rc::new(Cell::new(DEFAULT_LINE_HEIGHT)),
355 measured_wrap_width: Rc::new(Cell::new(None)),
356 cached_handler,
357 cached_pan_resolver,
358 handle_controller: None,
359 }
360 }
361
362 pub fn with_line_limits(mut self, line_limits: TextFieldLineLimits) -> Self {
364 self.line_limits = line_limits;
365 self.cached_pan_resolver = Self::create_pan_resolver(
366 self.state.clone(),
367 self.refs.clone(),
368 line_limits,
369 self.style.clone(),
370 );
371 self
372 }
373
374 pub fn with_handle_controller(mut self, controller: TextFieldHandleController) -> Self {
376 self.handle_controller = Some(controller);
377 self
378 }
379
380 fn create_pan_resolver(
388 state: TextFieldState,
389 refs: TextFieldRefs,
390 line_limits: TextFieldLineLimits,
391 style: TextStyle,
392 ) -> TextPanResolver {
393 Rc::new(move |viewport_width: f32| {
394 if !line_limits.is_single_line() {
395 refs.scroll_offset.set(0.0);
397 return 0.0;
398 }
399 let text = state.text();
400 let pos = state.selection().start.min(text.len());
401 let text_width = crate::text::measure_text(
402 &crate::text::AnnotatedString::from(text.as_str()),
403 &style,
404 )
405 .width;
406 let cursor_x = crate::text::measure_text(
407 &crate::text::AnnotatedString::from(&text[..pos]),
408 &style,
409 )
410 .width;
411 let offset = compute_horizontal_scroll_offset(
412 refs.scroll_offset.get(),
413 cursor_x,
414 text_width,
415 viewport_width,
416 );
417 refs.scroll_offset.set(offset);
418 offset
419 })
420 }
421
422 pub fn text_pan_resolver(&self) -> Option<TextPanResolver> {
427 self.line_limits
428 .is_single_line()
429 .then(|| self.cached_pan_resolver.clone())
430 }
431
432 pub fn scroll_offset(&self) -> f32 {
434 self.refs.scroll_offset.get()
435 }
436
437 pub fn line_limits(&self) -> TextFieldLineLimits {
439 self.line_limits
440 }
441
442 fn create_handler(
444 state: TextFieldState,
445 refs: TextFieldRefs,
446 line_limits: TextFieldLineLimits,
447 style: TextStyle, ) -> Rc<dyn Fn(PointerEvent)> {
449 use crate::text_selection::{
452 classify_tap_count, find_line_boundaries, find_paragraph_boundaries,
453 resolve_selection_tap_count, tap_selection_granularity, SelectionGranularity,
454 MULTI_TAP_SLOP_PX, MULTI_TAP_TIMEOUT_MS,
455 };
456 use crate::word_boundaries::find_word_boundaries;
457
458 Rc::new(move |event: PointerEvent| {
459 refs.node_origin.set(Point {
466 x: event.global_position.x - event.position.x,
467 y: event.global_position.y - event.position.y,
468 });
469
470 let click_x =
474 (event.position.x - refs.content_offset.get() + refs.scroll_offset.get()).max(0.0);
475 let click_y = (event.position.y - refs.content_y_offset.get()).max(0.0);
476
477 match event.kind {
478 PointerEventKind::Down => {
479 refs.last_pointer_source.set(event.source);
483
484 let handler =
486 TextFieldHandler::new(state.clone(), refs.node_id.get(), line_limits);
487 crate::text_field_focus::request_focus(refs.is_focused.clone(), handler);
488
489 let now = web_time::Instant::now();
490 let text = state.text();
491 let pos = crate::text::get_offset_for_position(
492 &crate::text::AnnotatedString::from(text.as_str()),
493 &style,
494 click_x,
495 click_y,
496 );
497
498 let previous = refs.last_click_pos.get().and_then(|(px, py)| {
503 let count = refs.click_count.get();
504 (count > 0).then_some((count, px, py))
505 });
506 let elapsed_ms = refs
507 .last_click_time
508 .get()
509 .map(|last| now.duration_since(last).as_millis())
510 .unwrap_or(u128::MAX);
511 let tap_count = classify_tap_count(
512 previous,
513 elapsed_ms,
514 event.position.x,
515 event.position.y,
516 MULTI_TAP_TIMEOUT_MS,
517 MULTI_TAP_SLOP_PX,
518 );
519
520 let selection = state.selection();
528 let tap_in_selection =
529 !selection.collapsed() && pos >= selection.min() && pos <= selection.max();
530 let repeat_in_place = refs
533 .last_click_pos
534 .get()
535 .map(|(px, py)| {
536 let dx = event.position.x - px;
537 let dy = event.position.y - py;
538 dx * dx + dy * dy <= MULTI_TAP_SLOP_PX * MULTI_TAP_SLOP_PX
539 })
540 .unwrap_or(false);
541 let effective_count = resolve_selection_tap_count(
542 tap_count,
543 refs.click_count.get(),
544 tap_in_selection,
545 repeat_in_place,
546 );
547
548 match tap_selection_granularity(effective_count) {
549 SelectionGranularity::Paragraph => {
550 let (start, end) = find_paragraph_boundaries(&text, pos);
552 state.edit(|buffer| {
553 buffer.select(TextRange::new(start, end));
554 });
555 refs.drag_anchor.set(Some(start));
556 }
557 SelectionGranularity::Line => {
558 let (line_start, line_end) = find_line_boundaries(&text, pos);
560 state.edit(|buffer| {
561 buffer.select(TextRange::new(line_start, line_end));
562 });
563 refs.drag_anchor.set(Some(line_start));
564 }
565 SelectionGranularity::Word => {
566 let (word_start, word_end) = find_word_boundaries(&text, pos);
569 state.edit(|buffer| {
570 buffer.select(TextRange::new(word_start, word_end));
571 });
572 refs.drag_anchor.set(Some(word_start));
573 }
574 SelectionGranularity::Caret => {
575 refs.drag_anchor.set(Some(pos));
577 state.edit(|buffer| {
578 buffer.place_cursor_before_char(pos);
579 });
580 }
581 }
582
583 refs.click_count.set(effective_count);
584 refs.last_click_time.set(Some(now));
585 refs.last_click_pos
586 .set(Some((event.position.x, event.position.y)));
587 event.consume();
588 }
589 PointerEventKind::Move => {
590 if let Some(anchor) = refs.drag_anchor.get() {
592 if *refs.is_focused.borrow() {
593 let text = state.text();
594 let current_pos = crate::text::get_offset_for_position(
595 &crate::text::AnnotatedString::from(text.as_str()),
596 &style,
597 click_x,
598 click_y,
599 );
600
601 state.set_selection(TextRange::new(anchor, current_pos));
603
604 crate::request_render_invalidation();
606
607 event.consume();
608 }
609 }
610 }
611 PointerEventKind::Up => {
612 refs.drag_anchor.set(None);
614 }
615 _ => {}
616 }
617 })
618 }
619
620 pub fn with_cursor_color(mut self, color: Color) -> Self {
622 self.cursor_brush = Brush::solid(color);
623 self
624 }
625
626 pub fn set_focused(&mut self, focused: bool) {
628 let current = *self.refs.is_focused.borrow();
629 if current != focused {
630 *self.refs.is_focused.borrow_mut() = focused;
631 }
632 }
633
634 pub fn is_focused(&self) -> bool {
636 *self.refs.is_focused.borrow()
637 }
638
639 pub fn is_focused_rc(&self) -> Rc<RefCell<bool>> {
641 self.refs.is_focused.clone()
642 }
643
644 pub fn content_offset_rc(&self) -> Rc<Cell<f32>> {
646 self.refs.content_offset.clone()
647 }
648
649 pub fn content_y_offset_rc(&self) -> Rc<Cell<f32>> {
651 self.refs.content_y_offset.clone()
652 }
653
654 pub(crate) fn window_origin_sink(&self) -> Rc<Cell<Point>> {
667 self.refs.node_origin.clone()
668 }
669
670 pub fn text(&self) -> String {
672 self.state.text()
673 }
674
675 pub fn style(&self) -> &TextStyle {
676 &self.style
677 }
678
679 pub fn selection(&self) -> TextRange {
681 self.state.selection()
682 }
683
684 pub fn cursor_brush(&self) -> Brush {
686 self.cursor_brush.clone()
687 }
688
689 pub fn selection_brush(&self) -> Brush {
691 self.selection_brush.clone()
692 }
693
694 pub fn insert_text(&mut self, text: &str) {
696 self.state.edit(|buffer| {
697 buffer.insert(text);
698 });
699 }
700
701 pub fn copy_selection(&self) -> Option<String> {
704 self.state.copy_selection()
705 }
706
707 pub fn cut_selection(&mut self) -> Option<String> {
710 let text = self.copy_selection();
711 if text.is_some() {
712 self.state.edit(|buffer| {
713 buffer.delete(buffer.selection());
714 });
715 }
716 text
717 }
718
719 pub fn get_state(&self) -> cranpose_foundation::text::TextFieldState {
722 self.state.clone()
723 }
724
725 pub fn set_content_offset(&self, offset: f32) {
728 self.refs.content_offset.set(offset);
729 }
730
731 pub fn set_content_y_offset(&self, offset: f32) {
734 self.refs.content_y_offset.set(offset);
735 }
736
737 fn wrap_width(&self, available_width: f32) -> Option<f32> {
744 (!self.line_limits.is_single_line() && available_width.is_finite() && available_width > 0.0)
745 .then_some(available_width)
746 }
747
748 fn measure_text_content(&self, wrap_width: Option<f32>) -> Size {
754 let text = self.state.text();
755 let node_id = self.refs.node_id.get();
756 let annotated = crate::text::AnnotatedString::from(text.as_str());
757 let metrics = match wrap_width {
758 Some(max_width) => crate::text::measure_text_with_options_for_node(
759 node_id,
760 &annotated,
761 &self.style,
762 crate::text::TextLayoutOptions::default(),
763 Some(max_width),
764 ),
765 None => crate::text::measure_text_for_node(node_id, &annotated, &self.style),
766 };
767 self.measured_line_height.set(metrics.line_height);
768 Size {
769 width: metrics.width,
770 height: metrics.height,
771 }
772 }
773
774 fn update_cached_state(&mut self) -> bool {
776 let value = self.state.value();
777 let text_changed = value.text != self.cached_text;
778 let selection_changed = value.selection != self.cached_selection;
779
780 if text_changed {
781 self.cached_text = value.text;
782 }
783 if selection_changed {
784 self.cached_selection = value.selection;
785 }
786
787 text_changed || selection_changed
788 }
789
790 pub fn position_cursor_at_offset(&self, x_offset: f32) {
793 let text = self.state.text();
794 if text.is_empty() {
795 self.state.edit(|buffer| {
796 buffer.place_cursor_at_start();
797 });
798 return;
799 }
800
801 let byte_offset = crate::text::get_offset_for_position(
804 &crate::text::AnnotatedString::from(text.as_str()),
805 &self.style,
806 x_offset + self.refs.scroll_offset.get(),
807 0.0,
808 );
809
810 self.state.edit(|buffer| {
811 buffer.place_cursor_before_char(byte_offset);
812 });
813 }
814
815 }
819
820impl DelegatableNode for TextFieldModifierNode {
821 fn node_state(&self) -> &NodeState {
822 &self.node_state
823 }
824}
825
826impl ModifierNode for TextFieldModifierNode {
827 fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
828 self.refs.node_id.set(context.node_id());
830
831 context.invalidate(InvalidationKind::Layout);
832 context.invalidate(InvalidationKind::Draw);
833 context.invalidate(InvalidationKind::Semantics);
834 }
835
836 fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
837 Some(self)
838 }
839
840 fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
841 Some(self)
842 }
843
844 fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
845 Some(self)
846 }
847
848 fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
849 Some(self)
850 }
851
852 fn as_semantics_node(&self) -> Option<&dyn SemanticsNode> {
853 Some(self)
854 }
855
856 fn as_semantics_node_mut(&mut self) -> Option<&mut dyn SemanticsNode> {
857 Some(self)
858 }
859
860 fn as_pointer_input_node(&self) -> Option<&dyn PointerInputNode> {
861 Some(self)
862 }
863
864 fn as_pointer_input_node_mut(&mut self) -> Option<&mut dyn PointerInputNode> {
865 Some(self)
866 }
867}
868
869impl LayoutModifierNode for TextFieldModifierNode {
870 fn measure(
871 &self,
872 _context: &mut dyn ModifierNodeContext,
873 _measurable: &dyn Measurable,
874 constraints: Constraints,
875 ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
876 let wrap_width = self.wrap_width(constraints.max_width);
880 self.measured_wrap_width.set(wrap_width);
883 let text_size = self.measure_text_content(wrap_width);
884
885 let min_height = if text_size.height < 1.0 {
887 DEFAULT_LINE_HEIGHT
888 } else {
889 text_size.height
890 };
891
892 let width = text_size
894 .width
895 .max(constraints.min_width)
896 .min(constraints.max_width);
897 let height = min_height
898 .max(constraints.min_height)
899 .min(constraints.max_height);
900
901 let size = Size { width, height };
902 self.measured_size.set(size);
903
904 let _ = (self.cached_pan_resolver)(size.width);
907
908 cranpose_ui_layout::LayoutModifierMeasureResult::with_size(size)
909 }
910
911 fn min_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
912 self.measure_text_content(None).width
913 }
914
915 fn max_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
916 self.measure_text_content(None).width
917 }
918
919 fn min_intrinsic_height(&self, _measurable: &dyn Measurable, width: f32) -> f32 {
920 self.measure_text_content(self.wrap_width(width))
921 .height
922 .max(DEFAULT_LINE_HEIGHT)
923 }
924
925 fn max_intrinsic_height(&self, _measurable: &dyn Measurable, width: f32) -> f32 {
926 self.measure_text_content(self.wrap_width(width))
927 .height
928 .max(DEFAULT_LINE_HEIGHT)
929 }
930}
931
932impl DrawModifierNode for TextFieldModifierNode {
933 fn draw(&self, _draw_scope: &mut dyn DrawScope) {
934 }
938
939 fn create_draw_closure(
940 &self,
941 ) -> Option<Rc<dyn Fn(cranpose_foundation::Size) -> Vec<cranpose_ui_graphics::DrawPrimitive>>>
942 {
943 use cranpose_ui_graphics::DrawPrimitive;
944
945 let is_focused = self.refs.is_focused.clone();
947 let state = self.state.clone();
948 let content_offset = self.refs.content_offset.clone();
949 let content_y_offset = self.refs.content_y_offset.clone();
950 let cursor_brush = self.cursor_brush.clone();
951 let selection_brush = self.selection_brush.clone();
952 let style = self.style.clone();
953 let cached_line_height = self.measured_line_height.clone();
954 let measured_size = self.measured_size.clone();
955 let measured_wrap_width = self.measured_wrap_width.clone();
956 let node_id = self.refs.node_id.clone();
957 let pan_resolver = self.cached_pan_resolver.clone();
958 let handle_controller = self.handle_controller.clone();
959 let node_origin = self.refs.node_origin.clone();
960 let last_pointer_source = self.refs.last_pointer_source.clone();
961
962 Some(Rc::new(move |size| {
963 if !*is_focused.borrow() {
965 if let Some(controller) = &handle_controller {
968 controller.publish(TextFieldHandleMetrics {
969 focused: false,
970 touch: false,
971 node_origin: node_origin.get(),
972 padding_left: 0.0,
973 padding_top: 0.0,
974 scroll_offset: 0.0,
975 line_height: cached_line_height.get(),
976 wrap_width: measured_wrap_width.get(),
977 });
978 }
979 return vec![];
980 }
981
982 let mut primitives = Vec::new();
983
984 let text = state.text();
985 let selection = state.selection();
986 let padding_left = content_offset.get();
987 let padding_top = content_y_offset.get();
988 let line_height = cached_line_height.get();
991
992 let measured = measured_size.get();
995 let viewport_width = if measured.width > 0.0 {
996 measured.width
997 } else {
998 (size.width - padding_left).max(0.0)
999 };
1000 let viewport_height = if measured.height > 0.0 {
1001 measured.height
1002 } else {
1003 (size.height - padding_top).max(0.0)
1004 };
1005 let pan = pan_resolver(viewport_width);
1007
1008 if let Some(controller) = &handle_controller {
1011 controller.publish(TextFieldHandleMetrics {
1012 focused: true,
1013 touch: last_pointer_source.get().is_touch_like(),
1014 node_origin: node_origin.get(),
1015 padding_left,
1016 padding_top,
1017 scroll_offset: pan,
1018 line_height,
1019 wrap_width: measured_wrap_width.get(),
1020 });
1021 }
1022 let clip_bounds = cranpose_ui_graphics::Rect {
1026 x: padding_left,
1027 y: padding_top,
1028 width: viewport_width,
1029 height: viewport_height,
1030 };
1031
1032 if !selection.collapsed() {
1034 let sel_start = selection.min();
1035 let sel_end = selection.max();
1036
1037 let lines: Vec<&str> = text.split('\n').collect();
1038 let mut byte_offset: usize = 0;
1039
1040 for (line_idx, line) in lines.iter().enumerate() {
1041 let line_start = byte_offset;
1042 let line_end = byte_offset + line.len();
1043
1044 if sel_end > line_start && sel_start < line_end {
1045 let sel_start_in_line = sel_start.saturating_sub(line_start);
1046 let sel_end_in_line = (sel_end - line_start).min(line.len());
1047
1048 let sel_start_x = crate::text::measure_text(
1049 &crate::text::AnnotatedString::from(&line[..sel_start_in_line]),
1050 &style,
1051 )
1052 .width
1053 + padding_left
1054 - pan;
1055 let sel_end_x = crate::text::measure_text(
1056 &crate::text::AnnotatedString::from(&line[..sel_end_in_line]),
1057 &style,
1058 )
1059 .width
1060 + padding_left
1061 - pan;
1062 let sel_width = sel_end_x - sel_start_x;
1063
1064 if sel_width > 0.0 {
1065 let sel_rect = cranpose_ui_graphics::Rect {
1066 x: sel_start_x,
1067 y: padding_top + line_idx as f32 * line_height,
1068 width: sel_width,
1069 height: line_height,
1070 };
1071 if let Some(clipped) = intersect_rect(sel_rect, clip_bounds) {
1072 primitives.push(DrawPrimitive::Rect {
1073 rect: clipped,
1074 brush: selection_brush.clone(),
1075 });
1076 }
1077 }
1078 }
1079 byte_offset = line_end + 1;
1080 }
1081 }
1082
1083 if let Some(comp_range) = state.composition() {
1086 let comp_start = comp_range.min();
1087 let comp_end = comp_range.max();
1088
1089 if comp_start < comp_end && comp_end <= text.len() {
1090 let lines: Vec<&str> = text.split('\n').collect();
1091 let mut byte_offset: usize = 0;
1092
1093 let underline_brush = cranpose_ui_graphics::Brush::solid(
1095 cranpose_ui_graphics::Color(0.8, 0.8, 0.8, 0.8),
1096 );
1097 let underline_height: f32 = 2.0;
1098
1099 for (line_idx, line) in lines.iter().enumerate() {
1100 let line_start = byte_offset;
1101 let line_end = byte_offset + line.len();
1102
1103 if comp_end > line_start && comp_start < line_end {
1105 let comp_start_in_line = comp_start.saturating_sub(line_start);
1106 let comp_end_in_line = (comp_end - line_start).min(line.len());
1107
1108 let comp_start_in_line = if line.is_char_boundary(comp_start_in_line) {
1110 comp_start_in_line
1111 } else {
1112 0
1113 };
1114 let comp_end_in_line = if line.is_char_boundary(comp_end_in_line) {
1115 comp_end_in_line
1116 } else {
1117 line.len()
1118 };
1119
1120 let comp_start_x = crate::text::measure_text(
1121 &crate::text::AnnotatedString::from(&line[..comp_start_in_line]),
1122 &style,
1123 )
1124 .width
1125 + padding_left
1126 - pan;
1127 let comp_end_x = crate::text::measure_text(
1128 &crate::text::AnnotatedString::from(&line[..comp_end_in_line]),
1129 &style,
1130 )
1131 .width
1132 + padding_left
1133 - pan;
1134 let comp_width = comp_end_x - comp_start_x;
1135
1136 if comp_width > 0.0 {
1137 let underline_rect = cranpose_ui_graphics::Rect {
1139 x: comp_start_x,
1140 y: padding_top + (line_idx as f32 + 1.0) * line_height
1141 - underline_height,
1142 width: comp_width,
1143 height: underline_height,
1144 };
1145 if let Some(clipped) = intersect_rect(underline_rect, clip_bounds) {
1146 primitives.push(DrawPrimitive::Rect {
1147 rect: clipped,
1148 brush: underline_brush.clone(),
1149 });
1150 }
1151 }
1152 }
1153 byte_offset = line_end + 1;
1154 }
1155 }
1156 }
1157
1158 if crate::cursor_animation::is_cursor_visible() {
1160 let pos = selection.start.min(text.len());
1161 let (line_index, line_start) = caret_visual_line_for_offset(
1167 &text,
1168 &style,
1169 node_id.get(),
1170 measured_wrap_width.get(),
1171 pos,
1172 );
1173 let cursor_x = crate::text::measure_text(
1174 &crate::text::AnnotatedString::from(&text[line_start..pos]),
1175 &style,
1176 )
1177 .width
1178 + padding_left
1179 - pan;
1180 let cursor_y = padding_top + line_index as f32 * line_height;
1181
1182 let cursor_rect = cranpose_ui_graphics::Rect {
1183 x: cursor_x,
1184 y: cursor_y,
1185 width: CURSOR_WIDTH,
1186 height: line_height,
1187 };
1188
1189 if let Some(clipped) = intersect_rect(cursor_rect, clip_bounds) {
1190 primitives.push(DrawPrimitive::Rect {
1191 rect: clipped,
1192 brush: cursor_brush.clone(),
1193 });
1194 }
1195 }
1196
1197 primitives
1198 }))
1199 }
1200}
1201
1202impl SemanticsNode for TextFieldModifierNode {
1203 fn merge_semantics(&self, config: &mut SemanticsConfiguration) {
1204 let text = self.state.text();
1205 config.content_description = Some(text);
1206 config.is_editable_text = true;
1207 config.text_selection = Some(self.state.selection());
1208 }
1209}
1210
1211impl PointerInputNode for TextFieldModifierNode {
1212 fn on_pointer_event(
1213 &mut self,
1214 _context: &mut dyn ModifierNodeContext,
1215 _event: &PointerEvent,
1216 ) -> bool {
1217 false
1228 }
1229
1230 fn hit_test(&self, x: f32, y: f32) -> bool {
1231 let size = self.measured_size.get();
1233 x >= 0.0 && x <= size.width && y >= 0.0 && y <= size.height
1234 }
1235
1236 fn pointer_input_handler(&self) -> Option<Rc<dyn Fn(PointerEvent)>> {
1237 Some(self.cached_handler.clone())
1239 }
1240}
1241
1242#[derive(Clone)]
1253pub struct TextFieldElement {
1254 state: TextFieldState,
1256 style: TextStyle,
1258 cursor_color: Color,
1260 line_limits: TextFieldLineLimits,
1262 handle_controller: Option<TextFieldHandleController>,
1265}
1266
1267impl TextFieldElement {
1268 pub fn new(state: TextFieldState, style: TextStyle) -> Self {
1270 Self {
1271 state,
1272 style,
1273 cursor_color: DEFAULT_CURSOR_COLOR,
1274 line_limits: TextFieldLineLimits::default(),
1275 handle_controller: None,
1276 }
1277 }
1278
1279 pub fn with_cursor_color(mut self, color: Color) -> Self {
1281 self.cursor_color = color;
1282 self
1283 }
1284
1285 pub fn with_line_limits(mut self, line_limits: TextFieldLineLimits) -> Self {
1287 self.line_limits = line_limits;
1288 self
1289 }
1290
1291 pub fn with_handle_controller(mut self, controller: TextFieldHandleController) -> Self {
1293 self.handle_controller = Some(controller);
1294 self
1295 }
1296}
1297
1298impl std::fmt::Debug for TextFieldElement {
1299 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1300 f.debug_struct("TextFieldElement")
1301 .field("text", &self.state.text())
1302 .field("style", &self.style)
1303 .field("cursor_color", &self.cursor_color)
1304 .finish()
1305 }
1306}
1307
1308impl Hash for TextFieldElement {
1309 fn hash<H: Hasher>(&self, state: &mut H) {
1310 std::ptr::hash(std::rc::Rc::as_ptr(&self.state.inner), state);
1313 self.cursor_color.0.to_bits().hash(state);
1315 self.cursor_color.1.to_bits().hash(state);
1316 self.cursor_color.2.to_bits().hash(state);
1317 self.cursor_color.3.to_bits().hash(state);
1318 self.style.render_hash().hash(state);
1319 self.line_limits.hash(state);
1320 }
1321}
1322
1323impl PartialEq for TextFieldElement {
1324 fn eq(&self, other: &Self) -> bool {
1325 self.state == other.state
1329 && self.style == other.style
1330 && self.cursor_color == other.cursor_color
1331 && self.line_limits == other.line_limits
1332 }
1333}
1334
1335impl Eq for TextFieldElement {}
1336
1337impl ModifierNodeElement for TextFieldElement {
1338 type Node = TextFieldModifierNode;
1339
1340 fn create(&self) -> Self::Node {
1341 let mut node = TextFieldModifierNode::new(self.state.clone(), self.style.clone())
1342 .with_cursor_color(self.cursor_color)
1343 .with_line_limits(self.line_limits);
1344 if let Some(controller) = self.handle_controller.clone() {
1345 node = node.with_handle_controller(controller);
1346 }
1347 node
1348 }
1349
1350 fn update(&self, node: &mut Self::Node) {
1351 node.state = self.state.clone();
1353 node.style = self.style.clone();
1354 node.cursor_brush = Brush::solid(self.cursor_color);
1355 node.line_limits = self.line_limits;
1356 node.handle_controller = self.handle_controller.clone();
1357
1358 node.cached_handler = TextFieldModifierNode::create_handler(
1360 node.state.clone(),
1361 node.refs.clone(),
1362 node.line_limits,
1363 self.style.clone(),
1364 );
1365
1366 node.cached_pan_resolver = TextFieldModifierNode::create_pan_resolver(
1368 node.state.clone(),
1369 node.refs.clone(),
1370 node.line_limits,
1371 self.style.clone(),
1372 );
1373
1374 if node.update_cached_state() {
1376 }
1379 }
1380
1381 fn capabilities(&self) -> NodeCapabilities {
1382 NodeCapabilities::LAYOUT
1383 | NodeCapabilities::DRAW
1384 | NodeCapabilities::SEMANTICS
1385 | NodeCapabilities::POINTER_INPUT
1386 }
1387
1388 fn always_update(&self) -> bool {
1389 true
1391 }
1392}
1393
1394#[cfg(test)]
1395mod tests {
1396 use super::*;
1397 use crate::text::TextStyle;
1398 use cranpose_core::{DefaultScheduler, Runtime};
1399 use std::sync::Arc;
1400
1401 fn with_test_runtime<T>(f: impl FnOnce() -> T) -> T {
1403 let _runtime = Runtime::new(Arc::new(DefaultScheduler));
1404 f()
1405 }
1406
1407 #[test]
1408 fn text_field_node_creation() {
1409 let _app_context = crate::render_state::app_context_test_scope();
1410 with_test_runtime(|| {
1411 let state = TextFieldState::new("Hello");
1412 let node = TextFieldModifierNode::new(state, TextStyle::default());
1413 assert_eq!(node.text(), "Hello");
1414 assert!(!node.is_focused());
1415 });
1416 }
1417
1418 #[test]
1419 fn text_field_node_focus() {
1420 let _app_context = crate::render_state::app_context_test_scope();
1421 with_test_runtime(|| {
1422 let state = TextFieldState::new("Test");
1423 let mut node = TextFieldModifierNode::new(state, TextStyle::default());
1424 assert!(!node.is_focused());
1425
1426 node.set_focused(true);
1427 assert!(node.is_focused());
1428
1429 node.set_focused(false);
1430 assert!(!node.is_focused());
1431 });
1432 }
1433
1434 #[test]
1435 fn text_field_element_creates_node() {
1436 let _app_context = crate::render_state::app_context_test_scope();
1437 with_test_runtime(|| {
1438 let state = TextFieldState::new("Hello World");
1439 let element = TextFieldElement::new(state, TextStyle::default());
1440
1441 let node = element.create();
1442 assert_eq!(node.text(), "Hello World");
1443 });
1444 }
1445
1446 #[test]
1455 fn touch_press_publishes_touch_handle_metrics_but_mouse_does_not() {
1456 use cranpose_foundation::{PointerEvent, PointerEventKind, PointerSource};
1457 use cranpose_ui_graphics::Point;
1458
1459 let _app_context = crate::render_state::app_context_test_scope();
1460 with_test_runtime(|| {
1461 let state = TextFieldState::new("hello world");
1462 let controller = TextFieldHandleController::new();
1463 let node = TextFieldModifierNode::new(state.clone(), TextStyle::default())
1464 .with_handle_controller(controller.clone());
1465 node.measured_size.set(Size {
1467 width: 120.0,
1468 height: 20.0,
1469 });
1470
1471 let handler = node
1472 .pointer_input_handler()
1473 .expect("field exposes a pointer handler");
1474 let draw = node
1475 .create_draw_closure()
1476 .expect("field exposes a draw closure");
1477 let at = Point { x: 12.0, y: 8.0 };
1478 let size = Size {
1479 width: 120.0,
1480 height: 20.0,
1481 };
1482
1483 handler(
1486 PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Touch),
1487 );
1488 let _ = draw(size);
1489 let metrics = controller
1490 .metrics()
1491 .expect("focused field publishes handle metrics");
1492 assert!(metrics.focused, "a tap focuses the field");
1493 assert!(
1494 metrics.touch,
1495 "a touch tap must publish touch = true so the finger handles show"
1496 );
1497
1498 handler(
1501 PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Mouse),
1502 );
1503 let _ = draw(size);
1504 let metrics = controller
1505 .metrics()
1506 .expect("focused field publishes handle metrics");
1507 assert!(
1508 !metrics.touch,
1509 "a mouse tap must publish touch = false (clean caret, no finger handle)"
1510 );
1511
1512 crate::text_field_focus::clear_focus();
1513 });
1514 }
1515
1516 #[test]
1524 fn double_tap_selects_the_word_under_the_finger() {
1525 use cranpose_foundation::{PointerEvent, PointerEventKind, PointerSource};
1526 use cranpose_ui_graphics::Point;
1527
1528 let _app_context = crate::render_state::app_context_test_scope();
1529 with_test_runtime(|| {
1530 let state = TextFieldState::new("hello world");
1531 let node = TextFieldModifierNode::new(state.clone(), TextStyle::default());
1532 node.measured_size.set(Size {
1533 width: 200.0,
1534 height: 20.0,
1535 });
1536 let handler = node
1537 .pointer_input_handler()
1538 .expect("field exposes a pointer handler");
1539
1540 let at = Point { x: 2.0, y: 8.0 };
1543 handler(
1544 PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Touch),
1545 );
1546 handler(
1547 PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Touch),
1548 );
1549
1550 let selection = state.selection();
1551 assert!(
1552 !selection.collapsed(),
1553 "a double tap must produce a (word) selection, got {selection:?}"
1554 );
1555 let selected = &state.text()[selection.min()..selection.max()];
1556 assert_eq!(
1557 selected, "hello",
1558 "double tap should select the whole word under the finger"
1559 );
1560
1561 crate::text_field_focus::clear_focus();
1562 });
1563 }
1564
1565 #[test]
1569 fn repeated_taps_escalate_word_line_paragraph_then_cycle() {
1570 use cranpose_foundation::{PointerEvent, PointerEventKind, PointerSource};
1571 use cranpose_ui_graphics::Point;
1572
1573 let _app_context = crate::render_state::app_context_test_scope();
1574 with_test_runtime(|| {
1575 let text = "alpha beta\ngamma delta\n\nsecond para";
1578 let state = TextFieldState::new(text);
1579 let node = TextFieldModifierNode::new(state.clone(), TextStyle::default())
1580 .with_line_limits(TextFieldLineLimits::MultiLine {
1581 min_lines: 1,
1582 max_lines: usize::MAX,
1583 });
1584 node.measured_size.set(Size {
1585 width: 400.0,
1586 height: 80.0,
1587 });
1588 let handler = node
1589 .pointer_input_handler()
1590 .expect("field exposes a pointer handler");
1591
1592 let at = Point { x: 2.0, y: 4.0 };
1594 let tap = || {
1595 handler(
1596 PointerEvent::new(PointerEventKind::Down, at, at)
1597 .with_source(PointerSource::Touch),
1598 );
1599 };
1600 let selected = |state: &TextFieldState| {
1601 let s = state.selection();
1602 state.text()[s.min()..s.max()].to_string()
1603 };
1604
1605 tap(); assert!(state.selection().collapsed(), "first tap places the caret");
1607 tap(); assert_eq!(selected(&state), "alpha", "double tap selects the word");
1609 tap(); assert_eq!(
1611 selected(&state),
1612 "alpha beta",
1613 "triple tap selects the line"
1614 );
1615 tap(); assert_eq!(
1617 selected(&state),
1618 "alpha beta\ngamma delta",
1619 "fourth tap grows to the paragraph"
1620 );
1621 tap(); assert_eq!(
1623 selected(&state),
1624 "alpha",
1625 "fifth tap cycles back to the word"
1626 );
1627
1628 crate::text_field_focus::clear_focus();
1629 });
1630 }
1631
1632 #[test]
1636 fn single_tap_inside_selection_selects_the_word() {
1637 use cranpose_foundation::{PointerEvent, PointerEventKind, PointerSource};
1638 use cranpose_ui_graphics::Point;
1639
1640 let _app_context = crate::render_state::app_context_test_scope();
1641 with_test_runtime(|| {
1642 let state = TextFieldState::new("hello world");
1643 let node = TextFieldModifierNode::new(state.clone(), TextStyle::default());
1644 node.measured_size.set(Size {
1645 width: 200.0,
1646 height: 20.0,
1647 });
1648 let handler = node
1649 .pointer_input_handler()
1650 .expect("field exposes a pointer handler");
1651
1652 state.edit(|buffer| buffer.select(TextRange::new(0, 11)));
1654 assert!(!state.selection().collapsed());
1655
1656 let at = Point { x: 2.0, y: 8.0 };
1659 handler(
1660 PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Touch),
1661 );
1662
1663 let selection = state.selection();
1664 assert!(
1665 !selection.collapsed(),
1666 "a tap inside a selection must not collapse it, got {selection:?}"
1667 );
1668 assert_eq!(
1669 &state.text()[selection.min()..selection.max()],
1670 "hello",
1671 "a tap inside a selection re-selects the word under the finger"
1672 );
1673
1674 crate::text_field_focus::clear_focus();
1675 });
1676 }
1677
1678 #[test]
1686 fn slow_taps_inside_selection_cycle_word_line_paragraph_by_location() {
1687 use cranpose_foundation::{PointerEvent, PointerEventKind, PointerSource};
1688 use cranpose_ui_graphics::Point;
1689
1690 let _app_context = crate::render_state::app_context_test_scope();
1691 with_test_runtime(|| {
1692 let text = "alpha beta\ngamma delta\n\nsecond para";
1693 let state = TextFieldState::new(text);
1694 let node = TextFieldModifierNode::new(state.clone(), TextStyle::default())
1695 .with_line_limits(TextFieldLineLimits::MultiLine {
1696 min_lines: 1,
1697 max_lines: usize::MAX,
1698 });
1699 node.measured_size.set(Size {
1700 width: 400.0,
1701 height: 80.0,
1702 });
1703 let handler = node
1704 .pointer_input_handler()
1705 .expect("field exposes a pointer handler");
1706
1707 state.edit(|buffer| buffer.select(TextRange::new(0, text.len())));
1709
1710 let at = Point { x: 2.0, y: 4.0 };
1711 let selected = |state: &TextFieldState| {
1712 let s = state.selection();
1713 state.text()[s.min()..s.max()].to_string()
1714 };
1715 let slow_tap = || {
1718 node.refs.last_click_time.set(None);
1719 handler(
1720 PointerEvent::new(PointerEventKind::Down, at, at)
1721 .with_source(PointerSource::Touch),
1722 );
1723 };
1724
1725 slow_tap(); assert_eq!(
1727 selected(&state),
1728 "alpha",
1729 "tap inside selection grabs the word"
1730 );
1731 slow_tap(); assert_eq!(
1733 selected(&state),
1734 "alpha beta",
1735 "same-spot tap grows to the line even after the timeout"
1736 );
1737 slow_tap(); assert_eq!(
1739 selected(&state),
1740 "alpha beta\ngamma delta",
1741 "same-spot tap grows to the paragraph"
1742 );
1743 slow_tap(); assert_eq!(
1745 selected(&state),
1746 "alpha",
1747 "same-spot tap cycles back to the word"
1748 );
1749
1750 crate::text_field_focus::clear_focus();
1751 });
1752 }
1753
1754 #[test]
1755 fn text_field_element_equality() {
1756 let _app_context = crate::render_state::app_context_test_scope();
1757 with_test_runtime(|| {
1758 let state1 = TextFieldState::new("Hello");
1759 let state2 = TextFieldState::new("Hello"); let elem1 = TextFieldElement::new(state1.clone(), TextStyle::default());
1762 let elem2 = TextFieldElement::new(state1.clone(), TextStyle::default()); let elem3 = TextFieldElement::new(state2, TextStyle::default()); assert_eq!(elem1, elem2, "Same state should be equal");
1768 assert_ne!(elem1, elem3, "Different states should not be equal");
1769 });
1770 }
1771
1772 #[test]
1773 fn text_field_element_update_refreshes_existing_node_style() {
1774 let _app_context = crate::render_state::app_context_test_scope();
1775 with_test_runtime(|| {
1776 let state = TextFieldState::new("themed text");
1777 let dark_style = TextStyle::from_span_style(crate::text::SpanStyle {
1778 color: Some(Color::from_rgba_u8(228, 240, 252, 255)),
1779 ..crate::text::SpanStyle::default()
1780 });
1781 let light_style = TextStyle::from_span_style(crate::text::SpanStyle {
1782 color: Some(Color::from_rgba_u8(14, 58, 96, 255)),
1783 ..crate::text::SpanStyle::default()
1784 });
1785 let initial = TextFieldElement::new(state.clone(), dark_style);
1786 let updated = TextFieldElement::new(state, light_style.clone());
1787 let mut node = initial.create();
1788
1789 updated.update(&mut node);
1790
1791 assert_eq!(node.text(), "themed text");
1792 assert_eq!(node.style(), &light_style);
1793 });
1794 }
1795
1796 #[test]
1801 fn multiline_field_measures_wrapped_height() {
1802 let _app_context = crate::render_state::app_context_test_scope();
1803 with_test_runtime(|| {
1804 let long = "abcd ".repeat(40); let state = TextFieldState::new(&long);
1806 let node = TextFieldModifierNode::new(state, TextStyle::default());
1807 assert!(
1808 !node.line_limits().is_single_line(),
1809 "default fields are multi-line"
1810 );
1811
1812 let natural = node.measure_text_content(None);
1813 let wrapped = node.measure_text_content(node.wrap_width(20.0));
1814
1815 assert!(
1816 wrapped.height > natural.height,
1817 "wrapped multi-line height {} must exceed the single-line height {}",
1818 wrapped.height,
1819 natural.height
1820 );
1821 });
1822 }
1823
1824 #[test]
1827 fn single_line_field_never_wraps() {
1828 let _app_context = crate::render_state::app_context_test_scope();
1829 with_test_runtime(|| {
1830 let state = TextFieldState::new("abcd ".repeat(40));
1831 let node = TextFieldModifierNode::new(state, TextStyle::default())
1832 .with_line_limits(TextFieldLineLimits::SingleLine);
1833 assert_eq!(
1834 node.wrap_width(20.0),
1835 None,
1836 "single-line fields must not wrap"
1837 );
1838 });
1839 }
1840
1841 #[test]
1847 fn test_cursor_x_position_calculation() {
1848 let _app_context = crate::render_state::app_context_test_scope();
1849 with_test_runtime(|| {
1850 let style = crate::text::TextStyle::default();
1852
1853 let empty_width =
1855 crate::text::measure_text(&crate::text::AnnotatedString::from(""), &style).width;
1856 assert!(
1857 empty_width.abs() < 0.1,
1858 "Empty text should have 0 width, got {}",
1859 empty_width
1860 );
1861
1862 let hi_width =
1864 crate::text::measure_text(&crate::text::AnnotatedString::from("Hi"), &style).width;
1865 assert!(
1866 hi_width > 0.0,
1867 "Text 'Hi' should have positive width: {}",
1868 hi_width
1869 );
1870
1871 let h_width =
1873 crate::text::measure_text(&crate::text::AnnotatedString::from("H"), &style).width;
1874 assert!(h_width > 0.0, "Text 'H' should have positive width");
1875 assert!(
1876 h_width < hi_width,
1877 "'H' width {} should be less than 'Hi' width {}",
1878 h_width,
1879 hi_width
1880 );
1881
1882 let state = TextFieldState::new("Hi");
1884 assert_eq!(
1885 state.selection().start,
1886 2,
1887 "Cursor should be at position 2 (end of 'Hi')"
1888 );
1889
1890 let text = state.text();
1892 let cursor_pos = state.selection().start;
1893 let text_before_cursor = &text[..cursor_pos.min(text.len())];
1894 assert_eq!(text_before_cursor, "Hi");
1895
1896 let cursor_x = crate::text::measure_text(
1898 &crate::text::AnnotatedString::from(text_before_cursor),
1899 &style,
1900 )
1901 .width;
1902 assert!(
1903 (cursor_x - hi_width).abs() < 0.1,
1904 "Cursor x {} should equal 'Hi' width {}",
1905 cursor_x,
1906 hi_width
1907 );
1908 });
1909 }
1910
1911 #[test]
1913 fn test_focused_node_creates_cursor() {
1914 let _app_context = crate::render_state::app_context_test_scope();
1915 with_test_runtime(|| {
1916 let state = TextFieldState::new("Test");
1917 let element = TextFieldElement::new(state.clone(), TextStyle::default());
1918 let node = element.create();
1919
1920 assert!(!node.is_focused());
1922
1923 *node.refs.is_focused.borrow_mut() = true;
1925 assert!(node.is_focused());
1926
1927 assert_eq!(node.text(), "Test");
1929
1930 assert_eq!(node.selection().start, 4);
1932 });
1933 }
1934}