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 zoom(&mut self, magnification: f32, new_center: DevicePoint) {
79        const MINIMUM_PINCH_ZOOM: f32 = 1.0;
80        const MAXIMUM_PINCH_ZOOM: f32 = 10.0;
81        let new_factor =
82            (self.zoom_factor * magnification).clamp(MINIMUM_PINCH_ZOOM, MAXIMUM_PINCH_ZOOM);
83        let old_factor = std::mem::replace(&mut self.zoom_factor, new_factor);
84
85        if self.zoom_factor <= 1.0 {
86            self.transform = Transform2D::identity();
87            return;
88        }
89
90        let magnification = self.zoom_factor / old_factor;
91        let transform = self
92            .transform
93            .then_translate(Vector2D::new(-new_center.x, -new_center.y))
94            .then_scale(magnification, magnification)
95            .then_translate(Vector2D::new(new_center.x, new_center.y));
96        self.set_transform(transform);
97    }
98
99    /// Pan the pinch zoom viewoprt by the given [`Scroll`] and if it is a delta,
100    /// modify the delta to reflect the remaining unused scroll delta.
101    pub(crate) fn pan(&mut self, scroll: &mut Scroll, scale: Scale<f32, CSSPixel, DevicePixel>) {
102        let remaining = self.pan_with_device_scroll(*scroll, scale);
103
104        if let Scroll::Delta(delta) = scroll {
105            *delta = remaining.into();
106        }
107    }
108
109    /// Pan the pinch zoom viewport by the given delta and return the remaining device
110    /// pixel value that was unused.
111    pub(crate) fn pan_with_device_scroll(
112        &mut self,
113        scroll: Scroll,
114        scale: Scale<f32, CSSPixel, DevicePixel>,
115    ) -> DeviceVector2D {
116        let current_viewport = Rect::new(
117            Point2D::origin(),
118            self.unscaled_viewport_size.to_vector().to_size(),
119        );
120        let layout_viewport_in_device_pixels =
121            self.transform.outer_transformed_rect(&current_viewport);
122        let max_viewport_offset = -(layout_viewport_in_device_pixels.size -
123            self.unscaled_viewport_size.to_vector().to_size());
124        let max_delta = layout_viewport_in_device_pixels.origin - max_viewport_offset;
125
126        let delta = match scroll {
127            Scroll::Delta(delta) => delta.as_device_vector(scale),
128            Scroll::Start => DeviceVector2D::new(0.0, max_delta.y),
129            Scroll::End => DeviceVector2D::new(0.0, -layout_viewport_in_device_pixels.origin.y),
130        };
131
132        let mut remaining = Vector2D::zero();
133        if delta.x < 0.0 {
134            remaining.x = (delta.x - layout_viewport_in_device_pixels.origin.x).min(0.0);
135        }
136        if delta.y < 0.0 {
137            remaining.y = (delta.y - layout_viewport_in_device_pixels.origin.y).min(0.0);
138        }
139        if delta.x > 0.0 {
140            remaining.x = (delta.x - max_delta.x).max(0.0);
141        }
142        if delta.y > 0.0 {
143            remaining.y = (delta.y - max_delta.y).max(0.0);
144        }
145
146        self.set_transform(
147            self.transform
148                .then_translate(Vector2D::new(-delta.x, -delta.y)),
149        );
150
151        remaining
152    }
153
154    /// Get the [`PinchZoomInfos`] from this [`PinchZoom`] state.
155    pub(crate) fn get_pinch_zoom_infos_for_script(
156        &self,
157        viewport_scale: Scale<f32, CSSPixel, DevicePixel>,
158    ) -> PinchZoomInfos {
159        PinchZoomInfos {
160            zoom_factor: Scale::new(self.zoom_factor),
161            rect: self.pinch_zoom_rect_relative_to_unscaled_viewport() / viewport_scale,
162        }
163    }
164}