ui pipeline
This commit is contained in:
BIN
goldens/last_frame.png
Normal file
BIN
goldens/last_frame.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
255
main.sx
255
main.sx
@@ -2,118 +2,89 @@
|
|||||||
#import "modules/sdl3.sx";
|
#import "modules/sdl3.sx";
|
||||||
#import "modules/opengl.sx";
|
#import "modules/opengl.sx";
|
||||||
#import "modules/math";
|
#import "modules/math";
|
||||||
stb :: #import "modules/stb.sx";
|
#import "modules/stb.sx";
|
||||||
|
#import "ui";
|
||||||
|
|
||||||
WIDTH :f32: 800;
|
WIDTH :f32: 800;
|
||||||
HEIGHT :f32: 600;
|
HEIGHT :f32: 600;
|
||||||
|
|
||||||
|
save_snapshot :: (path: [:0]u8, w: s32, h: s32) {
|
||||||
|
stride : s64 = xx (w * 4);
|
||||||
|
buf_size : s64 = stride * xx h;
|
||||||
|
pixels : [*]u8 = xx context.allocator.alloc(buf_size);
|
||||||
|
glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
|
||||||
|
|
||||||
|
// Flip vertically (GL reads bottom-up, PNG expects top-down)
|
||||||
|
row_buf : [*]u8 = xx context.allocator.alloc(stride);
|
||||||
|
i : s32 = 0;
|
||||||
|
while i < h / 2 {
|
||||||
|
top : s64 = xx i * stride;
|
||||||
|
bot : s64 = xx (h - 1 - i) * stride;
|
||||||
|
memcpy(row_buf, @pixels[top], stride);
|
||||||
|
memcpy(@pixels[top], @pixels[bot], stride);
|
||||||
|
memcpy(@pixels[bot], row_buf, stride);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
stbi_write_png(path, w, h, 4, pixels, xx stride);
|
||||||
|
out("Saved ");
|
||||||
|
out(path);
|
||||||
|
out("\n");
|
||||||
|
}
|
||||||
|
|
||||||
main :: () -> void {
|
main :: () -> void {
|
||||||
print("init video: \n");
|
|
||||||
SDL_Init(SDL_INIT_VIDEO);
|
SDL_Init(SDL_INIT_VIDEO);
|
||||||
|
|
||||||
print("init opengl: \n");
|
|
||||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
||||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
|
||||||
|
|
||||||
print("init opengl profile: \n");
|
|
||||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
|
||||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG);
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG);
|
||||||
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
||||||
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
|
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
|
||||||
|
|
||||||
print("create window: \n");
|
window := SDL_CreateWindow("SX UI Demo", xx WIDTH, xx HEIGHT, SDL_WINDOW_OPENGL);
|
||||||
window := SDL_CreateWindow("SX Game", xx WIDTH, xx HEIGHT, SDL_WINDOW_OPENGL);
|
|
||||||
gl_ctx := SDL_GL_CreateContext(window);
|
gl_ctx := SDL_GL_CreateContext(window);
|
||||||
SDL_GL_MakeCurrent(window, gl_ctx);
|
SDL_GL_MakeCurrent(window, gl_ctx);
|
||||||
SDL_GL_SetSwapInterval(1);
|
SDL_GL_SetSwapInterval(1);
|
||||||
|
|
||||||
print("load gl: \n");
|
|
||||||
load_gl(SDL_GL_GetProcAddress);
|
load_gl(SDL_GL_GetProcAddress);
|
||||||
|
load_gl_ui(SDL_GL_GetProcAddress);
|
||||||
|
|
||||||
glEnable(GL_DEPTH_TEST);
|
// --- Build UI ---
|
||||||
glDepthFunc(GL_LESS);
|
pipeline : UIPipeline = ---;
|
||||||
|
pipeline.init(WIDTH, HEIGHT);
|
||||||
|
|
||||||
program := create_program(VERT_SHADER_SRC, FRAG_SHADER_SRC);
|
// Create a simple layout: VStack with colored rects and a button
|
||||||
glUseProgram(program);
|
root := VStack.{ spacing = 10.0, alignment = .center };
|
||||||
|
|
||||||
print("uniform locations: \n");
|
header := RectView.{ color = COLOR_YELLOW, preferred_height = 80.0, corner_radius = 8.0 };
|
||||||
mvp_loc := glGetUniformLocation(program, "uMVP");
|
root.add(xx header);
|
||||||
light_loc := glGetUniformLocation(program, "uLightDir");
|
|
||||||
wire_loc := glGetUniformLocation(program, "uWire");
|
|
||||||
|
|
||||||
print("vertices\n");
|
btn := Button.{ label = "Click Me", font_size = 14.0, style = ButtonStyle.default(), on_tap = null };
|
||||||
// Cube vertices: pos(vec4 w=1) + normal(vec4 w=0), 36 vertices × 2 vec4s = 72
|
root.add(xx btn);
|
||||||
vertices : []Vector(4, f32) = .[
|
|
||||||
// Front face (z = +0.5)
|
|
||||||
.[-0.5, -0.5, 0.5, 1.0], .[ 0.0, 0.0, 1.0, 0.0],
|
|
||||||
.[ 0.5, -0.5, 0.5, 1.0], .[ 0.0, 0.0, 1.0, 0.0],
|
|
||||||
.[ 0.5, 0.5, 0.5, 1.0], .[ 0.0, 0.0, 1.0, 0.0],
|
|
||||||
.[-0.5, -0.5, 0.5, 1.0], .[ 0.0, 0.0, 1.0, 0.0],
|
|
||||||
.[ 0.5, 0.5, 0.5, 1.0], .[ 0.0, 0.0, 1.0, 0.0],
|
|
||||||
.[-0.5, 0.5, 0.5, 1.0], .[ 0.0, 0.0, 1.0, 0.0],
|
|
||||||
// Back face (z = -0.5)
|
|
||||||
.[ 0.5, -0.5, -0.5, 1.0], .[ 0.0, 0.0, -1.0, 0.0],
|
|
||||||
.[-0.5, -0.5, -0.5, 1.0], .[ 0.0, 0.0, -1.0, 0.0],
|
|
||||||
.[-0.5, 0.5, -0.5, 1.0], .[ 0.0, 0.0, -1.0, 0.0],
|
|
||||||
.[ 0.5, -0.5, -0.5, 1.0], .[ 0.0, 0.0, -1.0, 0.0],
|
|
||||||
.[-0.5, 0.5, -0.5, 1.0], .[ 0.0, 0.0, -1.0, 0.0],
|
|
||||||
.[ 0.5, 0.5, -0.5, 1.0], .[ 0.0, 0.0, -1.0, 0.0],
|
|
||||||
// Top face (y = +0.5)
|
|
||||||
.[-0.5, 0.5, 0.5, 1.0], .[ 0.0, 1.0, 0.0, 0.0],
|
|
||||||
.[ 0.5, 0.5, 0.5, 1.0], .[ 0.0, 1.0, 0.0, 0.0],
|
|
||||||
.[ 0.5, 0.5, -0.5, 1.0], .[ 0.0, 1.0, 0.0, 0.0],
|
|
||||||
.[-0.5, 0.5, 0.5, 1.0], .[ 0.0, 1.0, 0.0, 0.0],
|
|
||||||
.[ 0.5, 0.5, -0.5, 1.0], .[ 0.0, 1.0, 0.0, 0.0],
|
|
||||||
.[-0.5, 0.5, -0.5, 1.0], .[ 0.0, 1.0, 0.0, 0.0],
|
|
||||||
// Bottom face (y = -0.5)
|
|
||||||
.[-0.5, -0.5, -0.5, 1.0], .[0.0, -1.0, 0.0, 0.0],
|
|
||||||
.[ 0.5, -0.5, -0.5, 1.0], .[0.0, -1.0, 0.0, 0.0],
|
|
||||||
.[ 0.5, -0.5, 0.5, 1.0], .[0.0, -1.0, 0.0, 0.0],
|
|
||||||
.[-0.5, -0.5, -0.5, 1.0], .[0.0, -1.0, 0.0, 0.0],
|
|
||||||
.[ 0.5, -0.5, 0.5, 1.0], .[0.0, -1.0, 0.0, 0.0],
|
|
||||||
.[-0.5, -0.5, 0.5, 1.0], .[0.0, -1.0, 0.0, 0.0],
|
|
||||||
// Right face (x = +0.5)
|
|
||||||
.[ 0.5, -0.5, 0.5, 1.0], .[1.0, 0.0, 0.0, 0.0],
|
|
||||||
.[ 0.5, -0.5, -0.5, 1.0], .[1.0, 0.0, 0.0, 0.0],
|
|
||||||
.[ 0.5, 0.5, -0.5, 1.0], .[1.0, 0.0, 0.0, 0.0],
|
|
||||||
.[ 0.5, -0.5, 0.5, 1.0], .[1.0, 0.0, 0.0, 0.0],
|
|
||||||
.[ 0.5, 0.5, -0.5, 1.0], .[1.0, 0.0, 0.0, 0.0],
|
|
||||||
.[ 0.5, 0.5, 0.5, 1.0], .[1.0, 0.0, 0.0, 0.0],
|
|
||||||
// Left face (x = -0.5)
|
|
||||||
.[-0.5, -0.5, -0.5, 1.0], .[-1.0, 0.0, 0.0, 0.0],
|
|
||||||
.[-0.5, -0.5, 0.5, 1.0], .[-1.0, 0.0, 0.0, 0.0],
|
|
||||||
.[-0.5, 0.5, 0.5, 1.0], .[-1.0, 0.0, 0.0, 0.0],
|
|
||||||
.[-0.5, -0.5, -0.5, 1.0], .[-1.0, 0.0, 0.0, 0.0],
|
|
||||||
.[-0.5, 0.5, 0.5, 1.0], .[-1.0, 0.0, 0.0, 0.0],
|
|
||||||
.[-0.5, 0.5, -0.5, 1.0], .[-1.0, 0.0, 0.0, 0.0]
|
|
||||||
];
|
|
||||||
|
|
||||||
print("cube buffer: \n");
|
body := HStack.{ spacing = 10.0, alignment = .center };
|
||||||
vao : u32 = 0;
|
|
||||||
vbo : u32 = 0;
|
|
||||||
glGenVertexArrays(1, @vao);
|
|
||||||
glGenBuffers(1, @vbo);
|
|
||||||
|
|
||||||
glBindVertexArray(vao);
|
left := RectView.{ color = COLOR_RED, preferred_width = 200.0, preferred_height = 300.0, corner_radius = 4.0 };
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
body.add(xx left);
|
||||||
glBufferData(GL_ARRAY_BUFFER, 1152, xx vertices, GL_STATIC_DRAW);
|
|
||||||
|
|
||||||
glVertexAttribPointer(0, 3, GL_FLOAT, 0, 32, xx 0);
|
right := RectView.{ color = COLOR_GREEN, preferred_width = 200.0, preferred_height = 300.0, corner_radius = 4.0 };
|
||||||
glEnableVertexAttribArray(0);
|
body.add(xx right);
|
||||||
|
|
||||||
glVertexAttribPointer(1, 3, GL_FLOAT, 0, 32, xx 16);
|
root.add(xx body);
|
||||||
glEnableVertexAttribArray(1);
|
|
||||||
|
|
||||||
glUniform3f(light_loc, 0.5, 0.7, 1.0);
|
footer := RectView.{ color = COLOR_DARK_GRAY, preferred_height = 60.0 };
|
||||||
glUniform1f(wire_loc, 0.0);
|
root.add(xx footer);
|
||||||
|
|
||||||
|
pipeline.set_root(xx root);
|
||||||
|
|
||||||
|
// --- Main loop ---
|
||||||
running := true;
|
running := true;
|
||||||
event : SDL_Event = .none;
|
sdl_event : SDL_Event = .none;
|
||||||
|
|
||||||
print("loop: \n");
|
|
||||||
while running {
|
while running {
|
||||||
while SDL_PollEvent(event) {
|
while SDL_PollEvent(sdl_event) {
|
||||||
if event == {
|
if sdl_event == {
|
||||||
case .quit: running = false;
|
case .quit: running = false;
|
||||||
case .key_up: (e) {
|
case .key_up: (e) {
|
||||||
if e.key == {
|
if e.key == {
|
||||||
@@ -121,123 +92,29 @@ main :: () -> void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Forward to UI
|
||||||
|
ui_event := translate_sdl_event(@sdl_event);
|
||||||
|
if ui_event.type != .none {
|
||||||
|
pipeline.dispatch_event(@ui_event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ticks := SDL_GetTicks();
|
glClearColor(0.12, 0.12, 0.15, 1.0);
|
||||||
ms : f32 = xx ticks;
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
angle := ms * 0.001;
|
|
||||||
|
|
||||||
proj := mat4_perspective(PI/ 4.0, WIDTH / HEIGHT, 0.1, 100.0);
|
pipeline.tick();
|
||||||
view := mat4_translate(0.0, 0.0, -3.0);
|
|
||||||
rot_y := mat4_rotate_y(angle);
|
|
||||||
rot_x := mat4_rotate_x(angle * 0.7);
|
|
||||||
model := mat4_multiply(rot_y, rot_x);
|
|
||||||
vm := mat4_multiply(view, model);
|
|
||||||
mvp := mat4_multiply(proj, vm);
|
|
||||||
|
|
||||||
glUniformMatrix4fv(mvp_loc, 1, 0, mvp.data);
|
|
||||||
|
|
||||||
glClearColor(0.1, 0.1, 0.15, 1.0);
|
|
||||||
glClear(GL_COLOR_BUFFER_BIT + GL_DEPTH_BUFFER_BIT);
|
|
||||||
|
|
||||||
glUniform1f(wire_loc, 0.0);
|
|
||||||
glBindVertexArray(vao);
|
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 36);
|
|
||||||
|
|
||||||
glDepthFunc(GL_LEQUAL);
|
|
||||||
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
|
|
||||||
glLineWidth(2.0);
|
|
||||||
glUniform1f(wire_loc, 1.0);
|
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 36);
|
|
||||||
|
|
||||||
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
|
|
||||||
glDepthFunc(GL_LESS);
|
|
||||||
glUniform1f(wire_loc, 0.0);
|
|
||||||
|
|
||||||
print("{}\n", ms);
|
|
||||||
|
|
||||||
SDL_GL_SwapWindow(window);
|
SDL_GL_SwapWindow(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-render one frame for snapshot (back buffer is stale after swap)
|
||||||
|
glClearColor(0.12, 0.12, 0.15, 1.0);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
pipeline.tick();
|
||||||
|
save_snapshot("goldens/last_frame.png", xx WIDTH, xx HEIGHT);
|
||||||
|
|
||||||
SDL_GL_DestroyContext(gl_ctx);
|
SDL_GL_DestroyContext(gl_ctx);
|
||||||
SDL_DestroyWindow(window);
|
SDL_DestroyWindow(window);
|
||||||
SDL_Quit();
|
SDL_Quit();
|
||||||
}
|
}
|
||||||
|
|
||||||
create_program :: (vert_src: [:0]u8, frag_src: [:0]u8) -> u32 {
|
|
||||||
vs := compile_shader(GL_VERTEX_SHADER, vert_src);
|
|
||||||
fs := compile_shader(GL_FRAGMENT_SHADER, frag_src);
|
|
||||||
|
|
||||||
prog := glCreateProgram();
|
|
||||||
glAttachShader(prog, vs);
|
|
||||||
glAttachShader(prog, fs);
|
|
||||||
glLinkProgram(prog);
|
|
||||||
|
|
||||||
status : s32 = 0;
|
|
||||||
glGetProgramiv(prog, GL_LINK_STATUS, @status);
|
|
||||||
if status == GL_FALSE {
|
|
||||||
log_buf: [512]u8 = ---;
|
|
||||||
glGetProgramInfoLog(prog, 512, null, log_buf);
|
|
||||||
print("error program link\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
glDeleteShader(vs);
|
|
||||||
glDeleteShader(fs);
|
|
||||||
return prog;
|
|
||||||
}
|
|
||||||
|
|
||||||
compile_shader :: (shader_type : u32, source: [:0]u8) -> u32 {
|
|
||||||
shader := glCreateShader(shader_type);
|
|
||||||
glShaderSource(shader, 1, source, null);
|
|
||||||
glCompileShader(shader);
|
|
||||||
|
|
||||||
status : s32 = 0;
|
|
||||||
glGetShaderiv(shader, GL_COMPILE_STATUS, @status);
|
|
||||||
if status == GL_FALSE {
|
|
||||||
log_buf : [512]u8 = ---;
|
|
||||||
glGetShaderInfoLog(shader, 512, null, log_buf);
|
|
||||||
print("error compile shader\n");
|
|
||||||
}
|
|
||||||
return shader;
|
|
||||||
}
|
|
||||||
|
|
||||||
VERT_SHADER_SRC : [:0]u8 = #string GLSL
|
|
||||||
#version 330 core
|
|
||||||
layout (location = 0) in vec3 aPos;
|
|
||||||
layout (location = 1) in vec3 aNormal;
|
|
||||||
uniform mat4 uMVP;
|
|
||||||
out vec3 vNormal;
|
|
||||||
out vec3 vPos;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
gl_Position = uMVP * vec4(aPos, 1.0);
|
|
||||||
vNormal = aNormal;
|
|
||||||
vPos = aPos;
|
|
||||||
}
|
|
||||||
GLSL;
|
|
||||||
|
|
||||||
FRAG_SHADER_SRC : [:0]u8 = #string GLSL
|
|
||||||
#version 330 core
|
|
||||||
in vec3 vNormal;
|
|
||||||
in vec3 vPos;
|
|
||||||
out vec4 FragColor;
|
|
||||||
uniform vec3 uLightDir;
|
|
||||||
uniform float uWire;
|
|
||||||
void main() {
|
|
||||||
if (uWire > 0.5) {
|
|
||||||
FragColor = vec4(0.05, 0.05, 0.05, 1.0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
vec3 n = normalize(vNormal);
|
|
||||||
vec3 l = normalize(uLightDir);
|
|
||||||
float diff = max(dot(n, l), 0.15);
|
|
||||||
float cx = floor(vPos.x * 2.0 + 0.001);
|
|
||||||
float cy = floor(vPos.y * 2.0 + 0.001);
|
|
||||||
float cz = floor(vPos.z * 2.0 + 0.001);
|
|
||||||
float check = mod(cx + cy + cz, 2.0);
|
|
||||||
vec3 col1 = vec3(0.9, 0.5, 0.2);
|
|
||||||
vec3 col2 = vec3(0.2, 0.6, 0.9);
|
|
||||||
vec3 base = mix(col1, col2, check);
|
|
||||||
FragColor = vec4(base * diff, 1.0);
|
|
||||||
}
|
|
||||||
GLSL;
|
|
||||||
|
|||||||
@@ -1,155 +0,0 @@
|
|||||||
#import "std.sx";
|
|
||||||
|
|
||||||
// --- Allocator protocol ---
|
|
||||||
|
|
||||||
Allocator :: struct {
|
|
||||||
ctx: *void;
|
|
||||||
alloc_fn: (*void, s64) -> *void;
|
|
||||||
free_fn: (*void, *void) -> void;
|
|
||||||
}
|
|
||||||
|
|
||||||
allocator_alloc :: (a: Allocator, size: s64) -> *void {
|
|
||||||
a.alloc_fn(a.ctx, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
allocator_dealloc :: (a: Allocator, ptr: *void) {
|
|
||||||
a.free_fn(a.ctx, ptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
alloc :: ufcs allocator_alloc;
|
|
||||||
dealloc :: ufcs allocator_dealloc;
|
|
||||||
|
|
||||||
// --- GPA: general purpose allocator (malloc/free wrapper) ---
|
|
||||||
|
|
||||||
GPA :: struct {
|
|
||||||
alloc_count: s64;
|
|
||||||
}
|
|
||||||
|
|
||||||
gpa_alloc :: (ctx: *void, size: s64) -> *void {
|
|
||||||
gpa : *GPA = xx ctx;
|
|
||||||
gpa.alloc_count += 1;
|
|
||||||
malloc(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
gpa_free :: (ctx: *void, ptr: *void) {
|
|
||||||
gpa : *GPA = xx ctx;
|
|
||||||
gpa.alloc_count -= 1;
|
|
||||||
free(ptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
gpa_create :: (gpa: *GPA) -> Allocator {
|
|
||||||
Allocator.{ ctx = gpa, alloc_fn = gpa_alloc, free_fn = gpa_free };
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Arena: multi-chunk bump allocator (Zig-inspired) ---
|
|
||||||
|
|
||||||
ArenaChunk :: struct {
|
|
||||||
next: *ArenaChunk;
|
|
||||||
cap: s64;
|
|
||||||
}
|
|
||||||
|
|
||||||
Arena :: struct {
|
|
||||||
first: *ArenaChunk;
|
|
||||||
end_index: s64;
|
|
||||||
parent: Allocator;
|
|
||||||
}
|
|
||||||
|
|
||||||
arena_add_chunk :: (a: *Arena, min_size: s64) {
|
|
||||||
prev_cap := if a.first != null then a.first.cap else 0;
|
|
||||||
needed := min_size + 16 + 16;
|
|
||||||
len := (prev_cap + needed) * 3 / 2;
|
|
||||||
raw := a.parent.alloc(len);
|
|
||||||
chunk : *ArenaChunk = xx raw;
|
|
||||||
chunk.next = a.first;
|
|
||||||
chunk.cap = len;
|
|
||||||
a.first = chunk;
|
|
||||||
a.end_index = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
arena_alloc :: (ctx: *void, size: s64) -> *void {
|
|
||||||
a : *Arena = xx ctx;
|
|
||||||
aligned := (size + 7) & (0 - 8);
|
|
||||||
if a.first != null {
|
|
||||||
usable := a.first.cap - 16;
|
|
||||||
if a.end_index + aligned <= usable {
|
|
||||||
buf : [*]u8 = xx a.first;
|
|
||||||
ptr := @buf[16 + a.end_index];
|
|
||||||
a.end_index = a.end_index + aligned;
|
|
||||||
return ptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
arena_add_chunk(a, aligned);
|
|
||||||
buf : [*]u8 = xx a.first;
|
|
||||||
ptr := @buf[16 + a.end_index];
|
|
||||||
a.end_index = a.end_index + aligned;
|
|
||||||
ptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
arena_free :: (ctx: *void, ptr: *void) {
|
|
||||||
}
|
|
||||||
|
|
||||||
arena_create :: (a: *Arena, parent: Allocator, size: s64) -> Allocator {
|
|
||||||
a.first = null;
|
|
||||||
a.end_index = 0;
|
|
||||||
a.parent = parent;
|
|
||||||
arena_add_chunk(a, size);
|
|
||||||
Allocator.{ ctx = a, alloc_fn = arena_alloc, free_fn = arena_free };
|
|
||||||
}
|
|
||||||
|
|
||||||
arena_reset :: (a: *Arena) {
|
|
||||||
// Keep first chunk (newest/largest), free the rest
|
|
||||||
if a.first != null {
|
|
||||||
it := a.first.next;
|
|
||||||
while it != null {
|
|
||||||
next := it.next;
|
|
||||||
a.parent.dealloc(it);
|
|
||||||
it = next;
|
|
||||||
}
|
|
||||||
a.first.next = null;
|
|
||||||
}
|
|
||||||
a.end_index = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
arena_deinit :: (a: *Arena) {
|
|
||||||
it := a.first;
|
|
||||||
while it != null {
|
|
||||||
next := it.next;
|
|
||||||
a.parent.dealloc(it);
|
|
||||||
it = next;
|
|
||||||
}
|
|
||||||
a.first = null;
|
|
||||||
a.end_index = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- BufAlloc: bump allocator backed by a user-provided slice ---
|
|
||||||
|
|
||||||
BufAlloc :: struct {
|
|
||||||
buf: [*]u8;
|
|
||||||
len: s64;
|
|
||||||
pos: s64;
|
|
||||||
}
|
|
||||||
|
|
||||||
buf_alloc :: (ctx: *void, size: s64) -> *void {
|
|
||||||
b : *BufAlloc = xx ctx;
|
|
||||||
aligned := (size + 7) & (0 - 8);
|
|
||||||
if b.pos + aligned > b.len {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
ptr := @b.buf[b.pos];
|
|
||||||
b.pos = b.pos + aligned;
|
|
||||||
ptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
buf_free :: (ctx: *void, ptr: *void) {
|
|
||||||
}
|
|
||||||
|
|
||||||
buf_create :: (b: *BufAlloc, buf: [*]u8, len: s64) -> Allocator {
|
|
||||||
b.buf = buf;
|
|
||||||
b.len = len;
|
|
||||||
b.pos = 0;
|
|
||||||
Allocator.{ ctx = b, alloc_fn = buf_alloc, free_fn = buf_free };
|
|
||||||
}
|
|
||||||
|
|
||||||
buf_reset :: (b: *BufAlloc) {
|
|
||||||
b.pos = 0;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
PI :f32: 3.14159265;
|
|
||||||
|
|
||||||
sqrt :: (x: $T) -> T #builtin;
|
|
||||||
sin :: (x: $T) -> T #builtin;
|
|
||||||
cos :: (x: $T) -> T #builtin;
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
#import "math.sx";
|
|
||||||
|
|
||||||
Matrix44 :: union {
|
|
||||||
data: [16]f32;
|
|
||||||
struct { c0, c1, c2, c3: Vector(4, f32); };
|
|
||||||
}
|
|
||||||
|
|
||||||
mat4_perspective :: (fov: f32, aspect: f32, near: f32, far: f32) -> Matrix44 {
|
|
||||||
half := fov / 2.0;
|
|
||||||
f := cos(half) / sin(half);
|
|
||||||
m : Matrix44 = ---;
|
|
||||||
m.c0 = .[f / aspect, 0.0, 0.0, 0.0];
|
|
||||||
m.c1 = .[0.0, f, 0.0, 0.0];
|
|
||||||
m.c2 = .[0.0, 0.0, (far + near) / (near - far), -1.0];
|
|
||||||
m.c3 = .[0.0, 0.0, (2.0 * far * near) / (near - far), 0.0];
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
|
|
||||||
mat4_translate :: (tx: f32, ty: f32, tz: f32) -> Matrix44 {
|
|
||||||
m : Matrix44 = ---;
|
|
||||||
m.c0 = .[1.0, 0.0, 0.0, 0.0];
|
|
||||||
m.c1 = .[0.0, 1.0, 0.0, 0.0];
|
|
||||||
m.c2 = .[0.0, 0.0, 1.0, 0.0];
|
|
||||||
m.c3 = .[tx, ty, tz, 1.0];
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
|
|
||||||
mat4_rotate_y :: (angle: f32) -> Matrix44 {
|
|
||||||
c := cos(angle);
|
|
||||||
s := sin(angle);
|
|
||||||
m : Matrix44 = ---;
|
|
||||||
m.c0 = .[c, 0.0, 0.0 - s, 0.0];
|
|
||||||
m.c1 = .[0.0, 1.0, 0.0, 0.0];
|
|
||||||
m.c2 = .[s, 0.0, c, 0.0];
|
|
||||||
m.c3 = .[0.0, 0.0, 0.0, 1.0];
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
|
|
||||||
mat4_rotate_x :: (angle: f32) -> Matrix44 {
|
|
||||||
c := cos(angle);
|
|
||||||
s := sin(angle);
|
|
||||||
m : Matrix44 = ---;
|
|
||||||
m.c0 = .[1.0, 0.0, 0.0, 0.0];
|
|
||||||
m.c1 = .[0.0, c, s, 0.0];
|
|
||||||
m.c2 = .[0.0, 0.0 - s, c, 0.0];
|
|
||||||
m.c3 = .[0.0, 0.0, 0.0, 1.0];
|
|
||||||
m;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
mat4_multiply :: (a: *Matrix44, b: *Matrix44) -> Matrix44 {
|
|
||||||
out: Matrix44 = ---;
|
|
||||||
out.c0 = a.c0 * b.c0.x + a.c1 * b.c0.y + a.c2 * b.c0.z + a.c3 * b.c0.w;
|
|
||||||
out.c1 = a.c0 * b.c1.x + a.c1 * b.c1.y + a.c2 * b.c1.z + a.c3 * b.c1.w;
|
|
||||||
out.c2 = a.c0 * b.c2.x + a.c1 * b.c2.y + a.c2 * b.c2.z + a.c3 * b.c2.w;
|
|
||||||
out.c3 = a.c0 * b.c3.x + a.c1 * b.c3.y + a.c2 * b.c3.z + a.c3 * b.c3.w;
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
multiply :: ufcs mat4_multiply;
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
#import "math.sx";
|
|
||||||
|
|
||||||
vec3_dot :: (a: Vector(3,f32), b: Vector(3,f32)) -> f32 {
|
|
||||||
return a.x*b.x + a.y*b.y + a.z*b.z;
|
|
||||||
}
|
|
||||||
dot :: ufcs vec3_dot;
|
|
||||||
|
|
||||||
vec3_cross :: (a: Vector(3,f32), b: Vector(3,f32)) -> Vector(3,f32) {
|
|
||||||
.[a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a.x*b.y - a.y*b.x];
|
|
||||||
}
|
|
||||||
cross :: ufcs vec3_cross;
|
|
||||||
|
|
||||||
vec3_length :: (v: Vector(3,f32)) -> f32 {
|
|
||||||
return sqrt(dot(v, v));
|
|
||||||
}
|
|
||||||
length :: ufcs vec3_length;
|
|
||||||
|
|
||||||
|
|
||||||
vec3_normal :: (v: Vector(3,f32)) -> Vector(3,f32) {
|
|
||||||
return v / length(v);
|
|
||||||
}
|
|
||||||
normal :: ufcs vec3_normal;
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
// OpenGL 3.3 Core — runtime-loaded function pointers
|
|
||||||
// No #library needed — caller provides a loader (e.g. SDL_GL_GetProcAddress)
|
|
||||||
|
|
||||||
// Constants
|
|
||||||
GL_FALSE :s32: 0;
|
|
||||||
GL_TRUE :s32: 1;
|
|
||||||
GL_DEPTH_TEST :u32: 0x0B71;
|
|
||||||
GL_CULL_FACE :u32: 0x0B44;
|
|
||||||
GL_BLEND :u32: 0x0BE2;
|
|
||||||
GL_TRIANGLES :u32: 4;
|
|
||||||
GL_LINES :u32: 1;
|
|
||||||
GL_FLOAT :u32: 0x1406;
|
|
||||||
GL_UNSIGNED_INT :u32: 0x1405;
|
|
||||||
GL_VERTEX_SHADER :u32: 0x8B31;
|
|
||||||
GL_FRAGMENT_SHADER :u32: 0x8B30;
|
|
||||||
GL_COMPILE_STATUS :u32: 0x8B81;
|
|
||||||
GL_LINK_STATUS :u32: 0x8B82;
|
|
||||||
GL_ARRAY_BUFFER :u32: 0x8892;
|
|
||||||
GL_ELEMENT_ARRAY_BUFFER :u32: 0x8893;
|
|
||||||
GL_STATIC_DRAW :u32: 0x88E4;
|
|
||||||
GL_COLOR_BUFFER_BIT :u32: 0x4000;
|
|
||||||
GL_DEPTH_BUFFER_BIT :u32: 0x0100;
|
|
||||||
GL_FRONT_AND_BACK :u32: 0x0408;
|
|
||||||
GL_LINE :u32: 0x1B01;
|
|
||||||
GL_FILL :u32: 0x1B02;
|
|
||||||
|
|
||||||
// Function pointer variables (mutable, loaded at runtime)
|
|
||||||
glClearColor : (f32, f32, f32, f32) -> void = ---;
|
|
||||||
glClear : (u32) -> void = ---;
|
|
||||||
glEnable : (u32) -> void = ---;
|
|
||||||
glDisable : (u32) -> void = ---;
|
|
||||||
glViewport : (s32, s32, s32, s32) -> void = ---;
|
|
||||||
glDrawArrays : (u32, s32, s32) -> void = ---;
|
|
||||||
glPolygonMode : (u32, u32) -> void = ---;
|
|
||||||
glLineWidth : (f32) -> void = ---;
|
|
||||||
glCreateShader : (u32) -> u32 = ---;
|
|
||||||
glShaderSource : (u32, s32, *[:0]u8, *s32) -> void = ---;
|
|
||||||
glCompileShader : (u32) -> void = ---;
|
|
||||||
glGetShaderiv : (u32, u32, *s32) -> void = ---;
|
|
||||||
glGetShaderInfoLog : (u32, s32, *s32, [*]u8) -> void = ---;
|
|
||||||
glCreateProgram : () -> u32 = ---;
|
|
||||||
glAttachShader : (u32, u32) -> void = ---;
|
|
||||||
glLinkProgram : (u32) -> void = ---;
|
|
||||||
glGetProgramiv : (u32, u32, *s32) -> void = ---;
|
|
||||||
glGetProgramInfoLog : (u32, s32, *s32, [*]u8) -> void = ---;
|
|
||||||
glUseProgram : (u32) -> void = ---;
|
|
||||||
glDeleteShader : (u32) -> void = ---;
|
|
||||||
glGenVertexArrays : (s32, *u32) -> void = ---;
|
|
||||||
glGenBuffers : (s32, *u32) -> void = ---;
|
|
||||||
glBindVertexArray : (u32) -> void = ---;
|
|
||||||
glBindBuffer : (u32, u32) -> void = ---;
|
|
||||||
glBufferData : (u32, s64, *void, u32) -> void = ---;
|
|
||||||
glVertexAttribPointer : (u32, s32, u32, u8, s32, *void) -> void = ---;
|
|
||||||
glEnableVertexAttribArray : (u32) -> void = ---;
|
|
||||||
glGetUniformLocation : (u32, [:0]u8) -> s32 = ---;
|
|
||||||
glUniformMatrix4fv : (s32, s32, u8, [16]f32) -> void = ---;
|
|
||||||
glUniform3f : (s32, f32, f32, f32) -> void = ---;
|
|
||||||
glDepthFunc : (u32) -> void = ---;
|
|
||||||
glUniform1f : (s32, f32) -> void = ---;
|
|
||||||
GL_LESS :u32: 0x0201;
|
|
||||||
GL_LEQUAL :u32: 0x0203;
|
|
||||||
|
|
||||||
// Loader: call once after creating GL context
|
|
||||||
// Pass in a proc loader (e.g. SDL_GL_GetProcAddress)
|
|
||||||
load_gl :: (get_proc: ([:0]u8) -> *void) {
|
|
||||||
glClearColor = xx get_proc("glClearColor");
|
|
||||||
glClear = xx get_proc("glClear");
|
|
||||||
glEnable = xx get_proc("glEnable");
|
|
||||||
glDisable = xx get_proc("glDisable");
|
|
||||||
glViewport = xx get_proc("glViewport");
|
|
||||||
glDrawArrays = xx get_proc("glDrawArrays");
|
|
||||||
glPolygonMode = xx get_proc("glPolygonMode");
|
|
||||||
glLineWidth = xx get_proc("glLineWidth");
|
|
||||||
glCreateShader = xx get_proc("glCreateShader");
|
|
||||||
glShaderSource = xx get_proc("glShaderSource");
|
|
||||||
glCompileShader = xx get_proc("glCompileShader");
|
|
||||||
glGetShaderiv = xx get_proc("glGetShaderiv");
|
|
||||||
glGetShaderInfoLog = xx get_proc("glGetShaderInfoLog");
|
|
||||||
glCreateProgram = xx get_proc("glCreateProgram");
|
|
||||||
glAttachShader = xx get_proc("glAttachShader");
|
|
||||||
glLinkProgram = xx get_proc("glLinkProgram");
|
|
||||||
glGetProgramiv = xx get_proc("glGetProgramiv");
|
|
||||||
glGetProgramInfoLog = xx get_proc("glGetProgramInfoLog");
|
|
||||||
glUseProgram = xx get_proc("glUseProgram");
|
|
||||||
glDeleteShader = xx get_proc("glDeleteShader");
|
|
||||||
glGenVertexArrays = xx get_proc("glGenVertexArrays");
|
|
||||||
glGenBuffers = xx get_proc("glGenBuffers");
|
|
||||||
glBindVertexArray = xx get_proc("glBindVertexArray");
|
|
||||||
glBindBuffer = xx get_proc("glBindBuffer");
|
|
||||||
glBufferData = xx get_proc("glBufferData");
|
|
||||||
glVertexAttribPointer = xx get_proc("glVertexAttribPointer");
|
|
||||||
glEnableVertexAttribArray = xx get_proc("glEnableVertexAttribArray");
|
|
||||||
glGetUniformLocation = xx get_proc("glGetUniformLocation");
|
|
||||||
glUniformMatrix4fv = xx get_proc("glUniformMatrix4fv");
|
|
||||||
glUniform3f = xx get_proc("glUniform3f");
|
|
||||||
glDepthFunc = xx get_proc("glDepthFunc");
|
|
||||||
glUniform1f = xx get_proc("glUniform1f");
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
raylib :: #library "raylib";
|
|
||||||
|
|
||||||
Color :: struct {
|
|
||||||
r, g, b, a: u8;
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector2 :: struct {
|
|
||||||
x, y: f32;
|
|
||||||
}
|
|
||||||
|
|
||||||
InitWindow :: (width: s32, height: s32, title: [:0]u8) -> void #foreign raylib;
|
|
||||||
CloseWindow :: () -> void #foreign raylib;
|
|
||||||
WindowShouldClose :: () -> bool #foreign raylib;
|
|
||||||
BeginDrawing :: () -> void #foreign raylib;
|
|
||||||
EndDrawing :: () -> void #foreign raylib;
|
|
||||||
ClearBackground :: (color: Color) -> void #foreign raylib;
|
|
||||||
DrawTriangle :: (v1: Vector2, v2: Vector2, v3: Vector2, color: Color) -> void #foreign raylib;
|
|
||||||
334
modules/sdl3.sx
334
modules/sdl3.sx
@@ -1,334 +0,0 @@
|
|||||||
sdl3 :: #library "SDL3";
|
|
||||||
|
|
||||||
// SDL_InitFlags
|
|
||||||
SDL_INIT_VIDEO :u32: 0x20;
|
|
||||||
|
|
||||||
// SDL_WindowFlags
|
|
||||||
SDL_WINDOW_OPENGL :u64: 0x2;
|
|
||||||
|
|
||||||
// SDL_GLAttr (enum starting at 0)
|
|
||||||
SDL_GL_DOUBLEBUFFER :s32: 5;
|
|
||||||
SDL_GL_DEPTH_SIZE :s32: 6;
|
|
||||||
SDL_GL_CONTEXT_MAJOR_VERSION :s32: 17;
|
|
||||||
SDL_GL_CONTEXT_MINOR_VERSION :s32: 18;
|
|
||||||
SDL_GL_CONTEXT_FLAGS :s32: 19;
|
|
||||||
SDL_GL_CONTEXT_PROFILE_MASK :s32: 20;
|
|
||||||
|
|
||||||
// SDL_GLProfile
|
|
||||||
SDL_GL_CONTEXT_PROFILE_CORE :s32: 0x1;
|
|
||||||
|
|
||||||
// SDL_GLContextFlag
|
|
||||||
SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG :s32: 0x2;
|
|
||||||
|
|
||||||
// SDL_Keycode — virtual key codes (layout-dependent)
|
|
||||||
SDL_Keycode :: enum u32 {
|
|
||||||
// Common
|
|
||||||
unknown :: 0x00;
|
|
||||||
return_key :: 0x0D;
|
|
||||||
escape :: 0x1B;
|
|
||||||
backspace :: 0x08;
|
|
||||||
tab :: 0x09;
|
|
||||||
space :: 0x20;
|
|
||||||
delete_key :: 0x7F;
|
|
||||||
|
|
||||||
// Punctuation
|
|
||||||
exclaim :: 0x21;
|
|
||||||
double_quote :: 0x22;
|
|
||||||
hash :: 0x23;
|
|
||||||
dollar :: 0x24;
|
|
||||||
percent :: 0x25;
|
|
||||||
ampersand :: 0x26;
|
|
||||||
apostrophe :: 0x27;
|
|
||||||
leftparen :: 0x28;
|
|
||||||
rightparen :: 0x29;
|
|
||||||
asterisk :: 0x2A;
|
|
||||||
plus :: 0x2B;
|
|
||||||
comma :: 0x2C;
|
|
||||||
minus :: 0x2D;
|
|
||||||
period :: 0x2E;
|
|
||||||
slash :: 0x2F;
|
|
||||||
colon :: 0x3A;
|
|
||||||
semicolon :: 0x3B;
|
|
||||||
less :: 0x3C;
|
|
||||||
equals :: 0x3D;
|
|
||||||
greater :: 0x3E;
|
|
||||||
question :: 0x3F;
|
|
||||||
at :: 0x40;
|
|
||||||
leftbracket :: 0x5B;
|
|
||||||
backslash :: 0x5C;
|
|
||||||
rightbracket :: 0x5D;
|
|
||||||
caret :: 0x5E;
|
|
||||||
underscore :: 0x5F;
|
|
||||||
grave :: 0x60;
|
|
||||||
leftbrace :: 0x7B;
|
|
||||||
pipe :: 0x7C;
|
|
||||||
rightbrace :: 0x7D;
|
|
||||||
tilde :: 0x7E;
|
|
||||||
plusminus :: 0xB1;
|
|
||||||
|
|
||||||
// Numbers
|
|
||||||
key_0 :: 0x30;
|
|
||||||
key_1 :: 0x31;
|
|
||||||
key_2 :: 0x32;
|
|
||||||
key_3 :: 0x33;
|
|
||||||
key_4 :: 0x34;
|
|
||||||
key_5 :: 0x35;
|
|
||||||
key_6 :: 0x36;
|
|
||||||
key_7 :: 0x37;
|
|
||||||
key_8 :: 0x38;
|
|
||||||
key_9 :: 0x39;
|
|
||||||
|
|
||||||
// Letters
|
|
||||||
a :: 0x61;
|
|
||||||
b :: 0x62;
|
|
||||||
c :: 0x63;
|
|
||||||
d :: 0x64;
|
|
||||||
e :: 0x65;
|
|
||||||
f :: 0x66;
|
|
||||||
g :: 0x67;
|
|
||||||
h :: 0x68;
|
|
||||||
i :: 0x69;
|
|
||||||
j :: 0x6A;
|
|
||||||
k :: 0x6B;
|
|
||||||
l :: 0x6C;
|
|
||||||
m :: 0x6D;
|
|
||||||
n :: 0x6E;
|
|
||||||
o :: 0x6F;
|
|
||||||
p :: 0x70;
|
|
||||||
q :: 0x71;
|
|
||||||
r :: 0x72;
|
|
||||||
s :: 0x73;
|
|
||||||
t :: 0x74;
|
|
||||||
u :: 0x75;
|
|
||||||
v :: 0x76;
|
|
||||||
w :: 0x77;
|
|
||||||
x :: 0x78;
|
|
||||||
y :: 0x79;
|
|
||||||
z :: 0x7A;
|
|
||||||
|
|
||||||
// Function keys
|
|
||||||
f1 :: 0x4000003A;
|
|
||||||
f2 :: 0x4000003B;
|
|
||||||
f3 :: 0x4000003C;
|
|
||||||
f4 :: 0x4000003D;
|
|
||||||
f5 :: 0x4000003E;
|
|
||||||
f6 :: 0x4000003F;
|
|
||||||
f7 :: 0x40000040;
|
|
||||||
f8 :: 0x40000041;
|
|
||||||
f9 :: 0x40000042;
|
|
||||||
f10 :: 0x40000043;
|
|
||||||
f11 :: 0x40000044;
|
|
||||||
f12 :: 0x40000045;
|
|
||||||
f13 :: 0x40000068;
|
|
||||||
f14 :: 0x40000069;
|
|
||||||
f15 :: 0x4000006A;
|
|
||||||
f16 :: 0x4000006B;
|
|
||||||
f17 :: 0x4000006C;
|
|
||||||
f18 :: 0x4000006D;
|
|
||||||
f19 :: 0x4000006E;
|
|
||||||
f20 :: 0x4000006F;
|
|
||||||
f21 :: 0x40000070;
|
|
||||||
f22 :: 0x40000071;
|
|
||||||
f23 :: 0x40000072;
|
|
||||||
f24 :: 0x40000073;
|
|
||||||
|
|
||||||
// Navigation
|
|
||||||
capslock :: 0x40000039;
|
|
||||||
printscreen :: 0x40000046;
|
|
||||||
scrolllock :: 0x40000047;
|
|
||||||
pause :: 0x40000048;
|
|
||||||
insert :: 0x40000049;
|
|
||||||
home :: 0x4000004A;
|
|
||||||
pageup :: 0x4000004B;
|
|
||||||
end :: 0x4000004D;
|
|
||||||
pagedown :: 0x4000004E;
|
|
||||||
right :: 0x4000004F;
|
|
||||||
left :: 0x40000050;
|
|
||||||
down :: 0x40000051;
|
|
||||||
up :: 0x40000052;
|
|
||||||
|
|
||||||
// Keypad
|
|
||||||
numlock :: 0x40000053;
|
|
||||||
kp_divide :: 0x40000054;
|
|
||||||
kp_multiply :: 0x40000055;
|
|
||||||
kp_minus :: 0x40000056;
|
|
||||||
kp_plus :: 0x40000057;
|
|
||||||
kp_enter :: 0x40000058;
|
|
||||||
kp_1 :: 0x40000059;
|
|
||||||
kp_2 :: 0x4000005A;
|
|
||||||
kp_3 :: 0x4000005B;
|
|
||||||
kp_4 :: 0x4000005C;
|
|
||||||
kp_5 :: 0x4000005D;
|
|
||||||
kp_6 :: 0x4000005E;
|
|
||||||
kp_7 :: 0x4000005F;
|
|
||||||
kp_8 :: 0x40000060;
|
|
||||||
kp_9 :: 0x40000061;
|
|
||||||
kp_0 :: 0x40000062;
|
|
||||||
kp_period :: 0x40000063;
|
|
||||||
kp_equals :: 0x40000067;
|
|
||||||
kp_comma :: 0x40000085;
|
|
||||||
|
|
||||||
// Modifiers
|
|
||||||
lctrl :: 0x400000E0;
|
|
||||||
lshift :: 0x400000E1;
|
|
||||||
lalt :: 0x400000E2;
|
|
||||||
lgui :: 0x400000E3;
|
|
||||||
rctrl :: 0x400000E4;
|
|
||||||
rshift :: 0x400000E5;
|
|
||||||
ralt :: 0x400000E6;
|
|
||||||
rgui :: 0x400000E7;
|
|
||||||
mode :: 0x40000101;
|
|
||||||
|
|
||||||
// Editing
|
|
||||||
undo :: 0x4000007A;
|
|
||||||
cut :: 0x4000007B;
|
|
||||||
copy :: 0x4000007C;
|
|
||||||
paste :: 0x4000007D;
|
|
||||||
find :: 0x4000007E;
|
|
||||||
|
|
||||||
// Media
|
|
||||||
mute :: 0x4000007F;
|
|
||||||
volumeup :: 0x40000080;
|
|
||||||
volumedown :: 0x40000081;
|
|
||||||
media_play :: 0x40000106;
|
|
||||||
media_pause :: 0x40000107;
|
|
||||||
media_fast_forward :: 0x40000109;
|
|
||||||
media_rewind :: 0x4000010A;
|
|
||||||
media_next_track :: 0x4000010B;
|
|
||||||
media_previous_track :: 0x4000010C;
|
|
||||||
media_stop :: 0x4000010D;
|
|
||||||
media_eject :: 0x4000010E;
|
|
||||||
media_play_pause :: 0x4000010F;
|
|
||||||
|
|
||||||
// System
|
|
||||||
application :: 0x40000065;
|
|
||||||
power :: 0x40000066;
|
|
||||||
execute :: 0x40000074;
|
|
||||||
help :: 0x40000075;
|
|
||||||
menu :: 0x40000076;
|
|
||||||
select :: 0x40000077;
|
|
||||||
sleep :: 0x40000102;
|
|
||||||
wake :: 0x40000103;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event payload structs — match SDL3 layout from byte 4 onward (after the u32 type tag)
|
|
||||||
// Common header: reserved (u32), timestamp (u64)
|
|
||||||
|
|
||||||
SDL_WindowData :: struct {
|
|
||||||
timestamp: u64; // event time in nanoseconds
|
|
||||||
window_id: u32;
|
|
||||||
data1: s32; // event-dependent: x position for moved, width for resized
|
|
||||||
data2: s32; // event-dependent: y position for moved, height for resized
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_Keymod :: enum flags u16 {
|
|
||||||
lshift :: 0b0000_0000_0000_0001; // left Shift
|
|
||||||
rshift :: 0b0000_0000_0000_0010; // right Shift
|
|
||||||
level5 :: 0b0000_0000_0000_0100; // Level 5 Shift
|
|
||||||
lctrl :: 0b0000_0000_0100_0000; // left Ctrl
|
|
||||||
rctrl :: 0b0000_0000_1000_0000; // right Ctrl
|
|
||||||
lalt :: 0b0000_0001_0000_0000; // left Alt
|
|
||||||
ralt :: 0b0000_0010_0000_0000; // right Alt
|
|
||||||
lgui :: 0b0000_0100_0000_0000; // left GUI (Windows/Cmd key)
|
|
||||||
rgui :: 0b0000_1000_0000_0000; // right GUI (Windows/Cmd key)
|
|
||||||
num :: 0b0001_0000_0000_0000; // Num Lock
|
|
||||||
caps :: 0b0010_0000_0000_0000; // Caps Lock
|
|
||||||
mode :: 0b0100_0000_0000_0000; // AltGr
|
|
||||||
scroll :: 0b1000_0000_0000_0000; // Scroll Lock
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_KeyData :: struct {
|
|
||||||
timestamp: u64; // event time in nanoseconds
|
|
||||||
window_id: u32; // window with keyboard focus
|
|
||||||
which: u32; // keyboard instance id, 0 if unknown or virtual
|
|
||||||
scancode: u32; // physical key code (layout-independent)
|
|
||||||
key: SDL_Keycode; // virtual key code (layout-dependent)
|
|
||||||
mod: SDL_Keymod; // active modifier keys
|
|
||||||
raw: u16; // platform-specific scancode
|
|
||||||
down: u8; // 1 if pressed, 0 if released
|
|
||||||
repeat: u8; // 1 if this is a key repeat
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_MouseMotionData :: struct {
|
|
||||||
timestamp: u64; // event time in nanoseconds
|
|
||||||
window_id: u32; // window with mouse focus
|
|
||||||
which: u32; // mouse instance id, 0 for touch events
|
|
||||||
state: u32; // button state bitmask (bit 0 = left, 1 = middle, 2 = right)
|
|
||||||
x: f32; // x position relative to window
|
|
||||||
y: f32; // y position relative to window
|
|
||||||
xrel: f32; // relative motion in x
|
|
||||||
yrel: f32; // relative motion in y
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_MouseButtonData :: struct {
|
|
||||||
timestamp: u64; // event time in nanoseconds
|
|
||||||
window_id: u32; // window with mouse focus
|
|
||||||
which: u32; // mouse instance id, 0 for touch events
|
|
||||||
button: u8; // button index (1 = left, 2 = middle, 3 = right)
|
|
||||||
down: u8; // 1 if pressed, 0 if released
|
|
||||||
clicks: u8; // 1 for single-click, 2 for double-click, etc.
|
|
||||||
_: u8;
|
|
||||||
x: f32; // x position relative to window
|
|
||||||
y: f32; // y position relative to window
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_MouseWheelData :: struct {
|
|
||||||
timestamp: u64; // event time in nanoseconds
|
|
||||||
window_id: u32; // window with mouse focus
|
|
||||||
which: u32; // mouse instance id
|
|
||||||
x: f32; // horizontal scroll (positive = right)
|
|
||||||
y: f32; // vertical scroll (positive = away from user)
|
|
||||||
direction: u32; // 0 = normal, 1 = flipped (multiply by -1 to normalize)
|
|
||||||
mouse_x: f32; // mouse x position relative to window
|
|
||||||
mouse_y: f32; // mouse y position relative to window
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_Event :: enum struct { tag: u32; _: u32; payload: [30]u32; } {
|
|
||||||
none :: 0;
|
|
||||||
|
|
||||||
// Application
|
|
||||||
quit :: 0x100;
|
|
||||||
|
|
||||||
// Window
|
|
||||||
window_shown :: 0x202: SDL_WindowData;
|
|
||||||
window_hidden :: 0x203: SDL_WindowData;
|
|
||||||
window_exposed :: 0x204: SDL_WindowData;
|
|
||||||
window_moved :: 0x205: SDL_WindowData;
|
|
||||||
window_resized :: 0x206: SDL_WindowData;
|
|
||||||
window_minimized :: 0x209: SDL_WindowData;
|
|
||||||
window_maximized :: 0x20A: SDL_WindowData;
|
|
||||||
window_restored :: 0x20B: SDL_WindowData;
|
|
||||||
window_mouse_enter :: 0x20C: SDL_WindowData;
|
|
||||||
window_mouse_leave :: 0x20D: SDL_WindowData;
|
|
||||||
window_focus_gained :: 0x20E: SDL_WindowData;
|
|
||||||
window_focus_lost :: 0x20F: SDL_WindowData;
|
|
||||||
window_close_requested :: 0x210: SDL_WindowData;
|
|
||||||
window_destroyed :: 0x219: SDL_WindowData;
|
|
||||||
|
|
||||||
// Keyboard
|
|
||||||
key_down :: 0x300: SDL_KeyData;
|
|
||||||
key_up :: 0x301: SDL_KeyData;
|
|
||||||
|
|
||||||
// Mouse
|
|
||||||
mouse_motion :: 0x400: SDL_MouseMotionData;
|
|
||||||
mouse_button_down :: 0x401: SDL_MouseButtonData;
|
|
||||||
mouse_button_up :: 0x402: SDL_MouseButtonData;
|
|
||||||
mouse_wheel :: 0x403: SDL_MouseWheelData;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Functions
|
|
||||||
SDL_Init :: (flags: u32) -> bool #foreign sdl3;
|
|
||||||
SDL_Quit :: () -> void #foreign sdl3;
|
|
||||||
SDL_CreateWindow :: (title: [:0]u8, w: s32, h: s32, flags: u64) -> *void #foreign sdl3;
|
|
||||||
SDL_DestroyWindow :: (window: *void) -> void #foreign sdl3;
|
|
||||||
SDL_GL_SetAttribute :: (attr: s32, value: s32) -> bool #foreign sdl3;
|
|
||||||
SDL_GL_CreateContext :: (window: *void) -> *void #foreign sdl3;
|
|
||||||
SDL_GL_DestroyContext :: (context: *void) -> bool #foreign sdl3;
|
|
||||||
SDL_GL_MakeCurrent :: (window: *void, context: *void) -> bool #foreign sdl3;
|
|
||||||
SDL_GL_SwapWindow :: (window: *void) -> bool #foreign sdl3;
|
|
||||||
SDL_GL_SetSwapInterval :: (interval: s32) -> bool #foreign sdl3;
|
|
||||||
SDL_GL_GetProcAddress :: (proc: [:0]u8) -> *void #foreign sdl3;
|
|
||||||
SDL_PollEvent :: (event: *SDL_Event) -> bool #foreign sdl3;
|
|
||||||
SDL_GetTicks :: () -> u64 #foreign sdl3;
|
|
||||||
SDL_Delay :: (ms: u32) -> void #foreign sdl3;
|
|
||||||
SDL_GetError :: () -> [*]u8 #foreign sdl3;
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
// POSIX socket module (macOS only)
|
|
||||||
// sockaddr_in layout and constants are platform-specific.
|
|
||||||
|
|
||||||
libc :: #library "c";
|
|
||||||
|
|
||||||
// POSIX socket API
|
|
||||||
socket :: (domain: s32, kind: s32, protocol: s32) -> s32 #foreign libc;
|
|
||||||
setsockopt :: (fd: s32, level: s32, optname: s32, optval: *s32, optlen: u32) -> s32 #foreign libc;
|
|
||||||
bind :: (fd: s32, addr: *SockAddr, addrlen: u32) -> s32 #foreign libc;
|
|
||||||
listen :: (fd: s32, backlog: s32) -> s32 #foreign libc;
|
|
||||||
accept :: (fd: s32, addr: *SockAddr, addrlen: *u32) -> s32 #foreign libc;
|
|
||||||
read :: (fd: s32, buf: [*]u8, count: s64) -> s64 #foreign libc;
|
|
||||||
write :: (fd: s32, buf: [*]u8, count: s64) -> s64 #foreign libc;
|
|
||||||
close :: (fd: s32) -> s32 #foreign libc;
|
|
||||||
|
|
||||||
// Constants (macOS)
|
|
||||||
AF_INET :s32: 2;
|
|
||||||
SOCK_STREAM :s32: 1;
|
|
||||||
SOL_SOCKET :s32: 0xFFFF;
|
|
||||||
SO_REUSEADDR :s32: 0x4;
|
|
||||||
|
|
||||||
// macOS sockaddr_in (16 bytes, has sin_len field)
|
|
||||||
SockAddr :: struct {
|
|
||||||
sin_len: u8;
|
|
||||||
sin_family: u8;
|
|
||||||
sin_port: u16;
|
|
||||||
sin_addr: u32 = 0;
|
|
||||||
sin_zero: u64 = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
htons :: (port: s64) -> u16 {
|
|
||||||
cast(u16) (((port & 0xFF) << 8) | ((port >> 8) & 0xFF));
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#import c {
|
|
||||||
#include "vendors/stb_image/stb_image.h";
|
|
||||||
#include "vendors/stb_image/stb_image_write.h";
|
|
||||||
#source "vendors/stb_image/stb_image_impl.c";
|
|
||||||
#source "vendors/stb_image/stb_image_write_impl.c";
|
|
||||||
};
|
|
||||||
352
modules/std.sx
352
modules/std.sx
@@ -1,352 +0,0 @@
|
|||||||
Vector :: ($N: int, $T: Type) -> Type #builtin;
|
|
||||||
out :: (str: string) -> void #builtin;
|
|
||||||
size_of :: ($T: Type) -> s64 #builtin;
|
|
||||||
malloc :: (size: s64) -> *void #builtin;
|
|
||||||
memcpy :: (dst: *void, src: *void, size: s64) -> *void #builtin;
|
|
||||||
memset :: (dst: *void, val: s64, size: s64) -> void #builtin;
|
|
||||||
free :: (ptr: *void) -> void #builtin;
|
|
||||||
type_of :: (val: $T) -> Type #builtin;
|
|
||||||
type_name :: ($T: Type) -> string #builtin;
|
|
||||||
field_count :: ($T: Type) -> s64 #builtin;
|
|
||||||
field_name :: ($T: Type, idx: s64) -> string #builtin;
|
|
||||||
field_value :: (s: $T, idx: s64) -> Any #builtin;
|
|
||||||
is_flags :: ($T: Type) -> bool #builtin;
|
|
||||||
field_value_int :: ($T: Type, idx: s64) -> s64 #builtin;
|
|
||||||
field_index :: ($T: Type, val: T) -> s64 #builtin;
|
|
||||||
string :: []u8 #builtin;
|
|
||||||
|
|
||||||
#import "allocators.sx";
|
|
||||||
|
|
||||||
// --- Context ---
|
|
||||||
|
|
||||||
Context :: struct {
|
|
||||||
allocator: Allocator;
|
|
||||||
data: *void;
|
|
||||||
}
|
|
||||||
|
|
||||||
context : Context = ---;
|
|
||||||
|
|
||||||
// --- Slice & string allocation ---
|
|
||||||
|
|
||||||
context_alloc :: (size: s64) -> *void {
|
|
||||||
if context.allocator.ctx != null then context.allocator.alloc(size) else malloc(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
cstring :: (size: s64) -> string {
|
|
||||||
raw := context_alloc(size + 1);
|
|
||||||
memset(raw, 0, size + 1);
|
|
||||||
s : string = ---;
|
|
||||||
s.ptr = xx raw;
|
|
||||||
s.len = size;
|
|
||||||
s;
|
|
||||||
}
|
|
||||||
|
|
||||||
alloc_slice :: ($T: Type, count: s64) -> []T {
|
|
||||||
raw := context_alloc(count * size_of(T));
|
|
||||||
memset(raw, 0, count * size_of(T));
|
|
||||||
s : []T = ---;
|
|
||||||
s.ptr = xx raw;
|
|
||||||
s.len = count;
|
|
||||||
s;
|
|
||||||
}
|
|
||||||
|
|
||||||
int_to_string :: (n: s64) -> string {
|
|
||||||
if n == 0 { return "0"; }
|
|
||||||
neg := n < 0;
|
|
||||||
v := if neg then 0 - n else n;
|
|
||||||
// Single pass: fill digits backwards into temp string, then substr
|
|
||||||
tmp := cstring(20);
|
|
||||||
i := 19;
|
|
||||||
while v > 0 {
|
|
||||||
tmp[i] = (v % 10) + 48;
|
|
||||||
v = v / 10;
|
|
||||||
i -= 1;
|
|
||||||
}
|
|
||||||
if neg { tmp[i] = 45; i -= 1; }
|
|
||||||
substr(tmp, i + 1, 20 - i - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool_to_string :: (b: bool) -> string {
|
|
||||||
if b then "true" else "false";
|
|
||||||
}
|
|
||||||
|
|
||||||
float_to_string :: (f: f64) -> string {
|
|
||||||
neg := f < 0.0;
|
|
||||||
v := if neg then 0.0 - f else f;
|
|
||||||
int_part := cast(s64) v;
|
|
||||||
frac := cast(s64) ((v - cast(f64) int_part) * 1000000.0);
|
|
||||||
if frac < 0 { frac = 0 - frac; }
|
|
||||||
istr := int_to_string(int_part);
|
|
||||||
fstr := int_to_string(frac);
|
|
||||||
il := istr.len;
|
|
||||||
fl := fstr.len;
|
|
||||||
prefix := if neg then 1 else 0;
|
|
||||||
total := prefix + il + 1 + 6;
|
|
||||||
buf := cstring(total);
|
|
||||||
pos := 0;
|
|
||||||
if neg { buf[0] = 45; pos = 1; }
|
|
||||||
memcpy(@buf[pos], istr.ptr, il);
|
|
||||||
pos = pos + il;
|
|
||||||
buf[pos] = 46;
|
|
||||||
pos += 1;
|
|
||||||
pad := 6 - fl;
|
|
||||||
memset(@buf[pos], 48, pad);
|
|
||||||
pos = pos + pad;
|
|
||||||
memcpy(@buf[pos], fstr.ptr, fl);
|
|
||||||
buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
hex_group :: (buf: string, offset: s64, val: s64) {
|
|
||||||
i := offset + 3;
|
|
||||||
v := val;
|
|
||||||
while i >= offset {
|
|
||||||
d := v % 16;
|
|
||||||
buf[i] = if d < 10 then d + 48 else d - 10 + 97;
|
|
||||||
v = v / 16;
|
|
||||||
i -= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int_to_hex_string :: (n: s64) -> string {
|
|
||||||
if n == 0 { return "0"; }
|
|
||||||
|
|
||||||
// Split into four 16-bit groups for correct unsigned treatment
|
|
||||||
g0 := n % 65536;
|
|
||||||
if g0 < 0 { g0 = g0 + 65536; }
|
|
||||||
r1 := (n - g0) / 65536;
|
|
||||||
g1 := r1 % 65536;
|
|
||||||
if g1 < 0 { g1 = g1 + 65536; }
|
|
||||||
r2 := (r1 - g1) / 65536;
|
|
||||||
g2 := r2 % 65536;
|
|
||||||
if g2 < 0 { g2 = g2 + 65536; }
|
|
||||||
r3 := (r2 - g2) / 65536;
|
|
||||||
g3 := r3 % 65536;
|
|
||||||
if g3 < 0 { g3 = g3 + 65536; }
|
|
||||||
|
|
||||||
buf := cstring(16);
|
|
||||||
hex_group(buf, 0, g3);
|
|
||||||
hex_group(buf, 4, g2);
|
|
||||||
hex_group(buf, 8, g1);
|
|
||||||
hex_group(buf, 12, g0);
|
|
||||||
|
|
||||||
// Skip leading zeros (keep at least 1 digit)
|
|
||||||
start := 0;
|
|
||||||
while start < 15 {
|
|
||||||
if buf[start] != 48 { break; }
|
|
||||||
start += 1;
|
|
||||||
}
|
|
||||||
substr(buf, start, 16 - start);
|
|
||||||
}
|
|
||||||
|
|
||||||
concat :: (a: string, b: string) -> string {
|
|
||||||
al := a.len;
|
|
||||||
bl := b.len;
|
|
||||||
buf := cstring(al + bl);
|
|
||||||
memcpy(buf.ptr, a.ptr, al);
|
|
||||||
memcpy(@buf[al], b.ptr, bl);
|
|
||||||
buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
substr :: (s: string, start: s64, len: s64) -> string {
|
|
||||||
buf := cstring(len);
|
|
||||||
memcpy(buf.ptr, @s[start], len);
|
|
||||||
buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct_to_string :: (s: $T) -> string {
|
|
||||||
result := concat(type_name(T), "{");
|
|
||||||
i := 0;
|
|
||||||
while i < field_count(T) {
|
|
||||||
if i > 0 { result = concat(result, ", "); }
|
|
||||||
result = concat(result, field_name(T, i));
|
|
||||||
result = concat(result, ": ");
|
|
||||||
result = concat(result, any_to_string(field_value(s, i)));
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
concat(result, "}");
|
|
||||||
}
|
|
||||||
|
|
||||||
vector_to_string :: (v: $T) -> string {
|
|
||||||
result := "[";
|
|
||||||
i := 0;
|
|
||||||
while i < field_count(T) {
|
|
||||||
if i > 0 { result = concat(result, ", "); }
|
|
||||||
result = concat(result, any_to_string(field_value(v, i)));
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
concat(result, "]");
|
|
||||||
}
|
|
||||||
|
|
||||||
array_to_string :: (a: $T) -> string {
|
|
||||||
result := "[";
|
|
||||||
i := 0;
|
|
||||||
while i < field_count(T) {
|
|
||||||
if i > 0 { result = concat(result, ", "); }
|
|
||||||
result = concat(result, any_to_string(field_value(a, i)));
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
concat(result, "]");
|
|
||||||
}
|
|
||||||
|
|
||||||
slice_to_string :: (items: []$T) -> string {
|
|
||||||
result := "[";
|
|
||||||
i := 0;
|
|
||||||
while i < items.len {
|
|
||||||
if i > 0 { result = concat(result, ", "); }
|
|
||||||
result = concat(result, any_to_string(field_value(items, i)));
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
concat(result, "]");
|
|
||||||
}
|
|
||||||
|
|
||||||
pointer_to_string :: (p: $T) -> string {
|
|
||||||
addr : s64 = xx p;
|
|
||||||
if addr == 0 { "null"; } else {
|
|
||||||
concat(type_name(T), concat("@0x", int_to_hex_string(addr)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
flags_to_string :: (val: $T) -> string {
|
|
||||||
v := cast(s64) val;
|
|
||||||
result := "";
|
|
||||||
i := 0;
|
|
||||||
while i < field_count(T) {
|
|
||||||
fv := field_value_int(T, i);
|
|
||||||
if v & fv {
|
|
||||||
if result.len > 0 { result = concat(result, " | "); }
|
|
||||||
result = concat(result, concat(".", field_name(T, i)));
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
if result.len == 0 { result = "0"; }
|
|
||||||
result;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum_to_string :: (u: $T) -> string {
|
|
||||||
if is_flags(T) { return flags_to_string(u); }
|
|
||||||
idx := field_index(T, u);
|
|
||||||
result := concat(".", field_name(T, idx));
|
|
||||||
payload := field_value(u, idx);
|
|
||||||
pstr := any_to_string(payload);
|
|
||||||
if pstr.len > 0 {
|
|
||||||
result = concat(result, concat("(", concat(pstr, ")")));
|
|
||||||
}
|
|
||||||
result;
|
|
||||||
}
|
|
||||||
|
|
||||||
any_to_string :: (val: Any) -> string {
|
|
||||||
result := "<?>";
|
|
||||||
type := type_of(val);
|
|
||||||
if type == {
|
|
||||||
case void: result = "";
|
|
||||||
case int: result = int_to_string(xx val);
|
|
||||||
case string: { s : string = xx val; result = s; }
|
|
||||||
case bool: result = bool_to_string(xx val);
|
|
||||||
case float: result = float_to_string(xx val);
|
|
||||||
case struct: result = struct_to_string(cast(type) val);
|
|
||||||
case enum: result = enum_to_string(cast(type) val);
|
|
||||||
case vector: result = vector_to_string(cast(type) val);
|
|
||||||
case array: result = array_to_string(cast(type) val);
|
|
||||||
case slice: result = slice_to_string(cast(type) val);
|
|
||||||
case pointer: result = pointer_to_string(cast(type) val);
|
|
||||||
case type: { s : string = xx val; result = s; }
|
|
||||||
}
|
|
||||||
result;
|
|
||||||
}
|
|
||||||
|
|
||||||
build_format :: (fmt: string) -> string {
|
|
||||||
code := "result := \"\"; ";
|
|
||||||
seg_start := 0;
|
|
||||||
i := 0;
|
|
||||||
arg_idx := 0;
|
|
||||||
while i < fmt.len {
|
|
||||||
if fmt[i] == 123 {
|
|
||||||
if i + 1 < fmt.len {
|
|
||||||
if fmt[i + 1] == 125 {
|
|
||||||
if i > seg_start {
|
|
||||||
code = concat(code, "result = concat(result, substr(fmt, ");
|
|
||||||
code = concat(code, int_to_string(seg_start));
|
|
||||||
code = concat(code, ", ");
|
|
||||||
code = concat(code, int_to_string(i - seg_start));
|
|
||||||
code = concat(code, ")); ");
|
|
||||||
}
|
|
||||||
code = concat(code, "result = concat(result, any_to_string(args[");
|
|
||||||
code = concat(code, int_to_string(arg_idx));
|
|
||||||
code = concat(code, "])); ");
|
|
||||||
arg_idx += 1;
|
|
||||||
i += 2;
|
|
||||||
seg_start = i;
|
|
||||||
} else if fmt[i + 1] == 123 {
|
|
||||||
code = concat(code, "result = concat(result, substr(fmt, ");
|
|
||||||
code = concat(code, int_to_string(seg_start));
|
|
||||||
code = concat(code, ", ");
|
|
||||||
code = concat(code, int_to_string(i - seg_start + 1));
|
|
||||||
code = concat(code, ")); ");
|
|
||||||
i += 2;
|
|
||||||
seg_start = i;
|
|
||||||
} else {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
} else if fmt[i] == 125 {
|
|
||||||
if i + 1 < fmt.len {
|
|
||||||
if fmt[i + 1] == 125 {
|
|
||||||
code = concat(code, "result = concat(result, substr(fmt, ");
|
|
||||||
code = concat(code, int_to_string(seg_start));
|
|
||||||
code = concat(code, ", ");
|
|
||||||
code = concat(code, int_to_string(i - seg_start + 1));
|
|
||||||
code = concat(code, ")); ");
|
|
||||||
i += 2;
|
|
||||||
seg_start = i;
|
|
||||||
} else {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if seg_start < fmt.len {
|
|
||||||
code = concat(code, "result = concat(result, substr(fmt, ");
|
|
||||||
code = concat(code, int_to_string(seg_start));
|
|
||||||
code = concat(code, ", ");
|
|
||||||
code = concat(code, int_to_string(fmt.len - seg_start));
|
|
||||||
code = concat(code, ")); ");
|
|
||||||
}
|
|
||||||
code;
|
|
||||||
}
|
|
||||||
|
|
||||||
format :: ($fmt: string, args: ..Any) -> string {
|
|
||||||
#insert build_format(fmt);
|
|
||||||
#insert "result;";
|
|
||||||
}
|
|
||||||
|
|
||||||
print :: ($fmt: string, args: ..Any) {
|
|
||||||
#insert build_format(fmt);
|
|
||||||
#insert "out(result);";
|
|
||||||
}
|
|
||||||
|
|
||||||
List :: struct ($T: Type) {
|
|
||||||
items: [*]T = null;
|
|
||||||
len: s64 = 0;
|
|
||||||
cap: s64 = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
append ::(list: *List($T), item: T) {
|
|
||||||
if list.len >= list.cap {
|
|
||||||
new_cap := if list.cap == 0 then 4 else list.cap * 2;
|
|
||||||
new_items : [*]T = xx malloc(new_cap * size_of(T));
|
|
||||||
if list.len > 0 {
|
|
||||||
memcpy(new_items, list.items, list.len * size_of(T));
|
|
||||||
free(list.items);
|
|
||||||
}
|
|
||||||
list.items = new_items;
|
|
||||||
list.cap = new_cap;
|
|
||||||
}
|
|
||||||
list.items[list.len] = item;
|
|
||||||
list.len += 1;
|
|
||||||
}
|
|
||||||
94
ui/button.sx
Normal file
94
ui/button.sx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
#import "modules/std.sx";
|
||||||
|
#import "ui/types.sx";
|
||||||
|
#import "ui/render.sx";
|
||||||
|
#import "ui/events.sx";
|
||||||
|
#import "ui/view.sx";
|
||||||
|
#import "ui/label.sx";
|
||||||
|
|
||||||
|
ButtonStyle :: struct {
|
||||||
|
background: Color;
|
||||||
|
foreground: Color;
|
||||||
|
hover_bg: Color;
|
||||||
|
pressed_bg: Color;
|
||||||
|
corner_radius: f32;
|
||||||
|
padding: EdgeInsets;
|
||||||
|
|
||||||
|
default :: () -> ButtonStyle {
|
||||||
|
ButtonStyle.{
|
||||||
|
background = COLOR_BLUE,
|
||||||
|
foreground = COLOR_WHITE,
|
||||||
|
hover_bg = Color.rgb(0, 100, 220),
|
||||||
|
pressed_bg = Color.rgb(0, 80, 180),
|
||||||
|
corner_radius = 6.0,
|
||||||
|
padding = EdgeInsets.symmetric(16.0, 8.0)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button :: struct {
|
||||||
|
label: string;
|
||||||
|
font_size: f32;
|
||||||
|
style: ButtonStyle;
|
||||||
|
on_tap: ?Closure();
|
||||||
|
hovered: bool;
|
||||||
|
pressed: bool;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for Button {
|
||||||
|
size_that_fits :: (self: *Button, proposal: ProposedSize) -> Size {
|
||||||
|
scale := self.font_size / GLYPH_HEIGHT_APPROX;
|
||||||
|
text_w := xx self.label.len * GLYPH_WIDTH_APPROX * scale;
|
||||||
|
text_h := self.font_size;
|
||||||
|
Size.{
|
||||||
|
width = text_w + self.style.padding.horizontal(),
|
||||||
|
height = text_h + self.style.padding.vertical()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
layout :: (self: *Button, bounds: Frame) {}
|
||||||
|
|
||||||
|
render :: (self: *Button, ctx: *RenderContext, frame: Frame) {
|
||||||
|
bg := if self.pressed then self.style.pressed_bg
|
||||||
|
else if self.hovered then self.style.hover_bg
|
||||||
|
else self.style.background;
|
||||||
|
|
||||||
|
ctx.add_rounded_rect(frame, bg, self.style.corner_radius);
|
||||||
|
|
||||||
|
// Text centered in frame
|
||||||
|
scale := self.font_size / GLYPH_HEIGHT_APPROX;
|
||||||
|
text_w := xx self.label.len * GLYPH_WIDTH_APPROX * scale;
|
||||||
|
text_h := self.font_size;
|
||||||
|
text_x := frame.origin.x + (frame.size.width - text_w) * 0.5;
|
||||||
|
text_y := frame.origin.y + (frame.size.height - text_h) * 0.5;
|
||||||
|
text_frame := Frame.make(text_x, text_y, text_w, text_h);
|
||||||
|
ctx.add_text(text_frame, self.label, self.font_size, self.style.foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_event :: (self: *Button, event: *Event, frame: Frame) -> bool {
|
||||||
|
if event.type == {
|
||||||
|
case .mouse_moved: {
|
||||||
|
self.hovered = frame.contains(event.position);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
case .mouse_down: {
|
||||||
|
if frame.contains(event.position) {
|
||||||
|
self.pressed = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .mouse_up: {
|
||||||
|
if self.pressed {
|
||||||
|
self.pressed = false;
|
||||||
|
if frame.contains(event.position) {
|
||||||
|
if handler := self.on_tap {
|
||||||
|
handler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false;
|
||||||
|
}
|
||||||
|
}
|
||||||
106
ui/events.sx
Normal file
106
ui/events.sx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
#import "modules/std.sx";
|
||||||
|
#import "modules/sdl3.sx";
|
||||||
|
#import "ui/types.sx";
|
||||||
|
|
||||||
|
EventType :: enum {
|
||||||
|
none;
|
||||||
|
mouse_down;
|
||||||
|
mouse_up;
|
||||||
|
mouse_moved;
|
||||||
|
mouse_wheel;
|
||||||
|
key_down;
|
||||||
|
key_up;
|
||||||
|
text_input;
|
||||||
|
window_resize;
|
||||||
|
quit;
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseButton :: enum {
|
||||||
|
none;
|
||||||
|
left;
|
||||||
|
middle;
|
||||||
|
right;
|
||||||
|
}
|
||||||
|
|
||||||
|
Event :: struct {
|
||||||
|
type: EventType;
|
||||||
|
position: Point;
|
||||||
|
delta: Point;
|
||||||
|
button: MouseButton;
|
||||||
|
key: u32;
|
||||||
|
text: string;
|
||||||
|
timestamp: u64;
|
||||||
|
|
||||||
|
make :: (type: EventType) -> Event {
|
||||||
|
e : Event = ---;
|
||||||
|
memset(@e, 0, size_of(Event));
|
||||||
|
e.type = type;
|
||||||
|
e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translate SDL_Event → our Event type
|
||||||
|
translate_sdl_event :: (sdl: *SDL_Event) -> Event {
|
||||||
|
if sdl.* == {
|
||||||
|
case .quit: {
|
||||||
|
return Event.make(.quit);
|
||||||
|
}
|
||||||
|
case .key_down: (data) {
|
||||||
|
e := Event.make(.key_down);
|
||||||
|
e.key = xx data.key;
|
||||||
|
e.timestamp = data.timestamp;
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
case .key_up: (data) {
|
||||||
|
e := Event.make(.key_up);
|
||||||
|
e.key = xx data.key;
|
||||||
|
e.timestamp = data.timestamp;
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
case .mouse_motion: (data) {
|
||||||
|
e := Event.make(.mouse_moved);
|
||||||
|
e.position = Point.{ x = data.x, y = data.y };
|
||||||
|
e.delta = Point.{ x = data.xrel, y = data.yrel };
|
||||||
|
e.timestamp = data.timestamp;
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
case .mouse_button_down: (data) {
|
||||||
|
e := Event.make(.mouse_down);
|
||||||
|
e.position = Point.{ x = data.x, y = data.y };
|
||||||
|
e.button = if data.button == {
|
||||||
|
case 1: .left;
|
||||||
|
case 2: .middle;
|
||||||
|
case 3: .right;
|
||||||
|
else: .none;
|
||||||
|
};
|
||||||
|
e.timestamp = data.timestamp;
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
case .mouse_button_up: (data) {
|
||||||
|
e := Event.make(.mouse_up);
|
||||||
|
e.position = Point.{ x = data.x, y = data.y };
|
||||||
|
e.button = if data.button == {
|
||||||
|
case 1: .left;
|
||||||
|
case 2: .middle;
|
||||||
|
case 3: .right;
|
||||||
|
else: .none;
|
||||||
|
};
|
||||||
|
e.timestamp = data.timestamp;
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
case .mouse_wheel: (data) {
|
||||||
|
e := Event.make(.mouse_wheel);
|
||||||
|
e.delta = Point.{ x = data.x, y = data.y };
|
||||||
|
e.position = Point.{ x = data.mouse_x, y = data.mouse_y };
|
||||||
|
e.timestamp = data.timestamp;
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
case .window_resized: (data) {
|
||||||
|
e := Event.make(.window_resize);
|
||||||
|
e.position = Point.{ x = xx data.data1, y = xx data.data2 };
|
||||||
|
e.timestamp = data.timestamp;
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event.make(.none);
|
||||||
|
}
|
||||||
44
ui/label.sx
Normal file
44
ui/label.sx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
#import "modules/std.sx";
|
||||||
|
#import "ui/types.sx";
|
||||||
|
#import "ui/render.sx";
|
||||||
|
#import "ui/events.sx";
|
||||||
|
#import "ui/view.sx";
|
||||||
|
|
||||||
|
GLYPH_WIDTH_APPROX :f32: 8.0;
|
||||||
|
GLYPH_HEIGHT_APPROX :f32: 16.0;
|
||||||
|
|
||||||
|
Label :: struct {
|
||||||
|
text: string;
|
||||||
|
font_size: f32;
|
||||||
|
color: Color;
|
||||||
|
|
||||||
|
make :: (text: string) -> Label {
|
||||||
|
Label.{
|
||||||
|
text = text,
|
||||||
|
font_size = 14.0,
|
||||||
|
color = COLOR_WHITE
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for Label {
|
||||||
|
size_that_fits :: (self: *Label, proposal: ProposedSize) -> Size {
|
||||||
|
// Approximate: chars × avg glyph width, scaled by font size
|
||||||
|
scale := self.font_size / GLYPH_HEIGHT_APPROX;
|
||||||
|
w := xx self.text.len * GLYPH_WIDTH_APPROX * scale;
|
||||||
|
h := self.font_size;
|
||||||
|
Size.{ width = w, height = h };
|
||||||
|
}
|
||||||
|
|
||||||
|
layout :: (self: *Label, bounds: Frame) {
|
||||||
|
// Leaf view — nothing to place
|
||||||
|
}
|
||||||
|
|
||||||
|
render :: (self: *Label, ctx: *RenderContext, frame: Frame) {
|
||||||
|
ctx.add_text(frame, self.text, self.font_size, self.color);
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_event :: (self: *Label, event: *Event, frame: Frame) -> bool {
|
||||||
|
false;
|
||||||
|
}
|
||||||
|
}
|
||||||
152
ui/layout.sx
Normal file
152
ui/layout.sx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
#import "modules/std.sx";
|
||||||
|
#import "modules/math";
|
||||||
|
#import "ui/types.sx";
|
||||||
|
#import "ui/view.sx";
|
||||||
|
|
||||||
|
// VStack layout: measure all children, stack vertically
|
||||||
|
// Width is constrained from parent; height is unspecified (children choose)
|
||||||
|
layout_vstack :: (children: *List(ViewChild), bounds: Frame, spacing: f32, alignment: HAlignment) {
|
||||||
|
n := children.len;
|
||||||
|
if n == 0 { return; }
|
||||||
|
|
||||||
|
content_width := bounds.size.width;
|
||||||
|
|
||||||
|
y := bounds.origin.y;
|
||||||
|
i := 0;
|
||||||
|
while i < n {
|
||||||
|
child := @children.items[i];
|
||||||
|
child_size := child.view.size_that_fits(ProposedSize.{
|
||||||
|
width = content_width,
|
||||||
|
height = null
|
||||||
|
});
|
||||||
|
x_offset := align_h(alignment, child_size.width, content_width);
|
||||||
|
child.computed_frame = Frame.{
|
||||||
|
origin = Point.{ x = bounds.origin.x + x_offset, y = y },
|
||||||
|
size = child_size
|
||||||
|
};
|
||||||
|
child.view.layout(child.computed_frame);
|
||||||
|
y = y + child_size.height + spacing;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HStack layout: measure all children, stack horizontally
|
||||||
|
// Height is constrained from parent; width is unspecified (children choose)
|
||||||
|
layout_hstack :: (children: *List(ViewChild), bounds: Frame, spacing: f32, alignment: VAlignment) {
|
||||||
|
n := children.len;
|
||||||
|
if n == 0 { return; }
|
||||||
|
|
||||||
|
content_height := bounds.size.height;
|
||||||
|
|
||||||
|
x := bounds.origin.x;
|
||||||
|
i := 0;
|
||||||
|
while i < n {
|
||||||
|
child := @children.items[i];
|
||||||
|
child_size := child.view.size_that_fits(ProposedSize.{
|
||||||
|
width = null,
|
||||||
|
height = content_height
|
||||||
|
});
|
||||||
|
y_offset := align_v(alignment, child_size.height, content_height);
|
||||||
|
child.computed_frame = Frame.{
|
||||||
|
origin = Point.{ x = x, y = bounds.origin.y + y_offset },
|
||||||
|
size = child_size
|
||||||
|
};
|
||||||
|
child.view.layout(child.computed_frame);
|
||||||
|
x = x + child_size.width + spacing;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZStack layout: all children get same bounds, aligned
|
||||||
|
layout_zstack :: (children: *List(ViewChild), bounds: Frame, alignment: Alignment) {
|
||||||
|
n := children.len;
|
||||||
|
if n == 0 { return; }
|
||||||
|
|
||||||
|
proposal := ProposedSize.{
|
||||||
|
width = bounds.size.width,
|
||||||
|
height = bounds.size.height
|
||||||
|
};
|
||||||
|
|
||||||
|
i := 0;
|
||||||
|
while i < n {
|
||||||
|
child := @children.items[i];
|
||||||
|
child_size := child.view.size_that_fits(proposal);
|
||||||
|
x_offset := align_h(alignment.h, child_size.width, bounds.size.width);
|
||||||
|
y_offset := align_v(alignment.v, child_size.height, bounds.size.height);
|
||||||
|
child.computed_frame = Frame.{
|
||||||
|
origin = Point.{ x = bounds.origin.x + x_offset, y = bounds.origin.y + y_offset },
|
||||||
|
size = child_size
|
||||||
|
};
|
||||||
|
child.view.layout(child.computed_frame);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measure helpers — compute stack size from children
|
||||||
|
|
||||||
|
measure_vstack :: (children: *List(ViewChild), proposal: ProposedSize, spacing: f32) -> Size {
|
||||||
|
n := children.len;
|
||||||
|
if n == 0 { return Size.zero(); }
|
||||||
|
|
||||||
|
max_width : f32 = 0.0;
|
||||||
|
total_height : f32 = 0.0;
|
||||||
|
|
||||||
|
// Measure children: constrain width, leave height unspecified
|
||||||
|
child_proposal := ProposedSize.{ width = proposal.width, height = null };
|
||||||
|
i := 0;
|
||||||
|
while i < n {
|
||||||
|
child_size := children.items[i].view.size_that_fits(child_proposal);
|
||||||
|
children.items[i].computed_frame.size = child_size;
|
||||||
|
if child_size.width > max_width { max_width = child_size.width; }
|
||||||
|
total_height = total_height + child_size.height;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
total_height = total_height + spacing * xx (n - 1);
|
||||||
|
|
||||||
|
result_width := min(proposal.width ?? max_width, max_width);
|
||||||
|
Size.{ width = result_width, height = total_height };
|
||||||
|
}
|
||||||
|
|
||||||
|
measure_hstack :: (children: *List(ViewChild), proposal: ProposedSize, spacing: f32) -> Size {
|
||||||
|
n := children.len;
|
||||||
|
if n == 0 { return Size.zero(); }
|
||||||
|
|
||||||
|
total_width : f32 = 0.0;
|
||||||
|
max_height : f32 = 0.0;
|
||||||
|
|
||||||
|
// Measure children: constrain height, leave width unspecified
|
||||||
|
child_proposal := ProposedSize.{ width = null, height = proposal.height };
|
||||||
|
i := 0;
|
||||||
|
while i < n {
|
||||||
|
child_size := children.items[i].view.size_that_fits(child_proposal);
|
||||||
|
children.items[i].computed_frame.size = child_size;
|
||||||
|
total_width = total_width + child_size.width;
|
||||||
|
if child_size.height > max_height { max_height = child_size.height; }
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
total_width = total_width + spacing * xx (n - 1);
|
||||||
|
|
||||||
|
result_height := min(proposal.height ?? max_height, max_height);
|
||||||
|
Size.{ width = total_width, height = result_height };
|
||||||
|
}
|
||||||
|
|
||||||
|
measure_zstack :: (children: *List(ViewChild), proposal: ProposedSize) -> Size {
|
||||||
|
n := children.len;
|
||||||
|
if n == 0 { return Size.zero(); }
|
||||||
|
|
||||||
|
max_width : f32 = 0.0;
|
||||||
|
max_height : f32 = 0.0;
|
||||||
|
|
||||||
|
i := 0;
|
||||||
|
while i < n {
|
||||||
|
child_size := children.items[i].view.size_that_fits(proposal);
|
||||||
|
children.items[i].computed_frame.size = child_size;
|
||||||
|
if child_size.width > max_width { max_width = child_size.width; }
|
||||||
|
if child_size.height > max_height { max_height = child_size.height; }
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Size.{ width = max_width, height = max_height };
|
||||||
|
}
|
||||||
72
ui/pipeline.sx
Normal file
72
ui/pipeline.sx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
#import "modules/std.sx";
|
||||||
|
#import "modules/opengl.sx";
|
||||||
|
#import "ui/types.sx";
|
||||||
|
#import "ui/render.sx";
|
||||||
|
#import "ui/events.sx";
|
||||||
|
#import "ui/view.sx";
|
||||||
|
#import "ui/renderer.sx";
|
||||||
|
|
||||||
|
UIPipeline :: struct {
|
||||||
|
renderer: UIRenderer;
|
||||||
|
render_tree: RenderTree;
|
||||||
|
screen_width: f32;
|
||||||
|
screen_height: f32;
|
||||||
|
root: ViewChild;
|
||||||
|
has_root: bool;
|
||||||
|
|
||||||
|
init :: (self: *UIPipeline, width: f32, height: f32) {
|
||||||
|
self.render_tree = RenderTree.init();
|
||||||
|
self.renderer.init();
|
||||||
|
self.screen_width = width;
|
||||||
|
self.screen_height = height;
|
||||||
|
self.has_root = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
set_root :: (self: *UIPipeline, view: View) {
|
||||||
|
self.root = ViewChild.make(view);
|
||||||
|
self.has_root = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
resize :: (self: *UIPipeline, width: f32, height: f32) {
|
||||||
|
self.screen_width = width;
|
||||||
|
self.screen_height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process a single event through the view tree
|
||||||
|
dispatch_event :: (self: *UIPipeline, event: *Event) -> bool {
|
||||||
|
if self.has_root == false { return false; }
|
||||||
|
self.root.view.handle_event(event, self.root.computed_frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run one frame: layout → render → commit
|
||||||
|
tick :: (self: *UIPipeline) {
|
||||||
|
if self.has_root == false { return; }
|
||||||
|
|
||||||
|
screen := Frame.make(0.0, 0.0, self.screen_width, self.screen_height);
|
||||||
|
proposal := ProposedSize.fixed(self.screen_width, self.screen_height);
|
||||||
|
|
||||||
|
// Layout
|
||||||
|
root_size := self.root.view.size_that_fits(proposal);
|
||||||
|
self.root.computed_frame = Frame.{
|
||||||
|
origin = Point.zero(),
|
||||||
|
size = root_size
|
||||||
|
};
|
||||||
|
self.root.view.layout(self.root.computed_frame);
|
||||||
|
|
||||||
|
// Render to tree
|
||||||
|
self.render_tree.clear();
|
||||||
|
ctx := RenderContext.init(@self.render_tree);
|
||||||
|
self.root.view.render(@ctx, self.root.computed_frame);
|
||||||
|
|
||||||
|
// Commit to GPU
|
||||||
|
glEnable(GL_BLEND);
|
||||||
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
|
||||||
|
self.renderer.begin(self.screen_width, self.screen_height);
|
||||||
|
self.renderer.process(@self.render_tree);
|
||||||
|
self.renderer.flush();
|
||||||
|
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
}
|
||||||
|
}
|
||||||
180
ui/render.sx
Normal file
180
ui/render.sx
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
#import "modules/std.sx";
|
||||||
|
#import "ui/types.sx";
|
||||||
|
|
||||||
|
RenderNodeType :: enum {
|
||||||
|
rect;
|
||||||
|
rounded_rect;
|
||||||
|
text;
|
||||||
|
image;
|
||||||
|
clip_push;
|
||||||
|
clip_pop;
|
||||||
|
opacity_push;
|
||||||
|
opacity_pop;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderNode :: struct {
|
||||||
|
type: RenderNodeType;
|
||||||
|
frame: Frame;
|
||||||
|
|
||||||
|
// Rect / rounded_rect
|
||||||
|
fill_color: Color;
|
||||||
|
stroke_color: Color;
|
||||||
|
stroke_width: f32;
|
||||||
|
corner_radius: f32;
|
||||||
|
|
||||||
|
// Text
|
||||||
|
text: string;
|
||||||
|
font_size: f32;
|
||||||
|
text_color: Color;
|
||||||
|
|
||||||
|
// Image
|
||||||
|
texture_id: u32;
|
||||||
|
|
||||||
|
// Opacity
|
||||||
|
opacity: f32;
|
||||||
|
|
||||||
|
depth: s64;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderTree :: struct {
|
||||||
|
nodes: List(RenderNode);
|
||||||
|
generation: s64;
|
||||||
|
|
||||||
|
init :: () -> RenderTree {
|
||||||
|
RenderTree.{ generation = 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
clear :: (self: *RenderTree) {
|
||||||
|
self.nodes.len = 0;
|
||||||
|
self.generation += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
add :: (self: *RenderTree, node: RenderNode) -> s64 {
|
||||||
|
idx := self.nodes.len;
|
||||||
|
self.nodes.append(node);
|
||||||
|
idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stateful builder — views use this to emit render nodes
|
||||||
|
|
||||||
|
RenderContext :: struct {
|
||||||
|
tree: *RenderTree;
|
||||||
|
clip_depth: s64;
|
||||||
|
opacity: f32;
|
||||||
|
depth: s64;
|
||||||
|
|
||||||
|
init :: (tree: *RenderTree) -> RenderContext {
|
||||||
|
RenderContext.{
|
||||||
|
tree = tree,
|
||||||
|
clip_depth = 0,
|
||||||
|
opacity = 1.0,
|
||||||
|
depth = 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
add_rect :: (self: *RenderContext, frame: Frame, fill: Color) {
|
||||||
|
node : RenderNode = ---;
|
||||||
|
memset(@node, 0, size_of(RenderNode));
|
||||||
|
node.type = .rect;
|
||||||
|
node.frame = frame;
|
||||||
|
node.fill_color = fill;
|
||||||
|
node.opacity = self.opacity;
|
||||||
|
node.depth = self.depth;
|
||||||
|
self.tree.add(node);
|
||||||
|
self.depth += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
add_rounded_rect :: (self: *RenderContext, frame: Frame, fill: Color, radius: f32) {
|
||||||
|
node : RenderNode = ---;
|
||||||
|
memset(@node, 0, size_of(RenderNode));
|
||||||
|
node.type = .rounded_rect;
|
||||||
|
node.frame = frame;
|
||||||
|
node.fill_color = fill;
|
||||||
|
node.corner_radius = radius;
|
||||||
|
node.opacity = self.opacity;
|
||||||
|
node.depth = self.depth;
|
||||||
|
self.tree.add(node);
|
||||||
|
self.depth += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
add_stroked_rect :: (self: *RenderContext, frame: Frame, fill: Color, stroke: Color, stroke_w: f32, radius: f32) {
|
||||||
|
node : RenderNode = ---;
|
||||||
|
memset(@node, 0, size_of(RenderNode));
|
||||||
|
node.type = .rounded_rect;
|
||||||
|
node.frame = frame;
|
||||||
|
node.fill_color = fill;
|
||||||
|
node.stroke_color = stroke;
|
||||||
|
node.stroke_width = stroke_w;
|
||||||
|
node.corner_radius = radius;
|
||||||
|
node.opacity = self.opacity;
|
||||||
|
node.depth = self.depth;
|
||||||
|
self.tree.add(node);
|
||||||
|
self.depth += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
add_text :: (self: *RenderContext, frame: Frame, text: string, font_size: f32, color: Color) {
|
||||||
|
node : RenderNode = ---;
|
||||||
|
memset(@node, 0, size_of(RenderNode));
|
||||||
|
node.type = .text;
|
||||||
|
node.frame = frame;
|
||||||
|
node.text = text;
|
||||||
|
node.font_size = font_size;
|
||||||
|
node.text_color = color;
|
||||||
|
node.opacity = self.opacity;
|
||||||
|
node.depth = self.depth;
|
||||||
|
self.tree.add(node);
|
||||||
|
self.depth += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
add_image :: (self: *RenderContext, frame: Frame, texture_id: u32) {
|
||||||
|
node : RenderNode = ---;
|
||||||
|
memset(@node, 0, size_of(RenderNode));
|
||||||
|
node.type = .image;
|
||||||
|
node.frame = frame;
|
||||||
|
node.texture_id = texture_id;
|
||||||
|
node.opacity = self.opacity;
|
||||||
|
node.depth = self.depth;
|
||||||
|
self.tree.add(node);
|
||||||
|
self.depth += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
push_clip :: (self: *RenderContext, frame: Frame) {
|
||||||
|
node : RenderNode = ---;
|
||||||
|
memset(@node, 0, size_of(RenderNode));
|
||||||
|
node.type = .clip_push;
|
||||||
|
node.frame = frame;
|
||||||
|
node.depth = self.depth;
|
||||||
|
self.tree.add(node);
|
||||||
|
self.clip_depth += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pop_clip :: (self: *RenderContext) {
|
||||||
|
node : RenderNode = ---;
|
||||||
|
memset(@node, 0, size_of(RenderNode));
|
||||||
|
node.type = .clip_pop;
|
||||||
|
node.depth = self.depth;
|
||||||
|
self.tree.add(node);
|
||||||
|
self.clip_depth -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
push_opacity :: (self: *RenderContext, alpha: f32) {
|
||||||
|
prev := self.opacity;
|
||||||
|
self.opacity = prev * alpha;
|
||||||
|
node : RenderNode = ---;
|
||||||
|
memset(@node, 0, size_of(RenderNode));
|
||||||
|
node.type = .opacity_push;
|
||||||
|
node.opacity = self.opacity;
|
||||||
|
node.depth = self.depth;
|
||||||
|
self.tree.add(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
pop_opacity :: (self: *RenderContext, prev_opacity: f32) {
|
||||||
|
node : RenderNode = ---;
|
||||||
|
memset(@node, 0, size_of(RenderNode));
|
||||||
|
node.type = .opacity_pop;
|
||||||
|
node.depth = self.depth;
|
||||||
|
self.tree.add(node);
|
||||||
|
self.opacity = prev_opacity;
|
||||||
|
}
|
||||||
|
}
|
||||||
279
ui/renderer.sx
Normal file
279
ui/renderer.sx
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
#import "modules/std.sx";
|
||||||
|
#import "modules/opengl.sx";
|
||||||
|
#import "modules/math";
|
||||||
|
#import "ui/types.sx";
|
||||||
|
#import "ui/render.sx";
|
||||||
|
|
||||||
|
// Vertex: pos(2) + uv(2) + color(4) + params(4) = 12 floats
|
||||||
|
UI_VERTEX_FLOATS :s64: 12;
|
||||||
|
UI_VERTEX_BYTES :s64: 48;
|
||||||
|
MAX_UI_VERTICES :s64: 16384;
|
||||||
|
|
||||||
|
UIRenderer :: struct {
|
||||||
|
vao: u32;
|
||||||
|
vbo: u32;
|
||||||
|
shader: u32;
|
||||||
|
proj_loc: s32;
|
||||||
|
vertices: [*]f32;
|
||||||
|
vertex_count: s64;
|
||||||
|
screen_width: f32;
|
||||||
|
screen_height: f32;
|
||||||
|
white_texture: u32;
|
||||||
|
|
||||||
|
init :: (self: *UIRenderer) {
|
||||||
|
// Create shader
|
||||||
|
self.shader = create_program(UI_VERT_SRC, UI_FRAG_SRC);
|
||||||
|
|
||||||
|
self.proj_loc = glGetUniformLocation(self.shader, "uProj");
|
||||||
|
|
||||||
|
// Allocate vertex buffer (CPU side)
|
||||||
|
buf_size := MAX_UI_VERTICES * UI_VERTEX_BYTES;
|
||||||
|
self.vertices = xx context.allocator.alloc(buf_size);
|
||||||
|
memset(self.vertices, 0, buf_size);
|
||||||
|
self.vertex_count = 0;
|
||||||
|
|
||||||
|
// Create VAO/VBO
|
||||||
|
glGenVertexArrays(1, @self.vao);
|
||||||
|
glGenBuffers(1, @self.vbo);
|
||||||
|
glBindVertexArray(self.vao);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, self.vbo);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, xx buf_size, null, GL_DYNAMIC_DRAW);
|
||||||
|
|
||||||
|
// pos (2 floats)
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, 0, xx UI_VERTEX_BYTES, xx 0);
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
// uv (2 floats)
|
||||||
|
glVertexAttribPointer(1, 2, GL_FLOAT, 0, xx UI_VERTEX_BYTES, xx 8);
|
||||||
|
glEnableVertexAttribArray(1);
|
||||||
|
// color (4 floats)
|
||||||
|
glVertexAttribPointer(2, 4, GL_FLOAT, 0, xx UI_VERTEX_BYTES, xx 16);
|
||||||
|
glEnableVertexAttribArray(2);
|
||||||
|
// params: corner_radius, border_width, rect_w, rect_h
|
||||||
|
glVertexAttribPointer(3, 4, GL_FLOAT, 0, xx UI_VERTEX_BYTES, xx 32);
|
||||||
|
glEnableVertexAttribArray(3);
|
||||||
|
|
||||||
|
glBindVertexArray(0);
|
||||||
|
|
||||||
|
// 1x1 white texture for solid rects
|
||||||
|
self.white_texture = create_white_texture();
|
||||||
|
}
|
||||||
|
|
||||||
|
begin :: (self: *UIRenderer, width: f32, height: f32) {
|
||||||
|
self.screen_width = width;
|
||||||
|
self.screen_height = height;
|
||||||
|
self.vertex_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit a quad (2 triangles = 6 vertices)
|
||||||
|
push_quad :: (self: *UIRenderer, frame: Frame, color: Color, radius: f32, border_w: f32) {
|
||||||
|
if self.vertex_count + 6 > MAX_UI_VERTICES {
|
||||||
|
self.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
x0 := frame.origin.x;
|
||||||
|
y0 := frame.origin.y;
|
||||||
|
x1 := x0 + frame.size.width;
|
||||||
|
y1 := y0 + frame.size.height;
|
||||||
|
|
||||||
|
r := color.rf();
|
||||||
|
g := color.gf();
|
||||||
|
b := color.bf();
|
||||||
|
a := color.af();
|
||||||
|
|
||||||
|
w := frame.size.width;
|
||||||
|
h := frame.size.height;
|
||||||
|
|
||||||
|
// 6 vertices for quad: TL, TR, BL, TR, BR, BL
|
||||||
|
self.write_vertex(x0, y0, 0.0, 0.0, r, g, b, a, radius, border_w, w, h);
|
||||||
|
self.write_vertex(x1, y0, 1.0, 0.0, r, g, b, a, radius, border_w, w, h);
|
||||||
|
self.write_vertex(x0, y1, 0.0, 1.0, r, g, b, a, radius, border_w, w, h);
|
||||||
|
self.write_vertex(x1, y0, 1.0, 0.0, r, g, b, a, radius, border_w, w, h);
|
||||||
|
self.write_vertex(x1, y1, 1.0, 1.0, r, g, b, a, radius, border_w, w, h);
|
||||||
|
self.write_vertex(x0, y1, 0.0, 1.0, r, g, b, a, radius, border_w, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
write_vertex :: (self: *UIRenderer, x: f32, y: f32, u: f32, v: f32, r: f32, g: f32, b: f32, a: f32, cr: f32, bw: f32, rw: f32, rh: f32) {
|
||||||
|
off := self.vertex_count * UI_VERTEX_FLOATS;
|
||||||
|
self.vertices[off + 0] = x;
|
||||||
|
self.vertices[off + 1] = y;
|
||||||
|
self.vertices[off + 2] = u;
|
||||||
|
self.vertices[off + 3] = v;
|
||||||
|
self.vertices[off + 4] = r;
|
||||||
|
self.vertices[off + 5] = g;
|
||||||
|
self.vertices[off + 6] = b;
|
||||||
|
self.vertices[off + 7] = a;
|
||||||
|
self.vertices[off + 8] = cr;
|
||||||
|
self.vertices[off + 9] = bw;
|
||||||
|
self.vertices[off + 10] = rw;
|
||||||
|
self.vertices[off + 11] = rh;
|
||||||
|
self.vertex_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk the render tree and emit quads
|
||||||
|
process :: (self: *UIRenderer, tree: *RenderTree) {
|
||||||
|
i := 0;
|
||||||
|
while i < tree.nodes.len {
|
||||||
|
node := tree.nodes.items[i];
|
||||||
|
if node.type == {
|
||||||
|
case .rect: {
|
||||||
|
self.push_quad(node.frame, node.fill_color, 0.0, 0.0);
|
||||||
|
}
|
||||||
|
case .rounded_rect: {
|
||||||
|
self.push_quad(node.frame, node.fill_color, node.corner_radius, node.stroke_width);
|
||||||
|
}
|
||||||
|
case .text: {
|
||||||
|
// TODO: text rendering via glyph atlas
|
||||||
|
// For now, render a placeholder rect
|
||||||
|
self.push_quad(node.frame, node.text_color, 0.0, 0.0);
|
||||||
|
}
|
||||||
|
case .image: {
|
||||||
|
// TODO: textured quad
|
||||||
|
self.push_quad(node.frame, COLOR_WHITE, 0.0, 0.0);
|
||||||
|
}
|
||||||
|
case .clip_push: {
|
||||||
|
self.flush();
|
||||||
|
glEnable(GL_SCISSOR_TEST);
|
||||||
|
glScissor(
|
||||||
|
xx node.frame.origin.x,
|
||||||
|
xx (self.screen_height - node.frame.origin.y - node.frame.size.height),
|
||||||
|
xx node.frame.size.width,
|
||||||
|
xx node.frame.size.height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case .clip_pop: {
|
||||||
|
self.flush();
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
}
|
||||||
|
case .opacity_push: {}
|
||||||
|
case .opacity_pop: {}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flush :: (self: *UIRenderer) {
|
||||||
|
if self.vertex_count == 0 { return; }
|
||||||
|
|
||||||
|
glUseProgram(self.shader);
|
||||||
|
|
||||||
|
// Orthographic projection: (0,0) top-left, (w,h) bottom-right
|
||||||
|
proj := Mat4.ortho(0.0, self.screen_width, self.screen_height, 0.0, -1.0, 1.0);
|
||||||
|
glUniformMatrix4fv(self.proj_loc, 1, 0, proj.data);
|
||||||
|
|
||||||
|
glBindVertexArray(self.vao);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, self.vbo);
|
||||||
|
|
||||||
|
upload_size : s64 = self.vertex_count * UI_VERTEX_BYTES;
|
||||||
|
glBufferSubData(GL_ARRAY_BUFFER, 0, xx upload_size, self.vertices);
|
||||||
|
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, xx self.vertex_count);
|
||||||
|
|
||||||
|
glBindVertexArray(0);
|
||||||
|
self.vertex_count = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional GL functions needed for UI renderer
|
||||||
|
GL_SCISSOR_TEST :u32: 0x0C11;
|
||||||
|
GL_DYNAMIC_DRAW :u32: 0x88E8;
|
||||||
|
GL_TEXTURE_2D :u32: 0x0DE1;
|
||||||
|
GL_TEXTURE_MIN_FILTER :u32: 0x2801;
|
||||||
|
GL_TEXTURE_MAG_FILTER :u32: 0x2800;
|
||||||
|
GL_NEAREST :u32: 0x2600;
|
||||||
|
GL_RGBA :u32: 0x1908;
|
||||||
|
GL_UNSIGNED_BYTE :u32: 0x1401;
|
||||||
|
GL_SRC_ALPHA :u32: 0x0302;
|
||||||
|
GL_ONE_MINUS_SRC_ALPHA :u32: 0x0303;
|
||||||
|
|
||||||
|
glScissor : (s32, s32, s32, s32) -> void = ---;
|
||||||
|
glBufferSubData : (u32, s64, s64, *void) -> void = ---;
|
||||||
|
glGenTextures : (s32, *u32) -> void = ---;
|
||||||
|
glBindTexture : (u32, u32) -> void = ---;
|
||||||
|
glTexImage2D : (u32, s32, s32, s32, s32, s32, u32, u32, *void) -> void = ---;
|
||||||
|
glTexParameteri : (u32, u32, s32) -> void = ---;
|
||||||
|
glBlendFunc : (u32, u32) -> void = ---;
|
||||||
|
glReadPixels : (s32, s32, s32, s32, u32, u32, *void) -> void = ---;
|
||||||
|
|
||||||
|
// Load additional GL functions (call after load_gl)
|
||||||
|
load_gl_ui :: (get_proc: ([:0]u8) -> *void) {
|
||||||
|
glScissor = xx get_proc("glScissor");
|
||||||
|
glBufferSubData = xx get_proc("glBufferSubData");
|
||||||
|
glGenTextures = xx get_proc("glGenTextures");
|
||||||
|
glBindTexture = xx get_proc("glBindTexture");
|
||||||
|
glTexImage2D = xx get_proc("glTexImage2D");
|
||||||
|
glTexParameteri = xx get_proc("glTexParameteri");
|
||||||
|
glBlendFunc = xx get_proc("glBlendFunc");
|
||||||
|
glReadPixels = xx get_proc("glReadPixels");
|
||||||
|
}
|
||||||
|
|
||||||
|
create_white_texture :: () -> u32 {
|
||||||
|
tex : u32 = 0;
|
||||||
|
glGenTextures(1, @tex);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, tex);
|
||||||
|
pixel : [4]u8 = .[255, 255, 255, 255];
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, xx GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, @pixel);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, xx GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, xx GL_NEAREST);
|
||||||
|
tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- UI Shaders ---
|
||||||
|
|
||||||
|
UI_VERT_SRC :: #string GLSL
|
||||||
|
#version 330 core
|
||||||
|
layout(location = 0) in vec2 aPos;
|
||||||
|
layout(location = 1) in vec2 aUV;
|
||||||
|
layout(location = 2) in vec4 aColor;
|
||||||
|
layout(location = 3) in vec4 aParams;
|
||||||
|
|
||||||
|
uniform mat4 uProj;
|
||||||
|
|
||||||
|
out vec2 vUV;
|
||||||
|
out vec4 vColor;
|
||||||
|
out vec4 vParams;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
gl_Position = uProj * vec4(aPos, 0.0, 1.0);
|
||||||
|
vUV = aUV;
|
||||||
|
vColor = aColor;
|
||||||
|
vParams = aParams;
|
||||||
|
}
|
||||||
|
GLSL;
|
||||||
|
|
||||||
|
UI_FRAG_SRC :: #string GLSL
|
||||||
|
#version 330 core
|
||||||
|
in vec2 vUV;
|
||||||
|
in vec4 vColor;
|
||||||
|
in vec4 vParams; // corner_radius, border_width, rect_w, rect_h
|
||||||
|
|
||||||
|
out vec4 FragColor;
|
||||||
|
|
||||||
|
float roundedBoxSDF(vec2 center, vec2 half_size, float radius) {
|
||||||
|
vec2 q = abs(center) - half_size + vec2(radius);
|
||||||
|
return length(max(q, vec2(0.0))) + min(max(q.x, q.y), 0.0) - radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float radius = vParams.x;
|
||||||
|
float border = vParams.y;
|
||||||
|
vec2 rectSize = vParams.zw;
|
||||||
|
|
||||||
|
if (radius > 0.0 || border > 0.0) {
|
||||||
|
vec2 half_size = rectSize * 0.5;
|
||||||
|
vec2 center = (vUV - vec2(0.5)) * rectSize;
|
||||||
|
float dist = roundedBoxSDF(center, half_size, radius);
|
||||||
|
float aa = fwidth(dist);
|
||||||
|
float alpha = 1.0 - smoothstep(-aa, aa, dist);
|
||||||
|
|
||||||
|
if (border > 0.0) {
|
||||||
|
float inner = roundedBoxSDF(center, half_size - vec2(border), max(radius - border, 0.0));
|
||||||
|
float border_alpha = smoothstep(-aa, aa, inner);
|
||||||
|
alpha = alpha * max(border_alpha, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
FragColor = vec4(vColor.rgb, vColor.a * alpha);
|
||||||
|
} else {
|
||||||
|
FragColor = vColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GLSL;
|
||||||
181
ui/stacks.sx
Normal file
181
ui/stacks.sx
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
#import "modules/std.sx";
|
||||||
|
#import "modules/math";
|
||||||
|
#import "ui/types.sx";
|
||||||
|
#import "ui/render.sx";
|
||||||
|
#import "ui/events.sx";
|
||||||
|
#import "ui/view.sx";
|
||||||
|
#import "ui/layout.sx";
|
||||||
|
|
||||||
|
VStack :: struct {
|
||||||
|
children: List(ViewChild);
|
||||||
|
spacing: f32;
|
||||||
|
alignment: HAlignment;
|
||||||
|
|
||||||
|
add :: (self: *VStack, view: View) {
|
||||||
|
self.children.append(ViewChild.make(view));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for VStack {
|
||||||
|
size_that_fits :: (self: *VStack, proposal: ProposedSize) -> Size {
|
||||||
|
measure_vstack(@self.children, proposal, self.spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
layout :: (self: *VStack, bounds: Frame) {
|
||||||
|
layout_vstack(@self.children, bounds, self.spacing, self.alignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
render :: (self: *VStack, ctx: *RenderContext, frame: Frame) {
|
||||||
|
i := 0;
|
||||||
|
while i < self.children.len {
|
||||||
|
child := @self.children.items[i];
|
||||||
|
child.view.render(ctx, child.computed_frame);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_event :: (self: *VStack, event: *Event, frame: Frame) -> bool {
|
||||||
|
// Iterate children in reverse (front-to-back for overlapping)
|
||||||
|
i := self.children.len - 1;
|
||||||
|
while i >= 0 {
|
||||||
|
child := @self.children.items[i];
|
||||||
|
if child.view.handle_event(event, child.computed_frame) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
i -= 1;
|
||||||
|
}
|
||||||
|
false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack :: struct {
|
||||||
|
children: List(ViewChild);
|
||||||
|
spacing: f32;
|
||||||
|
alignment: VAlignment;
|
||||||
|
|
||||||
|
add :: (self: *HStack, view: View) {
|
||||||
|
self.children.append(ViewChild.make(view));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for HStack {
|
||||||
|
size_that_fits :: (self: *HStack, proposal: ProposedSize) -> Size {
|
||||||
|
measure_hstack(@self.children, proposal, self.spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
layout :: (self: *HStack, bounds: Frame) {
|
||||||
|
layout_hstack(@self.children, bounds, self.spacing, self.alignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
render :: (self: *HStack, ctx: *RenderContext, frame: Frame) {
|
||||||
|
i := 0;
|
||||||
|
while i < self.children.len {
|
||||||
|
child := @self.children.items[i];
|
||||||
|
child.view.render(ctx, child.computed_frame);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_event :: (self: *HStack, event: *Event, frame: Frame) -> bool {
|
||||||
|
i := self.children.len - 1;
|
||||||
|
while i >= 0 {
|
||||||
|
child := @self.children.items[i];
|
||||||
|
if child.view.handle_event(event, child.computed_frame) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
i -= 1;
|
||||||
|
}
|
||||||
|
false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ZStack :: struct {
|
||||||
|
children: List(ViewChild);
|
||||||
|
alignment: Alignment;
|
||||||
|
|
||||||
|
add :: (self: *ZStack, view: View) {
|
||||||
|
self.children.append(ViewChild.make(view));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for ZStack {
|
||||||
|
size_that_fits :: (self: *ZStack, proposal: ProposedSize) -> Size {
|
||||||
|
measure_zstack(@self.children, proposal);
|
||||||
|
}
|
||||||
|
|
||||||
|
layout :: (self: *ZStack, bounds: Frame) {
|
||||||
|
layout_zstack(@self.children, bounds, self.alignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
render :: (self: *ZStack, ctx: *RenderContext, frame: Frame) {
|
||||||
|
// Render back-to-front (first child is bottommost)
|
||||||
|
i := 0;
|
||||||
|
while i < self.children.len {
|
||||||
|
child := @self.children.items[i];
|
||||||
|
child.view.render(ctx, child.computed_frame);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_event :: (self: *ZStack, event: *Event, frame: Frame) -> bool {
|
||||||
|
// Handle front-to-back (last child is topmost)
|
||||||
|
i := self.children.len - 1;
|
||||||
|
while i >= 0 {
|
||||||
|
child := @self.children.items[i];
|
||||||
|
if child.view.handle_event(event, child.computed_frame) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
i -= 1;
|
||||||
|
}
|
||||||
|
false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spacer — fills available space
|
||||||
|
|
||||||
|
Spacer :: struct {
|
||||||
|
min_length: f32;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for Spacer {
|
||||||
|
size_that_fits :: (self: *Spacer, proposal: ProposedSize) -> Size {
|
||||||
|
w := proposal.width ?? self.min_length;
|
||||||
|
h := proposal.height ?? self.min_length;
|
||||||
|
Size.{ width = max(w, self.min_length), height = max(h, self.min_length) };
|
||||||
|
}
|
||||||
|
|
||||||
|
layout :: (self: *Spacer, bounds: Frame) {}
|
||||||
|
render :: (self: *Spacer, ctx: *RenderContext, frame: Frame) {}
|
||||||
|
handle_event :: (self: *Spacer, event: *Event, frame: Frame) -> bool { false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rect — simple colored rectangle view
|
||||||
|
|
||||||
|
RectView :: struct {
|
||||||
|
color: Color;
|
||||||
|
corner_radius: f32;
|
||||||
|
preferred_width: f32;
|
||||||
|
preferred_height: f32;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for RectView {
|
||||||
|
size_that_fits :: (self: *RectView, proposal: ProposedSize) -> Size {
|
||||||
|
w := proposal.width ?? self.preferred_width;
|
||||||
|
h := proposal.height ?? self.preferred_height;
|
||||||
|
Size.{ width = w, height = h };
|
||||||
|
}
|
||||||
|
|
||||||
|
layout :: (self: *RectView, bounds: Frame) {}
|
||||||
|
|
||||||
|
render :: (self: *RectView, ctx: *RenderContext, frame: Frame) {
|
||||||
|
if self.corner_radius > 0.0 {
|
||||||
|
ctx.add_rounded_rect(frame, self.color, self.corner_radius);
|
||||||
|
} else {
|
||||||
|
ctx.add_rect(frame, self.color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_event :: (self: *RectView, event: *Event, frame: Frame) -> bool { false; }
|
||||||
|
}
|
||||||
181
ui/types.sx
Normal file
181
ui/types.sx
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
#import "modules/std.sx";
|
||||||
|
#import "modules/math";
|
||||||
|
|
||||||
|
Point :: struct {
|
||||||
|
x, y: f32;
|
||||||
|
|
||||||
|
zero :: () -> Point { Point.{ x = 0.0, y = 0.0 }; }
|
||||||
|
|
||||||
|
add :: (self: Point, b: Point) -> Point {
|
||||||
|
Point.{ x = self.x + b.x, y = self.y + b.y };
|
||||||
|
}
|
||||||
|
|
||||||
|
sub :: (self: Point, b: Point) -> Point {
|
||||||
|
Point.{ x = self.x - b.x, y = self.y - b.y };
|
||||||
|
}
|
||||||
|
|
||||||
|
scale :: (self: Point, s: f32) -> Point {
|
||||||
|
Point.{ x = self.x * s, y = self.y * s };
|
||||||
|
}
|
||||||
|
|
||||||
|
distance :: (self: Point, b: Point) -> f32 {
|
||||||
|
dx := self.x - b.x;
|
||||||
|
dy := self.y - b.y;
|
||||||
|
sqrt(dx * dx + dy * dy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Size :: struct {
|
||||||
|
width, height: f32;
|
||||||
|
|
||||||
|
zero :: () -> Size { Size.{ width = 0.0, height = 0.0 }; }
|
||||||
|
|
||||||
|
contains :: (self: Size, point: Point) -> bool {
|
||||||
|
point.x >= 0.0 and point.x <= self.width and point.y >= 0.0 and point.y <= self.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Frame :: struct {
|
||||||
|
origin: Point;
|
||||||
|
size: Size;
|
||||||
|
|
||||||
|
zero :: () -> Frame { Frame.{ origin = Point.zero(), size = Size.zero() }; }
|
||||||
|
|
||||||
|
make :: (x: f32, y: f32, w: f32, h: f32) -> Frame {
|
||||||
|
Frame.{ origin = Point.{ x = x, y = y }, size = Size.{ width = w, height = h } };
|
||||||
|
}
|
||||||
|
|
||||||
|
max_x :: (self: Frame) -> f32 { self.origin.x + self.size.width; }
|
||||||
|
max_y :: (self: Frame) -> f32 { self.origin.y + self.size.height; }
|
||||||
|
|
||||||
|
contains :: (self: Frame, point: Point) -> bool {
|
||||||
|
point.x >= self.origin.x and point.x <= self.max_x()
|
||||||
|
and point.y >= self.origin.y and point.y <= self.max_y();
|
||||||
|
}
|
||||||
|
|
||||||
|
intersection :: (self: Frame, other: Frame) -> Frame {
|
||||||
|
x1 := max(self.origin.x, other.origin.x);
|
||||||
|
y1 := max(self.origin.y, other.origin.y);
|
||||||
|
x2 := min(self.max_x(), other.max_x());
|
||||||
|
y2 := min(self.max_y(), other.max_y());
|
||||||
|
if x2 <= x1 or y2 <= y1 then Frame.zero()
|
||||||
|
else Frame.make(x1, y1, x2 - x1, y2 - y1);
|
||||||
|
}
|
||||||
|
|
||||||
|
inset :: (self: Frame, insets: EdgeInsets) -> Frame {
|
||||||
|
Frame.make(
|
||||||
|
self.origin.x + insets.left,
|
||||||
|
self.origin.y + insets.top,
|
||||||
|
self.size.width - insets.left - insets.right,
|
||||||
|
self.size.height - insets.top - insets.bottom
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EdgeInsets :: struct {
|
||||||
|
top, left, bottom, right: f32;
|
||||||
|
|
||||||
|
zero :: () -> EdgeInsets { EdgeInsets.{ top = 0.0, left = 0.0, bottom = 0.0, right = 0.0 }; }
|
||||||
|
|
||||||
|
all :: (v: f32) -> EdgeInsets {
|
||||||
|
EdgeInsets.{ top = v, left = v, bottom = v, right = v };
|
||||||
|
}
|
||||||
|
|
||||||
|
symmetric :: (h: f32, v: f32) -> EdgeInsets {
|
||||||
|
EdgeInsets.{ top = v, left = h, bottom = v, right = h };
|
||||||
|
}
|
||||||
|
|
||||||
|
horizontal :: (self: EdgeInsets) -> f32 { self.left + self.right; }
|
||||||
|
vertical :: (self: EdgeInsets) -> f32 { self.top + self.bottom; }
|
||||||
|
}
|
||||||
|
|
||||||
|
Color :: struct {
|
||||||
|
r, g, b, a: u8;
|
||||||
|
|
||||||
|
rgba :: (r: u8, g: u8, b: u8, a: u8) -> Color {
|
||||||
|
Color.{ r = r, g = g, b = b, a = a };
|
||||||
|
}
|
||||||
|
|
||||||
|
rgb :: (r: u8, g: u8, b: u8) -> Color {
|
||||||
|
Color.{ r = r, g = g, b = b, a = 255 };
|
||||||
|
}
|
||||||
|
|
||||||
|
rf :: (self: Color) -> f32 { xx self.r / 255.0; }
|
||||||
|
gf :: (self: Color) -> f32 { xx self.g / 255.0; }
|
||||||
|
bf :: (self: Color) -> f32 { xx self.b / 255.0; }
|
||||||
|
af :: (self: Color) -> f32 { xx self.a / 255.0; }
|
||||||
|
|
||||||
|
with_alpha :: (self: Color, a: u8) -> Color {
|
||||||
|
Color.{ r = self.r, g = self.g, b = self.b, a = a };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Named color constants
|
||||||
|
COLOR_WHITE :: Color.{ r = 255, g = 255, b = 255, a = 255 };
|
||||||
|
COLOR_BLACK :: Color.{ r = 0, g = 0, b = 0, a = 255 };
|
||||||
|
COLOR_RED :: Color.{ r = 255, g = 59, b = 48, a = 255 };
|
||||||
|
COLOR_GREEN :: Color.{ r = 52, g = 199, b = 89, a = 255 };
|
||||||
|
COLOR_BLUE :: Color.{ r = 0, g = 122, b = 255, a = 255 };
|
||||||
|
COLOR_YELLOW :: Color.{ r = 255, g = 204, b = 0, a = 255 };
|
||||||
|
COLOR_ORANGE :: Color.{ r = 255, g = 149, b = 0, a = 255 };
|
||||||
|
COLOR_GRAY :: Color.{ r = 142, g = 142, b = 147, a = 255 };
|
||||||
|
COLOR_DARK_GRAY :: Color.{ r = 44, g = 44, b = 46, a = 255 };
|
||||||
|
COLOR_LIGHT_GRAY :: Color.{ r = 209, g = 209, b = 214, a = 255 };
|
||||||
|
COLOR_TRANSPARENT :: Color.{ r = 0, g = 0, b = 0, a = 0 };
|
||||||
|
|
||||||
|
// Size proposal — optional dimensions (null = flexible)
|
||||||
|
ProposedSize :: struct {
|
||||||
|
width: ?f32;
|
||||||
|
height: ?f32;
|
||||||
|
|
||||||
|
fixed :: (w: f32, h: f32) -> ProposedSize {
|
||||||
|
ProposedSize.{ width = w, height = h };
|
||||||
|
}
|
||||||
|
|
||||||
|
flexible :: () -> ProposedSize {
|
||||||
|
ProposedSize.{ width = null, height = null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HAlignment :: enum {
|
||||||
|
leading;
|
||||||
|
center;
|
||||||
|
trailing;
|
||||||
|
}
|
||||||
|
|
||||||
|
VAlignment :: enum {
|
||||||
|
top;
|
||||||
|
center;
|
||||||
|
bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
Alignment :: struct {
|
||||||
|
h: HAlignment;
|
||||||
|
v: VAlignment;
|
||||||
|
}
|
||||||
|
|
||||||
|
ALIGN_CENTER :: Alignment.{ h = .center, v = .center };
|
||||||
|
ALIGN_TOP_LEADING :: Alignment.{ h = .leading, v = .top };
|
||||||
|
ALIGN_TOP :: Alignment.{ h = .center, v = .top };
|
||||||
|
ALIGN_TOP_TRAILING :: Alignment.{ h = .trailing, v = .top };
|
||||||
|
ALIGN_LEADING :: Alignment.{ h = .leading, v = .center };
|
||||||
|
ALIGN_TRAILING :: Alignment.{ h = .trailing, v = .center };
|
||||||
|
ALIGN_BOTTOM :: Alignment.{ h = .center, v = .bottom };
|
||||||
|
|
||||||
|
// Compute x offset for a child of child_width inside container_width
|
||||||
|
align_h :: (alignment: HAlignment, child_width: f32, container_width: f32) -> f32 {
|
||||||
|
if alignment == {
|
||||||
|
case .leading: 0.0;
|
||||||
|
case .center: { (container_width - child_width) * 0.5; }
|
||||||
|
case .trailing: container_width - child_width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute y offset for a child of child_height inside container_height
|
||||||
|
align_v :: (alignment: VAlignment, child_height: f32, container_height: f32) -> f32 {
|
||||||
|
if alignment == {
|
||||||
|
case .top: 0.0;
|
||||||
|
case .center: { (container_height - child_height) * 0.5; }
|
||||||
|
case .bottom: container_height - child_height;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
ui/view.sx
Normal file
30
ui/view.sx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#import "ui/types.sx";
|
||||||
|
#import "ui/render.sx";
|
||||||
|
#import "ui/events.sx";
|
||||||
|
|
||||||
|
View :: protocol {
|
||||||
|
// Measure: given a size proposal, return desired size
|
||||||
|
size_that_fits :: (proposal: ProposedSize) -> Size;
|
||||||
|
|
||||||
|
// Place: position children within the given bounds
|
||||||
|
layout :: (bounds: Frame);
|
||||||
|
|
||||||
|
// Render: emit render nodes
|
||||||
|
render :: (ctx: *RenderContext, frame: Frame);
|
||||||
|
|
||||||
|
// Event handling: return true if the event was consumed
|
||||||
|
handle_event :: (event: *Event, frame: Frame) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A child view with its computed frame (set during layout)
|
||||||
|
ViewChild :: struct {
|
||||||
|
view: View;
|
||||||
|
computed_frame: Frame;
|
||||||
|
|
||||||
|
make :: (view: View) -> ViewChild {
|
||||||
|
ViewChild.{
|
||||||
|
view = view,
|
||||||
|
computed_frame = Frame.zero()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user