Skip to main content

blockworx_editor/
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 renamed
24/// would read as absent. `Gesture::author` is the only way in, and it folds
25/// what the gesture has said so far onto the base — so the next read, in
26/// this same frame, sees it.
27pub struct Gesture {
28    ops: CommitBuilder,
29    /// What this gesture is called before it has authored anything —
30    /// finished at the seal, where the ops say what it acted on.
31    label: Label,
32    /// The base with [`Self::ops`] folded on. `None` while the gesture has
33    /// authored nothing, because then the base already *is* the prediction —
34    /// an idle frame pays neither the fold nor the index rebuild.
35    staged: Option<Staged>,
36    /// The ids this gesture has handed out, beside the document's own
37    /// marks. A mint that never becomes a `Create` still raises this, so
38    /// two mints of one kind cannot collide inside a gesture; an abandoned
39    /// gesture takes its marks with it, which costs nothing because it
40    /// created nothing.
41    ids: Allocator,
42    /// Whether the session behind this sink may write at all. Carried here
43    /// because this is the write door: a read-only session's gesture
44    /// declines in [`Self::author`], and the tools read the same answer
45    /// through [`Drawing::authoring`](crate::widget::drawing::Drawing::authoring)
46    /// to decide what to offer in the first place.
47    writability: Writability,
48}
49
50struct Staged {
51    doc: Document,
52    index: DocIndex,
53}
54
55impl Gesture {
56    /// Open a gesture under its semantic label.
57    pub fn open(label: Label, writability: Writability) -> Self {
58        Self {
59            // The builder's own label never reaches a commit: the seal
60            // replaces it with what the ops turned out to describe.
61            ops: CommitBuilder::new(String::new()),
62            label,
63            staged: None,
64            ids: Allocator::default(),
65            writability,
66        }
67    }
68
69    /// A gesture between gestures, and the sink a read pass is handed.
70    /// Nothing writes into it — every write opens its own labelled one — so
71    /// neither this label nor its writability reaches a commit.
72    pub fn idle() -> Self {
73        Self::open(Label::verb("Idle"), Writability::Writable)
74    }
75
76    pub fn writability(&self) -> Writability {
77        self.writability
78    }
79
80    pub fn ops(&self) -> &[OpCodes] {
81        self.ops.ops()
82    }
83
84    /// The allocator an emitter whose id count is data-dependent (paste)
85    /// mints from: this gesture's marks raised to the prediction's, so
86    /// nothing it has authored can be minted twice.
87    pub fn ids(&self, base: &Document) -> Allocator {
88        let mut ids = self.ids;
89        ids.raise_to(&self.prediction(base).ids());
90        ids
91    }
92
93    /// A fresh id for a setter that mints one.
94    pub fn mint<K: IdKind>(&mut self, base: &Document) -> Id<K> {
95        self.ids = self.ids(base);
96        self.ids.mint()
97    }
98
99    /// The document as this gesture has left it: the base until it writes.
100    fn prediction<'v>(&'v self, base: &'v Document) -> &'v Document {
101        self.staged.as_ref().map_or(base, |staged| &staged.doc)
102    }
103
104    /// The document this gesture reads: `base` with its own writes folded
105    /// on. Identical to `base` until it writes.
106    pub fn view<'v>(&'v self, base: IndexedDocument<'v>) -> IndexedDocument<'v> {
107        self.staged
108            .as_ref()
109            .and_then(|staged| staged.index.view_of(&staged.doc))
110            .unwrap_or(base)
111    }
112
113    /// The one write door. `emit` is handed the document as this gesture has
114    /// left it and the sink to author into; whatever it pushes advances the
115    /// prediction before the call returns, so no caller can read a document
116    /// that predates its own edit.
117    ///
118    /// `what` names the edit for the mutation log (`RUST_LOG=edit=debug`),
119    /// which narrates here rather than at each setter: authoring without
120    /// narrating would mean calling this without an argument.
121    ///
122    /// Crate-private on purpose: a gesture is authored into through a
123    /// [`Drawing`] method, never by the tool
124    /// holding it.
125    pub(crate) fn author(
126        &mut self,
127        base: IndexedDocument<'_>,
128        what: &'static str,
129        emit: impl FnOnce(&IndexedDocument<'_>, &mut CommitBuilder),
130    ) {
131        if self.writability == Writability::ReadOnly {
132            tracing::debug!(target: "edit", edit = what, "withheld: the session is read-only");
133            return;
134        }
135        let _s = tracing::info_span!("author", label = what).entered();
136        let before = self.ops.ops().len();
137        {
138            // Disjoint fields: the view borrows the staged prediction, the
139            // sink is the builder beside it.
140            let view = self
141                .staged
142                .as_ref()
143                .and_then(|staged| staged.index.view_of(&staged.doc))
144                .unwrap_or(base);
145            emit(&view, &mut self.ops);
146        }
147        let authored: Vec<String> = self.ops.ops()[before..]
148            .iter()
149            .map(OpCodes::narrate)
150            .collect();
151        if authored.is_empty() {
152            // A named setter that found nothing to change: the emitters drop
153            // non-edits, which is what keeps an abandoned gesture empty.
154            tracing::debug!(target: "edit", edit = what, "declined");
155            return;
156        }
157        tracing::debug!(target: "edit", edit = what, ops = ?authored, "authored");
158        self.restage(base.doc);
159    }
160
161    /// Re-fold the whole gesture onto `base`. Whole, not incremental: op
162    /// order within a commit is free for references (a pin may precede its
163    /// owner's create), so a half-gesture can fail a validation the finished
164    /// one passes.
165    #[tracing::instrument(level = "info", skip_all, fields(ops = self.ops.ops().len()))]
166    fn restage(&mut self, base: &Document) {
167        let staged = Commit::new(String::new(), self.ops.ops().to_vec());
168        match base.try_apply(&staged) {
169            Ok(doc) => {
170                let index = {
171                    let _s = tracing::info_span!("doc_index").entered();
172                    DocIndex::of(&doc)
173                };
174                self.staged = Some(Staged { doc, index });
175            }
176            // The refusal the submission will report. Keeping the previous
177            // prediction rather than dropping to the base leaves the user
178            // looking at what they drew until the seal says otherwise.
179            Err(refusal) => {
180                tracing::error!("the gesture's own prediction was refused: {refusal}");
181            }
182        }
183    }
184
185    /// Seal the gesture under `label` and leave a fresh idle one in its
186    /// place. `None` when nothing was authored, which is what makes an
187    /// abandoned drag a non-event.
188    pub fn seal(&mut self, label: String) -> Option<Commit> {
189        let fresh = Self::open(Label::verb("Idle"), self.writability);
190        std::mem::replace(self, fresh)
191            .ops
192            .seal()
193            .map(|commit| commit.relabelled(label))
194    }
195}
196
197/// The current scope, over the gesture's view of the repo's document.
198pub fn drawing<'a>(
199    repo: &'a Repo,
200    index: &'a mut DocIndex,
201    path: &'a BlockPath,
202    presentation: &'a mut Presentation,
203    gesture: &'a mut Gesture,
204) -> Drawing<'a> {
205    let base = index.view(repo.document());
206    Drawing::new(base, path, presentation, gesture)
207}
208
209/// Finish the gesture: run the solve rider over what it wrote and seal it,
210/// leaving `gesture` open on a fresh idle one. `None` when it authored
211/// nothing.
212#[tracing::instrument(level = "info", skip_all)]
213pub fn seal(
214    document: &Document,
215    index: &mut DocIndex,
216    path: &BlockPath,
217    presentation: &Presentation,
218    gesture: &mut Gesture,
219) -> Option<Sealed> {
220    if gesture.ops().is_empty() {
221        return None;
222    }
223    // Described before the rider runs: the wires it re-solves are a
224    // consequence of the edit, not the thing the user did, and counting
225    // them would turn "move a block" into "5 shapes".
226    let label = gesture
227        .label
228        .describing(&index.view(document), gesture.ops());
229    let base = index.view(document);
230    // Read before authoring: the rider is scoped to what the gesture wrote, and
231    // the footprints it *left* are only in the pre-gesture document.
232    let written = gesture.ops().to_vec();
233    let vacated = vacated_footprints(document, &written);
234    let mut disturbed = None;
235    gesture.author(base, "solve_rider", |indexed, sink| {
236        disturbed = crate::widget::routing::solve_rider(
237            indexed,
238            path,
239            presentation,
240            crate::widget::routing::Edited {
241                ops: &written,
242                vacated: &vacated,
243            },
244            sink,
245        );
246    });
247    gesture
248        .seal(label)
249        .map(|commit| Sealed { commit, disturbed })
250}
251
252/// What a sealed gesture hands back: the commit, and the rectangle it
253/// disturbed — so the reconstruction that follows can be confined to it rather
254/// than re-deriving every wire in the document.
255///
256/// `None` for a gesture whose reach is not known, which re-derives everything.
257pub struct Sealed {
258    pub commit: Commit,
259    pub disturbed: Option<blockworx_geom::Rect>,
260}
261
262/// Where the shapes a gesture moved *were*, before it moved them. A wire the
263/// departing footprint was blocking may now run straight, and only the
264/// pre-gesture document still knows where that footprint was.
265fn vacated_footprints(document: &Document, ops: &[OpCodes]) -> Vec<blockworx_geom::Rect> {
266    ops.iter()
267        .filter_map(|op| match op.target() {
268            blockworx_doc::id::EntityRef::Block(id) => {
269                Some(crate::grid::px_rect(document.block(&id)?.rect))
270            }
271            _ => None,
272        })
273        .collect()
274}