Skip to main content

wisp/scene/
highlight.rs

1//! Highlight overlay presets (M-VEC.8 / AUT-60).
2//!
3//! Reusable [`Vector`] constructors for "guide the viewer's eye to
4//! this region" overlays. Outputs are plain [`Vector`]s so callers
5//! can chain `with_*` builders, drive masks, or feed into vector
6//! storyboards.
7
8use crate::color::Color;
9use crate::scene::Fill;
10use crate::scene::vector::{Vector, VectorShape, VectorStroke};
11
12/// Highlight preset constructors.
13///
14/// All constructors return [`Vector`]s — pure data — so the caller
15/// is free to apply transforms, opacity, or render-time decoration
16/// before adding to the stage.
17pub struct Highlight;
18
19impl Highlight {
20    /// Outline-ring highlight: a stroke-only outline with no fill.
21    /// The classic "glowing border" effect for buttons / form fields.
22    #[must_use]
23    pub fn outline(shape: VectorShape, color: Color, width: f32) -> Vector {
24        Vector::new(shape).with_stroke(VectorStroke::new(width, color))
25    }
26
27    /// Filled highlight: a translucent fill of the shape so the
28    /// underlying content shows through. `alpha` is the multiplied
29    /// opacity (clamped to `[0, 1]`); `color`'s own alpha is honored
30    /// as well.
31    #[must_use]
32    pub fn filled(shape: VectorShape, color: Color, alpha: f32) -> Vector {
33        let alpha = alpha.clamp(0.0, 1.0);
34        Vector::new(shape).with_fill(Fill::Solid(color.with_alpha(color.a * alpha)))
35    }
36
37    /// "Pill" highlight — a rounded-rect filled overlay over a
38    /// horizontal label. Convenience over `filled` for the common
39    /// menu-item / chip use case.
40    #[must_use]
41    pub fn pill(rect: crate::math::Rect, color: Color, alpha: f32) -> Vector {
42        let radius = (rect.size.y * 0.5).abs();
43        Self::filled(VectorShape::rounded_rect(rect, radius), color, alpha)
44    }
45
46    /// Cheap soft-glow approximation: a stroke that's wider than a
47    /// regular outline, with a low-alpha color. Not a true Gaussian
48    /// glow — that's deferred to AUT-49 (M-DYN.7 feathered-edge mask)
49    /// once feathering ships. Documented as preliminary.
50    #[must_use]
51    pub fn glow(shape: VectorShape, color: Color, width: f32) -> Vector {
52        Vector::new(shape).with_stroke(VectorStroke::new(width, color.with_alpha(color.a * 0.4)))
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use crate::math::Rect;
60
61    #[test]
62    fn outline_has_stroke_no_fill() {
63        let v = Highlight::outline(
64            VectorShape::rect(Rect::new(-0.5, -0.5, 1.0, 1.0)),
65            Color::WHITE,
66            0.05,
67        );
68        assert!(v.fill.is_none());
69        let s = v.stroke.expect("outline has stroke");
70        assert!((s.width - 0.05).abs() < f32::EPSILON);
71    }
72
73    #[test]
74    fn filled_alpha_multiplies_color() {
75        let base = Color::rgba(0.5, 0.5, 0.5, 1.0);
76        let v = Highlight::filled(VectorShape::circle(glam::Vec2::ZERO, 0.4), base, 0.5);
77        let Some(Fill::Solid(c)) = v.fill else {
78            panic!("expected solid fill");
79        };
80        assert!((c.a - 0.5).abs() < f32::EPSILON);
81    }
82
83    #[test]
84    fn pill_uses_half_height_radius() {
85        let v = Highlight::pill(Rect::new(-0.5, -0.05, 1.0, 0.1), Color::WHITE, 0.6);
86        match &v.shape {
87            VectorShape::RoundedRect { radius, .. } => {
88                assert!((radius - 0.05).abs() < f32::EPSILON);
89            }
90            _ => panic!("pill should be a rounded rect"),
91        }
92    }
93
94    #[test]
95    fn glow_alpha_scales_down_to_indicate_softness() {
96        let v = Highlight::glow(
97            VectorShape::rect(Rect::new(-0.5, -0.5, 1.0, 1.0)),
98            Color::rgba(1.0, 1.0, 1.0, 1.0),
99            0.1,
100        );
101        let s = v.stroke.expect("glow has stroke");
102        assert!((s.color.a - 0.4).abs() < 1e-3);
103    }
104}