1use std::rc::Rc;
73
74use frust::authoring::PaintScene;
75use kurbo::{Point, Shape, Size};
76use peniko::{
77 Brush, Color, ColorStop, Gradient, GradientKind, LinearGradientPosition,
78 RadialGradientPosition, SweepGradientPosition,
79};
80
81use crate::interaction::DISABLED_CONTAINER_OPACITY;
82
83use super::core::{PATH_TOLERANCE, inset_stroke_rect};
84use super::{ButtonDecoration, ButtonSurface, DecorationOutcome};
85
86#[derive(Clone, Copy, Debug, PartialEq)]
93pub struct GradientAlignment {
94 pub x: f64,
95 pub y: f64,
96}
97
98impl GradientAlignment {
99 pub const CENTER_LEFT: Self = Self { x: -1.0, y: 0.0 };
101 pub const CENTER_RIGHT: Self = Self { x: 1.0, y: 0.0 };
103 pub const CENTER: Self = Self { x: 0.0, y: 0.0 };
106
107 pub fn within_rect(&self, origin: Point, size: Size) -> Point {
114 let half_w = size.width / 2.0;
115 let half_h = size.height / 2.0;
116 Point::new(
117 origin.x + half_w + self.x * half_w,
118 origin.y + half_h + self.y * half_h,
119 )
120 }
121}
122
123pub fn implied_stops(color_count: usize) -> Vec<f32> {
128 match color_count {
129 0 => Vec::new(),
130 1 => vec![0.0],
131 n => {
132 let separation = 1.0 / (n - 1) as f32;
133 (0..n).map(|i| i as f32 * separation).collect()
134 }
135 }
136}
137
138fn color_stops(colors: &[Color], stops: &Option<Vec<f32>>) -> Vec<ColorStop> {
141 let resolved = stops.clone().unwrap_or_else(|| implied_stops(colors.len()));
142 colors
143 .iter()
144 .zip(resolved)
145 .map(|(color, stop)| ColorStop::from((stop, *color)))
146 .collect()
147}
148
149#[derive(Clone, Debug, PartialEq)]
155pub struct LinearGradientSpec {
156 pub begin: GradientAlignment,
157 pub end: GradientAlignment,
158 pub colors: Vec<Color>,
159 pub stops: Option<Vec<f32>>,
160}
161
162impl LinearGradientSpec {
163 pub fn new(colors: impl Into<Vec<Color>>) -> Self {
167 Self {
168 begin: GradientAlignment::CENTER_LEFT,
169 end: GradientAlignment::CENTER_RIGHT,
170 colors: colors.into(),
171 stops: None,
172 }
173 }
174
175 pub fn begin(mut self, begin: GradientAlignment) -> Self {
177 self.begin = begin;
178 self
179 }
180
181 pub fn end(mut self, end: GradientAlignment) -> Self {
183 self.end = end;
184 self
185 }
186
187 pub fn stops(mut self, stops: Vec<f32>) -> Self {
189 self.stops = Some(stops);
190 self
191 }
192
193 fn resolve(&self, origin: Point, size: Size) -> Gradient {
194 let start = self.begin.within_rect(origin, size);
195 let end = self.end.within_rect(origin, size);
196 Gradient::new_linear(start, end)
197 .with_stops(color_stops(&self.colors, &self.stops).as_slice())
198 }
199}
200
201#[derive(Clone, Debug, PartialEq)]
206pub struct RadialGradientSpec {
207 pub center: GradientAlignment,
208 pub radius: f64,
211 pub colors: Vec<Color>,
212 pub stops: Option<Vec<f32>>,
213}
214
215impl RadialGradientSpec {
216 pub fn new(colors: impl Into<Vec<Color>>) -> Self {
219 Self {
220 center: GradientAlignment::CENTER,
221 radius: 0.5,
222 colors: colors.into(),
223 stops: None,
224 }
225 }
226
227 pub fn center(mut self, center: GradientAlignment) -> Self {
229 self.center = center;
230 self
231 }
232
233 pub fn radius(mut self, radius: f64) -> Self {
235 self.radius = radius;
236 self
237 }
238
239 pub fn stops(mut self, stops: Vec<f32>) -> Self {
241 self.stops = Some(stops);
242 self
243 }
244
245 fn resolve(&self, origin: Point, size: Size) -> Gradient {
246 let center = self.center.within_rect(origin, size);
247 let shortest_side = size.width.min(size.height);
249 let radius = (self.radius * shortest_side) as f32;
250 Gradient::new_radial(center, radius)
251 .with_stops(color_stops(&self.colors, &self.stops).as_slice())
252 }
253}
254
255#[derive(Clone, Debug, PartialEq)]
261pub struct SweepGradientSpec {
262 pub center: GradientAlignment,
263 pub start_angle: f64,
264 pub end_angle: f64,
265 pub colors: Vec<Color>,
266 pub stops: Option<Vec<f32>>,
267}
268
269impl SweepGradientSpec {
270 pub fn new(colors: impl Into<Vec<Color>>) -> Self {
273 Self {
274 center: GradientAlignment::CENTER,
275 start_angle: 0.0,
276 end_angle: std::f64::consts::TAU,
277 colors: colors.into(),
278 stops: None,
279 }
280 }
281
282 pub fn center(mut self, center: GradientAlignment) -> Self {
284 self.center = center;
285 self
286 }
287
288 pub fn angles(mut self, start_angle: f64, end_angle: f64) -> Self {
290 self.start_angle = start_angle;
291 self.end_angle = end_angle;
292 self
293 }
294
295 pub fn stops(mut self, stops: Vec<f32>) -> Self {
297 self.stops = Some(stops);
298 self
299 }
300
301 fn resolve(&self, origin: Point, size: Size) -> Gradient {
302 let center = self.center.within_rect(origin, size);
303 Gradient::new_sweep(center, self.start_angle as f32, self.end_angle as f32)
304 .with_stops(color_stops(&self.colors, &self.stops).as_slice())
305 }
306}
307
308#[derive(Clone, Debug, PartialEq)]
312pub enum GradientSpec {
313 Linear(LinearGradientSpec),
314 Radial(RadialGradientSpec),
315 Sweep(SweepGradientSpec),
316}
317
318impl GradientSpec {
319 pub fn resolve(&self, origin: Point, size: Size) -> Gradient {
322 match self {
323 Self::Linear(spec) => spec.resolve(origin, size),
324 Self::Radial(spec) => spec.resolve(origin, size),
325 Self::Sweep(spec) => spec.resolve(origin, size),
326 }
327 }
328}
329
330impl From<LinearGradientSpec> for GradientSpec {
331 fn from(spec: LinearGradientSpec) -> Self {
332 Self::Linear(spec)
333 }
334}
335
336impl From<RadialGradientSpec> for GradientSpec {
337 fn from(spec: RadialGradientSpec) -> Self {
338 Self::Radial(spec)
339 }
340}
341
342impl From<SweepGradientSpec> for GradientSpec {
343 fn from(spec: SweepGradientSpec) -> Self {
344 Self::Sweep(spec)
345 }
346}
347
348#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
353pub struct GradientStates {
354 pub hovered: bool,
355 pub focused: bool,
356 pub pressed: bool,
357 pub dragged: bool,
358 pub disabled: bool,
359}
360
361impl GradientStates {
362 pub fn from_surface(surface: &ButtonSurface) -> Self {
365 Self {
366 hovered: surface.state.hovered,
367 focused: surface.state.focused,
368 pressed: surface.state.pressed,
369 dragged: surface.state.dragged,
370 disabled: !surface.enabled,
371 }
372 }
373}
374
375pub type GradientProperty = Rc<dyn Fn(GradientStates) -> Option<GradientSpec>>;
377
378pub fn constant_gradient(spec: impl Into<GradientSpec>) -> GradientProperty {
383 let spec = spec.into();
384 Rc::new(move |_states| Some(spec.clone()))
385}
386
387#[derive(Clone, Default)]
396pub struct GradientButtonDecoration {
397 background: Option<GradientProperty>,
398 foreground: Option<GradientProperty>,
399 overlay: Option<GradientProperty>,
400 outline: Option<GradientProperty>,
401}
402
403impl GradientButtonDecoration {
404 pub fn new() -> Self {
407 Self::default()
408 }
409
410 pub fn background(mut self, property: GradientProperty) -> Self {
412 self.background = Some(property);
413 self
414 }
415
416 pub fn foreground(mut self, property: GradientProperty) -> Self {
419 self.foreground = Some(property);
420 self
421 }
422
423 pub fn overlay(mut self, property: GradientProperty) -> Self {
425 self.overlay = Some(property);
426 self
427 }
428
429 pub fn outline(mut self, property: GradientProperty) -> Self {
431 self.outline = Some(property);
432 self
433 }
434}
435
436impl ButtonDecoration for GradientButtonDecoration {
437 fn paint_fill(&self, surface: &ButtonSurface, scene: &mut dyn PaintScene) -> DecorationOutcome {
438 let Some(property) = &self.background else {
439 return DecorationOutcome::Skipped;
440 };
441 let Some(spec) = property(GradientStates::from_surface(surface)) else {
442 return DecorationOutcome::Skipped;
443 };
444 let mut gradient = spec.resolve(surface.origin, surface.size);
445 if !surface.enabled {
446 gradient = gradient.multiply_alpha(DISABLED_CONTAINER_OPACITY);
453 }
454 scene.fill_rounded_rect_brush(
455 surface.origin,
456 surface.size,
457 surface.radius,
458 &Brush::Gradient(gradient),
459 );
460 DecorationOutcome::Painted
461 }
462
463 fn paint_overlay(
464 &self,
465 surface: &ButtonSurface,
466 scene: &mut dyn PaintScene,
467 ) -> DecorationOutcome {
468 let Some(property) = &self.overlay else {
469 return DecorationOutcome::Skipped;
470 };
471 if let Some(spec) = property(GradientStates::from_surface(surface)) {
477 let opacity = if surface.enabled {
481 surface.state.resolve_opacity()
482 } else {
483 0.0
484 };
485 if opacity > 0.0 {
486 let gradient = spec
487 .resolve(surface.origin, surface.size)
488 .multiply_alpha(opacity);
489 scene.fill_rounded_rect_brush(
490 surface.origin,
491 surface.size,
492 surface.radius,
493 &Brush::Gradient(gradient),
494 );
495 }
496 }
497 DecorationOutcome::Painted
498 }
499
500 fn paint_outline(
501 &self,
502 surface: &ButtonSurface,
503 scene: &mut dyn PaintScene,
504 ) -> DecorationOutcome {
505 let Some(property) = &self.outline else {
506 return DecorationOutcome::Skipped;
507 };
508 let Some(spec) = property(GradientStates::from_surface(surface)) else {
509 return DecorationOutcome::Skipped;
510 };
511 if surface.outline_width <= 0.0 {
512 return DecorationOutcome::Skipped;
515 }
516 let gradient = spec.resolve(surface.origin, surface.size);
517 let path = inset_stroke_rect(surface.size, surface.radius, surface.outline_width)
518 .to_path(PATH_TOLERANCE);
519 scene.stroke_path(
520 surface.origin,
521 &path,
522 surface.outline_width,
523 &Brush::Gradient(gradient),
524 );
525 DecorationOutcome::Painted
526 }
527
528 fn foreground_brush(&self, surface: &ButtonSurface) -> Option<Brush> {
529 let property = self.foreground.as_ref()?;
530 let spec = property(GradientStates::from_surface(surface))?;
531 Some(Brush::Gradient(spec.resolve(surface.origin, surface.size)))
532 }
533}
534
535pub(super) fn brush_relative_to_run_origin(brush: &Brush, run_origin: Point) -> Brush {
556 let Brush::Gradient(gradient) = brush else {
557 return brush.clone();
558 };
559 let shift = |p: Point| Point::new(p.x - run_origin.x, p.y - run_origin.y);
560 let mut gradient = gradient.clone();
561 gradient.kind = match gradient.kind {
562 GradientKind::Linear(pos) => LinearGradientPosition {
563 start: shift(pos.start),
564 end: shift(pos.end),
565 }
566 .into(),
567 GradientKind::Radial(pos) => RadialGradientPosition {
568 start_center: shift(pos.start_center),
569 start_radius: pos.start_radius,
570 end_center: shift(pos.end_center),
571 end_radius: pos.end_radius,
572 }
573 .into(),
574 GradientKind::Sweep(pos) => SweepGradientPosition {
575 center: shift(pos.center),
576 start_angle: pos.start_angle,
577 end_angle: pos.end_angle,
578 }
579 .into(),
580 };
581 Brush::Gradient(gradient)
582}
583
584#[cfg(test)]
585mod tests {
586 use super::*;
587 use crate::button::{ButtonView, ButtonWidget, button};
588 use frust::authoring::scene::GlyphRun;
589 use frust::authoring::text::TextContext;
590 use frust::authoring::{BoxConstraints, BuildCtx, LayoutCtx, PaintCtx, View, Widget};
591 use kurbo::BezPath;
592 use peniko::{Extend, GradientKind};
593 use std::any::Any;
594
595 fn colors(hexes: &[u32]) -> Vec<Color> {
596 hexes
597 .iter()
598 .map(|&h| {
599 Color::from_rgb8(
600 ((h >> 16) & 0xFF) as u8,
601 ((h >> 8) & 0xFF) as u8,
602 (h & 0xFF) as u8,
603 )
604 })
605 .collect()
606 }
607
608 #[test]
611 fn implied_stops_matches_dart_uis_own_table() {
612 assert_eq!(implied_stops(0), Vec::<f32>::new());
613 assert_eq!(implied_stops(1), vec![0.0]);
614 assert_eq!(implied_stops(2), vec![0.0, 1.0]);
615 assert_eq!(implied_stops(3), vec![0.0, 0.5, 1.0]);
616 assert_eq!(implied_stops(4), vec![0.0, 1.0 / 3.0, 2.0 / 3.0, 1.0]);
617 }
618
619 #[test]
622 fn linear_gradient_resolves_begin_end_and_implied_stops_at_two_sizes() {
623 let spec = LinearGradientSpec::new(colors(&[0x6750A4, 0x9A82DB]));
624 for (origin, size) in [
625 (Point::new(0.0, 0.0), Size::new(100.0, 40.0)),
626 (Point::new(10.0, 20.0), Size::new(200.0, 80.0)),
627 ] {
628 let gradient = spec.resolve(origin, size);
629 let GradientKind::Linear(pos) = gradient.kind else {
630 panic!("expected a linear gradient kind");
631 };
632 assert_eq!(
635 pos.start,
636 Point::new(origin.x, origin.y + size.height / 2.0)
637 );
638 assert_eq!(
639 pos.end,
640 Point::new(origin.x + size.width, origin.y + size.height / 2.0)
641 );
642 assert_eq!(gradient.extend, Extend::Pad);
643 let stops: Vec<(f32, Color)> = gradient
644 .stops
645 .iter()
646 .map(|s| (s.offset, s.color.to_alpha_color()))
647 .collect();
648 assert_eq!(
649 stops,
650 vec![(0.0, colors(&[0x6750A4])[0]), (1.0, colors(&[0x9A82DB])[0]),]
651 );
652 }
653 }
654
655 #[test]
656 fn linear_gradient_explicit_begin_end_and_stops_are_honored() {
657 let spec = LinearGradientSpec::new(colors(&[0xFF0000, 0x00FF00, 0x0000FF]))
658 .begin(GradientAlignment { x: 0.0, y: -1.0 })
659 .end(GradientAlignment { x: 0.0, y: 1.0 })
660 .stops(vec![0.0, 0.25, 1.0]);
661 let origin = Point::new(0.0, 0.0);
662 let size = Size::new(64.0, 32.0);
663 let gradient = spec.resolve(origin, size);
664 let GradientKind::Linear(pos) = gradient.kind else {
665 panic!("expected a linear gradient kind");
666 };
667 assert_eq!(pos.start, Point::new(32.0, 0.0));
669 assert_eq!(pos.end, Point::new(32.0, 32.0));
670 let offsets: Vec<f32> = gradient.stops.iter().map(|s| s.offset).collect();
671 assert_eq!(offsets, vec![0.0, 0.25, 1.0]);
672 }
673
674 #[test]
677 fn radial_gradient_resolves_center_and_shortest_side_radius_at_two_sizes() {
678 let spec = RadialGradientSpec::new(colors(&[0xFFFFFF, 0x000000]));
679 for (origin, size) in [
680 (Point::new(0.0, 0.0), Size::new(100.0, 40.0)),
681 (Point::new(5.0, 5.0), Size::new(60.0, 90.0)),
682 ] {
683 let gradient = spec.resolve(origin, size);
684 let GradientKind::Radial(pos) = gradient.kind else {
685 panic!("expected a radial gradient kind");
686 };
687 let expected_center =
688 Point::new(origin.x + size.width / 2.0, origin.y + size.height / 2.0);
689 assert_eq!(pos.start_center, expected_center);
690 assert_eq!(pos.end_center, expected_center);
691 assert_eq!(pos.start_radius, 0.0);
692 let shortest_side = size.width.min(size.height);
693 assert_eq!(pos.end_radius, (0.5 * shortest_side) as f32);
694 }
695 }
696
697 #[test]
698 fn radial_gradient_radius_override_scales_with_shortest_side() {
699 let spec = RadialGradientSpec::new(colors(&[0xFFFFFF, 0x000000])).radius(1.0);
700 let gradient = spec.resolve(Point::new(0.0, 0.0), Size::new(50.0, 200.0));
701 let GradientKind::Radial(pos) = gradient.kind else {
702 panic!("expected a radial gradient kind");
703 };
704 assert_eq!(pos.end_radius, 50.0, "radius scales off the shortest side");
705 }
706
707 #[test]
710 fn sweep_gradient_defaults_to_a_full_clockwise_turn_at_two_sizes() {
711 let spec = SweepGradientSpec::new(colors(&[0xFF0000, 0x00FF00, 0x0000FF]));
712 for (origin, size) in [
713 (Point::new(0.0, 0.0), Size::new(80.0, 80.0)),
714 (Point::new(3.0, 4.0), Size::new(120.0, 40.0)),
715 ] {
716 let gradient = spec.resolve(origin, size);
717 let GradientKind::Sweep(pos) = gradient.kind else {
718 panic!("expected a sweep gradient kind");
719 };
720 assert_eq!(
721 pos.center,
722 Point::new(origin.x + size.width / 2.0, origin.y + size.height / 2.0)
723 );
724 assert_eq!(pos.start_angle, 0.0);
725 assert_eq!(pos.end_angle, std::f32::consts::TAU);
726 assert_eq!(gradient.stops.len(), 3);
727 }
728 }
729
730 #[derive(Default)]
733 struct Recorder {
734 rrect_brushes: Vec<Brush>,
735 rrects: Vec<(Point, Size, f64, Color)>,
736 stroke_brushes: Vec<Brush>,
737 strokes: Vec<f64>,
738 runs: Vec<Brush>,
739 }
740
741 impl PaintScene for Recorder {
742 fn fill_rect(&mut self, _o: Point, _s: Size, _c: Color) {}
743 fn draw_text(&mut self, _o: Point, _t: &str) {}
744 fn fill_rounded_rect(&mut self, o: Point, s: Size, radius: f64, color: Color) {
745 self.rrects.push((o, s, radius, color));
746 }
747 fn fill_rounded_rect_brush(&mut self, _o: Point, _s: Size, _radius: f64, brush: &Brush) {
748 self.rrect_brushes.push(brush.clone());
749 }
750 fn stroke_path(&mut self, _o: Point, _path: &BezPath, width: f64, brush: &Brush) {
751 self.stroke_brushes.push(brush.clone());
752 self.strokes.push(width);
753 }
754 fn draw_glyph_run(&mut self, run: GlyphRun) {
755 self.runs.push(run.brush);
756 }
757 }
758
759 fn surface(state: InteractionStateForTest) -> ButtonSurface {
760 use crate::interaction::InteractionState;
761 ButtonSurface {
762 origin: Point::new(4.0, 8.0),
763 size: Size::new(120.0, 40.0),
764 radius: 20.0,
765 container: Color::TRANSPARENT,
766 content: Color::from_rgb8(0, 0, 0),
767 outline: None,
768 outline_width: 1.0,
769 enabled: state.enabled,
770 state: InteractionState {
771 hovered: state.hovered,
772 focused: false,
773 pressed: false,
774 dragged: false,
775 },
776 }
777 }
778
779 struct InteractionStateForTest {
780 enabled: bool,
781 hovered: bool,
782 }
783
784 fn enabled_idle() -> InteractionStateForTest {
785 InteractionStateForTest {
786 enabled: true,
787 hovered: false,
788 }
789 }
790
791 fn enabled_hovered() -> InteractionStateForTest {
792 InteractionStateForTest {
793 enabled: true,
794 hovered: true,
795 }
796 }
797
798 fn disabled() -> InteractionStateForTest {
799 InteractionStateForTest {
800 enabled: false,
801 hovered: false,
802 }
803 }
804
805 fn linear_two_stop() -> GradientProperty {
806 constant_gradient(LinearGradientSpec::new(colors(&[0x6750A4, 0x9A82DB])))
807 }
808
809 #[test]
810 fn unconfigured_layers_skip_and_paint_nothing() {
811 let deco = GradientButtonDecoration::new();
812 let surf = surface(enabled_idle());
813 let mut rec = Recorder::default();
814 assert_eq!(deco.paint_fill(&surf, &mut rec), DecorationOutcome::Skipped);
815 assert_eq!(
816 deco.paint_overlay(&surf, &mut rec),
817 DecorationOutcome::Skipped
818 );
819 assert_eq!(
820 deco.paint_outline(&surf, &mut rec),
821 DecorationOutcome::Skipped
822 );
823 assert!(deco.foreground_brush(&surf).is_none());
824 assert!(rec.rrect_brushes.is_empty());
825 assert!(rec.stroke_brushes.is_empty());
826 }
827
828 #[test]
829 fn fill_gradient_paints_and_reports_painted_suppressing_the_core_fill() {
830 let deco = GradientButtonDecoration::new().background(linear_two_stop());
831 let surf = surface(enabled_idle());
832 let mut rec = Recorder::default();
833 assert_eq!(deco.paint_fill(&surf, &mut rec), DecorationOutcome::Painted);
834 assert_eq!(rec.rrect_brushes.len(), 1);
835 assert!(matches!(rec.rrect_brushes[0], Brush::Gradient(_)));
836 }
837
838 #[test]
839 fn disabled_fill_gradient_dims_to_disabled_container_opacity() {
840 let deco = GradientButtonDecoration::new().background(linear_two_stop());
841 let surf = surface(disabled());
842 let mut rec = Recorder::default();
843 deco.paint_fill(&surf, &mut rec);
844 let Brush::Gradient(gradient) = &rec.rrect_brushes[0] else {
845 panic!("expected a gradient brush");
846 };
847 for stop in gradient.stops.iter() {
848 assert_eq!(stop.color.components[3], DISABLED_CONTAINER_OPACITY);
849 }
850 }
851
852 #[test]
853 fn overlay_gradient_reports_painted_even_at_zero_opacity_but_paints_nothing() {
854 let deco = GradientButtonDecoration::new().overlay(linear_two_stop());
858 let surf = surface(enabled_idle());
859 let mut rec = Recorder::default();
860 assert_eq!(
861 deco.paint_overlay(&surf, &mut rec),
862 DecorationOutcome::Painted
863 );
864 assert!(
865 rec.rrect_brushes.is_empty(),
866 "zero opacity paints no overlay quad"
867 );
868 }
869
870 #[test]
871 fn overlay_gradient_paints_at_the_hovered_states_opacity() {
872 let deco = GradientButtonDecoration::new().overlay(linear_two_stop());
873 let surf = surface(enabled_hovered());
874 let mut rec = Recorder::default();
875 deco.paint_overlay(&surf, &mut rec);
876 assert_eq!(rec.rrect_brushes.len(), 1);
877 let Brush::Gradient(gradient) = &rec.rrect_brushes[0] else {
878 panic!("expected a gradient brush");
879 };
880 for stop in gradient.stops.iter() {
881 assert_eq!(stop.color.components[3], crate::interaction::HOVER_OPACITY);
882 }
883 }
884
885 #[test]
886 fn disabled_overlay_gradient_paints_nothing_but_still_suppresses_the_core_layer() {
887 let deco = GradientButtonDecoration::new().overlay(linear_two_stop());
888 let surf = surface(disabled());
889 let mut rec = Recorder::default();
890 assert_eq!(
891 deco.paint_overlay(&surf, &mut rec),
892 DecorationOutcome::Painted
893 );
894 assert!(rec.rrect_brushes.is_empty());
895 }
896
897 #[test]
898 fn outline_gradient_strokes_at_the_surfaces_outline_width() {
899 let deco = GradientButtonDecoration::new().outline(linear_two_stop());
900 let surf = surface(enabled_idle());
901 let mut rec = Recorder::default();
902 assert_eq!(
903 deco.paint_outline(&surf, &mut rec),
904 DecorationOutcome::Painted
905 );
906 assert_eq!(rec.strokes, vec![surf.outline_width]);
907 assert!(matches!(rec.stroke_brushes[0], Brush::Gradient(_)));
908 }
909
910 #[test]
911 fn foreground_brush_resolves_a_gradient_brush_when_configured() {
912 let deco = GradientButtonDecoration::new().foreground(linear_two_stop());
913 let surf = surface(enabled_idle());
914 let brush = deco.foreground_brush(&surf);
915 assert!(matches!(brush, Some(Brush::Gradient(_))));
916 }
917
918 #[derive(Default)]
925 struct GlyphRunRecorder {
926 runs: Vec<GlyphRun>,
927 }
928 impl PaintScene for GlyphRunRecorder {
929 fn fill_rect(&mut self, _o: Point, _s: Size, _c: Color) {}
930 fn draw_text(&mut self, _o: Point, _t: &str) {}
931 fn fill_rounded_rect(&mut self, _o: Point, _s: Size, _radius: f64, _color: Color) {}
932 fn fill_rounded_rect_brush(&mut self, _o: Point, _s: Size, _radius: f64, _brush: &Brush) {}
933 fn stroke_path(&mut self, _o: Point, _path: &BezPath, _width: f64, _brush: &Brush) {}
934 fn draw_glyph_run(&mut self, run: GlyphRun) {
935 self.runs.push(run);
936 }
937 }
938
939 fn rendered_linear_start(run: &GlyphRun) -> Point {
947 let Brush::Gradient(gradient) = &run.brush else {
948 panic!("expected a gradient brush");
949 };
950 let GradientKind::Linear(pos) = gradient.kind else {
951 panic!("expected a linear gradient kind");
952 };
953 run.transform * pos.start
954 }
955
956 #[test]
962 fn gradient_label_tracks_the_glyph_run_across_consecutive_paints_at_different_origins() {
963 let deco: Rc<dyn ButtonDecoration> =
964 Rc::new(GradientButtonDecoration::new().foreground(linear_two_stop()));
965 let view = button::<(), _>("Save", |_| {}).decoration(deco);
966 let mut widget = build(&view);
967
968 let mut tcx = TextContext::new();
969 let mut lctx = LayoutCtx::with_text_context(&mut tcx as &mut dyn Any);
970 let size = widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(300.0, 100.0)));
971
972 let origin_delta = 10.0;
973 let mut rec1 = GlyphRunRecorder::default();
974 widget.paint(&mut PaintCtx::new(Point::new(10.0, 0.0), size), &mut rec1);
975 let mut rec2 = GlyphRunRecorder::default();
976 widget.paint(
977 &mut PaintCtx::new(Point::new(10.0 + origin_delta, 0.0), size),
978 &mut rec2,
979 );
980
981 let run1 = &rec1.runs[0];
982 let run2 = &rec2.runs[0];
983 let glyph1 = run1.transform * Point::new(run1.glyphs[0].x as f64, run1.glyphs[0].y as f64);
984 let glyph2 = run2.transform * Point::new(run2.glyphs[0].x as f64, run2.glyphs[0].y as f64);
985 let grad1 = rendered_linear_start(run1);
986 let grad2 = rendered_linear_start(run2);
987
988 assert_eq!(
989 glyph2.x - glyph1.x,
990 origin_delta,
991 "glyph run must move by exactly the origin delta"
992 );
993 assert_eq!(
994 grad2.x - grad1.x,
995 origin_delta,
996 "gradient must move by exactly the origin delta too, not a doubled (or otherwise \
997 mismatched) amount a coordinate-space bug between the brush and the run's own \
998 transform would produce"
999 );
1000 assert_eq!(
1001 grad1.x - glyph1.x,
1002 grad2.x - glyph2.x,
1003 "the gradient's offset from its own glyph run must be the same constant in both \
1004 frames, never one that drifts with the button's position"
1005 );
1006 }
1007
1008 fn build<S: 'static>(view: &ButtonView<S>) -> ButtonWidget {
1011 let mut counter = 0u64;
1012 View::<S>::build(view, &mut BuildCtx::new(&mut counter))
1013 }
1014
1015 #[test]
1016 fn a_full_gradient_decoration_suppresses_every_core_solid_layer_and_paints_gradients() {
1017 let deco: Rc<dyn ButtonDecoration> = Rc::new(
1018 GradientButtonDecoration::new()
1019 .background(linear_two_stop())
1020 .outline(linear_two_stop())
1021 .foreground(linear_two_stop()),
1022 );
1023 let view = button::<(), _>("Save", |_| {}).decoration(deco);
1024 let mut widget = build(&view);
1025
1026 let mut tcx = TextContext::new();
1027 let mut lctx = LayoutCtx::with_text_context(&mut tcx as &mut dyn Any);
1028 let size = widget.layout(&mut lctx, &BoxConstraints::loose(Size::new(300.0, 100.0)));
1029
1030 let mut rec = Recorder::default();
1031 let mut pctx = PaintCtx::new(Point::ZERO, size);
1032 widget.paint(&mut pctx, &mut rec);
1033
1034 assert!(
1035 rec.rrects.is_empty(),
1036 "the solid fill must be suppressed by the gradient decoration"
1037 );
1038 assert!(
1039 !rec.rrect_brushes.is_empty(),
1040 "a gradient fill must have painted instead"
1041 );
1042 assert!(
1043 rec.stroke_brushes
1044 .iter()
1045 .any(|b| matches!(b, Brush::Gradient(_))),
1046 "the outline must have painted as a gradient stroke"
1047 );
1048 assert!(
1049 rec.runs.iter().any(|b| matches!(b, Brush::Gradient(_))),
1050 "the label run must have been re-brushed with the foreground gradient"
1051 );
1052 }
1053}