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
46#[derive(Clone)]
52pub(crate) struct TextFieldRefs {
53 pub is_focused: Rc<RefCell<bool>>,
55 pub content_offset: Rc<Cell<f32>>,
57 pub content_y_offset: Rc<Cell<f32>>,
59 pub drag_anchor: Rc<Cell<Option<usize>>>,
61 pub last_click_time: Rc<Cell<Option<web_time::Instant>>>,
63 pub click_count: Rc<Cell<u8>>,
65 pub node_id: Rc<Cell<Option<cranpose_core::NodeId>>>,
67}
68
69impl TextFieldRefs {
70 pub fn new() -> Self {
72 Self {
73 is_focused: Rc::new(RefCell::new(false)),
74 content_offset: Rc::new(Cell::new(0.0_f32)),
75 content_y_offset: Rc::new(Cell::new(0.0_f32)),
76 drag_anchor: Rc::new(Cell::new(None::<usize>)),
77 last_click_time: Rc::new(Cell::new(None::<web_time::Instant>)),
78 click_count: Rc::new(Cell::new(0_u8)),
79 node_id: Rc::new(Cell::new(None::<cranpose_core::NodeId>)),
80 }
81 }
82}
83
84use crate::text::TextStyle; pub struct TextFieldModifierNode {
93 state: TextFieldState,
95 refs: TextFieldRefs,
97 style: TextStyle, cursor_brush: Brush,
101 selection_brush: Brush,
103 line_limits: TextFieldLineLimits,
105 cached_text: String,
107 cached_selection: TextRange,
109 node_state: NodeState,
111 measured_size: Cell<Size>,
113 measured_line_height: Rc<Cell<f32>>,
115 cached_handler: Rc<dyn Fn(PointerEvent)>,
117}
118
119impl std::fmt::Debug for TextFieldModifierNode {
120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 f.debug_struct("TextFieldModifierNode")
122 .field("text", &self.state.text())
123 .field("style", &self.style)
124 .field("is_focused", &*self.refs.is_focused.borrow())
125 .finish()
126 }
127}
128
129use crate::text_field_handler::TextFieldHandler;
131
132impl TextFieldModifierNode {
133 pub fn new(state: TextFieldState, style: TextStyle) -> Self {
135 let value = state.value();
136 let refs = TextFieldRefs::new();
137 let line_limits = TextFieldLineLimits::default();
138 let cached_handler =
139 Self::create_handler(state.clone(), refs.clone(), line_limits, style.clone());
140
141 Self {
142 state,
143 refs,
144 style,
145 cursor_brush: Brush::solid(DEFAULT_CURSOR_COLOR),
146 selection_brush: Brush::solid(DEFAULT_SELECTION_COLOR),
147 line_limits,
148 cached_text: value.text,
149 cached_selection: value.selection,
150 node_state: NodeState::new(),
151 measured_size: Cell::new(Size {
152 width: 0.0,
153 height: 0.0,
154 }),
155 measured_line_height: Rc::new(Cell::new(DEFAULT_LINE_HEIGHT)),
156 cached_handler,
157 }
158 }
159
160 pub fn with_line_limits(mut self, line_limits: TextFieldLineLimits) -> Self {
162 self.line_limits = line_limits;
163 self
164 }
165
166 pub fn line_limits(&self) -> TextFieldLineLimits {
168 self.line_limits
169 }
170
171 fn create_handler(
173 state: TextFieldState,
174 refs: TextFieldRefs,
175 line_limits: TextFieldLineLimits,
176 style: TextStyle, ) -> Rc<dyn Fn(PointerEvent)> {
178 use crate::word_boundaries::find_word_boundaries;
180
181 Rc::new(move |event: PointerEvent| {
182 let click_x = (event.position.x - refs.content_offset.get()).max(0.0);
184 let click_y = (event.position.y - refs.content_y_offset.get()).max(0.0);
185
186 match event.kind {
187 PointerEventKind::Down => {
188 let handler =
190 TextFieldHandler::new(state.clone(), refs.node_id.get(), line_limits);
191 crate::text_field_focus::request_focus(refs.is_focused.clone(), handler);
192
193 let now = web_time::Instant::now();
194 let text = state.text();
195 let pos = crate::text::get_offset_for_position(
196 &crate::text::AnnotatedString::from(text.as_str()),
197 &style,
198 click_x,
199 click_y,
200 );
201
202 let is_double_click = if let Some(last) = refs.last_click_time.get() {
204 now.duration_since(last).as_millis() < DOUBLE_CLICK_MS
205 } else {
206 false
207 };
208
209 if is_double_click {
210 let count = refs.click_count.get() + 1;
212 refs.click_count.set(count.min(3));
213
214 if count >= 3 {
215 state.edit(|buffer| {
217 buffer.select_all();
218 });
219 refs.drag_anchor.set(Some(0));
221 } else if count >= 2 {
222 let (word_start, word_end) = find_word_boundaries(&text, pos);
224 state.edit(|buffer| {
225 buffer.select(TextRange::new(word_start, word_end));
226 });
227 refs.drag_anchor.set(Some(word_start));
229 }
230 } else {
231 refs.click_count.set(1);
233 refs.drag_anchor.set(Some(pos));
234 state.edit(|buffer| {
235 buffer.place_cursor_before_char(pos);
236 });
237 }
238
239 refs.last_click_time.set(Some(now));
240 event.consume();
241 }
242 PointerEventKind::Move => {
243 if let Some(anchor) = refs.drag_anchor.get() {
245 if *refs.is_focused.borrow() {
246 let text = state.text();
247 let current_pos = crate::text::get_offset_for_position(
248 &crate::text::AnnotatedString::from(text.as_str()),
249 &style,
250 click_x,
251 click_y,
252 );
253
254 state.set_selection(TextRange::new(anchor, current_pos));
256
257 crate::request_render_invalidation();
259
260 event.consume();
261 }
262 }
263 }
264 PointerEventKind::Up => {
265 refs.drag_anchor.set(None);
267 }
268 _ => {}
269 }
270 })
271 }
272
273 pub fn with_cursor_color(mut self, color: Color) -> Self {
275 self.cursor_brush = Brush::solid(color);
276 self
277 }
278
279 pub fn set_focused(&mut self, focused: bool) {
281 let current = *self.refs.is_focused.borrow();
282 if current != focused {
283 *self.refs.is_focused.borrow_mut() = focused;
284 }
285 }
286
287 pub fn is_focused(&self) -> bool {
289 *self.refs.is_focused.borrow()
290 }
291
292 pub fn is_focused_rc(&self) -> Rc<RefCell<bool>> {
294 self.refs.is_focused.clone()
295 }
296
297 pub fn content_offset_rc(&self) -> Rc<Cell<f32>> {
299 self.refs.content_offset.clone()
300 }
301
302 pub fn content_y_offset_rc(&self) -> Rc<Cell<f32>> {
304 self.refs.content_y_offset.clone()
305 }
306
307 pub fn text(&self) -> String {
309 self.state.text()
310 }
311
312 pub fn style(&self) -> &TextStyle {
313 &self.style
314 }
315
316 pub fn selection(&self) -> TextRange {
318 self.state.selection()
319 }
320
321 pub fn cursor_brush(&self) -> Brush {
323 self.cursor_brush.clone()
324 }
325
326 pub fn selection_brush(&self) -> Brush {
328 self.selection_brush.clone()
329 }
330
331 pub fn insert_text(&mut self, text: &str) {
333 self.state.edit(|buffer| {
334 buffer.insert(text);
335 });
336 }
337
338 pub fn copy_selection(&self) -> Option<String> {
341 self.state.copy_selection()
342 }
343
344 pub fn cut_selection(&mut self) -> Option<String> {
347 let text = self.copy_selection();
348 if text.is_some() {
349 self.state.edit(|buffer| {
350 buffer.delete(buffer.selection());
351 });
352 }
353 text
354 }
355
356 pub fn get_state(&self) -> cranpose_foundation::text::TextFieldState {
359 self.state.clone()
360 }
361
362 pub fn set_content_offset(&self, offset: f32) {
365 self.refs.content_offset.set(offset);
366 }
367
368 pub fn set_content_y_offset(&self, offset: f32) {
371 self.refs.content_y_offset.set(offset);
372 }
373
374 fn measure_text_content(&self) -> Size {
376 let text = self.state.text();
377 let node_id = self.refs.node_id.get();
378 let metrics = crate::text::measure_text_for_node(
379 node_id,
380 &crate::text::AnnotatedString::from(text.as_str()),
381 &self.style,
382 );
383 self.measured_line_height.set(metrics.line_height);
384 Size {
385 width: metrics.width,
386 height: metrics.height,
387 }
388 }
389
390 fn update_cached_state(&mut self) -> bool {
392 let value = self.state.value();
393 let text_changed = value.text != self.cached_text;
394 let selection_changed = value.selection != self.cached_selection;
395
396 if text_changed {
397 self.cached_text = value.text;
398 }
399 if selection_changed {
400 self.cached_selection = value.selection;
401 }
402
403 text_changed || selection_changed
404 }
405
406 pub fn position_cursor_at_offset(&self, x_offset: f32) {
409 let text = self.state.text();
410 if text.is_empty() {
411 self.state.edit(|buffer| {
412 buffer.place_cursor_at_start();
413 });
414 return;
415 }
416
417 let byte_offset = crate::text::get_offset_for_position(
419 &crate::text::AnnotatedString::from(text.as_str()),
420 &self.style,
421 x_offset,
422 0.0,
423 );
424
425 self.state.edit(|buffer| {
426 buffer.place_cursor_before_char(byte_offset);
427 });
428 }
429
430 }
434
435impl DelegatableNode for TextFieldModifierNode {
436 fn node_state(&self) -> &NodeState {
437 &self.node_state
438 }
439}
440
441impl ModifierNode for TextFieldModifierNode {
442 fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
443 self.refs.node_id.set(context.node_id());
445
446 context.invalidate(InvalidationKind::Layout);
447 context.invalidate(InvalidationKind::Draw);
448 context.invalidate(InvalidationKind::Semantics);
449 }
450
451 fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
452 Some(self)
453 }
454
455 fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
456 Some(self)
457 }
458
459 fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
460 Some(self)
461 }
462
463 fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
464 Some(self)
465 }
466
467 fn as_semantics_node(&self) -> Option<&dyn SemanticsNode> {
468 Some(self)
469 }
470
471 fn as_semantics_node_mut(&mut self) -> Option<&mut dyn SemanticsNode> {
472 Some(self)
473 }
474
475 fn as_pointer_input_node(&self) -> Option<&dyn PointerInputNode> {
476 Some(self)
477 }
478
479 fn as_pointer_input_node_mut(&mut self) -> Option<&mut dyn PointerInputNode> {
480 Some(self)
481 }
482}
483
484impl LayoutModifierNode for TextFieldModifierNode {
485 fn measure(
486 &self,
487 _context: &mut dyn ModifierNodeContext,
488 _measurable: &dyn Measurable,
489 constraints: Constraints,
490 ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
491 let text_size = self.measure_text_content();
493
494 let min_height = if text_size.height < 1.0 {
496 DEFAULT_LINE_HEIGHT
497 } else {
498 text_size.height
499 };
500
501 let width = text_size
503 .width
504 .max(constraints.min_width)
505 .min(constraints.max_width);
506 let height = min_height
507 .max(constraints.min_height)
508 .min(constraints.max_height);
509
510 let size = Size { width, height };
511 self.measured_size.set(size);
512
513 cranpose_ui_layout::LayoutModifierMeasureResult::with_size(size)
514 }
515
516 fn min_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
517 self.measure_text_content().width
518 }
519
520 fn max_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
521 self.measure_text_content().width
522 }
523
524 fn min_intrinsic_height(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
525 self.measure_text_content().height.max(DEFAULT_LINE_HEIGHT)
526 }
527
528 fn max_intrinsic_height(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
529 self.measure_text_content().height.max(DEFAULT_LINE_HEIGHT)
530 }
531}
532
533impl DrawModifierNode for TextFieldModifierNode {
534 fn draw(&self, _draw_scope: &mut dyn DrawScope) {
535 }
539
540 fn create_draw_closure(
541 &self,
542 ) -> Option<Rc<dyn Fn(cranpose_foundation::Size) -> Vec<cranpose_ui_graphics::DrawPrimitive>>>
543 {
544 use cranpose_ui_graphics::DrawPrimitive;
545
546 let is_focused = self.refs.is_focused.clone();
548 let state = self.state.clone();
549 let content_offset = self.refs.content_offset.clone();
550 let content_y_offset = self.refs.content_y_offset.clone();
551 let cursor_brush = self.cursor_brush.clone();
552 let selection_brush = self.selection_brush.clone();
553 let style = self.style.clone();
554 let cached_line_height = self.measured_line_height.clone();
555
556 Some(Rc::new(move |_size| {
557 if !*is_focused.borrow() {
559 return vec![];
560 }
561
562 let mut primitives = Vec::new();
563
564 let text = state.text();
565 let selection = state.selection();
566 let padding_left = content_offset.get();
567 let padding_top = content_y_offset.get();
568 let line_height = cached_line_height.get();
571
572 if !selection.collapsed() {
574 let sel_start = selection.min();
575 let sel_end = selection.max();
576
577 let lines: Vec<&str> = text.split('\n').collect();
578 let mut byte_offset: usize = 0;
579
580 for (line_idx, line) in lines.iter().enumerate() {
581 let line_start = byte_offset;
582 let line_end = byte_offset + line.len();
583
584 if sel_end > line_start && sel_start < line_end {
585 let sel_start_in_line = sel_start.saturating_sub(line_start);
586 let sel_end_in_line = (sel_end - line_start).min(line.len());
587
588 let sel_start_x = crate::text::measure_text(
589 &crate::text::AnnotatedString::from(&line[..sel_start_in_line]),
590 &style,
591 )
592 .width
593 + padding_left;
594 let sel_end_x = crate::text::measure_text(
595 &crate::text::AnnotatedString::from(&line[..sel_end_in_line]),
596 &style,
597 )
598 .width
599 + padding_left;
600 let sel_width = sel_end_x - sel_start_x;
601
602 if sel_width > 0.0 {
603 let sel_rect = cranpose_ui_graphics::Rect {
604 x: sel_start_x,
605 y: padding_top + line_idx as f32 * line_height,
606 width: sel_width,
607 height: line_height,
608 };
609 primitives.push(DrawPrimitive::Rect {
610 rect: sel_rect,
611 brush: selection_brush.clone(),
612 });
613 }
614 }
615 byte_offset = line_end + 1;
616 }
617 }
618
619 if let Some(comp_range) = state.composition() {
622 let comp_start = comp_range.min();
623 let comp_end = comp_range.max();
624
625 if comp_start < comp_end && comp_end <= text.len() {
626 let lines: Vec<&str> = text.split('\n').collect();
627 let mut byte_offset: usize = 0;
628
629 let underline_brush = cranpose_ui_graphics::Brush::solid(
631 cranpose_ui_graphics::Color(0.8, 0.8, 0.8, 0.8),
632 );
633 let underline_height: f32 = 2.0;
634
635 for (line_idx, line) in lines.iter().enumerate() {
636 let line_start = byte_offset;
637 let line_end = byte_offset + line.len();
638
639 if comp_end > line_start && comp_start < line_end {
641 let comp_start_in_line = comp_start.saturating_sub(line_start);
642 let comp_end_in_line = (comp_end - line_start).min(line.len());
643
644 let comp_start_in_line = if line.is_char_boundary(comp_start_in_line) {
646 comp_start_in_line
647 } else {
648 0
649 };
650 let comp_end_in_line = if line.is_char_boundary(comp_end_in_line) {
651 comp_end_in_line
652 } else {
653 line.len()
654 };
655
656 let comp_start_x = crate::text::measure_text(
657 &crate::text::AnnotatedString::from(&line[..comp_start_in_line]),
658 &style,
659 )
660 .width
661 + padding_left;
662 let comp_end_x = crate::text::measure_text(
663 &crate::text::AnnotatedString::from(&line[..comp_end_in_line]),
664 &style,
665 )
666 .width
667 + padding_left;
668 let comp_width = comp_end_x - comp_start_x;
669
670 if comp_width > 0.0 {
671 let underline_rect = cranpose_ui_graphics::Rect {
673 x: comp_start_x,
674 y: padding_top + (line_idx as f32 + 1.0) * line_height
675 - underline_height,
676 width: comp_width,
677 height: underline_height,
678 };
679 primitives.push(DrawPrimitive::Rect {
680 rect: underline_rect,
681 brush: underline_brush.clone(),
682 });
683 }
684 }
685 byte_offset = line_end + 1;
686 }
687 }
688 }
689
690 if crate::cursor_animation::is_cursor_visible() {
692 let pos = selection.start.min(text.len());
693 let text_before = &text[..pos];
694 let line_index = text_before.matches('\n').count();
695 let line_start = text_before.rfind('\n').map(|i| i + 1).unwrap_or(0);
696 let cursor_x = crate::text::measure_text(
697 &crate::text::AnnotatedString::from(&text_before[line_start..]),
698 &style,
699 )
700 .width
701 + padding_left;
702 let cursor_y = padding_top + line_index as f32 * line_height;
703
704 let cursor_rect = cranpose_ui_graphics::Rect {
705 x: cursor_x,
706 y: cursor_y,
707 width: CURSOR_WIDTH,
708 height: line_height,
709 };
710
711 primitives.push(DrawPrimitive::Rect {
712 rect: cursor_rect,
713 brush: cursor_brush.clone(),
714 });
715 }
716
717 primitives
718 }))
719 }
720}
721
722impl SemanticsNode for TextFieldModifierNode {
723 fn merge_semantics(&self, config: &mut SemanticsConfiguration) {
724 let text = self.state.text();
725 config.content_description = Some(text);
726 }
730}
731
732impl PointerInputNode for TextFieldModifierNode {
733 fn on_pointer_event(
734 &mut self,
735 _context: &mut dyn ModifierNodeContext,
736 _event: &PointerEvent,
737 ) -> bool {
738 false
749 }
750
751 fn hit_test(&self, x: f32, y: f32) -> bool {
752 let size = self.measured_size.get();
754 x >= 0.0 && x <= size.width && y >= 0.0 && y <= size.height
755 }
756
757 fn pointer_input_handler(&self) -> Option<Rc<dyn Fn(PointerEvent)>> {
758 Some(self.cached_handler.clone())
760 }
761}
762
763#[derive(Clone)]
774pub struct TextFieldElement {
775 state: TextFieldState,
777 style: TextStyle, cursor_color: Color,
781 line_limits: TextFieldLineLimits,
783}
784
785impl TextFieldElement {
786 pub fn new(state: TextFieldState, style: TextStyle) -> Self {
788 Self {
790 state,
791 style,
792 cursor_color: DEFAULT_CURSOR_COLOR,
793 line_limits: TextFieldLineLimits::default(),
794 }
795 }
796
797 pub fn with_cursor_color(mut self, color: Color) -> Self {
799 self.cursor_color = color;
800 self
801 }
802
803 pub fn with_line_limits(mut self, line_limits: TextFieldLineLimits) -> Self {
805 self.line_limits = line_limits;
806 self
807 }
808}
809
810impl std::fmt::Debug for TextFieldElement {
811 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
812 f.debug_struct("TextFieldElement")
813 .field("text", &self.state.text())
814 .field("style", &self.style)
815 .field("cursor_color", &self.cursor_color)
816 .finish()
817 }
818}
819
820impl Hash for TextFieldElement {
821 fn hash<H: Hasher>(&self, state: &mut H) {
822 std::ptr::hash(std::rc::Rc::as_ptr(&self.state.inner), state);
825 self.cursor_color.0.to_bits().hash(state);
827 self.cursor_color.1.to_bits().hash(state);
828 self.cursor_color.2.to_bits().hash(state);
829 self.cursor_color.3.to_bits().hash(state);
830 }
832}
833
834impl PartialEq for TextFieldElement {
835 fn eq(&self, other: &Self) -> bool {
836 self.state == other.state
840 && self.style == other.style
841 && self.cursor_color == other.cursor_color
842 && self.line_limits == other.line_limits
843 }
844}
845
846impl Eq for TextFieldElement {}
847
848impl ModifierNodeElement for TextFieldElement {
849 type Node = TextFieldModifierNode;
850
851 fn create(&self) -> Self::Node {
852 TextFieldModifierNode::new(self.state.clone(), self.style.clone())
853 .with_cursor_color(self.cursor_color)
854 .with_line_limits(self.line_limits)
855 }
856
857 fn update(&self, node: &mut Self::Node) {
858 node.state = self.state.clone();
860 node.cursor_brush = Brush::solid(self.cursor_color);
861 node.line_limits = self.line_limits;
862
863 node.cached_handler = TextFieldModifierNode::create_handler(
865 node.state.clone(),
866 node.refs.clone(),
867 node.line_limits,
868 node.style.clone(),
869 );
870
871 if node.update_cached_state() {
873 }
876 }
877
878 fn capabilities(&self) -> NodeCapabilities {
879 NodeCapabilities::LAYOUT
880 | NodeCapabilities::DRAW
881 | NodeCapabilities::SEMANTICS
882 | NodeCapabilities::POINTER_INPUT
883 }
884
885 fn always_update(&self) -> bool {
886 true
888 }
889}
890
891#[cfg(test)]
892mod tests {
893 use super::*;
894 use crate::text::TextStyle;
895 use cranpose_core::{DefaultScheduler, Runtime};
896 use std::sync::Arc;
897
898 fn with_test_runtime<T>(f: impl FnOnce() -> T) -> T {
900 let _runtime = Runtime::new(Arc::new(DefaultScheduler));
901 f()
902 }
903
904 #[test]
905 fn text_field_node_creation() {
906 with_test_runtime(|| {
907 let state = TextFieldState::new("Hello");
908 let node = TextFieldModifierNode::new(state, TextStyle::default());
909 assert_eq!(node.text(), "Hello");
910 assert!(!node.is_focused());
911 });
912 }
913
914 #[test]
915 fn text_field_node_focus() {
916 with_test_runtime(|| {
917 let state = TextFieldState::new("Test");
918 let mut node = TextFieldModifierNode::new(state, TextStyle::default());
919 assert!(!node.is_focused());
920
921 node.set_focused(true);
922 assert!(node.is_focused());
923
924 node.set_focused(false);
925 assert!(!node.is_focused());
926 });
927 }
928
929 #[test]
930 fn text_field_element_creates_node() {
931 with_test_runtime(|| {
932 let state = TextFieldState::new("Hello World");
933 let element = TextFieldElement::new(state, TextStyle::default());
934
935 let node = element.create();
936 assert_eq!(node.text(), "Hello World");
937 });
938 }
939
940 #[test]
941 fn text_field_element_equality() {
942 with_test_runtime(|| {
943 let state1 = TextFieldState::new("Hello");
944 let state2 = TextFieldState::new("Hello"); let elem1 = TextFieldElement::new(state1.clone(), TextStyle::default());
947 let elem2 = TextFieldElement::new(state1.clone(), TextStyle::default()); let elem3 = TextFieldElement::new(state2, TextStyle::default()); assert_eq!(elem1, elem2, "Same state should be equal");
953 assert_ne!(elem1, elem3, "Different states should not be equal");
954 });
955 }
956
957 #[test]
963 fn test_cursor_x_position_calculation() {
964 with_test_runtime(|| {
965 let style = crate::text::TextStyle::default();
967
968 let empty_width =
970 crate::text::measure_text(&crate::text::AnnotatedString::from(""), &style).width;
971 assert!(
972 empty_width.abs() < 0.1,
973 "Empty text should have 0 width, got {}",
974 empty_width
975 );
976
977 let hi_width =
979 crate::text::measure_text(&crate::text::AnnotatedString::from("Hi"), &style).width;
980 assert!(
981 hi_width > 0.0,
982 "Text 'Hi' should have positive width: {}",
983 hi_width
984 );
985
986 let h_width =
988 crate::text::measure_text(&crate::text::AnnotatedString::from("H"), &style).width;
989 assert!(h_width > 0.0, "Text 'H' should have positive width");
990 assert!(
991 h_width < hi_width,
992 "'H' width {} should be less than 'Hi' width {}",
993 h_width,
994 hi_width
995 );
996
997 let state = TextFieldState::new("Hi");
999 assert_eq!(
1000 state.selection().start,
1001 2,
1002 "Cursor should be at position 2 (end of 'Hi')"
1003 );
1004
1005 let text = state.text();
1007 let cursor_pos = state.selection().start;
1008 let text_before_cursor = &text[..cursor_pos.min(text.len())];
1009 assert_eq!(text_before_cursor, "Hi");
1010
1011 let cursor_x = crate::text::measure_text(
1013 &crate::text::AnnotatedString::from(text_before_cursor),
1014 &style,
1015 )
1016 .width;
1017 assert!(
1018 (cursor_x - hi_width).abs() < 0.1,
1019 "Cursor x {} should equal 'Hi' width {}",
1020 cursor_x,
1021 hi_width
1022 );
1023 });
1024 }
1025
1026 #[test]
1028 fn test_focused_node_creates_cursor() {
1029 with_test_runtime(|| {
1030 let state = TextFieldState::new("Test");
1031 let element = TextFieldElement::new(state.clone(), TextStyle::default());
1032 let node = element.create();
1033
1034 assert!(!node.is_focused());
1036
1037 *node.refs.is_focused.borrow_mut() = true;
1039 assert!(node.is_focused());
1040
1041 assert_eq!(node.text(), "Test");
1043
1044 assert_eq!(node.selection().start, 4);
1046 });
1047 }
1048}