From 6b4d7fbc54ebcfd91720022f22342a1888b12242 Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 24 Feb 2026 19:22:05 +0200 Subject: [PATCH] ui pipeline --- goldens/last_frame.png | Bin 0 -> 20961 bytes main.sx | 261 ++++++++--------------------- modules/allocators.sx | 155 ----------------- modules/math/math.sx | 5 - modules/math/matrix44.sx | 59 ------- modules/math/vector3.sx | 22 --- modules/opengl.sx | 98 ----------- modules/raylib.sx | 17 -- modules/sdl3.sx | 334 ------------------------------------- modules/socket.sx | 33 ---- modules/stb.sx | 6 - modules/std.sx | 352 --------------------------------------- ui/button.sx | 94 +++++++++++ ui/events.sx | 106 ++++++++++++ ui/label.sx | 44 +++++ ui/layout.sx | 152 +++++++++++++++++ ui/pipeline.sx | 72 ++++++++ ui/render.sx | 180 ++++++++++++++++++++ ui/renderer.sx | 279 +++++++++++++++++++++++++++++++ ui/stacks.sx | 181 ++++++++++++++++++++ ui/types.sx | 181 ++++++++++++++++++++ ui/view.sx | 30 ++++ 22 files changed, 1388 insertions(+), 1273 deletions(-) create mode 100644 goldens/last_frame.png delete mode 100644 modules/allocators.sx delete mode 100644 modules/math/math.sx delete mode 100644 modules/math/matrix44.sx delete mode 100644 modules/math/vector3.sx delete mode 100644 modules/opengl.sx delete mode 100644 modules/raylib.sx delete mode 100644 modules/sdl3.sx delete mode 100644 modules/socket.sx delete mode 100644 modules/stb.sx delete mode 100644 modules/std.sx create mode 100644 ui/button.sx create mode 100644 ui/events.sx create mode 100644 ui/label.sx create mode 100644 ui/layout.sx create mode 100644 ui/pipeline.sx create mode 100644 ui/render.sx create mode 100644 ui/renderer.sx create mode 100644 ui/stacks.sx create mode 100644 ui/types.sx create mode 100644 ui/view.sx diff --git a/goldens/last_frame.png b/goldens/last_frame.png new file mode 100644 index 0000000000000000000000000000000000000000..31f294277a93dc2d99bacfa489aeb165a74866b0 GIT binary patch literal 20961 zcmeHPYg7~07M_HF0TUU)N{C8`52~#dl!zz-k!VoM%ld8=qjGhsR()4GvjoxU^9pX0r_oBgsbAf?~rP9@f zs3!)Uv@ihTd`nuj^q*r3(WEt^t{1_wl`}%tG?cCM8vS^;u~5DT(TsFEm5geoEt6v&VW z4xJF~v>cdm0vlu*;UISmjaLpUK-P^Sgky_Hl1Tu1j14tTWVxrS{9lBY5N&1Sdfq}P zg27500^DFP=>h~hY$7;I36WU*0j_6aVB0k(h+uhY`p2n1vznOrS+(V+d>< ziL>D*AscWbSrHm3&mAYjVn7B!20#Wt217*n^CN?(NgJA(As_BTlqq0g#h6hv^={03 z@q1E_ruuQOim((hZWR4Fu{VZgOR2_8sU)##^cHFOW#3VB!T^FP%iLa_J^EqNlt|y$ zjzis@NF-rcGbVDSa+~rp>G0RJ{$0qyH@!X`%g?$i zga@?snd4%}Wh&)#@^R9_>h~rg#4G?)=48WvrZ1bzHk$yS}zA z0?qeh))B@PIhz|q6&T>D>%&SZ|eGj z5v13M3`r6jlAp1}aW%>qSHC9|KYs|Um6DppU zEF9gJJkD_-%O2$Knw0&us_rL!FFgGt^xg#aOy? z%znms1EYe?UyR6%&t{Ic;)Q&A=bmVsH8h4&gs!HFt|l)MI`LEo}CWv)xqP%ih}Ele*8Pd`-xr=fZQdkIUP5F;tP) z@q||~qLkI~8~iFBtHL(uw1?MoXeKAXq1XiC6Ys@O&7BUsGmvVBJ(xJZZQ=~wLssB9 z3O9f&+TWHi!KZ}oGpt@^vnA-MqA<7cW`V++>4zclVZalWP>c#041Kt!^M(3bmT;vM z)tzpuQ3*-wevUtsq_#RX@&(;1_J(Tmd(}@Rg_1&Akh~k=I3+7!O@1qis!$xK~DWp1CjxbYt=Mb7m2LoVdY zg_3`?PnPVeV4&eunilChQ)@`g(W`2AIMhTedirofU4+a1K=~u&W)gITacuk*igt|6 zvfG>er8llHgB@@>(P#!|yck=8)Q_*}_F8rE|bVEG&t)OM4OyvyC4P~ow* zM6Jy~b8E9lWj7;uo}3NYlET6vwQbmA2K}&s9HjY12>xv7ihC$=zkSx@O*`(CA%Dl7 zor3YEu3llHaX-ai-*gHrDMqL=G2{^{>bQ+$;Y&gy9=`Me^LY@H98gG`EsvjS;El+c ze5#J_=9i+3;iT`wo_gFB|+bYyHu1 zP5TS9LCMAH+LW+B0I99RPC{z>y1d$Qb!bq|h1eUds8(ev|6-73*YS@{WxC>3YzGYm z?49P$xHxscmFEyo$8lD*hz1+r{Gsteg|gD0MiaX0db-++ zr8(#Fh5C}QUbMK?x>L7WwWy(HZ|IwA_8ijOR|r>Y&rMGsh7aDAt(fS3!E4r1zjP+y z#=S-dyz<$xa$squc^}>&sC*tAB8U=GQ`d8{%=N9W=~OhuxYMEdwI`bZeT|@ OLJQ`F%sn3<*z-R?wuhzw literal 0 HcmV?d00001 diff --git a/main.sx b/main.sx index 31fe903..eac8343 100644 --- a/main.sx +++ b/main.sx @@ -2,118 +2,89 @@ #import "modules/sdl3.sx"; #import "modules/opengl.sx"; #import "modules/math"; -stb :: #import "modules/stb.sx"; +#import "modules/stb.sx"; +#import "ui"; WIDTH :f32: 800; 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 { - print("init video: \n"); SDL_Init(SDL_INIT_VIDEO); - print("init opengl: \n"); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_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_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG); SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); - print("create window: \n"); - window := SDL_CreateWindow("SX Game", xx WIDTH, xx HEIGHT, SDL_WINDOW_OPENGL); + window := SDL_CreateWindow("SX UI Demo", xx WIDTH, xx HEIGHT, SDL_WINDOW_OPENGL); gl_ctx := SDL_GL_CreateContext(window); SDL_GL_MakeCurrent(window, gl_ctx); SDL_GL_SetSwapInterval(1); - print("load gl: \n"); load_gl(SDL_GL_GetProcAddress); + load_gl_ui(SDL_GL_GetProcAddress); - glEnable(GL_DEPTH_TEST); - glDepthFunc(GL_LESS); - - program := create_program(VERT_SHADER_SRC, FRAG_SHADER_SRC); - glUseProgram(program); - - print("uniform locations: \n"); - mvp_loc := glGetUniformLocation(program, "uMVP"); - light_loc := glGetUniformLocation(program, "uLightDir"); - wire_loc := glGetUniformLocation(program, "uWire"); + // --- Build UI --- + pipeline : UIPipeline = ---; + pipeline.init(WIDTH, HEIGHT); - print("vertices\n"); - // Cube vertices: pos(vec4 w=1) + normal(vec4 w=0), 36 vertices × 2 vec4s = 72 - 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"); - vao : u32 = 0; - vbo : u32 = 0; - glGenVertexArrays(1, @vao); - glGenBuffers(1, @vbo); + // Create a simple layout: VStack with colored rects and a button + root := VStack.{ spacing = 10.0, alignment = .center }; - glBindVertexArray(vao); - glBindBuffer(GL_ARRAY_BUFFER, vbo); - glBufferData(GL_ARRAY_BUFFER, 1152, xx vertices, GL_STATIC_DRAW); + header := RectView.{ color = COLOR_YELLOW, preferred_height = 80.0, corner_radius = 8.0 }; + root.add(xx header); - glVertexAttribPointer(0, 3, GL_FLOAT, 0, 32, xx 0); - glEnableVertexAttribArray(0); + btn := Button.{ label = "Click Me", font_size = 14.0, style = ButtonStyle.default(), on_tap = null }; + root.add(xx btn); - glVertexAttribPointer(1, 3, GL_FLOAT, 0, 32, xx 16); - glEnableVertexAttribArray(1); + body := HStack.{ spacing = 10.0, alignment = .center }; - glUniform3f(light_loc, 0.5, 0.7, 1.0); - glUniform1f(wire_loc, 0.0); + left := RectView.{ color = COLOR_RED, preferred_width = 200.0, preferred_height = 300.0, corner_radius = 4.0 }; + body.add(xx left); + right := RectView.{ color = COLOR_GREEN, preferred_width = 200.0, preferred_height = 300.0, corner_radius = 4.0 }; + body.add(xx right); + + root.add(xx body); + + footer := RectView.{ color = COLOR_DARK_GRAY, preferred_height = 60.0 }; + root.add(xx footer); + + pipeline.set_root(xx root); + + // --- Main loop --- running := true; - event : SDL_Event = .none; + sdl_event : SDL_Event = .none; - print("loop: \n"); while running { - while SDL_PollEvent(event) { - if event == { + while SDL_PollEvent(sdl_event) { + if sdl_event == { case .quit: running = false; case .key_up: (e) { 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(); - ms : f32 = xx ticks; - angle := ms * 0.001; + glClearColor(0.12, 0.12, 0.15, 1.0); + glClear(GL_COLOR_BUFFER_BIT); - proj := mat4_perspective(PI/ 4.0, WIDTH / HEIGHT, 0.1, 100.0); - 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); + pipeline.tick(); 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_DestroyWindow(window); 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; diff --git a/modules/allocators.sx b/modules/allocators.sx deleted file mode 100644 index 1cf3b84..0000000 --- a/modules/allocators.sx +++ /dev/null @@ -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; -} diff --git a/modules/math/math.sx b/modules/math/math.sx deleted file mode 100644 index c341b0f..0000000 --- a/modules/math/math.sx +++ /dev/null @@ -1,5 +0,0 @@ -PI :f32: 3.14159265; - -sqrt :: (x: $T) -> T #builtin; -sin :: (x: $T) -> T #builtin; -cos :: (x: $T) -> T #builtin; diff --git a/modules/math/matrix44.sx b/modules/math/matrix44.sx deleted file mode 100644 index 300e8d4..0000000 --- a/modules/math/matrix44.sx +++ /dev/null @@ -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; diff --git a/modules/math/vector3.sx b/modules/math/vector3.sx deleted file mode 100644 index 3a423ee..0000000 --- a/modules/math/vector3.sx +++ /dev/null @@ -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; diff --git a/modules/opengl.sx b/modules/opengl.sx deleted file mode 100644 index 06b0878..0000000 --- a/modules/opengl.sx +++ /dev/null @@ -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"); -} diff --git a/modules/raylib.sx b/modules/raylib.sx deleted file mode 100644 index 80e0d3d..0000000 --- a/modules/raylib.sx +++ /dev/null @@ -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; diff --git a/modules/sdl3.sx b/modules/sdl3.sx deleted file mode 100644 index 6a3f612..0000000 --- a/modules/sdl3.sx +++ /dev/null @@ -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; diff --git a/modules/socket.sx b/modules/socket.sx deleted file mode 100644 index ebc0eab..0000000 --- a/modules/socket.sx +++ /dev/null @@ -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)); -} diff --git a/modules/stb.sx b/modules/stb.sx deleted file mode 100644 index 7b8c866..0000000 --- a/modules/stb.sx +++ /dev/null @@ -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"; -}; diff --git a/modules/std.sx b/modules/std.sx deleted file mode 100644 index 8eb4bd4..0000000 --- a/modules/std.sx +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/ui/button.sx b/ui/button.sx new file mode 100644 index 0000000..7537fd1 --- /dev/null +++ b/ui/button.sx @@ -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; + } +} diff --git a/ui/events.sx b/ui/events.sx new file mode 100644 index 0000000..4c27f1c --- /dev/null +++ b/ui/events.sx @@ -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); +} diff --git a/ui/label.sx b/ui/label.sx new file mode 100644 index 0000000..cac3e00 --- /dev/null +++ b/ui/label.sx @@ -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; + } +} diff --git a/ui/layout.sx b/ui/layout.sx new file mode 100644 index 0000000..a5f37c0 --- /dev/null +++ b/ui/layout.sx @@ -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 }; +} diff --git a/ui/pipeline.sx b/ui/pipeline.sx new file mode 100644 index 0000000..725b86b --- /dev/null +++ b/ui/pipeline.sx @@ -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); + } +} diff --git a/ui/render.sx b/ui/render.sx new file mode 100644 index 0000000..58ce9c3 --- /dev/null +++ b/ui/render.sx @@ -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; + } +} diff --git a/ui/renderer.sx b/ui/renderer.sx new file mode 100644 index 0000000..85dabd5 --- /dev/null +++ b/ui/renderer.sx @@ -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; diff --git a/ui/stacks.sx b/ui/stacks.sx new file mode 100644 index 0000000..37a5622 --- /dev/null +++ b/ui/stacks.sx @@ -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; } +} diff --git a/ui/types.sx b/ui/types.sx new file mode 100644 index 0000000..a200ad3 --- /dev/null +++ b/ui/types.sx @@ -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; + } +} diff --git a/ui/view.sx b/ui/view.sx new file mode 100644 index 0000000..ad666b7 --- /dev/null +++ b/ui/view.sx @@ -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() + }; + } +}