diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 11:53:55 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:16 -0300 |
| commit | c30835e62d3b7dd03ee8b28fbcff97d3b88bcd95 (patch) | |
| tree | 7c669b04867f171b1f7b895201277bcb7b3ec386 /src | |
| parent | 7a818260ff0c7fc9e2effa634cdf947fde48e043 (diff) | |
| parent | 7830d11aef6b3ebee5c66835670f7179176ffd3a (diff) | |
| download | pardes-c30835e62d3b7dd03ee8b28fbcff97d3b88bcd95.tar.gz pardes-c30835e62d3b7dd03ee8b28fbcff97d3b88bcd95.zip | |
Merge: 9P round 21 + Afterglow removed + symmetric focus dim
Diffstat (limited to 'src')
| -rw-r--r-- | src/config.zig | 10 | ||||
| -rw-r--r-- | src/draw.zig | 85 | ||||
| -rw-r--r-- | src/gui/gui.zig | 152 | ||||
| -rw-r--r-- | src/surface.zig | 3 |
4 files changed, 84 insertions, 166 deletions
diff --git a/src/config.zig b/src/config.zig index a703f79e..ea7a877a 100644 --- a/src/config.zig +++ b/src/config.zig @@ -127,7 +127,6 @@ pub const leader_path = paths: { if (builtins.capabilities.lift) table.set(.HoverGlow, null); if (builtins.capabilities.lift) table.set(.Occlusion, null); if (builtins.capabilities.lift) table.set(.Parallax, null); - if (builtins.capabilities.lift) table.set(.Afterglow, null); table.set(.JumpTrail, null); table.set(.ChipShadow, null); table.set(.ThumbFlash, null); @@ -691,7 +690,6 @@ pub const Runtime = struct { parallax: bool = false, /// G14: text that changes glows for a moment in the theme's accent and /// fades, a pixel shell's. - afterglow: bool = false, /// T2: in a terminal, a jump of the cursor leaves a short trail that /// fades in 120 ms (a truecolor terminal's; a pixel shell glides). jump_trail: bool = false, @@ -924,7 +922,7 @@ pub const Runtime = struct { /// The message timings a setting word sets, each in milliseconds. pub const MessageTime = enum { linger, fall, dissolve }; - pub const Toggle = enum { colors, wrap, tree_context_tag_style, tag_bottom, debug, focus_tint, syntax_bold, verbose, message_animation, ligatures, cursor_blink, selection_glow, hover_glow, occlusion, parallax, afterglow, jump_trail, chip_shadow, thumb_flash }; + pub const Toggle = enum { colors, wrap, tree_context_tag_style, tag_bottom, debug, focus_tint, syntax_bold, verbose, message_animation, ligatures, cursor_blink, selection_glow, hover_glow, occlusion, parallax, jump_trail, chip_shadow, thumb_flash }; /// A bundled post pass, a Shadertoy file in shaders/post/: the CRT, and /// G4's bloom, vignette and grain (docs/effects.md). pub const Scene = enum { crt, bloom, vignette, grain }; @@ -987,7 +985,6 @@ pub const Runtime = struct { .{ .word = "HoverGlow", .action = .{ .toggle = .hover_glow }, .availability = .lift, .doc = "A soft underline of light under the word a look-hover would open, on or off." }, .{ .word = "Occlusion", .action = .{ .toggle = .occlusion }, .availability = .lift, .doc = "Pane bodies darken faintly toward their edges, on or off." }, .{ .word = "Parallax", .action = .{ .toggle = .parallax }, .availability = .lift, .doc = "A theme's page pattern moves with the text at a quarter of its speed, on or off." }, - .{ .word = "Afterglow", .action = .{ .toggle = .afterglow }, .availability = .lift, .doc = "Text that changes glows for a moment and fades, on or off." }, .{ .word = "JumpTrail", .action = .{ .toggle = .jump_trail }, .doc = "In a terminal, a jump of the cursor leaves a short trail that fades, on or off." }, .{ .word = "ChipShadow", .action = .{ .toggle = .chip_shadow }, .doc = "In a terminal, a notice chip casts a cell's shadow right and down, on or off." }, .{ .word = "ThumbFlash", .action = .{ .toggle = .thumb_flash }, .doc = "In a terminal, a pane's scroll thumb brightens as it scrolls and fades back, on or off." }, @@ -1067,9 +1064,9 @@ pub const Runtime = struct { .toggle => |field| if (field == .ligatures) { if (setting.availability != .ligatures) @compileError("the ligatures toggle must use the ligatures capability"); - } else if (field == .cursor_blink or field == .selection_glow or field == .hover_glow or field == .occlusion or field == .parallax or field == .afterglow) { + } else if (field == .cursor_blink or field == .selection_glow or field == .hover_glow or field == .occlusion or field == .parallax) { if (setting.availability != .lift) - @compileError("CursorBlink, SelectionGlow, HoverGlow, Occlusion, Parallax and Afterglow must use the lift capability (a pixel shell's)"); + @compileError("CursorBlink, SelectionGlow, HoverGlow, Occlusion and Parallax must use the lift capability (a pixel shell's)"); } else if (setting.availability != null) @compileError("unconditional toggles cannot carry a backend capability"), else => if (setting.availability != null) @@ -1728,7 +1725,6 @@ pub const Runtime = struct { .hover_glow => "HoverGlow on\n", .occlusion => "Occlusion on\n", .parallax => "Parallax on\n", - .afterglow => "Afterglow on\n", .jump_trail => "JumpTrail on\n", .chip_shadow => "ChipShadow on\n", .thumb_flash => "ThumbFlash on\n", diff --git a/src/draw.zig b/src/draw.zig index 15411770..9c9e11dd 100644 --- a/src/draw.zig +++ b/src/draw.zig @@ -382,8 +382,10 @@ fn contrast(a: [3]u8, b: [3]u8) f32 { /// Pane `id`'s text, `amount` (0 to 1) of the way to its ground, on the grid /// and on every layer of it: its tag, its body, its notices. Its grip keeps -/// its colour, which already says it is not the focused one. -fn recede(p: *Pardes, s: *Surface, id: usize, amount: f32) void { +/// its colour, which already says it is not the focused one. The focused +/// pane, still brightening, is faded in its body's text alone: its tag and +/// a selection are focus indicators, which no effect may alter. +fn recede(p: *Pardes, s: *Surface, id: usize, amount: f32, focused: bool) void { const th = p.theme(); const chrome = p.chromeTheme(); const r = p.rects[id]; @@ -397,11 +399,15 @@ fn recede(p: *Pardes, s: *Surface, id: usize, amount: f32) void { const cell = s.at(col, row); if (cell.default) continue; const tagline_role = cell.style.font_role == .tagline; + if (focused and (tagline_role or selected(cell, th.sel_bg))) continue; fadeStyle(&cell.style, if (tagline_role) chrome.tag_fg else th.fg, if (tagline_role) chrome.tag_bg else th.bg, palette, amount, &memo); } } const body = &s.body_layers[id]; - if (body.rows != 0) for (body.cells[0..@as(usize, body.cols) * body.rows]) |*cell| fadeStyle(&cell.style, th.fg, th.bg, palette, amount, &memo); + if (body.rows != 0) for (body.cells[0..@as(usize, body.cols) * body.rows]) |*cell| { + if (!(focused and selected(cell, th.sel_bg))) fadeStyle(&cell.style, th.fg, th.bg, palette, amount, &memo); + }; + if (focused) return; const tag = &s.tag_layers[id]; if (tag.rows != 0) for (tag.cells[0..@as(usize, tag.cols) * tag.rows]) |*cell| fadeStyle(&cell.style, chrome.tag_fg, tag.bg, palette, amount, &memo); for (0..Pane.Notices.max) |i| { @@ -410,6 +416,13 @@ fn recede(p: *Pardes, s: *Surface, id: usize, amount: f32) void { } } +fn selected(cell: *const Cell, sel_bg: [3]u8) bool { + return switch (cell.style.bg) { + .rgb => |rgb| std.mem.eql(u8, &rgb, &sel_bg), + else => false, + }; +} + /// The few ink and paper pairs a pane has, each faded once a frame. const Memo = struct { keys: [32]?[6]u8 = @splat(null), @@ -552,7 +565,7 @@ pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface { .rail_px = @intCast(@min(255, @as(u32, p.theme().rail_px orelse 12) * p.settings.grip_width / 100)), .name_ink = chrome.tag_name_fg, .active_name_ink = if (p.settings.focus_tint) chrome.tag_active_name_fg else chrome.tag_name_fg, - .fx = @as(u8, @intFromBool(p.settings.selection_glow)) | @as(u8, @intFromBool(p.settings.hover_glow)) << 1 | @as(u8, @intFromBool(p.settings.occlusion)) << 2 | @as(u8, @intFromBool(p.settings.parallax)) << 3 | @as(u8, @intFromBool(p.settings.afterglow)) << 4, + .fx = @as(u8, @intFromBool(p.settings.selection_glow)) | @as(u8, @intFromBool(p.settings.hover_glow)) << 1 | @as(u8, @intFromBool(p.settings.occlusion)) << 2 | @as(u8, @intFromBool(p.settings.parallax)) << 3, }; if (p.theme().decor) |decor| { s.chrome.decor_dots = decor.page_dots; @@ -588,11 +601,14 @@ pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface { if (region.kind == .notice) try joinNotice(p, s, arena, region); } + // Both ways on the same spring: the pane focus leaves dims as the one + // it reaches brightens, so a focus change never pops (Motion off snaps + // both). for (p.panes, p.lifts, 0..) |slot, spring, id| { - if (slot == null or id == p.active) continue; + if (slot == null) continue; const focus = spring.value(p.now_ns) / animation.Motion.of(p.settings.motion).gain; const amount = @as(f32, @floatFromInt(inactiveDim(p))) / 100 * std.math.clamp(1 - focus, 0, 1); - if (amount > 0.001) recede(p, s, id, amount); + if (amount > 0.001) recede(p, s, id, amount, id == p.active); } try paintHeaders(p, s, arena); @@ -2248,3 +2264,60 @@ test "focus moved to another column glides from where the cursor was drawn" { try std.testing.expectApproxEqAbs(box.x, landed.chrome.cursor_quad[0], 0.01); try std.testing.expect(!p.cursor_glide.moving() and !p.cursor_crossing); } + +test "a pane focus reaches brightens on the same spring the one it leaves dims on, and Motion off snaps" { + if (!builtins.capabilities.lift) return error.SkipZigTest; + const p = try Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 24 }); + defer p.deinit(); + _ = try p.setTestFile("some text on a line\n" ** 4); + p.newScratchBelow(0); + p.settings.inactive_dim = 40; + p.settings.motion = .smooth; + var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); + defer arena.deinit(); + const ink = struct { + // The file's first glyph's ink, in whichever form its body has. + fn of(s: *const Surface) [3]u8 { + const body = &s.body_layers[0]; + const cells = if (body.rows != 0) body.cells[0..@as(usize, body.cols) * body.rows] else s.cells; + for (cells) |cell| if (!cell.default and cell.len == 1 and cell.text[0] == 's' and cell.style.font_role == .body) return switch (cell.style.fg) { + .rgb => |rgb| rgb, + else => .{ 0, 0, 0 }, + }; + return .{ 0, 0, 0 }; + } + }.of; + p.active = 1; + p.advance(std.time.ns_per_s); + _ = try p.render(arena.allocator()); + p.advance(3 * std.time.ns_per_s); + _ = arena.reset(.retain_capacity); + const dimmed = ink(try p.render(arena.allocator())); + p.active = 0; + var now: u64 = 3 * std.time.ns_per_s; + var last = dimmed[0]; + var steps: usize = 0; + for (0..30) |_| { + now += 16 * std.time.ns_per_ms; + p.advance(now); + _ = arena.reset(.retain_capacity); + const at = ink(try p.render(arena.allocator()))[0]; + // Brighter frame by frame (a flavour may pass it a little), never + // the whole way (46 levels here) at once. + try std.testing.expect(@abs(@as(i32, at) - @as(i32, last)) <= 12); + if (at != last) steps += 1; + last = at; + } + try std.testing.expect(steps >= 4); + p.advance(now + 2 * std.time.ns_per_s); + _ = arena.reset(.retain_capacity); + const bright = ink(try p.render(arena.allocator())); + try std.testing.expect(bright[0] > dimmed[0]); + + // Motion off: both snap. + p.settings.motion = .off; + p.active = 1; + p.advance(now + 3 * std.time.ns_per_s); + _ = arena.reset(.retain_capacity); + try std.testing.expectEqualSlices(u8, &dimmed, &ink(try p.render(arena.allocator()))); +} diff --git a/src/gui/gui.zig b/src/gui/gui.zig index 851218bb..d935b5fd 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -1876,7 +1876,6 @@ const Gui = struct { sel_glow: SelGlow = .{}, hover_glow: SelGlow = .{}, /// G14: the cells last drawn and when each last changed. - afterglow: Afterglow = .{}, /// The cursor's blink (blinkNow): when the frame drawn last arrived, /// a redraw owed for an edge, and this frame's cursor mode and alpha. @@ -2586,7 +2585,6 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u defer c.ui_font_free(g.font); defer gpa.free(g.scroll_edge); // grown on demand by stepScroll defer g.glide.deinit(gpa); - defer g.afterglow.deinit(gpa); resetGlyphAtlas(&g); if (attach) |requested| return attachRequested(gpa, &g, requested); @@ -5035,7 +5033,7 @@ fn stepGlide(g: *Gui, core: *pardes.Pardes, gpa: std.mem.Allocator) void { const glide = &g.glide; // The glide runs on the core's continuous path: frames every loop, and // a virtual clock carried on. - defer core.shell_continuous = glide.moving() or g.sel_glow.fading or g.hover_glow.fading or g.afterglow.glowing; + defer core.shell_continuous = glide.moving() or g.sel_glow.fading or g.hover_glow.fading; glide.now_ns = core.now_ns; glide.motion = pardes.animation.Motion.of(core.settings.motion); if (!glide.spring.settled) { @@ -5862,7 +5860,6 @@ fn renderFrame( g.sel_glow.now_ns = if (core) |p| p.now_ns else c.SDL_GetTicksNS(); // Off, or while panes move, no glow and nothing fading. if (surface.chrome.fx & 1 == 0 or surface.panelTracks().len != 0) g.sel_glow = .{ .now_ns = g.sel_glow.now_ns }; - if (surface.chrome.fx & 16 == 0) g.afterglow.glowing = false; g.hover_glow.now_ns = g.sel_glow.now_ns; if (surface.chrome.fx & 2 == 0 or surface.panelTracks().len != 0) g.hover_glow = .{ .now_ns = g.sel_glow.now_ns }; if (g.glide.moving()) g.glide.offset = g.glide.spring.value(g.glide.now_ns); @@ -8833,7 +8830,6 @@ fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, g // G11: a glow under the word a look-hover would open. if (index == 0 and groups.tracks == 0 and chrome.fx & 2 != 0) try glowBlocks(g, gpa, surface, .hover, layout, win_w, win_h); // G14: what changed glows, while nothing moves. - if (index == 0 and groups.tracks == 0 and chrome.fx & 16 != 0) try afterglowDecor(g, gpa, surface, layout, win_w, win_h); // G12: the bodies' edges shaded, a pane's in its own group. if (chrome.fx & 4 != 0) try occlusion(g, gpa, placed, groups, index, surface, ceilings, layout, win_w, win_h, track); // A single spine runs down each column, through every tag and body. @@ -9266,91 +9262,6 @@ fn occlusion(g: *Gui, gpa: std.mem.Allocator, regions: []const pardes.Region, gr } } -/// G14's state: the grid last drawn, when each cell last changed, and each -/// body's line then (a scroll changes every cell and is not a change). -const Afterglow = struct { - cells: []pardes.Cell = &.{}, - changed: []u64 = &.{}, - cols: u16 = 0, - rows: u16 = 0, - lines: [pardes.MAX_PANES]i32 = @splat(std.math.minInt(i32)), - /// A cell still glows: the core keeps frames coming. - glowing: bool = false, - - fn deinit(glow: *Afterglow, gpa: std.mem.Allocator) void { - gpa.free(glow.cells); - gpa.free(glow.changed); - } -}; - -const afterglow_ns: u64 = 250 * std.time.ns_per_ms; - -/// G14: a cell of a pane body whose text changed since the last frame -/// glows in the theme's accent at 18% and fades out over 250 ms with an -/// ease-out, as a run of cells a row. Never on a tag or a grip (bodies -/// only), the cursor or a selection; a body whose view moved (a scroll) -/// starts over rather than lighting up whole. -fn afterglowDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, layout: CellLayout, win_w: f32, win_h: f32) !void { - const glow = &g.afterglow; - const now = g.sel_glow.now_ns; - const n = @as(usize, surface.cols) * surface.rows; - const fresh = glow.cols != surface.cols or glow.rows != surface.rows or glow.cells.len != n; - if (fresh) { - glow.cells = try gpa.realloc(glow.cells, n); - glow.changed = try gpa.realloc(glow.changed, n); - @memcpy(glow.cells, surface.cells); - @memset(glow.changed, 0); - glow.cols = surface.cols; - glow.rows = surface.rows; - } - const chrome = &surface.chrome; - const page = chrome.page orelse bg_default; - const fg = chrome.fg orelse fg_default; - const cap = @min(0.18, lightenCeiling(fg, page, chrome.accent)); - var glowing = false; - for (surface.regionList()) |body| { - if (body.kind != .body or body.owner >= glow.lines.len) continue; - const scrolled = glow.lines[body.owner] != body.line; - glow.lines[body.owner] = body.line; - var row = body.rect.y; - while (row < body.rect.y + body.rect.h and row < surface.rows) : (row += 1) { - var run_start: ?u16 = null; - var run_age: u64 = 0; - var col = body.rect.x; - while (col <= body.rect.x + body.rect.w and col <= surface.cols) : (col += 1) { - var age: ?u64 = null; - if (col < body.rect.x + body.rect.w and col < surface.cols) { - const at = @as(usize, row) * surface.cols + col; - const cell = surface.cells[at]; - const differs = !std.meta.eql(cell.style, glow.cells[at].style) or !std.mem.eql(u8, cell.grapheme(), glow.cells[at].grapheme()) or cell.default != glow.cells[at].default; - if (differs and !fresh) glow.changed[at] = if (scrolled) 0 else now; - const selected = !cell.default and switch (cell.style.bg) { - .rgb => |rgb| std.mem.eql(u8, &rgb, &chrome.sel_bg), - else => false, - }; - const cursor_here = if (surface.cursor) |cu| cu.x == col and cu.y == row else false; - const since = now -| glow.changed[at]; - if (glow.changed[at] != 0 and since < afterglow_ns and !selected and !cursor_here) age = since; - } - // A run is cells that changed together. - if (run_start) |start| if (age == null or age.? != run_age) { - const t = @as(f32, @floatFromInt(run_age)) / @as(f32, @floatFromInt(afterglow_ns)); - const fade = (1 - t) * (1 - t) * (1 - t); - try addDecor(g, gpa, @as(f32, @floatFromInt(start)) * layout.w, @as(f32, @floatFromInt(row)) * layout.h, @as(f32, @floatFromInt(col)) * layout.w, @as(f32, @floatFromInt(row + 1)) * layout.h, chrome.accent, cap * fade, null, win_w, win_h); - glowing = true; - run_start = null; - }; - if (age) |a| if (run_start == null) { - run_start = col; - run_age = a; - }; - } - } - } - @memcpy(glow.cells, surface.cells); - glow.glowing = glowing; -} - /// A theme's tag plaques (Chrome.decor_box_*), drawn under the cells of /// group `index`: each pane tag, and with the still panes the column and /// workspace tags, becomes a framed box inside its own band with a hard @@ -10001,67 +9912,6 @@ test "a body tells its first line, and lapis's dots under it move a quarter as f try std.testing.expectEqual(@as(f32, 0), decor.parallaxAt(900, 10)); } -test "changed text glows and fades in 250 ms; the cursor and a scroll do not light up" { - const gpa = std.testing.allocator; - const core = try pardes.Pardes.init(gpa, .{ .cols = 80, .rows = 24 }); - defer core.deinit(); - const pane = try core.setTestFile("line\n" ** 60); - core.settings.afterglow = true; - pardes.test_api.sync(core); - const s = try core.render(core.scratch.allocator()); - const body = for (s.regionList()) |region| { - if (region.kind == .body) break region; - } else return error.NoBody; - s.cursor = .{ .x = body.rect.x + 12, .y = body.rect.y + 2 }; - var g: Gui = undefined; - g.decor = .empty; - defer g.decor.deinit(gpa); - g.cell_w = 10; - g.cell_h = 20; - g.afterglow = .{}; - defer g.afterglow.deinit(gpa); - const layout = fixedCellLayout(&g); - const Count = struct { - fn glows(items: []const CellInstance, accent: [3]u8) usize { - var n: usize = 0; - for (items) |item| { - const rgb: [3]u8 = .{ @intFromFloat(@round(item.br * 255)), @intFromFloat(@round(item.bg * 255)), @intFromFloat(@round(item.bb * 255)) }; - if (std.mem.eql(u8, &rgb, &accent) and item.fr > 0.001) n += 1; - } - return n; - } - }; - g.sel_glow = .{ .now_ns = std.time.ns_per_s }; - try afterglowDecor(&g, gpa, s, layout, 800, 480); - try std.testing.expectEqual(@as(usize, 0), g.decor.items.len); - // Three cells of a row change, the cursor's among them. - for (10..13) |x| { - const cell = &s.cells[@as(usize, body.rect.y + 2) * s.cols + body.rect.x + x]; - cell.* = .{ .default = false, .style = .{ .fg = .{ .rgb = .{ 1, 2, 3 } } } }; - cell.text[0] = 'X'; - cell.len = 1; - } - g.sel_glow.now_ns += 16 * std.time.ns_per_ms; - try afterglowDecor(&g, gpa, s, layout, 800, 480); - try std.testing.expectEqual(@as(usize, 1), Count.glows(g.decor.items, s.chrome.accent)); - const lit = g.decor.items[0]; - // Two cells wide: the cursor's is left out. - try std.testing.expectApproxEqAbs(@as(f32, 2 * 10 * 2.0 / 800.0), lit.x1 - lit.x0, 1e-4); - try std.testing.expect(g.afterglow.glowing); - // Gone by 250 ms. - g.decor.clearRetainingCapacity(); - g.sel_glow.now_ns += 260 * std.time.ns_per_ms; - try afterglowDecor(&g, gpa, s, layout, 800, 480); - try std.testing.expectEqual(@as(usize, 0), g.decor.items.len); - try std.testing.expect(!g.afterglow.glowing); - // A scroll changes every cell and lights none. - pane.scrollBy(5); - const scrolled = try core.render(core.scratch.allocator()); - g.sel_glow.now_ns += 16 * std.time.ns_per_ms; - try afterglowDecor(&g, gpa, scrolled, layout, 800, 480); - try std.testing.expectEqual(@as(usize, 0), g.decor.items.len); -} - /// What a bundled post pass leaves alone (Post.Frame.spare), in window /// pixels: every tag with its grip, the column and workspace tags, and the /// notices, the chrome the focus indicators live in. A grip, the gap after diff --git a/src/surface.zig b/src/surface.zig index 22a71a27..d1ea78b1 100644 --- a/src/surface.zig +++ b/src/surface.zig @@ -455,8 +455,7 @@ pub const Chrome = struct { /// A pixel shell's small effects: bit 0 a glow round the body's /// selection (G9, SelectionGlow), bit 1 one under the word a look-hover /// would open (G11, HoverGlow), bit 2 the bodies' edge shade (G12, - /// Occlusion), bit 3 the page pattern's parallax (G13, Parallax), bit 4 - /// changed text's afterglow (G14, Afterglow). + /// Occlusion), bit 3 the page pattern's parallax (G13, Parallax). fx: u8 = 0, }; |
