compositing/
pinch_zoom.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use embedder_traits::Scroll;
6use euclid::{Point2D, Rect, Scale, Transform2D, Vector2D};
7use style_traits::CSSPixel;
8use webrender_api::units::{DevicePixel, DevicePoint, DeviceRect, DeviceSize, DeviceVector2D};
9
10/// A [`PinchZoom`] describes the pinch zoom viewport of a `WebView`. This is used to
11/// track the current pinch zoom transformation and to clamp all pinching and panning
12/// to the unscaled `WebView` viewport.
13#[derive(Clone, Copy, Debug, PartialEq)]
14pub(crate) struct PinchZoom {
15    zoom_factor: f32,
16    transform: Transform2D<f32, DevicePixel, DevicePixel>,
17    unscaled_viewport_size: DeviceSize,
18}
19
20impl PinchZoom {
21    pub(crate) fn new(webview_rect: DeviceRect) -> Self {
22        Self {
23            zoom_factor: 1.0,
24            unscaled_viewport_size: webview_rect.size(),
25            transform: Transform2D::identity(),
26        }
27    }
28
29    pub(crate) fn transform(&self) -> Transform2D<f32, DevicePixel, DevicePixel> {
30        self.transform
31    }
32
33    pub(crate) fn zoom_factor(&self) -> Scale<f32, DevicePixel, DevicePixel> {
34        Scale::new(self.zoom_factor)
35    }
36
37    fn set_transform(&mut self, transform: Transform2D<f32, DevicePixel, DevicePixel>) {
38        let rect = Rect::new(
39            Point2D::origin(),
40            self.unscaled_viewport_size.to_vector().to_size(),
41        )
42        .cast_unit();
43        let mut rect = transform
44            .inverse()
45            .expect("Should always be able to invert provided transform")
46            .outer_transformed_rect(&rect);
47        rect.origin = rect.origin.clamp(
48            Point2D::origin(),
49            (self.unscaled_viewport_size - rect.size)
50                .to_vector()
51                .to_point(),
52        );
53        let scale = self.unscaled_viewport_size.width / rect.width();
54        self.transform = Transform2D::identity()
55            .then_translate(Vector2D::new(-rect.origin.x, -rect.origin.y))
56            .then_scale(scale, scale);
57    }
58
59    pub(crate) fn zoom(&mut self, magnification: f32, new_center: DevicePoint) {
60        const MINIMUM_PINCH_ZOOM: f32 = 1.0;
61        const MAXIMUM_PINCH_ZOOM: f32 = 10.0;
62        let new_factor =
63            (self.zoom_factor * magnification).clamp(MINIMUM_PINCH_ZOOM, MAXIMUM_PINCH_ZOOM);
64        let old_factor = std::mem::replace(&mut self.zoom_factor, new_factor);
65
66        if self.zoom_factor <= 1.0 {
67            self.transform = Transform2D::identity();
68            return;
69        }
70
71        let magnification = self.zoom_factor / old_factor;
72        let transform = self
73            .transform
74            .then_translate(Vector2D::new(-new_center.x, -new_center.y))
75            .then_scale(magnification, magnification)
76            .then_translate(Vector2D::new(new_center.x, new_center.y));
77        self.set_transform(transform);
78    }
79
80    /// Pan the pinch zoom viewoprt by the given [`Scroll`] and if it is a delta,
81    /// modify the delta to reflect the remaining unused scroll delta.
82    pub(crate) fn pan(&mut self, scroll: &mut Scroll, scale: Scale<f32, CSSPixel, DevicePixel>) {
83        let remaining = self.pan_with_device_scroll(*scroll, scale);
84
85        if let Scroll::Delta(delta) = scroll {
86            *delta = remaining.into();
87        }
88    }
89
90    /// Pan the pinch zoom viewport by the given delta and return the remaining device
91    /// pixel value that was unused.
92    pub(crate) fn pan_with_device_scroll(
93        &mut self,
94        scroll: Scroll,
95        scale: Scale<f32, CSSPixel, DevicePixel>,
96    ) -> DeviceVector2D {
97        let current_viewport = Rect::new(
98            Point2D::origin(),
99            self.unscaled_viewport_size.to_vector().to_size(),
100        );
101        let layout_viewport_in_device_pixels =
102            self.transform.outer_transformed_rect(&current_viewport);
103        let max_viewport_offset = -(layout_viewport_in_device_pixels.size -
104            self.unscaled_viewport_size.to_vector().to_size());
105        let max_delta = layout_viewport_in_device_pixels.origin - max_viewport_offset;
106
107        let delta = match scroll {
108            Scroll::Delta(delta) => delta.as_device_vector(scale),
109            Scroll::Start => DeviceVector2D::new(0.0, max_delta.y),
110            Scroll::End => DeviceVector2D::new(0.0, -layout_viewport_in_device_pixels.origin.y),
111        };
112
113        let mut remaining = Vector2D::zero();
114        if delta.x < 0.0 {
115            remaining.x = (delta.x - layout_viewport_in_device_pixels.origin.x).min(0.0);
116        }
117        if delta.y < 0.0 {
118            remaining.y = (delta.y - layout_viewport_in_device_pixels.origin.y).min(0.0);
119        }
120        if delta.x > 0.0 {
121            remaining.x = (delta.x - max_delta.x).max(0.0);
122        }
123        if delta.y > 0.0 {
124            remaining.y = (delta.y - max_delta.y).max(0.0);
125        }
126
127        self.set_transform(
128            self.transform
129                .then_translate(Vector2D::new(-delta.x, -delta.y)),
130        );
131
132        remaining
133    }
134}