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