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}