clay tree
This commit is contained in:
@@ -9,6 +9,9 @@ var util = use('util')
|
|||||||
var input = use('input')
|
var input = use('input')
|
||||||
var prosperon = use('prosperon')
|
var prosperon = use('prosperon')
|
||||||
|
|
||||||
|
var CHILDREN = Symbol('children')
|
||||||
|
var PARENT = Symbol('parent')
|
||||||
|
|
||||||
function normalizeSpacing(spacing) {
|
function normalizeSpacing(spacing) {
|
||||||
if (typeof spacing == 'number') {
|
if (typeof spacing == 'number') {
|
||||||
return {l: spacing, r: spacing, t: spacing, b: spacing}
|
return {l: spacing, r: spacing, t: spacing, b: spacing}
|
||||||
@@ -47,8 +50,11 @@ var clay_base = {
|
|||||||
|
|
||||||
var root_item;
|
var root_item;
|
||||||
var root_config;
|
var root_config;
|
||||||
var boxes = [];
|
var tree_root;
|
||||||
var clay = {}
|
var clay = {}
|
||||||
|
clay.CHILDREN = CHILDREN
|
||||||
|
clay.PARENT = PARENT
|
||||||
|
|
||||||
var focused_textbox = null
|
var focused_textbox = null
|
||||||
|
|
||||||
clay.behave = layout.behave;
|
clay.behave = layout.behave;
|
||||||
@@ -58,7 +64,6 @@ clay.draw = function draw(fn)
|
|||||||
{
|
{
|
||||||
var size = [prosperon.camera.width, prosperon.camera.height]
|
var size = [prosperon.camera.width, prosperon.camera.height]
|
||||||
lay_ctx.reset();
|
lay_ctx.reset();
|
||||||
boxes = [];
|
|
||||||
var root = lay_ctx.item();
|
var root = lay_ctx.item();
|
||||||
// Accept both array and object formats
|
// Accept both array and object formats
|
||||||
if (Array.isArray(size)) {
|
if (Array.isArray(size)) {
|
||||||
@@ -68,42 +73,51 @@ clay.draw = function draw(fn)
|
|||||||
lay_ctx.set_contain(root,layout.contain.row);
|
lay_ctx.set_contain(root,layout.contain.row);
|
||||||
root_item = root;
|
root_item = root;
|
||||||
root_config = Object.assign({}, clay_base);
|
root_config = Object.assign({}, clay_base);
|
||||||
boxes.push({
|
tree_root = {
|
||||||
id: root,
|
id: root,
|
||||||
config: root_config,
|
config: root_config,
|
||||||
parent: null,
|
};
|
||||||
});
|
tree_root[CHILDREN] = [];
|
||||||
|
tree_root[PARENT] = null;
|
||||||
fn()
|
fn()
|
||||||
lay_ctx.run();
|
lay_ctx.run();
|
||||||
|
|
||||||
// Adjust bounding boxes for padding
|
// Adjust bounding boxes for padding - traverse tree instead of array
|
||||||
for (var i = 0; i < boxes.length; i++) {
|
function adjust_bounding_boxes(node) {
|
||||||
var box = boxes[i];
|
node.content = lay_ctx.get_rect(node.id);
|
||||||
box.content = lay_ctx.get_rect(box.id);
|
node.boundingbox = Object.assign({}, node.content);
|
||||||
box.boundingbox = Object.assign({}, box.content);
|
|
||||||
|
|
||||||
var padding = normalizeSpacing(box.config.padding || 0);
|
var padding = normalizeSpacing(node.config.padding || 0);
|
||||||
|
|
||||||
box.boundingbox.x -= padding.l;
|
node.boundingbox.x -= padding.l;
|
||||||
box.boundingbox.y -= padding.t;
|
node.boundingbox.y -= padding.t;
|
||||||
box.boundingbox.width += padding.l + padding.r;
|
node.boundingbox.width += padding.l + padding.r;
|
||||||
box.boundingbox.height += padding.t + padding.b;
|
node.boundingbox.height += padding.t + padding.b;
|
||||||
|
|
||||||
box.marginbox = Object.assign({}, box.content);
|
node.marginbox = Object.assign({}, node.content);
|
||||||
var margin = normalizeSpacing(box.config.margin || 0);
|
var margin = normalizeSpacing(node.config.margin || 0);
|
||||||
box.marginbox.x -= margin.l;
|
node.marginbox.x -= margin.l;
|
||||||
box.marginbox.y -= margin.t;
|
node.marginbox.y -= margin.t;
|
||||||
box.marginbox.width += margin.l+margin.r;
|
node.marginbox.width += margin.l+margin.r;
|
||||||
box.marginbox.height += margin.t+margin.b;
|
node.marginbox.height += margin.t+margin.b;
|
||||||
box.content.y *= -1;
|
node.content.y *= -1;
|
||||||
box.content.y += size.height;
|
node.content.y += size.height;
|
||||||
box.boundingbox.y *= -1;
|
node.boundingbox.y *= -1;
|
||||||
box.boundingbox.y += size.height;
|
node.boundingbox.y += size.height;
|
||||||
box.content.anchor_y = 1;
|
node.content.anchor_y = 1;
|
||||||
box.boundingbox.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)
|
function create_view_fn(base_config)
|
||||||
@@ -115,12 +129,21 @@ function create_view_fn(base_config)
|
|||||||
|
|
||||||
var prev_item = root_item;
|
var prev_item = root_item;
|
||||||
var prev_config = root_config;
|
var prev_config = root_config;
|
||||||
|
var prev_tree_root = tree_root;
|
||||||
root_item = item;
|
root_item = item;
|
||||||
root_config = config;
|
root_config = config;
|
||||||
root_config._childIndex = 0; // Initialize child index
|
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?.();
|
fn?.();
|
||||||
root_item = prev_item;
|
root_item = prev_item;
|
||||||
root_config = prev_config;
|
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_size(item,use_config.size);
|
||||||
lay_ctx.set_contain(item,use_config.contain);
|
lay_ctx.set_contain(item,use_config.contain);
|
||||||
lay_ctx.set_behave(item,use_config.behave);
|
lay_ctx.set_behave(item,use_config.behave);
|
||||||
boxes.push({
|
var tree_node = {
|
||||||
id: item,
|
id: item,
|
||||||
config: use_config,
|
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);
|
lay_ctx.insert(root_item,item);
|
||||||
|
|
||||||
// Increment the parent's child index
|
// 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);
|
var tsize = config.font.text_size(str, 0, config.size.x, config.text_break, config.text_align);
|
||||||
tsize.x = Math.ceil(tsize.x)
|
tsize.x = Math.ceil(tsize.x)
|
||||||
tsize.y = Math.ceil(tsize.y)
|
tsize.y = Math.ceil(tsize.y)
|
||||||
log.console(json.encode(tsize))
|
|
||||||
config.size = config.size.map((x,i) => Math.max(x, tsize[i]));
|
config.size = config.size.map((x,i) => Math.max(x, tsize[i]));
|
||||||
config.text = str;
|
config.text = str;
|
||||||
add_item(config);
|
add_item(config);
|
||||||
@@ -268,15 +292,15 @@ clay.textbox = function(str, on_change, ...configs) {
|
|||||||
|
|
||||||
var point = use('point')
|
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) {
|
function draw_node(node) {
|
||||||
var config = cmd.config
|
var config = node.config
|
||||||
var boundingbox = geometry.rect_move(cmd.boundingbox,point.add(pos,config.offset))
|
var boundingbox = geometry.rect_move(node.boundingbox,point.add(pos,config.offset))
|
||||||
var content = geometry.rect_move(cmd.content,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
|
// 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.hovered.__proto__ = config
|
||||||
config = config.hovered
|
config = config.hovered
|
||||||
}
|
}
|
||||||
@@ -294,7 +318,16 @@ clay.draw_commands = function draw_commands(cmds, pos = {x:0,y:0})
|
|||||||
|
|
||||||
if (config.image)
|
if (config.image)
|
||||||
draw.image(config.image, content, 0, config.color)
|
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 = {};
|
var dbg_colors = {};
|
||||||
@@ -302,15 +335,56 @@ clay.debug_colors = dbg_colors;
|
|||||||
dbg_colors.content = [1,0,0,0.1];
|
dbg_colors.content = [1,0,0,0.1];
|
||||||
dbg_colors.boundingbox = [0,1,0,0,0.1];
|
dbg_colors.boundingbox = [0,1,0,0,0.1];
|
||||||
dbg_colors.margin = [0,0,1,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++) {
|
function draw_debug_node(node) {
|
||||||
var cmd = cmds[i];
|
var boundingbox = geometry.rect_move(node.boundingbox,pos);
|
||||||
var boundingbox = geometry.rect_move(cmd.boundingbox,pos);
|
var content = geometry.rect_move(node.content,pos);
|
||||||
var content = geometry.rect_move(cmd.content,pos);
|
|
||||||
draw.rectangle(content, dbg_colors.content);
|
draw.rectangle(content, dbg_colors.content);
|
||||||
draw.rectangle(boundingbox, dbg_colors.boundingbox);
|
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 geometry = use('geometry')
|
||||||
var point = use('point')
|
var point = use('point')
|
||||||
|
var clay = use('prosperon/clay')
|
||||||
|
|
||||||
var clay_input = {}
|
var clay_input = {}
|
||||||
|
|
||||||
// Hit-test boxes against a mouse position
|
function rect_contains(node, pos) {
|
||||||
// boxes: array of box objects from clay.draw()
|
var bb = geometry.rect_move(node.boundingbox, node.config.offset || {x: 0, y: 0})
|
||||||
// pos: {x, y} position to test
|
return geometry.rect_point_inside(bb, pos)
|
||||||
// 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)
|
function pointer_enabled(node) {
|
||||||
for (var i = boxes.length - 1; i >= 0; i--) {
|
var p = node.config.pointer_events
|
||||||
var box = boxes[i]
|
if (!p || p == 'auto') return true
|
||||||
var boundingbox = geometry.rect_move(box.boundingbox, box.config.offset)
|
if (p == 'none') return false
|
||||||
if (geometry.rect_point_inside(boundingbox, pos)) {
|
return true
|
||||||
hovered = box
|
}
|
||||||
break
|
|
||||||
|
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 (should_skip_self(node)) return null
|
||||||
if (hovered && hovered.config.hovered) {
|
return next_path
|
||||||
hovered.state = hovered.state || {}
|
|
||||||
hovered.state.hovered = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear previous hover state if different
|
var path = find_path(tree_root, []) || []
|
||||||
if (prev_state.hovered && prev_state.hovered != hovered) {
|
var deepest = path.length ? path[path.length - 1] : null
|
||||||
prev_state.hovered.state = prev_state.hovered.state || {}
|
|
||||||
prev_state.hovered.state.hovered = false
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
return {
|
||||||
hovered: hovered,
|
path: path,
|
||||||
clicked: clicked
|
deepest: deepest,
|
||||||
|
action_target: action_target,
|
||||||
|
hover_chain: new_hover_chain
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle click events
|
clay_input.click = function click(tree_root, mousepos, button = 'left') {
|
||||||
clay_input.click = function click(boxes, mousepos, button = 'left') {
|
var hit_result = clay_input.hit(tree_root, mousepos, {})
|
||||||
var hit_result = clay_input.hit(boxes, mousepos)
|
var target = hit_result.action_target
|
||||||
var clicked = hit_result.hovered
|
if (target && target.config.action) target.config.action()
|
||||||
|
return target || null
|
||||||
|
}
|
||||||
|
|
||||||
if (clicked && clicked.config.action) {
|
clay_input.get_actionable = function get_actionable(tree_root) {
|
||||||
clicked.config.action()
|
var actionable = []
|
||||||
return clicked
|
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 null
|
||||||
|
}
|
||||||
|
return rec(tree_root)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get boxes with actions for navigation
|
clay_input.filter = function filter(tree_root, predicate) {
|
||||||
clay_input.get_actionable = function get_actionable(boxes) {
|
var results = []
|
||||||
return boxes.filter(box => box.config.action)
|
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
|
return clay_input
|
||||||
Reference in New Issue
Block a user