mirror of
https://github.com/slint-ui/slint.git
synced 2025-12-23 09:19:32 +00:00
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.
This commit is contained in:
parent
9f4b3cd778
commit
820ae2b041
22 changed files with 411 additions and 137 deletions
|
|
@ -105,33 +105,37 @@ class ConicGradientBrush
|
|||
public:
|
||||
/// Constructs an empty conic gradient with no color stops.
|
||||
ConicGradientBrush() = default;
|
||||
/// Constructs a new conic gradient. The color stops will be
|
||||
/// 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(const GradientStop *firstStop, int stopCount)
|
||||
: inner(make_conic_gradient(firstStop, 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.from_angle; }
|
||||
|
||||
/// Returns the number of gradient stops.
|
||||
int stopCount() const { return int(inner.size()); }
|
||||
int stopCount() const { return int(inner.stops.size()); }
|
||||
|
||||
/// Returns a pointer to the first gradient stop; undefined if the gradient has not stops.
|
||||
const GradientStop *stopsBegin() const { return inner.begin(); }
|
||||
const GradientStop *stopsBegin() const { return inner.stops.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(); }
|
||||
const GradientStop *stopsEnd() const { return inner.stops.end(); }
|
||||
|
||||
private:
|
||||
cbindgen_private::types::ConicGradientBrush inner;
|
||||
|
||||
friend class slint::Brush;
|
||||
|
||||
static SharedVector<private_api::GradientStop>
|
||||
make_conic_gradient(const GradientStop *firstStop, int stopCount)
|
||||
static cbindgen_private::types::ConicGradientBrush
|
||||
make_conic_gradient(float angle, const GradientStop *firstStop, int stopCount)
|
||||
{
|
||||
SharedVector<private_api::GradientStop> gradient;
|
||||
cbindgen_private::types::ConicGradientBrush gradient;
|
||||
gradient.from_angle = angle;
|
||||
for (int i = 0; i < stopCount; ++i, ++firstStop)
|
||||
gradient.push_back(*firstStop);
|
||||
gradient.stops.push_back(*firstStop);
|
||||
return gradient;
|
||||
}
|
||||
};
|
||||
|
|
@ -223,8 +227,8 @@ Color Brush::color() const
|
|||
}
|
||||
break;
|
||||
case Tag::ConicGradient:
|
||||
if (data.conic_gradient._0.size() > 0) {
|
||||
result.inner = data.conic_gradient._0[0].color;
|
||||
if (data.conic_gradient._0.stops.size() > 0) {
|
||||
result.inner = data.conic_gradient._0.stops[0].color;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -252,9 +256,9 @@ inline Brush Brush::brighter(float factor) const
|
|||
}
|
||||
break;
|
||||
case Tag::ConicGradient:
|
||||
for (std::size_t i = 0; 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);
|
||||
for (std::size_t i = 0; i < data.conic_gradient._0.stops.size(); ++i) {
|
||||
cbindgen_private::types::slint_color_brighter(&data.conic_gradient._0.stops[i].color, factor,
|
||||
&result.data.conic_gradient._0.stops[i].color);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -282,9 +286,9 @@ inline Brush Brush::darker(float factor) const
|
|||
}
|
||||
break;
|
||||
case Tag::ConicGradient:
|
||||
for (std::size_t i = 0; 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);
|
||||
for (std::size_t i = 0; i < data.conic_gradient._0.stops.size(); ++i) {
|
||||
cbindgen_private::types::slint_color_darker(&data.conic_gradient._0.stops[i].color, factor,
|
||||
&result.data.conic_gradient._0.stops[i].color);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -314,10 +318,10 @@ inline Brush Brush::transparentize(float factor) const
|
|||
}
|
||||
break;
|
||||
case Tag::ConicGradient:
|
||||
for (std::size_t i = 0; i < data.conic_gradient._0.size(); ++i) {
|
||||
for (std::size_t i = 0; i < data.conic_gradient._0.stops.size(); ++i) {
|
||||
cbindgen_private::types::slint_color_transparentize(
|
||||
&data.conic_gradient._0[i].color, factor,
|
||||
&result.data.conic_gradient._0[i].color);
|
||||
&data.conic_gradient._0.stops[i].color, factor,
|
||||
&result.data.conic_gradient._0.stops[i].color);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -347,10 +351,10 @@ inline Brush Brush::with_alpha(float alpha) const
|
|||
}
|
||||
break;
|
||||
case Tag::ConicGradient:
|
||||
for (std::size_t i = 0; i < data.conic_gradient._0.size(); ++i) {
|
||||
for (std::size_t i = 0; i < data.conic_gradient._0.stops.size(); ++i) {
|
||||
cbindgen_private::types::slint_color_with_alpha(
|
||||
&data.conic_gradient._0[i].color, alpha,
|
||||
&result.data.conic_gradient._0[i].color);
|
||||
&data.conic_gradient._0.stops[i].color, alpha,
|
||||
&result.data.conic_gradient._0.stops[i].color);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,12 +167,16 @@ export component Example inherits Window {
|
|||
Conic gradients are gradients where the color transitions rotate around a center point (like the angle on a color wheel).
|
||||
To describe a conic gradient, use the `@conic-gradient` macro with the following signature:
|
||||
|
||||
### @conic-gradient(color angle, color angle, ...)
|
||||
### @conic-gradient([from angle,] color angle, color angle, ...)
|
||||
|
||||
The conic gradient is described by a series of color stops, each consisting of a color and an angle.
|
||||
The angle specifies where the color is placed along the circular sweep (0deg to 360deg).
|
||||
Colors are interpolated between the stops along the circular path.
|
||||
|
||||
The optional `from` parameter specifies the starting angle of the gradient rotation.
|
||||
If omitted, the gradient starts at 0deg (pointing upward). For example, `from 90deg` rotates
|
||||
the entire gradient 90 degrees clockwise.
|
||||
|
||||
Example:
|
||||
|
||||
<CodeSnippetMD imagePath="/src/assets/generated/gradients-conic.png" scale="3" imageWidth="200" imageHeight="200" imageAlt='Conic Gradient Example'>
|
||||
|
|
@ -189,6 +193,20 @@ export component Example inherits Window {
|
|||
|
||||
This creates a color wheel effect with red at the top (0deg/360deg), green at 120 degrees, and blue at 240 degrees.
|
||||
|
||||
You can also rotate the gradient using the `from` parameter:
|
||||
|
||||
```slint
|
||||
export component Example inherits Window {
|
||||
preferred-width: 100px;
|
||||
preferred-height: 100px;
|
||||
Rectangle {
|
||||
background: @conic-gradient(from 90deg, #f00 0deg, #0f0 120deg, #00f 240deg, #f00 360deg);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This rotates the same color wheel 90 degrees clockwise, so red starts at the right (90deg) instead of the top.
|
||||
|
||||
:::note[Known Limitation]
|
||||
Negative angles cannot be used directly in conic gradients (e.g., `#ff0000 -90deg`).
|
||||
Instead, use one of these workarounds:
|
||||
|
|
|
|||
|
|
@ -565,8 +565,9 @@ fn into_qbrush(
|
|||
return qcg;
|
||||
}
|
||||
};
|
||||
let count = g.stops().count();
|
||||
for (idx, s) in g.stops().enumerate() {
|
||||
let rotated = g.rotated_stops();
|
||||
let count = rotated.len();
|
||||
for (idx, s) in rotated.iter().enumerate() {
|
||||
// Qt's conical gradient goes counter-clockwise, but Slint expects clockwise
|
||||
// So we need to invert the positions: Qt position = 1.0 - Slint position
|
||||
let pos: f32 = 1.0 - mangle_position(s.position, idx, count);
|
||||
|
|
|
|||
|
|
@ -733,6 +733,8 @@ pub enum Expression {
|
|||
},
|
||||
|
||||
ConicGradient {
|
||||
/// The starting angle (rotation) of the gradient, corresponding to CSS `from <angle>`
|
||||
from_angle: Box<Expression>,
|
||||
/// First expression in the tuple is a color, second expression is the stop angle
|
||||
stops: Vec<(Expression, Expression)>,
|
||||
},
|
||||
|
|
@ -968,7 +970,8 @@ impl Expression {
|
|||
visitor(s);
|
||||
}
|
||||
}
|
||||
Expression::ConicGradient { stops } => {
|
||||
Expression::ConicGradient { from_angle, stops } => {
|
||||
visitor(from_angle);
|
||||
for (c, s) in stops {
|
||||
visitor(c);
|
||||
visitor(s);
|
||||
|
|
@ -1071,7 +1074,8 @@ impl Expression {
|
|||
visitor(s);
|
||||
}
|
||||
}
|
||||
Expression::ConicGradient { stops } => {
|
||||
Expression::ConicGradient { from_angle, stops } => {
|
||||
visitor(from_angle);
|
||||
for (c, s) in stops {
|
||||
visitor(c);
|
||||
visitor(s);
|
||||
|
|
@ -1168,8 +1172,9 @@ impl Expression {
|
|||
Expression::RadialGradient { stops } => {
|
||||
stops.iter().all(|(c, s)| c.is_constant(ga) && s.is_constant(ga))
|
||||
}
|
||||
Expression::ConicGradient { stops } => {
|
||||
stops.iter().all(|(c, s)| c.is_constant(ga) && s.is_constant(ga))
|
||||
Expression::ConicGradient { from_angle, stops } => {
|
||||
from_angle.is_constant(ga)
|
||||
&& stops.iter().all(|(c, s)| c.is_constant(ga) && s.is_constant(ga))
|
||||
}
|
||||
Expression::EnumerationValue(_) => true,
|
||||
Expression::ReturnStatement(expr) => {
|
||||
|
|
@ -1792,14 +1797,11 @@ pub fn pretty_print(f: &mut dyn std::fmt::Write, expression: &Expression) -> std
|
|||
}
|
||||
write!(f, ")")
|
||||
}
|
||||
Expression::ConicGradient { stops } => {
|
||||
write!(f, "@conic-gradient(")?;
|
||||
let mut first = true;
|
||||
Expression::ConicGradient { from_angle, stops } => {
|
||||
write!(f, "@conic-gradient(from ")?;
|
||||
pretty_print(f, from_angle)?;
|
||||
for (c, s) in stops {
|
||||
if !first {
|
||||
write!(f, ", ")?;
|
||||
}
|
||||
first = false;
|
||||
write!(f, ", ")?;
|
||||
pretty_print(f, c)?;
|
||||
write!(f, " ")?;
|
||||
pretty_print(f, s)?;
|
||||
|
|
|
|||
|
|
@ -3464,7 +3464,7 @@ fn compile_expression(expr: &llr::Expression, ctx: &EvaluationContext) -> String
|
|||
stops_it.join(", "), angle, stops.len()
|
||||
)
|
||||
}
|
||||
Expression::RadialGradient{ stops} => {
|
||||
Expression::RadialGradient{ stops } => {
|
||||
let mut stops_it = stops.iter().map(|(color, stop)| {
|
||||
let color = compile_expression(color, ctx);
|
||||
let position = compile_expression(stop, ctx);
|
||||
|
|
@ -3475,15 +3475,16 @@ fn compile_expression(expr: &llr::Expression, ctx: &EvaluationContext) -> String
|
|||
stops_it.join(", "), stops.len()
|
||||
)
|
||||
}
|
||||
Expression::ConicGradient{ stops} => {
|
||||
Expression::ConicGradient{ from_angle, stops } => {
|
||||
let from_angle = compile_expression(from_angle, ctx);
|
||||
let mut stops_it = stops.iter().map(|(color, stop)| {
|
||||
let color = compile_expression(color, ctx);
|
||||
let position = compile_expression(stop, ctx);
|
||||
format!("slint::private_api::GradientStop{{ {color}, float({position}), }}")
|
||||
});
|
||||
format!(
|
||||
"[&] {{ const slint::private_api::GradientStop stops[] = {{ {} }}; return slint::Brush(slint::private_api::ConicGradientBrush(stops, {})); }}()",
|
||||
stops_it.join(", "), stops.len()
|
||||
"[&] {{ const slint::private_api::GradientStop stops[] = {{ {} }}; return slint::Brush(slint::private_api::ConicGradientBrush(float({}), stops, {})); }}()",
|
||||
stops_it.join(", "), from_angle, stops.len()
|
||||
)
|
||||
}
|
||||
Expression::EnumerationValue(value) => {
|
||||
|
|
|
|||
|
|
@ -2732,14 +2732,15 @@ fn compile_expression(expr: &Expression, ctx: &EvaluationContext) -> TokenStream
|
|||
sp::RadialGradientBrush::new_circle([#(#stops),*])
|
||||
))
|
||||
}
|
||||
Expression::ConicGradient { stops } => {
|
||||
Expression::ConicGradient { from_angle, stops } => {
|
||||
let from_angle = compile_expression(from_angle, ctx);
|
||||
let stops = stops.iter().map(|(color, stop)| {
|
||||
let color = compile_expression(color, ctx);
|
||||
let position = compile_expression(stop, ctx);
|
||||
quote!(sp::GradientStop{ color: #color, position: #position as _ })
|
||||
});
|
||||
quote!(slint::Brush::ConicGradient(
|
||||
sp::ConicGradientBrush::new([#(#stops),*])
|
||||
sp::ConicGradientBrush::new(#from_angle as _, [#(#stops),*])
|
||||
))
|
||||
}
|
||||
Expression::EnumerationValue(value) => {
|
||||
|
|
|
|||
|
|
@ -159,6 +159,8 @@ pub enum Expression {
|
|||
},
|
||||
|
||||
ConicGradient {
|
||||
/// The starting angle (rotation) of the gradient, corresponding to CSS `from <angle>`
|
||||
from_angle: Box<Expression>,
|
||||
/// First expression in the tuple is a color, second expression is the stop position (normalized angle 0-1)
|
||||
stops: Vec<(Expression, Expression)>,
|
||||
},
|
||||
|
|
@ -390,7 +392,8 @@ macro_rules! visit_impl {
|
|||
$visitor(b);
|
||||
}
|
||||
}
|
||||
Expression::ConicGradient { stops } => {
|
||||
Expression::ConicGradient { from_angle, stops } => {
|
||||
$visitor(from_angle);
|
||||
for (a, b) in stops {
|
||||
$visitor(a);
|
||||
$visitor(b);
|
||||
|
|
|
|||
|
|
@ -233,7 +233,8 @@ pub fn lower_expression(
|
|||
.map(|(a, b)| (lower_expression(a, ctx), lower_expression(b, ctx)))
|
||||
.collect::<_>(),
|
||||
},
|
||||
tree_Expression::ConicGradient { stops } => llr_Expression::ConicGradient {
|
||||
tree_Expression::ConicGradient { from_angle, stops } => llr_Expression::ConicGradient {
|
||||
from_angle: Box::new(lower_expression(from_angle, ctx)),
|
||||
stops: stops
|
||||
.iter()
|
||||
.map(|(a, b)| (lower_expression(a, ctx), lower_expression(b, ctx)))
|
||||
|
|
|
|||
|
|
@ -331,9 +331,10 @@ impl<'a, T> Display for DisplayExpression<'a, T> {
|
|||
"@radial-gradient(circle, {})",
|
||||
stops.iter().map(|(e1, e2)| format!("{} {}", e(e1), e(e2))).join(", ")
|
||||
),
|
||||
Expression::ConicGradient { stops } => write!(
|
||||
Expression::ConicGradient { from_angle, stops } => write!(
|
||||
f,
|
||||
"@conic-gradient({})",
|
||||
"@conic-gradient(from {}, {})",
|
||||
e(from_angle),
|
||||
stops.iter().map(|(e1, e2)| format!("{} {}", e(e1), e(e2))).join(", ")
|
||||
),
|
||||
Expression::EnumerationValue(x) => write!(f, "{x}"),
|
||||
|
|
|
|||
|
|
@ -511,27 +511,28 @@ impl Expression {
|
|||
enum GradKind {
|
||||
Linear { angle: Box<Expression> },
|
||||
Radial,
|
||||
Conic,
|
||||
Conic { from_angle: Box<Expression> },
|
||||
}
|
||||
|
||||
let mut subs = node
|
||||
let all_subs: Vec<_> = node
|
||||
.children_with_tokens()
|
||||
.filter(|n| matches!(n.kind(), SyntaxKind::Comma | SyntaxKind::Expression));
|
||||
.filter(|n| matches!(n.kind(), SyntaxKind::Comma | SyntaxKind::Expression))
|
||||
.collect();
|
||||
|
||||
let grad_token = node.child_token(SyntaxKind::Identifier).unwrap();
|
||||
let grad_text = grad_token.text();
|
||||
|
||||
let grad_kind = if grad_text.starts_with("linear") {
|
||||
let angle_expr = match subs.next() {
|
||||
let (grad_kind, stops_start_idx) = if grad_text.starts_with("linear") {
|
||||
let angle_expr = match all_subs.first() {
|
||||
Some(e) if e.kind() == SyntaxKind::Expression => {
|
||||
syntax_nodes::Expression::from(e.into_node().unwrap())
|
||||
syntax_nodes::Expression::from(e.as_node().unwrap().clone())
|
||||
}
|
||||
_ => {
|
||||
ctx.diag.push_error("Expected angle expression".into(), &node);
|
||||
return Expression::Invalid;
|
||||
}
|
||||
};
|
||||
if subs.next().is_some_and(|s| s.kind() != SyntaxKind::Comma) {
|
||||
if !all_subs.get(1).is_some_and(|s| s.kind() == SyntaxKind::Comma) {
|
||||
ctx.diag.push_error(
|
||||
"Angle expression must be an angle followed by a comma".into(),
|
||||
&node,
|
||||
|
|
@ -545,28 +546,65 @@ impl Expression {
|
|||
ctx.diag,
|
||||
),
|
||||
);
|
||||
GradKind::Linear { angle }
|
||||
(GradKind::Linear { angle }, 2)
|
||||
} else if grad_text.starts_with("radial") {
|
||||
if !matches!(subs.next(), Some(NodeOrToken::Node(n)) if n.text().to_string().trim() == "circle")
|
||||
{
|
||||
if !all_subs.first().is_some_and(|n| {
|
||||
matches!(n, NodeOrToken::Node(node) if node.text().to_string().trim() == "circle")
|
||||
}) {
|
||||
ctx.diag.push_error("Expected 'circle': currently, only @radial-gradient(circle, ...) are supported".into(), &node);
|
||||
return Expression::Invalid;
|
||||
}
|
||||
let comma = subs.next();
|
||||
let comma = all_subs.get(1);
|
||||
if matches!(&comma, Some(NodeOrToken::Node(n)) if n.text().to_string().trim() == "at") {
|
||||
ctx.diag.push_error("'at' in @radial-gradient is not yet supported".into(), &comma);
|
||||
return Expression::Invalid;
|
||||
}
|
||||
if comma.as_ref().is_some_and(|s| s.kind() != SyntaxKind::Comma) {
|
||||
ctx.diag.push_error(
|
||||
"'circle' must be followed by a comma".into(),
|
||||
comma.as_ref().map_or(&node, |x| x as &dyn Spanned),
|
||||
"'at' in @radial-gradient is not yet supported".into(),
|
||||
comma.unwrap(),
|
||||
);
|
||||
return Expression::Invalid;
|
||||
}
|
||||
GradKind::Radial
|
||||
if !comma.is_some_and(|s| s.kind() == SyntaxKind::Comma) {
|
||||
ctx.diag.push_error(
|
||||
"'circle' must be followed by a comma".into(),
|
||||
comma.map_or(&node as &dyn Spanned, |x| x as &dyn Spanned),
|
||||
);
|
||||
return Expression::Invalid;
|
||||
}
|
||||
(GradKind::Radial, 2)
|
||||
} else if grad_text.starts_with("conic") {
|
||||
GradKind::Conic
|
||||
// Check for optional "from <angle>" syntax
|
||||
let (from_angle, start_idx) = if all_subs.first().is_some_and(|n| {
|
||||
matches!(n, NodeOrToken::Node(node) if node.text().to_string().trim() == "from")
|
||||
}) {
|
||||
// Parse "from <angle>" syntax
|
||||
let angle_expr = match all_subs.get(1) {
|
||||
Some(e) if e.kind() == SyntaxKind::Expression => {
|
||||
syntax_nodes::Expression::from(e.as_node().unwrap().clone())
|
||||
}
|
||||
_ => {
|
||||
ctx.diag.push_error("Expected angle expression after 'from'".into(), &node);
|
||||
return Expression::Invalid;
|
||||
}
|
||||
};
|
||||
if !all_subs.get(2).is_some_and(|s| s.kind() == SyntaxKind::Comma) {
|
||||
ctx.diag.push_error(
|
||||
"'from <angle>' must be followed by a comma".into(),
|
||||
&node,
|
||||
);
|
||||
return Expression::Invalid;
|
||||
}
|
||||
let angle = Box::new(
|
||||
Expression::from_expression_node(angle_expr.clone(), ctx).maybe_convert_to(
|
||||
Type::Angle,
|
||||
&angle_expr,
|
||||
ctx.diag,
|
||||
),
|
||||
);
|
||||
(angle, 3)
|
||||
} else {
|
||||
// Default to 0deg when "from" is omitted
|
||||
(Box::new(Expression::NumberLiteral(0., Unit::Deg)), 0)
|
||||
};
|
||||
(GradKind::Conic { from_angle }, start_idx)
|
||||
} else {
|
||||
// Parser should have ensured we have one of the linear, radial or conic gradient
|
||||
panic!("Not a gradient {grad_text:?}");
|
||||
|
|
@ -579,11 +617,11 @@ impl Expression {
|
|||
Finished,
|
||||
}
|
||||
let mut current_stop = Stop::Empty;
|
||||
for n in subs {
|
||||
for n in all_subs.iter().skip(stops_start_idx) {
|
||||
if n.kind() == SyntaxKind::Comma {
|
||||
match std::mem::replace(&mut current_stop, Stop::Empty) {
|
||||
Stop::Empty => {
|
||||
ctx.diag.push_error("Expected expression".into(), &n);
|
||||
ctx.diag.push_error("Expected expression".into(), n);
|
||||
break;
|
||||
}
|
||||
Stop::Finished => {}
|
||||
|
|
@ -607,18 +645,18 @@ impl Expression {
|
|||
};
|
||||
match std::mem::replace(&mut current_stop, Stop::Finished) {
|
||||
Stop::Empty => {
|
||||
current_stop = Stop::Color(e.maybe_convert_to(Type::Color, &n, ctx.diag))
|
||||
current_stop = Stop::Color(e.maybe_convert_to(Type::Color, n, ctx.diag))
|
||||
}
|
||||
Stop::Finished => {
|
||||
ctx.diag.push_error("Expected comma".into(), &n);
|
||||
ctx.diag.push_error("Expected comma".into(), n);
|
||||
break;
|
||||
}
|
||||
Stop::Color(col) => {
|
||||
let stop_type = match &grad_kind {
|
||||
GradKind::Conic => Type::Angle,
|
||||
GradKind::Conic { .. } => Type::Angle,
|
||||
_ => Type::Float32,
|
||||
};
|
||||
stops.push((col, e.maybe_convert_to(stop_type, &n, ctx.diag)))
|
||||
stops.push((col, e.maybe_convert_to(stop_type, n, ctx.diag)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -678,19 +716,13 @@ impl Expression {
|
|||
match grad_kind {
|
||||
GradKind::Linear { angle } => Expression::LinearGradient { angle, stops },
|
||||
GradKind::Radial => Expression::RadialGradient { stops },
|
||||
GradKind::Conic => {
|
||||
// For conic gradients, we need to:
|
||||
// 1. Ensure angle expressions are converted to Type::Angle
|
||||
// 2. Normalize to 0-1 range for internal representation
|
||||
GradKind::Conic { from_angle } => {
|
||||
// Normalize angles to 0-1 range by dividing by 360deg
|
||||
let normalized_stops = stops
|
||||
.into_iter()
|
||||
.map(|(color, angle_expr)| {
|
||||
// First ensure the angle expression is properly typed as Angle
|
||||
let angle_typed =
|
||||
angle_expr.maybe_convert_to(Type::Angle, &node, &mut ctx.diag);
|
||||
|
||||
// Convert angle to 0-1 range by dividing by 360deg
|
||||
// This ensures all angle units (deg, rad, turn) are normalized
|
||||
let normalized_pos = Expression::BinaryExpression {
|
||||
lhs: Box::new(angle_typed),
|
||||
rhs: Box::new(Expression::NumberLiteral(360., Unit::Deg)),
|
||||
|
|
@ -699,7 +731,17 @@ impl Expression {
|
|||
(color, normalized_pos)
|
||||
})
|
||||
.collect();
|
||||
Expression::ConicGradient { stops: normalized_stops }
|
||||
|
||||
let normalized_from_angle = Box::new(Expression::BinaryExpression {
|
||||
lhs: from_angle,
|
||||
rhs: Box::new(Expression::NumberLiteral(360., Unit::Deg)),
|
||||
op: '/',
|
||||
});
|
||||
|
||||
Expression::ConicGradient {
|
||||
from_angle: normalized_from_angle,
|
||||
stops: normalized_stops,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,9 +91,12 @@ fn without_side_effects(expression: &Expression) -> bool {
|
|||
Expression::RadialGradient { stops } => stops
|
||||
.iter()
|
||||
.all(|(start, end)| without_side_effects(start) && without_side_effects(end)),
|
||||
Expression::ConicGradient { stops } => stops
|
||||
.iter()
|
||||
.all(|(start, end)| without_side_effects(start) && without_side_effects(end)),
|
||||
Expression::ConicGradient { from_angle, stops } => {
|
||||
without_side_effects(from_angle)
|
||||
&& stops
|
||||
.iter()
|
||||
.all(|(start, end)| without_side_effects(start) && without_side_effects(end))
|
||||
}
|
||||
Expression::EnumerationValue(_) => true,
|
||||
// A return statement is never without side effects, as an important "side effect" is that
|
||||
// the current function stops at this point.
|
||||
|
|
|
|||
28
internal/compiler/tests/syntax/basic/conic-gradient.slint
Normal file
28
internal/compiler/tests/syntax/basic/conic-gradient.slint
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// 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
|
||||
|
||||
export component X {
|
||||
// Valid conic gradients
|
||||
property<brush> g1: @conic-gradient();
|
||||
property<brush> g2: @conic-gradient(blue 0deg, red 180deg);
|
||||
property<brush> g3: @conic-gradient(blue 45deg, red 180deg, green 270deg);
|
||||
property<brush> g4: @conic-gradient(from 90deg, blue 0deg, red 180deg);
|
||||
property<brush> g5: @conic-gradient(from 90deg + 0.5turn, true ? blue : red 45deg, red 180deg);
|
||||
|
||||
// Angles outside 0-360deg range using expressions
|
||||
property<brush> g6: @conic-gradient(blue 0deg - 45deg, red 180deg);
|
||||
property<brush> g7: @conic-gradient(blue 450deg, red 720deg);
|
||||
property<brush> g8: @conic-gradient(from 0deg - 90deg, blue 0deg, red 180deg);
|
||||
property<brush> g9: @conic-gradient(from 450deg, blue 0deg, red 180deg);
|
||||
property<angle> neg_angle: -45deg;
|
||||
property<brush> g10: @conic-gradient(blue neg_angle, red 180deg);
|
||||
property<brush> g11: @conic-gradient(from neg_angle, blue 0deg, red 180deg);
|
||||
|
||||
// Error cases for 'from' syntax
|
||||
property<brush> g12: @conic-gradient(from 2, blue 45deg, red 180deg);
|
||||
// ^error{Cannot convert float to angle. Use an unit, or multiply by 1deg to convert explicitly}
|
||||
property<brush> g13: @conic-gradient(from,);
|
||||
// ^error{Expected angle expression after 'from'}
|
||||
property<brush> g14: @conic-gradient(from 90deg blue 0deg, red 180deg);
|
||||
// ^error{'from <angle>' must be followed by a comma}
|
||||
}
|
||||
|
|
@ -833,7 +833,8 @@ impl Snapshotter {
|
|||
.map(|(e1, e2)| (self.snapshot_expression(e1), self.snapshot_expression(e2)))
|
||||
.collect(),
|
||||
},
|
||||
Expression::ConicGradient { stops } => Expression::ConicGradient {
|
||||
Expression::ConicGradient { from_angle, stops } => Expression::ConicGradient {
|
||||
from_angle: Box::new(self.snapshot_expression(from_angle)),
|
||||
stops: stops
|
||||
.iter()
|
||||
.map(|(e1, e2)| (self.snapshot_expression(e1), self.snapshot_expression(e2)))
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ use euclid::default::{Point2D, Size2D};
|
|||
|
||||
#[cfg(not(feature = "std"))]
|
||||
use num_traits::float::Float;
|
||||
#[cfg(not(feature = "std"))]
|
||||
use num_traits::Euclid;
|
||||
|
||||
/// A brush is a data structure that is used to describe how
|
||||
/// a shape, such as a rectangle, path or even text, shall be filled.
|
||||
|
|
@ -112,12 +114,13 @@ impl Brush {
|
|||
GradientStop { color: s.color.brighter(factor), position: s.position }
|
||||
})))
|
||||
}
|
||||
Brush::ConicGradient(g) => {
|
||||
Brush::ConicGradient(ConicGradientBrush::new(g.stops().map(|s| GradientStop {
|
||||
Brush::ConicGradient(g) => Brush::ConicGradient(ConicGradientBrush::new(
|
||||
g.from_angle,
|
||||
g.stops().map(|s| GradientStop {
|
||||
color: s.color.brighter(factor),
|
||||
position: s.position,
|
||||
})))
|
||||
}
|
||||
}),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -138,6 +141,7 @@ impl Brush {
|
|||
.map(|s| GradientStop { color: s.color.darker(factor), position: s.position }),
|
||||
)),
|
||||
Brush::ConicGradient(g) => Brush::ConicGradient(ConicGradientBrush::new(
|
||||
g.from_angle,
|
||||
g.stops()
|
||||
.map(|s| GradientStop { color: s.color.darker(factor), position: s.position }),
|
||||
)),
|
||||
|
|
@ -165,12 +169,13 @@ impl Brush {
|
|||
GradientStop { color: s.color.transparentize(amount), position: s.position }
|
||||
})))
|
||||
}
|
||||
Brush::ConicGradient(g) => {
|
||||
Brush::ConicGradient(ConicGradientBrush::new(g.stops().map(|s| GradientStop {
|
||||
Brush::ConicGradient(g) => Brush::ConicGradient(ConicGradientBrush::new(
|
||||
g.from_angle,
|
||||
g.stops().map(|s| GradientStop {
|
||||
color: s.color.transparentize(amount),
|
||||
position: s.position,
|
||||
})))
|
||||
}
|
||||
}),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -192,12 +197,13 @@ impl Brush {
|
|||
GradientStop { color: s.color.with_alpha(alpha), position: s.position }
|
||||
})))
|
||||
}
|
||||
Brush::ConicGradient(g) => {
|
||||
Brush::ConicGradient(ConicGradientBrush::new(g.stops().map(|s| GradientStop {
|
||||
Brush::ConicGradient(g) => Brush::ConicGradient(ConicGradientBrush::new(
|
||||
g.from_angle,
|
||||
g.stops().map(|s| GradientStop {
|
||||
color: s.color.with_alpha(alpha),
|
||||
position: s.position,
|
||||
})))
|
||||
}
|
||||
}),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -254,20 +260,163 @@ impl RadialGradientBrush {
|
|||
/// The ConicGradientBrush describes a way of filling a shape with a gradient
|
||||
/// that rotates around a center point
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
#[repr(transparent)]
|
||||
pub struct ConicGradientBrush(SharedVector<GradientStop>);
|
||||
#[repr(C)]
|
||||
pub struct ConicGradientBrush {
|
||||
/// The starting angle (rotation) of the gradient in normalized form (0.0 = 0°, 1.0 = 360°)
|
||||
pub from_angle: f32,
|
||||
/// The color stops of the gradient
|
||||
pub stops: SharedVector<GradientStop>,
|
||||
}
|
||||
|
||||
impl ConicGradientBrush {
|
||||
/// Creates a new conic gradient with the provided color stops.
|
||||
/// The stops should have angle positions in the range 0.0 to 1.0,
|
||||
/// where 0.0 is 0 degrees (north) and 1.0 is 360 degrees.
|
||||
pub fn new(stops: impl IntoIterator<Item = GradientStop>) -> Self {
|
||||
Self(stops.into_iter().collect())
|
||||
/// Creates a new conic gradient with the provided starting angle and color stops.
|
||||
///
|
||||
/// 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 {
|
||||
let mut stops: alloc::vec::Vec<_> = stops.into_iter().collect();
|
||||
stops.sort_by(|a, b| {
|
||||
a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)
|
||||
});
|
||||
|
||||
// Add interpolated boundary stop at 0.0 if needed
|
||||
let has_stop_at_0 = stops.iter().any(|s| s.position.abs() < f32::EPSILON);
|
||||
if !has_stop_at_0 {
|
||||
// Find stops closest to boundaries for interpolation
|
||||
// For 0.0: find the stop just below 0 and just at/above 0
|
||||
let stop_below_0 = stops.iter().filter(|s| s.position < 0.0).last(); // closest to 0 from below
|
||||
let stop_above_0 = stops.iter().filter(|s| s.position >= 0.0).next(); // closest to 0 from above
|
||||
if let (Some(below), Some(above)) = (stop_below_0, stop_above_0) {
|
||||
// Interpolate between the stop below 0 and the stop above 0
|
||||
// Example: -10deg and 10deg → interpolate at 0deg
|
||||
let t = (0.0 - below.position) / (above.position - below.position);
|
||||
let color_at_0 = Self::interpolate_color(below.color, above.color, t);
|
||||
stops.insert(0, GradientStop { position: 0.0, color: color_at_0 });
|
||||
} else if let Some(above) = stop_above_0 {
|
||||
// Only stops above 0, use the first stop's color
|
||||
stops.insert(0, GradientStop { position: 0.0, color: above.color });
|
||||
}
|
||||
}
|
||||
|
||||
// Add interpolated boundary stop at 1.0 if needed
|
||||
let has_stop_at_1 = stops.iter().any(|s| (s.position - 1.0).abs() < f32::EPSILON);
|
||||
if !has_stop_at_1 {
|
||||
// For 1.0: find the stop just at/below 1 and just above 1
|
||||
let stop_below_1 = stops.iter().filter(|s| s.position <= 1.0).last(); // closest to 1 from below
|
||||
let stop_above_1 = stops.iter().filter(|s| s.position > 1.0).next(); // closest to 1 from above
|
||||
|
||||
if let (Some(below), Some(above)) = (stop_below_1, stop_above_1) {
|
||||
// Interpolate between the stop below 1 and the stop above 1
|
||||
// Example: 350deg and 370deg → interpolate at 360deg
|
||||
let t = (1.0 - below.position) / (above.position - below.position);
|
||||
let color_at_1 = Self::interpolate_color(below.color, above.color, t);
|
||||
stops.push(GradientStop { position: 1.0, color: color_at_1 });
|
||||
} else if let Some(below) = stop_below_1 {
|
||||
// Only stops below 1, use the last stop's color
|
||||
stops.push(GradientStop { position: 1.0, color: below.color });
|
||||
}
|
||||
}
|
||||
|
||||
// Drop stops under 0deg and over 360deg
|
||||
stops = stops.into_iter().filter(|s| 0.0 <= s.position && s.position <= 1.0).collect();
|
||||
|
||||
// Adjust first stop (at 0.0) to avoid duplicate with stop at 1.0
|
||||
if let Some(first) = stops.first_mut() {
|
||||
if first.position.abs() < f32::EPSILON {
|
||||
first.position = f32::EPSILON;
|
||||
}
|
||||
}
|
||||
|
||||
Self { from_angle, stops: SharedVector::from_iter(stops.into_iter()) }
|
||||
}
|
||||
|
||||
/// Returns the color stops of the conic gradient.
|
||||
pub fn stops(&self) -> impl Iterator<Item = &GradientStop> {
|
||||
self.0.iter()
|
||||
self.stops.iter()
|
||||
}
|
||||
|
||||
/// Returns the color stops with the `from_angle` rotation applied.
|
||||
///
|
||||
/// This method returns a new vector of stops where:
|
||||
/// 1. Stops outside [0.0, 1.0] range are removed
|
||||
/// 2. Boundary stops at 0.0 and 1.0 are added if missing (with interpolated colors)
|
||||
/// 3. Each stop's position is adjusted by adding `from_angle` and wrapping to [0.0, 1.0]
|
||||
/// 4. The stops are sorted by their rotated positions
|
||||
///
|
||||
/// 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> {
|
||||
let mut stops: alloc::vec::Vec<_> = self.stops.iter().copied().collect();
|
||||
|
||||
let from_angle = self.from_angle - self.from_angle.floor();
|
||||
if from_angle.abs() > f32::EPSILON {
|
||||
// Step 2: Apply rotation by adding stops and wrapping to [0, 1) range
|
||||
stops = stops
|
||||
.iter()
|
||||
.map(|stop| {
|
||||
#[cfg(feature = "std")]
|
||||
let rotated_position = (stop.position + from_angle).rem_euclid(1.0);
|
||||
#[cfg(not(feature = "std"))]
|
||||
let rotated_position = (stop.position + from_angle).rem_euclid(&1.0);
|
||||
GradientStop { position: rotated_position, color: stop.color }
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Step 3: Separate duplicate positions with different colors to avoid flickering
|
||||
for i in 0..stops.len() {
|
||||
let j = (i + 1) % stops.len();
|
||||
if (stops[i].position - stops[j].position).abs() < f32::EPSILON
|
||||
&& stops[i].color != stops[j].color
|
||||
{
|
||||
stops[i].position = (stops[i].position - f32::EPSILON).max(0.0);
|
||||
stops[j].position = (stops[j].position + f32::EPSILON).min(1.0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Sort by rotated position
|
||||
stops.sort_by(|a, b| {
|
||||
a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)
|
||||
});
|
||||
|
||||
// Step 5: Add boundary stops at 0.0 and 1.0 if missing
|
||||
let has_stop_at_0 = stops.iter().any(|s| s.position.abs() < f32::EPSILON);
|
||||
if !has_stop_at_0 {
|
||||
// Find the color at position 0.0 by interpolating from the last and first stops
|
||||
if let (Some(last), Some(first)) = (stops.last(), stops.first()) {
|
||||
let gap = 1.0 - last.position + first.position;
|
||||
let color_at_0 = if gap > f32::EPSILON {
|
||||
let t = (1.0 - last.position) / gap;
|
||||
Self::interpolate_color(last.color, first.color, t)
|
||||
} else {
|
||||
last.color
|
||||
};
|
||||
stops.insert(0, GradientStop { position: 0.0, color: color_at_0 });
|
||||
}
|
||||
}
|
||||
|
||||
let has_stop_at_1 = stops.iter().any(|s| (s.position - 1.0).abs() < f32::EPSILON);
|
||||
if !has_stop_at_1 {
|
||||
// Add stop at 1.0 with same color as stop at 0.0
|
||||
if let Some(first) = stops.first() {
|
||||
stops.push(GradientStop { position: 1.0, color: first.color });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stops
|
||||
}
|
||||
|
||||
/// Helper: Linearly interpolate between two colors
|
||||
fn interpolate_color(c1: Color, c2: Color, t: f32) -> Color {
|
||||
let argb1 = c1.to_argb_u8();
|
||||
let argb2 = c2.to_argb_u8();
|
||||
Color::from_argb_u8(
|
||||
((1.0 - t) * argb1.alpha as f32 + t * argb2.alpha as f32) as u8,
|
||||
((1.0 - t) * argb1.red as f32 + t * argb2.red as f32) as u8,
|
||||
((1.0 - t) * argb1.green as f32 + t * argb2.green as f32) as u8,
|
||||
((1.0 - t) * argb1.blue as f32 + t * argb2.blue as f32) as u8,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -382,7 +531,7 @@ impl InterpolatedPropertyValue for Brush {
|
|||
}
|
||||
(Brush::SolidColor(col), Brush::ConicGradient(grad)) => {
|
||||
let mut new_grad = grad.clone();
|
||||
for x in new_grad.0.make_mut_slice().iter_mut() {
|
||||
for x in new_grad.stops.make_mut_slice().iter_mut() {
|
||||
x.color = col.interpolate(&x.color, t);
|
||||
}
|
||||
Brush::ConicGradient(new_grad)
|
||||
|
|
@ -391,11 +540,12 @@ impl InterpolatedPropertyValue for Brush {
|
|||
Self::interpolate(b, a, 1. - t)
|
||||
}
|
||||
(Brush::ConicGradient(lhs), Brush::ConicGradient(rhs)) => {
|
||||
if lhs.0.len() < rhs.0.len() {
|
||||
if lhs.stops.len() < rhs.stops.len() {
|
||||
Self::interpolate(target_value, self, 1. - t)
|
||||
} else {
|
||||
let mut new_grad = lhs.clone();
|
||||
let mut iter = new_grad.0.make_mut_slice().iter_mut();
|
||||
new_grad.from_angle = lhs.from_angle.interpolate(&rhs.from_angle, t);
|
||||
let mut iter = new_grad.stops.make_mut_slice().iter_mut();
|
||||
for s2 in rhs.stops() {
|
||||
let s1 = iter.next().unwrap();
|
||||
s1.color = s1.color.interpolate(&s2.color, t);
|
||||
|
|
|
|||
|
|
@ -1427,7 +1427,8 @@ fn process_rectangle_impl(
|
|||
} else if let Brush::ConicGradient(g) = &args.background {
|
||||
let conic_grad = ConicGradientCommand {
|
||||
stops: g
|
||||
.stops()
|
||||
.rotated_stops()
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let mut stop = *s;
|
||||
stop.color = alpha_color(stop.color, args.alpha);
|
||||
|
|
|
|||
|
|
@ -371,12 +371,16 @@ pub fn eval_expression(expression: &Expression, local_context: &mut EvalLocalCon
|
|||
GradientStop{ color, position }
|
||||
}))))
|
||||
}
|
||||
Expression::ConicGradient{stops} => {
|
||||
Value::Brush(Brush::ConicGradient(ConicGradientBrush::new(stops.iter().map(|(color, stop)| {
|
||||
let color = eval_expression(color, local_context).try_into().unwrap();
|
||||
let position = eval_expression(stop, local_context).try_into().unwrap();
|
||||
GradientStop{ color, position }
|
||||
}))))
|
||||
Expression::ConicGradient{ from_angle, stops } => {
|
||||
let from_angle: f32 = eval_expression(from_angle, local_context).try_into().unwrap();
|
||||
Value::Brush(Brush::ConicGradient(ConicGradientBrush::new(
|
||||
from_angle,
|
||||
stops.iter().map(|(color, stop)| {
|
||||
let color = eval_expression(color, local_context).try_into().unwrap();
|
||||
let position = eval_expression(stop, local_context).try_into().unwrap();
|
||||
GradientStop{ color, position }
|
||||
})
|
||||
)))
|
||||
}
|
||||
Expression::EnumerationValue(value) => {
|
||||
Value::EnumerationValue(value.enumeration.name.to_string(), value.to_string())
|
||||
|
|
|
|||
|
|
@ -1501,18 +1501,12 @@ impl<'a, R: femtovg::Renderer + TextureImporter> GLItemRenderer<'a, R> {
|
|||
let path_width = path_bounds.width();
|
||||
let path_height = path_bounds.height();
|
||||
|
||||
let mut stops: Vec<_> = gradient
|
||||
.stops()
|
||||
let stops: Vec<_> = gradient
|
||||
.rotated_stops()
|
||||
.iter()
|
||||
.map(|stop| (stop.position, to_femtovg_color(&stop.color)))
|
||||
.collect();
|
||||
|
||||
// Add an extra stop at 1.0 with the same color as the last stop
|
||||
if let Some(last_stop) = stops.last().cloned() {
|
||||
if last_stop.0 != 1.0 {
|
||||
stops.push((1.0, last_stop.1));
|
||||
}
|
||||
}
|
||||
|
||||
femtovg::Paint::conic_gradient_stops(path_width / 2., path_height / 2., stops)
|
||||
}
|
||||
_ => return None,
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ impl<'a> SkiaItemRenderer<'a> {
|
|||
}
|
||||
Brush::ConicGradient(g) => {
|
||||
let (colors, pos): (Vec<_>, Vec<_>) =
|
||||
g.stops().map(|s| (to_skia_color(&s.color), s.position)).unzip();
|
||||
g.rotated_stops().iter().map(|s| (to_skia_color(&s.color), s.position)).unzip();
|
||||
|
||||
paint.set_dither(true);
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,14 @@ export component TestCase inherits Window {
|
|||
// Edge case: invisible stop at start
|
||||
Rectangle { background: @conic-gradient(transparent 0deg, red 3.6deg, white 180deg, transparent 360deg); }
|
||||
}
|
||||
Row {
|
||||
// Test 'from <angle>' syntax: default (from 0deg)
|
||||
Rectangle { background: @conic-gradient(red 0deg, blue 180deg, red 360deg); }
|
||||
// Rotated by 90deg
|
||||
Rectangle { background: @conic-gradient(from 90deg, red 0deg, blue 180deg, red 360deg); }
|
||||
// Rotated by 180deg
|
||||
Rectangle { background: @conic-gradient(from 180deg, red 0deg, blue 180deg, red 360deg); }
|
||||
}
|
||||
}
|
||||
|
||||
init => {
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 5 KiB After Width: | Height: | Size: 5.6 KiB |
|
|
@ -241,15 +241,18 @@ fn eval_expression(
|
|||
},
|
||||
)),
|
||||
)),
|
||||
Expression::ConicGradient { stops } => Value::Brush(slint::Brush::ConicGradient(
|
||||
i_slint_core::graphics::ConicGradientBrush::new(stops.iter().map(|(color, stop)| {
|
||||
let color =
|
||||
eval_expression(color, local_context, None).try_into().unwrap_or_default();
|
||||
let position =
|
||||
eval_expression(stop, local_context, None).try_into().unwrap_or_default();
|
||||
i_slint_core::graphics::GradientStop { color, position }
|
||||
})),
|
||||
)),
|
||||
Expression::ConicGradient { from_angle, stops } => Value::Brush(
|
||||
slint::Brush::ConicGradient(i_slint_core::graphics::ConicGradientBrush::new(
|
||||
eval_expression(from_angle, local_context, None).try_into().unwrap_or_default(),
|
||||
stops.iter().map(|(color, stop)| {
|
||||
let color =
|
||||
eval_expression(color, local_context, None).try_into().unwrap_or_default();
|
||||
let position =
|
||||
eval_expression(stop, local_context, None).try_into().unwrap_or_default();
|
||||
i_slint_core::graphics::GradientStop { color, position }
|
||||
}),
|
||||
)),
|
||||
),
|
||||
Expression::EnumerationValue(value) => {
|
||||
Value::EnumerationValue(value.enumeration.name.to_string(), value.to_string())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,8 +107,16 @@ fn as_slint_brush(
|
|||
}
|
||||
ui::BrushKind::Conic => {
|
||||
let stops = sorted_gradient_stops(stops);
|
||||
let angle = angle.rem_euclid(360.0);
|
||||
let prefix = if angle.abs() > f32::EPSILON {
|
||||
slint::format!("from {}deg, ", angle)
|
||||
} else {
|
||||
slint::SharedString::new()
|
||||
};
|
||||
|
||||
slint::format!(
|
||||
"@conic-gradient({})",
|
||||
"@conic-gradient({}{})",
|
||||
prefix,
|
||||
stops
|
||||
.iter()
|
||||
.map(|s| format!("{} {}deg", color_to_string(s.color), s.position * 360.0))
|
||||
|
|
@ -147,7 +155,7 @@ pub fn create_brush(
|
|||
i_slint_core::graphics::RadialGradientBrush::new_circle(stops.drain(..)),
|
||||
),
|
||||
ui::BrushKind::Conic => slint::Brush::ConicGradient(
|
||||
i_slint_core::graphics::ConicGradientBrush::new(stops.drain(..)),
|
||||
i_slint_core::graphics::ConicGradientBrush::new(angle / 360.0, stops.drain(..)),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue