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