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