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}
47
48#[derive(Clone)]
53pub struct TextFieldHandleController {
54 inner: Rc<TextFieldHandleControllerInner>,
55}
56
57impl PartialEq for TextFieldHandleController {
58 fn eq(&self, other: &Self) -> bool {
59 Rc::ptr_eq(&self.inner, &other.inner)
60 }
61}
62
63struct TextFieldHandleControllerInner {
64 metrics: Cell<Option<TextFieldHandleMetrics>>,
65 revision: MutableState<u64>,
66}
67
68impl TextFieldHandleController {
69 pub fn new() -> Self {
72 Self {
73 inner: Rc::new(TextFieldHandleControllerInner {
74 metrics: Cell::new(None),
75 revision: mutableStateOf(0u64),
76 }),
77 }
78 }
79
80 pub(crate) fn publish(&self, metrics: TextFieldHandleMetrics) {
83 if self.inner.metrics.get() != Some(metrics) {
84 self.inner.metrics.set(Some(metrics));
85 self.inner
86 .revision
87 .update(|value| *value = value.wrapping_add(1));
88 }
89 }
90
91 pub fn metrics(&self) -> Option<TextFieldHandleMetrics> {
94 let _ = self.inner.revision.value();
95 self.inner.metrics.get()
96 }
97}
98
99impl Default for TextFieldHandleController {
100 fn default() -> Self {
101 Self::new()
102 }
103}
104
105const DEFAULT_CURSOR_COLOR: Color = Color(1.0, 1.0, 1.0, 1.0);
107
108const DEFAULT_SELECTION_COLOR: Color = Color(0.0, 0.5, 1.0, 0.3);
110
111const DEFAULT_LINE_HEIGHT: f32 = 20.0;
113
114const CURSOR_WIDTH: f32 = 2.0;
116
117pub(crate) fn compute_horizontal_scroll_offset(
128 current_offset: f32,
129 cursor_x: f32,
130 text_width: f32,
131 viewport_width: f32,
132) -> f32 {
133 if viewport_width <= 0.0 {
134 return 0.0;
135 }
136 let max_offset = (text_width + CURSOR_WIDTH - viewport_width).max(0.0);
137 let mut offset = current_offset.clamp(0.0, max_offset);
138 let visible_end = offset + viewport_width - CURSOR_WIDTH;
139 if cursor_x > visible_end {
140 offset = cursor_x - viewport_width + CURSOR_WIDTH;
142 } else if cursor_x < offset {
143 offset = cursor_x;
145 }
146 offset.clamp(0.0, max_offset)
147}
148
149pub(crate) fn intersect_rect(
154 rect: cranpose_ui_graphics::Rect,
155 bounds: cranpose_ui_graphics::Rect,
156) -> Option<cranpose_ui_graphics::Rect> {
157 let x0 = rect.x.max(bounds.x);
158 let y0 = rect.y.max(bounds.y);
159 let x1 = (rect.x + rect.width).min(bounds.x + bounds.width);
160 let y1 = (rect.y + rect.height).min(bounds.y + bounds.height);
161 (x1 > x0 && y1 > y0).then_some(cranpose_ui_graphics::Rect {
162 x: x0,
163 y: y0,
164 width: x1 - x0,
165 height: y1 - y0,
166 })
167}
168
169pub type TextPanResolver = Rc<dyn Fn(f32) -> f32>;
172
173#[derive(Clone)]
179pub(crate) struct TextFieldRefs {
180 pub is_focused: Rc<RefCell<bool>>,
182 pub content_offset: Rc<Cell<f32>>,
184 pub content_y_offset: Rc<Cell<f32>>,
186 pub drag_anchor: Rc<Cell<Option<usize>>>,
188 pub last_click_time: Rc<Cell<Option<web_time::Instant>>>,
190 pub last_click_pos: Rc<Cell<Option<(f32, f32)>>>,
192 pub click_count: Rc<Cell<u8>>,
194 pub node_id: Rc<Cell<Option<cranpose_core::NodeId>>>,
196 pub scroll_offset: Rc<Cell<f32>>,
199 pub last_pointer_source: Rc<Cell<PointerSource>>,
203 pub node_origin: Rc<Cell<Point>>,
207}
208
209impl TextFieldRefs {
210 pub fn new() -> Self {
212 Self {
213 is_focused: Rc::new(RefCell::new(false)),
214 content_offset: Rc::new(Cell::new(0.0_f32)),
215 content_y_offset: Rc::new(Cell::new(0.0_f32)),
216 drag_anchor: Rc::new(Cell::new(None::<usize>)),
217 last_click_time: Rc::new(Cell::new(None::<web_time::Instant>)),
218 last_click_pos: Rc::new(Cell::new(None::<(f32, f32)>)),
219 click_count: Rc::new(Cell::new(0_u8)),
220 node_id: Rc::new(Cell::new(None::<cranpose_core::NodeId>)),
221 scroll_offset: Rc::new(Cell::new(0.0_f32)),
222 last_pointer_source: Rc::new(Cell::new(PointerSource::Unknown)),
223 node_origin: Rc::new(Cell::new(Point { x: 0.0, y: 0.0 })),
224 }
225 }
226}
227
228use crate::text::TextStyle; pub struct TextFieldModifierNode {
237 state: TextFieldState,
239 refs: TextFieldRefs,
241 style: TextStyle, cursor_brush: Brush,
245 selection_brush: Brush,
247 line_limits: TextFieldLineLimits,
249 cached_text: String,
251 cached_selection: TextRange,
253 node_state: NodeState,
255 measured_size: Rc<Cell<Size>>,
257 measured_line_height: Rc<Cell<f32>>,
259 cached_handler: Rc<dyn Fn(PointerEvent)>,
261 cached_pan_resolver: TextPanResolver,
263 handle_controller: Option<TextFieldHandleController>,
267}
268
269impl std::fmt::Debug for TextFieldModifierNode {
270 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271 f.debug_struct("TextFieldModifierNode")
272 .field("text", &self.state.text())
273 .field("style", &self.style)
274 .field("is_focused", &*self.refs.is_focused.borrow())
275 .finish()
276 }
277}
278
279use crate::text_field_handler::TextFieldHandler;
281
282impl TextFieldModifierNode {
283 pub fn new(state: TextFieldState, style: TextStyle) -> Self {
285 let value = state.value();
286 let refs = TextFieldRefs::new();
287 let line_limits = TextFieldLineLimits::default();
288 let cached_handler =
289 Self::create_handler(state.clone(), refs.clone(), line_limits, style.clone());
290 let cached_pan_resolver =
291 Self::create_pan_resolver(state.clone(), refs.clone(), line_limits, style.clone());
292
293 Self {
294 state,
295 refs,
296 style,
297 cursor_brush: Brush::solid(DEFAULT_CURSOR_COLOR),
298 selection_brush: Brush::solid(DEFAULT_SELECTION_COLOR),
299 line_limits,
300 cached_text: value.text,
301 cached_selection: value.selection,
302 node_state: NodeState::new(),
303 measured_size: Rc::new(Cell::new(Size {
304 width: 0.0,
305 height: 0.0,
306 })),
307 measured_line_height: Rc::new(Cell::new(DEFAULT_LINE_HEIGHT)),
308 cached_handler,
309 cached_pan_resolver,
310 handle_controller: None,
311 }
312 }
313
314 pub fn with_line_limits(mut self, line_limits: TextFieldLineLimits) -> Self {
316 self.line_limits = line_limits;
317 self.cached_pan_resolver = Self::create_pan_resolver(
318 self.state.clone(),
319 self.refs.clone(),
320 line_limits,
321 self.style.clone(),
322 );
323 self
324 }
325
326 pub fn with_handle_controller(mut self, controller: TextFieldHandleController) -> Self {
328 self.handle_controller = Some(controller);
329 self
330 }
331
332 fn create_pan_resolver(
340 state: TextFieldState,
341 refs: TextFieldRefs,
342 line_limits: TextFieldLineLimits,
343 style: TextStyle,
344 ) -> TextPanResolver {
345 Rc::new(move |viewport_width: f32| {
346 if !line_limits.is_single_line() {
347 refs.scroll_offset.set(0.0);
349 return 0.0;
350 }
351 let text = state.text();
352 let pos = state.selection().start.min(text.len());
353 let text_width = crate::text::measure_text(
354 &crate::text::AnnotatedString::from(text.as_str()),
355 &style,
356 )
357 .width;
358 let cursor_x = crate::text::measure_text(
359 &crate::text::AnnotatedString::from(&text[..pos]),
360 &style,
361 )
362 .width;
363 let offset = compute_horizontal_scroll_offset(
364 refs.scroll_offset.get(),
365 cursor_x,
366 text_width,
367 viewport_width,
368 );
369 refs.scroll_offset.set(offset);
370 offset
371 })
372 }
373
374 pub fn text_pan_resolver(&self) -> Option<TextPanResolver> {
379 self.line_limits
380 .is_single_line()
381 .then(|| self.cached_pan_resolver.clone())
382 }
383
384 pub fn scroll_offset(&self) -> f32 {
386 self.refs.scroll_offset.get()
387 }
388
389 pub fn line_limits(&self) -> TextFieldLineLimits {
391 self.line_limits
392 }
393
394 fn create_handler(
396 state: TextFieldState,
397 refs: TextFieldRefs,
398 line_limits: TextFieldLineLimits,
399 style: TextStyle, ) -> Rc<dyn Fn(PointerEvent)> {
401 use crate::text_selection::{
404 classify_tap, find_line_boundaries, TapCount, MULTI_TAP_SLOP_PX, MULTI_TAP_TIMEOUT_MS,
405 };
406 use crate::word_boundaries::find_word_boundaries;
407
408 Rc::new(move |event: PointerEvent| {
409 refs.node_origin.set(Point {
413 x: event.global_position.x - event.position.x,
414 y: event.global_position.y - event.position.y,
415 });
416
417 let click_x =
421 (event.position.x - refs.content_offset.get() + refs.scroll_offset.get()).max(0.0);
422 let click_y = (event.position.y - refs.content_y_offset.get()).max(0.0);
423
424 match event.kind {
425 PointerEventKind::Down => {
426 refs.last_pointer_source.set(event.source);
430
431 let handler =
433 TextFieldHandler::new(state.clone(), refs.node_id.get(), line_limits);
434 crate::text_field_focus::request_focus(refs.is_focused.clone(), handler);
435
436 let now = web_time::Instant::now();
437 let text = state.text();
438 let pos = crate::text::get_offset_for_position(
439 &crate::text::AnnotatedString::from(text.as_str()),
440 &style,
441 click_x,
442 click_y,
443 );
444
445 let previous = refs.click_count.get().try_into().ok().and_then(|count| {
450 let (px, py) = refs.last_click_pos.get()?;
451 Some((count, px, py))
452 });
453 let elapsed_ms = refs
454 .last_click_time
455 .get()
456 .map(|last| now.duration_since(last).as_millis())
457 .unwrap_or(u128::MAX);
458 let tap = classify_tap(
459 previous,
460 elapsed_ms,
461 event.position.x,
462 event.position.y,
463 MULTI_TAP_TIMEOUT_MS,
464 MULTI_TAP_SLOP_PX,
465 );
466
467 match tap {
468 TapCount::Triple => {
469 let (line_start, line_end) = find_line_boundaries(&text, pos);
471 state.edit(|buffer| {
472 buffer.select(TextRange::new(line_start, line_end));
473 });
474 refs.drag_anchor.set(Some(line_start));
475 }
476 TapCount::Double => {
477 let (word_start, word_end) = find_word_boundaries(&text, pos);
479 state.edit(|buffer| {
480 buffer.select(TextRange::new(word_start, word_end));
481 });
482 refs.drag_anchor.set(Some(word_start));
483 }
484 TapCount::Single => {
485 refs.drag_anchor.set(Some(pos));
487 state.edit(|buffer| {
488 buffer.place_cursor_before_char(pos);
489 });
490 }
491 }
492
493 refs.click_count.set(tap.as_u8());
494 refs.last_click_time.set(Some(now));
495 refs.last_click_pos
496 .set(Some((event.position.x, event.position.y)));
497 event.consume();
498 }
499 PointerEventKind::Move => {
500 if let Some(anchor) = refs.drag_anchor.get() {
502 if *refs.is_focused.borrow() {
503 let text = state.text();
504 let current_pos = crate::text::get_offset_for_position(
505 &crate::text::AnnotatedString::from(text.as_str()),
506 &style,
507 click_x,
508 click_y,
509 );
510
511 state.set_selection(TextRange::new(anchor, current_pos));
513
514 crate::request_render_invalidation();
516
517 event.consume();
518 }
519 }
520 }
521 PointerEventKind::Up => {
522 refs.drag_anchor.set(None);
524 }
525 _ => {}
526 }
527 })
528 }
529
530 pub fn with_cursor_color(mut self, color: Color) -> Self {
532 self.cursor_brush = Brush::solid(color);
533 self
534 }
535
536 pub fn set_focused(&mut self, focused: bool) {
538 let current = *self.refs.is_focused.borrow();
539 if current != focused {
540 *self.refs.is_focused.borrow_mut() = focused;
541 }
542 }
543
544 pub fn is_focused(&self) -> bool {
546 *self.refs.is_focused.borrow()
547 }
548
549 pub fn is_focused_rc(&self) -> Rc<RefCell<bool>> {
551 self.refs.is_focused.clone()
552 }
553
554 pub fn content_offset_rc(&self) -> Rc<Cell<f32>> {
556 self.refs.content_offset.clone()
557 }
558
559 pub fn content_y_offset_rc(&self) -> Rc<Cell<f32>> {
561 self.refs.content_y_offset.clone()
562 }
563
564 pub fn text(&self) -> String {
566 self.state.text()
567 }
568
569 pub fn style(&self) -> &TextStyle {
570 &self.style
571 }
572
573 pub fn selection(&self) -> TextRange {
575 self.state.selection()
576 }
577
578 pub fn cursor_brush(&self) -> Brush {
580 self.cursor_brush.clone()
581 }
582
583 pub fn selection_brush(&self) -> Brush {
585 self.selection_brush.clone()
586 }
587
588 pub fn insert_text(&mut self, text: &str) {
590 self.state.edit(|buffer| {
591 buffer.insert(text);
592 });
593 }
594
595 pub fn copy_selection(&self) -> Option<String> {
598 self.state.copy_selection()
599 }
600
601 pub fn cut_selection(&mut self) -> Option<String> {
604 let text = self.copy_selection();
605 if text.is_some() {
606 self.state.edit(|buffer| {
607 buffer.delete(buffer.selection());
608 });
609 }
610 text
611 }
612
613 pub fn get_state(&self) -> cranpose_foundation::text::TextFieldState {
616 self.state.clone()
617 }
618
619 pub fn set_content_offset(&self, offset: f32) {
622 self.refs.content_offset.set(offset);
623 }
624
625 pub fn set_content_y_offset(&self, offset: f32) {
628 self.refs.content_y_offset.set(offset);
629 }
630
631 fn wrap_width(&self, available_width: f32) -> Option<f32> {
638 (!self.line_limits.is_single_line() && available_width.is_finite() && available_width > 0.0)
639 .then_some(available_width)
640 }
641
642 fn measure_text_content(&self, wrap_width: Option<f32>) -> Size {
648 let text = self.state.text();
649 let node_id = self.refs.node_id.get();
650 let annotated = crate::text::AnnotatedString::from(text.as_str());
651 let metrics = match wrap_width {
652 Some(max_width) => crate::text::measure_text_with_options_for_node(
653 node_id,
654 &annotated,
655 &self.style,
656 crate::text::TextLayoutOptions::default(),
657 Some(max_width),
658 ),
659 None => crate::text::measure_text_for_node(node_id, &annotated, &self.style),
660 };
661 self.measured_line_height.set(metrics.line_height);
662 Size {
663 width: metrics.width,
664 height: metrics.height,
665 }
666 }
667
668 fn update_cached_state(&mut self) -> bool {
670 let value = self.state.value();
671 let text_changed = value.text != self.cached_text;
672 let selection_changed = value.selection != self.cached_selection;
673
674 if text_changed {
675 self.cached_text = value.text;
676 }
677 if selection_changed {
678 self.cached_selection = value.selection;
679 }
680
681 text_changed || selection_changed
682 }
683
684 pub fn position_cursor_at_offset(&self, x_offset: f32) {
687 let text = self.state.text();
688 if text.is_empty() {
689 self.state.edit(|buffer| {
690 buffer.place_cursor_at_start();
691 });
692 return;
693 }
694
695 let byte_offset = crate::text::get_offset_for_position(
698 &crate::text::AnnotatedString::from(text.as_str()),
699 &self.style,
700 x_offset + self.refs.scroll_offset.get(),
701 0.0,
702 );
703
704 self.state.edit(|buffer| {
705 buffer.place_cursor_before_char(byte_offset);
706 });
707 }
708
709 }
713
714impl DelegatableNode for TextFieldModifierNode {
715 fn node_state(&self) -> &NodeState {
716 &self.node_state
717 }
718}
719
720impl ModifierNode for TextFieldModifierNode {
721 fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
722 self.refs.node_id.set(context.node_id());
724
725 context.invalidate(InvalidationKind::Layout);
726 context.invalidate(InvalidationKind::Draw);
727 context.invalidate(InvalidationKind::Semantics);
728 }
729
730 fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
731 Some(self)
732 }
733
734 fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
735 Some(self)
736 }
737
738 fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
739 Some(self)
740 }
741
742 fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
743 Some(self)
744 }
745
746 fn as_semantics_node(&self) -> Option<&dyn SemanticsNode> {
747 Some(self)
748 }
749
750 fn as_semantics_node_mut(&mut self) -> Option<&mut dyn SemanticsNode> {
751 Some(self)
752 }
753
754 fn as_pointer_input_node(&self) -> Option<&dyn PointerInputNode> {
755 Some(self)
756 }
757
758 fn as_pointer_input_node_mut(&mut self) -> Option<&mut dyn PointerInputNode> {
759 Some(self)
760 }
761}
762
763impl LayoutModifierNode for TextFieldModifierNode {
764 fn measure(
765 &self,
766 _context: &mut dyn ModifierNodeContext,
767 _measurable: &dyn Measurable,
768 constraints: Constraints,
769 ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
770 let text_size = self.measure_text_content(self.wrap_width(constraints.max_width));
774
775 let min_height = if text_size.height < 1.0 {
777 DEFAULT_LINE_HEIGHT
778 } else {
779 text_size.height
780 };
781
782 let width = text_size
784 .width
785 .max(constraints.min_width)
786 .min(constraints.max_width);
787 let height = min_height
788 .max(constraints.min_height)
789 .min(constraints.max_height);
790
791 let size = Size { width, height };
792 self.measured_size.set(size);
793
794 let _ = (self.cached_pan_resolver)(size.width);
797
798 cranpose_ui_layout::LayoutModifierMeasureResult::with_size(size)
799 }
800
801 fn min_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
802 self.measure_text_content(None).width
803 }
804
805 fn max_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
806 self.measure_text_content(None).width
807 }
808
809 fn min_intrinsic_height(&self, _measurable: &dyn Measurable, width: f32) -> f32 {
810 self.measure_text_content(self.wrap_width(width))
811 .height
812 .max(DEFAULT_LINE_HEIGHT)
813 }
814
815 fn max_intrinsic_height(&self, _measurable: &dyn Measurable, width: f32) -> f32 {
816 self.measure_text_content(self.wrap_width(width))
817 .height
818 .max(DEFAULT_LINE_HEIGHT)
819 }
820}
821
822impl DrawModifierNode for TextFieldModifierNode {
823 fn draw(&self, _draw_scope: &mut dyn DrawScope) {
824 }
828
829 fn create_draw_closure(
830 &self,
831 ) -> Option<Rc<dyn Fn(cranpose_foundation::Size) -> Vec<cranpose_ui_graphics::DrawPrimitive>>>
832 {
833 use cranpose_ui_graphics::DrawPrimitive;
834
835 let is_focused = self.refs.is_focused.clone();
837 let state = self.state.clone();
838 let content_offset = self.refs.content_offset.clone();
839 let content_y_offset = self.refs.content_y_offset.clone();
840 let cursor_brush = self.cursor_brush.clone();
841 let selection_brush = self.selection_brush.clone();
842 let style = self.style.clone();
843 let cached_line_height = self.measured_line_height.clone();
844 let measured_size = self.measured_size.clone();
845 let pan_resolver = self.cached_pan_resolver.clone();
846 let handle_controller = self.handle_controller.clone();
847 let node_origin = self.refs.node_origin.clone();
848 let last_pointer_source = self.refs.last_pointer_source.clone();
849
850 Some(Rc::new(move |size| {
851 if !*is_focused.borrow() {
853 if let Some(controller) = &handle_controller {
856 controller.publish(TextFieldHandleMetrics {
857 focused: false,
858 touch: false,
859 node_origin: node_origin.get(),
860 padding_left: 0.0,
861 padding_top: 0.0,
862 scroll_offset: 0.0,
863 line_height: cached_line_height.get(),
864 });
865 }
866 return vec![];
867 }
868
869 let mut primitives = Vec::new();
870
871 let text = state.text();
872 let selection = state.selection();
873 let padding_left = content_offset.get();
874 let padding_top = content_y_offset.get();
875 let line_height = cached_line_height.get();
878
879 let measured = measured_size.get();
882 let viewport_width = if measured.width > 0.0 {
883 measured.width
884 } else {
885 (size.width - padding_left).max(0.0)
886 };
887 let viewport_height = if measured.height > 0.0 {
888 measured.height
889 } else {
890 (size.height - padding_top).max(0.0)
891 };
892 let pan = pan_resolver(viewport_width);
894
895 if let Some(controller) = &handle_controller {
898 controller.publish(TextFieldHandleMetrics {
899 focused: true,
900 touch: last_pointer_source.get().is_touch_like(),
901 node_origin: node_origin.get(),
902 padding_left,
903 padding_top,
904 scroll_offset: pan,
905 line_height,
906 });
907 }
908 let clip_bounds = cranpose_ui_graphics::Rect {
912 x: padding_left,
913 y: padding_top,
914 width: viewport_width,
915 height: viewport_height,
916 };
917
918 if !selection.collapsed() {
920 let sel_start = selection.min();
921 let sel_end = selection.max();
922
923 let lines: Vec<&str> = text.split('\n').collect();
924 let mut byte_offset: usize = 0;
925
926 for (line_idx, line) in lines.iter().enumerate() {
927 let line_start = byte_offset;
928 let line_end = byte_offset + line.len();
929
930 if sel_end > line_start && sel_start < line_end {
931 let sel_start_in_line = sel_start.saturating_sub(line_start);
932 let sel_end_in_line = (sel_end - line_start).min(line.len());
933
934 let sel_start_x = crate::text::measure_text(
935 &crate::text::AnnotatedString::from(&line[..sel_start_in_line]),
936 &style,
937 )
938 .width
939 + padding_left
940 - pan;
941 let sel_end_x = crate::text::measure_text(
942 &crate::text::AnnotatedString::from(&line[..sel_end_in_line]),
943 &style,
944 )
945 .width
946 + padding_left
947 - pan;
948 let sel_width = sel_end_x - sel_start_x;
949
950 if sel_width > 0.0 {
951 let sel_rect = cranpose_ui_graphics::Rect {
952 x: sel_start_x,
953 y: padding_top + line_idx as f32 * line_height,
954 width: sel_width,
955 height: line_height,
956 };
957 if let Some(clipped) = intersect_rect(sel_rect, clip_bounds) {
958 primitives.push(DrawPrimitive::Rect {
959 rect: clipped,
960 brush: selection_brush.clone(),
961 });
962 }
963 }
964 }
965 byte_offset = line_end + 1;
966 }
967 }
968
969 if let Some(comp_range) = state.composition() {
972 let comp_start = comp_range.min();
973 let comp_end = comp_range.max();
974
975 if comp_start < comp_end && comp_end <= text.len() {
976 let lines: Vec<&str> = text.split('\n').collect();
977 let mut byte_offset: usize = 0;
978
979 let underline_brush = cranpose_ui_graphics::Brush::solid(
981 cranpose_ui_graphics::Color(0.8, 0.8, 0.8, 0.8),
982 );
983 let underline_height: f32 = 2.0;
984
985 for (line_idx, line) in lines.iter().enumerate() {
986 let line_start = byte_offset;
987 let line_end = byte_offset + line.len();
988
989 if comp_end > line_start && comp_start < line_end {
991 let comp_start_in_line = comp_start.saturating_sub(line_start);
992 let comp_end_in_line = (comp_end - line_start).min(line.len());
993
994 let comp_start_in_line = if line.is_char_boundary(comp_start_in_line) {
996 comp_start_in_line
997 } else {
998 0
999 };
1000 let comp_end_in_line = if line.is_char_boundary(comp_end_in_line) {
1001 comp_end_in_line
1002 } else {
1003 line.len()
1004 };
1005
1006 let comp_start_x = crate::text::measure_text(
1007 &crate::text::AnnotatedString::from(&line[..comp_start_in_line]),
1008 &style,
1009 )
1010 .width
1011 + padding_left
1012 - pan;
1013 let comp_end_x = crate::text::measure_text(
1014 &crate::text::AnnotatedString::from(&line[..comp_end_in_line]),
1015 &style,
1016 )
1017 .width
1018 + padding_left
1019 - pan;
1020 let comp_width = comp_end_x - comp_start_x;
1021
1022 if comp_width > 0.0 {
1023 let underline_rect = cranpose_ui_graphics::Rect {
1025 x: comp_start_x,
1026 y: padding_top + (line_idx as f32 + 1.0) * line_height
1027 - underline_height,
1028 width: comp_width,
1029 height: underline_height,
1030 };
1031 if let Some(clipped) = intersect_rect(underline_rect, clip_bounds) {
1032 primitives.push(DrawPrimitive::Rect {
1033 rect: clipped,
1034 brush: underline_brush.clone(),
1035 });
1036 }
1037 }
1038 }
1039 byte_offset = line_end + 1;
1040 }
1041 }
1042 }
1043
1044 if crate::cursor_animation::is_cursor_visible() {
1046 let pos = selection.start.min(text.len());
1047 let text_before = &text[..pos];
1048 let line_index = text_before.matches('\n').count();
1049 let line_start = text_before.rfind('\n').map(|i| i + 1).unwrap_or(0);
1050 let cursor_x = crate::text::measure_text(
1051 &crate::text::AnnotatedString::from(&text_before[line_start..]),
1052 &style,
1053 )
1054 .width
1055 + padding_left
1056 - pan;
1057 let cursor_y = padding_top + line_index as f32 * line_height;
1058
1059 let cursor_rect = cranpose_ui_graphics::Rect {
1060 x: cursor_x,
1061 y: cursor_y,
1062 width: CURSOR_WIDTH,
1063 height: line_height,
1064 };
1065
1066 if let Some(clipped) = intersect_rect(cursor_rect, clip_bounds) {
1067 primitives.push(DrawPrimitive::Rect {
1068 rect: clipped,
1069 brush: cursor_brush.clone(),
1070 });
1071 }
1072 }
1073
1074 primitives
1075 }))
1076 }
1077}
1078
1079impl SemanticsNode for TextFieldModifierNode {
1080 fn merge_semantics(&self, config: &mut SemanticsConfiguration) {
1081 let text = self.state.text();
1082 config.content_description = Some(text);
1083 config.is_editable_text = true;
1084 config.text_selection = Some(self.state.selection());
1085 }
1086}
1087
1088impl PointerInputNode for TextFieldModifierNode {
1089 fn on_pointer_event(
1090 &mut self,
1091 _context: &mut dyn ModifierNodeContext,
1092 _event: &PointerEvent,
1093 ) -> bool {
1094 false
1105 }
1106
1107 fn hit_test(&self, x: f32, y: f32) -> bool {
1108 let size = self.measured_size.get();
1110 x >= 0.0 && x <= size.width && y >= 0.0 && y <= size.height
1111 }
1112
1113 fn pointer_input_handler(&self) -> Option<Rc<dyn Fn(PointerEvent)>> {
1114 Some(self.cached_handler.clone())
1116 }
1117}
1118
1119#[derive(Clone)]
1130pub struct TextFieldElement {
1131 state: TextFieldState,
1133 style: TextStyle,
1135 cursor_color: Color,
1137 line_limits: TextFieldLineLimits,
1139 handle_controller: Option<TextFieldHandleController>,
1142}
1143
1144impl TextFieldElement {
1145 pub fn new(state: TextFieldState, style: TextStyle) -> Self {
1147 Self {
1148 state,
1149 style,
1150 cursor_color: DEFAULT_CURSOR_COLOR,
1151 line_limits: TextFieldLineLimits::default(),
1152 handle_controller: None,
1153 }
1154 }
1155
1156 pub fn with_cursor_color(mut self, color: Color) -> Self {
1158 self.cursor_color = color;
1159 self
1160 }
1161
1162 pub fn with_line_limits(mut self, line_limits: TextFieldLineLimits) -> Self {
1164 self.line_limits = line_limits;
1165 self
1166 }
1167
1168 pub fn with_handle_controller(mut self, controller: TextFieldHandleController) -> Self {
1170 self.handle_controller = Some(controller);
1171 self
1172 }
1173}
1174
1175impl std::fmt::Debug for TextFieldElement {
1176 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1177 f.debug_struct("TextFieldElement")
1178 .field("text", &self.state.text())
1179 .field("style", &self.style)
1180 .field("cursor_color", &self.cursor_color)
1181 .finish()
1182 }
1183}
1184
1185impl Hash for TextFieldElement {
1186 fn hash<H: Hasher>(&self, state: &mut H) {
1187 std::ptr::hash(std::rc::Rc::as_ptr(&self.state.inner), state);
1190 self.cursor_color.0.to_bits().hash(state);
1192 self.cursor_color.1.to_bits().hash(state);
1193 self.cursor_color.2.to_bits().hash(state);
1194 self.cursor_color.3.to_bits().hash(state);
1195 self.style.render_hash().hash(state);
1196 self.line_limits.hash(state);
1197 }
1198}
1199
1200impl PartialEq for TextFieldElement {
1201 fn eq(&self, other: &Self) -> bool {
1202 self.state == other.state
1206 && self.style == other.style
1207 && self.cursor_color == other.cursor_color
1208 && self.line_limits == other.line_limits
1209 }
1210}
1211
1212impl Eq for TextFieldElement {}
1213
1214impl ModifierNodeElement for TextFieldElement {
1215 type Node = TextFieldModifierNode;
1216
1217 fn create(&self) -> Self::Node {
1218 let mut node = TextFieldModifierNode::new(self.state.clone(), self.style.clone())
1219 .with_cursor_color(self.cursor_color)
1220 .with_line_limits(self.line_limits);
1221 if let Some(controller) = self.handle_controller.clone() {
1222 node = node.with_handle_controller(controller);
1223 }
1224 node
1225 }
1226
1227 fn update(&self, node: &mut Self::Node) {
1228 node.state = self.state.clone();
1230 node.style = self.style.clone();
1231 node.cursor_brush = Brush::solid(self.cursor_color);
1232 node.line_limits = self.line_limits;
1233 node.handle_controller = self.handle_controller.clone();
1234
1235 node.cached_handler = TextFieldModifierNode::create_handler(
1237 node.state.clone(),
1238 node.refs.clone(),
1239 node.line_limits,
1240 self.style.clone(),
1241 );
1242
1243 node.cached_pan_resolver = TextFieldModifierNode::create_pan_resolver(
1245 node.state.clone(),
1246 node.refs.clone(),
1247 node.line_limits,
1248 self.style.clone(),
1249 );
1250
1251 if node.update_cached_state() {
1253 }
1256 }
1257
1258 fn capabilities(&self) -> NodeCapabilities {
1259 NodeCapabilities::LAYOUT
1260 | NodeCapabilities::DRAW
1261 | NodeCapabilities::SEMANTICS
1262 | NodeCapabilities::POINTER_INPUT
1263 }
1264
1265 fn always_update(&self) -> bool {
1266 true
1268 }
1269}
1270
1271#[cfg(test)]
1272mod tests {
1273 use super::*;
1274 use crate::text::TextStyle;
1275 use cranpose_core::{DefaultScheduler, Runtime};
1276 use std::sync::Arc;
1277
1278 fn with_test_runtime<T>(f: impl FnOnce() -> T) -> T {
1280 let _runtime = Runtime::new(Arc::new(DefaultScheduler));
1281 f()
1282 }
1283
1284 #[test]
1285 fn text_field_node_creation() {
1286 let _app_context = crate::render_state::app_context_test_scope();
1287 with_test_runtime(|| {
1288 let state = TextFieldState::new("Hello");
1289 let node = TextFieldModifierNode::new(state, TextStyle::default());
1290 assert_eq!(node.text(), "Hello");
1291 assert!(!node.is_focused());
1292 });
1293 }
1294
1295 #[test]
1296 fn text_field_node_focus() {
1297 let _app_context = crate::render_state::app_context_test_scope();
1298 with_test_runtime(|| {
1299 let state = TextFieldState::new("Test");
1300 let mut node = TextFieldModifierNode::new(state, TextStyle::default());
1301 assert!(!node.is_focused());
1302
1303 node.set_focused(true);
1304 assert!(node.is_focused());
1305
1306 node.set_focused(false);
1307 assert!(!node.is_focused());
1308 });
1309 }
1310
1311 #[test]
1312 fn text_field_element_creates_node() {
1313 let _app_context = crate::render_state::app_context_test_scope();
1314 with_test_runtime(|| {
1315 let state = TextFieldState::new("Hello World");
1316 let element = TextFieldElement::new(state, TextStyle::default());
1317
1318 let node = element.create();
1319 assert_eq!(node.text(), "Hello World");
1320 });
1321 }
1322
1323 #[test]
1332 fn touch_press_publishes_touch_handle_metrics_but_mouse_does_not() {
1333 use cranpose_foundation::{PointerEvent, PointerEventKind, PointerSource};
1334 use cranpose_ui_graphics::Point;
1335
1336 let _app_context = crate::render_state::app_context_test_scope();
1337 with_test_runtime(|| {
1338 let state = TextFieldState::new("hello world");
1339 let controller = TextFieldHandleController::new();
1340 let node = TextFieldModifierNode::new(state.clone(), TextStyle::default())
1341 .with_handle_controller(controller.clone());
1342 node.measured_size.set(Size {
1344 width: 120.0,
1345 height: 20.0,
1346 });
1347
1348 let handler = node
1349 .pointer_input_handler()
1350 .expect("field exposes a pointer handler");
1351 let draw = node
1352 .create_draw_closure()
1353 .expect("field exposes a draw closure");
1354 let at = Point { x: 12.0, y: 8.0 };
1355 let size = Size {
1356 width: 120.0,
1357 height: 20.0,
1358 };
1359
1360 handler(
1363 PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Touch),
1364 );
1365 let _ = draw(size);
1366 let metrics = controller
1367 .metrics()
1368 .expect("focused field publishes handle metrics");
1369 assert!(metrics.focused, "a tap focuses the field");
1370 assert!(
1371 metrics.touch,
1372 "a touch tap must publish touch = true so the finger handles show"
1373 );
1374
1375 handler(
1378 PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Mouse),
1379 );
1380 let _ = draw(size);
1381 let metrics = controller
1382 .metrics()
1383 .expect("focused field publishes handle metrics");
1384 assert!(
1385 !metrics.touch,
1386 "a mouse tap must publish touch = false (clean caret, no finger handle)"
1387 );
1388
1389 crate::text_field_focus::clear_focus();
1390 });
1391 }
1392
1393 #[test]
1394 fn text_field_element_equality() {
1395 let _app_context = crate::render_state::app_context_test_scope();
1396 with_test_runtime(|| {
1397 let state1 = TextFieldState::new("Hello");
1398 let state2 = TextFieldState::new("Hello"); let elem1 = TextFieldElement::new(state1.clone(), TextStyle::default());
1401 let elem2 = TextFieldElement::new(state1.clone(), TextStyle::default()); let elem3 = TextFieldElement::new(state2, TextStyle::default()); assert_eq!(elem1, elem2, "Same state should be equal");
1407 assert_ne!(elem1, elem3, "Different states should not be equal");
1408 });
1409 }
1410
1411 #[test]
1412 fn text_field_element_update_refreshes_existing_node_style() {
1413 let _app_context = crate::render_state::app_context_test_scope();
1414 with_test_runtime(|| {
1415 let state = TextFieldState::new("themed text");
1416 let dark_style = TextStyle::from_span_style(crate::text::SpanStyle {
1417 color: Some(Color::from_rgba_u8(228, 240, 252, 255)),
1418 ..crate::text::SpanStyle::default()
1419 });
1420 let light_style = TextStyle::from_span_style(crate::text::SpanStyle {
1421 color: Some(Color::from_rgba_u8(14, 58, 96, 255)),
1422 ..crate::text::SpanStyle::default()
1423 });
1424 let initial = TextFieldElement::new(state.clone(), dark_style);
1425 let updated = TextFieldElement::new(state, light_style.clone());
1426 let mut node = initial.create();
1427
1428 updated.update(&mut node);
1429
1430 assert_eq!(node.text(), "themed text");
1431 assert_eq!(node.style(), &light_style);
1432 });
1433 }
1434
1435 #[test]
1440 fn multiline_field_measures_wrapped_height() {
1441 let _app_context = crate::render_state::app_context_test_scope();
1442 with_test_runtime(|| {
1443 let long = "abcd ".repeat(40); let state = TextFieldState::new(&long);
1445 let node = TextFieldModifierNode::new(state, TextStyle::default());
1446 assert!(
1447 !node.line_limits().is_single_line(),
1448 "default fields are multi-line"
1449 );
1450
1451 let natural = node.measure_text_content(None);
1452 let wrapped = node.measure_text_content(node.wrap_width(20.0));
1453
1454 assert!(
1455 wrapped.height > natural.height,
1456 "wrapped multi-line height {} must exceed the single-line height {}",
1457 wrapped.height,
1458 natural.height
1459 );
1460 });
1461 }
1462
1463 #[test]
1466 fn single_line_field_never_wraps() {
1467 let _app_context = crate::render_state::app_context_test_scope();
1468 with_test_runtime(|| {
1469 let state = TextFieldState::new("abcd ".repeat(40));
1470 let node = TextFieldModifierNode::new(state, TextStyle::default())
1471 .with_line_limits(TextFieldLineLimits::SingleLine);
1472 assert_eq!(
1473 node.wrap_width(20.0),
1474 None,
1475 "single-line fields must not wrap"
1476 );
1477 });
1478 }
1479
1480 #[test]
1486 fn test_cursor_x_position_calculation() {
1487 let _app_context = crate::render_state::app_context_test_scope();
1488 with_test_runtime(|| {
1489 let style = crate::text::TextStyle::default();
1491
1492 let empty_width =
1494 crate::text::measure_text(&crate::text::AnnotatedString::from(""), &style).width;
1495 assert!(
1496 empty_width.abs() < 0.1,
1497 "Empty text should have 0 width, got {}",
1498 empty_width
1499 );
1500
1501 let hi_width =
1503 crate::text::measure_text(&crate::text::AnnotatedString::from("Hi"), &style).width;
1504 assert!(
1505 hi_width > 0.0,
1506 "Text 'Hi' should have positive width: {}",
1507 hi_width
1508 );
1509
1510 let h_width =
1512 crate::text::measure_text(&crate::text::AnnotatedString::from("H"), &style).width;
1513 assert!(h_width > 0.0, "Text 'H' should have positive width");
1514 assert!(
1515 h_width < hi_width,
1516 "'H' width {} should be less than 'Hi' width {}",
1517 h_width,
1518 hi_width
1519 );
1520
1521 let state = TextFieldState::new("Hi");
1523 assert_eq!(
1524 state.selection().start,
1525 2,
1526 "Cursor should be at position 2 (end of 'Hi')"
1527 );
1528
1529 let text = state.text();
1531 let cursor_pos = state.selection().start;
1532 let text_before_cursor = &text[..cursor_pos.min(text.len())];
1533 assert_eq!(text_before_cursor, "Hi");
1534
1535 let cursor_x = crate::text::measure_text(
1537 &crate::text::AnnotatedString::from(text_before_cursor),
1538 &style,
1539 )
1540 .width;
1541 assert!(
1542 (cursor_x - hi_width).abs() < 0.1,
1543 "Cursor x {} should equal 'Hi' width {}",
1544 cursor_x,
1545 hi_width
1546 );
1547 });
1548 }
1549
1550 #[test]
1552 fn test_focused_node_creates_cursor() {
1553 let _app_context = crate::render_state::app_context_test_scope();
1554 with_test_runtime(|| {
1555 let state = TextFieldState::new("Test");
1556 let element = TextFieldElement::new(state.clone(), TextStyle::default());
1557 let node = element.create();
1558
1559 assert!(!node.is_focused());
1561
1562 *node.refs.is_focused.borrow_mut() = true;
1564 assert!(node.is_focused());
1565
1566 assert_eq!(node.text(), "Test");
1568
1569 assert_eq!(node.selection().start, 4);
1571 });
1572 }
1573}