diff --git a/clay.cm b/clay.cm index 36c97cd7..a2f33944 100644 --- a/clay.cm +++ b/clay.cm @@ -1,17 +1,21 @@ -// clay2.cm - Revised UI layout engine emitting flat drawables +// clay.cm - UI layout engine emitting flat drawables with annotated tree // -// Changes from clay.cm: -// - No __proto__, uses meme/merge +// - Uses meme/merge for config chains // - Emits flat list of drawables for film2d // - Supports scissor clipping -// -// Now returns [drawable, drawable, ...] instead of {type:'group', ...} +// - 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() @@ -65,28 +69,49 @@ 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) { lay_ctx.reset() + _next_id = 0 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) + + // Annotate tree for clay_input (boundingbox, CHILDREN, PARENT) + annotate_tree(root_node, size.height, null) + + // Build flat drawable list and attach to tree root + root_node.drawables = build_drawables(root_node, size.height) + return root_node } function build_drawables(node, root_height, parent, parent_scissor) { @@ -130,8 +155,10 @@ function build_drawables(node, root_height, parent, parent_scissor) { // Background if (node.config.background_image) { + _next_id = _next_id + 1 if (node.config.slice) { push(drawables, { + _id: _next_id, type: 'sprite', image: node.config.background_image, pos: {x: vis_x, y: vis_y}, @@ -139,11 +166,12 @@ function build_drawables(node, root_height, parent, parent_scissor) { 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, // slightly behind content + layer: p_layer - 0.1, scissor: current_scissor }) } else { push(drawables, { + _id: _next_id, type: 'sprite', image: node.config.background_image, pos: {x: vis_x, y: vis_y}, @@ -155,7 +183,9 @@ function build_drawables(node, root_height, parent, parent_scissor) { }) } } else if (node.config.background_color) { + _next_id = _next_id + 1 push(drawables, { + _id: _next_id, type: 'rect', pos: {x: vis_x, y: vis_y}, width: rect.width, @@ -165,38 +195,34 @@ function build_drawables(node, root_height, parent, parent_scissor) { scissor: current_scissor }) } - + // Content (Image/Text) if (node.config.image) { + _next_id = _next_id + 1 push(drawables, { + _id: _next_id, type: 'sprite', image: node.config.image, pos: {x: vis_x, y: vis_y}, width: rect.width, - height: rect.height, + height: rect.height, color: node.config.color, layer: p_layer, scissor: current_scissor }) } - + if (node.config.text) { + _next_id = _next_id + 1 push(drawables, { + _id: _next_id, type: 'text', text: node.config.text, font: node.config.font_path, - size: node.config.font_size, + 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. + pos: {x: vis_x, y: vis_y + rect.height}, + anchor_y: 1.0, layer: p_layer, scissor: current_scissor }) @@ -216,6 +242,20 @@ function build_drawables(node, root_height, parent, parent_scissor) { 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}) @@ -305,34 +345,34 @@ clay.zstack = function(configs, fn) { } // Leaf nodes -clay.image = function(path, configs) { +clay.image = function(path, configs, extra) { var img = graphics.texture(path) var c = [{image: path}] - var final_config = process_configs(configs) + 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} - - var _configs = is_array(configs) ? configs : [configs] + push(c, {size: {width: img.width, height: img.height}}) push_node(array(c, _configs), null) pop_node() } -clay.text = function(str, configs) { +clay.text = function(str, configs, extra) { var c = [{text: str}] - var final_config = process_configs(configs) + 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(c, {size: {width: 100, height: 20}}) } - var _configs = is_array(configs) ? configs : [configs] - push_node(array(c, _configs), null) pop_node() } clay.rectangle = function(configs) { - var _configs = is_array(configs) ? configs : [configs] + var _configs = configs ? (is_array(configs) ? configs : [configs]) : [{}] push_node(_configs, null) pop_node() } @@ -350,6 +390,47 @@ clay.button = function(str, action, configs) { }) } +// 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} + push(result, d) + } + return result +} + // Constants clay.behave = layout.behave clay.contain = layout.contain diff --git a/clay_input.cm b/clay_input.cm index a641d21a..cb815a62 100644 --- a/clay_input.cm +++ b/clay_input.cm @@ -34,7 +34,8 @@ function find_path(node, path, pos) { if (!pointer_enabled(node)) return null if (!rect_contains(node, pos)) return null - var next_path = array(path, node) + var next_path = array(path) + next_path[] = node var i = 0 var child = null var child_path = null diff --git a/core.cm b/core.cm index 77ba5fe5..eb617fe7 100644 --- a/core.cm +++ b/core.cm @@ -78,6 +78,9 @@ core.start = function(config) { // Start main loop _main_loop() + // Call start callback after the first frame and main loop are established + if (config.start) config.start() + return true } diff --git a/sdl_gpu.cm b/sdl_gpu.cm index fd0d5d2b..4a73c714 100644 --- a/sdl_gpu.cm +++ b/sdl_gpu.cm @@ -1824,8 +1824,8 @@ function _render_text(ctx, drawable) { if (ax != 0 || ay != 0) { dim = font.text_size(drawable.text) if (dim) { - text_pos.x -= dim.x * ax - text_pos.y -= dim.y * ay + text_pos.x -= dim[0] * ax + text_pos.y -= dim[1] * ay } }