Files
game/main.sx
2026-03-03 13:25:25 +02:00

268 lines
9.3 KiB
Plaintext

#import "modules/std.sx";
#import "build.sx";
#import "modules/compiler.sx";
#import "modules/sdl3.sx";
#import "modules/opengl.sx";
#import "modules/math";
#import "modules/stb.sx";
#import "modules/stb_truetype.sx";
#import "modules/wasm.sx";
#import "ui";
#run configure_build();
// --- Frame state (globals for emscripten callback) ---
g_window : *void = ---;
g_pipeline : *UIPipeline = ---;
g_running : bool = true;
g_width : s32 = 800; // logical window size
g_height : s32 = 600;
g_pixel_w : s32 = 800; // physical pixel size
g_pixel_h : s32 = 600;
load_texture :: (path: [:0]u8) -> u32 {
w : s32 = 0;
h : s32 = 0;
ch : s32 = 0;
pixels := stbi_load(path, @w, @h, @ch, 4);
if xx pixels == 0 { return 0; }
tex : u32 = 0;
glGenTextures(1, @tex);
glBindTexture(GL_TEXTURE_2D, tex);
glTexImage2D(GL_TEXTURE_2D, 0, xx GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, xx GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, xx GL_LINEAR);
stbi_image_free(pixels);
tex;
}
save_snapshot :: (path: [:0]u8, w: s32, h: s32) {
stride : s64 = w * 4;
buf_size : s64 = stride * 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");
}
run_ui_tests :: (pipeline: *UIPipeline) {
// Do a layout pass first so frames are computed
glClearColor(0.12, 0.12, 0.15, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
pipeline.tick();
// Button "Click Me" is roughly at center x=400, y=165 based on golden
btn_x : f32 = 400.0;
btn_y : f32 = 165.0;
btn_pos := Point.{ x = btn_x, y = btn_y };
// Empty area below the rects
empty_x : f32 = 400.0;
empty_y : f32 = 500.0;
// --- Test 1: Click button (no drag) ---
out("=== Test 1: button click ===\n");
out("expect: Button tapped!\n");
e :Event = .mouse_down(MouseButtonData.{ position = btn_pos, button = .left });
pipeline.dispatch_event(@e);
e = .mouse_up(MouseButtonData.{ position = btn_pos, button = .left });
pipeline.dispatch_event(@e);
out("=== end test 1 ===\n\n");
// --- Test 2: Drag from button area (should scroll, NOT tap) ---
out("=== Test 2: drag from button ===\n");
out("expect: NO Button tapped, scroll should move\n");
e = .mouse_down(MouseButtonData.{ position = btn_pos, button = .left });
pipeline.dispatch_event(@e);
// Move down 50px in steps (exceeds 4px threshold)
i : s32 = 1;
while i <= 10 {
e = .mouse_moved(MouseMotionData.{
position = Point.{ x = btn_x, y = btn_y + xx i * 5.0 },
delta = Point.{ x = 0.0, y = 5.0 }
});
pipeline.dispatch_event(@e);
i += 1;
}
e = .mouse_up(MouseButtonData.{ position = Point.{ x = btn_x, y = btn_y + 50.0 }, button = .left });
pipeline.dispatch_event(@e);
out("=== end test 2 ===\n\n");
// --- Test 3: Drag from empty area (should scroll) ---
out("=== Test 3: drag from empty area ===\n");
out("expect: scroll should move\n");
e = .mouse_down(MouseButtonData.{ position = Point.{ x = empty_x, y = empty_y }, button = .left });
pipeline.dispatch_event(@e);
i = 1;
while i <= 10 {
e = .mouse_moved(MouseMotionData.{
position = Point.{ x = empty_x, y = empty_y - xx i * 5.0 },
delta = Point.{ x = 0.0, y = 0.0 - 5.0 }
});
pipeline.dispatch_event(@e);
i += 1;
}
e = .mouse_up(MouseButtonData.{ position = Point.{ x = empty_x, y = empty_y - 50.0 }, button = .left });
pipeline.dispatch_event(@e);
out("=== end test 3 ===\n\n");
// Render after tests and save snapshot to see scrolled state
glClear(GL_COLOR_BUFFER_BIT);
pipeline.tick();
save_snapshot("goldens/test_after_drag.png", g_pixel_w, g_pixel_h);
}
// One frame of the main loop — called repeatedly by emscripten or desktop while-loop
frame :: () {
sdl_event : SDL_Event = .none;
while SDL_PollEvent(@sdl_event) {
print("SDL event: {}\n", sdl_event.tag);
if sdl_event == {
case .quit: { g_running = false; }
case .key_up: (e) {
if e.key == { case .escape: { g_running = false; } }
}
case .window_resized: (data) {
g_width = data.data1;
g_height = data.data2;
SDL_GetWindowSizeInPixels(g_window, @g_pixel_w, @g_pixel_h);
g_pipeline.resize(xx g_width, xx g_height);
}
}
ui_event := translate_sdl_event(@sdl_event);
if ui_event != .none {
print(" ui event dispatched\n");
g_pipeline.dispatch_event(@ui_event);
} else {
print(" -> .none\n");
}
}
glViewport(0, 0, g_pixel_w, g_pixel_h);
glClearColor(0.12, 0.12, 0.15, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
g_pipeline.*.tick();
SDL_GL_SwapWindow(g_window);
}
main :: () -> void {
SDL_Init(SDL_INIT_VIDEO);
inline if OS == .wasm {
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
} else {
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
}
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
// Auto-size: on desktop use 75% of usable display, on WASM SDL picks up canvas size
init_w : s32 = 800;
init_h : s32 = 600;
inline if OS == .wasm {
init_w = emscripten_run_script_int("window.innerWidth");
init_h = emscripten_run_script_int("window.innerHeight");
} else {
display_id := SDL_GetPrimaryDisplay();
bounds : SDL_Rect = ---;
if SDL_GetDisplayUsableBounds(display_id, @bounds) {
init_w = bounds.w * 3 / 4;
init_h = bounds.h * 3 / 4;
}
}
window := SDL_CreateWindow("SX UI Demo", init_w, init_h, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY);
gl_ctx := SDL_GL_CreateContext(window);
SDL_GL_MakeCurrent(window, gl_ctx);
SDL_GL_SetSwapInterval(1);
load_gl(xx SDL_GL_GetProcAddress);
// Query actual window size (may differ from requested, especially on WASM)
SDL_GetWindowSize(window, @g_width, @g_height);
SDL_GetWindowSizeInPixels(window, @g_pixel_w, @g_pixel_h);
width_f : f32 = xx g_width;
height_f : f32 = xx g_height;
dpi_scale : f32 = if width_f > 0.0 then xx g_pixel_w / width_f else 1.0;
// Set viewport to physical pixel dimensions
glViewport(0, 0, g_pixel_w, g_pixel_h);
// --- Build UI ---
pipeline : UIPipeline = ---;
pipeline.init(width_f, height_f);
pipeline.init_font("assets/fonts/default.ttf", 32.0, dpi_scale);
scroll_content := VStack.{ spacing = 10.0, alignment = .center } {
self.add(
Label.{ text = "Hello, SX!", font_size = 24.0, color = COLOR_WHITE }
|> padding(EdgeInsets.all(8.0))
);
self.add(
RectView.{ color = COLOR_YELLOW, preferred_height = 80.0, corner_radius = 8.0 }
|> padding(EdgeInsets.all(8.0))
|> on_tap(closure(() { out("Yellow tapped!\n"); }))
);
self.add(
Button.{ label = "Click Me", font_size = 14.0, style = ButtonStyle.default(), on_tap = closure(() { out("Button tapped!\n"); }) }
);
self.add(HStack.{ spacing = 10.0, alignment = .center } {
self.add(RectView.{ color = COLOR_RED, preferred_width = 200.0, preferred_height = 300.0, corner_radius = 4.0 });
self.add(RectView.{ color = COLOR_GREEN, preferred_width = 200.0, preferred_height = 300.0, corner_radius = 4.0 });
});
self.add(
RectView.{ color = COLOR_DARK_GRAY, preferred_height = 60.0 }
|> padding(.symmetric(16.0, 8.0))
|> background(COLOR_BLUE, 8.0)
);
self.add(RectView.{ color = COLOR_ORANGE, preferred_height = 120.0, corner_radius = 12.0 });
self.add(RectView.{ color = COLOR_GRAY, preferred_height = 200.0, corner_radius = 8.0 });
};
root := ScrollView.{ child = ViewChild.{ view = scroll_content }, axes = .vertical };
pipeline.set_root(root);
// Store state in globals for frame callback
g_window = xx window;
g_pipeline = @pipeline;
// --- Main loop ---
inline if OS == .wasm {
emscripten_set_main_loop(xx frame, 0, 1);
} else {
while g_running {
frame();
}
}
save_snapshot("goldens/last_frame.png", g_pixel_w, g_pixel_h);
SDL_GL_DestroyContext(gl_ctx);
SDL_DestroyWindow(window);
SDL_Quit();
}