monospace_editing/command.rs
1//! What a caller means to do to a diagram: one thing, with the fields that go with it, and the
2//! diagram's own operation underneath. See the _Vocabulary_ row for `Command` and §2 of
3//! [`docs/editing-model.md`](../../../docs/editing-model.md).
4
5use monospace_diagram::{
6 Anchor, Delta, Diagram, Direction, End, Reference, Shape, ShapeId, Stroke, Terminal, Text,
7};
8
9/// One thing a caller means to do to a diagram.
10///
11/// **It carries the meaning and not the finished figure, and each one says which of the diagram's
12/// seven operations it becomes.** `Replace(ShapeId, Shape)` would restate the diagram's own
13/// operation and throw away what the caller knew — a delta is what a person reading a list of
14/// these recognizes, and what a line of a script has to be written in either way — and the
15/// decision of which operation a meaning becomes is the work of this layer rather than of every
16/// caller that has one.
17///
18/// **The set is open and a command added to it is additive.** A session already does whatever a
19/// command says, so one more meaning is one more arm here and nothing else. The set below is
20/// what the callers so far needed and is no promise that its size is final.
21///
22/// The identities are compared by value, so one issued by another diagram is a well-formed value
23/// matching nothing here — the same answer the diagram gives everything that names a figure it
24/// does not hold. **A command that changes nothing is not a step** (#285): the session records one only for
25/// the diagram a command leaves, so one naming a figure the diagram does not hold, and one
26/// whose change gives the figure back, leave the history where it was. See
27/// [`Session`](crate::Session) and §5 of the model.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum Command {
30 /// The figure goes `by`, along each axis.
31 Move {
32 /// The figure to move.
33 id: ShapeId,
34 /// How far, along each screen axis. Negative amounts move against it.
35 by: Delta,
36 },
37 /// The figure becomes so much wider and taller, or a line longer or shorter along its orientation.
38 Resize {
39 /// The figure to resize.
40 id: ShapeId,
41 /// How much the size changes along screen axes: for a box `dx` is width and `dy` is height;
42 /// for a line the component along its orientation changes its length.
43 by: Delta,
44 },
45 /// The line's orientation is toggled between horizontal and vertical.
46 RotateLine {
47 /// The line whose orientation is being toggled.
48 id: ShapeId,
49 },
50 /// The figure's stroke is replaced with `stroke`.
51 SetStroke {
52 /// The figure whose stroke is being set.
53 id: ShapeId,
54 /// The stroke to apply.
55 stroke: Stroke,
56 },
57 /// The box's text is replaced with `text`, or taken out when it is `None`.
58 SetText {
59 /// The box whose text is being set.
60 id: ShapeId,
61 /// The block to store, or `None` for no text at all.
62 text: Option<Text>,
63 },
64 /// The connector's terminals are swapped.
65 InvertTerminals {
66 /// The connector whose terminals are being inverted.
67 id: ShapeId,
68 },
69 /// The connector's endpoint specified by `end` goes `by` and adjusts standoff by `out`.
70 MoveEndpoint {
71 /// The connector whose endpoint is being moved.
72 id: ShapeId,
73 /// Which endpoint is targeted (`From` or `To`).
74 end: End,
75 /// How far to move the endpoint along screen axes.
76 by: Delta,
77 /// How far outward to adjust standoff.
78 out: i16,
79 },
80 /// The connector's endpoint specified by `end` has its offset and standoff reset to zero.
81 ResetEndpoint {
82 /// The connector whose endpoint is being reset.
83 id: ShapeId,
84 /// Which endpoint is targeted (`From` or `To`).
85 end: End,
86 },
87 /// The connector's endpoint specified by `end` has its terminal replaced by `terminal`.
88 SetEndpointTerminal {
89 /// The connector whose endpoint terminal is being set.
90 id: ShapeId,
91 /// Which endpoint is targeted (`From` or `To`).
92 end: End,
93 /// The terminal style to apply.
94 terminal: Terminal,
95 },
96 /// The connector's endpoint specified by `end` is detached into an absolute canvas position.
97 DetachEndpoint {
98 /// The connector whose endpoint is being detached.
99 id: ShapeId,
100 /// Which endpoint is targeted (`From` or `To`).
101 end: End,
102 },
103 /// The connector's detached endpoint specified by `end` has its departure direction set to `leaving`.
104 SetEndpointLeaving {
105 /// The connector whose endpoint leaving direction is being set.
106 id: ShapeId,
107 /// Which endpoint is targeted (`From` or `To`).
108 end: End,
109 /// The direction the connector leaves the endpoint in.
110 leaving: Direction,
111 },
112 /// One end of a connector hangs from one of `target`'s four sides.
113 Hang {
114 /// The connector whose end is being hung.
115 id: ShapeId,
116 /// Which endpoint hangs (`From` or `To`).
117 end: End,
118 /// The figure the end hangs from.
119 target: ShapeId,
120 /// Which of that figure's four sides it hangs from.
121 anchor: Anchor,
122 },
123 /// The figure goes one place toward the front of the order.
124 Forward {
125 /// The figure to move forward.
126 id: ShapeId,
127 },
128 /// The figure goes one place toward the back of the order.
129 Backward {
130 /// The figure to move backward.
131 id: ShapeId,
132 },
133 /// The figure goes to the front of the order.
134 ToFront {
135 /// The figure to move to the front.
136 id: ShapeId,
137 },
138 /// The figure goes to the back of the order.
139 ToBack {
140 /// The figure to move to the back.
141 id: ShapeId,
142 },
143 /// The figure is added to the front of the drawing order.
144 Add {
145 /// The figure to add.
146 shape: Shape,
147 },
148 /// The figure is taken out.
149 Remove {
150 /// The figure to take out.
151 id: ShapeId,
152 },
153}
154
155impl Command {
156 /// The one of the diagram's seven operations this command is, carried out on `diagram`.
157 ///
158 /// **The whole of this method is the layer's mapping**, and it is why a caller does not have to
159 /// write it: reading the figure, rebuilding the one end that changes and keeping everything
160 /// else is logic that is subtly wrong in four different ways when each caller writes it. Every
161 /// operation is the diagram's and unchanged, and a command that changed the diagram by any
162 /// other way is not a command.
163 ///
164 /// **It takes the diagram and hands one back rather than writing into it**, which is what every
165 /// operation of the diagram now does. A command naming a figure the diagram does not hold hands
166 /// back the diagram it was given rather than a diagram something happened to: the two are the
167 /// same, and saying so here is how a caller reads a missing figure as silence rather than as a
168 /// change.
169 ///
170 /// **`pub(crate)` and no longer the public way in.** A caller performs a command by handing it
171 /// to a session — `Session::perform` is where it is recorded, discarded-with and carried out,
172 /// and only the session can do any of that. **What this method stays is what the model's
173 /// "a command that changed the diagram by any other way is not a command" is about**: it is
174 /// reachable from inside the crate and nowhere else, so the claim is a rule about the type
175 /// rather than a request from outside it.
176 ///
177 /// **`None` is the diagram the command found, and it is not a failure.** A figure the diagram does
178 /// not hold, a change that gives the figure back as it was, and a figure brought forward or to
179 /// the front when it is already at the front, or backward or to the back when it is already at
180 /// the back are one answer: nothing changed. What `Some` holds is the whole diagram the command
181 /// left, and the session records it as the step.
182 pub(crate) fn apply_to(&self, diagram: &Diagram) -> Option<Diagram> {
183 match *self {
184 Self::Add { ref shape } => Some(diagram.add(shape.clone()).0),
185 Self::Move { id, by } => changed(diagram, id, |shape| shape.displaced_by(by)),
186 Self::Resize { id, by } => changed(diagram, id, |shape| shape.resized_by(by)),
187 Self::RotateLine { id } => changed(diagram, id, Shape::with_rotated_orientation),
188 Self::SetStroke { id, ref stroke } => {
189 let stroke = stroke.clone();
190 changed(diagram, id, |shape| shape.with_stroke(stroke))
191 }
192 Self::SetText { id, ref text } => {
193 let text = text.clone();
194 changed(diagram, id, |shape| shape.with_text(text))
195 }
196 Self::InvertTerminals { id } => changed(diagram, id, Shape::with_inverted_terminals),
197 Self::MoveEndpoint { id, end, by, out } => {
198 changed(diagram, id, |shape| shape.with_endpoint_moved(end, by, out))
199 }
200 Self::ResetEndpoint { id, end } => {
201 changed(diagram, id, |shape| shape.with_endpoint_reset(end))
202 }
203 Self::SetEndpointTerminal {
204 id,
205 end,
206 ref terminal,
207 } => {
208 let terminal = terminal.clone();
209 changed(diagram, id, |shape| {
210 shape.with_endpoint_terminal(end, terminal)
211 })
212 }
213 Self::DetachEndpoint { id, end } => changed(diagram, id, |shape| {
214 shape.with_endpoint_detached(end, diagram)
215 }),
216 Self::SetEndpointLeaving { id, end, leaving } => changed(diagram, id, |shape| {
217 shape.with_endpoint_leaving(end, leaving)
218 }),
219 Self::Hang {
220 id,
221 end,
222 target,
223 anchor,
224 } => changed(diagram, id, |shape| {
225 shape.with_endpoint_attached(
226 end,
227 Reference::new(target, anchor, Delta::ZERO, 0),
228 diagram,
229 )
230 }),
231 Self::Forward { id } => diagram.can_forward(id).then(|| diagram.forward(id)),
232 Self::Backward { id } => diagram.can_backward(id).then(|| diagram.backward(id)),
233 Self::ToFront { id } => diagram.can_forward(id).then(|| diagram.to_front(id)),
234 Self::ToBack { id } => diagram.can_backward(id).then(|| diagram.to_back(id)),
235 Self::Remove { id } => diagram.get(id).map(|_| diagram.remove(id)),
236 }
237 }
238}
239
240/// The diagram `change` leaves, or `None` when it leaves the figure `id` names as it found it.
241///
242/// **A figure that is not there and a change that gives the same figure back are one answer**: the
243/// diagram a command leaves is the one it was given, and nothing above this line needs to tell the
244/// two apart. `Shape` compares by value, which is the diagram's own notion of the same figure and
245/// not a second one written here.
246fn changed(
247 diagram: &Diagram,
248 id: ShapeId,
249 change: impl FnOnce(&Shape) -> Shape,
250) -> Option<Diagram> {
251 let before = diagram.get(id)?;
252 let after = change(before);
253
254 (after != *before).then(|| diagram.replace(id, after))
255}