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 pub fn metrics(&self) -> Option<TextFieldHandleMetrics> {
91 let _ = self.inner.activation.value();
92 self.inner.metrics.get()
93 }
94
95 pub fn live_metrics(&self) -> Option<TextFieldHandleMetrics> {
103 let _ = self.inner.geometry.value();
104 self.inner.metrics.get()
105 }
106
107 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
200pub 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 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 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 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 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 pub fn scroll_offset(&self) -> f32 {
550 self.refs.scroll_offset.get()
551 }
552
553 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 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 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 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 pub fn text(&self) -> String {
772 self.state.text()
773 }
774
775 pub fn style(&self) -> &TextStyle {
776 &self.style
777 }
778
779 pub fn selection(&self) -> TextRange {
781 self.state.selection()
782 }
783
784 pub fn cursor_brush(&self) -> Brush {
786 self.cursor_brush.clone()
787 }
788
789 pub fn selection_brush(&self) -> Brush {
791 self.selection_brush.clone()
792 }
793
794 pub fn insert_text(&mut self, text: &str) {
796 self.state.edit(|buffer| {
797 buffer.insert(text);
798 });
799 }
800
801 pub fn copy_selection(&self) -> Option<String> {
804 self.state.copy_selection()
805 }
806
807 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 pub fn set_content_offset(&self, offset: f32) {
822 self.refs.content_offset.set(offset);
823 }
824
825 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#[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 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 pub fn with_cursor_color(mut self, color: Color) -> Self {
1327 self.cursor_color = color;
1328 self
1329 }
1330
1331 pub fn with_line_limits(mut self, line_limits: TextFieldLineLimits) -> Self {
1333 self.line_limits = line_limits;
1334 self
1335 }
1336
1337 pub fn with_handle_controller(mut self, controller: TextFieldHandleController) -> Self {
1339 self.handle_controller = Some(controller);
1340 self
1341 }
1342
1343 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}