summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-31 19:34:08 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:08 -0300
commit98418fd7ddb525b75b376e9e7d970be2a9f62ff5 (patch)
tree17125238ec7ad5088848b1a1f17dbc0bbfb203f5
parent005178177a1ddb2eb2f1af5fad36e2717758ee8e (diff)
downloadpardes-98418fd7ddb525b75b376e9e7d970be2a9f62ff5.tar.gz
pardes-98418fd7ddb525b75b376e9e7d970be2a9f62ff5.zip
ctrl+ and ctrl- resize the gui's font
-rw-r--r--src/gui/gui.zig107
1 files 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 {