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, 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}