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
204pub(crate) fn caret_visual_line_for_offset(
205    text: &str,
206    style: &TextStyle,
207    node_id: Option<cranpose_core::NodeId>,
208    wrap_width: Option<f32>,
209    offset: usize,
210    affinity: crate::text_selection::LineAffinity,
211) -> (usize, usize) {
212    let offset = offset.min(text.len());
213    match wrap_width {
214        Some(width) if width.is_finite() && width > 0.0 => {
215            let annotated = crate::text::AnnotatedString::from(text);
216            let ranges = crate::text::wrapped_line_ranges(
217                node_id,
218                &annotated,
219                style,
220                crate::text::TextLayoutOptions::default(),
221                Some(width),
222            );
223            crate::text_selection::caret_visual_line(&ranges, offset, affinity)
224        }
225        _ => {
226            let before = &text[..offset];
227            let line_index = before.matches('\n').count();
228            let line_start = before.rfind('\n').map(|i| i + 1).unwrap_or(0);
229            (line_index, line_start)
230        }
231    }
232}
233
234#[allow(clippy::too_many_arguments)]
235pub(crate) fn range_visual_line_rects(
236    text: &str,
237    style: &TextStyle,
238    node_id: Option<cranpose_core::NodeId>,
239    wrap_width: Option<f32>,
240    padding_left: f32,
241    padding_top: f32,
242    pan: f32,
243    line_height: f32,
244    start: usize,
245    end: usize,
246) -> Vec<cranpose_ui_graphics::Rect> {
247    if start >= end {
248        return Vec::new();
249    }
250    let annotated = crate::text::AnnotatedString::from(text);
251    let line_ranges = crate::text::wrapped_line_ranges(
252        node_id,
253        &annotated,
254        style,
255        crate::text::TextLayoutOptions::default(),
256        wrap_width,
257    );
258    let mut rects = Vec::new();
259    for (line_idx, line_range) in line_ranges.iter().enumerate() {
260        let line_start = line_range.start;
261        let line_end = line_range.end;
262        if end <= line_start || start >= line_end {
263            continue;
264        }
265        let seg_start = start.max(line_start);
266        let seg_end = end.min(line_end);
267        let x0 = crate::text::measure_text(
268            &crate::text::AnnotatedString::from(&text[line_start..seg_start]),
269            style,
270        )
271        .width
272            + padding_left
273            - pan;
274        let x1 = crate::text::measure_text(
275            &crate::text::AnnotatedString::from(&text[line_start..seg_end]),
276            style,
277        )
278        .width
279            + padding_left
280            - pan;
281        let width = x1 - x0;
282        if width > 0.0 {
283            rects.push(cranpose_ui_graphics::Rect {
284                x: x0,
285                y: padding_top + line_idx as f32 * line_height,
286                width,
287                height: line_height,
288            });
289        }
290    }
291    rects
292}
293
294fn build_focus_handler(
295    state: TextFieldState,
296    refs: &TextFieldRefs,
297    line_limits: TextFieldLineLimits,
298    style: &TextStyle,
299) -> Rc<dyn crate::text_field_focus::FocusedTextFieldHandler> {
300    crate::text_field_handler::TextFieldHandler::new(
301        state,
302        refs.node_id.get(),
303        line_limits,
304        crate::text_field_handler::CaretGeometryRefs {
305            node_origin: refs.node_origin.clone(),
306            content_offset: refs.content_offset.clone(),
307            content_y_offset: refs.content_y_offset.clone(),
308            scroll_offset: refs.scroll_offset.clone(),
309            style: style.clone(),
310        },
311    )
312}
313
314fn request_pointer_focus(
315    state: TextFieldState,
316    refs: &TextFieldRefs,
317    line_limits: TextFieldLineLimits,
318    style: &TextStyle,
319    modal_depth: usize,
320) {
321    if modal_depth < crate::modal::current_modal_depth()
322        || refs
323            .node_id
324            .get()
325            .is_some_and(crate::focus_dispatch::request_focus_in_context)
326    {
327        return;
328    }
329    crate::text_field_focus::request_focus(
330        refs.is_focused.clone(),
331        build_focus_handler(state, refs, line_limits, style),
332        modal_depth,
333    );
334}
335
336struct TextFieldFocusBridge {
337    state: TextFieldState,
338    refs: TextFieldRefs,
339    style: TextStyle,
340    line_limits: TextFieldLineLimits,
341}
342
343impl crate::focus_dispatch::FocusTargetHandle for TextFieldFocusBridge {
344    fn set_focus_state(&self, state: FocusState) {
345        if state.is_focused() {
346            crate::text_field_focus::request_focus(
347                self.refs.is_focused.clone(),
348                build_focus_handler(self.state, &self.refs, self.line_limits, &self.style),
349                self.refs.modal_depth.get(),
350            );
351        } else if crate::text_field_focus::focused_field_node() == self.refs.node_id.get() {
352            crate::text_field_focus::clear_focus();
353        }
354    }
355}
356
357#[derive(Clone)]
358pub(crate) struct TextFieldRefs {
359    pub is_focused: Rc<RefCell<bool>>,
360    pub content_offset: Rc<Cell<f32>>,
361    pub content_y_offset: Rc<Cell<f32>>,
362    pub drag_anchor: Rc<Cell<Option<usize>>>,
363    pub last_click_time: Rc<Cell<Option<web_time::Instant>>>,
364    pub last_click_pos: Rc<Cell<Option<(f32, f32)>>>,
365    pub click_count: Rc<Cell<u8>>,
366    pub node_id: Rc<Cell<Option<cranpose_core::NodeId>>>,
367    pub scroll_offset: Rc<Cell<f32>>,
368    pub direct_manipulation: Rc<Cell<bool>>,
369    pub node_origin: Rc<Cell<Point>>,
370    pub line_height: Rc<Cell<f32>>,
371    pub wrap_width: Rc<Cell<Option<f32>>>,
372    pub press_track: MutableState<Option<PointerPressTrack>>,
373    pub gesture_claimed: Rc<Cell<bool>>,
374    pub modal_depth: Rc<Cell<usize>>,
375}
376
377#[derive(Clone, Copy, Debug, PartialEq)]
378pub struct PointerPressTrack {
379    pub start: Point,
380    pub position: Point,
381}
382
383impl TextFieldRefs {
384    pub fn new() -> Self {
385        Self {
386            is_focused: Rc::new(RefCell::new(false)),
387            content_offset: Rc::new(Cell::new(0.0_f32)),
388            content_y_offset: Rc::new(Cell::new(0.0_f32)),
389            drag_anchor: Rc::new(Cell::new(None::<usize>)),
390            last_click_time: Rc::new(Cell::new(None::<web_time::Instant>)),
391            last_click_pos: Rc::new(Cell::new(None::<(f32, f32)>)),
392            click_count: Rc::new(Cell::new(0_u8)),
393            node_id: Rc::new(Cell::new(None::<cranpose_core::NodeId>)),
394            scroll_offset: Rc::new(Cell::new(0.0_f32)),
395            direct_manipulation: Rc::new(Cell::new(false)),
396            node_origin: Rc::new(Cell::new(Point { x: 0.0, y: 0.0 })),
397            line_height: Rc::new(Cell::new(DEFAULT_LINE_HEIGHT)),
398            wrap_width: Rc::new(Cell::new(None::<f32>)),
399            press_track: mutableStateOf(None::<PointerPressTrack>),
400            gesture_claimed: Rc::new(Cell::new(false)),
401            modal_depth: Rc::new(Cell::new(0)),
402        }
403    }
404}
405
406use crate::text::TextStyle;
407
408pub struct TextFieldModifierNode {
409    state: TextFieldState,
410    refs: TextFieldRefs,
411    style: TextStyle,
412    cursor_brush: Brush,
413    selection_brush: Brush,
414    line_limits: TextFieldLineLimits,
415    cached_text: String,
416    cached_selection: TextRange,
417    node_state: NodeState,
418    measured_size: Rc<Cell<Size>>,
419    measured_line_height: Rc<Cell<f32>>,
420    measured_wrap_width: Rc<Cell<Option<f32>>>,
421    cached_handler: Rc<dyn Fn(PointerEvent)>,
422    cached_pan_resolver: TextPanResolver,
423    handle_controller: Option<TextFieldHandleController>,
424    modal_depth: usize,
425    focus_bridge: Option<Rc<dyn crate::focus_dispatch::FocusTargetHandle>>,
426}
427
428impl std::fmt::Debug for TextFieldModifierNode {
429    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
430        f.debug_struct("TextFieldModifierNode")
431            .field("text", &self.state.text())
432            .field("style", &self.style)
433            .field("is_focused", &*self.refs.is_focused.borrow())
434            .finish()
435    }
436}
437
438impl TextFieldModifierNode {
439    /// Creates a new text field modifier node.
440    pub fn new(state: TextFieldState, style: TextStyle) -> Self {
441        let value = state.value();
442        let refs = TextFieldRefs::new();
443        let refs_line_height = refs.line_height.clone();
444        let refs_wrap_width = refs.wrap_width.clone();
445        let line_limits = TextFieldLineLimits::default();
446        let cached_handler =
447            Self::create_handler(state, refs.clone(), line_limits, style.clone(), 0);
448        let cached_pan_resolver =
449            Self::create_pan_resolver(state, refs.clone(), line_limits, style.clone());
450
451        Self {
452            state,
453            refs,
454            style,
455            cursor_brush: Brush::solid(DEFAULT_CURSOR_COLOR),
456            selection_brush: Brush::solid(DEFAULT_SELECTION_COLOR),
457            line_limits,
458            cached_text: value.text,
459            cached_selection: value.selection,
460            node_state: NodeState::new(),
461            measured_size: Rc::new(Cell::new(Size {
462                width: 0.0,
463                height: 0.0,
464            })),
465            measured_line_height: refs_line_height,
466            measured_wrap_width: refs_wrap_width,
467            cached_handler,
468            cached_pan_resolver,
469            handle_controller: None,
470            modal_depth: 0,
471            focus_bridge: None,
472        }
473    }
474
475    /// Creates a node with custom line limits.
476    pub fn with_line_limits(mut self, line_limits: TextFieldLineLimits) -> Self {
477        self.line_limits = line_limits;
478        self.rebuild_cached_closures();
479        self
480    }
481
482    fn rebuild_cached_closures(&mut self) {
483        self.cached_handler = Self::create_handler(
484            self.state,
485            self.refs.clone(),
486            self.line_limits,
487            self.style.clone(),
488            self.modal_depth,
489        );
490        self.cached_pan_resolver = Self::create_pan_resolver(
491            self.state,
492            self.refs.clone(),
493            self.line_limits,
494            self.style.clone(),
495        );
496    }
497
498    /// Installs the controller the field publishes live handle metrics to.
499    pub fn with_handle_controller(mut self, controller: TextFieldHandleController) -> Self {
500        self.handle_controller = Some(controller);
501        self
502    }
503
504    fn create_pan_resolver(
505        state: TextFieldState,
506        refs: TextFieldRefs,
507        line_limits: TextFieldLineLimits,
508        style: TextStyle,
509    ) -> TextPanResolver {
510        Rc::new(move |viewport_width: f32| {
511            if !line_limits.is_single_line() {
512                refs.scroll_offset.set(0.0);
513                return 0.0;
514            }
515            let text = state.text();
516            let pos = state.selection().start.min(text.len());
517            let text_width = crate::text::measure_text(
518                &crate::text::AnnotatedString::from(text.as_str()),
519                &style,
520            )
521            .width;
522            let cursor_x = crate::text::measure_text(
523                &crate::text::AnnotatedString::from(&text[..pos]),
524                &style,
525            )
526            .width;
527            let offset = compute_horizontal_scroll_offset(
528                refs.scroll_offset.get(),
529                cursor_x,
530                text_width,
531                viewport_width,
532            );
533            refs.scroll_offset.set(offset);
534            offset
535        })
536    }
537
538    /// Returns the pan resolver for single-line fields, `None` for multi-line.
539    ///
540    /// Exposed to the modifier slices so the render scene builder can pan the
541    /// text glyphs by the same offset used for the cursor and selection.
542    pub fn text_pan_resolver(&self) -> Option<TextPanResolver> {
543        self.line_limits
544            .is_single_line()
545            .then(|| self.cached_pan_resolver.clone())
546    }
547
548    /// Returns the current horizontal scroll (pan) offset in px.
549    pub fn scroll_offset(&self) -> f32 {
550        self.refs.scroll_offset.get()
551    }
552
553    /// Returns the current line limits configuration.
554    pub fn line_limits(&self) -> TextFieldLineLimits {
555        self.line_limits
556    }
557
558    fn create_handler(
559        state: TextFieldState,
560        refs: TextFieldRefs,
561        line_limits: TextFieldLineLimits,
562        style: TextStyle,
563        modal_depth: usize,
564    ) -> Rc<dyn Fn(PointerEvent)> {
565        use crate::{
566            text_selection::{
567                MULTI_TAP_SLOP_PX, MULTI_TAP_TIMEOUT_MS, SelectionGranularity, classify_tap_count,
568                find_line_boundaries, find_paragraph_boundaries, resolve_selection_tap_count,
569                tap_selection_granularity,
570            },
571            word_boundaries::find_word_boundaries,
572        };
573
574        Rc::new(move |event: PointerEvent| {
575            refs.node_origin.set(Point {
576                x: event.global_position.x - event.position.x,
577                y: event.global_position.y - event.position.y,
578            });
579
580            let click_x =
581                (event.position.x - refs.content_offset.get() + refs.scroll_offset.get()).max(0.0);
582            let click_y = (event.position.y - refs.content_y_offset.get()).max(0.0);
583
584            match event.kind {
585                PointerEventKind::Down => {
586                    refs.direct_manipulation.set(true);
587                    refs.press_track.set(Some(PointerPressTrack {
588                        start: event.global_position,
589                        position: event.global_position,
590                    }));
591                    refs.gesture_claimed.set(false);
592
593                    request_pointer_focus(state, &refs, line_limits, &style, modal_depth);
594
595                    let now = web_time::Instant::now();
596                    let text = state.text();
597                    let pos = crate::text::offset_for_position_wrapped(
598                        &text,
599                        &style,
600                        refs.node_id.get(),
601                        refs.wrap_width.get(),
602                        refs.line_height.get(),
603                        click_x,
604                        click_y,
605                    );
606
607                    let previous = refs.last_click_pos.get().and_then(|(px, py)| {
608                        let count = refs.click_count.get();
609                        (count > 0).then_some((count, px, py))
610                    });
611                    let elapsed_ms = refs
612                        .last_click_time
613                        .get()
614                        .map(|last| now.duration_since(last).as_millis())
615                        .unwrap_or(u128::MAX);
616                    let tap_count = classify_tap_count(
617                        previous,
618                        elapsed_ms,
619                        event.position.x,
620                        event.position.y,
621                        MULTI_TAP_TIMEOUT_MS,
622                        MULTI_TAP_SLOP_PX,
623                    );
624
625                    let selection = state.selection();
626                    let tap_in_selection =
627                        !selection.collapsed() && pos >= selection.min() && pos <= selection.max();
628                    let repeat_in_place = refs
629                        .last_click_pos
630                        .get()
631                        .map(|(px, py)| {
632                            let dx = event.position.x - px;
633                            let dy = event.position.y - py;
634                            dx * dx + dy * dy <= MULTI_TAP_SLOP_PX * MULTI_TAP_SLOP_PX
635                        })
636                        .unwrap_or(false);
637                    let effective_count = resolve_selection_tap_count(
638                        tap_count,
639                        refs.click_count.get(),
640                        tap_in_selection,
641                        repeat_in_place,
642                    );
643
644                    match tap_selection_granularity(effective_count) {
645                        SelectionGranularity::Paragraph => {
646                            let (start, end) = find_paragraph_boundaries(&text, pos);
647                            state.edit(|buffer| {
648                                buffer.select(TextRange::new(start, end));
649                            });
650                            refs.drag_anchor.set(Some(start));
651                        }
652                        SelectionGranularity::Line => {
653                            let (line_start, line_end) = find_line_boundaries(&text, pos);
654                            state.edit(|buffer| {
655                                buffer.select(TextRange::new(line_start, line_end));
656                            });
657                            refs.drag_anchor.set(Some(line_start));
658                        }
659                        SelectionGranularity::Word => {
660                            let (word_start, word_end) = find_word_boundaries(&text, pos);
661                            state.edit(|buffer| {
662                                buffer.select(TextRange::new(word_start, word_end));
663                            });
664                            refs.drag_anchor.set(Some(word_start));
665                        }
666                        SelectionGranularity::Caret => {
667                            refs.drag_anchor.set(Some(pos));
668                            state.edit(|buffer| {
669                                buffer.place_cursor_before_char(pos);
670                            });
671                        }
672                    }
673
674                    refs.click_count.set(effective_count);
675                    refs.last_click_time.set(Some(now));
676                    refs.last_click_pos
677                        .set(Some((event.position.x, event.position.y)));
678                    event.consume();
679                }
680                PointerEventKind::Move => {
681                    if let Some(mut track) = refs.press_track.get() {
682                        track.position = event.global_position;
683                        refs.press_track.set(Some(track));
684                        if let Some(node_id) = refs.node_id.get() {
685                            crate::schedule_draw_repass(node_id);
686                        }
687                        crate::request_render_invalidation();
688                    }
689                    if refs.gesture_claimed.get() {
690                        event.consume();
691                        return;
692                    }
693                    if let Some(anchor) = refs.drag_anchor.get()
694                        && *refs.is_focused.borrow()
695                    {
696                        let text = state.text();
697                        let current_pos = crate::text::offset_for_position_wrapped(
698                            &text,
699                            &style,
700                            refs.node_id.get(),
701                            refs.wrap_width.get(),
702                            refs.line_height.get(),
703                            click_x,
704                            click_y,
705                        );
706
707                        state.set_selection(TextRange::new(anchor, current_pos));
708
709                        crate::request_render_invalidation();
710
711                        event.consume();
712                    }
713                }
714                PointerEventKind::Up => {
715                    refs.drag_anchor.set(None);
716                    refs.press_track.set(None);
717                    refs.gesture_claimed.set(false);
718                    if let Some(node_id) = refs.node_id.get() {
719                        crate::schedule_draw_repass(node_id);
720                    }
721                    crate::request_render_invalidation();
722                }
723                PointerEventKind::Cancel => {
724                    refs.press_track.set(None);
725                    refs.gesture_claimed.set(false);
726                    if let Some(node_id) = refs.node_id.get() {
727                        crate::schedule_draw_repass(node_id);
728                    }
729                    crate::request_render_invalidation();
730                }
731                _ => {}
732            }
733        })
734    }
735
736    /// Creates a node with a custom accent: the caret is drawn solid in
737    /// `color` and the selection highlight is derived from it at
738    /// [`crate::widgets::SELECTION_HIGHLIGHT_ALPHA`] — the reference field
739    /// tints caret, handles and highlight from the one accent.
740    pub fn with_cursor_color(mut self, color: Color) -> Self {
741        self.cursor_brush = Brush::solid(color);
742        self.selection_brush = Brush::solid(
743            color.with_alpha(crate::widgets::basic_text_field::SELECTION_HIGHLIGHT_ALPHA),
744        );
745        self
746    }
747
748    /// Sets the focus state.
749    pub fn set_focused(&mut self, focused: bool) {
750        let current = *self.refs.is_focused.borrow();
751        if current != focused {
752            *self.refs.is_focused.borrow_mut() = focused;
753            if !focused {
754                self.refs.direct_manipulation.set(false);
755                self.refs.press_track.set(None);
756                self.refs.gesture_claimed.set(false);
757            }
758        }
759    }
760
761    /// Returns whether the field is focused.
762    pub fn is_focused(&self) -> bool {
763        *self.refs.is_focused.borrow()
764    }
765
766    pub(crate) fn window_origin_sink(&self) -> Rc<Cell<Point>> {
767        self.refs.node_origin.clone()
768    }
769
770    /// Returns the current text.
771    pub fn text(&self) -> String {
772        self.state.text()
773    }
774
775    pub fn style(&self) -> &TextStyle {
776        &self.style
777    }
778
779    /// Returns the current selection.
780    pub fn selection(&self) -> TextRange {
781        self.state.selection()
782    }
783
784    /// Returns the cursor brush for rendering.
785    pub fn cursor_brush(&self) -> Brush {
786        self.cursor_brush.clone()
787    }
788
789    /// Returns the selection brush for rendering selection highlight.
790    pub fn selection_brush(&self) -> Brush {
791        self.selection_brush.clone()
792    }
793
794    /// Inserts text at the current cursor position (for paste operations).
795    pub fn insert_text(&mut self, text: &str) {
796        self.state.edit(|buffer| {
797            buffer.insert(text);
798        });
799    }
800
801    /// Copies the selected text and returns it (for web copy operation).
802    /// Returns None if no selection.
803    pub fn copy_selection(&self) -> Option<String> {
804        self.state.copy_selection()
805    }
806
807    /// Cuts the selected text: copies and deletes it.
808    /// Returns the cut text, or None if no selection.
809    pub fn cut_selection(&mut self) -> Option<String> {
810        let text = self.copy_selection();
811        if text.is_some() {
812            self.state.edit(|buffer| {
813                buffer.delete(buffer.selection());
814            });
815        }
816        text
817    }
818
819    /// Updates the content offset (padding.left) for accurate click-to-position cursor placement.
820    /// Called from slices collection where padding is known.
821    pub fn set_content_offset(&self, offset: f32) {
822        self.refs.content_offset.set(offset);
823    }
824
825    /// Updates the content Y offset (padding.top) for cursor Y positioning.
826    /// Called from slices collection where padding is known.
827    pub fn set_content_y_offset(&self, offset: f32) {
828        self.refs.content_y_offset.set(offset);
829    }
830
831    fn wrap_width(&self, available_width: f32) -> Option<f32> {
832        (!self.line_limits.is_single_line() && available_width.is_finite() && available_width > 0.0)
833            .then_some(available_width)
834    }
835
836    fn measure_text_content(&self, wrap_width: Option<f32>) -> Size {
837        let text = self.state.text();
838        let node_id = self.refs.node_id.get();
839        let annotated = crate::text::AnnotatedString::from(text.as_str());
840        let metrics = match wrap_width {
841            Some(max_width) => crate::text::measure_text_with_options_for_node(
842                node_id,
843                &annotated,
844                &self.style,
845                crate::text::TextLayoutOptions::default(),
846                Some(max_width),
847            ),
848            None => crate::text::measure_text_for_node(node_id, &annotated, &self.style),
849        };
850        self.measured_line_height.set(metrics.line_height);
851        Size {
852            width: metrics.width,
853            height: metrics.height,
854        }
855    }
856
857    fn update_cached_state(&mut self) -> bool {
858        let value = self.state.value();
859        let text_changed = value.text != self.cached_text;
860        let selection_changed = value.selection != self.cached_selection;
861
862        if text_changed {
863            self.cached_text = value.text;
864        }
865        if selection_changed {
866            self.cached_selection = value.selection;
867        }
868
869        text_changed || selection_changed
870    }
871}
872
873impl DelegatableNode for TextFieldModifierNode {
874    fn node_state(&self) -> &NodeState {
875        &self.node_state
876    }
877}
878
879impl ModifierNode for TextFieldModifierNode {
880    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
881        self.refs.node_id.set(context.node_id());
882
883        context.invalidate(InvalidationKind::Layout);
884        context.invalidate(InvalidationKind::Draw);
885        context.invalidate(InvalidationKind::Semantics);
886
887        if let Some(node_id) = context.node_id() {
888            let bridge: Rc<dyn crate::focus_dispatch::FocusTargetHandle> =
889                Rc::new(TextFieldFocusBridge {
890                    state: self.state,
891                    refs: self.refs.clone(),
892                    style: self.style.clone(),
893                    line_limits: self.line_limits,
894                });
895            self.focus_bridge = Some(Rc::clone(&bridge));
896            crate::focus_dispatch::register_focus_target(node_id, bridge);
897        }
898    }
899
900    fn on_detach(&mut self) {
901        if let (Some(node_id), Some(bridge)) = (self.refs.node_id.get(), self.focus_bridge.take()) {
902            crate::focus_dispatch::unregister_focus_target(node_id, &bridge);
903        }
904    }
905
906    fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
907        Some(self)
908    }
909
910    fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
911        Some(self)
912    }
913
914    fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
915        Some(self)
916    }
917
918    fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
919        Some(self)
920    }
921
922    fn as_semantics_node(&self) -> Option<&dyn SemanticsNode> {
923        Some(self)
924    }
925
926    fn as_semantics_node_mut(&mut self) -> Option<&mut dyn SemanticsNode> {
927        Some(self)
928    }
929
930    fn as_pointer_input_node(&self) -> Option<&dyn PointerInputNode> {
931        Some(self)
932    }
933
934    fn as_pointer_input_node_mut(&mut self) -> Option<&mut dyn PointerInputNode> {
935        Some(self)
936    }
937}
938
939impl LayoutModifierNode for TextFieldModifierNode {
940    fn measure(
941        &self,
942        _context: &mut dyn ModifierNodeContext,
943        _measurable: &dyn Measurable,
944        constraints: Constraints,
945    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
946        let wrap_width = self.wrap_width(constraints.max_width);
947        self.measured_wrap_width.set(wrap_width);
948        let text_size = self.measure_text_content(wrap_width);
949
950        let min_height = if text_size.height < 1.0 {
951            DEFAULT_LINE_HEIGHT
952        } else {
953            text_size.height
954        };
955
956        let width = text_size
957            .width
958            .max(constraints.min_width)
959            .min(constraints.max_width);
960        let height = min_height
961            .max(constraints.min_height)
962            .min(constraints.max_height);
963
964        let size = Size { width, height };
965        self.measured_size.set(size);
966
967        let _ = (self.cached_pan_resolver)(size.width);
968
969        cranpose_ui_layout::LayoutModifierMeasureResult::with_size(size)
970    }
971
972    fn min_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
973        self.measure_text_content(None).width
974    }
975
976    fn max_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
977        self.measure_text_content(None).width
978    }
979
980    fn min_intrinsic_height(&self, _measurable: &dyn Measurable, width: f32) -> f32 {
981        self.measure_text_content(self.wrap_width(width))
982            .height
983            .max(DEFAULT_LINE_HEIGHT)
984    }
985
986    fn max_intrinsic_height(&self, _measurable: &dyn Measurable, width: f32) -> f32 {
987        self.measure_text_content(self.wrap_width(width))
988            .height
989            .max(DEFAULT_LINE_HEIGHT)
990    }
991}
992
993fn content_viewport(
994    measured: cranpose_ui_graphics::Size,
995    size: cranpose_foundation::Size,
996    padding_left: f32,
997    padding_top: f32,
998) -> (f32, f32) {
999    let width = if measured.width > 0.0 {
1000        measured.width
1001    } else {
1002        (size.width - padding_left).max(0.0)
1003    };
1004    let height = if measured.height > 0.0 {
1005        measured.height
1006    } else {
1007        (size.height - padding_top).max(0.0)
1008    };
1009    (width, height)
1010}
1011
1012impl DrawModifierNode for TextFieldModifierNode {
1013    fn draw(&self, _draw_scope: &mut dyn DrawScope) {}
1014
1015    fn create_draw_closure(
1016        &self,
1017    ) -> Option<Rc<dyn Fn(&mut cranpose_ui_graphics::DrawScopeDefault)>> {
1018        use cranpose_ui_graphics::{DrawPrimitive, DrawScope as _};
1019
1020        let is_focused = self.refs.is_focused.clone();
1021        let state = self.state;
1022        let content_offset = self.refs.content_offset.clone();
1023        let content_y_offset = self.refs.content_y_offset.clone();
1024        let cursor_brush = self.cursor_brush.clone();
1025        let style = self.style.clone();
1026        let cached_line_height = self.measured_line_height.clone();
1027        let measured_size = self.measured_size.clone();
1028        let measured_wrap_width = self.measured_wrap_width.clone();
1029        let node_id = self.refs.node_id.clone();
1030        let pan_resolver = self.cached_pan_resolver.clone();
1031        let handle_controller = self.handle_controller.clone();
1032        let node_origin = self.refs.node_origin.clone();
1033        let direct_manipulation = self.refs.direct_manipulation.clone();
1034        let press_track = self.refs.press_track;
1035        let gesture_claimed = self.refs.gesture_claimed.clone();
1036
1037        Some(Rc::new(move |scope| {
1038            let size = scope.size();
1039            if !*is_focused.borrow() {
1040                if let Some(controller) = &handle_controller {
1041                    controller.publish(TextFieldHandleMetrics {
1042                        focused: false,
1043                        direct_manipulation: false,
1044                        node_origin: node_origin.get(),
1045                        padding_left: 0.0,
1046                        padding_top: 0.0,
1047                        scroll_offset: 0.0,
1048                        line_height: cached_line_height.get(),
1049                        glyph_box: crate::text::glyph_line_box(&style, cached_line_height.get()),
1050                        wrap_width: measured_wrap_width.get(),
1051                    });
1052                }
1053                return;
1054            }
1055
1056            let mut primitives = Vec::new();
1057
1058            let text = state.text();
1059            let selection = state.selection();
1060            let padding_left = content_offset.get();
1061            let padding_top = content_y_offset.get();
1062            let line_height = cached_line_height.get();
1063
1064            let (viewport_width, viewport_height) =
1065                content_viewport(measured_size.get(), size, padding_left, padding_top);
1066            let pan = pan_resolver(viewport_width);
1067
1068            if let Some(controller) = &handle_controller {
1069                controller.adopt_gesture_claim(&gesture_claimed);
1070                controller.adopt_press_track(press_track);
1071                controller.publish(TextFieldHandleMetrics {
1072                    focused: true,
1073                    direct_manipulation: direct_manipulation.get(),
1074                    node_origin: node_origin.get(),
1075                    padding_left,
1076                    padding_top,
1077                    scroll_offset: pan,
1078                    line_height,
1079                    glyph_box: crate::text::glyph_line_box(&style, line_height),
1080                    wrap_width: measured_wrap_width.get(),
1081                });
1082            }
1083            let clip_bounds = cranpose_ui_graphics::Rect {
1084                x: padding_left,
1085                y: padding_top,
1086                width: viewport_width,
1087                height: viewport_height,
1088            };
1089
1090            if let Some(comp_range) = state.composition() {
1091                let comp_start = comp_range.min();
1092                let comp_end = comp_range.max();
1093
1094                if comp_start < comp_end && comp_end <= text.len() {
1095                    let underline_brush = cranpose_ui_graphics::Brush::solid(
1096                        cranpose_ui_graphics::Color(0.8, 0.8, 0.8, 0.8),
1097                    );
1098                    let underline_height: f32 = 2.0;
1099
1100                    for line_rect in range_visual_line_rects(
1101                        &text,
1102                        &style,
1103                        node_id.get(),
1104                        measured_wrap_width.get(),
1105                        padding_left,
1106                        padding_top,
1107                        pan,
1108                        line_height,
1109                        comp_start,
1110                        comp_end,
1111                    ) {
1112                        let underline_rect = cranpose_ui_graphics::Rect {
1113                            x: line_rect.x,
1114                            y: line_rect.y + line_height - underline_height,
1115                            width: line_rect.width,
1116                            height: underline_height,
1117                        };
1118                        if let Some(clipped) = intersect_rect(underline_rect, clip_bounds) {
1119                            primitives.push(DrawPrimitive::Rect {
1120                                rect: clipped,
1121                                brush: underline_brush.clone(),
1122                                stroke: None,
1123                            });
1124                        }
1125                    }
1126                }
1127            }
1128
1129            if selection.collapsed() && crate::cursor_animation::is_cursor_visible() {
1130                let pos = selection.start.min(text.len());
1131                let (line_index, line_start) = caret_visual_line_for_offset(
1132                    &text,
1133                    &style,
1134                    node_id.get(),
1135                    measured_wrap_width.get(),
1136                    pos,
1137                    crate::text_selection::LineAffinity::Upstream,
1138                );
1139                let cursor_x = crate::text::measure_text(
1140                    &crate::text::AnnotatedString::from(&text[line_start..pos]),
1141                    &style,
1142                )
1143                .width
1144                    + padding_left
1145                    - pan;
1146                let (box_off, box_h) = crate::text::glyph_line_box(&style, line_height);
1147                let cursor_y = padding_top + line_index as f32 * line_height + box_off;
1148
1149                let cursor_rect = cranpose_ui_graphics::Rect {
1150                    x: cursor_x,
1151                    y: cursor_y,
1152                    width: CURSOR_WIDTH,
1153                    height: box_h,
1154                };
1155
1156                if let Some(clipped) = intersect_rect(cursor_rect, clip_bounds) {
1157                    primitives.push(DrawPrimitive::Rect {
1158                        rect: clipped,
1159                        brush: cursor_brush.clone(),
1160                        stroke: None,
1161                    });
1162                }
1163            }
1164
1165            scope.push_recorded(primitives);
1166        }))
1167    }
1168
1169    fn create_behind_draw_closure(
1170        &self,
1171    ) -> Option<Rc<dyn Fn(&mut cranpose_ui_graphics::DrawScopeDefault)>> {
1172        use cranpose_ui_graphics::{DrawPrimitive, DrawScope as _};
1173
1174        let is_focused = self.refs.is_focused.clone();
1175        let state = self.state;
1176        let content_offset = self.refs.content_offset.clone();
1177        let content_y_offset = self.refs.content_y_offset.clone();
1178        let selection_brush = self.selection_brush.clone();
1179        let style = self.style.clone();
1180        let cached_line_height = self.measured_line_height.clone();
1181        let measured_size = self.measured_size.clone();
1182        let measured_wrap_width = self.measured_wrap_width.clone();
1183        let node_id = self.refs.node_id.clone();
1184        let pan_resolver = self.cached_pan_resolver.clone();
1185
1186        Some(Rc::new(move |scope| {
1187            let size = scope.size();
1188            if !*is_focused.borrow() {
1189                return;
1190            }
1191            let selection = state.selection();
1192            if selection.collapsed() {
1193                return;
1194            }
1195            let text = state.text();
1196            let padding_left = content_offset.get();
1197            let padding_top = content_y_offset.get();
1198            let line_height = cached_line_height.get();
1199            let (viewport_width, viewport_height) =
1200                content_viewport(measured_size.get(), size, padding_left, padding_top);
1201            let pan = pan_resolver(viewport_width);
1202            let clip_bounds = cranpose_ui_graphics::Rect {
1203                x: padding_left,
1204                y: padding_top,
1205                width: viewport_width,
1206                height: viewport_height,
1207            };
1208
1209            let mut primitives = Vec::new();
1210            let (box_off, box_h) = crate::text::glyph_line_box(&style, line_height);
1211            for sel_rect in range_visual_line_rects(
1212                &text,
1213                &style,
1214                node_id.get(),
1215                measured_wrap_width.get(),
1216                padding_left,
1217                padding_top,
1218                pan,
1219                line_height,
1220                selection.min(),
1221                selection.max(),
1222            ) {
1223                let sel_rect = cranpose_ui_graphics::Rect {
1224                    y: sel_rect.y + box_off,
1225                    height: box_h,
1226                    ..sel_rect
1227                };
1228                if let Some(clipped) = intersect_rect(sel_rect, clip_bounds) {
1229                    primitives.push(DrawPrimitive::Rect {
1230                        rect: clipped,
1231                        brush: selection_brush.clone(),
1232                        stroke: None,
1233                    });
1234                }
1235            }
1236            scope.push_recorded(primitives);
1237        }))
1238    }
1239}
1240
1241impl SemanticsNode for TextFieldModifierNode {
1242    fn merge_semantics(&self, config: &mut SemanticsConfiguration) {
1243        let text = self.state.text();
1244        if config.content_description.is_none() {
1245            config.content_description = Some(text.clone());
1246        }
1247        config.text = Some(text);
1248        config.is_editable_text = 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
1269fn floor_char_boundary(text: &str, index: usize) -> usize {
1270    let mut index = index.min(text.len());
1271    while !text.is_char_boundary(index) {
1272        index -= 1;
1273    }
1274    index
1275}
1276
1277impl PointerInputNode for TextFieldModifierNode {
1278    fn on_pointer_event(
1279        &mut self,
1280        _context: &mut dyn ModifierNodeContext,
1281        _event: &PointerEvent,
1282    ) -> bool {
1283        false
1284    }
1285
1286    fn hit_test(&self, x: f32, y: f32) -> bool {
1287        let size = self.measured_size.get();
1288        x >= 0.0 && x <= size.width && y >= 0.0 && y <= size.height
1289    }
1290
1291    fn pointer_input_handler(&self) -> Option<Rc<dyn Fn(PointerEvent)>> {
1292        Some(self.cached_handler.clone())
1293    }
1294}
1295
1296/// Element that creates and updates `TextFieldModifierNode` instances.
1297///
1298/// This follows the modifier element pattern where the element is responsible for:
1299/// - Creating new nodes (via `create`)
1300/// - Updating existing nodes when properties change (via `update`)
1301/// - Declaring capabilities (LAYOUT | DRAW | SEMANTICS)
1302#[derive(Clone)]
1303pub struct TextFieldElement {
1304    state: TextFieldState,
1305    style: TextStyle,
1306    cursor_color: Color,
1307    line_limits: TextFieldLineLimits,
1308    handle_controller: Option<TextFieldHandleController>,
1309    modal_depth: usize,
1310}
1311
1312impl TextFieldElement {
1313    /// Creates a new text field element.
1314    pub fn new(state: TextFieldState, style: TextStyle) -> Self {
1315        Self {
1316            state,
1317            style,
1318            cursor_color: DEFAULT_CURSOR_COLOR,
1319            line_limits: TextFieldLineLimits::default(),
1320            handle_controller: None,
1321            modal_depth: 0,
1322        }
1323    }
1324
1325    /// Creates an element with custom cursor color.
1326    pub fn with_cursor_color(mut self, color: Color) -> Self {
1327        self.cursor_color = color;
1328        self
1329    }
1330
1331    /// Creates an element with custom line limits.
1332    pub fn with_line_limits(mut self, line_limits: TextFieldLineLimits) -> Self {
1333        self.line_limits = line_limits;
1334        self
1335    }
1336
1337    /// Installs the finger-handle metrics channel shared with the composable.
1338    pub fn with_handle_controller(mut self, controller: TextFieldHandleController) -> Self {
1339        self.handle_controller = Some(controller);
1340        self
1341    }
1342
1343    /// Sets the modal depth this field was composed at (see
1344    /// [`crate::modal::local_modal_depth`]).
1345    pub fn with_modal_depth(mut self, depth: usize) -> Self {
1346        self.modal_depth = depth;
1347        self
1348    }
1349}
1350
1351impl std::fmt::Debug for TextFieldElement {
1352    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1353        f.debug_struct("TextFieldElement")
1354            .field("text", &self.state.text())
1355            .field("style", &self.style)
1356            .field("cursor_color", &self.cursor_color)
1357            .finish()
1358    }
1359}
1360
1361impl Hash for TextFieldElement {
1362    fn hash<H: Hasher>(&self, state: &mut H) {
1363        self.state.id().hash(state);
1364        self.cursor_color.0.to_bits().hash(state);
1365        self.cursor_color.1.to_bits().hash(state);
1366        self.cursor_color.2.to_bits().hash(state);
1367        self.cursor_color.3.to_bits().hash(state);
1368        self.style.render_hash().hash(state);
1369        self.line_limits.hash(state);
1370        self.modal_depth.hash(state);
1371    }
1372}
1373
1374impl PartialEq for TextFieldElement {
1375    fn eq(&self, other: &Self) -> bool {
1376        self.state == other.state
1377            && self.style == other.style
1378            && self.cursor_color == other.cursor_color
1379            && self.line_limits == other.line_limits
1380            && self.modal_depth == other.modal_depth
1381    }
1382}
1383
1384impl Eq for TextFieldElement {}
1385
1386impl ModifierNodeElement for TextFieldElement {
1387    type Node = TextFieldModifierNode;
1388
1389    fn create(&self) -> Self::Node {
1390        let mut node = TextFieldModifierNode::new(self.state, self.style.clone())
1391            .with_cursor_color(self.cursor_color)
1392            .with_line_limits(self.line_limits);
1393        node.modal_depth = self.modal_depth;
1394        node.refs.modal_depth.set(self.modal_depth);
1395        if let Some(controller) = self.handle_controller.clone() {
1396            node = node.with_handle_controller(controller);
1397        }
1398        node.rebuild_cached_closures();
1399        node
1400    }
1401
1402    fn update(&self, node: &mut Self::Node) {
1403        node.state = self.state;
1404        node.style = self.style.clone();
1405        node.cursor_brush = Brush::solid(self.cursor_color);
1406        node.line_limits = self.line_limits;
1407        node.handle_controller = self.handle_controller.clone();
1408        node.modal_depth = self.modal_depth;
1409        node.refs.modal_depth.set(self.modal_depth);
1410        node.rebuild_cached_closures();
1411
1412        if node.update_cached_state() {}
1413    }
1414
1415    fn capabilities(&self) -> NodeCapabilities {
1416        NodeCapabilities::LAYOUT
1417            | NodeCapabilities::DRAW
1418            | NodeCapabilities::SEMANTICS
1419            | NodeCapabilities::POINTER_INPUT
1420    }
1421
1422    fn always_update(&self) -> bool {
1423        true
1424    }
1425}
1426
1427#[cfg(test)]
1428mod tests {
1429    use std::sync::Arc;
1430
1431    use cranpose_core::{DefaultScheduler, Runtime};
1432
1433    use super::*;
1434    use crate::text::TextStyle;
1435
1436    fn with_test_runtime<T>(f: impl FnOnce() -> T) -> T {
1437        let _runtime = Runtime::new(Arc::new(DefaultScheduler));
1438        f()
1439    }
1440
1441    #[test]
1442    fn text_field_node_creation() {
1443        let _app_context = crate::render_state::app_context_test_scope();
1444        with_test_runtime(|| {
1445            let state = TextFieldState::new("Hello");
1446            let node = TextFieldModifierNode::new(state, TextStyle::default());
1447            assert_eq!(node.text(), "Hello");
1448            assert!(!node.is_focused());
1449        });
1450    }
1451
1452    #[test]
1453    fn selection_rects_follow_wrapped_visual_lines() {
1454        let _app_context = crate::render_state::app_context_test_scope();
1455        let text = "aaaaa\nbb";
1456        let style = TextStyle::default();
1457        let line_height = 10.0_f32;
1458
1459        let rects = range_visual_line_rects(
1460            text,
1461            &style,
1462            None,
1463            Some(30.0),
1464            0.0,
1465            0.0,
1466            0.0,
1467            line_height,
1468            6,
1469            8,
1470        );
1471        assert_eq!(rects.len(), 1, "one visual line touched, got {rects:?}");
1472        assert_eq!(
1473            rects[0].y,
1474            2.0 * line_height,
1475            "highlight must land on visual line 2, not logical line 1"
1476        );
1477        assert!(rects[0].width > 0.0);
1478
1479        let spanning = range_visual_line_rects(
1480            text,
1481            &style,
1482            None,
1483            Some(30.0),
1484            0.0,
1485            0.0,
1486            0.0,
1487            line_height,
1488            0,
1489            5,
1490        );
1491        assert_eq!(spanning.len(), 2, "wrapped line spans two visual rows");
1492        assert_eq!(spanning[0].y, 0.0);
1493        assert_eq!(spanning[1].y, line_height);
1494    }
1495
1496    #[test]
1497    fn tap_resolves_offset_on_wrapped_visual_line() {
1498        let _app_context = crate::render_state::app_context_test_scope();
1499        let text = "aaaaa\nbb";
1500        let style = TextStyle::default();
1501        let line_height = 10.0_f32;
1502
1503        let off = crate::text::offset_for_position_wrapped(
1504            text,
1505            &style,
1506            None,
1507            Some(30.0),
1508            line_height,
1509            8.0,
1510            22.0,
1511        );
1512        assert!(
1513            (6..=8).contains(&off),
1514            "tap on visual line 'bb' resolved to {off}, expected 6..=8"
1515        );
1516
1517        let off1 = crate::text::offset_for_position_wrapped(
1518            text,
1519            &style,
1520            None,
1521            Some(30.0),
1522            line_height,
1523            4.0,
1524            12.0,
1525        );
1526        assert!(
1527            (3..=5).contains(&off1),
1528            "tap on wrapped 'aa' resolved to {off1}, expected 3..=5"
1529        );
1530
1531        let off2 = crate::text::offset_for_position_wrapped(
1532            "hello",
1533            &style,
1534            None,
1535            None,
1536            line_height,
1537            0.0,
1538            0.0,
1539        );
1540        assert_eq!(off2, 0);
1541    }
1542
1543    #[test]
1544    fn text_field_node_focus() {
1545        let _app_context = crate::render_state::app_context_test_scope();
1546        with_test_runtime(|| {
1547            let state = TextFieldState::new("Test");
1548            let mut node = TextFieldModifierNode::new(state, TextStyle::default());
1549            assert!(!node.is_focused());
1550
1551            node.set_focused(true);
1552            assert!(node.is_focused());
1553
1554            node.set_focused(false);
1555            assert!(!node.is_focused());
1556        });
1557    }
1558
1559    #[test]
1560    fn text_field_element_creates_node() {
1561        let _app_context = crate::render_state::app_context_test_scope();
1562        with_test_runtime(|| {
1563            let state = TextFieldState::new("Hello World");
1564            let element = TextFieldElement::new(state, TextStyle::default());
1565
1566            let node = element.create();
1567            assert_eq!(node.text(), "Hello World");
1568        });
1569    }
1570
1571    #[test]
1572    fn every_primary_pointer_source_publishes_direct_manipulation_metrics() {
1573        use cranpose_foundation::{PointerEvent, PointerEventKind, PointerSource};
1574        use cranpose_ui_graphics::Point;
1575
1576        let _app_context = crate::render_state::app_context_test_scope();
1577        with_test_runtime(|| {
1578            let state = TextFieldState::new("hello world");
1579            let controller = TextFieldHandleController::new();
1580            let mut node = TextFieldModifierNode::new(state, TextStyle::default())
1581                .with_handle_controller(controller.clone());
1582            node.measured_size.set(Size {
1583                width: 120.0,
1584                height: 20.0,
1585            });
1586
1587            let handler = node
1588                .pointer_input_handler()
1589                .expect("field exposes a pointer handler");
1590            let draw = node
1591                .create_draw_closure()
1592                .expect("field exposes a draw closure");
1593            let at = Point { x: 12.0, y: 8.0 };
1594            let size = Size {
1595                width: 120.0,
1596                height: 20.0,
1597            };
1598            let run_draw = || {
1599                let mut scope = crate::draw::command_draw_scope(size);
1600                draw(&mut scope);
1601            };
1602
1603            node.set_focused(true);
1604            run_draw();
1605            let keyboard_metrics = controller
1606                .metrics()
1607                .expect("focused field publishes handle metrics");
1608            assert!(!keyboard_metrics.direct_manipulation);
1609
1610            handler(
1611                PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Touch),
1612            );
1613            run_draw();
1614            let metrics = controller
1615                .metrics()
1616                .expect("focused field publishes handle metrics");
1617            assert!(metrics.focused, "a tap focuses the field");
1618            assert!(
1619                metrics.direct_manipulation,
1620                "a touch tap must expose direct-manipulation handles"
1621            );
1622            assert!(
1623                controller.press().is_some(),
1624                "touch must publish the live press"
1625            );
1626
1627            handler(
1628                PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Mouse),
1629            );
1630            run_draw();
1631            let metrics = controller
1632                .metrics()
1633                .expect("focused field publishes handle metrics");
1634            assert!(
1635                metrics.direct_manipulation,
1636                "a mouse tap must expose the same direct-manipulation handles"
1637            );
1638            assert!(
1639                controller.press().is_some(),
1640                "mouse must publish the live press"
1641            );
1642
1643            handler(
1644                PointerEvent::new(PointerEventKind::Down, at, at)
1645                    .with_source(PointerSource::Stylus),
1646            );
1647            run_draw();
1648            let metrics = controller
1649                .metrics()
1650                .expect("focused field publishes handle metrics");
1651            assert!(
1652                metrics.direct_manipulation,
1653                "a stylus contact must expose the same direct-manipulation handles"
1654            );
1655            assert!(
1656                controller.press().is_some(),
1657                "stylus must publish the live press"
1658            );
1659
1660            crate::text_field_focus::clear_focus();
1661        });
1662    }
1663
1664    #[test]
1665    fn double_tap_selects_the_word_under_the_finger() {
1666        use cranpose_foundation::{PointerEvent, PointerEventKind, PointerSource};
1667        use cranpose_ui_graphics::Point;
1668
1669        let _app_context = crate::render_state::app_context_test_scope();
1670        with_test_runtime(|| {
1671            let state = TextFieldState::new("hello world");
1672            let node = TextFieldModifierNode::new(state, TextStyle::default());
1673            node.measured_size.set(Size {
1674                width: 200.0,
1675                height: 20.0,
1676            });
1677            let handler = node
1678                .pointer_input_handler()
1679                .expect("field exposes a pointer handler");
1680
1681            let at = Point { x: 2.0, y: 8.0 };
1682            handler(
1683                PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Touch),
1684            );
1685            handler(
1686                PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Touch),
1687            );
1688
1689            let selection = state.selection();
1690            assert!(
1691                !selection.collapsed(),
1692                "a double tap must produce a (word) selection, got {selection:?}"
1693            );
1694            let selected = &state.text()[selection.min()..selection.max()];
1695            assert_eq!(
1696                selected, "hello",
1697                "double tap should select the whole word under the finger"
1698            );
1699
1700            crate::text_field_focus::clear_focus();
1701        });
1702    }
1703
1704    #[test]
1705    fn repeated_taps_escalate_word_line_paragraph_then_cycle() {
1706        use cranpose_foundation::{PointerEvent, PointerEventKind, PointerSource};
1707        use cranpose_ui_graphics::Point;
1708
1709        let _app_context = crate::render_state::app_context_test_scope();
1710        with_test_runtime(|| {
1711            let text = "alpha beta\ngamma delta\n\nsecond para";
1712            let state = TextFieldState::new(text);
1713            let node = TextFieldModifierNode::new(state, TextStyle::default()).with_line_limits(
1714                TextFieldLineLimits::MultiLine {
1715                    min_lines: 1,
1716                    max_lines: usize::MAX,
1717                },
1718            );
1719            node.measured_size.set(Size {
1720                width: 400.0,
1721                height: 80.0,
1722            });
1723            let handler = node
1724                .pointer_input_handler()
1725                .expect("field exposes a pointer handler");
1726
1727            let at = Point { x: 2.0, y: 4.0 };
1728            let tap = || {
1729                handler(
1730                    PointerEvent::new(PointerEventKind::Down, at, at)
1731                        .with_source(PointerSource::Touch),
1732                );
1733            };
1734            let selected = |state: &TextFieldState| {
1735                let s = state.selection();
1736                state.text()[s.min()..s.max()].to_string()
1737            };
1738
1739            tap();
1740            assert!(state.selection().collapsed(), "first tap places the caret");
1741            tap();
1742            assert_eq!(selected(&state), "alpha", "double tap selects the word");
1743            tap();
1744            assert_eq!(
1745                selected(&state),
1746                "alpha beta",
1747                "triple tap selects the line"
1748            );
1749            tap();
1750            assert_eq!(
1751                selected(&state),
1752                "alpha beta\ngamma delta",
1753                "fourth tap grows to the paragraph"
1754            );
1755            tap();
1756            assert_eq!(
1757                selected(&state),
1758                "alpha",
1759                "fifth tap cycles back to the word"
1760            );
1761
1762            crate::text_field_focus::clear_focus();
1763        });
1764    }
1765
1766    #[test]
1767    fn single_tap_inside_selection_selects_the_word() {
1768        use cranpose_foundation::{PointerEvent, PointerEventKind, PointerSource};
1769        use cranpose_ui_graphics::Point;
1770
1771        let _app_context = crate::render_state::app_context_test_scope();
1772        with_test_runtime(|| {
1773            let state = TextFieldState::new("hello world");
1774            let node = TextFieldModifierNode::new(state, TextStyle::default());
1775            node.measured_size.set(Size {
1776                width: 200.0,
1777                height: 20.0,
1778            });
1779            let handler = node
1780                .pointer_input_handler()
1781                .expect("field exposes a pointer handler");
1782
1783            state.edit(|buffer| buffer.select(TextRange::new(0, 11)));
1784            assert!(!state.selection().collapsed());
1785
1786            let at = Point { x: 2.0, y: 8.0 };
1787            handler(
1788                PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Touch),
1789            );
1790
1791            let selection = state.selection();
1792            assert!(
1793                !selection.collapsed(),
1794                "a tap inside a selection must not collapse it, got {selection:?}"
1795            );
1796            assert_eq!(
1797                &state.text()[selection.min()..selection.max()],
1798                "hello",
1799                "a tap inside a selection re-selects the word under the finger"
1800            );
1801
1802            crate::text_field_focus::clear_focus();
1803        });
1804    }
1805
1806    #[test]
1807    fn slow_taps_inside_selection_cycle_word_line_paragraph_by_location() {
1808        use cranpose_foundation::{PointerEvent, PointerEventKind, PointerSource};
1809        use cranpose_ui_graphics::Point;
1810
1811        let _app_context = crate::render_state::app_context_test_scope();
1812        with_test_runtime(|| {
1813            let text = "alpha beta\ngamma delta\n\nsecond para";
1814            let state = TextFieldState::new(text);
1815            let node = TextFieldModifierNode::new(state, TextStyle::default()).with_line_limits(
1816                TextFieldLineLimits::MultiLine {
1817                    min_lines: 1,
1818                    max_lines: usize::MAX,
1819                },
1820            );
1821            node.measured_size.set(Size {
1822                width: 400.0,
1823                height: 80.0,
1824            });
1825            let handler = node
1826                .pointer_input_handler()
1827                .expect("field exposes a pointer handler");
1828
1829            state.edit(|buffer| buffer.select(TextRange::new(0, text.len())));
1830
1831            let at = Point { x: 2.0, y: 4.0 };
1832            let selected = |state: &TextFieldState| {
1833                let s = state.selection();
1834                state.text()[s.min()..s.max()].to_string()
1835            };
1836            let slow_tap = || {
1837                node.refs.last_click_time.set(None);
1838                handler(
1839                    PointerEvent::new(PointerEventKind::Down, at, at)
1840                        .with_source(PointerSource::Touch),
1841                );
1842            };
1843
1844            slow_tap();
1845            assert_eq!(
1846                selected(&state),
1847                "alpha",
1848                "tap inside selection grabs the word"
1849            );
1850            slow_tap();
1851            assert_eq!(
1852                selected(&state),
1853                "alpha beta",
1854                "same-spot tap grows to the line even after the timeout"
1855            );
1856            slow_tap();
1857            assert_eq!(
1858                selected(&state),
1859                "alpha beta\ngamma delta",
1860                "same-spot tap grows to the paragraph"
1861            );
1862            slow_tap();
1863            assert_eq!(
1864                selected(&state),
1865                "alpha",
1866                "same-spot tap cycles back to the word"
1867            );
1868
1869            crate::text_field_focus::clear_focus();
1870        });
1871    }
1872
1873    #[test]
1874    fn text_field_element_equality() {
1875        let _app_context = crate::render_state::app_context_test_scope();
1876        with_test_runtime(|| {
1877            let state1 = TextFieldState::new("Hello");
1878            let state2 = TextFieldState::new("Hello");
1879
1880            let elem1 = TextFieldElement::new(state1, TextStyle::default());
1881            let elem2 = TextFieldElement::new(state1, TextStyle::default());
1882            let elem3 = TextFieldElement::new(state2, TextStyle::default());
1883
1884            assert_eq!(elem1, elem2, "Same state should be equal");
1885            assert_ne!(elem1, elem3, "Different states should not be equal");
1886        });
1887    }
1888
1889    #[test]
1890    fn text_field_element_update_refreshes_existing_node_style() {
1891        let _app_context = crate::render_state::app_context_test_scope();
1892        with_test_runtime(|| {
1893            let state = TextFieldState::new("themed text");
1894            let dark_style = TextStyle::from_span_style(crate::text::SpanStyle {
1895                color: Some(Color::from_rgba_u8(228, 240, 252, 255)),
1896                ..crate::text::SpanStyle::default()
1897            });
1898            let light_style = TextStyle::from_span_style(crate::text::SpanStyle {
1899                color: Some(Color::from_rgba_u8(14, 58, 96, 255)),
1900                ..crate::text::SpanStyle::default()
1901            });
1902            let initial = TextFieldElement::new(state, dark_style);
1903            let updated = TextFieldElement::new(state, light_style.clone());
1904            let mut node = initial.create();
1905
1906            updated.update(&mut node);
1907
1908            assert_eq!(node.text(), "themed text");
1909            assert_eq!(node.style(), &light_style);
1910        });
1911    }
1912
1913    #[test]
1914    fn multiline_field_measures_wrapped_height() {
1915        let _app_context = crate::render_state::app_context_test_scope();
1916        with_test_runtime(|| {
1917            let long = "abcd ".repeat(40);
1918            let state = TextFieldState::new(&long);
1919            let node = TextFieldModifierNode::new(state, TextStyle::default());
1920            assert!(
1921                !node.line_limits().is_single_line(),
1922                "default fields are multi-line"
1923            );
1924
1925            let natural = node.measure_text_content(None);
1926            let wrapped = node.measure_text_content(node.wrap_width(20.0));
1927
1928            assert!(
1929                wrapped.height > natural.height,
1930                "wrapped multi-line height {} must exceed the single-line height {}",
1931                wrapped.height,
1932                natural.height
1933            );
1934        });
1935    }
1936
1937    #[test]
1938    fn single_line_field_never_wraps() {
1939        let _app_context = crate::render_state::app_context_test_scope();
1940        with_test_runtime(|| {
1941            let state = TextFieldState::new("abcd ".repeat(40));
1942            let node = TextFieldModifierNode::new(state, TextStyle::default())
1943                .with_line_limits(TextFieldLineLimits::SingleLine);
1944            assert_eq!(
1945                node.wrap_width(20.0),
1946                None,
1947                "single-line fields must not wrap"
1948            );
1949        });
1950    }
1951
1952    #[test]
1953    fn test_cursor_x_position_calculation() {
1954        let _app_context = crate::render_state::app_context_test_scope();
1955        with_test_runtime(|| {
1956            let style = crate::text::TextStyle::default();
1957
1958            let empty_width =
1959                crate::text::measure_text(&crate::text::AnnotatedString::from(""), &style).width;
1960            assert!(
1961                empty_width.abs() < 0.1,
1962                "Empty text should have 0 width, got {}",
1963                empty_width
1964            );
1965
1966            let hi_width =
1967                crate::text::measure_text(&crate::text::AnnotatedString::from("Hi"), &style).width;
1968            assert!(
1969                hi_width > 0.0,
1970                "Text 'Hi' should have positive width: {}",
1971                hi_width
1972            );
1973
1974            let h_width =
1975                crate::text::measure_text(&crate::text::AnnotatedString::from("H"), &style).width;
1976            assert!(h_width > 0.0, "Text 'H' should have positive width");
1977            assert!(
1978                h_width < hi_width,
1979                "'H' width {} should be less than 'Hi' width {}",
1980                h_width,
1981                hi_width
1982            );
1983
1984            let state = TextFieldState::new("Hi");
1985            assert_eq!(
1986                state.selection().start,
1987                2,
1988                "Cursor should be at position 2 (end of 'Hi')"
1989            );
1990
1991            let text = state.text();
1992            let cursor_pos = state.selection().start;
1993            let text_before_cursor = &text[..cursor_pos.min(text.len())];
1994            assert_eq!(text_before_cursor, "Hi");
1995
1996            let cursor_x = crate::text::measure_text(
1997                &crate::text::AnnotatedString::from(text_before_cursor),
1998                &style,
1999            )
2000            .width;
2001            assert!(
2002                (cursor_x - hi_width).abs() < 0.1,
2003                "Cursor x {} should equal 'Hi' width {}",
2004                cursor_x,
2005                hi_width
2006            );
2007        });
2008    }
2009
2010    #[test]
2011    fn test_focused_node_creates_cursor() {
2012        let _app_context = crate::render_state::app_context_test_scope();
2013        with_test_runtime(|| {
2014            let state = TextFieldState::new("Test");
2015            let element = TextFieldElement::new(state, TextStyle::default());
2016            let node = element.create();
2017
2018            assert!(!node.is_focused());
2019
2020            *node.refs.is_focused.borrow_mut() = true;
2021            assert!(node.is_focused());
2022
2023            assert_eq!(node.text(), "Test");
2024
2025            assert_eq!(node.selection().start, 4);
2026        });
2027    }
2028
2029    #[test]
2030    fn a_focus_requester_makes_the_text_field_receive_keyboard_input() {
2031        use cranpose_foundation::{BasicModifierNodeContext, ModifierNodeChain};
2032
2033        use crate::{
2034            key_event::{KeyCode, KeyEvent, KeyEventType, Modifiers},
2035            modifier::{FocusRequester, FocusRequesterElement},
2036        };
2037
2038        let _app_context = crate::render_state::app_context_test_scope();
2039        with_test_runtime(|| {
2040            let state = TextFieldState::new("");
2041            let requester = FocusRequester::new();
2042
2043            let mut context = BasicModifierNodeContext::new();
2044            context.set_node_id(Some(1));
2045            let mut chain = ModifierNodeChain::new();
2046            chain.update(
2047                vec![
2048                    cranpose_foundation::modifier_element(FocusRequesterElement::new(
2049                        requester.clone(),
2050                    )),
2051                    cranpose_foundation::modifier_element(TextFieldElement::new(
2052                        state,
2053                        TextStyle::default(),
2054                    )),
2055                ],
2056                &mut context,
2057            );
2058
2059            assert!(!crate::text_field_focus::has_focused_field());
2060
2061            requester
2062                .request_focus()
2063                .expect("the text field must accept a programmatic focus request");
2064
2065            assert!(crate::text_field_focus::has_focused_field());
2066
2067            let key_down = KeyEvent::new(KeyCode::H, "h", Modifiers::NONE, KeyEventType::KeyDown);
2068            assert!(
2069                crate::text_field_focus::dispatch_key_event(&key_down),
2070                "the field must consume a key event once focused programmatically"
2071            );
2072            assert_eq!(state.text(), "h");
2073        });
2074    }
2075
2076    #[test]
2077    fn two_text_fields_hand_off_keyboard_focus_via_their_requesters() {
2078        use cranpose_foundation::{BasicModifierNodeContext, ModifierNodeChain};
2079
2080        use crate::modifier::{FocusRequester, FocusRequesterElement};
2081
2082        let _app_context = crate::render_state::app_context_test_scope();
2083        with_test_runtime(|| {
2084            let state_a = TextFieldState::new("a-text");
2085            let state_b = TextFieldState::new("b-text");
2086            let requester_a = FocusRequester::new();
2087            let requester_b = FocusRequester::new();
2088
2089            let mut context = BasicModifierNodeContext::new();
2090            context.set_node_id(Some(1));
2091            let mut chain_a = ModifierNodeChain::new();
2092            chain_a.update(
2093                vec![
2094                    cranpose_foundation::modifier_element(FocusRequesterElement::new(
2095                        requester_a.clone(),
2096                    )),
2097                    cranpose_foundation::modifier_element(TextFieldElement::new(
2098                        state_a,
2099                        TextStyle::default(),
2100                    )),
2101                ],
2102                &mut context,
2103            );
2104
2105            context.set_node_id(Some(2));
2106            let mut chain_b = ModifierNodeChain::new();
2107            chain_b.update(
2108                vec![
2109                    cranpose_foundation::modifier_element(FocusRequesterElement::new(
2110                        requester_b.clone(),
2111                    )),
2112                    cranpose_foundation::modifier_element(TextFieldElement::new(
2113                        state_b,
2114                        TextStyle::default(),
2115                    )),
2116                ],
2117                &mut context,
2118            );
2119
2120            requester_a.request_focus().expect("field a accepts focus");
2121            assert_eq!(
2122                crate::text_field_focus::focused_field_node(),
2123                Some(1),
2124                "field a should own text-field keyboard focus"
2125            );
2126
2127            requester_b.request_focus().expect("field b accepts focus");
2128            assert_eq!(
2129                crate::text_field_focus::focused_field_node(),
2130                Some(2),
2131                "field b must take over text-field keyboard focus from field a"
2132            );
2133        });
2134    }
2135}