Skip to content

Instantly share code, notes, and snippets.

@skinnyjames
Last active December 14, 2024 17:50
Show Gist options
  • Save skinnyjames/c8388ccbbd60c53c0d86df258a7924cb to your computer and use it in GitHub Desktop.
Save skinnyjames/c8388ccbbd60c53c0d86df258a7924cb to your computer and use it in GitHub Desktop.
# frozen_string_literal: true
# dependencies
require_relative "../src/hokusai"
require_relative "../src/hokusai/backends/sdl2"
require_relative "../src/hokusai/backends/raylib"
require_relative "./stock"
require_relative "./tic_tac_toe"
# can use ruby stdlib
# or any library in the ruby ecosystem
require "json"
require "net/http"
module Demo
# Block that represents a singular post
#
# Displays post content, and throws in a game of Tic Tac Toe for good measure.
#
# Takes up full width on small viewport, centers on large viewport
# Duplication in the template can easily be extracted into a separate block
class Post < Hokusai::Block
template <<~EOF
[template]
vblock
hblock
vblock.about { width="80" :background="about_background" }
image {
width="80"
height="80"
:source="about_image_source"
}
label { :content="post_author_name" size="9" }
vblock.content
label { :content="post_title" size="20" }
text {
:content="post_body"
:padding="text_padding"
size="17
@height_updated="text_height_updated"
}
tic_tac_toe { height="400" width="400" }
EOF
# Mandatory props
computed! :entry
computed! :index
# injects the screen type from the provisioned value
# and aliases it to :media_type
inject :screen_type, :media_type
uses(
hblock: Hokusai::Blocks::Hblock,
vblock: Hokusai::Blocks::Vblock,
image: Hokusai::Blocks::Image,
text: Hokusai::Blocks::Text,
label: Hokusai::Blocks::Label,
empty: Hokusai::Blocks::Empty,
tic_tac_toe: TicTacToe::App
)
# This block is rendered inside a scrollable panel
# in between a `clip begin` and `clip end` command
#
# `Hokusai.can_render(canvas) will tell us if this block will even be visible`
# If not visible, I won't waste memory / cpu to render
def render(canvas)
if Hokusai.can_render(canvas)
yield(canvas)
end
end
def post_index
index.to_s
end
def post_body
entry.content
end
def post_title
@post_title ||= "#{entry.title.upcase} - #{DateTime.now.strftime("%m/%d/%Y %H:%M %p")}"
end
def post_author_name
@post_author_name ||= entry.id.even? ? "Adeline" : "Skinnyjames"
end
def about_image_source
@about_image_source ||= entry.id.even? ? "#{__dir__}/assets/addy.png" : "#{__dir__}/assets/baby_sean.png"
end
# Cache the color so that it isn't spinning up new objects at `O(n) complexity`
#
# TODO: extract styling / prop declaration into separate template
def about_background
@author_background ||= Hokusai::Color.new(155,155,155,20)
end
# Cache the text padding
def text_padding
@text_padding ||= Hokusai::Padding.new(20, 5, 20, 5)
end
# `height_updated` handler for the text node
#
# Since panels are just blocks, and have no insight to the height of their children
# I will update the height of this post dynamically based on the height of the rendered text
def text_height_updated(height)
# tic tac toe height + about node height || content height + padding
@height = 400 + [80, height].max + 40
end
def on_mounted
text_height_updated(0)
end
# Lifecycle hook
#
# `height` and `width` are the only special props
# they are used by the layout rendered to compute the canvas
#
# We will set it after the block is updated on each iteration of the event loop
def after_updated
node.meta.set_prop(:height, @height) if @height != node.meta.get_prop(:height)
end
end
# Plain old ruby class
PostEntry = Struct.new(:id, :title, :content)
# Entrypoint Block
# Nothing is special about this block - any block can be used as an entrypoint
#
# NOTE: Scrollbars, panels, and other basic functions are also plain blocks
class App < Hokusai::Block
template <<~EOF
[template]
vblock { background="246,255,195"}
stock { height="150" }
panel { scroll_color="255,246,116" scroll_background="0,0,0,30"}
[for="post in posts"]
post {
:key="key(post)"
:index="index"
:entry="post"
}
EOF
# keys map to template node names
# values map to blocks
uses(
hblock: Hokusai::Blocks::Hblock,
vblock: Hokusai::Blocks::Vblock,
label: Hokusai::Blocks::Label,
panel: Hokusai::Blocks::Panel,
text: Hokusai::Blocks::Text,
stock: StockDecider::App, # some app to chart stock prices for a friend
post: Post,
)
# methods can be accessed in computed props
attr_accessor :posts
# initializer override
def initialize(**args)
@posts = []
super
end
# loop state can be passed to methods
def key(entry)
entry.id
end
# lifecycle hook
# `on_mounted`
# `before_updated`
# `after_updated`
# `on_destroy`
def on_mounted
uri = URI("https://jsonplaceholder.typicode.com/posts")
res = JSON.parse(Net::HTTP.get(uri), symbolize_names: true)
self.posts = res.map { |json| PostEntry.new(json[:id], json[:title], json[:body]) }.freeze
# can access details about this block
#
# get the node count
puts node.meta.node_count
# show the ast
puts dump
end
end
end
# Backends include Raylib and SDL2
Hokusai::Backends::RaylibBackend.run(Demo::App) do |config|
# backend specific configuration
config.width = 600
config.height = 500
config.title = "Demo Application"
config.after_load do
# most heavy logic, including text wrapping calculations are implemented in C
font = Hokusai::Backends::RaylibBackend::Font.from("#{__dir__}/assets/OpenSans-Regular.ttf")
Hokusai.fonts.register "opensans", font
Hokusai.fonts.activate "opensans"
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment