Skip to main content

cranpose_ui/
text_field_modifier_node.rs

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