use std::rc::Rc;
use frust::authoring::PaintScene;
use kurbo::{Point, Shape, Size};
use peniko::{
Brush, Color, ColorStop, Gradient, GradientKind, LinearGradientPosition,
RadialGradientPosition, SweepGradientPosition,
};
use crate::interaction::DISABLED_CONTAINER_OPACITY;
use super::core::{PATH_TOLERANCE, inset_stroke_rect};
use super::{ButtonDecoration, ButtonSurface, DecorationOutcome};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GradientAlignment {
pub x: f64,
pub y: f64,
}
impl GradientAlignment {
pub const CENTER_LEFT: Self = Self { x: -1.0, y: 0.0 };
pub const CENTER_RIGHT: Self = Self { x: 1.0, y: 0.0 };
pub const CENTER: Self = Self { x: 0.0, y: 0.0 };
pub fn within_rect(&self, origin: Point, size: Size) -> Point {
let half_w = size.width / 2.0;
let half_h = size.height / 2.0;
Point::new(
origin.x + half_w + self.x * half_w,
origin.y + half_h + self.y * half_h,
)
}
}
pub fn implied_stops(color_count: usize) -> Vec<f32> {
match color_count {
0 => Vec::new(),
1 => vec![0.0],
n => {
let separation = 1.0 / (n - 1) as f32;
(0..n).map(|i| i as f32 * separation).collect()
}
}
}
fn color_stops(colors: &[Color], stops: &Option<Vec<f32>>) -> Vec<ColorStop> {
let resolved = stops.clone().unwrap_or_else(|| implied_stops(colors.len()));
colors
.iter()
.zip(resolved)
.map(|(color, stop)| ColorStop::from((stop, *color)))
.collect()
}
#[derive(Clone, Debug, PartialEq)]
pub struct LinearGradientSpec {
pub begin: GradientAlignment,
pub end: GradientAlignment,
pub colors: Vec<Color>,
pub stops: Option<Vec<f32>>,
}
impl LinearGradientSpec {
pub fn new(colors: impl Into<Vec<Color>>) -> Self {
Self {
begin: GradientAlignment::CENTER_LEFT,
end: GradientAlignment::CENTER_RIGHT,
colors: colors.into(),
stops: None,
}
}
pub fn begin(mut self, begin: GradientAlignment) -> Self {
self.begin = begin;
self
}
pub fn end(mut self, end: GradientAlignment) -> Self {
self.end = end;
self
}
pub fn stops(mut self, stops: Vec<f32>) -> Self {
self.stops = Some(stops);
self
}
fn resolve(&self, origin: Point, size: Size) -> Gradient {
let start = self.begin.within_rect(origin, size);
let end = self.end.within_rect(origin, size);
Gradient::new_linear(start, end)
.with_stops(color_stops(&self.colors, &self.stops).as_slice())
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct RadialGradientSpec {
pub center: GradientAlignment,
pub radius: f64,
pub colors: Vec<Color>,
pub stops: Option<Vec<f32>>,
}
impl RadialGradientSpec {
pub fn new(colors: impl Into<Vec<Color>>) -> Self {
Self {
center: GradientAlignment::CENTER,
radius: 0.5,
colors: colors.into(),
stops: None,
}
}
pub fn center(mut self, center: GradientAlignment) -> Self {
self.center = center;
self
}
pub fn radius(mut self, radius: f64) -> Self {
self.radius = radius;
self
}
pub fn stops(mut self, stops: Vec<f32>) -> Self {
self.stops = Some(stops);
self
}
fn resolve(&self, origin: Point, size: Size) -> Gradient {
let center = self.center.within_rect(origin, size);
let shortest_side = size.width.min(size.height);
let radius = (self.radius * shortest_side) as f32;
Gradient::new_radial(center, radius)
.with_stops(color_stops(&self.colors, &self.stops).as_slice())
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct SweepGradientSpec {
pub center: GradientAlignment,
pub start_angle: f64,
pub end_angle: f64,
pub colors: Vec<Color>,
pub stops: Option<Vec<f32>>,
}
impl SweepGradientSpec {
pub fn new(colors: impl Into<Vec<Color>>) -> Self {
Self {
center: GradientAlignment::CENTER,
start_angle: 0.0,
end_angle: std::f64::consts::TAU,
colors: colors.into(),
stops: None,
}
}
pub fn center(mut self, center: GradientAlignment) -> Self {
self.center = center;
self
}
pub fn angles(mut self, start_angle: f64, end_angle: f64) -> Self {
self.start_angle = start_angle;
self.end_angle = end_angle;
self
}
pub fn stops(mut self, stops: Vec<f32>) -> Self {
self.stops = Some(stops);
self
}
fn resolve(&self, origin: Point, size: Size) -> Gradient {
let center = self.center.within_rect(origin, size);
Gradient::new_sweep(center, self.start_angle as f32, self.end_angle as f32)
.with_stops(color_stops(&self.colors, &self.stops).as_slice())
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum GradientSpec {
Linear(LinearGradientSpec),
Radial(RadialGradientSpec),
Sweep(SweepGradientSpec),
}
impl GradientSpec {
pub fn resolve(&self, origin: Point, size: Size) -> Gradient {
match self {
Self::Linear(spec) => spec.resolve(origin, size),
Self::Radial(spec) => spec.resolve(origin, size),
Self::Sweep(spec) => spec.resolve(origin, size),
}
}
}
impl From<LinearGradientSpec> for GradientSpec {
fn from(spec: LinearGradientSpec) -> Self {
Self::Linear(spec)
}
}
impl From<RadialGradientSpec> for GradientSpec {
fn from(spec: RadialGradientSpec) -> Self {
Self::Radial(spec)
}
}
impl From<SweepGradientSpec> for GradientSpec {
fn from(spec: SweepGradientSpec) -> Self {
Self::Sweep(spec)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct GradientStates {
pub hovered: bool,
pub focused: bool,
pub pressed: bool,
pub dragged: bool,
pub disabled: bool,
}
impl GradientStates {
pub fn from_surface(surface: &ButtonSurface) -> Self {
Self {
hovered: surface.state.hovered,
focused: surface.state.focused,
pressed: surface.state.pressed,
dragged: surface.state.dragged,
disabled: !surface.enabled,
}
}
}
pub type GradientProperty = Rc<dyn Fn(GradientStates) -> Option<GradientSpec>>;
pub fn constant_gradient(spec: impl Into<GradientSpec>) -> GradientProperty {
let spec = spec.into();
Rc::new(move |_states| Some(spec.clone()))
}
#[derive(Clone, Default)]
pub struct GradientButtonDecoration {
background: Option<GradientProperty>,
foreground: Option<GradientProperty>,
overlay: Option<GradientProperty>,
outline: Option<GradientProperty>,
}
impl GradientButtonDecoration {
pub fn new() -> Self {
Self::default()
}
pub fn background(mut self, property: GradientProperty) -> Self {
self.background = Some(property);
self
}
pub fn foreground(mut self, property: GradientProperty) -> Self {
self.foreground = Some(property);
self
}
pub fn overlay(mut self, property: GradientProperty) -> Self {
self.overlay = Some(property);
self
}
pub fn outline(mut self, property: GradientProperty) -> Self {
self.outline = Some(property);
self
}
}
impl ButtonDecoration for GradientButtonDecoration {
fn paint_fill(&self, surface: &ButtonSurface, scene: &mut dyn PaintScene) -> DecorationOutcome {
let Some(property) = &self.background else {
return DecorationOutcome::Skipped;
};
let Some(spec) = property(GradientStates::from_surface(surface)) else {
return DecorationOutcome::Skipped;
};
let mut gradient = spec.resolve(surface.origin, surface.size);
if !surface.enabled {
gradient = gradient.multiply_alpha(DISABLED_CONTAINER_OPACITY);
}
scene.fill_rounded_rect_brush(
surface.origin,
surface.size,
surface.radius,
&Brush::Gradient(gradient),
);
DecorationOutcome::Painted
}
fn paint_overlay(
&self,
surface: &ButtonSurface,
scene: &mut dyn PaintScene,
) -> DecorationOutcome {
let Some(property) = &self.overlay else {
return DecorationOutcome::Skipped;
};
if let Some(spec) = property(GradientStates::from_surface(surface)) {
let opacity = if surface.enabled {
surface.state.resolve_opacity()
} else {
0.0
};
if opacity > 0.0 {
let gradient = spec
.resolve(surface.origin, surface.size)
.multiply_alpha(opacity);
scene.fill_rounded_rect_brush(
surface.origin,
surface.size,
surface.radius,
&Brush::Gradient(gradient),
);
}
}
DecorationOutcome::Painted
}
fn paint_outline(
&self,
surface: &ButtonSurface,
scene: &mut dyn PaintScene,
) -> DecorationOutcome {
let Some(property) = &self.outline else {
return DecorationOutcome::Skipped;
};
let Some(spec) = property(GradientStates::from_surface(surface)) else {
return DecorationOutcome::Skipped;
};
if surface.outline_width <= 0.0 {
return DecorationOutcome::Skipped;
}
let gradient = spec.resolve(surface.origin, surface.size);
let path = inset_stroke_rect(surface.size, surface.radius, surface.outline_width)
.to_path(PATH_TOLERANCE);
scene.stroke_path(
surface.origin,
&path,
surface.outline_width,
&Brush::Gradient(gradient),
);
DecorationOutcome::Painted
}
fn foreground_brush(&self, surface: &ButtonSurface) -> Option<Brush> {
let property = self.foreground.as_ref()?;
let spec = property(GradientStates::from_surface(surface))?;
Some(Brush::Gradient(spec.resolve(surface.origin, surface.size)))
}
}
pub(super) fn brush_relative_to_run_origin(brush: &Brush, run_origin: Point) -> Brush {
let Brush::Gradient(gradient) = brush else {
return brush.clone();
};
let shift = |p: Point| Point::new(p.x - run_origin.x, p.y - run_origin.y);
let mut gradient = gradient.clone();
gradient.kind = match gradient.kind {
GradientKind::Linear(pos) => LinearGradientPosition {
start: shift(pos.start),
end: shift(pos.end),
}
.into(),
GradientKind::Radial(pos) => RadialGradientPosition {
start_center: shift(pos.start_center),
start_radius: pos.start_radius,
end_center: shift(pos.end_center),
end_radius: pos.end_radius,
}
.into(),
GradientKind::Sweep(pos) => SweepGradientPosition {
center: shift(pos.center),
start_angle: pos.start_angle,
end_angle: pos.end_angle,
}
.into(),
};
Brush::Gradient(gradient)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::button::{ButtonView, ButtonWidget, button};
use frust::authoring::scene::GlyphRun;
use frust::authoring::text::TextContext;
use frust::authoring::{BoxConstraints, BuildCtx, LayoutCtx, PaintCtx, View, Widget};
use kurbo::BezPath;
use peniko::{Extend, GradientKind};
use std::any::Any;
fn colors(hexes: &[u32]) -> Vec<Color> {
hexes
.iter()
.map(|&h| {
Color::from_rgb8(
((h >> 16) & 0xFF) as u8,
((h >> 8) & 0xFF) as u8,
(h & 0xFF) as u8,
)
})
.collect()
}
#[test]
fn implied_stops_matches_dart_uis_own_table() {
assert_eq!(implied_stops(0), Vec::<f32>::new());
assert_eq!(implied_stops(1), vec![0.0]);
assert_eq!(implied_stops(2), vec![0.0, 1.0]);
assert_eq!(implied_stops(3), vec![0.0, 0.5, 1.0]);
assert_eq!(implied_stops(4), vec![0.0, 1.0 / 3.0, 2.0 / 3.0, 1.0]);
}
#[test]
fn linear_gradient_resolves_begin_end_and_implied_stops_at_two_sizes() {
let spec = LinearGradientSpec::new(colors(&[0x6750A4, 0x9A82DB]));
for (origin, size) in [
(Point::new(0.0, 0.0), Size::new(100.0, 40.0)),
(Point::new(10.0, 20.0), Size::new(200.0, 80.0)),
] {
let gradient = spec.resolve(origin, size);
let GradientKind::Linear(pos) = gradient.kind else {
panic!("expected a linear gradient kind");
};
assert_eq!(
pos.start,
Point::new(origin.x, origin.y + size.height / 2.0)
);
assert_eq!(
pos.end,
Point::new(origin.x + size.width, origin.y + size.height / 2.0)
);
assert_eq!(gradient.extend, Extend::Pad);
let stops: Vec<(f32, Color)> = gradient
.stops
.iter()
.map(|s| (s.offset, s.color.to_alpha_color()))
.collect();
assert_eq!(
stops,
vec![(0.0, colors(&[0x6750A4])[0]), (1.0, colors(&[0x9A82DB])[0]),]
);
}
}
#[test]
fn linear_gradient_explicit_begin_end_and_stops_are_honored() {
let spec = LinearGradientSpec::new(colors(&[0xFF0000, 0x00FF00, 0x0000FF]))
.begin(GradientAlignment { x: 0.0, y: -1.0 })
.end(GradientAlignment { x: 0.0, y: 1.0 })
.stops(vec![0.0, 0.25, 1.0]);
let origin = Point::new(0.0, 0.0);
let size = Size::new(64.0, 32.0);
let gradient = spec.resolve(origin, size);
let GradientKind::Linear(pos) = gradient.kind else {
panic!("expected a linear gradient kind");
};
assert_eq!(pos.start, Point::new(32.0, 0.0));
assert_eq!(pos.end, Point::new(32.0, 32.0));
let offsets: Vec<f32> = gradient.stops.iter().map(|s| s.offset).collect();
assert_eq!(offsets, vec![0.0, 0.25, 1.0]);
}
#[test]
fn radial_gradient_resolves_center_and_shortest_side_radius_at_two_sizes() {
let spec = RadialGradientSpec::new(colors(&[0xFFFFFF, 0x000000]));
for (origin, size) in [
(Point::new(0.0, 0.0), Size::new(100.0, 40.0)),
(Point::new(5.0, 5.0), Size::new(60.0, 90.0)),
] {
let gradient = spec.resolve(origin, size);
let GradientKind::Radial(pos) = gradient.kind else {
panic!("expected a radial gradient kind");
};
let expected_center =
Point::new(origin.x + size.width / 2.0, origin.y + size.height / 2.0);
assert_eq!(pos.start_center, expected_center);
assert_eq!(pos.end_center, expected_center);
assert_eq!(pos.start_radius, 0.0);
let shortest_side = size.width.min(size.height);
assert_eq!(pos.end_radius, (0.5 * shortest_side) as f32);
}
}
#[test]
fn radial_gradient_radius_override_scales_with_shortest_side() {
let spec = RadialGradientSpec::new(colors(&[0xFFFFFF, 0x000000])).radius(1.0);
let gradient = spec.resolve(Point::new(0.0, 0.0), Size::new(50.0, 200.0));
let GradientKind::Radial(pos) = gradient.kind else {
panic!("expected a radial gradient kind");
};
assert_eq!(pos.end_radius, 50.0, "radius scales off the shortest side");
}
#[test]
fn sweep_gradient_defaults_to_a_full_clockwise_turn_at_two_sizes() {
let spec = SweepGradientSpec::new(colors(&[0xFF0000, 0x00FF00, 0x0000FF]));
for (origin, size) in [
(Point::new(0.0, 0.0), Size::new(80.0, 80.0)),
(Point::new(3.0, 4.0), Size::new(120.0, 40.0)),
] {
let gradient = spec.resolve(origin, size);
let GradientKind::Sweep(pos) = gradient.kind else {
panic!("expected a sweep gradient kind");
};
assert_eq!(
pos.center,
Point::new(origin.x + size.width / 2.0, origin.y + size.height / 2.0)
);
assert_eq!(pos.start_angle, 0.0);
assert_eq!(pos.end_angle, std::f32::consts::TAU);
assert_eq!(gradient.stops.len(), 3);
}
}
#[derive(Default)]
struct Recorder {
rrect_brushes: Vec<Brush>,
rrects: Vec<(Point, Size, f64, Color)>,
stroke_brushes: Vec<Brush>,
strokes: Vec<f64>,
runs: Vec<Brush>,
}
impl PaintScene for Recorder {
fn fill_rect(&mut self, _o: Point, _s: Size, _c: Color) {}
fn draw_text(&mut self, _o: Point, _t: &str) {}
fn fill_rounded_rect(&mut self, o: Point, s: Size, radius: f64, color: Color) {
self.rrects.push((o, s, radius, color));
}
fn fill_rounded_rect_brush(&mut self, _o: Point, _s: Size, _radius: f64, brush: &Brush) {
self.rrect_brushes.push(brush.clone());
}
fn stroke_path(&mut self, _o: Point, _path: &BezPath, width: f64, brush: &Brush) {
self.stroke_brushes.push(brush.clone());
self.strokes.push(width);
}
fn draw_glyph_run(&mut self, run: GlyphRun) {
self.runs.push(run.brush);
}
}
fn surface(state: InteractionStateForTest) -> ButtonSurface {
use crate::interaction::InteractionState;
ButtonSurface {
origin: Point::new(4.0, 8.0),
size: Size::new(120.0, 40.0),
radius: 20.0,
container: Color::TRANSPARENT,
content: Color::from_rgb8(0, 0, 0),
outline: None,
outline_width: 1.0,
enabled: state.enabled,
state: InteractionState {
hovered: state.hovered,
focused: false,
pressed: false,
dragged: false,
},
}
}
struct InteractionStateForTest {
enabled: bool,
hovered: bool,
}
fn enabled_idle() -> InteractionStateForTest {
InteractionStateForTest {
enabled: true,
hovered: false,
}
}
fn enabled_hovered() -> InteractionStateForTest {
InteractionStateForTest {
enabled: true,
hovered: true,
}
}
fn disabled() -> InteractionStateForTest {
InteractionStateForTest {
enabled: false,
hovered: false,
}
}
fn linear_two_stop() -> GradientProperty {
constant_gradient(LinearGradientSpec::new(colors(&[0x6750A4, 0x9A82DB])))
}
#[test]
fn unconfigured_layers_skip_and_paint_nothing() {
let deco = GradientButtonDecoration::new();
let surf = surface(enabled_idle());
let mut rec = Recorder::default();
assert_eq!(deco.paint_fill(&surf, &mut rec), DecorationOutcome::Skipped);
assert_eq!(
deco.paint_overlay(&surf, &mut rec),
DecorationOutcome::Skipped
);
assert_eq!(
deco.paint_outline(&surf, &mut rec),
DecorationOutcome::Skipped
);
assert!(deco.foreground_brush(&surf).is_none());
assert!(rec.rrect_brushes.is_empty());
assert!(rec.stroke_brushes.is_empty());
}
#[test]
fn fill_gradient_paints_and_reports_painted_suppressing_the_core_fill() {
let deco = GradientButtonDecoration::new().background(linear_two_stop());
let surf = surface(enabled_idle());
let mut rec = Recorder::default();
assert_eq!(deco.paint_fill(&surf, &mut rec), DecorationOutcome::Painted);
assert_eq!(rec.rrect_brushes.len(), 1);
assert!(matches!(rec.rrect_brushes[0], Brush::Gradient(_)));
}
#[test]
fn disabled_fill_gradient_dims_to_disabled_container_opacity() {
let deco = GradientButtonDecoration::new().background(linear_two_stop());
let surf = surface(disabled());
let mut rec = Recorder::default();
deco.paint_fill(&surf, &mut rec);
let Brush::Gradient(gradient) = &rec.rrect_brushes[0] else {
panic!("expected a gradient brush");
};
for stop in gradient.stops.iter() {
assert_eq!(stop.color.components[3], DISABLED_CONTAINER_OPACITY);
}
}
#[test]
fn overlay_gradient_reports_painted_even_at_zero_opacity_but_paints_nothing() {
let deco = GradientButtonDecoration::new().overlay(linear_two_stop());
let surf = surface(enabled_idle());
let mut rec = Recorder::default();
assert_eq!(
deco.paint_overlay(&surf, &mut rec),
DecorationOutcome::Painted
);
assert!(
rec.rrect_brushes.is_empty(),
"zero opacity paints no overlay quad"
);
}
#[test]
fn overlay_gradient_paints_at_the_hovered_states_opacity() {
let deco = GradientButtonDecoration::new().overlay(linear_two_stop());
let surf = surface(enabled_hovered());
let mut rec = Recorder::default();
deco.paint_overlay(&surf, &mut rec);
assert_eq!(rec.rrect_brushes.len(), 1);
let Brush::Gradient(gradient) = &rec.rrect_brushes[0] else {
panic!("expected a gradient brush");
};
for stop in gradient.stops.iter() {
assert_eq!(stop.color.components[3], crate::interaction::HOVER_OPACITY);
}
}
#[test]
fn disabled_overlay_gradient_paints_nothing_but_still_suppresses_the_core_layer() {
let deco = GradientButtonDecoration::new().overlay(linear_two_stop());
let surf = surface(disabled());
let mut rec = Recorder::default();
assert_eq!(
deco.paint_overlay(&surf, &mut rec),
DecorationOutcome::Painted
);
assert!(rec.rrect_brushes.is_empty());
}
#[test]
fn outline_gradient_strokes_at_the_surfaces_outline_width() {
let deco = GradientButtonDecoration::new().outline(linear_two_stop());
let surf = surface(enabled_idle());
let mut rec = Recorder::default();
assert_eq!(
deco.paint_outline(&surf, &mut rec),
DecorationOutcome::Painted
);
assert_eq!(rec.strokes, vec![surf.outline_width]);
assert!(matches!(rec.stroke_brushes[0], Brush::Gradient(_)));
}
#[test]
fn foreground_brush_resolves_a_gradient_brush_when_configured() {
let deco = GradientButtonDecoration::new().foreground(linear_two_stop());
let surf = surface(enabled_idle());
let brush = deco.foreground_brush(&surf);
assert!(matches!(brush, Some(Brush::Gradient(_))));
}
#[derive(Default)]
struct GlyphRunRecorder {
runs: Vec<GlyphRun>,
}
impl PaintScene for GlyphRunRecorder {
fn fill_rect(&mut self, _o: Point, _s: Size, _c: Color) {}
fn draw_text(&mut self, _o: Point, _t: &str) {}
fn fill_rounded_rect(&mut self, _o: Point, _s: Size, _radius: f64, _color: Color) {}
fn fill_rounded_rect_brush(&mut self, _o: Point, _s: Size, _radius: f64, _brush: &Brush) {}
fn stroke_path(&mut self, _o: Point, _path: &BezPath, _width: f64, _brush: &Brush) {}
fn draw_glyph_run(&mut self, run: GlyphRun) {
self.runs.push(run);
}
}
fn rendered_linear_start(run: &GlyphRun) -> Point {
let Brush::Gradient(gradient) = &run.brush else {
panic!("expected a gradient brush");
};
let GradientKind::Linear(pos) = gradient.kind else {
panic!("expected a linear gradient kind");
};
run.transform * pos.start
}
#[test]
fn gradient_label_tracks_the_glyph_run_across_consecutive_paints_at_different_origins() {
let deco: Rc<dyn ButtonDecoration> =
Rc::new(GradientButtonDecoration::new().foreground(linear_two_stop()));
let view = button::<(), _>("Save", |_| {}).decoration(deco);
let mut widget = build(&view);
let mut tcx = TextContext::new();
let mut lctx = LayoutCtx::with_text_context(&mut tcx as &mut dyn Any);
let size = widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(300.0, 100.0)));
let origin_delta = 10.0;
let mut rec1 = GlyphRunRecorder::default();
widget.paint(&mut PaintCtx::new(Point::new(10.0, 0.0), size), &mut rec1);
let mut rec2 = GlyphRunRecorder::default();
widget.paint(
&mut PaintCtx::new(Point::new(10.0 + origin_delta, 0.0), size),
&mut rec2,
);
let run1 = &rec1.runs[0];
let run2 = &rec2.runs[0];
let glyph1 = run1.transform * Point::new(run1.glyphs[0].x as f64, run1.glyphs[0].y as f64);
let glyph2 = run2.transform * Point::new(run2.glyphs[0].x as f64, run2.glyphs[0].y as f64);
let grad1 = rendered_linear_start(run1);
let grad2 = rendered_linear_start(run2);
assert_eq!(
glyph2.x - glyph1.x,
origin_delta,
"glyph run must move by exactly the origin delta"
);
assert_eq!(
grad2.x - grad1.x,
origin_delta,
"gradient must move by exactly the origin delta too, not a doubled (or otherwise \
mismatched) amount a coordinate-space bug between the brush and the run's own \
transform would produce"
);
assert_eq!(
grad1.x - glyph1.x,
grad2.x - glyph2.x,
"the gradient's offset from its own glyph run must be the same constant in both \
frames, never one that drifts with the button's position"
);
}
fn build<S: 'static>(view: &ButtonView<S>) -> ButtonWidget {
let mut counter = 0u64;
View::<S>::build(view, &mut BuildCtx::new(&mut counter))
}
#[test]
fn a_full_gradient_decoration_suppresses_every_core_solid_layer_and_paints_gradients() {
let deco: Rc<dyn ButtonDecoration> = Rc::new(
GradientButtonDecoration::new()
.background(linear_two_stop())
.outline(linear_two_stop())
.foreground(linear_two_stop()),
);
let view = button::<(), _>("Save", |_| {}).decoration(deco);
let mut widget = build(&view);
let mut tcx = TextContext::new();
let mut lctx = LayoutCtx::with_text_context(&mut tcx as &mut dyn Any);
let size = widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(300.0, 100.0)));
let mut rec = Recorder::default();
let mut pctx = PaintCtx::new(Point::ZERO, size);
widget.paint(&mut pctx, &mut rec);
assert!(
rec.rrects.is_empty(),
"the solid fill must be suppressed by the gradient decoration"
);
assert!(
!rec.rrect_brushes.is_empty(),
"a gradient fill must have painted instead"
);
assert!(
rec.stroke_brushes
.iter()
.any(|b| matches!(b, Brush::Gradient(_))),
"the outline must have painted as a gradient stroke"
);
assert!(
rec.runs.iter().any(|b| matches!(b, Brush::Gradient(_))),
"the label run must have been re-brushed with the foreground gradient"
);
}
}