Skip to main content

jag_ui/elements/
badge.rs

1//! Small label badge element.
2
3use 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
13/// A small rounded pill badge for labeling or status indication.
14pub struct Badge {
15    pub rect: Rect,
16    /// Display text.
17    pub text: String,
18    /// Background color.
19    pub bg_color: ColorLinPremul,
20    /// Text color.
21    pub text_color: ColorLinPremul,
22    /// Font size.
23    pub font_size: f32,
24    /// Horizontal padding.
25    pub padding_x: f32,
26    /// Vertical padding.
27    pub padding_y: f32,
28}
29
30impl Badge {
31    /// Create a badge with default styling (blue pill).
32    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    /// Set custom colors.
50    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    /// Compute the auto-sized rect based on text length.
57    /// The badge sizes itself around its text if rect dimensions are zero.
58    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    /// Corner radius for the pill shape (half of height).
65    fn pill_radius(&self) -> f32 {
66        self.rect.h * 0.5
67    }
68
69    /// Hit-test.
70    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
78// ---------------------------------------------------------------------------
79// Element trait
80// ---------------------------------------------------------------------------
81
82impl 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        // Centered text
105        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
123// ---------------------------------------------------------------------------
124// EventHandler trait
125// ---------------------------------------------------------------------------
126
127impl 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// ---------------------------------------------------------------------------
156// Tests
157// ---------------------------------------------------------------------------
158
159#[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        // Width should be approx 5 chars * 12 * 0.6 + 16 padding = 52
177        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}