BorderPainting.cpp 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. /*
  2. * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
  3. * Copyright (c) 2021-2022, Sam Atkins <atkinssj@serenityos.org>
  4. * Copyright (c) 2022, MacDue <macdue@dueutil.tech>
  5. *
  6. * SPDX-License-Identifier: BSD-2-Clause
  7. */
  8. #include <LibGfx/AntiAliasingPainter.h>
  9. #include <LibGfx/Painter.h>
  10. #include <LibGfx/Path.h>
  11. #include <LibWeb/Painting/BorderPainting.h>
  12. #include <LibWeb/Painting/PaintContext.h>
  13. namespace Web::Painting {
  14. BorderRadiiData normalized_border_radii_data(Layout::Node const& node, CSSPixelRect const& rect, CSS::BorderRadiusData top_left_radius, CSS::BorderRadiusData top_right_radius, CSS::BorderRadiusData bottom_right_radius, CSS::BorderRadiusData bottom_left_radius)
  15. {
  16. BorderRadiusData bottom_left_radius_px {};
  17. BorderRadiusData bottom_right_radius_px {};
  18. BorderRadiusData top_left_radius_px {};
  19. BorderRadiusData top_right_radius_px {};
  20. auto width_length = CSS::Length::make_px(rect.width());
  21. bottom_left_radius_px.horizontal_radius = bottom_left_radius.horizontal_radius.resolved(node, width_length).to_px(node);
  22. bottom_right_radius_px.horizontal_radius = bottom_right_radius.horizontal_radius.resolved(node, width_length).to_px(node);
  23. top_left_radius_px.horizontal_radius = top_left_radius.horizontal_radius.resolved(node, width_length).to_px(node);
  24. top_right_radius_px.horizontal_radius = top_right_radius.horizontal_radius.resolved(node, width_length).to_px(node);
  25. auto height_length = CSS::Length::make_px(rect.height());
  26. bottom_left_radius_px.vertical_radius = bottom_left_radius.vertical_radius.resolved(node, height_length).to_px(node);
  27. bottom_right_radius_px.vertical_radius = bottom_right_radius.vertical_radius.resolved(node, height_length).to_px(node);
  28. top_left_radius_px.vertical_radius = top_left_radius.vertical_radius.resolved(node, height_length).to_px(node);
  29. top_right_radius_px.vertical_radius = top_right_radius.vertical_radius.resolved(node, height_length).to_px(node);
  30. // Scale overlapping curves according to https://www.w3.org/TR/css-backgrounds-3/#corner-overlap
  31. CSSPixels f = 1.0f;
  32. auto width_reciprocal = 1.0f / rect.width().value();
  33. auto height_reciprocal = 1.0f / rect.height().value();
  34. f = max(f, width_reciprocal * (top_left_radius_px.horizontal_radius + top_right_radius_px.horizontal_radius));
  35. f = max(f, height_reciprocal * (top_right_radius_px.vertical_radius + bottom_right_radius_px.vertical_radius));
  36. f = max(f, width_reciprocal * (bottom_left_radius_px.horizontal_radius + bottom_right_radius_px.horizontal_radius));
  37. f = max(f, height_reciprocal * (top_left_radius_px.vertical_radius + bottom_left_radius_px.vertical_radius));
  38. f = 1.0f / f.value();
  39. top_left_radius_px.horizontal_radius *= f;
  40. top_left_radius_px.vertical_radius *= f;
  41. top_right_radius_px.horizontal_radius *= f;
  42. top_right_radius_px.vertical_radius *= f;
  43. bottom_right_radius_px.horizontal_radius *= f;
  44. bottom_right_radius_px.vertical_radius *= f;
  45. bottom_left_radius_px.horizontal_radius *= f;
  46. bottom_left_radius_px.vertical_radius *= f;
  47. return BorderRadiiData { top_left_radius_px, top_right_radius_px, bottom_right_radius_px, bottom_left_radius_px };
  48. }
  49. void paint_border(PaintContext& context, BorderEdge edge, DevicePixelRect const& rect, BorderRadiiData const& border_radii_data, BordersData const& borders_data)
  50. {
  51. auto const& border_data = [&] {
  52. switch (edge) {
  53. case BorderEdge::Top:
  54. return borders_data.top;
  55. case BorderEdge::Right:
  56. return borders_data.right;
  57. case BorderEdge::Bottom:
  58. return borders_data.bottom;
  59. default: // BorderEdge::Left:
  60. return borders_data.left;
  61. }
  62. }();
  63. CSSPixels width = border_data.width;
  64. if (width <= 0)
  65. return;
  66. auto color = border_data.color;
  67. auto border_style = border_data.line_style;
  68. auto device_pixel_width = context.enclosing_device_pixels(width);
  69. struct Points {
  70. DevicePixelPoint p1;
  71. DevicePixelPoint p2;
  72. };
  73. auto points_for_edge = [](BorderEdge edge, DevicePixelRect const& rect) -> Points {
  74. switch (edge) {
  75. case BorderEdge::Top:
  76. return { rect.top_left(), rect.top_right() };
  77. case BorderEdge::Right:
  78. return { rect.top_right(), rect.bottom_right() };
  79. case BorderEdge::Bottom:
  80. return { rect.bottom_left(), rect.bottom_right() };
  81. default: // Edge::Left
  82. return { rect.top_left(), rect.bottom_left() };
  83. }
  84. };
  85. if (border_style == CSS::LineStyle::Inset) {
  86. auto top_left_color = Color::from_rgb(0x5a5a5a);
  87. auto bottom_right_color = Color::from_rgb(0x888888);
  88. color = (edge == BorderEdge::Left || edge == BorderEdge::Top) ? top_left_color : bottom_right_color;
  89. } else if (border_style == CSS::LineStyle::Outset) {
  90. auto top_left_color = Color::from_rgb(0x888888);
  91. auto bottom_right_color = Color::from_rgb(0x5a5a5a);
  92. color = (edge == BorderEdge::Left || edge == BorderEdge::Top) ? top_left_color : bottom_right_color;
  93. }
  94. auto gfx_line_style = Gfx::Painter::LineStyle::Solid;
  95. if (border_style == CSS::LineStyle::Dotted)
  96. gfx_line_style = Gfx::Painter::LineStyle::Dotted;
  97. if (border_style == CSS::LineStyle::Dashed)
  98. gfx_line_style = Gfx::Painter::LineStyle::Dashed;
  99. if (gfx_line_style != Gfx::Painter::LineStyle::Solid) {
  100. auto [p1, p2] = points_for_edge(edge, rect);
  101. switch (edge) {
  102. case BorderEdge::Top:
  103. p1.translate_by(device_pixel_width / 2, device_pixel_width / 2);
  104. p2.translate_by(-device_pixel_width / 2, device_pixel_width / 2);
  105. break;
  106. case BorderEdge::Right:
  107. p1.translate_by(-device_pixel_width / 2, device_pixel_width / 2);
  108. p2.translate_by(-device_pixel_width / 2, -device_pixel_width / 2);
  109. break;
  110. case BorderEdge::Bottom:
  111. p1.translate_by(device_pixel_width / 2, -device_pixel_width / 2);
  112. p2.translate_by(-device_pixel_width / 2, -device_pixel_width / 2);
  113. break;
  114. case BorderEdge::Left:
  115. p1.translate_by(device_pixel_width / 2, device_pixel_width / 2);
  116. p2.translate_by(device_pixel_width / 2, -device_pixel_width / 2);
  117. break;
  118. }
  119. if (border_style == CSS::LineStyle::Dotted) {
  120. Gfx::AntiAliasingPainter aa_painter { context.painter() };
  121. aa_painter.draw_line(p1.to_type<int>(), p2.to_type<int>(), color, device_pixel_width.value(), gfx_line_style);
  122. return;
  123. }
  124. context.painter().draw_line(p1.to_type<int>(), p2.to_type<int>(), color, device_pixel_width.value(), gfx_line_style);
  125. return;
  126. }
  127. auto draw_horizontal_or_vertical_line = [&](auto p1, auto p2) {
  128. // Note: Using fill_rect() here since draw_line() produces some overlapping pixels
  129. // at the end of a line, which cause issues on borders with transparency.
  130. p2.translate_by(1, 1);
  131. context.painter().fill_rect(Gfx::IntRect::from_two_points(p1.template to_type<int>(), p2.template to_type<int>()), color);
  132. };
  133. 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) {
  134. auto [p1, p2] = points_for_edge(edge, rect);
  135. auto p1_step = radius ? 0 : border.width / static_cast<float>(device_pixel_width.value());
  136. auto p2_step = opposite_radius ? 0 : opposite_border.width / static_cast<float>(device_pixel_width.value());
  137. for (DevicePixels i = 0; i < device_pixel_width; ++i) {
  138. draw_horizontal_or_vertical_line(p1, p2);
  139. p1_step_translate(p1, p1_step);
  140. p2_step_translate(p2, p2_step);
  141. }
  142. };
  143. // FIXME: There is some overlap where two borders (without border radii meet),
  144. // which produces artifacts if the border color has some transparency.
  145. // (this only happens if the angle between the two borders is not 45 degrees)
  146. switch (edge) {
  147. case BorderEdge::Top:
  148. draw_border(
  149. borders_data.left, border_radii_data.top_left, borders_data.right, border_radii_data.top_right,
  150. [](auto& current_p1, auto step) {
  151. current_p1.translate_by(step, 1);
  152. },
  153. [](auto& current_p2, auto step) {
  154. current_p2.translate_by(-step, 1);
  155. });
  156. break;
  157. case BorderEdge::Right:
  158. draw_border(
  159. borders_data.top, border_radii_data.top_right, borders_data.bottom, border_radii_data.bottom_right,
  160. [](auto& current_p1, auto step) {
  161. current_p1.translate_by(-1, step);
  162. },
  163. [](auto& current_p2, auto step) {
  164. current_p2.translate_by(-1, -step);
  165. });
  166. break;
  167. case BorderEdge::Bottom:
  168. draw_border(
  169. borders_data.left, border_radii_data.bottom_left, borders_data.right, border_radii_data.bottom_right,
  170. [](auto& current_p1, auto step) {
  171. current_p1.translate_by(step, -1);
  172. },
  173. [](auto& current_p2, auto step) {
  174. current_p2.translate_by(-step, -1);
  175. });
  176. break;
  177. case BorderEdge::Left:
  178. draw_border(
  179. borders_data.top, border_radii_data.top_left, borders_data.bottom, border_radii_data.bottom_left,
  180. [](auto& current_p1, auto step) {
  181. current_p1.translate_by(1, step);
  182. },
  183. [](auto& current_p2, auto step) {
  184. current_p2.translate_by(1, -step);
  185. });
  186. break;
  187. }
  188. }
  189. RefPtr<Gfx::Bitmap> get_cached_corner_bitmap(DevicePixelSize corners_size)
  190. {
  191. auto allocate_mask_bitmap = [&]() -> RefPtr<Gfx::Bitmap> {
  192. auto bitmap = Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, corners_size.to_type<int>());
  193. if (!bitmap.is_error())
  194. return bitmap.release_value();
  195. return nullptr;
  196. };
  197. // FIXME: Allocate per page?
  198. static thread_local auto corner_bitmap = allocate_mask_bitmap();
  199. // Only reallocate the corner bitmap is the existing one is too small.
  200. // (should mean no more allocations after the first paint -- amortised zero allocations :^))
  201. if (corner_bitmap && corner_bitmap->rect().size().contains(corners_size.to_type<int>())) {
  202. Gfx::Painter painter { *corner_bitmap };
  203. painter.clear_rect({ { 0, 0 }, corners_size }, Gfx::Color());
  204. } else {
  205. corner_bitmap = allocate_mask_bitmap();
  206. if (!corner_bitmap) {
  207. dbgln("Failed to allocate corner bitmap with size {}", corners_size);
  208. return nullptr;
  209. }
  210. }
  211. return corner_bitmap;
  212. }
  213. void paint_all_borders(PaintContext& context, CSSPixelRect const& bordered_rect, BorderRadiiData const& border_radii_data, BordersData const& borders_data)
  214. {
  215. if (borders_data.top.width <= 0 && borders_data.right.width <= 0 && borders_data.left.width <= 0 && borders_data.bottom.width <= 0)
  216. return;
  217. auto border_rect = context.rounded_device_rect(bordered_rect);
  218. auto top_left = border_radii_data.top_left.as_corner(context);
  219. auto top_right = border_radii_data.top_right.as_corner(context);
  220. auto bottom_right = border_radii_data.bottom_right.as_corner(context);
  221. auto bottom_left = border_radii_data.bottom_left.as_corner(context);
  222. // Disable border radii if the corresponding borders don't exist:
  223. if (borders_data.bottom.width <= 0 && borders_data.left.width <= 0)
  224. bottom_left = { 0, 0 };
  225. if (borders_data.bottom.width <= 0 && borders_data.right.width <= 0)
  226. bottom_right = { 0, 0 };
  227. if (borders_data.top.width <= 0 && borders_data.left.width <= 0)
  228. top_left = { 0, 0 };
  229. if (borders_data.top.width <= 0 && borders_data.right.width <= 0)
  230. top_right = { 0, 0 };
  231. DevicePixelRect top_border_rect = {
  232. border_rect.x() + top_left.horizontal_radius,
  233. border_rect.y(),
  234. border_rect.width() - top_left.horizontal_radius - top_right.horizontal_radius,
  235. context.enclosing_device_pixels(borders_data.top.width)
  236. };
  237. DevicePixelRect right_border_rect = {
  238. border_rect.x() + (border_rect.width() - context.enclosing_device_pixels(borders_data.right.width)),
  239. border_rect.y() + top_right.vertical_radius,
  240. context.enclosing_device_pixels(borders_data.right.width),
  241. border_rect.height() - top_right.vertical_radius - bottom_right.vertical_radius
  242. };
  243. DevicePixelRect bottom_border_rect = {
  244. border_rect.x() + bottom_left.horizontal_radius,
  245. border_rect.y() + (border_rect.height() - context.enclosing_device_pixels(borders_data.bottom.width)),
  246. border_rect.width() - bottom_left.horizontal_radius - bottom_right.horizontal_radius,
  247. context.enclosing_device_pixels(borders_data.bottom.width)
  248. };
  249. DevicePixelRect left_border_rect = {
  250. border_rect.x(),
  251. border_rect.y() + top_left.vertical_radius,
  252. context.enclosing_device_pixels(borders_data.left.width),
  253. border_rect.height() - top_left.vertical_radius - bottom_left.vertical_radius
  254. };
  255. // Avoid overlapping pixels on the edges, in the easy 45 degree corners case:
  256. if (!top_left && top_border_rect.height() == left_border_rect.width())
  257. top_border_rect.shrink(0, 0, 0, 1);
  258. if (!top_right && top_border_rect.height() == right_border_rect.width())
  259. top_border_rect.shrink(0, 1, 0, 0);
  260. if (!bottom_left && bottom_border_rect.height() == left_border_rect.width())
  261. bottom_border_rect.shrink(0, 0, 0, 1);
  262. if (!bottom_right && bottom_border_rect.height() == right_border_rect.width())
  263. bottom_border_rect.shrink(0, 1, 0, 0);
  264. auto border_color_no_alpha = borders_data.top.color;
  265. border_color_no_alpha.set_alpha(255);
  266. // Paint the strait line part of the border:
  267. Painting::paint_border(context, Painting::BorderEdge::Top, top_border_rect, border_radii_data, borders_data);
  268. Painting::paint_border(context, Painting::BorderEdge::Right, right_border_rect, border_radii_data, borders_data);
  269. Painting::paint_border(context, Painting::BorderEdge::Bottom, bottom_border_rect, border_radii_data, borders_data);
  270. Painting::paint_border(context, Painting::BorderEdge::Left, left_border_rect, border_radii_data, borders_data);
  271. if (!top_left && !top_right && !bottom_left && !bottom_right)
  272. return;
  273. // Cache the smallest possible bitmap to render just the corners for the border.
  274. auto expand_width = abs(context.enclosing_device_pixels(borders_data.left.width) - context.enclosing_device_pixels(borders_data.right.width));
  275. auto expand_height = abs(context.enclosing_device_pixels(borders_data.top.width) - context.enclosing_device_pixels(borders_data.bottom.width));
  276. DevicePixelRect corner_mask_rect {
  277. 0, 0,
  278. max(
  279. top_left.horizontal_radius + top_right.horizontal_radius + expand_width.value(),
  280. bottom_left.horizontal_radius + bottom_right.horizontal_radius + expand_height.value()),
  281. max(
  282. top_left.vertical_radius + bottom_left.vertical_radius + expand_width.value(),
  283. top_right.vertical_radius + bottom_right.vertical_radius + expand_height.value())
  284. };
  285. auto corner_bitmap = get_cached_corner_bitmap(corner_mask_rect.size());
  286. if (!corner_bitmap)
  287. return;
  288. Gfx::Painter painter { *corner_bitmap };
  289. Gfx::AntiAliasingPainter aa_painter { painter };
  290. // Paint a little tile sheet for the corners
  291. // TODO: Support various line styles on the corners (dotted, dashes, etc)
  292. // Paint the outer (minimal) corner rounded rectangle:
  293. aa_painter.fill_rect_with_rounded_corners(corner_mask_rect.to_type<int>(), border_color_no_alpha, top_left, top_right, bottom_right, bottom_left);
  294. // Subtract the inner corner rectangle:
  295. auto inner_corner_mask_rect = corner_mask_rect.shrunken(
  296. context.enclosing_device_pixels(borders_data.top.width),
  297. context.enclosing_device_pixels(borders_data.right.width),
  298. context.enclosing_device_pixels(borders_data.bottom.width),
  299. context.enclosing_device_pixels(borders_data.left.width));
  300. auto inner_top_left = top_left;
  301. auto inner_top_right = top_right;
  302. auto inner_bottom_right = bottom_right;
  303. auto inner_bottom_left = bottom_left;
  304. inner_top_left.horizontal_radius = max(0, inner_top_left.horizontal_radius - context.enclosing_device_pixels(borders_data.left.width).value());
  305. inner_top_left.vertical_radius = max(0, inner_top_left.vertical_radius - context.enclosing_device_pixels(borders_data.top.width).value());
  306. inner_top_right.horizontal_radius = max(0, inner_top_right.horizontal_radius - context.enclosing_device_pixels(borders_data.right.width).value());
  307. inner_top_right.vertical_radius = max(0, inner_top_right.vertical_radius - context.enclosing_device_pixels(borders_data.top.width).value());
  308. inner_bottom_right.horizontal_radius = max(0, inner_bottom_right.horizontal_radius - context.enclosing_device_pixels(borders_data.right.width).value());
  309. inner_bottom_right.vertical_radius = max(0, inner_bottom_right.vertical_radius - context.enclosing_device_pixels(borders_data.bottom.width).value());
  310. inner_bottom_left.horizontal_radius = max(0, inner_bottom_left.horizontal_radius - context.enclosing_device_pixels(borders_data.left.width).value());
  311. inner_bottom_left.vertical_radius = max(0, inner_bottom_left.vertical_radius - context.enclosing_device_pixels(borders_data.bottom.width).value());
  312. aa_painter.fill_rect_with_rounded_corners(inner_corner_mask_rect.to_type<int>(), border_color_no_alpha, inner_top_left, inner_top_right, inner_bottom_right, inner_bottom_left, Gfx::AntiAliasingPainter::BlendMode::AlphaSubtract);
  313. // TODO: Support dual color corners. Other browsers will render a rounded corner between two borders of
  314. // different colors using both colours, normally split at a 45 degree angle (though the exact angle is interpolated).
  315. auto blit_corner = [&](Gfx::IntPoint position, Gfx::IntRect const& src_rect, Color corner_color) {
  316. context.painter().blit_filtered(position, *corner_bitmap, src_rect, [&](auto const& corner_pixel) {
  317. return corner_color.with_alpha((corner_color.alpha() * corner_pixel.alpha()) / 255);
  318. });
  319. };
  320. // FIXME: Corners should actually split between the two colors, if both are provided (and differ)
  321. auto pick_corner_color = [](auto const& border, auto const& adjacent_border) {
  322. if (border.width > 0)
  323. return border.color;
  324. return adjacent_border.color;
  325. };
  326. // Blit the corners into to their corresponding locations:
  327. if (top_left)
  328. blit_corner(border_rect.top_left().to_type<int>(), top_left.as_rect(), pick_corner_color(borders_data.top, borders_data.left));
  329. if (top_right)
  330. blit_corner(border_rect.top_right().to_type<int>().translated(-top_right.horizontal_radius + 1, 0), top_right.as_rect().translated(corner_mask_rect.width().value() - top_right.horizontal_radius, 0), pick_corner_color(borders_data.top, borders_data.right));
  331. if (bottom_right)
  332. blit_corner(border_rect.bottom_right().to_type<int>().translated(-bottom_right.horizontal_radius + 1, -bottom_right.vertical_radius + 1), bottom_right.as_rect().translated(corner_mask_rect.width().value() - bottom_right.horizontal_radius, corner_mask_rect.height().value() - bottom_right.vertical_radius), pick_corner_color(borders_data.bottom, borders_data.right));
  333. if (bottom_left)
  334. blit_corner(border_rect.bottom_left().to_type<int>().translated(0, -bottom_left.vertical_radius + 1), bottom_left.as_rect().translated(0, corner_mask_rect.height().value() - bottom_left.vertical_radius), pick_corner_color(borders_data.bottom, borders_data.left));
  335. }
  336. }