servo-paint-api 0.1.0

A component of the servo web-engine.
Documentation
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

//! This module contains helpers for Viewport

use std::collections::HashMap;
use std::str::FromStr;

use euclid::Scale;
use serde::{Deserialize, Serialize};
use servo_geometry::DeviceIndependentPixel;
use style_traits::CSSPixel;

/// Default viewport constraints
///
/// <https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#initial-scale>
pub const MIN_PAGE_ZOOM: Scale<f32, CSSPixel, DeviceIndependentPixel> = Scale::new(0.1);
/// <https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#initial-scale>
pub const MAX_PAGE_ZOOM: Scale<f32, CSSPixel, DeviceIndependentPixel> = Scale::new(10.0);
/// <https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#initial-scale>
pub const DEFAULT_PAGE_ZOOM: Scale<f32, CSSPixel, DeviceIndependentPixel> = Scale::new(1.0);

/// <https://drafts.csswg.org/css-viewport/#parsing-algorithm>
const SEPARATORS: [char; 2] = [',', ';']; // Comma (0x2c) and Semicolon (0x3b)

/// A set of viewport descriptors:
///
/// <https://www.w3.org/TR/css-viewport-1/#viewport-meta>
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct ViewportDescription {
    // https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#width
    // the (minimum width) size of the viewport
    // TODO: width Needs to be implemented
    // https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#width
    // the (minimum height) size of the viewport
    // TODO: height Needs to be implemented
    /// <https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#initial-scale>
    /// the zoom level when the page is first loaded
    pub initial_scale: Scale<f32, CSSPixel, DeviceIndependentPixel>,

    /// The minimum page zoom that is allowed on this viewport's page.
    /// <https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#minimum_scale>
    pub minimum_scale: Scale<f32, CSSPixel, DeviceIndependentPixel>,

    /// The maximum page zoom that is allowed on this viewport's page.
    /// <https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#maximum_scale>
    pub maximum_scale: Scale<f32, CSSPixel, DeviceIndependentPixel>,

    /// <https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#user_scalable>
    /// whether zoom in and zoom out actions are allowed on the page
    pub user_scalable: UserScalable,
}

/// The errors that the viewport parsing can generate.
#[derive(Debug)]
pub enum ViewportDescriptionParseError {
    /// When viewport attribute string is empty
    Empty,
}

/// A set of User Zoom values:
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub enum UserScalable {
    /// Zoom is not allowed
    No = 0,
    /// Zoom is allowed
    Yes = 1,
}

/// Parses a viewport user scalable value.
impl TryFrom<&str> for UserScalable {
    type Error = &'static str;
    fn try_from(value: &str) -> Result<Self, Self::Error> {
        match value.to_lowercase().as_str() {
            "yes" => Ok(UserScalable::Yes),
            "no" => Ok(UserScalable::No),
            _ => match value.parse::<f32>() {
                Ok(1.0) => Ok(UserScalable::Yes),
                Ok(0.0) => Ok(UserScalable::No),
                _ => Err("can't convert character to UserScalable"),
            },
        }
    }
}

impl Default for ViewportDescription {
    fn default() -> Self {
        ViewportDescription {
            initial_scale: DEFAULT_PAGE_ZOOM,
            minimum_scale: MIN_PAGE_ZOOM,
            maximum_scale: MAX_PAGE_ZOOM,
            user_scalable: UserScalable::Yes,
        }
    }
}

impl ViewportDescription {
    /// Iterates over the key-value pairs generated from meta tag and returns a ViewportDescription
    fn process_viewport_key_value_pairs(pairs: HashMap<String, String>) -> ViewportDescription {
        let mut description = ViewportDescription::default();
        for (key, value) in &pairs {
            match key.as_str() {
                "initial-scale" => {
                    if let Some(zoom) = Self::parse_viewport_value_as_zoom(value) {
                        description.initial_scale = zoom;
                    }
                },
                "minimum-scale" => {
                    if let Some(zoom) = Self::parse_viewport_value_as_zoom(value) {
                        description.minimum_scale = zoom;
                    }
                },
                "maximum-scale" => {
                    if let Some(zoom) = Self::parse_viewport_value_as_zoom(value) {
                        description.maximum_scale = zoom;
                    }
                },
                "user-scalable" => {
                    if let Ok(user_zoom_allowed) = value.as_str().try_into() {
                        description.user_scalable = user_zoom_allowed;
                    }
                },
                _ => (),
            }
        }
        description.initial_scale =
            Scale::new(description.clamp_zoom(description.initial_scale.get()));
        description
    }

    /// Parses a viewport zoom value.
    fn parse_viewport_value_as_zoom(
        value: &str,
    ) -> Option<Scale<f32, CSSPixel, DeviceIndependentPixel>> {
        value
            .to_lowercase()
            .as_str()
            .parse::<f32>()
            .ok()
            .filter(|&n| (0.0..=10.0).contains(&n))
            .map(Scale::new)
    }

    /// Constrains a zoom value within the allowed scale range
    pub fn clamp_zoom(&self, zoom: f32) -> f32 {
        zoom.clamp(self.minimum_scale.get(), self.maximum_scale.get())
    }
}

/// <https://drafts.csswg.org/css-viewport/#parsing-algorithm>
///
/// This implementation differs from the specified algorithm, but is equivalent because
/// 1. It uses higher-level string operations to process string instead of character-by-character iteration.
/// 2. Uses trim() operation to handle whitespace instead of explicitly handling throughout the parsing process.
impl FromStr for ViewportDescription {
    type Err = ViewportDescriptionParseError;
    fn from_str(string: &str) -> Result<Self, Self::Err> {
        if string.is_empty() {
            return Err(ViewportDescriptionParseError::Empty);
        }

        // Parse key-value pairs from the content string
        // 1. Split the content string using SEPARATORS
        // 2. Split into key-value pair using "=" and trim whitespaces
        // 3. Insert into HashMap
        let parsed_values = string
            .split(SEPARATORS)
            .filter_map(|pair| {
                let mut parts = pair.split('=').map(str::trim);
                if let (Some(key), Some(value)) = (parts.next(), parts.next()) {
                    Some((key.to_string(), value.to_string()))
                } else {
                    None
                }
            })
            .collect::<HashMap<String, String>>();

        Ok(Self::process_viewport_key_value_pairs(parsed_values))
    }
}