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