From 02f5d233d3ae5ff6ddf5748c534426327d7d5908 Mon Sep 17 00:00:00 2001 From: SGavrl <277843151+SGavrl@users.noreply.github.com> Date: Fri, 31 Jul 2026 00:08:03 +0000 Subject: [PATCH] Converge container query layout --- crates/obscura-render/src/css.rs | 1530 ++++++++++++++++++++++++++-- crates/obscura-render/src/dom.rs | 835 ++++++++++++++- crates/obscura-render/src/lib.rs | 7 + crates/obscura-render/src/style.rs | 19 +- 4 files changed, 2295 insertions(+), 96 deletions(-) diff --git a/crates/obscura-render/src/css.rs b/crates/obscura-render/src/css.rs index aa87e53..50089b3 100644 --- a/crates/obscura-render/src/css.rs +++ b/crates/obscura-render/src/css.rs @@ -13,7 +13,7 @@ use std::collections::HashMap; use crate::LayoutStyle; -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] struct ContainerConditionId(u32); impl ContainerConditionId { @@ -37,16 +37,31 @@ struct ContainerQuery { #[derive(Clone, Debug, PartialEq)] enum ContainerQueryExpr { Feature(ContainerSizeFeature), + /// Syntactically valid future/general-enclosed syntax has Kleene + /// `unknown` truth. Retaining it prevents one unknown comma arm from + /// discarding supported alternatives in the same `@container` rule. + Unknown, Not(Box), And(Vec), Or(Vec), } #[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum ContainerQueryAxis { Width, InlineSize } +enum ContainerQueryAxis { + Width, + Height, + InlineSize, + BlockSize, +} #[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum ContainerQueryComparison { Min, Max } +enum ContainerQueryComparison { + Min, + Max, + GreaterThan, + LessThan, + Equal, +} #[derive(Clone, Copy, Debug, PartialEq)] struct ContainerSizeFeature { @@ -81,6 +96,373 @@ struct PseudoRule { container_condition_id: ContainerConditionId, } +#[derive(Clone, Debug)] +pub(crate) struct ContainerBox { + pub(crate) container_type: crate::ContainerType, + /// Axes on which size containment actually applies to the generated box. + /// This can be `Normal` even when computed `container-type` is non-normal + /// (for example a non-atomic inline or an internal table box). + pub(crate) available_type: crate::ContainerType, + pub(crate) names: Vec, + pub(crate) content_width: f32, + pub(crate) content_height: f32, + pub(crate) font_size: f32, +} + +impl PartialEq for ContainerBox { + fn eq(&self, other: &Self) -> bool { + if self.container_type != other.container_type + || self.available_type != other.available_type + || self.names != other.names + { + return false; + } + match self.available_type { + crate::ContainerType::Normal => true, + crate::ContainerType::InlineSize => { + self.content_width == other.content_width + && self.font_size == other.font_size + } + crate::ContainerType::Size => { + self.content_width == other.content_width + && self.content_height == other.content_height + && self.font_size == other.font_size + } + } + } +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub(crate) struct ContainerSnapshot { + pub(crate) boxes: HashMap, + pub(crate) root_font_size: f32, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ContainerQueryTruth { + True, + False, + Unknown, +} + +impl ContainerQueryTruth { + fn and(self, other: Self) -> Self { + match (self, other) { + (Self::False, _) | (_, Self::False) => Self::False, + (Self::True, Self::True) => Self::True, + _ => Self::Unknown, + } + } + + fn or(self, other: Self) -> Self { + match (self, other) { + (Self::True, _) | (_, Self::True) => Self::True, + (Self::False, Self::False) => Self::False, + _ => Self::Unknown, + } + } + + fn not(self) -> Self { + match self { + Self::True => Self::False, + Self::False => Self::True, + Self::Unknown => Self::Unknown, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +enum ContainerQuerySubjectKind { + Element, + OriginatingPseudo, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +struct ContainerQueryCacheKey { + subject: NodeId, + condition: ContainerConditionId, + kind: ContainerQuerySubjectKind, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct ContainerQueryDecision { + truth: ContainerQueryTruth, + selected_containers: Vec>, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub(crate) struct ContainerDecisionSignature { + decisions: HashMap, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub(crate) struct ContainerQueryStats { + pub(crate) evaluations: usize, + pub(crate) cache_hits: usize, + pub(crate) ancestor_steps: usize, +} + +pub(crate) struct ContainerQueryEvaluator<'a> { + tree: &'a DomTree, + snapshot: &'a ContainerSnapshot, + cache: HashMap, + stats: ContainerQueryStats, +} + +impl<'a> ContainerQueryEvaluator<'a> { + pub(crate) fn new(tree: &'a DomTree, snapshot: &'a ContainerSnapshot) -> Self { + Self { + tree, + snapshot, + cache: HashMap::new(), + stats: ContainerQueryStats::default(), + } + } + + pub(crate) fn finish(self) -> (ContainerDecisionSignature, ContainerQueryStats) { + ( + ContainerDecisionSignature { + decisions: self.cache, + }, + self.stats, + ) + } + + fn condition_matches( + &mut self, + sheet: &Stylesheet, + subject: NodeId, + condition: ContainerConditionId, + kind: ContainerQuerySubjectKind, + ) -> bool { + self.evaluate_condition_chain(sheet, subject, condition, kind).truth + == ContainerQueryTruth::True + } + + fn evaluate_condition_chain( + &mut self, + sheet: &Stylesheet, + subject: NodeId, + condition: ContainerConditionId, + kind: ContainerQuerySubjectKind, + ) -> ContainerQueryDecision { + if condition == ContainerConditionId::NONE { + return ContainerQueryDecision { + truth: ContainerQueryTruth::True, + selected_containers: Vec::new(), + }; + } + let key = ContainerQueryCacheKey { + subject, + condition, + kind, + }; + if let Some(decision) = self.cache.get(&key) { + self.stats.cache_hits += 1; + return decision.clone(); + } + self.stats.evaluations += 1; + let node = &sheet.container_conditions[condition.0 as usize]; + let mut own_truth = ContainerQueryTruth::False; + let mut selected_containers = Vec::with_capacity(node.alternatives.len()); + for query in &node.alternatives { + let (truth, selected) = self.evaluate_query(subject, kind, query); + own_truth = own_truth.or(truth); + selected_containers.push(selected); + if own_truth == ContainerQueryTruth::True { + break; + } + } + if own_truth == ContainerQueryTruth::False { + let decision = ContainerQueryDecision { + truth: ContainerQueryTruth::False, + selected_containers, + }; + self.cache.insert(key, decision.clone()); + return decision; + } + let parent = + self.evaluate_condition_chain(sheet, subject, node.parent, kind); + selected_containers.extend(parent.selected_containers); + let decision = ContainerQueryDecision { + truth: own_truth.and(parent.truth), + selected_containers, + }; + self.cache.insert(key, decision.clone()); + decision + } + + fn evaluate_query( + &mut self, + subject: NodeId, + kind: ContainerQuerySubjectKind, + query: &ContainerQuery, + ) -> (ContainerQueryTruth, Option) { + let required_axes = query + .condition + .as_ref() + .map(container_query_required_axes) + .unwrap_or_default(); + let mut candidate = match kind { + ContainerQuerySubjectKind::Element => { + self.tree.get_node(subject).and_then(|node| node.parent) + } + ContainerQuerySubjectKind::OriginatingPseudo => Some(subject), + }; + while let Some(id) = candidate { + self.stats.ancestor_steps += 1; + let parent = self.tree.get_node(id).and_then(|node| node.parent); + if let Some(container) = self.snapshot.boxes.get(&id) { + let supports_axis = match container.container_type { + crate::ContainerType::Normal => { + !required_axes.inline && !required_axes.block + } + crate::ContainerType::InlineSize => !required_axes.block, + crate::ContainerType::Size => true, + }; + let name_matches = query + .name + .as_ref() + .map_or(true, |name| container.names.iter().any(|item| item == name)); + if supports_axis && name_matches { + let axis_available = match container.available_type { + crate::ContainerType::Normal => { + !required_axes.inline && !required_axes.block + } + crate::ContainerType::InlineSize => !required_axes.block, + crate::ContainerType::Size => true, + }; + if !axis_available { + return (ContainerQueryTruth::Unknown, Some(id)); + } + let truth = query.condition.as_ref().map_or( + ContainerQueryTruth::True, + |condition| { + evaluate_container_query_expr( + condition, + container, + self.snapshot.root_font_size, + ) + }, + ); + return (truth, Some(id)); + } + } + candidate = parent; + } + (ContainerQueryTruth::False, None) + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +struct ContainerQueryRequiredAxes { + inline: bool, + block: bool, +} + +fn container_query_required_axes(expr: &ContainerQueryExpr) -> ContainerQueryRequiredAxes { + match expr { + ContainerQueryExpr::Feature(feature) => match feature.axis { + ContainerQueryAxis::Width | ContainerQueryAxis::InlineSize => { + ContainerQueryRequiredAxes { + inline: true, + block: false, + } + } + ContainerQueryAxis::Height | ContainerQueryAxis::BlockSize => { + ContainerQueryRequiredAxes { + inline: false, + block: true, + } + } + }, + ContainerQueryExpr::Unknown => ContainerQueryRequiredAxes::default(), + ContainerQueryExpr::Not(inner) => container_query_required_axes(inner), + ContainerQueryExpr::And(items) | ContainerQueryExpr::Or(items) => { + items.iter().fold( + ContainerQueryRequiredAxes::default(), + |mut axes, item| { + let item = container_query_required_axes(item); + axes.inline |= item.inline; + axes.block |= item.block; + axes + }, + ) + } + } +} + +fn evaluate_container_query_expr( + expr: &ContainerQueryExpr, + container: &ContainerBox, + root_font_size: f32, +) -> ContainerQueryTruth { + match expr { + ContainerQueryExpr::Feature(feature) => { + let actual = match feature.axis { + ContainerQueryAxis::Width | ContainerQueryAxis::InlineSize => { + container.content_width + } + ContainerQueryAxis::Height | ContainerQueryAxis::BlockSize => { + container.content_height + } + }; + let threshold = match feature.length { + ContainerQueryLength::Px(value) => value, + ContainerQueryLength::Em(value) => value * container.font_size, + ContainerQueryLength::Rem(value) => value * root_font_size, + }; + if !actual.is_finite() || !threshold.is_finite() { + return ContainerQueryTruth::Unknown; + } + let matches = match feature.comparison { + ContainerQueryComparison::Min => actual >= threshold, + ContainerQueryComparison::Max => actual <= threshold, + ContainerQueryComparison::GreaterThan => actual > threshold, + ContainerQueryComparison::LessThan => actual < threshold, + ContainerQueryComparison::Equal => actual == threshold, + }; + if matches { + ContainerQueryTruth::True + } else { + ContainerQueryTruth::False + } + } + ContainerQueryExpr::Unknown => ContainerQueryTruth::Unknown, + ContainerQueryExpr::Not(inner) => { + evaluate_container_query_expr(inner, container, root_font_size).not() + } + ContainerQueryExpr::And(items) => { + let mut truth = ContainerQueryTruth::True; + for item in items { + truth = truth.and(evaluate_container_query_expr( + item, + container, + root_font_size, + )); + if truth == ContainerQueryTruth::False { + break; + } + } + truth + } + ContainerQueryExpr::Or(items) => { + let mut truth = ContainerQueryTruth::False; + for item in items { + truth = truth.or(evaluate_container_query_expr( + item, + container, + root_font_size, + )); + if truth == ContainerQueryTruth::True { + break; + } + } + truth + } + } +} + /// An indexed set of author rules ready for fast per-element matching. pub struct Stylesheet { rules: Vec, @@ -104,9 +486,48 @@ pub struct Stylesheet { impl Stylesheet { /// Until Stage B supplies completed container geometry, preserved /// conditional rules remain inactive rather than using viewport geometry. - fn container_condition_is_active(&self, id: ContainerConditionId) -> bool { + fn container_condition_is_active( + &self, + id: ContainerConditionId, + subject: NodeId, + kind: ContainerQuerySubjectKind, + evaluator: &mut Option<&mut ContainerQueryEvaluator<'_>>, + ) -> bool { debug_assert!((id.0 as usize) < self.container_conditions.len()); - id == ContainerConditionId::NONE + if id == ContainerConditionId::NONE { + return true; + } + evaluator.as_deref_mut().is_some_and(|evaluator| { + evaluator.condition_matches(self, subject, id, kind) + }) + } + + pub(crate) fn has_container_queries(&self) -> bool { + self.rules + .iter() + .any(|rule| rule.container_condition_id != ContainerConditionId::NONE) + || self + .before_rules + .iter() + .chain(&self.after_rules) + .any(|rule| rule.container_condition_id != ContainerConditionId::NONE) + } + + pub(crate) fn container_condition_depth(&self) -> usize { + self.container_conditions + .iter() + .skip(1) + .map(|node| { + let mut depth = 1usize; + let mut parent = node.parent; + while parent != ContainerConditionId::NONE { + depth += 1; + parent = self.container_conditions[parent.0 as usize].parent; + } + depth + }) + .max() + .unwrap_or(0) } /// Parse and index a set of raw CSS sources (the text of each ` +
+ "#, + ); + let (laid, telemetry) = + layout_dom_with_web_fonts_measured(&tree, (1280.0, 720.0), &HashMap::new(), &[]); + let container = tree.get_element_by_id("container").unwrap(); + let target = tree.get_element_by_id("target").unwrap(); + assert!((laid.rects[&container].width - 500.0).abs() < 0.1); + assert_eq!(laid.styles[&target].width, crate::Dimension::Px(44.0)); + assert_eq!(telemetry.passes, 2); + assert_eq!( + telemetry.termination, + ContainerLayoutTermination::GeometryStable + ); + } + + #[test] + fn container_query_named_and_unnamed_lookup_choose_different_ancestors() { + let tree = parse_html( + r#" +
+ "#, + ); + let laid = layout_dom(&tree, (1280.0, 720.0)); + let target = tree.get_element_by_id("target").unwrap(); + assert_eq!(laid.styles[&target].width, crate::Dimension::Px(11.0)); + assert_eq!(laid.styles[&target].height, crate::Dimension::Px(1.0)); + } + + #[test] + fn nested_container_queries_select_independent_ancestors() { + let tree = parse_html( + r#" +
+ "#, + ); + let laid = layout_dom(&tree, (1280.0, 720.0)); + let target = tree.get_element_by_id("target").unwrap(); + assert_eq!(laid.styles[&target].width, crate::Dimension::Px(19.0)); + } + + #[test] + fn container_query_pseudo_can_select_its_originating_element() { + let tree = parse_html( + r#"
"#, + ); + let laid = layout_dom(&tree, (1280.0, 720.0)); + let container = tree.get_element_by_id("container").unwrap(); + assert_eq!( + laid.styles[&container] + .before_pseudo + .as_ref() + .and_then(|pseudo| pseudo.before_content.as_deref()), + Some("active") + ); + } + + #[test] + fn container_type_zeroes_shrink_to_fit_inline_intrinsic_widths() { + let tree = parse_html( + r#" +
+
+
+
+
"#, + ); + let laid = layout_dom(&tree, (600.0, 300.0)); + for name in ["inline-block", "inline-flex", "inline-grid"] { + let id = tree.get_element_by_id(name).unwrap(); + let rect = laid.rects[&id]; + assert!( + rect.width.abs() < 0.01, + "{name} descendants leaked into contained intrinsic width: {rect:?}" + ); + assert!( + rect.height >= 29.9, + "inline-size containment must retain content-driven block size: {rect:?}" + ); + } + } + + #[test] + fn size_container_auto_block_size_ignores_descendants() { + let tree = parse_html( + r#" +
+
"#, + ); + let laid = layout_dom(&tree, (600.0, 300.0)); + let inline_only = laid.rects[&tree.get_element_by_id("inline-only").unwrap()]; + let both = laid.rects[&tree.get_element_by_id("both").unwrap()]; + assert!((inline_only.height - 80.0).abs() < 0.01, "{inline_only:?}"); + assert!( + both.height.abs() < 0.01, + "size containment must use the zero contain-intrinsic block size: {both:?}" + ); + } + + #[test] + fn container_type_establishes_a_float_containing_formatting_context() { + let tree = parse_html( + r#" +
"#, + ); + let laid = layout_dom(&tree, (600.0, 200.0)); + let container = laid.rects[&tree.get_element_by_id("container").unwrap()]; + assert!( + (container.height - 40.0).abs() < 0.01, + "query container must contain its descendant float: {container:?}" + ); + } + + #[test] + fn ineligible_nearest_inline_container_does_not_fall_through() { + let tree = parse_html( + r#" +
"#, + ); + let laid = layout_dom(&tree, (600.0, 200.0)); + let target = tree.get_element_by_id("target").unwrap(); + assert_eq!( + laid.styles[&target].width, + crate::Dimension::Px(1.0), + "the unavailable nearest query axis must be unknown, not select the outer container" + ); + } + + #[test] + fn ineligible_nearest_table_container_does_not_fall_through() { + let tree = parse_html( + r#" +
"#, + ); + let laid = layout_dom(&tree, (600.0, 200.0)); + let inner = tree.get_element_by_id("inner").unwrap(); + let target = tree.get_element_by_id("target").unwrap(); + assert!( + laid.styles[&inner].is_table_box, + "the internal block approximation lost authored table provenance" + ); + assert_eq!( + laid.styles[&target].width, + crate::Dimension::Px(1.0), + "an unavailable nearest table query axis must not fall through to the outer container" + ); + } + + #[test] + fn formerly_oscillating_inline_container_settles_consistently() { + let tree = parse_html( + r#" +
"#, + ); + let (laid, telemetry) = + layout_dom_with_web_fonts_measured(&tree, (600.0, 200.0), &HashMap::new(), &[]); + let container = laid.rects[&tree.get_element_by_id("container").unwrap()]; + let child = tree.get_element_by_id("child").unwrap(); + assert!(container.width.abs() < 0.01, "{container:?}"); + assert_eq!(laid.styles[&child].width, crate::Dimension::Px(200.0)); + assert!(matches!( + telemetry.termination, + ContainerLayoutTermination::GeometryStable + | ContainerLayoutTermination::SignatureStable + )); + } + + #[test] + fn pass_cap_uses_visible_conservative_fallback_without_panicking() { + let tree = parse_html( + r#" +
"#, + ); + let (laid, telemetry) = layout_dom_with_web_fonts_pass_limit( + &tree, + (600.0, 200.0), + &HashMap::new(), + &[], + Some(2), + ); + let target = tree.get_element_by_id("target").unwrap(); + assert_eq!(laid.styles[&target].width, crate::Dimension::Px(1.0)); + assert_eq!( + telemetry.termination, + ContainerLayoutTermination::PassCapFallback + ); + assert_eq!(telemetry.passes, 3); + } + + #[test] + fn ten_level_container_activation_propagates_without_fixed_pass_truncation() { + let mut css = String::from( + "#c0{container:c0/inline-size;width:100px}#target{width:1px}", + ); + for level in 0..10 { + css.push_str(&format!( + "@container c{level} (min-width:100px){{#c{}{{container:c{}/inline-size;width:100px}}}}", + level + 1, + level + 1 + )); + } + css.push_str( + "@container c10 (min-width:100px){#target{width:77px}}", + ); + let mut body = String::new(); + for level in 0..=10 { + body.push_str(&format!("
")); + } + body.push_str("
"); + for _ in 0..=10 { + body.push_str("
"); + } + let tree = parse_html(&format!("{body}")); + let (laid, telemetry) = + layout_dom_with_web_fonts_measured(&tree, (800.0, 600.0), &HashMap::new(), &[]); + let target = tree.get_element_by_id("target").unwrap(); + assert_eq!(laid.styles[&target].width, crate::Dimension::Px(77.0)); + assert!( + telemetry.passes > 8, + "deep propagation unexpectedly truncated: {telemetry:?}" + ); + assert!(!matches!( + telemetry.termination, + ContainerLayoutTermination::PassCapFallback + | ContainerLayoutTermination::OscillationFallback + )); + } + + #[test] + fn layout_without_container_queries_keeps_one_pass_fast_path() { + let tree = parse_html( + r#" +
"#, + ); + let (_, telemetry) = + layout_dom_with_web_fonts_measured(&tree, (800.0, 600.0), &HashMap::new(), &[]); + assert_eq!( + telemetry, + ContainerLayoutTelemetry { + passes: 1, + termination: ContainerLayoutTermination::NoQueries, + query: crate::css::ContainerQueryStats::default(), + } + ); + } + + #[test] + fn container_convergence_classifies_self_consistent_stability() { + assert_eq!( + container_iteration_termination(true, &3, Some(&2)), + Some(ContainerLayoutTermination::GeometryStable) + ); + assert_eq!( + container_iteration_termination(false, &3, Some(&3)), + Some(ContainerLayoutTermination::SignatureStable) + ); + assert_eq!( + container_iteration_termination(false, &3, Some(&2)), + None + ); + assert_eq!(CONTAINER_LAYOUT_SAFETY_LIMIT, 512); + } + #[test] fn declarative_shadow_style_does_not_leak_into_document_cascade() { let tree = parse_html( diff --git a/crates/obscura-render/src/lib.rs b/crates/obscura-render/src/lib.rs index c7f3ff9..1ca0157 100644 --- a/crates/obscura-render/src/lib.rs +++ b/crates/obscura-render/src/lib.rs @@ -222,6 +222,13 @@ pub struct LayoutStyle { /// layout such as table cells, rather than the computed CSS display. /// Descendants are not CSS flex items in these containers. pub internal_flex_container: bool, + /// The computed inner display is `table`. + /// + /// Taffy has no table display mode, so authored table boxes are represented + /// by block/grid-compatible layout styles. Keep this provenance for CSS + /// features such as container-query axis availability that depend on the + /// computed display rather than our internal layout approximation. + pub(crate) is_table_box: bool, /// The HTML UA sheet's vendor `text-align` behavior for `
`. /// Unlike ordinary `text-align:center`, it also centers fixed-width block /// descendants while leaving auto-width blocks fill-available. diff --git a/crates/obscura-render/src/style.rs b/crates/obscura-render/src/style.rs index 52bf54d..057b30e 100644 --- a/crates/obscura-render/src/style.rs +++ b/crates/obscura-render/src/style.rs @@ -554,20 +554,27 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) { | "contents" ) { style.internal_flex_container = false; + style.is_table_box = false; style.is_inline_block = false; style.flow_root = false; } match value { "none" => style.display = crate::Display::None, "flex" => style.display = crate::Display::Flex, - "inline-flex" => style.display = crate::Display::Flex, + "inline-flex" => { + style.display = crate::Display::Flex; + style.is_inline_block = true; + } "inline" => style.display = crate::Display::Inline, "inline-block" => { style.display = crate::Display::Inline; style.is_inline_block = true; } "grid" => style.display = crate::Display::Grid, - "inline-grid" => style.display = crate::Display::Grid, + "inline-grid" => { + style.display = crate::Display::Grid; + style.is_inline_block = true; + } "block" => style.display = crate::Display::Block, "flow-root" => { style.display = crate::Display::Block; @@ -580,11 +587,13 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) { "table" => { style.display = crate::Display::Block; style.flow_root = true; + style.is_table_box = true; } "inline-table" => { style.display = crate::Display::Inline; style.is_inline_block = true; style.flow_root = true; + style.is_table_box = true; } "contents" => { // `display:contents` can override an earlier `display:none` @@ -4481,11 +4490,17 @@ mod tests { let table = compute_style("div", Some("display:none; display:table")); assert_eq!(table.display, Display::Block); assert!(table.flow_root); + assert!(table.is_table_box); let inline_table = compute_style("div", Some("display:block; display:inline-table")); assert_eq!(inline_table.display, Display::Inline); assert!(inline_table.is_inline_block); assert!(inline_table.flow_root); + assert!(inline_table.is_table_box); + + let reset_table = compute_style("div", Some("display:table; display:grid")); + assert_eq!(reset_table.display, Display::Grid); + assert!(!reset_table.is_table_box); } #[test]