summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/config.md2
-rw-r--r--docs/effects.md10
-rw-r--r--src/config.zig12
-rw-r--r--src/draw.zig2
-rw-r--r--src/gui/gui.zig154
-rw-r--r--src/surface.zig3
6 files changed, 177 insertions, 6 deletions
diff --git a/docs/config.md b/docs/config.md
index 97bf95a1..c66b124e 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -580,6 +580,8 @@ Occlusion on pane bodies darken faintly toward their edges (2%), never
over the cursor
Parallax on a theme's page pattern (lapis's dots) moves with the text
at a quarter of its speed
+Afterglow on text that changes glows for a moment in the theme's accent
+ and fades (250 ms), never on a tag, the cursor or a selection
CursorBlink on the cursor blinks, solid while typing and half a second
after, eased at each edge, solid after 10 idle seconds
GripWidth 150 the grip's button and the scrollbar under it, percent of the
diff --git a/docs/effects.md b/docs/effects.md
index fbb44dfa..6a283e45 100644
--- a/docs/effects.md
+++ b/docs/effects.md
@@ -47,6 +47,7 @@ why. The verdict is the user's.
| G11 look-hover glow | `HoverGlow on\|off` (off) | a soft underline of the theme's accent under the word a look-hover would open, fading in over 80 ms | .scratch/render/hover/ (forge, acme, lapis; zooms) | pending |
| G12 ambient occlusion | `Occlusion on\|off` (off) | pane bodies darken toward their edges, at most 2% (§9.1: 1 to 2%) | .scratch/render/occlusion/ (on/off on forge, acme, lapis; acme-diff-x40.png) | pending |
| G13 parallax | `Parallax on\|off` (off) | a theme's page pattern under the bodies (lapis's dots) moves with the text at a quarter of its speed, glides included | .scratch/render/parallax/ (lapis, notches down and up) | pending |
+| G14 afterglow | `Afterglow on\|off` (off) | a body cell whose text changed glows in the accent at 18% and fades over 250 ms | .scratch/render/afterglow/ (forge, acme: two in-place edits) | pending |
## Motion flavours
@@ -237,3 +238,12 @@ so they drift with the text as it glides and settle with it (measured: 4
lines at 35 px move the 22 px grid 13 px, 35 mod 22). The cell shader
reads the offset from the instance's serial slot, free since G7 took the
dissolve's per-cell noise out. Without a pattern, nothing happens.
+
+Notes on G14: the GUI keeps the grid it last drew and when each cell last
+changed; a changed cell of a pane body glows in the theme's accent, 18% at
+most and under the text's contrast floor, and fades with an ease-out over
+250 ms, drawn as one flat rectangle a run of cells that changed together.
+Never on a tag or a grip (bodies only), the cursor's cell or a selection;
+a body whose first line moved (a scroll) starts over instead of lighting
+up whole. It runs on the continuous path while anything glows, and costs
+a compare of the grid a frame while on, nothing while off.
diff --git a/src/config.zig b/src/config.zig
index d11cbe76..2a764118 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -126,6 +126,7 @@ 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);
if (builtins.capabilities.lift) table.set(.Lift, null);
table.set(.Motion, null);
table.set(.InactiveDim, null);
@@ -682,6 +683,9 @@ pub const Runtime = struct {
/// G13: a theme's page pattern under the bodies (lapis's dots) moves
/// with the text at a quarter of its speed, a pixel shell's.
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,
/// How long a message stays after the key or click that dismisses it.
message_linger_ms: u16 = 800,
/// How long its fall into the row takes, easing out (§8.1: arriving,
@@ -905,7 +909,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 };
+ 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 };
/// 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 };
@@ -968,6 +972,7 @@ 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 = "Lift", .action = .lift, .availability = .lift, .doc = "How the focused pane stands off the page: off, shadow, rim or auto." },
.{ .word = "Motion", .action = .motion, .doc = "How animations move: off, crisp, smooth, bouncy or playful." },
.{ .word = "InactiveDim", .action = .inactive_dim, .doc = "Fade the unfocused panes' text by a percentage, 0 through 100, 0 off." },
@@ -1044,9 +1049,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) {
+ } else if (field == .cursor_blink or field == .selection_glow or field == .hover_glow or field == .occlusion or field == .parallax or field == .afterglow) {
if (setting.availability != .lift)
- @compileError("CursorBlink, SelectionGlow, HoverGlow, Occlusion and Parallax must use the lift capability (a pixel shell's)");
+ @compileError("CursorBlink, SelectionGlow, HoverGlow, Occlusion, Parallax and Afterglow 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)
@@ -1705,6 +1710,7 @@ pub const Runtime = struct {
.hover_glow => "HoverGlow on\n",
.occlusion => "Occlusion on\n",
.parallax => "Parallax on\n",
+ .afterglow => "Afterglow on\n",
},
.shell => "Shell requested (new panes) fish\n",
.tagline_size => "TaglineSize 73\n",
diff --git a/src/draw.zig b/src/draw.zig
index aef93f51..93d5b2d9 100644
--- a/src/draw.zig
+++ b/src/draw.zig
@@ -473,7 +473,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,
+ .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,
};
if (p.theme().decor) |decor| {
s.chrome.decor_dots = decor.page_dots;
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index dc58ee3f..b631a1cb 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -1874,6 +1874,8 @@ const Gui = struct {
/// last drawn, for their fade-in.
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.
@@ -2583,6 +2585,7 @@ 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);
@@ -4997,7 +5000,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;
+ defer core.shell_continuous = glide.moving() or g.sel_glow.fading or g.hover_glow.fading or g.afterglow.glowing;
glide.now_ns = core.now_ns;
glide.motion = pardes.animation.Motion.of(core.settings.motion);
if (!glide.spring.settled) {
@@ -5823,6 +5826,7 @@ 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);
@@ -8761,6 +8765,8 @@ fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, g
if (index == 0 and groups.tracks == 0 and chrome.fx & 1 != 0) try glowBlocks(g, gpa, surface, .selection, layout, win_w, win_h);
// 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.
@@ -9193,6 +9199,91 @@ 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
@@ -9795,6 +9886,67 @@ 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 d1ea78b1..22a71a27 100644
--- a/src/surface.zig
+++ b/src/surface.zig
@@ -455,7 +455,8 @@ 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).
+ /// Occlusion), bit 3 the page pattern's parallax (G13, Parallax), bit 4
+ /// changed text's afterglow (G14, Afterglow).
fx: u8 = 0,
};