1use crate::{
12 document::{BlockPath, Document},
13 store::RectId,
14 tools::{
15 nav_tree::{PathHistory, level_blocks},
16 tool::Action,
17 toolbar::icon_button,
18 },
19};
20
21const BACK_ICON: egui::ImageSource<'static> =
22 egui::include_image!("../../icons/icon-arrow-left.svg");
23const FORWARD_ICON: egui::ImageSource<'static> =
24 egui::include_image!("../../icons/icon-arrow-right.svg");
25
26const MAX_CRUMBS: usize = 5;
29
30const ROOT_CRUMB: &str = "top";
33
34const ELLIPSIS: &str = "…";
37
38const SEPARATOR: &str = "/";
40
41const FIELD_ROUNDING: f32 = 6.0;
44const FIELD_PAD_X: i8 = 6;
45const FIELD_PAD_Y: i8 = 1;
46const CRUMB_GAP: f32 = 3.0;
47
48struct Crumb {
51 target: Option<RectId>,
52 label: String,
53}
54
55fn crumbs(document: &Document, path: &BlockPath) -> (bool, Vec<Crumb>) {
59 let mut all = vec![Crumb {
60 target: None,
61 label: ROOT_CRUMB.to_owned(),
62 }];
63 all.extend(path.segments().iter().map(|&id| Crumb {
64 target: Some(id),
65 label: crate::tools::nav_tree::block_label(document, id),
66 }));
67 let elided = all.len() > MAX_CRUMBS;
68 if elided {
69 all.drain(..all.len() - MAX_CRUMBS);
70 }
71 (elided, all)
72}
73
74#[derive(Clone, Copy)]
77pub struct AddressScene<'a> {
78 pub document: &'a Document,
79 pub path: &'a BlockPath,
80 pub history: PathHistory,
81}
82
83pub(crate) fn arrows(ui: &mut egui::Ui, history: PathHistory) -> Option<Action> {
86 let mut action = None;
87 if ui
88 .add_enabled(history.can_back, icon_button(ui, BACK_ICON))
89 .on_hover_text("Back")
90 .clicked()
91 {
92 action = Some(Action::PathBack);
93 }
94 if ui
95 .add_enabled(history.can_forward, icon_button(ui, FORWARD_ICON))
96 .on_hover_text("Forward")
97 .clicked()
98 {
99 action = Some(Action::PathForward);
100 }
101 action
102}
103
104pub(crate) fn breadcrumb(
107 ui: &mut egui::Ui,
108 document: &Document,
109 path: &BlockPath,
110) -> Option<Action> {
111 let mut action = None;
112 egui::Frame::new()
113 .fill(ui.visuals().extreme_bg_color)
114 .corner_radius(FIELD_ROUNDING)
115 .inner_margin(egui::Margin::symmetric(FIELD_PAD_X, FIELD_PAD_Y))
116 .show(ui, |ui| {
117 ui.horizontal(|ui| {
118 ui.spacing_mut().item_spacing.x = CRUMB_GAP;
119 let (elided, crumbs) = crumbs(document, path);
120 if elided {
121 ui.weak(ELLIPSIS);
124 ui.weak(SEPARATOR);
125 }
126 for crumb in &crumbs {
127 if ui.button(&crumb.label).clicked() {
128 action = Some(Action::PathTo(crumb.target));
129 }
130 ui.weak(SEPARATOR);
131 }
132 if let Some(descend) = descend_menu(ui, document, path) {
133 action = Some(descend);
134 }
135 });
136 });
137 action
138}
139
140fn descend_menu(ui: &mut egui::Ui, document: &Document, path: &BlockPath) -> Option<Action> {
144 let children = level_blocks(document, path);
145 if children.is_empty() {
146 ui.add_enabled(false, egui::Button::new(ELLIPSIS))
147 .on_disabled_hover_text("Nothing to open here");
148 return None;
149 }
150 let mut picked = None;
151 ui.menu_button(ELLIPSIS, |ui| {
152 for (id, label) in &children {
153 if ui.button(label).clicked() {
154 picked = Some(Action::ExpandBlock(*id));
155 ui.close();
156 }
157 }
158 })
159 .response
160 .on_hover_text("Open a block on this level");
161 picked
162}
163
164#[cfg(all(test, feature = "kittest"))]
165mod kittest_visual {
166 use super::*;
167 use crate::canvas::palette::Luminance;
168 use crate::document::Block;
169 use crate::font::build_fonts;
170 use crate::preferences::{FontChoice, Theme};
171 use egui::{Rect, pos2, vec2};
172 use egui_kittest::Harness;
173
174 #[test]
176 fn address_bar_strip() {
177 let mut doc = Document::default();
178 let mut parent = doc.top_id;
179 let mut path = BlockPath::empty();
180 for name in ["Thing 1", "Core"] {
181 let id = doc.add_child(
182 parent,
183 Block::new(
184 name.into(),
185 Rect::from_min_max(pos2(0.0, 0.0), pos2(60.0, 60.0)),
186 ),
187 );
188 path.push(id);
189 parent = id;
190 }
191 doc.add_child(
193 parent,
194 Block::new(
195 "alu".into(),
196 Rect::from_min_max(pos2(0.0, 0.0), pos2(60.0, 60.0)),
197 ),
198 );
199 let mut harness = Harness::builder()
200 .with_size(vec2(560.0, 90.0))
201 .build_ui(move |ui| {
202 let ctx = ui.ctx().clone();
203 egui_extras::install_image_loaders(&ctx);
204 ctx.set_fonts(build_fonts(FontChoice::Basic));
205 ctx.set_visuals(Theme::Catppuccin.palette(Luminance::Dark).egui_visuals());
206 egui::Frame::popup(ui.style()).show(ui, |ui| {
209 ui.horizontal(|ui| {
210 let _ = arrows(
211 ui,
212 PathHistory {
213 can_back: true,
214 can_forward: false,
215 },
216 );
217 let _ = breadcrumb(ui, &doc, &path);
218 });
219 });
220 });
221 harness.run();
222 harness.snapshot("address_bar");
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229 use crate::document::Block;
230 use egui::{Rect, pos2};
231
232 fn nested(depth: usize) -> (Document, Vec<RectId>) {
235 let mut doc = Document::default();
236 let mut parent = doc.top_id;
237 let mut ids = Vec::new();
238 for i in 0..depth {
239 let id = doc.add_child(
240 parent,
241 Block::new(
242 format!("level{i}"),
243 Rect::from_min_max(pos2(0.0, 0.0), pos2(60.0, 60.0)),
244 ),
245 );
246 ids.push(id);
247 parent = id;
248 }
249 (doc, ids)
250 }
251
252 fn path_of(ids: &[RectId]) -> BlockPath {
253 let mut path = BlockPath::empty();
254 for &id in ids {
255 path.push(id);
256 }
257 path
258 }
259
260 #[test]
261 fn a_short_path_shows_the_root_and_every_segment() {
262 let (doc, ids) = nested(2);
263 let (elided, crumbs) = crumbs(&doc, &path_of(&ids));
264 assert!(!elided);
265 let labels: Vec<&str> = crumbs.iter().map(|c| c.label.as_str()).collect();
266 assert_eq!(labels, [ROOT_CRUMB, "level0", "level1"]);
267 assert_eq!(crumbs[0].target, None, "the root crumb clears the path");
268 assert_eq!(crumbs[2].target, Some(ids[1]));
269 }
270
271 #[test]
274 fn a_deep_path_elides_its_head() {
275 let (doc, ids) = nested(8);
276 let (elided, crumbs) = crumbs(&doc, &path_of(&ids));
277 assert!(elided);
278 assert_eq!(crumbs.len(), MAX_CRUMBS);
279 let labels: Vec<&str> = crumbs.iter().map(|c| c.label.as_str()).collect();
280 assert_eq!(labels, ["level3", "level4", "level5", "level6", "level7"]);
281 assert!(crumbs.iter().all(|c| c.target.is_some()));
283 }
284
285 #[test]
286 fn the_root_alone_is_one_crumb() {
287 let (doc, _) = nested(0);
288 let (elided, crumbs) = crumbs(&doc, &BlockPath::empty());
289 assert!(!elided);
290 assert_eq!(crumbs.len(), 1);
291 assert_eq!(crumbs[0].label, ROOT_CRUMB);
292 }
293}