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