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