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 TagStyle {
11 #[default]
13 Filled,
14 Outlined,
16 Subtle,
18}
19
20pub struct Tag {
33 text: String,
35 color: Color,
37 text_color: Option<Color>,
39 style: TagStyle,
41 closable: bool,
43 icon: Option<char>,
45 selected: bool,
47 disabled: bool,
49 props: WidgetProps,
51}
52
53impl Tag {
54 pub fn new(text: impl Into<String>) -> Self {
56 Self {
57 text: text.into(),
58 color: Color::rgb(80, 80, 80),
59 text_color: None,
60 style: TagStyle::Filled,
61 closable: false,
62 icon: None,
63 selected: false,
64 disabled: false,
65 props: WidgetProps::new(),
66 }
67 }
68
69 pub fn color(mut self, color: Color) -> Self {
71 self.color = color;
72 self
73 }
74
75 pub fn text_color(mut self, color: Color) -> Self {
77 self.text_color = Some(color);
78 self
79 }
80
81 pub fn style(mut self, style: TagStyle) -> Self {
83 self.style = style;
84 self
85 }
86
87 pub fn outlined(mut self) -> Self {
89 self.style = TagStyle::Outlined;
90 self
91 }
92
93 pub fn subtle(mut self) -> Self {
95 self.style = TagStyle::Subtle;
96 self
97 }
98
99 pub fn closable(mut self) -> Self {
101 self.closable = true;
102 self
103 }
104
105 pub fn icon(mut self, icon: char) -> Self {
107 self.icon = Some(icon);
108 self
109 }
110
111 pub fn selected(mut self) -> Self {
113 self.selected = true;
114 self
115 }
116
117 pub fn disabled(mut self) -> Self {
119 self.disabled = true;
120 self
121 }
122
123 pub fn blue(mut self) -> Self {
125 self.color = Color::rgb(60, 120, 200);
126 self
127 }
128
129 pub fn green(mut self) -> Self {
131 self.color = Color::rgb(40, 160, 80);
132 self
133 }
134
135 pub fn red(mut self) -> Self {
137 self.color = Color::rgb(200, 60, 60);
138 self
139 }
140
141 pub fn yellow(mut self) -> Self {
143 self.color = Color::rgb(200, 180, 40);
144 self
145 }
146
147 pub fn purple(mut self) -> Self {
149 self.color = Color::rgb(140, 80, 180);
150 self
151 }
152
153 fn effective_colors(&self) -> (Option<Color>, Color) {
155 let text_color = self.text_color.unwrap_or(Color::WHITE);
156
157 if self.disabled {
158 return (Some(Color::rgb(60, 60, 60)), Color::rgb(120, 120, 120));
159 }
160
161 match self.style {
162 TagStyle::Filled => (Some(self.color), text_color),
163 TagStyle::Outlined => (None, self.color),
164 TagStyle::Subtle => {
165 let light_bg = Color::rgb(
167 self.color.r.saturating_add(180),
168 self.color.g.saturating_add(180),
169 self.color.b.saturating_add(180),
170 );
171 (Some(light_bg), self.color)
172 }
173 }
174 }
175}
176
177impl Default for Tag {
178 fn default() -> Self {
179 Self::new("")
180 }
181}
182
183impl View for Tag {
184 crate::impl_view_meta!("Tag");
185
186 fn render(&self, ctx: &mut RenderContext) {
187 let area = ctx.area;
188 let (bg, fg) = self.effective_colors();
189
190 let mut content = String::new();
191
192 if let Some(icon) = self.icon {
194 content.push(icon);
195 content.push(' ');
196 }
197
198 content.push_str(&self.text);
200
201 if self.closable {
203 content.push_str(" ×");
204 }
205
206 let _text_len = content.chars().count() as u16;
207
208 let (left_char, right_char) = match self.style {
210 TagStyle::Outlined => ('⟨', '⟩'),
211 _ => (' ', ' '),
212 };
213
214 let mut x = area.x;
216
217 let mut left = Cell::new(left_char);
219 if let Some(bg_color) = bg {
220 left.bg = Some(bg_color);
221 }
222 left.fg = Some(fg);
223 ctx.buffer.set(x, area.y, left);
224 x += 1;
225
226 for ch in content.chars() {
228 if x >= area.x + area.width - 1 {
229 break;
230 }
231 let mut cell = Cell::new(ch);
232 cell.fg = Some(fg);
233 if let Some(bg_color) = bg {
234 cell.bg = Some(bg_color);
235 }
236 if self.selected {
237 cell.modifier |= Modifier::BOLD;
238 }
239 if self.disabled {
240 cell.modifier |= Modifier::DIM;
241 }
242 ctx.buffer.set(x, area.y, cell);
243 x += 1;
244 }
245
246 if x < area.x + area.width {
248 let mut right = Cell::new(right_char);
249 if let Some(bg_color) = bg {
250 right.bg = Some(bg_color);
251 }
252 right.fg = Some(fg);
253 ctx.buffer.set(x, area.y, right);
254 }
255 }
256}
257
258impl_styled_view!(Tag);
259impl_props_builders!(Tag);
260
261pub fn tag(text: impl Into<String>) -> Tag {
263 Tag::new(text)
264}
265
266pub fn chip(text: impl Into<String>) -> Tag {
268 Tag::new(text)
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use crate::layout::Rect;
275 use crate::render::Buffer;
276
277 #[test]
278 fn test_tag_new() {
279 let t = Tag::new("Rust");
280 assert_eq!(t.text, "Rust");
281 assert!(!t.closable);
282 }
283
284 #[test]
285 fn test_tag_styles() {
286 let t = tag("Test").outlined();
287 assert_eq!(t.style, TagStyle::Outlined);
288
289 let t = tag("Test").subtle();
290 assert_eq!(t.style, TagStyle::Subtle);
291 }
292
293 #[test]
294 fn test_tag_colors() {
295 let t = tag("Test").blue();
296 assert_eq!(t.color, Color::rgb(60, 120, 200));
297
298 let t = tag("Test").red();
299 assert_eq!(t.color, Color::rgb(200, 60, 60));
300 }
301
302 #[test]
303 fn test_tag_closable() {
304 let t = tag("Test").closable();
305 assert!(t.closable);
306 }
307
308 #[test]
309 fn test_tag_icon() {
310 let t = tag("Rust").icon('🦀');
311 assert_eq!(t.icon, Some('🦀'));
312 }
313
314 #[test]
315 fn test_tag_selected_disabled() {
316 let t = tag("Test").selected().disabled();
317 assert!(t.selected);
318 assert!(t.disabled);
319 }
320
321 #[test]
322 fn test_tag_render() {
323 let mut buffer = Buffer::new(20, 1);
324 let area = Rect::new(0, 0, 20, 1);
325 let mut ctx = RenderContext::new(&mut buffer, area);
326
327 let t = tag("Rust").blue();
328 t.render(&mut ctx);
329
330 let text: String = (0..20)
331 .filter_map(|x| buffer.get(x, 0).map(|c| c.symbol))
332 .collect();
333 assert!(text.contains("Rust"));
334 }
335
336 #[test]
337 fn test_helper_functions() {
338 let t = tag("A");
339 assert_eq!(t.text, "A");
340
341 let c = chip("B");
342 assert_eq!(c.text, "B");
343 }
344}