From fe7e7974836bd95402947647ed11e31ceec60ffb Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Wed, 2 Aug 2023 17:24:14 +0100 Subject: [PATCH] LibWeb: Implement the CSS `outline` property :^) ...along with `outline-color`, `outline-style`, and `outline-width`. This re-uses the existing border-painting code, which seems to work well enough! This replaces the previous code for drawing focus-outlines, with generic outline painting for any elements that want it. Focus outlines are now instead supported by this code in Default.css: ```css :focus-visible { outline: auto; } ``` --- Base/res/html/misc/inline-node.html | 16 +++++++++ Base/res/html/misc/outline.html | 33 +++++++++++++++++ Base/res/html/misc/welcome.html | 1 + .../Libraries/LibWeb/CSS/ComputedValues.h | 13 +++++++ Userland/Libraries/LibWeb/CSS/Enums.json | 12 +++++++ Userland/Libraries/LibWeb/CSS/Properties.json | 11 +++--- .../CSS/ResolvedCSSStyleDeclaration.cpp | 13 +++++++ .../Libraries/LibWeb/CSS/StyleProperties.cpp | 6 ++++ .../Libraries/LibWeb/CSS/StyleProperties.h | 1 + Userland/Libraries/LibWeb/Layout/Node.cpp | 7 ++++ .../LibWeb/Painting/BorderPainting.cpp | 27 +++++++++++++- .../LibWeb/Painting/BorderPainting.h | 9 +++++ .../LibWeb/Painting/InlinePaintable.cpp | 26 +++++++++++--- .../LibWeb/Painting/PaintableBox.cpp | 35 ++++++------------- .../LibWeb/Painting/StackingContext.cpp | 4 +-- 15 files changed, 174 insertions(+), 40 deletions(-) create mode 100644 Base/res/html/misc/outline.html diff --git a/Base/res/html/misc/inline-node.html b/Base/res/html/misc/inline-node.html index 85d5f90d8e..d2f247cc45 100644 --- a/Base/res/html/misc/inline-node.html +++ b/Base/res/html/misc/inline-node.html @@ -32,6 +32,19 @@ border-radius: 6px; box-shadow: 4px 4px 4px darkgreen; } + + .outline { + outline: 3px dotted magenta; + } + .outline2 { + outline: 1px solid red; + border-radius: 10px; + } + .outline3 { + outline: 2px solid green; + border-radius: 10px; + border: 2px solid black; + } @@ -43,5 +56,8 @@ Hello world this is some text in a box. This text has a background and this text has a shadow!
This text should only have a strip of red on the left
+
+ This text has an outline and this text has an outline with a border radius, and this also has a border. +
diff --git a/Base/res/html/misc/outline.html b/Base/res/html/misc/outline.html new file mode 100644 index 0000000000..f1ba9ef76a --- /dev/null +++ b/Base/res/html/misc/outline.html @@ -0,0 +1,33 @@ + + + + Outlines + + + +

Outlines

+

I have the default outline!

+

I have an outline!

+

I have an outline and a radius!

+

My outline is dotted and brown!

+ + diff --git a/Base/res/html/misc/welcome.html b/Base/res/html/misc/welcome.html index 38382d98f3..c7043ddbe1 100644 --- a/Base/res/html/misc/welcome.html +++ b/Base/res/html/misc/welcome.html @@ -126,6 +126,7 @@
  • Fonts
  • Borders
  • Border-Radius
  • +
  • Outlines
  • Lists
  • Flexboxes
  • Flexbox order
  • diff --git a/Userland/Libraries/LibWeb/CSS/ComputedValues.h b/Userland/Libraries/LibWeb/CSS/ComputedValues.h index adbce49c4b..25a6e1e7f6 100644 --- a/Userland/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Userland/Libraries/LibWeb/CSS/ComputedValues.h @@ -107,6 +107,9 @@ public: static Vector> grid_template_areas() { return {}; } static CSS::Time transition_delay() { return CSS::Time::make_seconds(0); } static CSS::ObjectFit object_fit() { return CSS::ObjectFit::Fill; } + static Color outline_color() { return Color::Black; } + static CSS::OutlineStyle outline_style() { return CSS::OutlineStyle::None; } + static CSS::Length outline_width() { return CSS::Length::make_px(3); } }; enum class BackgroundSize { @@ -324,6 +327,10 @@ public: CSS::FontVariant font_variant() const { return m_inherited.font_variant; } CSS::Time transition_delay() const { return m_noninherited.transition_delay; } + Color outline_color() const { return m_noninherited.outline_color; } + CSS::OutlineStyle outline_style() const { return m_noninherited.outline_style; } + CSS::Length outline_width() const { return m_noninherited.outline_width; } + ComputedValues clone_inherited_values() const { ComputedValues clone; @@ -434,6 +441,9 @@ protected: Gfx::Color stop_color { InitialValues::stop_color() }; float stop_opacity { InitialValues::stop_opacity() }; CSS::Time transition_delay { InitialValues::transition_delay() }; + Color outline_color { InitialValues::outline_color() }; + CSS::OutlineStyle outline_style { InitialValues::outline_style() }; + CSS::Length outline_width { InitialValues::outline_width() }; } m_noninherited; }; @@ -543,6 +553,9 @@ public: void set_stop_color(Color value) { m_noninherited.stop_color = value; } void set_stop_opacity(float value) { m_noninherited.stop_opacity = value; } void set_text_anchor(CSS::TextAnchor value) { m_inherited.text_anchor = value; } + void set_outline_color(Color value) { m_noninherited.outline_color = value; } + void set_outline_style(CSS::OutlineStyle value) { m_noninherited.outline_style = value; } + void set_outline_width(CSS::Length value) { m_noninherited.outline_width = value; } }; } diff --git a/Userland/Libraries/LibWeb/CSS/Enums.json b/Userland/Libraries/LibWeb/CSS/Enums.json index d30405af05..423ade2ff3 100644 --- a/Userland/Libraries/LibWeb/CSS/Enums.json +++ b/Userland/Libraries/LibWeb/CSS/Enums.json @@ -245,6 +245,18 @@ "none", "scale-down" ], + "outline-style": [ + "auto", + "none", + "dotted", + "dashed", + "solid", + "double", + "groove", + "ridge", + "inset", + "outset" + ], "overflow": [ "auto", "clip", diff --git a/Userland/Libraries/LibWeb/CSS/Properties.json b/Userland/Libraries/LibWeb/CSS/Properties.json index fb047d0958..726c683c76 100644 --- a/Userland/Libraries/LibWeb/CSS/Properties.json +++ b/Userland/Libraries/LibWeb/CSS/Properties.json @@ -1587,8 +1587,7 @@ "outline": { "affects-layout": false, "inherited": false, - "__comment": "FIXME: Initial value is really `medium invert none` but we don't yet parse the outline shorthand.", - "initial": "none", + "initial": "medium currentColor none", "longhands": [ "outline-color", "outline-style", @@ -1598,12 +1597,10 @@ "outline-color": { "affects-layout": false, "inherited": false, - "initial": "invert", + "__comment": "FIXME: We don't yet support `invert`. Until we do, the spec directs us to use `currentColor` as the default instead, and reject `invert`", + "initial": "currentColor", "valid-types": [ "color" - ], - "valid-identifiers": [ - "invert" ] }, "outline-style": { @@ -1611,7 +1608,7 @@ "inherited": false, "initial": "none", "valid-types": [ - "line-style" + "outline-style" ] }, "outline-width": { diff --git a/Userland/Libraries/LibWeb/CSS/ResolvedCSSStyleDeclaration.cpp b/Userland/Libraries/LibWeb/CSS/ResolvedCSSStyleDeclaration.cpp index b886b0d4a9..f8c5a55ca2 100644 --- a/Userland/Libraries/LibWeb/CSS/ResolvedCSSStyleDeclaration.cpp +++ b/Userland/Libraries/LibWeb/CSS/ResolvedCSSStyleDeclaration.cpp @@ -729,6 +729,19 @@ ErrorOr> ResolvedCSSStyleDeclaration::style_value_for_p return NumberStyleValue::create(layout_node.computed_values().opacity()); case PropertyID::Order: return IntegerStyleValue::create(layout_node.computed_values().order()); + case PropertyID::Outline: { + return StyleValueList::create( + { TRY(style_value_for_property(layout_node, PropertyID::OutlineColor)).release_nonnull(), + TRY(style_value_for_property(layout_node, PropertyID::OutlineStyle)).release_nonnull(), + TRY(style_value_for_property(layout_node, PropertyID::OutlineWidth)).release_nonnull() }, + StyleValueList::Separator::Space); + } + case PropertyID::OutlineColor: + return ColorStyleValue::create(layout_node.computed_values().outline_color()); + case PropertyID::OutlineStyle: + return IdentifierStyleValue::create(to_value_id(layout_node.computed_values().outline_style())); + case PropertyID::OutlineWidth: + return LengthStyleValue::create(layout_node.computed_values().outline_width()); case PropertyID::OverflowX: return IdentifierStyleValue::create(to_value_id(layout_node.computed_values().overflow_x())); case PropertyID::OverflowY: diff --git a/Userland/Libraries/LibWeb/CSS/StyleProperties.cpp b/Userland/Libraries/LibWeb/CSS/StyleProperties.cpp index 16ad612786..69e5f6cac0 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleProperties.cpp +++ b/Userland/Libraries/LibWeb/CSS/StyleProperties.cpp @@ -626,6 +626,12 @@ Optional StyleProperties::line_style(CSS::PropertyID property_id return value_id_to_line_style(value->to_identifier()); } +Optional StyleProperties::outline_style() const +{ + auto value = property(CSS::PropertyID::OutlineStyle); + return value_id_to_outline_style(value->to_identifier()); +} + Optional StyleProperties::float_() const { auto value = property(CSS::PropertyID::Float); diff --git a/Userland/Libraries/LibWeb/CSS/StyleProperties.h b/Userland/Libraries/LibWeb/CSS/StyleProperties.h index c857f7dd08..4f321c28e6 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleProperties.h +++ b/Userland/Libraries/LibWeb/CSS/StyleProperties.h @@ -68,6 +68,7 @@ public: Optional cursor() const; Optional white_space() const; Optional line_style(CSS::PropertyID) const; + Optional outline_style() const; Vector text_decoration_line() const; Optional text_decoration_style() const; Optional text_transform() const; diff --git a/Userland/Libraries/LibWeb/Layout/Node.cpp b/Userland/Libraries/LibWeb/Layout/Node.cpp index 6284f85c50..7fba504bec 100644 --- a/Userland/Libraries/LibWeb/Layout/Node.cpp +++ b/Userland/Libraries/LibWeb/Layout/Node.cpp @@ -707,6 +707,13 @@ void NodeWithStyle::apply_style(const CSS::StyleProperties& computed_style) do_border_style(computed_values.border_right(), CSS::PropertyID::BorderRightWidth, CSS::PropertyID::BorderRightColor, CSS::PropertyID::BorderRightStyle); do_border_style(computed_values.border_bottom(), CSS::PropertyID::BorderBottomWidth, CSS::PropertyID::BorderBottomColor, CSS::PropertyID::BorderBottomStyle); + if (auto outline_color = computed_style.property(CSS::PropertyID::OutlineColor); outline_color->has_color()) + computed_values.set_outline_color(outline_color->to_color(*this)); + if (auto outline_style = computed_style.outline_style(); outline_style.has_value()) + computed_values.set_outline_style(outline_style.value()); + if (auto outline_width = computed_style.property(CSS::PropertyID::OutlineWidth); outline_width->is_length()) + computed_values.set_outline_width(outline_width->as_length().length()); + computed_values.set_content(computed_style.content()); computed_values.set_grid_auto_columns(computed_style.grid_auto_columns()); computed_values.set_grid_auto_rows(computed_style.grid_auto_rows()); diff --git a/Userland/Libraries/LibWeb/Painting/BorderPainting.cpp b/Userland/Libraries/LibWeb/Painting/BorderPainting.cpp index d300e39c11..94bb2f99ac 100644 --- a/Userland/Libraries/LibWeb/Painting/BorderPainting.cpp +++ b/Userland/Libraries/LibWeb/Painting/BorderPainting.cpp @@ -1,6 +1,6 @@ /* * Copyright (c) 2020, Andreas Kling - * Copyright (c) 2021-2022, Sam Atkins + * Copyright (c) 2021-2023, Sam Atkins * Copyright (c) 2022, MacDue * * SPDX-License-Identifier: BSD-2-Clause @@ -10,6 +10,8 @@ #include #include #include +#include +#include #include #include @@ -627,4 +629,27 @@ void paint_all_borders(PaintContext& context, CSSPixelRect const& bordered_rect, } } +Optional borders_data_for_outline(Layout::Node const& layout_node, Color outline_color, CSS::OutlineStyle outline_style, CSSPixels outline_width) +{ + CSS::LineStyle line_style; + if (outline_style == CSS::OutlineStyle::Auto) { + // `auto` lets us do whatever we want for the outline. 2px of the link colour seems reasonable. + line_style = CSS::LineStyle::Dotted; + outline_color = layout_node.document().link_color(); + outline_width = 2; + } else { + line_style = CSS::value_id_to_line_style(CSS::to_value_id(outline_style)).value_or(CSS::LineStyle::None); + } + + if (outline_color.alpha() == 0 || line_style == CSS::LineStyle::None || outline_width == 0) + return {}; + + CSS::BorderData border_data { + .color = outline_color, + .line_style = line_style, + .width = outline_width, + }; + return BordersData { border_data, border_data, border_data, border_data }; +} + } diff --git a/Userland/Libraries/LibWeb/Painting/BorderPainting.h b/Userland/Libraries/LibWeb/Painting/BorderPainting.h index 47e85fe10d..adc0fbbec0 100644 --- a/Userland/Libraries/LibWeb/Painting/BorderPainting.h +++ b/Userland/Libraries/LibWeb/Painting/BorderPainting.h @@ -1,5 +1,6 @@ /* * Copyright (c) 2020, Andreas Kling + * Copyright (c) 2021-2023, Sam Atkins * * SPDX-License-Identifier: BSD-2-Clause */ @@ -55,6 +56,11 @@ struct BorderRadiiData { bottom_right.shrink(right, bottom); bottom_left.shrink(left, bottom); } + + inline void inflate(CSSPixels top, CSSPixels right, CSSPixels bottom, CSSPixels left) + { + shrink(-top, -right, -bottom, -left); + } }; BorderRadiiData normalized_border_radii_data(Layout::Node const&, CSSPixelRect const&, CSS::BorderRadiusData top_left_radius, CSS::BorderRadiusData top_right_radius, CSS::BorderRadiusData bottom_right_radius, CSS::BorderRadiusData bottom_left_radius); @@ -72,6 +78,9 @@ struct BordersData { CSS::BorderData left; }; +// Returns OptionalNone if there is no outline to paint. +Optional borders_data_for_outline(Layout::Node const&, Color outline_color, CSS::OutlineStyle outline_style, CSSPixels outline_width); + RefPtr get_cached_corner_bitmap(DevicePixelSize corners_size); void paint_border(PaintContext& context, BorderEdge edge, DevicePixelRect const& rect, Gfx::AntiAliasingPainter::CornerRadius const& radius, Gfx::AntiAliasingPainter::CornerRadius const& opposite_radius, BordersData const& borders_data, Gfx::Path& path, bool last); diff --git a/Userland/Libraries/LibWeb/Painting/InlinePaintable.cpp b/Userland/Libraries/LibWeb/Painting/InlinePaintable.cpp index a3557b836b..e4d9835240 100644 --- a/Userland/Libraries/LibWeb/Painting/InlinePaintable.cpp +++ b/Userland/Libraries/LibWeb/Painting/InlinePaintable.cpp @@ -86,7 +86,7 @@ void InlinePaintable::paint(PaintContext& context, PaintPhase phase) const }); } - if (phase == PaintPhase::Border) { + auto paint_border_or_outline = [&](Optional outline_data = {}) { auto top_left_border_radius = computed_values().border_top_left_radius(); auto top_right_border_radius = computed_values().border_top_right_radius(); auto bottom_right_border_radius = computed_values().border_bottom_right_radius(); @@ -115,13 +115,31 @@ void InlinePaintable::paint(PaintContext& context, PaintPhase phase) const absolute_fragment_rect.set_width(absolute_fragment_rect.width() + extra_end_width); } - auto bordered_rect = absolute_fragment_rect.inflated(borders_data.top.width, borders_data.right.width, borders_data.bottom.width, borders_data.left.width); - auto border_radii_data = normalized_border_radii_data(layout_node(), bordered_rect, top_left_border_radius, top_right_border_radius, bottom_right_border_radius, bottom_left_border_radius); + auto borders_rect = absolute_fragment_rect.inflated(borders_data.top.width, borders_data.right.width, borders_data.bottom.width, borders_data.left.width); + auto border_radii_data = normalized_border_radii_data(layout_node(), borders_rect, top_left_border_radius, top_right_border_radius, bottom_right_border_radius, bottom_left_border_radius); - paint_all_borders(context, bordered_rect, border_radii_data, borders_data); + if (outline_data.has_value()) { + border_radii_data.inflate(outline_data->top.width, outline_data->right.width, outline_data->bottom.width, outline_data->left.width); + borders_rect.inflate(outline_data->top.width, outline_data->right.width, outline_data->bottom.width, outline_data->left.width); + paint_all_borders(context, borders_rect, border_radii_data, *outline_data); + } else { + paint_all_borders(context, borders_rect, border_radii_data, borders_data); + } return IterationDecision::Continue; }); + }; + + if (phase == PaintPhase::Border) { + paint_border_or_outline(); + } + + if (phase == PaintPhase::Outline) { + auto outline_width = computed_values().outline_width().to_px(layout_node()); + auto maybe_outline_data = borders_data_for_outline(layout_node(), computed_values().outline_color(), computed_values().outline_style(), outline_width); + if (maybe_outline_data.has_value()) { + paint_border_or_outline(maybe_outline_data.value()); + } } if (phase == PaintPhase::Overlay && layout_node().document().inspected_layout_node() == &layout_node()) { diff --git a/Userland/Libraries/LibWeb/Painting/PaintableBox.cpp b/Userland/Libraries/LibWeb/Painting/PaintableBox.cpp index 047991c8d7..460a3753a6 100644 --- a/Userland/Libraries/LibWeb/Painting/PaintableBox.cpp +++ b/Userland/Libraries/LibWeb/Painting/PaintableBox.cpp @@ -170,6 +170,16 @@ void PaintableBox::paint(PaintContext& context, PaintPhase phase) const paint_border(context); } + if (phase == PaintPhase::Outline) { + auto outline_width = computed_values().outline_width().to_px(layout_node()); + auto borders_data = borders_data_for_outline(layout_node(), computed_values().outline_color(), computed_values().outline_style(), outline_width); + if (borders_data.has_value()) { + auto border_radius_data = normalized_border_radii_data(ShrinkRadiiForBorders::No); + border_radius_data.inflate(outline_width, outline_width, outline_width, outline_width); + paint_all_borders(context, absolute_border_box_rect().inflated(outline_width, outline_width, outline_width, outline_width), border_radius_data, borders_data.value()); + } + } + if (phase == PaintPhase::Overlay && should_clip_rect) context.painter().restore(); @@ -216,12 +226,6 @@ void PaintableBox::paint(PaintContext& context, PaintPhase phase) const context.painter().draw_rect(size_text_device_rect, context.palette().threed_shadow1()); context.painter().draw_text(size_text_device_rect, size_text, font, Gfx::TextAlignment::Center, context.palette().color(Gfx::ColorRole::TooltipText)); } - - if (phase == PaintPhase::Outline && layout_box().dom_node() && layout_box().dom_node()->is_element() && verify_cast(*layout_box().dom_node()).is_focused()) { - // FIXME: Implement this as `outline` using :focus-visible in the default UA stylesheet to make it possible to override/disable. - auto focus_outline_rect = context.enclosing_device_rect(absolute_border_box_rect()).inflated(4, 4); - context.painter().draw_focus_rect(focus_outline_rect.to_type(), context.palette().focus_outline()); - } } BordersData PaintableBox::remove_element_kind_from_borders_data(PaintableBox::BordersDataWithElementKind borders_data) @@ -639,25 +643,6 @@ void PaintableWithLines::paint(PaintContext& context, PaintPhase phase) const if (corner_clipper.has_value()) corner_clipper->blit_corner_clipping(context.painter()); } - - // FIXME: Merge this loop with the above somehow.. - if (phase == PaintPhase::Outline) { - for (auto& line_box : m_line_boxes) { - for (auto& fragment : line_box.fragments()) { - auto* node = fragment.layout_node().dom_node(); - if (!node) - continue; - auto* parent = node->parent_element(); - if (!parent) - continue; - if (parent->is_focused()) { - // FIXME: Implement this as `outline` using :focus-visible in the default UA stylesheet to make it possible to override/disable. - auto focus_outline_rect = context.enclosing_device_rect(fragment.absolute_rect()).to_type().inflated(4, 4); - context.painter().draw_focus_rect(focus_outline_rect, context.palette().focus_outline()); - } - } - } - } } bool PaintableWithLines::handle_mousewheel(Badge, CSSPixelPoint, unsigned, unsigned, int wheel_delta_x, int wheel_delta_y) diff --git a/Userland/Libraries/LibWeb/Painting/StackingContext.cpp b/Userland/Libraries/LibWeb/Painting/StackingContext.cpp index b904f74a76..c81b93e280 100644 --- a/Userland/Libraries/LibWeb/Painting/StackingContext.cpp +++ b/Userland/Libraries/LibWeb/Painting/StackingContext.cpp @@ -124,9 +124,7 @@ void StackingContext::paint_descendants(PaintContext& context, Layout::Node cons paint_descendants(context, child, phase); break; case StackingContextPaintPhase::FocusAndOverlay: - if (context.has_focus()) { - paint_node(child, context, PaintPhase::Outline); - } + paint_node(child, context, PaintPhase::Outline); paint_node(child, context, PaintPhase::Overlay); paint_descendants(context, child, phase); break;