Skip to main content

blockworx/
gesture.rs

1//! The gesture bracket: what a gesture is, stated once.
2//!
3//! Everything written between [`Gesture::open`] and [`seal`] lands in one
4//! commit — the solve rider completes it, the seal decides whether there is
5//! anything to say, and the repo folds it.
6
7use blockworx_doc::commit::{Commit, CommitBuilder};
8use blockworx_doc::document::{DocIndex, Document, IndexedDocument};
9use blockworx_doc::id::{Allocator, Id, IdKind};
10use blockworx_doc::opcode::OpCodes;
11
12use crate::edit::describe::Label;
13use crate::path::BlockPath;
14use crate::presentation::Presentation;
15use crate::widget::drawing::Drawing;
16use blockworx_doc::repo::Repo;
17use blockworx_store::doc::Writability;
18
19/// The ops a gesture has authored, and the document they imply.
20///
21/// The prediction is why this is a type rather than a bare [`CommitBuilder`].
22/// The repo's document only advances at submit, so a gesture reading it
23/// would not see its own writes: a block placed and immediately
24/// renamed reads as absent, which is what the four `Arming` tool states used
25/// to defer around. [`Gesture::author`] is the only way in, and it folds
26/// what the gesture has said so far onto the base — so the next read, in
27/// this same frame, sees it.
28pub struct Gesture {
29    ops: CommitBuilder,
30    /// What this gesture is called before it has authored anything —
31    /// finished at the seal, where the ops say what it acted on.
32    label: Label,
33    /// The base with [`Self::ops`] folded on. `None` while the gesture has
34    /// authored nothing, because then the base already *is* the prediction —
35    /// an idle frame pays neither the fold nor the index rebuild.
36    staged: Option<Staged>,
37    /// The ids this gesture has handed out, beside the document's own
38    /// marks. A mint that never becomes a `Create` still raises this, so
39    /// two mints of one kind cannot collide inside a gesture; an abandoned
40    /// gesture takes its marks with it, which costs nothing because it
41    /// created nothing.
42    ids: Allocator,
43    /// Whether the session behind this sink may write at all. Carried here
44    /// because this is the write door: a read-only session's gesture
45    /// declines in [`Self::author`], and the tools read the same answer
46    /// through [`Drawing::authoring`](crate::widget::drawing::Drawing::authoring)
47    /// to decide what to offer in the first place.
48    writability: Writability,
49}
50
51struct Staged {
52    doc: Document,
53    index: DocIndex,
54}
55
56impl Gesture {
57    /// Open a gesture under its semantic label.
58    pub fn open(label: Label, writability: Writability) -> Self {
59        Self {
60            // The builder's own label never reaches a commit: the seal
61            // replaces it with what the ops turned out to describe.
62            ops: CommitBuilder::new(String::new()),
63            label,
64            staged: None,
65            ids: Allocator::default(),
66            writability,
67        }
68    }
69
70    /// A gesture between gestures, and the sink a read pass is handed.
71    /// Nothing writes into it — every write opens its own labelled one — so
72    /// neither this label nor its writability reaches a commit.
73    pub fn idle() -> Self {
74        Self::open(Label::verb("Idle"), Writability::Writable)
75    }
76
77    pub fn writability(&self) -> Writability {
78        self.writability
79    }
80
81    pub fn ops(&self) -> &[OpCodes] {
82        self.ops.ops()
83    }
84
85    /// The allocator an emitter whose id count is data-dependent (paste)
86    /// mints from: this gesture's marks raised to the prediction's, so
87    /// nothing it has authored can be minted twice.
88    pub fn ids(&self, base: &Document) -> Allocator {
89        let mut ids = self.ids;
90        ids.raise_to(&self.prediction(base).ids());
91        ids
92    }
93
94    /// A fresh id for a setter that mints one.
95    pub fn mint<K: IdKind>(&mut self, base: &Document) -> Id<K> {
96        self.ids = self.ids(base);
97        self.ids.mint()
98    }
99
100    /// The document as this gesture has left it: the base until it writes.
101    fn prediction<'v>(&'v self, base: &'v Document) -> &'v Document {
102        self.staged.as_ref().map_or(base, |staged| &staged.doc)
103    }
104
105    /// The document this gesture reads: `base` with its own writes folded
106    /// on. Identical to `base` until it writes.
107    pub fn view<'v>(&'v self, base: IndexedDocument<'v>) -> IndexedDocument<'v> {
108        self.staged
109            .as_ref()
110            .and_then(|staged| staged.index.view_of(&staged.doc))
111            .unwrap_or(base)
112    }
113
114    /// The one write door. `emit` is handed the document as this gesture has
115    /// left it and the sink to author into; whatever it pushes advances the
116    /// prediction before the call returns, so no caller can read a document
117    /// that predates its own edit.
118    ///
119    /// `what` names the edit for the mutation log (`RUST_LOG=edit=debug`),
120    /// which narrates here rather than at each setter: authoring without
121    /// narrating would mean calling this without an argument.
122    pub fn author(
123        &mut self,
124        base: IndexedDocument<'_>,
125        what: &'static str,
126        emit: impl FnOnce(&IndexedDocument<'_>, &mut CommitBuilder),
127    ) {
128        if self.writability == Writability::ReadOnly {
129            tracing::debug!(target: "edit", edit = what, "withheld: the session is read-only");
130            return;
131        }
132        let before = self.ops.ops().len();
133        {
134            // Disjoint fields: the view borrows the staged prediction, the
135            // sink is the builder beside it.
136            let view = self
137                .staged
138                .as_ref()
139                .and_then(|staged| staged.index.view_of(&staged.doc))
140                .unwrap_or(base);
141            emit(&view, &mut self.ops);
142        }
143        let authored: Vec<String> = self.ops.ops()[before..]
144            .iter()
145            .map(OpCodes::narrate)
146            .collect();
147        if authored.is_empty() {
148            // A named setter that found nothing to change: the emitters drop
149            // non-edits, which is what keeps an abandoned gesture empty.
150            tracing::debug!(target: "edit", edit = what, "declined");
151            return;
152        }
153        tracing::debug!(target: "edit", edit = what, ops = ?authored, "authored");
154        self.restage(base.doc);
155    }
156
157    /// Re-fold the whole gesture onto `base`. Whole, not incremental: op
158    /// order within a commit is free for references (a pin may precede its
159    /// owner's create), so a half-gesture can fail a validation the finished
160    /// one passes.
161    fn restage(&mut self, base: &Document) {
162        let staged = Commit::new(String::new(), self.ops.ops().to_vec());
163        match base.try_apply(&staged) {
164            Ok(doc) => {
165                let index = DocIndex::of(&doc);
166                self.staged = Some(Staged { doc, index });
167            }
168            // The refusal the submission will report. Keeping the previous
169            // prediction rather than dropping to the base leaves the user
170            // looking at what they drew until the seal says otherwise.
171            Err(refusal) => {
172                tracing::error!("the gesture's own prediction was refused: {refusal}");
173            }
174        }
175    }
176
177    /// Seal the gesture under `label` and leave a fresh idle one in its
178    /// place. `None` when nothing was authored, which is what makes an
179    /// abandoned drag a non-event.
180    pub(crate) fn seal(&mut self, label: String) -> Option<Commit> {
181        let fresh = Self::open(Label::verb("Idle"), self.writability);
182        std::mem::replace(self, fresh)
183            .ops
184            .seal()
185            .map(|commit| commit.relabelled(label))
186    }
187}
188
189/// The current scope, over the gesture's view of the repo's document.
190pub fn drawing<'a>(
191    repo: &'a Repo,
192    index: &'a mut DocIndex,
193    path: &'a BlockPath,
194    presentation: &'a mut Presentation,
195    gesture: &'a mut Gesture,
196) -> Drawing<'a> {
197    let base = index.view(repo.document());
198    Drawing::new(base, path, presentation, gesture)
199}
200
201/// Finish the gesture: run the solve rider over what it wrote and seal it,
202/// leaving `gesture` open on a fresh idle one. `None` when it authored
203/// nothing.
204pub fn seal(
205    document: &Document,
206    index: &mut DocIndex,
207    path: &BlockPath,
208    presentation: &Presentation,
209    gesture: &mut Gesture,
210) -> Option<Commit> {
211    if gesture.ops().is_empty() {
212        return None;
213    }
214    // Described before the rider runs: the wires it re-solves are a
215    // consequence of the edit, not the thing the user did, and counting
216    // them would turn "move a block" into "5 shapes".
217    let label = gesture
218        .label
219        .describing(&index.view(document), gesture.ops());
220    let base = index.view(document);
221    gesture.author(base, "solve_rider", |indexed, sink| {
222        crate::widget::routing::solve_rider(indexed, path, presentation, sink);
223    });
224    gesture.seal(label)
225}