Compare commits
4 Commits
c3
...
correct-pa
Author | SHA1 | Date | |
---|---|---|---|
f409a67130 | |||
d0a8e8a1ff | |||
fa334ed154 | |||
b35bb427a7 |
6
Makefile
6
Makefile
@ -1,9 +1,3 @@
|
||||
C3FLAGS = -g
|
||||
|
||||
main: src/main.c3 src/renderer.c3 $(wildcard lib/ugui/src/*.c3)
|
||||
make -C resources/shaders
|
||||
c3c build ${C3FLAGS}
|
||||
|
||||
test_renderer: test_renderer.c3 src/renderer.c3 resources/shaders/source/*
|
||||
scripts/compile_shaders.sh
|
||||
c3c compile -g -O0 test_renderer.c3 src/renderer.c3 --libdir lib --lib sdl3 --lib ugui
|
||||
|
55
TODO
55
TODO
@ -5,7 +5,7 @@
|
||||
[x] Port font system from C to C3 (rewrite1)
|
||||
[ ] Update ARCHITECTURE.md
|
||||
[ ] Write a README.md
|
||||
[x] Use an arena allocator for cache
|
||||
[ ] Use an arena allocator for cache
|
||||
[ ] Do not redraw if there was no update (no layout and no draw)
|
||||
[ ] Do command buffer damage tracking based on a context grid (see rxi writeup)
|
||||
[x] Better handling of the active and focused widgets, try
|
||||
@ -15,12 +15,10 @@ to maintain focus until mouse release (fix scroll bars)
|
||||
[x] Implement a z index and sort command buffer based on that
|
||||
[ ] Ctx.set_z_index()
|
||||
[x] Sort command buffer on insertion
|
||||
[x] Standardize element handling, for example all buttons do almost the same thing, so write a lot
|
||||
of boiler plate and reuse it
|
||||
[x] The id combination in gen_id() uses an intger division, which is costly, use another combination
|
||||
function that is non-linear and doesn't use division
|
||||
[x] Standardize element handling, for example all buttons do almost the same thing, so write a lot of boiler plate and reuse it
|
||||
[x] The id combination in gen_id() uses an intger division, which is costly, use another combination function that is non-linear and doesn't use division
|
||||
[ ] Animations, somehow
|
||||
[x] Maybe cache codepoint converted strings
|
||||
[ ] Maybe cache codepoint converted strings
|
||||
[x] Fix scroll wheel when div is scrolled
|
||||
[ ] Be consistent with the initialization methods some are foo.new() and some are foo.init()
|
||||
[ ] Implement image loading (.bmp, .ff, .qoi and .png), in the future even lossy images like .jpg
|
||||
@ -41,37 +39,20 @@ to maintain focus until mouse release (fix scroll bars)
|
||||
each struct Elem, struct Cmd, etc. is as big as the largest element. It would
|
||||
be better to use a different allcation strategy.
|
||||
[ ] Add a way to handle time events like double clicks
|
||||
[x] Fix how padding is applied in push_rect. In CSS padding is applied between the border and the
|
||||
content, the background color is applied starting from the border. Right now push_rect() offsets
|
||||
the background rect by both border and padding
|
||||
[x] Investigate why the debug pointer (cyan rectangle) disappears...
|
||||
[ ] Border and padding do not go well together if the library issues two rect commands, the visible
|
||||
border is effectively the border size plus the padding since there is a gap between the border
|
||||
rect and the internal rect. A better solution is to leave it up to the renderer to draw the rect
|
||||
correctly
|
||||
|
||||
## Layout
|
||||
|
||||
[x] Flexbox
|
||||
[x] Center elements to the row/column
|
||||
[x] Text wrapping / reflow
|
||||
[x] Implement a better and unified way to place a glyph and get the cursor position, maybe with a struct
|
||||
[ ] Correct whitespace handling in text (\t \r etc)
|
||||
[x] Consider a multi-pass recursive approach to layout (like https://github.com/nicbarker/clay)
|
||||
[ ] For some reason padding is not correct, look at the sliders, they have 2px per side when the
|
||||
theme specifies 4px per side
|
||||
[ ] Center elements to the row/column
|
||||
[ ] Text wrapping / reflow
|
||||
[ ] Consider a multi-pass recursive approach to layout (like https://github.com/nicbarker/clay)
|
||||
instead of the curren multi-frame approach.
|
||||
[x] Implement column/row sizing (min, max)
|
||||
[x] Implement a way to size the element as the current row/column size
|
||||
* +-------------+
|
||||
* | |
|
||||
* +-------------+
|
||||
* +--+
|
||||
* | |
|
||||
* +--+
|
||||
* <------------->
|
||||
* column size
|
||||
See the calculator example for why it is useful
|
||||
[ ] Find a way to concile pixel measurements to the mm ones used in css, for example in min/max sizing
|
||||
of elements
|
||||
[x] Center elements to div (center children_bounds to the center of the div bounds and shift the origin accordingly)
|
||||
[x] Use containing_rect() in position_element() to skip some computing and semplify the function
|
||||
[x] Rename position_element() to layout_element()
|
||||
[x] Make functions to mark rows/columns as full, to fix the calculator demo
|
||||
|
||||
## Input
|
||||
|
||||
@ -80,8 +61,6 @@ to maintain focus until mouse release (fix scroll bars)
|
||||
[ ] Touch input
|
||||
[x] Do not set input event to true if the movement was zero (like no mouse movement)
|
||||
[ ] Use input event flags, for example to consume the input event
|
||||
[ ] Fix bug in text box: when spamming keys you can get multiple characters in the text input field
|
||||
of the context, this causes a bug where only the first char is actually used
|
||||
|
||||
## Commands
|
||||
|
||||
@ -90,9 +69,8 @@ to maintain focus until mouse release (fix scroll bars)
|
||||
- border radius
|
||||
[x] add a command to update an atlas
|
||||
[ ] New window command, useful for popups
|
||||
[x] Text command returns the text bounds, this way we can avoid the pattern
|
||||
[ ] Text command returns the text bounds, this way we can avoid the pattern
|
||||
draw_text(a, pos) -> off = compute_bounds(a) -> draw_text(b, pos+off) -> ...
|
||||
[ ] Rounded rectangle with different radius for each corner
|
||||
|
||||
## Atlas
|
||||
|
||||
@ -115,6 +93,11 @@ to maintain focus until mouse release (fix scroll bars)
|
||||
[x] Checkbox
|
||||
[ ] Selectable text box
|
||||
|
||||
## Main / exaple
|
||||
|
||||
[ ] Create maps from ids to textures and images instead of hardcoding them
|
||||
|
||||
|
||||
## API
|
||||
|
||||
[ ] Introduce a Layout structure that specifies the positioning of elements inside
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit e7356df5d1d0c22a6bba822bda6994062b0b75d7
|
||||
Subproject commit 076355e2d126e7546e53663b97e8dec22667d34d
|
@ -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.
|
@ -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;
|
||||
|
@ -9,23 +9,21 @@ import std::sort;
|
||||
// TODO: specify the allocator
|
||||
|
||||
struct Fifo {
|
||||
Allocator allocator;
|
||||
Type[] arr;
|
||||
usz out;
|
||||
usz count;
|
||||
}
|
||||
|
||||
fn void? Fifo.init(&fifo, usz size, Allocator allocator)
|
||||
fn void? Fifo.init(&fifo, usz size)
|
||||
{
|
||||
fifo.allocator = allocator;
|
||||
fifo.arr = allocator::new_array(fifo.allocator, Type, size);
|
||||
fifo.arr = mem::new_array(Type, size);
|
||||
fifo.out = 0;
|
||||
fifo.count = 0;
|
||||
}
|
||||
|
||||
fn void Fifo.free(&fifo)
|
||||
{
|
||||
(void)allocator::free(fifo.allocator, fifo.arr);
|
||||
(void)mem::free(fifo.arr);
|
||||
}
|
||||
|
||||
fn void? Fifo.enqueue(&fifo, Type *elem)
|
||||
@ -71,8 +69,8 @@ macro usz Fifo.len(&fifo) @operator(len)
|
||||
|
||||
fn void? Fifo.sort(&fifo)
|
||||
{
|
||||
Type[] arr = allocator::new_array(fifo.allocator, Type, fifo.count);
|
||||
defer allocator::free(fifo.allocator, arr);
|
||||
Type[] arr = mem::new_array(Type, fifo.count);
|
||||
defer mem::free(arr);
|
||||
|
||||
foreach(i, c: fifo) {
|
||||
arr[i] = c;
|
||||
|
@ -1,343 +0,0 @@
|
||||
module mtree{Type};
|
||||
|
||||
/* ================================================================================================
|
||||
* MTree, Bitmap-based tree
|
||||
* ================================================================================================
|
||||
*
|
||||
* Overview
|
||||
* --------
|
||||
* The MTree is a bitmap-based tree structure composed of three core elements:
|
||||
* - Element Vector: Stores user data.
|
||||
* - Reference Node Vector: Manages node relationships.
|
||||
* - Bitmap: Marks used indices.
|
||||
*
|
||||
* The name "MTree" originates from "Matrix Tree," where the vector is divided into
|
||||
* sectors of power-of-two sizes. Each node's bitmap marks the positions of its
|
||||
* children within the same sector.
|
||||
*
|
||||
* If a parent and its children are in different sectors, a new node is created.
|
||||
* The parent's "next" field points to this new node, forming a chain that must
|
||||
* be traversed during iteration.
|
||||
*
|
||||
*
|
||||
* Example (sector size = 8)
|
||||
* -------------------------
|
||||
*
|
||||
* _________________________________
|
||||
* |__ __ _______________________ |
|
||||
* | | | | _ |
|
||||
* | v v vv |v
|
||||
* +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
* refs_vec:| 0| 1| 2| 3| 4| 5| 6| 7| 8| 9|10|11|12|13|14|15|16|...
|
||||
* +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|
||||
* \__________ __________/ \__________ __________/ \__
|
||||
* V V
|
||||
* sector 0 sector 1
|
||||
*
|
||||
*
|
||||
* Node Relationships:
|
||||
* -------------------
|
||||
* - Root (Element 0) has three direct children: 1, 2, and 10.
|
||||
* - Node 10 is in a different sector than the root, so root.next points to Node 11.
|
||||
* - Node 11 has Node 10 as a direct child and Node 0 (root) as its parent.
|
||||
*
|
||||
* Bitmap Representation:
|
||||
* ---------------------
|
||||
*
|
||||
* root = {
|
||||
* .parent = -1; // Root has no parent
|
||||
* .next = 11; // Points to Node 11
|
||||
* .children = 0b00000110; // [0|1|1|0|0|0|0|0] (Children: 1, 2)
|
||||
* }
|
||||
*
|
||||
* node11 = {
|
||||
* .parent = 0; // Parent is root (Node 0)
|
||||
* .next = -1; // Last in the chain
|
||||
* .children = 0b00000100; // [0|0|1|0|0|0|0|0] (Child: 10)
|
||||
* }
|
||||
*
|
||||
* ================================================================================================
|
||||
*/
|
||||
|
||||
import std::core::mem;
|
||||
import std::core::mem::allocator;
|
||||
import std::io;
|
||||
import std::bits;
|
||||
import std::collections::list;
|
||||
|
||||
|
||||
alias Bitmap = ulong;
|
||||
const BITS = Bitmap.sizeof*8;
|
||||
|
||||
alias IdxList = List{int};
|
||||
|
||||
// next: if positive it contains the index of the next node that contains the children information
|
||||
struct RefNode {
|
||||
int next;
|
||||
int parent;
|
||||
Bitmap children;
|
||||
}
|
||||
|
||||
struct MTree {
|
||||
usz elements;
|
||||
Allocator allocator;
|
||||
IdxList queue;
|
||||
Bitmap[] used;
|
||||
Type[] elem_vec; // element vector
|
||||
RefNode[] refs_vec; // relationship vector
|
||||
}
|
||||
|
||||
|
||||
fn void MTree.init(&tree, usz size, Allocator allocator = mem)
|
||||
{
|
||||
// round size to the nearest multiple of BITS
|
||||
size = size + size%BITS;
|
||||
|
||||
tree.elements = 0;
|
||||
tree.allocator = allocator;
|
||||
tree.queue.init(tree.allocator, size);
|
||||
tree.used = allocator::new_array(tree.allocator, Bitmap, size/BITS);
|
||||
tree.elem_vec = allocator::new_array(tree.allocator, Type, size);
|
||||
tree.refs_vec = allocator::new_array(tree.allocator, RefNode, size);
|
||||
|
||||
foreach (&r: tree.refs_vec) {
|
||||
r.next = -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn void MTree.free(&tree)
|
||||
{
|
||||
tree.elements = 0;
|
||||
tree.queue.free();
|
||||
(void)allocator::free(tree.allocator, tree.used);
|
||||
(void)allocator::free(tree.allocator, tree.elem_vec);
|
||||
(void)allocator::free(tree.allocator, tree.refs_vec);
|
||||
}
|
||||
|
||||
|
||||
fn int MTree.get_free_spot(&tree)
|
||||
{
|
||||
foreach (idx, d: tree.used) {
|
||||
if (d != $typeof(d).max) {
|
||||
int spot = (int)idx*BITS + BITS-(int)d.clz();
|
||||
return spot;
|
||||
}
|
||||
}
|
||||
unreachable("no free spots left");
|
||||
}
|
||||
|
||||
<* @require idx >= 0 *>
|
||||
macro void MTree.set_used(&tree, int idx)
|
||||
{
|
||||
int r = idx % BITS;
|
||||
int q = idx / BITS;
|
||||
tree.used[q] |= (1l << r);
|
||||
}
|
||||
|
||||
<* @require idx >= 0 *>
|
||||
macro void MTree.unset_used(&tree, int idx)
|
||||
{
|
||||
int r = idx % BITS;
|
||||
int q = idx / BITS;
|
||||
tree.used[q] &= ~(1l << r);
|
||||
}
|
||||
|
||||
<* @require idx >= 0 *>
|
||||
macro bool MTree.is_used(&tree, int idx)
|
||||
{
|
||||
int r = idx % BITS;
|
||||
int q = idx / BITS;
|
||||
return !!(tree.used[q] & (1l << r));
|
||||
}
|
||||
|
||||
|
||||
// get the last node in the "next" chain
|
||||
<* @require tree.is_used(parent) == true *>
|
||||
fn int MTree.last_node(&tree, int parent)
|
||||
{
|
||||
while(tree.refs_vec[parent].next >= 0) {
|
||||
parent = tree.refs_vec[parent].next;
|
||||
}
|
||||
return parent;
|
||||
}
|
||||
|
||||
|
||||
<* @require tree.elements == 0 || tree.is_used(parent) == true *>
|
||||
fn int MTree.add(&tree, int parent, Type t)
|
||||
{
|
||||
int idx = tree.get_free_spot();
|
||||
int subtree = idx / BITS;
|
||||
|
||||
tree.set_used(idx);
|
||||
tree.elem_vec[idx] = t;
|
||||
tree.refs_vec[idx] = (RefNode){
|
||||
.parent = parent,
|
||||
.next = -1,
|
||||
};
|
||||
|
||||
tree.elements++;
|
||||
// root element, has no parent
|
||||
if (tree.elements == 1) {
|
||||
tree.refs_vec[idx].parent = -1;
|
||||
return idx;
|
||||
}
|
||||
|
||||
|
||||
// if the parent already has a node in the same subtree as the child then update that node's
|
||||
// children bitmap
|
||||
bool done;
|
||||
for (int p = parent; p >= 0; p = tree.refs_vec[p].next) {
|
||||
int ps = p/BITS;
|
||||
if (ps == subtree) {
|
||||
tree.refs_vec[p].children |= (1l << (idx%BITS));
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// on fail we need to create another parent node
|
||||
if (!done) {
|
||||
int new_next = tree.get_free_spot();
|
||||
// if the new node does not land in the same subtree as the child we cannot do
|
||||
// anything since the references are immutable
|
||||
if (new_next/BITS != subtree) {
|
||||
unreachable("cannot allocate new child for parent");
|
||||
}
|
||||
tree.set_used(new_next);
|
||||
tree.elements++;
|
||||
// update the "next" chain
|
||||
int last_link = tree.last_node(parent);
|
||||
tree.refs_vec[last_link].next = new_next;
|
||||
tree.refs_vec[new_next].next = -1;
|
||||
tree.refs_vec[new_next].children |= (long)(1 << (idx%BITS));
|
||||
tree.refs_vec[new_next].parent = last_link;
|
||||
// FIXME: the elem_vec is not updated, do we need to?
|
||||
}
|
||||
|
||||
return idx;
|
||||
}
|
||||
|
||||
|
||||
// get the index of the n-th children of parent, -1 otherwise
|
||||
// usage: for (int i, c; (c = tree.children_it(parent, i)) >= 0; i++) { ... }
|
||||
fn int MTree.children_it(&tree, int parent, int n)
|
||||
{
|
||||
int tot_children;
|
||||
int child;
|
||||
for (int p = parent; p >= 0; p = tree.refs_vec[p].next) {
|
||||
int cn = (int)tree.refs_vec[p].children.popcount();
|
||||
tot_children += cn;
|
||||
|
||||
// we are in the right subtree
|
||||
if (tot_children > n) {
|
||||
child = (p/BITS) * BITS; // start at the parent's subtree index
|
||||
int j = cn - (tot_children - n); // we need the j-th children of this node
|
||||
Bitmap u = tree.refs_vec[p].children;
|
||||
|
||||
child += j; // add the children number
|
||||
do {
|
||||
child += (int)u.ctz(); // increment by the skipped zeroes
|
||||
u >>= u.ctz() + 1;
|
||||
j--;
|
||||
} while (j >= 0);
|
||||
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
fn int MTree.children_num(&tree, int parent)
|
||||
{
|
||||
int n;
|
||||
for (int p = parent; p >= 0; p = tree.refs_vec[p].next) {
|
||||
n += (int)tree.refs_vec[p].children.popcount();
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
fn int MTree.subtree_size(&tree, int parent)
|
||||
{
|
||||
int x = tree.children_num(parent);
|
||||
int c;
|
||||
for (int n; (c = tree.children_it(parent, n)) >= 0; n++) {
|
||||
x += tree.subtree_size(c);
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
|
||||
|
||||
fn int MTree.level_order_it(&tree, int parent, int i)
|
||||
{
|
||||
if (i == 0) {
|
||||
tree.queue.clear();
|
||||
tree.queue.push(parent);
|
||||
}
|
||||
|
||||
if (tree.queue.len() == 0) return -1;
|
||||
|
||||
int p = tree.queue.pop_first()!!;
|
||||
int c;
|
||||
for (int n; (c = tree.children_it(p, n)) >= 0; n++) {
|
||||
tree.queue.push(c);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
fn void MTree.prune(&tree, int parent)
|
||||
{
|
||||
int c;
|
||||
for (int i = 0; (c = tree.children_it(parent, i)) >= 0; i++) {
|
||||
tree.prune(c); // prune the subtree
|
||||
|
||||
// delete all children including their next chain
|
||||
for (int p = c; p >= 0;) {
|
||||
int next = tree.refs_vec[p].next;
|
||||
tree.unset_used(p);
|
||||
tree.refs_vec[p] = {.next = -1};
|
||||
p = next;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// finally delete the parent
|
||||
for (int p = parent; p >= 0;) {
|
||||
int next = tree.refs_vec[p].next;
|
||||
tree.unset_used(p);
|
||||
tree.elements--;
|
||||
tree.refs_vec[p] = {.next = -1};
|
||||
p = next;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
<* @require tree.is_used(ref) *>
|
||||
fn Type MTree.get(&tree, int ref) => tree.elem_vec[ref];
|
||||
|
||||
<* @require tree.is_used(ref) *>
|
||||
fn Type MTree.parentof(&tree, int ref) => tree.refs_vec[ref].parent;
|
||||
|
||||
fn void MTree.nuke(&tree)
|
||||
{
|
||||
foreach (idx, &b: tree.used) {
|
||||
*b = 0;
|
||||
tree.refs_vec[idx] = {.next = -1};
|
||||
}
|
||||
tree.elements = 0;
|
||||
}
|
||||
|
||||
|
||||
macro bool MTree.is_root(&t, int i) => t.refs_vec[i].parent == -1;
|
||||
|
||||
fn void MTree.print(&tree)
|
||||
{
|
||||
foreach (idx, c: tree.elem_vec) {
|
||||
if (tree.is_used((int)idx)) {
|
||||
io::printfn("[%d](%s) parent:%d next:%d children:%b",
|
||||
idx, c, tree.refs_vec[idx].parent, tree.refs_vec[idx].next,
|
||||
tree.refs_vec[idx].children
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
187
lib/ugui.c3l/src/ugui_button.c3
Normal file
187
lib/ugui.c3l/src/ugui_button.c3
Normal 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)!;
|
||||
}
|
@ -14,6 +14,7 @@ enum CmdType {
|
||||
// command to draw a rect
|
||||
struct CmdRect {
|
||||
Rect rect;
|
||||
ushort thickness;
|
||||
ushort radius;
|
||||
Color color;
|
||||
}
|
||||
@ -58,26 +59,7 @@ fn int Cmd.compare_to(Cmd a, Cmd b)
|
||||
// implement the Printable interface
|
||||
fn usz? Cmd.to_format(Cmd* cmd, Formatter *f) @dynamic
|
||||
{
|
||||
usz ret;
|
||||
|
||||
ret += f.printf("Cmd{ type: %s, z_index: %d, ", cmd.type, cmd.z_index)!;
|
||||
switch (cmd.type) {
|
||||
case CMD_RECT:
|
||||
ret += f.print("CmdRect")!;
|
||||
ret += io::struct_to_format(cmd.rect, f, false)!;
|
||||
case CMD_SCISSOR:
|
||||
ret += f.print("CmdScissor")!;
|
||||
ret += io::struct_to_format(cmd.scissor, f, false)!;
|
||||
case CMD_SPRITE:
|
||||
ret += f.print("CmdSprite")!;
|
||||
ret += io::struct_to_format(cmd.sprite, f, false)!;
|
||||
case CMD_UPDATE_ATLAS:
|
||||
ret += f.print("CmdUpdateAtlas")!;
|
||||
ret += io::struct_to_format(cmd.update_atlas, f, false)!;
|
||||
}
|
||||
|
||||
ret += f.print("}")!;
|
||||
return ret;
|
||||
return f.printf("Cmd{ type: %s, z_index: %d }", cmd.type, cmd.z_index);
|
||||
}
|
||||
|
||||
macro bool cull_rect(Rect rect, Rect clip = {0,0,short.max,short.max})
|
||||
@ -96,11 +78,7 @@ macro Ctx.push_cmd(&ctx, Cmd *cmd, int z_index)
|
||||
case CMD_SPRITE: rect = cmd.sprite.rect;
|
||||
default: return ctx.cmd_queue.enqueue(cmd);
|
||||
}
|
||||
if (cull_rect(rect, ctx.div_scissor)) {
|
||||
// println("NOPE: ", cmd.rect.rect, cmd.z_index);
|
||||
// unreachable();
|
||||
return;
|
||||
}
|
||||
if (cull_rect(rect, ctx.div_scissor)) return;
|
||||
return ctx.cmd_queue.enqueue(cmd);
|
||||
}
|
||||
|
||||
@ -113,22 +91,21 @@ fn void? Ctx.push_scissor(&ctx, Rect rect, int z_index)
|
||||
ctx.push_cmd(&sc, z_index)!;
|
||||
}
|
||||
|
||||
fn void? Ctx.reset_scissor(&ctx, int z_index) => ctx.push_cmd(&&(Cmd){.type=CMD_SCISSOR,.scissor.rect=ctx.div_scissor}, z_index)!;
|
||||
|
||||
fn void? Ctx.push_rect(&ctx, Rect rect, int z_index, Style* style)
|
||||
{
|
||||
Rect border = style.border;
|
||||
Rect padding = style.padding;
|
||||
ushort border = style.border;
|
||||
ushort radius = style.radius;
|
||||
Color bg = style.bg;
|
||||
Color border_color = style.secondary;
|
||||
|
||||
// FIXME: this implies that the border has to be uniform
|
||||
if (!border.is_null()) {
|
||||
if (border != 0) {
|
||||
Cmd cmd = {
|
||||
.type = CMD_RECT,
|
||||
.rect.rect = rect,
|
||||
.rect.color = border_color,
|
||||
.rect.radius = radius + border.x,
|
||||
.rect.radius = radius+border,
|
||||
.rect.thickness = border,
|
||||
};
|
||||
ctx.push_cmd(&cmd, z_index)!;
|
||||
}
|
||||
@ -136,18 +113,19 @@ fn void? Ctx.push_rect(&ctx, Rect rect, int z_index, Style* style)
|
||||
Cmd cmd = {
|
||||
.type = CMD_RECT,
|
||||
.rect.rect = {
|
||||
.x = rect.x + border.x,
|
||||
.y = rect.y + border.y,
|
||||
.w = rect.w - (border.x+border.w),
|
||||
.h = rect.h - (border.y+border.h),
|
||||
.x = rect.x + border + padding.x,
|
||||
.y = rect.y + border + padding.y,
|
||||
.h = rect.h - (border*2) - (padding.y+padding.h),
|
||||
.w = rect.w - (border*2) - (padding.x+padding.w),
|
||||
},
|
||||
.rect.color = bg,
|
||||
.rect.radius = radius,
|
||||
.rect.thickness = max(rect.w, rect.h)/2+1,
|
||||
};
|
||||
if (cull_rect(cmd.rect.rect, ctx.div_scissor)) return;
|
||||
ctx.push_cmd(&cmd, z_index)!;
|
||||
}
|
||||
|
||||
// TODO: accept a Sprite* instead of all this shit
|
||||
fn void? Ctx.push_sprite(&ctx, Rect bounds, Rect texture, Id texture_id, int z_index, Color hue = 0xffffffffu.to_rgba(), SpriteType type = SPRITE_NORMAL)
|
||||
{
|
||||
Cmd cmd = {
|
||||
@ -161,6 +139,58 @@ fn void? Ctx.push_sprite(&ctx, Rect bounds, Rect texture, Id texture_id, int z_i
|
||||
ctx.push_cmd(&cmd, z_index)!;
|
||||
}
|
||||
|
||||
fn void? Ctx.push_string(&ctx, Rect bounds, char[] text, int z_index, Color hue)
|
||||
{
|
||||
if (text.len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.push_scissor(bounds, z_index)!;
|
||||
|
||||
short baseline = (short)ctx.font.ascender;
|
||||
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
|
||||
short line_gap = (short)ctx.font.linegap;
|
||||
Id texture_id = ctx.font.id; // or ctx.font.atlas.id
|
||||
Point orig = {
|
||||
.x = bounds.x,
|
||||
.y = bounds.y,
|
||||
};
|
||||
|
||||
short line_len;
|
||||
Codepoint cp;
|
||||
usz off, x;
|
||||
while (off < text.len && (cp = str_to_codepoint(text[off..], &x)) != 0) {
|
||||
off += x;
|
||||
Glyph* gp;
|
||||
if (!ascii::is_cntrl((char)cp)) {
|
||||
gp = ctx.font.get_glyph(cp)!;
|
||||
Rect gb = {
|
||||
.x = orig.x + line_len + gp.ox,
|
||||
.y = orig.y + gp.oy + baseline,
|
||||
.w = gp.w,
|
||||
.h = gp.h,
|
||||
};
|
||||
Rect gt = {
|
||||
.x = gp.u,
|
||||
.y = gp.v,
|
||||
.w = gp.w,
|
||||
.h = gp.h,
|
||||
};
|
||||
// push the sprite only if it collides with the bounds
|
||||
if (!cull_rect(gb, bounds)) ctx.push_sprite(gb, gt, texture_id, z_index, hue)!;
|
||||
line_len += gp.adv;
|
||||
} else if (cp == '\n'){
|
||||
orig.y += line_height + line_gap;
|
||||
line_len = 0;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: we never get here if an error was thrown before
|
||||
ctx.push_scissor({}, z_index)!;
|
||||
}
|
||||
|
||||
fn void? Ctx.push_update_atlas(&ctx, Atlas* atlas)
|
||||
{
|
||||
Cmd up = {
|
||||
|
@ -1,6 +1,6 @@
|
||||
module ugui;
|
||||
|
||||
import mtree;
|
||||
import vtree;
|
||||
import cache;
|
||||
import fifo;
|
||||
|
||||
@ -9,15 +9,6 @@ import std::core::string;
|
||||
import std::core::mem::allocator;
|
||||
|
||||
|
||||
macro println(...)
|
||||
{
|
||||
$for var $i = 0; $i < $vacount; $i++:
|
||||
io::print($vaexpr[$i]);
|
||||
$endfor
|
||||
io::printn();
|
||||
}
|
||||
|
||||
|
||||
// element ids are just long ints
|
||||
alias Id = uint;
|
||||
|
||||
@ -38,26 +29,23 @@ bitstruct ElemFlags : uint {
|
||||
bitstruct ElemEvents : uint {
|
||||
bool key_press : 0;
|
||||
bool key_release : 1;
|
||||
bool key_repeat : 2;
|
||||
bool key_hold : 2;
|
||||
bool mouse_hover : 3;
|
||||
bool mouse_press : 4;
|
||||
bool mouse_release : 5;
|
||||
bool mouse_hold : 6;
|
||||
bool update : 7;
|
||||
bool text_input : 8;
|
||||
bool has_focus : 9;
|
||||
}
|
||||
|
||||
// element structure
|
||||
struct Elem {
|
||||
Id id;
|
||||
int tree_idx;
|
||||
isz tree_idx;
|
||||
ElemFlags flags;
|
||||
ElemEvents events;
|
||||
Rect bounds;
|
||||
Rect children_bounds;
|
||||
ElemType type;
|
||||
Layout layout;
|
||||
union {
|
||||
ElemDiv div;
|
||||
ElemButton button;
|
||||
@ -69,7 +57,7 @@ struct Elem {
|
||||
|
||||
|
||||
// relationships between elements are stored in a tree, it stores just the ids
|
||||
alias IdTree = mtree::MTree{Id};
|
||||
alias IdTree = vtree::VTree{Id};
|
||||
|
||||
// elements themselves are kept in a cache
|
||||
const uint MAX_ELEMENTS = 256;
|
||||
@ -77,7 +65,7 @@ alias ElemCache = cache::Cache{Id, Elem, MAX_ELEMENTS};
|
||||
|
||||
alias CmdQueue = fifo::Fifo{Cmd};
|
||||
|
||||
faultdef INVALID_SIZE, EVENT_UNSUPPORTED, WRONG_ELEMENT_TYPE, WRONG_ID;
|
||||
faultdef INVALID_SIZE, EVENT_UNSUPPORTED, UNEXPECTED_ELEMENT, WRONG_ELEMENT_TYPE, WRONG_ID;
|
||||
|
||||
const Rect DIV_FILL = { .x = 0, .y = 0, .w = 0, .h = 0 };
|
||||
|
||||
@ -122,17 +110,14 @@ struct Ctx {
|
||||
Id focus_id;
|
||||
|
||||
Rect div_scissor; // the current div bounds used for scissor test
|
||||
int active_div; // tree node indicating the current active div
|
||||
isz active_div; // tree node indicating the current active div
|
||||
}
|
||||
|
||||
// return a pointer to the parent of the current active div
|
||||
fn Elem*? Ctx.get_parent(&ctx)
|
||||
{
|
||||
Id parent_id = ctx.tree.get(ctx.active_div);
|
||||
Elem*? parent = ctx.cache.search(parent_id);
|
||||
if (catch parent) return parent;
|
||||
if (parent.type != ETYPE_DIV) return WRONG_ELEMENT_TYPE?;
|
||||
return parent;
|
||||
Id parent_id = ctx.tree.get(ctx.active_div)!;
|
||||
return ctx.cache.search(parent_id);
|
||||
}
|
||||
|
||||
macro @bits(#a) => $typeof(#a).sizeof*8;
|
||||
@ -143,8 +128,7 @@ const uint GOLDEN_RATIO = 0x9E3779B9;
|
||||
// with the Cantor pairing function
|
||||
fn Id? Ctx.gen_id(&ctx, Id id2)
|
||||
{
|
||||
// FIXME: this is SHIT
|
||||
Id id1 = ctx.tree.get(ctx.active_div);
|
||||
Id id1 = ctx.tree.get(ctx.active_div)!;
|
||||
// Mix the two IDs non-linearly
|
||||
Id mixed = id1 ^ id2.rotate_left(13);
|
||||
mixed ^= id1.rotate_left(7);
|
||||
@ -166,19 +150,19 @@ macro Id @compute_id(...)
|
||||
// resets all flags except is_new which is set accordingly
|
||||
fn Elem*? Ctx.get_elem(&ctx, Id id, ElemType type)
|
||||
{
|
||||
Elem empty_elem;
|
||||
bool is_new;
|
||||
Elem* elem;
|
||||
elem = ctx.cache.get_or_insert(&&(Elem){}, id, &is_new)!;
|
||||
elem = ctx.cache.get_or_insert(&empty_elem, id, &is_new)!;
|
||||
elem.flags = (ElemFlags)0;
|
||||
elem.flags.is_new = is_new;
|
||||
elem.id = id;
|
||||
elem.layout = {};
|
||||
if (is_new == false && elem.type != type) {
|
||||
return WRONG_ELEMENT_TYPE?;
|
||||
} else {
|
||||
elem.type = type;
|
||||
}
|
||||
elem.tree_idx = ctx.tree.add(ctx.active_div, id);
|
||||
elem.tree_idx = ctx.tree.add(id, ctx.active_div)!;
|
||||
return elem;
|
||||
}
|
||||
|
||||
@ -196,22 +180,22 @@ macro Elem* Ctx.find_elem(&ctx, Id id)
|
||||
|
||||
fn Elem*? Ctx.get_active_div(&ctx)
|
||||
{
|
||||
Id id = ctx.tree.get(ctx.active_div);
|
||||
Id id = ctx.tree.get(ctx.active_div)!;
|
||||
return ctx.cache.search(id);
|
||||
}
|
||||
|
||||
fn void? Ctx.init(&ctx, Allocator allocator)
|
||||
fn void? Ctx.init(&ctx)
|
||||
{
|
||||
ctx.tree.init(MAX_ELEMENTS, allocator);
|
||||
ctx.tree.init(MAX_ELEMENTS)!;
|
||||
defer catch { (void)ctx.tree.free(); }
|
||||
|
||||
ctx.cache.init(allocator)!;
|
||||
ctx.cache.init()!;
|
||||
defer catch { (void)ctx.cache.free(); }
|
||||
|
||||
ctx.cmd_queue.init(MAX_CMDS, allocator)!;
|
||||
ctx.cmd_queue.init(MAX_CMDS)!;
|
||||
defer catch { (void)ctx.cmd_queue.free(); }
|
||||
|
||||
ctx.styles.init(allocator);
|
||||
ctx.styles.init(allocator::heap());
|
||||
ctx.styles.register_style(&DEFAULT_STYLE, @str_hash("default"));
|
||||
defer catch { ctx.styles.free(); }
|
||||
|
||||
@ -245,15 +229,16 @@ fn void? Ctx.frame_begin(&ctx)
|
||||
//elem.flags.has_focus = ctx.has_focus;
|
||||
|
||||
elem.bounds = {0, 0, ctx.width, ctx.height};
|
||||
elem.div.layout = LAYOUT_ROW;
|
||||
elem.div.z_index = 0;
|
||||
elem.div.children_bounds = elem.bounds;
|
||||
elem.div.scroll_x.enabled = false;
|
||||
elem.div.scroll_y.enabled = false;
|
||||
elem.layout.dir = ROW;
|
||||
elem.layout.anchor = TOP_LEFT;
|
||||
elem.layout.w = @exact(ctx.width);
|
||||
elem.layout.h = @exact(ctx.height);
|
||||
elem.div.pcb = {};
|
||||
elem.div.origin_c = {};
|
||||
elem.div.origin_r = {};
|
||||
|
||||
ctx.div_scissor = elem.bounds;
|
||||
ctx.div_scissor = {0, 0, ctx.width, ctx.height};
|
||||
|
||||
// The root element does not push anything to the stack
|
||||
// TODO: add a background color taken from a theme or config
|
||||
@ -265,13 +250,7 @@ fn void? Ctx.frame_end(&ctx)
|
||||
{
|
||||
// FIXME: this is not guaranteed to be root. the user might forget to close a div or some other element
|
||||
Elem* root = ctx.get_active_div()!;
|
||||
if (root.id != ROOT_ID) {
|
||||
io::printn(root.id);
|
||||
return WRONG_ID?;
|
||||
}
|
||||
|
||||
// DO THE LAYOUT
|
||||
ctx.layout_element_tree();
|
||||
root.div.layout = LAYOUT_ROW;
|
||||
|
||||
// 1. clear the tree
|
||||
ctx.tree.nuke();
|
||||
@ -308,13 +287,7 @@ $if DEBUG == 1:
|
||||
$endif
|
||||
|
||||
// sort the command buffer by the z-index
|
||||
// FIXME: sorting the buffer fucks with scissor commands that have to be kept in place
|
||||
//ctx.cmd_queue.sort()!;
|
||||
|
||||
// foreach (i, c: ctx.cmd_queue) {
|
||||
// io::printf("[%d]: ", i);
|
||||
// io::printn(c);
|
||||
// }
|
||||
ctx.cmd_queue.sort()!;
|
||||
}
|
||||
|
||||
<*
|
||||
@ -336,7 +309,7 @@ macro bool Ctx.elem_focus(&ctx, Elem *elem)
|
||||
fn ElemEvents Ctx.get_elem_events(&ctx, Elem *elem)
|
||||
{
|
||||
bool hover = ctx.is_hovered(elem);
|
||||
bool focus = ctx.elem_focus(elem) || (hover && ctx.is_mouse_pressed(BTN_LEFT));
|
||||
bool focus = ctx.focus_id == elem.id || (hover && ctx.is_mouse_pressed(BTN_LEFT));
|
||||
|
||||
if (ctx.is_mouse_pressed(BTN_ANY) && !hover){
|
||||
focus = false;
|
||||
@ -347,15 +320,11 @@ fn ElemEvents Ctx.get_elem_events(&ctx, Elem *elem)
|
||||
if (focus) { ctx.focus_id = elem.id; }
|
||||
|
||||
ElemEvents ev = {
|
||||
.has_focus = focus,
|
||||
.mouse_hover = hover,
|
||||
.mouse_press = hover && focus && ctx.is_mouse_pressed(BTN_ANY),
|
||||
.mouse_hover = hover,
|
||||
.mouse_press = hover && focus && ctx.is_mouse_pressed(BTN_ANY),
|
||||
.mouse_release = hover && focus && ctx.is_mouse_released(BTN_ANY),
|
||||
.mouse_hold = hover && focus && ctx.is_mouse_down(BTN_ANY),
|
||||
.key_press = focus && ctx.input.events.key_press,
|
||||
.key_release = focus && ctx.input.events.key_release,
|
||||
.key_repeat = focus && ctx.input.events.key_repeat,
|
||||
.text_input = focus && (ctx.input.keyboard.text_len || ctx.input.keyboard.modkeys & KMOD_TXT),
|
||||
.mouse_hold = hover && focus && ctx.is_mouse_down(BTN_ANY),
|
||||
.text_input = focus && (ctx.input.keyboard.text_len || ctx.input.keyboard.modkeys & KMOD_TXT),
|
||||
};
|
||||
return ev;
|
||||
}
|
||||
|
153
lib/ugui.c3l/src/ugui_div.c3
Normal file
153
lib/ugui.c3l/src/ugui_div.c3
Normal 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)!;
|
||||
}
|
@ -9,34 +9,11 @@ import std::io;
|
||||
import std::ascii;
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------------- //
|
||||
// CODEPOINT //
|
||||
// ---------------------------------------------------------------------------------- //
|
||||
|
||||
// unicode code point, different type for a different hash
|
||||
alias Codepoint = uint;
|
||||
|
||||
<*
|
||||
@require off != null
|
||||
@require str.ptr != null
|
||||
*>
|
||||
fn Codepoint str_to_codepoint(char[] str, usz* off)
|
||||
{
|
||||
Codepoint cp;
|
||||
isz b = grapheme::decode_utf8(str, str.len, (uint*)&cp);
|
||||
if (b == 0 || b > str.len) {
|
||||
return 0;
|
||||
}
|
||||
*off = b;
|
||||
return cp;
|
||||
}
|
||||
|
||||
//macro uint Codepoint.hash(self) => ((uint)self).hash();
|
||||
|
||||
// ---------------------------------------------------------------------------------- //
|
||||
// FONT ATLAS //
|
||||
// ---------------------------------------------------------------------------------- //
|
||||
|
||||
/* width and height of a glyph contain the kering advance
|
||||
* (u,v)
|
||||
* +-------------*---+ -
|
||||
@ -83,7 +60,7 @@ struct Font {
|
||||
|
||||
fn void? Font.load(&font, String name, ZString path, uint height, float scale)
|
||||
{
|
||||
font.table.init(allocator::mem, capacity: FONT_CACHED);
|
||||
font.table.init(allocator::heap(), capacity: FONT_CACHED);
|
||||
font.id = name.hash();
|
||||
|
||||
font.size = height*scale;
|
||||
@ -112,7 +89,6 @@ fn void? Font.load(&font, String name, ZString path, uint height, float scale)
|
||||
ushort size = (ushort)font.size*(ushort)($$sqrt((float)FONT_CACHED));
|
||||
font.atlas.new(font.id, ATLAS_GRAYSCALE, size, size)!;
|
||||
|
||||
// FIXME: this crashes with O1 or greater, why??
|
||||
// preallocate the ASCII range
|
||||
for (char c = ' '; c < '~'; c++) {
|
||||
font.get_glyph((Codepoint)c)!;
|
||||
@ -184,16 +160,93 @@ fn void Font.free(&font)
|
||||
schrift::freefont(font.sft.font);
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------------- //
|
||||
// FONT LOAD AND QUERY //
|
||||
// ---------------------------------------------------------------------------------- //
|
||||
|
||||
fn void? Ctx.load_font(&ctx, String name, ZString path, uint height, float scale = 1.0)
|
||||
{
|
||||
return ctx.font.load(name, path, height, scale);
|
||||
}
|
||||
|
||||
<*
|
||||
@require off != null
|
||||
*>
|
||||
fn Codepoint str_to_codepoint(char[] str, usz* off)
|
||||
{
|
||||
Codepoint cp;
|
||||
isz b = grapheme::decode_utf8(str, str.len, (uint*)&cp);
|
||||
if (b == 0 || b > str.len) {
|
||||
return 0;
|
||||
}
|
||||
*off = b;
|
||||
return cp;
|
||||
}
|
||||
|
||||
fn Rect? Ctx.get_text_bounds(&ctx, String text)
|
||||
{
|
||||
Rect text_bounds;
|
||||
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
|
||||
short line_gap = (short)ctx.font.linegap;
|
||||
text_bounds.h = line_height;
|
||||
Glyph* gp;
|
||||
|
||||
// TODO: account for unicode codepoints
|
||||
short line_len;
|
||||
Codepoint cp;
|
||||
usz off, x;
|
||||
while ((cp = str_to_codepoint(text[off..], &x)) != 0) {
|
||||
off += x;
|
||||
bool n;
|
||||
if (!ascii::is_cntrl((char)cp)) {
|
||||
gp = ctx.font.get_glyph(cp)!;
|
||||
line_len += gp.adv;
|
||||
} else if (cp == '\n'){
|
||||
text_bounds.h += line_height + line_gap;
|
||||
line_len = 0;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
if (line_len > text_bounds.w) {
|
||||
text_bounds.w = line_len;
|
||||
}
|
||||
}
|
||||
|
||||
return text_bounds;
|
||||
}
|
||||
|
||||
fn Point? Ctx.get_cursor_position(&ctx, String text)
|
||||
{
|
||||
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
|
||||
short line_gap = (short)ctx.font.linegap;
|
||||
Glyph* gp;
|
||||
|
||||
// TODO: account for unicode codepoints
|
||||
Point line;
|
||||
Codepoint cp;
|
||||
usz off, x;
|
||||
while ((cp = str_to_codepoint(text[off..], &x)) != 0) {
|
||||
off += x;
|
||||
bool n;
|
||||
if (!ascii::is_cntrl((char)cp)) {
|
||||
gp = ctx.font.get_glyph(cp)!;
|
||||
line.x += gp.adv;
|
||||
} else if (cp == '\n'){
|
||||
line.y += line_height + line_gap;
|
||||
line.x = 0;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return line;
|
||||
}
|
||||
|
||||
|
||||
fn Point Ctx.center_text(&ctx, Rect text_bounds, Rect bounds)
|
||||
{
|
||||
short dw = bounds.w - text_bounds.w;
|
||||
short dh = bounds.h - text_bounds.h;
|
||||
|
||||
return {.x = dw/2, .y = dh/2};
|
||||
}
|
||||
|
||||
// TODO: check if the font is present in the context
|
||||
fn Id Ctx.get_font_id(&ctx, String label)
|
||||
{
|
||||
@ -211,266 +264,3 @@ fn Atlas*? Ctx.get_font_atlas(&ctx, String name)
|
||||
}
|
||||
|
||||
fn int Font.line_height(&font) => (int)(font.ascender - font.descender + (float)0.5);
|
||||
|
||||
// ---------------------------------------------------------------------------------- //
|
||||
// TEXT MEASUREMENT //
|
||||
// ---------------------------------------------------------------------------------- //
|
||||
|
||||
|
||||
const uint TAB_SIZE = 4;
|
||||
|
||||
struct TextSize {
|
||||
Size width, height;
|
||||
int area;
|
||||
}
|
||||
|
||||
// Measeure the size of a string.
|
||||
// width.min: as if each word is broken up by a new line
|
||||
// width.max: the width of the string left as-is
|
||||
// height.min: the height of the string left as-is
|
||||
// height.max: the height of the string with each word broken up by a new line
|
||||
fn TextSize? Ctx.measure_string(&ctx, String text)
|
||||
{
|
||||
if (text == "") return (TextSize){};
|
||||
|
||||
Font* font = &ctx.font;
|
||||
short baseline = (short)font.ascender;
|
||||
short line_height = (short)font.line_height();
|
||||
short line_gap = (short)font.linegap;
|
||||
short space_width = font.get_glyph(' ').adv!;
|
||||
short tab_width = space_width * TAB_SIZE;
|
||||
|
||||
isz off;
|
||||
usz x;
|
||||
|
||||
TextSize ts;
|
||||
|
||||
short word_width;
|
||||
short words = 1;
|
||||
Rect bounds; // unaltered text bounds;
|
||||
Point origin;
|
||||
|
||||
Codepoint cp = str_to_codepoint(text[off..], &x);
|
||||
for (; cp != 0; cp = str_to_codepoint(text[off..], &x)) {
|
||||
off += x;
|
||||
Glyph* gp = font.get_glyph(cp)!;
|
||||
|
||||
// update the text bounds
|
||||
switch {
|
||||
case cp == '\n':
|
||||
origin.x = 0;
|
||||
origin.y += line_height + line_gap;
|
||||
case cp == '\t':
|
||||
origin.x += tab_width;
|
||||
case ascii::is_cntrl((char)cp):
|
||||
break;
|
||||
default:
|
||||
Rect b = {
|
||||
.x = origin.x + gp.ox,
|
||||
.y = origin.y + gp.oy + baseline,
|
||||
.w = gp.w,
|
||||
.h = gp.h,
|
||||
};
|
||||
bounds = containing_rect(bounds, b);
|
||||
origin.x += gp.adv;
|
||||
}
|
||||
|
||||
// update the word width
|
||||
switch {
|
||||
case ascii::is_space((char)cp):
|
||||
if (word_width > ts.width.min) ts.width.min = word_width;
|
||||
word_width = 0;
|
||||
words++;
|
||||
default:
|
||||
//word_width += gp.w + gp.ox;
|
||||
if (off < text.len) {
|
||||
word_width += gp.adv;
|
||||
} else {
|
||||
word_width += gp.w + gp.ox;
|
||||
}
|
||||
}
|
||||
}
|
||||
// end of string is also end of word
|
||||
if (word_width > ts.width.min) ts.width.min = word_width;
|
||||
|
||||
ts.width.max = bounds.w;
|
||||
ts.height.min = bounds.h;
|
||||
ts.height.max = words * line_height + line_gap * (words-1);
|
||||
ts.area = bounds.w * bounds.h;
|
||||
|
||||
return ts;
|
||||
}
|
||||
|
||||
// layout a string inside a bounding box, following the given alignment (anchor).
|
||||
// returns the position of the cursor, the returned height is the line height and the width is the last
|
||||
// character's advance value
|
||||
// TODO: implement a "reflow" flag to toggle reflow if a character goes out of bounds
|
||||
// TODO: also return the total bounds of the laid out string
|
||||
fn Rect? Ctx.layout_string(&ctx, String text, Rect bounds, Anchor anchor, int z_index, Color hue, isz cursor = -1)
|
||||
{
|
||||
Font* font = &ctx.font;
|
||||
short line_height = (short)font.line_height();
|
||||
Rect cursor_rect = {.h = line_height};
|
||||
|
||||
if (bounds.w <= 0 || bounds.h <= 0) return cursor_rect;
|
||||
ctx.push_scissor(bounds, z_index)!;
|
||||
|
||||
if (text == "") {
|
||||
// when text is empty but we need to draw a cursor use a visually empty string
|
||||
if (cursor >= 0) {
|
||||
text = "\f";
|
||||
} else {
|
||||
return cursor_rect;
|
||||
}
|
||||
}
|
||||
|
||||
Id texture_id = font.id;
|
||||
short baseline = (short)font.ascender;
|
||||
short line_gap = (short)font.linegap;
|
||||
short space_width = font.get_glyph(' ').adv!;
|
||||
short tab_width = space_width * TAB_SIZE;
|
||||
|
||||
Point origin = bounds.position();
|
||||
|
||||
// measure string height inside the bounds to center it vertically
|
||||
// FIXME: this is fast but it just gives back the height in LINES, most lines have only short
|
||||
// characters which would result in a lower height
|
||||
int string_height = line_height;
|
||||
foreach (c: text) {
|
||||
string_height += (line_height + line_gap) * (int)(c == '\n');
|
||||
}
|
||||
|
||||
switch (anchor) {
|
||||
case TOP_LEFT: nextcase;
|
||||
case TOP: nextcase;
|
||||
case TOP_RIGHT:
|
||||
origin.y += 0;
|
||||
case LEFT: nextcase;
|
||||
case CENTER: nextcase;
|
||||
case RIGHT:
|
||||
origin.y += (short)(bounds.h - string_height)/2;
|
||||
case BOTTOM_LEFT: nextcase;
|
||||
case BOTTOM: nextcase;
|
||||
case BOTTOM_RIGHT:
|
||||
origin.y += (short)(bounds.h - string_height);
|
||||
}
|
||||
|
||||
// measure the line until it exits the bounds or the string ends
|
||||
usz line_start, line_end;
|
||||
do {
|
||||
int line_width;
|
||||
Point o = {.x = bounds.x, .y = bounds.y};
|
||||
|
||||
Codepoint cp;
|
||||
isz off = line_start;
|
||||
for ITER: (usz x; (cp = str_to_codepoint(text[off..], &x)) != 0; off += x) {
|
||||
Glyph* gp = font.get_glyph(cp)!;
|
||||
|
||||
switch {
|
||||
case cp == '\n':
|
||||
off += x;
|
||||
break ITER;
|
||||
case cp == '\t':
|
||||
o.x += tab_width;
|
||||
case ascii::is_cntrl((char)cp):
|
||||
break;
|
||||
default:
|
||||
Rect b = {
|
||||
.x = o.x + gp.ox,
|
||||
.y = o.y + gp.oy + baseline,
|
||||
.w = gp.w,
|
||||
.h = gp.h,
|
||||
};
|
||||
|
||||
if (b.x + b.w > bounds.x + bounds.w) {
|
||||
break ITER;
|
||||
}
|
||||
o.x += gp.adv;
|
||||
line_width += gp.adv;
|
||||
}
|
||||
}
|
||||
line_end = off;
|
||||
if (line_end == line_start) unreachable("something went wrong in measuring the line");
|
||||
|
||||
// with the line width calculate the right origin and layout the line
|
||||
origin.x = bounds.x;
|
||||
short next_line_x = bounds.x; // the x coordinate of the origin if the line_width is zero
|
||||
switch (anchor) {
|
||||
case TOP_LEFT: nextcase;
|
||||
case LEFT: nextcase;
|
||||
case BOTTOM_LEFT:
|
||||
// TODO: we didn't need to measure the line width with this alignment
|
||||
origin.x += 0;
|
||||
next_line_x += 0;
|
||||
case TOP: nextcase;
|
||||
case CENTER: nextcase;
|
||||
case BOTTOM:
|
||||
origin.x += (short)(bounds.w - line_width)/2+1;
|
||||
next_line_x += bounds.w/2;
|
||||
case TOP_RIGHT: nextcase;
|
||||
case RIGHT: nextcase;
|
||||
case BOTTOM_RIGHT:
|
||||
origin.x += (short)(bounds.w - line_width);
|
||||
next_line_x += bounds.w;
|
||||
}
|
||||
|
||||
// reset the cursor to the line
|
||||
if (line_start <= cursor && cursor <= line_end) {
|
||||
cursor_rect.x = origin.x;
|
||||
cursor_rect.y = origin.y;
|
||||
cursor_rect.w = 0;
|
||||
if (cursor && text[cursor-1] == '\n') {
|
||||
cursor_rect.x = next_line_x;
|
||||
cursor_rect.y += line_height;
|
||||
}
|
||||
}
|
||||
|
||||
Point line_origin = origin;
|
||||
|
||||
// see the fixme when measuring the height
|
||||
//ctx.push_rect({.x = origin.x,.y=origin.y,.w=(short)line_width,.h=(short)string_height}, z_index, &&(Style){.bg=0xff000042u.@to_rgba()})!;
|
||||
String line = text[line_start:line_end-line_start];
|
||||
off = 0;
|
||||
for (usz x; (cp = str_to_codepoint(line[off..], &x)) != 0 && off < line.len; off += x) {
|
||||
Glyph* gp = font.get_glyph(cp)!;
|
||||
|
||||
switch {
|
||||
case cp == '\t':
|
||||
origin.x += tab_width;
|
||||
case ascii::is_cntrl((char)cp):
|
||||
break;
|
||||
default:
|
||||
Rect b = {
|
||||
.x = origin.x + gp.ox,
|
||||
.y = origin.y + gp.oy + baseline,
|
||||
.w = gp.w,
|
||||
.h = gp.h
|
||||
};
|
||||
Rect uv = {
|
||||
.x = gp.u,
|
||||
.y = gp.v,
|
||||
.w = gp.w,
|
||||
.h = gp.h
|
||||
};
|
||||
ctx.push_sprite(b, uv, texture_id, z_index, hue)!;
|
||||
//ctx.push_rect(b, z_index, &&(Style){.bg=0x0000ff66u.@to_rgba()})!;
|
||||
|
||||
origin.x += gp.adv;
|
||||
}
|
||||
|
||||
if (line_start + off < cursor && text[cursor-1] != '\n') {
|
||||
cursor_rect.x = origin.x;
|
||||
cursor_rect.y = origin.y;
|
||||
cursor_rect.w = gp.adv;
|
||||
}
|
||||
}
|
||||
|
||||
// done with the line
|
||||
line_start = line_end;
|
||||
origin.y += line_height + line_gap;
|
||||
} while(line_end < text.len);
|
||||
|
||||
ctx.reset_scissor(z_index)!;
|
||||
|
||||
return cursor_rect;
|
||||
}
|
||||
|
@ -13,9 +13,6 @@ bitstruct InputEvents : uint {
|
||||
bool mouse_scroll : 4; // mouse scroll wheel. x or y
|
||||
bool text_input : 5;
|
||||
bool mod_key : 6;
|
||||
bool key_press : 7;
|
||||
bool key_release : 8;
|
||||
bool key_repeat : 9;
|
||||
}
|
||||
|
||||
bitstruct MouseButtons : uint {
|
||||
@ -69,13 +66,15 @@ const MouseButtons BTN_5 = {.btn_5 = true};
|
||||
|
||||
const ModKeys KEY_ANY = (ModKeys)(ModKeys.inner.max);
|
||||
|
||||
fn bool Ctx.check_key_combo(&ctx, ModKeys mod, String ...keys)
|
||||
fn bool Ctx.check_key_combo(&ctx, ModKeys mod, String keys)
|
||||
{
|
||||
bool is_mod = (bool)(ctx.input.keyboard.modkeys & mod);
|
||||
bool is_keys = true;
|
||||
String haystack = (String)ctx.get_keys();
|
||||
foreach (needle: keys) {
|
||||
is_keys = is_keys && haystack.contains(needle);
|
||||
String haystack = (String)ctx.input.keyboard.text[0..ctx.input.keyboard.text_len];
|
||||
char[2] needle;
|
||||
foreach (c: keys) {
|
||||
needle[0] = c;
|
||||
is_keys = is_keys && haystack.contains((String)needle[..]);
|
||||
}
|
||||
return is_mod && is_keys;
|
||||
}
|
||||
@ -157,21 +156,6 @@ fn void Ctx.input_mouse_wheel(&ctx, short x, short y, float scale = 1.0)
|
||||
ctx.input.events.mouse_scroll = x !=0 || y != 0;
|
||||
}
|
||||
|
||||
fn void Ctx.input_key_press(&ctx)
|
||||
{
|
||||
ctx.input.events.key_press = true;
|
||||
}
|
||||
|
||||
fn void Ctx.input_key_release(&ctx)
|
||||
{
|
||||
ctx.input.events.key_release = true;
|
||||
}
|
||||
|
||||
fn void Ctx.input_key_repeat(&ctx)
|
||||
{
|
||||
ctx.input.events.key_repeat = true;
|
||||
}
|
||||
|
||||
// append utf-8 encoded text to the context text input
|
||||
fn void Ctx.input_text_utf8(&ctx, char[] text)
|
||||
{
|
||||
@ -211,9 +195,6 @@ fn void Ctx.input_char(&ctx, char c)
|
||||
ctx.input_text_utf8(b[..]);
|
||||
}
|
||||
|
||||
fn String Ctx.get_keys(&ctx) => (String)ctx.input.keyboard.text[:ctx.input.keyboard.text_len];
|
||||
fn ModKeys Ctx.get_mod(&ctx) => ctx.input.keyboard.modkeys;
|
||||
|
||||
// Modifier keys, like control or backspace
|
||||
// TODO: make this call repetible to input modkeys one by one
|
||||
fn void Ctx.input_mod_keys(&ctx, ModKeys modkeys)
|
||||
|
@ -1,146 +1,84 @@
|
||||
module ugui;
|
||||
|
||||
import std::math;
|
||||
import std::io;
|
||||
|
||||
enum LayoutDirection {
|
||||
ROW,
|
||||
COLUMN,
|
||||
enum Layout {
|
||||
LAYOUT_ROW,
|
||||
LAYOUT_COLUMN,
|
||||
LAYOUT_FLOATING,
|
||||
LAYOUT_ABSOLUTE,
|
||||
}
|
||||
|
||||
enum Anchor {
|
||||
TOP_LEFT,
|
||||
LEFT,
|
||||
BOTTOM_LEFT,
|
||||
BOTTOM,
|
||||
BOTTOM_RIGHT,
|
||||
RIGHT,
|
||||
TOP_RIGHT,
|
||||
TOP,
|
||||
CENTER
|
||||
}
|
||||
|
||||
struct Layout {
|
||||
Size w, h; // size of the CONTENT, does not include margin, border and padding
|
||||
struct children { // the children size includes the children's margin/border/pading
|
||||
Size w, h;
|
||||
}
|
||||
TextSize text;
|
||||
ushort grow_children;
|
||||
short occupied;
|
||||
struct origin {
|
||||
short x, y;
|
||||
}
|
||||
// false: the element is laid out according to the parent
|
||||
// true: the element is laid out separate from all other children and the relative position to
|
||||
// the parent is the .origin field
|
||||
bool absolute;
|
||||
LayoutDirection dir; // the direction the children are laid out
|
||||
Anchor anchor; // how the children are positioned
|
||||
Rect content_offset; // combined effect of margin, border and padding
|
||||
}
|
||||
|
||||
// Returns the width and height of a @FIT() element based on it's wanted size (min/max)
|
||||
// and the content size, this function is used to both update the parent's children size and
|
||||
// give the dimensions of a fit element
|
||||
// TODO: test and cleanup this function
|
||||
macro Point Layout.get_dimensions(&el)
|
||||
fn void? Ctx.layout_set_row(&ctx)
|
||||
{
|
||||
Point dim;
|
||||
// if the direction is ROW then the text is placed horizontally with the children
|
||||
if (el.dir == ROW) {
|
||||
Size content_width = el.children.w + el.text.width;
|
||||
Size width = el.w.combine(content_width);
|
||||
short final_width = width.greater();
|
||||
Id parent_id = ctx.tree.get(ctx.active_div)!;
|
||||
Elem *parent = ctx.cache.search(parent_id)!;
|
||||
|
||||
short text_height;
|
||||
if (el.text.area != 0) {
|
||||
short text_width = (@exact(final_width) - el.children.w).combine(el.text.width).min;
|
||||
text_height = @exact((short)(el.text.area / text_width)).combine(el.text.height).min;
|
||||
}
|
||||
|
||||
Size content_height = el.children.h.comb_max(@exact(text_height));
|
||||
Size height = el.h.combine(content_height);
|
||||
short final_height = height.greater();
|
||||
|
||||
dim = {
|
||||
.x = final_width + el.content_offset.x + el.content_offset.w,
|
||||
.y = final_height + el.content_offset.y + el.content_offset.h,
|
||||
};
|
||||
} else {
|
||||
// if the direction is COLUMN the text and children are one on top of the other
|
||||
Size content_width = el.children.w.comb_max(el.text.width);
|
||||
Size width = el.w.combine(content_width);
|
||||
short final_width = width.greater();
|
||||
|
||||
short text_height;
|
||||
if (el.text.area != 0) {
|
||||
short text_width = @exact(final_width).combine(el.text.width).min;
|
||||
text_height = @exact((short)(el.text.area / text_width)).combine(el.text.height).min;
|
||||
}
|
||||
|
||||
Size content_height = el.children.h + @exact(text_height);
|
||||
Size height = el.h.combine(content_height);
|
||||
short final_height = height.greater();
|
||||
|
||||
dim = {
|
||||
.x = final_width + el.content_offset.x + el.content_offset.w,
|
||||
.y = final_height + el.content_offset.y + el.content_offset.h,
|
||||
};
|
||||
if (parent.type != ETYPE_DIV) {
|
||||
// what?
|
||||
return UNEXPECTED_ELEMENT?;
|
||||
}
|
||||
|
||||
// GROSS HACK FOR EXACT DIMENSIONS
|
||||
if (el.w.@is_exact()) dim.x = el.w.min + el.content_offset.x + el.content_offset.w;
|
||||
if (el.h.@is_exact()) dim.y = el.h.min + el.content_offset.y + el.content_offset.h;
|
||||
|
||||
// GROSS HACK FOR GROW DIMENSIONS
|
||||
// FIXME: does this always work?
|
||||
if (el.w.@is_grow()) dim.x = 0;
|
||||
if (el.h.@is_grow()) dim.y = 0;
|
||||
|
||||
return dim;
|
||||
parent.div.layout = LAYOUT_ROW;
|
||||
}
|
||||
|
||||
// The content space of the element
|
||||
macro Point Elem.content_space(&e)
|
||||
fn void? Ctx.layout_set_column(&ctx)
|
||||
{
|
||||
return {
|
||||
.x = e.bounds.w - e.layout.content_offset.x - e.layout.content_offset.w,
|
||||
.y = e.bounds.h - e.layout.content_offset.y - e.layout.content_offset.h,
|
||||
Id parent_id = ctx.tree.get(ctx.active_div)!;
|
||||
Elem *parent = ctx.cache.search(parent_id)!;
|
||||
|
||||
if (parent.type != ETYPE_DIV) {
|
||||
// what?
|
||||
return UNEXPECTED_ELEMENT?;
|
||||
}
|
||||
|
||||
parent.div.layout = LAYOUT_COLUMN;
|
||||
}
|
||||
|
||||
fn void? Ctx.layout_set_floating(&ctx)
|
||||
{
|
||||
Id parent_id = ctx.tree.get(ctx.active_div)!;
|
||||
Elem *parent = ctx.cache.search(parent_id)!;
|
||||
|
||||
if (parent.type != ETYPE_DIV) {
|
||||
// what?
|
||||
return UNEXPECTED_ELEMENT?;
|
||||
}
|
||||
|
||||
parent.div.layout = LAYOUT_FLOATING;
|
||||
}
|
||||
|
||||
fn void? Ctx.layout_next_row(&ctx)
|
||||
{
|
||||
Id parent_id = ctx.tree.get(ctx.active_div)!;
|
||||
Elem *parent = ctx.cache.search(parent_id)!;
|
||||
|
||||
if (parent.type != ETYPE_DIV) {
|
||||
return UNEXPECTED_ELEMENT?;
|
||||
}
|
||||
|
||||
parent.div.origin_r = {
|
||||
.x = parent.bounds.x,
|
||||
.y = parent.div.children_bounds.bottom_right().y,
|
||||
};
|
||||
}
|
||||
parent.div.origin_c = parent.div.origin_r;
|
||||
}
|
||||
|
||||
// Update the parent element's children size
|
||||
fn void update_parent_size(Elem* child, Elem* parent)
|
||||
fn void? Ctx.layout_next_column(&ctx)
|
||||
{
|
||||
Layout* cl = &child.layout;
|
||||
Layout* pl = &parent.layout;
|
||||
Id parent_id = ctx.tree.get(ctx.active_div)!;
|
||||
Elem *parent = ctx.cache.search(parent_id)!;
|
||||
|
||||
// if the element has absolute position do not update the parent
|
||||
if (cl.absolute) return;
|
||||
|
||||
Point child_size = cl.get_dimensions();
|
||||
|
||||
switch (pl.dir) {
|
||||
case ROW: // on rows grow the ch width by the child width and only grow ch height if it exceeds
|
||||
pl.children.w += @exact(child_size.x);
|
||||
pl.children.h = pl.children.h.comb_max(@exact(child_size.y));
|
||||
if (child.layout.w.@is_grow()) parent.layout.grow_children++;
|
||||
case COLUMN: // do the opposite on column
|
||||
pl.children.w = pl.children.w.comb_max(@exact(child_size.x));
|
||||
pl.children.h += @exact(child_size.y);
|
||||
if (child.layout.h.@is_grow()) parent.layout.grow_children++;
|
||||
if (parent.type != ETYPE_DIV) {
|
||||
return UNEXPECTED_ELEMENT?;
|
||||
}
|
||||
|
||||
parent.div.origin_c = {
|
||||
.x = parent.div.children_bounds.bottom_right().x,
|
||||
.y = parent.bounds.y,
|
||||
};
|
||||
parent.div.origin_r = parent.div.origin_c;
|
||||
}
|
||||
|
||||
fn void update_children_bounds(Elem* child, Elem* parent)
|
||||
{
|
||||
parent.children_bounds = containing_rect(child.bounds, parent.bounds);
|
||||
}
|
||||
|
||||
macro Rect Elem.content_bounds(&elem) => elem.bounds.pad(elem.layout.content_offset);
|
||||
|
||||
/*
|
||||
macro Rect Elem.get_view(&elem)
|
||||
{
|
||||
Rect off;
|
||||
@ -159,199 +97,100 @@ macro Point Elem.get_view_off(&elem)
|
||||
{
|
||||
return elem.get_view().sub(elem.bounds).position();
|
||||
}
|
||||
*/
|
||||
|
||||
// Assign the width and height of an element in the directions that it doesn't need to grow
|
||||
fn void resolve_dimensions(Elem* e, Elem* p)
|
||||
// position the rectangle inside the parent according to the layout
|
||||
// parent: parent div
|
||||
// rect: the requested size
|
||||
// style: apply style
|
||||
<*
|
||||
@require ctx != null
|
||||
@require parent.type == ETYPE_DIV
|
||||
*>
|
||||
fn Rect Ctx.position_element(&ctx, Elem *parent, Rect rect, Style* style)
|
||||
{
|
||||
Layout* el = &e.layout;
|
||||
Layout* pl = &p.layout;
|
||||
ElemDiv* div = &parent.div;
|
||||
|
||||
Point elem_dimensions = el.get_dimensions();
|
||||
e.bounds.w = elem_dimensions.x;
|
||||
e.bounds.h = elem_dimensions.y;
|
||||
|
||||
// if the element has absolute position do not update the parent
|
||||
if (el.absolute) return;
|
||||
Rect parent_bounds, parent_view;
|
||||
Rect child_placement, child_occupied;
|
||||
|
||||
switch (pl.dir) {
|
||||
case ROW:
|
||||
if (!el.w.@is_grow()) pl.occupied += e.bounds.w;
|
||||
case COLUMN:
|
||||
if (!el.h.@is_grow()) pl.occupied += e.bounds.h;
|
||||
}
|
||||
}
|
||||
|
||||
fn void resolve_grow_elements(Elem* e, Elem* p)
|
||||
{
|
||||
// WIDTH
|
||||
if (e.layout.w.@is_grow()) {
|
||||
if (p.layout.dir == ROW) { // grow along the axis, divide the parent size
|
||||
short slot = (short)((p.content_space().x - p.layout.occupied) / p.layout.grow_children);
|
||||
e.bounds.w = slot;
|
||||
p.layout.grow_children--;
|
||||
p.layout.occupied += slot;
|
||||
} else if (p.layout.dir == COLUMN) { // grow across the layout axis, inherit width of the parent
|
||||
e.bounds.w = p.content_space().x;
|
||||
}
|
||||
// 1. Select the right origin
|
||||
Point origin;
|
||||
switch (div.layout) {
|
||||
case LAYOUT_ROW:
|
||||
origin = div.origin_r;
|
||||
case LAYOUT_COLUMN:
|
||||
origin = div.origin_c;
|
||||
case LAYOUT_FLOATING: // none, relative to zero zero
|
||||
case LAYOUT_ABSOLUTE: // absolute position, this is a no-op, return the rect
|
||||
return rect;
|
||||
default: // error
|
||||
return {};
|
||||
}
|
||||
|
||||
// HEIGHT
|
||||
if (e.layout.h.@is_grow()) {
|
||||
if (p.layout.dir == COLUMN) { // grow along the axis, divide the parent size
|
||||
short slot = (short)((p.content_space().y - p.layout.occupied) / p.layout.grow_children);
|
||||
e.bounds.h = slot;
|
||||
p.layout.grow_children--;
|
||||
p.layout.occupied += slot;
|
||||
} else if (p.layout.dir == ROW) { // grow across the layout axis, inherit width of the parent
|
||||
e.bounds.h = p.content_space().y;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 2. Compute the parent's view
|
||||
parent_bounds = parent.bounds;
|
||||
parent_view = parent.get_view();
|
||||
|
||||
fn void resolve_placement(Elem* c, Elem* p)
|
||||
{
|
||||
Layout* pl = &p.layout;
|
||||
Layout* cl = &c.layout;
|
||||
// 3. Compute the placement and occupied area
|
||||
|
||||
Point off = {
|
||||
.x = p.bounds.x + pl.origin.x + pl.content_offset.x,
|
||||
.y = p.bounds.y + pl.origin.y + pl.content_offset.y,
|
||||
// grow rect (wanted size) when widht or height are less than zero
|
||||
bool adapt_x = rect.w <= 0;
|
||||
bool adapt_y = rect.h <= 0;
|
||||
if (adapt_x) rect.w = parent_bounds.w - parent_bounds.x - origin.x;
|
||||
if (adapt_y) rect.h = parent_bounds.h - parent_bounds.y - origin.y;
|
||||
|
||||
// offset placement and area
|
||||
child_placement = child_placement.off(origin.add(rect.position()));
|
||||
child_occupied = child_occupied.off(origin.add(rect.position()));
|
||||
|
||||
Rect margin = style.margin;
|
||||
Rect padding = style.padding;
|
||||
ushort border = style.border;
|
||||
|
||||
// padding, grows both the placement and occupied area
|
||||
child_placement = child_placement.grow(padding.position().add(padding.size()));
|
||||
child_occupied = child_occupied.grow(padding.position().add(padding.size()));
|
||||
// border, grows both the placement and occupied area
|
||||
child_placement = child_placement.grow({border*2, border*2});
|
||||
child_occupied = child_occupied.grow({border*2, border*2});
|
||||
// margin, offsets the placement and grows the occupied area
|
||||
child_placement = child_placement.off(margin.position());
|
||||
child_occupied = child_occupied.grow(margin.position().add(margin.size()));
|
||||
|
||||
// oh yeah also adjust the rect if i was to grow
|
||||
if (adapt_x) rect.w -= padding.x+padding.w + border*2 + margin.x+margin.w;
|
||||
if (adapt_y) rect.h -= padding.y+padding.h + border*2 + margin.y+margin.h;
|
||||
|
||||
// set the size
|
||||
child_placement = child_placement.grow(rect.size());
|
||||
child_occupied = child_occupied.grow(rect.size());
|
||||
|
||||
// 4. Update the parent's origin
|
||||
div.origin_r = {
|
||||
.x = child_occupied.bottom_right().x,
|
||||
.y = origin.y,
|
||||
};
|
||||
div.origin_c = {
|
||||
.x = origin.x,
|
||||
.y = child_occupied.bottom_right().y,
|
||||
};
|
||||
|
||||
// if the element has absolute position assign the origin and do not update the parent
|
||||
if (cl.absolute) {
|
||||
c.bounds.x = p.bounds.x + pl.content_offset.x + cl.origin.x;
|
||||
c.bounds.y = p.bounds.x + pl.content_offset.x + cl.origin.y;
|
||||
return;
|
||||
// 5. Update the parent's children bounds
|
||||
if (!child_occupied.bottom_right().in_rect(div.children_bounds)) {
|
||||
// right overflow
|
||||
if (child_occupied.bottom_right().x > div.children_bounds.bottom_right().x) {
|
||||
div.children_bounds.w += child_occupied.bottom_right().x - div.children_bounds.bottom_right().x;
|
||||
}
|
||||
// bottom overflow
|
||||
if (child_occupied.bottom_right().y > div.children_bounds.bottom_right().y) {
|
||||
div.children_bounds.h += child_occupied.bottom_right().y - div.children_bounds.bottom_right().y;
|
||||
}
|
||||
}
|
||||
|
||||
switch (pl.anchor) {
|
||||
case TOP_LEFT:
|
||||
c.bounds.x = off.x;
|
||||
c.bounds.y = off.y;
|
||||
case LEFT:
|
||||
c.bounds.x = off.x;
|
||||
c.bounds.y = off.y + p.content_space().y/2;
|
||||
if (pl.dir == COLUMN) {
|
||||
c.bounds.y -= pl.occupied/2;
|
||||
} else if (pl.dir == ROW) {
|
||||
c.bounds.y -= c.bounds.h/2;
|
||||
}
|
||||
case BOTTOM_LEFT:
|
||||
c.bounds.x = off.x;
|
||||
c.bounds.y = off.y + p.content_space().y ;
|
||||
if (pl.dir == COLUMN) {
|
||||
c.bounds.y -= pl.occupied;
|
||||
} else if (pl.dir == ROW) {
|
||||
c.bounds.y -= c.bounds.h;
|
||||
}
|
||||
case BOTTOM:
|
||||
c.bounds.x = off.x + p.content_space().x/2;
|
||||
c.bounds.y = off.y + p.content_space().y;
|
||||
if (pl.dir == COLUMN) {
|
||||
c.bounds.y -= pl.occupied;
|
||||
c.bounds.x -= c.bounds.w/2;
|
||||
} else if (pl.dir == ROW) {
|
||||
c.bounds.y -= c.bounds.h;
|
||||
c.bounds.x -= pl.occupied/2;
|
||||
}
|
||||
case BOTTOM_RIGHT:
|
||||
c.bounds.x = off.x + p.content_space().x;
|
||||
c.bounds.y = off.y + p.content_space().y;
|
||||
if (pl.dir == COLUMN) {
|
||||
c.bounds.y -= pl.occupied;
|
||||
c.bounds.x -= c.bounds.w;
|
||||
} else if (pl.dir == ROW) {
|
||||
c.bounds.y -= c.bounds.h;
|
||||
c.bounds.x -= pl.occupied;
|
||||
}
|
||||
case RIGHT:
|
||||
c.bounds.x = off.x + p.content_space().x;
|
||||
c.bounds.y = off.y + p.content_space().y/2;
|
||||
if (pl.dir == COLUMN) {
|
||||
c.bounds.y -= pl.occupied/2;
|
||||
c.bounds.x -= c.bounds.w;
|
||||
} else if (pl.dir == ROW) {
|
||||
c.bounds.y -= c.bounds.h/2;
|
||||
c.bounds.x -= pl.occupied;
|
||||
}
|
||||
case TOP_RIGHT:
|
||||
c.bounds.x = off.x + p.content_space().x;
|
||||
c.bounds.y = off.y;
|
||||
if (pl.dir == COLUMN) {
|
||||
c.bounds.x -= c.bounds.w;
|
||||
} else if (pl.dir == ROW) {
|
||||
c.bounds.x -= pl.occupied;
|
||||
}
|
||||
case TOP:
|
||||
c.bounds.x = off.x + p.content_space().x/2;
|
||||
c.bounds.y = off.y;
|
||||
if (pl.dir == COLUMN) {
|
||||
c.bounds.x -= c.bounds.w/2;
|
||||
} else if (pl.dir == ROW) {
|
||||
c.bounds.x -= pl.occupied/2;
|
||||
}
|
||||
case CENTER:
|
||||
c.bounds.x = off.x + p.content_space().x/2;
|
||||
c.bounds.y = off.y + p.content_space().y/2;
|
||||
if (pl.dir == COLUMN) {
|
||||
c.bounds.x -= c.bounds.w/2;
|
||||
c.bounds.y -= pl.occupied/2;
|
||||
} else if (pl.dir == ROW) {
|
||||
c.bounds.x -= pl.occupied/2;
|
||||
c.bounds.y -= c.bounds.h/2;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
switch (pl.dir) {
|
||||
case ROW:
|
||||
pl.origin.x += c.bounds.w;
|
||||
case COLUMN:
|
||||
pl.origin.y += c.bounds.h;
|
||||
default: unreachable("unknown layout direction");
|
||||
}
|
||||
}
|
||||
|
||||
fn void Ctx.layout_element_tree(&ctx)
|
||||
{
|
||||
int current;
|
||||
for (int n; (current = ctx.tree.level_order_it(0, n)) >= 0; n++) {
|
||||
Elem* p = ctx.find_elem(ctx.tree.get(current));
|
||||
//if (ctx.tree.is_root(current)!!) p = &&{};
|
||||
|
||||
int ch;
|
||||
// RESOLVE KNOWN DIMENSIONS
|
||||
for (int i; (ch = ctx.tree.children_it(current, i)) >= 0; i++) {
|
||||
Elem* c = ctx.find_elem(ctx.tree.get(ch));
|
||||
if (ctx.tree.is_root(ch)) {
|
||||
resolve_dimensions(p, &&{});
|
||||
} else {
|
||||
resolve_dimensions(c, p);
|
||||
}
|
||||
}
|
||||
|
||||
// RESOLVE GROW CHILDREN
|
||||
for (int i; (ch = ctx.tree.children_it(current, i)) >= 0; i++) {
|
||||
Elem* c = ctx.find_elem(ctx.tree.get(ch));
|
||||
if (ctx.tree.is_root(ch)) {
|
||||
resolve_grow_elements(p, &&{});
|
||||
} else {
|
||||
resolve_grow_elements(c, p);
|
||||
}
|
||||
}
|
||||
|
||||
// RESOLVE CHILDREN PLACEMENT
|
||||
for (int i; (ch = ctx.tree.children_it(current, i)) >= 0; i++) {
|
||||
Elem* c = ctx.find_elem(ctx.tree.get(ch));
|
||||
if (ctx.tree.is_root(ch)) {
|
||||
resolve_placement(p, &&{});
|
||||
update_children_bounds(p, &&{});
|
||||
} else {
|
||||
resolve_placement(c, p);
|
||||
update_children_bounds(c, p);
|
||||
}
|
||||
}
|
||||
// 99. return the placement
|
||||
if (child_placement.collides(parent_view)) {
|
||||
return child_placement.off(parent.get_view_off().neg());
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
@ -10,9 +10,6 @@ struct Rect {
|
||||
short x, y, w, h;
|
||||
}
|
||||
|
||||
// TODO: find another name
|
||||
const Rect RECT_MAX = {0, 0, short.max, short.max};
|
||||
|
||||
// return true if rect a contains b
|
||||
macro bool Rect.contains(Rect a, Rect b)
|
||||
{
|
||||
@ -36,26 +33,11 @@ macro bool Rect.collides(Rect a, Rect b)
|
||||
return !(a.x > b.x+b.w || a.x+a.w < b.x || a.y > b.y+b.h || a.y+a.h < b.y);
|
||||
}
|
||||
|
||||
// return a rect that contains both rects, a bounding box of both
|
||||
macro Rect containing_rect(Rect a, Rect b)
|
||||
{
|
||||
short min_x = (short)min(a.x, b.x);
|
||||
short min_y = (short)min(a.y, b.y);
|
||||
short max_x = (short)max(a.x + a.w, b.x + b.w);
|
||||
short max_y = (short)max(a.y + a.h, b.y + b.h);
|
||||
return {
|
||||
.x = min_x,
|
||||
.y = min_y,
|
||||
.w = (short)(max_x - min_x),
|
||||
.h = (short)(max_y - min_y)
|
||||
};
|
||||
}
|
||||
|
||||
// check for empty rect
|
||||
macro bool Rect.is_null(Rect r) => r.x == 0 && r.y == 0 && r.x == 0 && r.w == 0;
|
||||
|
||||
// returns the element-wise addition of r1 and r2
|
||||
macro Rect Rect.add(Rect r1, Rect r2) @operator_s(+)
|
||||
macro Rect Rect.add(Rect r1, Rect r2)
|
||||
{
|
||||
return {
|
||||
.x = r1.x + r2.x,
|
||||
@ -66,7 +48,7 @@ macro Rect Rect.add(Rect r1, Rect r2) @operator_s(+)
|
||||
}
|
||||
|
||||
// returns the element-wise subtraction of r1 and r2
|
||||
macro Rect Rect.sub(Rect r1, Rect r2) @operator_s(-)
|
||||
macro Rect Rect.sub(Rect r1, Rect r2)
|
||||
{
|
||||
return {
|
||||
.x = r1.x - r2.x,
|
||||
@ -77,7 +59,7 @@ macro Rect Rect.sub(Rect r1, Rect r2) @operator_s(-)
|
||||
}
|
||||
|
||||
// returns the element-wise multiplication of r1 and r2
|
||||
macro Rect Rect.mul(Rect r1, Rect r2) @operator_s(*)
|
||||
macro Rect Rect.mul(Rect r1, Rect r2)
|
||||
{
|
||||
return {
|
||||
.x = r1.x * r2.x,
|
||||
@ -154,36 +136,6 @@ macro Point Rect.bottom_right(Rect r)
|
||||
};
|
||||
}
|
||||
|
||||
macro Rect Rect.center_to(Rect a, Rect b)
|
||||
{
|
||||
return {
|
||||
.x = b.x + (b.w - a.w)/2,
|
||||
.y = b.y + (b.h - a.h)/2,
|
||||
.w = a.w,
|
||||
.h = a.h,
|
||||
};
|
||||
}
|
||||
|
||||
macro Rect Rect.pad(Rect a, Rect b)
|
||||
{
|
||||
return {
|
||||
.x = a.x + b.x,
|
||||
.y = a.y + b.y,
|
||||
.w = a.w - b.x - b.w,
|
||||
.h = a.h - b.y - b.h,
|
||||
};
|
||||
}
|
||||
|
||||
macro Rect Rect.expand(Rect a, Rect b)
|
||||
{
|
||||
return {
|
||||
.x = a.x - b.x,
|
||||
.y = a.y - b.y,
|
||||
.w = a.w + b.x + b.w,
|
||||
.h = a.h + b.y + b.h,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------------- //
|
||||
// POINT //
|
||||
@ -199,12 +151,39 @@ macro bool Point.in_rect(Point p, Rect r)
|
||||
return (p.x >= r.x && p.x <= r.x + r.w) && (p.y >= r.y && p.y <= r.y + r.h);
|
||||
}
|
||||
|
||||
macro Point Point.add(Point a, Point b) @operator_s(+) => {.x = a.x+b.x, .y = a.y+b.y};
|
||||
macro Point Point.sub(Point a, Point b) @operator_s(-) => {.x = a.x-b.x, .y = a.y-b.y};
|
||||
macro Point Point.neg(Point p) @operator_s(-) => {-p.x, -p.y};
|
||||
macro Point Point.add(Point a, Point b)
|
||||
{
|
||||
return {
|
||||
.x = a.x + b.x,
|
||||
.y = a.y + b.y,
|
||||
};
|
||||
}
|
||||
|
||||
macro Point Point.max(Point a, Point b) => {.x = max(a.x, b.x), .y = max(a.y, b.y)};
|
||||
macro Point Point.min(Point a, Point b) => {.x = min(a.x, b.x), .y = min(a.y, b.y)};
|
||||
macro Point Point.sub(Point a, Point b)
|
||||
{
|
||||
return {
|
||||
.x = a.x - b.x,
|
||||
.y = a.y - b.y,
|
||||
};
|
||||
}
|
||||
|
||||
macro Point Point.neg(Point p) => {-p.x, -p.y};
|
||||
|
||||
macro Point Point.max(Point a, Point b)
|
||||
{
|
||||
return {
|
||||
.x = max(a.x, b.x),
|
||||
.y = max(a.y, b.y),
|
||||
};
|
||||
}
|
||||
|
||||
macro Point Point.min(Point a, Point b)
|
||||
{
|
||||
return {
|
||||
.x = min(a.x, b.x),
|
||||
.y = min(a.y, b.y),
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------- //
|
||||
// COLOR //
|
||||
@ -235,29 +214,8 @@ macro Color uint.@to_rgba($u)
|
||||
};
|
||||
}
|
||||
|
||||
macro uint Color.to_uint(c) => c.r | (c.g << 8) | (c.b << 16) | (c.a << 24);
|
||||
|
||||
// ---------------------------------------------------------------------------------- //
|
||||
// SIZE //
|
||||
// ---------------------------------------------------------------------------------- //
|
||||
|
||||
struct Size {
|
||||
short min, max;
|
||||
macro uint Color.to_uint(c)
|
||||
{
|
||||
uint u = c.r | (c.g << 8) | (c.b << 16) | (c.a << 24);
|
||||
return u;
|
||||
}
|
||||
|
||||
macro Size @grow() => {.min = 0, .max = 0};
|
||||
macro Size @exact(short s) => {.min = s, .max = s};
|
||||
// PROBLEM WE ARE OVERFLOWING
|
||||
macro Size @fit(short min = 0, short max = 999) => {.min = min, .max = max};
|
||||
macro bool Size.@is_grow(s) => (s.min == 0 && s.max == 0);
|
||||
macro bool Size.@is_exact(s) => (s.min == s.max && s.min != 0);
|
||||
macro bool Size.@is_fit(s) => (s.min != s.max);
|
||||
|
||||
macro Size Size.add(a, Size b) @operator_s(+) => {.min = a.min+b.min, .max = a.max+b.max};
|
||||
macro Size Size.sub(a, Size b) @operator_s(-) => {.min = a.min-b.min, .max = a.max-b.max};
|
||||
|
||||
macro Size Size.combine(a, Size b) => {.min = max(a.min, b.min), .max = min(a.max, b.max)};
|
||||
macro Size Size.comb_max(a, Size b) => {.min = max(a.min, b.min), .max = max(a.max, b.max)};
|
||||
macro Size Size.comb_min(a, Size b) => {.min = min(a.min, b.min), .max = min(a.max, b.max)};
|
||||
|
||||
macro short Size.greater(a) => a.min > a.max ? a.min : a.max;
|
||||
|
@ -8,17 +8,18 @@ struct ElemSlider {
|
||||
Rect handle;
|
||||
}
|
||||
|
||||
|
||||
/* handle
|
||||
* +----+-----+---------------------+
|
||||
* | |#####| |
|
||||
* +----+-----+---------------------+
|
||||
*/
|
||||
macro Ctx.slider_hor(&ctx, Size w, Size h, float* value, float hpercent = 0.25, ...)
|
||||
=> ctx.slider_hor_id(@compute_id($vasplat), w, h, value, hpercent);
|
||||
macro Ctx.slider_hor(&ctx, Rect size, float* value, float hpercent = 0.25, ...)
|
||||
=> ctx.slider_hor_id(@compute_id($vasplat), size, value, hpercent);
|
||||
<*
|
||||
@require value != null
|
||||
*>
|
||||
fn ElemEvents? Ctx.slider_hor_id(&ctx, Id id, Size w, Size h, float* value, float hpercent = 0.25)
|
||||
fn ElemEvents? Ctx.slider_hor_id(&ctx, Id id, Rect size, float* value, float hpercent = 0.25)
|
||||
{
|
||||
id = ctx.gen_id(id)!;
|
||||
|
||||
@ -26,21 +27,16 @@ fn ElemEvents? Ctx.slider_hor_id(&ctx, Id id, Size w, Size h, float* value, floa
|
||||
Elem* elem = ctx.get_elem(id, ETYPE_SLIDER)!;
|
||||
Style* style = ctx.styles.get_style(@str_hash("slider"));
|
||||
|
||||
elem.layout.w = w;
|
||||
elem.layout.h = h;
|
||||
elem.layout.content_offset = style.margin + style.border + style.padding;
|
||||
update_parent_size(elem, parent);
|
||||
|
||||
Rect bg_bounds = elem.bounds.pad(style.margin);
|
||||
Rect content_bounds = elem.bounds.pad(style.margin + style.border + style.padding);
|
||||
// 2. Layout
|
||||
elem.bounds = ctx.position_element(parent, size, style);
|
||||
|
||||
// handle width
|
||||
short hw = (short)(content_bounds.w * hpercent);
|
||||
short hw = (short)(elem.bounds.w * hpercent);
|
||||
Rect handle = {
|
||||
.x = calc_slider(content_bounds.x, content_bounds.w-hw, *value),
|
||||
.y = content_bounds.y,
|
||||
.x = calc_slider(elem.bounds.x, elem.bounds.w-hw, *value),
|
||||
.y = elem.bounds.y,
|
||||
.w = hw,
|
||||
.h = content_bounds.h,
|
||||
.h = elem.bounds.h,
|
||||
};
|
||||
elem.slider.handle = handle;
|
||||
|
||||
@ -48,8 +44,8 @@ fn ElemEvents? Ctx.slider_hor_id(&ctx, Id id, Size w, Size h, float* value, floa
|
||||
elem.events = ctx.get_elem_events(elem);
|
||||
|
||||
if (ctx.elem_focus(elem) && ctx.is_mouse_down(BTN_LEFT)) {
|
||||
*value = calc_value(content_bounds.x, m.x, content_bounds.w, hw);
|
||||
elem.slider.handle.x = calc_slider(content_bounds.x, content_bounds.w-hw, *value);
|
||||
*value = calc_value(elem.bounds.x, m.x, elem.bounds.w, hw);
|
||||
elem.slider.handle.x = calc_slider(elem.bounds.x, elem.bounds.w-hw, *value);
|
||||
elem.events.update = true;
|
||||
}
|
||||
|
||||
@ -57,7 +53,7 @@ fn ElemEvents? Ctx.slider_hor_id(&ctx, Id id, Size w, Size h, float* value, floa
|
||||
Style s = *style;
|
||||
Rect padding = s.padding;
|
||||
s.padding = {};
|
||||
ctx.push_rect(bg_bounds, parent.div.z_index, &s)!;
|
||||
ctx.push_rect(elem.bounds, parent.div.z_index, &s)!;
|
||||
s.bg = s.primary;
|
||||
s.padding = padding;
|
||||
s.border = {};
|
||||
@ -79,9 +75,9 @@ fn ElemEvents? Ctx.slider_hor_id(&ctx, Id id, Size w, Size h, float* value, floa
|
||||
* | |
|
||||
* +--+
|
||||
*/
|
||||
macro Ctx.slider_ver(&ctx, Size w, Size h, float* value, float hpercent = 0.25, ...)
|
||||
=> ctx.slider_ver_id(@compute_id($vasplat), w, h, value, hpercent);
|
||||
fn ElemEvents? Ctx.slider_ver_id(&ctx, Id id, Size w, Size h, float* value, float hpercent = 0.25)
|
||||
macro Ctx.slider_ver(&ctx, Rect size, float* value, float hpercent = 0.25, ...)
|
||||
=> ctx.slider_ver_id(@compute_id($vasplat), size, value, hpercent);
|
||||
fn ElemEvents? Ctx.slider_ver_id(&ctx, Id id, Rect size, float* value, float hpercent = 0.25)
|
||||
{
|
||||
id = ctx.gen_id(id)!;
|
||||
|
||||
@ -89,21 +85,22 @@ fn ElemEvents? Ctx.slider_ver_id(&ctx, Id id, Size w, Size h, float* value, floa
|
||||
Elem *elem = ctx.get_elem(id, ETYPE_SLIDER)!;
|
||||
Style* style = ctx.styles.get_style(@str_hash("slider"));
|
||||
|
||||
elem.layout.w = w;
|
||||
elem.layout.h = h;
|
||||
elem.layout.content_offset = style.margin + style.border + style.padding;
|
||||
update_parent_size(elem, parent);
|
||||
// 1. Fill the element fields
|
||||
if (elem.flags.is_new) {
|
||||
elem.type = ETYPE_SLIDER;
|
||||
} else if (elem.type != ETYPE_SLIDER) {
|
||||
return WRONG_ELEMENT_TYPE?;
|
||||
}
|
||||
|
||||
// 2. Layout
|
||||
Rect bg_bounds = elem.bounds.pad(style.margin);
|
||||
Rect content_bounds = elem.bounds.pad(style.margin + style.border + style.padding);
|
||||
elem.bounds = ctx.position_element(parent, size, style);
|
||||
|
||||
// handle height
|
||||
short hh = (short)(content_bounds.h * hpercent);
|
||||
short hh = (short)(elem.bounds.h * hpercent);
|
||||
Rect handle = {
|
||||
.x = content_bounds.x,
|
||||
.y = calc_slider(content_bounds.y, content_bounds.h-hh, *value),
|
||||
.w = content_bounds.w,
|
||||
.x = elem.bounds.x,
|
||||
.y = calc_slider(elem.bounds.y, elem.bounds.h-hh, *value),
|
||||
.w = elem.bounds.w,
|
||||
.h = hh,
|
||||
};
|
||||
elem.slider.handle = handle;
|
||||
@ -112,8 +109,8 @@ fn ElemEvents? Ctx.slider_ver_id(&ctx, Id id, Size w, Size h, float* value, floa
|
||||
elem.events = ctx.get_elem_events(elem);
|
||||
|
||||
if (ctx.elem_focus(elem) && ctx.is_mouse_down(BTN_LEFT)) {
|
||||
*value = calc_value(content_bounds.y, m.y, content_bounds.h, hh);
|
||||
elem.slider.handle.y = calc_slider(content_bounds.y, content_bounds.h-hh, *value);
|
||||
*value = calc_value(elem.bounds.y, m.y, elem.bounds.h, hh);
|
||||
elem.slider.handle.y = calc_slider(elem.bounds.y, elem.bounds.h-hh, *value);
|
||||
elem.events.update = true;
|
||||
}
|
||||
|
||||
@ -121,7 +118,7 @@ fn ElemEvents? Ctx.slider_ver_id(&ctx, Id id, Size w, Size h, float* value, floa
|
||||
Style s = *style;
|
||||
Rect padding = s.padding;
|
||||
s.padding = {};
|
||||
ctx.push_rect(bg_bounds, parent.div.z_index, &s)!;
|
||||
ctx.push_rect(elem.bounds, parent.div.z_index, &s)!;
|
||||
s.bg = s.primary;
|
||||
s.padding = padding;
|
||||
s.border = {};
|
@ -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)!;
|
||||
}
|
||||
|
@ -7,8 +7,10 @@ import std::io;
|
||||
// global style, similar to the css box model
|
||||
struct Style { // css box model
|
||||
Rect padding;
|
||||
Rect border;
|
||||
Rect margin;
|
||||
ushort border;
|
||||
ushort radius;
|
||||
ushort size;
|
||||
|
||||
Color bg; // background color
|
||||
Color fg; // foreground color
|
||||
@ -16,17 +18,15 @@ struct Style { // css box model
|
||||
Color secondary; // secondary color
|
||||
Color accent; // accent color
|
||||
|
||||
ushort radius;
|
||||
short size;
|
||||
}
|
||||
|
||||
const Style DEFAULT_STYLE = {
|
||||
.margin = {2, 2, 2, 2},
|
||||
.border = {2, 2, 2, 2},
|
||||
.padding = {1, 1, 1, 1},
|
||||
.radius = 0,
|
||||
.padding = {},
|
||||
.border = 2,
|
||||
.radius = 12,
|
||||
.size = 16,
|
||||
|
||||
|
||||
.bg = 0x282828ffu.@to_rgba(),
|
||||
.fg = 0xfbf1c7ffu.@to_rgba(),
|
||||
.primary = 0xcc241dffu.@to_rgba(),
|
||||
@ -60,7 +60,7 @@ fn int StyleMap.import_style_string(&map, String text)
|
||||
Parser p;
|
||||
p.lex.text = text;
|
||||
int added;
|
||||
|
||||
|
||||
while (p.parse_style() == true) {
|
||||
added++;
|
||||
// set the default style correctly
|
||||
@ -79,7 +79,7 @@ fn int Ctx.import_style_from_file(&ctx, String path)
|
||||
text = mem::new_array(char, size);
|
||||
file::load_buffer(path, text)!!;
|
||||
defer mem::free(text);
|
||||
|
||||
|
||||
int added = ctx.import_style_from_string((String)text);
|
||||
return added;
|
||||
}
|
||||
@ -89,8 +89,8 @@ fn int Ctx.import_style_from_file(&ctx, String path)
|
||||
* Style can be serialized and deserialized with a subset of CSS
|
||||
* <style name> {
|
||||
* padding: left right top bottom;
|
||||
* border: left right top bottom;
|
||||
* margin: left right top bottoms;
|
||||
* border: uint;
|
||||
* radius: uint;
|
||||
* size: uint;
|
||||
* Color: #RRGGBBAA;
|
||||
@ -102,7 +102,7 @@ fn int Ctx.import_style_from_file(&ctx, String path)
|
||||
* The field "style name" will be hashed and the hash used as the id int the style map.
|
||||
* Fields may be excluded, each excluded field is set to zero.
|
||||
* The default unit is pixels, but millimeters is also available. The parser function accepts a scale
|
||||
* factor that has to be obtained with the window manager functions.
|
||||
* factor that has to be obtained with the window manager functions.
|
||||
*/
|
||||
|
||||
module ugui::css;
|
||||
@ -115,10 +115,7 @@ import std::io;
|
||||
enum TokenType {
|
||||
INVALID,
|
||||
IDENTIFIER,
|
||||
RCURLY,
|
||||
LCURLY,
|
||||
SEMICOLON,
|
||||
COLON,
|
||||
PUNCT,
|
||||
NUMBER,
|
||||
COLOR,
|
||||
EOF,
|
||||
@ -188,22 +185,16 @@ fn Token Lexer.next_token(&lex)
|
||||
// skip whitespace
|
||||
while (ascii::is_space_m(lex.peep())) {
|
||||
if (lex.advance() == 0) return {.type = EOF};
|
||||
if (lex.off >= lex.text.len) return {.type = EOF};
|
||||
if (lex.off >= lex.text.len) return {.type = EOF};
|
||||
}
|
||||
t.off = lex.off;
|
||||
|
||||
switch (true) {
|
||||
case ascii::is_punct_m(lex.peep()) && lex.peep() != '#': // punctuation
|
||||
t.type = PUNCT;
|
||||
t.text = lex.text[lex.off:1];
|
||||
if (lex.advance() == 0) { t.type = INVALID; break; }
|
||||
switch (t.text[0]) {
|
||||
case ':': t.type = COLON;
|
||||
case ';': t.type = SEMICOLON;
|
||||
case '{': t.type = LCURLY;
|
||||
case '}': t.type = RCURLY;
|
||||
default: t.type = INVALID;
|
||||
}
|
||||
|
||||
|
||||
case lex.peep() == '#': // color
|
||||
t.type = COLOR;
|
||||
if (lex.advance() == 0) { t.type = INVALID; break; }
|
||||
@ -216,7 +207,7 @@ fn Token Lexer.next_token(&lex)
|
||||
t.type = INVALID;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
char[10] hex_str = (char[])"0x";
|
||||
hex_str[2..] = lex.text[hex_start..lex.off-1];
|
||||
uint? color_hex = ((String)hex_str[..]).to_uint();
|
||||
@ -225,21 +216,21 @@ fn Token Lexer.next_token(&lex)
|
||||
break;
|
||||
}
|
||||
t.color = color_hex.to_rgba();
|
||||
|
||||
|
||||
case ascii::is_alpha_m(lex.peep()): // identifier
|
||||
t.type = IDENTIFIER;
|
||||
while (ascii::is_alnum_m(lex.peep()) || lex.peep() == '-' || lex.peep() == '_') {
|
||||
if (lex.advance() == 0) { t.type = INVALID; break; }
|
||||
}
|
||||
t.text = lex.text[t.off..lex.off-1];
|
||||
|
||||
|
||||
case ascii::is_digit_m(lex.peep()): // number
|
||||
t.type = NUMBER;
|
||||
t.unit = PIXELS;
|
||||
// find the end of the number
|
||||
usz end;
|
||||
while (ascii::is_alnum_m(lex.peep()) || lex.peep() == '+' || lex.peep() == '-' || lex.peep() == '.') {
|
||||
if (lex.advance() == 0) { t.type = INVALID; break; }
|
||||
if (lex.advance() == 0) { t.type = INVALID; break; }
|
||||
}
|
||||
end = lex.off;
|
||||
if (end - t.off > 2) {
|
||||
@ -261,7 +252,7 @@ fn Token Lexer.next_token(&lex)
|
||||
t.value = value;
|
||||
t.text = lex.text[t.off..lex.off-1];
|
||||
}
|
||||
|
||||
|
||||
if (t.type == INVALID) {
|
||||
io::eprintfn("CSS Lexing ERROR at %d:%d: '%s' is not a valid token", t.line, t.col, lex.text[t.off..lex.off]);
|
||||
}
|
||||
@ -285,10 +276,21 @@ struct Parser {
|
||||
float mm_to_px;
|
||||
}
|
||||
|
||||
macro bool Parser.expect_text(&p, Token* t, TokenType type, String text)
|
||||
{
|
||||
*t = p.lex.next_token();
|
||||
if (t.type == type && t.text == text) {
|
||||
return true;
|
||||
}
|
||||
io::eprintfn("CSS parsing error at %d:%d: expected type:%s text:'%s' but got type:%s text:'%s'",
|
||||
t.line, t.col, type, text, t.type, t.text);
|
||||
return false;
|
||||
}
|
||||
|
||||
macro bool Parser.expect(&p, Token* t, TokenType type)
|
||||
{
|
||||
*t = p.lex.next_token();
|
||||
if (t.type == type) return true;
|
||||
if (t.type == type) return true;
|
||||
io::eprintfn("CSS parsing error at %d:%d: expected %s but got %s", t.line, t.col, type, t.type);
|
||||
return false;
|
||||
}
|
||||
@ -298,22 +300,22 @@ fn bool Parser.parse_style(&p)
|
||||
Token t;
|
||||
p.style = {};
|
||||
p.style_id = 0;
|
||||
|
||||
|
||||
// style name
|
||||
if (p.expect(&t, IDENTIFIER) == false) return false;
|
||||
p.style_id = t.text.hash();
|
||||
|
||||
// style body
|
||||
if (p.expect(&t, LCURLY) == false) return false;
|
||||
|
||||
if (p.expect_text(&t, PUNCT, "{") == false) return false;
|
||||
|
||||
while (true) {
|
||||
if (p.parse_property() == false) return false;
|
||||
t = p.lex.peep_token();
|
||||
if (t.type != IDENTIFIER) break;
|
||||
}
|
||||
|
||||
if (p.expect(&t, RCURLY) == false) return false;
|
||||
|
||||
|
||||
if (p.expect_text(&t, PUNCT, "}") == false) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -321,7 +323,7 @@ fn bool Parser.parse_property(&p)
|
||||
{
|
||||
Token t, prop;
|
||||
if (p.expect(&prop, IDENTIFIER) == false) return false;
|
||||
if (p.expect(&t, COLON) == false) return false;
|
||||
if (p.expect_text(&t, PUNCT, ":") == false) return false;
|
||||
|
||||
switch (prop.text) {
|
||||
case "padding":
|
||||
@ -329,11 +331,6 @@ fn bool Parser.parse_property(&p)
|
||||
if (p.parse_size(&padding) == false) return false;
|
||||
p.style.padding = padding;
|
||||
|
||||
case "border":
|
||||
Rect border;
|
||||
if (p.parse_size(&border) == false) return false;
|
||||
p.style.border = border;
|
||||
|
||||
case "margin":
|
||||
Rect margin;
|
||||
if (p.parse_size(&margin) == false) return false;
|
||||
@ -364,30 +361,39 @@ fn bool Parser.parse_property(&p)
|
||||
if (p.parse_color(&accent) == false) return false;
|
||||
p.style.accent = accent;
|
||||
|
||||
case "border":
|
||||
short border;
|
||||
if (p.parse_number(&border) == false) return false;
|
||||
if (border < 0) {
|
||||
io::eprintfn("CSS parsing error at %d:%d: 'border' must be a positive number, got %d", t.line, t.col, border);
|
||||
return false;
|
||||
}
|
||||
p.style.border = (ushort)border;
|
||||
|
||||
case "radius":
|
||||
short r;
|
||||
if (p.parse_number(&r) == false) return false;
|
||||
if (r < 0) {
|
||||
io::eprintfn("CSS parsing error at %d:%d: 'radius' must be a positive number, got %d", t.line, t.col, r);
|
||||
short radius;
|
||||
if (p.parse_number(&radius) == false) return false;
|
||||
if (radius < 0) {
|
||||
io::eprintfn("CSS parsing error at %d:%d: 'radius' must be a positive number, got %d", t.line, t.col, radius);
|
||||
return false;
|
||||
}
|
||||
p.style.radius = (ushort)r;
|
||||
|
||||
p.style.radius = (ushort)radius;
|
||||
|
||||
case "size":
|
||||
short s;
|
||||
if (p.parse_number(&s) == false) return false;
|
||||
if (s < 0) {
|
||||
io::eprintfn("CSS parsing error at %d:%d: 'size' must be a positive number, got %d", t.line, t.col, s);
|
||||
short size;
|
||||
if (p.parse_number(&size) == false) return false;
|
||||
if (size < 0) {
|
||||
io::eprintfn("CSS parsing error at %d:%d: 'size' must be a positive number, got %d", t.line, t.col, size);
|
||||
return false;
|
||||
}
|
||||
p.style.size = (ushort)s;
|
||||
p.style.size = (ushort)size;
|
||||
|
||||
|
||||
default:
|
||||
io::eprintfn("CSS parsing error at %d:%d: '%s' is not a valid property", prop.line, prop.col, prop.text);
|
||||
return false;
|
||||
}
|
||||
if (p.expect(&t, SEMICOLON) == false) return false;
|
||||
if (p.expect_text(&t, PUNCT, ";") == false) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -412,9 +418,9 @@ fn bool Parser.parse_size(&p, Rect* r)
|
||||
{
|
||||
short x;
|
||||
Token t;
|
||||
|
||||
|
||||
if (p.parse_number(&x) == false) return false;
|
||||
|
||||
|
||||
t = p.lex.peep_token();
|
||||
if (t.type == NUMBER) {
|
||||
// we got another number so we expect three more
|
||||
@ -426,7 +432,7 @@ fn bool Parser.parse_size(&p, Rect* r)
|
||||
if (p.parse_number(&x) == false) return false;
|
||||
r.h = x;
|
||||
return true;
|
||||
} else if (t.type == SEMICOLON) {
|
||||
} else if (t.type == PUNCT && t.text == ";") {
|
||||
// just one number, all dimensions are the same
|
||||
r.x = r.y = r.w = r.h = x;
|
||||
return true;
|
||||
|
90
lib/ugui.c3l/src/ugui_text.c3
Normal file
90
lib/ugui.c3l/src/ugui_text.c3
Normal 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;
|
||||
}
|
@ -1,125 +0,0 @@
|
||||
module ugui;
|
||||
|
||||
import grapheme;
|
||||
import std::ascii;
|
||||
|
||||
struct TextEdit {
|
||||
char[] buffer;
|
||||
usz chars;
|
||||
usz cursor;
|
||||
}
|
||||
|
||||
fn String TextEdit.to_string(&te) => (String)te.buffer[:te.chars];
|
||||
fn String TextEdit.until_cursor(&te) => (String)te.buffer[:te.cursor];
|
||||
fn String TextEdit.from_cursor(&te) => (String)te.buffer[te.cursor..];
|
||||
|
||||
// implement text editing operations on the buffer
|
||||
// returns true if the buffer is full
|
||||
fn bool Ctx.text_edit(&ctx, TextEdit* te)
|
||||
{
|
||||
String in = ctx.get_keys();
|
||||
ModKeys mod = ctx.get_mod();
|
||||
usz free = te.buffer.len - te.chars;
|
||||
usz after = te.chars - te.cursor;
|
||||
|
||||
// append text input to the buffer
|
||||
if (in.len <= free) {
|
||||
// make space
|
||||
te.buffer[te.cursor+in.len : after] = te.buffer[te.cursor : after];
|
||||
// insert characters
|
||||
te.buffer[te.cursor : in.len] = in[..];
|
||||
// increment characters and cursor
|
||||
te.chars += in.len;
|
||||
te.cursor += in.len;
|
||||
free -= in.len;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
// handle modkeys
|
||||
if (te.chars) {
|
||||
// handle backspace and delete
|
||||
if (mod.bkspc) {
|
||||
if (te.cursor > 0) {
|
||||
// TODO: only delete until punctuation
|
||||
usz how_many = mod & KMOD_CTRL ? te.until_cursor().prev_word_off() : te.until_cursor().prev_char_off();
|
||||
te.buffer[te.cursor-how_many : after] = te.buffer[te.cursor : after];
|
||||
te.cursor -= how_many;
|
||||
te.chars -= how_many;
|
||||
free += how_many;
|
||||
}
|
||||
}
|
||||
if (mod.del) {
|
||||
if (after > 0 && te.cursor < te.chars) {
|
||||
usz how_many = mod & KMOD_CTRL ? te.from_cursor().next_word_off() : te.from_cursor().next_char_off();
|
||||
te.buffer[te.cursor : after] = te.buffer[te.cursor+how_many : after];
|
||||
te.chars -= how_many;
|
||||
after -= how_many;
|
||||
free += how_many;
|
||||
}
|
||||
}
|
||||
|
||||
// handle arrow keys
|
||||
if (mod.left) {
|
||||
if (te.cursor > 0) {
|
||||
usz how_many = mod & KMOD_CTRL ? te.until_cursor().prev_word_off() : te.until_cursor().prev_char_off();
|
||||
te.cursor -= how_many;
|
||||
after += how_many;
|
||||
}
|
||||
}
|
||||
if (mod.right) {
|
||||
if (after > 0) {
|
||||
usz how_many = mod & KMOD_CTRL ? te.from_cursor().next_word_off() : te.from_cursor().next_char_off();
|
||||
te.cursor += how_many;
|
||||
after -= how_many;
|
||||
}
|
||||
}
|
||||
if (mod.up) {
|
||||
// back up to previous line
|
||||
if (te.cursor > 0) {
|
||||
usz curr_line_start = te.until_cursor().rindex_of_char('\n') ?? 0;
|
||||
usz prev_line_start = curr_line_start ? te.until_cursor()[..curr_line_start-1].rindex_of_char('\n') ?? 0 : 0;
|
||||
usz curr_line_off = te.cursor - curr_line_start;
|
||||
usz prev_line_len = curr_line_start - prev_line_start;
|
||||
te.cursor = prev_line_start + min(curr_line_off-1, prev_line_len);
|
||||
after = te.chars - te.cursor;
|
||||
}
|
||||
}
|
||||
if (mod.down) {
|
||||
// down to the next line
|
||||
if (after > 0) {
|
||||
usz curr_line_start = te.until_cursor().rindex_of_char('\n') ?? 0;
|
||||
usz curr_line_off = te.cursor - curr_line_start;
|
||||
usz next_line_start = te.from_cursor().index_of_char('\n') + te.cursor + 1 ?? te.chars;
|
||||
usz next_line_end = ((String)te.buffer[next_line_start..]).index_of_char('\n') + next_line_start ?? te.chars;
|
||||
usz next_line_len = next_line_end - next_line_start;
|
||||
te.cursor = next_line_start + min(curr_line_off, next_line_len);
|
||||
after = te.chars - te.cursor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return free == 0;
|
||||
}
|
||||
|
||||
macro isz char[].next_char_off(b) => grapheme::next_character_break_utf8(b.ptr, b.len);
|
||||
macro isz char[].next_word_off(b) => grapheme::next_word_break_utf8(b.ptr, b.len);
|
||||
|
||||
fn isz char[].prev_char_off(b)
|
||||
{
|
||||
foreach_r (off, c: b) {
|
||||
if (c & 0xC0 == 0x80) continue;
|
||||
return b.len - off;
|
||||
}
|
||||
return b.len;
|
||||
}
|
||||
|
||||
fn isz char[].prev_word_off(b)
|
||||
{
|
||||
for (isz off = b.len-1; off > 0;) {
|
||||
isz c_off = b[..off].prev_char_off();
|
||||
off -= c_off;
|
||||
if (ascii::is_punct(b[off]) || ascii::is_space(b[off])) return b.len - off - 1;
|
||||
}
|
||||
return b.len;
|
||||
}
|
@ -4,51 +4,39 @@ faultdef CANNOT_SHRINK, INVALID_REFERENCE, TREE_FULL, REFERENCE_NOT_PRESENT, INV
|
||||
module vtree{ElemType};
|
||||
|
||||
import std::core::mem;
|
||||
import std::core::mem::allocator;
|
||||
import std::io;
|
||||
|
||||
struct VTree {
|
||||
Allocator allocator;
|
||||
usz elements;
|
||||
ElemType[] vector; // vector of element ids
|
||||
isz[] refs, ordered_refs;
|
||||
}
|
||||
|
||||
|
||||
macro VTree.ref_is_valid(&tree, isz ref) => (ref >= 0 && ref < tree.refs.len);
|
||||
macro VTree.ref_is_present(&tree, isz ref) => tree.refs[ref] >= 0;
|
||||
macro VTree.size(&tree) => tree.refs.len;
|
||||
macro VTree.ref_is_valid(&tree, isz ref) { return (ref >= 0 && ref < tree.refs.len); }
|
||||
macro VTree.ref_is_present(&tree, isz ref) { return tree.refs[ref] >= 0; }
|
||||
macro VTree.size(&tree) { return tree.refs.len; }
|
||||
|
||||
// macro to zero an element
|
||||
// macro to zero an elemen
|
||||
macro @zero()
|
||||
{
|
||||
$if @assignable_to(0, ElemType):
|
||||
$if $assignable(0, ElemType):
|
||||
return 0;
|
||||
$endif
|
||||
|
||||
$if @assignable_to(null, ElemType):
|
||||
return null;
|
||||
$endif
|
||||
|
||||
$if @assignable_to({}, ElemType):
|
||||
$else
|
||||
return {};
|
||||
$endif
|
||||
|
||||
//$assert true == false : ElemType.nameof +++ " is not assignable to zero or equivalent";
|
||||
}
|
||||
|
||||
fn void? VTree.init(&tree, usz size, Allocator allocator)
|
||||
fn void? VTree.init(&tree, usz size)
|
||||
{
|
||||
tree.allocator = allocator;
|
||||
tree.vector = mem::new_array(ElemType, size);
|
||||
defer catch { (void)mem::free(tree.vector); }
|
||||
|
||||
tree.vector = allocator::new_array(tree.allocator, ElemType, size);
|
||||
defer catch { (void)allocator::free(tree.allocator, tree.vector); }
|
||||
tree.refs = mem::new_array(isz, size);
|
||||
defer catch { (void)mem::free(tree.refs); }
|
||||
|
||||
tree.refs = allocator::new_array(tree.allocator, isz, size);
|
||||
defer catch { (void)allocator::free(tree.allocator, tree.refs); }
|
||||
|
||||
tree.ordered_refs = allocator::new_array(tree.allocator, isz, size);
|
||||
defer catch { (void)allocator::free(tree.allocator, tree.ordered_refs); }
|
||||
tree.ordered_refs = mem::new_array(isz, size);
|
||||
defer catch { (void)mem::free(tree.ordered_refs); }
|
||||
|
||||
// set all refs to -1, meaning invalid (free) element
|
||||
tree.refs[..] = -1;
|
||||
@ -58,9 +46,9 @@ fn void? VTree.init(&tree, usz size, Allocator allocator)
|
||||
|
||||
fn void VTree.free(&tree)
|
||||
{
|
||||
(void)allocator::free(tree.allocator, tree.vector);
|
||||
(void)allocator::free(tree.allocator, tree.refs);
|
||||
(void)allocator::free(tree.allocator, tree.ordered_refs);
|
||||
(void)mem::free(tree.vector);
|
||||
(void)mem::free(tree.refs);
|
||||
(void)mem::free(tree.ordered_refs);
|
||||
}
|
||||
|
||||
fn void VTree.pack(&tree)
|
||||
@ -114,14 +102,14 @@ fn void? VTree.resize(&tree, usz newsize)
|
||||
|
||||
usz old_size = tree.size();
|
||||
|
||||
tree.vector = ((ElemType*)allocator::realloc(tree.allocator, tree.vector, newsize*ElemType.sizeof))[:newsize];
|
||||
defer catch { (void)allocator::free(tree.allocator, tree.vector); }
|
||||
tree.vector = ((ElemType*)mem::realloc(tree.vector, newsize*ElemType.sizeof))[:newsize];
|
||||
defer catch { (void)mem::free(tree.vector); }
|
||||
|
||||
tree.refs = ((isz*)allocator::realloc(tree.allocator, tree.refs, newsize*isz.sizeof))[:newsize];
|
||||
defer catch { (void)allocator::free(tree.allocator, tree.refs); }
|
||||
tree.refs = ((isz*)mem::realloc(tree.refs, newsize*isz.sizeof))[:newsize];
|
||||
defer catch { (void)mem::free(tree.refs); }
|
||||
|
||||
tree.ordered_refs = ((isz*)allocator::realloc(tree.allocator, tree.ordered_refs, newsize*isz.sizeof))[:newsize];
|
||||
defer catch { (void)allocator::free(tree.allocator, tree.ordered_refs); }
|
||||
tree.ordered_refs = ((isz*)mem::realloc(tree.ordered_refs, newsize*isz.sizeof))[:newsize];
|
||||
defer catch { (void)mem::free(tree.ordered_refs); }
|
||||
|
||||
if (newsize > tree.size()) {
|
||||
tree.vector[old_size..newsize-1] = @zero();
|
||||
@ -225,8 +213,6 @@ fn usz? VTree.subtree_size(&tree, isz ref)
|
||||
return count;
|
||||
}
|
||||
|
||||
fn bool? VTree.is_root(&tree, isz node) => node == tree.parentof(node)!;
|
||||
|
||||
// iterate through the first level children, use a cursor like strtok_r
|
||||
fn isz? VTree.children_it(&tree, isz parent, isz *cursor)
|
||||
{
|
||||
|
@ -1,225 +0,0 @@
|
||||
module ugui;
|
||||
|
||||
import std::io;
|
||||
|
||||
// button element
|
||||
struct ElemButton {
|
||||
int filler;
|
||||
}
|
||||
|
||||
|
||||
macro Ctx.button(&ctx, String label = "", String icon = "", ...)
|
||||
=> ctx.button_id(@compute_id($vasplat), label, icon);
|
||||
fn ElemEvents? Ctx.button_id(&ctx, Id id, String label, String icon)
|
||||
{
|
||||
id = ctx.gen_id(id)!;
|
||||
Elem *parent = ctx.get_parent()!;
|
||||
Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
|
||||
Style* style = ctx.styles.get_style(@str_hash("button"));
|
||||
|
||||
Sprite* sprite = icon != "" ? ctx.sprite_atlas.get(icon)! : &&(Sprite){};
|
||||
Rect icon_size = sprite.rect();
|
||||
|
||||
ushort min_size = style.size;
|
||||
ushort half_lh = (ushort)(ctx.font.line_height() / 2);
|
||||
ushort inner_pad = label != "" && icon != "" ? half_lh : 0;
|
||||
/*
|
||||
* +--------------------------------------+
|
||||
* | +--------+ |
|
||||
* | | | +-----------------+ |
|
||||
* | | icon | | label | |
|
||||
* | | | +-----------------+ |
|
||||
* | +--------+<->| |
|
||||
* +-------------^------------------------+
|
||||
* |inner_pad
|
||||
*/
|
||||
Point content_size = {
|
||||
.x = icon_size.w + inner_pad, // text sizing is handled differently
|
||||
.y = icon_size.h + inner_pad,
|
||||
};
|
||||
elem.layout.w = @fit(min_size);
|
||||
elem.layout.h = @fit(min_size);
|
||||
elem.layout.children.w = @exact(content_size.x);
|
||||
elem.layout.children.h = @exact(content_size.y);
|
||||
elem.layout.text = ctx.measure_string(label)!;
|
||||
elem.layout.content_offset = style.margin + style.border + style.padding;
|
||||
|
||||
update_parent_size(elem, parent);
|
||||
|
||||
elem.events = ctx.get_elem_events(elem);
|
||||
Rect content_bounds = elem.content_bounds();
|
||||
|
||||
Rect icon_bounds = {
|
||||
.x = content_bounds.x,
|
||||
.y = content_bounds.y,
|
||||
.w = icon_size.w,
|
||||
.h = icon_size.h
|
||||
};
|
||||
icon_bounds = icon_size.center_to(icon_bounds);
|
||||
|
||||
Rect text_bounds = {
|
||||
.x = content_bounds.x + icon_bounds.w + inner_pad,
|
||||
.y = content_bounds.y,
|
||||
.w = content_bounds.w - icon_bounds.w - inner_pad,
|
||||
.h = content_bounds.h,
|
||||
};
|
||||
//text_bounds = text_size.center_to(text_bounds);
|
||||
|
||||
bool is_active = ctx.elem_focus(elem) || elem.events.mouse_hover;
|
||||
Style s = *style;
|
||||
if (is_active) {
|
||||
s.secondary = s.primary;
|
||||
s.bg = s.accent;
|
||||
}
|
||||
|
||||
ctx.push_rect(elem.bounds.pad(style.margin), parent.div.z_index, &s)!;
|
||||
if (icon != "") {
|
||||
ctx.push_sprite(icon_bounds, sprite.uv(), ctx.sprite_atlas.id, parent.div.z_index, type: sprite.type)!;
|
||||
}
|
||||
if (label != "") {
|
||||
ctx.layout_string(label, text_bounds, CENTER, parent.div.z_index, style.fg)!;
|
||||
}
|
||||
return elem.events;
|
||||
}
|
||||
|
||||
|
||||
macro Ctx.checkbox(&ctx, String desc, bool* active, String tick_sprite = "", ...)
|
||||
=> ctx.checkbox_id(@compute_id($vasplat), desc, active, tick_sprite);
|
||||
fn void? Ctx.checkbox_id(&ctx, Id id, String description, bool* active, String tick_sprite)
|
||||
{
|
||||
id = ctx.gen_id(id)!;
|
||||
Elem *parent = ctx.get_parent()!;
|
||||
Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
|
||||
Style* style = ctx.styles.get_style(@str_hash("checkbox"));
|
||||
|
||||
short inner_pad = description != "" ? style.size/2 : 0;
|
||||
/*
|
||||
* |< >| style.size/2
|
||||
* +---------------------|---|-----------+
|
||||
* | | .-----. ---|--
|
||||
* | +-----------------+ ' ### ' | ^
|
||||
* | | description | | ##### | | style.size
|
||||
* | +-----------------+ . ### . | v
|
||||
* | '-----' ---|--
|
||||
* +-------------------------|-------|---+
|
||||
* |<----->| style.size
|
||||
*/
|
||||
|
||||
elem.layout.w = @fit(style.size);
|
||||
elem.layout.h = @fit(style.size);
|
||||
elem.layout.children.w = @exact(style.size + inner_pad);
|
||||
elem.layout.children.h = @exact(style.size);
|
||||
elem.layout.text = ctx.measure_string(description)!;
|
||||
elem.layout.content_offset = style.margin + style.border + style.padding;
|
||||
|
||||
update_parent_size(elem, parent);
|
||||
|
||||
elem.events = ctx.get_elem_events(elem);
|
||||
if (elem.events.mouse_hover && elem.events.mouse_release) *active = !(*active);
|
||||
|
||||
|
||||
Rect content_bounds = elem.bounds.pad(elem.layout.content_offset);
|
||||
Rect text_bounds = {
|
||||
.x = content_bounds.x,
|
||||
.y = content_bounds.y,
|
||||
.w = content_bounds.w - inner_pad - style.size,
|
||||
.h = content_bounds.h
|
||||
};
|
||||
Rect check_bounds = {
|
||||
.x = content_bounds.x + text_bounds.w + inner_pad,
|
||||
.y = content_bounds.y + (content_bounds.h - style.size)/2,
|
||||
.w = style.size,
|
||||
.h = style.size,
|
||||
};
|
||||
|
||||
Style s;
|
||||
s.bg = style.bg;
|
||||
s.secondary = style.secondary;
|
||||
s.border = style.border;
|
||||
s.radius = style.radius;
|
||||
|
||||
ctx.layout_string(description, text_bounds, CENTER, parent.div.z_index, style.fg)!;
|
||||
if (tick_sprite != "") {
|
||||
ctx.push_rect(check_bounds, parent.div.z_index, &s)!;
|
||||
if (*active) {
|
||||
Sprite* sprite = ctx.sprite_atlas.get(tick_sprite)!;
|
||||
Id tex_id = ctx.sprite_atlas.id;
|
||||
ctx.push_sprite(sprite.rect().center_to(check_bounds), sprite.uv(), tex_id, parent.div.z_index, type: sprite.type)!;
|
||||
}
|
||||
} else {
|
||||
if (*active) {
|
||||
s.bg = style.primary;
|
||||
ctx.push_rect(check_bounds, parent.div.z_index, &s)!;
|
||||
} else {
|
||||
ctx.push_rect(check_bounds, parent.div.z_index, &s)!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro Ctx.toggle(&ctx, String desc, bool* active)
|
||||
=> ctx.toggle_id(@compute_id($vasplat), desc, active);
|
||||
fn void? Ctx.toggle_id(&ctx, Id id, String description, bool* active)
|
||||
{
|
||||
id = ctx.gen_id(id)!;
|
||||
|
||||
Elem *parent = ctx.get_parent()!;
|
||||
Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
|
||||
Style* style = ctx.styles.get_style(@str_hash("toggle"));
|
||||
|
||||
short inner_pad = description != "" ? style.size/2 : 0;
|
||||
/*
|
||||
* |< >| style.size/2
|
||||
* +---------------------|---|-----------------+
|
||||
* | | .-----------. ---|--
|
||||
* | +-----------------+ ' ##### ' | ^
|
||||
* | | description | | ##### | | style.size
|
||||
* | +-----------------+ . ##### . | v
|
||||
* | '-----------' ---|--
|
||||
* +-------------------------|-------------|---+
|
||||
* |<----->| style.size*2
|
||||
*/
|
||||
|
||||
elem.layout.w = @fit(style.size*2);
|
||||
elem.layout.h = @fit(style.size);
|
||||
elem.layout.children.w = @exact(style.size*2 + inner_pad);
|
||||
elem.layout.children.h = @exact(style.size);
|
||||
elem.layout.text = ctx.measure_string(description)!;
|
||||
elem.layout.content_offset = style.margin + style.border + style.padding;
|
||||
|
||||
update_parent_size(elem, parent);
|
||||
|
||||
elem.events = ctx.get_elem_events(elem);
|
||||
if (elem.events.mouse_hover && elem.events.mouse_release) *active = !(*active);
|
||||
|
||||
Rect content_bounds = elem.bounds.pad(elem.layout.content_offset);
|
||||
Rect text_bounds = {
|
||||
.x = content_bounds.x,
|
||||
.y = content_bounds.y,
|
||||
.w = content_bounds.w - inner_pad - style.size*2,
|
||||
.h = content_bounds.h
|
||||
};
|
||||
Rect toggle_bounds = {
|
||||
.x = content_bounds.x + text_bounds.w + inner_pad,
|
||||
.y = content_bounds.y + (content_bounds.h - style.size)/2,
|
||||
.w = style.size*2,
|
||||
.h = style.size,
|
||||
};
|
||||
Rect toggle = {
|
||||
.x = toggle_bounds.x + (*active ? style.size : 0),
|
||||
.y = toggle_bounds.y,
|
||||
.w = style.size,
|
||||
.h = style.size
|
||||
};
|
||||
|
||||
Style s;
|
||||
s.bg = style.bg;
|
||||
s.secondary = style.secondary;
|
||||
s.border = style.border;
|
||||
s.radius = style.radius;
|
||||
ctx.layout_string(description, text_bounds, CENTER, parent.div.z_index, style.fg)!;
|
||||
ctx.push_rect(toggle_bounds, parent.div.z_index, &s)!;
|
||||
s.bg = style.primary;
|
||||
s.border = {};
|
||||
ctx.push_rect(toggle.pad(style.border), parent.div.z_index, &s)!;
|
||||
|
||||
}
|
@ -1,146 +0,0 @@
|
||||
module ugui;
|
||||
|
||||
import std::io;
|
||||
import std::math;
|
||||
|
||||
// div element
|
||||
struct ElemDiv {
|
||||
struct scroll_x {
|
||||
bool enabled;
|
||||
bool on;
|
||||
float value;
|
||||
}
|
||||
struct scroll_y {
|
||||
bool enabled;
|
||||
bool on;
|
||||
float value;
|
||||
}
|
||||
ushort scroll_size;
|
||||
int z_index;
|
||||
}
|
||||
|
||||
|
||||
// useful macro to start and end a div, capturing the trailing block
|
||||
macro Ctx.@div(&ctx,
|
||||
Size width = @grow, Size height = @grow,
|
||||
LayoutDirection dir = ROW,
|
||||
Anchor anchor = TOP_LEFT,
|
||||
bool absolute = false, Point off = {},
|
||||
bool scroll_x = false, bool scroll_y = false,
|
||||
...;
|
||||
@body()
|
||||
)
|
||||
{
|
||||
ctx.div_begin(width, height, dir, anchor, absolute, off, scroll_x, scroll_y, $vasplat)!;
|
||||
@body();
|
||||
return ctx.div_end()!;
|
||||
}
|
||||
|
||||
macro Ctx.div_begin(&ctx,
|
||||
Size width = @grow(), Size height = @grow(),
|
||||
LayoutDirection dir = ROW,
|
||||
Anchor anchor = TOP_LEFT,
|
||||
bool absolute = false, Point off = {},
|
||||
bool scroll_x = false, bool scroll_y = false,
|
||||
...
|
||||
)
|
||||
{
|
||||
return ctx.div_begin_id(@compute_id($vasplat), width, height, dir, anchor, absolute, off, scroll_x, scroll_y);
|
||||
}
|
||||
|
||||
fn void? Ctx.div_begin_id(&ctx,
|
||||
Id id,
|
||||
Size width, Size height,
|
||||
LayoutDirection dir,
|
||||
Anchor anchor,
|
||||
bool absolute, Point off,
|
||||
bool scroll_x, bool scroll_y
|
||||
)
|
||||
{
|
||||
id = ctx.gen_id(id)!;
|
||||
|
||||
Elem* elem = ctx.get_elem(id, ETYPE_DIV)!;
|
||||
Elem* parent = ctx.get_parent()!;
|
||||
ctx.active_div = elem.tree_idx;
|
||||
|
||||
Style* style = ctx.styles.get_style(@str_hash("default"));
|
||||
Style* slider_style = ctx.styles.get_style(@str_hash("slider"));
|
||||
|
||||
elem.div.scroll_x.enabled = scroll_x;
|
||||
elem.div.scroll_y.enabled = scroll_y;
|
||||
elem.div.scroll_size = slider_style.size ? slider_style.size : (style.size ? style.size : DEFAULT_STYLE.size);
|
||||
elem.div.z_index = parent.div.z_index + 1;
|
||||
|
||||
// update layout with correct info
|
||||
elem.layout = {
|
||||
.w = width,
|
||||
.h = height,
|
||||
.dir = dir,
|
||||
.anchor = anchor,
|
||||
.content_offset = style.margin + style.border + style.padding,
|
||||
.absolute = absolute,
|
||||
};
|
||||
if (absolute) {
|
||||
elem.layout.origin.x = off.x;
|
||||
elem.layout.origin.y = off.y;
|
||||
}
|
||||
|
||||
ctx.push_rect(elem.bounds.pad(style.margin), elem.div.z_index, style)!;
|
||||
|
||||
// update the ctx scissor, it HAS to be after drawing the background
|
||||
ctx.div_scissor = elem.bounds.pad(elem.layout.content_offset);
|
||||
ctx.push_scissor(elem.bounds.pad(elem.layout.content_offset), elem.div.z_index)!;
|
||||
|
||||
elem.events = ctx.get_elem_events(elem);
|
||||
|
||||
// TODO: check active
|
||||
// TODO: check resizeable
|
||||
}
|
||||
|
||||
fn Id? Ctx.div_end(&ctx)
|
||||
{
|
||||
Elem* elem = ctx.get_active_div()!;
|
||||
|
||||
/* FIXME: this needs the absolute positioning to work
|
||||
// set the scrollbar flag, is used in layout
|
||||
Point cbc = elem.children_bounds.bottom_right();
|
||||
Point bc = elem.bounds.bottom_right();
|
||||
// horizontal overflow
|
||||
elem.div.scroll_x.on = cbc.x > bc.x && elem.div.scroll_x.enabled;
|
||||
// vertical overflow
|
||||
elem.div.scroll_y.on = cbc.y > bc.y && elem.div.scroll_y.enabled;
|
||||
|
||||
Id hsid_raw = @str_hash("div_scrollbar_horizontal");
|
||||
Id vsid_raw = @str_hash("div_scrollbar_vertical");
|
||||
Id hsid_real = ctx.gen_id(@str_hash("div_scrollbar_horizontal"))!;
|
||||
Id vsid_real = ctx.gen_id(@str_hash("div_scrollbar_vertical"))!;
|
||||
short wdim = elem.div.scroll_y.on ? (ctx.focus_id == vsid_real || ctx.is_hovered(ctx.find_elem(vsid_real)) ? elem.div.scroll_size*2 : elem.div.scroll_size) : 0;
|
||||
short hdim = elem.div.scroll_x.on ? (ctx.focus_id == hsid_real || ctx.is_hovered(ctx.find_elem(hsid_real)) ? elem.div.scroll_size*2 : elem.div.scroll_size) : 0;
|
||||
|
||||
if (elem.div.scroll_y.on) {
|
||||
if (ctx.input.events.mouse_scroll && ctx.hover_id == elem.id) {
|
||||
elem.div.scroll_y.value += ctx.input.mouse.scroll.y * 0.07f;
|
||||
elem.div.scroll_y.value = math::clamp(elem.div.scroll_y.value, 0.0f, 1.0f);
|
||||
}
|
||||
ctx.slider_ver_id(vsid_raw, @exact(wdim), @exact(elem.bounds.h - hdim), &elem.div.scroll_y.value, max((float)bc.y / cbc.y, (float)0.15))!;
|
||||
}
|
||||
|
||||
if (elem.div.scroll_x.on) {
|
||||
if (ctx.input.events.mouse_scroll && ctx.hover_id == elem.id) {
|
||||
elem.div.scroll_x.value += ctx.input.mouse.scroll.x * 0.07f;
|
||||
elem.div.scroll_x.value = math::clamp(elem.div.scroll_x.value, 0.0f, 1.0f);
|
||||
}
|
||||
ctx.slider_hor_id(hsid_raw, @exact(elem.bounds.w - wdim), @exact(hdim), &elem.div.scroll_x.value, max((float)bc.x / cbc.x, (float)0.15))!;
|
||||
}
|
||||
*/
|
||||
|
||||
// the active_div returns to the parent of the current one
|
||||
ctx.active_div = ctx.tree.parentof(ctx.active_div);
|
||||
Elem* parent = ctx.get_parent()!;
|
||||
ctx.div_scissor = parent.bounds.pad(parent.layout.content_offset);
|
||||
ctx.reset_scissor(elem.div.z_index)!;
|
||||
|
||||
update_parent_size(elem, parent);
|
||||
|
||||
return elem.id;
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
module ugui;
|
||||
|
||||
|
||||
macro Ctx.separator(&ctx, int width, int height, ...)
|
||||
=> ctx.separator_id(@compute_id($vasplat), width, height);
|
||||
fn void? Ctx.separator_id(&ctx, Id id, int width, int height)
|
||||
{
|
||||
id = ctx.gen_id(id)!;
|
||||
Elem *parent = ctx.get_parent()!;
|
||||
Elem *elem = ctx.get_elem(id, ETYPE_NONE)!;
|
||||
|
||||
elem.layout.w = @exact((short)width);
|
||||
elem.layout.h = @exact((short)height);
|
||||
|
||||
update_parent_size(elem, parent);
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
module ugui;
|
||||
|
||||
struct ElemSprite {
|
||||
Id id;
|
||||
}
|
||||
|
||||
macro Ctx.sprite(&ctx, String name, ...)
|
||||
=> ctx.sprite_id(@compute_id($vasplat), name);
|
||||
fn void? Ctx.sprite_id(&ctx, Id id, String name)
|
||||
{
|
||||
id = ctx.gen_id(id)!;
|
||||
Elem *parent = ctx.get_parent()!;
|
||||
Elem *elem = ctx.get_elem(id, ETYPE_SPRITE)!;
|
||||
|
||||
Style* style = ctx.styles.get_style(@str_hash("sprite"));
|
||||
Sprite* sprite = ctx.sprite_atlas.get(name)!;
|
||||
elem.sprite.id = ctx.get_sprite_atlas_id(name);
|
||||
|
||||
elem.layout.w = elem.layout.children.w = @exact(sprite.w);
|
||||
elem.layout.h = elem.layout.children.h = @exact(sprite.h);
|
||||
|
||||
update_parent_size(elem, parent);
|
||||
|
||||
Id tex_id = ctx.sprite_atlas.id;
|
||||
return ctx.push_sprite(elem.bounds, sprite.uv(), tex_id, parent.div.z_index, type: sprite.type)!;
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
module ugui;
|
||||
|
||||
import std::io;
|
||||
|
||||
struct ElemText {
|
||||
Id hash;
|
||||
TextSize size;
|
||||
TextEdit* te;
|
||||
}
|
||||
|
||||
/* Layout some text without bounds.
|
||||
* There is a limitation where the current frame bounds are based on the last frame, this is usually
|
||||
* not a problem but it is in the situation where the text changes almost all frames.
|
||||
*/
|
||||
macro Ctx.text(&ctx, String text, ...)
|
||||
=> ctx.text_id(@compute_id($vasplat), text);
|
||||
fn void? Ctx.text_id(&ctx, Id id, String text)
|
||||
{
|
||||
id = ctx.gen_id(id)!;
|
||||
|
||||
Elem *parent = ctx.get_parent()!;
|
||||
Elem *elem = ctx.get_elem(id, ETYPE_TEXT)!;
|
||||
Style* style = ctx.styles.get_style(@str_hash("text"));
|
||||
|
||||
Id text_hash = text.hash();
|
||||
if (elem.flags.is_new || elem.text.hash != text_hash) {
|
||||
elem.text.size = ctx.measure_string(text)!;
|
||||
}
|
||||
elem.text.hash = text_hash;
|
||||
|
||||
elem.layout.w = @fit(style.size);
|
||||
elem.layout.h = @fit(style.size);
|
||||
elem.layout.text = elem.text.size;
|
||||
elem.layout.content_offset = style.margin + style.border + style.padding;
|
||||
|
||||
update_parent_size(elem, parent);
|
||||
|
||||
ctx.layout_string(text, elem.bounds.pad(elem.layout.content_offset), TOP_LEFT, parent.div.z_index, style.fg)!;
|
||||
}
|
||||
|
||||
|
||||
macro Ctx.text_box(&ctx, Size w, Size h, TextEdit* te, ...)
|
||||
=> ctx.text_box_id(@compute_id($vasplat), w, h, te);
|
||||
fn ElemEvents? Ctx.text_box_id(&ctx, Id id, Size w, Size h, TextEdit* te)
|
||||
{
|
||||
id = ctx.gen_id(id)!;
|
||||
|
||||
Elem *parent = ctx.get_parent()!;
|
||||
Elem *elem = ctx.get_elem(id, ETYPE_TEXT)!;
|
||||
Style* style = ctx.styles.get_style(@str_hash("text-box"));
|
||||
|
||||
elem.text.te = te;
|
||||
|
||||
Id text_hash = te.to_string().hash();
|
||||
if (elem.flags.is_new || elem.text.hash != text_hash) {
|
||||
elem.text.size = ctx.measure_string(te.to_string())!;
|
||||
}
|
||||
elem.text.hash = text_hash;
|
||||
|
||||
elem.layout.w = w;
|
||||
elem.layout.h = h;
|
||||
elem.layout.text = elem.text.size;
|
||||
elem.layout.content_offset = style.margin + style.border + style.padding;
|
||||
|
||||
update_parent_size(elem, parent);
|
||||
|
||||
// check input and update the text
|
||||
elem.events = ctx.get_elem_events(elem);
|
||||
|
||||
if (elem.events.text_input || elem.events.key_press) {
|
||||
ctx.text_edit(elem.text.te);
|
||||
}
|
||||
|
||||
// draw the box
|
||||
|
||||
Rect bg_bounds = elem.bounds.pad(style.margin);
|
||||
Rect text_bounds = elem.bounds.pad(elem.layout.content_offset);
|
||||
ctx.push_rect(bg_bounds, parent.div.z_index, style)!;
|
||||
Rect cur;
|
||||
cur = ctx.layout_string(elem.text.te.to_string(), text_bounds, TOP_LEFT, parent.div.z_index, style.fg, elem.text.te.cursor)!;
|
||||
|
||||
// draw the cursor if the element has focus
|
||||
cur.w = 2;
|
||||
if (elem.events.has_focus) {
|
||||
ctx.push_scissor(text_bounds, parent.div.z_index)!;
|
||||
ctx.push_rect(cur, parent.div.z_index, &&(Style){.bg = style.fg})!;
|
||||
ctx.reset_scissor(parent.div.z_index)!;
|
||||
}
|
||||
return elem.events;
|
||||
}
|
21
resources/shaders/source/font.frag.glsl
Normal file
21
resources/shaders/source/font.frag.glsl
Normal 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);
|
||||
}
|
35
resources/shaders/source/msdf.frag.glsl
Normal file
35
resources/shaders/source/msdf.frag.glsl
Normal 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;
|
||||
}
|
32
resources/shaders/source/rect.frag.glsl
Normal file
32
resources/shaders/source/rect.frag.glsl
Normal 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);
|
||||
}
|
31
resources/shaders/source/rect.vert.glsl
Normal file
31
resources/shaders/source/rect.vert.glsl
Normal 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);
|
||||
}
|
18
resources/shaders/source/sprite.frag.glsl
Normal file
18
resources/shaders/source/sprite.frag.glsl
Normal 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);
|
||||
}
|
28
resources/shaders/source/sprite.vert.glsl
Normal file
28
resources/shaders/source/sprite.vert.glsl
Normal 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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -2,34 +2,39 @@ default {
|
||||
bg: #282828ff;
|
||||
fg: #fbf1c7ff;
|
||||
primary: #cc241dff;
|
||||
secondary: #6c19ca8f;
|
||||
secondary: #458588ff;
|
||||
accent: #fabd2fff;
|
||||
border: 1;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
radius: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 2;
|
||||
margin: 2 2 2 2;
|
||||
border: 2;
|
||||
padding: 2;
|
||||
radius: 10;
|
||||
size: 32;
|
||||
|
||||
bg: #3c3836ff;
|
||||
fg: #fbf1c7ff;
|
||||
primary: #cc241dff;
|
||||
secondary: #458588ff;
|
||||
accent: #504945ff;
|
||||
accent: #fabd2fff;
|
||||
}
|
||||
|
||||
button-active {
|
||||
margin: 2 2 2 2;
|
||||
border: 2;
|
||||
radius: 10;
|
||||
|
||||
bg: #504945ff;
|
||||
fg: #fbf1c7ff;
|
||||
primary: #cc241dff;
|
||||
secondary: #cc241dff;
|
||||
accent: #fabd2fff;
|
||||
}
|
||||
|
||||
checkbox {
|
||||
margin: 2;
|
||||
margin: 2 2 2 2;
|
||||
border: 2;
|
||||
padding: 1;
|
||||
radius: 10;
|
||||
size: 20;
|
||||
size: 16;
|
||||
bg: #3c3836ff;
|
||||
fg: #fbf1c7ff;
|
||||
primary: #cc241dff;
|
||||
@ -38,11 +43,10 @@ checkbox {
|
||||
}
|
||||
|
||||
toggle {
|
||||
margin: 2;
|
||||
margin: 2 2 2 2;
|
||||
border: 2;
|
||||
padding: 1;
|
||||
radius: 10;
|
||||
size: 20;
|
||||
radius: 0;
|
||||
size: 16;
|
||||
bg: #3c3836ff;
|
||||
fg: #fbf1c7ff;
|
||||
primary: #cc241dff;
|
||||
@ -51,8 +55,8 @@ toggle {
|
||||
}
|
||||
|
||||
slider {
|
||||
margin: 2;
|
||||
padding: 2;
|
||||
margin: 2 2 2 2;
|
||||
padding: 4 4 4 4;
|
||||
border: 1;
|
||||
radius: 4;
|
||||
size: 8;
|
||||
@ -62,14 +66,3 @@ slider {
|
||||
secondary: #458588ff;
|
||||
accent: #fabd2fff;
|
||||
}
|
||||
|
||||
text-box {
|
||||
bg: #4a4543ff;
|
||||
fg: #fbf1c7ff;
|
||||
primary: #cc241dff;
|
||||
secondary: #458588ff;
|
||||
accent: #fabd2fff;
|
||||
border: 1;
|
||||
padding: 4;
|
||||
margin: 2;
|
||||
}
|
||||
|
416
src/main.c3
416
src/main.c3
@ -6,7 +6,6 @@ import std::time;
|
||||
import std::collections::ringbuffer;
|
||||
import std::core::string;
|
||||
import std::ascii;
|
||||
import std::core::mem::allocator;
|
||||
import sdlrenderer::ren;
|
||||
import sdl3::sdl;
|
||||
|
||||
@ -14,6 +13,7 @@ alias Times = ringbuffer::RingBuffer{time::NanoDuration[128]};
|
||||
|
||||
fn void Times.print_stats(×)
|
||||
{
|
||||
if (times.written == 0);
|
||||
time::NanoDuration min, max, avg, x;
|
||||
min = times.get(0);
|
||||
for (usz i = 0; i < times.written; i++) {
|
||||
@ -22,7 +22,7 @@ fn void Times.print_stats(×)
|
||||
if (x > max) { max = x; }
|
||||
avg += x;
|
||||
}
|
||||
avg = (NanoDuration)((ulong)avg/128.0);
|
||||
avg = (NanoDuration)((ulong)avg/times.written);
|
||||
|
||||
io::printfn("min=%s, max=%s, avg=%s", min, max, avg);
|
||||
}
|
||||
@ -33,6 +33,7 @@ struct TimeStats {
|
||||
|
||||
fn TimeStats Times.get_stats(×)
|
||||
{
|
||||
if (times.written == 0) return {};
|
||||
time::NanoDuration min, max, avg, x;
|
||||
min = times.get(0);
|
||||
for (usz i = 0; i < times.written; i++) {
|
||||
@ -41,93 +42,96 @@ fn TimeStats Times.get_stats(×)
|
||||
if (x > max) { max = x; }
|
||||
avg += x;
|
||||
}
|
||||
avg = (NanoDuration)((ulong)avg/128.0);
|
||||
avg = (NanoDuration)((ulong)avg/times.written);
|
||||
|
||||
return {.min = min, .max = max, .avg = avg};
|
||||
}
|
||||
|
||||
|
||||
const char[*] VS_PATH = "resources/shaders/compiled/ugui.vert.spv";
|
||||
const char[*] FS_PATH = "resources/shaders/compiled/ugui.frag.spv";
|
||||
const char[*] MSDF_FS_PATH = "resources/shaders/compiled/msdf.frag.spv";
|
||||
const char[*] SPRITE_FS_PATH = "resources/shaders/compiled/sprite.frag.spv";
|
||||
const char[*] FONT_FS_PATH = "resources/shaders/compiled/font.frag.spv";
|
||||
const char[*] RECT_FS_PATH = "resources/shaders/compiled/rect.frag.spv";
|
||||
|
||||
const char[*] SPRITE_VS_PATH = "resources/shaders/compiled/sprite.vert.spv";
|
||||
const char[*] RECT_VS_PATH = "resources/shaders/compiled/rect.vert.spv";
|
||||
|
||||
const char[*] STYLESHEET_PATH = "resources/style.css";
|
||||
|
||||
const bool LIMIT_FPS = true;
|
||||
const bool VSYNC = true;
|
||||
|
||||
fn int main(String[] args)
|
||||
{
|
||||
ArenaAllocator arena;
|
||||
char[] mem = mem::new_array(char, 1024*1024);
|
||||
defer (void)mem::free(mem);
|
||||
arena.init(mem);
|
||||
|
||||
ugui::Ctx ui;
|
||||
ui.init(&arena)!!;
|
||||
ui.init()!!;
|
||||
defer ui.free();
|
||||
|
||||
ren::Renderer ren;
|
||||
ren.init("Ugui Test", 800, 600, VSYNC);
|
||||
ren.init("Ugui Test", 800, 600, true);
|
||||
defer ren.free();
|
||||
ui.input_window_size(800, 600)!!;
|
||||
|
||||
// ========================================================================================== //
|
||||
// FONT LOADING //
|
||||
// ========================================================================================== //
|
||||
// import font in the ui context
|
||||
ui.load_font("font1", "resources/hack-nerd.ttf", 16)!!;
|
||||
//
|
||||
// FONT LOADING
|
||||
//
|
||||
{
|
||||
// import font in the ui context
|
||||
ui.load_font("font1", "resources/hack-nerd.ttf", 16)!!;
|
||||
|
||||
// create the rendering pipeline
|
||||
ren.font_atlas_id = ui.get_font_id("font1");
|
||||
ren.load_spirv_shader_from_file("UGUI_PIPELINE_FONT", SPRITE_VS_PATH, FONT_FS_PATH, 1, 0);
|
||||
ren.create_pipeline("UGUI_PIPELINE_FONT", SPRITE);
|
||||
|
||||
// send the atlas to the gpu
|
||||
Atlas* font_atlas = ui.get_font_atlas("font1")!!;
|
||||
ren.new_texture("font1", JUST_ALPHA, font_atlas.buffer, font_atlas.width, font_atlas.height);
|
||||
}
|
||||
|
||||
// set the renderer's font atlas
|
||||
ren.font_atlas_id = ui.get_font_id("font1");
|
||||
//
|
||||
// ICON LOADING
|
||||
//
|
||||
{
|
||||
// create the atlas and upload some icons
|
||||
ui.sprite_atlas_create("icons", AtlasType.ATLAS_R8G8B8A8, 512, 512)!!;
|
||||
ui.import_sprite_file_qoi("tux", "resources/tux.qoi")!!;
|
||||
ui.import_sprite_file_qoi("tick", "resources/tick_sdf.qoi", SpriteType.SPRITE_MSDF)!!;
|
||||
|
||||
// create the rendering pipelines
|
||||
ren.sprite_atlas_id = ui.get_sprite_atlas_id("icons");
|
||||
// normal sprite pipeline
|
||||
ren.load_spirv_shader_from_file("UGUI_PIPELINE_SPRITE", SPRITE_VS_PATH, SPRITE_FS_PATH, 1, 0);
|
||||
ren.create_pipeline("UGUI_PIPELINE_SPRITE", SPRITE);
|
||||
// msdf sprite pipeline
|
||||
ren.load_spirv_shader_from_file("UGUI_PIPELINE_SPRITE_MSDF", SPRITE_VS_PATH, MSDF_FS_PATH, 1, 0);
|
||||
ren.create_pipeline("UGUI_PIPELINE_SPRITE_MSDF", SPRITE);
|
||||
|
||||
// upload the atlas to the gpu
|
||||
Atlas atlas = ui.sprite_atlas.atlas;
|
||||
ren.new_texture("icons", FULL_COLOR, atlas.buffer, atlas.width, atlas.height);
|
||||
}
|
||||
|
||||
// send the atlas to the gpu
|
||||
Atlas* font_atlas = ui.get_font_atlas("font1")!!;
|
||||
ren.new_texture("font1", JUST_ALPHA, font_atlas.buffer, font_atlas.width, font_atlas.height);
|
||||
//
|
||||
// RECT PIPELINE
|
||||
//
|
||||
ren.load_spirv_shader_from_file("UGUI_PIPELINE_RECT", RECT_VS_PATH, RECT_FS_PATH, 0, 0);
|
||||
ren.create_pipeline("UGUI_PIPELINE_RECT", RECT);
|
||||
|
||||
// ========================================================================================== //
|
||||
// ICON LOADING //
|
||||
// ========================================================================================== //
|
||||
// create the atlas and upload some icons
|
||||
ui.sprite_atlas_create("icons", AtlasType.ATLAS_R8G8B8A8, 512, 512)!!;
|
||||
ui.import_sprite_file_qoi("tux", "resources/tux.qoi")!!;
|
||||
ui.import_sprite_file_qoi("tick", "resources/tick_sdf.qoi", SpriteType.SPRITE_MSDF)!!;
|
||||
|
||||
// set the renderer's sprite atlas
|
||||
ren.sprite_atlas_id = ui.get_sprite_atlas_id("icons");
|
||||
|
||||
// upload the atlas to the gpu
|
||||
Atlas atlas = ui.sprite_atlas.atlas;
|
||||
ren.new_texture("icons", FULL_COLOR, atlas.buffer, atlas.width, atlas.height);
|
||||
|
||||
// ========================================================================================== //
|
||||
// PIPELINE SETUP //
|
||||
// ========================================================================================== //
|
||||
ren.load_spirv_shader_from_file("UGUI_PIPELINE", VS_PATH, FS_PATH, 2, 0);
|
||||
ren.create_pipeline("UGUI_PIPELINE", RECT);
|
||||
|
||||
// ========================================================================================== //
|
||||
// CSS INPUT //
|
||||
// ========================================================================================== //
|
||||
// CSS INPUT
|
||||
io::printfn("imported %d styles", ui.import_style_from_file(STYLESHEET_PATH));
|
||||
|
||||
// ========================================================================================== //
|
||||
// OTHER VARIABLES //
|
||||
// ========================================================================================== //
|
||||
TextEdit te;
|
||||
te.buffer = mem::new_array(char, 256);
|
||||
defer mem::free(te.buffer);
|
||||
|
||||
isz frame;
|
||||
double fps;
|
||||
bool toggle = true;
|
||||
time::Clock clock;
|
||||
time::Clock fps_clock;
|
||||
time::Clock sleep_clock;
|
||||
Times ui_times;
|
||||
Times draw_times;
|
||||
|
||||
// ========================================================================================== //
|
||||
// MAIN LOOP //
|
||||
// ========================================================================================== //
|
||||
|
||||
//
|
||||
// MAIN LOOP
|
||||
//
|
||||
sdl::start_text_input(ren.win);
|
||||
|
||||
sdl::Event e;
|
||||
@ -138,36 +142,26 @@ fn int main(String[] args)
|
||||
clock.mark();
|
||||
fps_clock.mark();
|
||||
sleep_clock.mark();
|
||||
|
||||
|
||||
do {
|
||||
switch (e.type) {
|
||||
case EVENT_QUIT:
|
||||
case EVENT_QUIT:
|
||||
quit = true;
|
||||
case EVENT_KEY_UP:
|
||||
ui.input_key_release();
|
||||
nextcase;
|
||||
case EVENT_KEY_UP: nextcase;
|
||||
case EVENT_KEY_DOWN:
|
||||
ui.input_key_press();
|
||||
if (e.key.repeat) ui.input_key_repeat();
|
||||
|
||||
mod.rctrl = e.key.key == K_RCTRL ? !!(e.type == EVENT_KEY_DOWN) : mod.rctrl;
|
||||
mod.lctrl = e.key.key == K_LCTRL ? !!(e.type == EVENT_KEY_DOWN) : mod.lctrl;
|
||||
mod.bkspc = e.key.key == K_BACKSPACE ? !!(e.type == EVENT_KEY_DOWN) : mod.bkspc;
|
||||
mod.del = e.key.key == K_DELETE ? !!(e.type == EVENT_KEY_DOWN) : mod.del;
|
||||
mod.up = e.key.key == K_UP ? !!(e.type == EVENT_KEY_DOWN) : mod.up;
|
||||
mod.down = e.key.key == K_DOWN ? !!(e.type == EVENT_KEY_DOWN) : mod.down;
|
||||
mod.left = e.key.key == K_LEFT ? !!(e.type == EVENT_KEY_DOWN) : mod.left;
|
||||
mod.right = e.key.key == K_RIGHT ? !!(e.type == EVENT_KEY_DOWN) : mod.right;
|
||||
|
||||
// pressing ctrl+key or alt+key does not generate a character as such no
|
||||
// TEXT_INPUT event is generated. When those keys are pressed we have to
|
||||
// pressing ctrl+key or alt+key does not generate a character as such no
|
||||
// TEXT_INPUT event is generated. When those keys are pressed we have to
|
||||
// do manual text input, bummer
|
||||
if (e.type == EVENT_KEY_DOWN && (mod.lctrl || mod.rctrl)) {
|
||||
if (ascii::is_alnum_m((uint)e.key.key)) {
|
||||
ui.input_char((char)e.key.key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (e.type == EVENT_KEY_DOWN && e.key.key == K_RETURN) ui.input_char('\n');
|
||||
|
||||
case EVENT_TEXT_INPUT:
|
||||
@ -207,33 +201,101 @@ fn int main(String[] args)
|
||||
|
||||
if (ui.check_key_combo(ugui::KMOD_CTRL, "q")) quit = true;
|
||||
|
||||
const String APPLICATION = "calculator";
|
||||
$switch APPLICATION:
|
||||
$case "debug":
|
||||
debug_app(&ui);
|
||||
$case "calculator":
|
||||
calculator(&ui, &te);
|
||||
$endswitch
|
||||
ui.div_begin({.w=-100})!!;
|
||||
{
|
||||
ui.layout_set_column()!!;
|
||||
if (ui.button({0,0,30,30}, toggle)!!.mouse_press) {
|
||||
io::printn("press button0");
|
||||
toggle = !toggle;
|
||||
}
|
||||
//ui.layout_next_column()!!;
|
||||
if (ui.button({0,0,30,30})!!.mouse_press) {
|
||||
io::printn("press button1");
|
||||
}
|
||||
//ui.layout_next_column()!!;
|
||||
if (ui.button({0,0,30,30})!!.mouse_release) {
|
||||
io::printn("release button2");
|
||||
}
|
||||
|
||||
ui.layout_set_row()!!;
|
||||
ui.layout_next_row()!!;
|
||||
static float rf, gf, bf, af;
|
||||
ui.slider_ver({0,0,30,100}, &rf)!!;
|
||||
ui.slider_ver({0,0,30,100}, &gf)!!;
|
||||
ui.slider_ver({0,0,30,100}, &bf)!!;
|
||||
ui.slider_ver({0,0,30,100}, &af)!!;
|
||||
|
||||
ui.layout_next_column()!!;
|
||||
ui.text_unbounded("Ciao Mamma\nAbilità ⚡\n'\udb80\udd2c'")!!;
|
||||
|
||||
ui.layout_next_column()!!;
|
||||
ui.button_label("Continua!")!!;
|
||||
|
||||
ui.layout_next_row()!!;
|
||||
static bool check;
|
||||
ui.checkbox("", {}, &check, "tick")!!;
|
||||
ui.checkbox("", {}, &check)!!;
|
||||
ui.toggle("", {}, &toggle)!!;
|
||||
};
|
||||
ui.sprite("tux")!!;
|
||||
|
||||
static char[128] text_box = "ciao mamma";
|
||||
static usz text_len = "ciao mamma".len;
|
||||
ui.text_box({0,0,200,200}, text_box[..], &text_len)!!;
|
||||
|
||||
ui.div_end()!!;
|
||||
|
||||
ui.div_begin(ugui::DIV_FILL, scroll_x: true, scroll_y: true)!!;
|
||||
{
|
||||
ui.layout_set_column()!!;
|
||||
static float slider2 = 0.5;
|
||||
if (ui.slider_ver({0,0,30,100}, &slider2)!!.update) {
|
||||
io::printfn("other slider: %f", slider2);
|
||||
}
|
||||
ui.button({0,0,50,50})!!;
|
||||
ui.button({0,0,50,50})!!;
|
||||
ui.button({0,0,50,50})!!;
|
||||
ui.button({0,0,50,50})!!;
|
||||
if (toggle) {
|
||||
ui.button({0,0,50,50})!!;
|
||||
ui.button({0,0,50,50})!!;
|
||||
ui.button({0,0,50,50})!!;
|
||||
ui.button({0,0,50,50})!!;
|
||||
}
|
||||
ui.layout_next_column()!!;
|
||||
ui.layout_set_row()!!;
|
||||
static float f1, f2;
|
||||
ui.slider_hor({0,0,100,30}, &f1)!!;
|
||||
ui.slider_hor({0,0,100,30}, &f2)!!;
|
||||
};
|
||||
ui.div_end()!!;
|
||||
|
||||
// Timings counter
|
||||
TimeStats dts = draw_times.get_stats();
|
||||
TimeStats uts = ui_times.get_stats();
|
||||
|
||||
ui.@div(ugui::@fit(), ugui::@fit(), COLUMN, TOP_LEFT, true) {
|
||||
ui.text(string::tformat("frame %d, fps = %.2f", frame, fps))!!;
|
||||
ui.text(string::tformat("ui avg: %s\ndraw avg: %s\nTOT: %s", uts.avg, dts.avg, uts.avg+dts.avg))!!;
|
||||
}!!;
|
||||
ui.layout_set_floating()!!;
|
||||
// FIXME: I cannot anchor shit to the bottom of the screen
|
||||
ui.div_begin({0, ui.height-150, -300, 150})!!;
|
||||
{
|
||||
ui.layout_set_column()!!;
|
||||
ui.text_unbounded(string::tformat("frame %d, fps = %.2f", frame, fps))!!;
|
||||
ui.text_unbounded(string::tformat("ui avg: %s\ndraw avg: %s\nTOT: %s", uts.avg, dts.avg, uts.avg+dts.avg))!!;
|
||||
ui.text_unbounded(string::tformat("%s %s", mod.lctrl, (String)ui.input.keyboard.text[..]))!!;
|
||||
};
|
||||
ui.div_end()!!;
|
||||
|
||||
ui.frame_end()!!;
|
||||
/* End UI Handling */
|
||||
ui_times.push(clock.mark());
|
||||
//ui_times.print_stats();
|
||||
|
||||
|
||||
/* Start UI Drawing */
|
||||
ren.begin_render(true);
|
||||
|
||||
ren.render_ugui(&ui.cmd_queue);
|
||||
|
||||
|
||||
ren.end_render();
|
||||
|
||||
draw_times.push(clock.mark());
|
||||
@ -241,190 +303,12 @@ $endswitch
|
||||
/* End Drawing */
|
||||
|
||||
// wait for the next event, timeout after 100ms
|
||||
int timeout = LIMIT_FPS ? (int)(100.0-sleep_clock.mark().to_ms()-0.5) : 0;
|
||||
sdl::wait_event_timeout(&e, timeout);
|
||||
|
||||
sdl::wait_event_timeout(&e, (int)(100.0-sleep_clock.mark().to_ms()-0.5));
|
||||
|
||||
fps = 1.0 / fps_clock.mark().to_sec();
|
||||
frame++;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
fn void debug_app(ugui::Ctx* ui)
|
||||
{
|
||||
static bool toggle;
|
||||
ui.div_begin({.w=-100})!!;
|
||||
{
|
||||
ui.layout_set_column()!!;
|
||||
if (ui.button(icon:"tux")!!.mouse_press) {
|
||||
io::printn("press button0");
|
||||
toggle = !toggle;
|
||||
}
|
||||
//ui.layout_next_column()!!;
|
||||
if (ui.button(label: "ciao", icon: "tick")!!.mouse_press) {
|
||||
io::printn("press button1");
|
||||
}
|
||||
//ui.layout_next_column()!!;
|
||||
if (ui.button()!!.mouse_release) {
|
||||
io::printn("release button2");
|
||||
}
|
||||
|
||||
ui.layout_set_row()!!;
|
||||
ui.layout_next_row()!!;
|
||||
static float rf, gf, bf, af;
|
||||
ui.slider_ver({0,0,30,100}, &rf)!!;
|
||||
ui.slider_ver({0,0,30,100}, &gf)!!;
|
||||
ui.slider_ver({0,0,30,100}, &bf)!!;
|
||||
ui.slider_ver({0,0,30,100}, &af)!!;
|
||||
|
||||
ui.layout_next_column()!!;
|
||||
ui.text_unbounded("Ciao Mamma\nAbilità ⚡\n'\udb80\udd2c'")!!;
|
||||
|
||||
ui.layout_next_column()!!;
|
||||
ui.button("Continua!")!!;
|
||||
|
||||
ui.layout_next_row()!!;
|
||||
static bool check;
|
||||
ui.checkbox("", {}, &check, "tick")!!;
|
||||
ui.checkbox("", {}, &check)!!;
|
||||
ui.toggle("", {}, &toggle)!!;
|
||||
|
||||
ui.sprite("tux")!!;
|
||||
static char[128] text_box = "ciao mamma";
|
||||
static usz text_len = "ciao mamma".len;
|
||||
ui.text_box({0,0,200,200}, text_box[..], &text_len)!!;
|
||||
};
|
||||
ui.div_end()!!;
|
||||
|
||||
ui.div_begin(ugui::DIV_FILL, scroll_x: true, scroll_y: true)!!;
|
||||
{
|
||||
ui.layout_set_column()!!;
|
||||
static float slider2 = 0.5;
|
||||
if (ui.slider_ver({0,0,30,100}, &slider2)!!.update) {
|
||||
io::printfn("other slider: %f", slider2);
|
||||
}
|
||||
ui.button()!!;
|
||||
ui.button()!!;
|
||||
ui.button()!!;
|
||||
ui.button()!!;
|
||||
if (toggle) {
|
||||
ui.button()!!;
|
||||
ui.button()!!;
|
||||
ui.button()!!;
|
||||
ui.button()!!;
|
||||
}
|
||||
ui.layout_next_column()!!;
|
||||
ui.layout_set_row()!!;
|
||||
static float f1, f2;
|
||||
ui.slider_hor({0,0,100,30}, &f1)!!;
|
||||
ui.slider_hor({0,0,100,30}, &f2)!!;
|
||||
};
|
||||
ui.div_end()!!;
|
||||
}
|
||||
*/
|
||||
|
||||
import std::os::process;
|
||||
|
||||
fn void calculator(ugui::Ctx* ui, TextEdit* te)
|
||||
{
|
||||
static char[128] buffer;
|
||||
static usz len;
|
||||
bool eval;
|
||||
|
||||
// keyboard input
|
||||
switch(ui.get_keys()) {
|
||||
case "+": nextcase;
|
||||
case "-": nextcase;
|
||||
case "*": nextcase;
|
||||
case "/": nextcase;
|
||||
case "(": nextcase;
|
||||
case ")": nextcase;
|
||||
case ".": nextcase;
|
||||
case "0": nextcase;
|
||||
case "1": nextcase;
|
||||
case "2": nextcase;
|
||||
case "3": nextcase;
|
||||
case "4": nextcase;
|
||||
case "5": nextcase;
|
||||
case "6": nextcase;
|
||||
case "7": nextcase;
|
||||
case "8": nextcase;
|
||||
case "9":
|
||||
buffer[len++] = ui.get_keys()[0];
|
||||
case "\n":
|
||||
eval = len != 0;
|
||||
case "c":
|
||||
len = 0;
|
||||
case "d":
|
||||
if (len > 0) len--;
|
||||
}
|
||||
|
||||
// ui input/output
|
||||
ui.@div(ugui::@grow(), ugui::@grow(), ROW, CENTER) { // center everything on the screen
|
||||
ui.@div(ugui::@fit(), ugui::@fit(), COLUMN, TOP_LEFT) {
|
||||
|
||||
ui.@div(ugui::@grow(), ugui::@fit(), ROW, CENTER) {ui.text("SHITTY AHH CALCULATOR")!!;}!!;
|
||||
ui.@div(ugui::@grow(), ugui::@exact(100), ROW, RIGHT) {
|
||||
ui.text((String)buffer[:len])!!;
|
||||
}!!;
|
||||
ui.@div(ugui::@fit(), ugui::@fit(), ROW, TOP_LEFT) {
|
||||
ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) {
|
||||
ui.button("7")!!.mouse_press ? buffer[len++] = '7' : 0;
|
||||
ui.button("4")!!.mouse_press ? buffer[len++] = '4' : 0;
|
||||
ui.button("1")!!.mouse_press ? buffer[len++] = '1' : 0;
|
||||
ui.button("0")!!.mouse_press ? buffer[len++] = '0' : 0;
|
||||
}!!;
|
||||
ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) {
|
||||
ui.button("8")!!.mouse_press ? buffer[len++] = '8' : 0;
|
||||
ui.button("5")!!.mouse_press ? buffer[len++] = '5' : 0;
|
||||
ui.button("2")!!.mouse_press ? buffer[len++] = '2' : 0;
|
||||
ui.button(".")!!.mouse_press ? buffer[len++] = '.' : 0;
|
||||
}!!;
|
||||
ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) {
|
||||
ui.button("9")!!.mouse_press ? buffer[len++] = '9' : 0;
|
||||
ui.button("6")!!.mouse_press ? buffer[len++] = '6' : 0;
|
||||
ui.button("3")!!.mouse_press ? buffer[len++] = '3' : 0;
|
||||
ui.button("(")!!.mouse_press ? buffer[len++] = '(' : 0;
|
||||
}!!;
|
||||
ui.@div(ugui::@exact(10), ugui::@exact(10)) {}!!;
|
||||
ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) {
|
||||
ui.button("x")!!.mouse_press ? buffer[len++] = '*' : 0;
|
||||
ui.button("/")!!.mouse_press ? buffer[len++] = '/' : 0;
|
||||
ui.button("+")!!.mouse_press ? buffer[len++] = '+' : 0;
|
||||
ui.button(")")!!.mouse_press ? buffer[len++] = ')' : 0;
|
||||
}!!;
|
||||
ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) {
|
||||
ui.button("C")!!.mouse_press ? len = 0 : 0;
|
||||
ui.button("D")!!.mouse_press ? len > 0 ? len-- : 0 : 0;
|
||||
ui.button("-")!!.mouse_press ? buffer[len++] = '-' : 0;
|
||||
|
||||
// eval the expression with 'bc'
|
||||
if (ui.button("=")!!.mouse_press || eval) {
|
||||
char[128] out;
|
||||
String y = string::tformat("echo '%s' | bc", (String)buffer[:len]);
|
||||
String x = process::execute_stdout_to_buffer(out[:128], (String[]){"sh", "-c", y}) ?? "";
|
||||
buffer[:x.len] = x[..];
|
||||
len = x.len;
|
||||
}
|
||||
}!!;
|
||||
}!!;
|
||||
|
||||
ui.@div(ugui::@grow(), ugui::@fit(), anchor: CENTER) {
|
||||
static bool state;
|
||||
ui.checkbox("boolean", &state, "tick")!!;
|
||||
ui.sprite("tux")!!;
|
||||
ui.toggle("lmao", &state)!!;
|
||||
}!!;
|
||||
ui.@div(ugui::@grow(), ugui::@exact(50), anchor: CENTER, scroll_y: true) {
|
||||
static float f;
|
||||
ui.slider_hor(ugui::@exact(100), ugui::@exact(20), &f)!!;
|
||||
ui.slider_ver(ugui::@exact(20), ugui::@exact(100), &f)!!;
|
||||
}!!;
|
||||
ui.@div(ugui::@grow(), ugui::@fit(), anchor: CENTER, scroll_y: true) {
|
||||
ui.text_box(ugui::@grow(), ugui::@exact(100), te)!!;
|
||||
}!!;
|
||||
|
||||
}!!; }!!;
|
||||
}
|
||||
|
332
src/renderer.c3
332
src/renderer.c3
@ -1,8 +1,8 @@
|
||||
// extends the List type to search elements that have a type.id property
|
||||
<*
|
||||
@require $defined((Type){}.id) : `No .id member found in the type`
|
||||
*>
|
||||
module idlist{Type};
|
||||
|
||||
// extends the List type to search elements that have a type.id property
|
||||
// TODO: check that type has an id
|
||||
|
||||
import std::collections::list;
|
||||
|
||||
alias IdList = List{Type};
|
||||
@ -22,61 +22,16 @@ macro Type* IdList.get_from_id(&self, id)
|
||||
return null;
|
||||
}
|
||||
|
||||
module sdlrenderer::ren;
|
||||
|
||||
// 2D renderer for ugui, based on SDL3 using the new GPU API
|
||||
module sdlrenderer::ren;
|
||||
|
||||
import std::io;
|
||||
import std::core::mem;
|
||||
import sdl3::sdl;
|
||||
import idlist;
|
||||
import ugui;
|
||||
|
||||
|
||||
// ============================================================================================== //
|
||||
// CONSTANTS //
|
||||
// ============================================================================================== //
|
||||
|
||||
const int DEBUG = 1;
|
||||
const bool CYCLE = true;
|
||||
const int MAX_QUAD_BATCH = 2048;
|
||||
|
||||
|
||||
// ============================================================================================== //
|
||||
// STRUCTURES //
|
||||
// ============================================================================================== //
|
||||
|
||||
// How each vertex is represented in the gpu
|
||||
struct Vertex {
|
||||
short x, y;
|
||||
}
|
||||
|
||||
// Attributes of each quad instance
|
||||
struct QuadAttributes {
|
||||
struct pos {
|
||||
short x, y, w, h;
|
||||
}
|
||||
struct uv {
|
||||
short u, v;
|
||||
}
|
||||
uint color;
|
||||
uint type;
|
||||
}
|
||||
|
||||
// A single quad
|
||||
struct Quad {
|
||||
struct vertices {
|
||||
Vertex v1,v2,v3,v4;
|
||||
}
|
||||
struct indices {
|
||||
short i1,i2,i3,i4,i5,i6;
|
||||
}
|
||||
}
|
||||
|
||||
// the viewport size uniform passed to the gpu
|
||||
struct ViewsizeUniform @align(16) {
|
||||
int w, h;
|
||||
}
|
||||
|
||||
struct Shader {
|
||||
sdl::GPUShader* frag;
|
||||
sdl::GPUShader* vert;
|
||||
@ -96,6 +51,7 @@ struct Texture {
|
||||
}
|
||||
|
||||
// The GPU buffers that contain quad info, the size is determined by MAX_QUAD_BATCH
|
||||
const int MAX_QUAD_BATCH = 2048;
|
||||
struct QuadBuffer {
|
||||
sdl::GPUBuffer* vert_buf; // on-gpu vertex buffer
|
||||
sdl::GPUBuffer* idx_buf; // on-gpu index buffer
|
||||
@ -130,20 +86,42 @@ struct Renderer {
|
||||
Id sprite_atlas_id;
|
||||
Id font_atlas_id;
|
||||
|
||||
int scissor_x, scissor_y, scissor_w, scissor_h;
|
||||
int scissor_x, scissor_y, scissor_w, scissor_h;
|
||||
}
|
||||
|
||||
// How each vertex is represented in the gpu
|
||||
struct Vertex {
|
||||
short x, y;
|
||||
}
|
||||
|
||||
// ============================================================================================== //
|
||||
// RENDERERER METHODS //
|
||||
// ============================================================================================== //
|
||||
// Attributes of each quad instance
|
||||
struct QuadAttributes {
|
||||
struct pos {
|
||||
short x, y, w, h;
|
||||
}
|
||||
struct uv {
|
||||
short u, v;
|
||||
}
|
||||
uint color;
|
||||
}
|
||||
|
||||
// A single quad
|
||||
struct Quad {
|
||||
struct vertices {
|
||||
Vertex v1,v2,v3,v4;
|
||||
}
|
||||
struct indices {
|
||||
short i1,i2,i3,i4,i5,i6;
|
||||
}
|
||||
}
|
||||
|
||||
struct ViewsizeUniform @align(16) {
|
||||
int w, h;
|
||||
}
|
||||
|
||||
const int DEBUG = 1;
|
||||
const bool CYCLE = true;
|
||||
|
||||
/* Initialize the renderer structure, this does a couple of things
|
||||
* 1. Initializes the SDL video subsystem with the correct hints
|
||||
* 2. Creates a window and attaches it to the gpu device
|
||||
* 3. Allocates the quad buffer and uploads the quad mesh to the GPU
|
||||
*/
|
||||
fn void Renderer.init(&self, ZString title, uint width, uint height, bool vsync)
|
||||
{
|
||||
// set wayland hint automagically
|
||||
@ -162,7 +140,7 @@ $if DEBUG == 0:
|
||||
}
|
||||
$else
|
||||
// in debug mode set the video driver to X11 because renderdoc
|
||||
// doesn't support debugging in wayland yet.
|
||||
// doesn't support debugging in wayland yet.
|
||||
sdl::set_hint(sdl::HINT_VIDEO_DRIVER, "x11");
|
||||
sdl::set_hint(sdl::HINT_RENDER_GPU_DEBUG, "1");
|
||||
$endif
|
||||
@ -221,6 +199,7 @@ $endif
|
||||
unreachable("failed to initialize quad buffer (index): %s", sdl::get_error());
|
||||
}
|
||||
|
||||
|
||||
// upload the quad mesh
|
||||
GPUTransferBuffer *ts = sdl::create_gpu_transfer_buffer(self.gpu,
|
||||
&&(GPUTransferBufferCreateInfo){.usage = GPU_TRANSFERBUFFERUSAGE_UPLOAD, .size = Quad.sizeof}
|
||||
@ -254,7 +233,7 @@ $endif
|
||||
quad.indices.i4 = 1; // v2
|
||||
quad.indices.i5 = 2; // v3
|
||||
quad.indices.i6 = 3; // v4
|
||||
|
||||
|
||||
sdl::unmap_gpu_transfer_buffer(self.gpu, ts);
|
||||
|
||||
GPUCommandBuffer* cmd = sdl::acquire_gpu_command_buffer(self.gpu);
|
||||
@ -264,13 +243,13 @@ $endif
|
||||
GPUCopyPass* cpy = sdl::begin_gpu_copy_pass(cmd);
|
||||
|
||||
// upload vertices
|
||||
sdl::upload_to_gpu_buffer(cpy,
|
||||
sdl::upload_to_gpu_buffer(cpy,
|
||||
&&(GPUTransferBufferLocation){.transfer_buffer = ts, .offset = Quad.vertices.offsetof},
|
||||
&&(GPUBufferRegion){.buffer = qb.vert_buf, .offset = 0, .size = Quad.vertices.sizeof},
|
||||
false
|
||||
);
|
||||
// upload indices
|
||||
sdl::upload_to_gpu_buffer(cpy,
|
||||
sdl::upload_to_gpu_buffer(cpy,
|
||||
&&(GPUTransferBufferLocation){.transfer_buffer = ts, .offset = Quad.indices.offsetof},
|
||||
&&(GPUBufferRegion){.buffer = qb.idx_buf, .offset = 0, .size = Quad.indices.sizeof},
|
||||
false
|
||||
@ -299,13 +278,6 @@ $endif
|
||||
qb.initialized = true;
|
||||
}
|
||||
|
||||
|
||||
/* Frees all the renderer structures:
|
||||
* - Frees all textures and pipelines
|
||||
* - Releases all GPU transfer buffers
|
||||
* - Closes the main window
|
||||
* - Releases the GPU device
|
||||
*/
|
||||
fn void Renderer.free(&self)
|
||||
{
|
||||
foreach (&s: self.shaders) {
|
||||
@ -338,24 +310,17 @@ fn void Renderer.free(&self)
|
||||
sdl::quit();
|
||||
}
|
||||
|
||||
|
||||
fn void Renderer.resize_window(&self, uint width, uint height)
|
||||
{
|
||||
sdl::set_window_size(self.win, width, height);
|
||||
}
|
||||
|
||||
|
||||
fn void Renderer.get_window_size(&self, int* width, int* height)
|
||||
{
|
||||
sdl::get_window_size_in_pixels(self.win, width, height);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================================== //
|
||||
// SHADER LOADING //
|
||||
// ============================================================================================== //
|
||||
|
||||
|
||||
// Both the vertex shader and fragment shader have an implicit uniform buffer at binding 0 that
|
||||
// contains the viewport size. It is populated automatically at every begin_render() call
|
||||
fn void Renderer.load_spirv_shader_from_mem(&self, String name, char[] vert_code, char[] frag_code, uint textures, uint uniforms)
|
||||
@ -413,8 +378,7 @@ fn void Renderer.load_spirv_shader_from_mem(&self, String name, char[] vert_code
|
||||
self.shaders.push(s);
|
||||
}
|
||||
|
||||
|
||||
fn void Renderer.load_spirv_shader_from_file(&self, String name, String vert_path, String frag_path, uint textures, uint uniforms)
|
||||
fn void Renderer.load_spirv_shader_from_file(&self, String name, String vert_path, String frag_path, uint textures, uint uniforms)
|
||||
{
|
||||
if (vert_path == "" || frag_path == "") {
|
||||
unreachable("need both a vertex shader and fragment shader path");
|
||||
@ -438,12 +402,6 @@ fn void Renderer.load_spirv_shader_from_file(&self, String name, String vert_pat
|
||||
self.load_spirv_shader_from_mem(name, vert_code, frag_code, textures, uniforms);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================================== //
|
||||
// PIPELINE CREATION //
|
||||
// ============================================================================================== //
|
||||
|
||||
|
||||
// this describes what we want to draw, since for drawing different things we have to change
|
||||
// the GPUPrimitiveType and GPURasterizerState for the pipeline.
|
||||
enum PipelineType : (GPUPrimitiveType primitive_type, GPURasterizerState raster_state) {
|
||||
@ -467,8 +425,8 @@ fn void Renderer.create_pipeline(&self, String shader_name, PipelineType type)
|
||||
.fragment_shader = s.frag,
|
||||
// This structure specifies how the vertex buffer looks in memory, what it contains
|
||||
// and what is passed where to the gpu. Each vertex has three attributes, position,
|
||||
// color and uv coordinates. Since this is a 2D pixel-based renderer the position
|
||||
// is represented by two floats, the color as 32 bit rgba and the uv also as intgers.
|
||||
// color and uv coordinates. Since this is a 2D pixel-based renderer the position
|
||||
// is represented by two floats, the color as 32 bit rgba and the uv also as intgers.
|
||||
.vertex_input_state = {
|
||||
// the description of each vertex buffer, for now I use only one buffer
|
||||
.vertex_buffer_descriptions = (GPUVertexBufferDescription[]){
|
||||
@ -509,15 +467,9 @@ fn void Renderer.create_pipeline(&self, String shader_name, PipelineType type)
|
||||
.buffer_slot = 1,
|
||||
.format = GPU_VERTEXELEMENTFORMAT_UBYTE4,
|
||||
.offset = QuadAttributes.color.offsetof,
|
||||
},
|
||||
{ // at location four there is the quad type
|
||||
.location = 4,
|
||||
.buffer_slot = 1,
|
||||
.format = GPU_VERTEXELEMENTFORMAT_UINT,
|
||||
.offset = QuadAttributes.type.offsetof,
|
||||
}
|
||||
},
|
||||
.num_vertex_attributes = 5,
|
||||
.num_vertex_attributes = 4,
|
||||
},
|
||||
// the pipeline's primitive type and rasterizer state differs based on what needs to
|
||||
// be drawn
|
||||
@ -561,21 +513,12 @@ fn void Renderer.create_pipeline(&self, String shader_name, PipelineType type)
|
||||
self.pipelines.push(p);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================================== //
|
||||
// TEXTURE LOADING //
|
||||
// ============================================================================================== //
|
||||
|
||||
|
||||
|
||||
// NOTE: with TEXTUREUSAGE_SAMPLER the texture format cannot be intger _UINT so it has to be nermalized
|
||||
enum TextureType : (GPUTextureFormat format) {
|
||||
FULL_COLOR = GPU_TEXTUREFORMAT_R8G8B8A8_UNORM,
|
||||
JUST_ALPHA = GPU_TEXTUREFORMAT_R8_UNORM
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// This macro wraps new_texture_by_id() by accepting either a name or an id directly
|
||||
macro void Renderer.new_texture(&self, name_or_id, TextureType type, char[] pixels, uint width, uint height)
|
||||
{
|
||||
$switch $typeof(name_or_id):
|
||||
@ -585,8 +528,6 @@ macro void Renderer.new_texture(&self, name_or_id, TextureType type, char[] pixe
|
||||
$endswitch
|
||||
}
|
||||
|
||||
|
||||
// This macro wraps update_texture_by_id() by accepting either a name or an id directly
|
||||
macro void Renderer.update_texture(&self, name_or_id, char[] pixels, uint width, uint height, uint x = 0, uint y = 0)
|
||||
{
|
||||
$switch $typeof(name_or_id):
|
||||
@ -597,11 +538,10 @@ macro void Renderer.update_texture(&self, name_or_id, char[] pixels, uint width,
|
||||
}
|
||||
|
||||
|
||||
/* Create a new gpu texture from a pixel buffer, the format has to be specified.
|
||||
* The new texture is given an id and pushed into the renderer's texture list.
|
||||
*/
|
||||
// create a new gpu texture from a pixel buffer, the format has to be specified
|
||||
// the new texture s given an id and pushed into a texture list
|
||||
fn void Renderer.new_texture_by_id(&self, Id id, TextureType type, char[] pixels, uint width, uint height)
|
||||
{
|
||||
{
|
||||
// the texture description
|
||||
GPUTextureCreateInfo tci = {
|
||||
.type = GPU_TEXTURETYPE_2D,
|
||||
@ -647,11 +587,6 @@ fn void Renderer.new_texture_by_id(&self, Id id, TextureType type, char[] pixels
|
||||
self.update_texture_by_id(id, pixels, width, height, 0, 0);
|
||||
}
|
||||
|
||||
|
||||
/* Updates a texture on the gpu.
|
||||
* pixels: the pixel array that contais the texture content
|
||||
* width, height: the size of the
|
||||
*/
|
||||
fn void Renderer.update_texture_by_id(&self, Id id, char[] pixels, uint width, uint height, uint x, uint y)
|
||||
{
|
||||
Texture* t = self.textures.get_from_id(id);
|
||||
@ -705,34 +640,23 @@ fn void Renderer.update_texture_by_id(&self, Id id, char[] pixels, uint width, u
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================================== //
|
||||
// RENDER COMMANDS //
|
||||
// ============================================================================================== //
|
||||
|
||||
const uint TYPE_RECT = 0;
|
||||
const uint TYPE_FONT = 1;
|
||||
const uint TYPE_SPRITE = 2;
|
||||
const uint TYPE_MSDF = 3;
|
||||
|
||||
fn bool Renderer.push_sprite(&self, short x, short y, short w, short h, short u, short v, uint color = 0xffffffff, uint type)
|
||||
fn bool Renderer.push_sprite(&self, short x, short y, short w, short h, short u, short v, uint color = 0xffffffff)
|
||||
{
|
||||
QuadAttributes qa = {
|
||||
.pos = {.x = x, .y = y, .w = w, .h = h},
|
||||
.uv = {.u = u, .v = v},
|
||||
.color = color,
|
||||
.type = type,
|
||||
.color = color
|
||||
};
|
||||
|
||||
return self.map_quad(qa);
|
||||
}
|
||||
|
||||
fn bool Renderer.push_quad(&self, short x, short y, short w, short h, uint color, ushort radius, uint type)
|
||||
fn bool Renderer.push_quad(&self, short x, short y, short w, short h, uint color, ushort radius = 0, ushort thickness = 0)
|
||||
{
|
||||
QuadAttributes qa = {
|
||||
.pos = {.x = x, .y = y, .w = w, .h = h},
|
||||
.uv = {.u = radius, .v = radius},
|
||||
.color = color,
|
||||
.type = type
|
||||
.uv = {.u = radius, .v = radius+thickness},
|
||||
.color = color
|
||||
};
|
||||
|
||||
return self.map_quad(qa);
|
||||
@ -770,7 +694,7 @@ fn void Renderer.upload_quads(&self)
|
||||
GPUCopyPass* cpy = sdl::begin_gpu_copy_pass(cmd);
|
||||
|
||||
// upload quad attributes
|
||||
sdl::upload_to_gpu_buffer(cpy,
|
||||
sdl::upload_to_gpu_buffer(cpy,
|
||||
&&(GPUTransferBufferLocation){.transfer_buffer = qb.attr_ts, .offset = 0},
|
||||
&&(GPUBufferRegion){.buffer = qb.attr_buf, .offset = 0, .size = QuadAttributes.sizeof * qb.count},
|
||||
false
|
||||
@ -822,7 +746,7 @@ fn void Renderer.begin_render(&self, bool clear_screen)
|
||||
sdl::push_gpu_fragment_uniform_data(self.render_cmdbuf, 0, &v, ViewsizeUniform.sizeof);
|
||||
|
||||
if (clear_screen) {
|
||||
GPURenderPass* pass = sdl::begin_gpu_render_pass(self.render_cmdbuf,
|
||||
GPURenderPass* pass = sdl::begin_gpu_render_pass(self.render_cmdbuf,
|
||||
&&(GPUColorTargetInfo){
|
||||
.texture = self.swapchain_texture,
|
||||
.mip_level = 0,
|
||||
@ -847,17 +771,15 @@ fn void Renderer.begin_render(&self, bool clear_screen)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn void Renderer.end_render(&self)
|
||||
{
|
||||
sdl::submit_gpu_command_buffer(self.render_cmdbuf);
|
||||
self.reset_quads();
|
||||
}
|
||||
|
||||
|
||||
fn void Renderer.start_render_pass(&self, String pipeline_name)
|
||||
{
|
||||
self.render_pass = sdl::begin_gpu_render_pass(self.render_cmdbuf,
|
||||
self.render_pass = sdl::begin_gpu_render_pass(self.render_cmdbuf,
|
||||
&&(GPUColorTargetInfo){
|
||||
.texture = self.swapchain_texture,
|
||||
.mip_level = 0,
|
||||
@ -888,43 +810,19 @@ fn void Renderer.start_render_pass(&self, String pipeline_name)
|
||||
sdl::bind_gpu_graphics_pipeline(self.render_pass, p);
|
||||
}
|
||||
|
||||
|
||||
fn void Renderer.end_render_pass(&self)
|
||||
{
|
||||
sdl::end_gpu_render_pass(self.render_pass);
|
||||
sdl::end_gpu_render_pass(self.render_pass);
|
||||
}
|
||||
|
||||
|
||||
fn void Renderer.bind_textures(&self, String... texture_names)
|
||||
fn void Renderer.bind_texture(&self, String texture_name)
|
||||
{
|
||||
// TODO: bind in one pass
|
||||
foreach (idx, name: texture_names) {
|
||||
ren::Texture* tx = self.textures.get_from_name(name);
|
||||
if (tx == null) {
|
||||
unreachable("texture '%s' was not registered", name);
|
||||
}
|
||||
sdl::bind_gpu_fragment_samplers(self.render_pass, (uint)idx,
|
||||
(GPUTextureSamplerBinding[]){{.texture = tx.texture, .sampler = tx.sampler}}, 1
|
||||
);
|
||||
}
|
||||
ren::Texture* tx = self.textures.get_from_name(texture_name);
|
||||
sdl::bind_gpu_fragment_samplers(self.render_pass, 0,
|
||||
(GPUTextureSamplerBinding[]){{.texture = tx.texture, .sampler = tx.sampler}}, 1
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
fn void Renderer.bind_textures_id(&self, ugui::Id... texture_ids)
|
||||
{
|
||||
// TODO: bind in one pass
|
||||
foreach (idx, id: texture_ids) {
|
||||
ren::Texture* tx = self.textures.get_from_id(id);
|
||||
if (tx == null) {
|
||||
unreachable("texture [%d] was not registered", id);
|
||||
}
|
||||
sdl::bind_gpu_fragment_samplers(self.render_pass, (uint)idx,
|
||||
(GPUTextureSamplerBinding[]){{.texture = tx.texture, .sampler = tx.sampler}}, 1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn void Renderer.set_scissor(&self, int x, int y, int w, int h)
|
||||
{
|
||||
// in vulkan scissor size must be positive, clamp to zero
|
||||
@ -954,10 +852,10 @@ fn void Renderer.reset_scissor(&self)
|
||||
* - draw
|
||||
* - push uniform
|
||||
* - draw
|
||||
*
|
||||
*
|
||||
* 2. The GPU buffers are read per-command-buffer and not per
|
||||
* render pass. So I cannot override an element in the buffer
|
||||
* before submitting the command buffer.
|
||||
* before submitting the command buffer.
|
||||
*/
|
||||
/// === END NOTES ===
|
||||
|
||||
@ -968,31 +866,29 @@ fn void Renderer.render_ugui(&self, CmdQueue* queue)
|
||||
foreach (&c : queue) {
|
||||
if (c.type == CMD_RECT) {
|
||||
CmdRect r = c.rect;
|
||||
self.push_quad(r.rect.x, r.rect.y, r.rect.w, r.rect.h, r.color.to_uint(), r.radius, TYPE_RECT);
|
||||
self.push_quad(r.rect.x, r.rect.y, r.rect.w, r.rect.h, r.color.to_uint(), r.radius, r.thickness);
|
||||
} else if (c.type == CMD_SPRITE) {
|
||||
CmdSprite s = c.sprite;
|
||||
uint type;
|
||||
if (s.texture_id == self.font_atlas_id) {
|
||||
type = TYPE_FONT;
|
||||
} else if (s.texture_id == self.sprite_atlas_id && s.type == SPRITE_NORMAL) {
|
||||
type = TYPE_SPRITE;
|
||||
} else if (s.texture_id == self.sprite_atlas_id && s.type == SPRITE_MSDF) {
|
||||
type = TYPE_MSDF;
|
||||
} else {
|
||||
unreachable("unrecognized command type");
|
||||
}
|
||||
self.push_sprite(s.rect.x, s.rect.y, s.texture_rect.w, s.texture_rect.h, s.texture_rect.x, s.texture_rect.y, s.hue.to_uint(), type);
|
||||
self.push_sprite(s.rect.x, s.rect.y, s.texture_rect.w, s.texture_rect.h, s.texture_rect.x, s.texture_rect.y, s.hue.to_uint());
|
||||
}
|
||||
}
|
||||
self.upload_quads();
|
||||
|
||||
self.start_render_pass("UGUI_PIPELINE");
|
||||
self.bind_textures_id(self.font_atlas_id, self.sprite_atlas_id);
|
||||
|
||||
uint calls = 0;
|
||||
Cmd* last_command;
|
||||
uint off;
|
||||
uint count;
|
||||
for (Cmd* cmd; (cmd = queue.dequeue() ?? null) != null;) {
|
||||
if (last_command == null || last_command.type != cmd.type) {
|
||||
self.end_command(last_command, off, count);
|
||||
self.begin_command(cmd);
|
||||
off += count;
|
||||
count = 0;
|
||||
}
|
||||
|
||||
switch (cmd.type) {
|
||||
case CMD_RECT: nextcase;
|
||||
case CMD_SPRITE:
|
||||
count++;
|
||||
case CMD_UPDATE_ATLAS:
|
||||
// TODO: verify the correct type
|
||||
CmdUpdateAtlas u = cmd.update_atlas;
|
||||
@ -1007,20 +903,60 @@ fn void Renderer.render_ugui(&self, CmdQueue* queue)
|
||||
self.scissor_y = s.y;
|
||||
self.scissor_w = s.w;
|
||||
self.scissor_h = s.h;
|
||||
default:
|
||||
self.set_scissor(self.scissor_x, self.scissor_y, self.scissor_w, self.scissor_h);
|
||||
uint count = 1;
|
||||
while (queue.len() != 0 && (queue.get(0).type == CMD_RECT || queue.get(0).type == CMD_SPRITE)) {
|
||||
count++;
|
||||
(void)queue.dequeue();
|
||||
}
|
||||
self.draw_quads(off, count);
|
||||
off += count;
|
||||
calls++;
|
||||
// self.draw_quads(off++, 1);
|
||||
default: unreachable("unknown command: %s", cmd.type);
|
||||
}
|
||||
|
||||
last_command = cmd;
|
||||
}
|
||||
self.end_render_pass();
|
||||
// ugui::println("calls: ", calls);
|
||||
self.end_command(last_command, off, count);
|
||||
}
|
||||
|
||||
fn void Renderer.begin_command(&self, Cmd* cmd)
|
||||
{
|
||||
if (cmd == null) return;
|
||||
|
||||
switch (cmd.type) {
|
||||
case CMD_RECT:
|
||||
self.start_render_pass("UGUI_PIPELINE_RECT");
|
||||
self.set_scissor(self.scissor_x, self.scissor_y, self.scissor_w, self.scissor_h);
|
||||
case CMD_SPRITE:
|
||||
// TODO: support multiple sprite and font atlases
|
||||
CmdSprite s = cmd.sprite;
|
||||
String pipeline;
|
||||
String texture;
|
||||
if (s.texture_id == self.sprite_atlas_id) {
|
||||
switch (s.type) {
|
||||
case SPRITE_NORMAL: pipeline = "UGUI_PIPELINE_SPRITE";
|
||||
case SPRITE_SDF: pipeline = "UGUI_PIPELINE_SPRITE_SDF";
|
||||
case SPRITE_MSDF: pipeline = "UGUI_PIPELINE_SPRITE_MSDF";
|
||||
case SPRITE_ANIMATED: unreachable("animated sprtes are unsupported for now");
|
||||
default: unreachable("unknown sprite type %s", s.type);
|
||||
}
|
||||
texture = "icons";
|
||||
} else if (s.texture_id == self.font_atlas_id) {
|
||||
pipeline = "UGUI_PIPELINE_FONT";
|
||||
texture = "font1";
|
||||
}
|
||||
self.start_render_pass(pipeline);
|
||||
self.bind_texture(texture);
|
||||
self.set_scissor(self.scissor_x, self.scissor_y, self.scissor_w, self.scissor_h);
|
||||
case CMD_UPDATE_ATLAS: break;
|
||||
case CMD_SCISSOR: break;
|
||||
default: unreachable("unknown command: %s", cmd.type);
|
||||
}
|
||||
}
|
||||
|
||||
fn void Renderer.end_command(&self, Cmd* cmd, uint off, uint count)
|
||||
{
|
||||
if (cmd == null) return;
|
||||
|
||||
switch (cmd.type) {
|
||||
case CMD_RECT: nextcase;
|
||||
case CMD_SPRITE:
|
||||
self.draw_quads(off, count);
|
||||
self.end_render_pass();
|
||||
case CMD_UPDATE_ATLAS: break;
|
||||
case CMD_SCISSOR: break;
|
||||
default: unreachable("unknown command: %s", cmd.type);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -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++;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user