clay tree

This commit is contained in:
2025-11-06 13:49:51 -06:00
parent 721ac3bb93
commit 6961e19114
2 changed files with 245 additions and 87 deletions

View File

@@ -9,6 +9,9 @@ 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}
@@ -47,8 +50,11 @@ var clay_base = {
var root_item;
var root_config;
var boxes = [];
var tree_root;
var clay = {}
clay.CHILDREN = CHILDREN
clay.PARENT = PARENT
var focused_textbox = null
clay.behave = layout.behave;
@@ -58,7 +64,6 @@ clay.draw = function draw(fn)
{
var size = [prosperon.camera.width, prosperon.camera.height]
lay_ctx.reset();
boxes = [];
var root = lay_ctx.item();
// Accept both array and object formats
if (Array.isArray(size)) {
@@ -68,42 +73,51 @@ clay.draw = function draw(fn)
lay_ctx.set_contain(root,layout.contain.row);
root_item = root;
root_config = Object.assign({}, clay_base);
boxes.push({
tree_root = {
id: root,
config: root_config,
parent: null,
});
};
tree_root[CHILDREN] = [];
tree_root[PARENT] = null;
fn()
lay_ctx.run();
// Adjust bounding boxes for padding
for (var i = 0; i < boxes.length; i++) {
var box = boxes[i];
box.content = lay_ctx.get_rect(box.id);
box.boundingbox = Object.assign({}, box.content);
// 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(box.config.padding || 0);
var padding = normalizeSpacing(node.config.padding || 0);
box.boundingbox.x -= padding.l;
box.boundingbox.y -= padding.t;
box.boundingbox.width += padding.l + padding.r;
box.boundingbox.height += padding.t + padding.b;
node.boundingbox.x -= padding.l;
node.boundingbox.y -= padding.t;
node.boundingbox.width += padding.l + padding.r;
node.boundingbox.height += padding.t + padding.b;
box.marginbox = Object.assign({}, box.content);
var margin = normalizeSpacing(box.config.margin || 0);
box.marginbox.x -= margin.l;
box.marginbox.y -= margin.t;
box.marginbox.width += margin.l+margin.r;
box.marginbox.height += margin.t+margin.b;
box.content.y *= -1;
box.content.y += size.height;
box.boundingbox.y *= -1;
box.boundingbox.y += size.height;
box.content.anchor_y = 1;
box.boundingbox.anchor_y = 1;
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);
}
}
}
return boxes;
adjust_bounding_boxes(tree_root);
return tree_root;
}
function create_view_fn(base_config)
@@ -115,12 +129,21 @@ function create_view_fn(base_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;
}
}
@@ -183,11 +206,13 @@ function add_item(config)
lay_ctx.set_size(item,use_config.size);
lay_ctx.set_contain(item,use_config.contain);
lay_ctx.set_behave(item,use_config.behave);
boxes.push({
var tree_node = {
id: item,
config: use_config,
parent: root_item,
});
};
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
@@ -226,7 +251,6 @@ clay.text = function text(str, ...configs)
var tsize = config.font.text_size(str, 0, config.size.x, config.text_break, config.text_align);
tsize.x = Math.ceil(tsize.x)
tsize.y = Math.ceil(tsize.y)
log.console(json.encode(tsize))
config.size = config.size.map((x,i) => Math.max(x, tsize[i]));
config.text = str;
add_item(config);
@@ -268,15 +292,15 @@ clay.textbox = function(str, on_change, ...configs) {
var point = use('point')
clay.draw_commands = function draw_commands(cmds, pos = {x:0,y:0})
clay.draw_commands = function draw_commands(tree_root, pos = {x:0,y:0})
{
for (var cmd of cmds) {
var config = cmd.config
var boundingbox = geometry.rect_move(cmd.boundingbox,point.add(pos,config.offset))
var content = geometry.rect_move(cmd.content,point.add(pos, config.offset))
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))
// Check if this box should use hover styling
if (cmd.state && cmd.state.hovered && config.hovered) {
if (node.state && node.state.hovered && config.hovered) {
config.hovered.__proto__ = config
config = config.hovered
}
@@ -294,23 +318,73 @@ clay.draw_commands = function draw_commands(cmds, pos = {x:0,y:0})
if (config.image)
draw.image(config.image, content, 0, config.color)
// Recursively draw children
if (node[CHILDREN]) {
for (var child of node[CHILDREN]) {
draw_node(child);
}
}
}
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(cmds, pos = {x:0, y:0})
clay.draw_debug = function draw_debug(tree_root, pos = {x:0, y:0})
{
for (var i = 0; i < cmds.length; i++) {
var cmd = cmds[i];
var boundingbox = geometry.rect_move(cmd.boundingbox,pos);
var content = geometry.rect_move(cmd.content,pos);
function draw_debug_node(node) {
var boundingbox = geometry.rect_move(node.boundingbox,pos);
var content = geometry.rect_move(node.content,pos);
draw.rectangle(content, dbg_colors.content);
draw.rectangle(boundingbox, dbg_colors.boundingbox);
// draw.rectangle(geometry.rect_move(cmd.marginbox,pos), dbg_colors.margin);
// 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) {
log.console(json.encode(tree_root[clay.CHILDREN]))
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)`)
}
}

View File

@@ -3,61 +3,145 @@
var geometry = use('geometry')
var point = use('point')
var clay = use('prosperon/clay')
var clay_input = {}
// Hit-test boxes against a mouse position
// boxes: array of box objects from clay.draw()
// pos: {x, y} position to test
// prev_state: previous input state for tracking changes
clay_input.hit = function hit(boxes, pos, prev_state = {}) {
var hovered = null
var clicked = null
function rect_contains(node, pos) {
var bb = geometry.rect_move(node.boundingbox, node.config.offset || {x: 0, y: 0})
return geometry.rect_point_inside(bb, pos)
}
// Find the topmost hovered box (iterate in reverse for proper z-order)
for (var i = boxes.length - 1; i >= 0; i--) {
var box = boxes[i]
var boundingbox = geometry.rect_move(box.boundingbox, box.config.offset)
if (geometry.rect_point_inside(boundingbox, pos)) {
hovered = box
break
function pointer_enabled(node) {
var p = node.config.pointer_events
if (!p || p == 'auto') return true
if (p == 'none') return false
return true
}
function should_skip_children(node) {
var p = node.config.pointer_events
if (p == 'box-only') return true
return false
}
function should_skip_self(node) {
var p = node.config.pointer_events
if (p == 'box-none') return true
return false
}
clay_input.hit = function hit(tree_root, pos, prev_state = {}) {
function find_path(node, path) {
if (!pointer_enabled(node)) return null
if (!rect_contains(node, pos)) return null
var next_path = path.concat(node)
if (node[clay.CHILDREN] && !should_skip_children(node)) {
// Children drawn later should be tested first; reverse if your render order differs
for (var i = node[clay.CHILDREN].length - 1; i >= 0; i--) {
var child = node[clay.CHILDREN][i]
var child_path = find_path(child, next_path)
if (child_path) return child_path
}
}
// Update hover state
if (hovered && hovered.config.hovered) {
hovered.state = hovered.state || {}
hovered.state.hovered = true
if (should_skip_self(node)) return null
return next_path
}
// Clear previous hover state if different
if (prev_state.hovered && prev_state.hovered != hovered) {
prev_state.hovered.state = prev_state.hovered.state || {}
prev_state.hovered.state.hovered = false
}
var path = find_path(tree_root, []) || []
var deepest = path.length ? path[path.length - 1] : null
return {
hovered: hovered,
clicked: clicked
function nearest_actionable(path) {
for (var i = path.length - 1; i >= 0; i--) {
var n = path[i]
if (n && n.config && typeof n.config.action == 'function') return n
}
}
// Handle click events
clay_input.click = function click(boxes, mousepos, button = 'left') {
var hit_result = clay_input.hit(boxes, mousepos)
var clicked = hit_result.hovered
if (clicked && clicked.config.action) {
clicked.config.action()
return clicked
}
return null
}
// Get boxes with actions for navigation
clay_input.get_actionable = function get_actionable(boxes) {
return boxes.filter(box => box.config.action)
var action_target = nearest_actionable(path)
// Hover bookkeeping: let child hover, and optionally parent hover via hover_includes_children
var new_hover_chain = []
if (deepest) {
if (deepest.config && deepest.config.hovered) {
deepest.state = deepest.state || {}
deepest.state.hovered = true
new_hover_chain.push(deepest)
}
for (var i = path.length - 2; i >= 0; i--) {
var anc = path[i]
if (anc.config && anc.config.hovered) {
anc.state = anc.state || {}
anc.state.hovered = true
new_hover_chain.push(anc)
}
}
}
// Clear previous hovers not in the new chain
if (prev_state.hover_chain) {
for (var i = 0; i < prev_state.hover_chain.length; i++) {
var n = prev_state.hover_chain[i]
if (new_hover_chain.indexOf(n) == -1) {
n.state = n.state || {}
n.state.hovered = false
}
}
}
return {
path: path,
deepest: deepest,
action_target: action_target,
hover_chain: new_hover_chain
}
}
clay_input.click = function click(tree_root, mousepos, button = 'left') {
var hit_result = clay_input.hit(tree_root, mousepos, {})
var target = hit_result.action_target
if (target && target.config.action) target.config.action()
return target || null
}
clay_input.get_actionable = function get_actionable(tree_root) {
var actionable = []
function walk(node) {
if (node.config.action) actionable.push(node)
if (node[clay.CHILDREN])
for (var child of node[clay.CHILDREN]) walk(child)
}
walk(tree_root)
return actionable
}
clay_input.find_by_id = function find_by_id(tree_root, id) {
function rec(node) {
if (node.id == id) return node
if (node[clay.CHILDREN])
for (var child of node[clay.CHILDREN]) {
var f = rec(child)
if (f) return f
}
return null
}
return rec(tree_root)
}
clay_input.filter = function filter(tree_root, predicate) {
var results = []
function rec(node) {
if (predicate(node)) results.push(node)
if (node[clay.CHILDREN])
for (var child of node[clay.CHILDREN]) rec(child)
}
rec(tree_root)
return results
}
return clay_input