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
84pub struct TextFieldModifierNode {
91 state: TextFieldState,
93 refs: TextFieldRefs,
95 cursor_brush: Brush,
97 selection_brush: Brush,
99 line_limits: TextFieldLineLimits,
101 cached_text: String,
103 cached_selection: TextRange,
105 node_state: NodeState,
107 measured_size: Cell<Size>,
109 cached_handler: Rc<dyn Fn(PointerEvent)>,
111}
112
113impl std::fmt::Debug for TextFieldModifierNode {
114 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115 f.debug_struct("TextFieldModifierNode")
116 .field("text", &self.state.text())
117 .field("is_focused", &*self.refs.is_focused.borrow())
118 .finish()
119 }
120}
121
122use crate::text_field_handler::TextFieldHandler;
124
125impl TextFieldModifierNode {
126 pub fn new(state: TextFieldState) -> Self {
128 let value = state.value();
129 let refs = TextFieldRefs::new();
130 let line_limits = TextFieldLineLimits::default();
131 let cached_handler = Self::create_handler(state.clone(), refs.clone(), line_limits);
132
133 Self {
134 state,
135 refs,
136 cursor_brush: Brush::solid(DEFAULT_CURSOR_COLOR),
137 selection_brush: Brush::solid(DEFAULT_SELECTION_COLOR),
138 line_limits,
139 cached_text: value.text,
140 cached_selection: value.selection,
141 node_state: NodeState::new(),
142 measured_size: Cell::new(Size {
143 width: 0.0,
144 height: 0.0,
145 }),
146 cached_handler,
147 }
148 }
149
150 pub fn with_line_limits(mut self, line_limits: TextFieldLineLimits) -> Self {
152 self.line_limits = line_limits;
153 self
154 }
155
156 pub fn line_limits(&self) -> TextFieldLineLimits {
158 self.line_limits
159 }
160
161 fn create_handler(
163 state: TextFieldState,
164 refs: TextFieldRefs,
165 line_limits: TextFieldLineLimits,
166 ) -> Rc<dyn Fn(PointerEvent)> {
167 use crate::word_boundaries::find_word_boundaries;
169
170 Rc::new(move |event: PointerEvent| {
171 let click_x = (event.position.x - refs.content_offset.get()).max(0.0);
173 let click_y = (event.position.y - refs.content_y_offset.get()).max(0.0);
174
175 match event.kind {
176 PointerEventKind::Down => {
177 let handler =
179 TextFieldHandler::new(state.clone(), refs.node_id.get(), line_limits);
180 crate::text_field_focus::request_focus(refs.is_focused.clone(), handler);
181
182 let now = web_time::Instant::now();
183 let text = state.text();
184 let pos = crate::text::get_offset_for_position(&text, click_x, click_y);
185
186 let is_double_click = if let Some(last) = refs.last_click_time.get() {
188 now.duration_since(last).as_millis() < DOUBLE_CLICK_MS
189 } else {
190 false
191 };
192
193 if is_double_click {
194 let count = refs.click_count.get() + 1;
196 refs.click_count.set(count.min(3));
197
198 if count >= 3 {
199 state.edit(|buffer| {
201 buffer.select_all();
202 });
203 refs.drag_anchor.set(Some(0));
205 } else if count >= 2 {
206 let (word_start, word_end) = find_word_boundaries(&text, pos);
208 state.edit(|buffer| {
209 buffer.select(TextRange::new(word_start, word_end));
210 });
211 refs.drag_anchor.set(Some(word_start));
213 }
214 } else {
215 refs.click_count.set(1);
217 refs.drag_anchor.set(Some(pos));
218 state.edit(|buffer| {
219 buffer.place_cursor_before_char(pos);
220 });
221 }
222
223 refs.last_click_time.set(Some(now));
224 event.consume();
225 }
226 PointerEventKind::Move => {
227 if let Some(anchor) = refs.drag_anchor.get() {
229 if *refs.is_focused.borrow() {
230 let text = state.text();
231 let current_pos =
232 crate::text::get_offset_for_position(&text, click_x, click_y);
233
234 state.set_selection(TextRange::new(anchor, current_pos));
236
237 crate::request_render_invalidation();
239
240 event.consume();
241 }
242 }
243 }
244 PointerEventKind::Up => {
245 refs.drag_anchor.set(None);
247 }
248 _ => {}
249 }
250 })
251 }
252
253 pub fn with_cursor_color(mut self, color: Color) -> Self {
255 self.cursor_brush = Brush::solid(color);
256 self
257 }
258
259 pub fn set_focused(&mut self, focused: bool) {
261 let current = *self.refs.is_focused.borrow();
262 if current != focused {
263 *self.refs.is_focused.borrow_mut() = focused;
264 }
265 }
266
267 pub fn is_focused(&self) -> bool {
269 *self.refs.is_focused.borrow()
270 }
271
272 pub fn is_focused_rc(&self) -> Rc<RefCell<bool>> {
274 self.refs.is_focused.clone()
275 }
276
277 pub fn content_offset_rc(&self) -> Rc<Cell<f32>> {
279 self.refs.content_offset.clone()
280 }
281
282 pub fn content_y_offset_rc(&self) -> Rc<Cell<f32>> {
284 self.refs.content_y_offset.clone()
285 }
286
287 pub fn text(&self) -> String {
289 self.state.text()
290 }
291
292 pub fn selection(&self) -> TextRange {
294 self.state.selection()
295 }
296
297 pub fn cursor_brush(&self) -> Brush {
299 self.cursor_brush.clone()
300 }
301
302 pub fn selection_brush(&self) -> Brush {
304 self.selection_brush.clone()
305 }
306
307 pub fn insert_text(&mut self, text: &str) {
309 self.state.edit(|buffer| {
310 buffer.insert(text);
311 });
312 }
313
314 pub fn copy_selection(&self) -> Option<String> {
317 self.state.copy_selection()
318 }
319
320 pub fn cut_selection(&mut self) -> Option<String> {
323 let text = self.copy_selection();
324 if text.is_some() {
325 self.state.edit(|buffer| {
326 buffer.delete(buffer.selection());
327 });
328 }
329 text
330 }
331
332 pub fn get_state(&self) -> cranpose_foundation::text::TextFieldState {
335 self.state.clone()
336 }
337
338 pub fn set_content_offset(&self, offset: f32) {
341 self.refs.content_offset.set(offset);
342 }
343
344 pub fn set_content_y_offset(&self, offset: f32) {
347 self.refs.content_y_offset.set(offset);
348 }
349
350 fn measure_text_content(&self) -> Size {
352 let text = self.state.text();
353 let metrics = crate::text::measure_text(&text);
354 Size {
355 width: metrics.width,
356 height: metrics.height,
357 }
358 }
359
360 fn update_cached_state(&mut self) -> bool {
362 let value = self.state.value();
363 let text_changed = value.text != self.cached_text;
364 let selection_changed = value.selection != self.cached_selection;
365
366 if text_changed {
367 self.cached_text = value.text;
368 }
369 if selection_changed {
370 self.cached_selection = value.selection;
371 }
372
373 text_changed || selection_changed
374 }
375
376 pub fn position_cursor_at_offset(&self, x_offset: f32) {
379 let text = self.state.text();
380 if text.is_empty() {
381 self.state.edit(|buffer| {
382 buffer.place_cursor_at_start();
383 });
384 return;
385 }
386
387 let byte_offset = crate::text::get_offset_for_position(&text, x_offset, 0.0);
389
390 self.state.edit(|buffer| {
391 buffer.place_cursor_before_char(byte_offset);
392 });
393 }
394
395 }
399
400impl DelegatableNode for TextFieldModifierNode {
401 fn node_state(&self) -> &NodeState {
402 &self.node_state
403 }
404}
405
406impl ModifierNode for TextFieldModifierNode {
407 fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
408 self.refs.node_id.set(context.node_id());
410
411 context.invalidate(InvalidationKind::Layout);
412 context.invalidate(InvalidationKind::Draw);
413 context.invalidate(InvalidationKind::Semantics);
414 }
415
416 fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
417 Some(self)
418 }
419
420 fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
421 Some(self)
422 }
423
424 fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
425 Some(self)
426 }
427
428 fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
429 Some(self)
430 }
431
432 fn as_semantics_node(&self) -> Option<&dyn SemanticsNode> {
433 Some(self)
434 }
435
436 fn as_semantics_node_mut(&mut self) -> Option<&mut dyn SemanticsNode> {
437 Some(self)
438 }
439
440 fn as_pointer_input_node(&self) -> Option<&dyn PointerInputNode> {
441 Some(self)
442 }
443
444 fn as_pointer_input_node_mut(&mut self) -> Option<&mut dyn PointerInputNode> {
445 Some(self)
446 }
447}
448
449impl LayoutModifierNode for TextFieldModifierNode {
450 fn measure(
451 &self,
452 _context: &mut dyn ModifierNodeContext,
453 _measurable: &dyn Measurable,
454 constraints: Constraints,
455 ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
456 let text_size = self.measure_text_content();
458
459 let min_height = if text_size.height < 1.0 {
461 DEFAULT_LINE_HEIGHT
462 } else {
463 text_size.height
464 };
465
466 let width = text_size
468 .width
469 .max(constraints.min_width)
470 .min(constraints.max_width);
471 let height = min_height
472 .max(constraints.min_height)
473 .min(constraints.max_height);
474
475 let size = Size { width, height };
476 self.measured_size.set(size);
477
478 cranpose_ui_layout::LayoutModifierMeasureResult::with_size(size)
479 }
480
481 fn min_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
482 self.measure_text_content().width
483 }
484
485 fn max_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
486 self.measure_text_content().width
487 }
488
489 fn min_intrinsic_height(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
490 self.measure_text_content().height.max(DEFAULT_LINE_HEIGHT)
491 }
492
493 fn max_intrinsic_height(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
494 self.measure_text_content().height.max(DEFAULT_LINE_HEIGHT)
495 }
496}
497
498impl DrawModifierNode for TextFieldModifierNode {
499 fn draw(&self, _draw_scope: &mut dyn DrawScope) {
500 }
504
505 fn create_draw_closure(
506 &self,
507 ) -> Option<Rc<dyn Fn(cranpose_foundation::Size) -> Vec<cranpose_ui_graphics::DrawPrimitive>>>
508 {
509 use cranpose_ui_graphics::DrawPrimitive;
510
511 let is_focused = self.refs.is_focused.clone();
513 let state = self.state.clone();
514 let content_offset = self.refs.content_offset.clone();
515 let content_y_offset = self.refs.content_y_offset.clone();
516 let cursor_brush = self.cursor_brush.clone();
517 let selection_brush = self.selection_brush.clone();
518
519 Some(Rc::new(move |_size| {
520 if !*is_focused.borrow() {
522 return vec![];
523 }
524
525 let mut primitives = Vec::new();
526
527 let text = state.text();
528 let selection = state.selection();
529 let padding_left = content_offset.get();
530 let padding_top = content_y_offset.get();
531 let line_height = crate::text::measure_text(&text).line_height;
532
533 if !selection.collapsed() {
535 let sel_start = selection.min();
536 let sel_end = selection.max();
537
538 let lines: Vec<&str> = text.split('\n').collect();
539 let mut byte_offset: usize = 0;
540
541 for (line_idx, line) in lines.iter().enumerate() {
542 let line_start = byte_offset;
543 let line_end = byte_offset + line.len();
544
545 if sel_end > line_start && sel_start < line_end {
546 let sel_start_in_line = sel_start.saturating_sub(line_start);
547 let sel_end_in_line = (sel_end - line_start).min(line.len());
548
549 let sel_start_x = crate::text::measure_text(&line[..sel_start_in_line])
550 .width
551 + padding_left;
552 let sel_end_x = crate::text::measure_text(&line[..sel_end_in_line]).width
553 + padding_left;
554 let sel_width = sel_end_x - sel_start_x;
555
556 if sel_width > 0.0 {
557 let sel_rect = cranpose_ui_graphics::Rect {
558 x: sel_start_x,
559 y: padding_top + line_idx as f32 * line_height,
560 width: sel_width,
561 height: line_height,
562 };
563 primitives.push(DrawPrimitive::Rect {
564 rect: sel_rect,
565 brush: selection_brush.clone(),
566 });
567 }
568 }
569 byte_offset = line_end + 1;
570 }
571 }
572
573 if let Some(comp_range) = state.composition() {
576 let comp_start = comp_range.min();
577 let comp_end = comp_range.max();
578
579 if comp_start < comp_end && comp_end <= text.len() {
580 let lines: Vec<&str> = text.split('\n').collect();
581 let mut byte_offset: usize = 0;
582
583 let underline_brush = cranpose_ui_graphics::Brush::solid(
585 cranpose_ui_graphics::Color(0.8, 0.8, 0.8, 0.8),
586 );
587 let underline_height: f32 = 2.0;
588
589 for (line_idx, line) in lines.iter().enumerate() {
590 let line_start = byte_offset;
591 let line_end = byte_offset + line.len();
592
593 if comp_end > line_start && comp_start < line_end {
595 let comp_start_in_line = comp_start.saturating_sub(line_start);
596 let comp_end_in_line = (comp_end - line_start).min(line.len());
597
598 let comp_start_in_line = if line.is_char_boundary(comp_start_in_line) {
600 comp_start_in_line
601 } else {
602 0
603 };
604 let comp_end_in_line = if line.is_char_boundary(comp_end_in_line) {
605 comp_end_in_line
606 } else {
607 line.len()
608 };
609
610 let comp_start_x =
611 crate::text::measure_text(&line[..comp_start_in_line]).width
612 + padding_left;
613 let comp_end_x = crate::text::measure_text(&line[..comp_end_in_line])
614 .width
615 + padding_left;
616 let comp_width = comp_end_x - comp_start_x;
617
618 if comp_width > 0.0 {
619 let underline_rect = cranpose_ui_graphics::Rect {
621 x: comp_start_x,
622 y: padding_top + (line_idx as f32 + 1.0) * line_height
623 - underline_height,
624 width: comp_width,
625 height: underline_height,
626 };
627 primitives.push(DrawPrimitive::Rect {
628 rect: underline_rect,
629 brush: underline_brush.clone(),
630 });
631 }
632 }
633 byte_offset = line_end + 1;
634 }
635 }
636 }
637
638 if crate::cursor_animation::is_cursor_visible() {
640 let pos = selection.start.min(text.len());
641 let text_before = &text[..pos];
642 let line_index = text_before.matches('\n').count();
643 let line_start = text_before.rfind('\n').map(|i| i + 1).unwrap_or(0);
644 let cursor_x =
645 crate::text::measure_text(&text_before[line_start..]).width + padding_left;
646 let cursor_y = padding_top + line_index as f32 * line_height;
647
648 let cursor_rect = cranpose_ui_graphics::Rect {
649 x: cursor_x,
650 y: cursor_y,
651 width: CURSOR_WIDTH,
652 height: line_height,
653 };
654
655 primitives.push(DrawPrimitive::Rect {
656 rect: cursor_rect,
657 brush: cursor_brush.clone(),
658 });
659 }
660
661 primitives
662 }))
663 }
664}
665
666impl SemanticsNode for TextFieldModifierNode {
667 fn merge_semantics(&self, config: &mut SemanticsConfiguration) {
668 let text = self.state.text();
669 config.content_description = Some(text);
670 }
674}
675
676impl PointerInputNode for TextFieldModifierNode {
677 fn on_pointer_event(
678 &mut self,
679 _context: &mut dyn ModifierNodeContext,
680 _event: &PointerEvent,
681 ) -> bool {
682 false
693 }
694
695 fn hit_test(&self, x: f32, y: f32) -> bool {
696 let size = self.measured_size.get();
698 x >= 0.0 && x <= size.width && y >= 0.0 && y <= size.height
699 }
700
701 fn pointer_input_handler(&self) -> Option<Rc<dyn Fn(PointerEvent)>> {
702 Some(self.cached_handler.clone())
704 }
705}
706
707#[derive(Clone)]
718pub struct TextFieldElement {
719 state: TextFieldState,
721 cursor_color: Color,
723 line_limits: TextFieldLineLimits,
725}
726
727impl TextFieldElement {
728 pub fn new(state: TextFieldState) -> Self {
730 Self {
731 state,
732 cursor_color: DEFAULT_CURSOR_COLOR,
733 line_limits: TextFieldLineLimits::default(),
734 }
735 }
736
737 pub fn with_cursor_color(mut self, color: Color) -> Self {
739 self.cursor_color = color;
740 self
741 }
742
743 pub fn with_line_limits(mut self, line_limits: TextFieldLineLimits) -> Self {
745 self.line_limits = line_limits;
746 self
747 }
748}
749
750impl std::fmt::Debug for TextFieldElement {
751 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
752 f.debug_struct("TextFieldElement")
753 .field("text", &self.state.text())
754 .field("cursor_color", &self.cursor_color)
755 .finish()
756 }
757}
758
759impl Hash for TextFieldElement {
760 fn hash<H: Hasher>(&self, state: &mut H) {
761 std::ptr::hash(std::rc::Rc::as_ptr(&self.state.inner), state);
764 self.cursor_color.0.to_bits().hash(state);
766 self.cursor_color.1.to_bits().hash(state);
767 self.cursor_color.2.to_bits().hash(state);
768 self.cursor_color.3.to_bits().hash(state);
769 }
770}
771
772impl PartialEq for TextFieldElement {
773 fn eq(&self, other: &Self) -> bool {
774 self.state == other.state
778 && self.cursor_color == other.cursor_color
779 && self.line_limits == other.line_limits
780 }
781}
782
783impl Eq for TextFieldElement {}
784
785impl ModifierNodeElement for TextFieldElement {
786 type Node = TextFieldModifierNode;
787
788 fn create(&self) -> Self::Node {
789 TextFieldModifierNode::new(self.state.clone())
790 .with_cursor_color(self.cursor_color)
791 .with_line_limits(self.line_limits)
792 }
793
794 fn update(&self, node: &mut Self::Node) {
795 node.state = self.state.clone();
797 node.cursor_brush = Brush::solid(self.cursor_color);
798 node.line_limits = self.line_limits;
799
800 node.cached_handler = TextFieldModifierNode::create_handler(
802 node.state.clone(),
803 node.refs.clone(),
804 node.line_limits,
805 );
806
807 if node.update_cached_state() {
809 }
812 }
813
814 fn capabilities(&self) -> NodeCapabilities {
815 NodeCapabilities::LAYOUT
816 | NodeCapabilities::DRAW
817 | NodeCapabilities::SEMANTICS
818 | NodeCapabilities::POINTER_INPUT
819 }
820
821 fn always_update(&self) -> bool {
822 true
824 }
825}
826
827#[cfg(test)]
828mod tests {
829 use super::*;
830 use cranpose_core::{DefaultScheduler, Runtime};
831 use std::sync::Arc;
832
833 fn with_test_runtime<T>(f: impl FnOnce() -> T) -> T {
835 let _runtime = Runtime::new(Arc::new(DefaultScheduler));
836 f()
837 }
838
839 #[test]
840 fn text_field_node_creation() {
841 with_test_runtime(|| {
842 let state = TextFieldState::new("Hello");
843 let node = TextFieldModifierNode::new(state);
844 assert_eq!(node.text(), "Hello");
845 assert!(!node.is_focused());
846 });
847 }
848
849 #[test]
850 fn text_field_node_focus() {
851 with_test_runtime(|| {
852 let state = TextFieldState::new("Test");
853 let mut node = TextFieldModifierNode::new(state);
854 assert!(!node.is_focused());
855
856 node.set_focused(true);
857 assert!(node.is_focused());
858
859 node.set_focused(false);
860 assert!(!node.is_focused());
861 });
862 }
863
864 #[test]
865 fn text_field_element_creates_node() {
866 with_test_runtime(|| {
867 let state = TextFieldState::new("Hello World");
868 let element = TextFieldElement::new(state);
869
870 let node = element.create();
871 assert_eq!(node.text(), "Hello World");
872 });
873 }
874
875 #[test]
876 fn text_field_element_equality() {
877 with_test_runtime(|| {
878 let state1 = TextFieldState::new("Hello");
879 let state2 = TextFieldState::new("Hello"); let elem1 = TextFieldElement::new(state1.clone());
882 let elem2 = TextFieldElement::new(state1.clone()); let elem3 = TextFieldElement::new(state2); assert_eq!(elem1, elem2, "Same state should be equal");
888 assert_ne!(elem1, elem3, "Different states should not be equal");
889 });
890 }
891
892 #[test]
898 fn test_cursor_x_position_calculation() {
899 with_test_runtime(|| {
900 let empty_width = crate::text::measure_text("").width;
904 assert!(
905 empty_width.abs() < 0.1,
906 "Empty text should have 0 width, got {}",
907 empty_width
908 );
909
910 let hi_width = crate::text::measure_text("Hi").width;
912 assert!(
913 hi_width > 0.0,
914 "Text 'Hi' should have positive width: {}",
915 hi_width
916 );
917
918 let h_width = crate::text::measure_text("H").width;
920 assert!(h_width > 0.0, "Text 'H' should have positive width");
921 assert!(
922 h_width < hi_width,
923 "'H' width {} should be less than 'Hi' width {}",
924 h_width,
925 hi_width
926 );
927
928 let state = TextFieldState::new("Hi");
930 assert_eq!(
931 state.selection().start,
932 2,
933 "Cursor should be at position 2 (end of 'Hi')"
934 );
935
936 let text = state.text();
938 let cursor_pos = state.selection().start;
939 let text_before_cursor = &text[..cursor_pos.min(text.len())];
940 assert_eq!(text_before_cursor, "Hi");
941
942 let cursor_x = crate::text::measure_text(text_before_cursor).width;
944 assert!(
945 (cursor_x - hi_width).abs() < 0.1,
946 "Cursor x {} should equal 'Hi' width {}",
947 cursor_x,
948 hi_width
949 );
950 });
951 }
952
953 #[test]
955 fn test_focused_node_creates_cursor() {
956 with_test_runtime(|| {
957 let state = TextFieldState::new("Test");
958 let element = TextFieldElement::new(state.clone());
959 let node = element.create();
960
961 assert!(!node.is_focused());
963
964 *node.refs.is_focused.borrow_mut() = true;
966 assert!(node.is_focused());
967
968 assert_eq!(node.text(), "Test");
970
971 assert_eq!(node.selection().start, 4);
973 });
974 }
975}