The state pattern pracitices for a diagraming app
I got inspired by this presentation (even didn’t attend the event) and wrote down some ideas about a diagraming app that is kind of similar to what the presentation was featuring. We have obviously no definitive way to make them effectively though, the more knowledge and practices about these kind of apps we have, the better our apps must become. So, why not write down my experimental practices for anyone’s sake?
https://speakerdeck.com/seanchas116/gong-tong-bian-ji-toroturunozuo-rifang
Where we are
There are a variety of applications we can make up with HTML Canvas. It means we can have at least as many variety of state pattern strategies as they are. Let’s make things simpler a bit. What we’d like to make is a diagraming app.
I’m not going to talk about why we should follow the state pattern or what-the-heck-is-the-state-pattern kind of topics. The main theme of this article is how we practically apply the state pattern to our diagraming app.
To get familiar with it, in case you might not, I strongly recommend you to read this masterpiece article in advance. Making your own diagraming apps a few times from scratch is definitely the great way to grasp the broad idea about a diagraming app as well.
https://gameprogrammingpatterns.com/state.html
What’s our goal
What can we do via the app we are trying to make? Here’s the list of them.
- Pan the viewport
- Select and deselect shapes
- Put new shapes
- Delete shapes
- Move shapes
- Draw a line
- Move a vertex of a line
Already looks intimidating, doesn’t it? No? Cool! I guess implementing these features isn’t actually the infeasible challenge for most of you. However, the important thing we need to consider from the beginning is scalability. The reason is obvious, right? I’m sure we never get satisfied with these features. We’ll crave more and more features such as transforming, layout and snapping. We may want more variety of specific featured shapes such as a circle, an arrow and even a table.
What is scalability, by the way? It’s definitely a controvertial word. Let’s declare its meaning for this article. We can say our app has good scalability when the complexity of the app linearly increases as it gains new features. You may want me to answer what the complexity means and how we can measure it. Believe in your intinct.
States
Default state
Now we have the canvas in our app anyhow. What are we doing then? Nothing, of course. That is our default state. Leave this state until we need to get back here.
Pan state
Let’s begin from the most primordial feature: panning. Suppose we have an example shape in the canvas. What would we like to do next? We’d like to enjoy the liberty of observing the shape, of course. I’m a person advocating panning by the middle button dragging because I’m a big fun of Blender. Then, it appears we’d like to activate pan state
on middle button down
.
Looks quite simple so far.
Select state
How to name a state might be the toughest issue of us human beings. pan
or panning
, select
or selected
and so on. It’s entirely up to your preferences though, I stick to the shortest form as much as possible this time just because it’ll help me reduce my typos.
Well, we’d like to select a shape somehow. How? Left click, of course. Precisely, let us select a shape by left button up
on the shape. Although click
event is available to us, I’m not a big fun of handling this event directly with this kind of canvas app. It’s also a bit awkward when we try to handle down
, up
and click
harmoniously for whatever reason.
Now, we got select
state and moved to it. What is the state supposed to do then? It’s a big topic as a matter of fact. The state should handle almost anything or at least as many as default
state. It’s a kind of intransient state.
What the heck is intransient state? Good question and there’s no definitive definition for it since it just came up to my mind as I wrote this article. The idea is simple. A state is intransient if it handles some events to activate conventional operations. What the heck are conventional operations then? There’s no definitive definition as well though, I can list up some examples such as “panning”, “(de)selecting shapes”, “moving shapes”, “undo/redo” etc. I hope you can see the idea from these examples.
Is pan
state intransitive? No, it’s transient. It could trriger some conventional operations if we really want it to do, but most of time we expect it just to pan the canvas and go back to the previous state as we release the button. Do something, then move to next state immediately, sounds transient, doesn’t it? That’s the idea.
What does intransitive state matter to us? It matters a lot. A lot and lot, because it has to handle a lot of events for conventional operations. Honestly, I don’t have great answer for how to effectively implement intransient states. Class oriented strategy may work to unify a great part of those states, even though I’m basically a class hater in JS realm. I’m not going to elaborate the detail about the implementations here, it’s would be a another huge topic.
Put the implementations aside or forward, maybe behind, it appears some states such as pan
state should be activated from any intransient states. Then, we can make our diagram a bit more general and simpler like this.
So far so good. Thanks to the idea of the intransient state, we can extract some conventional transitions from each state. It’ll make our state diagram lovely.
Put new shapes
Sadly, things become complicated from now on, but it also mean we are now getting into more practical operations. First of all, we’d like to put a new shape into the canvas.
Let’s define the process for putting a new shape to begin with. Suppose there’s a button for a shape in our app, and we can put a new shape by drag & drop operation from the button to the canvas.
One issue was detected. I said there’s a button in our app. It’s a button, it’s a HTML button element. Do our states also need to handle any events taking place outside the canvas element? Partially yes. We sometimes want our states to handle certain custom events and this is one of those cases.
In short, we’d like to activate put shape
state by left button down
on the button. Since this left button down
takes place outside the canvas, we’d better make up a custom event for this feature. Otherwise, we would end up making tons of custom events for each UI outside the canvas.
This time, we’re going to make up activate state
event that contains some parameters to activate certain state. One notice is that whether the certain state is activated or not is still up to the current state. If the current state doesn’t handle the event, just nothing happens then.
The event looks like this for example. It would change depending on an implementation.
{ type: "activate-state", data: { name: "put-shape", shapes: [..] } }
Which state would we like to activate ‘put shape’ from? We have had the answer, of course. It should be any intransient states because the new shape button should be available to us whenever we are in an intransitive state. After putting a new shape on the canvas, probably we’d like to select the newly created shape for further operations. So put shape
state is a transient state and let us move to select
state consecutively.
We also need to save and select the new shape as we finish put shape
state. To make things simple, we are mainly focusing at the transition between states.
Delete shapes
We already have enough to tools to achieve this feature. Let’s say we’d like to delete selected shapes by the event "Delete" key press
. Maybe release
, whatever you like.
Since we have select
state that should be the one handling this operation.
As long as select
state checks shapes' selection status correctly, all we need to do is just adding the handling for the event "Delete" key press
. Then, select
state takes care of the state transition as shapes' selection status changes.
Move shapes
Almost likewise put shape
state, but the activation trigger is a bit different and should take place from select
state.
Things are going easy, right? Then, let’s make them a bit complicated because we want our app to be convenient, don’t we?
Imagine, we’d like to move a shape that isn’t currently selected. Up to now, the only way to activate move
state requires us to select the shape in advance, then start dragging the shape. That means we have to press the button twice. We don’t want to bother to press it twice, let us move any shape by just dragging regardless of its selection status. It seems we want below state transition.
Wait, we already have this transition and default
state is one of intransient states, so we these triggers are competing each other.
It might be a good idea just to move to move
state, but probably we would end up facing some troubles. We usually expect specific appearance during moving shapes such as cursor style and the bounding box. It doesn’t feel good if those specific appearances momentarily show up every time we select a shape. Therefore, we need to add branches to activate either select
or move
state by the event left button down
on a shape.
Either way, we do left button down
on a shape at first. Then, if we drag, the state should change to move
state. If we don’t drag and release the button, the state should change to select
state. It seems we need to have a flag to track if we hold the button and wait the next action while the flag is on.
No way. We are practicing the state pattern, so why not making a state instead of the flag? Let’s name it left down on shape
state. I know it’s long, you can name it as you like.
We don’t need to bother to draw a flowchart for the flag because the dedicated state presents the simple transition. Even the role of this state feels a bit special, the state does nothing special but handling some events, left button up
and mouse move
, like other states. So there’s no new idea for this transition, we just added a state and let it handle some events.
Now, We’ve got a great insight from this example. Anytime we feel like having a flag to handle something, we’d better consider if we can achieve it by adding a state without introducing the flag into currently existing states. When the number of those flags isn’t large, we may feel we can handle it somehow. However, it’s never the case as the number of them increases over the time.
Draw a line
Did you remember we are making a diagraming tool? We can now create, select, move and delete a shape. Let’s get a line then. I suppose we’d like to draw a line by following steps.
Press a button to get ready to draw a line
Do
left button down
to start drawing the line- The first vertex is placed by this step
Do
mouse move
to move the second vertexDo
left button up
to finish drawing the line- We’d expect to select the newly created line
As you see, it appears we need two new states to achieve these steps. Let’s name them line ready
state and line draw
state. Likewise put shape
state, we should be able to activate the first state anytime the button is available to us. That means the state should be able to be activated from any intransient states.
Although the whole transitions look bigger since four different states are related to this operation, each transition is pretty straightforward as a matter of fact. It’s basically just one-way transition and all events for the transitions are already familiar to us.
Move a vertex of a line
Might as well, we feel like moving a vertex! Problem detected, which state can we activate move vertex
state from? The activation event is surely left button down
on a vertex, it seems easy enough. We can suppose that the line we are about to edit should be selected in advance. A line is certainly a kind of shapes, so that we should be in select
state when a line is selected.
Although things become complicated once again, we know what we should rely on to begin with. That’s right, make a state to handle complexity we are facing to.
How’s this? It would work, but we should remind ourselves that intransient
in the image represents all intransient states. The number of select xx
states would also increase because we need new one every time we try to implement specific operations for a specific shape type. Therefore, the relationships between intransient states and select xx
states would explode over the time.
One way I can suggest is having a hub state to handle selection branches line this.
I put any state
before selection hub
state because this is the real situation we are facing to. selection hub
state is activated from technically any states that can change shapes' selection status. I’ve avoided mentioning multiple shape selection so far to make things simple, but it does matter to handling select xx
states.
This is a good occasion, let’s have select multiple
state now. Suppose we selected a line and another conventional shape, so we are in select multiple
state. Next, we deselected the conventional shape. Where are we then? We should be select line
state for sure. I’m sorry I already don’t want to bother to draw this situation without having selection hub
state. So, this is the result.
Since any state
represents literally any states, this image isn’t as simple as it looks like. However, it’s also not as complicated as you might imagine it is. Aside from selection hub
state, all other states just have to activate selection hub
state as shapes' selection changes anyhow. As long as we manage to cram all selection branches into the hub state, it’ll take care of all complexity of selection transitions. Each branch wouldn’t be so complicated to deal with though, once they spread throughout our states it’s gonna be a disaster. So, let’s cram them all into the hub state. The day is saved.
We’ve got a great tool, then let’s get back to the first place. What we need remained is just one more state: move vertex
state. It’s almost identical to draw line
state though, the situation is a bit different from its situation. So, making new state is simpler than taking care of minor differences of them in one state.
Wrap up
Thanks for reading all the way up to here. We’ve got through all features we expected. I hope this article gives you some sort of insight about the state pattern for diagraming tools. These are literally the very first steps to sculpture your ideal tool and there’re still tons of topics yet to talk about. You may have much better strategy than this article. Congrats, share it with me discreetly, please!
Have a nice diagraming day 👋