Improve responsive layout and retained rendering
This commit is contained in:
1 parent
ede14b7740
commit
50b2ea14ff
6 files changed
+1552
-101
No files matched your search
@@ -46,6 +46,104 @@ pub struct InvalidationDependency {
|
|||||||
pub reaches: InvalidationReaches,
|
pub reaches: InvalidationReaches,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A cheap positive key from the compound which anchors one `:has()`.
|
||||||
|
///
|
||||||
|
/// This is only an early rejection filter. Compounds without a direct key
|
||||||
|
/// remain unkeyed rather than borrowing a key from `:is()`/`:not()`, whose
|
||||||
|
/// boolean structure cannot be represented by one key without false negatives.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
enum RelationalSelectorKey {
|
||||||
|
Id(String),
|
||||||
|
Class(String),
|
||||||
|
Attribute(String),
|
||||||
|
LocalName(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Invalidation metadata for one `:has()` occurrence.
|
||||||
|
///
|
||||||
|
/// Gecko models the selector inside `:has()` as an upward dependency chain
|
||||||
|
/// (parent/ancestors/previous siblings), then resumes the ordinary selector
|
||||||
|
/// path outside the anchor. Obscura stores the smaller information needed by
|
||||||
|
/// its whole-subtree cascade: an optional anchor key, the outward reach, and
|
||||||
|
/// whether a key-independent child-list side effect can change the match.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) struct RelationalInvalidation {
|
||||||
|
pub rule_order: usize,
|
||||||
|
anchor_key: Option<RelationalSelectorKey>,
|
||||||
|
relative_keys: Vec<RelationalSelectorKey>,
|
||||||
|
pub anchor_reaches: InvalidationReaches,
|
||||||
|
pub unkeyed_subject: bool,
|
||||||
|
pub sibling_side_effect: bool,
|
||||||
|
pub structural_side_effect: bool,
|
||||||
|
pub text_side_effect: bool,
|
||||||
|
pub unrepresentable_outer_path: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RelationalInvalidation {
|
||||||
|
pub(crate) fn anchor_may_match(&self, tree: &DomTree, node: NodeId) -> bool {
|
||||||
|
let Some(dom_node) = tree.get_node(node) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if dom_node.as_element().is_none() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let quirks = tree.is_quirks();
|
||||||
|
match self.anchor_key.as_ref() {
|
||||||
|
None => true,
|
||||||
|
Some(RelationalSelectorKey::Id(expected)) => dom_node
|
||||||
|
.get_attribute("id")
|
||||||
|
.is_some_and(|actual| {
|
||||||
|
actual == expected || (quirks && actual.eq_ignore_ascii_case(expected))
|
||||||
|
}),
|
||||||
|
Some(RelationalSelectorKey::Class(expected)) => dom_node
|
||||||
|
.get_attribute("class")
|
||||||
|
.is_some_and(|classes| {
|
||||||
|
classes.split_whitespace().any(|actual| {
|
||||||
|
actual == expected || (quirks && actual.eq_ignore_ascii_case(expected))
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
Some(RelationalSelectorKey::Attribute(expected)) => {
|
||||||
|
dom_node.get_attribute(expected).is_some()
|
||||||
|
}
|
||||||
|
Some(RelationalSelectorKey::LocalName(expected)) => dom_node
|
||||||
|
.as_element()
|
||||||
|
.is_some_and(|element| element.local.as_ref().eq_ignore_ascii_case(expected)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn relative_path_may_match(&self, tree: &DomTree, node: NodeId) -> bool {
|
||||||
|
let Some(dom_node) = tree.get_node(node) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some(element) = dom_node.as_element() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let quirks = tree.is_quirks();
|
||||||
|
self.relative_keys.iter().any(|key| match key {
|
||||||
|
RelationalSelectorKey::Id(expected) => dom_node
|
||||||
|
.get_attribute("id")
|
||||||
|
.is_some_and(|actual| {
|
||||||
|
actual == expected || (quirks && actual.eq_ignore_ascii_case(expected))
|
||||||
|
}),
|
||||||
|
RelationalSelectorKey::Class(expected) => dom_node
|
||||||
|
.get_attribute("class")
|
||||||
|
.is_some_and(|classes| {
|
||||||
|
classes.split_whitespace().any(|actual| {
|
||||||
|
actual == expected || (quirks && actual.eq_ignore_ascii_case(expected))
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
RelationalSelectorKey::Attribute(expected) => dom_node.attrs().is_some_and(|attrs| {
|
||||||
|
attrs
|
||||||
|
.iter()
|
||||||
|
.any(|attribute| attribute.name.local.as_ref().eq_ignore_ascii_case(expected))
|
||||||
|
}),
|
||||||
|
RelationalSelectorKey::LocalName(expected) => {
|
||||||
|
element.local.as_ref().eq_ignore_ascii_case(expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Selector dependencies retained alongside the compiled stylesheet.
|
/// Selector dependencies retained alongside the compiled stylesheet.
|
||||||
///
|
///
|
||||||
/// This follows Gecko's conservative invalidation-map shape: live mutations
|
/// This follows Gecko's conservative invalidation-map shape: live mutations
|
||||||
@@ -62,6 +160,7 @@ pub struct InvalidationMap {
|
|||||||
conservative_rule_orders: Vec<usize>,
|
conservative_rule_orders: Vec<usize>,
|
||||||
relational_rule_orders: Vec<usize>,
|
relational_rule_orders: Vec<usize>,
|
||||||
unkeyed_relational_rule_orders: Vec<usize>,
|
unkeyed_relational_rule_orders: Vec<usize>,
|
||||||
|
relational_invalidations: Vec<RelationalInvalidation>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InvalidationMap {
|
impl InvalidationMap {
|
||||||
@@ -110,6 +209,10 @@ impl InvalidationMap {
|
|||||||
!self.unkeyed_relational_rule_orders.is_empty()
|
!self.unkeyed_relational_rule_orders.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn relational_invalidations(&self) -> &[RelationalInvalidation] {
|
||||||
|
&self.relational_invalidations
|
||||||
|
}
|
||||||
|
|
||||||
pub fn dependency_count(&self) -> usize {
|
pub fn dependency_count(&self) -> usize {
|
||||||
self.ids
|
self.ids
|
||||||
.values()
|
.values()
|
||||||
@@ -192,6 +295,12 @@ impl InvalidationMap {
|
|||||||
self.unkeyed_relational_rule_orders.push(rule_order);
|
self.unkeyed_relational_rule_orders.push(rule_order);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn push_relational_invalidation(&mut self, invalidation: RelationalInvalidation) {
|
||||||
|
if !self.relational_invalidations.contains(&invalidation) {
|
||||||
|
self.relational_invalidations.push(invalidation);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compose_invalidation_reach(
|
fn compose_invalidation_reach(
|
||||||
@@ -567,6 +676,259 @@ fn relative_selector_subject_has_key(selector: &str) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pick one positive key which every match of this exact anchor compound must
|
||||||
|
/// have. Functional pseudos are skipped as opaque boolean expressions; using
|
||||||
|
/// a key from one `:is()` arm as a mandatory filter would be unsound.
|
||||||
|
fn relational_anchor_key(compound: &str) -> Option<RelationalSelectorKey> {
|
||||||
|
if let Some(local_name) = compound_local_name(compound) {
|
||||||
|
return Some(RelationalSelectorKey::LocalName(local_name));
|
||||||
|
}
|
||||||
|
let chars = compound.chars().collect::<Vec<_>>();
|
||||||
|
let mut index = 0usize;
|
||||||
|
while index < chars.len() {
|
||||||
|
match chars[index] {
|
||||||
|
'#' => {
|
||||||
|
let (id, next) = consume_css_identifier(&chars, index + 1);
|
||||||
|
if !id.is_empty() {
|
||||||
|
return Some(RelationalSelectorKey::Id(id));
|
||||||
|
}
|
||||||
|
index = next.max(index + 1);
|
||||||
|
}
|
||||||
|
'.' => {
|
||||||
|
let (class, next) = consume_css_identifier(&chars, index + 1);
|
||||||
|
if !class.is_empty() {
|
||||||
|
return Some(RelationalSelectorKey::Class(class));
|
||||||
|
}
|
||||||
|
index = next.max(index + 1);
|
||||||
|
}
|
||||||
|
'[' => {
|
||||||
|
let close = matching_delimiter(&chars, index, '[', ']')?;
|
||||||
|
let mut name_index = index + 1;
|
||||||
|
while chars.get(name_index).is_some_and(|ch| ch.is_whitespace()) {
|
||||||
|
name_index += 1;
|
||||||
|
}
|
||||||
|
// `Node::get_attribute` is intentionally the unqualified HTML
|
||||||
|
// lookup. A namespace-qualified selector cannot use that as a
|
||||||
|
// mandatory anchor filter without risking a false rejection.
|
||||||
|
if chars.get(name_index) == Some(&'*') || chars.get(name_index) == Some(&'|') {
|
||||||
|
index = close + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (first, first_end) = consume_css_identifier(&chars, name_index);
|
||||||
|
if chars.get(first_end) == Some(&'|') {
|
||||||
|
index = close + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (attribute, end) = (first, first_end);
|
||||||
|
if !attribute.is_empty() && end <= close {
|
||||||
|
return Some(RelationalSelectorKey::Attribute(
|
||||||
|
attribute.to_ascii_lowercase(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
index = close + 1;
|
||||||
|
}
|
||||||
|
':' => {
|
||||||
|
let pseudo_start = index + usize::from(chars.get(index + 1) == Some(&':')) + 1;
|
||||||
|
let (_, next) = consume_css_identifier(&chars, pseudo_start);
|
||||||
|
if chars.get(next) == Some(&'(') {
|
||||||
|
let close = matching_delimiter(&chars, next, '(', ')')?;
|
||||||
|
index = close + 1;
|
||||||
|
} else {
|
||||||
|
index = next.max(index + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'\\' => index = (index + 2).min(chars.len()),
|
||||||
|
_ => index += 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_relational_selector_key(
|
||||||
|
keys: &mut Vec<RelationalSelectorKey>,
|
||||||
|
key: RelationalSelectorKey,
|
||||||
|
) {
|
||||||
|
if !keys.contains(&key) {
|
||||||
|
keys.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collect cheap positive keys anywhere along a relative-selector path. A
|
||||||
|
/// child-list insertion which contains none of these keys cannot make the path
|
||||||
|
/// start matching. This is an early rejection only: each key is treated as an
|
||||||
|
/// alternative trigger, so compounds requiring several keys remain sound.
|
||||||
|
fn collect_relational_selector_keys(
|
||||||
|
selector: &str,
|
||||||
|
keys: &mut Vec<RelationalSelectorKey>,
|
||||||
|
) {
|
||||||
|
let selector = selector.trim();
|
||||||
|
let selector = selector
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.filter(|character| matches!(character, '>' | '+' | '~'))
|
||||||
|
.map(|character| selector[character.len_utf8()..].trim_start())
|
||||||
|
.unwrap_or(selector);
|
||||||
|
let (compounds, _) = invalidation_compounds(selector);
|
||||||
|
for (compound, _) in compounds {
|
||||||
|
collect_relational_compound_keys(&compound, keys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_relational_compound_keys(
|
||||||
|
compound: &str,
|
||||||
|
keys: &mut Vec<RelationalSelectorKey>,
|
||||||
|
) {
|
||||||
|
if let Some(local_name) = compound_local_name(compound) {
|
||||||
|
push_relational_selector_key(keys, RelationalSelectorKey::LocalName(local_name));
|
||||||
|
}
|
||||||
|
let chars = compound.chars().collect::<Vec<_>>();
|
||||||
|
let mut index = 0usize;
|
||||||
|
while index < chars.len() {
|
||||||
|
match chars[index] {
|
||||||
|
'#' => {
|
||||||
|
let (id, next) = consume_css_identifier(&chars, index + 1);
|
||||||
|
if !id.is_empty() {
|
||||||
|
push_relational_selector_key(keys, RelationalSelectorKey::Id(id));
|
||||||
|
}
|
||||||
|
index = next.max(index + 1);
|
||||||
|
}
|
||||||
|
'.' => {
|
||||||
|
let (class, next) = consume_css_identifier(&chars, index + 1);
|
||||||
|
if !class.is_empty() {
|
||||||
|
push_relational_selector_key(keys, RelationalSelectorKey::Class(class));
|
||||||
|
}
|
||||||
|
index = next.max(index + 1);
|
||||||
|
}
|
||||||
|
'[' => {
|
||||||
|
let Some(close) = matching_delimiter(&chars, index, '[', ']') else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut name_index = index + 1;
|
||||||
|
while chars.get(name_index).is_some_and(|ch| ch.is_whitespace()) {
|
||||||
|
name_index += 1;
|
||||||
|
}
|
||||||
|
let (first, first_end) = if chars.get(name_index) == Some(&'*') {
|
||||||
|
(String::new(), name_index + 1)
|
||||||
|
} else {
|
||||||
|
consume_css_identifier(&chars, name_index)
|
||||||
|
};
|
||||||
|
let (attribute, end) = if chars.get(first_end) == Some(&'|') {
|
||||||
|
consume_css_identifier(&chars, first_end + 1)
|
||||||
|
} else {
|
||||||
|
(first, first_end)
|
||||||
|
};
|
||||||
|
if !attribute.is_empty() && end <= close {
|
||||||
|
push_relational_selector_key(
|
||||||
|
keys,
|
||||||
|
RelationalSelectorKey::Attribute(attribute.to_ascii_lowercase()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
index = close + 1;
|
||||||
|
}
|
||||||
|
':' => {
|
||||||
|
let pseudo_start = index + usize::from(chars.get(index + 1) == Some(&':')) + 1;
|
||||||
|
let (name, next) = consume_css_identifier(&chars, pseudo_start);
|
||||||
|
if chars.get(next) != Some(&'(') {
|
||||||
|
index = next.max(index + 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(close) = matching_delimiter(&chars, next, '(', ')') else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let arguments = chars[next + 1..close].iter().collect::<String>();
|
||||||
|
match name.to_ascii_lowercase().as_str() {
|
||||||
|
"is" | "where" | "not" | "has" => {
|
||||||
|
for alternative in split_selector_list(&arguments) {
|
||||||
|
collect_relational_selector_keys(alternative.trim(), keys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"nth-child" | "nth-last-child" | "nth-of-type" | "nth-last-of-type" => {
|
||||||
|
if let Some(of_selector) = nth_of_selector(&arguments) {
|
||||||
|
for alternative in split_selector_list(of_selector) {
|
||||||
|
collect_relational_selector_keys(alternative.trim(), keys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
index = close + 1;
|
||||||
|
}
|
||||||
|
'\\' => index = (index + 2).min(chars.len()),
|
||||||
|
_ => index += 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selector_contains_pseudo(selector: &str, names: &[&str]) -> bool {
|
||||||
|
let chars = selector.chars().collect::<Vec<_>>();
|
||||||
|
let mut index = 0usize;
|
||||||
|
let mut quote = None;
|
||||||
|
let mut bracket_depth = 0usize;
|
||||||
|
while index < chars.len() {
|
||||||
|
let ch = chars[index];
|
||||||
|
if ch == '\\' {
|
||||||
|
index = (index + 2).min(chars.len());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(active_quote) = quote {
|
||||||
|
if ch == active_quote {
|
||||||
|
quote = None;
|
||||||
|
}
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match ch {
|
||||||
|
'\'' | '"' => quote = Some(ch),
|
||||||
|
'[' => bracket_depth += 1,
|
||||||
|
']' => bracket_depth = bracket_depth.saturating_sub(1),
|
||||||
|
':' if bracket_depth == 0 && chars.get(index + 1) != Some(&':') => {
|
||||||
|
let (name, next) = consume_css_identifier(&chars, index + 1);
|
||||||
|
if names
|
||||||
|
.iter()
|
||||||
|
.any(|expected| name.eq_ignore_ascii_case(expected))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
index = next.max(index + 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selector_contains_adjacent_combinator(selector: &str) -> bool {
|
||||||
|
let chars = selector.chars().collect::<Vec<_>>();
|
||||||
|
let mut index = 0usize;
|
||||||
|
let mut quote = None;
|
||||||
|
let mut bracket_depth = 0usize;
|
||||||
|
while index < chars.len() {
|
||||||
|
let ch = chars[index];
|
||||||
|
if ch == '\\' {
|
||||||
|
index = (index + 2).min(chars.len());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(active_quote) = quote {
|
||||||
|
if ch == active_quote {
|
||||||
|
quote = None;
|
||||||
|
}
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match ch {
|
||||||
|
'\'' | '"' => quote = Some(ch),
|
||||||
|
'[' => bracket_depth += 1,
|
||||||
|
']' => bracket_depth = bracket_depth.saturating_sub(1),
|
||||||
|
'+' if bracket_depth == 0 => return true,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
fn note_compound_dependencies(
|
fn note_compound_dependencies(
|
||||||
map: &mut InvalidationMap,
|
map: &mut InvalidationMap,
|
||||||
compound: &str,
|
compound: &str,
|
||||||
@@ -688,10 +1050,55 @@ fn note_compound_dependencies(
|
|||||||
);
|
);
|
||||||
map.mark_conservative(rule_order);
|
map.mark_conservative(rule_order);
|
||||||
let alternatives = split_selector_list(&arguments);
|
let alternatives = split_selector_list(&arguments);
|
||||||
|
let mut relative_keys = Vec::new();
|
||||||
|
for alternative in &alternatives {
|
||||||
|
collect_relational_selector_keys(
|
||||||
|
alternative.trim(),
|
||||||
|
&mut relative_keys,
|
||||||
|
);
|
||||||
|
}
|
||||||
let unkeyed = alternatives.iter().any(|alternative| {
|
let unkeyed = alternatives.iter().any(|alternative| {
|
||||||
!relative_selector_subject_has_key(alternative.trim())
|
!relative_selector_subject_has_key(alternative.trim())
|
||||||
});
|
});
|
||||||
map.mark_relational(rule_order, unkeyed);
|
map.mark_relational(rule_order, unkeyed);
|
||||||
|
let sibling_side_effect = alternatives
|
||||||
|
.iter()
|
||||||
|
.any(|alternative| {
|
||||||
|
selector_contains_adjacent_combinator(alternative.trim())
|
||||||
|
});
|
||||||
|
let structural_side_effect = alternatives.iter().any(|alternative| {
|
||||||
|
selector_contains_pseudo(
|
||||||
|
alternative.trim(),
|
||||||
|
&[
|
||||||
|
"empty",
|
||||||
|
"first-child",
|
||||||
|
"last-child",
|
||||||
|
"only-child",
|
||||||
|
"first-of-type",
|
||||||
|
"last-of-type",
|
||||||
|
"only-of-type",
|
||||||
|
"nth-child",
|
||||||
|
"nth-last-child",
|
||||||
|
"nth-of-type",
|
||||||
|
"nth-last-of-type",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let text_side_effect = alternatives.iter().any(|alternative| {
|
||||||
|
selector_contains_pseudo(alternative.trim(), &["empty"])
|
||||||
|
});
|
||||||
|
map.push_relational_invalidation(RelationalInvalidation {
|
||||||
|
rule_order,
|
||||||
|
anchor_key: relational_anchor_key(compound),
|
||||||
|
relative_keys,
|
||||||
|
anchor_reaches: reaches,
|
||||||
|
unkeyed_subject: unkeyed,
|
||||||
|
sibling_side_effect,
|
||||||
|
structural_side_effect,
|
||||||
|
text_side_effect,
|
||||||
|
unrepresentable_outer_path: reaches
|
||||||
|
.contains(InvalidationReaches::CONSERVATIVE),
|
||||||
|
});
|
||||||
for alternative in alternatives {
|
for alternative in alternatives {
|
||||||
note_selector_dependencies(
|
note_selector_dependencies(
|
||||||
map,
|
map,
|
||||||
@@ -5972,6 +6379,59 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relational_invalidation_records_anchor_reach_and_tree_side_effects() {
|
||||||
|
let map = test_invalidation_map(
|
||||||
|
r#"
|
||||||
|
.host:has(.signal) .out { color:red }
|
||||||
|
.row:has(+ :is(.notice,.warning)) { color:red }
|
||||||
|
.card:has(.item:first-child) { color:red }
|
||||||
|
.shell:has(.label:empty) { color:red }
|
||||||
|
.anchor:has(.signal) ~ .panel .leaf { color:red }
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
let entries = map.relational_invalidations();
|
||||||
|
assert_eq!(entries.len(), 5);
|
||||||
|
assert!(entries[0]
|
||||||
|
.anchor_reaches
|
||||||
|
.contains(InvalidationReaches::DESCENDANTS));
|
||||||
|
assert!(!entries[0].unkeyed_subject);
|
||||||
|
assert!(entries[1].sibling_side_effect);
|
||||||
|
assert!(!entries[1].unkeyed_subject);
|
||||||
|
assert!(entries[2].structural_side_effect);
|
||||||
|
assert!(!entries[2].text_side_effect);
|
||||||
|
assert!(entries[3].structural_side_effect);
|
||||||
|
assert!(entries[3].text_side_effect);
|
||||||
|
assert!(entries[4].unrepresentable_outer_path);
|
||||||
|
|
||||||
|
let tree = obscura_dom::parse_html(
|
||||||
|
"<section id=host class=host></section><section id=other class=other></section>",
|
||||||
|
);
|
||||||
|
assert!(entries[0].anchor_may_match(
|
||||||
|
&tree,
|
||||||
|
tree.get_element_by_id("host").unwrap(),
|
||||||
|
));
|
||||||
|
assert!(!entries[0].anchor_may_match(
|
||||||
|
&tree,
|
||||||
|
tree.get_element_by_id("other").unwrap(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relational_anchor_filter_never_unqualifies_namespaced_attributes() {
|
||||||
|
for compound in [
|
||||||
|
"[xlink|href]:has(.signal)",
|
||||||
|
"[*|href]:has(.signal)",
|
||||||
|
"[|href]:has(.signal)",
|
||||||
|
] {
|
||||||
|
assert_eq!(relational_anchor_key(compound), None, "{compound}");
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
relational_anchor_key("[xlink|href].host:has(.signal)"),
|
||||||
|
Some(RelationalSelectorKey::Class("host".into())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn invalidation_map_includes_generated_content_host_dependencies() {
|
fn invalidation_map_includes_generated_content_host_dependencies() {
|
||||||
let map = test_invalidation_map(
|
let map = test_invalidation_map(
|
||||||
|
|||||||
@@ -2460,68 +2460,140 @@ fn node_is_style_text(tree: &DomTree, node: NodeId, parent: Option<NodeId>) -> b
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dependencies_include_relational_rule(
|
fn subtree_may_match_relational_path(
|
||||||
map: &crate::css::InvalidationMap,
|
|
||||||
dependencies: &[crate::css::InvalidationDependency],
|
|
||||||
) -> bool {
|
|
||||||
dependencies
|
|
||||||
.iter()
|
|
||||||
.any(|dependency| {
|
|
||||||
map.is_relational_rule(dependency.rule_order)
|
|
||||||
&& dependency
|
|
||||||
.reaches
|
|
||||||
.contains(crate::css::InvalidationReaches::CONSERVATIVE)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn subtree_may_affect_relational_selector(
|
|
||||||
tree: &DomTree,
|
tree: &DomTree,
|
||||||
map: &crate::css::InvalidationMap,
|
invalidation: &crate::css::RelationalInvalidation,
|
||||||
root: NodeId,
|
root: NodeId,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if map.has_unkeyed_relational_rules() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
std::iter::once(root)
|
std::iter::once(root)
|
||||||
.chain(tree.descendants(root))
|
.chain(tree.descendants(root))
|
||||||
.any(|id| {
|
.any(|id| invalidation.relative_path_may_match(tree, id))
|
||||||
let Some(node) = tree.get_node(id) else {
|
}
|
||||||
return false;
|
|
||||||
|
/// Candidate anchors after an insertion. Relative selectors search from the
|
||||||
|
/// changed subtree toward ancestors and earlier siblings; only the boundary
|
||||||
|
/// can connect the already-fresh inserted subtree to retained elements.
|
||||||
|
fn add_inserted_relational_anchor_candidates(
|
||||||
|
tree: &DomTree,
|
||||||
|
node: NodeId,
|
||||||
|
candidates: &mut HashSet<NodeId>,
|
||||||
|
) {
|
||||||
|
let mut current = Some(node);
|
||||||
|
while let Some(id) = current {
|
||||||
|
let mut sibling = tree.get_node(id).and_then(|node| node.prev_sibling);
|
||||||
|
while let Some(previous) = sibling {
|
||||||
|
candidates.insert(previous);
|
||||||
|
sibling = tree.get_node(previous).and_then(|node| node.prev_sibling);
|
||||||
|
}
|
||||||
|
current = tree.get_node(id).and_then(|node| node.parent);
|
||||||
|
if let Some(parent) = current {
|
||||||
|
candidates.insert(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removal has already destroyed the old sibling links. As Gecko does for a
|
||||||
|
/// removal side effect, inspect every sibling at each old ancestor boundary;
|
||||||
|
/// this includes the old previous/next neighbors without retaining a DOM
|
||||||
|
/// snapshot in every mutation record.
|
||||||
|
fn add_removed_relational_anchor_candidates(
|
||||||
|
tree: &DomTree,
|
||||||
|
old_parent: NodeId,
|
||||||
|
candidates: &mut HashSet<NodeId>,
|
||||||
|
) {
|
||||||
|
let mut current = Some(old_parent);
|
||||||
|
while let Some(id) = current {
|
||||||
|
candidates.insert(id);
|
||||||
|
candidates.extend(tree.children(id));
|
||||||
|
let parent = tree.get_node(id).and_then(|node| node.parent);
|
||||||
|
if let Some(parent) = parent {
|
||||||
|
candidates.extend(tree.children(parent));
|
||||||
|
}
|
||||||
|
current = parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_relational_anchor_scope(
|
||||||
|
tree: &DomTree,
|
||||||
|
anchor: NodeId,
|
||||||
|
reaches: crate::css::InvalidationReaches,
|
||||||
|
dirty: &mut HashSet<NodeId>,
|
||||||
|
) {
|
||||||
|
// Re-cascading the anchor subtree covers anchor-self changes, inheritance,
|
||||||
|
// and every descendant subject. It is intentionally broader than the
|
||||||
|
// dependency's exact reach but keeps the retained-style implementation
|
||||||
|
// independent of selector matching internals.
|
||||||
|
add_style_subtree(tree, anchor, dirty);
|
||||||
|
if reaches.contains(crate::css::InvalidationReaches::SIBLINGS) {
|
||||||
|
add_following_sibling_subtrees(tree, anchor, dirty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply Gecko-style upward `:has()` invalidation for one child-list or text
|
||||||
|
/// mutation. Returns false only when the path outside the anchor combines
|
||||||
|
/// traversals which the renderer's flat reach bits cannot represent soundly.
|
||||||
|
fn add_relational_tree_invalidation(
|
||||||
|
tree: &DomTree,
|
||||||
|
map: &crate::css::InvalidationMap,
|
||||||
|
mutation: &TreeStyleMutation,
|
||||||
|
dirty: &mut HashSet<NodeId>,
|
||||||
|
) -> bool {
|
||||||
|
if map.relational_invalidations().is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let mut candidates = HashSet::new();
|
||||||
|
match *mutation {
|
||||||
|
TreeStyleMutation::Insert {
|
||||||
|
node,
|
||||||
|
old_parent,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
add_inserted_relational_anchor_candidates(tree, node, &mut candidates);
|
||||||
|
if let Some(old_parent) = old_parent {
|
||||||
|
add_removed_relational_anchor_candidates(tree, old_parent, &mut candidates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TreeStyleMutation::Remove { old_parent, .. } => {
|
||||||
|
add_removed_relational_anchor_candidates(tree, old_parent, &mut candidates);
|
||||||
|
}
|
||||||
|
TreeStyleMutation::Text { node, parent } => {
|
||||||
|
add_inserted_relational_anchor_candidates(tree, node, &mut candidates);
|
||||||
|
if let Some(parent) = parent {
|
||||||
|
candidates.insert(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for invalidation in map.relational_invalidations() {
|
||||||
|
let triggered = match mutation {
|
||||||
|
TreeStyleMutation::Remove { .. } => true,
|
||||||
|
TreeStyleMutation::Insert { node, .. } => {
|
||||||
|
subtree_may_match_relational_path(tree, invalidation, *node)
|
||||||
|
|| invalidation.unkeyed_subject
|
||||||
|
|| invalidation.sibling_side_effect
|
||||||
|
|| invalidation.structural_side_effect
|
||||||
|
}
|
||||||
|
TreeStyleMutation::Text { .. } => invalidation.text_side_effect,
|
||||||
};
|
};
|
||||||
let Some(_element) = node.as_element() else {
|
if !triggered {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for anchor in candidates.iter().copied() {
|
||||||
|
if !invalidation.anchor_may_match(tree, anchor) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if invalidation.unrepresentable_outer_path {
|
||||||
return false;
|
return false;
|
||||||
};
|
|
||||||
if node.get_attribute("id").is_some_and(|id| {
|
|
||||||
dependencies_include_relational_rule(map, map.id_dependencies(id))
|
|
||||||
}) {
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
if node.get_attribute("class").is_some_and(|classes| {
|
add_relational_anchor_scope(
|
||||||
classes.split_whitespace().any(|class| {
|
tree,
|
||||||
dependencies_include_relational_rule(map, map.class_dependencies(class))
|
anchor,
|
||||||
})
|
invalidation.anchor_reaches,
|
||||||
}) {
|
dirty,
|
||||||
return true;
|
);
|
||||||
}
|
}
|
||||||
if dependencies_include_relational_rule(
|
|
||||||
map,
|
|
||||||
map.local_name_dependencies(
|
|
||||||
node.as_element()
|
|
||||||
.map(|element| element.local.as_ref())
|
|
||||||
.unwrap_or_default(),
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
node.attrs().is_some_and(|attributes| {
|
true
|
||||||
attributes.iter().any(|attribute| {
|
|
||||||
dependencies_include_relational_rule(
|
|
||||||
map,
|
|
||||||
map.attribute_dependencies(&attribute.qualified_name()),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A child-list change can alter every structural/sibling match under each
|
/// A child-list change can alter every structural/sibling match under each
|
||||||
@@ -2580,12 +2652,7 @@ fn retained_style_plan(
|
|||||||
{
|
{
|
||||||
return RetainedStylePlan::Full;
|
return RetainedStylePlan::Full;
|
||||||
}
|
}
|
||||||
if subtree_may_affect_relational_selector(tree, map, node)
|
if !add_relational_tree_invalidation(tree, map, mutation, &mut dirty) {
|
||||||
|| old_parent.is_some_and(|parent| {
|
|
||||||
subtree_may_affect_relational_selector(tree, map, parent)
|
|
||||||
})
|
|
||||||
|| subtree_may_affect_relational_selector(tree, map, new_parent)
|
|
||||||
{
|
|
||||||
return RetainedStylePlan::Full;
|
return RetainedStylePlan::Full;
|
||||||
}
|
}
|
||||||
add_style_subtree(tree, node, &mut dirty);
|
add_style_subtree(tree, node, &mut dirty);
|
||||||
@@ -2600,9 +2667,7 @@ fn retained_style_plan(
|
|||||||
{
|
{
|
||||||
return RetainedStylePlan::Full;
|
return RetainedStylePlan::Full;
|
||||||
}
|
}
|
||||||
if subtree_may_affect_relational_selector(tree, map, node)
|
if !add_relational_tree_invalidation(tree, map, mutation, &mut dirty) {
|
||||||
|| subtree_may_affect_relational_selector(tree, map, old_parent)
|
|
||||||
{
|
|
||||||
return RetainedStylePlan::Full;
|
return RetainedStylePlan::Full;
|
||||||
}
|
}
|
||||||
add_tree_parent_scope(tree, old_parent, dirty_parent_siblings, &mut dirty);
|
add_tree_parent_scope(tree, old_parent, dirty_parent_siblings, &mut dirty);
|
||||||
@@ -2611,7 +2676,7 @@ fn retained_style_plan(
|
|||||||
if node_is_style_text(tree, node, parent) {
|
if node_is_style_text(tree, node, parent) {
|
||||||
return RetainedStylePlan::Full;
|
return RetainedStylePlan::Full;
|
||||||
}
|
}
|
||||||
if !map.state_dependencies("has").is_empty() {
|
if !add_relational_tree_invalidation(tree, map, mutation, &mut dirty) {
|
||||||
return RetainedStylePlan::Full;
|
return RetainedStylePlan::Full;
|
||||||
}
|
}
|
||||||
if let Some(parent) = parent {
|
if let Some(parent) = parent {
|
||||||
@@ -3037,7 +3102,7 @@ fn layout_dom_once(
|
|||||||
|
|
||||||
// The leaf context is the index of a cosmic-text inline formatting
|
// The leaf context is the index of a cosmic-text inline formatting
|
||||||
// context in `engine`; leaves without text carry no context.
|
// context in `engine`; leaves without text carry no context.
|
||||||
let mut taffy_tree: TaffyTree<usize> = TaffyTree::new();
|
let mut taffy_tree: TaffyTree<usize> = crate::new_taffy_tree();
|
||||||
let mut id_map: HashMap<taffy::NodeId, NodeId> = HashMap::new();
|
let mut id_map: HashMap<taffy::NodeId, NodeId> = HashMap::new();
|
||||||
let mut words: HashMap<taffy::NodeId, (NodeId, String)> = HashMap::new();
|
let mut words: HashMap<taffy::NodeId, (NodeId, String)> = HashMap::new();
|
||||||
let mut engine = crate::inline::TextEngine::new_with_web_fonts(fonts);
|
let mut engine = crate::inline::TextEngine::new_with_web_fonts(fonts);
|
||||||
@@ -3219,6 +3284,13 @@ fn layout_dom_once(
|
|||||||
// inherited context for a fresh descendant directly from the
|
// inherited context for a fresh descendant directly from the
|
||||||
// prior computed values, then retain the style byte-for-byte.
|
// prior computed values, then retain the style byte-for-byte.
|
||||||
if let Some(style) = styles.get(&id) {
|
if let Some(style) = styles.get(&id) {
|
||||||
|
crate::style::set_grid_calc_context(
|
||||||
|
style,
|
||||||
|
style.font_size.or(inh.font_size).unwrap_or(16.0),
|
||||||
|
root_fs,
|
||||||
|
vw,
|
||||||
|
vh,
|
||||||
|
);
|
||||||
inh.display = style.display;
|
inh.display = style.display;
|
||||||
inh.display_contents = style.display_contents;
|
inh.display_contents = style.display_contents;
|
||||||
inh.is_inline_block = style.is_inline_block;
|
inh.is_inline_block = style.is_inline_block;
|
||||||
@@ -3354,6 +3426,8 @@ fn layout_dom_once(
|
|||||||
(
|
(
|
||||||
parent.grid_auto_columns.clone(),
|
parent.grid_auto_columns.clone(),
|
||||||
parent.grid_auto_rows.clone(),
|
parent.grid_auto_rows.clone(),
|
||||||
|
parent.grid_calc_expressions[2].clone(),
|
||||||
|
parent.grid_calc_expressions[3].clone(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
@@ -3365,6 +3439,10 @@ fn layout_dom_once(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|tracks| tracks.0.clone())
|
.map(|tracks| tracks.0.clone())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
style.grid_calc_expressions[2] = inherited_grid_auto_tracks
|
||||||
|
.as_ref()
|
||||||
|
.map(|tracks| tracks.2.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
style.grid_auto_columns_inherit = false;
|
style.grid_auto_columns_inherit = false;
|
||||||
}
|
}
|
||||||
if style.grid_auto_rows_inherit {
|
if style.grid_auto_rows_inherit {
|
||||||
@@ -3372,6 +3450,10 @@ fn layout_dom_once(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|tracks| tracks.1.clone())
|
.map(|tracks| tracks.1.clone())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
style.grid_calc_expressions[3] = inherited_grid_auto_tracks
|
||||||
|
.as_ref()
|
||||||
|
.map(|tracks| tracks.3.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
style.grid_auto_rows_inherit = false;
|
style.grid_auto_rows_inherit = false;
|
||||||
}
|
}
|
||||||
if style.display_inherit {
|
if style.display_inherit {
|
||||||
@@ -3419,6 +3501,7 @@ fn layout_dom_once(
|
|||||||
// em in non-font-size properties is relative to this element's
|
// em in non-font-size properties is relative to this element's
|
||||||
// OWN computed font-size; resolve every relative length now.
|
// OWN computed font-size; resolve every relative length now.
|
||||||
let em_px = style.font_size.unwrap_or(parent_fs);
|
let em_px = style.font_size.unwrap_or(parent_fs);
|
||||||
|
crate::style::set_grid_calc_context(style, em_px, root_fs, vw, vh);
|
||||||
if let Some(expression) = style.letter_spacing_expression.as_deref() {
|
if let Some(expression) = style.letter_spacing_expression.as_deref() {
|
||||||
style.letter_spacing = crate::style::resolve_contextual_length(
|
style.letter_spacing = crate::style::resolve_contextual_length(
|
||||||
expression, em_px, root_fs, vw, vh, em_px,
|
expression, em_px, root_fs, vw, vh, em_px,
|
||||||
@@ -3823,6 +3906,8 @@ fn layout_dom_once(
|
|||||||
let host_is_table_box = style.is_table_box;
|
let host_is_table_box = style.is_table_box;
|
||||||
let host_grid_auto_columns = style.grid_auto_columns.clone();
|
let host_grid_auto_columns = style.grid_auto_columns.clone();
|
||||||
let host_grid_auto_rows = style.grid_auto_rows.clone();
|
let host_grid_auto_rows = style.grid_auto_rows.clone();
|
||||||
|
let host_grid_auto_column_calcs = style.grid_calc_expressions[2].clone();
|
||||||
|
let host_grid_auto_row_calcs = style.grid_calc_expressions[3].clone();
|
||||||
let host_overflow_x = if style.overflow_scroll_x {
|
let host_overflow_x = if style.overflow_scroll_x {
|
||||||
2
|
2
|
||||||
} else if style.overflow_clip_x {
|
} else if style.overflow_clip_x {
|
||||||
@@ -3840,10 +3925,12 @@ fn layout_dom_once(
|
|||||||
let settle_pseudo = |pseudo: &mut crate::LayoutStyle| {
|
let settle_pseudo = |pseudo: &mut crate::LayoutStyle| {
|
||||||
if pseudo.grid_auto_columns_inherit {
|
if pseudo.grid_auto_columns_inherit {
|
||||||
pseudo.grid_auto_columns = host_grid_auto_columns.clone();
|
pseudo.grid_auto_columns = host_grid_auto_columns.clone();
|
||||||
|
pseudo.grid_calc_expressions[2] = host_grid_auto_column_calcs.clone();
|
||||||
pseudo.grid_auto_columns_inherit = false;
|
pseudo.grid_auto_columns_inherit = false;
|
||||||
}
|
}
|
||||||
if pseudo.grid_auto_rows_inherit {
|
if pseudo.grid_auto_rows_inherit {
|
||||||
pseudo.grid_auto_rows = host_grid_auto_rows.clone();
|
pseudo.grid_auto_rows = host_grid_auto_rows.clone();
|
||||||
|
pseudo.grid_calc_expressions[3] = host_grid_auto_row_calcs.clone();
|
||||||
pseudo.grid_auto_rows_inherit = false;
|
pseudo.grid_auto_rows_inherit = false;
|
||||||
}
|
}
|
||||||
if pseudo.display_inherit {
|
if pseudo.display_inherit {
|
||||||
@@ -3886,6 +3973,7 @@ fn layout_dom_once(
|
|||||||
pseudo.font_size = Some(host_font_size);
|
pseudo.font_size = Some(host_font_size);
|
||||||
}
|
}
|
||||||
let pseudo_em = pseudo.font_size.unwrap_or(host_font_size);
|
let pseudo_em = pseudo.font_size.unwrap_or(host_font_size);
|
||||||
|
crate::style::set_grid_calc_context(pseudo, pseudo_em, root_fs, vw, vh);
|
||||||
if let Some(expression) = pseudo.letter_spacing_expression.as_deref() {
|
if let Some(expression) = pseudo.letter_spacing_expression.as_deref() {
|
||||||
pseudo.letter_spacing = crate::style::resolve_contextual_length(
|
pseudo.letter_spacing = crate::style::resolve_contextual_length(
|
||||||
expression, pseudo_em, root_fs, vw, vh, pseudo_em,
|
expression, pseudo_em, root_fs, vw, vh, pseudo_em,
|
||||||
@@ -13244,11 +13332,11 @@ mod tests {
|
|||||||
Incremental,
|
Incremental,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Keyed :has() rules do not poison unrelated mutations, but a matching
|
// Keyed :has() rules do not poison unrelated mutations. A matching
|
||||||
// inserted key still takes the correctness-first full path.
|
// insertion walks upward to the keyed anchor and retains clean peers.
|
||||||
for (name, inserted_class, expectation) in [
|
for (name, inserted_class, expectation) in [
|
||||||
("unrelated keyed has insertion", "other", Incremental),
|
("unrelated keyed has insertion", "other", Incremental),
|
||||||
("matching keyed has insertion", "signal", ConservativeFallback),
|
("matching keyed has insertion", "signal", Incremental),
|
||||||
] {
|
] {
|
||||||
let html = format!(
|
let html = format!(
|
||||||
r#"<style>.host:has(.signal) .dependent{{width:101px}}.clean{{height:19px}}</style>
|
r#"<style>.host:has(.signal) .dependent{{width:101px}}.clean{{height:19px}}</style>
|
||||||
@@ -13283,7 +13371,7 @@ mod tests {
|
|||||||
|
|
||||||
for (name, inserted_tag, expectation) in [
|
for (name, inserted_tag, expectation) in [
|
||||||
("unrelated keyed tag has insertion", "i", Incremental),
|
("unrelated keyed tag has insertion", "i", Incremental),
|
||||||
("matching keyed tag has insertion", "span", ConservativeFallback),
|
("matching keyed tag has insertion", "span", Incremental),
|
||||||
] {
|
] {
|
||||||
let html = format!(
|
let html = format!(
|
||||||
r#"<style>.host:has(> span) .dependent{{width:103px}}.clean{{height:21px}}</style>
|
r#"<style>.host:has(> span) .dependent{{width:103px}}.clean{{height:21px}}</style>
|
||||||
@@ -13348,6 +13436,367 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn retained_relational_styles_match_forced_full_direction_matrix() {
|
||||||
|
use RetainedDifferentialExpectation::{ConservativeFallback, Incremental};
|
||||||
|
|
||||||
|
// Descendant/child traversal and selector-list pseudos all invalidate
|
||||||
|
// the keyed anchor without cascading an unrelated sibling branch.
|
||||||
|
for (name, relative) in [
|
||||||
|
("has descendant insertion", ".signal"),
|
||||||
|
("has child insertion", "> .signal"),
|
||||||
|
("has is insertion", ":is(.signal,.alternate)"),
|
||||||
|
("has where insertion", ":where(.signal,.alternate)"),
|
||||||
|
] {
|
||||||
|
let tree = parse_html(&format!(
|
||||||
|
r#"<style>.host:has({relative}) .dependent{{width:111px;height:7px}}.clean{{height:9px}}</style>
|
||||||
|
<main><section id=host class=host><div id=target><i id=signal class=signal></i></div><span class=dependent></span></section><aside class=clean></aside></main>"#,
|
||||||
|
));
|
||||||
|
let target = tree.get_element_by_id("target").unwrap();
|
||||||
|
let signal = tree.get_element_by_id("signal").unwrap();
|
||||||
|
tree.remove_child(signal);
|
||||||
|
let mut cache = crate::css::StylesheetCache::default();
|
||||||
|
let initial = layout_dom_with_web_fonts_and_stylesheet_cache(
|
||||||
|
&tree,
|
||||||
|
(360.0, 260.0),
|
||||||
|
&HashMap::new(),
|
||||||
|
&[],
|
||||||
|
&mut cache,
|
||||||
|
);
|
||||||
|
tree.append_child(target, signal);
|
||||||
|
finish_retained_tree_case(
|
||||||
|
name,
|
||||||
|
&tree,
|
||||||
|
&mut cache,
|
||||||
|
initial,
|
||||||
|
TreeStyleMutation::Insert {
|
||||||
|
node: signal,
|
||||||
|
old_parent: None,
|
||||||
|
new_parent: target,
|
||||||
|
},
|
||||||
|
Incremental,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A relative sibling combinator searches toward earlier siblings of
|
||||||
|
// the changed boundary. Both exact and general sibling paths retain a
|
||||||
|
// clean branch.
|
||||||
|
for (name, combinator) in [
|
||||||
|
("has adjacent sibling insertion", "+"),
|
||||||
|
("has general sibling insertion", "~"),
|
||||||
|
] {
|
||||||
|
let tree = parse_html(&format!(
|
||||||
|
r#"<style>.host:has({combinator} .signal){{width:113px;height:11px}}.clean{{height:13px}}</style>
|
||||||
|
<main id=parent><section id=host class=host></section><i id=signal class=signal></i><aside id=clean class=clean></aside></main>"#,
|
||||||
|
));
|
||||||
|
let parent = tree.get_element_by_id("parent").unwrap();
|
||||||
|
let signal = tree.get_element_by_id("signal").unwrap();
|
||||||
|
let clean = tree.get_element_by_id("clean").unwrap();
|
||||||
|
tree.remove_child(signal);
|
||||||
|
let mut cache = crate::css::StylesheetCache::default();
|
||||||
|
let initial = layout_dom_with_web_fonts_and_stylesheet_cache(
|
||||||
|
&tree,
|
||||||
|
(360.0, 260.0),
|
||||||
|
&HashMap::new(),
|
||||||
|
&[],
|
||||||
|
&mut cache,
|
||||||
|
);
|
||||||
|
tree.insert_before(clean, signal);
|
||||||
|
finish_retained_tree_case(
|
||||||
|
name,
|
||||||
|
&tree,
|
||||||
|
&mut cache,
|
||||||
|
initial,
|
||||||
|
TreeStyleMutation::Insert {
|
||||||
|
node: signal,
|
||||||
|
old_parent: None,
|
||||||
|
new_parent: parent,
|
||||||
|
},
|
||||||
|
Incremental,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inserting an unrelated node can break `+`, even though the inserted
|
||||||
|
// subtree has none of the selector's indexed keys.
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<style>.host:has(> .left + .right){width:127px}.clean{height:15px}</style>
|
||||||
|
<main><section id=host class=host><i class=left></i><b id=blocker></b><i id=right class=right></i></section><aside class=clean></aside></main>"#,
|
||||||
|
);
|
||||||
|
let host = tree.get_element_by_id("host").unwrap();
|
||||||
|
let blocker = tree.get_element_by_id("blocker").unwrap();
|
||||||
|
let right = tree.get_element_by_id("right").unwrap();
|
||||||
|
tree.remove_child(blocker);
|
||||||
|
let mut cache = crate::css::StylesheetCache::default();
|
||||||
|
let initial = layout_dom_with_web_fonts_and_stylesheet_cache(
|
||||||
|
&tree,
|
||||||
|
(360.0, 260.0),
|
||||||
|
&HashMap::new(),
|
||||||
|
&[],
|
||||||
|
&mut cache,
|
||||||
|
);
|
||||||
|
tree.insert_before(right, blocker);
|
||||||
|
finish_retained_tree_case(
|
||||||
|
"has adjacent insertion side effect",
|
||||||
|
&tree,
|
||||||
|
&mut cache,
|
||||||
|
initial,
|
||||||
|
TreeStyleMutation::Insert {
|
||||||
|
node: blocker,
|
||||||
|
old_parent: None,
|
||||||
|
new_parent: host,
|
||||||
|
},
|
||||||
|
Incremental,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Structural matching changes even when the inserted node does not
|
||||||
|
// carry the keyed relative subject class.
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<style>.host:has(> .signal:first-child){width:131px}.clean{height:17px}</style>
|
||||||
|
<main><section id=host class=host><b id=blocker></b><i id=signal class=signal></i></section><aside class=clean></aside></main>"#,
|
||||||
|
);
|
||||||
|
let host = tree.get_element_by_id("host").unwrap();
|
||||||
|
let blocker = tree.get_element_by_id("blocker").unwrap();
|
||||||
|
let signal = tree.get_element_by_id("signal").unwrap();
|
||||||
|
tree.remove_child(blocker);
|
||||||
|
let mut cache = crate::css::StylesheetCache::default();
|
||||||
|
let initial = layout_dom_with_web_fonts_and_stylesheet_cache(
|
||||||
|
&tree,
|
||||||
|
(360.0, 260.0),
|
||||||
|
&HashMap::new(),
|
||||||
|
&[],
|
||||||
|
&mut cache,
|
||||||
|
);
|
||||||
|
tree.insert_before(signal, blocker);
|
||||||
|
finish_retained_tree_case(
|
||||||
|
"has structural insertion side effect",
|
||||||
|
&tree,
|
||||||
|
&mut cache,
|
||||||
|
initial,
|
||||||
|
TreeStyleMutation::Insert {
|
||||||
|
node: blocker,
|
||||||
|
old_parent: None,
|
||||||
|
new_parent: host,
|
||||||
|
},
|
||||||
|
Incremental,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Character data can toggle :empty without changing any indexed key.
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<style>.host:has(.label:empty){width:133px}.clean{height:18px}</style>
|
||||||
|
<main><section id=host class=host><span id=label class=label>x</span></section><aside class=clean></aside></main>"#,
|
||||||
|
);
|
||||||
|
let label = tree.get_element_by_id("label").unwrap();
|
||||||
|
let text = tree.children(label)[0];
|
||||||
|
let mut cache = crate::css::StylesheetCache::default();
|
||||||
|
let initial = layout_dom_with_web_fonts_and_stylesheet_cache(
|
||||||
|
&tree,
|
||||||
|
(360.0, 260.0),
|
||||||
|
&HashMap::new(),
|
||||||
|
&[],
|
||||||
|
&mut cache,
|
||||||
|
);
|
||||||
|
tree.with_node_mut(text, |node| {
|
||||||
|
if let obscura_dom::tree::NodeData::Text { contents } = &mut node.data {
|
||||||
|
contents.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
finish_retained_tree_case(
|
||||||
|
"has empty text side effect",
|
||||||
|
&tree,
|
||||||
|
&mut cache,
|
||||||
|
initial,
|
||||||
|
TreeStyleMutation::Text {
|
||||||
|
node: text,
|
||||||
|
parent: Some(label),
|
||||||
|
},
|
||||||
|
Incremental,
|
||||||
|
);
|
||||||
|
|
||||||
|
// The old sibling links are gone by the time removal invalidation
|
||||||
|
// runs. Broadening each old ancestor boundary must cover both a keyed
|
||||||
|
// removal and an adjacency match created by removing an interloper.
|
||||||
|
for (name, css, removed_id) in [
|
||||||
|
(
|
||||||
|
"has descendant removal",
|
||||||
|
".host:has(.signal){width:137px}",
|
||||||
|
"signal",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"has adjacent removal side effect",
|
||||||
|
".host:has(> .left + .right){width:139px}",
|
||||||
|
"blocker",
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let tree = parse_html(&format!(
|
||||||
|
r#"<style>{css}.clean{{height:19px}}</style><main><section id=host class=host><i class=left></i><b id=blocker></b><i class=right></i><i id=signal class=signal></i></section><aside class=clean></aside></main>"#,
|
||||||
|
));
|
||||||
|
let host = tree.get_element_by_id("host").unwrap();
|
||||||
|
let removed = tree.get_element_by_id(removed_id).unwrap();
|
||||||
|
let mut cache = crate::css::StylesheetCache::default();
|
||||||
|
let initial = layout_dom_with_web_fonts_and_stylesheet_cache(
|
||||||
|
&tree,
|
||||||
|
(360.0, 260.0),
|
||||||
|
&HashMap::new(),
|
||||||
|
&[],
|
||||||
|
&mut cache,
|
||||||
|
);
|
||||||
|
tree.remove_child(removed);
|
||||||
|
finish_retained_tree_case(
|
||||||
|
name,
|
||||||
|
&tree,
|
||||||
|
&mut cache,
|
||||||
|
initial,
|
||||||
|
TreeStyleMutation::Remove {
|
||||||
|
node: removed,
|
||||||
|
old_parent: host,
|
||||||
|
},
|
||||||
|
Incremental,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detached nodes are mutable before the queued render flush. Removal
|
||||||
|
// invalidation must not depend on selector keys still being present in
|
||||||
|
// the post-mutation subtree.
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<style>.host:has(.signal){width:141px}.clean{height:20px}</style><main><section id=host class=host><i id=signal class=signal></i></section><aside class=clean></aside></main>"#,
|
||||||
|
);
|
||||||
|
let host = tree.get_element_by_id("host").unwrap();
|
||||||
|
let signal = tree.get_element_by_id("signal").unwrap();
|
||||||
|
let mut cache = crate::css::StylesheetCache::default();
|
||||||
|
let initial = layout_dom_with_web_fonts_and_stylesheet_cache(
|
||||||
|
&tree,
|
||||||
|
(360.0, 260.0),
|
||||||
|
&HashMap::new(),
|
||||||
|
&[],
|
||||||
|
&mut cache,
|
||||||
|
);
|
||||||
|
tree.remove_child(signal);
|
||||||
|
tree.with_node_mut(signal, |node| {
|
||||||
|
node.set_attribute("class", "detached-and-renamed".into())
|
||||||
|
});
|
||||||
|
finish_retained_tree_case(
|
||||||
|
"has removal after detached key mutation",
|
||||||
|
&tree,
|
||||||
|
&mut cache,
|
||||||
|
initial,
|
||||||
|
TreeStyleMutation::Remove {
|
||||||
|
node: signal,
|
||||||
|
old_parent: host,
|
||||||
|
},
|
||||||
|
Incremental,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reparenting changes anchors in both old and new ancestor chains.
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<style>.host:has(> .signal){width:149px}.clean{height:21px}</style>
|
||||||
|
<main><section id=old class=host><i id=signal class=signal></i></section><section id=new class=host></section><aside class=clean></aside></main>"#,
|
||||||
|
);
|
||||||
|
let old = tree.get_element_by_id("old").unwrap();
|
||||||
|
let new = tree.get_element_by_id("new").unwrap();
|
||||||
|
let signal = tree.get_element_by_id("signal").unwrap();
|
||||||
|
let mut cache = crate::css::StylesheetCache::default();
|
||||||
|
let initial = layout_dom_with_web_fonts_and_stylesheet_cache(
|
||||||
|
&tree,
|
||||||
|
(360.0, 260.0),
|
||||||
|
&HashMap::new(),
|
||||||
|
&[],
|
||||||
|
&mut cache,
|
||||||
|
);
|
||||||
|
tree.append_child(new, signal);
|
||||||
|
finish_retained_tree_case(
|
||||||
|
"has reparent old and new anchors",
|
||||||
|
&tree,
|
||||||
|
&mut cache,
|
||||||
|
initial,
|
||||||
|
TreeStyleMutation::Insert {
|
||||||
|
node: signal,
|
||||||
|
old_parent: Some(old),
|
||||||
|
new_parent: new,
|
||||||
|
},
|
||||||
|
Incremental,
|
||||||
|
);
|
||||||
|
|
||||||
|
// A sibling-then-descendant path outside the anchor is deliberately
|
||||||
|
// left on the correctness-first full path until dependency chains can
|
||||||
|
// encode the continuation explicitly.
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<style>.host:has(.signal)~.panel .leaf{width:151px}</style><main><section id=host class=host><i id=signal class=signal></i></section><div class=panel><b class=leaf></b></div></main>"#,
|
||||||
|
);
|
||||||
|
let host = tree.get_element_by_id("host").unwrap();
|
||||||
|
let signal = tree.get_element_by_id("signal").unwrap();
|
||||||
|
tree.remove_child(signal);
|
||||||
|
let mut cache = crate::css::StylesheetCache::default();
|
||||||
|
let initial = layout_dom_with_web_fonts_and_stylesheet_cache(
|
||||||
|
&tree,
|
||||||
|
(360.0, 260.0),
|
||||||
|
&HashMap::new(),
|
||||||
|
&[],
|
||||||
|
&mut cache,
|
||||||
|
);
|
||||||
|
tree.append_child(host, signal);
|
||||||
|
finish_retained_tree_case(
|
||||||
|
"has unrepresentable outer mixed traversal",
|
||||||
|
&tree,
|
||||||
|
&mut cache,
|
||||||
|
initial,
|
||||||
|
TreeStyleMutation::Insert {
|
||||||
|
node: signal,
|
||||||
|
old_parent: None,
|
||||||
|
new_parent: host,
|
||||||
|
},
|
||||||
|
ConservativeFallback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn retained_relational_invalidation_keeps_two_thousand_node_peer_clean() {
|
||||||
|
let mut clean = String::new();
|
||||||
|
for index in 0..2_000 {
|
||||||
|
clean.push_str(&format!("<span class=clean data-index={index}></span>"));
|
||||||
|
}
|
||||||
|
let tree = parse_html(&format!(
|
||||||
|
r#"<style>.host:has(> .signal) .dependent{{width:157px}}.clean{{height:1px}}</style>
|
||||||
|
<main><section id=host class=host><i id=signal class=signal></i><b class=dependent></b></section><aside>{clean}</aside></main>"#,
|
||||||
|
));
|
||||||
|
let host = tree.get_element_by_id("host").unwrap();
|
||||||
|
let signal = tree.get_element_by_id("signal").unwrap();
|
||||||
|
tree.remove_child(signal);
|
||||||
|
let mut cache = crate::css::StylesheetCache::default();
|
||||||
|
let mut initial = layout_dom_with_web_fonts_and_stylesheet_cache(
|
||||||
|
&tree,
|
||||||
|
(800.0, 600.0),
|
||||||
|
&HashMap::new(),
|
||||||
|
&[],
|
||||||
|
&mut cache,
|
||||||
|
);
|
||||||
|
tree.append_child(host, signal);
|
||||||
|
let retained = RetainedStyleMaps {
|
||||||
|
styles: std::mem::take(&mut initial.styles),
|
||||||
|
custom_properties: std::mem::take(&mut initial.custom_properties),
|
||||||
|
};
|
||||||
|
let (incremental, telemetry) = layout_dom_with_web_fonts_pass_limit(
|
||||||
|
&tree,
|
||||||
|
(800.0, 600.0),
|
||||||
|
&HashMap::new(),
|
||||||
|
&[],
|
||||||
|
None,
|
||||||
|
Some(&mut cache),
|
||||||
|
Some(retained),
|
||||||
|
&[TreeStyleMutation::Insert {
|
||||||
|
node: signal,
|
||||||
|
old_parent: None,
|
||||||
|
new_parent: host,
|
||||||
|
}
|
||||||
|
.into()],
|
||||||
|
);
|
||||||
|
let full = layout_dom(&tree, (800.0, 600.0));
|
||||||
|
assert_computed_styles_match("relational 2k clean peer", &incremental, &full);
|
||||||
|
assert_eq!(incremental.rects, full.rects);
|
||||||
|
assert_eq!(telemetry.retained_fallback, 0, "{telemetry:?}");
|
||||||
|
assert!(telemetry.retained_reused >= 2_000, "{telemetry:?}");
|
||||||
|
assert!(telemetry.retained_fresh < 24, "{telemetry:?}");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn retained_tree_invalidation_keeps_large_unrelated_branch_clean() {
|
fn retained_tree_invalidation_keeps_large_unrelated_branch_clean() {
|
||||||
let mut clean = String::new();
|
let mut clean = String::new();
|
||||||
@@ -14507,6 +14956,28 @@ mod tests {
|
|||||||
assert_eq!(pseudo.line_height, host.line_height);
|
assert_eq!(pseudo.line_height, host.line_height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generated_pseudo_grid_calc_uses_computed_font_and_viewport_context() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<style>
|
||||||
|
html { font-size:16px }
|
||||||
|
#host { font-size:20px }
|
||||||
|
#host::before {
|
||||||
|
content:""; display:grid;
|
||||||
|
grid-template-columns:calc(1em + 1rem + 10vw);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div id="host"></div>"#,
|
||||||
|
);
|
||||||
|
let laid = layout_dom(&tree, (1000.0, 400.0));
|
||||||
|
let host = tree.get_element_by_id("host").unwrap();
|
||||||
|
let pseudo = laid.styles[&host].before_pseudo.as_ref().unwrap();
|
||||||
|
let calc = &pseudo.grid_calc_expressions[0][0];
|
||||||
|
let handle = std::sync::Arc::as_ptr(calc).cast();
|
||||||
|
|
||||||
|
assert!((crate::style::resolve_grid_calc(handle, 1000.0) - 136.0).abs() < 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn positioned_pseudo_does_not_wrap_shrink_to_fit_host_text() {
|
fn positioned_pseudo_does_not_wrap_shrink_to_fit_host_text() {
|
||||||
let tree = parse_html(
|
let tree = parse_html(
|
||||||
|
|||||||
@@ -2884,6 +2884,66 @@ fn sample_gradient(
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether one shaped inline item can contribute ink to the destination
|
||||||
|
/// surface. `SwashCache::with_pixels` rasterizes a glyph before its callback
|
||||||
|
/// can reject out-of-bounds pixels, so letting a long document reach that
|
||||||
|
/// callback for every offscreen glyph makes viewport screenshots scale with
|
||||||
|
/// the full page height. Keep this test deliberately conservative: line boxes
|
||||||
|
/// are expanded by four ems for unusual font ink bounds, and every authored
|
||||||
|
/// relative-inline offset participates in the envelope. Translation and
|
||||||
|
/// isolated transform layers arrive through `offset`, in the same coordinate
|
||||||
|
/// space as the destination pixmap.
|
||||||
|
fn inline_item_may_intersect_surface(
|
||||||
|
item: &InlineItem,
|
||||||
|
offset: (f32, f32),
|
||||||
|
clip: Option<Rect>,
|
||||||
|
surface_height: u32,
|
||||||
|
raster_scale: f32,
|
||||||
|
) -> bool {
|
||||||
|
if !raster_scale.is_finite() || raster_scale <= 0.0 || surface_height == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let surface_bottom = surface_height as f32 / raster_scale;
|
||||||
|
if let Some(clip) = clip {
|
||||||
|
if clip.y >= surface_bottom || clip.y + clip.height <= 0.0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut line_top = f32::INFINITY;
|
||||||
|
let mut line_bottom = f32::NEG_INFINITY;
|
||||||
|
let mut max_font_size = 0.0f32;
|
||||||
|
for run in item.buffer.layout_runs() {
|
||||||
|
line_top = line_top.min(run.line_top);
|
||||||
|
line_bottom = line_bottom.max(run.line_top + run.line_height);
|
||||||
|
for glyph in run.glyphs {
|
||||||
|
if glyph.font_size.is_finite() {
|
||||||
|
max_font_size = max_font_size.max(glyph.font_size.max(0.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !line_top.is_finite() || !line_bottom.is_finite() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some fonts have ink well outside their ascender/descender metrics. Four
|
||||||
|
// ems on each side is intentionally much larger than normal glyph ink,
|
||||||
|
// while still culling text that is genuinely pages away from the viewport.
|
||||||
|
let ink_guard = max_font_size.mul_add(4.0, 4.0);
|
||||||
|
let mut relative_top = 0.0f32;
|
||||||
|
let mut relative_bottom = 0.0f32;
|
||||||
|
for range in &item.relative_owner_ranges {
|
||||||
|
if range.offset.1.is_finite() {
|
||||||
|
relative_top = relative_top.min(range.offset.1);
|
||||||
|
relative_bottom = relative_bottom.max(range.offset.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let origin_y = item.origin.1 + offset.1;
|
||||||
|
let visual_top = origin_y + line_top + relative_top - ink_guard;
|
||||||
|
let visual_bottom = origin_y + line_bottom + relative_bottom + ink_guard;
|
||||||
|
visual_top < surface_bottom && visual_bottom > 0.0
|
||||||
|
}
|
||||||
|
|
||||||
impl TextEngine {
|
impl TextEngine {
|
||||||
/// Rasterize inline context `idx` into `pixmap`, honoring its finalized
|
/// Rasterize inline context `idx` into `pixmap`, honoring its finalized
|
||||||
/// clip. Tests and generated items whose coordinates do not change between
|
/// clip. Tests and generated items whose coordinates do not change between
|
||||||
@@ -2964,6 +3024,16 @@ impl TextEngine {
|
|||||||
let Some(item) = items.get_mut(idx) else {
|
let Some(item) = items.get_mut(idx) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let clip = clip_override.or(item.clip);
|
||||||
|
if !inline_item_may_intersect_surface(
|
||||||
|
item,
|
||||||
|
offset,
|
||||||
|
clip,
|
||||||
|
pixmap.height(),
|
||||||
|
raster_scale,
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let (ox, oy) = (
|
let (ox, oy) = (
|
||||||
(item.origin.0 + offset.0) * raster_scale,
|
(item.origin.0 + offset.0) * raster_scale,
|
||||||
(item.origin.1 + offset.1) * raster_scale,
|
(item.origin.1 + offset.1) * raster_scale,
|
||||||
@@ -2972,7 +3042,6 @@ impl TextEngine {
|
|||||||
// but the clip is already in screen space (owner-shifted at
|
// but the clip is already in screen space (owner-shifted at
|
||||||
// `resolve_clip_rects`) and must not move with the container, or a
|
// `resolve_clip_rects`) and must not move with the container, or a
|
||||||
// translated slide would drag its viewport's clip along with it.
|
// translated slide would drag its viewport's clip along with it.
|
||||||
let clip = clip_override.or(item.clip);
|
|
||||||
let pw = pixmap.width() as i32;
|
let pw = pixmap.width() as i32;
|
||||||
let ph = pixmap.height() as i32;
|
let ph = pixmap.height() as i32;
|
||||||
let clip_bounds = clip.map(|c| {
|
let clip_bounds = clip.map(|c| {
|
||||||
@@ -3356,6 +3425,103 @@ mod tests {
|
|||||||
const RED: [u8; 4] = [255, 0, 0, 255];
|
const RED: [u8; 4] = [255, 0, 0, 255];
|
||||||
const BLUE: [u8; 4] = [0, 0, 255, 255];
|
const BLUE: [u8; 4] = [0, 0, 255, 255];
|
||||||
|
|
||||||
|
fn surface_cull_fixture() -> (TextEngine, usize) {
|
||||||
|
let tree = obscura_dom::parse_html("<p id='copy'>Visible text</p>");
|
||||||
|
let copy = tree.get_element_by_id("copy").unwrap();
|
||||||
|
let style = LayoutStyle {
|
||||||
|
display: Display::Block,
|
||||||
|
font_size: Some(16.0),
|
||||||
|
line_height: Some(crate::LineHeight::Px(20.0)),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut engine = TextEngine::new();
|
||||||
|
let item = engine
|
||||||
|
.try_build(&tree, copy, &HashMap::from([(copy, style)]))
|
||||||
|
.unwrap();
|
||||||
|
engine.finalize(item, (0.0, 10.0), 200.0, None);
|
||||||
|
(engine, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_surface_cull_is_conservative_for_offsets_and_ink_overhang() {
|
||||||
|
let (mut engine, item) = surface_cull_fixture();
|
||||||
|
assert!(inline_item_may_intersect_surface(
|
||||||
|
&engine.items[item],
|
||||||
|
(0.0, 0.0),
|
||||||
|
None,
|
||||||
|
100,
|
||||||
|
1.0,
|
||||||
|
));
|
||||||
|
|
||||||
|
engine.items[item].origin.1 = -60.0;
|
||||||
|
assert!(
|
||||||
|
inline_item_may_intersect_surface(
|
||||||
|
&engine.items[item],
|
||||||
|
(0.0, 0.0),
|
||||||
|
None,
|
||||||
|
100,
|
||||||
|
1.0,
|
||||||
|
),
|
||||||
|
"the four-em ink guard must preserve unusual glyph overhang"
|
||||||
|
);
|
||||||
|
engine.items[item].origin.1 = -500.0;
|
||||||
|
assert!(!inline_item_may_intersect_surface(
|
||||||
|
&engine.items[item],
|
||||||
|
(0.0, 0.0),
|
||||||
|
None,
|
||||||
|
100,
|
||||||
|
1.0,
|
||||||
|
));
|
||||||
|
engine.items[item].origin.1 = 500.0;
|
||||||
|
assert!(!inline_item_may_intersect_surface(
|
||||||
|
&engine.items[item],
|
||||||
|
(0.0, 0.0),
|
||||||
|
None,
|
||||||
|
100,
|
||||||
|
1.0,
|
||||||
|
));
|
||||||
|
assert!(
|
||||||
|
inline_item_may_intersect_surface(
|
||||||
|
&engine.items[item],
|
||||||
|
(0.0, -490.0),
|
||||||
|
None,
|
||||||
|
100,
|
||||||
|
1.0,
|
||||||
|
),
|
||||||
|
"the transform/layer translation must be applied before culling"
|
||||||
|
);
|
||||||
|
|
||||||
|
engine.items[item]
|
||||||
|
.relative_owner_ranges
|
||||||
|
.push(RelativeOwnerTextRange {
|
||||||
|
start: 0,
|
||||||
|
end: 1,
|
||||||
|
offset: (0.0, -490.0),
|
||||||
|
});
|
||||||
|
assert!(
|
||||||
|
inline_item_may_intersect_surface(
|
||||||
|
&engine.items[item],
|
||||||
|
(0.0, 0.0),
|
||||||
|
None,
|
||||||
|
100,
|
||||||
|
1.0,
|
||||||
|
),
|
||||||
|
"a relatively positioned inline may move visible ink into the surface"
|
||||||
|
);
|
||||||
|
assert!(!inline_item_may_intersect_surface(
|
||||||
|
&engine.items[item],
|
||||||
|
(0.0, 0.0),
|
||||||
|
Some(Rect {
|
||||||
|
x: 0.0,
|
||||||
|
y: 200.0,
|
||||||
|
width: 100.0,
|
||||||
|
height: 20.0,
|
||||||
|
}),
|
||||||
|
100,
|
||||||
|
1.0,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn system_ui_resolves_in_stack_order_to_chromium_linux_face() {
|
fn system_ui_resolves_in_stack_order_to_chromium_linux_face() {
|
||||||
let stack = "system-ui, -apple-system, \"Segoe UI\", Roboto, \
|
let stack = "system-ui, -apple-system, \"Segoe UI\", Roboto, \
|
||||||
|
|||||||
@@ -1007,6 +1007,10 @@ pub struct LayoutStyle {
|
|||||||
// resolved to line placements on children in a later pass.
|
// resolved to line placements on children in a later pass.
|
||||||
pub grid_template_columns: Vec<taffy::GridTemplateComponent<String>>,
|
pub grid_template_columns: Vec<taffy::GridTemplateComponent<String>>,
|
||||||
pub grid_template_rows: Vec<taffy::GridTemplateComponent<String>>,
|
pub grid_template_rows: Vec<taffy::GridTemplateComponent<String>>,
|
||||||
|
/// Keeps the opaque `calc()` handles embedded in grid track sizing
|
||||||
|
/// functions alive until every Taffy layout pass has completed.
|
||||||
|
pub(crate) grid_calc_expressions:
|
||||||
|
[Vec<std::sync::Arc<crate::style::GridCalcExpression>>; 4],
|
||||||
/// Track sizing functions for columns created outside the explicit grid.
|
/// Track sizing functions for columns created outside the explicit grid.
|
||||||
/// An empty list is the CSS initial `auto` value: taffy supplies one
|
/// An empty list is the CSS initial `auto` value: taffy supplies one
|
||||||
/// automatic implicit track and cycles a non-empty authored list.
|
/// automatic implicit track and cycles a non-empty authored list.
|
||||||
@@ -1605,7 +1609,15 @@ pub struct NodeRect {
|
|||||||
/// Lay out `root` within a `viewport` (width, height) in CSS pixels and return
|
/// Lay out `root` within a `viewport` (width, height) in CSS pixels and return
|
||||||
/// the border-box geometry per node, mirroring the input tree.
|
/// the border-box geometry per node, mirroring the input tree.
|
||||||
pub fn layout(root: &LayoutNode, viewport: (f32, f32)) -> NodeRect {
|
pub fn layout(root: &LayoutNode, viewport: (f32, f32)) -> NodeRect {
|
||||||
let mut tree: TaffyTree = TaffyTree::new();
|
let root_font_size = root.style.font_size.unwrap_or(16.0);
|
||||||
|
initialize_grid_calc_contexts(
|
||||||
|
root,
|
||||||
|
root_font_size,
|
||||||
|
root_font_size,
|
||||||
|
viewport.0 / 100.0,
|
||||||
|
viewport.1 / 100.0,
|
||||||
|
);
|
||||||
|
let mut tree: TaffyTree = new_taffy_tree();
|
||||||
let root_id = build_node(&mut tree, root);
|
let root_id = build_node(&mut tree, root);
|
||||||
let _ = tree.compute_layout(
|
let _ = tree.compute_layout(
|
||||||
root_id,
|
root_id,
|
||||||
@@ -1617,6 +1629,28 @@ pub fn layout(root: &LayoutNode, viewport: (f32, f32)) -> NodeRect {
|
|||||||
read_node(&tree, root_id)
|
read_node(&tree, root_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn initialize_grid_calc_contexts(
|
||||||
|
node: &LayoutNode,
|
||||||
|
inherited_font_size: f32,
|
||||||
|
root_font_size: f32,
|
||||||
|
vw: f32,
|
||||||
|
vh: f32,
|
||||||
|
) {
|
||||||
|
let font_size = node.style.font_size.unwrap_or(inherited_font_size);
|
||||||
|
crate::style::set_grid_calc_context(&node.style, font_size, root_font_size, vw, vh);
|
||||||
|
for child in &node.children {
|
||||||
|
initialize_grid_calc_contexts(child, font_size, root_font_size, vw, vh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn new_taffy_tree<NodeContext>() -> TaffyTree<NodeContext> {
|
||||||
|
let mut tree = TaffyTree::new();
|
||||||
|
// Every opaque handle is backed by an Arc retained in the LayoutStyle
|
||||||
|
// map/input tree, which outlives all computations on `tree`.
|
||||||
|
tree.set_calc_resolver(crate::style::resolve_grid_calc);
|
||||||
|
tree
|
||||||
|
}
|
||||||
|
|
||||||
fn build_node(tree: &mut TaffyTree, node: &LayoutNode) -> NodeId {
|
fn build_node(tree: &mut TaffyTree, node: &LayoutNode) -> NodeId {
|
||||||
let style = to_taffy_style(&node.style);
|
let style = to_taffy_style(&node.style);
|
||||||
if node.children.is_empty() {
|
if node.children.is_empty() {
|
||||||
@@ -1960,6 +1994,49 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grid_calc_tracks_resolve_against_container_width() {
|
||||||
|
let grid = crate::style::compute_style(
|
||||||
|
"div",
|
||||||
|
Some(
|
||||||
|
"display:grid;width:1440px;height:20px;\
|
||||||
|
grid-template-columns:\
|
||||||
|
minmax(0,calc((100% - (50rem + 20vw))/2)) 1fr \
|
||||||
|
minmax(0,calc((100% - (50rem + 20vw))/2))",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let child = || {
|
||||||
|
LayoutNode::leaf(crate::style::compute_style(
|
||||||
|
"div",
|
||||||
|
Some("display:block;height:10px"),
|
||||||
|
))
|
||||||
|
};
|
||||||
|
let output = layout(
|
||||||
|
&LayoutNode {
|
||||||
|
style: grid.clone(),
|
||||||
|
text: None,
|
||||||
|
children: vec![child(), child(), child()],
|
||||||
|
},
|
||||||
|
(1440.0, 100.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!((output.children[0].border_box.width - 176.0).abs() < 0.01);
|
||||||
|
assert!((output.children[1].border_box.x - 176.0).abs() < 0.01);
|
||||||
|
assert!((output.children[1].border_box.width - 1088.0).abs() < 0.01);
|
||||||
|
|
||||||
|
let resized = layout(
|
||||||
|
&LayoutNode {
|
||||||
|
style: grid,
|
||||||
|
text: None,
|
||||||
|
children: vec![child(), child(), child()],
|
||||||
|
},
|
||||||
|
(1000.0, 100.0),
|
||||||
|
);
|
||||||
|
assert!((resized.children[0].border_box.width - 220.0).abs() < 0.01);
|
||||||
|
assert!((resized.children[1].border_box.x - 220.0).abs() < 0.01);
|
||||||
|
assert!((resized.children[1].border_box.width - 1000.0).abs() < 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn block_auto_margins_absorb_horizontal_free_space() {
|
fn block_auto_margins_absorb_horizontal_free_space() {
|
||||||
let centered = LayoutStyle {
|
let centered = LayoutStyle {
|
||||||
|
|||||||
@@ -1687,14 +1687,16 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
"grid-template-columns" => {
|
"grid-template-columns" => {
|
||||||
let (tracks, names) = parse_track_list_named(value);
|
let (tracks, names, calc_expressions) = parse_track_list_named(value);
|
||||||
style.grid_template_columns_subgrid = is_subgrid_track_list(value);
|
style.grid_template_columns_subgrid = is_subgrid_track_list(value);
|
||||||
style.grid_template_columns = tracks;
|
style.grid_template_columns = tracks;
|
||||||
|
style.grid_calc_expressions[0] = calc_expressions;
|
||||||
style.grid_col_line_names = (!names.is_empty()).then(|| build_line_map(names));
|
style.grid_col_line_names = (!names.is_empty()).then(|| build_line_map(names));
|
||||||
}
|
}
|
||||||
"grid-template-rows" => {
|
"grid-template-rows" => {
|
||||||
let (tracks, names) = parse_track_list_named(value);
|
let (tracks, names, calc_expressions) = parse_track_list_named(value);
|
||||||
style.grid_template_rows = tracks;
|
style.grid_template_rows = tracks;
|
||||||
|
style.grid_calc_expressions[1] = calc_expressions;
|
||||||
style.grid_row_line_names = (!names.is_empty()).then(|| build_line_map(names));
|
style.grid_row_line_names = (!names.is_empty()).then(|| build_line_map(names));
|
||||||
}
|
}
|
||||||
"grid-auto-columns" => apply_grid_auto_tracks(style, value, true),
|
"grid-auto-columns" => apply_grid_auto_tracks(style, value, true),
|
||||||
@@ -2553,7 +2555,7 @@ fn supports_conservative_known_value(name: &str, value: &str) -> bool {
|
|||||||
.all(|token| matches!(*token, "row" | "column" | "dense"))
|
.all(|token| matches!(*token, "row" | "column" | "dense"))
|
||||||
}
|
}
|
||||||
"grid-template-columns" | "grid-template-rows" => {
|
"grid-template-columns" | "grid-template-rows" => {
|
||||||
let (tracks, _) = parse_track_list_named(value);
|
let (tracks, _, _) = parse_track_list_named(value);
|
||||||
!tracks.is_empty() || lower.starts_with("subgrid")
|
!tracks.is_empty() || lower.starts_with("subgrid")
|
||||||
}
|
}
|
||||||
"grid-template-areas" => !parse_grid_areas(value).is_empty() || lower == "none",
|
"grid-template-areas" => !parse_grid_areas(value).is_empty() || lower == "none",
|
||||||
@@ -3499,6 +3501,141 @@ fn scale_number(s: &str) -> Option<f32> {
|
|||||||
/// Parse a CSS grid track list (`min-content 1fr min-content`, `12.25rem
|
/// Parse a CSS grid track list (`min-content 1fr min-content`, `12.25rem
|
||||||
/// minmax(0,1fr)`) into taffy sizing functions. Tokenizes respecting the
|
/// minmax(0,1fr)`) into taffy sizing functions. Tokenizes respecting the
|
||||||
/// parentheses in `minmax(...)` / `fit-content(...)`.
|
/// parentheses in `minmax(...)` / `fit-content(...)`.
|
||||||
|
#[repr(align(8))]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct GridCalcExpression {
|
||||||
|
expression: String,
|
||||||
|
em_px: std::sync::atomic::AtomicU32,
|
||||||
|
rem_px: std::sync::atomic::AtomicU32,
|
||||||
|
vw: std::sync::atomic::AtomicU32,
|
||||||
|
vh: std::sync::atomic::AtomicU32,
|
||||||
|
context_initialized: std::sync::atomic::AtomicBool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GridCalcExpression {
|
||||||
|
fn parse(value: &str) -> Option<Self> {
|
||||||
|
let value = value.trim().to_ascii_lowercase();
|
||||||
|
if !(value.starts_with("calc(")
|
||||||
|
|| value.starts_with("min(")
|
||||||
|
|| value.starts_with("max(")
|
||||||
|
|| value.starts_with("clamp(")
|
||||||
|
|| value.starts_with("round("))
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track math is resolved late against the actual grid-axis basis.
|
||||||
|
// Accept only supported length units, numbers, and math function
|
||||||
|
// names. The computed font and viewport contexts are attached by the
|
||||||
|
// top-down style pass before Taffy resolves the percentage basis.
|
||||||
|
for word in value
|
||||||
|
.split(|character: char| !character.is_ascii_alphabetic() && character != '-')
|
||||||
|
.filter(|word| !word.is_empty() && *word != "-")
|
||||||
|
{
|
||||||
|
if !matches!(
|
||||||
|
word,
|
||||||
|
"calc"
|
||||||
|
| "min"
|
||||||
|
| "max"
|
||||||
|
| "clamp"
|
||||||
|
| "round"
|
||||||
|
| "nearest"
|
||||||
|
| "up"
|
||||||
|
| "down"
|
||||||
|
| "to-zero"
|
||||||
|
| "px"
|
||||||
|
| "pt"
|
||||||
|
| "em"
|
||||||
|
| "rem"
|
||||||
|
| "ex"
|
||||||
|
| "vw"
|
||||||
|
| "vh"
|
||||||
|
| "dvw"
|
||||||
|
| "dvh"
|
||||||
|
| "svw"
|
||||||
|
| "svh"
|
||||||
|
| "lvw"
|
||||||
|
| "lvh"
|
||||||
|
| "vmin"
|
||||||
|
| "vmax"
|
||||||
|
) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Probe more than one basis so malformed expressions and non-finite
|
||||||
|
// arithmetic never become an opaque Taffy handle.
|
||||||
|
for basis in [0.0, 100.0] {
|
||||||
|
let resolved = resolve_contextual_length(&value, 16.0, 16.0, 10.0, 10.0, basis)?;
|
||||||
|
if !resolved.is_finite() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Self {
|
||||||
|
expression: value,
|
||||||
|
em_px: std::sync::atomic::AtomicU32::new(16.0f32.to_bits()),
|
||||||
|
rem_px: std::sync::atomic::AtomicU32::new(16.0f32.to_bits()),
|
||||||
|
vw: std::sync::atomic::AtomicU32::new(0.0f32.to_bits()),
|
||||||
|
vh: std::sync::atomic::AtomicU32::new(0.0f32.to_bits()),
|
||||||
|
context_initialized: std::sync::atomic::AtomicBool::new(false),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_context(&self, em_px: f32, rem_px: f32, vw: f32, vh: f32) {
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
// Viewport units are used values and must follow every new layout
|
||||||
|
// viewport, including repeated calls through the public LayoutNode API.
|
||||||
|
self.vw.store(vw.to_bits(), Ordering::Relaxed);
|
||||||
|
self.vh.store(vh.to_bits(), Ordering::Relaxed);
|
||||||
|
// A cloned handle represents the same computed track value. In
|
||||||
|
// particular, explicit `inherit` must preserve the parent's em basis
|
||||||
|
// even when the child has a different font-size.
|
||||||
|
if self.context_initialized.load(Ordering::Acquire) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.em_px.store(em_px.to_bits(), Ordering::Relaxed);
|
||||||
|
self.rem_px.store(rem_px.to_bits(), Ordering::Relaxed);
|
||||||
|
self.context_initialized.store(true, Ordering::Release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_grid_calc_context(
|
||||||
|
style: &LayoutStyle,
|
||||||
|
em_px: f32,
|
||||||
|
rem_px: f32,
|
||||||
|
vw: f32,
|
||||||
|
vh: f32,
|
||||||
|
) {
|
||||||
|
for expression in style.grid_calc_expressions.iter().flatten() {
|
||||||
|
expression.set_context(em_px, rem_px, vw, vh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve an Arc-backed grid calc handle installed in Taffy.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// `value` must point to a live `GridCalcExpression`. `LayoutStyle` retains
|
||||||
|
/// the Arc owners for the entire lifetime of every layout tree computation.
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
pub(crate) fn resolve_grid_calc(value: *const (), basis: f32) -> f32 {
|
||||||
|
// SAFETY: upheld by `new_taffy_tree` and the Arc ownership documented on
|
||||||
|
// `LayoutStyle::grid_calc_expressions`.
|
||||||
|
let calc = unsafe { &*value.cast::<GridCalcExpression>() };
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
resolve_contextual_length(
|
||||||
|
&calc.expression,
|
||||||
|
f32::from_bits(calc.em_px.load(Ordering::Relaxed)),
|
||||||
|
f32::from_bits(calc.rem_px.load(Ordering::Relaxed)),
|
||||||
|
f32::from_bits(calc.vw.load(Ordering::Relaxed)),
|
||||||
|
f32::from_bits(calc.vh.load(Ordering::Relaxed)),
|
||||||
|
basis,
|
||||||
|
)
|
||||||
|
.filter(|value| value.is_finite())
|
||||||
|
.unwrap_or(0.0)
|
||||||
|
.max(0.0)
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse a track list into taffy sizing functions plus the `[line-name]` map
|
/// Parse a track list into taffy sizing functions plus the `[line-name]` map
|
||||||
/// (name -> 1-based grid line number). `repeat(n, ...)` is expanded to n copies;
|
/// (name -> 1-based grid line number). `repeat(n, ...)` is expanded to n copies;
|
||||||
/// auto-fill/auto-fit remain typed repetitions so layout can derive their count
|
/// auto-fill/auto-fit remain typed repetitions so layout can derive their count
|
||||||
@@ -3509,10 +3646,12 @@ pub(crate) fn parse_track_list_named(
|
|||||||
) -> (
|
) -> (
|
||||||
Vec<taffy::GridTemplateComponent<String>>,
|
Vec<taffy::GridTemplateComponent<String>>,
|
||||||
Vec<(String, i16)>,
|
Vec<(String, i16)>,
|
||||||
|
Vec<std::sync::Arc<GridCalcExpression>>,
|
||||||
) {
|
) {
|
||||||
let tokens = tokenize_tracks(value);
|
let tokens = tokenize_tracks(value);
|
||||||
let mut tracks = Vec::new();
|
let mut tracks = Vec::new();
|
||||||
let mut names = Vec::new();
|
let mut names = Vec::new();
|
||||||
|
let mut calc_expressions = Vec::new();
|
||||||
let mut line: i16 = 1;
|
let mut line: i16 = 1;
|
||||||
// A subgridded axis owns line names but no sizing functions. Do not turn
|
// A subgridded axis owns line names but no sizing functions. Do not turn
|
||||||
// the keyword into the generic unknown-token => auto-track fallback.
|
// the keyword into the generic unknown-token => auto-track fallback.
|
||||||
@@ -3521,15 +3660,22 @@ pub(crate) fn parse_track_list_named(
|
|||||||
.map(|token| token.eq_ignore_ascii_case("subgrid"))
|
.map(|token| token.eq_ignore_ascii_case("subgrid"))
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
for tok in tokens.into_iter().skip(usize::from(is_subgrid)) {
|
for tok in tokens.into_iter().skip(usize::from(is_subgrid)) {
|
||||||
expand_track_token(&tok, &mut tracks, &mut names, &mut line);
|
expand_track_token(
|
||||||
|
&tok,
|
||||||
|
&mut tracks,
|
||||||
|
&mut names,
|
||||||
|
&mut calc_expressions,
|
||||||
|
&mut line,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
(tracks, names)
|
(tracks, names, calc_expressions)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expand_track_token(
|
fn expand_track_token(
|
||||||
tok: &str,
|
tok: &str,
|
||||||
tracks: &mut Vec<taffy::GridTemplateComponent<String>>,
|
tracks: &mut Vec<taffy::GridTemplateComponent<String>>,
|
||||||
names: &mut Vec<(String, i16)>,
|
names: &mut Vec<(String, i16)>,
|
||||||
|
calc_expressions: &mut Vec<std::sync::Arc<GridCalcExpression>>,
|
||||||
line: &mut i16,
|
line: &mut i16,
|
||||||
) {
|
) {
|
||||||
let t = tok.trim();
|
let t = tok.trim();
|
||||||
@@ -3554,7 +3700,7 @@ fn expand_track_token(
|
|||||||
let repeated_tracks = subtoks
|
let repeated_tracks = subtoks
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|st| !st.trim_start().starts_with('['))
|
.filter(|st| !st.trim_start().starts_with('['))
|
||||||
.map(|st| track(st))
|
.map(|st| track(st, calc_expressions))
|
||||||
.collect();
|
.collect();
|
||||||
tracks.push(taffy::GridTemplateComponent::Repeat(
|
tracks.push(taffy::GridTemplateComponent::Repeat(
|
||||||
taffy::GridTemplateRepetition {
|
taffy::GridTemplateRepetition {
|
||||||
@@ -3569,13 +3715,16 @@ fn expand_track_token(
|
|||||||
let count = cnt.trim().parse::<usize>().unwrap_or(1).min(1000);
|
let count = cnt.trim().parse::<usize>().unwrap_or(1).min(1000);
|
||||||
for _ in 0..count {
|
for _ in 0..count {
|
||||||
for st in &subtoks {
|
for st in &subtoks {
|
||||||
expand_track_token(st, tracks, names, line);
|
expand_track_token(st, tracks, names, calc_expressions, line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
tracks.push(taffy::GridTemplateComponent::Single(track(t)));
|
tracks.push(taffy::GridTemplateComponent::Single(track(
|
||||||
|
t,
|
||||||
|
calc_expressions,
|
||||||
|
)));
|
||||||
*line += 1;
|
*line += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3626,7 +3775,10 @@ fn tokenize_tracks(value: &str) -> Vec<String> {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
fn track(tok: &str) -> taffy::TrackSizingFunction {
|
fn track(
|
||||||
|
tok: &str,
|
||||||
|
calc_expressions: &mut Vec<std::sync::Arc<GridCalcExpression>>,
|
||||||
|
) -> taffy::TrackSizingFunction {
|
||||||
use taffy::MinMax;
|
use taffy::MinMax;
|
||||||
let t = tok.trim();
|
let t = tok.trim();
|
||||||
let lower = t.to_ascii_lowercase();
|
let lower = t.to_ascii_lowercase();
|
||||||
@@ -3634,10 +3786,11 @@ fn track(tok: &str) -> taffy::TrackSizingFunction {
|
|||||||
.strip_prefix("minmax(")
|
.strip_prefix("minmax(")
|
||||||
.and_then(|s| s.strip_suffix(')'))
|
.and_then(|s| s.strip_suffix(')'))
|
||||||
{
|
{
|
||||||
if let Some((a, b)) = inner.split_once(',') {
|
let arguments = split_top_level(inner, ',');
|
||||||
|
if let [a, b] = arguments.as_slice() {
|
||||||
return MinMax {
|
return MinMax {
|
||||||
min: min_track(a.trim()),
|
min: min_track(a.trim(), calc_expressions),
|
||||||
max: max_track(b.trim()),
|
max: max_track(b.trim(), calc_expressions),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3645,15 +3798,28 @@ fn track(tok: &str) -> taffy::TrackSizingFunction {
|
|||||||
.strip_prefix("fit-content(")
|
.strip_prefix("fit-content(")
|
||||||
.and_then(|s| s.strip_suffix(')'))
|
.and_then(|s| s.strip_suffix(')'))
|
||||||
{
|
{
|
||||||
let px = px_value(inner.trim()).unwrap_or(0.0);
|
let limit = inner.trim();
|
||||||
|
let max = if let Some(percent) = limit
|
||||||
|
.strip_suffix('%')
|
||||||
|
.and_then(|number| number.trim().parse::<f32>().ok())
|
||||||
|
{
|
||||||
|
taffy::MaxTrackSizingFunction::fit_content_percent(percent / 100.0)
|
||||||
|
} else if let Some(px) = px_value(limit) {
|
||||||
|
taffy::MaxTrackSizingFunction::fit_content_px(px)
|
||||||
|
} else {
|
||||||
|
// Taffy has no opaque-calc form for fit-content's distinct clamp
|
||||||
|
// semantics. Preserve a non-collapsing intrinsic fallback rather
|
||||||
|
// than turning a valid expression into fit-content(0px).
|
||||||
|
taffy::MaxTrackSizingFunction::auto()
|
||||||
|
};
|
||||||
return MinMax {
|
return MinMax {
|
||||||
min: taffy::MinTrackSizingFunction::auto(),
|
min: taffy::MinTrackSizingFunction::auto(),
|
||||||
max: taffy::MaxTrackSizingFunction::fit_content_px(px),
|
max,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
MinMax {
|
MinMax {
|
||||||
min: min_track(t),
|
min: min_track(t, calc_expressions),
|
||||||
max: max_track(t),
|
max: max_track(t, calc_expressions),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3661,7 +3827,12 @@ fn track(tok: &str) -> taffy::TrackSizingFunction {
|
|||||||
/// `grid-auto-rows`. Unlike template track lists, implicit track lists do not
|
/// `grid-auto-rows`. Unlike template track lists, implicit track lists do not
|
||||||
/// accept line names or `repeat()`: the authored list itself is repeated as
|
/// accept line names or `repeat()`: the authored list itself is repeated as
|
||||||
/// needed for successive implicit tracks.
|
/// needed for successive implicit tracks.
|
||||||
fn parse_grid_auto_track_list(value: &str) -> Option<Vec<taffy::TrackSizingFunction>> {
|
fn parse_grid_auto_track_list(
|
||||||
|
value: &str,
|
||||||
|
) -> Option<(
|
||||||
|
Vec<taffy::TrackSizingFunction>,
|
||||||
|
Vec<std::sync::Arc<GridCalcExpression>>,
|
||||||
|
)> {
|
||||||
let tokens = tokenize_tracks(value);
|
let tokens = tokenize_tracks(value);
|
||||||
if tokens.is_empty()
|
if tokens.is_empty()
|
||||||
|| tokens.iter().any(|token| {
|
|| tokens.iter().any(|token| {
|
||||||
@@ -3678,7 +3849,12 @@ fn parse_grid_auto_track_list(value: &str) -> Option<Vec<taffy::TrackSizingFunct
|
|||||||
{
|
{
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some(tokens.iter().map(|token| track(token)).collect())
|
let mut calc_expressions = Vec::new();
|
||||||
|
let tracks = tokens
|
||||||
|
.iter()
|
||||||
|
.map(|token| track(token, &mut calc_expressions))
|
||||||
|
.collect();
|
||||||
|
Some((tracks, calc_expressions))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn valid_grid_auto_track(value: &str) -> bool {
|
fn valid_grid_auto_track(value: &str) -> bool {
|
||||||
@@ -3716,6 +3892,9 @@ fn valid_grid_track_breadth(value: &str, flex_allowed: bool) -> bool {
|
|||||||
|
|
||||||
fn valid_grid_track_length_percentage(value: &str) -> bool {
|
fn valid_grid_track_length_percentage(value: &str) -> bool {
|
||||||
let lower = value.to_ascii_lowercase();
|
let lower = value.to_ascii_lowercase();
|
||||||
|
if GridCalcExpression::parse(&lower).is_some() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if lower == "0" {
|
if lower == "0" {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -3735,30 +3914,37 @@ fn valid_grid_track_length_percentage(value: &str) -> bool {
|
|||||||
|
|
||||||
fn apply_grid_auto_tracks(style: &mut LayoutStyle, value: &str, columns: bool) {
|
fn apply_grid_auto_tracks(style: &mut LayoutStyle, value: &str, columns: bool) {
|
||||||
let lower = value.trim().to_ascii_lowercase();
|
let lower = value.trim().to_ascii_lowercase();
|
||||||
let (tracks, inherit) = match lower.as_str() {
|
let (tracks, inherit, calc_expressions) = match lower.as_str() {
|
||||||
"inherit" => (Vec::new(), true),
|
"inherit" => (Vec::new(), true, Vec::new()),
|
||||||
// These properties are non-inherited. The compact cascade has no
|
// These properties are non-inherited. The compact cascade has no
|
||||||
// retained lower-origin/layer declaration for revert, so it follows
|
// retained lower-origin/layer declaration for revert, so it follows
|
||||||
// the existing non-inherited-property policy and uses the initial
|
// the existing non-inherited-property policy and uses the initial
|
||||||
// automatic implicit track.
|
// automatic implicit track.
|
||||||
"initial" | "unset" | "revert" | "revert-layer" => (Vec::new(), false),
|
"initial" | "unset" | "revert" | "revert-layer" => {
|
||||||
|
(Vec::new(), false, Vec::new())
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let Some(tracks) = parse_grid_auto_track_list(value) else {
|
let Some((tracks, calc_expressions)) = parse_grid_auto_track_list(value) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
(tracks, false)
|
(tracks, false, calc_expressions)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if columns {
|
if columns {
|
||||||
style.grid_auto_columns = tracks;
|
style.grid_auto_columns = tracks;
|
||||||
|
style.grid_calc_expressions[2] = calc_expressions;
|
||||||
style.grid_auto_columns_inherit = inherit;
|
style.grid_auto_columns_inherit = inherit;
|
||||||
} else {
|
} else {
|
||||||
style.grid_auto_rows = tracks;
|
style.grid_auto_rows = tracks;
|
||||||
|
style.grid_calc_expressions[3] = calc_expressions;
|
||||||
style.grid_auto_rows_inherit = inherit;
|
style.grid_auto_rows_inherit = inherit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn min_track(tok: &str) -> taffy::MinTrackSizingFunction {
|
fn min_track(
|
||||||
|
tok: &str,
|
||||||
|
calc_expressions: &mut Vec<std::sync::Arc<GridCalcExpression>>,
|
||||||
|
) -> taffy::MinTrackSizingFunction {
|
||||||
use taffy::MinTrackSizingFunction as M;
|
use taffy::MinTrackSizingFunction as M;
|
||||||
let lower = tok.to_ascii_lowercase();
|
let lower = tok.to_ascii_lowercase();
|
||||||
match lower.as_str() {
|
match lower.as_str() {
|
||||||
@@ -3776,6 +3962,11 @@ fn min_track(tok: &str) -> taffy::MinTrackSizingFunction {
|
|||||||
M::percent(p / 100.0)
|
M::percent(p / 100.0)
|
||||||
} else if let Some(px) = px_value(other) {
|
} else if let Some(px) = px_value(other) {
|
||||||
M::length(px)
|
M::length(px)
|
||||||
|
} else if let Some(calc) = GridCalcExpression::parse(other) {
|
||||||
|
let calc = std::sync::Arc::new(calc);
|
||||||
|
let handle = std::sync::Arc::as_ptr(&calc).cast();
|
||||||
|
calc_expressions.push(calc);
|
||||||
|
M::calc(handle)
|
||||||
} else {
|
} else {
|
||||||
M::auto()
|
M::auto()
|
||||||
}
|
}
|
||||||
@@ -3783,7 +3974,10 @@ fn min_track(tok: &str) -> taffy::MinTrackSizingFunction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn max_track(tok: &str) -> taffy::MaxTrackSizingFunction {
|
fn max_track(
|
||||||
|
tok: &str,
|
||||||
|
calc_expressions: &mut Vec<std::sync::Arc<GridCalcExpression>>,
|
||||||
|
) -> taffy::MaxTrackSizingFunction {
|
||||||
use taffy::MaxTrackSizingFunction as M;
|
use taffy::MaxTrackSizingFunction as M;
|
||||||
let lower = tok.to_ascii_lowercase();
|
let lower = tok.to_ascii_lowercase();
|
||||||
match lower.as_str() {
|
match lower.as_str() {
|
||||||
@@ -3803,6 +3997,11 @@ fn max_track(tok: &str) -> taffy::MaxTrackSizingFunction {
|
|||||||
M::percent(p / 100.0)
|
M::percent(p / 100.0)
|
||||||
} else if let Some(px) = px_value(other) {
|
} else if let Some(px) = px_value(other) {
|
||||||
M::length(px)
|
M::length(px)
|
||||||
|
} else if let Some(calc) = GridCalcExpression::parse(other) {
|
||||||
|
let calc = std::sync::Arc::new(calc);
|
||||||
|
let handle = std::sync::Arc::as_ptr(&calc).cast();
|
||||||
|
calc_expressions.push(calc);
|
||||||
|
M::calc(handle)
|
||||||
} else {
|
} else {
|
||||||
M::auto()
|
M::auto()
|
||||||
}
|
}
|
||||||
@@ -3849,14 +4048,16 @@ fn parse_grid_template(style: &mut LayoutStyle, value: &str) {
|
|||||||
// Rows side carries area strings interleaved with row track sizes.
|
// Rows side carries area strings interleaved with row track sizes.
|
||||||
style.grid_areas = Some(parse_grid_areas(rows_part));
|
style.grid_areas = Some(parse_grid_areas(rows_part));
|
||||||
} else if !rows_part.is_empty() {
|
} else if !rows_part.is_empty() {
|
||||||
let (tracks, names) = parse_track_list_named(rows_part);
|
let (tracks, names, calc_expressions) = parse_track_list_named(rows_part);
|
||||||
style.grid_template_rows = tracks;
|
style.grid_template_rows = tracks;
|
||||||
|
style.grid_calc_expressions[1] = calc_expressions;
|
||||||
style.grid_row_line_names = (!names.is_empty()).then(|| build_line_map(names));
|
style.grid_row_line_names = (!names.is_empty()).then(|| build_line_map(names));
|
||||||
}
|
}
|
||||||
if let Some(cols) = cols_part {
|
if let Some(cols) = cols_part {
|
||||||
let (tracks, names) = parse_track_list_named(cols);
|
let (tracks, names, calc_expressions) = parse_track_list_named(cols);
|
||||||
style.grid_template_columns_subgrid = is_subgrid_track_list(cols);
|
style.grid_template_columns_subgrid = is_subgrid_track_list(cols);
|
||||||
style.grid_template_columns = tracks;
|
style.grid_template_columns = tracks;
|
||||||
|
style.grid_calc_expressions[0] = calc_expressions;
|
||||||
style.grid_col_line_names = (!names.is_empty()).then(|| build_line_map(names));
|
style.grid_col_line_names = (!names.is_empty()).then(|| build_line_map(names));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3873,9 +4074,11 @@ fn parse_grid_shorthand(style: &mut LayoutStyle, value: &str) {
|
|||||||
let columns = columns.trim();
|
let columns = columns.trim();
|
||||||
if rows.to_ascii_lowercase().contains("auto-flow") {
|
if rows.to_ascii_lowercase().contains("auto-flow") {
|
||||||
style.grid_template_rows.clear();
|
style.grid_template_rows.clear();
|
||||||
let (tracks, names) = parse_track_list_named(columns);
|
style.grid_calc_expressions[1].clear();
|
||||||
|
let (tracks, names, calc_expressions) = parse_track_list_named(columns);
|
||||||
style.grid_template_columns_subgrid = is_subgrid_track_list(columns);
|
style.grid_template_columns_subgrid = is_subgrid_track_list(columns);
|
||||||
style.grid_template_columns = tracks;
|
style.grid_template_columns = tracks;
|
||||||
|
style.grid_calc_expressions[0] = calc_expressions;
|
||||||
style.grid_col_line_names = (!names.is_empty()).then(|| build_line_map(names));
|
style.grid_col_line_names = (!names.is_empty()).then(|| build_line_map(names));
|
||||||
style.grid_auto_flow = Some(if rows.to_ascii_lowercase().contains("dense") {
|
style.grid_auto_flow = Some(if rows.to_ascii_lowercase().contains("dense") {
|
||||||
taffy::GridAutoFlow::RowDense
|
taffy::GridAutoFlow::RowDense
|
||||||
@@ -3884,9 +4087,11 @@ fn parse_grid_shorthand(style: &mut LayoutStyle, value: &str) {
|
|||||||
});
|
});
|
||||||
} else if columns.to_ascii_lowercase().contains("auto-flow") {
|
} else if columns.to_ascii_lowercase().contains("auto-flow") {
|
||||||
style.grid_template_columns.clear();
|
style.grid_template_columns.clear();
|
||||||
|
style.grid_calc_expressions[0].clear();
|
||||||
style.grid_template_columns_subgrid = false;
|
style.grid_template_columns_subgrid = false;
|
||||||
let (tracks, names) = parse_track_list_named(rows);
|
let (tracks, names, calc_expressions) = parse_track_list_named(rows);
|
||||||
style.grid_template_rows = tracks;
|
style.grid_template_rows = tracks;
|
||||||
|
style.grid_calc_expressions[1] = calc_expressions;
|
||||||
style.grid_row_line_names = (!names.is_empty()).then(|| build_line_map(names));
|
style.grid_row_line_names = (!names.is_empty()).then(|| build_line_map(names));
|
||||||
style.grid_auto_flow = Some(if columns.to_ascii_lowercase().contains("dense") {
|
style.grid_auto_flow = Some(if columns.to_ascii_lowercase().contains("dense") {
|
||||||
taffy::GridAutoFlow::ColumnDense
|
taffy::GridAutoFlow::ColumnDense
|
||||||
@@ -8512,6 +8717,56 @@ mod tests {
|
|||||||
assert_eq!(shorthand.grid_template_columns.len(), 3);
|
assert_eq!(shorthand.grid_template_columns.len(), 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grid_track_math_is_deferred_to_the_used_axis_basis() {
|
||||||
|
let style = compute_style(
|
||||||
|
"div",
|
||||||
|
Some(
|
||||||
|
"grid-template-columns:\
|
||||||
|
minmax(0,calc((100% - 84rem)/2)) 1fr \
|
||||||
|
minmax(0,max(10px,min(calc(25% - 5px),clamp(20px,10%,200px))))",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let owners = &style.grid_calc_expressions[0];
|
||||||
|
assert_eq!(owners.len(), 2);
|
||||||
|
|
||||||
|
set_grid_calc_context(&style, 16.0, 16.0, 14.4, 10.0);
|
||||||
|
let first = std::sync::Arc::as_ptr(&owners[0]).cast();
|
||||||
|
assert!((resolve_grid_calc(first, 1440.0) - 48.0).abs() < 0.01);
|
||||||
|
let nested = std::sync::Arc::as_ptr(&owners[1]).cast();
|
||||||
|
assert!((resolve_grid_calc(nested, 1000.0) - 100.0).abs() < 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn winning_grid_track_declaration_releases_overridden_calc_owners() {
|
||||||
|
let style = compute_style(
|
||||||
|
"div",
|
||||||
|
Some(
|
||||||
|
"grid-template-columns:calc(50% - 10px);\
|
||||||
|
grid-template-rows:calc(25% - 5px);\
|
||||||
|
grid-template-columns:1fr",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert!(style.grid_calc_expressions[0].is_empty());
|
||||||
|
// A non-minmax breadth supplies both the minimum and maximum sizing
|
||||||
|
// function, each with its own stable opaque handle.
|
||||||
|
assert_eq!(style.grid_calc_expressions[1].len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inherited_grid_calc_preserves_parent_em_context() {
|
||||||
|
let parent = compute_style("div", Some("grid-auto-columns:calc(1em + 10%)"));
|
||||||
|
set_grid_calc_context(&parent, 20.0, 16.0, 10.0, 10.0);
|
||||||
|
|
||||||
|
let mut child = compute_style("div", Some("grid-auto-columns:inherit"));
|
||||||
|
child.grid_auto_columns = parent.grid_auto_columns.clone();
|
||||||
|
child.grid_calc_expressions[2] = parent.grid_calc_expressions[2].clone();
|
||||||
|
set_grid_calc_context(&child, 40.0, 16.0, 10.0, 10.0);
|
||||||
|
|
||||||
|
let handle = std::sync::Arc::as_ptr(&child.grid_calc_expressions[2][0]).cast();
|
||||||
|
assert!((resolve_grid_calc(handle, 100.0) - 30.0).abs() < 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn implicit_grid_track_lists_parse_and_css_wide_values_reset_them() {
|
fn implicit_grid_track_lists_parse_and_css_wide_values_reset_them() {
|
||||||
let tracks = compute_style(
|
let tracks = compute_style(
|
||||||
|
|||||||
Vendored
+25
-3
@@ -80,11 +80,23 @@ impl std::error::Error for TaffyError {}
|
|||||||
pub(crate) struct TaffyConfig {
|
pub(crate) struct TaffyConfig {
|
||||||
/// Whether to round layout values
|
/// Whether to round layout values
|
||||||
pub(crate) use_rounding: bool,
|
pub(crate) use_rounding: bool,
|
||||||
|
/// Resolves an opaque `calc()` handle against its percentage basis.
|
||||||
|
#[cfg(feature = "calc")]
|
||||||
|
pub(crate) calc_resolver: fn(*const (), f32) -> f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "calc")]
|
||||||
|
fn default_calc_resolver(_value: *const (), _basis: f32) -> f32 {
|
||||||
|
0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TaffyConfig {
|
impl Default for TaffyConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self { use_rounding: true }
|
Self {
|
||||||
|
use_rounding: true,
|
||||||
|
#[cfg(feature = "calc")]
|
||||||
|
calc_resolver: default_calc_resolver,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,8 +400,8 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
fn resolve_calc_value(&self, _val: *const (), _basis: f32) -> f32 {
|
fn resolve_calc_value(&self, val: *const (), basis: f32) -> f32 {
|
||||||
0.0
|
(self.taffy.config.calc_resolver)(val, basis)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
@@ -566,6 +578,16 @@ impl<NodeContext> TaffyTree<NodeContext> {
|
|||||||
self.config.use_rounding = false;
|
self.config.use_rounding = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Installs the resolver used for opaque `calc()` handles.
|
||||||
|
///
|
||||||
|
/// Every calc handle stored in a node style must be understood by
|
||||||
|
/// `resolver` and remain alive for every layout computation performed by
|
||||||
|
/// this tree. Taffy treats handles as opaque and never dereferences them.
|
||||||
|
#[cfg(feature = "calc")]
|
||||||
|
pub fn set_calc_resolver(&mut self, resolver: fn(*const (), f32) -> f32) {
|
||||||
|
self.config.calc_resolver = resolver;
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates and adds a new unattached leaf node to the tree, and returns the node of the new node
|
/// Creates and adds a new unattached leaf node to the tree, and returns the node of the new node
|
||||||
pub fn new_leaf(&mut self, layout: Style) -> TaffyResult<NodeId> {
|
pub fn new_leaf(&mut self, layout: Style) -> TaffyResult<NodeId> {
|
||||||
let id = self.nodes.insert(NodeData::new(layout));
|
let id = self.nodes.insert(NodeData::new(layout));
|
||||||
|
|||||||
Reference in new issue
Block a user