This file documents tinytools software architecture.
tinytools consists of the M (as in MVC) of the tinytools app. The VC implementations (separate repository) are connected via a Reflex interface in GoatWidget
- Document: the thing you are working on in tinytools, I don't know how else to describe this 🤣
- Scene: same as document
- Canvas: the 2D interactable area of tinytools
- Canvas also refers to the designated area of the document that we care about (TODO explain better)
- Layers: the logical list view of elements and folders in the scene
- Owl: an element in the scene
- Element: same as owl
- Layers: the layer system which is a logical view into the scene state
- OwlState: the data structure representing the document state
The main tinytools state hierarchy looks something like this:
╔GoatState══════════════════╗
║-scene info ║
║-render state ║
║-input state ║
║ ║
║╔OwlPFWorkspace═══════════╗║
║║-do/undo stack ║║
║║ ║║
║║╔OwlState═══════════════╗║║
║║║-canvas ║║║
║║║ ║║║
║║║╔OwlTree══════════════╗║║║
║║║║tree of all elements ║║║║
║║║║or "owls" in the doc ║║║║
║║║╚═════════════════════╝║║║
║║╚═══════════════════════╝║║
║╚═════════════════════════╝║
╚═══════════════════════════╝
Goat.hs is the effective entry point into tinytools with endoGoatCmd... :: Endo GoatState as the primary state update functions.
- NOTE
GoatStatewill eventually be refactored to contain a list ofGoatTabs, one for each document tab.
OwlPFWorkspace contains the LLamaStack which contains information on OwlState update operations or Llamas
OwlState contains a SCanvas which represents the visible area of the document.
OwlTree is a specialized tree object representing the elements, called "Owls" in the document. The tree is indexed by REltId which is a unique integer id for each owl.
The application state exposes a reflex interfaces via GoatWidget which essentially wraps a GoatState and wires the reflex inputs to update the state and connects the state to various outputs.
The inputs on the left in the diagram below are reflex Events and the outputs on the right or reflex Dynamics.
╔GoatWidget═════════════════╗
╔═════════kb/mouse══════════>║ ║═════════owl state═══════════╗
║ ║ ║ ║
║════════screen size════════>║ ║════════canvas info═════════>║
║ ║ ║ ║
║═══════canvas resize═══════>║ ║══════rendered scene════════>║
║ ║ ║ ║
║═══════style changes═══════>║ ║═════════selection══════════>║
║ ║ ║ ║
║═════════load file═════════>║*--------------* ║═══════handler info═════════>║
║ ║|GoatState | ║ ║
║═══════════other═══════════>║| | ║═══════════other════════════>║
║ ║*--------------* ║ ║
║ ╚═══════════════════════════╝ ║
║ ║
║ ║
║ █tinytools-myviewcontroller██ ║
║ █ ╔═════╗╔═════════════════╗█ ║
║ █ ║... ║║ ║█ ║
║ █ ║ ║║ ╔════╗ ║█ ║
║ █ ║ ║║ ║ ║═════╗ ║█ ║
║ █ ║ ║║ ╚════╝ ║ ║█ ║
║ █ ║ ║║ v ║█ ║
╚════════════════════════════█ ║═════║║ ╔boop══╗ ╔════╗ ║█<════════════════════════════╝
█ ║ ║║ ║@@@@@@║ ║ ║ ║█
█ ║ ║║ ║@@@@@@║ ╚════╝ ║█
█ ║═════║║ ║@@@@@@║ ║█
█ ║ ║║ ║@@@@@@║ ║█
█ ║ ║║ ╚══════╝ ║█
█ ╚═════╝╚═════════════════╝█
█████████████████████████████
Please see comments in GoatWidget and GoatWidgetConfig in GoatWidget.hs for more details.
For an example setup, see Potato.Flow.Vty.Main in the tinytools-vty project
Some mouse and keyboard input is handled within tinytools. In particular, the canvas and layers input are handled by tinytools.
Mouse input is reported as click streams, see LMouseData
Mouse input must be reported separately for the Layers and canvas area by setting the _lMouseData_isLayerMouse field. All input sequences in one area must end with a mouse release (_lMouseData_isRelease = True) before switching to the other area or it will assert. (in retrospect, this is could have been done a little more clear/safe)
Most mouse and keyboard input is managed by instances of the Handler class. The layers and canvas portions can each only have 1 handler each at a time.
If a Handler method processes the input, it returns a PotatoHandlerOutput which contains information on how to update the scene as well as a new Handler.
The handler lifecycle is important and has a few subtle/idiosyncratic requirements. Below is a description of the key state transition of the handler lifecycle (for both Layers and Canvas handlers, although some scenarios described really only apply to the Canvas handler)
- there is always a handler
- handlers may be active or inactive
- an example of an active handler is when `BoxHandler; is in the middle of resizing an object via mouse drag
- after processing input, handlers return some output action as well as a maybe a new handler
- if no new handler is returned, the handler does not process the input and the input may be processed in other ways
- for example, if a mouse down input is not handled by the canvas handler, then a
SelectHandleris created (replacing the other handler) to process the input
- for example, if a mouse down input is not handled by the canvas handler, then a
- If a new handler is returned, it is set as the handler
- if no new handler is returned, the handler does not process the input and the input may be processed in other ways
- after a new object(s) is created locally
- select the newly created object(s)
- NOTE this is SUPER important, as the handler that just created the element expects it to be selected to continue modifying it
- if the current handler is not active, create a new handler based on the newly created object
- if the current handler is active, do nothing, trust that the current handler knows what it's doing
- select the newly created object(s)
- after any change to the doc's OwlState, refresh the handler, which may produce a non-trivial output
- if Esc is pressed, cancel the current handler, which may produce a non-trivial output
tinytools contains 3 main optimizations for rendering
- a broadphase culling step
- This is managed by the state object
BroadPhaseState - Note, the current implementation just does a linear cull but someday it will use a proper KD tree.
- This is managed by the state object
- incremental rendering of only parts that changed
- Each change operation produces a list of boxes indicating the areas that changed and only those areas are re-rendered
- Only the visible screen region is rendered. The screen region can be set with
_goatWidgetConfig_canvasRegionDim
- caching
- each object has a generic cache which is the post rendered unicode output as well as an optional specific cache based on its needs
The rendered scene available through _goatWidget_renderedCanvas and the rendered selection is available through _goatWidget_renderedSelection.
The handler gizmos (the things you use to manipulate the scene or layers) are available through _goatWidget_handlerRenderOutput and _goatWidget_layersHandlerRenderOutput
The broadphase culling step is managed by the BroadPhase class which uses axis aligned bounding boxes (AABBs) to cull out anything outside the regions to be re-rendered.
- NOTE
BroadPhaseuses a linear search implementation and will be upgrade to a kd-tree implementation if that should ever make a meaningful difference.
Each update operation returns a list of modified elements and only the areas on the screen that are impacted by the modified elements are rerenderered.
GoatState stores a RenderCache object which is a map from each owl's id into a OwlItemCache.
Each time there is a change, the RenderCache is cleared on the objects that got changed, and will get regenerated in the render step.
OwlItemCache include caches specific to each owl type as well as a generic PreRender cache that applies to all owl types. The PreRender cache is also used for rendering unicode wide chars.
The RenderCache is also used by the handlers for visual based interface operations.
tinytools has many many unit tests (UTs). They come in 3 types:
- direct testing of some functions e.g.
plus 1 1 `shouldBe` 2 - testing via
GoatTestermonad - DEPRECATED testing via
GoatWidget
GoatTester is a State monad that contains a GoatState and an interface to sensibly update, read, and test the state. Mosts tests thus consist of several operations (as if one were using the app) before checking if the state is as expected.
These tests go through the Reflex interface which is very unecessary and have since been replaced by GoatTester.
foldGoatFnevaluated multiple times per input event. Specifically, any input event to GoatWidget will causefoldGoatFnto be evaluated at least twice.
There are many future additions we'd like to make to tinytools. The most relevant ones to be aware of are:
- better styling tools
- style objects to be switched over to swatch references rather than stored per-owl
- user defined swatch pallete
- glyph widget ༼ つ ◕_◕ ༽つ
- more primitive shapes...
- ellipse
- parallelogram
- multi-document support, in particular, part of
GoatStatewill be separated out into[GoatTab] - multiplayer support
- we will use a trusted/permissioned decentralized consensus algorithm for this
- refactor
OwlTreeto be a tree andOwlPartialmentto be a zipper rather than a list of ids such that we can remove most partial functions in Owl.hsOwlTreestill needs to support id lookup as some things will continue to use id references- attachments
- over the wire changes (when multiplayer support is added)
- see ::FUTURE FEATURES:: in TODO.txt for even more stuff :D
- why are things randomly named after animals like 🐐🦙🦉?
- I was having a hard time coming up with unambiguous terminology following more conventional naming patterns. You can see use more conventional naming in some places.
- I like animals
- why are so many things prefixed with potato
- some animals eat potatoes 🥔