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.is_clickable = true;
1250 config.multiline = !matches!(self.line_limits, TextFieldLineLimits::SingleLine);
1251 let state = self.state;
1252 config.set_text = Some(cranpose_foundation::SemanticsSetText::new(move |text| {
1253 state.set_text(text)
1254 }));
1255 config.set_selection = Some(cranpose_foundation::SemanticsSetSelection::new(
1256 move |anchor, focus| {
1257 let text = state.text();
1258 let anchor = floor_char_boundary(&text, anchor);
1259 let focus = floor_char_boundary(&text, focus);
1260 state.set_selection(TextRange::new(anchor, focus));
1261 crate::cursor_animation::reset_cursor_blink();
1262 crate::request_render_invalidation();
1263 true
1264 },
1265 ));
1266 config.text_selection = Some(self.state.selection());
1267 }
1268}
1269
1270fn floor_char_boundary(text: &str, index: usize) -> usize {
1271 let mut index = index.min(text.len());
1272 while !text.is_char_boundary(index) {
1273 index -= 1;
1274 }
1275 index
1276}
1277
1278impl PointerInputNode for TextFieldModifierNode {
1279 fn on_pointer_event(
1280 &mut self,
1281 _context: &mut dyn ModifierNodeContext,
1282 _event: &PointerEvent,
1283 ) -> bool {
1284 false
1285 }
1286
1287 fn hit_test(&self, x: f32, y: f32) -> bool {
1288 let size = self.measured_size.get();
1289 x >= 0.0 && x <= size.width && y >= 0.0 && y <= size.height
1290 }
1291
1292 fn pointer_input_handler(&self) -> Option<Rc<dyn Fn(PointerEvent)>> {
1293 Some(self.cached_handler.clone())
1294 }
1295}
1296
1297#[derive(Clone)]
1304pub struct TextFieldElement {
1305 state: TextFieldState,
1306 style: TextStyle,
1307 cursor_color: Color,
1308 line_limits: TextFieldLineLimits,
1309 handle_controller: Option<TextFieldHandleController>,
1310 modal_depth: usize,
1311}
1312
1313impl TextFieldElement {
1314 pub fn new(state: TextFieldState, style: TextStyle) -> Self {
1316 Self {
1317 state,
1318 style,
1319 cursor_color: DEFAULT_CURSOR_COLOR,
1320 line_limits: TextFieldLineLimits::default(),
1321 handle_controller: None,
1322 modal_depth: 0,
1323 }
1324 }
1325
1326 pub fn with_cursor_color(mut self, color: Color) -> Self {
1328 self.cursor_color = color;
1329 self
1330 }
1331
1332 pub fn with_line_limits(mut self, line_limits: TextFieldLineLimits) -> Self {
1334 self.line_limits = line_limits;
1335 self
1336 }
1337
1338 pub fn with_handle_controller(mut self, controller: TextFieldHandleController) -> Self {
1340 self.handle_controller = Some(controller);
1341 self
1342 }
1343
1344 pub fn with_modal_depth(mut self, depth: usize) -> Self {
1347 self.modal_depth = depth;
1348 self
1349 }
1350}
1351
1352impl std::fmt::Debug for TextFieldElement {
1353 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1354 f.debug_struct("TextFieldElement")
1355 .field("text", &self.state.text())
1356 .field("style", &self.style)
1357 .field("cursor_color", &self.cursor_color)
1358 .finish()
1359 }
1360}
1361
1362impl Hash for TextFieldElement {
1363 fn hash<H: Hasher>(&self, state: &mut H) {
1364 self.state.id().hash(state);
1365 self.cursor_color.0.to_bits().hash(state);
1366 self.cursor_color.1.to_bits().hash(state);
1367 self.cursor_color.2.to_bits().hash(state);
1368 self.cursor_color.3.to_bits().hash(state);
1369 self.style.render_hash().hash(state);
1370 self.line_limits.hash(state);
1371 self.modal_depth.hash(state);
1372 }
1373}
1374
1375impl PartialEq for TextFieldElement {
1376 fn eq(&self, other: &Self) -> bool {
1377 self.state == other.state
1378 && self.style == other.style
1379 && self.cursor_color == other.cursor_color
1380 && self.line_limits == other.line_limits
1381 && self.modal_depth == other.modal_depth
1382 }
1383}
1384
1385impl Eq for TextFieldElement {}
1386
1387impl ModifierNodeElement for TextFieldElement {
1388 type Node = TextFieldModifierNode;
1389
1390 fn create(&self) -> Self::Node {
1391 let mut node = TextFieldModifierNode::new(self.state, self.style.clone())
1392 .with_cursor_color(self.cursor_color)
1393 .with_line_limits(self.line_limits);
1394 node.modal_depth = self.modal_depth;
1395 node.refs.modal_depth.set(self.modal_depth);
1396 if let Some(controller) = self.handle_controller.clone() {
1397 node = node.with_handle_controller(controller);
1398 }
1399 node.rebuild_cached_closures();
1400 node
1401 }
1402
1403 fn update(&self, node: &mut Self::Node) {
1404 node.state = self.state;
1405 node.style = self.style.clone();
1406 node.cursor_brush = Brush::solid(self.cursor_color);
1407 node.line_limits = self.line_limits;
1408 node.handle_controller = self.handle_controller.clone();
1409 node.modal_depth = self.modal_depth;
1410 node.refs.modal_depth.set(self.modal_depth);
1411 node.rebuild_cached_closures();
1412
1413 if node.update_cached_state() {}
1414 }
1415
1416 fn capabilities(&self) -> NodeCapabilities {
1417 NodeCapabilities::LAYOUT
1418 | NodeCapabilities::DRAW
1419 | NodeCapabilities::SEMANTICS
1420 | NodeCapabilities::POINTER_INPUT
1421 }
1422
1423 fn always_update(&self) -> bool {
1424 true
1425 }
1426}
1427
1428#[cfg(test)]
1429mod tests {
1430 use std::sync::Arc;
1431
1432 use cranpose_core::{DefaultScheduler, Runtime};
1433
1434 use super::*;
1435 use crate::text::TextStyle;
1436
1437 fn with_test_runtime<T>(f: impl FnOnce() -> T) -> T {
1438 let _runtime = Runtime::new(Arc::new(DefaultScheduler));
1439 f()
1440 }
1441
1442 #[test]
1443 fn text_field_node_creation() {
1444 let _app_context = crate::render_state::app_context_test_scope();
1445 with_test_runtime(|| {
1446 let state = TextFieldState::new("Hello");
1447 let node = TextFieldModifierNode::new(state, TextStyle::default());
1448 assert_eq!(node.text(), "Hello");
1449 assert!(!node.is_focused());
1450 });
1451 }
1452
1453 #[test]
1454 fn selection_rects_follow_wrapped_visual_lines() {
1455 let _app_context = crate::render_state::app_context_test_scope();
1456 let text = "aaaaa\nbb";
1457 let style = TextStyle::default();
1458 let line_height = 10.0_f32;
1459
1460 let rects = range_visual_line_rects(
1461 text,
1462 &style,
1463 None,
1464 Some(30.0),
1465 0.0,
1466 0.0,
1467 0.0,
1468 line_height,
1469 6,
1470 8,
1471 );
1472 assert_eq!(rects.len(), 1, "one visual line touched, got {rects:?}");
1473 assert_eq!(
1474 rects[0].y,
1475 2.0 * line_height,
1476 "highlight must land on visual line 2, not logical line 1"
1477 );
1478 assert!(rects[0].width > 0.0);
1479
1480 let spanning = range_visual_line_rects(
1481 text,
1482 &style,
1483 None,
1484 Some(30.0),
1485 0.0,
1486 0.0,
1487 0.0,
1488 line_height,
1489 0,
1490 5,
1491 );
1492 assert_eq!(spanning.len(), 2, "wrapped line spans two visual rows");
1493 assert_eq!(spanning[0].y, 0.0);
1494 assert_eq!(spanning[1].y, line_height);
1495 }
1496
1497 #[test]
1498 fn tap_resolves_offset_on_wrapped_visual_line() {
1499 let _app_context = crate::render_state::app_context_test_scope();
1500 let text = "aaaaa\nbb";
1501 let style = TextStyle::default();
1502 let line_height = 10.0_f32;
1503
1504 let off = crate::text::offset_for_position_wrapped(
1505 text,
1506 &style,
1507 None,
1508 Some(30.0),
1509 line_height,
1510 8.0,
1511 22.0,
1512 );
1513 assert!(
1514 (6..=8).contains(&off),
1515 "tap on visual line 'bb' resolved to {off}, expected 6..=8"
1516 );
1517
1518 let off1 = crate::text::offset_for_position_wrapped(
1519 text,
1520 &style,
1521 None,
1522 Some(30.0),
1523 line_height,
1524 4.0,
1525 12.0,
1526 );
1527 assert!(
1528 (3..=5).contains(&off1),
1529 "tap on wrapped 'aa' resolved to {off1}, expected 3..=5"
1530 );
1531
1532 let off2 = crate::text::offset_for_position_wrapped(
1533 "hello",
1534 &style,
1535 None,
1536 None,
1537 line_height,
1538 0.0,
1539 0.0,
1540 );
1541 assert_eq!(off2, 0);
1542 }
1543
1544 #[test]
1545 fn text_field_node_focus() {
1546 let _app_context = crate::render_state::app_context_test_scope();
1547 with_test_runtime(|| {
1548 let state = TextFieldState::new("Test");
1549 let mut node = TextFieldModifierNode::new(state, TextStyle::default());
1550 assert!(!node.is_focused());
1551
1552 node.set_focused(true);
1553 assert!(node.is_focused());
1554
1555 node.set_focused(false);
1556 assert!(!node.is_focused());
1557 });
1558 }
1559
1560 #[test]
1561 fn text_field_element_creates_node() {
1562 let _app_context = crate::render_state::app_context_test_scope();
1563 with_test_runtime(|| {
1564 let state = TextFieldState::new("Hello World");
1565 let element = TextFieldElement::new(state, TextStyle::default());
1566
1567 let node = element.create();
1568 assert_eq!(node.text(), "Hello World");
1569 });
1570 }
1571
1572 #[test]
1573 fn every_primary_pointer_source_publishes_direct_manipulation_metrics() {
1574 use cranpose_foundation::{PointerEvent, PointerEventKind, PointerSource};
1575 use cranpose_ui_graphics::Point;
1576
1577 let _app_context = crate::render_state::app_context_test_scope();
1578 with_test_runtime(|| {
1579 let state = TextFieldState::new("hello world");
1580 let controller = TextFieldHandleController::new();
1581 let mut node = TextFieldModifierNode::new(state, TextStyle::default())
1582 .with_handle_controller(controller.clone());
1583 node.measured_size.set(Size {
1584 width: 120.0,
1585 height: 20.0,
1586 });
1587
1588 let handler = node
1589 .pointer_input_handler()
1590 .expect("field exposes a pointer handler");
1591 let draw = node
1592 .create_draw_closure()
1593 .expect("field exposes a draw closure");
1594 let at = Point { x: 12.0, y: 8.0 };
1595 let size = Size {
1596 width: 120.0,
1597 height: 20.0,
1598 };
1599 let run_draw = || {
1600 let mut scope = crate::draw::command_draw_scope(size);
1601 draw(&mut scope);
1602 };
1603
1604 node.set_focused(true);
1605 run_draw();
1606 let keyboard_metrics = controller
1607 .metrics()
1608 .expect("focused field publishes handle metrics");
1609 assert!(!keyboard_metrics.direct_manipulation);
1610
1611 handler(
1612 PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Touch),
1613 );
1614 run_draw();
1615 let metrics = controller
1616 .metrics()
1617 .expect("focused field publishes handle metrics");
1618 assert!(metrics.focused, "a tap focuses the field");
1619 assert!(
1620 metrics.direct_manipulation,
1621 "a touch tap must expose direct-manipulation handles"
1622 );
1623 assert!(
1624 controller.press().is_some(),
1625 "touch must publish the live press"
1626 );
1627
1628 handler(
1629 PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Mouse),
1630 );
1631 run_draw();
1632 let metrics = controller
1633 .metrics()
1634 .expect("focused field publishes handle metrics");
1635 assert!(
1636 metrics.direct_manipulation,
1637 "a mouse tap must expose the same direct-manipulation handles"
1638 );
1639 assert!(
1640 controller.press().is_some(),
1641 "mouse must publish the live press"
1642 );
1643
1644 handler(
1645 PointerEvent::new(PointerEventKind::Down, at, at)
1646 .with_source(PointerSource::Stylus),
1647 );
1648 run_draw();
1649 let metrics = controller
1650 .metrics()
1651 .expect("focused field publishes handle metrics");
1652 assert!(
1653 metrics.direct_manipulation,
1654 "a stylus contact must expose the same direct-manipulation handles"
1655 );
1656 assert!(
1657 controller.press().is_some(),
1658 "stylus must publish the live press"
1659 );
1660
1661 crate::text_field_focus::clear_focus();
1662 });
1663 }
1664
1665 #[test]
1666 fn double_tap_selects_the_word_under_the_finger() {
1667 use cranpose_foundation::{PointerEvent, PointerEventKind, PointerSource};
1668 use cranpose_ui_graphics::Point;
1669
1670 let _app_context = crate::render_state::app_context_test_scope();
1671 with_test_runtime(|| {
1672 let state = TextFieldState::new("hello world");
1673 let node = TextFieldModifierNode::new(state, TextStyle::default());
1674 node.measured_size.set(Size {
1675 width: 200.0,
1676 height: 20.0,
1677 });
1678 let handler = node
1679 .pointer_input_handler()
1680 .expect("field exposes a pointer handler");
1681
1682 let at = Point { x: 2.0, y: 8.0 };
1683 handler(
1684 PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Touch),
1685 );
1686 handler(
1687 PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Touch),
1688 );
1689
1690 let selection = state.selection();
1691 assert!(
1692 !selection.collapsed(),
1693 "a double tap must produce a (word) selection, got {selection:?}"
1694 );
1695 let selected = &state.text()[selection.min()..selection.max()];
1696 assert_eq!(
1697 selected, "hello",
1698 "double tap should select the whole word under the finger"
1699 );
1700
1701 crate::text_field_focus::clear_focus();
1702 });
1703 }
1704
1705 #[test]
1706 fn repeated_taps_escalate_word_line_paragraph_then_cycle() {
1707 use cranpose_foundation::{PointerEvent, PointerEventKind, PointerSource};
1708 use cranpose_ui_graphics::Point;
1709
1710 let _app_context = crate::render_state::app_context_test_scope();
1711 with_test_runtime(|| {
1712 let text = "alpha beta\ngamma delta\n\nsecond para";
1713 let state = TextFieldState::new(text);
1714 let node = TextFieldModifierNode::new(state, TextStyle::default()).with_line_limits(
1715 TextFieldLineLimits::MultiLine {
1716 min_lines: 1,
1717 max_lines: usize::MAX,
1718 },
1719 );
1720 node.measured_size.set(Size {
1721 width: 400.0,
1722 height: 80.0,
1723 });
1724 let handler = node
1725 .pointer_input_handler()
1726 .expect("field exposes a pointer handler");
1727
1728 let at = Point { x: 2.0, y: 4.0 };
1729 let tap = || {
1730 handler(
1731 PointerEvent::new(PointerEventKind::Down, at, at)
1732 .with_source(PointerSource::Touch),
1733 );
1734 };
1735 let selected = |state: &TextFieldState| {
1736 let s = state.selection();
1737 state.text()[s.min()..s.max()].to_string()
1738 };
1739
1740 tap();
1741 assert!(state.selection().collapsed(), "first tap places the caret");
1742 tap();
1743 assert_eq!(selected(&state), "alpha", "double tap selects the word");
1744 tap();
1745 assert_eq!(
1746 selected(&state),
1747 "alpha beta",
1748 "triple tap selects the line"
1749 );
1750 tap();
1751 assert_eq!(
1752 selected(&state),
1753 "alpha beta\ngamma delta",
1754 "fourth tap grows to the paragraph"
1755 );
1756 tap();
1757 assert_eq!(
1758 selected(&state),
1759 "alpha",
1760 "fifth tap cycles back to the word"
1761 );
1762
1763 crate::text_field_focus::clear_focus();
1764 });
1765 }
1766
1767 #[test]
1768 fn single_tap_inside_selection_selects_the_word() {
1769 use cranpose_foundation::{PointerEvent, PointerEventKind, PointerSource};
1770 use cranpose_ui_graphics::Point;
1771
1772 let _app_context = crate::render_state::app_context_test_scope();
1773 with_test_runtime(|| {
1774 let state = TextFieldState::new("hello world");
1775 let node = TextFieldModifierNode::new(state, TextStyle::default());
1776 node.measured_size.set(Size {
1777 width: 200.0,
1778 height: 20.0,
1779 });
1780 let handler = node
1781 .pointer_input_handler()
1782 .expect("field exposes a pointer handler");
1783
1784 state.edit(|buffer| buffer.select(TextRange::new(0, 11)));
1785 assert!(!state.selection().collapsed());
1786
1787 let at = Point { x: 2.0, y: 8.0 };
1788 handler(
1789 PointerEvent::new(PointerEventKind::Down, at, at).with_source(PointerSource::Touch),
1790 );
1791
1792 let selection = state.selection();
1793 assert!(
1794 !selection.collapsed(),
1795 "a tap inside a selection must not collapse it, got {selection:?}"
1796 );
1797 assert_eq!(
1798 &state.text()[selection.min()..selection.max()],
1799 "hello",
1800 "a tap inside a selection re-selects the word under the finger"
1801 );
1802
1803 crate::text_field_focus::clear_focus();
1804 });
1805 }
1806
1807 #[test]
1808 fn slow_taps_inside_selection_cycle_word_line_paragraph_by_location() {
1809 use cranpose_foundation::{PointerEvent, PointerEventKind, PointerSource};
1810 use cranpose_ui_graphics::Point;
1811
1812 let _app_context = crate::render_state::app_context_test_scope();
1813 with_test_runtime(|| {
1814 let text = "alpha beta\ngamma delta\n\nsecond para";
1815 let state = TextFieldState::new(text);
1816 let node = TextFieldModifierNode::new(state, TextStyle::default()).with_line_limits(
1817 TextFieldLineLimits::MultiLine {
1818 min_lines: 1,
1819 max_lines: usize::MAX,
1820 },
1821 );
1822 node.measured_size.set(Size {
1823 width: 400.0,
1824 height: 80.0,
1825 });
1826 let handler = node
1827 .pointer_input_handler()
1828 .expect("field exposes a pointer handler");
1829
1830 state.edit(|buffer| buffer.select(TextRange::new(0, text.len())));
1831
1832 let at = Point { x: 2.0, y: 4.0 };
1833 let selected = |state: &TextFieldState| {
1834 let s = state.selection();
1835 state.text()[s.min()..s.max()].to_string()
1836 };
1837 let slow_tap = || {
1838 node.refs.last_click_time.set(None);
1839 handler(
1840 PointerEvent::new(PointerEventKind::Down, at, at)
1841 .with_source(PointerSource::Touch),
1842 );
1843 };
1844
1845 slow_tap();
1846 assert_eq!(
1847 selected(&state),
1848 "alpha",
1849 "tap inside selection grabs the word"
1850 );
1851 slow_tap();
1852 assert_eq!(
1853 selected(&state),
1854 "alpha beta",
1855 "same-spot tap grows to the line even after the timeout"
1856 );
1857 slow_tap();
1858 assert_eq!(
1859 selected(&state),
1860 "alpha beta\ngamma delta",
1861 "same-spot tap grows to the paragraph"
1862 );
1863 slow_tap();
1864 assert_eq!(
1865 selected(&state),
1866 "alpha",
1867 "same-spot tap cycles back to the word"
1868 );
1869
1870 crate::text_field_focus::clear_focus();
1871 });
1872 }
1873
1874 #[test]
1875 fn text_field_element_equality() {
1876 let _app_context = crate::render_state::app_context_test_scope();
1877 with_test_runtime(|| {
1878 let state1 = TextFieldState::new("Hello");
1879 let state2 = TextFieldState::new("Hello");
1880
1881 let elem1 = TextFieldElement::new(state1, TextStyle::default());
1882 let elem2 = TextFieldElement::new(state1, TextStyle::default());
1883 let elem3 = TextFieldElement::new(state2, TextStyle::default());
1884
1885 assert_eq!(elem1, elem2, "Same state should be equal");
1886 assert_ne!(elem1, elem3, "Different states should not be equal");
1887 });
1888 }
1889
1890 #[test]
1891 fn text_field_element_update_refreshes_existing_node_style() {
1892 let _app_context = crate::render_state::app_context_test_scope();
1893 with_test_runtime(|| {
1894 let state = TextFieldState::new("themed text");
1895 let dark_style = TextStyle::from_span_style(crate::text::SpanStyle {
1896 color: Some(Color::from_rgba_u8(228, 240, 252, 255)),
1897 ..crate::text::SpanStyle::default()
1898 });
1899 let light_style = TextStyle::from_span_style(crate::text::SpanStyle {
1900 color: Some(Color::from_rgba_u8(14, 58, 96, 255)),
1901 ..crate::text::SpanStyle::default()
1902 });
1903 let initial = TextFieldElement::new(state, dark_style);
1904 let updated = TextFieldElement::new(state, light_style.clone());
1905 let mut node = initial.create();
1906
1907 updated.update(&mut node);
1908
1909 assert_eq!(node.text(), "themed text");
1910 assert_eq!(node.style(), &light_style);
1911 });
1912 }
1913
1914 #[test]
1915 fn multiline_field_measures_wrapped_height() {
1916 let _app_context = crate::render_state::app_context_test_scope();
1917 with_test_runtime(|| {
1918 let long = "abcd ".repeat(40);
1919 let state = TextFieldState::new(&long);
1920 let node = TextFieldModifierNode::new(state, TextStyle::default());
1921 assert!(
1922 !node.line_limits().is_single_line(),
1923 "default fields are multi-line"
1924 );
1925
1926 let natural = node.measure_text_content(None);
1927 let wrapped = node.measure_text_content(node.wrap_width(20.0));
1928
1929 assert!(
1930 wrapped.height > natural.height,
1931 "wrapped multi-line height {} must exceed the single-line height {}",
1932 wrapped.height,
1933 natural.height
1934 );
1935 });
1936 }
1937
1938 #[test]
1939 fn single_line_field_never_wraps() {
1940 let _app_context = crate::render_state::app_context_test_scope();
1941 with_test_runtime(|| {
1942 let state = TextFieldState::new("abcd ".repeat(40));
1943 let node = TextFieldModifierNode::new(state, TextStyle::default())
1944 .with_line_limits(TextFieldLineLimits::SingleLine);
1945 assert_eq!(
1946 node.wrap_width(20.0),
1947 None,
1948 "single-line fields must not wrap"
1949 );
1950 });
1951 }
1952
1953 #[test]
1954 fn test_cursor_x_position_calculation() {
1955 let _app_context = crate::render_state::app_context_test_scope();
1956 with_test_runtime(|| {
1957 let style = crate::text::TextStyle::default();
1958
1959 let empty_width =
1960 crate::text::measure_text(&crate::text::AnnotatedString::from(""), &style).width;
1961 assert!(
1962 empty_width.abs() < 0.1,
1963 "Empty text should have 0 width, got {}",
1964 empty_width
1965 );
1966
1967 let hi_width =
1968 crate::text::measure_text(&crate::text::AnnotatedString::from("Hi"), &style).width;
1969 assert!(
1970 hi_width > 0.0,
1971 "Text 'Hi' should have positive width: {}",
1972 hi_width
1973 );
1974
1975 let h_width =
1976 crate::text::measure_text(&crate::text::AnnotatedString::from("H"), &style).width;
1977 assert!(h_width > 0.0, "Text 'H' should have positive width");
1978 assert!(
1979 h_width < hi_width,
1980 "'H' width {} should be less than 'Hi' width {}",
1981 h_width,
1982 hi_width
1983 );
1984
1985 let state = TextFieldState::new("Hi");
1986 assert_eq!(
1987 state.selection().start,
1988 2,
1989 "Cursor should be at position 2 (end of 'Hi')"
1990 );
1991
1992 let text = state.text();
1993 let cursor_pos = state.selection().start;
1994 let text_before_cursor = &text[..cursor_pos.min(text.len())];
1995 assert_eq!(text_before_cursor, "Hi");
1996
1997 let cursor_x = crate::text::measure_text(
1998 &crate::text::AnnotatedString::from(text_before_cursor),
1999 &style,
2000 )
2001 .width;
2002 assert!(
2003 (cursor_x - hi_width).abs() < 0.1,
2004 "Cursor x {} should equal 'Hi' width {}",
2005 cursor_x,
2006 hi_width
2007 );
2008 });
2009 }
2010
2011 #[test]
2012 fn test_focused_node_creates_cursor() {
2013 let _app_context = crate::render_state::app_context_test_scope();
2014 with_test_runtime(|| {
2015 let state = TextFieldState::new("Test");
2016 let element = TextFieldElement::new(state, TextStyle::default());
2017 let node = element.create();
2018
2019 assert!(!node.is_focused());
2020
2021 *node.refs.is_focused.borrow_mut() = true;
2022 assert!(node.is_focused());
2023
2024 assert_eq!(node.text(), "Test");
2025
2026 assert_eq!(node.selection().start, 4);
2027 });
2028 }
2029
2030 #[test]
2031 fn a_focus_requester_makes_the_text_field_receive_keyboard_input() {
2032 use cranpose_foundation::{BasicModifierNodeContext, ModifierNodeChain};
2033
2034 use crate::{
2035 key_event::{KeyCode, KeyEvent, KeyEventType, Modifiers},
2036 modifier::{FocusRequester, FocusRequesterElement},
2037 };
2038
2039 let _app_context = crate::render_state::app_context_test_scope();
2040 with_test_runtime(|| {
2041 let state = TextFieldState::new("");
2042 let requester = FocusRequester::new();
2043
2044 let mut context = BasicModifierNodeContext::new();
2045 context.set_node_id(Some(1));
2046 let mut chain = ModifierNodeChain::new();
2047 chain.update(
2048 vec![
2049 cranpose_foundation::modifier_element(FocusRequesterElement::new(
2050 requester.clone(),
2051 )),
2052 cranpose_foundation::modifier_element(TextFieldElement::new(
2053 state,
2054 TextStyle::default(),
2055 )),
2056 ],
2057 &mut context,
2058 );
2059
2060 assert!(!crate::text_field_focus::has_focused_field());
2061
2062 requester
2063 .request_focus()
2064 .expect("the text field must accept a programmatic focus request");
2065
2066 assert!(crate::text_field_focus::has_focused_field());
2067
2068 let key_down = KeyEvent::new(KeyCode::H, "h", Modifiers::NONE, KeyEventType::KeyDown);
2069 assert!(
2070 crate::text_field_focus::dispatch_key_event(&key_down),
2071 "the field must consume a key event once focused programmatically"
2072 );
2073 assert_eq!(state.text(), "h");
2074 });
2075 }
2076
2077 #[test]
2078 fn two_text_fields_hand_off_keyboard_focus_via_their_requesters() {
2079 use cranpose_foundation::{BasicModifierNodeContext, ModifierNodeChain};
2080
2081 use crate::modifier::{FocusRequester, FocusRequesterElement};
2082
2083 let _app_context = crate::render_state::app_context_test_scope();
2084 with_test_runtime(|| {
2085 let state_a = TextFieldState::new("a-text");
2086 let state_b = TextFieldState::new("b-text");
2087 let requester_a = FocusRequester::new();
2088 let requester_b = FocusRequester::new();
2089
2090 let mut context = BasicModifierNodeContext::new();
2091 context.set_node_id(Some(1));
2092 let mut chain_a = ModifierNodeChain::new();
2093 chain_a.update(
2094 vec![
2095 cranpose_foundation::modifier_element(FocusRequesterElement::new(
2096 requester_a.clone(),
2097 )),
2098 cranpose_foundation::modifier_element(TextFieldElement::new(
2099 state_a,
2100 TextStyle::default(),
2101 )),
2102 ],
2103 &mut context,
2104 );
2105
2106 context.set_node_id(Some(2));
2107 let mut chain_b = ModifierNodeChain::new();
2108 chain_b.update(
2109 vec![
2110 cranpose_foundation::modifier_element(FocusRequesterElement::new(
2111 requester_b.clone(),
2112 )),
2113 cranpose_foundation::modifier_element(TextFieldElement::new(
2114 state_b,
2115 TextStyle::default(),
2116 )),
2117 ],
2118 &mut context,
2119 );
2120
2121 requester_a.request_focus().expect("field a accepts focus");
2122 assert_eq!(
2123 crate::text_field_focus::focused_field_node(),
2124 Some(1),
2125 "field a should own text-field keyboard focus"
2126 );
2127
2128 requester_b.request_focus().expect("field b accepts focus");
2129 assert_eq!(
2130 crate::text_field_focus::focused_field_node(),
2131 Some(2),
2132 "field b must take over text-field keyboard focus from field a"
2133 );
2134 });
2135 }
2136}