T13: push display scale factor into Servo's hidpi so Retina pages lay out at logical CSS dimensions
This commit is contained in:
@@ -81,6 +81,7 @@ impl ServoLiveClient {
|
|||||||
width: request.width,
|
width: request.width,
|
||||||
height: request.height,
|
height: request.height,
|
||||||
page_zoom_percent: request.page_zoom_percent,
|
page_zoom_percent: request.page_zoom_percent,
|
||||||
|
device_pixel_ratio: request.device_pixel_ratio,
|
||||||
scroll_delta_x: request.scroll_delta_x,
|
scroll_delta_x: request.scroll_delta_x,
|
||||||
scroll_delta_y: request.scroll_delta_y,
|
scroll_delta_y: request.scroll_delta_y,
|
||||||
click_x: request.click_x,
|
click_x: request.click_x,
|
||||||
@@ -212,6 +213,11 @@ pub(crate) struct ServoLiveEnsureRequest {
|
|||||||
pub(crate) width: u32,
|
pub(crate) width: u32,
|
||||||
pub(crate) height: u32,
|
pub(crate) height: u32,
|
||||||
pub(crate) page_zoom_percent: u16,
|
pub(crate) page_zoom_percent: u16,
|
||||||
|
/// Display scale factor (1.0 standard, 2.0 Retina). Servo's
|
||||||
|
/// WebView lays out CSS pixels = device pixels / hidpi factor;
|
||||||
|
/// without this, a Retina viewport gets desktop-CSS-pixel layout
|
||||||
|
/// and every visible element renders at half its expected size.
|
||||||
|
pub(crate) device_pixel_ratio: f32,
|
||||||
pub(crate) scroll_delta_x: i32,
|
pub(crate) scroll_delta_x: i32,
|
||||||
pub(crate) scroll_delta_y: i32,
|
pub(crate) scroll_delta_y: i32,
|
||||||
pub(crate) click_x: Option<u32>,
|
pub(crate) click_x: Option<u32>,
|
||||||
@@ -401,6 +407,7 @@ enum LiveRequest {
|
|||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
page_zoom_percent: u16,
|
page_zoom_percent: u16,
|
||||||
|
device_pixel_ratio: f32,
|
||||||
scroll_delta_x: i32,
|
scroll_delta_x: i32,
|
||||||
scroll_delta_y: i32,
|
scroll_delta_y: i32,
|
||||||
click_x: Option<u32>,
|
click_x: Option<u32>,
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ use gpui::{Bounds, Pixels, Point};
|
|||||||
pub(super) struct WebSurfaceSize {
|
pub(super) struct WebSurfaceSize {
|
||||||
pub(super) width: u32,
|
pub(super) width: u32,
|
||||||
pub(super) height: u32,
|
pub(super) height: u32,
|
||||||
|
/// Encoded as percent × 1 (e.g. 100 = 1.0 DPR, 200 = 2.0 DPR on
|
||||||
|
/// Retina). u16 instead of f32 so the struct keeps `Eq` —
|
||||||
|
/// `WebSurfaceSession::started_loading` compares sizes by value
|
||||||
|
/// and a float wouldn't compose with that. Convert to/from f32
|
||||||
|
/// at the wire boundary via `device_pixel_ratio_f32`.
|
||||||
|
pub(super) device_pixel_ratio_percent: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebSurfaceSize {
|
impl WebSurfaceSize {
|
||||||
@@ -11,8 +17,25 @@ impl WebSurfaceSize {
|
|||||||
Some(Self {
|
Some(Self {
|
||||||
width: viewport_dimension(bounds.size.width, scale_factor)?,
|
width: viewport_dimension(bounds.size.width, scale_factor)?,
|
||||||
height: viewport_dimension(bounds.size.height, scale_factor)?,
|
height: viewport_dimension(bounds.size.height, scale_factor)?,
|
||||||
|
device_pixel_ratio_percent: encode_scale_factor(scale_factor),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn device_pixel_ratio_f32(&self) -> f32 {
|
||||||
|
f32::from(self.device_pixel_ratio_percent) / 100.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Round `scale_factor` to the nearest whole percent and clamp into a
|
||||||
|
/// sane range. macOS reports 1.0 / 2.0 typically, fractional values
|
||||||
|
/// (1.25 / 1.5 / 1.75) show up on Windows / mixed-DPI setups. The
|
||||||
|
/// clamp guards against `inf`/`nan` from a misbehaving platform.
|
||||||
|
fn encode_scale_factor(scale_factor: f32) -> u16 {
|
||||||
|
if !scale_factor.is_finite() || scale_factor <= 0.0 {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
let scaled = (scale_factor * 100.0).round();
|
||||||
|
scaled.clamp(50.0, 500.0) as u16
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ impl WebSurfaceRuntime {
|
|||||||
width: size.width,
|
width: size.width,
|
||||||
height: size.height,
|
height: size.height,
|
||||||
page_zoom_percent: zoom_percent,
|
page_zoom_percent: zoom_percent,
|
||||||
|
device_pixel_ratio: size.device_pixel_ratio_f32(),
|
||||||
scroll_delta_x: input.scroll_delta.map_or(0, |delta| delta.x()),
|
scroll_delta_x: input.scroll_delta.map_or(0, |delta| delta.x()),
|
||||||
scroll_delta_y: input.scroll_delta.map_or(0, |delta| delta.y()),
|
scroll_delta_y: input.scroll_delta.map_or(0, |delta| delta.y()),
|
||||||
click_x: input.click_point.map(|point| point.x()),
|
click_x: input.click_point.map(|point| point.x()),
|
||||||
|
|||||||
@@ -7,10 +7,9 @@ rust-version.workspace = true
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
servo-engine = ["dep:dpi", "dep:serde", "dep:serde_json", "dep:servo", "dep:url"]
|
servo-engine = ["dep:dpi", "dep:euclid", "dep:serde", "dep:serde_json", "dep:servo", "dep:url"]
|
||||||
hardware-render = [
|
hardware-render = [
|
||||||
"servo-engine",
|
"servo-engine",
|
||||||
"dep:euclid",
|
|
||||||
"dep:gleam",
|
"dep:gleam",
|
||||||
"dep:glow",
|
"dep:glow",
|
||||||
"dep:image",
|
"dep:image",
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ fn handle_request(
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
page_zoom_percent,
|
page_zoom_percent,
|
||||||
|
device_pixel_ratio,
|
||||||
scroll_delta_x,
|
scroll_delta_x,
|
||||||
scroll_delta_y,
|
scroll_delta_y,
|
||||||
click_x,
|
click_x,
|
||||||
@@ -114,7 +115,14 @@ fn handle_request(
|
|||||||
let session =
|
let session =
|
||||||
ensure_session(host, sessions, tab_id.clone(), &tab, &profile, width, height)?;
|
ensure_session(host, sessions, tab_id.clone(), &tab, &profile, width, height)?;
|
||||||
|
|
||||||
if apply_layout(host, session, width, height, page_zoom_percent)? {
|
if apply_layout(
|
||||||
|
host,
|
||||||
|
session,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
page_zoom_percent,
|
||||||
|
device_pixel_ratio,
|
||||||
|
)? {
|
||||||
session.awaiting_visible_frame = true;
|
session.awaiting_visible_frame = true;
|
||||||
}
|
}
|
||||||
apply_permissions(host, session, &profile, site_permissions)?;
|
apply_permissions(host, session, &profile, site_permissions)?;
|
||||||
@@ -305,8 +313,26 @@ fn apply_layout(
|
|||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
page_zoom_percent: u16,
|
page_zoom_percent: u16,
|
||||||
|
device_pixel_ratio: f32,
|
||||||
) -> Result<bool, LiveSidecarError> {
|
) -> Result<bool, LiveSidecarError> {
|
||||||
let mut changed = false;
|
let mut changed = false;
|
||||||
|
// Push the device pixel ratio BEFORE resize. Servo's WebView
|
||||||
|
// defaults hidpi to 1.0; without this the first layout treats
|
||||||
|
// physical-pixel viewport widths as CSS-pixel widths and the page
|
||||||
|
// lays out half the size you'd expect on a Retina display. The
|
||||||
|
// sidecar-side `LiveSession::hidpi_scale_milli` keeps a u32 of
|
||||||
|
// (scale × 1000) so f32 jitter from JSON parsing doesn't churn
|
||||||
|
// the setter every frame.
|
||||||
|
let hidpi_scale_milli = encode_hidpi_scale_milli(device_pixel_ratio);
|
||||||
|
if session.hidpi_scale_milli != hidpi_scale_milli {
|
||||||
|
host.set_hidpi_scale(ely_servo_host::HidpiScaleRequest {
|
||||||
|
webview_id: session.webview_id.clone(),
|
||||||
|
scale_factor: hidpi_scale_milli_to_f32(hidpi_scale_milli),
|
||||||
|
})?;
|
||||||
|
session.hidpi_scale_milli = hidpi_scale_milli;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
if session.width != width || session.height != height {
|
if session.width != width || session.height != height {
|
||||||
host.resize(ResizeRequest { webview_id: session.webview_id.clone(), width, height })?;
|
host.resize(ResizeRequest { webview_id: session.webview_id.clone(), width, height })?;
|
||||||
session.width = width;
|
session.width = width;
|
||||||
@@ -326,6 +352,18 @@ fn apply_layout(
|
|||||||
Ok(changed)
|
Ok(changed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn encode_hidpi_scale_milli(scale: f32) -> u32 {
|
||||||
|
if !scale.is_finite() || scale <= 0.0 {
|
||||||
|
return 1_000;
|
||||||
|
}
|
||||||
|
let scaled = (scale * 1_000.0).round();
|
||||||
|
scaled.clamp(500.0, 5_000.0) as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hidpi_scale_milli_to_f32(milli: u32) -> f32 {
|
||||||
|
milli as f32 / 1_000.0
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_permissions(
|
fn apply_permissions(
|
||||||
host: &mut SoftwareServoHost,
|
host: &mut SoftwareServoHost,
|
||||||
session: &LiveSession,
|
session: &LiveSession,
|
||||||
@@ -474,6 +512,11 @@ struct LiveSession {
|
|||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
page_zoom_percent: u16,
|
page_zoom_percent: u16,
|
||||||
|
/// Last hidpi factor pushed to Servo, encoded as `(scale × 1000)`.
|
||||||
|
/// Stored as a u32 so equality is cheap and stable across the
|
||||||
|
/// f32 jitter that JSON parsing can introduce. Init to 0 so the
|
||||||
|
/// first apply_layout call always pushes a real value.
|
||||||
|
hidpi_scale_milli: u32,
|
||||||
scroll_x: i32,
|
scroll_x: i32,
|
||||||
scroll_y: i32,
|
scroll_y: i32,
|
||||||
awaiting_visible_frame: bool,
|
awaiting_visible_frame: bool,
|
||||||
@@ -497,6 +540,7 @@ impl LiveSession {
|
|||||||
width: width.max(1),
|
width: width.max(1),
|
||||||
height: height.max(1),
|
height: height.max(1),
|
||||||
page_zoom_percent: DEFAULT_ZOOM_PERCENT,
|
page_zoom_percent: DEFAULT_ZOOM_PERCENT,
|
||||||
|
hidpi_scale_milli: 0,
|
||||||
scroll_x: 0,
|
scroll_x: 0,
|
||||||
scroll_y: 0,
|
scroll_y: 0,
|
||||||
awaiting_visible_frame: false,
|
awaiting_visible_frame: false,
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ pub(super) enum LiveRequest {
|
|||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
page_zoom_percent: u16,
|
page_zoom_percent: u16,
|
||||||
|
/// Display scale factor reported by the host's window
|
||||||
|
/// (1.0 standard, 2.0 Retina). The sidecar plumbs this into
|
||||||
|
/// Servo's `WebView::set_hidpi_scale_factor` so CSS layout
|
||||||
|
/// happens at logical-pixel dimensions instead of physical.
|
||||||
|
/// Defaults to 1.0 for backward compatibility if a client
|
||||||
|
/// (e.g. the live perf bench) omits the field.
|
||||||
|
#[serde(default = "default_device_pixel_ratio")]
|
||||||
|
device_pixel_ratio: f32,
|
||||||
scroll_delta_x: i32,
|
scroll_delta_x: i32,
|
||||||
scroll_delta_y: i32,
|
scroll_delta_y: i32,
|
||||||
click_x: Option<u32>,
|
click_x: Option<u32>,
|
||||||
@@ -35,6 +43,10 @@ pub(super) enum LiveRequest {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_device_pixel_ratio() -> f32 {
|
||||||
|
1.0
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub(super) struct LiveSitePermission {
|
pub(super) struct LiveSitePermission {
|
||||||
pub origin: String,
|
pub origin: String,
|
||||||
|
|||||||
@@ -235,6 +235,17 @@ pub struct PageZoomRequest {
|
|||||||
pub zoom_factor: f32,
|
pub zoom_factor: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the WebView's hidpi (device → CSS pixel) scale. Servo's
|
||||||
|
/// builder defaults this to 1.0; on Retina hosts that produces
|
||||||
|
/// half-size layout because the page treats physical pixels as CSS
|
||||||
|
/// pixels. The embedder should mirror the platform's reported scale
|
||||||
|
/// factor on every viewport-bound change.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct HidpiScaleRequest {
|
||||||
|
pub webview_id: WebViewId,
|
||||||
|
pub scale_factor: f32,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct MouseClickRequest {
|
pub struct MouseClickRequest {
|
||||||
pub webview_id: WebViewId,
|
pub webview_id: WebViewId,
|
||||||
@@ -316,6 +327,8 @@ pub trait ServoHost {
|
|||||||
|
|
||||||
fn set_page_zoom(&mut self, request: PageZoomRequest) -> Result<(), ServoHostError>;
|
fn set_page_zoom(&mut self, request: PageZoomRequest) -> Result<(), ServoHostError>;
|
||||||
|
|
||||||
|
fn set_hidpi_scale(&mut self, request: HidpiScaleRequest) -> Result<(), ServoHostError>;
|
||||||
|
|
||||||
fn click(&mut self, request: MouseClickRequest) -> Result<(), ServoHostError>;
|
fn click(&mut self, request: MouseClickRequest) -> Result<(), ServoHostError>;
|
||||||
|
|
||||||
fn hover(&mut self, request: MouseHoverRequest) -> Result<(), ServoHostError>;
|
fn hover(&mut self, request: MouseHoverRequest) -> Result<(), ServoHostError>;
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ pub use error::ServoHostError;
|
|||||||
pub use hardware_rendering_context::HardwareOffscreenContext;
|
pub use hardware_rendering_context::HardwareOffscreenContext;
|
||||||
pub use iosurface_handle::{IOSurfaceHandle, IOSurfaceIdentity};
|
pub use iosurface_handle::{IOSurfaceHandle, IOSurfaceIdentity};
|
||||||
pub use host::{
|
pub use host::{
|
||||||
KeyboardTextRequest, MouseClickRequest, MouseDragRequest, MouseHoverRequest,
|
HidpiScaleRequest, KeyboardTextRequest, MouseClickRequest, MouseDragRequest,
|
||||||
NavigationRequest, PageZoomRequest, PermissionDecision, PermissionRequest, RenderedFrame,
|
MouseHoverRequest, NavigationRequest, PageZoomRequest, PermissionDecision, PermissionRequest,
|
||||||
RenderedFrameSummary, ResizeRequest, ScreenshotRequest, ScrollRequest, ServoHost,
|
RenderedFrame, RenderedFrameSummary, ResizeRequest, ScreenshotRequest, ScrollRequest,
|
||||||
TouchTapRequest, WebViewSnapshot, WebViewState,
|
ServoHost, TouchTapRequest, WebViewSnapshot, WebViewState,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "servo-engine")]
|
#[cfg(feature = "servo-engine")]
|
||||||
pub use runtime::{RenderingContextKind, ServoSurfaceSize, SoftwareServoHost};
|
pub use runtime::{RenderingContextKind, ServoSurfaceSize, SoftwareServoHost};
|
||||||
|
|||||||
@@ -13,17 +13,33 @@ use std::{
|
|||||||
|
|
||||||
use dpi::PhysicalSize;
|
use dpi::PhysicalSize;
|
||||||
use ely_domain::{ProfileId, TabId, WebViewId};
|
use ely_domain::{ProfileId, TabId, WebViewId};
|
||||||
|
use euclid::Scale;
|
||||||
use servo::{
|
use servo::{
|
||||||
DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePoint, DeviceVector2D, Opts,
|
DeviceIndependentPixel, DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixel,
|
||||||
RenderingContext, Scroll, Servo, ServoBuilder, WebViewBuilder, WebViewPoint, WebViewVector,
|
DevicePoint, DeviceVector2D, Opts, RenderingContext, Scroll, Servo, ServoBuilder,
|
||||||
|
WebViewBuilder, WebViewPoint, WebViewVector,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Wrap an `f32` scale factor in Servo's typed `Scale<f32, DeviceIndependentPixel,
|
||||||
|
/// DevicePixel>`. The clamp guards against `NaN`/`inf` reaching Servo's
|
||||||
|
/// layout (which assumes a positive finite scale).
|
||||||
|
fn hidpi_scale_from_factor(
|
||||||
|
scale_factor: f32,
|
||||||
|
) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
|
||||||
|
let safe = if scale_factor.is_finite() && scale_factor > 0.0 {
|
||||||
|
scale_factor.clamp(0.5, 5.0)
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
Scale::new(safe)
|
||||||
|
}
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
KeyboardTextRequest, MouseClickRequest, MouseDragRequest, MouseHoverRequest,
|
HidpiScaleRequest, KeyboardTextRequest, MouseClickRequest, MouseDragRequest,
|
||||||
NavigationRequest, PageZoomRequest, PermissionDecision, PermissionRequest, RenderedFrame,
|
MouseHoverRequest, NavigationRequest, PageZoomRequest, PermissionDecision, PermissionRequest,
|
||||||
ResizeRequest, ScreenshotRequest, ScrollRequest, ServoHost, ServoHostError, TouchTapRequest,
|
RenderedFrame, ResizeRequest, ScreenshotRequest, ScrollRequest, ServoHost, ServoHostError,
|
||||||
WebViewSnapshot, WebViewState,
|
TouchTapRequest, WebViewSnapshot, WebViewState,
|
||||||
runtime_input::{
|
runtime_input::{
|
||||||
send_keyboard_text, send_mouse_click, send_mouse_drag, send_mouse_hover, send_touch_tap,
|
send_keyboard_text, send_mouse_click, send_mouse_drag, send_mouse_hover, send_touch_tap,
|
||||||
},
|
},
|
||||||
@@ -254,6 +270,17 @@ impl ServoHost for SoftwareServoHost {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_hidpi_scale(&mut self, request: HidpiScaleRequest) -> Result<(), ServoHostError> {
|
||||||
|
let webview = self
|
||||||
|
.webviews
|
||||||
|
.get(&request.webview_id)
|
||||||
|
.ok_or_else(|| ServoHostError::WebViewNotFound { id: request.webview_id.clone() })?;
|
||||||
|
|
||||||
|
let scale = hidpi_scale_from_factor(request.scale_factor);
|
||||||
|
webview.webview.set_hidpi_scale_factor(scale);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn hover(&mut self, request: MouseHoverRequest) -> Result<(), ServoHostError> {
|
fn hover(&mut self, request: MouseHoverRequest) -> Result<(), ServoHostError> {
|
||||||
let webview = self.webview_for_input(&request.webview_id)?;
|
let webview = self.webview_for_input(&request.webview_id)?;
|
||||||
send_mouse_hover(&webview.webview, request.x, request.y);
|
send_mouse_hover(&webview.webview, request.x, request.y);
|
||||||
|
|||||||
Reference in New Issue
Block a user