Files
prosperon/clay2.cm
2026-01-08 09:10:37 -06:00

354 lines
9.1 KiB
Plaintext

// clay2.cm - Revised UI layout engine emitting flat drawables
//
// Changes from clay.cm:
// - No __proto__, uses meme/merge
// - Emits flat list of drawables for film2d
// - Supports scissor clipping
//
// Now returns [drawable, drawable, ...] instead of {type:'group', ...}
var layout = use('layout')
var graphics = use('graphics')
var clay = {}
// 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) {
fallback = fallback || {r:1, g:1, b:1, a:1}
if (!c) return {r:fallback.r, g:fallback.g, b:fallback.b, a:fallback.a}
return {
r: c.r != null ? c.r : fallback.r,
g: c.g != null ? c.g : fallback.g,
b: c.b != null ? c.b : fallback.b,
a: c.a != null ? c.a : fallback.a
}
}
function normalize_spacing(s) {
if (is_number(s)) return {l:s, r:s, t:s, b:s}
if (is_array(s)) {
if (s.length == 2) return {l:s[0], r:s[0], t:s[1], b:s[1]}
if (s.length == 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
var tree_root
var config_stack = []
// Rewriting state management for cleaner recursion
var tree_stack = []
clay.layout = function(fn, size) {
lay_ctx.reset()
var root_id = lay_ctx.item()
lay_ctx.set_size(root_id, size)
lay_ctx.set_contain(root_id, layout.contain.row)
var root_node = {
id: root_id,
config: meme(base_config, {size: size}),
children: []
}
tree_stack = [root_node]
fn() // User builds tree
lay_ctx.run()
// Post-layout: build flat drawable list
return build_drawables(root_node, size.height)
}
function build_drawables(node, root_height, parent_abs_x, parent_abs_y, parent_scissor, parent_layer) {
parent_abs_x = parent_abs_x || 0
parent_abs_y = parent_abs_y || 0
parent_layer = parent_layer || 0 // UI usually on top, but let's start at 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
if (node.config.clipped) {
var sx = vis_x
var sy = vis_y
var sw = rect.width
var sh = rect.height
// Intersect with parent
if (parent_scissor) {
sx = number.max(sx, parent_scissor.x)
sy = number.max(sy, parent_scissor.y)
var right = number.min(vis_x + sw, parent_scissor.x + parent_scissor.width)
var bottom = number.min(vis_y + sh, parent_scissor.y + parent_scissor.height)
sw = number.max(0, right - sx)
sh = number.max(0, bottom - sy)
}
current_scissor = {x: sx, y: sy, width: sw, height: sh}
}
// Background
if (node.config.background_image) {
if (node.config.slice) {
drawables.push({
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: parent_layer - 0.1, // slightly behind content
scissor: current_scissor
})
} else {
drawables.push({
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: parent_layer - 0.1,
scissor: current_scissor
})
}
} else if (node.config.background_color) {
drawables.push({
type: 'rect',
pos: {x: vis_x, y: vis_y},
width: rect.width,
height: rect.height,
color: node.config.background_color,
layer: parent_layer - 0.1,
scissor: current_scissor
})
}
// Content (Image/Text)
if (node.config.image) {
drawables.push({
type: 'sprite',
image: node.config.image,
pos: {x: vis_x, y: vis_y},
width: rect.width,
height: rect.height,
color: node.config.color,
layer: parent_layer,
scissor: current_scissor
})
}
if (node.config.text) {
drawables.push({
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}, // Baseline adjustment
anchor_y: 1.0, // Text usually draws from baseline up or top down?
// film2d text uses top-left by default unless anchor set.
// Original clay put it at `y + rect.height`.
// Let's assume origin top-left, so we might need anchor adjustment or just position.
// If frame is top-down (0 at top), `abs_y` is top.
// `rect.y` in layout is bottom-up? "rect.y is from bottom" says original comment.
// `abs_y = root_height - (rect.y + rect.height)` -> Top edge of element.
// Text usually wants baseline.
// If we put it at `vis_y + rect.height`, that's bottom of element.
layer: parent_layer,
scissor: current_scissor
})
}
// Children
for (var child of node.children) {
var child_drawables = build_drawables(child, root_height, vis_x, vis_y, current_scissor, parent_layer + 0.01)
for (var i = 0; i < child_drawables.length; i++) {
drawables.push(child_drawables[i])
}
}
return drawables
}
// --- Item Creation Helpers ---
function process_configs(configs) {
var cfg = meme(base_config, ...configs)
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)
if (config.size) {
var s = config.size
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[tree_stack.length-1]
parent.children.push(node)
lay_ctx.insert(parent.id, item)
tree_stack.push(node)
return node
}
function pop_node() {
tree_stack.pop()
}
// Generic container
clay.container = function(configs, fn) {
if (is_function(configs)) { fn = configs; configs = {} }
if (!is_array(configs)) configs = [configs]
push_node(configs, null)
if (fn) fn()
pop_node()
}
// Stacks
clay.vstack = function(configs, fn) {
if (is_function(configs)) { fn = configs; configs = {} }
if (!is_array(configs)) configs = [configs]
var c = layout.contain.column
push_node(configs, c)
if (fn) fn()
pop_node()
}
clay.hstack = function(configs, fn) {
if (is_function(configs)) { fn = configs; configs = {} }
if (!is_array(configs)) configs = [configs]
var c = layout.contain.row
push_node(configs, c)
if (fn) fn()
pop_node()
}
clay.zstack = function(configs, fn) {
if (is_function(configs)) { fn = configs; configs = {} }
if (!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) {
var img = graphics.texture(path)
var c = {image: path}
var final_config = process_configs(configs)
if (!final_config.size && !final_config.behave) {
c.size = {width: img.width, height: img.height}
}
push_node([c, ...configs], null)
pop_node()
}
clay.text = function(str, ...configs) {
var c = {text: str}
var final_config = process_configs(configs)
if (!final_config.size && !final_config.behave) {
c.size = {width: 100, height: 20}
}
push_node([c, ...configs], null)
pop_node()
}
clay.rectangle = function(...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}
}
clay.zstack([btn_config, ...configs], function() {
clay.text(str, {color: {r:1,g:1,b:1,a:1}})
})
}
// Constants
clay.behave = layout.behave
clay.contain = layout.contain
return clay