Skip to main content

ferroui_controls/primitives/
text_selection_canvas.rs

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
24/// Some platforms do not allow the handles to move over each other, that
25/// is, to reverse the bounds of the selection.
26const SHOULD_WRAP_AROUND_SELECTION: bool = !cfg!(target_os = "android");
27
28const CONTEXT_MENU_PADDING: f64 = 16.0;
29
30/// The handlers the canvas has on its presenter and on the text box of the
31/// presenter.
32struct 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/// The canvas of the touch selection handles of a text presenter. It lives
42/// in the text selector layer of the top level.
43#[repr(C)]
44pub struct TextSelectionHandleCanvas {
45    base: Canvas,
46    caret_handle: Ref<TextSelectionHandle>,
47    handle1: Ref<TextSelectionHandle>,
48    handle2: Ref<TextSelectionHandle>,
49    // The presenter owns the canvas: the canvas refers to it (and to its
50    // text box) weakly.
51    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        // The sender of the handlers of upstream is the handle a handler is
91        // attached to.
92        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    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
182    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            // Show flyout if there's a selection
327            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        // Some platforms do not allow handles to cause selection bounds to
421        // reverse, i.e. allow handles to move over each other.
422        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            // For platforms that do, update the handle types for each and
433            // adjust selection.
434            let last_position =
435                || textbox.text().map_or(0, |text| text.encode_utf16().count() as i32 - 1);
436            // The saved bounds are those of the start of the drag: there is
437            // no drag delta without a drag start. An absent bound is not
438            // assigned.
439            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                // If handle was previously the selection end, set new
447                // selection end to the previous selection start.
448                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                // If handle was previously the selection start, set new
475                // selection start to the previous selection end.
476                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    /// The position of a handle for the character at `index` and whether the
574    /// run of the character is right to left.
575    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        // A presenter that is gone took its handlers with it; the listener
614        // and the handlers of the text box are released all the same.
615        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) = &current {
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        // If visibility changes, reset the canvas visibility states.
739        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
859/// Listener to layout changes for presenter.
860struct 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    /// Sets the one handler of the `Invalidated` event.
872    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}