tested new layout system

This commit is contained in:
Alessandro Mauri 2025-09-03 23:29:20 +02:00
parent 24ac28e0d9
commit 2619873ca7
3 changed files with 667 additions and 1 deletions

220
lib/ugui.c3l/LAYOUT Normal file
View File

@ -0,0 +1,220 @@
Div Children Alignment
+------------------------------------------------+
|TOP-LEFT TOP TOP-RIGHT|
| |
| |
| |
| |
| |
| |
|LEFT CENTER RIGHT|
| |
| |
| |
| |
| |
| |
|BOTTOM-LEFT BOTTOM BOTTOM-RIGHT|
+------------------------------------------------+
ALIGNMENT CHART:
+------------------------------+----------------------+------------------------------+-----------------------+------------------------------+----------------------+
| TOP-LEFT, ROW: | TOP-LEFT, COLUMN: | BOTTOM, ROW: | BOTTOM, COLUMN: | TOP-RIGHT, ROW: | TOP-RIGHT, COLUMN: |
| | | | | | |
| +------------------------- - | +----------- - | | +----+ | | - -----------+ |
| |+-------++-----++-----+ | |+-------+ | | | E1 | | | +-------+| |
| || E1 || E2 || | | || E1 | | | | | | - -----------------------+ | | E1 || |
| || |+-----+| E3 | | || | | | +----+ | +-------++-----++-----+| | | || |
| |+-------+ | | | |+-------+ | +-------+ +---+ | +------+ | | E1 || E2 || || | +-------+| |
| | +-----+ | |+----+ | | E1 |+------+|E3 | | | E2 | | | |+-----+| E3 || | +----+| |
| ' | || E2 | | | || E2 || | | | | | +-------+ | || | | E2 || |
| ' | |+----+ | +-------++------++---+ | +------+ | +-----+| | +----+| |
| | |+---------+ | - ------------------------ - | +--+ | ' | +---------+| |
| | || E3 | | | |E3| | ' | | E3 || |
| | |+---------+ | | +--+ | | +---------+| |
| | ' | | - ----------- - | | ' |
| | ' | | | | ' |
| | | | | | |
+------------------------------+----------------------+------------------------------+-----------------------+------------------------------+----------------------+
| LEFT, ROW: | LEFT, COLUMN: | BOTTOM-RIGHT, ROW: | BOTTOM-RIGHT, COLUMN: | TOP, ROW: | TOP, COLUMN: |
| | | | | | |
| | ' | | ' | | - -------------- - |
| ' | |+-------+ | | +-------+| | | +----------+ |
| | +----+ | || E1 | | ' | | E1 || | - ----------------------- - | | E1 | |
| |+------+ | | | || | | +-----+| | | || | +------++----++-----+ | | | |
| || |+-----+| | | |+-------+ | +-------+ | || | +-------+| | | E1 || E2 || E3 | | +----------+ |
| || E1 || E2 || E3 | | |+----+ | | E1 |+-----+| E3 || | +----+| | | |+----+| | | +--------+ |
| || |+-----+| | | || E2 | | | || E2 || || | | E2 || | +------+ | | | | E2 | |
| |+------+ | | | |+----+ | +-------++-----++-----+| | +----+| | +-----+ | +--------+ |
| | +----+ | |+---------+ | - -----------------------+ | +---------+| | | +------+ |
| ' | || E3 | | | | E3 || | | | E3 | |
| ' | |+---------+ | | +---------+| | | | | |
| | ' | | - -----------+ | | +------+ |
| | ' | | | | |
+------------------------------+----------------------+------------------------------+-----------------------+------------------------------+----------------------+
| BOTTOM-LEFT, ROW: | BOTTOM-LEFT, COLUMN: | RIGHT, ROW: | RIGHT, COLUMN: | CENTER, ROW: | CENTER, COLUMN: |
| | | | | | |
| | ' | | ' | | | |
| | |+-------+ | | +-------+| | | | +-----------+ |
| | || E1 | | ' | | E1 || | | | | E1 | |
| ' | || | | +----+| | | || | | +----+ | | | | |
| | +-----+ | |+-------+ | +------+ | || | +-------+| | +------+ | | | | +-----------+ |
| |+-------+ | | | |+----+ | | |+-----+| || | +----+| | | |+----+| | | +---------+ |
| || E1 |+-----+| E3 | | || E2 | | | E1 || E2 || E3 || | | E2 || | ---|--E1--||-E2-||-E3-|--- | ----|---E2----|---- |
| || || E2 || | | |+----+ | | |+-----+| || | +----+| | | |+----+| | | +---------+ |
| |+-------++-----++-----+ | |+---------+ | +------+ | || | +---------+| | +------+ | | | | +-------+ |
| +------------------------- - | || E3 | | +----+| | | E3 || | | +----+ | | E3 | |
| | |+---------+ | ' | +---------+| | | | | | | |
| | +----------- - | ' | ' | | | +-------+ |
| | | | ' | | | |
| | | | | | |
+------------------------------+----------------------+------------------------------+-----------------------+------------------------------+----------------------+
div (
align: TOP-LEFT | LEFT | BOTTOM-LEFT | BOTTOM | BOTTOM-RIGHT | RIGHT | TOP-RIGHT | RIGHT | CENTER
size_x/y: EXACT(x) | GROW() | FIT(min, max)
scroll_x/y: true | false
resize_x/y: true | false
layout: ROW | COLUMN
)
align: alignment of the children elements
size: how the div should be sized
scroll: enables scrollbars
layout: the layout direction of the children
COLUMN ROW
+--------------------+ +----------------------------------------------------+
| +----------------+ | |+----------------+ +------------------+|
| | | | || |+------------+| ||
| | | | || || || E3 ||
| | E1 | | || E1 || E2 || ||
| | | | || || || ||
| | | | || |+------------++------------------+|
| +----------------+ | |+----------------+ |
| +------------+ | +----------------------------------------------------+
| | | |
| | E2 | |
| | | |
| +------------+ |
|+------------------+|
|| ||
|| E3 ||
|| ||
|| ||
|+------------------+|
+--------------------+
Element {
id: uint
sizing: { min_w, min_h max_w, max_h }
bounds: { x, y, w, h }
}
id: unique identifier of the element
sizing: the size that the element wants
bounds: the absoulte bounds that the element got assigned
Rendering
=========
Rendering happens when the element is called (immediately for leaf widgets like buttons and at the end
for root widgets like divs). The drawing is done on the bounds assigned to the widget, these bounds
have a one-frame delay on the current layout.
The layout is calculated by each div at the end of their block and at frame end all the sizes and positions
are assigned at frame end by iterating the element tree.
ElemDiv {
align: TOP-LEFT | LEFT | BOTTOM-LEFT | BOTTOM | BOTTOM-RIGHT | RIGHT | TOP-RIGHT | RIGHT | CENTER
size_x/y: { min, max }
scroll_x/y: true | false
layout: ROW | COLUMN
children_size_x/y: { min, max }
}
size:
- min != max -> FIT sizing, fit to the content but respect the min and max size
- min == max == 0 -> GROW sizing, grow to the max amount of space possible
- min == max != 0 -> EXACT sizing
children_size: the size of the combined children sizes
root(size_x: screen width, size_y: screen height, layout: ROW) {
div1(layout: COLUMN, size_x: FIT, size_y GROW, resize_x: true) {
E1()
E2()
E3()
E4()
} <-(end div 1)
div2(size_x: GROW, size_y: GROW) {
...
} <-(end div 2)
div3(layout: COLUMN, size_x: FIT, size_y: GROW) {
E5()
E6()
E7()
} <-(end div 3)
} <-(end root)
(frame end)
+-Root-Div------------------------------------------------+
|+-Div-1----------++-Div-2--------------------++-Div-3---+|
||+--------------+|| ||+-------+||
||| E1 ||| ||| E5 |||
||| ||| ||| |||
||+--------------+|| ||+-------+|| [Root Div]
||+--------------+|| ||+-------+|| |
||| E2 ||| ||| E6 ||| +----------+----+-------+
||| ||| ||| ||| v v v
||+--------------+|| ||+-------+|| [Div 1] [Div 2] [Div 3]
||+------+ || ||+-------+|| | |
||| | || ||| E7 ||| +----+----+----+ |
||| E3 | || ||| ||| v v v v |
||| | || ||+-------+|| [E1] [E2] [E3] [E4] +----+----+
||+------+ || || || v v v
||+------+ || || || [E5] [E6] [E7]
||| | || || ||
||| E4 | || || ||
||| | || || ||
||+------+ || || ||
|| || || ||
|+----------------++--------------------------++---------+|
+---------------------------------------------------------+
the call order is as follows
E1() -> updates the children size of div1
E2() -> " "
E3() -> " "
E4() -> " "
end div1() -> updates the children size of root
end div2() -> updates the children size of root
E5() -> updates the children size of div3
E6() -> " "
E7() -> " "
end root() -> does nothing
at frame end:
* Root: the root has a size constraint of fit so the bounds get assigned the whole window
* Div 1: the width has a size of fit, so it gets set to the children bounds, the height is set
to the root height since it has a height of GROW
- E1 to E4 get laid out
* Div 2: it has a width of GROW which is **along** the layout axis, so it gets added to grow list
the height gets set to the root height
* Div 3: the width is FIT, so it gets set to the content width, the height gets se to the root
height.
- E5 to E7 get laid out
* Div 2: is given a width (if there were other contending grow divs along the layout axis they
would also get sized).
- Now that div 2 has a size all it's children can be given a size
Styling
=======

View File

@ -24,9 +24,17 @@ macro @zero()
{
$if @assignable_to(0, ElemType):
return 0;
$else
$endif
$if @assignable_to(null, ElemType):
return null;
$endif
$if @assignable_to({}, ElemType):
return {};
$endif
//$assert true == false : ElemType.nameof +++ " is not assignable to zero or equivalent";
}
fn void? VTree.init(&tree, usz size, Allocator allocator)
@ -217,6 +225,8 @@ fn usz? VTree.subtree_size(&tree, isz ref)
return count;
}
fn bool? VTree.is_root(&tree, isz node) => node == tree.parentof(node)!;
// iterate through the first level children, use a cursor like strtok_r
fn isz? VTree.children_it(&tree, isz parent, isz *cursor)
{

436
test/test_tree_layout.c3 Normal file
View File

@ -0,0 +1,436 @@
import vtree;
import std::io;
import std::math;
import std::thread;
const short WIDTH = 128;
const short HEIGHT = 64;
struct Size {
short min, max;
}
macro Size @grow() => {.min = 0, .max = 0};
macro Size @exact(short s) => {.min = s, .max = s};
macro Size @fit(short min = 0, short max = short.max) => {.min = min, .max = max};
macro bool Size.@is_grow(s) => (s.min == 0 && s.max == 0);
macro bool Size.@is_exact(s) => (s.min == s.max && s.min != 0);
macro bool Size.@is_fit(s) => (s.min != s.max);
struct Rect {
short x, y, w, h;
}
enum LayoutDirection {
ROW,
COLUMN
}
enum ElemType {
DIV,
ELEM
}
enum Anchor {
TOP_LEFT,
LEFT,
BOTTOM_LEFT,
BOTTOM,
BOTTOM_RIGHT,
RIGHT,
TOP_RIGHT,
TOP,
CENTER
}
struct Elem {
ElemType type;
Size w, h;
Rect bounds;
Size ch_w, ch_h; // children width / height
uint grow_children; // how many children want to grow, decreased once a child has grown
short orig_x, orig_y;
short occupied; // occupied space in the layout direction
LayoutDirection layout_dir;
Anchor anchor;
}
alias ElemTree = vtree::VTree{Elem*};
char[HEIGHT][WIDTH] screen;
fn void paint(Rect bounds, char c)
{
for (short x = bounds.x; x < WIDTH && x < bounds.x + bounds.w; x++) {
for (short y = bounds.y; y < HEIGHT && y < bounds.y + bounds.h; y++) {
screen[x][y] = c;
}
}
}
fn isz Elem.div_start(&e, ElemTree* tree, isz parent, Size w, Size h, LayoutDirection dir = ROW, Anchor anchor = TOP_LEFT, char c = ' ')
{
e.type = DIV;
e.w = w;
e.h = h;
e.layout_dir = dir;
e.anchor = anchor;
e.grow_children = 0;
e.occupied = 0;
e.ch_w = e.ch_h = {};
e.orig_x = e.orig_y = 0;
// update grow children if necessary
Elem* p = tree.get(parent) ?? &&{};
if ((p.layout_dir == ROW && e.w.@is_grow()) || ((p.layout_dir == COLUMN && e.h.@is_grow()))) {
p.grow_children++;
}
paint(e.bounds, c);
return tree.add(e, parent)!!;
}
fn void update_parent_size(Elem* parent, Elem* child)
{
// update the parent children size
switch (parent.layout_dir) {
case ROW: // on rows grow the ch width by the child width and only grow ch height if it exceeds
parent.ch_w.min += child.w.min;
parent.ch_w.max += child.w.max;
parent.ch_h.min = math::max(child.h.min, parent.ch_h.min);
parent.ch_h.max = math::max(child.h.max, parent.ch_h.max);
case COLUMN: // do the opposite on column
parent.ch_w.min = math::max(child.w.min, parent.ch_w.min);
parent.ch_w.max = math::max(child.w.max, parent.ch_w.max);
parent.ch_h.min += child.h.min;
parent.ch_h.max += child.h.max;
}
}
fn isz Elem.div_end(&e, ElemTree* tree, isz node)
{
isz parent = tree.parentof(node) ?? -1;
if (parent > 0) {
Elem* p = tree.get(parent)!!;
update_parent_size(p, e);
}
return parent;
}
fn void resolve_dimensions(Elem* e, Elem* p)
{
// ASSIGN WIDTH
switch {
case e.w.@is_exact():
e.bounds.w = e.w.min;
case e.w.@is_grow():
break;
// done in another pass
case e.w.@is_fit(): // fit the element's children
short min = math::max(e.ch_w.min, e.w.min);
short max = math::min(e.ch_w.max, e.w.max);
if (max >= min) { // OK!
e.bounds.w = max;
} else {
unreachable("cannot fit children");
}
default: unreachable("width is not exact, grow or fit");
}
// ASSIGN HEIGHT
switch {
case e.h.@is_exact():
e.bounds.h = e.h.min;
case e.h.@is_grow():
break;
// done in another pass
case e.h.@is_fit(): // fit the element's children
short min = math::max(e.ch_h.min, e.h.min);
short max = math::min(e.ch_h.max, e.h.max);
if (max >= min) { // OK!
e.bounds.h = max;
} else {
unreachable("cannot fit children");
}
default: unreachable("width is not exact, grow or fit");
}
switch (p.layout_dir) {
case ROW:
if (!e.w.@is_grow()) p.occupied += e.bounds.w;
case COLUMN:
if (!e.h.@is_grow()) p.occupied += e.bounds.h;
}
}
fn void resolve_grow_elements(Elem* e, Elem* p)
{
// WIDTH
if (e.w.@is_grow()) {
if (p.layout_dir == ROW) { // grow along the axis, divide the parent size
e.bounds.w = (short)((int)(p.bounds.w - p.occupied) / (int)p.grow_children);
p.grow_children--;
p.occupied += e.bounds.w;
} else if (p.layout_dir == COLUMN) { // grow across the layout axis, inherit width of the parent
e.bounds.w = p.bounds.w;
}
}
// HEIGHT
if (e.h.@is_grow()) {
if (p.layout_dir == COLUMN) { // grow along the axis, divide the parent size
e.bounds.h = (short)((int)(p.bounds.h - p.occupied) / (int)p.grow_children);
p.grow_children--;
p.occupied += e.bounds.h;
} else if (p.layout_dir == ROW) { // grow across the layout axis, inherit width of the parent
e.bounds.h = p.bounds.h;
}
}
}
fn void resolve_placement(Elem* e, Elem* p)
{
switch (p.anchor) {
case TOP_LEFT:
e.bounds.x = p.bounds.x + p.orig_x;
e.bounds.y = p.bounds.y + p.orig_y;
case LEFT:
e.bounds.x = p.bounds.x + p.orig_x;
e.bounds.y = p.bounds.y + p.orig_y + p.bounds.h/2;
if (p.layout_dir == COLUMN) {
e.bounds.y -= p.occupied/2;
} else if (p.layout_dir == ROW) {
e.bounds.y -= e.bounds.h/2;
}
case BOTTOM_LEFT:
e.bounds.x = p.bounds.x + p.orig_x;
e.bounds.y = p.bounds.y + p.bounds.h + p.orig_y;
if (p.layout_dir == COLUMN) {
e.bounds.y -= p.occupied;
} else if (p.layout_dir == ROW) {
e.bounds.y -= e.bounds.h;
}
case BOTTOM:
e.bounds.x = p.bounds.x + p.orig_x + p.bounds.w/2;
e.bounds.y = p.bounds.y + p.bounds.h + p.orig_y;
if (p.layout_dir == COLUMN) {
e.bounds.y -= p.occupied;
e.bounds.x -= e.bounds.w/2;
} else if (p.layout_dir == ROW) {
e.bounds.y -= e.bounds.h;
e.bounds.x -= p.occupied/2;
}
case BOTTOM_RIGHT:
e.bounds.x = p.bounds.x + p.orig_x + p.bounds.w;
e.bounds.y = p.bounds.y + p.bounds.h + p.orig_y;
if (p.layout_dir == COLUMN) {
e.bounds.y -= p.occupied;
e.bounds.x -= e.bounds.w;
} else if (p.layout_dir == ROW) {
e.bounds.y -= e.bounds.h;
e.bounds.x -= p.occupied;
}
case RIGHT:
e.bounds.x = p.bounds.x + p.orig_x + p.bounds.w;
e.bounds.y = p.bounds.y + p.orig_y + p.bounds.h/2;
if (p.layout_dir == COLUMN) {
e.bounds.y -= p.occupied/2;
e.bounds.x -= e.bounds.w;
} else if (p.layout_dir == ROW) {
e.bounds.y -= e.bounds.h/2;
e.bounds.x -= p.occupied;
}
case TOP_RIGHT:
e.bounds.x = p.bounds.x + p.orig_x + p.bounds.w;
e.bounds.y = p.bounds.y + p.orig_y;
if (p.layout_dir == COLUMN) {
e.bounds.x -= e.bounds.w;
} else if (p.layout_dir == ROW) {
e.bounds.x -= p.occupied;
}
case TOP:
e.bounds.x = p.bounds.x + p.orig_x + p.bounds.w/2;
e.bounds.y = p.bounds.y + p.orig_y;
if (p.layout_dir == COLUMN) {
e.bounds.x -= e.bounds.w/2;
} else if (p.layout_dir == ROW) {
e.bounds.x -= p.occupied/2;
}
case CENTER:
e.bounds.x = p.bounds.x + p.orig_x + p.bounds.w/2;
e.bounds.y = p.bounds.y + p.orig_y + p.bounds.h/2;
if (p.layout_dir == COLUMN) {
e.bounds.x -= e.bounds.w/2;
e.bounds.y -= p.occupied/2;
} else if (p.layout_dir == ROW) {
e.bounds.x -= p.occupied/2;
e.bounds.y -= e.bounds.h/2;
}
break;
}
/*
e.bounds.x = p.bounds.x + p.orig_x;
e.bounds.y = p.bounds.y + p.orig_y;
*/
switch (p.layout_dir) {
case ROW:
p.orig_x += e.bounds.w;
case COLUMN:
p.orig_y += e.bounds.h;
default: unreachable("unknown layout direction");
}
}
fn void frame_end(ElemTree* tree, isz root)
{
// assign the element bounds
isz cursor = -1;
/*
// RESOLVE DIMENSIONS
isz current = tree.level_order_it(root, &cursor)!!;
for (; current >= 0; current = tree.level_order_it(root, &cursor)!!) {
Elem* e = tree.get(current)!!;
isz pi = tree.parentof(current)!!;
Elem* p = (pi != current) ? tree.get(pi) ?? &&{} : &&{};
resolve_dimensions(e, p);
}
// RESOLVE GROW ELEMENTS
cursor = -1;
current = tree.level_order_it(root, &cursor)!!;
for (; current >= 0; current = tree.level_order_it(root, &cursor)!!) {
Elem* e = tree.get(current)!!;
isz pi = tree.parentof(current)!!; if (ch == current) continue;
Elem* p = (pi != current) ? tree.get(pi) ?? &&{} : &&{};
resolve_grow_elements(e, p);
}
// RESOLVE PLACEMENT
cursor = -1;
current = tree.level_order_it(root, &cursor)!!;
for (; current >= 0; current = tree.level_order_it(root, &cursor)!!) {
Elem* e = tree.get(current)!!;
isz pi = tree.parentof(current)!!;
Elem* p = (pi != current) ? tree.get(pi) ?? &&{} : &&{};
resolve_placement(e, p);
}
*/
cursor = -1;
isz current = tree.level_order_it(root, &cursor)!!;
for (; current >= 0; current = tree.level_order_it(root, &cursor)!!) {
Elem* p = tree.get(current)!!;
// RESOLVE KNOWN DIMENSIONS
isz ch_cur = 0;
isz ch = tree.children_it(current, &ch_cur)!!;
for (; ch >= 0; ch = tree.children_it(current, &ch_cur)!!) {
Elem* c = tree.get(ch)!!;
if (tree.is_root(ch)!!) {
resolve_dimensions(p, &&{});
} else {
resolve_dimensions(c, p);
}
}
// RESOLVE GROW CHILDREN
ch_cur = 0;
ch = tree.children_it(current, &ch_cur)!!;
for (; ch >= 0; ch = tree.children_it(current, &ch_cur)!!) {
Elem* c = tree.get(ch)!!;
if (tree.is_root(ch)!!) {
resolve_grow_elements(p, &&{});
} else {
resolve_grow_elements(c, p);
}
}
// RESOLVE CHILDREN PLACEMENT
ch_cur = 0;
ch = tree.children_it(current, &ch_cur)!!;
for (; ch >= 0; ch = tree.children_it(current, &ch_cur)!!) {
Elem* c = tree.get(ch)!!;
if (tree.is_root(ch)!!) {
resolve_placement(p, &&{});
} else {
resolve_placement(c, p);
}
}
}
}
fn void main()
{
ElemTree tree;
tree.init(64, mem)!!;
isz parent;
defer (void)tree.free();
Elem root; // root div
Elem div1, div2, div3, div4;
usz frame;
while (true) {
parent = root.div_start(&tree, parent, @exact(WIDTH), @exact(HEIGHT), ROW, anchor: RIGHT);
/*
{
parent = div1.div_start(&tree, parent, @grow(), @grow(), dir: ROW, c: '1');
{
parent = div4.div_start(&tree, parent, @exact(30), @exact(30), dir: ROW, c: '4');
parent = div4.div_end(&tree, parent);
}
parent = div1.div_end(&tree, parent);
if (frame < 200) {
parent = div2.div_start(&tree, parent, @exact(20), @fit(), dir: COLUMN, c: '2');
{
parent = div3.div_start(&tree, parent, @exact(10), @exact(10), dir: ROW, c: '3');
parent = div3.div_end(&tree, parent);
}
parent = div2.div_end(&tree, parent);
}
}
*/
parent = div3.div_start(&tree, parent, @fit(), @fit(), COLUMN, anchor: CENTER);
{
parent = div1.div_start(&tree, parent, @exact(20), @exact(20), dir: ROW, c: '1');
parent = div1.div_end(&tree, parent);
parent = div2.div_start(&tree, parent, @exact(10), @exact(10), dir: ROW, c: '2');
parent = div2.div_end(&tree, parent);
}
parent = div3.div_end(&tree, parent);
parent = root.div_end(&tree, parent);
frame_end(&tree, parent);
tree.nuke();
// draw the screen
//io::print("\e[1;1H\e[2J");
for (short x = 0; x < WIDTH+2; x++) io::printf("%c", x == 0 || x == WIDTH+1 ? '+' : '-');
io::printn();
for (short y = 0; y < HEIGHT; y++) {
io::print("|");
for (short x = 0; x < WIDTH; x++) {
char c = screen[x][y] == 0 ? 'x' : screen[x][y];
io::printf("%c", c);
}
io::print("|");
io::printn();
}
for (short x = 0; x < WIDTH+2; x++) io::printf("%c", x == 0 || x == WIDTH+1 ? '+' : '-');
io::printn("\n\n");
thread::sleep_ms(10);
frame++;
}
}