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