Compare commits

..

4 Commits

47 changed files with 2299 additions and 4776 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

View File

@ -1,43 +0,0 @@
# UGUI
Immediate mode ui library for C3. (Name subject to change).
This repo contains the library itself, a renderer based on SLD3's new GPU API and a test program.
The library is located at `lib/ugui.c3l`.
The layout algorithm is similar to flexbox. A graphical description of the layout algorithm is located in the [LAYOUT](lib/ugui.c3l/LAYOUT) file. The structure of the library is (was) described in the [ARCHITECTURE](ARCHITECTURE.md) file, most of it is now irrelevant and will be updated in the future.
## Dependencies
* `shaderc` for `glslc`
* `c3c` version >0.7.5
* `SDL3`
* A C compiler
## Building
1. Clone the repo and all dependencies
```sh
git clone https://github.com/ma-ale/ugui
cd ugui
git submodule update --recursive --init
```
2. Build libgrapheme
```sh
cd lib/grapheme.c3l
make
cd ../..
```
3. Compile libschrift
```sh
cd lib/schrift.c3l
make
cd ../..
```
4. Build and run the project
```sh
make && build/ugui
```

68
TODO
View File

@ -4,22 +4,21 @@
[x] Implement div.view and scrollbars
[x] Port font system from C to C3 (rewrite1)
[ ] Update ARCHITECTURE.md
[x] Write a README.md
[x] Use an arena allocator for cache
[ ] Write a README.md
[ ] 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 to maintain focus until mouse release (fix scroll bars)
[x] Better handling of the active and focused widgets, try
to maintain focus until mouse release (fix scroll bars)
[x] Clip element bounds to parent div, specifically text
[ ] Resizeable divs
[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
@ -29,48 +28,31 @@
[ ] .png
[ ] .jpg
[ ] gif support?
[x] layout_set_max_rows() and layout_set_max_columns()
[ ] layout_set_max_rows() and layout_set_max_columns()
[x] Maybe SDF sprites??
[x] Stylesheets and stylesheet import
[x] use SDF to draw anti-aliased rounded rectangles https://zed.dev/blog/videogame
[ ] Subdivide modules into ugui::ug for exported functions and ugui::core for
internal use functions (used to create widgets)
[x] The render loop RAPES the gpu, valve pls fix
[x] The way the element structures are implemented wastes a lot of memory since
[ ] The way the element structures are implemented wastes a lot of memory since
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
[x] 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
@ -78,9 +60,7 @@
[x] Mouse scroll wheel
[ ] Touch input
[x] Do not set input event to true if the movement was zero (like no mouse movement)
[x] Use input event flags, for example to consume the input event
[x] 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
[ ] Use input event flags, for example to consume the input event
## Commands
@ -88,10 +68,9 @@
- border width
- border radius
[x] add a command to update an atlas
[x] New window command, useful for popups
[x] Text command returns the text bounds, this way we can avoid the pattern
[ ] New window command, useful for popups
[ ] 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
@ -114,6 +93,11 @@
[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

@ -1,217 +0,0 @@
module ugui;
import std::io;
import std::collections::list;
// command type
enum CmdType {
CMD_RECT,
CMD_UPDATE_ATLAS,
CMD_SPRITE,
CMD_SCISSOR,
CMD_REQ_SKIP_FRAME,
}
// command to draw a rect
struct CmdRect {
Rect rect;
ushort radius;
Color color;
}
struct CmdUpdateAtlas {
Id id;
char* raw_buffer;
short width, height, bpp;
}
struct CmdSprite {
Id texture_id;
SpriteType type;
Rect rect;
Rect texture_rect;
Color hue;
}
// if rect is zero Rect{0} then reset the scissor
struct CmdScissor {
Rect rect;
}
// command structure
struct Cmd (Printable) {
CmdType type;
int z_index;
union {
CmdRect rect;
CmdUpdateAtlas update_atlas;
CmdSprite sprite;
CmdScissor scissor;
}
}
// command queue
alias CmdQueue = list::List{Cmd};
fn int Cmd.compare_to(Cmd a, Cmd b)
{
if (a.z_index == b.z_index) return 0;
return a.z_index > b.z_index ? 1 : -1;
}
// FIXME: This sorting method does not fully respect layer ordering for popups.
// Popups that start on the same layer but are enqueued at different times
// may still be rendered on top of each other, even if the first popup
// has elements in higher layers than the second. The current approach
// does not truly sort by layer; it only moves elements from higher layers
// to the end of the queue as they are encountered.
fn void? CmdQueue.sort(&queue)
{
CmdQueue stack;
stack.init(allocator::tmem);
for (isz i = queue.len()-1; i > 0; i--) {
Cmd cur = (*queue)[i];
Cmd next = (*queue)[i - 1];
// cur < next
if (cur.compare_to(next) < 0) {
stack.push(next);
queue.remove_at(i-1);
}
}
usz l = stack.len();
for (usz i; i < l; i++) {
queue.push(stack.pop())!;
}
}
// 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)!;
case CMD_REQ_SKIP_FRAME:
ret += f.print("Skip Frame Request")!;
}
ret += f.print("}")!;
return ret;
}
macro bool cull_rect(Rect rect, Rect clip = {0,0,short.max,short.max})
{
bool no_area = rect.w <= 0 || rect.h <= 0;
return no_area || !rect.collides(clip);
}
// FIXME: this whole thing could be done at compile time, maybe
macro Ctx.push_cmd(&ctx, Cmd cmd, int z_index)
{
cmd.z_index = z_index;
Rect rect;
switch (cmd.type) {
case CMD_RECT: rect = cmd.rect.rect;
case CMD_SPRITE: rect = cmd.sprite.rect;
default: return ctx.cmd_queue.push(cmd);
}
if (cull_rect(rect, ctx.div_scissor)) {
// println("NOPE: ", cmd.rect.rect, cmd.z_index);
// unreachable();
return;
}
return ctx.cmd_queue.push(cmd);
}
fn void? Ctx.push_scissor(&ctx, Rect rect, int z_index)
{
Cmd sc = {
.type = CMD_SCISSOR,
.scissor.rect = rect.intersection(ctx.div_scissor),
};
ctx.push_cmd(sc, z_index);
}
fn void? Ctx.reset_scissor(&ctx, int z_index) => ctx.push_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;
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()) {
Cmd cmd = {
.type = CMD_RECT,
.rect.rect = rect,
.rect.color = border_color,
.rect.radius = radius + border.x,
};
ctx.push_cmd(cmd, z_index);
}
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),
},
.rect.color = bg,
.rect.radius = radius,
};
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 = {
.type = CMD_SPRITE,
.sprite.type = type,
.sprite.rect = bounds,
.sprite.texture_rect = texture,
.sprite.texture_id = texture_id,
.sprite.hue = hue,
};
ctx.push_cmd(cmd, z_index);
}
fn void? Ctx.push_update_atlas(&ctx, Atlas* atlas)
{
Cmd up = {
.type = CMD_UPDATE_ATLAS,
.update_atlas = {
.id = atlas.id,
.raw_buffer = atlas.buffer,
.width = atlas.width,
.height = atlas.height,
.bpp = (ushort)atlas.type.bpp(),
},
};
// update the atlases before everything else
ctx.push_cmd(up, -1);
}
macro Ctx.dbg_rect(&ctx, Rect r, uint c = 0xff000042u) => ctx.push_rect(r, int.max, &&(Style){.bg=c.to_rgba()})!!;

91
lib/ugui.c3l/src/fifo.c3 Normal file
View File

@ -0,0 +1,91 @@
module fifo::faults;
faultdef FULL, EMPTY;
module fifo{Type};
import std::core::mem;
import std::sort;
// TODO: specify the allocator
struct Fifo {
Type[] arr;
usz out;
usz count;
}
fn void? Fifo.init(&fifo, usz size)
{
fifo.arr = mem::new_array(Type, size);
fifo.out = 0;
fifo.count = 0;
}
fn void Fifo.free(&fifo)
{
(void)mem::free(fifo.arr);
}
fn void? Fifo.enqueue(&fifo, Type *elem)
{
if (fifo.count >= fifo.arr.len) {
return fifo::faults::FULL?;
}
usz in = (fifo.out + fifo.count) % fifo.arr.len;
fifo.arr[in] = *elem;
fifo.count++;
}
fn Type*? Fifo.dequeue(&fifo)
{
if (fifo.count == 0) {
return fifo::faults::EMPTY?;
}
Type *ret = &fifo.arr[fifo.out];
fifo.count--;
fifo.out = (fifo.out + 1) % fifo.arr.len;
return ret;
}
macro Type Fifo.get(&fifo, usz i) @operator([])
{
return fifo.arr[(fifo.out + i) % fifo.arr.len];
}
fn void Fifo.set(&fifo, usz i, Type val) @operator([]=)
{
fifo.arr[(fifo.out + i) % fifo.arr.len] = val;
}
macro Type* Fifo.get_ref(&fifo, usz i) @operator(&[])
{
return &fifo.arr[(fifo.out + i) % fifo.arr.len];
}
macro usz Fifo.len(&fifo) @operator(len)
{
return fifo.count;
}
fn void? Fifo.sort(&fifo)
{
Type[] arr = mem::new_array(Type, fifo.count);
defer mem::free(arr);
foreach(i, c: fifo) {
arr[i] = c;
}
// doesn't keep ordering
//sort::quicksort(arr);
// seems to keep the right order but we will never know...
// also since most things are already ordered the time is closer to O(n) than to O(n^2)
sort::insertionsort(arr);
fifo.count = 0;
fifo.out = 0;
foreach (&c: arr) {
fifo.enqueue(c)!;
}
}

View File

@ -1,344 +0,0 @@
module ugui;
import std::math;
import std::io;
enum LayoutDirection {
ROW,
COLUMN,
}
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;
Point origin;
Point scroll_offset;
// 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)
{
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();
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,
};
}
// 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;
}
// The content space of the element
macro Point Elem.content_space(&e)
{
return {
.x = e.bounds.w - e.layout.content_offset.x - e.layout.content_offset.w,
.y = e.bounds.h - e.layout.content_offset.y - e.layout.content_offset.h,
};
}
// Update the parent element's children size
fn void update_parent_size(Elem* child, Elem* parent)
{
Layout* cl = &child.layout;
Layout* pl = &parent.layout;
// 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++;
}
}
fn void update_children_bounds(Elem* child, Elem* parent)
{
if (child.layout.absolute) return;
parent.children_bounds = containing_rect(child.bounds + parent.layout.scroll_offset, parent.bounds);
}
macro Rect Elem.content_bounds(&elem) => elem.bounds.pad(elem.layout.content_offset);
// 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)
{
Layout* el = &e.layout;
Layout* pl = &p.layout;
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;
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 (e.layout.absolute) { // absolute children do not need to share space
e.bounds.w = p.content_space().x;
} else if (p.layout.dir == ROW) { // grow along the axis, divide the parent size
short slot = (short)((p.content_space().x - p.layout.occupied) / p.layout.grow_children);
e.bounds.w = slot;
p.layout.grow_children--;
p.layout.occupied += slot;
} else if (p.layout.dir == COLUMN) { // grow across the layout axis, inherit width of the parent
e.bounds.w = p.content_space().x;
}
}
// HEIGHT
if (e.layout.h.@is_grow()) {
if (e.layout.absolute) { // absolute children do not need to share space
e.bounds.h = p.content_space().y;
} else if (p.layout.dir == COLUMN) { // grow along the axis, divide the parent size
short slot = (short)((p.content_space().y - p.layout.occupied) / p.layout.grow_children);
e.bounds.h = slot;
p.layout.grow_children--;
p.layout.occupied += slot;
} else if (p.layout.dir == ROW) { // grow across the layout axis, inherit width of the parent
e.bounds.h = p.content_space().y;
}
}
}
fn void resolve_placement(Elem* c, Elem* p)
{
Layout* pl = &p.layout;
Layout* cl = &c.layout;
Point off = {
.x = p.bounds.x + pl.origin.x + pl.content_offset.x,
.y = p.bounds.y + pl.origin.y + pl.content_offset.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;
}
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))!;
p.layout.origin = -p.layout.scroll_offset;
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);
}
// FIXME: this stuff would be better elsewhere but we are already iteraring through all
// elements so here it fits really well
ctx.update_hover_and_focus(c);
}
}
}

View File

@ -1,367 +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
}
<* @param [&inout] tree *>
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;
}
}
<* @param [&inout] tree *>
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);
}
<* @param [&inout] tree *>
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;
}
}
return CAPACITY_EXCEEDED?;
}
<* @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
@param [&inout] tree
*>
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) {
return CAPACITY_EXCEEDED?;
}
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++) { ... }
<* @param [&in] tree *>
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;
}
<* @param [&in] tree *>
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;
}
<* @param [&in] tree *>
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;
}
<* @param [&inout] tree *>
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;
}
<* @param [&inout] tree *>
fn void MTree.prune(&tree, int parent)
{
if (!tree.is_used(parent)) return;
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 ref >= 0 , ref < tree.elem_vec.len
@param [&inout] tree
*>
fn Type? MTree.get(&tree, int ref) @operator([])
{
if (tree.is_used(ref)) return tree.elem_vec[ref];
return NOT_FOUND?;
}
<* @param [&in] tree *>
fn Type? MTree.parentof(&tree, int ref)
{
if (!tree.is_used(ref)) return NOT_FOUND?;
return tree.refs_vec[ref].parent;
}
<* @param [&inout] tree *>
fn void MTree.nuke(&tree)
{
foreach (idx, &b: tree.used) {
*b = 0;
tree.refs_vec[idx] = {.next = -1};
}
tree.elements = 0;
}
<* @param [&in] t *>
macro bool MTree.is_root(&t, int i) => t.is_used(i) && t.refs_vec[i].parent == -1;
<* @param [&in] tree *>
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

@ -1,584 +0,0 @@
module ugui;
import std::collections::list;
import std::core::string;
struct LineInfo @local {
usz start, end;
short width, height;
short first_off; // first character offset
}
macro usz LineInfo.len(li) => li.end-li.start;
alias LineStack @local = list::List{LineInfo};
fn short Rect.y_off(Rect bounds, short height, Anchor anchor) @local
{
short off;
switch (anchor) {
case TOP_LEFT: nextcase;
case TOP: nextcase;
case TOP_RIGHT:
off = 0;
case LEFT: nextcase;
case CENTER: nextcase;
case RIGHT:
off = (short)(bounds.h - height)/2;
case BOTTOM_LEFT: nextcase;
case BOTTOM: nextcase;
case BOTTOM_RIGHT:
off = (short)(bounds.h - height);
}
return off;
}
fn short Rect.x_off(Rect bounds, short width, Anchor anchor) @local
{
short off;
switch (anchor) {
case TOP_LEFT: nextcase;
case LEFT: nextcase;
case BOTTOM_LEFT:
off = 0;
case TOP: nextcase;
case CENTER: nextcase;
case BOTTOM:
off = (short)(bounds.w - width)/2;
case TOP_RIGHT: nextcase;
case RIGHT: nextcase;
case BOTTOM_RIGHT:
off = (short)(bounds.w - width);
}
return off;
}
// ---------------------------------------------------------------------------------- //
// STRING LAYOUT //
// ---------------------------------------------------------------------------------- //
struct GlyphIterator {
short baseline;
short line_height;
short line_gap;
short space_width;
short tab_width;
Rect bounds;
Anchor anchor;
bool reflow;
Font* font;
LineStack lines;
usz line_off, line_idx;
String text;
Codepoint cp;
Glyph* gp;
Point o;
Rect str_bounds;
}
<*
@param [&inout] self
@param [in] text
@param [&inout] font
*>
fn void? GlyphIterator.init(&self, Allocator allocator, String text, Rect bounds, Font* font, Anchor anchor, bool reflow, uint tab_size)
{
self.font = font;
self.line_height = (short)font.line_height();
self.baseline = (short)font.ascender;
self.line_gap = (short)font.linegap;
self.space_width = font.get_glyph(' ').adv!;
self.tab_width = self.space_width * (short)tab_size;
self.bounds = bounds;
self.o = bounds.position();
self.reflow = reflow;
self.text = text;
self.anchor = anchor;
// if the anchor is top_left we can skip dividing the string line by line, in GlyphIterator.next
// this has to be accounted for
if (anchor != TOP_LEFT) {
self.lines.init(allocator, 4);
self.populate_lines_stack()!;
self.line_off = 0;
self.line_idx = 0;
if (self.lines.len() > 0) {
self.o.y += bounds.y_off(self.str_bounds.h, anchor);
self.o.x += bounds.x_off(self.lines[0].width, anchor) - self.lines[0].first_off;
}
}
}
fn void? GlyphIterator.populate_lines_stack(&self)
{
usz line_start;
LineInfo li;
Point o = self.o;
StringIterator ti = self.text.iterator();
usz prev_off;
for (Codepoint cp; ti.has_next();) {
cp = ti.next()!;
usz off = ti.current;
bool push = false;
li.height = self.line_height;
switch {
case cp == '\n':
push = true;
case cp == '\t':
o.x += self.tab_width;
case ascii::is_cntrl((char)cp):
break;
default:
Glyph* gp = self.font.get_glyph(cp)!;
if (off == line_start) {
li.first_off = gp.ox;
o.x -= gp.ox;
}
Rect b = gp.bounds().off(o);
b.y += self.baseline;
if (self.reflow && b.x + b.w > self.bounds.x + self.bounds.w) {
push = true;
// roll back this character since it is on the next line
ti.current = prev_off;
off = prev_off;
} else {
o.x += gp.adv;
li.width += gp.adv;
}
}
if (push) {
li.start = line_start;
li.end = off;
self.lines.push(li);
self.str_bounds.w = max(self.str_bounds.w, li.width);
self.str_bounds.h += li.height;
o.x = self.bounds.x;
o.y += self.line_height;
line_start = off;
li.height = 0;
li.width = 0;
}
prev_off = off;
}
if (line_start != ti.current) {
// FIXME: crap, can we not repeat this code?
li.start = line_start;
li.end = ti.current;
self.lines.push(li);
self.str_bounds.w = max(self.str_bounds.w, li.width);
self.str_bounds.h += li.height;
}
self.str_bounds.h += (short)(self.lines.len()-1) * self.line_gap;
}
fn String GlyphIterator.current_line(&self)
{
LineInfo li = self.lines[self.line_idx];
return self.text[li.start:li.len()];
}
fn Rect? GlyphIterator.next(&self)
{
// check if there is a next glyph and maybe update the line and offset indices
if (self.anchor != TOP_LEFT) {
if (self.line_idx >= self.lines.len()) {
return NO_MORE_ELEMENT?;
}
LineInfo li = self.lines[self.line_idx];
if (self.line_off >= li.len()) {
self.line_idx++;
if (self.line_idx >= self.lines.len()) {
return NO_MORE_ELEMENT?;
}
self.line_off = 0;
li = self.lines[self.line_idx];
self.o.y += self.line_height + self.line_gap;
self.o.x = self.bounds.x + self.bounds.x_off(li.width, self.anchor) - li.first_off;
}
} else if (self.line_off >= self.text.len) {
return NO_MORE_ELEMENT?;
}
String t;
if (self.anchor != TOP_LEFT) {
t = self.current_line()[self.line_off..];
} else {
t = self.text[self.line_off..];
}
usz read = t.len < 4 ? t.len : 4;
self.cp = conv::utf8_to_char32(&t[0], &read)!;
self.line_off += read;
self.gp = self.font.get_glyph(self.cp)!;
Rect b = {.x = self.o.x, .y = self.o.y};
switch {
case self.cp == '\n':
if (self.anchor == TOP_LEFT) {
self.o.x = self.bounds.x;
self.o.y += self.line_height + self.line_gap;
self.line_idx++;
}
break;
case self.cp == '\t':
self.o.x += self.tab_width;
case ascii::is_cntrl((char)self.cp):
break;
default:
b = self.gp.bounds().off(self.o);
b.y += self.baseline;
if (self.anchor == TOP_LEFT) {
//if (self.o.x == self.bounds.x) self.bounds.x -= self.gp.ox;
if (self.reflow && b.bottom_right().x > self.bounds.bottom_right().x) {
self.o.x = self.bounds.x - self.gp.ox;
self.o.y += self.line_height + self.line_gap;
self.line_idx++;
b = self.gp.bounds().off(self.o);
b.y += self.baseline;
}
}
self.o.x += self.gp.adv;
}
return b;
}
fn bool GlyphIterator.has_next(&self)
{
if (self.anchor == TOP_LEFT) {
return self.line_off < self.text.len;
}
if (self.line_idx >= self.lines.len()) {
return false;
}
LineInfo li = self.lines[self.line_idx];
if (self.line_idx == self.lines.len() - 1 && self.line_off >= li.len()) {
return false;
}
return true;
}
fn usz GlyphIterator.current_offset(&self)
{
if (self.anchor == TOP_LEFT) return self.line_off;
return self.lines[self.line_idx].start + self.line_off;
}
// layout a string inside a bounding box, following the given alignment (anchor).
<*
@param [&in] ctx
@param [in] text
*>
fn void? Ctx.layout_string(&ctx, String text, Rect bounds, Anchor anchor, int z_index, Color hue, bool reflow = false)
{
if (text == "") return;
if (bounds.w <= 0 || bounds.h <= 0) return;
ctx.push_scissor(bounds, z_index)!;
Font* font = &ctx.font;
Id texture_id = font.id;
GlyphIterator gi;
gi.init(tmem, text, bounds, font, anchor, reflow, TAB_SIZE)!;
while (gi.has_next()) {
Rect b = gi.next()!;
Rect uv = gi.gp.uv();
ctx.push_sprite(b, uv, texture_id, z_index, hue)!;
}
ctx.reset_scissor(z_index)!;
// ctx.dbg_rect(str_bounds.off(bounds.position()));
}
// ---------------------------------------------------------------------------------- //
// CURSOR AND MOUSE //
// ---------------------------------------------------------------------------------- //
fn Rect? Ctx.get_cursor_position(&ctx, String text, Rect bounds, Anchor anchor, usz cursor, bool reflow = false)
{
if (bounds.w <= 0 || bounds.h <= 0) return {};
Font* font = &ctx.font;
Id texture_id = font.id;
if (text == "") text = "\f";
GlyphIterator gi;
gi.init(tmem, text, bounds, font, anchor, reflow, TAB_SIZE)!;
Rect cursor_rect;
cursor_rect.x = gi.o.x;
cursor_rect.y = gi.o.y;
cursor_rect.h = (short)font.line_height();
if (cursor == 0) return cursor_rect;
while (gi.has_next()) {
Rect b = gi.next()!;
if (gi.current_offset() == cursor) {
if (gi.cp == '\n') {
if (!gi.has_next()) {
cursor_rect.x = bounds.x + bounds.x_off(0, anchor);
cursor_rect.y = b.y + gi.line_height + gi.line_gap;
} else {
gi.next()!;
cursor_rect.x = gi.o.x - gi.gp.adv;
cursor_rect.y = gi.o.y;
}
} else {
// Use the updated origin position instead of glyph bounds
cursor_rect.x = gi.o.x;
cursor_rect.y = gi.o.y;
}
return cursor_rect;
}
}
return {};
}
<*
@param [&in] ctx
@param [in] text
*>
fn usz? Ctx.hit_test_string(&ctx, String text, Rect bounds, Anchor anchor, Point p, bool reflow = false)
{
if (text == "") return 0;
if (bounds.w <= 0 || bounds.h <= 0) return 0;
Font* font = &ctx.font;
GlyphIterator gi;
gi.init(tmem, text, bounds, font, anchor, reflow, TAB_SIZE)!;
usz prev_offset = 0;
Point prev_o = gi.o;
while (gi.has_next()) {
Point o_before = gi.o;
usz offset_before = gi.current_offset();
Rect b = gi.next()!;
switch {
case gi.cp == '\n':
// Check if point is on this line before the newline
Rect line_rect = {
.x = prev_o.x,
.y = prev_o.y,
.w = (short)(o_before.x - prev_o.x),
.h = gi.line_height
};
if (p.in_rect(line_rect)) return offset_before;
prev_o = gi.o;
break;
case gi.cp == '\t':
// Check if point is in the tab space
Rect tab_rect = {
.x = o_before.x,
.y = o_before.y,
.w = gi.tab_width,
.h = gi.line_height
};
if (p.in_rect(tab_rect)) return offset_before;
break;
case ascii::is_cntrl((char)gi.cp):
break;
default:
// Create a hit test rect for this character
Rect hit_rect = {
.x = o_before.x,
.y = o_before.y,
.w = gi.gp.adv,
.h = gi.line_height
};
if (p.in_rect(hit_rect)) {
// Check if cursor should be before or after this character
// by checking which half of the character was clicked
short mid_x = o_before.x + gi.gp.adv / 2;
if (p.x < mid_x) {
return offset_before;
} else {
return gi.current_offset();
}
}
}
prev_offset = gi.current_offset();
}
// Point is after all text
return text.len;
}
// TODO: implement a function `layout_string_with_selection` to avoid iterating over the string twice
fn void? Ctx.draw_string_selection(&ctx, String text, Rect bounds, Anchor anchor, usz start, usz end, int z_index, Color hue, bool reflow = false)
{
if (text == "") return;
if (bounds.w <= 0 || bounds.h <= 0) return;
if (start > end) @swap(start, end);
// Ensure start < end
if (start > end) {
usz temp = start;
start = end;
end = temp;
}
ctx.push_scissor(bounds, z_index)!;
Font* font = &ctx.font;
GlyphIterator gi;
gi.init(tmem, text, bounds, font, anchor, reflow, TAB_SIZE)!;
Rect sel_rect = { .h = gi.line_height }; // selection rect
isz sel_line = -1; // selection line
while (gi.has_next()) {
Rect b = gi.next()!;
usz off = gi.current_offset()-1;
isz line = gi.line_idx;
bool in_selection = start <= off && off <= end;
if (in_selection && line != sel_line) {
if (sel_line != -1) {
ctx.push_rect(sel_rect, z_index, &&{.bg = hue})!;
}
sel_rect = {.x = gi.o.x - b.w, .y = gi.o.y, .w = 0, .h = gi.line_height};
sel_line = line;
}
if (in_selection) {
sel_rect.w = gi.o.x - sel_rect.x;
if (gi.cp == '\n') sel_rect.w += gi.space_width;
}
if (off > end) break;
}
ctx.push_rect(sel_rect, z_index, &&{.bg = hue})!;
ctx.reset_scissor(z_index)!;
}
// ---------------------------------------------------------------------------------- //
// 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
<*
@param [&in] ctx
@param [in] text
*>
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;
StringIterator it = text.iterator();
for (Codepoint cp; it.has_next();) {
cp = it.next()!;
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 = gp.bounds().off(origin);
b.y += baseline;
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;
}

View File

@ -1,181 +0,0 @@
module ugui;
import ugui::textedit::te;
struct TextEdit {
char[] buffer;
usz chars;
usz cursor;
isz sel_len;
}
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();
te.insert_utf8(in);
// handle backspace and delete
if (mod.bkspc) {
te.remove_character(false);
}
if (mod.del) {
te.remove_character(true);
}
// handle arrow keys
if (mod.left) {
te.move_cursor(false, !!(mod & KMOD_SHIFT));
}
if (mod.right) {
te.move_cursor(true, !!(mod & KMOD_SHIFT));
}
// TODO: up, down
// TODO: mod.home
// TODO: mod.end
// TODO: selection with shift+arrows
println("cursor: ", te.cursor, " sel_len: ", te.sel_len);
return te.chars < te.buffer.len;
}
module ugui::textedit::te;
import std::core::string;
// returns the offset of the next codepoint in the buffer from the cursor
fn usz TextEdit.next_char_off(&te)
{
usz len = min(te.chars - te.cursor, 4);
if (len == 0) return len;
conv::utf8_to_char32(&te.buffer[te.cursor], &len)!!;
return len;
}
// returns the offset of the previous codepoint in the buffer from the cursor
fn usz TextEdit.prev_char_off(&te)
{
if (te.cursor == 0) return 0;
String b = (String)te.buffer[..te.cursor];
usz len;
foreach_r (off, c: b) {
if (c & 0xC0 == 0x80) continue;
len = b.len - off;
break;
}
// verify the utf8 character
conv::utf8_to_char32(&b[te.cursor - len], &len)!!;
return len;
}
// moves the cursor forwards or backwards by one codepoint without exiting the bounds, if select is
// true also change the selection width
fn void TextEdit.move_cursor(&te, bool forward, bool select)
{
// in selection but trying to move without selecting, snap the cursor and reset selection
if (te.sel_len != 0 && !select) {
if (te.sel_len > 0 && forward) {
// selection is in front of the cursor and trying to move right, snap the cursor to the
// end of selection
te.cursor += te.sel_len;
} else if (te.sel_len < 0 && !forward) {
// selection is behind the cursor and trying to move left, snap the cursor to the start
// of selection
te.cursor += te.sel_len;
}
te.sel_len = 0;
} else {
isz off = forward ? te.next_char_off() : -te.prev_char_off();
if (te.cursor + off < 0 || te.cursor + off > te.chars) return;
te.cursor += off;
// if trying to select increment selection width
if (select) {
te.sel_len -= off;
}
}
}
// set the cursor to the exact offset provided, the selection flags controls wether the selection has
// expanded or rese
fn void TextEdit.set_cursor(&te, usz cur, bool select)
{
if (!select) te.sel_len = 0;
if (cur == te.cursor) return;
usz prev_cur = te.cursor;
te.cursor = cur;
if (select) {
te.sel_len += prev_cur - cur;
}
}
fn void TextEdit.delete_selection(&te)
{
if (te.sel_len == 0) return;
usz start = te.sel_len > 0 ? te.cursor : te.cursor + te.sel_len;
usz end = te.sel_len > 0 ? te.cursor + te.sel_len : te.cursor;
usz len = te.chars - end;
te.buffer[start:len] = te.buffer[end:len];
te.cursor -= te.sel_len < 0 ? -te.sel_len : 0;
te.chars -= te.sel_len < 0 ? -te.sel_len : te.sel_len;
te.sel_len = 0;
}
// removes the character before or after the cursor
fn void TextEdit.remove_character(&te, bool forward)
{
// if there is a selection active then delete that selection
if (te.sel_len) {
te.delete_selection();
return;
}
if (te.chars == 0) return;
if (forward) {
usz rem = te.chars - te.cursor;
if (rem > 0) {
usz len = te.next_char_off();
te.buffer[te.cursor:rem-len] = te.buffer[te.cursor+len:rem-len];
te.chars -= len;
}
} else {
if (te.cursor > 0) {
usz len = te.prev_char_off();
te.buffer[te.cursor-len:te.chars-te.cursor] = te.buffer[te.cursor:te.chars-te.cursor];
te.chars -= len;
te.cursor -= len;
}
}
}
// insert a character at the cursor and update the cursor
fn void TextEdit.insert_character(&te, uint cp)
{
char[4] b;
usz len = conv::char32_to_utf8(cp, b[..])!!;
te.insert_utf8((String)b[:len]);
}
fn void TextEdit.insert_utf8(&te, String s)
{
if (s.len == 0) return;
if (s.len + te.chars > te.buffer.len) return;
te.delete_selection();
te.buffer[te.cursor+s.len : te.chars-te.cursor] = te.buffer[te.cursor : te.chars-te.cursor];
te.buffer[te.cursor : s.len] = s[..];
te.chars += s.len;
te.cursor += s.len;
}

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

@ -0,0 +1,209 @@
module ugui;
import std::ascii;
import std::io;
// command type
enum CmdType {
CMD_RECT,
CMD_UPDATE_ATLAS,
CMD_SPRITE,
CMD_SCISSOR,
}
// command to draw a rect
struct CmdRect {
Rect rect;
ushort thickness;
ushort radius;
Color color;
}
struct CmdUpdateAtlas {
Id id;
char* raw_buffer;
short width, height, bpp;
}
struct CmdSprite {
Id texture_id;
SpriteType type;
Rect rect;
Rect texture_rect;
Color hue;
}
// if rect is zero Rect{0} then reset the scissor
struct CmdScissor {
Rect rect;
}
// command structure
struct Cmd (Printable) {
CmdType type;
int z_index;
union {
CmdRect rect;
CmdUpdateAtlas update_atlas;
CmdSprite sprite;
CmdScissor scissor;
}
}
fn int Cmd.compare_to(Cmd a, Cmd b)
{
if (a.z_index == b.z_index) return 0;
return a.z_index > b.z_index ? 1 : -1;
}
// implement the Printable interface
fn usz? Cmd.to_format(Cmd* cmd, Formatter *f) @dynamic
{
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})
{
bool no_area = rect.w <= 0 || rect.h <= 0;
return no_area || !rect.collides(clip);
}
// FIXME: this whole thing could be done at compile time, maybe
macro Ctx.push_cmd(&ctx, Cmd *cmd, int z_index)
{
cmd.z_index = z_index;
Rect rect;
switch (cmd.type) {
case CMD_RECT: rect = cmd.rect.rect;
case CMD_SPRITE: rect = cmd.sprite.rect;
default: return ctx.cmd_queue.enqueue(cmd);
}
if (cull_rect(rect, ctx.div_scissor)) return;
return ctx.cmd_queue.enqueue(cmd);
}
fn void? Ctx.push_scissor(&ctx, Rect rect, int z_index)
{
Cmd sc = {
.type = CMD_SCISSOR,
.scissor.rect = rect.intersection(ctx.div_scissor),
};
ctx.push_cmd(&sc, z_index)!;
}
fn void? Ctx.push_rect(&ctx, Rect rect, int z_index, Style* style)
{
Rect padding = style.padding;
ushort border = style.border;
ushort radius = style.radius;
Color bg = style.bg;
Color border_color = style.secondary;
if (border != 0) {
Cmd cmd = {
.type = CMD_RECT,
.rect.rect = rect,
.rect.color = border_color,
.rect.radius = radius+border,
.rect.thickness = border,
};
ctx.push_cmd(&cmd, z_index)!;
}
Cmd cmd = {
.type = CMD_RECT,
.rect.rect = {
.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)!;
}
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 = {
.type = CMD_SPRITE,
.sprite.type = type,
.sprite.rect = bounds,
.sprite.texture_rect = texture,
.sprite.texture_id = texture_id,
.sprite.hue = hue,
};
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 = {
.type = CMD_UPDATE_ATLAS,
.update_atlas = {
.id = atlas.id,
.raw_buffer = atlas.buffer,
.width = atlas.width,
.height = atlas.height,
.bpp = (ushort)atlas.type.bpp(),
},
};
// update the atlases before everything else
ctx.push_cmd(&up, -1)!;
}

View File

@ -1,23 +1,12 @@
module ugui;
import mtree;
import vtree;
import cache;
import fifo;
import std::io;
import std::core::string;
import std::core::mem::allocator;
import std::collections::pair;
import std::sort;
macro println(...)
{
$for var $i = 0; $i < $vacount; $i++:
io::print($vaexpr[$i]);
$endfor
io::printn();
}
// element ids are just long ints
@ -34,34 +23,29 @@ enum ElemType {
bitstruct ElemFlags : uint {
bool updated : 0;
bool is_new : 1; // element is new in the cache
bool shown : 2; // element has been shown (drawn) this frame
bool is_new : 1;
}
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;
int z_index;
union {
ElemDiv div;
ElemButton button;
@ -72,43 +56,24 @@ struct Elem {
}
const uint MAX_ELEMENTS = 256;
const uint MAX_COMMANDS = 2048;
const uint STACK_STEP = 10;
const uint ROOT_ID = 1;
const uint TEXT_MAX = 64;
// Tuple of Element pointers, used when it is useful to get both parent and child
alias PElemTuple = pair::Pair{Elem*, 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;
alias ElemCache = cache::Cache{Id, Elem, MAX_ELEMENTS};
faultdef INVALID_SIZE, EVENT_UNSUPPORTED, WRONG_ELEMENT_TYPE, WRONG_ID;
alias CmdQueue = fifo::Fifo{Cmd};
struct InputData {
InputEvents events;
struct mouse {
Point pos, delta;
// mouse_down: bitmap of mouse buttons that are held
// mouse_updated: bitmap of mouse buttons that have been updated
// mouse_released = mouse_updated & ~mouse_down
// mouse_pressed = mouse_updated & mouse_down
MouseButtons down;
MouseButtons updated;
// scroll wheel
Point scroll;
}
struct keyboard {
char[TEXT_MAX] text;
usz text_len;
ModKeys modkeys;
}
}
faultdef INVALID_SIZE, EVENT_UNSUPPORTED, UNEXPECTED_ELEMENT, WRONG_ELEMENT_TYPE, WRONG_ID;
const Rect DIV_FILL = { .x = 0, .y = 0, .w = 0, .h = 0 };
const uint STACK_STEP = 10;
const uint MAX_ELEMS = 128;
const uint MAX_CMDS = 2048;
const uint ROOT_ID = 1;
const uint TEXT_MAX = 64;
struct Ctx {
IdTree tree;
@ -120,24 +85,39 @@ struct Ctx {
Font font;
SpriteAtlas sprite_atlas;
bool skip_frame;
bool has_focus;
InputData input, current_input;
struct input {
InputEvents events;
struct mouse {
Point pos, delta;
// mouse_down: bitmap of mouse buttons that are held
// mouse_updated: bitmap of mouse buttons that have been updated
// mouse_released = mouse_updated & ~mouse_down
// mouse_pressed = mouse_updated & mouse_down
MouseButtons down;
MouseButtons updated;
// scroll wheel
Point scroll;
}
struct keyboard {
char[TEXT_MAX] text;
usz text_len;
ModKeys modkeys;
}
}
Id hover_id;
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[ctx.active_div]!;
Elem* parent = ctx.cache.search(parent_id)!;
return parent;
Id parent_id = ctx.tree.get(ctx.active_div)!;
return ctx.cache.search(parent_id);
}
macro @bits(#a) => $typeof(#a).sizeof*8;
@ -148,7 +128,6 @@ 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)!;
// Mix the two IDs non-linearly
Id mixed = id1 ^ id2.rotate_left(13);
@ -169,27 +148,22 @@ macro Id @compute_id(...)
// get or push an element from the cache, return a pointer to it
// resets all flags except is_new which is set accordingly
fn PElemTuple? Ctx.get_elem(&ctx, Id id, ElemType type)
fn Elem*? Ctx.get_elem(&ctx, Id id, ElemType type)
{
Elem empty_elem;
bool is_new;
Elem* parent;
Elem* elem;
parent = ctx.get_parent() ?? &&(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.flags.shown = true;
elem.id = id;
elem.layout = {};
if (is_new == false && elem.type != type) {
return WRONG_ELEMENT_TYPE?;
} else {
elem.type = type;
}
elem.z_index = parent.z_index;
elem.tree_idx = ctx.tree.add(ctx.active_div, id)!;
return {elem, parent};
elem.tree_idx = ctx.tree.add(id, ctx.active_div)!;
return elem;
}
// find an element, does not allocate a new one in cache
@ -210,18 +184,18 @@ fn Elem*? Ctx.get_active_div(&ctx)
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(allocator::mem, MAX_COMMANDS);
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(); }
@ -243,7 +217,7 @@ fn void? Ctx.frame_begin(&ctx)
// 1. Reset the active div
// 2. Get the root element from the cache and update it
ctx.active_div = 0;
Elem* elem = ctx.get_elem(ROOT_ID, ETYPE_DIV)!.first;
Elem* elem = ctx.get_elem(ROOT_ID, ETYPE_DIV)!;
ctx.active_div = elem.tree_idx;
// The root should have the updated flag only if the size of the window
// was changed between frasmes, this propagates an element size recalculation
@ -255,52 +229,36 @@ fn void? Ctx.frame_begin(&ctx)
//elem.flags.has_focus = ctx.has_focus;
elem.bounds = {0, 0, ctx.width, ctx.height};
elem.z_index = 0;
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.skip_frame = false;
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
}
const int DEBUG = 1;
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) {
return WRONG_ID?;
}
// 2. clear input fields
ctx.input = ctx.current_input;
ctx.current_input.events = {};
ctx.current_input.keyboard.text_len = 0;
// DO THE LAYOUT
ctx.layout_element_tree()!;
foreach (idx, id : ctx.tree.elem_vec) {
if (!ctx.tree.is_used((int)idx)) continue;
Elem* c = ctx.find_elem(id);
// reset events
c.events = {};
// reset shown flag
// TODO: use shown_last_frame to avoid this loop entirely
c.flags.shown = false;
}
// Propagate input events to the right elements
ctx.set_elem_events(ctx.hover_id);
ctx.set_elem_events(ctx.focus_id);
root.div.layout = LAYOUT_ROW;
// 1. clear the tree
ctx.tree.nuke();
// 2. clear input fields
ctx.input.events = (InputEvents)0;
ctx.input.keyboard.text_len = 0;
// send atlas updates
if (ctx.font.should_update) {
ctx.push_update_atlas(&ctx.font.atlas)!;
@ -310,20 +268,9 @@ fn void? Ctx.frame_end(&ctx)
ctx.push_update_atlas(&ctx.sprite_atlas.atlas)!;
ctx.sprite_atlas.should_update = false;
}
// send skip frame request
if (ctx.skip_frame) {
ctx.cmd_queue.push({.type = CMD_REQ_SKIP_FRAME});
}
// sort the command buffer by the z-index
// FIXME: sorting the buffer fucks with scissor commands that have to be kept in place
// TODO: instead of sorting at the end perform ordered inserts into the command buffer
//sort::countingsort(ctx.cmd_queue, fn uint(Cmd c) => c.z_index+1);
ctx.cmd_queue.sort()!;
// debug
$if $feature(DEBUG_POINTER):
// debug
$if DEBUG == 1:
// draw mouse position
Cmd cmd = {
.type = CMD_RECT,
@ -336,64 +283,48 @@ $if $feature(DEBUG_POINTER):
},
.rect.color = 0xff00ffffu.to_rgba()
};
ctx.cmd_queue.push(cmd);
ctx.cmd_queue.enqueue(&cmd)!;
$endif
// foreach (i, c: ctx.cmd_queue) {
// io::printf("[%d]: ", i);
// io::printn(c);
// }
// sort the command buffer by the z-index
ctx.cmd_queue.sort()!;
}
<*
* @ensure elem != null
*>
macro bool Ctx.is_hovered(&ctx, Elem *elem)
{
return ctx.input.mouse.pos.in_rect(elem.bounds);
}
macro bool Ctx.is_hovered(&ctx, Elem *elem) => ctx.input.mouse.pos.in_rect(elem.bounds);
macro bool Ctx.elem_focus(&ctx, Elem *elem)
{
return ctx.focus_id == elem.id;
}
// Check if the element is hovered and/or focused, if it is update the context ids.
// The order in which the elements are passed to this function is not relevant
fn void Ctx.update_hover_and_focus(&ctx, Elem* elem)
// TODO: add other events
// FIXME: this does not work with touch
// FIXME: hacked together, please do better
fn ElemEvents Ctx.get_elem_events(&ctx, Elem *elem)
{
bool hover = ctx.is_hovered(elem);
bool focus = ctx.focus_id == elem.id || (hover && ctx.is_mouse_pressed(BTN_ANY));
bool focus = ctx.focus_id == elem.id || (hover && ctx.is_mouse_pressed(BTN_LEFT));
if (hover) {
Elem* prev_hover = ctx.find_elem(ctx.hover_id);
bool different = prev_hover.id != elem.id;
bool still_hovered = ctx.is_hovered(prev_hover);
bool shown = prev_hover.flags.shown;
bool above = prev_hover.z_index > elem.z_index;
hover = !(different && still_hovered && shown && above);
if (ctx.is_mouse_pressed(BTN_ANY) && !hover){
focus = false;
if (ctx.focus_id == elem.id) ctx.focus_id = 0;
}
if (focus) {
Elem* prev_focus = ctx.find_elem(ctx.hover_id);
bool different = prev_focus.id != elem.id;
bool shown = prev_focus.flags.shown;
bool above = prev_focus.z_index > elem.z_index;
focus = !(different && shown && above);
}
if (hover) ctx.hover_id = elem.id;
if (focus) ctx.focus_id = elem.id;
}
// FIXME: this does not work with touch
fn void Ctx.set_elem_events(&ctx, Id id)
{
bool hover = id == ctx.hover_id;
bool focus = id == ctx.focus_id;
Elem* e = ctx.find_elem(id);
if (hover) { ctx.hover_id = elem.id; }
if (focus) { ctx.focus_id = elem.id; }
e.events = {
.has_focus = focus,
.mouse_hover = hover,
.mouse_press = hover && focus && ctx.is_mouse_pressed(BTN_ANY),
ElemEvents ev = {
.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

@ -1,6 +1,7 @@
module ugui;
import schrift;
import grapheme;
import std::collections::map;
import std::core::mem;
import std::core::mem::allocator;
@ -8,19 +9,11 @@ import std::io;
import std::ascii;
// ---------------------------------------------------------------------------------- //
// CODEPOINT //
// ---------------------------------------------------------------------------------- //
// unicode code point, different type for a different hash
alias Codepoint = uint;
//macro uint Codepoint.hash(self) => ((uint)self).hash();
// ---------------------------------------------------------------------------------- //
// FONT ATLAS //
// ---------------------------------------------------------------------------------- //
/* width and height of a glyph contain the kering advance
* (u,v)
* +-------------*---+ -
@ -65,18 +58,9 @@ struct Font {
bool should_update; // should send update_atlas command, resets at frame_end()
}
macro Rect Glyph.bounds(&g) => {.x = g.ox, .y = g.oy, .w = g.w, .h = g.h};
macro Rect Glyph.uv(&g) => {.x = g.u, .y = g.v, .w = g.w, .h = g.h};
<*
@param [&inout] font
@param [in] name
@param [&in] path
@require height > 0, scale > 0: "height and scale must be positive non-zero"
*>
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;
@ -105,16 +89,12 @@ 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)!;
}
}
<*
@param [&inout] font
*>
fn Glyph*? Font.get_glyph(&font, Codepoint code)
{
Glyph*? gp;
@ -173,9 +153,6 @@ fn Glyph*? Font.get_glyph(&font, Codepoint code)
return font.table.get_ref(code);
}
<*
@param [&inout] font
*>
fn void Font.free(&font)
{
font.atlas.free();
@ -183,33 +160,99 @@ fn void Font.free(&font)
schrift::freefont(font.sft.font);
}
// ---------------------------------------------------------------------------------- //
// FONT LOAD AND QUERY //
// ---------------------------------------------------------------------------------- //
<*
@param [&inout] ctx
@param [in] name
@param [&in] path
@require height > 0, scale > 0: "height and scale must be positive non-zero"
*>
fn void? Ctx.load_font(&ctx, String name, ZString path, uint height, float scale = 1.0)
{
return ctx.font.load(name, path, height, scale);
}
<*
@param [&in] ctx
@param [in] label
@require off != null
*>
// TODO: check if the font is present in the context
fn Id Ctx.get_font_id(&ctx, String label) => (Id)label.hash();
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)
{
return (Id)label.hash();
}
<*
@param [&in] ctx
@param [in] name
*>
fn Atlas*? Ctx.get_font_atlas(&ctx, String name)
{
// TODO: use the font name, for now there is only one font
@ -220,5 +263,4 @@ fn Atlas*? Ctx.get_font_atlas(&ctx, String name)
return &ctx.font.atlas;
}
<* @param [&in] font *>
fn int Font.line_height(&font) => (int)(font.ascender - font.descender + (float)0.5);

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.current_input.keyboard.modkeys & mod);
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;
}
@ -86,7 +85,7 @@ fn void? Ctx.input_window_size(&ctx, short width, short height)
if (width <= 0 || height <= 0) {
return INVALID_SIZE?;
}
ctx.current_input.events.resize = ctx.width != width || ctx.height != height;
ctx.input.events.resize = ctx.width != width || ctx.height != height;
ctx.width = width;
ctx.height = height;
}
@ -96,13 +95,13 @@ fn void Ctx.input_changefocus(&ctx, bool has_focus)
{
// FIXME: raylib only has an API to query the focus status so we have to
// update the input flag only if the focus changed
ctx.current_input.events.change_focus = ctx.has_focus != has_focus;
ctx.input.events.change_focus = ctx.has_focus != has_focus;
ctx.has_focus = has_focus;
}
macro Ctx.mouse_pressed(&ctx) => ctx.current_input.mouse.updated & ctx.current_input.mouse.down;
macro Ctx.mouse_released(&ctx) => ctx.current_input.mouse.updated & ~ctx.current_input.mouse.down;
macro Ctx.mouse_down(&ctx) => ctx.current_input.mouse.down;
macro Ctx.mouse_pressed(&ctx) => ctx.input.mouse.updated & ctx.input.mouse.down;
macro Ctx.mouse_released(&ctx) => ctx.input.mouse.updated & ~ctx.input.mouse.down;
macro Ctx.mouse_down(&ctx) => ctx.input.mouse.down;
// FIXME: hthis compairson could be done with a cast using MouseButtons.inner
// property but I could not figure out how
@ -113,63 +112,48 @@ macro Ctx.is_mouse_down(&ctx, MouseButtons btn) => (ctx.mouse_down() & btn) != B
// Mouse Buttons down
fn void Ctx.input_mouse_button(&ctx, MouseButtons buttons)
{
ctx.current_input.mouse.updated = ctx.current_input.mouse.down ^ buttons;
ctx.current_input.mouse.down = buttons;
ctx.current_input.events.mouse_btn = (uint)ctx.current_input.mouse.down != 0 || (uint)ctx.current_input.mouse.updated != 0;
ctx.input.mouse.updated = ctx.input.mouse.down ^ buttons;
ctx.input.mouse.down = buttons;
ctx.input.events.mouse_btn = (uint)ctx.input.mouse.down != 0 || (uint)ctx.input.mouse.updated != 0;
}
// Mouse was moved, report absolute position
fn void Ctx.input_mouse_abs(&ctx, short x, short y)
{
ctx.current_input.mouse.pos.x = math::clamp(x, (short)0, ctx.width);
ctx.current_input.mouse.pos.y = math::clamp(y, (short)0, ctx.height);
ctx.input.mouse.pos.x = math::clamp(x, (short)0, ctx.width);
ctx.input.mouse.pos.y = math::clamp(y, (short)0, ctx.height);
short dx, dy;
dx = x - ctx.current_input.mouse.pos.x;
dy = y - ctx.current_input.mouse.pos.y;
dx = x - ctx.input.mouse.pos.x;
dy = y - ctx.input.mouse.pos.y;
ctx.current_input.mouse.delta.x = dx;
ctx.current_input.mouse.delta.y = dy;
ctx.input.mouse.delta.x = dx;
ctx.input.mouse.delta.y = dy;
ctx.current_input.events.mouse_move = dx != 0 || dy != 0;
ctx.input.events.mouse_move = dx != 0 || dy != 0;
}
// Mouse was moved, report relative motion
fn void Ctx.input_mouse_delta(&ctx, short dx, short dy)
{
ctx.current_input.mouse.delta.x = dx;
ctx.current_input.mouse.delta.y = dy;
ctx.input.mouse.delta.x = dx;
ctx.input.mouse.delta.y = dy;
short mx, my;
mx = ctx.current_input.mouse.pos.x + dx;
my = ctx.current_input.mouse.pos.y + dy;
mx = ctx.input.mouse.pos.x + dx;
my = ctx.input.mouse.pos.y + dy;
ctx.current_input.mouse.pos.x = math::clamp(mx, (short)0, ctx.width);
ctx.current_input.mouse.pos.y = math::clamp(my, (short)0, ctx.height);
ctx.input.mouse.pos.x = math::clamp(mx, (short)0, ctx.width);
ctx.input.mouse.pos.y = math::clamp(my, (short)0, ctx.height);
ctx.current_input.events.mouse_move = dx != 0 || dy != 0;
ctx.input.events.mouse_move = dx != 0 || dy != 0;
}
fn void Ctx.input_mouse_wheel(&ctx, short x, short y, float scale = 1.0)
{
ctx.current_input.mouse.scroll.x = (short)((float)-x*scale);
ctx.current_input.mouse.scroll.y = (short)((float)-y*scale);
ctx.current_input.events.mouse_scroll = x !=0 || y != 0;
}
fn void Ctx.input_key_press(&ctx)
{
ctx.current_input.events.key_press = true;
}
fn void Ctx.input_key_release(&ctx)
{
ctx.current_input.events.key_release = true;
}
fn void Ctx.input_key_repeat(&ctx)
{
ctx.current_input.events.key_repeat = true;
ctx.input.mouse.scroll.x = (short)((float)-x*scale);
ctx.input.mouse.scroll.y = (short)((float)-y*scale);
ctx.input.events.mouse_scroll = x !=0 || y != 0;
}
// append utf-8 encoded text to the context text input
@ -177,12 +161,12 @@ fn void Ctx.input_text_utf8(&ctx, char[] text)
{
if (text.len == 0) { return; }
usz remaining = ctx.current_input.keyboard.text.len - ctx.current_input.keyboard.text_len;
usz remaining = ctx.input.keyboard.text.len - ctx.input.keyboard.text_len;
usz len = text.len > remaining ? remaining : text.len;
char[] s = ctx.current_input.keyboard.text[ctx.current_input.keyboard.text_len ..];
char[] s = ctx.input.keyboard.text[ctx.input.keyboard.text_len ..];
s[..len-1] = text[..len-1];
ctx.current_input.keyboard.text_len += len;
ctx.current_input.events.text_input = true;
ctx.input.keyboard.text_len += len;
ctx.input.events.text_input = true;
}
fn void Ctx.input_text_unicode(&ctx, char[] text)
@ -190,8 +174,8 @@ fn void Ctx.input_text_unicode(&ctx, char[] text)
if (text.ptr == null || text.len == 0) { return; }
char[32] tmp;
usz remaining = ctx.current_input.keyboard.text.len - ctx.current_input.keyboard.text_len;
char[] s = ctx.current_input.keyboard.text[ctx.current_input.keyboard.text_len ..];
usz remaining = ctx.input.keyboard.text.len - ctx.input.keyboard.text_len;
char[] s = ctx.input.keyboard.text[ctx.input.keyboard.text_len ..];
usz off;
foreach (idx, cp: text) {
@ -200,9 +184,9 @@ fn void Ctx.input_text_unicode(&ctx, char[] text)
s[off..off+enc] = tmp[..enc];
off += enc;
}
ctx.current_input.keyboard.text_len += off;
ctx.input.keyboard.text_len += off;
ctx.current_input.events.text_input = true;
ctx.input.events.text_input = true;
}
fn void Ctx.input_char(&ctx, char c)
@ -211,13 +195,10 @@ 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)
{
ctx.current_input.keyboard.modkeys = modkeys;
ctx.current_input.events.mod_key = (uint)ctx.current_input.keyboard.modkeys != 0;
ctx.input.keyboard.modkeys = modkeys;
ctx.input.events.mod_key = (uint)ctx.input.keyboard.modkeys != 0;
}

View File

@ -0,0 +1,196 @@
module ugui;
enum Layout {
LAYOUT_ROW,
LAYOUT_COLUMN,
LAYOUT_FLOATING,
LAYOUT_ABSOLUTE,
}
fn void? Ctx.layout_set_row(&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_ROW;
}
fn void? Ctx.layout_set_column(&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_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;
}
fn void? Ctx.layout_next_column(&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_c = {
.x = parent.div.children_bounds.bottom_right().x,
.y = parent.bounds.y,
};
parent.div.origin_r = parent.div.origin_c;
}
macro Rect Elem.get_view(&elem)
{
Rect off;
if (elem.div.scroll_x.enabled && elem.div.scroll_x.on) {
off.x = (short)((float)(elem.div.pcb.w - elem.bounds.w) * elem.div.scroll_x.value);
off.w = -elem.div.scroll_size;
}
if (elem.div.scroll_y.enabled && elem.div.scroll_y.on) {
off.y = (short)((float)(elem.div.pcb.h - elem.bounds.h) * elem.div.scroll_y.value);
off.h = -elem.div.scroll_size;
}
return elem.bounds.add(off);
}
macro Point Elem.get_view_off(&elem)
{
return elem.get_view().sub(elem.bounds).position();
}
// 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)
{
ElemDiv* div = &parent.div;
Rect parent_bounds, parent_view;
Rect child_placement, child_occupied;
// 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 {};
}
// 2. Compute the parent's view
parent_bounds = parent.bounds;
parent_view = parent.get_view();
// 3. Compute the placement and occupied area
// 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,
};
// 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;
}
}
// 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,
@ -124,7 +106,7 @@ macro Rect Rect.min(Rect a, Rect b)
}
// Offset a rect by a point
macro Rect Rect.off(Rect r, Point p) @operator_s(+)
macro Rect Rect.off(Rect r, Point p)
{
return {
.x = r.x + p.x,
@ -154,35 +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 //
@ -198,16 +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 bool Point.outside(Point p, Rect r) => !p.in_rect(r);
macro Point Point.add(Point a, Point b)
{
return {
.x = a.x + b.x,
.y = a.y + b.y,
};
}
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.sub(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.neg(Point p) => {-p.x, -p.y};
macro bool Point.equals(Point a, Point b) @operator_s(==) => a.x == b.x && a.y == b.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 //
@ -238,31 +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 //
// ---------------------------------------------------------------------------------- //
macro short short.add_no_of(short a, short b) => (short)max(min((int)a + (int)b, short.max), short.min) @inline;
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};
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);
macro Size Size.add(a, Size b) @operator_s(+) => {.min = a.min.add_no_of(b.min), .max = a.max.add_no_of(b.max)};
macro Size Size.sub(a, Size b) @operator_s(-) => {.min = a.min.add_no_of(-b.min), .max = a.max.add_no_of(-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

@ -0,0 +1,132 @@
module ugui;
import std::io;
import std::math;
// slider element
struct ElemSlider {
Rect handle;
}
/* handle
* +----+-----+---------------------+
* | |#####| |
* +----+-----+---------------------+
*/
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, Rect size, float* value, float hpercent = 0.25)
{
id = ctx.gen_id(id)!;
Elem* parent = ctx.get_parent()!;
Elem* elem = ctx.get_elem(id, ETYPE_SLIDER)!;
Style* style = ctx.styles.get_style(@str_hash("slider"));
// 2. Layout
elem.bounds = ctx.position_element(parent, size, style);
// handle width
short hw = (short)(elem.bounds.w * hpercent);
Rect handle = {
.x = calc_slider(elem.bounds.x, elem.bounds.w-hw, *value),
.y = elem.bounds.y,
.w = hw,
.h = elem.bounds.h,
};
elem.slider.handle = handle;
Point m = ctx.input.mouse.pos;
elem.events = ctx.get_elem_events(elem);
if (ctx.elem_focus(elem) && ctx.is_mouse_down(BTN_LEFT)) {
*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;
}
// Draw the slider background and handle
Style s = *style;
Rect padding = s.padding;
s.padding = {};
ctx.push_rect(elem.bounds, parent.div.z_index, &s)!;
s.bg = s.primary;
s.padding = padding;
s.border = {};
ctx.push_rect(elem.slider.handle, parent.div.z_index, &s)!;
return elem.events;
}
/*
* +--+
* | |
* | |
* +--+
* |##| handle
* |##|
* +--+
* | |
* | |
* +--+
*/
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)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id, ETYPE_SLIDER)!;
Style* style = ctx.styles.get_style(@str_hash("slider"));
// 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
elem.bounds = ctx.position_element(parent, size, style);
// handle height
short hh = (short)(elem.bounds.h * hpercent);
Rect handle = {
.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;
Point m = ctx.input.mouse.pos;
elem.events = ctx.get_elem_events(elem);
if (ctx.elem_focus(elem) && ctx.is_mouse_down(BTN_LEFT)) {
*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;
}
// Draw the slider background and handle
Style s = *style;
Rect padding = s.padding;
s.padding = {};
ctx.push_rect(elem.bounds, parent.div.z_index, &s)!;
s.bg = s.primary;
s.padding = padding;
s.border = {};
ctx.push_rect(elem.slider.handle, parent.div.z_index, &s)!;
return elem.events;
}
macro short calc_slider(short off, short dim, float value) => (short)off + (short)(dim * value);
macro float calc_value(short off, short mouse, short dim, short slider)
=> math::clamp((float)(mouse-off-slider/2)/(float)(dim-slider), 0.0f, 1.0f);

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,22 +18,20 @@ 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(),
.bg = 0x282828ffu.@to_rgba(),
.fg = 0xfbf1c7ffu.@to_rgba(),
.primary = 0xcc241dffu.@to_rgba(),
.secondary = 0x458588ffu.@to_rgba(),
.accent = 0xfabd2fffu.@to_rgba(),
.accent = 0xfabd2fffu.@to_rgba(),
};
// style is stored in a hashmap, each style has an Id that can be generated by a string or whatever
@ -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; }
@ -211,37 +202,35 @@ fn Token Lexer.next_token(&lex)
while (ascii::is_alnum_m(lex.peep())) {
if (lex.advance() == 0) { t.type = INVALID; break; }
}
usz h_len = lex.off - hex_start;
if (h_len != 8 && h_len != 6) {
io::eprintfn("CSS lexing error at %d:%d: the only suppported color formats are #RRGGBBAA or #RRGGBB", t.line, t.col);
if (lex.off - hex_start != 8) {
io::eprintfn("CSS lexing error at %d:%d: the only suppported color format is #RRGGBBAA", t.line, t.col);
t.type = INVALID;
break;
}
char[10] hex_str = (char[])"0x";
hex_str[2:h_len] = lex.text[hex_start:h_len];
if (h_len == 6) hex_str[8..9] = "ff"[..];
hex_str[2..] = lex.text[hex_start..lex.off-1];
uint? color_hex = ((String)hex_str[..]).to_uint();
if (catch color_hex) {
t.type = INVALID;
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) {
@ -263,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]);
}
@ -287,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;
}
@ -300,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;
}
@ -323,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":
@ -331,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;
@ -366,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;
}
@ -414,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
@ -428,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;
}

338
lib/ugui.c3l/src/vtree.c3 Normal file
View File

@ -0,0 +1,338 @@
module vtree::faults;
faultdef CANNOT_SHRINK, INVALID_REFERENCE, TREE_FULL, REFERENCE_NOT_PRESENT, INVALID_ARGUMENT;
module vtree{ElemType};
import std::core::mem;
import std::io;
struct VTree {
usz elements;
ElemType[] vector; // vector of element ids
isz[] refs, ordered_refs;
}
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 elemen
macro @zero()
{
$if $assignable(0, ElemType):
return 0;
$else
return {};
$endif
}
fn void? VTree.init(&tree, usz size)
{
tree.vector = mem::new_array(ElemType, size);
defer catch { (void)mem::free(tree.vector); }
tree.refs = mem::new_array(isz, size);
defer catch { (void)mem::free(tree.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;
tree.elements = 0;
}
fn void VTree.free(&tree)
{
(void)mem::free(tree.vector);
(void)mem::free(tree.refs);
(void)mem::free(tree.ordered_refs);
}
fn void VTree.pack(&tree)
{
// TODO: add a PACKED flag to skip this
isz free_spot = -1;
for (usz i = 0; i < tree.size(); i++) {
if (tree.refs[i] == -1) {
free_spot = i;
continue;
}
// find a item that can be packed
if (free_spot >= 0 && tree.refs[i] >= 0) {
isz old_ref = i;
// move the item
tree.vector[free_spot] = tree.vector[i];
tree.refs[free_spot] = tree.refs[i];
tree.vector[i] = {};
tree.refs[i] = -1;
// and move all references
for (usz j = 0; j < tree.size(); j++) {
if (tree.refs[j] == old_ref) {
tree.refs[j] = free_spot;
}
}
// mark the free spot as used
free_spot = -1;
}
}
}
fn void? VTree.resize(&tree, usz newsize)
{
// return error when shrinking with too many elements
if (newsize < tree.elements) {
return vtree::faults::CANNOT_SHRINK?;
}
// pack the vector when shrinking to avoid data loss
if ((int)newsize < tree.size()) {
// FIXME: packing destroys all references to elements of vec
// so shrinking may cause dangling pointers
return vtree::faults::CANNOT_SHRINK?;
}
usz old_size = tree.size();
tree.vector = ((ElemType*)mem::realloc(tree.vector, newsize*ElemType.sizeof))[:newsize];
defer catch { (void)mem::free(tree.vector); }
tree.refs = ((isz*)mem::realloc(tree.refs, newsize*isz.sizeof))[:newsize];
defer catch { (void)mem::free(tree.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();
tree.refs[old_size..newsize-1] = -1;
}
}
// add an element to the tree, return it's ref
fn isz? VTree.add(&tree, ElemType elem, isz parent)
{
// invalid parent
if (!tree.ref_is_valid(parent)) {
return vtree::faults::INVALID_REFERENCE?;
}
// no space left
if (tree.elements >= tree.size()) {
return vtree::faults::TREE_FULL?;
}
// check if the parent exists
// if there are no elements in the tree the first add will set the root
if (!tree.ref_is_present(parent) && tree.elements != 0) {
return vtree::faults::REFERENCE_NOT_PRESENT?;
}
// get the first free spot
isz free_spot = -1;
for (usz i = 0; i < tree.size(); i++) {
if (tree.refs[i] == -1) {
free_spot = i;
break;
}
}
if (free_spot < 0) {
return vtree::faults::TREE_FULL?;
}
// finally add the element
tree.vector[free_spot] = elem;
tree.refs[free_spot] = parent;
tree.elements++;
return free_spot;
}
// prune the tree starting from the ref
// returns the number of pruned elements
fn usz? VTree.prune(&tree, isz ref)
{
if (!tree.ref_is_valid(ref)) {
return vtree::faults::INVALID_REFERENCE?;
}
if (!tree.ref_is_present(ref)) {
return 0;
}
tree.vector[ref] = @zero();
tree.refs[ref] = -1;
tree.elements--;
usz count = 1;
for (usz i = 0; tree.elements > 0 && i < tree.size(); i++) {
if (tree.refs[i] == ref) {
count += tree.prune(i)!;
}
}
return count;
}
fn usz VTree.nuke(&tree)
{
tree.vector[0..] = @zero();
tree.refs[0..] = -1;
usz x = tree.elements;
tree.elements = 0;
return x;
}
// find the size of the subtree starting from ref
fn usz? VTree.subtree_size(&tree, isz ref)
{
if (!tree.ref_is_valid(ref)) {
return vtree::faults::INVALID_REFERENCE?;
}
if (!tree.ref_is_present(ref)) {
return 0;
}
usz count = 1;
for (usz i = 0; i < tree.size(); i++) {
// only root has the reference to itself
if (tree.refs[i] == ref && ref != i) {
count += tree.subtree_size(i)!;
}
}
return count;
}
// iterate through the first level children, use a cursor like strtok_r
fn isz? VTree.children_it(&tree, isz parent, isz *cursor)
{
if (cursor == null) {
return vtree::faults::INVALID_ARGUMENT?;
}
// if the cursor is out of bounds then we are done for sure
if (!tree.ref_is_valid(*cursor)) {
return vtree::faults::INVALID_REFERENCE?;
}
// same for the parent, if it's invalid it can't have children
if (!tree.ref_is_valid(parent) || !tree.ref_is_present(parent)) {
return vtree::faults::INVALID_REFERENCE?;
}
// find the first child, update the cursor and return the ref
for (isz i = *cursor; i < tree.size(); i++) {
if (tree.refs[i] == parent) {
*cursor = i + 1;
return i;
}
}
// if no children are found return -1
*cursor = -1;
return -1;
}
/* iterates trough every leaf of the subtree in the following manner
* node [x], x: visit order
* [0]
* / | \
* / [2] [3]
* [1] |
* / \ [6]
* [4] [5]
*/
fn isz? VTree.level_order_it(&tree, isz ref, isz *cursor)
{
if (cursor == null) {
return vtree::faults::INVALID_ARGUMENT?;
}
isz[] queue = tree.ordered_refs;
// TODO: this could also be done when adding or removing elements
// first call, create a ref array ordered like we desire
if (*cursor == -1) {
*cursor = 0;
queue[..] = -1;
// iterate through the queue appending found children
isz pos, off;
do {
// printf ("ref=%d\n", ref);
for (isz i = 0; i < tree.size(); i++) {
if (tree.refs[i] == ref) {
queue[pos++] = i;
}
}
for (; ref == queue[off] && off < tree.size(); off++);
ref = queue[off];
} while (tree.ref_is_valid(ref));
// This line is why tree.ordered_refs has to be size+1
queue[off + 1] = -1;
}
// PRINT_ARR(queue, tree.size());
// return -1;
// on successive calls just iterate through the queue until we find an
// invalid ref, if the user set the cursor to -1 it means it has found what
// he needed, so free
if (*cursor < 0) {
return -1;
} else if (tree.ref_is_valid(*cursor)) {
return queue[(*cursor)++];
}
return -1;
}
fn isz? VTree.parentof(&tree, isz ref)
{
if (!tree.ref_is_valid(ref)) {
return vtree::faults::INVALID_REFERENCE?;
}
if (!tree.ref_is_present(ref)) {
return vtree::faults::REFERENCE_NOT_PRESENT?;
}
return tree.refs[ref];
}
fn ElemType? VTree.get(&tree, isz ref)
{
if (!tree.ref_is_valid(ref)) {
return vtree::faults::INVALID_REFERENCE?;
}
if (!tree.ref_is_present(ref)) {
return vtree::faults::REFERENCE_NOT_PRESENT?;
}
return tree.vector[ref];
}
fn void VTree.print(&tree)
{
for (isz i = 0; i < tree.size(); i++) {
if (tree.refs[i] == -1) {
continue;
}
io::printf("[%d] {parent=%d, data=", i, tree.refs[i]);
io::print(tree.vector[i]);
io::printn("}");
}
}

View File

@ -1,226 +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, elem;
ctx.get_elem(id, ETYPE_BUTTON)!.unpack(&elem, &parent);
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);
// if (ctx.input.z_index == elem.z_index) println("true ", elem.z_index);
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 = elem.events.has_focus || 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.z_index, &s)!;
if (icon != "") {
ctx.push_sprite(icon_bounds, sprite.uv(), ctx.sprite_atlas.id, parent.z_index, type: sprite.type)!;
}
if (label != "") {
ctx.layout_string(label, text_bounds, CENTER, parent.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, elem;
ctx.get_elem(id, ETYPE_BUTTON)!.unpack(&elem, &parent);
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.z_index, style.fg)!;
if (tick_sprite != "") {
ctx.push_rect(check_bounds, parent.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.z_index, type: sprite.type)!;
}
} else {
if (*active) {
s.bg = style.primary;
ctx.push_rect(check_bounds, parent.z_index, &s)!;
} else {
ctx.push_rect(check_bounds, parent.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, elem;
ctx.get_elem(id, ETYPE_BUTTON)!.unpack(&elem, &parent);
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.z_index, style.fg)!;
ctx.push_rect(toggle_bounds, parent.z_index, &s)!;
s.bg = style.primary;
s.border = {};
ctx.push_rect(toggle.pad(style.border), parent.z_index, &s)!;
}

View File

@ -1,237 +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;
}
}
macro Ctx.@center(&ctx, LayoutDirection dir = ROW, ...; @body())
{
ctx.@div(@grow(), @grow(), dir, CENTER) {
@body();
}!;
}
macro Ctx.@row(&ctx, Anchor anchor = TOP_LEFT, ...; @body())
{
ctx.@div(@fit(), @fit(), ROW, anchor: anchor) {
@body();
}!;
}
macro Ctx.@column(&ctx, Anchor anchor = TOP_LEFT, ...; @body())
{
ctx.@div(@fit(), @fit(), COLUMN, anchor: anchor) {
@body();
}!;
}
// 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* parent, elem;
ctx.get_elem(id, ETYPE_DIV)!.unpack(&elem, &parent);
ctx.active_div = elem.tree_idx;
Style* style = ctx.styles.get_style(@str_hash("div"));
elem.div.scroll_x.enabled = scroll_x;
elem.div.scroll_y.enabled = scroll_y;
// 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.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.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()!;
Style* style = ctx.styles.get_style(@str_hash("div"));
Rect bounds = elem.bounds.pad(style.margin + style.border);
// 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(hsid_raw)!;
Id vsid_real = ctx.gen_id(vsid_raw)!;
if (elem.div.scroll_y.on) {
if (ctx.input.events.mouse_scroll && ctx.hover_id == elem.id && !(ctx.get_mod() & KMOD_SHIFT)) {
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.scrollbar(vsid_raw, &elem.div.scroll_y.value, max((float)bc.y / cbc.y, (float)0.15))!;
elem.layout.scroll_offset.y = (short)(elem.div.scroll_y.value*(float)(elem.children_bounds.h-elem.bounds.h));
} else {
elem.div.scroll_y.value = 0;
}
if (elem.div.scroll_x.on) {
if (ctx.input.events.mouse_scroll && ctx.hover_id == elem.id) {
if (ctx.get_mod() & KMOD_SHIFT) { // horizontal scroll with shift
elem.div.scroll_x.value += ctx.input.mouse.scroll.y * 0.07f;
elem.div.scroll_x.value = math::clamp(elem.div.scroll_x.value, 0.0f, 1.0f);
} else {
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.scrollbar(vsid_raw, &elem.div.scroll_x.value, max((float)bc.x / cbc.x, (float)0.15), false)!;
elem.layout.scroll_offset.x = (short)(elem.div.scroll_x.value*(float)(elem.children_bounds.w-elem.bounds.w));
} else {
elem.div.scroll_x.value = 0;
}
// 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.z_index)!;
update_parent_size(elem, parent);
return elem.id;
}
macro bool? Ctx.popup_begin(&ctx, Point pos,
Size width, Size height,
LayoutDirection dir = ROW, Anchor anchor = TOP_LEFT,
bool scroll_x = false, bool scroll_y = false,
...
)
=> ctx.popup_begin_id(@compute_id($vasplat), pos, width, height, dir, anchor, scroll_x, scroll_y);
fn bool? Ctx.popup_begin_id(&ctx,
Id id,
Point pos,
Size width, Size height,
LayoutDirection dir, Anchor anchor,
bool scroll_x, bool scroll_y
)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_DIV)!.unpack(&elem, &parent);
ctx.active_div = elem.tree_idx;
Style* style = ctx.styles.get_style(@str_hash("popup"));
elem.div.scroll_x.enabled = scroll_x;
elem.div.scroll_y.enabled = scroll_y;
elem.z_index++;
// update layout with correct info
elem.layout = {
.w = width,
.h = height,
.dir = dir,
.anchor = anchor,
.content_offset = style.margin + style.border + style.padding,
.absolute = true,
.origin.x = pos.x - parent.bounds.x,
.origin.y = pos.y - parent.bounds.y,
};
// if the position is not the one expected then request a frame-skip to not flash appear in the
// wrong position for one frame
if (elem.bounds.position() != pos) {
ctx.skip_frame = true;
return true;
}
ctx.push_rect(elem.bounds.pad(style.margin), elem.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.z_index)!;
//elem.events = ctx.get_elem_events(elem);
// check close condition, mouse release anywhere outside the div bounds
if ((ctx.mouse_released() & BTN_ANY) && ctx.input.mouse.pos.outside(elem.bounds)) {
return false;
}
// TODO: check active
// TODO: check resizeable
return true;
}

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, elem;
ctx.get_elem(id, ETYPE_NONE)!.unpack(&elem, &parent);
elem.layout.w = @exact((short)width);
elem.layout.h = @exact((short)height);
update_parent_size(elem, parent);
}

View File

@ -1,186 +0,0 @@
module ugui;
import std::io;
import std::math;
// slider element
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);
<*
@require value != null
*>
fn ElemEvents? Ctx.slider_hor_id(&ctx, Id id, Size w, Size h, float* value, float hpercent = 0.25)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_SLIDER)!.unpack(&elem, &parent);
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);
// handle width
short hw = (short)(content_bounds.w * hpercent);
Rect handle = {
.x = calc_slider(content_bounds.x, content_bounds.w-hw, *value),
.y = content_bounds.y,
.w = hw,
.h = content_bounds.h,
};
elem.slider.handle = handle;
Point m = ctx.input.mouse.pos;
//elem.events = ctx.get_elem_events(elem);
if (elem.events.has_focus && 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);
elem.events.update = true;
}
// Draw the slider background and handle
Style s = *style;
Rect padding = s.padding;
s.padding = {};
ctx.push_rect(bg_bounds, parent.z_index, &s)!;
s.bg = s.primary;
s.padding = padding;
s.border = {};
ctx.push_rect(elem.slider.handle, parent.z_index, &s)!;
return elem.events;
}
/*
* +--+
* | |
* | |
* +--+
* |##| handle
* |##|
* +--+
* | |
* | |
* +--+
*/
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)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_SLIDER)!.unpack(&elem, &parent);
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);
// 2. Layout
Rect bg_bounds = elem.bounds.pad(style.margin);
Rect content_bounds = elem.bounds.pad(style.margin + style.border + style.padding);
// handle height
short hh = (short)(content_bounds.h * hpercent);
Rect handle = {
.x = content_bounds.x,
.y = calc_slider(content_bounds.y, content_bounds.h-hh, *value),
.w = content_bounds.w,
.h = hh,
};
elem.slider.handle = handle;
Point m = ctx.input.mouse.pos;
//elem.events = ctx.get_elem_events(elem);
if (elem.events.has_focus && 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);
elem.events.update = true;
}
// Draw the slider background and handle
Style s = *style;
Rect padding = s.padding;
s.padding = {};
ctx.push_rect(bg_bounds, parent.z_index, &s)!;
s.bg = s.primary;
s.padding = padding;
s.border = {};
ctx.push_rect(elem.slider.handle, parent.z_index, &s)!;
return elem.events;
}
fn void? Ctx.scrollbar(&ctx, Id id, float *value, float handle_percent, bool vertical = true)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_SLIDER)!.unpack(&elem, &parent);
Style* style = ctx.styles.get_style(@str_hash("scrollbar"));
Rect pb = parent.bounds.pad(parent.layout.content_offset);
elem.bounds.x = vertical ? pb.bottom_right().x - style.size: pb.x;
elem.bounds.y = vertical ? pb.y : pb.bottom_right().y - style.size;
if (vertical) {
elem.layout.w = @exact(style.size);
elem.layout.h = @grow();
elem.bounds.x -= style.margin.x + style.margin.w + style.border.x + style.border.w;
} else {
elem.layout.w = @grow();
elem.layout.h = @exact(style.size);
elem.bounds.y -= style.margin.y + style.margin.h + style.border.y + style.border.h;
}
elem.layout.content_offset = style.margin + style.border + style.padding;
elem.layout.absolute = true;
update_parent_size(elem, parent);
Rect content_bounds = elem.bounds.pad(elem.layout.content_offset);
//elem.events = ctx.get_elem_events(elem);
short o = vertical ? content_bounds.y : content_bounds.x;
short m = vertical ? ctx.input.mouse.pos.y : ctx.input.mouse.pos.x;
short s = vertical ? content_bounds.h : content_bounds.w;
short h = (short)((float)s * handle_percent);
if (elem.events.has_focus && ctx.is_mouse_down(BTN_LEFT)) {
*value = calc_value(o, m, s, h);
elem.events.update = true;
}
short handle_pos = calc_slider(o, s-h, *value);
elem.slider.handle = {
.x = vertical ? content_bounds.x : handle_pos,
.y = vertical ? handle_pos : content_bounds.y,
.w = vertical ? content_bounds.w : h,
.h = vertical ? h : content_bounds.h,
};
Rect bg_bounds = elem.bounds.pad(style.margin);
ctx.push_rect(bg_bounds, parent.z_index, style)!;
ctx.push_rect(elem.slider.handle, parent.z_index, &&(Style){.bg = style.primary, .radius = style.radius})!;
}
macro short calc_slider(short off, short dim, float value) => (short)off + (short)(dim * value);
macro float calc_value(short off, short mouse, short dim, short slider)
=> math::clamp((float)(mouse-off-slider/2)/(float)(dim-slider), 0.0f, 1.0f);

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, elem;
ctx.get_elem(id, ETYPE_SPRITE)!.unpack(&elem, &parent);
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.z_index, type: sprite.type)!;
}

View File

@ -1,96 +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, elem;
ctx.get_elem(id, ETYPE_TEXT)!.unpack(&elem, &parent);
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.z_index, style.fg)!;
}
macro Ctx.text_box(&ctx, Size w, Size h, TextEdit* te, Anchor text_alignment = TOP_LEFT, bool reflow = true, ...)
=> ctx.text_box_id(@compute_id($vasplat), w, h, te, text_alignment, reflow);
fn ElemEvents? Ctx.text_box_id(&ctx, Id id, Size w, Size h, TextEdit* te, Anchor text_alignment, bool reflow)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_TEXT)!.unpack(&elem, &parent);
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);
}
Rect bg_bounds = elem.bounds.pad(style.margin);
Rect text_bounds = elem.bounds.pad(elem.layout.content_offset);
ctx.push_rect(bg_bounds, parent.z_index, style)!;
String s = elem.text.te.to_string();
if (te.sel_len) {
usz start = te.sel_len > 0 ? te.cursor : te.cursor + te.sel_len;
usz end = (te.sel_len > 0 ? te.cursor + te.sel_len : te.cursor) - 1;
ctx.draw_string_selection(s, text_bounds, text_alignment, start, end, parent.z_index, style.accent, reflow)!;
}
ctx.layout_string(s, text_bounds, text_alignment, parent.z_index, style.fg, reflow)!;
// draw the cursor if the element has focus
if (elem.events.has_focus) {
if (elem.events.mouse_press || elem.events.mouse_hold) {
usz cur = ctx.hit_test_string(s, text_bounds, text_alignment, ctx.input.mouse.pos, reflow)!;
te.set_cursor(cur, elem.events.mouse_hold & !elem.events.mouse_press);
}
Rect cur = ctx.get_cursor_position(s, text_bounds, text_alignment, te.cursor, reflow)!;
cur.w = 2;
ctx.push_rect(cur, parent.z_index, &&(Style){.bg = style.fg})!;
}
return elem.events;
}

View File

@ -3,7 +3,7 @@
"warnings": ["no-unused"],
"dependency-search-paths": ["lib", "lib/vendor/libraries"],
"dependencies": ["sdl3", "ugui"],
"features": ["DEBUG_POINTER"],
"features": [],
"authors": ["Alessandro Mauri <ale@shitposting.expert>"],
"version": "0.1.0",
"sources": ["src/**"],
@ -14,6 +14,7 @@
"type": "executable"
}
},
"cpu": "native",
"opt": "O0",
"debug-info": "full"
}

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

@ -1,35 +1,40 @@
div {
bg: #282828;
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,25 +66,3 @@ slider {
secondary: #458588ff;
accent: #fabd2fff;
}
scrollbar {
padding: 2;
size: 8;
bg: #45858842;
fg: #fbf1c7ff;
primary: #cc241dff;
secondary: #458588ff;
accent: #fabd2fff;
}
text-box {
bg: #4a4543ff;
fg: #fbf1c7ff;
primary: #cc241dff;
secondary: #458588ff;
accent: #fabd2fff;
border: 1;
padding: 4;
margin: 2;
}

View File

@ -1,11 +1,11 @@
import std::io;
import vtree;
import cache;
import ugui;
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;
@ -13,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++) {
@ -21,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);
}
@ -32,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++) {
@ -40,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;
@ -137,38 +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.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.rshift = e.key.key == K_RSHIFT ? !!(e.type == EVENT_KEY_DOWN) : mod.rshift;
mod.lshift = e.key.key == K_LSHIFT ? !!(e.type == EVENT_KEY_DOWN) : mod.lshift;
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:
@ -208,35 +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);
$case "popup":
popup(&ui);
$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, absolute: true, off: {10, 10}) {
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());
@ -244,243 +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;
if (ui.skip_frame) timeout = 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 LayoutDirection d = ROW;
ui.@div(ugui::@grow(), ugui::@grow(), anchor: CENTER) {
ui.@div(ugui::@exact(300), ugui::@exact(300), d, scroll_x: true, scroll_y: true) {
if (ui.button("one")!!.mouse_release) d = COLUMN;
if (ui.button("two")!!.mouse_release) d = ROW;
ui.button("three")!!;
ui.button("four")!!;
ui.button("five")!!;
ui.button("six")!!;
ui.button("seven")!!;
ui.button("eight")!!;
ui.button("nine")!!;
ui.button("ten")!!;
}!!;
}!!;
}
/*
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--;
}
static bool toggle;
static ugui::Point pos;
if (ui.is_mouse_released(ugui::BTN_RIGHT)) {
pos = ui.input.mouse.pos;
toggle = !toggle;
}
if (toggle) {
toggle = ui.popup_begin(pos, ugui::@fit(50), ugui::@fit(100), COLUMN)!!;
ui.button("Uno")!!;
ui.button("Due")!!;
ui.button("Tre")!!;
ui.button("Quattro")!!;
ui.div_end()!!;
}
// 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.@row() {
ui.@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.@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.@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.@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.@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) {
ui.text_box(ugui::@grow(), ugui::@exact(100), te, TOP_LEFT)!!;
}!!;
}!!; }!!;
}
fn void popup(ugui::Ctx* ui)
{
static bool toggle;
static ugui::Point pos;
if (toggle) {
toggle = ui.popup_begin(pos, ugui::@fit(), ugui::@fit(), COLUMN)!!;
ui.button("POP")!!;
ui.button("UP")!!;
ui.div_end()!!;
}
ui.@center(COLUMN) {
if (ui.button("ciao")!!.mouse_release) {
pos = ui.input.mouse.pos;
toggle = ~toggle;
}
if (ui.button("mamma")!!.mouse_press) ugui::println("pressed!");
}!!;
}

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,60 +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 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;
@ -95,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
@ -129,24 +86,46 @@ 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
$if $feature(RENDER_DEBUG) == false && $feature(USE_WAYLAND) == true:
$if DEBUG == 0:
bool has_wayland = false;
for (int i = 0; i < sdl::get_num_video_drivers(); i++) {
ZString driver = sdl::get_video_driver(i);
@ -161,7 +140,7 @@ $if $feature(RENDER_DEBUG) == false && $feature(USE_WAYLAND) == true:
}
$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
@ -220,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}
@ -253,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);
@ -263,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
@ -298,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) {
@ -337,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)
@ -412,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");
@ -437,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) {
@ -466,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[]){
@ -508,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
@ -560,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):
@ -584,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):
@ -596,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,
@ -646,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);
@ -704,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);
@ -769,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
@ -821,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,
@ -846,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,
@ -887,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
@ -953,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 ===
@ -967,39 +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);
bool no_draws;
uint calls = 0;
Cmd* last_command;
uint off;
while (true) {
Cmd? cmd = queue.pop_first();
if (catch e = cmd) {
if (e != NO_MORE_ELEMENT) unreachable();
break;
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_REQ_SKIP_FRAME:
no_draws = true;
case CMD_RECT: nextcase;
case CMD_SPRITE:
count++;
case CMD_UPDATE_ATLAS:
// TODO: verify the correct type
CmdUpdateAtlas u = cmd.update_atlas;
@ -1014,22 +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:
if (no_draws) break;
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.pop_first();
}
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++;
}
}