paint/
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 paint_api::PinchZoomInfos;
8use style_traits::CSSPixel;
9use webrender_api::units::{DevicePixel, DevicePoint, DeviceRect, DeviceSize, DeviceVector2D};
10
11/// A [`PinchZoom`] describes the pinch zoom viewport of a `WebView`. This is used to
12/// track the current pinch zoom transformation and to clamp all pinching and panning
13/// to the unscaled `WebView` viewport.
14#[derive(Clone, Copy, Debug, PartialEq)]
15pub(crate) struct PinchZoom {
16    zoom_factor: f32,
17    transform: Transform2D<f32, DevicePixel, DevicePixel>,
18    unscaled_viewport_size: DeviceSize,
19}
20
21impl PinchZoom {
22    pub(crate) fn new(webview_rect: DeviceRect) -> Self {
23        Self {
24            zoom_factor: 1.0,
25            unscaled_viewport_size: webview_rect.size(),
26            transform: Transform2D::identity(),
27        }
28    }
29
30    pub(crate) fn transform(&self) -> Transform2D<f32, DevicePixel, DevicePixel> {
31        self.transform
32    }
33
34    pub(crate) fn zoom_factor(&self) -> Scale<f32, DevicePixel, DevicePixel> {
35        Scale::new(self.zoom_factor)
36    }
37
38    pub(crate) fn resize_unscaled_viewport(&mut self, webview_rect: DeviceRect) {
39        self.unscaled_viewport_size = webview_rect.size();
40    }
41
42    /// The boundary of the pinch zoom viewport relative to the unscaled viewport. For the
43    /// script's `VisualViewport` interface and calculations (such as scroll).
44    fn pinch_zoom_rect_relative_to_unscaled_viewport(&self) -> Rect<f32, DevicePixel> {
45        let rect = Rect::new(
46            Point2D::origin(),
47            self.unscaled_viewport_size.to_vector().to_size(),
48        )
49        .cast_unit();
50        self.transform
51            .inverse()
52            .expect("Should always be able to invert provided transform")
53            .outer_transformed_rect(&rect)
54    }
55
56    fn set_transform(&mut self, transform: Transform2D<f32, DevicePixel, DevicePixel>) {
57        let rect = Rect::new(
58            Point2D::origin(),
59            self.unscaled_viewport_size.to_vector().to_size(),
60        )
61        .cast_unit();
62        let mut rect = transform
63            .inverse()
64            .expect("Should always be able to invert provided transform")
65            .outer_transformed_rect(&rect);
66        rect.origin = rect.origin.clamp(
67            Point2D::origin(),
68            (self.unscaled_viewport_size - rect.size)
69                .to_vector()
70                .to_point(),
71        );
72        let scale = self.unscaled_viewport_size.width / rect.width();
73        self.transform = Transform2D::identity()
74            .then_translate(Vector2D::new(-rect.origin.x, -rect.origin.y))
75            .then_scale(scale, scale);
76    }
77
78    pub(crate) fn set_zoom(&mut self, new_factor: f32, new_center: DevicePoint) {
79        if new_factor <= 1.0 {
80            self.zoom_factor = 1.0; // Update the zoom factor to 1.0 to avoid precision issues when zooming back in after zooming out fully.
81            self.transform = Transform2D::identity();
82            return;
83        }
84
85        let old_factor = std::mem::replace(&mut self.zoom_factor, new_factor);
86
87        let magnification = self.zoom_factor / old_factor;
88        let transform = self
89            .transform
90            .then_translate(Vector2D::new(-new_center.x, -new_center.y))
91            .then_scale(magnification, magnification)
92            .then_translate(Vector2D::new(new_center.x, new_center.y));
93        self.set_transform(transform);
94    }
95
96    /// Pan the pinch zoom viewoprt by the given [`Scroll`] and if it is a delta,
97    /// modify the delta to reflect the remaining unused scroll delta.
98    pub(crate) fn pan(&mut self, scroll: &mut Scroll, scale: Scale<f32, CSSPixel, DevicePixel>) {
99        let remaining = self.pan_with_device_scroll(*scroll, scale);
100
101        if let Scroll::Delta(delta) = scroll {
102            *delta = remaining.into();
103        }
104    }
105
106    /// Pan the pinch zoom viewport by the given delta and return the remaining device
107    /// pixel value that was unused.
108    pub(crate) fn pan_with_device_scroll(
109        &mut self,
110        scroll: Scroll,
111        scale: Scale<f32, CSSPixel, DevicePixel>,
112    ) -> DeviceVector2D {
113        let current_viewport = Rect::new(
114            Point2D::origin(),
115            self.unscaled_viewport_size.to_vector().to_size(),
116        );
117        let layout_viewport_in_device_pixels =
118            self.transform.outer_transformed_rect(&current_viewport);
119        let max_viewport_offset = -(layout_viewport_in_device_pixels.size -
120            self.unscaled_viewport_size.to_vector().to_size());
121        let max_delta = layout_viewport_in_device_pixels.origin - max_viewport_offset;
122
123        let delta = match scroll {
124            Scroll::Delta(delta) => delta.as_device_vector(scale),
125            Scroll::Start => DeviceVector2D::new(0.0, max_delta.y),
126            Scroll::End => DeviceVector2D::new(0.0, -layout_viewport_in_device_pixels.origin.y),
127        };
128
129        let mut remaining = Vector2D::zero();
130        if delta.x < 0.0 {
131            remaining.x = (delta.x - layout_viewport_in_device_pixels.origin.x).min(0.0);
132        }
133        if delta.y < 0.0 {
134            remaining.y = (delta.y - layout_viewport_in_device_pixels.origin.y).min(0.0);
135        }
136        if delta.x > 0.0 {
137            remaining.x = (delta.x - max_delta.x).max(0.0);
138        }
139        if delta.y > 0.0 {
140            remaining.y = (delta.y - max_delta.y).max(0.0);
141        }
142
143        self.set_transform(
144            self.transform
145                .then_translate(Vector2D::new(-delta.x, -delta.y)),
146        );
147
148        remaining
149    }
150
151    /// Get the [`PinchZoomInfos`] from this [`PinchZoom`] state.
152    pub(crate) fn get_pinch_zoom_infos_for_script(
153        &self,
154        viewport_scale: Scale<f32, CSSPixel, DevicePixel>,
155    ) -> PinchZoomInfos {
156        PinchZoomInfos {
157            zoom_factor: Scale::new(self.zoom_factor),
158            rect: self.pinch_zoom_rect_relative_to_unscaled_viewport() / viewport_scale,
159        }
160    }
161}