container_rule.rs (21205B)
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 [`@container`][container] rule. 6 //! 7 //! [container]: https://drafts.csswg.org/css-contain-3/#container-rule 8 9 use crate::computed_value_flags::ComputedValueFlags; 10 use crate::derives::*; 11 use crate::dom::TElement; 12 use crate::logical_geometry::{LogicalSize, WritingMode}; 13 use crate::parser::ParserContext; 14 use crate::properties::ComputedValues; 15 use crate::queries::feature::{AllowsRanges, Evaluator, FeatureFlags, QueryFeatureDescription}; 16 use crate::queries::values::Orientation; 17 use crate::queries::{FeatureType, QueryCondition}; 18 use crate::shared_lock::{ 19 DeepCloneWithLock, Locked, SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard, 20 }; 21 use crate::stylesheets::{CssRules, CustomMediaEvaluator}; 22 use crate::stylist::Stylist; 23 use crate::values::computed::{CSSPixelLength, ContainerType, Context, Ratio}; 24 use crate::values::specified::ContainerName; 25 use app_units::Au; 26 use cssparser::{Parser, SourceLocation}; 27 use euclid::default::Size2D; 28 #[cfg(feature = "gecko")] 29 use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf}; 30 use selectors::kleene_value::KleeneValue; 31 use servo_arc::Arc; 32 use std::fmt::{self, Write}; 33 use style_traits::{CssStringWriter, CssWriter, ParseError, ToCss}; 34 35 /// A container rule. 36 #[derive(Debug, ToShmem)] 37 pub struct ContainerRule { 38 /// The container query and name. 39 pub condition: Arc<ContainerCondition>, 40 /// The nested rules inside the block. 41 pub rules: Arc<Locked<CssRules>>, 42 /// The source position where this rule was found. 43 pub source_location: SourceLocation, 44 } 45 46 impl ContainerRule { 47 /// Returns the query condition. 48 pub fn query_condition(&self) -> &QueryCondition { 49 &self.condition.condition 50 } 51 52 /// Returns the query name filter. 53 pub fn container_name(&self) -> &ContainerName { 54 &self.condition.name 55 } 56 57 /// Measure heap usage. 58 #[cfg(feature = "gecko")] 59 pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize { 60 // Measurement of other fields may be added later. 61 self.rules.unconditional_shallow_size_of(ops) 62 + self.rules.read_with(guard).size_of(guard, ops) 63 } 64 } 65 66 impl DeepCloneWithLock for ContainerRule { 67 fn deep_clone_with_lock(&self, lock: &SharedRwLock, guard: &SharedRwLockReadGuard) -> Self { 68 let rules = self.rules.read_with(guard); 69 Self { 70 condition: self.condition.clone(), 71 rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard))), 72 source_location: self.source_location.clone(), 73 } 74 } 75 } 76 77 impl ToCssWithGuard for ContainerRule { 78 fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { 79 dest.write_str("@container ")?; 80 { 81 let mut writer = CssWriter::new(dest); 82 if !self.condition.name.is_none() { 83 self.condition.name.to_css(&mut writer)?; 84 writer.write_char(' ')?; 85 } 86 self.condition.condition.to_css(&mut writer)?; 87 } 88 self.rules.read_with(guard).to_css_block(guard, dest) 89 } 90 } 91 92 /// A container condition and filter, combined. 93 #[derive(Debug, ToShmem, ToCss)] 94 pub struct ContainerCondition { 95 #[css(skip_if = "ContainerName::is_none")] 96 name: ContainerName, 97 condition: QueryCondition, 98 #[css(skip)] 99 flags: FeatureFlags, 100 } 101 102 /// The result of a successful container query lookup. 103 pub struct ContainerLookupResult<E> { 104 /// The relevant container. 105 pub element: E, 106 /// The sizing / writing-mode information of the container. 107 pub info: ContainerInfo, 108 /// The style of the element. 109 pub style: Arc<ComputedValues>, 110 } 111 112 fn container_type_axes(ty_: ContainerType, wm: WritingMode) -> FeatureFlags { 113 if ty_.intersects(ContainerType::SIZE) { 114 FeatureFlags::all_container_axes() 115 } else if ty_.intersects(ContainerType::INLINE_SIZE) { 116 let physical_axis = if wm.is_vertical() { 117 FeatureFlags::CONTAINER_REQUIRES_HEIGHT_AXIS 118 } else { 119 FeatureFlags::CONTAINER_REQUIRES_WIDTH_AXIS 120 }; 121 FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS | physical_axis 122 } else { 123 FeatureFlags::empty() 124 } 125 } 126 127 enum TraversalResult<T> { 128 InProgress, 129 StopTraversal, 130 Done(T), 131 } 132 133 fn traverse_container<E, F, R>( 134 mut e: E, 135 originating_element_style: Option<&ComputedValues>, 136 evaluator: F, 137 ) -> Option<(E, R)> 138 where 139 E: TElement, 140 F: Fn(E, Option<&ComputedValues>) -> TraversalResult<R>, 141 { 142 if originating_element_style.is_some() { 143 match evaluator(e, originating_element_style) { 144 TraversalResult::InProgress => {}, 145 TraversalResult::StopTraversal => return None, 146 TraversalResult::Done(result) => return Some((e, result)), 147 } 148 } 149 while let Some(element) = e.traversal_parent() { 150 match evaluator(element, None) { 151 TraversalResult::InProgress => {}, 152 TraversalResult::StopTraversal => return None, 153 TraversalResult::Done(result) => return Some((element, result)), 154 } 155 e = element; 156 } 157 158 None 159 } 160 161 impl ContainerCondition { 162 /// Parse a container condition. 163 pub fn parse<'a>( 164 context: &ParserContext, 165 input: &mut Parser<'a, '_>, 166 ) -> Result<Self, ParseError<'a>> { 167 let name = input 168 .try_parse(|input| ContainerName::parse_for_query(context, input)) 169 .ok() 170 .unwrap_or_else(ContainerName::none); 171 let condition = QueryCondition::parse(context, input, FeatureType::Container)?; 172 let flags = condition.cumulative_flags(); 173 Ok(Self { 174 name, 175 condition, 176 flags, 177 }) 178 } 179 180 fn valid_container_info<E>( 181 &self, 182 potential_container: E, 183 originating_element_style: Option<&ComputedValues>, 184 ) -> TraversalResult<ContainerLookupResult<E>> 185 where 186 E: TElement, 187 { 188 let data; 189 let style = match originating_element_style { 190 Some(s) => s, 191 None => { 192 data = match potential_container.borrow_data() { 193 Some(d) => d, 194 None => return TraversalResult::InProgress, 195 }; 196 &**data.styles.primary() 197 }, 198 }; 199 let wm = style.writing_mode; 200 let box_style = style.get_box(); 201 202 // Filter by container-type. 203 let container_type = box_style.clone_container_type(); 204 let available_axes = container_type_axes(container_type, wm); 205 if !available_axes.contains(self.flags.container_axes()) { 206 return TraversalResult::InProgress; 207 } 208 209 // Filter by container-name. 210 let container_name = box_style.clone_container_name(); 211 for filter_name in self.name.0.iter() { 212 if !container_name.0.contains(filter_name) { 213 return TraversalResult::InProgress; 214 } 215 } 216 217 let size = potential_container.query_container_size(&box_style.clone_display()); 218 let style = style.to_arc(); 219 TraversalResult::Done(ContainerLookupResult { 220 element: potential_container, 221 info: ContainerInfo { size, wm }, 222 style, 223 }) 224 } 225 226 /// Performs container lookup for a given element. 227 pub fn find_container<E>( 228 &self, 229 e: E, 230 originating_element_style: Option<&ComputedValues>, 231 ) -> Option<ContainerLookupResult<E>> 232 where 233 E: TElement, 234 { 235 match traverse_container( 236 e, 237 originating_element_style, 238 |element, originating_element_style| { 239 self.valid_container_info(element, originating_element_style) 240 }, 241 ) { 242 Some((_, result)) => Some(result), 243 None => None, 244 } 245 } 246 247 /// Tries to match a container query condition for a given element. 248 pub(crate) fn matches<E>( 249 &self, 250 stylist: &Stylist, 251 element: E, 252 originating_element_style: Option<&ComputedValues>, 253 invalidation_flags: &mut ComputedValueFlags, 254 ) -> KleeneValue 255 where 256 E: TElement, 257 { 258 let result = self.find_container(element, originating_element_style); 259 let (container, info) = match result { 260 Some(r) => (Some(r.element), Some((r.info, r.style))), 261 None => (None, None), 262 }; 263 // Set up the lookup for the container in question, as the condition may be using container 264 // query lengths. 265 let size_query_container_lookup = ContainerSizeQuery::for_option_element( 266 container, /* known_parent_style = */ None, /* is_pseudo = */ false, 267 ); 268 Context::for_container_query_evaluation( 269 stylist.device(), 270 Some(stylist), 271 info, 272 size_query_container_lookup, 273 |context| { 274 let matches = self 275 .condition 276 .matches(context, &mut CustomMediaEvaluator::none()); 277 if context 278 .style() 279 .flags() 280 .contains(ComputedValueFlags::USES_VIEWPORT_UNITS) 281 { 282 // TODO(emilio): Might need something similar to improve 283 // invalidation of font relative container-query lengths. 284 invalidation_flags 285 .insert(ComputedValueFlags::USES_VIEWPORT_UNITS_ON_CONTAINER_QUERIES); 286 } 287 matches 288 }, 289 ) 290 } 291 } 292 293 /// Information needed to evaluate an individual container query. 294 #[derive(Copy, Clone)] 295 pub struct ContainerInfo { 296 size: Size2D<Option<Au>>, 297 wm: WritingMode, 298 } 299 300 impl ContainerInfo { 301 fn size(&self) -> Option<Size2D<Au>> { 302 Some(Size2D::new(self.size.width?, self.size.height?)) 303 } 304 } 305 306 fn eval_width(context: &Context) -> Option<CSSPixelLength> { 307 let info = context.container_info.as_ref()?; 308 Some(CSSPixelLength::new(info.size.width?.to_f32_px())) 309 } 310 311 fn eval_height(context: &Context) -> Option<CSSPixelLength> { 312 let info = context.container_info.as_ref()?; 313 Some(CSSPixelLength::new(info.size.height?.to_f32_px())) 314 } 315 316 fn eval_inline_size(context: &Context) -> Option<CSSPixelLength> { 317 let info = context.container_info.as_ref()?; 318 Some(CSSPixelLength::new( 319 LogicalSize::from_physical(info.wm, info.size) 320 .inline? 321 .to_f32_px(), 322 )) 323 } 324 325 fn eval_block_size(context: &Context) -> Option<CSSPixelLength> { 326 let info = context.container_info.as_ref()?; 327 Some(CSSPixelLength::new( 328 LogicalSize::from_physical(info.wm, info.size) 329 .block? 330 .to_f32_px(), 331 )) 332 } 333 334 fn eval_aspect_ratio(context: &Context) -> Option<Ratio> { 335 let info = context.container_info.as_ref()?; 336 Some(Ratio::new( 337 info.size.width?.0 as f32, 338 info.size.height?.0 as f32, 339 )) 340 } 341 342 fn eval_orientation(context: &Context, value: Option<Orientation>) -> KleeneValue { 343 let size = match context.container_info.as_ref().and_then(|info| info.size()) { 344 Some(size) => size, 345 None => return KleeneValue::Unknown, 346 }; 347 KleeneValue::from(Orientation::eval(size, value)) 348 } 349 350 /// https://drafts.csswg.org/css-contain-3/#container-features 351 /// 352 /// TODO: Support style queries, perhaps. 353 pub static CONTAINER_FEATURES: [QueryFeatureDescription; 6] = [ 354 feature!( 355 atom!("width"), 356 AllowsRanges::Yes, 357 Evaluator::OptionalLength(eval_width), 358 FeatureFlags::CONTAINER_REQUIRES_WIDTH_AXIS, 359 ), 360 feature!( 361 atom!("height"), 362 AllowsRanges::Yes, 363 Evaluator::OptionalLength(eval_height), 364 FeatureFlags::CONTAINER_REQUIRES_HEIGHT_AXIS, 365 ), 366 feature!( 367 atom!("inline-size"), 368 AllowsRanges::Yes, 369 Evaluator::OptionalLength(eval_inline_size), 370 FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS, 371 ), 372 feature!( 373 atom!("block-size"), 374 AllowsRanges::Yes, 375 Evaluator::OptionalLength(eval_block_size), 376 FeatureFlags::CONTAINER_REQUIRES_BLOCK_AXIS, 377 ), 378 feature!( 379 atom!("aspect-ratio"), 380 AllowsRanges::Yes, 381 Evaluator::OptionalNumberRatio(eval_aspect_ratio), 382 // XXX from_bits_truncate is const, but the pipe operator isn't, so this 383 // works around it. 384 FeatureFlags::from_bits_truncate( 385 FeatureFlags::CONTAINER_REQUIRES_BLOCK_AXIS.bits() 386 | FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS.bits() 387 ), 388 ), 389 feature!( 390 atom!("orientation"), 391 AllowsRanges::No, 392 keyword_evaluator!(eval_orientation, Orientation), 393 FeatureFlags::from_bits_truncate( 394 FeatureFlags::CONTAINER_REQUIRES_BLOCK_AXIS.bits() 395 | FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS.bits() 396 ), 397 ), 398 ]; 399 400 /// Result of a container size query, signifying the hypothetical containment boundary in terms of physical axes. 401 /// Defined by up to two size containers. Queries on logical axes are resolved with respect to the querying 402 /// element's writing mode. 403 #[derive(Copy, Clone, Default)] 404 pub struct ContainerSizeQueryResult { 405 width: Option<Au>, 406 height: Option<Au>, 407 } 408 409 impl ContainerSizeQueryResult { 410 fn get_viewport_size(context: &Context) -> Size2D<Au> { 411 use crate::values::specified::ViewportVariant; 412 context.viewport_size_for_viewport_unit_resolution(ViewportVariant::Small) 413 } 414 415 fn get_logical_viewport_size(context: &Context) -> LogicalSize<Au> { 416 LogicalSize::from_physical( 417 context.builder.writing_mode, 418 Self::get_viewport_size(context), 419 ) 420 } 421 422 /// Get the inline-size of the query container. 423 pub fn get_container_inline_size(&self, context: &Context) -> Au { 424 if context.builder.writing_mode.is_horizontal() { 425 if let Some(w) = self.width { 426 return w; 427 } 428 } else { 429 if let Some(h) = self.height { 430 return h; 431 } 432 } 433 Self::get_logical_viewport_size(context).inline 434 } 435 436 /// Get the block-size of the query container. 437 pub fn get_container_block_size(&self, context: &Context) -> Au { 438 if context.builder.writing_mode.is_horizontal() { 439 self.get_container_height(context) 440 } else { 441 self.get_container_width(context) 442 } 443 } 444 445 /// Get the width of the query container. 446 pub fn get_container_width(&self, context: &Context) -> Au { 447 if let Some(w) = self.width { 448 return w; 449 } 450 Self::get_viewport_size(context).width 451 } 452 453 /// Get the height of the query container. 454 pub fn get_container_height(&self, context: &Context) -> Au { 455 if let Some(h) = self.height { 456 return h; 457 } 458 Self::get_viewport_size(context).height 459 } 460 461 // Merge the result of a subsequent lookup, preferring the initial result. 462 fn merge(self, new_result: Self) -> Self { 463 let mut result = self; 464 if let Some(width) = new_result.width { 465 result.width.get_or_insert(width); 466 } 467 if let Some(height) = new_result.height { 468 result.height.get_or_insert(height); 469 } 470 result 471 } 472 473 fn is_complete(&self) -> bool { 474 self.width.is_some() && self.height.is_some() 475 } 476 } 477 478 /// Unevaluated lazy container size query. 479 pub enum ContainerSizeQuery<'a> { 480 /// Query prior to evaluation. 481 NotEvaluated(Box<dyn Fn() -> ContainerSizeQueryResult + 'a>), 482 /// Cached evaluated result. 483 Evaluated(ContainerSizeQueryResult), 484 } 485 486 impl<'a> ContainerSizeQuery<'a> { 487 fn evaluate_potential_size_container<E>( 488 e: E, 489 originating_element_style: Option<&ComputedValues>, 490 ) -> TraversalResult<ContainerSizeQueryResult> 491 where 492 E: TElement, 493 { 494 let data; 495 let style = match originating_element_style { 496 Some(s) => s, 497 None => { 498 data = match e.borrow_data() { 499 Some(d) => d, 500 None => return TraversalResult::InProgress, 501 }; 502 &**data.styles.primary() 503 }, 504 }; 505 if !style 506 .flags 507 .contains(ComputedValueFlags::SELF_OR_ANCESTOR_HAS_SIZE_CONTAINER_TYPE) 508 { 509 // We know we won't find a size container. 510 return TraversalResult::StopTraversal; 511 } 512 513 let wm = style.writing_mode; 514 let box_style = style.get_box(); 515 516 let container_type = box_style.clone_container_type(); 517 let size = e.query_container_size(&box_style.clone_display()); 518 if container_type.intersects(ContainerType::SIZE) { 519 TraversalResult::Done(ContainerSizeQueryResult { 520 width: size.width, 521 height: size.height, 522 }) 523 } else if container_type.intersects(ContainerType::INLINE_SIZE) { 524 if wm.is_horizontal() { 525 TraversalResult::Done(ContainerSizeQueryResult { 526 width: size.width, 527 height: None, 528 }) 529 } else { 530 TraversalResult::Done(ContainerSizeQueryResult { 531 width: None, 532 height: size.height, 533 }) 534 } 535 } else { 536 TraversalResult::InProgress 537 } 538 } 539 540 /// Find the query container size for a given element. Meant to be used as a callback for new(). 541 fn lookup<E>( 542 element: E, 543 originating_element_style: Option<&ComputedValues>, 544 ) -> ContainerSizeQueryResult 545 where 546 E: TElement + 'a, 547 { 548 match traverse_container( 549 element, 550 originating_element_style, 551 |e, originating_element_style| { 552 Self::evaluate_potential_size_container(e, originating_element_style) 553 }, 554 ) { 555 Some((container, result)) => { 556 if result.is_complete() { 557 result 558 } else { 559 // Traverse up from the found size container to see if we can get a complete containment. 560 result.merge(Self::lookup(container, None)) 561 } 562 }, 563 None => ContainerSizeQueryResult::default(), 564 } 565 } 566 567 /// Create a new instance of the container size query for given element, with a deferred lookup callback. 568 pub fn for_element<E>( 569 element: E, 570 known_parent_style: Option<&'a ComputedValues>, 571 is_pseudo: bool, 572 ) -> Self 573 where 574 E: TElement + 'a, 575 { 576 let parent; 577 let data; 578 let parent_style = match known_parent_style { 579 Some(s) => Some(s), 580 None => { 581 // No need to bother if we're the top element. 582 parent = match element.traversal_parent() { 583 Some(parent) => parent, 584 None => return Self::none(), 585 }; 586 data = parent.borrow_data(); 587 data.as_ref().map(|data| &**data.styles.primary()) 588 }, 589 }; 590 591 // If there's no style, such as being `display: none` or so, we still want to show a 592 // correct computed value, so give it a try. 593 let should_traverse = parent_style.map_or(true, |s| { 594 s.flags 595 .contains(ComputedValueFlags::SELF_OR_ANCESTOR_HAS_SIZE_CONTAINER_TYPE) 596 }); 597 if !should_traverse { 598 return Self::none(); 599 } 600 return Self::NotEvaluated(Box::new(move || { 601 Self::lookup(element, if is_pseudo { known_parent_style } else { None }) 602 })); 603 } 604 605 /// Create a new instance, but with optional element. 606 pub fn for_option_element<E>( 607 element: Option<E>, 608 known_parent_style: Option<&'a ComputedValues>, 609 is_pseudo: bool, 610 ) -> Self 611 where 612 E: TElement + 'a, 613 { 614 if let Some(e) = element { 615 Self::for_element(e, known_parent_style, is_pseudo) 616 } else { 617 Self::none() 618 } 619 } 620 621 /// Create a query that evaluates to empty, for cases where container size query is not required. 622 pub fn none() -> Self { 623 ContainerSizeQuery::Evaluated(ContainerSizeQueryResult::default()) 624 } 625 626 /// Get the result of the container size query, doing the lookup if called for the first time. 627 pub fn get(&mut self) -> ContainerSizeQueryResult { 628 match self { 629 Self::NotEvaluated(lookup) => { 630 *self = Self::Evaluated((lookup)()); 631 match self { 632 Self::Evaluated(info) => *info, 633 _ => unreachable!("Just evaluated but not set?"), 634 } 635 }, 636 Self::Evaluated(info) => *info, 637 } 638 } 639 }