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.zig192
1 files changed, 157 insertions, 35 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 5d9783e3..d8bae6dc 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -7448,55 +7448,90 @@ fn addSoft(g: *Gui, gpa: std.mem.Allocator, caster: [4]f32, sigma_in: f32, dx: f
const LiftCeilings = struct {
shadow: f32,
white: f32,
- accent: f32,
+ /// A rim is light on a dark page and shade on a light one, where light
+ /// would not show.
+ rim: [3]u8,
+ /// Over the text on the page, and over a selection's text on its own
+ /// ground: the lower of the two.
fn of(chrome: *const pardes.Chrome) LiftCeilings {
const fg = chrome.fg orelse fg_default;
const bg = chrome.page orelse bg_default;
- return .{ .shadow = shadowCeiling(fg, bg), .white = lightenCeiling(fg, bg, .{ 255, 255, 255 }), .accent = lightenCeiling(fg, bg, chrome.accent) };
+ return .{
+ .shadow = @min(shadowCeiling(fg, bg), shadowCeiling(chrome.sel_fg, chrome.sel_bg)),
+ .white = @min(lightenCeiling(fg, bg, .{ 255, 255, 255 }), lightenCeiling(chrome.sel_fg, chrome.sel_bg, .{ 255, 255, 255 })),
+ .rim = if (luminance(bg) < 0.18) .{ 255, 255, 255 } else .{ 0, 0, 0 },
+ };
}
};
/// What stands a lifted part off the page, `lift` 0 to 1 (a pane's spring,
-/// a notice's 1), in the style chosen; `auto` reads the page: a shadow on a
-/// light one, a lighter surface and a faint rim on a dark one, where a
-/// shadow has nothing to darken.
-fn addLift(g: *Gui, gpa: std.mem.Allocator, style: pardes.config.Runtime.LiftStyle, chrome: *const pardes.Chrome, ceilings: LiftCeilings, caster: [4]f32, lift: f32, track: ?pardes.animation.Track, win_w: f32, win_h: f32) !void {
+/// a notice's 1), in the style chosen: a soft shadow, or a rim along the
+/// top of the focused pane's tag rows (`rim_edge`, a pane's only:
+/// never a notice's).
+fn addLift(g: *Gui, gpa: std.mem.Allocator, style: pardes.config.Runtime.LiftStyle, ceilings: LiftCeilings, caster: [4]f32, rim_edge: ?[3]f32, bodies: []const [4]f32, lift: f32, track: ?pardes.animation.Track, win_w: f32, win_h: f32) !void {
if (lift <= 0.001) return;
const h: f32 = @floatFromInt(g.cell_h);
- const dark_page = luminance(chrome.page orelse bg_default) < 0.18;
- const parts: struct { shadow: bool = false, surface: bool = false, rim: f32 = 0, glow: bool = false } = switch (style) {
+ // The core sends `auto` already resolved (draw.liftStyle).
+ const shadow = switch (style) {
.off => return,
- .shadow => .{ .shadow = true },
- .surface => .{ .surface = true },
- .rim => .{ .rim = 1 },
- .glow => .{ .glow = true },
- .auto => if (dark_page) .{ .surface = true, .rim = 0.5 } else .{ .shadow = true },
+ .shadow, .auto => true,
+ .rim => false,
};
- if (parts.surface) {
- // A raised surface catches more light: its ground, and all on it,
- // a few percent lighter, in whole pixels.
- const a = @min(Lift.surface * lift, ceilings.white);
- if (a > 0.002) try addDecor(g, gpa, caster[0], caster[1], caster[2], caster[3], .{ 255, 255, 255 }, a, track, win_w, win_h);
+ if (shadow) {
+ // Nothing of a shadow may touch a focus indicator (docs/effects.md):
+ // it falls on pane bodies and rails, never a tag, a grip or a header.
+ const first = g.decor.items.len;
+ try addSoft(g, gpa, caster, Shadow.sigma * h * lift, Shadow.side * h * lift, Shadow.drop * h * lift, @min(Shadow.strength * lift, ceilings.shadow), .{ 0, 0, 0 }, track, win_w, win_h);
+ try clipToBodies(g, gpa, first, bodies);
}
- if (parts.shadow) try addSoft(g, gpa, caster, Shadow.sigma * h * lift, Shadow.side * h * lift, Shadow.drop * h * lift, @min(Shadow.strength * lift, ceilings.shadow), .{ 0, 0, 0 }, track, win_w, win_h);
- // Light from above and to the left: a thin halo off the top and left
- // edges, gone by the bottom and the right.
- if (parts.rim > 0) try addSoft(g, gpa, caster, Lift.rim_sigma * h * lift, -Lift.rim_offset * h * lift, -Lift.rim_offset * h * lift, @min(Lift.rim * parts.rim * lift, ceilings.white), .{ 255, 255, 255 }, track, win_w, win_h);
- if (parts.glow) try addSoft(g, gpa, caster, Lift.glow_sigma * h * lift, 0, 0, @min(Lift.glow * lift, ceilings.accent), chrome.accent, track, win_w, win_h);
+ // A hairline just above the focused tag rows, never on them: it adds to
+ // the indicator without changing its colour.
+ if (style == .rim) if (rim_edge) |edge| {
+ const light = ceilings.rim[0] == 255;
+ const alpha = @min(Lift.rim * lift, if (light) ceilings.white else ceilings.shadow);
+ try addDecor(g, gpa, edge[0], edge[2] - 1, edge[1], edge[2], ceilings.rim, alpha, track, win_w, win_h);
+ };
}
-/// The lighter styles' strengths, in the §8.2 budget: coloured light at
-/// most 25%, a surface a few percent.
+/// A rim's strength, in the §8.2 budget for coloured light.
const Lift = struct {
- const surface: f32 = 0.055;
- const rim: f32 = 0.22;
- const rim_sigma: f32 = 0.12;
- const rim_offset: f32 = 0.1;
- const glow: f32 = 0.2;
- const glow_sigma: f32 = 0.5;
+ const rim: f32 = 0.35;
};
+/// A body region in window pixels.
+fn bodyPixels(region: pardes.Region, layout: CellLayout) [4]f32 {
+ return .{
+ @as(f32, @floatFromInt(region.rect.x)) * layout.w,
+ @as(f32, @floatFromInt(region.rect.y)) * layout.h,
+ @as(f32, @floatFromInt(region.rect.x + region.rect.w)) * layout.w,
+ @as(f32, @floatFromInt(region.rect.y + region.rect.h)) * layout.h,
+ };
+}
+
+/// Replaces the decor from `first` on with a copy per pane body it reaches,
+/// each clipped to that body (and to what a vertical transition already
+/// clipped it to): the lift then never reaches a tag, a grip or a header.
+fn clipToBodies(g: *Gui, gpa: std.mem.Allocator, first: usize, bodies: []const [4]f32) !void {
+ const added = g.decor.items.len - first;
+ if (added == 0) return;
+ var own: [8]CellInstance = undefined;
+ const n = @min(added, own.len);
+ @memcpy(own[0..n], g.decor.items[first..][0..n]);
+ g.decor.shrinkRetainingCapacity(first);
+ for (own[0..n]) |instance| for (bodies) |body| {
+ var copy = instance;
+ const vertical = copy.clip_x1 >= copy.clip_x0;
+ copy.clip_x0 = if (vertical) @max(copy.clip_x0, body[0]) else body[0];
+ copy.clip_y0 = if (vertical) @max(copy.clip_y0, body[1]) else body[1];
+ copy.clip_x1 = if (vertical) @min(copy.clip_x1, body[2]) else body[2];
+ copy.clip_y1 = if (vertical) @min(copy.clip_y1, body[3]) else body[3];
+ if (copy.clip_x1 <= copy.clip_x0 or copy.clip_y1 <= copy.clip_y0) continue;
+ try g.decor.append(gpa, copy);
+ };
+}
+
+
/// Like shadowCeiling for a light colour laid over at `a` (sRGB blend, as
/// the decor pipeline blends a colour that is not black): the largest `a`
/// that keeps text and page apart by min(their contrast, 4.5).
@@ -7547,11 +7582,11 @@ fn shadowCeiling(fg: [3]u8, bg: [3]u8) f32 {
return std.math.clamp(1 - 0.05 * (target - 1) / room, 0, 1);
}
-test "a rim, a glow or a surface never lightens a native theme's text below its contrast or 4.5" {
+test "a rim never lightens a native theme's text below its contrast or 4.5" {
for (pardes.themes) |theme| {
const fg = theme.fg orelse fg_default;
const bg = theme.bg orelse bg_default;
- for ([_][3]u8{ .{ 255, 255, 255 }, theme.box }) |light| {
+ for ([_][3]u8{.{ 255, 255, 255 }}) |light| {
const a = @min(Lift.rim, lightenCeiling(fg, bg, light));
const blend = struct {
fn of(under: [3]u8, over: [3]u8, t: f32) [3]u8 {
@@ -7570,6 +7605,74 @@ test "a rim, a glow or a surface never lightens a native theme's text below its
}
}
+test "a lift never touches a tag, a grip or a header, on a light theme or a dark one" {
+ const gpa = std.testing.allocator;
+ for ([_][]const u8{ "forge", "daybreak" }) |name| {
+ const core = try pardes.Pardes.init(gpa, .{ .cols = 80, .rows = 24 });
+ defer core.deinit();
+ core.update(.{ .resize = .{ .cols = 80, .rows = 24, .row_metrics = .{ .body_w = 10, .body_h = 20, .tagline_w = 6, .tagline_h = 12 } } });
+ _ = try core.newShell(1, "");
+ try std.testing.expect(pardes.layout.splitColumn(core, 0, 1, false));
+ var line: [64]u8 = undefined;
+ try std.testing.expect(core.executeBuiltinLine(0, try std.fmt.bufPrint(&line, "Theme {s}", .{name})));
+ for (0..40) |_| core.update(.tick);
+ for (std.enums.values(pardes.config.Runtime.LiftStyle)) |style| {
+ core.settings.lift = style;
+ try std.testing.expect(core.executeBuiltinLine(0, "Msg a floating notice"));
+ core.advance(core.now_ns + 2 * std.time.ns_per_s);
+ pardes.test_api.sync(core);
+ const s = try core.render(core.scratch.allocator());
+ core.advance(core.now_ns + 2 * std.time.ns_per_s);
+ const settled = try core.render(core.scratch.allocator());
+ _ = s;
+ // The focus indicator itself: the focused pane's tag is not the
+ // others' colour, whatever the lift.
+ var active: ?[3]u8 = null;
+ var other: ?[3]u8 = null;
+ for (settled.tagLayers()) |layer| if (layer.rows != 0 and layer.kind == .pane) {
+ if (layer.id == core.active) active = layer.bg else other = layer.bg;
+ };
+ try std.testing.expect(!std.mem.eql(u8, &active.?, &other.?));
+ var g: Gui = undefined;
+ g.cell_w = 10;
+ g.cell_h = 20;
+ g.tagline_width = 6;
+ g.tagline_height = 12;
+ g.decor = .empty;
+ defer g.decor.deinit(gpa);
+ var groups = makeGroups(&.{}, false);
+ try buildDecor(&g, gpa, settled, &groups, fixedCellLayout(&g), 800, 480, true);
+ var lifted: usize = 0;
+ for (g.decor.items) |item| {
+ if (item.clip_x1 < item.clip_x0) continue;
+ lifted += 1;
+ // Inside a body or a rail, all of it.
+ const inside = for (settled.regionList()) |region| {
+ if (region.kind != .body and region.kind != .rail) continue;
+ const r = bodyPixels(region, fixedCellLayout(&g));
+ if (item.clip_x0 >= r[0] and item.clip_y0 >= r[1] and item.clip_x1 <= r[2] and item.clip_y1 <= r[3]) break true;
+ } else false;
+ try std.testing.expect(inside);
+ }
+ const light_page = std.mem.eql(u8, name, "daybreak");
+ try std.testing.expectEqual(style == .shadow or (style == .auto and light_page), lifted != 0);
+ // A rim is one hairline, just above the focused pane's tag row.
+ var rims: usize = 0;
+ for (g.decor.items) |item| {
+ if (item.clip_x1 >= item.clip_x0) continue;
+ const top = (1 - item.y0) / 2 * 480;
+ const height = (item.y0 - item.y1) / 2 * 480;
+ const on_active_tag = for (settled.regionList()) |region| {
+ if (region.kind == .tag and region.owner == core.active and @abs(top + 1 - @as(f32, @floatFromInt(region.rect.y)) * 20) < 0.01 and @abs(height - 1) < 0.01) break true;
+ } else false;
+ // Not the rule under a column bar, which may run there too.
+ rims += @intFromBool(on_active_tag and item.fr < 1);
+ }
+ try std.testing.expectEqual(@as(usize, @intFromBool(style == .rim)), rims);
+ }
+ }
+}
+
test "a shadow never darkens a native theme's text below its contrast or 4.5" {
var shaded: usize = 0;
for (pardes.themes) |theme| {
@@ -7597,6 +7700,13 @@ fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, g
const chrome = &surface.chrome;
const regions = surface.regionList();
const ceilings = LiftCeilings.of(chrome);
+ var current_buf: [pardes.MAX_PANES][4]f32 = undefined;
+ var ncurrent: usize = 0;
+ for (regions) |part| if ((part.kind == .body or part.kind == .rail) and ncurrent < current_buf.len) {
+ current_buf[ncurrent] = bodyPixels(part, layout);
+ ncurrent += 1;
+ };
+ const current_bodies = current_buf[0..ncurrent];
const lift_style = std.enums.fromInt(pardes.config.Runtime.LiftStyle, chrome.lift_style) orelse .off;
const notice_lift = for (regions) |region| {
if (region.kind == .notice) break region.lift;
@@ -7638,6 +7748,14 @@ fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, g
// A closing pane is gone: its chrome is the last frame's.
const closing = if (track) |active| active.phase == .closing else false;
const placed = if (closing) surface.previous_regions else regions;
+ // Where a lift may fall: pane bodies, never tags (addLift).
+ var body_buf: [pardes.MAX_PANES][4]f32 = undefined;
+ var nbodies: usize = 0;
+ for (placed) |part| if ((part.kind == .body or part.kind == .rail) and nbodies < body_buf.len) {
+ body_buf[nbodies] = bodyPixels(part, layout);
+ nbodies += 1;
+ };
+ const bodies = body_buf[0..nbodies];
const grid = if (closing) surface.previous_cells else surface.cells;
try paneDecor(g, gpa, placed, groups, index, chrome, layout, surface.cols, surface.rows, win_w, win_h, track);
// A single spine runs down each column, through every tag and body.
@@ -7663,7 +7781,11 @@ fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, g
box[2] = @max(box[2], @as(f32, @floatFromInt(part.rect.x + part.rect.w)) * layout.w);
box[3] = @max(box[3], @as(f32, @floatFromInt(part.rect.y + part.rect.h)) * layout.h);
}
- try addLift(g, gpa, lift_style, chrome, ceilings, box, grip.lift, track, win_w, win_h);
+ // The top edge of its tag rows, for a rim: x0, x1, y.
+ const rim_edge: ?[3]f32 = for (placed) |part| {
+ if (part.owner == grip.owner and part.kind == .tag) break .{ @as(f32, @floatFromInt(grip.rect.x)) * layout.w, @as(f32, @floatFromInt(part.rect.x + part.rect.w)) * layout.w, @as(f32, @floatFromInt(part.rect.y)) * layout.h };
+ } else null;
+ try addLift(g, gpa, lift_style, ceilings, box, rim_edge, bodies, grip.lift, track, win_w, win_h);
}
}
// A notice chip is tagline hung over the body's top rows, so it is
@@ -7676,7 +7798,7 @@ fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, g
const x = @as(f32, @floatFromInt(layer.viewport.x)) * layout.w;
const top = (@as(f32, @floatFromInt(layer.viewport.y)) + layer.slide) * layout.h;
const track = groups.items[groupAt(groups, layer.viewport.x, layer.viewport.y)].track;
- try addLift(g, gpa, lift_style, chrome, ceilings, .{ x, top, x + @as(f32, @floatFromInt(layer.viewport.w)) * layout.w, top + layout.h }, notice_lift * layer.fade, track, win_w, win_h);
+ try addLift(g, gpa, lift_style, ceilings, .{ x, top, x + @as(f32, @floatFromInt(layer.viewport.w)) * layout.w, top + layout.h }, null, current_bodies, notice_lift * layer.fade, track, win_w, win_h);
}
for (surface.tagLayers()) |*layer| {
if (layer.rows == 0 or layer.kind != .notice or noticeGroup(groups, layer) != index) continue;