diff --git a/Libraries/LibWeb/CSS/Rust/build.rs b/Libraries/LibWeb/CSS/Rust/build.rs index 039574b1b6e14..eac97a573d792 100644 --- a/Libraries/LibWeb/CSS/Rust/build.rs +++ b/Libraries/LibWeb/CSS/Rust/build.rs @@ -579,7 +579,10 @@ fn main() -> Result<(), Box> { generate_ffi_header( selector_config, - &[manifest_dir.join("src/selector_engine.rs")], + &[ + manifest_dir.join("src/selector_engine.rs"), + manifest_dir.join("src/ffi_support.rs"), + ], &out_dir, &ffi_out_dir, Path::new("SelectorRustFFI.h"), @@ -625,6 +628,7 @@ fn main() -> Result<(), Box> { manifest_dir.join("src/computed_values.rs"), manifest_dir.join("src/property_metadata.rs"), manifest_dir.join("src/style_compute.rs"), + manifest_dir.join("src/display.rs"), manifest_dir.join("src/css_pixels.rs"), manifest_dir.join("src/cascaded_properties.rs"), manifest_dir.join("src/custom_properties.rs"), diff --git a/Libraries/LibWeb/CSS/Rust/src/css_enums.rs b/Libraries/LibWeb/CSS/Rust/src/css_enums.rs new file mode 100644 index 0000000000000..e9e82b2887d88 --- /dev/null +++ b/Libraries/LibWeb/CSS/Rust/src/css_enums.rs @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +//! Generated CSS keyword and enum vocabulary, shared by the crate's modules. The constants are +//! generated by build.rs from the same JSON files as the C++ enums, so the numeric values cannot +//! drift between the two sides. + +include!(concat!(env!("OUT_DIR"), "/keywords_generated.rs")); +include!(concat!(env!("OUT_DIR"), "/css_enums_generated.rs")); diff --git a/Libraries/LibWeb/CSS/Rust/src/display.rs b/Libraries/LibWeb/CSS/Rust/src/display.rs new file mode 100644 index 0000000000000..cdcf0616484df --- /dev/null +++ b/Libraries/LibWeb/CSS/Rust/src/display.rs @@ -0,0 +1,341 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +//! The CSS `display` value and its predicates, mirroring `Web::CSS::Display`. + +use crate::css_enums::{display_box, display_inside, display_internal, display_outside}; + +/// Mirror of the CSS Display value; the C++ tagged union crosses as explicit +/// fields, with the unused fields zeroed so equality is field-wise. `tag` uses +/// the same discriminants as Display::Type. +#[repr(C)] +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct FfiDisplay { + /// 0 = outside-and-inside, 1 = internal, 2 = box. + pub tag: u8, + pub outside: u8, + pub inside: u8, + pub list_item: bool, + pub internal: u8, + pub box_value: u8, +} + +pub const DISPLAY_TAG_OUTSIDE_AND_INSIDE: u8 = 0; +pub const DISPLAY_TAG_INTERNAL: u8 = 1; +pub const DISPLAY_TAG_BOX: u8 = 2; + +impl FfiDisplay { + pub fn from_raw(raw: u32) -> Self { + let [tag, first, second, third] = raw.to_le_bytes(); + match tag { + DISPLAY_TAG_OUTSIDE_AND_INSIDE => Self::outside_and_inside(first, second, third != 0), + DISPLAY_TAG_INTERNAL => Self::internal(first), + DISPLAY_TAG_BOX => Self { + tag, + outside: 0, + inside: 0, + list_item: false, + internal: 0, + box_value: first, + }, + _ => unreachable!("invalid display tag"), + } + } + + pub fn outside_and_inside(outside: u8, inside: u8, list_item: bool) -> Self { + Self { + tag: DISPLAY_TAG_OUTSIDE_AND_INSIDE, + outside, + inside, + list_item, + internal: 0, + box_value: 0, + } + } + + pub fn internal(internal: u8) -> Self { + Self { + tag: DISPLAY_TAG_INTERNAL, + outside: 0, + inside: 0, + list_item: false, + internal, + box_value: 0, + } + } + + pub fn block() -> Self { + Self::outside_and_inside(display_outside::BLOCK, display_inside::FLOW, false) + } + + pub fn inline() -> Self { + Self::outside_and_inside(display_outside::INLINE, display_inside::FLOW, false) + } + + pub fn inline_block() -> Self { + Self::outside_and_inside(display_outside::INLINE, display_inside::FLOW_ROOT, false) + } + + pub fn flow_root() -> Self { + Self::outside_and_inside(display_outside::BLOCK, display_inside::FLOW_ROOT, false) + } + + pub fn table() -> Self { + Self::outside_and_inside(display_outside::BLOCK, display_inside::TABLE, false) + } + + pub fn inline_table() -> Self { + Self::outside_and_inside(display_outside::INLINE, display_inside::TABLE, false) + } + + pub fn encoded(&self) -> u32 { + let (first, second, third) = match self.tag { + DISPLAY_TAG_OUTSIDE_AND_INSIDE => (self.outside, self.inside, self.list_item as u8), + DISPLAY_TAG_INTERNAL => (self.internal, 0, 0), + DISPLAY_TAG_BOX => (self.box_value, 0, 0), + _ => unreachable!("invalid display tag"), + }; + self.tag as u32 | (first as u32) << 8 | (second as u32) << 16 | (third as u32) << 24 + } + + pub fn none() -> Self { + Self { + tag: DISPLAY_TAG_BOX, + outside: 0, + inside: 0, + list_item: false, + internal: 0, + box_value: display_box::NONE, + } + } + + pub fn contents() -> Self { + Self { + tag: DISPLAY_TAG_BOX, + outside: 0, + inside: 0, + list_item: false, + internal: 0, + box_value: display_box::CONTENTS, + } + } + + pub fn is_outside_and_inside(&self) -> bool { + self.tag == DISPLAY_TAG_OUTSIDE_AND_INSIDE + } + + pub fn is_internal(&self) -> bool { + self.tag == DISPLAY_TAG_INTERNAL + } + + pub fn is_none(&self) -> bool { + self.tag == DISPLAY_TAG_BOX && self.box_value == display_box::NONE + } + + pub fn is_contents(&self) -> bool { + self.tag == DISPLAY_TAG_BOX && self.box_value == display_box::CONTENTS + } + + pub fn is_block_outside(&self) -> bool { + self.is_outside_and_inside() && self.outside == display_outside::BLOCK + } + + pub fn is_inline_outside(&self) -> bool { + self.is_outside_and_inside() && self.outside == display_outside::INLINE + } + + pub fn is_inline_block(&self) -> bool { + self.is_inline_outside() && self.is_flow_root_inside() + } + + pub fn is_list_item(&self) -> bool { + self.is_outside_and_inside() && self.list_item + } + + pub fn is_flow_inside(&self) -> bool { + self.is_outside_and_inside() && self.inside == display_inside::FLOW + } + + pub fn is_flow_root_inside(&self) -> bool { + self.is_outside_and_inside() && self.inside == display_inside::FLOW_ROOT + } + + pub fn is_table_inside(&self) -> bool { + self.is_outside_and_inside() && self.inside == display_inside::TABLE + } + + pub fn is_flex_inside(&self) -> bool { + self.is_outside_and_inside() && self.inside == display_inside::FLEX + } + + pub fn is_grid_inside(&self) -> bool { + self.is_outside_and_inside() && self.inside == display_inside::GRID + } + + pub fn is_ruby_inside(&self) -> bool { + self.is_outside_and_inside() && self.inside == display_inside::RUBY + } + + pub fn is_math_inside(&self) -> bool { + self.is_outside_and_inside() && self.inside == display_inside::MATH + } + + pub fn is_table_row(&self) -> bool { + self.is_internal() && self.internal == display_internal::TABLE_ROW + } + + pub fn is_table_cell(&self) -> bool { + self.is_internal() && self.internal == display_internal::TABLE_CELL + } + + pub fn is_table_column(&self) -> bool { + self.is_internal() && self.internal == display_internal::TABLE_COLUMN + } + + pub fn is_table_column_group(&self) -> bool { + self.is_internal() && self.internal == display_internal::TABLE_COLUMN_GROUP + } + + pub fn is_table_row_group(&self) -> bool { + self.is_internal() && self.internal == display_internal::TABLE_ROW_GROUP + } + + pub fn is_table_header_group(&self) -> bool { + self.is_internal() && self.internal == display_internal::TABLE_HEADER_GROUP + } + + pub fn is_table_footer_group(&self) -> bool { + self.is_internal() && self.internal == display_internal::TABLE_FOOTER_GROUP + } + + pub fn is_table_caption(&self) -> bool { + self.is_internal() && self.internal == display_internal::TABLE_CAPTION + } + + // https://drafts.csswg.org/css-display-3/#internal-table-element + pub fn is_internal_table(&self) -> bool { + self.is_internal() + && (self.internal == display_internal::TABLE_ROW_GROUP + || self.internal == display_internal::TABLE_HEADER_GROUP + || self.internal == display_internal::TABLE_FOOTER_GROUP + || self.internal == display_internal::TABLE_ROW + || self.internal == display_internal::TABLE_CELL + || self.internal == display_internal::TABLE_COLUMN_GROUP + || self.internal == display_internal::TABLE_COLUMN) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn display_raw_value_decodes_for_box_type_transformation() { + let inline_flex = FfiDisplay::from_raw(u32::from_ne_bytes([ + DISPLAY_TAG_OUTSIDE_AND_INSIDE, + display_outside::INLINE, + display_inside::FLEX, + 0, + ])); + assert!(inline_flex.is_inline_outside()); + assert!(inline_flex.is_flex_inside()); + + let none = FfiDisplay::from_raw(u32::from_ne_bytes([DISPLAY_TAG_BOX, display_box::NONE, 0, 0])); + assert!(none.is_none()); + } + + #[test] + fn display_encodes_for_adjustment_store() { + let list_item = FfiDisplay::outside_and_inside(display_outside::BLOCK, display_inside::FLOW, true); + assert_eq!( + list_item.encoded(), + DISPLAY_TAG_OUTSIDE_AND_INSIDE as u32 + | (display_outside::BLOCK as u32) << 8 + | (display_inside::FLOW as u32) << 16 + | 1 << 24 + ); + + assert_eq!( + FfiDisplay::internal(display_internal::TABLE_ROW).encoded(), + DISPLAY_TAG_INTERNAL as u32 | (display_internal::TABLE_ROW as u32) << 8 + ); + assert_eq!( + FfiDisplay::none().encoded(), + DISPLAY_TAG_BOX as u32 | (display_box::NONE as u32) << 8 + ); + } + + #[test] + fn encoding_round_trips() { + let values = [ + FfiDisplay::block(), + FfiDisplay::inline(), + FfiDisplay::inline_block(), + FfiDisplay::flow_root(), + FfiDisplay::table(), + FfiDisplay::inline_table(), + FfiDisplay::none(), + FfiDisplay::contents(), + FfiDisplay::outside_and_inside(display_outside::BLOCK, display_inside::FLOW, true), + FfiDisplay::internal(display_internal::TABLE_CELL), + ]; + for value in values { + assert!(FfiDisplay::from_raw(value.encoded()) == value); + } + } + + #[test] + fn internal_table_predicates_match_each_internal_value() { + let row = FfiDisplay::internal(display_internal::TABLE_ROW); + assert!(row.is_table_row() && row.is_internal_table() && !row.is_table_cell()); + + let cell = FfiDisplay::internal(display_internal::TABLE_CELL); + assert!(cell.is_table_cell() && cell.is_internal_table()); + + let column = FfiDisplay::internal(display_internal::TABLE_COLUMN); + assert!(column.is_table_column() && column.is_internal_table()); + + let column_group = FfiDisplay::internal(display_internal::TABLE_COLUMN_GROUP); + assert!(column_group.is_table_column_group() && column_group.is_internal_table()); + + let row_group = FfiDisplay::internal(display_internal::TABLE_ROW_GROUP); + assert!(row_group.is_table_row_group() && row_group.is_internal_table()); + + let header_group = FfiDisplay::internal(display_internal::TABLE_HEADER_GROUP); + assert!(header_group.is_table_header_group() && header_group.is_internal_table()); + + let footer_group = FfiDisplay::internal(display_internal::TABLE_FOOTER_GROUP); + assert!(footer_group.is_table_footer_group() && footer_group.is_internal_table()); + + // table-caption is internal but not an internal table element. + let caption = FfiDisplay::internal(display_internal::TABLE_CAPTION); + assert!(caption.is_table_caption() && !caption.is_internal_table()); + } + + #[test] + fn outside_and_inside_predicates() { + let table = FfiDisplay::table(); + assert!(table.is_table_inside() && table.is_block_outside() && !table.is_internal()); + + let inline_table = FfiDisplay::inline_table(); + assert!(inline_table.is_table_inside() && inline_table.is_inline_outside()); + + let list_item = FfiDisplay::outside_and_inside(display_outside::BLOCK, display_inside::FLOW, true); + assert!(list_item.is_list_item() && list_item.is_flow_inside()); + assert!(!FfiDisplay::block().is_list_item()); + + assert!(FfiDisplay::block().is_flow_inside()); + assert!(FfiDisplay::flow_root().is_flow_root_inside()); + assert!(FfiDisplay::inline_block().is_inline_block()); + + let ruby = FfiDisplay::outside_and_inside(display_outside::INLINE, display_inside::RUBY, false); + assert!(ruby.is_ruby_inside()); + + // Box-tag values answer no outside/inside predicate. + assert!(!FfiDisplay::none().is_flow_inside()); + assert!(!FfiDisplay::none().is_block_outside()); + } +} diff --git a/Libraries/LibWeb/CSS/Rust/src/ffi_support.rs b/Libraries/LibWeb/CSS/Rust/src/ffi_support.rs new file mode 100644 index 0000000000000..816459c1ce005 --- /dev/null +++ b/Libraries/LibWeb/CSS/Rust/src/ffi_support.rs @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +//! Shared plumbing for the C++/Rust FFI boundary: retained C++ object pointers, the call-scope +//! lifetime marker that pins borrowed DOM data, and borrowed DOM string views. + +use std::ffi::c_void; +use std::marker::PhantomData; +use std::ptr::NonNull; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub(crate) struct RetainedCxxPointer(Option>); + +impl RetainedCxxPointer { + pub(crate) fn new(pointer: *const c_void) -> Self { + Self(NonNull::new(pointer.cast_mut())) + } + + pub(crate) fn as_ptr(self) -> *const c_void { + self.0.map_or(std::ptr::null(), |pointer| pointer.as_ptr().cast_const()) + } +} + +/// Zero-sized marker whose borrow scopes every pointer C++ lends to Rust for the duration of one +/// synchronous FFI call. Types holding `PhantomData<&'a FfiCallScope>` cannot outlive the call. +pub(crate) struct FfiCallScope; + +#[derive(Clone, Copy)] +#[repr(C)] +pub struct FfiDomStringView { + pub data: *const c_void, + pub length: usize, + pub is_ascii: bool, +} + +#[derive(Clone, Copy)] +pub(crate) struct DomStringView<'a> { + view: FfiDomStringView, + marker: PhantomData<&'a FfiCallScope>, +} + +impl DomStringView<'_> { + /// The caller vouches that `view` borrows storage which stays valid for the current FFI call; + /// the lifetime parameter ties the wrapper to that call scope. + pub(crate) fn new(view: FfiDomStringView) -> Self { + Self { + view, + marker: PhantomData, + } + } + + pub(crate) fn len(self) -> usize { + self.view.length + } + + pub(crate) fn code_unit_at(self, index: usize) -> u16 { + assert!(index < self.len()); + assert!(!self.view.data.is_null()); + if self.view.is_ascii { + // SAFETY: C++ guarantees that an ASCII view points at `length` bytes. + return u16::from(unsafe { *(self.view.data.cast::().add(index)) }); + } + // SAFETY: C++ guarantees that a UTF-16 view points at `length` aligned code units. + unsafe { *(self.view.data.cast::().add(index)) } + } +} + +pub(crate) fn ascii_lowercase(code_unit: u16) -> u16 { + if (u16::from(b'A')..=u16::from(b'Z')).contains(&code_unit) { + code_unit + u16::from(b'a' - b'A') + } else { + code_unit + } +} + +pub(crate) fn utf16_equals_ignoring_ascii_case(first: DomStringView<'_>, second: &[u16]) -> bool { + first.len() == second.len() + && second + .iter() + .enumerate() + .all(|(index, &second)| ascii_lowercase(first.code_unit_at(index)) == ascii_lowercase(second)) +} + +pub(crate) fn utf16_equals(first: DomStringView<'_>, second: &[u16]) -> bool { + first.len() == second.len() + && second + .iter() + .enumerate() + .all(|(index, &second)| first.code_unit_at(index) == second) +} diff --git a/Libraries/LibWeb/CSS/Rust/src/lib.rs b/Libraries/LibWeb/CSS/Rust/src/lib.rs index 87e41152ebfd7..44306027de2d0 100644 --- a/Libraries/LibWeb/CSS/Rust/src/lib.rs +++ b/Libraries/LibWeb/CSS/Rust/src/lib.rs @@ -11,10 +11,13 @@ mod rust_allocator; pub mod calc; pub mod cascaded_properties; pub mod computed_values; +pub mod css_enums; pub mod css_pixels; mod css_tokenizer; pub mod custom_properties; +pub mod display; pub mod ffi_stats; +pub mod ffi_support; pub mod property_metadata; mod selector_engine; pub mod style_compute; diff --git a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs index a39718494f8dd..246ba5e1811b6 100644 --- a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs +++ b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs @@ -8,11 +8,14 @@ use std::ffi::c_void; use std::marker::PhantomData; use std::ops::Deref; use std::ops::DerefMut; -use std::ptr::NonNull; use std::rc::Rc; use std::sync::atomic::{AtomicU64, Ordering}; use crate::abort_on_panic; +use crate::ffi_support::{ + DomStringView, FfiCallScope, FfiDomStringView, RetainedCxxPointer, ascii_lowercase, utf16_equals, + utf16_equals_ignoring_ascii_case, +}; static NEXT_SELECTOR_ID: AtomicU64 = AtomicU64::new(1); @@ -21,19 +24,6 @@ pub type SelectorString = Box<[u16]>; /// functional pseudo-classes to share those selectors with the C++ selector tree that owns them. pub type SelectorList = Box<[Rc]>; -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -struct RetainedCxxPointer(Option>); - -impl RetainedCxxPointer { - fn new(pointer: *const c_void) -> Self { - Self(NonNull::new(pointer.cast_mut())) - } - - fn as_ptr(self) -> *const c_void { - self.0.map_or(std::ptr::null(), |pointer| pointer.as_ptr().cast_const()) - } -} - #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(u8)] // NB: Some variants are only constructed by C++ through the FFI. @@ -1618,11 +1608,8 @@ impl FfiElement { crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); // SAFETY: The handle identifies a live DOM element. C++ returns its current ID with // backing storage owned by the element for this matching call. - DomStringView { - // SAFETY: C++ guarantees that the returned string view remains valid for matching. - view: unsafe { selector_ffi_element_id_value(self.pointer) }, - marker: PhantomData, - } + // SAFETY: C++ guarantees that the returned string view remains valid for matching. + DomStringView::new(unsafe { selector_ffi_element_id_value(self.pointer) }) } fn id_and_class_names_are_case_insensitive(self) -> bool { @@ -1704,10 +1691,7 @@ impl FfiElement { // SAFETY: The handle identifies a live DOM element. C++ returns its current language with // backing storage owned by the element for this matching call. let view = unsafe { selector_ffi_element_language(self.pointer) }; - (view.length != 0).then_some(DomStringView { - view, - marker: PhantomData, - }) + (view.length != 0).then_some(DomStringView::new(view)) } fn is_focused(self) -> bool { @@ -1894,22 +1878,16 @@ impl FfiElement { crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); // SAFETY: The handle identifies a live DOM element. C++ returns a string view borrowed // from its current local name for this matching call. - DomStringView { - // SAFETY: C++ guarantees that the returned string view remains valid for matching. - view: unsafe { selector_ffi_element_local_name(self.pointer) }, - marker: PhantomData, - } + // SAFETY: C++ guarantees that the returned string view remains valid for matching. + DomStringView::new(unsafe { selector_ffi_element_local_name(self.pointer) }) } unsafe fn class_name<'a>(self, index: usize) -> DomStringView<'a> { crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); // SAFETY: The handle identifies a live DOM element and `index` identifies one of its // current classes. C++ returns a view borrowed for this matching call. - DomStringView { - // SAFETY: The caller guarantees that `index` is within the current class list. - view: unsafe { selector_ffi_element_class_name(self.pointer, index) }, - marker: PhantomData, - } + // SAFETY: The caller guarantees that `index` is within the current class list. + DomStringView::new(unsafe { selector_ffi_element_class_name(self.pointer, index) }) } fn attribute_count(self) -> usize { @@ -1970,14 +1948,6 @@ pub struct FfiInternedStringList { pub count: usize, } -#[derive(Clone, Copy)] -#[repr(C)] -pub struct FfiDomStringView { - pub data: *const c_void, - pub length: usize, - pub is_ascii: bool, -} - #[derive(Clone, Copy)] #[repr(C)] pub struct FfiDomAttribute { @@ -2003,33 +1973,7 @@ impl<'a> DomAttribute<'a> { } fn value(self) -> DomStringView<'a> { - DomStringView { - view: self.attribute.value, - marker: PhantomData, - } - } -} - -#[derive(Clone, Copy)] -struct DomStringView<'a> { - view: FfiDomStringView, - marker: PhantomData<&'a FfiCallScope>, -} - -impl DomStringView<'_> { - fn len(self) -> usize { - self.view.length - } - - fn code_unit_at(self, index: usize) -> u16 { - assert!(index < self.len()); - assert!(!self.view.data.is_null()); - if self.view.is_ascii { - // SAFETY: C++ guarantees that an ASCII view points at `length` bytes. - return u16::from(unsafe { *(self.view.data.cast::().add(index)) }); - } - // SAFETY: C++ guarantees that a UTF-16 view points at `length` aligned code units. - unsafe { *(self.view.data.cast::().add(index)) } + DomStringView::new(self.attribute.value) } } @@ -2240,30 +2184,6 @@ fn ffi_string_view(string: &[u16]) -> FfiStringView { } } -fn ascii_lowercase(code_unit: u16) -> u16 { - if (u16::from(b'A')..=u16::from(b'Z')).contains(&code_unit) { - code_unit + u16::from(b'a' - b'A') - } else { - code_unit - } -} - -fn utf16_equals_ignoring_ascii_case(first: DomStringView<'_>, second: &[u16]) -> bool { - first.len() == second.len() - && second - .iter() - .enumerate() - .all(|(index, &second)| ascii_lowercase(first.code_unit_at(index)) == ascii_lowercase(second)) -} - -fn utf16_equals(first: DomStringView<'_>, second: &[u16]) -> bool { - first.len() == second.len() - && second - .iter() - .enumerate() - .all(|(index, &second)| first.code_unit_at(index) == second) -} - struct SelectorSubtags<'a> { value: &'a [u16], position: usize, @@ -2577,8 +2497,6 @@ fn is_ascii_case_insensitive_html_attribute(name: &[u16]) -> bool { NAMES.iter().any(|candidate| utf16_equals_ascii(name, candidate)) } -struct FfiCallScope; - struct FfiDom<'a> { context: *mut c_void, marker: PhantomData<&'a mut FfiCallScope>, diff --git a/Libraries/LibWeb/CSS/Rust/src/style_compute.rs b/Libraries/LibWeb/CSS/Rust/src/style_compute.rs index d5fc1ab3074d3..06f6d51222036 100644 --- a/Libraries/LibWeb/CSS/Rust/src/style_compute.rs +++ b/Libraries/LibWeb/CSS/Rust/src/style_compute.rs @@ -21,14 +21,15 @@ use std::sync::OnceLock; use crate::abort_on_panic; use crate::cascaded_properties::CascadedPropertyStore; use crate::css_pixels::CssPixels; +use crate::display::FfiDisplay; use crate::property_metadata::longhands_for_shorthand; use crate::property_metadata::property_is_inherited; use crate::property_metadata::property_is_shorthand; use crate::style_value::StyleValueData; +pub use crate::css_enums::*; + include!(concat!(env!("OUT_DIR"), "/length_units_generated.rs")); -include!(concat!(env!("OUT_DIR"), "/keywords_generated.rs")); -include!(concat!(env!("OUT_DIR"), "/css_enums_generated.rs")); /// The font metrics needed for font-relative length resolution, as unrounded /// CSS pixel values. @@ -3051,143 +3052,6 @@ pub unsafe extern "C" fn rust_for_each_property_expanding_shorthands( abort_on_panic(|| expand_shorthands(unsafe { &*callbacks }, property_id, shell, data)); } -/// Mirror of the CSS Display value; the C++ tagged union crosses as explicit -/// fields, with the unused fields zeroed so equality is field-wise. `tag` uses -/// the same discriminants as Display::Type. -#[repr(C)] -#[derive(Clone, Copy, PartialEq, Eq)] -pub struct FfiDisplay { - /// 0 = outside-and-inside, 1 = internal, 2 = box. - pub tag: u8, - pub outside: u8, - pub inside: u8, - pub list_item: bool, - pub internal: u8, - pub box_value: u8, -} - -const DISPLAY_TAG_OUTSIDE_AND_INSIDE: u8 = 0; -const DISPLAY_TAG_INTERNAL: u8 = 1; -const DISPLAY_TAG_BOX: u8 = 2; - -impl FfiDisplay { - fn from_raw(raw: u32) -> Self { - let [tag, first, second, third] = raw.to_ne_bytes(); - match tag { - DISPLAY_TAG_OUTSIDE_AND_INSIDE => Self::outside_and_inside(first, second, third != 0), - DISPLAY_TAG_INTERNAL => Self::internal(first), - DISPLAY_TAG_BOX => Self { - tag, - outside: 0, - inside: 0, - list_item: false, - internal: 0, - box_value: first, - }, - _ => unreachable!("invalid display tag"), - } - } - - fn outside_and_inside(outside: u8, inside: u8, list_item: bool) -> Self { - Self { - tag: DISPLAY_TAG_OUTSIDE_AND_INSIDE, - outside, - inside, - list_item, - internal: 0, - box_value: 0, - } - } - - fn internal(internal: u8) -> Self { - Self { - tag: DISPLAY_TAG_INTERNAL, - outside: 0, - inside: 0, - list_item: false, - internal, - box_value: 0, - } - } - - fn block() -> Self { - Self::outside_and_inside(display_outside::BLOCK, display_inside::FLOW, false) - } - - fn inline() -> Self { - Self::outside_and_inside(display_outside::INLINE, display_inside::FLOW, false) - } - - fn inline_block() -> Self { - Self::outside_and_inside(display_outside::INLINE, display_inside::FLOW_ROOT, false) - } - - fn flow_root() -> Self { - Self::outside_and_inside(display_outside::BLOCK, display_inside::FLOW_ROOT, false) - } - - fn encoded(&self) -> u32 { - let (first, second, third) = match self.tag { - DISPLAY_TAG_OUTSIDE_AND_INSIDE => (self.outside, self.inside, self.list_item as u8), - DISPLAY_TAG_INTERNAL => (self.internal, 0, 0), - DISPLAY_TAG_BOX => (self.box_value, 0, 0), - _ => unreachable!("invalid display tag"), - }; - self.tag as u32 | (first as u32) << 8 | (second as u32) << 16 | (third as u32) << 24 - } - - fn none() -> Self { - Self { - tag: DISPLAY_TAG_BOX, - outside: 0, - inside: 0, - list_item: false, - internal: 0, - box_value: display_box::NONE, - } - } - - fn is_outside_and_inside(&self) -> bool { - self.tag == DISPLAY_TAG_OUTSIDE_AND_INSIDE - } - - fn is_internal(&self) -> bool { - self.tag == DISPLAY_TAG_INTERNAL - } - - fn is_none(&self) -> bool { - self.tag == DISPLAY_TAG_BOX && self.box_value == display_box::NONE - } - - fn is_contents(&self) -> bool { - self.tag == DISPLAY_TAG_BOX && self.box_value == display_box::CONTENTS - } - - fn is_block_outside(&self) -> bool { - self.is_outside_and_inside() && self.outside == display_outside::BLOCK - } - - fn is_inline_outside(&self) -> bool { - self.is_outside_and_inside() && self.outside == display_outside::INLINE - } - - fn is_math_inside(&self) -> bool { - self.is_outside_and_inside() && self.inside == display_inside::MATH - } - - fn is_inline_block(&self) -> bool { - self.is_inline_outside() && self.inside == display_inside::FLOW_ROOT - } - - fn is_grid_inside(&self) -> bool { - self.is_outside_and_inside() && self.inside == display_inside::GRID - } - - fn is_flex_inside(&self) -> bool { - self.is_outside_and_inside() && self.inside == display_inside::FLEX - } -} - /// The element facts the box type transformation needs, marshalled by the C++ /// side. The parent display is the first non-`display: contents` ancestor's. #[repr(C)] @@ -3735,42 +3599,6 @@ mod tests { assert!(!result.inherited); } - #[test] - fn display_raw_value_decodes_for_box_type_transformation() { - let inline_flex = FfiDisplay::from_raw(u32::from_ne_bytes([ - DISPLAY_TAG_OUTSIDE_AND_INSIDE, - display_outside::INLINE, - display_inside::FLEX, - 0, - ])); - assert!(inline_flex.is_inline_outside()); - assert!(inline_flex.is_flex_inside()); - - let none = FfiDisplay::from_raw(u32::from_ne_bytes([DISPLAY_TAG_BOX, display_box::NONE, 0, 0])); - assert!(none.is_none()); - } - - #[test] - fn display_encodes_for_adjustment_store() { - let list_item = FfiDisplay::outside_and_inside(display_outside::BLOCK, display_inside::FLOW, true); - assert_eq!( - list_item.encoded(), - DISPLAY_TAG_OUTSIDE_AND_INSIDE as u32 - | (display_outside::BLOCK as u32) << 8 - | (display_inside::FLOW as u32) << 16 - | 1 << 24 - ); - - assert_eq!( - FfiDisplay::internal(display_internal::TABLE_ROW).encoded(), - DISPLAY_TAG_INTERNAL as u32 | (display_internal::TABLE_ROW as u32) << 8 - ); - assert_eq!( - FfiDisplay::none().encoded(), - DISPLAY_TAG_BOX as u32 | (display_box::NONE as u32) << 8 - ); - } - fn element_adjustment_input() -> FfiBoxTypeTransformationInput { FfiBoxTypeTransformationInput { display: FfiDisplay::inline(), @@ -3801,15 +3629,7 @@ mod tests { fn element_styles_adjust_from_marshaled_facts() { let mut input = element_adjustment_input(); input.disallow_display_contents = true; - let contents = FfiDisplay { - tag: DISPLAY_TAG_BOX, - outside: 0, - inside: 0, - list_item: false, - internal: 0, - box_value: display_box::CONTENTS, - }; - let adjustment = adjust_element_style(&input, contents, keyword::LEFT); + let adjustment = adjust_element_style(&input, FfiDisplay::contents(), keyword::LEFT); assert!(adjustment.changed_display); assert!(adjustment.display.is_none()); diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index ab40fb04d8eae..fca332d264b2f 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -636,6 +636,13 @@ void Document::reset_style_invalidation_counters() const m_style_invalidations_since_last_counter_dump = 0; } +void Document::record_layout_tree_build(u64 rebuilt_subtree_root_count, bool escaped_rebuild_roots) +{ + ++m_layout_tree_build_stats.builds; + m_layout_tree_build_stats.last_build_rebuilt_subtree_roots = rebuilt_subtree_root_count; + m_layout_tree_build_stats.last_build_escaped_rebuild_roots = escaped_rebuild_roots; +} + void Document::record_style_invalidation() const { ++m_style_invalidation_counters.style_invalidations; @@ -2006,6 +2013,7 @@ Document::PartialRelayoutResult Document::try_partial_relayout(HashTable(*tree_builder.build(*this)); + record_layout_tree_build(tree_builder.rebuilt_subtree_roots().size(), tree_builder.layout_tree_update_escaped_rebuild_roots()); needs_layout_tree_rebuild = false; layout_tree_was_built_in_partial_branch = true; pending_updates_escaped_during_partial_build = m_partial_relayout_invalidation.escapes() @@ -2173,6 +2181,7 @@ void Document::update_layout(UpdateLayoutReason reason) if (needs_layout_tree_rebuild) { Layout::TreeBuilder tree_builder; m_layout_root = as(*tree_builder.build(*this)); + record_layout_tree_build(tree_builder.rebuilt_subtree_roots().size(), tree_builder.layout_tree_update_escaped_rebuild_roots()); // NB: Called during layout update. if (document_element && document_element->unsafe_layout_node()) diff --git a/Libraries/LibWeb/DOM/Document.h b/Libraries/LibWeb/DOM/Document.h index a1f7d5c26b81e..675e9fa0fa418 100644 --- a/Libraries/LibWeb/DOM/Document.h +++ b/Libraries/LibWeb/DOM/Document.h @@ -1050,6 +1050,16 @@ class WEB_API Document void record_full_style_invalidation() const; static void set_style_invalidation_counter_dump_interval(Optional); + // Confinement report of the most recent layout tree build, for tests observing whether a + // partial rebuild stayed inside its rebuilt subtrees. + struct LayoutTreeBuildStats { + u64 builds { 0 }; + u64 last_build_rebuilt_subtree_roots { 0 }; + bool last_build_escaped_rebuild_roots { false }; + }; + LayoutTreeBuildStats const& layout_tree_build_stats() const { return m_layout_tree_build_stats; } + void record_layout_tree_build(u64 rebuilt_subtree_root_count, bool escaped_rebuild_roots); + void set_needs_accumulated_visual_contexts_update(bool); bool needs_accumulated_visual_contexts_update() const { return m_needs_accumulated_visual_contexts_update; } void schedule_accumulated_visual_context_value_update(Element&); @@ -1722,6 +1732,7 @@ class WEB_API Document Optional m_caret_hit_test_debug_rect; mutable StyleInvalidationCounters m_style_invalidation_counters; + LayoutTreeBuildStats m_layout_tree_build_stats; mutable u64 m_style_invalidations_since_last_counter_dump { 0 }; mutable GC::Ptr m_adopted_style_sheets; diff --git a/Libraries/LibWeb/Internals/Internals.cpp b/Libraries/LibWeb/Internals/Internals.cpp index de8e08f5c6424..0e19a475deaf0 100644 --- a/Libraries/LibWeb/Internals/Internals.cpp +++ b/Libraries/LibWeb/Internals/Internals.cpp @@ -957,6 +957,16 @@ void Internals::reset_style_invalidation_counters() window().associated_document().reset_style_invalidation_counters(); } +JS::Object* Internals::layout_tree_build_stats() +{ + auto object = JS::Object::create(realm(), nullptr); + auto const& stats = window().associated_document().layout_tree_build_stats(); + object->define_direct_property("builds"_utf16_fly_string, JS::Value(stats.builds), JS::default_attributes); + object->define_direct_property("lastBuildRebuiltSubtreeRoots"_utf16_fly_string, JS::Value(stats.last_build_rebuilt_subtree_roots), JS::default_attributes); + object->define_direct_property("lastBuildEscapedRebuildRoots"_utf16_fly_string, JS::Value(stats.last_build_escaped_rebuild_roots), JS::default_attributes); + return object; +} + JS::Object* Internals::style_ffi_counters() { auto object = JS::Object::create(realm(), nullptr); diff --git a/Libraries/LibWeb/Internals/Internals.h b/Libraries/LibWeb/Internals/Internals.h index f5652c2490e14..5fb7e6f26a168 100644 --- a/Libraries/LibWeb/Internals/Internals.h +++ b/Libraries/LibWeb/Internals/Internals.h @@ -147,6 +147,7 @@ class WEB_API Internals final : public InternalsBase { JS::Object* get_style_invalidation_counters(); void reset_style_invalidation_counters(); + JS::Object* layout_tree_build_stats(); JS::Object* computed_values_stats(); JS::Object* style_ffi_counters(); void reset_style_ffi_counters(); diff --git a/Libraries/LibWeb/Internals/Internals.idl b/Libraries/LibWeb/Internals/Internals.idl index 836b8423f6a82..ecafd866ec9c5 100644 --- a/Libraries/LibWeb/Internals/Internals.idl +++ b/Libraries/LibWeb/Internals/Internals.idl @@ -140,6 +140,11 @@ interface Internals { // styleInvalidations, elementStyleRecomputations, and elementStyleNoopRecomputations. object getStyleInvalidationCounters(); undefined resetStyleInvalidationCounters(); + // Returns the confinement report of the most recent layout tree build for the current + // document. Keys: builds (cumulative build count), lastBuildRebuiltSubtreeRoots (number of + // subtrees rebuilt in place), lastBuildEscapedRebuildRoots (whether any tree mutation + // escaped the rebuilt subtrees, disqualifying the build from partial relayout). + object layoutTreeBuildStats(); // Returns process-wide ComputedValues instance statistics. // Keys: liveComputedValues, totalComputedValuesCreated. object computedValuesStats(); diff --git a/Libraries/LibWeb/Layout/Node.cpp b/Libraries/LibWeb/Layout/Node.cpp index ad27fe2822d6d..51afe3ec8911a 100644 --- a/Libraries/LibWeb/Layout/Node.cpp +++ b/Libraries/LibWeb/Layout/Node.cpp @@ -902,6 +902,28 @@ void NodeWithStyle::set_computed_values(NonnullRefPtr m_computed_values = move(computed_values); } +void NodeWithStyle::set_display(CSS::Display display) +{ + modify_computed_values([&](auto& values) { + values.set_display(display); + }); +} + +void NodeWithStyle::set_content(CSS::ContentData const& content) +{ + modify_computed_values([&](auto& values) { + values.set_content(content); + }); +} + +void NodeWithStyle::set_overflow(CSS::Overflow overflow_x, CSS::Overflow overflow_y) +{ + modify_computed_values([&](auto& values) { + values.set_overflow_x(overflow_x); + values.set_overflow_y(overflow_y); + }); +} + void NodeWithStyle::reset_table_box_computed_values_used_by_wrapper_to_init_values() { VERIFY(this->display().is_table_inside()); diff --git a/Libraries/LibWeb/Layout/Node.h b/Libraries/LibWeb/Layout/Node.h index b696375b85eed..18e0477ada363 100644 --- a/Libraries/LibWeb/Layout/Node.h +++ b/Libraries/LibWeb/Layout/Node.h @@ -381,6 +381,10 @@ class WEB_API NodeWithStyle : public Node { void set_computed_values(NonnullRefPtr); + void set_display(CSS::Display); + void set_content(CSS::ContentData const&); + void set_overflow(CSS::Overflow overflow_x, CSS::Overflow overflow_y); + u32 layout_index() const { return m_layout_index; } void set_layout_index(u32 index) { m_layout_index = index; } diff --git a/Libraries/LibWeb/Layout/TreeBuilder.cpp b/Libraries/LibWeb/Layout/TreeBuilder.cpp index e156b51ece03e..c9a6a8a4f7c09 100644 --- a/Libraries/LibWeb/Layout/TreeBuilder.cpp +++ b/Libraries/LibWeb/Layout/TreeBuilder.cpp @@ -534,6 +534,32 @@ static Optional find_first_letter_in_block(BlockContainer& bl return {}; } +struct FirstLetterTextSlices { + NonnullRefPtr first_letter_slice; + NonnullRefPtr remainder_slice; +}; + +static FirstLetterTextSlices create_first_letter_text_slices(DOM::Document& document, TextNode& text_node, size_t letter_end) +{ + auto const full_length = text_node.text().length_in_code_units(); + + // The first-letter and remainder boxes render slices of the same DOM text node; generated text + // (from a content property) has no DOM node and gets plain generated slices of its text instead. + if (auto* dom_text = text_node.dom_text()) { + auto& mutable_dom_text = const_cast(*dom_text); + auto remainder_slice = make_ref_counted(document, mutable_dom_text, Node::AttachToDOMNode::Yes, letter_end, full_length - letter_end); + auto first_letter_slice = make_ref_counted(document, mutable_dom_text, Node::AttachToDOMNode::No, 0, letter_end); + remainder_slice->set_first_letter_slice(*first_letter_slice); + return { move(first_letter_slice), move(remainder_slice) }; + } + + auto text = text_node.text(); + return { + make_ref_counted(document, Utf16String::from_utf16(text.utf16_view().substring_view(0, letter_end))), + make_ref_counted(document, Utf16String::from_utf16(text.utf16_view().substring_view(letter_end, full_length - letter_end))), + }; +} + void TreeBuilder::create_first_letter_wrapper_if_needed(DOM::Element& element, BlockContainer& block_container) { if (!element.computed_values(CSS::PseudoElement::FirstLetter)) @@ -544,26 +570,9 @@ void TreeBuilder::create_first_letter_wrapper_if_needed(DOM::Element& element, B return; auto& text_node = *target->text_node; - auto const full_length = text_node.text().length_in_code_units(); - - auto const letter_end = target->letter_end; - auto& document = element.document(); - RefPtr remainder_slice; - RefPtr first_letter_slice; - if (auto* dom_text = text_node.dom_text()) { - auto& mutable_dom_text = const_cast(*dom_text); - auto dom_remainder_slice = make_ref_counted(document, mutable_dom_text, Node::AttachToDOMNode::Yes, letter_end, full_length - letter_end); - auto dom_first_letter_slice = make_ref_counted(document, mutable_dom_text, Node::AttachToDOMNode::No, 0, letter_end); - dom_remainder_slice->set_first_letter_slice(*dom_first_letter_slice); - remainder_slice = move(dom_remainder_slice); - first_letter_slice = move(dom_first_letter_slice); - } else { - auto text = text_node.text(); - remainder_slice = make_ref_counted(document, Utf16String::from_utf16(text.utf16_view().substring_view(letter_end, full_length - letter_end))); - first_letter_slice = make_ref_counted(document, Utf16String::from_utf16(text.utf16_view().substring_view(0, letter_end))); - } + auto [first_letter_slice, remainder_slice] = create_first_letter_text_slices(document, text_node, target->letter_end); auto first_letter_values = element.computed_values(CSS::PseudoElement::FirstLetter); VERIFY(first_letter_values); @@ -584,6 +593,21 @@ void TreeBuilder::create_first_letter_wrapper_if_needed(DOM::Element& element, B parent->remove_child(text_node); } +NonnullRefPtr TreeBuilder::create_and_attach_list_item_marker(ListItemBox& list_box, DOM::Element& element, NonnullRefPtr marker_style) +{ + auto list_item_marker = make_ref_counted( + list_box.document(), + list_box.computed_values().list_style_type(), + list_box.computed_values().list_style_position(), + element, + move(marker_style)); + list_item_marker->attach_style_resources(); + list_box.set_marker(list_item_marker); + element.set_synthetic_pseudo_element_node({}, CSS::PseudoElement::Marker, list_item_marker); + list_box.prepend_child(*list_item_marker); + return list_item_marker; +} + RefPtr TreeBuilder::create_pseudo_element_if_needed(DOM::Element& element, CSS::PseudoElement pseudo_element, Optional insertion_mode) { auto& document = element.document(); @@ -604,19 +628,21 @@ RefPtr TreeBuilder::create_pseudo_element_if_needed(DOM::Element& return {}; auto initial_quote_nesting_level = m_quote_nesting_level; - DOM::AbstractElement element_reference { element, pseudo_element }; - auto [pseudo_element_content, final_quote_nesting_level] = pseudo_element_values->resolved_content(element_reference, initial_quote_nesting_level); - m_quote_nesting_level = final_quote_nesting_level; + + // NB: Whether this pseudo-element generates a box depends only on the shape of its computed + // content value, so the full resolution (which reads counters that do not exist until + // the box is inserted) can wait until after insertion. + auto const computed_content_type = pseudo_element_values->computed_content().type; // ::before and ::after only exist if they have content. `content: normal` computes to `none` for them. if (first_is_one_of(pseudo_element, CSS::PseudoElement::Before, CSS::PseudoElement::After) - && (pseudo_element_content.type == CSS::ContentData::Type::Normal - || pseudo_element_content.type == CSS::ContentData::Type::None)) + && (computed_content_type == CSS::ComputedContentData::Type::Normal + || computed_content_type == CSS::ComputedContentData::Type::None)) return {}; // For ::marker with content 'none' -- do nothing. if (pseudo_element == CSS::PseudoElement::Marker - && pseudo_element_content.type == CSS::ContentData::Type::None) + && computed_content_type == CSS::ComputedContentData::Type::None) return {}; // For ::marker with content 'normal', create the marker pseudo-element from a ListItemMarkerBox @@ -624,27 +650,16 @@ RefPtr TreeBuilder::create_pseudo_element_if_needed(DOM::Element& // are rendered using the special list-item counter. // See: https://github.com/LadybirdBrowser/ladybird/issues/4782 // NB: Called during layout tree construction. - if (pseudo_element == CSS::PseudoElement::Marker && pseudo_element_content.type == CSS::ContentData::Type::Normal) + if (pseudo_element == CSS::PseudoElement::Marker && computed_content_type == CSS::ComputedContentData::Type::Normal) if (auto* list_box = as_if(*element.unsafe_layout_node())) { // https://www.w3.org/TR/css-lists-3/#content-property // "::marker does not generate a box" when list-style-type is 'none' and there's no marker image. Custom // ::marker content is already excluded by the outer condition checking for Type::Normal. - auto const& list_style_type = list_box->computed_values().list_style_type(); - if (list_style_type.has() && !list_box->list_style_image()) { + if (list_box->computed_values().list_style_type().has() && !list_box->list_style_image()) { return {}; } - auto list_item_marker = make_ref_counted( - document, - list_style_type, - list_box->computed_values().list_style_position(), - element, - NonnullRefPtr { *pseudo_element_values }); - list_item_marker->attach_style_resources(); - list_box->set_marker(list_item_marker); - element.set_synthetic_pseudo_element_node({}, CSS::PseudoElement::Marker, list_item_marker); - list_box->prepend_child(*list_item_marker); - return list_item_marker; + return create_and_attach_list_item_marker(*list_box, element, NonnullRefPtr { *pseudo_element_values }); } RefPtr pseudo_element_node; @@ -670,12 +685,10 @@ RefPtr TreeBuilder::create_pseudo_element_if_needed(DOM::Element& if (is_content_replacement) { pseudo_element_node = create_content_image_box(document, nullptr, NonnullRefPtr { *pseudo_element_values }, const_cast(*replacement_image)); if (auto adjusted_display = adjusted_table_display_for_replaced_element(pseudo_element_display); adjusted_display.has_value()) - pseudo_element_node->modify_computed_values([&](auto& values) { values.set_display(*adjusted_display); }); + pseudo_element_node->set_display(*adjusted_display); } else if (pseudo_element_display.is_contents()) { pseudo_element_node = make_ref_counted(document, nullptr, NonnullRefPtr { *pseudo_element_values }); - pseudo_element_node->modify_computed_values([](auto& values) { - values.set_display(CSS::Display(CSS::DisplayOutside::Inline, CSS::DisplayInside::Flow)); - }); + pseudo_element_node->set_display(CSS::Display(CSS::DisplayOutside::Inline, CSS::DisplayInside::Flow)); } else { pseudo_element_node = DOM::Element::create_layout_node_for_display_type(document, pseudo_element_display, NonnullRefPtr { *pseudo_element_values }, nullptr); if (!pseudo_element_node) @@ -684,20 +697,9 @@ RefPtr TreeBuilder::create_pseudo_element_if_needed(DOM::Element& pseudo_element_node->attach_style_resources(); // FIXME: This code actually computes style for element::marker, and shouldn't for element::pseudo::marker - if (is(*pseudo_element_node)) { - auto& style_computer = document.style_computer(); - - auto marker_style = style_computer.compute_style({ element, CSS::PseudoElement::Marker }); - auto list_item_marker = make_ref_counted( - document, - pseudo_element_node->computed_values().list_style_type(), - pseudo_element_node->computed_values().list_style_position(), - element, - marker_style); - list_item_marker->attach_style_resources(); - static_cast(*pseudo_element_node).set_marker(list_item_marker); - element.set_synthetic_pseudo_element_node({}, CSS::PseudoElement::Marker, list_item_marker); - pseudo_element_node->prepend_child(*list_item_marker); + if (auto* list_box = as_if(*pseudo_element_node)) { + auto marker_style = document.style_computer().compute_style({ element, CSS::PseudoElement::Marker }); + (void)create_and_attach_list_item_marker(*list_box, element, move(marker_style)); // FIXME: Support counters on element::pseudo::marker } @@ -708,46 +710,37 @@ RefPtr TreeBuilder::create_pseudo_element_if_needed(DOM::Element& element.set_synthetic_pseudo_element_node({}, pseudo_element, pseudo_element_node); if (insertion_mode.has_value()) insert_node_into_inline_or_block_ancestor(*pseudo_element_node, pseudo_element_node->display(), insertion_mode.value()); - pseudo_element_node->modify_computed_values([&](auto& values) { - values.set_content(pseudo_element_content); - }); + // Resolve counters before content: counter() and counters() items in the content list read + // the counters established for this pseudo-element's box. + DOM::AbstractElement element_reference { element, pseudo_element }; CSS::resolve_counters(element_reference); - // Now that we have counters, we can compute the content for real. Which is silly. - if (pseudo_element_content.type == CSS::ContentData::Type::List) { - auto [new_content, _] = pseudo_element_values->resolved_content(element_reference, initial_quote_nesting_level); - pseudo_element_node->modify_computed_values([&](auto& values) { - values.set_content(new_content); - }); - // FIXME: Handle images, and multiple values - if (new_content.type == CSS::ContentData::Type::List) { - if (!is_content_replacement) { - push_parent(*pseudo_element_node); - for (auto& item : new_content.data) { - RefPtr layout_node; - if (auto const* string = item.get_pointer()) { - layout_node = make_ref_counted(document, *string); - } else { - auto& image = *item.get>(); - auto image_box = create_content_image_box(document, nullptr, NonnullRefPtr { pseudo_element_node->computed_values() }, image); - // https://drafts.csswg.org/css-content-3/#content-property - // For , this is an inline anonymous replaced element. - image_box->modify_computed_values([](auto& values) { - values.set_display(CSS::Display(CSS::DisplayOutside::Inline, CSS::DisplayInside::Flow)); - }); - image_box->attach_style_resources(); - layout_node = move(image_box); - } - layout_node->set_generated_for(pseudo_element, element); - auto display = layout_node->is_text_node() ? CSS::Display::from_short(CSS::Display::Short::Inline) : as(*layout_node).display(); - insert_node_into_inline_or_block_ancestor(*layout_node, display, AppendOrPrepend::Append); - } - pop_parent(); + auto [pseudo_element_content, final_quote_nesting_level] = pseudo_element_values->resolved_content(element_reference, initial_quote_nesting_level); + m_quote_nesting_level = final_quote_nesting_level; + pseudo_element_node->set_content(pseudo_element_content); + + // FIXME: Handle images, and multiple values + if (pseudo_element_content.type == CSS::ContentData::Type::List && !is_content_replacement) { + push_parent(*pseudo_element_node); + for (auto& item : pseudo_element_content.data) { + RefPtr layout_node; + if (auto const* string = item.get_pointer()) { + layout_node = make_ref_counted(document, *string); + } else { + auto& image = *item.get>(); + auto image_box = create_content_image_box(document, nullptr, NonnullRefPtr { pseudo_element_node->computed_values() }, image); + // https://drafts.csswg.org/css-content-3/#content-property + // For , this is an inline anonymous replaced element. + image_box->set_display(CSS::Display(CSS::DisplayOutside::Inline, CSS::DisplayInside::Flow)); + image_box->attach_style_resources(); + layout_node = move(image_box); } - } else { - TODO(); + layout_node->set_generated_for(pseudo_element, element); + auto display = layout_node->is_text_node() ? CSS::Display::from_short(CSS::Display::Short::Inline) : as(*layout_node).display(); + insert_node_into_inline_or_block_ancestor(*layout_node, display, AppendOrPrepend::Append); } + pop_parent(); } return pseudo_element_node; @@ -803,6 +796,33 @@ static bool layout_node_is_attached_to_dom_subtree(Node const& layout_node, DOM: return false; } +// The replacement box represents the same element in the same tree position, so layout state +// saved by the previous layout pass carries over to it: the saved abspos layout inputs, and the +// flat fragment and inline-box-piece lists held by the containing block of a node that +// participated in inline layout, which a subtree relayout that skips the containing block never +// rebuilds. +static void transfer_saved_layout_state_to_replacement_box(Layout::Node& old_layout_node, Layout::Node& new_layout_node) +{ + if (auto const* old_box = as_if(old_layout_node)) { + if (auto* new_box = as_if(new_layout_node)) { + if (old_box->saved_abspos_layout_inputs()) + new_box->set_saved_abspos_layout_inputs(*old_box->saved_abspos_layout_inputs()); + } + } + if (auto* containing_block = old_layout_node.containing_block()) { + if (auto* paintable_with_lines = as_if(containing_block->paintable().ptr())) { + for (auto& fragment : paintable_with_lines->fragments()) { + if (fragment.has_layout_node() && &fragment.layout_node() == &old_layout_node) + fragment.set_layout_node(new_layout_node); + } + for (auto& piece : paintable_with_lines->inline_box_pieces()) { + if (piece.node.ptr() == &old_layout_node) + piece.node = &new_layout_node; + } + } + } +} + static DOM::Element* display_contents_style_parent_for_text_node(DOM::Text& text_node) { auto* parent = text_node.flat_tree_parent(); @@ -866,16 +886,8 @@ void TreeBuilder::detach_top_layer_element_layout_subtree(DOM::Element& element) element.for_each_shadow_including_inclusive_descendant([&](auto& node) { return clear_stale_layout_and_paint_node(node, &element); }); - // Assigned slottables are flat tree children of a slot, not DOM descendants. - if (auto* slot_element = as_if(element)) { - for (auto const& slottable : slot_element->assigned_nodes_internal()) { - slottable.visit([&](DOM::Node& slottable_root) { - slottable_root.for_each_shadow_including_inclusive_descendant([&](auto& node) { - return clear_stale_layout_and_paint_node(node, &slottable_root); - }); - }); - } - } + if (auto* slot_element = as_if(element)) + clear_stale_layout_nodes_for_assigned_slottables(*slot_element); } static bool element_has_an_unrendered_flat_tree_ancestor(DOM::Element const& element) @@ -892,6 +904,118 @@ static bool element_has_an_unrendered_flat_tree_ancestor(DOM::Element const& ele return false; } +void TreeBuilder::update_layout_tree_for_shadow_root_children(DOM::ShadowRoot& shadow_root, Context& context, MustCreateSubtree must_create_subtree) +{ + for (auto* node = shadow_root.first_child(); node; node = node->next_sibling()) + update_layout_tree(*node, context, must_create_subtree); + shadow_root.set_child_needs_layout_tree_update(false); + shadow_root.set_needs_layout_tree_update(false, DOM::SetNeedsLayoutTreeUpdateReason::None); +} + +void TreeBuilder::update_layout_tree_for_dom_children(DOM::ParentNode& parent, Context& context, MustCreateSubtree must_create_subtree) +{ + for (auto* node = parent.first_child(); node; node = node->next_sibling()) + update_layout_tree(*node, context, must_create_subtree); +} + +void TreeBuilder::update_layout_tree_for_assigned_slottables(HTML::HTMLSlotElement& slot_element, Context& context, MustCreateSubtree must_create_subtree) +{ + auto must_create_subtree_for_slottable = must_create_subtree; + if (slot_element.needs_layout_tree_update()) + must_create_subtree_for_slottable = MustCreateSubtree::Yes; + + for (auto const& slottable : slot_element.assigned_nodes_internal()) + slottable.visit([&](auto& node) { update_layout_tree(node, context, must_create_subtree_for_slottable); }); +} + +void TreeBuilder::clear_stale_layout_nodes_for_assigned_slottables(HTML::HTMLSlotElement& slot_element) +{ + // Assigned slottables are flat tree children of a slot, not DOM descendants, so subtree + // cleanup of the slot does not reach them. + for (auto const& slottable : slot_element.assigned_nodes_internal()) { + slottable.visit([&](DOM::Node& slottable_root) { + slottable_root.for_each_shadow_including_inclusive_descendant([&](auto& node) { + return clear_stale_layout_and_paint_node(node, &slottable_root); + }); + }); + } +} + +// Elements inside a `display:none` subtree are skipped by `Document::update_style_recursively`, +// so a bypass path (top-layer iteration, slot projection, SVG mask/clip-path or pattern +// reference) may reach an element whose `needs_style_update` flag is still set or whose +// `computed_values` is null. Route through `update_style_for_element`, which seeds the style +// computer's ancestor filter so descendant-combinator selectors continue to match during the +// lazy re-cascade. +static void update_style_if_needed_for_layout_tree_bypass_path(DOM::Element& element) +{ + if (element.needs_style_update() || !element.computed_values()) { + element.document().update_style_for_element({ element }); + element.set_needs_style_update(false); + } +} + +RefPtr TreeBuilder::create_layout_node_for_element(DOM::Element& element, Context& context) const +{ + auto& document = element.document(); + NonnullRefPtr computed_values = *element.computed_values(); + + if (auto content_replacement = create_content_replacement_if_needed(element, computed_values)) + return content_replacement; + + if (context.layout_svg_mask_or_clip_path) { + RefPtr layout_node; + if (is(element)) + layout_node = make_ref_counted(document, static_cast(element), move(computed_values)); + else if (is(element)) + layout_node = make_ref_counted(document, static_cast(element), move(computed_values)); + else + VERIFY_NOT_REACHED(); + // Only layout direct uses of SVG masks/clipPaths. + context.layout_svg_mask_or_clip_path = false; + return layout_node; + } + + if (context.layout_svg_pattern) { + context.layout_svg_pattern = false; + return make_ref_counted(document, as(element), move(computed_values)); + } + + return element.create_layout_node(move(computed_values)); +} + +static RefPtr create_layout_node_for_text(DOM::Text& text_node) +{ + auto& document = text_node.document(); + RefPtr layout_node = make_ref_counted(document, text_node); + if (auto* style_parent = display_contents_style_parent_for_text_node(text_node); style_parent && display_contents_text_needs_style_wrapper(text_node, *style_parent)) { + auto wrapper = make_ref_counted(document, nullptr, style_parent->computed_values().release_nonnull()); + wrapper->attach_style_resources(); + wrapper->set_display(CSS::Display(CSS::DisplayOutside::Inline, CSS::DisplayInside::Flow)); + wrapper->set_children_are_inline(true); + wrapper->append_child(*layout_node); + return wrapper; + } + return layout_node; +} + +// Each element rendered in the top layer has a ::backdrop pseudo-element, for which it is the +// originating element. When the element's box replaces an existing one in place, the ::backdrop +// box must be inserted before the old box so it ends up behind the element; otherwise it is +// appended before the element's own box is. +void TreeBuilder::create_backdrop_for_top_layer_element_if_needed(DOM::Element& element, Layout::Node* old_layout_node, bool may_replace_existing_layout_node) +{ + if (may_replace_existing_layout_node) { + if (auto backdrop_node = create_pseudo_element_if_needed(element, CSS::PseudoElement::Backdrop, {})) { + // The ::backdrop box is a fresh sibling of the rebuild root, outside it. + note_tree_restructuring_at(*old_layout_node->parent()); + old_layout_node->parent()->insert_before(*backdrop_node, old_layout_node); + } + } else { + (void)create_pseudo_element_if_needed(element, CSS::PseudoElement::Backdrop, AppendOrPrepend::Append); + } +} + void TreeBuilder::update_layout_tree(DOM::Node& dom_node, TreeBuilder::Context& context, MustCreateSubtree must_create_subtree) { // NB: Called during layout tree construction. @@ -977,16 +1101,7 @@ void TreeBuilder::update_layout_tree(DOM::Node& dom_node, TreeBuilder::Context& old_backdrop_node->remove(); } element.clear_synthetic_pseudo_element_layout_nodes(Badge {}); - // Elements inside a `display:none` subtree are skipped by - // `Document::update_style_recursively`, so a bypass path (top-layer iteration, slot - // projection, SVG mask/clip-path or pattern reference) may reach an element whose - // `needs_style_update` flag is still set or whose `computed_values` is null. Route - // through `update_style_for_element`, which seeds the style computer's ancestor filter - // so descendant-combinator selectors continue to match during the lazy re-cascade. - if (element.needs_style_update() || !element.computed_values()) { - document.update_style_for_element({ element }); - element.set_needs_style_update(false); - } + update_style_if_needed_for_layout_tree_bypass_path(element); computed_values = element.computed_values(); display = computed_values->display(); if (display.is_none()) @@ -996,42 +1111,15 @@ void TreeBuilder::update_layout_tree(DOM::Node& dom_node, TreeBuilder::Context& update_layout_tree_for_display_contents(element, context, must_create_subtree, should_create_layout_node); return; } - if (auto content_replacement = create_content_replacement_if_needed(element, NonnullRefPtr { *computed_values })) { - layout_node = content_replacement.release_nonnull(); - } else if (context.layout_svg_mask_or_clip_path) { - if (is(dom_node)) - layout_node = make_ref_counted(document, static_cast(dom_node), computed_values.release_nonnull()); - else if (is(dom_node)) - layout_node = make_ref_counted(document, static_cast(dom_node), computed_values.release_nonnull()); - else - VERIFY_NOT_REACHED(); - // Only layout direct uses of SVG masks/clipPaths. - context.layout_svg_mask_or_clip_path = false; - } else if (context.layout_svg_pattern) { - layout_node = make_ref_counted(document, as(dom_node), computed_values.release_nonnull()); - context.layout_svg_pattern = false; - } else { - layout_node = element.create_layout_node(computed_values.release_nonnull()); - } + layout_node = create_layout_node_for_element(element, context); } else if (is(dom_node)) { auto document_style = style_computer.create_document_style(); computed_values = move(document_style); display = computed_values->display(); layout_node = make_ref_counted(static_cast(dom_node), computed_values.release_nonnull()); } else if (is(dom_node)) { - auto& text_node = static_cast(dom_node); - layout_node = make_ref_counted(document, text_node); + layout_node = create_layout_node_for_text(static_cast(dom_node)); display = CSS::Display(CSS::DisplayOutside::Inline, CSS::DisplayInside::Flow); - if (auto* style_parent = display_contents_style_parent_for_text_node(text_node); style_parent && display_contents_text_needs_style_wrapper(text_node, *style_parent)) { - auto wrapper = make_ref_counted(document, nullptr, style_parent->computed_values().release_nonnull()); - wrapper->attach_style_resources(); - wrapper->modify_computed_values([&](auto& values) { - values.set_display(display); - }); - wrapper->set_children_are_inline(true); - wrapper->append_child(*layout_node); - layout_node = move(wrapper); - } } } @@ -1044,9 +1132,7 @@ void TreeBuilder::update_layout_tree(DOM::Node& dom_node, TreeBuilder::Context& if (layout_node->is_replaced_element()) { if (auto adjusted_display = adjusted_table_display_for_replaced_element(display); adjusted_display.has_value()) { display = *adjusted_display; - as(*layout_node).modify_computed_values([&](auto& values) { - values.set_display(display); - }); + as(*layout_node).set_display(display); } } @@ -1068,20 +1154,8 @@ void TreeBuilder::update_layout_tree(DOM::Node& dom_node, TreeBuilder::Context& if (dom_node.is_element() && should_create_layout_node) { auto& element = static_cast(dom_node); - // Each element rendered in the top layer has a ::backdrop pseudo-element, for which it is the originating element. - if (element.rendered_in_top_layer() && context.layout_top_layer) { - // If we're inserting a new element, we can append the ::backdrop node now, before layout_node is appended. - // Otherwise, we need to insert the ::backdrop before old_layout_node so it's behind the layout_node. - if (may_replace_existing_layout_node) { - if (auto backdrop_node = create_pseudo_element_if_needed(element, CSS::PseudoElement::Backdrop, {})) { - // The ::backdrop box is a fresh sibling of the rebuild root, outside it. - note_tree_restructuring_at(*old_layout_node->parent()); - old_layout_node->parent()->insert_before(*backdrop_node, old_layout_node); - } - } else { - (void)create_pseudo_element_if_needed(element, CSS::PseudoElement::Backdrop, AppendOrPrepend::Append); - } - } + if (element.rendered_in_top_layer() && context.layout_top_layer) + create_backdrop_for_top_layer_element_if_needed(element, old_layout_node, may_replace_existing_layout_node); } // A top layer member nested inside this member must be skipped at its normal position @@ -1094,30 +1168,7 @@ void TreeBuilder::update_layout_tree(DOM::Node& dom_node, TreeBuilder::Context& m_layout_root = layout_node; } else if (should_create_layout_node) { if (may_replace_existing_layout_node) { - // The replacement box represents the same element in the same tree position, so the - // layout inputs saved by the previous layout pass carry over to it. - if (auto const* old_box = as_if(*old_layout_node)) { - if (auto* new_box = as_if(*layout_node)) { - if (old_box->saved_abspos_layout_inputs()) - new_box->set_saved_abspos_layout_inputs(*old_box->saved_abspos_layout_inputs()); - } - } - // A replaced node that participated in inline layout is referenced by the flat - // fragment and inline-box-piece lists held by its containing block; repoint those - // references at the replacement, since a subtree relayout that skips the containing - // block never rebuilds them. - if (auto* containing_block = old_layout_node->containing_block()) { - if (auto* paintable_with_lines = as_if(containing_block->paintable().ptr())) { - for (auto& fragment : paintable_with_lines->fragments()) { - if (fragment.has_layout_node() && &fragment.layout_node() == old_layout_node.ptr()) - fragment.set_layout_node(*layout_node); - } - for (auto& piece : paintable_with_lines->inline_box_pieces()) { - if (piece.node.ptr() == old_layout_node.ptr()) - piece.node = layout_node.ptr(); - } - } - } + transfer_saved_layout_state_to_replacement_box(*old_layout_node, *layout_node); old_layout_node->prepare_subtree_for_detach_from_layout_tree(); old_layout_node->parent()->replace_child(*layout_node, *old_layout_node); } else if (layout_node->is_svg_box()) { @@ -1175,20 +1226,14 @@ void TreeBuilder::update_layout_tree(DOM::Node& dom_node, TreeBuilder::Context& } push_parent(as(*layout_node->first_child())); } - for (auto* node = shadow_root->first_child(); node; node = node->next_sibling()) { - update_layout_tree(*node, context, should_create_layout_node ? MustCreateSubtree::Yes : MustCreateSubtree::No); - } + update_layout_tree_for_shadow_root_children(*shadow_root, context, should_create_layout_node ? MustCreateSubtree::Yes : MustCreateSubtree::No); if (layout_node->is_replaced_box_with_children()) pop_parent(); - shadow_root->set_child_needs_layout_tree_update(false); - shadow_root->set_needs_layout_tree_update(false, DOM::SetNeedsLayoutTreeUpdateReason::None); } else if (should_layout_dom_children) { if (auto* switch_element = as_if(dom_node)) { update_layout_tree_for_svg_switch_children(*switch_element, context, should_create_layout_node ? MustCreateSubtree::Yes : MustCreateSubtree::No); } else { - // This is the same as as(dom_node).for_each_child - for (auto* node = as(dom_node).first_child(); node; node = node->next_sibling()) - update_layout_tree(*node, context, should_create_layout_node ? MustCreateSubtree::Yes : MustCreateSubtree::No); + update_layout_tree_for_dom_children(as(dom_node), context, should_create_layout_node ? MustCreateSubtree::Yes : MustCreateSubtree::No); } } @@ -1216,28 +1261,11 @@ void TreeBuilder::update_layout_tree(DOM::Node& dom_node, TreeBuilder::Context& auto& slot_element = static_cast(dom_node); if (slot_element.computed_values()->content_visibility() != CSS::ContentVisibility::Hidden) { - auto slottables = slot_element.assigned_nodes_internal(); push_parent(as(*layout_node)); - - MustCreateSubtree must_create_subtree_for_slottable = must_create_subtree; - if (slot_element.needs_layout_tree_update()) - must_create_subtree_for_slottable = MustCreateSubtree::Yes; - - for (auto const& slottable : slottables) { - slottable.visit([&](auto& node) { update_layout_tree(node, context, must_create_subtree_for_slottable); }); - } - + update_layout_tree_for_assigned_slottables(slot_element, context, must_create_subtree); pop_parent(); } else { - // Assigned slottables are not DOM descendants of the slot, so the generic - // content-visibility:hidden descendant cleanup above does not reach them. - for (auto const& slottable : slot_element.assigned_nodes_internal()) { - slottable.visit([&](DOM::Node& slottable_root) { - slottable_root.for_each_shadow_including_inclusive_descendant([&](auto& node) { - return clear_stale_layout_and_paint_node(node, &slottable_root); - }); - }); - } + clear_stale_layout_nodes_for_assigned_slottables(slot_element); } } @@ -1289,36 +1317,17 @@ void TreeBuilder::update_layout_tree_for_display_contents(DOM::Element& element, auto shadow_root = element.shadow_root(); if (!element_has_content_visibility_hidden && (should_create_layout_node || element.child_needs_layout_tree_update())) { - if (shadow_root) { - for (auto* node = shadow_root->first_child(); node; node = node->next_sibling()) - update_layout_tree(*node, context, should_create_layout_node ? MustCreateSubtree::Yes : MustCreateSubtree::No); - shadow_root->set_child_needs_layout_tree_update(false); - shadow_root->set_needs_layout_tree_update(false, DOM::SetNeedsLayoutTreeUpdateReason::None); - } else if (should_layout_dom_children) { - for (auto* node = element.first_child(); node; node = node->next_sibling()) - update_layout_tree(*node, context, should_create_layout_node ? MustCreateSubtree::Yes : MustCreateSubtree::No); - } + if (shadow_root) + update_layout_tree_for_shadow_root_children(*shadow_root, context, should_create_layout_node ? MustCreateSubtree::Yes : MustCreateSubtree::No); + else if (should_layout_dom_children) + update_layout_tree_for_dom_children(element, context, should_create_layout_node ? MustCreateSubtree::Yes : MustCreateSubtree::No); } - if (is(element)) { - auto& slot_element = static_cast(element); - - if (!element_has_content_visibility_hidden) { - MustCreateSubtree must_create_subtree_for_slottable = must_create_subtree; - if (slot_element.needs_layout_tree_update()) - must_create_subtree_for_slottable = MustCreateSubtree::Yes; - - for (auto const& slottable : slot_element.assigned_nodes_internal()) - slottable.visit([&](auto& node) { update_layout_tree(node, context, must_create_subtree_for_slottable); }); - } else { - for (auto const& slottable : slot_element.assigned_nodes_internal()) { - slottable.visit([&](DOM::Node& slottable_root) { - slottable_root.for_each_shadow_including_inclusive_descendant([&](auto& node) { - return clear_stale_layout_and_paint_node(node, &slottable_root); - }); - }); - } - } + if (auto* slot_element = as_if(element)) { + if (!element_has_content_visibility_hidden) + update_layout_tree_for_assigned_slottables(*slot_element, context, must_create_subtree); + else + clear_stale_layout_nodes_for_assigned_slottables(*slot_element); } if (!element_has_content_visibility_hidden) @@ -1355,6 +1364,30 @@ void TreeBuilder::update_layout_tree_for_svg_switch_children(SVG::SVGSwitchEleme update_layout_tree(*rendered_child, context, must_create_subtree); } +// A full-height flex column that centers the button contents vertically. +static NonnullRefPtr create_button_flex_wrapper(NodeWithStyle& parent) +{ + auto flex_wrapper = parent.create_anonymous_wrapper(); + flex_wrapper->modify_computed_values([](auto& values) { + values.set_display(CSS::Display { CSS::DisplayOutside::Block, CSS::DisplayInside::Flex }); + values.set_justify_content(CSS::JustifyContent::Center); + values.set_flex_direction(CSS::FlexDirection::Column); + values.set_height(CSS::Size::make_percentage(CSS::Percentage(100))); + }); + return flex_wrapper; +} + +// Let percentage-sized descendants shrink to fixed-height buttons instead of the flex +// item's automatic minimum size. +static NonnullRefPtr create_button_content_box_wrapper(NodeWithStyle& parent) +{ + auto content_box_wrapper = parent.create_anonymous_wrapper(); + content_box_wrapper->modify_computed_values([](auto& values) { + values.set_min_height(CSS::Size::make_px(CSSPixels(0))); + }); + return content_box_wrapper; +} + void TreeBuilder::wrap_in_button_layout_tree_if_needed(DOM::Node& dom_node, Layout::Node& layout_node) { auto const* html_element = as_if(dom_node); @@ -1371,20 +1404,9 @@ void TreeBuilder::wrap_in_button_layout_tree_if_needed(DOM::Node& dom_node, Layo // If the box does not overflow in the vertical axis, then it is centered vertically. // FIXME: Only apply alignment when box overflows - auto flex_wrapper = parent.create_anonymous_wrapper(); - flex_wrapper->modify_computed_values([](auto& values) { - values.set_display(CSS::Display { CSS::DisplayOutside::Block, CSS::DisplayInside::Flex }); - values.set_justify_content(CSS::JustifyContent::Center); - values.set_flex_direction(CSS::FlexDirection::Column); - values.set_height(CSS::Size::make_percentage(CSS::Percentage(100))); - }); + auto flex_wrapper = create_button_flex_wrapper(parent); - auto content_box_wrapper = parent.create_anonymous_wrapper(); - // Let percentage-sized descendants shrink to fixed-height buttons instead of the flex - // item's automatic minimum size. - content_box_wrapper->modify_computed_values([](auto& values) { - values.set_min_height(CSS::Size::make_px(CSSPixels(0))); - }); + auto content_box_wrapper = create_button_content_box_wrapper(parent); content_box_wrapper->set_children_are_inline(parent.children_are_inline()); Vector> sequence; @@ -1503,9 +1525,7 @@ void TreeBuilder::update_layout_tree_after_children(DOM::Node& dom_node, Layout: if (auto* fieldset_box = as_if(layout_node)) { if (auto legend = fieldset_box->rendered_legend()) { auto wrapper = fieldset_box->create_anonymous_wrapper(); - wrapper->modify_computed_values([](auto& values) { - values.set_display(CSS::Display::from_short(CSS::Display::Short::FlowRoot)); - }); + wrapper->set_display(CSS::Display::from_short(CSS::Display::Short::FlowRoot)); // https://html.spec.whatwg.org/multipage/rendering.html#the-fieldset-and-legend-elements // The following properties are expected to inherit from the fieldset element: @@ -1514,14 +1534,8 @@ void TreeBuilder::update_layout_tree_after_children(DOM::Node& dom_node, Layout: // grid-column-gap, grid-row-gap, grid-template-areas, grid-template-columns, grid-template-rows), // justify-content, justify-items, overflow, padding, text-overflow, unicode-bidi // FIXME: Transfer all of these properties, not just overflow. - wrapper->modify_computed_values([&](auto& values) { - values.set_overflow_x(fieldset_box->computed_values().overflow_x()); - values.set_overflow_y(fieldset_box->computed_values().overflow_y()); - }); - fieldset_box->modify_computed_values([](auto& values) { - values.set_overflow_x(CSS::InitialValues::overflow()); - values.set_overflow_y(CSS::InitialValues::overflow()); - }); + wrapper->set_overflow(fieldset_box->computed_values().overflow_x(), fieldset_box->computed_values().overflow_y()); + fieldset_box->set_overflow(CSS::InitialValues::overflow(), CSS::InitialValues::overflow()); for (auto child = fieldset_box->first_child(); child;) { auto next = child->next_sibling(); diff --git a/Libraries/LibWeb/Layout/TreeBuilder.h b/Libraries/LibWeb/Layout/TreeBuilder.h index 8ab91c0859baa..511b81ca6f843 100644 --- a/Libraries/LibWeb/Layout/TreeBuilder.h +++ b/Libraries/LibWeb/Layout/TreeBuilder.h @@ -53,6 +53,10 @@ class TreeBuilder { void update_layout_tree(DOM::Node&, Context&, MustCreateSubtree); void update_layout_tree_for_display_contents(DOM::Element&, Context&, MustCreateSubtree, bool should_create_layout_node); void update_layout_tree_for_svg_switch_children(SVG::SVGSwitchElement&, Context&, MustCreateSubtree); + void update_layout_tree_for_shadow_root_children(DOM::ShadowRoot&, Context&, MustCreateSubtree); + void update_layout_tree_for_dom_children(DOM::ParentNode&, Context&, MustCreateSubtree); + void update_layout_tree_for_assigned_slottables(HTML::HTMLSlotElement&, Context&, MustCreateSubtree); + static void clear_stale_layout_nodes_for_assigned_slottables(HTML::HTMLSlotElement&); static TraversalDecision clear_stale_layout_and_paint_node(DOM::Node&, DOM::Node const* cleared_subtree_root = nullptr); void push_parent(Layout::NodeWithStyle& node) { m_ancestor_stack.append(&node); } @@ -71,6 +75,9 @@ class TreeBuilder { void missing_cells_fixup(Vector> const&); void insert_node_into_inline_or_block_ancestor(Layout::Node&, CSS::Display, AppendOrPrepend); + RefPtr create_layout_node_for_element(DOM::Element&, Context&) const; + void create_backdrop_for_top_layer_element_if_needed(DOM::Element&, Layout::Node* old_layout_node, bool may_replace_existing_layout_node); + static NonnullRefPtr create_and_attach_list_item_marker(ListItemBox&, DOM::Element&, NonnullRefPtr marker_style); RefPtr create_pseudo_element_if_needed(DOM::Element&, CSS::PseudoElement, Optional); RefPtr create_content_replacement_if_needed(DOM::Element&, NonnullRefPtr) const; static void create_first_letter_wrapper_if_needed(DOM::Element&, Layout::BlockContainer&); diff --git a/Tests/LibWeb/Text/expected/layout-tree-update/tree-build-confinement.txt b/Tests/LibWeb/Text/expected/layout-tree-update/tree-build-confinement.txt new file mode 100644 index 0000000000000..07683583ed593 --- /dev/null +++ b/Tests/LibWeb/Text/expected/layout-tree-update/tree-build-confinement.txt @@ -0,0 +1,10 @@ +append inline child: rebuilt subtree roots=1, escaped=false +two independent mutations: rebuilt subtree roots=2, escaped=false +display none to block: rebuilt subtree roots=1, escaped=false +append block into inline parent: rebuilt subtree roots=1, escaped=false +append under display:contents: rebuilt subtree roots=1, escaped=false +showModal: rebuilt subtree roots=0, escaped=true +mutate inside open dialog: rebuilt subtree roots=1, escaped=false +close dialog: rebuilt subtree roots=0, escaped=false +attach shadow root with content: rebuilt subtree roots=1, escaped=false +append abspos child: rebuilt subtree roots=1, escaped=false diff --git a/Tests/LibWeb/Text/input/layout-tree-update/tree-build-confinement.html b/Tests/LibWeb/Text/input/layout-tree-update/tree-build-confinement.html new file mode 100644 index 0000000000000..fbf091cfba1c8 --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-tree-update/tree-build-confinement.html @@ -0,0 +1,92 @@ + + + +
hello
+
world
+ +
inside contents
+

dialog

+
+