A lot of work

* moved all ugui code to lib/ugui.c3l and made it a library/module
* started work on a sdl3 renderer, with shaders etc
* added the new sdl3.c3l library as a dependency
* makefile is for the renderer
This commit is contained in:
Alessandro Mauri 2025-06-01 16:44:31 +02:00
parent 2380c7693c
commit 712ce50631
32 changed files with 838 additions and 1 deletions

View file

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

View file

@ -1,46 +0,0 @@
module fifo{Type};
import std::core::mem;
faultdef 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 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 EMPTY?;
}
Type *ret = &fifo.arr[fifo.out];
fifo.count--;
fifo.out = (fifo.out + 1) % fifo.arr.len;
return ret;
}

628
src/renderer.c3 Normal file
View file

@ -0,0 +1,628 @@
module sdlrenderer::ren;
// 2D renderer for ugui, based on SDL3 using the new GPU API
// TODO: use unreachable() instead of the combo eprintfn();exit(1); since it does the same thing
// but also adds a helpful stack trace
import std::io;
import std::core::mem;
import sdl3::sdl;
import libc;
import std::collections::list;
struct Shader {
sdl::GPUShader* frag;
sdl::GPUShader* vert;
uint id;
}
struct Pipeline {
sdl::GPUGraphicsPipeline* pipeline;
uint id;
}
struct Texture {
sdl::GPUTexture* texture;
sdl::GPUSampler* sampler;
ushort width, height;
uint id;
}
// gpu buffer that contains a single quad
struct QuadBuffer {
sdl::GPUBuffer* vert_buf;
sdl::GPUBuffer* idx_buf;
bool initialized;
}
alias ShaderList = List{Shader};
alias PipelineList = List{Pipeline};
alias TextureList = List{Texture};
struct Renderer {
sdl::Window* win;
sdl::GPUDevice* gpu;
QuadBuffer quad_buffer;
ShaderList shaders;
PipelineList pipelines;
TextureList textures;
}
// how each vertex is represented in the gpu
struct Vertex @packed {
struct pos {
float x, y;
}
struct uv {
float u, v;
}
struct col { // FIXME: this is shit
union {
char r, g, b, a;
char[4] arr;
uint u;
}
}
}
struct Quad @packed {
struct vertices {
Vertex v1,v2,v3,v4;
}
struct indices {
short i1,i2,i3,i4,i5,i6;
}
}
const int DEBUG = 1;
fn void Renderer.init(&self, ZString title)
{
// 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
sdl::set_hint(sdl::HINT_VIDEO_DRIVER, "x11");
$endif
sdl::set_hint(sdl::HINT_RENDER_GPU_DEBUG, "1");
// init subsystems
if (!sdl::init(INIT_VIDEO)) {
io::eprintfn("sdl error: %s", sdl::get_error());
libc::exit(1);
}
// create the window
self.win = sdl::create_window(title, 640, 480, WINDOW_RESIZABLE|WINDOW_VULKAN);
if (self.win == null) {
io::eprintfn("sdl error: %s", sdl::get_error());
libc::exit(1);
}
// get the gpu device handle
self.gpu = sdl::create_gpu_device(GPU_SHADERFORMAT_SPIRV, true, "vulkan");
if (self.gpu == null) {
io::eprintfn("failed to create gpu device: %s", sdl::get_error());
libc::exit(1);
}
if (!sdl::claim_window_for_gpu_device(self.gpu, self.win)) {
io::eprintfn("failed to claim window for use with gpu: %s", sdl::get_error());
libc::exit(1);
}
// initialize the quad buffer
self.quad_buffer.vert_buf = sdl::create_gpu_buffer(self.gpu,
&&(GPUBufferCreateInfo){.usage = GPU_BUFFERUSAGE_VERTEX, .size = Quad.vertices.sizeof}
);
if (self.quad_buffer.vert_buf == null) {
io::eprintfn("failed to initialize quad buffer (vertex): %s", sdl::get_error());
libc::exit(1);
}
self.quad_buffer.idx_buf = sdl::create_gpu_buffer(self.gpu,
&&(GPUBufferCreateInfo){.usage = GPU_BUFFERUSAGE_INDEX, .size = Quad.indices.sizeof}
);
if (self.quad_buffer.idx_buf == null) {
io::eprintfn("failed to initialize quad buffer (index): %s", sdl::get_error());
libc::exit(1);
}
self.quad_buffer.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();
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.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 = uniforms,
.num_storage_buffers = 0,
.num_storage_textures = 0
};
s.vert = sdl::create_gpu_shader(self.gpu, &shader_info);
if (s.vert == null) {
io::eprintfn("failed to create gpu vertex shader: %s", sdl::get_error());
libc::exit(1);
}
}
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 = 0,
.num_storage_buffers = 0,
.num_storage_textures = 0
};
s.frag = sdl::create_gpu_shader(self.gpu, &shader_info);
if (s.frag == null) {
io::eprintfn("failed to create gpu fragment shader: %s", sdl::get_error());
libc::exit(1);
}
}
// 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
if (vert_path != "") {
vert_code = mem::new_array(char, file::get_size(vert_path)!!+1);
file::load_buffer(vert_path, vert_code)!!;
}
// create fragment shader
if (frag_path != "") {
frag_code = mem::new_array(char, file::get_size(frag_path)!!+1);
file::load_buffer(frag_path, frag_code)!!;
}
self.load_spirv_shader_from_mem(name, vert_code, frag_code, textures, uniforms);
if (vert_code.ptr) mem::free(vert_code);
if (frag_code.ptr) mem::free(frag_code);
}
fn Shader* ShaderList.get_from_name(&self, String name)
{
uint id = name.hash();
foreach(&s: self) {
if (s.id == id) {
return s;
}
}
return null;
}
fn Pipeline* PipelineList.get_from_name(&self, String name)
{
uint id = name.hash();
foreach(&p: self) {
if (p.id == id) {
return p;
}
}
return null;
}
fn Texture* TextureList.get_from_name(&self, String name)
{
uint id = name.hash();
foreach(&t: self) {
if (t.id == id) {
return t;
}
}
return null;
}
// 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) {
io::eprintfn("error in creating pipeline: no shader named %s", shader_name);
libc::exit(1);
}
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[]){{
.slot = 0,
.pitch = Vertex.sizeof,
.input_rate = GPU_VERTEXINPUTRATE_VERTEX,
.instance_step_rate = 0,
}},
.num_vertex_buffers = 1,
// the description of each vertex, each vertex has three properties
.vertex_attributes = (GPUVertexAttribute[]){
{ // at location zero there is the position of the vertex
.location = 0,
.buffer_slot = 0, // only one buffer so always slot zero
.format = GPU_VERTEXELEMENTFORMAT_FLOAT2,
.offset = Vertex.pos.offsetof,
},
{ // at location one there are the uv coordinates
.location = 1,
.buffer_slot = 0,
.format = GPU_VERTEXELEMENTFORMAT_FLOAT2,
.offset = Vertex.uv.offsetof,
},
{ // at location two there is the color
.location = 2,
.buffer_slot = 0,
.format = GPU_VERTEXELEMENTFORMAT_UBYTE4, // 4x8bit unsigned rgba format
.offset = Vertex.col.offsetof,
}
},
.num_vertex_attributes = 3,
},
// 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) {
io::eprintfn("failed to create pipeline (shaders: %s, type: %s): %s", shader_name, type.nameof, sdl::get_error());
libc::exit(1);
}
self.pipelines.push(p);
}
enum TextureType : (GPUTextureFormat format) {
FULL_COLOR = GPU_TEXTUREFORMAT_R8G8B8A8_UINT,
JUST_ALPHA = GPU_TEXTUREFORMAT_R8_UINT
}
// 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(&self, String name, TextureType type, char[] pixels, ushort width, ushort height)
{
uint id = name.hash();
// 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 = 0, // no mip maps
// .sample_count not used since the texture is not a render target
};
GPUTexture* texture = sdl::create_gpu_texture(self.gpu, &tci);
if (texture == null) {
io::eprintfn("failed to create texture (name: %s, type: %s): %s", name, type.nameof, sdl::get_error());
libc::exit(1);
}
// 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) {
io::eprintfn("failed to create sampler (texture name: %s, type: %s): %s", name, type.nameof, sdl::get_error());
libc::exit(1);
}
Texture t = {
.id = id,
.texture = texture,
.sampler = sampler,
};
self.textures.push(t);
// upload the texture data
self.update_texture(name, pixels, width, height);
}
fn void Renderer.update_texture(&self, String name, char[] pixels, ushort width, ushort height, ushort x = 0, ushort y = 0)
{
Texture* t = self.textures.get_from_name(name);
if (t == null || t.texture == null) {
io::eprintf("failed updating texture: no texture named %s", name);
libc::exit(1);
}
GPUTexture* texture = t.texture;
// FIXME: do a better job at validating the copy
if (x > t.width || y > t.height) {
io::eprintf("failed updating texture: attempting to copy outside of the texture region", name);
libc::exit(1);
}
// upload image data
GPUCommandBuffer* cmdbuf = sdl::acquire_gpu_command_buffer(self.gpu);
if (cmdbuf == null) {
io::eprintfn("failed to upload texture data at acquiring command buffer: %s", sdl::get_error());
libc::exit(1);
}
GPUCopyPass* copypass = sdl::begin_gpu_copy_pass(cmdbuf);
if (copypass == null) {
io::eprintfn("failed to upload texture data at beginning copy pass: %s", sdl::get_error());
libc::exit(1);
}
GPUTransferBuffer* buf = sdl::create_gpu_transfer_buffer(self.gpu,
&&(GPUTransferBufferCreateInfo){.usage = GPU_TRANSFERBUFFERUSAGE_UPLOAD, .size = pixels.len}
);
if (buf == null) {
io::eprintfn("failed to upload texture data at creating the transfer buffer: %s", sdl::get_error());
libc::exit(1);
}
char* gpu_mem = (char*)sdl::map_gpu_transfer_buffer(self.gpu, buf, false);
if (gpu_mem == null) {
io::eprintfn("failed to upload texture data at mapping the transfer buffer: %s", sdl::get_error());
libc::exit(1);
}
// 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)) {
io::eprintfn("failed to upload texture data at command buffer submission: %s", sdl::get_error());
libc::exit(1);
}
sdl::release_gpu_transfer_buffer(self.gpu, buf);
}
macro void Vertex.norm(&p, float w, float h)
{
p.pos.x = p.pos.x * 2.0 / w - 1.0;
p.pos.y = -(p.pos.y * 2.0 / h - 1.0);
}
// an highly inefficient way to draw a single quad, no batching, per-quad upload
fn void Renderer.draw_rect(&self, short x, short y, short w, short h, uint color, String shader_name)
{
// upload the quad data to the gpu
if (self.quad_buffer.initialized == false) {
io::eprintfn("quad buffer not initialized");
libc::exit(1);
}
GPUTransferBuffer* buf = sdl::create_gpu_transfer_buffer(self.gpu,
&&(GPUTransferBufferCreateInfo){.usage = GPU_TRANSFERBUFFERUSAGE_UPLOAD, .size = Quad.sizeof}
);
if (buf == null) {
io::eprintfn("failed to create gpu transfer buffer: %s", sdl::get_error());
libc::exit(1);
}
Quad* quad = (Quad*)sdl::map_gpu_transfer_buffer(self.gpu, buf, false);
if (quad == null) {
io::eprintfn("failed to map gpu transfer buffer: %s", sdl::get_error());
libc::exit(1);
}
/* v1 v4
* +-------------+
* | _/|
* | _/ |
* | 1 _/ |
* | _/ |
* | _/ |
* | _/ 2 |
* |/ |
* +-------------+
* v2 v3
*/
quad.vertices.v1 = {.pos = {.x = x, .y = y}, .col.u = color};
quad.vertices.v2 = {.pos = {.x = x, .y = (float)y+h}, .col.u = color};
quad.vertices.v3 = {.pos = {.x = (float)x+w, .y = (float)y+h}, .col.u = color};
quad.vertices.v4 = {.pos = {.x = (float)x+w, .y = y}, .col.u = color};
quad.vertices.v1.norm(640.0, 480.0);
quad.vertices.v2.norm(640.0, 480.0);
quad.vertices.v3.norm(640.0, 480.0);
quad.vertices.v4.norm(640.0, 480.0);
// triangle 1
quad.indices.i1 = 0; // v1
quad.indices.i2 = 1; // v2
quad.indices.i3 = 3; // v4
// triangle 2
quad.indices.i4 = 1; // v2
quad.indices.i5 = 2; // v3
quad.indices.i6 = 3; // v4
sdl::unmap_gpu_transfer_buffer(self.gpu, buf);
GPUCommandBuffer* cmd = sdl::acquire_gpu_command_buffer(self.gpu);
if (cmd == null) {
io::eprintfn("failed to upload quad at acquiring command buffer: %s", sdl::get_error());
libc::exit(1);
}
GPUCopyPass* cpy = sdl::begin_gpu_copy_pass(cmd);
// upload vertices
sdl::upload_to_gpu_buffer(cpy,
&&(GPUTransferBufferLocation){.transfer_buffer = buf, .offset = Quad.vertices.offsetof},
&&(GPUBufferRegion){.buffer = self.quad_buffer.vert_buf, .offset = 0, .size = Quad.vertices.sizeof},
false
);
// upload indices
sdl::upload_to_gpu_buffer(cpy,
&&(GPUTransferBufferLocation){.transfer_buffer = buf, .offset = Quad.indices.offsetof},
&&(GPUBufferRegion){.buffer = self.quad_buffer.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, buf);
sdl::wait_for_gpu_idle(self.gpu);
/*
// now finally draw the quad
// if we are not in a render pass then we can't render shit
if (self.render_cmd == null) {
unreachable("start rendering first before trying to render a quad");
}
// FIXME: this could be done at the start of rendering
GPUTexture* t;
if (!sdl::wait_and_acquire_gpu_swapchain_texture(self.render_cmd, self.win, &t, null, null)) {
unreachable("failed to acquire swapchain texture: %s", sdl::get_error());
}
// TODO: begin render pass
Pipeline* p = self.pipelines.get_from_name(shader_name);
if (p == null) {
unreachable("no pipeline named: %s", shader_name);
}
// bind the data
sdl::bind_gpu_graphics_pipeline(self.render_pass, pipeline);
sdl::bind_gpu_vertex_buffer(self.render_pass, 0,
&&(GPUBufferBinding){.buffer = self.quad_buffer.vert_buf, .offset = 0}, 1
);
sdl::bind_gpu_index_buffer(self.render_pass, 0,
&&(GPUBufferBinding){.buffer = self.quad_buffer.idx_buf, .offset = 0}, 1
);
sdl::draw_gpu_indexed_primitives(self.render_pass, 6, 1, 0, 0, 0);
*/
}
// TODO: fn Renderer.draw_quad, it has to use a vertex buffer and an index buffer
// TODO: fn Renderer.draw_sprite, same as draw_quad but also bind the texture
// TODO: fn Renderer.begin_render
// TODO: fn Renderer.end_render

View file

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

View file

@ -1,217 +0,0 @@
module ugui;
import std::io;
// button element
struct ElemButton {
int filler;
}
// draw a button, return the events on that button
// FIXME: "state" should be renamed "active" to toggle between an usable button and
// an inactive (greyed-out) button
fn ElemEvents? Ctx.button(&ctx, String label, Rect size, bool state = false)
{
Id id = ctx.gen_id(label)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id)!;
// add it to the tree
ctx.tree.add(id, ctx.active_div)!;
if (elem.flags.is_new) {
elem.type = ETYPE_BUTTON;
} else if (elem.type != ETYPE_BUTTON) {
return WRONG_ELEMENT_TYPE?;
}
elem.bounds = ctx.position_element(parent, size, true);
// 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 {}; }
Color col = 0x0000ffffu.to_rgba();
elem.events = ctx.get_elem_events(elem);
if (state) {
col = 0xff0000ffu.to_rgba();
} else if (ctx.elem_focus(elem) || elem.events.mouse_hover) {
col = 0xff00ffffu.to_rgba();
}
// Draw the button
ctx.push_rect(elem.bounds, col, do_border: true, do_radius: true)!;
return elem.events;
}
fn ElemEvents? Ctx.button_label(&ctx, String label, Rect size = {0,0,short.max,short.max}, bool state = false)
{
Id id = ctx.gen_id(label)!;
Elem *parent = ctx.get_parent()!;
Elem *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
elem.type = ETYPE_BUTTON;
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
Rect text_size = ctx.get_text_bounds(label)!;
Rect btn_size = text_size.add({0,0,10,10});
// 2. Layout
elem.bounds = ctx.position_element(parent, btn_size, true);
if (elem.bounds.is_null()) { return {}; }
Color col = 0x0000ffffu.to_rgba();
elem.events = ctx.get_elem_events(elem);
if (state) {
col = 0xff0000ffu.to_rgba();
} else if (ctx.elem_focus(elem) || elem.events.mouse_hover) {
col = 0xff00ffffu.to_rgba();
}
// Draw the button
text_size.x = elem.bounds.x;
text_size.y = elem.bounds.y;
Point off = ctx.center_text(text_size, elem.bounds);
text_size.x += off.x;
text_size.y += off.y;
ctx.push_rect(elem.bounds, col, do_border: true, do_radius: true)!;
ctx.push_string(text_size, label)!;
return elem.events;
}
fn ElemEvents? Ctx.button_icon(&ctx, String label, String icon, String on_icon = "", bool state = false)
{
Id id = ctx.gen_id(label)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id)!;
// add it to the tree
ctx.tree.add(id, ctx.active_div)!;
if (elem.flags.is_new) {
elem.type = ETYPE_BUTTON;
} else if (elem.type != ETYPE_BUTTON) {
return WRONG_ELEMENT_TYPE?;
}
Sprite* def_sprite = ctx.sprite_atlas.get(icon)!;
Sprite* on_sprite = ctx.sprite_atlas.get(on_icon) ?? &&(Sprite){};
Rect max_size = def_sprite.rect().max(on_sprite.rect());
elem.bounds = ctx.position_element(parent, max_size, true);
// 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 {}; }
Color col = 0x0000ffffu.to_rgba();
elem.events = ctx.get_elem_events(elem);
Id tex_id = ctx.sprite_atlas.id;
if (state && on_icon != "") {
ctx.push_sprite(elem.bounds, on_sprite.uv(), tex_id, type: on_sprite.type)!;
} else {
ctx.push_sprite(elem.bounds, def_sprite.uv(), tex_id, type: def_sprite.type)!;
}
// Draw the button
ctx.push_rect(elem.bounds, col, do_border: true, do_radius: true)!;
return elem.events;
}
// FIXME: this should be inside the style
const ushort DEFAULT_CHECKBOX_SIZE = 16;
fn void? Ctx.checkbox(&ctx, String label, String description, Point off, bool* state, String tick_sprite = {})
{
Id id = ctx.gen_id(label)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id)!;
// add it to the tree
ctx.tree.add(id, ctx.active_div)!;
// FIXME: for now checkboxes and buttons have no members so the element types
// can be the same
if (elem.flags.is_new) {
elem.type = ETYPE_BUTTON;
} else if (elem.type != ETYPE_BUTTON) {
return WRONG_ELEMENT_TYPE?;
}
Rect size = {off.x, off.y, DEFAULT_CHECKBOX_SIZE, DEFAULT_CHECKBOX_SIZE};
elem.bounds = ctx.position_element(parent, size, true);
// 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) *state = !(*state);
Color col;
if (tick_sprite != {}) {
col = ctx.style.bgcolor;
ctx.push_rect(elem.bounds, col, do_border: true, do_radius: true)!;
if (*state) {
ctx.draw_sprite_raw(tick_sprite, elem.bounds)!;
}
} else {
if (*state) {
col = 0xff0000ffu.to_rgba();
} else {
col = 0xff00ffffu.to_rgba();
}
// Draw the button
ctx.push_rect(elem.bounds, col, do_border: true, do_radius: true)!;
}
}
// FIXME: this should be inside the style
const short DEFAULT_SWITCH_SIZE = 16;
fn void? Ctx.toggle(&ctx, String label, String description, Point off, bool* state)
{
Id id = ctx.gen_id(label)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id)!;
// add it to the tree
ctx.tree.add(id, ctx.active_div)!;
// FIXME: for now switches and buttons have no members so the element types
// can be the same
if (elem.flags.is_new) {
elem.type = ETYPE_BUTTON;
} else if (elem.type != ETYPE_BUTTON) {
return WRONG_ELEMENT_TYPE?;
}
Rect size = {off.x, off.y, DEFAULT_SWITCH_SIZE*2, DEFAULT_SWITCH_SIZE};
elem.bounds = ctx.position_element(parent, size, true);
// 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) *state = !(*state);
Color col;
if (*state) {
col = 0xff0000ffu.to_rgba();
} else {
col = 0xff00ffffu.to_rgba();
}
// Draw the button
// FIXME: THIS IS SHIT
ctx.push_rect(elem.bounds, ctx.style.bgcolor, do_border: true, do_radius: true)!;
Rect t = elem.bounds.add({*state ? (DEFAULT_SWITCH_SIZE+3) : +3, +3, -DEFAULT_SWITCH_SIZE-6, -6});
ctx.push_rect(t, col, do_border: false, do_radius: true)!;
}

View file

@ -1,191 +0,0 @@
module ugui;
import std::ascii;
// 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 {
CmdType type;
union {
CmdRect rect;
CmdUpdateAtlas update_atlas;
CmdSprite sprite;
CmdScissor scissor;
}
}
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)
{
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);
}
// 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)
{
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.push_cmd(&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,
};
if (cull_rect(cmd.rect.rect, ctx.div_scissor)) return;
ctx.push_cmd(&cmd)!;
}
// TODO: add texture id
fn void? Ctx.push_sprite(&ctx, Rect bounds, Rect texture, Id texture_id, 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)!;
}
fn void? Ctx.push_string(&ctx, Rect bounds, String text, Color hue = 0xffffffffu.to_rgba())
{
if (text.len == 0) {
return;
}
ctx.push_scissor(bounds)!;
short baseline = (short)ctx.font.ascender;
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
short line_gap = (short)ctx.font.linegap;
Id texture_id = ctx.font.id; // or ctx.font.atlas.id
Point orig = {
.x = bounds.x,
.y = bounds.y,
};
short line_len;
Codepoint cp;
usz off, x;
while ((cp = str_to_codepoint(text[off..], &x)) != 0) {
off += x;
Glyph* gp;
if (!ascii::is_cntrl((char)cp)) {
gp = ctx.font.get_glyph(cp)!;
Rect gb = {
.x = orig.x + line_len + gp.ox,
.y = orig.y + gp.oy + baseline,
.w = gp.w,
.h = gp.h,
};
Rect gt = {
.x = gp.u,
.y = gp.v,
.w = gp.w,
.h = gp.h,
};
// push the sprite only if it collides with the bounds
if (!cull_rect(gb, bounds)) ctx.push_sprite(gb, gt, texture_id, hue)!;
line_len += gp.adv;
} else if (cp == '\n'){
orig.y += line_height + line_gap;
line_len = 0;
} else {
continue;
}
}
// FIXME: we never get here if an error was thrown before
ctx.push_scissor({})!;
}
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(),
},
};
ctx.push_cmd(&up)!;
}
fn void? Ctx.push_scissor(&ctx, Rect rect)
{
Cmd sc = {
.type = CMD_SCISSOR,
.scissor.rect = rect.intersection(ctx.div_scissor),
};
ctx.push_cmd(&sc)!;
}

View file

@ -1,340 +0,0 @@
module ugui;
import vtree;
import cache;
import fifo;
import std::io;
import std::core::string;
// element ids are just long ints
alias Id = usz;
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;
}
// element structure
struct Elem {
Id id;
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, UNEXPECTED_ELEMENT, WRONG_ELEMENT_TYPE;
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 = 256;
const uint ROOT_ID = 1;
const uint TEXT_MAX = 64;
// 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 {
IdTree tree;
ElemCache cache;
CmdQueue cmd_queue;
// total size in pixels of the context
ushort width, height;
Style style;
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 down;
}
}
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)!;
return ctx.cache.search(parent_id);
}
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
macro Id? Ctx.gen_id(&ctx, String label)
{
Id id1 = ctx.tree.get(ctx.active_div)!;
Id id2 = label.hash();
// Mix the two IDs non-linearly
Id mixed = id1 ^ id2.rotate_left(13);
mixed ^= id1.rotate_left(7);
mixed += GOLDEN_RATIO;
return mixed;
}
// 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)
{
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;
// FIXME: should this be here? or is it better to have the elements set the id?
elem.id = id;
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;
}
// FIXME: Since ids are now keyed with the element's parent id, this function does not work
// outside of the element's div block.
// 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 = ctx.get_id(label);
// 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.active_div = 0;
// TODO: add style config
ctx.style.margin = {2, 2, 2, 2};
ctx.style.border = {2, 2, 2, 2};
ctx.style.padding = {1, 1, 1, 1};
ctx.style.radius = 5;
ctx.style.bgcolor = 0x282828ffu.to_rgba();
ctx.style.fgcolor = 0xfbf1c7ffu.to_rgba();
ctx.style.brcolor = 0xd79921ffu.to_rgba();
}
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();
}
fn void? Ctx.frame_begin(&ctx)
{
// 2. Get the root element from the cache and update it
Elem* 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
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 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 = elem.flags,
};
*elem = def_root;
// 3. Push the root element into the element tree
ctx.active_div = ctx.tree.add(ROOT_ID, 0)!;
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
}
fn void? Ctx.frame_end(&ctx)
{
Elem* root = ctx.get_elem_by_tree_idx(0)!;
root.div.layout = LAYOUT_ROW;
// 1. clear the tree
ctx.tree.nuke();
// 2. clear input fields
ctx.input.events = (InputEvents)0;
ctx.input.keyboard.text_len = 0;
// send atlas updates
if (ctx.font.should_update) {
ctx.push_update_atlas(&ctx.font.atlas)!;
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;
}
$if 1:
// draw mouse position
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 = 0xff00ffffu.to_rgba()
};
ctx.cmd_queue.enqueue(&cmd)!;
$endif
}
<*
* @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),
};
return ev;
}

View file

@ -1,152 +0,0 @@
module ugui;
import std::io;
import std::math;
const short SCROLLBAR_DIM = 5;
// div element
struct ElemDiv {
Layout layout;
struct scroll_x {
bool enabled;
bool on;
float value;
}
struct scroll_y {
bool enabled;
bool on;
float value;
}
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
fn void? Ctx.div_begin(&ctx, String label, Rect size, bool scroll_x = false, bool scroll_y = false)
{
Id id = ctx.gen_id(label)!;
Elem* parent = ctx.get_parent()!;
Elem* elem = ctx.get_elem(id)!;
isz div_node = ctx.tree.add(id, ctx.active_div)!;
ctx.active_div = div_node;
bool is_new = elem.flags.is_new;
if (elem.flags.is_new) {
elem.type = ETYPE_DIV;
} else if (elem.type != ETYPE_DIV) {
return WRONG_ELEMENT_TYPE?;
}
elem.div.scroll_x.enabled = scroll_x;
elem.div.scroll_y.enabled = scroll_y;
// 2. layout the element
Rect wanted_size = {
.x = size.x,
.y = size.y,
.w = size.w < 0 ? max(elem.div.pcb.w, (short)-size.w) : size.w,
.h = size.h < 0 ? max(elem.div.pcb.h, (short)-size.h) : size.h,
};
elem.bounds = ctx.position_element(parent, wanted_size);
elem.div.children_bounds = {};
// update the ctx scissor
ctx.div_scissor = elem.bounds;
ctx.push_scissor(elem.bounds)!;
// 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, ctx.style.bgcolor, do_border: do_border)!;
elem.events = ctx.get_elem_events(elem);
// TODO: check active
// TODO: check resizeable
}
fn void? Ctx.div_end(&ctx)
{
// swap the children bounds
Elem* parent = ctx.get_parent()!;
Elem* elem = ctx.get_elem_by_tree_idx(ctx.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 = ctx.gen_id("div_scrollbar_horizontal")!;
Id vsid = ctx.gen_id("div_scrollbar_vertical")!;
short wdim = elem.div.scroll_y.on ? (ctx.focus_id == vsid || ctx.is_hovered(ctx.find_elem(vsid)) ? SCROLLBAR_DIM*3 : SCROLLBAR_DIM) : 0;
short hdim = elem.div.scroll_x.on ? (ctx.focus_id == hsid || ctx.is_hovered(ctx.find_elem(hsid)) ? SCROLLBAR_DIM*3 : SCROLLBAR_DIM) : 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("div_scrollbar_vertical", 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("div_scrollbar_horizontal", hslider, &elem.div.scroll_x.value, max((float)bc.x / cbc.x, (float)0.15))!;
elem.div.layout = prev_l;
}
// the active_div returns to the parent of the current one
ctx.active_div = ctx.tree.parentof(ctx.active_div)!;
}

View file

@ -1,226 +0,0 @@
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
*>
fn Codepoint str_to_codepoint(char[] str, usz* off)
{
Codepoint cp;
isz b = grapheme::decode_utf8(str, str.len, (uint*)&cp);
if (b == 0 || b > str.len) {
return 0;
}
*off = b;
return cp;
}
fn Rect? Ctx.get_text_bounds(&ctx, String text)
{
Rect text_bounds;
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
short line_gap = (short)ctx.font.linegap;
text_bounds.h = line_height;
Glyph* gp;
// TODO: account for unicode codepoints
short line_len;
Codepoint cp;
usz off, x;
while ((cp = str_to_codepoint(text[off..], &x)) != 0) {
off += x;
bool n;
if (!ascii::is_cntrl((char)cp)) {
gp = ctx.font.get_glyph(cp)!;
line_len += gp.adv;
} else if (cp == '\n'){
text_bounds.h += line_height + line_gap;
line_len = 0;
} else {
continue;
}
if (line_len > text_bounds.w) {
text_bounds.w = line_len;
}
}
return text_bounds;
}
fn Point Ctx.center_text(&ctx, Rect text_bounds, Rect bounds)
{
short dw = bounds.w - text_bounds.w;
short dh = bounds.h - text_bounds.h;
return {.x = dw/2, .y = dh/2};
}
// TODO: check if the font is present in the context
fn Id Ctx.get_font_id(&ctx, String label)
{
return (Id)label.hash();
}

View file

@ -1,189 +0,0 @@
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, same as SDL
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;
}
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_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.down & 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, 0u16, ctx.width);
ctx.input.mouse.pos.y = math::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 = 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, 0u16, ctx.width);
ctx.input.mouse.pos.y = math::clamp(my, 0u16, 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;
}
// Mouse Buttons down
fn void Ctx.input_mod_keys(&ctx, ModKeys modkeys)
{
ctx.input.keyboard.down = modkeys;
ctx.input.events.mod_key = (uint)ctx.input.keyboard.down != 0;
}

View file

@ -1,196 +0,0 @@
module ugui;
enum Layout {
LAYOUT_ROW,
LAYOUT_COLUMN,
LAYOUT_FLOATING,
LAYOUT_ABSOLUTE,
}
fn void? Ctx.layout_set_row(&ctx)
{
Id parent_id = ctx.tree.get(ctx.active_div)!;
Elem *parent = ctx.cache.search(parent_id)!;
if (parent.type != ETYPE_DIV) {
// what?
return UNEXPECTED_ELEMENT?;
}
parent.div.layout = LAYOUT_ROW;
}
fn void? Ctx.layout_set_column(&ctx)
{
Id parent_id = ctx.tree.get(ctx.active_div)!;
Elem *parent = ctx.cache.search(parent_id)!;
if (parent.type != ETYPE_DIV) {
// what?
return UNEXPECTED_ELEMENT?;
}
parent.div.layout = LAYOUT_COLUMN;
}
fn void? Ctx.layout_set_floating(&ctx)
{
Id parent_id = ctx.tree.get(ctx.active_div)!;
Elem *parent = ctx.cache.search(parent_id)!;
if (parent.type != ETYPE_DIV) {
// what?
return UNEXPECTED_ELEMENT?;
}
parent.div.layout = LAYOUT_FLOATING;
}
fn void? Ctx.layout_next_row(&ctx)
{
Id parent_id = ctx.tree.get(ctx.active_div)!;
Elem *parent = ctx.cache.search(parent_id)!;
if (parent.type != ETYPE_DIV) {
return UNEXPECTED_ELEMENT?;
}
parent.div.origin_r = {
.x = parent.bounds.x,
.y = parent.div.children_bounds.bottom_right().y,
};
parent.div.origin_c = parent.div.origin_r;
}
fn void? Ctx.layout_next_column(&ctx)
{
Id parent_id = ctx.tree.get(ctx.active_div)!;
Elem *parent = ctx.cache.search(parent_id)!;
if (parent.type != ETYPE_DIV) {
return UNEXPECTED_ELEMENT?;
}
parent.div.origin_c = {
.x = parent.div.children_bounds.bottom_right().x,
.y = parent.bounds.y,
};
parent.div.origin_r = parent.div.origin_c;
}
macro Rect Elem.get_view(&elem)
{
Rect off;
if (elem.div.scroll_x.enabled && elem.div.scroll_x.on) {
off.x = (short)((float)(elem.div.pcb.w - elem.bounds.w) * elem.div.scroll_x.value);
off.w = -SCROLLBAR_DIM;
}
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 = -SCROLLBAR_DIM;
}
return elem.bounds.add(off);
}
macro Point Elem.get_view_off(&elem)
{
return elem.get_view().sub(elem.bounds).position();
}
// position the rectangle inside the parent according to the layout
// parent: parent div
// rect: the requested size
// style: apply style
<*
@require ctx != null
@require parent.type == ETYPE_DIV
*>
fn Rect Ctx.position_element(&ctx, Elem *parent, Rect rect, bool style = false)
{
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()));
if (style) {
Rect margin = ctx.style.margin;
Rect border = ctx.style.border;
Rect padding = ctx.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
if (!child_occupied.bottom_right().in_rect(div.children_bounds)) {
// right overflow
if (child_occupied.bottom_right().x > div.children_bounds.bottom_right().x) {
div.children_bounds.w += child_occupied.bottom_right().x - div.children_bounds.bottom_right().x;
}
// bottom overflow
if (child_occupied.bottom_right().y > div.children_bounds.bottom_right().y) {
div.children_bounds.h += child_occupied.bottom_right().y - div.children_bounds.bottom_right().y;
}
}
// 99. return the placement
if (child_placement.collides(parent_view)) {
return child_placement.off(parent.get_view_off().neg());
} else {
return {};
}
}

View file

@ -1,204 +0,0 @@
module ugui;
// ---------------------------------------------------------------------------------- //
// RECTANGLE //
// ---------------------------------------------------------------------------------- //
// Rect and it's methods
struct Rect {
short x, y, w, h;
}
// 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);
}
// 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,
};
}
// ---------------------------------------------------------------------------------- //
// 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(uint u)
{
return {
.r = (char)((u >> 24) & 0xff),
.g = (char)((u >> 16) & 0xff),
.b = (char)((u >> 8) & 0xff),
.a = (char)((u >> 0) & 0xff)
};
}

View file

@ -1,135 +0,0 @@
module ugui;
import std::io;
import std::math;
// slider element
struct ElemSlider {
Rect handle;
}
/* handle
* +----+-----+---------------------+
* | |#####| |
* +----+-----+---------------------+
*/
<*
@require value != null
*>
fn ElemEvents? Ctx.slider_hor(&ctx,
String label,
Rect size,
float* value,
float hpercent = 0.25,
Color bgcolor = 0x0000ffffu.to_rgba(),
Color handlecolor = 0x0ff000ffu.to_rgba())
{
Id id = ctx.gen_id(label)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id)!;
// add it to the tree
ctx.tree.add(id, ctx.active_div)!;
// 1. Fill the element fields
if (elem.flags.is_new) {
elem.type = ETYPE_SLIDER;
} else if (elem.type != ETYPE_SLIDER) {
return WRONG_ELEMENT_TYPE?;
}
// 2. Layout
elem.bounds = ctx.position_element(parent, size, true);
// handle width
short hw = (short)(elem.bounds.w * hpercent);
Rect handle = {
.x = calc_slider(elem.bounds.x, elem.bounds.w-hw, *value),
.y = elem.bounds.y,
.w = hw,
.h = elem.bounds.h,
};
elem.slider.handle = handle;
Point m = ctx.input.mouse.pos;
elem.events = ctx.get_elem_events(elem);
if (ctx.elem_focus(elem) && ctx.is_mouse_down(BTN_LEFT)) {
*value = calc_value(elem.bounds.x, m.x, elem.bounds.w, hw);
elem.slider.handle.x = calc_slider(elem.bounds.x, elem.bounds.w-hw, *value);
elem.events.update = true;
}
// Draw the slider background and handle
ctx.push_rect(elem.bounds, bgcolor)!;
ctx.push_rect(elem.slider.handle, handlecolor)!;
return elem.events;
}
/*
* +-+
* | |
* | |
* +-+
* |#| handle
* |#|
* +-+
* | |
* | |
* +-+
*/
fn ElemEvents? Ctx.slider_ver(&ctx,
String label,
Rect size,
float* value,
float hpercent = 0.25,
Color bgcolor = 0x0000ffffu.to_rgba(),
Color handlecolor = 0x0ff000ffu.to_rgba())
{
Id id = ctx.gen_id(label)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id)!;
// add it to the tree
ctx.tree.add(id, ctx.active_div)!;
// 1. Fill the element fields
if (elem.flags.is_new) {
elem.type = ETYPE_SLIDER;
} else if (elem.type != ETYPE_SLIDER) {
return WRONG_ELEMENT_TYPE?;
}
// 2. Layout
elem.bounds = ctx.position_element(parent, size, true);
// handle height
short hh = (short)(elem.bounds.h * hpercent);
Rect handle = {
.x = elem.bounds.x,
.y = calc_slider(elem.bounds.y, elem.bounds.h-hh, *value),
.w = elem.bounds.w,
.h = hh,
};
elem.slider.handle = handle;
Point m = ctx.input.mouse.pos;
elem.events = ctx.get_elem_events(elem);
if (ctx.elem_focus(elem) && ctx.is_mouse_down(BTN_LEFT)) {
*value = calc_value(elem.bounds.y, m.y, elem.bounds.h, hh);
elem.slider.handle.y = calc_slider(elem.bounds.y, elem.bounds.h-hh, *value);
elem.events.update = true;
}
// Draw the slider background and handle
ctx.push_rect(elem.bounds, bgcolor)!;
ctx.push_rect(elem.slider.handle, handlecolor)!;
return elem.events;
}
macro short calc_slider(short off, short dim, float value) => (short)off + (short)(dim * value);
macro float calc_value(short off, short mouse, short dim, short slider)
=> math::clamp((float)(mouse-off-slider/2)/(float)(dim-slider), 0.0f, 1.0f);

View file

@ -1,175 +0,0 @@
module ugui;
import std::core::mem::allocator;
import std::collections::map;
import std::io;
import mqoi;
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)
{
mqoi::Desc image_desc;
uint w, h;
File file = file::open(path, "rb")!;
defer (void) file.close();
while (!mqoi::desc_done(&image_desc)) {
mqoi::desc_push(&image_desc, file.read_byte()!);
}
if (mqoi::desc_verify(&image_desc, &w, &h) != 0) {
return io::FILE_NOT_VALID?;
}
mqoi::Dec dec;
mqoi::Rgba* px;
usz idx;
char[] pixels = mem::new_array(char, (usz)w*h*4);
defer mem::free(pixels);
mqoi::dec_init(&dec, w*h);
while (!mqoi::dec_done(&dec)) {
mqoi::dec_push(&dec, file.read_byte()!);
while ((px = mqoi::dec_pop(&dec)) != null) {
pixels[idx..idx+3] = px.value[..];
idx += 4;
}
}
ctx.sprite_atlas.insert(name, type, pixels, (ushort)w, (ushort)h, (ushort)w)!;
}
fn void? Ctx.draw_sprite(&ctx, String label, String name, Point off = {0,0})
{
Id id = ctx.gen_id(label)!;
Elem *parent = ctx.get_parent()!;
Elem *elem = ctx.get_elem(id)!;
// add it to the tree
ctx.tree.add(id, ctx.active_div)!;
if (elem.flags.is_new) {
elem.type = ETYPE_SPRITE;
} else if (elem.type != ETYPE_SPRITE) {
return WRONG_ELEMENT_TYPE?;
}
Sprite* sprite = ctx.sprite_atlas.get(name)!;
Rect uv = { sprite.u, sprite.v, sprite.w, sprite.h };
Rect bounds = { 0, 0, sprite.w, sprite.h };
elem.bounds = ctx.position_element(parent, bounds.off(off), true);
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)!;
}
fn void? Ctx.draw_sprite_raw(&ctx, String name, Rect bounds)
{
Sprite* sprite = ctx.sprite_atlas.get(name)!;
Id tex_id = ctx.sprite_atlas.id;
return ctx.push_sprite(bounds, sprite.uv(), tex_id, type: sprite.type)!;
}

View file

@ -1,30 +0,0 @@
module ugui;
import std::io;
struct ElemText {
char* str;
}
fn void? Ctx.text_unbounded(&ctx, String label, String text)
{
Id id = ctx.gen_id(label)!;
Elem *parent = ctx.get_parent()!;
Elem *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
elem.type = ETYPE_TEXT;
elem.text.str = text;
// if the element is new or the parent was updated then redo layout
Rect text_size = ctx.get_text_bounds(text)!;
// 2. Layout
elem.bounds = ctx.position_element(parent, text_size, true);
if (elem.bounds.is_null()) { return; }
ctx.push_string(elem.bounds, text)!;
}

View file

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