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 {
416 x: event.global_position.x - event.position.x,
417 y: event.global_position.y - event.position.y,
418 });
419
420 let click_x =
424 (event.position.x - refs.content_offset.get() + refs.scroll_offset.get()).max(0.0);
425 let click_y = (event.position.y - refs.content_y_offset.get()).max(0.0);
426
427 match event.kind {
428 PointerEventKind::Down => {
429 refs.last_pointer_source.set(event.source);
433
434 let handler =
436 TextFieldHandler::new(state.clone(), refs.node_id.get(), line_limits);
437 crate::text_field_focus::request_focus(refs.is_focused.clone(), handler);
438
439 let now = web_time::Instant::now();
440 let text = state.text();
441 let pos = crate::text::get_offset_for_position(
442 &crate::text::AnnotatedString::from(text.as_str()),
443 &style,
444 click_x,
445 click_y,
446 );
447
448 let previous = refs.click_count.get().try_into().ok().and_then(|count| {
453 let (px, py) = refs.last_click_pos.get()?;
454 Some((count, px, py))
455 });
456 let elapsed_ms = refs
457 .last_click_time
458 .get()
459 .map(|last| now.duration_since(last).as_millis())
460 .unwrap_or(u128::MAX);
461 let tap = classify_tap(
462 previous,
463 elapsed_ms,
464 event.position.x,
465 event.position.y,
466 MULTI_TAP_TIMEOUT_MS,
467 MULTI_TAP_SLOP_PX,
468 );
469
470 match tap {
471 TapCount::Triple => {
472 let (line_start, line_end) = find_line_boundaries(&text, pos);
474 state.edit(|buffer| {
475 buffer.select(TextRange::new(line_start, line_end));
476 });
477 refs.drag_anchor.set(Some(line_start));
478 }
479 TapCount::Double => {
480 let (word_start, word_end) = find_word_boundaries(&text, pos);
482 state.edit(|buffer| {
483 buffer.select(TextRange::new(word_start, word_end));
484 });
485 refs.drag_anchor.set(Some(word_start));
486 }
487 TapCount::Single => {
488 refs.drag_anchor.set(Some(pos));
490 state.edit(|buffer| {
491 buffer.place_cursor_before_char(pos);
492 });
493 }
494 }
495
496 refs.click_count.set(tap.as_u8());
497 refs.last_click_time.set(Some(now));
498 refs.last_click_pos
499 .set(Some((event.position.x, event.position.y)));
500 event.consume();
501 }
502 PointerEventKind::Move => {
503 if let Some(anchor) = refs.drag_anchor.get() {
505 if *refs.is_focused.borrow() {
506 let text = state.text();
507 let current_pos = crate::text::get_offset_for_position(
508 &crate::text::AnnotatedString::from(text.as_str()),
509 &style,
510 click_x,
511 click_y,
512 );
513
514 state.set_selection(TextRange::new(anchor, current_pos));
516
517 crate::request_render_invalidation();
519
520 event.consume();
521 }
522 }
523 }
524 PointerEventKind::Up => {
525 refs.drag_anchor.set(None);
527 }
528 _ => {}
529 }
530 })
531 }
532
533 pub fn with_cursor_color(mut self, color: Color) -> Self {
535 self.cursor_brush = Brush::solid(color);
536 self
537 }
538
539 pub fn set_focused(&mut self, focused: bool) {
541 let current = *self.refs.is_focused.borrow();
542 if current != focused {
543 *self.refs.is_focused.borrow_mut() = focused;
544 }
545 }
546
547 pub fn is_focused(&self) -> bool {
549 *self.refs.is_focused.borrow()
550 }
551
552 pub fn is_focused_rc(&self) -> Rc<RefCell<bool>> {
554 self.refs.is_focused.clone()
555 }
556
557 pub fn content_offset_rc(&self) -> Rc<Cell<f32>> {
559 self.refs.content_offset.clone()
560 }
561
562 pub fn content_y_offset_rc(&self) -> Rc<Cell<f32>> {
564 self.refs.content_y_offset.clone()
565 }
566
567 pub(crate) fn window_origin_sink(&self) -> Rc<Cell<Point>> {
580 self.refs.node_origin.clone()
581 }
582
583 pub fn text(&self) -> String {
585 self.state.text()
586 }
587
588 pub fn style(&self) -> &TextStyle {
589 &self.style
590 }
591
592 pub fn selection(&self) -> TextRange {
594 self.state.selection()
595 }
596
597 pub fn cursor_brush(&self) -> Brush {
599 self.cursor_brush.clone()
600 }
601
602 pub fn selection_brush(&self) -> Brush {
604 self.selection_brush.clone()
605 }
606
607 pub fn insert_text(&mut self, text: &str) {
609 self.state.edit(|buffer| {
610 buffer.insert(text);
611 });
612 }
613
614 pub fn copy_selection(&self) -> Option<String> {
617 self.state.copy_selection()
618 }
619
620 pub fn cut_selection(&mut self) -> Option<String> {
623 let text = self.copy_selection();
624 if text.is_some() {
625 self.state.edit(|buffer| {
626 buffer.delete(buffer.selection());
627 });
628 }
629 text
630 }
631
632 pub fn get_state(&self) -> cranpose_foundation::text::TextFieldState {
635 self.state.clone()
636 }
637
638 pub fn set_content_offset(&self, offset: f32) {
641 self.refs.content_offset.set(offset);
642 }
643
644 pub fn set_content_y_offset(&self, offset: f32) {
647 self.refs.content_y_offset.set(offset);
648 }
649
650 fn wrap_width(&self, available_width: f32) -> Option<f32> {
657 (!self.line_limits.is_single_line() && available_width.is_finite() && available_width > 0.0)
658 .then_some(available_width)
659 }
660
661 fn measure_text_content(&self, wrap_width: Option<f32>) -> Size {
667 let text = self.state.text();
668 let node_id = self.refs.node_id.get();
669 let annotated = crate::text::AnnotatedString::from(text.as_str());
670 let metrics = match wrap_width {
671 Some(max_width) => crate::text::measure_text_with_options_for_node(
672 node_id,
673 &annotated,
674 &self.style,
675 crate::text::TextLayoutOptions::default(),
676 Some(max_width),
677 ),
678 None => crate::text::measure_text_for_node(node_id, &annotated, &self.style),
679 };
680 self.measured_line_height.set(metrics.line_height);
681 Size {
682 width: metrics.width,
683 height: metrics.height,
684 }
685 }
686
687 fn update_cached_state(&mut self) -> bool {
689 let value = self.state.value();
690 let text_changed = value.text != self.cached_text;
691 let selection_changed = value.selection != self.cached_selection;
692
693 if text_changed {
694 self.cached_text = value.text;
695 }
696 if selection_changed {
697 self.cached_selection = value.selection;
698 }
699
700 text_changed || selection_changed
701 }
702
703 pub fn position_cursor_at_offset(&self, x_offset: f32) {
706 let text = self.state.text();
707 if text.is_empty() {
708 self.state.edit(|buffer| {
709 buffer.place_cursor_at_start();
710 });
711 return;
712 }
713
714 let byte_offset = crate::text::get_offset_for_position(
717 &crate::text::AnnotatedString::from(text.as_str()),
718 &self.style,
719 x_offset + self.refs.scroll_offset.get(),
720 0.0,
721 );
722
723 self.state.edit(|buffer| {
724 buffer.place_cursor_before_char(byte_offset);
725 });
726 }
727
728 }
732
733impl DelegatableNode for TextFieldModifierNode {
734 fn node_state(&self) -> &NodeState {
735 &self.node_state
736 }
737}
738
739impl ModifierNode for TextFieldModifierNode {
740 fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
741 self.refs.node_id.set(context.node_id());
743
744 context.invalidate(InvalidationKind::Layout);
745 context.invalidate(InvalidationKind::Draw);
746 context.invalidate(InvalidationKind::Semantics);
747 }
748
749 fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
750 Some(self)
751 }
752
753 fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
754 Some(self)
755 }
756
757 fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
758 Some(self)
759 }
760
761 fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
762 Some(self)
763 }
764
765 fn as_semantics_node(&self) -> Option<&dyn SemanticsNode> {
766 Some(self)
767 }
768
769 fn as_semantics_node_mut(&mut self) -> Option<&mut dyn SemanticsNode> {
770 Some(self)
771 }
772
773 fn as_pointer_input_node(&self) -> Option<&dyn PointerInputNode> {
774 Some(self)
775 }
776
777 fn as_pointer_input_node_mut(&mut self) -> Option<&mut dyn PointerInputNode> {
778 Some(self)
779 }
780}
781
782impl LayoutModifierNode for TextFieldModifierNode {
783 fn measure(
784 &self,
785 _context: &mut dyn ModifierNodeContext,
786 _measurable: &dyn Measurable,
787 constraints: Constraints,
788 ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
789 let text_size = self.measure_text_content(self.wrap_width(constraints.max_width));
793
794 let min_height = if text_size.height < 1.0 {
796 DEFAULT_LINE_HEIGHT
797 } else {
798 text_size.height
799 };
800
801 let width = text_size
803 .width
804 .max(constraints.min_width)
805 .min(constraints.max_width);
806 let height = min_height
807 .max(constraints.min_height)
808 .min(constraints.max_height);
809
810 let size = Size { width, height };
811 self.measured_size.set(size);
812
813 let _ = (self.cached_pan_resolver)(size.width);
816
817 cranpose_ui_layout::LayoutModifierMeasureResult::with_size(size)
818 }
819
820 fn min_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
821 self.measure_text_content(None).width
822 }
823
824 fn max_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
825 self.measure_text_content(None).width
826 }
827
828 fn min_intrinsic_height(&self, _measurable: &dyn Measurable, width: f32) -> f32 {
829 self.measure_text_content(self.wrap_width(width))
830 .height
831 .max(DEFAULT_LINE_HEIGHT)
832 }
833
834 fn max_intrinsic_height(&self, _measurable: &dyn Measurable, width: f32) -> f32 {
835 self.measure_text_content(self.wrap_width(width))
836 .height
837 .max(DEFAULT_LINE_HEIGHT)
838 }
839}
840
841impl DrawModifierNode for TextFieldModifierNode {
842 fn draw(&self, _draw_scope: &mut dyn DrawScope) {
843 }
847
848 fn create_draw_closure(
849 &self,
850 ) -> Option<Rc<dyn Fn(cranpose_foundation::Size) -> Vec<cranpose_ui_graphics::DrawPrimitive>>>
851 {
852 use cranpose_ui_graphics::DrawPrimitive;
853
854 let is_focused = self.refs.is_focused.clone();
856 let state = self.state.clone();
857 let content_offset = self.refs.content_offset.clone();
858 let content_y_offset = self.refs.content_y_offset.clone();
859 let cursor_brush = self.cursor_brush.clone();
860 let selection_brush = self.selection_brush.clone();
861 let style = self.style.clone();
862 let cached_line_height = self.measured_line_height.clone();
863 let measured_size = self.measured_size.clone();
864 let pan_resolver = self.cached_pan_resolver.clone();
865 let handle_controller = self.handle_controller.clone();
866 let node_origin = self.refs.node_origin.clone();
867 let last_pointer_source = self.refs.last_pointer_source.clone();
868
869 Some(Rc::new(move |size| {
870 if !*is_focused.borrow() {
872 if let Some(controller) = &handle_controller {
875 controller.publish(TextFieldHandleMetrics {
876 focused: false,
877 touch: false,
878 node_origin: node_origin.get(),
879 padding_left: 0.0,
880 padding_top: 0.0,
881 scroll_offset: 0.0,
882 line_height: cached_line_height.get(),
883 });
884 }
885 return vec![];
886 }
887
888 let mut primitives = Vec::new();
889
890 let text = state.text();
891 let selection = state.selection();
892 let padding_left = content_offset.get();
893 let padding_top = content_y_offset.get();
894 let line_height = cached_line_height.get();
897
898 let measured = measured_size.get();
901 let viewport_width = if measured.width > 0.0 {
902 measured.width
903 } else {
904 (size.width - padding_left).max(0.0)
905 };
906 let viewport_height = if measured.height > 0.0 {
907 measured.height
908 } else {
909 (size.height - padding_top).max(0.0)
910 };
911 let pan = pan_resolver(viewport_width);
913
914 if let Some(controller) = &handle_controller {
917 controller.publish(TextFieldHandleMetrics {
918 focused: true,
919 touch: last_pointer_source.get().is_touch_like(),
920 node_origin: node_origin.get(),
921 padding_left,
922 padding_top,
923 scroll_offset: pan,
924 line_height,
925 });
926 }
927 let clip_bounds = cranpose_ui_graphics::Rect {
931 x: padding_left,
932 y: padding_top,
933 width: viewport_width,
934 height: viewport_height,
935 };
936
937 if !selection.collapsed() {
939 let sel_start = selection.min();
940 let sel_end = selection.max();
941
942 let lines: Vec<&str> = text.split('\n').collect();
943 let mut byte_offset: usize = 0;
944
945 for (line_idx, line) in lines.iter().enumerate() {
946 let line_start = byte_offset;
947 let line_end = byte_offset + line.len();
948
949 if sel_end > line_start && sel_start < line_end {
950 let sel_start_in_line = sel_start.saturating_sub(line_start);
951 let sel_end_in_line = (sel_end - line_start).min(line.len());
952
953 let sel_start_x = crate::text::measure_text(
954 &crate::text::AnnotatedString::from(&line[..sel_start_in_line]),
955 &style,
956 )
957 .width
958 + padding_left
959 - pan;
960 let sel_end_x = crate::text::measure_text(
961 &crate::text::AnnotatedString::from(&line[..sel_end_in_line]),
962 &style,
963 )
964 .width
965 + padding_left
966 - pan;
967 let sel_width = sel_end_x - sel_start_x;
968
969 if sel_width > 0.0 {
970 let sel_rect = cranpose_ui_graphics::Rect {
971 x: sel_start_x,
972 y: padding_top + line_idx as f32 * line_height,
973 width: sel_width,
974 height: line_height,
975 };
976 if let Some(clipped) = intersect_rect(sel_rect, clip_bounds) {
977 primitives.push(DrawPrimitive::Rect {
978 rect: clipped,
979 brush: selection_brush.clone(),
980 });
981 }
982 }
983 }
984 byte_offset = line_end + 1;
985 }
986 }
987
988 if let Some(comp_range) = state.composition() {
991 let comp_start = comp_range.min();
992 let comp_end = comp_range.max();
993
994 if comp_start < comp_end && comp_end <= text.len() {
995 let lines: Vec<&str> = text.split('\n').collect();
996 let mut byte_offset: usize = 0;
997
998 let underline_brush = cranpose_ui_graphics::Brush::solid(
1000 cranpose_ui_graphics::Color(0.8, 0.8, 0.8, 0.8),
1001 );
1002 let underline_height: f32 = 2.0;
1003
1004 for (line_idx, line) in lines.iter().enumerate() {
1005 let line_start = byte_offset;
1006 let line_end = byte_offset + line.len();
1007
1008 if comp_end > line_start && comp_start < line_end {
1010 let comp_start_in_line = comp_start.saturating_sub(line_start);
1011 let comp_end_in_line = (comp_end - line_start).min(line.len());
1012
1013 let comp_start_in_line = if line.is_char_boundary(comp_start_in_line) {
1015 comp_start_in_line
1016 } else {
1017 0
1018 };
1019 let comp_end_in_line = if line.is_char_boundary(comp_end_in_line) {
1020 comp_end_in_line
1021 } else {
1022 line.len()
1023 };
1024
1025 let comp_start_x = crate::text::measure_text(
1026 &crate::text::AnnotatedString::from(&line[..comp_start_in_line]),
1027 &style,
1028 )
1029 .width
1030 + padding_left
1031 - pan;
1032 let comp_end_x = crate::text::measure_text(
1033 &crate::text::AnnotatedString::from(&line[..comp_end_in_line]),
1034 &style,
1035 )
1036 .width
1037 + padding_left
1038 - pan;
1039 let comp_width = comp_end_x - comp_start_x;
1040
1041 if comp_width > 0.0 {
1042 let underline_rect = cranpose_ui_graphics::Rect {
1044 x: comp_start_x,
1045 y: padding_top + (line_idx as f32 + 1.0) * line_height
1046 - underline_height,
1047 width: comp_width,
1048 height: underline_height,
1049 };
1050 if let Some(clipped) = intersect_rect(underline_rect, clip_bounds) {
1051 primitives.push(DrawPrimitive::Rect {
1052 rect: clipped,
1053 brush: underline_brush.clone(),
1054 });
1055 }
1056 }
1057 }
1058 byte_offset = line_end + 1;
1059 }
1060 }
1061 }
1062
1063 if crate::cursor_animation::is_cursor_visible() {
1065 let pos = selection.start.min(text.len());
1066 let text_before = &text[..pos];
1067 let line_index = text_before.matches('\n').count();
1068 let line_start = text_before.rfind('\n').map(|i| i + 1).unwrap_or(0);
1069 let cursor_x = crate::text::measure_text(
1070 &crate::text::AnnotatedString::from(&text_before[line_start..]),
1071 &style,
1072 )
1073 .width
1074 + padding_left
1075 - pan;
1076 let cursor_y = padding_top + line_index as f32 * line_height;
1077
1078 let cursor_rect = cranpose_ui_graphics::Rect {
1079 x: cursor_x,
1080 y: cursor_y,
1081 width: CURSOR_WIDTH,
1082 height: line_height,
1083 };
1084
1085 if let Some(clipped) = intersect_rect(cursor_rect, clip_bounds) {
1086 primitives.push(DrawPrimitive::Rect {
1087 rect: clipped,
1088 brush: cursor_brush.clone(),
1089 });
1090 }
1091 }
1092
1093 primitives
1094 }))
1095 }
1096}
1097
1098impl SemanticsNode for TextFieldModifierNode {
1099 fn merge_semantics(&self, config: &mut SemanticsConfiguration) {
1100 let text = self.state.text();
1101 config.content_description = Some(text);
1102 config.is_editable_text = true;
1103 config.text_selection = Some(self.state.selection());
1104 }
1105}
1106
1107impl PointerInputNode for TextFieldModifierNode {
1108 fn on_pointer_event(
1109 &mut self,
1110 _context: &mut dyn ModifierNodeContext,
1111 _event: &PointerEvent,
1112 ) -> bool {
1113 false
1124 }
1125
1126 fn hit_test(&self, x: f32, y: f32) -> bool {
1127 let size = self.measured_size.get();
1129 x >= 0.0 && x <= size.width && y >= 0.0 && y <= size.height
1130 }
1131
1132 fn pointer_input_handler(&self) -> Option<Rc<dyn Fn(PointerEvent)>> {
1133 Some(self.cached_handler.clone())
1135 }
1136}
1137
1138#[derive(Clone)]
1149pub struct TextFieldElement {
1150 state: TextFieldState,
1152 style: TextStyle,
1154 cursor_color: Color,
1156 line_limits: TextFieldLineLimits,
1158 handle_controller: Option<TextFieldHandleController>,
1161}
1162
1163impl TextFieldElement {
1164 pub fn new(state: TextFieldState, style: TextStyle) -> Self {
1166 Self {
1167 state,
1168 style,
1169 cursor_color: DEFAULT_CURSOR_COLOR,
1170 line_limits: TextFieldLineLimits::default(),
1171 handle_controller: None,
1172 }
1173 }
1174
1175 pub fn with_cursor_color(mut self, color: Color) -> Self {
1177 self.cursor_color = color;
1178 self
1179 }
1180
1181 pub fn with_line_limits(mut self, line_limits: TextFieldLineLimits) -> Self {
1183 self.line_limits = line_limits;
1184 self
1185 }
1186
1187 pub fn with_handle_controller(mut self, controller: TextFieldHandleController) -> Self {
1189 self.handle_controller = Some(controller);
1190 self
1191 }
1192}
1193
1194impl std::fmt::Debug for TextFieldElement {
1195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1196 f.debug_struct("TextFieldElement")
1197 .field("text", &self.state.text())
1198 .field("style", &self.style)
1199 .field("cursor_color", &self.cursor_color)
1200 .finish()
1201 }
1202}
1203
1204impl Hash for TextFieldElement {
1205 fn hash<H: Hasher>(&self, state: &mut H) {
1206 std::ptr::hash(std::rc::Rc::as_ptr(&self.state.inner), state);
1209 self.cursor_color.0.to_bits().hash(state);
1211 self.cursor_color.1.to_bits().hash(state);
1212 self.cursor_color.2.to_bits().hash(state);
1213 self.cursor_color.3.to_bits().hash(state);
1214 self.style.render_hash().hash(state);
1215 self.line_limits.hash(state);
1216 }
1217}
1218
1219impl PartialEq for TextFieldElement {
1220 fn eq(&self, other: &Self) -> bool {
1221 self.state == other.state
1225 && self.style == other.style
1226 && self.cursor_color == other.cursor_color
1227 && self.line_limits == other.line_limits
1228 }
1229}
1230
1231impl Eq for TextFieldElement {}
1232
1233impl ModifierNodeElement for TextFieldElement {
1234 type Node = TextFieldModifierNode;
1235
1236 fn create(&self) -> Self::Node {
1237 let mut node = TextFieldModifierNode::new(self.state.clone(), self.style.clone())
1238 .with_cursor_color(self.cursor_color)
1239 .with_line_limits(self.line_limits);
1240 if let Some(controller) = self.handle_controller.clone() {
1241 node = node.with_handle_controller(controller);
1242 }
1243 node
1244 }
1245
1246 fn update(&self, node: &mut Self::Node) {
1247 node.state = self.state.clone();
1249 node.style = self.style.clone();
1250 node.cursor_brush = Brush::solid(self.cursor_color);
1251 node.line_limits = self.line_limits;
1252 node.handle_controller = self.handle_controller.clone();
1253
1254 node.cached_handler = TextFieldModifierNode::create_handler(
1256 node.state.clone(),
1257 node.refs.clone(),
1258 node.line_limits,
1259 self.style.clone(),
1260 );
1261
1262 node.cached_pan_resolver = TextFieldModifierNode::create_pan_resolver(
1264 node.state.clone(),
1265 node.refs.clone(),
1266 node.line_limits,
1267 self.style.clone(),
1268 );
1269
1270 if node.update_cached_state() {
1272 }
1275 }
1276
1277 fn capabilities(&self) -> NodeCapabilities {
1278 NodeCapabilities::LAYOUT
1279 | NodeCapabilities::DRAW
1280 | NodeCapabilities::SEMANTICS
1281 | NodeCapabilities::POINTER_INPUT
1282 }
1283
1284 fn always_update(&self) -> bool {
1285 true
1287 }
1288}
1289
1290#[cfg(test)]
1291mod tests {
1292 use super::*;
1293 use crate::text::TextStyle;
1294 use cranpose_core::{DefaultScheduler, Runtime};
1295 use std::sync::Arc;
1296
1297 fn with_test_runtime<T>(f: impl FnOnce() -> T) -> T {
1299 let _runtime = Runtime::new(Arc::new(DefaultScheduler));
1300 f()
1301 }
1302
1303 #[test]
1304 fn text_field_node_creation() {
1305 let _app_context = crate::render_state::app_context_test_scope();
1306 with_test_runtime(|| {
1307 let state = TextFieldState::new("Hello");
1308 let node = TextFieldModifierNode::new(state, TextStyle::default());
1309 assert_eq!(node.text(), "Hello");
1310 assert!(!node.is_focused());
1311 });
1312 }
1313
1314 #[test]
1315 fn text_field_node_focus() {
1316 let _app_context = crate::render_state::app_context_test_scope();
1317 with_test_runtime(|| {
1318 let state = TextFieldState::new("Test");
1319 let mut node = TextFieldModifierNode::new(state, TextStyle::default());
1320 assert!(!node.is_focused());
1321
1322 node.set_focused(true);
1323 assert!(node.is_focused());
1324
1325 node.set_focused(false);
1326 assert!(!node.is_focused());
1327 });
1328 }
1329
1330 #[test]
1331 fn text_field_element_creates_node() {
1332 let _app_context = crate::render_state::app_context_test_scope();
1333 with_test_runtime(|| {
1334 let state = TextFieldState::new("Hello World");
1335 let element = TextFieldElement::new(state, TextStyle::default());
1336
1337 let node = element.create();
1338 assert_eq!(node.text(), "Hello World");
1339 });
1340 }
1341
1342 #[test]
1351 fn touch_press_publishes_touch_handle_metrics_but_mouse_does_not() {
1352 use cranpose_foundation::{PointerEvent, PointerEventKind, PointerSource};
1353 use cranpose_ui_graphics::Point;
1354
1355 let _app_context = crate::render_state::app_context_test_scope();
1356 with_test_runtime(|| {
1357 let state = TextFieldState::new("hello world");
1358 let controller = TextFieldHandleController::new();
1359 let node = TextFieldModifierNode::new(state.clone(), TextStyle::default())
1360 .with_handle_controller(controller.clone());
1361 node.measured_size.set(Size {
1363 width: 120.0,
1364 height: 20.0,
1365 });
1366
1367 let handler = node
1368 .pointer_input_handler()
1369 .expect("field exposes a pointer handler");
1370 let draw = node
1371 .create_draw_closure()
1372 .expect("field exposes a draw closure");
1373 let at = Point { x: 12.0, y: 8.0 };
1374 let size = Size {
1375 width: 120.0,
1376 height: 20.0,
1377 };
1378
1379 handler(
1382 PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Touch),
1383 );
1384 let _ = draw(size);
1385 let metrics = controller
1386 .metrics()
1387 .expect("focused field publishes handle metrics");
1388 assert!(metrics.focused, "a tap focuses the field");
1389 assert!(
1390 metrics.touch,
1391 "a touch tap must publish touch = true so the finger handles show"
1392 );
1393
1394 handler(
1397 PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Mouse),
1398 );
1399 let _ = draw(size);
1400 let metrics = controller
1401 .metrics()
1402 .expect("focused field publishes handle metrics");
1403 assert!(
1404 !metrics.touch,
1405 "a mouse tap must publish touch = false (clean caret, no finger handle)"
1406 );
1407
1408 crate::text_field_focus::clear_focus();
1409 });
1410 }
1411
1412 #[test]
1420 fn double_tap_selects_the_word_under_the_finger() {
1421 use cranpose_foundation::{PointerEvent, PointerEventKind, PointerSource};
1422 use cranpose_ui_graphics::Point;
1423
1424 let _app_context = crate::render_state::app_context_test_scope();
1425 with_test_runtime(|| {
1426 let state = TextFieldState::new("hello world");
1427 let node = TextFieldModifierNode::new(state.clone(), TextStyle::default());
1428 node.measured_size.set(Size {
1429 width: 200.0,
1430 height: 20.0,
1431 });
1432 let handler = node
1433 .pointer_input_handler()
1434 .expect("field exposes a pointer handler");
1435
1436 let at = Point { x: 2.0, y: 8.0 };
1439 handler(
1440 PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Touch),
1441 );
1442 handler(
1443 PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Touch),
1444 );
1445
1446 let selection = state.selection();
1447 assert!(
1448 !selection.collapsed(),
1449 "a double tap must produce a (word) selection, got {selection:?}"
1450 );
1451 let selected = &state.text()[selection.min()..selection.max()];
1452 assert_eq!(
1453 selected, "hello",
1454 "double tap should select the whole word under the finger"
1455 );
1456
1457 crate::text_field_focus::clear_focus();
1458 });
1459 }
1460
1461 #[test]
1462 fn text_field_element_equality() {
1463 let _app_context = crate::render_state::app_context_test_scope();
1464 with_test_runtime(|| {
1465 let state1 = TextFieldState::new("Hello");
1466 let state2 = TextFieldState::new("Hello"); let elem1 = TextFieldElement::new(state1.clone(), TextStyle::default());
1469 let elem2 = TextFieldElement::new(state1.clone(), TextStyle::default()); let elem3 = TextFieldElement::new(state2, TextStyle::default()); assert_eq!(elem1, elem2, "Same state should be equal");
1475 assert_ne!(elem1, elem3, "Different states should not be equal");
1476 });
1477 }
1478
1479 #[test]
1480 fn text_field_element_update_refreshes_existing_node_style() {
1481 let _app_context = crate::render_state::app_context_test_scope();
1482 with_test_runtime(|| {
1483 let state = TextFieldState::new("themed text");
1484 let dark_style = TextStyle::from_span_style(crate::text::SpanStyle {
1485 color: Some(Color::from_rgba_u8(228, 240, 252, 255)),
1486 ..crate::text::SpanStyle::default()
1487 });
1488 let light_style = TextStyle::from_span_style(crate::text::SpanStyle {
1489 color: Some(Color::from_rgba_u8(14, 58, 96, 255)),
1490 ..crate::text::SpanStyle::default()
1491 });
1492 let initial = TextFieldElement::new(state.clone(), dark_style);
1493 let updated = TextFieldElement::new(state, light_style.clone());
1494 let mut node = initial.create();
1495
1496 updated.update(&mut node);
1497
1498 assert_eq!(node.text(), "themed text");
1499 assert_eq!(node.style(), &light_style);
1500 });
1501 }
1502
1503 #[test]
1508 fn multiline_field_measures_wrapped_height() {
1509 let _app_context = crate::render_state::app_context_test_scope();
1510 with_test_runtime(|| {
1511 let long = "abcd ".repeat(40); let state = TextFieldState::new(&long);
1513 let node = TextFieldModifierNode::new(state, TextStyle::default());
1514 assert!(
1515 !node.line_limits().is_single_line(),
1516 "default fields are multi-line"
1517 );
1518
1519 let natural = node.measure_text_content(None);
1520 let wrapped = node.measure_text_content(node.wrap_width(20.0));
1521
1522 assert!(
1523 wrapped.height > natural.height,
1524 "wrapped multi-line height {} must exceed the single-line height {}",
1525 wrapped.height,
1526 natural.height
1527 );
1528 });
1529 }
1530
1531 #[test]
1534 fn single_line_field_never_wraps() {
1535 let _app_context = crate::render_state::app_context_test_scope();
1536 with_test_runtime(|| {
1537 let state = TextFieldState::new("abcd ".repeat(40));
1538 let node = TextFieldModifierNode::new(state, TextStyle::default())
1539 .with_line_limits(TextFieldLineLimits::SingleLine);
1540 assert_eq!(
1541 node.wrap_width(20.0),
1542 None,
1543 "single-line fields must not wrap"
1544 );
1545 });
1546 }
1547
1548 #[test]
1554 fn test_cursor_x_position_calculation() {
1555 let _app_context = crate::render_state::app_context_test_scope();
1556 with_test_runtime(|| {
1557 let style = crate::text::TextStyle::default();
1559
1560 let empty_width =
1562 crate::text::measure_text(&crate::text::AnnotatedString::from(""), &style).width;
1563 assert!(
1564 empty_width.abs() < 0.1,
1565 "Empty text should have 0 width, got {}",
1566 empty_width
1567 );
1568
1569 let hi_width =
1571 crate::text::measure_text(&crate::text::AnnotatedString::from("Hi"), &style).width;
1572 assert!(
1573 hi_width > 0.0,
1574 "Text 'Hi' should have positive width: {}",
1575 hi_width
1576 );
1577
1578 let h_width =
1580 crate::text::measure_text(&crate::text::AnnotatedString::from("H"), &style).width;
1581 assert!(h_width > 0.0, "Text 'H' should have positive width");
1582 assert!(
1583 h_width < hi_width,
1584 "'H' width {} should be less than 'Hi' width {}",
1585 h_width,
1586 hi_width
1587 );
1588
1589 let state = TextFieldState::new("Hi");
1591 assert_eq!(
1592 state.selection().start,
1593 2,
1594 "Cursor should be at position 2 (end of 'Hi')"
1595 );
1596
1597 let text = state.text();
1599 let cursor_pos = state.selection().start;
1600 let text_before_cursor = &text[..cursor_pos.min(text.len())];
1601 assert_eq!(text_before_cursor, "Hi");
1602
1603 let cursor_x = crate::text::measure_text(
1605 &crate::text::AnnotatedString::from(text_before_cursor),
1606 &style,
1607 )
1608 .width;
1609 assert!(
1610 (cursor_x - hi_width).abs() < 0.1,
1611 "Cursor x {} should equal 'Hi' width {}",
1612 cursor_x,
1613 hi_width
1614 );
1615 });
1616 }
1617
1618 #[test]
1620 fn test_focused_node_creates_cursor() {
1621 let _app_context = crate::render_state::app_context_test_scope();
1622 with_test_runtime(|| {
1623 let state = TextFieldState::new("Test");
1624 let element = TextFieldElement::new(state.clone(), TextStyle::default());
1625 let node = element.create();
1626
1627 assert!(!node.is_focused());
1629
1630 *node.refs.is_focused.borrow_mut() = true;
1632 assert!(node.is_focused());
1633
1634 assert_eq!(node.text(), "Test");
1636
1637 assert_eq!(node.selection().start, 4);
1639 });
1640 }
1641}