1use std::collections::{HashMap, HashSet};
23
24use blockworx_doc::id::BlockId;
25use blockworx_editor::path::Scope;
26
27use crate::chrome::NavTree;
28
29#[derive(Clone, Copy, PartialEq, Eq, Debug)]
31pub enum Filtered {
32 Yes,
33 No,
34}
35
36impl Filtered {
37 #[must_use]
40 pub fn of(filter: &str) -> (Self, Option<&str>) {
41 match Some(filter.trim()).filter(|text| !text.is_empty()) {
42 Some(text) => (Filtered::Yes, Some(text)),
43 None => (Filtered::No, None),
44 }
45 }
46}
47
48#[derive(Clone, PartialEq, Eq, Debug)]
50pub struct Row {
51 pub id: BlockId,
52 pub depth: usize,
53 pub has_children: bool,
54 pub open: bool,
56 pub label: String,
57 pub ancestors: Option<Vec<String>>,
61 pub leaves: Option<usize>,
65 pub accent: Option<u8>,
68 pub last_child: bool,
71}
72
73pub struct Index {
80 parent: HashMap<BlockId, Scope>,
81 matched: HashSet<BlockId>,
82}
83
84impl Index {
85 #[must_use]
90 pub fn of(tree: &NavTree, filter: Option<&str>) -> Self {
91 let mut parent: HashMap<BlockId, Scope> = HashMap::new();
92 let mut matched: HashSet<BlockId> = HashSet::new();
93 for (node, above) in tree.walk() {
94 parent.insert(
95 node.id,
96 above.last().map_or(Scope::Root, |&id| Scope::Block(id)),
97 );
98 if filter.is_some_and(|text| matches_filter(&node.label, text)) {
99 matched.insert(node.id);
100 }
101 }
102 Self { parent, matched }
103 }
104
105 #[must_use]
108 pub fn parent_of(&self, id: BlockId) -> Option<Scope> {
109 self.parent.get(&id).copied()
110 }
111
112 #[must_use]
114 pub fn ancestor_ids(&self, top: Scope, target: BlockId) -> Vec<BlockId> {
115 let mut up = Vec::new();
116 let mut cur = target;
117 while let Some(&at) = self.parent.get(&cur) {
118 if at == top {
119 break;
120 }
121 let Scope::Block(next) = at else { break };
122 up.push(next);
123 cur = next;
124 }
125 up.reverse();
126 up
127 }
128
129 #[must_use]
132 pub fn under(&self, root: BlockId, target: BlockId) -> bool {
133 let mut cur = target;
134 while let Some(&at) = self.parent.get(&cur) {
135 let Scope::Block(next) = at else { return false };
136 if next == root {
137 return true;
138 }
139 cur = next;
140 }
141 false
142 }
143
144 pub fn expand_ancestors(&self, top: Scope, target: BlockId, expanded: &mut HashSet<BlockId>) {
148 for id in self.ancestor_ids(top, target) {
149 expanded.insert(id);
150 }
151 }
152
153 #[must_use]
157 pub fn ancestry(&self, tree: &NavTree, id: BlockId) -> Vec<String> {
158 self.ancestor_ids(Scope::Root, id)
159 .into_iter()
160 .map(|at| label_of(tree, at))
161 .collect()
162 }
163
164 #[must_use]
170 pub fn rows(
171 &self,
172 tree: &NavTree,
173 expanded: &HashSet<BlockId>,
174 root: Scope,
175 filtered: Filtered,
176 ) -> Vec<Row> {
177 let mut out = Vec::new();
178 self.walk(
179 Walk {
180 tree,
181 expanded,
182 node: root,
183 depth: 0,
184 filtered,
185 },
186 &mut out,
187 );
188 mark_last_children(&mut out);
189 out
190 }
191
192 fn walk(&self, at: Walk<'_>, out: &mut Vec<Row>) {
193 let Walk {
194 tree,
195 expanded,
196 node,
197 depth,
198 filtered,
199 } = at;
200 for child in tree.children_of(node) {
201 let cid = child.id;
202 let has_children = child.opens_a_scope();
203 match filtered {
204 Filtered::Yes => {
205 if self.matched.contains(&cid) {
206 out.push(Row {
207 id: cid,
208 depth: 0,
209 has_children,
210 open: false,
211 label: child.label.clone(),
212 ancestors: Some(self.ancestry(tree, cid)),
213 leaves: None,
214 accent: child.accent,
215 last_child: true,
216 });
217 }
218 self.walk(
219 Walk {
220 node: Scope::Block(cid),
221 ..at
222 },
223 out,
224 );
225 }
226 Filtered::No => {
227 let open = has_children && expanded.contains(&cid);
228 out.push(Row {
229 id: cid,
230 depth,
231 has_children,
232 open,
233 label: child.label.clone(),
234 ancestors: None,
235 leaves: (has_children && !open).then(|| child.leaves()),
236 accent: child.accent,
237 last_child: false,
238 });
239 if open {
240 self.walk(
241 Walk {
242 node: Scope::Block(cid),
243 depth: depth + 1,
244 ..at
245 },
246 out,
247 );
248 }
249 }
250 }
251 }
252 }
253}
254
255#[derive(Clone, Copy)]
258struct Walk<'a> {
259 tree: &'a NavTree,
260 expanded: &'a HashSet<BlockId>,
261 node: Scope,
262 depth: usize,
263 filtered: Filtered,
264}
265
266fn mark_last_children(rows: &mut [Row]) {
271 for i in 0..rows.len() {
272 let depth = rows[i].depth;
273 rows[i].last_child = rows[i + 1..]
274 .iter()
275 .take_while(|later| later.depth >= depth)
276 .all(|later| later.depth > depth);
277 }
278}
279
280#[must_use]
286pub fn rooted_at(tree: &NavTree, focus: Option<BlockId>) -> Scope {
287 focus
288 .filter(|id| tree.node(*id).is_some())
289 .map_or(Scope::Root, Scope::Block)
290}
291
292#[must_use]
295pub fn matches_filter(label: &str, filter: &str) -> bool {
296 let wanted = filter.trim().to_lowercase();
297 wanted.is_empty() || label.to_lowercase().contains(&wanted)
298}
299
300#[must_use]
303pub fn label_of(tree: &NavTree, id: BlockId) -> String {
304 tree.node(id)
305 .map_or_else(|| id.to_string(), |node| node.label.clone())
306}
307
308#[must_use]
313pub fn shortened(segments: &[String], room: f32, measure: impl Fn(&str) -> f32) -> String {
314 if segments.is_empty() {
315 return String::new();
316 }
317 for start in 0..segments.len() {
318 let text = if start == 0 {
319 segments.join("/")
320 } else {
321 format!("\u{2026}/{}", segments[start..].join("/"))
322 };
323 if measure(&text) <= room || start + 1 == segments.len() {
324 return text;
325 }
326 }
327 String::new()
328}
329
330#[cfg(any(test, feature = "test-support"))]
335#[must_use]
336pub fn branch_rows(tree: &NavTree) -> Vec<BlockId> {
337 let expanded: HashSet<BlockId> = tree.walk().map(|(node, _)| node.id).collect();
338 Index::of(tree, None)
339 .rows(tree, &expanded, Scope::Root, Filtered::No)
340 .into_iter()
341 .filter(|row| row.has_children)
342 .map(|row| row.id)
343 .collect()
344}
345
346#[cfg(test)]
347mod tests {
348 use blockworx_doc::fixtures::block_id;
349 use blockworx_editor::path::{BlockPath, tree_root};
350 use blockworx_editor::widget::test_fixtures::{self as fx, Scene};
351 use blockworx_geom::{Rect, pos2};
352
353 use super::*;
354
355 fn tree_of(scene: &mut Scene) -> NavTree {
357 NavTree::of(&scene.indexed(), BlockPath::empty(), Vec::new())
358 }
359
360 fn square(n: u32, name: &str) -> Vec<blockworx_doc::opcode::OpCodes> {
361 vec![
362 fx::block_in(
363 n,
364 Scope::Root,
365 Rect::from_min_max(pos2(0.0, 0.0), pos2(60.0, 60.0)),
366 ),
367 fx::titled(n, name),
368 ]
369 }
370
371 fn nested(n: u32, parent: u32, name: &str) -> Vec<blockworx_doc::opcode::OpCodes> {
372 vec![
373 fx::block_in(
374 n,
375 Scope::Block(block_id(parent)),
376 Rect::from_min_max(pos2(0.0, 0.0), pos2(60.0, 60.0)),
377 ),
378 fx::titled(n, name),
379 ]
380 }
381
382 fn deep() -> Vec<blockworx_doc::opcode::OpCodes> {
385 square(1, "Platform")
386 .into_iter()
387 .chain(nested(2, 1, "Services"))
388 .chain(nested(3, 2, "Auth"))
389 .chain(square(4, "Marketing"))
390 .collect()
391 }
392
393 fn rows_of(
396 tree: &NavTree,
397 root: Scope,
398 filter: Option<&str>,
399 expanded: &HashSet<BlockId>,
400 ) -> Vec<Row> {
401 let (filtered, _) = Filtered::of(filter.unwrap_or_default());
402 Index::of(tree, filter).rows(tree, expanded, root, filtered)
403 }
404
405 fn labels(rows: &[Row]) -> Vec<(&str, usize, bool)> {
406 rows.iter()
407 .map(|row| (row.label.as_str(), row.depth, row.last_child))
408 .collect()
409 }
410
411 #[test]
412 fn matches_filter_is_case_insensitive_substring_and_empty_matches_all() {
413 assert!(matches_filter("Adder", ""));
414 assert!(matches_filter("Adder", " "));
415 assert!(matches_filter("Adder", "add"));
416 assert!(matches_filter("Adder", "DER"));
417 assert!(!matches_filter("Adder", "mux"));
418 }
419
420 #[test]
422 fn the_filter_reads_the_type_as_well_as_the_name() {
423 let mut scene = Scene::new(
424 square(1, "Adder")
425 .into_iter()
426 .chain([fx::typed(1, "ALU")])
427 .collect(),
428 );
429 let label = crate::chrome::block_label(&scene.indexed(), block_id(1));
430 assert_eq!(label, "Adder/ALU");
431 assert!(matches_filter(&label, "alu"));
432 }
433
434 #[test]
438 fn a_search_flattens_to_the_matches_with_their_path_beneath() {
439 let target = block_id(2);
440 let mut scene = Scene::new(
441 square(1, "Platform")
442 .into_iter()
443 .chain(nested(2, 1, "Authentication"))
444 .chain(square(3, "Marketing"))
445 .collect(),
446 );
447 let tree = tree_of(&mut scene);
448
449 let rows = rows_of(&tree, Scope::Root, Some("auth"), &HashSet::new());
450 assert_eq!(
451 rows.iter().map(|row| row.id).collect::<Vec<_>>(),
452 vec![target],
453 "only the matching label matches, and the ancestor is not a row",
454 );
455 assert_eq!(rows[0].depth, 0, "a flattened match is not indented");
456 assert_eq!(
457 rows[0].ancestors.as_deref(),
458 Some(["Platform".to_owned()].as_slice()),
459 "the match does not carry the path it lives at",
460 );
461 assert_eq!(
462 Index::of(&tree, None).ancestry(&tree, target),
463 vec!["Platform"],
464 );
465 }
466
467 #[test]
471 fn an_ancestor_path_is_shortened_from_the_left() {
472 let segments: Vec<String> = ["Platform", "Services", "Auth"]
473 .iter()
474 .map(|at| (*at).to_owned())
475 .collect();
476 let measure = |text: &str| text.chars().count() as f32;
479
480 assert_eq!(
481 shortened(&segments, 100.0, measure),
482 "Platform/Services/Auth"
483 );
484 assert_eq!(
485 shortened(&segments, 20.0, measure),
486 "\u{2026}/Services/Auth"
487 );
488 assert_eq!(
489 shortened(&segments, 6.0, measure),
490 "\u{2026}/Auth",
491 "the immediate parent is the last thing to go",
492 );
493 assert_eq!(shortened(&[], 100.0, measure), "");
494 }
495
496 #[test]
499 fn a_closed_branch_counts_its_leaves() {
500 let mut scene = Scene::new(
501 square(1, "Platform")
502 .into_iter()
503 .chain(nested(2, 1, "Services"))
504 .chain(nested(3, 2, "Auth"))
505 .chain(nested(4, 2, "Billing"))
506 .collect(),
507 );
508 let tree = tree_of(&mut scene);
509 let closed = rows_of(&tree, Scope::Root, None, &HashSet::new());
510 assert_eq!(closed.len(), 1, "precondition: one closed branch");
511 assert_eq!(
512 closed[0].leaves,
513 Some(2),
514 "Platform holds two leaves, however deep they sit",
515 );
516
517 let expanded: HashSet<BlockId> = std::iter::once(block_id(1)).collect();
518 let open = rows_of(&tree, Scope::Root, None, &expanded);
519 assert_eq!(open[0].leaves, None, "an open branch shows its contents");
520 assert_eq!(open[1].leaves, Some(2), "and its closed child counts");
521 }
522
523 #[test]
526 fn a_focus_re_roots_the_tree_and_resets_the_indentation() {
527 let mut scene = Scene::new(deep());
528 let tree = tree_of(&mut scene);
529 assert_eq!(
530 rooted_at(&tree, None),
531 Scope::Root,
532 "no focus hangs the tree from the document",
533 );
534 let root = rooted_at(&tree, Some(block_id(2)));
535 assert_eq!(root, Scope::Block(block_id(2)));
536
537 let rows = rows_of(&tree, root, None, &HashSet::new());
538 assert_eq!(
539 rows.iter().map(|row| row.id).collect::<Vec<_>>(),
540 vec![block_id(3)]
541 );
542 assert_eq!(rows[0].depth, 0, "the focused root's children start over");
543
544 assert_eq!(rooted_at(&tree, Some(block_id(9))), Scope::Root);
546 }
547
548 #[test]
552 fn a_parents_guide_stops_at_its_last_child() {
553 let mut scene = Scene::new(deep());
554 let tree = tree_of(&mut scene);
555 let expanded: HashSet<BlockId> = [block_id(1), block_id(2)].into_iter().collect();
556 let rows = rows_of(&tree, Scope::Root, None, &expanded);
557 let named = |name: &str| {
558 rows.iter()
559 .find(|row| row.label.starts_with(name))
560 .unwrap_or_else(|| panic!("no {name} row: {:?}", labels(&rows)))
561 };
562 assert_eq!(
563 rows.len(),
564 4,
565 "precondition: Platform > Services > Auth, and Marketing beside them: {:?}",
566 labels(&rows),
567 );
568 assert!(
569 !named("Platform").last_child,
570 "Marketing follows Platform at the root, so Platform is not the last child",
571 );
572 assert!(
573 named("Marketing").last_child,
574 "nothing follows Marketing at the root",
575 );
576 assert!(
577 named("Services").last_child && named("Auth").last_child,
578 "an only child is its parent's last: {:?}",
579 labels(&rows),
580 );
581 assert!(
582 named("Auth").depth > named("Services").depth,
583 "precondition: the deep row really is deeper",
584 );
585 }
586
587 #[test]
590 fn a_block_is_under_its_ancestors_and_nothing_else() {
591 let mut scene = Scene::new(deep());
592 let tree = tree_of(&mut scene);
593 let index = Index::of(&tree, None);
594 assert!(
595 index.under(block_id(1), block_id(3)),
596 "Auth is inside Platform"
597 );
598 assert!(index.under(block_id(2), block_id(3)));
599 assert!(
600 !index.under(block_id(4), block_id(3)),
601 "Auth is not inside Marketing",
602 );
603 assert!(
604 !index.under(block_id(3), block_id(3)),
605 "nothing is inside itself",
606 );
607 }
608
609 #[test]
612 fn expand_ancestors_opens_the_chain_to_a_deep_block() {
613 let (a, b, c) = (block_id(1), block_id(2), block_id(3));
614 let mut scene = Scene::new(
615 square(1, "A")
616 .into_iter()
617 .chain(nested(2, 1, "B"))
618 .chain(nested(3, 2, "C"))
619 .collect(),
620 );
621 let tree = tree_of(&mut scene);
622
623 let mut expanded = HashSet::new();
624 Index::of(&tree, None).expand_ancestors(Scope::Root, c, &mut expanded);
625 assert!(expanded.contains(&a));
626 assert!(expanded.contains(&b));
627 assert!(!expanded.contains(&c), "the target is not force-expanded");
628
629 let rows = rows_of(&tree, Scope::Root, None, &expanded);
630 assert!(
631 rows.iter().any(|row| row.id == c),
632 "the deep block is now a row",
633 );
634 }
635
636 #[test]
639 fn flatten_is_collapsed_until_expanded() {
640 let (parent, child) = (block_id(1), block_id(2));
641 let mut scene = Scene::new(
642 square(1, "Platform")
643 .into_iter()
644 .chain(nested(2, 1, "Services"))
645 .collect(),
646 );
647 let tree = tree_of(&mut scene);
648
649 let collapsed = rows_of(&tree, Scope::Root, None, &HashSet::new());
650 assert_eq!(
651 collapsed.iter().map(|row| row.id).collect::<Vec<_>>(),
652 vec![parent]
653 );
654 assert!(collapsed[0].has_children);
655 assert!(!collapsed[0].open);
656
657 let expanded: HashSet<BlockId> = std::iter::once(parent).collect();
658 let opened = rows_of(&tree, Scope::Root, None, &expanded);
659 assert_eq!(
660 opened.iter().map(|row| row.id).collect::<Vec<_>>(),
661 vec![parent, child]
662 );
663 }
664
665 #[test]
670 fn the_tree_hangs_from_the_designated_top_or_the_document_root() {
671 let (outer, inner) = (block_id(1), block_id(2));
672 let ops: Vec<_> = square(1, "Platform")
673 .into_iter()
674 .chain(nested(2, 1, "Services"))
675 .collect();
676
677 let mut rooted = Scene::new(ops.clone());
678 let indexed = rooted.indexed();
679 assert_eq!(tree_root(&indexed), Scope::Root, "no top designated yet");
680 let tree = NavTree::of(&indexed, BlockPath::empty(), Vec::new());
681 let rows = rows_of(&tree, Scope::Root, None, &HashSet::new());
682 assert_eq!(
683 rows.iter().map(|row| row.id).collect::<Vec<_>>(),
684 vec![outer]
685 );
686
687 let mut topped = Scene::new(ops.into_iter().chain([fx::top(1)]).collect());
688 let indexed = topped.indexed();
689 assert_eq!(tree_root(&indexed), Scope::Block(outer));
690 let tree = NavTree::of(&indexed, BlockPath::empty(), Vec::new());
691 let rows = rows_of(&tree, Scope::Root, None, &HashSet::new());
692 assert_eq!(
693 rows.iter().map(|row| row.id).collect::<Vec<_>>(),
694 vec![inner],
695 "the top block is the invisible root, not a row",
696 );
697 }
698}