summaryrefslogtreecommitdiff
path: root/src/draw.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/draw.zig')
-rw-r--r--src/draw.zig85
1 files changed, 79 insertions, 6 deletions
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())));
+}