// 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 (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 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 = max(sx, parent_scissor.x) sy = max(sy, parent_scissor.y) var right = min(vis_x + sw, parent_scissor.x + parent_scissor.width) var bottom = min(vis_y + sh, parent_scissor.y + parent_scissor.height) sw = max(0, right - sx) sh = max(0, bottom - sy) } current_scissor = {x: sx, y: sy, width: sw, height: sh} } // Background if (node.config.background_image) { if (node.config.slice) { push(drawables, { 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 { push(drawables, { 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) { push(drawables, { 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) { push(drawables, { 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) { push(drawables, { 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 arrfor(node.children, function(child) { drawables = array(drawables, build_drawables(child, root_height, vis_x, vis_y, current_scissor, parent_layer + 0.01)) }) 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[length(tree_stack)-1] push(parent.children, node) lay_ctx.insert(parent.id, item) push(tree_stack, node) return node } function pop_node() { pop(tree_stack) } // 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} if (!is_array(configs)) configs = [configs] push_node(array(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} } if (!is_array(configs)) configs = [configs] push_node(array(c, configs), null) pop_node() } clay.rectangle = function(configs) { if (!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} }] if (!is_array(configs)) configs = [configs] clay.zstack(array(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