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