diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-03 14:43:30 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:07 -0300 |
| commit | d1ed5bd8f55935e6470c4a7559216119571a084f (patch) | |
| tree | 434478ce8a5dcd9aed367334816f54da9953e56d /ui_sdl.zig | |
| parent | 153f0a88862507cf1f5dda71f71db3d6649dfeaa (diff) | |
| download | pardes-d1ed5bd8f55935e6470c4a7559216119571a084f.tar.gz pardes-d1ed5bd8f55935e6470c4a7559216119571a084f.zip | |
fixed scaling and fitting on web ui
Diffstat (limited to 'ui_sdl.zig')
| -rw-r--r-- | ui_sdl.zig | 185 |
1 files changed, 149 insertions, 36 deletions
@@ -22,6 +22,7 @@ const std = @import("std"); const builtin = @import("builtin"); const vaxis = @import("vaxis"); +const ui_sdl_config = @import("ui_sdl_config"); const posix = std.posix; const linux = std.os.linux; const is_emscripten = builtin.os.tag == .emscripten; @@ -33,6 +34,9 @@ pub const c = @cImport({ @cInclude("font.h"); }); +extern "c" fn emscripten_get_element_css_size(target: [*:0]const u8, width: *f64, height: *f64) c_int; +const EMSCRIPTEN_RESULT_SUCCESS: c_int = 0; + // ---- shared (backend-agnostic) types: re-exported straight from libvaxis ---- pub const Key = vaxis.Key; pub const Mouse = vaxis.Mouse; @@ -69,6 +73,8 @@ const vert_glsl_es = if (is_emscripten) @embedFile("ui.vert.es.glsl") else ""; const frag_glsl_es = if (is_emscripten) @embedFile("ui.frag.es.glsl") else ""; const log = std.log.scoped(.ui_sdl); +const web_render_scale: f32 = ui_sdl_config.web_render_scale; +const web_render_scale_f64: f64 = @floatCast(web_render_scale); // A standard 16-color ANSI palette (xterm), used to resolve vaxis.Color.index // cells — the dark theme passes palette indices through (palColor), and there's @@ -161,8 +167,8 @@ reported_cols: u16 = 0, reported_rows: u16 = 0, // ---- constants + helper types (must come after all fields in Zig 0.16) ---- -pub const atlas_w: u32 = 1024; -pub const atlas_h: u32 = 1024; +pub const atlas_w: u32 = if (is_emscripten) 2048 else 1024; +pub const atlas_h: u32 = if (is_emscripten) 2048 else 1024; pub const Slot = struct { u: u32, v: u32 }; pub const CellKey = packed struct { cp: u32, fg: u32, bg: u32 }; pub const DirtyRange = struct { start: u32, count: u32 }; @@ -186,6 +192,25 @@ const CellInstance = extern struct { bb: f32, }; +const CellLayout = struct { + w: f32, + h: f32, + x_off: f32, + y_off: f32, +}; + +const MouseCell = struct { + col: i16, + row: i16, +}; + +fn webGlFilter() c_int { + return switch (ui_sdl_config.web_filter) { + .linear => c.GL_LINEAR, + .nearest => c.GL_NEAREST, + }; +} + fn envU16(env_map: *std.process.Environ.Map, name: []const u8) ?u16 { const raw = env_map.get(name) orelse return null; return std.fmt.parseInt(u16, raw, 10) catch null; @@ -455,18 +480,23 @@ fn initGl( return error.FontInit; }; const px: f32 = 18.0; - const scale = c.ui_font_scale_for_height(font, px); + const base_scale = c.ui_font_scale_for_height(font, px); var cw: c_int = 10; var chh: c_int = 20; var asc: c_int = 16; - c.ui_font_cell_metrics(font, scale, &cw, &chh, &asc); + c.ui_font_cell_metrics(font, base_scale, &cw, &chh, &asc); + const scale = c.ui_font_scale_for_height(font, px * web_render_scale); + const cell_w: u32 = @intFromFloat(@ceil(@as(f32, @floatFromInt(@max(cw, 1))) * web_render_scale)); + const cell_h: u32 = @intFromFloat(@ceil(@as(f32, @floatFromInt(@max(chh, 1))) * web_render_scale)); + const ascent: c_int = @intFromFloat(@ceil(@as(f32, @floatFromInt(@max(asc, 1))) * web_render_scale)); const program = try makeGlProgram(); var atlas_tex: c_uint = 0; c.glGenTextures(1, &atlas_tex); c.glBindTexture(c.GL_TEXTURE_2D, atlas_tex); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, c.GL_LINEAR); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, c.GL_LINEAR); + const filter = webGlFilter(); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, filter); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, filter); c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_CLAMP_TO_EDGE); c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_CLAMP_TO_EDGE); c.glPixelStorei(c.GL_UNPACK_ALIGNMENT, 1); @@ -498,9 +528,9 @@ fn initGl( .gl_u_atlas = c.glGetUniformLocation(program, "u_atlas"), .font = font, .scale = scale, - .cell_w = @intCast(@max(cw, 1)), - .cell_h = @intCast(@max(chh, 1)), - .ascent = asc, + .cell_w = cell_w, + .cell_h = cell_h, + .ascent = ascent, .screen = .{}, .atlas_stage = atlas_stage, .glyphs = std.AutoHashMap(u32, Slot).init(alloc), @@ -511,7 +541,7 @@ fn initGl( .trace_fd = trace_fd, .trace_every = @max(trace_every, 1), }; - _ = c.ui_font_raster(font, scale, ' ', atlas_stage.ptr, @intCast(atlas_w), @intCast(self.cell_w), @intCast(self.cell_h), asc); + _ = c.ui_font_raster(font, scale, ' ', atlas_stage.ptr, @intCast(atlas_w), @intCast(self.cell_w), @intCast(self.cell_h), self.ascent); self.space_slot = .{ .u = 0, .v = 0 }; self.pen_x = self.cell_w; self.tracef("sdl.init browser=webgl2 trace_every={d}", .{self.trace_every}); @@ -598,7 +628,7 @@ pub fn resize(self: *SdlBackend, alloc: std.mem.Allocator, writer: *std.Io.Write self.screen.deinit(alloc); self.screen = try vaxis.Screen.init(alloc, ws); self.screen.width_method = .wcwidth; - if (self.capture_images or is_emscripten) { + if (self.capture_images) { _ = c.SDL_SetWindowSize( self.sdl_window, @intCast(@as(u32, ws.cols) * self.cell_w), @@ -690,8 +720,7 @@ pub fn render(self: *SdlBackend, writer: *std.Io.Writer) !void { }); const instances: [*]CellInstance = @ptrCast(@alignCast(vptr)); t_mapped = nowNs(); - const cw_f: f32 = @floatFromInt(self.cell_w); - const ch_f: f32 = @floatFromInt(self.cell_h); + const layout = self.fixedCellLayout(); var r: u16 = 0; while (r < rows) : (r += 1) { var col: u16 = 0; @@ -702,7 +731,7 @@ pub fn render(self: *SdlBackend, writer: *std.Io.Writer) !void { self.cell_keys[cell_index] = key; self.addDirtyRange(&dirty_ranges, cell_index); dirty_cells += 1; - self.emitInstance(instances, cell_index, col, r, cw_f, ch_f, win_w, win_h, key); + self.emitInstance(instances, cell_index, col, r, layout, win_w, win_h, key); } } c.SDL_UnmapGPUTransferBuffer(self.device, self.vxfer); @@ -813,8 +842,7 @@ fn renderGl(self: *SdlBackend) !void { } try self.ensureGlInstances(cells); - const cw_f: f32 = @floatFromInt(self.cell_w); - const ch_f: f32 = @floatFromInt(self.cell_h); + const layout = self.cellLayout(sw, sh); const win_w: f32 = @floatFromInt(sw); const win_h: f32 = @floatFromInt(sh); var r: u16 = 0; @@ -822,7 +850,7 @@ fn renderGl(self: *SdlBackend) !void { var col: u16 = 0; while (col < cols) : (col += 1) { const cell_index: u32 = @as(u32, r) * @as(u32, cols) + @as(u32, col); - self.emitInstance(self.gl_instances.ptr, cell_index, col, r, cw_f, ch_f, win_w, win_h, self.cellKey(self.screen.buf[cell_index])); + self.emitInstance(self.gl_instances.ptr, cell_index, col, r, layout, win_w, win_h, self.cellKey(self.screen.buf[cell_index])); } } if (self.atlas_dirty) self.uploadAtlasGl(); @@ -852,6 +880,73 @@ fn renderGl(self: *SdlBackend) !void { _ = c.SDL_GL_SwapWindow(self.sdl_window); } +fn fixedCellLayout(self: *const SdlBackend) CellLayout { + return .{ + .w = @floatFromInt(self.cell_w), + .h = @floatFromInt(self.cell_h), + .x_off = 0, + .y_off = 0, + }; +} + +fn cellLayout(self: *const SdlBackend, sw: u32, sh: u32) CellLayout { + const fixed = self.fixedCellLayout(); + if (!is_emscripten) return fixed; + + const cols = self.screen.width; + const rows = self.screen.height; + if (cols == 0 or rows == 0) return fixed; + + const cols_f: f32 = @floatFromInt(cols); + const rows_f: f32 = @floatFromInt(rows); + const grid_w = cols_f * fixed.w; + const grid_h = rows_f * fixed.h; + if (grid_w <= 0 or grid_h <= 0) return fixed; + + const win_w: f32 = @floatFromInt(sw); + const win_h: f32 = @floatFromInt(sh); + const scale = @min(win_w / grid_w, win_h / grid_h); + const scaled_grid_w = grid_w * scale; + const scaled_grid_h = grid_h * scale; + return .{ + .w = fixed.w * scale, + .h = fixed.h * scale, + .x_off = if (win_w > scaled_grid_w) (win_w - scaled_grid_w) * 0.5 else 0, + .y_off = if (win_h > scaled_grid_h) (win_h - scaled_grid_h) * 0.5 else 0, + }; +} + +fn fixedMouseCell(self: *const SdlBackend, x: f32, y: f32) MouseCell { + return .{ + .col = @as(i16, @intCast(@divTrunc(@as(i32, @intFromFloat(x)), @as(i32, @intCast(self.cell_w))))), + .row = @as(i16, @intCast(@divTrunc(@as(i32, @intFromFloat(y)), @as(i32, @intCast(self.cell_h))))), + }; +} + +fn floorToI16(value: f32) i16 { + const floored = @floor(value); + const min: f32 = @floatFromInt(std.math.minInt(i16)); + const max: f32 = @floatFromInt(std.math.maxInt(i16)); + if (floored <= min) return std.math.minInt(i16); + if (floored >= max) return std.math.maxInt(i16); + return @intFromFloat(floored); +} + +fn mouseCell(self: *const SdlBackend, x: f32, y: f32) MouseCell { + if (!is_emscripten) return self.fixedMouseCell(x, y); + + var pw: c_int = 0; + var ph: c_int = 0; + _ = c.SDL_GetWindowSizeInPixels(self.sdl_window, &pw, &ph); + const sw: u32 = @max(1, @as(u32, @intCast(pw))); + const sh: u32 = @max(1, @as(u32, @intCast(ph))); + const layout = self.cellLayout(sw, sh); + return .{ + .col = floorToI16((x - layout.x_off) / layout.w), + .row = floorToI16((y - layout.y_off) / layout.h), + }; +} + // PARDES_TEST_GRID debug mode: dump the retained cell grid as text to stdout // (fd 1). One frame per render, delimited by ---FRAME---/---ENDFRAME--- markers. fn dumpGrid(self: *SdlBackend) !void { @@ -1309,17 +1404,16 @@ fn emitInstance( cell_index: u32, col: u16, row: u16, - cw_f: f32, - ch_f: f32, + layout: CellLayout, win_w: f32, win_h: f32, key: CellKey, ) void { // cell pixel rect (top-left origin) - const px0 = @as(f32, @floatFromInt(col)) * cw_f; - const py0 = @as(f32, @floatFromInt(row)) * ch_f; - const px1 = px0 + cw_f; - const py1 = py0 + ch_f; + const px0 = layout.x_off + @as(f32, @floatFromInt(col)) * layout.w; + const py0 = layout.y_off + @as(f32, @floatFromInt(row)) * layout.h; + const px1 = px0 + layout.w; + const py1 = py0 + layout.h; // -> NDC (SDL GPU clip space: x right, y up; framebuffer row 0 = top) const x0 = (px0 / win_w) * 2.0 - 1.0; const x1 = (px1 / win_w) * 2.0 - 1.0; @@ -1634,6 +1728,10 @@ fn SdlLoop(comptime EventType: type) type { while (c.SDL_PollEvent(&sev)) { if (try self.dispatch(&sev)) |ev| return ev; } + if (is_emscripten) { + self.postWinsize(); + if (try self.queue.tryPop()) |ev| return ev; + } return null; } @@ -1659,7 +1757,25 @@ fn SdlLoop(comptime EventType: type) type { } var pw: c_int = 0; var ph: c_int = 0; - _ = c.SDL_GetWindowSizeInPixels(self.vx.sdl_window, &pw, &ph); + if (is_emscripten) { + var css_w: f64 = 0; + var css_h: f64 = 0; + if (emscripten_get_element_css_size("#canvas", &css_w, &css_h) == EMSCRIPTEN_RESULT_SUCCESS and css_w > 0 and css_h > 0) { + pw = @intFromFloat(@round(css_w * web_render_scale_f64)); + ph = @intFromFloat(@round(css_h * web_render_scale_f64)); + var cur_w: c_int = 0; + var cur_h: c_int = 0; + _ = c.SDL_GetWindowSizeInPixels(self.vx.sdl_window, &cur_w, &cur_h); + if (cur_w != pw or cur_h != ph) { + _ = c.SDL_SetWindowSize(self.vx.sdl_window, pw, ph); + _ = c.SDL_SyncWindow(self.vx.sdl_window); + } + } else { + _ = c.SDL_GetWindowSizeInPixels(self.vx.sdl_window, &pw, &ph); + } + } else { + _ = c.SDL_GetWindowSizeInPixels(self.vx.sdl_window, &pw, &ph); + } const cw: u32 = self.vx.cell_w; const ch: u32 = self.vx.cell_h; const cols: u16 = @intCast(@max(1, @divTrunc(@as(u32, @intCast(pw)), cw))); @@ -1712,11 +1828,10 @@ fn SdlLoop(comptime EventType: type) type { const b = sev.button; const t: vaxis.Mouse.Type = if (b.down) .press else .release; const button = mouseButton(b.button) orelse return null; - const col = @as(i16, @intCast(@divTrunc(@as(i32, @intFromFloat(b.x)), @as(i32, @intCast(self.vx.cell_w))))); - const row = @as(i16, @intCast(@divTrunc(@as(i32, @intFromFloat(b.y)), @as(i32, @intCast(self.vx.cell_h))))); + const cell = self.vx.mouseCell(b.x, b.y); return EventType{ .mouse = .{ - .col = col, - .row = row, + .col = cell.col, + .row = cell.row, .button = button, .mods = sdlMouseMods(self.live_mods), .type = t, @@ -1729,11 +1844,10 @@ fn SdlLoop(comptime EventType: type) type { const held = heldButton(m.state); const t: vaxis.Mouse.Type = if (held != null) .drag else .motion; const button = held orelse .none; - const col = @as(i16, @intCast(@divTrunc(@as(i32, @intFromFloat(m.x)), @as(i32, @intCast(self.vx.cell_w))))); - const row = @as(i16, @intCast(@divTrunc(@as(i32, @intFromFloat(m.y)), @as(i32, @intCast(self.vx.cell_h))))); + const cell = self.vx.mouseCell(m.x, m.y); return EventType{ .mouse = .{ - .col = col, - .row = row, + .col = cell.col, + .row = cell.row, .button = button, .mods = sdlMouseMods(self.live_mods), .type = t, @@ -1743,11 +1857,10 @@ fn SdlLoop(comptime EventType: type) type { const w = sev.wheel; const dy = w.y; const button: vaxis.Mouse.Button = if (dy > 0) .wheel_up else if (dy < 0) .wheel_down else return null; - const col = @as(i16, @intCast(@divTrunc(@as(i32, @intFromFloat(w.mouse_x)), @as(i32, @intCast(self.vx.cell_w))))); - const row = @as(i16, @intCast(@divTrunc(@as(i32, @intFromFloat(w.mouse_y)), @as(i32, @intCast(self.vx.cell_h))))); + const cell = self.vx.mouseCell(w.mouse_x, w.mouse_y); return EventType{ .mouse = .{ - .col = col, - .row = row, + .col = cell.col, + .row = cell.row, .button = button, .mods = sdlMouseMods(self.live_mods), .type = .press, |
