Expose live computed CSS variables to scripts

This commit is contained in:
SGavrl committed 2026-07-31 08:26:08 +00:00
1 parent 2343681497
commit 5f67bad4fe
6 files changed
+264 -14

No files matched your search

+30 -1
View File
@@ -955,7 +955,9 @@ impl Stylesheet {
}
let registration_changed_props = resolved_props != *parent_props;
let has_own = !own.is_empty();
let mut own_names = std::collections::HashSet::new();
for (k, v) in own {
own_names.insert(k.clone());
let registration = self.registered_custom_properties.get(&k);
let set_initial = |props: &mut HashMap<String, String>| {
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 {
Some(resolved_props)
} else {
@@ -1419,7 +1444,11 @@ fn substitute_declarations(css: &str, props: &HashMap<String, String>) -> String
/// represents CSS's guaranteed-invalid value. Crucially, invalidity propagates
/// through an intermediate custom property so an outer var() can use its own
/// 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 {
return None;
}
+13
View File
@@ -225,6 +225,13 @@ impl OverflowClip {
pub struct DomLayout {
pub rects: HashMap<NodeId, Rect>,
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
/// per node, in SCREEN space (the clip owner's box shifted by the owner's
/// accumulated translate; see `resolve_clip_rects`). `None` means
@@ -1348,6 +1355,7 @@ fn cascade_walk(
sheet: &crate::css::Stylesheet,
matcher: &mut obscura_dom::selector::Matcher,
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>>,
container_evaluator: &mut Option<&mut crate::css::ContainerQueryEvaluator<'_>>,
quirks_mode: bool,
@@ -1471,6 +1479,7 @@ fn cascade_walk(
if let Some(m) = effective_props {
this_props = std::rc::Rc::new(m);
}
custom_properties.insert(id, this_props.clone());
let (before_pseudo, after_pseudo) =
if let Some(evaluator) = container_evaluator.as_deref_mut() {
sheet.pseudo_styles_with_container_queries(
@@ -1508,6 +1517,7 @@ fn cascade_walk(
sheet,
matcher,
styles,
custom_properties,
&this_props,
container_evaluator,
quirks_mode,
@@ -1960,6 +1970,7 @@ fn layout_dom_once(
let t1 = std::time::Instant::now();
let mut matcher = tree.matcher();
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 mut evaluator =
snapshot.map(|snapshot| crate::css::ContainerQueryEvaluator::new(tree, snapshot));
@@ -1980,6 +1991,7 @@ fn layout_dom_once(
&sheet,
&mut matcher,
&mut styles,
&mut custom_properties,
&root_props,
&mut evaluator_ref,
quirks_mode,
@@ -3948,6 +3960,7 @@ fn layout_dom_once(
DomLayout {
rects,
styles,
custom_properties,
clip_rects,
translates,
text_runs,
+32
View File
@@ -461,6 +461,26 @@ impl PreparedRender {
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("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 {
let horizontal_non_content =
@@ -621,6 +641,18 @@ impl PreparedRender {
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
/// path used by a later CSSOM integration and shares paint's clamped scroll,
/// fixed-subtree, and sticky-positioning derivatives.