photon_ui/components/
breadcrumbs.rs1use crate::{
6 Component,
7 RenderError,
8 Rendered,
9 theme::{
10 Style,
11 Theme,
12 stylize,
13 },
14};
15
16pub struct Breadcrumbs {
22 items: Vec<String>,
23 separator: String,
24}
25
26impl Breadcrumbs {
27 pub fn new(items: Vec<impl Into<String>>) -> Self {
29 Self {
30 items: items.into_iter().map(Into::into).collect(),
31 separator: " > ".to_string(),
32 }
33 }
34
35 pub fn separator(mut self, sep: impl Into<String>) -> Self {
37 self.separator = sep.into();
38 self
39 }
40}
41
42impl Component for Breadcrumbs {
43 fn render(&self, _width: u16) -> Result<Rendered, RenderError> {
44 let theme = Theme::palette();
45 let primary_style = Style::new().fg(theme.text()).bold();
46 let secondary_style = Style::new().fg(theme.text_muted());
47 let separator_style = Style::new().fg(theme.border());
48
49 let mut parts: Vec<String> = Vec::new();
50 let count = self.items.len();
51
52 for (i, item) in self.items.iter().enumerate() {
53 if i > 0 {
54 parts.push(stylize(&self.separator, &separator_style));
55 }
56
57 let styled = if i == count.saturating_sub(1) {
58 stylize(item, &primary_style)
59 } else {
60 stylize(item, &secondary_style)
61 };
62 parts.push(styled);
63 }
64
65 let line = parts.concat();
66
67 Ok(Rendered {
68 lines: vec![line],
69 cursor: None,
70 images: Vec::new(),
71 })
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78 use crate::theme::Theme;
79
80 #[test]
81 fn renders_single_item_bold() {
82 Theme::with(Theme::Light, || {
83 let bc = Breadcrumbs::new(vec!["Home"]);
84 let rendered = bc.render(80).unwrap();
85 assert_eq!(rendered.lines.len(), 1);
86 let line = &rendered.lines[0];
87 assert!(line.contains("Home"));
88 assert!(line.contains("\x1b[1m"));
89 });
90 }
91
92 #[test]
93 fn renders_multiple_items_with_separator() {
94 Theme::with(Theme::Light, || {
95 let bc = Breadcrumbs::new(vec!["Home", "Settings", "Account"]);
96 let rendered = bc.render(80).unwrap();
97 let line = &rendered.lines[0];
98
99 assert!(line.contains("Home"));
100 assert!(line.contains("Settings"));
101 assert!(line.contains("Account"));
102 assert!(line.contains(" > "));
103 });
104 }
105
106 #[test]
107 fn custom_separator() {
108 Theme::with(Theme::Light, || {
109 let bc = Breadcrumbs::new(vec!["A", "B"]).separator(" / ");
110 let rendered = bc.render(80).unwrap();
111 let line = &rendered.lines[0];
112
113 assert!(line.contains(" / "));
114 assert!(!line.contains(" > "));
115 });
116 }
117
118 #[test]
119 fn empty_items_renders_empty_line() {
120 Theme::with(Theme::Light, || {
121 let bc = Breadcrumbs::new(Vec::<String>::new());
122 let rendered = bc.render(80).unwrap();
123 assert_eq!(rendered.lines.len(), 1);
124 assert_eq!(rendered.lines[0], "");
125 });
126 }
127
128 #[test]
129 fn uses_primary_color_for_last_item() {
130 Theme::with(Theme::Light, || {
131 let bc = Breadcrumbs::new(vec!["Home", "Settings"]);
132 let rendered = bc.render(80).unwrap();
133 let line = &rendered.lines[0];
134
135 assert!(line.contains("\x1b[38;2;31;31;31m"));
137 });
138 }
139
140 #[test]
141 fn uses_secondary_color_for_earlier_items() {
142 Theme::with(Theme::Light, || {
143 let bc = Breadcrumbs::new(vec!["Home", "Settings"]);
144 let rendered = bc.render(80).unwrap();
145 let line = &rendered.lines[0];
146
147 assert!(line.contains("\x1b[38;2;102;102;102m"));
149 });
150 }
151
152 #[test]
153 fn last_item_is_bold() {
154 Theme::with(Theme::Light, || {
155 let bc = Breadcrumbs::new(vec!["Home", "Settings"]);
156 let rendered = bc.render(80).unwrap();
157 let line = &rendered.lines[0];
158
159 assert!(line.contains("\x1b[1m"));
161 });
162 }
163}