Skip to main content

ferroui_base/input/gesture_recognizers/
pinch_gesture_recognizer.rs

1use super::{GestureRecognizer, GestureRecognizerImpl};
2use crate::input::{
3    IPointer, PinchEndedEventArgs, PinchEventArgs, PointerEventArgs, PointerPressedEventArgs,
4    PointerReleasedEventArgs, PointerType,
5};
6use crate::{ferro_class, ferro_impl_classes, instantiate, FerroObjectImpl, Point, Ref, StyledElementImpl, Vector};
7use std::cell::{Cell, RefCell};
8use std::rc::Rc;
9
10fn same_pointer(a: &Option<Rc<dyn IPointer>>, b: &Rc<dyn IPointer>) -> bool {
11    a.as_ref().is_some_and(|a| std::ptr::addr_eq(Rc::as_ptr(a), Rc::as_ptr(b)))
12}
13
14/// Recognizes the pinch gesture: two contacts moving closer together or
15/// further apart, and rotating around each other.
16#[repr(C)]
17pub struct PinchGestureRecognizer {
18    base: GestureRecognizer,
19    initial_distance: Cell<f32>,
20    first_contact: RefCell<Option<Rc<dyn IPointer>>>,
21    first_point: Cell<Point>,
22    second_contact: RefCell<Option<Rc<dyn IPointer>>>,
23    second_point: Cell<Point>,
24    origin: Cell<Point>,
25    previous_angle: Cell<f64>,
26}
27
28ferro_class!(PinchGestureRecognizer: GestureRecognizer);
29crate::ferro_class_info!(PinchGestureRecognizer { new: PinchGestureRecognizer::new });
30ferro_impl_classes!(PinchGestureRecognizer: FerroObjectImpl, StyledElementImpl);
31
32impl GestureRecognizerImpl for PinchGestureRecognizer {
33    fn pointer_capture_lost(this: &Self, pointer: &Rc<dyn IPointer>) {
34        this.remove_contact(pointer);
35    }
36
37    fn pointer_moved(this: &Self, e: &PointerEventArgs) {
38        let Some(target) = this.target() else { return };
39
40        if same_pointer(&this.first_contact.borrow(), e.pointer()) {
41            this.first_point.set(e.get_position(Some(&target)));
42        } else if same_pointer(&this.second_contact.borrow(), e.pointer()) {
43            this.second_point.set(e.get_position(Some(&target)));
44        } else {
45            return;
46        }
47
48        if this.first_contact.borrow().is_some() && this.second_contact.borrow().is_some() {
49            let distance = Self::get_distance(this.first_point.get(), this.second_point.get());
50            let scale = (distance / this.initial_distance.get()) as f64;
51            let degree = Self::get_angle_degree_from_points(this.first_point.get(), this.second_point.get());
52
53            let pinch_event_args =
54                PinchEventArgs::with_angle(scale, this.origin.get(), degree, this.previous_angle.get() - degree);
55            this.previous_angle.set(degree);
56            target.raise_event(&pinch_event_args);
57
58            e.set_handled(pinch_event_args.handled());
59            e.prevent_gesture_recognition();
60        }
61    }
62
63    fn pointer_pressed(this: &Self, e: &PointerPressedEventArgs) {
64        let Some(target) = this.target() else { return };
65
66        if e.pointer().type_() != PointerType::Touch && e.pointer().type_() != PointerType::Pen {
67            return;
68        }
69
70        if this.first_contact.borrow().is_none() {
71            *this.first_contact.borrow_mut() = Some(e.pointer().clone());
72            this.first_point.set(e.get_position(Some(&target)));
73            return;
74        } else if this.second_contact.borrow().is_none() && !same_pointer(&this.first_contact.borrow(), e.pointer()) {
75            *this.second_contact.borrow_mut() = Some(e.pointer().clone());
76            this.second_point.set(e.get_position(Some(&target)));
77        } else {
78            return;
79        }
80
81        let first = this.first_contact.borrow().clone();
82        let second = this.second_contact.borrow().clone();
83
84        if let (Some(first), Some(second)) = (first, second) {
85            let (first_point, second_point) = (this.first_point.get(), this.second_point.get());
86            this.initial_distance.set(Self::get_distance(first_point, second_point));
87
88            this.origin
89                .set(Point::new((first_point.x + second_point.x) / 2.0, (first_point.y + second_point.y) / 2.0));
90
91            this.previous_angle.set(Self::get_angle_degree_from_points(first_point, second_point));
92
93            this.capture(&first);
94            this.capture(&second);
95            e.prevent_gesture_recognition();
96        }
97    }
98
99    fn pointer_released(this: &Self, e: &PointerReleasedEventArgs) {
100        if this.remove_contact(e.pointer()) {
101            e.prevent_gesture_recognition();
102        }
103    }
104}
105
106impl PinchGestureRecognizer {
107    pub fn new() -> Ref<Self> {
108        instantiate(Self {
109            base: GestureRecognizer::construct(),
110            initial_distance: Cell::new(0.0),
111            first_contact: RefCell::new(None),
112            first_point: Cell::new(Point::default()),
113            second_contact: RefCell::new(None),
114            second_point: Cell::new(Point::default()),
115            origin: Cell::new(Point::default()),
116            previous_angle: Cell::new(0.0),
117        })
118    }
119
120    fn remove_contact(&self, pointer: &Rc<dyn IPointer>) -> bool {
121        let is_first = same_pointer(&self.first_contact.borrow(), pointer);
122        let is_second = same_pointer(&self.second_contact.borrow(), pointer);
123
124        if is_first || is_second {
125            if is_second {
126                drop(self.second_contact.replace(None));
127            }
128
129            if is_first {
130                let second = self.second_contact.replace(None);
131                drop(self.first_contact.replace(second));
132            }
133
134            if let Some(target) = self.target() {
135                target.raise_event(&PinchEndedEventArgs::new());
136            }
137
138            return true;
139        }
140
141        false
142    }
143
144    fn get_distance(a: Point, b: Point) -> f32 {
145        let length = b - a;
146        Vector::new(length.x, length.y).length() as f32
147    }
148
149    fn get_angle_degree_from_points(a: Point, b: Point) -> f64 {
150        let delta_x = a.x - b.x;
151        // The sign is reversed, because on the screen the Y axes are
152        // reversed with respect to the Cartesian plane.
153        let delta_y = -(a.y - b.y);
154        // radians from -pi to +pi
155        let rad = delta_x.atan2(delta_y);
156        // atan2 returns a value in degrees from -180 to +180; to get the
157        // angle between 0 and 360 degrees add 180 degrees.
158        (rad * (180.0 / std::f64::consts::PI)) + 180.0
159    }
160}