1use std::fmt;
5
6use blockworx_doc::{
7 block_model::Block,
8 document::{Document, IndexedDocument, chronological},
9 id::BlockId,
10};
11
12#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
16pub enum Scope {
17 Root,
18 Block(BlockId),
19}
20
21impl Scope {
22 pub fn from_wire(id: BlockId) -> Scope {
26 if id == BlockId::NULL {
27 Scope::Root
28 } else {
29 Scope::Block(id)
30 }
31 }
32
33 pub fn wire_id(self) -> BlockId {
35 match self {
36 Scope::Root => BlockId::NULL,
37 Scope::Block(id) => id,
38 }
39 }
40
41 pub fn block(self) -> Option<BlockId> {
43 match self {
44 Scope::Root => None,
45 Scope::Block(id) => Some(id),
46 }
47 }
48}
49
50pub enum Resolved<'a> {
54 Root,
55 Block(&'a Block),
56 Absent,
57}
58
59pub fn resolve<'a>(indexed: &IndexedDocument<'a>, scope: Scope) -> Resolved<'a> {
62 match scope {
63 Scope::Root => Resolved::Root,
64 Scope::Block(id) => indexed
65 .doc
66 .block(&id)
67 .map_or(Resolved::Absent, Resolved::Block),
68 }
69}
70
71#[derive(Clone, Default, Debug, PartialEq, Eq)]
75pub struct BlockPath(Vec<BlockId>);
76
77impl BlockPath {
78 pub fn empty() -> Self {
79 Self(Vec::new())
80 }
81
82 pub fn opening(doc: &Document) -> Self {
85 match Scope::from_wire(doc.title_block().top) {
86 Scope::Root => Self::empty(),
87 Scope::Block(top) => Self(vec![top]),
88 }
89 }
90
91 pub fn to_parent_of(doc: &Document, target: BlockId) -> Option<Self> {
94 let mut trail = Vec::new();
95 let mut current = Scope::from_wire(doc.block(&target)?.parent);
96 while let Scope::Block(id) = current {
97 trail.push(id);
98 current = Scope::from_wire(doc.block(&id)?.parent);
99 }
100 trail.reverse();
101 Some(Self(trail))
102 }
103
104 pub fn showing(doc: &Document, scope: Scope) -> Option<Self> {
108 match scope {
109 Scope::Root => Some(Self::empty()),
110 Scope::Block(id) => {
111 let mut path = Self::to_parent_of(doc, id)?;
112 path.push(id);
113 Some(path)
114 }
115 }
116 }
117
118 pub fn scope(&self) -> Scope {
122 self.0.last().copied().map_or(Scope::Root, Scope::Block)
123 }
124
125 pub fn push(&mut self, id: BlockId) {
126 self.0.push(id);
127 }
128
129 pub fn pop(&mut self) -> Option<BlockId> {
130 self.0.pop()
131 }
132
133 pub fn segments(&self) -> &[BlockId] {
134 &self.0
135 }
136
137 pub fn is_held_by(&self, doc: &Document) -> bool {
142 self.0.iter().all(|id| doc.block(id).is_some())
143 }
144}
145
146pub fn child_blocks(indexed: &IndexedDocument<'_>, scope: Scope) -> Vec<BlockId> {
151 let Some(entry) = indexed.index.scope(scope.wire_id()) else {
152 return Vec::new();
153 };
154 chronological(
155 entry
156 .children
157 .iter()
158 .filter_map(|id| Some((*id, indexed.doc.block(id)?))),
159 )
160}
161
162#[derive(Clone, Copy, PartialEq, Eq, Debug)]
166pub enum Structure {
167 Leaf,
168 Nested,
169}
170
171impl Structure {
172 pub fn opens_a_scope(self) -> bool {
173 self == Structure::Nested
174 }
175}
176
177pub fn structure(indexed: &IndexedDocument<'_>, id: BlockId) -> Structure {
181 let holds_a_block = indexed.index.scope(id).is_some_and(|entry| {
182 entry
183 .children
184 .iter()
185 .any(|child| indexed.doc.block(child).is_some())
186 });
187 if holds_a_block {
188 Structure::Nested
189 } else {
190 Structure::Leaf
191 }
192}
193
194impl fmt::Display for BlockPath {
195 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196 let mut first = true;
197 for seg in &self.0 {
198 if !first {
199 f.write_str(":")?;
200 }
201 write!(f, "{seg}")?;
202 first = false;
203 }
204 Ok(())
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use blockworx_doc::fixtures::block_id;
212
213 #[test]
214 fn push_pop_segments() {
215 let mut p = BlockPath::empty();
216 assert!(p.segments().is_empty());
217 assert_eq!(p.scope(), Scope::Root, "the empty path is the root scope");
218 p.push(block_id(1));
219 p.push(block_id(3));
220 assert_eq!(p.segments(), &[block_id(1), block_id(3)]);
221 assert_eq!(
222 p.scope(),
223 Scope::Block(block_id(3)),
224 "the scope is the last segment"
225 );
226 assert_eq!(p.pop(), Some(block_id(3)));
227 assert_eq!(p.segments(), &[block_id(1)]);
228 }
229
230 #[test]
233 fn wire_conversions_round_trip() {
234 assert_eq!(Scope::from_wire(BlockId::NULL), Scope::Root);
235 assert_eq!(Scope::Root.wire_id(), BlockId::NULL);
236 assert_eq!(Scope::from_wire(block_id(2)), Scope::Block(block_id(2)));
237 assert_eq!(Scope::Block(block_id(2)).wire_id(), block_id(2));
238 assert_eq!(Scope::Root.block(), None);
239 }
240
241 #[test]
247 fn the_canvas_the_navigator_and_the_pdf_agree_on_what_opens_a_scope() {
248 use crate::shape::ShapeRef;
249 use crate::widget::test_fixtures::{self as fx, Scene};
250 use std::collections::BTreeSet;
251
252 let mut scene = Scene::new(fx::nested_scopes());
253 let live: Vec<BlockId> = {
254 let indexed = scene.indexed();
255 indexed.doc.blocks().map(|(id, _)| id).collect()
256 };
257 let nested: BTreeSet<BlockId> = live
258 .iter()
259 .copied()
260 .filter(|&id| structure(&scene.indexed(), id).opens_a_scope())
261 .collect();
262 assert!(
263 !nested.is_empty() && nested.len() < live.len(),
264 "precondition: the fixture must hold both kinds of block, got {nested:?} of {live:?}",
265 );
266
267 let doc = scene.doc.clone();
270 let mut double_bordered: BTreeSet<BlockId> = BTreeSet::new();
271 for scope in std::iter::once(BlockPath::empty()).chain(live.iter().map(|&id| {
272 let mut path = BlockPath::to_parent_of(&doc, id).expect("a live block has a path");
273 path.push(id);
274 path
275 })) {
276 scene.path = scope;
277 let drawing = scene.drawing();
278 for (id, shape) in drawing.blocks_layer() {
279 let ShapeRef::Block(block) = shape else {
280 continue;
281 };
282 if block.structure.opens_a_scope() {
283 double_bordered.insert(id.block().expect("a block layer holds blocks"));
284 }
285 }
286 }
287
288 let indexed = scene.indexed();
289 let pages: BTreeSet<BlockId> = crate::export::pdf::page_scopes(&indexed)
290 .into_iter()
291 .filter_map(|scope| scope.block())
292 .collect();
293 let branches: BTreeSet<BlockId> = crate::tools::nav_tree::branch_rows(&indexed)
294 .into_iter()
295 .collect();
296
297 assert_eq!(double_bordered, nested, "the canvas draws a different set");
298 assert_eq!(pages, nested, "the PDF pages a different set");
299 let root = crate::tools::nav_tree::tree_root(&indexed)
303 .block()
304 .expect("the fixture designates a top");
305 assert_eq!(
306 branches,
307 nested
308 .iter()
309 .copied()
310 .filter(|&id| id != root)
311 .collect::<BTreeSet<_>>(),
312 "the navigator branches on a different set",
313 );
314 }
315
316 #[test]
317 fn display() {
318 let mut p = BlockPath::empty();
319 assert_eq!(p.to_string(), "");
320 p.push(block_id(1));
321 p.push(block_id(7));
322 assert_eq!(p.to_string(), format!("{}:{}", block_id(1), block_id(7)));
323 }
324}