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