summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 18:14:36 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commitf41d0209f65237568c9f7d527d5989ac8921b74c (patch)
treec313dc20fa0593ecff725de32d6ad191b01d82ac
parentb4781b334a06d16aedb0b6a3d604cb249eb619de (diff)
downloadpardes-f41d0209f65237568c9f7d527d5989ac8921b74c.tar.gz
pardes-f41d0209f65237568c9f7d527d5989ac8921b74c.zip
Lift takes a style: shadow, surface, rim, glow or auto
The user wants Lift to read on dark themes, where a shadow has nothing to darken. `Lift shadow|surface|rim|glow|auto` (`on` is auto, bare flips auto and off): surface lightens the lifted part's ground about 5.5% (Material-dark), rim is a thin soft light off its top and left edges, glow a faint halo in the theme's accent (Chrome.accent, the focused box colour), auto a shadow on a light page and surface with half a rim on a dark one (by the page's luminance). Rim and glow are the soft kernel in a light colour (no pre-warp: coloured light is laid on as is, within §8.2's budget); each style is capped per theme so text keeps min(its contrast, 4.5) (tests: darkening and lightening over the native themes). The style travels in the palette (Chrome.lift_style, one byte on the wire). Config reports and ctl reads `Lift <style>`. Shared files touched: config.zig, builtins.zig, ninep/ctl.zig, detached/wire.zig (palette byte), gui.zig. Not touched: pardes.zig, Messages.zig, mouse.zig.
-rw-r--r--shaders/decor.frag.glsl6
-rw-r--r--src/builtins.zig2
-rw-r--r--src/config.zig59
-rw-r--r--src/detached/wire.zig4
-rw-r--r--src/draw.zig8
-rw-r--r--src/gui/gui.zig132
-rw-r--r--src/ninep/ctl.zig1
-rw-r--r--src/surface.zig4
8 files changed, 186 insertions, 30 deletions
diff --git a/shaders/decor.frag.glsl b/shaders/decor.frag.glsl
index 77f9c95e..3e2c286f 100644
--- a/shaders/decor.frag.glsl
+++ b/shaders/decor.frag.glsl
@@ -8,7 +8,7 @@
// colour rides in the cell's ground (a_bg), its coverage in a_fg.r, so the
// result is the overlay pipeline's exactly.
//
-// Soft shadow (flag 0x10000000): a rectangle's shadow blurred by a Gaussian,
+// Soft (flag 0x10000000): a rectangle's shadow, rim or glow blurred by a Gaussian,
// in closed form (the product of two erf edges), on one quad grown by 3
// sigma: no blur pass. Everything is in the quad's own 0..1 space, so a
// track's transform carries it: sigma per axis in the uv, the caster's
@@ -62,7 +62,9 @@ void main() {
vec2 caster1 = 1.0 - m - v_fg.gb;
if (all(greaterThanEqual(p, caster0)) && all(lessThan(p, caster1))) discard;
float a = v_fg.r * edge(p.x, m.x, 1.0 - m.x, s.x) * edge(p.y, m.y, 1.0 - m.y, s.y);
- a = 1.0 - pow(max(1.0 - a, 0.0), 1.0 / 2.2);
+ // Black darkens in linear light; a light colour (a rim, a glow) is laid
+ // on as it is, within §8.2's 25%.
+ if (all(equal(v_bg, vec3(0.0)))) a = 1.0 - pow(max(1.0 - a, 0.0), 1.0 / 2.2);
a = clamp(a + (ign(gl_FragCoord.xy) - 0.5) / 255.0, 0.0, 1.0);
o_col = vec4(v_bg * opacity, a);
}
diff --git a/src/builtins.zig b/src/builtins.zig
index 53cf0999..7b2aca38 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -171,7 +171,7 @@ pub const registry = struct {
inline for (comptime settingList(), manualBuiltinCount()..) |setting, i|
if (@intFromEnum(b) == i) return switch (setting.action) {
// a switch flips bare, and DumpDir bare is the default
- .toggle, .transition, .scene, .dump_dir => false,
+ .toggle, .transition, .scene, .dump_dir, .lift => false,
.shell, .theme, .font, .tagline_size, .window_opacity, .window_blur, .message_ms, .shader, .shader_animation => true,
};
unreachable;
diff --git a/src/config.zig b/src/config.zig
index 0ed8726a..f7e83e91 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -619,10 +619,11 @@ pub const Runtime = struct {
/// A font's programming ligatures (`->`, `!=`) draw across their cells.
/// Only a shell that shapes text has the setting (`Capabilities.ligatures`).
ligatures: bool = true,
- /// The focused pane and the notices stand off the page on soft shadows
- /// (G1, docs/render-pipeline.md §9.1). Opt-in until the focus lift's
+ /// How the focused pane and the notices stand off the page (G1,
+ /// docs/render-pipeline.md §9.1), or off. Opt-in until the focus lift's
/// default is decided. Only a pixel shell draws it (`Capabilities.lift`).
- lift: bool = false,
+ lift: LiftStyle = .off,
+
/// Builtins announce themselves on the message row.
verbose: bool = true,
/// A message falls into its row and dissolves out of it.
@@ -669,6 +670,13 @@ pub const Runtime = struct {
pub const ShaderAnimation = enum { off, on, always };
+ /// shadow: a soft drop shadow. surface: the lifted pane's ground
+ /// lightens, as a raised surface does on a dark theme. rim: a soft light
+ /// along its top and left edges. glow: a faint halo in the theme's
+ /// accent. auto: shadow on a light page, surface and a faint rim on a
+ /// dark one.
+ pub const LiftStyle = enum(u8) { off, shadow, surface, rim, glow, auto };
+
/// Each pass the bundled Crt or a Shadertoy file.
pub const Post = struct {
pub const max = 8;
@@ -822,7 +830,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, column_tags, syntax_bold, verbose, message_animation, ligatures, lift };
+ pub const Toggle = enum { colors, wrap, tree_context_tag_style, tag_bottom, debug, focus_tint, column_tags, syntax_bold, verbose, message_animation, ligatures };
/// The bundled post pass, a Shadertoy file in shaders/post/.
pub const Scene = enum { crt };
@@ -836,6 +844,8 @@ pub const Runtime = struct {
window_blur,
message_ms: MessageTime,
dump_dir,
+ /// A lift style, `on` (auto) or `off`; bare flips auto and off.
+ lift,
transition: animation.Transition,
scene: Scene,
/// A Shadertoy file into the post chain or out of it; `off` empties
@@ -870,7 +880,7 @@ pub const Runtime = struct {
.{ .word = "Verbose", .action = .{ .toggle = .verbose } },
.{ .word = "MessageAnimation", .action = .{ .toggle = .message_animation } },
.{ .word = "Ligatures", .action = .{ .toggle = .ligatures }, .availability = .ligatures },
- .{ .word = "Lift", .action = .{ .toggle = .lift }, .availability = .lift },
+ .{ .word = "Lift", .action = .lift, .availability = .lift },
.{ .word = "MessageLinger", .action = .{ .message_ms = .linger } },
.{ .word = "MessageFall", .action = .{ .message_ms = .fall } },
.{ .word = "MessageDissolve", .action = .{ .message_ms = .dissolve } },
@@ -932,9 +942,11 @@ pub const Runtime = struct {
@compileError("scene effects must use the scene-shader capability"),
// A toggle is plain state any shell can hold, except one for
// what only some shells draw: that one exists where they do.
- .toggle => |field| if (field == .ligatures or field == .lift) {
- if (setting.availability == null or !std.mem.eql(u8, @tagName(setting.availability.?), @tagName(field)))
- @compileError("a pixel shell's toggle must use its own capability: " ++ @tagName(field));
+ .lift => if (setting.availability != .lift)
+ @compileError("Lift must use the lift capability"),
+ .toggle => |field| if (field == .ligatures) {
+ if (setting.availability != .ligatures)
+ @compileError("the ligatures toggle must use the ligatures capability");
} else if (setting.availability != null)
@compileError("unconditional toggles cannot carry a backend capability"),
else => if (setting.availability != null)
@@ -1053,6 +1065,15 @@ pub const Runtime = struct {
const text = std.mem.trim(u8, argument orelse return false, " \t\r\n");
state.shader_animation = std.meta.stringToEnum(ShaderAnimation, text) orelse return false;
},
+ .lift => {
+ const text = std.mem.trim(u8, argument orelse "", " \t\r\n");
+ state.lift = if (text.len == 0)
+ (if (state.lift == .off) .auto else .off)
+ else if (std.mem.eql(u8, text, "on"))
+ .auto
+ else
+ std.meta.stringToEnum(LiftStyle, text) orelse return false;
+ },
.theme, .font => return false,
}
return true;
@@ -1222,6 +1243,10 @@ pub const Runtime = struct {
try out.print("{s} {s}\n", .{ setting.word, @tagName(state.shader_animation) })
else
try out.print("{s} unsupported\n", .{setting.word}),
+ .lift => if (setting.enabled(context.capabilities))
+ try out.print("{s} {s}\n", .{ setting.word, @tagName(state.lift) })
+ else
+ try out.print("{s} unsupported\n", .{setting.word}),
};
if (context.startup_config_path) |path|
@@ -1353,6 +1378,21 @@ pub const Runtime = struct {
try std.testing.expectEqual(@as(u8, 0), state.post.level(.crt));
}
+ test "Lift takes a style, on is auto, and bare flips it off and back to auto" {
+ var state: Runtime = .{};
+ const lift = find("Lift").?;
+ try std.testing.expect(apply(&state, lift, null));
+ try std.testing.expectEqual(LiftStyle.auto, state.lift);
+ try std.testing.expect(apply(&state, lift, "rim"));
+ try std.testing.expectEqual(LiftStyle.rim, state.lift);
+ try std.testing.expect(apply(&state, lift, null));
+ try std.testing.expectEqual(LiftStyle.off, state.lift);
+ try std.testing.expect(apply(&state, lift, "on"));
+ try std.testing.expectEqual(LiftStyle.auto, state.lift);
+ try std.testing.expect(!apply(&state, lift, "sparkle"));
+ try std.testing.expectEqual(LiftStyle.auto, state.lift);
+ }
+
test "tagline size validates before mutating live state" {
const setting = find("TaglineSize").?;
var state: Runtime = .{};
@@ -1474,6 +1514,7 @@ pub const Runtime = struct {
.dump_dir => "~/dumps",
.shader => "~/crt.glsl",
.shader_animation => "always",
+ .lift => "glow",
else => null,
};
try std.testing.expect(apply(&state, setting, argument));
@@ -1494,7 +1535,6 @@ pub const Runtime = struct {
.verbose => "Verbose off\n",
.message_animation => "MessageAnimation off\n",
.ligatures => "Ligatures off\n",
- .lift => "Lift on\n",
},
.shell => "Shell requested (new panes) fish\n",
.tagline_size => "TaglineSize 73\n",
@@ -1525,6 +1565,7 @@ pub const Runtime = struct {
},
.shader => "Shader ~/crt.glsl\n",
.shader_animation => "ShaderAnimation always\n",
+ .lift => "Lift glow\n",
.theme, .font => unreachable,
};
try std.testing.expect(std.mem.indexOf(u8, report, expected) != null);
diff --git a/src/detached/wire.zig b/src/detached/wire.zig
index 6e3b2d01..0a066410 100644
--- a/src/detached/wire.zig
+++ b/src/detached/wire.zig
@@ -267,6 +267,8 @@ pub const Frame = struct {
rgb.* = .{ 0, 0, 0 };
for (&rgb.*.?) |*channel| channel.* = try r.getByte();
}
+ } else if (field.type == u8) {
+ rgb.* = try r.getByte();
} else for (rgb) |*channel| channel.* = try r.getByte();
}
try r.end();
@@ -816,6 +818,8 @@ pub fn encodeFrameLayers(out: []u8, cols: u16, rows: u16, cursor: ?Cursor, point
if (field.type == ?[3]u8) {
try w.putBool(rgb != null);
if (rgb) |value| for (value) |channel| try w.putByte(channel);
+ } else if (field.type == u8) {
+ try w.putByte(rgb);
} else for (rgb) |channel| try w.putByte(channel);
}
}
diff --git a/src/draw.zig b/src/draw.zig
index 70b629a5..98932c23 100644
--- a/src/draw.zig
+++ b/src/draw.zig
@@ -128,12 +128,12 @@ pub fn place(p: *Pardes, s: *Surface) void {
spring.* = .{};
}
// At this frame's own time, not the stepped animations' 16 ms grid.
- spring.retarget(if (p.settings.lift and id == p.active) 1 else 0, p.now_ns);
+ spring.retarget(if (p.settings.lift != .off and id == p.active) 1 else 0, p.now_ns);
_ = spring.step(p.now_ns);
}
for (s.regions[0..s.nregions]) |*region| switch (region.kind) {
.grip, .tag, .body, .rail => region.lift = p.lifts[region.owner].value(p.now_ns),
- .notice => region.lift = if (p.settings.lift) 1 else 0,
+ .notice => region.lift = if (p.settings.lift != .off) 1 else 0,
else => {},
};
if (p.drag == .column_move) rail: {
@@ -200,6 +200,8 @@ pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface {
.tag_fg = chrome.tag_fg,
.scroll_track = chrome.scroll_track,
.scroll_thumb = chrome.scroll_thumb,
+ .accent = chrome.box,
+ .lift_style = @intFromEnum(p.settings.lift),
};
place(p, s);
// Separators have their own role; a scrollbar need not frame the page.
@@ -1133,7 +1135,7 @@ test "a lift is sampled at each frame's own time, and asks for frames only while
p.advance(1000 * ms);
_ = try p.render(arena.allocator());
try std.testing.expect(!p.continuous());
- p.settings.lift = true;
+ p.settings.lift = .shadow;
var lifts: [3]f32 = undefined;
// 144 Hz: 6.9 ms apart, none of them on a 16 ms step of its own.
for (&lifts, 0..) |*lift, i| {
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 905caa48..5d9783e3 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -7406,15 +7406,13 @@ const Shadow = struct {
const strength: f32 = 0.42;
};
-/// A soft shadow under `caster` (window pixels), `lift` 0 to 1, no darker
-/// than `ceiling`, moved by `track` as its caster is.
-fn addShadow(g: *Gui, gpa: std.mem.Allocator, caster: [4]f32, lift: f32, ceiling: f32, track: ?pardes.animation.Track, win_w: f32, win_h: f32) !void {
- const strength = @min(Shadow.strength * lift, ceiling);
- if (lift <= 0.001 or strength <= 0.002) return;
- const h: f32 = @floatFromInt(g.cell_h);
- const sigma = @max(0.5, Shadow.sigma * h * lift);
- const dx = Shadow.side * h * lift;
- const dy = Shadow.drop * h * lift;
+/// A soft rectangle around `caster` (window pixels), blurred by `sigma`
+/// and moved by (dx, dy): black is a shadow, a light colour a rim or a
+/// glow. The caster's own rectangle is left alone, and `track` moves it all
+/// as its caster moves.
+fn addSoft(g: *Gui, gpa: std.mem.Allocator, caster: [4]f32, sigma_in: f32, dx: f32, dy: f32, strength: f32, rgb: [3]u8, track: ?pardes.animation.Track, win_w: f32, win_h: f32) !void {
+ if (strength <= 0.002) return;
+ const sigma = @max(0.5, sigma_in);
const x0 = caster[0] + dx - 3 * sigma;
const y0 = caster[1] + dy - 3 * sigma;
const x1 = caster[2] + dx + 3 * sigma;
@@ -7436,15 +7434,95 @@ fn addShadow(g: *Gui, gpa: std.mem.Allocator, caster: [4]f32, lift: f32, ceiling
.fr = strength,
.fg = dx / w,
.fb = dy / ht,
- .br = 0,
- .bg = 0,
- .bb = 0,
+ .br = @as(f32, @floatFromInt(rgb[0])) / 255,
+ .bg = @as(f32, @floatFromInt(rgb[1])) / 255,
+ .bb = @as(f32, @floatFromInt(rgb[2])) / 255,
};
setTransitionFields(&instance, track, fixedCellLayout(g), win_w, win_h, 0);
instance.effect |= soft_shadow_bit;
try g.decor.append(gpa, instance);
}
+/// How far each lift style may go on this theme without its text falling
+/// below min(its contrast, 4.5): a shadow darkens, the others lighten.
+const LiftCeilings = struct {
+ shadow: f32,
+ white: f32,
+ accent: f32,
+
+ 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) };
+ }
+};
+
+/// 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 {
+ 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) {
+ .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 },
+ };
+ 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 (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);
+}
+
+/// The lighter styles' strengths, in the §8.2 budget: coloured light at
+/// most 25%, a surface a few percent.
+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;
+};
+
+/// 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).
+fn lightenCeiling(fg: [3]u8, bg: [3]u8, rgb: [3]u8) f32 {
+ const contrast = struct {
+ fn of(a: [3]u8, b: [3]u8) f32 {
+ const la = luminance(a);
+ const lb = luminance(b);
+ return (@max(la, lb) + 0.05) / (@min(la, lb) + 0.05);
+ }
+ fn blend(under: [3]u8, over: [3]u8, a: f32) [3]u8 {
+ var out: [3]u8 = undefined;
+ for (&out, under, over) |*o, x, y| o.* = @intFromFloat(@round(@as(f32, @floatFromInt(x)) * (1 - a) + @as(f32, @floatFromInt(y)) * a));
+ return out;
+ }
+ };
+ const target = @min(contrast.of(fg, bg), 4.5);
+ var lo: f32 = 0;
+ var hi: f32 = 1;
+ for (0..24) |_| {
+ const mid = (lo + hi) / 2;
+ if (contrast.of(contrast.blend(fg, rgb, mid), contrast.blend(bg, rgb, mid)) >= target - 1e-3) lo = mid else hi = mid;
+ }
+ return lo;
+}
+
/// sRGB's relative luminance (WCAG).
fn luminance(rgb: [3]u8) f32 {
var sum: f32 = 0;
@@ -7469,6 +7547,29 @@ 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" {
+ 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| {
+ const a = @min(Lift.rim, lightenCeiling(fg, bg, light));
+ const blend = struct {
+ fn of(under: [3]u8, over: [3]u8, t: f32) [3]u8 {
+ var out: [3]u8 = undefined;
+ for (&out, under, over) |*o, x, y| o.* = @intFromFloat(@round(@as(f32, @floatFromInt(x)) * (1 - t) + @as(f32, @floatFromInt(y)) * t));
+ return out;
+ }
+ }.of;
+ const ratio = struct {
+ fn of(x: [3]u8, y: [3]u8) f32 {
+ return (@max(luminance(x), luminance(y)) + 0.05) / (@min(luminance(x), luminance(y)) + 0.05);
+ }
+ }.of;
+ try std.testing.expect(ratio(blend(fg, light, a), blend(bg, light, a)) >= @min(ratio(fg, bg), 4.5) - 2e-3);
+ }
+ }
+}
+
test "a shadow never darkens a native theme's text below its contrast or 4.5" {
var shaded: usize = 0;
for (pardes.themes) |theme| {
@@ -7495,7 +7596,8 @@ fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, g
g.decor.clearRetainingCapacity();
const chrome = &surface.chrome;
const regions = surface.regionList();
- const ceiling = shadowCeiling(chrome.fg orelse fg_default, chrome.page orelse bg_default);
+ const ceilings = LiftCeilings.of(chrome);
+ 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;
} else 0;
@@ -7561,7 +7663,7 @@ 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 addShadow(g, gpa, box, grip.lift, ceiling, track, win_w, win_h);
+ try addLift(g, gpa, lift_style, chrome, ceilings, box, grip.lift, track, win_w, win_h);
}
}
// A notice chip is tagline hung over the body's top rows, so it is
@@ -7574,7 +7676,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 addShadow(g, gpa, .{ x, top, x + @as(f32, @floatFromInt(layer.viewport.w)) * layout.w, top + layout.h }, notice_lift * layer.fade, ceiling, track, win_w, win_h);
+ 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);
}
for (surface.tagLayers()) |*layer| {
if (layer.rows == 0 or layer.kind != .notice or noticeGroup(groups, layer) != index) continue;
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index a64521e7..46c451ed 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -305,6 +305,7 @@ fn writeSettings(p: *Pardes, w: *std.Io.Writer) !void {
.scene => |effect| if (s.post.level(effect) == 0) try w.print("{s} off\n", .{word}) else try w.print("{s} {d}\n", .{ word, s.post.level(effect) }),
.shader => for (s.post.list()) |entry| if (entry.scene == null) try w.print("{s} {s}\n", .{ word, entry.path.get() }),
.shader_animation => try w.print("{s} {s}\n", .{ word, @tagName(s.shader_animation) }),
+ .lift => try w.print("{s} {s}\n", .{ word, @tagName(s.lift) }),
}
}
try w.writeAll("LocationsConfig ");
diff --git a/src/surface.zig b/src/surface.zig
index 94b37915..e7149155 100644
--- a/src/surface.zig
+++ b/src/surface.zig
@@ -396,6 +396,10 @@ pub const Chrome = struct {
tag_fg: [3]u8 = .{ 0, 0, 0 },
scroll_track: [3]u8 = .{ 0, 0, 0 },
scroll_thumb: [3]u8 = .{ 0, 0, 0 },
+ /// The theme's accent (a glow's tint).
+ accent: [3]u8 = .{ 0, 0, 0 },
+ /// How a lifted part stands off the page (config.Runtime.LiftStyle).
+ lift_style: u8 = 0,
};
/// The canonical interface: what a frame of pardes IS. The tty shell writes