1#![forbid(unsafe_code)]
2
3use crate::{Widget, apply_style, draw_text_span};
14use ftui_core::geometry::Rect;
15use ftui_render::cell::Cell;
16use ftui_render::frame::Frame;
17use ftui_style::Style;
18use ftui_text::display_width;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub struct Badge<'a> {
23 label: &'a str,
24 style: Style,
25 pad_left: u16,
26 pad_right: u16,
27}
28
29impl<'a> Badge<'a> {
30 #[must_use]
32 pub fn new(label: &'a str) -> Self {
33 Self {
34 label,
35 style: Style::default(),
36 pad_left: 1,
37 pad_right: 1,
38 }
39 }
40
41 #[must_use]
43 pub fn with_style(mut self, style: Style) -> Self {
44 self.style = style;
45 self
46 }
47
48 #[must_use]
50 pub fn with_padding(mut self, left: u16, right: u16) -> Self {
51 self.pad_left = left;
52 self.pad_right = right;
53 self
54 }
55
56 #[inline]
58 #[must_use]
59 pub fn width(&self) -> u16 {
60 let label_width = display_width(self.label) as u16;
61 label_width
62 .saturating_add(self.pad_left)
63 .saturating_add(self.pad_right)
64 }
65
66 #[inline]
67 fn render_spaces(
68 frame: &mut Frame,
69 mut x: u16,
70 y: u16,
71 n: u16,
72 style: Style,
73 max_x: u16,
74 ) -> u16 {
75 let mut cell = Cell::from_char(' ');
76 apply_style(&mut cell, style);
77 for _ in 0..n {
78 if x >= max_x {
79 break;
80 }
81 frame.buffer.set_fast(x, y, cell);
82 x = x.saturating_add(1);
83 }
84 x
85 }
86}
87
88impl Widget for Badge<'_> {
89 fn render(&self, area: Rect, frame: &mut Frame) {
90 if area.is_empty() {
91 return;
92 }
93
94 let y = area.y;
95 let max_x = area.right();
96 let mut x = area.x;
97
98 x = Self::render_spaces(frame, x, y, self.pad_left, self.style, max_x);
99 x = draw_text_span(frame, x, y, self.label, self.style, max_x);
100 let _ = Self::render_spaces(frame, x, y, self.pad_right, self.style, max_x);
101 }
102
103 fn is_essential(&self) -> bool {
104 false
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use ftui_render::cell::PackedRgba;
112 use ftui_render::grapheme_pool::GraphemePool;
113
114 #[test]
115 fn width_includes_padding() {
116 let badge = Badge::new("OK");
117 assert_eq!(badge.width(), 4);
118 let badge = Badge::new("OK").with_padding(2, 3);
119 assert_eq!(badge.width(), 7);
120 }
121
122 #[test]
123 fn renders_padded_label_with_style() {
124 let style = Style::new()
125 .fg(PackedRgba::rgb(1, 2, 3))
126 .bg(PackedRgba::rgb(4, 5, 6));
127 let badge = Badge::new("OK").with_style(style);
128
129 let mut pool = GraphemePool::new();
130 let mut frame = Frame::new(10, 1, &mut pool);
131 badge.render(Rect::new(0, 0, 10, 1), &mut frame);
132
133 let expected = [' ', 'O', 'K', ' '];
134 for (x, ch) in expected.into_iter().enumerate() {
135 let cell = frame.buffer.get(x as u16, 0).unwrap();
136 assert_eq!(cell.content.as_char(), Some(ch));
137 assert_eq!(cell.fg, PackedRgba::rgb(1, 2, 3));
138 assert_eq!(cell.bg, PackedRgba::rgb(4, 5, 6));
139 }
140 }
141
142 #[test]
143 fn truncates_in_small_area() {
144 let style = Style::new()
145 .fg(PackedRgba::rgb(1, 2, 3))
146 .bg(PackedRgba::rgb(4, 5, 6));
147 let badge = Badge::new("OK").with_style(style);
148
149 let mut pool = GraphemePool::new();
150 let mut frame = Frame::new(2, 1, &mut pool);
151 badge.render(Rect::new(0, 0, 2, 1), &mut frame);
152
153 assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some(' '));
154 assert_eq!(frame.buffer.get(1, 0).unwrap().content.as_char(), Some('O'));
155 }
156
157 #[test]
158 fn default_padding_is_one() {
159 let badge = Badge::new("X");
160 assert_eq!(badge.width(), 3);
162 }
163
164 #[test]
165 fn zero_padding() {
166 let badge = Badge::new("AB").with_padding(0, 0);
167 assert_eq!(badge.width(), 2);
168 }
169
170 #[test]
171 fn empty_label_width() {
172 let badge = Badge::new("");
173 assert_eq!(badge.width(), 2);
175 }
176
177 #[test]
178 fn render_empty_area_is_noop() {
179 let badge = Badge::new("Test");
180 let mut pool = GraphemePool::new();
181 let mut frame = Frame::new(10, 1, &mut pool);
182 badge.render(Rect::new(0, 0, 0, 0), &mut frame);
183 }
185
186 #[test]
187 fn is_not_essential() {
188 let badge = Badge::new("OK");
189 assert!(!badge.is_essential());
190 }
191
192 #[test]
193 fn badge_eq_and_hash() {
194 let a = Badge::new("X").with_padding(1, 1);
195 let b = Badge::new("X").with_padding(1, 1);
196 assert_eq!(a, b);
197
198 let mut set = std::collections::HashSet::new();
199 set.insert(a);
200 assert!(set.contains(&b));
201 }
202
203 #[test]
204 fn badge_debug() {
205 let badge = Badge::new("OK");
206 let s = format!("{badge:?}");
207 assert!(s.contains("Badge"));
208 assert!(s.contains("OK"));
209 }
210}