Compare commits

..

4 Commits

40 changed files with 1470 additions and 3644 deletions

View File

@ -1,9 +1,3 @@
C3FLAGS = -g
main: src/main.c3 src/renderer.c3 $(wildcard lib/ugui/src/*.c3)
make -C resources/shaders
c3c build ${C3FLAGS}
test_renderer: test_renderer.c3 src/renderer.c3 resources/shaders/source/*
scripts/compile_shaders.sh
c3c compile -g -O0 test_renderer.c3 src/renderer.c3 --libdir lib --lib sdl3 --lib ugui

55
TODO
View File

@ -5,7 +5,7 @@
[x] Port font system from C to C3 (rewrite1)
[ ] Update ARCHITECTURE.md
[ ] Write a README.md
[x] Use an arena allocator for cache
[ ] Use an arena allocator for cache
[ ] Do not redraw if there was no update (no layout and no draw)
[ ] Do command buffer damage tracking based on a context grid (see rxi writeup)
[x] Better handling of the active and focused widgets, try
@ -15,12 +15,10 @@ to maintain focus until mouse release (fix scroll bars)
[x] Implement a z index and sort command buffer based on that
[ ] Ctx.set_z_index()
[x] Sort command buffer on insertion
[x] Standardize element handling, for example all buttons do almost the same thing, so write a lot
of boiler plate and reuse it
[x] The id combination in gen_id() uses an intger division, which is costly, use another combination
function that is non-linear and doesn't use division
[x] Standardize element handling, for example all buttons do almost the same thing, so write a lot of boiler plate and reuse it
[x] The id combination in gen_id() uses an intger division, which is costly, use another combination function that is non-linear and doesn't use division
[ ] Animations, somehow
[x] Maybe cache codepoint converted strings
[ ] Maybe cache codepoint converted strings
[x] Fix scroll wheel when div is scrolled
[ ] Be consistent with the initialization methods some are foo.new() and some are foo.init()
[ ] Implement image loading (.bmp, .ff, .qoi and .png), in the future even lossy images like .jpg
@ -41,37 +39,20 @@ to maintain focus until mouse release (fix scroll bars)
each struct Elem, struct Cmd, etc. is as big as the largest element. It would
be better to use a different allcation strategy.
[ ] Add a way to handle time events like double clicks
[x] Fix how padding is applied in push_rect. In CSS padding is applied between the border and the
content, the background color is applied starting from the border. Right now push_rect() offsets
the background rect by both border and padding
[x] Investigate why the debug pointer (cyan rectangle) disappears...
[ ] Border and padding do not go well together if the library issues two rect commands, the visible
border is effectively the border size plus the padding since there is a gap between the border
rect and the internal rect. A better solution is to leave it up to the renderer to draw the rect
correctly
## Layout
[x] Flexbox
[x] Center elements to the row/column
[x] Text wrapping / reflow
[x] Implement a better and unified way to place a glyph and get the cursor position, maybe with a struct
[ ] Correct whitespace handling in text (\t \r etc)
[x] Consider a multi-pass recursive approach to layout (like https://github.com/nicbarker/clay)
[ ] For some reason padding is not correct, look at the sliders, they have 2px per side when the
theme specifies 4px per side
[ ] Center elements to the row/column
[ ] Text wrapping / reflow
[ ] Consider a multi-pass recursive approach to layout (like https://github.com/nicbarker/clay)
instead of the curren multi-frame approach.
[x] Implement column/row sizing (min, max)
[x] Implement a way to size the element as the current row/column size
* +-------------+
* | |
* +-------------+
* +--+
* | |
* +--+
* <------------->
* column size
See the calculator example for why it is useful
[ ] Find a way to concile pixel measurements to the mm ones used in css, for example in min/max sizing
of elements
[x] Center elements to div (center children_bounds to the center of the div bounds and shift the origin accordingly)
[x] Use containing_rect() in position_element() to skip some computing and semplify the function
[x] Rename position_element() to layout_element()
[x] Make functions to mark rows/columns as full, to fix the calculator demo
## Input
@ -80,8 +61,6 @@ to maintain focus until mouse release (fix scroll bars)
[ ] Touch input
[x] Do not set input event to true if the movement was zero (like no mouse movement)
[ ] Use input event flags, for example to consume the input event
[ ] Fix bug in text box: when spamming keys you can get multiple characters in the text input field
of the context, this causes a bug where only the first char is actually used
## Commands
@ -90,9 +69,8 @@ to maintain focus until mouse release (fix scroll bars)
- border radius
[x] add a command to update an atlas
[ ] New window command, useful for popups
[x] Text command returns the text bounds, this way we can avoid the pattern
[ ] Text command returns the text bounds, this way we can avoid the pattern
draw_text(a, pos) -> off = compute_bounds(a) -> draw_text(b, pos+off) -> ...
[ ] Rounded rectangle with different radius for each corner
## Atlas
@ -115,6 +93,11 @@ to maintain focus until mouse release (fix scroll bars)
[x] Checkbox
[ ] Selectable text box
## Main / exaple
[ ] Create maps from ids to textures and images instead of hardcoding them
## API
[ ] Introduce a Layout structure that specifies the positioning of elements inside

@ -1 +1 @@
Subproject commit e7356df5d1d0c22a6bba822bda6994062b0b75d7
Subproject commit 076355e2d126e7546e53663b97e8dec22667d34d

View File

@ -1,243 +0,0 @@
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
+--------------------+ +----------------------------------------------------+
| +----------------+ | |+----------------+ |
| | | | || |+------------+ |
| | | | || || |+------------------+|
| | E1 | | || E1 || E2 || E3 ||
| | | | || || |+------------------+|
| | | | || |+------------+ |
| +----------------+ | |+----------------+ |
| +------------+ | +----------------------------------------------------+
| | | |
| | E2 | |
| | | | (both have center alignment)
| +------------+ |
|+------------------+|
|| ||
|| 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
=======
The element bounds include the whole CSS box model:
+---------------------------------------------+
| MARGIN |
| +-----------------------------------+ |
| |xxxxxxxxxxx BORDER xxxxxxxxxxxx| |
| |x+-------------------------------+x| |
| |x| PADDING |x| |
| |x| +-----------------------+ |x| |
| |x| | | |x| |
| |x| | CONTENT | |x| |
| |x| | | |x| |
| |x| +-----------------------+ |x| |
| |x| |x| |
| |x+-------------------------------+x| |
| |xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx| |
| +-----------------------------------+ |
| |
+---------------------------------------------+
Styling happens via a .css file, the sizing strictly refers to the content, so if the the user
requests an exact size of 100px*100px the content box will have those dimensions, but the element
bounds will be larger.

View File

@ -9,6 +9,10 @@ module cache{Key, Value, SIZE};
* the elements that were not recently used.
*/
// FIXME: this module should really allocate all resources on an arena or temp
// allocator, since all memory allocations are connected and freeing
// happens at the same time
import std::core::mem;
import std::core::mem::allocator;
import std::collections::bitset;
@ -21,7 +25,6 @@ alias IdTableEntry = map::Entry{Key, usz};
const usz CACHE_NCYCLES = (usz)(SIZE * 2.0/3.0);
struct Cache {
Allocator allocator;
BitArr present, used;
IdTable table;
Value[] pool;
@ -40,20 +43,19 @@ macro Cache.cycle(&cache) @private {
}
}
fn void? Cache.init(&cache, Allocator allocator)
fn void? Cache.init(&cache)
{
cache.allocator = allocator;
cache.table.init(allocator, capacity: SIZE);
cache.table.init(allocator::heap(), capacity: SIZE);
// FIXME: this shit is SLOW
foreach (idx, bit : cache.used) { cache.used[idx] = false; }
foreach (idx, bit : cache.present) { cache.present[idx] = false; }
cache.pool = allocator::new_array(allocator, Value, SIZE);
cache.pool = mem::new_array(Value, SIZE);
}
fn void Cache.free(&cache)
{
(void)cache.table.free();
(void)allocator::free(cache.allocator, cache.pool);
(void)mem::free(cache.pool);
}
fn Value*? Cache.search(&cache, Key id)
@ -75,7 +77,6 @@ fn Value*? Cache.search(&cache, Key id)
}
/* HIT, set as recently used */
//io::printfn("HIT: %d [%d]", entry.value, entry.key);
cache.used[entry.value] = true;
return &(cache.pool[entry.value]);
}
@ -96,19 +97,16 @@ fn void Cache.remove(&cache, Key id)
/* If there is no free space left then just return the first position */
fn usz Cache.get_free_spot(&cache) @private
{
// TODO: in the upgrade to c3 1.7.5 use @bitsof()
const BITS = $typeof(cache.present.data[0]).sizeof*8;
foreach (idx, d: cache.present.data) {
if (d != $typeof(d).max) {
usz spot = idx*BITS + BITS-d.clz();
if (cache.used[spot]) unreachable("free spot is not actually free: %d", spot);
return spot;
if (d.clz() != BITS) {
return idx*BITS + BITS-d.clz();
}
}
return 0;
}
fn Value*? Cache.insert_at(&cache, Value* g, Key id, usz index) @private
fn Value*? Cache.insert_at(&cache, Value *g, Key id, usz index) @private
{
// TODO: verify index, g and id
Value* spot;

View File

@ -9,23 +9,21 @@ import std::sort;
// TODO: specify the allocator
struct Fifo {
Allocator allocator;
Type[] arr;
usz out;
usz count;
}
fn void? Fifo.init(&fifo, usz size, Allocator allocator)
fn void? Fifo.init(&fifo, usz size)
{
fifo.allocator = allocator;
fifo.arr = allocator::new_array(fifo.allocator, Type, size);
fifo.arr = mem::new_array(Type, size);
fifo.out = 0;
fifo.count = 0;
}
fn void Fifo.free(&fifo)
{
(void)allocator::free(fifo.allocator, fifo.arr);
(void)mem::free(fifo.arr);
}
fn void? Fifo.enqueue(&fifo, Type *elem)
@ -71,8 +69,8 @@ macro usz Fifo.len(&fifo) @operator(len)
fn void? Fifo.sort(&fifo)
{
Type[] arr = allocator::new_array(fifo.allocator, Type, fifo.count);
defer allocator::free(fifo.allocator, arr);
Type[] arr = mem::new_array(Type, fifo.count);
defer mem::free(arr);
foreach(i, c: fifo) {
arr[i] = c;

View File

@ -1,343 +0,0 @@
module mtree{Type};
/* ================================================================================================
* MTree, Bitmap-based tree
* ================================================================================================
*
* Overview
* --------
* The MTree is a bitmap-based tree structure composed of three core elements:
* - Element Vector: Stores user data.
* - Reference Node Vector: Manages node relationships.
* - Bitmap: Marks used indices.
*
* The name "MTree" originates from "Matrix Tree," where the vector is divided into
* sectors of power-of-two sizes. Each node's bitmap marks the positions of its
* children within the same sector.
*
* If a parent and its children are in different sectors, a new node is created.
* The parent's "next" field points to this new node, forming a chain that must
* be traversed during iteration.
*
*
* Example (sector size = 8)
* -------------------------
*
* _________________________________
* |__ __ _______________________ |
* | | | | _ |
* | v v vv |v
* +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
* refs_vec:| 0| 1| 2| 3| 4| 5| 6| 7| 8| 9|10|11|12|13|14|15|16|...
* +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
* \__________ __________/ \__________ __________/ \__
* V V
* sector 0 sector 1
*
*
* Node Relationships:
* -------------------
* - Root (Element 0) has three direct children: 1, 2, and 10.
* - Node 10 is in a different sector than the root, so root.next points to Node 11.
* - Node 11 has Node 10 as a direct child and Node 0 (root) as its parent.
*
* Bitmap Representation:
* ---------------------
*
* root = {
* .parent = -1; // Root has no parent
* .next = 11; // Points to Node 11
* .children = 0b00000110; // [0|1|1|0|0|0|0|0] (Children: 1, 2)
* }
*
* node11 = {
* .parent = 0; // Parent is root (Node 0)
* .next = -1; // Last in the chain
* .children = 0b00000100; // [0|0|1|0|0|0|0|0] (Child: 10)
* }
*
* ================================================================================================
*/
import std::core::mem;
import std::core::mem::allocator;
import std::io;
import std::bits;
import std::collections::list;
alias Bitmap = ulong;
const BITS = Bitmap.sizeof*8;
alias IdxList = List{int};
// next: if positive it contains the index of the next node that contains the children information
struct RefNode {
int next;
int parent;
Bitmap children;
}
struct MTree {
usz elements;
Allocator allocator;
IdxList queue;
Bitmap[] used;
Type[] elem_vec; // element vector
RefNode[] refs_vec; // relationship vector
}
fn void MTree.init(&tree, usz size, Allocator allocator = mem)
{
// round size to the nearest multiple of BITS
size = size + size%BITS;
tree.elements = 0;
tree.allocator = allocator;
tree.queue.init(tree.allocator, size);
tree.used = allocator::new_array(tree.allocator, Bitmap, size/BITS);
tree.elem_vec = allocator::new_array(tree.allocator, Type, size);
tree.refs_vec = allocator::new_array(tree.allocator, RefNode, size);
foreach (&r: tree.refs_vec) {
r.next = -1;
}
}
fn void MTree.free(&tree)
{
tree.elements = 0;
tree.queue.free();
(void)allocator::free(tree.allocator, tree.used);
(void)allocator::free(tree.allocator, tree.elem_vec);
(void)allocator::free(tree.allocator, tree.refs_vec);
}
fn int MTree.get_free_spot(&tree)
{
foreach (idx, d: tree.used) {
if (d != $typeof(d).max) {
int spot = (int)idx*BITS + BITS-(int)d.clz();
return spot;
}
}
unreachable("no free spots left");
}
<* @require idx >= 0 *>
macro void MTree.set_used(&tree, int idx)
{
int r = idx % BITS;
int q = idx / BITS;
tree.used[q] |= (1l << r);
}
<* @require idx >= 0 *>
macro void MTree.unset_used(&tree, int idx)
{
int r = idx % BITS;
int q = idx / BITS;
tree.used[q] &= ~(1l << r);
}
<* @require idx >= 0 *>
macro bool MTree.is_used(&tree, int idx)
{
int r = idx % BITS;
int q = idx / BITS;
return !!(tree.used[q] & (1l << r));
}
// get the last node in the "next" chain
<* @require tree.is_used(parent) == true *>
fn int MTree.last_node(&tree, int parent)
{
while(tree.refs_vec[parent].next >= 0) {
parent = tree.refs_vec[parent].next;
}
return parent;
}
<* @require tree.elements == 0 || tree.is_used(parent) == true *>
fn int MTree.add(&tree, int parent, Type t)
{
int idx = tree.get_free_spot();
int subtree = idx / BITS;
tree.set_used(idx);
tree.elem_vec[idx] = t;
tree.refs_vec[idx] = (RefNode){
.parent = parent,
.next = -1,
};
tree.elements++;
// root element, has no parent
if (tree.elements == 1) {
tree.refs_vec[idx].parent = -1;
return idx;
}
// if the parent already has a node in the same subtree as the child then update that node's
// children bitmap
bool done;
for (int p = parent; p >= 0; p = tree.refs_vec[p].next) {
int ps = p/BITS;
if (ps == subtree) {
tree.refs_vec[p].children |= (1l << (idx%BITS));
done = true;
break;
}
}
// on fail we need to create another parent node
if (!done) {
int new_next = tree.get_free_spot();
// if the new node does not land in the same subtree as the child we cannot do
// anything since the references are immutable
if (new_next/BITS != subtree) {
unreachable("cannot allocate new child for parent");
}
tree.set_used(new_next);
tree.elements++;
// update the "next" chain
int last_link = tree.last_node(parent);
tree.refs_vec[last_link].next = new_next;
tree.refs_vec[new_next].next = -1;
tree.refs_vec[new_next].children |= (long)(1 << (idx%BITS));
tree.refs_vec[new_next].parent = last_link;
// FIXME: the elem_vec is not updated, do we need to?
}
return idx;
}
// get the index of the n-th children of parent, -1 otherwise
// usage: for (int i, c; (c = tree.children_it(parent, i)) >= 0; i++) { ... }
fn int MTree.children_it(&tree, int parent, int n)
{
int tot_children;
int child;
for (int p = parent; p >= 0; p = tree.refs_vec[p].next) {
int cn = (int)tree.refs_vec[p].children.popcount();
tot_children += cn;
// we are in the right subtree
if (tot_children > n) {
child = (p/BITS) * BITS; // start at the parent's subtree index
int j = cn - (tot_children - n); // we need the j-th children of this node
Bitmap u = tree.refs_vec[p].children;
child += j; // add the children number
do {
child += (int)u.ctz(); // increment by the skipped zeroes
u >>= u.ctz() + 1;
j--;
} while (j >= 0);
return child;
}
}
return -1;
}
fn int MTree.children_num(&tree, int parent)
{
int n;
for (int p = parent; p >= 0; p = tree.refs_vec[p].next) {
n += (int)tree.refs_vec[p].children.popcount();
}
return n;
}
fn int MTree.subtree_size(&tree, int parent)
{
int x = tree.children_num(parent);
int c;
for (int n; (c = tree.children_it(parent, n)) >= 0; n++) {
x += tree.subtree_size(c);
}
return x;
}
fn int MTree.level_order_it(&tree, int parent, int i)
{
if (i == 0) {
tree.queue.clear();
tree.queue.push(parent);
}
if (tree.queue.len() == 0) return -1;
int p = tree.queue.pop_first()!!;
int c;
for (int n; (c = tree.children_it(p, n)) >= 0; n++) {
tree.queue.push(c);
}
return p;
}
fn void MTree.prune(&tree, int parent)
{
int c;
for (int i = 0; (c = tree.children_it(parent, i)) >= 0; i++) {
tree.prune(c); // prune the subtree
// delete all children including their next chain
for (int p = c; p >= 0;) {
int next = tree.refs_vec[p].next;
tree.unset_used(p);
tree.refs_vec[p] = {.next = -1};
p = next;
}
}
// finally delete the parent
for (int p = parent; p >= 0;) {
int next = tree.refs_vec[p].next;
tree.unset_used(p);
tree.elements--;
tree.refs_vec[p] = {.next = -1};
p = next;
}
}
<* @require tree.is_used(ref) *>
fn Type MTree.get(&tree, int ref) => tree.elem_vec[ref];
<* @require tree.is_used(ref) *>
fn Type MTree.parentof(&tree, int ref) => tree.refs_vec[ref].parent;
fn void MTree.nuke(&tree)
{
foreach (idx, &b: tree.used) {
*b = 0;
tree.refs_vec[idx] = {.next = -1};
}
tree.elements = 0;
}
macro bool MTree.is_root(&t, int i) => t.refs_vec[i].parent == -1;
fn void MTree.print(&tree)
{
foreach (idx, c: tree.elem_vec) {
if (tree.is_used((int)idx)) {
io::printfn("[%d](%s) parent:%d next:%d children:%b",
idx, c, tree.refs_vec[idx].parent, tree.refs_vec[idx].next,
tree.refs_vec[idx].children
);
}
}
}

View File

@ -0,0 +1,187 @@
module ugui;
import std::io;
// button element
struct ElemButton {
int filler;
}
// draw a button, return the events on that button
macro Ctx.button(&ctx, Rect size, bool state = false, ...)
=> ctx.button_id(@compute_id($vasplat), size, state);
fn ElemEvents? Ctx.button_id(&ctx, Id id, Rect size, bool active)
{
id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
Style* style_norm = ctx.styles.get_style(@str_hash("button"));
elem.bounds = ctx.position_element(parent, size, style_norm);
// if the bounds are null the element is outside the div view,
// no interaction should occur so just return
if (elem.bounds.is_null()) { return {}; }
Style* style_active = ctx.styles.get_style(@str_hash("button-active"));
elem.events = ctx.get_elem_events(elem);
bool is_active = active || ctx.elem_focus(elem) || elem.events.mouse_hover;
// Draw the button
ctx.push_rect(elem.bounds, parent.div.z_index, is_active ? style_active : style_norm)!;
return elem.events;
}
macro Ctx.button_label(&ctx, String label, Rect size = {0,0,short.max,short.max}, bool active = false, ...)
=> ctx.button_label_id(@compute_id($vasplat), label, size, active);
fn ElemEvents? Ctx.button_label_id(&ctx, Id id, String label, Rect size, bool active)
{
id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
Rect text_size = ctx.get_text_bounds(label)!;
Rect btn_size = text_size.add({0,0,10,10});
Style* style_norm = ctx.styles.get_style(@str_hash("button"));
// 2. Layout
elem.bounds = ctx.position_element(parent, btn_size, style_norm);
if (elem.bounds.is_null()) { return {}; }
Style* style_active = ctx.styles.get_style(@str_hash("button-active"));
elem.events = ctx.get_elem_events(elem);
bool is_active = active || ctx.elem_focus(elem) || elem.events.mouse_hover;
Style* style = is_active ? style_active : style_norm;
// Draw the button
text_size.x = elem.bounds.x;
text_size.y = elem.bounds.y;
Point off = ctx.center_text(text_size, elem.bounds);
text_size.x += off.x;
text_size.y += off.y;
ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
ctx.push_string(text_size, label, parent.div.z_index, style.fg)!;
return elem.events;
}
macro Ctx.button_icon(&ctx, String icon, String on_icon = "", bool active = false, ...)
=> ctx.button_icon_id(@compute_id($vasplat), icon, on_icon, active);
fn ElemEvents? Ctx.button_icon_id(&ctx, Id id, String icon, String on_icon, bool active)
{
id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
Sprite* def_sprite = ctx.sprite_atlas.get(icon)!;
Sprite* on_sprite = ctx.sprite_atlas.get(on_icon) ?? &&(Sprite){};
Rect max_size = def_sprite.rect().max(on_sprite.rect());
Style* style_norm = ctx.styles.get_style(@str_hash("button"));
elem.bounds = ctx.position_element(parent, max_size, style_norm);
// if the bounds are null the element is outside the div view,
// no interaction should occur so just return
if (elem.bounds.is_null()) { return {}; }
Style* style_active = ctx.styles.get_style(@str_hash("button-active"));
elem.events = ctx.get_elem_events(elem);
bool is_active = active || ctx.elem_focus(elem) || elem.events.mouse_hover;
Style* style = is_active ? style_active : style_norm;
Id tex_id = ctx.sprite_atlas.id;
if (active && on_icon != "") {
ctx.push_sprite(elem.bounds, on_sprite.uv(), tex_id, parent.div.z_index, type: on_sprite.type)!;
} else {
ctx.push_sprite(elem.bounds, def_sprite.uv(), tex_id, parent.div.z_index, type: def_sprite.type)!;
}
// Draw the button
ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
return elem.events;
}
// FIXME: this should be inside the style
macro Ctx.checkbox(&ctx, String desc, Point off, bool* active, String tick_sprite = {}, ...)
=> ctx.checkbox_id(@compute_id($vasplat), desc, off, active, tick_sprite);
fn void? Ctx.checkbox_id(&ctx, Id id, String description, Point off, bool* active, String tick_sprite)
{
id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
Style* style = ctx.styles.get_style(@str_hash("checkbox"));
Rect size = {off.x, off.y, style.size, style.size};
elem.bounds = ctx.position_element(parent, size, style);
// if the bounds are null the element is outside the div view,
// no interaction should occur so just return
if (elem.bounds.is_null()) return;
elem.events = ctx.get_elem_events(elem);
if (elem.events.mouse_hover && elem.events.mouse_release) *active = !(*active);
if (tick_sprite != {}) {
ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
if (*active) {
ctx.draw_sprite_raw(tick_sprite, elem.bounds, center: true)!;
}
} else {
ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
if (*active) {
ushort x = style.size / 4;
Rect check = elem.bounds.add({x, x, -x*2, -x*2});
Style s = *style;
s.bg = s.primary;
s.margin = s.padding = {};
s.border = 0;
ctx.push_rect(check, parent.div.z_index, &s)!;
}
}
}
// FIXME: this should be inside the style
macro Ctx.toggle(&ctx, String desc, Point off, bool* active)
=> ctx.toggle_id(@compute_id($vasplat), desc, off, active);
fn void? Ctx.toggle_id(&ctx, Id id, String description, Point off, bool* active)
{
id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
Style* style = ctx.styles.get_style(@str_hash("toggle"));
Rect size = {off.x, off.y, style.size*2, style.size};
elem.bounds = ctx.position_element(parent, size, style);
// if the bounds are null the element is outside the div view,
// no interaction should occur so just return
if (elem.bounds.is_null()) return;
elem.events = ctx.get_elem_events(elem);
if (elem.events.mouse_hover && elem.events.mouse_release) *active = !(*active);
// Draw the button
// FIXME: THIS IS SHIT
ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
Rect t = elem.bounds.add({*active ? (style.size+3) : +3, +3, -style.size-6, -6});
Style s = *style;
s.bg = s.primary;
s.margin = s.padding = {};
s.border = 0;
ctx.push_rect(t, parent.div.z_index, &s)!;
}

View File

@ -14,6 +14,7 @@ enum CmdType {
// command to draw a rect
struct CmdRect {
Rect rect;
ushort thickness;
ushort radius;
Color color;
}
@ -58,26 +59,7 @@ fn int Cmd.compare_to(Cmd a, Cmd b)
// implement the Printable interface
fn usz? Cmd.to_format(Cmd* cmd, Formatter *f) @dynamic
{
usz ret;
ret += f.printf("Cmd{ type: %s, z_index: %d, ", cmd.type, cmd.z_index)!;
switch (cmd.type) {
case CMD_RECT:
ret += f.print("CmdRect")!;
ret += io::struct_to_format(cmd.rect, f, false)!;
case CMD_SCISSOR:
ret += f.print("CmdScissor")!;
ret += io::struct_to_format(cmd.scissor, f, false)!;
case CMD_SPRITE:
ret += f.print("CmdSprite")!;
ret += io::struct_to_format(cmd.sprite, f, false)!;
case CMD_UPDATE_ATLAS:
ret += f.print("CmdUpdateAtlas")!;
ret += io::struct_to_format(cmd.update_atlas, f, false)!;
}
ret += f.print("}")!;
return ret;
return f.printf("Cmd{ type: %s, z_index: %d }", cmd.type, cmd.z_index);
}
macro bool cull_rect(Rect rect, Rect clip = {0,0,short.max,short.max})
@ -96,11 +78,7 @@ macro Ctx.push_cmd(&ctx, Cmd *cmd, int z_index)
case CMD_SPRITE: rect = cmd.sprite.rect;
default: return ctx.cmd_queue.enqueue(cmd);
}
if (cull_rect(rect, ctx.div_scissor)) {
// println("NOPE: ", cmd.rect.rect, cmd.z_index);
// unreachable();
return;
}
if (cull_rect(rect, ctx.div_scissor)) return;
return ctx.cmd_queue.enqueue(cmd);
}
@ -113,22 +91,21 @@ fn void? Ctx.push_scissor(&ctx, Rect rect, int z_index)
ctx.push_cmd(&sc, z_index)!;
}
fn void? Ctx.reset_scissor(&ctx, int z_index) => ctx.push_cmd(&&(Cmd){.type=CMD_SCISSOR,.scissor.rect=ctx.div_scissor}, z_index)!;
fn void? Ctx.push_rect(&ctx, Rect rect, int z_index, Style* style)
{
Rect border = style.border;
Rect padding = style.padding;
ushort border = style.border;
ushort radius = style.radius;
Color bg = style.bg;
Color border_color = style.secondary;
// FIXME: this implies that the border has to be uniform
if (!border.is_null()) {
if (border != 0) {
Cmd cmd = {
.type = CMD_RECT,
.rect.rect = rect,
.rect.color = border_color,
.rect.radius = radius + border.x,
.rect.radius = radius+border,
.rect.thickness = border,
};
ctx.push_cmd(&cmd, z_index)!;
}
@ -136,18 +113,19 @@ fn void? Ctx.push_rect(&ctx, Rect rect, int z_index, Style* style)
Cmd cmd = {
.type = CMD_RECT,
.rect.rect = {
.x = rect.x + border.x,
.y = rect.y + border.y,
.w = rect.w - (border.x+border.w),
.h = rect.h - (border.y+border.h),
.x = rect.x + border + padding.x,
.y = rect.y + border + padding.y,
.h = rect.h - (border*2) - (padding.y+padding.h),
.w = rect.w - (border*2) - (padding.x+padding.w),
},
.rect.color = bg,
.rect.radius = radius,
.rect.thickness = max(rect.w, rect.h)/2+1,
};
if (cull_rect(cmd.rect.rect, ctx.div_scissor)) return;
ctx.push_cmd(&cmd, z_index)!;
}
// TODO: accept a Sprite* instead of all this shit
fn void? Ctx.push_sprite(&ctx, Rect bounds, Rect texture, Id texture_id, int z_index, Color hue = 0xffffffffu.to_rgba(), SpriteType type = SPRITE_NORMAL)
{
Cmd cmd = {
@ -161,6 +139,58 @@ fn void? Ctx.push_sprite(&ctx, Rect bounds, Rect texture, Id texture_id, int z_i
ctx.push_cmd(&cmd, z_index)!;
}
fn void? Ctx.push_string(&ctx, Rect bounds, char[] text, int z_index, Color hue)
{
if (text.len == 0) {
return;
}
ctx.push_scissor(bounds, z_index)!;
short baseline = (short)ctx.font.ascender;
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
short line_gap = (short)ctx.font.linegap;
Id texture_id = ctx.font.id; // or ctx.font.atlas.id
Point orig = {
.x = bounds.x,
.y = bounds.y,
};
short line_len;
Codepoint cp;
usz off, x;
while (off < text.len && (cp = str_to_codepoint(text[off..], &x)) != 0) {
off += x;
Glyph* gp;
if (!ascii::is_cntrl((char)cp)) {
gp = ctx.font.get_glyph(cp)!;
Rect gb = {
.x = orig.x + line_len + gp.ox,
.y = orig.y + gp.oy + baseline,
.w = gp.w,
.h = gp.h,
};
Rect gt = {
.x = gp.u,
.y = gp.v,
.w = gp.w,
.h = gp.h,
};
// push the sprite only if it collides with the bounds
if (!cull_rect(gb, bounds)) ctx.push_sprite(gb, gt, texture_id, z_index, hue)!;
line_len += gp.adv;
} else if (cp == '\n'){
orig.y += line_height + line_gap;
line_len = 0;
} else {
continue;
}
}
// FIXME: we never get here if an error was thrown before
ctx.push_scissor({}, z_index)!;
}
fn void? Ctx.push_update_atlas(&ctx, Atlas* atlas)
{
Cmd up = {

View File

@ -1,6 +1,6 @@
module ugui;
import mtree;
import vtree;
import cache;
import fifo;
@ -9,15 +9,6 @@ import std::core::string;
import std::core::mem::allocator;
macro println(...)
{
$for var $i = 0; $i < $vacount; $i++:
io::print($vaexpr[$i]);
$endfor
io::printn();
}
// element ids are just long ints
alias Id = uint;
@ -38,26 +29,23 @@ bitstruct ElemFlags : uint {
bitstruct ElemEvents : uint {
bool key_press : 0;
bool key_release : 1;
bool key_repeat : 2;
bool key_hold : 2;
bool mouse_hover : 3;
bool mouse_press : 4;
bool mouse_release : 5;
bool mouse_hold : 6;
bool update : 7;
bool text_input : 8;
bool has_focus : 9;
}
// element structure
struct Elem {
Id id;
int tree_idx;
isz tree_idx;
ElemFlags flags;
ElemEvents events;
Rect bounds;
Rect children_bounds;
ElemType type;
Layout layout;
union {
ElemDiv div;
ElemButton button;
@ -69,7 +57,7 @@ struct Elem {
// relationships between elements are stored in a tree, it stores just the ids
alias IdTree = mtree::MTree{Id};
alias IdTree = vtree::VTree{Id};
// elements themselves are kept in a cache
const uint MAX_ELEMENTS = 256;
@ -77,7 +65,7 @@ alias ElemCache = cache::Cache{Id, Elem, MAX_ELEMENTS};
alias CmdQueue = fifo::Fifo{Cmd};
faultdef INVALID_SIZE, EVENT_UNSUPPORTED, WRONG_ELEMENT_TYPE, WRONG_ID;
faultdef INVALID_SIZE, EVENT_UNSUPPORTED, UNEXPECTED_ELEMENT, WRONG_ELEMENT_TYPE, WRONG_ID;
const Rect DIV_FILL = { .x = 0, .y = 0, .w = 0, .h = 0 };
@ -122,17 +110,14 @@ struct Ctx {
Id focus_id;
Rect div_scissor; // the current div bounds used for scissor test
int active_div; // tree node indicating the current active div
isz active_div; // tree node indicating the current active div
}
// return a pointer to the parent of the current active div
fn Elem*? Ctx.get_parent(&ctx)
{
Id parent_id = ctx.tree.get(ctx.active_div);
Elem*? parent = ctx.cache.search(parent_id);
if (catch parent) return parent;
if (parent.type != ETYPE_DIV) return WRONG_ELEMENT_TYPE?;
return parent;
Id parent_id = ctx.tree.get(ctx.active_div)!;
return ctx.cache.search(parent_id);
}
macro @bits(#a) => $typeof(#a).sizeof*8;
@ -143,8 +128,7 @@ const uint GOLDEN_RATIO = 0x9E3779B9;
// with the Cantor pairing function
fn Id? Ctx.gen_id(&ctx, Id id2)
{
// FIXME: this is SHIT
Id id1 = ctx.tree.get(ctx.active_div);
Id id1 = ctx.tree.get(ctx.active_div)!;
// Mix the two IDs non-linearly
Id mixed = id1 ^ id2.rotate_left(13);
mixed ^= id1.rotate_left(7);
@ -166,19 +150,19 @@ macro Id @compute_id(...)
// resets all flags except is_new which is set accordingly
fn Elem*? Ctx.get_elem(&ctx, Id id, ElemType type)
{
Elem empty_elem;
bool is_new;
Elem* elem;
elem = ctx.cache.get_or_insert(&&(Elem){}, id, &is_new)!;
elem = ctx.cache.get_or_insert(&empty_elem, id, &is_new)!;
elem.flags = (ElemFlags)0;
elem.flags.is_new = is_new;
elem.id = id;
elem.layout = {};
if (is_new == false && elem.type != type) {
return WRONG_ELEMENT_TYPE?;
} else {
elem.type = type;
}
elem.tree_idx = ctx.tree.add(ctx.active_div, id);
elem.tree_idx = ctx.tree.add(id, ctx.active_div)!;
return elem;
}
@ -196,22 +180,22 @@ macro Elem* Ctx.find_elem(&ctx, Id id)
fn Elem*? Ctx.get_active_div(&ctx)
{
Id id = ctx.tree.get(ctx.active_div);
Id id = ctx.tree.get(ctx.active_div)!;
return ctx.cache.search(id);
}
fn void? Ctx.init(&ctx, Allocator allocator)
fn void? Ctx.init(&ctx)
{
ctx.tree.init(MAX_ELEMENTS, allocator);
ctx.tree.init(MAX_ELEMENTS)!;
defer catch { (void)ctx.tree.free(); }
ctx.cache.init(allocator)!;
ctx.cache.init()!;
defer catch { (void)ctx.cache.free(); }
ctx.cmd_queue.init(MAX_CMDS, allocator)!;
ctx.cmd_queue.init(MAX_CMDS)!;
defer catch { (void)ctx.cmd_queue.free(); }
ctx.styles.init(allocator);
ctx.styles.init(allocator::heap());
ctx.styles.register_style(&DEFAULT_STYLE, @str_hash("default"));
defer catch { ctx.styles.free(); }
@ -245,15 +229,16 @@ fn void? Ctx.frame_begin(&ctx)
//elem.flags.has_focus = ctx.has_focus;
elem.bounds = {0, 0, ctx.width, ctx.height};
elem.div.layout = LAYOUT_ROW;
elem.div.z_index = 0;
elem.div.children_bounds = elem.bounds;
elem.div.scroll_x.enabled = false;
elem.div.scroll_y.enabled = false;
elem.layout.dir = ROW;
elem.layout.anchor = TOP_LEFT;
elem.layout.w = @exact(ctx.width);
elem.layout.h = @exact(ctx.height);
elem.div.pcb = {};
elem.div.origin_c = {};
elem.div.origin_r = {};
ctx.div_scissor = elem.bounds;
ctx.div_scissor = {0, 0, ctx.width, ctx.height};
// The root element does not push anything to the stack
// TODO: add a background color taken from a theme or config
@ -265,13 +250,7 @@ fn void? Ctx.frame_end(&ctx)
{
// FIXME: this is not guaranteed to be root. the user might forget to close a div or some other element
Elem* root = ctx.get_active_div()!;
if (root.id != ROOT_ID) {
io::printn(root.id);
return WRONG_ID?;
}
// DO THE LAYOUT
ctx.layout_element_tree();
root.div.layout = LAYOUT_ROW;
// 1. clear the tree
ctx.tree.nuke();
@ -308,13 +287,7 @@ $if DEBUG == 1:
$endif
// sort the command buffer by the z-index
// FIXME: sorting the buffer fucks with scissor commands that have to be kept in place
//ctx.cmd_queue.sort()!;
// foreach (i, c: ctx.cmd_queue) {
// io::printf("[%d]: ", i);
// io::printn(c);
// }
ctx.cmd_queue.sort()!;
}
<*
@ -336,7 +309,7 @@ macro bool Ctx.elem_focus(&ctx, Elem *elem)
fn ElemEvents Ctx.get_elem_events(&ctx, Elem *elem)
{
bool hover = ctx.is_hovered(elem);
bool focus = ctx.elem_focus(elem) || (hover && ctx.is_mouse_pressed(BTN_LEFT));
bool focus = ctx.focus_id == elem.id || (hover && ctx.is_mouse_pressed(BTN_LEFT));
if (ctx.is_mouse_pressed(BTN_ANY) && !hover){
focus = false;
@ -347,15 +320,11 @@ fn ElemEvents Ctx.get_elem_events(&ctx, Elem *elem)
if (focus) { ctx.focus_id = elem.id; }
ElemEvents ev = {
.has_focus = focus,
.mouse_hover = hover,
.mouse_press = hover && focus && ctx.is_mouse_pressed(BTN_ANY),
.mouse_hover = hover,
.mouse_press = hover && focus && ctx.is_mouse_pressed(BTN_ANY),
.mouse_release = hover && focus && ctx.is_mouse_released(BTN_ANY),
.mouse_hold = hover && focus && ctx.is_mouse_down(BTN_ANY),
.key_press = focus && ctx.input.events.key_press,
.key_release = focus && ctx.input.events.key_release,
.key_repeat = focus && ctx.input.events.key_repeat,
.text_input = focus && (ctx.input.keyboard.text_len || ctx.input.keyboard.modkeys & KMOD_TXT),
.mouse_hold = hover && focus && ctx.is_mouse_down(BTN_ANY),
.text_input = focus && (ctx.input.keyboard.text_len || ctx.input.keyboard.modkeys & KMOD_TXT),
};
return ev;
}

View File

@ -0,0 +1,153 @@
module ugui;
import std::io;
import std::math;
// div element
struct ElemDiv {
Layout layout;
struct scroll_x {
bool enabled;
bool on;
float value;
}
struct scroll_y {
bool enabled;
bool on;
float value;
}
ushort scroll_size;
int z_index;
Rect children_bounds; // current frame children bounds
Rect pcb; // previous frame children bounds
Point origin_r, origin_c;
}
// begin a widget container, or div, the size determines the offset (x,y) width and height.
// if the width or height are zero the width or height are set to the maximum available.
// if the width or height are negative the width or height will be calculated based on the children size
// sort similar to a flexbox, and the minimum size is set by the negative of the width or height
// FIXME: there is a bug if the size.w or size.h == -0
macro Ctx.div_begin(&ctx, Rect size, bool scroll_x = false, bool scroll_y = false, ...)
=> ctx.div_begin_id(@compute_id($vasplat), size, scroll_x, scroll_y);
fn void? Ctx.div_begin_id(&ctx, Id id, Rect size, bool scroll_x, bool scroll_y)
{
id = ctx.gen_id(id)!;
Elem* parent = ctx.get_parent()!;
Elem* elem = ctx.get_elem(id, ETYPE_DIV)!;
ctx.active_div = elem.tree_idx;
Style* style = ctx.styles.get_style(@str_hash("default"));
Style* slider_style = ctx.styles.get_style(@str_hash("slider"));
elem.div.scroll_x.enabled = scroll_x;
elem.div.scroll_y.enabled = scroll_y;
elem.div.scroll_size = slider_style.size ? slider_style.size : (style.size ? style.size : DEFAULT_STYLE.size);
elem.div.z_index = parent.div.z_index + 1;
// 2. layout the element
Rect wanted_size = {
.x = size.x,
.y = size.y,
.w = size.w < 0 ? max(elem.div.pcb.w, (short)-size.w) : size.w,
.h = size.h < 0 ? max(elem.div.pcb.h, (short)-size.h) : size.h,
};
elem.bounds = ctx.position_element(parent, wanted_size, style);
elem.div.children_bounds = {};
// update the ctx scissor
ctx.div_scissor = elem.bounds;
ctx.push_scissor(elem.bounds, elem.div.z_index)!;
// 4. Fill the div fields
elem.div.origin_c = {
.x = elem.bounds.x,
.y = elem.bounds.y
};
elem.div.origin_r = elem.div.origin_c;
elem.div.layout = parent.div.layout;
// Add the background to the draw stack
bool do_border = parent.div.layout == LAYOUT_FLOATING;
ctx.push_rect(elem.bounds, elem.div.z_index, style)!;
elem.events = ctx.get_elem_events(elem);
// TODO: check active
// TODO: check resizeable
}
fn void? Ctx.div_end(&ctx)
{
// swap the children bounds
Elem* parent = ctx.get_parent()!;
Elem* elem = ctx.get_active_div()!;
elem.div.pcb = elem.div.children_bounds;
// FIXME: this causes all elements inside the div to loose focus since the mouse press happens
// both inside the element and inside the div bounds
//elem.events = ctx.get_elem_events(elem);
Rect cb = elem.div.pcb;
// children bounds bottom-right corner
Point cbc = {
.x = cb.x + cb.w,
.y = cb.y + cb.h,
};
// div bounds bottom-right corner
Point bc = {
.x = elem.bounds.x + elem.bounds.w,
.y = elem.bounds.y + elem.bounds.h,
};
// set the scrollbar flag, is used in layout
// horizontal overflow
elem.div.scroll_x.on = cbc.x > bc.x && elem.div.scroll_x.enabled;
// vertical overflow
elem.div.scroll_y.on = cbc.y > bc.y && elem.div.scroll_y.enabled;
Id hsid_raw = @str_hash("div_scrollbar_horizontal");
Id vsid_raw = @str_hash("div_scrollbar_vertical");
Id hsid_real = ctx.gen_id(@str_hash("div_scrollbar_horizontal"))!;
Id vsid_real = ctx.gen_id(@str_hash("div_scrollbar_vertical"))!;
short wdim = elem.div.scroll_y.on ? (ctx.focus_id == vsid_real || ctx.is_hovered(ctx.find_elem(vsid_real)) ? elem.div.scroll_size*2 : elem.div.scroll_size) : 0;
short hdim = elem.div.scroll_x.on ? (ctx.focus_id == hsid_real || ctx.is_hovered(ctx.find_elem(hsid_real)) ? elem.div.scroll_size*2 : elem.div.scroll_size) : 0;
if (elem.div.scroll_y.on) {
if (ctx.input.events.mouse_scroll && ctx.hover_id == elem.id) {
elem.div.scroll_y.value += ctx.input.mouse.scroll.y * 0.07f;
elem.div.scroll_y.value = math::clamp(elem.div.scroll_y.value, 0.0f, 1.0f);
}
Rect vslider = {
.x = elem.bounds.x + elem.bounds.w - wdim,
.y = elem.bounds.y,
.w = wdim,
.h = elem.bounds.h - hdim,
};
Layout prev_l = elem.div.layout;
elem.div.layout = LAYOUT_ABSOLUTE;
ctx.slider_ver_id(vsid_raw, vslider, &elem.div.scroll_y.value, max((float)bc.y / cbc.y, (float)0.15))!;
elem.div.layout = prev_l;
}
if (elem.div.scroll_x.on) {
if (ctx.input.events.mouse_scroll && ctx.hover_id == elem.id) {
elem.div.scroll_x.value += ctx.input.mouse.scroll.x * 0.07f;
elem.div.scroll_x.value = math::clamp(elem.div.scroll_x.value, 0.0f, 1.0f);
}
Rect hslider = {
.x = elem.bounds.x,
.y = elem.bounds.y + elem.bounds.h - hdim,
.w = elem.bounds.w - wdim,
.h = hdim,
};
Layout prev_l = elem.div.layout;
elem.div.layout = LAYOUT_ABSOLUTE;
ctx.slider_hor_id(hsid_raw, hslider, &elem.div.scroll_x.value, max((float)bc.x / cbc.x, (float)0.15))!;
elem.div.layout = prev_l;
}
// the active_div returns to the parent of the current one
ctx.active_div = ctx.tree.parentof(ctx.active_div)!;
}

View File

@ -9,34 +9,11 @@ import std::io;
import std::ascii;
// ---------------------------------------------------------------------------------- //
// CODEPOINT //
// ---------------------------------------------------------------------------------- //
// unicode code point, different type for a different hash
alias Codepoint = uint;
<*
@require off != null
@require str.ptr != null
*>
fn Codepoint str_to_codepoint(char[] str, usz* off)
{
Codepoint cp;
isz b = grapheme::decode_utf8(str, str.len, (uint*)&cp);
if (b == 0 || b > str.len) {
return 0;
}
*off = b;
return cp;
}
//macro uint Codepoint.hash(self) => ((uint)self).hash();
// ---------------------------------------------------------------------------------- //
// FONT ATLAS //
// ---------------------------------------------------------------------------------- //
/* width and height of a glyph contain the kering advance
* (u,v)
* +-------------*---+ -
@ -83,7 +60,7 @@ struct Font {
fn void? Font.load(&font, String name, ZString path, uint height, float scale)
{
font.table.init(allocator::mem, capacity: FONT_CACHED);
font.table.init(allocator::heap(), capacity: FONT_CACHED);
font.id = name.hash();
font.size = height*scale;
@ -112,7 +89,6 @@ fn void? Font.load(&font, String name, ZString path, uint height, float scale)
ushort size = (ushort)font.size*(ushort)($$sqrt((float)FONT_CACHED));
font.atlas.new(font.id, ATLAS_GRAYSCALE, size, size)!;
// FIXME: this crashes with O1 or greater, why??
// preallocate the ASCII range
for (char c = ' '; c < '~'; c++) {
font.get_glyph((Codepoint)c)!;
@ -184,16 +160,93 @@ fn void Font.free(&font)
schrift::freefont(font.sft.font);
}
// ---------------------------------------------------------------------------------- //
// FONT LOAD AND QUERY //
// ---------------------------------------------------------------------------------- //
fn void? Ctx.load_font(&ctx, String name, ZString path, uint height, float scale = 1.0)
{
return ctx.font.load(name, path, height, scale);
}
<*
@require off != null
*>
fn Codepoint str_to_codepoint(char[] str, usz* off)
{
Codepoint cp;
isz b = grapheme::decode_utf8(str, str.len, (uint*)&cp);
if (b == 0 || b > str.len) {
return 0;
}
*off = b;
return cp;
}
fn Rect? Ctx.get_text_bounds(&ctx, String text)
{
Rect text_bounds;
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
short line_gap = (short)ctx.font.linegap;
text_bounds.h = line_height;
Glyph* gp;
// TODO: account for unicode codepoints
short line_len;
Codepoint cp;
usz off, x;
while ((cp = str_to_codepoint(text[off..], &x)) != 0) {
off += x;
bool n;
if (!ascii::is_cntrl((char)cp)) {
gp = ctx.font.get_glyph(cp)!;
line_len += gp.adv;
} else if (cp == '\n'){
text_bounds.h += line_height + line_gap;
line_len = 0;
} else {
continue;
}
if (line_len > text_bounds.w) {
text_bounds.w = line_len;
}
}
return text_bounds;
}
fn Point? Ctx.get_cursor_position(&ctx, String text)
{
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
short line_gap = (short)ctx.font.linegap;
Glyph* gp;
// TODO: account for unicode codepoints
Point line;
Codepoint cp;
usz off, x;
while ((cp = str_to_codepoint(text[off..], &x)) != 0) {
off += x;
bool n;
if (!ascii::is_cntrl((char)cp)) {
gp = ctx.font.get_glyph(cp)!;
line.x += gp.adv;
} else if (cp == '\n'){
line.y += line_height + line_gap;
line.x = 0;
} else {
continue;
}
}
return line;
}
fn Point Ctx.center_text(&ctx, Rect text_bounds, Rect bounds)
{
short dw = bounds.w - text_bounds.w;
short dh = bounds.h - text_bounds.h;
return {.x = dw/2, .y = dh/2};
}
// TODO: check if the font is present in the context
fn Id Ctx.get_font_id(&ctx, String label)
{
@ -211,266 +264,3 @@ fn Atlas*? Ctx.get_font_atlas(&ctx, String name)
}
fn int Font.line_height(&font) => (int)(font.ascender - font.descender + (float)0.5);
// ---------------------------------------------------------------------------------- //
// TEXT MEASUREMENT //
// ---------------------------------------------------------------------------------- //
const uint TAB_SIZE = 4;
struct TextSize {
Size width, height;
int area;
}
// Measeure the size of a string.
// width.min: as if each word is broken up by a new line
// width.max: the width of the string left as-is
// height.min: the height of the string left as-is
// height.max: the height of the string with each word broken up by a new line
fn TextSize? Ctx.measure_string(&ctx, String text)
{
if (text == "") return (TextSize){};
Font* font = &ctx.font;
short baseline = (short)font.ascender;
short line_height = (short)font.line_height();
short line_gap = (short)font.linegap;
short space_width = font.get_glyph(' ').adv!;
short tab_width = space_width * TAB_SIZE;
isz off;
usz x;
TextSize ts;
short word_width;
short words = 1;
Rect bounds; // unaltered text bounds;
Point origin;
Codepoint cp = str_to_codepoint(text[off..], &x);
for (; cp != 0; cp = str_to_codepoint(text[off..], &x)) {
off += x;
Glyph* gp = font.get_glyph(cp)!;
// update the text bounds
switch {
case cp == '\n':
origin.x = 0;
origin.y += line_height + line_gap;
case cp == '\t':
origin.x += tab_width;
case ascii::is_cntrl((char)cp):
break;
default:
Rect b = {
.x = origin.x + gp.ox,
.y = origin.y + gp.oy + baseline,
.w = gp.w,
.h = gp.h,
};
bounds = containing_rect(bounds, b);
origin.x += gp.adv;
}
// update the word width
switch {
case ascii::is_space((char)cp):
if (word_width > ts.width.min) ts.width.min = word_width;
word_width = 0;
words++;
default:
//word_width += gp.w + gp.ox;
if (off < text.len) {
word_width += gp.adv;
} else {
word_width += gp.w + gp.ox;
}
}
}
// end of string is also end of word
if (word_width > ts.width.min) ts.width.min = word_width;
ts.width.max = bounds.w;
ts.height.min = bounds.h;
ts.height.max = words * line_height + line_gap * (words-1);
ts.area = bounds.w * bounds.h;
return ts;
}
// layout a string inside a bounding box, following the given alignment (anchor).
// returns the position of the cursor, the returned height is the line height and the width is the last
// character's advance value
// TODO: implement a "reflow" flag to toggle reflow if a character goes out of bounds
// TODO: also return the total bounds of the laid out string
fn Rect? Ctx.layout_string(&ctx, String text, Rect bounds, Anchor anchor, int z_index, Color hue, isz cursor = -1)
{
Font* font = &ctx.font;
short line_height = (short)font.line_height();
Rect cursor_rect = {.h = line_height};
if (bounds.w <= 0 || bounds.h <= 0) return cursor_rect;
ctx.push_scissor(bounds, z_index)!;
if (text == "") {
// when text is empty but we need to draw a cursor use a visually empty string
if (cursor >= 0) {
text = "\f";
} else {
return cursor_rect;
}
}
Id texture_id = font.id;
short baseline = (short)font.ascender;
short line_gap = (short)font.linegap;
short space_width = font.get_glyph(' ').adv!;
short tab_width = space_width * TAB_SIZE;
Point origin = bounds.position();
// measure string height inside the bounds to center it vertically
// FIXME: this is fast but it just gives back the height in LINES, most lines have only short
// characters which would result in a lower height
int string_height = line_height;
foreach (c: text) {
string_height += (line_height + line_gap) * (int)(c == '\n');
}
switch (anchor) {
case TOP_LEFT: nextcase;
case TOP: nextcase;
case TOP_RIGHT:
origin.y += 0;
case LEFT: nextcase;
case CENTER: nextcase;
case RIGHT:
origin.y += (short)(bounds.h - string_height)/2;
case BOTTOM_LEFT: nextcase;
case BOTTOM: nextcase;
case BOTTOM_RIGHT:
origin.y += (short)(bounds.h - string_height);
}
// measure the line until it exits the bounds or the string ends
usz line_start, line_end;
do {
int line_width;
Point o = {.x = bounds.x, .y = bounds.y};
Codepoint cp;
isz off = line_start;
for ITER: (usz x; (cp = str_to_codepoint(text[off..], &x)) != 0; off += x) {
Glyph* gp = font.get_glyph(cp)!;
switch {
case cp == '\n':
off += x;
break ITER;
case cp == '\t':
o.x += tab_width;
case ascii::is_cntrl((char)cp):
break;
default:
Rect b = {
.x = o.x + gp.ox,
.y = o.y + gp.oy + baseline,
.w = gp.w,
.h = gp.h,
};
if (b.x + b.w > bounds.x + bounds.w) {
break ITER;
}
o.x += gp.adv;
line_width += gp.adv;
}
}
line_end = off;
if (line_end == line_start) unreachable("something went wrong in measuring the line");
// with the line width calculate the right origin and layout the line
origin.x = bounds.x;
short next_line_x = bounds.x; // the x coordinate of the origin if the line_width is zero
switch (anchor) {
case TOP_LEFT: nextcase;
case LEFT: nextcase;
case BOTTOM_LEFT:
// TODO: we didn't need to measure the line width with this alignment
origin.x += 0;
next_line_x += 0;
case TOP: nextcase;
case CENTER: nextcase;
case BOTTOM:
origin.x += (short)(bounds.w - line_width)/2+1;
next_line_x += bounds.w/2;
case TOP_RIGHT: nextcase;
case RIGHT: nextcase;
case BOTTOM_RIGHT:
origin.x += (short)(bounds.w - line_width);
next_line_x += bounds.w;
}
// reset the cursor to the line
if (line_start <= cursor && cursor <= line_end) {
cursor_rect.x = origin.x;
cursor_rect.y = origin.y;
cursor_rect.w = 0;
if (cursor && text[cursor-1] == '\n') {
cursor_rect.x = next_line_x;
cursor_rect.y += line_height;
}
}
Point line_origin = origin;
// see the fixme when measuring the height
//ctx.push_rect({.x = origin.x,.y=origin.y,.w=(short)line_width,.h=(short)string_height}, z_index, &&(Style){.bg=0xff000042u.@to_rgba()})!;
String line = text[line_start:line_end-line_start];
off = 0;
for (usz x; (cp = str_to_codepoint(line[off..], &x)) != 0 && off < line.len; off += x) {
Glyph* gp = font.get_glyph(cp)!;
switch {
case cp == '\t':
origin.x += tab_width;
case ascii::is_cntrl((char)cp):
break;
default:
Rect b = {
.x = origin.x + gp.ox,
.y = origin.y + gp.oy + baseline,
.w = gp.w,
.h = gp.h
};
Rect uv = {
.x = gp.u,
.y = gp.v,
.w = gp.w,
.h = gp.h
};
ctx.push_sprite(b, uv, texture_id, z_index, hue)!;
//ctx.push_rect(b, z_index, &&(Style){.bg=0x0000ff66u.@to_rgba()})!;
origin.x += gp.adv;
}
if (line_start + off < cursor && text[cursor-1] != '\n') {
cursor_rect.x = origin.x;
cursor_rect.y = origin.y;
cursor_rect.w = gp.adv;
}
}
// done with the line
line_start = line_end;
origin.y += line_height + line_gap;
} while(line_end < text.len);
ctx.reset_scissor(z_index)!;
return cursor_rect;
}

View File

@ -13,9 +13,6 @@ bitstruct InputEvents : uint {
bool mouse_scroll : 4; // mouse scroll wheel. x or y
bool text_input : 5;
bool mod_key : 6;
bool key_press : 7;
bool key_release : 8;
bool key_repeat : 9;
}
bitstruct MouseButtons : uint {
@ -69,13 +66,15 @@ const MouseButtons BTN_5 = {.btn_5 = true};
const ModKeys KEY_ANY = (ModKeys)(ModKeys.inner.max);
fn bool Ctx.check_key_combo(&ctx, ModKeys mod, String ...keys)
fn bool Ctx.check_key_combo(&ctx, ModKeys mod, String keys)
{
bool is_mod = (bool)(ctx.input.keyboard.modkeys & mod);
bool is_keys = true;
String haystack = (String)ctx.get_keys();
foreach (needle: keys) {
is_keys = is_keys && haystack.contains(needle);
String haystack = (String)ctx.input.keyboard.text[0..ctx.input.keyboard.text_len];
char[2] needle;
foreach (c: keys) {
needle[0] = c;
is_keys = is_keys && haystack.contains((String)needle[..]);
}
return is_mod && is_keys;
}
@ -157,21 +156,6 @@ fn void Ctx.input_mouse_wheel(&ctx, short x, short y, float scale = 1.0)
ctx.input.events.mouse_scroll = x !=0 || y != 0;
}
fn void Ctx.input_key_press(&ctx)
{
ctx.input.events.key_press = true;
}
fn void Ctx.input_key_release(&ctx)
{
ctx.input.events.key_release = true;
}
fn void Ctx.input_key_repeat(&ctx)
{
ctx.input.events.key_repeat = true;
}
// append utf-8 encoded text to the context text input
fn void Ctx.input_text_utf8(&ctx, char[] text)
{
@ -211,9 +195,6 @@ fn void Ctx.input_char(&ctx, char c)
ctx.input_text_utf8(b[..]);
}
fn String Ctx.get_keys(&ctx) => (String)ctx.input.keyboard.text[:ctx.input.keyboard.text_len];
fn ModKeys Ctx.get_mod(&ctx) => ctx.input.keyboard.modkeys;
// Modifier keys, like control or backspace
// TODO: make this call repetible to input modkeys one by one
fn void Ctx.input_mod_keys(&ctx, ModKeys modkeys)

View File

@ -1,146 +1,84 @@
module ugui;
import std::math;
import std::io;
enum LayoutDirection {
ROW,
COLUMN,
enum Layout {
LAYOUT_ROW,
LAYOUT_COLUMN,
LAYOUT_FLOATING,
LAYOUT_ABSOLUTE,
}
enum Anchor {
TOP_LEFT,
LEFT,
BOTTOM_LEFT,
BOTTOM,
BOTTOM_RIGHT,
RIGHT,
TOP_RIGHT,
TOP,
CENTER
}
struct Layout {
Size w, h; // size of the CONTENT, does not include margin, border and padding
struct children { // the children size includes the children's margin/border/pading
Size w, h;
}
TextSize text;
ushort grow_children;
short occupied;
struct origin {
short x, y;
}
// 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
}
// 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
// TODO: test and cleanup this function
macro Point Layout.get_dimensions(&el)
fn void? Ctx.layout_set_row(&ctx)
{
Point dim;
// 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();
Id parent_id = ctx.tree.get(ctx.active_div)!;
Elem *parent = ctx.cache.search(parent_id)!;
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;
}
Size content_height = el.children.h.comb_max(@exact(text_height));
Size height = el.h.combine(content_height);
short final_height = height.greater();
dim = {
.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();
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;
}
Size content_height = el.children.h + @exact(text_height);
Size height = el.h.combine(content_height);
short final_height = height.greater();
dim = {
.x = final_width + el.content_offset.x + el.content_offset.w,
.y = final_height + el.content_offset.y + el.content_offset.h,
};
if (parent.type != ETYPE_DIV) {
// what?
return UNEXPECTED_ELEMENT?;
}
// 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;
// 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;
return dim;
parent.div.layout = LAYOUT_ROW;
}
// The content space of the element
macro Point Elem.content_space(&e)
fn void? Ctx.layout_set_column(&ctx)
{
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,
Id parent_id = ctx.tree.get(ctx.active_div)!;
Elem *parent = ctx.cache.search(parent_id)!;
if (parent.type != ETYPE_DIV) {
// what?
return UNEXPECTED_ELEMENT?;
}
parent.div.layout = LAYOUT_COLUMN;
}
fn void? Ctx.layout_set_floating(&ctx)
{
Id parent_id = ctx.tree.get(ctx.active_div)!;
Elem *parent = ctx.cache.search(parent_id)!;
if (parent.type != ETYPE_DIV) {
// what?
return UNEXPECTED_ELEMENT?;
}
parent.div.layout = LAYOUT_FLOATING;
}
fn void? Ctx.layout_next_row(&ctx)
{
Id parent_id = ctx.tree.get(ctx.active_div)!;
Elem *parent = ctx.cache.search(parent_id)!;
if (parent.type != ETYPE_DIV) {
return UNEXPECTED_ELEMENT?;
}
parent.div.origin_r = {
.x = parent.bounds.x,
.y = parent.div.children_bounds.bottom_right().y,
};
}
parent.div.origin_c = parent.div.origin_r;
}
// Update the parent element's children size
fn void update_parent_size(Elem* child, Elem* parent)
fn void? Ctx.layout_next_column(&ctx)
{
Layout* cl = &child.layout;
Layout* pl = &parent.layout;
Id parent_id = ctx.tree.get(ctx.active_div)!;
Elem *parent = ctx.cache.search(parent_id)!;
// if the element has absolute position do not update the parent
if (cl.absolute) return;
Point child_size = cl.get_dimensions();
switch (pl.dir) {
case ROW: // on rows grow the ch width by the child width and only grow ch height if it exceeds
pl.children.w += @exact(child_size.x);
pl.children.h = pl.children.h.comb_max(@exact(child_size.y));
if (child.layout.w.@is_grow()) parent.layout.grow_children++;
case COLUMN: // do the opposite on column
pl.children.w = pl.children.w.comb_max(@exact(child_size.x));
pl.children.h += @exact(child_size.y);
if (child.layout.h.@is_grow()) parent.layout.grow_children++;
if (parent.type != ETYPE_DIV) {
return UNEXPECTED_ELEMENT?;
}
parent.div.origin_c = {
.x = parent.div.children_bounds.bottom_right().x,
.y = parent.bounds.y,
};
parent.div.origin_r = parent.div.origin_c;
}
fn void update_children_bounds(Elem* child, Elem* parent)
{
parent.children_bounds = containing_rect(child.bounds, parent.bounds);
}
macro Rect Elem.content_bounds(&elem) => elem.bounds.pad(elem.layout.content_offset);
/*
macro Rect Elem.get_view(&elem)
{
Rect off;
@ -159,199 +97,100 @@ macro Point Elem.get_view_off(&elem)
{
return elem.get_view().sub(elem.bounds).position();
}
*/
// 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)
// position the rectangle inside the parent according to the layout
// parent: parent div
// rect: the requested size
// style: apply style
<*
@require ctx != null
@require parent.type == ETYPE_DIV
*>
fn Rect Ctx.position_element(&ctx, Elem *parent, Rect rect, Style* style)
{
Layout* el = &e.layout;
Layout* pl = &p.layout;
ElemDiv* div = &parent.div;
Point elem_dimensions = el.get_dimensions();
e.bounds.w = elem_dimensions.x;
e.bounds.h = elem_dimensions.y;
// if the element has absolute position do not update the parent
if (el.absolute) return;
Rect parent_bounds, parent_view;
Rect child_placement, child_occupied;
switch (pl.dir) {
case ROW:
if (!el.w.@is_grow()) pl.occupied += e.bounds.w;
case COLUMN:
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()) {
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;
}
// 1. Select the right origin
Point origin;
switch (div.layout) {
case LAYOUT_ROW:
origin = div.origin_r;
case LAYOUT_COLUMN:
origin = div.origin_c;
case LAYOUT_FLOATING: // none, relative to zero zero
case LAYOUT_ABSOLUTE: // absolute position, this is a no-op, return the rect
return rect;
default: // error
return {};
}
// HEIGHT
if (e.layout.h.@is_grow()) {
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;
}
}
}
// 2. Compute the parent's view
parent_bounds = parent.bounds;
parent_view = parent.get_view();
fn void resolve_placement(Elem* c, Elem* p)
{
Layout* pl = &p.layout;
Layout* cl = &c.layout;
// 3. Compute the placement and occupied area
Point off = {
.x = p.bounds.x + pl.origin.x + pl.content_offset.x,
.y = p.bounds.y + pl.origin.y + pl.content_offset.y,
// grow rect (wanted size) when widht or height are less than zero
bool adapt_x = rect.w <= 0;
bool adapt_y = rect.h <= 0;
if (adapt_x) rect.w = parent_bounds.w - parent_bounds.x - origin.x;
if (adapt_y) rect.h = parent_bounds.h - parent_bounds.y - origin.y;
// offset placement and area
child_placement = child_placement.off(origin.add(rect.position()));
child_occupied = child_occupied.off(origin.add(rect.position()));
Rect margin = style.margin;
Rect padding = style.padding;
ushort border = style.border;
// padding, grows both the placement and occupied area
child_placement = child_placement.grow(padding.position().add(padding.size()));
child_occupied = child_occupied.grow(padding.position().add(padding.size()));
// border, grows both the placement and occupied area
child_placement = child_placement.grow({border*2, border*2});
child_occupied = child_occupied.grow({border*2, border*2});
// margin, offsets the placement and grows the occupied area
child_placement = child_placement.off(margin.position());
child_occupied = child_occupied.grow(margin.position().add(margin.size()));
// oh yeah also adjust the rect if i was to grow
if (adapt_x) rect.w -= padding.x+padding.w + border*2 + margin.x+margin.w;
if (adapt_y) rect.h -= padding.y+padding.h + border*2 + margin.y+margin.h;
// set the size
child_placement = child_placement.grow(rect.size());
child_occupied = child_occupied.grow(rect.size());
// 4. Update the parent's origin
div.origin_r = {
.x = child_occupied.bottom_right().x,
.y = origin.y,
};
div.origin_c = {
.x = origin.x,
.y = child_occupied.bottom_right().y,
};
// 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;
// 5. Update the parent's children bounds
if (!child_occupied.bottom_right().in_rect(div.children_bounds)) {
// right overflow
if (child_occupied.bottom_right().x > div.children_bounds.bottom_right().x) {
div.children_bounds.w += child_occupied.bottom_right().x - div.children_bounds.bottom_right().x;
}
// bottom overflow
if (child_occupied.bottom_right().y > div.children_bounds.bottom_right().y) {
div.children_bounds.h += child_occupied.bottom_right().y - div.children_bounds.bottom_right().y;
}
}
switch (pl.anchor) {
case TOP_LEFT:
c.bounds.x = off.x;
c.bounds.y = off.y;
case LEFT:
c.bounds.x = off.x;
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;
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:
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:
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:
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:
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:
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:
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");
}
}
fn void Ctx.layout_element_tree(&ctx)
{
int current;
for (int n; (current = ctx.tree.level_order_it(0, n)) >= 0; n++) {
Elem* p = ctx.find_elem(ctx.tree.get(current));
//if (ctx.tree.is_root(current)!!) p = &&{};
int ch;
// RESOLVE KNOWN DIMENSIONS
for (int i; (ch = ctx.tree.children_it(current, i)) >= 0; i++) {
Elem* c = ctx.find_elem(ctx.tree.get(ch));
if (ctx.tree.is_root(ch)) {
resolve_dimensions(p, &&{});
} else {
resolve_dimensions(c, p);
}
}
// RESOLVE GROW CHILDREN
for (int i; (ch = ctx.tree.children_it(current, i)) >= 0; i++) {
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);
}
}
// RESOLVE CHILDREN PLACEMENT
for (int i; (ch = ctx.tree.children_it(current, i)) >= 0; i++) {
Elem* c = ctx.find_elem(ctx.tree.get(ch));
if (ctx.tree.is_root(ch)) {
resolve_placement(p, &&{});
update_children_bounds(p, &&{});
} else {
resolve_placement(c, p);
update_children_bounds(c, p);
}
}
// 99. return the placement
if (child_placement.collides(parent_view)) {
return child_placement.off(parent.get_view_off().neg());
} else {
return {};
}
}

View File

@ -10,9 +10,6 @@ struct Rect {
short x, y, w, h;
}
// TODO: find another name
const Rect RECT_MAX = {0, 0, short.max, short.max};
// return true if rect a contains b
macro bool Rect.contains(Rect a, Rect b)
{
@ -36,26 +33,11 @@ macro bool Rect.collides(Rect a, Rect b)
return !(a.x > b.x+b.w || a.x+a.w < b.x || a.y > b.y+b.h || a.y+a.h < b.y);
}
// return a rect that contains both rects, a bounding box of both
macro Rect containing_rect(Rect a, Rect b)
{
short min_x = (short)min(a.x, b.x);
short min_y = (short)min(a.y, b.y);
short max_x = (short)max(a.x + a.w, b.x + b.w);
short max_y = (short)max(a.y + a.h, b.y + b.h);
return {
.x = min_x,
.y = min_y,
.w = (short)(max_x - min_x),
.h = (short)(max_y - min_y)
};
}
// check for empty rect
macro bool Rect.is_null(Rect r) => r.x == 0 && r.y == 0 && r.x == 0 && r.w == 0;
// returns the element-wise addition of r1 and r2
macro Rect Rect.add(Rect r1, Rect r2) @operator_s(+)
macro Rect Rect.add(Rect r1, Rect r2)
{
return {
.x = r1.x + r2.x,
@ -66,7 +48,7 @@ macro Rect Rect.add(Rect r1, Rect r2) @operator_s(+)
}
// returns the element-wise subtraction of r1 and r2
macro Rect Rect.sub(Rect r1, Rect r2) @operator_s(-)
macro Rect Rect.sub(Rect r1, Rect r2)
{
return {
.x = r1.x - r2.x,
@ -77,7 +59,7 @@ macro Rect Rect.sub(Rect r1, Rect r2) @operator_s(-)
}
// returns the element-wise multiplication of r1 and r2
macro Rect Rect.mul(Rect r1, Rect r2) @operator_s(*)
macro Rect Rect.mul(Rect r1, Rect r2)
{
return {
.x = r1.x * r2.x,
@ -154,36 +136,6 @@ macro Point Rect.bottom_right(Rect r)
};
}
macro Rect Rect.center_to(Rect a, Rect b)
{
return {
.x = b.x + (b.w - a.w)/2,
.y = b.y + (b.h - a.h)/2,
.w = a.w,
.h = a.h,
};
}
macro Rect Rect.pad(Rect a, Rect b)
{
return {
.x = a.x + b.x,
.y = a.y + b.y,
.w = a.w - b.x - b.w,
.h = a.h - b.y - b.h,
};
}
macro Rect Rect.expand(Rect a, Rect b)
{
return {
.x = a.x - b.x,
.y = a.y - b.y,
.w = a.w + b.x + b.w,
.h = a.h + b.y + b.h,
};
}
// ---------------------------------------------------------------------------------- //
// POINT //
@ -199,12 +151,39 @@ macro bool Point.in_rect(Point p, Rect r)
return (p.x >= r.x && p.x <= r.x + r.w) && (p.y >= r.y && p.y <= r.y + r.h);
}
macro Point Point.add(Point a, Point b) @operator_s(+) => {.x = a.x+b.x, .y = a.y+b.y};
macro Point Point.sub(Point a, Point b) @operator_s(-) => {.x = a.x-b.x, .y = a.y-b.y};
macro Point Point.neg(Point p) @operator_s(-) => {-p.x, -p.y};
macro Point Point.add(Point a, Point b)
{
return {
.x = a.x + b.x,
.y = a.y + b.y,
};
}
macro Point Point.max(Point a, Point b) => {.x = max(a.x, b.x), .y = max(a.y, b.y)};
macro Point Point.min(Point a, Point b) => {.x = min(a.x, b.x), .y = min(a.y, b.y)};
macro Point Point.sub(Point a, Point b)
{
return {
.x = a.x - b.x,
.y = a.y - b.y,
};
}
macro Point Point.neg(Point p) => {-p.x, -p.y};
macro Point Point.max(Point a, Point b)
{
return {
.x = max(a.x, b.x),
.y = max(a.y, b.y),
};
}
macro Point Point.min(Point a, Point b)
{
return {
.x = min(a.x, b.x),
.y = min(a.y, b.y),
};
}
// ---------------------------------------------------------------------------------- //
// COLOR //
@ -235,29 +214,8 @@ macro Color uint.@to_rgba($u)
};
}
macro uint Color.to_uint(c) => c.r | (c.g << 8) | (c.b << 16) | (c.a << 24);
// ---------------------------------------------------------------------------------- //
// SIZE //
// ---------------------------------------------------------------------------------- //
struct Size {
short min, max;
macro uint Color.to_uint(c)
{
uint u = c.r | (c.g << 8) | (c.b << 16) | (c.a << 24);
return u;
}
macro Size @grow() => {.min = 0, .max = 0};
macro Size @exact(short s) => {.min = s, .max = s};
// PROBLEM WE ARE OVERFLOWING
macro Size @fit(short min = 0, short max = 999) => {.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);
macro Size Size.add(a, Size b) @operator_s(+) => {.min = a.min+b.min, .max = a.max+b.max};
macro Size Size.sub(a, Size b) @operator_s(-) => {.min = a.min-b.min, .max = a.max-b.max};
macro Size Size.combine(a, Size b) => {.min = max(a.min, b.min), .max = min(a.max, b.max)};
macro Size Size.comb_max(a, Size b) => {.min = max(a.min, b.min), .max = max(a.max, b.max)};
macro Size Size.comb_min(a, Size b) => {.min = min(a.min, b.min), .max = min(a.max, b.max)};
macro short Size.greater(a) => a.min > a.max ? a.min : a.max;

View File

@ -8,17 +8,18 @@ struct ElemSlider {
Rect handle;
}
/* handle
* +----+-----+---------------------+
* | |#####| |
* +----+-----+---------------------+
*/
macro Ctx.slider_hor(&ctx, Size w, Size h, float* value, float hpercent = 0.25, ...)
=> ctx.slider_hor_id(@compute_id($vasplat), w, h, value, hpercent);
macro Ctx.slider_hor(&ctx, Rect size, float* value, float hpercent = 0.25, ...)
=> ctx.slider_hor_id(@compute_id($vasplat), size, value, hpercent);
<*
@require value != null
*>
fn ElemEvents? Ctx.slider_hor_id(&ctx, Id id, Size w, Size h, float* value, float hpercent = 0.25)
fn ElemEvents? Ctx.slider_hor_id(&ctx, Id id, Rect size, float* value, float hpercent = 0.25)
{
id = ctx.gen_id(id)!;
@ -26,21 +27,16 @@ fn ElemEvents? Ctx.slider_hor_id(&ctx, Id id, Size w, Size h, float* value, floa
Elem* elem = ctx.get_elem(id, ETYPE_SLIDER)!;
Style* style = ctx.styles.get_style(@str_hash("slider"));
elem.layout.w = w;
elem.layout.h = h;
elem.layout.content_offset = style.margin + style.border + style.padding;
update_parent_size(elem, parent);
Rect bg_bounds = elem.bounds.pad(style.margin);
Rect content_bounds = elem.bounds.pad(style.margin + style.border + style.padding);
// 2. Layout
elem.bounds = ctx.position_element(parent, size, style);
// handle width
short hw = (short)(content_bounds.w * hpercent);
short hw = (short)(elem.bounds.w * hpercent);
Rect handle = {
.x = calc_slider(content_bounds.x, content_bounds.w-hw, *value),
.y = content_bounds.y,
.x = calc_slider(elem.bounds.x, elem.bounds.w-hw, *value),
.y = elem.bounds.y,
.w = hw,
.h = content_bounds.h,
.h = elem.bounds.h,
};
elem.slider.handle = handle;
@ -48,8 +44,8 @@ fn ElemEvents? Ctx.slider_hor_id(&ctx, Id id, Size w, Size h, float* value, floa
elem.events = ctx.get_elem_events(elem);
if (ctx.elem_focus(elem) && ctx.is_mouse_down(BTN_LEFT)) {
*value = calc_value(content_bounds.x, m.x, content_bounds.w, hw);
elem.slider.handle.x = calc_slider(content_bounds.x, content_bounds.w-hw, *value);
*value = calc_value(elem.bounds.x, m.x, elem.bounds.w, hw);
elem.slider.handle.x = calc_slider(elem.bounds.x, elem.bounds.w-hw, *value);
elem.events.update = true;
}
@ -57,7 +53,7 @@ fn ElemEvents? Ctx.slider_hor_id(&ctx, Id id, Size w, Size h, float* value, floa
Style s = *style;
Rect padding = s.padding;
s.padding = {};
ctx.push_rect(bg_bounds, parent.div.z_index, &s)!;
ctx.push_rect(elem.bounds, parent.div.z_index, &s)!;
s.bg = s.primary;
s.padding = padding;
s.border = {};
@ -79,9 +75,9 @@ fn ElemEvents? Ctx.slider_hor_id(&ctx, Id id, Size w, Size h, float* value, floa
* | |
* +--+
*/
macro Ctx.slider_ver(&ctx, Size w, Size h, float* value, float hpercent = 0.25, ...)
=> ctx.slider_ver_id(@compute_id($vasplat), w, h, value, hpercent);
fn ElemEvents? Ctx.slider_ver_id(&ctx, Id id, Size w, Size h, float* value, float hpercent = 0.25)
macro Ctx.slider_ver(&ctx, Rect size, float* value, float hpercent = 0.25, ...)
=> ctx.slider_ver_id(@compute_id($vasplat), size, value, hpercent);
fn ElemEvents? Ctx.slider_ver_id(&ctx, Id id, Rect size, float* value, float hpercent = 0.25)
{
id = ctx.gen_id(id)!;
@ -89,21 +85,22 @@ fn ElemEvents? Ctx.slider_ver_id(&ctx, Id id, Size w, Size h, float* value, floa
Elem *elem = ctx.get_elem(id, ETYPE_SLIDER)!;
Style* style = ctx.styles.get_style(@str_hash("slider"));
elem.layout.w = w;
elem.layout.h = h;
elem.layout.content_offset = style.margin + style.border + style.padding;
update_parent_size(elem, parent);
// 1. Fill the element fields
if (elem.flags.is_new) {
elem.type = ETYPE_SLIDER;
} else if (elem.type != ETYPE_SLIDER) {
return WRONG_ELEMENT_TYPE?;
}
// 2. Layout
Rect bg_bounds = elem.bounds.pad(style.margin);
Rect content_bounds = elem.bounds.pad(style.margin + style.border + style.padding);
elem.bounds = ctx.position_element(parent, size, style);
// handle height
short hh = (short)(content_bounds.h * hpercent);
short hh = (short)(elem.bounds.h * hpercent);
Rect handle = {
.x = content_bounds.x,
.y = calc_slider(content_bounds.y, content_bounds.h-hh, *value),
.w = content_bounds.w,
.x = elem.bounds.x,
.y = calc_slider(elem.bounds.y, elem.bounds.h-hh, *value),
.w = elem.bounds.w,
.h = hh,
};
elem.slider.handle = handle;
@ -112,8 +109,8 @@ fn ElemEvents? Ctx.slider_ver_id(&ctx, Id id, Size w, Size h, float* value, floa
elem.events = ctx.get_elem_events(elem);
if (ctx.elem_focus(elem) && ctx.is_mouse_down(BTN_LEFT)) {
*value = calc_value(content_bounds.y, m.y, content_bounds.h, hh);
elem.slider.handle.y = calc_slider(content_bounds.y, content_bounds.h-hh, *value);
*value = calc_value(elem.bounds.y, m.y, elem.bounds.h, hh);
elem.slider.handle.y = calc_slider(elem.bounds.y, elem.bounds.h-hh, *value);
elem.events.update = true;
}
@ -121,7 +118,7 @@ fn ElemEvents? Ctx.slider_ver_id(&ctx, Id id, Size w, Size h, float* value, floa
Style s = *style;
Rect padding = s.padding;
s.padding = {};
ctx.push_rect(bg_bounds, parent.div.z_index, &s)!;
ctx.push_rect(elem.bounds, parent.div.z_index, &s)!;
s.bg = s.primary;
s.padding = padding;
s.border = {};

View File

@ -30,6 +30,10 @@ struct SpriteAtlas {
bool should_update;
}
struct ElemSprite {
Id id;
}
// name: some examples are "icons" or "images"
fn void? SpriteAtlas.init(&this, String name, AtlasType type, ushort width, ushort height)
{
@ -40,7 +44,7 @@ fn void? SpriteAtlas.init(&this, String name, AtlasType type, ushort width, usho
this.id = name.hash();
this.atlas.new(this.id, AtlasType.ATLAS_R8G8B8A8, width, height)!;
this.sprites.init(allocator::mem, capacity: SRITES_PER_ATLAS);
this.sprites.init(allocator::heap(), capacity: SRITES_PER_ATLAS);
this.should_update = false;
}
@ -100,8 +104,50 @@ fn void? Ctx.import_sprite_file_qoi(&ctx, String name, String path, SpriteType t
{
QOIDesc desc;
char[] pixels = qoi::read(allocator::mem, path, &desc, QOIChannels.RGBA)!;
char[] pixels = qoi::read(allocator::heap(), path, &desc, QOIChannels.RGBA)!;
defer mem::free(pixels);
ctx.sprite_atlas.insert(name, type, pixels, (ushort)desc.width, (ushort)desc.height, (ushort)desc.width)!;
}
macro Ctx.sprite(&ctx, String name, Point off = {0,0}, ...)
=> ctx.sprite_id(@compute_id($vasplat), name, off);
fn void? Ctx.sprite_id(&ctx, Id id, String name, Point off)
{
id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id, ETYPE_SPRITE)!;
Style* style = ctx.styles.get_style(@str_hash("sprite"));
Sprite* sprite = ctx.sprite_atlas.get(name)!;
Rect uv = { sprite.u, sprite.v, sprite.w, sprite.h };
Rect bounds = { 0, 0, sprite.w, sprite.h };
elem.bounds = ctx.position_element(parent, bounds.off(off), style);
elem.sprite.id = ctx.get_sprite_atlas_id(name);
// if the bounds are null the element is outside the div view,
// no interaction should occur so just return
if (elem.bounds.is_null()) return;
Id tex_id = ctx.sprite_atlas.id;
return ctx.push_sprite(elem.bounds, uv, tex_id, parent.div.z_index)!;
}
fn void? Ctx.draw_sprite_raw(&ctx, String name, Rect bounds, bool center = false)
{
Elem *parent = ctx.get_parent()!;
Sprite* sprite = ctx.sprite_atlas.get(name)!;
Id tex_id = ctx.sprite_atlas.id;
if (center) {
Point off = {.x = (bounds.w - sprite.w) / 2, .y = (bounds.h - sprite.h) / 2};
bounds = bounds.off(off);
}
return ctx.push_sprite(bounds, sprite.uv(), tex_id, parent.div.z_index, type: sprite.type)!;
}

View File

@ -7,8 +7,10 @@ import std::io;
// global style, similar to the css box model
struct Style { // css box model
Rect padding;
Rect border;
Rect margin;
ushort border;
ushort radius;
ushort size;
Color bg; // background color
Color fg; // foreground color
@ -16,17 +18,15 @@ struct Style { // css box model
Color secondary; // secondary color
Color accent; // accent color
ushort radius;
short size;
}
const Style DEFAULT_STYLE = {
.margin = {2, 2, 2, 2},
.border = {2, 2, 2, 2},
.padding = {1, 1, 1, 1},
.radius = 0,
.padding = {},
.border = 2,
.radius = 12,
.size = 16,
.bg = 0x282828ffu.@to_rgba(),
.fg = 0xfbf1c7ffu.@to_rgba(),
.primary = 0xcc241dffu.@to_rgba(),
@ -60,7 +60,7 @@ fn int StyleMap.import_style_string(&map, String text)
Parser p;
p.lex.text = text;
int added;
while (p.parse_style() == true) {
added++;
// set the default style correctly
@ -79,7 +79,7 @@ fn int Ctx.import_style_from_file(&ctx, String path)
text = mem::new_array(char, size);
file::load_buffer(path, text)!!;
defer mem::free(text);
int added = ctx.import_style_from_string((String)text);
return added;
}
@ -89,8 +89,8 @@ fn int Ctx.import_style_from_file(&ctx, String path)
* Style can be serialized and deserialized with a subset of CSS
* <style name> {
* padding: left right top bottom;
* border: left right top bottom;
* margin: left right top bottoms;
* border: uint;
* radius: uint;
* size: uint;
* Color: #RRGGBBAA;
@ -102,7 +102,7 @@ fn int Ctx.import_style_from_file(&ctx, String path)
* The field "style name" will be hashed and the hash used as the id int the style map.
* Fields may be excluded, each excluded field is set to zero.
* The default unit is pixels, but millimeters is also available. The parser function accepts a scale
* factor that has to be obtained with the window manager functions.
* factor that has to be obtained with the window manager functions.
*/
module ugui::css;
@ -115,10 +115,7 @@ import std::io;
enum TokenType {
INVALID,
IDENTIFIER,
RCURLY,
LCURLY,
SEMICOLON,
COLON,
PUNCT,
NUMBER,
COLOR,
EOF,
@ -188,22 +185,16 @@ fn Token Lexer.next_token(&lex)
// skip whitespace
while (ascii::is_space_m(lex.peep())) {
if (lex.advance() == 0) return {.type = EOF};
if (lex.off >= lex.text.len) return {.type = EOF};
if (lex.off >= lex.text.len) return {.type = EOF};
}
t.off = lex.off;
switch (true) {
case ascii::is_punct_m(lex.peep()) && lex.peep() != '#': // punctuation
t.type = PUNCT;
t.text = lex.text[lex.off:1];
if (lex.advance() == 0) { t.type = INVALID; break; }
switch (t.text[0]) {
case ':': t.type = COLON;
case ';': t.type = SEMICOLON;
case '{': t.type = LCURLY;
case '}': t.type = RCURLY;
default: t.type = INVALID;
}
case lex.peep() == '#': // color
t.type = COLOR;
if (lex.advance() == 0) { t.type = INVALID; break; }
@ -216,7 +207,7 @@ fn Token Lexer.next_token(&lex)
t.type = INVALID;
break;
}
char[10] hex_str = (char[])"0x";
hex_str[2..] = lex.text[hex_start..lex.off-1];
uint? color_hex = ((String)hex_str[..]).to_uint();
@ -225,21 +216,21 @@ fn Token Lexer.next_token(&lex)
break;
}
t.color = color_hex.to_rgba();
case ascii::is_alpha_m(lex.peep()): // identifier
t.type = IDENTIFIER;
while (ascii::is_alnum_m(lex.peep()) || lex.peep() == '-' || lex.peep() == '_') {
if (lex.advance() == 0) { t.type = INVALID; break; }
}
t.text = lex.text[t.off..lex.off-1];
case ascii::is_digit_m(lex.peep()): // number
t.type = NUMBER;
t.unit = PIXELS;
// find the end of the number
usz end;
while (ascii::is_alnum_m(lex.peep()) || lex.peep() == '+' || lex.peep() == '-' || lex.peep() == '.') {
if (lex.advance() == 0) { t.type = INVALID; break; }
if (lex.advance() == 0) { t.type = INVALID; break; }
}
end = lex.off;
if (end - t.off > 2) {
@ -261,7 +252,7 @@ fn Token Lexer.next_token(&lex)
t.value = value;
t.text = lex.text[t.off..lex.off-1];
}
if (t.type == INVALID) {
io::eprintfn("CSS Lexing ERROR at %d:%d: '%s' is not a valid token", t.line, t.col, lex.text[t.off..lex.off]);
}
@ -285,10 +276,21 @@ struct Parser {
float mm_to_px;
}
macro bool Parser.expect_text(&p, Token* t, TokenType type, String text)
{
*t = p.lex.next_token();
if (t.type == type && t.text == text) {
return true;
}
io::eprintfn("CSS parsing error at %d:%d: expected type:%s text:'%s' but got type:%s text:'%s'",
t.line, t.col, type, text, t.type, t.text);
return false;
}
macro bool Parser.expect(&p, Token* t, TokenType type)
{
*t = p.lex.next_token();
if (t.type == type) return true;
if (t.type == type) return true;
io::eprintfn("CSS parsing error at %d:%d: expected %s but got %s", t.line, t.col, type, t.type);
return false;
}
@ -298,22 +300,22 @@ fn bool Parser.parse_style(&p)
Token t;
p.style = {};
p.style_id = 0;
// style name
if (p.expect(&t, IDENTIFIER) == false) return false;
p.style_id = t.text.hash();
// style body
if (p.expect(&t, LCURLY) == false) return false;
if (p.expect_text(&t, PUNCT, "{") == false) return false;
while (true) {
if (p.parse_property() == false) return false;
t = p.lex.peep_token();
if (t.type != IDENTIFIER) break;
}
if (p.expect(&t, RCURLY) == false) return false;
if (p.expect_text(&t, PUNCT, "}") == false) return false;
return true;
}
@ -321,7 +323,7 @@ fn bool Parser.parse_property(&p)
{
Token t, prop;
if (p.expect(&prop, IDENTIFIER) == false) return false;
if (p.expect(&t, COLON) == false) return false;
if (p.expect_text(&t, PUNCT, ":") == false) return false;
switch (prop.text) {
case "padding":
@ -329,11 +331,6 @@ fn bool Parser.parse_property(&p)
if (p.parse_size(&padding) == false) return false;
p.style.padding = padding;
case "border":
Rect border;
if (p.parse_size(&border) == false) return false;
p.style.border = border;
case "margin":
Rect margin;
if (p.parse_size(&margin) == false) return false;
@ -364,30 +361,39 @@ fn bool Parser.parse_property(&p)
if (p.parse_color(&accent) == false) return false;
p.style.accent = accent;
case "border":
short border;
if (p.parse_number(&border) == false) return false;
if (border < 0) {
io::eprintfn("CSS parsing error at %d:%d: 'border' must be a positive number, got %d", t.line, t.col, border);
return false;
}
p.style.border = (ushort)border;
case "radius":
short r;
if (p.parse_number(&r) == false) return false;
if (r < 0) {
io::eprintfn("CSS parsing error at %d:%d: 'radius' must be a positive number, got %d", t.line, t.col, r);
short radius;
if (p.parse_number(&radius) == false) return false;
if (radius < 0) {
io::eprintfn("CSS parsing error at %d:%d: 'radius' must be a positive number, got %d", t.line, t.col, radius);
return false;
}
p.style.radius = (ushort)r;
p.style.radius = (ushort)radius;
case "size":
short s;
if (p.parse_number(&s) == false) return false;
if (s < 0) {
io::eprintfn("CSS parsing error at %d:%d: 'size' must be a positive number, got %d", t.line, t.col, s);
short size;
if (p.parse_number(&size) == false) return false;
if (size < 0) {
io::eprintfn("CSS parsing error at %d:%d: 'size' must be a positive number, got %d", t.line, t.col, size);
return false;
}
p.style.size = (ushort)s;
p.style.size = (ushort)size;
default:
io::eprintfn("CSS parsing error at %d:%d: '%s' is not a valid property", prop.line, prop.col, prop.text);
return false;
}
if (p.expect(&t, SEMICOLON) == false) return false;
if (p.expect_text(&t, PUNCT, ";") == false) return false;
return true;
}
@ -412,9 +418,9 @@ fn bool Parser.parse_size(&p, Rect* r)
{
short x;
Token t;
if (p.parse_number(&x) == false) return false;
t = p.lex.peep_token();
if (t.type == NUMBER) {
// we got another number so we expect three more
@ -426,7 +432,7 @@ fn bool Parser.parse_size(&p, Rect* r)
if (p.parse_number(&x) == false) return false;
r.h = x;
return true;
} else if (t.type == SEMICOLON) {
} else if (t.type == PUNCT && t.text == ";") {
// just one number, all dimensions are the same
r.x = r.y = r.w = r.h = x;
return true;

View File

@ -0,0 +1,90 @@
module ugui;
import std::io;
struct ElemText {
char[] str;
usz cursor; // cursor offset
}
macro Ctx.text_unbounded(&ctx, String text, ...)
=> ctx.text_unbounded_id(@compute_id($vasplat), text);
fn void? Ctx.text_unbounded_id(&ctx, Id id, String text)
{
id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id, ETYPE_TEXT)!;
Style* style = ctx.styles.get_style(@str_hash("text"));
elem.text.str = text;
// if the element is new or the parent was updated then redo layout
Rect text_size = ctx.get_text_bounds(text)!;
// 2. Layout
elem.bounds = ctx.position_element(parent, text_size, style);
if (elem.bounds.is_null()) { return; }
ctx.push_string(elem.bounds, text, parent.div.z_index, style.fg)!;
}
macro Ctx.text_box(&ctx, Rect size, char[] text, usz* text_len, ...)
=> ctx.text_box_id(@compute_id($vasplat), size, text, text_len);
fn ElemEvents? Ctx.text_box_id(&ctx, Id id, Rect size, char[] text, usz* text_len)
{
id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id, ETYPE_TEXT)!;
Style* style = ctx.styles.get_style(@str_hash("text-box"));
elem.text.str = text;
// layout the text box
elem.bounds = ctx.position_element(parent, size, style);
// check input and update the text
elem.events = ctx.get_elem_events(elem);
if (elem.events.text_input) {
usz l = ctx.input.keyboard.text_len;
char[] t = ctx.input.keyboard.text[..l];
if (l != 0 && l < text.len - *text_len) {
text[*text_len..*text_len+l] = t[..];
*text_len += l;
}
if (ctx.input.keyboard.modkeys.bkspc) {
*text_len = *text_len > 0 ? *text_len-1 : 0;
}
}
elem.text.cursor = *text_len;
// draw the box
short line_height = (short)ctx.font.line_height();
Rect text_box = elem.bounds.sub({0,0,0,line_height});
Rect input_box = {
.x = elem.bounds.x,
.y = elem.bounds.y + elem.bounds.h - line_height,
.w = elem.bounds.w,
.h = line_height,
};
Rect cursor;
Point b = ctx.get_cursor_position((String)text[:elem.text.cursor])!;
cursor = {
.x = b.x,
.y = b.y,
.w = 3,
.h = line_height,
};
cursor = cursor.off(elem.bounds.position());
ctx.push_rect(text_box, parent.div.z_index, style)!;
ctx.push_string(text_box, text[:*text_len], parent.div.z_index, style.fg)!;
ctx.push_rect(input_box, parent.div.z_index, style)!;
ctx.push_rect(cursor, parent.div.z_index, style)!;
return elem.events;
}

View File

@ -1,125 +0,0 @@
module ugui;
import grapheme;
import std::ascii;
struct TextEdit {
char[] buffer;
usz chars;
usz cursor;
}
fn String TextEdit.to_string(&te) => (String)te.buffer[:te.chars];
fn String TextEdit.until_cursor(&te) => (String)te.buffer[:te.cursor];
fn String TextEdit.from_cursor(&te) => (String)te.buffer[te.cursor..];
// implement text editing operations on the buffer
// returns true if the buffer is full
fn bool Ctx.text_edit(&ctx, TextEdit* te)
{
String in = ctx.get_keys();
ModKeys mod = ctx.get_mod();
usz free = te.buffer.len - te.chars;
usz after = te.chars - te.cursor;
// append text input to the buffer
if (in.len <= free) {
// make space
te.buffer[te.cursor+in.len : after] = te.buffer[te.cursor : after];
// insert characters
te.buffer[te.cursor : in.len] = in[..];
// increment characters and cursor
te.chars += in.len;
te.cursor += in.len;
free -= in.len;
} else {
return true;
}
// handle modkeys
if (te.chars) {
// handle backspace and delete
if (mod.bkspc) {
if (te.cursor > 0) {
// TODO: only delete until punctuation
usz how_many = mod & KMOD_CTRL ? te.until_cursor().prev_word_off() : te.until_cursor().prev_char_off();
te.buffer[te.cursor-how_many : after] = te.buffer[te.cursor : after];
te.cursor -= how_many;
te.chars -= how_many;
free += how_many;
}
}
if (mod.del) {
if (after > 0 && te.cursor < te.chars) {
usz how_many = mod & KMOD_CTRL ? te.from_cursor().next_word_off() : te.from_cursor().next_char_off();
te.buffer[te.cursor : after] = te.buffer[te.cursor+how_many : after];
te.chars -= how_many;
after -= how_many;
free += how_many;
}
}
// handle arrow keys
if (mod.left) {
if (te.cursor > 0) {
usz how_many = mod & KMOD_CTRL ? te.until_cursor().prev_word_off() : te.until_cursor().prev_char_off();
te.cursor -= how_many;
after += how_many;
}
}
if (mod.right) {
if (after > 0) {
usz how_many = mod & KMOD_CTRL ? te.from_cursor().next_word_off() : te.from_cursor().next_char_off();
te.cursor += how_many;
after -= how_many;
}
}
if (mod.up) {
// back up to previous line
if (te.cursor > 0) {
usz curr_line_start = te.until_cursor().rindex_of_char('\n') ?? 0;
usz prev_line_start = curr_line_start ? te.until_cursor()[..curr_line_start-1].rindex_of_char('\n') ?? 0 : 0;
usz curr_line_off = te.cursor - curr_line_start;
usz prev_line_len = curr_line_start - prev_line_start;
te.cursor = prev_line_start + min(curr_line_off-1, prev_line_len);
after = te.chars - te.cursor;
}
}
if (mod.down) {
// down to the next line
if (after > 0) {
usz curr_line_start = te.until_cursor().rindex_of_char('\n') ?? 0;
usz curr_line_off = te.cursor - curr_line_start;
usz next_line_start = te.from_cursor().index_of_char('\n') + te.cursor + 1 ?? te.chars;
usz next_line_end = ((String)te.buffer[next_line_start..]).index_of_char('\n') + next_line_start ?? te.chars;
usz next_line_len = next_line_end - next_line_start;
te.cursor = next_line_start + min(curr_line_off, next_line_len);
after = te.chars - te.cursor;
}
}
}
return free == 0;
}
macro isz char[].next_char_off(b) => grapheme::next_character_break_utf8(b.ptr, b.len);
macro isz char[].next_word_off(b) => grapheme::next_word_break_utf8(b.ptr, b.len);
fn isz char[].prev_char_off(b)
{
foreach_r (off, c: b) {
if (c & 0xC0 == 0x80) continue;
return b.len - off;
}
return b.len;
}
fn isz char[].prev_word_off(b)
{
for (isz off = b.len-1; off > 0;) {
isz c_off = b[..off].prev_char_off();
off -= c_off;
if (ascii::is_punct(b[off]) || ascii::is_space(b[off])) return b.len - off - 1;
}
return b.len;
}

View File

@ -4,51 +4,39 @@ faultdef CANNOT_SHRINK, INVALID_REFERENCE, TREE_FULL, REFERENCE_NOT_PRESENT, INV
module vtree{ElemType};
import std::core::mem;
import std::core::mem::allocator;
import std::io;
struct VTree {
Allocator allocator;
usz elements;
ElemType[] vector; // vector of element ids
isz[] refs, ordered_refs;
}
macro VTree.ref_is_valid(&tree, isz ref) => (ref >= 0 && ref < tree.refs.len);
macro VTree.ref_is_present(&tree, isz ref) => tree.refs[ref] >= 0;
macro VTree.size(&tree) => tree.refs.len;
macro VTree.ref_is_valid(&tree, isz ref) { return (ref >= 0 && ref < tree.refs.len); }
macro VTree.ref_is_present(&tree, isz ref) { return tree.refs[ref] >= 0; }
macro VTree.size(&tree) { return tree.refs.len; }
// macro to zero an element
// macro to zero an elemen
macro @zero()
{
$if @assignable_to(0, ElemType):
$if $assignable(0, ElemType):
return 0;
$endif
$if @assignable_to(null, ElemType):
return null;
$endif
$if @assignable_to({}, ElemType):
$else
return {};
$endif
//$assert true == false : ElemType.nameof +++ " is not assignable to zero or equivalent";
}
fn void? VTree.init(&tree, usz size, Allocator allocator)
fn void? VTree.init(&tree, usz size)
{
tree.allocator = allocator;
tree.vector = mem::new_array(ElemType, size);
defer catch { (void)mem::free(tree.vector); }
tree.vector = allocator::new_array(tree.allocator, ElemType, size);
defer catch { (void)allocator::free(tree.allocator, tree.vector); }
tree.refs = mem::new_array(isz, size);
defer catch { (void)mem::free(tree.refs); }
tree.refs = allocator::new_array(tree.allocator, isz, size);
defer catch { (void)allocator::free(tree.allocator, tree.refs); }
tree.ordered_refs = allocator::new_array(tree.allocator, isz, size);
defer catch { (void)allocator::free(tree.allocator, tree.ordered_refs); }
tree.ordered_refs = mem::new_array(isz, size);
defer catch { (void)mem::free(tree.ordered_refs); }
// set all refs to -1, meaning invalid (free) element
tree.refs[..] = -1;
@ -58,9 +46,9 @@ fn void? VTree.init(&tree, usz size, Allocator allocator)
fn void VTree.free(&tree)
{
(void)allocator::free(tree.allocator, tree.vector);
(void)allocator::free(tree.allocator, tree.refs);
(void)allocator::free(tree.allocator, tree.ordered_refs);
(void)mem::free(tree.vector);
(void)mem::free(tree.refs);
(void)mem::free(tree.ordered_refs);
}
fn void VTree.pack(&tree)
@ -114,14 +102,14 @@ fn void? VTree.resize(&tree, usz newsize)
usz old_size = tree.size();
tree.vector = ((ElemType*)allocator::realloc(tree.allocator, tree.vector, newsize*ElemType.sizeof))[:newsize];
defer catch { (void)allocator::free(tree.allocator, tree.vector); }
tree.vector = ((ElemType*)mem::realloc(tree.vector, newsize*ElemType.sizeof))[:newsize];
defer catch { (void)mem::free(tree.vector); }
tree.refs = ((isz*)allocator::realloc(tree.allocator, tree.refs, newsize*isz.sizeof))[:newsize];
defer catch { (void)allocator::free(tree.allocator, tree.refs); }
tree.refs = ((isz*)mem::realloc(tree.refs, newsize*isz.sizeof))[:newsize];
defer catch { (void)mem::free(tree.refs); }
tree.ordered_refs = ((isz*)allocator::realloc(tree.allocator, tree.ordered_refs, newsize*isz.sizeof))[:newsize];
defer catch { (void)allocator::free(tree.allocator, tree.ordered_refs); }
tree.ordered_refs = ((isz*)mem::realloc(tree.ordered_refs, newsize*isz.sizeof))[:newsize];
defer catch { (void)mem::free(tree.ordered_refs); }
if (newsize > tree.size()) {
tree.vector[old_size..newsize-1] = @zero();
@ -225,8 +213,6 @@ 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)
{

View File

@ -1,225 +0,0 @@
module ugui;
import std::io;
// button element
struct ElemButton {
int filler;
}
macro Ctx.button(&ctx, String label = "", String icon = "", ...)
=> ctx.button_id(@compute_id($vasplat), label, icon);
fn ElemEvents? Ctx.button_id(&ctx, Id id, String label, String icon)
{
id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
Style* style = ctx.styles.get_style(@str_hash("button"));
Sprite* sprite = icon != "" ? ctx.sprite_atlas.get(icon)! : &&(Sprite){};
Rect icon_size = sprite.rect();
ushort min_size = style.size;
ushort half_lh = (ushort)(ctx.font.line_height() / 2);
ushort inner_pad = label != "" && icon != "" ? half_lh : 0;
/*
* +--------------------------------------+
* | +--------+ |
* | | | +-----------------+ |
* | | icon | | label | |
* | | | +-----------------+ |
* | +--------+<->| |
* +-------------^------------------------+
* |inner_pad
*/
Point content_size = {
.x = icon_size.w + inner_pad, // text sizing is handled differently
.y = icon_size.h + inner_pad,
};
elem.layout.w = @fit(min_size);
elem.layout.h = @fit(min_size);
elem.layout.children.w = @exact(content_size.x);
elem.layout.children.h = @exact(content_size.y);
elem.layout.text = ctx.measure_string(label)!;
elem.layout.content_offset = style.margin + style.border + style.padding;
update_parent_size(elem, parent);
elem.events = ctx.get_elem_events(elem);
Rect content_bounds = elem.content_bounds();
Rect icon_bounds = {
.x = content_bounds.x,
.y = content_bounds.y,
.w = icon_size.w,
.h = icon_size.h
};
icon_bounds = icon_size.center_to(icon_bounds);
Rect text_bounds = {
.x = content_bounds.x + icon_bounds.w + inner_pad,
.y = content_bounds.y,
.w = content_bounds.w - icon_bounds.w - inner_pad,
.h = content_bounds.h,
};
//text_bounds = text_size.center_to(text_bounds);
bool is_active = ctx.elem_focus(elem) || elem.events.mouse_hover;
Style s = *style;
if (is_active) {
s.secondary = s.primary;
s.bg = s.accent;
}
ctx.push_rect(elem.bounds.pad(style.margin), parent.div.z_index, &s)!;
if (icon != "") {
ctx.push_sprite(icon_bounds, sprite.uv(), ctx.sprite_atlas.id, parent.div.z_index, type: sprite.type)!;
}
if (label != "") {
ctx.layout_string(label, text_bounds, CENTER, parent.div.z_index, style.fg)!;
}
return elem.events;
}
macro Ctx.checkbox(&ctx, String desc, bool* active, String tick_sprite = "", ...)
=> ctx.checkbox_id(@compute_id($vasplat), desc, active, tick_sprite);
fn void? Ctx.checkbox_id(&ctx, Id id, String description, bool* active, String tick_sprite)
{
id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
Style* style = ctx.styles.get_style(@str_hash("checkbox"));
short inner_pad = description != "" ? style.size/2 : 0;
/*
* |< >| style.size/2
* +---------------------|---|-----------+
* | | .-----. ---|--
* | +-----------------+ ' ### ' | ^
* | | description | | ##### | | style.size
* | +-----------------+ . ### . | v
* | '-----' ---|--
* +-------------------------|-------|---+
* |<----->| style.size
*/
elem.layout.w = @fit(style.size);
elem.layout.h = @fit(style.size);
elem.layout.children.w = @exact(style.size + inner_pad);
elem.layout.children.h = @exact(style.size);
elem.layout.text = ctx.measure_string(description)!;
elem.layout.content_offset = style.margin + style.border + style.padding;
update_parent_size(elem, parent);
elem.events = ctx.get_elem_events(elem);
if (elem.events.mouse_hover && elem.events.mouse_release) *active = !(*active);
Rect content_bounds = elem.bounds.pad(elem.layout.content_offset);
Rect text_bounds = {
.x = content_bounds.x,
.y = content_bounds.y,
.w = content_bounds.w - inner_pad - style.size,
.h = content_bounds.h
};
Rect check_bounds = {
.x = content_bounds.x + text_bounds.w + inner_pad,
.y = content_bounds.y + (content_bounds.h - style.size)/2,
.w = style.size,
.h = style.size,
};
Style s;
s.bg = style.bg;
s.secondary = style.secondary;
s.border = style.border;
s.radius = style.radius;
ctx.layout_string(description, text_bounds, CENTER, parent.div.z_index, style.fg)!;
if (tick_sprite != "") {
ctx.push_rect(check_bounds, parent.div.z_index, &s)!;
if (*active) {
Sprite* sprite = ctx.sprite_atlas.get(tick_sprite)!;
Id tex_id = ctx.sprite_atlas.id;
ctx.push_sprite(sprite.rect().center_to(check_bounds), sprite.uv(), tex_id, parent.div.z_index, type: sprite.type)!;
}
} else {
if (*active) {
s.bg = style.primary;
ctx.push_rect(check_bounds, parent.div.z_index, &s)!;
} else {
ctx.push_rect(check_bounds, parent.div.z_index, &s)!;
}
}
}
macro Ctx.toggle(&ctx, String desc, bool* active)
=> ctx.toggle_id(@compute_id($vasplat), desc, active);
fn void? Ctx.toggle_id(&ctx, Id id, String description, bool* active)
{
id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
Style* style = ctx.styles.get_style(@str_hash("toggle"));
short inner_pad = description != "" ? style.size/2 : 0;
/*
* |< >| style.size/2
* +---------------------|---|-----------------+
* | | .-----------. ---|--
* | +-----------------+ ' ##### ' | ^
* | | description | | ##### | | style.size
* | +-----------------+ . ##### . | v
* | '-----------' ---|--
* +-------------------------|-------------|---+
* |<----->| style.size*2
*/
elem.layout.w = @fit(style.size*2);
elem.layout.h = @fit(style.size);
elem.layout.children.w = @exact(style.size*2 + inner_pad);
elem.layout.children.h = @exact(style.size);
elem.layout.text = ctx.measure_string(description)!;
elem.layout.content_offset = style.margin + style.border + style.padding;
update_parent_size(elem, parent);
elem.events = ctx.get_elem_events(elem);
if (elem.events.mouse_hover && elem.events.mouse_release) *active = !(*active);
Rect content_bounds = elem.bounds.pad(elem.layout.content_offset);
Rect text_bounds = {
.x = content_bounds.x,
.y = content_bounds.y,
.w = content_bounds.w - inner_pad - style.size*2,
.h = content_bounds.h
};
Rect toggle_bounds = {
.x = content_bounds.x + text_bounds.w + inner_pad,
.y = content_bounds.y + (content_bounds.h - style.size)/2,
.w = style.size*2,
.h = style.size,
};
Rect toggle = {
.x = toggle_bounds.x + (*active ? style.size : 0),
.y = toggle_bounds.y,
.w = style.size,
.h = style.size
};
Style s;
s.bg = style.bg;
s.secondary = style.secondary;
s.border = style.border;
s.radius = style.radius;
ctx.layout_string(description, text_bounds, CENTER, parent.div.z_index, style.fg)!;
ctx.push_rect(toggle_bounds, parent.div.z_index, &s)!;
s.bg = style.primary;
s.border = {};
ctx.push_rect(toggle.pad(style.border), parent.div.z_index, &s)!;
}

View File

@ -1,146 +0,0 @@
module ugui;
import std::io;
import std::math;
// div element
struct ElemDiv {
struct scroll_x {
bool enabled;
bool on;
float value;
}
struct scroll_y {
bool enabled;
bool on;
float value;
}
ushort scroll_size;
int z_index;
}
// useful macro to start and end a div, capturing the trailing block
macro Ctx.@div(&ctx,
Size width = @grow, Size height = @grow,
LayoutDirection dir = ROW,
Anchor anchor = TOP_LEFT,
bool absolute = false, Point off = {},
bool scroll_x = false, bool scroll_y = false,
...;
@body()
)
{
ctx.div_begin(width, height, dir, anchor, absolute, off, scroll_x, scroll_y, $vasplat)!;
@body();
return ctx.div_end()!;
}
macro Ctx.div_begin(&ctx,
Size width = @grow(), Size height = @grow(),
LayoutDirection dir = ROW,
Anchor anchor = TOP_LEFT,
bool absolute = false, Point off = {},
bool scroll_x = false, bool scroll_y = false,
...
)
{
return ctx.div_begin_id(@compute_id($vasplat), width, height, dir, anchor, absolute, off, scroll_x, scroll_y);
}
fn void? Ctx.div_begin_id(&ctx,
Id id,
Size width, Size height,
LayoutDirection dir,
Anchor anchor,
bool absolute, Point off,
bool scroll_x, bool scroll_y
)
{
id = ctx.gen_id(id)!;
Elem* elem = ctx.get_elem(id, ETYPE_DIV)!;
Elem* parent = ctx.get_parent()!;
ctx.active_div = elem.tree_idx;
Style* style = ctx.styles.get_style(@str_hash("default"));
Style* slider_style = ctx.styles.get_style(@str_hash("slider"));
elem.div.scroll_x.enabled = scroll_x;
elem.div.scroll_y.enabled = scroll_y;
elem.div.scroll_size = slider_style.size ? slider_style.size : (style.size ? style.size : DEFAULT_STYLE.size);
elem.div.z_index = parent.div.z_index + 1;
// update layout with correct info
elem.layout = {
.w = width,
.h = height,
.dir = dir,
.anchor = anchor,
.content_offset = style.margin + style.border + style.padding,
.absolute = absolute,
};
if (absolute) {
elem.layout.origin.x = off.x;
elem.layout.origin.y = off.y;
}
ctx.push_rect(elem.bounds.pad(style.margin), elem.div.z_index, style)!;
// update the ctx scissor, it HAS to be after drawing the background
ctx.div_scissor = elem.bounds.pad(elem.layout.content_offset);
ctx.push_scissor(elem.bounds.pad(elem.layout.content_offset), elem.div.z_index)!;
elem.events = ctx.get_elem_events(elem);
// TODO: check active
// TODO: check resizeable
}
fn Id? Ctx.div_end(&ctx)
{
Elem* elem = ctx.get_active_div()!;
/* FIXME: this needs the absolute positioning to work
// set the scrollbar flag, is used in layout
Point cbc = elem.children_bounds.bottom_right();
Point bc = elem.bounds.bottom_right();
// horizontal overflow
elem.div.scroll_x.on = cbc.x > bc.x && elem.div.scroll_x.enabled;
// vertical overflow
elem.div.scroll_y.on = cbc.y > bc.y && elem.div.scroll_y.enabled;
Id hsid_raw = @str_hash("div_scrollbar_horizontal");
Id vsid_raw = @str_hash("div_scrollbar_vertical");
Id hsid_real = ctx.gen_id(@str_hash("div_scrollbar_horizontal"))!;
Id vsid_real = ctx.gen_id(@str_hash("div_scrollbar_vertical"))!;
short wdim = elem.div.scroll_y.on ? (ctx.focus_id == vsid_real || ctx.is_hovered(ctx.find_elem(vsid_real)) ? elem.div.scroll_size*2 : elem.div.scroll_size) : 0;
short hdim = elem.div.scroll_x.on ? (ctx.focus_id == hsid_real || ctx.is_hovered(ctx.find_elem(hsid_real)) ? elem.div.scroll_size*2 : elem.div.scroll_size) : 0;
if (elem.div.scroll_y.on) {
if (ctx.input.events.mouse_scroll && ctx.hover_id == elem.id) {
elem.div.scroll_y.value += ctx.input.mouse.scroll.y * 0.07f;
elem.div.scroll_y.value = math::clamp(elem.div.scroll_y.value, 0.0f, 1.0f);
}
ctx.slider_ver_id(vsid_raw, @exact(wdim), @exact(elem.bounds.h - hdim), &elem.div.scroll_y.value, max((float)bc.y / cbc.y, (float)0.15))!;
}
if (elem.div.scroll_x.on) {
if (ctx.input.events.mouse_scroll && ctx.hover_id == elem.id) {
elem.div.scroll_x.value += ctx.input.mouse.scroll.x * 0.07f;
elem.div.scroll_x.value = math::clamp(elem.div.scroll_x.value, 0.0f, 1.0f);
}
ctx.slider_hor_id(hsid_raw, @exact(elem.bounds.w - wdim), @exact(hdim), &elem.div.scroll_x.value, max((float)bc.x / cbc.x, (float)0.15))!;
}
*/
// the active_div returns to the parent of the current one
ctx.active_div = ctx.tree.parentof(ctx.active_div);
Elem* parent = ctx.get_parent()!;
ctx.div_scissor = parent.bounds.pad(parent.layout.content_offset);
ctx.reset_scissor(elem.div.z_index)!;
update_parent_size(elem, parent);
return elem.id;
}

View File

@ -1,16 +0,0 @@
module ugui;
macro Ctx.separator(&ctx, int width, int height, ...)
=> ctx.separator_id(@compute_id($vasplat), width, height);
fn void? Ctx.separator_id(&ctx, Id id, int width, int height)
{
id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id, ETYPE_NONE)!;
elem.layout.w = @exact((short)width);
elem.layout.h = @exact((short)height);
update_parent_size(elem, parent);
}

View File

@ -1,26 +0,0 @@
module ugui;
struct ElemSprite {
Id id;
}
macro Ctx.sprite(&ctx, String name, ...)
=> ctx.sprite_id(@compute_id($vasplat), name);
fn void? Ctx.sprite_id(&ctx, Id id, String name)
{
id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id, ETYPE_SPRITE)!;
Style* style = ctx.styles.get_style(@str_hash("sprite"));
Sprite* sprite = ctx.sprite_atlas.get(name)!;
elem.sprite.id = ctx.get_sprite_atlas_id(name);
elem.layout.w = elem.layout.children.w = @exact(sprite.w);
elem.layout.h = elem.layout.children.h = @exact(sprite.h);
update_parent_size(elem, parent);
Id tex_id = ctx.sprite_atlas.id;
return ctx.push_sprite(elem.bounds, sprite.uv(), tex_id, parent.div.z_index, type: sprite.type)!;
}

View File

@ -1,90 +0,0 @@
module ugui;
import std::io;
struct ElemText {
Id hash;
TextSize size;
TextEdit* te;
}
/* Layout some text without bounds.
* There is a limitation where the current frame bounds are based on the last frame, this is usually
* not a problem but it is in the situation where the text changes almost all frames.
*/
macro Ctx.text(&ctx, String text, ...)
=> ctx.text_id(@compute_id($vasplat), text);
fn void? Ctx.text_id(&ctx, Id id, String text)
{
id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id, ETYPE_TEXT)!;
Style* style = ctx.styles.get_style(@str_hash("text"));
Id text_hash = text.hash();
if (elem.flags.is_new || elem.text.hash != text_hash) {
elem.text.size = ctx.measure_string(text)!;
}
elem.text.hash = text_hash;
elem.layout.w = @fit(style.size);
elem.layout.h = @fit(style.size);
elem.layout.text = elem.text.size;
elem.layout.content_offset = style.margin + style.border + style.padding;
update_parent_size(elem, parent);
ctx.layout_string(text, elem.bounds.pad(elem.layout.content_offset), TOP_LEFT, parent.div.z_index, style.fg)!;
}
macro Ctx.text_box(&ctx, Size w, Size h, TextEdit* te, ...)
=> ctx.text_box_id(@compute_id($vasplat), w, h, te);
fn ElemEvents? Ctx.text_box_id(&ctx, Id id, Size w, Size h, TextEdit* te)
{
id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id, ETYPE_TEXT)!;
Style* style = ctx.styles.get_style(@str_hash("text-box"));
elem.text.te = te;
Id text_hash = te.to_string().hash();
if (elem.flags.is_new || elem.text.hash != text_hash) {
elem.text.size = ctx.measure_string(te.to_string())!;
}
elem.text.hash = text_hash;
elem.layout.w = w;
elem.layout.h = h;
elem.layout.text = elem.text.size;
elem.layout.content_offset = style.margin + style.border + style.padding;
update_parent_size(elem, parent);
// check input and update the text
elem.events = ctx.get_elem_events(elem);
if (elem.events.text_input || elem.events.key_press) {
ctx.text_edit(elem.text.te);
}
// draw the box
Rect bg_bounds = elem.bounds.pad(style.margin);
Rect text_bounds = elem.bounds.pad(elem.layout.content_offset);
ctx.push_rect(bg_bounds, parent.div.z_index, style)!;
Rect cur;
cur = ctx.layout_string(elem.text.te.to_string(), text_bounds, TOP_LEFT, parent.div.z_index, style.fg, elem.text.te.cursor)!;
// draw the cursor if the element has focus
cur.w = 2;
if (elem.events.has_focus) {
ctx.push_scissor(text_bounds, parent.div.z_index)!;
ctx.push_rect(cur, parent.div.z_index, &&(Style){.bg = style.fg})!;
ctx.reset_scissor(parent.div.z_index)!;
}
return elem.events;
}

View File

@ -0,0 +1,21 @@
#version 450
layout(set = 3, binding = 0) uniform Viewport {
ivec2 view;
};
layout(set = 2, binding = 0) uniform sampler2D tx;
layout(location = 0) in vec2 uv;
layout(location = 1) in vec4 color;
layout(location = 0) out vec4 fragColor;
void main()
{
ivec2 ts = textureSize(tx, 0);
vec2 fts = vec2(ts);
vec2 real_uv = uv / fts;
vec4 opacity = texture(tx, real_uv);
fragColor = vec4(color.rgb, color.a*opacity.r);
}

View File

@ -0,0 +1,35 @@
#version 450
layout(set = 3, binding = 0) uniform Viewport {
ivec2 view;
};
layout(set = 2, binding = 0) uniform sampler2D tx;
const float PX_RANGE = 4.0f;
layout(location = 0) in vec2 uv;
layout(location = 1) in vec4 color;
layout(location = 0) out vec4 fragColor;
float screen_px_range(vec2 uv) {
vec2 unit_range = vec2(PX_RANGE)/vec2(textureSize(tx, 0));
vec2 texel_size = vec2(1.0)/fwidth(uv);
return max(0.5*dot(unit_range, texel_size), 1.0);
}
float median(float r, float g, float b) {
return max(min(r, g), min(max(r, g), b));
}
void main() {
ivec2 ts = textureSize(tx, 0);
vec2 fts = vec2(ts);
vec2 real_uv = uv / fts;
vec3 msd = texture(tx, real_uv).rgb;
float sd = median(msd.r, msd.g, msd.b);
float distance = screen_px_range(real_uv)*(sd - 0.5);
float opacity = clamp(distance + 0.5, 0.0, 1.0);
fragColor = color * opacity;
}

View File

@ -0,0 +1,32 @@
#version 450
layout(set = 3, binding = 0) uniform Viewport {
ivec2 view;
};
layout(location = 0) in vec4 in_color;
layout(location = 1) in vec4 in_quad_size; // x,y, w,h
layout(location = 2) in float in_radius;
layout(location = 3) in float thickness;
layout(location = 0) out vec4 fragColor;
// SDF for a rounded rectangle given the centerpoint, half size and radius, all in pixels
float sdf_rr(vec2 p, vec2 half_size, float radius) {
vec2 q = abs(p) - half_size + radius;
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - radius;
}
const float smoothness = 0.9;
void main()
{
vec2 centerpoint = in_quad_size.xy + in_quad_size.zw * 0.5;
vec2 half_size = in_quad_size.zw * 0.5;
float distance = -sdf_rr(vec2(gl_FragCoord) - centerpoint, half_size, in_radius);
float alpha_out = smoothstep(0.0-smoothness, 0.0, distance);
float alpha_in = 1.0 - smoothstep(thickness-smoothness, thickness, distance);
fragColor = vec4(in_color.rgb, in_color.a * alpha_out * alpha_in);
}

View File

@ -0,0 +1,31 @@
#version 450
layout(set = 1, binding = 0) uniform Viewport {
ivec2 view;
};
layout(location = 0) in ivec2 position;
layout(location = 1) in ivec4 attr; // quad x,y,w,h
layout(location = 2) in ivec2 uv; // x,y in the texture
layout(location = 3) in uvec4 color;
layout(location = 0) out vec4 out_color;
layout(location = 1) out vec4 out_quad_size;
layout(location = 2) out float out_radius;
layout(location = 3) out float out_thickness;
void main()
{
// vertex position
ivec2 px_pos = attr.xy + position.xy * attr.zw;
vec2 clip_pos;
clip_pos.x = float(px_pos.x)*2.0 / view.x - 1.0;
clip_pos.y = -(float(px_pos.y)*2.0 / view.y - 1.0);
gl_Position = vec4(clip_pos, 0.0, 1.0);
out_color = vec4(color) / 255.0;
out_quad_size = vec4(attr);
out_radius = float(abs(uv.x));
out_thickness = float(uv.y - uv.x);
}

View File

@ -0,0 +1,18 @@
#version 450
layout(set = 3, binding = 0) uniform Viewport {
ivec2 view;
};
layout(location = 0) in vec2 uv;
layout(location = 0) out vec4 fragColor;
layout(set = 2, binding = 0) uniform sampler2D tx;
void main()
{
ivec2 ts = textureSize(tx, 0);
vec2 fts = vec2(ts);
vec2 real_uv = uv / fts;
fragColor = texture(tx, real_uv);
}

View File

@ -0,0 +1,28 @@
#version 450
layout(set = 1, binding = 0) uniform Viewport {
ivec2 view;
};
layout(location = 0) in ivec2 position;
layout(location = 1) in ivec4 attr; // quad x,y,w,h
layout(location = 2) in ivec2 in_uv;
layout(location = 3) in uvec4 color;
layout(location = 0) out vec2 out_uv;
layout(location = 1) out vec4 out_color;
void main()
{
// vertex position
ivec2 px_pos = attr.xy + position.xy * attr.zw;
vec2 clip_pos;
clip_pos.x = float(px_pos.x)*2.0 / view.x - 1.0;
clip_pos.y = -(float(px_pos.y)*2.0 / view.y - 1.0);
gl_Position = vec4(clip_pos, 0.0, 1.0);
vec2 px_uv = in_uv.xy + position.xy * attr.zw;
out_uv = vec2(px_uv);
out_color = vec4(color) / 255.0;
}

View File

@ -1,118 +0,0 @@
#version 450
/* Combined fragment shader to render UGUI commands */
// type values, these are the same as in renderer.c3
const uint TYPE_RECT = 0;
const uint TYPE_FONT = 1;
const uint TYPE_SPRITE = 2;
const uint TYPE_MSDF = 3;
// viewport size
layout(set = 3, binding = 0) uniform Viewport {
ivec2 view;
};
// textures
layout(set = 2, binding = 0) uniform sampler2D font_atlas;
layout(set = 2, binding = 1) uniform sampler2D sprite_atlas;
// inputs
layout(location = 0) in vec4 in_color;
layout(location = 1) in vec2 in_uv;
layout(location = 2) in vec4 in_quad_size;
layout(location = 3) in float in_radius;
layout(location = 4) flat in uint in_type;
// outputs
layout(location = 0) out vec4 fragColor;
// SDF for a rounded rectangle given the centerpoint, half size and radius, all in pixels
float sdf_rr(vec2 p, vec2 half_size, float radius) {
vec2 q = abs(p) - half_size + radius;
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - radius;
}
const float PX_RANGE = 4.0f;
float screen_px_range(vec2 uv, sampler2D tx) {
vec2 unit_range = vec2(PX_RANGE)/vec2(textureSize(tx, 0));
vec2 texel_size = vec2(1.0)/fwidth(uv);
return max(0.5*dot(unit_range, texel_size), 1.0);
}
float median(float r, float g, float b) {
return max(min(r, g), min(max(r, g), b));
}
// main for TYPE_RECT, draw a rouded rectangle with a SDF
void rect_main()
{
vec2 centerpoint = in_quad_size.xy + in_quad_size.zw * 0.5;
vec2 half_size = in_quad_size.zw * 0.5;
float distance = sdf_rr(vec2(gl_FragCoord) - centerpoint, half_size, in_radius);
float alpha = 1.0 - smoothstep(0.0, 1.5, distance);
fragColor = vec4(in_color.rgb, in_color.a * alpha);
}
// main for TYPE_SPRITE, draws a sprite sampled from an atlas
void sprite_main()
{
ivec2 ts = textureSize(sprite_atlas, 0);
vec2 fts = vec2(ts);
vec2 real_uv = in_uv / fts;
fragColor = texture(sprite_atlas, real_uv);
}
// main for TYPE_FONT, draws a character sampled from an atlas that contains only the alpha channel
void font_main()
{
ivec2 ts = textureSize(font_atlas, 0);
vec2 fts = vec2(ts);
vec2 real_uv = in_uv / fts;
vec4 opacity = texture(font_atlas, real_uv);
fragColor = vec4(in_color.rgb, in_color.a*opacity.r);
}
// main for TYPE_MSDF, draws a sprite that is stored as a multi-channel SDF
void msdf_main() {
ivec2 ts = textureSize(sprite_atlas, 0);
vec2 fts = vec2(ts);
vec2 real_uv = in_uv / fts;
vec3 msd = texture(sprite_atlas, real_uv).rgb;
float sd = median(msd.r, msd.g, msd.b);
float distance = screen_px_range(real_uv, sprite_atlas)*(sd - 0.5);
float opacity = clamp(distance + 0.5, 0.0, 1.0);
fragColor = in_color * opacity;
}
// shader main
void main()
{
switch (in_type) {
case TYPE_RECT:
rect_main();
break;
case TYPE_FONT:
font_main();
break;
case TYPE_SPRITE:
sprite_main();
break;
case TYPE_MSDF:
msdf_main();
break;
default:
// ERROR, invalid type, return magenta
fragColor = vec4(1.0, 0.0, 1.0, 1.0);
}
}

View File

@ -1,47 +0,0 @@
#version 450
/* Combined vertex shader to render UGUI commands */
// Viewport size in pixels
layout(set = 1, binding = 0) uniform Viewport {
ivec2 view;
};
// inputs
layout(location = 0) in ivec2 in_position;
layout(location = 1) in ivec4 in_attr; // quad x,y,w,h
layout(location = 2) in ivec2 in_uv;
layout(location = 3) in uvec4 in_color;
layout(location = 4) in uint in_type;
// outputs
layout(location = 0) out vec4 out_color;
layout(location = 1) out vec2 out_uv;
layout(location = 2) out vec4 out_quad_size;
layout(location = 3) out float out_radius;
layout(location = 4) out uint out_type;
void main()
{
// vertex position
ivec2 px_pos = in_attr.xy + in_position.xy * in_attr.zw;
vec2 clip_pos;
clip_pos.x = float(px_pos.x)*2.0 / view.x - 1.0;
clip_pos.y = -(float(px_pos.y)*2.0 / view.y - 1.0);
gl_Position = vec4(clip_pos, 0.0, 1.0);
// color output
out_color = vec4(in_color) / 255.0;
// uv output. only useful if the type is SPRITE
vec2 px_uv = in_uv.xy + in_position.xy * in_attr.zw;
out_uv = vec2(px_uv);
// quad size and radius output, only useful if type is RECT
out_quad_size = vec4(in_attr);
out_radius = float(abs(in_uv.x));
// type output
out_type = in_type;
}

View File

@ -2,34 +2,39 @@ default {
bg: #282828ff;
fg: #fbf1c7ff;
primary: #cc241dff;
secondary: #6c19ca8f;
secondary: #458588ff;
accent: #fabd2fff;
border: 1;
padding: 0;
margin: 0;
radius: 0;
}
button {
margin: 2;
margin: 2 2 2 2;
border: 2;
padding: 2;
radius: 10;
size: 32;
bg: #3c3836ff;
fg: #fbf1c7ff;
primary: #cc241dff;
secondary: #458588ff;
accent: #504945ff;
accent: #fabd2fff;
}
button-active {
margin: 2 2 2 2;
border: 2;
radius: 10;
bg: #504945ff;
fg: #fbf1c7ff;
primary: #cc241dff;
secondary: #cc241dff;
accent: #fabd2fff;
}
checkbox {
margin: 2;
margin: 2 2 2 2;
border: 2;
padding: 1;
radius: 10;
size: 20;
size: 16;
bg: #3c3836ff;
fg: #fbf1c7ff;
primary: #cc241dff;
@ -38,11 +43,10 @@ checkbox {
}
toggle {
margin: 2;
margin: 2 2 2 2;
border: 2;
padding: 1;
radius: 10;
size: 20;
radius: 0;
size: 16;
bg: #3c3836ff;
fg: #fbf1c7ff;
primary: #cc241dff;
@ -51,8 +55,8 @@ toggle {
}
slider {
margin: 2;
padding: 2;
margin: 2 2 2 2;
padding: 4 4 4 4;
border: 1;
radius: 4;
size: 8;
@ -62,14 +66,3 @@ slider {
secondary: #458588ff;
accent: #fabd2fff;
}
text-box {
bg: #4a4543ff;
fg: #fbf1c7ff;
primary: #cc241dff;
secondary: #458588ff;
accent: #fabd2fff;
border: 1;
padding: 4;
margin: 2;
}

View File

@ -6,7 +6,6 @@ import std::time;
import std::collections::ringbuffer;
import std::core::string;
import std::ascii;
import std::core::mem::allocator;
import sdlrenderer::ren;
import sdl3::sdl;
@ -14,6 +13,7 @@ alias Times = ringbuffer::RingBuffer{time::NanoDuration[128]};
fn void Times.print_stats(&times)
{
if (times.written == 0);
time::NanoDuration min, max, avg, x;
min = times.get(0);
for (usz i = 0; i < times.written; i++) {
@ -22,7 +22,7 @@ fn void Times.print_stats(&times)
if (x > max) { max = x; }
avg += x;
}
avg = (NanoDuration)((ulong)avg/128.0);
avg = (NanoDuration)((ulong)avg/times.written);
io::printfn("min=%s, max=%s, avg=%s", min, max, avg);
}
@ -33,6 +33,7 @@ struct TimeStats {
fn TimeStats Times.get_stats(&times)
{
if (times.written == 0) return {};
time::NanoDuration min, max, avg, x;
min = times.get(0);
for (usz i = 0; i < times.written; i++) {
@ -41,93 +42,96 @@ fn TimeStats Times.get_stats(&times)
if (x > max) { max = x; }
avg += x;
}
avg = (NanoDuration)((ulong)avg/128.0);
avg = (NanoDuration)((ulong)avg/times.written);
return {.min = min, .max = max, .avg = avg};
}
const char[*] VS_PATH = "resources/shaders/compiled/ugui.vert.spv";
const char[*] FS_PATH = "resources/shaders/compiled/ugui.frag.spv";
const char[*] MSDF_FS_PATH = "resources/shaders/compiled/msdf.frag.spv";
const char[*] SPRITE_FS_PATH = "resources/shaders/compiled/sprite.frag.spv";
const char[*] FONT_FS_PATH = "resources/shaders/compiled/font.frag.spv";
const char[*] RECT_FS_PATH = "resources/shaders/compiled/rect.frag.spv";
const char[*] SPRITE_VS_PATH = "resources/shaders/compiled/sprite.vert.spv";
const char[*] RECT_VS_PATH = "resources/shaders/compiled/rect.vert.spv";
const char[*] STYLESHEET_PATH = "resources/style.css";
const bool LIMIT_FPS = true;
const bool VSYNC = true;
fn int main(String[] args)
{
ArenaAllocator arena;
char[] mem = mem::new_array(char, 1024*1024);
defer (void)mem::free(mem);
arena.init(mem);
ugui::Ctx ui;
ui.init(&arena)!!;
ui.init()!!;
defer ui.free();
ren::Renderer ren;
ren.init("Ugui Test", 800, 600, VSYNC);
ren.init("Ugui Test", 800, 600, true);
defer ren.free();
ui.input_window_size(800, 600)!!;
// ========================================================================================== //
// FONT LOADING //
// ========================================================================================== //
// import font in the ui context
ui.load_font("font1", "resources/hack-nerd.ttf", 16)!!;
//
// FONT LOADING
//
{
// import font in the ui context
ui.load_font("font1", "resources/hack-nerd.ttf", 16)!!;
// create the rendering pipeline
ren.font_atlas_id = ui.get_font_id("font1");
ren.load_spirv_shader_from_file("UGUI_PIPELINE_FONT", SPRITE_VS_PATH, FONT_FS_PATH, 1, 0);
ren.create_pipeline("UGUI_PIPELINE_FONT", SPRITE);
// send the atlas to the gpu
Atlas* font_atlas = ui.get_font_atlas("font1")!!;
ren.new_texture("font1", JUST_ALPHA, font_atlas.buffer, font_atlas.width, font_atlas.height);
}
// set the renderer's font atlas
ren.font_atlas_id = ui.get_font_id("font1");
//
// ICON LOADING
//
{
// create the atlas and upload some icons
ui.sprite_atlas_create("icons", AtlasType.ATLAS_R8G8B8A8, 512, 512)!!;
ui.import_sprite_file_qoi("tux", "resources/tux.qoi")!!;
ui.import_sprite_file_qoi("tick", "resources/tick_sdf.qoi", SpriteType.SPRITE_MSDF)!!;
// create the rendering pipelines
ren.sprite_atlas_id = ui.get_sprite_atlas_id("icons");
// normal sprite pipeline
ren.load_spirv_shader_from_file("UGUI_PIPELINE_SPRITE", SPRITE_VS_PATH, SPRITE_FS_PATH, 1, 0);
ren.create_pipeline("UGUI_PIPELINE_SPRITE", SPRITE);
// msdf sprite pipeline
ren.load_spirv_shader_from_file("UGUI_PIPELINE_SPRITE_MSDF", SPRITE_VS_PATH, MSDF_FS_PATH, 1, 0);
ren.create_pipeline("UGUI_PIPELINE_SPRITE_MSDF", SPRITE);
// upload the atlas to the gpu
Atlas atlas = ui.sprite_atlas.atlas;
ren.new_texture("icons", FULL_COLOR, atlas.buffer, atlas.width, atlas.height);
}
// send the atlas to the gpu
Atlas* font_atlas = ui.get_font_atlas("font1")!!;
ren.new_texture("font1", JUST_ALPHA, font_atlas.buffer, font_atlas.width, font_atlas.height);
//
// RECT PIPELINE
//
ren.load_spirv_shader_from_file("UGUI_PIPELINE_RECT", RECT_VS_PATH, RECT_FS_PATH, 0, 0);
ren.create_pipeline("UGUI_PIPELINE_RECT", RECT);
// ========================================================================================== //
// ICON LOADING //
// ========================================================================================== //
// create the atlas and upload some icons
ui.sprite_atlas_create("icons", AtlasType.ATLAS_R8G8B8A8, 512, 512)!!;
ui.import_sprite_file_qoi("tux", "resources/tux.qoi")!!;
ui.import_sprite_file_qoi("tick", "resources/tick_sdf.qoi", SpriteType.SPRITE_MSDF)!!;
// set the renderer's sprite atlas
ren.sprite_atlas_id = ui.get_sprite_atlas_id("icons");
// upload the atlas to the gpu
Atlas atlas = ui.sprite_atlas.atlas;
ren.new_texture("icons", FULL_COLOR, atlas.buffer, atlas.width, atlas.height);
// ========================================================================================== //
// PIPELINE SETUP //
// ========================================================================================== //
ren.load_spirv_shader_from_file("UGUI_PIPELINE", VS_PATH, FS_PATH, 2, 0);
ren.create_pipeline("UGUI_PIPELINE", RECT);
// ========================================================================================== //
// CSS INPUT //
// ========================================================================================== //
// CSS INPUT
io::printfn("imported %d styles", ui.import_style_from_file(STYLESHEET_PATH));
// ========================================================================================== //
// OTHER VARIABLES //
// ========================================================================================== //
TextEdit te;
te.buffer = mem::new_array(char, 256);
defer mem::free(te.buffer);
isz frame;
double fps;
bool toggle = true;
time::Clock clock;
time::Clock fps_clock;
time::Clock sleep_clock;
Times ui_times;
Times draw_times;
// ========================================================================================== //
// MAIN LOOP //
// ========================================================================================== //
//
// MAIN LOOP
//
sdl::start_text_input(ren.win);
sdl::Event e;
@ -138,36 +142,26 @@ fn int main(String[] args)
clock.mark();
fps_clock.mark();
sleep_clock.mark();
do {
switch (e.type) {
case EVENT_QUIT:
case EVENT_QUIT:
quit = true;
case EVENT_KEY_UP:
ui.input_key_release();
nextcase;
case EVENT_KEY_UP: nextcase;
case EVENT_KEY_DOWN:
ui.input_key_press();
if (e.key.repeat) ui.input_key_repeat();
mod.rctrl = e.key.key == K_RCTRL ? !!(e.type == EVENT_KEY_DOWN) : mod.rctrl;
mod.lctrl = e.key.key == K_LCTRL ? !!(e.type == EVENT_KEY_DOWN) : mod.lctrl;
mod.bkspc = e.key.key == K_BACKSPACE ? !!(e.type == EVENT_KEY_DOWN) : mod.bkspc;
mod.del = e.key.key == K_DELETE ? !!(e.type == EVENT_KEY_DOWN) : mod.del;
mod.up = e.key.key == K_UP ? !!(e.type == EVENT_KEY_DOWN) : mod.up;
mod.down = e.key.key == K_DOWN ? !!(e.type == EVENT_KEY_DOWN) : mod.down;
mod.left = e.key.key == K_LEFT ? !!(e.type == EVENT_KEY_DOWN) : mod.left;
mod.right = e.key.key == K_RIGHT ? !!(e.type == EVENT_KEY_DOWN) : mod.right;
// pressing ctrl+key or alt+key does not generate a character as such no
// TEXT_INPUT event is generated. When those keys are pressed we have to
// pressing ctrl+key or alt+key does not generate a character as such no
// TEXT_INPUT event is generated. When those keys are pressed we have to
// do manual text input, bummer
if (e.type == EVENT_KEY_DOWN && (mod.lctrl || mod.rctrl)) {
if (ascii::is_alnum_m((uint)e.key.key)) {
ui.input_char((char)e.key.key);
}
}
if (e.type == EVENT_KEY_DOWN && e.key.key == K_RETURN) ui.input_char('\n');
case EVENT_TEXT_INPUT:
@ -207,33 +201,101 @@ fn int main(String[] args)
if (ui.check_key_combo(ugui::KMOD_CTRL, "q")) quit = true;
const String APPLICATION = "calculator";
$switch APPLICATION:
$case "debug":
debug_app(&ui);
$case "calculator":
calculator(&ui, &te);
$endswitch
ui.div_begin({.w=-100})!!;
{
ui.layout_set_column()!!;
if (ui.button({0,0,30,30}, toggle)!!.mouse_press) {
io::printn("press button0");
toggle = !toggle;
}
//ui.layout_next_column()!!;
if (ui.button({0,0,30,30})!!.mouse_press) {
io::printn("press button1");
}
//ui.layout_next_column()!!;
if (ui.button({0,0,30,30})!!.mouse_release) {
io::printn("release button2");
}
ui.layout_set_row()!!;
ui.layout_next_row()!!;
static float rf, gf, bf, af;
ui.slider_ver({0,0,30,100}, &rf)!!;
ui.slider_ver({0,0,30,100}, &gf)!!;
ui.slider_ver({0,0,30,100}, &bf)!!;
ui.slider_ver({0,0,30,100}, &af)!!;
ui.layout_next_column()!!;
ui.text_unbounded("Ciao Mamma\nAbilità ⚡\n'\udb80\udd2c'")!!;
ui.layout_next_column()!!;
ui.button_label("Continua!")!!;
ui.layout_next_row()!!;
static bool check;
ui.checkbox("", {}, &check, "tick")!!;
ui.checkbox("", {}, &check)!!;
ui.toggle("", {}, &toggle)!!;
};
ui.sprite("tux")!!;
static char[128] text_box = "ciao mamma";
static usz text_len = "ciao mamma".len;
ui.text_box({0,0,200,200}, text_box[..], &text_len)!!;
ui.div_end()!!;
ui.div_begin(ugui::DIV_FILL, scroll_x: true, scroll_y: true)!!;
{
ui.layout_set_column()!!;
static float slider2 = 0.5;
if (ui.slider_ver({0,0,30,100}, &slider2)!!.update) {
io::printfn("other slider: %f", slider2);
}
ui.button({0,0,50,50})!!;
ui.button({0,0,50,50})!!;
ui.button({0,0,50,50})!!;
ui.button({0,0,50,50})!!;
if (toggle) {
ui.button({0,0,50,50})!!;
ui.button({0,0,50,50})!!;
ui.button({0,0,50,50})!!;
ui.button({0,0,50,50})!!;
}
ui.layout_next_column()!!;
ui.layout_set_row()!!;
static float f1, f2;
ui.slider_hor({0,0,100,30}, &f1)!!;
ui.slider_hor({0,0,100,30}, &f2)!!;
};
ui.div_end()!!;
// Timings counter
TimeStats dts = draw_times.get_stats();
TimeStats uts = ui_times.get_stats();
ui.@div(ugui::@fit(), ugui::@fit(), COLUMN, TOP_LEFT, true) {
ui.text(string::tformat("frame %d, fps = %.2f", frame, fps))!!;
ui.text(string::tformat("ui avg: %s\ndraw avg: %s\nTOT: %s", uts.avg, dts.avg, uts.avg+dts.avg))!!;
}!!;
ui.layout_set_floating()!!;
// FIXME: I cannot anchor shit to the bottom of the screen
ui.div_begin({0, ui.height-150, -300, 150})!!;
{
ui.layout_set_column()!!;
ui.text_unbounded(string::tformat("frame %d, fps = %.2f", frame, fps))!!;
ui.text_unbounded(string::tformat("ui avg: %s\ndraw avg: %s\nTOT: %s", uts.avg, dts.avg, uts.avg+dts.avg))!!;
ui.text_unbounded(string::tformat("%s %s", mod.lctrl, (String)ui.input.keyboard.text[..]))!!;
};
ui.div_end()!!;
ui.frame_end()!!;
/* End UI Handling */
ui_times.push(clock.mark());
//ui_times.print_stats();
/* Start UI Drawing */
ren.begin_render(true);
ren.render_ugui(&ui.cmd_queue);
ren.end_render();
draw_times.push(clock.mark());
@ -241,190 +303,12 @@ $endswitch
/* End Drawing */
// wait for the next event, timeout after 100ms
int timeout = LIMIT_FPS ? (int)(100.0-sleep_clock.mark().to_ms()-0.5) : 0;
sdl::wait_event_timeout(&e, timeout);
sdl::wait_event_timeout(&e, (int)(100.0-sleep_clock.mark().to_ms()-0.5));
fps = 1.0 / fps_clock.mark().to_sec();
frame++;
}
return 0;
}
/*
fn void debug_app(ugui::Ctx* ui)
{
static bool toggle;
ui.div_begin({.w=-100})!!;
{
ui.layout_set_column()!!;
if (ui.button(icon:"tux")!!.mouse_press) {
io::printn("press button0");
toggle = !toggle;
}
//ui.layout_next_column()!!;
if (ui.button(label: "ciao", icon: "tick")!!.mouse_press) {
io::printn("press button1");
}
//ui.layout_next_column()!!;
if (ui.button()!!.mouse_release) {
io::printn("release button2");
}
ui.layout_set_row()!!;
ui.layout_next_row()!!;
static float rf, gf, bf, af;
ui.slider_ver({0,0,30,100}, &rf)!!;
ui.slider_ver({0,0,30,100}, &gf)!!;
ui.slider_ver({0,0,30,100}, &bf)!!;
ui.slider_ver({0,0,30,100}, &af)!!;
ui.layout_next_column()!!;
ui.text_unbounded("Ciao Mamma\nAbilità ⚡\n'\udb80\udd2c'")!!;
ui.layout_next_column()!!;
ui.button("Continua!")!!;
ui.layout_next_row()!!;
static bool check;
ui.checkbox("", {}, &check, "tick")!!;
ui.checkbox("", {}, &check)!!;
ui.toggle("", {}, &toggle)!!;
ui.sprite("tux")!!;
static char[128] text_box = "ciao mamma";
static usz text_len = "ciao mamma".len;
ui.text_box({0,0,200,200}, text_box[..], &text_len)!!;
};
ui.div_end()!!;
ui.div_begin(ugui::DIV_FILL, scroll_x: true, scroll_y: true)!!;
{
ui.layout_set_column()!!;
static float slider2 = 0.5;
if (ui.slider_ver({0,0,30,100}, &slider2)!!.update) {
io::printfn("other slider: %f", slider2);
}
ui.button()!!;
ui.button()!!;
ui.button()!!;
ui.button()!!;
if (toggle) {
ui.button()!!;
ui.button()!!;
ui.button()!!;
ui.button()!!;
}
ui.layout_next_column()!!;
ui.layout_set_row()!!;
static float f1, f2;
ui.slider_hor({0,0,100,30}, &f1)!!;
ui.slider_hor({0,0,100,30}, &f2)!!;
};
ui.div_end()!!;
}
*/
import std::os::process;
fn void calculator(ugui::Ctx* ui, TextEdit* te)
{
static char[128] buffer;
static usz len;
bool eval;
// keyboard input
switch(ui.get_keys()) {
case "+": nextcase;
case "-": nextcase;
case "*": nextcase;
case "/": nextcase;
case "(": nextcase;
case ")": nextcase;
case ".": nextcase;
case "0": nextcase;
case "1": nextcase;
case "2": nextcase;
case "3": nextcase;
case "4": nextcase;
case "5": nextcase;
case "6": nextcase;
case "7": nextcase;
case "8": nextcase;
case "9":
buffer[len++] = ui.get_keys()[0];
case "\n":
eval = len != 0;
case "c":
len = 0;
case "d":
if (len > 0) len--;
}
// ui input/output
ui.@div(ugui::@grow(), ugui::@grow(), ROW, CENTER) { // center everything on the screen
ui.@div(ugui::@fit(), ugui::@fit(), COLUMN, TOP_LEFT) {
ui.@div(ugui::@grow(), ugui::@fit(), ROW, CENTER) {ui.text("SHITTY AHH CALCULATOR")!!;}!!;
ui.@div(ugui::@grow(), ugui::@exact(100), ROW, RIGHT) {
ui.text((String)buffer[:len])!!;
}!!;
ui.@div(ugui::@fit(), ugui::@fit(), ROW, TOP_LEFT) {
ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) {
ui.button("7")!!.mouse_press ? buffer[len++] = '7' : 0;
ui.button("4")!!.mouse_press ? buffer[len++] = '4' : 0;
ui.button("1")!!.mouse_press ? buffer[len++] = '1' : 0;
ui.button("0")!!.mouse_press ? buffer[len++] = '0' : 0;
}!!;
ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) {
ui.button("8")!!.mouse_press ? buffer[len++] = '8' : 0;
ui.button("5")!!.mouse_press ? buffer[len++] = '5' : 0;
ui.button("2")!!.mouse_press ? buffer[len++] = '2' : 0;
ui.button(".")!!.mouse_press ? buffer[len++] = '.' : 0;
}!!;
ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) {
ui.button("9")!!.mouse_press ? buffer[len++] = '9' : 0;
ui.button("6")!!.mouse_press ? buffer[len++] = '6' : 0;
ui.button("3")!!.mouse_press ? buffer[len++] = '3' : 0;
ui.button("(")!!.mouse_press ? buffer[len++] = '(' : 0;
}!!;
ui.@div(ugui::@exact(10), ugui::@exact(10)) {}!!;
ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) {
ui.button("x")!!.mouse_press ? buffer[len++] = '*' : 0;
ui.button("/")!!.mouse_press ? buffer[len++] = '/' : 0;
ui.button("+")!!.mouse_press ? buffer[len++] = '+' : 0;
ui.button(")")!!.mouse_press ? buffer[len++] = ')' : 0;
}!!;
ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) {
ui.button("C")!!.mouse_press ? len = 0 : 0;
ui.button("D")!!.mouse_press ? len > 0 ? len-- : 0 : 0;
ui.button("-")!!.mouse_press ? buffer[len++] = '-' : 0;
// eval the expression with 'bc'
if (ui.button("=")!!.mouse_press || eval) {
char[128] out;
String y = string::tformat("echo '%s' | bc", (String)buffer[:len]);
String x = process::execute_stdout_to_buffer(out[:128], (String[]){"sh", "-c", y}) ?? "";
buffer[:x.len] = x[..];
len = x.len;
}
}!!;
}!!;
ui.@div(ugui::@grow(), ugui::@fit(), anchor: CENTER) {
static bool state;
ui.checkbox("boolean", &state, "tick")!!;
ui.sprite("tux")!!;
ui.toggle("lmao", &state)!!;
}!!;
ui.@div(ugui::@grow(), ugui::@exact(50), anchor: CENTER, scroll_y: true) {
static float f;
ui.slider_hor(ugui::@exact(100), ugui::@exact(20), &f)!!;
ui.slider_ver(ugui::@exact(20), ugui::@exact(100), &f)!!;
}!!;
ui.@div(ugui::@grow(), ugui::@fit(), anchor: CENTER, scroll_y: true) {
ui.text_box(ugui::@grow(), ugui::@exact(100), te)!!;
}!!;
}!!; }!!;
}

View File

@ -1,8 +1,8 @@
// extends the List type to search elements that have a type.id property
<*
@require $defined((Type){}.id) : `No .id member found in the type`
*>
module idlist{Type};
// extends the List type to search elements that have a type.id property
// TODO: check that type has an id
import std::collections::list;
alias IdList = List{Type};
@ -22,61 +22,16 @@ macro Type* IdList.get_from_id(&self, id)
return null;
}
module sdlrenderer::ren;
// 2D renderer for ugui, based on SDL3 using the new GPU API
module sdlrenderer::ren;
import std::io;
import std::core::mem;
import sdl3::sdl;
import idlist;
import ugui;
// ============================================================================================== //
// CONSTANTS //
// ============================================================================================== //
const int DEBUG = 1;
const bool CYCLE = true;
const int MAX_QUAD_BATCH = 2048;
// ============================================================================================== //
// STRUCTURES //
// ============================================================================================== //
// How each vertex is represented in the gpu
struct Vertex {
short x, y;
}
// Attributes of each quad instance
struct QuadAttributes {
struct pos {
short x, y, w, h;
}
struct uv {
short u, v;
}
uint color;
uint type;
}
// A single quad
struct Quad {
struct vertices {
Vertex v1,v2,v3,v4;
}
struct indices {
short i1,i2,i3,i4,i5,i6;
}
}
// the viewport size uniform passed to the gpu
struct ViewsizeUniform @align(16) {
int w, h;
}
struct Shader {
sdl::GPUShader* frag;
sdl::GPUShader* vert;
@ -96,6 +51,7 @@ struct Texture {
}
// The GPU buffers that contain quad info, the size is determined by MAX_QUAD_BATCH
const int MAX_QUAD_BATCH = 2048;
struct QuadBuffer {
sdl::GPUBuffer* vert_buf; // on-gpu vertex buffer
sdl::GPUBuffer* idx_buf; // on-gpu index buffer
@ -130,20 +86,42 @@ struct Renderer {
Id sprite_atlas_id;
Id font_atlas_id;
int scissor_x, scissor_y, scissor_w, scissor_h;
int scissor_x, scissor_y, scissor_w, scissor_h;
}
// How each vertex is represented in the gpu
struct Vertex {
short x, y;
}
// ============================================================================================== //
// RENDERERER METHODS //
// ============================================================================================== //
// Attributes of each quad instance
struct QuadAttributes {
struct pos {
short x, y, w, h;
}
struct uv {
short u, v;
}
uint color;
}
// A single quad
struct Quad {
struct vertices {
Vertex v1,v2,v3,v4;
}
struct indices {
short i1,i2,i3,i4,i5,i6;
}
}
struct ViewsizeUniform @align(16) {
int w, h;
}
const int DEBUG = 1;
const bool CYCLE = true;
/* Initialize the renderer structure, this does a couple of things
* 1. Initializes the SDL video subsystem with the correct hints
* 2. Creates a window and attaches it to the gpu device
* 3. Allocates the quad buffer and uploads the quad mesh to the GPU
*/
fn void Renderer.init(&self, ZString title, uint width, uint height, bool vsync)
{
// set wayland hint automagically
@ -162,7 +140,7 @@ $if DEBUG == 0:
}
$else
// in debug mode set the video driver to X11 because renderdoc
// doesn't support debugging in wayland yet.
// doesn't support debugging in wayland yet.
sdl::set_hint(sdl::HINT_VIDEO_DRIVER, "x11");
sdl::set_hint(sdl::HINT_RENDER_GPU_DEBUG, "1");
$endif
@ -221,6 +199,7 @@ $endif
unreachable("failed to initialize quad buffer (index): %s", sdl::get_error());
}
// upload the quad mesh
GPUTransferBuffer *ts = sdl::create_gpu_transfer_buffer(self.gpu,
&&(GPUTransferBufferCreateInfo){.usage = GPU_TRANSFERBUFFERUSAGE_UPLOAD, .size = Quad.sizeof}
@ -254,7 +233,7 @@ $endif
quad.indices.i4 = 1; // v2
quad.indices.i5 = 2; // v3
quad.indices.i6 = 3; // v4
sdl::unmap_gpu_transfer_buffer(self.gpu, ts);
GPUCommandBuffer* cmd = sdl::acquire_gpu_command_buffer(self.gpu);
@ -264,13 +243,13 @@ $endif
GPUCopyPass* cpy = sdl::begin_gpu_copy_pass(cmd);
// upload vertices
sdl::upload_to_gpu_buffer(cpy,
sdl::upload_to_gpu_buffer(cpy,
&&(GPUTransferBufferLocation){.transfer_buffer = ts, .offset = Quad.vertices.offsetof},
&&(GPUBufferRegion){.buffer = qb.vert_buf, .offset = 0, .size = Quad.vertices.sizeof},
false
);
// upload indices
sdl::upload_to_gpu_buffer(cpy,
sdl::upload_to_gpu_buffer(cpy,
&&(GPUTransferBufferLocation){.transfer_buffer = ts, .offset = Quad.indices.offsetof},
&&(GPUBufferRegion){.buffer = qb.idx_buf, .offset = 0, .size = Quad.indices.sizeof},
false
@ -299,13 +278,6 @@ $endif
qb.initialized = true;
}
/* Frees all the renderer structures:
* - Frees all textures and pipelines
* - Releases all GPU transfer buffers
* - Closes the main window
* - Releases the GPU device
*/
fn void Renderer.free(&self)
{
foreach (&s: self.shaders) {
@ -338,24 +310,17 @@ fn void Renderer.free(&self)
sdl::quit();
}
fn void Renderer.resize_window(&self, uint width, uint height)
{
sdl::set_window_size(self.win, width, height);
}
fn void Renderer.get_window_size(&self, int* width, int* height)
{
sdl::get_window_size_in_pixels(self.win, width, height);
}
// ============================================================================================== //
// SHADER LOADING //
// ============================================================================================== //
// Both the vertex shader and fragment shader have an implicit uniform buffer at binding 0 that
// contains the viewport size. It is populated automatically at every begin_render() call
fn void Renderer.load_spirv_shader_from_mem(&self, String name, char[] vert_code, char[] frag_code, uint textures, uint uniforms)
@ -413,8 +378,7 @@ fn void Renderer.load_spirv_shader_from_mem(&self, String name, char[] vert_code
self.shaders.push(s);
}
fn void Renderer.load_spirv_shader_from_file(&self, String name, String vert_path, String frag_path, uint textures, uint uniforms)
fn void Renderer.load_spirv_shader_from_file(&self, String name, String vert_path, String frag_path, uint textures, uint uniforms)
{
if (vert_path == "" || frag_path == "") {
unreachable("need both a vertex shader and fragment shader path");
@ -438,12 +402,6 @@ fn void Renderer.load_spirv_shader_from_file(&self, String name, String vert_pat
self.load_spirv_shader_from_mem(name, vert_code, frag_code, textures, uniforms);
}
// ============================================================================================== //
// PIPELINE CREATION //
// ============================================================================================== //
// this describes what we want to draw, since for drawing different things we have to change
// the GPUPrimitiveType and GPURasterizerState for the pipeline.
enum PipelineType : (GPUPrimitiveType primitive_type, GPURasterizerState raster_state) {
@ -467,8 +425,8 @@ fn void Renderer.create_pipeline(&self, String shader_name, PipelineType type)
.fragment_shader = s.frag,
// This structure specifies how the vertex buffer looks in memory, what it contains
// and what is passed where to the gpu. Each vertex has three attributes, position,
// color and uv coordinates. Since this is a 2D pixel-based renderer the position
// is represented by two floats, the color as 32 bit rgba and the uv also as intgers.
// color and uv coordinates. Since this is a 2D pixel-based renderer the position
// is represented by two floats, the color as 32 bit rgba and the uv also as intgers.
.vertex_input_state = {
// the description of each vertex buffer, for now I use only one buffer
.vertex_buffer_descriptions = (GPUVertexBufferDescription[]){
@ -509,15 +467,9 @@ fn void Renderer.create_pipeline(&self, String shader_name, PipelineType type)
.buffer_slot = 1,
.format = GPU_VERTEXELEMENTFORMAT_UBYTE4,
.offset = QuadAttributes.color.offsetof,
},
{ // at location four there is the quad type
.location = 4,
.buffer_slot = 1,
.format = GPU_VERTEXELEMENTFORMAT_UINT,
.offset = QuadAttributes.type.offsetof,
}
},
.num_vertex_attributes = 5,
.num_vertex_attributes = 4,
},
// the pipeline's primitive type and rasterizer state differs based on what needs to
// be drawn
@ -561,21 +513,12 @@ fn void Renderer.create_pipeline(&self, String shader_name, PipelineType type)
self.pipelines.push(p);
}
// ============================================================================================== //
// TEXTURE LOADING //
// ============================================================================================== //
// NOTE: with TEXTUREUSAGE_SAMPLER the texture format cannot be intger _UINT so it has to be nermalized
enum TextureType : (GPUTextureFormat format) {
FULL_COLOR = GPU_TEXTUREFORMAT_R8G8B8A8_UNORM,
JUST_ALPHA = GPU_TEXTUREFORMAT_R8_UNORM
}
}
// This macro wraps new_texture_by_id() by accepting either a name or an id directly
macro void Renderer.new_texture(&self, name_or_id, TextureType type, char[] pixels, uint width, uint height)
{
$switch $typeof(name_or_id):
@ -585,8 +528,6 @@ macro void Renderer.new_texture(&self, name_or_id, TextureType type, char[] pixe
$endswitch
}
// This macro wraps update_texture_by_id() by accepting either a name or an id directly
macro void Renderer.update_texture(&self, name_or_id, char[] pixels, uint width, uint height, uint x = 0, uint y = 0)
{
$switch $typeof(name_or_id):
@ -597,11 +538,10 @@ macro void Renderer.update_texture(&self, name_or_id, char[] pixels, uint width,
}
/* Create a new gpu texture from a pixel buffer, the format has to be specified.
* The new texture is given an id and pushed into the renderer's texture list.
*/
// create a new gpu texture from a pixel buffer, the format has to be specified
// the new texture s given an id and pushed into a texture list
fn void Renderer.new_texture_by_id(&self, Id id, TextureType type, char[] pixels, uint width, uint height)
{
{
// the texture description
GPUTextureCreateInfo tci = {
.type = GPU_TEXTURETYPE_2D,
@ -647,11 +587,6 @@ fn void Renderer.new_texture_by_id(&self, Id id, TextureType type, char[] pixels
self.update_texture_by_id(id, pixels, width, height, 0, 0);
}
/* Updates a texture on the gpu.
* pixels: the pixel array that contais the texture content
* width, height: the size of the
*/
fn void Renderer.update_texture_by_id(&self, Id id, char[] pixels, uint width, uint height, uint x, uint y)
{
Texture* t = self.textures.get_from_id(id);
@ -705,34 +640,23 @@ fn void Renderer.update_texture_by_id(&self, Id id, char[] pixels, uint width, u
}
// ============================================================================================== //
// RENDER COMMANDS //
// ============================================================================================== //
const uint TYPE_RECT = 0;
const uint TYPE_FONT = 1;
const uint TYPE_SPRITE = 2;
const uint TYPE_MSDF = 3;
fn bool Renderer.push_sprite(&self, short x, short y, short w, short h, short u, short v, uint color = 0xffffffff, uint type)
fn bool Renderer.push_sprite(&self, short x, short y, short w, short h, short u, short v, uint color = 0xffffffff)
{
QuadAttributes qa = {
.pos = {.x = x, .y = y, .w = w, .h = h},
.uv = {.u = u, .v = v},
.color = color,
.type = type,
.color = color
};
return self.map_quad(qa);
}
fn bool Renderer.push_quad(&self, short x, short y, short w, short h, uint color, ushort radius, uint type)
fn bool Renderer.push_quad(&self, short x, short y, short w, short h, uint color, ushort radius = 0, ushort thickness = 0)
{
QuadAttributes qa = {
.pos = {.x = x, .y = y, .w = w, .h = h},
.uv = {.u = radius, .v = radius},
.color = color,
.type = type
.uv = {.u = radius, .v = radius+thickness},
.color = color
};
return self.map_quad(qa);
@ -770,7 +694,7 @@ fn void Renderer.upload_quads(&self)
GPUCopyPass* cpy = sdl::begin_gpu_copy_pass(cmd);
// upload quad attributes
sdl::upload_to_gpu_buffer(cpy,
sdl::upload_to_gpu_buffer(cpy,
&&(GPUTransferBufferLocation){.transfer_buffer = qb.attr_ts, .offset = 0},
&&(GPUBufferRegion){.buffer = qb.attr_buf, .offset = 0, .size = QuadAttributes.sizeof * qb.count},
false
@ -822,7 +746,7 @@ fn void Renderer.begin_render(&self, bool clear_screen)
sdl::push_gpu_fragment_uniform_data(self.render_cmdbuf, 0, &v, ViewsizeUniform.sizeof);
if (clear_screen) {
GPURenderPass* pass = sdl::begin_gpu_render_pass(self.render_cmdbuf,
GPURenderPass* pass = sdl::begin_gpu_render_pass(self.render_cmdbuf,
&&(GPUColorTargetInfo){
.texture = self.swapchain_texture,
.mip_level = 0,
@ -847,17 +771,15 @@ fn void Renderer.begin_render(&self, bool clear_screen)
}
}
fn void Renderer.end_render(&self)
{
sdl::submit_gpu_command_buffer(self.render_cmdbuf);
self.reset_quads();
}
fn void Renderer.start_render_pass(&self, String pipeline_name)
{
self.render_pass = sdl::begin_gpu_render_pass(self.render_cmdbuf,
self.render_pass = sdl::begin_gpu_render_pass(self.render_cmdbuf,
&&(GPUColorTargetInfo){
.texture = self.swapchain_texture,
.mip_level = 0,
@ -888,43 +810,19 @@ fn void Renderer.start_render_pass(&self, String pipeline_name)
sdl::bind_gpu_graphics_pipeline(self.render_pass, p);
}
fn void Renderer.end_render_pass(&self)
{
sdl::end_gpu_render_pass(self.render_pass);
sdl::end_gpu_render_pass(self.render_pass);
}
fn void Renderer.bind_textures(&self, String... texture_names)
fn void Renderer.bind_texture(&self, String texture_name)
{
// TODO: bind in one pass
foreach (idx, name: texture_names) {
ren::Texture* tx = self.textures.get_from_name(name);
if (tx == null) {
unreachable("texture '%s' was not registered", name);
}
sdl::bind_gpu_fragment_samplers(self.render_pass, (uint)idx,
(GPUTextureSamplerBinding[]){{.texture = tx.texture, .sampler = tx.sampler}}, 1
);
}
ren::Texture* tx = self.textures.get_from_name(texture_name);
sdl::bind_gpu_fragment_samplers(self.render_pass, 0,
(GPUTextureSamplerBinding[]){{.texture = tx.texture, .sampler = tx.sampler}}, 1
);
}
fn void Renderer.bind_textures_id(&self, ugui::Id... texture_ids)
{
// TODO: bind in one pass
foreach (idx, id: texture_ids) {
ren::Texture* tx = self.textures.get_from_id(id);
if (tx == null) {
unreachable("texture [%d] was not registered", id);
}
sdl::bind_gpu_fragment_samplers(self.render_pass, (uint)idx,
(GPUTextureSamplerBinding[]){{.texture = tx.texture, .sampler = tx.sampler}}, 1
);
}
}
fn void Renderer.set_scissor(&self, int x, int y, int w, int h)
{
// in vulkan scissor size must be positive, clamp to zero
@ -954,10 +852,10 @@ fn void Renderer.reset_scissor(&self)
* - draw
* - push uniform
* - draw
*
*
* 2. The GPU buffers are read per-command-buffer and not per
* render pass. So I cannot override an element in the buffer
* before submitting the command buffer.
* before submitting the command buffer.
*/
/// === END NOTES ===
@ -968,31 +866,29 @@ fn void Renderer.render_ugui(&self, CmdQueue* queue)
foreach (&c : queue) {
if (c.type == CMD_RECT) {
CmdRect r = c.rect;
self.push_quad(r.rect.x, r.rect.y, r.rect.w, r.rect.h, r.color.to_uint(), r.radius, TYPE_RECT);
self.push_quad(r.rect.x, r.rect.y, r.rect.w, r.rect.h, r.color.to_uint(), r.radius, r.thickness);
} else if (c.type == CMD_SPRITE) {
CmdSprite s = c.sprite;
uint type;
if (s.texture_id == self.font_atlas_id) {
type = TYPE_FONT;
} else if (s.texture_id == self.sprite_atlas_id && s.type == SPRITE_NORMAL) {
type = TYPE_SPRITE;
} else if (s.texture_id == self.sprite_atlas_id && s.type == SPRITE_MSDF) {
type = TYPE_MSDF;
} else {
unreachable("unrecognized command type");
}
self.push_sprite(s.rect.x, s.rect.y, s.texture_rect.w, s.texture_rect.h, s.texture_rect.x, s.texture_rect.y, s.hue.to_uint(), type);
self.push_sprite(s.rect.x, s.rect.y, s.texture_rect.w, s.texture_rect.h, s.texture_rect.x, s.texture_rect.y, s.hue.to_uint());
}
}
self.upload_quads();
self.start_render_pass("UGUI_PIPELINE");
self.bind_textures_id(self.font_atlas_id, self.sprite_atlas_id);
uint calls = 0;
Cmd* last_command;
uint off;
uint count;
for (Cmd* cmd; (cmd = queue.dequeue() ?? null) != null;) {
if (last_command == null || last_command.type != cmd.type) {
self.end_command(last_command, off, count);
self.begin_command(cmd);
off += count;
count = 0;
}
switch (cmd.type) {
case CMD_RECT: nextcase;
case CMD_SPRITE:
count++;
case CMD_UPDATE_ATLAS:
// TODO: verify the correct type
CmdUpdateAtlas u = cmd.update_atlas;
@ -1007,20 +903,60 @@ fn void Renderer.render_ugui(&self, CmdQueue* queue)
self.scissor_y = s.y;
self.scissor_w = s.w;
self.scissor_h = s.h;
default:
self.set_scissor(self.scissor_x, self.scissor_y, self.scissor_w, self.scissor_h);
uint count = 1;
while (queue.len() != 0 && (queue.get(0).type == CMD_RECT || queue.get(0).type == CMD_SPRITE)) {
count++;
(void)queue.dequeue();
}
self.draw_quads(off, count);
off += count;
calls++;
// self.draw_quads(off++, 1);
default: unreachable("unknown command: %s", cmd.type);
}
last_command = cmd;
}
self.end_render_pass();
// ugui::println("calls: ", calls);
self.end_command(last_command, off, count);
}
fn void Renderer.begin_command(&self, Cmd* cmd)
{
if (cmd == null) return;
switch (cmd.type) {
case CMD_RECT:
self.start_render_pass("UGUI_PIPELINE_RECT");
self.set_scissor(self.scissor_x, self.scissor_y, self.scissor_w, self.scissor_h);
case CMD_SPRITE:
// TODO: support multiple sprite and font atlases
CmdSprite s = cmd.sprite;
String pipeline;
String texture;
if (s.texture_id == self.sprite_atlas_id) {
switch (s.type) {
case SPRITE_NORMAL: pipeline = "UGUI_PIPELINE_SPRITE";
case SPRITE_SDF: pipeline = "UGUI_PIPELINE_SPRITE_SDF";
case SPRITE_MSDF: pipeline = "UGUI_PIPELINE_SPRITE_MSDF";
case SPRITE_ANIMATED: unreachable("animated sprtes are unsupported for now");
default: unreachable("unknown sprite type %s", s.type);
}
texture = "icons";
} else if (s.texture_id == self.font_atlas_id) {
pipeline = "UGUI_PIPELINE_FONT";
texture = "font1";
}
self.start_render_pass(pipeline);
self.bind_texture(texture);
self.set_scissor(self.scissor_x, self.scissor_y, self.scissor_w, self.scissor_h);
case CMD_UPDATE_ATLAS: break;
case CMD_SCISSOR: break;
default: unreachable("unknown command: %s", cmd.type);
}
}
fn void Renderer.end_command(&self, Cmd* cmd, uint off, uint count)
{
if (cmd == null) return;
switch (cmd.type) {
case CMD_RECT: nextcase;
case CMD_SPRITE:
self.draw_quads(off, count);
self.end_render_pass();
case CMD_UPDATE_ATLAS: break;
case CMD_SCISSOR: break;
default: unreachable("unknown command: %s", cmd.type);
}
}

View File

@ -1,342 +0,0 @@
module mtree{Type};
import std::core::mem;
import std::core::mem::allocator;
import std::io;
import std::bits;
import std::collections::list;
alias Bitmap = ulong;
const BITS = Bitmap.sizeof*8;
alias IdxList = list::List{int};
// more: if positive it contains the index of the next node that contains the children information
struct Node {
int more;
int parent;
Bitmap children;
}
struct MTree {
usz elements;
Allocator allocator;
IdxList queue;
Bitmap[] used;
Type[] elem_mat; // element matrix
Node[] refs_mat; // relationship matrix
}
fn void MTree.init(&tree, usz size, Allocator allocator = mem)
{
// round size to the nearest multiple of BITS
size = size + size%BITS;
tree.elements = 0;
tree.allocator = allocator;
tree.queue.init(allocator, size);
tree.used = allocator::new_array(tree.allocator, Bitmap, size/BITS);
tree.elem_mat = allocator::new_array(tree.allocator, Type, size);
tree.refs_mat = allocator::new_array(tree.allocator, Node, size);
foreach (&r: tree.refs_mat) {
r.more = -1;
}
}
fn void MTree.free(&tree)
{
tree.elements = 0;
tree.queue.free();
(void)allocator::free(tree.allocator, tree.used);
(void)allocator::free(tree.allocator, tree.elem_mat);
(void)allocator::free(tree.allocator, tree.refs_mat);
}
fn int MTree.get_free_spot(&tree)
{
foreach (idx, d: tree.used) {
if (d != $typeof(d).max) {
int spot = (int)idx*BITS + BITS-(int)d.clz();
return spot;
}
}
unreachable("no free spots left");
}
<* @require idx >= 0 *>
fn void MTree.set_used(&tree, int idx)
{
int r = idx % BITS;
int q = idx / BITS;
tree.used[q] |= (1l << r);
}
<* @require idx >= 0 *>
fn void MTree.unset_used(&tree, int idx)
{
int r = idx % BITS;
int q = idx / BITS;
tree.used[q] &= ~(1l << r);
}
<* @require idx >= 0 *>
fn bool MTree.is_used(&tree, int idx)
{
int r = idx % BITS;
int q = idx / BITS;
return !!(tree.used[q] & (1l << r));
}
// get the last node in the "more" chain
<* @require tree.is_used(parent) == true *>
fn int MTree.last_node(&tree, int parent)
{
while(tree.refs_mat[parent].more >= 0) {
parent = tree.refs_mat[parent].more;
}
return parent;
}
<* @require tree.elements == 0 || tree.is_used(parent) == true *>
fn int MTree.add(&tree, int parent, Type t)
{
int idx = tree.get_free_spot();
int subtree = idx / BITS;
tree.set_used(idx);
tree.elem_mat[idx] = t;
tree.refs_mat[idx] = (Node){
.parent = parent,
.more = -1,
};
tree.elements++;
// root element, has no parent
if (tree.elements == 1) {
tree.refs_mat[idx].parent = -1;
return idx;
}
// if the parent already has a node in the same subtree as the child then update that node's
// children bitmap
bool done;
for (int p = parent; p >= 0; p = tree.refs_mat[p].more) {
int ps = p/BITS;
if (ps == subtree) {
tree.refs_mat[p].children |= (1l << (idx%BITS));
done = true;
break;
}
}
// on fail we need to create another parent node
if (!done) {
int new_more = tree.get_free_spot();
// if the new node does not land in the same subtree as the child we cannot do
// anything since the references are immutable
if (new_more/BITS != subtree) {
unreachable("cannot allocate new child for parent");
}
tree.set_used(new_more);
tree.elements++;
// update the "more" chain
int last_link = tree.last_node(parent);
tree.refs_mat[last_link].more = new_more;
tree.refs_mat[new_more].more = -1;
tree.refs_mat[new_more].children |= (long)(1 << (idx%BITS));
tree.refs_mat[new_more].parent = last_link;
// FIXME: the elem_mat is not updated, do we need to?
}
return idx;
}
// get the index of the n-th children of parent, -1 otherwise
// usage: for (int i, c; (c = tree.children_it(parent, i)) >= 0; i++) { ... }
fn int MTree.children_it(&tree, int parent, int n)
{
int tot_children;
int child;
for (int p = parent; p >= 0; p = tree.refs_mat[p].more) {
int cn = (int)tree.refs_mat[p].children.popcount();
tot_children += cn;
// we are in the right subtree
if (tot_children > n) {
child = (p/BITS) * BITS; // start at the parent's subtree index
int j = cn - (tot_children - n); // we need the j-th children of this node
Bitmap u = tree.refs_mat[p].children;
child += j; // add the children number
do {
child += (int)u.ctz(); // increment by the skipped zeroes
u >>= u.ctz() + 1;
j--;
} while (j >= 0);
return child;
}
}
return -1;
}
fn int MTree.children_num(&tree, int parent)
{
int n;
for (int p = parent; p >= 0; p = tree.refs_mat[p].more) {
n += (int)tree.refs_mat[p].children.popcount();
}
return n;
}
fn int MTree.subtree_size(&tree, int parent)
{
int x = tree.children_num(parent);
int c;
for (int n; (c = tree.children_it(parent, n)) >= 0; n++) {
x += tree.subtree_size(c);
}
return x;
}
fn int MTree.level_order_it(&tree, int parent, int i)
{
if (i == 0) {
tree.queue.clear();
tree.queue.push(parent);
}
if (tree.queue.len() == 0) return -1;
int p = tree.queue.pop_first()!!;
int c;
for (int n; (c = tree.children_it(p, n)) >= 0; n++) {
tree.queue.push(c);
}
return p;
}
fn void MTree.prune(&tree, int parent)
{
int c;
for (int i = 0; (c = tree.children_it(parent, i)) >= 0; i++) {
tree.prune(c); // prune the subtree
// delete all children including their more chain
for (int p = c; p >= 0;) {
int next = tree.refs_mat[p].more;
tree.unset_used(p);
tree.refs_mat[p] = {.more = -1};
p = next;
}
}
// finally delete the parent
for (int p = parent; p >= 0;) {
int next = tree.refs_mat[p].more;
tree.unset_used(p);
tree.elements--;
tree.refs_mat[p] = {.more = -1};
p = next;
}
}
macro bool MTree.is_root(&t, int i) => t.refs_mat[i].parent == -1;
fn void MTree.print(&tree)
{
foreach (idx, c: tree.elem_mat) {
if (tree.is_used((int)idx)) {
io::printfn("[%d](%s) parent:%d more:%d children:%b",
idx, c, tree.refs_mat[idx].parent, tree.refs_mat[idx].more,
tree.refs_mat[idx].children
);
}
}
}
module foo;
import std::io;
import mtree;
alias Tree = mtree::MTree{int};
fn int main()
{
Tree t;
t.init(256);
defer t.free();
/*
int root = t.add(0, 0);
int c1 = t.add(root, 1);
int c2 = t.add(root, 2);
int c11 = t.add(c1, 11);
int c12 = t.add(c1, 12);
int c3 = t.add(root, 3);
for (int x = 0; x < 70; x++) {
t.add(c2, x);
}
int c31 = t.add(c3, 31);
int c32 = t.add(c3, 32);
int c4 = t.add(root, 4);
int c13 = t.add(c1, 13);
int c14 = t.add(c1, 14);
int c15 = t.add(c1, 15);
t.prune(c2);
io::printn("printing tree");
t.print();
usz x;
foreach_r (u: t.used) {
x += u.popcount();
io::printf("%b ", u);
}
io::printfn("TOT:%d/%d",x,t.elements);
io::printn(t.subtree_size(root));
io::printn();
*/
int root = t.add(0, 0);
int c1 = t.add(root, 1);
int c2 = t.add(root, 2);
int c3 = t.add(root, 3);
int c11 = t.add(c1, 11);
int c12 = t.add(c1, 12);
int c111 = t.add(c11, 111);
int c121 = t.add(c12, 121);
int c31 = t.add(c3, 31);
int c;
for (int i; (c = t.level_order_it(root, i)) >= 0; i++) {
io::printfn("%d-th: [%d](%d)", i, c, t.elem_mat[c]);
}
return 0;
}

View File

@ -1,436 +0,0 @@
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++;
}
}