LibGfx+LibWeb: Draw glyph runs with subpixel accuracy

This improves the quality of our font rendering, especially when
animations are involved. Relevant changes:

  * Skia fonts have their subpixel flag set, which means that individual
    glyphs are rendered at subpixel offsets causing glyph runs as a
    whole to look better.

  * Fragment offsets are no longer rounded to whole device pixels, and
    instead the floating point offset is kept. This allows us to pass
    through the floating point baseline position all the way to the Skia
    calls, which already expected that to be a float position.

The `scrollable-contains-table.html` ref test needed different table
headings since they would slightly inflate the column size in the test
file, but not the reference.
This commit is contained in:
Jelle Raaijmakers 2024-12-09 10:24:26 +01:00 committed by Jelle Raaijmakers
commit 4d9f17eddf
Notes: github-actions[bot] 2024-12-21 22:10:48 +00:00
83 changed files with 524 additions and 525 deletions

View file

@ -5,10 +5,8 @@
*/
#include <AK/Utf8View.h>
#include <LibWeb/DOM/Range.h>
#include <LibWeb/Layout/LayoutState.h>
#include <LibWeb/Layout/TextNode.h>
#include <LibWeb/Layout/Viewport.h>
#include <ctype.h>
namespace Web::Layout {

View file

@ -269,7 +269,7 @@ void LineBuilder::update_last_line()
for (size_t i = 0; i < line_box.fragments().size(); ++i) {
auto& fragment = line_box.fragments()[i];
CSSPixels new_fragment_inline_offset = round(inline_offset + fragment.inline_offset());
CSSPixels new_fragment_inline_offset = inline_offset + fragment.inline_offset();
CSSPixels new_fragment_block_offset = 0;
auto block_offset_value_for_alignment = [&](CSS::VerticalAlign vertical_align) {

View file

@ -7,10 +7,7 @@
#pragma once
#include <AK/NonnullRefPtr.h>
#include <AK/TypeCasts.h>
#include <AK/Vector.h>
#include <LibGC/Root.h>
#include <LibGfx/Rect.h>
#include <LibJS/Heap/Cell.h>
#include <LibWeb/CSS/ComputedValues.h>
#include <LibWeb/CSS/StyleComputer.h>

View file

@ -29,9 +29,12 @@ static RefPtr<DisplayList> compute_text_clip_paths(PaintContext& context, Painta
auto fragment_absolute_rect = fragment.absolute_rect();
auto fragment_absolute_device_rect = context.enclosing_device_rect(fragment_absolute_rect);
DevicePixelPoint baseline_start { fragment_absolute_device_rect.x(), fragment_absolute_device_rect.y() + context.rounded_device_pixels(fragment.baseline()) };
auto scale = context.device_pixels_per_css_pixel();
display_list_recorder.draw_text_run(baseline_start.to_type<int>(), *glyph_run, Gfx::Color::Black, fragment_absolute_device_rect.to_type<int>(), scale, fragment.orientation());
auto baseline_start = Gfx::FloatPoint {
fragment_absolute_rect.x().to_float(),
fragment_absolute_rect.y().to_float() + fragment.baseline().to_float(),
} * scale;
display_list_recorder.draw_text_run(baseline_start, *glyph_run, Gfx::Color::Black, fragment_absolute_device_rect.to_type<int>(), scale, fragment.orientation());
};
paintable.for_each_in_inclusive_subtree([&](auto& paintable) {

View file

@ -164,12 +164,12 @@ struct PaintTextShadow {
double glyph_run_scale { 1 };
Gfx::IntRect shadow_bounding_rect;
Gfx::IntRect text_rect;
Gfx::IntPoint draw_location;
Gfx::FloatPoint draw_location;
int blur_radius;
Color color;
[[nodiscard]] Gfx::IntRect bounding_rect() const { return { draw_location, shadow_bounding_rect.size() }; }
void translate_by(Gfx::IntPoint const& offset) { draw_location.translate_by(offset); }
[[nodiscard]] Gfx::IntRect bounding_rect() const { return { draw_location.to_type<int>(), shadow_bounding_rect.size() }; }
void translate_by(Gfx::IntPoint const& offset) { draw_location.translate_by(offset.to_type<float>()); }
};
struct FillRectWithRoundedCorners {

View file

@ -504,7 +504,7 @@ void DisplayListPlayerSkia::paint_text_shadow(PaintTextShadow const& command)
.glyph_run = command.glyph_run,
.scale = command.glyph_run_scale,
.rect = command.text_rect,
.translation = command.draw_location.to_type<float>() + command.text_rect.location().to_type<float>(),
.translation = command.draw_location + command.text_rect.location().to_type<float>(),
.color = command.color,
});
canvas.restore();

View file

@ -244,10 +244,10 @@ void DisplayListRecorder::draw_text(Gfx::IntRect const& rect, String raw_text, G
}
auto metrics = font.pixel_metrics();
float baseline_y = static_cast<float>(rect.y()) + metrics.ascent + (static_cast<float>(rect.height()) - (metrics.ascent + metrics.descent)) / 2.0f;
draw_text_run(Gfx::IntPoint(roundf(baseline_x), roundf(baseline_y)), *glyph_run, color, rect, 1.0, Orientation::Horizontal);
draw_text_run({ baseline_x, baseline_y }, *glyph_run, color, rect, 1.0, Orientation::Horizontal);
}
void DisplayListRecorder::draw_text_run(Gfx::IntPoint baseline_start, Gfx::GlyphRun const& glyph_run, Color color, Gfx::IntRect const& rect, double scale, Orientation orientation)
void DisplayListRecorder::draw_text_run(Gfx::FloatPoint baseline_start, Gfx::GlyphRun const& glyph_run, Color color, Gfx::IntRect const& rect, double scale, Orientation orientation)
{
if (rect.is_empty())
return;
@ -255,7 +255,7 @@ void DisplayListRecorder::draw_text_run(Gfx::IntPoint baseline_start, Gfx::Glyph
.glyph_run = glyph_run,
.scale = scale,
.rect = rect,
.translation = baseline_start.to_type<float>(),
.translation = baseline_start,
.color = color,
.orientation = orientation,
});
@ -331,7 +331,7 @@ void DisplayListRecorder::paint_inner_box_shadow_params(PaintBoxShadowParams par
append(PaintInnerBoxShadow { .box_shadow_params = params });
}
void DisplayListRecorder::paint_text_shadow(int blur_radius, Gfx::IntRect bounding_rect, Gfx::IntRect text_rect, Gfx::GlyphRun const& glyph_run, double glyph_run_scale, Color color, Gfx::IntPoint draw_location)
void DisplayListRecorder::paint_text_shadow(int blur_radius, Gfx::IntRect bounding_rect, Gfx::IntRect text_rect, Gfx::GlyphRun const& glyph_run, double glyph_run_scale, Color color, Gfx::FloatPoint draw_location)
{
append(PaintTextShadow {
.glyph_run = glyph_run,

View file

@ -8,8 +8,6 @@
#include <AK/Forward.h>
#include <AK/NonnullRefPtr.h>
#include <AK/SegmentedVector.h>
#include <AK/Utf8View.h>
#include <AK/Vector.h>
#include <LibGfx/Color.h>
#include <LibGfx/Forward.h>
@ -106,7 +104,7 @@ public:
void draw_text(Gfx::IntRect const&, String, Gfx::Font const&, Gfx::TextAlignment, Color);
// Streamlined text drawing routine that does no wrapping/elision/alignment.
void draw_text_run(Gfx::IntPoint baseline_start, Gfx::GlyphRun const& glyph_run, Color color, Gfx::IntRect const& rect, double scale, Gfx::Orientation);
void draw_text_run(Gfx::FloatPoint baseline_start, Gfx::GlyphRun const& glyph_run, Color color, Gfx::IntRect const& rect, double scale, Gfx::Orientation);
void add_clip_rect(Gfx::IntRect const& rect);
@ -137,7 +135,7 @@ public:
void paint_outer_box_shadow_params(PaintBoxShadowParams params);
void paint_inner_box_shadow_params(PaintBoxShadowParams params);
void paint_text_shadow(int blur_radius, Gfx::IntRect bounding_rect, Gfx::IntRect text_rect, Gfx::GlyphRun const&, double glyph_run_scale, Color color, Gfx::IntPoint draw_location);
void paint_text_shadow(int blur_radius, Gfx::IntRect bounding_rect, Gfx::IntRect text_rect, Gfx::GlyphRun const&, double glyph_run_scale, Color color, Gfx::FloatPoint draw_location);
void fill_rect_with_rounded_corners(Gfx::IntRect const& rect, Color color, CornerRadius top_left_radius, CornerRadius top_right_radius, CornerRadius bottom_right_radius, CornerRadius bottom_left_radius);
void fill_rect_with_rounded_corners(Gfx::IntRect const& a_rect, Color color, int radius);

View file

@ -683,16 +683,19 @@ void paint_text_fragment(PaintContext& context, TextPaintable const& paintable,
if (!glyph_run)
return;
DevicePixelPoint baseline_start { fragment_absolute_device_rect.x(), fragment_absolute_device_rect.y() + context.rounded_device_pixels(fragment.baseline()) };
auto scale = context.device_pixels_per_css_pixel();
painter.draw_text_run(baseline_start.to_type<int>(), *glyph_run, paintable.computed_values().webkit_text_fill_color(), fragment_absolute_device_rect.to_type<int>(), scale, fragment.orientation());
auto baseline_start = Gfx::FloatPoint {
fragment_absolute_rect.x().to_float(),
fragment_absolute_rect.y().to_float() + fragment.baseline().to_float(),
} * scale;
painter.draw_text_run(baseline_start, *glyph_run, paintable.computed_values().webkit_text_fill_color(), fragment_absolute_device_rect.to_type<int>(), scale, fragment.orientation());
auto selection_rect = context.enclosing_device_rect(fragment.selection_rect()).to_type<int>();
if (!selection_rect.is_empty()) {
painter.fill_rect(selection_rect, CSS::SystemColor::highlight());
DisplayListRecorderStateSaver saver(painter);
painter.add_clip_rect(selection_rect);
painter.draw_text_run(baseline_start.to_type<int>(), *glyph_run, CSS::SystemColor::highlight_text(), fragment_absolute_device_rect.to_type<int>(), scale, fragment.orientation());
painter.draw_text_run(baseline_start, *glyph_run, CSS::SystemColor::highlight_text(), fragment_absolute_device_rect.to_type<int>(), scale, fragment.orientation());
}
paint_text_decoration(context, paintable, fragment);
@ -744,9 +747,8 @@ void PaintableWithLines::paint(PaintContext& context, PaintPhase phase) const
// So, we paint the shadows before painting any text.
// FIXME: Find a smarter way to do this?
if (phase == PaintPhase::Foreground) {
for (auto& fragment : fragments()) {
for (auto& fragment : fragments())
paint_text_shadow(context, fragment, fragment.shadows());
}
}
for (auto const& fragment : m_fragments) {

View file

@ -74,13 +74,10 @@ void paint_text_shadow(PaintContext& context, PaintableFragment const& fragment,
auto fragment_width = context.enclosing_device_pixels(fragment.width()).value();
auto fragment_height = context.enclosing_device_pixels(fragment.height()).value();
auto draw_rect = context.enclosing_device_rect(fragment.absolute_rect()).to_type<int>();
auto fragment_baseline = context.rounded_device_pixels(fragment.baseline()).value();
// Note: Box-shadow layers are ordered front-to-back, so we paint them in reverse
for (auto& layer : shadow_layers.in_reverse()) {
int offset_x = context.rounded_device_pixels(layer.offset_x).value();
int offset_y = context.rounded_device_pixels(layer.offset_y).value();
int blur_radius = context.rounded_device_pixels(layer.blur_radius).value();
// Space around the painted text to allow it to blur.
@ -95,12 +92,14 @@ void paint_text_shadow(PaintContext& context, PaintableFragment const& fragment,
text_rect.width() + margin + margin,
text_rect.height() + margin + margin
};
Gfx::IntPoint draw_location {
draw_rect.x() + offset_x - margin,
draw_rect.y() + offset_y - margin
};
context.display_list_recorder().paint_text_shadow(blur_radius, bounding_rect, text_rect.translated(0, fragment_baseline), *glyph_run, context.device_pixels_per_css_pixel(), layer.color, draw_location);
auto scale = context.device_pixels_per_css_pixel();
auto draw_location = Gfx::FloatPoint {
fragment.absolute_rect().x() + layer.offset_x - margin,
fragment.absolute_rect().y() + layer.offset_y - margin,
} * scale;
context.display_list_recorder().paint_text_shadow(blur_radius, bounding_rect, text_rect.translated(0, fragment_baseline), *glyph_run, scale, layer.color, draw_location);
}
}