From aff2a8537216fb2f8a18f2c6e2cf47b34d455f48 Mon Sep 17 00:00:00 2001
From: Shubham Gupta <32428749+shubhamg13@users.noreply.github.com>
Date: Fri, 6 Jun 2025 23:13:51 +0800
Subject: [PATCH] Add support for parsing and applying `viewport` ``
(#35901)
This patch comprises following steps:
1. Parses the `viewport` Attribute from ``.
2. Creates a `ViewportDescription` struct.
3. Populate values into Viewport struct.
4. Pass & Stash Viewport Description to Webview.
5. Process parsed values of `viewport `
Testing: Tested locally.
Fixes: #36159
---------
Signed-off-by: Shubham Gupta
Signed-off-by: Xiaocheng Hu
Co-authored-by: Xiaocheng Hu
---
components/compositing/compositor.rs | 5 +
components/compositing/tracing.rs | 1 +
components/compositing/webview_renderer.rs | 51 +++--
components/script/dom/htmlmetaelement.rs | 30 +++
components/shared/compositing/lib.rs | 5 +
.../compositing/viewport_description.rs | 175 ++++++++++++++++++
6 files changed, 248 insertions(+), 19 deletions(-)
create mode 100644 components/shared/compositing/viewport_description.rs
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..056ffc16b89 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,
}
}
@@ -812,7 +811,8 @@ impl WebViewRenderer {
let mut combined_magnification = 1.0;
for scroll_event in self.pending_scroll_zoom_events.drain(..) {
match scroll_event {
- ScrollZoomEvent::PinchZoom(magnification) => {
+ ScrollZoomEvent::PinchZoom(magnification) |
+ ScrollZoomEvent::ViewportZoom(magnification) => {
combined_magnification *= magnification
},
ScrollZoomEvent::Scroll(scroll_event_info) => {
@@ -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))
+ }
+}