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