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};
16use cranpose_ui_layout::ceil_to_px;
17
18#[derive(Clone, Copy, PartialEq, Debug)]
19pub struct TextFieldHandleMetrics {
20 pub focused: bool,
21 pub direct_manipulation: bool,
22 pub node_origin: Point,
23 pub padding_left: f32,
24 pub padding_top: f32,
25 pub scroll_offset: f32,
26 pub line_height: f32,
27 pub glyph_box: (f32, f32),
28 pub wrap_width: Option<f32>,
29}
30
31#[derive(Clone)]
32pub struct TextFieldHandleController {
33 inner: Rc<TextFieldHandleControllerInner>,
34}
35
36impl PartialEq for TextFieldHandleController {
37 fn eq(&self, other: &Self) -> bool {
38 Rc::ptr_eq(&self.inner, &other.inner)
39 }
40}
41
42struct TextFieldHandleControllerInner {
43 metrics: Cell<Option<TextFieldHandleMetrics>>,
44 activation: MutableState<u64>,
45 geometry: MutableState<u64>,
46 gesture_claim: RefCell<Option<Rc<Cell<bool>>>>,
47 press_track: Cell<Option<MutableState<Option<PointerPressTrack>>>>,
48}
49
50impl TextFieldHandleController {
51 pub fn new() -> Self {
52 Self {
53 inner: Rc::new(TextFieldHandleControllerInner {
54 metrics: Cell::new(None),
55 activation: mutableStateOf(0u64),
56 geometry: mutableStateOf(0u64),
57 gesture_claim: RefCell::new(None),
58 press_track: Cell::new(None),
59 }),
60 }
61 }
62
63 pub(crate) fn publish(&self, metrics: TextFieldHandleMetrics) {
64 let previous = self.inner.metrics.replace(Some(metrics));
65 if previous == Some(metrics) {
66 return;
67 }
68 let was_live = previous.map(|metrics| (metrics.focused, metrics.direct_manipulation));
69 if was_live != Some((metrics.focused, metrics.direct_manipulation)) {
70 self.bump(&self.inner.activation);
71 }
72 self.bump(&self.inner.geometry);
73 }
74
75 fn bump(&self, revision: &MutableState<u64>) {
76 revision.update(|value| *value = value.wrapping_add(1));
77 }
78
79 pub fn metrics(&self) -> Option<TextFieldHandleMetrics> {
92 let _ = self.inner.activation.value();
93 self.inner.metrics.get()
94 }
95
96 pub fn live_metrics(&self) -> Option<TextFieldHandleMetrics> {
104 let _ = self.inner.geometry.value();
105 self.inner.metrics.get()
106 }
107
108 pub fn metrics_now(&self) -> Option<TextFieldHandleMetrics> {
114 self.inner.metrics.get()
115 }
116
117 pub(crate) fn adopt_gesture_claim(&self, claim: &Rc<Cell<bool>>) {
118 let mut slot = self.inner.gesture_claim.borrow_mut();
119 let adopted = slot.as_ref().is_some_and(|held| Rc::ptr_eq(held, claim));
120 if !adopted {
121 *slot = Some(Rc::clone(claim));
122 }
123 }
124
125 pub(crate) fn adopt_press_track(&self, press_track: MutableState<Option<PointerPressTrack>>) {
126 if self.inner.press_track.get() != Some(press_track) {
127 self.inner.press_track.set(Some(press_track));
128 self.bump(&self.inner.activation);
129 }
130 }
131
132 pub fn press(&self) -> Option<PointerPressTrack> {
133 self.inner.press_track.get().and_then(|state| state.get())
134 }
135
136 pub fn claim_gesture(&self) {
137 if let Some(claim) = self.inner.gesture_claim.borrow().as_ref() {
138 claim.set(true);
139 }
140 }
141
142 pub fn gesture_claimed(&self) -> bool {
143 self.inner
144 .gesture_claim
145 .borrow()
146 .as_ref()
147 .is_some_and(|claim| claim.get())
148 }
149}
150
151impl Default for TextFieldHandleController {
152 fn default() -> Self {
153 Self::new()
154 }
155}
156
157const DEFAULT_CURSOR_COLOR: Color = Color(1.0, 1.0, 1.0, 1.0);
158
159pub(crate) const DEFAULT_SELECTION_COLOR: Color = Color(0.0, 0.5, 1.0, 0.3);
160
161const DEFAULT_LINE_HEIGHT: f32 = 20.0;
162
163const MIN_SIZE_TEXT: &str = "HHHHHHHHHH";
166
167const CURSOR_WIDTH: f32 = 2.0;
168
169pub(crate) fn compute_horizontal_scroll_offset(
170 current_offset: f32,
171 cursor_x: f32,
172 text_width: f32,
173 viewport_width: f32,
174) -> f32 {
175 if viewport_width <= 0.0 {
176 return 0.0;
177 }
178 let max_offset = (text_width + CURSOR_WIDTH - viewport_width).max(0.0);
179 let mut offset = current_offset.clamp(0.0, max_offset);
180 let visible_end = offset + viewport_width - CURSOR_WIDTH;
181 if cursor_x > visible_end {
182 offset = cursor_x - viewport_width + CURSOR_WIDTH;
183 } else if cursor_x < offset {
184 offset = cursor_x;
185 }
186 offset.clamp(0.0, max_offset)
187}
188
189pub(crate) fn intersect_rect(
190 rect: cranpose_ui_graphics::Rect,
191 bounds: cranpose_ui_graphics::Rect,
192) -> Option<cranpose_ui_graphics::Rect> {
193 let x0 = rect.x.max(bounds.x);
194 let y0 = rect.y.max(bounds.y);
195 let x1 = (rect.x + rect.width).min(bounds.x + bounds.width);
196 let y1 = (rect.y + rect.height).min(bounds.y + bounds.height);
197 (x1 > x0 && y1 > y0).then_some(cranpose_ui_graphics::Rect {
198 x: x0,
199 y: y0,
200 width: x1 - x0,
201 height: y1 - y0,
202 })
203}
204
205pub type TextPanResolver = Rc<dyn Fn(f32) -> f32>;
208
209#[derive(Clone)]
210pub(crate) struct TextFieldLayoutHandle {
211 state: TextFieldState,
212 node_id: Rc<Cell<Option<cranpose_core::NodeId>>>,
213 wrap_width: Rc<Cell<Option<f32>>>,
214}
215
216impl TextFieldLayoutHandle {
217 pub(crate) fn measured_layout(
218 &self,
219 style: &TextStyle,
220 ) -> std::rc::Rc<crate::text::PreparedTextLayout> {
221 crate::text::prepare_text_layout_for_node(
222 self.node_id.get(),
223 &crate::text::AnnotatedString::from(self.state.text()),
224 style,
225 crate::text::TextLayoutOptions::default(),
226 self.wrap_width.get(),
227 )
228 }
229}
230
231pub(crate) fn caret_visual_line_for_offset(
232 text: &str,
233 style: &TextStyle,
234 node_id: Option<cranpose_core::NodeId>,
235 wrap_width: Option<f32>,
236 offset: usize,
237 affinity: crate::text_selection::LineAffinity,
238) -> (usize, usize) {
239 let offset = offset.min(text.len());
240 match wrap_width {
241 Some(width) if width.is_finite() && width > 0.0 => {
242 let annotated = crate::text::AnnotatedString::from(text);
243 let ranges = crate::text::wrapped_line_ranges(
244 node_id,
245 &annotated,
246 style,
247 crate::text::TextLayoutOptions::default(),
248 Some(width),
249 );
250 crate::text_selection::caret_visual_line(&ranges, offset, affinity)
251 }
252 _ => {
253 let before = &text[..offset];
254 let line_index = before.matches('\n').count();
255 let line_start = before.rfind('\n').map_or(0, |i| i + 1);
256 (line_index, line_start)
257 }
258 }
259}
260
261#[expect(clippy::too_many_arguments)]
262pub(crate) fn range_visual_line_rects(
263 text: &str,
264 style: &TextStyle,
265 node_id: Option<cranpose_core::NodeId>,
266 wrap_width: Option<f32>,
267 pan: f32,
268 line_height: f32,
269 start: usize,
270 end: usize,
271) -> Vec<cranpose_ui_graphics::Rect> {
272 if start >= end {
273 return Vec::new();
274 }
275 let annotated = crate::text::AnnotatedString::from(text);
276 let line_ranges = crate::text::wrapped_line_ranges(
277 node_id,
278 &annotated,
279 style,
280 crate::text::TextLayoutOptions::default(),
281 wrap_width,
282 );
283 let mut rects = Vec::new();
284 for (line_idx, line_range) in line_ranges.iter().enumerate() {
285 let line_start = line_range.start;
286 let line_end = line_range.end;
287 if end <= line_start || start >= line_end {
288 continue;
289 }
290 let seg_start = start.max(line_start);
291 let seg_end = end.min(line_end);
292 let x0 = crate::text::measure_text(
293 &crate::text::AnnotatedString::from(&text[line_start..seg_start]),
294 style,
295 )
296 .width
297 - pan;
298 let x1 = crate::text::measure_text(
299 &crate::text::AnnotatedString::from(&text[line_start..seg_end]),
300 style,
301 )
302 .width
303 - pan;
304 let width = x1 - x0;
305 if width > 0.0 {
306 rects.push(cranpose_ui_graphics::Rect {
307 x: x0,
308 y: line_idx as f32 * line_height,
309 width,
310 height: line_height,
311 });
312 }
313 }
314 rects
315}
316
317fn build_focus_handler(
318 state: TextFieldState,
319 refs: &TextFieldRefs,
320 line_limits: TextFieldLineLimits,
321 style: &TextStyle,
322) -> Rc<dyn crate::text_field_focus::FocusedTextFieldHandler> {
323 crate::text_field_handler::TextFieldHandler::new(
324 state,
325 refs.node_id.get(),
326 refs.focus_node.get(),
327 line_limits,
328 crate::text_field_handler::CaretGeometryRefs {
329 node_origin: refs.node_origin.clone(),
330 content_origin: refs.content_origin.clone(),
331 scroll_offset: refs.scroll_offset.clone(),
332 style: style.clone(),
333 },
334 )
335}
336
337fn request_pointer_focus(
338 state: TextFieldState,
339 refs: &TextFieldRefs,
340 line_limits: TextFieldLineLimits,
341 style: &TextStyle,
342 modal_depth: usize,
343) {
344 if modal_depth < crate::modal::current_modal_depth()
345 || refs
346 .focus_node
347 .get()
348 .is_some_and(crate::focus_dispatch::request_focus_in_context)
349 {
350 return;
351 }
352 crate::text_field_focus::request_focus(
353 refs.is_focused.clone(),
354 build_focus_handler(state, refs, line_limits, style),
355 modal_depth,
356 );
357}
358
359pub(crate) struct TextFieldFocusBridge {
360 state: TextFieldState,
361 refs: TextFieldRefs,
362 style: TextStyle,
363 line_limits: TextFieldLineLimits,
364}
365
366impl TextFieldFocusBridge {
367 pub(crate) fn handle(
369 state: TextFieldState,
370 refs: TextFieldRefs,
371 style: TextStyle,
372 line_limits: TextFieldLineLimits,
373 ) -> Rc<dyn crate::focus_dispatch::FocusTargetHandle> {
374 Rc::new(Self {
375 state,
376 refs,
377 style,
378 line_limits,
379 })
380 }
381}
382
383impl crate::focus_dispatch::FocusTargetHandle for TextFieldFocusBridge {
384 fn set_focus_state(&self, state: FocusState) {
385 if state.is_focused() {
386 crate::text_field_focus::request_focus(
387 self.refs.is_focused.clone(),
388 build_focus_handler(self.state, &self.refs, self.line_limits, &self.style),
389 self.refs.modal_depth.get(),
390 );
391 } else if crate::text_field_focus::focused_field_node() == self.refs.node_id.get() {
392 crate::text_field_focus::clear_focus();
393 }
394 }
395}
396
397#[derive(Clone)]
398pub(crate) struct TextFieldRefs {
399 pub is_focused: Rc<RefCell<bool>>,
400 pub content_origin: Rc<RefCell<crate::modifier::CoordinatorRect>>,
403 pub drag_anchor: Rc<Cell<Option<crate::text_selection::SelectionAnchor>>>,
404 pub last_click_time: Rc<Cell<Option<web_time::Instant>>>,
405 pub last_click_pos: Rc<Cell<Option<(f32, f32)>>>,
406 pub click_count: Rc<Cell<u8>>,
407 pub node_id: Rc<Cell<Option<cranpose_core::NodeId>>>,
408 pub focus_node: Rc<Cell<Option<cranpose_core::NodeId>>>,
411 pub scroll_offset: Rc<Cell<f32>>,
412 pub direct_manipulation: Rc<Cell<bool>>,
413 pub node_origin: Rc<Cell<Point>>,
414 pub line_height: Rc<Cell<f32>>,
415 pub wrap_width: Rc<Cell<Option<f32>>>,
416 pub press_track: MutableState<Option<PointerPressTrack>>,
417 pub gesture_claimed: Rc<Cell<bool>>,
418 pub modal_depth: Rc<Cell<usize>>,
419}
420
421#[derive(Clone, Copy, Debug, PartialEq)]
422pub struct PointerPressTrack {
423 pub start: Point,
424 pub position: Point,
425}
426
427impl TextFieldRefs {
428 pub fn new() -> Self {
429 Self {
430 is_focused: Rc::new(RefCell::new(false)),
431 content_origin: Rc::default(),
432 drag_anchor: Rc::new(Cell::new(None)),
433 last_click_time: Rc::new(Cell::new(None::<web_time::Instant>)),
434 last_click_pos: Rc::new(Cell::new(None::<(f32, f32)>)),
435 click_count: Rc::new(Cell::new(0_u8)),
436 node_id: Rc::new(Cell::new(None::<cranpose_core::NodeId>)),
437 focus_node: Rc::new(Cell::new(None::<cranpose_core::NodeId>)),
438 scroll_offset: Rc::new(Cell::new(0.0_f32)),
439 direct_manipulation: Rc::new(Cell::new(false)),
440 node_origin: Rc::new(Cell::new(Point { x: 0.0, y: 0.0 })),
441 line_height: Rc::new(Cell::new(DEFAULT_LINE_HEIGHT)),
442 wrap_width: Rc::new(Cell::new(None::<f32>)),
443 press_track: mutableStateOf(None::<PointerPressTrack>),
444 gesture_claimed: Rc::new(Cell::new(false)),
445 modal_depth: Rc::new(Cell::new(0)),
446 }
447 }
448
449 pub(crate) fn key(&self) -> u64 {
452 Rc::as_ptr(&self.is_focused) as usize as u64
453 }
454}
455
456impl PartialEq for TextFieldRefs {
457 fn eq(&self, other: &Self) -> bool {
458 Rc::ptr_eq(&self.is_focused, &other.is_focused)
459 }
460}
461
462use crate::text::TextStyle;
463
464pub struct TextFieldModifierNode {
465 state: TextFieldState,
466 refs: TextFieldRefs,
467 style: TextStyle,
468 cursor_brush: Brush,
469 selection_brush: Brush,
470 line_limits: TextFieldLineLimits,
471 cached_text: String,
472 cached_selection: TextRange,
473 node_state: NodeState,
474 measured_size: Rc<Cell<Size>>,
475 min_size: Cell<Option<(f32, Size)>>,
477 measured_line_height: Rc<Cell<f32>>,
478 measured_wrap_width: Rc<Cell<Option<f32>>>,
479 cached_handler: Rc<dyn Fn(PointerEvent)>,
480 cached_pan_resolver: TextPanResolver,
481 handle_controller: Option<TextFieldHandleController>,
482 modal_depth: usize,
483 focus_bridge: Option<Rc<dyn crate::focus_dispatch::FocusTargetHandle>>,
484 decorated: bool,
487}
488
489impl std::fmt::Debug for TextFieldModifierNode {
490 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
491 f.debug_struct("TextFieldModifierNode")
492 .field("text", &self.state.text())
493 .field("style", &self.style)
494 .field("is_focused", &*self.refs.is_focused.borrow())
495 .finish()
496 }
497}
498
499impl TextFieldModifierNode {
500 pub fn new(state: TextFieldState, style: TextStyle) -> Self {
502 Self::with_refs(state, style, TextFieldRefs::new(), false)
503 }
504
505 fn with_refs(
506 state: TextFieldState,
507 style: TextStyle,
508 refs: TextFieldRefs,
509 decorated: bool,
510 ) -> Self {
511 let value = state.value();
512 let refs_line_height = refs.line_height.clone();
513 let refs_wrap_width = refs.wrap_width.clone();
514 let line_limits = TextFieldLineLimits::default();
515 let cached_handler =
516 Self::create_handler(state, refs.clone(), line_limits, style.clone(), 0);
517 let cached_pan_resolver =
518 Self::create_pan_resolver(state, refs.clone(), line_limits, style.clone());
519
520 Self {
521 state,
522 refs,
523 style,
524 cursor_brush: Brush::solid(DEFAULT_CURSOR_COLOR),
525 selection_brush: Brush::solid(DEFAULT_SELECTION_COLOR),
526 line_limits,
527 cached_text: value.text,
528 cached_selection: value.selection,
529 node_state: NodeState::new(),
530 measured_size: Rc::new(Cell::new(Size {
531 width: 0.0,
532 height: 0.0,
533 })),
534 min_size: Cell::new(None),
535 measured_line_height: refs_line_height,
536 measured_wrap_width: refs_wrap_width,
537 cached_handler,
538 cached_pan_resolver,
539 handle_controller: None,
540 modal_depth: 0,
541 focus_bridge: None,
542 decorated,
543 }
544 }
545
546 pub fn with_line_limits(mut self, line_limits: TextFieldLineLimits) -> Self {
548 self.line_limits = line_limits;
549 self.rebuild_cached_closures();
550 self
551 }
552
553 fn rebuild_cached_closures(&mut self) {
554 self.cached_handler = Self::create_handler(
555 self.state,
556 self.refs.clone(),
557 self.line_limits,
558 self.style.clone(),
559 self.modal_depth,
560 );
561 self.cached_pan_resolver = Self::create_pan_resolver(
562 self.state,
563 self.refs.clone(),
564 self.line_limits,
565 self.style.clone(),
566 );
567 }
568
569 pub fn with_handle_controller(mut self, controller: TextFieldHandleController) -> Self {
571 self.handle_controller = Some(controller);
572 self
573 }
574
575 fn create_pan_resolver(
576 state: TextFieldState,
577 refs: TextFieldRefs,
578 line_limits: TextFieldLineLimits,
579 style: TextStyle,
580 ) -> TextPanResolver {
581 Rc::new(move |viewport_width: f32| {
582 if !line_limits.is_single_line() {
583 refs.scroll_offset.set(0.0);
584 return 0.0;
585 }
586 let text = state.text();
587 let pos = state.selection().start.min(text.len());
588 let text_width = crate::text::measure_text(
589 &crate::text::AnnotatedString::from(text.as_str()),
590 &style,
591 )
592 .width;
593 let cursor_x = crate::text::measure_text(
594 &crate::text::AnnotatedString::from(&text[..pos]),
595 &style,
596 )
597 .width;
598 let offset = compute_horizontal_scroll_offset(
599 refs.scroll_offset.get(),
600 cursor_x,
601 text_width,
602 viewport_width,
603 );
604 refs.scroll_offset.set(offset);
605 offset
606 })
607 }
608
609 pub fn text_pan_resolver(&self) -> Option<TextPanResolver> {
614 self.line_limits
615 .is_single_line()
616 .then(|| self.cached_pan_resolver.clone())
617 }
618
619 pub fn scroll_offset(&self) -> f32 {
621 self.refs.scroll_offset.get()
622 }
623
624 pub fn line_limits(&self) -> TextFieldLineLimits {
626 self.line_limits
627 }
628
629 pub(crate) fn create_handler(
630 state: TextFieldState,
631 refs: TextFieldRefs,
632 line_limits: TextFieldLineLimits,
633 style: TextStyle,
634 modal_depth: usize,
635 ) -> Rc<dyn Fn(PointerEvent)> {
636 use crate::text_selection::{
637 MULTI_TAP_SLOP_PX, MULTI_TAP_TIMEOUT_MS, SelectionAnchor, SelectionGranularity,
638 classify_tap_count, resolve_selection_tap_count, tap_selection_granularity,
639 };
640
641 Rc::new(move |event: PointerEvent| {
642 refs.node_origin.set(Point {
643 x: event.global_position.x - event.position.x,
644 y: event.global_position.y - event.position.y,
645 });
646
647 let content = refs.content_origin.borrow().origin();
648 let click_x = (event.position.x - content.x + refs.scroll_offset.get()).max(0.0);
649 let click_y = (event.position.y - content.y).max(0.0);
650
651 match event.kind {
652 PointerEventKind::Down => {
653 refs.direct_manipulation.set(true);
654 refs.press_track.set(Some(PointerPressTrack {
655 start: event.global_position,
656 position: event.global_position,
657 }));
658 refs.gesture_claimed.set(false);
659
660 request_pointer_focus(state, &refs, line_limits, &style, modal_depth);
661
662 let now = web_time::Instant::now();
663 let text = state.text();
664 let pos = crate::text::offset_for_position_wrapped(
665 &text,
666 &style,
667 refs.node_id.get(),
668 refs.wrap_width.get(),
669 refs.line_height.get(),
670 click_x,
671 click_y,
672 );
673
674 let previous = refs.last_click_pos.get().and_then(|(px, py)| {
675 let count = refs.click_count.get();
676 (count > 0).then_some((count, px, py))
677 });
678 let elapsed_ms = refs
679 .last_click_time
680 .get()
681 .map_or(u128::MAX, |last| now.duration_since(last).as_millis());
682 let tap_count = classify_tap_count(
683 previous,
684 elapsed_ms,
685 event.position.x,
686 event.position.y,
687 MULTI_TAP_TIMEOUT_MS,
688 MULTI_TAP_SLOP_PX,
689 );
690
691 let selection = state.selection();
692 let tap_in_selection =
693 !selection.collapsed() && pos >= selection.min() && pos <= selection.max();
694 let repeat_in_place = refs.last_click_pos.get().is_some_and(|(px, py)| {
695 let dx = event.position.x - px;
696 let dy = event.position.y - py;
697 dx * dx + dy * dy <= MULTI_TAP_SLOP_PX * MULTI_TAP_SLOP_PX
698 });
699 let effective_count = resolve_selection_tap_count(
700 tap_count,
701 refs.click_count.get(),
702 tap_in_selection,
703 repeat_in_place,
704 );
705
706 let granularity = tap_selection_granularity(effective_count);
707 let anchor = SelectionAnchor::at(&text, pos, granularity);
708 refs.drag_anchor.set(Some(anchor));
709 state.edit(|buffer| match granularity {
710 SelectionGranularity::Caret => buffer.place_cursor_before_char(pos),
711 _ => buffer.select(TextRange::new(anchor.start, anchor.end)),
712 });
713
714 refs.click_count.set(effective_count);
715 refs.last_click_time.set(Some(now));
716 refs.last_click_pos
717 .set(Some((event.position.x, event.position.y)));
718 event.consume();
719 }
720 PointerEventKind::Move => {
721 if let Some(mut track) = refs.press_track.get() {
722 track.position = event.global_position;
723 refs.press_track.set(Some(track));
724 if let Some(node_id) = refs.node_id.get() {
725 crate::schedule_draw_repass(node_id);
726 }
727 crate::request_render_invalidation();
728 }
729 if refs.gesture_claimed.get() {
730 event.consume();
731 return;
732 }
733 if let Some(anchor) = refs.drag_anchor.get()
734 && *refs.is_focused.borrow()
735 {
736 let text = state.text();
737 let current_pos = crate::text::offset_for_position_wrapped(
738 &text,
739 &style,
740 refs.node_id.get(),
741 refs.wrap_width.get(),
742 refs.line_height.get(),
743 click_x,
744 click_y,
745 );
746
747 state.set_selection(anchor.dragged_to(&text, current_pos));
748
749 crate::request_render_invalidation();
750
751 event.consume();
752 }
753 }
754 PointerEventKind::Up => {
755 refs.drag_anchor.set(None);
756 refs.press_track.set(None);
757 refs.gesture_claimed.set(false);
758 if let Some(node_id) = refs.node_id.get() {
759 crate::schedule_draw_repass(node_id);
760 }
761 crate::request_render_invalidation();
762 }
763 PointerEventKind::Cancel => {
764 refs.press_track.set(None);
765 refs.gesture_claimed.set(false);
766 if let Some(node_id) = refs.node_id.get() {
767 crate::schedule_draw_repass(node_id);
768 }
769 crate::request_render_invalidation();
770 }
771 _ => {}
772 }
773 })
774 }
775
776 pub fn with_cursor_color(mut self, color: Color) -> Self {
781 self.cursor_brush = Brush::solid(color);
782 self.selection_brush = Brush::solid(
783 color.with_alpha(crate::widgets::basic_text_field::SELECTION_HIGHLIGHT_ALPHA),
784 );
785 self
786 }
787
788 pub fn set_focused(&mut self, focused: bool) {
790 let current = *self.refs.is_focused.borrow();
791 if current != focused {
792 *self.refs.is_focused.borrow_mut() = focused;
793 if !focused {
794 self.refs.direct_manipulation.set(false);
795 self.refs.press_track.set(None);
796 self.refs.gesture_claimed.set(false);
797 }
798 }
799 }
800
801 pub fn is_focused(&self) -> bool {
803 *self.refs.is_focused.borrow()
804 }
805
806 pub(crate) fn window_origin_sink(&self) -> Rc<Cell<Point>> {
807 self.refs.node_origin.clone()
808 }
809
810 pub(crate) fn layout_handle(&self) -> TextFieldLayoutHandle {
811 TextFieldLayoutHandle {
812 state: self.state,
813 node_id: self.refs.node_id.clone(),
814 wrap_width: self.measured_wrap_width.clone(),
815 }
816 }
817
818 pub fn text(&self) -> String {
820 self.state.text()
821 }
822
823 pub fn style(&self) -> &TextStyle {
824 &self.style
825 }
826
827 pub fn selection(&self) -> TextRange {
829 self.state.selection()
830 }
831
832 pub fn cursor_brush(&self) -> Brush {
834 self.cursor_brush.clone()
835 }
836
837 pub fn selection_brush(&self) -> Brush {
839 self.selection_brush.clone()
840 }
841
842 pub fn insert_text(&mut self, text: &str) {
844 self.state.edit(|buffer| {
845 buffer.insert(text);
846 });
847 }
848
849 pub fn copy_selection(&self) -> Option<String> {
852 self.state.copy_selection()
853 }
854
855 pub fn cut_selection(&mut self) -> Option<String> {
858 let text = self.copy_selection();
859 if text.is_some() {
860 self.state.edit(|buffer| {
861 buffer.delete(buffer.selection());
862 });
863 }
864 text
865 }
866
867 pub(crate) fn set_content_origin(&self, origin: crate::modifier::CoordinatorRect) {
870 *self.refs.content_origin.borrow_mut() = origin;
871 }
872
873 fn wrap_width(&self, available_width: f32) -> Option<f32> {
874 (!self.line_limits.is_single_line() && available_width.is_finite() && available_width > 0.0)
875 .then_some(available_width)
876 }
877
878 fn measure_text_content(&self, wrap_width: Option<f32>) -> Size {
879 let text = self.state.text();
880 let node_id = self.refs.node_id.get();
881 let annotated = crate::text::AnnotatedString::from(text.as_str());
882 let metrics = match wrap_width {
883 Some(max_width) => crate::text::measure_text_with_options_for_node(
884 node_id,
885 &annotated,
886 &self.style,
887 crate::text::TextLayoutOptions::default(),
888 Some(max_width),
889 ),
890 None => crate::text::measure_text_for_node(node_id, &annotated, &self.style),
891 };
892 self.measured_line_height.set(metrics.line_height);
893 Size {
894 width: metrics.width,
895 height: metrics.height,
896 }
897 }
898
899 fn text_size(&self, wrap_width: Option<f32>, density: f32) -> Size {
902 let size = self.measure_text_content(wrap_width);
903 Size {
904 width: ceil_to_px(size.width, density),
905 height: ceil_to_px(size.height, density),
906 }
907 }
908
909 fn min_size(&self, density: f32) -> Size {
913 if let Some((taken_at, size)) = self.min_size.get()
914 && taken_at == density
915 {
916 return size;
917 }
918 let metrics = crate::text::measure_text(
919 &crate::text::AnnotatedString::from(MIN_SIZE_TEXT),
920 &self.style,
921 );
922 let size = Size {
923 width: ceil_to_px(metrics.width, density),
924 height: ceil_to_px(metrics.height, density),
925 };
926 self.min_size.set(Some((density, size)));
927 size
928 }
929
930 fn update_cached_state(&mut self) -> bool {
931 let value = self.state.value();
932 let text_changed = value.text != self.cached_text;
933 let selection_changed = value.selection != self.cached_selection;
934
935 if text_changed {
936 self.cached_text = value.text;
937 }
938 if selection_changed {
939 self.cached_selection = value.selection;
940 }
941
942 text_changed || selection_changed
943 }
944}
945
946impl DelegatableNode for TextFieldModifierNode {
947 fn node_state(&self) -> &NodeState {
948 &self.node_state
949 }
950}
951
952impl ModifierNode for TextFieldModifierNode {
953 fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
954 self.refs.node_id.set(context.node_id());
955
956 context.invalidate(InvalidationKind::Layout);
957 context.invalidate(InvalidationKind::Draw);
958 if self.decorated {
959 return;
960 }
961 context.invalidate(InvalidationKind::Semantics);
962 self.refs.focus_node.set(context.node_id());
963
964 if let Some(node_id) = context.node_id() {
965 let bridge = TextFieldFocusBridge::handle(
966 self.state,
967 self.refs.clone(),
968 self.style.clone(),
969 self.line_limits,
970 );
971 self.focus_bridge = Some(Rc::clone(&bridge));
972 crate::focus_dispatch::register_focus_target(node_id, bridge);
973 }
974 }
975
976 fn on_detach(&mut self) {
977 if let (Some(node_id), Some(bridge)) = (self.refs.node_id.get(), self.focus_bridge.take()) {
978 crate::focus_dispatch::unregister_focus_target(node_id, &bridge);
979 }
980 }
981
982 fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
983 Some(self)
984 }
985
986 fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
987 Some(self)
988 }
989
990 fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
991 Some(self)
992 }
993
994 fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
995 Some(self)
996 }
997
998 fn as_semantics_node(&self) -> Option<&dyn SemanticsNode> {
999 (!self.decorated).then_some(self)
1000 }
1001
1002 fn as_semantics_node_mut(&mut self) -> Option<&mut dyn SemanticsNode> {
1003 if self.decorated { None } else { Some(self) }
1004 }
1005
1006 fn as_pointer_input_node(&self) -> Option<&dyn PointerInputNode> {
1007 (!self.decorated).then_some(self)
1008 }
1009
1010 fn as_pointer_input_node_mut(&mut self) -> Option<&mut dyn PointerInputNode> {
1011 if self.decorated { None } else { Some(self) }
1012 }
1013}
1014
1015impl LayoutModifierNode for TextFieldModifierNode {
1016 fn measure(
1017 &self,
1018 context: &mut dyn ModifierNodeContext,
1019 _measurable: &dyn Measurable,
1020 constraints: Constraints,
1021 ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
1022 let density = context.density();
1023 let wrap_width = self.wrap_width(constraints.max_width);
1024 self.measured_wrap_width.set(wrap_width);
1025 let text = self.text_size(wrap_width, density);
1026 let min = self.min_size(density);
1027 let fit = |length: f32, min: f32, low: f32, high: f32| {
1029 length.max(min.max(low).min(high)).min(high)
1030 };
1031 let size = Size {
1032 width: fit(
1033 text.width,
1034 min.width,
1035 constraints.min_width,
1036 constraints.max_width,
1037 ),
1038 height: fit(
1039 text.height,
1040 min.height,
1041 constraints.min_height,
1042 constraints.max_height,
1043 ),
1044 };
1045 self.measured_size.set(size);
1046
1047 let _ = (self.cached_pan_resolver)(size.width);
1048
1049 cranpose_ui_layout::LayoutModifierMeasureResult::with_size(size)
1050 }
1051
1052 fn min_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32, density: f32) -> f32 {
1055 self.text_size(None, density).width
1056 }
1057
1058 fn max_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32, density: f32) -> f32 {
1059 self.text_size(None, density).width
1060 }
1061
1062 fn min_intrinsic_height(&self, _measurable: &dyn Measurable, width: f32, density: f32) -> f32 {
1063 self.intrinsic_height(width, density)
1064 }
1065
1066 fn max_intrinsic_height(&self, _measurable: &dyn Measurable, width: f32, density: f32) -> f32 {
1067 self.intrinsic_height(width, density)
1068 }
1069}
1070
1071impl TextFieldModifierNode {
1072 fn intrinsic_height(&self, width: f32, density: f32) -> f32 {
1073 self.text_size(self.wrap_width(width), density)
1074 .height
1075 .max(self.min_size(density).height)
1076 }
1077}
1078
1079fn content_viewport(
1083 measured: cranpose_ui_graphics::Size,
1084 size: cranpose_foundation::Size,
1085) -> (f32, f32) {
1086 let width = if measured.width > 0.0 {
1087 measured.width
1088 } else {
1089 size.width
1090 };
1091 let height = if measured.height > 0.0 {
1092 measured.height
1093 } else {
1094 size.height
1095 };
1096 (width, height)
1097}
1098
1099impl DrawModifierNode for TextFieldModifierNode {
1100 fn create_draw_closure(
1101 &self,
1102 ) -> Option<Rc<dyn Fn(&mut cranpose_ui_graphics::DrawScopeDefault)>> {
1103 use cranpose_ui_graphics::{DrawPrimitive, DrawScope as _};
1104
1105 let is_focused = self.refs.is_focused.clone();
1106 let state = self.state;
1107 let content_origin = self.refs.content_origin.clone();
1108 let cursor_brush = self.cursor_brush.clone();
1109 let style = self.style.clone();
1110 let cached_line_height = self.measured_line_height.clone();
1111 let measured_size = self.measured_size.clone();
1112 let measured_wrap_width = self.measured_wrap_width.clone();
1113 let node_id = self.refs.node_id.clone();
1114 let pan_resolver = self.cached_pan_resolver.clone();
1115 let handle_controller = self.handle_controller.clone();
1116 let node_origin = self.refs.node_origin.clone();
1117 let direct_manipulation = self.refs.direct_manipulation.clone();
1118 let press_track = self.refs.press_track;
1119 let gesture_claimed = self.refs.gesture_claimed.clone();
1120
1121 Some(Rc::new(move |scope| {
1122 let size = scope.size();
1123 if !*is_focused.borrow() {
1124 if let Some(controller) = &handle_controller {
1125 controller.publish(TextFieldHandleMetrics {
1126 focused: false,
1127 direct_manipulation: false,
1128 node_origin: node_origin.get(),
1129 padding_left: 0.0,
1130 padding_top: 0.0,
1131 scroll_offset: 0.0,
1132 line_height: cached_line_height.get(),
1133 glyph_box: crate::text::glyph_line_box(&style, cached_line_height.get()),
1134 wrap_width: measured_wrap_width.get(),
1135 });
1136 }
1137 return;
1138 }
1139
1140 let mut primitives = Vec::new();
1141
1142 let text = state.text();
1143 let selection = state.selection();
1144 let line_height = cached_line_height.get();
1145
1146 let (viewport_width, viewport_height) = content_viewport(measured_size.get(), size);
1147 let pan = pan_resolver(viewport_width);
1148
1149 if let Some(controller) = &handle_controller {
1150 controller.adopt_gesture_claim(&gesture_claimed);
1151 controller.adopt_press_track(press_track);
1152 controller.publish(TextFieldHandleMetrics {
1153 focused: true,
1154 direct_manipulation: direct_manipulation.get(),
1155 node_origin: node_origin.get(),
1156 padding_left: content_origin.borrow().origin().x,
1157 padding_top: content_origin.borrow().origin().y,
1158 scroll_offset: pan,
1159 line_height,
1160 glyph_box: crate::text::glyph_line_box(&style, line_height),
1161 wrap_width: measured_wrap_width.get(),
1162 });
1163 }
1164 let clip_bounds = cranpose_ui_graphics::Rect {
1165 x: 0.0,
1166 y: 0.0,
1167 width: viewport_width,
1168 height: viewport_height,
1169 };
1170
1171 if let Some(comp_range) = state.composition() {
1172 let comp_start = comp_range.min();
1173 let comp_end = comp_range.max();
1174
1175 if comp_start < comp_end && comp_end <= text.len() {
1176 let underline_brush = cranpose_ui_graphics::Brush::solid(
1177 cranpose_ui_graphics::Color(0.8, 0.8, 0.8, 0.8),
1178 );
1179 let underline_height: f32 = 2.0;
1180
1181 for line_rect in range_visual_line_rects(
1182 &text,
1183 &style,
1184 node_id.get(),
1185 measured_wrap_width.get(),
1186 pan,
1187 line_height,
1188 comp_start,
1189 comp_end,
1190 ) {
1191 let underline_rect = cranpose_ui_graphics::Rect {
1192 x: line_rect.x,
1193 y: line_rect.y + line_height - underline_height,
1194 width: line_rect.width,
1195 height: underline_height,
1196 };
1197 if let Some(clipped) = intersect_rect(underline_rect, clip_bounds) {
1198 primitives.push(DrawPrimitive::Rect {
1199 rect: clipped,
1200 brush: underline_brush.clone(),
1201 stroke: None,
1202 });
1203 }
1204 }
1205 }
1206 }
1207
1208 if selection.collapsed() && crate::cursor_animation::is_cursor_visible() {
1209 let pos = selection.start.min(text.len());
1210 let (line_index, line_start) = caret_visual_line_for_offset(
1211 &text,
1212 &style,
1213 node_id.get(),
1214 measured_wrap_width.get(),
1215 pos,
1216 crate::text_selection::LineAffinity::Upstream,
1217 );
1218 let cursor_x = crate::text::measure_text(
1219 &crate::text::AnnotatedString::from(&text[line_start..pos]),
1220 &style,
1221 )
1222 .width
1223 - pan;
1224 let (box_off, box_h) = crate::text::glyph_line_box(&style, line_height);
1225 let cursor_y = line_index as f32 * line_height + box_off;
1226
1227 let cursor_rect = cranpose_ui_graphics::Rect {
1228 x: cursor_x,
1229 y: cursor_y,
1230 width: CURSOR_WIDTH,
1231 height: box_h,
1232 };
1233
1234 if let Some(clipped) = intersect_rect(cursor_rect, clip_bounds) {
1235 primitives.push(DrawPrimitive::Rect {
1236 rect: clipped,
1237 brush: cursor_brush.clone(),
1238 stroke: None,
1239 });
1240 }
1241 }
1242
1243 scope.push_recorded(primitives);
1244 }))
1245 }
1246
1247 fn create_behind_draw_closure(
1248 &self,
1249 ) -> Option<Rc<dyn Fn(&mut cranpose_ui_graphics::DrawScopeDefault)>> {
1250 use cranpose_ui_graphics::{DrawPrimitive, DrawScope as _};
1251
1252 let is_focused = self.refs.is_focused.clone();
1253 let state = self.state;
1254 let selection_brush = self.selection_brush.clone();
1255 let style = self.style.clone();
1256 let cached_line_height = self.measured_line_height.clone();
1257 let measured_size = self.measured_size.clone();
1258 let measured_wrap_width = self.measured_wrap_width.clone();
1259 let node_id = self.refs.node_id.clone();
1260 let pan_resolver = self.cached_pan_resolver.clone();
1261
1262 Some(Rc::new(move |scope| {
1263 let size = scope.size();
1264 if !*is_focused.borrow() {
1265 return;
1266 }
1267 let selection = state.selection();
1268 if selection.collapsed() {
1269 return;
1270 }
1271 let text = state.text();
1272 let line_height = cached_line_height.get();
1273 let (viewport_width, viewport_height) = content_viewport(measured_size.get(), size);
1274 let pan = pan_resolver(viewport_width);
1275 let clip_bounds = cranpose_ui_graphics::Rect {
1276 x: 0.0,
1277 y: 0.0,
1278 width: viewport_width,
1279 height: viewport_height,
1280 };
1281
1282 let mut primitives = Vec::new();
1283 let (box_off, box_h) = crate::text::glyph_line_box(&style, line_height);
1284 for sel_rect in range_visual_line_rects(
1285 &text,
1286 &style,
1287 node_id.get(),
1288 measured_wrap_width.get(),
1289 pan,
1290 line_height,
1291 selection.min(),
1292 selection.max(),
1293 ) {
1294 let sel_rect = cranpose_ui_graphics::Rect {
1295 y: sel_rect.y + box_off,
1296 height: box_h,
1297 ..sel_rect
1298 };
1299 if let Some(clipped) = intersect_rect(sel_rect, clip_bounds) {
1300 primitives.push(DrawPrimitive::Rect {
1301 rect: clipped,
1302 brush: selection_brush.clone(),
1303 stroke: None,
1304 });
1305 }
1306 }
1307 scope.push_recorded(primitives);
1308 }))
1309 }
1310}
1311
1312impl SemanticsNode for TextFieldModifierNode {
1313 fn merge_semantics(&self, config: &mut SemanticsConfiguration) {
1314 merge_text_field_semantics(self.state, self.line_limits, config);
1315 }
1316
1317 fn reach(&self) -> cranpose_foundation::SemanticsReach {
1318 cranpose_foundation::SemanticsReach::default()
1319 }
1320}
1321
1322pub(crate) fn merge_text_field_semantics(
1325 state: TextFieldState,
1326 line_limits: TextFieldLineLimits,
1327 config: &mut SemanticsConfiguration,
1328) {
1329 let text = state.text();
1330 if config.content_description.is_none() {
1331 config.content_description = Some(text.clone());
1332 }
1333 config.text = Some(text);
1334 config.is_editable_text = true;
1335 config.is_clickable = true;
1336 config.multiline = !matches!(line_limits, TextFieldLineLimits::SingleLine);
1337 config.set_text = Some(cranpose_foundation::SemanticsSetText::new(move |text| {
1338 state.set_text(text)
1339 }));
1340 config.set_selection = Some(cranpose_foundation::SemanticsSetSelection::new(
1341 move |anchor, focus| {
1342 let text = state.text();
1343 let anchor = floor_char_boundary(&text, anchor);
1344 let focus = floor_char_boundary(&text, focus);
1345 state.set_selection(TextRange::new(anchor, focus));
1346 crate::cursor_animation::reset_cursor_blink();
1347 crate::request_render_invalidation();
1348 true
1349 },
1350 ));
1351 config.text_selection = Some(state.selection());
1352}
1353
1354fn floor_char_boundary(text: &str, index: usize) -> usize {
1355 let mut index = index.min(text.len());
1356 while !text.is_char_boundary(index) {
1357 index -= 1;
1358 }
1359 index
1360}
1361
1362impl PointerInputNode for TextFieldModifierNode {
1363 fn on_pointer_event(
1364 &mut self,
1365 _context: &mut dyn ModifierNodeContext,
1366 _event: &PointerEvent,
1367 ) -> bool {
1368 false
1369 }
1370
1371 fn hit_test(&self, x: f32, y: f32) -> bool {
1372 let size = self.measured_size.get();
1373 x >= 0.0 && x <= size.width && y >= 0.0 && y <= size.height
1374 }
1375
1376 fn pointer_input_handler(&self) -> Option<Rc<dyn Fn(PointerEvent)>> {
1377 Some(self.cached_handler.clone())
1378 }
1379}
1380
1381#[derive(Clone)]
1388pub struct TextFieldElement {
1389 state: TextFieldState,
1390 style: TextStyle,
1391 cursor_color: Color,
1392 line_limits: TextFieldLineLimits,
1393 handle_controller: Option<TextFieldHandleController>,
1394 modal_depth: usize,
1395 decorator: Option<TextFieldRefs>,
1396}
1397
1398impl TextFieldElement {
1399 pub fn new(state: TextFieldState, style: TextStyle) -> Self {
1401 Self {
1402 state,
1403 style,
1404 cursor_color: DEFAULT_CURSOR_COLOR,
1405 line_limits: TextFieldLineLimits::default(),
1406 handle_controller: None,
1407 modal_depth: 0,
1408 decorator: None,
1409 }
1410 }
1411
1412 pub(crate) fn decorated_by(mut self, refs: TextFieldRefs) -> Self {
1415 self.decorator = Some(refs);
1416 self
1417 }
1418
1419 pub fn with_cursor_color(mut self, color: Color) -> Self {
1421 self.cursor_color = color;
1422 self
1423 }
1424
1425 pub fn with_line_limits(mut self, line_limits: TextFieldLineLimits) -> Self {
1427 self.line_limits = line_limits;
1428 self
1429 }
1430
1431 pub fn with_handle_controller(mut self, controller: TextFieldHandleController) -> Self {
1433 self.handle_controller = Some(controller);
1434 self
1435 }
1436
1437 pub fn with_modal_depth(mut self, depth: usize) -> Self {
1440 self.modal_depth = depth;
1441 self
1442 }
1443}
1444
1445impl std::fmt::Debug for TextFieldElement {
1446 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1447 f.debug_struct("TextFieldElement")
1448 .field("text", &self.state.text())
1449 .field("style", &self.style)
1450 .field("cursor_color", &self.cursor_color)
1451 .finish()
1452 }
1453}
1454
1455impl Hash for TextFieldElement {
1456 fn hash<H: Hasher>(&self, state: &mut H) {
1457 self.state.id().hash(state);
1458 self.cursor_color.0.to_bits().hash(state);
1459 self.cursor_color.1.to_bits().hash(state);
1460 self.cursor_color.2.to_bits().hash(state);
1461 self.cursor_color.3.to_bits().hash(state);
1462 self.style.render_hash().hash(state);
1463 self.line_limits.hash(state);
1464 self.modal_depth.hash(state);
1465 self.decorator.as_ref().map(TextFieldRefs::key).hash(state);
1466 }
1467}
1468
1469impl PartialEq for TextFieldElement {
1470 fn eq(&self, other: &Self) -> bool {
1471 self.state == other.state
1472 && self.style == other.style
1473 && self.cursor_color == other.cursor_color
1474 && self.line_limits == other.line_limits
1475 && self.modal_depth == other.modal_depth
1476 && self.decorator == other.decorator
1477 }
1478}
1479
1480impl Eq for TextFieldElement {}
1481
1482impl ModifierNodeElement for TextFieldElement {
1483 type Node = TextFieldModifierNode;
1484
1485 fn create(&self) -> Self::Node {
1486 let refs = self.decorator.clone().unwrap_or_else(TextFieldRefs::new);
1487 let decorated = self.decorator.is_some();
1488 let mut node =
1489 TextFieldModifierNode::with_refs(self.state, self.style.clone(), refs, decorated)
1490 .with_cursor_color(self.cursor_color)
1491 .with_line_limits(self.line_limits);
1492 node.modal_depth = self.modal_depth;
1493 node.refs.modal_depth.set(self.modal_depth);
1494 if let Some(controller) = self.handle_controller.clone() {
1495 node = node.with_handle_controller(controller);
1496 }
1497 node.rebuild_cached_closures();
1498 node
1499 }
1500
1501 fn update(&self, node: &mut Self::Node) {
1502 node.state = self.state;
1503 if node.style != self.style {
1504 node.min_size.set(None);
1505 node.style = self.style.clone();
1506 }
1507 node.cursor_brush = Brush::solid(self.cursor_color);
1508 node.line_limits = self.line_limits;
1509 node.handle_controller.clone_from(&self.handle_controller);
1510 node.modal_depth = self.modal_depth;
1511 node.refs.modal_depth.set(self.modal_depth);
1512 node.rebuild_cached_closures();
1513
1514 if node.update_cached_state() {}
1515 }
1516
1517 fn key(&self) -> Option<u64> {
1520 self.decorator.as_ref().map(TextFieldRefs::key)
1521 }
1522
1523 fn capabilities(&self) -> NodeCapabilities {
1524 let drawn = NodeCapabilities::LAYOUT | NodeCapabilities::DRAW;
1525 if self.decorator.is_some() {
1526 drawn
1527 } else {
1528 drawn | NodeCapabilities::SEMANTICS | NodeCapabilities::POINTER_INPUT
1529 }
1530 }
1531
1532 fn always_update(&self) -> bool {
1533 true
1534 }
1535}
1536
1537#[cfg(test)]
1538#[path = "tests/text_field_modifier_node_tests.rs"]
1539mod tests;