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