slint/api/cpp/include/slint_brush.h
Tasuku Suzuki f24ad34a03
Some checks are pending
autofix.ci / format_fix (push) Waiting to run
autofix.ci / lint_typecheck (push) Waiting to run
autofix.ci / ci (push) Blocked by required conditions
Add support for CSS conic-gradient 'from <angle>' syntax (#9830)
* Add support for CSS conic-gradient 'from <angle>' syntax

Implement rotation support for conic gradients by adding the 'from <angle>'
syntax, which rotates the entire gradient by the specified angle.

- Add `from_angle` field to ConicGradient expression
- Parse 'from <angle>' syntax in compiler (defaults to 0deg when omitted)
- Normalize angles to 0-1 range (0.0 = 0°, 1.0 = 360°)
- Add `ConicGradientBrush::rotated_stops()` method that:
  * Applies rotation by adding from_angle to each stop position
  * Adds boundary stops at 0.0 and 1.0 with interpolated colors
  * Handles stops outside [0, 1] range for boundary interpolation
- Update all renderers (Skia, FemtoVG, Qt, Software) to use rotated_stops()

The rotation is applied at render time by the rotated_stops() method,
which ensures all renderers consistently handle the gradient rotation.

* Add screenshot to rotated conic gradient docs example

Wraps the rotated conic gradient example in CodeSnippetMD to automatically
generate and display a visual screenshot of the gradient rotation effect.
This makes it easier for users to understand how the 'from' parameter rotates
the gradient.

* Make ConicGradientBrush fields private

The from_angle and stops fields don't need to be pub since:
- Rust code in the same module can access them without pub
- C++ FFI access works through cbindgen-generated struct (C++ struct members are public by default)

* Optimize ConicGradientBrush::rotated_stops to avoid allocations

- Changed return type from Vec to SharedVector
- When from_angle is zero, returns a clone of internal SharedVector
  (only increments reference count instead of allocating new Vec)
- Removed break from duplicate position separation loop to handle
  all duplicate pairs, not just the first one
- Updated documentation to match actual implementation

* Remove automatic sorting in ConicGradientBrush::new() to match CSS spec

- CSS conic-gradient does not automatically sort color stops
- Stops are processed in the order specified by the user
- Changed boundary stop interpolation logic to use max_by/min_by
  instead of relying on sorted order
- This allows CSS-style hard transitions when stops are out of order

* Move conic gradient rotation processing to construction time

Major changes:
- ConicGradientBrush::new() now applies rotation and boundary stop
  processing immediately, instead of deferring to rotated_stops()
- Removed rotated_stops() method - backends now use stops() directly
- Changed to LinearGradientBrush pattern: store angle in first dummy stop
- Added angle() method to retrieve the stored angle
- Maintained #[repr(transparent)] by removing from_angle field
- All backends updated: rotated_stops() -> stops()
  - Qt backend
  - Skia renderer
  - femtovg renderer
  - Software renderer

C++ API changes:
- Added FFI function slint_conic_gradient_new() for C++ to call Rust's new()
- Updated make_conic_gradient() to call FFI function instead of manually
  constructing SharedVector
- Ensures C++-created gradients get full rotation processing

Benefits:
- Eliminates per-frame rotation calculations
- Reduces memory usage (no from_angle field)
- Consistent with LinearGradientBrush design
- C++ and Rust APIs now produce identical results

* Change ConicGradientBrush::new() from_angle parameter to use degrees

- Changed from_angle parameter from normalized form (0.0-1.0) to degrees
- Matches LinearGradientBrush API convention (angle in degrees)
- Updated internal conversion: from_angle / 360.0 for normalization
- Stores angle as-is in degrees in the first dummy stop
- FFI function slint_conic_gradient_new() passes degrees directly

Example usage:
  ConicGradientBrush::new(90.0, stops)  // 90 degrees
  LinearGradientBrush::new(90.0, stops) // 90 degrees (consistent)

* Fix ConicGradient color transformation methods to preserve angle

Changed brighter(), darker(), transparentize(), and with_alpha() methods
to clone and modify the gradient in-place instead of calling new().

- Clones the existing gradient (preserves angle and rotation)
- Modifies only color stops (skips first stop which contains angle)
- Avoids re-running expensive rotation processing
- Maintains the original angle information

Before: ConicGradientBrush::new(0.0, ...) // Lost angle information
After:  Clone + modify colors in-place     // Preserves angle

* Use premultiplied alpha interpolation for conic gradient colors

- Changed interpolate_color() to use premultiplied RGBA interpolation
- Updated signature to match Color::mix convention (&Color, factor)
- Added documentation explaining why we can't use Color::mix() here
  (Sass algorithm vs CSS gradient color interpolation)
- Reference: https://www.w3.org/TR/css-images-4/#color-interpolation

This ensures correct visual interpolation of semi-transparent colors
in gradient boundaries, following CSS gradient specification.

* Run rustfmt on conic gradient code

* Fix ConicGradientBrush edge cases and add comprehensive tests

- Handle stops that are all below 0.0 or all above 1.0
- Add default transparent gradient when no valid stops remain
- Add 7 unit tests covering basic functionality and edge cases

* Apply clippy suggestion: use retain() instead of filter().collect()

* Fix radial-gradient parsing to allow empty gradients

Allow @radial-gradient(circle) without color stops, fixing syntax test
regression from commit 820ae2b04.

The previous logic required a comma after 'circle', but it should only
error if there's something that is NOT a comma.

* Fix conic-gradient syntax test error markers

Update error markers to match actual compiler error positions.
The 'from 2' case produces two errors:
- One at the @conic-gradient expression level
- One at the literal '2' position

Auto-updated using SLINT_SYNTAX_TEST_UPDATE=1.

* Refactor ConicGradientBrush epsilon adjustment and update tests

- Move epsilon adjustment for first stop into rotation block
  (only needed when rotation is applied)
- Update property_view test to reflect boundary stops added by
  ConicGradientBrush::new()

* Update conic-gradient screenshot reference image

Update the reference screenshot to match the current rendering output.
The small pixel differences (1% different pixels, max color diff 3.46)
are due to minor rounding differences in the conic gradient implementation.

* Fix ConicGradientBrush C++ FFI to avoid C-linkage return type error

Refactored ConicGradientBrush construction to match LinearGradientBrush
pattern, fixing macOS Clang error about returning C++ types from extern "C"
functions.

Changes:
- Rust: Split ConicGradientBrush::new into simple construction + separate
  normalize_stops() and apply_rotation() methods
- Rust: Added FFI functions slint_conic_gradient_normalize_stops() and
  slint_conic_gradient_apply_rotation() that take pointers (no return value)
- C++: Construct SharedVector directly in make_conic_gradient(), then call
  Rust functions via pointer (matching LinearGradientBrush pattern)
- Optimized both methods to only copy when changes are needed

This resolves the macOS Clang error:
"'slint_conic_gradient_new' has C-linkage specified, but returns incomplete
type 'ConicGradientBrush' which could be incompatible with C"

The new approach maintains ABI compatibility while keeping complex gradient
processing logic in Rust.

* Fix C++ header generation to avoid GradientStop redefinition error

Resolves the macOS CI compilation error where GradientStop and
ConicGradientBrush were being defined in multiple headers
(slint_color_internal.h, slint_image_internal.h, and slint_brush_internal.h).

Changes:
- cbindgen.rs: Add ConicGradientBrush and FFI functions to slint_brush_internal.h include list
- cbindgen.rs: Add GradientStop, ConicGradientBrush, and FFI functions to exclude list for other headers
- slint_color.h: Add forward declaration for ConicGradientBrush
- slint_color.h: Add friend declaration for ConicGradientBrush to allow access to Color::inner

Root cause: After adding extern "C" functions in graphics/brush.rs,
cbindgen automatically detects and tries to include them in all headers
that use graphics/brush.rs as a source. The exclude list + filter logic
ensures these types only appear in slint_brush_internal.h.

This fixes the C++ compilation errors:
- "redefinition of 'GradientStop'"
- "ConicGradientBrush does not name a type"
- "Color::inner is private within this context"

* Prepare ConicGradientBrush FFI for Rust 2024 edition

Update FFI functions to use the new `#[unsafe(no_mangle)]` attribute
syntax and safe function signatures in preparation for Rust 2024 edition.

- Add `#![allow(unsafe_code)]` to graphics module for `#[unsafe(no_mangle)]`
- Add `#[cfg(feature = "ffi")]` to conditionally compile FFI functions
- Change from raw pointers to safe references (&mut)
- Remove manual null checks and unsafe blocks
2025-11-13 16:05:16 +01:00

388 lines
15 KiB
C++

// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
#pragma once
#include <string_view>
#include "slint_color.h"
#include "slint_brush_internal.h"
#include "slint_string.h"
namespace slint {
namespace private_api {
using cbindgen_private::types::GradientStop;
/// \private
/// LinearGradientBrush represents a gradient for a brush that is a linear sequence of color stops,
/// that are aligned at a specific angle.
class LinearGradientBrush
{
public:
/// Constructs an empty linear gradient with no color stops.
LinearGradientBrush() = default;
/// Constructs a new linear gradient with the specified \a angle. The color stops will be
/// constructed from the stops array pointed to be \a firstStop, with the length \a stopCount.
LinearGradientBrush(float angle, const GradientStop *firstStop, int stopCount)
: inner(make_linear_gradient(angle, firstStop, stopCount))
{
}
/// Returns the linear gradient's angle in degrees.
float angle() const
{
// The gradient's first stop is a fake stop to store the angle
return inner[0].position;
}
/// Returns the number of gradient stops.
int stopCount() const { return int(inner.size()) - 1; }
/// Returns a pointer to the first gradient stop; undefined if the gradient has not stops.
const GradientStop *stopsBegin() const { return inner.begin() + 1; }
/// Returns a pointer past the last gradient stop. The returned pointer cannot be dereferenced,
/// it can only be used for comparison.
const GradientStop *stopsEnd() const { return inner.end(); }
private:
cbindgen_private::types::LinearGradientBrush inner;
friend class slint::Brush;
static SharedVector<private_api::GradientStop>
make_linear_gradient(float angle, const GradientStop *firstStop, int stopCount)
{
SharedVector<private_api::GradientStop> gradient;
gradient.push_back({ Color::from_argb_encoded(0).inner, angle });
for (int i = 0; i < stopCount; ++i, ++firstStop)
gradient.push_back(*firstStop);
return gradient;
}
};
/// \private
/// RadialGradientBrush represents a circular gradient centered in the middle
class RadialGradientBrush
{
public:
/// Constructs an empty linear gradient with no color stops.
RadialGradientBrush() = default;
/// Constructs a new circular radial gradient . The color stops will be
/// constructed from the stops array pointed to be \a firstStop, with the length \a stopCount.
RadialGradientBrush(const GradientStop *firstStop, int stopCount)
: inner(make_circle_gradient(firstStop, stopCount))
{
}
/// Returns the number of gradient stops.
int stopCount() const { return int(inner.size()); }
/// Returns a pointer to the first gradient stop; undefined if the gradient has not stops.
const GradientStop *stopsBegin() const { return inner.begin(); }
/// Returns a pointer past the last gradient stop. The returned pointer cannot be dereferenced,
/// it can only be used for comparison.
const GradientStop *stopsEnd() const { return inner.end(); }
private:
cbindgen_private::types::RadialGradientBrush inner;
friend class slint::Brush;
static SharedVector<private_api::GradientStop>
make_circle_gradient(const GradientStop *firstStop, int stopCount)
{
SharedVector<private_api::GradientStop> gradient;
for (int i = 0; i < stopCount; ++i, ++firstStop)
gradient.push_back(*firstStop);
return gradient;
}
};
/// \private
/// ConicGradientBrush represents a conic gradient that rotates around a center point
class ConicGradientBrush
{
public:
/// Constructs an empty conic gradient with no color stops.
ConicGradientBrush() = default;
/// Constructs a new conic gradient with the specified starting \a angle. The color stops will
/// be constructed from the stops array pointed to be \a firstStop, with the length \a
/// stopCount.
ConicGradientBrush(float angle, const GradientStop *firstStop, int stopCount)
: inner(make_conic_gradient(angle, firstStop, stopCount))
{
}
/// Returns the conic gradient's starting angle (rotation) in degrees.
float angle() const { return inner[0].position; }
/// Returns the number of gradient stops.
int stopCount() const { return int(inner.size()) - 1; }
/// Returns a pointer to the first gradient stop; undefined if the gradient has not stops.
const GradientStop *stopsBegin() const { return inner.begin() + 1; }
/// Returns a pointer past the last gradient stop. The returned pointer cannot be dereferenced,
/// it can only be used for comparison.
const GradientStop *stopsEnd() const { return inner.end(); }
private:
cbindgen_private::types::ConicGradientBrush inner;
friend class slint::Brush;
static SharedVector<private_api::GradientStop>
make_conic_gradient(float angle, const GradientStop *firstStop, int stopCount)
{
SharedVector<private_api::GradientStop> gradient;
// The gradient's first stop is a fake stop to store the angle (same pattern as
// LinearGradient)
gradient.push_back({ Color::from_argb_encoded(0).inner, angle });
for (int i = 0; i < stopCount; ++i, ++firstStop)
gradient.push_back(*firstStop);
// Normalize stops to [0, 1] range with proper boundary stops
cbindgen_private::types::slint_conic_gradient_normalize_stops(&gradient);
// Apply rotation if angle is non-zero
if (angle != 0.0f) {
cbindgen_private::types::slint_conic_gradient_apply_rotation(&gradient, angle);
}
return gradient;
}
};
}
/// Brush is used to declare how to fill or outline shapes, such as rectangles, paths or text. A
/// brush is either a solid color or a linear gradient.
class Brush
{
public:
/// Constructs a new brush that is a transparent color.
Brush() : Brush(Color {}) { }
/// Constructs a new brush that is of color \a color.
Brush(const Color &color) : data(Inner::SolidColor(color.inner)) { }
/// \private
/// Constructs a new brush that is the gradient \a gradient.
Brush(const private_api::LinearGradientBrush &gradient)
: data(Inner::LinearGradient(gradient.inner))
{
}
/// \private
/// Constructs a new brush that is the gradient \a gradient.
Brush(const private_api::RadialGradientBrush &gradient)
: data(Inner::RadialGradient(gradient.inner))
{
}
/// \private
/// Constructs a new brush that is the gradient \a gradient.
Brush(const private_api::ConicGradientBrush &gradient)
: data(Inner::ConicGradient(gradient.inner))
{
}
/// Returns the color of the brush. If the brush is a gradient, this function returns the color
/// of the first stop.
inline Color color() const;
/// Returns a new version of this brush that has the brightness increased
/// by the specified factor. This is done by calling Color::brighter on
/// all the colors of this brush.
[[nodiscard]] inline Brush brighter(float factor) const;
/// Returns a new version of this color that has the brightness decreased
/// by the specified factor. This is done by calling Color::darker on
/// all the colors of this brush.
[[nodiscard]] inline Brush darker(float factor) const;
/// Returns a new version of this brush with the opacity decreased by \a factor.
///
/// This is done by calling Color::transparentize on all the colors of this brush.
[[nodiscard]] inline Brush transparentize(float factor) const;
/// Returns a new version of this brush with the related color's opacities
/// set to \a alpha.
[[nodiscard]] inline Brush with_alpha(float alpha) const;
/// Returns true if \a a is equal to \a b. If \a a holds a color, then \a b must also hold a
/// color that is identical to \a a's color. If it holds a gradient, then the gradients must be
/// identical. Returns false if the brushes differ in what they hold or their respective color
/// or gradient are not equal.
friend bool operator==(const Brush &a, const Brush &b) { return a.data == b.data; }
/// Returns false if \a is not equal to \a b; true otherwise.
friend bool operator!=(const Brush &a, const Brush &b) { return a.data != b.data; }
private:
using Tag = cbindgen_private::types::Brush::Tag;
using Inner = cbindgen_private::types::Brush;
Inner data;
friend struct private_api::Property<Brush>;
};
Color Brush::color() const
{
Color result;
switch (data.tag) {
case Tag::SolidColor:
result.inner = data.solid_color._0;
break;
case Tag::LinearGradient:
if (data.linear_gradient._0.size() > 1) {
result.inner = data.linear_gradient._0[1].color;
}
break;
case Tag::RadialGradient:
if (data.radial_gradient._0.size() > 0) {
result.inner = data.radial_gradient._0[0].color;
}
break;
case Tag::ConicGradient:
if (data.conic_gradient._0.size() > 1) {
result.inner = data.conic_gradient._0[1].color;
}
break;
}
return result;
}
inline Brush Brush::brighter(float factor) const
{
Brush result = *this;
switch (data.tag) {
case Tag::SolidColor:
cbindgen_private::types::slint_color_brighter(&data.solid_color._0, factor,
&result.data.solid_color._0);
break;
case Tag::LinearGradient:
for (std::size_t i = 1; i < data.linear_gradient._0.size(); ++i) {
cbindgen_private::types::slint_color_brighter(&data.linear_gradient._0[i].color, factor,
&result.data.linear_gradient._0[i].color);
}
break;
case Tag::RadialGradient:
for (std::size_t i = 0; i < data.radial_gradient._0.size(); ++i) {
cbindgen_private::types::slint_color_brighter(&data.radial_gradient._0[i].color, factor,
&result.data.radial_gradient._0[i].color);
}
break;
case Tag::ConicGradient:
for (std::size_t i = 1; i < data.conic_gradient._0.size(); ++i) {
cbindgen_private::types::slint_color_brighter(&data.conic_gradient._0[i].color, factor,
&result.data.conic_gradient._0[i].color);
}
break;
}
return result;
}
inline Brush Brush::darker(float factor) const
{
Brush result = *this;
switch (data.tag) {
case Tag::SolidColor:
cbindgen_private::types::slint_color_darker(&data.solid_color._0, factor,
&result.data.solid_color._0);
break;
case Tag::LinearGradient:
for (std::size_t i = 1; i < data.linear_gradient._0.size(); ++i) {
cbindgen_private::types::slint_color_darker(&data.linear_gradient._0[i].color, factor,
&result.data.linear_gradient._0[i].color);
}
break;
case Tag::RadialGradient:
for (std::size_t i = 0; i < data.radial_gradient._0.size(); ++i) {
cbindgen_private::types::slint_color_darker(&data.radial_gradient._0[i].color, factor,
&result.data.radial_gradient._0[i].color);
}
break;
case Tag::ConicGradient:
for (std::size_t i = 1; i < data.conic_gradient._0.size(); ++i) {
cbindgen_private::types::slint_color_darker(&data.conic_gradient._0[i].color, factor,
&result.data.conic_gradient._0[i].color);
}
break;
}
return result;
}
inline Brush Brush::transparentize(float factor) const
{
Brush result = *this;
switch (data.tag) {
case Tag::SolidColor:
cbindgen_private::types::slint_color_transparentize(&data.solid_color._0, factor,
&result.data.solid_color._0);
break;
case Tag::LinearGradient:
for (std::size_t i = 1; i < data.linear_gradient._0.size(); ++i) {
cbindgen_private::types::slint_color_transparentize(
&data.linear_gradient._0[i].color, factor,
&result.data.linear_gradient._0[i].color);
}
break;
case Tag::RadialGradient:
for (std::size_t i = 0; i < data.radial_gradient._0.size(); ++i) {
cbindgen_private::types::slint_color_transparentize(
&data.radial_gradient._0[i].color, factor,
&result.data.radial_gradient._0[i].color);
}
break;
case Tag::ConicGradient:
for (std::size_t i = 1; i < data.conic_gradient._0.size(); ++i) {
cbindgen_private::types::slint_color_transparentize(
&data.conic_gradient._0[i].color, factor,
&result.data.conic_gradient._0[i].color);
}
break;
}
return result;
}
inline Brush Brush::with_alpha(float alpha) const
{
Brush result = *this;
switch (data.tag) {
case Tag::SolidColor:
cbindgen_private::types::slint_color_with_alpha(&data.solid_color._0, alpha,
&result.data.solid_color._0);
break;
case Tag::LinearGradient:
for (std::size_t i = 1; i < data.linear_gradient._0.size(); ++i) {
cbindgen_private::types::slint_color_with_alpha(
&data.linear_gradient._0[i].color, alpha,
&result.data.linear_gradient._0[i].color);
}
break;
case Tag::RadialGradient:
for (std::size_t i = 0; i < data.radial_gradient._0.size(); ++i) {
cbindgen_private::types::slint_color_with_alpha(
&data.radial_gradient._0[i].color, alpha,
&result.data.radial_gradient._0[i].color);
}
break;
case Tag::ConicGradient:
for (std::size_t i = 1; i < data.conic_gradient._0.size(); ++i) {
cbindgen_private::types::slint_color_with_alpha(
&data.conic_gradient._0[i].color, alpha,
&result.data.conic_gradient._0[i].color);
}
break;
}
return result;
}
namespace private_api {
template<>
inline void Property<slint::Brush>::set_animated_value(
const slint::Brush &new_value,
const cbindgen_private::PropertyAnimation &animation_data) const
{
cbindgen_private::slint_property_set_animated_value_brush(&inner, &value, &new_value,
&animation_data);
}
} // namespace private_api
} // namespace slint