2022-01-17 15:07:19 +01:00
/*
2024-10-04 13:19:50 +02:00
* Copyright ( c ) 2022 , Andreas Kling < andreas @ ladybird . org >
2022-01-17 15:07:19 +01:00
*
* SPDX - License - Identifier : BSD - 2 - Clause
*/
2024-12-05 01:21:45 +01:00
# include <LibGfx/Font/FontVariant.h>
2025-08-08 21:17:33 +10:00
# include <LibWeb/HTML/FormAssociatedElement.h>
2022-01-17 15:07:19 +01:00
# include <LibWeb/Layout/BreakNode.h>
2022-02-14 15:52:29 +01:00
# include <LibWeb/Layout/InlineFormattingContext.h>
2022-01-17 15:07:19 +01:00
# include <LibWeb/Layout/InlineLevelIterator.h>
# include <LibWeb/Layout/InlineNode.h>
2022-01-20 16:17:29 +01:00
# include <LibWeb/Layout/ListItemMarkerBox.h>
2022-01-17 15:07:19 +01:00
# include <LibWeb/Layout/ReplacedBox.h>
namespace Web : : Layout {
2024-03-15 19:25:00 +01:00
InlineLevelIterator : : InlineLevelIterator ( Layout : : InlineFormattingContext & inline_formatting_context , Layout : : LayoutState & layout_state , Layout : : BlockContainer const & containing_block , LayoutState : : UsedValues const & containing_block_used_values , LayoutMode layout_mode )
2022-02-14 15:52:29 +01:00
: m_inline_formatting_context ( inline_formatting_context )
2022-07-16 23:43:48 +02:00
, m_layout_state ( layout_state )
2024-03-15 19:25:00 +01:00
, m_containing_block ( containing_block )
, m_containing_block_used_values ( containing_block_used_values )
, m_next_node ( containing_block . first_child ( ) )
2022-02-14 15:52:29 +01:00
, m_layout_mode ( layout_mode )
{
skip_to_next ( ) ;
}
2022-02-20 15:51:24 +01:00
void InlineLevelIterator : : enter_node_with_box_model_metrics ( Layout : : NodeWithStyleAndBoxModelMetrics const & node )
2022-02-14 15:52:29 +01:00
{
if ( ! m_extra_leading_metrics . has_value ( ) )
m_extra_leading_metrics = ExtraBoxMetrics { } ;
2022-02-20 15:51:24 +01:00
// FIXME: It's really weird that *this* is where we assign box model metrics for these layout nodes..
2022-02-14 15:52:29 +01:00
2022-07-16 23:43:48 +02:00
auto & used_values = m_layout_state . get_mutable ( node ) ;
2022-02-20 15:51:24 +01:00
auto const & computed_values = node . computed_values ( ) ;
2025-09-01 12:51:52 +01:00
used_values . margin_top = computed_values . margin ( ) . top ( ) . to_px_or_zero ( node , m_containing_block_used_values . content_width ( ) ) ;
used_values . margin_bottom = computed_values . margin ( ) . bottom ( ) . to_px_or_zero ( node , m_containing_block_used_values . content_width ( ) ) ;
2025-02-22 14:59:09 +01:00
2025-09-01 12:51:52 +01:00
used_values . margin_left = computed_values . margin ( ) . left ( ) . to_px_or_zero ( node , m_containing_block_used_values . content_width ( ) ) ;
2022-07-16 23:43:48 +02:00
used_values . border_left = computed_values . border_left ( ) . width ;
2025-09-01 12:51:52 +01:00
used_values . padding_left = computed_values . padding ( ) . left ( ) . to_px_or_zero ( node , m_containing_block_used_values . content_width ( ) ) ;
2022-02-20 15:51:24 +01:00
2025-09-01 12:51:52 +01:00
used_values . margin_right = computed_values . margin ( ) . right ( ) . to_px_or_zero ( node , m_containing_block_used_values . content_width ( ) ) ;
2025-02-22 14:59:09 +01:00
used_values . border_right = computed_values . border_right ( ) . width ;
2025-09-01 12:51:52 +01:00
used_values . padding_right = computed_values . padding ( ) . right ( ) . to_px_or_zero ( node , m_containing_block_used_values . content_width ( ) ) ;
2025-02-22 14:59:09 +01:00
2024-10-15 20:08:48 +02:00
used_values . border_top = computed_values . border_top ( ) . width ;
used_values . border_bottom = computed_values . border_bottom ( ) . width ;
2025-09-01 12:51:52 +01:00
used_values . padding_bottom = computed_values . padding ( ) . bottom ( ) . to_px_or_zero ( node , m_containing_block_used_values . content_width ( ) ) ;
used_values . padding_top = computed_values . padding ( ) . top ( ) . to_px_or_zero ( node , m_containing_block_used_values . content_width ( ) ) ;
2024-04-23 13:34:09 -06:00
2022-07-16 23:43:48 +02:00
m_extra_leading_metrics - > margin + = used_values . margin_left ;
m_extra_leading_metrics - > border + = used_values . border_left ;
m_extra_leading_metrics - > padding + = used_values . padding_left ;
2022-02-14 15:52:29 +01:00
2023-08-15 09:34:06 +02:00
// Now's our chance to resolve the inset properties for this node.
2024-11-11 15:54:21 +01:00
m_inline_formatting_context . compute_inset ( node , m_inline_formatting_context . content_box_rect ( m_containing_block_used_values ) . size ( ) ) ;
2023-08-15 09:34:06 +02:00
2022-02-14 15:52:29 +01:00
m_box_model_node_stack . append ( node ) ;
}
void InlineLevelIterator : : exit_node_with_box_model_metrics ( )
{
if ( ! m_extra_trailing_metrics . has_value ( ) )
m_extra_trailing_metrics = ExtraBoxMetrics { } ;
auto & node = m_box_model_node_stack . last ( ) ;
2022-07-16 23:43:48 +02:00
auto & used_values = m_layout_state . get_mutable ( node ) ;
2022-02-14 15:52:29 +01:00
2022-07-16 23:43:48 +02:00
m_extra_trailing_metrics - > margin + = used_values . margin_right ;
m_extra_trailing_metrics - > border + = used_values . border_right ;
m_extra_trailing_metrics - > padding + = used_values . padding_right ;
2022-02-14 15:52:29 +01:00
m_box_model_node_stack . take_last ( ) ;
}
2022-01-20 12:18:25 +01:00
// This is similar to Layout::Node::next_in_pre_order() but will not descend into inline-block nodes.
2022-02-20 15:51:24 +01:00
Layout : : Node const * InlineLevelIterator : : next_inline_node_in_pre_order ( Layout : : Node const & current , Layout : : Node const * stay_within )
2022-01-20 12:18:25 +01:00
{
2022-10-06 16:21:11 +02:00
if ( current . first_child ( )
& & current . first_child ( ) - > display ( ) . is_inline_outside ( )
& & current . display ( ) . is_flow_inside ( )
& & ! current . is_replaced_box ( ) ) {
2022-03-22 19:18:05 +01:00
if ( ! current . is_box ( ) | | ! static_cast < Box const & > ( current ) . is_out_of_flow ( m_inline_formatting_context ) )
return current . first_child ( ) ;
}
2022-01-20 12:18:25 +01:00
2022-02-20 15:51:24 +01:00
Layout : : Node const * node = & current ;
Layout : : Node const * next = nullptr ;
2022-01-20 12:18:25 +01:00
while ( ! ( next = node - > next_sibling ( ) ) ) {
node = node - > parent ( ) ;
2022-02-14 15:52:29 +01:00
// If node is the last node on the "box model node stack", pop it off.
if ( ! m_box_model_node_stack . is_empty ( )
2023-02-26 16:09:02 -07:00
& & m_box_model_node_stack . last ( ) = = node ) {
2022-02-14 15:52:29 +01:00
exit_node_with_box_model_metrics ( ) ;
}
2022-01-20 12:18:25 +01:00
if ( ! node | | node = = stay_within )
return nullptr ;
}
2022-02-26 09:25:24 +01:00
// If node is the last node on the "box model node stack", pop it off.
if ( ! m_box_model_node_stack . is_empty ( )
2023-02-26 16:09:02 -07:00
& & m_box_model_node_stack . last ( ) = = node ) {
2022-02-26 09:25:24 +01:00
exit_node_with_box_model_metrics ( ) ;
}
2022-01-20 12:18:25 +01:00
return next ;
}
2022-02-14 15:52:29 +01:00
void InlineLevelIterator : : compute_next ( )
2022-01-17 15:07:19 +01:00
{
2022-02-14 15:52:29 +01:00
if ( m_next_node = = nullptr )
return ;
2022-01-17 15:07:19 +01:00
do {
2024-03-15 19:25:00 +01:00
m_next_node = next_inline_node_in_pre_order ( * m_next_node , m_containing_block ) ;
2024-04-25 21:10:30 +02:00
if ( m_next_node & & m_next_node - > is_svg_mask_box ( ) ) {
// NOTE: It is possible to encounter SVGMaskBox nodes while doing layout of formatting context established by <foreignObject> with a mask.
// We should skip and let SVGFormattingContext take care of them.
m_next_node = m_next_node - > next_sibling ( ) ;
}
2022-03-22 19:18:05 +01:00
} while ( m_next_node & & ( ! m_next_node - > is_inline ( ) & & ! m_next_node - > is_out_of_flow ( m_inline_formatting_context ) ) ) ;
2022-02-14 15:52:29 +01:00
}
void InlineLevelIterator : : skip_to_next ( )
{
2022-10-06 16:21:11 +02:00
if ( m_next_node
& & is < Layout : : NodeWithStyleAndBoxModelMetrics > ( * m_next_node )
& & m_next_node - > display ( ) . is_flow_inside ( )
& & ! m_next_node - > is_out_of_flow ( m_inline_formatting_context )
& & ! m_next_node - > is_replaced_box ( ) )
2022-02-20 15:51:24 +01:00
enter_node_with_box_model_metrics ( static_cast < Layout : : NodeWithStyleAndBoxModelMetrics const & > ( * m_next_node ) ) ;
2022-02-14 15:52:29 +01:00
m_current_node = m_next_node ;
compute_next ( ) ;
2022-01-17 15:07:19 +01:00
}
2023-08-17 05:38:52 +00:00
Optional < InlineLevelIterator : : Item > InlineLevelIterator : : next ( )
2023-08-19 03:36:53 +00:00
{
if ( m_lookahead_items . is_empty ( ) )
return next_without_lookahead ( ) ;
return m_lookahead_items . dequeue ( ) ;
}
CSSPixels InlineLevelIterator : : next_non_whitespace_sequence_width ( )
{
CSSPixels next_width = 0 ;
for ( ; ; ) {
auto next_item_opt = next_without_lookahead ( ) ;
if ( ! next_item_opt . has_value ( ) )
break ;
m_lookahead_items . enqueue ( next_item_opt . release_value ( ) ) ;
auto & next_item = m_lookahead_items . tail ( ) ;
2023-08-22 06:38:17 +00:00
if ( next_item . type = = InlineLevelIterator : : Item : : Type : : ForcedBreak )
break ;
2025-05-22 00:31:24 +12:00
if ( next_item . node - > computed_values ( ) . text_wrap_mode ( ) = = CSS : : TextWrapMode : : Wrap ) {
2023-08-19 03:36:53 +00:00
if ( next_item . type ! = InlineLevelIterator : : Item : : Type : : Text )
break ;
if ( next_item . is_collapsible_whitespace )
break ;
2025-08-08 21:13:35 +10:00
auto const & next_text_node = as < Layout : : TextNode > ( * ( next_item . node ) ) ;
2025-07-25 09:34:41 -04:00
auto next_view = next_text_node . text_for_rendering ( ) . substring_view ( next_item . offset_in_node , next_item . length_in_node ) ;
if ( next_view . is_ascii_whitespace ( ) )
2023-08-19 03:36:53 +00:00
break ;
}
next_width + = next_item . border_box_width ( ) ;
}
return next_width ;
}
2024-08-18 17:58:05 +01:00
Gfx : : GlyphRun : : TextType InlineLevelIterator : : resolve_text_direction_from_context ( )
{
VERIFY ( m_text_node_context . has_value ( ) ) ;
Optional < Gfx : : GlyphRun : : TextType > next_known_direction ;
for ( size_t i = 0 ; ; + + i ) {
auto peek = m_text_node_context - > chunk_iterator . peek ( i ) ;
if ( ! peek . has_value ( ) )
break ;
if ( peek - > text_type = = Gfx : : GlyphRun : : TextType : : Ltr | | peek - > text_type = = Gfx : : GlyphRun : : TextType : : Rtl ) {
next_known_direction = peek - > text_type ;
break ;
}
}
auto last_known_direction = m_text_node_context - > last_known_direction ;
if ( last_known_direction . has_value ( ) & & next_known_direction . has_value ( ) & & * last_known_direction ! = * next_known_direction ) {
switch ( m_containing_block - > computed_values ( ) . direction ( ) ) {
case CSS : : Direction : : Ltr :
return Gfx : : GlyphRun : : TextType : : Ltr ;
case CSS : : Direction : : Rtl :
return Gfx : : GlyphRun : : TextType : : Rtl ;
}
}
if ( last_known_direction . has_value ( ) )
return * last_known_direction ;
if ( next_known_direction . has_value ( ) )
return * next_known_direction ;
return Gfx : : GlyphRun : : TextType : : ContextDependent ;
}
2024-12-05 01:21:45 +01:00
HashMap < StringView , u8 > InlineLevelIterator : : shape_features_map ( ) const
{
HashMap < StringView , u8 > features ;
2025-08-08 21:13:35 +10:00
auto const & computed_values = m_current_node - > computed_values ( ) ;
2024-12-05 01:21:45 +01:00
// 6.4 https://drafts.csswg.org/css-fonts/#font-variant-ligatures-prop
auto ligature_or_null = computed_values . font_variant_ligatures ( ) ;
2025-06-27 15:26:39 +01:00
auto disable_all_ligatures = [ & ] ( ) {
features . set ( " liga " sv , 0 ) ;
features . set ( " clig " sv , 0 ) ;
features . set ( " dlig " sv , 0 ) ;
features . set ( " hlig " sv , 0 ) ;
features . set ( " calt " sv , 0 ) ;
} ;
2024-12-05 01:21:45 +01:00
if ( ligature_or_null . has_value ( ) ) {
auto ligature = ligature_or_null . release_value ( ) ;
if ( ligature . none ) {
2025-06-27 08:19:58 +01:00
// Specifies that all types of ligatures and contextual forms covered by this property are explicitly disabled.
2025-06-27 15:26:39 +01:00
disable_all_ligatures ( ) ;
2024-12-05 01:21:45 +01:00
} else {
switch ( ligature . common ) {
case Gfx : : FontVariantLigatures : : Common : : Common :
// Enables display of common ligatures (OpenType features: liga, clig).
features . set ( " liga " sv , 1 ) ;
features . set ( " clig " sv , 1 ) ;
break ;
case Gfx : : FontVariantLigatures : : Common : : NoCommon :
// Disables display of common ligatures (OpenType features: liga, clig).
features . set ( " liga " sv , 0 ) ;
features . set ( " clig " sv , 0 ) ;
break ;
case Gfx : : FontVariantLigatures : : Common : : Unset :
break ;
}
switch ( ligature . discretionary ) {
case Gfx : : FontVariantLigatures : : Discretionary : : Discretionary :
// Enables display of discretionary ligatures (OpenType feature: dlig).
features . set ( " dlig " sv , 1 ) ;
break ;
case Gfx : : FontVariantLigatures : : Discretionary : : NoDiscretionary :
// Disables display of discretionary ligatures (OpenType feature: dlig).
features . set ( " dlig " sv , 0 ) ;
break ;
case Gfx : : FontVariantLigatures : : Discretionary : : Unset :
break ;
}
switch ( ligature . historical ) {
case Gfx : : FontVariantLigatures : : Historical : : Historical :
// Enables display of historical ligatures (OpenType feature: hlig).
features . set ( " hlig " sv , 1 ) ;
break ;
case Gfx : : FontVariantLigatures : : Historical : : NoHistorical :
// Disables display of historical ligatures (OpenType feature: hlig).
features . set ( " hlig " sv , 0 ) ;
break ;
case Gfx : : FontVariantLigatures : : Historical : : Unset :
break ;
}
switch ( ligature . contextual ) {
case Gfx : : FontVariantLigatures : : Contextual : : Contextual :
// Enables display of contextual ligatures (OpenType feature: calt).
features . set ( " calt " sv , 1 ) ;
break ;
case Gfx : : FontVariantLigatures : : Contextual : : NoContextual :
// Disables display of contextual ligatures (OpenType feature: calt).
features . set ( " calt " sv , 0 ) ;
break ;
case Gfx : : FontVariantLigatures : : Contextual : : Unset :
break ;
}
}
2025-06-27 15:26:39 +01:00
} else if ( computed_values . text_rendering ( ) = = CSS : : TextRendering : : Optimizespeed ) {
// AD-HOC: Disable ligatures if font-variant-ligatures is set to normal and text rendering is set to optimize speed.
disable_all_ligatures ( ) ;
2024-12-05 01:21:45 +01:00
} else {
// A value of normal specifies that common default features are enabled, as described in detail in the next section.
features . set ( " liga " sv , 1 ) ;
features . set ( " clig " sv , 1 ) ;
}
// 6.5 https://drafts.csswg.org/css-fonts/#font-variant-position-prop
switch ( computed_values . font_variant_position ( ) ) {
case CSS : : FontVariantPosition : : Normal :
// None of the features listed below are enabled.
break ;
case CSS : : FontVariantPosition : : Sub :
// Enables display of subscripts (OpenType feature: subs).
features . set ( " subs " sv , 1 ) ;
break ;
case CSS : : FontVariantPosition : : Super :
// Enables display of superscripts (OpenType feature: sups).
features . set ( " sups " sv , 1 ) ;
break ;
default :
break ;
}
// 6.6 https://drafts.csswg.org/css-fonts/#font-variant-caps-prop
switch ( computed_values . font_variant_caps ( ) ) {
case CSS : : FontVariantCaps : : Normal :
// None of the features listed below are enabled.
break ;
case CSS : : FontVariantCaps : : SmallCaps :
// Enables display of small capitals (OpenType feature: smcp). Small-caps glyphs typically use the form of uppercase letters but are reduced to the size of lowercase letters.
features . set ( " smcp " sv , 1 ) ;
break ;
case CSS : : FontVariantCaps : : AllSmallCaps :
// Enables display of small capitals for both upper and lowercase letters (OpenType features: c2sc, smcp).
features . set ( " c2sc " sv , 1 ) ;
features . set ( " smcp " sv , 1 ) ;
break ;
case CSS : : FontVariantCaps : : PetiteCaps :
// Enables display of petite capitals (OpenType feature: pcap).
features . set ( " pcap " sv , 1 ) ;
break ;
case CSS : : FontVariantCaps : : AllPetiteCaps :
// Enables display of petite capitals for both upper and lowercase letters (OpenType features: c2pc, pcap).
features . set ( " c2pc " sv , 1 ) ;
features . set ( " pcap " sv , 1 ) ;
break ;
case CSS : : FontVariantCaps : : Unicase :
// Enables display of mixture of small capitals for uppercase letters with normal lowercase letters (OpenType feature: unic).
features . set ( " unic " sv , 1 ) ;
break ;
case CSS : : FontVariantCaps : : TitlingCaps :
// Enables display of titling capitals (OpenType feature: titl).
features . set ( " titl " sv , 1 ) ;
break ;
default :
break ;
}
// 6.7 https://drafts.csswg.org/css-fonts/#font-variant-numeric-prop
auto numeric_or_null = computed_values . font_variant_numeric ( ) ;
if ( numeric_or_null . has_value ( ) ) {
auto numeric = numeric_or_null . release_value ( ) ;
if ( numeric . figure = = Gfx : : FontVariantNumeric : : Figure : : Oldstyle ) {
// Enables display of old-style numerals (OpenType feature: onum).
features . set ( " onum " sv , 1 ) ;
} else if ( numeric . figure = = Gfx : : FontVariantNumeric : : Figure : : Lining ) {
// Enables display of lining numerals (OpenType feature: lnum).
features . set ( " lnum " sv , 1 ) ;
}
if ( numeric . spacing = = Gfx : : FontVariantNumeric : : Spacing : : Proportional ) {
// Enables display of proportional numerals (OpenType feature: pnum).
features . set ( " pnum " sv , 1 ) ;
} else if ( numeric . spacing = = Gfx : : FontVariantNumeric : : Spacing : : Tabular ) {
// Enables display of tabular numerals (OpenType feature: tnum).
features . set ( " tnum " sv , 1 ) ;
}
if ( numeric . fraction = = Gfx : : FontVariantNumeric : : Fraction : : Diagonal ) {
// Enables display of diagonal fractions (OpenType feature: frac).
features . set ( " frac " sv , 1 ) ;
} else if ( numeric . fraction = = Gfx : : FontVariantNumeric : : Fraction : : Stacked ) {
// Enables display of stacked fractions (OpenType feature: afrc).
features . set ( " afrc " sv , 1 ) ;
features . set ( " afrc " sv , 1 ) ;
}
if ( numeric . ordinal ) {
// Enables display of letter forms used with ordinal numbers (OpenType feature: ordn).
features . set ( " ordn " sv , 1 ) ;
}
if ( numeric . slashed_zero ) {
// Enables display of slashed zeros (OpenType feature: zero).
features . set ( " zero " sv , 1 ) ;
}
}
// 6.10 https://drafts.csswg.org/css-fonts/#font-variant-east-asian-prop
auto east_asian_or_null = computed_values . font_variant_east_asian ( ) ;
if ( east_asian_or_null . has_value ( ) ) {
auto east_asian = east_asian_or_null . release_value ( ) ;
switch ( east_asian . variant ) {
case Gfx : : FontVariantEastAsian : : Variant : : Jis78 :
// Enables display of JIS78 forms (OpenType feature: jp78).
features . set ( " jp78 " sv , 1 ) ;
break ;
case Gfx : : FontVariantEastAsian : : Variant : : Jis83 :
// Enables display of JIS83 forms (OpenType feature: jp83).
features . set ( " jp83 " sv , 1 ) ;
break ;
case Gfx : : FontVariantEastAsian : : Variant : : Jis90 :
// Enables display of JIS90 forms (OpenType feature: jp90).
features . set ( " jp90 " sv , 1 ) ;
break ;
case Gfx : : FontVariantEastAsian : : Variant : : Jis04 :
// Enables display of JIS04 forms (OpenType feature: jp04).
features . set ( " jp04 " sv , 1 ) ;
break ;
case Gfx : : FontVariantEastAsian : : Variant : : Simplified :
// Enables display of simplified forms (OpenType feature: smpl).
features . set ( " smpl " sv , 1 ) ;
break ;
case Gfx : : FontVariantEastAsian : : Variant : : Traditional :
// Enables display of traditional forms (OpenType feature: trad).
features . set ( " trad " sv , 1 ) ;
break ;
default :
break ;
}
switch ( east_asian . width ) {
case Gfx : : FontVariantEastAsian : : Width : : FullWidth :
// Enables display of full-width forms (OpenType feature: fwid).
features . set ( " fwid " sv , 1 ) ;
break ;
case Gfx : : FontVariantEastAsian : : Width : : Proportional :
// Enables display of proportional-width forms (OpenType feature: pwid).
features . set ( " pwid " sv , 1 ) ;
break ;
default :
break ;
}
if ( east_asian . ruby ) {
// Enables display of ruby forms (OpenType feature: ruby).
features . set ( " ruby " sv , 1 ) ;
}
}
2025-06-22 18:59:42 +01:00
// FIXME: vkrn should be enabled for vertical text.
switch ( computed_values . font_kerning ( ) ) {
case CSS : : FontKerning : : Auto :
2025-06-27 08:00:27 +01:00
// AD-HOC: Disable kerning if font-kerning is set to normal and text rendering is set to optimize speed.
features . set ( " kern " sv , computed_values . text_rendering ( ) ! = CSS : : TextRendering : : Optimizespeed ? 1 : 0 ) ;
break ;
2025-06-22 18:59:42 +01:00
case CSS : : FontKerning : : Normal :
features . set ( " kern " sv , 1 ) ;
break ;
case CSS : : FontKerning : : None :
features . set ( " kern " sv , 0 ) ;
break ;
default :
break ;
}
2024-12-05 01:21:45 +01:00
return features ;
}
Gfx : : ShapeFeatures InlineLevelIterator : : create_and_merge_font_features ( ) const
{
HashMap < StringView , u8 > merged_features ;
2025-08-08 21:13:35 +10:00
auto const & computed_values = m_inline_formatting_context . containing_block ( ) . computed_values ( ) ;
2024-12-05 01:21:45 +01:00
// https://www.w3.org/TR/css-fonts-3/#feature-precedence
// FIXME 1. Font features enabled by default, including features required for a given script.
// FIXME 2. If the font is defined via an @font-face rule, the font features implied by the font-feature-settings descriptor in the @font-face rule.
// 3. Font features implied by the value of the ‘ font-variant’ property, the related ‘ font-variant’ subproperties and any other CSS property that uses OpenType features (e.g. the ‘ font-kerning’ property).
2025-07-25 12:48:39 +02:00
merged_features . update ( shape_features_map ( ) ) ;
2024-12-05 01:21:45 +01:00
// FIXME 4. Feature settings determined by properties other than ‘ font-variant’ or ‘ font-feature-settings’ . For example, setting a non-default value for the ‘ letter-spacing’ property disables common ligatures.
// 5. Font features implied by the value of ‘ font-feature-settings’ property.
2025-01-22 15:00:13 +00:00
CSS : : CalculationResolutionContext calculation_context { . length_resolution_context = CSS : : Length : : ResolutionContext : : for_layout_node ( * m_current_node . ptr ( ) ) } ;
2024-12-05 01:21:45 +01:00
auto font_feature_settings = computed_values . font_feature_settings ( ) ;
if ( font_feature_settings . has_value ( ) ) {
auto const & feature_settings = font_feature_settings . value ( ) ;
for ( auto const & [ key , feature_value ] : feature_settings ) {
2025-01-22 15:00:13 +00:00
merged_features . set ( key , feature_value . resolved ( calculation_context ) . value_or ( 0 ) ) ;
2024-12-05 01:21:45 +01:00
}
}
Gfx : : ShapeFeatures shape_features ;
shape_features . ensure_capacity ( merged_features . size ( ) ) ;
for ( auto & it : merged_features ) {
2025-06-16 11:58:38 +02:00
shape_features . unchecked_append ( { { it . key [ 0 ] , it . key [ 1 ] , it . key [ 2 ] , it . key [ 3 ] } , static_cast < u32 > ( it . value ) } ) ;
2024-12-05 01:21:45 +01:00
}
return shape_features ;
}
2023-08-19 03:36:53 +00:00
Optional < InlineLevelIterator : : Item > InlineLevelIterator : : next_without_lookahead ( )
2022-01-17 15:07:19 +01:00
{
if ( ! m_current_node )
return { } ;
2025-09-12 10:06:27 +02:00
if ( auto * text_node = as_if < Layout : : TextNode > ( * m_current_node ) ) {
2022-03-27 21:14:10 +02:00
if ( ! m_text_node_context . has_value ( ) )
2025-09-12 10:06:27 +02:00
enter_text_node ( * text_node ) ;
2025-08-08 21:09:42 +10:00
else
m_text_node_context - > is_first_chunk = false ;
2022-01-17 15:07:19 +01:00
2025-08-08 21:17:33 +10:00
if ( ! m_text_node_context - > chunk_iterator . peek ( 0 ) . has_value ( ) )
m_text_node_context - > is_last_chunk = true ;
2024-08-18 17:58:05 +01:00
auto chunk_opt = m_text_node_context - > chunk_iterator . next ( ) ;
2025-08-08 21:17:33 +10:00
auto is_empty_editable = false ;
2022-01-17 15:07:19 +01:00
if ( ! chunk_opt . has_value ( ) ) {
2025-08-08 21:17:33 +10:00
auto const is_only_chunk = m_text_node_context - > is_first_chunk & & m_text_node_context - > is_last_chunk ;
if ( is_only_chunk & & text_node - > text_for_rendering ( ) . is_empty ( ) ) {
if ( auto const * shadow_root = as_if < DOM : : ShadowRoot > ( text_node - > dom_node ( ) . root ( ) ) )
if ( auto const * form_associated_element = as_if < HTML : : FormAssociatedTextControlElement > ( shadow_root - > host ( ) ) )
is_empty_editable = form_associated_element - > is_mutable ( ) ;
is_empty_editable | = text_node - > dom_node ( ) . parent ( ) & & text_node - > dom_node ( ) . parent ( ) - > is_editing_host ( ) ;
}
2022-01-17 15:07:19 +01:00
2025-08-08 21:17:33 +10:00
if ( is_empty_editable ) {
chunk_opt = m_text_node_context - > chunk_iterator . create_empty_chunk ( ) ;
} else {
m_text_node_context = { } ;
skip_to_next ( ) ;
return next_without_lookahead ( ) ;
}
}
2022-02-14 15:52:29 +01:00
2022-01-17 15:07:19 +01:00
auto & chunk = chunk_opt . value ( ) ;
2024-08-18 17:58:05 +01:00
auto text_type = chunk . text_type ;
if ( text_type = = Gfx : : GlyphRun : : TextType : : Ltr | | text_type = = Gfx : : GlyphRun : : TextType : : Rtl )
m_text_node_context - > last_known_direction = text_type ;
2025-09-12 10:06:27 +02:00
auto do_respect_linebreak = m_text_node_context - > chunk_iterator . should_respect_linebreaks ( ) ;
if ( do_respect_linebreak & & chunk . has_breaking_newline ) {
2024-08-18 17:58:05 +01:00
m_text_node_context - > is_last_chunk = true ;
if ( chunk . is_all_whitespace )
text_type = Gfx : : GlyphRun : : TextType : : EndPadding ;
}
if ( text_type = = Gfx : : GlyphRun : : TextType : : ContextDependent )
text_type = resolve_text_direction_from_context ( ) ;
2022-03-26 18:59:54 +01:00
2025-09-12 10:06:27 +02:00
if ( do_respect_linebreak & & chunk . has_breaking_newline )
return Item { . type = Item : : Type : : ForcedBreak } ;
2022-03-26 18:59:54 +01:00
2025-09-12 10:06:27 +02:00
auto letter_spacing = text_node - > computed_values ( ) . letter_spacing ( ) ;
2025-08-02 00:07:48 +12:00
// FIXME: We should apply word spacing to all word-separator characters not just breaking tabs
2025-09-12 10:06:27 +02:00
auto word_spacing = text_node - > computed_values ( ) . word_spacing ( ) ;
2024-11-05 07:11:34 +00:00
2024-10-15 18:21:49 +01:00
auto x = 0.0f ;
if ( chunk . has_breaking_tab ) {
CSSPixels accumulated_width ;
// make sure to account for any fragments that take up a portion of the measured tab stop distance
auto fragments = m_containing_block_used_values . line_boxes . last ( ) . fragments ( ) ;
for ( auto const & frag : fragments ) {
accumulated_width + = frag . width ( ) ;
}
// https://drafts.csswg.org/css-text/#tab-size-property
2025-09-12 10:06:27 +02:00
CSS : : CalculationResolutionContext calculation_context { . length_resolution_context = CSS : : Length : : ResolutionContext : : for_layout_node ( * text_node ) } ;
auto tab_size = text_node - > computed_values ( ) . tab_size ( ) ;
2024-10-15 18:21:49 +01:00
CSSPixels tab_width ;
tab_width = tab_size . visit (
[ & ] ( CSS : : LengthOrCalculated const & t ) - > CSSPixels {
2025-01-22 15:00:13 +00:00
return t . resolved ( calculation_context )
2025-09-12 10:06:27 +02:00
. map ( [ & ] ( auto & it ) { return it . to_px ( * text_node ) ; } )
2025-01-22 15:00:13 +00:00
. value_or ( 0 ) ;
2024-10-15 18:21:49 +01:00
} ,
[ & ] ( CSS : : NumberOrCalculated const & n ) - > CSSPixels {
2025-01-22 15:00:13 +00:00
auto tab_number = n . resolved ( calculation_context ) . value_or ( 0 ) ;
2024-10-15 18:21:49 +01:00
2024-10-23 08:27:22 +01:00
return CSSPixels : : nearest_value_for ( tab_number * ( chunk . font - > glyph_width ( ' ' ) + word_spacing . to_float ( ) + letter_spacing . to_float ( ) ) ) ;
2024-10-15 18:21:49 +01:00
} ) ;
// https://drafts.csswg.org/css-text/#white-space-phase-2
// if fragments have added to the width, calculate the net distance to the next tab stop, otherwise the shift will just be the tab width
auto tab_stop_dist = accumulated_width > 0 ? ( ceil ( ( accumulated_width / tab_width ) ) * tab_width ) - accumulated_width : tab_width ;
auto ch_width = chunk . font - > glyph_width ( ' 0 ' ) ;
// If this distance is less than 0.5ch, then the subsequent tab stop is used instead
if ( tab_stop_dist < ch_width * 0.5 )
tab_stop_dist + = tab_width ;
// account for consecutive tabs
auto num_of_tabs = 0 ;
for ( auto code_point : chunk . view ) {
if ( code_point ! = ' \t ' )
break ;
num_of_tabs + + ;
}
tab_stop_dist = tab_stop_dist * num_of_tabs ;
2024-10-22 13:07:26 +01:00
// remove tabs, we don't want to render them when we shape the text
chunk . view = chunk . view . substring_view ( num_of_tabs ) ;
2024-10-15 18:21:49 +01:00
x = tab_stop_dist . to_float ( ) ;
}
2024-12-05 01:21:45 +01:00
auto shape_features = create_and_merge_font_features ( ) ;
auto glyph_run = Gfx : : shape_text ( { x , 0 } , letter_spacing . to_float ( ) , chunk . view , chunk . font , text_type , shape_features ) ;
2024-10-15 18:21:49 +01:00
2025-04-19 23:36:03 +02:00
CSSPixels chunk_width = CSSPixels : : nearest_value_for ( glyph_run - > width ( ) + x ) ;
2023-10-28 15:39:57 +02:00
2022-10-14 12:30:44 +02:00
// NOTE: We never consider `content: ""` to be collapsible whitespace.
2025-08-08 21:17:33 +10:00
bool is_generated_empty_string = is_empty_editable | | ( text_node - > is_generated_for_pseudo_element ( ) & & chunk . length = = 0 ) ;
2025-09-12 10:06:27 +02:00
auto collapse_whitespace = m_text_node_context - > chunk_iterator . should_collapse_whitespace ( ) ;
2022-10-14 12:30:44 +02:00
2022-01-17 15:07:19 +01:00
Item item {
. type = Item : : Type : : Text ,
2025-09-12 10:06:27 +02:00
. node = text_node ,
2024-09-14 19:54:41 +02:00
. glyph_run = move ( glyph_run ) ,
2022-01-17 15:07:19 +01:00
. offset_in_node = chunk . start ,
. length_in_node = chunk . length ,
. width = chunk_width ,
2025-09-12 10:06:27 +02:00
. is_collapsible_whitespace = collapse_whitespace & & chunk . is_all_whitespace & & ! is_generated_empty_string ,
2022-01-17 15:07:19 +01:00
} ;
2022-02-14 15:52:29 +01:00
add_extra_box_model_metrics_to_item ( item , m_text_node_context - > is_first_chunk , m_text_node_context - > is_last_chunk ) ;
2022-01-17 15:07:19 +01:00
return item ;
}
2022-03-02 14:53:39 +01:00
if ( m_current_node - > is_absolutely_positioned ( ) ) {
2025-08-08 21:13:35 +10:00
auto const & node = * m_current_node ;
2022-02-15 01:54:22 +01:00
skip_to_next ( ) ;
2022-03-07 22:27:09 +01:00
return Item {
. type = Item : : Type : : AbsolutelyPositionedElement ,
. node = & node ,
} ;
2022-02-15 01:54:22 +01:00
}
2022-03-22 19:18:05 +01:00
if ( m_current_node - > is_floating ( ) ) {
2025-08-08 21:13:35 +10:00
auto const & node = * m_current_node ;
2022-03-22 19:18:05 +01:00
skip_to_next ( ) ;
return Item {
. type = Item : : Type : : FloatingElement ,
. node = & node ,
} ;
}
2022-01-17 15:07:19 +01:00
if ( is < Layout : : BreakNode > ( * m_current_node ) ) {
2025-08-08 21:13:35 +10:00
auto const & node = * m_current_node ;
2022-01-17 15:07:19 +01:00
skip_to_next ( ) ;
return Item {
. type = Item : : Type : : ForcedBreak ,
2023-07-19 23:44:40 +00:00
. node = & node ,
2022-01-17 15:07:19 +01:00
} ;
}
2022-01-20 16:17:29 +01:00
if ( is < Layout : : ListItemMarkerBox > ( * m_current_node ) ) {
skip_to_next ( ) ;
2023-08-19 03:36:53 +00:00
return next_without_lookahead ( ) ;
2022-01-20 16:17:29 +01:00
}
2022-01-17 15:07:19 +01:00
if ( ! is < Layout : : Box > ( * m_current_node ) ) {
skip_to_next ( ) ;
2023-08-19 03:36:53 +00:00
return next_without_lookahead ( ) ;
2022-01-17 15:07:19 +01:00
}
if ( is < Layout : : ReplacedBox > ( * m_current_node ) ) {
2025-08-08 21:13:35 +10:00
auto const & replaced_box = static_cast < Layout : : ReplacedBox const & > ( * m_current_node ) ;
2022-02-20 15:51:24 +01:00
// FIXME: This const_cast is gross.
const_cast < Layout : : ReplacedBox & > ( replaced_box ) . prepare_for_replaced_layout ( ) ;
2022-01-17 15:07:19 +01:00
}
2025-08-08 21:13:35 +10:00
auto const & box = as < Layout : : Box > ( * m_current_node ) ;
auto const & box_state = m_layout_state . get ( box ) ;
2022-02-14 15:52:29 +01:00
m_inline_formatting_context . dimension_box_on_line ( box , m_layout_mode ) ;
2022-01-17 15:07:19 +01:00
2022-02-14 15:52:29 +01:00
auto item = Item {
2022-01-17 15:07:19 +01:00
. type = Item : : Type : : Element ,
. node = & box ,
. offset_in_node = 0 ,
. length_in_node = 0 ,
2022-07-17 17:59:02 +02:00
. width = box_state . content_width ( ) ,
2022-02-20 15:51:24 +01:00
. padding_start = box_state . padding_left ,
. padding_end = box_state . padding_right ,
. border_start = box_state . border_left ,
. border_end = box_state . border_right ,
. margin_start = box_state . margin_left ,
. margin_end = box_state . margin_right ,
2022-01-17 15:07:19 +01:00
} ;
2022-02-14 15:52:29 +01:00
add_extra_box_model_metrics_to_item ( item , true , true ) ;
2025-06-20 00:40:15 +02:00
skip_to_next ( ) ;
2022-02-14 15:52:29 +01:00
return item ;
2022-01-17 15:07:19 +01:00
}
2022-03-27 21:14:10 +02:00
void InlineLevelIterator : : enter_text_node ( Layout : : TextNode const & text_node )
2022-01-17 15:07:19 +01:00
{
2025-05-22 00:31:24 +12:00
auto white_space_collapse = text_node . computed_values ( ) . white_space_collapse ( ) ;
auto text_wrap_mode = text_node . computed_values ( ) . text_wrap_mode ( ) ;
2025-09-12 10:06:27 +02:00
// https://drafts.csswg.org/css-text-4/#collapse
2025-05-22 00:31:24 +12:00
bool do_wrap_lines = text_wrap_mode = = CSS : : TextWrapMode : : Wrap ;
2025-09-12 10:06:27 +02:00
bool do_respect_linebreaks = first_is_one_of ( white_space_collapse , CSS : : WhiteSpaceCollapse : : Preserve , CSS : : WhiteSpaceCollapse : : PreserveBreaks , CSS : : WhiteSpaceCollapse : : BreakSpaces ) ;
2022-11-26 00:14:36 +01:00
2022-01-17 15:07:19 +01:00
m_text_node_context = TextNodeContext {
2022-02-14 15:52:29 +01:00
. is_first_chunk = true ,
. is_last_chunk = false ,
2024-09-22 10:03:23 -04:00
. chunk_iterator = TextNode : : ChunkIterator { text_node , do_wrap_lines , do_respect_linebreaks } ,
2022-01-17 15:07:19 +01:00
} ;
2022-02-14 15:52:29 +01:00
}
void InlineLevelIterator : : add_extra_box_model_metrics_to_item ( Item & item , bool add_leading_metrics , bool add_trailing_metrics )
{
if ( add_leading_metrics & & m_extra_leading_metrics . has_value ( ) ) {
item . margin_start + = m_extra_leading_metrics - > margin ;
item . border_start + = m_extra_leading_metrics - > border ;
item . padding_start + = m_extra_leading_metrics - > padding ;
m_extra_leading_metrics = { } ;
}
if ( add_trailing_metrics & & m_extra_trailing_metrics . has_value ( ) ) {
item . margin_end + = m_extra_trailing_metrics - > margin ;
item . border_end + = m_extra_trailing_metrics - > border ;
item . padding_end + = m_extra_trailing_metrics - > padding ;
m_extra_trailing_metrics = { } ;
}
2022-01-17 15:07:19 +01:00
}
}