diff --git a/components/compositing/compositor.rs b/components/compositing/compositor.rs index 0acbbec977a..a4afeb01b3c 100644 --- a/components/compositing/compositor.rs +++ b/components/compositing/compositor.rs @@ -973,6 +973,11 @@ impl IOCompositor { warn!("Sending response to get screen size failed ({error:?})."); } }, + CompositorMsg::Viewport(webview_id, viewport_description) => { + if let Some(webview) = self.webview_renderers.get_mut(webview_id) { + webview.set_viewport_description(viewport_description); + } + }, } } diff --git a/components/compositing/tracing.rs b/components/compositing/tracing.rs index a8bb8b42bb8..65f9bd76c08 100644 --- a/components/compositing/tracing.rs +++ b/components/compositing/tracing.rs @@ -59,6 +59,7 @@ mod from_constellation { Self::GetScreenSize(..) => target!("GetScreenSize"), Self::GetAvailableScreenSize(..) => target!("GetAvailableScreenSize"), Self::CollectMemoryReport(..) => target!("CollectMemoryReport"), + Self::Viewport(..) => target!("Viewport"), } } } diff --git a/components/compositing/webview_renderer.rs b/components/compositing/webview_renderer.rs index 84d5fd877e6..dfe1289ef12 100644 --- a/components/compositing/webview_renderer.rs +++ b/components/compositing/webview_renderer.rs @@ -8,6 +8,9 @@ use std::collections::{HashMap, VecDeque}; use std::rc::Rc; use base::id::{PipelineId, WebViewId}; +use compositing_traits::viewport_description::{ + DEFAULT_ZOOM, MAX_ZOOM, MIN_ZOOM, ViewportDescription, +}; use compositing_traits::{SendableFrameTree, WebViewTrait}; use constellation_traits::{EmbedderToConstellationMessage, ScrollState, WindowSizeType}; use embedder_traits::{ @@ -28,10 +31,6 @@ use webrender_api::{ExternalScrollId, HitTestFlags, ScrollLocation}; use crate::compositor::{HitTestError, PipelineDetails, ServoRenderer}; use crate::touch::{TouchHandler, TouchMoveAction, TouchMoveAllowed, TouchSequenceState}; -// Default viewport constraints -const MAX_ZOOM: f32 = 8.0; -const MIN_ZOOM: f32 = 0.1; - #[derive(Clone, Copy)] struct ScrollEvent { /// Scroll by this offset, or to Start or End @@ -44,8 +43,10 @@ struct ScrollEvent { #[derive(Clone, Copy)] enum ScrollZoomEvent { - /// An pinch zoom event that magnifies the view by the given factor. + /// A pinch zoom event that magnifies the view by the given factor. PinchZoom(f32), + /// A zoom event that magnifies the view by the factor parsed from meta tag. + ViewportZoom(f32), /// A scroll event that scrolls the scroll node at the given location by the /// given amount. Scroll(ScrollEvent), @@ -58,7 +59,7 @@ pub(crate) struct ScrollResult { pub offset: LayoutVector2D, } -#[derive(PartialEq)] +#[derive(Debug, PartialEq)] pub(crate) enum PinchZoomResult { DidPinchZoom, DidNotPinchZoom, @@ -89,9 +90,6 @@ pub(crate) struct WebViewRenderer { pub page_zoom: Scale, /// "Mobile-style" zoom that does not reflow the page. viewport_zoom: PinchZoomFactor, - /// Viewport zoom constraints provided by @viewport. - min_viewport_zoom: Option, - max_viewport_zoom: Option, /// The HiDPI scale factor for the `WebView` associated with this renderer. This is controlled /// by the embedding layer. hidpi_scale_factor: Scale, @@ -102,6 +100,8 @@ pub(crate) struct WebViewRenderer { pending_point_input_events: RefCell>, /// WebRender is not ready between `SendDisplayList` and `WebRenderFrameReady` messages. pub webrender_frame_ready: Cell, + /// Viewport Description + viewport_description: Option, } impl Drop for WebViewRenderer { @@ -131,13 +131,12 @@ impl WebViewRenderer { global, pending_scroll_zoom_events: Default::default(), page_zoom: Scale::new(1.0), - viewport_zoom: PinchZoomFactor::new(1.0), - min_viewport_zoom: Some(PinchZoomFactor::new(1.0)), - max_viewport_zoom: None, + viewport_zoom: PinchZoomFactor::new(DEFAULT_ZOOM), hidpi_scale_factor: Scale::new(hidpi_scale_factor.0), animating: false, pending_point_input_events: Default::default(), webrender_frame_ready: Cell::default(), + viewport_description: None, } } @@ -809,12 +808,15 @@ impl WebViewRenderer { // Batch up all scroll events into one, or else we'll do way too much painting. let mut combined_scroll_event: Option = None; - let mut combined_magnification = 1.0; + let mut combined_magnification = self.pinch_zoom_level().get(); for scroll_event in self.pending_scroll_zoom_events.drain(..) { match scroll_event { ScrollZoomEvent::PinchZoom(magnification) => { combined_magnification *= magnification }, + ScrollZoomEvent::ViewportZoom(magnification) => { + combined_magnification = magnification + }, ScrollZoomEvent::Scroll(scroll_event_info) => { let combined_event = match combined_scroll_event.as_mut() { None => { @@ -865,9 +867,7 @@ impl WebViewRenderer { self.send_scroll_positions_to_layout_for_pipeline(scroll_result.pipeline_id); } - let pinch_zoom_result = match self - .set_pinch_zoom_level(self.pinch_zoom_level().get() * combined_magnification) - { + let pinch_zoom_result = match self.set_pinch_zoom_level(combined_magnification) { true => PinchZoomResult::DidPinchZoom, false => PinchZoomResult::DidNotPinchZoom, }; @@ -941,11 +941,8 @@ impl WebViewRenderer { } fn set_pinch_zoom_level(&mut self, mut zoom: f32) -> bool { - if let Some(min) = self.min_viewport_zoom { - zoom = f32::max(min.get(), zoom); - } - if let Some(max) = self.max_viewport_zoom { - zoom = f32::min(max.get(), zoom); + if let Some(viewport) = self.viewport_description.as_ref() { + zoom = viewport.clamp_zoom(zoom); } let old_zoom = std::mem::replace(&mut self.viewport_zoom, PinchZoomFactor::new(zoom)); @@ -975,7 +972,12 @@ impl WebViewRenderer { // TODO: Scroll to keep the center in view? self.pending_scroll_zoom_events - .push(ScrollZoomEvent::PinchZoom(magnification)); + .push(ScrollZoomEvent::PinchZoom( + self.viewport_description + .clone() + .unwrap_or_default() + .clamp_zoom(magnification), + )); } fn send_window_size_message(&self) { @@ -1042,6 +1044,17 @@ impl WebViewRenderer { let screen_geometry = self.webview.screen_geometry().unwrap_or_default(); (screen_geometry.available_size.to_f32() / self.hidpi_scale_factor).to_i32() } + + pub fn set_viewport_description(&mut self, viewport_description: ViewportDescription) { + self.pending_scroll_zoom_events + .push(ScrollZoomEvent::ViewportZoom( + self.viewport_description + .clone() + .unwrap_or_default() + .clamp_zoom(viewport_description.initial_scale.get()), + )); + self.viewport_description = Some(viewport_description); + } } #[derive(Clone, Copy, Debug, PartialEq)] diff --git a/components/script/dom/htmlmetaelement.rs b/components/script/dom/htmlmetaelement.rs index e94a5e1ff33..4ec6db7212c 100644 --- a/components/script/dom/htmlmetaelement.rs +++ b/components/script/dom/htmlmetaelement.rs @@ -2,6 +2,10 @@ * 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/. */ +use std::str::FromStr; + +use compositing_traits::CompositorMsg; +use compositing_traits::viewport_description::ViewportDescription; use dom_struct::dom_struct; use html5ever::{LocalName, Prefix, local_name, ns}; use js::rust::HandleObject; @@ -61,6 +65,9 @@ impl HTMLMetaElement { if name == "referrer" { self.apply_referrer(); } + if name == "viewport" { + self.parse_and_send_viewport_if_necessary(); + } // https://html.spec.whatwg.org/multipage/#attr-meta-http-equiv } else if !self.HttpEquiv().is_empty() { // TODO: Implement additional http-equiv candidates @@ -115,6 +122,29 @@ impl HTMLMetaElement { } } + /// + fn parse_and_send_viewport_if_necessary(&self) { + // Skip processing if this isn't the top level frame + if !self.owner_window().is_top_level() { + return; + } + let element = self.upcast::(); + let Some(content) = element.get_attribute(&ns!(), &local_name!("content")) else { + return; + }; + + if let Ok(viewport) = ViewportDescription::from_str(&content.value()) { + self.owner_window() + .compositor_api() + .sender() + .send(CompositorMsg::Viewport( + self.owner_window().webview_id(), + viewport, + )) + .unwrap(); + } + } + /// fn apply_csp_list(&self) { if let Some(parent) = self.upcast::().GetParentElement() { diff --git a/components/shared/compositing/lib.rs b/components/shared/compositing/lib.rs index 67ff0046885..8dadf93d628 100644 --- a/components/shared/compositing/lib.rs +++ b/components/shared/compositing/lib.rs @@ -23,6 +23,7 @@ use webrender_api::DocumentId; pub mod display_list; pub mod rendering_context; +pub mod viewport_description; use std::collections::HashMap; use std::sync::{Arc, Mutex}; @@ -42,6 +43,8 @@ use webrender_api::{ ImageKey, NativeFontHandle, PipelineId as WebRenderPipelineId, }; +use crate::viewport_description::ViewportDescription; + /// Sends messages to the compositor. #[derive(Clone)] pub struct CompositorProxy { @@ -176,6 +179,8 @@ pub enum CompositorMsg { /// Measure the current memory usage associated with the compositor. /// The report must be sent on the provided channel once it's complete. CollectMemoryReport(ReportsChan), + /// A top-level frame has parsed a viewport metatag and is sending the new constraints. + Viewport(WebViewId, ViewportDescription), } impl Debug for CompositorMsg { diff --git a/components/shared/compositing/viewport_description.rs b/components/shared/compositing/viewport_description.rs new file mode 100644 index 00000000000..83b29371f84 --- /dev/null +++ b/components/shared/compositing/viewport_description.rs @@ -0,0 +1,175 @@ +/* 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::default::Scale; +use serde::{Deserialize, Serialize}; + +/// Default viewport constraints +/// +/// +pub const MIN_ZOOM: f32 = 0.1; +/// +pub const MAX_ZOOM: f32 = 10.0; +/// +pub const DEFAULT_ZOOM: f32 = 1.0; + +/// +const SEPARATORS: [char; 2] = [',', ';']; // Comma (0x2c) and Semicolon (0x3b) + +/// A set of viewport descriptors: +/// +/// +#[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 + /// + /// the zoom level when the page is first loaded + pub initial_scale: Scale, + + /// + /// how much zoom out is allowed on the page. + pub minimum_scale: Scale, + + /// + /// how much zoom in is allowed on the page + pub maximum_scale: Scale, + + /// + /// 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 { + match value.to_lowercase().as_str() { + "yes" => Ok(UserScalable::Yes), + "no" => Ok(UserScalable::No), + _ => match value.parse::() { + 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: Scale::new(DEFAULT_ZOOM), + minimum_scale: Scale::new(MIN_ZOOM), + maximum_scale: Scale::new(MAX_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) -> 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 + } + + /// Parses a viewport zoom value. + fn parse_viewport_value_as_zoom(value: &str) -> Option> { + value + .to_lowercase() + .as_str() + .parse::() + .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()) + } +} + +/// +/// +/// 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 { + 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::>(); + + Ok(Self::process_viewport_key_value_pairs(parsed_values)) + } +}