diff options
Diffstat (limited to 'src/gui/gui.zig')
| -rw-r--r-- | src/gui/gui.zig | 99 |
1 files changed, 96 insertions, 3 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig index 6280ab00..66812575 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -22,6 +22,7 @@ const config = @import("../config.zig"); const look = @import("../look.zig"); const deck = @import("deck.zig"); const crt = @import("crt.zig"); +const fonts = @import("fonts.zig"); // the Font builtin's half of the seam const is_emscripten = builtin.os.tag == .emscripten; @@ -780,6 +781,15 @@ const Gui = struct { overlay_vertices: []OverlayVertex, font: *c.UIFont, + /// the file behind `font`, when it is one the Font builtin loaded. Empty + /// for the font the binary ships with, which is @embedFile'd and not ours + /// to free — stb keeps a pointer into these bytes, so they outlive nothing. + font_bytes: []u8 = &.{}, + /// the cell height the metrics are asked for, in pixels. A field and not + /// the local constant it used to be because refitFont reads it: changing + /// the FACE has to re-ask at the same size, and changing the SIZE (the + /// Ctrl+/Ctrl- this leaves the path for) is writing here and calling that. + px: f32, scale: f32, cell_w: u32, cell_h: u32, @@ -1027,6 +1037,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void { .overlay_vxfer = overlay_vxfer, .overlay_vertices = overlay_vertices, .font = font, + .px = px, .scale = scale, .cell_w = cell_w, .cell_h = cell_h, @@ -1037,6 +1048,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void { .capture_dir = capture_dir orelse "", }; defer g.glyphs.deinit(); + defer if (g.font_bytes.len != 0) gpa.free(g.font_bytes); // set by Font, if it ran defer gpa.free(g.scroll_edge); // grown on demand by stepScroll // slot (0,0) is the space glyph (blank cells sample alpha=0 → bg only) _ = c.ui_font_raster(font, scale, ' ', atlas_stage.ptr, @intCast(atlas_w), @intCast(cell_w), @intCast(cell_h), asc); @@ -1179,12 +1191,35 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void { core.deinit(); core = nc; } + // Font builtin: the core resolved a name to a path and asked for it — + // it cannot load a font itself, having no rasterizer, no atlas and no + // window. Inline here beside Restore because it is the same kind of + // thing and this is the flat loop. + if (fonts.want) |path| blk: { + fonts.want = null; + const bytes = look.readFile(gpa, path) catch break :blk; + const nf = c.ui_font_new(bytes.ptr, @intCast(bytes.len)) orelse { + // stb turned it down (an .otf whose outlines it cannot read). + // Keep wearing the one that works: a font pardes cannot + // rasterize is a blank window with no way back out of it. + log.err("ui_font_new failed: {s}", .{path}); + gpa.free(bytes); + break :blk; + }; + c.ui_font_free(g.font); + if (g.font_bytes.len != 0) gpa.free(g.font_bytes); + g.font = nf; + g.font_bytes = bytes; // stb reads them for as long as the font lives + refitFont(&g, core); + } // 5. live cwd for tags/look: cheap /proc readlink per pane, per frame pollCwds(core, &ptys); - // 6. the grid follows the window (covers WINDOW_RESIZED and test resizes) + // 6. the grid follows the window (covers WINDOW_RESIZED and test + // resizes). Off g.cell_w/h, not the init locals: a font change moves + // them, and this is the line that would go on dividing by the old cell. _ = c.SDL_GetWindowSizeInPixels(window, &pw, &ph); - const cols: u16 = @intCast(@max(1, @divTrunc(@as(u32, @intCast(@max(pw, 1))), cell_w))); - const rows: u16 = @intCast(@max(1, @divTrunc(@as(u32, @intCast(@max(ph, 1))), cell_h))); + const cols: u16 = @intCast(@max(1, @divTrunc(@as(u32, @intCast(@max(pw, 1))), g.cell_w))); + const rows: u16 = @intCast(@max(1, @divTrunc(@as(u32, @intCast(@max(ph, 1))), g.cell_h))); if (cols != core.screen_w or rows != core.screen_h) core.update(.{ .resize = .{ .cols = cols, .rows = rows } }); // 7. render — the scroll animation steps LAST before it, while @@ -1417,6 +1452,7 @@ fn runWeb(opts_in: pardes.Options) !void { .gl_overlay_vbo = overlay_vbo, .gl_u_atlas = c.glGetUniformLocation(program, "u_atlas"), .font = font, + .px = px, .scale = scale, .cell_w = cell_w, .cell_h = cell_h, @@ -2794,6 +2830,63 @@ fn firstCp(s: []const u8) u32 { return std.unicode.utf8Decode(s[0..n]) catch ' '; } +/// Re-measure the cell, throw the glyph atlas away, and re-fit the grid to the +/// window. THE path for any change to what a cell LOOKS like: point g.font at +/// a different face (the Font builtin, above) or write a different g.px (the +/// Ctrl+/Ctrl- sizing this leaves the road open for) and call this — those are +/// one line each, and everything that has to follow from them is here. +/// +/// The atlas is the part that must not be skipped, and the reason the whole +/// thing is a function rather than three lines at a call site. It is keyed by +/// CODEPOINT ALONE — one face, one size, a pen walking rows of cell_w×cell_h +/// slots — so after a change every slot in it holds the wrong picture at the +/// wrong metrics, and every codepoint already in the map would keep being +/// drawn from that slot forever, because ensureGlyph's first line is a cache +/// hit. Clearing the map, zeroing the staging bitmap and rewinding the pen put +/// it back to exactly what init built, and ensureGlyph refills it as the next +/// frame draws. The zeroing is not tidiness: the upload is the WHOLE texture, +/// the new cell size is a different grid over the same 2048², and a leftover +/// bitmap no slot points at any more would still be sampled by whatever new +/// slot overlaps it. +fn refitFont(g: *Gui, core: *pardes.Pardes) void { + // Native rasters at the size it measures. Web MEASURES at the CSS size and + // RASTERS web_render_scale bigger, so text stays sharp on a scaled canvas + // and cellLayout scales the quads back down — the same two scales initGl + // takes, asked again because a different face answers differently. + const measure = c.ui_font_scale_for_height(g.font, g.px); + g.scale = if (is_emscripten) c.ui_font_scale_for_height(g.font, g.px * web_render_scale) else measure; + var cw: c_int = 10; + var chh: c_int = 20; + var asc: c_int = 16; + c.ui_font_cell_metrics(g.font, measure, &cw, &chh, &asc); + const grow: f32 = if (is_emscripten) web_render_scale else 1.0; + g.cell_w = @intFromFloat(@ceil(@as(f32, @floatFromInt(@max(cw, 1))) * grow)); + g.cell_h = @intFromFloat(@ceil(@as(f32, @floatFromInt(@max(chh, 1))) * grow)); + g.ascent = @intFromFloat(@ceil(@as(f32, @floatFromInt(@max(asc, 1))) * grow)); + + g.glyphs.clearRetainingCapacity(); + @memset(g.atlas_stage, 0); + // slot (0,0) is the space glyph, exactly as init lays it out + _ = c.ui_font_raster(g.font, g.scale, ' ', g.atlas_stage.ptr, @intCast(atlas_w), @intCast(g.cell_w), @intCast(g.cell_h), g.ascent); + g.space_slot = .{ .u = 0, .v = 0 }; + g.pen_x = g.cell_w; + g.pen_y = 0; + g.atlas_dirty = true; + + // ...and the grid: the same window is a different number of cells now. The + // shells re-derive this every frame anyway, so this is only the frame the + // change happens on — but it is the frame the surface is about to be + // rendered for, and a stale screen_w here is a row of cells drawn off the + // right edge of the window. + var pw: c_int = 0; + var ph: c_int = 0; + _ = c.SDL_GetWindowSizeInPixels(g.window, &pw, &ph); + const cols: u16 = @intCast(@max(1, @divTrunc(@as(u32, @intCast(@max(pw, 1))), g.cell_w))); + const rows: u16 = @intCast(@max(1, @divTrunc(@as(u32, @intCast(@max(ph, 1))), g.cell_h))); + if (cols != core.screen_w or rows != core.screen_h) + core.update(.{ .resize = .{ .cols = cols, .rows = rows } }); +} + fn ensureGlyph(g: *Gui, cp: u32) Slot { if (g.glyphs.get(cp)) |s| return s; if (g.pen_x + g.cell_w > atlas_w) { |
