ugui.c3l/src/layout.c3

345 lines
9.8 KiB
Plaintext
Raw Normal View History

2024-10-29 22:45:47 +01:00
module ugui;
import std::math;
import std::io;
enum LayoutDirection {
ROW,
COLUMN,
2024-12-19 00:29:30 +01:00
}
2024-10-29 22:45:47 +01:00
enum Anchor {
TOP_LEFT,
LEFT,
BOTTOM_LEFT,
BOTTOM,
BOTTOM_RIGHT,
RIGHT,
TOP_RIGHT,
TOP,
CENTER
2024-10-29 22:45:47 +01:00
}
struct Layout {
Size w, h; // size of the CONTENT, does not include margin, border and padding
2025-09-12 22:18:15 +02:00
struct children { // the children size includes the children's margin/border/pading
Size w, h;
}
2025-09-06 23:07:43 +02:00
TextSize text;
ushort grow_children;
short occupied;
2025-09-29 23:51:02 +02:00
Point origin;
Point scroll_offset;
2025-09-17 22:45:03 +02:00
// false: the element is laid out according to the parent
// true: the element is laid out separate from all other children and the relative position to
// the parent is the .origin field
bool absolute;
LayoutDirection dir; // the direction the children are laid out
Anchor anchor; // how the children are positioned
Rect content_offset; // combined effect of margin, border and padding
2024-10-29 22:45:47 +01:00
}
2025-09-06 23:07:43 +02:00
// Returns the width and height of a @FIT() element based on it's wanted size (min/max)
// and the content size, this function is used to both update the parent's children size and
// give the dimensions of a fit element
2025-09-12 12:47:00 +02:00
// TODO: test and cleanup this function
2025-09-06 23:07:43 +02:00
macro Point Layout.get_dimensions(&el)
{
2025-09-12 22:18:15 +02:00
Point dim;
2025-09-12 12:47:00 +02:00
// if the direction is ROW then the text is placed horizontally with the children
if (el.dir == ROW) {
Size content_width = el.children.w + el.text.width;
Size width = el.w.combine(content_width);
short final_width = width.greater();
2025-09-12 22:18:15 +02:00
2025-09-12 12:47:00 +02:00
short text_height;
if (el.text.area != 0) {
short text_width = (@exact(final_width) - el.children.w).combine(el.text.width).min;
text_height = @exact((short)(el.text.area / text_width)).combine(el.text.height).min;
}
2025-09-12 22:18:15 +02:00
2025-09-12 12:47:00 +02:00
Size content_height = el.children.h.comb_max(@exact(text_height));
Size height = el.h.combine(content_height);
short final_height = height.greater();
2025-09-12 22:18:15 +02:00
dim = {
2025-09-12 12:47:00 +02:00
.x = final_width + el.content_offset.x + el.content_offset.w,
.y = final_height + el.content_offset.y + el.content_offset.h,
};
} else {
// if the direction is COLUMN the text and children are one on top of the other
Size content_width = el.children.w.comb_max(el.text.width);
Size width = el.w.combine(content_width);
short final_width = width.greater();
2025-09-12 22:18:15 +02:00
2025-09-12 12:47:00 +02:00
short text_height;
if (el.text.area != 0) {
short text_width = @exact(final_width).combine(el.text.width).min;
text_height = @exact((short)(el.text.area / text_width)).combine(el.text.height).min;
}
2025-09-12 22:18:15 +02:00
2025-09-12 12:47:00 +02:00
Size content_height = el.children.h + @exact(text_height);
Size height = el.h.combine(content_height);
short final_height = height.greater();
2025-09-12 22:18:15 +02:00
dim = {
2025-09-12 12:47:00 +02:00
.x = final_width + el.content_offset.x + el.content_offset.w,
.y = final_height + el.content_offset.y + el.content_offset.h,
};
2025-09-06 23:07:43 +02:00
}
2025-09-12 22:18:15 +02:00
// GROSS HACK FOR EXACT DIMENSIONS
if (el.w.@is_exact()) dim.x = el.w.min + el.content_offset.x + el.content_offset.w;
if (el.h.@is_exact()) dim.y = el.h.min + el.content_offset.y + el.content_offset.h;
2025-09-14 20:32:31 +02:00
// GROSS HACK FOR GROW DIMENSIONS
// FIXME: does this always work?
if (el.w.@is_grow()) dim.x = 0;
if (el.h.@is_grow()) dim.y = 0;
2025-09-14 20:32:31 +02:00
2025-09-12 22:18:15 +02:00
return dim;
2025-09-06 23:07:43 +02:00
}
// The content space of the element
macro Point Elem.content_space(&e)
{
return {
.x = e.bounds.w - e.layout.content_offset.x - e.layout.content_offset.w,
.y = e.bounds.h - e.layout.content_offset.y - e.layout.content_offset.h,
2024-10-29 22:45:47 +01:00
};
2025-09-29 23:51:02 +02:00
}
// Update the parent element's children size
fn void update_parent_size(Elem* child, Elem* parent)
2024-10-29 22:45:47 +01:00
{
Layout* cl = &child.layout;
2025-09-12 22:18:15 +02:00
Layout* pl = &parent.layout;
2025-09-17 22:45:03 +02:00
// if the element has absolute position do not update the parent
if (cl.absolute) return;
2025-09-06 23:07:43 +02:00
Point child_size = cl.get_dimensions();
2024-10-29 22:45:47 +01:00
switch (pl.dir) {
case ROW: // on rows grow the ch width by the child width and only grow ch height if it exceeds
2025-09-06 23:07:43 +02:00
pl.children.w += @exact(child_size.x);
pl.children.h = pl.children.h.comb_max(@exact(child_size.y));
2025-09-14 20:32:31 +02:00
if (child.layout.w.@is_grow()) parent.layout.grow_children++;
case COLUMN: // do the opposite on column
2025-09-06 23:07:43 +02:00
pl.children.w = pl.children.w.comb_max(@exact(child_size.x));
pl.children.h += @exact(child_size.y);
2025-09-14 20:32:31 +02:00
if (child.layout.h.@is_grow()) parent.layout.grow_children++;
}
}
fn void update_children_bounds(Elem* child, Elem* parent)
{
2025-09-29 23:51:02 +02:00
if (child.layout.absolute) return;
parent.children_bounds = containing_rect(child.bounds + parent.layout.scroll_offset, parent.bounds);
2024-10-29 22:45:47 +01:00
}
macro Rect Elem.content_bounds(&elem) => elem.bounds.pad(elem.layout.content_offset);
2024-12-26 22:58:24 +01:00
// Assign the width and height of an element in the directions that it doesn't need to grow
fn void resolve_dimensions(Elem* e, Elem* p)
2024-10-29 22:45:47 +01:00
{
2025-09-06 12:50:36 +02:00
Layout* el = &e.layout;
Layout* pl = &p.layout;
2024-10-29 22:45:47 +01:00
2025-09-12 22:18:15 +02:00
Point elem_dimensions = el.get_dimensions();
e.bounds.w = elem_dimensions.x;
e.bounds.h = elem_dimensions.y;
2025-09-29 23:51:02 +02:00
2025-09-17 22:45:03 +02:00
// if the element has absolute position do not update the parent
if (el.absolute) return;
2025-09-06 12:50:36 +02:00
switch (pl.dir) {
case ROW:
2025-09-06 12:50:36 +02:00
if (!el.w.@is_grow()) pl.occupied += e.bounds.w;
case COLUMN:
2025-09-06 12:50:36 +02:00
if (!el.h.@is_grow()) pl.occupied += e.bounds.h;
}
}
fn void resolve_grow_elements(Elem* e, Elem* p)
{
// WIDTH
if (e.layout.w.@is_grow()) {
2025-09-29 23:24:19 +02:00
if (e.layout.absolute) { // absolute children do not need to share space
e.bounds.w = p.content_space().x;
} else if (p.layout.dir == ROW) { // grow along the axis, divide the parent size
short slot = (short)((p.content_space().x - p.layout.occupied) / p.layout.grow_children);
e.bounds.w = slot;
p.layout.grow_children--;
p.layout.occupied += slot;
} else if (p.layout.dir == COLUMN) { // grow across the layout axis, inherit width of the parent
e.bounds.w = p.content_space().x;
}
}
2025-09-12 22:18:15 +02:00
// HEIGHT
if (e.layout.h.@is_grow()) {
2025-09-29 23:24:19 +02:00
if (e.layout.absolute) { // absolute children do not need to share space
e.bounds.h = p.content_space().y;
} else if (p.layout.dir == COLUMN) { // grow along the axis, divide the parent size
short slot = (short)((p.content_space().y - p.layout.occupied) / p.layout.grow_children);
e.bounds.h = slot;
p.layout.grow_children--;
p.layout.occupied += slot;
} else if (p.layout.dir == ROW) { // grow across the layout axis, inherit width of the parent
e.bounds.h = p.content_space().y;
}
}
}
fn void resolve_placement(Elem* c, Elem* p)
{
Layout* pl = &p.layout;
Layout* cl = &c.layout;
Point off = {
2025-09-14 20:32:31 +02:00
.x = p.bounds.x + pl.origin.x + pl.content_offset.x,
.y = p.bounds.y + pl.origin.y + pl.content_offset.y,
2024-11-14 23:42:20 +01:00
};
2025-09-17 22:45:03 +02:00
// if the element has absolute position assign the origin and do not update the parent
if (cl.absolute) {
c.bounds.x = p.bounds.x + pl.content_offset.x + cl.origin.x;
c.bounds.y = p.bounds.x + pl.content_offset.x + cl.origin.y;
return;
}
switch (pl.anchor) {
case TOP_LEFT:
c.bounds.x = off.x;
c.bounds.y = off.y;
case LEFT:
c.bounds.x = off.x;
2025-09-14 20:32:31 +02:00
c.bounds.y = off.y + p.content_space().y/2;
if (pl.dir == COLUMN) {
c.bounds.y -= pl.occupied/2;
} else if (pl.dir == ROW) {
c.bounds.y -= c.bounds.h/2;
}
case BOTTOM_LEFT:
c.bounds.x = off.x;
2025-09-14 20:32:31 +02:00
c.bounds.y = off.y + p.content_space().y ;
if (pl.dir == COLUMN) {
c.bounds.y -= pl.occupied;
} else if (pl.dir == ROW) {
c.bounds.y -= c.bounds.h;
}
case BOTTOM:
2025-09-14 20:32:31 +02:00
c.bounds.x = off.x + p.content_space().x/2;
c.bounds.y = off.y + p.content_space().y;
if (pl.dir == COLUMN) {
c.bounds.y -= pl.occupied;
c.bounds.x -= c.bounds.w/2;
} else if (pl.dir == ROW) {
c.bounds.y -= c.bounds.h;
c.bounds.x -= pl.occupied/2;
}
case BOTTOM_RIGHT:
2025-09-14 20:32:31 +02:00
c.bounds.x = off.x + p.content_space().x;
c.bounds.y = off.y + p.content_space().y;
if (pl.dir == COLUMN) {
c.bounds.y -= pl.occupied;
c.bounds.x -= c.bounds.w;
} else if (pl.dir == ROW) {
c.bounds.y -= c.bounds.h;
c.bounds.x -= pl.occupied;
}
case RIGHT:
2025-09-14 20:32:31 +02:00
c.bounds.x = off.x + p.content_space().x;
c.bounds.y = off.y + p.content_space().y/2;
if (pl.dir == COLUMN) {
c.bounds.y -= pl.occupied/2;
c.bounds.x -= c.bounds.w;
} else if (pl.dir == ROW) {
c.bounds.y -= c.bounds.h/2;
c.bounds.x -= pl.occupied;
}
case TOP_RIGHT:
2025-09-14 20:32:31 +02:00
c.bounds.x = off.x + p.content_space().x;
c.bounds.y = off.y;
if (pl.dir == COLUMN) {
c.bounds.x -= c.bounds.w;
} else if (pl.dir == ROW) {
c.bounds.x -= pl.occupied;
}
case TOP:
2025-09-14 20:32:31 +02:00
c.bounds.x = off.x + p.content_space().x/2;
c.bounds.y = off.y;
if (pl.dir == COLUMN) {
c.bounds.x -= c.bounds.w/2;
} else if (pl.dir == ROW) {
c.bounds.x -= pl.occupied/2;
}
case CENTER:
2025-09-14 20:32:31 +02:00
c.bounds.x = off.x + p.content_space().x/2;
c.bounds.y = off.y + p.content_space().y/2;
if (pl.dir == COLUMN) {
c.bounds.x -= c.bounds.w/2;
c.bounds.y -= pl.occupied/2;
} else if (pl.dir == ROW) {
c.bounds.x -= pl.occupied/2;
c.bounds.y -= c.bounds.h/2;
}
break;
}
switch (pl.dir) {
case ROW:
pl.origin.x += c.bounds.w;
case COLUMN:
pl.origin.y += c.bounds.h;
default: unreachable("unknown layout direction");
}
}
2025-10-03 15:19:52 +02:00
fn void? Ctx.layout_element_tree(&ctx)
{
int current;
for (int n; (current = ctx.tree.level_order_it(0, n)) >= 0; n++) {
2025-10-03 15:19:52 +02:00
Elem* p = ctx.find_elem(ctx.tree.get(current))!;
2025-09-29 23:51:02 +02:00
p.layout.origin = -p.layout.scroll_offset;
int ch;
// RESOLVE KNOWN DIMENSIONS
for (int i; (ch = ctx.tree.children_it(current, i)) >= 0; i++) {
2025-10-03 15:19:52 +02:00
Elem* c = ctx.find_elem(ctx.tree.get(ch))!;
if (ctx.tree.is_root(ch)) {
resolve_dimensions(p, &&{});
} else {
resolve_dimensions(c, p);
}
}
2024-10-29 22:45:47 +01:00
// RESOLVE GROW CHILDREN
for (int i; (ch = ctx.tree.children_it(current, i)) >= 0; i++) {
2025-10-03 15:19:52 +02:00
Elem* c = ctx.find_elem(ctx.tree.get(ch))!;
if (ctx.tree.is_root(ch)) {
resolve_grow_elements(p, &&{});
} else {
resolve_grow_elements(c, p);
}
}
2025-09-12 22:18:15 +02:00
// RESOLVE CHILDREN PLACEMENT
for (int i; (ch = ctx.tree.children_it(current, i)) >= 0; i++) {
2025-10-03 15:19:52 +02:00
Elem* c = ctx.find_elem(ctx.tree.get(ch))!;
if (ctx.tree.is_root(ch)) {
resolve_placement(p, &&{});
2025-09-14 20:32:31 +02:00
update_children_bounds(p, &&{});
} else {
resolve_placement(c, p);
update_children_bounds(c, p);
}
2025-10-10 22:31:28 +02:00
// FIXME: this stuff would be better elsewhere but we are already iteraring through all
// elements so here it fits really well
ctx.update_hover_and_focus(c);
}
2024-11-17 21:36:46 +01:00
}
2024-10-29 22:45:47 +01:00
}