Match inline font fragment geometry

This commit is contained in:
SGavrl committed 2026-07-31 10:02:59 +00:00
1 parent 61d61feb50
commit 3e377cef67
9 files changed
+561 -20

No files matched your search

+9
View File
@@ -3044,6 +3044,14 @@ class Element extends Node {
const geometry = this._renderBoxGeometry();
if (geometry === null) return new DOMRectList([]);
if (geometry !== undefined) {
if (Array.isArray(geometry.clientRects)) {
return new DOMRectList(geometry.clientRects.map(
rect => this._rectFromRenderGeometry({
...rect,
viewportFixed: geometry.viewportFixed,
})
));
}
return new DOMRectList([this._rectFromRenderGeometry(geometry)]);
}
return new DOMRectList([this.getBoundingClientRect()]);
@@ -5945,6 +5953,7 @@ globalThis.getComputedStyle = (el) => {
'margin-top': '0px', 'margin-right': '0px', 'margin-bottom': '0px', 'margin-left': '0px',
'padding-top': '0px', 'padding-right': '0px', 'padding-bottom': '0px', 'padding-left': '0px',
'font-size': '16px', 'line-height': 'normal', 'font-weight': '400',
'letter-spacing': 'normal',
'font-family': 'Times',
color: 'rgb(0, 0, 0)', 'background-color': 'rgba(0, 0, 0, 0)',
'border-width': '0px', 'border-style': 'none', 'border-color': 'rgb(0, 0, 0)',
+30 -14
View File
@@ -2427,10 +2427,11 @@ pub(crate) fn clamp_scroll_offset(state: &mut ObscuraState, requested: (f32, f32
/// cache. The cache is computed lazily on first read and cleared on navigation
/// (see `set_dom`). Coordinates are viewport-relative after the shared root
/// scroll offset, except for viewport-fixed subtrees. Returns JSON
/// `{"x","y","width","height","clientWidth","clientHeight"}` in CSS pixels,
/// or an empty string when the node has no box. The client dimensions are the
/// unscaled padding box used by CSSOM View, whereas the rect is the visual
/// viewport-relative border box. Feature-gated.
/// `{"x","y","width","height","clientWidth","clientHeight","clientRects"}`
/// in CSS pixels, or an empty string when the node has no box. The client
/// dimensions are the unscaled padding box used by CSSOM View, `clientRects`
/// retains every inline continuation, and the top-level rect is their visual
/// viewport-relative bounding union. Feature-gated.
#[cfg(feature = "render")]
#[op2]
#[string]
@@ -2447,17 +2448,32 @@ fn op_layout_geometry(state: &OpState, #[string] nid_str: String) -> String {
let Some((client_width, client_height)) = prepared.client_size(nid) else {
return String::new();
};
let Some(client_rects) = prepared.viewport_client_rects(nid, scroll) else {
return String::new();
};
let client_rects = client_rects
.into_iter()
.map(|rect| {
serde_json::json!({
"x": rect.x,
"y": rect.y,
"width": rect.width,
"height": rect.height,
})
})
.collect::<Vec<_>>();
let viewport_fixed = prepared.viewport_fixed_nodes().contains(&nid);
return format!(
"{{\"x\":{},\"y\":{},\"width\":{},\"height\":{},\"clientWidth\":{},\"clientHeight\":{},\"viewportFixed\":{}}}",
rect.x,
rect.y,
rect.width,
rect.height,
client_width,
client_height,
viewport_fixed
);
return serde_json::json!({
"x": rect.x,
"y": rect.y,
"width": rect.width,
"height": rect.height,
"clientWidth": client_width,
"clientHeight": client_height,
"clientRects": client_rects,
"viewportFixed": viewport_fixed,
})
.to_string();
}
String::new()
}
+59
View File
@@ -2275,6 +2275,9 @@ mod tests {
rect: [rect.x, rect.y, rect.width, rect.height],
after: [afterRect.x, afterRect.y],
client: [token.clientWidth, token.clientHeight],
clientRects: Array.from(token.getClientRects(), r => [
r.x, r.y, r.width, r.height
]),
atomic: [atomic.width, atomic.height],
replaced: [replaced.width, replaced.height],
item: [item.width, item.height, getComputedStyle(item).display]
@@ -2298,6 +2301,19 @@ mod tests {
);
assert!(token_height < 40.0, "ignored block size leaked into geometry");
assert_eq!(result["client"], serde_json::json!([0, 0]));
let client_rects = result["clientRects"].as_array().unwrap();
assert_eq!(client_rects.len(), 1);
let client_rect = client_rects[0].as_array().unwrap();
for (actual, expected) in client_rect
.iter()
.map(|value| value.as_f64().unwrap())
.zip([token_x, token_y, token_width, token_height])
{
assert!(
(actual - expected).abs() < 0.001,
"getClientRects must expose the renderer's inline fragments"
);
}
let after = result["after"].as_array().unwrap();
assert!(after[0].as_f64().unwrap() >= token_x + token_width - 0.01);
assert!((after[1].as_f64().unwrap() - token_y).abs() < 0.01);
@@ -2377,6 +2393,49 @@ mod tests {
);
}
#[cfg(feature = "render")]
#[test]
fn computed_typography_uses_resolved_renderer_values() {
let dom = parse_html(
r#"<html><head><style>
#parent {
font-size:20px; line-height:1.5;
letter-spacing:-.05em; white-space:pre-wrap;
text-align:end
}
#child { font-size:10px }
#zero { letter-spacing:0px; white-space:break-spaces }
</style></head><body>
<div id="parent"><span id="child">child</span></div>
<div id="zero">zero</div>
</body></html>"#,
);
let mut rt = ObscuraJsRuntime::new();
rt.set_dom(dom);
rt.set_viewport(400.0, 200.0);
rt.run_page_init();
let result = rt
.evaluate(
r#"
const sample = id => {
const s = getComputedStyle(document.getElementById(id));
return [s.lineHeight, s.letterSpacing, s.whiteSpace, s.textAlign];
};
return [sample("parent"), sample("child"), sample("zero")];
"#,
)
.unwrap();
assert_eq!(
result,
serde_json::json!([
["30px", "-1px", "pre-wrap", "end"],
["15px", "-1px", "pre-wrap", "end"],
["normal", "normal", "break-spaces", "start"],
])
);
}
#[cfg(feature = "render")]
#[test]
fn computed_style_exposes_cascaded_custom_properties_and_invalidates() {