1use super::traits::{RenderContext, View, WidgetProps};
4use crate::render::{Cell, Modifier};
5use crate::style::Color;
6use crate::{impl_props_builders, impl_styled_view};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum BadgeVariant {
11 #[default]
13 Default,
14 Primary,
16 Success,
18 Warning,
20 Error,
22 Info,
24}
25
26impl BadgeVariant {
27 pub fn colors(&self) -> (Color, Color) {
29 match self {
30 BadgeVariant::Default => (Color::rgb(80, 80, 80), Color::WHITE),
31 BadgeVariant::Primary => (Color::rgb(50, 100, 200), Color::WHITE),
32 BadgeVariant::Success => (Color::rgb(40, 160, 80), Color::WHITE),
33 BadgeVariant::Warning => (Color::rgb(200, 150, 40), Color::BLACK),
34 BadgeVariant::Error => (Color::rgb(200, 60, 60), Color::WHITE),
35 BadgeVariant::Info => (Color::rgb(60, 160, 180), Color::WHITE),
36 }
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
42pub enum BadgeShape {
43 #[default]
45 Rounded,
46 Square,
48 Pill,
50 Dot,
52}
53
54pub struct Badge {
66 text: String,
68 variant: BadgeVariant,
70 shape: BadgeShape,
72 bg_color: Option<Color>,
74 fg_color: Option<Color>,
76 bold: bool,
78 max_width: u16,
80 props: WidgetProps,
81}
82
83impl Badge {
84 pub fn new(text: impl Into<String>) -> Self {
86 Self {
87 text: text.into(),
88 variant: BadgeVariant::Default,
89 shape: BadgeShape::Rounded,
90 bg_color: None,
91 fg_color: None,
92 bold: false,
93 max_width: 0,
94 props: WidgetProps::new(),
95 }
96 }
97
98 pub fn dot() -> Self {
100 Self {
101 text: String::new(),
102 variant: BadgeVariant::Default,
103 shape: BadgeShape::Dot,
104 bg_color: None,
105 fg_color: None,
106 bold: false,
107 max_width: 0,
108 props: WidgetProps::new(),
109 }
110 }
111
112 pub fn variant(mut self, variant: BadgeVariant) -> Self {
114 self.variant = variant;
115 self
116 }
117
118 pub fn shape(mut self, shape: BadgeShape) -> Self {
120 self.shape = shape;
121 self
122 }
123
124 pub fn primary(mut self) -> Self {
126 self.variant = BadgeVariant::Primary;
127 self
128 }
129
130 pub fn success(mut self) -> Self {
132 self.variant = BadgeVariant::Success;
133 self
134 }
135
136 pub fn warning(mut self) -> Self {
138 self.variant = BadgeVariant::Warning;
139 self
140 }
141
142 pub fn error(mut self) -> Self {
144 self.variant = BadgeVariant::Error;
145 self
146 }
147
148 pub fn info(mut self) -> Self {
150 self.variant = BadgeVariant::Info;
151 self
152 }
153
154 pub fn pill(mut self) -> Self {
156 self.shape = BadgeShape::Pill;
157 self
158 }
159
160 pub fn square(mut self) -> Self {
162 self.shape = BadgeShape::Square;
163 self
164 }
165
166 pub fn bg(mut self, color: Color) -> Self {
168 self.bg_color = Some(color);
169 self
170 }
171
172 pub fn fg(mut self, color: Color) -> Self {
174 self.fg_color = Some(color);
175 self
176 }
177
178 pub fn colors(mut self, bg: Color, fg: Color) -> Self {
180 self.bg_color = Some(bg);
181 self.fg_color = Some(fg);
182 self
183 }
184
185 pub fn bold(mut self) -> Self {
187 self.bold = true;
188 self
189 }
190
191 pub fn max_width(mut self, width: u16) -> Self {
193 self.max_width = width;
194 self
195 }
196
197 fn effective_colors(&self) -> (Color, Color) {
199 let (default_bg, default_fg) = self.variant.colors();
200 (
201 self.bg_color.unwrap_or(default_bg),
202 self.fg_color.unwrap_or(default_fg),
203 )
204 }
205}
206
207impl Default for Badge {
208 fn default() -> Self {
209 Self::new("")
210 }
211}
212
213impl View for Badge {
214 fn render(&self, ctx: &mut RenderContext) {
215 let area = ctx.area;
216 let (bg, fg) = self.effective_colors();
217
218 match self.shape {
219 BadgeShape::Dot => {
220 let mut cell = Cell::new('●');
222 cell.fg = Some(bg); ctx.buffer.set(area.x, area.y, cell);
224 }
225 BadgeShape::Rounded | BadgeShape::Square | BadgeShape::Pill => {
226 let text_len = self.text.chars().count() as u16;
227 let padding = match self.shape {
228 BadgeShape::Pill => 2,
229 BadgeShape::Rounded => 1,
230 BadgeShape::Square => 1,
231 _ => 1,
232 };
233
234 let total_width = text_len + padding * 2;
235 let width = if self.max_width > 0 {
236 total_width.min(self.max_width).min(area.width)
237 } else {
238 total_width.min(area.width)
239 };
240
241 for i in 0..width {
243 let x = area.x + i;
244 let ch = if i < padding || i >= width - padding {
245 ' '
246 } else {
247 let char_idx = (i - padding) as usize;
248 self.text.chars().nth(char_idx).unwrap_or(' ')
249 };
250
251 let mut cell = Cell::new(ch);
252 cell.fg = Some(fg);
253 cell.bg = Some(bg);
254 if self.bold {
255 cell.modifier |= Modifier::BOLD;
256 }
257 ctx.buffer.set(x, area.y, cell);
258 }
259 }
260 }
261 }
262
263 crate::impl_view_meta!("Badge");
264}
265
266pub fn badge(text: impl Into<String>) -> Badge {
268 Badge::new(text)
269}
270
271pub fn dot_badge() -> Badge {
273 Badge::dot()
274}
275
276impl_styled_view!(Badge);
277impl_props_builders!(Badge);
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use crate::layout::Rect;
283 use crate::render::Buffer;
284
285 #[test]
286 fn test_badge_new() {
287 let b = Badge::new("Test");
288 assert_eq!(b.text, "Test");
289 assert_eq!(b.variant, BadgeVariant::Default);
290 }
291
292 #[test]
293 fn test_badge_variants() {
294 let b = badge("OK").success();
295 assert_eq!(b.variant, BadgeVariant::Success);
296
297 let b = badge("Error").error();
298 assert_eq!(b.variant, BadgeVariant::Error);
299
300 let b = badge("Info").info();
301 assert_eq!(b.variant, BadgeVariant::Info);
302 }
303
304 #[test]
305 fn test_badge_shapes() {
306 let b = badge("Tag").pill();
307 assert_eq!(b.shape, BadgeShape::Pill);
308
309 let b = badge("Box").square();
310 assert_eq!(b.shape, BadgeShape::Square);
311 }
312
313 #[test]
314 fn test_badge_dot() {
315 let b = Badge::dot().success();
316 assert_eq!(b.shape, BadgeShape::Dot);
317 }
318
319 #[test]
320 fn test_badge_render() {
321 let mut buffer = Buffer::new(20, 1);
322 let area = Rect::new(0, 0, 20, 1);
323 let mut ctx = RenderContext::new(&mut buffer, area);
324
325 let b = badge("NEW").primary();
326 b.render(&mut ctx);
327
328 let text: String = (0..20)
330 .filter_map(|x| buffer.get(x, 0).map(|c| c.symbol))
331 .collect();
332 assert!(text.contains("NEW"));
333 }
334
335 #[test]
336 fn test_badge_dot_render() {
337 let mut buffer = Buffer::new(5, 1);
338 let area = Rect::new(0, 0, 5, 1);
339 let mut ctx = RenderContext::new(&mut buffer, area);
340
341 let b = dot_badge().success();
342 b.render(&mut ctx);
343
344 assert_eq!(buffer.get(0, 0).map(|c| c.symbol), Some('●'));
345 }
346
347 #[test]
348 fn test_variant_colors() {
349 let (bg, fg) = BadgeVariant::Success.colors();
350 assert_eq!(fg, Color::WHITE);
351 assert_ne!(bg, Color::WHITE);
352 }
353
354 #[test]
355 fn test_custom_colors() {
356 let b = badge("Test").bg(Color::MAGENTA).fg(Color::BLACK);
357
358 let (bg, fg) = b.effective_colors();
359 assert_eq!(bg, Color::MAGENTA);
360 assert_eq!(fg, Color::BLACK);
361 }
362
363 #[test]
364 fn test_helper_functions() {
365 let b = badge("Hi");
366 assert_eq!(b.text, "Hi");
367
368 let d = dot_badge();
369 assert_eq!(d.shape, BadgeShape::Dot);
370 }
371}