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, 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, ProjectiveTransform};
16use cranpose_ui_layout::ceil_to_px;
17
18#[derive(Clone, Copy, PartialEq, Debug)]
19pub struct TextFieldHandleMetrics {
20    pub focused: bool,
21    pub direct_manipulation: bool,
22    /// Maps the field's local coordinates to its window.
23    pub local_to_window: ProjectiveTransform,
24    pub padding_left: f32,
25    pub padding_top: f32,
26    pub scroll_offset: f32,
27    pub line_height: f32,
28    pub glyph_box: (f32, f32),
29    pub wrap_width: Option<f32>,
30}
31
32#[derive(Clone)]
33pub struct TextFieldHandleController {
34    inner: Rc<TextFieldHandleControllerInner>,
35}
36
37impl PartialEq for TextFieldHandleController {
38    fn eq(&self, other: &Self) -> bool {
39        Rc::ptr_eq(&self.inner, &other.inner)
40    }
41}
42
43struct TextFieldHandleControllerInner {
44    metrics: Cell<Option<TextFieldHandleMetrics>>,
45    activation: MutableState<u64>,
46    geometry: MutableState<u64>,
47    gesture_claim: RefCell<Option<Rc<Cell<bool>>>>,
48    press_track: Cell<Option<MutableState<Option<PointerPressTrack>>>>,
49}
50
51impl TextFieldHandleController {
52    pub fn new() -> Self {
53        Self {
54            inner: Rc::new(TextFieldHandleControllerInner {
55                metrics: Cell::new(None),
56                activation: mutableStateOf(0u64),
57                geometry: mutableStateOf(0u64),
58                gesture_claim: RefCell::new(None),
59                press_track: Cell::new(None),
60            }),
61        }
62    }
63
64    pub(crate) fn publish(&self, metrics: TextFieldHandleMetrics) {
65        let previous = self.inner.metrics.replace(Some(metrics));
66        if previous == Some(metrics) {
67            return;
68        }
69        let was_live = previous.map(|metrics| (metrics.focused, metrics.direct_manipulation));
70        if was_live != Some((metrics.focused, metrics.direct_manipulation)) {
71            self.bump(&self.inner.activation);
72        }
73        self.bump(&self.inner.geometry);
74    }
75
76    fn bump(&self, revision: &MutableState<u64>) {
77        revision.update(|value| *value = value.wrapping_add(1));
78    }
79
80    /// The field's current handle metrics, subscribing the caller to whether
81    /// the handles are live -- to [`TextFieldHandleMetrics::focused`] and
82    /// [`TextFieldHandleMetrics::direct_manipulation`], and to nothing else.
83    ///
84    /// The rest of the metrics describe where the field sits, and a field
85    /// moves for reasons that have nothing to do with its handles: a list
86    /// scrolling under it, a bounce settling, a layout shifting by a
87    /// fraction of a pixel. A caller that only wants to know whether to emit
88    /// handles must not be recomposed by any of that, so geometry is not part
89    /// of this subscription. Use [`Self::live_metrics`] once the handles are
90    /// live and their position has to follow the field, and
91    /// [`Self::metrics_now`] outside composition.
92    pub fn metrics(&self) -> Option<TextFieldHandleMetrics> {
93        let _ = self.inner.activation.value();
94        self.inner.metrics.get()
95    }
96
97    /// The field's current handle metrics, subscribing the caller to the
98    /// field's position as well.
99    ///
100    /// For a caller that places something at the caret and has to keep it
101    /// there while the field moves. Reach for it only past a
102    /// [`TextFieldHandleMetrics::focused`] check, because every caller that
103    /// holds this subscription recomposes whenever the field moves at all.
104    pub fn live_metrics(&self) -> Option<TextFieldHandleMetrics> {
105        let _ = self.inner.geometry.value();
106        self.inner.metrics.get()
107    }
108
109    /// The field's current handle metrics, subscribing the caller to nothing.
110    ///
111    /// For gesture callbacks and effects, which run after composition and want
112    /// the position as it is now rather than as it was when their scope last
113    /// composed.
114    pub fn metrics_now(&self) -> Option<TextFieldHandleMetrics> {
115        self.inner.metrics.get()
116    }
117
118    pub(crate) fn adopt_gesture_claim(&self, claim: &Rc<Cell<bool>>) {
119        let mut slot = self.inner.gesture_claim.borrow_mut();
120        let adopted = slot.as_ref().is_some_and(|held| Rc::ptr_eq(held, claim));
121        if !adopted {
122            *slot = Some(Rc::clone(claim));
123        }
124    }
125
126    pub(crate) fn adopt_press_track(&self, press_track: MutableState<Option<PointerPressTrack>>) {
127        if self.inner.press_track.get() != Some(press_track) {
128            self.inner.press_track.set(Some(press_track));
129            self.bump(&self.inner.activation);
130        }
131    }
132
133    pub fn press(&self) -> Option<PointerPressTrack> {
134        self.inner.press_track.get().and_then(|state| state.get())
135    }
136
137    pub fn claim_gesture(&self) {
138        if let Some(claim) = self.inner.gesture_claim.borrow().as_ref() {
139            claim.set(true);
140        }
141    }
142
143    pub fn gesture_claimed(&self) -> bool {
144        self.inner
145            .gesture_claim
146            .borrow()
147            .as_ref()
148            .is_some_and(|claim| claim.get())
149    }
150}
151
152impl Default for TextFieldHandleController {
153    fn default() -> Self {
154        Self::new()
155    }
156}
157
158const DEFAULT_CURSOR_COLOR: Color = Color(1.0, 1.0, 1.0, 1.0);
159
160pub(crate) const DEFAULT_SELECTION_COLOR: Color = Color(0.0, 0.5, 1.0, 0.3);
161
162const DEFAULT_LINE_HEIGHT: f32 = 20.0;
163
164/// What Compose's `textFieldMinSize` lays out to find a field's smallest
165/// box: one line of ten 'H's.
166const MIN_SIZE_TEXT: &str = "HHHHHHHHHH";
167
168const CURSOR_WIDTH: f32 = 2.0;
169
170pub(crate) fn compute_horizontal_scroll_offset(
171    current_offset: f32,
172    cursor_x: f32,
173    text_width: f32,
174    viewport_width: f32,
175) -> f32 {
176    if viewport_width <= 0.0 {
177        return 0.0;
178    }
179    let max_offset = (text_width + CURSOR_WIDTH - viewport_width).max(0.0);
180    let mut offset = current_offset.clamp(0.0, max_offset);
181    let visible_end = offset + viewport_width - CURSOR_WIDTH;
182    if cursor_x > visible_end {
183        offset = cursor_x - viewport_width + CURSOR_WIDTH;
184    } else if cursor_x < offset {
185        offset = cursor_x;
186    }
187    offset.clamp(0.0, max_offset)
188}
189
190pub(crate) fn intersect_rect(
191    rect: cranpose_ui_graphics::Rect,
192    bounds: cranpose_ui_graphics::Rect,
193) -> Option<cranpose_ui_graphics::Rect> {
194    let x0 = rect.x.max(bounds.x);
195    let y0 = rect.y.max(bounds.y);
196    let x1 = (rect.x + rect.width).min(bounds.x + bounds.width);
197    let y1 = (rect.y + rect.height).min(bounds.y + bounds.height);
198    (x1 > x0 && y1 > y0).then_some(cranpose_ui_graphics::Rect {
199        x: x0,
200        y: y0,
201        width: x1 - x0,
202        height: y1 - y0,
203    })
204}
205
206/// Resolver that recomputes (and stores) the horizontal pan offset for a
207/// text field given the current content viewport width in px.
208pub type TextPanResolver = Rc<dyn Fn(f32) -> f32>;
209
210#[derive(Clone)]
211pub(crate) struct TextFieldLayoutHandle {
212    state: TextFieldState,
213    node_id: Rc<Cell<Option<cranpose_core::NodeId>>>,
214    wrap_width: Rc<Cell<Option<f32>>>,
215}
216
217impl TextFieldLayoutHandle {
218    pub(crate) fn measured_layout(
219        &self,
220        style: &TextStyle,
221    ) -> std::rc::Rc<crate::text::PreparedTextLayout> {
222        crate::text::prepare_text_layout_for_node(
223            self.node_id.get(),
224            &crate::text::AnnotatedString::from(self.state.text()),
225            style,
226            crate::text::TextLayoutOptions::default(),
227            self.wrap_width.get(),
228        )
229    }
230}
231
232pub(crate) fn caret_visual_line_for_offset(
233    text: &str,
234    style: &TextStyle,
235    node_id: Option<cranpose_core::NodeId>,
236    wrap_width: Option<f32>,
237    offset: usize,
238    affinity: crate::text_selection::LineAffinity,
239) -> (usize, usize) {
240    let offset = offset.min(text.len());
241    match wrap_width {
242        Some(width) if width.is_finite() && width > 0.0 => {
243            let annotated = crate::text::AnnotatedString::from(text);
244            let ranges = crate::text::wrapped_line_ranges(
245                node_id,
246                &annotated,
247                style,
248                crate::text::TextLayoutOptions::default(),
249                Some(width),
250            );
251            crate::text_selection::caret_visual_line(&ranges, offset, affinity)
252        }
253        _ => {
254            let before = &text[..offset];
255            let line_index = before.matches('\n').count();
256            let line_start = before.rfind('\n').map_or(0, |i| i + 1);
257            (line_index, line_start)
258        }
259    }
260}
261
262#[expect(clippy::too_many_arguments)]
263pub(crate) fn range_visual_line_rects(
264    text: &str,
265    style: &TextStyle,
266    node_id: Option<cranpose_core::NodeId>,
267    wrap_width: Option<f32>,
268    pan: f32,
269    line_height: f32,
270    start: usize,
271    end: usize,
272) -> Vec<cranpose_ui_graphics::Rect> {
273    if start >= end {
274        return Vec::new();
275    }
276    let annotated = crate::text::AnnotatedString::from(text);
277    let line_ranges = crate::text::wrapped_line_ranges(
278        node_id,
279        &annotated,
280        style,
281        crate::text::TextLayoutOptions::default(),
282        wrap_width,
283    );
284    let mut rects = Vec::new();
285    for (line_idx, line_range) in line_ranges.iter().enumerate() {
286        let line_start = line_range.start;
287        let line_end = line_range.end;
288        if end <= line_start || start >= line_end {
289            continue;
290        }
291        let seg_start = start.max(line_start);
292        let seg_end = end.min(line_end);
293        let x0 = crate::text::measure_text(
294            &crate::text::AnnotatedString::from(&text[line_start..seg_start]),
295            style,
296        )
297        .width
298            - pan;
299        let x1 = crate::text::measure_text(
300            &crate::text::AnnotatedString::from(&text[line_start..seg_end]),
301            style,
302        )
303        .width
304            - pan;
305        let width = x1 - x0;
306        if width > 0.0 {
307            rects.push(cranpose_ui_graphics::Rect {
308                x: x0,
309                y: line_idx as f32 * line_height,
310                width,
311                height: line_height,
312            });
313        }
314    }
315    rects
316}
317
318fn build_focus_handler(
319    state: TextFieldState,
320    refs: &TextFieldRefs,
321    line_limits: TextFieldLineLimits,
322    style: &TextStyle,
323) -> Rc<dyn crate::text_field_focus::FocusedTextFieldHandler> {
324    crate::text_field_handler::TextFieldHandler::new(
325        state,
326        refs.node_id.get(),
327        refs.focus_node.get(),
328        line_limits,
329        refs.focus_options.get().show_keyboard_on_focus,
330        crate::text_field_handler::CaretGeometryRefs {
331            local_to_window: refs.local_to_window.clone(),
332            content_origin: refs.content_origin.clone(),
333            scroll_offset: refs.scroll_offset.clone(),
334            style: style.clone(),
335        },
336    )
337}
338
339fn request_pointer_focus(
340    state: TextFieldState,
341    refs: &TextFieldRefs,
342    line_limits: TextFieldLineLimits,
343    style: &TextStyle,
344    modal_depth: usize,
345) {
346    if modal_depth < crate::modal::current_modal_depth() {
347        return;
348    }
349    let routed = refs
350        .focus_node
351        .get()
352        .is_some_and(crate::focus_dispatch::request_focus_in_context);
353    if !routed {
354        crate::text_field_focus::request_focus(
355            refs.is_focused.clone(),
356            build_focus_handler(state, refs, line_limits, style),
357            modal_depth,
358        );
359    }
360    if !refs.focus_options.get().show_keyboard_on_focus && *refs.is_focused.borrow() {
361        crate::text_input_session::notify_text_input_focus_gained();
362    }
363}
364
365pub(crate) struct TextFieldFocusBridge {
366    state: TextFieldState,
367    refs: TextFieldRefs,
368    style: TextStyle,
369    line_limits: TextFieldLineLimits,
370}
371
372impl TextFieldFocusBridge {
373    /// The focus target of the field with these refs.
374    pub(crate) fn handle(
375        state: TextFieldState,
376        refs: TextFieldRefs,
377        style: TextStyle,
378        line_limits: TextFieldLineLimits,
379    ) -> Rc<dyn crate::focus_dispatch::FocusTargetHandle> {
380        Rc::new(Self {
381            state,
382            refs,
383            style,
384            line_limits,
385        })
386    }
387}
388
389impl crate::focus_dispatch::FocusTargetHandle for TextFieldFocusBridge {
390    fn set_focus_state(&self, state: FocusState) {
391        if state.is_focused() {
392            crate::text_field_focus::request_focus(
393                self.refs.is_focused.clone(),
394                build_focus_handler(self.state, &self.refs, self.line_limits, &self.style),
395                self.refs.focus_options.get().modal_depth,
396            );
397        } else if crate::text_field_focus::focused_field_node() == self.refs.node_id.get() {
398            crate::text_field_focus::clear_focus();
399        }
400    }
401}
402
403#[derive(Clone, Copy)]
404struct TextFieldFocusOptions {
405    modal_depth: usize,
406    show_keyboard_on_focus: bool,
407}
408
409#[derive(Clone)]
410pub(crate) struct TextFieldRefs {
411    pub is_focused: Rc<RefCell<bool>>,
412    /// Where the field's content sits in its node: the rect its layout
413    /// placed the field at, after every layout modifier before it.
414    pub content_origin: Rc<RefCell<crate::modifier::CoordinatorRect>>,
415    pub drag_anchor: Rc<Cell<Option<crate::text_selection::SelectionAnchor>>>,
416    pub last_click_time: Rc<Cell<Option<web_time::Instant>>>,
417    pub last_click_pos: Rc<Cell<Option<(f32, f32)>>>,
418    pub click_count: Rc<Cell<u8>>,
419    pub node_id: Rc<Cell<Option<cranpose_core::NodeId>>>,
420    /// The node focus and semantics know the field by: the decoration box
421    /// around a decorated field, else the field's own node.
422    pub focus_node: Rc<Cell<Option<cranpose_core::NodeId>>>,
423    pub scroll_offset: Rc<Cell<f32>>,
424    pub direct_manipulation: Rc<Cell<bool>>,
425    pub local_to_window: Rc<Cell<ProjectiveTransform>>,
426    pub line_height: Rc<Cell<f32>>,
427    pub wrap_width: Rc<Cell<Option<f32>>>,
428    pub press_track: MutableState<Option<PointerPressTrack>>,
429    pub gesture_claimed: Rc<Cell<bool>>,
430    focus_options: Rc<Cell<TextFieldFocusOptions>>,
431}
432
433#[derive(Clone, Copy, Debug, PartialEq)]
434pub struct PointerPressTrack {
435    pub start: Point,
436    pub position: Point,
437}
438
439impl TextFieldRefs {
440    pub fn new() -> Self {
441        Self {
442            is_focused: Rc::new(RefCell::new(false)),
443            content_origin: Rc::default(),
444            drag_anchor: Rc::new(Cell::new(None)),
445            last_click_time: Rc::new(Cell::new(None::<web_time::Instant>)),
446            last_click_pos: Rc::new(Cell::new(None::<(f32, f32)>)),
447            click_count: Rc::new(Cell::new(0_u8)),
448            node_id: Rc::new(Cell::new(None::<cranpose_core::NodeId>)),
449            focus_node: Rc::new(Cell::new(None::<cranpose_core::NodeId>)),
450            scroll_offset: Rc::new(Cell::new(0.0_f32)),
451            direct_manipulation: Rc::new(Cell::new(false)),
452            local_to_window: Rc::new(Cell::new(ProjectiveTransform::identity())),
453            line_height: Rc::new(Cell::new(DEFAULT_LINE_HEIGHT)),
454            wrap_width: Rc::new(Cell::new(None::<f32>)),
455            press_track: mutableStateOf(None::<PointerPressTrack>),
456            gesture_claimed: Rc::new(Cell::new(false)),
457            focus_options: Rc::new(Cell::new(TextFieldFocusOptions {
458                modal_depth: 0,
459                show_keyboard_on_focus: true,
460            })),
461        }
462    }
463
464    /// Tells these refs from any others, so the nodes sharing them can be
465    /// keyed by them.
466    pub(crate) fn key(&self) -> u64 {
467        Rc::as_ptr(&self.is_focused) as usize as u64
468    }
469}
470
471impl PartialEq for TextFieldRefs {
472    fn eq(&self, other: &Self) -> bool {
473        Rc::ptr_eq(&self.is_focused, &other.is_focused)
474    }
475}
476
477use crate::text::TextStyle;
478
479pub struct TextFieldModifierNode {
480    state: TextFieldState,
481    refs: TextFieldRefs,
482    style: TextStyle,
483    cursor_brush: Brush,
484    selection_brush: Brush,
485    line_limits: TextFieldLineLimits,
486    cached_text: String,
487    cached_selection: TextRange,
488    node_state: NodeState,
489    measured_size: Rc<Cell<Size>>,
490    /// [`Self::min_size`] and the density it was taken at.
491    min_size: Cell<Option<(f32, Size)>>,
492    measured_line_height: Rc<Cell<f32>>,
493    measured_wrap_width: Rc<Cell<Option<f32>>>,
494    cached_handler: Rc<dyn Fn(PointerEvent)>,
495    cached_pan_resolver: TextPanResolver,
496    handle_controller: Option<TextFieldHandleController>,
497    modal_depth: usize,
498    focus_bridge: Option<Rc<dyn crate::focus_dispatch::FocusTargetHandle>>,
499    /// A decoration box around the field takes its input, focus and
500    /// semantics, sharing its refs; the field only lays out and draws.
501    decorated: bool,
502}
503
504impl std::fmt::Debug for TextFieldModifierNode {
505    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
506        f.debug_struct("TextFieldModifierNode")
507            .field("text", &self.state.text())
508            .field("style", &self.style)
509            .field("is_focused", &*self.refs.is_focused.borrow())
510            .finish()
511    }
512}
513
514impl TextFieldModifierNode {
515    /// Creates a new text field modifier node.
516    pub fn new(state: TextFieldState, style: TextStyle) -> Self {
517        Self::with_refs(state, style, TextFieldRefs::new(), false)
518    }
519
520    fn with_refs(
521        state: TextFieldState,
522        style: TextStyle,
523        refs: TextFieldRefs,
524        decorated: bool,
525    ) -> Self {
526        let value = state.value();
527        let refs_line_height = refs.line_height.clone();
528        let refs_wrap_width = refs.wrap_width.clone();
529        let line_limits = TextFieldLineLimits::default();
530        let cached_handler =
531            Self::create_handler(state, refs.clone(), line_limits, style.clone(), 0);
532        let cached_pan_resolver =
533            Self::create_pan_resolver(state, refs.clone(), line_limits, style.clone());
534
535        Self {
536            state,
537            refs,
538            style,
539            cursor_brush: Brush::solid(DEFAULT_CURSOR_COLOR),
540            selection_brush: Brush::solid(DEFAULT_SELECTION_COLOR),
541            line_limits,
542            cached_text: value.text,
543            cached_selection: value.selection,
544            node_state: NodeState::new(),
545            measured_size: Rc::new(Cell::new(Size {
546                width: 0.0,
547                height: 0.0,
548            })),
549            min_size: Cell::new(None),
550            measured_line_height: refs_line_height,
551            measured_wrap_width: refs_wrap_width,
552            cached_handler,
553            cached_pan_resolver,
554            handle_controller: None,
555            modal_depth: 0,
556            focus_bridge: None,
557            decorated,
558        }
559    }
560
561    /// Creates a node with custom line limits.
562    pub fn with_line_limits(mut self, line_limits: TextFieldLineLimits) -> Self {
563        self.line_limits = line_limits;
564        self.rebuild_cached_closures();
565        self
566    }
567
568    fn rebuild_cached_closures(&mut self) {
569        self.cached_handler = Self::create_handler(
570            self.state,
571            self.refs.clone(),
572            self.line_limits,
573            self.style.clone(),
574            self.modal_depth,
575        );
576        self.cached_pan_resolver = Self::create_pan_resolver(
577            self.state,
578            self.refs.clone(),
579            self.line_limits,
580            self.style.clone(),
581        );
582    }
583
584    /// Installs the controller the field publishes live handle metrics to.
585    pub fn with_handle_controller(mut self, controller: TextFieldHandleController) -> Self {
586        self.handle_controller = Some(controller);
587        self
588    }
589
590    fn create_pan_resolver(
591        state: TextFieldState,
592        refs: TextFieldRefs,
593        line_limits: TextFieldLineLimits,
594        style: TextStyle,
595    ) -> TextPanResolver {
596        Rc::new(move |viewport_width: f32| {
597            if !line_limits.is_single_line() {
598                refs.scroll_offset.set(0.0);
599                return 0.0;
600            }
601            let text = state.text();
602            let pos = state.selection().start.min(text.len());
603            let text_width = crate::text::measure_text(
604                &crate::text::AnnotatedString::from(text.as_str()),
605                &style,
606            )
607            .width;
608            let cursor_x = crate::text::measure_text(
609                &crate::text::AnnotatedString::from(&text[..pos]),
610                &style,
611            )
612            .width;
613            let offset = compute_horizontal_scroll_offset(
614                refs.scroll_offset.get(),
615                cursor_x,
616                text_width,
617                viewport_width,
618            );
619            refs.scroll_offset.set(offset);
620            offset
621        })
622    }
623
624    /// Returns the pan resolver for single-line fields, `None` for multi-line.
625    ///
626    /// Exposed to the modifier slices so the render scene builder can pan the
627    /// text glyphs by the same offset used for the cursor and selection.
628    pub fn text_pan_resolver(&self) -> Option<TextPanResolver> {
629        self.line_limits
630            .is_single_line()
631            .then(|| self.cached_pan_resolver.clone())
632    }
633
634    /// Returns the current horizontal scroll (pan) offset in px.
635    pub fn scroll_offset(&self) -> f32 {
636        self.refs.scroll_offset.get()
637    }
638
639    /// Returns the current line limits configuration.
640    pub fn line_limits(&self) -> TextFieldLineLimits {
641        self.line_limits
642    }
643
644    pub(crate) fn create_handler(
645        state: TextFieldState,
646        refs: TextFieldRefs,
647        line_limits: TextFieldLineLimits,
648        style: TextStyle,
649        modal_depth: usize,
650    ) -> Rc<dyn Fn(PointerEvent)> {
651        use crate::text_selection::{
652            MULTI_TAP_SLOP_PX, MULTI_TAP_TIMEOUT_MS, SelectionAnchor, SelectionGranularity,
653            classify_tap_count, resolve_selection_tap_count, tap_selection_granularity,
654        };
655
656        Rc::new(move |event: PointerEvent| {
657            let content = refs.content_origin.borrow().origin();
658            let click_x = (event.position.x - content.x + refs.scroll_offset.get()).max(0.0);
659            let click_y = (event.position.y - content.y).max(0.0);
660
661            match event.kind {
662                PointerEventKind::Down => {
663                    refs.direct_manipulation.set(true);
664                    refs.press_track.set(Some(PointerPressTrack {
665                        start: event.global_position,
666                        position: event.global_position,
667                    }));
668                    refs.gesture_claimed.set(false);
669
670                    request_pointer_focus(state, &refs, line_limits, &style, modal_depth);
671
672                    let now = web_time::Instant::now();
673                    let text = state.text();
674                    let pos = crate::text::offset_for_position_wrapped(
675                        &text,
676                        &style,
677                        refs.node_id.get(),
678                        refs.wrap_width.get(),
679                        refs.line_height.get(),
680                        click_x,
681                        click_y,
682                    );
683
684                    let previous = refs.last_click_pos.get().and_then(|(px, py)| {
685                        let count = refs.click_count.get();
686                        (count > 0).then_some((count, px, py))
687                    });
688                    let elapsed_ms = refs
689                        .last_click_time
690                        .get()
691                        .map_or(u128::MAX, |last| now.duration_since(last).as_millis());
692                    let tap_count = classify_tap_count(
693                        previous,
694                        elapsed_ms,
695                        event.position.x,
696                        event.position.y,
697                        MULTI_TAP_TIMEOUT_MS,
698                        MULTI_TAP_SLOP_PX,
699                    );
700
701                    let selection = state.selection();
702                    let tap_in_selection =
703                        !selection.collapsed() && pos >= selection.min() && pos <= selection.max();
704                    let repeat_in_place = refs.last_click_pos.get().is_some_and(|(px, py)| {
705                        let dx = event.position.x - px;
706                        let dy = event.position.y - py;
707                        dx * dx + dy * dy <= MULTI_TAP_SLOP_PX * MULTI_TAP_SLOP_PX
708                    });
709                    let effective_count = resolve_selection_tap_count(
710                        tap_count,
711                        refs.click_count.get(),
712                        tap_in_selection,
713                        repeat_in_place,
714                    );
715
716                    let granularity = tap_selection_granularity(effective_count);
717                    let anchor = SelectionAnchor::at(&text, pos, granularity);
718                    refs.drag_anchor.set(Some(anchor));
719                    state.edit(|buffer| match granularity {
720                        SelectionGranularity::Caret => buffer.place_cursor_before_char(pos),
721                        _ => buffer.select(TextRange::new(anchor.start, anchor.end)),
722                    });
723
724                    refs.click_count.set(effective_count);
725                    refs.last_click_time.set(Some(now));
726                    refs.last_click_pos
727                        .set(Some((event.position.x, event.position.y)));
728                    event.consume();
729                }
730                PointerEventKind::Move => {
731                    if let Some(mut track) = refs.press_track.get() {
732                        track.position = event.global_position;
733                        refs.press_track.set(Some(track));
734                        if let Some(node_id) = refs.node_id.get() {
735                            crate::schedule_draw_repass(node_id);
736                        }
737                        crate::request_render_invalidation();
738                    }
739                    if refs.gesture_claimed.get() {
740                        event.consume();
741                        return;
742                    }
743                    if let Some(anchor) = refs.drag_anchor.get()
744                        && *refs.is_focused.borrow()
745                    {
746                        let text = state.text();
747                        let current_pos = crate::text::offset_for_position_wrapped(
748                            &text,
749                            &style,
750                            refs.node_id.get(),
751                            refs.wrap_width.get(),
752                            refs.line_height.get(),
753                            click_x,
754                            click_y,
755                        );
756
757                        state.set_selection(anchor.dragged_to(&text, current_pos));
758
759                        crate::request_render_invalidation();
760
761                        event.consume();
762                    }
763                }
764                PointerEventKind::Up => {
765                    refs.drag_anchor.set(None);
766                    refs.press_track.set(None);
767                    refs.gesture_claimed.set(false);
768                    if let Some(node_id) = refs.node_id.get() {
769                        crate::schedule_draw_repass(node_id);
770                    }
771                    crate::request_render_invalidation();
772                }
773                PointerEventKind::Cancel => {
774                    refs.press_track.set(None);
775                    refs.gesture_claimed.set(false);
776                    if let Some(node_id) = refs.node_id.get() {
777                        crate::schedule_draw_repass(node_id);
778                    }
779                    crate::request_render_invalidation();
780                }
781                _ => {}
782            }
783        })
784    }
785
786    /// Creates a node with a custom accent: the caret is drawn solid in
787    /// `color` and the selection highlight is derived from it at
788    /// [`crate::widgets::SELECTION_HIGHLIGHT_ALPHA`] — the reference field
789    /// tints caret, handles and highlight from the one accent.
790    pub fn with_cursor_color(mut self, color: Color) -> Self {
791        self.cursor_brush = Brush::solid(color);
792        self.selection_brush = Brush::solid(
793            color.with_alpha(crate::widgets::basic_text_field::SELECTION_HIGHLIGHT_ALPHA),
794        );
795        self
796    }
797
798    /// Sets the focus state.
799    pub fn set_focused(&mut self, focused: bool) {
800        let current = *self.refs.is_focused.borrow();
801        if current != focused {
802            *self.refs.is_focused.borrow_mut() = focused;
803            if !focused {
804                self.refs.direct_manipulation.set(false);
805                self.refs.press_track.set(None);
806                self.refs.gesture_claimed.set(false);
807            }
808        }
809    }
810
811    /// Returns whether the field is focused.
812    pub fn is_focused(&self) -> bool {
813        *self.refs.is_focused.borrow()
814    }
815
816    pub(crate) fn window_transform_sink(&self) -> Rc<Cell<ProjectiveTransform>> {
817        self.refs.local_to_window.clone()
818    }
819
820    pub(crate) fn layout_handle(&self) -> TextFieldLayoutHandle {
821        TextFieldLayoutHandle {
822            state: self.state,
823            node_id: self.refs.node_id.clone(),
824            wrap_width: self.measured_wrap_width.clone(),
825        }
826    }
827
828    /// Returns the current text.
829    pub fn text(&self) -> String {
830        self.state.text()
831    }
832
833    pub fn style(&self) -> &TextStyle {
834        &self.style
835    }
836
837    /// Returns the current selection.
838    pub fn selection(&self) -> TextRange {
839        self.state.selection()
840    }
841
842    /// Returns the cursor brush for rendering.
843    pub fn cursor_brush(&self) -> Brush {
844        self.cursor_brush.clone()
845    }
846
847    /// Returns the selection brush for rendering selection highlight.
848    pub fn selection_brush(&self) -> Brush {
849        self.selection_brush.clone()
850    }
851
852    /// Inserts text at the current cursor position (for paste operations).
853    pub fn insert_text(&mut self, text: &str) {
854        self.state.edit(|buffer| {
855            buffer.insert(text);
856        });
857    }
858
859    /// Copies the selected text and returns it (for web copy operation).
860    /// Returns None if no selection.
861    pub fn copy_selection(&self) -> Option<String> {
862        self.state.copy_selection()
863    }
864
865    /// Cuts the selected text: copies and deletes it.
866    /// Returns the cut text, or None if no selection.
867    pub fn cut_selection(&mut self) -> Option<String> {
868        let text = self.copy_selection();
869        if text.is_some() {
870            self.state.edit(|buffer| {
871                buffer.delete(buffer.selection());
872            });
873        }
874        text
875    }
876
877    /// Where the field's content sits in its node, set by slice collection
878    /// and placed by layout; clicks, the caret and the handles map through it.
879    pub(crate) fn set_content_origin(&self, origin: crate::modifier::CoordinatorRect) {
880        *self.refs.content_origin.borrow_mut() = origin;
881    }
882
883    fn wrap_width(&self, available_width: f32) -> Option<f32> {
884        (!self.line_limits.is_single_line() && available_width.is_finite() && available_width > 0.0)
885            .then_some(available_width)
886    }
887
888    fn measure_text_content(&self, wrap_width: Option<f32>) -> crate::text::TextMetrics {
889        let text = self.state.text();
890        let node_id = self.refs.node_id.get();
891        let annotated = crate::text::AnnotatedString::from(text.as_str());
892        let metrics = match wrap_width {
893            Some(max_width) => crate::text::measure_text_with_options_for_node(
894                node_id,
895                &annotated,
896                &self.style,
897                crate::text::TextLayoutOptions::default(),
898                Some(max_width),
899            ),
900            None => crate::text::measure_text_for_node(node_id, &annotated, &self.style),
901        };
902        self.measured_line_height.set(metrics.line_height);
903        metrics
904    }
905
906    /// The text's size on `density`'s grid: Compose sizes a field's layout
907    /// as its paragraph's size, `ceil`ed to whole pixels.
908    fn text_size(&self, wrap_width: Option<f32>, density: f32) -> (Size, crate::text::TextMetrics) {
909        let metrics = self.measure_text_content(wrap_width);
910        (
911            Size {
912                width: ceil_to_px(metrics.width, density),
913                height: ceil_to_px(metrics.height, density),
914            },
915            metrics,
916        )
917    }
918
919    /// The smallest box the field takes, as Compose's `textFieldMinSize`
920    /// gives it: one line of ten 'H's in its style, so an empty field is as
921    /// tall as a line and wide enough to type into.
922    fn min_size(&self, density: f32) -> Size {
923        if let Some((taken_at, size)) = self.min_size.get()
924            && taken_at == density
925        {
926            return size;
927        }
928        let metrics = crate::text::measure_text(
929            &crate::text::AnnotatedString::from(MIN_SIZE_TEXT),
930            &self.style,
931        );
932        let size = Size {
933            width: ceil_to_px(metrics.width, density),
934            height: ceil_to_px(metrics.height, density),
935        };
936        self.min_size.set(Some((density, size)));
937        size
938    }
939
940    fn update_cached_state(&mut self) -> bool {
941        let value = self.state.value();
942        let text_changed = value.text != self.cached_text;
943        let selection_changed = value.selection != self.cached_selection;
944
945        if text_changed {
946            self.cached_text = value.text;
947        }
948        if selection_changed {
949            self.cached_selection = value.selection;
950        }
951
952        text_changed || selection_changed
953    }
954}
955
956impl DelegatableNode for TextFieldModifierNode {
957    fn node_state(&self) -> &NodeState {
958        &self.node_state
959    }
960}
961
962impl ModifierNode for TextFieldModifierNode {
963    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
964        self.refs.node_id.set(context.node_id());
965
966        context.invalidate(InvalidationKind::Layout);
967        context.invalidate(InvalidationKind::Draw);
968        if self.decorated {
969            return;
970        }
971        context.invalidate(InvalidationKind::Semantics);
972        self.refs.focus_node.set(context.node_id());
973
974        if let Some(node_id) = context.node_id() {
975            let bridge = TextFieldFocusBridge::handle(
976                self.state,
977                self.refs.clone(),
978                self.style.clone(),
979                self.line_limits,
980            );
981            self.focus_bridge = Some(Rc::clone(&bridge));
982            crate::focus_dispatch::register_focus_target(node_id, bridge);
983        }
984    }
985
986    fn on_detach(&mut self) {
987        if let (Some(node_id), Some(bridge)) = (self.refs.node_id.get(), self.focus_bridge.take()) {
988            crate::focus_dispatch::unregister_focus_target(node_id, &bridge);
989        }
990    }
991
992    fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
993        Some(self)
994    }
995
996    fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
997        Some(self)
998    }
999
1000    fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
1001        Some(self)
1002    }
1003
1004    fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
1005        Some(self)
1006    }
1007
1008    cranpose_foundation::impl_semantics_node!();
1009
1010    fn as_pointer_input_node(&self) -> Option<&dyn PointerInputNode> {
1011        (!self.decorated).then_some(self)
1012    }
1013
1014    fn as_pointer_input_node_mut(&mut self) -> Option<&mut dyn PointerInputNode> {
1015        if self.decorated { None } else { Some(self) }
1016    }
1017}
1018
1019impl LayoutModifierNode for TextFieldModifierNode {
1020    fn measure(
1021        &self,
1022        context: &mut dyn ModifierNodeContext,
1023        _measurable: &dyn Measurable,
1024        constraints: Constraints,
1025    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
1026        let density = context.density();
1027        let wrap_width = self.wrap_width(constraints.max_width);
1028        self.measured_wrap_width.set(wrap_width);
1029        let (text, metrics) = self.text_size(wrap_width, density);
1030        let min = self.min_size(density);
1031        // The minimum joins the constraints' own, as a floor they bound.
1032        let fit = |length: f32, min: f32, low: f32, high: f32| {
1033            length.max(min.max(low).min(high)).min(high)
1034        };
1035        let size = Size {
1036            width: fit(
1037                text.width,
1038                min.width,
1039                constraints.min_width,
1040                constraints.max_width,
1041            ),
1042            height: fit(
1043                text.height,
1044                min.height,
1045                constraints.min_height,
1046                constraints.max_height,
1047            ),
1048        };
1049        self.measured_size.set(size);
1050
1051        let _ = (self.cached_pan_resolver)(size.width);
1052
1053        let first = crate::text::measure::text_line_box(&self.style)
1054            .map(crate::text::LineBox::first_baseline)
1055            .or_else(|| crate::text::measure::first_baseline(&self.style));
1056        let lines = cranpose_ui_layout::AlignmentLines::new(
1057            first,
1058            first.map(|baseline| {
1059                baseline + metrics.line_count.saturating_sub(1) as f32 * metrics.line_height
1060            }),
1061        );
1062        cranpose_ui_layout::LayoutModifierMeasureResult::with_size(size).with_alignment_lines(lines)
1063    }
1064
1065    // Compose's minimum size is a plain layout modifier, which leaves the
1066    // intrinsics to the text; an empty text is still a line tall.
1067    fn min_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32, density: f32) -> f32 {
1068        self.text_size(None, density).0.width
1069    }
1070
1071    fn max_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32, density: f32) -> f32 {
1072        self.text_size(None, density).0.width
1073    }
1074
1075    fn min_intrinsic_height(&self, _measurable: &dyn Measurable, width: f32, density: f32) -> f32 {
1076        self.intrinsic_height(width, density)
1077    }
1078
1079    fn max_intrinsic_height(&self, _measurable: &dyn Measurable, width: f32, density: f32) -> f32 {
1080        self.intrinsic_height(width, density)
1081    }
1082}
1083
1084impl TextFieldModifierNode {
1085    fn intrinsic_height(&self, width: f32, density: f32) -> f32 {
1086        self.text_size(self.wrap_width(width), density)
1087            .0
1088            .height
1089            .max(self.min_size(density).height)
1090    }
1091}
1092
1093/// The field's content viewport: its measured text area, or the draw
1094/// scope's size before the first measure. The draw scope is the field's
1095/// content rect, already inside the padding declared before the field.
1096fn content_viewport(
1097    measured: cranpose_ui_graphics::Size,
1098    size: cranpose_foundation::Size,
1099) -> (f32, f32) {
1100    let width = if measured.width > 0.0 {
1101        measured.width
1102    } else {
1103        size.width
1104    };
1105    let height = if measured.height > 0.0 {
1106        measured.height
1107    } else {
1108        size.height
1109    };
1110    (width, height)
1111}
1112
1113impl DrawModifierNode for TextFieldModifierNode {
1114    fn create_draw_closure(
1115        &self,
1116    ) -> Option<Rc<dyn Fn(&mut cranpose_ui_graphics::DrawScopeDefault)>> {
1117        use cranpose_ui_graphics::{DrawPrimitive, DrawScope as _};
1118
1119        let is_focused = self.refs.is_focused.clone();
1120        let state = self.state;
1121        let content_origin = self.refs.content_origin.clone();
1122        let cursor_brush = self.cursor_brush.clone();
1123        let style = self.style.clone();
1124        let cached_line_height = self.measured_line_height.clone();
1125        let measured_size = self.measured_size.clone();
1126        let measured_wrap_width = self.measured_wrap_width.clone();
1127        let node_id = self.refs.node_id.clone();
1128        let pan_resolver = self.cached_pan_resolver.clone();
1129        let handle_controller = self.handle_controller.clone();
1130        let local_to_window = self.refs.local_to_window.clone();
1131        let direct_manipulation = self.refs.direct_manipulation.clone();
1132        let press_track = self.refs.press_track;
1133        let gesture_claimed = self.refs.gesture_claimed.clone();
1134
1135        Some(Rc::new(move |scope| {
1136            let size = scope.size();
1137            if !*is_focused.borrow() {
1138                if let Some(controller) = &handle_controller {
1139                    controller.publish(TextFieldHandleMetrics {
1140                        focused: false,
1141                        direct_manipulation: false,
1142                        local_to_window: local_to_window.get(),
1143                        padding_left: 0.0,
1144                        padding_top: 0.0,
1145                        scroll_offset: 0.0,
1146                        line_height: cached_line_height.get(),
1147                        glyph_box: crate::text::glyph_line_box(&style, cached_line_height.get()),
1148                        wrap_width: measured_wrap_width.get(),
1149                    });
1150                }
1151                return;
1152            }
1153
1154            let mut primitives = Vec::new();
1155
1156            let text = state.text();
1157            let selection = state.selection();
1158            let line_height = cached_line_height.get();
1159
1160            let (viewport_width, viewport_height) = content_viewport(measured_size.get(), size);
1161            let pan = pan_resolver(viewport_width);
1162
1163            if let Some(controller) = &handle_controller {
1164                controller.adopt_gesture_claim(&gesture_claimed);
1165                controller.adopt_press_track(press_track);
1166                controller.publish(TextFieldHandleMetrics {
1167                    focused: true,
1168                    direct_manipulation: direct_manipulation.get(),
1169                    local_to_window: local_to_window.get(),
1170                    padding_left: content_origin.borrow().origin().x,
1171                    padding_top: content_origin.borrow().origin().y,
1172                    scroll_offset: pan,
1173                    line_height,
1174                    glyph_box: crate::text::glyph_line_box(&style, line_height),
1175                    wrap_width: measured_wrap_width.get(),
1176                });
1177            }
1178            let clip_bounds = cranpose_ui_graphics::Rect {
1179                x: 0.0,
1180                y: 0.0,
1181                width: viewport_width,
1182                height: viewport_height,
1183            };
1184
1185            if let Some(comp_range) = state.composition() {
1186                let comp_start = comp_range.min();
1187                let comp_end = comp_range.max();
1188
1189                if comp_start < comp_end && comp_end <= text.len() {
1190                    let underline_brush = cranpose_ui_graphics::Brush::solid(
1191                        cranpose_ui_graphics::Color(0.8, 0.8, 0.8, 0.8),
1192                    );
1193                    let underline_height: f32 = 2.0;
1194
1195                    for line_rect in range_visual_line_rects(
1196                        &text,
1197                        &style,
1198                        node_id.get(),
1199                        measured_wrap_width.get(),
1200                        pan,
1201                        line_height,
1202                        comp_start,
1203                        comp_end,
1204                    ) {
1205                        let underline_rect = cranpose_ui_graphics::Rect {
1206                            x: line_rect.x,
1207                            y: line_rect.y + line_height - underline_height,
1208                            width: line_rect.width,
1209                            height: underline_height,
1210                        };
1211                        if let Some(clipped) = intersect_rect(underline_rect, clip_bounds) {
1212                            primitives.push(DrawPrimitive::Rect {
1213                                rect: clipped,
1214                                brush: underline_brush.clone(),
1215                                stroke: None,
1216                            });
1217                        }
1218                    }
1219                }
1220            }
1221
1222            if selection.collapsed() && crate::cursor_animation::is_cursor_visible() {
1223                let pos = selection.start.min(text.len());
1224                let (line_index, line_start) = caret_visual_line_for_offset(
1225                    &text,
1226                    &style,
1227                    node_id.get(),
1228                    measured_wrap_width.get(),
1229                    pos,
1230                    crate::text_selection::LineAffinity::Upstream,
1231                );
1232                let cursor_x = crate::text::measure_text(
1233                    &crate::text::AnnotatedString::from(&text[line_start..pos]),
1234                    &style,
1235                )
1236                .width
1237                    - pan;
1238                let (box_off, box_h) = crate::text::glyph_line_box(&style, line_height);
1239                let cursor_y = line_index as f32 * line_height + box_off;
1240
1241                let cursor_rect = cranpose_ui_graphics::Rect {
1242                    x: cursor_x,
1243                    y: cursor_y,
1244                    width: CURSOR_WIDTH,
1245                    height: box_h,
1246                };
1247
1248                if let Some(clipped) = intersect_rect(cursor_rect, clip_bounds) {
1249                    primitives.push(DrawPrimitive::Rect {
1250                        rect: clipped,
1251                        brush: cursor_brush.clone(),
1252                        stroke: None,
1253                    });
1254                }
1255            }
1256
1257            scope.push_recorded(primitives);
1258        }))
1259    }
1260
1261    fn create_behind_draw_closure(
1262        &self,
1263    ) -> Option<Rc<dyn Fn(&mut cranpose_ui_graphics::DrawScopeDefault)>> {
1264        use cranpose_ui_graphics::{DrawPrimitive, DrawScope as _};
1265
1266        let is_focused = self.refs.is_focused.clone();
1267        let state = self.state;
1268        let selection_brush = self.selection_brush.clone();
1269        let style = self.style.clone();
1270        let cached_line_height = self.measured_line_height.clone();
1271        let measured_size = self.measured_size.clone();
1272        let measured_wrap_width = self.measured_wrap_width.clone();
1273        let node_id = self.refs.node_id.clone();
1274        let pan_resolver = self.cached_pan_resolver.clone();
1275
1276        Some(Rc::new(move |scope| {
1277            let size = scope.size();
1278            if !*is_focused.borrow() {
1279                return;
1280            }
1281            let selection = state.selection();
1282            if selection.collapsed() {
1283                return;
1284            }
1285            let text = state.text();
1286            let line_height = cached_line_height.get();
1287            let (viewport_width, viewport_height) = content_viewport(measured_size.get(), size);
1288            let pan = pan_resolver(viewport_width);
1289            let clip_bounds = cranpose_ui_graphics::Rect {
1290                x: 0.0,
1291                y: 0.0,
1292                width: viewport_width,
1293                height: viewport_height,
1294            };
1295
1296            let mut primitives = Vec::new();
1297            let (box_off, box_h) = crate::text::glyph_line_box(&style, line_height);
1298            for sel_rect in range_visual_line_rects(
1299                &text,
1300                &style,
1301                node_id.get(),
1302                measured_wrap_width.get(),
1303                pan,
1304                line_height,
1305                selection.min(),
1306                selection.max(),
1307            ) {
1308                let sel_rect = cranpose_ui_graphics::Rect {
1309                    y: sel_rect.y + box_off,
1310                    height: box_h,
1311                    ..sel_rect
1312                };
1313                if let Some(clipped) = intersect_rect(sel_rect, clip_bounds) {
1314                    primitives.push(DrawPrimitive::Rect {
1315                        rect: clipped,
1316                        brush: selection_brush.clone(),
1317                        stroke: None,
1318                    });
1319                }
1320            }
1321            scope.push_recorded(primitives);
1322        }))
1323    }
1324}
1325
1326impl SemanticsNode for TextFieldModifierNode {
1327    fn merge_semantics(&self, config: &mut SemanticsConfiguration) {
1328        if !self.decorated {
1329            merge_text_field_semantics(self.state, self.line_limits, config);
1330        }
1331    }
1332
1333    /// Neither modal nor hidden, but the field's text and selection are read
1334    /// live from its state, which an app may set without touching the node.
1335    fn reach(&self) -> cranpose_foundation::SemanticsReach {
1336        cranpose_foundation::SemanticsReach {
1337            merges_live_state: true,
1338            ..cranpose_foundation::SemanticsReach::default()
1339        }
1340    }
1341}
1342
1343/// What an editable field tells accessibility, from the node that takes its
1344/// input.
1345pub(crate) fn merge_text_field_semantics(
1346    state: TextFieldState,
1347    line_limits: TextFieldLineLimits,
1348    config: &mut SemanticsConfiguration,
1349) {
1350    let text = state.text();
1351    if config.content_description.is_none() {
1352        config.content_description = Some(text.clone());
1353    }
1354    config.text = Some(text);
1355    config.is_editable_text = true;
1356    config.is_clickable = true;
1357    config.multiline = !matches!(line_limits, TextFieldLineLimits::SingleLine);
1358    config.set_text = Some(cranpose_foundation::SemanticsSetText::new(move |text| {
1359        state.set_text(text)
1360    }));
1361    config.set_selection = Some(cranpose_foundation::SemanticsSetSelection::new(
1362        move |anchor, focus| {
1363            let text = state.text();
1364            let anchor = floor_char_boundary(&text, anchor);
1365            let focus = floor_char_boundary(&text, focus);
1366            state.set_selection(TextRange::new(anchor, focus));
1367            crate::cursor_animation::reset_cursor_blink();
1368            crate::request_render_invalidation();
1369            true
1370        },
1371    ));
1372    config.text_selection = Some(state.selection());
1373}
1374
1375fn floor_char_boundary(text: &str, index: usize) -> usize {
1376    let mut index = index.min(text.len());
1377    while !text.is_char_boundary(index) {
1378        index -= 1;
1379    }
1380    index
1381}
1382
1383impl PointerInputNode for TextFieldModifierNode {
1384    fn on_pointer_event(
1385        &mut self,
1386        _context: &mut dyn ModifierNodeContext,
1387        _event: &PointerEvent,
1388    ) -> bool {
1389        false
1390    }
1391
1392    fn hit_test(&self, x: f32, y: f32) -> bool {
1393        let size = self.measured_size.get();
1394        x >= 0.0 && x <= size.width && y >= 0.0 && y <= size.height
1395    }
1396
1397    fn pointer_input_handler(&self) -> Option<Rc<dyn Fn(PointerEvent)>> {
1398        Some(self.cached_handler.clone())
1399    }
1400}
1401
1402/// Element that creates and updates `TextFieldModifierNode` instances.
1403///
1404/// This follows the modifier element pattern where the element is responsible for:
1405/// - Creating new nodes (via `create`)
1406/// - Updating existing nodes when properties change (via `update`)
1407/// - Declaring capabilities (LAYOUT | DRAW | SEMANTICS)
1408#[derive(Clone)]
1409pub struct TextFieldElement {
1410    state: TextFieldState,
1411    style: TextStyle,
1412    cursor_color: Color,
1413    line_limits: TextFieldLineLimits,
1414    show_keyboard_on_focus: bool,
1415    handle_controller: Option<TextFieldHandleController>,
1416    modal_depth: usize,
1417    decorator: Option<TextFieldRefs>,
1418}
1419
1420impl TextFieldElement {
1421    /// Creates a new text field element.
1422    pub fn new(state: TextFieldState, style: TextStyle) -> Self {
1423        Self {
1424            state,
1425            style,
1426            cursor_color: DEFAULT_CURSOR_COLOR,
1427            line_limits: TextFieldLineLimits::default(),
1428            show_keyboard_on_focus: true,
1429            handle_controller: None,
1430            modal_depth: 0,
1431            decorator: None,
1432        }
1433    }
1434
1435    /// Hands the field's input, focus and semantics to the decoration box
1436    /// that shares `refs`.
1437    pub(crate) fn decorated_by(mut self, refs: TextFieldRefs) -> Self {
1438        self.decorator = Some(refs);
1439        self
1440    }
1441
1442    /// Creates an element with custom cursor color.
1443    pub fn with_cursor_color(mut self, color: Color) -> Self {
1444        self.cursor_color = color;
1445        self
1446    }
1447
1448    /// Creates an element with custom line limits.
1449    pub fn with_line_limits(mut self, line_limits: TextFieldLineLimits) -> Self {
1450        self.line_limits = line_limits;
1451        self
1452    }
1453
1454    /// Sets whether focus opens the software keyboard. A pointer tap always
1455    /// requests it, including when the field is already focused.
1456    pub fn with_show_keyboard_on_focus(mut self, show: bool) -> Self {
1457        self.show_keyboard_on_focus = show;
1458        self
1459    }
1460
1461    /// Installs the finger-handle metrics channel shared with the composable.
1462    pub fn with_handle_controller(mut self, controller: TextFieldHandleController) -> Self {
1463        self.handle_controller = Some(controller);
1464        self
1465    }
1466
1467    /// Sets the modal depth this field was composed at (see
1468    /// [`crate::modal::local_modal_depth`]).
1469    pub fn with_modal_depth(mut self, depth: usize) -> Self {
1470        self.modal_depth = depth;
1471        self
1472    }
1473}
1474
1475impl std::fmt::Debug for TextFieldElement {
1476    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1477        f.debug_struct("TextFieldElement")
1478            .field("text", &self.state.text())
1479            .field("style", &self.style)
1480            .field("cursor_color", &self.cursor_color)
1481            .finish()
1482    }
1483}
1484
1485impl Hash for TextFieldElement {
1486    fn hash<H: Hasher>(&self, state: &mut H) {
1487        self.state.id().hash(state);
1488        self.cursor_color.0.to_bits().hash(state);
1489        self.cursor_color.1.to_bits().hash(state);
1490        self.cursor_color.2.to_bits().hash(state);
1491        self.cursor_color.3.to_bits().hash(state);
1492        self.style.render_hash().hash(state);
1493        self.line_limits.hash(state);
1494        self.show_keyboard_on_focus.hash(state);
1495        self.modal_depth.hash(state);
1496        self.decorator.as_ref().map(TextFieldRefs::key).hash(state);
1497    }
1498}
1499
1500impl PartialEq for TextFieldElement {
1501    fn eq(&self, other: &Self) -> bool {
1502        self.state == other.state
1503            && self.style == other.style
1504            && self.cursor_color == other.cursor_color
1505            && self.line_limits == other.line_limits
1506            && self.show_keyboard_on_focus == other.show_keyboard_on_focus
1507            && self.modal_depth == other.modal_depth
1508            && self.decorator == other.decorator
1509    }
1510}
1511
1512impl Eq for TextFieldElement {}
1513
1514impl ModifierNodeElement for TextFieldElement {
1515    type Node = TextFieldModifierNode;
1516
1517    fn create(&self) -> Self::Node {
1518        let refs = self.decorator.clone().unwrap_or_else(TextFieldRefs::new);
1519        let decorated = self.decorator.is_some();
1520        let mut node =
1521            TextFieldModifierNode::with_refs(self.state, self.style.clone(), refs, decorated)
1522                .with_cursor_color(self.cursor_color)
1523                .with_line_limits(self.line_limits);
1524        node.modal_depth = self.modal_depth;
1525        node.refs.focus_options.set(TextFieldFocusOptions {
1526            modal_depth: self.modal_depth,
1527            show_keyboard_on_focus: self.show_keyboard_on_focus,
1528        });
1529        if let Some(controller) = self.handle_controller.clone() {
1530            node = node.with_handle_controller(controller);
1531        }
1532        node.rebuild_cached_closures();
1533        node
1534    }
1535
1536    fn update(&self, node: &mut Self::Node) {
1537        node.state = self.state;
1538        if node.style != self.style {
1539            node.min_size.set(None);
1540            node.style = self.style.clone();
1541        }
1542        node.cursor_brush = Brush::solid(self.cursor_color);
1543        node.line_limits = self.line_limits;
1544        node.handle_controller.clone_from(&self.handle_controller);
1545        node.modal_depth = self.modal_depth;
1546        let previous_focus_options = node.refs.focus_options.replace(TextFieldFocusOptions {
1547            modal_depth: self.modal_depth,
1548            show_keyboard_on_focus: self.show_keyboard_on_focus,
1549        });
1550        if self.show_keyboard_on_focus
1551            && !previous_focus_options.show_keyboard_on_focus
1552            && *node.refs.is_focused.borrow()
1553        {
1554            crate::text_input_session::notify_text_input_focus_gained();
1555        }
1556        node.rebuild_cached_closures();
1557
1558        if node.update_cached_state() {}
1559    }
1560
1561    /// A decorated field's node shares its decoration's refs, so it is
1562    /// never handed to a field decorated by another.
1563    fn key(&self) -> Option<u64> {
1564        self.decorator.as_ref().map(TextFieldRefs::key)
1565    }
1566
1567    fn capabilities(&self) -> NodeCapabilities {
1568        let drawn = NodeCapabilities::LAYOUT | NodeCapabilities::DRAW | NodeCapabilities::SEMANTICS;
1569        if self.decorator.is_some() {
1570            drawn
1571        } else {
1572            drawn | NodeCapabilities::POINTER_INPUT
1573        }
1574    }
1575
1576    fn always_update(&self) -> bool {
1577        true
1578    }
1579}
1580
1581#[cfg(test)]
1582#[path = "tests/text_field_modifier_node_tests.rs"]
1583mod tests;