Skip to main content

blockworx_editor/
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`).
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".
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.
74#[derive(Clone, Default, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
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 scope the document hangs from — its designated top block, or the
147/// document root while there is none (the root is a scope like any other,
148/// and a fresh boot has no top block). The navigator's tree, the
149/// PDF's first page and the canvas's outermost frame all start here.
150pub fn tree_root(document: &IndexedDocument<'_>) -> Scope {
151    Scope::from_wire(document.doc.title_block().top)
152}
153
154/// The blocks `scope` holds, in the document's one draw order. The single
155/// definition of "a scope's children, ordered": the drawing surface, the
156/// navigator tree, and the command palette all read it here, so a scope
157/// cannot present its contents in two orders.
158pub fn child_blocks(indexed: &IndexedDocument<'_>, scope: Scope) -> Vec<BlockId> {
159    let Some(entry) = indexed.index.scope(scope.wire_id()) else {
160        return Vec::new();
161    };
162    chronological(
163        entry
164            .children
165            .iter()
166            .filter_map(|id| Some((*id, indexed.doc.block(id)?))),
167    )
168}
169
170/// Whether a block holds blocks of its own — the fact the navigator hangs a
171/// disclosure triangle on, the PDF cuts a page for, and the canvas draws a
172/// second, inset border for.
173#[derive(Clone, Copy, PartialEq, Eq, Debug)]
174pub enum Structure {
175    Leaf,
176    Nested,
177}
178
179impl Structure {
180    pub fn opens_a_scope(self) -> bool {
181        self == Structure::Nested
182    }
183}
184
185/// The single definition of "does this block open a scope", so those three
186/// cannot answer it differently. One index probe and no allocation — every
187/// drawn block and every visible navigator row asks it each frame.
188pub fn structure(indexed: &IndexedDocument<'_>, id: BlockId) -> Structure {
189    let holds_a_block = indexed.index.scope(id).is_some_and(|entry| {
190        entry
191            .children
192            .iter()
193            .any(|child| indexed.doc.block(child).is_some())
194    });
195    if holds_a_block {
196        Structure::Nested
197    } else {
198        Structure::Leaf
199    }
200}
201
202impl fmt::Display for BlockPath {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        let mut first = true;
205        for seg in &self.0 {
206            if !first {
207                f.write_str(":")?;
208            }
209            write!(f, "{seg}")?;
210            first = false;
211        }
212        Ok(())
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use blockworx_doc::fixtures::block_id;
220
221    #[test]
222    fn push_pop_segments() {
223        let mut p = BlockPath::empty();
224        assert!(p.segments().is_empty());
225        assert_eq!(p.scope(), Scope::Root, "the empty path is the root scope");
226        p.push(block_id(1));
227        p.push(block_id(3));
228        assert_eq!(p.segments(), &[block_id(1), block_id(3)]);
229        assert_eq!(
230            p.scope(),
231            Scope::Block(block_id(3)),
232            "the scope is the last segment"
233        );
234        assert_eq!(p.pop(), Some(block_id(3)));
235        assert_eq!(p.segments(), &[block_id(1)]);
236    }
237
238    /// The boundary conversions are inverses, and the sentinel never leaks:
239    /// a wire NULL is the root on the way in and comes back out as NULL.
240    #[test]
241    fn wire_conversions_round_trip() {
242        assert_eq!(Scope::from_wire(BlockId::NULL), Scope::Root);
243        assert_eq!(Scope::Root.wire_id(), BlockId::NULL);
244        assert_eq!(Scope::from_wire(block_id(2)), Scope::Block(block_id(2)));
245        assert_eq!(Scope::Block(block_id(2)).wire_id(), block_id(2));
246        assert_eq!(Scope::Root.block(), None);
247    }
248
249    #[test]
250    fn display() {
251        let mut p = BlockPath::empty();
252        assert_eq!(p.to_string(), "");
253        p.push(block_id(1));
254        p.push(block_id(7));
255        assert_eq!(p.to_string(), format!("{}:{}", block_id(1), block_id(7)));
256    }
257}