410 lines
12 KiB
Plaintext
410 lines
12 KiB
Plaintext
// 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('draw2d')
|
|
var graphics = use('graphics')
|
|
var input = use('sdl/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;
|
|
|
|
// Apply max_size clamping post-layout
|
|
if (node.config.max_size) {
|
|
if (node.config.max_size.width != null) {
|
|
// Clamp the layout rect size
|
|
var rect = lay_ctx.get_rect(node.id);
|
|
rect.width = Math.min(rect.width, node.config.max_size.width);
|
|
// Also clamp bounding box
|
|
node.content.width = Math.min(node.content.width, node.config.max_size.width);
|
|
node.boundingbox.width = Math.min(node.boundingbox.width, node.config.max_size.width + padding.l + padding.r);
|
|
node.marginbox.width = Math.min(node.marginbox.width, node.config.max_size.width + padding.l + padding.r + margin.l + margin.r);
|
|
}
|
|
if (node.config.max_size.height != null) {
|
|
// Clamp the layout rect size
|
|
var rect = lay_ctx.get_rect(node.id);
|
|
rect.height = Math.min(rect.height, node.config.max_size.height);
|
|
// Also clamp bounding box
|
|
node.content.height = Math.min(node.content.height, node.config.max_size.height);
|
|
node.boundingbox.height = Math.min(node.boundingbox.height, node.config.max_size.height + padding.t + padding.b);
|
|
node.marginbox.height = Math.min(node.marginbox.height, node.config.max_size.height + padding.t + padding.b + 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 - only clamp computed size, don't set explicit size pre-layout
|
|
if (use_config.max_size) {
|
|
// max_size should not force explicit sizing pre-layout - let children compute natural size,
|
|
// then clamp the container after layout. For now, just ensure we don't set size to max_size.
|
|
}
|
|
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
|