Skip to main content

ferroui_controls/primitives/
text_selection_handle.rs

1use super::{
2    SelectionHandleType, TemplateAppliedEventArgs, TemplatedControlImpl, TemplatedControlImplExt, Thumb, ThumbImpl,
3    ThumbImplExt,
4};
5use crate::{Border, Canvas, ControlImpl, ControlImplExt, SizeChangedEventArgs};
6use ferroui_base::input::{
7    InputElement, InputElementImpl, InputElementImplExt, PointerCaptureLostEventArgs, PointerEventArgs,
8    PointerPressedEventArgs, PointerReleasedEventArgs, VectorEventArgs,
9};
10use ferroui_base::interactivity::{InteractiveImpl, RoutedEventArgs, RoutingStrategies};
11use ferroui_base::layout::{LayoutableImpl, LayoutableImplExt};
12use ferroui_base::reactive::IDisposable;
13use ferroui_base::{
14    ferro_class, ferro_class_info, ferro_impl_classes, instantiate, FerroObjectImpl, Point, Rect, Ref,
15    StyledElementImpl, Vector, VisualImpl,
16};
17use std::cell::{Cell, RefCell};
18use std::rc::Rc;
19
20const PC_PRESSED: &str = ":pressed";
21const PC_CARET: &str = ":caret";
22const PC_START: &str = ":start";
23const PC_END: &str = ":end";
24
25/// A control that enables easy control over text selection using touch
26/// based input.
27#[repr(C)]
28pub struct TextSelectionHandle {
29    base: Thumb,
30    selection_handle_type: Cell<SelectionHandleType>,
31    is_rtl: Cell<bool>,
32    is_dragging: Cell<bool>,
33    needs_indicator_update: Cell<bool>,
34    start_position: Cell<Point>,
35    delta: Cell<Vector>,
36    last_point: Cell<Option<Point>>,
37    indicator: RefCell<Option<(Ref<Border>, Rc<dyn IDisposable>)>>,
38    last_requested_position: Cell<Option<Point>>,
39}
40
41ferro_class!(TextSelectionHandle: Thumb);
42
43ferro_class_info!(TextSelectionHandle {
44    new: TextSelectionHandle::new,
45    markup: {
46        attributes: [TemplatePart("PART_Indicator", type(Ref<Border>))],
47    },
48});
49
50ferro_impl_classes!(TextSelectionHandle: FerroObjectImpl, StyledElementImpl, VisualImpl, InteractiveImpl);
51
52impl TemplatedControlImpl for TextSelectionHandle {
53    fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
54        Self::parent_on_apply_template(this, e);
55
56        if let Some((_, layout_updated)) = this.indicator.take() {
57            layout_updated.dispose();
58        }
59
60        let indicator = e.name_scope().get_as::<Border>("PART_Indicator");
61
62        let weak = this.to_ref().downgrade();
63        let layout_updated = indicator.layout_updated(move || {
64            if let Some(this) = weak.upgrade() {
65                this.indicator_layout_updated();
66            }
67        });
68        *this.indicator.borrow_mut() = Some((indicator, layout_updated));
69
70        this.update_handle_classes();
71    }
72}
73
74impl ControlImpl for TextSelectionHandle {
75    fn on_size_changed(this: &Self, e: &SizeChangedEventArgs) {
76        Self::parent_on_size_changed(this, e);
77
78        if let Some(last_requested_position) = this.last_requested_position.get() {
79            this.set_top_left(last_requested_position);
80            this.last_requested_position.set(None);
81            this.needs_indicator_update.set(false);
82        }
83    }
84
85    fn on_loaded(this: &Self, args: &RoutedEventArgs) {
86        Self::parent_on_loaded(this, args);
87
88        this.update_handle_classes();
89
90        this.invalidate_visual();
91    }
92}
93
94impl ThumbImpl for TextSelectionHandle {
95    fn on_drag_started(this: &Self, e: &VectorEventArgs) {
96        Self::parent_on_drag_started(this, e);
97
98        this.start_position.set(this.get_top_left());
99        this.delta.set(Vector::default());
100        this.is_dragging.set(true);
101    }
102
103    fn on_drag_delta(this: &Self, e: &VectorEventArgs) {
104        Self::parent_on_drag_delta(this, e);
105        let new_delta = e.vector;
106
107        if !e.handled() && (new_delta - this.delta.get()).length().abs() > 0.0 {
108            this.delta.set(new_delta);
109            let point = this.start_position.get() + new_delta;
110            Canvas::set_top(this, point.y);
111            Canvas::set_left(this, point.x);
112        }
113    }
114
115    fn on_drag_completed(this: &Self, e: &VectorEventArgs) {
116        this.is_dragging.set(false);
117        this.start_position.set(Point::default());
118        Self::parent_on_drag_completed(this, e);
119    }
120}
121
122impl LayoutableImpl for TextSelectionHandle {
123    fn arrange_core(this: &Self, final_rect: Rect) {
124        this.update_handle_classes();
125
126        Self::parent_arrange_core(this, final_rect);
127    }
128}
129
130impl InputElementImpl for TextSelectionHandle {
131    fn on_pointer_capture_lost(this: &Self, e: &PointerCaptureLostEventArgs) {
132        if let Some(last_point) = this.last_point.get() {
133            let mut ev = VectorEventArgs::new();
134            ev.set_routed_event(Some(Thumb::drag_completed_event()));
135            ev.vector = last_point.into();
136
137            this.last_point.set(None);
138
139            this.raise_event(&ev);
140        }
141
142        this.pseudo_classes().remove_pseudo(PC_PRESSED);
143
144        Self::parent_on_pointer_capture_lost(this, e);
145    }
146
147    fn on_pointer_moved(this: &Self, e: &PointerEventArgs) {
148        let this_element: Ref<InputElement> = this.to_ref().upcast();
149        if e.pointer().captured() != Some(this_element.clone()) {
150            return;
151        }
152
153        let root = this.visual_root();
154        let mut ev = VectorEventArgs::new();
155
156        match this.last_point.get() {
157            None => {
158                let last_point = e.get_position(root.as_deref());
159                this.last_point.set(Some(last_point));
160                e.pointer().capture(Some(&this_element));
161
162                ev.set_routed_event(Some(Thumb::drag_started_event()));
163                ev.vector = last_point.into();
164            }
165            Some(last_point) => {
166                let vector = e.get_position(root.as_deref()) - last_point;
167
168                ev.set_routed_event(Some(Thumb::drag_delta_event()));
169                ev.vector = vector.into();
170            }
171        }
172
173        this.raise_event(&ev);
174    }
175
176    fn on_pointer_pressed(this: &Self, e: &PointerPressedEventArgs) {
177        this.pseudo_classes().add_pseudo(PC_PRESSED);
178        let this_element: Ref<InputElement> = this.to_ref().upcast();
179        e.pointer().capture(Some(&this_element));
180    }
181
182    fn on_pointer_released(this: &Self, e: &PointerReleasedEventArgs) {
183        if this.last_point.get().is_some() {
184            e.set_handled(true);
185            this.last_point.set(None);
186
187            let mut ev = VectorEventArgs::new();
188            ev.set_routed_event(Some(Thumb::drag_completed_event()));
189            ev.vector = e.get_position(this.visual_root().as_deref()).into();
190
191            this.raise_event(&ev);
192        }
193
194        this.pseudo_classes().remove_pseudo(PC_PRESSED);
195        e.pointer().capture(None);
196    }
197}
198
199impl TextSelectionHandle {
200    fn static_constructor() {
201        Thumb::drag_started_event().add_class_handler_with::<TextSelectionHandle>(
202            |x, e| x.on_drag_started(e),
203            RoutingStrategies::BUBBLE,
204            false,
205        );
206        Thumb::drag_delta_event().add_class_handler_with::<TextSelectionHandle>(
207            |x, e| x.on_drag_delta(e),
208            RoutingStrategies::BUBBLE,
209            false,
210        );
211        Thumb::drag_completed_event().add_class_handler_with::<TextSelectionHandle>(
212            |x, e| x.on_drag_completed(e),
213            RoutingStrategies::BUBBLE,
214            false,
215        );
216    }
217
218    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
219    pub fn construct() -> Self {
220        Self {
221            base: Thumb::construct(),
222            selection_handle_type: Cell::new(SelectionHandleType::Caret),
223            is_rtl: Cell::new(false),
224            is_dragging: Cell::new(false),
225            needs_indicator_update: Cell::new(false),
226            start_position: Cell::new(Point::default()),
227            delta: Cell::new(Vector::default()),
228            last_point: Cell::new(None),
229            indicator: RefCell::new(None),
230            last_requested_position: Cell::new(None),
231        }
232    }
233
234    pub fn new() -> Ref<Self> {
235        instantiate(Self::construct())
236    }
237
238    pub(crate) fn selection_handle_type(&self) -> SelectionHandleType {
239        self.selection_handle_type.get()
240    }
241
242    pub(crate) fn set_selection_handle_type(&self, value: SelectionHandleType) {
243        self.selection_handle_type.set(value);
244        self.update_handle_classes();
245    }
246
247    pub(crate) fn is_rtl(&self) -> bool {
248        self.is_rtl.get()
249    }
250
251    pub(crate) fn set_is_rtl(&self, value: bool) {
252        self.is_rtl.set(value);
253        self.update_handle_classes();
254    }
255
256    pub(crate) fn indicator_position(&self) -> Point {
257        let top_left = self.get_top_left();
258        top_left.with_x(top_left.x + self.get_indicator_offset())
259    }
260
261    pub(crate) fn is_dragging(&self) -> bool {
262        self.is_dragging.get()
263    }
264
265    // The getter of the upstream internal property; only the setter is used by the canvas.
266    #[allow(dead_code)]
267    pub(crate) fn needs_indicator_update(&self) -> bool {
268        self.needs_indicator_update.get()
269    }
270
271    pub(crate) fn set_needs_indicator_update(&self, value: bool) {
272        self.needs_indicator_update.set(value)
273    }
274
275    fn indicator(&self) -> Option<Ref<Border>> {
276        self.indicator.borrow().as_ref().map(|(indicator, _)| indicator.clone())
277    }
278
279    fn indicator_layout_updated(&self) {
280        if self.needs_indicator_update.get() {
281            if let Some(last_requested_position) = self.last_requested_position.get() {
282                self.set_top_left(last_requested_position);
283
284                self.needs_indicator_update.set(!self.indicator().is_none_or(|indicator| indicator.is_arrange_valid()));
285            }
286        }
287    }
288
289    pub(crate) fn set_top_left(&self, point: Point) {
290        if self.indicator.borrow().is_none() || self.needs_indicator_update.get() {
291            self.last_requested_position.set(Some(point));
292        }
293        Canvas::set_top(self, point.y);
294        Canvas::set_left(self, point.x - self.get_indicator_offset());
295    }
296
297    pub(crate) fn get_top_left(&self) -> Point {
298        Point::new(Canvas::get_left(self), Canvas::get_top(self))
299    }
300
301    pub(crate) fn get_indicator_offset(&self) -> f64 {
302        if let Some(indicator) = self.indicator() {
303            return indicator.bounds().center().x;
304        }
305
306        match self.selection_handle_type.get() {
307            SelectionHandleType::Caret => self.bounds().width / 2.0,
308            SelectionHandleType::Start => {
309                if self.is_rtl.get() {
310                    0.0
311                } else {
312                    self.bounds().width
313                }
314            }
315            SelectionHandleType::End => {
316                if self.is_rtl.get() {
317                    self.bounds().width
318                } else {
319                    0.0
320                }
321            }
322        }
323    }
324
325    fn update_handle_classes(&self) {
326        let pseudo_classes = self.pseudo_classes();
327        pseudo_classes.remove_pseudo(PC_CARET);
328        pseudo_classes.remove_pseudo(PC_START);
329        pseudo_classes.remove_pseudo(PC_END);
330
331        pseudo_classes.add_pseudo(match (self.selection_handle_type.get(), self.is_rtl.get()) {
332            (SelectionHandleType::Caret, _) => PC_CARET,
333            (SelectionHandleType::Start, false) | (SelectionHandleType::End, true) => PC_START,
334            (SelectionHandleType::Start, true) | (SelectionHandleType::End, false) => PC_END,
335        });
336        self.invalidate_visual();
337    }
338}