Expose live computed CSS variables to scripts
This commit is contained in:
1 parent
2343681497
commit
5f67bad4fe
6 files changed
+258
-8
No files matched your search
Vendored
+48
-3
@@ -81,7 +81,19 @@ globalThis.dispatchEvent = function(event) {
|
|||||||
return !event.defaultPrevented;
|
return !event.defaultPrevented;
|
||||||
};
|
};
|
||||||
|
|
||||||
const _dom = (cmd, a1, a2) => Deno.core.ops.op_dom(cmd, String(a1 ?? ""), String(a2 ?? ""));
|
let _domMutationEpoch = 0;
|
||||||
|
const _DOM_MUTATION_COMMANDS = new Set([
|
||||||
|
"append_child", "insert_before", "remove_child",
|
||||||
|
"set_attribute", "remove_attribute",
|
||||||
|
"set_text_content", "set_inner_html", "set_inner_html_context",
|
||||||
|
"create_element", "create_text_node", "create_comment_node",
|
||||||
|
"create_document_fragment", "clone_node",
|
||||||
|
]);
|
||||||
|
const _dom = (cmd, a1, a2) => {
|
||||||
|
const result = Deno.core.ops.op_dom(cmd, String(a1 ?? ""), String(a2 ?? ""));
|
||||||
|
if (_DOM_MUTATION_COMMANDS.has(cmd)) _domMutationEpoch++;
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
const _nativeFns = new Set();
|
const _nativeFns = new Set();
|
||||||
// Exact toString override for members whose native form is not just
|
// Exact toString override for members whose native form is not just
|
||||||
@@ -2556,6 +2568,23 @@ class Element extends Node {
|
|||||||
return this.getAttribute("href") || "";
|
return this.getAttribute("href") || "";
|
||||||
}
|
}
|
||||||
set href(v) { this.setAttribute("href", v); }
|
set href(v) { this.setAttribute("href", v); }
|
||||||
|
// HTMLHyperlinkElementUtils / HTMLAnchorElement reflected content
|
||||||
|
// attributes. Real-world locale, routing, and analytics code commonly
|
||||||
|
// enumerates `[hreflang]` links and reads the IDL property rather than
|
||||||
|
// getAttribute(); leaving it undefined aborts the entire component even
|
||||||
|
// though the attribute is present in the DOM.
|
||||||
|
get hreflang() { return this.getAttribute("hreflang") || ""; }
|
||||||
|
set hreflang(v) { this.setAttribute("hreflang", v); }
|
||||||
|
get rel() { return this.getAttribute("rel") || ""; }
|
||||||
|
set rel(v) { this.setAttribute("rel", v); }
|
||||||
|
get target() { return this.getAttribute("target") || ""; }
|
||||||
|
set target(v) { this.setAttribute("target", v); }
|
||||||
|
get download() { return this.getAttribute("download") || ""; }
|
||||||
|
set download(v) { this.setAttribute("download", v); }
|
||||||
|
get ping() { return this.getAttribute("ping") || ""; }
|
||||||
|
set ping(v) { this.setAttribute("ping", v); }
|
||||||
|
get referrerPolicy() { return this.getAttribute("referrerpolicy") || ""; }
|
||||||
|
set referrerPolicy(v) { this.setAttribute("referrerpolicy", v); }
|
||||||
// HTMLHyperlinkElementUtils URL-decomposition members, live on <a>/<area>.
|
// HTMLHyperlinkElementUtils URL-decomposition members, live on <a>/<area>.
|
||||||
get protocol() { const u = (this.localName === 'a' || this.localName === 'area') ? _elemHrefURL(this) : null; return u ? u.protocol : ''; }
|
get protocol() { const u = (this.localName === 'a' || this.localName === 'area') ? _elemHrefURL(this) : null; return u ? u.protocol : ''; }
|
||||||
set protocol(v) { if (this.localName === 'a' || this.localName === 'area') _setElemHrefPart(this, 'protocol', v); }
|
set protocol(v) { if (this.localName === 'a' || this.localName === 'area') _setElemHrefPart(this, 'protocol', v); }
|
||||||
@@ -5863,12 +5892,21 @@ globalThis.getComputedStyle = (el) => {
|
|||||||
// cascade/layout. Fetch it once per CSSStyleDeclaration proxy: reading a
|
// cascade/layout. Fetch it once per CSSStyleDeclaration proxy: reading a
|
||||||
// dozen properties must not trigger a dozen layout/native crossings.
|
// dozen properties must not trigger a dozen layout/native crossings.
|
||||||
let rendered = null;
|
let rendered = null;
|
||||||
|
let renderedEpoch = -1;
|
||||||
|
let computedNames = [];
|
||||||
|
const refreshRendered = () => {
|
||||||
|
if (renderedEpoch === _domMutationEpoch) return;
|
||||||
|
renderedEpoch = _domMutationEpoch;
|
||||||
|
rendered = null;
|
||||||
if (typeof Deno.core.ops.op_computed_style === 'function' && el?._nid != null) {
|
if (typeof Deno.core.ops.op_computed_style === 'function' && el?._nid != null) {
|
||||||
try {
|
try {
|
||||||
const raw = Deno.core.ops.op_computed_style(String(el._nid | 0));
|
const raw = Deno.core.ops.op_computed_style(String(el._nid | 0));
|
||||||
rendered = raw ? JSON.parse(raw) : null;
|
rendered = raw ? JSON.parse(raw) : null;
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
computedNames = rendered ? Object.keys(rendered) : [];
|
||||||
|
};
|
||||||
|
refreshRendered();
|
||||||
// React virtualization libraries (react-window, tanstack-virtual,
|
// React virtualization libraries (react-window, tanstack-virtual,
|
||||||
// react-virtuoso) all compute container dimensions via getComputedStyle.
|
// react-virtuoso) all compute container dimensions via getComputedStyle.
|
||||||
// The defaults table previously returned `auto` for width/height and
|
// The defaults table previously returned `auto` for width/height and
|
||||||
@@ -5924,6 +5962,7 @@ globalThis.getComputedStyle = (el) => {
|
|||||||
|
|
||||||
const lookup = (rawProp) => {
|
const lookup = (rawProp) => {
|
||||||
if (typeof rawProp !== 'string') return '';
|
if (typeof rawProp !== 'string') return '';
|
||||||
|
refreshRendered();
|
||||||
const kebab = rawProp.replace(/([A-Z])/g, '-$1').toLowerCase();
|
const kebab = rawProp.replace(/([A-Z])/g, '-$1').toLowerCase();
|
||||||
if (rendered && Object.prototype.hasOwnProperty.call(rendered, kebab))
|
if (rendered && Object.prototype.hasOwnProperty.call(rendered, kebab))
|
||||||
return rendered[kebab];
|
return rendered[kebab];
|
||||||
@@ -5944,8 +5983,14 @@ globalThis.getComputedStyle = (el) => {
|
|||||||
if (prop === Symbol.toPrimitive || prop === Symbol.toStringTag) return undefined;
|
if (prop === Symbol.toPrimitive || prop === Symbol.toStringTag) return undefined;
|
||||||
if (prop === 'getPropertyValue') return (name) => lookup(name);
|
if (prop === 'getPropertyValue') return (name) => lookup(name);
|
||||||
if (prop === 'getPropertyPriority') return () => '';
|
if (prop === 'getPropertyPriority') return () => '';
|
||||||
if (prop === 'item') return (i) => '';
|
if (prop === 'item') return (i) => {
|
||||||
if (prop === 'length') return 0;
|
refreshRendered();
|
||||||
|
return computedNames[i | 0] || '';
|
||||||
|
};
|
||||||
|
if (prop === 'length') {
|
||||||
|
refreshRendered();
|
||||||
|
return computedNames.length;
|
||||||
|
}
|
||||||
if (prop === 'cssText') return '';
|
if (prop === 'cssText') return '';
|
||||||
if (prop === 'parentRule') return null;
|
if (prop === 'parentRule') return null;
|
||||||
// CSSStyleDeclaration's `has` trap intentionally reports every known
|
// CSSStyleDeclaration's `has` trap intentionally reports every known
|
||||||
|
|||||||
@@ -2473,10 +2473,23 @@ fn op_computed_style(state: &OpState, #[string] nid_str: String) -> String {
|
|||||||
let nid: u32 = nid_str.parse().unwrap_or(0);
|
let nid: u32 = nid_str.parse().unwrap_or(0);
|
||||||
let nid = obscura_dom::tree::NodeId::new(nid);
|
let nid = obscura_dom::tree::NodeId::new(nid);
|
||||||
let mut gs = shared.borrow_mut();
|
let mut gs = shared.borrow_mut();
|
||||||
ensure_prepared_render(&mut gs)
|
let Some(prepared) = ensure_prepared_render(&mut gs) else {
|
||||||
.and_then(|prepared| prepared.computed_style(nid))
|
return String::new();
|
||||||
.and_then(|snapshot| serde_json::to_string(&snapshot).ok())
|
};
|
||||||
.unwrap_or_default()
|
let Some(snapshot) = prepared.computed_style(nid) else {
|
||||||
|
return String::new();
|
||||||
|
};
|
||||||
|
let custom = prepared
|
||||||
|
.computed_custom_properties(nid)
|
||||||
|
.unwrap_or_default();
|
||||||
|
let mut object = serde_json::Map::with_capacity(snapshot.len() + custom.len());
|
||||||
|
for (name, value) in snapshot {
|
||||||
|
object.insert(name.to_string(), serde_json::Value::String(value));
|
||||||
|
}
|
||||||
|
for (name, value) in custom {
|
||||||
|
object.insert(name, serde_json::Value::String(value));
|
||||||
|
}
|
||||||
|
serde_json::Value::Object(object).to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Root scrolling overflow in CSS pixels. The JS CSSOM probes this op only in
|
/// Root scrolling overflow in CSS pixels. The JS CSSOM probes this op only in
|
||||||
|
|||||||
@@ -2168,6 +2168,52 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hyperlink_content_attributes_reflect_through_the_idl_surface() {
|
||||||
|
let mut rt = setup_runtime(
|
||||||
|
r#"<html><body>
|
||||||
|
<a id="locale" hreflang="en-US" rel="alternate"
|
||||||
|
target="_blank" download="guide.pdf"
|
||||||
|
ping="/audit" referrerpolicy="no-referrer">English</a>
|
||||||
|
</body></html>"#,
|
||||||
|
);
|
||||||
|
let result = rt
|
||||||
|
.evaluate(
|
||||||
|
r#"
|
||||||
|
const link = document.getElementById("locale");
|
||||||
|
const initial = [
|
||||||
|
link.hreflang, link.rel, link.target, link.download,
|
||||||
|
link.ping, link.referrerPolicy,
|
||||||
|
link.hreflang.split("-")[1]
|
||||||
|
];
|
||||||
|
link.hreflang = "de-DE";
|
||||||
|
link.referrerPolicy = "origin";
|
||||||
|
return [
|
||||||
|
initial,
|
||||||
|
link.getAttribute("hreflang"),
|
||||||
|
link.getAttribute("referrerpolicy")
|
||||||
|
];
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
serde_json::json!([
|
||||||
|
[
|
||||||
|
"en-US",
|
||||||
|
"alternate",
|
||||||
|
"_blank",
|
||||||
|
"guide.pdf",
|
||||||
|
"/audit",
|
||||||
|
"no-referrer",
|
||||||
|
"US"
|
||||||
|
],
|
||||||
|
"de-DE",
|
||||||
|
"origin"
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "render")]
|
#[cfg(feature = "render")]
|
||||||
#[test]
|
#[test]
|
||||||
fn computed_style_uses_renderer_stylesheet_cascade_and_invalidates() {
|
fn computed_style_uses_renderer_stylesheet_cascade_and_invalidates() {
|
||||||
@@ -2239,6 +2285,78 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "render")]
|
||||||
|
#[test]
|
||||||
|
fn computed_style_exposes_cascaded_custom_properties_and_invalidates() {
|
||||||
|
let dom = parse_html(
|
||||||
|
r#"<html><head><style>
|
||||||
|
:root { --inherited-space: 17px; --derived-space: var(--inherited-space); }
|
||||||
|
#nav {
|
||||||
|
--r-globalnav-font-size:17px;
|
||||||
|
--local-scale:1.25;
|
||||||
|
font-size:var(--r-globalnav-font-size);
|
||||||
|
}
|
||||||
|
</style></head><body>
|
||||||
|
<nav id="nav"><span id="child"></span></nav>
|
||||||
|
</body></html>"#,
|
||||||
|
);
|
||||||
|
let mut rt = ObscuraJsRuntime::new();
|
||||||
|
rt.set_dom(dom);
|
||||||
|
rt.set_viewport(400.0, 200.0);
|
||||||
|
rt.run_page_init();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
rt.evaluate(
|
||||||
|
r#"
|
||||||
|
const nav = document.getElementById("nav");
|
||||||
|
const child = document.getElementById("child");
|
||||||
|
const navStyle = getComputedStyle(nav);
|
||||||
|
const childStyle = getComputedStyle(child);
|
||||||
|
let enumeratesBase = false;
|
||||||
|
for (let i = 0; i < navStyle.length; i++) {
|
||||||
|
if (navStyle.item(i) === "--r-globalnav-font-size")
|
||||||
|
enumeratesBase = true;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
navStyle.fontSize,
|
||||||
|
navStyle.getPropertyValue("--r-globalnav-font-size"),
|
||||||
|
parseInt(navStyle.fontSize) /
|
||||||
|
parseInt(navStyle.getPropertyValue("--r-globalnav-font-size")),
|
||||||
|
navStyle.getPropertyValue("--inherited-space"),
|
||||||
|
navStyle.getPropertyValue("--derived-space"),
|
||||||
|
navStyle.getPropertyValue("--local-scale"),
|
||||||
|
childStyle.getPropertyValue("--inherited-space"),
|
||||||
|
childStyle.getPropertyValue("--local-scale"),
|
||||||
|
enumeratesBase
|
||||||
|
];
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
serde_json::json!([
|
||||||
|
"17px", "17px", 1,
|
||||||
|
"17px", "17px", "1.25", "17px", "1.25", true
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
rt.evaluate(
|
||||||
|
r#"
|
||||||
|
const nav = document.getElementById("nav");
|
||||||
|
const computed = getComputedStyle(nav);
|
||||||
|
nav.style.setProperty("--inherited-space", "23px");
|
||||||
|
nav.style.fontSize = "19px";
|
||||||
|
return [
|
||||||
|
computed.fontSize,
|
||||||
|
computed.getPropertyValue("--inherited-space"),
|
||||||
|
computed.getPropertyValue("--derived-space")
|
||||||
|
];
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
serde_json::json!(["19px", "23px", "17px"])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "current_thread")]
|
#[tokio::test(flavor = "current_thread")]
|
||||||
async fn idle_event_loop_flushes_resolved_promise_continuations() {
|
async fn idle_event_loop_flushes_resolved_promise_continuations() {
|
||||||
let mut rt = setup_runtime("<html><body><div id='state'>pending</div></body></html>");
|
let mut rt = setup_runtime("<html><body><div id='state'>pending</div></body></html>");
|
||||||
|
|||||||
@@ -955,7 +955,9 @@ impl Stylesheet {
|
|||||||
}
|
}
|
||||||
let registration_changed_props = resolved_props != *parent_props;
|
let registration_changed_props = resolved_props != *parent_props;
|
||||||
let has_own = !own.is_empty();
|
let has_own = !own.is_empty();
|
||||||
|
let mut own_names = std::collections::HashSet::new();
|
||||||
for (k, v) in own {
|
for (k, v) in own {
|
||||||
|
own_names.insert(k.clone());
|
||||||
let registration = self.registered_custom_properties.get(&k);
|
let registration = self.registered_custom_properties.get(&k);
|
||||||
let set_initial = |props: &mut HashMap<String, String>| {
|
let set_initial = |props: &mut HashMap<String, String>| {
|
||||||
if let Some(initial) = registration.and_then(|entry| entry.initial_value.as_ref()) {
|
if let Some(initial) = registration.and_then(|entry| entry.initial_value.as_ref()) {
|
||||||
@@ -997,6 +999,29 @@ impl Stylesheet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Custom properties inherit their computed value, not their original
|
||||||
|
// token stream. Resolve only declarations won on this element against
|
||||||
|
// the complete same-element environment (so forward references work),
|
||||||
|
// then pass those substituted values to descendants. Re-resolving an
|
||||||
|
// inherited `--b:var(--a)` after a child overrides `--a` is observably
|
||||||
|
// wrong: browsers keep the parent's already-computed `--b`.
|
||||||
|
let resolution_environment = resolved_props.clone();
|
||||||
|
for name in own_names {
|
||||||
|
let Some(value) = resolution_environment.get(&name) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if let Some(computed) = substitute_var_value(value, &resolution_environment, 0) {
|
||||||
|
resolved_props.insert(name, computed);
|
||||||
|
} else if let Some(initial) = self
|
||||||
|
.registered_custom_properties
|
||||||
|
.get(&name)
|
||||||
|
.and_then(|entry| entry.initial_value.clone())
|
||||||
|
{
|
||||||
|
resolved_props.insert(name, initial);
|
||||||
|
} else {
|
||||||
|
resolved_props.remove(&name);
|
||||||
|
}
|
||||||
|
}
|
||||||
let effective = if has_own || registration_changed_props {
|
let effective = if has_own || registration_changed_props {
|
||||||
Some(resolved_props)
|
Some(resolved_props)
|
||||||
} else {
|
} else {
|
||||||
@@ -1419,7 +1444,11 @@ fn substitute_declarations(css: &str, props: &HashMap<String, String>) -> String
|
|||||||
/// represents CSS's guaranteed-invalid value. Crucially, invalidity propagates
|
/// represents CSS's guaranteed-invalid value. Crucially, invalidity propagates
|
||||||
/// through an intermediate custom property so an outer var() can use its own
|
/// through an intermediate custom property so an outer var() can use its own
|
||||||
/// fallback (`--toggle:var(--missing) dark; color:var(--toggle,light)`).
|
/// fallback (`--toggle:var(--missing) dark; color:var(--toggle,light)`).
|
||||||
fn substitute_var_value(input: &str, props: &HashMap<String, String>, depth: u8) -> Option<String> {
|
pub(crate) fn substitute_var_value(
|
||||||
|
input: &str,
|
||||||
|
props: &HashMap<String, String>,
|
||||||
|
depth: u8,
|
||||||
|
) -> Option<String> {
|
||||||
if depth > 16 {
|
if depth > 16 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -225,6 +225,13 @@ impl OverflowClip {
|
|||||||
pub struct DomLayout {
|
pub struct DomLayout {
|
||||||
pub rects: HashMap<NodeId, Rect>,
|
pub rects: HashMap<NodeId, Rect>,
|
||||||
pub styles: HashMap<NodeId, crate::LayoutStyle>,
|
pub styles: HashMap<NodeId, crate::LayoutStyle>,
|
||||||
|
/// Computed custom properties in force for each element. Values are
|
||||||
|
/// reference-counted because the overwhelmingly common case is inheritance
|
||||||
|
/// without an override: an entire subtree can share one cascade map while
|
||||||
|
/// CSSOM still exposes every inherited `--token` through
|
||||||
|
/// `getComputedStyle()`.
|
||||||
|
pub custom_properties:
|
||||||
|
HashMap<NodeId, std::rc::Rc<HashMap<String, String>>>,
|
||||||
/// The per-axis clip inherited from ancestor non-visible overflow, keyed
|
/// The per-axis clip inherited from ancestor non-visible overflow, keyed
|
||||||
/// per node, in SCREEN space (the clip owner's box shifted by the owner's
|
/// per node, in SCREEN space (the clip owner's box shifted by the owner's
|
||||||
/// accumulated translate; see `resolve_clip_rects`). `None` means
|
/// accumulated translate; see `resolve_clip_rects`). `None` means
|
||||||
@@ -1348,6 +1355,7 @@ fn cascade_walk(
|
|||||||
sheet: &crate::css::Stylesheet,
|
sheet: &crate::css::Stylesheet,
|
||||||
matcher: &mut obscura_dom::selector::Matcher,
|
matcher: &mut obscura_dom::selector::Matcher,
|
||||||
styles: &mut HashMap<NodeId, crate::LayoutStyle>,
|
styles: &mut HashMap<NodeId, crate::LayoutStyle>,
|
||||||
|
custom_properties: &mut HashMap<NodeId, std::rc::Rc<HashMap<String, String>>>,
|
||||||
parent_props: &std::rc::Rc<HashMap<String, String>>,
|
parent_props: &std::rc::Rc<HashMap<String, String>>,
|
||||||
container_evaluator: &mut Option<&mut crate::css::ContainerQueryEvaluator<'_>>,
|
container_evaluator: &mut Option<&mut crate::css::ContainerQueryEvaluator<'_>>,
|
||||||
quirks_mode: bool,
|
quirks_mode: bool,
|
||||||
@@ -1471,6 +1479,7 @@ fn cascade_walk(
|
|||||||
if let Some(m) = effective_props {
|
if let Some(m) = effective_props {
|
||||||
this_props = std::rc::Rc::new(m);
|
this_props = std::rc::Rc::new(m);
|
||||||
}
|
}
|
||||||
|
custom_properties.insert(id, this_props.clone());
|
||||||
let (before_pseudo, after_pseudo) =
|
let (before_pseudo, after_pseudo) =
|
||||||
if let Some(evaluator) = container_evaluator.as_deref_mut() {
|
if let Some(evaluator) = container_evaluator.as_deref_mut() {
|
||||||
sheet.pseudo_styles_with_container_queries(
|
sheet.pseudo_styles_with_container_queries(
|
||||||
@@ -1508,6 +1517,7 @@ fn cascade_walk(
|
|||||||
sheet,
|
sheet,
|
||||||
matcher,
|
matcher,
|
||||||
styles,
|
styles,
|
||||||
|
custom_properties,
|
||||||
&this_props,
|
&this_props,
|
||||||
container_evaluator,
|
container_evaluator,
|
||||||
quirks_mode,
|
quirks_mode,
|
||||||
@@ -1960,6 +1970,7 @@ fn layout_dom_once(
|
|||||||
let t1 = std::time::Instant::now();
|
let t1 = std::time::Instant::now();
|
||||||
let mut matcher = tree.matcher();
|
let mut matcher = tree.matcher();
|
||||||
let mut styles: HashMap<NodeId, crate::LayoutStyle> = HashMap::new();
|
let mut styles: HashMap<NodeId, crate::LayoutStyle> = HashMap::new();
|
||||||
|
let mut custom_properties = HashMap::new();
|
||||||
let root_props = std::rc::Rc::new(HashMap::new());
|
let root_props = std::rc::Rc::new(HashMap::new());
|
||||||
let mut evaluator =
|
let mut evaluator =
|
||||||
snapshot.map(|snapshot| crate::css::ContainerQueryEvaluator::new(tree, snapshot));
|
snapshot.map(|snapshot| crate::css::ContainerQueryEvaluator::new(tree, snapshot));
|
||||||
@@ -1980,6 +1991,7 @@ fn layout_dom_once(
|
|||||||
&sheet,
|
&sheet,
|
||||||
&mut matcher,
|
&mut matcher,
|
||||||
&mut styles,
|
&mut styles,
|
||||||
|
&mut custom_properties,
|
||||||
&root_props,
|
&root_props,
|
||||||
&mut evaluator_ref,
|
&mut evaluator_ref,
|
||||||
quirks_mode,
|
quirks_mode,
|
||||||
@@ -3948,6 +3960,7 @@ fn layout_dom_once(
|
|||||||
DomLayout {
|
DomLayout {
|
||||||
rects,
|
rects,
|
||||||
styles,
|
styles,
|
||||||
|
custom_properties,
|
||||||
clip_rects,
|
clip_rects,
|
||||||
translates,
|
translates,
|
||||||
text_runs,
|
text_runs,
|
||||||
|
|||||||
@@ -461,6 +461,26 @@ impl PreparedRender {
|
|||||||
css_color(style.background_color.unwrap_or([0, 0, 0, 0])),
|
css_color(style.background_color.unwrap_or([0, 0, 0, 0])),
|
||||||
);
|
);
|
||||||
out.insert("color", css_color(style.color.unwrap_or([0, 0, 0, 255])));
|
out.insert("color", css_color(style.color.unwrap_or([0, 0, 0, 255])));
|
||||||
|
out.insert("font-size", css_px(style.font_size.unwrap_or(16.0)));
|
||||||
|
out.insert(
|
||||||
|
"font-weight",
|
||||||
|
style
|
||||||
|
.font_weight
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "400".to_string()),
|
||||||
|
);
|
||||||
|
if let Some(family) = &style.font_family {
|
||||||
|
out.insert("font-family", family.clone());
|
||||||
|
}
|
||||||
|
out.insert(
|
||||||
|
"line-height",
|
||||||
|
match style.line_height.unwrap_or(crate::LineHeight::Normal) {
|
||||||
|
crate::LineHeight::Normal => "normal".to_string(),
|
||||||
|
crate::LineHeight::Ratio(value) => css_number(value),
|
||||||
|
crate::LineHeight::Px(value) => css_px(value),
|
||||||
|
crate::LineHeight::Relative(value) => dimension_css(value, "normal"),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(rect) = rect {
|
if let Some(rect) = rect {
|
||||||
let horizontal_non_content =
|
let horizontal_non_content =
|
||||||
@@ -621,6 +641,18 @@ impl PreparedRender {
|
|||||||
Some(out)
|
Some(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cascaded custom properties exposed by CSSOM alongside the compact
|
||||||
|
/// fixed-property snapshot above. These maps are shared across unchanged
|
||||||
|
/// inherited subtrees by `DomLayout`, so reading one does not require a
|
||||||
|
/// second cascade or duplicate every design token for every descendant.
|
||||||
|
pub fn computed_custom_properties(
|
||||||
|
&self,
|
||||||
|
id: obscura_dom::tree::NodeId,
|
||||||
|
) -> Option<HashMap<String, String>> {
|
||||||
|
let properties = self.layout.custom_properties.get(&id)?;
|
||||||
|
Some(properties.as_ref().clone())
|
||||||
|
}
|
||||||
|
|
||||||
/// Border box in the current root viewport. This is the read-only geometry
|
/// Border box in the current root viewport. This is the read-only geometry
|
||||||
/// path used by a later CSSOM integration and shares paint's clamped scroll,
|
/// path used by a later CSSOM integration and shares paint's clamped scroll,
|
||||||
/// fixed-subtree, and sticky-positioning derivatives.
|
/// fixed-subtree, and sticky-positioning derivatives.
|
||||||
|
|||||||
Reference in new issue
Block a user