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(&text, &style, click_x, click_y);
193
194 let is_double_click = if let Some(last) = refs.last_click_time.get() {
196 now.duration_since(last).as_millis() < DOUBLE_CLICK_MS
197 } else {
198 false
199 };
200
201 if is_double_click {
202 let count = refs.click_count.get() + 1;
204 refs.click_count.set(count.min(3));
205
206 if count >= 3 {
207 state.edit(|buffer| {
209 buffer.select_all();
210 });
211 refs.drag_anchor.set(Some(0));
213 } else if count >= 2 {
214 let (word_start, word_end) = find_word_boundaries(&text, pos);
216 state.edit(|buffer| {
217 buffer.select(TextRange::new(word_start, word_end));
218 });
219 refs.drag_anchor.set(Some(word_start));
221 }
222 } else {
223 refs.click_count.set(1);
225 refs.drag_anchor.set(Some(pos));
226 state.edit(|buffer| {
227 buffer.place_cursor_before_char(pos);
228 });
229 }
230
231 refs.last_click_time.set(Some(now));
232 event.consume();
233 }
234 PointerEventKind::Move => {
235 if let Some(anchor) = refs.drag_anchor.get() {
237 if *refs.is_focused.borrow() {
238 let text = state.text();
239 let current_pos = crate::text::get_offset_for_position(
240 &text, &style, click_x, click_y,
241 );
242
243 state.set_selection(TextRange::new(anchor, current_pos));
245
246 crate::request_render_invalidation();
248
249 event.consume();
250 }
251 }
252 }
253 PointerEventKind::Up => {
254 refs.drag_anchor.set(None);
256 }
257 _ => {}
258 }
259 })
260 }
261
262 pub fn with_cursor_color(mut self, color: Color) -> Self {
264 self.cursor_brush = Brush::solid(color);
265 self
266 }
267
268 pub fn set_focused(&mut self, focused: bool) {
270 let current = *self.refs.is_focused.borrow();
271 if current != focused {
272 *self.refs.is_focused.borrow_mut() = focused;
273 }
274 }
275
276 pub fn is_focused(&self) -> bool {
278 *self.refs.is_focused.borrow()
279 }
280
281 pub fn is_focused_rc(&self) -> Rc<RefCell<bool>> {
283 self.refs.is_focused.clone()
284 }
285
286 pub fn content_offset_rc(&self) -> Rc<Cell<f32>> {
288 self.refs.content_offset.clone()
289 }
290
291 pub fn content_y_offset_rc(&self) -> Rc<Cell<f32>> {
293 self.refs.content_y_offset.clone()
294 }
295
296 pub fn text(&self) -> String {
298 self.state.text()
299 }
300
301 pub fn selection(&self) -> TextRange {
303 self.state.selection()
304 }
305
306 pub fn cursor_brush(&self) -> Brush {
308 self.cursor_brush.clone()
309 }
310
311 pub fn selection_brush(&self) -> Brush {
313 self.selection_brush.clone()
314 }
315
316 pub fn insert_text(&mut self, text: &str) {
318 self.state.edit(|buffer| {
319 buffer.insert(text);
320 });
321 }
322
323 pub fn copy_selection(&self) -> Option<String> {
326 self.state.copy_selection()
327 }
328
329 pub fn cut_selection(&mut self) -> Option<String> {
332 let text = self.copy_selection();
333 if text.is_some() {
334 self.state.edit(|buffer| {
335 buffer.delete(buffer.selection());
336 });
337 }
338 text
339 }
340
341 pub fn get_state(&self) -> cranpose_foundation::text::TextFieldState {
344 self.state.clone()
345 }
346
347 pub fn set_content_offset(&self, offset: f32) {
350 self.refs.content_offset.set(offset);
351 }
352
353 pub fn set_content_y_offset(&self, offset: f32) {
356 self.refs.content_y_offset.set(offset);
357 }
358
359 fn measure_text_content(&self) -> Size {
361 let text = self.state.text();
362 let metrics = crate::text::measure_text(&text, &self.style);
363 Size {
364 width: metrics.width,
365 height: metrics.height,
366 }
367 }
368
369 fn update_cached_state(&mut self) -> bool {
371 let value = self.state.value();
372 let text_changed = value.text != self.cached_text;
373 let selection_changed = value.selection != self.cached_selection;
374
375 if text_changed {
376 self.cached_text = value.text;
377 }
378 if selection_changed {
379 self.cached_selection = value.selection;
380 }
381
382 text_changed || selection_changed
383 }
384
385 pub fn position_cursor_at_offset(&self, x_offset: f32) {
388 let text = self.state.text();
389 if text.is_empty() {
390 self.state.edit(|buffer| {
391 buffer.place_cursor_at_start();
392 });
393 return;
394 }
395
396 let byte_offset = crate::text::get_offset_for_position(&text, &self.style, x_offset, 0.0);
398
399 self.state.edit(|buffer| {
400 buffer.place_cursor_before_char(byte_offset);
401 });
402 }
403
404 }
408
409impl DelegatableNode for TextFieldModifierNode {
410 fn node_state(&self) -> &NodeState {
411 &self.node_state
412 }
413}
414
415impl ModifierNode for TextFieldModifierNode {
416 fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
417 self.refs.node_id.set(context.node_id());
419
420 context.invalidate(InvalidationKind::Layout);
421 context.invalidate(InvalidationKind::Draw);
422 context.invalidate(InvalidationKind::Semantics);
423 }
424
425 fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
426 Some(self)
427 }
428
429 fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
430 Some(self)
431 }
432
433 fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
434 Some(self)
435 }
436
437 fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
438 Some(self)
439 }
440
441 fn as_semantics_node(&self) -> Option<&dyn SemanticsNode> {
442 Some(self)
443 }
444
445 fn as_semantics_node_mut(&mut self) -> Option<&mut dyn SemanticsNode> {
446 Some(self)
447 }
448
449 fn as_pointer_input_node(&self) -> Option<&dyn PointerInputNode> {
450 Some(self)
451 }
452
453 fn as_pointer_input_node_mut(&mut self) -> Option<&mut dyn PointerInputNode> {
454 Some(self)
455 }
456}
457
458impl LayoutModifierNode for TextFieldModifierNode {
459 fn measure(
460 &self,
461 _context: &mut dyn ModifierNodeContext,
462 _measurable: &dyn Measurable,
463 constraints: Constraints,
464 ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
465 let text_size = self.measure_text_content();
467
468 let min_height = if text_size.height < 1.0 {
470 DEFAULT_LINE_HEIGHT
471 } else {
472 text_size.height
473 };
474
475 let width = text_size
477 .width
478 .max(constraints.min_width)
479 .min(constraints.max_width);
480 let height = min_height
481 .max(constraints.min_height)
482 .min(constraints.max_height);
483
484 let size = Size { width, height };
485 self.measured_size.set(size);
486
487 cranpose_ui_layout::LayoutModifierMeasureResult::with_size(size)
488 }
489
490 fn min_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
491 self.measure_text_content().width
492 }
493
494 fn max_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
495 self.measure_text_content().width
496 }
497
498 fn min_intrinsic_height(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
499 self.measure_text_content().height.max(DEFAULT_LINE_HEIGHT)
500 }
501
502 fn max_intrinsic_height(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
503 self.measure_text_content().height.max(DEFAULT_LINE_HEIGHT)
504 }
505}
506
507impl DrawModifierNode for TextFieldModifierNode {
508 fn draw(&self, _draw_scope: &mut dyn DrawScope) {
509 }
513
514 fn create_draw_closure(
515 &self,
516 ) -> Option<Rc<dyn Fn(cranpose_foundation::Size) -> Vec<cranpose_ui_graphics::DrawPrimitive>>>
517 {
518 use cranpose_ui_graphics::DrawPrimitive;
519
520 let is_focused = self.refs.is_focused.clone();
522 let state = self.state.clone();
523 let content_offset = self.refs.content_offset.clone();
524 let content_y_offset = self.refs.content_y_offset.clone();
525 let cursor_brush = self.cursor_brush.clone();
526 let selection_brush = self.selection_brush.clone();
527 let style = self.style.clone(); Some(Rc::new(move |_size| {
530 if !*is_focused.borrow() {
532 return vec![];
533 }
534
535 let mut primitives = Vec::new();
536
537 let text = state.text();
538 let selection = state.selection();
539 let padding_left = content_offset.get();
540 let padding_top = content_y_offset.get();
541 let line_height = crate::text::measure_text(&text, &style).line_height;
542
543 if !selection.collapsed() {
545 let sel_start = selection.min();
546 let sel_end = selection.max();
547
548 let lines: Vec<&str> = text.split('\n').collect();
549 let mut byte_offset: usize = 0;
550
551 for (line_idx, line) in lines.iter().enumerate() {
552 let line_start = byte_offset;
553 let line_end = byte_offset + line.len();
554
555 if sel_end > line_start && sel_start < line_end {
556 let sel_start_in_line = sel_start.saturating_sub(line_start);
557 let sel_end_in_line = (sel_end - line_start).min(line.len());
558
559 let sel_start_x =
560 crate::text::measure_text(&line[..sel_start_in_line], &style).width
561 + padding_left;
562 let sel_end_x = crate::text::measure_text(&line[..sel_end_in_line], &style)
563 .width
564 + padding_left;
565 let sel_width = sel_end_x - sel_start_x;
566
567 if sel_width > 0.0 {
568 let sel_rect = cranpose_ui_graphics::Rect {
569 x: sel_start_x,
570 y: padding_top + line_idx as f32 * line_height,
571 width: sel_width,
572 height: line_height,
573 };
574 primitives.push(DrawPrimitive::Rect {
575 rect: sel_rect,
576 brush: selection_brush.clone(),
577 });
578 }
579 }
580 byte_offset = line_end + 1;
581 }
582 }
583
584 if let Some(comp_range) = state.composition() {
587 let comp_start = comp_range.min();
588 let comp_end = comp_range.max();
589
590 if comp_start < comp_end && comp_end <= text.len() {
591 let lines: Vec<&str> = text.split('\n').collect();
592 let mut byte_offset: usize = 0;
593
594 let underline_brush = cranpose_ui_graphics::Brush::solid(
596 cranpose_ui_graphics::Color(0.8, 0.8, 0.8, 0.8),
597 );
598 let underline_height: f32 = 2.0;
599
600 for (line_idx, line) in lines.iter().enumerate() {
601 let line_start = byte_offset;
602 let line_end = byte_offset + line.len();
603
604 if comp_end > line_start && comp_start < line_end {
606 let comp_start_in_line = comp_start.saturating_sub(line_start);
607 let comp_end_in_line = (comp_end - line_start).min(line.len());
608
609 let comp_start_in_line = if line.is_char_boundary(comp_start_in_line) {
611 comp_start_in_line
612 } else {
613 0
614 };
615 let comp_end_in_line = if line.is_char_boundary(comp_end_in_line) {
616 comp_end_in_line
617 } else {
618 line.len()
619 };
620
621 let comp_start_x =
622 crate::text::measure_text(&line[..comp_start_in_line], &style)
623 .width
624 + padding_left;
625 let comp_end_x =
626 crate::text::measure_text(&line[..comp_end_in_line], &style).width
627 + padding_left;
628 let comp_width = comp_end_x - comp_start_x;
629
630 if comp_width > 0.0 {
631 let underline_rect = cranpose_ui_graphics::Rect {
633 x: comp_start_x,
634 y: padding_top + (line_idx as f32 + 1.0) * line_height
635 - underline_height,
636 width: comp_width,
637 height: underline_height,
638 };
639 primitives.push(DrawPrimitive::Rect {
640 rect: underline_rect,
641 brush: underline_brush.clone(),
642 });
643 }
644 }
645 byte_offset = line_end + 1;
646 }
647 }
648 }
649
650 if crate::cursor_animation::is_cursor_visible() {
652 let pos = selection.start.min(text.len());
653 let text_before = &text[..pos];
654 let line_index = text_before.matches('\n').count();
655 let line_start = text_before.rfind('\n').map(|i| i + 1).unwrap_or(0);
656 let cursor_x = crate::text::measure_text(&text_before[line_start..], &style).width
657 + padding_left;
658 let cursor_y = padding_top + line_index as f32 * line_height;
659
660 let cursor_rect = cranpose_ui_graphics::Rect {
661 x: cursor_x,
662 y: cursor_y,
663 width: CURSOR_WIDTH,
664 height: line_height,
665 };
666
667 primitives.push(DrawPrimitive::Rect {
668 rect: cursor_rect,
669 brush: cursor_brush.clone(),
670 });
671 }
672
673 primitives
674 }))
675 }
676}
677
678impl SemanticsNode for TextFieldModifierNode {
679 fn merge_semantics(&self, config: &mut SemanticsConfiguration) {
680 let text = self.state.text();
681 config.content_description = Some(text);
682 }
686}
687
688impl PointerInputNode for TextFieldModifierNode {
689 fn on_pointer_event(
690 &mut self,
691 _context: &mut dyn ModifierNodeContext,
692 _event: &PointerEvent,
693 ) -> bool {
694 false
705 }
706
707 fn hit_test(&self, x: f32, y: f32) -> bool {
708 let size = self.measured_size.get();
710 x >= 0.0 && x <= size.width && y >= 0.0 && y <= size.height
711 }
712
713 fn pointer_input_handler(&self) -> Option<Rc<dyn Fn(PointerEvent)>> {
714 Some(self.cached_handler.clone())
716 }
717}
718
719#[derive(Clone)]
730pub struct TextFieldElement {
731 state: TextFieldState,
733 style: TextStyle, cursor_color: Color,
737 line_limits: TextFieldLineLimits,
739}
740
741impl TextFieldElement {
742 pub fn new(state: TextFieldState, style: TextStyle) -> Self {
744 Self {
746 state,
747 style,
748 cursor_color: DEFAULT_CURSOR_COLOR,
749 line_limits: TextFieldLineLimits::default(),
750 }
751 }
752
753 pub fn with_cursor_color(mut self, color: Color) -> Self {
755 self.cursor_color = color;
756 self
757 }
758
759 pub fn with_line_limits(mut self, line_limits: TextFieldLineLimits) -> Self {
761 self.line_limits = line_limits;
762 self
763 }
764}
765
766impl std::fmt::Debug for TextFieldElement {
767 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
768 f.debug_struct("TextFieldElement")
769 .field("text", &self.state.text())
770 .field("style", &self.style)
771 .field("cursor_color", &self.cursor_color)
772 .finish()
773 }
774}
775
776impl Hash for TextFieldElement {
777 fn hash<H: Hasher>(&self, state: &mut H) {
778 std::ptr::hash(std::rc::Rc::as_ptr(&self.state.inner), state);
781 self.cursor_color.0.to_bits().hash(state);
783 self.cursor_color.1.to_bits().hash(state);
784 self.cursor_color.2.to_bits().hash(state);
785 self.cursor_color.3.to_bits().hash(state);
786 }
788}
789
790impl PartialEq for TextFieldElement {
791 fn eq(&self, other: &Self) -> bool {
792 self.state == other.state
796 && self.style == other.style
797 && self.cursor_color == other.cursor_color
798 && self.line_limits == other.line_limits
799 }
800}
801
802impl Eq for TextFieldElement {}
803
804impl ModifierNodeElement for TextFieldElement {
805 type Node = TextFieldModifierNode;
806
807 fn create(&self) -> Self::Node {
808 TextFieldModifierNode::new(self.state.clone(), self.style.clone())
809 .with_cursor_color(self.cursor_color)
810 .with_line_limits(self.line_limits)
811 }
812
813 fn update(&self, node: &mut Self::Node) {
814 node.state = self.state.clone();
816 node.cursor_brush = Brush::solid(self.cursor_color);
817 node.line_limits = self.line_limits;
818
819 node.cached_handler = TextFieldModifierNode::create_handler(
821 node.state.clone(),
822 node.refs.clone(),
823 node.line_limits,
824 node.style.clone(),
825 );
826
827 if node.update_cached_state() {
829 }
832 }
833
834 fn capabilities(&self) -> NodeCapabilities {
835 NodeCapabilities::LAYOUT
836 | NodeCapabilities::DRAW
837 | NodeCapabilities::SEMANTICS
838 | NodeCapabilities::POINTER_INPUT
839 }
840
841 fn always_update(&self) -> bool {
842 true
844 }
845}
846
847#[cfg(test)]
848mod tests {
849 use super::*;
850 use crate::text::TextStyle;
851 use cranpose_core::{DefaultScheduler, Runtime};
852 use std::sync::Arc;
853
854 fn with_test_runtime<T>(f: impl FnOnce() -> T) -> T {
856 let _runtime = Runtime::new(Arc::new(DefaultScheduler));
857 f()
858 }
859
860 #[test]
861 fn text_field_node_creation() {
862 with_test_runtime(|| {
863 let state = TextFieldState::new("Hello");
864 let node = TextFieldModifierNode::new(state, TextStyle::default());
865 assert_eq!(node.text(), "Hello");
866 assert!(!node.is_focused());
867 });
868 }
869
870 #[test]
871 fn text_field_node_focus() {
872 with_test_runtime(|| {
873 let state = TextFieldState::new("Test");
874 let mut node = TextFieldModifierNode::new(state, TextStyle::default());
875 assert!(!node.is_focused());
876
877 node.set_focused(true);
878 assert!(node.is_focused());
879
880 node.set_focused(false);
881 assert!(!node.is_focused());
882 });
883 }
884
885 #[test]
886 fn text_field_element_creates_node() {
887 with_test_runtime(|| {
888 let state = TextFieldState::new("Hello World");
889 let element = TextFieldElement::new(state, TextStyle::default());
890
891 let node = element.create();
892 assert_eq!(node.text(), "Hello World");
893 });
894 }
895
896 #[test]
897 fn text_field_element_equality() {
898 with_test_runtime(|| {
899 let state1 = TextFieldState::new("Hello");
900 let state2 = TextFieldState::new("Hello"); let elem1 = TextFieldElement::new(state1.clone(), TextStyle::default());
903 let elem2 = TextFieldElement::new(state1.clone(), TextStyle::default()); let elem3 = TextFieldElement::new(state2, TextStyle::default()); assert_eq!(elem1, elem2, "Same state should be equal");
909 assert_ne!(elem1, elem3, "Different states should not be equal");
910 });
911 }
912
913 #[test]
919 fn test_cursor_x_position_calculation() {
920 with_test_runtime(|| {
921 let style = crate::text::TextStyle::default();
923
924 let empty_width = crate::text::measure_text("", &style).width;
926 assert!(
927 empty_width.abs() < 0.1,
928 "Empty text should have 0 width, got {}",
929 empty_width
930 );
931
932 let hi_width = crate::text::measure_text("Hi", &style).width;
934 assert!(
935 hi_width > 0.0,
936 "Text 'Hi' should have positive width: {}",
937 hi_width
938 );
939
940 let h_width = crate::text::measure_text("H", &style).width;
942 assert!(h_width > 0.0, "Text 'H' should have positive width");
943 assert!(
944 h_width < hi_width,
945 "'H' width {} should be less than 'Hi' width {}",
946 h_width,
947 hi_width
948 );
949
950 let state = TextFieldState::new("Hi");
952 assert_eq!(
953 state.selection().start,
954 2,
955 "Cursor should be at position 2 (end of 'Hi')"
956 );
957
958 let text = state.text();
960 let cursor_pos = state.selection().start;
961 let text_before_cursor = &text[..cursor_pos.min(text.len())];
962 assert_eq!(text_before_cursor, "Hi");
963
964 let cursor_x = crate::text::measure_text(text_before_cursor, &style).width;
966 assert!(
967 (cursor_x - hi_width).abs() < 0.1,
968 "Cursor x {} should equal 'Hi' width {}",
969 cursor_x,
970 hi_width
971 );
972 });
973 }
974
975 #[test]
977 fn test_focused_node_creates_cursor() {
978 with_test_runtime(|| {
979 let state = TextFieldState::new("Test");
980 let element = TextFieldElement::new(state.clone(), TextStyle::default());
981 let node = element.create();
982
983 assert!(!node.is_focused());
985
986 *node.refs.is_focused.borrow_mut() = true;
988 assert!(node.is_focused());
989
990 assert_eq!(node.text(), "Test");
992
993 assert_eq!(node.selection().start, 4);
995 });
996 }
997}