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