From 15d860c05cfbac3797b3ce042d83a3e61607970b Mon Sep 17 00:00:00 2001 From: Shubham Gupta Date: Tue, 11 Mar 2025 11:03:47 +0800 Subject: [PATCH 1/7] Process viewport attribute. Signed-off-by: Shubham Gupta --- components/script/dom/htmlmetaelement.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/components/script/dom/htmlmetaelement.rs b/components/script/dom/htmlmetaelement.rs index e94a5e1ff33..d1bfffa6609 100644 --- a/components/script/dom/htmlmetaelement.rs +++ b/components/script/dom/htmlmetaelement.rs @@ -61,6 +61,9 @@ impl HTMLMetaElement { if name == "referrer" { self.apply_referrer(); } + if name == "viewport" { + self.parse_viewport(); + } // https://html.spec.whatwg.org/multipage/#attr-meta-http-equiv } else if !self.HttpEquiv().is_empty() { // TODO: Implement additional http-equiv candidates @@ -115,6 +118,11 @@ impl HTMLMetaElement { } } + /// + fn parse_viewport(&self) { + let _element = self.upcast::(); + } + /// fn apply_csp_list(&self) { if let Some(parent) = self.upcast::().GetParentElement() { From 6b63f6f82f1dfe1ea575e59620a6440b57c0dfea Mon Sep 17 00:00:00 2001 From: Shubham Gupta Date: Wed, 26 Mar 2025 13:45:44 +0800 Subject: [PATCH 2/7] Add ViewportDescription Signed-off-by: Shubham Gupta --- .../compositing/viewport_description.rs | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 components/shared/compositing/viewport_description.rs diff --git a/components/shared/compositing/viewport_description.rs b/components/shared/compositing/viewport_description.rs new file mode 100644 index 00000000000..2418921e2c3 --- /dev/null +++ b/components/shared/compositing/viewport_description.rs @@ -0,0 +1,139 @@ +/* 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 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; + +/// 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_pair(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()) + } +} From 55b1a9be2fd9b219b019022d98b0276de86f0a79 Mon Sep 17 00:00:00 2001 From: Shubham Gupta Date: Wed, 26 Mar 2025 13:49:40 +0800 Subject: [PATCH 3/7] Process Key Value Pairs into ViewportDescription Signed-off-by: Shubham Gupta --- components/script/dom/htmlmetaelement.rs | 14 ++++++-- components/shared/compositing/lib.rs | 1 + .../compositing/viewport_description.rs | 36 +++++++++++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/components/script/dom/htmlmetaelement.rs b/components/script/dom/htmlmetaelement.rs index d1bfffa6609..8453258308d 100644 --- a/components/script/dom/htmlmetaelement.rs +++ b/components/script/dom/htmlmetaelement.rs @@ -2,6 +2,7 @@ * 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 compositing_traits::viewport_description::ViewportDescription; use dom_struct::dom_struct; use html5ever::{LocalName, Prefix, local_name, ns}; use js::rust::HandleObject; @@ -62,7 +63,7 @@ impl HTMLMetaElement { self.apply_referrer(); } if name == "viewport" { - self.parse_viewport(); + self.parse_and_send_viewport_if_necessary(); } // https://html.spec.whatwg.org/multipage/#attr-meta-http-equiv } else if !self.HttpEquiv().is_empty() { @@ -119,8 +120,15 @@ impl HTMLMetaElement { } /// - fn parse_viewport(&self) { - let _element = self.upcast::(); + 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::(); + if let Some(content) = element.get_attribute(&ns!(), &local_name!("content")) { + let _viewport = ViewportDescription::from_str(&content.value()).unwrap_or_default(); + } } /// diff --git a/components/shared/compositing/lib.rs b/components/shared/compositing/lib.rs index 67ff0046885..ee4e230fcf0 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}; diff --git a/components/shared/compositing/viewport_description.rs b/components/shared/compositing/viewport_description.rs index 2418921e2c3..64f374d7b7b 100644 --- a/components/shared/compositing/viewport_description.rs +++ b/components/shared/compositing/viewport_description.rs @@ -5,6 +5,7 @@ //! This module contains helpers for Viewport use std::collections::HashMap; +use std::str::FromStr; use euclid::default::Scale; use serde::{Deserialize, Serialize}; @@ -18,6 +19,9 @@ 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: /// /// @@ -137,3 +141,35 @@ impl ViewportDescription { 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_pair(parsed_values)) + } +} From 4c12008f5843413fbf5b2454e1284d61dc464b6c Mon Sep 17 00:00:00 2001 From: Shubham Gupta Date: Wed, 26 Mar 2025 14:05:45 +0800 Subject: [PATCH 4/7] Pass Viewport Description to compositor Signed-off-by: Shubham Gupta --- components/compositing/compositor.rs | 1 + components/compositing/tracing.rs | 1 + components/script/dom/htmlmetaelement.rs | 18 ++++++++++++++++-- components/shared/compositing/lib.rs | 4 ++++ 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/components/compositing/compositor.rs b/components/compositing/compositor.rs index 0acbbec977a..afd4124cf87 100644 --- a/components/compositing/compositor.rs +++ b/components/compositing/compositor.rs @@ -973,6 +973,7 @@ impl IOCompositor { warn!("Sending response to get screen size failed ({error:?})."); } }, + CompositorMsg::Viewport(_webview_id, _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/script/dom/htmlmetaelement.rs b/components/script/dom/htmlmetaelement.rs index 8453258308d..4ec6db7212c 100644 --- a/components/script/dom/htmlmetaelement.rs +++ b/components/script/dom/htmlmetaelement.rs @@ -2,6 +2,9 @@ * 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}; @@ -126,8 +129,19 @@ impl HTMLMetaElement { return; } let element = self.upcast::(); - if let Some(content) = element.get_attribute(&ns!(), &local_name!("content")) { - let _viewport = ViewportDescription::from_str(&content.value()).unwrap_or_default(); + 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(); } } diff --git a/components/shared/compositing/lib.rs b/components/shared/compositing/lib.rs index ee4e230fcf0..8dadf93d628 100644 --- a/components/shared/compositing/lib.rs +++ b/components/shared/compositing/lib.rs @@ -43,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 { @@ -177,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 { From e0803c1420dd6caec629a3f917b25e3debd39b3a Mon Sep 17 00:00:00 2001 From: Shubham Gupta Date: Thu, 27 Mar 2025 22:59:33 +0800 Subject: [PATCH 5/7] Stash the ViewportDescription to |webview| Signed-off-by: Shubham Gupta --- components/compositing/compositor.rs | 6 +++++- components/compositing/webview_renderer.rs | 10 +++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/components/compositing/compositor.rs b/components/compositing/compositor.rs index afd4124cf87..a4afeb01b3c 100644 --- a/components/compositing/compositor.rs +++ b/components/compositing/compositor.rs @@ -973,7 +973,11 @@ impl IOCompositor { warn!("Sending response to get screen size failed ({error:?})."); } }, - CompositorMsg::Viewport(_webview_id, _viewport_description) => {}, + 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/webview_renderer.rs b/components/compositing/webview_renderer.rs index 84d5fd877e6..b80e3422dd3 100644 --- a/components/compositing/webview_renderer.rs +++ b/components/compositing/webview_renderer.rs @@ -8,6 +8,7 @@ use std::collections::{HashMap, VecDeque}; use std::rc::Rc; use base::id::{PipelineId, WebViewId}; +use compositing_traits::viewport_description::ViewportDescription; use compositing_traits::{SendableFrameTree, WebViewTrait}; use constellation_traits::{EmbedderToConstellationMessage, ScrollState, WindowSizeType}; use embedder_traits::{ @@ -58,7 +59,7 @@ pub(crate) struct ScrollResult { pub offset: LayoutVector2D, } -#[derive(PartialEq)] +#[derive(Debug, PartialEq)] pub(crate) enum PinchZoomResult { DidPinchZoom, DidNotPinchZoom, @@ -102,6 +103,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 { @@ -138,6 +141,7 @@ impl WebViewRenderer { animating: false, pending_point_input_events: Default::default(), webrender_frame_ready: Cell::default(), + viewport_description: None, } } @@ -1042,6 +1046,10 @@ 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.viewport_description = Some(viewport_description); + } } #[derive(Clone, Copy, Debug, PartialEq)] From 2d2419539f673a9a0ecb0800174da6edbd04bf62 Mon Sep 17 00:00:00 2001 From: Shubham Gupta Date: Thu, 27 Mar 2025 23:32:35 +0800 Subject: [PATCH 6/7] Use viewport attributes parsed from meta in Webview Signed-off-by: Shubham Gupta --- components/compositing/webview_renderer.rs | 49 ++++++++++++---------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/components/compositing/webview_renderer.rs b/components/compositing/webview_renderer.rs index b80e3422dd3..dfe1289ef12 100644 --- a/components/compositing/webview_renderer.rs +++ b/components/compositing/webview_renderer.rs @@ -8,7 +8,9 @@ use std::collections::{HashMap, VecDeque}; use std::rc::Rc; use base::id::{PipelineId, WebViewId}; -use compositing_traits::viewport_description::ViewportDescription; +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::{ @@ -29,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 @@ -45,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), @@ -90,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, @@ -134,9 +131,7 @@ 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(), @@ -813,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 => { @@ -869,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, }; @@ -945,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)); @@ -979,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) { @@ -1048,6 +1046,13 @@ impl WebViewRenderer { } 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); } } From 2d5818f6165862e94cfdd536ec7dad69edd96e4f Mon Sep 17 00:00:00 2001 From: Xiaocheng Hu Date: Tue, 29 Apr 2025 18:06:41 +0800 Subject: [PATCH 7/7] Apply suggestions from code review Signed-off-by: Xiaocheng Hu --- components/shared/compositing/viewport_description.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/shared/compositing/viewport_description.rs b/components/shared/compositing/viewport_description.rs index 64f374d7b7b..83b29371f84 100644 --- a/components/shared/compositing/viewport_description.rs +++ b/components/shared/compositing/viewport_description.rs @@ -95,7 +95,7 @@ impl Default for ViewportDescription { impl ViewportDescription { /// Iterates over the key-value pairs generated from meta tag and returns a ViewportDescription - fn process_viewport_key_value_pair(pairs: HashMap) -> ViewportDescription { + fn process_viewport_key_value_pairs(pairs: HashMap) -> ViewportDescription { let mut description = ViewportDescription::default(); for (key, value) in &pairs { match key.as_str() { @@ -170,6 +170,6 @@ impl FromStr for ViewportDescription { }) .collect::>(); - Ok(Self::process_viewport_key_value_pair(parsed_values)) + Ok(Self::process_viewport_key_value_pairs(parsed_values)) } }