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}