Newer
Older
minerva / Userland / Libraries / LibWeb / Painting / AffineDisplayListPlayerCPU.cpp
@minerva minerva on 13 Jul 15 KB Initial commit
/*
 * Copyright (c) 2024, MacDue <macdue@dueutil.tech>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/TypeCasts.h>
#include <LibGfx/Font/ScaledFont.h>
#include <LibWeb/Painting/AffineDisplayListPlayerCPU.h>
namespace Web::Painting {

// This executor is hopes to handle (at least) 2D CSS transforms. All commands
// implemented here are required to support affine transformations, if that is
// not possible the implementation should say in CommandExecutorCPU. Note: The
// transform can be assumed to be non-identity or translation, so there's no
// need to add fast paths here (those will be handled in the normal executor).

bool AffineDisplayListPlayerCPU::needs_expensive_clipping(Gfx::IntRect bounding_rect) const
{
    auto& current_stacking_context = stacking_context();
    if (current_stacking_context.clip.is_rectangular)
        return false;
    auto dest = current_stacking_context.transform.map_to_quad(bounding_rect.to_type<float>());
    for (auto point : { dest.p1(), dest.p2(), dest.p3(), dest.p4() }) {
        if (!current_stacking_context.clip.quad.contains(point))
            return true;
    }
    return false;
}

void AffineDisplayListPlayerCPU::prepare_clipping(Gfx::IntRect bounding_rect)
{
    if (m_expensive_clipping_target)
        return;
    if (!needs_expensive_clipping(bounding_rect))
        return;
    auto& current_stacking_context = stacking_context();
    auto clip_bounds = current_stacking_context.clip.bounds;
    if (clip_bounds.is_empty())
        return;
    m_expensive_clipping_target = Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, clip_bounds.size()).release_value_but_fixme_should_propagate_errors();
    m_expensive_clipping_mask = Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, clip_bounds.size()).release_value_but_fixme_should_propagate_errors();

    // Prepare clip mask:
    set_target(clip_bounds.top_left(), *m_expensive_clipping_mask);
    Gfx::Path clip_path;
    clip_path.quad(current_stacking_context.clip.quad);
    aa_painter().fill_path(clip_path, Gfx::Color::Black, Gfx::WindingRule::EvenOdd);

    // Prepare painter:
    set_target(clip_bounds.top_left(), *m_expensive_clipping_target);
}

void AffineDisplayListPlayerCPU::flush_clipping(Optional<StackingContext const&> current_stacking_context)
{
    if (!m_expensive_clipping_target)
        return;
    if (!current_stacking_context.has_value())
        current_stacking_context = stacking_context();
    set_target(current_stacking_context->origin, *current_stacking_context->target);
    m_expensive_clipping_target->apply_mask(*m_expensive_clipping_mask, Gfx::Bitmap::MaskKind::Alpha);
    painter().blit(current_stacking_context->clip.bounds.top_left(), *m_expensive_clipping_target, m_expensive_clipping_target->rect());
    painter().add_clip_rect(current_stacking_context->clip.bounds);
    m_expensive_clipping_target = nullptr;
    m_expensive_clipping_mask = nullptr;
}

AffineDisplayListPlayerCPU::AffineDisplayListPlayerCPU(Gfx::Bitmap& bitmap, Gfx::AffineTransform transform, Gfx::IntRect clip)
    : m_painter(bitmap)
{
    painter().add_clip_rect(clip);
    m_stacking_contexts.append(StackingContext {
        .transform = transform,
        .clip = Clip {
            .quad = Gfx::AffineTransform {}.map_to_quad(clip.to_type<float>()),
            .bounds = clip,
            .is_rectangular = true },
        .target = bitmap });
}

CommandResult AffineDisplayListPlayerCPU::draw_glyph_run(DrawGlyphRun const& command)
{
    prepare_clipping(command.bounding_rect());
    auto scale = Gfx::AffineTransform {}.scale(command.scale, command.scale);
    auto const& glyphs = command.glyph_run->glyphs();
    Gfx::Path path;
    auto const& font = command.glyph_run->font();
    for (auto const& glyph_or_emoji : glyphs) {
        if (auto* glyph = glyph_or_emoji.get_pointer<Gfx::DrawGlyph>()) {
            if (!is<Gfx::ScaledFont>(font))
                return CommandResult::Continue;
            auto& scaled_font = static_cast<Gfx::ScaledFont const&>(font);
            auto position = glyph->position.translated(scaled_font.glyph_left_bearing(glyph->code_point), 0);
            auto glyph_id = scaled_font.glyph_id_for_code_point(glyph->code_point);
            Gfx::Path glyph_path;
            scaled_font.append_glyph_path_to(glyph_path, glyph_id);
            glyph_path.transform(Gfx::AffineTransform(scale).translate(position));
            path.append_path(glyph_path);
        } else {
            // TODO: Implement bitmap emojis via transformed bitmaps.
        }
    }
    auto path_transform = Gfx::AffineTransform(stacking_context().transform).multiply(Gfx::AffineTransform {}.set_translation(command.translation));
    path.transform(path_transform);
    aa_painter().fill_path(path, command.color);
    return CommandResult::Continue;
}

CommandResult AffineDisplayListPlayerCPU::fill_rect(FillRect const& command)
{
    prepare_clipping(command.bounding_rect());
    // FIXME: Support clip_paths.
    Gfx::Path path;
    path.rect(command.rect.to_type<float>());
    aa_painter().fill_path(path.copy_transformed(stacking_context().transform), command.color, Gfx::WindingRule::EvenOdd);
    return CommandResult::Continue;
}

CommandResult AffineDisplayListPlayerCPU::draw_scaled_bitmap(DrawScaledBitmap const& command)
{
    prepare_clipping(command.bounding_rect());
    painter().draw_scaled_bitmap_with_transform(command.dst_rect, command.bitmap, command.src_rect.to_type<float>(), stacking_context().transform, 1.0f, command.scaling_mode);
    return CommandResult::Continue;
}

CommandResult AffineDisplayListPlayerCPU::draw_scaled_immutable_bitmap(DrawScaledImmutableBitmap const& command)
{
    prepare_clipping(command.bounding_rect());
    painter().draw_scaled_bitmap_with_transform(command.dst_rect, command.bitmap->bitmap(), command.src_rect.to_type<float>(), stacking_context().transform, 1.0f, command.scaling_mode);
    return CommandResult::Continue;
}

CommandResult AffineDisplayListPlayerCPU::set_clip_rect(SetClipRect const& clip)
{
    flush_clipping();
    auto& current_stacking_context = stacking_context();
    painter().clear_clip_rect();
    auto clip_quad = current_stacking_context.transform.map_to_quad(clip.rect.to_type<float>());
    current_stacking_context.clip.bounds = enclosing_int_rect(clip_quad.bounding_rect());
    // FIXME: Flips and rotations by x*90° should also be marked as rectangular.
    current_stacking_context.clip.is_rectangular = current_stacking_context.transform.is_identity_or_translation_or_scale(Gfx::AffineTransform::AllowNegativeScaling::Yes);
    current_stacking_context.clip.quad = clip_quad;
    painter().add_clip_rect(current_stacking_context.clip.bounds);
    return CommandResult::Continue;
}

CommandResult AffineDisplayListPlayerCPU::clear_clip_rect(ClearClipRect const&)
{
    flush_clipping();
    auto& current_stacking_context = stacking_context();
    painter().clear_clip_rect();
    current_stacking_context.clip.bounds = current_stacking_context.rect();
    current_stacking_context.clip.quad = Gfx::AffineTransform {}.map_to_quad(current_stacking_context.clip.bounds.to_type<float>());
    current_stacking_context.clip.is_rectangular = true;
    return CommandResult::Continue;
}

CommandResult AffineDisplayListPlayerCPU::push_stacking_context(PushStackingContext const& command)
{
    // FIXME: Support masks (not possible to do while PushStackingContext takes a bitmap mask).
    // Note: Image rendering is not relevant as this does not transform via a bitmap.
    // Note: `position: fixed` does not apply when CSS transforms are involved.
    if (command.opacity == 0.0f)
        return CommandResult::SkipStackingContext;

    // FIXME: Attempt to support 3D transforms... Somehow?
    auto affine_transform = Gfx::extract_2d_affine_transform(command.transform.matrix);
    auto new_transform = Gfx::AffineTransform {}
                             .set_translation(command.post_transform_translation.to_type<float>())
                             .translate(command.transform.origin)
                             .multiply(affine_transform)
                             .translate(-command.transform.origin);

    auto const& current_stacking_context = stacking_context();

    StackingContext new_stacking_context {
        .transform = Gfx::AffineTransform(current_stacking_context.transform).multiply(new_transform),
        .clip = current_stacking_context.clip,
        .target = current_stacking_context.target,
        .origin = current_stacking_context.origin,
        .opacity = command.opacity
    };

    if (command.opacity < 1.0f) {
        flush_clipping();
        auto paint_rect = enclosing_int_rect(new_stacking_context.transform.map(command.source_paintable_rect.to_type<float>()))
                              .intersected(current_stacking_context.rect());
        if (paint_rect.is_empty())
            return CommandResult::SkipStackingContext;
        auto new_target = Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, paint_rect.size()).release_value_but_fixme_should_propagate_errors();
        new_stacking_context.target = new_target;
        new_stacking_context.origin = paint_rect.top_left();
        set_target(new_stacking_context.origin, *new_target);
    }

    m_stacking_contexts.append(new_stacking_context);
    return CommandResult::Continue;
}

CommandResult AffineDisplayListPlayerCPU::pop_stacking_context(PopStackingContext const&)
{
    auto active_stacking_contexts = m_stacking_contexts.size() - 1;
    bool is_final_stacking_context = active_stacking_contexts <= 1;
    auto prev_stacking_context = m_stacking_contexts.take_last();
    auto& current_stacking_context = stacking_context();
    bool need_to_flush_clipping = is_final_stacking_context
        || prev_stacking_context.clip != current_stacking_context.clip
        || prev_stacking_context.opacity < 1.0f;
    if (need_to_flush_clipping) {
        flush_clipping(prev_stacking_context);
        painter().clear_clip_rect();
        painter().add_clip_rect(stacking_context().clip.bounds);
    }
    if (prev_stacking_context.opacity < 1.0f) {
        set_target(current_stacking_context.origin, *current_stacking_context.target);
        prepare_clipping(prev_stacking_context.rect());
        painter().blit(prev_stacking_context.origin, *prev_stacking_context.target, prev_stacking_context.target->rect(), prev_stacking_context.opacity);
    }
    return is_final_stacking_context ? CommandResult::ContinueWithParentExecutor : CommandResult::Continue;
}

CommandResult AffineDisplayListPlayerCPU::paint_linear_gradient(PaintLinearGradient const&)
{
    // FIXME: Implement.
    return CommandResult::Continue;
}

CommandResult AffineDisplayListPlayerCPU::paint_outer_box_shadow(PaintOuterBoxShadow const&)
{
    // FIXME: Implement.
    return CommandResult::Continue;
}

CommandResult AffineDisplayListPlayerCPU::paint_inner_box_shadow(PaintInnerBoxShadow const&)
{
    // FIXME: Implement.
    return CommandResult::Continue;
}

CommandResult AffineDisplayListPlayerCPU::paint_text_shadow(PaintTextShadow const&)
{
    // FIXME: Implement.
    return CommandResult::Continue;
}

CommandResult AffineDisplayListPlayerCPU::fill_rect_with_rounded_corners(FillRectWithRoundedCorners const& command)
{
    prepare_clipping(command.bounding_rect());
    Gfx::Path path;
    path.rounded_rect(command.rect.to_type<float>(), command.top_left_radius, command.top_right_radius, command.bottom_right_radius, command.bottom_left_radius);
    path = path.copy_transformed(stacking_context().transform);
    aa_painter().fill_path(path, command.color, Gfx::WindingRule::EvenOdd);
    return CommandResult::Continue;
}

CommandResult AffineDisplayListPlayerCPU::fill_path_using_color(FillPathUsingColor const& command)
{
    prepare_clipping(command.bounding_rect());
    auto path_transform = Gfx::AffineTransform(stacking_context().transform).multiply(Gfx::AffineTransform {}.set_translation(command.aa_translation));
    aa_painter().fill_path(command.path.copy_transformed(path_transform), command.color, command.winding_rule);
    return CommandResult::Continue;
}

CommandResult AffineDisplayListPlayerCPU::fill_path_using_paint_style(FillPathUsingPaintStyle const&)
{
    // FIXME: Implement.
    return CommandResult::Continue;
}

CommandResult AffineDisplayListPlayerCPU::stroke_path_using_color(StrokePathUsingColor const& command)
{
    prepare_clipping(command.bounding_rect());
    auto path_transform = Gfx::AffineTransform(stacking_context().transform).multiply(Gfx::AffineTransform {}.set_translation(command.aa_translation));
    // FIXME: Pass command.cap_style, command.join_style, command.miter_limit here!
    aa_painter().stroke_path(command.path.copy_transformed(path_transform), command.color, { command.thickness });
    return CommandResult::Continue;
}

CommandResult AffineDisplayListPlayerCPU::stroke_path_using_paint_style(StrokePathUsingPaintStyle const&)
{
    // FIXME: Implement.
    return CommandResult::Continue;
}

CommandResult AffineDisplayListPlayerCPU::draw_ellipse(DrawEllipse const&)
{
    // FIXME: Implement.
    return CommandResult::Continue;
}

CommandResult AffineDisplayListPlayerCPU::fill_ellipse(FillEllipse const&)
{
    // FIXME: Implement.
    return CommandResult::Continue;
}

CommandResult AffineDisplayListPlayerCPU::draw_line(DrawLine const& command)
{
    prepare_clipping(Gfx::IntRect::from_two_points(command.from, command.to).inflated(command.thickness, command.thickness));
    // FIXME: Implement other line styles.
    Gfx::Path path;
    path.move_to(command.from.to_type<float>());
    path.line_to(command.to.to_type<float>());
    // FIXME: Probably want to use butt linecaps here?
    aa_painter().stroke_path(path, command.color, { static_cast<float>(command.thickness) });
    return CommandResult::Continue;
}

CommandResult AffineDisplayListPlayerCPU::apply_backdrop_filter(ApplyBackdropFilter const&)
{
    // FIXME: Implement.
    return CommandResult::Continue;
}

CommandResult AffineDisplayListPlayerCPU::draw_rect(DrawRect const& command)
{
    prepare_clipping(command.bounding_rect());
    Gfx::Path path;
    path.rect(command.rect.to_type<float>());
    // FIXME: Probably want to use miter linejoins here?
    aa_painter().stroke_path(path.copy_transformed(stacking_context().transform), command.color, { 1 });
    return CommandResult::Continue;
}

CommandResult AffineDisplayListPlayerCPU::paint_radial_gradient(PaintRadialGradient const&)
{
    // FIXME: Implement.
    return CommandResult::Continue;
}

CommandResult AffineDisplayListPlayerCPU::paint_conic_gradient(PaintConicGradient const&)
{
    // FIXME: Implement.
    return CommandResult::Continue;
}

CommandResult AffineDisplayListPlayerCPU::draw_triangle_wave(DrawTriangleWave const&)
{
    // FIXME: Implement.
    return CommandResult::Continue;
}

CommandResult AffineDisplayListPlayerCPU::sample_under_corners(SampleUnderCorners const&)
{
    // FIXME: Implement? -- Likely not a good approach for transforms.
    return CommandResult::Continue;
}

CommandResult AffineDisplayListPlayerCPU::blit_corner_clipping(BlitCornerClipping const&)
{
    // FIXME: Implement? -- Likely not a good approach for transforms.
    return CommandResult::Continue;
}

bool AffineDisplayListPlayerCPU::would_be_fully_clipped_by_painter(Gfx::IntRect rect) const
{
    auto const& current_stacking_context = stacking_context();
    auto transformed_rect = current_stacking_context.transform.map(rect.to_type<float>()).to_type<int>();
    return transformed_rect.intersected(current_stacking_context.clip.bounds).is_empty();
}

}