1use super::popup_positioning::{CustomPopupPlacement, CustomPopupPlacementCallback, PopupAnchor};
2use super::{PopupFlyoutBase, SelectionHandleType, TextSelectionHandle};
3use crate::platform::{FeedbackAction, PlatformFeedbackExtensions};
4use crate::presenters::TextPresenter;
5use crate::text_box_text_input_method_client::TextInputMethodChange;
6use crate::{Canvas, CanvasImpl, Control, ControlImpl, FlyoutShowMode, PanelImpl, PlacementMode, TextBox};
7use ferroui_base::input::{
8 ContextRequestedEventArgs, FocusChangedEventArgs, InputElement, InputElementImpl, KeyEventArgs,
9 PointerPressedEventArgs, PointerType, ScrollGestureEventArgs, TappedEventArgs, VectorEventArgs,
10};
11use ferroui_base::interactivity::{Interactive, InteractiveImpl, RoutedEventArgs, RoutedEventHandlerToken};
12use ferroui_base::layout::{Layoutable, LayoutableImpl, LayoutableImplExt};
13use ferroui_base::media::text_formatting::ShapedTextRun;
14use ferroui_base::reactive::{Disposable, IDisposable};
15use ferroui_base::threading::{DispatcherPriority, DispatcherTimer};
16use ferroui_base::{
17 ferro_class, ferro_class_info, ferro_impl_classes, instantiate, FerroObjectImpl, FerroObjectImplExt,
18 FerroPropertyChangedEventArgs, Point, Ref, Size, StyledElementImpl, Thickness, Visual, VisualImpl, WeakRef,
19};
20use std::cell::{Cell, RefCell};
21use std::rc::Rc;
22use std::time::Duration;
23
24const SHOULD_WRAP_AROUND_SELECTION: bool = !cfg!(target_os = "android");
27
28const CONTEXT_MENU_PADDING: f64 = 16.0;
29
30struct PresenterSubscriptions {
33 key_down: RoutedEventHandlerToken,
34 tapped: RoutedEventHandlerToken,
35 pointer_pressed: RoutedEventHandlerToken,
36 got_focus: RoutedEventHandlerToken,
37 is_effectively_visible_changed: Rc<dyn IDisposable>,
38 text_box: Option<(Rc<dyn IDisposable>, RoutedEventHandlerToken)>,
39}
40
41#[repr(C)]
44pub struct TextSelectionHandleCanvas {
45 base: Canvas,
46 caret_handle: Ref<TextSelectionHandle>,
47 handle1: Ref<TextSelectionHandle>,
48 handle2: Ref<TextSelectionHandle>,
49 presenter: RefCell<Option<WeakRef<TextPresenter>>>,
52 text_box: RefCell<Option<WeakRef<TextBox>>>,
53 subscriptions: RefCell<Option<PresenterSubscriptions>>,
54 show_handle: Cell<bool>,
55 show_disposable: RefCell<Option<Rc<dyn IDisposable>>>,
56 layout_listener: PresenterVisualListener,
57 saved_selection_start: Cell<Option<i32>>,
58 save_selection_end: Cell<Option<i32>>,
59 initial_handle_type: Cell<Option<SelectionHandleType>>,
60 is_in_touch_mode: Cell<bool>,
61}
62
63ferro_class!(TextSelectionHandleCanvas: Canvas);
64ferro_class_info!(TextSelectionHandleCanvas { new: TextSelectionHandleCanvas::new });
65
66ferro_impl_classes!(
67 TextSelectionHandleCanvas: StyledElementImpl,
68 VisualImpl,
69 InteractiveImpl,
70 InputElementImpl,
71 ControlImpl,
72 PanelImpl,
73 CanvasImpl
74);
75
76impl FerroObjectImpl for TextSelectionHandleCanvas {
77 fn constructed(this: &Self) {
78 Self::parent_constructed(this);
79
80 this.caret_handle.set_selection_handle_type(SelectionHandleType::Caret);
81 this.handle1.set_selection_handle_type(SelectionHandleType::Start);
82 this.handle2.set_selection_handle_type(SelectionHandleType::End);
83
84 this.children().add(this.caret_handle.clone());
85 this.children().add(this.handle1.clone());
86 this.children().add(this.handle2.clone());
87
88 let weak = this.to_ref().downgrade();
89
90 let drag_started = |handle: &Ref<TextSelectionHandle>| {
93 let (weak, sender) = (weak.clone(), handle.downgrade());
94 handle.drag_started(move |_, _| {
95 if let (Some(this), Some(sender)) = (weak.upgrade(), sender.upgrade()) {
96 this.handle_drag_started(&sender);
97 }
98 });
99 };
100 let drag_completed = |handle: &Ref<TextSelectionHandle>| {
101 let weak = weak.clone();
102 handle.drag_completed(move |_, _| {
103 if let Some(this) = weak.upgrade() {
104 this.handle_drag_completed();
105 }
106 });
107 };
108 let selection_drag_delta = |handle: &Ref<TextSelectionHandle>| {
109 let (weak, sender) = (weak.clone(), handle.downgrade());
110 handle.drag_delta(move |_, e| {
111 if let (Some(this), Some(sender)) = (weak.upgrade(), sender.upgrade()) {
112 this.selection_handle_drag_delta(&sender, e);
113 }
114 });
115 };
116
117 drag_started(&this.caret_handle);
118 {
119 let weak = weak.clone();
120 this.caret_handle.drag_delta(move |_, _| {
121 if let Some(this) = weak.upgrade() {
122 this.caret_handle_drag_delta();
123 }
124 });
125 }
126 drag_completed(&this.caret_handle);
127 selection_drag_delta(&this.handle1);
128 drag_completed(&this.handle1);
129 drag_started(&this.handle1);
130 selection_drag_delta(&this.handle2);
131 drag_completed(&this.handle2);
132 drag_started(&this.handle2);
133
134 this.handle1.set_top_left(Point::default());
135 this.caret_handle.set_top_left(Point::default());
136 this.handle2.set_top_left(Point::default());
137
138 for handle in [&this.handle1, &this.caret_handle, &this.handle2] {
139 let weak = weak.clone();
140 handle.add_handler(InputElement::context_canceled_event(), move |_, _: &RoutedEventArgs| {
141 if let Some(this) = weak.upgrade() {
142 this.caret_context_canceled();
143 }
144 });
145 }
146 for handle in [&this.handle1, &this.caret_handle, &this.handle2] {
147 let weak = weak.clone();
148 handle.add_handler(
149 InputElement::context_requested_event(),
150 move |_, e: &ContextRequestedEventArgs| {
151 if let Some(this) = weak.upgrade() {
152 this.caret_context_requested(e);
153 }
154 },
155 );
156 }
157
158 this.set_is_visible(this.show_handles());
159
160 this.set_clip_to_bounds(false);
161
162 this.layout_listener.set_invalidated(move || {
163 if let Some(this) = weak.upgrade() {
164 this.layout_listener_invalidated();
165 }
166 });
167 }
168}
169
170impl LayoutableImpl for TextSelectionHandleCanvas {
171 fn measure_override(this: &Self, available_size: Size) -> Size {
172 if this.show_handles() {
173 this.move_handles_to_selection();
174 }
175
176 Self::parent_measure_override(this, available_size)
177 }
178}
179
180impl TextSelectionHandleCanvas {
181 pub fn construct() -> Self {
183 Self {
184 base: Canvas::construct(),
185 caret_handle: TextSelectionHandle::new(),
186 handle1: TextSelectionHandle::new(),
187 handle2: TextSelectionHandle::new(),
188 presenter: RefCell::new(None),
189 text_box: RefCell::new(None),
190 subscriptions: RefCell::new(None),
191 show_handle: Cell::new(false),
192 show_disposable: RefCell::new(None),
193 layout_listener: PresenterVisualListener::new(),
194 saved_selection_start: Cell::new(None),
195 save_selection_end: Cell::new(None),
196 initial_handle_type: Cell::new(None),
197 is_in_touch_mode: Cell::new(false),
198 }
199 }
200
201 pub fn new() -> Ref<Self> {
202 instantiate(Self::construct())
203 }
204
205 fn presenter(&self) -> Option<Ref<TextPresenter>> {
206 self.presenter.borrow().as_ref().and_then(WeakRef::upgrade)
207 }
208
209 fn text_box(&self) -> Option<Ref<TextBox>> {
210 self.text_box.borrow().as_ref().and_then(WeakRef::upgrade)
211 }
212
213 pub(crate) fn show_handles(&self) -> bool {
214 self.show_handle.get()
215 }
216
217 pub(crate) fn set_show_handles(&self, value: bool) {
218 self.show_handle.set(value);
219
220 if !value {
221 self.handle1.set_is_visible(false);
222 self.handle2.set_is_visible(false);
223 self.caret_handle.set_is_visible(false);
224 }
225
226 let has_text = self.presenter().and_then(|presenter| presenter.text()).is_some_and(|text| !text.is_empty());
227 self.set_is_visible(has_text && value);
228 }
229
230 fn layout_listener_invalidated(&self) {
231 self.invalidate_measure();
232 }
233
234 fn caret_context_canceled(&self) {
235 self.close_flyout();
236 }
237
238 fn caret_context_requested(&self, e: &ContextRequestedEventArgs) {
239 self.show_flyout();
240 e.set_handled(true);
241
242 if e.is_holding() {
243 let element: &InputElement = self;
244 element.perform_feedback(FeedbackAction::hold());
245 }
246 }
247
248 fn handle_drag_started(&self, sender: &TextSelectionHandle) {
249 let text_box = self.text_box();
250 self.saved_selection_start.set(text_box.as_ref().map(|text_box| text_box.selection_start()));
251 self.save_selection_end.set(text_box.as_ref().map(|text_box| text_box.selection_end()));
252 self.initial_handle_type.set(Some(sender.selection_handle_type()));
253 self.close_flyout();
254 }
255
256 fn close_flyout(&self) {
257 if let Some(text_box) = self.text_box() {
258 text_box.raise_event(&RoutedEventArgs::with_event(InputElement::context_canceled_event()));
259 }
260 }
261
262 fn selection_handle_drag_delta(&self, handle: &Ref<TextSelectionHandle>, e: &VectorEventArgs) {
263 self.drag_selection_handle(handle);
264 e.set_handled(true);
265 }
266
267 fn caret_handle_drag_delta(&self) {
268 if let (Some(presenter), Some(text_box)) = (self.presenter(), self.text_box()) {
269 let indicator_position = self.get_search_point(&self.caret_handle);
270 let point = self.to_presenter(indicator_position);
271 let _change = self.begin_change();
272 presenter.move_caret_to_point(point);
273 let caret_index = presenter.caret_index();
274 text_box.set_current_value(TextBox::caret_index_property(), caret_index);
275 text_box.set_current_value(TextBox::selection_start_property(), caret_index);
276 text_box.set_current_value(TextBox::selection_end_property(), caret_index);
277
278 let caret_bound = presenter.cursor_rectangle();
279 self.caret_handle.set_top_left(self.to_layer(caret_bound.bottom_left()));
280 }
281 }
282
283 pub fn hide(&self) {
284 self.set_show_handles(false);
285 self.is_in_touch_mode.set(false);
286 }
287
288 pub(crate) fn show(&self, force_touch_mode: bool) {
289 self.is_in_touch_mode.set(self.is_in_touch_mode.get() || force_touch_mode);
290 if self.is_in_touch_mode.get() {
291 self.set_show_handles(true);
292 self.move_handles_to_selection();
293
294 self.check_state_and_show_flyout();
295 }
296 }
297
298 fn begin_change(&self) -> Option<TextInputMethodChange> {
299 self.presenter().and_then(|presenter| presenter.current_im_client()).map(|client| client.begin_change())
300 }
301
302 fn handle_drag_completed(&self) {
303 self.saved_selection_start.set(Some(-1));
304 self.save_selection_end.set(Some(-1));
305
306 if !self.handle1.is_dragging() {
307 self.handle1.set_needs_indicator_update(true);
308 self.handle1.set_selection_handle_type(SelectionHandleType::Start);
309 }
310
311 if !self.handle2.is_dragging() {
312 self.handle2.set_needs_indicator_update(true);
313 self.handle2.set_selection_handle_type(SelectionHandleType::End);
314 }
315
316 self.move_handles_to_selection();
317 self.check_state_and_show_flyout();
318 }
319
320 fn check_state_and_show_flyout(&self) {
321 let text_box = self.text_box();
322 let selection_start = text_box.as_ref().map(|text_box| text_box.selection_start());
323 let selection_end = text_box.as_ref().map(|text_box| text_box.selection_end());
324
325 if selection_start != selection_end && !(self.handle1.is_dragging() || self.handle2.is_dragging()) {
326 self.show_flyout();
328 }
329 }
330
331 fn ensure_visible(&self) {
332 if let Some(show_disposable) = self.show_disposable.take() {
333 show_disposable.dispose();
334 }
335
336 let Some(presenter) = self.presenter() else {
337 return;
338 };
339
340 if presenter.visual_root().is_some_and(|root| root.is::<InputElement>()) {
341 let Some(bounds) = presenter.get_transformed_bounds() else {
342 return;
343 };
344
345 let padding = self.text_box().map_or(Thickness::symmetric(4.0, 0.0), |text_box| text_box.padding());
346 let clip = bounds.clip.inflate_thickness(padding);
347 let is_occluded = |point: Point| !clip.contains(point);
348
349 let is_selection_dragging = self.handle1.is_dragging() || self.handle2.is_dragging();
350
351 let has_selection = presenter.selection_start() != presenter.selection_end() || is_selection_dragging;
352
353 let show_handles = self.show_handles();
354 self.handle1.set_is_visible(
355 show_handles
356 && has_selection
357 && (self.handle1.is_dragging() || !is_occluded(self.handle1.indicator_position())),
358 );
359 self.handle2.set_is_visible(
360 show_handles
361 && has_selection
362 && (self.handle2.is_dragging() || !is_occluded(self.handle2.indicator_position())),
363 );
364 self.caret_handle.set_is_visible(
365 show_handles
366 && !has_selection
367 && (self.caret_handle.is_dragging() || !is_occluded(self.caret_handle.indicator_position())),
368 );
369
370 if self.show_handles() && !has_selection {
371 let weak = self.to_ref().downgrade();
372 let show_disposable = DispatcherTimer::run_once(
373 move || {
374 if let Some(this) = weak.upgrade() {
375 this.set_show_handles(false);
376 let show_disposable = this.show_disposable.borrow().clone();
377 if let Some(show_disposable) = show_disposable {
378 show_disposable.dispose();
379 }
380 }
381 },
382 Duration::from_secs(5),
383 DispatcherPriority::BACKGROUND,
384 );
385 *self.show_disposable.borrow_mut() = Some(show_disposable);
386 }
387 }
388 }
389
390 fn get_search_point(&self, handle: &TextSelectionHandle) -> Point {
391 let Some(presenter) = self.presenter() else {
392 return Point::default();
393 };
394
395 let caret_bounds = presenter.cursor_rectangle();
396 let search_offset = caret_bounds.height / 2.0;
397 let indicator = handle.indicator_position();
398 indicator.with_y(indicator.y - search_offset)
399 }
400
401 fn drag_selection_handle(&self, handle: &Ref<TextSelectionHandle>) {
402 let (Some(presenter), Some(textbox)) = (self.presenter(), self.text_box()) else {
403 return;
404 };
405
406 self.close_flyout();
407
408 let mut position = {
409 let indicator_position = self.get_search_point(handle);
410 let point = self.to_presenter(indicator_position);
411 let hit = presenter.text_layout().hit_test_point(point);
412 let character_hit = hit.character_hit();
413 character_hit.first_character_index() + character_hit.trailing_length()
414 };
415
416 let other_handle = if *handle == self.handle1 { &self.handle2 } else { &self.handle1 };
417
418 let _change = self.begin_change();
419
420 if !SHOULD_WRAP_AROUND_SELECTION {
423 if handle.selection_handle_type() == SelectionHandleType::Start {
424 position = if position >= textbox.selection_end() { textbox.selection_end() - 1 } else { position };
425 textbox.set_current_value(TextBox::selection_start_property(), position);
426 } else {
427 position =
428 if position <= textbox.selection_start() { textbox.selection_start() + 1 } else { position };
429 textbox.set_current_value(TextBox::selection_end_property(), position);
430 }
431 } else {
432 let last_position =
435 || textbox.text().map_or(0, |text| text.encode_utf16().count() as i32 - 1);
436 let set = |property, value: Option<i32>| {
440 if let Some(value) = value {
441 textbox.set_current_value(property, value);
442 }
443 };
444
445 if self.initial_handle_type.get() == Some(SelectionHandleType::Start) {
446 let other_position = self.save_selection_end.get();
449 let has_position_swapped = other_position.is_some_and(|other| position > other);
450
451 if Some(position) == other_position {
452 position = if has_position_swapped {
453 i32::min(position + 1, last_position())
454 } else {
455 i32::max(position - 1, 0)
456 };
457 }
458
459 set(
460 TextBox::selection_start_property(),
461 if has_position_swapped { other_position } else { Some(position) },
462 );
463 set(
464 TextBox::selection_end_property(),
465 if has_position_swapped { Some(position) } else { other_position },
466 );
467
468 other_handle.set_selection_handle_type(if has_position_swapped {
469 SelectionHandleType::Start
470 } else {
471 SelectionHandleType::End
472 });
473 } else {
474 let other_position = self.saved_selection_start.get();
477 let has_position_swapped = other_position.is_some_and(|other| position < other);
478
479 if Some(position) == other_position {
480 position = if !has_position_swapped {
481 i32::min(position + 1, last_position())
482 } else {
483 i32::max(position - 1, 0)
484 };
485 }
486
487 set(
488 TextBox::selection_start_property(),
489 if has_position_swapped { Some(position) } else { other_position },
490 );
491 set(
492 TextBox::selection_end_property(),
493 if has_position_swapped { other_position } else { Some(position) },
494 );
495
496 other_handle.set_selection_handle_type(if has_position_swapped {
497 SelectionHandleType::End
498 } else {
499 SelectionHandleType::Start
500 });
501 }
502 }
503
504 presenter.move_caret_to_text_position(position, false);
505 let caret_bound = presenter.cursor_rectangle();
506 handle.set_top_left(self.to_layer(caret_bound.bottom_left()));
507
508 self.move_handles_to_selection();
509 }
510
511 fn to_layer(&self, point: Point) -> Point {
512 let Some(presenter) = self.presenter() else {
513 return point;
514 };
515 match presenter.visual_root() {
516 Some(root) => presenter.translate_point(point, &root).unwrap_or(point),
517 None => point,
518 }
519 }
520
521 fn to_presenter(&self, point: Point) -> Point {
522 let Some(presenter) = self.presenter() else {
523 return point;
524 };
525 presenter.visual_root().and_then(|root| root.translate_point(point, &presenter)).unwrap_or(point)
526 }
527
528 fn to_text_box(&self, point: Point) -> Point {
529 let Some(text_box) = self.text_box() else {
530 return point;
531 };
532 text_box.visual_root().and_then(|root| root.translate_point(point, &text_box)).unwrap_or(point)
533 }
534
535 pub fn move_handles_to_selection(&self) {
536 let Some(presenter) = self.presenter() else {
537 return;
538 };
539
540 let selection_start = presenter.selection_start();
541 let selection_end = presenter.selection_end();
542 let has_selection = selection_start != selection_end;
543
544 if !self.caret_handle.is_dragging() {
545 let points = presenter.get_caret_points();
546
547 self.caret_handle.set_top_left(self.to_layer(points.1));
548 }
549
550 if has_selection {
551 let start = i32::min(selection_start, selection_end);
552 let end = i32::max(selection_start, selection_end);
553
554 let start_point = Self::get_position(&presenter, start, true);
555 let end_point = Self::get_position(&presenter, end - 1, false);
556
557 for handle in [&self.handle1, &self.handle2] {
558 if !handle.is_dragging() {
559 let (position, is_rtl) =
560 if handle.selection_handle_type() == SelectionHandleType::Start { start_point } else { end_point };
561 if is_rtl != handle.is_rtl() {
562 handle.set_needs_indicator_update(true);
563 }
564 handle.set_is_rtl(is_rtl);
565 handle.set_top_left(self.to_layer(position));
566 }
567 }
568 }
569
570 self.ensure_visible();
571 }
572
573 fn get_position(presenter: &TextPresenter, index: i32, start: bool) -> (Point, bool) {
576 let text_layout = presenter.text_layout();
577 let rect = text_layout.hit_test_text_range(index, 1).first().copied().unwrap_or_default();
578
579 let line_index = text_layout.get_line_index_from_character_index(index, false);
580 let text_line = &text_layout.text_lines()[line_index as usize];
581 let line_start = text_line.first_text_source_index();
582 let character_line_index = i32::max(0, index - line_start);
583 let mut is_left_to_right = true;
584 let mut search_length = 0;
585
586 for run in text_line.text_runs().iter() {
587 is_left_to_right = run
588 .as_any()
589 .downcast_ref::<ShapedTextRun>()
590 .is_none_or(|shaped| shaped.shaped_buffer().is_left_to_right());
591
592 search_length += run.length();
593 if search_length > character_line_index {
594 break;
595 }
596 }
597
598 let is_rtl = !is_left_to_right;
599 let mut reversed = is_rtl;
600 if !start {
601 reversed = !reversed;
602 }
603
604 (if reversed { rect.bottom_right() } else { rect.bottom_left() }, is_rtl)
605 }
606
607 pub(crate) fn set_presenter(&self, text_presenter: Option<&Ref<TextPresenter>>) {
608 let current = self.presenter();
609 if current.as_ref() == text_presenter && (current.is_some() || self.subscriptions.borrow().is_none()) {
610 return;
611 }
612
613 if current.is_some() || self.subscriptions.borrow().is_some() {
616 self.layout_listener.detach();
617 let subscriptions = self.subscriptions.take();
618 if let Some(subscriptions) = subscriptions {
619 if let Some(presenter) = ¤t {
620 presenter.remove_handler(InputElement::key_down_event(), subscriptions.key_down);
621 presenter.remove_handler(InputElement::tapped_event(), subscriptions.tapped);
622 presenter.remove_handler(InputElement::pointer_pressed_event(), subscriptions.pointer_pressed);
623 presenter.remove_handler(InputElement::got_focus_event(), subscriptions.got_focus);
624 }
625 subscriptions.is_effectively_visible_changed.dispose();
626
627 if let Some((property_changed, scroll_gesture)) = subscriptions.text_box {
628 property_changed.dispose();
629 if let Some(text_box) = self.text_box() {
630 text_box.remove_handler(InputElement::scroll_gesture_event(), scroll_gesture);
631 }
632 }
633 }
634 *self.text_box.borrow_mut() = None;
635
636 *self.presenter.borrow_mut() = None;
637 }
638
639 *self.presenter.borrow_mut() = text_presenter.map(Ref::downgrade);
640 if let Some(presenter) = text_presenter {
641 self.layout_listener.attach(presenter);
642 let weak = self.to_ref().downgrade();
643
644 let key_down = {
645 let weak = weak.clone();
646 presenter.add_handler_with(
647 InputElement::key_down_event(),
648 move |_, _: &KeyEventArgs| {
649 if let Some(this) = weak.upgrade() {
650 this.presenter_key_down();
651 }
652 },
653 Interactive::DEFAULT_ROUTES,
654 true,
655 )
656 };
657 let tapped = {
658 let weak = weak.clone();
659 presenter.add_handler(InputElement::tapped_event(), move |_, e: &TappedEventArgs| {
660 if let Some(this) = weak.upgrade() {
661 this.presenter_tapped(e);
662 }
663 })
664 };
665 let pointer_pressed = {
666 let weak = weak.clone();
667 presenter.add_handler_with(
668 InputElement::pointer_pressed_event(),
669 move |_, e: &PointerPressedEventArgs| {
670 if let Some(this) = weak.upgrade() {
671 this.presenter_pressed(e);
672 }
673 },
674 Interactive::DEFAULT_ROUTES,
675 true,
676 )
677 };
678 let got_focus = {
679 let weak = weak.clone();
680 presenter.add_handler_with(
681 InputElement::got_focus_event(),
682 move |_, _: &FocusChangedEventArgs| {
683 if let Some(this) = weak.upgrade() {
684 this.presenter_focused();
685 }
686 },
687 Interactive::DEFAULT_ROUTES,
688 true,
689 )
690 };
691
692 let is_effectively_visible_changed = {
693 let weak = weak.clone();
694 presenter.is_effectively_visible_changed(move || {
695 if let Some(this) = weak.upgrade() {
696 this.presenter_is_effectively_visible_changed();
697 }
698 })
699 };
700
701 let text_box = presenter.find_ancestor_of_type::<TextBox>(false);
702 *self.text_box.borrow_mut() = text_box.as_ref().map(Ref::downgrade);
703
704 let text_box = text_box.map(|text_box| {
705 let property_changed = {
706 let weak = weak.clone();
707 text_box.property_changed(move |e| {
708 if let Some(this) = weak.upgrade() {
709 this.text_box_property_changed(e);
710 }
711 })
712 };
713 let scroll_gesture = text_box.add_handler_with(
714 InputElement::scroll_gesture_event(),
715 move |_, _: &ScrollGestureEventArgs| {
716 if let Some(this) = weak.upgrade() {
717 this.text_box_scrolling();
718 }
719 },
720 Interactive::DEFAULT_ROUTES,
721 true,
722 );
723 (property_changed, scroll_gesture)
724 });
725
726 *self.subscriptions.borrow_mut() = Some(PresenterSubscriptions {
727 key_down,
728 tapped,
729 pointer_pressed,
730 got_focus,
731 is_effectively_visible_changed,
732 text_box,
733 });
734 }
735 }
736
737 fn presenter_is_effectively_visible_changed(&self) {
738 self.close_flyout();
740 self.set_show_handles(false);
741 self.ensure_visible();
742 }
743
744 fn text_box_scrolling(&self) {
745 self.close_flyout();
746 }
747
748 fn presenter_pressed(&self, e: &PointerPressedEventArgs) {
749 self.is_in_touch_mode.set(e.pointer().type_() != PointerType::Mouse);
750 }
751
752 fn presenter_focused(&self) {
753 if self.presenter().is_some_and(|presenter| presenter.selection_start() != presenter.selection_end()) {
754 self.set_show_handles(true);
755 self.ensure_visible();
756 }
757 }
758
759 fn presenter_tapped(&self, e: &TappedEventArgs) {
760 self.is_in_touch_mode.set(e.pointer().type_() != PointerType::Mouse);
761
762 if self.is_in_touch_mode.get() {
763 self.move_handles_to_selection();
764 } else {
765 self.set_show_handles(false);
766 if let Some(show_disposable) = self.show_disposable.take() {
767 show_disposable.dispose();
768 }
769 }
770 }
771
772 pub(crate) fn show_flyout(&self) -> bool {
773 let Some(text_box) = self.text_box() else {
774 return false;
775 };
776 let Some(flyout) = text_box.context_flyout().and_then(|flyout| flyout.cast::<PopupFlyoutBase>()) else {
777 return false;
778 };
779
780 let line_height = text_box.line_height();
781 let vertical_offset =
782 (if line_height.is_nan() { text_box.font_size() } else { line_height }) + CONTEXT_MENU_PADDING;
783
784 let topleft = if text_box.selection_start() != text_box.selection_end() {
785 if self.handle1.is_effectively_visible() && self.handle2.is_effectively_visible() {
786 let p1 = self.handle1.indicator_position();
787 let p2 = self.handle2.indicator_position();
788
789 Some(Point::new((p1.x + p2.x) / 2.0, f64::min(p1.y, p2.y)))
790 } else if self.handle1.is_effectively_visible() {
791 Some(self.handle1.indicator_position())
792 } else if self.handle2.is_effectively_visible() {
793 Some(self.handle2.indicator_position())
794 } else {
795 None
796 }
797 } else if self.caret_handle.is_effectively_visible() {
798 Some(self.caret_handle.indicator_position())
799 } else {
800 None
801 };
802
803 let Some(topleft) = topleft else {
804 return false;
805 };
806
807 let old_placement = flyout.placement();
808 let old_callback = flyout.custom_popup_placement_callback();
809 let old_show_mode = flyout.show_mode();
810 let point = self.to_text_box(topleft);
811 let point = point.with_y(f64::max(0.0, point.y));
812
813 let place: CustomPopupPlacementCallback = Rc::new(move |parameters: &mut CustomPopupPlacement| {
814 parameters.set_anchor(PopupAnchor::TOP_LEFT);
815 let offset = parameters.offset;
816 parameters.offset = offset.with_x(point.x).with_y(point.y - vertical_offset);
817 });
818 flyout.set_custom_popup_placement_callback(Some(place));
819 flyout.set_placement(PlacementMode::Custom);
820 flyout.set_show_mode(FlyoutShowMode::Transient);
821
822 text_box.raise_event(&ContextRequestedEventArgs::new());
823
824 flyout.set_placement(old_placement);
825 flyout.set_custom_popup_placement_callback(old_callback);
826 flyout.set_show_mode(old_show_mode);
827
828 true
829 }
830
831 fn text_box_property_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
832 if e.property() == TextPresenter::text_property().as_property() {
833 self.set_show_handles(false);
834 self.close_flyout();
835 } else if e.property() == TextBox::selection_start_property().as_property()
836 || e.property() == TextBox::selection_end_property().as_property()
837 || e.property() == TextBox::caret_index_property().as_property()
838 {
839 self.move_handles_to_selection();
840 }
841 }
842
843 fn presenter_key_down(&self) {
844 self.set_show_handles(false);
845 self.is_in_touch_mode.set(false);
846 }
847
848 #[cfg(test)]
849 pub(crate) fn handles(&self) -> [Ref<TextSelectionHandle>; 3] {
850 [self.caret_handle.clone(), self.handle1.clone(), self.handle2.clone()]
851 }
852
853 #[cfg(test)]
854 pub(crate) fn is_in_touch_mode(&self) -> bool {
855 self.is_in_touch_mode.get()
856 }
857}
858
859struct PresenterVisualListener {
861 presenter: RefCell<Option<WeakRef<TextPresenter>>>,
862 invalidated: RefCell<Option<Rc<dyn Fn()>>>,
863 disposables: RefCell<Option<Vec<Rc<dyn IDisposable>>>>,
864}
865
866impl PresenterVisualListener {
867 fn new() -> Self {
868 Self { presenter: RefCell::new(None), invalidated: RefCell::new(None), disposables: RefCell::new(None) }
869 }
870
871 fn set_invalidated(&self, handler: impl Fn() + 'static) {
873 *self.invalidated.borrow_mut() = Some(Rc::new(handler));
874 }
875
876 fn attach(&self, presenter: &Ref<TextPresenter>) {
877 if self.presenter.borrow().is_some() {
878 panic!("Listener is already attached to a TextPresenter");
879 }
880
881 self.dispose_all();
882 *self.presenter.borrow_mut() = Some(presenter.downgrade());
883
884 let mut disposables = Vec::new();
885 let mut current: Option<Ref<Visual>> = Some(presenter.clone().upcast());
886 while let Some(visual) = current {
887 self.attach_events(&visual, &mut disposables);
888 current = visual.visual_parent();
889 }
890 *self.disposables.borrow_mut() = Some(disposables);
891 }
892
893 fn attach_events(&self, visual: &Ref<Visual>, disposables: &mut Vec<Rc<dyn IDisposable>>) {
894 let invalidated = self.invalidated.borrow().clone();
895 let on_invalidated = move || {
896 if let Some(invalidated) = &invalidated {
897 invalidated();
898 }
899 };
900
901 if let Some(layoutable) = visual.cast::<Layoutable>() {
902 let on_invalidated = on_invalidated.clone();
903 disposables.push(layoutable.effective_viewport_changed(move |_| on_invalidated()));
904 }
905 if let Some(control) = visual.cast::<Control>() {
906 let token = control.size_changed(move |_, _| on_invalidated());
907 let control = control.downgrade();
908 disposables.push(Disposable::create(move || {
909 if let Some(control) = control.upgrade() {
910 control.remove_handler(Control::size_changed_event(), token);
911 }
912 }));
913 }
914 }
915
916 fn dispose_all(&self) {
917 let disposables = self.disposables.take();
918 for disposable in disposables.into_iter().flatten() {
919 disposable.dispose();
920 }
921 }
922
923 fn detach(&self) {
924 self.dispose_all();
925
926 *self.presenter.borrow_mut() = None;
927 }
928}