summaryrefslogtreecommitdiff
path: root/Userland/Libraries/LibWeb/Painting/BorderPainting.cpp
blob: d4a7ff1ffa02e580e3bc2c70a89048a1df04aca6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
/*
 * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
 * Copyright (c) 2021, Sam Atkins <atkinssj@serenityos.org>
 * Copyright (c) 2022, MacDue <macdue@dueutil.tech>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <LibGfx/AntiAliasingPainter.h>
#include <LibGfx/Painter.h>
#include <LibGfx/Path.h>
#include <LibWeb/Painting/BorderPainting.h>
#include <LibWeb/Painting/PaintContext.h>

namespace Web::Painting {

BorderRadiiData normalized_border_radii_data(Layout::Node const& node, Gfx::FloatRect const& rect, CSS::BorderRadiusData top_left_radius, CSS::BorderRadiusData top_right_radius, CSS::BorderRadiusData bottom_right_radius, CSS::BorderRadiusData bottom_left_radius)
{
    BorderRadiusData bottom_left_radius_px {};
    BorderRadiusData bottom_right_radius_px {};
    BorderRadiusData top_left_radius_px {};
    BorderRadiusData top_right_radius_px {};

    auto width_length = CSS::Length::make_px(rect.width());
    bottom_left_radius_px.horizontal_radius = bottom_left_radius.horizontal_radius.resolved(node, width_length).to_px(node);
    bottom_right_radius_px.horizontal_radius = bottom_right_radius.horizontal_radius.resolved(node, width_length).to_px(node);
    top_left_radius_px.horizontal_radius = top_left_radius.horizontal_radius.resolved(node, width_length).to_px(node);
    top_right_radius_px.horizontal_radius = top_right_radius.horizontal_radius.resolved(node, width_length).to_px(node);

    auto height_length = CSS::Length::make_px(rect.height());
    bottom_left_radius_px.vertical_radius = bottom_left_radius.vertical_radius.resolved(node, height_length).to_px(node);
    bottom_right_radius_px.vertical_radius = bottom_right_radius.vertical_radius.resolved(node, height_length).to_px(node);
    top_left_radius_px.vertical_radius = top_left_radius.vertical_radius.resolved(node, height_length).to_px(node);
    top_right_radius_px.vertical_radius = top_right_radius.vertical_radius.resolved(node, height_length).to_px(node);

    // Scale overlapping curves according to https://www.w3.org/TR/css-backgrounds-3/#corner-overlap
    auto f = 1.0f;
    auto width_reciprocal = 1.0f / rect.width();
    auto height_reciprocal = 1.0f / rect.height();
    f = max(f, width_reciprocal * (top_left_radius_px.horizontal_radius + top_right_radius_px.horizontal_radius));
    f = max(f, height_reciprocal * (top_right_radius_px.vertical_radius + bottom_right_radius_px.vertical_radius));
    f = max(f, width_reciprocal * (bottom_left_radius_px.horizontal_radius + bottom_right_radius_px.horizontal_radius));
    f = max(f, height_reciprocal * (top_left_radius_px.vertical_radius + bottom_left_radius_px.vertical_radius));

    f = 1.0f / f;

    top_left_radius_px.horizontal_radius *= f;
    top_left_radius_px.vertical_radius *= f;
    top_right_radius_px.horizontal_radius *= f;
    top_right_radius_px.vertical_radius *= f;
    bottom_right_radius_px.horizontal_radius *= f;
    bottom_right_radius_px.vertical_radius *= f;
    bottom_left_radius_px.horizontal_radius *= f;
    bottom_left_radius_px.vertical_radius *= f;

    return BorderRadiiData { top_left_radius_px, top_right_radius_px, bottom_right_radius_px, bottom_left_radius_px };
}

void paint_border(PaintContext& context, BorderEdge edge, Gfx::IntRect const& rect, BorderRadiiData const& border_radii_data, BordersData const& borders_data)
{
    auto const& border_data = [&] {
        switch (edge) {
        case BorderEdge::Top:
            return borders_data.top;
        case BorderEdge::Right:
            return borders_data.right;
        case BorderEdge::Bottom:
            return borders_data.bottom;
        default: // BorderEdge::Left:
            return borders_data.left;
        }
    }();

    float width = border_data.width;
    if (width <= 0)
        return;

    auto color = border_data.color;
    auto border_style = border_data.line_style;
    int int_width = ceil(width);

    struct Points {
        Gfx::IntPoint p1;
        Gfx::IntPoint p2;
    };

    auto points_for_edge = [](BorderEdge edge, Gfx::IntRect const& rect) -> Points {
        switch (edge) {
        case BorderEdge::Top:
            return { rect.top_left(), rect.top_right() };
        case BorderEdge::Right:
            return { rect.top_right(), rect.bottom_right() };
        case BorderEdge::Bottom:
            return { rect.bottom_left(), rect.bottom_right() };
        default: // Edge::Left
            return { rect.top_left(), rect.bottom_left() };
        }
    };

    if (border_style == CSS::LineStyle::Inset) {
        auto top_left_color = Color::from_rgb(0x5a5a5a);
        auto bottom_right_color = Color::from_rgb(0x888888);
        color = (edge == BorderEdge::Left || edge == BorderEdge::Top) ? top_left_color : bottom_right_color;
    } else if (border_style == CSS::LineStyle::Outset) {
        auto top_left_color = Color::from_rgb(0x888888);
        auto bottom_right_color = Color::from_rgb(0x5a5a5a);
        color = (edge == BorderEdge::Left || edge == BorderEdge::Top) ? top_left_color : bottom_right_color;
    }

    auto gfx_line_style = Gfx::Painter::LineStyle::Solid;
    if (border_style == CSS::LineStyle::Dotted)
        gfx_line_style = Gfx::Painter::LineStyle::Dotted;
    if (border_style == CSS::LineStyle::Dashed)
        gfx_line_style = Gfx::Painter::LineStyle::Dashed;

    if (gfx_line_style != Gfx::Painter::LineStyle::Solid) {
        auto [p1, p2] = points_for_edge(edge, rect);
        switch (edge) {
        case BorderEdge::Top:
            p1.translate_by(int_width / 2, int_width / 2);
            p2.translate_by(-int_width / 2, int_width / 2);
            break;
        case BorderEdge::Right:
            p1.translate_by(-int_width / 2, int_width / 2);
            p2.translate_by(-int_width / 2, -int_width / 2);
            break;
        case BorderEdge::Bottom:
            p1.translate_by(int_width / 2, -int_width / 2);
            p2.translate_by(-int_width / 2, -int_width / 2);
            break;
        case BorderEdge::Left:
            p1.translate_by(int_width / 2, int_width / 2);
            p2.translate_by(int_width / 2, -int_width / 2);
            break;
        }
        if (border_style == CSS::LineStyle::Dotted) {
            Gfx::AntiAliasingPainter aa_painter { context.painter() };
            aa_painter.draw_line(p1.to_type<float>(), p2.to_type<float>(), color, int_width, gfx_line_style);
            return;
        }
        context.painter().draw_line(p1, p2, color, int_width, gfx_line_style);
        return;
    }

    auto draw_horizontal_or_vertical_line = [&](auto p1, auto p2) {
        // Note: Using fill_rect() here since draw_line() produces some overlapping pixels
        // at the end of a line, which cause issues on borders with transparency.
        p2.translate_by(1, 1);
        context.painter().fill_rect(Gfx::IntRect::from_two_points(p1, p2), color);
    };

    auto draw_border = [&](auto const& border, auto const& radius, auto const& opposite_border, auto const& opposite_radius, auto p1_step_translate, auto p2_step_translate) {
        auto [p1, p2] = points_for_edge(edge, rect);
        auto current_p1 = p1.to_type<float>();
        auto current_p2 = p2.to_type<float>();
        auto p1_step = radius ? 0 : border.width / static_cast<float>(int_width);
        auto p2_step = opposite_radius ? 0 : opposite_border.width / static_cast<float>(int_width);
        for (int i = 0; i < int_width; ++i) {
            draw_horizontal_or_vertical_line(current_p1.to_type<int>(), current_p2.to_type<int>());
            p1_step_translate(current_p1, p1_step);
            p2_step_translate(current_p2, p2_step);
        }
    };

    // FIXME: There is some overlap where two borders (without border radii meet),
    // which produces artifacts if the border color has some transparency.
    // (this only happens if the angle between the two borders is not 45 degrees)
    switch (edge) {
    case BorderEdge::Top:
        draw_border(
            borders_data.left, border_radii_data.top_left, borders_data.right, border_radii_data.top_right,
            [](auto& current_p1, auto step) {
                current_p1.translate_by(step, 1);
            },
            [](auto& current_p2, auto step) {
                current_p2.translate_by(-step, 1);
            });
        break;
    case BorderEdge::Right:
        draw_border(
            borders_data.top, border_radii_data.top_right, borders_data.bottom, border_radii_data.bottom_right,
            [](auto& current_p1, auto step) {
                current_p1.translate_by(-1, step);
            },
            [](auto& current_p2, auto step) {
                current_p2.translate_by(-1, -step);
            });
        break;
    case BorderEdge::Bottom:
        draw_border(
            borders_data.left, border_radii_data.bottom_left, borders_data.right, border_radii_data.bottom_right,
            [](auto& current_p1, auto step) {
                current_p1.translate_by(step, -1);
            },
            [](auto& current_p2, auto step) {
                current_p2.translate_by(-step, -1);
            });
        break;
    case BorderEdge::Left:
        draw_border(
            borders_data.top, border_radii_data.top_left, borders_data.bottom, border_radii_data.bottom_left,
            [](auto& current_p1, auto step) {
                current_p1.translate_by(1, step);
            },
            [](auto& current_p2, auto step) {
                current_p2.translate_by(1, -step);
            });
        break;
    }
}

RefPtr<Gfx::Bitmap> get_cached_corner_bitmap(Gfx::IntSize const& corners_size)
{
    auto allocate_mask_bitmap = [&]() -> RefPtr<Gfx::Bitmap> {
        auto bitmap = Gfx::Bitmap::try_create(Gfx::BitmapFormat::BGRA8888, corners_size);
        if (!bitmap.is_error())
            return bitmap.release_value();
        return nullptr;
    };
    // FIXME: Allocate per page?
    static thread_local auto corner_bitmap = allocate_mask_bitmap();
    // Only reallocate the corner bitmap is the existing one is too small.
    // (should mean no more allocations after the first paint -- amortised zero allocations :^))
    if (corner_bitmap && corner_bitmap->rect().size().contains(corners_size)) {
        Gfx::Painter painter { *corner_bitmap };
        painter.clear_rect({ { 0, 0 }, corners_size }, Gfx::Color());
    } else {
        corner_bitmap = allocate_mask_bitmap();
        if (!corner_bitmap) {
            dbgln("Failed to allocate corner bitmap with size {}", corners_size);
            return nullptr;
        }
    }
    return corner_bitmap;
}

void paint_all_borders(PaintContext& context, Gfx::FloatRect const& bordered_rect, BorderRadiiData const& border_radii_data, BordersData const& borders_data)
{
    if (borders_data.top.width <= 0 && borders_data.right.width <= 0 && borders_data.left.width <= 0 && borders_data.bottom.width <= 0)
        return;

    Gfx::IntRect border_rect = bordered_rect.to_rounded<int>();

    auto top_left = border_radii_data.top_left.as_corner();
    auto top_right = border_radii_data.top_right.as_corner();
    auto bottom_right = border_radii_data.bottom_right.as_corner();
    auto bottom_left = border_radii_data.bottom_left.as_corner();

    // Disable border radii if the corresponding borders don't exist:
    if (borders_data.bottom.width <= 0 && borders_data.left.width <= 0)
        bottom_left = { 0, 0 };
    if (borders_data.bottom.width <= 0 && borders_data.right.width <= 0)
        bottom_right = { 0, 0 };
    if (borders_data.top.width <= 0 && borders_data.left.width <= 0)
        top_left = { 0, 0 };
    if (borders_data.top.width <= 0 && borders_data.right.width <= 0)
        top_right = { 0, 0 };

    auto int_width = [&](auto value) -> int {
        return ceil(value);
    };

    Gfx::IntRect top_border_rect = {
        border_rect.x() + top_left.horizontal_radius,
        border_rect.y(),
        border_rect.width() - top_left.horizontal_radius - top_right.horizontal_radius,
        int_width(borders_data.top.width)
    };
    Gfx::IntRect right_border_rect = {
        border_rect.x() + (border_rect.width() - int_width(borders_data.right.width)),
        border_rect.y() + top_right.vertical_radius,
        int_width(borders_data.right.width),
        border_rect.height() - top_right.vertical_radius - bottom_right.vertical_radius
    };
    Gfx::IntRect bottom_border_rect = {
        border_rect.x() + bottom_left.horizontal_radius,
        border_rect.y() + (border_rect.height() - int_width(borders_data.bottom.width)),
        border_rect.width() - bottom_left.horizontal_radius - bottom_right.horizontal_radius,
        int_width(borders_data.bottom.width)
    };
    Gfx::IntRect left_border_rect = {
        border_rect.x(),
        border_rect.y() + top_left.vertical_radius,
        int_width(borders_data.left.width),
        border_rect.height() - top_left.vertical_radius - bottom_left.vertical_radius
    };

    // Avoid overlapping pixels on the edges, in the easy 45 degree corners case:
    if (!top_left && top_border_rect.height() == left_border_rect.width())
        top_border_rect.shrink(0, 0, 0, 1);
    if (!top_right && top_border_rect.height() == right_border_rect.width())
        top_border_rect.shrink(0, 1, 0, 0);
    if (!bottom_left && bottom_border_rect.height() == left_border_rect.width())
        bottom_border_rect.shrink(0, 0, 0, 1);
    if (!bottom_right && bottom_border_rect.height() == right_border_rect.width())
        bottom_border_rect.shrink(0, 1, 0, 0);

    auto border_color_no_alpha = borders_data.top.color;
    border_color_no_alpha.set_alpha(255);

    // Paint the strait line part of the border:
    Painting::paint_border(context, Painting::BorderEdge::Top, top_border_rect, border_radii_data, borders_data);
    Painting::paint_border(context, Painting::BorderEdge::Right, right_border_rect, border_radii_data, borders_data);
    Painting::paint_border(context, Painting::BorderEdge::Bottom, bottom_border_rect, border_radii_data, borders_data);
    Painting::paint_border(context, Painting::BorderEdge::Left, left_border_rect, border_radii_data, borders_data);

    if (!top_left && !top_right && !bottom_left && !bottom_right)
        return;

    // Cache the smallest possible bitmap to render just the corners for the border.
    auto expand_width = abs(int_width(borders_data.left.width) - int_width(borders_data.right.width));
    auto expand_height = abs(int_width(borders_data.top.width) - int_width(borders_data.bottom.width));
    Gfx::IntRect corner_mask_rect {
        0, 0,
        max(
            top_left.horizontal_radius + top_right.horizontal_radius + expand_width,
            bottom_left.horizontal_radius + bottom_right.horizontal_radius + expand_height),
        max(
            top_left.vertical_radius + bottom_left.vertical_radius + expand_width,
            top_right.vertical_radius + bottom_right.vertical_radius + expand_height)
    };

    auto corner_bitmap = get_cached_corner_bitmap(corner_mask_rect.size());
    if (!corner_bitmap)
        return;
    Gfx::Painter painter { *corner_bitmap };

    Gfx::AntiAliasingPainter aa_painter { painter };

    // Paint a little tile sheet for the corners
    // TODO: Support various line styles on the corners (dotted, dashes, etc)

    // Paint the outer (minimal) corner rounded rectangle:
    aa_painter.fill_rect_with_rounded_corners(corner_mask_rect, border_color_no_alpha, top_left, top_right, bottom_right, bottom_left);

    // Subtract the inner corner rectangle:
    auto inner_corner_mask_rect = corner_mask_rect.shrunken(
        int_width(borders_data.top.width),
        int_width(borders_data.right.width),
        int_width(borders_data.bottom.width),
        int_width(borders_data.left.width));
    auto inner_top_left = top_left;
    auto inner_top_right = top_right;
    auto inner_bottom_right = bottom_right;
    auto inner_bottom_left = bottom_left;
    inner_top_left.horizontal_radius = max(0, inner_top_left.horizontal_radius - int_width(borders_data.left.width));
    inner_top_left.vertical_radius = max(0, inner_top_left.vertical_radius - int_width(borders_data.top.width));
    inner_top_right.horizontal_radius = max(0, inner_top_right.horizontal_radius - int_width(borders_data.right.width));
    inner_top_right.vertical_radius = max(0, inner_top_right.vertical_radius - int_width(borders_data.top.width));
    inner_bottom_right.horizontal_radius = max(0, inner_bottom_right.horizontal_radius - int_width(borders_data.right.width));
    inner_bottom_right.vertical_radius = max(0, inner_bottom_right.vertical_radius - int_width(borders_data.bottom.width));
    inner_bottom_left.horizontal_radius = max(0, inner_bottom_left.horizontal_radius - int_width(borders_data.left.width));
    inner_bottom_left.vertical_radius = max(0, inner_bottom_left.vertical_radius - int_width(borders_data.bottom.width));
    aa_painter.fill_rect_with_rounded_corners(inner_corner_mask_rect, border_color_no_alpha, inner_top_left, inner_top_right, inner_bottom_right, inner_bottom_left, Gfx::AntiAliasingPainter::BlendMode::AlphaSubtract);

    // TODO: Support dual color corners. Other browsers will render a rounded corner between two borders of
    // different colors using both colours, normally split at a 45 degree angle (though the exact angle is interpolated).
    auto blit_corner = [&](Gfx::IntPoint const& position, Gfx::IntRect const& src_rect, Color corner_color) {
        context.painter().blit_filtered(position, *corner_bitmap, src_rect, [&](auto const& corner_pixel) {
            return corner_color.with_alpha((corner_color.alpha() * corner_pixel.alpha()) / 255);
        });
    };

    // FIXME: Corners should actually split between the two colors, if both are provided (and differ)
    auto pick_corner_color = [](auto const& border, auto const& adjacent_border) {
        if (border.width > 0)
            return border.color;
        return adjacent_border.color;
    };

    // Blit the corners into to their corresponding locations:
    if (top_left)
        blit_corner(border_rect.top_left(), top_left.as_rect(), pick_corner_color(borders_data.top, borders_data.left));

    if (top_right)
        blit_corner(border_rect.top_right().translated(-top_right.horizontal_radius + 1, 0), top_right.as_rect().translated(corner_mask_rect.width() - top_right.horizontal_radius, 0), pick_corner_color(borders_data.top, borders_data.right));

    if (bottom_right)
        blit_corner(border_rect.bottom_right().translated(-bottom_right.horizontal_radius + 1, -bottom_right.vertical_radius + 1), bottom_right.as_rect().translated(corner_mask_rect.width() - bottom_right.horizontal_radius, corner_mask_rect.height() - bottom_right.vertical_radius), pick_corner_color(borders_data.bottom, borders_data.right));

    if (bottom_left)
        blit_corner(border_rect.bottom_left().translated(0, -bottom_left.vertical_radius + 1), bottom_left.as_rect().translated(0, corner_mask_rect.height() - bottom_left.vertical_radius), pick_corner_color(borders_data.bottom, borders_data.left));
}

}