1use std::{
7 cell::{Cell, RefCell},
8 rc::{Rc, Weak},
9};
10
11use cranpose_core::{MutableState, NodeId, SideEffect, mutableStateOf, remember};
12use cranpose_foundation::{
13 modifier_element,
14 text::{TextFieldLineLimits, TextFieldState, TextRange},
15};
16use cranpose_ui_graphics::{Color, Point, Rect};
17
18use crate::{
19 bring_into_view::local_bring_into_view_responder,
20 clipboard_session::{clipboard_can_paste, clipboard_paste_into_focus, clipboard_write_text},
21 composable,
22 layout::{
23 core::Alignment,
24 policies::{BoxMeasurePolicy, EmptyMeasurePolicy},
25 },
26 modifier::Modifier,
27 safe_area::local_ime_insets,
28 text::{AnnotatedString, TextStyle, measure_text},
29 text_field_decorator_node::TextFieldDecoratorElement,
30 text_field_focus::{dispatch_copy, dispatch_cut, dispatch_select_all},
31 text_field_modifier_node::{
32 TextFieldElement, TextFieldHandleController, TextFieldHandleMetrics, TextFieldRefs,
33 },
34 text_selection::{
35 HANDLE_RADIUS, HandleGrabOffset, HandleKind, LineAffinity, selection_after_handle_drag,
36 },
37 widgets::{
38 CaretActionMenu, Layout, MenuAnchor, SelectionHandle, SelectionLoupe, TextSelectionMenu,
39 loupe_target_for_drag,
40 },
41};
42
43pub const SELECTION_HIGHLIGHT_ALPHA: f32 = 0.32;
48
49const TEXT_LONG_PRESS_MS: u64 = 500;
52const TEXT_LONG_PRESS_SLOP: f32 = 12.0;
55
56struct LongPressWatcher {
63 controller: TextFieldHandleController,
64 state: TextFieldState,
65 style: TextStyle,
66 start: Point,
67 start_nanos: Cell<Option<u64>>,
68 registration: RefCell<Option<cranpose_core::internal::FrameCallbackRegistration>>,
69 frame_clock: cranpose_core::internal::FrameClock,
70}
71
72impl LongPressWatcher {
73 fn arm(self: &Rc<Self>) {
74 let weak: Weak<LongPressWatcher> = Rc::downgrade(self);
75 let registration = self.frame_clock.with_frame_nanos(move |now| {
76 let Some(watcher) = weak.upgrade() else {
77 return;
78 };
79 watcher.tick(now);
80 });
81 *self.registration.borrow_mut() = Some(registration);
82 crate::request_render_invalidation();
83 }
84
85 fn tick(self: Rc<Self>, now: u64) {
86 self.registration.borrow_mut().take();
87 let Some(press) = self.controller.press() else {
88 return;
89 };
90 let moved = (press.position.x - self.start.x)
91 .abs()
92 .max((press.position.y - self.start.y).abs());
93 if (press.start.x - self.start.x).abs() > 0.5
94 || (press.start.y - self.start.y).abs() > 0.5
95 || moved > TEXT_LONG_PRESS_SLOP
96 {
97 return;
98 }
99 let start = match self.start_nanos.get() {
100 Some(value) => value,
101 None => {
102 self.start_nanos.set(Some(now));
103 now
104 }
105 };
106 if now.saturating_sub(start) < TEXT_LONG_PRESS_MS * 1_000_000 {
107 self.arm();
108 return;
109 }
110 self.controller.claim_gesture();
111 let Some(metrics) = self.controller.metrics() else {
112 return;
113 };
114 let text = self.state.text();
115 let offset = window_pos_to_offset(&text, &self.style, &metrics, self.start, 0.0);
116 let (word_start, word_end) = crate::word_boundaries::find_word_boundaries(&text, offset);
117 self.state.edit(|buffer| {
118 buffer.select(TextRange::new(word_start, word_end));
119 });
120 crate::request_render_invalidation();
121 }
122}
123
124fn handle_tip_local_pos(
130 text: &str,
131 style: &TextStyle,
132 metrics: &TextFieldHandleMetrics,
133 offset: usize,
134 affinity: LineAffinity,
135) -> Point {
136 let offset = offset.min(text.len());
137 let (line_index, line_start) = crate::text_field_modifier_node::caret_visual_line_for_offset(
138 text,
139 style,
140 None,
141 metrics.wrap_width,
142 offset,
143 affinity,
144 );
145 let caret_x = measure_text(&AnnotatedString::from(&text[line_start..offset]), style).width;
146 Point {
147 x: metrics.padding_left + caret_x - metrics.scroll_offset,
148 y: metrics.padding_top
149 + line_index as f32 * metrics.line_height
150 + metrics.glyph_box.0
151 + metrics.glyph_box.1,
152 }
153}
154
155fn caret_window_geometry(
156 text: &str,
157 style: &TextStyle,
158 metrics: &TextFieldHandleMetrics,
159 offset: usize,
160 affinity: LineAffinity,
161) -> (Point, Rect) {
162 let tip = handle_tip_local_pos(text, style, metrics, offset, affinity);
163 let bounds = metrics.local_to_window.bounds_for_rect(Rect {
164 x: tip.x,
165 y: tip.y - metrics.glyph_box.1,
166 width: 2.0,
167 height: metrics.glyph_box.1,
168 });
169 (metrics.local_to_window.map_point(tip), bounds)
170}
171
172fn window_pos_to_offset(
179 text: &str,
180 style: &TextStyle,
181 metrics: &TextFieldHandleMetrics,
182 window_pos: Point,
183 y_bias: f32,
184) -> usize {
185 let Some(inverse) = metrics.local_to_window.inverse() else {
186 return 0;
187 };
188 let local = inverse.map_point(Point {
189 y: window_pos.y + y_bias,
190 ..window_pos
191 });
192 let local_x = (local.x - metrics.padding_left + metrics.scroll_offset).max(0.0);
193 let local_y = (local.y - 0.5 * metrics.line_height - metrics.padding_top).max(0.0);
194 crate::text::offset_for_position_wrapped(
195 text,
196 style,
197 None,
198 metrics.wrap_width,
199 metrics.line_height,
200 local_x,
201 local_y,
202 )
203}
204#[composable]
221pub fn BasicTextField(state: TextFieldState, modifier: Modifier, style: TextStyle) -> NodeId {
222 BasicTextFieldWithOptions(
223 state,
224 modifier,
225 BasicTextFieldOptions {
226 text_style: style,
227 ..BasicTextFieldOptions::default()
228 },
229 )
230}
231
232#[derive(Debug, Clone, PartialEq)]
234pub struct BasicTextFieldOptions {
235 pub text_style: TextStyle,
237 pub cursor_color: Color,
239 pub line_limits: TextFieldLineLimits,
241 pub show_keyboard_on_focus: bool,
248}
249
250impl Default for BasicTextFieldOptions {
251 fn default() -> Self {
252 Self {
253 text_style: TextStyle::default(),
254 cursor_color: Color(0.0, 0.478, 1.0, 1.0),
255 line_limits: TextFieldLineLimits::default(),
256 show_keyboard_on_focus: true,
257 }
258 }
259}
260
261#[derive(Clone)]
266pub struct BasicTextFieldDecorationScope {
267 inner: Rc<dyn Fn() -> NodeId>,
268}
269
270impl BasicTextFieldDecorationScope {
271 pub fn inner_text_field(&self) -> NodeId {
272 (self.inner)()
273 }
274}
275
276impl PartialEq for BasicTextFieldDecorationScope {
277 fn eq(&self, other: &Self) -> bool {
278 Rc::ptr_eq(&self.inner, &other.inner)
279 }
280}
281
282#[composable(no_skip)]
291pub fn BasicTextFieldDecorated<D>(
292 state: TextFieldState,
293 modifier: Modifier,
294 options: BasicTextFieldOptions,
295 decoration_box: D,
296) -> NodeId
297where
298 D: Fn(BasicTextFieldDecorationScope) + 'static,
299{
300 let refs = remember(TextFieldRefs::new).with(TextFieldRefs::clone);
301 let decorator = TextFieldDecoratorElement::new(
302 state,
303 refs.clone(),
304 options.text_style.clone(),
305 options.line_limits,
306 crate::modal::local_modal_depth().current(),
307 );
308 let scope = BasicTextFieldDecorationScope {
309 inner: Rc::new(move || {
310 TextFieldNode(
311 state,
312 Modifier::empty(),
313 options.clone(),
314 Some(refs.clone()),
315 )
316 }),
317 };
318 Layout(
319 modifier.then(Modifier::from_parts(&[modifier_element(decorator)])),
320 BoxMeasurePolicy::new(Alignment::TOP_START, true),
321 move || decoration_box(scope.clone()),
322 )
323}
324
325#[composable]
329pub fn BasicTextFieldWithOptions(
330 state: TextFieldState,
331 modifier: Modifier,
332 options: BasicTextFieldOptions,
333) -> NodeId {
334 TextFieldNode(state, modifier, options, None)
335}
336
337#[composable]
341fn TextFieldNode(
342 state: TextFieldState,
343 modifier: Modifier,
344 options: BasicTextFieldOptions,
345 decorator: Option<TextFieldRefs>,
346) -> NodeId {
347 let _text = state.text();
348 let _selection = state.selection();
349
350 let controller =
351 remember(TextFieldHandleController::new).with(TextFieldHandleController::clone);
352
353 let modal_depth = crate::modal::local_modal_depth().current();
354 let mut text_field_element = TextFieldElement::new(state, options.text_style.clone())
355 .with_cursor_color(options.cursor_color)
356 .with_line_limits(options.line_limits)
357 .with_show_keyboard_on_focus(options.show_keyboard_on_focus)
358 .with_handle_controller(controller.clone())
359 .with_modal_depth(modal_depth);
360 if let Some(refs) = decorator {
361 text_field_element = text_field_element.decorated_by(refs);
362 }
363
364 let text_field_modifier = modifier_element(text_field_element);
365 let final_modifier = Modifier::from_parts(&[text_field_modifier]);
366 let combined_modifier = modifier.then(final_modifier);
367
368 let node = Layout(combined_modifier, EmptyMeasurePolicy, || {});
369
370 BringCaretIntoView(state, options.text_style.clone(), controller.clone());
371
372 SelectionHandles(state, options.text_style, controller, options.cursor_color);
373
374 node
375}
376
377#[cfg(test)]
378#[path = "tests/basic_text_field_options_tests.rs"]
379mod options_tests;
380
381fn caret_window_rect(
385 text: &str,
386 style: &TextStyle,
387 metrics: &TextFieldHandleMetrics,
388 offset: usize,
389) -> Rect {
390 caret_window_geometry(text, style, metrics, offset, LineAffinity::Upstream).1
391}
392
393#[composable]
403fn BringCaretIntoView(
404 state: TextFieldState,
405 style: TextStyle,
406 controller: TextFieldHandleController,
407) {
408 let Some(metrics) = controller.metrics() else {
409 return;
410 };
411 let ime_bottom = local_ime_insets().current().bottom;
412 let responder = local_bring_into_view_responder().current();
413
414 let previous: Rc<Cell<Option<(usize, usize, i64)>>> =
415 remember(|| Rc::new(Cell::new(None))).with(Rc::clone);
416
417 if !metrics.focused {
418 previous.set(None);
419 return;
420 }
421 let Some(responder) = responder else {
422 return;
423 };
424
425 let text = state.text();
426 let selection = state.selection();
427
428 let key = (
429 selection.start,
430 selection.end,
431 (ime_bottom * 4.0).round() as i64,
432 );
433 SideEffect(move || {
434 if previous.get() == Some(key) {
435 return;
436 }
437 previous.set(Some(key));
438 let Some(metrics) = controller.metrics_now() else {
439 return;
440 };
441 let caret = caret_window_rect(&text, &style, &metrics, selection.start);
442 responder.bring_into_view(caret, ime_bottom);
443 });
444}
445
446#[composable]
451fn SelectionHandles(
452 state: TextFieldState,
453 style: TextStyle,
454 controller: TextFieldHandleController,
455 accent: Color,
456) {
457 let selection = state.selection();
458 let current_range = (selection.min(), selection.max());
459 let active_press = controller.press();
460
461 let menu_open = remember(|| mutableStateOf(true)).with(|state| *state);
462 let caret_menu_open = remember(|| mutableStateOf(false)).with(|state| *state);
463 let caret_menu_offset: Rc<Cell<usize>> =
464 remember(|| Rc::new(Cell::new(0usize))).with(Rc::clone);
465 let previous_range: Rc<Cell<(usize, usize)>> =
466 remember(|| Rc::new(Cell::new(current_range))).with(Rc::clone);
467 {
468 let previous_range = Rc::clone(&previous_range);
469 SideEffect(move || {
470 if previous_range.get() != current_range {
471 previous_range.set(current_range);
472 menu_open.set(true);
473 }
474 });
475 }
476 {
477 let caret_menu_offset = Rc::clone(&caret_menu_offset);
478 let caret_start = selection.start;
479 SideEffect(move || {
480 if caret_menu_open.value()
481 && (!selection.collapsed() || caret_start != caret_menu_offset.get())
482 {
483 caret_menu_open.set(false);
484 }
485 });
486 }
487
488 let Some(metrics) = controller.metrics() else {
489 return;
490 };
491 if !metrics.focused || !metrics.direct_manipulation {
492 return;
493 }
494 let Some(metrics) = controller.live_metrics() else {
497 return;
498 };
499
500 let text = state.text();
501
502 let press_watcher: Rc<Cell<Option<(u32, u32)>>> =
503 remember(|| Rc::new(Cell::new(None))).with(Rc::clone);
504 let press_watcher_ref: Rc<RefCell<Option<Rc<LongPressWatcher>>>> =
505 remember(|| Rc::new(RefCell::new(None))).with(Rc::clone);
506 match active_press {
507 Some(press) => {
508 let key = (press.start.x.to_bits(), press.start.y.to_bits());
509 if press_watcher.get() != Some(key) {
510 press_watcher.set(Some(key));
511 let watcher = Rc::new(LongPressWatcher {
512 controller: controller.clone(),
513 state,
514 style: style.clone(),
515 start: press.start,
516 start_nanos: Cell::new(None),
517 registration: RefCell::new(None),
518 frame_clock: cranpose_core::with_current_composer(|composer| {
519 composer.runtime_handle()
520 })
521 .frame_clock(),
522 });
523 watcher.arm();
524 *press_watcher_ref.borrow_mut() = Some(watcher);
525 }
526 }
527 None => {
528 press_watcher.set(None);
529 press_watcher_ref.borrow_mut().take();
530 }
531 }
532
533 let drag_pos: MutableState<Option<Point>> =
534 remember(|| mutableStateOf(None::<Point>)).with(|state| *state);
535 let drag_bias: Rc<Cell<Option<HandleGrabOffset>>> =
536 remember(|| Rc::new(Cell::new(None))).with(Rc::clone);
537 let last_dragged: Rc<Cell<Option<HandleKind>>> =
538 remember(|| Rc::new(Cell::new(None))).with(Rc::clone);
539 let menu_anchor_range: Rc<Cell<(usize, usize)>> =
540 remember(|| Rc::new(Cell::new(current_range))).with(Rc::clone);
541 {
542 let last_dragged = Rc::clone(&last_dragged);
543 let menu_anchor_range = Rc::clone(&menu_anchor_range);
544 SideEffect(move || {
545 if menu_anchor_range.get() != current_range {
546 menu_anchor_range.set(current_range);
547 if drag_pos.value().is_none() {
548 last_dragged.set(None);
549 }
550 }
551 });
552 }
553 let cursor_tip_y: Rc<Cell<f32>> = remember(|| Rc::new(Cell::new(0.0f32))).with(Rc::clone);
554 let start_tip_y: Rc<Cell<f32>> = remember(|| Rc::new(Cell::new(0.0f32))).with(Rc::clone);
555 let end_tip_y: Rc<Cell<f32>> = remember(|| Rc::new(Cell::new(0.0f32))).with(Rc::clone);
556
557 if selection.collapsed() {
558 let (tip, caret_rect) = caret_window_geometry(
559 &text,
560 &style,
561 &metrics,
562 selection.start,
563 LineAffinity::Upstream,
564 );
565 let on_drag = drag_caret_closure(state, style.clone(), controller, Rc::clone(&drag_bias));
566 let open_caret_menu = {
567 let caret_menu_offset = Rc::clone(&caret_menu_offset);
568 move || {
569 caret_menu_offset.set(state.selection().start);
570 caret_menu_open.set(true);
571 }
572 };
573 let on_tap = open_caret_menu.clone();
574 let on_long_press = open_caret_menu;
575 let grab_bias = Rc::clone(&drag_bias);
576 let end_bias = Rc::clone(&drag_bias);
577 cursor_tip_y.set(tip.y);
578 let tip_y = Rc::clone(&cursor_tip_y);
579 SelectionHandle(
580 HandleKind::Cursor,
581 tip,
582 caret_rect.height,
583 HANDLE_RADIUS,
584 accent,
585 move |pos| {
586 track_handle_grab(&grab_bias, HandleKind::Cursor, tip_y.get(), pos.y);
587 drag_pos.set(Some(pos));
588 on_drag(pos);
589 },
590 move || {
591 drag_pos.set(None);
592 end_bias.set(None);
593 crate::cursor_animation::reset_cursor_blink();
594 },
595 on_long_press,
596 on_tap,
597 );
598
599 if caret_menu_open.value() {
600 let can_paste = clipboard_can_paste();
601 let can_undo = state.can_undo();
602 let can_redo = state.can_redo();
603 let undo_state = state;
604 let redo_state = state;
605 CaretActionMenu(
606 MenuAnchor {
607 center_x: tip.x,
608 line_top: caret_rect.y,
609 line_bottom: caret_rect.y + caret_rect.height,
610 },
611 drag_pos.value().is_none(),
612 can_paste,
613 can_undo,
614 can_redo,
615 move || {
616 clipboard_paste_into_focus();
617 caret_menu_open.set(false);
618 },
619 move || {
620 dispatch_select_all();
621 caret_menu_open.set(false);
622 },
623 move || {
624 undo_state.undo();
625 crate::request_render_invalidation();
626 caret_menu_open.set(false);
627 },
628 move || {
629 redo_state.redo();
630 crate::request_render_invalidation();
631 caret_menu_open.set(false);
632 },
633 );
634 }
635 } else {
636 let start = selection.min();
637 let end = selection.max();
638 let (start_tip, start_rect) =
639 caret_window_geometry(&text, &style, &metrics, start, LineAffinity::Downstream);
640 let (end_tip, end_rect) =
641 caret_window_geometry(&text, &style, &metrics, end, LineAffinity::Upstream);
642
643 let last_dragged_start = Rc::clone(&last_dragged);
644 let last_dragged_end = Rc::clone(&last_dragged);
645 let on_drag_start = drag_edge_closure(
646 HandleKind::SelectionStart,
647 state,
648 style.clone(),
649 controller.clone(),
650 Rc::clone(&drag_bias),
651 );
652 let grab_bias = Rc::clone(&drag_bias);
653 let end_bias = Rc::clone(&drag_bias);
654 start_tip_y.set(start_tip.y);
655 let start_tip_live = Rc::clone(&start_tip_y);
656 SelectionHandle(
657 HandleKind::SelectionStart,
658 start_tip,
659 start_rect.height,
660 HANDLE_RADIUS,
661 accent,
662 move |pos| {
663 track_handle_grab(
664 &grab_bias,
665 HandleKind::SelectionStart,
666 start_tip_live.get(),
667 pos.y,
668 );
669 last_dragged_start.set(Some(HandleKind::SelectionStart));
670 drag_pos.set(Some(pos));
671 on_drag_start(pos);
672 },
673 move || {
674 drag_pos.set(None);
675 end_bias.set(None);
676 },
677 move || menu_open.set(true),
678 move || menu_open.set(true),
679 );
680
681 let on_drag_end = drag_edge_closure(
682 HandleKind::SelectionEnd,
683 state,
684 style.clone(),
685 controller.clone(),
686 Rc::clone(&drag_bias),
687 );
688 let grab_bias = Rc::clone(&drag_bias);
689 let end_bias = Rc::clone(&drag_bias);
690 end_tip_y.set(end_tip.y);
691 let end_tip_live = Rc::clone(&end_tip_y);
692 SelectionHandle(
693 HandleKind::SelectionEnd,
694 end_tip,
695 end_rect.height,
696 HANDLE_RADIUS,
697 accent,
698 move |pos| {
699 track_handle_grab(
700 &grab_bias,
701 HandleKind::SelectionEnd,
702 end_tip_live.get(),
703 pos.y,
704 );
705 last_dragged_end.set(Some(HandleKind::SelectionEnd));
706 drag_pos.set(Some(pos));
707 on_drag_end(pos);
708 },
709 move || {
710 drag_pos.set(None);
711 end_bias.set(None);
712 },
713 move || menu_open.set(true),
714 move || menu_open.set(true),
715 );
716
717 if menu_open.value() {
718 let can_paste = clipboard_can_paste();
719 let slide_point = if controller.gesture_claimed() {
720 active_press.map(|press| press.position)
721 } else {
722 None
723 };
724 let (center_x, caret_rect) = match last_dragged.get() {
725 Some(HandleKind::SelectionStart) => (start_tip.x, start_rect),
726 Some(HandleKind::SelectionEnd | HandleKind::Cursor) => (end_tip.x, end_rect),
727 None => ((start_tip.x + end_tip.x) * 0.5, start_rect),
728 };
729 TextSelectionMenu(
730 MenuAnchor {
731 center_x,
732 line_top: caret_rect.y,
733 line_bottom: caret_rect.y + caret_rect.height,
734 },
735 drag_pos.value().is_none(),
736 slide_point,
737 can_paste,
738 move || {
739 if let Some(text) = dispatch_copy() {
740 clipboard_write_text(&text);
741 }
742 menu_open.set(false);
743 },
744 move || {
745 if let Some(text) = dispatch_cut() {
746 clipboard_write_text(&text);
747 }
748 menu_open.set(false);
749 },
750 move || {
751 clipboard_paste_into_focus();
752 menu_open.set(false);
753 },
754 move || {
755 dispatch_select_all();
756 menu_open.set(false);
757 },
758 );
759 }
760 }
761
762 let loupe_target = drag_pos.value().and_then(|finger| {
763 let bias = drag_bias.get().map_or(0.0, |grab| grab.bias());
764 let offset = window_pos_to_offset(&text, &style, &metrics, finger, bias);
765 let (_, caret_rect) =
766 caret_window_geometry(&text, &style, &metrics, offset, LineAffinity::Upstream);
767 loupe_target_for_drag(finger, caret_rect.y + caret_rect.height, caret_rect.height)
768 });
769 SelectionLoupe(loupe_target);
770}
771
772fn track_handle_grab(
773 drag_bias: &Cell<Option<HandleGrabOffset>>,
774 kind: HandleKind,
775 handle_tip_y: f32,
776 finger_y: f32,
777) -> f32 {
778 let drifts = kind != HandleKind::SelectionStart;
779 let mut grab = drag_bias
780 .get()
781 .unwrap_or_else(|| HandleGrabOffset::begin_for(handle_tip_y, finger_y, drifts));
782 let bias = grab.track(finger_y);
783 drag_bias.set(Some(grab));
784 bias
785}
786
787fn drag_caret_closure(
791 state: TextFieldState,
792 style: TextStyle,
793 controller: TextFieldHandleController,
794 drag_bias: Rc<Cell<Option<HandleGrabOffset>>>,
795) -> Rc<dyn Fn(Point)> {
796 Rc::new(move |window_pos: Point| {
797 let Some(metrics) = controller.metrics() else {
798 return;
799 };
800 let text = state.text();
801 let bias = drag_bias.get().map_or(0.0, |grab| grab.bias());
802 let offset = window_pos_to_offset(&text, &style, &metrics, window_pos, bias);
803 state.set_selection(TextRange::new(offset, offset));
804 crate::cursor_animation::suspend_cursor_blink();
805 crate::request_render_invalidation();
806 })
807}
808
809fn drag_edge_closure(
813 dragged: HandleKind,
814 state: TextFieldState,
815 style: TextStyle,
816 controller: TextFieldHandleController,
817 drag_bias: Rc<Cell<Option<HandleGrabOffset>>>,
818) -> Rc<dyn Fn(Point)> {
819 Rc::new(move |window_pos: Point| {
820 let Some(metrics) = controller.metrics() else {
821 return;
822 };
823 let text = state.text();
824 let bias = drag_bias.get().map_or(0.0, |grab| grab.bias());
825 let dragged_offset = window_pos_to_offset(&text, &style, &metrics, window_pos, bias);
826 let selection = state.selection();
827 let fixed_edge = match dragged {
828 HandleKind::SelectionStart => selection.max(),
829 _ => selection.min(),
830 };
831 let (min, max) =
832 selection_after_handle_drag(dragged, fixed_edge, dragged_offset, text.len());
833 state.set_selection(TextRange::new(min, max));
834 crate::request_render_invalidation();
835 })
836}
837
838#[cfg(test)]
839#[path = "tests/basic_text_field_tests.rs"]
840mod tests;