LibWeb: Apply the font-variant-emoji property to font selection

This commit is contained in:
Tim Ledbetter 2026-06-19 12:39:49 +01:00 committed by Andreas Kling
parent 57103da08d
commit 360bfd66f6
8 changed files with 58 additions and 1 deletions

View file

@ -182,6 +182,7 @@ public:
static CursorData cursor() { return { CursorPredefined::Auto }; }
static WhiteSpaceCollapse white_space_collapse() { return WhiteSpaceCollapse::Collapse; }
static WordBreak word_break() { return WordBreak::Normal; }
static FontVariantEmoji font_variant_emoji() { return FontVariantEmoji::Normal; }
static CSSPixels word_spacing() { return 0; }
static CSSPixels letter_spacing() { return 0; }
static Variant<Length, double> tab_size() { return 8; }
@ -574,6 +575,7 @@ public:
WhiteSpaceCollapse white_space_collapse() const { return m_inherited.white_space_collapse; }
WhiteSpaceTrimData white_space_trim() const { return m_noninherited.white_space_trim; }
WordBreak word_break() const { return m_inherited.word_break; }
FontVariantEmoji font_variant_emoji() const { return m_inherited.font_variant_emoji; }
CSSPixels const& word_spacing() const { return m_inherited.word_spacing; }
CSSPixels letter_spacing() const { return m_inherited.letter_spacing; }
FlexDirection flex_direction() const { return m_noninherited.flex_direction; }
@ -805,6 +807,7 @@ protected:
CSSPixels text_underline_offset { InitialValues::text_underline_offset() };
WhiteSpaceCollapse white_space_collapse { InitialValues::white_space_collapse() };
WordBreak word_break { InitialValues::word_break() };
FontVariantEmoji font_variant_emoji { InitialValues::font_variant_emoji() };
ListStylePosition list_style_position { InitialValues::list_style_position() };
Visibility visibility { InitialValues::visibility() };
CSSPixels word_spacing { InitialValues::word_spacing() };
@ -1027,6 +1030,7 @@ public:
void set_white_space_trim(WhiteSpaceTrimData value) { m_noninherited.white_space_trim = value; }
void set_word_spacing(CSSPixels value) { m_inherited.word_spacing = value; }
void set_word_break(WordBreak value) { m_inherited.word_break = value; }
void set_font_variant_emoji(FontVariantEmoji value) { m_inherited.font_variant_emoji = value; }
void set_letter_spacing(CSSPixels value) { m_inherited.letter_spacing = value; }
void set_width(Size const& width) { m_noninherited.width = width; }
void set_min_width(Size const& width) { m_noninherited.min_width = width; }

View file

@ -709,6 +709,7 @@ void NodeWithStyle::apply_style(CSS::ComputedProperties const& computed_style)
computed_values.set_font_size(computed_style.font_size());
computed_values.set_font_weight(computed_style.font_weight());
computed_values.set_line_height(computed_style.line_height());
computed_values.set_font_variant_emoji(computed_style.font_variant_emoji());
// NOTE: color must be set after color-scheme to ensure currentColor can be resolved in other properties (e.g. background-color).
// NOTE: color must be set after font_size as `CalculatedStyleValue`s can rely on it being set for resolving lengths.

View file

@ -513,6 +513,7 @@ TextNode::ChunkList const& TextNode::chunks_for_layout(bool should_wrap_lines, b
.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(),
.font_cascade_list = computed_values.font_list(),
};
@ -558,6 +559,7 @@ TextNode::ChunkIterator::ChunkIterator(TextNode const& text_node, Utf16View cons
, m_grapheme_segmenter(grapheme_segmenter)
, m_line_segmenter(line_segmenter)
, m_word_break(word_break)
, m_font_variant_emoji(text_node.computed_values().font_variant_emoji())
{
m_should_collapse_whitespace = first_is_one_of(text_node.computed_values().white_space_collapse(), CSS::WhiteSpaceCollapse::Collapse, CSS::WhiteSpaceCollapse::PreserveBreaks);
}
@ -778,7 +780,22 @@ Gfx::EmojiPresentationResult TextNode::ChunkIterator::emoji_presentation_at(size
if (next_offset < m_view.length_in_code_units())
next_code_point = m_view.code_point_at(next_offset);
return Gfx::emoji_presentation_for_code_point(code_point, next_code_point);
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::Chunk> TextNode::ChunkIterator::next_without_peek()

View file

@ -83,6 +83,7 @@ public:
Unicode::Segmenter& m_grapheme_segmenter;
Unicode::Segmenter& m_line_segmenter;
CSS::WordBreak m_word_break;
CSS::FontVariantEmoji m_font_variant_emoji;
size_t m_current_index { 0 };
Vector<Chunk> m_peek_queue;
@ -130,6 +131,7 @@ private:
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 };
RefPtr<Gfx::FontCascadeList const> font_cascade_list;
bool operator==(ChunkCacheKey const&) const = default;

View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<style>
@font-face {
font-family: ColorEmojiFont;
src: url("../data/fonts/NotoColorEmoji-Regular_subset.ttf");
}
@font-face {
font-family: MonoEmojiFont;
src: url("../data/fonts/NotoEmoji-Regular_subset.ttf");
}
div { font: 48px serif; }
</style>
<div style="font-family: MonoEmojiFont; font-variant-emoji: text">&#x2139;</div>
<div style="font-family: ColorEmojiFont; font-variant-emoji: emoji">&#x2139;</div>
<div style="font-family: ColorEmojiFont; font-variant-emoji: emoji">&#x1fae8;</div>
<div style="font-family: MonoEmojiFont; font-variant-emoji: text">&#x1fae8;</div>

View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<link rel="match" href="../expected/css-font-variant-emoji-presentation-ref.html">
<style>
@font-face {
font-family: ColorEmojiFont;
src: url("../data/fonts/NotoColorEmoji-Regular_subset.ttf");
}
@font-face {
font-family: MonoEmojiFont;
src: url("../data/fonts/NotoEmoji-Regular_subset.ttf");
}
div { font: 48px ColorEmojiFont, MonoEmojiFont; }
</style>
<div style="font-variant-emoji: unicode">&#x2139;</div>
<div style="font-variant-emoji: emoji">&#x2139;</div>
<div style="font-variant-emoji: unicode">&#x1fae8;</div>
<div style="font-variant-emoji: text">&#x1fae8;</div>