clay tree
This commit is contained in:
@@ -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,7 +318,16 @@ 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 = {};
|
||||
@@ -302,15 +335,56 @@ 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)`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
// 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 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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if (should_skip_self(node)) return null
|
||||
return next_path
|
||||
}
|
||||
|
||||
var path = find_path(tree_root, []) || []
|
||||
var deepest = path.length ? path[path.length - 1] : null
|
||||
|
||||
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
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update hover state
|
||||
if (hovered && hovered.config.hovered) {
|
||||
hovered.state = hovered.state || {}
|
||||
hovered.state.hovered = true
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
hovered: hovered,
|
||||
clicked: clicked
|
||||
path: path,
|
||||
deepest: deepest,
|
||||
action_target: action_target,
|
||||
hover_chain: new_hover_chain
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
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)
|
||||
}
|
||||
|
||||
return null
|
||||
walk(tree_root)
|
||||
return actionable
|
||||
}
|
||||
|
||||
// Get boxes with actions for navigation
|
||||
clay_input.get_actionable = function get_actionable(boxes) {
|
||||
return boxes.filter(box => box.config.action)
|
||||
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)
|
||||
}
|
||||
|
||||
return clay_input
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user