1use glam::Vec2;
17
18use crate::color::Color;
19use crate::math::Rect;
20use crate::scene::Fill;
21use crate::scene::vector::{Vector, VectorShape, VectorStroke};
22
23pub struct Callout;
25
26impl Callout {
27 #[must_use]
30 pub fn label_box(rect: Rect, fill: Color, stroke: Option<VectorStroke>, radius: f32) -> Vector {
31 let mut v =
32 Vector::new(VectorShape::rounded_rect(rect, radius)).with_fill(Fill::Solid(fill));
33 if let Some(s) = stroke {
34 v = v.with_stroke(s);
35 }
36 v
37 }
38
39 #[must_use]
41 pub fn badge(center: Vec2, radius: f32, fill: Color) -> Vector {
42 Vector::new(VectorShape::circle(center, radius)).with_fill(Fill::Solid(fill))
43 }
44
45 #[must_use]
49 pub fn caption_pill(rect: Rect, fill: Color) -> Vector {
50 let radius = (rect.size.y * 0.5).abs();
51 Vector::new(VectorShape::rounded_rect(rect, radius)).with_fill(Fill::Solid(fill))
52 }
53
54 #[must_use]
62 pub fn arrow_to(
63 from: glam::Vec2,
64 to: glam::Vec2,
65 width: f32,
66 color: Color,
67 ) -> crate::scene::Graphics {
68 use crate::scene::path::PathBuilder;
69 let stem = to - from;
70 let stem_len = stem.length();
71 if stem_len < 1e-6 {
72 return crate::scene::Graphics::new();
73 }
74 let head = stem_len * 0.18;
75 let dir = stem / stem_len;
76 let perp = glam::Vec2::new(-dir.y, dir.x);
77 let back = to - dir * head;
78 let left = back + perp * head * 0.5;
79 let right = back - perp * head * 0.5;
80 PathBuilder::new()
81 .move_to(from)
82 .line_to(to)
83 .move_to(left)
84 .line_to(to)
85 .line_to(right)
86 .build()
87 .stroke_to_graphics(width, color, 0.005)
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn label_box_with_stroke_has_both() {
97 let v = Callout::label_box(
98 Rect::new(-0.5, -0.2, 1.0, 0.4),
99 Color::rgba_u8(220, 220, 130, 230),
100 Some(VectorStroke::new(0.012, Color::WHITE)),
101 0.04,
102 );
103 assert!(v.fill.is_some());
104 assert!(v.stroke.is_some());
105 }
106
107 #[test]
108 fn label_box_without_stroke_omits_it() {
109 let v = Callout::label_box(
110 Rect::new(-0.5, -0.2, 1.0, 0.4),
111 Color::rgba_u8(220, 220, 130, 230),
112 None,
113 0.04,
114 );
115 assert!(v.fill.is_some());
116 assert!(v.stroke.is_none());
117 }
118
119 #[test]
120 fn badge_is_circle_at_center() {
121 let v = Callout::badge(Vec2::new(0.4, 0.5), 0.06, Color::rgba(1.0, 0.4, 0.0, 1.0));
122 match &v.shape {
123 VectorShape::Circle { center, radius } => {
124 assert!((center.x - 0.4).abs() < f32::EPSILON);
125 assert!((radius - 0.06).abs() < f32::EPSILON);
126 }
127 _ => panic!("badge should be a circle"),
128 }
129 }
130
131 #[test]
132 fn caption_pill_radius_is_half_height() {
133 let v = Callout::caption_pill(
134 Rect::new(-0.6, -0.04, 1.2, 0.08),
135 Color::rgba_u8(255, 255, 255, 220),
136 );
137 match &v.shape {
138 VectorShape::RoundedRect { radius, .. } => {
139 assert!((radius - 0.04).abs() < f32::EPSILON);
140 }
141 _ => panic!("caption_pill should be a rounded rect"),
142 }
143 }
144}