Three root causes, all fixed and verified on the Pixel 9 Pro: 1. Present phase (backend + renderer): VSYNC_LOCKED_RENDER defers the render to the vsync wake; RENDER_AHEAD presents the previous frame at the start of render() (phase = wake + ~1.1 ms) and holds the new frame one slot. Crash fix: a held wgpu frame must be PRESENTED, never merely dropped, before the next acquire (AlreadyAcquired panic at launch otherwise); discard_pending_frame presents too. 2. Full-res prefetch churn (src/lib.rs): the 8-slot full cache cycled through a ~20-cell viewport forever (137 decodes/20 s) and each eviction free()'d a 50 MB Vec on the UI thread (20-52 ms CPU spikes, ~7/s = the residual SLOWEV NonMain jank). Fix: 60 s tombstone cooldown on evicted paths (idle prefetch skips, urgent tap bypasses) + off-thread reaper for the freed blobs (insert: 21 ms -> 4 us). 3. (earlier) Skia final flush + Mailbox present mode (vendor, M3). Instrumentation (all behind --features perf): OTHR/INVK/INVK_TAG/FLCK lines for cross-thread closure timing and push-site tagging. Warm measured swipe, 8580-file grid: cadence median 8.33 ms (exact 120 Hz), p99 15.5 ms, 5/552 gaps >16.7 ms (0.9%, max 28.3 ms, zero >33 ms); PRES phase median ~994 us, zero late frames. Docs updated (docs/BUGS/scroll-jank-120hz.md, full scorecard + line reference). Also: vendored i-slint-core + wgpu-hal (vkshim interposition patch), vkshim profiling tooling, backend made a non-optional dep, debuggable flag for simpleperf (REMOVE before shipping).
1470 lines
60 KiB
Rust
1470 lines
60 KiB
Rust
// 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
|
||
|
||
/*!
|
||
This module contains brush related types for the run-time library.
|
||
*/
|
||
|
||
use super::Color;
|
||
use crate::SharedVector;
|
||
use crate::lengths::{PhysicalPx, ScaleFactor};
|
||
use crate::properties::InterpolatedPropertyValue;
|
||
use alloc::borrow::Cow;
|
||
use euclid::default::{Point2D, Size2D};
|
||
|
||
#[cfg(not(feature = "std"))]
|
||
use num_traits::float::Float;
|
||
|
||
/// 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.
|
||
/// A brush can also be applied to the outline of a shape, that means
|
||
/// the fill of the outline itself.
|
||
#[derive(Clone, PartialEq, Debug, derive_more::From)]
|
||
#[repr(C)]
|
||
#[non_exhaustive]
|
||
pub enum Brush {
|
||
/// The color variant of brush is a plain color that is to be used for the fill.
|
||
SolidColor(Color),
|
||
/// The linear gradient variant of a brush describes the gradient stops for a fill
|
||
/// where all color stops are along a line that's rotated by the specified angle.
|
||
LinearGradient(LinearGradientBrush),
|
||
/// The radial gradient variant of a brush describes a circular gradient.
|
||
/// The center defaults to the middle of the bounding box.
|
||
RadialGradient(RadialGradientBrush),
|
||
/// The conical gradient variant of a brush describes a gradient that rotates around
|
||
/// a center point, like the hands of a clock
|
||
ConicGradient(ConicGradientBrush),
|
||
}
|
||
|
||
/// Construct a brush with transparent color
|
||
impl Default for Brush {
|
||
fn default() -> Self {
|
||
Self::SolidColor(Color::default())
|
||
}
|
||
}
|
||
|
||
impl Brush {
|
||
/// If the brush is SolidColor, the contained color is returned.
|
||
/// If the brush is a LinearGradient, the color of the first stop is returned.
|
||
pub fn color(&self) -> Color {
|
||
match self {
|
||
Brush::SolidColor(col) => *col,
|
||
Brush::LinearGradient(gradient) => {
|
||
gradient.stops().next().map(|stop| stop.color).unwrap_or_default()
|
||
}
|
||
Brush::RadialGradient(gradient) => {
|
||
gradient.stops().next().map(|stop| stop.color).unwrap_or_default()
|
||
}
|
||
Brush::ConicGradient(gradient) => {
|
||
gradient.stops().next().map(|stop| stop.color).unwrap_or_default()
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Returns true if this brush contains a fully transparent color (alpha value is zero)
|
||
///
|
||
/// ```
|
||
/// # use i_slint_core::graphics::*;
|
||
/// assert!(Brush::default().is_transparent());
|
||
/// assert!(Brush::SolidColor(Color::from_argb_u8(0, 255, 128, 140)).is_transparent());
|
||
/// assert!(!Brush::SolidColor(Color::from_argb_u8(25, 128, 140, 210)).is_transparent());
|
||
/// ```
|
||
pub fn is_transparent(&self) -> bool {
|
||
match self {
|
||
Brush::SolidColor(c) => c.alpha() == 0,
|
||
Brush::LinearGradient(_) => false,
|
||
Brush::RadialGradient(_) => false,
|
||
Brush::ConicGradient(_) => false,
|
||
}
|
||
}
|
||
|
||
/// Returns true if this brush is fully opaque
|
||
///
|
||
/// ```
|
||
/// # use i_slint_core::graphics::*;
|
||
/// assert!(!Brush::default().is_opaque());
|
||
/// assert!(!Brush::SolidColor(Color::from_argb_u8(25, 255, 128, 140)).is_opaque());
|
||
/// assert!(Brush::SolidColor(Color::from_rgb_u8(128, 140, 210)).is_opaque());
|
||
/// ```
|
||
pub fn is_opaque(&self) -> bool {
|
||
match self {
|
||
Brush::SolidColor(c) => c.alpha() == 255,
|
||
Brush::LinearGradient(g) => g.stops().all(|s| s.color.alpha() == 255),
|
||
Brush::RadialGradient(g) => g.stops().all(|s| s.color.alpha() == 255),
|
||
Brush::ConicGradient(g) => g.stops().all(|s| s.color.alpha() == 255),
|
||
}
|
||
}
|
||
|
||
/// 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.
|
||
#[must_use]
|
||
pub fn brighter(&self, factor: f32) -> Self {
|
||
match self {
|
||
Brush::SolidColor(c) => Brush::SolidColor(c.brighter(factor)),
|
||
Brush::LinearGradient(g) => Brush::LinearGradient(LinearGradientBrush::new(
|
||
g.angle(),
|
||
g.stops().map(|s| GradientStop {
|
||
color: s.color.brighter(factor),
|
||
position: s.position,
|
||
}),
|
||
)),
|
||
Brush::RadialGradient(g) => {
|
||
let mut new_grad = g.clone();
|
||
for s in new_grad.0.make_mut_slice().iter_mut().skip(RadialGradientBrush::HEADER) {
|
||
s.color = s.color.brighter(factor);
|
||
}
|
||
Brush::RadialGradient(new_grad)
|
||
}
|
||
Brush::ConicGradient(g) => {
|
||
let mut new_grad = g.clone();
|
||
for x in new_grad.0.make_mut_slice().iter_mut().skip(ConicGradientBrush::HEADER) {
|
||
x.color = x.color.brighter(factor);
|
||
}
|
||
Brush::ConicGradient(new_grad)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Returns a new version of this brush that has the brightness decreased
|
||
/// by the specified factor. This is done by calling [`Color::darker`] on
|
||
/// all the color of this brush.
|
||
#[must_use]
|
||
pub fn darker(&self, factor: f32) -> Self {
|
||
match self {
|
||
Brush::SolidColor(c) => Brush::SolidColor(c.darker(factor)),
|
||
Brush::LinearGradient(g) => Brush::LinearGradient(LinearGradientBrush::new(
|
||
g.angle(),
|
||
g.stops()
|
||
.map(|s| GradientStop { color: s.color.darker(factor), position: s.position }),
|
||
)),
|
||
Brush::RadialGradient(g) => {
|
||
let mut new_grad = g.clone();
|
||
for s in new_grad.0.make_mut_slice().iter_mut().skip(RadialGradientBrush::HEADER) {
|
||
s.color = s.color.darker(factor);
|
||
}
|
||
Brush::RadialGradient(new_grad)
|
||
}
|
||
Brush::ConicGradient(g) => {
|
||
let mut new_grad = g.clone();
|
||
for x in new_grad.0.make_mut_slice().iter_mut().skip(ConicGradientBrush::HEADER) {
|
||
x.color = x.color.darker(factor);
|
||
}
|
||
Brush::ConicGradient(new_grad)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Returns a new version of this brush with the opacity decreased by `factor`.
|
||
///
|
||
/// The transparency is obtained by multiplying the alpha channel by `(1 - factor)`.
|
||
///
|
||
/// See also [`Color::transparentize`]
|
||
#[must_use]
|
||
pub fn transparentize(&self, amount: f32) -> Self {
|
||
match self {
|
||
Brush::SolidColor(c) => Brush::SolidColor(c.transparentize(amount)),
|
||
Brush::LinearGradient(g) => Brush::LinearGradient(LinearGradientBrush::new(
|
||
g.angle(),
|
||
g.stops().map(|s| GradientStop {
|
||
color: s.color.transparentize(amount),
|
||
position: s.position,
|
||
}),
|
||
)),
|
||
Brush::RadialGradient(g) => {
|
||
let mut new_grad = g.clone();
|
||
for s in new_grad.0.make_mut_slice().iter_mut().skip(RadialGradientBrush::HEADER) {
|
||
s.color = s.color.transparentize(amount);
|
||
}
|
||
Brush::RadialGradient(new_grad)
|
||
}
|
||
Brush::ConicGradient(g) => {
|
||
let mut new_grad = g.clone();
|
||
for x in new_grad.0.make_mut_slice().iter_mut().skip(ConicGradientBrush::HEADER) {
|
||
x.color = x.color.transparentize(amount);
|
||
}
|
||
Brush::ConicGradient(new_grad)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Returns a new version of this brush with the related color's opacities
|
||
/// set to `alpha`.
|
||
#[must_use]
|
||
pub fn with_alpha(&self, alpha: f32) -> Self {
|
||
match self {
|
||
Brush::SolidColor(c) => Brush::SolidColor(c.with_alpha(alpha)),
|
||
Brush::LinearGradient(g) => Brush::LinearGradient(LinearGradientBrush::new(
|
||
g.angle(),
|
||
g.stops().map(|s| GradientStop {
|
||
color: s.color.with_alpha(alpha),
|
||
position: s.position,
|
||
}),
|
||
)),
|
||
Brush::RadialGradient(g) => {
|
||
let mut new_grad = g.clone();
|
||
for s in new_grad.0.make_mut_slice().iter_mut().skip(RadialGradientBrush::HEADER) {
|
||
s.color = s.color.with_alpha(alpha);
|
||
}
|
||
Brush::RadialGradient(new_grad)
|
||
}
|
||
Brush::ConicGradient(g) => {
|
||
let mut new_grad = g.clone();
|
||
for x in new_grad.0.make_mut_slice().iter_mut().skip(ConicGradientBrush::HEADER) {
|
||
x.color = x.color.with_alpha(alpha);
|
||
}
|
||
Brush::ConicGradient(new_grad)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The LinearGradientBrush describes a way of filling a shape with different colors, which
|
||
/// are interpolated between different stops. The colors are aligned with a line that's rotated
|
||
/// by the LinearGradient's angle.
|
||
#[derive(Clone, PartialEq, Debug)]
|
||
#[repr(transparent)]
|
||
pub struct LinearGradientBrush(SharedVector<GradientStop>);
|
||
|
||
impl LinearGradientBrush {
|
||
/// Creates a new linear gradient, described by the specified angle and the provided color stops.
|
||
///
|
||
/// The angle need to be specified in degrees.
|
||
/// The stops don't need to be sorted as this function will sort them.
|
||
pub fn new(angle: f32, stops: impl IntoIterator<Item = GradientStop>) -> Self {
|
||
let stop_iter = stops.into_iter();
|
||
let mut encoded_angle_and_stops = SharedVector::with_capacity(stop_iter.size_hint().0 + 1);
|
||
// The gradient's first stop is a fake stop to store the angle
|
||
encoded_angle_and_stops.push(GradientStop { color: Default::default(), position: angle });
|
||
encoded_angle_and_stops.extend(stop_iter);
|
||
Self(encoded_angle_and_stops)
|
||
}
|
||
/// Returns the angle of the linear gradient in degrees.
|
||
pub fn angle(&self) -> f32 {
|
||
self.0[0].position
|
||
}
|
||
/// Returns the color stops of the linear gradient.
|
||
/// The stops are sorted by positions.
|
||
pub fn stops(&self) -> impl Iterator<Item = &GradientStop> {
|
||
// skip the first fake stop that just contains the angle
|
||
self.0.iter().skip(1)
|
||
}
|
||
|
||
/// The color stops as a slice, without the angle header stop.
|
||
fn stops_slice(&self) -> &[GradientStop] {
|
||
self.0.as_slice().get(1..).unwrap_or_default()
|
||
}
|
||
}
|
||
|
||
/// NaN-aware float equality: two NaNs compare equal (unlike IEEE 754).
|
||
#[inline]
|
||
fn nan_eq(a: f32, b: f32) -> bool {
|
||
a == b || (a.is_nan() && b.is_nan())
|
||
}
|
||
|
||
/// Shared center resolution for the logical and scaled radial/conic methods.
|
||
/// When `scale_factor` is 1.0 this is identical to the unscaled case.
|
||
#[inline]
|
||
fn center_or_bbox(cx: f32, cy: f32, width: f32, height: f32, scale_factor: f32) -> (f32, f32) {
|
||
if cx.is_nan() { (width / 2.0, height / 2.0) } else { (cx * scale_factor, cy * scale_factor) }
|
||
}
|
||
|
||
/// The RadialGradientBrush describes a way of filling a shape with a circular gradient.
|
||
///
|
||
/// The center defaults to the middle of the bounding box; the radius defaults to half the
|
||
/// bounding box diagonal. Use [`with_center`](Self::with_center) and
|
||
/// [`with_radius`](Self::with_radius) to override these defaults.
|
||
///
|
||
/// Internally the brush encodes center and radius as the first three fake
|
||
/// [`GradientStop`] entries (indices 0–2), following the same pattern as
|
||
/// [`LinearGradientBrush`] (which stores the angle as stop 0).
|
||
#[derive(Clone, Debug)]
|
||
#[repr(transparent)]
|
||
pub struct RadialGradientBrush(SharedVector<GradientStop>);
|
||
|
||
impl RadialGradientBrush {
|
||
const HEADER: usize = 3;
|
||
|
||
/// Creates a new circle radial gradient centered in the element's bounding box,
|
||
/// described by the provided color stops.
|
||
pub fn new_circle(stops: impl IntoIterator<Item = GradientStop>) -> Self {
|
||
let stop_iter = stops.into_iter();
|
||
let mut v = SharedVector::with_capacity(Self::HEADER + stop_iter.size_hint().0);
|
||
// Header stops: center_x (NaN=bbox), center_y (NaN=bbox), radius (negative=bbox diagonal/2)
|
||
v.push(GradientStop { color: Default::default(), position: f32::NAN });
|
||
v.push(GradientStop { color: Default::default(), position: f32::NAN });
|
||
v.push(GradientStop { color: Default::default(), position: -1.0 });
|
||
v.extend(stop_iter);
|
||
Self(v)
|
||
}
|
||
|
||
#[inline]
|
||
fn center_x(&self) -> f32 {
|
||
self.0[0].position
|
||
}
|
||
#[inline]
|
||
fn center_y(&self) -> f32 {
|
||
self.0[1].position
|
||
}
|
||
#[inline]
|
||
fn radius(&self) -> f32 {
|
||
self.0[2].position
|
||
}
|
||
|
||
/// Returns the color stops of the radial gradient.
|
||
pub fn stops(&self) -> impl Iterator<Item = &GradientStop> {
|
||
self.0.iter().skip(Self::HEADER)
|
||
}
|
||
|
||
/// The color stops as a slice, without the header stops.
|
||
fn stops_slice(&self) -> &[GradientStop] {
|
||
self.0.as_slice().get(Self::HEADER..).unwrap_or_default()
|
||
}
|
||
|
||
/// Sets an explicit center, returning `self` for chaining. `cx` and `cy` are in the
|
||
/// element's local logical coordinate space.
|
||
pub fn with_center(mut self, cx: f32, cy: f32) -> Self {
|
||
let s = self.0.make_mut_slice();
|
||
s[0].position = cx;
|
||
s[1].position = cy;
|
||
self
|
||
}
|
||
|
||
/// Sets an explicit radius, returning `self` for chaining. `r` is in the element's local
|
||
/// logical coordinate space.
|
||
pub fn with_radius(mut self, r: f32) -> Self {
|
||
self.0.make_mut_slice()[2].position = r;
|
||
self
|
||
}
|
||
|
||
/// Returns the gradient center, falling back to the bounding box center when not explicitly set.
|
||
///
|
||
/// `width` and `height` are the element's logical dimensions.
|
||
pub fn center_or_default(&self, width: f32, height: f32) -> (f32, f32) {
|
||
debug_assert!(
|
||
self.center_x().is_nan() == self.center_y().is_nan(),
|
||
"center_x and center_y must both be NaN or both finite"
|
||
);
|
||
center_or_bbox(self.center_x(), self.center_y(), width, height, 1.0)
|
||
}
|
||
|
||
/// Returns the gradient center in a scaled coordinate space.
|
||
///
|
||
/// `width` and `height` are the dimensions in the target coordinate space. Explicit center
|
||
/// values are local logical lengths and are multiplied by `scale_factor`; default centers are
|
||
/// derived from the dimensions directly.
|
||
pub fn center_or_default_scaled(
|
||
&self,
|
||
width: f32,
|
||
height: f32,
|
||
scale_factor: f32,
|
||
) -> (f32, f32) {
|
||
debug_assert!(
|
||
self.center_x().is_nan() == self.center_y().is_nan(),
|
||
"center_x and center_y must both be NaN or both finite"
|
||
);
|
||
center_or_bbox(self.center_x(), self.center_y(), width, height, scale_factor)
|
||
}
|
||
|
||
/// Returns the gradient radius, falling back to half of the bounding box diagonal when not
|
||
/// explicitly set.
|
||
///
|
||
/// `width` and `height` are the element's logical dimensions.
|
||
pub fn radius_or_default(&self, width: f32, height: f32) -> f32 {
|
||
let r = self.radius();
|
||
if r < 0.0 { 0.5 * (width * width + height * height).sqrt() } else { r }
|
||
}
|
||
|
||
/// Returns the gradient radius in a scaled coordinate space.
|
||
///
|
||
/// `width` and `height` are the dimensions in the target coordinate space. Explicit radius
|
||
/// values are local logical lengths and are multiplied by `scale_factor`; the default radius is
|
||
/// derived from the dimensions directly.
|
||
pub fn radius_or_default_scaled(&self, width: f32, height: f32, scale_factor: f32) -> f32 {
|
||
let r = self.radius();
|
||
if r < 0.0 { 0.5 * (width * width + height * height).sqrt() } else { r * scale_factor }
|
||
}
|
||
}
|
||
|
||
/// Equality is render-equivalence: two NaN center fields compare equal because both use the
|
||
/// bounding box center. Any two negative radii compare equal because both use the default radius.
|
||
impl PartialEq for RadialGradientBrush {
|
||
fn eq(&self, other: &Self) -> bool {
|
||
if self.0.len() != other.0.len() {
|
||
return false;
|
||
}
|
||
nan_eq(self.center_x(), other.center_x())
|
||
&& nan_eq(self.center_y(), other.center_y())
|
||
&& (self.radius() == other.radius() || (self.radius() < 0.0 && other.radius() < 0.0))
|
||
&& self.0.iter().skip(Self::HEADER).eq(other.0.iter().skip(Self::HEADER))
|
||
}
|
||
}
|
||
|
||
/// The ConicGradientBrush describes a way of filling a shape with a gradient
|
||
/// that rotates around a center point.
|
||
///
|
||
/// The center defaults to the middle of the bounding box. Use
|
||
/// [`with_center`](Self::with_center) to override.
|
||
///
|
||
/// Internally the first three fake [`GradientStop`] entries encode the starting angle
|
||
/// (index 0), center_x (index 1), and center_y (index 2). Real color stops begin at index 3.
|
||
#[derive(Clone, Debug)]
|
||
#[repr(transparent)]
|
||
pub struct ConicGradientBrush(SharedVector<GradientStop>);
|
||
|
||
/// Equality is render-equivalence: two NaN center fields compare equal because both use the
|
||
/// bounding box center.
|
||
impl PartialEq for ConicGradientBrush {
|
||
fn eq(&self, other: &Self) -> bool {
|
||
if self.0.len() != other.0.len() {
|
||
return false;
|
||
}
|
||
// angle (index 0) uses plain f32 equality (not NaN-aware)
|
||
self.0[0].position == other.0[0].position
|
||
&& nan_eq(self.center_x(), other.center_x())
|
||
&& nan_eq(self.center_y(), other.center_y())
|
||
&& self.0.iter().skip(Self::HEADER).eq(other.0.iter().skip(Self::HEADER))
|
||
}
|
||
}
|
||
|
||
impl ConicGradientBrush {
|
||
const HEADER: usize = 3;
|
||
|
||
/// Creates a new conic gradient, described by the specified angle and the provided color stops.
|
||
///
|
||
/// The angle need to be specified in degrees (CSS `from <angle>` syntax).
|
||
/// The stops don't need to be sorted as this function will normalize and process them.
|
||
pub fn new(angle: f32, stops: impl IntoIterator<Item = GradientStop>) -> Self {
|
||
let stop_iter = stops.into_iter();
|
||
let mut v = SharedVector::with_capacity(Self::HEADER + stop_iter.size_hint().0);
|
||
// Header stops: angle, center_x (NaN=bbox), center_y (NaN=bbox)
|
||
v.push(GradientStop { color: Default::default(), position: angle });
|
||
v.push(GradientStop { color: Default::default(), position: f32::NAN });
|
||
v.push(GradientStop { color: Default::default(), position: f32::NAN });
|
||
v.extend(stop_iter);
|
||
let mut result = Self(v);
|
||
result.normalize_stops();
|
||
if angle.abs() > f32::EPSILON {
|
||
result.apply_rotation(angle);
|
||
}
|
||
result
|
||
}
|
||
|
||
/// Normalizes the gradient stops to be within [0, 1] range with proper boundary stops.
|
||
fn normalize_stops(&mut self) {
|
||
// Check if we need to make any changes
|
||
let stops_slice = &self.0[Self::HEADER..];
|
||
let has_stop_at_0 = stops_slice.iter().any(|s| s.position.abs() < f32::EPSILON);
|
||
let has_stop_at_1 = stops_slice.iter().any(|s| (s.position - 1.0).abs() < f32::EPSILON);
|
||
let has_stops_outside = stops_slice.iter().any(|s| s.position < 0.0 || s.position > 1.0);
|
||
let is_empty = stops_slice.is_empty();
|
||
|
||
// If no changes needed, return early
|
||
if has_stop_at_0 && has_stop_at_1 && !has_stops_outside && !is_empty {
|
||
return;
|
||
}
|
||
|
||
// Need to make changes, so copy
|
||
let mut stops: alloc::vec::Vec<_> = stops_slice.to_vec();
|
||
|
||
// Add interpolated boundary stop at 0.0 if needed
|
||
if !has_stop_at_0 {
|
||
let stop_below_0 = stops.iter().filter(|s| s.position < 0.0).max_by(|a, b| {
|
||
a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)
|
||
});
|
||
let stop_above_0 = stops.iter().filter(|s| s.position > 0.0).min_by(|a, b| {
|
||
a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)
|
||
});
|
||
if let (Some(below), Some(above)) = (stop_below_0, stop_above_0) {
|
||
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 {
|
||
stops.insert(0, GradientStop { position: 0.0, color: above.color });
|
||
} else if let Some(below) = stop_below_0 {
|
||
stops.insert(0, GradientStop { position: 0.0, color: below.color });
|
||
}
|
||
}
|
||
|
||
// Add interpolated boundary stop at 1.0 if needed
|
||
if !has_stop_at_1 {
|
||
let stop_below_1 = stops.iter().filter(|s| s.position < 1.0).max_by(|a, b| {
|
||
a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)
|
||
});
|
||
let stop_above_1 = stops.iter().filter(|s| s.position > 1.0).min_by(|a, b| {
|
||
a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)
|
||
});
|
||
|
||
if let (Some(below), Some(above)) = (stop_below_1, stop_above_1) {
|
||
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 {
|
||
stops.push(GradientStop { position: 1.0, color: below.color });
|
||
} else if let Some(above) = stop_above_1 {
|
||
stops.push(GradientStop { position: 1.0, color: above.color });
|
||
}
|
||
}
|
||
|
||
// Drop stops outside [0, 1] range
|
||
if has_stops_outside {
|
||
stops.retain(|s| 0.0 <= s.position && s.position <= 1.0);
|
||
}
|
||
|
||
// Handle empty gradients
|
||
if stops.is_empty() {
|
||
stops.push(GradientStop { position: 0.0, color: Color::default() });
|
||
stops.push(GradientStop { position: 1.0, color: Color::default() });
|
||
}
|
||
|
||
// Rebuild internal storage, preserving the full header (angle, center_x, center_y)
|
||
let angle = self.angle();
|
||
let cx = self.center_x();
|
||
let cy = self.center_y();
|
||
self.0 = SharedVector::with_capacity(stops.len() + Self::HEADER);
|
||
self.0.push(GradientStop { color: Default::default(), position: angle });
|
||
self.0.push(GradientStop { color: Default::default(), position: cx });
|
||
self.0.push(GradientStop { color: Default::default(), position: cy });
|
||
self.0.extend(stops);
|
||
}
|
||
|
||
/// Apply rotation to the gradient (CSS `from <angle>` syntax).
|
||
///
|
||
/// The `from_angle` parameter is specified in degrees and rotates the entire gradient clockwise.
|
||
fn apply_rotation(&mut self, from_angle: f32) {
|
||
// Convert degrees to normalized 0-1 range
|
||
let normalized_from_angle = (from_angle / 360.0) - (from_angle / 360.0).floor();
|
||
|
||
// If no rotation needed, just update the stored angle
|
||
if normalized_from_angle.abs() < f32::EPSILON {
|
||
self.0.make_mut_slice()[0].position = from_angle;
|
||
return;
|
||
}
|
||
|
||
// Update the stored angle
|
||
self.0.make_mut_slice()[0].position = from_angle;
|
||
|
||
// Need to rotate, so copy
|
||
let mut stops: alloc::vec::Vec<_> = self.0.iter().skip(Self::HEADER).copied().collect();
|
||
|
||
// Adjust first stop (at 0.0) to avoid duplicate with stop at 1.0
|
||
if let Some(first) = stops.first_mut()
|
||
&& first.position.abs() < f32::EPSILON
|
||
{
|
||
first.position = f32::EPSILON;
|
||
}
|
||
|
||
// Step 1: Apply rotation by adding from_angle and wrapping to [0, 1) range
|
||
stops = stops
|
||
.iter()
|
||
.map(|stop| {
|
||
// f32::rem_euclid is not always available, and when it is it has a
|
||
// different signature than num_traits::Euclid::rem_euclid (issue #11333).
|
||
let rotated_position =
|
||
num_traits::Euclid::rem_euclid(&(stop.position + normalized_from_angle), &1.0);
|
||
GradientStop { position: rotated_position, color: stop.color }
|
||
})
|
||
.collect();
|
||
|
||
// Step 2: 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);
|
||
}
|
||
}
|
||
|
||
// Step 3: Sort by rotated position
|
||
stops.sort_by(|a, b| {
|
||
a.position.partial_cmp(&b.position).unwrap_or(core::cmp::Ordering::Equal)
|
||
});
|
||
|
||
// Step 4: 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 && 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 && let Some(first) = stops.first() {
|
||
stops.push(GradientStop { position: 1.0, color: first.color });
|
||
}
|
||
|
||
// Rebuild internal storage, preserving the full header (angle, center_x, center_y)
|
||
let cx = self.center_x();
|
||
let cy = self.center_y();
|
||
self.0 = SharedVector::with_capacity(stops.len() + Self::HEADER);
|
||
self.0.push(GradientStop { color: Default::default(), position: from_angle });
|
||
self.0.push(GradientStop { color: Default::default(), position: cx });
|
||
self.0.push(GradientStop { color: Default::default(), position: cy });
|
||
self.0.extend(stops);
|
||
}
|
||
|
||
/// Returns the starting angle (rotation) of the conic gradient in degrees.
|
||
fn angle(&self) -> f32 {
|
||
self.0[0].position
|
||
}
|
||
|
||
#[inline]
|
||
fn center_x(&self) -> f32 {
|
||
self.0[1].position
|
||
}
|
||
#[inline]
|
||
fn center_y(&self) -> f32 {
|
||
self.0[2].position
|
||
}
|
||
|
||
/// Returns the color stops of the conic gradient.
|
||
/// The stops are already rotated according to the `from_angle` specified in `new()`.
|
||
pub fn stops(&self) -> impl Iterator<Item = &GradientStop> {
|
||
self.0.iter().skip(Self::HEADER)
|
||
}
|
||
|
||
/// The color stops as a slice, without the header stops.
|
||
fn stops_slice(&self) -> &[GradientStop] {
|
||
self.0.as_slice().get(Self::HEADER..).unwrap_or_default()
|
||
}
|
||
|
||
/// Sets an explicit center, returning `self` for chaining. `cx` and `cy` are in the
|
||
/// element's local logical coordinate space.
|
||
pub fn with_center(mut self, cx: f32, cy: f32) -> Self {
|
||
let s = self.0.make_mut_slice();
|
||
s[1].position = cx;
|
||
s[2].position = cy;
|
||
self
|
||
}
|
||
|
||
/// Returns the gradient center, falling back to the bounding box center when not explicitly set.
|
||
///
|
||
/// `width` and `height` are the element's logical dimensions.
|
||
pub fn center_or_default(&self, width: f32, height: f32) -> (f32, f32) {
|
||
debug_assert!(
|
||
self.center_x().is_nan() == self.center_y().is_nan(),
|
||
"center_x and center_y must both be NaN or both finite"
|
||
);
|
||
center_or_bbox(self.center_x(), self.center_y(), width, height, 1.0)
|
||
}
|
||
|
||
/// Returns the gradient center in a scaled coordinate space.
|
||
///
|
||
/// `width` and `height` are the dimensions in the target coordinate space. Explicit center
|
||
/// values are local logical lengths and are multiplied by `scale_factor`; default centers are
|
||
/// derived from the dimensions directly.
|
||
pub fn center_or_default_scaled(
|
||
&self,
|
||
width: f32,
|
||
height: f32,
|
||
scale_factor: f32,
|
||
) -> (f32, f32) {
|
||
debug_assert!(
|
||
self.center_x().is_nan() == self.center_y().is_nan(),
|
||
"center_x and center_y must both be NaN or both finite"
|
||
);
|
||
center_or_bbox(self.center_x(), self.center_y(), width, height, scale_factor)
|
||
}
|
||
|
||
/// Helper: Linearly interpolate between two colors using premultiplied alpha.
|
||
///
|
||
/// This is used for interpolating gradient boundary colors in CSS-style gradients.
|
||
/// We cannot use Color::mix() here because it implements Sass color mixing algorithm,
|
||
/// which is different from CSS gradient color interpolation.
|
||
///
|
||
/// CSS gradients interpolate in premultiplied RGBA space:
|
||
/// https://www.w3.org/TR/css-color-4/#interpolation-alpha
|
||
fn interpolate_color(c1: &Color, c2: &Color, factor: f32) -> Color {
|
||
let argb1 = c1.to_argb_u8();
|
||
let argb2 = c2.to_argb_u8();
|
||
|
||
// Convert to premultiplied alpha
|
||
let a1 = argb1.alpha as f32 / 255.0;
|
||
let a2 = argb2.alpha as f32 / 255.0;
|
||
let r1 = argb1.red as f32 * a1;
|
||
let g1 = argb1.green as f32 * a1;
|
||
let b1 = argb1.blue as f32 * a1;
|
||
let r2 = argb2.red as f32 * a2;
|
||
let g2 = argb2.green as f32 * a2;
|
||
let b2 = argb2.blue as f32 * a2;
|
||
|
||
// Interpolate in premultiplied space
|
||
let alpha = (1.0 - factor) * a1 + factor * a2;
|
||
let red = (1.0 - factor) * r1 + factor * r2;
|
||
let green = (1.0 - factor) * g1 + factor * g2;
|
||
let blue = (1.0 - factor) * b1 + factor * b2;
|
||
|
||
// Convert back from premultiplied alpha
|
||
if alpha > 0.0 {
|
||
Color::from_argb_u8(
|
||
(alpha * 255.0) as u8,
|
||
(red / alpha).min(255.0) as u8,
|
||
(green / alpha).min(255.0) as u8,
|
||
(blue / alpha).min(255.0) as u8,
|
||
)
|
||
} else {
|
||
Color::from_argb_u8(0, 0, 0, 0)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// C FFI function to normalize the gradient stops to be within [0, 1] range
|
||
#[cfg(feature = "ffi")]
|
||
#[unsafe(no_mangle)]
|
||
pub extern "C" fn slint_conic_gradient_normalize_stops(gradient: &mut ConicGradientBrush) {
|
||
gradient.normalize_stops();
|
||
}
|
||
|
||
/// C FFI function to apply rotation to a ConicGradientBrush
|
||
#[cfg(feature = "ffi")]
|
||
#[unsafe(no_mangle)]
|
||
pub extern "C" fn slint_conic_gradient_apply_rotation(
|
||
gradient: &mut ConicGradientBrush,
|
||
angle_degrees: f32,
|
||
) {
|
||
gradient.apply_rotation(angle_degrees);
|
||
}
|
||
|
||
/// Compare two brushes using Rust's render-equivalent equality.
|
||
#[cfg(feature = "ffi")]
|
||
#[unsafe(no_mangle)]
|
||
pub extern "C" fn slint_brush_compare_equal(brush1: &Brush, brush2: &Brush) -> bool {
|
||
brush1.eq(brush2)
|
||
}
|
||
|
||
/// GradientStop describes a single color stop in a gradient. The colors between multiple
|
||
/// stops are interpolated.
|
||
#[repr(C)]
|
||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||
pub struct GradientStop {
|
||
/// The color to draw at this stop.
|
||
pub color: Color,
|
||
/// The position of this stop on the entire shape, as a normalized value between 0 and 1.
|
||
pub position: f32,
|
||
}
|
||
|
||
/// Returns the start / end points of a gradient within a rectangle of the given size, based on the angle (in degree).
|
||
pub fn line_for_angle(angle: f32, size: Size2D<f32>) -> (Point2D<f32>, Point2D<f32>) {
|
||
let angle = (angle + 90.).to_radians();
|
||
let (s, c) = angle.sin_cos();
|
||
|
||
let (a, b) = if s.abs() < f32::EPSILON {
|
||
let y = size.height / 2.;
|
||
return if c < 0. {
|
||
(Point2D::new(0., y), Point2D::new(size.width, y))
|
||
} else {
|
||
(Point2D::new(size.width, y), Point2D::new(0., y))
|
||
};
|
||
} else if c * s < 0. {
|
||
// Intersection between the gradient line, and an orthogonal line that goes through (height, 0)
|
||
let x = (s * size.width + c * size.height) * s / 2.;
|
||
let y = -c * x / s + size.height;
|
||
(Point2D::new(x, y), Point2D::new(size.width - x, size.height - y))
|
||
} else {
|
||
// Intersection between the gradient line, and an orthogonal line that goes through (0, 0)
|
||
let x = (s * size.width - c * size.height) * s / 2.;
|
||
let y = -c * x / s;
|
||
(Point2D::new(size.width - x, size.height - y), Point2D::new(x, y))
|
||
};
|
||
|
||
if s > 0. { (a, b) } else { (b, a) }
|
||
}
|
||
|
||
impl InterpolatedPropertyValue for Brush {
|
||
fn interpolate(&self, target_value: &Self, t: f32) -> Self {
|
||
match (self, target_value) {
|
||
(Brush::SolidColor(source_col), Brush::SolidColor(target_col)) => {
|
||
Brush::SolidColor(source_col.interpolate(target_col, t))
|
||
}
|
||
(Brush::SolidColor(col), Brush::LinearGradient(grad)) => {
|
||
let mut new_grad = grad.clone();
|
||
for x in new_grad.0.make_mut_slice().iter_mut().skip(1) {
|
||
x.color = col.interpolate(&x.color, t);
|
||
}
|
||
Brush::LinearGradient(new_grad)
|
||
}
|
||
(a @ Brush::LinearGradient(_), b @ Brush::SolidColor(_)) => {
|
||
Self::interpolate(b, a, 1. - t)
|
||
}
|
||
(Brush::LinearGradient(lhs), Brush::LinearGradient(rhs)) => {
|
||
if lhs.0.len() < rhs.0.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();
|
||
{
|
||
let angle = &mut iter.next().unwrap().position;
|
||
*angle = angle.interpolate(&rhs.angle(), t);
|
||
}
|
||
for s2 in rhs.stops() {
|
||
let s1 = iter.next().unwrap();
|
||
s1.color = s1.color.interpolate(&s2.color, t);
|
||
s1.position = s1.position.interpolate(&s2.position, t);
|
||
}
|
||
for x in iter {
|
||
x.position = x.position.interpolate(&1.0, t);
|
||
}
|
||
Brush::LinearGradient(new_grad)
|
||
}
|
||
}
|
||
(Brush::SolidColor(col), Brush::RadialGradient(grad)) => {
|
||
let mut new_grad = grad.clone();
|
||
for x in new_grad.0.make_mut_slice().iter_mut().skip(RadialGradientBrush::HEADER) {
|
||
x.color = col.interpolate(&x.color, t);
|
||
}
|
||
Brush::RadialGradient(new_grad)
|
||
}
|
||
(a @ Brush::RadialGradient(_), b @ Brush::SolidColor(_)) => {
|
||
Self::interpolate(b, a, 1. - t)
|
||
}
|
||
(Brush::RadialGradient(lhs), Brush::RadialGradient(rhs)) => {
|
||
if lhs.0.len() < rhs.0.len() {
|
||
Self::interpolate(target_value, self, 1. - t)
|
||
} else {
|
||
let mut new_grad = lhs.clone();
|
||
{
|
||
let s = new_grad.0.make_mut_slice();
|
||
// Center: interpolate when both sides are explicit. When one side is the
|
||
// default (NaN), lhs wins for t < 1 and snaps to rhs at t == 1.
|
||
if !lhs.center_x().is_nan() && !rhs.center_x().is_nan() {
|
||
s[0].position = lhs.center_x().interpolate(&rhs.center_x(), t);
|
||
s[1].position = lhs.center_y().interpolate(&rhs.center_y(), t);
|
||
} else if t >= 1.0 {
|
||
s[0].position = rhs.center_x();
|
||
s[1].position = rhs.center_y();
|
||
}
|
||
// Radius: same snap behavior when one side is the default (negative).
|
||
if lhs.radius() >= 0.0 && rhs.radius() >= 0.0 {
|
||
s[2].position = lhs.radius().interpolate(&rhs.radius(), t);
|
||
} else if t >= 1.0 {
|
||
s[2].position = rhs.radius();
|
||
}
|
||
let mut rhs_stops = rhs.stops();
|
||
let mut iter = s.iter_mut().skip(RadialGradientBrush::HEADER);
|
||
let mut last_color = Color::default();
|
||
for s2 in &mut rhs_stops {
|
||
let s1 = iter.next().unwrap();
|
||
last_color = s2.color;
|
||
s1.color = s1.color.interpolate(&s2.color, t);
|
||
s1.position = s1.position.interpolate(&s2.position, t);
|
||
}
|
||
for x in iter {
|
||
x.position = x.position.interpolate(&1.0, t);
|
||
x.color = x.color.interpolate(&last_color, t);
|
||
}
|
||
}
|
||
Brush::RadialGradient(new_grad)
|
||
}
|
||
}
|
||
(Brush::SolidColor(col), Brush::ConicGradient(grad)) => {
|
||
let mut new_grad = grad.clone();
|
||
for x in new_grad.0.make_mut_slice().iter_mut().skip(ConicGradientBrush::HEADER) {
|
||
x.color = col.interpolate(&x.color, t);
|
||
}
|
||
Brush::ConicGradient(new_grad)
|
||
}
|
||
(a @ Brush::ConicGradient(_), b @ Brush::SolidColor(_)) => {
|
||
Self::interpolate(b, a, 1. - t)
|
||
}
|
||
(Brush::ConicGradient(lhs), Brush::ConicGradient(rhs)) => {
|
||
if lhs.0.len() < rhs.0.len() {
|
||
Self::interpolate(target_value, self, 1. - t)
|
||
} else {
|
||
let mut new_grad = lhs.clone();
|
||
{
|
||
let s = new_grad.0.make_mut_slice();
|
||
// angle (s[0])
|
||
s[0].position = lhs.angle().interpolate(&rhs.angle(), t);
|
||
// Center: interpolate when both sides are explicit. When one side is the
|
||
// default (NaN), lhs wins for t < 1 and snaps to rhs at t == 1.
|
||
if !lhs.center_x().is_nan() && !rhs.center_x().is_nan() {
|
||
s[1].position = lhs.center_x().interpolate(&rhs.center_x(), t);
|
||
s[2].position = lhs.center_y().interpolate(&rhs.center_y(), t);
|
||
} else if t >= 1.0 {
|
||
s[1].position = rhs.center_x();
|
||
s[2].position = rhs.center_y();
|
||
}
|
||
let mut rhs_stops = rhs.stops();
|
||
let mut iter = s.iter_mut().skip(ConicGradientBrush::HEADER);
|
||
for s2 in &mut rhs_stops {
|
||
let s1 = iter.next().unwrap();
|
||
s1.color = s1.color.interpolate(&s2.color, t);
|
||
s1.position = s1.position.interpolate(&s2.position, t);
|
||
}
|
||
for x in iter {
|
||
x.position = x.position.interpolate(&1.0, t);
|
||
}
|
||
}
|
||
Brush::ConicGradient(new_grad)
|
||
}
|
||
}
|
||
(a @ Brush::LinearGradient(_), b @ Brush::RadialGradient(_))
|
||
| (a @ Brush::RadialGradient(_), b @ Brush::LinearGradient(_))
|
||
| (a @ Brush::LinearGradient(_), b @ Brush::ConicGradient(_))
|
||
| (a @ Brush::ConicGradient(_), b @ Brush::LinearGradient(_))
|
||
| (a @ Brush::RadialGradient(_), b @ Brush::ConicGradient(_))
|
||
| (a @ Brush::ConicGradient(_), b @ Brush::RadialGradient(_)) => {
|
||
// Just go to an intermediate color.
|
||
let color = Color::interpolate(&b.color(), &a.color(), t);
|
||
if t < 0.5 {
|
||
Self::interpolate(a, &Brush::SolidColor(color), t * 2.)
|
||
} else {
|
||
Self::interpolate(&Brush::SolidColor(color), b, (t - 0.5) * 2.)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A [`Brush`] resolved by [`resolve_brush`]: gradient geometry in physical pixels
|
||
/// plus sanitized color stops, so that all renderers share one interpretation of
|
||
/// the brush model.
|
||
#[derive(Clone, Debug, PartialEq)]
|
||
pub enum ResolvedBrush<'a> {
|
||
/// A plain color fill.
|
||
SolidColor(Color),
|
||
/// See [`ResolvedLinearGradient`].
|
||
LinearGradient(ResolvedLinearGradient<'a>),
|
||
/// See [`ResolvedRadialGradient`].
|
||
RadialGradient(ResolvedRadialGradient<'a>),
|
||
/// See [`ResolvedConicGradient`].
|
||
ConicGradient(ResolvedConicGradient<'a>),
|
||
}
|
||
|
||
/// A linear gradient whose stops span the line from `start` to `end`.
|
||
#[derive(Clone, Debug, PartialEq)]
|
||
pub struct ResolvedLinearGradient<'a> {
|
||
/// The point stop position 0 lies on.
|
||
pub start: euclid::Point2D<f32, PhysicalPx>,
|
||
/// The point stop position 1 lies on.
|
||
pub end: euclid::Point2D<f32, PhysicalPx>,
|
||
/// The sanitized color stops.
|
||
pub stops: Cow<'a, [GradientStop]>,
|
||
}
|
||
|
||
/// A radial gradient whose stops span from `center` to `radius`.
|
||
#[derive(Clone, Debug, PartialEq)]
|
||
pub struct ResolvedRadialGradient<'a> {
|
||
/// The center of the gradient.
|
||
pub center: euclid::Point2D<f32, PhysicalPx>,
|
||
/// The radius stop position 1 lies on.
|
||
pub radius: euclid::Length<f32, PhysicalPx>,
|
||
/// The sanitized color stops.
|
||
pub stops: Cow<'a, [GradientStop]>,
|
||
}
|
||
|
||
/// A conic gradient whose stops run one full clockwise turn around `center`,
|
||
/// starting at 12 o'clock (Slint's 0°).
|
||
#[derive(Clone, Debug, PartialEq)]
|
||
pub struct ResolvedConicGradient<'a> {
|
||
/// The center of the gradient.
|
||
pub center: euclid::Point2D<f32, PhysicalPx>,
|
||
/// The sanitized color stops.
|
||
pub stops: Cow<'a, [GradientStop]>,
|
||
}
|
||
|
||
/// Resolves a brush against the shape it fills: `size` is the shape's size in
|
||
/// physical pixels; explicit gradient center and radius values are local logical
|
||
/// lengths converted through `scale_factor`. Returns `None` for a fully
|
||
/// transparent brush.
|
||
///
|
||
/// The returned stops are canonical - sorted, strictly increasing, within [0, 1] -
|
||
/// with out-of-range linear/radial stops folded into the gradient geometry and
|
||
/// conic stops clamped (a conic gradient cannot extend past a full turn), so they
|
||
/// are directly usable by backends that render non-canonical stops incorrectly
|
||
/// (vello draws them as a solid fill of the first color). Already-canonical stops,
|
||
/// which is all the compiler produces, are borrowed rather than copied.
|
||
///
|
||
/// A free function rather than a `Brush` method so that it stays out of the public
|
||
/// API that `Brush` is re-exported into.
|
||
pub fn resolve_brush<'a>(
|
||
brush: &'a Brush,
|
||
size: euclid::Size2D<f32, PhysicalPx>,
|
||
scale_factor: ScaleFactor,
|
||
) -> Option<ResolvedBrush<'a>> {
|
||
if brush.is_transparent() {
|
||
return None;
|
||
}
|
||
Some(match brush {
|
||
Brush::SolidColor(color) => ResolvedBrush::SolidColor(*color),
|
||
Brush::LinearGradient(gradient) => {
|
||
let (stops, extent) = sanitize_color_stops(gradient.stops_slice(), true);
|
||
let (start, mut end) = line_for_angle(gradient.angle(), size.to_untyped());
|
||
if extent != 1.0 {
|
||
// sanitize_color_stops scaled the offsets down into [0, 1]; scale
|
||
// the gradient line to match, or the ramp comes out compressed.
|
||
end = start + (end - start) * extent;
|
||
}
|
||
ResolvedBrush::LinearGradient(ResolvedLinearGradient {
|
||
start: start.cast_unit(),
|
||
end: end.cast_unit(),
|
||
stops,
|
||
})
|
||
}
|
||
Brush::RadialGradient(gradient) => {
|
||
let (stops, extent) = sanitize_color_stops(gradient.stops_slice(), true);
|
||
let (center_x, center_y) =
|
||
gradient.center_or_default_scaled(size.width, size.height, scale_factor.get());
|
||
let radius =
|
||
gradient.radius_or_default_scaled(size.width, size.height, scale_factor.get())
|
||
* extent;
|
||
ResolvedBrush::RadialGradient(ResolvedRadialGradient {
|
||
center: euclid::point2(center_x, center_y),
|
||
radius: euclid::Length::new(radius),
|
||
stops,
|
||
})
|
||
}
|
||
Brush::ConicGradient(gradient) => {
|
||
let (stops, _) = sanitize_color_stops(gradient.stops_slice(), false);
|
||
let (center_x, center_y) =
|
||
gradient.center_or_default_scaled(size.width, size.height, scale_factor.get());
|
||
ResolvedBrush::ConicGradient(ResolvedConicGradient {
|
||
center: euclid::point2(center_x, center_y),
|
||
stops,
|
||
})
|
||
}
|
||
})
|
||
}
|
||
|
||
/// Massage gradient color stops into a canonical form (see [`resolve_brush`]).
|
||
/// Stops below 0 are replaced by the interpolated color at 0 (the CSS behavior).
|
||
/// For stops beyond 1: with `can_extend`, all positions are divided by the maximum
|
||
/// and that maximum is returned as the second tuple element, so the caller can grow
|
||
/// the gradient geometry by the same factor; without it, they are clamped to the
|
||
/// interpolated color at 1. Duplicate positions (hard color steps) are separated by
|
||
/// the smallest representable amount.
|
||
fn sanitize_color_stops(
|
||
stops: &[GradientStop],
|
||
can_extend: bool,
|
||
) -> (Cow<'_, [GradientStop]>, f32) {
|
||
/// Plain per-channel interpolation, matching how the gradient ramp itself blends.
|
||
fn color_at(position: f32, a: &GradientStop, b: &GradientStop) -> Color {
|
||
let t = if b.position > a.position {
|
||
((position - a.position) / (b.position - a.position)).clamp(0., 1.)
|
||
} else {
|
||
0.
|
||
};
|
||
let (ca, cb) = (a.color.to_argb_u8(), b.color.to_argb_u8());
|
||
let lerp = |x: u8, y: u8| (x as f32 + (y as f32 - x as f32) * t) as u8;
|
||
Color::from_argb_u8(
|
||
lerp(ca.alpha, cb.alpha),
|
||
lerp(ca.red, cb.red),
|
||
lerp(ca.green, cb.green),
|
||
lerp(ca.blue, cb.blue),
|
||
)
|
||
}
|
||
|
||
// The compiler always produces canonical stop lists, so this fast path is the
|
||
// common case. A NaN position fails these comparisons: slow path.
|
||
if stops.first().is_none_or(|first| first.position >= 0.)
|
||
&& stops.last().is_none_or(|last| last.position <= 1.)
|
||
&& stops.windows(2).all(|pair| pair[0].position < pair[1].position)
|
||
{
|
||
return (Cow::Borrowed(stops), 1.0);
|
||
}
|
||
|
||
let mut stops: alloc::vec::Vec<GradientStop> = stops.to_vec();
|
||
stops.sort_by(|a, b| a.position.total_cmp(&b.position));
|
||
|
||
// Replace everything below 0 with the interpolated color at 0.
|
||
while stops.len() >= 2 && stops[1].position <= 0. {
|
||
stops.remove(0);
|
||
}
|
||
if let [first, second, ..] = stops.as_slice()
|
||
&& first.position < 0.
|
||
{
|
||
stops[0] = GradientStop { color: color_at(0., first, second), position: 0. };
|
||
} else if let [only] = stops.as_slice()
|
||
&& only.position < 0.
|
||
{
|
||
stops[0].position = 0.;
|
||
}
|
||
|
||
// Handle stops beyond 1: normalize (the caller extends the geometry) or clamp to
|
||
// the interpolated color at 1.
|
||
let mut extent = 1.0f32;
|
||
if stops.last().is_some_and(|last| last.position > 1.) {
|
||
if can_extend {
|
||
extent = stops.last().unwrap().position;
|
||
for stop in &mut stops {
|
||
stop.position /= extent;
|
||
}
|
||
} else {
|
||
while stops.len() >= 2 && stops[stops.len() - 2].position >= 1. {
|
||
stops.pop();
|
||
}
|
||
let clamped_last = match stops.as_slice() {
|
||
[.., second_to_last, last] if last.position > 1. => {
|
||
Some(GradientStop { color: color_at(1., second_to_last, last), position: 1. })
|
||
}
|
||
[only] if only.position > 1. => {
|
||
Some(GradientStop { color: only.color, position: 1. })
|
||
}
|
||
_ => None,
|
||
};
|
||
if let Some(stop) = clamped_last {
|
||
*stops.last_mut().unwrap() = stop;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Make positions strictly increasing: separate duplicates (hard color steps) by the
|
||
// smallest representable amount, then push back anything that got nudged past 1.
|
||
let mut previous = f32::NEG_INFINITY;
|
||
for stop in &mut stops {
|
||
if stop.position <= previous {
|
||
stop.position = previous.next_up();
|
||
}
|
||
previous = stop.position;
|
||
}
|
||
let mut next = 1.0f32.next_up();
|
||
for stop in stops.iter_mut().rev() {
|
||
if stop.position >= next {
|
||
stop.position = next.next_down();
|
||
}
|
||
next = stop.position;
|
||
}
|
||
|
||
(Cow::Owned(stops), extent)
|
||
}
|
||
|
||
#[test]
|
||
fn test_resolve_sanitizes_out_of_range_stops() {
|
||
// Stops beyond 1 extend the gradient line instead of being clamped.
|
||
let brush = Brush::LinearGradient(LinearGradientBrush::new(
|
||
180.,
|
||
[
|
||
GradientStop { position: 0.0, color: Color::from_rgb_u8(255, 0, 0) },
|
||
GradientStop { position: 2.0, color: Color::from_rgb_u8(0, 0, 255) },
|
||
],
|
||
));
|
||
let Some(ResolvedBrush::LinearGradient(gradient)) =
|
||
resolve_brush(&brush, [100., 50.].into(), ScaleFactor::new(1.0))
|
||
else {
|
||
panic!("expected a resolved linear gradient");
|
||
};
|
||
assert_eq!(gradient.stops.last().unwrap().position, 1.0);
|
||
// A 180° gradient runs from the top edge down; the end extends past the shape.
|
||
assert_eq!(gradient.start.y, 0.);
|
||
assert_eq!(gradient.end.y, 100.);
|
||
|
||
// A conic gradient cannot extend, so out-of-range stops are clamped to the
|
||
// interpolated color at position 1 instead. Note that ConicGradientBrush::new
|
||
// already normalizes, so resolving keeps its stops within [0, 1].
|
||
let brush = Brush::ConicGradient(ConicGradientBrush::new(
|
||
0.,
|
||
[
|
||
GradientStop { position: 0.0, color: Color::from_rgb_u8(255, 0, 0) },
|
||
GradientStop { position: 2.0, color: Color::from_rgb_u8(0, 0, 255) },
|
||
],
|
||
));
|
||
let Some(ResolvedBrush::ConicGradient(gradient)) =
|
||
resolve_brush(&brush, [100., 50.].into(), ScaleFactor::new(1.0))
|
||
else {
|
||
panic!("expected a resolved conic gradient");
|
||
};
|
||
assert!(gradient.stops.iter().all(|stop| (0. ..=1.).contains(&stop.position)));
|
||
assert_eq!(gradient.center, euclid::point2(50., 25.));
|
||
}
|
||
|
||
#[test]
|
||
fn test_resolve_makes_stops_strictly_increasing() {
|
||
let brush = Brush::LinearGradient(LinearGradientBrush::new(
|
||
0.,
|
||
[
|
||
GradientStop { position: 0.5, color: Color::from_rgb_u8(255, 0, 0) },
|
||
GradientStop { position: 0.5, color: Color::from_rgb_u8(0, 255, 0) },
|
||
GradientStop { position: 0.2, color: Color::from_rgb_u8(0, 0, 255) },
|
||
],
|
||
));
|
||
let Some(ResolvedBrush::LinearGradient(gradient)) =
|
||
resolve_brush(&brush, [100., 100.].into(), ScaleFactor::new(1.0))
|
||
else {
|
||
panic!("expected a resolved linear gradient");
|
||
};
|
||
// Sorted and strictly increasing: the duplicate hard step is separated minimally.
|
||
assert!(gradient.stops.windows(2).all(|pair| pair[0].position < pair[1].position));
|
||
assert_eq!(gradient.stops[0].color, Color::from_rgb_u8(0, 0, 255));
|
||
}
|
||
|
||
#[test]
|
||
fn test_resolve_replaces_stops_below_zero() {
|
||
let brush = Brush::LinearGradient(LinearGradientBrush::new(
|
||
0.,
|
||
[
|
||
GradientStop { position: -1.0, color: Color::from_rgb_u8(0, 0, 0) },
|
||
GradientStop { position: 1.0, color: Color::from_rgb_u8(200, 200, 200) },
|
||
],
|
||
));
|
||
let Some(ResolvedBrush::LinearGradient(gradient)) =
|
||
resolve_brush(&brush, [100., 100.].into(), ScaleFactor::new(1.0))
|
||
else {
|
||
panic!("expected a resolved linear gradient");
|
||
};
|
||
// The first stop is replaced by the interpolated color at position 0.
|
||
assert_eq!(gradient.stops[0].position, 0.0);
|
||
assert_eq!(gradient.stops[0].color, Color::from_rgb_u8(100, 100, 100));
|
||
}
|
||
|
||
#[test]
|
||
fn test_resolve_transparent_brush() {
|
||
assert_eq!(resolve_brush(&Brush::default(), [100., 100.].into(), ScaleFactor::new(1.0)), None);
|
||
}
|
||
|
||
#[test]
|
||
fn test_resolve_borrows_canonical_stops() {
|
||
// Already canonical stops are borrowed from the brush, not copied.
|
||
let brush = Brush::LinearGradient(LinearGradientBrush::new(
|
||
90.,
|
||
[
|
||
GradientStop { position: 0.0, color: Color::from_rgb_u8(255, 0, 0) },
|
||
GradientStop { position: 1.0, color: Color::from_rgb_u8(0, 0, 255) },
|
||
],
|
||
));
|
||
let Some(ResolvedBrush::LinearGradient(gradient)) =
|
||
resolve_brush(&brush, [100., 100.].into(), ScaleFactor::new(1.0))
|
||
else {
|
||
panic!("expected a resolved linear gradient");
|
||
};
|
||
assert!(matches!(gradient.stops, Cow::Borrowed(_)));
|
||
assert_eq!(gradient.stops.len(), 2);
|
||
}
|
||
|
||
#[test]
|
||
#[allow(clippy::float_cmp)] // We want bit-wise equality here
|
||
fn test_linear_gradient_encoding() {
|
||
let stops: SharedVector<GradientStop> = [
|
||
GradientStop { position: 0.0, color: Color::from_argb_u8(255, 255, 0, 0) },
|
||
GradientStop { position: 0.5, color: Color::from_argb_u8(255, 0, 255, 0) },
|
||
GradientStop { position: 1.0, color: Color::from_argb_u8(255, 0, 0, 255) },
|
||
]
|
||
.into();
|
||
let grad = LinearGradientBrush::new(256., stops.clone());
|
||
assert_eq!(grad.angle(), 256.);
|
||
assert!(grad.stops().eq(stops.iter()));
|
||
}
|
||
|
||
#[test]
|
||
fn test_conic_gradient_basic() {
|
||
// Test basic conic gradient with no rotation
|
||
let grad = ConicGradientBrush::new(
|
||
0.0,
|
||
[
|
||
GradientStop { position: 0.0, color: Color::from_rgb_u8(255, 0, 0) },
|
||
GradientStop { position: 0.5, color: Color::from_rgb_u8(0, 255, 0) },
|
||
GradientStop { position: 1.0, color: Color::from_rgb_u8(255, 0, 0) },
|
||
],
|
||
);
|
||
assert_eq!(grad.angle(), 0.0);
|
||
assert_eq!(grad.stops().count(), 3);
|
||
}
|
||
|
||
#[test]
|
||
fn test_conic_gradient_with_rotation() {
|
||
// Test conic gradient with 90 degree rotation
|
||
let grad = ConicGradientBrush::new(
|
||
90.0,
|
||
[
|
||
GradientStop { position: 0.0, color: Color::from_rgb_u8(255, 0, 0) },
|
||
GradientStop { position: 1.0, color: Color::from_rgb_u8(255, 0, 0) },
|
||
],
|
||
);
|
||
assert_eq!(grad.angle(), 90.0);
|
||
// After rotation, stops should still be present and sorted
|
||
assert!(grad.stops().count() >= 2);
|
||
}
|
||
|
||
#[test]
|
||
fn test_conic_gradient_negative_angle() {
|
||
// Test with negative angle - should be normalized
|
||
let grad = ConicGradientBrush::new(
|
||
-90.0,
|
||
[GradientStop { position: 0.5, color: Color::from_rgb_u8(255, 0, 0) }],
|
||
);
|
||
assert_eq!(grad.angle(), -90.0); // Angle is stored as-is
|
||
assert!(grad.stops().count() >= 2); // Should have boundary stops added
|
||
}
|
||
|
||
#[test]
|
||
fn test_conic_gradient_stops_outside_range() {
|
||
// Test with stops outside [0, 1] range
|
||
let grad = ConicGradientBrush::new(
|
||
0.0,
|
||
[
|
||
GradientStop { position: -0.2, color: Color::from_rgb_u8(255, 0, 0) },
|
||
GradientStop { position: 0.5, color: Color::from_rgb_u8(0, 255, 0) },
|
||
GradientStop { position: 1.2, color: Color::from_rgb_u8(0, 0, 255) },
|
||
],
|
||
);
|
||
// All stops should be within [0, 1] after processing
|
||
for stop in grad.stops() {
|
||
assert!(stop.position >= 0.0 && stop.position <= 1.0);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_conic_gradient_all_stops_below_zero() {
|
||
// Test edge case: all stops are below 0
|
||
let grad = ConicGradientBrush::new(
|
||
0.0,
|
||
[
|
||
GradientStop { position: -0.5, color: Color::from_rgb_u8(255, 0, 0) },
|
||
GradientStop { position: -0.3, color: Color::from_rgb_u8(0, 255, 0) },
|
||
],
|
||
);
|
||
// Should create valid boundary stops
|
||
assert!(grad.stops().count() >= 2);
|
||
// First stop should be at or near 0.0
|
||
let first = grad.stops().next().unwrap();
|
||
assert!(first.position >= 0.0 && first.position < 0.1);
|
||
}
|
||
|
||
#[test]
|
||
fn test_conic_gradient_all_stops_above_one() {
|
||
// Test edge case: all stops are above 1
|
||
let grad = ConicGradientBrush::new(
|
||
0.0,
|
||
[
|
||
GradientStop { position: 1.2, color: Color::from_rgb_u8(255, 0, 0) },
|
||
GradientStop { position: 1.5, color: Color::from_rgb_u8(0, 255, 0) },
|
||
],
|
||
);
|
||
// Should create valid boundary stops
|
||
assert!(grad.stops().count() >= 2);
|
||
// Last stop should be at or near 1.0
|
||
let last = grad.stops().last().unwrap();
|
||
assert!(last.position > 0.9 && last.position <= 1.0);
|
||
}
|
||
|
||
#[test]
|
||
fn test_conic_gradient_empty() {
|
||
// Test edge case: no stops provided
|
||
let grad = ConicGradientBrush::new(0.0, []);
|
||
// Should create default transparent stops
|
||
assert_eq!(grad.stops().count(), 2);
|
||
}
|
||
|
||
#[test]
|
||
fn test_radial_gradient_preserves_center_on_brighter() {
|
||
let grad = RadialGradientBrush::new_circle([
|
||
GradientStop { position: 0.0, color: Color::from_rgb_u8(200, 100, 50) },
|
||
GradientStop { position: 1.0, color: Color::from_rgb_u8(50, 200, 100) },
|
||
])
|
||
.with_center(10.0, 20.0)
|
||
.with_radius(30.0);
|
||
let brighter = Brush::RadialGradient(grad.clone()).brighter(0.5);
|
||
if let Brush::RadialGradient(b) = brighter {
|
||
assert_eq!(b.center_x(), 10.0);
|
||
assert_eq!(b.center_y(), 20.0);
|
||
assert_eq!(b.radius(), 30.0);
|
||
} else {
|
||
panic!("Expected RadialGradient");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_radial_gradient_default_center() {
|
||
let grad = RadialGradientBrush::new_circle([]);
|
||
assert!(grad.center_x().is_nan());
|
||
assert!(grad.center_y().is_nan());
|
||
assert!(grad.radius() < 0.0);
|
||
assert_eq!(grad.center_or_default(100.0, 80.0), (50.0, 40.0));
|
||
assert!((grad.radius_or_default(60.0, 80.0) - 50.0).abs() < 0.01);
|
||
}
|
||
|
||
#[test]
|
||
fn test_radial_gradient_scaled_explicit_values() {
|
||
let grad = RadialGradientBrush::new_circle([]).with_center(10.0, 20.0).with_radius(30.0);
|
||
|
||
assert_eq!(grad.center_or_default_scaled(200.0, 160.0, 2.0), (20.0, 40.0));
|
||
assert_eq!(grad.radius_or_default_scaled(200.0, 160.0, 2.0), 60.0);
|
||
}
|
||
|
||
#[test]
|
||
fn test_radial_gradient_scaled_defaults_use_physical_frame() {
|
||
let grad = RadialGradientBrush::new_circle([]);
|
||
|
||
assert_eq!(grad.center_or_default_scaled(200.0, 160.0, 2.0), (100.0, 80.0));
|
||
assert!((grad.radius_or_default_scaled(120.0, 160.0, 2.0) - 100.0).abs() < 0.01);
|
||
}
|
||
|
||
#[test]
|
||
fn test_radial_gradient_interpolation_reaches_explicit_metadata() {
|
||
let source = Brush::RadialGradient(RadialGradientBrush::new_circle([
|
||
GradientStop { position: 0.0, color: Color::from_rgb_u8(0, 0, 0) },
|
||
GradientStop { position: 1.0, color: Color::from_rgb_u8(255, 255, 255) },
|
||
]));
|
||
let target_grad = RadialGradientBrush::new_circle([
|
||
GradientStop { position: 0.0, color: Color::from_rgb_u8(0, 0, 0) },
|
||
GradientStop { position: 1.0, color: Color::from_rgb_u8(255, 255, 255) },
|
||
])
|
||
.with_center(10.0, 20.0)
|
||
.with_radius(30.0);
|
||
let target = Brush::RadialGradient(target_grad.clone());
|
||
|
||
if let Brush::RadialGradient(result) = source.interpolate(&target, 1.0) {
|
||
assert_eq!(result.center_x(), target_grad.center_x());
|
||
assert_eq!(result.center_y(), target_grad.center_y());
|
||
assert_eq!(result.radius(), target_grad.radius());
|
||
} else {
|
||
panic!("Expected RadialGradient");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_radial_gradient_interpolation_reaches_default_metadata() {
|
||
let source_grad = RadialGradientBrush::new_circle([
|
||
GradientStop { position: 0.0, color: Color::from_rgb_u8(0, 0, 0) },
|
||
GradientStop { position: 1.0, color: Color::from_rgb_u8(255, 255, 255) },
|
||
])
|
||
.with_center(10.0, 20.0)
|
||
.with_radius(30.0);
|
||
let source = Brush::RadialGradient(source_grad);
|
||
let target = Brush::RadialGradient(RadialGradientBrush::new_circle([
|
||
GradientStop { position: 0.0, color: Color::from_rgb_u8(0, 0, 0) },
|
||
GradientStop { position: 1.0, color: Color::from_rgb_u8(255, 255, 255) },
|
||
]));
|
||
|
||
if let Brush::RadialGradient(result) = source.interpolate(&target, 1.0) {
|
||
assert!(result.center_x().is_nan());
|
||
assert!(result.center_y().is_nan());
|
||
assert!(result.radius() < 0.0);
|
||
} else {
|
||
panic!("Expected RadialGradient");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_conic_gradient_interpolation_reaches_explicit_center() {
|
||
let source = Brush::ConicGradient(ConicGradientBrush::new(
|
||
0.0,
|
||
[
|
||
GradientStop { position: 0.0, color: Color::from_rgb_u8(0, 0, 0) },
|
||
GradientStop { position: 1.0, color: Color::from_rgb_u8(255, 255, 255) },
|
||
],
|
||
));
|
||
let target_grad = ConicGradientBrush::new(
|
||
0.0,
|
||
[
|
||
GradientStop { position: 0.0, color: Color::from_rgb_u8(0, 0, 0) },
|
||
GradientStop { position: 1.0, color: Color::from_rgb_u8(255, 255, 255) },
|
||
],
|
||
)
|
||
.with_center(40.0, 50.0);
|
||
let target = Brush::ConicGradient(target_grad.clone());
|
||
|
||
if let Brush::ConicGradient(result) = source.interpolate(&target, 1.0) {
|
||
assert_eq!(result.center_x(), target_grad.center_x());
|
||
assert_eq!(result.center_y(), target_grad.center_y());
|
||
} else {
|
||
panic!("Expected ConicGradient");
|
||
}
|
||
}
|