summaryrefslogtreecommitdiff
path: root/ui_sdl.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-03 14:43:30 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:07 -0300
commitd1ed5bd8f55935e6470c4a7559216119571a084f (patch)
tree434478ce8a5dcd9aed367334816f54da9953e56d /ui_sdl.zig
parent153f0a88862507cf1f5dda71f71db3d6649dfeaa (diff)
downloadpardes-d1ed5bd8f55935e6470c4a7559216119571a084f.tar.gz
pardes-d1ed5bd8f55935e6470c4a7559216119571a084f.zip
fixed scaling and fitting on web ui
Diffstat (limited to 'ui_sdl.zig')
-rw-r--r--ui_sdl.zig185
1 files changed, 149 insertions, 36 deletions
diff --git a/ui_sdl.zig b/ui_sdl.zig
index 8bc68db5..8fbc78f5 100644
--- a/ui_sdl.zig
+++ b/ui_sdl.zig
@@ -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,