stylesheets.rs (26038B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 5 //! A collection of invalidations due to changes in which stylesheets affect a 6 //! document. 7 8 #![deny(unsafe_code)] 9 10 use crate::context::QuirksMode; 11 use crate::data::ElementData; 12 use crate::derives::*; 13 use crate::dom::{TDocument, TElement, TNode}; 14 use crate::invalidation::element::element_wrapper::{ElementSnapshot, ElementWrapper}; 15 use crate::invalidation::element::restyle_hints::RestyleHint; 16 use crate::media_queries::Device; 17 use crate::selector_map::PrecomputedHashSet; 18 use crate::selector_parser::{SelectorImpl, Snapshot, SnapshotMap}; 19 use crate::shared_lock::SharedRwLockReadGuard; 20 use crate::simple_buckets_map::SimpleBucketsMap; 21 use crate::stylesheets::{ 22 CssRule, CssRuleRef, CustomMediaMap, EffectiveRules, EffectiveRulesIterator, 23 StylesheetInDocument, 24 }; 25 use crate::stylist::CascadeDataDifference; 26 use crate::values::specified::position::PositionTryFallbacksItem; 27 use crate::values::AtomIdent; 28 use crate::Atom; 29 use crate::LocalName as SelectorLocalName; 30 use selectors::parser::{Component, LocalName, Selector}; 31 32 /// The kind of change that happened for a given rule. 33 #[repr(u32)] 34 #[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)] 35 pub enum RuleChangeKind { 36 /// Some change in the rule which we don't know about, and could have made 37 /// the rule change in any way. 38 Generic = 0, 39 /// The rule was inserted. 40 Insertion, 41 /// The rule was removed. 42 Removal, 43 /// A change in the declarations of a style rule. 44 StyleRuleDeclarations, 45 /// A change in the declarations of an @position-try rule. 46 PositionTryDeclarations, 47 } 48 49 /// A style sheet invalidation represents a kind of element or subtree that may 50 /// need to be restyled. Whether it represents a whole subtree or just a single 51 /// element is determined by the given InvalidationKind in 52 /// StylesheetInvalidationSet's maps. 53 #[derive(Debug, Eq, Hash, MallocSizeOf, PartialEq)] 54 enum Invalidation { 55 /// An element with a given id. 56 ID(AtomIdent), 57 /// An element with a given class name. 58 Class(AtomIdent), 59 /// An element with a given local name. 60 LocalName { 61 name: SelectorLocalName, 62 lower_name: SelectorLocalName, 63 }, 64 } 65 66 impl Invalidation { 67 fn is_id(&self) -> bool { 68 matches!(*self, Invalidation::ID(..)) 69 } 70 71 fn is_id_or_class(&self) -> bool { 72 matches!(*self, Invalidation::ID(..) | Invalidation::Class(..)) 73 } 74 } 75 76 /// Whether we should invalidate just the element, or the whole subtree within 77 /// it. 78 #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Ord, PartialEq, PartialOrd)] 79 enum InvalidationKind { 80 None = 0, 81 Element, 82 Scope, 83 } 84 85 impl std::ops::BitOrAssign for InvalidationKind { 86 #[inline] 87 fn bitor_assign(&mut self, other: Self) { 88 *self = std::cmp::max(*self, other); 89 } 90 } 91 92 impl InvalidationKind { 93 #[inline] 94 fn is_scope(self) -> bool { 95 matches!(self, Self::Scope) 96 } 97 98 #[inline] 99 fn add(&mut self, other: Option<&InvalidationKind>) { 100 if let Some(other) = other { 101 *self |= *other; 102 } 103 } 104 } 105 106 /// A set of invalidations due to stylesheet changes. 107 /// 108 /// TODO(emilio): We might be able to do the same analysis for media query changes too (or even 109 /// selector changes?) specially now that we take the cascade data difference into account. 110 #[derive(Debug, Default, MallocSizeOf)] 111 pub struct StylesheetInvalidationSet { 112 buckets: SimpleBucketsMap<InvalidationKind>, 113 style_fully_invalid: bool, 114 /// The difference between the old and new cascade data, incrementally collected until flush() 115 /// returns it. 116 pub cascade_data_difference: CascadeDataDifference, 117 } 118 119 impl StylesheetInvalidationSet { 120 /// Create an empty `StylesheetInvalidationSet`. 121 pub fn new() -> Self { 122 Default::default() 123 } 124 125 /// Mark the DOM tree styles' as fully invalid. 126 pub fn invalidate_fully(&mut self) { 127 debug!("StylesheetInvalidationSet::invalidate_fully"); 128 self.buckets.clear(); 129 self.style_fully_invalid = true; 130 } 131 132 /// Analyze the given stylesheet, and collect invalidations from their rules, in order to avoid 133 /// doing a full restyle when we style the document next time. 134 pub fn collect_invalidations_for<S>( 135 &mut self, 136 device: &Device, 137 custom_media: &CustomMediaMap, 138 stylesheet: &S, 139 guard: &SharedRwLockReadGuard, 140 ) where 141 S: StylesheetInDocument, 142 { 143 debug!("StylesheetInvalidationSet::collect_invalidations_for"); 144 if self.style_fully_invalid { 145 debug!(" > Fully invalid already"); 146 return; 147 } 148 149 if !stylesheet.enabled() || !stylesheet.is_effective_for_device(device, custom_media, guard) 150 { 151 debug!(" > Stylesheet was not effective"); 152 return; // Nothing to do here. 153 } 154 155 let quirks_mode = device.quirks_mode(); 156 for rule in stylesheet 157 .contents(guard) 158 .effective_rules(device, custom_media, guard) 159 { 160 self.collect_invalidations_for_rule( 161 rule, 162 guard, 163 device, 164 quirks_mode, 165 /* is_generic_change = */ false, 166 // Note(dshin): Technically, the iterator should provide the ancestor chain as it 167 // traverses down, but it shouldn't make a difference. 168 &[], 169 ); 170 if self.style_fully_invalid { 171 break; 172 } 173 } 174 175 debug!( 176 " > resulting class invalidations: {:?}", 177 self.buckets.classes 178 ); 179 debug!(" > resulting id invalidations: {:?}", self.buckets.ids); 180 debug!( 181 " > resulting local name invalidations: {:?}", 182 self.buckets.local_names 183 ); 184 debug!(" > style_fully_invalid: {}", self.style_fully_invalid); 185 } 186 187 /// Returns whether there's no invalidation to process. 188 pub fn is_empty(&self) -> bool { 189 !self.style_fully_invalid 190 && self.buckets.is_empty() 191 && self.cascade_data_difference.is_empty() 192 } 193 194 fn invalidation_kind_for<E>( 195 &self, 196 element: E, 197 snapshot: Option<&Snapshot>, 198 quirks_mode: QuirksMode, 199 ) -> InvalidationKind 200 where 201 E: TElement, 202 { 203 debug_assert!(!self.style_fully_invalid); 204 205 let mut kind = InvalidationKind::None; 206 207 if !self.buckets.classes.is_empty() { 208 element.each_class(|c| { 209 kind.add(self.buckets.classes.get(c, quirks_mode)); 210 }); 211 212 if kind.is_scope() { 213 return kind; 214 } 215 216 if let Some(snapshot) = snapshot { 217 snapshot.each_class(|c| { 218 kind.add(self.buckets.classes.get(c, quirks_mode)); 219 }); 220 221 if kind.is_scope() { 222 return kind; 223 } 224 } 225 } 226 227 if !self.buckets.ids.is_empty() { 228 if let Some(ref id) = element.id() { 229 kind.add(self.buckets.ids.get(id, quirks_mode)); 230 if kind.is_scope() { 231 return kind; 232 } 233 } 234 235 if let Some(ref old_id) = snapshot.and_then(|s| s.id_attr()) { 236 kind.add(self.buckets.ids.get(old_id, quirks_mode)); 237 if kind.is_scope() { 238 return kind; 239 } 240 } 241 } 242 243 if !self.buckets.local_names.is_empty() { 244 kind.add(self.buckets.local_names.get(element.local_name())); 245 } 246 247 kind 248 } 249 250 /// Processes the style invalidation set, invalidating elements as needed. 251 /// Returns true if any style invalidations occurred. 252 pub fn process_style<E>(&self, root: E, snapshots: Option<&SnapshotMap>) -> bool 253 where 254 E: TElement, 255 { 256 debug!( 257 "StylesheetInvalidationSet::process_style({root:?}, snapshots: {})", 258 snapshots.is_some() 259 ); 260 261 { 262 let mut data = match root.mutate_data() { 263 Some(data) => data, 264 None => return false, 265 }; 266 267 if self.style_fully_invalid { 268 debug!("process_invalidations: fully_invalid({:?})", root); 269 data.hint.insert(RestyleHint::restyle_subtree()); 270 return true; 271 } 272 } 273 274 if self.buckets.is_empty() { 275 debug!("process_invalidations: empty invalidation set"); 276 return false; 277 } 278 279 let quirks_mode = root.as_node().owner_doc().quirks_mode(); 280 self.process_invalidations_in_subtree(root, snapshots, quirks_mode) 281 } 282 283 /// Process style invalidations in a given subtree. This traverses the 284 /// subtree looking for elements that match the invalidations in our hash 285 /// map members. 286 /// 287 /// Returns whether it invalidated at least one element's style. 288 #[allow(unsafe_code)] 289 fn process_invalidations_in_subtree<E>( 290 &self, 291 element: E, 292 snapshots: Option<&SnapshotMap>, 293 quirks_mode: QuirksMode, 294 ) -> bool 295 where 296 E: TElement, 297 { 298 debug!("process_invalidations_in_subtree({:?})", element); 299 let mut data = match element.mutate_data() { 300 Some(data) => data, 301 None => return false, 302 }; 303 304 if !data.has_styles() { 305 return false; 306 } 307 308 if data.hint.contains_subtree() { 309 debug!( 310 "process_invalidations_in_subtree: {:?} was already invalid", 311 element 312 ); 313 return false; 314 } 315 316 let element_wrapper = snapshots.map(|s| ElementWrapper::new(element, s)); 317 let snapshot = element_wrapper.as_ref().and_then(|e| e.snapshot()); 318 319 match self.invalidation_kind_for(element, snapshot, quirks_mode) { 320 InvalidationKind::None => {}, 321 InvalidationKind::Element => { 322 debug!( 323 "process_invalidations_in_subtree: {:?} matched self", 324 element 325 ); 326 data.hint.insert(RestyleHint::RESTYLE_SELF); 327 }, 328 InvalidationKind::Scope => { 329 debug!( 330 "process_invalidations_in_subtree: {:?} matched subtree", 331 element 332 ); 333 data.hint.insert(RestyleHint::restyle_subtree()); 334 return true; 335 }, 336 } 337 338 let mut any_children_invalid = false; 339 340 for child in element.traversal_children() { 341 let child = match child.as_element() { 342 Some(e) => e, 343 None => continue, 344 }; 345 346 any_children_invalid |= 347 self.process_invalidations_in_subtree(child, snapshots, quirks_mode); 348 } 349 350 if any_children_invalid { 351 debug!( 352 "Children of {:?} changed, setting dirty descendants", 353 element 354 ); 355 unsafe { element.set_dirty_descendants() } 356 } 357 358 data.hint.contains(RestyleHint::RESTYLE_SELF) || any_children_invalid 359 } 360 361 /// TODO(emilio): Reuse the bucket stuff from selectormap? That handles :is() / :where() etc. 362 fn scan_component( 363 component: &Component<SelectorImpl>, 364 invalidation: &mut Option<Invalidation>, 365 ) { 366 match *component { 367 Component::LocalName(LocalName { 368 ref name, 369 ref lower_name, 370 }) => { 371 if invalidation.is_none() { 372 *invalidation = Some(Invalidation::LocalName { 373 name: name.clone(), 374 lower_name: lower_name.clone(), 375 }); 376 } 377 }, 378 Component::Class(ref class) => { 379 if invalidation.as_ref().map_or(true, |s| !s.is_id_or_class()) { 380 *invalidation = Some(Invalidation::Class(class.clone())); 381 } 382 }, 383 Component::ID(ref id) => { 384 if invalidation.as_ref().map_or(true, |s| !s.is_id()) { 385 *invalidation = Some(Invalidation::ID(id.clone())); 386 } 387 }, 388 _ => { 389 // Ignore everything else, at least for now. 390 }, 391 } 392 } 393 394 /// Collect invalidations for a given selector. 395 /// 396 /// We look at the outermost local name, class, or ID selector to the left 397 /// of an ancestor combinator, in order to restyle only a given subtree. 398 /// 399 /// If the selector has no ancestor combinator, then we do the same for 400 /// the only sequence it has, but record it as an element invalidation 401 /// instead of a subtree invalidation. 402 /// 403 /// We prefer IDs to classs, and classes to local names, on the basis 404 /// that the former should be more specific than the latter. We also 405 /// prefer to generate subtree invalidations for the outermost part 406 /// of the selector, to reduce the amount of traversal we need to do 407 /// when flushing invalidations. 408 fn collect_invalidations( 409 &mut self, 410 selector: &Selector<SelectorImpl>, 411 quirks_mode: QuirksMode, 412 ) { 413 debug!( 414 "StylesheetInvalidationSet::collect_invalidations({:?})", 415 selector 416 ); 417 418 let mut element_invalidation: Option<Invalidation> = None; 419 let mut subtree_invalidation: Option<Invalidation> = None; 420 421 let mut scan_for_element_invalidation = true; 422 let mut scan_for_subtree_invalidation = false; 423 424 let mut iter = selector.iter(); 425 426 loop { 427 for component in &mut iter { 428 if scan_for_element_invalidation { 429 Self::scan_component(component, &mut element_invalidation); 430 } else if scan_for_subtree_invalidation { 431 Self::scan_component(component, &mut subtree_invalidation); 432 } 433 } 434 match iter.next_sequence() { 435 None => break, 436 Some(combinator) => { 437 scan_for_subtree_invalidation = combinator.is_ancestor(); 438 }, 439 } 440 scan_for_element_invalidation = false; 441 } 442 443 if let Some(s) = subtree_invalidation { 444 debug!(" > Found subtree invalidation: {:?}", s); 445 if self.insert_invalidation(s, InvalidationKind::Scope, quirks_mode) { 446 return; 447 } 448 } 449 450 if let Some(s) = element_invalidation { 451 debug!(" > Found element invalidation: {:?}", s); 452 if self.insert_invalidation(s, InvalidationKind::Element, quirks_mode) { 453 return; 454 } 455 } 456 457 // The selector was of a form that we can't handle. Any element could 458 // match it, so let's just bail out. 459 debug!(" > Can't handle selector or OOMd, marking fully invalid"); 460 self.invalidate_fully() 461 } 462 463 fn insert_invalidation( 464 &mut self, 465 invalidation: Invalidation, 466 kind: InvalidationKind, 467 quirks_mode: QuirksMode, 468 ) -> bool { 469 match invalidation { 470 Invalidation::Class(c) => { 471 let entry = match self.buckets.classes.try_entry(c.0, quirks_mode) { 472 Ok(e) => e, 473 Err(..) => return false, 474 }; 475 *entry.or_insert(InvalidationKind::None) |= kind; 476 }, 477 Invalidation::ID(i) => { 478 let entry = match self.buckets.ids.try_entry(i.0, quirks_mode) { 479 Ok(e) => e, 480 Err(..) => return false, 481 }; 482 *entry.or_insert(InvalidationKind::None) |= kind; 483 }, 484 Invalidation::LocalName { name, lower_name } => { 485 let insert_lower = name != lower_name; 486 if self.buckets.local_names.try_reserve(1).is_err() { 487 return false; 488 } 489 let entry = self.buckets.local_names.entry(name); 490 *entry.or_insert(InvalidationKind::None) |= kind; 491 if insert_lower { 492 if self.buckets.local_names.try_reserve(1).is_err() { 493 return false; 494 } 495 let entry = self.buckets.local_names.entry(lower_name); 496 *entry.or_insert(InvalidationKind::None) |= kind; 497 } 498 }, 499 } 500 501 true 502 } 503 504 /// Collects invalidations for a given CSS rule, if not fully invalid already. 505 pub fn rule_changed<S>( 506 &mut self, 507 stylesheet: &S, 508 rule: &CssRule, 509 guard: &SharedRwLockReadGuard, 510 device: &Device, 511 quirks_mode: QuirksMode, 512 custom_media: &CustomMediaMap, 513 change_kind: RuleChangeKind, 514 ancestors: &[CssRuleRef], 515 ) where 516 S: StylesheetInDocument, 517 { 518 debug!("StylesheetInvalidationSet::rule_changed"); 519 if !stylesheet.enabled() || !stylesheet.is_effective_for_device(device, custom_media, guard) 520 { 521 debug!(" > Stylesheet was not effective"); 522 return; // Nothing to do here. 523 } 524 525 if ancestors 526 .iter() 527 .any(|r| !EffectiveRules::is_effective(guard, device, quirks_mode, custom_media, r)) 528 { 529 debug!(" > Ancestor rules not effective"); 530 return; 531 } 532 533 if change_kind == RuleChangeKind::PositionTryDeclarations { 534 // @position-try declaration changes need to be dealt explicitly, since the 535 // declarations are mutable and we can't otherwise detect changes to them. 536 match *rule { 537 CssRule::PositionTry(ref pt) => { 538 self.cascade_data_difference 539 .changed_position_try_names 540 .insert(pt.read_with(guard).name.0.clone()); 541 }, 542 _ => debug_assert!(false, "how did position-try decls change on anything else?"), 543 } 544 return; 545 } 546 547 if self.style_fully_invalid { 548 return; 549 } 550 551 // If the change is generic, we don't have the old rule information to know e.g., the old 552 // media condition, or the old selector text, so we might need to invalidate more 553 // aggressively. That only applies to the changed rules, for other rules we can just 554 // collect invalidations as normal. 555 let is_generic_change = change_kind == RuleChangeKind::Generic; 556 self.collect_invalidations_for_rule( 557 rule, 558 guard, 559 device, 560 quirks_mode, 561 is_generic_change, 562 ancestors, 563 ); 564 if self.style_fully_invalid { 565 return; 566 } 567 568 if !is_generic_change 569 && !EffectiveRules::is_effective(guard, device, quirks_mode, custom_media, &rule.into()) 570 { 571 return; 572 } 573 574 let rules = EffectiveRulesIterator::effective_children( 575 device, 576 quirks_mode, 577 custom_media, 578 guard, 579 rule, 580 ); 581 for rule in rules { 582 self.collect_invalidations_for_rule( 583 rule, 584 guard, 585 device, 586 quirks_mode, 587 /* is_generic_change = */ false, 588 // Note(dshin): Technically, the iterator should provide the ancestor chain as it traverses down, which sould be appended to `ancestors`, but it shouldn't matter. 589 &[], 590 ); 591 if self.style_fully_invalid { 592 break; 593 } 594 } 595 } 596 597 /// Collects invalidations for a given CSS rule. 598 fn collect_invalidations_for_rule( 599 &mut self, 600 rule: &CssRule, 601 guard: &SharedRwLockReadGuard, 602 device: &Device, 603 quirks_mode: QuirksMode, 604 is_generic_change: bool, 605 ancestors: &[CssRuleRef], 606 ) { 607 use crate::stylesheets::CssRule::*; 608 debug!("StylesheetInvalidationSet::collect_invalidations_for_rule"); 609 debug_assert!(!self.style_fully_invalid, "Not worth being here!"); 610 611 match *rule { 612 Style(ref lock) => { 613 if is_generic_change { 614 // TODO(emilio): We need to do this for selector / keyframe 615 // name / font-face changes, because we don't have the old 616 // selector / name. If we distinguish those changes 617 // specially, then we can at least use this invalidation for 618 // style declaration changes. 619 return self.invalidate_fully(); 620 } 621 622 let style_rule = lock.read_with(guard); 623 for selector in style_rule.selectors.slice() { 624 self.collect_invalidations(selector, quirks_mode); 625 if self.style_fully_invalid { 626 return; 627 } 628 } 629 }, 630 NestedDeclarations(..) => { 631 if ancestors.iter().any(|r| matches!(r, CssRuleRef::Scope(_))) { 632 self.invalidate_fully(); 633 } 634 }, 635 Namespace(..) => { 636 // It's not clear what handling changes for this correctly would 637 // look like. 638 }, 639 LayerStatement(..) => { 640 // Layer statement insertions might alter styling order, so we need to always 641 // invalidate fully. 642 return self.invalidate_fully(); 643 }, 644 Document(..) | Import(..) | Media(..) | Supports(..) | Container(..) 645 | LayerBlock(..) | StartingStyle(..) => { 646 // Do nothing, relevant nested rules are visited as part of rule iteration. 647 }, 648 FontFace(..) => { 649 // Do nothing, @font-face doesn't affect computed style information on it's own. 650 // We'll restyle when the font face loads, if needed. 651 }, 652 Page(..) | Margin(..) => { 653 // Do nothing, we don't support OM mutations on print documents, and page rules 654 // can't affect anything else. 655 }, 656 Keyframes(ref lock) => { 657 if is_generic_change { 658 return self.invalidate_fully(); 659 } 660 let keyframes_rule = lock.read_with(guard); 661 if device.animation_name_may_be_referenced(&keyframes_rule.name) { 662 debug!( 663 " > Found @keyframes rule potentially referenced \ 664 from the page, marking the whole tree invalid." 665 ); 666 self.invalidate_fully(); 667 } else { 668 // Do nothing, this animation can't affect the style of existing elements. 669 } 670 }, 671 CounterStyle(..) | Property(..) | FontFeatureValues(..) | FontPaletteValues(..) => { 672 debug!(" > Found unsupported rule, marking the whole subtree invalid."); 673 self.invalidate_fully(); 674 }, 675 Scope(..) => { 676 // Addition/removal of @scope requires re-evaluation of scope proximity to properly 677 // figure out the styling order. 678 self.invalidate_fully(); 679 }, 680 PositionTry(..) => { 681 // @position-try changes doesn't change style-time information (only layout 682 // information) and is handled by invalidate_position_try. So do nothing. 683 }, 684 CustomMedia(..) => { 685 // @custom-media might be referenced by other rules which we can't get a hand on in 686 // here, so we don't know which elements are affected. 687 // 688 // TODO: Maybe track referenced custom-media rules like we do for @keyframe? 689 self.invalidate_fully(); 690 }, 691 } 692 } 693 } 694 695 /// Invalidates for any absolutely positioned element that references the given @position-try fallback names. 696 pub fn invalidate_position_try<E>( 697 element: E, 698 changed_names: &PrecomputedHashSet<Atom>, 699 invalidate_self: &mut impl FnMut(E, &mut ElementData), 700 invalidated_descendants: &mut impl FnMut(E), 701 ) -> bool 702 where 703 E: TElement, 704 { 705 debug_assert!( 706 !changed_names.is_empty(), 707 "Don't call me if there's nothing to do" 708 ); 709 let mut data = match element.mutate_data() { 710 Some(data) => data, 711 None => return false, 712 }; 713 714 let mut self_invalid = false; 715 let style = data.styles.primary(); 716 if style.clone_position().is_absolutely_positioned() { 717 let fallbacks = style.clone_position_try_fallbacks(); 718 let referenced = fallbacks.0.iter().any(|f| match f { 719 PositionTryFallbacksItem::IdentAndOrTactic(ident_or_tactic) => { 720 changed_names.contains(&ident_or_tactic.ident.0) 721 }, 722 PositionTryFallbacksItem::PositionArea(..) => false, 723 }); 724 725 if referenced { 726 self_invalid = true; 727 invalidate_self(element, &mut data); 728 } 729 } 730 let mut any_children_invalid = false; 731 for child in element.traversal_children() { 732 let Some(e) = child.as_element() else { 733 continue; 734 }; 735 any_children_invalid |= 736 invalidate_position_try(e, changed_names, invalidate_self, invalidated_descendants); 737 } 738 if any_children_invalid { 739 invalidated_descendants(element); 740 } 741 self_invalid || any_children_invalid 742 }