Files
prosperon/clay.cm
2026-02-26 15:54:11 -06:00

441 lines
11 KiB
Plaintext

// clay.cm - UI layout engine emitting flat drawables with annotated tree
//
// - Uses meme/merge for config chains
// - Emits flat list of drawables for film2d
// - Supports scissor clipping
// - Returns annotated tree root with .drawables for clay_input compatibility
var layout = use('layout')
var graphics = use('graphics')
var clay = {}
// Unique key objects for tree traversal (used by clay_input)
var CHILDREN = {}
var PARENT = {}
clay.CHILDREN = CHILDREN
clay.PARENT = PARENT
// Layout context
var lay_ctx = layout.make_context()
// Base configuration for UI elements
var base_config = {
font: null,
background_image: null,
slice: 0,
font_path: 'fonts/dos', // Default font
font_size: 16,
color: {r:1, g:1, b:1, a:1},
spacing: 0,
padding: 0,
margin: 0,
offset: {x:0, y:0},
size: null,
background_color: null,
clipped: false,
text_break: 'word',
text_align: 'left',
max_size: null,
contain: 0,
behave: 0
}
function normalize_color(c, fallback) {
var fb = fallback || {r:1, g:1, b:1, a:1}
if (!c) return {r:fb.r, g:fb.g, b:fb.b, a:fb.a}
return {
r: c.r != null ? c.r : fb.r,
g: c.g != null ? c.g : fb.g,
b: c.b != null ? c.b : fb.b,
a: c.a != null ? c.a : fb.a
}
}
function normalize_spacing(s) {
if (is_number(s)) return {l:s, r:s, t:s, b:s}
if (is_array(s)) {
if (length(s) == 2) return {l:s[0], r:s[0], t:s[1], b:s[1]}
if (length(s) == 4) return {l:s[0], r:s[1], t:s[2], b:s[3]}
}
if (is_object(s)) return {l:s.l||0, r:s.r||0, t:s.t||0, b:s.b||0}
return {l:0, r:0, t:0, b:0}
}
// Tree building state
var root_item = null
var tree_root = null
var config_stack = []
// Rewriting state management for cleaner recursion
var tree_stack = []
var _next_id = 0
function annotate_tree(node, root_height, parent_node) {
var rect = lay_ctx.get_rect(node.id)
node.boundingbox = {
x: rect.x,
y: root_height - (rect.y + rect.height),
width: rect.width,
height: rect.height
}
node[CHILDREN] = node.children
node[PARENT] = parent_node
arrfor(node.children, function(child) {
annotate_tree(child, root_height, node)
})
}
clay.layout = function(fn, size) {
var sz = is_array(size) ? {width: size[0], height: size[1]} : size
lay_ctx.reset()
_next_id = 0
var root_id = lay_ctx.item()
lay_ctx.set_size(root_id, sz)
lay_ctx.set_contain(root_id, layout.contain.row)
var root_node = {
id: root_id,
config: meme(base_config, {size: sz}),
children: []
}
tree_stack = [root_node]
fn() // User builds tree
lay_ctx.run()
// Annotate tree for clay_input (boundingbox, CHILDREN, PARENT)
annotate_tree(root_node, sz.height, null)
// Build flat drawable list and attach to tree root
root_node.drawables = build_drawables(root_node, sz.height)
return root_node
}
function build_drawables(node, root_height, parent, parent_scissor) {
var p_abs_x = (parent && parent.x) || 0
var p_abs_y = (parent && parent.y) || 0
var p_layer = (parent && parent.layer) || 0
var rect = lay_ctx.get_rect(node.id)
// Calculate absolute world Y for this node (bottom-up layout to top-down render)
var abs_y = root_height - (rect.y + rect.height)
var abs_x = rect.x
// IMPORTANT: The offset in config is applied VISUALLY.
var vis_x = abs_x + node.config.offset.x
var vis_y = abs_y + node.config.offset.y
var drawables = []
// Scissor
var current_scissor = parent_scissor
var sx = vis_x
var sy = vis_y
var sw = rect.width
var sh = rect.height
var clip_right = null
var clip_bottom = null
if (node.config.clipped) {
// Intersect with parent
if (parent_scissor) {
sx = max(sx, parent_scissor.x)
sy = max(sy, parent_scissor.y)
clip_right = min(vis_x + sw, parent_scissor.x + parent_scissor.width)
clip_bottom = min(vis_y + sh, parent_scissor.y + parent_scissor.height)
sw = max(0, clip_right - sx)
sh = max(0, clip_bottom - sy)
}
current_scissor = {x: sx, y: sy, width: sw, height: sh}
}
// Background
if (node.config.background_image) {
_next_id = _next_id + 1
if (node.config.slice) {
drawables[] = {
_id: _next_id,
type: 'sprite',
image: node.config.background_image,
pos: {x: vis_x, y: vis_y},
width: rect.width,
height: rect.height,
slice: node.config.slice,
color: node.config.background_color || {r:1, g:1, b:1, a:1},
layer: p_layer - 0.1,
scissor: current_scissor
}
} else {
drawables[] = {
_id: _next_id,
type: 'sprite',
image: node.config.background_image,
pos: {x: vis_x, y: vis_y},
width: rect.width,
height: rect.height,
color: node.config.background_color || {r:1, g:1, b:1, a:1},
layer: p_layer - 0.1,
scissor: current_scissor
}
}
} else if (node.config.background_color) {
_next_id = _next_id + 1
drawables[] = {
_id: _next_id,
type: 'rect',
pos: {x: vis_x, y: vis_y},
width: rect.width,
height: rect.height,
color: node.config.background_color,
layer: p_layer - 0.1,
scissor: current_scissor
}
}
// Content (Image/Text)
if (node.config.image) {
_next_id = _next_id + 1
drawables[] = {
_id: _next_id,
type: 'sprite',
image: node.config.image,
pos: {x: vis_x, y: vis_y},
width: rect.width,
height: rect.height,
color: node.config.color,
layer: p_layer,
scissor: current_scissor
}
}
if (node.config.text) {
_next_id = _next_id + 1
drawables[] = {
_id: _next_id,
type: 'text',
text: node.config.text,
font: node.config.font_path,
size: node.config.font_size,
color: node.config.color,
pos: {x: vis_x, y: vis_y + rect.height},
anchor_y: 1.0,
layer: p_layer,
scissor: current_scissor
}
}
// Children
arrfor(node.children, function(child) {
drawables = array(drawables, build_drawables(child, root_height, {x: vis_x, y: vis_y, layer: p_layer + 0.01}, current_scissor))
})
return drawables
}
// --- Item Creation Helpers ---
function process_configs(configs) {
var cfg = meme(base_config, configs)
// Parse shorthand font string (e.g. 'blackcastle.64') into font_path and font_size
var font_parts = null
var parsed_size = null
if (cfg.font && is_text(cfg.font)) {
font_parts = array(cfg.font, '.')
if (length(font_parts) >= 2) {
parsed_size = number(font_parts[length(font_parts) - 1])
if (parsed_size) {
cfg.font_size = parsed_size
cfg.font_path = font_parts[0]
}
}
}
cfg.color = normalize_color(cfg.color, base_config.color)
if (cfg.background_color) cfg.background_color = normalize_color(cfg.background_color, {r:1,g:1,b:1,a:1})
if (!cfg.offset) cfg.offset = {x:0, y:0}
else cfg.offset = {x: cfg.offset.x || 0, y: cfg.offset.y || 0}
return cfg
}
function push_node(configs, contain_mode) {
var config = process_configs(configs)
if (contain_mode != null) config.contain = contain_mode
var item = lay_ctx.item()
// Apply layout props
lay_ctx.set_margins(item, normalize_spacing(config.margin))
lay_ctx.set_contain(item, config.contain)
lay_ctx.set_behave(item, config.behave)
var s = config.size
if (s) {
if (is_array(s)) s = {width: s[0], height: s[1]}
lay_ctx.set_size(item, s)
}
var node = {
id: item,
config: config,
children: []
}
// Add to parent
var parent = tree_stack[length(tree_stack)-1]
parent.children[] = node
lay_ctx.insert(parent.id, item)
tree_stack[] = node
return node
}
function pop_node() {
tree_stack[]
}
// Generic container
clay.container = function(configs, fn) {
var _fn = is_function(configs) ? configs : fn
var _configs = is_function(configs) ? [{}] : (is_array(configs) ? configs : [configs])
push_node(_configs, null)
if (_fn) _fn()
pop_node()
}
// Stacks
clay.vstack = function(configs, fn) {
var _fn = is_function(configs) ? configs : fn
var _configs = is_function(configs) ? [{}] : (is_array(configs) ? configs : [configs])
var c = layout.contain.column
push_node(_configs, c)
if (_fn) _fn()
pop_node()
}
clay.hstack = function(configs, fn) {
var _fn = is_function(configs) ? configs : fn
var _configs = is_function(configs) ? [{}] : (is_array(configs) ? configs : [configs])
var c = layout.contain.row
push_node(_configs, c)
if (_fn) _fn()
pop_node()
}
clay.zstack = function(configs, fn) {
var _fn = is_function(configs) ? configs : fn
var _configs = is_function(configs) ? [{}] : (is_array(configs) ? configs : [configs])
var c = layout.contain.layout
push_node(_configs, c)
if (_fn) _fn()
pop_node()
}
// Leaf nodes
clay.image = function(path, configs, extra) {
var img = graphics.texture(path)
var c = [{image: path}]
var _configs = configs ? (is_array(configs) ? configs : [configs]) : []
if (extra) _configs = array(_configs, is_array(extra) ? extra : [extra])
var final_config = process_configs(_configs)
if (!final_config.size && !final_config.behave)
c[] = {size: {width: img.width, height: img.height}}
push_node(array(c, _configs), null)
pop_node()
}
clay.text = function(str, configs, extra) {
var c = [{text: str}]
var _configs = configs ? (is_array(configs) ? configs : [configs]) : []
if (extra) _configs = array(_configs, is_array(extra) ? extra : [extra])
var final_config = process_configs(_configs)
if (!final_config.size && !final_config.behave) {
c[] = {size: {width: 100, height: 20}}
}
push_node(array(c, _configs), null)
pop_node()
}
clay.rectangle = function(configs) {
var _configs = configs ? (is_array(configs) ? configs : [configs]) : [{}]
push_node(_configs, null)
pop_node()
}
clay.button = function(str, action, configs) {
var btn_config = [{
padding: 10,
background_color: {r:0.3, g:0.3, b:0.4, a:1}
}]
var _configs = is_array(configs) ? configs : [configs]
var merged = process_configs(_configs)
clay.zstack(array(btn_config, _configs), function() {
clay.text(str, {color: {r:1,g:1,b:1,a:1}, font_path: merged.font_path})
})
}
// Spacer — fills available space
clay.spacer = function(config) {
var cfg = config || {}
if (!cfg.behave) cfg.behave = layout.behave.hfill | layout.behave.vfill
push_node([cfg], null)
pop_node()
}
// Convenience draw wrapper — auto-detects call patterns:
// clay.draw(fn) — default 640x360
// clay.draw(fn, size_obj) — fn first, size object second
// clay.draw(size_array, fn) — size array first, fn second
clay.draw = function(arg1, arg2) {
var fn = null
var size = {width: 640, height: 360}
if (is_function(arg1)) {
fn = arg1
if (arg2) {
if (is_array(arg2)) size = {width: arg2[0], height: arg2[1]}
else if (is_object(arg2)) size = arg2
}
} else if (is_array(arg1)) {
size = {width: arg1[0], height: arg1[1]}
fn = arg2
}
return clay.layout(fn, size)
}
// Offset all drawables in an array by a position
clay.offset_drawables = function(drawables, offset) {
var result = []
var i = 0
var d = null
for (i = 0; i < length(drawables); i = i + 1) {
d = meme(drawables[i])
d.pos = {x: d.pos.x + offset.x, y: d.pos.y + offset.y}
result[] = d
}
return result
}
// Constants
clay.behave = layout.behave
clay.contain = layout.contain
return clay