1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-26 02:17:34 +00:00

LibGfx: Make text painting better at aligning vector fonts vertically

This is achieved by simplifying the logic in TextLayout. We get rid
of all the various ways that the layout bounding rect can get cropped.
Then we make sure to use the right pixel metrics.

Finally we use the font's own line gap metrics instead of hard-coding 4.

The end result is that text painted with vector fonts now gets pretty
reasonable vertical alignment in most cases.
This commit is contained in:
Andreas Kling 2023-01-06 11:55:23 +01:00
parent f5e18dda2b
commit b2d3ceaec5
4 changed files with 22 additions and 58 deletions

View file

@ -117,7 +117,7 @@ void Label::size_to_fit()
int Label::text_calculated_preferred_height() const int Label::text_calculated_preferred_height() const
{ {
return static_cast<int>(ceilf(Gfx::TextLayout(font(), Utf8View { m_text }, text_rect().to_type<float>()).bounding_rect(Gfx::TextWrapping::Wrap, Gfx::Painter::LINE_SPACING).height())); return static_cast<int>(ceilf(Gfx::TextLayout(font(), Utf8View { m_text }, text_rect().to_type<float>()).bounding_rect(Gfx::TextWrapping::Wrap).height()));
} }
Optional<UISize> Label::calculated_preferred_size() const Optional<UISize> Label::calculated_preferred_size() const

View file

@ -1518,11 +1518,6 @@ void draw_text_line(FloatRect const& a_rect, Utf8View const& text, Font const& f
VERIFY_NOT_REACHED(); VERIFY_NOT_REACHED();
} }
if (is_vertically_centered_text_alignment(alignment)) {
auto distance_from_baseline_to_bottom = (font.pixel_size() - 1) - font.baseline();
rect.translate_by(0, distance_from_baseline_to_bottom / 2);
}
auto point = rect.location(); auto point = rect.location();
auto space_width = font.glyph_width(' ') + font.glyph_spacing(); auto space_width = font.glyph_width(' ') + font.glyph_spacing();
@ -1726,19 +1721,17 @@ void Painter::do_draw_text(FloatRect const& rect, Utf8View const& text, Font con
TextLayout layout(font, text, rect); TextLayout layout(font, text, rect);
auto line_height = font.pixel_size() + LINE_SPACING; auto line_height = font.preferred_line_height();
auto lines = layout.lines(elision, wrapping, LINE_SPACING); auto lines = layout.lines(elision, wrapping);
auto bounding_rect = layout.bounding_rect(wrapping, LINE_SPACING); auto bounding_rect = layout.bounding_rect(wrapping);
bounding_rect.align_within(rect, alignment); bounding_rect.align_within(rect, alignment);
bounding_rect.intersect(rect);
for (size_t i = 0; i < lines.size(); ++i) { for (size_t i = 0; i < lines.size(); ++i) {
auto line = Utf8View { lines[i] }; auto line = Utf8View { lines[i] };
FloatRect line_rect { bounding_rect.x(), bounding_rect.y() + i * line_height, bounding_rect.width(), line_height }; FloatRect line_rect { bounding_rect.x(), bounding_rect.y() + i * line_height, bounding_rect.width(), line_height };
line_rect.intersect(rect);
TextDirection line_direction = get_text_direction(line); TextDirection line_direction = get_text_direction(line);
if (text_contains_bidirectional_text(line, line_direction)) { // Slow Path: The line contains mixed BiDi classes if (text_contains_bidirectional_text(line, line_direction)) { // Slow Path: The line contains mixed BiDi classes

View file

@ -20,15 +20,15 @@ struct Block {
Utf8View characters; Utf8View characters;
}; };
FloatRect TextLayout::bounding_rect(TextWrapping wrapping, int line_spacing) const FloatRect TextLayout::bounding_rect(TextWrapping wrapping) const
{ {
auto lines = wrap_lines(TextElision::None, wrapping, line_spacing, FitWithinRect::No); auto lines = wrap_lines(TextElision::None, wrapping);
if (!lines.size()) { if (lines.is_empty()) {
return {}; return {};
} }
FloatRect bounding_rect = { FloatRect bounding_rect = {
0, 0, 0, (lines.size() * (m_font.pixel_size() + line_spacing)) - line_spacing 0, 0, 0, (static_cast<float>(lines.size()) * (m_font_metrics.ascent + m_font_metrics.descent + m_font_metrics.line_gap)) - m_font_metrics.line_gap
}; };
for (auto& line : lines) { for (auto& line : lines) {
@ -40,7 +40,7 @@ FloatRect TextLayout::bounding_rect(TextWrapping wrapping, int line_spacing) con
return bounding_rect; return bounding_rect;
} }
Vector<DeprecatedString, 32> TextLayout::wrap_lines(TextElision elision, TextWrapping wrapping, int line_spacing, FitWithinRect fit_within_rect) const Vector<DeprecatedString, 32> TextLayout::wrap_lines(TextElision elision, TextWrapping wrapping) const
{ {
Vector<Block> blocks; Vector<Block> blocks;
@ -106,34 +106,16 @@ Vector<DeprecatedString, 32> TextLayout::wrap_lines(TextElision elision, TextWra
}); });
} }
size_t max_lines_that_can_fit = 0;
if (m_rect.height() >= m_font.glyph_height()) {
// NOTE: If glyph height is 10 and line spacing is 1, we can fit a
// single line into a 10px rect and a 20px rect, but 2 lines into a
// 21px rect.
max_lines_that_can_fit = 1 + (m_rect.height() - m_font.glyph_height()) / (m_font.glyph_height() + line_spacing);
}
if (max_lines_that_can_fit == 0)
return {};
Vector<DeprecatedString> lines; Vector<DeprecatedString> lines;
StringBuilder builder; StringBuilder builder;
float line_width = 0; float line_width = 0;
size_t current_block = 0; size_t current_block = 0;
bool did_not_finish = false;
for (Block& block : blocks) { for (Block& block : blocks) {
switch (block.type) { switch (block.type) {
case BlockType::Newline: { case BlockType::Newline: {
lines.append(builder.to_deprecated_string()); lines.append(builder.to_deprecated_string());
builder.clear(); builder.clear();
line_width = 0; line_width = 0;
if (lines.size() == max_lines_that_can_fit && fit_within_rect == FitWithinRect::Yes) {
did_not_finish = true;
goto blocks_processed;
}
current_block++; current_block++;
continue; continue;
} }
@ -152,11 +134,6 @@ Vector<DeprecatedString, 32> TextLayout::wrap_lines(TextElision elision, TextWra
line_width = 0; line_width = 0;
} }
if (lines.size() == max_lines_that_can_fit && fit_within_rect == FitWithinRect::Yes) {
did_not_finish = true;
goto blocks_processed;
}
builder.append(block.characters.as_string()); builder.append(block.characters.as_string());
line_width += block_width; line_width += block_width;
current_block++; current_block++;
@ -164,18 +141,15 @@ Vector<DeprecatedString, 32> TextLayout::wrap_lines(TextElision elision, TextWra
} }
} }
blocks_processed: auto last_line = builder.to_deprecated_string();
if (!did_not_finish) { if (!last_line.is_empty())
auto last_line = builder.to_deprecated_string(); lines.append(last_line);
if (!last_line.is_empty())
lines.append(last_line);
}
switch (elision) { switch (elision) {
case TextElision::None: case TextElision::None:
break; break;
case TextElision::Right: { case TextElision::Right: {
lines.at(lines.size() - 1) = elide_text_from_right(Utf8View { lines.at(lines.size() - 1) }, did_not_finish); lines.at(lines.size() - 1) = elide_text_from_right(Utf8View { lines.at(lines.size() - 1) });
break; break;
} }
} }
@ -183,10 +157,10 @@ blocks_processed:
return lines; return lines;
} }
DeprecatedString TextLayout::elide_text_from_right(Utf8View text, bool force_elision) const DeprecatedString TextLayout::elide_text_from_right(Utf8View text) const
{ {
float text_width = m_font.width(text); float text_width = m_font.width(text);
if (force_elision || text_width > static_cast<unsigned>(m_rect.width())) { if (text_width > static_cast<float>(m_rect.width())) {
float ellipsis_width = m_font.width("..."sv); float ellipsis_width = m_font.width("..."sv);
float current_width = ellipsis_width; float current_width = ellipsis_width;
size_t glyph_spacing = m_font.glyph_spacing(); size_t glyph_spacing = m_font.glyph_spacing();

View file

@ -20,11 +20,6 @@
namespace Gfx { namespace Gfx {
enum class FitWithinRect {
Yes,
No
};
// FIXME: This currently isn't an ideal way of doing things; ideally, TextLayout // FIXME: This currently isn't an ideal way of doing things; ideally, TextLayout
// would be doing the rendering by painting individual glyphs. However, this // would be doing the rendering by painting individual glyphs. However, this
// would regress our Unicode bidirectional text support. Therefore, fixing this // would regress our Unicode bidirectional text support. Therefore, fixing this
@ -46,23 +41,25 @@ class TextLayout {
public: public:
TextLayout(Gfx::Font const& font, Utf8View const& text, FloatRect const& rect) TextLayout(Gfx::Font const& font, Utf8View const& text, FloatRect const& rect)
: m_font(font) : m_font(font)
, m_font_metrics(font.pixel_metrics())
, m_text(text) , m_text(text)
, m_rect(rect) , m_rect(rect)
{ {
} }
Vector<DeprecatedString, 32> lines(TextElision elision, TextWrapping wrapping, int line_spacing) const Vector<DeprecatedString, 32> lines(TextElision elision, TextWrapping wrapping) const
{ {
return wrap_lines(elision, wrapping, line_spacing, FitWithinRect::Yes); return wrap_lines(elision, wrapping);
} }
FloatRect bounding_rect(TextWrapping wrapping, int line_spacing) const; FloatRect bounding_rect(TextWrapping) const;
private: private:
Vector<DeprecatedString, 32> wrap_lines(TextElision, TextWrapping, int line_spacing, FitWithinRect) const; Vector<DeprecatedString, 32> wrap_lines(TextElision, TextWrapping) const;
DeprecatedString elide_text_from_right(Utf8View, bool force_elision) const; DeprecatedString elide_text_from_right(Utf8View) const;
Font const& m_font; Font const& m_font;
FontPixelMetrics m_font_metrics;
Utf8View m_text; Utf8View m_text;
FloatRect m_rect; FloatRect m_rect;
}; };