Application Anatomy
In our Counter application, we can see that the application is simply a class that inherits from Hokusai::Block.
In fact, there is no difference between a component (block) and an application in hokusai. This composability means that you could put a functioning terminal emulator inside a photoshop clone, next a spreadsheet program.
State
Since blocks in Hokusai are plain ruby objects, you can manage state with them.
For the counter, we want to keep track of the current count. An easy way to do this is to put state in the initializer.
class Counter < Hokusai::Block
#....
attr_accessor :count
def initialize(**args)
@count = 0
super
endWe could also manage this state in other ways as well
attr_accessor :count
def count = @count ||= 0Or use a lifecycle hook
attr_accessor :count
def on_mounted = @count = 0Templates
Templates are a bit more complicated. Internally, Hokusai is basically a machine that continuously generates an ordered list of things to draw in a window.
If we want to draw a red circle in the middle of blue square, the instructions might look like this.
- Draw a blue rectangle at the top left position (0, 0) and bottom right at (100, 100)
- Draw at red circle at position (50, 50) with a radius of 20
Of course, if we drew the red circle first, the blue square would be drawn on top, so we'd never see it.
A Hokusai template declares a tree of blocks. More specifically for this example, the template declares a tree of words that map to blocks. The tree is processed depth-first, and each time it is processed, it creates an ordered list of draw commands. When rendering, each block in the tree is given a recommended section of the window to draw in. The recommended sections are based on some basic layout rules.
For an idea of the rules, imagine a whitespace significant string template.
[template]
vblock
hblock
first
vblock
second
third
vblock
fourth
fifthLet's say hblock creates a container where children are positioned horizontally, and vblock creates a container where children are positioned vertically
Let's imagine that the default behavior is to:
- render the whole document in the window regardless if it fits.
- clip any excess content
- divide all space equally between children.
So for the document above, it would look something like:
|----------------|-----------------| | | | | | second | | | | | first |-----------------| | | | | | third | | | | |----------------------------------| | | | fourth | | | |----------------------------------| | | | fifth | | | |----------------------------------|
It becomes quite simple to compute the expected position of each child, as the parent already knows the verticality and dimensions of its children.
For instance, imagine adding a width to the first child, and trimming some height from the hblock
[template]
root
hblock {height="20"}
first {width="20"}
vblock
second
third
vblock
fourth
fifthThe resulting children can flex around this, resulting in something like
|------------|---------------------| | | second | | first |---------------------| | | third | |----------------------------------| | | | | | fourth | | | | | |----------------------------------| | | | | | fifth | | | | | |----------------------------------|
Earlier I mentioned the template declares a tree of words that map to blocks. In order to know which words map to which blocks in a string template, Hokusai provides a uses class method.
# ...
uses(
vblock: Hokusai::Blocks::Vblock,
hblock: Hokusai::Blocks::Hblock,
label: Hokusai::Blocks::Text,
)
# ...Reserved Template keywords
There are a couple of reserved string template keywords that should not be used with arbitrary blocks.
| name | purpose |
|---|---|
virtual | meant for marking a template as a no-op, useful for blocks that render themselves using the drawing api |
slot | meant for declaring a child as a slot. Slots allow one to compose reusable blocks that can have different children or behaviors |
Drawing API
Blocks don't always need to use templates. Hokusai provides an API generating draw commands directly.
Let's say we want to make a block that renders a circle without using any built-in blocks / templates.
class Circle < Hokusai::Block
template <<~EOF
[template]
virtual
EOF
# Render takes a `Hokusai::Canvas`, processes any draw commands,
# and yields the canvas to the next block.
def render(canvas)
x = canvas.x + (canvas.width / 2)
y = canvas.y + (canvas.height / 2)
radius = 5.0
draw do
circle(x, y, radius) do |command|
command.color = Hokusai::Color.new(255, 0, 0)
end
end
yield canvas
end
endOverloading the render method will allow the block to recieve a Hokusai::Canvas which contains the suggested coordinates where that block should draw.
Once inside, we calculate the center of the canvas using it's coordinates.
Then we open the drawing api by calling #draw, which exposes a DSL for drawing primitives, like a circle.
Now our Circle block can be used from another blocks template. In short, this block:
- Declares its template as virtual
- Declares a render method
- Calls the draw method inside render and invokes methods for drawing
A full list of commands can be found here.
Composability
You may have noticed from Counter that the template shows names nested inside each other.
This is because those blocks are slotted. A slot in Hokusai is placeholder for blocks provided by a parent which is not known. The blocks that fill the placeholder will be rendered as the children of the block declaring the slot, but these blocks will still receive props from and emit events to their original parent.
To illustrate, consider the template string for Hokusai::Blocks::Panel.
class Hokusai::Blocks::Panel < Hokusai::Block
template <<~EOF
[template]
hblock {
:background="background"
@wheel="wheel_handle"
}
clipped { :auto="autoclip" :offset="offset" }
dynamic { @size_updated="set_size" }
slot
[if="scroll_active"]
scrollbar.scroller {
@scroll="scroll_complete"
:top="panel_top"
:goto="scrollbar_goto"
:width="scroll_width"
:background="scroll_background"
:control_color="scroll_color"
:control_height="scroll_control_height"
}
EOF
uses(
clipped: Hokusai::Blocks::Clipped,
dynamic: Hokusai::Blocks::Dynamic,
hblock: Hokusai::Blocks::Hblock,
scrollbar: Hokusai::Blocks::Scrollbar
)
#...
endThe slot keyword in this template is a placeholder for any content that you want to be scrollable in a desktop app. There really isn't any built-in magic happening here. The scrollbar, clipping region, and dynamic sizing are all just plain Hokusai::Block which each call different commands.
With virtual, slot, and the drawing API, you can invent all of your own components. You don't really need to use the provided blocks if they don't fit your use case.