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, measure_text},
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<LongPressWatcher> = 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 None,
152 metrics.wrap_width,
153 offset,
154 affinity,
155 );
156 let caret_x = measure_text(&AnnotatedString::from(&text[line_start..offset]), style).width;
157 Point {
158 x: metrics.padding_left + caret_x - metrics.scroll_offset,
159 y: metrics.padding_top
160 + line_index as f32 * metrics.line_height
161 + metrics.glyph_box.0
162 + metrics.glyph_box.1,
163 }
164}
165
166fn caret_window_geometry(
167 text: &str,
168 style: &TextStyle,
169 metrics: &TextFieldHandleMetrics,
170 offset: usize,
171 affinity: LineAffinity,
172) -> (Point, Rect) {
173 let tip = handle_tip_local_pos(text, style, metrics, offset, affinity);
174 let bounds = metrics.local_to_window.bounds_for_rect(Rect {
175 x: tip.x,
176 y: tip.y - metrics.glyph_box.1,
177 width: 2.0,
178 height: metrics.glyph_box.1,
179 });
180 (metrics.local_to_window.map_point(tip), bounds)
181}
182
183fn window_pos_to_offset(
190 text: &str,
191 style: &TextStyle,
192 metrics: &TextFieldHandleMetrics,
193 window_pos: Point,
194 y_bias: f32,
195) -> usize {
196 let Some(inverse) = metrics.local_to_window.inverse() else {
197 return 0;
198 };
199 let local = inverse.map_point(Point {
200 y: window_pos.y + y_bias,
201 ..window_pos
202 });
203 let local_x = (local.x - metrics.padding_left + metrics.scroll_offset).max(0.0);
204 let local_y = (local.y - 0.5 * metrics.line_height - metrics.padding_top).max(0.0);
205 crate::text::offset_for_position_wrapped(
206 text,
207 style,
208 None,
209 metrics.wrap_width,
210 metrics.line_height,
211 local_x,
212 local_y,
213 )
214}
215#[composable]
232pub fn BasicTextField(state: TextFieldState, modifier: Modifier, style: TextStyle) -> NodeId {
233 BasicTextFieldWithOptions(
234 state,
235 modifier,
236 BasicTextFieldOptions {
237 text_style: style,
238 ..BasicTextFieldOptions::default()
239 },
240 )
241}
242
243#[derive(Debug, Clone, PartialEq)]
245pub struct BasicTextFieldOptions {
246 pub text_style: TextStyle,
248 pub cursor_color: Color,
250 pub line_limits: TextFieldLineLimits,
252 pub show_keyboard_on_focus: bool,
259 pub keyboard_options: KeyboardOptions,
261 pub on_keyboard_action: Option<KeyboardActionHandler>,
265 pub input_transformation: Option<InputTransformation>,
268 pub output_transformation: Option<OutputTransformation>,
272}
273
274impl Default for BasicTextFieldOptions {
275 fn default() -> Self {
276 Self {
277 text_style: TextStyle::default(),
278 cursor_color: Color(0.0, 0.478, 1.0, 1.0),
279 line_limits: TextFieldLineLimits::default(),
280 show_keyboard_on_focus: true,
281 keyboard_options: KeyboardOptions::default(),
282 on_keyboard_action: None,
283 input_transformation: None,
284 output_transformation: None,
285 }
286 }
287}
288
289#[derive(Clone)]
294pub struct BasicTextFieldDecorationScope {
295 inner: Rc<dyn Fn() -> NodeId>,
296}
297
298impl BasicTextFieldDecorationScope {
299 pub fn inner_text_field(&self) -> NodeId {
302 (self.inner)()
303 }
304}
305
306impl PartialEq for BasicTextFieldDecorationScope {
307 fn eq(&self, other: &Self) -> bool {
308 Rc::ptr_eq(&self.inner, &other.inner)
309 }
310}
311
312#[composable(no_skip)]
321pub fn BasicTextFieldDecorated<D>(
322 state: TextFieldState,
323 modifier: Modifier,
324 options: BasicTextFieldOptions,
325 decoration_box: D,
326) -> NodeId
327where
328 D: Fn(BasicTextFieldDecorationScope) + 'static,
329{
330 let refs = remember(TextFieldRefs::new).with(TextFieldRefs::clone);
331 let decorator = TextFieldDecoratorElement::new(
332 state,
333 refs.clone(),
334 options.text_style.clone(),
335 options.line_limits,
336 crate::modal::local_modal_depth().current(),
337 );
338 let scope = BasicTextFieldDecorationScope {
339 inner: Rc::new(move || {
340 TextFieldNode(
341 state,
342 Modifier::empty(),
343 options.clone(),
344 Some(refs.clone()),
345 false,
346 )
347 }),
348 };
349 Layout(
350 modifier.then(Modifier::from_parts(&[modifier_element(decorator)])),
351 BoxMeasurePolicy::new(Alignment::TOP_START, true),
352 move || decoration_box(scope.clone()),
353 )
354}
355
356#[composable]
360pub fn BasicTextFieldWithOptions(
361 state: TextFieldState,
362 modifier: Modifier,
363 options: BasicTextFieldOptions,
364) -> NodeId {
365 TextFieldNode(state, modifier, options, None, false)
366}
367
368#[composable]
372pub(crate) fn TextFieldNode(
373 state: TextFieldState,
374 modifier: Modifier,
375 options: BasicTextFieldOptions,
376 decorator: Option<TextFieldRefs>,
377 secure: bool,
378) -> NodeId {
379 #[cfg(feature = "localization")]
380 let options = BasicTextFieldOptions {
381 text_style: crate::localization::apply_text_locale(options.text_style),
382 ..options
383 };
384 state.with_text(|_| ());
385 let _selection = state.selection();
386
387 let controller =
388 remember(TextFieldHandleController::new).with(TextFieldHandleController::clone);
389
390 let modal_depth = crate::modal::local_modal_depth().current();
391 let mut text_field_element = TextFieldElement::new(state, options.text_style.clone())
392 .with_cursor_color(options.cursor_color)
393 .with_line_limits(options.line_limits)
394 .with_show_keyboard_on_focus(options.show_keyboard_on_focus)
395 .with_keyboard(options.keyboard_options, options.on_keyboard_action)
396 .with_input_transformation(options.input_transformation)
397 .with_output_transformation(options.output_transformation)
398 .secure(secure)
399 .with_handle_controller(controller.clone())
400 .with_modal_depth(modal_depth);
401 if let Some(refs) = decorator {
402 text_field_element = text_field_element.decorated_by(refs);
403 }
404
405 let text_field_modifier = modifier_element(text_field_element);
406 let final_modifier = Modifier::from_parts(&[text_field_modifier]);
407 let combined_modifier = modifier.then(final_modifier);
408
409 let node = Layout(combined_modifier, EmptyMeasurePolicy, || {});
410
411 BringCaretIntoView(state, options.text_style.clone(), controller.clone());
412
413 SelectionHandles(
414 state,
415 options.text_style,
416 controller,
417 options.cursor_color,
418 secure,
419 );
420
421 node
422}
423
424#[cfg(test)]
425#[path = "tests/basic_text_field_options_tests.rs"]
426mod options_tests;
427
428fn caret_window_rect(
432 text: &str,
433 style: &TextStyle,
434 metrics: &TextFieldHandleMetrics,
435 offset: usize,
436) -> Rect {
437 caret_window_geometry(text, style, metrics, offset, LineAffinity::Upstream).1
438}
439
440#[composable]
450fn BringCaretIntoView(
451 state: TextFieldState,
452 style: TextStyle,
453 controller: TextFieldHandleController,
454) {
455 let Some(metrics) = controller.metrics() else {
456 return;
457 };
458 let ime_bottom = local_ime_insets().current().bottom;
459 let responder = local_bring_into_view_responder().current();
460
461 let previous: Rc<Cell<Option<(usize, usize, i64)>>> =
462 remember(|| Rc::new(Cell::new(None))).with(Rc::clone);
463
464 if !metrics.focused {
465 previous.set(None);
466 return;
467 }
468 let Some(responder) = responder else {
469 return;
470 };
471
472 let presentation = controller.presentation(state);
473 let selection = state.selection();
474
475 let key = (
476 selection.start,
477 selection.end,
478 (ime_bottom * 4.0).round() as i64,
479 );
480 SideEffect(move || {
481 if previous.get() == Some(key) {
482 return;
483 }
484 previous.set(Some(key));
485 let Some(metrics) = controller.metrics_now() else {
486 return;
487 };
488 let caret = caret_window_rect(
489 presentation.text(),
490 &style,
491 &metrics,
492 presentation.to_shown(selection.start),
493 );
494 responder.bring_into_view(caret, ime_bottom);
495 });
496}
497
498#[composable]
503fn SelectionHandles(
504 state: TextFieldState,
505 style: TextStyle,
506 controller: TextFieldHandleController,
507 accent: Color,
508 secure: bool,
509) {
510 let selection = state.selection();
511 let current_range = (selection.min(), selection.max());
512 let active_press = controller.press();
513
514 let menu_open = remember(|| mutableStateOf(true)).with(|state| *state);
515 let caret_menu_open = remember(|| mutableStateOf(false)).with(|state| *state);
516 let caret_menu_offset: Rc<Cell<usize>> =
517 remember(|| Rc::new(Cell::new(0usize))).with(Rc::clone);
518 let previous_range: Rc<Cell<(usize, usize)>> =
519 remember(|| Rc::new(Cell::new(current_range))).with(Rc::clone);
520 {
521 let previous_range = Rc::clone(&previous_range);
522 SideEffect(move || {
523 if previous_range.get() != current_range {
524 previous_range.set(current_range);
525 menu_open.set(true);
526 }
527 });
528 }
529 {
530 let caret_menu_offset = Rc::clone(&caret_menu_offset);
531 let caret_start = selection.start;
532 SideEffect(move || {
533 if caret_menu_open.value()
534 && (!selection.collapsed() || caret_start != caret_menu_offset.get())
535 {
536 caret_menu_open.set(false);
537 }
538 });
539 }
540
541 let Some(metrics) = controller.metrics() else {
542 return;
543 };
544 if !metrics.focused || !metrics.direct_manipulation {
545 return;
546 }
547 let Some(metrics) = controller.live_metrics() else {
550 return;
551 };
552
553 let presentation = controller.presentation(state);
554 let text = presentation.text();
555
556 let press_watcher: Rc<Cell<Option<(u32, u32)>>> =
557 remember(|| Rc::new(Cell::new(None))).with(Rc::clone);
558 let press_watcher_ref: Rc<RefCell<Option<Rc<LongPressWatcher>>>> =
559 remember(|| Rc::new(RefCell::new(None))).with(Rc::clone);
560 match active_press {
561 Some(press) => {
562 let key = (press.start.x.to_bits(), press.start.y.to_bits());
563 if press_watcher.get() != Some(key) {
564 press_watcher.set(Some(key));
565 let watcher = Rc::new(LongPressWatcher {
566 controller: controller.clone(),
567 state,
568 style: style.clone(),
569 start: press.start,
570 start_nanos: Cell::new(None),
571 registration: RefCell::new(None),
572 frame_clock: cranpose_core::with_current_composer(|composer| {
573 composer.runtime_handle()
574 })
575 .frame_clock(),
576 });
577 watcher.arm();
578 *press_watcher_ref.borrow_mut() = Some(watcher);
579 }
580 }
581 None => {
582 press_watcher.set(None);
583 press_watcher_ref.borrow_mut().take();
584 }
585 }
586
587 let drag_pos: MutableState<Option<Point>> =
588 remember(|| mutableStateOf(None::<Point>)).with(|state| *state);
589 let drag_bias: Rc<Cell<Option<HandleGrabOffset>>> =
590 remember(|| Rc::new(Cell::new(None))).with(Rc::clone);
591 let last_dragged: Rc<Cell<Option<HandleKind>>> =
592 remember(|| Rc::new(Cell::new(None))).with(Rc::clone);
593 let menu_anchor_range: Rc<Cell<(usize, usize)>> =
594 remember(|| Rc::new(Cell::new(current_range))).with(Rc::clone);
595 {
596 let last_dragged = Rc::clone(&last_dragged);
597 let menu_anchor_range = Rc::clone(&menu_anchor_range);
598 SideEffect(move || {
599 if menu_anchor_range.get() != current_range {
600 menu_anchor_range.set(current_range);
601 if drag_pos.value().is_none() {
602 last_dragged.set(None);
603 }
604 }
605 });
606 }
607 let cursor_tip_y: Rc<Cell<f32>> = remember(|| Rc::new(Cell::new(0.0f32))).with(Rc::clone);
608 let start_tip_y: Rc<Cell<f32>> = remember(|| Rc::new(Cell::new(0.0f32))).with(Rc::clone);
609 let end_tip_y: Rc<Cell<f32>> = remember(|| Rc::new(Cell::new(0.0f32))).with(Rc::clone);
610
611 if selection.collapsed() {
612 let (tip, caret_rect) = caret_window_geometry(
613 text,
614 &style,
615 &metrics,
616 presentation.to_shown(selection.start),
617 LineAffinity::Upstream,
618 );
619 let on_drag = drag_caret_closure(state, style.clone(), controller, Rc::clone(&drag_bias));
620 let open_caret_menu = {
621 let caret_menu_offset = Rc::clone(&caret_menu_offset);
622 move || {
623 caret_menu_offset.set(state.selection().start);
624 caret_menu_open.set(true);
625 }
626 };
627 let on_tap = open_caret_menu.clone();
628 let on_long_press = open_caret_menu;
629 let grab_bias = Rc::clone(&drag_bias);
630 let end_bias = Rc::clone(&drag_bias);
631 cursor_tip_y.set(tip.y);
632 let tip_y = Rc::clone(&cursor_tip_y);
633 SelectionHandle(
634 HandleKind::Cursor,
635 tip,
636 caret_rect.height,
637 HANDLE_RADIUS,
638 accent,
639 move |pos| {
640 track_handle_grab(&grab_bias, HandleKind::Cursor, tip_y.get(), pos.y);
641 drag_pos.set(Some(pos));
642 on_drag(pos);
643 },
644 move || {
645 drag_pos.set(None);
646 end_bias.set(None);
647 crate::cursor_animation::reset_cursor_blink();
648 },
649 on_long_press,
650 on_tap,
651 );
652
653 if caret_menu_open.value() {
654 let can_paste = clipboard_can_paste();
655 let can_undo = state.can_undo();
656 let can_redo = state.can_redo();
657 let undo_state = state;
658 let redo_state = state;
659 CaretActionMenu(
660 MenuAnchor {
661 center_x: tip.x,
662 line_top: caret_rect.y,
663 line_bottom: caret_rect.y + caret_rect.height,
664 },
665 drag_pos.value().is_none(),
666 can_paste,
667 can_undo,
668 can_redo,
669 move || {
670 clipboard_paste_into_focus();
671 caret_menu_open.set(false);
672 },
673 move || {
674 dispatch_select_all();
675 caret_menu_open.set(false);
676 },
677 move || {
678 undo_state.undo();
679 crate::request_render_invalidation();
680 caret_menu_open.set(false);
681 },
682 move || {
683 redo_state.redo();
684 crate::request_render_invalidation();
685 caret_menu_open.set(false);
686 },
687 );
688 }
689 } else {
690 let (start, end) = presentation.range_to_shown(selection);
691 let (start_tip, start_rect) =
692 caret_window_geometry(text, &style, &metrics, start, LineAffinity::Downstream);
693 let (end_tip, end_rect) =
694 caret_window_geometry(text, &style, &metrics, end, LineAffinity::Upstream);
695
696 let last_dragged_start = Rc::clone(&last_dragged);
697 let last_dragged_end = Rc::clone(&last_dragged);
698 let on_drag_start = drag_edge_closure(
699 HandleKind::SelectionStart,
700 state,
701 style.clone(),
702 controller.clone(),
703 Rc::clone(&drag_bias),
704 );
705 let grab_bias = Rc::clone(&drag_bias);
706 let end_bias = Rc::clone(&drag_bias);
707 start_tip_y.set(start_tip.y);
708 let start_tip_live = Rc::clone(&start_tip_y);
709 SelectionHandle(
710 HandleKind::SelectionStart,
711 start_tip,
712 start_rect.height,
713 HANDLE_RADIUS,
714 accent,
715 move |pos| {
716 track_handle_grab(
717 &grab_bias,
718 HandleKind::SelectionStart,
719 start_tip_live.get(),
720 pos.y,
721 );
722 last_dragged_start.set(Some(HandleKind::SelectionStart));
723 drag_pos.set(Some(pos));
724 on_drag_start(pos);
725 },
726 move || {
727 drag_pos.set(None);
728 end_bias.set(None);
729 },
730 move || menu_open.set(true),
731 move || menu_open.set(true),
732 );
733
734 let on_drag_end = drag_edge_closure(
735 HandleKind::SelectionEnd,
736 state,
737 style.clone(),
738 controller.clone(),
739 Rc::clone(&drag_bias),
740 );
741 let grab_bias = Rc::clone(&drag_bias);
742 let end_bias = Rc::clone(&drag_bias);
743 end_tip_y.set(end_tip.y);
744 let end_tip_live = Rc::clone(&end_tip_y);
745 SelectionHandle(
746 HandleKind::SelectionEnd,
747 end_tip,
748 end_rect.height,
749 HANDLE_RADIUS,
750 accent,
751 move |pos| {
752 track_handle_grab(
753 &grab_bias,
754 HandleKind::SelectionEnd,
755 end_tip_live.get(),
756 pos.y,
757 );
758 last_dragged_end.set(Some(HandleKind::SelectionEnd));
759 drag_pos.set(Some(pos));
760 on_drag_end(pos);
761 },
762 move || {
763 drag_pos.set(None);
764 end_bias.set(None);
765 },
766 move || menu_open.set(true),
767 move || menu_open.set(true),
768 );
769
770 if menu_open.value() {
771 let can_paste = clipboard_can_paste();
772 let slide_point = if controller.gesture_claimed() {
773 active_press.map(|press| press.position)
774 } else {
775 None
776 };
777 let (center_x, caret_rect) = match last_dragged.get() {
778 Some(HandleKind::SelectionStart) => (start_tip.x, start_rect),
779 Some(HandleKind::SelectionEnd | HandleKind::Cursor) => (end_tip.x, end_rect),
780 None => ((start_tip.x + end_tip.x) * 0.5, start_rect),
781 };
782 TextSelectionMenu(
783 MenuAnchor {
784 center_x,
785 line_top: caret_rect.y,
786 line_bottom: caret_rect.y + caret_rect.height,
787 },
788 drag_pos.value().is_none(),
789 slide_point,
790 !secure,
791 can_paste,
792 move || {
793 if let Some(text) = dispatch_copy() {
794 clipboard_write_text(&text);
795 }
796 menu_open.set(false);
797 },
798 move || {
799 if let Some(text) = dispatch_cut() {
800 clipboard_write_text(&text);
801 }
802 menu_open.set(false);
803 },
804 move || {
805 clipboard_paste_into_focus();
806 menu_open.set(false);
807 },
808 move || {
809 dispatch_select_all();
810 menu_open.set(false);
811 },
812 );
813 }
814 }
815
816 let loupe_target = drag_pos.value().and_then(|finger| {
817 let bias = drag_bias.get().map_or(0.0, |grab| grab.bias());
818 let offset = window_pos_to_offset(text, &style, &metrics, finger, bias);
819 let (_, caret_rect) =
820 caret_window_geometry(text, &style, &metrics, offset, LineAffinity::Upstream);
821 loupe_target_for_drag(finger, caret_rect.y + caret_rect.height, caret_rect.height)
822 });
823 SelectionLoupe(loupe_target);
824}
825
826fn track_handle_grab(
827 drag_bias: &Cell<Option<HandleGrabOffset>>,
828 kind: HandleKind,
829 handle_tip_y: f32,
830 finger_y: f32,
831) -> f32 {
832 let drifts = kind != HandleKind::SelectionStart;
833 let mut grab = drag_bias
834 .get()
835 .unwrap_or_else(|| HandleGrabOffset::begin_for(handle_tip_y, finger_y, drifts));
836 let bias = grab.track(finger_y);
837 drag_bias.set(Some(grab));
838 bias
839}
840
841fn drag_caret_closure(
845 state: TextFieldState,
846 style: TextStyle,
847 controller: TextFieldHandleController,
848 drag_bias: Rc<Cell<Option<HandleGrabOffset>>>,
849) -> Rc<dyn Fn(Point)> {
850 Rc::new(move |window_pos: Point| {
851 let Some(metrics) = controller.metrics() else {
852 return;
853 };
854 let presentation = controller.presentation(state);
855 let bias = drag_bias.get().map_or(0.0, |grab| grab.bias());
856 let offset = presentation.to_source(window_pos_to_offset(
857 presentation.text(),
858 &style,
859 &metrics,
860 window_pos,
861 bias,
862 ));
863 state.set_selection(TextRange::new(offset, offset));
864 crate::cursor_animation::suspend_cursor_blink();
865 crate::request_render_invalidation();
866 })
867}
868
869fn drag_edge_closure(
873 dragged: HandleKind,
874 state: TextFieldState,
875 style: TextStyle,
876 controller: TextFieldHandleController,
877 drag_bias: Rc<Cell<Option<HandleGrabOffset>>>,
878) -> Rc<dyn Fn(Point)> {
879 Rc::new(move |window_pos: Point| {
880 let Some(metrics) = controller.metrics() else {
881 return;
882 };
883 let presentation = controller.presentation(state);
884 let bias = drag_bias.get().map_or(0.0, |grab| grab.bias());
885 let dragged_offset = presentation.to_source(window_pos_to_offset(
886 presentation.text(),
887 &style,
888 &metrics,
889 window_pos,
890 bias,
891 ));
892 let selection = state.selection();
893 let fixed_edge = match dragged {
894 HandleKind::SelectionStart => selection.max(),
895 _ => selection.min(),
896 };
897 let (min, max) = selection_after_handle_drag(
898 dragged,
899 fixed_edge,
900 dragged_offset,
901 state.with_text(str::len),
902 );
903 state.set_selection(TextRange::new(min, max));
904 crate::request_render_invalidation();
905 })
906}
907
908#[cfg(test)]
909#[path = "tests/basic_text_field_tests.rs"]
910mod tests;