diff options
| -rw-r--r-- | src/draw.zig | 83 |
1 files changed, 78 insertions, 5 deletions
diff --git a/src/draw.zig b/src/draw.zig index 992b8ba0..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), @@ -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()))); +} |
