// Layout code // Contain is for how it will treat its children. If they should be laid out as a row, or column, or in a flex style, etc. var layout = use('layout') var geometry = use('geometry') var draw = use('prosperon/draw2d') var graphics = use('graphics') var util = use('util') var input = use('input') var prosperon = use('prosperon') var CHILDREN = Symbol('children') var PARENT = Symbol('parent') function normalizeSpacing(spacing) { if (typeof spacing == 'number') { return {l: spacing, r: spacing, t: spacing, b: spacing} } else if (Array.isArray(spacing)) { if (spacing.length == 2) { return {l: spacing[0], r: spacing[0], t: spacing[1], b: spacing[1]} } else if (spacing.length == 4) { return {l: spacing[0], r: spacing[1], t: spacing[2], b: spacing[3]} } } else if (typeof spacing == 'object') { return {l: spacing.l || 0, r: spacing.r || 0, t: spacing.t || 0, b: spacing.b || 0} } else { return {l:0, r:0, t:0, b:0} } } var lay_ctx = layout.make_context(); var clay_base = { font: null, background_image: null, slice: 0, font: 'smalle.16', font_size: null, 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, // {width: null, height: null} }; var root_item; var root_config; var tree_root; var clay = {} clay.CHILDREN = CHILDREN clay.PARENT = PARENT var focused_textbox = null clay.behave = layout.behave; clay.contain = layout.contain; clay.draw = function draw(fn, size = [prosperon.camera.width, prosperon.camera.height]) { lay_ctx.reset(); var root = lay_ctx.item(); // Accept both array and object formats if (Array.isArray(size)) { size = {width: size[0], height: size[1]}; } lay_ctx.set_size(root,size); lay_ctx.set_contain(root,layout.contain.row); root_item = root; root_config = Object.assign({}, clay_base); tree_root = { id: root, config: root_config, }; tree_root[CHILDREN] = []; tree_root[PARENT] = null; fn() lay_ctx.run(); // Adjust bounding boxes for padding - traverse tree instead of array function adjust_bounding_boxes(node) { node.content = lay_ctx.get_rect(node.id); node.boundingbox = Object.assign({}, node.content); var padding = normalizeSpacing(node.config.padding || 0); node.boundingbox.x -= padding.l; node.boundingbox.y -= padding.t; node.boundingbox.width += padding.l + padding.r; node.boundingbox.height += padding.t + padding.b; node.marginbox = Object.assign({}, node.content); var margin = normalizeSpacing(node.config.margin || 0); node.marginbox.x -= margin.l; node.marginbox.y -= margin.t; node.marginbox.width += margin.l+margin.r; node.marginbox.height += margin.t+margin.b; node.content.y *= -1; node.content.y += size.height; node.boundingbox.y *= -1; node.boundingbox.y += size.height; node.content.anchor_y = 1; node.boundingbox.anchor_y = 1; // Recursively adjust children if (node[CHILDREN]) { for (var child of node[CHILDREN]) { adjust_bounding_boxes(child); } } } adjust_bounding_boxes(tree_root); return tree_root; } function create_view_fn(base_config) { var base = Object.assign(Object.create(clay_base), base_config); return function view(config = {}, fn) { config.__proto__ = base; var item = add_item(config); var prev_item = root_item; var prev_config = root_config; var prev_tree_root = tree_root; root_item = item; root_config = config; root_config._childIndex = 0; // Initialize child index // Find the tree node for this item and set it as current tree_root if (prev_tree_root[CHILDREN] && prev_tree_root[CHILDREN].length > 0) { tree_root = prev_tree_root[CHILDREN][prev_tree_root[CHILDREN].length - 1]; } else { // If no children yet, this shouldn't happen, but handle gracefully tree_root = prev_tree_root; } fn?.(); root_item = prev_item; root_config = prev_config; tree_root = prev_tree_root; } } clay.vstack = create_view_fn({ contain: layout.contain.column | layout.contain.start, }); clay.hstack = create_view_fn({ contain: layout.contain.row | layout.contain.start, }); clay.spacer = create_view_fn({ behave: layout.behave.hfill | layout.behave.vfill }); clay.frame = create_view_fn({}); function image_size(img) { return [img.width * (img.rect?.width || 1), img.height * (img.rect?.height || 1)]; } function add_item(config) { // Normalize the child's margin var margin = normalizeSpacing(config.margin || 0); var padding = normalizeSpacing(config.padding || 0); var childGap = root_config.child_gap || 0; // Adjust for child_gap root_config._childIndex ??= 0 if (root_config._childIndex > 0) { var parentContain = root_config.contain || 0; var directionMask = layout.contain.row | layout.contain.column; var direction = parentContain & directionMask; var isVStack = direction == layout.contain.column; var isHStack = direction == layout.contain.row; if (isVStack) { margin.t += childGap; } else if (isHStack) { margin.l += childGap; } } var use_config = Object.create(config); use_config.margin = { t: margin.t+padding.t, b: margin.b+padding.b, r:margin.r+padding.r, l:margin.l+padding.l }; var item = lay_ctx.item(); lay_ctx.set_margins(item, use_config.margin); use_config.size ??= {width:0, height:0} // Convert array to object if needed if (Array.isArray(use_config.size)) { use_config.size = {width: use_config.size[0], height: use_config.size[1]}; } // Apply max_size constraint if (use_config.max_size) { // For containers with max_size, set explicit size to enable proper clipping if (use_config.contain && use_config.size.width == 0 && use_config.max_size.width != null) { use_config.size.width = use_config.max_size.width; } if (use_config.contain && use_config.size.height == 0 && use_config.max_size.height != null) { use_config.size.height = use_config.max_size.height; } // Clamp any size that exceeds max_size if (use_config.max_size.width != null && use_config.size.width > use_config.max_size.width) { use_config.size.width = use_config.max_size.width; } if (use_config.max_size.height != null && use_config.size.height > use_config.max_size.height) { use_config.size.height = use_config.max_size.height; } } lay_ctx.set_size(item,use_config.size); lay_ctx.set_contain(item,use_config.contain); lay_ctx.set_behave(item,use_config.behave); var tree_node = { id: item, config: use_config, }; tree_node[CHILDREN] = []; tree_node[PARENT] = tree_root; tree_root[CHILDREN].push(tree_node); lay_ctx.insert(root_item,item); // Increment the parent's child index root_config._childIndex++; return item; } function rectify_configs(config_array) { if (config_array.length == 0) config_array = [{}]; for (var i = config_array.length-1; i > 0; i--) config_array[i].__proto__ = config_array[i-1]; config_array[0].__proto__ = clay_base; var cleanobj = Object.create(config_array[config_array.length-1]); return cleanobj; } clay.image = function image(path, ...configs) { var config = rectify_configs(configs); var image = graphics.texture(path); config.image = path; // Store the path string, not the texture object config.size ??= {width: image.width, height: image.height}; add_item(config); } clay.text = function text(str, ...configs) { var config = rectify_configs(configs); config.size ??= [0,0] config.font = graphics.get_font(config.font) config.text = str var tsize = config.font.text_size(str, 0, config.size[0], config.text_break, config.text_align); tsize.x = Math.ceil(tsize.x) tsize.y = Math.ceil(tsize.y) config.size = config.size.map((x,i) => Math.max(x, tsize[i])); add_item(config); } /* For a given size, the layout engine should "see" size + margin but its interior content should "see" size - padding hence, the layout box should be size-padding, with margin of margin+padding */ var button_base = Object.assign(Object.create(clay_base), { padding:0, hovered:{ } }); clay.button = function button(str, action, config = {}) { config.__proto__ = button_base; config.font = graphics.get_font(config.font) config.size = config.font.text_size(str, 0, 0, config.text_break, config.text_align) add_item(config); config.text = str; config.action = action; } var point = use('point') clay.draw_commands = function draw_commands(tree_root, pos = {x:0,y:0}) { function draw_node(node) { var config = node.config var boundingbox = geometry.rect_move(node.boundingbox,point.add(pos,config.offset)) var content = geometry.rect_move(node.content,point.add(pos, config.offset)) if (config.background_image) if (config.slice) draw.slice9(config.background_image, boundingbox, config.slice, config.background_color) else draw.image(config.background_image, boundingbox, 0, config.color) else if (config.background_color) draw.rectangle(boundingbox, null, {color:config.background_color}) if (config.text) { var baseline_y = content.y + content.height - config.font.ascent draw.text(config.text, {x: content.x, y: baseline_y}, config.font, config.color, content.width) } if (config.image) draw.image(config.image, content, 0, config.color) if (config.clipped) { draw.scissor(content) } // Recursively draw children if (node[CHILDREN]) { for (var child of node[CHILDREN]) { draw_node(child); } } if (config.clipped) draw.scissor(null) } draw_node(tree_root); } var dbg_colors = {}; clay.debug_colors = dbg_colors; dbg_colors.content = [1,0,0,0.1]; dbg_colors.boundingbox = [0,1,0,0,0.1]; dbg_colors.margin = [0,0,1,0.1]; clay.draw_debug = function draw_debug(tree_root, pos = {x:0, y:0}) { function draw_debug_node(node) { var boundingbox = geometry.rect_move(node.boundingbox,pos); var content = geometry.rect_move(node.content,pos); draw.rectangle(content, null, {color:dbg_colors.content}); draw.rectangle(boundingbox, null, {color:dbg_colors.boundingbox}); // draw.rectangle(geometry.rect_move(node.marginbox,pos), dbg_colors.margin); // Recursively draw debug for children if (node[CHILDREN]) { for (var child of node[CHILDREN]) { draw_debug_node(child); } } } draw_debug_node(tree_root); } clay.print_tree = function print_tree(tree_root, indent = 0) { var indent_str = ' '.repeat(indent) var node_type = 'unknown' if (tree_root.config.text) { node_type = 'text' } else if (tree_root.config.image) { node_type = 'image' } else if (tree_root.config.contain) { if (tree_root.config.contain & layout.contain.column) { node_type = 'vstack' } else if (tree_root.config.contain & layout.contain.row) { node_type = 'hstack' } else { node_type = 'container' } } else { node_type = 'node' } log.console(`${indent_str}${node_type} (id: ${tree_root.id})`) if (tree_root[CHILDREN] && tree_root[CHILDREN].length > 0) { log.console(`${indent_str} children: ${tree_root[CHILDREN].length}`) for (var child of tree_root[CHILDREN]) { print_tree(child, indent + 1) } } else { log.console(`${indent_str} (no children)`) } } return clay