Compare commits

...

136 Commits
rewrite2 ... c3

Author SHA1 Message Date
e8bb35811a renamed position_element() to layout_element() 2025-07-14 13:16:04 +02:00
278e4988e9 use containing_rect() in position_element() 2025-07-14 13:10:14 +02:00
78fc1c1e87 cleaner get_parent() 2025-07-14 13:05:30 +02:00
8d79f13fd6 added another demo ui 2025-07-14 12:59:07 +02:00
7713cd7da9 prettier radius 2025-07-14 12:58:15 +02:00
00aa01109e crash if last element is not root 2025-07-14 12:57:53 +02:00
5e68671828 fixed divs-in-divs 2025-07-14 12:57:29 +02:00
8367f6b617 better css lexer 2025-07-13 21:43:57 +02:00
dd073385c8 fix slider styling 2025-07-13 20:12:48 +02:00
80d17d7b33 unified button element 2025-07-13 20:08:18 +02:00
b48c413d2d changed text (glyph) placement 2025-07-10 11:05:39 +02:00
c1c1247af4 update TODO 2025-07-07 11:46:40 +02:00
5ae9b05223 moved style to style.css 2025-07-07 11:37:56 +02:00
9afb0d2acd better default style handling 2025-07-06 23:50:36 +02:00
c1bf8e891b fixed regression in scrollbars in divs 2025-07-06 01:41:57 +02:00
777e974841 use new style system 2025-07-05 16:37:08 +02:00
9fc1d90455 simple style import with a subset of css 2025-07-04 11:17:42 +02:00
a390a8908c changed Id to uint since builtin hash functions hash to uints 2025-07-01 16:48:30 +02:00
c49a689304 get_elem now also pushes into the tree and check the correct type 2025-07-01 16:03:11 +02:00
849b267f91 renamed get_element_by_tree_idx to get_active_div 2025-07-01 15:33:22 +02:00
1de4fd78b8 better checkboxes 2025-07-01 15:28:08 +02:00
586e81935c improved the api to not require explicit labels everywhere 2025-06-30 18:24:50 +02:00
972c9b581d text input box 2025-06-30 13:10:00 +02:00
6d2594db2d test id generation with macros 2025-06-30 13:08:52 +02:00
9827a899f1 get_line_height and get_cursor_position 2025-06-30 13:08:27 +02:00
88d9b65028 updated build configuration 2025-06-30 13:07:42 +02:00
c98c00bfb9 ecode editor config 2025-06-25 10:47:44 +02:00
fc3fa32ddd more smoothing on rounded corners 2025-06-25 10:47:23 +02:00
cd83c528ee fixed vulkan validation errors 2025-06-19 15:24:46 +02:00
5b0169cd94 better input handling 2025-06-17 18:23:36 +02:00
b411718c94 add fps counter 2025-06-15 23:47:09 +02:00
05232c1d24 do a single upload pass to reduce badwidth 2025-06-15 23:35:37 +02:00
0223536ac8 Merge branch 'c3-instanced' into c3 2025-06-15 20:54:57 +02:00
f30db0dd47 implemented instanced rendering 2025-06-15 18:54:35 +02:00
865c7dabaa indirect rendering 2025-06-15 00:26:27 +02:00
0ef2bceeec handle scissor and vsync 2025-06-14 15:09:34 +02:00
cda2f27a49 Batch uploads to the gpu 2025-06-14 14:49:53 +02:00
10ee643a0c disable vsync 2025-06-12 20:01:33 +02:00
00f5e71666 enable cycling when mapping the transfer buffers to avoid corruption 2025-06-12 19:56:43 +02:00
458c45d2b9 using the new renderer 2025-06-12 19:49:43 +02:00
c4a3dd3a26 tweak ugui api, add some usefult functions 2025-06-12 18:56:57 +02:00
2014c67bfd tweak renderer API 2025-06-12 18:56:20 +02:00
177e52b0d0 rounded quads baby! 2025-06-10 12:54:03 +02:00
3a0904023a updated sdl3.c3l location 2025-06-10 12:53:52 +02:00
c4c9716d61 remove mqoi dependency in ugui manifest 2025-06-09 16:35:23 +02:00
6208711292 use the language's qoi decoder 2025-06-09 16:06:49 +02:00
39bd7fb8bc removed module raylib.c3l (non vendor) 2025-06-09 15:51:16 +02:00
47eb3ff907 moved vendor libraries to lib/ as a submodule 2025-06-09 15:50:29 +02:00
21aa70d340 create binary directory before copying 2025-06-09 15:10:38 +02:00
bd8c73ecd5 sdl3.c3l as a submodule 2025-06-09 12:30:25 +02:00
6e65700f38 scary quads and nice sprites 2025-06-07 12:42:57 +02:00
e3d87525d4 draw multiple quads 2025-06-07 10:35:08 +02:00
3002123ef7 removed bad error handling and replaced it with worse error handling 2025-06-03 22:36:18 +02:00
ac3fcae649 sorting the command buffer 2025-06-03 22:03:14 +02:00
c9b74aebc7 implement z index in command buffer 2025-06-03 18:15:46 +02:00
f344c989db test different draw calls for one render pass (failed) 2025-06-03 18:15:33 +02:00
6c5acd6f23 enable foreach for FIFO 2025-06-03 18:15:12 +02:00
24bc2c67bc changed the pipeline to use 16 bit int as coords
thank you renderdoc for making me feel less stupid
2025-06-03 09:16:51 +02:00
712ce50631 A lot of work
* moved all ugui code to lib/ugui.c3l and made it a library/module
* started work on a sdl3 renderer, with shaders etc
* added the new sdl3.c3l library as a dependency
* makefile is for the renderer
2025-06-01 16:44:31 +02:00
2380c7693c add sdl3 dependency 2025-05-21 23:38:59 +02:00
79a2d66880 update project to c3 0.7.1 2025-05-05 16:23:26 +02:00
34e75f8c06 larger font cache 2025-02-08 12:51:10 +01:00
7c6f7d31d2 quick and dirty checkbox 2025-02-07 23:46:21 +01:00
52f3929a42 renamed ATlAS_RGBA32 to ATLAS_R8G8B8A8 2025-02-06 23:51:15 +01:00
e09107af98 simpler main 2025-02-06 23:44:03 +01:00
588a417413 checkbox and msdf sprite rendering 2025-02-04 22:40:44 +01:00
14359a9b7e first checkbox 2025-02-03 23:07:32 +01:00
196a2474fd todos 2025-02-03 23:07:14 +01:00
c53b9eed5e MAYBE correct layout with next_row and next_column 2025-02-01 01:01:13 +01:00
92614e4d8b removed comments 2025-01-31 23:15:40 +01:00
d94e430807 added library submodules 2025-01-31 12:37:15 +01:00
de64746fdf renamed libraries 2025-01-31 12:20:19 +01:00
0531f58a56 less cached elements by default 2025-01-30 22:27:46 +01:00
f516a68cee correct font atlas size 2025-01-30 22:27:24 +01:00
07857fcd44 switch to c3s' vendor raylib 5.5 2025-01-30 19:38:53 +01:00
fbe631b4b4 better sprites 2025-01-30 18:36:47 +01:00
b317951c32 first working prototype of sprite drawing 2025-01-29 01:10:18 +01:00
9aa0d58d68 better to_rgba() macro 2024-12-28 16:59:12 +01:00
78e2c64da6 implemented adaptive size divs 2024-12-26 22:58:43 +01:00
16adfd7cc5 idk look at the changes 2024-12-25 12:30:35 +01:00
1746f7d940 update TODO 2024-12-23 15:56:40 +01:00
169b5e1dfd cull commands that result in zero-area bounding boxes 2024-12-23 15:49:46 +01:00
87de68028a update todo 2024-12-20 20:58:12 +01:00
04843fe714 do not use xor to combine keys since when more than two layers deep this would result in identical keys 2024-12-20 20:17:53 +01:00
a0c6a3b2cb work on button with label 2024-12-20 19:50:58 +01:00
ca691d1294 enlarge scrollbars when focused 2024-12-20 02:17:41 +01:00
f7985f8c7f track focused and hovered elements 2024-12-20 01:45:10 +01:00
4bd827ce5c set ids <facepalm> 2024-12-19 19:42:02 +01:00
d31b4eab53 update todo 2024-12-19 15:27:14 +01:00
1088083e1e stuff, mostly renaming variables 2024-12-19 15:24:39 +01:00
601a396aa8 scrollbar fixup 2024-12-19 00:29:30 +01:00
a481269022 work on div sliders, major changes
* Ids are now keyed based on the parent's id, this means that an element can have
  the same label when placed in different divs
* Divs now enable the scissor test, this way the elements cannot draw outside of
  the parent div bounds
* Introduced a LAYOUT_ABSOLUTE that disables all layout logic, for internal use
* Divs now draw scrollbars using the slider_hor and slider_ver elements
2024-12-18 20:04:23 +01:00
499f6dc79b removed force update 2024-12-18 15:10:17 +01:00
740ea0c6be merged ugui_impl and ugui_data to ugui_core 2024-12-18 15:02:46 +01:00
8d4b353e88 a lot of work on sliders 2024-12-18 14:58:40 +01:00
c0e9565bf6 idk some stuff 2024-12-17 11:26:59 +01:00
c1a7b4fcdb display timing statistics 2024-12-16 17:06:46 +01:00
7d9a8a1363 pre-cache ascii range in font atlas 2024-12-16 17:06:33 +01:00
2e0c6333d3 draw border around floating divs 2024-12-16 17:06:16 +01:00
3a7655a3f0 specify texture id in the sprite command 2024-12-16 17:05:44 +01:00
7b7aac8df4 schrift use ZString where necessary 2024-12-16 14:08:08 +01:00
2e60e4c5b8 fix font alpha channel 2024-12-16 14:07:44 +01:00
bca29c537c scissor command 2024-12-15 22:29:07 +01:00
6d8300f9d9 better font atlas implementation 2024-12-15 21:39:26 +01:00
5a89e9ec7d fixed formatting fuckups offered by the zed team 2024-12-14 13:29:45 +01:00
0db858e814 use libgrapheme to interpret utf8 encoded strings 2024-12-14 01:12:54 +01:00
8cf3881b6b added libgrapheme 2024-12-13 16:44:07 +01:00
373243d138 fix rect roundness 2024-12-13 14:42:15 +01:00
3070fac9f5 cull zero area rects 2024-12-13 14:41:54 +01:00
5c687bd24e add push_sprite() 2024-12-13 14:05:07 +01:00
c880c2b26e correct text bounds 2024-12-13 13:56:02 +01:00
089140e1ed substitute enqueue() with push_rect() 2024-12-13 13:51:23 +01:00
fb177c03f7 some todos 2024-12-12 15:46:40 +01:00
328cac871a started work on border radius 2024-12-12 15:46:24 +01:00
f0aa59ef0b even better text rendering 2024-12-11 22:25:53 +01:00
61556d0a2c forgot the baseline 2024-12-11 20:39:06 +01:00
2356d165fe somewhat functional text rendering 2024-12-11 01:14:14 +01:00
dbe70eb4f4 added library schrift 2024-12-06 22:03:35 +01:00
f86a360f39 correct slider handle placement 2024-12-02 18:48:30 +01:00
7e18c7a316 only reset div elements to default if new element 2024-12-01 00:28:08 +01:00
537acd4765 added a way to force a relayout 2024-12-01 00:26:56 +01:00
d5bea68058 report timings 2024-12-01 00:25:43 +01:00
574a1f23dc some tests 2024-11-21 00:50:42 +01:00
f8e2c0b70c more TODO 2024-11-21 00:50:25 +01:00
9a785e0f06 use builtin hash functions instead of rolling my own 2024-11-21 00:50:11 +01:00
bb1745a05d use map::HashMap instead of map::Map and use clz() to find a free spot 2024-11-21 00:46:27 +01:00
04dff26067 reduced maximum elements to 1024 2024-11-21 00:45:50 +01:00
fa3362cc66 wait for events to reduce cpu usage 2024-11-20 17:12:01 +01:00
73bc933eb5 semi-working vertical slider 2024-11-17 23:36:12 +01:00
763e9ba8d6 correct placement and box model 2024-11-14 23:42:20 +01:00
250a0fb3b5 initial work on scrollable divs 2024-11-07 18:35:20 +01:00
8bc38452b3 renamed elem.rect to elem.bounds 2024-11-02 09:44:53 +01:00
1cad13e597 vertical slider 2024-11-02 09:41:54 +01:00
28598f0575 slider 2024-10-31 13:04:30 +01:00
f48151b38e initial refactor 2024-10-31 00:04:49 +01:00
39e78ea078 ported rewrite2 to c3 2024-10-29 22:45:47 +01:00
89 changed files with 5774 additions and 2911 deletions

View File

@ -1,41 +0,0 @@
# linux kernel style formatting
BasedOnStyle: LLVM
IndentWidth: 8
UseTab: AlignWithSpaces
BreakBeforeBraces: Linux
AllowShortIfStatementsOnASingleLine: false
IndentCaseLabels: false
ColumnLimit: 85
InsertBraces: true
SortIncludes: Never
BinPackParameters: false
BinPackArguments: false
Cpp11BracedListStyle: true
SpaceBeforeCpp11BracedList: true
SeparateDefinitionBlocks: Always
AlignAfterOpenBracket: BlockIndent
InsertNewlineAtEOF: true
AlignConsecutiveDeclarations:
Enabled: true
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: true
PadOperators: false
AlignConsecutiveMacros:
Enabled: true
AcrossEmptyLines: false
AcrossComments: true
AlignConsecutiveBitFields:
Enabled: true
AcrossEmptyLines: false
AcrossComments: true
AlignConsecutiveAssignments:
Enabled: true
AcrossEmptyLines: false
AcrossComments: true

43
.ecode/project_build.json Normal file
View File

@ -0,0 +1,43 @@
{
"Debug": {
"build": [
{
"args": "-C resources/shaders",
"command": "make",
"working_dir": ""
},
{
"args": "build -g",
"command": "c3c",
"working_dir": ""
}
],
"build_types": [],
"clean": [
{
"args": "clean",
"command": "c3c",
"working_dir": ""
}
],
"config": {
"clear_sys_env": false
},
"os": [
"linux"
],
"output_parser": {
"config": {
"relative_file_paths": true
}
},
"run": [
{
"args": "",
"command": "build/ugui",
"name": "Custom Executable",
"working_dir": ""
}
]
}
}

14
.gitignore vendored
View File

@ -1,8 +1,8 @@
microgui
ugui
*.o
test/test
**/compile_commands.json
**/.cache
test
raylib/*
*.a
build/*
**/.ccls-cache
perf.data*
*.rdc
test_renderer
resources/shaders/compiled/**

16
.gitmodules vendored Normal file
View File

@ -0,0 +1,16 @@
[submodule "lib/grapheme.c3l/thirdparty/libgrapheme"]
path = lib/grapheme.c3l/thirdparty/libgrapheme
url = git://git.suckless.org/libgrapheme
ignore = dirty
[submodule "lib/schrift.c3l/thirdparty/libschrift"]
path = lib/schrift.c3l/thirdparty/libschrift
url = https://github.com/tomolt/libschrift
ignore = dirty
[submodule "lib/sdl3.c3l"]
path = lib/sdl3.c3l
url = https://git.alemauri.eu/alema/sdl3.c3l
ignore = dirty
[submodule "lib/vendor"]
path = lib/vendor
url = https://github.com/c3lang/vendor
ignore = dirty

0
LICENSE Normal file
View File

View File

@ -1,15 +1,3 @@
CFLAGS = -Wall -Wextra -pedantic -std=c11 -g -Iraylib/include
CC = gcc
LDFLAGS = -Lraylib/lib -lm
all: ugui
ugui: ugui.o vectree.o cache.o timer.o raylib/lib/libraylib.a
ugui.o: ugui.c ugui.h
vectree.o: vectree.c ugui.h
cache.o: cache.c ugui.h
timer.o: timer.c timer.h
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

0
README.md Normal file
View File

127
TODO Normal file
View File

@ -0,0 +1,127 @@
# TODOs, semi-random sorting
[x] Implement glyph draw command
[x] Implement div.view and scrollbars
[x] Port font system from C to C3 (rewrite1)
[ ] Update ARCHITECTURE.md
[ ] Write a README.md
[ ] Use an arena allocator for cache
[ ] Do not redraw if there was no update (no layout and no draw)
[ ] Do command buffer damage tracking based on a context grid (see rxi writeup)
[x] Better handling of the active and focused widgets, try
to maintain focus until mouse release (fix scroll bars)
[x] Clip element bounds to parent div, specifically text
[ ] Resizeable divs
[x] Implement a z index and sort command buffer based on that
[ ] Ctx.set_z_index()
[x] Sort command buffer on insertion
[x] Standardize element handling, for example all buttons do almost the same thing, so write a lot
of boiler plate and reuse it
[x] The id combination in gen_id() uses an intger division, which is costly, use another combination
function that is non-linear and doesn't use division
[ ] Animations, somehow
[x] 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
[x] .qoi
[ ] .ff
[ ] .bmp
[ ] .png
[ ] .jpg
[ ] gif support?
[ ] layout_set_max_rows() and layout_set_max_columns()
[x] Maybe SDF sprites??
[x] Stylesheets and stylesheet import
[x] use SDF to draw anti-aliased rounded rectangles https://zed.dev/blog/videogame
[ ] Subdivide modules into ugui::ug for exported functions and ugui::core for
internal use functions (used to create widgets)
[x] The render loop RAPES the gpu, valve pls fix
[ ] The way the element structures are implemented wastes a lot of memory since
each struct Elem, struct Cmd, etc. is as big as the largest element. It would
be better to use a different allcation strategy.
[ ] Add a way to handle time events like double clicks
[x] Fix how padding is applied in push_rect. In CSS padding is applied between the border and the
content, the background color is applied starting from the border. Right now push_rect() offsets
the background rect by both border and padding
[ ] Investigate why the debug pointer (cyan rectangle) disappears...
## Layout
[x] Flexbox
[ ] 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)
[ ] Consider a multi-pass recursive approach to layout (like https://github.com/nicbarker/clay)
instead of the curren multi-frame approach.
[ ] Implement column/row sizing (min, max)
[ ] Find a way to concile pixel measurements to the mm ones used in css, for example in min/max sizing
of elements
[ ] 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()
[ ] Make functions to mark rows/columns as full, to fix the calculator demo
## Input
[x] Keyboard input
[x] Mouse scroll wheel
[ ] 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
[x] rect commads should have:
- border width
- 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
draw_text(a, pos) -> off = compute_bounds(a) -> draw_text(b, pos+off) -> ...
[ ] Rounded rectangle with different radius for each corner
## Atlas
[ ] Add an interface to create, destroy, update and get atlases based on their ids
[ ] Implement multiple font atlases
[ ] Pixel format conversion
## Fonts
[x] Fix the missing alpha channel
[x] Fix the alignment
## Widgets
[x] Dynamic text box to implement an fps counter
[x] Button with label
[x] Text Input box
[ ] Icon Buttons
[x] Switch
[x] Checkbox
[ ] Selectable text box
## API
[ ] Introduce a Layout structure that specifies the positioning of elements inside
a Div element. This would allow specifying alignment, maximum and minimum sizing
margins between children, etc.
This is different from style which is applied per-element.
[ ] Remove Ids for element that don't need them. Such elements are button, toggles,
and all elements which do not have internal data that has to be cached and/or
queried by the user for later use. This allows for smaller caches and in general
reduces some load, since most of the stuff is recomputed for every frame.
## SDL3 Renderer
[x] smart batching
[x] maybe use instancing since we are always drawing the same geometry. With instancing every
different quad could have its coulour, border and radius with much better performance than
issuing a draw call for every quad (and uploading it)
https://rastertek.com/dx11win10tut48.html
https://www.braynzarsoft.net/viewtutorial/q16390-33-instancing-with-indexed-primitives
[ ] implement min and max fps

211
cache.c
View File

@ -1,211 +0,0 @@
// LRU cache:
/*
* The cache uses a pool (array) containing all the elements and a hash table
* associating the position in the pool with the element id
*/
#include <stdlib.h>
#include <string.h>
#include "ugui.h"
// To have less collisions TABLE_SIZE has to be larger than the cache size
#define CACHE_SIZE 265
#define TABLE_SIZE (CACHE_SIZE * 1.5f)
#define CACHE_NCYCLES (CACHE_SIZE * 2 / 3)
#define CACHE_BSIZE (((CACHE_SIZE + 0x3f) & (~0x3f)) >> 6)
#define CACHE_BRESET(b) \
for (int i = 0; i < CACHE_BSIZE; b[i++] = 0) \
;
#define CACHE_BSET(b, x) b[(x) >> 6] |= (uint64_t)1 << ((x)&63)
#define CACHE_BTEST(b, x) (b[(x) >> 6] & ((uint64_t)1 << ((x)&63)))
/* Hash Table Implementation ----------------------------------------------------- */
#define HASH_MAXSIZE 4096
// hash table (id -> cache index)
typedef struct {
UgId id;
uint32_t index;
} IdElem;
typedef struct _IdTable {
uint32_t items, size, exp;
IdElem bucket[];
} IdTable;
IdTable *table_create(uint32_t size)
{
if (!size || size > HASH_MAXSIZE) {
return NULL;
}
/* round to the greater power of two */
/* FIXME: check for intger overflow here */
uint32_t exp = 32 - __builtin_clz(size - 1);
size = 1 << (exp);
/* FIXME: check for intger overflow here */
IdTable *ht = malloc(sizeof(IdTable) + sizeof(IdElem) * size);
if (ht) {
ht->items = 0;
ht->size = size;
memset(ht->bucket, 0, sizeof(IdTable) * size);
}
return ht;
}
void table_destroy(IdTable *ht)
{
if (ht) {
free(ht);
}
}
// Find and return the element by pointer
IdElem *table_search(IdTable *ht, UgId id)
{
if (!ht) {
return NULL;
}
// In this case id is the hash
uint32_t idx = id % ht->size;
for (uint32_t x = 0, i; x < ht->size; x++) {
i = (idx + x) % ht->size;
if (ht->bucket[i].id == 0 || ht->bucket[i].id == id) {
return &(ht->bucket[i]);
}
}
return NULL;
}
// FIXME: this simply overrides the found item
IdElem *table_insert(IdTable *ht, IdElem entry)
{
IdElem *r = table_search(ht, entry.id);
if (r != NULL) {
if (r->id != 0) {
ht->items++;
}
*r = entry;
}
return r;
}
IdElem *table_remove(IdTable *ht, UgId id)
{
if (!ht) {
return NULL;
}
IdElem *r = table_search(ht, id);
if (r) {
r->id = 0;
}
return r;
}
/* Cache Implementation ---------------------------------------------------------- */
// Every CACHE_CYCLES operations mark not-present the unused elements
#define CACHE_CYCLE(c) \
do { \
if (++(c->cycles) > CACHE_NCYCLES) { \
for (int i = 0; i < CACHE_BSIZE; i++) { \
c->present[i] &= c->used[i]; \
c->used[i] = 0; \
} \
c->cycles = 0; \
} \
} while (0)
/* FIXME: check for allocation errors */
UgElemCache ug_cache_init(void)
{
IdTable *t = table_create(TABLE_SIZE);
UgElem *a = malloc(sizeof(UgElem) * CACHE_SIZE);
uint64_t *p = malloc(sizeof(uint64_t) * CACHE_BSIZE);
uint64_t *u = malloc(sizeof(uint64_t) * CACHE_BSIZE);
CACHE_BRESET(p);
CACHE_BRESET(u);
return (UgElemCache) {.table = t, .array = a, .present = p, .used = u, 0};
}
void ug_cache_free(UgElemCache *cache)
{
if (cache) {
table_destroy(cache->table);
free(cache->array);
free(cache->present);
free(cache->used);
}
}
UgElem *ug_cache_search(UgElemCache *cache, UgId id)
{
if (!cache) {
return NULL;
}
IdElem *r;
r = table_search(cache->table, id);
/* MISS */
if (!r || id != r->id) {
return NULL;
}
/* MISS, the data is not valid (not present) */
if (!CACHE_BTEST(cache->present, r->index)) {
return NULL;
}
/* HIT, set as recently used */
CACHE_BSET(cache->used, r->index);
return (&cache->array[r->index]);
}
/* Look for a free spot in the present bitmap and return its index */
/* If there is no free space left then just return the first position */
int ug_cache_get_free_spot(UgElemCache *cache)
{
if (!cache) {
return -1;
}
int x = 0;
for (int b = 0; b < CACHE_BSIZE; b++) {
if (cache->present[b] == 0) {
x = 64;
} else {
x = __builtin_clzll(cache->present[b]);
}
x = 64 - x;
if (!CACHE_BTEST(cache->present, x + 64 * b)) {
return x + 64 * b;
}
}
return 0;
}
UgElem *ug_cache_insert_at(UgElemCache *cache, const UgElem *g, uint32_t index)
{
if (!cache) {
return NULL;
}
UgElem *spot = NULL;
/* Set used and present */
CACHE_BSET(cache->present, index);
CACHE_BSET(cache->used, index);
CACHE_CYCLE(cache);
spot = &(cache->array[index]);
*spot = *g;
IdElem e = {.id = g->id, .index = index};
if (!table_insert(cache->table, e)) {
return NULL;
}
return spot;
}
// Insert an element in the cache
UgElem *ug_cache_insert_new(UgElemCache *cache, const UgElem *g, uint32_t *index)
{
*index = ug_cache_get_free_spot(cache);
return ug_cache_insert_at(cache, g, *index);
}

View File

@ -1,5 +0,0 @@
-Iraylib/include
-Wall
-Wextra
-pedantic
-std=c11

0
docs/.gitkeep Normal file
View File

View File

@ -1 +0,0 @@
File Manager using ugui

View File

@ -1,193 +0,0 @@
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdarg.h>
#include <linux/limits.h>
#include <sys/stat.h>
#include <sys/types.h>
#define PATHS_NO 3
static char temp[PATH_MAX] = {0};
static int valid_paths = 0;
static char paths[PATHS_NO][PATH_MAX] = {0}; // 0: xdg, 1: fallback, 2: global
static const mode_t CONFDIR_MODE = S_IRWXU | S_IRWXU | S_IRWXU | S_IRWXU;
static void err(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
fprintf(stderr, "[libconf]: ");
vfprintf(stderr, fmt, ap);
fprintf(stderr, "\n");
va_end(ap);
}
// implements "mkdir -p"
static int mkdir_p(const char *path)
{
char tmp_path[PATH_MAX] = {0};
strncpy(tmp_path, path, PATH_MAX - 1);
struct stat st;
for (char *tk = strtok(tmp_path, "/"); tk != NULL; tk = strtok(NULL, "/")) {
if (tk != tmp_path) {
tk[-1] = '/';
}
if (stat(tmp_path, &st)) {
if (errno != ENOENT) {
err("could not stat() %s: %s",
tmp_path,
strerror(errno));
return 1;
}
if (mkdir(tmp_path, CONFDIR_MODE)) {
err("could not mkdir() %s: %s",
tmp_path,
strerror(errno));
return 1;
}
} else if (!S_ISDIR(st.st_mode)) {
err("could not create directory %s: file exists but it is "
"not a directory",
tmp_path);
return 1;
}
}
return 0;
}
// TODO: add a way to add a custom directory to the paths, for example via an
// environment variable
// TODO: maybe add a LIBCONF_PATH that overrides the defaults
/* default paths are:
* 1. ${XDG_CONFIG_HOME}/libconf/appid.d/
* 2. ${HOME}/.config/libconf/appid.d/
* 3. etc/libconf/appid.d/
*/
static int fill_paths(const char *id)
{
// TODO: verify id
if (id == NULL) {
err("must provide a valid app id");
return 1;
}
const char *xdg = getenv("XDG_CONFIG_HOME");
if (xdg != NULL) {
snprintf(paths[0], PATH_MAX, "%s/libconf/%s.d", xdg, id);
}
const char *home = getenv("HOME");
if (home) {
snprintf(temp, PATH_MAX, "%s/.config/libconf/%s.d", home, id);
if (mkdir_p(temp) == 0) {
strcpy(paths[1], temp);
}
}
// do not create global config path since most likely we don't have
// the necessary permissions to do so
snprintf(paths[2], PATH_MAX, "/etc/libconf/%s.d", id);
for (size_t i = 0; i < PATHS_NO; i++) {
printf("paths[%ld] = %s\n", i, paths[i]);
}
valid_paths = 1;
return 0;
}
// get config file path
const char *lcnf_get_conf_file(const char *id, const char *name, int global)
{
static char str[PATH_MAX] = {0};
if (id == NULL) {
return NULL;
}
if (!valid_paths) {
if (fill_paths(id)) {
return NULL;
}
}
// quick path for global config file
if (global && paths[2][0] != '\0') {
snprintf(str, PATH_MAX, "%s/%s", paths[2], name);
return str;
}
int found = 0;
for (size_t i = 0; i < PATHS_NO; i++) {
if (paths[i][0] == '\0') {
continue;
}
struct stat st;
snprintf(str, PATH_MAX, "%s/%s", paths[i], name);
if (stat(str, &st) && errno != ENOENT) {
err("could not stat %s: %s", str, strerror(errno));
}
if (S_ISREG(st.st_mode)) {
found = 1;
break;
}
}
return found ? str : NULL;
}
// get config file directory path
const char *lcnf_get_conf_dir(const char *id, int global)
{
if (id == NULL) {
return NULL;
}
if (global) {
return paths[2][0] != '\0' ? paths[2] : NULL;
}
if (paths[0][0] != '\0') {
return paths[0];
} else if (paths[1][0] != '\0') {
return paths[1];
} else {
return NULL;
}
}
// TODO: watch directory for file updates
int lcnf_watch_conf_dir(const char *id, int global);
// TODO: collect all files into a temporary one, useful for when you have multiple
// configuration files like 00-foo.conf, 01-bar.conf and 99-baz.conf
const char *lcnf_collect_conf_files(const char *id, int global);
#if 1
int main(void)
{
const char *p = lcnf_get_conf_file("pippo", "pippo.toml", 0);
if (p) {
printf("config found: %s\n", p);
} else {
printf("config not found\n");
}
return 0;
}
#endif

View File

@ -1,7 +0,0 @@
#ifndef LIBCONF_H_
#define LIBCONF_H_
const char *lcnf_get_conf_file(const char *id, const char *name, int global);
const char *lcnf_get_conf_dir(const char *id, int global);
#endif

View File

@ -1,8 +0,0 @@
#!/bin/sh
mkdir -p raylib
wget https://github.com/raysan5/raylib/releases/download/5.0/raylib-5.0_linux_amd64.tar.gz
tar -xvf raylib-5.0_linux_amd64.tar.gz
mv ./raylib-5.0_linux_amd64/* ./raylib/
rm -rf raylib-5.0_linux_amd64 raylib-5.0_linux_amd64.tar.gz

0
lib/.gitkeep Normal file
View File

View File

@ -0,0 +1,4 @@
all:
make -C thirdparty/libgrapheme
mkdir -p linux-x64
cp thirdparty/libgrapheme/libgrapheme.a linux-x64/libgrapheme.a

View File

@ -0,0 +1,46 @@
module grapheme;
const uint GRAPHEME_INVALID_CODEPOINT = 0xFFFD;
enum BidirectionalDirection {
GRAPHEME_BIDIRECTIONAL_DIRECTION_NEUTRAL,
GRAPHEME_BIDIRECTIONAL_DIRECTION_LTR,
GRAPHEME_BIDIRECTIONAL_DIRECTION_RTL,
}
fn isz bidirectional_get_line_embedding_levels(uint *, isz, ichar *, isz) @extern("grapheme_bidirectional_get_line_embedding_levels");
fn isz bidirectional_preprocess_paragraph(uint *, isz, BidirectionalDirection, uint *, isz, BidirectionalDirection *) @extern("grapheme_bidirectional_preprocess_paragraph");
fn isz bidirectional_reorder_line(uint *, uint *, isz, uint *, isz) @extern("grapheme_bidirectional_reorder_line");
fn isz decode_utf8(char *, isz, uint *) @extern("grapheme_decode_utf8");
fn isz encode_utf8(uint, char *, isz) @extern("grapheme_encode_utf8");
fn bool is_character_break(uint, uint, ushort *) @extern("grapheme_is_character_break");
fn bool is_lowercase(uint *, isz, isz *) @extern("grapheme_is_lowercase");
fn bool is_titlecase(uint *, isz, isz *) @extern("grapheme_is_titlecase");
fn bool is_uppercase(uint *, isz, isz *) @extern("grapheme_is_uppercase");
fn bool is_lowercase_utf8(char *, isz, isz *) @extern("grapheme_is_lowercase_utf8");
fn bool is_titlecase_utf8(char *, isz, isz *) @extern("grapheme_is_titlecase_utf8");
fn bool is_uppercase_utf8(char *, isz, isz *) @extern("grapheme_is_uppercase_utf8");
fn isz next_character_break(uint *, isz) @extern("grapheme_next_character_break");
fn isz next_line_break(uint *, isz) @extern("grapheme_next_line_break");
fn isz next_sentence_break(uint *, isz) @extern("grapheme_next_sentence_break");
fn isz next_word_break(uint *, isz) @extern("grapheme_next_word_break");
fn isz next_character_break_utf8(char *, isz) @extern("grapheme_next_character_break_utf8");
fn isz next_line_break_utf8(char *, isz) @extern("grapheme_next_line_break_utf8");
fn isz next_sentence_break_utf8(char *, isz) @extern("grapheme_next_sentence_break_utf8");
fn isz next_word_break_utf8(char *, isz) @extern("grapheme_next_word_break_utf8");
fn isz to_lowercase(uint *, isz, uint *, isz) @extern("grapheme_to_lowercase");
fn isz to_titlecase(uint *, isz, uint *, isz) @extern("grapheme_to_titlecase");
fn isz to_uppercase(uint *, isz, uint *, isz) @extern("grapheme_to_uppercase");
fn isz to_lowercase_utf8(char *, isz, char *, isz) @extern("grapheme_to_lowercase_utf8");
fn isz to_titlecase_utf8(char *, isz, char *, isz) @extern("grapheme_to_titlecase_utf8");
fn isz to_uppercase_utf8(char *, isz, char *, isz) @extern("grapheme_to_uppercase_utf8");

View File

@ -0,0 +1,9 @@
{
"provides": "grapheme",
"targets": {
"linux-x64": {
"dependencies": [],
"linked-libraries": ["grapheme", "c"]
}
}
}

View File

@ -0,0 +1,14 @@
{
"langrev": "1",
"warnings": ["no-unused"],
"dependency-search-paths": [".."],
"dependencies": ["grapheme"],
"authors": ["Alessandro Mauri <alemauri001@gmail.com>", "Laslo Hunhold <dev@frign.de>"],
"version": "0.1.0",
"sources": [],
"output": "build",
"target": "linux-x64",
"features": [],
"cpu": "generic",
"opt": "O0"
}

@ -0,0 +1 @@
Subproject commit 65b354f0fcb1d925f4340dbb4415ea06e8af2bec

4
lib/schrift.c3l/Makefile Normal file
View File

@ -0,0 +1,4 @@
all:
make -C thirdparty/libschrift
mkdir -p linux-x64
cp thirdparty/libschrift/libschrift.a linux-x64/libschrift.a

View File

@ -0,0 +1,58 @@
module schrift;
alias SftFont = void*;
alias SftUChar = uint;
alias SftGlyph = uint;
const int SFT_DOWNWARD_Y = 0x01;
struct Sft
{
SftFont font;
double xScale;
double yScale;
double xOffset;
double yOffset;
int flags;
}
struct SftLMetrics
{
double ascender;
double descender;
double lineGap;
}
struct SftGMetrics
{
double advanceWidth;
double leftSideBearing;
int yOffset;
int minWidth;
int minHeight;
}
struct SftKerning
{
double xShift;
double yShift;
}
struct SftImage
{
void *pixels;
int width;
int height;
}
extern fn ZString sft_version() @extern("sft_version");
extern fn SftFont loadmem(void* mem, usz size) @extern("sft_loadmem");
extern fn SftFont loadfile(ZString filename) @extern("sft_loadfile");
extern fn void freefont(SftFont font) @extern("sft_freefont");
extern fn int lmetrics(Sft* sft, SftLMetrics* metrics) @extern("sft_lmetrics");
extern fn int lookup(Sft* sft, SftUChar codepoint, SftGlyph* glyph) @extern("sft_lookup");
extern fn int gmetrics(Sft* sft, SftGlyph glyph, SftGMetrics* metrics) @extern("sft_gmetrics");
extern fn int kerning(Sft* sft, SftGlyph leftGlyph, SftGlyph rightGlyph, SftKerning* kerning) @extern("sft_kerning");
extern fn int render(Sft* sft, SftGlyph glyph, SftImage image) @extern("sft_render");

View File

@ -0,0 +1,9 @@
{
"provides" : "schrift",
"targets" : {
"linux-x64" : {
"dependencies" : [],
"linked-libraries" : ["schrift", "c"]
}
}
}

View File

@ -0,0 +1,14 @@
{
"langrev": "1",
"warnings": [ "no-unused" ],
"dependency-search-paths": [ ".." ],
"dependencies": [ "schrift" ],
"authors": [ "Alessandro Mauri <alemauri001@gmail.com>", "Thomas Oltmann <thomas.oltmann.hhg@gmail.com>" ],
"version": "0.1.0",
"sources": [ ],
"output": "build",
"target": "linux-x64",
"features": [],
"cpu": "generic",
"opt": "O0",
}

@ -0,0 +1 @@
Subproject commit 24737d2922b23df4a5692014f5ba03da0c296112

1
lib/sdl3.c3l Submodule

@ -0,0 +1 @@
Subproject commit 076355e2d126e7546e53663b97e8dec22667d34d

0
lib/ugui.c3l/LICENSE Normal file
View File

1
lib/ugui.c3l/README.md Normal file
View File

@ -0,0 +1 @@
Welcome to the ugui library.

View File

View File

@ -0,0 +1,11 @@
{
"provides" : "ugui",
"sources" : [ "src/**" ],
"targets" : {
"linux-x64" : {
"link-args" : [],
"dependencies" : ["schrift", "grapheme"],
"linked-libraries" : []
}
}
}

View File

147
lib/ugui.c3l/src/cache.c3 Normal file
View File

@ -0,0 +1,147 @@
module cache{Key, Value, SIZE};
/* LRU Cache
* The cache uses a pool (array) to store all the elements, each element has
* a key (id) and a value. A HashMap correlates the ids to an index in the pool.
* To keep track of which items were recently used two bit arrays are kept, one
* stores the "used" flag for each index and anothe the "present" flag.
* Every NCYCLES operations the present and used arrays are updated to free up
* 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;
import std::collections::map;
alias BitArr = bitset::BitSet{SIZE};
alias IdTable = map::HashMap{Key, usz};
alias IdTableEntry = map::Entry{Key, usz};
const usz CACHE_NCYCLES = (usz)(SIZE * 2.0/3.0);
struct Cache {
BitArr present, used;
IdTable table;
Value[] pool;
usz cycle_count;
}
// Every CACHE_CYCLES operations mark as not-present the unused elements
macro Cache.cycle(&cache) @private {
cache.cycle_count++;
if (cache.cycle_count > CACHE_NCYCLES) {
for (usz i = 0; i < cache.present.data.len; i++) {
cache.present.data[i] &= cache.used.data[i];
cache.used.data[i] = 0;
}
cache.cycle_count = 0;
}
}
fn void? Cache.init(&cache)
{
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 = mem::new_array(Value, SIZE);
}
fn void Cache.free(&cache)
{
(void)cache.table.free();
(void)mem::free(cache.pool);
}
fn Value*? Cache.search(&cache, Key id)
{
// get_entry() faults on miss
IdTableEntry* entry = cache.table.get_entry(id)!;
/* MISS, wrong key */
if (entry.key != id) {
cache.table.remove(id)!;
return NOT_FOUND?;
}
/* MISS, the data is not valid (not present) */
if (!cache.present[entry.value]) {
// if the data is not present but it is still in the table, remove it
cache.table.remove(id)!;
return NOT_FOUND?;
}
/* HIT, set as recently used */
cache.used[entry.value] = true;
return &(cache.pool[entry.value]);
}
fn void Cache.remove(&cache, Key id)
{
IdTableEntry*? entry = cache.table.get_entry(id);
if (catch entry) {
return;
}
// found, remove it
cache.present[entry.value] = false;
(void)cache.table.remove(id);
}
/* Look for a free spot in the present bitmap and return its index */
/* If there is no free space left then just return the first position */
fn usz Cache.get_free_spot(&cache) @private
{
const BITS = $typeof(cache.present.data[0]).sizeof*8;
foreach (idx, d: cache.present.data) {
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
{
// TODO: verify index, g and id
Value* spot;
/* Set used and present */
cache.present.set(index);
cache.used.set(index);
cache.cycle();
spot = &(cache.pool[index]);
*spot = *g;
cache.table.set(id, index);
return spot;
}
// Insert an element in the cache, returns the index
fn Value*? Cache.insert_new(&cache, Value* g, Key id)
{
usz index = cache.get_free_spot();
return cache.insert_at(g, id, index);
}
fn Value*? Cache.get_or_insert(&cache, Value* g, Key id, bool *is_new = null)
{
Value*? c = cache.search(id);
if (catch e = c) {
if (e != NOT_FOUND) {
return e?;
} else {
// if the element is new (inserted) set the is_new flag
if (is_new) *is_new = true;
return cache.insert_new(g, id);
}
} else {
if (is_new) *is_new = false;
return c;
}
}

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

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

View File

@ -0,0 +1,131 @@
module ugui;
import std::io;
faultdef CANNOT_PLACE, INVALID_TYPE;
enum AtlasType {
ATLAS_GRAYSCALE,
ATLAS_R8G8B8A8,
}
// black and white atlas
struct Atlas {
AtlasType type;
Id id;
ushort width, height;
char[] buffer;
Point row;
ushort row_h;
}
// bytes per pixel
macro usz AtlasType.bpp(type)
{
switch (type) {
case ATLAS_GRAYSCALE: return 1;
case ATLAS_R8G8B8A8: return 4;
}
}
macro typeid AtlasType.underlying(type)
{
switch (type) {
case ATLAS_GRAYSCALE: return char;
case ATLAS_R8G8B8A8: return uint;
}
}
/*
// FIXME: in and out types are not always known at compile time
macro @pixel_convert(p, AtlasType $in, AtlasType $out)
{
$if $in == $out:
return p;
$else
$switch
$case $in == ATLAS_R8G8B8A8 && $out == ATLAS_GRAYSCALE:
var r = ((p >> 0) & 0xff);
var g = ((p >> 8) & 0xff);
var b = ((p >> 16) & 0xff);
var a = ((p >> 24) & 0xff);
if (a == 0) return (char)0;
return (ATLAS_GRAYSCALE.underlying())(((float)r+g+b) / 3.0f);
$case $in == ATLAS_GRAYSCALE && $out == ATLAS_R8G8B8A8:
var x = (char)(p/3.0);
return (ATLAS_R8G8B8A8.underlying())(x|(x<<8)|(x<<16)|(255<<24));
$default: $error "Unimplemented pixel format conversion";
$endswitch
$endif
}
*/
fn void? Atlas.new(&atlas, Id id, AtlasType type, ushort width, ushort height)
{
atlas.id = id;
atlas.type = type;
atlas.width = width;
atlas.height = height;
atlas.buffer = mem::new_array(char, (usz)atlas.width*atlas.height*type.bpp());
}
fn void Atlas.free(&atlas)
{
free(atlas.buffer);
}
/*
* pixels -> +--------------+-----+
* | | | h
* | | | e
* | | | i
* | | | g
* | | | h
* | | | t
* +--------------+-----+
* |<--- width -->|
* |<----- stride ----->|
* bytes per pixels are inferred and have to be the same
* as the atlas type
*/
// place a rect inside the atlas
// uses a row first algorithm
// TODO: use a skyline algorithm https://jvernay.fr/en/blog/skyline-2d-packer/implementation/
fn Point? Atlas.place(&atlas, char[] pixels, ushort w, ushort h, ushort stride)
{
Point p;
if (atlas.row.x + w <= atlas.width && atlas.row.y + h <= atlas.height) {
p = atlas.row;
} else {
atlas.row.x = 0;
atlas.row.y = atlas.row.y + atlas.row_h;
atlas.row_h = 0;
if (atlas.row.x + w <= atlas.width && atlas.row.y + h <= atlas.height) {
p = atlas.row;
} else {
return CANNOT_PLACE?;
}
}
usz bpp = atlas.type.bpp();
for (usz y = 0; y < h; y++) {
for (usz x = 0; x < w; x++) {
char[] buf = atlas.buffer[(usz)(p.y+y)*atlas.width*bpp + (p.x+x)*bpp ..];
char[] pix = pixels[(usz)y*stride*bpp + x*bpp ..];
buf[0..bpp-1] = pix[0..bpp-1];
}
}
atlas.row.x += w;
if (h > atlas.row_h) {
atlas.row_h = h;
}
return p;
}

View File

@ -0,0 +1,153 @@
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 min_size = {0, 0, style.size, style.size};
Rect text_size = ctx.get_text_bounds(label)!;
Rect icon_size = sprite.rect();
ushort h_lh = (ushort)(ctx.font.line_height() / 2);
ushort left_pad = label != "" ? h_lh : 0;
ushort inner_pad = label != "" && icon != "" ? h_lh : 0;
ushort right_pad = left_pad;
/* |left_pad
* +--v-----------------------------------+
* |<->+--------+ |
* | | | +-----------------+ |
* | | icon | | label | |
* | | | +-----------------+ |
* | +--------+<->| |<->|
* +-------------^--------------------^---+
* |inner_pad |right_pad
*/
Rect tot_size = {
.w = (short)max(min_size.w, icon_size.w + text_size.w + left_pad + inner_pad + right_pad),
.h = (short)max(min_size.h, max(icon_size.h, text_size.h)),
};
elem.bounds = ctx.layout_element(parent, tot_size, style);
if (elem.bounds.is_null()) return {};
elem.events = ctx.get_elem_events(elem);
Rect content_bounds = elem.content_bounds(style);
Rect text_bounds = {
.x = content_bounds.x + icon_size.w + left_pad + inner_pad,
.y = content_bounds.y,
.w = content_bounds.w - icon_size.w - left_pad - inner_pad - right_pad,
.h = content_bounds.h
};
text_bounds = text_size.center_to(text_bounds);
Rect icon_bounds = {
.x = content_bounds.x,
.y = content_bounds.y,
.w = icon_size.w + right_pad + inner_pad,
.h = (short)max(icon_size.h, content_bounds.h)
};
icon_bounds = icon_size.center_to(icon_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, parent.div.z_index, &s)!;
ctx.push_sprite(icon_bounds, sprite.uv(), ctx.sprite_atlas.id, parent.div.z_index, type: sprite.type)!;
// Style ss = {.bg = 0x000000ffu.@to_rgba()};
// ctx.push_rect(content_bounds, parent.div.z_index, &ss)!;
ctx.push_string(text_bounds, label, parent.div.z_index, style.fg)!;
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.layout_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.border = s.padding = {};
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.layout_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.border = s.padding = {};
ctx.push_rect(t, parent.div.z_index, &s)!;
}

View File

@ -0,0 +1,181 @@
module ugui;
import std::ascii;
import std::io;
// command type
enum CmdType {
CMD_RECT,
CMD_UPDATE_ATLAS,
CMD_SPRITE,
CMD_SCISSOR,
}
// command to draw a rect
struct CmdRect {
Rect rect;
ushort radius;
Color color;
}
struct CmdUpdateAtlas {
Id id;
char* raw_buffer;
short width, height, bpp;
}
struct CmdSprite {
Id texture_id;
SpriteType type;
Rect rect;
Rect texture_rect;
Color hue;
}
// if rect is zero Rect{0} then reset the scissor
struct CmdScissor {
Rect rect;
}
// command structure
struct Cmd (Printable) {
CmdType type;
int z_index;
union {
CmdRect rect;
CmdUpdateAtlas update_atlas;
CmdSprite sprite;
CmdScissor scissor;
}
}
fn int Cmd.compare_to(Cmd a, Cmd b)
{
if (a.z_index == b.z_index) return 0;
return a.z_index > b.z_index ? 1 : -1;
}
// implement the Printable interface
fn usz? Cmd.to_format(Cmd* cmd, Formatter *f) @dynamic
{
return f.printf("Cmd{ type: %s, z_index: %d }", cmd.type, cmd.z_index);
}
macro bool cull_rect(Rect rect, Rect clip = {0,0,short.max,short.max})
{
bool no_area = rect.w <= 0 || rect.h <= 0;
return no_area || !rect.collides(clip);
}
// FIXME: this whole thing could be done at compile time, maybe
macro Ctx.push_cmd(&ctx, Cmd *cmd, int z_index)
{
cmd.z_index = z_index;
Rect rect;
switch (cmd.type) {
case CMD_RECT: rect = cmd.rect.rect;
case CMD_SPRITE: rect = cmd.sprite.rect;
default: return ctx.cmd_queue.enqueue(cmd);
}
if (cull_rect(rect, ctx.div_scissor)) return;
return ctx.cmd_queue.enqueue(cmd);
}
fn void? Ctx.push_scissor(&ctx, Rect rect, int z_index)
{
Cmd sc = {
.type = CMD_SCISSOR,
.scissor.rect = rect.intersection(ctx.div_scissor),
};
ctx.push_cmd(&sc, z_index)!;
}
fn void? Ctx.push_rect(&ctx, Rect rect, int z_index, Style* style)
{
Rect border = style.border;
ushort radius = style.radius;
Color bg = style.bg;
Color border_color = style.secondary;
// FIXME: this implies that the border has to be uniform
if (!border.is_null()) {
Cmd cmd = {
.type = CMD_RECT,
.rect.rect = rect,
.rect.color = border_color,
.rect.radius = radius + border.x,
};
ctx.push_cmd(&cmd, z_index)!;
}
Cmd cmd = {
.type = CMD_RECT,
.rect.rect = {
.x = rect.x + border.x,
.y = rect.y + border.y,
.w = rect.w - (border.x+border.w),
.h = rect.h - (border.y+border.h),
},
.rect.color = bg,
.rect.radius = radius,
};
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 = {
.type = CMD_SPRITE,
.sprite.type = type,
.sprite.rect = bounds,
.sprite.texture_rect = texture,
.sprite.texture_id = texture_id,
.sprite.hue = hue,
};
ctx.push_cmd(&cmd, z_index)!;
}
// TODO: do not return the WHOLE TextInfo but instead something smaller
fn TextInfo? Ctx.push_string(&ctx, Rect bounds, char[] text, int z_index, Color hue, bool reflow = false)
{
if (text.len == 0) {
return {};
}
ctx.push_scissor(bounds, z_index)!;
Id texture_id = ctx.font.id; // or ctx.font.atlas.id
Rect text_bounds = {bounds.x, bounds.y, 0, 0};
TextInfo ti;
ti.init(&ctx.font, (String)text, bounds);
while (ti.place_glyph(reflow)!) {
if (!cull_rect(ti.glyph_bounds, bounds)) {
ctx.push_sprite(ti.glyph_bounds, ti.glyph_uv, texture_id, z_index, hue)!;
}
}
ctx.push_scissor({}, z_index)!;
return ti;
}
fn void? Ctx.push_update_atlas(&ctx, Atlas* atlas)
{
Cmd up = {
.type = CMD_UPDATE_ATLAS,
.update_atlas = {
.id = atlas.id,
.raw_buffer = atlas.buffer,
.width = atlas.width,
.height = atlas.height,
.bpp = (ushort)atlas.type.bpp(),
},
};
// update the atlases before everything else
ctx.push_cmd(&up, -1)!;
}

View File

@ -0,0 +1,333 @@
module ugui;
import vtree;
import cache;
import fifo;
import std::io;
import std::core::string;
import std::core::mem::allocator;
// element ids are just long ints
alias Id = uint;
enum ElemType {
ETYPE_NONE,
ETYPE_DIV,
ETYPE_BUTTON,
ETYPE_SLIDER,
ETYPE_TEXT,
ETYPE_SPRITE,
}
bitstruct ElemFlags : uint {
bool updated : 0;
bool is_new : 1;
}
bitstruct ElemEvents : uint {
bool key_press : 0;
bool key_release : 1;
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;
}
// element structure
struct Elem {
Id id;
isz tree_idx;
ElemFlags flags;
ElemEvents events;
Rect bounds;
ElemType type;
union {
ElemDiv div;
ElemButton button;
ElemSlider slider;
ElemText text;
ElemSprite sprite;
}
}
// relationships between elements are stored in a tree, it stores just the ids
alias IdTree = vtree::VTree{Id};
// elements themselves are kept in a cache
const uint MAX_ELEMENTS = 256;
alias ElemCache = cache::Cache{Id, Elem, MAX_ELEMENTS};
alias CmdQueue = fifo::Fifo{Cmd};
faultdef INVALID_SIZE, EVENT_UNSUPPORTED, WRONG_ELEMENT_TYPE, WRONG_ID;
const Rect DIV_FILL = { .x = 0, .y = 0, .w = 0, .h = 0 };
const uint STACK_STEP = 10;
const uint MAX_ELEMS = 128;
const uint MAX_CMDS = 2048;
const uint ROOT_ID = 1;
const uint TEXT_MAX = 64;
struct Ctx {
IdTree tree;
ElemCache cache;
CmdQueue cmd_queue;
StyleMap styles;
// total size in pixels of the context
ushort width, height;
Font font;
SpriteAtlas sprite_atlas;
bool has_focus;
struct input {
InputEvents events;
struct mouse {
Point pos, delta;
// mouse_down: bitmap of mouse buttons that are held
// mouse_updated: bitmap of mouse buttons that have been updated
// mouse_released = mouse_updated & ~mouse_down
// mouse_pressed = mouse_updated & mouse_down
MouseButtons down;
MouseButtons updated;
// scroll wheel
Point scroll;
}
struct keyboard {
char[TEXT_MAX] text;
usz text_len;
ModKeys modkeys;
}
}
Id hover_id;
Id focus_id;
Rect div_scissor; // the current div bounds used for scissor test
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;
}
macro @bits(#a) => $typeof(#a).sizeof*8;
macro Id.rotate_left(id, uint $n) => (id << $n) | (id >> (@bits(id) - $n));
const uint GOLDEN_RATIO = 0x9E3779B9;
// generate an id combining the hashes of the parent id and the label
// with the Cantor pairing function
fn Id? Ctx.gen_id(&ctx, Id id2)
{
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);
mixed += GOLDEN_RATIO;
return mixed;
}
// compute the id from arguments and the line of the call
macro Id @compute_id(...)
{
Id id = (Id)$$LINE.hash() ^ (Id)@str_hash($$FILE);
$for var $i = 0; $i < $vacount; $i++:
id ^= (Id)$vaconst[$i].hash();
$endfor
return id;
}
// get or push an element from the cache, return a pointer to it
// resets all flags except is_new which is set accordingly
fn Elem*? Ctx.get_elem(&ctx, Id id, ElemType type)
{
Elem empty_elem;
bool is_new;
Elem* elem;
elem = ctx.cache.get_or_insert(&empty_elem, id, &is_new)!;
elem.flags = (ElemFlags)0;
elem.flags.is_new = is_new;
elem.id = id;
if (is_new == false && elem.type != type) {
return WRONG_ELEMENT_TYPE?;
} else {
elem.type = type;
}
elem.tree_idx = ctx.tree.add(id, ctx.active_div)!;
return elem;
}
// find an element, does not allocate a new one in cache
// THIS HAS TO BE A MACRO SINCE IT RETURNS A POINTER TO A TEMPORARY VALUE
macro Elem* Ctx.find_elem(&ctx, Id id)
{
Elem*? elem;
elem = ctx.cache.search(id);
if (catch elem) {
return &&(Elem){};
}
return elem;
}
fn Elem*? Ctx.get_active_div(&ctx)
{
Id id = ctx.tree.get(ctx.active_div)!;
return ctx.cache.search(id);
}
fn void? Ctx.init(&ctx)
{
ctx.tree.init(MAX_ELEMENTS)!;
defer catch { (void)ctx.tree.free(); }
ctx.cache.init()!;
defer catch { (void)ctx.cache.free(); }
ctx.cmd_queue.init(MAX_CMDS)!;
defer catch { (void)ctx.cmd_queue.free(); }
ctx.styles.init(allocator::heap());
ctx.styles.register_style(&DEFAULT_STYLE, @str_hash("default"));
defer catch { ctx.styles.free(); }
ctx.active_div = 0;
}
fn void Ctx.free(&ctx)
{
(void)ctx.tree.free();
(void)ctx.cache.free();
(void)ctx.cmd_queue.free();
(void)ctx.font.free();
(void)ctx.sprite_atlas.free();
(void)ctx.styles.free();
}
fn void? Ctx.frame_begin(&ctx)
{
// 1. Reset the active div
// 2. Get the root element from the cache and update it
ctx.active_div = 0;
Elem* elem = ctx.get_elem(ROOT_ID, ETYPE_DIV)!;
ctx.active_div = elem.tree_idx;
// The root should have the updated flag only if the size of the window
// was changed between frasmes, this propagates an element size recalculation
// down the element tree
elem.flags.updated = ctx.input.events.resize;
// if the window has focus then the root element also has focus, no other
// computation needed, child elements need to check the mouse positon and
// other stuff
//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.div.pcb = {};
elem.div.origin_c = {};
elem.div.origin_r = {};
ctx.div_scissor = {0, 0, ctx.width, ctx.height};
// The root element does not push anything to the stack
// TODO: add a background color taken from a theme or config
}
const int DEBUG = 1;
fn void? Ctx.frame_end(&ctx)
{
// FIXME: this is not guaranteed to be root. the user might forget to close a div or some other element
Elem* root = ctx.get_active_div()!;
if (root.id != ROOT_ID) return WRONG_ID?;
// 1. clear the tree
ctx.tree.nuke();
// 2. clear input fields
ctx.input.events = (InputEvents)0;
ctx.input.keyboard.text_len = 0;
// send atlas updates
if (ctx.font.should_update) {
ctx.push_update_atlas(&ctx.font.atlas)!;
ctx.font.should_update = false;
}
if (ctx.sprite_atlas.should_update) {
ctx.push_update_atlas(&ctx.sprite_atlas.atlas)!;
ctx.sprite_atlas.should_update = false;
}
// debug
$if DEBUG == 1:
// draw mouse position
Cmd cmd = {
.type = CMD_RECT,
.z_index = int.max-1, // hopefully over everything else
.rect.rect = {
.x = ctx.input.mouse.pos.x - 2,
.y = ctx.input.mouse.pos.y - 2,
.w = 4,
.h = 4,
},
.rect.color = 0xff00ffffu.to_rgba()
};
ctx.cmd_queue.enqueue(&cmd)!;
$endif
// sort the command buffer by the z-index
ctx.cmd_queue.sort()!;
}
<*
* @ensure elem != null
*>
macro bool Ctx.is_hovered(&ctx, Elem *elem)
{
return ctx.input.mouse.pos.in_rect(elem.bounds);
}
macro bool Ctx.elem_focus(&ctx, Elem *elem)
{
return ctx.focus_id == elem.id;
}
// TODO: add other events
// FIXME: this does not work with touch
// FIXME: hacked together, please do better
fn ElemEvents Ctx.get_elem_events(&ctx, Elem *elem)
{
bool hover = ctx.is_hovered(elem);
bool focus = ctx.focus_id == elem.id || (hover && ctx.is_mouse_pressed(BTN_LEFT));
if (ctx.is_mouse_pressed(BTN_ANY) && !hover){
focus = false;
if (ctx.focus_id == elem.id) ctx.focus_id = 0;
}
if (hover) { ctx.hover_id = elem.id; }
if (focus) { ctx.focus_id = elem.id; }
ElemEvents ev = {
.mouse_hover = hover,
.mouse_press = hover && focus && ctx.is_mouse_pressed(BTN_ANY),
.mouse_release = hover && focus && ctx.is_mouse_released(BTN_ANY),
.mouse_hold = hover && focus && ctx.is_mouse_down(BTN_ANY),
.text_input = focus && (ctx.input.keyboard.text_len || ctx.input.keyboard.modkeys & KMOD_TXT),
};
return ev;
}

View File

@ -0,0 +1,155 @@
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* 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;
// 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.layout_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* 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)!;
Elem* parent = ctx.get_parent()!;
// TODO: reset the scissor back to the parent div
ctx.div_scissor = parent.bounds;
}

View File

@ -0,0 +1,314 @@
module ugui;
import schrift;
import grapheme;
import std::collections::map;
import std::core::mem;
import std::core::mem::allocator;
import std::io;
import std::ascii;
// unicode code point, different type for a different hash
alias Codepoint = uint;
//macro uint Codepoint.hash(self) => ((uint)self).hash();
/* width and height of a glyph contain the kering advance
* (u,v)
* +-------------*---+ -
* | ^ | | ^
* | |oy | | |
* | v | | |
* | .ii. | | |
* | @@@@@@. | | |
* | V@Mio@@o | | |
* | :i. V@V | | h
* | :oM@@M | | |
* | :@@@MM@M | | |
* | @@o o@M | | |
* |<->:@@. M@M | | |
* |ox @@@o@@@@ | | |
* | :M@@V:@@.| | v
* +-------------*---+ -
* |<---- w ---->|
* |<------ adv ---->|
*/
struct Glyph {
Codepoint code;
ushort u, v;
ushort w, h;
short adv, ox, oy;
}
const uint FONT_CACHED = 255;
alias GlyphTable = map::HashMap{Codepoint, Glyph};
faultdef TTF_LOAD_FAILED, MISSING_GLYPH, BAD_GLYPH_METRICS, RENDER_ERROR;
struct Font {
schrift::Sft sft;
String path;
Id id; // font id, same as atlas id
GlyphTable table;
float size;
float ascender, descender, linegap; // Line Metrics
Atlas atlas;
bool should_update; // should send update_atlas command, resets at frame_end()
}
fn void? Font.load(&font, String name, ZString path, uint height, float scale)
{
font.table.init(allocator::heap(), capacity: FONT_CACHED);
font.id = name.hash();
font.size = height*scale;
font.sft = {
.xScale = (double)font.size,
.yScale = (double)font.size,
.flags = schrift::SFT_DOWNWARD_Y,
};
font.sft.font = schrift::loadfile(path);
if (font.sft.font == null) {
font.table.free();
return TTF_LOAD_FAILED?;
}
schrift::SftLMetrics lmetrics;
schrift::lmetrics(&font.sft, &lmetrics);
font.ascender = (float)lmetrics.ascender;
font.descender = (float)lmetrics.descender;
font.linegap = (float)lmetrics.lineGap;
//io::printfn("ascender:%d, descender:%d, linegap:%d", font.ascender, font.descender, font.linegap);
// TODO: allocate buffer based on FONT_CACHED and the size of a sample letter
// like the letter 'A'
ushort size = (ushort)font.size*(ushort)($$sqrt((float)FONT_CACHED));
font.atlas.new(font.id, ATLAS_GRAYSCALE, size, size)!;
// preallocate the ASCII range
for (char c = ' '; c < '~'; c++) {
font.get_glyph((Codepoint)c)!;
}
}
fn Glyph*? Font.get_glyph(&font, Codepoint code)
{
Glyph*? gp;
gp = font.table.get_ref(code);
if (catch excuse = gp) {
if (excuse != NOT_FOUND) {
return excuse?;
}
} else {
return gp;
}
// missing glyph, render and place into an atlas
Glyph glyph;
schrift::SftGlyph gid;
schrift::SftGMetrics gmtx;
if (schrift::lookup(&font.sft, (SftUChar)code, &gid) < 0) {
return MISSING_GLYPH?;
}
if (schrift::gmetrics(&font.sft, gid, &gmtx) < 0) {
return BAD_GLYPH_METRICS?;
}
schrift::SftImage img = {
.width = gmtx.minWidth,
.height = gmtx.minHeight,
};
char[] pixels = mem::new_array(char, (usz)img.width * img.height);
img.pixels = pixels;
if (schrift::render(&font.sft, gid, img) < 0) {
return RENDER_ERROR?;
}
glyph.code = code;
glyph.w = (ushort)img.width;
glyph.h = (ushort)img.height;
glyph.ox = (short)gmtx.leftSideBearing;
glyph.oy = (short)gmtx.yOffset;
glyph.adv = (short)gmtx.advanceWidth;
//io::printfn("code=%c, w=%d, h=%d, ox=%d, oy=%d, adv=%d",
// glyph.code, glyph.w, glyph.h, glyph.ox, glyph.oy, glyph.adv);
Point uv = font.atlas.place(pixels, glyph.w, glyph.h, (ushort)img.width)!;
glyph.u = uv.x;
glyph.v = uv.y;
mem::free(pixels);
font.table.set(code, glyph);
font.should_update = true;
return font.table.get_ref(code);
}
fn void Font.free(&font)
{
font.atlas.free();
font.table.free();
schrift::freefont(font.sft.font);
}
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
@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;
}
const uint TAB_SIZE = 4;
// TODO: change the name
// TODO: reorder to make smaller
struct TextInfo {
Font* font;
Rect bounds;
String text;
usz off;
// current glyph info
Point origin;
Rect glyph_bounds;
Rect glyph_uv;
// cursor info
usz cursor_idx;
Point cursor_pos;
// current text bounds
Rect text_bounds;
}
<*
@require f != null
*>
fn void TextInfo.init(&self, Font* f, String s, Rect bounds = RECT_MAX)
{
*self = {};
self.font = f;
self.text = s;
self.bounds = bounds;
self.origin = bounds.position();
self.text_bounds = { .x = bounds.x, .y = bounds.y };
}
fn void TextInfo.reset(&self)
{
TextInfo old = *self;
self.init(old.font, old.text, old.bounds);
}
fn bool? TextInfo.place_glyph(&self, bool reflow)
{
if (self.off >= self.text.len) {
return false;
}
if (!self.origin.in_rect(self.bounds)) {
return false;
}
short baseline = (short)self.font.ascender;
short line_height = (short)self.font.ascender - (short)self.font.descender;
short line_gap = (short)self.font.linegap;
Codepoint cp;
Glyph* gp;
usz x;
cp = str_to_codepoint(self.text[self.off..], &x);
if (cp == 0) return false;
self.off += x;
if (ascii::is_cntrl((char)cp) == false) {
gp = self.font.get_glyph(cp)!;
self.glyph_uv = {
.x = gp.u,
.y = gp.v,
.w = gp.w,
.h = gp.h,
};
self.glyph_bounds = {
.x = self.origin.x + gp.ox,
.y = self.origin.y + gp.oy + baseline,
.w = gp.w,
.h = gp.h,
};
// try to wrap the text if the charcater goes outside of the bounds
if (reflow && !self.bounds.contains(self.glyph_bounds)) {
self.origin.y += line_height + line_gap;
self.glyph_bounds.y += line_height + line_gap;
self.origin.x = self.bounds.x;
self.glyph_bounds.x = self.bounds.x;
}
// handle tab
if (cp == '\t') {
self.origin.x += gp.adv*TAB_SIZE;
} else {
self.origin.x += gp.adv;
}
} else if (cp == '\n'){
self.origin.y += line_height + line_gap;
self.origin.x = self.bounds.x;
}
self.text_bounds = containing_rect(self.text_bounds, self.glyph_bounds);
if (self.off == self.cursor_idx) {
self.cursor_pos = self.origin;
}
return true;
}
fn Rect? Ctx.get_text_bounds(&ctx, String text)
{
TextInfo ti;
ti.init(&ctx.font, text);
while (ti.place_glyph(false)!);
return ti.text_bounds;
}
// TODO: check if the font is present in the context
fn Id Ctx.get_font_id(&ctx, String label)
{
return (Id)label.hash();
}
fn Atlas*? Ctx.get_font_atlas(&ctx, String name)
{
// TODO: use the font name, for now there is only one font
if (name.hash() != ctx.font.id) {
return WRONG_ID?;
}
return &ctx.font.atlas;
}
fn int Font.line_height(&font) => (int)(font.ascender - font.descender + (float)0.5);

View File

@ -0,0 +1,204 @@
module ugui;
import grapheme;
import std::io;
import std::math;
import std::core::string;
bitstruct InputEvents : uint {
bool resize : 0; // window size was changed
bool change_focus : 1; // window focus changed
bool mouse_move : 2; // mouse was moved
bool mouse_btn : 3; // mouse button pressed or released
bool mouse_scroll : 4; // mouse scroll wheel. x or y
bool text_input : 5;
bool mod_key : 6;
}
bitstruct MouseButtons : uint {
bool btn_left : 0;
bool btn_middle : 1;
bool btn_right : 2;
bool btn_4 : 3;
bool btn_5 : 4;
}
// FIXME: all of these names were prefixed with key_ idk if this is better,
// if it is remove the prefix on MouseButtons as well
// Modifier Keys, intended as any key that is not text
bitstruct ModKeys : uint {
bool lshift : 0;
bool rshift : 1;
bool lctrl : 2;
bool rctrl : 3;
bool lalt : 4;
bool ralt : 5;
bool lgui : 6;
bool rgui : 7;
bool num : 8;
bool caps : 9;
bool mode : 10;
bool scroll : 11;
bool bkspc : 12;
bool del : 13;
// arrow keys
bool up : 14;
bool down : 15;
bool left : 16;
bool right : 17;
}
const ModKeys KMOD_CTRL = {.lctrl = true, .rctrl = true};
const ModKeys KMOD_SHIFT = {.lshift = true, .rshift = true};
const ModKeys KMOD_ALT = {.lalt = true, .ralt = true};
const ModKeys KMOD_GUI = {.lgui = true, .rgui = true};
const ModKeys KMOD_TXT = {.bkspc = true, .del = true}; // modkeys that act like text input
const ModKeys KMOD_NONE = {};
const ModKeys KMOD_ANY = (ModKeys)(ModKeys.inner.max);
const MouseButtons BTN_NONE = {};
const MouseButtons BTN_ANY = (MouseButtons)(MouseButtons.inner.max);
const MouseButtons BTN_LEFT = {.btn_left = true};
const MouseButtons BTN_MIDDLE = {.btn_middle = true};
const MouseButtons BTN_RIGHT = {.btn_right = true};
const MouseButtons BTN_4 = {.btn_4 = true};
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)
{
bool is_mod = (bool)(ctx.input.keyboard.modkeys & mod);
bool is_keys = true;
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;
}
// Window size was changed
fn void? Ctx.input_window_size(&ctx, short width, short height)
{
if (width <= 0 || height <= 0) {
return INVALID_SIZE?;
}
ctx.input.events.resize = ctx.width != width || ctx.height != height;
ctx.width = width;
ctx.height = height;
}
// Window gained/lost focus
fn void Ctx.input_changefocus(&ctx, bool has_focus)
{
// FIXME: raylib only has an API to query the focus status so we have to
// update the input flag only if the focus changed
ctx.input.events.change_focus = ctx.has_focus != has_focus;
ctx.has_focus = has_focus;
}
macro Ctx.mouse_pressed(&ctx) => ctx.input.mouse.updated & ctx.input.mouse.down;
macro Ctx.mouse_released(&ctx) => ctx.input.mouse.updated & ~ctx.input.mouse.down;
macro Ctx.mouse_down(&ctx) => ctx.input.mouse.down;
// FIXME: hthis compairson could be done with a cast using MouseButtons.inner
// property but I could not figure out how
macro Ctx.is_mouse_pressed(&ctx, MouseButtons btn) => (ctx.mouse_pressed() & btn) != BTN_NONE;
macro Ctx.is_mouse_released(&ctx, MouseButtons btn) => (ctx.mouse_released() & btn) != BTN_NONE;
macro Ctx.is_mouse_down(&ctx, MouseButtons btn) => (ctx.mouse_down() & btn) != BTN_NONE;
// Mouse Buttons down
fn void Ctx.input_mouse_button(&ctx, MouseButtons buttons)
{
ctx.input.mouse.updated = ctx.input.mouse.down ^ buttons;
ctx.input.mouse.down = buttons;
ctx.input.events.mouse_btn = (uint)ctx.input.mouse.down != 0 || (uint)ctx.input.mouse.updated != 0;
}
// Mouse was moved, report absolute position
fn void Ctx.input_mouse_abs(&ctx, short x, short y)
{
ctx.input.mouse.pos.x = math::clamp(x, (short)0, ctx.width);
ctx.input.mouse.pos.y = math::clamp(y, (short)0, ctx.height);
short dx, dy;
dx = x - ctx.input.mouse.pos.x;
dy = y - ctx.input.mouse.pos.y;
ctx.input.mouse.delta.x = dx;
ctx.input.mouse.delta.y = dy;
ctx.input.events.mouse_move = dx != 0 || dy != 0;
}
// Mouse was moved, report relative motion
fn void Ctx.input_mouse_delta(&ctx, short dx, short dy)
{
ctx.input.mouse.delta.x = dx;
ctx.input.mouse.delta.y = dy;
short mx, my;
mx = ctx.input.mouse.pos.x + dx;
my = ctx.input.mouse.pos.y + dy;
ctx.input.mouse.pos.x = math::clamp(mx, (short)0, ctx.width);
ctx.input.mouse.pos.y = math::clamp(my, (short)0, ctx.height);
ctx.input.events.mouse_move = dx != 0 || dy != 0;
}
fn void Ctx.input_mouse_wheel(&ctx, short x, short y, float scale = 1.0)
{
ctx.input.mouse.scroll.x = (short)((float)-x*scale);
ctx.input.mouse.scroll.y = (short)((float)-y*scale);
ctx.input.events.mouse_scroll = x !=0 || y != 0;
}
// append utf-8 encoded text to the context text input
fn void Ctx.input_text_utf8(&ctx, char[] text)
{
if (text.len == 0) { return; }
usz remaining = ctx.input.keyboard.text.len - ctx.input.keyboard.text_len;
usz len = text.len > remaining ? remaining : text.len;
char[] s = ctx.input.keyboard.text[ctx.input.keyboard.text_len ..];
s[..len-1] = text[..len-1];
ctx.input.keyboard.text_len += len;
ctx.input.events.text_input = true;
}
fn void Ctx.input_text_unicode(&ctx, char[] text)
{
if (text.ptr == null || text.len == 0) { return; }
char[32] tmp;
usz remaining = ctx.input.keyboard.text.len - ctx.input.keyboard.text_len;
char[] s = ctx.input.keyboard.text[ctx.input.keyboard.text_len ..];
usz off;
foreach (idx, cp: text) {
if (off >= remaining) { break; }
usz enc = grapheme::encode_utf8(cp, tmp[..], tmp.len);
s[off..off+enc] = tmp[..enc];
off += enc;
}
ctx.input.keyboard.text_len += off;
ctx.input.events.text_input = true;
}
fn void Ctx.input_char(&ctx, char c)
{
char[1] b = {c};
ctx.input_text_utf8(b[..]);
}
// Modifier keys, like control or backspace
// TODO: make this call repetible to input modkeys one by one
fn void Ctx.input_mod_keys(&ctx, ModKeys modkeys)
{
ctx.input.keyboard.modkeys = modkeys;
ctx.input.events.mod_key = (uint)ctx.input.keyboard.modkeys != 0;
}

View File

@ -0,0 +1,157 @@
module ugui;
enum Layout {
LAYOUT_ROW,
LAYOUT_COLUMN,
LAYOUT_FLOATING,
LAYOUT_ABSOLUTE,
}
fn void? Ctx.layout_set_row(&ctx)
{
Elem *parent = ctx.get_parent()!;
parent.div.layout = LAYOUT_ROW;
}
fn void? Ctx.layout_set_column(&ctx)
{
Elem *parent = ctx.get_parent()!;
parent.div.layout = LAYOUT_COLUMN;
}
fn void? Ctx.layout_set_floating(&ctx)
{
Elem *parent = ctx.get_parent()!;
parent.div.layout = LAYOUT_FLOATING;
}
fn void? Ctx.layout_next_row(&ctx)
{
Elem *parent = ctx.get_parent()!;
parent.div.origin_r = {
.x = parent.bounds.x,
.y = parent.div.children_bounds.bottom_right().y,
};
parent.div.origin_c = parent.div.origin_r;
}
fn void? Ctx.layout_next_column(&ctx)
{
Elem *parent = ctx.get_parent()!;
parent.div.origin_c = {
.x = parent.div.children_bounds.bottom_right().x,
.y = parent.bounds.y,
};
parent.div.origin_r = parent.div.origin_c;
}
macro Rect Elem.content_bounds(&elem, style) => elem.bounds.pad(style.border).pad(style.padding);
macro Rect Elem.get_view(&elem)
{
Rect off;
if (elem.div.scroll_x.enabled && elem.div.scroll_x.on) {
off.x = (short)((float)(elem.div.pcb.w - elem.bounds.w) * elem.div.scroll_x.value);
off.w = -elem.div.scroll_size;
}
if (elem.div.scroll_y.enabled && elem.div.scroll_y.on) {
off.y = (short)((float)(elem.div.pcb.h - elem.bounds.h) * elem.div.scroll_y.value);
off.h = -elem.div.scroll_size;
}
return elem.bounds.add(off);
}
macro Point Elem.get_view_off(&elem)
{
return elem.get_view().sub(elem.bounds).position();
}
// position the rectangle inside the parent according to the layout
// parent: parent div
// rect: the requested size
// style: apply style
<*
@require ctx != null
@require parent.type == ETYPE_DIV
*>
fn Rect Ctx.layout_element(&ctx, Elem *parent, Rect rect, Style* style)
{
ElemDiv* div = &parent.div;
Rect parent_bounds, parent_view;
Rect child_placement, child_occupied;
// 1. Select the right origin
Point origin;
switch (div.layout) {
case LAYOUT_ROW:
origin = div.origin_r;
case LAYOUT_COLUMN:
origin = div.origin_c;
case LAYOUT_FLOATING: // none, relative to zero zero
case LAYOUT_ABSOLUTE: // absolute position, this is a no-op, return the rect
return rect;
default: // error
return {};
}
// 2. Compute the parent's view
parent_bounds = parent.bounds;
parent_view = parent.get_view();
// 3. Compute the placement and occupied area
// grow rect (wanted size) when widht or height are less than zero
bool adapt_x = rect.w <= 0;
bool adapt_y = rect.h <= 0;
if (adapt_x) rect.w = parent_bounds.w - parent_bounds.x - origin.x;
if (adapt_y) rect.h = parent_bounds.h - parent_bounds.y - origin.y;
// offset placement and area
child_placement = child_placement.off(origin.add(rect.position()));
child_occupied = child_occupied.off(origin.add(rect.position()));
Rect margin = style.margin;
Rect border = style.border;
Rect padding = style.padding;
// 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.position().add(border.size()));
child_occupied = child_occupied.grow(border.position().add(border.size()));
// 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.x+border.w + margin.x+margin.w;
if (adapt_y) rect.h -= padding.y+padding.h + border.y+border.h + margin.y+margin.h;
// set the size
child_placement = child_placement.grow(rect.size());
child_occupied = child_occupied.grow(rect.size());
// 4. Update the parent's origin
div.origin_r = {
.x = child_occupied.bottom_right().x,
.y = origin.y,
};
div.origin_c = {
.x = origin.x,
.y = child_occupied.bottom_right().y,
};
// 5. Update the parent's children bounds
div.children_bounds = containing_rect(div.children_bounds, child_occupied);
// 99. return the placement
if (child_placement.collides(parent_view)) {
return child_placement.off(parent.get_view_off().neg());
} else {
return {};
}
}

View File

@ -0,0 +1,259 @@
module ugui;
// ---------------------------------------------------------------------------------- //
// RECTANGLE //
// ---------------------------------------------------------------------------------- //
// Rect and it's methods
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)
{
return (a.x <= b.x && a.y <= b.y && a.x+a.w >= b.x+b.w && a.y+a.h >= b.y+b.h);
}
// returns the intersection of a and b
macro Rect Rect.intersection(Rect a, Rect b)
{
return {
.x = (short)max(a.x, b.x),
.y = (short)max(a.y, b.y),
.w = (short)min(a.x+a.w, b.x+b.w) - (short)max(a.x, b.x),
.h = (short)min(a.y+a.h, b.y+b.h) - (short)max(a.y, b.y),
};
}
// returns true if the intersection not null
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)
{
return {
.x = r1.x + r2.x,
.y = r1.y + r2.y,
.w = r1.w + r2.w,
.h = r1.h + r2.h,
};
}
// returns the element-wise subtraction of r1 and r2
macro Rect Rect.sub(Rect r1, Rect r2)
{
return {
.x = r1.x - r2.x,
.y = r1.y - r2.y,
.w = r1.w - r2.w,
.h = r1.h - r2.h,
};
}
// returns the element-wise multiplication of r1 and r2
macro Rect Rect.mul(Rect r1, Rect r2)
{
return {
.x = r1.x * r2.x,
.y = r1.y * r2.y,
.w = r1.w * r2.w,
.h = r1.h * r2.h,
};
}
macro Point Rect.position(Rect r)
{
return {
.x = r.x,
.y = r.y,
};
}
macro Point Rect.size(Rect r)
{
return {
.x = r.w,
.y = r.h,
};
}
macro Rect Rect.max(Rect a, Rect b)
{
return {
.x = max(a.x, b.x),
.y = max(a.y, b.y),
.w = max(a.w, b.w),
.h = max(a.h, b.h),
};
}
macro Rect Rect.min(Rect a, Rect b)
{
return {
.x = min(a.x, b.x),
.y = min(a.y, b.y),
.w = min(a.w, b.w),
.h = min(a.h, b.h),
};
}
// Offset a rect by a point
macro Rect Rect.off(Rect r, Point p)
{
return {
.x = r.x + p.x,
.y = r.y + p.y,
.w = r.w,
.h = r.h,
};
}
// Resize a rect width and height
macro Rect Rect.grow(Rect r, Point p)
{
return {
.x = r.x,
.y = r.y,
.w = r.w + p.x,
.h = r.h + p.y,
};
}
// Return the bottom-right corner of a rectangle
macro Point Rect.bottom_right(Rect r)
{
return {
.x = r.x + r.w,
.y = r.y + r.h,
};
}
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,
};
}
// ---------------------------------------------------------------------------------- //
// POINT //
// ---------------------------------------------------------------------------------- //
struct Point {
short x, y;
}
// returns true if a point is inside the rectangle
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)
{
return {
.x = a.x + b.x,
.y = 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 //
// ---------------------------------------------------------------------------------- //
struct Color{
char r, g, b, a;
}
macro Color uint.to_rgba(u)
{
return {
.r = (char)((u >> 24) & 0xff),
.g = (char)((u >> 16) & 0xff),
.b = (char)((u >> 8) & 0xff),
.a = (char)((u >> 0) & 0xff)
};
}
macro Color uint.@to_rgba($u)
{
return {
.r = (char)(($u >> 24) & 0xff),
.g = (char)(($u >> 16) & 0xff),
.b = (char)(($u >> 8) & 0xff),
.a = (char)(($u >> 0) & 0xff)
};
}
macro uint Color.to_uint(c)
{
uint u = c.r | (c.g << 8) | (c.b << 16) | (c.a << 24);
return u;
}

View File

@ -0,0 +1,136 @@
module ugui;
import std::io;
import std::math;
// slider element
struct ElemSlider {
Rect handle;
}
/* handle
* +----+-----+---------------------+
* | |#####| |
* +----+-----+---------------------+
*/
macro Ctx.slider_hor(&ctx, Rect size, float* value, float hpercent = 0.25, ...)
=> ctx.slider_hor_id(@compute_id($vasplat), size, value, hpercent);
<*
@require value != null
*>
fn ElemEvents? Ctx.slider_hor_id(&ctx, Id id, Rect size, float* value, float hpercent = 0.25)
{
id = ctx.gen_id(id)!;
Elem* parent = ctx.get_parent()!;
Elem* elem = ctx.get_elem(id, ETYPE_SLIDER)!;
Style* style = ctx.styles.get_style(@str_hash("slider"));
// 2. Layout
elem.bounds = ctx.layout_element(parent, size, style);
if (elem.bounds.is_null()) return {};
Rect content_bounds = elem.content_bounds(style);
// handle width
short hw = (short)(content_bounds.w * hpercent);
Rect handle = {
.x = calc_slider(content_bounds.x, content_bounds.w-hw, *value),
.y = content_bounds.y,
.w = hw,
.h = content_bounds.h,
};
elem.slider.handle = handle;
Point m = ctx.input.mouse.pos;
elem.events = ctx.get_elem_events(elem);
if (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);
elem.events.update = true;
}
// Draw the slider background and handle
Style s = *style;
Rect padding = s.padding;
s.padding = {};
ctx.push_rect(elem.bounds, parent.div.z_index, &s)!;
s.bg = s.primary;
s.padding = padding;
s.border = {};
ctx.push_rect(elem.slider.handle, parent.div.z_index, &s)!;
return elem.events;
}
/*
* +--+
* | |
* | |
* +--+
* |##| handle
* |##|
* +--+
* | |
* | |
* +--+
*/
macro Ctx.slider_ver(&ctx, Rect size, float* value, float hpercent = 0.25, ...)
=> ctx.slider_ver_id(@compute_id($vasplat), size, value, hpercent);
fn ElemEvents? Ctx.slider_ver_id(&ctx, Id id, Rect size, float* value, float hpercent = 0.25)
{
id = ctx.gen_id(id)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id, ETYPE_SLIDER)!;
Style* style = ctx.styles.get_style(@str_hash("slider"));
// 1. Fill the element fields
if (elem.flags.is_new) {
elem.type = ETYPE_SLIDER;
} else if (elem.type != ETYPE_SLIDER) {
return WRONG_ELEMENT_TYPE?;
}
// 2. Layout
elem.bounds = ctx.layout_element(parent, size, style);
if (elem.bounds.is_null()) return {};
Rect content_bounds = elem.content_bounds(style);
// handle height
short hh = (short)(content_bounds.h * hpercent);
Rect handle = {
.x = content_bounds.x,
.y = calc_slider(content_bounds.y, content_bounds.h-hh, *value),
.w = content_bounds.w,
.h = hh,
};
elem.slider.handle = handle;
Point m = ctx.input.mouse.pos;
elem.events = ctx.get_elem_events(elem);
if (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);
elem.events.update = true;
}
// Draw the slider background and handle
Style s = *style;
Rect padding = s.padding;
s.padding = {};
ctx.push_rect(elem.bounds, parent.div.z_index, &s)!;
s.bg = s.primary;
s.padding = padding;
s.border = {};
ctx.push_rect(elem.slider.handle, parent.div.z_index, &s)!;
return elem.events;
}
macro short calc_slider(short off, short dim, float value) => (short)off + (short)(dim * value);
macro float calc_value(short off, short mouse, short dim, short slider)
=> math::clamp((float)(mouse-off-slider/2)/(float)(dim-slider), 0.0f, 1.0f);

View File

@ -0,0 +1,153 @@
module ugui;
import std::core::mem::allocator;
import std::collections::map;
import std::io;
import std::compression::qoi;
const usz SRITES_PER_ATLAS = 64;
enum SpriteType {
SPRITE_NORMAL,
SPRITE_SDF,
SPRITE_MSDF,
SPRITE_ANIMATED,
}
struct Sprite {
Id id;
SpriteType type;
ushort u, v;
ushort w, h;
}
alias SpriteMap = map::HashMap{Id, Sprite};
struct SpriteAtlas {
Id id;
Atlas atlas;
SpriteMap sprites;
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)
{
// FIXME: for now only R8G8B8A8 format is supported
if (type != ATLAS_R8G8B8A8) {
return INVALID_TYPE?;
}
this.id = name.hash();
this.atlas.new(this.id, AtlasType.ATLAS_R8G8B8A8, width, height)!;
this.sprites.init(allocator::heap(), capacity: SRITES_PER_ATLAS);
this.should_update = false;
}
fn void? SpriteAtlas.free(&this)
{
this.atlas.free();
this.sprites.free();
}
// FIXME: this should throw an error when a different pixel format than the atlas' is used
// or convert from the source's pixel format to the atlas'
fn Sprite*? SpriteAtlas.insert(&this, String name, SpriteType type, char[] pixels, ushort w, ushort h, ushort stride)
{
Sprite s;
s.id = name.hash();
s.type = type;
Point uv = this.atlas.place(pixels, w, h, stride)!;
s.w = w;
s.h = h;
s.u = uv.x;
s.v = uv.y;
this.sprites.set(s.id, s);
this.should_update = true;
return this.sprites.get_ref(s.id);
}
fn Sprite*? SpriteAtlas.get(&this, String name)
{
Id id = name.hash();
return this.sprites.get_ref(id);
}
fn Sprite*? SpriteAtlas.get_by_id(&this, Id id)
{
return this.sprites.get_ref(id);
}
macro Rect Sprite.rect(s) => {0,0,s.w,s.h};
macro Rect Sprite.uv(s) => {s.u,s.v,s.w,s.h};
fn void? Ctx.sprite_atlas_create(&ctx, String name, AtlasType type, ushort w, ushort h)
{
ctx.sprite_atlas.init(name, type, w, h)!;
}
fn Id Ctx.get_sprite_atlas_id(&ctx, String name)
{
return name.hash();
}
fn void? Ctx.import_sprite_memory(&ctx, String name, char[] pixels, ushort w, ushort h, ushort stride, SpriteType type = SPRITE_NORMAL)
{
ctx.sprite_atlas.insert(name, type, pixels, w, h, stride)!;
}
fn void? Ctx.import_sprite_file_qoi(&ctx, String name, String path, SpriteType type = SPRITE_NORMAL)
{
QOIDesc desc;
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.layout_element(parent, bounds.off(off), style);
elem.sprite.id = ctx.get_sprite_atlas_id(name);
// if the bounds are null the element is outside the div view,
// no interaction should occur so just return
if (elem.bounds.is_null()) return;
Id tex_id = ctx.sprite_atlas.id;
return ctx.push_sprite(elem.bounds, uv, tex_id, parent.div.z_index)!;
}
fn void? Ctx.draw_sprite_raw(&ctx, String name, Rect bounds, bool center = false)
{
Elem *parent = ctx.get_parent()!;
Sprite* sprite = ctx.sprite_atlas.get(name)!;
Id tex_id = ctx.sprite_atlas.id;
if (center) {
Point off = {.x = (bounds.w - sprite.w) / 2, .y = (bounds.h - sprite.h) / 2};
bounds = bounds.off(off);
}
return ctx.push_sprite(bounds, sprite.uv(), tex_id, parent.div.z_index, type: sprite.type)!;
}

View File

@ -0,0 +1,436 @@
module ugui;
import std::collections::map;
import std::core::mem::allocator;
import std::io;
// global style, similar to the css box model
struct Style { // css box model
Rect padding;
Rect border;
Rect margin;
Color bg; // background color
Color fg; // foreground color
Color primary; // primary color
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 = 12,
.size = 16,
.bg = 0x282828ffu.@to_rgba(),
.fg = 0xfbf1c7ffu.@to_rgba(),
.primary = 0xcc241dffu.@to_rgba(),
.secondary = 0x458588ffu.@to_rgba(),
.accent = 0xfabd2fffu.@to_rgba(),
};
// style is stored in a hashmap, each style has an Id that can be generated by a string or whatever
alias StyleMap = map::HashMap{Id, Style};
// push or update a new style into the map
fn void StyleMap.register_style(&map, Style* style, Id id)
{
if (style == null) return;
map.set(id, *style);
}
// get a style from the map, if the style is not found then use a default style.
fn Style* StyleMap.get_style(&map, Id id)
{
Style*? s = map.get_ref(id);
if (catch s) {
// io::eprintfn("WARNING: style %x not found, using default style", id);
return &DEFAULT_STYLE;
}
return s;
}
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
map.register_style(&p.style, p.style_id);
if (p.lex.peep_token().type == EOF) break;
}
return added;
}
fn int Ctx.import_style_from_string(&ctx, String text) => ctx.styles.import_style_string(text);
fn int Ctx.import_style_from_file(&ctx, String path)
{
char[] text;
usz size = file::get_size(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;
}
/*
* 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;
* radius: uint;
* size: uint;
* Color: #RRGGBBAA;
* Color: #RRGGBBAA;
* Color: #RRGGBBAA;
* Color: #RRGGBBAA;
* Color: #RRGGBBAA;
* }
* 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.
*/
module ugui::css;
import std::ascii;
import std::io;
// CSS parser module
enum TokenType {
INVALID,
IDENTIFIER,
RCURLY,
LCURLY,
SEMICOLON,
COLON,
NUMBER,
COLOR,
EOF,
}
enum Unit {
PIXELS,
MILLIMETERS
}
struct Token {
TokenType type;
usz line, col, off;
String text;
union {
struct {
float value;
Unit unit;
}
Color color;
}
}
fn short Token.to_px(&t, float mm_to_px)
{
if (t.type != NUMBER) {
unreachable("WFT you cannot convert to pixels a non-number");
}
if (t.unit == PIXELS) return (short)(t.value);
return (short)(t.value * mm_to_px);
}
struct Lexer {
String text;
usz line, col, off;
}
macro char Lexer.peep(&lex) => lex.text[lex.off];
fn char Lexer.advance(&lex)
{
if (lex.off >= lex.text.len) return '\0';
char c = lex.text[lex.off];
if (c == '\n') {
lex.col = 0;
lex.line++;
} else {
lex.col++;
}
lex.off++;
return c;
}
fn Token Lexer.next_token(&lex)
{
Token t;
t.type = INVALID;
t.off = lex.off;
t.col = lex.col;
t.line = lex.line;
if (lex.off >= lex.text.len) {
return {.type = EOF};
}
// 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};
}
t.off = lex.off;
switch (true) {
case ascii::is_punct_m(lex.peep()) && lex.peep() != '#': // punctuation
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; }
usz hex_start = t.off+1;
while (ascii::is_alnum_m(lex.peep())) {
if (lex.advance() == 0) { t.type = INVALID; break; }
}
if (lex.off - hex_start != 8) {
io::eprintfn("CSS lexing error at %d:%d: the only suppported color format is #RRGGBBAA", t.line, t.col);
t.type = INVALID;
break;
}
char[10] hex_str = (char[])"0x";
hex_str[2..] = lex.text[hex_start..lex.off-1];
uint? color_hex = ((String)hex_str[..]).to_uint();
if (catch color_hex) {
t.type = INVALID;
break;
}
t.color = color_hex.to_rgba();
case ascii::is_alpha_m(lex.peep()): // identifier
t.type = IDENTIFIER;
while (ascii::is_alnum_m(lex.peep()) || lex.peep() == '-' || lex.peep() == '_') {
if (lex.advance() == 0) { t.type = INVALID; break; }
}
t.text = lex.text[t.off..lex.off-1];
case ascii::is_digit_m(lex.peep()): // number
t.type = NUMBER;
t.unit = PIXELS;
// find the end of the number
usz end;
while (ascii::is_alnum_m(lex.peep()) || lex.peep() == '+' || lex.peep() == '-' || lex.peep() == '.') {
if (lex.advance() == 0) { t.type = INVALID; break; }
}
end = lex.off;
if (end - t.off > 2) {
if (lex.text[end-2:2] == "px") {
t.unit = PIXELS;
end -= 2;
} else if (lex.text[end-2:2] == "mm") {
t.unit = MILLIMETERS;
end -= 2;
} else if (lex.text[end-2:2] == "pt" || lex.text[end-2:2] == "em") {
io::eprintn("units 'em' or 'pt' are not supported at the moment");
t.type = INVALID;
break;
}
}
String number_str = lex.text[t.off..end-1];
float? value = number_str.to_float();
if (catch value) { t.type = INVALID; break; }
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]);
}
return t;
}
fn Token Lexer.peep_token(&lex)
{
Lexer start_state = *lex;
Token t;
t = lex.next_token();
*lex = start_state;
return t;
}
struct Parser {
Lexer lex;
Style style;
Id style_id;
float mm_to_px;
}
macro bool Parser.expect(&p, Token* t, TokenType type)
{
*t = p.lex.next_token();
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;
}
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;
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;
return true;
}
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;
switch (prop.text) {
case "padding":
Rect padding;
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;
p.style.margin = margin;
case "bg":
Color bg;
if (p.parse_color(&bg) == false) return false;
p.style.bg = bg;
case "fg":
Color fg;
if (p.parse_color(&fg) == false) return false;
p.style.fg = fg;
case "primary":
Color primary;
if (p.parse_color(&primary) == false) return false;
p.style.primary = primary;
case "secondary":
Color secondary;
if (p.parse_color(&secondary) == false) return false;
p.style.secondary = secondary;
case "accent":
Color accent;
if (p.parse_color(&accent) == false) return false;
p.style.accent = accent;
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);
return false;
}
p.style.radius = (ushort)r;
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);
return false;
}
p.style.size = (ushort)s;
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;
return true;
}
fn bool Parser.parse_number(&p, short* n)
{
Token t;
if (p.expect(&t, NUMBER) == false) return false;
*n = t.to_px(p.mm_to_px);
return true;
}
// FIXME: since '#' is punctuation this cannot be done in parsing but it has to be done in lexing
fn bool Parser.parse_color(&p, Color* c)
{
Token t;
if (p.expect(&t, COLOR) == false) return false;
*c = t.color;
return true;
}
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
r.x = x;
if (p.parse_number(&x) == false) return false;
r.y = x;
if (p.parse_number(&x) == false) return false;
r.w = x;
if (p.parse_number(&x) == false) return false;
r.h = x;
return true;
} else if (t.type == SEMICOLON) {
// just one number, all dimensions are the same
r.x = r.y = r.w = r.h = x;
return true;
}
return false;
}

View File

@ -0,0 +1,79 @@
module ugui;
import std::io;
struct ElemText {
String str;
usz cursor; // cursor offset
Id hash;
Rect bounds;
}
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"));
Id text_hash = text.hash();
if (elem.flags.is_new || elem.text.hash != text_hash) {
elem.text.bounds = ctx.get_text_bounds(text)!;
}
elem.text.str = text;
elem.text.hash = text_hash;
// 2. Layout
elem.bounds = ctx.layout_element(parent, elem.text.bounds, 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 = (String)text;
// layout the text box
elem.bounds = ctx.layout_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;
ctx.push_rect(text_box, parent.div.z_index, style)!;
ctx.push_string(text_box, text[:*text_len], parent.div.z_index, style.fg, true)!;
// TODO: draw cursor
return elem.events;
}

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

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

1
lib/vendor Submodule

@ -0,0 +1 @@
Subproject commit db006221a8af625630fdb8b56707f3d07d4314a2

20
project.json Normal file
View File

@ -0,0 +1,20 @@
{
"langrev": "1",
"warnings": ["no-unused"],
"dependency-search-paths": ["lib", "lib/vendor/libraries"],
"dependencies": ["sdl3", "ugui"],
"features": [],
"authors": ["Alessandro Mauri <ale@shitposting.expert>"],
"version": "0.1.0",
"sources": ["src/**"],
"output": "build",
"target": "linux-x64",
"targets": {
"ugui": {
"type": "executable"
}
},
"cpu": "native",
"opt": "O0",
"debug-info": "full"
}

0
resources/.gitkeep Normal file
View File

BIN
resources/hack-nerd.ttf Normal file

Binary file not shown.

View File

@ -0,0 +1,33 @@
SOURCE_DIR := ./source
COMPILED_DIR := ./compiled
SOURCE_FILES := $(wildcard $(SOURCE_DIR)/*.glsl)
COMPILED_FILES := $(patsubst $(SOURCE_DIR)/%.glsl,$(COMPILED_DIR)/%.spv,$(SOURCE_FILES))
all: $(COMPILED_FILES)
@echo "Compiling shaders from $(SOURCE_DIR) -> $(COMPILED_DIR)"
$(COMPILED_DIR)/%.spv: $(SOURCE_DIR)/%.glsl
@mkdir -p $(COMPILED_DIR)
@stage=$$(basename $< .glsl | cut -d. -f2); \
if [ "$$stage" = "frag" ] || [ "$$stage" = "vert" ]; then \
echo "$$stage $(notdir $<) > $(notdir $@)"; \
glslc -O0 -g -fshader-stage=$$stage $< -o $@; \
else \
echo "Skipping $<: unsupported stage $$stage"; \
fi
$(COMPILED_DIR):
mkdir -p $(COMPILED_DIR)
.PHONY: clean
clean:
rm -rf $(COMPILED_DIR)
.PHONY: tree
tree:
tree $(COMPILED_DIR)
.PHONY: compile_all
compile_all: clean all tree

View File

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

View File

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

View File

@ -0,0 +1,27 @@
#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 = 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;
}
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 = 1.0 - smoothstep(0.0, 1.5, distance);
fragColor = vec4(in_color.rgb, in_color.a * alpha);
}

View File

@ -0,0 +1,29 @@
#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;
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));
}

View File

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

View File

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

61
resources/style.css Normal file
View File

@ -0,0 +1,61 @@
default {
bg: #282828ff;
fg: #fbf1c7ff;
primary: #cc241dff;
secondary: #458588ff;
accent: #fabd2fff;
border: 1;
}
button {
margin: 2;
border: 2;
padding: 2;
radius: 10;
size: 32;
bg: #3c3836ff;
fg: #fbf1c7ff;
primary: #cc241dff;
secondary: #458588ff;
accent: #504945ff;
}
checkbox {
margin: 2;
border: 2;
padding: 1;
radius: 10;
size: 16;
bg: #3c3836ff;
fg: #fbf1c7ff;
primary: #cc241dff;
secondary: #458588ff;
accent: #fabd2fff;
}
toggle {
margin: 2;
border: 2;
padding: 1;
radius: 10;
size: 16;
bg: #3c3836ff;
fg: #fbf1c7ff;
primary: #cc241dff;
secondary: #458588ff;
accent: #fabd2fff;
}
slider {
margin: 2;
padding: 2;
border: 1;
radius: 4;
size: 8;
bg: #3c3836ff;
fg: #fbf1c7ff;
primary: #cc241dff;
secondary: #458588ff;
accent: #fabd2fff;
}

BIN
resources/tick_sdf.qoi Normal file

Binary file not shown.

BIN
resources/tux.qoi Normal file

Binary file not shown.

BIN
resources/tux_inv.qoi Normal file

Binary file not shown.

0
scripts/.gitkeep Normal file
View File

0
src/.gitkeep Normal file
View File

359
src/main.c3 Normal file
View File

@ -0,0 +1,359 @@
import std::io;
import vtree;
import cache;
import ugui;
import std::time;
import std::collections::ringbuffer;
import std::core::string;
import std::ascii;
import sdlrenderer::ren;
import sdl3::sdl;
alias Times = ringbuffer::RingBuffer{time::NanoDuration[128]};
fn void Times.print_stats(&times)
{
time::NanoDuration min, max, avg, x;
min = times.get(0);
for (usz i = 0; i < times.written; i++) {
x = times.get(i);
if (x < min) { min = x; }
if (x > max) { max = x; }
avg += x;
}
avg = (NanoDuration)((ulong)avg/128.0);
io::printfn("min=%s, max=%s, avg=%s", min, max, avg);
}
struct TimeStats {
time::NanoDuration min, max, avg;
}
fn TimeStats Times.get_stats(&times)
{
time::NanoDuration min, max, avg, x;
min = times.get(0);
for (usz i = 0; i < times.written; i++) {
x = times.get(i);
if (x < min) { min = x; }
if (x > max) { max = x; }
avg += x;
}
avg = (NanoDuration)((ulong)avg/128.0);
return {.min = min, .max = max, .avg = avg};
}
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";
fn int main(String[] args)
{
ugui::Ctx ui;
ui.init()!!;
defer ui.free();
ren::Renderer ren;
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)!!;
// 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);
}
//
// 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);
}
//
// 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);
// CSS INPUT
io::printfn("imported %d styles", ui.import_style_from_file(STYLESHEET_PATH));
isz frame;
double fps;
time::Clock clock;
time::Clock fps_clock;
time::Clock sleep_clock;
Times ui_times;
Times draw_times;
//
// MAIN LOOP
//
sdl::start_text_input(ren.win);
sdl::Event e;
bool quit = false;
ugui::ModKeys mod;
ugui::MouseButtons btn;
while (!quit) {
clock.mark();
fps_clock.mark();
sleep_clock.mark();
do {
switch (e.type) {
case EVENT_QUIT:
quit = true;
case EVENT_KEY_UP: nextcase;
case EVENT_KEY_DOWN:
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;
// 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:
ui.input_text_utf8(e.text.text.str_view());
case EVENT_WINDOW_RESIZED:
ui.input_window_size((short)e.window.data1, (short)e.window.data2)!!;
case EVENT_WINDOW_FOCUS_GAINED:
ui.input_changefocus(true);
case EVENT_WINDOW_FOCUS_LOST:
ui.input_changefocus(false);
case EVENT_MOUSE_MOTION:
ui.input_mouse_abs((short)e.motion.x, (short)e.motion.y);
case EVENT_MOUSE_WHEEL:
ui.input_mouse_wheel((short)e.wheel.integer_x, (short)e.wheel.integer_y);
case EVENT_MOUSE_BUTTON_DOWN: nextcase;
case EVENT_MOUSE_BUTTON_UP:
sdl::MouseButtonFlags mb = sdl::get_mouse_state(null, null);
btn = {
.btn_left = !!(mb & BUTTON_LMASK),
.btn_right = !!(mb & BUTTON_RMASK),
.btn_middle = !!(mb & BUTTON_MMASK),
.btn_4 = !!(mb & BUTTON_X1MASK),
.btn_5 = !!(mb & BUTTON_X2MASK),
};
case EVENT_POLL_SENTINEL: break;
default:
io::eprintfn("unhandled event: %s", e.type);
}
} while(sdl::poll_event(&e));
ui.input_mod_keys(mod);
ui.input_mouse_button(btn);
/* End Input Handling */
/* Start UI Handling */
ui.frame_begin()!!;
if (ui.check_key_combo(ugui::KMOD_CTRL, "q")) quit = true;
const String APPLICATION = "calculator";
$if APPLICATION == "debug":
debug_app(&ui);
$endif
$if APPLICATION == "calculator":
calculator(&ui);
$endif
// Timings counter
TimeStats dts = draw_times.get_stats();
TimeStats uts = ui_times.get_stats();
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.input.keyboard.text_len]))!!;
};
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());
//draw_times.print_stats();
/* End Drawing */
// wait for the next event, timeout after 100ms
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()!!;
}
fn void calculator(ugui::Ctx* ui)
{
ui.div_begin(ugui::DIV_FILL)!!;
ui.layout_set_row()!!;
ui.div_begin({0,0,-300,50})!!;
ui.text_unbounded("80085")!!;
ui.div_end()!!;
ui.layout_next_row()!!;
ui.button("7")!!;
ui.button("8")!!;
ui.button("9")!!;
ui.layout_next_row()!!;
ui.button("4")!!;
ui.button("5")!!;
ui.button("6")!!;
ui.layout_next_row()!!;
ui.button("3")!!;
ui.button("2")!!;
ui.button("1")!!;
ui.layout_next_row()!!;
ui.button(".")!!;
ui.button("0")!!;
ui.button("")!!;
ui.layout_next_column()!!;
ui.layout_set_column()!!;
ui.button("+")!!;
ui.button("-")!!;
ui.button("*")!!;
ui.button("/")!!;
ui.div_end()!!;
}

962
src/renderer.c3 Normal file
View File

@ -0,0 +1,962 @@
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};
macro Type* IdList.get_from_name(&self, String name)
{
return self.get_from_id(name.hash());
}
macro Type* IdList.get_from_id(&self, id)
{
foreach(&s: self) {
if (s.id == id) {
return s;
}
}
return null;
}
module sdlrenderer::ren;
// 2D renderer for ugui, based on SDL3 using the new GPU API
import std::io;
import std::core::mem;
import sdl3::sdl;
import idlist;
import ugui;
struct Shader {
sdl::GPUShader* frag;
sdl::GPUShader* vert;
ugui::Id id;
}
struct Pipeline {
sdl::GPUGraphicsPipeline* pipeline;
ugui::Id id;
}
struct Texture {
sdl::GPUTexture* texture;
sdl::GPUSampler* sampler;
ushort width, height;
ugui::Id id;
}
// 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
sdl::GPUBuffer* attr_buf; // on-gpu quad attribute buffer
sdl::GPUTransferBuffer* attr_ts;
QuadAttributes[] attr_ts_mapped;
// how many quads are currently stored
int count;
bool initialized;
}
alias ShaderList = IdList{Shader};
alias PipelineList = IdList{Pipeline};
alias TextureList = IdList{Texture};
struct Renderer {
sdl::Window* win;
sdl::GPUDevice* gpu;
sdl::GPURenderPass* render_pass;
sdl::GPUTexture* swapchain_texture;
sdl::GPUCommandBuffer* render_cmdbuf;
QuadBuffer quad_buffer;
ShaderList shaders;
PipelineList pipelines;
TextureList textures;
Id sprite_atlas_id;
Id font_atlas_id;
int scissor_x, scissor_y, scissor_w, scissor_h;
}
// 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;
}
// 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;
fn void Renderer.init(&self, ZString title, uint width, uint height, bool vsync)
{
// set wayland hint automagically
$if DEBUG == 0:
bool has_wayland = false;
for (int i = 0; i < sdl::get_num_video_drivers(); i++) {
ZString driver = sdl::get_video_driver(i);
if (driver.str_view() == "wayland") {
has_wayland = true;
break;
}
}
if (has_wayland) {
sdl::set_hint(sdl::HINT_VIDEO_DRIVER, "wayland");
}
$else
// in debug mode set the video driver to X11 because renderdoc
// 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
// init subsystems
if (!sdl::init(INIT_VIDEO)) {
unreachable("sdl error: %s", sdl::get_error());
}
// create the window
self.win = sdl::create_window(title, width, height, WINDOW_RESIZABLE|WINDOW_VULKAN);
if (self.win == null) {
unreachable("sdl error: %s", sdl::get_error());
}
// get the gpu device handle
self.gpu = sdl::create_gpu_device(GPU_SHADERFORMAT_SPIRV, true, "vulkan");
if (self.gpu == null) {
unreachable("failed to create gpu device: %s", sdl::get_error());
}
if (!sdl::claim_window_for_gpu_device(self.gpu, self.win)) {
unreachable("failed to claim window for use with gpu: %s", sdl::get_error());
}
// set swapchain parameters, like vsync
GPUPresentMode present_mode = vsync ? GPU_PRESENTMODE_VSYNC : GPU_PRESENTMODE_IMMEDIATE;
sdl::set_gpu_swapchain_parameters(self.gpu, self.win, GPU_SWAPCHAINCOMPOSITION_SDR, present_mode);
//
// initialize the quad buffer
// ==========================
QuadBuffer* qb = &self.quad_buffer;
// since instanced rendering is used, on the gpu there is only one mesh, a single quad.
// create the vertex and index buffer on the gpu
qb.vert_buf = sdl::create_gpu_buffer(self.gpu,
&&(GPUBufferCreateInfo){.usage = GPU_BUFFERUSAGE_VERTEX, .size = Quad.vertices.sizeof}
);
if (qb.vert_buf == null) {
unreachable("failed to initialize quad buffer (vertex): %s", sdl::get_error());
}
qb.idx_buf = sdl::create_gpu_buffer(self.gpu,
&&(GPUBufferCreateInfo){.usage = GPU_BUFFERUSAGE_INDEX, .size = Quad.indices.sizeof}
);
if (qb.idx_buf == null) {
unreachable("failed to initialize quad buffer (index): %s", sdl::get_error());
}
qb.attr_buf = sdl::create_gpu_buffer(self.gpu,
&&(GPUBufferCreateInfo){.usage = GPU_BUFFERUSAGE_VERTEX, .size = QuadAttributes.sizeof * MAX_QUAD_BATCH}
);
if (qb.attr_buf == null) {
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}
);
if (ts == null) {
unreachable("failed to create gpu transfer buffer: %s", sdl::get_error());
}
Quad* quad = (Quad*)sdl::map_gpu_transfer_buffer(self.gpu, ts, false);
/* v1 v4
* +-------------+
* | _/|
* | _/ |
* | 1 _/ |
* | _/ |
* | _/ |
* | _/ 2 |
* |/ |
* +-------------+
* v2 v3
*/
quad.vertices.v1 = {.x = 0, .y = 0};
quad.vertices.v2 = {.x = 0, .y = 1};
quad.vertices.v3 = {.x = 1, .y = 1};
quad.vertices.v4 = {.x = 1, .y = 0};
// triangle 1 indices
quad.indices.i1 = 0; // v1
quad.indices.i2 = 1; // v2
quad.indices.i3 = 3; // v4
// triangle 2 indices
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);
if (cmd == null) {
unreachable("failed to upload quad at acquiring command buffer: %s", sdl::get_error());
}
GPUCopyPass* cpy = sdl::begin_gpu_copy_pass(cmd);
// upload vertices
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,
&&(GPUTransferBufferLocation){.transfer_buffer = ts, .offset = Quad.indices.offsetof},
&&(GPUBufferRegion){.buffer = qb.idx_buf, .offset = 0, .size = Quad.indices.sizeof},
false
);
sdl::end_gpu_copy_pass(cpy);
if (!sdl::submit_gpu_command_buffer(cmd)) {
unreachable("failed to upload quads at submit command buffer: %s", sdl::get_error());
}
sdl::release_gpu_transfer_buffer(self.gpu, ts);
// create and map the quad attributes transfer buffer
qb.attr_ts = sdl::create_gpu_transfer_buffer(self.gpu,
&&(GPUTransferBufferCreateInfo){.usage = GPU_TRANSFERBUFFERUSAGE_UPLOAD, .size = QuadAttributes.sizeof * MAX_QUAD_BATCH}
);
if (qb.attr_ts == null) {
unreachable("failed to create gpu transfer buffer: %s", sdl::get_error());
}
qb.attr_ts_mapped = ((QuadAttributes*)sdl::map_gpu_transfer_buffer(self.gpu, qb.attr_ts, false))[:MAX_QUAD_BATCH];
if (qb.attr_ts_mapped.ptr == null) {
unreachable("failed to map vertex or index buffers: %s", sdl::get_error());
}
qb.initialized = true;
}
fn void Renderer.free(&self)
{
foreach (&s: self.shaders) {
sdl::release_gpu_shader(self.gpu, s.frag);
sdl::release_gpu_shader(self.gpu, s.vert);
}
self.shaders.free();
foreach (&p: self.pipelines) {
sdl::release_gpu_graphics_pipeline(self.gpu, p.pipeline);
}
self.pipelines.free();
foreach (&t: self.textures) {
sdl::release_gpu_texture(self.gpu, t.texture);
sdl::release_gpu_sampler(self.gpu, t.sampler);
}
self.textures.free();
QuadBuffer* qb = &self.quad_buffer;
sdl::unmap_gpu_transfer_buffer(self.gpu, qb.attr_ts);
sdl::release_gpu_transfer_buffer(self.gpu, qb.attr_ts);
sdl::release_gpu_buffer(self.gpu, qb.vert_buf);
sdl::release_gpu_buffer(self.gpu, qb.idx_buf);
sdl::release_gpu_buffer(self.gpu, qb.attr_buf);
sdl::release_window_from_gpu_device(self.gpu, self.win);
sdl::destroy_gpu_device(self.gpu);
sdl::destroy_window(self.win);
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);
}
// 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)
{
Shader s;
s.id = name.hash();
if (vert_code.len == 0 || frag_code.len == 0) {
unreachable("vertex shader and fragment shader cannot be empty");
}
if (vert_code.len > 0) {
// FIXME: these should be passed by parameter and/or automatically determined by parsing
// the shader code
GPUShaderCreateInfo shader_info = {
.code = vert_code.ptr,
.code_size = vert_code.len,
.entrypoint = "main",
.format = GPU_SHADERFORMAT_SPIRV,
.stage = GPU_SHADERSTAGE_VERTEX,
.num_samplers = 0,
.num_uniform_buffers = 1+uniforms,
.num_storage_buffers = 0,
.num_storage_textures = 0
};
s.vert = sdl::create_gpu_shader(self.gpu, &shader_info);
if (s.vert == null) {
unreachable("failed to create gpu vertex shader: %s", sdl::get_error());
}
}
if (frag_code.len > 0) {
// FIXME: these should be passed by parameter and/or automatically determined by parsing
// the shader code
GPUShaderCreateInfo shader_info = {
.code = frag_code.ptr,
.code_size = frag_code.len,
.entrypoint = "main",
.format = GPU_SHADERFORMAT_SPIRV,
.stage = GPU_SHADERSTAGE_FRAGMENT,
.num_samplers = textures,
.num_uniform_buffers = 1,
.num_storage_buffers = 0,
.num_storage_textures = 0
};
s.frag = sdl::create_gpu_shader(self.gpu, &shader_info);
if (s.frag == null) {
unreachable("failed to create gpu fragment shader: %s", sdl::get_error());
}
}
// push the shader into the list
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)
{
if (vert_path == "" || frag_path == "") {
unreachable("need both a vertex shader and fragment shader path");
}
char[] vert_code;
char[] frag_code;
// create vertex shader
usz size = file::get_size(vert_path)!!;
vert_code = mem::new_array(char, size + size%4);
file::load_buffer(vert_path, vert_code)!!;
defer mem::free(vert_code);
// create fragment shader
size = file::get_size(frag_path)!!;
frag_code = mem::new_array(char, size + size%4);
file::load_buffer(frag_path, frag_code)!!;
defer mem::free(frag_code);
self.load_spirv_shader_from_mem(name, vert_code, frag_code, textures, uniforms);
}
// 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) {
RECT = {GPU_PRIMITIVETYPE_TRIANGLELIST, {.fill_mode = GPU_FILLMODE_FILL, .cull_mode = GPU_CULLMODE_NONE, .front_face = GPU_FRONTFACE_COUNTER_CLOCKWISE}},
SPRITE = {GPU_PRIMITIVETYPE_TRIANGLELIST, {.fill_mode = GPU_FILLMODE_FILL, .cull_mode = GPU_CULLMODE_NONE, .front_face = GPU_FRONTFACE_COUNTER_CLOCKWISE}},
LINE = {GPU_PRIMITIVETYPE_LINELIST, {.fill_mode = GPU_FILLMODE_LINE, .cull_mode = GPU_CULLMODE_NONE, .front_face = GPU_FRONTFACE_COUNTER_CLOCKWISE}},
}
// create a graphics pipeline to draw to the window using a set of vertex/fragment shaders
// the pipeline is pushed into the renderer's pipeline list and it will have the same id as
// the shader set.
fn void Renderer.create_pipeline(&self, String shader_name, PipelineType type)
{
Shader *s = self.shaders.get_from_name(shader_name);
if (s == null) {
unreachable("error in creating pipeline: no shader named %s", shader_name);
}
GPUGraphicsPipelineCreateInfo ci = {
.vertex_shader = s.vert,
.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.
.vertex_input_state = {
// the description of each vertex buffer, for now I use only one buffer
.vertex_buffer_descriptions = (GPUVertexBufferDescription[]){
{ // first slot, per-vertex attributes
.slot = 0,
.pitch = Vertex.sizeof,
.input_rate = GPU_VERTEXINPUTRATE_VERTEX,
},
{ // second slot, per-instance attributes
.slot = 1,
.pitch = QuadAttributes.sizeof,
.input_rate = GPU_VERTEXINPUTRATE_INSTANCE,
}
},
.num_vertex_buffers = 2,
// the description of each vertex
.vertex_attributes = (GPUVertexAttribute[]){
{ // at location zero there is the position of the vertex
.location = 0,
.buffer_slot = 0, // buffer slot zero so per-vertex
.format = GPU_VERTEXELEMENTFORMAT_SHORT2, // x,y
.offset = 0,
},
{ // at location one there is the per-quad position
.location = 1,
.buffer_slot = 1, // buffer slot one so per-instance
.format = GPU_VERTEXELEMENTFORMAT_SHORT4, // x,y,w,h
.offset = QuadAttributes.pos.offsetof,
},
{ // at location two there are the per-quad uv coordinates
.location = 2,
.buffer_slot = 1,
.format = GPU_VERTEXELEMENTFORMAT_SHORT2,
.offset = QuadAttributes.uv.offsetof,
},
{ // at location three there is the quad color
.location = 3,
.buffer_slot = 1,
.format = GPU_VERTEXELEMENTFORMAT_UBYTE4,
.offset = QuadAttributes.color.offsetof,
}
},
.num_vertex_attributes = 4,
},
// the pipeline's primitive type and rasterizer state differs based on what needs to
// be drawn
.primitive_type = type.primitive_type,
.rasterizer_state = type.raster_state,
.multisample_state = {}, // no multisampling, all zeroes
.depth_stencil_state = {}, // no stencil test, all zeroes
.target_info = { // the target (texture) description
.color_target_descriptions = (GPUColorTargetDescription[]){{
// rendering happens to the window, so get it's format
.format = sdl::get_gpu_swapchain_texture_format(self.gpu, self.win),
.blend_state = {
// alpha blending on everything
// https://en.wikipedia.org/wiki/Alpha_compositing
.src_color_blendfactor = GPU_BLENDFACTOR_SRC_ALPHA,
.dst_color_blendfactor = GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
.color_blend_op = GPU_BLENDOP_ADD,
.src_alpha_blendfactor = GPU_BLENDFACTOR_SRC_ALPHA,
.dst_alpha_blendfactor = GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
.alpha_blend_op = GPU_BLENDOP_ADD,
.enable_blend = true,
// color write mask is not enabled so all rgba channels are written to
},
}},
.num_color_targets = 1,
.depth_stencil_format = {}, // FIXME: no stencil, no depth buffering
.has_depth_stencil_target = false,
},
};
// create the pipeline and add it to the pipeline list
Pipeline p = {
.id = s.id,
.pipeline = sdl::create_gpu_graphics_pipeline(self.gpu, &ci),
};
if (p.pipeline == null) {
unreachable("failed to create pipeline (shaders: %s, type: %s): %s", shader_name, type.nameof, sdl::get_error());
}
self.pipelines.push(p);
}
// 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
}
macro void Renderer.new_texture(&self, name_or_id, TextureType type, char[] pixels, uint width, uint height)
{
$switch $typeof(name_or_id):
$case uint: return self.new_texture_by_id(id, type, pixels, width, height);
$case String: return self.new_texture_by_id(name_or_id.hash(), type, pixels, width, height);
$default: unreachable("texture must have a name (String) or an id (uint)");
$endswitch
}
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):
$case uint: return self.update_texture_by_id(name_or_id, pixels, width, height, x, y);
$case String: return self.update_texture_by_id(name_or_id.hash(), pixels, width, height, x, y);
$default: unreachable("texture must have a name (String) or an id (uint)");
$endswitch
}
// 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,
.format = type.format,
// all textures are used with samplers, which means read-only textures that contain data to be sampled
.usage = GPU_TEXTUREUSAGE_SAMPLER,
.width = width,
.height = height,
.layer_count_or_depth = 1,
.num_levels = 1, // no mip maps so just one level
// .sample_count not used since the texture is not a render target
};
GPUTexture* texture = sdl::create_gpu_texture(self.gpu, &tci);
if (texture == null) {
unreachable("failed to create texture (id: %s, type: %s): %s", id, type.nameof, sdl::get_error());
}
// the sampler description, how the texture should be sampled
GPUSamplerCreateInfo sci = {
.min_filter = GPU_FILTER_LINEAR, // linear interpolation for textures
.mag_filter = GPU_FILTER_LINEAR,
.mipmap_mode = GPU_SAMPLERMIPMAPMODE_NEAREST,
.address_mode_u = GPU_SAMPLERADDRESSMODE_REPEAT, // tiling textures
.address_mode_v = GPU_SAMPLERADDRESSMODE_REPEAT,
.address_mode_w = GPU_SAMPLERADDRESSMODE_REPEAT,
// everything else is not used and not needed
};
GPUSampler* sampler = sdl::create_gpu_sampler(self.gpu, &sci);
if (sampler == null) {
unreachable("failed to create sampler (texture id: %s, type: %s): %s", id, type.nameof, sdl::get_error());
}
Texture t = {
.id = id,
.texture = texture,
.sampler = sampler,
};
self.textures.push(t);
// upload the texture data
self.update_texture_by_id(id, pixels, width, height, 0, 0);
}
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);
if (t == null || t.texture == null) {
unreachable("failed updating texture: no texture with id %s", id);
}
GPUTexture* texture = t.texture;
// FIXME: do a better job at validating the copy
if (x > t.width || y > t.height) {
unreachable("failed updating texture: attempting to copy outside of the texture region");
}
// upload image data
GPUCommandBuffer* cmdbuf = sdl::acquire_gpu_command_buffer(self.gpu);
if (cmdbuf == null) {
unreachable("failed to upload texture data at acquiring command buffer: %s", sdl::get_error());
}
GPUCopyPass* copypass = sdl::begin_gpu_copy_pass(cmdbuf);
if (copypass == null) {
unreachable("failed to upload texture data at beginning copy pass: %s", sdl::get_error());
}
GPUTransferBuffer* buf = sdl::create_gpu_transfer_buffer(self.gpu,
&&(GPUTransferBufferCreateInfo){.usage = GPU_TRANSFERBUFFERUSAGE_UPLOAD, .size = pixels.len}
);
if (buf == null) {
unreachable("failed to upload texture data at creating the transfer buffer: %s", sdl::get_error());
}
char* gpu_mem = (char*)sdl::map_gpu_transfer_buffer(self.gpu, buf, CYCLE);
if (gpu_mem == null) {
unreachable("failed to upload texture data at mapping the transfer buffer: %s", sdl::get_error());
}
// copy the data to the driver's memory
gpu_mem[:pixels.len] = pixels[..];
sdl::unmap_gpu_transfer_buffer(self.gpu, buf);
// upload the data to gpu memory
sdl::upload_to_gpu_texture(copypass,
&&(GPUTextureTransferInfo){.transfer_buffer = buf, .offset = 0},
&&(GPUTextureRegion){.texture = texture, .x = x, .y = y, .w = width, .h = height, .d = 1},
false
);
sdl::end_gpu_copy_pass(copypass);
if (!sdl::submit_gpu_command_buffer(cmdbuf)) {
unreachable("failed to upload texture data at command buffer submission: %s", sdl::get_error());
}
sdl::release_gpu_transfer_buffer(self.gpu, buf);
}
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
};
return self.map_quad(qa);
}
fn bool Renderer.push_quad(&self, short x, short y, short w, short h, uint color, ushort radius = 0)
{
QuadAttributes qa = {
.pos = {.x = x, .y = y, .w = w, .h = h},
.uv = {.u = radius, .v = radius},
.color = color
};
return self.map_quad(qa);
}
// this does not upload a quad, but it simply copies the quad data to the correct transfer buffers.
// Data transfer to the GPU only happens in draw_quads() to save time
fn bool Renderer.map_quad(&self, QuadAttributes qa)
{
if (self.quad_buffer.count >= MAX_QUAD_BATCH) {
return false;
}
QuadBuffer* qb = &self.quad_buffer;
// upload the quad data to the gpu
if (qb.initialized == false) {
unreachable("quad buffer not initialized");
}
qb.attr_ts_mapped[qb.count] = qa;
qb.count++;
return true;
}
fn void Renderer.upload_quads(&self)
{
QuadBuffer* qb = &self.quad_buffer;
GPUCommandBuffer* cmd = sdl::acquire_gpu_command_buffer(self.gpu);
if (cmd == null) {
unreachable("failed to upload quad at acquiring command buffer: %s", sdl::get_error());
}
GPUCopyPass* cpy = sdl::begin_gpu_copy_pass(cmd);
// upload quad attributes
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
);
sdl::end_gpu_copy_pass(cpy);
if (!sdl::submit_gpu_command_buffer(cmd)) {
unreachable("failed to upload quads at submit command buffer: %s", sdl::get_error());
}
}
// draw all quads in the quad buffer, since uniforms are per-drawcall it makes no sense
// to draw them one a the time
fn void Renderer.draw_quads(&self, uint off, uint count)
{
QuadBuffer* qb = &self.quad_buffer;
// too many quads to draw
if (off >= qb.count || count > qb.count - off) {
unreachable("too many quads, have %d, requested %d, offset %d", qb.count, count, off);
}
sdl::bind_gpu_vertex_buffers(self.render_pass, 0,
(GPUBufferBinding[]){
{.buffer = qb.vert_buf, .offset = 0},
{.buffer = qb.attr_buf, .offset = 0},
}, 2);
sdl::bind_gpu_index_buffer(self.render_pass, &&(GPUBufferBinding){.buffer = qb.idx_buf, .offset = 0}, GPU_INDEXELEMENTSIZE_16BIT);
sdl::draw_gpu_indexed_primitives(self.render_pass, 6, count, 0, 0, off);
}
fn void Renderer.reset_quads(&self)
{
self.quad_buffer.count = 0;
}
fn void Renderer.begin_render(&self, bool clear_screen)
{
self.render_cmdbuf = sdl::acquire_gpu_command_buffer(self.gpu);
sdl::wait_and_acquire_gpu_swapchain_texture(self.render_cmdbuf, self.win, &self.swapchain_texture, null, null);
// push the window size as a uniform
// TODO: maybe make this configurable and/or add more things
ViewsizeUniform v;
self.get_window_size(&v.w, &v.h);
sdl::push_gpu_vertex_uniform_data(self.render_cmdbuf, 0, &v, ViewsizeUniform.sizeof);
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,
&&(GPUColorTargetInfo){
.texture = self.swapchain_texture,
.mip_level = 0,
.layer_or_depth_plane = 0,
.clear_color = {.r = 1.0, .g = 0.0, .b = 1.0, .a = 1.0},
.load_op = GPU_LOADOP_CLEAR, // clear the screen at the start of the render pass
.store_op = GPU_STOREOP_STORE,
.resolve_texture = null,
.resolve_mip_level = 0,
.resolve_layer = 0,
.cycle = false,
.cycle_resolve_texture = false
},
1,
null // huh
);
if (pass == null) {
unreachable("render pass creation went wrong: %s", sdl::get_error());
}
sdl::end_gpu_render_pass(pass);
}
}
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,
&&(GPUColorTargetInfo){
.texture = self.swapchain_texture,
.mip_level = 0,
.layer_or_depth_plane = 0,
.clear_color = {.r = 0.0, .g = 0.0, .b = 0.0, .a = 1.0},
.load_op = GPU_LOADOP_DONT_CARE,
.store_op = GPU_STOREOP_STORE,
.resolve_texture = null,
.resolve_mip_level = 0,
.resolve_layer = 0,
.cycle = false,
.cycle_resolve_texture = false
},
1,
null // huh
);
if (self.render_pass == null) {
unreachable("render pass creation went wrong: %s", sdl::get_error());
}
sdl::GPUGraphicsPipeline* p;
p = self.pipelines.get_from_name(pipeline_name).pipeline;
if (p == null) {
unreachable("no pipeline");
}
sdl::bind_gpu_graphics_pipeline(self.render_pass, p);
}
fn void Renderer.end_render_pass(&self)
{
sdl::end_gpu_render_pass(self.render_pass);
}
fn void Renderer.bind_texture(&self, String texture_name)
{
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.set_scissor(&self, int x, int y, int w, int h)
{
// in vulkan scissor size must be positive, clamp to zero
w = max(w, 0);
h = max(h, 0);
sdl::set_gpu_scissor(self.render_pass, &&(sdl::Rect){x,y,w,h});
}
fn void Renderer.reset_scissor(&self)
{
int w, h;
sdl::get_window_size(self.win, &w, &h);
self.set_scissor(0, 0, w, h);
}
/// === NOTES ===
/* 1. The uniform data is per-render pass. So you can do:
* - push uniform
* - draw 1
* - draw 2
* But not:
* - push uniform
* - draw
* - push new uniform
* - draw
* And not even:
* - 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.
*/
/// === END NOTES ===
fn void Renderer.render_ugui(&self, CmdQueue* queue)
{
// upload pass
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);
} else if (c.type == CMD_SPRITE) {
CmdSprite s = c.sprite;
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();
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;
char[] pixels = u.raw_buffer[..u.width*u.height*u.bpp];
self.update_texture(u.id, pixels, u.width, u.height);
case CMD_SCISSOR:
ugui::Rect s = cmd.scissor.rect;
if (s.x == 0 && s.y == 0 && s.w == 0 && s.h == 0) {
self.get_window_size((int*)&s.w, (int*)&s.h);
}
self.scissor_x = s.x;
self.scissor_y = s.y;
self.scissor_w = s.w;
self.scissor_h = s.h;
default: unreachable("unknown command: %s", cmd.type);
}
last_command = cmd;
}
self.end_command(last_command, off, count);
}
fn void Renderer.begin_command(&self, Cmd* cmd)
{
if (cmd == null) return;
switch (cmd.type) {
case CMD_RECT:
self.start_render_pass("UGUI_PIPELINE_RECT");
self.set_scissor(self.scissor_x, self.scissor_y, self.scissor_w, self.scissor_h);
case CMD_SPRITE:
// TODO: support multiple sprite and font atlases
CmdSprite s = cmd.sprite;
String pipeline;
String texture;
if (s.texture_id == self.sprite_atlas_id) {
switch (s.type) {
case SPRITE_NORMAL: pipeline = "UGUI_PIPELINE_SPRITE";
case SPRITE_SDF: pipeline = "UGUI_PIPELINE_SPRITE_SDF";
case SPRITE_MSDF: pipeline = "UGUI_PIPELINE_SPRITE_MSDF";
case SPRITE_ANIMATED: unreachable("animated sprtes are unsupported for now");
default: unreachable("unknown sprite type %s", s.type);
}
texture = "icons";
} else if (s.texture_id == self.font_atlas_id) {
pipeline = "UGUI_PIPELINE_FONT";
texture = "font1";
}
self.start_render_pass(pipeline);
self.bind_texture(texture);
self.set_scissor(self.scissor_x, self.scissor_y, self.scissor_w, self.scissor_h);
case CMD_UPDATE_ATLAS: break;
case CMD_SCISSOR: break;
default: unreachable("unknown command: %s", cmd.type);
}
}
fn void Renderer.end_command(&self, Cmd* cmd, uint off, uint count)
{
if (cmd == null) return;
switch (cmd.type) {
case CMD_RECT: nextcase;
case CMD_SPRITE:
self.draw_quads(off, count);
self.end_render_pass();
case CMD_UPDATE_ATLAS: break;
case CMD_SCISSOR: break;
default: unreachable("unknown command: %s", cmd.type);
}
}

View File

@ -1,137 +0,0 @@
#ifndef _HASH_GENERIC
#define _HASH_GENERIC
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
// FIXME: change the api to just one HASH_DECL to HASH_PROTO and HASH_DEFINE
#define HASH_MAXSIZE 4096
// for fibonacci hashing, 2^{32,64}/<golden ratio>
#define HASH_RATIO32 ((uint64_t)2654435769u)
#define HASH_RATIO64 ((uint64_t)11400714819322457583u)
// salt for string hashing
#define HASH_SALT ((uint64_t)0xbabb0cac)
/* Ready-made compares */
static inline int hash_cmp_u32(uint32_t a, uint32_t b) { return a == b; }
static inline int hash_cmp_u64(uint64_t a, uint64_t b) { return a == b; }
static inline int hash_cmp_str(const char *a, const char *b) { return strcmp(a, b) == 0; }
/* Ready-made hashes */
static inline uint32_t hash_u64(uint64_t c)
{
return (uint64_t)((((uint64_t)c+HASH_SALT)*HASH_RATIO64)>>32);
}
static inline uint32_t hash_u32(uint32_t c)
{
return (uint32_t)((((uint64_t)c<<31)*HASH_RATIO64)>>32);
}
static inline uint32_t hash_str(const char *s)
{
uint32_t h = HASH_SALT;
const uint8_t *v = (const uint8_t *)(s);
for (int x = *s; x; x--) {
h += v[x-1];
h += h << 10;
h ^= h >> 6;
}
h += h << 3;
h ^= h >> 11;
h += h << 15;
return h;
}
#define HASH_DECL(htname, codetype, datatype, hashfn, cmpfn) \
struct htname##_entry { \
codetype code; \
datatype data; \
}; \
\
struct htname##_ref { \
uint32_t items, size, exp; \
struct htname##_entry bucket[]; \
}; \
\
\
struct htname##_ref * htname##_create(uint32_t size) \
{ \
if (!size || size > HASH_MAXSIZE) \
return NULL; \
/* round to the greater power of two */ \
/* FIXME: check for intger overflow here */ \
uint32_t exp = 32-__builtin_clz(size-1); \
size = 1<<(exp); \
/* FIXME: check for intger overflow here */ \
struct htname##_ref *ht = malloc(sizeof(struct htname##_ref)+sizeof(struct htname##_entry)*size); \
if (ht) { \
ht->items = 0; \
ht->size = size; \
ht->exp = exp; \
memset(ht->bucket, 0, sizeof(struct htname##_entry)*size); \
} \
return ht; \
} \
\
\
void htname##_destroy(struct htname##_ref *ht) \
{ \
if (ht) free(ht); \
} \
\
\
static uint32_t htname##_lookup(struct htname##_ref *ht, uint32_t hash, uint32_t idx) \
{ \
if (!ht) return 0; \
uint32_t mask = ht->size-1; \
uint32_t step = (hash >> (32 - ht->exp)) | 1; \
return (idx + step) & mask; \
} \
\
\
/* Find and return the element by code */ \
struct htname##_entry * htname##_search(struct htname##_ref *ht, codetype code)\
{ \
if (!ht) return NULL; \
uint32_t h = hashfn(code); \
for (uint32_t i=h, x=0; ; x++) { \
i = htname##_lookup(ht, h, i); \
if (x > (ht->size<<1) || \
!ht->bucket[i].code || \
cmpfn(ht->bucket[i].code, code) \
) { \
return &(ht->bucket[i]); \
} \
} \
return NULL; \
} \
\
\
/* FIXME: this simply overrides the found item */ \
struct htname##_entry * htname##_insert(struct htname##_ref *ht, struct htname##_entry *entry) \
{ \
struct htname##_entry *r = htname##_search(ht, entry->code); \
if (r) { \
if (!r->code) \
ht->items++; \
*r = *entry; \
} \
return r; \
} \
\
\
struct htname##_entry * htname##_remove(struct htname##_ref *ht, codetype code)\
{ \
if (!ht) return NULL; \
struct htname##_entry *r = htname##_search(ht, code); \
if (r) r->code = 0; \
return r; \
} \
#endif

View File

@ -1,118 +0,0 @@
#ifndef _STACK_GENERIC_H
#define _STACK_GENERIC_H
#define STACK_STEP 8
#define STACK_SALT 0xbabb0cac
// FIXME: find a way to not re-hash the whole stack when removing one item
// incremental hash for every grow
#if STACK_DISABLE_HASH
#define STACK_HASH(p, s, h) {}
#else
#define STACK_HASH(p, s, h) \
{ \
unsigned char *v = (unsigned char *)(p); \
for (int x = (s); x; x--) { \
(h) += v[x-1]; \
(h) += (h) << 10; \
(h) ^= (h) >> 6; \
} \
(h) += (h) << 3; \
(h) ^= (h) >> 11; \
(h) += (h) << 15; \
}
#endif
// TODO: add a rolling hash
#define STACK_DECL(stackname, type) \
struct stackname { \
type *items; \
int size, idx, old_idx; \
unsigned int hash, old_hash; \
}; \
\
\
struct stackname stackname##_init(void) \
{ \
return (struct stackname){0, .hash = STACK_SALT}; \
} \
\
\
int stackname##_grow(struct stackname *stack, int step) \
{ \
if (!stack) \
return -1; \
stack->items = realloc(stack->items, (stack->size+step)*sizeof(type)); \
if(!stack->items) \
return -1; \
memset(&(stack->items[stack->size]), 0, step*sizeof(*(stack->items))); \
stack->size += step; \
return 0; \
} \
\
\
int stackname##_push(struct stackname *stack, type *e) \
{ \
if (!stack || !e) \
return -1; \
if (stack->idx >= stack->size) \
if (stackname##_grow(stack, STACK_STEP)) \
return -1; \
stack->items[stack->idx++] = *e; \
STACK_HASH(e, sizeof(type), stack->hash); \
return 0; \
} \
\
\
type stackname##_pop(struct stackname *stack) \
{ \
if (!stack || stack->idx == 0 || stack->size == 0) \
return (type){0}; \
stack->hash = STACK_SALT; \
STACK_HASH(stack->items, sizeof(type)*(stack->idx-1), stack->hash); \
return stack->items[stack->idx--]; \
} \
\
\
int stackname##_clear(struct stackname *stack) \
{ \
if (!stack) \
return -1; \
stack->old_idx = stack->idx; \
stack->old_hash = stack->hash; \
stack->hash = STACK_SALT; \
stack->idx = 0; \
return 0; \
} \
\
\
int stackname##_changed(struct stackname *stack) \
{ \
if (!stack) \
return -1; \
return stack->hash != stack->old_hash; \
} \
\
\
int stackname##_size_changed(struct stackname *stack) \
{ \
if (!stack) \
return -1; \
return stack->size != stack->old_idx; \
} \
\
\
int stackname##_free(struct stackname *stack) \
{ \
if (stack) { \
stackname##_clear(stack); \
if (stack->items) \
free(stack->items); \
} \
return 0; \
} \
#endif

View File

@ -1,214 +0,0 @@
//#!/usr/bin/tcc -run
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define HASH_SALT (0xbabb0cac)
#define HASH_RATIO64 ((unsigned long int)11400714819322457583u)
struct ntree_node
{
unsigned int id;
const char *name;
int children_no, children_size;
struct ntree_node *children, *parent;
};
static char tmp_name_buffer[16] = {"node:"};
unsigned int hash_str_to_uint(const char *s)
{
unsigned int h = HASH_SALT;
const unsigned char *v = (const unsigned char *)(s);
for (int x = *s; x; x--) {
h += v[x-1];
h += h << 10;
h ^= h >> 6;
}
h += h << 3;
h ^= h >> 11;
h += h << 15;
return h;
}
unsigned int hash_u32(unsigned int c)
{
return (unsigned int)((((unsigned long int)c<<31)*HASH_RATIO64)>>32);
}
const char * generate_new_name(void)
{
static int count = 0;
unsigned int h = hash_u32(count++);
snprintf(tmp_name_buffer+sizeof("node:"), 16-sizeof("node:"), "%x", h);
return tmp_name_buffer;
}
int tree_prune(struct ntree_node *node)
{
if (node == NULL)
return 0;
if ((node->children_no == 0 && node->children != NULL) ||
(node->children_no != 0 && node->children == NULL)) {
printf("ERR: (%x %s) Inconsistent node children", node->id, node->name);
} else for (int i = 0; i < node->children_no; i++) {
tree_prune(&(node->children[i]));
}
if (node->children != NULL) {
free(node->children);
}
free((void *)node->name);
if (node->parent != NULL)
node->parent->children_no--;
*node = (struct ntree_node){0};
return 0;
}
struct ntree_node * tree_append(struct ntree_node *parent, const char *name)
{
if (name == NULL)
return NULL;
if (strlen(name) == 0)
name = generate_new_name();
// generate the children information
unsigned int id = hash_str_to_uint(name);
char *str = malloc(strlen(name)+1);
if (str == NULL)
return NULL;
strcpy(str, name);
// grow the parent buffer if necessary
if (parent->children_no >= parent->children_size) {
struct ntree_node *temp = NULL;
temp = realloc(parent->children, (parent->children_size+1)*sizeof(struct ntree_node));
if (temp == NULL) {
free(str);
return NULL;
}
parent->children = temp;
parent->children[parent->children_size] = (struct ntree_node){0};
parent->children_size++;
}
// find an open spot for the child
struct ntree_node *child = NULL;
for (int i = 0; i < parent->children_size; i++) {
if (parent->children[i].id == 0)
child = &(parent->children[i]);
}
if (child == NULL)
return NULL;
child->name = str;
child->id = id;
child->children = NULL;
child->children_no = 0;
child->parent = parent;
parent->children_no++;
//printf("append to %s, children: %d\n", parent->name, parent->children_no);
return child;
}
struct ntree_node * tree_find_id(struct ntree_node *root, unsigned int id)
{
if (id == 0 || root == NULL)
return NULL;
if (root->id == id)
return root;
else for (int i = 0; i < root->children_size; i++) {
if (root->children[i].id != 0 && tree_find_id(&(root->children[i]), id) != NULL)
return &(root->children[i]);
}
return NULL;
}
// TODO: add a foreach_child function
struct ntree_node * tree_find(struct ntree_node *root, const char *name)
{
if (name == NULL || strlen(name) == 0 || root == NULL)
return NULL;
unsigned int id = hash_str_to_uint(name);
return tree_find_id(root, id);
}
int tree_size(struct ntree_node *root)
{
if (root == NULL)
return 0;
int count = root->children_no;
if (count > 0) {
for (int i = 0; i < root->children_size; i++) {
if (root->children[i].id != 0)
count += tree_size(&(root->children[i]));
}
}
return count;
}
static int tree_print_ind(struct ntree_node *node, int dd)
{
for (int i = 0; i < dd; i++) printf(" ");
printf("[%s]\n", node->name);
for (int i = 0; i < node->children_size; i++) {
if (node->children[i].id == 0) continue;
tree_print_ind(&(node->children[i]), dd+1);
}
return node->children_no;
}
int tree_print(struct ntree_node *root)
{
if (root == NULL)
return 1;
tree_print_ind(root, 0);
return 0;
}
int main(void)
{
char *root_name = malloc(sizeof("root"));
strcpy(root_name, "root");
struct ntree_node root = {.name = root_name};
struct ntree_node *n;
n = tree_append(&root, "node 0:0");
tree_append(n, "node 0:0:0");
tree_append(&root, "node 0:1");
tree_append(&root, "node 0:2");
printf("Number of nodes %d\n", tree_size(&root));
tree_print(&root);
tree_prune(tree_find(&root, "node 0:0"));
printf("Number of nodes %d\n", tree_size(&root));
tree_prune(&root);
printf("Number of nodes %d\n", tree_size(&root));
return 0;
}

View File

@ -1,348 +0,0 @@
#ifndef _VECTREE_H
#define _VECTREE_H
#ifdef VTREE_DTYPE
typedef struct {
int size, elements;
VTREE_DTYPE *vector;
int *refs;
} Vtree;
int vtree_init(Vtree *tree, unsigned int size);
int vtree_pack(Vtree *tree);
int vtree_resize(Vtree *tree, unsigned int newsize);
int vtree_add(Vtree *tree, VTREE_DTYPE elem, int parent);
int vtree_prune(Vtree *tree, int ref);
int vtree_subtree_size(Vtree *tree, int ref);
int vtree_children_it(Vtree *tree, int parent, int *cursor);
int vtree_level_order_it(Vtree *tree, int ref, int **queue_p, int *cursor);
int vtree_destroy(Vtree *tree);
#ifdef VTREE_IMPL
#define IS_VALID_REF(t, r) ((r) >= 0 && (r) < (t)->size)
#define REF_IS_PRESENT(t, r) ((t)->refs[r] >= 0)
int vtree_init(Vtree *tree, unsigned int size)
{
if (tree == NULL) {
return -1;
}
tree->vector = malloc(sizeof(VTREE_DTYPE) * size);
if (tree->vector == NULL) {
return -1;
}
tree->refs = malloc(sizeof(int) * size);
if (tree->refs == NULL) {
free(tree->vector);
return -1;
}
// set all refs to -1, meaning invalid (free) element
for (unsigned int i = 0; i < size; i++) {
tree->refs[i] = -1;
}
// fill vector with zeroes
memset(tree->vector, 0, size * sizeof(VTREE_DTYPE));
tree->size = size;
tree->elements = 0;
return 0;
}
int vtree_destroy(Vtree *tree)
{
if (tree == NULL) {
return -1;
}
free(tree->vector);
free(tree->refs);
return 0;
}
int vtree_pack(Vtree *tree)
{
if (tree == NULL) {
return -1;
}
// TODO: add a PACKED flag to skip this
int free_spot = -1;
for (int i = 0; i < tree->size; i++) {
if (tree->refs[i] == -1) {
free_spot = i;
continue;
}
// find a item that can be packed
if (free_spot >= 0 && tree->refs[i] >= 0) {
int old_ref = i;
// move the item
tree->vector[free_spot] = tree->vector[i];
tree->refs[free_spot] = tree->refs[i];
tree->vector[i] = (VTREE_DTYPE){0};
tree->refs[i] = -1;
// and move all references
for (int x = 0; x < tree->size; x++) {
if (tree->refs[x] == old_ref) {
tree->refs[x] = free_spot;
}
}
// mark the free spot as used
free_spot = -1;
}
}
return 0;
}
int vtree_resize(Vtree *tree, unsigned int newsize)
{
if (tree == NULL) {
return -1;
}
// return error when shrinking with too many elements
if ((int)newsize < tree->elements) {
return -1;
}
// pack the vector when shrinking to avoid data loss
if ((int)newsize < tree->size) {
//if (vtree_pack(tree) < 0) {
// return -1;
//}
// TODO: allow shrinking, since packing destroys all references
return -1;
}
VTREE_DTYPE *newvec = realloc(tree->vector, newsize * sizeof(VTREE_DTYPE));
if (newvec == NULL) {
return -1;
}
int *newrefs = realloc(tree->refs, newsize * sizeof(int));
if (newrefs == NULL) {
return -1;
}
tree->vector = newvec;
tree->refs = newrefs;
if ((int)newsize > tree->size) {
for (int i = tree->size; i < (int)newsize; i++) {
tree->vector[i] = (VTREE_DTYPE){0};
tree->refs[i] = -1;
}
}
tree->size = newsize;
return 0;
}
// add an element to the tree, return it's ref
int vtree_add(Vtree *tree, VTREE_DTYPE elem, int parent)
{
if (tree == NULL) {
return -1;
}
// invalid parent
if (!IS_VALID_REF(tree, parent)) {
return -1;
}
// no space left
if (tree->elements >= tree->size) {
return -1;
}
// check if the parent exists
// if there are no elements in the tree the first add will set the root
if (!REF_IS_PRESENT(tree, parent) && tree->elements != 0) {
return -1;
}
// get the first free spot
int free_spot = -1;
for (int i = 0; i < tree->size; i++) {
if (tree->refs[i] == -1) {
free_spot = i;
break;
}
}
if (free_spot < 0) {
return -1;
}
// finally add the element
tree->vector[free_spot] = elem;
tree->refs[free_spot] = parent;
tree->elements++;
return free_spot;
}
// prune the tree starting from the ref
// returns the number of pruned elements
int vtree_prune(Vtree *tree, int ref)
{
if (tree == NULL) {
return -1;
}
if (!IS_VALID_REF(tree, ref)) {
return -1;
}
if (!REF_IS_PRESENT(tree, ref)) {
return 0;
}
tree->vector[ref] = (VTREE_DTYPE){0};
tree->refs[ref] = -1;
int count = 1;
for (int i = 0; i < tree->size; i++) {
if (tree->refs[i] == ref) {
count += vtree_prune(tree, i);
}
}
return count;
}
// find the size of the subtree starting from ref
int vtree_subtree_size(Vtree *tree, int ref)
{
if (tree == NULL) {
return -1;
}
if (!IS_VALID_REF(tree, ref)) {
return -1;
}
if (!REF_IS_PRESENT(tree, ref)) {
return 0;
}
int count = 1;
for (int i = 0; i < tree->size; i++) {
// only root has the reference to itself
if (tree->refs[i] == ref && ref != i) {
count += vtree_subtree_size(tree, i);
}
}
return count;
}
// iterate through the first level children, use a cursor like strtok_r
int vtree_children_it(Vtree *tree, int parent, int *cursor)
{
if (tree == NULL || cursor == NULL) {
return -1;
}
// if the cursor is out of bounds then we are done for sure
if (!IS_VALID_REF(tree, *cursor)) {
return -1;
}
// same for the parent, if it's invalid it can't have children
if (!IS_VALID_REF(tree, parent) || !REF_IS_PRESENT(tree, parent)) {
return -1;
}
// find the first child, update the cursor and return the ref
for (int i = *cursor; i < tree->size; i++) {
if (tree->refs[i] == parent) {
*cursor = i + 1;
return i;
}
}
// if no children are found return -1
*cursor = -1;
return -1;
}
/* iterates trough every leaf of the subtree in the following manner
* node [x], x: visit order
* [0]
* / | \
* / [2] [3]
* [1] |
* / \ [6]
* [4] [5]
*/
int vtree_level_order_it(Vtree *tree, int ref, int **queue_p, int *cursor)
{
if (tree == NULL || queue_p == NULL || cursor == NULL) {
return -1;
}
int *queue = *queue_p;
// TODO: this could also be done when adding or removing elements
// first call, create a ref array ordered like we desire
if (queue == NULL) {
*cursor = 0;
// create a queue of invalid refs, size is the worst case
queue = malloc(sizeof(int) * tree->size);
if (queue == NULL) {
return -1;
}
for (int i = 0; i < tree->size; i++) {
queue[i] = -1;
}
*queue_p = queue;
// iterate through the queue appending found children
int pos = 0, off = 0;
do {
//printf ("ref=%d\n", ref);
for (int i = 0; i < tree->size; i++) {
if (tree->refs[i] == ref) {
queue[pos++] = i;
}
}
for (;ref == queue[off] && off < tree->size; off++);
ref = queue[off];
} while (IS_VALID_REF(tree, ref));
}
//PRINT_ARR(queue, tree->size);
//return -1;
// on successive calls just iterate through the queue until we find an
// invalid ref
int ret = queue[(*cursor)++];
if (!IS_VALID_REF(tree, ret)) {
free(queue);
}
return ret;
}
#endif // VTREE_IMPL
#endif // VTREE_DTYPE
#endif

0
test/.gitkeep Normal file
View File

14
test/test_bitsruct.c3 Normal file
View File

@ -0,0 +1,14 @@
bitstruct Bits : uint {
bool a : 0;
bool b : 1;
bool c : 2;
}
fn int main()
{
Bits a = {false, true, false};
Bits b = {true, true, false};
Bits c = a | b;
return 0;
}

10
test/test_bittype.c3 Normal file
View File

@ -0,0 +1,10 @@
import std::io;
import std::collections::bitset;
def Bits = bitset::BitSet(<128>);
fn void main()
{
Bits b;
io::printn($typeof(b.data[0]).sizeof);
}

19
test/test_color.c3 Normal file
View File

@ -0,0 +1,19 @@
import std::io;
struct Color { short r,g,b,a; }
macro Color uint.to_rgba(uint u)
{
return Color{
.r = (char)((u >> 24) & 0xff),
.g = (char)((u >> 16) & 0xff),
.b = (char)((u >> 8) & 0xff),
.a = (char)((u >> 0) & 0xff)
};
}
fn void main(String[] args)
{
uint col = args[1].to_uint()!!;
io::printn(col.to_rgba());
}

14
test/test_custom_hash.c3 Normal file
View File

@ -0,0 +1,14 @@
import std::collections::map;
def Codepoint = uint;
fn uint Codepoint.hash(Codepoint code) => code < 128 ? code : ((uint)code).hash();
def CodeMap = map::HashMap(<Codepoint, Codepoint>);
fn int main()
{
CodeMap m;
m.new_init();
m.free();
return 0;
}

72
test/test_error.c3 Normal file
View File

@ -0,0 +1,72 @@
import std::io;
struct FaultStack {
usz elem;
anyfault[16] v;
}
fn void FaultStack.push(&fs, anyfault f)
{
if (fs.elem < fs.v.len) {
fs.v[fs.elem++] = f;
}
}
fn anyfault FaultStack.pop(&fs)
{
return fs.elem > 0 ? fs.v[fs.elem-- - 1] : anyfault{};
}
FaultStack fs;
fn int! err1()
{
return IoError.OUT_OF_SPACE?;
}
fn void! err2()
{
return IoError.EOF?;
}
/*
macro @unwrap(#f)
{
$if ($typeof(#f).typeid == void!.typeid) {
if (catch err = #f) { fs.push(err); }
return;
} $else {
$typeof(#f) x = #f;
if (catch err = x) {
fs.push(err);
return $typeof(#f!!){};
} else {return x;}
}
}
*/
<*
@require @typekind(#func) == OPTIONAL : `@unwrap requires an optional value`
*>
macro @unwrap(#func)
{
anyfault exc = @catch(#func);
if (exc != anyfault{}) {
fs.push(exc);
$if $typeof(#func!!).typeid != void.typeid:
return $typeof(#func!!){};
$else
return;
$endif
} else {
return #func!!;
}
}
fn void main()
{
@unwrap(err1());
@unwrap(err2());
io::printfn("%s", fs.v);
}

6
test/test_font.c3 Normal file
View File

@ -0,0 +1,6 @@
import rl;
fn int main(void)
{
return 0;
}

30
test/test_idgen.c3 Normal file
View File

@ -0,0 +1,30 @@
import std::io;
alias Id = uint;
fn void foo_ex(Id id)
{
io::printfn("id = %d", id);
}
macro Id @compute_id(...)
{
Id id = (Id)$$LINE.hash() ^ (Id)@str_hash($$FILE);
$for var $i = 0; $i < $vacount; $i++:
id ^= (Id)$vaconst[$i].hash();
$endfor
return id;
}
macro foo(...) => foo_ex(@compute_id($vasplat));
fn int main()
{
foo_ex(1234);
foo();
foo();
foo();
return 0;
}

26
test/test_keyboard.c3 Normal file
View File

@ -0,0 +1,26 @@
import rl;
import std::io;
fn int main(String[] args)
{
short width = 800;
short height = 450;
rl::set_config_flags(rl::FLAG_WINDOW_RESIZABLE);
rl::init_window(width, height, "Ugui Test");
rl::set_target_fps(60);
rl::enable_event_waiting();
// Main loop
KeyboardKey k;
while (!rl::window_should_close()) {
do {
k = rl::get_char_pressed();
io::printfn("%s", k);
} while (k != 0);
}
rl::close_window();
return 0;
}

26
test/test_union.c3 Normal file
View File

@ -0,0 +1,26 @@
struct CmdA {
int a, b;
}
struct CmdB {
float a, b;
}
union AnyCmd {
CmdA a;
CmdB b;
}
struct Cmd {
int type;
AnyCmd cmd;
}
fn int main()
{
Cmd c;
c.type = 1;
c.cmd.a = {.a = 1, .b = 2};
return 0;
}

7
test/test_vtree.c3 Normal file
View File

@ -0,0 +1,7 @@
import std::io;
import vtree;
fn int main()
{
return 0;
}

226
test/ugui_font.c3 Normal file
View File

@ -0,0 +1,226 @@
module ugui;
import cache;
//#include <grapheme.h>
//#include <assert.h>
//#include "stb_truetype.h"
//#include "stbimage_write.h"
// unicode code point, different type for a different hash
def Codepoint = uint;
/* width and height of a glyph contain the kering advance
* (u,v)
* +-------------*---+ -
* | ^ | | ^
* | |oy | | |
* | v | | |
* | .ii. | | |
* | @@@@@@. |<->| |
* | V@Mio@@o |adv| |h
* | :i. V@V | | |
* | :oM@@M | | |
* | :@@@MM@M | | |
* | @@o o@M | | |
* |<->:@@. M@M | | |
* |ox @@@o@@@@ | | |
* | :M@@V:@@.| | v
* +-------------*---+ -
* |<------------->|
* w
*/
struct Glyph {
Codepoint code;
uint u, v;
ushort w, h, a, x, y;
}
def GlyphCache = cache::Cache(<Codepoint, Glyph, 1024>);
// identity map the ASCII range
fn uint Codepoint.hash(Codepoint code) => code < 128 ? code : ((uint)code).hash();
struct FontAtlas {
uint width, height;
char* atlas;
uint glyph_max_w, glyph_max_h;
int size;
int file_size;
char *file;
void *priv;
}
macro is_utf8(char c) => c & 0x80;
const uint BDEPTH = 1;
const uint BORDER = 4;
// FIXME: as of now only monospaced fonts look decent since no
// kerning information is stored
struct Priv @private {
stbtt_fontinfo stb;
float scale;
int baseline;
unsigned char *bitmap;
struct cache c;
}
//#define PRIV(x) ((struct priv *)x->priv)
struct font_atlas * font_init(void)
{
struct font_atlas *p = emalloc(sizeof(struct font_atlas));
memset(p, 0, sizeof(struct font_atlas));
p->priv = emalloc(sizeof(struct priv));
memset(p->priv, 0, sizeof(struct priv));
PRIV(p)->c = cache_init();
return p;
}
// loads a font into memory, storing all the ASCII characters in the atlas, each font
// atlas structure holds glyphs of a specific size in pixels
// NOTE: size includes ascend and descend (so 12 does not mean that 'A' is 12px tall)
int font_load(struct font_atlas *atlas, const char *path, int size)
{
if (!atlas || !path)
return -1;
int err;
dump_file(path, &(atlas->file), &(atlas->file_size));
err = stbtt_InitFont(&(PRIV(atlas)->stb), (unsigned char *)atlas->file, 0);
if (err == 0) return -1;
int ascent, descent, linegap, baseline;
int x0,y0,x1,y1;
float scale;
stbtt_GetFontVMetrics(&(PRIV(atlas)->stb), &ascent, &descent, &linegap);
stbtt_GetFontBoundingBox(&(PRIV(atlas)->stb), &x0, &y0, &x1, &y1);
scale = stbtt_ScaleForPixelHeight(&(PRIV(atlas)->stb), size);
baseline = scale * -y0;
atlas->glyph_max_w = (scale*x1) - (scale*x0);
atlas->glyph_max_h = (baseline+scale*y1) - (baseline+scale*y0);
atlas->atlas = emalloc(CACHE_SIZE*BDEPTH*atlas->glyph_max_w*atlas->glyph_max_h);
memset(atlas->atlas, 0, CACHE_SIZE*BDEPTH*atlas->glyph_max_w*atlas->glyph_max_h);
PRIV(atlas)->baseline = atlas->glyph_max_h - baseline;
PRIV(atlas)->scale = scale;
PRIV(atlas)->bitmap = emalloc(BDEPTH*atlas->glyph_max_w*atlas->glyph_max_h);
// FIXME: make this a square atlas
atlas->width = atlas->glyph_max_w*CACHE_SIZE/4;
atlas->height = atlas->glyph_max_h*4;
atlas->size = size;
// preallocate all ascii characters
for (char c = ' '; c <= '~'; c++) {
if (!font_get_glyph_texture(atlas, c, NULL))
return -1;
}
return 0;
}
int font_free(struct font_atlas *atlas)
{
efree(atlas->atlas);
efree(atlas->file);
efree(PRIV(atlas)->bitmap);
cache_free(&PRIV(atlas)->c);
efree(atlas->priv);
efree(atlas);
return 0;
}
// TODO: time and take the median of the time it takes to generate the cache and
// the time it takes to draw the glyph
const struct font_glyph * font_get_glyph_texture(struct font_atlas *atlas, unsigned int code, int *updated)
{
int _u = 0;
if (!updated) updated = &_u;
const struct font_glyph *r;
if ((r = cache_search(&PRIV(atlas)->c, code)) != NULL) {
*updated = 0;
return r;
}
*updated = 1;
// generate the sdf and put it into the cache
// TODO: generate the whole block at once
int idx = stbtt_FindGlyphIndex(&PRIV(atlas)->stb, code);
int x0,y0,x1,y1,gw,gh,l,off_x,off_y,adv,base;
base = atlas->glyph_max_h - PRIV(atlas)->baseline;
stbtt_GetGlyphBitmapBoxSubpixel(
&PRIV(atlas)->stb,
idx,
PRIV(atlas)->scale,
PRIV(atlas)->scale,
0,0,
&x0,&y0,
&x1, &y1);
gw = x1 - x0;
gh = y1 - y0;
stbtt_GetGlyphHMetrics(&PRIV(atlas)->stb, idx, &adv, &l);
adv *= PRIV(atlas)->scale;
off_x = PRIV(atlas)->scale*l;
off_y = atlas->glyph_max_h+y0;
stbtt_MakeGlyphBitmapSubpixel(
&PRIV(atlas)->stb,
PRIV(atlas)->bitmap,
atlas->glyph_max_w,
atlas->glyph_max_h,
atlas->glyph_max_w,
PRIV(atlas)->scale,
PRIV(atlas)->scale,
0, 0,
idx);
// TODO: bounds check usign atlas height
// TODO: clear spot area in the atlas before writing on it
unsigned int spot = cache_get_free_spot(&PRIV(atlas)->c);
unsigned int ty = ((atlas->glyph_max_w * spot) / atlas->width) * atlas->glyph_max_h;
unsigned int tx = (atlas->glyph_max_w * spot) % atlas->width;
unsigned int w = atlas->width;
unsigned char *a = (void *)atlas->atlas;
//printf("max:%d %d spot:%d : %d %d %d %d\n", atlas->glyph_max_w, atlas->glyph_max_h, spot, tx, ty, off_x, off_y);
for (int y = 0; y < gh; y++) {
for (int x = 0; x < gw; x++) {
int c, r;
r = (ty+y)*w;
c = tx+x;
a[r+c] = PRIV(atlas)->bitmap[y*atlas->glyph_max_w+x];
}
}
struct font_glyph g = {
.codepoint = code,
.u = tx,
.v = ty,
.w = gw,
.h = gh,
.x = off_x,
.y = off_y-base,
.a = adv,
};
return cache_insert_at(&PRIV(atlas)->c, &g, g.codepoint, spot);
}
void font_dump(const struct font_atlas *atlas, const char *path)
{
stbi_write_png(
path,
atlas->width,
atlas->height,
BDEPTH,
atlas->atlas,
BDEPTH*atlas->width);
}

94
test_renderer.c3 Normal file
View File

@ -0,0 +1,94 @@
import sdlrenderer::ren;
import std::io;
import std::thread;
import sdl3::sdl;
import std::compression::qoi;
import std::core::mem::allocator;
char[*] shader_rect_vert = $embed("resources/shaders/compiled/rect.vert.spv");
char[*] shader_rect_frag = $embed("resources/shaders/compiled/rect.frag.spv");
char[*] shader_sprite_vert = $embed("resources/shaders/compiled/sprite.vert.spv");
char[*] shader_sprite_frag = $embed("resources/shaders/compiled/sprite.frag.spv");
const uint WINDOW_WIDTH = 640;
const uint WINDOW_HEIGHT = 480;
fn int main()
{
ren::Renderer ren;
ren.init("test window", WINDOW_WIDTH, WINDOW_HEIGHT);
// TODO: these could be the same function
ren.load_spirv_shader_from_mem("rect shader", &shader_rect_vert, &shader_rect_frag, 0, 0);
ren.create_pipeline("rect shader", RECT);
// load the tux qoi image
QOIDesc img_desc;
char[] img_pixels = qoi::read(allocator::temp(), "resources/tux.qoi", &img_desc)!!;
// and put it in a texture
ren.new_texture("tux", FULL_COLOR, img_pixels, img_desc.width, img_desc.height);
// create a new pipeline to use the texture
ren.load_spirv_shader_from_mem("sprite shader", &shader_sprite_vert, &shader_sprite_frag, 1, 0);
ren.create_pipeline("sprite shader", SPRITE);
sdl::Event e;
bool quit = false;
for (usz i = 0; !quit; i++) {
if (sdl::poll_event(&e)) {
if (e.type == EVENT_QUIT) {
quit = true;
}
}
if (i == 300) {
io::printn("ciao!");
img_pixels = qoi::read(allocator::temp(), "resources/tux_inv.qoi", &img_desc)!!;
ren.update_texture("tux", img_pixels, img_desc.width, img_desc.height);
}
ren.begin_render(true);
// Colored Rectangles Render Pass
ren.start_render_pass("rect shader");
// rect 1
ren.push_quad(100,100,100,100,0xff00ff00, 20);
// rect 2
ren.push_quad(0,0,20,20,0xff0000ff);
// rect 3
ren.push_quad(200,300,50,50,0xffff0000);
//ren.set_scissor(0,50,200,300);
ren.draw_quads();
ren.end_render_pass();
// End Rectangle Render Pass
// Textured Rectangles Render Pass
ren.start_render_pass("sprite shader");
// bind the pipeline's sampler
ren.bind_texture("tux");
// tux
ren.push_sprite(300, 0, 54, 64, 0, 0);
ren.reset_scissor();
ren.draw_quads();
ren.end_render_pass();
// End Textured Rectangle Render Pass
ren.end_render();
}
ren.free();
return 0;
}

69
timer.c
View File

@ -1,69 +0,0 @@
#define _POSIX_C_SOURCE 200809l
#include <time.h>
#include "timer.h"
const clockid_t CLOCK_ID = CLOCK_PROCESS_CPUTIME_ID;
struct _timer {
struct timespec start, stop;
struct timespec times[TIMER_MAX_PARTIAL];
} timer = {0};
int timer_start(void) { return clock_gettime(CLOCK_ID, &timer.start); }
int timer_reset(void)
{
timer = (struct _timer) {0};
return 0;
}
int timer_stop(void) { return clock_gettime(CLOCK_ID, &timer.stop); }
// partial clocks also update the stop time
int timer_partial(int idx)
{
if (idx > TIMER_MAX_PARTIAL || idx < 0) {
return -1;
}
clock_gettime(CLOCK_ID, &timer.stop);
return clock_gettime(CLOCK_ID, &(timer.times[idx]));
}
size_t timer_get_us(int idx)
{
if (idx > TIMER_MAX_PARTIAL) {
return -1;
}
struct timespec ts = {0};
if (idx < 0) {
ts = timer.stop;
} else {
ts = timer.times[idx];
}
ts.tv_sec -= timer.start.tv_sec;
ts.tv_nsec -= timer.start.tv_nsec;
// FIXME: check overflow
return (ts.tv_nsec / 1000) + (ts.tv_sec * 1000000);
}
double timer_get_sec(int idx)
{
if (idx > TIMER_MAX_PARTIAL) {
return -1;
}
struct timespec ts = {0};
if (idx < 0) {
ts = timer.stop;
} else {
ts = timer.times[idx];
}
ts.tv_sec -= timer.start.tv_sec;
ts.tv_nsec -= timer.start.tv_nsec;
return (double)ts.tv_sec + ((double)ts.tv_nsec / 1e9);
}

16
timer.h
View File

@ -1,16 +0,0 @@
#ifndef UG_TIMER_H_
#define UG_TIMER_H_
#include <stdlib.h>
#define TIMER_MAX_PARTIAL 10
int timer_start(void);
int timer_stop(void);
int timer_reset(void);
int timer_partial(int idx);
size_t timer_get_us(int idx);
double timer_get_sec(int idx);
#endif

1061
ugui.c

File diff suppressed because it is too large Load Diff

105
ugui.h
View File

@ -1,105 +0,0 @@
#ifndef _UGUI_H
#define _UGUI_H
#include <stdint.h>
typedef struct {
int32_t x, y, w, h;
} UgRect;
typedef struct {
int32_t x, y;
} UgPoint;
typedef struct {
uint8_t r, g, b, a;
} UgColor;
typedef uint64_t UgId;
typedef enum {
ETYPE_NONE = 0,
ETYPE_DIV,
ETYPE_BUTTON,
} UgElemType;
enum UgElemFlags {
ELEM_UPDATED = 1 << 0,
ELEM_HASFOCUS = 1 << 1,
};
enum UgElemEvent {
EVENT_KEY_PRESS = 1 << 0,
EVENT_KEY_RELEASE = 1 << 1,
EVENT_KEY_HOLD = 1 << 2,
EVENT_MOUSE_HOVER = 1 << 3,
EVENT_MOUSE_PRESS = 1 << 4,
EVENT_MOUSE_RELEASE = 1 << 5,
EVENT_MOUSE_HOLD = 1 << 6,
};
typedef struct {
UgId id;
uint32_t flags;
uint32_t event;
UgRect rect;
UgElemType type;
// type-specific fields
union {
struct UgDiv {
enum {
DIV_LAYOUT_ROW = 0,
DIV_LAYOUT_COLUMN,
DIV_LAYOUT_FLOATING,
} layout;
UgPoint origin_r, origin_c;
UgColor color_bg;
} div; // Div
};
} UgElem;
// TODO: add a packed flag
// TODO: add a fill index to skip some searching for free spots
typedef struct {
int size, elements;
UgId *vector; // vector of element ids
int *refs, *ordered_refs;
} UgTree;
typedef struct {
struct _IdTable *table;
UgElem *array;
uint64_t *present, *used;
int cycles;
} UgElemCache;
typedef struct _UgCtx UgCtx;
// tree implementation
int ug_tree_init(UgTree *tree, unsigned int size);
int ug_tree_pack(UgTree *tree);
int ug_tree_resize(UgTree *tree, unsigned int newsize);
int ug_tree_add(UgTree *tree, UgId elem, int parent);
int ug_tree_prune(UgTree *tree, int ref);
int ug_tree_subtree_size(UgTree *tree, int ref);
int ug_tree_children_it(UgTree *tree, int parent, int *cursor);
int ug_tree_level_order_it(UgTree *tree, int ref, int *cursor);
int ug_tree_parentof(UgTree *tree, int node);
int ug_tree_destroy(UgTree *tree);
UgId ug_tree_get(UgTree *tree, int node);
// cache implementation
UgElemCache ug_cache_init(void);
void ug_cache_free(UgElemCache *cache);
UgElem *ug_cache_search(UgElemCache *cache, UgId id);
UgElem *ug_cache_insert_new(UgElemCache *cache, const UgElem *g, uint32_t *index);
int ug_init(UgCtx *ctx);
int ug_destroy(UgCtx *ctx);
int ug_frame_begin(UgCtx *ctx);
int ug_frame_end(UgCtx *ctx);
#endif // _UGUI_H

355
vectree.c
View File

@ -1,355 +0,0 @@
#include <stdlib.h>
#include <string.h>
#include "ugui.h"
#define IS_VALID_REF(t, r) ((r) >= 0 && (r) < (t)->size)
#define REF_IS_PRESENT(t, r) ((t)->refs[r] >= 0)
int ug_tree_init(UgTree *tree, unsigned int size)
{
if (tree == NULL) {
return -1;
}
tree->vector = malloc(sizeof(UgId) * size);
if (tree->vector == NULL) {
return -1;
}
tree->refs = malloc(sizeof(int) * size);
if (tree->refs == NULL) {
free(tree->vector);
return -1;
}
// ordered refs are used in the iterators
tree->ordered_refs = malloc(sizeof(int) * (size + 1));
if (tree->ordered_refs == NULL) {
free(tree->vector);
free(tree->refs);
return -1;
}
// set all refs to -1, meaning invalid (free) element
for (unsigned int i = 0; i < size; i++) {
tree->refs[i] = -1;
}
// fill vector with zeroes
memset(tree->vector, 0, size * sizeof(UgId));
tree->size = size;
tree->elements = 0;
return 0;
}
int ug_tree_destroy(UgTree *tree)
{
if (tree == NULL) {
return -1;
}
free(tree->vector);
free(tree->refs);
return 0;
}
int ug_tree_pack(UgTree *tree)
{
if (tree == NULL) {
return -1;
}
// TODO: add a PACKED flag to skip this
int free_spot = -1;
for (int i = 0; i < tree->size; i++) {
if (tree->refs[i] == -1) {
free_spot = i;
continue;
}
// find a item that can be packed
if (free_spot >= 0 && tree->refs[i] >= 0) {
int old_ref = i;
// move the item
tree->vector[free_spot] = tree->vector[i];
tree->refs[free_spot] = tree->refs[i];
tree->vector[i] = 0;
tree->refs[i] = -1;
// and move all references
for (int x = 0; x < tree->size; x++) {
if (tree->refs[x] == old_ref) {
tree->refs[x] = free_spot;
}
}
// mark the free spot as used
free_spot = -1;
}
}
return 0;
}
int ug_tree_resize(UgTree *tree, unsigned int newsize)
{
if (tree == NULL) {
return -1;
}
// return error when shrinking with too many elements
if ((int)newsize < tree->elements) {
return -1;
}
// pack the vector when shrinking to avoid data loss
if ((int)newsize < tree->size) {
// if (ug_tree_pack(tree) < 0) {
// return -1;
// }
// TODO: allow shrinking, since packing destroys all references
return -1;
}
UgId *newvec = realloc(tree->vector, newsize * sizeof(UgId));
if (newvec == NULL) {
return -1;
}
int *newrefs = realloc(tree->refs, newsize * sizeof(int));
if (newrefs == NULL) {
return -1;
}
int *neworrefs = realloc(tree->ordered_refs, (newsize + 1) * sizeof(int));
if (neworrefs == NULL) {
return -1;
}
tree->vector = newvec;
tree->refs = newrefs;
tree->ordered_refs = neworrefs;
if ((int)newsize > tree->size) {
for (int i = tree->size; i < (int)newsize; i++) {
tree->vector[i] = 0;
tree->refs[i] = -1;
}
}
tree->size = newsize;
return 0;
}
// add an element to the tree, return it's ref
int ug_tree_add(UgTree *tree, UgId elem, int parent)
{
if (tree == NULL) {
return -1;
}
// invalid parent
if (!IS_VALID_REF(tree, parent)) {
return -1;
}
// no space left
if (tree->elements >= tree->size) {
return -1;
}
// check if the parent exists
// if there are no elements in the tree the first add will set the root
if (!REF_IS_PRESENT(tree, parent) && tree->elements != 0) {
return -1;
}
// get the first free spot
int free_spot = -1;
for (int i = 0; i < tree->size; i++) {
if (tree->refs[i] == -1) {
free_spot = i;
break;
}
}
if (free_spot < 0) {
return -1;
}
// finally add the element
tree->vector[free_spot] = elem;
tree->refs[free_spot] = parent;
tree->elements++;
return free_spot;
}
// prune the tree starting from the ref
// returns the number of pruned elements
int ug_tree_prune(UgTree *tree, int ref)
{
if (tree == NULL) {
return -1;
}
if (!IS_VALID_REF(tree, ref)) {
return -1;
}
if (!REF_IS_PRESENT(tree, ref)) {
return 0;
}
tree->vector[ref] = 0;
tree->refs[ref] = -1;
tree->elements--;
int count = 1;
for (int i = 0; tree->elements > 0 && i < tree->size; i++) {
if (tree->refs[i] == ref) {
count += ug_tree_prune(tree, i);
}
}
return count;
}
// find the size of the subtree starting from ref
int ug_tree_subtree_size(UgTree *tree, int ref)
{
if (tree == NULL) {
return -1;
}
if (!IS_VALID_REF(tree, ref)) {
return -1;
}
if (!REF_IS_PRESENT(tree, ref)) {
return 0;
}
int count = 1;
for (int i = 0; i < tree->size; i++) {
// only root has the reference to itself
if (tree->refs[i] == ref && ref != i) {
count += ug_tree_subtree_size(tree, i);
}
}
return count;
}
// iterate through the first level children, use a cursor like strtok_r
int ug_tree_children_it(UgTree *tree, int parent, int *cursor)
{
if (tree == NULL || cursor == NULL) {
return -1;
}
// if the cursor is out of bounds then we are done for sure
if (!IS_VALID_REF(tree, *cursor)) {
return -1;
}
// same for the parent, if it's invalid it can't have children
if (!IS_VALID_REF(tree, parent) || !REF_IS_PRESENT(tree, parent)) {
return -1;
}
// find the first child, update the cursor and return the ref
for (int i = *cursor; i < tree->size; i++) {
if (tree->refs[i] == parent) {
*cursor = i + 1;
return i;
}
}
// if no children are found return -1
*cursor = -1;
return -1;
}
/* iterates trough every leaf of the subtree in the following manner
* node [x], x: visit order
* [0]
* / | \
* / [2] [3]
* [1] |
* / \ [6]
* [4] [5]
*/
int ug_tree_level_order_it(UgTree *tree, int ref, int *cursor)
{
if (tree == NULL || cursor == NULL) {
return -1;
}
int *queue = tree->ordered_refs;
// TODO: this could also be done when adding or removing elements
// first call, create a ref array ordered like we desire
if (*cursor == -1) {
*cursor = 0;
for (int i = 0; i < tree->size; i++) {
queue[i] = -1;
}
// iterate through the queue appending found children
int pos = 0, off = 0;
do {
// printf ("ref=%d\n", ref);
for (int i = 0; i < tree->size; i++) {
if (tree->refs[i] == ref) {
queue[pos++] = i;
}
}
for (; ref == queue[off] && off < tree->size; off++)
;
ref = queue[off];
} while (IS_VALID_REF(tree, ref));
// This line is why tree->ordered_refs has to be size+1
queue[off + 1] = -1;
}
// PRINT_ARR(queue, tree->size);
// return -1;
// on successive calls just iterate through the queue until we find an
// invalid ref, if the user set the cursor to -1 it means it has found what
// he needed, so free
if (*cursor < 0) {
return -1;
} else if (IS_VALID_REF(tree, *cursor)) {
return queue[(*cursor)++];
}
return -1;
}
int ug_tree_parentof(UgTree *tree, int node)
{
if (tree == NULL || !IS_VALID_REF(tree, node) ||
!REF_IS_PRESENT(tree, node)) {
return -1;
}
return tree->refs[node];
}
UgId ug_tree_get(UgTree *tree, int node)
{
if (tree == NULL || !IS_VALID_REF(tree, node)) {
return 0;
}
return tree->vector[node];
}