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 #[must_use]
58 pub fn width(&self) -> u16 {
59 let label_width = display_width(self.label) as u16;
60 label_width
61 .saturating_add(self.pad_left)
62 .saturating_add(self.pad_right)
63 }
64
65 #[inline]
66 fn render_spaces(
67 frame: &mut Frame,
68 mut x: u16,
69 y: u16,
70 n: u16,
71 style: Style,
72 max_x: u16,
73 ) -> u16 {
74 let mut cell = Cell::from_char(' ');
75 apply_style(&mut cell, style);
76 for _ in 0..n {
77 if x >= max_x {
78 break;
79 }
80 frame.buffer.set(x, y, cell);
81 x = x.saturating_add(1);
82 }
83 x
84 }
85}
86
87impl Widget for Badge<'_> {
88 fn render(&self, area: Rect, frame: &mut Frame) {
89 if area.is_empty() {
90 return;
91 }
92
93 let y = area.y;
94 let max_x = area.right();
95 let mut x = area.x;
96
97 x = Self::render_spaces(frame, x, y, self.pad_left, self.style, max_x);
98 x = draw_text_span(frame, x, y, self.label, self.style, max_x);
99 let _ = Self::render_spaces(frame, x, y, self.pad_right, self.style, max_x);
100 }
101
102 fn is_essential(&self) -> bool {
103 false
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use ftui_render::cell::PackedRgba;
111 use ftui_render::grapheme_pool::GraphemePool;
112
113 #[test]
114 fn width_includes_padding() {
115 let badge = Badge::new("OK");
116 assert_eq!(badge.width(), 4);
117 let badge = Badge::new("OK").with_padding(2, 3);
118 assert_eq!(badge.width(), 7);
119 }
120
121 #[test]
122 fn renders_padded_label_with_style() {
123 let style = Style::new()
124 .fg(PackedRgba::rgb(1, 2, 3))
125 .bg(PackedRgba::rgb(4, 5, 6));
126 let badge = Badge::new("OK").with_style(style);
127
128 let mut pool = GraphemePool::new();
129 let mut frame = Frame::new(10, 1, &mut pool);
130 badge.render(Rect::new(0, 0, 10, 1), &mut frame);
131
132 let expected = [' ', 'O', 'K', ' '];
133 for (x, ch) in expected.into_iter().enumerate() {
134 let cell = frame.buffer.get(x as u16, 0).unwrap();
135 assert_eq!(cell.content.as_char(), Some(ch));
136 assert_eq!(cell.fg, PackedRgba::rgb(1, 2, 3));
137 assert_eq!(cell.bg, PackedRgba::rgb(4, 5, 6));
138 }
139 }
140
141 #[test]
142 fn truncates_in_small_area() {
143 let style = Style::new()
144 .fg(PackedRgba::rgb(1, 2, 3))
145 .bg(PackedRgba::rgb(4, 5, 6));
146 let badge = Badge::new("OK").with_style(style);
147
148 let mut pool = GraphemePool::new();
149 let mut frame = Frame::new(2, 1, &mut pool);
150 badge.render(Rect::new(0, 0, 2, 1), &mut frame);
151
152 assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some(' '));
153 assert_eq!(frame.buffer.get(1, 0).unwrap().content.as_char(), Some('O'));
154 }
155}