#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(); }