paint_api/
viewport_description.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
5//! This module contains helpers for Viewport
6
7use std::collections::HashMap;
8use std::str::FromStr;
9
10use euclid::Scale;
11use serde::{Deserialize, Serialize};
12use servo_geometry::DeviceIndependentPixel;
13use style_traits::CSSPixel;
14
15/// Default viewport constraints
16///
17/// <https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#initial-scale>
18pub const MIN_PAGE_ZOOM: Scale<f32, CSSPixel, DeviceIndependentPixel> = Scale::new(0.1);
19/// <https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#initial-scale>
20pub const MAX_PAGE_ZOOM: Scale<f32, CSSPixel, DeviceIndependentPixel> = Scale::new(10.0);
21/// <https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#initial-scale>
22pub const DEFAULT_PAGE_ZOOM: Scale<f32, CSSPixel, DeviceIndependentPixel> = Scale::new(1.0);
23
24/// <https://drafts.csswg.org/css-viewport/#parsing-algorithm>
25const SEPARATORS: [char; 2] = [',', ';']; // Comma (0x2c) and Semicolon (0x3b)
26
27/// A set of viewport descriptors:
28///
29/// <https://www.w3.org/TR/css-viewport-1/#viewport-meta>
30#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
31pub struct ViewportDescription {
32    // https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#width
33    // the (minimum width) size of the viewport
34    // TODO: width Needs to be implemented
35    // https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#width
36    // the (minimum height) size of the viewport
37    // TODO: height Needs to be implemented
38    /// <https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#initial-scale>
39    /// the zoom level when the page is first loaded
40    pub initial_scale: Scale<f32, CSSPixel, DeviceIndependentPixel>,
41
42    /// The minimum page zoom that is allowed on this viewport's page.
43    /// <https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#minimum_scale>
44    pub minimum_scale: Scale<f32, CSSPixel, DeviceIndependentPixel>,
45
46    /// The maximum page zoom that is allowed on this viewport's page.
47    /// <https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#maximum_scale>
48    pub maximum_scale: Scale<f32, CSSPixel, DeviceIndependentPixel>,
49
50    /// <https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#user_scalable>
51    /// whether zoom in and zoom out actions are allowed on the page
52    pub user_scalable: UserScalable,
53}
54
55/// The errors that the viewport parsing can generate.
56#[derive(Debug)]
57pub enum ViewportDescriptionParseError {
58    /// When viewport attribute string is empty
59    Empty,
60}
61
62/// A set of User Zoom values:
63#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
64pub enum UserScalable {
65    /// Zoom is not allowed
66    No = 0,
67    /// Zoom is allowed
68    Yes = 1,
69}
70
71/// Parses a viewport user scalable value.
72impl TryFrom<&str> for UserScalable {
73    type Error = &'static str;
74    fn try_from(value: &str) -> Result<Self, Self::Error> {
75        match value.to_lowercase().as_str() {
76            "yes" => Ok(UserScalable::Yes),
77            "no" => Ok(UserScalable::No),
78            _ => match value.parse::<f32>() {
79                Ok(1.0) => Ok(UserScalable::Yes),
80                Ok(0.0) => Ok(UserScalable::No),
81                _ => Err("can't convert character to UserScalable"),
82            },
83        }
84    }
85}
86
87impl Default for ViewportDescription {
88    fn default() -> Self {
89        ViewportDescription {
90            initial_scale: DEFAULT_PAGE_ZOOM,
91            minimum_scale: MIN_PAGE_ZOOM,
92            maximum_scale: MAX_PAGE_ZOOM,
93            user_scalable: UserScalable::Yes,
94        }
95    }
96}
97
98impl ViewportDescription {
99    /// Iterates over the key-value pairs generated from meta tag and returns a ViewportDescription
100    fn process_viewport_key_value_pairs(pairs: HashMap<String, String>) -> ViewportDescription {
101        let mut description = ViewportDescription::default();
102        for (key, value) in &pairs {
103            match key.as_str() {
104                "initial-scale" => {
105                    if let Some(zoom) = Self::parse_viewport_value_as_zoom(value) {
106                        description.initial_scale = zoom;
107                    }
108                },
109                "minimum-scale" => {
110                    if let Some(zoom) = Self::parse_viewport_value_as_zoom(value) {
111                        description.minimum_scale = zoom;
112                    }
113                },
114                "maximum-scale" => {
115                    if let Some(zoom) = Self::parse_viewport_value_as_zoom(value) {
116                        description.maximum_scale = zoom;
117                    }
118                },
119                "user-scalable" => {
120                    if let Ok(user_zoom_allowed) = value.as_str().try_into() {
121                        description.user_scalable = user_zoom_allowed;
122                    }
123                },
124                _ => (),
125            }
126        }
127        description.initial_scale =
128            Scale::new(description.clamp_zoom(description.initial_scale.get()));
129        description
130    }
131
132    /// Parses a viewport zoom value.
133    fn parse_viewport_value_as_zoom(
134        value: &str,
135    ) -> Option<Scale<f32, CSSPixel, DeviceIndependentPixel>> {
136        value
137            .to_lowercase()
138            .as_str()
139            .parse::<f32>()
140            .ok()
141            .filter(|&n| (0.0..=10.0).contains(&n))
142            .map(Scale::new)
143    }
144
145    /// Constrains a zoom value within the allowed scale range
146    pub fn clamp_zoom(&self, zoom: f32) -> f32 {
147        zoom.clamp(self.minimum_scale.get(), self.maximum_scale.get())
148    }
149}
150
151/// <https://drafts.csswg.org/css-viewport/#parsing-algorithm>
152///
153/// This implementation differs from the specified algorithm, but is equivalent because
154/// 1. It uses higher-level string operations to process string instead of character-by-character iteration.
155/// 2. Uses trim() operation to handle whitespace instead of explicitly handling throughout the parsing process.
156impl FromStr for ViewportDescription {
157    type Err = ViewportDescriptionParseError;
158    fn from_str(string: &str) -> Result<Self, Self::Err> {
159        if string.is_empty() {
160            return Err(ViewportDescriptionParseError::Empty);
161        }
162
163        // Parse key-value pairs from the content string
164        // 1. Split the content string using SEPARATORS
165        // 2. Split into key-value pair using "=" and trim whitespaces
166        // 3. Insert into HashMap
167        let parsed_values = string
168            .split(SEPARATORS)
169            .filter_map(|pair| {
170                let mut parts = pair.split('=').map(str::trim);
171                if let (Some(key), Some(value)) = (parts.next(), parts.next()) {
172                    Some((key.to_string(), value.to_string()))
173                } else {
174                    None
175                }
176            })
177            .collect::<HashMap<String, String>>();
178
179        Ok(Self::process_viewport_key_value_pairs(parsed_values))
180    }
181}