diff --git a/Libraries/LibGfx/Font/Font.cpp b/Libraries/LibGfx/Font/Font.cpp index 8c137488343d0..8e9ed26fdce27 100644 --- a/Libraries/LibGfx/Font/Font.cpp +++ b/Libraries/LibGfx/Font/Font.cpp @@ -29,6 +29,8 @@ extern "C" { float ladybird_gfx_font_glyph_width(void const*, u32); u32 ladybird_gfx_font_glyph_id(void const*, u32); +bool ladybird_gfx_font_contains_glyph(void const*, u32); +bool ladybird_gfx_font_is_emoji_font(void const*); } namespace Gfx { @@ -260,3 +262,15 @@ extern "C" u32 ladybird_gfx_font_glyph_id(void const* font, u32 code_point) VERIFY(font); return static_cast(font)->glyph_id_for_code_point(code_point); } + +extern "C" bool ladybird_gfx_font_contains_glyph(void const* font, u32 code_point) +{ + VERIFY(font); + return static_cast(font)->contains_glyph(code_point); +} + +extern "C" bool ladybird_gfx_font_is_emoji_font(void const* font) +{ + VERIFY(font); + return static_cast(font)->is_emoji_font(); +} diff --git a/Libraries/LibGfx/FontCascadeList.cpp b/Libraries/LibGfx/FontCascadeList.cpp index 46ac77acfd9f8..848cc912b8121 100644 --- a/Libraries/LibGfx/FontCascadeList.cpp +++ b/Libraries/LibGfx/FontCascadeList.cpp @@ -177,3 +177,52 @@ bool FontCascadeList::equals(FontCascadeList const& other) const } } + +extern "C" { +void const* ladybird_gfx_font_cascade_list_font_for_code_point(void const*, u32, bool, bool, bool); +void const* ladybird_gfx_font_cascade_list_first(void const*); +void ladybird_gfx_font_cascade_list_ref(void const*); +void ladybird_gfx_font_cascade_list_unref(void const*); +u8 ladybird_gfx_emoji_presentation_for_code_point(u32, u32, bool); +} + +extern "C" void const* ladybird_gfx_font_cascade_list_font_for_code_point(void const* list, u32 code_point, bool trigger_pending_loads, bool emoji_presentation, bool forced_presentation) +{ + VERIFY(list); + auto const& cascade_list = *static_cast(list); + return &cascade_list.font_for_code_point( + code_point, + trigger_pending_loads ? Gfx::FontCascadeList::TriggerPendingLoads::Yes : Gfx::FontCascadeList::TriggerPendingLoads::No, + { emoji_presentation ? Gfx::EmojiPresentation::Emoji : Gfx::EmojiPresentation::Text, + forced_presentation ? Gfx::ForcedPresentation::Yes : Gfx::ForcedPresentation::No }); +} + +extern "C" void const* ladybird_gfx_font_cascade_list_first(void const* list) +{ + VERIFY(list); + return &static_cast(list)->first(); +} + +extern "C" void ladybird_gfx_font_cascade_list_ref(void const* list) +{ + VERIFY(list); + static_cast(list)->ref(); +} + +extern "C" void ladybird_gfx_font_cascade_list_unref(void const* list) +{ + VERIFY(list); + static_cast(list)->unref(); +} + +extern "C" u8 ladybird_gfx_emoji_presentation_for_code_point(u32 code_point, u32 next_code_point, bool has_next_code_point) +{ + auto result = Gfx::emoji_presentation_for_code_point( + code_point, has_next_code_point ? Optional { next_code_point } : Optional {}); + u8 encoded = 0; + if (result.presentation == Gfx::EmojiPresentation::Emoji) + encoded |= 1; + if (result.forced == Gfx::ForcedPresentation::Yes) + encoded |= 2; + return encoded; +} diff --git a/Libraries/LibGfx/Rust/src/font.rs b/Libraries/LibGfx/Rust/src/font.rs index 42253def6e50a..255d369332e26 100644 --- a/Libraries/LibGfx/Rust/src/font.rs +++ b/Libraries/LibGfx/Rust/src/font.rs @@ -11,6 +11,23 @@ use std::ptr::NonNull; unsafe extern "C" { fn ladybird_gfx_font_glyph_width(font: *const c_void, code_point: u32) -> f32; fn ladybird_gfx_font_glyph_id(font: *const c_void, code_point: u32) -> u32; + fn ladybird_gfx_font_contains_glyph(font: *const c_void, code_point: u32) -> bool; + fn ladybird_gfx_font_is_emoji_font(font: *const c_void) -> bool; + fn ladybird_gfx_font_cascade_list_font_for_code_point( + list: *const c_void, + code_point: u32, + trigger_pending_loads: bool, + emoji_presentation: bool, + forced_presentation: bool, + ) -> *const c_void; + fn ladybird_gfx_font_cascade_list_first(list: *const c_void) -> *const c_void; + fn ladybird_gfx_font_cascade_list_ref(list: *const c_void); + fn ladybird_gfx_font_cascade_list_unref(list: *const c_void); + fn ladybird_gfx_emoji_presentation_for_code_point( + code_point: u32, + next_code_point: u32, + has_next_code_point: bool, + ) -> u8; } #[derive(Clone, Copy)] @@ -19,6 +36,14 @@ pub struct FontRef<'a> { _lifetime: PhantomData<&'a c_void>, } +impl PartialEq for FontRef<'_> { + fn eq(&self, other: &Self) -> bool { + self.raw == other.raw + } +} + +impl Eq for FontRef<'_> {} + impl<'a> FontRef<'a> { /// # Safety /// @@ -46,8 +71,125 @@ impl<'a> FontRef<'a> { unsafe { ladybird_gfx_font_glyph_id(self.raw.as_ptr(), code_point) } } + #[inline] + pub fn contains_glyph(self, code_point: u32) -> bool { + // SAFETY: FontRef's constructor requires the Gfx::Font to remain live + // for this reference's lifetime. + unsafe { ladybird_gfx_font_contains_glyph(self.raw.as_ptr(), code_point) } + } + + #[inline] + pub fn is_emoji_font(self) -> bool { + // SAFETY: FontRef's constructor requires the Gfx::Font to remain live + // for this reference's lifetime. + unsafe { ladybird_gfx_font_is_emoji_font(self.raw.as_ptr()) } + } + #[inline] pub(crate) fn as_ptr(self) -> *const c_void { self.raw.as_ptr() } + + #[inline] + pub fn as_raw(self) -> *const c_void { + self.raw.as_ptr() + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct EmojiPresentation { + pub is_emoji: bool, + pub forced: bool, +} + +pub fn emoji_presentation_for_code_point(code_point: u32, next_code_point: Option) -> EmojiPresentation { + // SAFETY: The lookup reads only immutable Unicode tables. + let encoded = unsafe { + ladybird_gfx_emoji_presentation_for_code_point( + code_point, + next_code_point.unwrap_or(0), + next_code_point.is_some(), + ) + }; + EmojiPresentation { + is_emoji: encoded & 1 != 0, + forced: encoded & 2 != 0, + } +} + +#[derive(Clone, Copy)] +pub struct FontCascadeListRef<'a> { + raw: NonNull, + _lifetime: PhantomData<&'a c_void>, +} + +impl<'a> FontCascadeListRef<'a> { + /// # Safety + /// + /// `raw` must point to a live `Gfx::FontCascadeList` for the returned + /// reference's lifetime. + #[inline] + pub unsafe fn from_raw(raw: *const c_void) -> Self { + Self { + raw: NonNull::new(raw.cast_mut()).expect("Gfx::FontCascadeList pointer must not be null"), + _lifetime: PhantomData, + } + } + + pub fn font_for_code_point( + self, + code_point: u32, + trigger_pending_loads: bool, + presentation: EmojiPresentation, + ) -> FontRef<'a> { + // SAFETY: The constructor requires the Gfx::FontCascadeList to remain + // live for this reference's lifetime, and the fonts it resolves are + // owned by it. + unsafe { + FontRef::from_raw(ladybird_gfx_font_cascade_list_font_for_code_point( + self.raw.as_ptr(), + code_point, + trigger_pending_loads, + presentation.is_emoji, + presentation.forced, + )) + } + } + + pub fn first(self) -> FontRef<'a> { + // SAFETY: The constructor requires the Gfx::FontCascadeList to remain + // live for this reference's lifetime. + unsafe { FontRef::from_raw(ladybird_gfx_font_cascade_list_first(self.raw.as_ptr())) } + } +} + +/// A strong reference to a `Gfx::FontCascadeList`, keeping the list and every +/// font it can resolve alive until dropped. +pub struct RetainedFontCascadeList { + raw: NonNull, +} + +impl RetainedFontCascadeList { + /// # Safety + /// + /// `raw` must point to a live `Gfx::FontCascadeList` at the time of the + /// call. + pub unsafe fn retain(raw: *const c_void) -> Self { + let raw = NonNull::new(raw.cast_mut()).expect("Gfx::FontCascadeList pointer must not be null"); + // SAFETY: The caller guarantees the list is live, and ref() keeps it + // that way until this reference drops. + unsafe { ladybird_gfx_font_cascade_list_ref(raw.as_ptr()) }; + Self { raw } + } + + pub fn as_raw(&self) -> *const c_void { + self.raw.as_ptr() + } +} + +impl Drop for RetainedFontCascadeList { + fn drop(&mut self) { + // SAFETY: retain() took a strong reference on construction. + unsafe { ladybird_gfx_font_cascade_list_unref(self.raw.as_ptr()) }; + } } diff --git a/Libraries/LibUnicode/Segmenter.cpp b/Libraries/LibUnicode/Segmenter.cpp index 76e35a79a0eea..12ab2faee077a 100644 --- a/Libraries/LibUnicode/Segmenter.cpp +++ b/Libraries/LibUnicode/Segmenter.cpp @@ -745,3 +745,79 @@ bool Segmenter::should_continue_beyond_word(Utf16View const& word) } } + +struct UnicodeLayoutSegmenterHandle { + NonnullOwnPtr segmenter; + Vector ascii_storage; +}; + +extern "C" { +void* unicode_layout_grapheme_segmenter_create(u16 const*, size_t); +void* unicode_layout_line_segmenter_create(u16 const*, size_t); +i64 unicode_layout_segmenter_next_boundary(void*, size_t, bool); +void unicode_layout_segmenter_destroy(void*); +} + +static Unicode::Segmenter& unicode_layout_segmenter_prototype(Unicode::SegmenterGranularity granularity) +{ + static thread_local Unicode::Segmenter* grapheme_prototype = nullptr; + static thread_local Unicode::Segmenter* line_prototype = nullptr; + auto& prototype = granularity == Unicode::SegmenterGranularity::Grapheme ? grapheme_prototype : line_prototype; + if (!prototype) + prototype = Unicode::Segmenter::create(granularity).leak_ptr(); + return *prototype; +} + +extern "C" void* unicode_layout_grapheme_segmenter_create(u16 const* text, size_t length_in_code_units) +{ + auto view = Utf16View { reinterpret_cast(text), length_in_code_units }; + if (view.is_ascii()) { + return new UnicodeLayoutSegmenterHandle { + .segmenter = Unicode::Segmenter::create_for_ascii_grapheme(length_in_code_units), + .ascii_storage = {}, + }; + } + auto segmenter = unicode_layout_segmenter_prototype(Unicode::SegmenterGranularity::Grapheme).clone(); + segmenter->set_segmented_text(view); + return new UnicodeLayoutSegmenterHandle { .segmenter = move(segmenter), .ascii_storage = {} }; +} + +extern "C" void* unicode_layout_line_segmenter_create(u16 const* text, size_t length_in_code_units) +{ + auto view = Utf16View { reinterpret_cast(text), length_in_code_units }; + if (view.is_ascii()) { + Vector ascii_storage; + ascii_storage.ensure_capacity(length_in_code_units); + for (size_t index = 0; index < length_in_code_units; ++index) + ascii_storage.unchecked_append(static_cast(text[index])); + auto ascii_view = ascii_storage.is_empty() + ? Utf16View {} + : Utf16View { StringView { ascii_storage.data(), ascii_storage.size() } }; + if (auto segmenter = Unicode::Segmenter::try_create_for_ascii_line(ascii_view)) { + return new UnicodeLayoutSegmenterHandle { + .segmenter = segmenter.release_nonnull(), + .ascii_storage = move(ascii_storage), + }; + } + } + auto segmenter = unicode_layout_segmenter_prototype(Unicode::SegmenterGranularity::Line).clone(); + segmenter->set_segmented_text(view); + return new UnicodeLayoutSegmenterHandle { .segmenter = move(segmenter), .ascii_storage = {} }; +} + +extern "C" i64 unicode_layout_segmenter_next_boundary(void* handle, size_t index, bool inclusive) +{ + VERIFY(handle); + auto& segmenter = *static_cast(handle)->segmenter; + auto boundary = segmenter.next_boundary( + index, inclusive ? Unicode::Segmenter::Inclusive::Yes : Unicode::Segmenter::Inclusive::No); + if (!boundary.has_value()) + return -1; + return static_cast(*boundary); +} + +extern "C" void unicode_layout_segmenter_destroy(void* handle) +{ + VERIFY(handle); + delete static_cast(handle); +} diff --git a/Libraries/LibWeb/HTML/HTMLElement.cpp b/Libraries/LibWeb/HTML/HTMLElement.cpp index dbc8111603646..7c7571a2a519b 100644 --- a/Libraries/LibWeb/HTML/HTMLElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLElement.cpp @@ -348,13 +348,27 @@ static Vector> rendered_text_collec if (auto const* layout_text_node = as_if(layout_node)) { Layout::TextOffsetMapping mapping { layout_text_node->dom_node() }; mapping.for_each_fragment([&](Layout::TextNode const& slice) { - Layout::TextNode::ChunkIterator iterator { slice, false, false }; - while (true) { - auto chunk = iterator.next(); - if (!chunk.has_value()) - break; - items.append(Utf16String::from_utf16(chunk.release_value().view)); + // Collapse whitespace like the text chunker feeding inline layout + // does: within each run of collapsible whitespace, every code + // point after the first is removed. + auto const& text = slice.text_for_rendering(); + auto should_collapse_whitespace = first_is_one_of( + slice.parent()->computed_values().white_space_collapse(), + CSS::WhiteSpaceCollapse::Collapse, CSS::WhiteSpaceCollapse::PreserveBreaks); + if (!should_collapse_whitespace) { + items.append(text); + return; + } + Utf16StringBuilder builder { text.length_in_code_units() }; + bool previous_code_unit_is_collapsible = false; + for (size_t index = 0; index < text.length_in_code_units(); ++index) { + auto code_unit = text.utf16_view().code_unit_at(index); + auto is_collapsible = code_unit < 0x80 && is_ascii_space(code_unit); + if (!is_collapsible || !previous_code_unit_is_collapsible) + builder.append_code_unit(code_unit); + previous_code_unit_is_collapsible = is_collapsible; } + items.append(builder.to_string()); }); return items; } diff --git a/Libraries/LibWeb/Layout/LayoutRustBridge.cpp b/Libraries/LibWeb/Layout/LayoutRustBridge.cpp index 20b6387134884..8e4867ac5097b 100644 --- a/Libraries/LibWeb/Layout/LayoutRustBridge.cpp +++ b/Libraries/LibWeb/Layout/LayoutRustBridge.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -17,7 +18,7 @@ #include #include #include -#include +#include #include #include #include @@ -59,6 +60,7 @@ #include #include #include +#include #include #include #include @@ -92,11 +94,6 @@ static Atomic s_outstanding_grid_name_handles; static Atomic s_outstanding_anchor_name_handles; static Atomic s_outstanding_svg_path_handles; -struct TextFactsSnapshotArena { - Vector text; - Vector chunks; -}; - static constexpr size_t style_field_encoding_width(RustFFI::FfiStyleFieldEncoding encoding) { switch (encoding) { @@ -130,6 +127,8 @@ static_assert(to_underlying(CSS::StyleGroupIndex::Count) == RustFFI::STYLE_GROUP F(TextJustify, CSS::ComputedValues::InheritedTextValues, text_justify, offsetof(CSS::ComputedValues::InheritedTextValues, text_justify), U8) \ F(WhiteSpaceCollapse, CSS::ComputedValues::InheritedTextValues, white_space_collapse, offsetof(CSS::ComputedValues::InheritedTextValues, white_space_collapse), U8) \ F(TextWrapMode, CSS::ComputedValues::InheritedTextValues, text_wrap_mode, offsetof(CSS::ComputedValues::InheritedTextValues, text_wrap_mode), U8) \ + F(WordBreak, CSS::ComputedValues::InheritedTextValues, word_break, offsetof(CSS::ComputedValues::InheritedTextValues, word_break), U8) \ + F(FontVariantEmoji, CSS::ComputedValues::FontValues, font_variant_emoji, offsetof(CSS::ComputedValues::FontValues, font_variant_emoji), U8) \ F(LineHeight, CSS::ComputedValues::FontValues, line_height.used_value, offsetof(CSS::ComputedValues::FontValues, line_height) + offsetof(CSS::LineHeightData, used_value), CssPixels) \ F(FontSize, CSS::ComputedValues::FontValues, font_size, offsetof(CSS::ComputedValues::FontValues, font_size), CssPixels) \ F(BoxSizing, CSS::ComputedValues::BoxValues, box_sizing, offsetof(CSS::ComputedValues::BoxValues, box_sizing), U8) \ @@ -1162,6 +1161,7 @@ void LayoutRustBridge::run_root_layout(Box& viewport, NodeWithStyleAndBoxModelMe }; viewport.document().invalidate_stacking_context_tree(); + viewport.document().layout_node_arena().sync_enrolled_text_node_content(); auto callbacks = formatting_context_callbacks(); auto sink = commit_sink(); RustFFI::rust_layout_run_root_layout( @@ -1184,6 +1184,7 @@ void LayoutRustBridge::compute_subtree_layout(Box& root, Painting::Paintable& pa }; root.document().invalidate_stacking_context_tree(); + root.document().layout_node_arena().sync_enrolled_text_node_content(); auto callbacks = formatting_context_callbacks(); auto sink = commit_sink(); RustFFI::rust_layout_compute_subtree_layout( @@ -1204,6 +1205,7 @@ void LayoutRustBridge::replay_saved_abspos_layout(Box& box, Painting::Paintable& }; box.document().invalidate_stacking_context_tree(); + box.document().layout_node_arena().sync_enrolled_text_node_content(); auto callbacks = formatting_context_callbacks(); auto sink = commit_sink(); RustFFI::rust_layout_replay_saved_abspos_layout(Node::slot_id(&box), &paintable_to_replace, &callbacks, &sink); @@ -1737,10 +1739,6 @@ RustFFI::FfiLayoutFcCallbacks LayoutRustBridge::formatting_context_callbacks() .initial_containing_block_inline_size = m_commit_root->document().viewport_rect().width().raw_value(), .document_in_quirks_mode = m_commit_root->document().in_quirks_mode(), .static_position_containing_block = [](void*, void* node) { return Node::slot_id(static_cast(node)->static_position_containing_block()); }, - .dom_node_is_inclusive_ancestor = [](void*, void* ancestor, void* node) { - auto const* ancestor_dom_node = static_cast(ancestor)->dom_node(); - auto const* dom_node = static_cast(node)->dom_node(); - return ancestor_dom_node && dom_node && ancestor_dom_node->is_inclusive_ancestor_of(*dom_node); }, .needs_inset_resolution = [](void*, void* node) { auto const& styled_node = *static_cast(node); if (styled_node.computed_values().position() == CSS::Positioning::Relative) @@ -1834,55 +1832,10 @@ RustFFI::FfiLayoutFcCallbacks LayoutRustBridge::formatting_context_callbacks() facts.marker_list_style_position = static_cast(to_underlying(marker->list_style_position())); } return facts; }, - .build_text_facts = [](void*, void* node, bool should_wrap_lines, bool should_respect_linebreaks, bool unidirectional_ltr, RustFFI::FfiTextNodeFacts* out) { - VERIFY(out); - auto const* text_node = as_if(*static_cast(node)); - if (!text_node) - return false; - - auto text_direction_mode = unidirectional_ltr - ? TextNode::TextDirectionMode::UnidirectionalLeftToRight - : TextNode::TextDirectionMode::PerCodePoint; - auto const& chunk_list = text_node->chunks_for_layout(should_wrap_lines, should_respect_linebreaks, text_direction_mode); - auto arena = make(); - auto const text = text_node->text_for_rendering().utf16_view(); - arena->text.ensure_capacity(text.length_in_code_units()); - for (size_t index = 0; index < text.length_in_code_units(); ++index) - arena->text.unchecked_append(text.code_unit_at(index)); - arena->chunks.ensure_capacity(chunk_list.chunks.size()); - for (auto const& chunk : chunk_list.chunks) { - arena->chunks.unchecked_append({ - .start = chunk.start, - .length = chunk.length, - .font = chunk.font.ptr(), - .has_breaking_newline = chunk.has_breaking_newline, - .has_breaking_tab = chunk.has_breaking_tab, - .is_all_whitespace = chunk.is_all_whitespace, - .can_break_after = chunk.can_break_after, - .text_type = static_cast(to_underlying(chunk.text_type)), - }); - } - - auto* owner = arena.leak_ptr(); - *out = { - .text_utf16 = owner->text.data(), - .text_length_in_code_units = owner->text.size(), - .chunks = owner->chunks.data(), - .chunk_count = owner->chunks.size(), - .should_collapse_whitespace = chunk_list.should_collapse_whitespace, - .is_generated_for_pseudo_element = text_node->is_generated_for_pseudo_element(), - .is_empty_editable = is_empty_editable_text_node(*text_node), - .has_dom_node = static_cast(*text_node).dom_node() != nullptr, - .retained = owner, - }; - return true; }, - .release_text_facts = [](void*, void* retained) { - VERIFY(retained); - delete static_cast(retained); }, - .text_may_require_bidi_processing = [](void*, void* node) { + .text_node_is_empty_editable = [](void*, void* node) { auto const* text_node = as_if(*static_cast(node)); VERIFY(text_node); - return Unicode::may_require_bidi_processing(text_node->text_for_rendering()); }, + return is_empty_editable_text_node(*text_node); }, .document_cursor_is_on_node = [](void*, void* node) { auto const* dom_node = static_cast(node)->dom_node(); if (!dom_node) @@ -2231,6 +2184,7 @@ static RustFFI::FfiResidualStyleValues decode_residual_style(RustFFI::FfiStylePa auto const& font = font_values.font_list->font_for_code_point(' '); auto const& metrics = font.pixel_metrics(); result.first_available_font = &font; + result.font_cascade_list = font_values.font_list.ptr(); result.font_ascent = metrics.ascent; result.font_descent = metrics.descent; result.font_x_height = metrics.x_height; @@ -2289,3 +2243,36 @@ extern "C" WEB_API void ladybird_layout_release_anchor_name_handle(size_t raw) --Web::Layout::s_outstanding_anchor_name_handles; Utf16FlyString::unref_raw(raw); } + +extern "C" WEB_API u8 ladybird_layout_text_type_for_code_point(u32 code_point) +{ + return static_cast(to_underlying(Web::Layout::text_type_for_code_point(code_point))); +} + +extern "C" WEB_API bool ladybird_layout_code_point_has_break_all_line_break_class(u32 code_point) +{ + return first_is_one_of(Unicode::line_break_class(code_point), + Unicode::LineBreakClass::Alphabetic, + Unicode::LineBreakClass::Numeric, + Unicode::LineBreakClass::ComplexContext, + Unicode::LineBreakClass::Ideographic); +} + +extern "C" WEB_API bool ladybird_layout_code_point_has_keep_all_line_break_class(u32 code_point) +{ + return first_is_one_of(Unicode::line_break_class(code_point), + Unicode::LineBreakClass::Alphabetic, + Unicode::LineBreakClass::Numeric, + Unicode::LineBreakClass::Ambiguous, + Unicode::LineBreakClass::Ideographic); +} + +extern "C" WEB_API bool ladybird_layout_code_point_has_combining_mark_line_break_class(u32 code_point) +{ + return Unicode::line_break_class(code_point) == Unicode::LineBreakClass::CombiningMark; +} + +extern "C" WEB_API bool ladybird_layout_code_point_has_emoji_property(u32 code_point) +{ + return Unicode::code_point_has_emoji_property(code_point); +} diff --git a/Libraries/LibWeb/Layout/LayoutRustBridge.h b/Libraries/LibWeb/Layout/LayoutRustBridge.h index 1f3aa58b1fd89..b185735db6e24 100644 --- a/Libraries/LibWeb/Layout/LayoutRustBridge.h +++ b/Libraries/LibWeb/Layout/LayoutRustBridge.h @@ -75,3 +75,11 @@ extern "C" WEB_API void ladybird_layout_release_grid_name_handle(size_t); // Releases one position-anchor name reference transferred by a lazy style // field decode. extern "C" WEB_API void ladybird_layout_release_anchor_name_handle(size_t); + +// Per-code-point classification lookups for the Rust text chunker. The +// line-break-class groupings implement the css-text-4 word-break policies. +extern "C" WEB_API u8 ladybird_layout_text_type_for_code_point(u32); +extern "C" WEB_API bool ladybird_layout_code_point_has_break_all_line_break_class(u32); +extern "C" WEB_API bool ladybird_layout_code_point_has_keep_all_line_break_class(u32); +extern "C" WEB_API bool ladybird_layout_code_point_has_combining_mark_line_break_class(u32); +extern "C" WEB_API bool ladybird_layout_code_point_has_emoji_property(u32); diff --git a/Libraries/LibWeb/Layout/Node.cpp b/Libraries/LibWeb/Layout/Node.cpp index 200fcb40add8a..7e330cbb4917f 100644 --- a/Libraries/LibWeb/Layout/Node.cpp +++ b/Libraries/LibWeb/Layout/Node.cpp @@ -1074,6 +1074,11 @@ void NodeWithStyle::set_computed_values(NonnullRefPtr { m_computed_values = move(computed_values); mirror_computed_values_to_node_data(); + + for (auto* child = first_child_ptr(); child; child = child->next_sibling_ptr()) { + if (auto* text_child = as_if(*child)) + text_child->enroll_for_arena_text_content_sync(); + } } void NodeWithStyle::mirror_computed_values_to_node_data() diff --git a/Libraries/LibWeb/Layout/Node.h b/Libraries/LibWeb/Layout/Node.h index 522b0bae5fab9..23ef0f548b045 100644 --- a/Libraries/LibWeb/Layout/Node.h +++ b/Libraries/LibWeb/Layout/Node.h @@ -105,6 +105,7 @@ class WEB_API Node static RustFFI::NodeSlotId slot_id(Node const*); u32 arena_slot_index() const { return m_slot.index; } void* arena_handle() const; + NodeArena& node_arena() const { return *m_arena; } bool is_anonymous() const { return has_flag(RustFFI::NodeFlag::Anonymous); } DOM::Node const* dom_node() const; diff --git a/Libraries/LibWeb/Layout/NodeArena.cpp b/Libraries/LibWeb/Layout/NodeArena.cpp index 7e902dc75d0ae..bb9b9455f40d2 100644 --- a/Libraries/LibWeb/Layout/NodeArena.cpp +++ b/Libraries/LibWeb/Layout/NodeArena.cpp @@ -6,6 +6,7 @@ #include #include +#include namespace Web::Layout { @@ -32,4 +33,30 @@ void NodeArena::free(RustFFI::NodeSlotId slot, u32 generation) RustFFI::layout_arena_free(m_handle, slot, generation); } +void NodeArena::enroll_text_node_for_content_sync(TextNode const& text_node) +{ + m_text_nodes_enrolled_for_content_sync.append(text_node.make_weak_ptr()); +} + +void NodeArena::sync_enrolled_text_node_content() +{ + if (m_text_nodes_enrolled_for_content_sync.is_empty()) + return; + // A node that is alive but detached keeps its enrollment: it cannot + // resolve style-dependent text without a parent, and it may be reinserted + // by a later tree update without another enrollment trigger. + Vector> still_detached_text_nodes; + for (auto& weak_text_node : m_text_nodes_enrolled_for_content_sync) { + auto const* text_node = weak_text_node.ptr(); + if (!text_node) + continue; + if (!text_node->parent()) { + still_detached_text_nodes.append(move(weak_text_node)); + continue; + } + text_node->sync_text_content_to_arena(); + } + m_text_nodes_enrolled_for_content_sync = move(still_detached_text_nodes); +} + } diff --git a/Libraries/LibWeb/Layout/NodeArena.h b/Libraries/LibWeb/Layout/NodeArena.h index d282caf9d8da0..b241bc7069e8e 100644 --- a/Libraries/LibWeb/Layout/NodeArena.h +++ b/Libraries/LibWeb/Layout/NodeArena.h @@ -9,11 +9,15 @@ #include #include #include +#include +#include #include #include namespace Web::Layout { +class TextNode; + static_assert(sizeof(RustFFI::NodeAllocation) == 24); static_assert(offsetof(RustFFI::NodeAllocation, slot) == 0); static_assert(offsetof(RustFFI::NodeAllocation, data) == 8); @@ -31,8 +35,12 @@ class WEB_API NodeArena : public RefCounted { void free(RustFFI::NodeSlotId, u32 generation); void* handle() const { return m_handle; } + void enroll_text_node_for_content_sync(TextNode const&); + void sync_enrolled_text_node_content(); + private: void* m_handle { nullptr }; + Vector> m_text_nodes_enrolled_for_content_sync; }; } diff --git a/Libraries/LibWeb/Layout/TextNode.cpp b/Libraries/LibWeb/Layout/TextNode.cpp index 29dfe6496fa3d..289848f80acfb 100644 --- a/Libraries/LibWeb/Layout/TextNode.cpp +++ b/Libraries/LibWeb/Layout/TextNode.cpp @@ -10,9 +10,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -22,16 +24,19 @@ namespace Web::Layout { TextNode::TextNode(DOM::Document& document, DOM::Text& text) : Node(document, &text) { + enroll_for_arena_text_content_sync(); } TextNode::TextNode(DOM::Document& document, DOM::Text& text, AttachToDOMNode attach_to_dom_node) : Node(document, &text, attach_to_dom_node) { + enroll_for_arena_text_content_sync(); } TextNode::TextNode(DOM::Document& document) : Node(document, nullptr) { + enroll_for_arena_text_content_sync(); } TextNode::~TextNode() = default; @@ -360,6 +365,8 @@ TextNode::TextForRenderingCacheKey TextNode::create_text_for_rendering_cache_key void TextNode::invalidate_text_for_rendering() { m_text_dependent_cache = {}; + m_arena_text_content_in_sync = false; + enroll_for_arena_text_content_sync(); } Utf16String const& TextNode::text_for_rendering() const @@ -376,13 +383,38 @@ TextNode::TextDependentCache const& TextNode::ensure_text_dependent_cache() cons .key = move(key), .text_for_rendering = move(text_for_rendering), .grapheme_segmenter = {}, - .line_segmenter = {}, - .chunk_cache = {}, }; + m_arena_text_content_in_sync = false; + enroll_for_arena_text_content_sync(); } return *m_text_dependent_cache; } +void TextNode::enroll_for_arena_text_content_sync() const +{ + if (m_enrolled_for_arena_text_content_sync) + return; + m_enrolled_for_arena_text_content_sync = true; + node_arena().enroll_text_node_for_content_sync(*this); +} + +void TextNode::sync_text_content_to_arena() const +{ + ensure_text_dependent_cache(); + m_enrolled_for_arena_text_content_sync = false; + if (m_arena_text_content_in_sync) + return; + auto view = m_text_dependent_cache->text_for_rendering.utf16_view(); + RustFFI::layout_arena_set_text_content( + arena_handle(), + slot_id(this), + view.has_ascii_storage() ? reinterpret_cast(view.ascii_span().data()) : nullptr, + view.has_ascii_storage() ? nullptr : reinterpret_cast(view.utf16_span().data()), + view.length_in_code_units(), + Unicode::may_require_bidi_processing(view)); + m_arena_text_content_in_sync = true; +} + Utf16String TextNode::compute_text_for_rendering(TextForRenderingCacheKey const& cache_key) const { auto const& text_data = text(); @@ -440,7 +472,7 @@ Utf16String TextNode::compute_text_for_rendering(TextForRenderingCacheKey const& // 4. Any collapsible space immediately following another collapsible space—even one outside the boundary of the // inline containing that space, provided both spaces are within the same inline formatting context—is // collapsed to have zero advance width. (It is invisible, but retains its soft wrap opportunity, if any.) - // AD-HOC: This is handled by TextNode::ChunkIterator by removing the space. + // AD-HOC: This is handled by the text chunker by removing the space. } // If white-space-collapse is set to preserve-spaces, each tab and segment break is converted to a space. @@ -454,8 +486,8 @@ Utf16String TextNode::compute_text_for_rendering(TextForRenderingCacheKey const& return " "_utf16; } - // AD-HOC: It's important to not change the amount of code units in the resulting transformed text, so ChunkIterator - // can pass views to this string with associated code unit offsets that still match the original text. + // AD-HOC: It's important to not change the amount of code units in the resulting transformed text, so the text + // chunker can produce code unit offsets that still match the original text. if (convert_newlines || convert_tabs) { Utf16StringBuilder text_builder { text.length_in_code_units() }; for (auto code_point : text) { @@ -487,91 +519,7 @@ Unicode::Segmenter& TextNode::grapheme_segmenter() const return *cache.grapheme_segmenter; } -Unicode::Segmenter& TextNode::line_segmenter() const -{ - auto const& cache = ensure_text_dependent_cache(); - auto const& text = cache.text_for_rendering; - if (!cache.line_segmenter) { - if (auto ascii = Unicode::Segmenter::try_create_for_ascii_line(text.utf16_view())) { - cache.line_segmenter = ascii.release_nonnull(); - } else { - cache.line_segmenter = document().line_segmenter().clone(); - cache.line_segmenter->set_segmented_text(text); - } - } - - return *cache.line_segmenter; -} - -TextNode::ChunkList const& TextNode::chunks_for_layout(bool should_wrap_lines, bool should_respect_linebreaks, TextDirectionMode text_direction_mode) const -{ - auto const& cache = ensure_text_dependent_cache(); - - auto const& computed_values = parent()->computed_values(); - ChunkCacheKey key { - .should_wrap_lines = should_wrap_lines, - .should_respect_linebreaks = should_respect_linebreaks, - .white_space_collapse = computed_values.white_space_collapse(), - .word_break = computed_values.word_break(), - .font_variant_emoji = computed_values.font_variant_emoji(), - .text_direction_mode = text_direction_mode, - .font_cascade_list = computed_values.font_list(), - }; - - if (cache.chunk_cache.has_value() && cache.chunk_cache->key == key) - return cache.chunk_cache->chunk_list; - - TextNode::ChunkIterator chunk_iterator { *this, text_direction_mode, should_wrap_lines, should_respect_linebreaks }; - Vector chunks; - while (true) { - auto chunk = chunk_iterator.next(); - if (!chunk.has_value()) - break; - chunks.append(chunk.release_value()); - } - - cache.chunk_cache = ChunkCacheEntry { - .key = key, - .chunk_list = { - .chunks = move(chunks), - .should_collapse_whitespace = chunk_iterator.should_collapse_whitespace(), - }, - }; - return cache.chunk_cache->chunk_list; -} - -static bool is_interword_space(u32 code_point) -{ - return code_point == 0x0020 || code_point == 0x00A0; -} - -TextNode::ChunkIterator::ChunkIterator(TextNode const& text_node, bool should_wrap_lines, bool should_respect_linebreaks) - : ChunkIterator(text_node, TextDirectionMode::PerCodePoint, should_wrap_lines, should_respect_linebreaks) -{ -} - -TextNode::ChunkIterator::ChunkIterator(TextNode const& text_node, TextDirectionMode text_direction_mode, bool should_wrap_lines, bool should_respect_linebreaks) - : ChunkIterator(text_node, text_node.text_for_rendering(), text_node.grapheme_segmenter(), text_node.line_segmenter(), text_node.parent()->computed_values().word_break(), text_direction_mode, should_wrap_lines, should_respect_linebreaks) -{ -} - -TextNode::ChunkIterator::ChunkIterator(TextNode const& text_node, Utf16View const& text, - Unicode::Segmenter& grapheme_segmenter, Unicode::Segmenter& line_segmenter, CSS::WordBreak word_break, - TextDirectionMode text_direction_mode, bool should_wrap_lines, bool should_respect_linebreaks) - : m_should_wrap_lines(should_wrap_lines) - , m_should_respect_linebreaks(should_respect_linebreaks) - , m_view(text) - , m_font_cascade_list(text_node.parent()->computed_values().font_list()) - , m_grapheme_segmenter(grapheme_segmenter) - , m_line_segmenter(line_segmenter) - , m_word_break(word_break) - , m_font_variant_emoji(text_node.parent()->computed_values().font_variant_emoji()) - , m_text_direction_mode(text_direction_mode) -{ - m_should_collapse_whitespace = first_is_one_of(text_node.parent()->computed_values().white_space_collapse(), CSS::WhiteSpaceCollapse::Collapse, CSS::WhiteSpaceCollapse::PreserveBreaks); -} - -static Gfx::GlyphRun::TextType text_type_for_code_point(u32 code_point) +Gfx::GlyphRun::TextType text_type_for_code_point(u32 code_point) { // Fast path for ASCII using a lookup table. // Each ASCII character has a statically known bidi class. @@ -642,317 +590,6 @@ static Gfx::GlyphRun::TextType text_type_for_code_point(u32 code_point) } } -Optional TextNode::ChunkIterator::next() -{ - if (!m_peek_queue.is_empty()) - return m_peek_queue.take_first(); - return next_without_peek(); -} - -Optional TextNode::ChunkIterator::peek(size_t count) -{ - while (m_peek_queue.size() <= count) { - auto next = next_without_peek(); - if (!next.has_value()) - return {}; - m_peek_queue.append(*next); - } - - return m_peek_queue[count]; -} - -TextNode::Chunk TextNode::ChunkIterator::create_empty_chunk() -{ - return TextNode::Chunk { - .view = {}, - .font = m_font_cascade_list.first(), - .is_all_whitespace = true, - .text_type = Gfx::GlyphRun::TextType::Common, - }; -} - -bool TextNode::ChunkIterator::is_at_line_break_opportunity() const -{ - auto has_break_all_class = [](u32 code_point) { - return first_is_one_of(Unicode::line_break_class(code_point), - Unicode::LineBreakClass::Alphabetic, - Unicode::LineBreakClass::Numeric, - Unicode::LineBreakClass::ComplexContext, - Unicode::LineBreakClass::Ideographic); - }; - - auto has_keep_all_class = [](u32 code_point) { - return first_is_one_of(Unicode::line_break_class(code_point), - Unicode::LineBreakClass::Alphabetic, - Unicode::LineBreakClass::Numeric, - Unicode::LineBreakClass::Ambiguous, - Unicode::LineBreakClass::Ideographic); - }; - - auto get_previous_code_point = [this]() -> Optional { - if (m_current_index == 0) - return {}; - size_t current_index = m_current_index; - auto previous_code_point = m_view.previous_code_point_at(current_index); - while (Unicode::line_break_class(previous_code_point) == Unicode::LineBreakClass::CombiningMark && current_index > 0) - previous_code_point = m_view.previous_code_point_at(current_index); - return previous_code_point; - }; - - if (!m_should_wrap_lines) - return false; - - auto is_at_line_segmenter_boundary = [this]() { - auto boundary = m_line_segmenter.next_boundary(m_current_index, Unicode::Segmenter::Inclusive::Yes); - return boundary.has_value() && boundary.value() == m_current_index; - }; - - // https://drafts.csswg.org/css-text-4/#word-break-property - // This property specifies soft wrap opportunities between and within “words”, i.e. where it is “normal” and - // permissible to break lines of text. It focuses on breaks between letters, and does not define whether and how - // soft wrap opportunities are created by white space and other space separators (though auto-phrase may suppress - // some), nor around punctuation. - switch (m_word_break) { - case CSS::WordBreak::Normal: - // https://drafts.csswg.org/css-text-4/#valdef-word-break-normal - // Words break according to their customary rules, as described above. Korean, which commonly exhibits two - // different behaviors, allows breaks between any two consecutive Hangul/Hanja. For Ethiopic, which also - // exhibits two different behaviors, such breaks within words are not allowed. - case CSS::WordBreak::BreakWord: - // https://drafts.csswg.org/css-text-4/#valdef-word-break-break-word - // For compatibility with legacy content, the word-break property also supports a deprecated break-word - // keyword. When specified, this has the same effect as word-break: normal and overflow-wrap: anywhere, - // regardless of the actual value of the overflow-wrap property. - return is_at_line_segmenter_boundary(); - case CSS::WordBreak::BreakAll: { - // https://drafts.csswg.org/css-text-4/#valdef-word-break-break-all - // Breaking is allowed within “words”: specifically, in addition to soft wrap opportunities allowed for normal, - // any typographic letter units (and any typographic character units resolving to the NU (“numeric”), - // AL (“alphabetic”), or SA (“Southeast Asian”) line breaking classes [UAX14]) are instead treated as ID - // (“ideographic characters”) for the purpose of line-breaking. Hyphenation is not applied. - if (m_current_index >= m_view.length_in_code_units()) - return false; - auto previous_code_point = get_previous_code_point(); - if (previous_code_point.has_value() && has_break_all_class(*previous_code_point) && has_break_all_class(m_view.code_point_at(m_current_index))) - return true; - return is_at_line_segmenter_boundary(); - } - case CSS::WordBreak::KeepAll: { - // https://drafts.csswg.org/css-text-4/#valdef-word-break-keep-all - // Breaking is forbidden within “words”: implicit soft wrap opportunities between typographic letter units - // (or other typographic character units belonging to the NU, AL, AI, or ID Unicode line breaking classes [UAX14]) - // are suppressed, i.e. breaks are prohibited between pairs of such characters (regardless of line-break - // settings other than anywhere) except where opportunities exist due to §6.1.1.1 Lexical Word Breaking. - // Otherwise this option is equivalent to normal. In this style, sequences of CJK characters do not break. - if (m_current_index >= m_view.length_in_code_units()) - return false; - auto previous_code_point = get_previous_code_point(); - if (previous_code_point.has_value() && has_keep_all_class(*previous_code_point) && has_keep_all_class(m_view.code_point_at(m_current_index))) - return false; - return is_at_line_segmenter_boundary(); - } - } - VERIFY_NOT_REACHED(); -} - -Gfx::Font const& TextNode::ChunkIterator::font_for_space(size_t at_index, u32 space_code_point) const -{ - auto has_glyph = [&](Gfx::Font const& font) { return font.contains_glyph(space_code_point); }; - - // 1. Prefer the last non-whitespace font in this node/run. - if (m_last_non_whitespace_font && !m_last_non_whitespace_font->is_emoji_font() && has_glyph(*m_last_non_whitespace_font)) - return *m_last_non_whitespace_font; - - // 2. Look ahead to the next non-space to infer the base font of this run. - for (size_t i = at_index; i < m_view.length_in_code_units();) { - auto cp = m_view.code_point_at(i); - if (!is_interword_space(cp) && cp != '\t' && cp != '\n') { - auto const& font = m_font_cascade_list.font_for_code_point(cp, Gfx::FontCascadeList::TriggerPendingLoads::Yes, emoji_presentation_at(i, cp)); - if (!font.is_emoji_font() && has_glyph(font)) - return font; - // Text is coming from an emoji face; we'll fall back to (3). - break; - } - i = m_grapheme_segmenter.next_boundary(i).value_or(m_view.length_in_code_units()); - } - - // 3. No text around (leading/trailing/all spaces) — pick a font with the glyph from the cascade. - return m_font_cascade_list.font_for_code_point(space_code_point, Gfx::FontCascadeList::TriggerPendingLoads::Yes); -} - -Gfx::EmojiPresentationResult TextNode::ChunkIterator::emoji_presentation_at(size_t code_unit_offset, u32 code_point) const -{ - auto next_offset = code_unit_offset + AK::UnicodeUtils::code_unit_length_for_code_point(code_point); - Optional next_code_point; - if (next_offset < m_view.length_in_code_units()) - next_code_point = m_view.code_point_at(next_offset); - - auto default_presentation = Gfx::emoji_presentation_for_code_point(code_point, next_code_point); - - if (default_presentation.forced == Gfx::ForcedPresentation::Yes || !Unicode::code_point_has_emoji_property(code_point)) - return default_presentation; - - switch (m_font_variant_emoji) { - case CSS::FontVariantEmoji::Text: - return { Gfx::EmojiPresentation::Text, Gfx::ForcedPresentation::Yes }; - case CSS::FontVariantEmoji::Emoji: - return { Gfx::EmojiPresentation::Emoji, Gfx::ForcedPresentation::Yes }; - case CSS::FontVariantEmoji::Unicode: - return { default_presentation.presentation, Gfx::ForcedPresentation::Yes }; - case CSS::FontVariantEmoji::Normal: - return default_presentation; - } - VERIFY_NOT_REACHED(); -} - -Optional TextNode::ChunkIterator::next_without_peek() -{ - if (m_current_index >= m_view.length_in_code_units()) - return {}; - - auto current_code_point = [this] { - return m_view.code_point_at(m_current_index); - }; - auto current_text_type = [this] { - if (m_text_direction_mode == TextDirectionMode::UnidirectionalLeftToRight) - return Gfx::GlyphRun::TextType::Ltr; - return text_type_for_code_point(m_view.code_point_at(m_current_index)); - }; - auto next_grapheme_boundary = [this] { - return m_grapheme_segmenter.next_boundary(m_current_index).value_or(m_view.length_in_code_units()); - }; - - // https://drafts.csswg.org/css-text-4/#collapsible-white-space - auto is_collapsible = [this](u32 code_point) { - return m_should_collapse_whitespace && is_ascii_space(code_point); - }; - - auto code_point = current_code_point(); - auto can_break_at_current_position = is_at_line_break_opportunity(); - auto start_of_chunk = m_current_index; - - auto const& expected_font_for = [&](u32 cp) -> Gfx::Font const& { - return is_interword_space(cp) - ? font_for_space(m_current_index, cp) - : m_font_cascade_list.font_for_code_point(cp, Gfx::FontCascadeList::TriggerPendingLoads::Yes, emoji_presentation_at(m_current_index, cp)); - }; - - auto const& font = expected_font_for(current_code_point()); - auto text_type = current_text_type(); - - auto broken_on_tab = false; - - while (m_current_index < m_view.length_in_code_units()) { - code_point = current_code_point(); - - if (code_point == '\t') { - if (auto result = try_commit_chunk(start_of_chunk, m_current_index, false, broken_on_tab, false, font, text_type); result.has_value()) - return result.release_value(); - - broken_on_tab = true; - // consume any consecutive tabs - while (m_current_index < m_view.length_in_code_units() && current_code_point() == '\t') - m_current_index = next_grapheme_boundary(); - can_break_at_current_position = is_at_line_break_opportunity(); - } - - auto const& expected_font = expected_font_for(code_point); - - if (&font != &expected_font) { - if (auto result = try_commit_chunk(start_of_chunk, m_current_index, false, broken_on_tab, can_break_at_current_position, font, text_type); result.has_value()) - return result.release_value(); - } - - if (m_should_respect_linebreaks && code_point == '\n') { - // Newline encountered, and we're supposed to preserve them. - // If we have accumulated some code points in the current chunk, commit them now and continue with the newline next time. - if (auto result = try_commit_chunk(start_of_chunk, m_current_index, false, broken_on_tab, false, font, text_type); result.has_value()) - return result.release_value(); - - // Otherwise, commit the newline! - m_current_index = next_grapheme_boundary(); - auto result = try_commit_chunk(start_of_chunk, m_current_index, true, broken_on_tab, false, font, text_type); - VERIFY(result.has_value()); - return result.release_value(); - } - - // If both this code point and the previous code point are collapsible, skip code points until we're at a non- - // collapsible code point. - if (is_collapsible(code_point) && m_current_index > 0 && is_collapsible(m_view.code_point_at(m_current_index - 1))) { - auto result = try_commit_chunk(start_of_chunk, m_current_index, false, broken_on_tab, false, font, text_type); - - while (m_current_index < m_view.length_in_code_units() && is_collapsible(current_code_point())) - m_current_index = next_grapheme_boundary(); - - if (result.has_value()) - return result.release_value(); - - return next_without_peek(); - } - - if (m_should_wrap_lines && text_type != current_text_type()) { - if (auto result = try_commit_chunk(start_of_chunk, m_current_index, false, broken_on_tab, can_break_at_current_position, font, text_type); result.has_value()) - return result.release_value(); - } - - if (m_should_wrap_lines) { - if (is_ascii_space(code_point)) { - // Whitespace encountered, and we're allowed to break on whitespace. - // If we have accumulated some code points in the current chunk, commit them now and continue with the whitespace next time. - if (auto result = try_commit_chunk(start_of_chunk, m_current_index, false, broken_on_tab, false, font, text_type); result.has_value()) - return result.release_value(); - - // Otherwise, commit the whitespace! - m_current_index = next_grapheme_boundary(); - can_break_at_current_position = is_at_line_break_opportunity(); - auto const& space_font = font_for_space(m_current_index, code_point); - if (auto result = try_commit_chunk(start_of_chunk, m_current_index, false, broken_on_tab, false, space_font, text_type); result.has_value()) - return result.release_value(); - continue; - } - - if (can_break_at_current_position) { - if (auto result = try_commit_chunk(start_of_chunk, m_current_index, false, broken_on_tab, true, font, text_type); result.has_value()) - return result.release_value(); - } - } - - m_current_index = next_grapheme_boundary(); - can_break_at_current_position = is_at_line_break_opportunity(); - } - - if (start_of_chunk != m_view.length_in_code_units()) { - // Try to output whatever's left at the end of the text node. - if (auto result = try_commit_chunk(start_of_chunk, m_view.length_in_code_units(), false, broken_on_tab, false, font, text_type); result.has_value()) - return result.release_value(); - } - - return {}; -} - -Optional TextNode::ChunkIterator::try_commit_chunk(size_t start, size_t end, bool has_breaking_newline, bool has_breaking_tab, bool can_break_after, Gfx::Font const& font, Gfx::GlyphRun::TextType text_type) const -{ - if (auto length_in_code_units = end - start; length_in_code_units > 0) { - auto chunk_view = m_view.substring_view(start, length_in_code_units); - auto is_all_whitespace = chunk_view.is_ascii_whitespace(); - if (!is_all_whitespace) - m_last_non_whitespace_font = font; - return Chunk { - .view = chunk_view, - .font = font, - .start = start, - .length = length_in_code_units, - .has_breaking_newline = has_breaking_newline, - .has_breaking_tab = has_breaking_tab, - .is_all_whitespace = is_all_whitespace, - .can_break_after = can_break_after, - .text_type = text_type, - }; - } - - return {}; -} - void TextNode::set_needs_repaint(InvalidateDisplayList should_invalidate_display_list) const { if (auto* containing_block = this->containing_block()) { diff --git a/Libraries/LibWeb/Layout/TextNode.h b/Libraries/LibWeb/Layout/TextNode.h index fb9c1f946f270..84c3a88365633 100644 --- a/Libraries/LibWeb/Layout/TextNode.h +++ b/Libraries/LibWeb/Layout/TextNode.h @@ -25,11 +25,6 @@ class TextNode : public Node { LAYOUT_NODE(TextNode, Node); public: - enum class TextDirectionMode { - PerCodePoint, - UnidirectionalLeftToRight, - }; - TextNode(DOM::Document&, DOM::Text&); virtual ~TextNode() override; @@ -44,70 +39,12 @@ class TextNode : public Node { Utf16String const& text_for_rendering() const; - struct Chunk { - Utf16View view; - NonnullRefPtr font; - size_t start { 0 }; - size_t length { 0 }; - bool has_breaking_newline { false }; - bool has_breaking_tab { false }; - bool is_all_whitespace { false }; - bool can_break_after { false }; - Gfx::GlyphRun::TextType text_type; - }; - - class ChunkIterator { - public: - ChunkIterator(TextNode const&, bool should_wrap_lines, bool should_respect_linebreaks); - ChunkIterator(TextNode const&, TextDirectionMode, bool should_wrap_lines, bool should_respect_linebreaks); - ChunkIterator(TextNode const&, Utf16View const&, Unicode::Segmenter& grapheme_segmenter, Unicode::Segmenter& line_segmenter, CSS::WordBreak, TextDirectionMode, bool should_wrap_lines, bool should_respect_linebreaks); - - bool should_wrap_lines() const { return m_should_wrap_lines; } - bool should_respect_linebreaks() const { return m_should_respect_linebreaks; } - bool should_collapse_whitespace() const { return m_should_collapse_whitespace; } - - Optional next(); - Optional peek(size_t); - - Chunk create_empty_chunk(); - - private: - Optional next_without_peek(); - Optional try_commit_chunk(size_t start, size_t end, bool has_breaking_newline, bool has_breaking_tab, bool can_break_after, Gfx::Font const&, Gfx::GlyphRun::TextType) const; - - [[nodiscard]] bool is_at_line_break_opportunity() const; - [[nodiscard]] Gfx::Font const& font_for_space(size_t at_index, u32 space_code_point) const; - [[nodiscard]] Gfx::EmojiPresentationResult emoji_presentation_at(size_t code_unit_offset, u32 code_point) const; - - bool const m_should_wrap_lines; - bool const m_should_respect_linebreaks; - bool m_should_collapse_whitespace; - Utf16View m_view; - Gfx::FontCascadeList const& m_font_cascade_list; - - Unicode::Segmenter& m_grapheme_segmenter; - Unicode::Segmenter& m_line_segmenter; - CSS::WordBreak m_word_break; - CSS::FontVariantEmoji m_font_variant_emoji; - TextDirectionMode m_text_direction_mode { TextDirectionMode::PerCodePoint }; - size_t m_current_index { 0 }; - - Vector m_peek_queue; - - mutable RefPtr m_last_non_whitespace_font; - }; - - struct ChunkList { - Vector chunks; - bool should_collapse_whitespace { false }; - }; - - ChunkList const& chunks_for_layout(bool should_wrap_lines, bool should_respect_linebreaks, TextDirectionMode) const; - void invalidate_text_for_rendering(); + void enroll_for_arena_text_content_sync() const; + void sync_text_content_to_arena() const; + Unicode::Segmenter& grapheme_segmenter() const; - Unicode::Segmenter& line_segmenter() const; void set_needs_repaint(InvalidateDisplayList = InvalidateDisplayList::Yes) const; @@ -132,29 +69,10 @@ class TextNode : public Node { bool operator==(TextForRenderingCacheKey const&) const = default; }; - struct ChunkCacheKey { - bool should_wrap_lines { false }; - bool should_respect_linebreaks { false }; - CSS::WhiteSpaceCollapse white_space_collapse { CSS::WhiteSpaceCollapse::Collapse }; - CSS::WordBreak word_break { CSS::WordBreak::Normal }; - CSS::FontVariantEmoji font_variant_emoji { CSS::FontVariantEmoji::Normal }; - TextDirectionMode text_direction_mode { TextDirectionMode::PerCodePoint }; - RefPtr font_cascade_list; - - bool operator==(ChunkCacheKey const&) const = default; - }; - - struct ChunkCacheEntry { - ChunkCacheKey key; - ChunkList chunk_list; - }; - struct TextDependentCache { TextForRenderingCacheKey key; Utf16String text_for_rendering; mutable OwnPtr grapheme_segmenter; - mutable OwnPtr line_segmenter; - mutable Optional chunk_cache; }; TextForRenderingCacheKey create_text_for_rendering_cache_key() const; @@ -162,6 +80,8 @@ class TextNode : public Node { TextDependentCache const& ensure_text_dependent_cache() const; mutable Optional m_text_dependent_cache; + mutable bool m_arena_text_content_in_sync { false }; + mutable bool m_enrolled_for_arena_text_content_sync { false }; }; class GeneratedTextNode final : public TextNode { @@ -206,6 +126,11 @@ class TextSliceNode final : public TextNode { WeakPtr m_first_letter_slice; }; +// Classifies a code point for direction-run splitting during text chunking: +// strong LTR/RTL, direction-neutral Common, or ContextDependent (resolved +// from surrounding runs). +Gfx::GlyphRun::TextType text_type_for_code_point(u32 code_point); + template<> inline bool Node::fast_is() const { return is_text_node(); } diff --git a/Libraries/LibWeb/Rust/src/layout/formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/formatting_context.rs index bf9b8af00a735..5754bf86f44eb 100644 --- a/Libraries/LibWeb/Rust/src/layout/formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/formatting_context.rs @@ -300,14 +300,8 @@ impl<'a, 'pass> AbsposEngine<'a, 'pass> { self.callbacks.is_ancestor(ancestor, node) } - fn dom_node_is_inclusive_ancestor(&self, ancestor: Node, node: Node) -> bool { - unsafe { - (self.callbacks.dom_node_is_inclusive_ancestor)( - self.callbacks.context, - self.callbacks.shell(ancestor), - self.callbacks.shell(node), - ) - } + fn belongs_to_inline_containing_block(&self, inline_node: Node, node: Node) -> bool { + !self.facts(node).is_anonymous() && self.node_is_ancestor(inline_node, node) } fn resolve_static_position_relative_to_containing_block( @@ -432,7 +426,7 @@ impl<'a, 'pass> AbsposEngine<'a, 'pass> { empty_bounding_rect: &mut Option, ) { for fragment in self.line_fragments(node) { - if !self.dom_node_is_inclusive_ancestor(inline_node, fragment.layout_node) { + if !self.belongs_to_inline_containing_block(inline_node, fragment.layout_node) { continue; } if fragment.is_atomic_inline { @@ -466,7 +460,7 @@ impl<'a, 'pass> AbsposEngine<'a, 'pass> { offset }; if facts.is_box() && !facts.is_anonymous() { - if !self.dom_node_is_inclusive_ancestor(inline_node, child) { + if !self.belongs_to_inline_containing_block(inline_node, child) { child = next; continue; } @@ -502,7 +496,7 @@ impl<'a, 'pass> AbsposEngine<'a, 'pass> { inline_node: Node, abspos_containing_block: Node, ) -> Option { - if !self.dom_node_is_inclusive_ancestor(inline_node, inline_node) { + if self.facts(inline_node).is_anonymous() { return None; } let outer_block = self.non_anonymous_containing_block(inline_node); @@ -4755,7 +4749,6 @@ pub struct FfiLayoutFcCallbacks { pub initial_containing_block_inline_size: CssPixels, pub document_in_quirks_mode: bool, pub static_position_containing_block: unsafe extern "C" fn(*mut c_void, *mut c_void) -> NodeSlotId, - pub dom_node_is_inclusive_ancestor: unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void) -> bool, pub needs_inset_resolution: unsafe extern "C" fn(*mut c_void, *mut c_void) -> bool, pub report_unexpected_fragmented_inline: unsafe extern "C" fn(*mut c_void, *mut c_void), pub decode_residual_style: crate::layout::FfiDecodeResidualStyleCallback, @@ -4764,10 +4757,7 @@ pub struct FfiLayoutFcCallbacks { pub build_style_snapshot: FfiBuildStyleSnapshotCallback, pub build_replaced_content_facts: unsafe extern "C" fn(*mut c_void, *mut c_void) -> crate::layout::FfiReplacedContentFacts, pub build_list_item_facts: unsafe extern "C" fn(*mut c_void, *mut c_void) -> crate::layout::FfiListItemFacts, - pub build_text_facts: - unsafe extern "C" fn(*mut c_void, *mut c_void, bool, bool, bool, *mut FfiTextNodeFacts) -> bool, - pub release_text_facts: unsafe extern "C" fn(*mut c_void, *mut c_void), - pub text_may_require_bidi_processing: unsafe extern "C" fn(*mut c_void, *mut c_void) -> bool, + pub text_node_is_empty_editable: unsafe extern "C" fn(*mut c_void, *mut c_void) -> bool, pub document_cursor_is_on_node: unsafe extern "C" fn(*mut c_void, *mut c_void) -> bool, pub build_table_box_facts: FfiBuildTableBoxFactsCallback, pub build_grid_facts: unsafe extern "C" fn(*mut c_void, *mut c_void) -> FfiGridStyleFacts, @@ -4802,6 +4792,16 @@ impl FfiLayoutFcCallbacks { unsafe { &*self.arena().data(node) } } + pub(crate) fn text_content(&self, node: Node) -> &'static crate::layout::layout_node_arena::TextContent { + let content = self + .arena() + .text_content(node) + .expect("text node content must be synced to the arena before layout"); + // SAFETY: The document arena outlives the layout pass, and text + // content is only mutated between passes. + unsafe { &*std::ptr::from_ref(content) } + } + pub(crate) fn shell(&self, node: Node) -> *mut c_void { let shell = self.node_data(node).shell; assert!(!shell.is_null()); diff --git a/Libraries/LibWeb/Rust/src/layout/inline_formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/inline_formatting_context.rs index 11ff213877025..68421f068567f 100644 --- a/Libraries/LibWeb/Rust/src/layout/inline_formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/inline_formatting_context.rs @@ -641,10 +641,7 @@ impl<'context, 'pass> InlineFormattingContext<'context, 'pass> { } pub(crate) fn text_may_require_bidi_processing(&self, node: Node) -> bool { - // SAFETY: The host reads the live TextNode synchronously. - unsafe { - (self.callbacks.text_may_require_bidi_processing)(self.callbacks.context, self.callbacks.shell(node)) - } + self.callbacks.text_content(node).may_require_bidi_processing } pub(crate) fn compute_inset(&self, node: Node) { diff --git a/Libraries/LibWeb/Rust/src/layout/inline_level_iterator.rs b/Libraries/LibWeb/Rust/src/layout/inline_level_iterator.rs index 74af78541aafb..fed17a80172c9 100644 --- a/Libraries/LibWeb/Rust/src/layout/inline_level_iterator.rs +++ b/Libraries/LibWeb/Rust/src/layout/inline_level_iterator.rs @@ -68,7 +68,8 @@ struct ExtraBoxMetrics { #[derive(Clone, Copy)] struct TextNodeContext { - facts: FfiTextNodeFacts, + chunks: &'static [TextChunk], + text: &'static [u16], next_chunk_index: usize, should_collapse_whitespace: bool, should_respect_linebreaks: bool, @@ -114,7 +115,6 @@ impl<'iterator, 'context, 'pass> InlineLevelIteratorGenerator<'iterator, 'contex iterator.skip_to_next(); iterator.generate_all_items(); InlineLevelIterator { - is_unidirectional_left_to_right: iterator.is_unidirectional_left_to_right, visited_fragmented_inlines: iterator.visited_fragmented_inlines, items: iterator.items, next_item_index: iterator.next_item_index, @@ -129,22 +129,6 @@ impl<'iterator, 'context, 'pass> InlineLevelIteratorGenerator<'iterator, 'contex self.context } - fn chunks(facts: FfiTextNodeFacts) -> &'static [FfiTextChunk] { - if facts.chunk_count == 0 { - return &[]; - } - // SAFETY: The state-owned text snapshot remains alive for the pass. - unsafe { std::slice::from_raw_parts(facts.chunks, facts.chunk_count) } - } - - fn text(facts: FfiTextNodeFacts) -> &'static [u16] { - if facts.text_length_in_code_units == 0 { - return &[]; - } - // SAFETY: The state-owned text snapshot remains alive for the pass. - unsafe { std::slice::from_raw_parts(facts.text_utf16, facts.text_length_in_code_units) } - } - fn is_out_of_flow(&self, node: Node) -> bool { let facts = self.context().facts(node); facts.is_floating() || facts.is_absolutely_positioned() @@ -322,22 +306,23 @@ impl<'iterator, 'context, 'pass> InlineLevelIteratorGenerator<'iterator, 'contex style.white_space_collapse, white_space_collapse::PRESERVE | white_space_collapse::PRESERVE_BREAKS | white_space_collapse::BREAK_SPACES ); + let should_collapse_whitespace = matches!( + style.white_space_collapse, + white_space_collapse::COLLAPSE | white_space_collapse::PRESERVE_BREAKS + ); let callbacks = self.context().callbacks; - let facts = self - .context() - .state - .text_facts( - &callbacks, - text_node, - should_wrap_lines, - should_respect_linebreaks, - self.is_unidirectional_left_to_right, - ) - .ffi(); + let chunks = self.context().state.text_chunks( + &callbacks, + text_node, + should_wrap_lines, + should_respect_linebreaks, + self.is_unidirectional_left_to_right, + ); self.text_node_context = Some(TextNodeContext { - facts, + chunks, + text: &callbacks.text_content(text_node).text, next_chunk_index: 0, - should_collapse_whitespace: facts.should_collapse_whitespace, + should_collapse_whitespace, should_respect_linebreaks, last_known_direction: None, }); @@ -345,7 +330,7 @@ impl<'iterator, 'context, 'pass> InlineLevelIteratorGenerator<'iterator, 'contex fn resolve_text_direction_from_context(&self) -> u8 { let context = self.text_node_context.unwrap(); - let next_known_direction = Self::chunks(context.facts)[context.next_chunk_index..] + let next_known_direction = context.chunks[context.next_chunk_index..] .iter() .find_map(|chunk| { matches!(chunk.text_type, GLYPH_TEXT_TYPE_LTR | GLYPH_TEXT_TYPE_RTL).then_some(chunk.text_type) @@ -405,7 +390,7 @@ impl<'iterator, 'context, 'pass> InlineLevelIteratorGenerator<'iterator, 'contex self.enter_text_node(text_node); } let mut text_context = self.text_node_context.unwrap(); - let chunks = Self::chunks(text_context.facts); + let chunks = text_context.chunks; let is_first_chunk = text_context.next_chunk_index == 0; let chunk = chunks.get(text_context.next_chunk_index).copied(); if chunk.is_some() { @@ -415,14 +400,18 @@ impl<'iterator, 'context, 'pass> InlineLevelIteratorGenerator<'iterator, 'contex let is_empty_editable = chunk.is_none() && is_first_chunk && is_last_chunk - && text_context.facts.text_length_in_code_units == 0 - && text_context.facts.is_empty_editable; + && text_context.text.is_empty() + && { + let callbacks = self.context().callbacks; + // SAFETY: The host reads the live TextNode synchronously. + unsafe { (callbacks.text_node_is_empty_editable)(callbacks.context, callbacks.shell(text_node)) } + }; let chunk = if let Some(chunk) = chunk { chunk } else if is_empty_editable { text_context.next_chunk_index = 1; let parent_style = self.context().style(self.context().parent_node(text_node)); - FfiTextChunk { + TextChunk { start: 0, length: 0, font: parent_style.first_available_font(), @@ -459,7 +448,7 @@ impl<'iterator, 'context, 'pass> InlineLevelIteratorGenerator<'iterator, 'contex let style = self.context().style(self.context().parent_node(text_node)); let mut inline_offset = 0.0f32; - let full_text = Self::text(text_context.facts); + let full_text = text_context.text; let mut shaped_start = chunk.start; let mut shaped_length = chunk.length; if chunk.has_breaking_tab { @@ -501,8 +490,8 @@ impl<'iterator, 'context, 'pass> InlineLevelIteratorGenerator<'iterator, 'contex style.letter_spacing.to_double() as f32, ); let chunk_inline_size = CssPixels::nearest_value_for_f32(glyphs.width + inline_offset); - let generated_empty = - is_empty_editable || (text_context.facts.is_generated_for_pseudo_element && chunk.length == 0); + let generated_empty = is_empty_editable + || (self.context().facts(text_node).is_generated_for_pseudo_element() && chunk.length == 0); let mut item = Item::new(ItemType::Text, text_node); item.glyphs = Some(glyphs); item.offset_in_node = chunk.start; @@ -591,7 +580,6 @@ impl<'iterator, 'context, 'pass> InlineLevelIteratorGenerator<'iterator, 'contex } pub(crate) struct InlineLevelIterator { - is_unidirectional_left_to_right: bool, visited_fragmented_inlines: Vec, items: Vec, next_item_index: usize, @@ -602,14 +590,6 @@ impl InlineLevelIterator { InlineLevelIteratorGenerator::generate(context) } - fn text(facts: FfiTextNodeFacts) -> &'static [u16] { - if facts.text_length_in_code_units == 0 { - return &[]; - } - // SAFETY: The state-owned text snapshot remains alive for the pass. - unsafe { std::slice::from_raw_parts(facts.text_utf16, facts.text_length_in_code_units) } - } - pub(crate) fn next(&mut self) -> Option { let index = self.next_item_index; if index >= self.items.len() { @@ -636,17 +616,7 @@ impl InlineLevelIterator { if item.type_ != ItemType::Text || item.is_collapsible_whitespace { break; } - let facts = context - .state - .text_facts( - &context.callbacks, - item.node, - true, - false, - self.is_unidirectional_left_to_right, - ) - .ffi(); - let text = Self::text(facts); + let text = &context.callbacks.text_content(item.node).text; if text[item.offset_in_node..item.offset_in_node + item.length_in_node] .iter() .all(|unit| *unit <= 0x7f && (*unit as u8).is_ascii_whitespace()) @@ -661,23 +631,8 @@ impl InlineLevelIterator { pub(crate) fn item_is_ascii_whitespace(&self, context: &InlineFormattingContext<'_, '_>, item: &Item) -> bool { assert_eq!(item.type_, ItemType::Text); - let style = context.style(context.parent_node(item.node)); - let should_wrap = style.text_wrap_mode == text_wrap_mode::WRAP; - let should_respect = matches!( - style.white_space_collapse, - white_space_collapse::PRESERVE | white_space_collapse::PRESERVE_BREAKS | white_space_collapse::BREAK_SPACES - ); - let facts = context - .state - .text_facts( - &context.callbacks, - item.node, - should_wrap, - should_respect, - self.is_unidirectional_left_to_right, - ) - .ffi(); - Self::text(facts)[item.offset_in_node..item.offset_in_node + item.length_in_node] + let text = &context.callbacks.text_content(item.node).text; + text[item.offset_in_node..item.offset_in_node + item.length_in_node] .iter() .all(|unit| *unit <= 0x7f && (*unit as u8).is_ascii_whitespace()) } @@ -687,33 +642,6 @@ impl InlineLevelIterator { } } -#[derive(Clone, Copy, Debug)] -#[repr(C)] -pub struct FfiTextChunk { - pub start: usize, - pub length: usize, - pub font: *const c_void, - pub has_breaking_newline: bool, - pub has_breaking_tab: bool, - pub is_all_whitespace: bool, - pub can_break_after: bool, - pub text_type: u8, -} - -#[derive(Clone, Copy, Debug)] -#[repr(C)] -pub struct FfiTextNodeFacts { - pub text_utf16: *const u16, - pub text_length_in_code_units: usize, - pub chunks: *const FfiTextChunk, - pub chunk_count: usize, - pub should_collapse_whitespace: bool, - pub is_generated_for_pseudo_element: bool, - pub is_empty_editable: bool, - pub has_dom_node: bool, - pub retained: *mut c_void, -} - #[derive(Clone, Copy, Debug, Default, PartialEq)] #[repr(C)] pub struct FfiDrawGlyph { @@ -724,56 +652,3 @@ pub struct FfiDrawGlyph { pub glyph_id: u32, pub should_paint: bool, } - -pub(crate) struct TextNodeFacts { - ffi: FfiTextNodeFacts, - release: unsafe extern "C" fn(*mut c_void, *mut c_void), -} - -impl TextNodeFacts { - pub(crate) fn build( - callbacks: &FfiLayoutFcCallbacks, - text_node: Node, - should_wrap_lines: bool, - should_respect_linebreaks: bool, - unidirectional_ltr: bool, - ) -> Self { - let mut ffi = std::mem::MaybeUninit::::uninit(); - // SAFETY: The host synchronously initializes `ffi` on the true path. - let built = unsafe { - (callbacks.build_text_facts)( - callbacks.context, - callbacks.shell(text_node), - should_wrap_lines, - should_respect_linebreaks, - unidirectional_ltr, - ffi.as_mut_ptr(), - ) - }; - assert!(built); - // SAFETY: `built` is true, so the callback initialized every field. - let ffi = unsafe { ffi.assume_init() }; - assert!(!ffi.retained.is_null()); - assert!(ffi.text_length_in_code_units == 0 || !ffi.text_utf16.is_null()); - assert!(ffi.chunk_count == 0 || !ffi.chunks.is_null()); - Self { - ffi, - release: callbacks.release_text_facts, - } - } - - pub(crate) fn ffi(&self) -> FfiTextNodeFacts { - self.ffi - } -} - -impl Drop for TextNodeFacts { - fn drop(&mut self) { - // The release operation is context-independent by contract: text - // snapshots can outlive the bridge instance that populated the state. - // SAFETY: `retained` is owned by this snapshot and released once. - unsafe { - (self.release)(std::ptr::null_mut(), self.ffi.retained); - } - } -} diff --git a/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs b/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs index 43ad465608a2c..1c23ac4192538 100644 --- a/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs +++ b/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs @@ -93,6 +93,41 @@ struct SavedAbsposLayoutInputsSlot { inputs: Option>, } +#[derive(Default)] +pub(crate) struct TextContent { + pub(crate) text: Vec, + pub(crate) may_require_bidi_processing: bool, +} + +#[derive(Default)] +struct TextContentSlot { + generation: u8, + content: Option>, +} + +#[derive(Clone, Copy, PartialEq)] +pub(crate) struct TextChunkCacheKey { + pub(crate) should_wrap_lines: bool, + pub(crate) should_respect_linebreaks: bool, + pub(crate) unidirectional_ltr: bool, + pub(crate) white_space_collapse: u8, + pub(crate) word_break: u8, + pub(crate) font_variant_emoji: u8, + pub(crate) font_cascade_list: *const c_void, +} + +struct TextChunkCacheEntry { + key: TextChunkCacheKey, + _retained_font_cascade_list: libgfx_rust::font::RetainedFontCascadeList, + chunks: Vec, +} + +#[derive(Default)] +struct TextChunkCacheSlot { + generation: u8, + entry: Option>, +} + // NodeData is sized to one cache line; the aligned chunk keeps every densely-strided slot // line-aligned, and per-slot bookkeeping lives in a parallel array so it stays that way. #[repr(align(64))] @@ -134,6 +169,8 @@ pub(crate) struct LayoutNodeArena { live_count: u32, intrinsic_size_caches: RefCell>, saved_abspos_layout_inputs: RefCell>, + text_contents: Vec, + text_chunk_caches: RefCell>, owner_thread: thread::ThreadId, } @@ -148,6 +185,8 @@ impl LayoutNodeArena { live_count: 0, intrinsic_size_caches: RefCell::new(Vec::new()), saved_abspos_layout_inputs: RefCell::new(Vec::new()), + text_contents: Vec::new(), + text_chunk_caches: RefCell::new(Vec::new()), owner_thread: thread::current().id(), } } @@ -234,6 +273,12 @@ impl LayoutNodeArena { if let Some(slot) = self.saved_abspos_layout_inputs.get_mut().get_mut(index as usize) { *slot = SavedAbsposLayoutInputsSlot::default(); } + if let Some(slot) = self.text_contents.get_mut(index as usize) { + *slot = TextContentSlot::default(); + } + if let Some(slot) = self.text_chunk_caches.get_mut().get_mut(index as usize) { + *slot = TextChunkCacheSlot::default(); + } *self.data_mut(index) = NodeData::default(); self.live_count = self @@ -498,6 +543,80 @@ impl LayoutNodeArena { } } + pub(crate) fn set_text_content(&mut self, id: NodeSlotId, text: Vec, may_require_bidi_processing: bool) { + self.assert_owner_thread(); + self.data(id); + let index = id.slot_index() as usize; + if self.text_contents.len() <= index { + self.text_contents.resize_with(index + 1, TextContentSlot::default); + } + self.text_contents[index] = TextContentSlot { + generation: id.generation(), + content: Some(Box::new(TextContent { + text, + may_require_bidi_processing, + })), + }; + if let Some(slot) = self.text_chunk_caches.get_mut().get_mut(index) { + *slot = TextChunkCacheSlot::default(); + } + } + + pub(crate) fn text_content(&self, id: NodeSlotId) -> Option<&TextContent> { + assert!(!id.is_invalid(), "invalid layout node arena slot ID"); + self.text_contents + .get(id.slot_index() as usize) + .filter(|slot| slot.generation == id.generation()) + .and_then(|slot| slot.content.as_deref()) + } + + pub(crate) fn text_chunks( + &self, + id: NodeSlotId, + key: TextChunkCacheKey, + compute: impl FnOnce() -> Vec, + ) -> &'static [crate::layout::TextChunk] { + // data() validates that id names a live slot with a matching generation. + self.data(id); + let index = id.slot_index() as usize; + + // SAFETY (for both laundered returns below): an entry is only replaced + // when its key changes or its slot is freed, and every key input is + // fixed for a given node within one layout pass while the arena + // itself outlives the pass, so a slice handed out during a pass stays + // valid for that pass. + { + let slots = self.text_chunk_caches.borrow(); + if let Some(slot) = slots.get(index) + && slot.generation == id.generation() + && let Some(entry) = slot.entry.as_deref() + && entry.key == key + { + return unsafe { std::slice::from_raw_parts(entry.chunks.as_ptr(), entry.chunks.len()) }; + } + } + + let chunks = compute(); + let mut slots = self.text_chunk_caches.borrow_mut(); + if slots.len() <= index { + slots.resize_with(index + 1, TextChunkCacheSlot::default); + } + slots[index] = TextChunkCacheSlot { + generation: id.generation(), + entry: Some(Box::new(TextChunkCacheEntry { + key, + // SAFETY: The caller derives the key's cascade-list pointer + // from a live style snapshot. + _retained_font_cascade_list: unsafe { + libgfx_rust::font::RetainedFontCascadeList::retain(key.font_cascade_list) + }, + chunks, + })), + }; + let entry = slots[index].entry.as_deref().expect("entry was just stored"); + unsafe { std::slice::from_raw_parts(entry.chunks.as_ptr(), entry.chunks.len()) } + } + pub(crate) fn transfer_saved_abspos_layout_inputs(&self, old: NodeSlotId, new: NodeSlotId) { self.assert_owner_thread(); assert_ne!(old, new, "cannot transfer saved abspos inputs to the same arena slot"); @@ -587,6 +706,38 @@ pub unsafe extern "C" fn layout_arena_free(arena: *mut c_void, id: NodeSlotId, g }); } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn layout_arena_set_text_content( + arena: *mut c_void, + id: NodeSlotId, + ascii_text: *const u8, + utf16_text: *const u16, + length_in_code_units: usize, + may_require_bidi_processing: bool, +) { + abort_on_panic(|| { + assert!(!arena.is_null(), "layout node arena handle is null"); + let text = if length_in_code_units == 0 { + Vec::new() + } else if !ascii_text.is_null() { + // SAFETY: The C++ caller passes the live ASCII storage of the + // node's rendered text for the duration of this synchronous call. + unsafe { std::slice::from_raw_parts(ascii_text, length_in_code_units) } + .iter() + .map(|unit| u16::from(*unit)) + .collect() + } else { + assert!(!utf16_text.is_null(), "text content push carries no storage"); + // SAFETY: The C++ caller passes the live UTF-16 storage of the + // node's rendered text for the duration of this synchronous call. + unsafe { std::slice::from_raw_parts(utf16_text, length_in_code_units) }.to_vec() + }; + // SAFETY: The C++ wrapper keeps the arena alive for this call and + // serializes all access on the document thread. + unsafe { &mut *arena.cast::() }.set_text_content(id, text, may_require_bidi_processing); + }); +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn layout_arena_transfer_saved_abspos_layout_inputs( arena: *mut c_void, diff --git a/Libraries/LibWeb/Rust/src/layout/layout_state.rs b/Libraries/LibWeb/Rust/src/layout/layout_state.rs index fd4b7dc69ea67..6dc90c30d6f14 100644 --- a/Libraries/LibWeb/Rust/src/layout/layout_state.rs +++ b/Libraries/LibWeb/Rust/src/layout/layout_state.rs @@ -575,6 +575,10 @@ impl<'pass> NodeFacts<'pass> { !crate::layout::has_flag(self.data(), NodeFlag::Anonymous) } + pub(crate) fn is_generated_for_pseudo_element(&self) -> bool { + self.data().generated_for != 0 + } + pub(crate) fn children_are_inline(&self) -> bool { crate::layout::has_flag(self.data(), NodeFlag::ChildrenAreInline) } @@ -844,7 +848,6 @@ pub(crate) struct LayoutState { list_item_facts: PagedStore, table_facts: PagedStore, grid_facts: PagedStore, - text_facts: PagedStore, line_data: PagedStore>, block_rare_data: PagedStore>, used_values_rare_data: PagedStore>, @@ -898,7 +901,6 @@ impl LayoutState { list_item_facts: PagedStore::default(), table_facts: PagedStore::default(), grid_facts: PagedStore::default(), - text_facts: PagedStore::default(), line_data: PagedStore::default(), block_rare_data: PagedStore::default(), used_values_rare_data: PagedStore::default(), @@ -1320,28 +1322,37 @@ impl LayoutState { self.grid_facts.allocate(slot_index, facts) } - pub(crate) fn text_facts( + pub(crate) fn text_chunks( &self, callbacks: &FfiLayoutFcCallbacks, node: Node, should_wrap_lines: bool, should_respect_linebreaks: bool, unidirectional_ltr: bool, - ) -> &TextNodeFacts { - let slot_index = callbacks.slot_index(node); - if let Some(facts) = self.text_facts.get(slot_index) { - return facts; - } - self.text_facts.allocate( - slot_index, - TextNodeFacts::build( - callbacks, - node, + ) -> &'static [TextChunk] { + let parent_style = self.style_facts(callbacks, callbacks.parent(node)); + let key = crate::layout::layout_node_arena::TextChunkCacheKey { + should_wrap_lines, + should_respect_linebreaks, + unidirectional_ltr, + white_space_collapse: parent_style.white_space_collapse, + word_break: parent_style.word_break, + font_variant_emoji: parent_style.font_variant_emoji, + font_cascade_list: parent_style.font_cascade_list(), + }; + let text = &callbacks.text_content(node).text; + callbacks.arena().text_chunks(node, key, || { + chunk_text(TextChunkInputs { + text, + font_cascade_list: key.font_cascade_list, + white_space_collapse: key.white_space_collapse, + word_break: key.word_break, + font_variant_emoji: key.font_variant_emoji, should_wrap_lines, should_respect_linebreaks, unidirectional_ltr, - ), - ) + }) + }) } pub(crate) fn line_data_cell(&self, slot_index: u32) -> &RefCell { diff --git a/Libraries/LibWeb/Rust/src/layout/line_builder.rs b/Libraries/LibWeb/Rust/src/layout/line_builder.rs index 6e26244a93f3d..e9fd09796eb63 100644 --- a/Libraries/LibWeb/Rust/src/layout/line_builder.rs +++ b/Libraries/LibWeb/Rust/src/layout/line_builder.rs @@ -109,22 +109,8 @@ impl<'builder, 'context, 'pass> LineBuilder<'builder, 'context, 'pass> { let style = self.context().style(style_source); let facts = self.context().facts(node); let (text_utf16, text_length) = if facts.is_text_node() { - let parent = self.context().parent_node(node); - let parent_style = self.context().style(parent); - let should_wrap = parent_style.text_wrap_mode == text_wrap_mode::WRAP; - let should_respect = matches!( - parent_style.white_space_collapse, - white_space_collapse::PRESERVE - | white_space_collapse::PRESERVE_BREAKS - | white_space_collapse::BREAK_SPACES - ); - let callbacks = self.context().callbacks; - let ffi = self - .context() - .state - .text_facts(&callbacks, node, should_wrap, should_respect, false) - .ffi(); - (ffi.text_utf16, ffi.text_length_in_code_units) + let text = &self.context().callbacks.text_content(node).text; + (text.as_ptr(), text.len()) } else { (std::ptr::null(), 0) }; diff --git a/Libraries/LibWeb/Rust/src/layout/mod.rs b/Libraries/LibWeb/Rust/src/layout/mod.rs index a67f7a1412890..98809cca35ef9 100644 --- a/Libraries/LibWeb/Rust/src/layout/mod.rs +++ b/Libraries/LibWeb/Rust/src/layout/mod.rs @@ -26,6 +26,7 @@ include!("line_box_fragment.rs"); include!("line_builder.rs"); include!("inline_formatting_context.rs"); include!("font.rs"); +include!("text_chunker.rs"); include!("replaced_with_children_formatting_context.rs"); include!("table_formatting_context.rs"); include!("geometry.rs"); diff --git a/Libraries/LibWeb/Rust/src/layout/style_facts.rs b/Libraries/LibWeb/Rust/src/layout/style_facts.rs index b0b5a34125eb6..807dcc587b919 100644 --- a/Libraries/LibWeb/Rust/src/layout/style_facts.rs +++ b/Libraries/LibWeb/Rust/src/layout/style_facts.rs @@ -217,6 +217,8 @@ pub enum FfiStyleField { TextJustify, WhiteSpaceCollapse, TextWrapMode, + WordBreak, + FontVariantEmoji, LineHeight, FontSize, BoxSizing, @@ -409,6 +411,7 @@ pub struct FfiResidualStyleValues { pub vertical_align_keyword: u8, pub vertical_align_value: FfiSizeValue, pub first_available_font: *const c_void, + pub font_cascade_list: *const c_void, pub font_ascent: f32, pub font_descent: f32, pub font_x_height: f32, @@ -443,6 +446,7 @@ impl Default for FfiResidualStyleValues { vertical_align_keyword: 0, vertical_align_value: FfiSizeValue::auto_value(), first_available_font: std::ptr::null(), + font_cascade_list: std::ptr::null(), font_ascent: 0.0, font_descent: 0.0, font_x_height: 0.0, @@ -766,6 +770,8 @@ pub(crate) struct DecodedStyleScalars { pub text_justify: u8, pub white_space_collapse: u8, pub text_wrap_mode: u8, + pub word_break: u8, + pub font_variant_emoji: u8, pub line_height: CssPixels, pub font_size: CssPixels, pub box_sizing: u8, @@ -859,6 +865,8 @@ impl DecodedStyleScalars { text_justify: reader.u8(FfiStyleField::TextJustify), white_space_collapse: reader.u8(FfiStyleField::WhiteSpaceCollapse), text_wrap_mode: reader.u8(FfiStyleField::TextWrapMode), + word_break: reader.u8(FfiStyleField::WordBreak), + font_variant_emoji: reader.u8(FfiStyleField::FontVariantEmoji), line_height: reader.css_pixels(FfiStyleField::LineHeight), font_size: reader.css_pixels(FfiStyleField::FontSize), box_sizing: reader.u8(FfiStyleField::BoxSizing), @@ -1073,6 +1081,10 @@ impl<'a> StyleValues<'a> { self.residual().first_available_font } + pub(crate) fn font_cascade_list(self) -> *const c_void { + self.residual().font_cascade_list + } + pub(crate) fn font_ascent(self) -> f32 { self.residual().font_ascent } diff --git a/Libraries/LibWeb/Rust/src/layout/text_chunker.rs b/Libraries/LibWeb/Rust/src/layout/text_chunker.rs new file mode 100644 index 0000000000000..c9481a3e195ce --- /dev/null +++ b/Libraries/LibWeb/Rust/src/layout/text_chunker.rs @@ -0,0 +1,632 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +use libgfx_rust::font::{EmojiPresentation, FontCascadeListRef, FontRef, emoji_presentation_for_code_point}; + +unsafe extern "C" { + fn unicode_layout_grapheme_segmenter_create(text: *const u16, length_in_code_units: usize) -> *mut c_void; + fn unicode_layout_line_segmenter_create(text: *const u16, length_in_code_units: usize) -> *mut c_void; + fn unicode_layout_segmenter_next_boundary(handle: *mut c_void, index: usize, inclusive: bool) -> i64; + fn unicode_layout_segmenter_destroy(handle: *mut c_void); + fn ladybird_layout_text_type_for_code_point(code_point: u32) -> u8; + fn ladybird_layout_code_point_has_break_all_line_break_class(code_point: u32) -> bool; + fn ladybird_layout_code_point_has_keep_all_line_break_class(code_point: u32) -> bool; + fn ladybird_layout_code_point_has_combining_mark_line_break_class(code_point: u32) -> bool; + fn ladybird_layout_code_point_has_emoji_property(code_point: u32) -> bool; +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) struct TextChunk { + pub start: usize, + pub length: usize, + pub font: *const c_void, + pub has_breaking_newline: bool, + pub has_breaking_tab: bool, + pub is_all_whitespace: bool, + pub can_break_after: bool, + pub text_type: u8, +} + +pub(crate) struct TextChunkInputs<'text> { + pub text: &'text [u16], + pub font_cascade_list: *const c_void, + pub white_space_collapse: u8, + pub word_break: u8, + pub font_variant_emoji: u8, + pub should_wrap_lines: bool, + pub should_respect_linebreaks: bool, + pub unidirectional_ltr: bool, +} + +pub(crate) fn chunk_text(inputs: TextChunkInputs<'_>) -> Vec { + let mut chunker = TextChunker::new(inputs); + let mut chunks = Vec::new(); + while let Some(chunk) = chunker.next_chunk() { + chunks.push(chunk); + } + chunks +} + +struct IcuSegmenterHandle { + raw: *mut c_void, +} + +impl IcuSegmenterHandle { + fn next_boundary(&self, index: usize, inclusive: bool) -> Option { + // SAFETY: The handle is live until drop, and the text it references + // outlives it per the segmenter constructor contracts below. + let boundary = unsafe { unicode_layout_segmenter_next_boundary(self.raw, index, inclusive) }; + (boundary >= 0).then_some(boundary as usize) + } +} + +impl Drop for IcuSegmenterHandle { + fn drop(&mut self) { + // SAFETY: The handle was created by a unicode_layout_*_segmenter_create + // call and is destroyed exactly once. + unsafe { unicode_layout_segmenter_destroy(self.raw) }; + } +} + +enum GraphemeSegmenter { + Ascii { length_in_code_units: usize }, + Icu(IcuSegmenterHandle), +} + +impl GraphemeSegmenter { + fn new(text: &[u16]) -> Self { + if text.iter().all(|unit| *unit <= 0x7f) { + return Self::Ascii { + length_in_code_units: text.len(), + }; + } + // SAFETY: The chunker borrows the text for its whole lifetime and the + // segmenter handle is dropped with the chunker, so the ICU segmenter + // never outlives the buffer it references. + let raw = unsafe { unicode_layout_grapheme_segmenter_create(text.as_ptr(), text.len()) }; + Self::Icu(IcuSegmenterHandle { raw }) + } + + /// Mirrors Unicode::AsciiGraphemeSegmenter for the ASCII case: every code + /// unit index is a boundary. + fn next_boundary(&self, index: usize, inclusive: bool) -> Option { + match self { + Self::Ascii { length_in_code_units } => { + if inclusive && index <= *length_in_code_units { + return Some(index); + } + if index >= *length_in_code_units { + return None; + } + Some(index + 1) + } + Self::Icu(handle) => handle.next_boundary(index, inclusive), + } + } +} + +struct LineSegmenter { + handle: IcuSegmenterHandle, +} + +impl LineSegmenter { + fn new(text: &[u16]) -> Self { + // SAFETY: See GraphemeSegmenter::new; the same lifetime contract holds, + // and the C++ side picks the ASCII line-breaking fast path internally. + let raw = unsafe { unicode_layout_line_segmenter_create(text.as_ptr(), text.len()) }; + Self { + handle: IcuSegmenterHandle { raw }, + } + } + + fn next_boundary(&self, index: usize, inclusive: bool) -> Option { + self.handle.next_boundary(index, inclusive) + } +} + +fn is_utf16_high_surrogate(code_unit: u16) -> bool { + (0xd800..=0xdbff).contains(&code_unit) +} + +fn is_utf16_low_surrogate(code_unit: u16) -> bool { + (0xdc00..=0xdfff).contains(&code_unit) +} + +/// Mirrors AK::Utf16View::code_point_at, including its lone-surrogate +/// pass-through behavior. +fn code_point_at(text: &[u16], index: usize) -> u32 { + let code_unit = text[index]; + let code_point = u32::from(code_unit); + if !is_utf16_high_surrogate(code_unit) && !is_utf16_low_surrogate(code_unit) { + return code_point; + } + if is_utf16_low_surrogate(code_unit) || index + 1 == text.len() { + return code_point; + } + let second = text[index + 1]; + if !is_utf16_low_surrogate(second) { + return code_point; + } + 0x10000 + ((code_point - 0xd800) << 10) + (u32::from(second) - 0xdc00) +} + +/// Mirrors AK::Utf16View::previous_code_point_at: steps `index` back over one +/// code point (surrogate-pair aware) and decodes it. +fn previous_code_point_at(text: &[u16], index: &mut usize) -> u32 { + assert!(*index > 0 && *index <= text.len()); + *index -= 1; + if *index > 0 && is_utf16_low_surrogate(text[*index]) && is_utf16_high_surrogate(text[*index - 1]) { + *index -= 1; + } + code_point_at(text, *index) +} + +fn code_unit_length_for_code_point(code_point: u32) -> usize { + if code_point >= 0x10000 { 2 } else { 1 } +} + +fn code_point_is_ascii_space(code_point: u32) -> bool { + matches!(code_point, 0x09..=0x0d | 0x20) +} + +fn is_interword_space(code_point: u32) -> bool { + code_point == 0x0020 || code_point == 0x00a0 +} + +#[derive(Clone, Copy)] +struct ChunkBreakFlags { + has_breaking_newline: bool, + has_breaking_tab: bool, + can_break_after: bool, +} + +struct TextChunker<'text> { + text: &'text [u16], + font_cascade_list: FontCascadeListRef<'text>, + grapheme_segmenter: GraphemeSegmenter, + line_segmenter: LineSegmenter, + word_break: u8, + font_variant_emoji: u8, + should_collapse_whitespace: bool, + should_wrap_lines: bool, + should_respect_linebreaks: bool, + unidirectional_ltr: bool, + current_index: usize, + last_non_whitespace_font: Option>, +} + +impl<'text> TextChunker<'text> { + fn new(inputs: TextChunkInputs<'text>) -> Self { + Self { + text: inputs.text, + // SAFETY: The caller guarantees the cascade list outlives the + // chunking run (layout retains the style that owns it). + font_cascade_list: unsafe { FontCascadeListRef::from_raw(inputs.font_cascade_list) }, + grapheme_segmenter: GraphemeSegmenter::new(inputs.text), + line_segmenter: LineSegmenter::new(inputs.text), + word_break: inputs.word_break, + font_variant_emoji: inputs.font_variant_emoji, + should_collapse_whitespace: matches!( + inputs.white_space_collapse, + white_space_collapse::COLLAPSE | white_space_collapse::PRESERVE_BREAKS + ), + should_wrap_lines: inputs.should_wrap_lines, + should_respect_linebreaks: inputs.should_respect_linebreaks, + unidirectional_ltr: inputs.unidirectional_ltr, + current_index: 0, + last_non_whitespace_font: None, + } + } + + fn current_code_point(&self) -> u32 { + code_point_at(self.text, self.current_index) + } + + fn current_text_type(&self) -> u8 { + if self.unidirectional_ltr { + return GLYPH_TEXT_TYPE_LTR; + } + // SAFETY: Pure Unicode table lookup. + unsafe { ladybird_layout_text_type_for_code_point(self.current_code_point()) } + } + + fn next_grapheme_boundary(&self) -> usize { + self.grapheme_segmenter + .next_boundary(self.current_index, false) + .unwrap_or(self.text.len()) + } + + fn is_collapsible(&self, code_point: u32) -> bool { + self.should_collapse_whitespace && code_point_is_ascii_space(code_point) + } + + fn is_at_line_break_opportunity(&self) -> bool { + if !self.should_wrap_lines { + return false; + } + + let get_previous_code_point = || -> Option { + if self.current_index == 0 { + return None; + } + let mut index = self.current_index; + let mut previous_code_point = previous_code_point_at(self.text, &mut index); + // SAFETY: Pure Unicode table lookups. + while unsafe { ladybird_layout_code_point_has_combining_mark_line_break_class(previous_code_point) } + && index > 0 + { + previous_code_point = previous_code_point_at(self.text, &mut index); + } + Some(previous_code_point) + }; + + let is_at_line_segmenter_boundary = + || self.line_segmenter.next_boundary(self.current_index, true) == Some(self.current_index); + + match self.word_break { + word_break::NORMAL | word_break::BREAK_WORD => is_at_line_segmenter_boundary(), + word_break::BREAK_ALL => { + if self.current_index >= self.text.len() { + return false; + } + // SAFETY: Pure Unicode table lookups. + if let Some(previous_code_point) = get_previous_code_point() + && unsafe { ladybird_layout_code_point_has_break_all_line_break_class(previous_code_point) } + && unsafe { ladybird_layout_code_point_has_break_all_line_break_class(self.current_code_point()) } + { + return true; + } + is_at_line_segmenter_boundary() + } + word_break::KEEP_ALL => { + if self.current_index >= self.text.len() { + return false; + } + // SAFETY: Pure Unicode table lookups. + if let Some(previous_code_point) = get_previous_code_point() + && unsafe { ladybird_layout_code_point_has_keep_all_line_break_class(previous_code_point) } + && unsafe { ladybird_layout_code_point_has_keep_all_line_break_class(self.current_code_point()) } + { + return false; + } + is_at_line_segmenter_boundary() + } + _ => unreachable!("invalid word-break value"), + } + } + + fn emoji_presentation_at(&self, code_unit_offset: usize, code_point: u32) -> EmojiPresentation { + let next_offset = code_unit_offset + code_unit_length_for_code_point(code_point); + let next_code_point = (next_offset < self.text.len()).then(|| code_point_at(self.text, next_offset)); + let default_presentation = emoji_presentation_for_code_point(code_point, next_code_point); + + // SAFETY: Pure Unicode table lookup. + if default_presentation.forced || !unsafe { ladybird_layout_code_point_has_emoji_property(code_point) } { + return default_presentation; + } + + match self.font_variant_emoji { + font_variant_emoji::TEXT => EmojiPresentation { + is_emoji: false, + forced: true, + }, + font_variant_emoji::EMOJI => EmojiPresentation { + is_emoji: true, + forced: true, + }, + font_variant_emoji::UNICODE => EmojiPresentation { + is_emoji: default_presentation.is_emoji, + forced: true, + }, + _ => default_presentation, + } + } + + fn font_for_space(&self, at_index: usize, space_code_point: u32) -> FontRef<'text> { + let has_glyph = |font: FontRef<'text>| font.contains_glyph(space_code_point); + + // 1. Prefer the last non-whitespace font in this node/run. + if let Some(last_font) = self.last_non_whitespace_font + && !last_font.is_emoji_font() + && has_glyph(last_font) + { + return last_font; + } + + // 2. Look ahead to the next non-space to infer the base font of this run. + let mut i = at_index; + while i < self.text.len() { + let code_point = code_point_at(self.text, i); + if !is_interword_space(code_point) && code_point != '\t' as u32 && code_point != '\n' as u32 { + let font = + self.font_cascade_list + .font_for_code_point(code_point, true, self.emoji_presentation_at(i, code_point)); + if !font.is_emoji_font() && has_glyph(font) { + return font; + } + // Text is coming from an emoji face; we'll fall back to (3). + break; + } + i = self.grapheme_segmenter.next_boundary(i, false).unwrap_or(self.text.len()); + } + + // 3. No text around (leading/trailing/all spaces) — pick a font with the glyph from the cascade. + self.font_cascade_list.font_for_code_point( + space_code_point, + true, + EmojiPresentation { + is_emoji: false, + forced: false, + }, + ) + } + + fn expected_font_for(&self, code_point: u32) -> FontRef<'text> { + if is_interword_space(code_point) { + self.font_for_space(self.current_index, code_point) + } else { + self.font_cascade_list.font_for_code_point( + code_point, + true, + self.emoji_presentation_at(self.current_index, code_point), + ) + } + } + + fn try_commit_chunk( + &mut self, + start: usize, + end: usize, + break_flags: ChunkBreakFlags, + font: FontRef<'text>, + text_type: u8, + ) -> Option { + let length_in_code_units = end - start; + if length_in_code_units == 0 { + return None; + } + let is_all_whitespace = self.text[start..end] + .iter() + .all(|unit| *unit <= 0x7f && code_point_is_ascii_space(u32::from(*unit))); + if !is_all_whitespace { + self.last_non_whitespace_font = Some(font); + } + Some(TextChunk { + start, + length: length_in_code_units, + font: font.as_raw(), + has_breaking_newline: break_flags.has_breaking_newline, + has_breaking_tab: break_flags.has_breaking_tab, + is_all_whitespace, + can_break_after: break_flags.can_break_after, + text_type, + }) + } + + fn next_chunk(&mut self) -> Option { + 'restart: loop { + if self.current_index >= self.text.len() { + return None; + } + + let mut code_point = self.current_code_point(); + let mut can_break_at_current_position = self.is_at_line_break_opportunity(); + let start_of_chunk = self.current_index; + + let font = self.expected_font_for(code_point); + let text_type = self.current_text_type(); + + let mut broken_on_tab = false; + + while self.current_index < self.text.len() { + code_point = self.current_code_point(); + + if code_point == '\t' as u32 { + if let Some(chunk) = self.try_commit_chunk( + start_of_chunk, + self.current_index, + ChunkBreakFlags { + has_breaking_newline: false, + has_breaking_tab: broken_on_tab, + can_break_after: false, + }, + font, + text_type, + ) { + return Some(chunk); + } + + broken_on_tab = true; + // consume any consecutive tabs + while self.current_index < self.text.len() && self.current_code_point() == '\t' as u32 { + self.current_index = self.next_grapheme_boundary(); + } + can_break_at_current_position = self.is_at_line_break_opportunity(); + } + + let expected_font = self.expected_font_for(code_point); + + if font != expected_font + && let Some(chunk) = self.try_commit_chunk( + start_of_chunk, + self.current_index, + ChunkBreakFlags { + has_breaking_newline: false, + has_breaking_tab: broken_on_tab, + can_break_after: can_break_at_current_position, + }, + font, + text_type, + ) + { + return Some(chunk); + } + + if self.should_respect_linebreaks && code_point == '\n' as u32 { + // Newline encountered, and we're supposed to preserve them. + // If we have accumulated some code points in the current chunk, commit them now and continue with + // the newline next time. + if let Some(chunk) = self.try_commit_chunk( + start_of_chunk, + self.current_index, + ChunkBreakFlags { + has_breaking_newline: false, + has_breaking_tab: broken_on_tab, + can_break_after: false, + }, + font, + text_type, + ) { + return Some(chunk); + } + + // Otherwise, commit the newline! + self.current_index = self.next_grapheme_boundary(); + let chunk = self.try_commit_chunk( + start_of_chunk, + self.current_index, + ChunkBreakFlags { + has_breaking_newline: true, + has_breaking_tab: broken_on_tab, + can_break_after: false, + }, + font, + text_type, + ); + return Some(chunk.expect("newline chunk must be non-empty")); + } + + // If both this code point and the previous code point are collapsible, skip code points until we're at + // a non-collapsible code point. + if self.is_collapsible(code_point) + && self.current_index > 0 + && self.is_collapsible(code_point_at(self.text, self.current_index - 1)) + { + let chunk = self.try_commit_chunk( + start_of_chunk, + self.current_index, + ChunkBreakFlags { + has_breaking_newline: false, + has_breaking_tab: broken_on_tab, + can_break_after: false, + }, + font, + text_type, + ); + + while self.current_index < self.text.len() && self.is_collapsible(self.current_code_point()) { + self.current_index = self.next_grapheme_boundary(); + } + + if let Some(chunk) = chunk { + return Some(chunk); + } + + continue 'restart; + } + + // NB: The current index can sit past the end here after consuming trailing tabs; the retired C++ + // iterator read the direction class through it anyway, so the bounds check turns an out-of-bounds + // read into a non-split. + if self.should_wrap_lines + && self.current_index < self.text.len() + && text_type != self.current_text_type() + && let Some(chunk) = self.try_commit_chunk( + start_of_chunk, + self.current_index, + ChunkBreakFlags { + has_breaking_newline: false, + has_breaking_tab: broken_on_tab, + can_break_after: can_break_at_current_position, + }, + font, + text_type, + ) + { + return Some(chunk); + } + + if self.should_wrap_lines { + if code_point_is_ascii_space(code_point) { + // Whitespace encountered, and we're allowed to break on whitespace. + // If we have accumulated some code points in the current chunk, commit them now and continue + // with the whitespace next time. + if let Some(chunk) = self.try_commit_chunk( + start_of_chunk, + self.current_index, + ChunkBreakFlags { + has_breaking_newline: false, + has_breaking_tab: broken_on_tab, + can_break_after: false, + }, + font, + text_type, + ) { + return Some(chunk); + } + + // Otherwise, commit the whitespace! + self.current_index = self.next_grapheme_boundary(); + can_break_at_current_position = self.is_at_line_break_opportunity(); + let space_font = self.font_for_space(self.current_index, code_point); + if let Some(chunk) = self.try_commit_chunk( + start_of_chunk, + self.current_index, + ChunkBreakFlags { + has_breaking_newline: false, + has_breaking_tab: broken_on_tab, + can_break_after: false, + }, + space_font, + text_type, + ) { + return Some(chunk); + } + continue; + } + + if can_break_at_current_position + && let Some(chunk) = self.try_commit_chunk( + start_of_chunk, + self.current_index, + ChunkBreakFlags { + has_breaking_newline: false, + has_breaking_tab: broken_on_tab, + can_break_after: true, + }, + font, + text_type, + ) + { + return Some(chunk); + } + } + + self.current_index = self.next_grapheme_boundary(); + can_break_at_current_position = self.is_at_line_break_opportunity(); + } + + if start_of_chunk != self.text.len() { + // Try to output whatever's left at the end of the text node. + if let Some(chunk) = self.try_commit_chunk( + start_of_chunk, + self.text.len(), + ChunkBreakFlags { + has_breaking_newline: false, + has_breaking_tab: broken_on_tab, + can_break_after: false, + }, + font, + text_type, + ) { + return Some(chunk); + } + } + + return None; + } + } +}