Skip to main content

cranpose_ui/
text_field_modifier_node.rs

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