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 #[cfg(feature = "localization")]
348 let options = BasicTextFieldOptions {
349 text_style: crate::localization::apply_text_locale(options.text_style),
350 ..options
351 };
352 let _text = state.text();
353 let _selection = state.selection();
354
355 let controller =
356 remember(TextFieldHandleController::new).with(TextFieldHandleController::clone);
357
358 let modal_depth = crate::modal::local_modal_depth().current();
359 let mut text_field_element = TextFieldElement::new(state, options.text_style.clone())
360 .with_cursor_color(options.cursor_color)
361 .with_line_limits(options.line_limits)
362 .with_show_keyboard_on_focus(options.show_keyboard_on_focus)
363 .with_handle_controller(controller.clone())
364 .with_modal_depth(modal_depth);
365 if let Some(refs) = decorator {
366 text_field_element = text_field_element.decorated_by(refs);
367 }
368
369 let text_field_modifier = modifier_element(text_field_element);
370 let final_modifier = Modifier::from_parts(&[text_field_modifier]);
371 let combined_modifier = modifier.then(final_modifier);
372
373 let node = Layout(combined_modifier, EmptyMeasurePolicy, || {});
374
375 BringCaretIntoView(state, options.text_style.clone(), controller.clone());
376
377 SelectionHandles(state, options.text_style, controller, options.cursor_color);
378
379 node
380}
381
382#[cfg(test)]
383#[path = "tests/basic_text_field_options_tests.rs"]
384mod options_tests;
385
386fn caret_window_rect(
390 text: &str,
391 style: &TextStyle,
392 metrics: &TextFieldHandleMetrics,
393 offset: usize,
394) -> Rect {
395 caret_window_geometry(text, style, metrics, offset, LineAffinity::Upstream).1
396}
397
398#[composable]
408fn BringCaretIntoView(
409 state: TextFieldState,
410 style: TextStyle,
411 controller: TextFieldHandleController,
412) {
413 let Some(metrics) = controller.metrics() else {
414 return;
415 };
416 let ime_bottom = local_ime_insets().current().bottom;
417 let responder = local_bring_into_view_responder().current();
418
419 let previous: Rc<Cell<Option<(usize, usize, i64)>>> =
420 remember(|| Rc::new(Cell::new(None))).with(Rc::clone);
421
422 if !metrics.focused {
423 previous.set(None);
424 return;
425 }
426 let Some(responder) = responder else {
427 return;
428 };
429
430 let text = state.text();
431 let selection = state.selection();
432
433 let key = (
434 selection.start,
435 selection.end,
436 (ime_bottom * 4.0).round() as i64,
437 );
438 SideEffect(move || {
439 if previous.get() == Some(key) {
440 return;
441 }
442 previous.set(Some(key));
443 let Some(metrics) = controller.metrics_now() else {
444 return;
445 };
446 let caret = caret_window_rect(&text, &style, &metrics, selection.start);
447 responder.bring_into_view(caret, ime_bottom);
448 });
449}
450
451#[composable]
456fn SelectionHandles(
457 state: TextFieldState,
458 style: TextStyle,
459 controller: TextFieldHandleController,
460 accent: Color,
461) {
462 let selection = state.selection();
463 let current_range = (selection.min(), selection.max());
464 let active_press = controller.press();
465
466 let menu_open = remember(|| mutableStateOf(true)).with(|state| *state);
467 let caret_menu_open = remember(|| mutableStateOf(false)).with(|state| *state);
468 let caret_menu_offset: Rc<Cell<usize>> =
469 remember(|| Rc::new(Cell::new(0usize))).with(Rc::clone);
470 let previous_range: Rc<Cell<(usize, usize)>> =
471 remember(|| Rc::new(Cell::new(current_range))).with(Rc::clone);
472 {
473 let previous_range = Rc::clone(&previous_range);
474 SideEffect(move || {
475 if previous_range.get() != current_range {
476 previous_range.set(current_range);
477 menu_open.set(true);
478 }
479 });
480 }
481 {
482 let caret_menu_offset = Rc::clone(&caret_menu_offset);
483 let caret_start = selection.start;
484 SideEffect(move || {
485 if caret_menu_open.value()
486 && (!selection.collapsed() || caret_start != caret_menu_offset.get())
487 {
488 caret_menu_open.set(false);
489 }
490 });
491 }
492
493 let Some(metrics) = controller.metrics() else {
494 return;
495 };
496 if !metrics.focused || !metrics.direct_manipulation {
497 return;
498 }
499 let Some(metrics) = controller.live_metrics() else {
502 return;
503 };
504
505 let text = state.text();
506
507 let press_watcher: Rc<Cell<Option<(u32, u32)>>> =
508 remember(|| Rc::new(Cell::new(None))).with(Rc::clone);
509 let press_watcher_ref: Rc<RefCell<Option<Rc<LongPressWatcher>>>> =
510 remember(|| Rc::new(RefCell::new(None))).with(Rc::clone);
511 match active_press {
512 Some(press) => {
513 let key = (press.start.x.to_bits(), press.start.y.to_bits());
514 if press_watcher.get() != Some(key) {
515 press_watcher.set(Some(key));
516 let watcher = Rc::new(LongPressWatcher {
517 controller: controller.clone(),
518 state,
519 style: style.clone(),
520 start: press.start,
521 start_nanos: Cell::new(None),
522 registration: RefCell::new(None),
523 frame_clock: cranpose_core::with_current_composer(|composer| {
524 composer.runtime_handle()
525 })
526 .frame_clock(),
527 });
528 watcher.arm();
529 *press_watcher_ref.borrow_mut() = Some(watcher);
530 }
531 }
532 None => {
533 press_watcher.set(None);
534 press_watcher_ref.borrow_mut().take();
535 }
536 }
537
538 let drag_pos: MutableState<Option<Point>> =
539 remember(|| mutableStateOf(None::<Point>)).with(|state| *state);
540 let drag_bias: Rc<Cell<Option<HandleGrabOffset>>> =
541 remember(|| Rc::new(Cell::new(None))).with(Rc::clone);
542 let last_dragged: Rc<Cell<Option<HandleKind>>> =
543 remember(|| Rc::new(Cell::new(None))).with(Rc::clone);
544 let menu_anchor_range: Rc<Cell<(usize, usize)>> =
545 remember(|| Rc::new(Cell::new(current_range))).with(Rc::clone);
546 {
547 let last_dragged = Rc::clone(&last_dragged);
548 let menu_anchor_range = Rc::clone(&menu_anchor_range);
549 SideEffect(move || {
550 if menu_anchor_range.get() != current_range {
551 menu_anchor_range.set(current_range);
552 if drag_pos.value().is_none() {
553 last_dragged.set(None);
554 }
555 }
556 });
557 }
558 let cursor_tip_y: Rc<Cell<f32>> = remember(|| Rc::new(Cell::new(0.0f32))).with(Rc::clone);
559 let start_tip_y: Rc<Cell<f32>> = remember(|| Rc::new(Cell::new(0.0f32))).with(Rc::clone);
560 let end_tip_y: Rc<Cell<f32>> = remember(|| Rc::new(Cell::new(0.0f32))).with(Rc::clone);
561
562 if selection.collapsed() {
563 let (tip, caret_rect) = caret_window_geometry(
564 &text,
565 &style,
566 &metrics,
567 selection.start,
568 LineAffinity::Upstream,
569 );
570 let on_drag = drag_caret_closure(state, style.clone(), controller, Rc::clone(&drag_bias));
571 let open_caret_menu = {
572 let caret_menu_offset = Rc::clone(&caret_menu_offset);
573 move || {
574 caret_menu_offset.set(state.selection().start);
575 caret_menu_open.set(true);
576 }
577 };
578 let on_tap = open_caret_menu.clone();
579 let on_long_press = open_caret_menu;
580 let grab_bias = Rc::clone(&drag_bias);
581 let end_bias = Rc::clone(&drag_bias);
582 cursor_tip_y.set(tip.y);
583 let tip_y = Rc::clone(&cursor_tip_y);
584 SelectionHandle(
585 HandleKind::Cursor,
586 tip,
587 caret_rect.height,
588 HANDLE_RADIUS,
589 accent,
590 move |pos| {
591 track_handle_grab(&grab_bias, HandleKind::Cursor, tip_y.get(), pos.y);
592 drag_pos.set(Some(pos));
593 on_drag(pos);
594 },
595 move || {
596 drag_pos.set(None);
597 end_bias.set(None);
598 crate::cursor_animation::reset_cursor_blink();
599 },
600 on_long_press,
601 on_tap,
602 );
603
604 if caret_menu_open.value() {
605 let can_paste = clipboard_can_paste();
606 let can_undo = state.can_undo();
607 let can_redo = state.can_redo();
608 let undo_state = state;
609 let redo_state = state;
610 CaretActionMenu(
611 MenuAnchor {
612 center_x: tip.x,
613 line_top: caret_rect.y,
614 line_bottom: caret_rect.y + caret_rect.height,
615 },
616 drag_pos.value().is_none(),
617 can_paste,
618 can_undo,
619 can_redo,
620 move || {
621 clipboard_paste_into_focus();
622 caret_menu_open.set(false);
623 },
624 move || {
625 dispatch_select_all();
626 caret_menu_open.set(false);
627 },
628 move || {
629 undo_state.undo();
630 crate::request_render_invalidation();
631 caret_menu_open.set(false);
632 },
633 move || {
634 redo_state.redo();
635 crate::request_render_invalidation();
636 caret_menu_open.set(false);
637 },
638 );
639 }
640 } else {
641 let start = selection.min();
642 let end = selection.max();
643 let (start_tip, start_rect) =
644 caret_window_geometry(&text, &style, &metrics, start, LineAffinity::Downstream);
645 let (end_tip, end_rect) =
646 caret_window_geometry(&text, &style, &metrics, end, LineAffinity::Upstream);
647
648 let last_dragged_start = Rc::clone(&last_dragged);
649 let last_dragged_end = Rc::clone(&last_dragged);
650 let on_drag_start = drag_edge_closure(
651 HandleKind::SelectionStart,
652 state,
653 style.clone(),
654 controller.clone(),
655 Rc::clone(&drag_bias),
656 );
657 let grab_bias = Rc::clone(&drag_bias);
658 let end_bias = Rc::clone(&drag_bias);
659 start_tip_y.set(start_tip.y);
660 let start_tip_live = Rc::clone(&start_tip_y);
661 SelectionHandle(
662 HandleKind::SelectionStart,
663 start_tip,
664 start_rect.height,
665 HANDLE_RADIUS,
666 accent,
667 move |pos| {
668 track_handle_grab(
669 &grab_bias,
670 HandleKind::SelectionStart,
671 start_tip_live.get(),
672 pos.y,
673 );
674 last_dragged_start.set(Some(HandleKind::SelectionStart));
675 drag_pos.set(Some(pos));
676 on_drag_start(pos);
677 },
678 move || {
679 drag_pos.set(None);
680 end_bias.set(None);
681 },
682 move || menu_open.set(true),
683 move || menu_open.set(true),
684 );
685
686 let on_drag_end = drag_edge_closure(
687 HandleKind::SelectionEnd,
688 state,
689 style.clone(),
690 controller.clone(),
691 Rc::clone(&drag_bias),
692 );
693 let grab_bias = Rc::clone(&drag_bias);
694 let end_bias = Rc::clone(&drag_bias);
695 end_tip_y.set(end_tip.y);
696 let end_tip_live = Rc::clone(&end_tip_y);
697 SelectionHandle(
698 HandleKind::SelectionEnd,
699 end_tip,
700 end_rect.height,
701 HANDLE_RADIUS,
702 accent,
703 move |pos| {
704 track_handle_grab(
705 &grab_bias,
706 HandleKind::SelectionEnd,
707 end_tip_live.get(),
708 pos.y,
709 );
710 last_dragged_end.set(Some(HandleKind::SelectionEnd));
711 drag_pos.set(Some(pos));
712 on_drag_end(pos);
713 },
714 move || {
715 drag_pos.set(None);
716 end_bias.set(None);
717 },
718 move || menu_open.set(true),
719 move || menu_open.set(true),
720 );
721
722 if menu_open.value() {
723 let can_paste = clipboard_can_paste();
724 let slide_point = if controller.gesture_claimed() {
725 active_press.map(|press| press.position)
726 } else {
727 None
728 };
729 let (center_x, caret_rect) = match last_dragged.get() {
730 Some(HandleKind::SelectionStart) => (start_tip.x, start_rect),
731 Some(HandleKind::SelectionEnd | HandleKind::Cursor) => (end_tip.x, end_rect),
732 None => ((start_tip.x + end_tip.x) * 0.5, start_rect),
733 };
734 TextSelectionMenu(
735 MenuAnchor {
736 center_x,
737 line_top: caret_rect.y,
738 line_bottom: caret_rect.y + caret_rect.height,
739 },
740 drag_pos.value().is_none(),
741 slide_point,
742 can_paste,
743 move || {
744 if let Some(text) = dispatch_copy() {
745 clipboard_write_text(&text);
746 }
747 menu_open.set(false);
748 },
749 move || {
750 if let Some(text) = dispatch_cut() {
751 clipboard_write_text(&text);
752 }
753 menu_open.set(false);
754 },
755 move || {
756 clipboard_paste_into_focus();
757 menu_open.set(false);
758 },
759 move || {
760 dispatch_select_all();
761 menu_open.set(false);
762 },
763 );
764 }
765 }
766
767 let loupe_target = drag_pos.value().and_then(|finger| {
768 let bias = drag_bias.get().map_or(0.0, |grab| grab.bias());
769 let offset = window_pos_to_offset(&text, &style, &metrics, finger, bias);
770 let (_, caret_rect) =
771 caret_window_geometry(&text, &style, &metrics, offset, LineAffinity::Upstream);
772 loupe_target_for_drag(finger, caret_rect.y + caret_rect.height, caret_rect.height)
773 });
774 SelectionLoupe(loupe_target);
775}
776
777fn track_handle_grab(
778 drag_bias: &Cell<Option<HandleGrabOffset>>,
779 kind: HandleKind,
780 handle_tip_y: f32,
781 finger_y: f32,
782) -> f32 {
783 let drifts = kind != HandleKind::SelectionStart;
784 let mut grab = drag_bias
785 .get()
786 .unwrap_or_else(|| HandleGrabOffset::begin_for(handle_tip_y, finger_y, drifts));
787 let bias = grab.track(finger_y);
788 drag_bias.set(Some(grab));
789 bias
790}
791
792fn drag_caret_closure(
796 state: TextFieldState,
797 style: TextStyle,
798 controller: TextFieldHandleController,
799 drag_bias: Rc<Cell<Option<HandleGrabOffset>>>,
800) -> Rc<dyn Fn(Point)> {
801 Rc::new(move |window_pos: Point| {
802 let Some(metrics) = controller.metrics() else {
803 return;
804 };
805 let text = state.text();
806 let bias = drag_bias.get().map_or(0.0, |grab| grab.bias());
807 let offset = window_pos_to_offset(&text, &style, &metrics, window_pos, bias);
808 state.set_selection(TextRange::new(offset, offset));
809 crate::cursor_animation::suspend_cursor_blink();
810 crate::request_render_invalidation();
811 })
812}
813
814fn drag_edge_closure(
818 dragged: HandleKind,
819 state: TextFieldState,
820 style: TextStyle,
821 controller: TextFieldHandleController,
822 drag_bias: Rc<Cell<Option<HandleGrabOffset>>>,
823) -> Rc<dyn Fn(Point)> {
824 Rc::new(move |window_pos: Point| {
825 let Some(metrics) = controller.metrics() else {
826 return;
827 };
828 let text = state.text();
829 let bias = drag_bias.get().map_or(0.0, |grab| grab.bias());
830 let dragged_offset = window_pos_to_offset(&text, &style, &metrics, window_pos, bias);
831 let selection = state.selection();
832 let fixed_edge = match dragged {
833 HandleKind::SelectionStart => selection.max(),
834 _ => selection.min(),
835 };
836 let (min, max) =
837 selection_after_handle_drag(dragged, fixed_edge, dragged_offset, text.len());
838 state.set_selection(TextRange::new(min, max));
839 crate::request_render_invalidation();
840 })
841}
842
843#[cfg(test)]
844#[path = "tests/basic_text_field_tests.rs"]
845mod tests;