summaryrefslogtreecommitdiff
path: root/src/gui/gui.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/gui/gui.zig')
-rw-r--r--src/gui/gui.zig152
1 files changed, 151 insertions, 1 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index b5da9ad6..f74d5a0a 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -1870,6 +1870,8 @@ const Gui = struct {
/// picture follows on a spring (Glide), never the other way round.
scroll_notch: i32 = 0,
glide: Glide = .{},
+ /// G9: the selection's glow blocks as last drawn, for their fade-in.
+ sel_glow: SelGlow = .{},
/// 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.
@@ -4978,7 +4980,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();
+ defer core.shell_continuous = glide.moving() or g.sel_glow.fading;
glide.now_ns = core.now_ns;
glide.motion = pardes.animation.Motion.of(core.settings.motion);
if (!glide.spring.settled) {
@@ -5801,6 +5803,9 @@ fn renderFrame(
// G6: what the scroll just done moved the pane's rows by, measured on
// this frame against the last (the glide follows it from there).
if (g.glide.pending) g.glide.measure(gpa, surface);
+ 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.selection_glow == 0 or surface.panelTracks().len != 0) g.sel_glow = .{ .now_ns = g.sel_glow.now_ns };
if (g.glide.moving()) g.glide.offset = g.glide.spring.value(g.glide.now_ns);
cell_decor = CellDecor.of(chrome);
// Plaques need an opaque window: with WindowOpacity under 100 the cells
@@ -8690,6 +8695,8 @@ fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, g
};
const bodies = body_buf[0..nbodies];
try paneDecor(g, gpa, placed, groups, index, chrome, layout, surface.cols, surface.rows, win_w, win_h, track);
+ // G9, while nothing moves: a halo round the body's selection.
+ if (index == 0 and groups.tracks == 0 and chrome.selection_glow != 0) try selectionGlow(g, gpa, surface, ceilings, layout, win_w, win_h);
// A single spine runs down each column, through every tag and body.
if (index == 0) for (regions) |column| {
if (column.kind != .column) continue;
@@ -8928,6 +8935,99 @@ fn dp(logical: f32) f32 {
return @max(1, @round(logical * display_scale));
}
+/// G9's state: the selection's blocks last drawn and when each appeared.
+const SelGlow = struct {
+ const Block = struct { box: [4]u16, start_ns: u64 };
+ blocks: [32]Block = undefined,
+ len: usize = 0,
+ now_ns: u64 = 0,
+ /// A block is still fading in: the core keeps frames coming.
+ fading: bool = false,
+};
+
+/// How long a glow takes to come up (ยง8.1's micro-feedback: 80 to 120 ms).
+const sel_glow_ns: u64 = 100 * std.time.ns_per_ms;
+
+/// G9: a soft halo of the selection's own colour round each block of it in
+/// a pane body (rows of selected cells that touch), fading in over 100 ms
+/// with an ease-out. The selection itself is the halo's caster, never
+/// touched; so is a cursor beside it; the halo stays on bodies (never a
+/// tag or a grip) and at most 22%, less where the text round it would drop
+/// below min(its contrast, 4.5).
+fn selectionGlow(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, ceilings: LiftCeilings, layout: CellLayout, win_w: f32, win_h: f32) !void {
+ _ = ceilings;
+ const chrome = &surface.chrome;
+ const sel = chrome.sel_bg;
+ const page = chrome.page orelse bg_default;
+ const fg = chrome.fg orelse fg_default;
+ const cap = @min(0.22, lightenCeiling(fg, page, sel));
+ var blocks: [32][4]u16 = undefined;
+ var nblocks: usize = 0;
+ var bodies: [pardes.MAX_PANES][4]f32 = undefined;
+ var nbodies: usize = 0;
+ for (surface.regionList()) |body| {
+ if (body.kind != .body or body.rect.w == 0) continue;
+ if (nbodies < bodies.len) {
+ bodies[nbodies] = bodyPixels(body, layout);
+ nbodies += 1;
+ }
+ var open: ?[4]u16 = null; // x0, y0, x1, y1 of the block being grown
+ var row = body.rect.y;
+ while (row < body.rect.y + body.rect.h and row < surface.rows) : (row += 1) {
+ var x0: ?u16 = null;
+ var x1: u16 = 0;
+ var col = body.rect.x;
+ while (col < body.rect.x + body.rect.w and col < surface.cols) : (col += 1) {
+ const cell = &surface.cells[@as(usize, row) * surface.cols + col];
+ const selected = !cell.default and switch (cell.style.bg) {
+ .rgb => |rgb| std.mem.eql(u8, &rgb, &sel),
+ else => false,
+ };
+ if (!selected) continue;
+ if (x0 == null) x0 = col;
+ x1 = col + 1;
+ }
+ if (x0) |start| {
+ if (open) |*block| {
+ block.* = .{ @min(block[0], start), block[1], @max(block[2], x1), row + 1 };
+ } else open = .{ start, row, x1, row + 1 };
+ } else if (open) |block| {
+ if (nblocks < blocks.len) blocks[nblocks] = block;
+ nblocks += 1;
+ open = null;
+ }
+ }
+ if (open) |block| if (nblocks < blocks.len) {
+ blocks[nblocks] = block;
+ nblocks += 1;
+ };
+ }
+ nblocks = @min(nblocks, blocks.len);
+ // Each block keeps the start of the one it overlaps from the last frame.
+ var next: SelGlow = .{ .now_ns = g.sel_glow.now_ns };
+ for (blocks[0..nblocks]) |block| {
+ const start = for (g.sel_glow.blocks[0..g.sel_glow.len]) |old| {
+ if (old.box[0] < block[2] and block[0] < old.box[2] and old.box[1] < block[3] and block[1] < old.box[3]) break old.start_ns;
+ } else g.sel_glow.now_ns;
+ next.blocks[next.len] = .{ .box = block, .start_ns = start };
+ next.len += 1;
+ const t = @as(f32, @floatFromInt(g.sel_glow.now_ns -| start)) / @as(f32, @floatFromInt(sel_glow_ns));
+ if (t < 1) next.fading = true;
+ const u = std.math.clamp(t, 0, 1);
+ const fade = 1 - (1 - u) * (1 - u) * (1 - u);
+ var caster: [4]f32 = .{ @as(f32, @floatFromInt(block[0])) * layout.w, @as(f32, @floatFromInt(block[1])) * layout.h, @as(f32, @floatFromInt(block[2])) * layout.w, @as(f32, @floatFromInt(block[3])) * layout.h };
+ // A cursor beside the selection is not glowed over: it joins the
+ // caster.
+ if (surface.cursor) |cu| if (cu.x + 1 >= block[0] and cu.x <= block[2] and cu.y + 1 >= block[1] and cu.y <= block[3]) {
+ caster = .{ @min(caster[0], @as(f32, @floatFromInt(cu.x)) * layout.w), @min(caster[1], @as(f32, @floatFromInt(cu.y)) * layout.h), @max(caster[2], @as(f32, @floatFromInt(cu.x + 1)) * layout.w), @max(caster[3], @as(f32, @floatFromInt(cu.y + 1)) * layout.h) };
+ };
+ const first = g.decor.items.len;
+ try addSoft(g, gpa, caster, 0.3 * layout.h, 0, 0, cap * fade, sel, null, win_w, win_h);
+ try clipToBodies(g, gpa, first, bodies[0..nbodies]);
+ }
+ g.sel_glow = next;
+}
+
/// 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
@@ -9364,6 +9464,56 @@ test "a pane on the move casts a shadow on the other panes' bodies alone, most a
try std.testing.expectEqual(@as(f32, 0), strengths[2]);
}
+test "the selection glows round its block, on the body only, fading in over 100 ms" {
+ const gpa = std.testing.allocator;
+ const core = try pardes.Pardes.init(gpa, .{ .cols = 80, .rows = 24 });
+ defer core.deinit();
+ _ = try core.setTestFile("some text on a line\n" ** 30);
+ core.settings.selection_glow = true;
+ pardes.test_api.sync(core);
+ const s = try core.render(core.scratch.allocator());
+ try std.testing.expectEqual(@as(u8, 1), s.chrome.selection_glow);
+ // Two rows of a selection in the body, as the core paints one.
+ const body = for (s.regionList()) |region| {
+ if (region.kind == .body) break region;
+ } else return error.NoBody;
+ for (0..2) |dy| for (5..12) |x| {
+ const cell = &s.cells[(@as(usize, body.rect.y) + 3 + dy) * s.cols + body.rect.x + x];
+ cell.default = false;
+ cell.style.bg = .{ .rgb = s.chrome.sel_bg };
+ };
+ s.cursor = null;
+ var g: Gui = undefined;
+ g.decor = .empty;
+ defer g.decor.deinit(gpa);
+ g.cell_w = 10;
+ g.cell_h = 20;
+ g.tagline_width = 6;
+ g.tagline_height = 12;
+ g.sel_glow = .{ .now_ns = std.time.ns_per_s };
+ var strengths: [2]f32 = .{ 0, 0 };
+ for ([_]u64{ 0, 150 * std.time.ns_per_ms }, 0..) |later, i| {
+ g.decor.clearRetainingCapacity();
+ g.sel_glow.now_ns = std.time.ns_per_s + later;
+ var groups = makeGroups(&.{}, false);
+ try buildDecor(&g, gpa, s, &groups, fixedCellLayout(&g), 800, 480, true);
+ for (g.decor.items) |item| {
+ if (item.effect & soft_shadow_bit == 0) continue;
+ 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, &s.chrome.sel_bg)) continue;
+ strengths[i] = @max(strengths[i], item.fr);
+ // On the body alone.
+ const r = bodyPixels(body, fixedCellLayout(&g));
+ try std.testing.expect(item.clip_x0 >= r[0] - 0.01 and item.clip_x1 <= r[2] + 0.01 and item.clip_y0 >= r[1] - 0.01 and item.clip_y1 <= r[3] + 0.01);
+ }
+ try std.testing.expectEqual(@as(usize, 1), g.sel_glow.len);
+ }
+ // Just appeared: nothing yet, and still fading; later, up to its cap.
+ try std.testing.expectEqual(@as(f32, 0), strengths[0]);
+ try std.testing.expect(strengths[1] > 0.05 and strengths[1] <= 0.22 + 1e-4);
+ try std.testing.expect(!g.sel_glow.fading);
+}
+
/// 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