1use cranpose_foundation::text::{TextFieldLineLimits, TextFieldState, TextRange};
20use cranpose_foundation::{
21 Constraints, DelegatableNode, DrawModifierNode, DrawScope, InvalidationKind,
22 LayoutModifierNode, Measurable, ModifierNode, ModifierNodeContext, ModifierNodeElement,
23 NodeCapabilities, NodeState, PointerEvent, PointerEventKind, PointerInputNode,
24 SemanticsConfiguration, SemanticsNode, Size,
25};
26use cranpose_ui_graphics::{Brush, Color};
27use std::cell::{Cell, RefCell};
28use std::hash::{Hash, Hasher};
29use std::rc::Rc;
30
31const DEFAULT_CURSOR_COLOR: Color = Color(1.0, 1.0, 1.0, 1.0);
33
34const DEFAULT_SELECTION_COLOR: Color = Color(0.0, 0.5, 1.0, 0.3);
36
37const DEFAULT_LINE_HEIGHT: f32 = 20.0;
39
40const CURSOR_WIDTH: f32 = 2.0;
42
43pub(crate) fn compute_horizontal_scroll_offset(
54 current_offset: f32,
55 cursor_x: f32,
56 text_width: f32,
57 viewport_width: f32,
58) -> f32 {
59 if viewport_width <= 0.0 {
60 return 0.0;
61 }
62 let max_offset = (text_width + CURSOR_WIDTH - viewport_width).max(0.0);
63 let mut offset = current_offset.clamp(0.0, max_offset);
64 let visible_end = offset + viewport_width - CURSOR_WIDTH;
65 if cursor_x > visible_end {
66 offset = cursor_x - viewport_width + CURSOR_WIDTH;
68 } else if cursor_x < offset {
69 offset = cursor_x;
71 }
72 offset.clamp(0.0, max_offset)
73}
74
75pub(crate) fn intersect_rect(
80 rect: cranpose_ui_graphics::Rect,
81 bounds: cranpose_ui_graphics::Rect,
82) -> Option<cranpose_ui_graphics::Rect> {
83 let x0 = rect.x.max(bounds.x);
84 let y0 = rect.y.max(bounds.y);
85 let x1 = (rect.x + rect.width).min(bounds.x + bounds.width);
86 let y1 = (rect.y + rect.height).min(bounds.y + bounds.height);
87 (x1 > x0 && y1 > y0).then_some(cranpose_ui_graphics::Rect {
88 x: x0,
89 y: y0,
90 width: x1 - x0,
91 height: y1 - y0,
92 })
93}
94
95pub type TextPanResolver = Rc<dyn Fn(f32) -> f32>;
98
99#[derive(Clone)]
105pub(crate) struct TextFieldRefs {
106 pub is_focused: Rc<RefCell<bool>>,
108 pub content_offset: Rc<Cell<f32>>,
110 pub content_y_offset: Rc<Cell<f32>>,
112 pub drag_anchor: Rc<Cell<Option<usize>>>,
114 pub last_click_time: Rc<Cell<Option<web_time::Instant>>>,
116 pub last_click_pos: Rc<Cell<Option<(f32, f32)>>>,
118 pub click_count: Rc<Cell<u8>>,
120 pub node_id: Rc<Cell<Option<cranpose_core::NodeId>>>,
122 pub scroll_offset: Rc<Cell<f32>>,
125}
126
127impl TextFieldRefs {
128 pub fn new() -> Self {
130 Self {
131 is_focused: Rc::new(RefCell::new(false)),
132 content_offset: Rc::new(Cell::new(0.0_f32)),
133 content_y_offset: Rc::new(Cell::new(0.0_f32)),
134 drag_anchor: Rc::new(Cell::new(None::<usize>)),
135 last_click_time: Rc::new(Cell::new(None::<web_time::Instant>)),
136 last_click_pos: Rc::new(Cell::new(None::<(f32, f32)>)),
137 click_count: Rc::new(Cell::new(0_u8)),
138 node_id: Rc::new(Cell::new(None::<cranpose_core::NodeId>)),
139 scroll_offset: Rc::new(Cell::new(0.0_f32)),
140 }
141 }
142}
143
144use crate::text::TextStyle; pub struct TextFieldModifierNode {
153 state: TextFieldState,
155 refs: TextFieldRefs,
157 style: TextStyle, cursor_brush: Brush,
161 selection_brush: Brush,
163 line_limits: TextFieldLineLimits,
165 cached_text: String,
167 cached_selection: TextRange,
169 node_state: NodeState,
171 measured_size: Rc<Cell<Size>>,
173 measured_line_height: Rc<Cell<f32>>,
175 cached_handler: Rc<dyn Fn(PointerEvent)>,
177 cached_pan_resolver: TextPanResolver,
179}
180
181impl std::fmt::Debug for TextFieldModifierNode {
182 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183 f.debug_struct("TextFieldModifierNode")
184 .field("text", &self.state.text())
185 .field("style", &self.style)
186 .field("is_focused", &*self.refs.is_focused.borrow())
187 .finish()
188 }
189}
190
191use crate::text_field_handler::TextFieldHandler;
193
194impl TextFieldModifierNode {
195 pub fn new(state: TextFieldState, style: TextStyle) -> Self {
197 let value = state.value();
198 let refs = TextFieldRefs::new();
199 let line_limits = TextFieldLineLimits::default();
200 let cached_handler =
201 Self::create_handler(state.clone(), refs.clone(), line_limits, style.clone());
202 let cached_pan_resolver =
203 Self::create_pan_resolver(state.clone(), refs.clone(), line_limits, style.clone());
204
205 Self {
206 state,
207 refs,
208 style,
209 cursor_brush: Brush::solid(DEFAULT_CURSOR_COLOR),
210 selection_brush: Brush::solid(DEFAULT_SELECTION_COLOR),
211 line_limits,
212 cached_text: value.text,
213 cached_selection: value.selection,
214 node_state: NodeState::new(),
215 measured_size: Rc::new(Cell::new(Size {
216 width: 0.0,
217 height: 0.0,
218 })),
219 measured_line_height: Rc::new(Cell::new(DEFAULT_LINE_HEIGHT)),
220 cached_handler,
221 cached_pan_resolver,
222 }
223 }
224
225 pub fn with_line_limits(mut self, line_limits: TextFieldLineLimits) -> Self {
227 self.line_limits = line_limits;
228 self.cached_pan_resolver = Self::create_pan_resolver(
229 self.state.clone(),
230 self.refs.clone(),
231 line_limits,
232 self.style.clone(),
233 );
234 self
235 }
236
237 fn create_pan_resolver(
245 state: TextFieldState,
246 refs: TextFieldRefs,
247 line_limits: TextFieldLineLimits,
248 style: TextStyle,
249 ) -> TextPanResolver {
250 Rc::new(move |viewport_width: f32| {
251 if !line_limits.is_single_line() {
252 refs.scroll_offset.set(0.0);
254 return 0.0;
255 }
256 let text = state.text();
257 let pos = state.selection().start.min(text.len());
258 let text_width = crate::text::measure_text(
259 &crate::text::AnnotatedString::from(text.as_str()),
260 &style,
261 )
262 .width;
263 let cursor_x = crate::text::measure_text(
264 &crate::text::AnnotatedString::from(&text[..pos]),
265 &style,
266 )
267 .width;
268 let offset = compute_horizontal_scroll_offset(
269 refs.scroll_offset.get(),
270 cursor_x,
271 text_width,
272 viewport_width,
273 );
274 refs.scroll_offset.set(offset);
275 offset
276 })
277 }
278
279 pub fn text_pan_resolver(&self) -> Option<TextPanResolver> {
284 self.line_limits
285 .is_single_line()
286 .then(|| self.cached_pan_resolver.clone())
287 }
288
289 pub fn scroll_offset(&self) -> f32 {
291 self.refs.scroll_offset.get()
292 }
293
294 pub fn line_limits(&self) -> TextFieldLineLimits {
296 self.line_limits
297 }
298
299 fn create_handler(
301 state: TextFieldState,
302 refs: TextFieldRefs,
303 line_limits: TextFieldLineLimits,
304 style: TextStyle, ) -> Rc<dyn Fn(PointerEvent)> {
306 use crate::text_selection::{
309 classify_tap, find_line_boundaries, TapCount, MULTI_TAP_SLOP_PX, MULTI_TAP_TIMEOUT_MS,
310 };
311 use crate::word_boundaries::find_word_boundaries;
312
313 Rc::new(move |event: PointerEvent| {
314 let click_x =
318 (event.position.x - refs.content_offset.get() + refs.scroll_offset.get()).max(0.0);
319 let click_y = (event.position.y - refs.content_y_offset.get()).max(0.0);
320
321 match event.kind {
322 PointerEventKind::Down => {
323 let handler =
325 TextFieldHandler::new(state.clone(), refs.node_id.get(), line_limits);
326 crate::text_field_focus::request_focus(refs.is_focused.clone(), handler);
327
328 let now = web_time::Instant::now();
329 let text = state.text();
330 let pos = crate::text::get_offset_for_position(
331 &crate::text::AnnotatedString::from(text.as_str()),
332 &style,
333 click_x,
334 click_y,
335 );
336
337 let previous = refs.click_count.get().try_into().ok().and_then(|count| {
342 let (px, py) = refs.last_click_pos.get()?;
343 Some((count, px, py))
344 });
345 let elapsed_ms = refs
346 .last_click_time
347 .get()
348 .map(|last| now.duration_since(last).as_millis())
349 .unwrap_or(u128::MAX);
350 let tap = classify_tap(
351 previous,
352 elapsed_ms,
353 event.position.x,
354 event.position.y,
355 MULTI_TAP_TIMEOUT_MS,
356 MULTI_TAP_SLOP_PX,
357 );
358
359 match tap {
360 TapCount::Triple => {
361 let (line_start, line_end) = find_line_boundaries(&text, pos);
363 state.edit(|buffer| {
364 buffer.select(TextRange::new(line_start, line_end));
365 });
366 refs.drag_anchor.set(Some(line_start));
367 }
368 TapCount::Double => {
369 let (word_start, word_end) = find_word_boundaries(&text, pos);
371 state.edit(|buffer| {
372 buffer.select(TextRange::new(word_start, word_end));
373 });
374 refs.drag_anchor.set(Some(word_start));
375 }
376 TapCount::Single => {
377 refs.drag_anchor.set(Some(pos));
379 state.edit(|buffer| {
380 buffer.place_cursor_before_char(pos);
381 });
382 }
383 }
384
385 refs.click_count.set(tap.as_u8());
386 refs.last_click_time.set(Some(now));
387 refs.last_click_pos
388 .set(Some((event.position.x, event.position.y)));
389 event.consume();
390 }
391 PointerEventKind::Move => {
392 if let Some(anchor) = refs.drag_anchor.get() {
394 if *refs.is_focused.borrow() {
395 let text = state.text();
396 let current_pos = crate::text::get_offset_for_position(
397 &crate::text::AnnotatedString::from(text.as_str()),
398 &style,
399 click_x,
400 click_y,
401 );
402
403 state.set_selection(TextRange::new(anchor, current_pos));
405
406 crate::request_render_invalidation();
408
409 event.consume();
410 }
411 }
412 }
413 PointerEventKind::Up => {
414 refs.drag_anchor.set(None);
416 }
417 _ => {}
418 }
419 })
420 }
421
422 pub fn with_cursor_color(mut self, color: Color) -> Self {
424 self.cursor_brush = Brush::solid(color);
425 self
426 }
427
428 pub fn set_focused(&mut self, focused: bool) {
430 let current = *self.refs.is_focused.borrow();
431 if current != focused {
432 *self.refs.is_focused.borrow_mut() = focused;
433 }
434 }
435
436 pub fn is_focused(&self) -> bool {
438 *self.refs.is_focused.borrow()
439 }
440
441 pub fn is_focused_rc(&self) -> Rc<RefCell<bool>> {
443 self.refs.is_focused.clone()
444 }
445
446 pub fn content_offset_rc(&self) -> Rc<Cell<f32>> {
448 self.refs.content_offset.clone()
449 }
450
451 pub fn content_y_offset_rc(&self) -> Rc<Cell<f32>> {
453 self.refs.content_y_offset.clone()
454 }
455
456 pub fn text(&self) -> String {
458 self.state.text()
459 }
460
461 pub fn style(&self) -> &TextStyle {
462 &self.style
463 }
464
465 pub fn selection(&self) -> TextRange {
467 self.state.selection()
468 }
469
470 pub fn cursor_brush(&self) -> Brush {
472 self.cursor_brush.clone()
473 }
474
475 pub fn selection_brush(&self) -> Brush {
477 self.selection_brush.clone()
478 }
479
480 pub fn insert_text(&mut self, text: &str) {
482 self.state.edit(|buffer| {
483 buffer.insert(text);
484 });
485 }
486
487 pub fn copy_selection(&self) -> Option<String> {
490 self.state.copy_selection()
491 }
492
493 pub fn cut_selection(&mut self) -> Option<String> {
496 let text = self.copy_selection();
497 if text.is_some() {
498 self.state.edit(|buffer| {
499 buffer.delete(buffer.selection());
500 });
501 }
502 text
503 }
504
505 pub fn get_state(&self) -> cranpose_foundation::text::TextFieldState {
508 self.state.clone()
509 }
510
511 pub fn set_content_offset(&self, offset: f32) {
514 self.refs.content_offset.set(offset);
515 }
516
517 pub fn set_content_y_offset(&self, offset: f32) {
520 self.refs.content_y_offset.set(offset);
521 }
522
523 fn wrap_width(&self, available_width: f32) -> Option<f32> {
530 (!self.line_limits.is_single_line() && available_width.is_finite() && available_width > 0.0)
531 .then_some(available_width)
532 }
533
534 fn measure_text_content(&self, wrap_width: Option<f32>) -> Size {
540 let text = self.state.text();
541 let node_id = self.refs.node_id.get();
542 let annotated = crate::text::AnnotatedString::from(text.as_str());
543 let metrics = match wrap_width {
544 Some(max_width) => crate::text::measure_text_with_options_for_node(
545 node_id,
546 &annotated,
547 &self.style,
548 crate::text::TextLayoutOptions::default(),
549 Some(max_width),
550 ),
551 None => crate::text::measure_text_for_node(node_id, &annotated, &self.style),
552 };
553 self.measured_line_height.set(metrics.line_height);
554 Size {
555 width: metrics.width,
556 height: metrics.height,
557 }
558 }
559
560 fn update_cached_state(&mut self) -> bool {
562 let value = self.state.value();
563 let text_changed = value.text != self.cached_text;
564 let selection_changed = value.selection != self.cached_selection;
565
566 if text_changed {
567 self.cached_text = value.text;
568 }
569 if selection_changed {
570 self.cached_selection = value.selection;
571 }
572
573 text_changed || selection_changed
574 }
575
576 pub fn position_cursor_at_offset(&self, x_offset: f32) {
579 let text = self.state.text();
580 if text.is_empty() {
581 self.state.edit(|buffer| {
582 buffer.place_cursor_at_start();
583 });
584 return;
585 }
586
587 let byte_offset = crate::text::get_offset_for_position(
590 &crate::text::AnnotatedString::from(text.as_str()),
591 &self.style,
592 x_offset + self.refs.scroll_offset.get(),
593 0.0,
594 );
595
596 self.state.edit(|buffer| {
597 buffer.place_cursor_before_char(byte_offset);
598 });
599 }
600
601 }
605
606impl DelegatableNode for TextFieldModifierNode {
607 fn node_state(&self) -> &NodeState {
608 &self.node_state
609 }
610}
611
612impl ModifierNode for TextFieldModifierNode {
613 fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
614 self.refs.node_id.set(context.node_id());
616
617 context.invalidate(InvalidationKind::Layout);
618 context.invalidate(InvalidationKind::Draw);
619 context.invalidate(InvalidationKind::Semantics);
620 }
621
622 fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
623 Some(self)
624 }
625
626 fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
627 Some(self)
628 }
629
630 fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
631 Some(self)
632 }
633
634 fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
635 Some(self)
636 }
637
638 fn as_semantics_node(&self) -> Option<&dyn SemanticsNode> {
639 Some(self)
640 }
641
642 fn as_semantics_node_mut(&mut self) -> Option<&mut dyn SemanticsNode> {
643 Some(self)
644 }
645
646 fn as_pointer_input_node(&self) -> Option<&dyn PointerInputNode> {
647 Some(self)
648 }
649
650 fn as_pointer_input_node_mut(&mut self) -> Option<&mut dyn PointerInputNode> {
651 Some(self)
652 }
653}
654
655impl LayoutModifierNode for TextFieldModifierNode {
656 fn measure(
657 &self,
658 _context: &mut dyn ModifierNodeContext,
659 _measurable: &dyn Measurable,
660 constraints: Constraints,
661 ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
662 let text_size = self.measure_text_content(self.wrap_width(constraints.max_width));
666
667 let min_height = if text_size.height < 1.0 {
669 DEFAULT_LINE_HEIGHT
670 } else {
671 text_size.height
672 };
673
674 let width = text_size
676 .width
677 .max(constraints.min_width)
678 .min(constraints.max_width);
679 let height = min_height
680 .max(constraints.min_height)
681 .min(constraints.max_height);
682
683 let size = Size { width, height };
684 self.measured_size.set(size);
685
686 let _ = (self.cached_pan_resolver)(size.width);
689
690 cranpose_ui_layout::LayoutModifierMeasureResult::with_size(size)
691 }
692
693 fn min_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
694 self.measure_text_content(None).width
695 }
696
697 fn max_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
698 self.measure_text_content(None).width
699 }
700
701 fn min_intrinsic_height(&self, _measurable: &dyn Measurable, width: f32) -> f32 {
702 self.measure_text_content(self.wrap_width(width))
703 .height
704 .max(DEFAULT_LINE_HEIGHT)
705 }
706
707 fn max_intrinsic_height(&self, _measurable: &dyn Measurable, width: f32) -> f32 {
708 self.measure_text_content(self.wrap_width(width))
709 .height
710 .max(DEFAULT_LINE_HEIGHT)
711 }
712}
713
714impl DrawModifierNode for TextFieldModifierNode {
715 fn draw(&self, _draw_scope: &mut dyn DrawScope) {
716 }
720
721 fn create_draw_closure(
722 &self,
723 ) -> Option<Rc<dyn Fn(cranpose_foundation::Size) -> Vec<cranpose_ui_graphics::DrawPrimitive>>>
724 {
725 use cranpose_ui_graphics::DrawPrimitive;
726
727 let is_focused = self.refs.is_focused.clone();
729 let state = self.state.clone();
730 let content_offset = self.refs.content_offset.clone();
731 let content_y_offset = self.refs.content_y_offset.clone();
732 let cursor_brush = self.cursor_brush.clone();
733 let selection_brush = self.selection_brush.clone();
734 let style = self.style.clone();
735 let cached_line_height = self.measured_line_height.clone();
736 let measured_size = self.measured_size.clone();
737 let pan_resolver = self.cached_pan_resolver.clone();
738
739 Some(Rc::new(move |size| {
740 if !*is_focused.borrow() {
742 return vec![];
743 }
744
745 let mut primitives = Vec::new();
746
747 let text = state.text();
748 let selection = state.selection();
749 let padding_left = content_offset.get();
750 let padding_top = content_y_offset.get();
751 let line_height = cached_line_height.get();
754
755 let measured = measured_size.get();
758 let viewport_width = if measured.width > 0.0 {
759 measured.width
760 } else {
761 (size.width - padding_left).max(0.0)
762 };
763 let viewport_height = if measured.height > 0.0 {
764 measured.height
765 } else {
766 (size.height - padding_top).max(0.0)
767 };
768 let pan = pan_resolver(viewport_width);
770 let clip_bounds = cranpose_ui_graphics::Rect {
774 x: padding_left,
775 y: padding_top,
776 width: viewport_width,
777 height: viewport_height,
778 };
779
780 if !selection.collapsed() {
782 let sel_start = selection.min();
783 let sel_end = selection.max();
784
785 let lines: Vec<&str> = text.split('\n').collect();
786 let mut byte_offset: usize = 0;
787
788 for (line_idx, line) in lines.iter().enumerate() {
789 let line_start = byte_offset;
790 let line_end = byte_offset + line.len();
791
792 if sel_end > line_start && sel_start < line_end {
793 let sel_start_in_line = sel_start.saturating_sub(line_start);
794 let sel_end_in_line = (sel_end - line_start).min(line.len());
795
796 let sel_start_x = crate::text::measure_text(
797 &crate::text::AnnotatedString::from(&line[..sel_start_in_line]),
798 &style,
799 )
800 .width
801 + padding_left
802 - pan;
803 let sel_end_x = crate::text::measure_text(
804 &crate::text::AnnotatedString::from(&line[..sel_end_in_line]),
805 &style,
806 )
807 .width
808 + padding_left
809 - pan;
810 let sel_width = sel_end_x - sel_start_x;
811
812 if sel_width > 0.0 {
813 let sel_rect = cranpose_ui_graphics::Rect {
814 x: sel_start_x,
815 y: padding_top + line_idx as f32 * line_height,
816 width: sel_width,
817 height: line_height,
818 };
819 if let Some(clipped) = intersect_rect(sel_rect, clip_bounds) {
820 primitives.push(DrawPrimitive::Rect {
821 rect: clipped,
822 brush: selection_brush.clone(),
823 });
824 }
825 }
826 }
827 byte_offset = line_end + 1;
828 }
829 }
830
831 if let Some(comp_range) = state.composition() {
834 let comp_start = comp_range.min();
835 let comp_end = comp_range.max();
836
837 if comp_start < comp_end && comp_end <= text.len() {
838 let lines: Vec<&str> = text.split('\n').collect();
839 let mut byte_offset: usize = 0;
840
841 let underline_brush = cranpose_ui_graphics::Brush::solid(
843 cranpose_ui_graphics::Color(0.8, 0.8, 0.8, 0.8),
844 );
845 let underline_height: f32 = 2.0;
846
847 for (line_idx, line) in lines.iter().enumerate() {
848 let line_start = byte_offset;
849 let line_end = byte_offset + line.len();
850
851 if comp_end > line_start && comp_start < line_end {
853 let comp_start_in_line = comp_start.saturating_sub(line_start);
854 let comp_end_in_line = (comp_end - line_start).min(line.len());
855
856 let comp_start_in_line = if line.is_char_boundary(comp_start_in_line) {
858 comp_start_in_line
859 } else {
860 0
861 };
862 let comp_end_in_line = if line.is_char_boundary(comp_end_in_line) {
863 comp_end_in_line
864 } else {
865 line.len()
866 };
867
868 let comp_start_x = crate::text::measure_text(
869 &crate::text::AnnotatedString::from(&line[..comp_start_in_line]),
870 &style,
871 )
872 .width
873 + padding_left
874 - pan;
875 let comp_end_x = crate::text::measure_text(
876 &crate::text::AnnotatedString::from(&line[..comp_end_in_line]),
877 &style,
878 )
879 .width
880 + padding_left
881 - pan;
882 let comp_width = comp_end_x - comp_start_x;
883
884 if comp_width > 0.0 {
885 let underline_rect = cranpose_ui_graphics::Rect {
887 x: comp_start_x,
888 y: padding_top + (line_idx as f32 + 1.0) * line_height
889 - underline_height,
890 width: comp_width,
891 height: underline_height,
892 };
893 if let Some(clipped) = intersect_rect(underline_rect, clip_bounds) {
894 primitives.push(DrawPrimitive::Rect {
895 rect: clipped,
896 brush: underline_brush.clone(),
897 });
898 }
899 }
900 }
901 byte_offset = line_end + 1;
902 }
903 }
904 }
905
906 if crate::cursor_animation::is_cursor_visible() {
908 let pos = selection.start.min(text.len());
909 let text_before = &text[..pos];
910 let line_index = text_before.matches('\n').count();
911 let line_start = text_before.rfind('\n').map(|i| i + 1).unwrap_or(0);
912 let cursor_x = crate::text::measure_text(
913 &crate::text::AnnotatedString::from(&text_before[line_start..]),
914 &style,
915 )
916 .width
917 + padding_left
918 - pan;
919 let cursor_y = padding_top + line_index as f32 * line_height;
920
921 let cursor_rect = cranpose_ui_graphics::Rect {
922 x: cursor_x,
923 y: cursor_y,
924 width: CURSOR_WIDTH,
925 height: line_height,
926 };
927
928 if let Some(clipped) = intersect_rect(cursor_rect, clip_bounds) {
929 primitives.push(DrawPrimitive::Rect {
930 rect: clipped,
931 brush: cursor_brush.clone(),
932 });
933 }
934 }
935
936 primitives
937 }))
938 }
939}
940
941impl SemanticsNode for TextFieldModifierNode {
942 fn merge_semantics(&self, config: &mut SemanticsConfiguration) {
943 let text = self.state.text();
944 config.content_description = Some(text);
945 config.is_editable_text = true;
946 config.text_selection = Some(self.state.selection());
947 }
948}
949
950impl PointerInputNode for TextFieldModifierNode {
951 fn on_pointer_event(
952 &mut self,
953 _context: &mut dyn ModifierNodeContext,
954 _event: &PointerEvent,
955 ) -> bool {
956 false
967 }
968
969 fn hit_test(&self, x: f32, y: f32) -> bool {
970 let size = self.measured_size.get();
972 x >= 0.0 && x <= size.width && y >= 0.0 && y <= size.height
973 }
974
975 fn pointer_input_handler(&self) -> Option<Rc<dyn Fn(PointerEvent)>> {
976 Some(self.cached_handler.clone())
978 }
979}
980
981#[derive(Clone)]
992pub struct TextFieldElement {
993 state: TextFieldState,
995 style: TextStyle,
997 cursor_color: Color,
999 line_limits: TextFieldLineLimits,
1001}
1002
1003impl TextFieldElement {
1004 pub fn new(state: TextFieldState, style: TextStyle) -> Self {
1006 Self {
1007 state,
1008 style,
1009 cursor_color: DEFAULT_CURSOR_COLOR,
1010 line_limits: TextFieldLineLimits::default(),
1011 }
1012 }
1013
1014 pub fn with_cursor_color(mut self, color: Color) -> Self {
1016 self.cursor_color = color;
1017 self
1018 }
1019
1020 pub fn with_line_limits(mut self, line_limits: TextFieldLineLimits) -> Self {
1022 self.line_limits = line_limits;
1023 self
1024 }
1025}
1026
1027impl std::fmt::Debug for TextFieldElement {
1028 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1029 f.debug_struct("TextFieldElement")
1030 .field("text", &self.state.text())
1031 .field("style", &self.style)
1032 .field("cursor_color", &self.cursor_color)
1033 .finish()
1034 }
1035}
1036
1037impl Hash for TextFieldElement {
1038 fn hash<H: Hasher>(&self, state: &mut H) {
1039 std::ptr::hash(std::rc::Rc::as_ptr(&self.state.inner), state);
1042 self.cursor_color.0.to_bits().hash(state);
1044 self.cursor_color.1.to_bits().hash(state);
1045 self.cursor_color.2.to_bits().hash(state);
1046 self.cursor_color.3.to_bits().hash(state);
1047 self.style.render_hash().hash(state);
1048 self.line_limits.hash(state);
1049 }
1050}
1051
1052impl PartialEq for TextFieldElement {
1053 fn eq(&self, other: &Self) -> bool {
1054 self.state == other.state
1058 && self.style == other.style
1059 && self.cursor_color == other.cursor_color
1060 && self.line_limits == other.line_limits
1061 }
1062}
1063
1064impl Eq for TextFieldElement {}
1065
1066impl ModifierNodeElement for TextFieldElement {
1067 type Node = TextFieldModifierNode;
1068
1069 fn create(&self) -> Self::Node {
1070 TextFieldModifierNode::new(self.state.clone(), self.style.clone())
1071 .with_cursor_color(self.cursor_color)
1072 .with_line_limits(self.line_limits)
1073 }
1074
1075 fn update(&self, node: &mut Self::Node) {
1076 node.state = self.state.clone();
1078 node.style = self.style.clone();
1079 node.cursor_brush = Brush::solid(self.cursor_color);
1080 node.line_limits = self.line_limits;
1081
1082 node.cached_handler = TextFieldModifierNode::create_handler(
1084 node.state.clone(),
1085 node.refs.clone(),
1086 node.line_limits,
1087 self.style.clone(),
1088 );
1089
1090 node.cached_pan_resolver = TextFieldModifierNode::create_pan_resolver(
1092 node.state.clone(),
1093 node.refs.clone(),
1094 node.line_limits,
1095 self.style.clone(),
1096 );
1097
1098 if node.update_cached_state() {
1100 }
1103 }
1104
1105 fn capabilities(&self) -> NodeCapabilities {
1106 NodeCapabilities::LAYOUT
1107 | NodeCapabilities::DRAW
1108 | NodeCapabilities::SEMANTICS
1109 | NodeCapabilities::POINTER_INPUT
1110 }
1111
1112 fn always_update(&self) -> bool {
1113 true
1115 }
1116}
1117
1118#[cfg(test)]
1119mod tests {
1120 use super::*;
1121 use crate::text::TextStyle;
1122 use cranpose_core::{DefaultScheduler, Runtime};
1123 use std::sync::Arc;
1124
1125 fn with_test_runtime<T>(f: impl FnOnce() -> T) -> T {
1127 let _runtime = Runtime::new(Arc::new(DefaultScheduler));
1128 f()
1129 }
1130
1131 #[test]
1132 fn text_field_node_creation() {
1133 let _app_context = crate::render_state::app_context_test_scope();
1134 with_test_runtime(|| {
1135 let state = TextFieldState::new("Hello");
1136 let node = TextFieldModifierNode::new(state, TextStyle::default());
1137 assert_eq!(node.text(), "Hello");
1138 assert!(!node.is_focused());
1139 });
1140 }
1141
1142 #[test]
1143 fn text_field_node_focus() {
1144 let _app_context = crate::render_state::app_context_test_scope();
1145 with_test_runtime(|| {
1146 let state = TextFieldState::new("Test");
1147 let mut node = TextFieldModifierNode::new(state, TextStyle::default());
1148 assert!(!node.is_focused());
1149
1150 node.set_focused(true);
1151 assert!(node.is_focused());
1152
1153 node.set_focused(false);
1154 assert!(!node.is_focused());
1155 });
1156 }
1157
1158 #[test]
1159 fn text_field_element_creates_node() {
1160 let _app_context = crate::render_state::app_context_test_scope();
1161 with_test_runtime(|| {
1162 let state = TextFieldState::new("Hello World");
1163 let element = TextFieldElement::new(state, TextStyle::default());
1164
1165 let node = element.create();
1166 assert_eq!(node.text(), "Hello World");
1167 });
1168 }
1169
1170 #[test]
1171 fn text_field_element_equality() {
1172 let _app_context = crate::render_state::app_context_test_scope();
1173 with_test_runtime(|| {
1174 let state1 = TextFieldState::new("Hello");
1175 let state2 = TextFieldState::new("Hello"); let elem1 = TextFieldElement::new(state1.clone(), TextStyle::default());
1178 let elem2 = TextFieldElement::new(state1.clone(), TextStyle::default()); let elem3 = TextFieldElement::new(state2, TextStyle::default()); assert_eq!(elem1, elem2, "Same state should be equal");
1184 assert_ne!(elem1, elem3, "Different states should not be equal");
1185 });
1186 }
1187
1188 #[test]
1189 fn text_field_element_update_refreshes_existing_node_style() {
1190 let _app_context = crate::render_state::app_context_test_scope();
1191 with_test_runtime(|| {
1192 let state = TextFieldState::new("themed text");
1193 let dark_style = TextStyle::from_span_style(crate::text::SpanStyle {
1194 color: Some(Color::from_rgba_u8(228, 240, 252, 255)),
1195 ..crate::text::SpanStyle::default()
1196 });
1197 let light_style = TextStyle::from_span_style(crate::text::SpanStyle {
1198 color: Some(Color::from_rgba_u8(14, 58, 96, 255)),
1199 ..crate::text::SpanStyle::default()
1200 });
1201 let initial = TextFieldElement::new(state.clone(), dark_style);
1202 let updated = TextFieldElement::new(state, light_style.clone());
1203 let mut node = initial.create();
1204
1205 updated.update(&mut node);
1206
1207 assert_eq!(node.text(), "themed text");
1208 assert_eq!(node.style(), &light_style);
1209 });
1210 }
1211
1212 #[test]
1217 fn multiline_field_measures_wrapped_height() {
1218 let _app_context = crate::render_state::app_context_test_scope();
1219 with_test_runtime(|| {
1220 let long = "abcd ".repeat(40); let state = TextFieldState::new(&long);
1222 let node = TextFieldModifierNode::new(state, TextStyle::default());
1223 assert!(
1224 !node.line_limits().is_single_line(),
1225 "default fields are multi-line"
1226 );
1227
1228 let natural = node.measure_text_content(None);
1229 let wrapped = node.measure_text_content(node.wrap_width(20.0));
1230
1231 assert!(
1232 wrapped.height > natural.height,
1233 "wrapped multi-line height {} must exceed the single-line height {}",
1234 wrapped.height,
1235 natural.height
1236 );
1237 });
1238 }
1239
1240 #[test]
1243 fn single_line_field_never_wraps() {
1244 let _app_context = crate::render_state::app_context_test_scope();
1245 with_test_runtime(|| {
1246 let state = TextFieldState::new("abcd ".repeat(40));
1247 let node = TextFieldModifierNode::new(state, TextStyle::default())
1248 .with_line_limits(TextFieldLineLimits::SingleLine);
1249 assert_eq!(
1250 node.wrap_width(20.0),
1251 None,
1252 "single-line fields must not wrap"
1253 );
1254 });
1255 }
1256
1257 #[test]
1263 fn test_cursor_x_position_calculation() {
1264 let _app_context = crate::render_state::app_context_test_scope();
1265 with_test_runtime(|| {
1266 let style = crate::text::TextStyle::default();
1268
1269 let empty_width =
1271 crate::text::measure_text(&crate::text::AnnotatedString::from(""), &style).width;
1272 assert!(
1273 empty_width.abs() < 0.1,
1274 "Empty text should have 0 width, got {}",
1275 empty_width
1276 );
1277
1278 let hi_width =
1280 crate::text::measure_text(&crate::text::AnnotatedString::from("Hi"), &style).width;
1281 assert!(
1282 hi_width > 0.0,
1283 "Text 'Hi' should have positive width: {}",
1284 hi_width
1285 );
1286
1287 let h_width =
1289 crate::text::measure_text(&crate::text::AnnotatedString::from("H"), &style).width;
1290 assert!(h_width > 0.0, "Text 'H' should have positive width");
1291 assert!(
1292 h_width < hi_width,
1293 "'H' width {} should be less than 'Hi' width {}",
1294 h_width,
1295 hi_width
1296 );
1297
1298 let state = TextFieldState::new("Hi");
1300 assert_eq!(
1301 state.selection().start,
1302 2,
1303 "Cursor should be at position 2 (end of 'Hi')"
1304 );
1305
1306 let text = state.text();
1308 let cursor_pos = state.selection().start;
1309 let text_before_cursor = &text[..cursor_pos.min(text.len())];
1310 assert_eq!(text_before_cursor, "Hi");
1311
1312 let cursor_x = crate::text::measure_text(
1314 &crate::text::AnnotatedString::from(text_before_cursor),
1315 &style,
1316 )
1317 .width;
1318 assert!(
1319 (cursor_x - hi_width).abs() < 0.1,
1320 "Cursor x {} should equal 'Hi' width {}",
1321 cursor_x,
1322 hi_width
1323 );
1324 });
1325 }
1326
1327 #[test]
1329 fn test_focused_node_creates_cursor() {
1330 let _app_context = crate::render_state::app_context_test_scope();
1331 with_test_runtime(|| {
1332 let state = TextFieldState::new("Test");
1333 let element = TextFieldElement::new(state.clone(), TextStyle::default());
1334 let node = element.create();
1335
1336 assert!(!node.is_focused());
1338
1339 *node.refs.is_focused.borrow_mut() = true;
1341 assert!(node.is_focused());
1342
1343 assert_eq!(node.text(), "Test");
1345
1346 assert_eq!(node.selection().start, 4);
1348 });
1349 }
1350}