diff --git a/Userland/Libraries/LibWeb/CSS/StyleValue.cpp b/Userland/Libraries/LibWeb/CSS/StyleValue.cpp index 6be6795082..e6fee9c440 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleValue.cpp +++ b/Userland/Libraries/LibWeb/CSS/StyleValue.cpp @@ -217,6 +217,12 @@ PositionStyleValue const& StyleValue::as_position() const return static_cast(*this); } +RadialGradientStyleValue const& StyleValue::as_radial_gradient() const +{ + VERIFY(is_radial_gradient()); + return static_cast(*this); +} + RectStyleValue const& StyleValue::as_rect() const { VERIFY(is_rect()); @@ -1984,6 +1990,163 @@ bool PositionValue::operator==(PositionValue const& other) const && variant_equals(vertical_position, other.vertical_position)); } +String RadialGradientStyleValue::to_string() const +{ + StringBuilder builder; + builder.appendff("radial-gradient({} "sv, + m_ending_shape == EndingShape::Circle ? "circle"sv : "ellipse"sv); + + m_size.visit( + [&](Extent extent) { + builder.append([&] { + switch (extent) { + case Extent::ClosestCorner: + return "closest-corner"sv; + case Extent::ClosestSide: + return "closest-side"sv; + case Extent::FarthestCorner: + return "farthest-corner"sv; + case Extent::FarthestSide: + return "farthest-side"sv; + default: + VERIFY_NOT_REACHED(); + } + }()); + }, + [&](CircleSize const& circle_size) { builder.append(circle_size.radius.to_string()); }, + [&](EllipseSize const& ellipse_size) { builder.appendff("{} {}", ellipse_size.radius_a.to_string(), ellipse_size.radius_b.to_string()); }); + + if (m_position != PositionValue::center()) { + builder.appendff(" at "sv); + m_position.serialize(builder); + } + + builder.append(", "sv); + serialize_color_stop_list(builder, m_color_stop_list); + builder.append(')'); + return builder.to_string(); +} + +Gfx::FloatSize RadialGradientStyleValue::resolve_size(Layout::Node const& node, Gfx::FloatPoint center, Gfx::FloatRect const& size) const +{ + auto const side_shape = [&](auto distance_function) { + auto const distance_from = [&](float v, float a, float b, auto distance_function) { + return distance_function(fabs(a - v), fabs(b - v)); + }; + auto x_dist = distance_from(center.x(), size.left(), size.right(), distance_function); + auto y_dist = distance_from(center.y(), size.top(), size.bottom(), distance_function); + if (m_ending_shape == EndingShape::Circle) { + auto dist = distance_function(x_dist, y_dist); + return Gfx::FloatSize { dist, dist }; + } else { + return Gfx::FloatSize { x_dist, y_dist }; + } + }; + + auto const closest_side_shape = [&] { + return side_shape(AK::min); + }; + + auto const farthest_side_shape = [&] { + return side_shape(AK::max); + }; + + auto const corner_distance = [&](auto distance_compare, Gfx::FloatPoint& corner) { + auto top_left_distance = size.top_left().distance_from(center); + auto top_right_distance = size.top_right().distance_from(center); + auto bottom_right_distance = size.bottom_right().distance_from(center); + auto bottom_left_distance = size.bottom_left().distance_from(center); + auto distance = top_left_distance; + if (distance_compare(top_right_distance, distance)) { + corner = size.top_right(); + distance = top_right_distance; + } + if (distance_compare(bottom_right_distance, distance)) { + corner = size.top_right(); + distance = bottom_right_distance; + } + if (distance_compare(bottom_left_distance, distance)) { + corner = size.top_right(); + distance = bottom_left_distance; + } + return distance; + }; + + auto const closest_corner_distance = [&](Gfx::FloatPoint& corner) { + return corner_distance([](float a, float b) { return a < b; }, corner); + }; + + auto const farthest_corner_distance = [&](Gfx::FloatPoint& corner) { + return corner_distance([](float a, float b) { return a > b; }, corner); + }; + + auto const corner_shape = [&](auto corner_distance, auto get_shape) { + Gfx::FloatPoint corner {}; + auto distance = corner_distance(corner); + if (m_ending_shape == EndingShape::Ellipse) { + auto shape = get_shape(); + auto aspect_ratio = shape.width() / shape.height(); + auto p = corner - center; + auto radius_a = AK::sqrt(p.y() * p.y() * aspect_ratio * aspect_ratio + p.x() * p.x()); + auto radius_b = radius_a / aspect_ratio; + return Gfx::FloatSize { radius_a, radius_b }; + } + return Gfx::FloatSize { distance, distance }; + }; + + // https://w3c.github.io/csswg-drafts/css-images/#radial-gradient-syntax + return m_size.visit( + [&](Extent extent) { + switch (extent) { + case Extent::ClosestSide: + // The ending shape is sized so that it exactly meets the side of the gradient box closest to the gradient’s center. + // If the shape is an ellipse, it exactly meets the closest side in each dimension. + return closest_side_shape(); + case Extent::ClosestCorner: + // The ending shape is sized so that it passes through the corner of the gradient box closest to the gradient’s center. + // If the shape is an ellipse, the ending shape is given the same aspect-ratio it would have if closest-side were specified + return corner_shape(closest_corner_distance, closest_side_shape); + case Extent::FarthestCorner: + // Same as closest-corner, except the ending shape is sized based on the farthest corner. + // If the shape is an ellipse, the ending shape is given the same aspect ratio it would have if farthest-side were specified. + return corner_shape(farthest_corner_distance, farthest_side_shape); + case Extent::FarthestSide: + // Same as closest-side, except the ending shape is sized based on the farthest side(s). + return farthest_side_shape(); + default: + VERIFY_NOT_REACHED(); + } + }, + [&](CircleSize const& circle_size) { + auto radius = circle_size.radius.to_px(node); + return Gfx::FloatSize { radius, radius }; + }, + [&](EllipseSize const& ellipse_size) { + auto radius_a = ellipse_size.radius_a.resolved(node, CSS::Length::make_px(size.width())).to_px(node); + auto radius_b = ellipse_size.radius_b.resolved(node, CSS::Length::make_px(size.height())).to_px(node); + return Gfx::FloatSize { radius_a, radius_b }; + }); +} + +void RadialGradientStyleValue::resolve_for_size(Layout::Node const&, Gfx::FloatSize const&) const +{ +} + +bool RadialGradientStyleValue::equals(StyleValue const& other) const +{ + if (type() != other.type()) + return false; + auto& other_gradient = other.as_radial_gradient(); + return (m_ending_shape == other_gradient.m_ending_shape + && variant_equals(m_size, other_gradient.m_size) + && m_position == other_gradient.m_position + && m_color_stop_list == other_gradient.m_color_stop_list); +} + +void RadialGradientStyleValue::paint(PaintContext&, Gfx::IntRect const&, CSS::ImageRendering) const +{ +} + String ConicGradientStyleValue::to_string() const { StringBuilder builder; diff --git a/Userland/Libraries/LibWeb/CSS/StyleValue.h b/Userland/Libraries/LibWeb/CSS/StyleValue.h index 0f5aaae9b4..00b45aaa0c 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleValue.h +++ b/Userland/Libraries/LibWeb/CSS/StyleValue.h @@ -241,6 +241,7 @@ public: Overflow, Percentage, Position, + RadialGradient, Rect, Resolution, Shadow, @@ -255,7 +256,7 @@ public: Type type() const { return m_type; } - bool is_abstract_image() const { return AK::first_is_one_of(type(), Type::Image, Type::LinearGradient, Type::ConicGradient); } + bool is_abstract_image() const { return AK::first_is_one_of(type(), Type::Image, Type::LinearGradient, Type::ConicGradient, Type::RadialGradient); } bool is_angle() const { return type() == Type::Angle; } bool is_background() const { return type() == Type::Background; } bool is_background_repeat() const { return type() == Type::BackgroundRepeat; } @@ -286,6 +287,7 @@ public: bool is_overflow() const { return type() == Type::Overflow; } bool is_percentage() const { return type() == Type::Percentage; } bool is_position() const { return type() == Type::Position; } + bool is_radial_gradient() const { return type() == Type::RadialGradient; } bool is_rect() const { return type() == Type::Rect; } bool is_resolution() const { return type() == Type::Resolution; } bool is_shadow() const { return type() == Type::Shadow; } @@ -330,6 +332,7 @@ public: OverflowStyleValue const& as_overflow() const; PercentageStyleValue const& as_percentage() const; PositionStyleValue const& as_position() const; + RadialGradientStyleValue const& as_radial_gradient() const; RectStyleValue const& as_rect() const; ResolutionStyleValue const& as_resolution() const; ShadowStyleValue const& as_shadow() const; @@ -372,6 +375,7 @@ public: OverflowStyleValue& as_overflow() { return const_cast(const_cast(*this).as_overflow()); } PercentageStyleValue& as_percentage() { return const_cast(const_cast(*this).as_percentage()); } PositionStyleValue& as_position() { return const_cast(const_cast(*this).as_position()); } + RadialGradientStyleValue& as_radial_gradient() { return const_cast(const_cast(*this).as_radial_gradient()); } RectStyleValue& as_rect() { return const_cast(const_cast(*this).as_rect()); } ResolutionStyleValue& as_resolution() { return const_cast(const_cast(*this).as_resolution()); } ShadowStyleValue& as_shadow() { return const_cast(const_cast(*this).as_shadow()); } @@ -1201,6 +1205,74 @@ enum class GradientRepeating { No }; +class RadialGradientStyleValue final : public AbstractImageStyleValue { +public: + enum class EndingShape { + Circle, + Ellipse + }; + + enum class Extent { + ClosestCorner, + ClosestSide, + FarthestCorner, + FarthestSide + }; + + struct CircleSize { + Length radius; + bool operator==(CircleSize const&) const = default; + }; + + struct EllipseSize { + LengthPercentage radius_a; + LengthPercentage radius_b; + bool operator==(EllipseSize const&) const = default; + }; + + using Size = Variant; + + static NonnullRefPtr create(EndingShape ending_shape, Size size, PositionValue position, Vector color_stop_list) + { + VERIFY(color_stop_list.size() >= 2); + return adopt_ref(*new RadialGradientStyleValue(ending_shape, size, position, move(color_stop_list))); + } + + virtual String to_string() const override; + + void paint(PaintContext&, Gfx::IntRect const& dest_rect, CSS::ImageRendering) const override; + + virtual bool equals(StyleValue const& other) const override; + + Vector const& color_stop_list() const + { + return m_color_stop_list; + } + + bool is_paintable() const override { return true; } + + void resolve_for_size(Layout::Node const&, Gfx::FloatSize const&) const override; + + Gfx::FloatSize resolve_size(Layout::Node const&, Gfx::FloatPoint, Gfx::FloatRect const&) const; + + virtual ~RadialGradientStyleValue() override = default; + +private: + RadialGradientStyleValue(EndingShape ending_shape, Size size, PositionValue position, Vector color_stop_list) + : AbstractImageStyleValue(Type::RadialGradient) + , m_ending_shape(ending_shape) + , m_size(size) + , m_position(position) + , m_color_stop_list(move(color_stop_list)) + { + } + + EndingShape m_ending_shape; + Size m_size; + PositionValue m_position; + Vector m_color_stop_list; +}; + class ConicGradientStyleValue final : public AbstractImageStyleValue { public: static NonnullRefPtr create(Angle from_angle, PositionValue position, Vector color_stop_list, GradientRepeating repeating) diff --git a/Userland/Libraries/LibWeb/Forward.h b/Userland/Libraries/LibWeb/Forward.h index b296efc7f6..7be9b1f836 100644 --- a/Userland/Libraries/LibWeb/Forward.h +++ b/Userland/Libraries/LibWeb/Forward.h @@ -90,6 +90,7 @@ class Percentage; class PercentageStyleValue; class PositionStyleValue; class PropertyOwningCSSStyleDeclaration; +class RadialGradientStyleValue; class RectStyleValue; class Resolution; class ResolutionStyleValue;