1use std::{
2 cell::{Cell, RefCell},
3 hash::{Hash, Hasher},
4 rc::Rc,
5};
6
7use cranpose_core::{MutableState, mutableStateOf};
8use cranpose_foundation::{
9 Constraints, DelegatableNode, DrawModifierNode, DrawScope, FocusState, InvalidationKind,
10 LayoutModifierNode, Measurable, ModifierNode, ModifierNodeContext, ModifierNodeElement,
11 NodeCapabilities, NodeState, PointerEvent, PointerEventKind, PointerInputNode,
12 SemanticsConfiguration, SemanticsNode, Size,
13 text::{TextFieldLineLimits, TextFieldState, TextRange},
14};
15use cranpose_ui_graphics::{Brush, Color, Point};
16
17#[derive(Clone, Copy, PartialEq, Debug)]
18pub struct TextFieldHandleMetrics {
19 pub focused: bool,
20 pub direct_manipulation: bool,
21 pub node_origin: Point,
22 pub padding_left: f32,
23 pub padding_top: f32,
24 pub scroll_offset: f32,
25 pub line_height: f32,
26 pub glyph_box: (f32, f32),
27 pub wrap_width: Option<f32>,
28}
29
30#[derive(Clone)]
31pub struct TextFieldHandleController {
32 inner: Rc<TextFieldHandleControllerInner>,
33}
34
35impl PartialEq for TextFieldHandleController {
36 fn eq(&self, other: &Self) -> bool {
37 Rc::ptr_eq(&self.inner, &other.inner)
38 }
39}
40
41struct TextFieldHandleControllerInner {
42 metrics: Cell<Option<TextFieldHandleMetrics>>,
43 revision: MutableState<u64>,
44 gesture_claim: RefCell<Option<Rc<Cell<bool>>>>,
45 press_track: Cell<Option<MutableState<Option<PointerPressTrack>>>>,
46}
47
48impl TextFieldHandleController {
49 pub fn new() -> Self {
50 Self {
51 inner: Rc::new(TextFieldHandleControllerInner {
52 metrics: Cell::new(None),
53 revision: mutableStateOf(0u64),
54 gesture_claim: RefCell::new(None),
55 press_track: Cell::new(None),
56 }),
57 }
58 }
59
60 pub(crate) fn publish(&self, metrics: TextFieldHandleMetrics) {
61 if self.inner.metrics.get() != Some(metrics) {
62 self.inner.metrics.set(Some(metrics));
63 self.inner
64 .revision
65 .update(|value| *value = value.wrapping_add(1));
66 }
67 }
68
69 pub fn metrics(&self) -> Option<TextFieldHandleMetrics> {
70 let _ = self.inner.revision.value();
71 self.inner.metrics.get()
72 }
73
74 pub(crate) fn adopt_gesture_claim(&self, claim: &Rc<Cell<bool>>) {
75 let mut slot = self.inner.gesture_claim.borrow_mut();
76 let adopted = slot.as_ref().is_some_and(|held| Rc::ptr_eq(held, claim));
77 if !adopted {
78 *slot = Some(Rc::clone(claim));
79 }
80 }
81
82 pub(crate) fn adopt_press_track(&self, press_track: MutableState<Option<PointerPressTrack>>) {
83 if self.inner.press_track.get() != Some(press_track) {
84 self.inner.press_track.set(Some(press_track));
85 self.inner
86 .revision
87 .update(|value| *value = value.wrapping_add(1));
88 }
89 }
90
91 pub fn press(&self) -> Option<PointerPressTrack> {
92 self.inner.press_track.get().and_then(|state| state.get())
93 }
94
95 pub fn claim_gesture(&self) {
96 if let Some(claim) = self.inner.gesture_claim.borrow().as_ref() {
97 claim.set(true);
98 }
99 }
100
101 pub fn gesture_claimed(&self) -> bool {
102 self.inner
103 .gesture_claim
104 .borrow()
105 .as_ref()
106 .is_some_and(|claim| claim.get())
107 }
108}
109
110impl Default for TextFieldHandleController {
111 fn default() -> Self {
112 Self::new()
113 }
114}
115
116const DEFAULT_CURSOR_COLOR: Color = Color(1.0, 1.0, 1.0, 1.0);
117
118const DEFAULT_SELECTION_COLOR: Color = Color(0.0, 0.5, 1.0, 0.3);
119
120const DEFAULT_LINE_HEIGHT: f32 = 20.0;
121
122const CURSOR_WIDTH: f32 = 2.0;
123
124pub(crate) fn compute_horizontal_scroll_offset(
125 current_offset: f32,
126 cursor_x: f32,
127 text_width: f32,
128 viewport_width: f32,
129) -> f32 {
130 if viewport_width <= 0.0 {
131 return 0.0;
132 }
133 let max_offset = (text_width + CURSOR_WIDTH - viewport_width).max(0.0);
134 let mut offset = current_offset.clamp(0.0, max_offset);
135 let visible_end = offset + viewport_width - CURSOR_WIDTH;
136 if cursor_x > visible_end {
137 offset = cursor_x - viewport_width + CURSOR_WIDTH;
138 } else if cursor_x < offset {
139 offset = cursor_x;
140 }
141 offset.clamp(0.0, max_offset)
142}
143
144pub(crate) fn intersect_rect(
145 rect: cranpose_ui_graphics::Rect,
146 bounds: cranpose_ui_graphics::Rect,
147) -> Option<cranpose_ui_graphics::Rect> {
148 let x0 = rect.x.max(bounds.x);
149 let y0 = rect.y.max(bounds.y);
150 let x1 = (rect.x + rect.width).min(bounds.x + bounds.width);
151 let y1 = (rect.y + rect.height).min(bounds.y + bounds.height);
152 (x1 > x0 && y1 > y0).then_some(cranpose_ui_graphics::Rect {
153 x: x0,
154 y: y0,
155 width: x1 - x0,
156 height: y1 - y0,
157 })
158}
159
160pub type TextPanResolver = Rc<dyn Fn(f32) -> f32>;
163
164pub(crate) fn caret_visual_line_for_offset(
165 text: &str,
166 style: &TextStyle,
167 node_id: Option<cranpose_core::NodeId>,
168 wrap_width: Option<f32>,
169 offset: usize,
170 affinity: crate::text_selection::LineAffinity,
171) -> (usize, usize) {
172 let offset = offset.min(text.len());
173 match wrap_width {
174 Some(width) if width.is_finite() && width > 0.0 => {
175 let annotated = crate::text::AnnotatedString::from(text);
176 let ranges = crate::text::wrapped_line_ranges(
177 node_id,
178 &annotated,
179 style,
180 crate::text::TextLayoutOptions::default(),
181 Some(width),
182 );
183 crate::text_selection::caret_visual_line(&ranges, offset, affinity)
184 }
185 _ => {
186 let before = &text[..offset];
187 let line_index = before.matches('\n').count();
188 let line_start = before.rfind('\n').map(|i| i + 1).unwrap_or(0);
189 (line_index, line_start)
190 }
191 }
192}
193
194#[allow(clippy::too_many_arguments)]
195pub(crate) fn range_visual_line_rects(
196 text: &str,
197 style: &TextStyle,
198 node_id: Option<cranpose_core::NodeId>,
199 wrap_width: Option<f32>,
200 padding_left: f32,
201 padding_top: f32,
202 pan: f32,
203 line_height: f32,
204 start: usize,
205 end: usize,
206) -> Vec<cranpose_ui_graphics::Rect> {
207 if start >= end {
208 return Vec::new();
209 }
210 let annotated = crate::text::AnnotatedString::from(text);
211 let line_ranges = crate::text::wrapped_line_ranges(
212 node_id,
213 &annotated,
214 style,
215 crate::text::TextLayoutOptions::default(),
216 wrap_width,
217 );
218 let mut rects = Vec::new();
219 for (line_idx, line_range) in line_ranges.iter().enumerate() {
220 let line_start = line_range.start;
221 let line_end = line_range.end;
222 if end <= line_start || start >= line_end {
223 continue;
224 }
225 let seg_start = start.max(line_start);
226 let seg_end = end.min(line_end);
227 let x0 = crate::text::measure_text(
228 &crate::text::AnnotatedString::from(&text[line_start..seg_start]),
229 style,
230 )
231 .width
232 + padding_left
233 - pan;
234 let x1 = crate::text::measure_text(
235 &crate::text::AnnotatedString::from(&text[line_start..seg_end]),
236 style,
237 )
238 .width
239 + padding_left
240 - pan;
241 let width = x1 - x0;
242 if width > 0.0 {
243 rects.push(cranpose_ui_graphics::Rect {
244 x: x0,
245 y: padding_top + line_idx as f32 * line_height,
246 width,
247 height: line_height,
248 });
249 }
250 }
251 rects
252}
253
254fn build_focus_handler(
255 state: TextFieldState,
256 refs: &TextFieldRefs,
257 line_limits: TextFieldLineLimits,
258 style: &TextStyle,
259) -> Rc<dyn crate::text_field_focus::FocusedTextFieldHandler> {
260 crate::text_field_handler::TextFieldHandler::new(
261 state,
262 refs.node_id.get(),
263 line_limits,
264 crate::text_field_handler::CaretGeometryRefs {
265 node_origin: refs.node_origin.clone(),
266 content_offset: refs.content_offset.clone(),
267 content_y_offset: refs.content_y_offset.clone(),
268 scroll_offset: refs.scroll_offset.clone(),
269 style: style.clone(),
270 },
271 )
272}
273
274struct TextFieldFocusBridge {
275 state: TextFieldState,
276 refs: TextFieldRefs,
277 style: TextStyle,
278 line_limits: TextFieldLineLimits,
279}
280
281impl crate::focus_dispatch::FocusTargetHandle for TextFieldFocusBridge {
282 fn set_focus_state(&self, state: FocusState) {
283 if state.is_focused() {
284 crate::text_field_focus::request_focus(
285 self.refs.is_focused.clone(),
286 build_focus_handler(self.state, &self.refs, self.line_limits, &self.style),
287 self.refs.modal_depth.get(),
288 );
289 } else if crate::text_field_focus::focused_field_node() == self.refs.node_id.get() {
290 crate::text_field_focus::clear_focus();
291 }
292 }
293}
294
295#[derive(Clone)]
296pub(crate) struct TextFieldRefs {
297 pub is_focused: Rc<RefCell<bool>>,
298 pub content_offset: Rc<Cell<f32>>,
299 pub content_y_offset: Rc<Cell<f32>>,
300 pub drag_anchor: Rc<Cell<Option<usize>>>,
301 pub last_click_time: Rc<Cell<Option<web_time::Instant>>>,
302 pub last_click_pos: Rc<Cell<Option<(f32, f32)>>>,
303 pub click_count: Rc<Cell<u8>>,
304 pub node_id: Rc<Cell<Option<cranpose_core::NodeId>>>,
305 pub scroll_offset: Rc<Cell<f32>>,
306 pub direct_manipulation: Rc<Cell<bool>>,
307 pub node_origin: Rc<Cell<Point>>,
308 pub line_height: Rc<Cell<f32>>,
309 pub wrap_width: Rc<Cell<Option<f32>>>,
310 pub press_track: MutableState<Option<PointerPressTrack>>,
311 pub gesture_claimed: Rc<Cell<bool>>,
312 pub modal_depth: Rc<Cell<usize>>,
313}
314
315#[derive(Clone, Copy, Debug, PartialEq)]
316pub struct PointerPressTrack {
317 pub start: Point,
318 pub position: Point,
319}
320
321impl TextFieldRefs {
322 pub fn new() -> Self {
323 Self {
324 is_focused: Rc::new(RefCell::new(false)),
325 content_offset: Rc::new(Cell::new(0.0_f32)),
326 content_y_offset: Rc::new(Cell::new(0.0_f32)),
327 drag_anchor: Rc::new(Cell::new(None::<usize>)),
328 last_click_time: Rc::new(Cell::new(None::<web_time::Instant>)),
329 last_click_pos: Rc::new(Cell::new(None::<(f32, f32)>)),
330 click_count: Rc::new(Cell::new(0_u8)),
331 node_id: Rc::new(Cell::new(None::<cranpose_core::NodeId>)),
332 scroll_offset: Rc::new(Cell::new(0.0_f32)),
333 direct_manipulation: Rc::new(Cell::new(false)),
334 node_origin: Rc::new(Cell::new(Point { x: 0.0, y: 0.0 })),
335 line_height: Rc::new(Cell::new(DEFAULT_LINE_HEIGHT)),
336 wrap_width: Rc::new(Cell::new(None::<f32>)),
337 press_track: mutableStateOf(None::<PointerPressTrack>),
338 gesture_claimed: Rc::new(Cell::new(false)),
339 modal_depth: Rc::new(Cell::new(0)),
340 }
341 }
342}
343
344use crate::text::TextStyle;
345
346pub struct TextFieldModifierNode {
347 state: TextFieldState,
348 refs: TextFieldRefs,
349 style: TextStyle,
350 cursor_brush: Brush,
351 selection_brush: Brush,
352 line_limits: TextFieldLineLimits,
353 cached_text: String,
354 cached_selection: TextRange,
355 node_state: NodeState,
356 measured_size: Rc<Cell<Size>>,
357 measured_line_height: Rc<Cell<f32>>,
358 measured_wrap_width: Rc<Cell<Option<f32>>>,
359 cached_handler: Rc<dyn Fn(PointerEvent)>,
360 cached_pan_resolver: TextPanResolver,
361 handle_controller: Option<TextFieldHandleController>,
362 modal_depth: usize,
363 focus_bridge: Option<Rc<dyn crate::focus_dispatch::FocusTargetHandle>>,
364}
365
366impl std::fmt::Debug for TextFieldModifierNode {
367 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368 f.debug_struct("TextFieldModifierNode")
369 .field("text", &self.state.text())
370 .field("style", &self.style)
371 .field("is_focused", &*self.refs.is_focused.borrow())
372 .finish()
373 }
374}
375
376impl TextFieldModifierNode {
377 pub fn new(state: TextFieldState, style: TextStyle) -> Self {
379 let value = state.value();
380 let refs = TextFieldRefs::new();
381 let refs_line_height = refs.line_height.clone();
382 let refs_wrap_width = refs.wrap_width.clone();
383 let line_limits = TextFieldLineLimits::default();
384 let cached_handler =
385 Self::create_handler(state, refs.clone(), line_limits, style.clone(), 0);
386 let cached_pan_resolver =
387 Self::create_pan_resolver(state, refs.clone(), line_limits, style.clone());
388
389 Self {
390 state,
391 refs,
392 style,
393 cursor_brush: Brush::solid(DEFAULT_CURSOR_COLOR),
394 selection_brush: Brush::solid(DEFAULT_SELECTION_COLOR),
395 line_limits,
396 cached_text: value.text,
397 cached_selection: value.selection,
398 node_state: NodeState::new(),
399 measured_size: Rc::new(Cell::new(Size {
400 width: 0.0,
401 height: 0.0,
402 })),
403 measured_line_height: refs_line_height,
404 measured_wrap_width: refs_wrap_width,
405 cached_handler,
406 cached_pan_resolver,
407 handle_controller: None,
408 modal_depth: 0,
409 focus_bridge: None,
410 }
411 }
412
413 pub fn with_line_limits(mut self, line_limits: TextFieldLineLimits) -> Self {
415 self.line_limits = line_limits;
416 self.rebuild_cached_closures();
417 self
418 }
419
420 fn rebuild_cached_closures(&mut self) {
421 self.cached_handler = Self::create_handler(
422 self.state,
423 self.refs.clone(),
424 self.line_limits,
425 self.style.clone(),
426 self.modal_depth,
427 );
428 self.cached_pan_resolver = Self::create_pan_resolver(
429 self.state,
430 self.refs.clone(),
431 self.line_limits,
432 self.style.clone(),
433 );
434 }
435
436 pub fn with_handle_controller(mut self, controller: TextFieldHandleController) -> Self {
438 self.handle_controller = Some(controller);
439 self
440 }
441
442 fn create_pan_resolver(
443 state: TextFieldState,
444 refs: TextFieldRefs,
445 line_limits: TextFieldLineLimits,
446 style: TextStyle,
447 ) -> TextPanResolver {
448 Rc::new(move |viewport_width: f32| {
449 if !line_limits.is_single_line() {
450 refs.scroll_offset.set(0.0);
451 return 0.0;
452 }
453 let text = state.text();
454 let pos = state.selection().start.min(text.len());
455 let text_width = crate::text::measure_text(
456 &crate::text::AnnotatedString::from(text.as_str()),
457 &style,
458 )
459 .width;
460 let cursor_x = crate::text::measure_text(
461 &crate::text::AnnotatedString::from(&text[..pos]),
462 &style,
463 )
464 .width;
465 let offset = compute_horizontal_scroll_offset(
466 refs.scroll_offset.get(),
467 cursor_x,
468 text_width,
469 viewport_width,
470 );
471 refs.scroll_offset.set(offset);
472 offset
473 })
474 }
475
476 pub fn text_pan_resolver(&self) -> Option<TextPanResolver> {
481 self.line_limits
482 .is_single_line()
483 .then(|| self.cached_pan_resolver.clone())
484 }
485
486 pub fn scroll_offset(&self) -> f32 {
488 self.refs.scroll_offset.get()
489 }
490
491 pub fn line_limits(&self) -> TextFieldLineLimits {
493 self.line_limits
494 }
495
496 fn create_handler(
497 state: TextFieldState,
498 refs: TextFieldRefs,
499 line_limits: TextFieldLineLimits,
500 style: TextStyle,
501 modal_depth: usize,
502 ) -> Rc<dyn Fn(PointerEvent)> {
503 use crate::{
504 text_selection::{
505 MULTI_TAP_SLOP_PX, MULTI_TAP_TIMEOUT_MS, SelectionGranularity, classify_tap_count,
506 find_line_boundaries, find_paragraph_boundaries, resolve_selection_tap_count,
507 tap_selection_granularity,
508 },
509 word_boundaries::find_word_boundaries,
510 };
511
512 Rc::new(move |event: PointerEvent| {
513 refs.node_origin.set(Point {
514 x: event.global_position.x - event.position.x,
515 y: event.global_position.y - event.position.y,
516 });
517
518 let click_x =
519 (event.position.x - refs.content_offset.get() + refs.scroll_offset.get()).max(0.0);
520 let click_y = (event.position.y - refs.content_y_offset.get()).max(0.0);
521
522 match event.kind {
523 PointerEventKind::Down => {
524 refs.direct_manipulation.set(true);
525 refs.press_track.set(Some(PointerPressTrack {
526 start: event.global_position,
527 position: event.global_position,
528 }));
529 refs.gesture_claimed.set(false);
530
531 crate::text_field_focus::request_focus(
532 refs.is_focused.clone(),
533 build_focus_handler(state, &refs, line_limits, &style),
534 modal_depth,
535 );
536
537 let now = web_time::Instant::now();
538 let text = state.text();
539 let pos = crate::text::offset_for_position_wrapped(
540 &text,
541 &style,
542 refs.node_id.get(),
543 refs.wrap_width.get(),
544 refs.line_height.get(),
545 click_x,
546 click_y,
547 );
548
549 let previous = refs.last_click_pos.get().and_then(|(px, py)| {
550 let count = refs.click_count.get();
551 (count > 0).then_some((count, px, py))
552 });
553 let elapsed_ms = refs
554 .last_click_time
555 .get()
556 .map(|last| now.duration_since(last).as_millis())
557 .unwrap_or(u128::MAX);
558 let tap_count = classify_tap_count(
559 previous,
560 elapsed_ms,
561 event.position.x,
562 event.position.y,
563 MULTI_TAP_TIMEOUT_MS,
564 MULTI_TAP_SLOP_PX,
565 );
566
567 let selection = state.selection();
568 let tap_in_selection =
569 !selection.collapsed() && pos >= selection.min() && pos <= selection.max();
570 let repeat_in_place = refs
571 .last_click_pos
572 .get()
573 .map(|(px, py)| {
574 let dx = event.position.x - px;
575 let dy = event.position.y - py;
576 dx * dx + dy * dy <= MULTI_TAP_SLOP_PX * MULTI_TAP_SLOP_PX
577 })
578 .unwrap_or(false);
579 let effective_count = resolve_selection_tap_count(
580 tap_count,
581 refs.click_count.get(),
582 tap_in_selection,
583 repeat_in_place,
584 );
585
586 match tap_selection_granularity(effective_count) {
587 SelectionGranularity::Paragraph => {
588 let (start, end) = find_paragraph_boundaries(&text, pos);
589 state.edit(|buffer| {
590 buffer.select(TextRange::new(start, end));
591 });
592 refs.drag_anchor.set(Some(start));
593 }
594 SelectionGranularity::Line => {
595 let (line_start, line_end) = find_line_boundaries(&text, pos);
596 state.edit(|buffer| {
597 buffer.select(TextRange::new(line_start, line_end));
598 });
599 refs.drag_anchor.set(Some(line_start));
600 }
601 SelectionGranularity::Word => {
602 let (word_start, word_end) = find_word_boundaries(&text, pos);
603 state.edit(|buffer| {
604 buffer.select(TextRange::new(word_start, word_end));
605 });
606 refs.drag_anchor.set(Some(word_start));
607 }
608 SelectionGranularity::Caret => {
609 refs.drag_anchor.set(Some(pos));
610 state.edit(|buffer| {
611 buffer.place_cursor_before_char(pos);
612 });
613 }
614 }
615
616 refs.click_count.set(effective_count);
617 refs.last_click_time.set(Some(now));
618 refs.last_click_pos
619 .set(Some((event.position.x, event.position.y)));
620 event.consume();
621 }
622 PointerEventKind::Move => {
623 if let Some(mut track) = refs.press_track.get() {
624 track.position = event.global_position;
625 refs.press_track.set(Some(track));
626 if let Some(node_id) = refs.node_id.get() {
627 crate::schedule_draw_repass(node_id);
628 }
629 crate::request_render_invalidation();
630 }
631 if refs.gesture_claimed.get() {
632 event.consume();
633 return;
634 }
635 if let Some(anchor) = refs.drag_anchor.get()
636 && *refs.is_focused.borrow()
637 {
638 let text = state.text();
639 let current_pos = crate::text::offset_for_position_wrapped(
640 &text,
641 &style,
642 refs.node_id.get(),
643 refs.wrap_width.get(),
644 refs.line_height.get(),
645 click_x,
646 click_y,
647 );
648
649 state.set_selection(TextRange::new(anchor, current_pos));
650
651 crate::request_render_invalidation();
652
653 event.consume();
654 }
655 }
656 PointerEventKind::Up => {
657 refs.drag_anchor.set(None);
658 refs.press_track.set(None);
659 refs.gesture_claimed.set(false);
660 if let Some(node_id) = refs.node_id.get() {
661 crate::schedule_draw_repass(node_id);
662 }
663 crate::request_render_invalidation();
664 }
665 PointerEventKind::Cancel => {
666 refs.press_track.set(None);
667 refs.gesture_claimed.set(false);
668 if let Some(node_id) = refs.node_id.get() {
669 crate::schedule_draw_repass(node_id);
670 }
671 crate::request_render_invalidation();
672 }
673 _ => {}
674 }
675 })
676 }
677
678 pub fn with_cursor_color(mut self, color: Color) -> Self {
683 self.cursor_brush = Brush::solid(color);
684 self.selection_brush = Brush::solid(
685 color.with_alpha(crate::widgets::basic_text_field::SELECTION_HIGHLIGHT_ALPHA),
686 );
687 self
688 }
689
690 pub fn set_focused(&mut self, focused: bool) {
692 let current = *self.refs.is_focused.borrow();
693 if current != focused {
694 *self.refs.is_focused.borrow_mut() = focused;
695 if !focused {
696 self.refs.direct_manipulation.set(false);
697 self.refs.press_track.set(None);
698 self.refs.gesture_claimed.set(false);
699 }
700 }
701 }
702
703 pub fn is_focused(&self) -> bool {
705 *self.refs.is_focused.borrow()
706 }
707
708 pub(crate) fn window_origin_sink(&self) -> Rc<Cell<Point>> {
709 self.refs.node_origin.clone()
710 }
711
712 pub fn text(&self) -> String {
714 self.state.text()
715 }
716
717 pub fn style(&self) -> &TextStyle {
718 &self.style
719 }
720
721 pub fn selection(&self) -> TextRange {
723 self.state.selection()
724 }
725
726 pub fn cursor_brush(&self) -> Brush {
728 self.cursor_brush.clone()
729 }
730
731 pub fn selection_brush(&self) -> Brush {
733 self.selection_brush.clone()
734 }
735
736 pub fn insert_text(&mut self, text: &str) {
738 self.state.edit(|buffer| {
739 buffer.insert(text);
740 });
741 }
742
743 pub fn copy_selection(&self) -> Option<String> {
746 self.state.copy_selection()
747 }
748
749 pub fn cut_selection(&mut self) -> Option<String> {
752 let text = self.copy_selection();
753 if text.is_some() {
754 self.state.edit(|buffer| {
755 buffer.delete(buffer.selection());
756 });
757 }
758 text
759 }
760
761 pub fn set_content_offset(&self, offset: f32) {
764 self.refs.content_offset.set(offset);
765 }
766
767 pub fn set_content_y_offset(&self, offset: f32) {
770 self.refs.content_y_offset.set(offset);
771 }
772
773 fn wrap_width(&self, available_width: f32) -> Option<f32> {
774 (!self.line_limits.is_single_line() && available_width.is_finite() && available_width > 0.0)
775 .then_some(available_width)
776 }
777
778 fn measure_text_content(&self, wrap_width: Option<f32>) -> Size {
779 let text = self.state.text();
780 let node_id = self.refs.node_id.get();
781 let annotated = crate::text::AnnotatedString::from(text.as_str());
782 let metrics = match wrap_width {
783 Some(max_width) => crate::text::measure_text_with_options_for_node(
784 node_id,
785 &annotated,
786 &self.style,
787 crate::text::TextLayoutOptions::default(),
788 Some(max_width),
789 ),
790 None => crate::text::measure_text_for_node(node_id, &annotated, &self.style),
791 };
792 self.measured_line_height.set(metrics.line_height);
793 Size {
794 width: metrics.width,
795 height: metrics.height,
796 }
797 }
798
799 fn update_cached_state(&mut self) -> bool {
800 let value = self.state.value();
801 let text_changed = value.text != self.cached_text;
802 let selection_changed = value.selection != self.cached_selection;
803
804 if text_changed {
805 self.cached_text = value.text;
806 }
807 if selection_changed {
808 self.cached_selection = value.selection;
809 }
810
811 text_changed || selection_changed
812 }
813}
814
815impl DelegatableNode for TextFieldModifierNode {
816 fn node_state(&self) -> &NodeState {
817 &self.node_state
818 }
819}
820
821impl ModifierNode for TextFieldModifierNode {
822 fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
823 self.refs.node_id.set(context.node_id());
824
825 context.invalidate(InvalidationKind::Layout);
826 context.invalidate(InvalidationKind::Draw);
827 context.invalidate(InvalidationKind::Semantics);
828
829 if let Some(node_id) = context.node_id() {
830 let bridge: Rc<dyn crate::focus_dispatch::FocusTargetHandle> =
831 Rc::new(TextFieldFocusBridge {
832 state: self.state,
833 refs: self.refs.clone(),
834 style: self.style.clone(),
835 line_limits: self.line_limits,
836 });
837 self.focus_bridge = Some(Rc::clone(&bridge));
838 crate::focus_dispatch::register_focus_target(node_id, bridge);
839 }
840 }
841
842 fn on_detach(&mut self) {
843 if let (Some(node_id), Some(bridge)) = (self.refs.node_id.get(), self.focus_bridge.take()) {
844 crate::focus_dispatch::unregister_focus_target(node_id, &bridge);
845 }
846 }
847
848 fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
849 Some(self)
850 }
851
852 fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
853 Some(self)
854 }
855
856 fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
857 Some(self)
858 }
859
860 fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
861 Some(self)
862 }
863
864 fn as_semantics_node(&self) -> Option<&dyn SemanticsNode> {
865 Some(self)
866 }
867
868 fn as_semantics_node_mut(&mut self) -> Option<&mut dyn SemanticsNode> {
869 Some(self)
870 }
871
872 fn as_pointer_input_node(&self) -> Option<&dyn PointerInputNode> {
873 Some(self)
874 }
875
876 fn as_pointer_input_node_mut(&mut self) -> Option<&mut dyn PointerInputNode> {
877 Some(self)
878 }
879}
880
881impl LayoutModifierNode for TextFieldModifierNode {
882 fn measure(
883 &self,
884 _context: &mut dyn ModifierNodeContext,
885 _measurable: &dyn Measurable,
886 constraints: Constraints,
887 ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
888 let wrap_width = self.wrap_width(constraints.max_width);
889 self.measured_wrap_width.set(wrap_width);
890 let text_size = self.measure_text_content(wrap_width);
891
892 let min_height = if text_size.height < 1.0 {
893 DEFAULT_LINE_HEIGHT
894 } else {
895 text_size.height
896 };
897
898 let width = text_size
899 .width
900 .max(constraints.min_width)
901 .min(constraints.max_width);
902 let height = min_height
903 .max(constraints.min_height)
904 .min(constraints.max_height);
905
906 let size = Size { width, height };
907 self.measured_size.set(size);
908
909 let _ = (self.cached_pan_resolver)(size.width);
910
911 cranpose_ui_layout::LayoutModifierMeasureResult::with_size(size)
912 }
913
914 fn min_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
915 self.measure_text_content(None).width
916 }
917
918 fn max_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
919 self.measure_text_content(None).width
920 }
921
922 fn min_intrinsic_height(&self, _measurable: &dyn Measurable, width: f32) -> f32 {
923 self.measure_text_content(self.wrap_width(width))
924 .height
925 .max(DEFAULT_LINE_HEIGHT)
926 }
927
928 fn max_intrinsic_height(&self, _measurable: &dyn Measurable, width: f32) -> f32 {
929 self.measure_text_content(self.wrap_width(width))
930 .height
931 .max(DEFAULT_LINE_HEIGHT)
932 }
933}
934
935fn content_viewport(
936 measured: cranpose_ui_graphics::Size,
937 size: cranpose_foundation::Size,
938 padding_left: f32,
939 padding_top: f32,
940) -> (f32, f32) {
941 let width = if measured.width > 0.0 {
942 measured.width
943 } else {
944 (size.width - padding_left).max(0.0)
945 };
946 let height = if measured.height > 0.0 {
947 measured.height
948 } else {
949 (size.height - padding_top).max(0.0)
950 };
951 (width, height)
952}
953
954impl DrawModifierNode for TextFieldModifierNode {
955 fn draw(&self, _draw_scope: &mut dyn DrawScope) {}
956
957 fn create_draw_closure(
958 &self,
959 ) -> Option<Rc<dyn Fn(&mut cranpose_ui_graphics::DrawScopeDefault)>> {
960 use cranpose_ui_graphics::{DrawPrimitive, DrawScope as _};
961
962 let is_focused = self.refs.is_focused.clone();
963 let state = self.state;
964 let content_offset = self.refs.content_offset.clone();
965 let content_y_offset = self.refs.content_y_offset.clone();
966 let cursor_brush = self.cursor_brush.clone();
967 let style = self.style.clone();
968 let cached_line_height = self.measured_line_height.clone();
969 let measured_size = self.measured_size.clone();
970 let measured_wrap_width = self.measured_wrap_width.clone();
971 let node_id = self.refs.node_id.clone();
972 let pan_resolver = self.cached_pan_resolver.clone();
973 let handle_controller = self.handle_controller.clone();
974 let node_origin = self.refs.node_origin.clone();
975 let direct_manipulation = self.refs.direct_manipulation.clone();
976 let press_track = self.refs.press_track;
977 let gesture_claimed = self.refs.gesture_claimed.clone();
978
979 Some(Rc::new(move |scope| {
980 let size = scope.size();
981 if !*is_focused.borrow() {
982 if let Some(controller) = &handle_controller {
983 controller.publish(TextFieldHandleMetrics {
984 focused: false,
985 direct_manipulation: false,
986 node_origin: node_origin.get(),
987 padding_left: 0.0,
988 padding_top: 0.0,
989 scroll_offset: 0.0,
990 line_height: cached_line_height.get(),
991 glyph_box: crate::text::glyph_line_box(&style, cached_line_height.get()),
992 wrap_width: measured_wrap_width.get(),
993 });
994 }
995 return;
996 }
997
998 let mut primitives = Vec::new();
999
1000 let text = state.text();
1001 let selection = state.selection();
1002 let padding_left = content_offset.get();
1003 let padding_top = content_y_offset.get();
1004 let line_height = cached_line_height.get();
1005
1006 let (viewport_width, viewport_height) =
1007 content_viewport(measured_size.get(), size, padding_left, padding_top);
1008 let pan = pan_resolver(viewport_width);
1009
1010 if let Some(controller) = &handle_controller {
1011 controller.adopt_gesture_claim(&gesture_claimed);
1012 controller.adopt_press_track(press_track);
1013 controller.publish(TextFieldHandleMetrics {
1014 focused: true,
1015 direct_manipulation: direct_manipulation.get(),
1016 node_origin: node_origin.get(),
1017 padding_left,
1018 padding_top,
1019 scroll_offset: pan,
1020 line_height,
1021 glyph_box: crate::text::glyph_line_box(&style, line_height),
1022 wrap_width: measured_wrap_width.get(),
1023 });
1024 }
1025 let clip_bounds = cranpose_ui_graphics::Rect {
1026 x: padding_left,
1027 y: padding_top,
1028 width: viewport_width,
1029 height: viewport_height,
1030 };
1031
1032 if let Some(comp_range) = state.composition() {
1033 let comp_start = comp_range.min();
1034 let comp_end = comp_range.max();
1035
1036 if comp_start < comp_end && comp_end <= text.len() {
1037 let underline_brush = cranpose_ui_graphics::Brush::solid(
1038 cranpose_ui_graphics::Color(0.8, 0.8, 0.8, 0.8),
1039 );
1040 let underline_height: f32 = 2.0;
1041
1042 for line_rect in range_visual_line_rects(
1043 &text,
1044 &style,
1045 node_id.get(),
1046 measured_wrap_width.get(),
1047 padding_left,
1048 padding_top,
1049 pan,
1050 line_height,
1051 comp_start,
1052 comp_end,
1053 ) {
1054 let underline_rect = cranpose_ui_graphics::Rect {
1055 x: line_rect.x,
1056 y: line_rect.y + line_height - underline_height,
1057 width: line_rect.width,
1058 height: underline_height,
1059 };
1060 if let Some(clipped) = intersect_rect(underline_rect, clip_bounds) {
1061 primitives.push(DrawPrimitive::Rect {
1062 rect: clipped,
1063 brush: underline_brush.clone(),
1064 stroke: None,
1065 });
1066 }
1067 }
1068 }
1069 }
1070
1071 if selection.collapsed() && crate::cursor_animation::is_cursor_visible() {
1072 let pos = selection.start.min(text.len());
1073 let (line_index, line_start) = caret_visual_line_for_offset(
1074 &text,
1075 &style,
1076 node_id.get(),
1077 measured_wrap_width.get(),
1078 pos,
1079 crate::text_selection::LineAffinity::Upstream,
1080 );
1081 let cursor_x = crate::text::measure_text(
1082 &crate::text::AnnotatedString::from(&text[line_start..pos]),
1083 &style,
1084 )
1085 .width
1086 + padding_left
1087 - pan;
1088 let (box_off, box_h) = crate::text::glyph_line_box(&style, line_height);
1089 let cursor_y = padding_top + line_index as f32 * line_height + box_off;
1090
1091 let cursor_rect = cranpose_ui_graphics::Rect {
1092 x: cursor_x,
1093 y: cursor_y,
1094 width: CURSOR_WIDTH,
1095 height: box_h,
1096 };
1097
1098 if let Some(clipped) = intersect_rect(cursor_rect, clip_bounds) {
1099 primitives.push(DrawPrimitive::Rect {
1100 rect: clipped,
1101 brush: cursor_brush.clone(),
1102 stroke: None,
1103 });
1104 }
1105 }
1106
1107 scope.push_recorded(primitives);
1108 }))
1109 }
1110
1111 fn create_behind_draw_closure(
1112 &self,
1113 ) -> Option<Rc<dyn Fn(&mut cranpose_ui_graphics::DrawScopeDefault)>> {
1114 use cranpose_ui_graphics::{DrawPrimitive, DrawScope as _};
1115
1116 let is_focused = self.refs.is_focused.clone();
1117 let state = self.state;
1118 let content_offset = self.refs.content_offset.clone();
1119 let content_y_offset = self.refs.content_y_offset.clone();
1120 let selection_brush = self.selection_brush.clone();
1121 let style = self.style.clone();
1122 let cached_line_height = self.measured_line_height.clone();
1123 let measured_size = self.measured_size.clone();
1124 let measured_wrap_width = self.measured_wrap_width.clone();
1125 let node_id = self.refs.node_id.clone();
1126 let pan_resolver = self.cached_pan_resolver.clone();
1127
1128 Some(Rc::new(move |scope| {
1129 let size = scope.size();
1130 if !*is_focused.borrow() {
1131 return;
1132 }
1133 let selection = state.selection();
1134 if selection.collapsed() {
1135 return;
1136 }
1137 let text = state.text();
1138 let padding_left = content_offset.get();
1139 let padding_top = content_y_offset.get();
1140 let line_height = cached_line_height.get();
1141 let (viewport_width, viewport_height) =
1142 content_viewport(measured_size.get(), size, padding_left, padding_top);
1143 let pan = pan_resolver(viewport_width);
1144 let clip_bounds = cranpose_ui_graphics::Rect {
1145 x: padding_left,
1146 y: padding_top,
1147 width: viewport_width,
1148 height: viewport_height,
1149 };
1150
1151 let mut primitives = Vec::new();
1152 let (box_off, box_h) = crate::text::glyph_line_box(&style, line_height);
1153 for sel_rect in range_visual_line_rects(
1154 &text,
1155 &style,
1156 node_id.get(),
1157 measured_wrap_width.get(),
1158 padding_left,
1159 padding_top,
1160 pan,
1161 line_height,
1162 selection.min(),
1163 selection.max(),
1164 ) {
1165 let sel_rect = cranpose_ui_graphics::Rect {
1166 y: sel_rect.y + box_off,
1167 height: box_h,
1168 ..sel_rect
1169 };
1170 if let Some(clipped) = intersect_rect(sel_rect, clip_bounds) {
1171 primitives.push(DrawPrimitive::Rect {
1172 rect: clipped,
1173 brush: selection_brush.clone(),
1174 stroke: None,
1175 });
1176 }
1177 }
1178 scope.push_recorded(primitives);
1179 }))
1180 }
1181}
1182
1183impl SemanticsNode for TextFieldModifierNode {
1184 fn merge_semantics(&self, config: &mut SemanticsConfiguration) {
1185 let text = self.state.text();
1186 config.content_description = Some(text);
1187 config.is_editable_text = true;
1188 config.text_selection = Some(self.state.selection());
1189 }
1190}
1191
1192impl PointerInputNode for TextFieldModifierNode {
1193 fn on_pointer_event(
1194 &mut self,
1195 _context: &mut dyn ModifierNodeContext,
1196 _event: &PointerEvent,
1197 ) -> bool {
1198 false
1199 }
1200
1201 fn hit_test(&self, x: f32, y: f32) -> bool {
1202 let size = self.measured_size.get();
1203 x >= 0.0 && x <= size.width && y >= 0.0 && y <= size.height
1204 }
1205
1206 fn pointer_input_handler(&self) -> Option<Rc<dyn Fn(PointerEvent)>> {
1207 Some(self.cached_handler.clone())
1208 }
1209}
1210
1211#[derive(Clone)]
1218pub struct TextFieldElement {
1219 state: TextFieldState,
1220 style: TextStyle,
1221 cursor_color: Color,
1222 line_limits: TextFieldLineLimits,
1223 handle_controller: Option<TextFieldHandleController>,
1224 modal_depth: usize,
1225}
1226
1227impl TextFieldElement {
1228 pub fn new(state: TextFieldState, style: TextStyle) -> Self {
1230 Self {
1231 state,
1232 style,
1233 cursor_color: DEFAULT_CURSOR_COLOR,
1234 line_limits: TextFieldLineLimits::default(),
1235 handle_controller: None,
1236 modal_depth: 0,
1237 }
1238 }
1239
1240 pub fn with_cursor_color(mut self, color: Color) -> Self {
1242 self.cursor_color = color;
1243 self
1244 }
1245
1246 pub fn with_line_limits(mut self, line_limits: TextFieldLineLimits) -> Self {
1248 self.line_limits = line_limits;
1249 self
1250 }
1251
1252 pub fn with_handle_controller(mut self, controller: TextFieldHandleController) -> Self {
1254 self.handle_controller = Some(controller);
1255 self
1256 }
1257
1258 pub fn with_modal_depth(mut self, depth: usize) -> Self {
1261 self.modal_depth = depth;
1262 self
1263 }
1264}
1265
1266impl std::fmt::Debug for TextFieldElement {
1267 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1268 f.debug_struct("TextFieldElement")
1269 .field("text", &self.state.text())
1270 .field("style", &self.style)
1271 .field("cursor_color", &self.cursor_color)
1272 .finish()
1273 }
1274}
1275
1276impl Hash for TextFieldElement {
1277 fn hash<H: Hasher>(&self, state: &mut H) {
1278 self.state.id().hash(state);
1279 self.cursor_color.0.to_bits().hash(state);
1280 self.cursor_color.1.to_bits().hash(state);
1281 self.cursor_color.2.to_bits().hash(state);
1282 self.cursor_color.3.to_bits().hash(state);
1283 self.style.render_hash().hash(state);
1284 self.line_limits.hash(state);
1285 self.modal_depth.hash(state);
1286 }
1287}
1288
1289impl PartialEq for TextFieldElement {
1290 fn eq(&self, other: &Self) -> bool {
1291 self.state == other.state
1292 && self.style == other.style
1293 && self.cursor_color == other.cursor_color
1294 && self.line_limits == other.line_limits
1295 && self.modal_depth == other.modal_depth
1296 }
1297}
1298
1299impl Eq for TextFieldElement {}
1300
1301impl ModifierNodeElement for TextFieldElement {
1302 type Node = TextFieldModifierNode;
1303
1304 fn create(&self) -> Self::Node {
1305 let mut node = TextFieldModifierNode::new(self.state, self.style.clone())
1306 .with_cursor_color(self.cursor_color)
1307 .with_line_limits(self.line_limits);
1308 node.modal_depth = self.modal_depth;
1309 node.refs.modal_depth.set(self.modal_depth);
1310 if let Some(controller) = self.handle_controller.clone() {
1311 node = node.with_handle_controller(controller);
1312 }
1313 node.rebuild_cached_closures();
1314 node
1315 }
1316
1317 fn update(&self, node: &mut Self::Node) {
1318 node.state = self.state;
1319 node.style = self.style.clone();
1320 node.cursor_brush = Brush::solid(self.cursor_color);
1321 node.line_limits = self.line_limits;
1322 node.handle_controller = self.handle_controller.clone();
1323 node.modal_depth = self.modal_depth;
1324 node.refs.modal_depth.set(self.modal_depth);
1325 node.rebuild_cached_closures();
1326
1327 if node.update_cached_state() {}
1328 }
1329
1330 fn capabilities(&self) -> NodeCapabilities {
1331 NodeCapabilities::LAYOUT
1332 | NodeCapabilities::DRAW
1333 | NodeCapabilities::SEMANTICS
1334 | NodeCapabilities::POINTER_INPUT
1335 }
1336
1337 fn always_update(&self) -> bool {
1338 true
1339 }
1340}
1341
1342#[cfg(test)]
1343mod tests {
1344 use std::sync::Arc;
1345
1346 use cranpose_core::{DefaultScheduler, Runtime};
1347
1348 use super::*;
1349 use crate::text::TextStyle;
1350
1351 fn with_test_runtime<T>(f: impl FnOnce() -> T) -> T {
1352 let _runtime = Runtime::new(Arc::new(DefaultScheduler));
1353 f()
1354 }
1355
1356 #[test]
1357 fn text_field_node_creation() {
1358 let _app_context = crate::render_state::app_context_test_scope();
1359 with_test_runtime(|| {
1360 let state = TextFieldState::new("Hello");
1361 let node = TextFieldModifierNode::new(state, TextStyle::default());
1362 assert_eq!(node.text(), "Hello");
1363 assert!(!node.is_focused());
1364 });
1365 }
1366
1367 #[test]
1368 fn selection_rects_follow_wrapped_visual_lines() {
1369 let _app_context = crate::render_state::app_context_test_scope();
1370 let text = "aaaaa\nbb";
1371 let style = TextStyle::default();
1372 let line_height = 10.0_f32;
1373
1374 let rects = range_visual_line_rects(
1375 text,
1376 &style,
1377 None,
1378 Some(30.0),
1379 0.0,
1380 0.0,
1381 0.0,
1382 line_height,
1383 6,
1384 8,
1385 );
1386 assert_eq!(rects.len(), 1, "one visual line touched, got {rects:?}");
1387 assert_eq!(
1388 rects[0].y,
1389 2.0 * line_height,
1390 "highlight must land on visual line 2, not logical line 1"
1391 );
1392 assert!(rects[0].width > 0.0);
1393
1394 let spanning = range_visual_line_rects(
1395 text,
1396 &style,
1397 None,
1398 Some(30.0),
1399 0.0,
1400 0.0,
1401 0.0,
1402 line_height,
1403 0,
1404 5,
1405 );
1406 assert_eq!(spanning.len(), 2, "wrapped line spans two visual rows");
1407 assert_eq!(spanning[0].y, 0.0);
1408 assert_eq!(spanning[1].y, line_height);
1409 }
1410
1411 #[test]
1412 fn tap_resolves_offset_on_wrapped_visual_line() {
1413 let _app_context = crate::render_state::app_context_test_scope();
1414 let text = "aaaaa\nbb";
1415 let style = TextStyle::default();
1416 let line_height = 10.0_f32;
1417
1418 let off = crate::text::offset_for_position_wrapped(
1419 text,
1420 &style,
1421 None,
1422 Some(30.0),
1423 line_height,
1424 8.0,
1425 22.0,
1426 );
1427 assert!(
1428 (6..=8).contains(&off),
1429 "tap on visual line 'bb' resolved to {off}, expected 6..=8"
1430 );
1431
1432 let off1 = crate::text::offset_for_position_wrapped(
1433 text,
1434 &style,
1435 None,
1436 Some(30.0),
1437 line_height,
1438 4.0,
1439 12.0,
1440 );
1441 assert!(
1442 (3..=5).contains(&off1),
1443 "tap on wrapped 'aa' resolved to {off1}, expected 3..=5"
1444 );
1445
1446 let off2 = crate::text::offset_for_position_wrapped(
1447 "hello",
1448 &style,
1449 None,
1450 None,
1451 line_height,
1452 0.0,
1453 0.0,
1454 );
1455 assert_eq!(off2, 0);
1456 }
1457
1458 #[test]
1459 fn text_field_node_focus() {
1460 let _app_context = crate::render_state::app_context_test_scope();
1461 with_test_runtime(|| {
1462 let state = TextFieldState::new("Test");
1463 let mut node = TextFieldModifierNode::new(state, TextStyle::default());
1464 assert!(!node.is_focused());
1465
1466 node.set_focused(true);
1467 assert!(node.is_focused());
1468
1469 node.set_focused(false);
1470 assert!(!node.is_focused());
1471 });
1472 }
1473
1474 #[test]
1475 fn text_field_element_creates_node() {
1476 let _app_context = crate::render_state::app_context_test_scope();
1477 with_test_runtime(|| {
1478 let state = TextFieldState::new("Hello World");
1479 let element = TextFieldElement::new(state, TextStyle::default());
1480
1481 let node = element.create();
1482 assert_eq!(node.text(), "Hello World");
1483 });
1484 }
1485
1486 #[test]
1487 fn every_primary_pointer_source_publishes_direct_manipulation_metrics() {
1488 use cranpose_foundation::{PointerEvent, PointerEventKind, PointerSource};
1489 use cranpose_ui_graphics::Point;
1490
1491 let _app_context = crate::render_state::app_context_test_scope();
1492 with_test_runtime(|| {
1493 let state = TextFieldState::new("hello world");
1494 let controller = TextFieldHandleController::new();
1495 let mut node = TextFieldModifierNode::new(state, TextStyle::default())
1496 .with_handle_controller(controller.clone());
1497 node.measured_size.set(Size {
1498 width: 120.0,
1499 height: 20.0,
1500 });
1501
1502 let handler = node
1503 .pointer_input_handler()
1504 .expect("field exposes a pointer handler");
1505 let draw = node
1506 .create_draw_closure()
1507 .expect("field exposes a draw closure");
1508 let at = Point { x: 12.0, y: 8.0 };
1509 let size = Size {
1510 width: 120.0,
1511 height: 20.0,
1512 };
1513 let run_draw = || {
1514 let mut scope = crate::draw::command_draw_scope(size);
1515 draw(&mut scope);
1516 };
1517
1518 node.set_focused(true);
1519 run_draw();
1520 let keyboard_metrics = controller
1521 .metrics()
1522 .expect("focused field publishes handle metrics");
1523 assert!(!keyboard_metrics.direct_manipulation);
1524
1525 handler(
1526 PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Touch),
1527 );
1528 run_draw();
1529 let metrics = controller
1530 .metrics()
1531 .expect("focused field publishes handle metrics");
1532 assert!(metrics.focused, "a tap focuses the field");
1533 assert!(
1534 metrics.direct_manipulation,
1535 "a touch tap must expose direct-manipulation handles"
1536 );
1537 assert!(
1538 controller.press().is_some(),
1539 "touch must publish the live press"
1540 );
1541
1542 handler(
1543 PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Mouse),
1544 );
1545 run_draw();
1546 let metrics = controller
1547 .metrics()
1548 .expect("focused field publishes handle metrics");
1549 assert!(
1550 metrics.direct_manipulation,
1551 "a mouse tap must expose the same direct-manipulation handles"
1552 );
1553 assert!(
1554 controller.press().is_some(),
1555 "mouse must publish the live press"
1556 );
1557
1558 handler(
1559 PointerEvent::new(PointerEventKind::Down, at, at)
1560 .with_source(PointerSource::Stylus),
1561 );
1562 run_draw();
1563 let metrics = controller
1564 .metrics()
1565 .expect("focused field publishes handle metrics");
1566 assert!(
1567 metrics.direct_manipulation,
1568 "a stylus contact must expose the same direct-manipulation handles"
1569 );
1570 assert!(
1571 controller.press().is_some(),
1572 "stylus must publish the live press"
1573 );
1574
1575 crate::text_field_focus::clear_focus();
1576 });
1577 }
1578
1579 #[test]
1580 fn double_tap_selects_the_word_under_the_finger() {
1581 use cranpose_foundation::{PointerEvent, PointerEventKind, PointerSource};
1582 use cranpose_ui_graphics::Point;
1583
1584 let _app_context = crate::render_state::app_context_test_scope();
1585 with_test_runtime(|| {
1586 let state = TextFieldState::new("hello world");
1587 let node = TextFieldModifierNode::new(state, TextStyle::default());
1588 node.measured_size.set(Size {
1589 width: 200.0,
1590 height: 20.0,
1591 });
1592 let handler = node
1593 .pointer_input_handler()
1594 .expect("field exposes a pointer handler");
1595
1596 let at = Point { x: 2.0, y: 8.0 };
1597 handler(
1598 PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Touch),
1599 );
1600 handler(
1601 PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Touch),
1602 );
1603
1604 let selection = state.selection();
1605 assert!(
1606 !selection.collapsed(),
1607 "a double tap must produce a (word) selection, got {selection:?}"
1608 );
1609 let selected = &state.text()[selection.min()..selection.max()];
1610 assert_eq!(
1611 selected, "hello",
1612 "double tap should select the whole word under the finger"
1613 );
1614
1615 crate::text_field_focus::clear_focus();
1616 });
1617 }
1618
1619 #[test]
1620 fn repeated_taps_escalate_word_line_paragraph_then_cycle() {
1621 use cranpose_foundation::{PointerEvent, PointerEventKind, PointerSource};
1622 use cranpose_ui_graphics::Point;
1623
1624 let _app_context = crate::render_state::app_context_test_scope();
1625 with_test_runtime(|| {
1626 let text = "alpha beta\ngamma delta\n\nsecond para";
1627 let state = TextFieldState::new(text);
1628 let node = TextFieldModifierNode::new(state, TextStyle::default()).with_line_limits(
1629 TextFieldLineLimits::MultiLine {
1630 min_lines: 1,
1631 max_lines: usize::MAX,
1632 },
1633 );
1634 node.measured_size.set(Size {
1635 width: 400.0,
1636 height: 80.0,
1637 });
1638 let handler = node
1639 .pointer_input_handler()
1640 .expect("field exposes a pointer handler");
1641
1642 let at = Point { x: 2.0, y: 4.0 };
1643 let tap = || {
1644 handler(
1645 PointerEvent::new(PointerEventKind::Down, at, at)
1646 .with_source(PointerSource::Touch),
1647 );
1648 };
1649 let selected = |state: &TextFieldState| {
1650 let s = state.selection();
1651 state.text()[s.min()..s.max()].to_string()
1652 };
1653
1654 tap();
1655 assert!(state.selection().collapsed(), "first tap places the caret");
1656 tap();
1657 assert_eq!(selected(&state), "alpha", "double tap selects the word");
1658 tap();
1659 assert_eq!(
1660 selected(&state),
1661 "alpha beta",
1662 "triple tap selects the line"
1663 );
1664 tap();
1665 assert_eq!(
1666 selected(&state),
1667 "alpha beta\ngamma delta",
1668 "fourth tap grows to the paragraph"
1669 );
1670 tap();
1671 assert_eq!(
1672 selected(&state),
1673 "alpha",
1674 "fifth tap cycles back to the word"
1675 );
1676
1677 crate::text_field_focus::clear_focus();
1678 });
1679 }
1680
1681 #[test]
1682 fn single_tap_inside_selection_selects_the_word() {
1683 use cranpose_foundation::{PointerEvent, PointerEventKind, PointerSource};
1684 use cranpose_ui_graphics::Point;
1685
1686 let _app_context = crate::render_state::app_context_test_scope();
1687 with_test_runtime(|| {
1688 let state = TextFieldState::new("hello world");
1689 let node = TextFieldModifierNode::new(state, TextStyle::default());
1690 node.measured_size.set(Size {
1691 width: 200.0,
1692 height: 20.0,
1693 });
1694 let handler = node
1695 .pointer_input_handler()
1696 .expect("field exposes a pointer handler");
1697
1698 state.edit(|buffer| buffer.select(TextRange::new(0, 11)));
1699 assert!(!state.selection().collapsed());
1700
1701 let at = Point { x: 2.0, y: 8.0 };
1702 handler(
1703 PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Touch),
1704 );
1705
1706 let selection = state.selection();
1707 assert!(
1708 !selection.collapsed(),
1709 "a tap inside a selection must not collapse it, got {selection:?}"
1710 );
1711 assert_eq!(
1712 &state.text()[selection.min()..selection.max()],
1713 "hello",
1714 "a tap inside a selection re-selects the word under the finger"
1715 );
1716
1717 crate::text_field_focus::clear_focus();
1718 });
1719 }
1720
1721 #[test]
1722 fn slow_taps_inside_selection_cycle_word_line_paragraph_by_location() {
1723 use cranpose_foundation::{PointerEvent, PointerEventKind, PointerSource};
1724 use cranpose_ui_graphics::Point;
1725
1726 let _app_context = crate::render_state::app_context_test_scope();
1727 with_test_runtime(|| {
1728 let text = "alpha beta\ngamma delta\n\nsecond para";
1729 let state = TextFieldState::new(text);
1730 let node = TextFieldModifierNode::new(state, TextStyle::default()).with_line_limits(
1731 TextFieldLineLimits::MultiLine {
1732 min_lines: 1,
1733 max_lines: usize::MAX,
1734 },
1735 );
1736 node.measured_size.set(Size {
1737 width: 400.0,
1738 height: 80.0,
1739 });
1740 let handler = node
1741 .pointer_input_handler()
1742 .expect("field exposes a pointer handler");
1743
1744 state.edit(|buffer| buffer.select(TextRange::new(0, text.len())));
1745
1746 let at = Point { x: 2.0, y: 4.0 };
1747 let selected = |state: &TextFieldState| {
1748 let s = state.selection();
1749 state.text()[s.min()..s.max()].to_string()
1750 };
1751 let slow_tap = || {
1752 node.refs.last_click_time.set(None);
1753 handler(
1754 PointerEvent::new(PointerEventKind::Down, at, at)
1755 .with_source(PointerSource::Touch),
1756 );
1757 };
1758
1759 slow_tap();
1760 assert_eq!(
1761 selected(&state),
1762 "alpha",
1763 "tap inside selection grabs the word"
1764 );
1765 slow_tap();
1766 assert_eq!(
1767 selected(&state),
1768 "alpha beta",
1769 "same-spot tap grows to the line even after the timeout"
1770 );
1771 slow_tap();
1772 assert_eq!(
1773 selected(&state),
1774 "alpha beta\ngamma delta",
1775 "same-spot tap grows to the paragraph"
1776 );
1777 slow_tap();
1778 assert_eq!(
1779 selected(&state),
1780 "alpha",
1781 "same-spot tap cycles back to the word"
1782 );
1783
1784 crate::text_field_focus::clear_focus();
1785 });
1786 }
1787
1788 #[test]
1789 fn text_field_element_equality() {
1790 let _app_context = crate::render_state::app_context_test_scope();
1791 with_test_runtime(|| {
1792 let state1 = TextFieldState::new("Hello");
1793 let state2 = TextFieldState::new("Hello");
1794
1795 let elem1 = TextFieldElement::new(state1, TextStyle::default());
1796 let elem2 = TextFieldElement::new(state1, TextStyle::default());
1797 let elem3 = TextFieldElement::new(state2, TextStyle::default());
1798
1799 assert_eq!(elem1, elem2, "Same state should be equal");
1800 assert_ne!(elem1, elem3, "Different states should not be equal");
1801 });
1802 }
1803
1804 #[test]
1805 fn text_field_element_update_refreshes_existing_node_style() {
1806 let _app_context = crate::render_state::app_context_test_scope();
1807 with_test_runtime(|| {
1808 let state = TextFieldState::new("themed text");
1809 let dark_style = TextStyle::from_span_style(crate::text::SpanStyle {
1810 color: Some(Color::from_rgba_u8(228, 240, 252, 255)),
1811 ..crate::text::SpanStyle::default()
1812 });
1813 let light_style = TextStyle::from_span_style(crate::text::SpanStyle {
1814 color: Some(Color::from_rgba_u8(14, 58, 96, 255)),
1815 ..crate::text::SpanStyle::default()
1816 });
1817 let initial = TextFieldElement::new(state, dark_style);
1818 let updated = TextFieldElement::new(state, light_style.clone());
1819 let mut node = initial.create();
1820
1821 updated.update(&mut node);
1822
1823 assert_eq!(node.text(), "themed text");
1824 assert_eq!(node.style(), &light_style);
1825 });
1826 }
1827
1828 #[test]
1829 fn multiline_field_measures_wrapped_height() {
1830 let _app_context = crate::render_state::app_context_test_scope();
1831 with_test_runtime(|| {
1832 let long = "abcd ".repeat(40);
1833 let state = TextFieldState::new(&long);
1834 let node = TextFieldModifierNode::new(state, TextStyle::default());
1835 assert!(
1836 !node.line_limits().is_single_line(),
1837 "default fields are multi-line"
1838 );
1839
1840 let natural = node.measure_text_content(None);
1841 let wrapped = node.measure_text_content(node.wrap_width(20.0));
1842
1843 assert!(
1844 wrapped.height > natural.height,
1845 "wrapped multi-line height {} must exceed the single-line height {}",
1846 wrapped.height,
1847 natural.height
1848 );
1849 });
1850 }
1851
1852 #[test]
1853 fn single_line_field_never_wraps() {
1854 let _app_context = crate::render_state::app_context_test_scope();
1855 with_test_runtime(|| {
1856 let state = TextFieldState::new("abcd ".repeat(40));
1857 let node = TextFieldModifierNode::new(state, TextStyle::default())
1858 .with_line_limits(TextFieldLineLimits::SingleLine);
1859 assert_eq!(
1860 node.wrap_width(20.0),
1861 None,
1862 "single-line fields must not wrap"
1863 );
1864 });
1865 }
1866
1867 #[test]
1868 fn test_cursor_x_position_calculation() {
1869 let _app_context = crate::render_state::app_context_test_scope();
1870 with_test_runtime(|| {
1871 let style = crate::text::TextStyle::default();
1872
1873 let empty_width =
1874 crate::text::measure_text(&crate::text::AnnotatedString::from(""), &style).width;
1875 assert!(
1876 empty_width.abs() < 0.1,
1877 "Empty text should have 0 width, got {}",
1878 empty_width
1879 );
1880
1881 let hi_width =
1882 crate::text::measure_text(&crate::text::AnnotatedString::from("Hi"), &style).width;
1883 assert!(
1884 hi_width > 0.0,
1885 "Text 'Hi' should have positive width: {}",
1886 hi_width
1887 );
1888
1889 let h_width =
1890 crate::text::measure_text(&crate::text::AnnotatedString::from("H"), &style).width;
1891 assert!(h_width > 0.0, "Text 'H' should have positive width");
1892 assert!(
1893 h_width < hi_width,
1894 "'H' width {} should be less than 'Hi' width {}",
1895 h_width,
1896 hi_width
1897 );
1898
1899 let state = TextFieldState::new("Hi");
1900 assert_eq!(
1901 state.selection().start,
1902 2,
1903 "Cursor should be at position 2 (end of 'Hi')"
1904 );
1905
1906 let text = state.text();
1907 let cursor_pos = state.selection().start;
1908 let text_before_cursor = &text[..cursor_pos.min(text.len())];
1909 assert_eq!(text_before_cursor, "Hi");
1910
1911 let cursor_x = crate::text::measure_text(
1912 &crate::text::AnnotatedString::from(text_before_cursor),
1913 &style,
1914 )
1915 .width;
1916 assert!(
1917 (cursor_x - hi_width).abs() < 0.1,
1918 "Cursor x {} should equal 'Hi' width {}",
1919 cursor_x,
1920 hi_width
1921 );
1922 });
1923 }
1924
1925 #[test]
1926 fn test_focused_node_creates_cursor() {
1927 let _app_context = crate::render_state::app_context_test_scope();
1928 with_test_runtime(|| {
1929 let state = TextFieldState::new("Test");
1930 let element = TextFieldElement::new(state, TextStyle::default());
1931 let node = element.create();
1932
1933 assert!(!node.is_focused());
1934
1935 *node.refs.is_focused.borrow_mut() = true;
1936 assert!(node.is_focused());
1937
1938 assert_eq!(node.text(), "Test");
1939
1940 assert_eq!(node.selection().start, 4);
1941 });
1942 }
1943
1944 #[test]
1945 fn a_focus_requester_makes_the_text_field_receive_keyboard_input() {
1946 use cranpose_foundation::{BasicModifierNodeContext, ModifierNodeChain};
1947
1948 use crate::{
1949 key_event::{KeyCode, KeyEvent, KeyEventType, Modifiers},
1950 modifier::{FocusRequester, FocusRequesterElement},
1951 };
1952
1953 let _app_context = crate::render_state::app_context_test_scope();
1954 with_test_runtime(|| {
1955 let state = TextFieldState::new("");
1956 let requester = FocusRequester::new();
1957
1958 let mut context = BasicModifierNodeContext::new();
1959 context.set_node_id(Some(1));
1960 let mut chain = ModifierNodeChain::new();
1961 chain.update(
1962 vec![
1963 cranpose_foundation::modifier_element(FocusRequesterElement::new(
1964 requester.clone(),
1965 )),
1966 cranpose_foundation::modifier_element(TextFieldElement::new(
1967 state,
1968 TextStyle::default(),
1969 )),
1970 ],
1971 &mut context,
1972 );
1973
1974 assert!(!crate::text_field_focus::has_focused_field());
1975
1976 requester
1977 .request_focus()
1978 .expect("the text field must accept a programmatic focus request");
1979
1980 assert!(crate::text_field_focus::has_focused_field());
1981
1982 let key_down = KeyEvent::new(KeyCode::H, "h", Modifiers::NONE, KeyEventType::KeyDown);
1983 assert!(
1984 crate::text_field_focus::dispatch_key_event(&key_down),
1985 "the field must consume a key event once focused programmatically"
1986 );
1987 assert_eq!(state.text(), "h");
1988 });
1989 }
1990
1991 #[test]
1992 fn two_text_fields_hand_off_keyboard_focus_via_their_requesters() {
1993 use cranpose_foundation::{BasicModifierNodeContext, ModifierNodeChain};
1994
1995 use crate::modifier::{FocusRequester, FocusRequesterElement};
1996
1997 let _app_context = crate::render_state::app_context_test_scope();
1998 with_test_runtime(|| {
1999 let state_a = TextFieldState::new("a-text");
2000 let state_b = TextFieldState::new("b-text");
2001 let requester_a = FocusRequester::new();
2002 let requester_b = FocusRequester::new();
2003
2004 let mut context = BasicModifierNodeContext::new();
2005 context.set_node_id(Some(1));
2006 let mut chain_a = ModifierNodeChain::new();
2007 chain_a.update(
2008 vec![
2009 cranpose_foundation::modifier_element(FocusRequesterElement::new(
2010 requester_a.clone(),
2011 )),
2012 cranpose_foundation::modifier_element(TextFieldElement::new(
2013 state_a,
2014 TextStyle::default(),
2015 )),
2016 ],
2017 &mut context,
2018 );
2019
2020 context.set_node_id(Some(2));
2021 let mut chain_b = ModifierNodeChain::new();
2022 chain_b.update(
2023 vec![
2024 cranpose_foundation::modifier_element(FocusRequesterElement::new(
2025 requester_b.clone(),
2026 )),
2027 cranpose_foundation::modifier_element(TextFieldElement::new(
2028 state_b,
2029 TextStyle::default(),
2030 )),
2031 ],
2032 &mut context,
2033 );
2034
2035 requester_a.request_focus().expect("field a accepts focus");
2036 assert_eq!(
2037 crate::text_field_focus::focused_field_node(),
2038 Some(1),
2039 "field a should own text-field keyboard focus"
2040 );
2041
2042 requester_b.request_focus().expect("field b accepts focus");
2043 assert_eq!(
2044 crate::text_field_focus::focused_field_node(),
2045 Some(2),
2046 "field b must take over text-field keyboard focus from field a"
2047 );
2048 });
2049 }
2050}