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 DOUBLE_CLICK_MS: u128 = 500;
39
40const DEFAULT_LINE_HEIGHT: f32 = 20.0;
42
43const CURSOR_WIDTH: f32 = 2.0;
45
46pub(crate) fn compute_horizontal_scroll_offset(
57 current_offset: f32,
58 cursor_x: f32,
59 text_width: f32,
60 viewport_width: f32,
61) -> f32 {
62 if viewport_width <= 0.0 {
63 return 0.0;
64 }
65 let max_offset = (text_width + CURSOR_WIDTH - viewport_width).max(0.0);
66 let mut offset = current_offset.clamp(0.0, max_offset);
67 let visible_end = offset + viewport_width - CURSOR_WIDTH;
68 if cursor_x > visible_end {
69 offset = cursor_x - viewport_width + CURSOR_WIDTH;
71 } else if cursor_x < offset {
72 offset = cursor_x;
74 }
75 offset.clamp(0.0, max_offset)
76}
77
78pub(crate) fn intersect_rect(
83 rect: cranpose_ui_graphics::Rect,
84 bounds: cranpose_ui_graphics::Rect,
85) -> Option<cranpose_ui_graphics::Rect> {
86 let x0 = rect.x.max(bounds.x);
87 let y0 = rect.y.max(bounds.y);
88 let x1 = (rect.x + rect.width).min(bounds.x + bounds.width);
89 let y1 = (rect.y + rect.height).min(bounds.y + bounds.height);
90 (x1 > x0 && y1 > y0).then_some(cranpose_ui_graphics::Rect {
91 x: x0,
92 y: y0,
93 width: x1 - x0,
94 height: y1 - y0,
95 })
96}
97
98pub type TextPanResolver = Rc<dyn Fn(f32) -> f32>;
101
102#[derive(Clone)]
108pub(crate) struct TextFieldRefs {
109 pub is_focused: Rc<RefCell<bool>>,
111 pub content_offset: Rc<Cell<f32>>,
113 pub content_y_offset: Rc<Cell<f32>>,
115 pub drag_anchor: Rc<Cell<Option<usize>>>,
117 pub last_click_time: Rc<Cell<Option<web_time::Instant>>>,
119 pub click_count: Rc<Cell<u8>>,
121 pub node_id: Rc<Cell<Option<cranpose_core::NodeId>>>,
123 pub scroll_offset: Rc<Cell<f32>>,
126}
127
128impl TextFieldRefs {
129 pub fn new() -> Self {
131 Self {
132 is_focused: Rc::new(RefCell::new(false)),
133 content_offset: Rc::new(Cell::new(0.0_f32)),
134 content_y_offset: Rc::new(Cell::new(0.0_f32)),
135 drag_anchor: Rc::new(Cell::new(None::<usize>)),
136 last_click_time: Rc::new(Cell::new(None::<web_time::Instant>)),
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::word_boundaries::find_word_boundaries;
308
309 Rc::new(move |event: PointerEvent| {
310 let click_x =
314 (event.position.x - refs.content_offset.get() + refs.scroll_offset.get()).max(0.0);
315 let click_y = (event.position.y - refs.content_y_offset.get()).max(0.0);
316
317 match event.kind {
318 PointerEventKind::Down => {
319 let handler =
321 TextFieldHandler::new(state.clone(), refs.node_id.get(), line_limits);
322 crate::text_field_focus::request_focus(refs.is_focused.clone(), handler);
323
324 let now = web_time::Instant::now();
325 let text = state.text();
326 let pos = crate::text::get_offset_for_position(
327 &crate::text::AnnotatedString::from(text.as_str()),
328 &style,
329 click_x,
330 click_y,
331 );
332
333 let is_double_click = if let Some(last) = refs.last_click_time.get() {
335 now.duration_since(last).as_millis() < DOUBLE_CLICK_MS
336 } else {
337 false
338 };
339
340 if is_double_click {
341 let count = refs.click_count.get() + 1;
343 refs.click_count.set(count.min(3));
344
345 if count >= 3 {
346 state.edit(|buffer| {
348 buffer.select_all();
349 });
350 refs.drag_anchor.set(Some(0));
352 } else if count >= 2 {
353 let (word_start, word_end) = find_word_boundaries(&text, pos);
355 state.edit(|buffer| {
356 buffer.select(TextRange::new(word_start, word_end));
357 });
358 refs.drag_anchor.set(Some(word_start));
360 }
361 } else {
362 refs.click_count.set(1);
364 refs.drag_anchor.set(Some(pos));
365 state.edit(|buffer| {
366 buffer.place_cursor_before_char(pos);
367 });
368 }
369
370 refs.last_click_time.set(Some(now));
371 event.consume();
372 }
373 PointerEventKind::Move => {
374 if let Some(anchor) = refs.drag_anchor.get() {
376 if *refs.is_focused.borrow() {
377 let text = state.text();
378 let current_pos = crate::text::get_offset_for_position(
379 &crate::text::AnnotatedString::from(text.as_str()),
380 &style,
381 click_x,
382 click_y,
383 );
384
385 state.set_selection(TextRange::new(anchor, current_pos));
387
388 crate::request_render_invalidation();
390
391 event.consume();
392 }
393 }
394 }
395 PointerEventKind::Up => {
396 refs.drag_anchor.set(None);
398 }
399 _ => {}
400 }
401 })
402 }
403
404 pub fn with_cursor_color(mut self, color: Color) -> Self {
406 self.cursor_brush = Brush::solid(color);
407 self
408 }
409
410 pub fn set_focused(&mut self, focused: bool) {
412 let current = *self.refs.is_focused.borrow();
413 if current != focused {
414 *self.refs.is_focused.borrow_mut() = focused;
415 }
416 }
417
418 pub fn is_focused(&self) -> bool {
420 *self.refs.is_focused.borrow()
421 }
422
423 pub fn is_focused_rc(&self) -> Rc<RefCell<bool>> {
425 self.refs.is_focused.clone()
426 }
427
428 pub fn content_offset_rc(&self) -> Rc<Cell<f32>> {
430 self.refs.content_offset.clone()
431 }
432
433 pub fn content_y_offset_rc(&self) -> Rc<Cell<f32>> {
435 self.refs.content_y_offset.clone()
436 }
437
438 pub fn text(&self) -> String {
440 self.state.text()
441 }
442
443 pub fn style(&self) -> &TextStyle {
444 &self.style
445 }
446
447 pub fn selection(&self) -> TextRange {
449 self.state.selection()
450 }
451
452 pub fn cursor_brush(&self) -> Brush {
454 self.cursor_brush.clone()
455 }
456
457 pub fn selection_brush(&self) -> Brush {
459 self.selection_brush.clone()
460 }
461
462 pub fn insert_text(&mut self, text: &str) {
464 self.state.edit(|buffer| {
465 buffer.insert(text);
466 });
467 }
468
469 pub fn copy_selection(&self) -> Option<String> {
472 self.state.copy_selection()
473 }
474
475 pub fn cut_selection(&mut self) -> Option<String> {
478 let text = self.copy_selection();
479 if text.is_some() {
480 self.state.edit(|buffer| {
481 buffer.delete(buffer.selection());
482 });
483 }
484 text
485 }
486
487 pub fn get_state(&self) -> cranpose_foundation::text::TextFieldState {
490 self.state.clone()
491 }
492
493 pub fn set_content_offset(&self, offset: f32) {
496 self.refs.content_offset.set(offset);
497 }
498
499 pub fn set_content_y_offset(&self, offset: f32) {
502 self.refs.content_y_offset.set(offset);
503 }
504
505 fn measure_text_content(&self) -> Size {
507 let text = self.state.text();
508 let node_id = self.refs.node_id.get();
509 let metrics = crate::text::measure_text_for_node(
510 node_id,
511 &crate::text::AnnotatedString::from(text.as_str()),
512 &self.style,
513 );
514 self.measured_line_height.set(metrics.line_height);
515 Size {
516 width: metrics.width,
517 height: metrics.height,
518 }
519 }
520
521 fn update_cached_state(&mut self) -> bool {
523 let value = self.state.value();
524 let text_changed = value.text != self.cached_text;
525 let selection_changed = value.selection != self.cached_selection;
526
527 if text_changed {
528 self.cached_text = value.text;
529 }
530 if selection_changed {
531 self.cached_selection = value.selection;
532 }
533
534 text_changed || selection_changed
535 }
536
537 pub fn position_cursor_at_offset(&self, x_offset: f32) {
540 let text = self.state.text();
541 if text.is_empty() {
542 self.state.edit(|buffer| {
543 buffer.place_cursor_at_start();
544 });
545 return;
546 }
547
548 let byte_offset = crate::text::get_offset_for_position(
551 &crate::text::AnnotatedString::from(text.as_str()),
552 &self.style,
553 x_offset + self.refs.scroll_offset.get(),
554 0.0,
555 );
556
557 self.state.edit(|buffer| {
558 buffer.place_cursor_before_char(byte_offset);
559 });
560 }
561
562 }
566
567impl DelegatableNode for TextFieldModifierNode {
568 fn node_state(&self) -> &NodeState {
569 &self.node_state
570 }
571}
572
573impl ModifierNode for TextFieldModifierNode {
574 fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
575 self.refs.node_id.set(context.node_id());
577
578 context.invalidate(InvalidationKind::Layout);
579 context.invalidate(InvalidationKind::Draw);
580 context.invalidate(InvalidationKind::Semantics);
581 }
582
583 fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
584 Some(self)
585 }
586
587 fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
588 Some(self)
589 }
590
591 fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
592 Some(self)
593 }
594
595 fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
596 Some(self)
597 }
598
599 fn as_semantics_node(&self) -> Option<&dyn SemanticsNode> {
600 Some(self)
601 }
602
603 fn as_semantics_node_mut(&mut self) -> Option<&mut dyn SemanticsNode> {
604 Some(self)
605 }
606
607 fn as_pointer_input_node(&self) -> Option<&dyn PointerInputNode> {
608 Some(self)
609 }
610
611 fn as_pointer_input_node_mut(&mut self) -> Option<&mut dyn PointerInputNode> {
612 Some(self)
613 }
614}
615
616impl LayoutModifierNode for TextFieldModifierNode {
617 fn measure(
618 &self,
619 _context: &mut dyn ModifierNodeContext,
620 _measurable: &dyn Measurable,
621 constraints: Constraints,
622 ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
623 let text_size = self.measure_text_content();
625
626 let min_height = if text_size.height < 1.0 {
628 DEFAULT_LINE_HEIGHT
629 } else {
630 text_size.height
631 };
632
633 let width = text_size
635 .width
636 .max(constraints.min_width)
637 .min(constraints.max_width);
638 let height = min_height
639 .max(constraints.min_height)
640 .min(constraints.max_height);
641
642 let size = Size { width, height };
643 self.measured_size.set(size);
644
645 let _ = (self.cached_pan_resolver)(size.width);
648
649 cranpose_ui_layout::LayoutModifierMeasureResult::with_size(size)
650 }
651
652 fn min_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
653 self.measure_text_content().width
654 }
655
656 fn max_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
657 self.measure_text_content().width
658 }
659
660 fn min_intrinsic_height(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
661 self.measure_text_content().height.max(DEFAULT_LINE_HEIGHT)
662 }
663
664 fn max_intrinsic_height(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
665 self.measure_text_content().height.max(DEFAULT_LINE_HEIGHT)
666 }
667}
668
669impl DrawModifierNode for TextFieldModifierNode {
670 fn draw(&self, _draw_scope: &mut dyn DrawScope) {
671 }
675
676 fn create_draw_closure(
677 &self,
678 ) -> Option<Rc<dyn Fn(cranpose_foundation::Size) -> Vec<cranpose_ui_graphics::DrawPrimitive>>>
679 {
680 use cranpose_ui_graphics::DrawPrimitive;
681
682 let is_focused = self.refs.is_focused.clone();
684 let state = self.state.clone();
685 let content_offset = self.refs.content_offset.clone();
686 let content_y_offset = self.refs.content_y_offset.clone();
687 let cursor_brush = self.cursor_brush.clone();
688 let selection_brush = self.selection_brush.clone();
689 let style = self.style.clone();
690 let cached_line_height = self.measured_line_height.clone();
691 let measured_size = self.measured_size.clone();
692 let pan_resolver = self.cached_pan_resolver.clone();
693
694 Some(Rc::new(move |size| {
695 if !*is_focused.borrow() {
697 return vec![];
698 }
699
700 let mut primitives = Vec::new();
701
702 let text = state.text();
703 let selection = state.selection();
704 let padding_left = content_offset.get();
705 let padding_top = content_y_offset.get();
706 let line_height = cached_line_height.get();
709
710 let measured = measured_size.get();
713 let viewport_width = if measured.width > 0.0 {
714 measured.width
715 } else {
716 (size.width - padding_left).max(0.0)
717 };
718 let viewport_height = if measured.height > 0.0 {
719 measured.height
720 } else {
721 (size.height - padding_top).max(0.0)
722 };
723 let pan = pan_resolver(viewport_width);
725 let clip_bounds = cranpose_ui_graphics::Rect {
729 x: padding_left,
730 y: padding_top,
731 width: viewport_width,
732 height: viewport_height,
733 };
734
735 if !selection.collapsed() {
737 let sel_start = selection.min();
738 let sel_end = selection.max();
739
740 let lines: Vec<&str> = text.split('\n').collect();
741 let mut byte_offset: usize = 0;
742
743 for (line_idx, line) in lines.iter().enumerate() {
744 let line_start = byte_offset;
745 let line_end = byte_offset + line.len();
746
747 if sel_end > line_start && sel_start < line_end {
748 let sel_start_in_line = sel_start.saturating_sub(line_start);
749 let sel_end_in_line = (sel_end - line_start).min(line.len());
750
751 let sel_start_x = crate::text::measure_text(
752 &crate::text::AnnotatedString::from(&line[..sel_start_in_line]),
753 &style,
754 )
755 .width
756 + padding_left
757 - pan;
758 let sel_end_x = crate::text::measure_text(
759 &crate::text::AnnotatedString::from(&line[..sel_end_in_line]),
760 &style,
761 )
762 .width
763 + padding_left
764 - pan;
765 let sel_width = sel_end_x - sel_start_x;
766
767 if sel_width > 0.0 {
768 let sel_rect = cranpose_ui_graphics::Rect {
769 x: sel_start_x,
770 y: padding_top + line_idx as f32 * line_height,
771 width: sel_width,
772 height: line_height,
773 };
774 if let Some(clipped) = intersect_rect(sel_rect, clip_bounds) {
775 primitives.push(DrawPrimitive::Rect {
776 rect: clipped,
777 brush: selection_brush.clone(),
778 });
779 }
780 }
781 }
782 byte_offset = line_end + 1;
783 }
784 }
785
786 if let Some(comp_range) = state.composition() {
789 let comp_start = comp_range.min();
790 let comp_end = comp_range.max();
791
792 if comp_start < comp_end && comp_end <= text.len() {
793 let lines: Vec<&str> = text.split('\n').collect();
794 let mut byte_offset: usize = 0;
795
796 let underline_brush = cranpose_ui_graphics::Brush::solid(
798 cranpose_ui_graphics::Color(0.8, 0.8, 0.8, 0.8),
799 );
800 let underline_height: f32 = 2.0;
801
802 for (line_idx, line) in lines.iter().enumerate() {
803 let line_start = byte_offset;
804 let line_end = byte_offset + line.len();
805
806 if comp_end > line_start && comp_start < line_end {
808 let comp_start_in_line = comp_start.saturating_sub(line_start);
809 let comp_end_in_line = (comp_end - line_start).min(line.len());
810
811 let comp_start_in_line = if line.is_char_boundary(comp_start_in_line) {
813 comp_start_in_line
814 } else {
815 0
816 };
817 let comp_end_in_line = if line.is_char_boundary(comp_end_in_line) {
818 comp_end_in_line
819 } else {
820 line.len()
821 };
822
823 let comp_start_x = crate::text::measure_text(
824 &crate::text::AnnotatedString::from(&line[..comp_start_in_line]),
825 &style,
826 )
827 .width
828 + padding_left
829 - pan;
830 let comp_end_x = crate::text::measure_text(
831 &crate::text::AnnotatedString::from(&line[..comp_end_in_line]),
832 &style,
833 )
834 .width
835 + padding_left
836 - pan;
837 let comp_width = comp_end_x - comp_start_x;
838
839 if comp_width > 0.0 {
840 let underline_rect = cranpose_ui_graphics::Rect {
842 x: comp_start_x,
843 y: padding_top + (line_idx as f32 + 1.0) * line_height
844 - underline_height,
845 width: comp_width,
846 height: underline_height,
847 };
848 if let Some(clipped) = intersect_rect(underline_rect, clip_bounds) {
849 primitives.push(DrawPrimitive::Rect {
850 rect: clipped,
851 brush: underline_brush.clone(),
852 });
853 }
854 }
855 }
856 byte_offset = line_end + 1;
857 }
858 }
859 }
860
861 if crate::cursor_animation::is_cursor_visible() {
863 let pos = selection.start.min(text.len());
864 let text_before = &text[..pos];
865 let line_index = text_before.matches('\n').count();
866 let line_start = text_before.rfind('\n').map(|i| i + 1).unwrap_or(0);
867 let cursor_x = crate::text::measure_text(
868 &crate::text::AnnotatedString::from(&text_before[line_start..]),
869 &style,
870 )
871 .width
872 + padding_left
873 - pan;
874 let cursor_y = padding_top + line_index as f32 * line_height;
875
876 let cursor_rect = cranpose_ui_graphics::Rect {
877 x: cursor_x,
878 y: cursor_y,
879 width: CURSOR_WIDTH,
880 height: line_height,
881 };
882
883 if let Some(clipped) = intersect_rect(cursor_rect, clip_bounds) {
884 primitives.push(DrawPrimitive::Rect {
885 rect: clipped,
886 brush: cursor_brush.clone(),
887 });
888 }
889 }
890
891 primitives
892 }))
893 }
894}
895
896impl SemanticsNode for TextFieldModifierNode {
897 fn merge_semantics(&self, config: &mut SemanticsConfiguration) {
898 let text = self.state.text();
899 config.content_description = Some(text);
900 config.is_editable_text = true;
901 config.text_selection = Some(self.state.selection());
902 }
903}
904
905impl PointerInputNode for TextFieldModifierNode {
906 fn on_pointer_event(
907 &mut self,
908 _context: &mut dyn ModifierNodeContext,
909 _event: &PointerEvent,
910 ) -> bool {
911 false
922 }
923
924 fn hit_test(&self, x: f32, y: f32) -> bool {
925 let size = self.measured_size.get();
927 x >= 0.0 && x <= size.width && y >= 0.0 && y <= size.height
928 }
929
930 fn pointer_input_handler(&self) -> Option<Rc<dyn Fn(PointerEvent)>> {
931 Some(self.cached_handler.clone())
933 }
934}
935
936#[derive(Clone)]
947pub struct TextFieldElement {
948 state: TextFieldState,
950 style: TextStyle,
952 cursor_color: Color,
954 line_limits: TextFieldLineLimits,
956}
957
958impl TextFieldElement {
959 pub fn new(state: TextFieldState, style: TextStyle) -> Self {
961 Self {
962 state,
963 style,
964 cursor_color: DEFAULT_CURSOR_COLOR,
965 line_limits: TextFieldLineLimits::default(),
966 }
967 }
968
969 pub fn with_cursor_color(mut self, color: Color) -> Self {
971 self.cursor_color = color;
972 self
973 }
974
975 pub fn with_line_limits(mut self, line_limits: TextFieldLineLimits) -> Self {
977 self.line_limits = line_limits;
978 self
979 }
980}
981
982impl std::fmt::Debug for TextFieldElement {
983 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
984 f.debug_struct("TextFieldElement")
985 .field("text", &self.state.text())
986 .field("style", &self.style)
987 .field("cursor_color", &self.cursor_color)
988 .finish()
989 }
990}
991
992impl Hash for TextFieldElement {
993 fn hash<H: Hasher>(&self, state: &mut H) {
994 std::ptr::hash(std::rc::Rc::as_ptr(&self.state.inner), state);
997 self.cursor_color.0.to_bits().hash(state);
999 self.cursor_color.1.to_bits().hash(state);
1000 self.cursor_color.2.to_bits().hash(state);
1001 self.cursor_color.3.to_bits().hash(state);
1002 self.style.render_hash().hash(state);
1003 self.line_limits.hash(state);
1004 }
1005}
1006
1007impl PartialEq for TextFieldElement {
1008 fn eq(&self, other: &Self) -> bool {
1009 self.state == other.state
1013 && self.style == other.style
1014 && self.cursor_color == other.cursor_color
1015 && self.line_limits == other.line_limits
1016 }
1017}
1018
1019impl Eq for TextFieldElement {}
1020
1021impl ModifierNodeElement for TextFieldElement {
1022 type Node = TextFieldModifierNode;
1023
1024 fn create(&self) -> Self::Node {
1025 TextFieldModifierNode::new(self.state.clone(), self.style.clone())
1026 .with_cursor_color(self.cursor_color)
1027 .with_line_limits(self.line_limits)
1028 }
1029
1030 fn update(&self, node: &mut Self::Node) {
1031 node.state = self.state.clone();
1033 node.style = self.style.clone();
1034 node.cursor_brush = Brush::solid(self.cursor_color);
1035 node.line_limits = self.line_limits;
1036
1037 node.cached_handler = TextFieldModifierNode::create_handler(
1039 node.state.clone(),
1040 node.refs.clone(),
1041 node.line_limits,
1042 self.style.clone(),
1043 );
1044
1045 node.cached_pan_resolver = TextFieldModifierNode::create_pan_resolver(
1047 node.state.clone(),
1048 node.refs.clone(),
1049 node.line_limits,
1050 self.style.clone(),
1051 );
1052
1053 if node.update_cached_state() {
1055 }
1058 }
1059
1060 fn capabilities(&self) -> NodeCapabilities {
1061 NodeCapabilities::LAYOUT
1062 | NodeCapabilities::DRAW
1063 | NodeCapabilities::SEMANTICS
1064 | NodeCapabilities::POINTER_INPUT
1065 }
1066
1067 fn always_update(&self) -> bool {
1068 true
1070 }
1071}
1072
1073#[cfg(test)]
1074mod tests {
1075 use super::*;
1076 use crate::text::TextStyle;
1077 use cranpose_core::{DefaultScheduler, Runtime};
1078 use std::sync::Arc;
1079
1080 fn with_test_runtime<T>(f: impl FnOnce() -> T) -> T {
1082 let _runtime = Runtime::new(Arc::new(DefaultScheduler));
1083 f()
1084 }
1085
1086 #[test]
1087 fn text_field_node_creation() {
1088 let _app_context = crate::render_state::app_context_test_scope();
1089 with_test_runtime(|| {
1090 let state = TextFieldState::new("Hello");
1091 let node = TextFieldModifierNode::new(state, TextStyle::default());
1092 assert_eq!(node.text(), "Hello");
1093 assert!(!node.is_focused());
1094 });
1095 }
1096
1097 #[test]
1098 fn text_field_node_focus() {
1099 let _app_context = crate::render_state::app_context_test_scope();
1100 with_test_runtime(|| {
1101 let state = TextFieldState::new("Test");
1102 let mut node = TextFieldModifierNode::new(state, TextStyle::default());
1103 assert!(!node.is_focused());
1104
1105 node.set_focused(true);
1106 assert!(node.is_focused());
1107
1108 node.set_focused(false);
1109 assert!(!node.is_focused());
1110 });
1111 }
1112
1113 #[test]
1114 fn text_field_element_creates_node() {
1115 let _app_context = crate::render_state::app_context_test_scope();
1116 with_test_runtime(|| {
1117 let state = TextFieldState::new("Hello World");
1118 let element = TextFieldElement::new(state, TextStyle::default());
1119
1120 let node = element.create();
1121 assert_eq!(node.text(), "Hello World");
1122 });
1123 }
1124
1125 #[test]
1126 fn text_field_element_equality() {
1127 let _app_context = crate::render_state::app_context_test_scope();
1128 with_test_runtime(|| {
1129 let state1 = TextFieldState::new("Hello");
1130 let state2 = TextFieldState::new("Hello"); let elem1 = TextFieldElement::new(state1.clone(), TextStyle::default());
1133 let elem2 = TextFieldElement::new(state1.clone(), TextStyle::default()); let elem3 = TextFieldElement::new(state2, TextStyle::default()); assert_eq!(elem1, elem2, "Same state should be equal");
1139 assert_ne!(elem1, elem3, "Different states should not be equal");
1140 });
1141 }
1142
1143 #[test]
1144 fn text_field_element_update_refreshes_existing_node_style() {
1145 let _app_context = crate::render_state::app_context_test_scope();
1146 with_test_runtime(|| {
1147 let state = TextFieldState::new("themed text");
1148 let dark_style = TextStyle::from_span_style(crate::text::SpanStyle {
1149 color: Some(Color::from_rgba_u8(228, 240, 252, 255)),
1150 ..crate::text::SpanStyle::default()
1151 });
1152 let light_style = TextStyle::from_span_style(crate::text::SpanStyle {
1153 color: Some(Color::from_rgba_u8(14, 58, 96, 255)),
1154 ..crate::text::SpanStyle::default()
1155 });
1156 let initial = TextFieldElement::new(state.clone(), dark_style);
1157 let updated = TextFieldElement::new(state, light_style.clone());
1158 let mut node = initial.create();
1159
1160 updated.update(&mut node);
1161
1162 assert_eq!(node.text(), "themed text");
1163 assert_eq!(node.style(), &light_style);
1164 });
1165 }
1166
1167 #[test]
1173 fn test_cursor_x_position_calculation() {
1174 let _app_context = crate::render_state::app_context_test_scope();
1175 with_test_runtime(|| {
1176 let style = crate::text::TextStyle::default();
1178
1179 let empty_width =
1181 crate::text::measure_text(&crate::text::AnnotatedString::from(""), &style).width;
1182 assert!(
1183 empty_width.abs() < 0.1,
1184 "Empty text should have 0 width, got {}",
1185 empty_width
1186 );
1187
1188 let hi_width =
1190 crate::text::measure_text(&crate::text::AnnotatedString::from("Hi"), &style).width;
1191 assert!(
1192 hi_width > 0.0,
1193 "Text 'Hi' should have positive width: {}",
1194 hi_width
1195 );
1196
1197 let h_width =
1199 crate::text::measure_text(&crate::text::AnnotatedString::from("H"), &style).width;
1200 assert!(h_width > 0.0, "Text 'H' should have positive width");
1201 assert!(
1202 h_width < hi_width,
1203 "'H' width {} should be less than 'Hi' width {}",
1204 h_width,
1205 hi_width
1206 );
1207
1208 let state = TextFieldState::new("Hi");
1210 assert_eq!(
1211 state.selection().start,
1212 2,
1213 "Cursor should be at position 2 (end of 'Hi')"
1214 );
1215
1216 let text = state.text();
1218 let cursor_pos = state.selection().start;
1219 let text_before_cursor = &text[..cursor_pos.min(text.len())];
1220 assert_eq!(text_before_cursor, "Hi");
1221
1222 let cursor_x = crate::text::measure_text(
1224 &crate::text::AnnotatedString::from(text_before_cursor),
1225 &style,
1226 )
1227 .width;
1228 assert!(
1229 (cursor_x - hi_width).abs() < 0.1,
1230 "Cursor x {} should equal 'Hi' width {}",
1231 cursor_x,
1232 hi_width
1233 );
1234 });
1235 }
1236
1237 #[test]
1239 fn test_focused_node_creates_cursor() {
1240 let _app_context = crate::render_state::app_context_test_scope();
1241 with_test_runtime(|| {
1242 let state = TextFieldState::new("Test");
1243 let element = TextFieldElement::new(state.clone(), TextStyle::default());
1244 let node = element.create();
1245
1246 assert!(!node.is_focused());
1248
1249 *node.refs.is_focused.borrow_mut() = true;
1251 assert!(node.is_focused());
1252
1253 assert_eq!(node.text(), "Test");
1255
1256 assert_eq!(node.selection().start, 4);
1258 });
1259 }
1260}