puts
One possible introduction to hokusai-pocket
Porkchop Casserole
The year is 2026. It’s Wednesday night, and I’m digesting ChatGPT porkchop casserole. Nobody is riding hoverboards, and nobody is asking about a hot new binary named hokusai-pocket
It is not exactly in my nature to give an unsolicited introductions to software that isn’t in any kind of marketable demand, but by the same coin, I perform many “unnatural” behaviors. Like working under the alienating conditions of late stage capitalism, or eating ChatGPT porkchop casserole.
By now in my life I know this for certain.
A solution born from a different intention will yield a different outcome, and be subject to different constraints.
Hokusai Pocket was made to make it easier and more enjoyable to approach complex graphical computer applications. It comes from a personal investment in an easier and more enjoyable type of future.
hokusai pocket is a tool for writing GUIs
At the core, Hokusai Pocket is a Ruby library for writing Graphical User Interfaces (GUI). A GUI is program that collects input (A mouse click here, keypress there, maybe a tap or two), and typically renders graphics in a window in response to that input.
Examples include:
- Minesweeper
- iTunes
- Photoshop
- Excel
If you are reading this, you are almost certainly using a GUI! (browser)
There are many approaches to GUI libraries. Hokusai Pocket aims to make writing GUI programs simple and fun. It provides an Ruby interface for writing stateful components that listen for input (events) and draw graphics in response.
Let’s take a look at a basic counter application.
Open a new file named counter.rb and paste the following code.
# counter.rb
class Counter < Hokusai::Block
template <<-EOF
[template]
hblock
label {
size="190"
:content="count.to_s"
:color="count_color"
}
hblock
vblock { @click="increment" :background="BLUE"}
label { content="Add" }
vblock { @click="decrement":background="RED" }
label { content="Subtract" }
EOF
uses(
vblock: Hokusai::Blocks::Vblock,
hblock: Hokusai::Blocks::Hblock,
label: Hokusai::Blocks::Text,
)
RED = [244, 0, 0]
BLUE = [0, 0, 244]
attr_accessor :count
def count_positive = count > 0
def increment(event) = self.count += 1
def decrement(event) = self.count -= 1
def count_color = count.negative? ? RED : BLUE
def initialize(**args)
@count = 0
super
end
end
Hokusai::Backend.run(Counter) do |config|
config.title = "Counter" # title
config.width = 550
config.height = 500
config.after_load do
Hokusai.fonts.register "default", Hokusai::Backend::Font.default
Hokusai.fonts.activate "default"
end
end
This code defines a Counter component, and passes it to the MRuby/Raylib backend.
Let’s try it out, but first we’ll need to download the hokusai-pocket binary.
The binary is available in a few ways.
- Download from the latest release
- Download from the latest test build
- Build from source
Download your choice and put the program on your path somewhere.
This all brings me to my next point.
hokusai-pocket is a tool for running GUIs
Aside from being a library that provides Ruby code to author GUI applications, it is also a binary to run those same applications in a portable way.
The GUI program should look the same no matter what platform it runs on.
To run the counter.rb program we just saved, we’ll use the run command from the same directory.
hokusai-pocket run:target=counter.rb
If everything went as planned, you should see a window that looks like this.
How easy! We just passed the file containing our ruby code to hokusai-pocket and got an interactive counter application.
The Anatomy of Counter
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
end
We could also manage this state in other ways as well
attr_accessor :count
def count = @count ||= 0
Or use a lifecycle hook
attr_accessor :count
def on_mounted = @count = 0
Templates
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
fifth
Let’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
fifth
The 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 |
The 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
end
Overloading 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 the source code.
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
)
#...
end
The 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.
Props
One way to make our Circle component more useful, is to allow its consumer to decide what color and radius the circle should be.
We can do this with props.
Props can be either static (received as a string) or dynamic (received as a ruby object), and are declared as such.
Let’s make our Circle more extensible with props and use it in another component
# circle.rb
class Circle < Hokusai::Block
template <<~EOF
[template]
virtual
EOF
# the computed class method tells us that we expect a prop
# and auto-generates an instance method for it.
computed :color, default: [255, 0, 0], convert: Hokusai::Color
computed :radius, default: 10.0, convert: proc(&:to_f)
def render(canvas)
x = canvas.x + (canvas.width / 2)
y = canvas.y + (canvas.height / 2)
draw do
# we can call `color` and `radius` directly now
circle(x, y, radius) do |command|
command.color = color
end
end
yield canvas
end
end
The Hokusai::Block::computed method allows us to declare expected props as well as change their type. These props can then be used directly in any of the block’s instance methods.
require_relative "./circle"
class ColoredCircles < Hokusai::Block
template <<~EOF
[template]
blue_circle { color="0,0,255" }
red_circle { :color="get_red_color" }
EOF
uses(
blue_circle: Circle,
red_circle: Circle
)
def get_red_color
[255, 0, 0]
end
end
The above component draws a blue circle above a red circle.
- static prop content (without a colon) are passed to the child as a string,
- dynamic prop content (starting with a colon) is evaluated in the context of the instance.
Events
Hokusai supports both builtin and custom events.
Event attributes are similar to prop declarations except begin with an @ symbol.
The value of the event attribute should be the name of the method that will receive the event.
Hokusai supports basic input events out of the box. the handlers attached to these events will receive the following objects as their parameter.
| Event | Object |
|---|---|
| @click | Hokusai::ClickEvent |
| @hover | Hokusai::HoverEvent |
| @mousemove | Hokusai::MouseMoveEvent |
| @mouseup | Hokusai::MouseUpEvent |
| @mousedown | Hokusai::MouseDownEvent |
| @mouseout | Hokusai::MouseOutEvent |
| @wheel | Hokusai::WheelEvent |
| @keypress | Hokusai::KeyPressEvent |
| @keyup | Hokusai::KeyUpEvent |
When the backend is configured to use touch, these events will also be exposed the application.
| Touch Event | Object |
|---|---|
| @tap | Hokusai::TapEvent |
| @doubletap | Hokusai::DoubleTapEvent |
| @taprelease | Hokusai::TapReleaseEvent |
| @drag | Hokusasi::DragEvent |
| @taphold | Hokusai::TapHoldEvent |
| @pinchin | Hokusai::PinchInEvent |
| @pinchout | Hokusai::PinchOutEvent |
| @swipe | Hokusai::SwipeEvent |
Now that our Circle block uses props, we can make it change colors on each click.
class ColorChangingCircle < Hokusai::Block
template <<~EOF
[template]
circle { :color="circle_color" @click="change_color" }
EOF
uses(circle: Circle)
def circle_color = @circle_color || [255,255,255]
def change_color(event) = @circle_color = [[255,0,0], [0,255,0], [0,0,255]].sample
end
In the above component, we pass a dynamic color prop based on the value of @circle_color
In the @click handler, and we set @circle_color to a random value of red, blue, or green.
This new value will be passed to the child on the next render.
Emits
Blocks can also emit their own events with Hokusai::Block#emit, the first argument to emit is the name of the event,
and the rest of the arguments are passed to any subscribed event handlers.
We can use it from our block like this.
class ColorChangingCircleWithCustomEmit < Hokusai::Block
template <<~EOF
[template]
circle { :color="circle_color" @click="change_color" }
EOF
uses(circle: Circle)
def circle_color = @circle_color || [255,255,255]
def change_color(event)
@circle_color = [[255,0,0], [0,255,0], [0,0,255]].sample
emit("color_changed", @circle_color)
end
end
Provisions
Provisions are probably the most useful idea in Hokusai Pocket.
A block can declare a provided method that can be injected into any descendant. Like a scoped global, a provision is useful for providing state to groups of related components and avoid drilling props.
The first argument to Hokusai::Block::provide is the name of the provision, the second points to an instance method on the block that returns the provided state. In child components, call
Hokusai::Block::inject with the provision name to make it available to that block.
A common idiom I use in more complex apps is to provide a control object at the root which maintains mutable state for the whole application, and inject it to components as needed.
Conclusion
There’s a lot of stuff I haven’t covered, but also, I am not perfect, and this work isn’t perfect either. There are quirks and annoyances (as with any library), but for a category of intention, I hope that it operates in an immensely fun and productive way.
But I do want to discuss a few footguns
- Sometimes the build breaks for windows, and the binary is missing winpthreads. It should be statically linked.
- Releases are somewhat sporadic. If there are specific needs then they will become more predictable. I use long running feature branches, so it’s worth checking out the CI builds over the releases.
- There are directives for conditionals and looping. Please ask me about them or look at projects that use them.
- You can create templates using a Ruby DSL instead of a string, but they don’t have feature parity. Sometimes it is more advantageous to use one or the other.
- For things that require a blocks height to be figured out at runtime, (such as wrapping text), the parent currently has no mechanic to figure this out. The height must be set on the node or emitted to the parent.
Projects that are currently using this library
Please enjoy, and if you can’t - I’d love to know.
— z