Skip to main content

cranpose_ui/
text_field_modifier_node.rs

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