Skip to main content

blockworx/
path.rs

1//! Where the editor is looking: the chain of blocks descended into, the
2//! scope that chain resolves to, and what that scope holds.
3
4use std::fmt;
5
6use blockworx_doc::{
7    block_model::Block,
8    document::{Document, IndexedDocument, chronological},
9    id::BlockId,
10};
11
12/// A scope holds shapes: either the document root, or a block. The root has
13/// no entity — it is never locked, never deleted, never renamed — so it is a
14/// variant, not a sentinel id (`docs/type-level-invariants.md`, P4).
15#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
16pub enum Scope {
17    Root,
18    Block(BlockId),
19}
20
21impl Scope {
22    /// The document boundary, inbound: registers and ops spell the root as
23    /// `BlockId::NULL`, and this pair of conversions is the only place the
24    /// editor reads or writes that sentinel.
25    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    /// The document boundary, outbound — see [`Self::from_wire`].
34    pub fn wire_id(self) -> BlockId {
35        match self {
36            Scope::Root => BlockId::NULL,
37            Scope::Block(id) => id,
38        }
39    }
40
41    /// The block behind this scope, where there is one.
42    pub fn block(self) -> Option<BlockId> {
43        match self {
44            Scope::Root => None,
45            Scope::Block(id) => Some(id),
46        }
47    }
48}
49
50/// What a scope reference resolves to. `Absent` — a block id the document no
51/// longer holds live — is a third answer, and the compiler makes every
52/// caller say which of the three it means.
53pub enum Resolved<'a> {
54    Root,
55    Block(&'a Block),
56    Absent,
57}
58
59/// Resolve `scope` against the document. The one definition of "is this the
60/// root, a live block, or nothing" — formerly rediscovered per site.
61pub 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/// The blocks descended into, outermost first. The scope being edited is
72/// the last segment; an empty path is the document root, which is a scope
73/// like any other (the flag-day playbook, F4/F9).
74#[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    /// The path that opens `doc`: inside its designated top block, or the
83    /// document root while there is no top yet.
84    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    /// The path whose scope holds `target`, or `None` for a block the
92    /// document does not hold. A top-level block's path is the empty one.
93    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    /// The path whose scope *is* `scope`: the trail down to it with the
105    /// scope's own block last. `None` for a block the document does not
106    /// hold; the root is always reachable.
107    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    /// The scope being edited. Segments are real blocks only — descending is
119    /// [`Self::push`]ing a block — so the empty path *is* the root, not a
120    /// path to a sentinel.
121    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    /// Whether every segment is a block `doc` still holds. The root path
138    /// always is — it is a scope, not a block. Asked when the document
139    /// under the editor changes to one that may never have held the blocks
140    /// this path descends through (the time machine).
141    pub fn is_held_by(&self, doc: &Document) -> bool {
142        self.0.iter().all(|id| doc.block(id).is_some())
143    }
144}
145
146/// The blocks `scope` holds, in the document's one draw order. The single
147/// definition of "a scope's children, ordered": the drawing surface, the
148/// navigator tree, and the command palette all read it here, so a scope
149/// cannot present its contents in two orders.
150pub 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/// Whether a block holds blocks of its own — the fact the navigator hangs a
163/// disclosure triangle on, the PDF cuts a page for, and the canvas draws a
164/// second, inset border for.
165#[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
177/// The single definition of "does this block open a scope", so those three
178/// cannot answer it differently. One index probe and no allocation — every
179/// drawn block and every visible navigator row asks it each frame.
180pub 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    /// The boundary conversions are inverses, and the sentinel never leaks:
231    /// a wire NULL is the root on the way in and comes back out as NULL.
232    #[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    /// One predicate, three consumers: the blocks the canvas draws a second,
242    /// inset border on, the blocks the navigator hangs a disclosure triangle
243    /// on, and the blocks the PDF cuts a page for are one set. They agree by
244    /// construction — all three call [`structure`] — and this fails the day one
245    /// of them grows a spelling of its own.
246    #[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        // The canvas: every scope in turn, since a level only ever draws its
268        // own children.
269        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        // The navigator hangs its tree *from* the top block, which is the PDF's
300        // first page and the canvas's outermost double border but never a row
301        // of its own — the one, principled difference.
302        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}