diff --git a/lib/ugui.c3l/LAYOUT b/lib/ugui.c3l/LAYOUT new file mode 100644 index 0000000..33c804e --- /dev/null +++ b/lib/ugui.c3l/LAYOUT @@ -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 +======= + diff --git a/lib/ugui.c3l/src/vtree.c3 b/lib/ugui.c3l/src/vtree.c3 index 7901a58..fdc94eb 100644 --- a/lib/ugui.c3l/src/vtree.c3 +++ b/lib/ugui.c3l/src/vtree.c3 @@ -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) { diff --git a/test/test_tree_layout.c3 b/test/test_tree_layout.c3 new file mode 100644 index 0000000..37d9920 --- /dev/null +++ b/test/test_tree_layout.c3 @@ -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++; + } +} \ No newline at end of file