1use std::sync::Arc;
4
5use crate::core::element::{Element, IntoElement};
6use crate::style::{BorderStyle, Color, Length, Padding, Style};
7use crate::widgets::{CapSides, CapStyle, Frame, HStack, Spacer, Text, VStack, ZStack};
8
9use super::segment_cap::segment_cap;
10
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
13pub enum BadgePosition {
14 TopStart,
16 #[default]
18 TopEnd,
19 BottomStart,
21 BottomEnd,
23}
24
25#[derive(Clone)]
27pub struct Badge {
28 content: Arc<str>,
29 child: Element,
30 style: Style,
31 text_style: Style,
32 border: bool,
33 border_style: BorderStyle,
34 padding: Padding,
35 offset: Padding,
36 position: BadgePosition,
37 width: Length,
38 height: Length,
39 cap_style: CapStyle,
40 cap_sides: CapSides,
41 cap_behind: Color,
42 cap_same_color: bool,
43}
44
45impl Badge {
46 pub fn new(content: impl Into<Arc<str>>) -> Self {
48 Self {
49 content: content.into(),
50 child: crate::widgets::Spacer::new().into(),
51 style: Style::default(),
52 text_style: Style::default(),
53 border: false,
54 border_style: BorderStyle::Plain,
55 padding: 0.into(),
56 offset: 0.into(),
57 position: BadgePosition::TopEnd,
58 width: Length::Auto,
59 height: Length::Auto,
60 cap_style: CapStyle::Padded,
61 cap_sides: CapSides::Both,
62 cap_behind: Color::Reset,
63 cap_same_color: false,
64 }
65 }
66
67 pub fn child(mut self, child: impl IntoElement) -> Self {
69 self.child = child.into();
70 self
71 }
72
73 pub fn style(mut self, style: Style) -> Self {
75 self.style = style;
76 self
77 }
78
79 pub fn text_style(mut self, style: Style) -> Self {
81 self.text_style = style;
82 self
83 }
84
85 pub fn border(mut self, border: bool) -> Self {
87 self.border = border;
88 self
89 }
90
91 pub fn border_style(mut self, border_style: BorderStyle) -> Self {
93 self.border_style = border_style;
94 self
95 }
96
97 pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
99 self.padding = padding.into();
100 self
101 }
102
103 pub fn offset(mut self, offset: impl Into<Padding>) -> Self {
105 self.offset = offset.into();
106 self
107 }
108
109 pub fn position(mut self, position: BadgePosition) -> Self {
111 self.position = position;
112 self
113 }
114
115 pub fn width(mut self, width: Length) -> Self {
117 self.width = width;
118 self
119 }
120
121 pub fn height(mut self, height: Length) -> Self {
123 self.height = height;
124 self
125 }
126
127 pub fn cap(mut self, cap_style: CapStyle) -> Self {
134 self.cap_style = cap_style;
135 self
136 }
137
138 pub fn cap_sides(mut self, cap_sides: CapSides) -> Self {
140 self.cap_sides = cap_sides;
141 self
142 }
143
144 pub fn cap_behind(mut self, color: Color) -> Self {
146 self.cap_behind = color;
147 self
148 }
149
150 pub fn cap_same_color(mut self, same_color: bool) -> Self {
155 self.cap_same_color = same_color;
156 self
157 }
158}
159
160impl From<Badge> for Element {
161 fn from(badge: Badge) -> Self {
162 let badge_style = badge.style;
163 let text_style = badge_style.patch(badge.text_style);
164 let badge_bg = badge_style
165 .bg
166 .map(crate::style::Paint::color)
167 .unwrap_or(crate::style::Color::Reset);
168
169 let (content, padding) = replace_padding_with_caps(
170 badge.content,
171 badge.padding,
172 badge.cap_style,
173 badge.cap_sides,
174 badge.cap_same_color,
175 );
176
177 let badge_el = Frame::new()
178 .border(badge.border)
179 .border_style(badge.border_style)
180 .padding(padding)
181 .style(badge_style)
182 .child(Text::new(content).style(text_style))
183 .width(badge.width)
184 .height(badge.height);
185
186 let badge_el = segment_cap(
187 badge_el.into(),
188 badge.cap_style,
189 badge.cap_sides,
190 badge_bg,
191 badge.cap_behind,
192 badge.cap_same_color,
193 );
194
195 let overlay_row = match badge.position {
196 BadgePosition::TopStart | BadgePosition::BottomStart => {
197 HStack::new().child(badge_el).child(Spacer::new())
198 }
199 BadgePosition::TopEnd | BadgePosition::BottomEnd => {
200 HStack::new().child(Spacer::new()).child(badge_el)
201 }
202 };
203
204 let overlay_column = match badge.position {
205 BadgePosition::TopStart | BadgePosition::TopEnd => {
206 VStack::new().child(overlay_row).child(Spacer::new())
207 }
208 BadgePosition::BottomStart | BadgePosition::BottomEnd => {
209 VStack::new().child(Spacer::new()).child(overlay_row)
210 }
211 };
212
213 let overlay = overlay_column.padding(badge.offset);
214
215 ZStack::new()
216 .passthrough(true)
217 .child(badge.child)
218 .child(overlay)
219 .into()
220 }
221}
222
223fn replace_padding_with_caps(
226 mut content: Arc<str>,
227 mut padding: Padding,
228 cap_style: CapStyle,
229 cap_sides: CapSides,
230 same_color_left: bool,
231) -> (Arc<str>, Padding) {
232 let has_glyphs = cap_style.glyphs().is_some();
233 let replace_left = cap_sides.has_left() && (has_glyphs || same_color_left);
234 let replace_right = cap_sides.has_right() && has_glyphs;
235
236 if replace_left {
237 if padding.left > 0 {
238 padding.left -= 1;
239 } else if let Some(stripped) = content.strip_prefix(' ') {
240 content = Arc::from(stripped);
241 }
242 }
243 if replace_right {
244 if padding.right > 0 {
245 padding.right -= 1;
246 } else if let Some(stripped) = content.strip_suffix(' ') {
247 content = Arc::from(stripped);
248 }
249 }
250
251 (content, padding)
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn caps_replace_label_spaces_without_changing_intrinsic_width() {
260 let (content, padding) = replace_padding_with_caps(
261 Arc::from(" MAIN "),
262 Padding::default(),
263 CapStyle::Half,
264 CapSides::Both,
265 false,
266 );
267
268 assert_eq!(content.as_ref(), "MAIN");
269 assert_eq!(padding, Padding::default());
270 assert_eq!(
271 2 + content.chars().count() + padding.horizontal() as usize,
272 6
273 );
274 }
275
276 #[test]
277 fn caps_prefer_explicit_padding_without_changing_intrinsic_width() {
278 let (content, padding) = replace_padding_with_caps(
279 Arc::from(" MAIN "),
280 Padding::from((0, 1)),
281 CapStyle::Round,
282 CapSides::Both,
283 false,
284 );
285
286 assert_eq!(content.as_ref(), " MAIN ");
287 assert_eq!(padding, Padding::default());
288 assert_eq!(
289 2 + content.chars().count() + padding.horizontal() as usize,
290 8
291 );
292 }
293
294 #[test]
295 fn padded_same_color_separator_replaces_left_padding() {
296 let (content, padding) = replace_padding_with_caps(
297 Arc::from(" READY "),
298 Padding::default(),
299 CapStyle::Padded,
300 CapSides::Both,
301 true,
302 );
303
304 assert_eq!(content.as_ref(), "READY ");
305 assert_eq!(padding, Padding::default());
306 assert_eq!(
307 1 + content.chars().count() + padding.horizontal() as usize,
308 7
309 );
310 }
311}