commit 0b3af7375e0698ab38a4036864f2b15ff9f2381d Author: Alessandro Mauri Date: Sat Oct 25 17:43:06 2025 +0200 initial commit diff --git a/.ecode/project_build.json b/.ecode/project_build.json new file mode 100644 index 0000000..304f2f4 --- /dev/null +++ b/.ecode/project_build.json @@ -0,0 +1,43 @@ +{ + "Debug": { + "build": [ + { + "args": "-C resources/shaders", + "command": "make", + "working_dir": "" + }, + { + "args": "build -g --threads 8", + "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": "" + } + ] + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d6409b0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +resources/shaders/compiled/** +build diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a5818b7 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,12 @@ +[submodule "lib/ugui_sdl.c3l"] + path = lib/ugui_sdl.c3l + url = https://git.alemauri.eu/alema/ugui_sdl.c3l.git +[submodule "lib/sdl3.c3l"] + path = lib/sdl3.c3l + url = https://git.alemauri.eu/alema/sdl3.c3l.git +[submodule "lib/schrift.c3l"] + path = lib/schrift.c3l + url = https://git.alemauri.eu/alema/schrift.c3l.git +[submodule "lib/ugui.c3l"] + path = lib/ugui.c3l + url = https://git.alemauri.eu/alema/ugui.c3l.git diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..b533650 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,184 @@ +## High level overview + +Under the hood every element has an id, this id allows the library to store state +between frames. +Elements are also cached such that when the ui tree is rebuilt at the beginning of +every frame the element data structure doesn't have to be rebuilt. + +Elements are arranged in a tree, nodes are container elements that can contain other +elements, leafs are elements that cannot contain other elements. + +Every element has a size and a position, containers also have to keep track of their +layout information and some other state. + +Elements can push commands into the draw stack, which is a structure that contains +all the draw commands that the user application has to perform do display the ui +correctly, such commands include drawing lines, rectangles, sprites, text, etc. + + +```text + +-----------+ + | ug_init() | + +-----+-----+ + | + | + | + +---------v----------+ + |ug_input_keyboard() | + |ug_input_mouse() <----+ + |ug_input_clipboard()| | + | ... | | + +---------+----------+ | + | | + | | + +-------v--------+ | + |ug_frame_begin()| | + +-------+--------+ | + | | + | | + +---------v----------+ | + |ug_window_start() | | + +---->ug_container_start()| | + | |ug_div_start() | | + | | ... | | + | +---------+----------+ | + | | | + | | | +multiple +--------v---------+ | + times |ug_layout_row() | | + | |ug_layout_column()| | + | |ug_layout_float() | | + | | ... | | + | +--------+---------+ | + | | | + | | | + | +------v------+ | + | |ug_button() | | + | |ug_text_box()| | + | |ug_slider() | | + | | ... | | + | +------+------+ | + | | | + +--------------+ | + | | + +--------v---------+ | + |ug_window_end() | | + |ug_container_end()| | + |ug_div_end() | | + | ... | | + +--------+---------+ | + | | + | | + | | + +------v-------+ | + |ug_frame_end()| | + +------+-------+ | + | | + | | + | | + +------v-------+ | + |user draws the| | + | ui +-------+ + +------+-------+ + | + |quit + | + +------v-------+ + | ug_destroy() | + +--------------+ +``` + +### Layouting + +Layouting happens in a dynamic grid, when a new element is inserted in a non-floating +manner it reserves a space in the grid, new elements are placed following this grid. + +Every div has two points of origin, one for the row layout and one for the column +layout, named origin_r and origin_c respectively + +origin_r is used when the row layout is used and it is used to position the child +elements one next to the other, as such it always points to the top-right edge +of the last row element + +```text +Layout: row +#: lost space + Parent div +x---------------------------------+ +|[origin_c] | +|[origin_r] | +| | +| | +| | +| | +| | +| | +| | ++---------------------------------+ + + Parent div ++-----x---------------------------+ +| |[origin_r] | +| E1 | | +| | | +x-----+---------------------------+ +|[origin_c] | +| | | +| | | +| | | +| | | +| | | ++-----+---------------------------+ + + Parent div ++-----+----------+-----x----------+ +| | E2 | |[origin_r]| +| E1 +----------+ | | +| |##########| E3 | | ++-----+##########| | | +|################| | | ++----------------x-----+----------+ +| [origin_c] | +| | | +| | | +| | | ++----------------+----------------+ +``` + +TODO: handle when the content overflows the div +- Use a different concept, like a view or relative space, for example the child +div could have position `[0,0]` but in reality it is relative to the origin of the +parent div +- each div could have a view and a total area of the content, when drawing everything +is clipped to the view and scrollbars are shown +- individual elements accept dimensions and the x/y coordinates could be interpreted +as offset if the layout is row/column or absolute coordinates if the leayout is floating + +A div can be marked resizeable or fixed, and static or dynamic. The difference being +that resizeable adds a resize handle to the div and dynamic lets the content overflow +causing scrollbars to be drawn + +### Notes + +How elements determine if they have focus or not + +```C +// in begin_{container} code +calculate focus +set has_focus property + + + +// in the element code +if(PARENT_HAS_FOCUS()) { + update stuff +} else { + fast path to return +} +``` + +How to get ids: + 1. use a name for each element + 2. supply an id for each element + 3. use a macro and the line position as id and then hash it + 4. use a macro, get the code line and hash it diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7b9969a --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +C3FLAGS = -g + +main: src/main.c3 $(wildcard lib/ugui.c3l/src/*.c3) $(wildcard lib/ugui_sdl.c3l/) + make -C resources/shaders + c3c build ${C3FLAGS} diff --git a/README.md b/README.md new file mode 100644 index 0000000..bb62933 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# UGUI + +Immediate mode ui library for C3. (Name subject to change). + +This repo contains the library itself, a renderer based on SLD3's new GPU API and a test program. +The library is located at `lib/ugui.c3l`. + +The layout algorithm is similar to flexbox. A graphical description of the layout algorithm is located in the [LAYOUT](lib/ugui.c3l/LAYOUT) file. The structure of the library is (was) described in the [ARCHITECTURE](ARCHITECTURE.md) file, most of it is now irrelevant and will be updated in the future. + +## Dependencies + +* `shaderc` for `glslc` +* `c3c` version >0.7.5 +* `SDL3` +* A C compiler + +## Building + +1. Clone the repo and all dependencies +```sh +git clone https://github.com/ma-ale/ugui +cd ugui +git submodule update --recursive --init +``` + +2. Build libgrapheme +```sh +cd lib/grapheme.c3l +make +cd ../.. +``` + +3. Compile libschrift +```sh +cd lib/schrift.c3l +make +cd ../.. +``` + +4. Build and run the project +```sh +make && build/ugui +``` \ No newline at end of file diff --git a/docs/.gitkeep b/docs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/schrift.c3l b/lib/schrift.c3l new file mode 160000 index 0000000..64d168f --- /dev/null +++ b/lib/schrift.c3l @@ -0,0 +1 @@ +Subproject commit 64d168f53b0fe70bda77e2ed7bec35f3f0f1af78 diff --git a/lib/sdl3.c3l b/lib/sdl3.c3l new file mode 160000 index 0000000..e7356df --- /dev/null +++ b/lib/sdl3.c3l @@ -0,0 +1 @@ +Subproject commit e7356df5d1d0c22a6bba822bda6994062b0b75d7 diff --git a/lib/ugui.c3l b/lib/ugui.c3l new file mode 160000 index 0000000..4f7fa7d --- /dev/null +++ b/lib/ugui.c3l @@ -0,0 +1 @@ +Subproject commit 4f7fa7d50c16db3acac020cdd7204e28d42efdf2 diff --git a/lib/ugui_sdl.c3l b/lib/ugui_sdl.c3l new file mode 160000 index 0000000..050624f --- /dev/null +++ b/lib/ugui_sdl.c3l @@ -0,0 +1 @@ +Subproject commit 050624fd67c2d80190114c7774ccfa64b0f449b9 diff --git a/project.json b/project.json new file mode 100644 index 0000000..d4e9123 --- /dev/null +++ b/project.json @@ -0,0 +1,20 @@ +{ + "langrev": "1", + "warnings": ["no-unused"], + "dependency-search-paths": ["lib"], + "dependencies": ["sdl3", "ugui", "ugui_sdl3"], + "features": ["DEBUG_POINTER"], + "authors": ["Alessandro Mauri "], + "version": "0.1.0", + "sources": ["src/**"], + "output": "build", + "target": "linux-x64", + "targets": { + "ugui": { + "type": "executable" + } + }, + "safe": true, + "opt": "O1", + "debug-info": "full" +} diff --git a/resources/.gitkeep b/resources/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/resources/hack-nerd.ttf b/resources/hack-nerd.ttf new file mode 100644 index 0000000..f397f20 Binary files /dev/null and b/resources/hack-nerd.ttf differ diff --git a/resources/shaders/Makefile b/resources/shaders/Makefile new file mode 100644 index 0000000..cdc4a01 --- /dev/null +++ b/resources/shaders/Makefile @@ -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 diff --git a/resources/shaders/source/ugui.frag.glsl b/resources/shaders/source/ugui.frag.glsl new file mode 100644 index 0000000..2618c2e --- /dev/null +++ b/resources/shaders/source/ugui.frag.glsl @@ -0,0 +1,118 @@ +#version 450 + +/* Combined fragment shader to render UGUI commands */ + +// type values, these are the same as in renderer.c3 +const uint TYPE_RECT = 0; +const uint TYPE_FONT = 1; +const uint TYPE_SPRITE = 2; +const uint TYPE_MSDF = 3; + +// viewport size +layout(set = 3, binding = 0) uniform Viewport { + ivec2 view; +}; + +// textures +layout(set = 2, binding = 0) uniform sampler2D font_atlas; +layout(set = 2, binding = 1) uniform sampler2D sprite_atlas; + +// inputs +layout(location = 0) in vec4 in_color; +layout(location = 1) in vec2 in_uv; +layout(location = 2) in vec4 in_quad_size; +layout(location = 3) in float in_radius; +layout(location = 4) flat in uint in_type; + +// outputs +layout(location = 0) out vec4 fragColor; + + +// SDF for a rounded rectangle given the centerpoint, half size and radius, all in pixels +float sdf_rr(vec2 p, vec2 half_size, float radius) { + vec2 q = abs(p) - half_size + radius; + return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - radius; +} + + +const float PX_RANGE = 4.0f; +float screen_px_range(vec2 uv, sampler2D tx) { + vec2 unit_range = vec2(PX_RANGE)/vec2(textureSize(tx, 0)); + vec2 texel_size = vec2(1.0)/fwidth(uv); + return max(0.5*dot(unit_range, texel_size), 1.0); +} + + +float median(float r, float g, float b) { + return max(min(r, g), min(max(r, g), b)); +} + + +// main for TYPE_RECT, draw a rouded rectangle with a SDF +void rect_main() +{ + vec2 centerpoint = in_quad_size.xy + in_quad_size.zw * 0.5; + vec2 half_size = in_quad_size.zw * 0.5; + float distance = sdf_rr(vec2(gl_FragCoord) - centerpoint, half_size, in_radius); + float alpha = 1.0 - smoothstep(0.0, 1.5, distance); + fragColor = vec4(in_color.rgb, in_color.a * alpha); +} + + +// main for TYPE_SPRITE, draws a sprite sampled from an atlas +void sprite_main() +{ + ivec2 ts = textureSize(sprite_atlas, 0); + vec2 fts = vec2(ts); + vec2 real_uv = in_uv.xy / fts; + fragColor = texture(sprite_atlas, real_uv); +} + + +// main for TYPE_FONT, draws a character sampled from an atlas that contains only the alpha channel +void font_main() +{ + ivec2 ts = textureSize(font_atlas, 0); + vec2 fts = vec2(ts); + vec2 real_uv = in_uv.xy / fts; + + vec4 opacity = texture(font_atlas, real_uv); + fragColor = vec4(in_color.rgb, in_color.a*opacity.r); +} + + +// main for TYPE_MSDF, draws a sprite that is stored as a multi-channel SDF +void msdf_main() { + ivec2 ts = textureSize(sprite_atlas, 0); + vec2 fts = vec2(ts); + vec2 real_uv = in_uv.xy / fts; + + vec3 msd = texture(sprite_atlas, real_uv).rgb; + float sd = median(msd.r, msd.g, msd.b); + float distance = screen_px_range(real_uv, sprite_atlas)*(sd - 0.5); + float opacity = clamp(distance + 0.5, 0.0, 1.0); + fragColor = in_color * opacity; +} + + +// shader main +void main() +{ + switch (in_type) { + case TYPE_RECT: + rect_main(); + break; + case TYPE_FONT: + font_main(); + break; + case TYPE_SPRITE: + sprite_main(); + break; + case TYPE_MSDF: + msdf_main(); + break; + default: + // ERROR, invalid type, return magenta + fragColor = vec4(1.0, 0.0, 1.0, 1.0); + } +} \ No newline at end of file diff --git a/resources/shaders/source/ugui.vert.glsl b/resources/shaders/source/ugui.vert.glsl new file mode 100644 index 0000000..ee29724 --- /dev/null +++ b/resources/shaders/source/ugui.vert.glsl @@ -0,0 +1,47 @@ +#version 450 + +/* Combined vertex shader to render UGUI commands */ + +// Viewport size in pixels +layout(set = 1, binding = 0) uniform Viewport { + ivec2 view; +}; + +// inputs +layout(location = 0) in ivec2 in_position; +layout(location = 1) in ivec4 in_attr; // quad x,y,w,h +layout(location = 2) in ivec4 in_uv; +layout(location = 3) in uvec4 in_color; +layout(location = 4) in uint in_type; + +// outputs +layout(location = 0) out vec4 out_color; +layout(location = 1) out vec2 out_uv; +layout(location = 2) out vec4 out_quad_size; +layout(location = 3) out float out_radius; +layout(location = 4) out uint out_type; + + +void main() +{ + // vertex position + ivec2 px_pos = in_attr.xy + in_position.xy * in_attr.zw; + vec2 clip_pos; + clip_pos.x = float(px_pos.x)*2.0 / view.x - 1.0; + clip_pos.y = -(float(px_pos.y)*2.0 / view.y - 1.0); + gl_Position = vec4(clip_pos, 0.0, 1.0); + + // color output + out_color = vec4(in_color) / 255.0; + + // uv output. only useful if the type is SPRITE + vec2 px_uv = in_uv.xy + in_position.xy * in_uv.zw; + out_uv = vec2(px_uv); + + // quad size and radius output, only useful if type is RECT + out_quad_size = vec4(in_attr); + out_radius = float(abs(in_uv.x)); + + // type output + out_type = in_type; +} \ No newline at end of file diff --git a/resources/style.css b/resources/style.css new file mode 100644 index 0000000..c043b0b --- /dev/null +++ b/resources/style.css @@ -0,0 +1,86 @@ +div { + bg: #282828; + fg: #fbf1c7ff; + primary: #cc241dff; + secondary: #6c19ca8f; + accent: #fabd2fff; + border: 1; + padding: 0; + margin: 0; + radius: 0; +} + +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: 20; + bg: #3c3836ff; + fg: #fbf1c7ff; + primary: #cc241dff; + secondary: #458588ff; + accent: #fabd2fff; +} + +toggle { + margin: 2; + border: 2; + padding: 1; + radius: 10; + size: 20; + 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; +} + +scrollbar { + padding: 2; + size: 8; + bg: #45858842; + fg: #fbf1c7ff; + primary: #cc241dff; + secondary: #458588ff; + accent: #fabd2fff; +} + + +text-box { + bg: #4a4543ff; + fg: #fbf1c7ff; + primary: #cc241dff; + secondary: #458588ff; + accent: #8f3f61a8; + border: 1; + padding: 4; + margin: 2; +} diff --git a/resources/tick_sdf.qoi b/resources/tick_sdf.qoi new file mode 100644 index 0000000..51a8f81 Binary files /dev/null and b/resources/tick_sdf.qoi differ diff --git a/resources/tux.qoi b/resources/tux.qoi new file mode 100644 index 0000000..57d6333 Binary files /dev/null and b/resources/tux.qoi differ diff --git a/resources/tux_inv.qoi b/resources/tux_inv.qoi new file mode 100644 index 0000000..947fb28 Binary files /dev/null and b/resources/tux_inv.qoi differ diff --git a/scripts/.gitkeep b/scripts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/.gitkeep b/src/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/main.c3 b/src/main.c3 new file mode 100644 index 0000000..7489ef5 --- /dev/null +++ b/src/main.c3 @@ -0,0 +1,420 @@ +import std::io; +import ugui; +import std::time; +import std::collections::ringbuffer; +import std::core::string; +import std::core::mem::allocator; +import ugui::sdl::ren; + + +alias Times = ringbuffer::RingBuffer{time::NanoDuration[128]}; + +fn void Times.print_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); + + 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[*] VS_PATH = "resources/shaders/compiled/ugui.vert.spv"; +const char[*] FS_PATH = "resources/shaders/compiled/ugui.frag.spv"; + +const char[*] STYLESHEET_PATH = "resources/style.css"; + +const bool LIMIT_FPS = true; +const bool VSYNC = true; + +fn int main(String[] args) +{ + ArenaAllocator arena; + char[] mem = mem::new_array(char, 1024*1024); + defer (void)mem::free(mem); + arena.init(mem); + + ugui::Ctx ui; + ui.init(&arena)!!; + defer ui.free(); + + ren::Renderer ren; + ren.init("Ugui Test", 800, 600, VSYNC); + defer ren.free(); + ui.input_window_size(800, 600)!!; + + // ========================================================================================== // + // FONT LOADING // + // ========================================================================================== // + // import font in the ui context + ui.load_font(&arena, "font1", "resources/hack-nerd.ttf", 16)!!; + + // set the renderer's font atlas + ren.font_atlas_id = ui.get_font_id("font1"); + + // 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)!!; + + // set the renderer's sprite atlas + ren.sprite_atlas_id = ui.get_sprite_atlas_id("icons"); + + // upload the atlas to the gpu + Atlas atlas = ui.sprite_atlas.atlas; + ren.new_texture("icons", FULL_COLOR, atlas.buffer, atlas.width, atlas.height); + + // ========================================================================================== // + // PIPELINE SETUP // + // ========================================================================================== // + ren.load_spirv_shader_from_file("UGUI_PIPELINE", VS_PATH, FS_PATH, 2, 0); + ren.create_pipeline("UGUI_PIPELINE", RECT); + + // ========================================================================================== // + // CSS INPUT // + // ========================================================================================== // + io::printfn("imported %d styles", ui.import_style_from_file(STYLESHEET_PATH)); + + // ========================================================================================== // + // OTHER VARIABLES // + // ========================================================================================== // + TextEdit te; + te.buffer = mem::new_array(char, 256); + defer mem::free(te.buffer); + + isz frame; + double fps; + time::Clock clock; + time::Clock fps_clock; + time::Clock sleep_clock; + Times ui_times; + Times draw_times; + + // ========================================================================================== // + // MAIN LOOP // + // ========================================================================================== // + ren::pre(ren.win); + + bool quit; + while (!quit) { + clock.mark(); + fps_clock.mark(); + sleep_clock.mark(); + + quit = ui.handle_events()!!; + + /* End Input Handling */ + + /* Start UI Handling */ + ui.frame_begin()!!; + + if (ui.check_key_combo(ugui::KMOD_CTRL, "q")) quit = true; + +const String APPLICATION = "calculator"; +$switch APPLICATION: +$case "debug": + debug_app(&ui); +$case "calculator": + calculator(&ui, &te); +$case "popup": + popup(&ui); +$endswitch + + // Timings counter + TimeStats dts = draw_times.get_stats(); + TimeStats uts = ui_times.get_stats(); + + ui.@div(ugui::@fit(), ugui::@fit(), COLUMN, TOP_LEFT, absolute: true, off: {10, 10}) { + ui.text(string::tformat("frame %d, fps = %.2f", frame, fps))!!; + ui.text(string::tformat("ui avg: %s\ndraw avg: %s\nTOT: %s", uts.avg, dts.avg, uts.avg+dts.avg))!!; + }!!; + + ui.frame_end()!!; + /* End UI Handling */ + ui_times.push(clock.mark()); + //ui_times.print_stats(); + + /* Start UI Drawing */ + ren.begin_render(true); + + ren.render_ugui(&ui.cmd_queue); + + ren.end_render(); + + draw_times.push(clock.mark()); + //draw_times.print_stats(); + /* End Drawing */ + + // wait for the next event, timeout after 100ms + int timeout = LIMIT_FPS ? (int)(100.0-sleep_clock.mark().to_ms()-0.5) : 0; + if (ui.skip_frame) timeout = 0; + ren::wait_events(timeout); + + fps = 1.0 / fps_clock.mark().to_sec(); + frame++; + } + + return 0; +} + +fn void debug_app(ugui::Ctx* ui) +{ + static LayoutDirection d = ROW; + ui.@div(ugui::@grow(), ugui::@grow(), anchor: CENTER) { + ui.@div(ugui::@exact(300), ugui::@exact(300), d, scroll_x: true, scroll_y: true) { + if (ui.button("one")!!.mouse_release) d = COLUMN; + if (ui.button("two")!!.mouse_release) d = ROW; + ui.button("three")!!; + ui.button("four")!!; + ui.button("five")!!; + ui.button("six")!!; + ui.button("seven")!!; + ui.button("eight")!!; + ui.button("nine")!!; + ui.button("ten")!!; + }!!; + }!!; +} + +/* +fn void debug_app(ugui::Ctx* ui) +{ + static bool toggle; + ui.div_begin({.w=-100})!!; + { + ui.layout_set_column()!!; + if (ui.button(icon:"tux")!!.mouse_press) { + io::printn("press button0"); + toggle = !toggle; + } + //ui.layout_next_column()!!; + if (ui.button(label: "ciao", icon: "tick")!!.mouse_press) { + io::printn("press button1"); + } + //ui.layout_next_column()!!; + if (ui.button()!!.mouse_release) { + io::printn("release button2"); + } + + ui.layout_set_row()!!; + ui.layout_next_row()!!; + static float rf, gf, bf, af; + ui.slider_ver({0,0,30,100}, &rf)!!; + ui.slider_ver({0,0,30,100}, &gf)!!; + ui.slider_ver({0,0,30,100}, &bf)!!; + ui.slider_ver({0,0,30,100}, &af)!!; + + ui.layout_next_column()!!; + ui.text_unbounded("Ciao Mamma\nAbilità ⚡\n'\udb80\udd2c'")!!; + + ui.layout_next_column()!!; + ui.button("Continua!")!!; + + ui.layout_next_row()!!; + static bool check; + ui.checkbox("", {}, &check, "tick")!!; + ui.checkbox("", {}, &check)!!; + ui.toggle("", {}, &toggle)!!; + + ui.sprite("tux")!!; + static char[128] text_box = "ciao mamma"; + static usz text_len = "ciao mamma".len; + ui.text_box({0,0,200,200}, text_box[..], &text_len)!!; + }; + ui.div_end()!!; + + ui.div_begin(ugui::DIV_FILL, scroll_x: true, scroll_y: true)!!; + { + ui.layout_set_column()!!; + static float slider2 = 0.5; + if (ui.slider_ver({0,0,30,100}, &slider2)!!.update) { + io::printfn("other slider: %f", slider2); + } + ui.button()!!; + ui.button()!!; + ui.button()!!; + ui.button()!!; + if (toggle) { + ui.button()!!; + ui.button()!!; + ui.button()!!; + ui.button()!!; + } + ui.layout_next_column()!!; + ui.layout_set_row()!!; + static float f1, f2; + ui.slider_hor({0,0,100,30}, &f1)!!; + ui.slider_hor({0,0,100,30}, &f2)!!; + }; + ui.div_end()!!; +} +*/ + +import std::os::process; + +fn void calculator(ugui::Ctx* ui, TextEdit* te) +{ + static char[128] buffer; + static usz len; + bool eval; + + // keyboard input + switch(ui.get_keys()) { + case "+": nextcase; + case "-": nextcase; + case "*": nextcase; + case "/": nextcase; + case "(": nextcase; + case ")": nextcase; + case ".": nextcase; + case "0": nextcase; + case "1": nextcase; + case "2": nextcase; + case "3": nextcase; + case "4": nextcase; + case "5": nextcase; + case "6": nextcase; + case "7": nextcase; + case "8": nextcase; + case "9": + buffer[len++] = ui.get_keys()[0]; + case "\n": + eval = len != 0; + case "c": + len = 0; + case "d": + if (len > 0) len--; + } + + static bool toggle; + static ugui::Point pos; + if (ui.is_mouse_released(ugui::BTN_RIGHT)) { + pos = ui.input.mouse.pos; + toggle = !toggle; + } + if (toggle) { + toggle = ui.popup_begin(pos, ugui::@fit(50), ugui::@fit(100), COLUMN)!!; + ui.button("Uno")!!; + ui.button("Due")!!; + ui.button("Tre")!!; + ui.button("Quattro")!!; + ui.div_end()!!; + } + + // ui input/output + ui.@div(ugui::@grow(), ugui::@grow(), ROW, CENTER) { // center everything on the screen + ui.@div(ugui::@fit(), ugui::@fit(), COLUMN, TOP_LEFT) { + + ui.@div(ugui::@grow(), ugui::@fit(), ROW, CENTER) {ui.text("SHITTY AHH CALCULATOR")!!;}!!; + ui.@div(ugui::@grow(), ugui::@exact(100), ROW, RIGHT) { + ui.text((String)buffer[:len])!!; + }!!; + ui.@row() { + ui.@column() { + ui.button("7")!!.mouse_press ? buffer[len++] = '7' : 0; + ui.button("4")!!.mouse_press ? buffer[len++] = '4' : 0; + ui.button("1")!!.mouse_press ? buffer[len++] = '1' : 0; + ui.button("0")!!.mouse_press ? buffer[len++] = '0' : 0; + }!!; + ui.@column() { + ui.button("8")!!.mouse_press ? buffer[len++] = '8' : 0; + ui.button("5")!!.mouse_press ? buffer[len++] = '5' : 0; + ui.button("2")!!.mouse_press ? buffer[len++] = '2' : 0; + ui.button(".")!!.mouse_press ? buffer[len++] = '.' : 0; + }!!; + ui.@column() { + ui.button("9")!!.mouse_press ? buffer[len++] = '9' : 0; + ui.button("6")!!.mouse_press ? buffer[len++] = '6' : 0; + ui.button("3")!!.mouse_press ? buffer[len++] = '3' : 0; + ui.button("(")!!.mouse_press ? buffer[len++] = '(' : 0; + }!!; + ui.@div(ugui::@exact(10), ugui::@exact(10)) {}!!; + ui.@column() { + ui.button("x")!!.mouse_press ? buffer[len++] = '*' : 0; + ui.button("/")!!.mouse_press ? buffer[len++] = '/' : 0; + ui.button("+")!!.mouse_press ? buffer[len++] = '+' : 0; + ui.button(")")!!.mouse_press ? buffer[len++] = ')' : 0; + }!!; + ui.@column() { + ui.button("C")!!.mouse_press ? len = 0 : 0; + ui.button("D")!!.mouse_press ? len > 0 ? len-- : 0 : 0; + ui.button("-")!!.mouse_press ? buffer[len++] = '-' : 0; + + // eval the expression with 'bc' + if (ui.button("=")!!.mouse_press || eval) { + char[128] out; + String y = string::tformat("echo '%s' | bc", (String)buffer[:len]); + String x = process::execute_stdout_to_buffer(out[:128], (String[]){"sh", "-c", y}) ?? ""; + buffer[:x.len] = x[..]; + len = x.len; + } + }!!; + }!!; + ui.@div(ugui::@grow(), ugui::@fit(), anchor: CENTER) { + static bool state; + ui.checkbox("boolean", &state, "tick")!!; + ui.sprite("tux", 32)!!; + ui.toggle("lmao", &state)!!; + }!!; + ui.@div(ugui::@grow(), ugui::@exact(50), anchor: CENTER, scroll_y: true) { + static float f; + ui.slider_hor(ugui::@exact(100), ugui::@exact(20), &f)!!; + ui.slider_ver(ugui::@exact(20), ugui::@exact(100), &f)!!; + }!!; + ui.@div(ugui::@grow(), ugui::@fit(), anchor: CENTER) { + ui.text_box(ugui::@grow(), ugui::@exact(100), te, TOP_LEFT)!!; + }!!; + }!!; }!!; +} + +fn void popup(ugui::Ctx* ui) +{ + static bool toggle; + static ugui::Point pos; + + if (toggle) { + toggle = ui.popup_begin(pos, ugui::@fit(), ugui::@fit(), COLUMN)!!; + ui.button("POP")!!; + ui.button("UP")!!; + ui.div_end()!!; + } + ui.@center(COLUMN) { + if (ui.button("ciao")!!.mouse_release) { + pos = ui.input.mouse.pos; + toggle = ~toggle; + } + if (ui.button("mamma")!!.mouse_press) ugui::println("pressed!"); + }!!; +} diff --git a/test/test_bitsruct.c3 b/test/test_bitsruct.c3 new file mode 100644 index 0000000..06d1722 --- /dev/null +++ b/test/test_bitsruct.c3 @@ -0,0 +1,14 @@ +bitstruct Bits : uint { + bool a : 0; + bool b : 1; + bool c : 2; +} + +fn int main() +{ + Bits a = {false, true, false}; + Bits b = {true, true, false}; + Bits c = a | b; + + return 0; +} diff --git a/test/test_bittype.c3 b/test/test_bittype.c3 new file mode 100644 index 0000000..e9802c9 --- /dev/null +++ b/test/test_bittype.c3 @@ -0,0 +1,10 @@ +import std::io; +import std::collections::bitset; + +def Bits = bitset::BitSet(<128>); + +fn void main() +{ + Bits b; + io::printn($typeof(b.data[0]).sizeof); +} diff --git a/test/test_color.c3 b/test/test_color.c3 new file mode 100644 index 0000000..08a3245 --- /dev/null +++ b/test/test_color.c3 @@ -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()); +} diff --git a/test/test_custom_hash.c3 b/test/test_custom_hash.c3 new file mode 100644 index 0000000..fc90f11 --- /dev/null +++ b/test/test_custom_hash.c3 @@ -0,0 +1,14 @@ +import std::collections::map; + +def Codepoint = uint; +fn uint Codepoint.hash(Codepoint code) => code < 128 ? code : ((uint)code).hash(); +def CodeMap = map::HashMap(); + + +fn int main() +{ + CodeMap m; + m.new_init(); + m.free(); + return 0; +} diff --git a/test/test_error.c3 b/test/test_error.c3 new file mode 100644 index 0000000..051c928 --- /dev/null +++ b/test/test_error.c3 @@ -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); +} diff --git a/test/test_font.c3 b/test/test_font.c3 new file mode 100644 index 0000000..db6861d --- /dev/null +++ b/test/test_font.c3 @@ -0,0 +1,6 @@ +import rl; + +fn int main(void) +{ + return 0; +} diff --git a/test/test_idgen.c3 b/test/test_idgen.c3 new file mode 100644 index 0000000..d4709d5 --- /dev/null +++ b/test/test_idgen.c3 @@ -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; +} \ No newline at end of file diff --git a/test/test_keyboard.c3 b/test/test_keyboard.c3 new file mode 100644 index 0000000..c9ab167 --- /dev/null +++ b/test/test_keyboard.c3 @@ -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; +} + diff --git a/test/test_mtree.c3 b/test/test_mtree.c3 new file mode 100644 index 0000000..f5df756 --- /dev/null +++ b/test/test_mtree.c3 @@ -0,0 +1,342 @@ +module mtree{Type}; + +import std::core::mem; +import std::core::mem::allocator; +import std::io; +import std::bits; +import std::collections::list; + + +alias Bitmap = ulong; +const BITS = Bitmap.sizeof*8; + +alias IdxList = list::List{int}; + +// more: if positive it contains the index of the next node that contains the children information +struct Node { + int more; + int parent; + Bitmap children; +} + +struct MTree { + usz elements; + Allocator allocator; + IdxList queue; + Bitmap[] used; + Type[] elem_mat; // element matrix + Node[] refs_mat; // relationship matrix +} + + +fn void MTree.init(&tree, usz size, Allocator allocator = mem) +{ + // round size to the nearest multiple of BITS + size = size + size%BITS; + + tree.elements = 0; + tree.allocator = allocator; + tree.queue.init(allocator, size); + tree.used = allocator::new_array(tree.allocator, Bitmap, size/BITS); + tree.elem_mat = allocator::new_array(tree.allocator, Type, size); + tree.refs_mat = allocator::new_array(tree.allocator, Node, size); + + foreach (&r: tree.refs_mat) { + r.more = -1; + } +} + + +fn void MTree.free(&tree) +{ + tree.elements = 0; + tree.queue.free(); + (void)allocator::free(tree.allocator, tree.used); + (void)allocator::free(tree.allocator, tree.elem_mat); + (void)allocator::free(tree.allocator, tree.refs_mat); +} + + +fn int MTree.get_free_spot(&tree) +{ + foreach (idx, d: tree.used) { + if (d != $typeof(d).max) { + int spot = (int)idx*BITS + BITS-(int)d.clz(); + return spot; + } + } + unreachable("no free spots left"); +} + +<* @require idx >= 0 *> +fn void MTree.set_used(&tree, int idx) +{ + int r = idx % BITS; + int q = idx / BITS; + tree.used[q] |= (1l << r); +} + +<* @require idx >= 0 *> +fn void MTree.unset_used(&tree, int idx) +{ + int r = idx % BITS; + int q = idx / BITS; + tree.used[q] &= ~(1l << r); +} + +<* @require idx >= 0 *> +fn bool MTree.is_used(&tree, int idx) +{ + int r = idx % BITS; + int q = idx / BITS; + return !!(tree.used[q] & (1l << r)); +} + + +// get the last node in the "more" chain +<* @require tree.is_used(parent) == true *> +fn int MTree.last_node(&tree, int parent) +{ + while(tree.refs_mat[parent].more >= 0) { + parent = tree.refs_mat[parent].more; + } + return parent; +} + + +<* @require tree.elements == 0 || tree.is_used(parent) == true *> +fn int MTree.add(&tree, int parent, Type t) +{ + int idx = tree.get_free_spot(); + int subtree = idx / BITS; + + tree.set_used(idx); + tree.elem_mat[idx] = t; + tree.refs_mat[idx] = (Node){ + .parent = parent, + .more = -1, + }; + + tree.elements++; + // root element, has no parent + if (tree.elements == 1) { + tree.refs_mat[idx].parent = -1; + return idx; + } + + + // if the parent already has a node in the same subtree as the child then update that node's + // children bitmap + bool done; + for (int p = parent; p >= 0; p = tree.refs_mat[p].more) { + int ps = p/BITS; + if (ps == subtree) { + tree.refs_mat[p].children |= (1l << (idx%BITS)); + done = true; + break; + } + } + // on fail we need to create another parent node + if (!done) { + int new_more = tree.get_free_spot(); + // if the new node does not land in the same subtree as the child we cannot do + // anything since the references are immutable + if (new_more/BITS != subtree) { + unreachable("cannot allocate new child for parent"); + } + tree.set_used(new_more); + tree.elements++; + // update the "more" chain + int last_link = tree.last_node(parent); + tree.refs_mat[last_link].more = new_more; + tree.refs_mat[new_more].more = -1; + tree.refs_mat[new_more].children |= (long)(1 << (idx%BITS)); + tree.refs_mat[new_more].parent = last_link; + // FIXME: the elem_mat is not updated, do we need to? + } + + return idx; +} + + +// get the index of the n-th children of parent, -1 otherwise +// usage: for (int i, c; (c = tree.children_it(parent, i)) >= 0; i++) { ... } +fn int MTree.children_it(&tree, int parent, int n) +{ + int tot_children; + int child; + for (int p = parent; p >= 0; p = tree.refs_mat[p].more) { + int cn = (int)tree.refs_mat[p].children.popcount(); + tot_children += cn; + + // we are in the right subtree + if (tot_children > n) { + child = (p/BITS) * BITS; // start at the parent's subtree index + int j = cn - (tot_children - n); // we need the j-th children of this node + Bitmap u = tree.refs_mat[p].children; + + child += j; // add the children number + do { + child += (int)u.ctz(); // increment by the skipped zeroes + u >>= u.ctz() + 1; + j--; + } while (j >= 0); + + return child; + } + } + return -1; +} + +fn int MTree.children_num(&tree, int parent) +{ + int n; + for (int p = parent; p >= 0; p = tree.refs_mat[p].more) { + n += (int)tree.refs_mat[p].children.popcount(); + } + return n; +} + +fn int MTree.subtree_size(&tree, int parent) +{ + int x = tree.children_num(parent); + int c; + for (int n; (c = tree.children_it(parent, n)) >= 0; n++) { + x += tree.subtree_size(c); + } + return x; +} + + +fn int MTree.level_order_it(&tree, int parent, int i) +{ + if (i == 0) { + tree.queue.clear(); + tree.queue.push(parent); + } + + if (tree.queue.len() == 0) return -1; + + int p = tree.queue.pop_first()!!; + int c; + for (int n; (c = tree.children_it(p, n)) >= 0; n++) { + tree.queue.push(c); + } + return p; +} + +fn void MTree.prune(&tree, int parent) +{ + int c; + for (int i = 0; (c = tree.children_it(parent, i)) >= 0; i++) { + tree.prune(c); // prune the subtree + + // delete all children including their more chain + for (int p = c; p >= 0;) { + int next = tree.refs_mat[p].more; + tree.unset_used(p); + tree.refs_mat[p] = {.more = -1}; + p = next; + } + + } + + // finally delete the parent + for (int p = parent; p >= 0;) { + int next = tree.refs_mat[p].more; + tree.unset_used(p); + tree.elements--; + tree.refs_mat[p] = {.more = -1}; + p = next; + } + +} + + +macro bool MTree.is_root(&t, int i) => t.refs_mat[i].parent == -1; + +fn void MTree.print(&tree) +{ + foreach (idx, c: tree.elem_mat) { + if (tree.is_used((int)idx)) { + io::printfn("[%d](%s) parent:%d more:%d children:%b", + idx, c, tree.refs_mat[idx].parent, tree.refs_mat[idx].more, + tree.refs_mat[idx].children + ); + } + } +} + + +module foo; + +import std::io; +import mtree; + +alias Tree = mtree::MTree{int}; + +fn int main() +{ + Tree t; + t.init(256); + defer t.free(); +/* + int root = t.add(0, 0); + int c1 = t.add(root, 1); + int c2 = t.add(root, 2); + + int c11 = t.add(c1, 11); + int c12 = t.add(c1, 12); + + int c3 = t.add(root, 3); + + for (int x = 0; x < 70; x++) { + t.add(c2, x); + } + + int c31 = t.add(c3, 31); + int c32 = t.add(c3, 32); + + int c4 = t.add(root, 4); + + int c13 = t.add(c1, 13); + int c14 = t.add(c1, 14); + int c15 = t.add(c1, 15); + + t.prune(c2); + + io::printn("printing tree"); + t.print(); + + usz x; + foreach_r (u: t.used) { + x += u.popcount(); + io::printf("%b ", u); + } + io::printfn("TOT:%d/%d",x,t.elements); + + io::printn(t.subtree_size(root)); + + io::printn(); +*/ + + int root = t.add(0, 0); + int c1 = t.add(root, 1); + int c2 = t.add(root, 2); + int c3 = t.add(root, 3); + + int c11 = t.add(c1, 11); + int c12 = t.add(c1, 12); + + int c111 = t.add(c11, 111); + int c121 = t.add(c12, 121); + + int c31 = t.add(c3, 31); + + int c; + for (int i; (c = t.level_order_it(root, i)) >= 0; i++) { + io::printfn("%d-th: [%d](%d)", i, c, t.elem_mat[c]); + } + + return 0; +} \ No newline at end of file diff --git a/test/test_renderer.c3 b/test/test_renderer.c3 new file mode 100644 index 0000000..38ead4c --- /dev/null +++ b/test/test_renderer.c3 @@ -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; +} \ No newline at end of file diff --git a/test/test_tree_layout.c3 b/test/test_tree_layout.c3 new file mode 100644 index 0000000..37d9920 --- /dev/null +++ b/test/test_tree_layout.c3 @@ -0,0 +1,436 @@ +import vtree; +import std::io; +import std::math; +import std::thread; + +const short WIDTH = 128; +const short HEIGHT = 64; + +struct Size { + short min, max; +} +macro Size @grow() => {.min = 0, .max = 0}; +macro Size @exact(short s) => {.min = s, .max = s}; +macro Size @fit(short min = 0, short max = short.max) => {.min = min, .max = max}; +macro bool Size.@is_grow(s) => (s.min == 0 && s.max == 0); +macro bool Size.@is_exact(s) => (s.min == s.max && s.min != 0); +macro bool Size.@is_fit(s) => (s.min != s.max); + + + +struct Rect { + short x, y, w, h; +} + +enum LayoutDirection { + ROW, + COLUMN +} + +enum ElemType { + DIV, + ELEM +} + +enum Anchor { + TOP_LEFT, + LEFT, + BOTTOM_LEFT, + BOTTOM, + BOTTOM_RIGHT, + RIGHT, + TOP_RIGHT, + TOP, + CENTER +} + +struct Elem { + ElemType type; + Size w, h; + Rect bounds; + Size ch_w, ch_h; // children width / height + uint grow_children; // how many children want to grow, decreased once a child has grown + short orig_x, orig_y; + short occupied; // occupied space in the layout direction + LayoutDirection layout_dir; + Anchor anchor; +} + +alias ElemTree = vtree::VTree{Elem*}; + + +char[HEIGHT][WIDTH] screen; +fn void paint(Rect bounds, char c) +{ + for (short x = bounds.x; x < WIDTH && x < bounds.x + bounds.w; x++) { + for (short y = bounds.y; y < HEIGHT && y < bounds.y + bounds.h; y++) { + screen[x][y] = c; + } + } +} + +fn isz Elem.div_start(&e, ElemTree* tree, isz parent, Size w, Size h, LayoutDirection dir = ROW, Anchor anchor = TOP_LEFT, char c = ' ') +{ + e.type = DIV; + e.w = w; + e.h = h; + e.layout_dir = dir; + e.anchor = anchor; + + e.grow_children = 0; + e.occupied = 0; + e.ch_w = e.ch_h = {}; + e.orig_x = e.orig_y = 0; + + // update grow children if necessary + Elem* p = tree.get(parent) ?? &&{}; + if ((p.layout_dir == ROW && e.w.@is_grow()) || ((p.layout_dir == COLUMN && e.h.@is_grow()))) { + p.grow_children++; + } + + paint(e.bounds, c); + return tree.add(e, parent)!!; +} + +fn void update_parent_size(Elem* parent, Elem* child) +{ + // update the parent children size + switch (parent.layout_dir) { + case ROW: // on rows grow the ch width by the child width and only grow ch height if it exceeds + parent.ch_w.min += child.w.min; + parent.ch_w.max += child.w.max; + parent.ch_h.min = math::max(child.h.min, parent.ch_h.min); + parent.ch_h.max = math::max(child.h.max, parent.ch_h.max); + case COLUMN: // do the opposite on column + parent.ch_w.min = math::max(child.w.min, parent.ch_w.min); + parent.ch_w.max = math::max(child.w.max, parent.ch_w.max); + parent.ch_h.min += child.h.min; + parent.ch_h.max += child.h.max; + } +} + +fn isz Elem.div_end(&e, ElemTree* tree, isz node) +{ + isz parent = tree.parentof(node) ?? -1; + if (parent > 0) { + Elem* p = tree.get(parent)!!; + update_parent_size(p, e); + } + return parent; +} + +fn void resolve_dimensions(Elem* e, Elem* p) +{ + // ASSIGN WIDTH + switch { + case e.w.@is_exact(): + e.bounds.w = e.w.min; + case e.w.@is_grow(): + break; + // done in another pass + case e.w.@is_fit(): // fit the element's children + short min = math::max(e.ch_w.min, e.w.min); + short max = math::min(e.ch_w.max, e.w.max); + if (max >= min) { // OK! + e.bounds.w = max; + } else { + unreachable("cannot fit children"); + } + default: unreachable("width is not exact, grow or fit"); + } + + // ASSIGN HEIGHT + switch { + case e.h.@is_exact(): + e.bounds.h = e.h.min; + case e.h.@is_grow(): + break; + // done in another pass + case e.h.@is_fit(): // fit the element's children + short min = math::max(e.ch_h.min, e.h.min); + short max = math::min(e.ch_h.max, e.h.max); + if (max >= min) { // OK! + e.bounds.h = max; + } else { + unreachable("cannot fit children"); + } + default: unreachable("width is not exact, grow or fit"); + } + + switch (p.layout_dir) { + case ROW: + if (!e.w.@is_grow()) p.occupied += e.bounds.w; + case COLUMN: + if (!e.h.@is_grow()) p.occupied += e.bounds.h; + } +} + +fn void resolve_grow_elements(Elem* e, Elem* p) +{ + // WIDTH + if (e.w.@is_grow()) { + if (p.layout_dir == ROW) { // grow along the axis, divide the parent size + e.bounds.w = (short)((int)(p.bounds.w - p.occupied) / (int)p.grow_children); + p.grow_children--; + p.occupied += e.bounds.w; + } else if (p.layout_dir == COLUMN) { // grow across the layout axis, inherit width of the parent + e.bounds.w = p.bounds.w; + } + } + + // HEIGHT + if (e.h.@is_grow()) { + if (p.layout_dir == COLUMN) { // grow along the axis, divide the parent size + e.bounds.h = (short)((int)(p.bounds.h - p.occupied) / (int)p.grow_children); + p.grow_children--; + p.occupied += e.bounds.h; + } else if (p.layout_dir == ROW) { // grow across the layout axis, inherit width of the parent + e.bounds.h = p.bounds.h; + } + } +} + +fn void resolve_placement(Elem* e, Elem* p) +{ + switch (p.anchor) { + case TOP_LEFT: + e.bounds.x = p.bounds.x + p.orig_x; + e.bounds.y = p.bounds.y + p.orig_y; + case LEFT: + e.bounds.x = p.bounds.x + p.orig_x; + e.bounds.y = p.bounds.y + p.orig_y + p.bounds.h/2; + if (p.layout_dir == COLUMN) { + e.bounds.y -= p.occupied/2; + } else if (p.layout_dir == ROW) { + e.bounds.y -= e.bounds.h/2; + } + case BOTTOM_LEFT: + e.bounds.x = p.bounds.x + p.orig_x; + e.bounds.y = p.bounds.y + p.bounds.h + p.orig_y; + if (p.layout_dir == COLUMN) { + e.bounds.y -= p.occupied; + } else if (p.layout_dir == ROW) { + e.bounds.y -= e.bounds.h; + } + case BOTTOM: + e.bounds.x = p.bounds.x + p.orig_x + p.bounds.w/2; + e.bounds.y = p.bounds.y + p.bounds.h + p.orig_y; + if (p.layout_dir == COLUMN) { + e.bounds.y -= p.occupied; + e.bounds.x -= e.bounds.w/2; + } else if (p.layout_dir == ROW) { + e.bounds.y -= e.bounds.h; + e.bounds.x -= p.occupied/2; + } + case BOTTOM_RIGHT: + e.bounds.x = p.bounds.x + p.orig_x + p.bounds.w; + e.bounds.y = p.bounds.y + p.bounds.h + p.orig_y; + if (p.layout_dir == COLUMN) { + e.bounds.y -= p.occupied; + e.bounds.x -= e.bounds.w; + } else if (p.layout_dir == ROW) { + e.bounds.y -= e.bounds.h; + e.bounds.x -= p.occupied; + } + case RIGHT: + e.bounds.x = p.bounds.x + p.orig_x + p.bounds.w; + e.bounds.y = p.bounds.y + p.orig_y + p.bounds.h/2; + if (p.layout_dir == COLUMN) { + e.bounds.y -= p.occupied/2; + e.bounds.x -= e.bounds.w; + } else if (p.layout_dir == ROW) { + e.bounds.y -= e.bounds.h/2; + e.bounds.x -= p.occupied; + } + case TOP_RIGHT: + e.bounds.x = p.bounds.x + p.orig_x + p.bounds.w; + e.bounds.y = p.bounds.y + p.orig_y; + if (p.layout_dir == COLUMN) { + e.bounds.x -= e.bounds.w; + } else if (p.layout_dir == ROW) { + e.bounds.x -= p.occupied; + } + case TOP: + e.bounds.x = p.bounds.x + p.orig_x + p.bounds.w/2; + e.bounds.y = p.bounds.y + p.orig_y; + if (p.layout_dir == COLUMN) { + e.bounds.x -= e.bounds.w/2; + } else if (p.layout_dir == ROW) { + e.bounds.x -= p.occupied/2; + } + case CENTER: + e.bounds.x = p.bounds.x + p.orig_x + p.bounds.w/2; + e.bounds.y = p.bounds.y + p.orig_y + p.bounds.h/2; + if (p.layout_dir == COLUMN) { + e.bounds.x -= e.bounds.w/2; + e.bounds.y -= p.occupied/2; + } else if (p.layout_dir == ROW) { + e.bounds.x -= p.occupied/2; + e.bounds.y -= e.bounds.h/2; + } + break; + } + +/* + e.bounds.x = p.bounds.x + p.orig_x; + e.bounds.y = p.bounds.y + p.orig_y; +*/ + + switch (p.layout_dir) { + case ROW: + p.orig_x += e.bounds.w; + case COLUMN: + p.orig_y += e.bounds.h; + default: unreachable("unknown layout direction"); + } +} + +fn void frame_end(ElemTree* tree, isz root) +{ + // assign the element bounds + isz cursor = -1; + + /* + // RESOLVE DIMENSIONS + isz current = tree.level_order_it(root, &cursor)!!; + for (; current >= 0; current = tree.level_order_it(root, &cursor)!!) { + Elem* e = tree.get(current)!!; + isz pi = tree.parentof(current)!!; + Elem* p = (pi != current) ? tree.get(pi) ?? &&{} : &&{}; + resolve_dimensions(e, p); + } + + // RESOLVE GROW ELEMENTS + cursor = -1; + current = tree.level_order_it(root, &cursor)!!; + for (; current >= 0; current = tree.level_order_it(root, &cursor)!!) { + Elem* e = tree.get(current)!!; + isz pi = tree.parentof(current)!!; if (ch == current) continue; + Elem* p = (pi != current) ? tree.get(pi) ?? &&{} : &&{}; + + resolve_grow_elements(e, p); + } + + + // RESOLVE PLACEMENT + cursor = -1; + current = tree.level_order_it(root, &cursor)!!; + for (; current >= 0; current = tree.level_order_it(root, &cursor)!!) { + Elem* e = tree.get(current)!!; + isz pi = tree.parentof(current)!!; + Elem* p = (pi != current) ? tree.get(pi) ?? &&{} : &&{}; + + resolve_placement(e, p); + } + */ + + cursor = -1; + isz current = tree.level_order_it(root, &cursor)!!; + for (; current >= 0; current = tree.level_order_it(root, &cursor)!!) { + Elem* p = tree.get(current)!!; + + // RESOLVE KNOWN DIMENSIONS + isz ch_cur = 0; + isz ch = tree.children_it(current, &ch_cur)!!; + for (; ch >= 0; ch = tree.children_it(current, &ch_cur)!!) { + Elem* c = tree.get(ch)!!; + if (tree.is_root(ch)!!) { + resolve_dimensions(p, &&{}); + } else { + resolve_dimensions(c, p); + } + } + + // RESOLVE GROW CHILDREN + ch_cur = 0; + ch = tree.children_it(current, &ch_cur)!!; + for (; ch >= 0; ch = tree.children_it(current, &ch_cur)!!) { + Elem* c = tree.get(ch)!!; + if (tree.is_root(ch)!!) { + resolve_grow_elements(p, &&{}); + } else { + resolve_grow_elements(c, p); + } + } + + // RESOLVE CHILDREN PLACEMENT + ch_cur = 0; + ch = tree.children_it(current, &ch_cur)!!; + for (; ch >= 0; ch = tree.children_it(current, &ch_cur)!!) { + Elem* c = tree.get(ch)!!; + if (tree.is_root(ch)!!) { + resolve_placement(p, &&{}); + } else { + resolve_placement(c, p); + } + } + } +} + +fn void main() +{ + ElemTree tree; + tree.init(64, mem)!!; + isz parent; + defer (void)tree.free(); + + Elem root; // root div + Elem div1, div2, div3, div4; + usz frame; + while (true) { + parent = root.div_start(&tree, parent, @exact(WIDTH), @exact(HEIGHT), ROW, anchor: RIGHT); + /* + { + parent = div1.div_start(&tree, parent, @grow(), @grow(), dir: ROW, c: '1'); + { + parent = div4.div_start(&tree, parent, @exact(30), @exact(30), dir: ROW, c: '4'); + parent = div4.div_end(&tree, parent); + } + parent = div1.div_end(&tree, parent); + + if (frame < 200) { + parent = div2.div_start(&tree, parent, @exact(20), @fit(), dir: COLUMN, c: '2'); + { + parent = div3.div_start(&tree, parent, @exact(10), @exact(10), dir: ROW, c: '3'); + parent = div3.div_end(&tree, parent); + } + parent = div2.div_end(&tree, parent); + } + } + */ + parent = div3.div_start(&tree, parent, @fit(), @fit(), COLUMN, anchor: CENTER); + { + parent = div1.div_start(&tree, parent, @exact(20), @exact(20), dir: ROW, c: '1'); + parent = div1.div_end(&tree, parent); + + parent = div2.div_start(&tree, parent, @exact(10), @exact(10), dir: ROW, c: '2'); + parent = div2.div_end(&tree, parent); + } + parent = div3.div_end(&tree, parent); + + parent = root.div_end(&tree, parent); + + frame_end(&tree, parent); + tree.nuke(); + + + // draw the screen + //io::print("\e[1;1H\e[2J"); + for (short x = 0; x < WIDTH+2; x++) io::printf("%c", x == 0 || x == WIDTH+1 ? '+' : '-'); + io::printn(); + for (short y = 0; y < HEIGHT; y++) { + io::print("|"); + for (short x = 0; x < WIDTH; x++) { + char c = screen[x][y] == 0 ? 'x' : screen[x][y]; + io::printf("%c", c); + } + io::print("|"); + io::printn(); + } + for (short x = 0; x < WIDTH+2; x++) io::printf("%c", x == 0 || x == WIDTH+1 ? '+' : '-'); + io::printn("\n\n"); + + thread::sleep_ms(10); + frame++; + } +} \ No newline at end of file diff --git a/test/test_union.c3 b/test/test_union.c3 new file mode 100644 index 0000000..df09af1 --- /dev/null +++ b/test/test_union.c3 @@ -0,0 +1,26 @@ + +struct CmdA { + int a, b; +} + +struct CmdB { + float a, b; +} + +union AnyCmd { + CmdA a; + CmdB b; +} + +struct Cmd { + int type; + AnyCmd cmd; +} + +fn int main() +{ + Cmd c; + c.type = 1; + c.cmd.a = {.a = 1, .b = 2}; + return 0; +} diff --git a/test/test_vtree.c3 b/test/test_vtree.c3 new file mode 100644 index 0000000..187ce91 --- /dev/null +++ b/test/test_vtree.c3 @@ -0,0 +1,7 @@ +import std::io; +import vtree; + +fn int main() +{ + return 0; +} diff --git a/test/ugui_font.c3 b/test/ugui_font.c3 new file mode 100644 index 0000000..ec1fc03 --- /dev/null +++ b/test/ugui_font.c3 @@ -0,0 +1,226 @@ +module ugui; + +import cache; +//#include +//#include + +//#include "stb_truetype.h" +//#include "stbimage_write.h" + +// unicode code point, different type for a different hash +def Codepoint = uint; + +/* width and height of a glyph contain the kering advance + * (u,v) + * +-------------*---+ - + * | ^ | | ^ + * | |oy | | | + * | v | | | + * | .ii. | | | + * | @@@@@@. |<->| | + * | V@Mio@@o |adv| |h + * | :i. V@V | | | + * | :oM@@M | | | + * | :@@@MM@M | | | + * | @@o o@M | | | + * |<->:@@. M@M | | | + * |ox @@@o@@@@ | | | + * | :M@@V:@@.| | v + * +-------------*---+ - + * |<------------->| + * w + */ +struct Glyph { + Codepoint code; + uint u, v; + ushort w, h, a, x, y; +} + +def GlyphCache = cache::Cache(); + +// identity map the ASCII range +fn uint Codepoint.hash(Codepoint code) => code < 128 ? code : ((uint)code).hash(); + +struct FontAtlas { + uint width, height; + char* atlas; + uint glyph_max_w, glyph_max_h; + int size; + int file_size; + char *file; + void *priv; +} + + +macro is_utf8(char c) => c & 0x80; +const uint BDEPTH = 1; +const uint BORDER = 4; + +// FIXME: as of now only monospaced fonts look decent since no +// kerning information is stored + +struct Priv @private { + stbtt_fontinfo stb; + float scale; + int baseline; + unsigned char *bitmap; + struct cache c; +} +//#define PRIV(x) ((struct priv *)x->priv) + + +struct font_atlas * font_init(void) +{ + struct font_atlas *p = emalloc(sizeof(struct font_atlas)); + memset(p, 0, sizeof(struct font_atlas)); + p->priv = emalloc(sizeof(struct priv)); + memset(p->priv, 0, sizeof(struct priv)); + PRIV(p)->c = cache_init(); + return p; +} + + +// loads a font into memory, storing all the ASCII characters in the atlas, each font +// atlas structure holds glyphs of a specific size in pixels +// NOTE: size includes ascend and descend (so 12 does not mean that 'A' is 12px tall) +int font_load(struct font_atlas *atlas, const char *path, int size) +{ + if (!atlas || !path) + return -1; + + int err; + + dump_file(path, &(atlas->file), &(atlas->file_size)); + + err = stbtt_InitFont(&(PRIV(atlas)->stb), (unsigned char *)atlas->file, 0); + if (err == 0) return -1; + + int ascent, descent, linegap, baseline; + int x0,y0,x1,y1; + float scale; + stbtt_GetFontVMetrics(&(PRIV(atlas)->stb), &ascent, &descent, &linegap); + stbtt_GetFontBoundingBox(&(PRIV(atlas)->stb), &x0, &y0, &x1, &y1); + scale = stbtt_ScaleForPixelHeight(&(PRIV(atlas)->stb), size); + baseline = scale * -y0; + atlas->glyph_max_w = (scale*x1) - (scale*x0); + atlas->glyph_max_h = (baseline+scale*y1) - (baseline+scale*y0); + atlas->atlas = emalloc(CACHE_SIZE*BDEPTH*atlas->glyph_max_w*atlas->glyph_max_h); + memset(atlas->atlas, 0, CACHE_SIZE*BDEPTH*atlas->glyph_max_w*atlas->glyph_max_h); + PRIV(atlas)->baseline = atlas->glyph_max_h - baseline; + PRIV(atlas)->scale = scale; + PRIV(atlas)->bitmap = emalloc(BDEPTH*atlas->glyph_max_w*atlas->glyph_max_h); + // FIXME: make this a square atlas + atlas->width = atlas->glyph_max_w*CACHE_SIZE/4; + atlas->height = atlas->glyph_max_h*4; + atlas->size = size; + + // preallocate all ascii characters + for (char c = ' '; c <= '~'; c++) { + if (!font_get_glyph_texture(atlas, c, NULL)) + return -1; + } + + return 0; +} + + +int font_free(struct font_atlas *atlas) +{ + efree(atlas->atlas); + efree(atlas->file); + efree(PRIV(atlas)->bitmap); + cache_free(&PRIV(atlas)->c); + efree(atlas->priv); + efree(atlas); + return 0; +} + + +// TODO: time and take the median of the time it takes to generate the cache and +// the time it takes to draw the glyph +const struct font_glyph * font_get_glyph_texture(struct font_atlas *atlas, unsigned int code, int *updated) +{ + int _u = 0; + if (!updated) updated = &_u; + + const struct font_glyph *r; + if ((r = cache_search(&PRIV(atlas)->c, code)) != NULL) { + *updated = 0; + return r; + } + + *updated = 1; + // generate the sdf and put it into the cache + // TODO: generate the whole block at once + int idx = stbtt_FindGlyphIndex(&PRIV(atlas)->stb, code); + int x0,y0,x1,y1,gw,gh,l,off_x,off_y,adv,base; + base = atlas->glyph_max_h - PRIV(atlas)->baseline; + stbtt_GetGlyphBitmapBoxSubpixel( + &PRIV(atlas)->stb, + idx, + PRIV(atlas)->scale, + PRIV(atlas)->scale, + 0,0, + &x0,&y0, + &x1, &y1); + gw = x1 - x0; + gh = y1 - y0; + stbtt_GetGlyphHMetrics(&PRIV(atlas)->stb, idx, &adv, &l); + adv *= PRIV(atlas)->scale; + off_x = PRIV(atlas)->scale*l; + off_y = atlas->glyph_max_h+y0; + stbtt_MakeGlyphBitmapSubpixel( + &PRIV(atlas)->stb, + PRIV(atlas)->bitmap, + atlas->glyph_max_w, + atlas->glyph_max_h, + atlas->glyph_max_w, + PRIV(atlas)->scale, + PRIV(atlas)->scale, + 0, 0, + idx); + + // TODO: bounds check usign atlas height + // TODO: clear spot area in the atlas before writing on it + unsigned int spot = cache_get_free_spot(&PRIV(atlas)->c); + unsigned int ty = ((atlas->glyph_max_w * spot) / atlas->width) * atlas->glyph_max_h; + unsigned int tx = (atlas->glyph_max_w * spot) % atlas->width; + unsigned int w = atlas->width; + + unsigned char *a = (void *)atlas->atlas; + + //printf("max:%d %d spot:%d : %d %d %d %d\n", atlas->glyph_max_w, atlas->glyph_max_h, spot, tx, ty, off_x, off_y); + + for (int y = 0; y < gh; y++) { + for (int x = 0; x < gw; x++) { + int c, r; + r = (ty+y)*w; + c = tx+x; + a[r+c] = PRIV(atlas)->bitmap[y*atlas->glyph_max_w+x]; + } + } + + struct font_glyph g = { + .codepoint = code, + .u = tx, + .v = ty, + .w = gw, + .h = gh, + .x = off_x, + .y = off_y-base, + .a = adv, + }; + return cache_insert_at(&PRIV(atlas)->c, &g, g.codepoint, spot); +} + + +void font_dump(const struct font_atlas *atlas, const char *path) +{ + stbi_write_png( + path, + atlas->width, + atlas->height, + BDEPTH, + atlas->atlas, + BDEPTH*atlas->width); +}