Compare commits
104 Commits
font_atlas
...
c3
Author | SHA1 | Date | |
---|---|---|---|
e8bb35811a | |||
278e4988e9 | |||
78fc1c1e87 | |||
8d79f13fd6 | |||
7713cd7da9 | |||
00aa01109e | |||
5e68671828 | |||
8367f6b617 | |||
dd073385c8 | |||
80d17d7b33 | |||
b48c413d2d | |||
c1c1247af4 | |||
5ae9b05223 | |||
9afb0d2acd | |||
c1bf8e891b | |||
777e974841 | |||
9fc1d90455 | |||
a390a8908c | |||
c49a689304 | |||
849b267f91 | |||
1de4fd78b8 | |||
586e81935c | |||
972c9b581d | |||
6d2594db2d | |||
9827a899f1 | |||
88d9b65028 | |||
c98c00bfb9 | |||
fc3fa32ddd | |||
cd83c528ee | |||
5b0169cd94 | |||
b411718c94 | |||
05232c1d24 | |||
0223536ac8 | |||
f30db0dd47 | |||
865c7dabaa | |||
0ef2bceeec | |||
cda2f27a49 | |||
10ee643a0c | |||
00f5e71666 | |||
458c45d2b9 | |||
c4a3dd3a26 | |||
2014c67bfd | |||
177e52b0d0 | |||
3a0904023a | |||
c4c9716d61 | |||
6208711292 | |||
39bd7fb8bc | |||
47eb3ff907 | |||
21aa70d340 | |||
bd8c73ecd5 | |||
6e65700f38 | |||
e3d87525d4 | |||
3002123ef7 | |||
ac3fcae649 | |||
c9b74aebc7 | |||
f344c989db | |||
6c5acd6f23 | |||
24bc2c67bc | |||
712ce50631 | |||
2380c7693c | |||
79a2d66880 | |||
34e75f8c06 | |||
7c6f7d31d2 | |||
52f3929a42 | |||
e09107af98 | |||
588a417413 | |||
14359a9b7e | |||
196a2474fd | |||
c53b9eed5e | |||
92614e4d8b | |||
d94e430807 | |||
de64746fdf | |||
0531f58a56 | |||
f516a68cee | |||
07857fcd44 | |||
fbe631b4b4 | |||
b317951c32 | |||
9aa0d58d68 | |||
78e2c64da6 | |||
16adfd7cc5 | |||
1746f7d940 | |||
169b5e1dfd | |||
87de68028a | |||
04843fe714 | |||
a0c6a3b2cb | |||
ca691d1294 | |||
f7985f8c7f | |||
4bd827ce5c | |||
d31b4eab53 | |||
1088083e1e | |||
601a396aa8 | |||
a481269022 | |||
499f6dc79b | |||
740ea0c6be | |||
8d4b353e88 | |||
c0e9565bf6 | |||
c1a7b4fcdb | |||
7d9a8a1363 | |||
2e0c6333d3 | |||
3a7655a3f0 | |||
7b7aac8df4 | |||
2e60e4c5b8 | |||
bca29c537c | |||
6d8300f9d9 |
43
.ecode/project_build.json
Normal file
43
.ecode/project_build.json
Normal 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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -2,3 +2,7 @@
|
||||
*.a
|
||||
build/*
|
||||
**/.ccls-cache
|
||||
perf.data*
|
||||
*.rdc
|
||||
test_renderer
|
||||
resources/shaders/compiled/**
|
||||
|
19
.gitmodules
vendored
19
.gitmodules
vendored
@ -1,3 +1,16 @@
|
||||
[submodule "lib/raylib.c3l"]
|
||||
path = lib/raylib.c3l
|
||||
url = https://github.com/NexushasTaken/raylib.c3l
|
||||
[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
|
||||
|
3
Makefile
Normal file
3
Makefile
Normal file
@ -0,0 +1,3 @@
|
||||
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
|
125
TODO
125
TODO
@ -1,32 +1,127 @@
|
||||
# TODOs, semi-random sorting
|
||||
|
||||
[x] Implement glyph draw command
|
||||
[x] Implement div.view and scrollbars
|
||||
[ ] Port font system from C to C3 (rewrite1)
|
||||
[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)
|
||||
[ ] Better handling of the active and focused widgets, try
|
||||
to maintain focus until mouse release (fix scroll bars)
|
||||
[ ] 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
|
||||
|
||||
## Atlases
|
||||
[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
|
||||
[ ] Fix the missing alpha channel
|
||||
|
||||
[x] Fix the missing alpha channel
|
||||
[x] Fix the alignment
|
||||
|
||||
## Raylib
|
||||
[ ] Implement type (Rect, Color, Point) conversion functions between rl:: and ugui::
|
||||
[x] Implement pixel radius rounding for border radius
|
||||
|
||||
## Widgets
|
||||
[ ] Dynamic text box to implement an fps counter
|
||||
[ ] Button with label
|
||||
|
||||
[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
|
@ -1,3 +1,4 @@
|
||||
all:
|
||||
make -C thirdparty/libgrapheme
|
||||
mkdir -p linux-x64
|
||||
cp thirdparty/libgrapheme/libgrapheme.a linux-x64/libgrapheme.a
|
14
lib/grapheme.c3l/project.json
Normal file
14
lib/grapheme.c3l/project.json
Normal 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"
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
{
|
||||
// Language version of C3.
|
||||
"langrev": "1",
|
||||
// Warnings used for all targets.
|
||||
"warnings": ["no-unused"],
|
||||
// Directories where C3 library files may be found.
|
||||
"dependency-search-paths": [".."],
|
||||
// Libraries to use for all targets.
|
||||
"dependencies": ["grapheme"],
|
||||
// Authors, optionally with email.
|
||||
"authors": ["Alessandro Mauri <alemauri001@gmail.com"],
|
||||
// Version using semantic versioning.
|
||||
"version": "0.1.0",
|
||||
// Sources compiled for all targets.
|
||||
"sources": [],
|
||||
// C sources if the project also compiles C sources
|
||||
// relative to the project file.
|
||||
// "c-sources": [ "csource/**" ],
|
||||
// Output location, relative to project file.
|
||||
"output": "build",
|
||||
// Architecture and OS target.
|
||||
// You can use 'c3c --list-targets' to list all valid targets.
|
||||
// "target": "windows-x64",
|
||||
"features": [
|
||||
// See rcore.c3
|
||||
//"SUPPORT_INTERNAL_MEMORY_MANAGEMENT",
|
||||
//"SUPPORT_STANDARD_FILEIO",
|
||||
//"SUPPORT_FILE_SYSTEM_FUNCTIONS",
|
||||
//"SUPPORT_DATA_ENCODER",
|
||||
// See text.c3
|
||||
//"SUPPORT_TEXT_CODEPOINTS_MANAGEMENT",
|
||||
//"SUPPORT_TEXT_C_STRING_MANAGEMENT",
|
||||
//"SUPPORT_RANDOM_GENERATION",
|
||||
//"SUPPORT_RAYGUI",
|
||||
//"RAYGUI_NO_ICONS",
|
||||
//"RAYGUI_CUSTOM_ICONS",
|
||||
],
|
||||
// Global settings.
|
||||
// CPU name, used for optimizations in the LLVM backend.
|
||||
"cpu": "generic",
|
||||
// Optimization: "O0", "O1", "O2", "O3", "O4", "O5", "Os", "Oz".
|
||||
"opt": "O0"
|
||||
// See resources/examples/project_all_settings.json and 'c3c --list-project-properties' to see more properties.
|
||||
}
|
Binary file not shown.
@ -1,45 +0,0 @@
|
||||
{
|
||||
// Language version of C3.
|
||||
"langrev": "1",
|
||||
// Warnings used for all targets.
|
||||
"warnings": [ "no-unused" ],
|
||||
// Directories where C3 library files may be found.
|
||||
"dependency-search-paths": [ ".." ],
|
||||
// Libraries to use for all targets.
|
||||
"dependencies": [ "schrift" ],
|
||||
// Authors, optionally with email.
|
||||
"authors": [ "Alessandro Mauri <alemauri001@gmail.com" ],
|
||||
// Version using semantic versioning.
|
||||
"version": "0.1.0",
|
||||
// Sources compiled for all targets.
|
||||
"sources": [ ],
|
||||
// C sources if the project also compiles C sources
|
||||
// relative to the project file.
|
||||
// "c-sources": [ "csource/**" ],
|
||||
// Output location, relative to project file.
|
||||
"output": "build",
|
||||
// Architecture and OS target.
|
||||
// You can use 'c3c --list-targets' to list all valid targets.
|
||||
// "target": "windows-x64",
|
||||
"features": [
|
||||
// See rcore.c3
|
||||
//"SUPPORT_INTERNAL_MEMORY_MANAGEMENT",
|
||||
//"SUPPORT_STANDARD_FILEIO",
|
||||
//"SUPPORT_FILE_SYSTEM_FUNCTIONS",
|
||||
//"SUPPORT_DATA_ENCODER",
|
||||
// See text.c3
|
||||
//"SUPPORT_TEXT_CODEPOINTS_MANAGEMENT",
|
||||
//"SUPPORT_TEXT_C_STRING_MANAGEMENT",
|
||||
//"SUPPORT_RANDOM_GENERATION",
|
||||
|
||||
//"SUPPORT_RAYGUI",
|
||||
//"RAYGUI_NO_ICONS",
|
||||
//"RAYGUI_CUSTOM_ICONS",
|
||||
],
|
||||
// Global settings.
|
||||
// CPU name, used for optimizations in the LLVM backend.
|
||||
"cpu": "generic",
|
||||
// Optimization: "O0", "O1", "O2", "O3", "O4", "O5", "Os", "Oz".
|
||||
"opt": "O0",
|
||||
// See resources/examples/project_all_settings.json and 'c3c --list-project-properties' to see more properties.
|
||||
}
|
@ -1 +0,0 @@
|
||||
Subproject commit c7ebe054ce16136c1128fab54fcce4921044293e
|
@ -1,3 +1,4 @@
|
||||
all:
|
||||
make -C thirdparty/libschrift
|
||||
mkdir -p linux-x64
|
||||
cp thirdparty/libschrift/libschrift.a linux-x64/libschrift.a
|
@ -1,8 +1,8 @@
|
||||
module schrift;
|
||||
|
||||
def SftFont = void*;
|
||||
def SftUChar = uint;
|
||||
def SftGlyph = uint;
|
||||
alias SftFont = void*;
|
||||
alias SftUChar = uint;
|
||||
alias SftGlyph = uint;
|
||||
|
||||
const int SFT_DOWNWARD_Y = 0x01;
|
||||
|
||||
@ -45,10 +45,10 @@ struct SftImage
|
||||
int height;
|
||||
}
|
||||
|
||||
extern fn char* sft_version() @extern("sft_version");
|
||||
extern fn ZString sft_version() @extern("sft_version");
|
||||
|
||||
extern fn SftFont loadmem(void* mem, usz size) @extern("sft_loadmem");
|
||||
extern fn SftFont loadfile(char* filename) @extern("sft_loadfile");
|
||||
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");
|
14
lib/schrift.c3l/project.json
Normal file
14
lib/schrift.c3l/project.json
Normal 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",
|
||||
}
|
1
lib/sdl3.c3l
Submodule
1
lib/sdl3.c3l
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 076355e2d126e7546e53663b97e8dec22667d34d
|
0
lib/ugui.c3l/LICENSE
Normal file
0
lib/ugui.c3l/LICENSE
Normal file
1
lib/ugui.c3l/README.md
Normal file
1
lib/ugui.c3l/README.md
Normal file
@ -0,0 +1 @@
|
||||
Welcome to the ugui library.
|
0
lib/ugui.c3l/linux-x64/.gitkeep
Normal file
0
lib/ugui.c3l/linux-x64/.gitkeep
Normal file
11
lib/ugui.c3l/manifest.json
Normal file
11
lib/ugui.c3l/manifest.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"provides" : "ugui",
|
||||
"sources" : [ "src/**" ],
|
||||
"targets" : {
|
||||
"linux-x64" : {
|
||||
"link-args" : [],
|
||||
"dependencies" : ["schrift", "grapheme"],
|
||||
"linked-libraries" : []
|
||||
}
|
||||
}
|
||||
}
|
0
lib/ugui.c3l/scripts/.gitkeep
Normal file
0
lib/ugui.c3l/scripts/.gitkeep
Normal file
@ -1,4 +1,4 @@
|
||||
module cache(<Key, Value, SIZE>);
|
||||
module cache{Key, Value, SIZE};
|
||||
|
||||
/* LRU Cache
|
||||
* The cache uses a pool (array) to store all the elements, each element has
|
||||
@ -14,12 +14,13 @@ module cache(<Key, Value, SIZE>);
|
||||
// happens at the same time
|
||||
|
||||
import std::core::mem;
|
||||
import std::core::mem::allocator;
|
||||
import std::collections::bitset;
|
||||
import std::collections::map;
|
||||
|
||||
def BitArr = bitset::BitSet(<SIZE>) @private;
|
||||
def IdTable = map::HashMap(<Key, usz>) @private;
|
||||
def IdTableEntry = map::Entry(<Key, usz>) @private;
|
||||
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);
|
||||
|
||||
@ -42,9 +43,9 @@ macro Cache.cycle(&cache) @private {
|
||||
}
|
||||
}
|
||||
|
||||
fn void! Cache.init(&cache)
|
||||
fn void? Cache.init(&cache)
|
||||
{
|
||||
cache.table.new_init(capacity: SIZE);
|
||||
cache.table.init(allocator::heap(), capacity: SIZE);
|
||||
// FIXME: this shit is SLOW
|
||||
foreach (idx, bit : cache.used) { cache.used[idx] = false; }
|
||||
foreach (idx, bit : cache.present) { cache.present[idx] = false; }
|
||||
@ -57,21 +58,22 @@ fn void Cache.free(&cache)
|
||||
(void)mem::free(cache.pool);
|
||||
}
|
||||
|
||||
fn Value*! Cache.search(&cache, Key id)
|
||||
fn Value*? Cache.search(&cache, Key id)
|
||||
{
|
||||
// get_entry() faults on miss
|
||||
IdTableEntry* entry = cache.table.get_entry(id)!;
|
||||
|
||||
/* MISS */
|
||||
/* MISS, wrong key */
|
||||
if (entry.key != id) {
|
||||
return SearchResult.MISSING?;
|
||||
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 SearchResult.MISSING?;
|
||||
return NOT_FOUND?;
|
||||
}
|
||||
|
||||
/* HIT, set as recently used */
|
||||
@ -79,6 +81,18 @@ fn Value*! Cache.search(&cache, Key id)
|
||||
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
|
||||
@ -92,7 +106,7 @@ fn usz Cache.get_free_spot(&cache) @private
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn Value*! Cache.insert_at(&cache, Value *g, Key id, usz index) @private
|
||||
fn Value*? Cache.insert_at(&cache, Value *g, Key id, usz index) @private
|
||||
{
|
||||
// TODO: verify index, g and id
|
||||
Value* spot;
|
||||
@ -109,17 +123,17 @@ fn Value*! Cache.insert_at(&cache, Value *g, Key id, usz index) @private
|
||||
}
|
||||
|
||||
// Insert an element in the cache, returns the index
|
||||
fn Value*! Cache.insert_new(&cache, Value* g, Key id)
|
||||
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)
|
||||
fn Value*? Cache.get_or_insert(&cache, Value* g, Key id, bool *is_new = null)
|
||||
{
|
||||
Value*! c = cache.search(id);
|
||||
Value*? c = cache.search(id);
|
||||
if (catch e = c) {
|
||||
if (e != SearchResult.MISSING) {
|
||||
if (e != NOT_FOUND) {
|
||||
return e?;
|
||||
} else {
|
||||
// if the element is new (inserted) set the is_new flag
|
91
lib/ugui.c3l/src/fifo.c3
Normal file
91
lib/ugui.c3l/src/fifo.c3
Normal 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)!;
|
||||
}
|
||||
}
|
131
lib/ugui.c3l/src/ugui_atlas.c3
Normal file
131
lib/ugui.c3l/src/ugui_atlas.c3
Normal 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;
|
||||
}
|
153
lib/ugui.c3l/src/ugui_button.c3
Normal file
153
lib/ugui.c3l/src/ugui_button.c3
Normal 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)!;
|
||||
}
|
181
lib/ugui.c3l/src/ugui_cmd.c3
Normal file
181
lib/ugui.c3l/src/ugui_cmd.c3
Normal 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)!;
|
||||
}
|
||||
|
333
lib/ugui.c3l/src/ugui_core.c3
Normal file
333
lib/ugui.c3l/src/ugui_core.c3
Normal 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;
|
||||
}
|
155
lib/ugui.c3l/src/ugui_div.c3
Normal file
155
lib/ugui.c3l/src/ugui_div.c3
Normal 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;
|
||||
}
|
314
lib/ugui.c3l/src/ugui_font.c3
Normal file
314
lib/ugui.c3l/src/ugui_font.c3
Normal 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);
|
204
lib/ugui.c3l/src/ugui_input.c3
Normal file
204
lib/ugui.c3l/src/ugui_input.c3
Normal 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;
|
||||
}
|
157
lib/ugui.c3l/src/ugui_layout.c3
Normal file
157
lib/ugui.c3l/src/ugui_layout.c3
Normal 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 {};
|
||||
}
|
||||
}
|
259
lib/ugui.c3l/src/ugui_shapes.c3
Normal file
259
lib/ugui.c3l/src/ugui_shapes.c3
Normal 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;
|
||||
}
|
136
lib/ugui.c3l/src/ugui_slider.c3
Normal file
136
lib/ugui.c3l/src/ugui_slider.c3
Normal 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);
|
153
lib/ugui.c3l/src/ugui_sprite.c3
Normal file
153
lib/ugui.c3l/src/ugui_sprite.c3
Normal 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)!;
|
||||
}
|
436
lib/ugui.c3l/src/ugui_style.c3
Normal file
436
lib/ugui.c3l/src/ugui_style.c3
Normal 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;
|
||||
}
|
79
lib/ugui.c3l/src/ugui_text.c3
Normal file
79
lib/ugui.c3l/src/ugui_text.c3
Normal 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;
|
||||
}
|
@ -1,4 +1,7 @@
|
||||
module vtree(<ElemType>);
|
||||
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;
|
||||
@ -9,13 +12,6 @@ struct VTree {
|
||||
isz[] refs, ordered_refs;
|
||||
}
|
||||
|
||||
fault VTreeError {
|
||||
CANNOT_SHRINK,
|
||||
INVALID_REFERENCE,
|
||||
TREE_FULL,
|
||||
REFERENCE_NOT_PRESENT,
|
||||
INVALID_ARGUMENT,
|
||||
}
|
||||
|
||||
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; }
|
||||
@ -27,11 +23,11 @@ macro @zero()
|
||||
$if $assignable(0, ElemType):
|
||||
return 0;
|
||||
$else
|
||||
return ElemType{0};
|
||||
return {};
|
||||
$endif
|
||||
}
|
||||
|
||||
fn void! VTree.init(&tree, usz size)
|
||||
fn void? VTree.init(&tree, usz size)
|
||||
{
|
||||
tree.vector = mem::new_array(ElemType, size);
|
||||
defer catch { (void)mem::free(tree.vector); }
|
||||
@ -74,7 +70,7 @@ fn void VTree.pack(&tree)
|
||||
tree.vector[free_spot] = tree.vector[i];
|
||||
tree.refs[free_spot] = tree.refs[i];
|
||||
|
||||
tree.vector[i] = @zero();
|
||||
tree.vector[i] = {};
|
||||
tree.refs[i] = -1;
|
||||
|
||||
// and move all references
|
||||
@ -90,18 +86,18 @@ fn void VTree.pack(&tree)
|
||||
}
|
||||
}
|
||||
|
||||
fn void! VTree.resize(&tree, usz newsize)
|
||||
fn void? VTree.resize(&tree, usz newsize)
|
||||
{
|
||||
// return error when shrinking with too many elements
|
||||
if (newsize < tree.elements) {
|
||||
return VTreeError.CANNOT_SHRINK?;
|
||||
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 VTreeError.CANNOT_SHRINK?;
|
||||
return vtree::faults::CANNOT_SHRINK?;
|
||||
}
|
||||
|
||||
usz old_size = tree.size();
|
||||
@ -122,22 +118,22 @@ fn void! VTree.resize(&tree, usz newsize)
|
||||
}
|
||||
|
||||
// add an element to the tree, return it's ref
|
||||
fn isz! VTree.add(&tree, ElemType elem, isz parent)
|
||||
fn isz? VTree.add(&tree, ElemType elem, isz parent)
|
||||
{
|
||||
// invalid parent
|
||||
if (!tree.ref_is_valid(parent)) {
|
||||
return VTreeError.INVALID_REFERENCE?;
|
||||
return vtree::faults::INVALID_REFERENCE?;
|
||||
}
|
||||
|
||||
// no space left
|
||||
if (tree.elements >= tree.size()) {
|
||||
return VTreeError.TREE_FULL?;
|
||||
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 VTreeError.REFERENCE_NOT_PRESENT?;
|
||||
return vtree::faults::REFERENCE_NOT_PRESENT?;
|
||||
}
|
||||
|
||||
// get the first free spot
|
||||
@ -149,7 +145,7 @@ fn isz! VTree.add(&tree, ElemType elem, isz parent)
|
||||
}
|
||||
}
|
||||
if (free_spot < 0) {
|
||||
return VTreeError.TREE_FULL?;
|
||||
return vtree::faults::TREE_FULL?;
|
||||
}
|
||||
|
||||
// finally add the element
|
||||
@ -162,10 +158,10 @@ fn isz! VTree.add(&tree, ElemType elem, isz parent)
|
||||
|
||||
// prune the tree starting from the ref
|
||||
// returns the number of pruned elements
|
||||
fn usz! VTree.prune(&tree, isz ref)
|
||||
fn usz? VTree.prune(&tree, isz ref)
|
||||
{
|
||||
if (!tree.ref_is_valid(ref)) {
|
||||
return VTreeError.INVALID_REFERENCE?;
|
||||
return vtree::faults::INVALID_REFERENCE?;
|
||||
}
|
||||
|
||||
if (!tree.ref_is_present(ref)) {
|
||||
@ -186,11 +182,20 @@ fn usz! VTree.prune(&tree, isz ref)
|
||||
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)
|
||||
fn usz? VTree.subtree_size(&tree, isz ref)
|
||||
{
|
||||
if (!tree.ref_is_valid(ref)) {
|
||||
return VTreeError.INVALID_REFERENCE?;
|
||||
return vtree::faults::INVALID_REFERENCE?;
|
||||
}
|
||||
|
||||
if (!tree.ref_is_present(ref)) {
|
||||
@ -209,20 +214,20 @@ fn usz! VTree.subtree_size(&tree, isz ref)
|
||||
}
|
||||
|
||||
// iterate through the first level children, use a cursor like strtok_r
|
||||
fn isz! VTree.children_it(&tree, isz parent, isz *cursor)
|
||||
fn isz? VTree.children_it(&tree, isz parent, isz *cursor)
|
||||
{
|
||||
if (cursor == null) {
|
||||
return VTreeError.INVALID_ARGUMENT?;
|
||||
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 VTreeError.INVALID_REFERENCE?;
|
||||
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 VTreeError.INVALID_REFERENCE?;
|
||||
return vtree::faults::INVALID_REFERENCE?;
|
||||
}
|
||||
|
||||
// find the first child, update the cursor and return the ref
|
||||
@ -247,10 +252,10 @@ fn isz! VTree.children_it(&tree, isz parent, isz *cursor)
|
||||
* / \ [6]
|
||||
* [4] [5]
|
||||
*/
|
||||
fn isz! VTree.level_order_it(&tree, isz ref, isz *cursor)
|
||||
fn isz? VTree.level_order_it(&tree, isz ref, isz *cursor)
|
||||
{
|
||||
if (cursor == null) {
|
||||
return VTreeError.INVALID_ARGUMENT?;
|
||||
return vtree::faults::INVALID_ARGUMENT?;
|
||||
}
|
||||
|
||||
isz[] queue = tree.ordered_refs;
|
||||
@ -294,27 +299,27 @@ fn isz! VTree.level_order_it(&tree, isz ref, isz *cursor)
|
||||
return -1;
|
||||
}
|
||||
|
||||
fn isz! VTree.parentof(&tree, isz ref)
|
||||
fn isz? VTree.parentof(&tree, isz ref)
|
||||
{
|
||||
if (!tree.ref_is_valid(ref)) {
|
||||
return VTreeError.INVALID_REFERENCE?;
|
||||
return vtree::faults::INVALID_REFERENCE?;
|
||||
}
|
||||
|
||||
if (!tree.ref_is_present(ref)) {
|
||||
return VTreeError.REFERENCE_NOT_PRESENT?;
|
||||
return vtree::faults::REFERENCE_NOT_PRESENT?;
|
||||
}
|
||||
|
||||
return tree.refs[ref];
|
||||
}
|
||||
|
||||
fn ElemType! VTree.get(&tree, isz ref)
|
||||
fn ElemType? VTree.get(&tree, isz ref)
|
||||
{
|
||||
if (!tree.ref_is_valid(ref)) {
|
||||
return VTreeError.INVALID_REFERENCE?;
|
||||
return vtree::faults::INVALID_REFERENCE?;
|
||||
}
|
||||
|
||||
if (!tree.ref_is_present(ref)) {
|
||||
return VTreeError.REFERENCE_NOT_PRESENT?;
|
||||
return vtree::faults::REFERENCE_NOT_PRESENT?;
|
||||
}
|
||||
|
||||
return tree.vector[ref];
|
1
lib/vendor
Submodule
1
lib/vendor
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit db006221a8af625630fdb8b56707f3d07d4314a2
|
57
project.json
57
project.json
@ -1,55 +1,20 @@
|
||||
{
|
||||
// Language version of C3.
|
||||
"langrev": "1",
|
||||
// Warnings used for all targets.
|
||||
"warnings": [ "no-unused" ],
|
||||
// Directories where C3 library files may be found.
|
||||
"dependency-search-paths": [ "lib" ],
|
||||
// Libraries to use for all targets.
|
||||
"dependencies": [ "raylib", "schrift", "grapheme" ],
|
||||
"features": [
|
||||
// See rcore.c3
|
||||
//"SUPPORT_INTERNAL_MEMORY_MANAGEMENT",
|
||||
//"SUPPORT_STANDARD_FILEIO",
|
||||
//"SUPPORT_FILE_SYSTEM_FUNCTIONS",
|
||||
//"SUPPORT_DATA_ENCODER",
|
||||
// See text.c3
|
||||
//"SUPPORT_TEXT_CODEPOINTS_MANAGEMENT",
|
||||
//"SUPPORT_TEXT_C_STRING_MANAGEMENT",
|
||||
//"SUPPORT_RANDOM_GENERATION",
|
||||
//"SUPPORT_RAYGUI",
|
||||
//"RAYGUI_NO_ICONS",
|
||||
//"RAYGUI_CUSTOM_ICONS",
|
||||
],
|
||||
// Authors, optionally with email.
|
||||
"authors": [ "John Doe <ale@shitposting.expert>" ],
|
||||
// Version using semantic versioning.
|
||||
"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 compiled for all targets.
|
||||
"sources": [ "src/**" ],
|
||||
// C sources if the project also compiles C sources
|
||||
// relative to the project file.
|
||||
// "c-sources": [ "csource/**" ],
|
||||
// Include directories for C sources relative to the project file.
|
||||
// "c-include-dirs": [ "csource/include" ],
|
||||
// Output location, relative to project file.
|
||||
"sources": ["src/**"],
|
||||
"output": "build",
|
||||
// Architecture and OS target.
|
||||
// You can use 'c3c --list-targets' to list all valid targets.
|
||||
// "target": "windows-x64",
|
||||
// Targets.
|
||||
"target": "linux-x64",
|
||||
"targets": {
|
||||
"ugui": {
|
||||
// Executable or library.
|
||||
"type": "executable",
|
||||
// Additional libraries, sources
|
||||
// and overrides of global settings here.
|
||||
"type": "executable"
|
||||
}
|
||||
},
|
||||
},
|
||||
// Global settings.
|
||||
// CPU name, used for optimizations in the LLVM backend.
|
||||
"cpu": "generic",
|
||||
// Optimization: "O0", "O1", "O2", "O3", "O4", "O5", "Os", "Oz".
|
||||
"cpu": "native",
|
||||
"opt": "O0",
|
||||
// See resources/examples/project_all_settings.json and 'c3c --list-project-properties' to see more properties.
|
||||
"debug-info": "full"
|
||||
}
|
||||
|
BIN
resources/hack-nerd.ttf
Normal file
BIN
resources/hack-nerd.ttf
Normal file
Binary file not shown.
33
resources/shaders/Makefile
Normal file
33
resources/shaders/Makefile
Normal 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
|
21
resources/shaders/source/font.frag.glsl
Normal file
21
resources/shaders/source/font.frag.glsl
Normal file
@ -0,0 +1,21 @@
|
||||
#version 450
|
||||
|
||||
layout(set = 3, binding = 0) uniform Viewport {
|
||||
ivec2 view;
|
||||
};
|
||||
layout(set = 2, binding = 0) uniform sampler2D tx;
|
||||
|
||||
layout(location = 0) in vec2 uv;
|
||||
layout(location = 1) in vec4 color;
|
||||
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
|
||||
void main()
|
||||
{
|
||||
ivec2 ts = textureSize(tx, 0);
|
||||
vec2 fts = vec2(ts);
|
||||
vec2 real_uv = uv / fts;
|
||||
|
||||
vec4 opacity = texture(tx, real_uv);
|
||||
fragColor = vec4(color.rgb, color.a*opacity.r);
|
||||
}
|
35
resources/shaders/source/msdf.frag.glsl
Normal file
35
resources/shaders/source/msdf.frag.glsl
Normal file
@ -0,0 +1,35 @@
|
||||
#version 450
|
||||
|
||||
layout(set = 3, binding = 0) uniform Viewport {
|
||||
ivec2 view;
|
||||
};
|
||||
layout(set = 2, binding = 0) uniform sampler2D tx;
|
||||
|
||||
const float PX_RANGE = 4.0f;
|
||||
|
||||
layout(location = 0) in vec2 uv;
|
||||
layout(location = 1) in vec4 color;
|
||||
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
|
||||
float screen_px_range(vec2 uv) {
|
||||
vec2 unit_range = vec2(PX_RANGE)/vec2(textureSize(tx, 0));
|
||||
vec2 texel_size = vec2(1.0)/fwidth(uv);
|
||||
return max(0.5*dot(unit_range, texel_size), 1.0);
|
||||
}
|
||||
|
||||
float median(float r, float g, float b) {
|
||||
return max(min(r, g), min(max(r, g), b));
|
||||
}
|
||||
|
||||
void main() {
|
||||
ivec2 ts = textureSize(tx, 0);
|
||||
vec2 fts = vec2(ts);
|
||||
vec2 real_uv = uv / fts;
|
||||
|
||||
vec3 msd = texture(tx, real_uv).rgb;
|
||||
float sd = median(msd.r, msd.g, msd.b);
|
||||
float distance = screen_px_range(real_uv)*(sd - 0.5);
|
||||
float opacity = clamp(distance + 0.5, 0.0, 1.0);
|
||||
fragColor = color * opacity;
|
||||
}
|
27
resources/shaders/source/rect.frag.glsl
Normal file
27
resources/shaders/source/rect.frag.glsl
Normal 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);
|
||||
}
|
29
resources/shaders/source/rect.vert.glsl
Normal file
29
resources/shaders/source/rect.vert.glsl
Normal 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));
|
||||
}
|
18
resources/shaders/source/sprite.frag.glsl
Normal file
18
resources/shaders/source/sprite.frag.glsl
Normal file
@ -0,0 +1,18 @@
|
||||
#version 450
|
||||
|
||||
layout(set = 3, binding = 0) uniform Viewport {
|
||||
ivec2 view;
|
||||
};
|
||||
|
||||
layout(location = 0) in vec2 uv;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
|
||||
layout(set = 2, binding = 0) uniform sampler2D tx;
|
||||
|
||||
void main()
|
||||
{
|
||||
ivec2 ts = textureSize(tx, 0);
|
||||
vec2 fts = vec2(ts);
|
||||
vec2 real_uv = uv / fts;
|
||||
fragColor = texture(tx, real_uv);
|
||||
}
|
28
resources/shaders/source/sprite.vert.glsl
Normal file
28
resources/shaders/source/sprite.vert.glsl
Normal file
@ -0,0 +1,28 @@
|
||||
#version 450
|
||||
|
||||
layout(set = 1, binding = 0) uniform Viewport {
|
||||
ivec2 view;
|
||||
};
|
||||
|
||||
layout(location = 0) in ivec2 position;
|
||||
layout(location = 1) in ivec4 attr; // quad x,y,w,h
|
||||
layout(location = 2) in ivec2 in_uv;
|
||||
layout(location = 3) in uvec4 color;
|
||||
|
||||
layout(location = 0) out vec2 out_uv;
|
||||
layout(location = 1) out vec4 out_color;
|
||||
|
||||
void main()
|
||||
{
|
||||
// vertex position
|
||||
ivec2 px_pos = attr.xy + position.xy * attr.zw;
|
||||
vec2 clip_pos;
|
||||
clip_pos.x = float(px_pos.x)*2.0 / view.x - 1.0;
|
||||
clip_pos.y = -(float(px_pos.y)*2.0 / view.y - 1.0);
|
||||
|
||||
gl_Position = vec4(clip_pos, 0.0, 1.0);
|
||||
|
||||
vec2 px_uv = in_uv.xy + position.xy * attr.zw;
|
||||
out_uv = vec2(px_uv);
|
||||
out_color = vec4(color) / 255.0;
|
||||
}
|
61
resources/style.css
Normal file
61
resources/style.css
Normal 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
BIN
resources/tick_sdf.qoi
Normal file
Binary file not shown.
BIN
resources/tux.qoi
Normal file
BIN
resources/tux.qoi
Normal file
Binary file not shown.
BIN
resources/tux_inv.qoi
Normal file
BIN
resources/tux_inv.qoi
Normal file
Binary file not shown.
49
src/fifo.c3
49
src/fifo.c3
@ -1,49 +0,0 @@
|
||||
module fifo(<Type>);
|
||||
|
||||
import std::core::mem;
|
||||
|
||||
fault FifoErr {
|
||||
FULL,
|
||||
EMPTY,
|
||||
}
|
||||
|
||||
// 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 FifoErr.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 FifoErr.EMPTY?;
|
||||
}
|
||||
Type *ret = &fifo.arr[fifo.out];
|
||||
fifo.count--;
|
||||
fifo.out = (fifo.out + 1) % fifo.arr.len;
|
||||
return ret;
|
||||
}
|
457
src/main.c3
457
src/main.c3
@ -2,11 +2,14 @@ import std::io;
|
||||
import vtree;
|
||||
import cache;
|
||||
import ugui;
|
||||
import rl;
|
||||
import std::time;
|
||||
import std::collections::ringbuffer;
|
||||
import std::core::string;
|
||||
import std::ascii;
|
||||
import sdlrenderer::ren;
|
||||
import sdl3::sdl;
|
||||
|
||||
def Times = ringbuffer::RingBuffer(<time::NanoDuration, 128>);
|
||||
alias Times = ringbuffer::RingBuffer{time::NanoDuration[128]};
|
||||
|
||||
fn void Times.print_stats(×)
|
||||
{
|
||||
@ -23,216 +26,334 @@ fn void Times.print_stats(×)
|
||||
io::printfn("min=%s, max=%s, avg=%s", min, max, avg);
|
||||
}
|
||||
|
||||
struct TimeStats {
|
||||
time::NanoDuration min, max, avg;
|
||||
}
|
||||
|
||||
fn TimeStats Times.get_stats(×)
|
||||
{
|
||||
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()!!;
|
||||
ui.font.load("/usr/share/fonts/TTF/HackNerdFontMono-Regular.ttf", 16)!!;
|
||||
defer ui.free();
|
||||
|
||||
short width = 800;
|
||||
short height = 450;
|
||||
rl::set_config_flags(rl::FLAG_WINDOW_RESIZABLE);
|
||||
rl::init_window(width, height, "Ugui Test");
|
||||
ui.input_window_size(width, height)!!;
|
||||
rl::set_target_fps(60);
|
||||
rl::enable_event_waiting();
|
||||
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;
|
||||
bool toggle = true;
|
||||
double fps;
|
||||
time::Clock clock;
|
||||
time::Clock fps_clock;
|
||||
time::Clock sleep_clock;
|
||||
Times ui_times;
|
||||
Times draw_times;
|
||||
|
||||
// Main loop
|
||||
while (!rl::window_should_close()) {
|
||||
clock.mark();
|
||||
|
||||
/* Start Input Handling */
|
||||
if (rl::is_window_resized()) {
|
||||
width = (short)rl::get_screen_width();
|
||||
height = (short)rl::get_screen_height();
|
||||
ui.input_window_size(width, height)!!;
|
||||
//
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
ui.input_changefocus(rl::is_window_focused());
|
||||
if (e.type == EVENT_KEY_DOWN && e.key.key == K_RETURN) ui.input_char('\n');
|
||||
|
||||
rl::Vector2 mpos = rl::get_mouse_position();
|
||||
ui.input_mouse_abs((short)mpos.x, (short)mpos.y);
|
||||
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);
|
||||
|
||||
ugui::MouseButtons buttons;
|
||||
buttons.btn_left = rl::is_mouse_button_down(rl::MOUSE_BUTTON_LEFT);
|
||||
buttons.btn_right = rl::is_mouse_button_down(rl::MOUSE_BUTTON_RIGHT);
|
||||
buttons.btn_middle = rl::is_mouse_button_down(rl::MOUSE_BUTTON_MIDDLE);
|
||||
ui.input_mouse_button(buttons);
|
||||
/* End Input Handling */
|
||||
|
||||
/* Start UI Handling */
|
||||
ui.frame_begin()!!;
|
||||
|
||||
// main div, fill the whole window
|
||||
ui.div_begin("main", ugui::Rect{.w=ui.width/2})!!;
|
||||
{|
|
||||
if (ui.check_key_combo(ugui::KMOD_CTRL, "q")) quit = true;
|
||||
|
||||
ui.layout_set_row()!!;
|
||||
if (ui.button("button0", ugui::Rect{0,0,30,30})!!.mouse_press) {
|
||||
io::printn("press button0");
|
||||
toggle = !toggle;
|
||||
ui.force_update()!!;
|
||||
}
|
||||
//ui.layout_next_column()!!;
|
||||
if (ui.button("button1", ugui::Rect{0,0,30,30})!!.mouse_press) {
|
||||
io::printn("press button1");
|
||||
}
|
||||
//ui.layout_next_column()!!;
|
||||
if (toggle) {
|
||||
if (ui.button("button2", ugui::Rect{0,0,30,30})!!.mouse_release) {
|
||||
io::printn("release button2");
|
||||
}
|
||||
}
|
||||
if (ui.slider_ver("slider", ugui::Rect{0,0,30,100})!!.update) {
|
||||
ugui::Elem* e = ui.get_elem_by_label("slider")!!;
|
||||
io::printfn("slider: %f", e.slider.value);
|
||||
}
|
||||
const String APPLICATION = "calculator";
|
||||
$if APPLICATION == "debug":
|
||||
debug_app(&ui);
|
||||
$endif
|
||||
$if APPLICATION == "calculator":
|
||||
calculator(&ui);
|
||||
$endif
|
||||
|
||||
ui.text_unbounded("text1", "Ciao Mamma\nAbilità ⚡")!!;
|
||||
|};
|
||||
ui.div_end()!!;
|
||||
// Timings counter
|
||||
TimeStats dts = draw_times.get_stats();
|
||||
TimeStats uts = ui_times.get_stats();
|
||||
|
||||
ui.div_begin("second", ugui::DIV_FILL)!!;
|
||||
ugui::Elem* de = ui.get_elem_by_label("second")!!;
|
||||
de.div.scroll.can_y = true;
|
||||
{|
|
||||
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()!!;
|
||||
if (ui.slider_ver("slider_other", ugui::Rect{0,0,30,100})!!.update) {
|
||||
ugui::Elem* e = ui.get_elem_by_label("slider_other")!!;
|
||||
io::printfn("other slider: %f", e.slider.value);
|
||||
}
|
||||
ui.button("button10", ugui::Rect{0,0,50,50})!!;
|
||||
ui.button("button11", ugui::Rect{0,0,50,50})!!;
|
||||
ui.button("button12", ugui::Rect{0,0,50,50})!!;
|
||||
ui.button("button13", ugui::Rect{0,0,50,50})!!;
|
||||
ui.button("button14", ugui::Rect{0,0,50,50})!!;
|
||||
ui.button("button15", ugui::Rect{0,0,50,50})!!;
|
||||
ui.button("button16", ugui::Rect{0,0,50,50})!!;
|
||||
ui.button("button17", ugui::Rect{0,0,50,50})!!;
|
||||
|};
|
||||
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();
|
||||
//ui_times.print_stats();
|
||||
|
||||
|
||||
/* Start UI Drawing */
|
||||
rl::begin_drawing();
|
||||
// ClearBackground(BLACK);
|
||||
ren.begin_render(true);
|
||||
|
||||
rl::Color c;
|
||||
rl::Rectangle r;
|
||||
static rl::Image font_atlas;
|
||||
static rl::Texture2D font_texture;
|
||||
for (Cmd* cmd; (cmd = ui.cmd_queue.dequeue() ?? null) != null;) {
|
||||
switch (cmd.type) {
|
||||
case ugui::CmdType.CMD_RECT:
|
||||
c = rl::Color{
|
||||
.r = cmd.rect.color.r,
|
||||
.g = cmd.rect.color.g,
|
||||
.b = cmd.rect.color.b,
|
||||
.a = cmd.rect.color.a,
|
||||
};
|
||||
r = rl::Rectangle{
|
||||
.x = cmd.rect.rect.x,
|
||||
.y = cmd.rect.rect.y,
|
||||
.height = cmd.rect.rect.h,
|
||||
.width = cmd.rect.rect.w,
|
||||
};
|
||||
float rad = cmd.rect.radius;
|
||||
// for some weird-ass reason the straight forward inverse formula does not work
|
||||
float roundness = r.width > r.height ? (2.1*rad)/r.height : (2.1*rad)/r.width;
|
||||
rl::draw_rectangle_rounded(r, roundness, 0, c);
|
||||
case ugui::CmdType.CMD_UPDATE_ATLAS:
|
||||
rl::unload_image(font_atlas);
|
||||
font_atlas.data = cmd.update_atlas.raw_buffer;
|
||||
font_atlas.width = cmd.update_atlas.width;
|
||||
font_atlas.height = cmd.update_atlas.height;
|
||||
font_atlas.mipmaps = 1;
|
||||
//font_atlas.format = rl::PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE;
|
||||
font_atlas.format = 1;
|
||||
if (rl::is_texture_ready(font_texture)) {
|
||||
rl::unload_texture(font_texture);
|
||||
}
|
||||
font_texture = rl::load_texture_from_image(font_atlas);
|
||||
//rl::draw_texture(font_texture, 0, 0, rl::WHITE);
|
||||
case ugui::CmdType.CMD_SPRITE:
|
||||
rl::Rectangle source = {
|
||||
.x = cmd.sprite.texture_rect.x,
|
||||
.y = cmd.sprite.texture_rect.y,
|
||||
.width = cmd.sprite.texture_rect.w,
|
||||
.height = cmd.sprite.texture_rect.h,
|
||||
};
|
||||
rl::Vector2 position = {
|
||||
.x = cmd.sprite.rect.x,
|
||||
.y = cmd.sprite.rect.y,
|
||||
};
|
||||
ren.render_ugui(&ui.cmd_queue);
|
||||
|
||||
ren.end_render();
|
||||
|
||||
rl::draw_texture_rec(font_texture, source, position, rl::WHITE);
|
||||
//rl::draw_rectangle(cmd.sprite.rect.x,
|
||||
// cmd.sprite.rect.y,
|
||||
// cmd.sprite.rect.w,
|
||||
// cmd.sprite.rect.h,
|
||||
// rl::WHITE
|
||||
//);
|
||||
default:
|
||||
io::printfn("Unknown cmd type: %s", cmd.type);
|
||||
}
|
||||
}
|
||||
draw_times.push(clock.mark());
|
||||
draw_times.print_stats();
|
||||
rl::end_drawing();
|
||||
//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++;
|
||||
}
|
||||
|
||||
rl::close_window();
|
||||
|
||||
ui.font.free();
|
||||
ui.free();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
fn void! test_vtree() @test
|
||||
fn void debug_app(ugui::Ctx* ui)
|
||||
{
|
||||
vtree::VTree(<String>) vt;
|
||||
vt.init(10)!!;
|
||||
defer vt.free();
|
||||
|
||||
assert(vt.size() == 10, "Size is incorrect");
|
||||
|
||||
isz ref = vt.add("Ciao Mamma", 0)!!;
|
||||
String s = vt.get(ref)!!;
|
||||
assert(s == "Ciao Mamma", "String is incorrect");
|
||||
isz par = vt.parentof(0)!!;
|
||||
assert(ref == par, "Not Root");
|
||||
|
||||
vt.print();
|
||||
}
|
||||
|
||||
def StrCache = cache::Cache(<int, String, 256>);
|
||||
fn void! test_cache() @test
|
||||
{
|
||||
StrCache cc;
|
||||
cc.init()!!;
|
||||
defer cc.free();
|
||||
|
||||
String*! r = cc.search(1);
|
||||
if (catch ex = r) {
|
||||
if (ex != SearchResult.MISSING) {
|
||||
return ex?;
|
||||
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");
|
||||
}
|
||||
|
||||
r = cc.get_or_insert(&&"Ciao Mamma", 1)!;
|
||||
assert(*r!! == "Ciao Mamma", "incorrect string");
|
||||
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
962
src/renderer.c3
Normal 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);
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
module ugui;
|
||||
|
||||
import std::io;
|
||||
|
||||
// draw a button, return the events on that button
|
||||
fn ElemEvents! Ctx.button(&ctx, String label, Rect size)
|
||||
{
|
||||
Id id = label.hash();
|
||||
|
||||
Elem *parent = ctx.get_parent()!;
|
||||
Elem *c_elem = ctx.get_elem(id)!;
|
||||
// add it to the tree
|
||||
ctx.tree.add(id, ctx.active_div)!;
|
||||
|
||||
// 1. Fill the element fields
|
||||
// this resets the flags
|
||||
c_elem.type = ETYPE_BUTTON;
|
||||
Color bg_color = uint_to_rgba(0x0000ffff);
|
||||
|
||||
// if the element is new or the parent was updated then redo layout
|
||||
if (c_elem.flags.is_new || parent.flags.updated) {
|
||||
// 2. Layout
|
||||
c_elem.bounds = ctx.position_element(parent, size, true);
|
||||
|
||||
// TODO: 3. Fill the button specific fields
|
||||
}
|
||||
|
||||
c_elem.events = ctx.get_elem_events(c_elem);
|
||||
if (parent.flags.has_focus) {
|
||||
if (c_elem.events.mouse_hover) {
|
||||
c_elem.flags.has_focus = true;
|
||||
bg_color = uint_to_rgba(0x00ff00ff);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the button
|
||||
ctx.push_rect(c_elem.bounds, bg_color, do_border: true, do_radius: true)!;
|
||||
|
||||
return c_elem.events;
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
module ugui;
|
||||
|
||||
import std::ascii;
|
||||
|
||||
// FIXME: is this really the best solution?
|
||||
// "rect" is the bounding box of the element, which includes the border and the padding (so not just the content)
|
||||
fn void! Ctx.push_rect(&ctx, Rect rect, Color color, bool do_border = false, bool do_padding = false, bool do_radius = false)
|
||||
{
|
||||
// FIXME: this should be culled higher up, maybe
|
||||
if (rect.w <= 0 || rect.h <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Rect border = ctx.style.border;
|
||||
Rect padding = ctx.style.padding;
|
||||
ushort radius = ctx.style.radius;
|
||||
Color border_color = ctx.style.brcolor;
|
||||
|
||||
if (do_border) {
|
||||
Cmd cmd = {
|
||||
.type = CMD_RECT,
|
||||
.rect.rect = rect,
|
||||
.rect.color = border_color,
|
||||
.rect.radius = do_radius ? radius : 0,
|
||||
};
|
||||
ctx.cmd_queue.enqueue(&cmd)!;
|
||||
}
|
||||
|
||||
Cmd cmd = {
|
||||
.type = CMD_RECT,
|
||||
.rect.rect = {
|
||||
.x = rect.x + (do_border ? border.x : 0) + (do_padding ? padding.x : 0),
|
||||
.y = rect.y + (do_border ? border.y : 0) + (do_padding ? padding.y : 0),
|
||||
.w = rect.w - (do_border ? border.x+border.w : 0) - (do_padding ? padding.x+padding.w : 0),
|
||||
.h = rect.h - (do_border ? border.y+border.h : 0) - (do_padding ? padding.y+padding.h : 0),
|
||||
},
|
||||
.rect.color = color,
|
||||
.rect.radius = do_radius ? radius : 0,
|
||||
};
|
||||
ctx.cmd_queue.enqueue(&cmd)!;
|
||||
}
|
||||
|
||||
// TODO: add texture id
|
||||
fn void! Ctx.push_sprite(&ctx, Rect bounds, Rect texture)
|
||||
{
|
||||
Cmd cmd = {
|
||||
.type = CMD_SPRITE,
|
||||
.sprite.rect = bounds,
|
||||
.sprite.texture_rect = texture,
|
||||
};
|
||||
ctx.cmd_queue.enqueue(&cmd)!;
|
||||
}
|
||||
|
||||
fn void! Ctx.push_string(&ctx, Rect bounds, String text)
|
||||
{
|
||||
if (text.len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
short baseline = (short)ctx.font.ascender;
|
||||
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
|
||||
short line_gap = (short)ctx.font.linegap;
|
||||
Point orig = {
|
||||
.x = bounds.x,
|
||||
.y = bounds.y,
|
||||
};
|
||||
|
||||
short line_len;
|
||||
Codepoint cp;
|
||||
usz off, x;
|
||||
while ((cp = str_to_codepoint(text[off..], &x)) != 0) {
|
||||
off += x;
|
||||
Glyph* gp;
|
||||
if (!ascii::is_cntrl((char)cp)) {
|
||||
gp = ctx.font.get_glyph(cp)!;
|
||||
Rect gb = {
|
||||
.x = orig.x + line_len + gp.ox,
|
||||
.y = orig.y + gp.oy + baseline,
|
||||
.w = gp.w,
|
||||
.h = gp.h,
|
||||
};
|
||||
Rect gt = {
|
||||
.x = gp.u,
|
||||
.y = gp.v,
|
||||
.w = gp.w,
|
||||
.h = gp.h,
|
||||
};
|
||||
if (rect_collision(gb, bounds)) {
|
||||
ctx.push_sprite(gb, gt)!;
|
||||
}
|
||||
line_len += gp.adv;
|
||||
} else if (cp == '\n'){
|
||||
orig.y += line_height + line_gap;
|
||||
line_len = 0;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
240
src/ugui_data.c3
240
src/ugui_data.c3
@ -1,240 +0,0 @@
|
||||
module ugui;
|
||||
|
||||
import std::core::string;
|
||||
|
||||
import vtree;
|
||||
import cache;
|
||||
import fifo;
|
||||
|
||||
|
||||
struct Rect {
|
||||
short x, y, w, h;
|
||||
}
|
||||
|
||||
struct Point {
|
||||
short x, y;
|
||||
}
|
||||
|
||||
struct Color{
|
||||
char r, g, b, a;
|
||||
}
|
||||
|
||||
// element ids are just long ints
|
||||
def Id = usz;
|
||||
|
||||
enum ElemType {
|
||||
ETYPE_NONE,
|
||||
ETYPE_DIV,
|
||||
ETYPE_BUTTON,
|
||||
ETYPE_SLIDER,
|
||||
ETYPE_TEXT,
|
||||
}
|
||||
|
||||
bitstruct ElemFlags : uint {
|
||||
bool updated : 0;
|
||||
bool has_focus : 1;
|
||||
bool is_new : 2;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
enum DivLayout {
|
||||
LAYOUT_ROW,
|
||||
LAYOUT_COLUMN,
|
||||
LAYOUT_FLOATING,
|
||||
}
|
||||
|
||||
// div element
|
||||
struct Div {
|
||||
DivLayout layout;
|
||||
struct scroll {
|
||||
bool can_x;
|
||||
bool can_y;
|
||||
bool on_x;
|
||||
bool on_y;
|
||||
float value_x;
|
||||
float value_y;
|
||||
}
|
||||
Rect children_bounds;
|
||||
Point origin_r, origin_c;
|
||||
Color color_bg;
|
||||
}
|
||||
|
||||
// slider element
|
||||
struct Slider {
|
||||
float value;
|
||||
Rect handle;
|
||||
}
|
||||
|
||||
struct Text {
|
||||
char* str;
|
||||
}
|
||||
|
||||
// element structure
|
||||
struct Elem {
|
||||
Id id;
|
||||
ElemFlags flags;
|
||||
ElemEvents events;
|
||||
Rect bounds;
|
||||
ElemType type;
|
||||
union {
|
||||
Div div;
|
||||
Slider slider;
|
||||
Text text;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// relationships between elements are stored in a tree, it stores just the ids
|
||||
def IdTree = vtree::VTree(<Id>) @private;
|
||||
|
||||
// elements themselves are kept in a cache
|
||||
const uint MAX_ELEMENTS = 1024;
|
||||
def ElemCache = cache::Cache(<Id, Elem, MAX_ELEMENTS>) @private;
|
||||
|
||||
def CmdQueue = fifo::Fifo(<Cmd>);
|
||||
|
||||
fault UgError {
|
||||
INVALID_SIZE,
|
||||
EVENT_UNSUPPORTED,
|
||||
UNEXPECTED_ELEMENT,
|
||||
}
|
||||
|
||||
macro 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)
|
||||
};
|
||||
}
|
||||
|
||||
const Rect DIV_FILL = { .x = 0, .y = 0, .w = 0, .h = 0 };
|
||||
|
||||
macro abs(a) { return a < 0 ? -a : a; }
|
||||
macro clamp(x, min, max) { return x < min ? min : (x > max ? max : x); }
|
||||
|
||||
const uint STACK_STEP = 10;
|
||||
const uint MAX_ELEMS = 128;
|
||||
const uint MAX_CMDS = 256;
|
||||
const uint ROOT_ID = 1;
|
||||
|
||||
// command type
|
||||
enum CmdType {
|
||||
CMD_RECT,
|
||||
CMD_UPDATE_ATLAS,
|
||||
CMD_SPRITE,
|
||||
}
|
||||
|
||||
// command to draw a rect
|
||||
// TODO: implement radius
|
||||
struct CmdRect {
|
||||
Rect rect;
|
||||
ushort radius;
|
||||
Color color;
|
||||
}
|
||||
|
||||
// FIXME: For now only support black and white atlas, so PIXELFORMAT_UNCOMPRESSED_GRAYSCALE
|
||||
struct CmdUpdateAtlas {
|
||||
char* raw_buffer;
|
||||
short width, height;
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// 1. Add atlases as a data type
|
||||
// 2. Each atlas has an id
|
||||
struct CmdSprite {
|
||||
Rect rect;
|
||||
Rect texture_rect;
|
||||
}
|
||||
|
||||
// command structure
|
||||
struct Cmd {
|
||||
CmdType type;
|
||||
union {
|
||||
CmdRect rect;
|
||||
CmdUpdateAtlas update_atlas;
|
||||
CmdSprite sprite;
|
||||
}
|
||||
}
|
||||
|
||||
enum Layout {
|
||||
ROW,
|
||||
COLUMN,
|
||||
FLOATING
|
||||
}
|
||||
|
||||
// global style, similar to the css box model
|
||||
struct Style { // css box model
|
||||
Rect padding;
|
||||
Rect border;
|
||||
Rect margin;
|
||||
Color bgcolor; // background color
|
||||
Color fgcolor; // foreground color
|
||||
Color brcolor; // border color
|
||||
ushort radius;
|
||||
}
|
||||
|
||||
|
||||
struct Ctx {
|
||||
Layout layout;
|
||||
IdTree tree;
|
||||
ElemCache cache;
|
||||
CmdQueue cmd_queue;
|
||||
// total size in pixels of the context
|
||||
ushort width, height;
|
||||
Style style;
|
||||
Font font;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
isz active_div; // tree node indicating the current active div
|
||||
}
|
||||
|
||||
macro 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);
|
||||
}
|
||||
|
||||
// return true if rect a contains b
|
||||
macro 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);
|
||||
}
|
||||
|
||||
macro rect_intersection(Rect a, Rect b)
|
||||
{
|
||||
return Rect{
|
||||
.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),
|
||||
};
|
||||
}
|
||||
|
||||
// rect intersection not null
|
||||
macro rect_collision(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);
|
||||
}
|
115
src/ugui_div.c3
115
src/ugui_div.c3
@ -1,115 +0,0 @@
|
||||
module ugui;
|
||||
|
||||
import std::io;
|
||||
|
||||
fn void! Ctx.div_begin(&ctx, String label, Rect size)
|
||||
{
|
||||
Id id = label.hash();
|
||||
|
||||
Elem *parent = ctx.get_parent()!;
|
||||
Elem* c_elem = ctx.get_elem(id)!;
|
||||
isz div_node = ctx.tree.add(id, ctx.active_div)!;
|
||||
ctx.active_div = div_node;
|
||||
|
||||
// 1. Fill the element fields
|
||||
c_elem.type = ETYPE_DIV;
|
||||
|
||||
// do layout and update flags only if the element was updated
|
||||
if (c_elem.flags.is_new || parent.flags.updated) {
|
||||
// 2. layout the element
|
||||
c_elem.bounds = ctx.position_element(parent, size);
|
||||
if (c_elem.flags.is_new) {
|
||||
c_elem.div.children_bounds = c_elem.bounds;
|
||||
c_elem.div.color_bg = ctx.style.bgcolor;
|
||||
c_elem.div.scroll.can_x = false;
|
||||
c_elem.div.scroll.can_y = false;
|
||||
c_elem.div.scroll.value_x = 0;
|
||||
c_elem.div.scroll.value_y = 0;
|
||||
}
|
||||
|
||||
// 3. Mark the element as updated
|
||||
c_elem.flags.updated = true;
|
||||
// 4. Fill the div fields
|
||||
c_elem.div.origin_c = Point{
|
||||
.x = c_elem.bounds.x,
|
||||
.y = c_elem.bounds.y,
|
||||
};
|
||||
c_elem.div.origin_r = c_elem.div.origin_c;
|
||||
c_elem.div.layout = parent.div.layout;
|
||||
}
|
||||
|
||||
// Add the background to the draw stack
|
||||
ctx.push_rect(c_elem.bounds, c_elem.div.color_bg)!;
|
||||
|
||||
// TODO: check active
|
||||
// TODO: check resizeable
|
||||
|
||||
// check and draw scroll bars
|
||||
if (!rect_contains(c_elem.bounds, c_elem.div.children_bounds)) {
|
||||
Point cbc = {
|
||||
.x = c_elem.div.children_bounds.x + c_elem.div.children_bounds.w,
|
||||
.y = c_elem.div.children_bounds.y + c_elem.div.children_bounds.h,
|
||||
};
|
||||
Point bc = {
|
||||
.x = c_elem.bounds.x + c_elem.bounds.w,
|
||||
.y = c_elem.bounds.y + c_elem.bounds.h,
|
||||
};
|
||||
// vertical overflow, check and draw scroll bar
|
||||
if (cbc.y > bc.y && c_elem.div.scroll.can_y) {
|
||||
// set the scrollbar flag, is used in layout
|
||||
c_elem.div.scroll.on_y = true;
|
||||
|
||||
Rect vslider = {
|
||||
.x = c_elem.bounds.x + c_elem.bounds.w - 10,
|
||||
.y = c_elem.bounds.y,
|
||||
.w = 10,
|
||||
.h = c_elem.bounds.h,
|
||||
};
|
||||
|
||||
float vh = max((float)bc.y / cbc.y, (float)0.15);
|
||||
Rect vhandle = {
|
||||
.x = c_elem.bounds.x + c_elem.bounds.w - 10,
|
||||
.y = (short)(c_elem.bounds.y + (int)(c_elem.bounds.h*(1-vh) * c_elem.div.scroll.value_y)),
|
||||
.w = 10,
|
||||
.h = (short)(c_elem.bounds.h * vh),
|
||||
};
|
||||
|
||||
c_elem.events = ctx.get_elem_events(c_elem);
|
||||
if (parent.flags.has_focus && c_elem.events.mouse_hover && c_elem.events.mouse_hold && point_in_rect(ctx.input.mouse.pos, vhandle)) {
|
||||
short y = (short)clamp(ctx.input.mouse.pos.y - vhandle.h/2, vslider.y, vslider.y + vslider.h - vhandle.h);
|
||||
vhandle.y = y;
|
||||
float v = (float)(vhandle.y-vslider.y) / (float)(vslider.h-vhandle.h);
|
||||
c_elem.div.scroll.value_y = v;
|
||||
c_elem.flags.updated = true;
|
||||
c_elem.div.origin_c = Point{
|
||||
.x = c_elem.bounds.x,
|
||||
.y = c_elem.bounds.y,
|
||||
};
|
||||
c_elem.div.origin_r = c_elem.div.origin_c;
|
||||
}
|
||||
|
||||
ctx.push_rect(vslider, uint_to_rgba(0x999999ff))!;
|
||||
ctx.push_rect(vhandle, uint_to_rgba(0x9999ffff))!;
|
||||
} else {
|
||||
c_elem.div.scroll.on_y = false;
|
||||
}
|
||||
}
|
||||
|
||||
// if the bounds are outside of the view then allocate space for scrollbars
|
||||
DivLayout old_layout = c_elem.div.layout;
|
||||
c_elem.div.layout = LAYOUT_FLOATING;
|
||||
c_elem.div.layout = old_layout;
|
||||
|
||||
if (parent.flags.has_focus) {
|
||||
if (point_in_rect(ctx.input.mouse.pos, c_elem.bounds)) {
|
||||
c_elem.flags.has_focus = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn void! Ctx.div_end(&ctx)
|
||||
{
|
||||
// the active_div returns to the parent of the current one
|
||||
ctx.active_div = ctx.tree.parentof(ctx.active_div)!;
|
||||
}
|
221
src/ugui_font.c3
221
src/ugui_font.c3
@ -1,221 +0,0 @@
|
||||
module ugui;
|
||||
|
||||
import schrift;
|
||||
import std::collections::map;
|
||||
import std::core::mem;
|
||||
import std::io;
|
||||
|
||||
// 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 | | |
|
||||
* | :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;
|
||||
short idx; // atlas index
|
||||
}
|
||||
|
||||
const uint FONT_CACHED = 512;
|
||||
def GlyphTable = map::HashMap(<Codepoint, Glyph>) @private;
|
||||
|
||||
fault UgFontError {
|
||||
TTF_LOAD_FAILED,
|
||||
MISSING_GLYPH,
|
||||
BAD_GLYPH_METRICS,
|
||||
RENDER_ERROR,
|
||||
}
|
||||
|
||||
fault UgAtlasError {
|
||||
CANNOT_PLACE,
|
||||
}
|
||||
|
||||
// black and white atlas
|
||||
struct AtlasBW {
|
||||
ushort width, height;
|
||||
char[] buffer;
|
||||
|
||||
Point row;
|
||||
ushort row_h;
|
||||
}
|
||||
|
||||
struct Font {
|
||||
schrift::Sft sft;
|
||||
String path;
|
||||
GlyphTable table;
|
||||
|
||||
float size;
|
||||
float ascender, descender, linegap; // Line Metrics
|
||||
AtlasBW[] atlas;
|
||||
}
|
||||
|
||||
fn void! AtlasBW.new(&atlas, ushort width, ushort height)
|
||||
{
|
||||
atlas.width = width;
|
||||
atlas.height = height;
|
||||
atlas.buffer = mem::new_array(char, (usz)atlas.width*atlas.height);
|
||||
}
|
||||
|
||||
fn void AtlasBW.free(&atlas)
|
||||
{
|
||||
free(atlas.buffer);
|
||||
}
|
||||
|
||||
// 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! AtlasBW.place(&atlas, char[] pixels, ushort w, ushort h)
|
||||
{
|
||||
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 UgAtlasError.CANNOT_PLACE?;
|
||||
}
|
||||
}
|
||||
|
||||
for (usz y = 0; y < h; y++) {
|
||||
for (usz x = 0; x < w; x++) {
|
||||
atlas.buffer[(usz)(p.y+y)*atlas.width + (p.x+x)] = pixels[y*w + x];
|
||||
}
|
||||
}
|
||||
|
||||
atlas.row.x += w;
|
||||
if (h > atlas.row_h) {
|
||||
atlas.row_h = h;
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
fn void! Font.load(&font, String path, uint height, float scale = 1)
|
||||
{
|
||||
font.table.new_init(capacity: FONT_CACHED);
|
||||
|
||||
font.size = height*scale;
|
||||
|
||||
font.sft = schrift::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) {
|
||||
return UgFontError.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'
|
||||
font.atlas = mem::new_array(AtlasBW, 1);
|
||||
ushort size = (ushort)font.size*256;
|
||||
font.atlas[0].new(size, size)!;
|
||||
|
||||
// preallocate the ASCII range
|
||||
// for (char c = ' '; c < '~'; c++) {
|
||||
// font.get_glyph((Codepoint)c)!;
|
||||
// }
|
||||
}
|
||||
|
||||
fn Glyph*! Font.get_glyph(&font, Codepoint code, bool* is_new = null)
|
||||
{
|
||||
Glyph*! gp;
|
||||
gp = font.table.get_ref(code);
|
||||
|
||||
if (catch excuse = gp) {
|
||||
if (excuse != SearchResult.MISSING) {
|
||||
return excuse?;
|
||||
}
|
||||
} else {
|
||||
if (is_new) { *is_new = false; }
|
||||
return gp;
|
||||
}
|
||||
|
||||
// missing glyph, render and place into an atlas
|
||||
Glyph glyph;
|
||||
|
||||
schrift::SftGlyph gid;
|
||||
schrift::SftGMetrics gmtx;
|
||||
|
||||
if (schrift::lookup(&font.sft, code, &gid) < 0) {
|
||||
return UgFontError.MISSING_GLYPH?;
|
||||
}
|
||||
|
||||
if (schrift::gmetrics(&font.sft, gid, &gmtx) < 0) {
|
||||
return UgFontError.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 UgFontError.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[0].place(pixels, glyph.w, glyph.h)!;
|
||||
glyph.idx = 0;
|
||||
glyph.u = uv.x;
|
||||
glyph.v = uv.y;
|
||||
|
||||
mem::free(pixels);
|
||||
|
||||
font.table.set(code, glyph);
|
||||
|
||||
if (is_new) { *is_new = true; }
|
||||
return font.table.get_ref(code);
|
||||
}
|
||||
|
||||
fn void Font.free(&font)
|
||||
{
|
||||
foreach (atlas: font.atlas) {
|
||||
atlas.free();
|
||||
}
|
||||
schrift::freefont(font.sft.font);
|
||||
}
|
152
src/ugui_impl.c3
152
src/ugui_impl.c3
@ -1,152 +0,0 @@
|
||||
module ugui;
|
||||
|
||||
import std::io;
|
||||
|
||||
// return a pointer to the parent of the current active div
|
||||
fn Elem*! Ctx.get_parent(&ctx)
|
||||
{
|
||||
// FIXME: if the tree held pointers to the elements then no more
|
||||
// redundant cache search
|
||||
Id parent_id = ctx.tree.get(ctx.active_div)!;
|
||||
return ctx.cache.search(parent_id);
|
||||
}
|
||||
|
||||
// get or push an element from the cache, return a pointer to it
|
||||
// resets all flags except is_new which is set accordingly
|
||||
macro Ctx.get_elem(&ctx, Id id)
|
||||
{
|
||||
Elem empty_elem;
|
||||
bool is_new;
|
||||
Elem* c_elem;
|
||||
c_elem = ctx.cache.get_or_insert(&empty_elem, id, &is_new)!;
|
||||
c_elem.flags = (ElemFlags)0;
|
||||
c_elem.flags.is_new = is_new;
|
||||
return c_elem;
|
||||
}
|
||||
|
||||
// this searches an element in the cache by label, it does not create a new element
|
||||
// if it does't find one
|
||||
macro Ctx.get_elem_by_label(&ctx, String label)
|
||||
{
|
||||
Id id = label.hash();
|
||||
return ctx.cache.search(id);
|
||||
}
|
||||
|
||||
macro Ctx.get_elem_by_tree_idx(&ctx, isz idx) @private
|
||||
{
|
||||
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_ELEMENTS)!;
|
||||
defer catch { (void)ctx.cmd_queue.free(); }
|
||||
|
||||
ctx.layout = Layout.ROW;
|
||||
ctx.active_div = 0;
|
||||
|
||||
// TODO: add style config
|
||||
ctx.style.margin = Rect{2, 2, 2, 2};
|
||||
ctx.style.border = Rect{2, 2, 2, 2};
|
||||
ctx.style.padding = Rect{1, 1, 1, 1};
|
||||
ctx.style.radius = 5;
|
||||
ctx.style.bgcolor = uint_to_rgba(0x282828ff);
|
||||
ctx.style.fgcolor = uint_to_rgba(0xfbf1c7ff);
|
||||
ctx.style.brcolor = uint_to_rgba(0xd79921ff);
|
||||
}
|
||||
|
||||
fn void Ctx.free(&ctx)
|
||||
{
|
||||
(void)ctx.tree.free();
|
||||
(void)ctx.cache.free();
|
||||
(void)ctx.cmd_queue.free();
|
||||
}
|
||||
|
||||
fn void! Ctx.frame_begin(&ctx)
|
||||
{
|
||||
|
||||
// 2. Get the root element from the cache and update it
|
||||
Elem* c_elem = ctx.get_elem(ROOT_ID)!;
|
||||
// The root should have the updated flag only if the size of the window
|
||||
// was changed between frames, this propagates an element size recalculation
|
||||
// down the element tree
|
||||
c_elem.flags.updated = ctx.input.events.resize | ctx.input.events.force_update;
|
||||
ctx.input.events.force_update = false;
|
||||
// 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
|
||||
c_elem.flags.has_focus = ctx.has_focus;
|
||||
|
||||
if (c_elem.flags.is_new || c_elem.flags.updated) {
|
||||
Elem def_root = {
|
||||
.id = ROOT_ID,
|
||||
.type = ETYPE_DIV,
|
||||
.bounds = {
|
||||
.w = ctx.width,
|
||||
.h = ctx.height,
|
||||
},
|
||||
.div = {
|
||||
.layout = LAYOUT_ROW,
|
||||
.children_bounds = {
|
||||
.w = ctx.width,
|
||||
.h = ctx.height,
|
||||
}
|
||||
},
|
||||
.flags = c_elem.flags,
|
||||
};
|
||||
|
||||
*c_elem = def_root;
|
||||
}
|
||||
|
||||
// 3. Push the root element into the element tree
|
||||
ctx.active_div = ctx.tree.add(ROOT_ID, 0)!;
|
||||
|
||||
// The root element does not push anything to the stack
|
||||
// TODO: add a background color taken from a theme or config
|
||||
}
|
||||
|
||||
fn void! Ctx.force_update(&ctx)
|
||||
{
|
||||
ctx.input.events.force_update = true;
|
||||
}
|
||||
|
||||
fn void! Ctx.frame_end(&ctx)
|
||||
{
|
||||
// 1. clear the tree
|
||||
ctx.tree.prune(0)!;
|
||||
|
||||
// 2. clear input fields
|
||||
bool f = ctx.input.events.force_update;
|
||||
ctx.input.events = (InputEvents)0;
|
||||
ctx.input.events.force_update = f;
|
||||
|
||||
// draw mouse position
|
||||
$if 1:
|
||||
Cmd cmd = {
|
||||
.type = CMD_RECT,
|
||||
.rect.rect = {
|
||||
.x = ctx.input.mouse.pos.x - 2,
|
||||
.y = ctx.input.mouse.pos.y - 2,
|
||||
.w = 4,
|
||||
.h = 4,
|
||||
},
|
||||
.rect.color = uint_to_rgba(0xff00ffff)
|
||||
};
|
||||
ctx.cmd_queue.enqueue(&cmd)!;
|
||||
$endif
|
||||
}
|
||||
|
||||
<*
|
||||
* @ensure elem != null
|
||||
*>
|
||||
fn bool Ctx.is_hovered(&ctx, Elem *elem)
|
||||
{
|
||||
return point_in_rect(ctx.input.mouse.pos, elem.bounds);
|
||||
}
|
@ -1,114 +0,0 @@
|
||||
module ugui;
|
||||
|
||||
import std::io;
|
||||
|
||||
// TODO: this could be a bitstruct
|
||||
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 force_update : 4;
|
||||
}
|
||||
|
||||
// Window size was changed
|
||||
fn void! Ctx.input_window_size(&ctx, short width, short height)
|
||||
{
|
||||
if (width <= 0 || height <= 0) {
|
||||
return UgError.INVALID_SIZE?;
|
||||
}
|
||||
ctx.width = width;
|
||||
ctx.height = height;
|
||||
ctx.input.events.resize = true;
|
||||
}
|
||||
|
||||
// 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
|
||||
if (ctx.has_focus != has_focus) {
|
||||
ctx.input.events.change_focus = true;
|
||||
}
|
||||
ctx.has_focus = has_focus;
|
||||
}
|
||||
|
||||
bitstruct MouseButtons : uint {
|
||||
bool btn_left : 0;
|
||||
bool btn_middle : 1;
|
||||
bool btn_right : 2;
|
||||
bool btn_4 : 3;
|
||||
bool btn_5 : 4;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
const MouseButtons BTN_NONE = (MouseButtons)0u;
|
||||
const MouseButtons BTN_ANY = (MouseButtons)(uint.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};
|
||||
|
||||
// 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;
|
||||
|
||||
macro ElemEvents Ctx.get_elem_events(&ctx, Elem *elem)
|
||||
{
|
||||
// TODO: add the other events
|
||||
// FIXME: active should be elsewhere
|
||||
bool active = elem.events.mouse_hold || ctx.is_hovered(elem);
|
||||
ElemEvents ev = {
|
||||
.mouse_hover = ctx.is_hovered(elem),
|
||||
.mouse_press = active & ctx.is_mouse_pressed(BTN_ANY),
|
||||
.mouse_release = active & ctx.is_mouse_released(BTN_ANY),
|
||||
.mouse_hold = active & ctx.is_mouse_down(BTN_ANY),
|
||||
};
|
||||
return ev;
|
||||
}
|
||||
|
||||
// Mouse Button moved
|
||||
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 = true;
|
||||
}
|
||||
|
||||
// Mouse was moved, report absolute position
|
||||
fn void Ctx.input_mouse_abs(&ctx, short x, short y)
|
||||
{
|
||||
ctx.input.mouse.pos.x = clamp(x, 0u16, ctx.width);
|
||||
ctx.input.mouse.pos.y = clamp(y, 0u16, 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 = true;
|
||||
}
|
||||
|
||||
// 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 = clamp(mx, 0u16, ctx.width);
|
||||
ctx.input.mouse.pos.y = clamp(my, 0u16, ctx.height);
|
||||
|
||||
ctx.input.events.mouse_move = true;
|
||||
}
|
@ -1,177 +0,0 @@
|
||||
module ugui;
|
||||
|
||||
|
||||
fn void! Ctx.layout_set_row(&ctx)
|
||||
{
|
||||
Id parent_id = ctx.tree.get(ctx.active_div)!;
|
||||
Elem *parent = ctx.cache.search(parent_id)!;
|
||||
|
||||
if (parent.type != ETYPE_DIV) {
|
||||
// what?
|
||||
return UgError.UNEXPECTED_ELEMENT?;
|
||||
}
|
||||
|
||||
parent.div.layout = LAYOUT_ROW;
|
||||
}
|
||||
|
||||
fn void! Ctx.layout_set_column(&ctx)
|
||||
{
|
||||
Id parent_id = ctx.tree.get(ctx.active_div)!;
|
||||
Elem *parent = ctx.cache.search(parent_id)!;
|
||||
|
||||
if (parent.type != ETYPE_DIV) {
|
||||
// what?
|
||||
return UgError.UNEXPECTED_ELEMENT?;
|
||||
}
|
||||
|
||||
parent.div.layout = LAYOUT_COLUMN;
|
||||
}
|
||||
|
||||
fn void! Ctx.layout_set_floating(&ctx)
|
||||
{
|
||||
Id parent_id = ctx.tree.get(ctx.active_div)!;
|
||||
Elem *parent = ctx.cache.search(parent_id)!;
|
||||
|
||||
if (parent.type != ETYPE_DIV) {
|
||||
// what?
|
||||
return UgError.UNEXPECTED_ELEMENT?;
|
||||
}
|
||||
|
||||
parent.div.layout = LAYOUT_FLOATING;
|
||||
}
|
||||
|
||||
fn void! Ctx.layout_next_row(&ctx)
|
||||
{
|
||||
Id parent_id = ctx.tree.get(ctx.active_div)!;
|
||||
Elem *parent = ctx.cache.search(parent_id)!;
|
||||
|
||||
if (parent.type != ETYPE_DIV) {
|
||||
// what?
|
||||
return UgError.UNEXPECTED_ELEMENT?;
|
||||
}
|
||||
|
||||
parent.div.origin_r = Point{
|
||||
.x = parent.bounds.x,
|
||||
.y = parent.div.origin_c.y,
|
||||
};
|
||||
parent.div.origin_c = parent.div.origin_r;
|
||||
}
|
||||
|
||||
fn void! Ctx.layout_next_column(&ctx)
|
||||
{
|
||||
Id parent_id = ctx.tree.get(ctx.active_div)!;
|
||||
Elem *parent = ctx.cache.search(parent_id)!;
|
||||
|
||||
if (parent.type != ETYPE_DIV) {
|
||||
// what?
|
||||
return UgError.UNEXPECTED_ELEMENT?;
|
||||
}
|
||||
|
||||
parent.div.origin_c = Point{
|
||||
.x = parent.div.origin_r.x,
|
||||
.y = parent.bounds.y,
|
||||
};
|
||||
parent.div.origin_r = parent.div.origin_c;
|
||||
}
|
||||
|
||||
// position the rectangle inside the parent according to the layout
|
||||
// parent: parent div
|
||||
// rect: the requested size
|
||||
// style: apply style
|
||||
<*
|
||||
@require ctx != null
|
||||
@require parent.type == ETYPE_DIV
|
||||
*>
|
||||
fn Rect Ctx.position_element(&ctx, Elem *parent, Rect rect, bool style = false)
|
||||
{
|
||||
Rect placement;
|
||||
Point origin;
|
||||
Div* div = &parent.div;
|
||||
|
||||
// 1. Select the right origin
|
||||
switch (div.layout) {
|
||||
case LAYOUT_ROW:
|
||||
origin = div.origin_r;
|
||||
case LAYOUT_COLUMN:
|
||||
origin = div.origin_c;
|
||||
case LAYOUT_FLOATING: // none
|
||||
default:
|
||||
// Error
|
||||
}
|
||||
|
||||
// the bottom-right border of the element box
|
||||
Point pl_corner;
|
||||
|
||||
// 2. Calculate the placement
|
||||
placement.x = (short)max(origin.x + rect.x, 0);
|
||||
placement.y = (short)max(origin.y + rect.y, 0);
|
||||
placement.w = rect.w > 0 ? rect.w : (short)max(parent.bounds.w - (placement.x - parent.bounds.x), 0);
|
||||
placement.h = rect.h > 0 ? rect.h : (short)max(parent.bounds.h - (placement.y - parent.bounds.y), 0);
|
||||
|
||||
pl_corner.x = placement.x + placement.w;
|
||||
pl_corner.y = placement.y + placement.h;
|
||||
|
||||
// 2.1 apply style, css box model
|
||||
if (style) {
|
||||
Rect margin = ctx.style.margin;
|
||||
Rect border = ctx.style.border;
|
||||
Rect padding = ctx.style.padding;
|
||||
|
||||
placement.x += margin.x;
|
||||
placement.y += margin.y;
|
||||
if (rect.w != 0) { placement.w += border.x+border.w + padding.x+padding.w; }
|
||||
if (rect.h != 0) { placement.h += border.y+border.h + padding.y+padding.h; }
|
||||
|
||||
pl_corner.x = placement.x + placement.w + margin.w;
|
||||
pl_corner.y = placement.y + placement.h + margin.h;
|
||||
}
|
||||
|
||||
// 3. Update the origins of the parent
|
||||
div.origin_r = Point{
|
||||
.x = pl_corner.x,
|
||||
.y = origin.y,
|
||||
};
|
||||
div.origin_c = Point{
|
||||
.x = origin.x,
|
||||
.y = pl_corner.y,
|
||||
};
|
||||
|
||||
// 4. Calculate the "scrolled" view
|
||||
Point off;
|
||||
Rect* cb = &div.children_bounds;
|
||||
if (div.scroll.can_x && div.scroll.on_x) {
|
||||
off.x = (short)(cb.w * div.scroll.value_x);
|
||||
}
|
||||
if (div.scroll.can_y && div.scroll.on_y) {
|
||||
off.y = (short)((float)(cb.h - parent.bounds.h) * div.scroll.value_y);
|
||||
}
|
||||
Rect view = {
|
||||
.x = parent.bounds.x + off.x,
|
||||
.y = parent.bounds.y + off.y,
|
||||
.w = parent.bounds.w,
|
||||
.h = parent.bounds.h,
|
||||
};
|
||||
|
||||
// 5. check if the placement overflows the children ounds, if so update them
|
||||
if (!point_in_rect(pl_corner, *cb)) {
|
||||
if (pl_corner.y > cb.y+cb.h) {
|
||||
cb.h = pl_corner.y - cb.y;
|
||||
}
|
||||
if (pl_corner.x > cb.x+cb.w) {
|
||||
cb.w += pl_corner.x - (cb.x + cb.w);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. check if the placement is inside the view
|
||||
if (rect_collision(placement, view)) {
|
||||
return Rect{
|
||||
.x = placement.x - off.x,
|
||||
.y = placement.y - off.y,
|
||||
.w = placement.w,
|
||||
.h = placement.h,
|
||||
};
|
||||
} else {
|
||||
return Rect{};
|
||||
}
|
||||
}
|
||||
|
@ -1,108 +0,0 @@
|
||||
module ugui;
|
||||
|
||||
import std::io;
|
||||
|
||||
/* handle
|
||||
* +----+-----+---------------------+
|
||||
* | |#####| |
|
||||
* +----+-----+---------------------+
|
||||
*/
|
||||
fn ElemEvents! Ctx.slider_hor(&ctx, String label, Rect size)
|
||||
{
|
||||
Id id = label.hash();
|
||||
|
||||
Elem *parent = ctx.get_parent()!;
|
||||
Elem *c_elem = ctx.get_elem(id)!;
|
||||
// add it to the tree
|
||||
ctx.tree.add(id, ctx.active_div)!;
|
||||
|
||||
// 1. Fill the element fields
|
||||
c_elem.type = ETYPE_SLIDER;
|
||||
|
||||
// if the element is new or the parent was updated then redo layout
|
||||
if (c_elem.flags.is_new || parent.flags.updated) {
|
||||
// 2. Layout
|
||||
c_elem.bounds = ctx.position_element(parent, size, true);
|
||||
c_elem.slider.handle = Rect{
|
||||
.x = (short)(c_elem.bounds.x + (int)(c_elem.bounds.w*(1.0-0.25) * c_elem.slider.value)),
|
||||
.y = c_elem.bounds.y,
|
||||
.w = (short)(c_elem.bounds.w * 0.25),
|
||||
.h = c_elem.bounds.h,
|
||||
};
|
||||
}
|
||||
|
||||
c_elem.events = ctx.get_elem_events(c_elem);
|
||||
if (parent.flags.has_focus && c_elem.events.mouse_hover) {
|
||||
if (point_in_rect(ctx.input.mouse.pos, c_elem.slider.handle) && c_elem.events.mouse_hold) {
|
||||
short x = (short)clamp(ctx.input.mouse.pos.x - c_elem.slider.handle.w/2, c_elem.bounds.x, c_elem.bounds.x + c_elem.bounds.w - c_elem.slider.handle.w);
|
||||
float v = (float)(c_elem.slider.handle.x-c_elem.bounds.x) / (float)(c_elem.bounds.w-c_elem.slider.handle.w);
|
||||
c_elem.slider.handle.x = x;
|
||||
c_elem.slider.value = v;
|
||||
c_elem.events.update = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the slider background and handle
|
||||
Color bg_color = uint_to_rgba(0x0000ffff);
|
||||
Color handle_color = uint_to_rgba(0x0ff000ff);
|
||||
ctx.push_rect(c_elem.bounds, bg_color)!;
|
||||
ctx.push_rect(c_elem.slider.handle, handle_color)!;
|
||||
|
||||
return c_elem.events;
|
||||
}
|
||||
|
||||
/*
|
||||
* +-+
|
||||
* | |
|
||||
* | |
|
||||
* +-+
|
||||
* |#| handle
|
||||
* |#|
|
||||
* +-+
|
||||
* | |
|
||||
* | |
|
||||
* +-+
|
||||
*/
|
||||
fn ElemEvents! Ctx.slider_ver(&ctx, String label, Rect size)
|
||||
{
|
||||
Id id = label.hash();
|
||||
|
||||
Elem *parent = ctx.get_parent()!;
|
||||
Elem *c_elem = ctx.get_elem(id)!;
|
||||
// add it to the tree
|
||||
ctx.tree.add(id, ctx.active_div)!;
|
||||
|
||||
// 1. Fill the element fields
|
||||
c_elem.type = ETYPE_SLIDER;
|
||||
|
||||
// if the element is new or the parent was updated then redo layout
|
||||
if (c_elem.flags.is_new || parent.flags.updated) {
|
||||
// 2. Layout
|
||||
c_elem.bounds = ctx.position_element(parent, size, true);
|
||||
c_elem.slider.handle = Rect{
|
||||
.x = c_elem.bounds.x,
|
||||
.y = (short)(c_elem.bounds.y + (int)(c_elem.bounds.h*(1.0-0.25) * c_elem.slider.value)),
|
||||
.w = c_elem.bounds.w,
|
||||
.h = (short)(c_elem.bounds.h * 0.25),
|
||||
};
|
||||
}
|
||||
|
||||
c_elem.events = ctx.get_elem_events(c_elem);
|
||||
if (parent.flags.has_focus && c_elem.events.mouse_hover) {
|
||||
if (point_in_rect(ctx.input.mouse.pos, c_elem.slider.handle) && c_elem.events.mouse_hold) {
|
||||
short y = (short)clamp(ctx.input.mouse.pos.y - c_elem.slider.handle.h/2, c_elem.bounds.y, c_elem.bounds.y + c_elem.bounds.h - c_elem.slider.handle.h);
|
||||
float v = (float)(c_elem.slider.handle.y-c_elem.bounds.y) / (float)(c_elem.bounds.h-c_elem.slider.handle.h);
|
||||
c_elem.slider.handle.y = y;
|
||||
c_elem.slider.value = v;
|
||||
c_elem.events.update = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the slider background and handle
|
||||
Color bg_color = uint_to_rgba(0x0000ffff);
|
||||
Color handle_color = uint_to_rgba(0x0ff000ff);
|
||||
ctx.push_rect(c_elem.bounds, bg_color)!;
|
||||
ctx.push_rect(c_elem.slider.handle, handle_color)!;
|
||||
|
||||
return c_elem.events;
|
||||
}
|
103
src/ugui_text.c3
103
src/ugui_text.c3
@ -1,103 +0,0 @@
|
||||
module ugui;
|
||||
|
||||
import std::io;
|
||||
import std::ascii;
|
||||
import grapheme;
|
||||
|
||||
<*
|
||||
@require off != null
|
||||
*>
|
||||
fn Codepoint str_to_codepoint(char[] str, usz* off)
|
||||
{
|
||||
Codepoint cp;
|
||||
isz b = grapheme::decode_utf8(str, str.len, &cp);
|
||||
if (b == 0 || b > str.len) {
|
||||
return 0;
|
||||
}
|
||||
*off = b;
|
||||
return cp;
|
||||
}
|
||||
|
||||
fn Rect! Ctx.get_text_bounds(&ctx, String text, bool* update_atlas)
|
||||
{
|
||||
Rect text_bounds;
|
||||
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
|
||||
short line_gap = (short)ctx.font.linegap;
|
||||
text_bounds.h = line_height;
|
||||
Glyph* gp;
|
||||
|
||||
// TODO: account for unicode codepoints
|
||||
short line_len;
|
||||
Codepoint cp;
|
||||
usz off, x;
|
||||
while ((cp = str_to_codepoint(text[off..], &x)) != 0) {
|
||||
off += x;
|
||||
bool n;
|
||||
if (!ascii::is_cntrl((char)cp)) {
|
||||
gp = ctx.font.get_glyph(cp, &n)!;
|
||||
line_len += gp.adv;
|
||||
if (n) { *update_atlas = true; }
|
||||
} else if (cp == '\n'){
|
||||
text_bounds.h += line_height + line_gap;
|
||||
line_len = 0;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
if (line_len > text_bounds.w) {
|
||||
text_bounds.w = line_len;
|
||||
}
|
||||
}
|
||||
|
||||
return text_bounds;
|
||||
}
|
||||
|
||||
fn void! Ctx.text_unbounded(&ctx, String label, String text)
|
||||
{
|
||||
Id id = label.hash();
|
||||
|
||||
Elem *parent = ctx.get_parent()!;
|
||||
Elem *c_elem = ctx.get_elem(id)!;
|
||||
// add it to the tree
|
||||
ctx.tree.add(id, ctx.active_div)!;
|
||||
|
||||
// 1. Fill the element fields
|
||||
// this resets the flags
|
||||
c_elem.type = ETYPE_TEXT;
|
||||
|
||||
short baseline = (short)ctx.font.ascender;
|
||||
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
|
||||
short line_gap = (short)ctx.font.linegap;
|
||||
bool update_atlas;
|
||||
// if the element is new or the parent was updated then redo layout
|
||||
if (c_elem.flags.is_new || parent.flags.updated) {
|
||||
Rect text_size = ctx.get_text_bounds(text, &update_atlas)!;
|
||||
|
||||
// 2. Layout
|
||||
c_elem.bounds = ctx.position_element(parent, text_size, true);
|
||||
|
||||
// 3. Fill the button specific fields
|
||||
c_elem.text.str = text;
|
||||
}
|
||||
|
||||
if (update_atlas) {
|
||||
// FIXME: atlas here is hardcoded, look at the todo in ugui_data
|
||||
Cmd up = {
|
||||
.type = CMD_UPDATE_ATLAS,
|
||||
.update_atlas = {
|
||||
.raw_buffer = ctx.font.atlas[0].buffer,
|
||||
.width = ctx.font.atlas[0].width,
|
||||
.height = ctx.font.atlas[0].height,
|
||||
},
|
||||
};
|
||||
ctx.cmd_queue.enqueue(&up)!;
|
||||
}
|
||||
|
||||
Cmd bounds = {
|
||||
.type = CMD_RECT,
|
||||
.rect.rect = c_elem.bounds,
|
||||
.rect.color = uint_to_rgba(0x000000ff),
|
||||
};
|
||||
ctx.cmd_queue.enqueue(&bounds)!;
|
||||
|
||||
ctx.push_string(c_elem.bounds, text)!;
|
||||
}
|
19
test/test_color.c3
Normal file
19
test/test_color.c3
Normal 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());
|
||||
}
|
72
test/test_error.c3
Normal file
72
test/test_error.c3
Normal 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);
|
||||
}
|
30
test/test_idgen.c3
Normal file
30
test/test_idgen.c3
Normal 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
26
test/test_keyboard.c3
Normal 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;
|
||||
}
|
||||
|
94
test_renderer.c3
Normal file
94
test_renderer.c3
Normal 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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user