style_adjuster.rs (42987B)
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 struct to encapsulate all the style fixups and flags propagations 6 //! a computed style needs in order for it to adhere to the CSS spec. 7 8 use crate::computed_value_flags::ComputedValueFlags; 9 use crate::dom::TElement; 10 use crate::logical_geometry::PhysicalSide; 11 use crate::properties::longhands::display::computed_value::T as Display; 12 use crate::properties::longhands::float::computed_value::T as Float; 13 use crate::properties::longhands::position::computed_value::T as Position; 14 #[cfg(feature = "gecko")] 15 use crate::properties::longhands::{ 16 contain::computed_value::T as Contain, container_type::computed_value::T as ContainerType, 17 content_visibility::computed_value::T as ContentVisibility, 18 overflow_x::computed_value::T as Overflow, 19 }; 20 use crate::properties::{ComputedValues, StyleBuilder}; 21 use crate::values::computed::position::{ 22 PositionTryFallbacksTryTactic, PositionTryFallbacksTryTacticKeyword, TryTacticAdjustment, 23 }; 24 use crate::values::specified::align::AlignFlags; 25 26 #[cfg(feature = "gecko")] 27 use selectors::parser::PseudoElement; 28 29 /// A struct that implements all the adjustment methods. 30 /// 31 /// NOTE(emilio): If new adjustments are introduced that depend on reset 32 /// properties of the parent, you may need tweaking the 33 /// `ChildCascadeRequirement` code in `matching.rs`. 34 /// 35 /// NOTE(emilio): Also, if new adjustments are introduced that break the 36 /// following invariant: 37 /// 38 /// Given same tag name, namespace, rules and parent style, two elements would 39 /// end up with exactly the same style. 40 /// 41 /// Then you need to adjust the lookup_by_rules conditions in the sharing cache. 42 pub struct StyleAdjuster<'a, 'b: 'a> { 43 style: &'a mut StyleBuilder<'b>, 44 } 45 46 #[cfg(feature = "gecko")] 47 fn is_topmost_svg_svg_element<E>(e: E) -> bool 48 where 49 E: TElement, 50 { 51 debug_assert!(e.is_svg_element()); 52 if e.local_name() != &*atom!("svg") { 53 return false; 54 } 55 56 let parent = match e.traversal_parent() { 57 Some(n) => n, 58 None => return true, 59 }; 60 61 if !parent.is_svg_element() { 62 return true; 63 } 64 65 parent.local_name() == &*atom!("foreignObject") 66 } 67 68 // https://drafts.csswg.org/css-display/#unbox 69 #[cfg(feature = "gecko")] 70 fn is_effective_display_none_for_display_contents<E>(element: E) -> bool 71 where 72 E: TElement, 73 { 74 use crate::Atom; 75 76 const SPECIAL_HTML_ELEMENTS: [Atom; 16] = [ 77 atom!("br"), 78 atom!("wbr"), 79 atom!("meter"), 80 atom!("progress"), 81 atom!("canvas"), 82 atom!("embed"), 83 atom!("object"), 84 atom!("audio"), 85 atom!("iframe"), 86 atom!("img"), 87 atom!("video"), 88 atom!("frame"), 89 atom!("frameset"), 90 atom!("input"), 91 atom!("textarea"), 92 atom!("select"), 93 ]; 94 95 // https://drafts.csswg.org/css-display/#unbox-svg 96 // 97 // There's a note about "Unknown elements", but there's not a good way to 98 // know what that means, or to get that information from here, and no other 99 // UA implements this either. 100 const SPECIAL_SVG_ELEMENTS: [Atom; 6] = [ 101 atom!("svg"), 102 atom!("a"), 103 atom!("g"), 104 atom!("use"), 105 atom!("tspan"), 106 atom!("textPath"), 107 ]; 108 109 // https://drafts.csswg.org/css-display/#unbox-html 110 if element.is_html_element() { 111 let local_name = element.local_name(); 112 return SPECIAL_HTML_ELEMENTS 113 .iter() 114 .any(|name| &**name == local_name); 115 } 116 117 // https://drafts.csswg.org/css-display/#unbox-svg 118 if element.is_svg_element() { 119 if is_topmost_svg_svg_element(element) { 120 return true; 121 } 122 let local_name = element.local_name(); 123 return !SPECIAL_SVG_ELEMENTS 124 .iter() 125 .any(|name| &**name == local_name); 126 } 127 128 // https://drafts.csswg.org/css-display/#unbox-mathml 129 if element.is_mathml_element() { 130 return true; 131 } 132 133 false 134 } 135 136 impl<'a, 'b: 'a> StyleAdjuster<'a, 'b> { 137 /// Trivially constructs a new StyleAdjuster. 138 #[inline] 139 pub fn new(style: &'a mut StyleBuilder<'b>) -> Self { 140 StyleAdjuster { style } 141 } 142 143 /// <https://fullscreen.spec.whatwg.org/#new-stacking-layer> 144 /// 145 /// Any position value other than 'absolute' and 'fixed' are 146 /// computed to 'absolute' if the element is in a top layer. 147 /// 148 fn adjust_for_top_layer(&mut self) { 149 if !self.style.in_top_layer() { 150 return; 151 } 152 if !self.style.is_absolutely_positioned() { 153 self.style.mutate_box().set_position(Position::Absolute); 154 } 155 if self.style.get_box().clone_display().is_contents() { 156 self.style.mutate_box().set_display(Display::Block); 157 } 158 } 159 160 /// -webkit-box with line-clamp and vertical orientation gets turned into 161 /// flow-root at computed-value time. 162 /// 163 /// This makes the element not be a flex container, with all that it 164 /// implies, but it should be safe. It matches blink, see 165 /// https://bugzilla.mozilla.org/show_bug.cgi?id=1786147#c10 166 #[cfg(feature = "gecko")] 167 fn adjust_for_webkit_line_clamp(&mut self) { 168 use crate::properties::longhands::_moz_box_orient::computed_value::T as BoxOrient; 169 use crate::values::specified::box_::{DisplayInside, DisplayOutside}; 170 let box_style = self.style.get_box(); 171 if box_style.clone__webkit_line_clamp().is_none() { 172 return; 173 } 174 let disp = box_style.clone_display(); 175 if disp.inside() != DisplayInside::WebkitBox { 176 return; 177 } 178 if self.style.get_xul().clone__moz_box_orient() != BoxOrient::Vertical { 179 return; 180 } 181 let new_display = if disp.outside() == DisplayOutside::Block { 182 Display::FlowRoot 183 } else { 184 debug_assert_eq!(disp.outside(), DisplayOutside::Inline); 185 Display::InlineBlock 186 }; 187 self.style 188 .mutate_box() 189 .set_adjusted_display(new_display, false); 190 } 191 192 /// CSS 2.1 section 9.7: 193 /// 194 /// If 'position' has the value 'absolute' or 'fixed', [...] the computed 195 /// value of 'float' is 'none'. 196 /// 197 fn adjust_for_position(&mut self) { 198 if self.style.is_absolutely_positioned() && self.style.is_floating() { 199 self.style.mutate_box().set_float(Float::None); 200 } 201 } 202 203 /// Whether we should skip any item-based display property blockification on 204 /// this element. 205 fn skip_item_display_fixup<E>(&self, element: Option<E>) -> bool 206 where 207 E: TElement, 208 { 209 if let Some(pseudo) = self.style.pseudo { 210 return pseudo.skip_item_display_fixup(); 211 } 212 213 element.is_some_and(|e| e.skip_item_display_fixup()) 214 } 215 216 /// Apply the blockification rules based on the table in CSS 2.2 section 9.7. 217 /// <https://drafts.csswg.org/css2/visuren.html#dis-pos-flo> 218 /// A ::marker pseudo-element with 'list-style-position:outside' needs to 219 /// have its 'display' blockified, unless the ::marker is for an inline 220 /// list-item (for which 'list-style-position:outside' behaves as 'inside'). 221 /// https://drafts.csswg.org/css-lists-3/#list-style-position-property 222 fn blockify_if_necessary<E>(&mut self, layout_parent_style: &ComputedValues, element: Option<E>) 223 where 224 E: TElement, 225 { 226 let mut blockify = false; 227 macro_rules! blockify_if { 228 ($if_what:expr) => { 229 if !blockify { 230 blockify = $if_what; 231 } 232 }; 233 } 234 235 blockify_if!(self.style.is_root_element); 236 if !self.skip_item_display_fixup(element) { 237 let parent_display = layout_parent_style.get_box().clone_display(); 238 blockify_if!(parent_display.is_item_container()); 239 } 240 241 let is_item_or_root = blockify; 242 243 blockify_if!(self.style.is_floating()); 244 blockify_if!(self.style.is_absolutely_positioned()); 245 246 if !blockify { 247 return; 248 } 249 250 let display = self.style.get_box().clone_display(); 251 let blockified_display = display.equivalent_block_display(self.style.is_root_element); 252 if display != blockified_display { 253 self.style 254 .mutate_box() 255 .set_adjusted_display(blockified_display, is_item_or_root); 256 } 257 } 258 259 /// Compute a few common flags for both text and element's style. 260 fn set_bits(&mut self) { 261 let box_style = self.style.get_box(); 262 let display = box_style.clone_display(); 263 264 if !display.is_contents() { 265 if !self 266 .style 267 .get_text() 268 .clone_text_decoration_line() 269 .is_empty() 270 { 271 self.style 272 .add_flags(ComputedValueFlags::HAS_TEXT_DECORATION_LINES); 273 } 274 275 if self.style.get_effects().clone_opacity() == 0. { 276 self.style 277 .add_flags(ComputedValueFlags::IS_IN_OPACITY_ZERO_SUBTREE); 278 } 279 } else if self 280 .style 281 .get_parent_box() 282 .clone_display() 283 .is_item_container() 284 || self 285 .style 286 .get_parent_flags() 287 .contains(ComputedValueFlags::DIPLAY_CONTENTS_IN_ITEM_CONTAINER) 288 { 289 self.style 290 .add_flags(ComputedValueFlags::DIPLAY_CONTENTS_IN_ITEM_CONTAINER); 291 } 292 293 if self.style.pseudo.is_some_and(|p| p.is_first_line()) { 294 self.style 295 .add_flags(ComputedValueFlags::IS_IN_FIRST_LINE_SUBTREE); 296 } 297 298 if self.style.is_root_element { 299 self.style 300 .add_flags(ComputedValueFlags::IS_ROOT_ELEMENT_STYLE); 301 } 302 303 #[cfg(feature = "gecko")] 304 if box_style 305 .clone_effective_containment() 306 .contains(Contain::STYLE) 307 { 308 self.style 309 .add_flags(ComputedValueFlags::SELF_OR_ANCESTOR_HAS_CONTAIN_STYLE); 310 } 311 312 if box_style.clone_container_type().is_size_container_type() { 313 self.style 314 .add_flags(ComputedValueFlags::SELF_OR_ANCESTOR_HAS_SIZE_CONTAINER_TYPE); 315 } 316 317 #[cfg(feature = "servo")] 318 if self.style.get_parent_column().is_multicol() { 319 self.style.add_flags(ComputedValueFlags::CAN_BE_FRAGMENTED); 320 } 321 } 322 323 /// Adjust the style for text style. 324 /// 325 /// The adjustments here are a subset of the adjustments generally, because 326 /// text only inherits properties. 327 /// 328 /// Note that this, for Gecko, comes through Servo_ComputedValues_Inherit. 329 #[cfg(feature = "gecko")] 330 pub fn adjust_for_text(&mut self) { 331 debug_assert!(!self.style.is_root_element); 332 self.adjust_for_text_combine_upright(); 333 self.adjust_for_text_in_ruby(); 334 self.set_bits(); 335 } 336 337 /// Change writing mode of the text frame for text-combine-upright. 338 /// 339 /// It is safe to look at our own style because we are looking at inherited 340 /// properties, and text is just plain inheritance. 341 /// 342 /// TODO(emilio): we should (Gecko too) revise these adjustments in presence 343 /// of display: contents. 344 /// 345 /// FIXME(emilio): How does this play with logical properties? Doesn't 346 /// mutating writing-mode change the potential physical sides chosen? 347 #[cfg(feature = "gecko")] 348 fn adjust_for_text_combine_upright(&mut self) { 349 use crate::computed_values::text_combine_upright::T as TextCombineUpright; 350 use crate::computed_values::writing_mode::T as WritingMode; 351 use crate::logical_geometry; 352 353 let writing_mode = self.style.get_inherited_box().clone_writing_mode(); 354 let text_combine_upright = self.style.get_inherited_text().clone_text_combine_upright(); 355 356 if matches!( 357 writing_mode, 358 WritingMode::VerticalRl | WritingMode::VerticalLr 359 ) && text_combine_upright == TextCombineUpright::All 360 { 361 self.style.add_flags(ComputedValueFlags::IS_TEXT_COMBINED); 362 self.style 363 .mutate_inherited_box() 364 .set_writing_mode(WritingMode::HorizontalTb); 365 self.style.writing_mode = 366 logical_geometry::WritingMode::new(self.style.get_inherited_box()); 367 } 368 } 369 370 /// Unconditionally propagates the line break suppression flag to text, and 371 /// additionally it applies it if it is in any ruby box. 372 /// 373 /// This is necessary because its parent may not itself have the flag set 374 /// (e.g. ruby or ruby containers), thus we may not inherit the flag from 375 /// them. 376 #[cfg(feature = "gecko")] 377 fn adjust_for_text_in_ruby(&mut self) { 378 let parent_display = self.style.get_parent_box().clone_display(); 379 if parent_display.is_ruby_type() 380 || self 381 .style 382 .get_parent_flags() 383 .contains(ComputedValueFlags::SHOULD_SUPPRESS_LINEBREAK) 384 { 385 self.style 386 .add_flags(ComputedValueFlags::SHOULD_SUPPRESS_LINEBREAK); 387 } 388 } 389 390 /// <https://drafts.csswg.org/css-writing-modes-3/#block-flow:> 391 /// 392 /// If a box has a different writing-mode value than its containing 393 /// block: 394 /// 395 /// - If the box has a specified display of inline, its display 396 /// computes to inline-block. [CSS21] 397 /// 398 /// This matches the adjustment that Gecko does, not exactly following 399 /// the spec. See also: 400 /// 401 /// <https://lists.w3.org/Archives/Public/www-style/2017Mar/0045.html> 402 /// <https://github.com/servo/servo/issues/15754> 403 fn adjust_for_writing_mode(&mut self, layout_parent_style: &ComputedValues) { 404 let our_writing_mode = self.style.get_inherited_box().clone_writing_mode(); 405 let parent_writing_mode = layout_parent_style.get_inherited_box().clone_writing_mode(); 406 407 if our_writing_mode != parent_writing_mode 408 && self.style.get_box().clone_display() == Display::Inline 409 { 410 // TODO(emilio): Figure out if we can just set the adjusted display 411 // on Gecko too and unify this code path. 412 if cfg!(feature = "servo") { 413 self.style 414 .mutate_box() 415 .set_adjusted_display(Display::InlineBlock, false); 416 } else { 417 self.style.mutate_box().set_display(Display::InlineBlock); 418 } 419 } 420 } 421 422 /// CSS overflow-x and overflow-y require some fixup as well in some cases. 423 /// https://drafts.csswg.org/css-overflow-3/#overflow-properties 424 /// "Computed value: as specified, except with `visible`/`clip` computing to 425 /// `auto`/`hidden` (respectively) if one of `overflow-x` or `overflow-y` is 426 /// neither `visible` nor `clip`." 427 fn adjust_for_overflow(&mut self) { 428 let overflow_x = self.style.get_box().clone_overflow_x(); 429 let overflow_y = self.style.get_box().clone_overflow_y(); 430 if overflow_x == overflow_y { 431 return; // optimization for the common case 432 } 433 434 if overflow_x.is_scrollable() != overflow_y.is_scrollable() { 435 let box_style = self.style.mutate_box(); 436 box_style.set_overflow_x(overflow_x.to_scrollable()); 437 box_style.set_overflow_y(overflow_y.to_scrollable()); 438 } 439 } 440 441 #[cfg(feature = "gecko")] 442 fn adjust_for_contain(&mut self) { 443 let box_style = self.style.get_box(); 444 let container_type = box_style.clone_container_type(); 445 let content_visibility = box_style.clone_content_visibility(); 446 if !container_type.is_size_container_type() 447 && content_visibility == ContentVisibility::Visible 448 { 449 debug_assert_eq!( 450 box_style.clone_contain(), 451 box_style.clone_effective_containment() 452 ); 453 return; 454 } 455 let old_contain = box_style.clone_contain(); 456 let mut new_contain = old_contain; 457 match content_visibility { 458 ContentVisibility::Visible => {}, 459 // `content-visibility:auto` also applies size containment when content 460 // is not relevant (and therefore skipped). This is checked in 461 // nsIFrame::GetContainSizeAxes. 462 ContentVisibility::Auto => { 463 new_contain.insert(Contain::LAYOUT | Contain::PAINT | Contain::STYLE) 464 }, 465 ContentVisibility::Hidden => new_contain 466 .insert(Contain::LAYOUT | Contain::PAINT | Contain::SIZE | Contain::STYLE), 467 } 468 if container_type.intersects(ContainerType::INLINE_SIZE) { 469 // https://drafts.csswg.org/css-contain-3/#valdef-container-type-inline-size: 470 // Applies layout containment, style containment, and inline-size 471 // containment to the principal box. 472 new_contain.insert(Contain::STYLE | Contain::INLINE_SIZE); 473 } else if container_type.intersects(ContainerType::SIZE) { 474 // https://drafts.csswg.org/css-contain-3/#valdef-container-type-size: 475 // Applies layout containment, style containment, and size 476 // containment to the principal box. 477 new_contain.insert(Contain::STYLE | Contain::SIZE); 478 } 479 if new_contain == old_contain { 480 debug_assert_eq!( 481 box_style.clone_contain(), 482 box_style.clone_effective_containment() 483 ); 484 return; 485 } 486 self.style 487 .mutate_box() 488 .set_effective_containment(new_contain); 489 } 490 491 /// content-visibility: auto should force contain-intrinsic-size to gain 492 /// an auto value 493 /// 494 /// <https://github.com/w3c/csswg-drafts/issues/8407> 495 #[cfg(feature = "gecko")] 496 fn adjust_for_contain_intrinsic_size(&mut self) { 497 let content_visibility = self.style.get_box().clone_content_visibility(); 498 if content_visibility != ContentVisibility::Auto { 499 return; 500 } 501 502 let pos = self.style.get_position(); 503 let new_width = pos.clone_contain_intrinsic_width().add_auto_if_needed(); 504 let new_height = pos.clone_contain_intrinsic_height().add_auto_if_needed(); 505 if new_width.is_none() && new_height.is_none() { 506 return; 507 } 508 509 let pos = self.style.mutate_position(); 510 if let Some(width) = new_width { 511 pos.set_contain_intrinsic_width(width); 512 } 513 if let Some(height) = new_height { 514 pos.set_contain_intrinsic_height(height); 515 } 516 } 517 518 /// Handles the relevant sections in: 519 /// 520 /// https://drafts.csswg.org/css-display/#unbox-html 521 /// 522 /// And forbidding display: contents in pseudo-elements, at least for now. 523 #[cfg(feature = "gecko")] 524 fn adjust_for_prohibited_display_contents<E>(&mut self, element: Option<E>) 525 where 526 E: TElement, 527 { 528 if self.style.get_box().clone_display() != Display::Contents { 529 return; 530 } 531 532 // FIXME(emilio): ::before and ::after should support display: contents, see bug 1418138. 533 if self.style.pseudo.is_some_and(|p| !p.is_element_backed()) { 534 self.style.mutate_box().set_display(Display::Inline); 535 return; 536 } 537 538 let element = match element { 539 Some(e) => e, 540 None => return, 541 }; 542 543 if is_effective_display_none_for_display_contents(element) { 544 self.style.mutate_box().set_display(Display::None); 545 } 546 } 547 548 /// <textarea>'s editor root needs to inherit the overflow value from its 549 /// parent, but we need to make sure it's still scrollable. 550 #[cfg(feature = "gecko")] 551 fn adjust_for_text_control_editing_root(&mut self) { 552 use crate::properties::longhands::white_space_collapse::computed_value::T as WhiteSpaceCollapse; 553 use crate::selector_parser::PseudoElement; 554 555 if self.style.pseudo != Some(&PseudoElement::MozTextControlEditingRoot) { 556 return; 557 } 558 559 let old_collapse = self.style.get_inherited_text().clone_white_space_collapse(); 560 let new_collapse = match old_collapse { 561 WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces => old_collapse, 562 WhiteSpaceCollapse::Collapse 563 | WhiteSpaceCollapse::PreserveSpaces 564 | WhiteSpaceCollapse::PreserveBreaks => WhiteSpaceCollapse::Preserve, 565 }; 566 if new_collapse != old_collapse { 567 self.style 568 .mutate_inherited_text() 569 .set_white_space_collapse(new_collapse); 570 } 571 572 let box_style = self.style.get_box(); 573 let overflow_x = box_style.clone_overflow_x(); 574 let overflow_y = box_style.clone_overflow_y(); 575 576 // If at least one is scrollable we'll adjust the other one in 577 // adjust_for_overflow if needed. 578 if overflow_x.is_scrollable() || overflow_y.is_scrollable() { 579 return; 580 } 581 582 let box_style = self.style.mutate_box(); 583 box_style.set_overflow_x(Overflow::Auto); 584 box_style.set_overflow_y(Overflow::Auto); 585 } 586 587 /// If a <fieldset> has grid/flex display type, we need to inherit 588 /// this type into its ::-moz-fieldset-content anonymous box. 589 #[cfg(feature = "gecko")] 590 fn adjust_for_fieldset_content(&mut self) { 591 use crate::selector_parser::PseudoElement; 592 if self.style.pseudo != Some(&PseudoElement::FieldsetContent) { 593 return; 594 } 595 let parent_display = self.style.get_parent_box().clone_display(); 596 debug_assert!( 597 !parent_display.is_contents(), 598 "How did we create a fieldset-content box with display: contents?" 599 ); 600 let new_display = match parent_display { 601 Display::Flex | Display::InlineFlex => Some(Display::Flex), 602 Display::Grid | Display::InlineGrid => Some(Display::Grid), 603 _ => None, 604 }; 605 if let Some(new_display) = new_display { 606 self.style.mutate_box().set_display(new_display); 607 } 608 } 609 610 /// -moz-center, -moz-left and -moz-right are used for HTML's alignment. 611 /// 612 /// This is covering the <div align="right"><table>...</table></div> case. 613 /// 614 /// In this case, we don't want to inherit the text alignment into the 615 /// table. 616 fn adjust_for_table_text_align(&mut self) { 617 use crate::properties::longhands::text_align::computed_value::T as TextAlign; 618 if self.style.get_box().clone_display() != Display::Table { 619 return; 620 } 621 622 match self.style.get_inherited_text().clone_text_align() { 623 TextAlign::MozLeft | TextAlign::MozCenter | TextAlign::MozRight => {}, 624 _ => return, 625 } 626 627 self.style 628 .mutate_inherited_text() 629 .set_text_align(TextAlign::Start) 630 } 631 632 #[cfg(feature = "gecko")] 633 fn should_suppress_linebreak<E>(&self, element: Option<E>) -> bool 634 where 635 E: TElement, 636 { 637 // Line break suppression should only be propagated to in-flow children. 638 if self.style.is_floating() || self.style.is_absolutely_positioned() { 639 return false; 640 } 641 let parent_display = self.style.get_parent_box().clone_display(); 642 if self 643 .style 644 .get_parent_flags() 645 .contains(ComputedValueFlags::SHOULD_SUPPRESS_LINEBREAK) 646 { 647 // Line break suppression is propagated to any children of 648 // line participants, and across display: contents boundaries. 649 if parent_display.is_line_participant() || parent_display.is_contents() { 650 return true; 651 } 652 } 653 match self.style.get_box().clone_display() { 654 // Ruby base and text are always non-breakable. 655 Display::RubyBase | Display::RubyText => true, 656 // Ruby base container and text container are breakable. 657 // Non-HTML elements may not form ruby base / text container because 658 // they may not respect ruby-internal display values, so we can't 659 // make them escaped from line break suppression. 660 // Note that, when certain HTML tags, e.g. form controls, have ruby 661 // level container display type, they could also escape from the 662 // line break suppression flag while they shouldn't. However, it is 663 // generally fine as far as they can't break the line inside them. 664 Display::RubyBaseContainer | Display::RubyTextContainer 665 if element.map_or(true, |e| e.is_html_element()) => 666 { 667 false 668 }, 669 // Anything else is non-breakable if and only if its layout parent 670 // has a ruby display type, because any of the ruby boxes can be 671 // anonymous. 672 _ => parent_display.is_ruby_type(), 673 } 674 } 675 676 /// Do ruby-related style adjustments, which include: 677 /// * propagate the line break suppression flag, 678 /// * inlinify block descendants, 679 /// * suppress border and padding for ruby level containers, 680 /// * correct unicode-bidi. 681 #[cfg(feature = "gecko")] 682 fn adjust_for_ruby<E>(&mut self, element: Option<E>) 683 where 684 E: TElement, 685 { 686 use crate::properties::longhands::unicode_bidi::computed_value::T as UnicodeBidi; 687 688 let self_display = self.style.get_box().clone_display(); 689 // Check whether line break should be suppressed for this element. 690 if self.should_suppress_linebreak(element) { 691 self.style 692 .add_flags(ComputedValueFlags::SHOULD_SUPPRESS_LINEBREAK); 693 // Inlinify the display type if allowed. 694 if !self.skip_item_display_fixup(element) { 695 let inline_display = self_display.inlinify(); 696 if self_display != inline_display { 697 self.style 698 .mutate_box() 699 .set_adjusted_display(inline_display, false); 700 } 701 } 702 } 703 // Suppress border and padding for ruby level containers. 704 // This is actually not part of the spec. It is currently unspecified 705 // how border and padding should be handled for ruby level container, 706 // and suppressing them here make it easier for layout to handle. 707 if self_display.is_ruby_level_container() { 708 self.style.reset_border_struct(); 709 self.style.reset_padding_struct(); 710 } 711 712 // Force bidi isolation on all internal ruby boxes and ruby container 713 // per spec https://drafts.csswg.org/css-ruby-1/#bidi 714 if self_display.is_ruby_type() { 715 let new_value = match self.style.get_text().clone_unicode_bidi() { 716 UnicodeBidi::Normal | UnicodeBidi::Embed => Some(UnicodeBidi::Isolate), 717 UnicodeBidi::BidiOverride => Some(UnicodeBidi::IsolateOverride), 718 _ => None, 719 }; 720 if let Some(new_value) = new_value { 721 self.style.mutate_text().set_unicode_bidi(new_value); 722 } 723 } 724 } 725 726 /// Computes the RELEVANT_LINK_VISITED flag based on the parent style and on 727 /// whether we're a relevant link. 728 /// 729 /// NOTE(emilio): We don't do this for text styles, which is... dubious, but 730 /// Gecko doesn't seem to do it either. It's extremely easy to do if needed 731 /// though. 732 /// 733 /// FIXME(emilio): This isn't technically a style adjustment thingie, could 734 /// it move somewhere else? 735 fn adjust_for_visited<E>(&mut self, element: Option<E>) 736 where 737 E: TElement, 738 { 739 if !self.style.has_visited_style() { 740 return; 741 } 742 743 let is_link_element = self.style.pseudo.is_none() && element.map_or(false, |e| e.is_link()); 744 745 if !is_link_element { 746 return; 747 } 748 749 if element.unwrap().is_visited_link() { 750 self.style 751 .add_flags(ComputedValueFlags::IS_RELEVANT_LINK_VISITED); 752 } else { 753 // Need to remove to handle unvisited link inside visited. 754 self.style 755 .remove_flags(ComputedValueFlags::IS_RELEVANT_LINK_VISITED); 756 } 757 } 758 759 /// Resolves "justify-items: legacy" based on the inherited style if needed 760 /// to comply with: 761 /// 762 /// <https://drafts.csswg.org/css-align/#valdef-justify-items-legacy> 763 #[cfg(feature = "gecko")] 764 fn adjust_for_justify_items(&mut self) { 765 use crate::values::specified::align; 766 let justify_items = self.style.get_position().clone_justify_items(); 767 if justify_items.specified != align::JustifyItems::legacy() { 768 return; 769 } 770 771 let parent_justify_items = self.style.get_parent_position().clone_justify_items(); 772 773 if !parent_justify_items.computed.contains(AlignFlags::LEGACY) { 774 return; 775 } 776 777 if parent_justify_items.computed == justify_items.computed { 778 return; 779 } 780 781 self.style 782 .mutate_position() 783 .set_computed_justify_items(parent_justify_items.computed); 784 } 785 786 /// If '-webkit-appearance' is 'menulist' on a <select> element then 787 /// the computed value of 'line-height' is 'normal'. 788 /// 789 /// https://github.com/w3c/csswg-drafts/issues/3257 790 #[cfg(feature = "gecko")] 791 fn adjust_for_appearance<E>(&mut self, element: Option<E>) 792 where 793 E: TElement, 794 { 795 use crate::properties::longhands::appearance::computed_value::T as Appearance; 796 use crate::properties::longhands::line_height::computed_value::T as LineHeight; 797 798 let box_ = self.style.get_box(); 799 let appearance = match box_.clone_appearance() { 800 Appearance::Auto => box_.clone__moz_default_appearance(), 801 a => a, 802 }; 803 804 if appearance == Appearance::Menulist { 805 if self.style.get_font().clone_line_height() == LineHeight::normal() { 806 return; 807 } 808 if self.style.pseudo.is_some() { 809 return; 810 } 811 let is_html_select_element = element.map_or(false, |e| { 812 e.is_html_element() && e.local_name() == &*atom!("select") 813 }); 814 if !is_html_select_element { 815 return; 816 } 817 self.style 818 .mutate_font() 819 .set_line_height(LineHeight::normal()); 820 } 821 } 822 823 /// A legacy ::marker (i.e. no 'content') without an author-specified 'font-family' 824 /// and 'list-style-type:disc|circle|square|disclosure-closed|disclosure-open' 825 /// is assigned 'font-family:-moz-bullet-font'. (This is for <ul><li> etc.) 826 /// We don't want synthesized italic/bold for this font, so turn that off too. 827 /// Likewise for 'letter/word-spacing' -- unless the author specified it then reset 828 /// them to their initial value because traditionally we never added such spacing 829 /// between a legacy bullet and the list item's content, so we keep that behavior 830 /// for web-compat reasons. 831 /// We intentionally don't check 'list-style-image' below since we want it to use 832 /// the same font as its fallback ('list-style-type') in case it fails to load. 833 #[cfg(feature = "gecko")] 834 fn adjust_for_marker_pseudo(&mut self) { 835 use crate::values::computed::counters::Content; 836 use crate::values::computed::font::{FontFamily, FontSynthesis, FontSynthesisStyle}; 837 use crate::values::computed::text::{LetterSpacing, WordSpacing}; 838 839 let is_legacy_marker = self.style.pseudo.map_or(false, |p| p.is_marker()) 840 && self.style.get_list().clone_list_style_type().is_bullet() 841 && self.style.get_counters().clone_content() == Content::Normal; 842 if !is_legacy_marker { 843 return; 844 } 845 let flags = self.style.flags.get(); 846 if !flags.contains(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_FONT_FAMILY) { 847 self.style 848 .mutate_font() 849 .set_font_family(FontFamily::moz_bullet().clone()); 850 851 // FIXME(mats): We can remove this if support for font-synthesis is added to @font-face rules. 852 // Then we can add it to the @font-face rule in html.css instead. 853 // https://github.com/w3c/csswg-drafts/issues/6081 854 if !flags.contains(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_FONT_SYNTHESIS_WEIGHT) { 855 self.style 856 .mutate_font() 857 .set_font_synthesis_weight(FontSynthesis::None); 858 } 859 if !flags.contains(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_FONT_SYNTHESIS_STYLE) { 860 self.style 861 .mutate_font() 862 .set_font_synthesis_style(FontSynthesisStyle::None); 863 } 864 } 865 if !flags.contains(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_LETTER_SPACING) { 866 self.style 867 .mutate_inherited_text() 868 .set_letter_spacing(LetterSpacing::normal()); 869 } 870 if !flags.contains(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_WORD_SPACING) { 871 self.style 872 .mutate_inherited_text() 873 .set_word_spacing(WordSpacing::normal()); 874 } 875 } 876 877 /// Performs adjustments for position-try-fallbacks. The properties that need adjustments here 878 /// are luckily not affected by previous adjustments nor by other computed-value-time effects, 879 /// so we can just perform them here. 880 /// 881 /// NOTE(emilio): If we ever perform the interleaving dance, this could / should probably move 882 /// around to the specific properties' to_computed_value implementations, but that seems 883 /// overkill for now. 884 fn adjust_for_try_tactic(&mut self, tactic: &PositionTryFallbacksTryTactic) { 885 debug_assert!(!tactic.is_empty()); 886 // TODO: This is supposed to use the containing block's WM (bug 1995256). 887 let wm = self.style.writing_mode; 888 // TODO: Flip inset / margin / sizes percentages and anchor lookup sides as necessary. 889 for tactic in tactic.iter() { 890 use PositionTryFallbacksTryTacticKeyword::*; 891 match tactic { 892 FlipBlock => { 893 self.flip_self_alignment(/* block = */ true); 894 self.flip_insets_and_margins(/* horizontal = */ wm.is_vertical()); 895 }, 896 FlipInline => { 897 self.flip_self_alignment(/* block = */ false); 898 self.flip_insets_and_margins(/* horizontal = */ wm.is_horizontal()); 899 }, 900 FlipX => { 901 self.flip_self_alignment(/* block = */ wm.is_vertical()); 902 self.flip_insets_and_margins(/* horizontal = */ true); 903 }, 904 FlipY => { 905 self.flip_self_alignment(/* block = */ wm.is_horizontal()); 906 self.flip_insets_and_margins(/* horizontal = */ false); 907 }, 908 FlipStart => { 909 self.flip_start(); 910 }, 911 } 912 self.apply_position_area_tactic(*tactic); 913 } 914 } 915 916 fn apply_position_area_tactic(&mut self, tactic: PositionTryFallbacksTryTacticKeyword) { 917 let pos = self.style.get_position(); 918 let old = pos.clone_position_area(); 919 let wm = self.style.writing_mode; 920 let new = old.with_tactic(wm, tactic); 921 if new == old { 922 return; 923 } 924 let pos = self.style.mutate_position(); 925 pos.set_position_area(new); 926 } 927 928 // TODO: Could avoid some clones here and below. 929 fn swap_insets(&mut self, a_side: PhysicalSide, b_side: PhysicalSide) { 930 debug_assert_ne!(a_side, b_side); 931 let pos = self.style.mutate_position(); 932 let mut a = pos.get_inset(a_side).clone(); 933 a.try_tactic_adjustment(a_side, b_side); 934 let mut b = pos.get_inset(b_side).clone(); 935 b.try_tactic_adjustment(b_side, a_side); 936 pos.set_inset(a_side, b); 937 pos.set_inset(b_side, a); 938 } 939 940 fn swap_margins(&mut self, a_side: PhysicalSide, b_side: PhysicalSide) { 941 debug_assert_ne!(a_side, b_side); 942 let margin = self.style.get_margin(); 943 let mut a = margin.get_margin(a_side).clone(); 944 a.try_tactic_adjustment(a_side, b_side); 945 let mut b = margin.get_margin(b_side).clone(); 946 b.try_tactic_adjustment(b_side, a_side); 947 let margin = self.style.mutate_margin(); 948 margin.set_margin(a_side, b); 949 margin.set_margin(b_side, a); 950 } 951 952 fn swap_sizes(&mut self, block_start: PhysicalSide, inline_start: PhysicalSide) { 953 let pos = self.style.mutate_position(); 954 let mut min_width = pos.clone_min_width(); 955 min_width.try_tactic_adjustment(inline_start, block_start); 956 let mut max_width = pos.clone_max_width(); 957 max_width.try_tactic_adjustment(inline_start, block_start); 958 let mut width = pos.clone_width(); 959 width.try_tactic_adjustment(inline_start, block_start); 960 961 let mut min_height = pos.clone_min_height(); 962 min_height.try_tactic_adjustment(block_start, inline_start); 963 let mut max_height = pos.clone_max_height(); 964 max_height.try_tactic_adjustment(block_start, inline_start); 965 let mut height = pos.clone_height(); 966 height.try_tactic_adjustment(block_start, inline_start); 967 968 let pos = self.style.mutate_position(); 969 pos.set_width(height); 970 pos.set_height(width); 971 pos.set_max_width(max_height); 972 pos.set_max_height(max_width); 973 pos.set_min_width(min_height); 974 pos.set_min_height(min_width); 975 } 976 977 fn flip_start(&mut self) { 978 let wm = self.style.writing_mode; 979 let bs = wm.block_start_physical_side(); 980 let is = wm.inline_start_physical_side(); 981 let be = wm.block_end_physical_side(); 982 let ie = wm.inline_end_physical_side(); 983 self.swap_sizes(bs, is); 984 self.swap_insets(bs, is); 985 self.swap_insets(ie, be); 986 self.swap_margins(bs, is); 987 self.swap_margins(ie, be); 988 self.flip_alignment_start(); 989 } 990 991 fn flip_insets_and_margins(&mut self, horizontal: bool) { 992 if horizontal { 993 self.swap_insets(PhysicalSide::Left, PhysicalSide::Right); 994 self.swap_margins(PhysicalSide::Left, PhysicalSide::Right); 995 } else { 996 self.swap_insets(PhysicalSide::Top, PhysicalSide::Bottom); 997 self.swap_margins(PhysicalSide::Top, PhysicalSide::Bottom); 998 } 999 } 1000 1001 fn flip_alignment_start(&mut self) { 1002 let pos = self.style.get_position(); 1003 let align = pos.clone_align_self(); 1004 let mut justify = pos.clone_justify_self(); 1005 if align == justify { 1006 return; 1007 } 1008 1009 // Fix-up potential justify-self: {left, right} values which might end up as alignment 1010 // values. 1011 if matches!(justify.value(), AlignFlags::LEFT | AlignFlags::RIGHT) { 1012 let left = justify.value() == AlignFlags::LEFT; 1013 let ltr = self.style.writing_mode.is_bidi_ltr(); 1014 justify = justify.with_value(if left == ltr { 1015 AlignFlags::SELF_START 1016 } else { 1017 AlignFlags::SELF_END 1018 }); 1019 } 1020 1021 let pos = self.style.mutate_position(); 1022 pos.set_align_self(justify); 1023 pos.set_justify_self(align); 1024 } 1025 1026 fn flip_self_alignment(&mut self, block: bool) { 1027 let pos = self.style.get_position(); 1028 let cur = if block { 1029 pos.clone_align_self() 1030 } else { 1031 pos.clone_justify_self() 1032 }; 1033 let flipped = cur.flip_position(); 1034 if flipped == cur { 1035 return; 1036 } 1037 let pos = self.style.mutate_position(); 1038 if block { 1039 pos.set_align_self(flipped); 1040 } else { 1041 pos.set_justify_self(flipped); 1042 } 1043 } 1044 1045 /// Adjusts the style to account for various fixups that don't fit naturally into the cascade. 1046 pub fn adjust<E>( 1047 &mut self, 1048 layout_parent_style: &ComputedValues, 1049 element: Option<E>, 1050 try_tactic: &PositionTryFallbacksTryTactic, 1051 ) where 1052 E: TElement, 1053 { 1054 if cfg!(debug_assertions) { 1055 if let Some(e) = element { 1056 if let Some(p) = e.implemented_pseudo_element() { 1057 // It'd be nice to assert `self.style.pseudo == Some(&pseudo)`, 1058 // but we do resolve ::-moz-list pseudos on ::before / ::after 1059 // content, sigh. 1060 debug_assert!( 1061 self.style.pseudo.is_some(), 1062 "Someone really messed up (no pseudo style for {e:?}, {p:?})" 1063 ); 1064 } 1065 } 1066 } 1067 // FIXME(emilio): The apply_declarations callsite in Servo's 1068 // animation, and the font stuff for Gecko 1069 // (Stylist::compute_for_declarations) should pass an element to 1070 // cascade(), then we can make this assertion hold everywhere. 1071 // debug_assert!( 1072 // element.is_some() || self.style.pseudo.is_some(), 1073 // "Should always have an element around for non-pseudo styles" 1074 // ); 1075 1076 self.adjust_for_visited(element); 1077 #[cfg(feature = "gecko")] 1078 { 1079 self.adjust_for_prohibited_display_contents(element); 1080 self.adjust_for_fieldset_content(); 1081 // NOTE: It's important that this happens before 1082 // adjust_for_overflow. 1083 self.adjust_for_text_control_editing_root(); 1084 } 1085 self.adjust_for_top_layer(); 1086 self.blockify_if_necessary(layout_parent_style, element); 1087 #[cfg(feature = "gecko")] 1088 self.adjust_for_webkit_line_clamp(); 1089 self.adjust_for_position(); 1090 self.adjust_for_overflow(); 1091 #[cfg(feature = "gecko")] 1092 { 1093 self.adjust_for_contain(); 1094 self.adjust_for_contain_intrinsic_size(); 1095 self.adjust_for_justify_items(); 1096 } 1097 self.adjust_for_table_text_align(); 1098 self.adjust_for_writing_mode(layout_parent_style); 1099 #[cfg(feature = "gecko")] 1100 { 1101 self.adjust_for_ruby(element); 1102 self.adjust_for_appearance(element); 1103 self.adjust_for_marker_pseudo(); 1104 } 1105 if !try_tactic.is_empty() { 1106 self.adjust_for_try_tactic(try_tactic); 1107 } 1108 self.set_bits(); 1109 } 1110 }