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