Skip to main content

blockworx/edit/
lock.rs

1//! What a block's lock protects, as a capability rather than a check.
2//!
3//! A locked block freezes its **interface**: which pins exist, what signal
4//! each carries, what each is called. It does not freeze **presentation**:
5//! where a pin sits on the edge, which way the block's pins face, whether a
6//! tag is shown. The split is by what the edit *means*, not by what it
7//! touches — moving a pin from slot 2 to slot 5 changes a drawing, changing
8//! it from Input to Output changes a definition.
9//!
10//! | Material — a lock refuses | Presentation — a lock allows |
11//! |---|---|
12//! | add a pin, add a port | move a pin to another slot or edge |
13//! | delete a pin or port | nudge or relocate a pin group |
14//! | set the I/O direction | flip pins left/right or top/bottom |
15//! | rename a pin | show or hide a tag |
16//! | edit a pin's type line | recolor, resize, retitle the block |
17//! | edit a pin's tag text | move or delete the whole block |
18//!
19//! Stated as types because a rule spread across call sites is a rule that
20//! drifts, and this one had: the guard reached the presentation edits it
21//! should have left alone, and missed `cycle_dir`, the one edit that most
22//! obviously changes a pin's meaning. Both errors were invisible because
23//! each site decided for itself.
24//!
25//! Now a material emitter takes a proof and there is no other way to name
26//! its target, so the rule is checked where it is minted and nowhere else.
27//! A presentation emitter takes a plain id and is offered no proof, which is
28//! how the table above reads in the signatures.
29
30use blockworx_doc::{
31    document::{Document, IndexedDocument},
32    id::PinId,
33};
34
35use crate::path::{Resolved, Scope, resolve};
36
37/// A pin whose owner accepts material edits to its interface.
38///
39/// Minted only by [`MaterialPin::of`], so every emitter that changes what a
40/// pin *is* asks the same question of the same rule.
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub struct MaterialPin(PinId);
43
44impl MaterialPin {
45    /// `None` for a pin the document does not hold, or one whose owning
46    /// block is locked.
47    pub fn of(doc: &Document, pin: PinId) -> Option<Self> {
48        let live = doc.pin(&pin)?;
49        let owner = live.owner;
50        // The document root has no block and therefore no lock: a port on
51        // the root is as editable as the root is, which is always.
52        let locked = doc.block(&owner).is_some_and(|block| block.locked);
53        (!locked).then_some(Self(pin))
54    }
55
56    /// Every pin of `pins` whose owner accepts material edits, in order. A
57    /// locked owner drops its pins from the set rather than refusing the
58    /// whole edit, which is how a mixed selection behaves everywhere else.
59    pub fn all(doc: &Document, pins: &[PinId]) -> Vec<Self> {
60        pins.iter().filter_map(|&id| Self::of(doc, id)).collect()
61    }
62
63    pub fn id(self) -> PinId {
64        self.0
65    }
66}
67
68/// A scope that accepts material interface edits — the document root, which
69/// has no lock to carry, or an unlocked block.
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub struct UnlockedScope(Scope);
72
73impl UnlockedScope {
74    /// `None` for a locked block, or for a scope the document does not hold
75    /// — which takes no edits at all, material or otherwise.
76    pub fn of(indexed: &IndexedDocument<'_>, owner: Scope) -> Option<Self> {
77        match resolve(indexed, owner) {
78            Resolved::Root => Some(Self(owner)),
79            Resolved::Block(block) => (!block.locked).then_some(Self(owner)),
80            Resolved::Absent => None,
81        }
82    }
83
84    pub fn scope(self) -> Scope {
85        self.0
86    }
87}