Skip to content

Conversation

@task-jp
Copy link
Contributor

@task-jp task-jp commented Oct 23, 2025

Summary

Implements rotation support for conic gradients by adding the CSS-standard from <angle> syntax, which rotates the entire gradient by the specified angle.

Syntax

@conic-gradient(from 90deg, red 0deg, blue 180deg, red 360deg)

Copy link
Member

@ogoffart ogoffart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the motivation behind this.

It looks like this makes Slint different from what the CSS spec says.

@task-jp
Copy link
Contributor Author

task-jp commented Oct 24, 2025

Thanks for your review.

What's the motivation behind this.

In https://snapshots.slint.dev/master/editor/index.html?load_url=https://raw.githubusercontent.com/slint-ui/slint/master/examples/speedometer/demo.slint, CarButton tries to rotate angles in @conic-gradients when it is pressed. To solve this, I thought it would be good to support any degrees.

this makes Slint different from what the CSS spec says.

Right. should we introduce the [ from [ <angle> | <zero> ] ]? for such usecase?

@ogoffart
Copy link
Member

I see. I can achieve the desired effect with

@conic-gradient( #020414 stop4 - 1turn, #ff0000 stop5 - 1turn, #000000 stop6 - 1turn, #020414 stop1, #ff0000 stop2, #020414 stop3, #020414 stop4, #ff0000 stop5, #000000 stop6);

I added stop4 to stop6 back at the beginning with a - 1turn
So when some value are more than 360deg, they don't wrap around, but the value before that wraps around.

I would prefer keeping close to the CSS spec for gradients.

@task-jp task-jp force-pushed the conic-gradients-rotation branch from 1a156d1 to 578faaf Compare October 30, 2025 09:43
@task-jp task-jp changed the title Support arbitrary angle ranges in conic-gradient Add support for CSS conic-gradient 'from <angle>' syntax Oct 30, 2025
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.
@task-jp task-jp force-pushed the conic-gradients-rotation branch from 5a02287 to 820ae2b Compare October 30, 2025 09:58
Copy link
Member

@ogoffart ogoffart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! thank you very much.

This looks quite good to me.

I'm just wondering what you think about the idea to normalize the gradient brush at construction time instead of every time it is used.

///
/// This is useful when you need to work with the actual visual positions of the stops
/// after the gradient has been rotated.
pub fn rotated_stops(&self) -> alloc::vec::Vec<GradientStop> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we be storing them like this instead of re-allocating a new vector every time we want to draw?

Copy link
Contributor Author

@task-jp task-jp Oct 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When from is not set, we can use the vector prepared in new(). In case of with from, we can pass the vector to backends as it is. However, (all?) backend that doesn't support angles < 0 and > 1 needs to do the conversion.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But why don't we compute that in new already since new already knows from?

}

/// Helper: Linearly interpolate between two colors
fn interpolate_color(c1: Color, c2: Color, t: f32) -> Color {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this different from Color::mix ?

Copy link
Contributor Author

@task-jp task-jp Oct 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CSS gradient spec uses simple linear interpolation between color stops. The gradient line’s color is interpolated between the colors of the two color stops, with the interpolation taking place in premultiplied RGBA space. premultiplied is not implemented in the interpolate_color yet.
https://www.w3.org/TR/css-images-3/#coloring-gradient-line

Color::mix implements the Sass color mixing algorithm, which applies alpha-weighted blending.
https://github.com/sass/sass/blob/47d30713765b975c86fa32ec359ed16e83ad1ecc/spec/built-in-modules/color.md#mix

Without alpha, no difference. with alpha, there is difference. Do you think Color should support two mix methods?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code of Color::mix is maybe a bit hard to read. But I think the reason is that when we go from, say #ffff to #f001, this should go from white to slightly red, but it shouldn't show the red too much.

This example illustrate the difference: slintpad

Now I must say that i haven't compared the functions implementation much, but your implementation here is not in premultiplied space.
But if you say that Color::mix doesn't work for this, so be it.

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.
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)
- 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
- 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
@task-jp task-jp force-pushed the conic-gradients-rotation branch from 2a5b848 to daabee4 Compare October 31, 2025 01:35
Comment on lines +274 to +276
/// The `from_angle` parameter is in normalized form (0.0 = 0°, 1.0 = 360°), corresponding
/// to CSS's `from <angle>` syntax. It rotates the entire gradient clockwise.
pub fn new(from_angle: f32, stops: impl IntoIterator<Item = GradientStop>) -> Self {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency with LinearGradientBrush::new, i think the angle should be specified in degrees.
In Slint, angles are always degrees.

}
Brush::ConicGradient(g) => {
Brush::ConicGradient(ConicGradientBrush::new(g.stops().map(|s| GradientStop {
Brush::ConicGradient(g) => Brush::ConicGradient(ConicGradientBrush::new(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this touches only the colors, and not the stops, this could just map the colors without having to re-normalize thing.
This doesn't have to be done in this PR, but we could imagine a (internal) Brush::visit_colors(&mut self, map: impl Fn(&mut Color)) and so one could implement brighter, darker, with_alpha and transparentize using it. But anyway, this can be done independently.

}

/// Helper: Linearly interpolate between two colors
fn interpolate_color(c1: Color, c2: Color, t: f32) -> Color {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code of Color::mix is maybe a bit hard to read. But I think the reason is that when we go from, say #ffff to #f001, this should go from white to slightly red, but it shouldn't show the red too much.

This example illustrate the difference: slintpad

Now I must say that i haven't compared the functions implementation much, but your implementation here is not in premultiplied space.
But if you say that Color::mix doesn't work for this, so be it.

///
/// This is useful when you need to work with the actual visual positions of the stops
/// after the gradient has been rotated.
pub fn rotated_stops(&self) -> alloc::vec::Vec<GradientStop> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But why don't we compute that in new already since new already knows from?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants