Skip to main content

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 beside the identity it touched, and the session records both as the step.
182    ///
183    /// **The identity is handed back rather than read off the command at walk time**, because
184    /// `Add` is the one command that cannot name the figure it creates: the diagram assigns it, and
185    /// this is the call that learns which one it was. Every other command names its figure, and
186    /// `Hang` names the connector rather than the target it hangs from — the figure the command
187    /// changed is the one a walk has to select.
188    pub(crate) fn apply_to(&self, diagram: &Diagram) -> Option<(Diagram, ShapeId)> {
189        match *self {
190            Self::Add { ref shape } => {
191                let (made, id) = diagram.add(shape.clone());
192                Some((made, id))
193            }
194            Self::Move { id, by } => touched(diagram, id, |shape| shape.displaced_by(by)),
195            Self::Resize { id, by } => touched(diagram, id, |shape| shape.resized_by(by)),
196            Self::RotateLine { id } => touched(diagram, id, Shape::with_rotated_orientation),
197            Self::SetStroke { id, ref stroke } => {
198                let stroke = stroke.clone();
199                touched(diagram, id, |shape| shape.with_stroke(stroke))
200            }
201            Self::SetText { id, ref text } => {
202                let text = text.clone();
203                touched(diagram, id, |shape| shape.with_text(text))
204            }
205            Self::InvertTerminals { id } => touched(diagram, id, Shape::with_inverted_terminals),
206            Self::MoveEndpoint { id, end, by, out } => {
207                touched(diagram, id, |shape| shape.with_endpoint_moved(end, by, out))
208            }
209            Self::ResetEndpoint { id, end } => {
210                touched(diagram, id, |shape| shape.with_endpoint_reset(end))
211            }
212            Self::SetEndpointTerminal {
213                id,
214                end,
215                ref terminal,
216            } => {
217                let terminal = terminal.clone();
218                touched(diagram, id, |shape| {
219                    shape.with_endpoint_terminal(end, terminal)
220                })
221            }
222            Self::DetachEndpoint { id, end } => touched(diagram, id, |shape| {
223                shape.with_endpoint_detached(end, diagram)
224            }),
225            Self::SetEndpointLeaving { id, end, leaving } => touched(diagram, id, |shape| {
226                shape.with_endpoint_leaving(end, leaving)
227            }),
228            Self::Hang {
229                id,
230                end,
231                target,
232                anchor,
233            } => touched(diagram, id, |shape| {
234                shape.with_endpoint_attached(
235                    end,
236                    Reference::new(target, anchor, Delta::ZERO, 0),
237                    diagram,
238                )
239            }),
240            Self::Forward { id } => diagram.can_forward(id).then(|| (diagram.forward(id), id)),
241            Self::Backward { id } => diagram.can_backward(id).then(|| (diagram.backward(id), id)),
242            Self::ToFront { id } => diagram.can_forward(id).then(|| (diagram.to_front(id), id)),
243            Self::ToBack { id } => diagram.can_backward(id).then(|| (diagram.to_back(id), id)),
244            Self::Remove { id } => diagram.get(id).map(|_| (diagram.remove(id), id)),
245        }
246    }
247}
248
249/// The diagram `change` leaves beside the figure `id` names, or `None` when it leaves that figure as
250/// it found it.
251///
252/// **A figure that is not there and a change that gives the same figure back are one answer**: the
253/// diagram a command leaves is the one it was given, and nothing above this line needs to tell the
254/// two apart. `Shape` compares by value, which is the diagram's own notion of the same figure and
255/// not a second one written here.
256fn touched(
257    diagram: &Diagram,
258    id: ShapeId,
259    change: impl FnOnce(&Shape) -> Shape,
260) -> Option<(Diagram, ShapeId)> {
261    let before = diagram.get(id)?;
262    let after = change(before);
263
264    (after != *before).then(|| (diagram.replace(id, after), id))
265}