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}