416 lines
10 KiB
Plaintext
416 lines
10 KiB
Plaintext
// clay2.cm - Revised UI layout engine emitting scene trees
|
|
//
|
|
// Changes from clay.cm:
|
|
// - No __proto__, uses meme/merge
|
|
// - Emits scene tree nodes for fx_graph instead of immediate draw commands
|
|
// - Supports scissor clipping on groups
|
|
|
|
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_spacing(s) {
|
|
if (typeof s == 'number') return {l:s, r:s, t:s, b:s}
|
|
if (isa(s, array)) {
|
|
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 (typeof s == 'object') 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 = []
|
|
|
|
clay.layout = function(fn, size) {
|
|
lay_ctx.reset()
|
|
|
|
var root = lay_ctx.item()
|
|
if (isa(size, array)) size = {width: size[0], height: size[1]}
|
|
|
|
lay_ctx.set_size(root, size)
|
|
lay_ctx.set_contain(root, layout.contain.row) // Default root layout
|
|
|
|
root_item = root
|
|
|
|
var root_config = meme(base_config)
|
|
tree_root = {
|
|
id: root,
|
|
config: root_config,
|
|
children: []
|
|
}
|
|
|
|
config_stack = [root_config]
|
|
|
|
fn()
|
|
|
|
lay_ctx.run()
|
|
|
|
// Post-layout: build scene tree
|
|
return build_scene_tree(tree_root, size.height)
|
|
}
|
|
|
|
function build_scene_tree(node, root_height, parent_abs_x, parent_abs_y) {
|
|
parent_abs_x = parent_abs_x || 0
|
|
parent_abs_y = parent_abs_y || 0
|
|
|
|
var rect = lay_ctx.get_rect(node.id)
|
|
|
|
// Calculate absolute world Y for this node (bottom-up layout to top-down render)
|
|
// rect.y is from bottom
|
|
var abs_y = root_height - (rect.y + rect.height)
|
|
var abs_x = rect.x
|
|
|
|
// Calculate relative position for the group
|
|
var rel_x = abs_x - parent_abs_x
|
|
var rel_y = abs_y - parent_abs_y
|
|
|
|
// The node to return. It might be a group or a sprite/text depending on config.
|
|
var scene_node = {
|
|
type: 'group',
|
|
pos: {x: rel_x + node.config.offset.x, y: rel_y + node.config.offset.y},
|
|
width: rect.width,
|
|
height: rect.height,
|
|
children: []
|
|
}
|
|
|
|
// Background
|
|
if (node.config.background_image) {
|
|
if (node.config.slice) {
|
|
scene_node.children.push({
|
|
type: 'sprite',
|
|
image: node.config.background_image,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
slice: node.config.slice,
|
|
color: node.config.background_color || {r:1, g:1, b:1, a:1},
|
|
layer: -1 // Back
|
|
})
|
|
} else {
|
|
scene_node.children.push({
|
|
type: 'sprite',
|
|
image: node.config.background_image,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
color: node.config.background_color || {r:1, g:1, b:1, a:1},
|
|
layer: -1
|
|
})
|
|
}
|
|
} else if (node.config.background_color) {
|
|
scene_node.children.push({
|
|
type: 'rect',
|
|
width: rect.width,
|
|
height: rect.height,
|
|
color: node.config.background_color,
|
|
layer: -1
|
|
})
|
|
}
|
|
|
|
// Content (Image/Text)
|
|
if (node.config.image) {
|
|
scene_node.children.push({
|
|
type: 'sprite',
|
|
image: node.config.image,
|
|
width: rect.width, // layout ensures aspect ratio if configured
|
|
height: rect.height,
|
|
color: node.config.color
|
|
})
|
|
}
|
|
|
|
if (node.config.text) {
|
|
scene_node.children.push({
|
|
type: 'text',
|
|
text: node.config.text,
|
|
font: node.config.font_path,
|
|
size: node.config.font_size,
|
|
color: node.config.color,
|
|
pos: {x: 0, y: rect.height} // Text baseline relative to group
|
|
})
|
|
}
|
|
|
|
// Clipping
|
|
if (node.config.clipped) {
|
|
// Scissor needs absolute coordinates
|
|
// We can compute them from our current absolute position
|
|
scene_node.scissor = {
|
|
x: abs_x + node.config.offset.x,
|
|
y: abs_y + node.config.offset.y,
|
|
width: rect.width,
|
|
height: rect.height
|
|
}
|
|
}
|
|
|
|
// Children
|
|
// Pass our absolute position as the new parent absolute position
|
|
var my_abs_x = abs_x + node.config.offset.x
|
|
var my_abs_y = abs_y + node.config.offset.y
|
|
|
|
for (var child of node.children) {
|
|
var child_node = build_scene_tree(child, root_height, my_abs_x, my_abs_y)
|
|
scene_node.children.push(child_node)
|
|
}
|
|
|
|
return scene_node
|
|
}
|
|
|
|
|
|
// --- Item Creation Helpers ---
|
|
|
|
function add_item(config) {
|
|
var parent_config = config_stack[config_stack.length-1]
|
|
|
|
// Merge parent spacing/padding/margin logic?
|
|
// clay.cm does normalizing and applying child_gap.
|
|
|
|
var use_config = meme(base_config, config)
|
|
|
|
var item = lay_ctx.item()
|
|
lay_ctx.set_margins(item, normalize_spacing(use_config.margin))
|
|
lay_ctx.set_contain(item, use_config.contain)
|
|
lay_ctx.set_behave(item, use_config.behave)
|
|
|
|
if (use_config.size) {
|
|
var s = use_config.size
|
|
if (isa(s, array)) s = {width: s[0], height: s[1]}
|
|
lay_ctx.set_size(item, s)
|
|
}
|
|
|
|
var node = {
|
|
id: item,
|
|
config: use_config,
|
|
children: []
|
|
}
|
|
|
|
// Link to tree
|
|
// We need to know current parent node.
|
|
// Track `tree_stack` alongside `config_stack`?
|
|
// Or just `tree_node_stack`.
|
|
|
|
// Let's fix the state tracking.
|
|
}
|
|
|
|
// Rewriting state management for cleaner recursion
|
|
var tree_stack = []
|
|
|
|
clay.layout = function(fn, size) {
|
|
lay_ctx.reset()
|
|
var root_id = lay_ctx.item()
|
|
if (isa(size, array)) size = {width: size[0], height: size[1]}
|
|
|
|
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()
|
|
|
|
return build_scene_tree(root_node, size.height)
|
|
}
|
|
|
|
function process_configs(configs) {
|
|
// Merge array of configs from right to left (right overrides left)
|
|
// And merge with base_config
|
|
var res = meme(base_config)
|
|
for (var c of configs) {
|
|
if (c) res = meme(res, c)
|
|
}
|
|
return res
|
|
}
|
|
|
|
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 (isa(s, array)) 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 (typeof configs == 'function') { fn = configs; configs = {} }
|
|
if (!isa(configs, array)) configs = [configs]
|
|
|
|
push_node(configs, null)
|
|
if (fn) fn()
|
|
pop_node()
|
|
}
|
|
|
|
// Stacks
|
|
clay.vstack = function(configs, fn) {
|
|
if (typeof configs == 'function') { fn = configs; configs = {} }
|
|
if (!isa(configs, array)) configs = [configs]
|
|
|
|
var c = layout.contain.column
|
|
// Check for alignment/justification in configs?
|
|
// Assume generic container props handle it via `contain` override or we defaults
|
|
|
|
push_node(configs, c)
|
|
if (fn) fn()
|
|
pop_node()
|
|
}
|
|
|
|
clay.hstack = function(configs, fn) {
|
|
if (typeof configs == 'function') { fn = configs; configs = {} }
|
|
if (!isa(configs, array)) configs = [configs]
|
|
|
|
var c = layout.contain.row
|
|
push_node(configs, c)
|
|
if (fn) fn()
|
|
pop_node()
|
|
}
|
|
|
|
clay.zstack = function(configs, fn) {
|
|
if (typeof configs == 'function') { fn = configs; configs = {} }
|
|
if (!isa(configs, array)) configs = [configs]
|
|
|
|
// Stack (overlap)
|
|
// layout.contain.stack? layout.contain.overlap?
|
|
// 'layout' module usually defaults to overlap if not row/column?
|
|
// Or we just don't set row/column bit.
|
|
var c = layout.contain.layout // Just layout (no flow)
|
|
|
|
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}
|
|
// Auto-size if not provided
|
|
// But we need to check if configs override it
|
|
var final_config = process_configs(configs)
|
|
if (!final_config.size && !final_config.behave) { // If no size and no fill behavior
|
|
c.size = {width: img.width, height: img.height}
|
|
}
|
|
|
|
push_node([c, ...configs], null)
|
|
pop_node()
|
|
}
|
|
|
|
clay.text = function(str, ...configs) {
|
|
var c = {text: str}
|
|
// Measuring
|
|
var final_config = process_configs(configs)
|
|
// measure text
|
|
var font = graphics.get_font(final_config.font_path) // or font cache
|
|
// Need to handle font object vs path string
|
|
// For now assume path string in config or default
|
|
|
|
// This measurement is synchronous and might differ from GPU font rendering slightly
|
|
// but good enough for layout.
|
|
// We need to know max width for wrapping?
|
|
// 'layout' doesn't easily support "height depends on width" during single pass?
|
|
// We specify a fixed size or behave.
|
|
|
|
// If no size specified, measure single line
|
|
if (!final_config.size && !final_config.behave) {
|
|
// Basic measurement
|
|
// Hack: use arbitrary width for now?
|
|
// Or we need a proper text measurement exposed.
|
|
// graphics.measure_text(font, text, size, break, align)
|
|
// Assume we have it or minimal version.
|
|
// clay.cm used `font.text_size`
|
|
|
|
// We'll rely on font path to get a font object
|
|
// var f = graphics.get_font(final_config.font_path) ...
|
|
// c.size = ...
|
|
c.size = {width: 100, height: 20} // Fallback for now to avoid crashes
|
|
}
|
|
|
|
push_node([c, ...configs], null)
|
|
pop_node()
|
|
}
|
|
|
|
clay.rectangle = function(...configs) {
|
|
// Just a container with background color really, but as a leaf
|
|
push_node(configs, null)
|
|
pop_node()
|
|
}
|
|
|
|
clay.button = function(str, action, ...configs) {
|
|
// Button is a container with text and click behavior
|
|
// For rendering, it's a zstack of background + text
|
|
|
|
// We can just define it as a container with background styling
|
|
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
|