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    pub(crate) fn resize_unscaled_viewport(&mut self, webview_rect: DeviceRect) {
38        self.unscaled_viewport_size = webview_rect.size();
39    }
40
41    fn set_transform(&mut self, transform: Transform2D<f32, DevicePixel, DevicePixel>) {
42        let rect = Rect::new(
43            Point2D::origin(),
44            self.unscaled_viewport_size.to_vector().to_size(),
45        )
46        .cast_unit();
47        let mut rect = transform
48            .inverse()
49            .expect("Should always be able to invert provided transform")
50            .outer_transformed_rect(&rect);
51        rect.origin = rect.origin.clamp(
52            Point2D::origin(),
53            (self.unscaled_viewport_size - rect.size)
54                .to_vector()
55                .to_point(),
56        );
57        let scale = self.unscaled_viewport_size.width / rect.width();
58        self.transform = Transform2D::identity()
59            .then_translate(Vector2D::new(-rect.origin.x, -rect.origin.y))
60            .then_scale(scale, scale);
61    }
62
63    pub(crate) fn zoom(&mut self, magnification: f32, new_center: DevicePoint) {
64        const MINIMUM_PINCH_ZOOM: f32 = 1.0;
65        const MAXIMUM_PINCH_ZOOM: f32 = 10.0;
66        let new_factor =
67            (self.zoom_factor * magnification).clamp(MINIMUM_PINCH_ZOOM, MAXIMUM_PINCH_ZOOM);
68        let old_factor = std::mem::replace(&mut self.zoom_factor, new_factor);
69
70        if self.zoom_factor <= 1.0 {
71            self.transform = Transform2D::identity();
72            return;
73        }
74
75        let magnification = self.zoom_factor / old_factor;
76        let transform = self
77            .transform
78            .then_translate(Vector2D::new(-new_center.x, -new_center.y))
79            .then_scale(magnification, magnification)
80            .then_translate(Vector2D::new(new_center.x, new_center.y));
81        self.set_transform(transform);
82    }
83
84    /// Pan the pinch zoom viewoprt by the given [`Scroll`] and if it is a delta,
85    /// modify the delta to reflect the remaining unused scroll delta.
86    pub(crate) fn pan(&mut self, scroll: &mut Scroll, scale: Scale<f32, CSSPixel, DevicePixel>) {
87        let remaining = self.pan_with_device_scroll(*scroll, scale);
88
89        if let Scroll::Delta(delta) = scroll {
90            *delta = remaining.into();
91        }
92    }
93
94    /// Pan the pinch zoom viewport by the given delta and return the remaining device
95    /// pixel value that was unused.
96    pub(crate) fn pan_with_device_scroll(
97        &mut self,
98        scroll: Scroll,
99        scale: Scale<f32, CSSPixel, DevicePixel>,
100    ) -> DeviceVector2D {
101        let current_viewport = Rect::new(
102            Point2D::origin(),
103            self.unscaled_viewport_size.to_vector().to_size(),
104        );
105        let layout_viewport_in_device_pixels =
106            self.transform.outer_transformed_rect(&current_viewport);
107        let max_viewport_offset = -(layout_viewport_in_device_pixels.size -
108            self.unscaled_viewport_size.to_vector().to_size());
109        let max_delta = layout_viewport_in_device_pixels.origin - max_viewport_offset;
110
111        let delta = match scroll {
112            Scroll::Delta(delta) => delta.as_device_vector(scale),
113            Scroll::Start => DeviceVector2D::new(0.0, max_delta.y),
114            Scroll::End => DeviceVector2D::new(0.0, -layout_viewport_in_device_pixels.origin.y),
115        };
116
117        let mut remaining = Vector2D::zero();
118        if delta.x < 0.0 {
119            remaining.x = (delta.x - layout_viewport_in_device_pixels.origin.x).min(0.0);
120        }
121        if delta.y < 0.0 {
122            remaining.y = (delta.y - layout_viewport_in_device_pixels.origin.y).min(0.0);
123        }
124        if delta.x > 0.0 {
125            remaining.x = (delta.x - max_delta.x).max(0.0);
126        }
127        if delta.y > 0.0 {
128            remaining.y = (delta.y - max_delta.y).max(0.0);
129        }
130
131        self.set_transform(
132            self.transform
133                .then_translate(Vector2D::new(-delta.x, -delta.y)),
134        );
135
136        remaining
137    }
138}