1use jag_draw::{Brush, ColorLinPremul, Rect, RoundedRadii, RoundedRect};
4use jag_surface::Canvas;
5
6use crate::event::{
7 EventHandler, EventResult, KeyboardEvent, MouseClickEvent, MouseMoveEvent, ScrollEvent,
8};
9use crate::focus::FocusId;
10
11use super::Element;
12
13pub struct Badge {
15 pub rect: Rect,
16 pub text: String,
18 pub bg_color: ColorLinPremul,
20 pub text_color: ColorLinPremul,
22 pub font_size: f32,
24 pub padding_x: f32,
26 pub padding_y: f32,
28}
29
30impl Badge {
31 pub fn new(text: impl Into<String>) -> Self {
33 Self {
34 rect: Rect {
35 x: 0.0,
36 y: 0.0,
37 w: 0.0,
38 h: 0.0,
39 },
40 text: text.into(),
41 bg_color: ColorLinPremul::from_srgba_u8([59, 130, 246, 255]),
42 text_color: ColorLinPremul::from_srgba_u8([255, 255, 255, 255]),
43 font_size: 12.0,
44 padding_x: 8.0,
45 padding_y: 4.0,
46 }
47 }
48
49 pub fn with_colors(mut self, bg: ColorLinPremul, fg: ColorLinPremul) -> Self {
51 self.bg_color = bg;
52 self.text_color = fg;
53 self
54 }
55
56 pub fn auto_size(&mut self) {
59 let approx_text_w = self.text.len() as f32 * self.font_size * 0.6;
60 self.rect.w = approx_text_w + self.padding_x * 2.0;
61 self.rect.h = self.font_size + self.padding_y * 2.0;
62 }
63
64 fn pill_radius(&self) -> f32 {
66 self.rect.h * 0.5
67 }
68
69 pub fn hit_test(&self, x: f32, y: f32) -> bool {
71 x >= self.rect.x
72 && x <= self.rect.x + self.rect.w
73 && y >= self.rect.y
74 && y <= self.rect.y + self.rect.h
75 }
76}
77
78impl Element for Badge {
83 fn rect(&self) -> Rect {
84 self.rect
85 }
86
87 fn set_rect(&mut self, rect: Rect) {
88 self.rect = rect;
89 }
90
91 fn render(&self, canvas: &mut Canvas, z: i32) {
92 let r = self.pill_radius();
93 let rrect = RoundedRect {
94 rect: self.rect,
95 radii: RoundedRadii {
96 tl: r,
97 tr: r,
98 br: r,
99 bl: r,
100 },
101 };
102 canvas.rounded_rect(rrect, Brush::Solid(self.bg_color), z);
103
104 let approx_text_w = self.text.len() as f32 * self.font_size * 0.6;
106 let tx = self.rect.x + (self.rect.w - approx_text_w) * 0.5;
107 let ty = self.rect.y + self.rect.h * 0.5 + self.font_size * 0.35;
108 canvas.draw_text_run_weighted(
109 [tx, ty],
110 self.text.clone(),
111 self.font_size,
112 600.0,
113 self.text_color,
114 z + 1,
115 );
116 }
117
118 fn focus_id(&self) -> Option<FocusId> {
119 None
120 }
121}
122
123impl EventHandler for Badge {
128 fn handle_mouse_click(&mut self, _event: &MouseClickEvent) -> EventResult {
129 EventResult::Ignored
130 }
131
132 fn handle_keyboard(&mut self, _event: &KeyboardEvent) -> EventResult {
133 EventResult::Ignored
134 }
135
136 fn handle_mouse_move(&mut self, _event: &MouseMoveEvent) -> EventResult {
137 EventResult::Ignored
138 }
139
140 fn handle_scroll(&mut self, _event: &ScrollEvent) -> EventResult {
141 EventResult::Ignored
142 }
143
144 fn is_focused(&self) -> bool {
145 false
146 }
147
148 fn set_focused(&mut self, _focused: bool) {}
149
150 fn contains_point(&self, x: f32, y: f32) -> bool {
151 self.hit_test(x, y)
152 }
153}
154
155#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn badge_defaults() {
165 let b = Badge::new("New");
166 assert_eq!(b.text, "New");
167 assert!((b.font_size - 12.0).abs() < f32::EPSILON);
168 }
169
170 #[test]
171 fn badge_auto_size() {
172 let mut b = Badge::new("Hello");
173 b.auto_size();
174 assert!(b.rect.w > 0.0);
175 assert!(b.rect.h > 0.0);
176 assert!((b.rect.w - 52.0).abs() < 1.0);
178 }
179
180 #[test]
181 fn badge_with_colors() {
182 let red = ColorLinPremul::from_srgba_u8([255, 0, 0, 255]);
183 let white = ColorLinPremul::from_srgba_u8([255, 255, 255, 255]);
184 let b = Badge::new("Error").with_colors(red, white);
185 assert_eq!(b.bg_color, red);
186 assert_eq!(b.text_color, white);
187 }
188
189 #[test]
190 fn badge_pill_radius() {
191 let mut b = Badge::new("X");
192 b.rect.h = 24.0;
193 assert!((b.pill_radius() - 12.0).abs() < f32::EPSILON);
194 }
195
196 #[test]
197 fn badge_hit_test() {
198 let mut b = Badge::new("Test");
199 b.rect = Rect {
200 x: 10.0,
201 y: 10.0,
202 w: 60.0,
203 h: 24.0,
204 };
205 assert!(b.hit_test(30.0, 20.0));
206 assert!(!b.hit_test(0.0, 0.0));
207 }
208
209 #[test]
210 fn badge_not_focusable() {
211 let b = Badge::new("X");
212 assert!(b.focus_id().is_none());
213 }
214}