From 98418fd7ddb525b75b376e9e7d970be2a9f62ff5 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Fri, 31 Jul 2026 19:34:08 -0300 Subject: ctrl+ and ctrl- resize the gui's font --- src/gui/gui.zig | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 105 insertions(+), 2 deletions(-) diff --git a/src/gui/gui.zig b/src/gui/gui.zig index 66812575..a26fdd1c 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -119,6 +119,49 @@ const max_overlay_vertices: usize = touch_click_flash_vertices + max_touch_point const scroll_ease: f32 = 0.35; const scroll_ease_min: f32 = 0.05; +// Ctrl+ / Ctrl-: how far one press moves g.px, and the two sizes it stops at. +// ONE size for the whole window, not one per pane: the core lays every pane +// out on a single uniform cell grid and Surface is one flat cols×rows array, +// so a second cell size would be a different core, not a different font. +// +// The step is 2 and not 1 because 1 is a press that sometimes does nothing — +// cell_w is round(advance × scale), the shipped face advances ~0.51px per px +// of size, and half the 1px steps therefore round to the same column width. A +// key that visibly works only every other press reads as a broken key. At 2 +// both axes move at every size in the range, on both parities of the ladder; +// that is what the test below walks. +// +// The ends are where a terminal stops being one. 8px is a 4×8 cell — the +// smallest thing with a glyph still in it — and 72px is 37×76, about thirty +// columns across a laptop screen. Past either the grid is not small or large, +// it is wedged, and there is no reset binding to get back out of it. +const font_px_step: f32 = 2.0; +const font_px_min: f32 = 8.0; +const font_px_max: f32 = 72.0; + +test "every font size step moves the cell, and the ends are reachable exactly" { + const font = c.ui_font_new(font_ttf.ptr, @intCast(font_ttf.len)).?; + defer c.ui_font_free(font); + // walk the WHOLE range rather than one ladder: dispatch starts from 27 + // native and 18 web, so both the odd and the even rungs get pressed, and + // a stall on either is a dead keystroke for somebody. + var px = font_px_min + font_px_step; + while (px <= font_px_max) : (px += 1.0) { + var cw: c_int = 0; + var ch: c_int = 0; + var asc: c_int = 0; + var pw: c_int = 0; + var ph: c_int = 0; + c.ui_font_cell_metrics(font, c.ui_font_scale_for_height(font, px), &cw, &ch, &asc); + c.ui_font_cell_metrics(font, c.ui_font_scale_for_height(font, px - font_px_step), &pw, &ph, &asc); + try std.testing.expect(cw > pw and ch > ph); + } + // ...and the clamp dispatch runs parks on each end instead of walking off + // it, from any size a press can leave g.px on + try std.testing.expectEqual(font_px_max, std.math.clamp(font_px_max + font_px_step, font_px_min, font_px_max)); + try std.testing.expectEqual(font_px_min, std.math.clamp(font_px_min - font_px_step, font_px_min, font_px_max)); +} + // One instance per cell; the vertex shader expands it to a 2-triangle quad // with gl_VertexIndex. Coords are NDC (y up), uv into the atlas, colors 0..1. const CellInstance = extern struct { @@ -1871,6 +1914,52 @@ fn dispatch(g: *Gui, core: *pardes.Pardes, sev: *const c.SDL_Event) void { c.SDL_EVENT_KEY_DOWN => { g.live_ctrl = (sev.key.mod & c.SDL_KMOD_CTRL) != 0; g.live_alt = (sev.key.mod & c.SDL_KMOD_ALT) != 0; + // Ctrl+ / Ctrl-: the font size. Here rather than in keyDown + // because it is the shell's business and not the core's — the + // core has no font — and because this is the only side of the + // wall where `g` is in scope anyway. + // + // SIX keycodes for two keys, and every one of them is a key + // somebody actually presses: + // - `+` on a US layout IS Shift-`=`, and SDL reports the + // UNSHIFTED keycode, so Ctrl-+ arrives as SDLK_EQUALS. Binding + // only SDLK_PLUS is the usual way to ship this dead. + // - SDLK_PLUS is nonetheless real: on the German/Nordic layouts + // `+` is its own unshifted key. Same for `_` under `-`. + // - the numpad is separate. SDL_HINT_KEYCODE_OPTIONS defaults to + // "french_numbers,latin_letters" — no "hide_numpad" — so KP_+ + // stays SDLK_KP_PLUS (0x40000057) forever and never reaches + // keyDown's `sym < 128` line at all. + // + // Nothing is taken away from anyone by claiming these. forwardKey + // encodes Ctrl only for a-z, A-Z, `@` and `[`..`_`, and both `=` + // (0x3d) and `-` (0x2d) fall below that last range, so a pane in + // tty mode already sent the pty NO bytes for either — including + // Ctrl-Shift-minus, which arrives here as SDLK_MINUS and reached + // the core as `-`, never as the `_` that would have been 0x1f. No + // chord in config.zig pairs ctrl with any of these codepoints + // either (`=` is Format and `_` is trim_sels, both unmodified; + // Alt-- and Alt-_ are the selection merges). And the numpad pair + // did nothing at all: keyDown drops every sym above 128. + // + // Returning here is the whole interception, with no TEXT_INPUT + // twin to also swallow: SDL only sends text when neither ctrl nor + // alt is down (SDL_x11events.c, `!(SDL_GetModState() & (CTRL|ALT))`). + const step: f32 = if (!g.live_ctrl) 0 else switch (sev.key.key) { + c.SDLK_EQUALS, c.SDLK_PLUS, c.SDLK_KP_PLUS => font_px_step, + c.SDLK_MINUS, c.SDLK_UNDERSCORE, c.SDLK_KP_MINUS => -font_px_step, + else => 0, + }; + if (step != 0) { + const want = std.math.clamp(g.px + step, font_px_min, font_px_max); + // at either end the key is inert rather than a re-raster of + // the size already on screen + if (want != g.px) { + g.px = want; + refitFont(g, core); + } + return; + } keyDown(core, sev.key.key, sev.key.mod); }, c.SDL_EVENT_KEY_UP => { @@ -2833,8 +2922,8 @@ fn firstCp(s: []const u8) u32 { /// 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. +/// Ctrl+/Ctrl- in dispatch) 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 @@ -2885,6 +2974,20 @@ fn refitFont(g: *Gui, core: *pardes.Pardes) void { 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 } }); + + // ...and a smooth scroll in flight is measured in the OLD grid: scroll_rect + // is a rect of the pane the last frame drew, and scroll_edge is a saved row + // of exactly that rect's body WIDTH. The resize above moves both under it, + // and emitScrollRows only checks that the old rect still FITS inside the new + // surface — which it does whenever the font got smaller — so the next frame + // would paint last frame's strip over cells that are no longer the same + // text. Stop the slide instead; the wheel refills all of it on the next + // tick. ponytail: the rows still owed the core (scroll_pending) are dropped + // rather than flushed, because a font resize mid-flick is not a moment + // anyone is counting rows in; flush them the day someone notices. + g.scroll_pane = null; + g.scroll_pending = 0; + g.scroll_lag = 0; } fn ensureGlyph(g: *Gui, cp: u32) Slot { -- cgit v1.3