use crate::render::{Cell, Modifier};
use crate::style::Color;
use crate::widget::theme::DARK_GRAY;
use crate::widget::traits::{RenderContext, View, WidgetProps};
use crate::{impl_props_builders, impl_styled_view};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BadgeVariant {
#[default]
Default,
Primary,
Success,
Warning,
Error,
Info,
}
impl BadgeVariant {
pub fn colors(&self) -> (Color, Color) {
match self {
BadgeVariant::Default => (DARK_GRAY, Color::WHITE),
BadgeVariant::Primary => (Color::rgb(50, 100, 200), Color::WHITE),
BadgeVariant::Success => (Color::rgb(40, 160, 80), Color::WHITE),
BadgeVariant::Warning => (Color::rgb(200, 150, 40), Color::BLACK),
BadgeVariant::Error => (Color::rgb(200, 60, 60), Color::WHITE),
BadgeVariant::Info => (Color::rgb(60, 160, 180), Color::WHITE),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BadgeShape {
#[default]
Rounded,
Square,
Pill,
Dot,
}
pub struct Badge {
text: String,
variant: BadgeVariant,
shape: BadgeShape,
bg_color: Option<Color>,
fg_color: Option<Color>,
bold: bool,
max_width: u16,
props: WidgetProps,
}
impl Badge {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
variant: BadgeVariant::Default,
shape: BadgeShape::Rounded,
bg_color: None,
fg_color: None,
bold: false,
max_width: 0,
props: WidgetProps::new(),
}
}
pub fn dot() -> Self {
Self {
text: String::new(),
variant: BadgeVariant::Default,
shape: BadgeShape::Dot,
bg_color: None,
fg_color: None,
bold: false,
max_width: 0,
props: WidgetProps::new(),
}
}
pub fn variant(mut self, variant: BadgeVariant) -> Self {
self.variant = variant;
self
}
pub fn shape(mut self, shape: BadgeShape) -> Self {
self.shape = shape;
self
}
pub fn primary(mut self) -> Self {
self.variant = BadgeVariant::Primary;
self
}
pub fn success(mut self) -> Self {
self.variant = BadgeVariant::Success;
self
}
pub fn warning(mut self) -> Self {
self.variant = BadgeVariant::Warning;
self
}
pub fn error(mut self) -> Self {
self.variant = BadgeVariant::Error;
self
}
pub fn info(mut self) -> Self {
self.variant = BadgeVariant::Info;
self
}
pub fn pill(mut self) -> Self {
self.shape = BadgeShape::Pill;
self
}
pub fn square(mut self) -> Self {
self.shape = BadgeShape::Square;
self
}
pub fn bg(mut self, color: Color) -> Self {
self.bg_color = Some(color);
self
}
pub fn fg(mut self, color: Color) -> Self {
self.fg_color = Some(color);
self
}
pub fn colors(mut self, bg: Color, fg: Color) -> Self {
self.bg_color = Some(bg);
self.fg_color = Some(fg);
self
}
pub fn bold(mut self) -> Self {
self.bold = true;
self
}
pub fn max_width(mut self, width: u16) -> Self {
self.max_width = width;
self
}
fn effective_colors(&self) -> (Color, Color) {
let (default_bg, default_fg) = self.variant.colors();
(
self.bg_color.unwrap_or(default_bg),
self.fg_color.unwrap_or(default_fg),
)
}
}
impl Default for Badge {
fn default() -> Self {
Self::new("")
}
}
impl View for Badge {
fn render(&self, ctx: &mut RenderContext) {
let area = ctx.area;
let (bg, fg) = self.effective_colors();
match self.shape {
BadgeShape::Dot => {
let mut cell = Cell::new('●');
cell.fg = Some(bg); ctx.set(0, 0, cell);
}
BadgeShape::Rounded | BadgeShape::Square | BadgeShape::Pill => {
let text_len = crate::utils::display_width(&self.text) as u16;
let padding = match self.shape {
BadgeShape::Pill => 2,
BadgeShape::Rounded => 1,
BadgeShape::Square => 1,
_ => 1,
};
let total_width = text_len + padding * 2;
let width = if self.max_width > 0 {
total_width.min(self.max_width).min(area.width)
} else {
total_width.min(area.width)
};
let text_chars: Vec<char> = self.text.chars().collect();
for i in 0..width {
let ch = if i < padding || i >= width - padding {
' '
} else {
let char_idx = (i - padding) as usize;
text_chars.get(char_idx).copied().unwrap_or(' ')
};
let mut cell = Cell::new(ch);
cell.fg = Some(fg);
cell.bg = Some(bg);
if self.bold {
cell.modifier |= Modifier::BOLD;
}
ctx.set(i, 0, cell);
}
}
}
}
crate::impl_view_meta!("Badge");
}
pub fn badge(text: impl Into<String>) -> Badge {
Badge::new(text)
}
pub fn dot_badge() -> Badge {
Badge::dot()
}
impl_styled_view!(Badge);
impl_props_builders!(Badge);
#[cfg(test)]
mod tests {
use super::*;
use crate::layout::Rect;
use crate::render::Buffer;
#[test]
fn test_badge_new() {
let b = Badge::new("v1.0");
assert_eq!(b.text, "v1.0");
}
#[test]
fn test_badge_dot() {
let b = Badge::dot();
assert_eq!(b.shape, BadgeShape::Dot);
}
#[test]
fn test_badge_variants() {
let b = Badge::new("OK").primary();
assert_eq!(b.variant, BadgeVariant::Primary);
let b = Badge::new("OK").success();
assert_eq!(b.variant, BadgeVariant::Success);
let b = Badge::new("ERR").error();
assert_eq!(b.variant, BadgeVariant::Error);
let b = Badge::new("!").warning();
assert_eq!(b.variant, BadgeVariant::Warning);
}
#[test]
fn test_badge_shapes() {
let _ = Badge::new("X").shape(BadgeShape::Rounded);
let _ = Badge::new("X").shape(BadgeShape::Square);
let _ = Badge::new("X").shape(BadgeShape::Pill);
}
#[test]
fn test_badge_render_no_panic() {
let mut buf = Buffer::new(15, 1);
let area = Rect::new(0, 0, 15, 1);
let mut ctx = RenderContext::new(&mut buf, area);
let b = Badge::new("Status").success();
b.render(&mut ctx);
}
#[test]
fn test_badge_helpers() {
let b = badge("test");
assert_eq!(b.text, "test");
let d = dot_badge();
assert_eq!(d.shape, BadgeShape::Dot);
}
}