summaryrefslogtreecommitdiff
path: root/src/gui
diff options
context:
space:
mode:
Diffstat (limited to 'src/gui')
-rw-r--r--src/gui/gui.zig335
1 files changed, 329 insertions, 6 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index bde9aecd..092bb3bf 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -507,6 +507,12 @@ const clear_bg_bit: u32 = 0x4000_0000;
const opaque_bg_bit: u32 = 0x2000_0000;
/// Decor that is a soft shadow, not a flat fill (decor.frag.glsl).
const soft_shadow_bit: u32 = 0x1000_0000;
+/// Decor that is a checker (decor.frag.glsl).
+const checker_bit: u32 = 0x0800_0000;
+/// A cell on the page's ground under the theme's dots, and a file name's
+/// cell that casts the theme's shadow (ui.frag.glsl, Chrome.decor_*).
+const dotted_bit: u32 = 0x1000_0000;
+const title_bit: u32 = 0x0800_0000;
const SavedImagePlace = struct {
key: pardes.ImageCacheKey,
@@ -1787,6 +1793,10 @@ const Gui = struct {
device: *c.SDL_GPUDevice,
swapchain_format: c.SDL_GPUTextureFormat,
pipeline: *c.SDL_GPUGraphicsPipeline,
+ /// The cell pipeline, blending premultiplied over what is drawn under:
+ /// for a frame with a theme's tag plaques, whose blank tag cells let
+ /// them show (tagPlaques).
+ pipeline_over: *c.SDL_GPUGraphicsPipeline,
decor_pipeline: *c.SDL_GPUGraphicsPipeline,
overlay_pipeline: *c.SDL_GPUGraphicsPipeline,
image_pipeline: *c.SDL_GPUGraphicsPipeline,
@@ -2068,6 +2078,9 @@ const Group = struct {
image_count: u32 = 0,
decor_start: u32 = 0,
decor_count: u32 = 0,
+ /// The first `under_count` of the group's decor are drawn before its
+ /// cells, under them (a theme's tag plaques, whose cells let them show).
+ under_count: u32 = 0,
};
/// The frame's draw groups, bottom to top: tier 0 (the page and the panes
@@ -2455,6 +2468,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
const atlas_xfer = c.SDL_CreateGPUTransferBuffer(device, &ax_info) orelse return error.GpuCreate;
const pipeline = try makePipeline(device, swapchain_format, frag_spv, 1, std.mem.zeroes(c.SDL_GPUColorTargetBlendState));
+ const pipeline_over = try makePipeline(device, swapchain_format, frag_spv, 1, premultipliedBlend());
const decor_pipeline = try makePipeline(device, swapchain_format, decor_frag_spv, 0, backgroundLayerBlend());
const overlay_pipeline = try makeOverlayPipeline(device, swapchain_format);
const image_pipeline = try makeImagePipeline(device, swapchain_format);
@@ -2485,6 +2499,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
.device = device,
.swapchain_format = swapchain_format,
.pipeline = pipeline,
+ .pipeline_over = pipeline_over,
.decor_pipeline = decor_pipeline,
.overlay_pipeline = overlay_pipeline,
.image_pipeline = image_pipeline,
@@ -2523,6 +2538,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
// regrown to; the image ones are released below.
defer {
c.SDL_ReleaseGPUGraphicsPipeline(device, g.pipeline);
+ c.SDL_ReleaseGPUGraphicsPipeline(device, g.pipeline_over);
c.SDL_ReleaseGPUGraphicsPipeline(device, g.decor_pipeline);
c.SDL_ReleaseGPUGraphicsPipeline(device, g.overlay_pipeline);
c.SDL_ReleaseGPUTexture(device, g.atlas_tex);
@@ -5418,6 +5434,20 @@ fn renderFrame(
defer if (building) build_zone.end();
const layout = fixedCellLayout(g);
const chrome = &surface.chrome;
+ cell_decor = CellDecor.of(chrome);
+ // Plaques need an opaque window: with WindowOpacity under 100 the cells
+ // are drawn as they are, and the plaques not at all.
+ cell_decor.plaques_off = backgroundOpacity(g.applied_window_opacity) < 1;
+ if (chrome.decor_box_border != null and !cell_decor.plaques_off) {
+ cell_decor.grounds = .{ chrome.tag_bg, chrome.tag_focus_bg };
+ for (surface.regionList()) |region| {
+ if (region.kind != .tag and region.kind != .column_tag and region.kind != .workspace_tag) continue;
+ if (cell_decor.nplaques == cell_decor.plaques.len) break;
+ const plaque = plaqueBox(g, chrome, surface.regionList(), region, layout, win_w, win_h) orelse continue;
+ cell_decor.plaques[cell_decor.nplaques] = plaque.band;
+ cell_decor.nplaques += 1;
+ }
+ }
var groups = makeGroups(surface.panelTracks(), surface.hasPanelDiff());
// Cursors stand still while panes move, as they always have; while the
// focused one glides or blinks, it is the overlay's quad instead. One
@@ -5696,9 +5726,16 @@ fn renderFrame(
const whole = c.SDL_Rect{ .x = 0, .y = 0, .w = @intCast(sw), .h = @intCast(sh) };
const decor_base = cell_total + shifted;
for (groups.items[0..groups.len], 0..) |group, index| {
+ if (group.under_count != 0 and g.vbuf != null) {
+ c.SDL_BindGPUGraphicsPipeline(rp, g.decor_pipeline);
+ pushLayerOpacity(cmd, rp, background_opacity);
+ const binding = c.SDL_GPUBufferBinding{ .buffer = g.vbuf.?, .offset = (decor_base + group.decor_start) * @sizeOf(CellInstance) };
+ c.SDL_BindGPUVertexBuffers(rp, 0, &binding, 1);
+ c.SDL_DrawGPUPrimitives(rp, 6, group.under_count, 0, 0);
+ }
const has_shifted = index == 0 and shifted != 0;
if ((group.cell_count != 0 or has_shifted) and g.vbuf != null) {
- c.SDL_BindGPUGraphicsPipeline(rp, g.pipeline);
+ c.SDL_BindGPUGraphicsPipeline(rp, if (cell_decor.nplaques != 0) g.pipeline_over else g.pipeline);
const samp_binding = c.SDL_GPUTextureSamplerBinding{ .texture = g.atlas_tex, .sampler = g.atlas_sampler };
c.SDL_BindGPUFragmentSamplers(rp, 0, &samp_binding, 1);
if (group.cell_count != 0) {
@@ -5716,13 +5753,13 @@ fn renderFrame(
}
}
if (group.image_count != 0) drawNativeImagesGpu(g, rp, surface, group, whole);
- if (group.decor_count != 0 and g.vbuf != null) {
+ if (group.decor_count > group.under_count and g.vbuf != null) {
c.SDL_BindGPUGraphicsPipeline(rp, g.decor_pipeline);
// Ink keeps its colour at any WindowOpacity.
pushLayerOpacity(cmd, rp, if (index == groups.ink) 1 else background_opacity);
- const binding = c.SDL_GPUBufferBinding{ .buffer = g.vbuf.?, .offset = (decor_base + group.decor_start) * @sizeOf(CellInstance) };
+ const binding = c.SDL_GPUBufferBinding{ .buffer = g.vbuf.?, .offset = (decor_base + group.decor_start + group.under_count) * @sizeOf(CellInstance) };
c.SDL_BindGPUVertexBuffers(rp, 0, &binding, 1);
- c.SDL_DrawGPUPrimitives(rp, 6, group.decor_count, 0, 0);
+ c.SDL_DrawGPUPrimitives(rp, 6, group.decor_count - group.under_count, 0, 0);
}
}
drawOverlayGpu(g, cmd, rp, overlay_count);
@@ -6024,6 +6061,9 @@ fn emitTagLayer(g: *Gui, instances: [*]CellInstance, next: *u32, layer: *const p
const y = layer.viewport.y + @as(u16, @intCast(index));
const top = @as(f32, @floatFromInt(y)) * bh;
emitInstance(g, instances, next.*, 0, y, .{ .w = band_right - left, .h = bh, .x_off = left, .y_off = slide }, win_w, win_h, track, .body, (&background)[0..1], 0, null, old_layer, false, page);
+ // A theme's plaque is drawn under the band: the band's own ground
+ // lets it show (tagPlaques).
+ if (cell_decor.nplaques != 0 and cell_decor.inPlaque(left + 0.5, top + 0.5)) instances[next.*].effect |= clear_bg_bit;
if (slide != 0) clipInstanceTop(&instances[next.*], top, win_h);
next.* += 1;
const cells = layer.cells[index * layer.cols ..][0..layer.cols];
@@ -6229,6 +6269,10 @@ fn emitSurfaceCell(
// this underlay at tagline height leaves page-colored gutters between
// adjacent workspace, column and pane tags at TaglineSize < 100.
emitInstance(g, instances, next.*, col, row, body_layout, win_w, win_h, track, .body, (&band_base)[0..1], 0, null, old_layer, false, page);
+ // Over a theme's plaque (not a grip's button) the underlay lets it
+ // show (tagPlaques).
+ if (cell_decor.nplaques != 0 and !here.grip and cell_decor.inPlaque((@as(f32, @floatFromInt(col)) + 0.5) * body_layout.w + body_layout.x_off, @as(f32, @floatFromInt(row)) * body_layout.h + body_layout.y_off + 0.5))
+ instances[next.*].effect |= clear_bg_bit;
next.* += 1;
// Tag glyphs keep their font size inside a full physical-width grip.
// A pane's grip cells are the cell shells' form (a dirty `*`): the
@@ -6553,6 +6597,21 @@ fn emitInstance(
// A block cursor is foreground UI ink, even though its color is carried
// in the reversed cell background. Keep it visible at WindowOpacity 0.
if (is_cursor) instances[idx].effect |= opaque_bg_bit;
+ // The theme's ornament (cell_decor): dots on the page's own ground, a
+ // shadow under a file name's letters. Never on a cursor.
+ if (!is_cursor) {
+ // A blank cell on a tag's ground inside a plaque: the plaque shows.
+ if (cell_decor.nplaques != 0 and resolved.role == .tagline and cellCodepoint(&line[at]) == ' ' and
+ (std.mem.eql(u8, &resolved.bg, &cell_decor.grounds[0]) or std.mem.eql(u8, &resolved.bg, &cell_decor.grounds[1])) and
+ (cell_decor.inPlaque(px0 + 0.5, py0 + 0.5) or cell_decor.inPlaque(px0 + layout.w - 0.5, py0 + 0.5)))
+ instances[idx].effect |= clear_bg_bit;
+ if (cell_decor.dots) |dotted| {
+ if (!resolved.clear_bg and std.mem.eql(u8, &resolved.bg, &dotted)) instances[idx].effect |= dotted_bit;
+ }
+ if (cell_decor.title) |inks| {
+ if (resolved.role == .tagline and (std.mem.eql(u8, &resolved.fg, &inks[0]) or std.mem.eql(u8, &resolved.fg, &inks[1]))) instances[idx].effect |= title_bit;
+ }
+ }
}
test "block cursor alone overrides transparent cell background" {
@@ -6569,6 +6628,107 @@ test "block cursor alone overrides transparent cell background" {
}
}
+test "lapis dots only the page's own ground and shadows only a file name, never a cursor" {
+ var g: Gui = undefined;
+ g.words = .{};
+ g.cell_w = 10;
+ g.cell_h = 20;
+ g.tagline_width = 8;
+ g.tagline_height = 16;
+ g.space_slot = .{ .u = 0, .v = 0 };
+ const page: [3]u8 = .{ 0x0a, 0x10, 0x30 };
+ const name: [3]u8 = .{ 0xe8, 0xc4, 0x6a };
+ var chrome: pardes.Chrome = .{ .page = page, .decor_dots = .{ 1, 2, 3 }, .decor_dot_alpha = 0x22, .decor_title = .{ 4, 5, 6 }, .name_ink = name, .active_name_ink = name };
+ cell_decor = CellDecor.of(&chrome);
+ defer cell_decor = .{};
+ const on_page: pardes.Cell = .{ .default = false, .style = .{ .fg = .{ .rgb = .{ 200, 200, 200 } }, .bg = .{ .rgb = page } } };
+ const selected: pardes.Cell = .{ .default = false, .style = .{ .fg = .{ .rgb = .{ 0, 0, 0 } }, .bg = .{ .rgb = name } } };
+ const title: pardes.Cell = .{ .default = false, .style = .{ .fg = .{ .rgb = name }, .bg = .{ .rgb = .{ 0x0f, 0x1a, 0x4a } }, .font_role = .tagline } };
+ const plain_tag: pardes.Cell = .{ .default = false, .style = .{ .fg = .{ .rgb = .{ 0x9a, 0xa6, 0xd9 } }, .bg = .{ .rgb = .{ 0x0f, 0x1a, 0x4a } }, .font_role = .tagline } };
+ var instances: [1]CellInstance = undefined;
+ const Case = struct { cell: pardes.Cell, role: pardes.FontRole, cursor: bool, dotted: bool, shadowed: bool };
+ for ([_]Case{
+ .{ .cell = on_page, .role = .body, .cursor = false, .dotted = true, .shadowed = false },
+ .{ .cell = on_page, .role = .body, .cursor = true, .dotted = false, .shadowed = false },
+ .{ .cell = selected, .role = .body, .cursor = false, .dotted = false, .shadowed = false },
+ .{ .cell = title, .role = .tagline, .cursor = false, .dotted = false, .shadowed = true },
+ .{ .cell = title, .role = .tagline, .cursor = true, .dotted = false, .shadowed = false },
+ .{ .cell = plain_tag, .role = .tagline, .cursor = false, .dotted = false, .shadowed = false },
+ }) |case| {
+ emitInstance(&g, &instances, 0, 0, 0, fixedCellLayout(&g), 800, 480, null, case.role, (&case.cell)[0..1], 0, null, false, case.cursor, Ground.opaqueRgb(page));
+ try std.testing.expectEqual(case.dotted, instances[0].effect & dotted_bit != 0);
+ try std.testing.expectEqual(case.shadowed, instances[0].effect & title_bit != 0);
+ }
+ // A theme without decor marks nothing.
+ chrome = .{ .page = page };
+ cell_decor = CellDecor.of(&chrome);
+ emitInstance(&g, &instances, 0, 0, 0, fixedCellLayout(&g), 800, 480, null, .body, (&on_page)[0..1], 0, null, false, false, Ground.opaqueRgb(page));
+ try std.testing.expectEqual(@as(u32, 0), instances[0].effect & (dotted_bit | title_bit));
+}
+
+test "lapis plaques stay in their tag's band, clear of every grip; only the focused one is striped" {
+ const gpa = std.testing.allocator;
+ const core = try pardes.Pardes.init(gpa, .{ .cols = 80, .rows = 24 });
+ defer core.deinit();
+ _ = try core.newShell(1, "");
+ try std.testing.expect(pardes.layout.splitColumn(core, 0, 1, false));
+ try std.testing.expect(core.executeBuiltinLine(0, "Theme lapis"));
+ for (0..40) |_| core.update(.tick);
+ pardes.test_api.sync(core);
+ const s = try core.render(core.scratch.allocator());
+ try std.testing.expect(s.chrome.decor_box_border != null and s.chrome.decor_checker != null and s.chrome.decor_dots != null and s.chrome.decor_title != null);
+ var g: Gui = undefined;
+ g.decor = .empty;
+ defer g.decor.deinit(gpa);
+ g.cell_w = 16;
+ g.cell_h = 32;
+ g.tagline_width = 12;
+ g.tagline_height = 22;
+ display_scale = 1;
+ var groups = makeGroups(&.{}, false);
+ try buildDecor(&g, gpa, s, &groups, fixedCellLayout(&g), 1280, 768, true);
+ const layout = fixedCellLayout(&g);
+ const under = g.decor.items[groups.items[0].decor_start..][0..groups.items[0].under_count];
+ try std.testing.expect(under.len != 0);
+ var stripes: usize = 0;
+ var shadows: usize = 0;
+ for (under) |item| {
+ const r: [4]f32 = .{ (item.x0 + 1) / 2 * 1280, (1 - item.y0) / 2 * 768, (item.x1 + 1) / 2 * 1280, (1 - item.y1) / 2 * 768 };
+ // Inside one tag's band, whole: never on a body, a grip or the
+ // next pane.
+ const home = for (s.regionList()) |region| {
+ if (region.kind != .tag and region.kind != .column_tag and region.kind != .workspace_tag) continue;
+ const band = plaqueBox(&g, &s.chrome, s.regionList(), region, layout, 1280, 768) orelse continue;
+ if (r[0] >= band.band[0] - 0.01 and r[1] >= band.band[1] - 0.01 and r[2] <= band.band[2] + 0.01 and r[3] <= band.band[3] + 0.01) break region;
+ } else return error.PlaqueOutsideItsBand;
+ for (s.regionList()) |grip| {
+ if (grip.kind != .grip and grip.kind != .column_grip) continue;
+ const gx0 = @as(f32, @floatFromInt(grip.rect.x)) * 16;
+ const gx1 = @as(f32, @floatFromInt(grip.rect.x + grip.rect.w)) * 16;
+ const gy0 = @as(f32, @floatFromInt(grip.rect.y)) * 32;
+ const gy1 = @as(f32, @floatFromInt(grip.rect.y + grip.rect.h)) * 32;
+ try std.testing.expect(r[2] <= gx0 + 0.01 or r[0] >= gx1 - 0.01 or r[3] <= gy0 + 0.01 or r[1] >= gy1 - 0.01);
+ }
+ if (item.effect & checker_bit != 0) {
+ try std.testing.expect(home.kind == .tag and home.active);
+ stripes += 1;
+ }
+ 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.decor_box_shadow.?)) shadows += 1;
+ }
+ try std.testing.expectEqual(@as(usize, 1), stripes);
+ try std.testing.expect(shadows >= 2);
+ // The scroll track is chequered.
+ var checkers: usize = 0;
+ for (g.decor.items[groups.items[0].decor_start + groups.items[0].under_count ..]) |item| checkers += @intFromBool(item.effect & checker_bit != 0);
+ try std.testing.expect(checkers >= 1);
+ // No other theme draws any of it.
+ try std.testing.expect(core.executeBuiltinLine(0, "Theme forge"));
+ for (0..40) |_| core.update(.tick);
+ const plain = try core.render(core.scratch.allocator());
+ try std.testing.expect(plain.chrome.decor_box_border == null and plain.chrome.decor_checker == null and plain.chrome.decor_dots == null and plain.chrome.decor_title == null);
+}
+
fn cellCodepoint(cell: *const pardes.Cell) u32 {
const grapheme = cell.grapheme();
return if (cell.default or grapheme.len == 0) ' ' else firstCp(grapheme);
@@ -7352,6 +7512,20 @@ fn makePipeline(device: *c.SDL_GPUDevice, color_format: c.SDL_GPUTextureFormat,
return c.SDL_CreateGPUGraphicsPipeline(device, &info) orelse error.GpuCreate;
}
+/// Premultiplied over: an opaque cell writes itself exactly, a see-through
+/// one only its ink.
+fn premultipliedBlend() c.SDL_GPUColorTargetBlendState {
+ var blend = std.mem.zeroes(c.SDL_GPUColorTargetBlendState);
+ blend.src_color_blendfactor = c.SDL_GPU_BLENDFACTOR_ONE;
+ blend.dst_color_blendfactor = c.SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
+ blend.color_blend_op = c.SDL_GPU_BLENDOP_ADD;
+ blend.src_alpha_blendfactor = c.SDL_GPU_BLENDFACTOR_ONE;
+ blend.dst_alpha_blendfactor = c.SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
+ blend.alpha_blend_op = c.SDL_GPU_BLENDOP_ADD;
+ blend.enable_blend = true;
+ return blend;
+}
+
fn backgroundLayerBlend() c.SDL_GPUColorTargetBlendState {
var blend = std.mem.zeroes(c.SDL_GPUColorTargetBlendState);
blend.src_color_blendfactor = c.SDL_GPU_BLENDFACTOR_SRC_ALPHA;
@@ -7742,6 +7916,29 @@ fn addDecor(g: *Gui, gpa: std.mem.Allocator, x0_in: f32, y0_in: f32, x1_in: f32,
try g.decor.append(gpa, instance);
}
+/// A checker (decor.frag.glsl): squares of `px` window pixels in `a` and
+/// `b` from the quad's corner (lapis's scroll track); `stripes`, bands of
+/// `px` across it instead (lapis's focused title bar), `a` first.
+fn addChecker(g: *Gui, gpa: std.mem.Allocator, x0: f32, y0: f32, x1: f32, y1: f32, a: [3]u8, b: [3]u8, px: f32, track: ?pardes.animation.Track, win_w: f32, win_h: f32) !void {
+ return addPattern(g, gpa, x0, y0, x1, y1, a, b, px, false, track, win_w, win_h);
+}
+
+fn addPattern(g: *Gui, gpa: std.mem.Allocator, x0: f32, y0: f32, x1: f32, y1: f32, a: [3]u8, b: [3]u8, px: f32, stripes: bool, track: ?pardes.animation.Track, win_w: f32, win_h: f32) !void {
+ const first = g.decor.items.len;
+ try addDecor(g, gpa, x0, y0, x1, y1, a, 1, track, win_w, win_h);
+ if (g.decor.items.len == first) return;
+ const instance = &g.decor.items[first];
+ instance.u0 = px;
+ instance.u1 = px;
+ instance.v0 = if (stripes) 1 else 0;
+ instance.v1 = if (stripes) 1 else 0;
+ instance.fr = @as(f32, @floatFromInt(b[0])) / 255.0;
+ instance.fg = @as(f32, @floatFromInt(b[1])) / 255.0;
+ instance.fb = @as(f32, @floatFromInt(b[2])) / 255.0;
+ instance.effect |= checker_bit;
+}
+
+
/// How a lift of 0 to 1 stands off the page: the shadow's blur and offset
/// in cell heights, and its darkest, where it leaves the caster's edge.
/// Within ยง8.2's budget: sigma 0.4-0.8 cell, at most 30% darker at the edge.
@@ -8062,6 +8259,13 @@ fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, g
for (groups.items[0..groups.len], 0..) |*group, index| {
group.decor_start = @intCast(g.decor.items.len);
defer group.decor_count = @as(u32, @intCast(g.decor.items.len)) - group.decor_start;
+ // Under the cells: a theme's tag plaques, this group's panes' (and
+ // the column and workspace tags with the still ones).
+ if (index <= groups.tracks) {
+ const closing = if (group.track) |active| active.phase == .closing else false;
+ try tagPlaques(g, gpa, if (closing) surface.previous_regions else regions, groups, index, chrome, layout, win_w, win_h, group.track);
+ }
+ group.under_count = @as(u32, @intCast(g.decor.items.len)) - group.decor_start;
// The grips' buttons before any rule: the rules over their rows cut
// them, and the band round them never covers a rule.
if (index <= groups.tracks) {
@@ -8226,7 +8430,10 @@ fn paneDecor(g: *Gui, gpa: std.mem.Allocator, regions: []const pardes.Region, gr
// the track edges it.
const column = scrollColumn(g, chrome, rail.rect.x, layout);
try addDecor(g, gpa, x0, y0, x0 + layout.w, y1, page, 1, track, win_w, win_h);
- try addDecor(g, gpa, column[0], y0, column[1], y1, chrome.scroll_track, 1, track, win_w, win_h);
+ if (chrome.decor_checker) |gold|
+ try addChecker(g, gpa, column[0], y0, column[1], y1, chrome.scroll_track, gold, dp(@floatFromInt(@max(1, chrome.decor_checker_px))), track, win_w, win_h)
+ else
+ try addDecor(g, gpa, column[0], y0, column[1], y1, chrome.scroll_track, 1, track, win_w, win_h);
if (rail.thumb_h != 0) {
const thumb_y0 = @as(f32, @floatFromInt(rail.rect.y + rail.thumb_y)) * layout.h;
const thumb_y1 = if (rail.thumb_y + rail.thumb_h == rail.rect.h) y1 else @as(f32, @floatFromInt(rail.rect.y + rail.thumb_y + rail.thumb_h)) * layout.h;
@@ -8269,11 +8476,126 @@ fn updateDisplayScale(window: *c.SDL_Window) void {
display_scale = std.math.clamp(display_scale, 1, 4);
}
+/// A theme's ornament as the cell shader draws it (Chrome.decor_*), for the
+/// frame being built: renderFrame sets it, writing a cell marks the cells
+/// it falls on, and pushLayerOpacity hands the shader its colours and
+/// sizes. ponytail: one window per process, as display_scale.
+var cell_decor: CellDecor = .{};
+const CellDecor = struct {
+ /// The page's ground, when the theme dots it.
+ dots: ?[3]u8 = null,
+ /// A file name's inks in a tag, when the theme shadows it.
+ title: ?[2][3]u8 = null,
+ /// UiUniforms after background_opacity (ui.frag.glsl): the dots' colour
+ /// and strength, their period and radii, the shadow's colour and offset.
+ uniforms: [12]f32 = @splat(0),
+ /// The tag plaques' bands (window pixels) and the tag grounds: a blank
+ /// tag cell there lets its plaque show through (tagPlaques).
+ plaques: [pardes.MAX_TAG_LAYERS][4]f32 = undefined,
+ nplaques: usize = 0,
+ grounds: [2][3]u8 = .{ .{ 0, 0, 0 }, .{ 0, 0, 0 } },
+ plaques_off: bool = false,
+
+ fn inPlaque(decor: *const CellDecor, x: f32, y: f32) bool {
+ for (decor.plaques[0..decor.nplaques]) |band|
+ if (x >= band[0] and x < band[2] and y >= band[1] and y < band[3]) return true;
+ return false;
+ }
+
+ fn of(chrome: *const pardes.Chrome) CellDecor {
+ var out: CellDecor = .{};
+ if (chrome.decor_dots) |rgb| if (chrome.page) |page| {
+ out.dots = page;
+ out.uniforms[0..4].* = .{ unit(rgb[0]), unit(rgb[1]), unit(rgb[2]), unit(chrome.decor_dot_alpha) };
+ out.uniforms[4..8].* = .{ dp(@floatFromInt(@max(2, chrome.decor_dot_px))), 0.75 * display_scale, 1.25 * display_scale, 0 };
+ };
+ if (chrome.decor_title) |rgb| {
+ out.title = .{ chrome.name_ink, chrome.active_name_ink };
+ out.uniforms[8..12].* = .{ unit(rgb[0]), unit(rgb[1]), unit(rgb[2]), dp(@floatFromInt(@max(1, chrome.decor_title_px))) };
+ }
+ return out;
+ }
+
+ fn unit(v: u8) f32 {
+ return @as(f32, @floatFromInt(v)) / 255;
+ }
+};
+
/// A logical pixel size in the display's pixels: rounded, at least one.
fn dp(logical: f32) f32 {
return @max(1, @round(logical * display_scale));
}
+/// 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
+/// shadow down and right that stays inside the band too, so it never falls
+/// on a body, a grip or the next pane. The focused pane's box is striped,
+/// its words on plates of the tag's ground (their cells stay opaque; the
+/// spaces between let the stripes through, emitInstance); a column or the
+/// workspace takes a quieter box, a one-pixel frame and a two-pixel shadow.
+fn tagPlaques(g: *Gui, gpa: std.mem.Allocator, regions: []const pardes.Region, groups: *const Groups, index: usize, chrome: *const pardes.Chrome, layout: CellLayout, win_w: f32, win_h: f32, track: ?pardes.animation.Track) !void {
+ const border_rgb = chrome.decor_box_border orelse return;
+ if (cell_decor.plaques_off) return;
+ const page = chrome.page orelse bg_default;
+ for (regions) |tag| {
+ const pane = tag.kind == .tag;
+ if (!pane and tag.kind != .column_tag and tag.kind != .workspace_tag) continue;
+ if (tag.rect.w == 0 or tag.rect.h == 0) continue;
+ if (pane and groupOf(groups, tag.serial) != index) continue;
+ if (!pane and index != 0) continue;
+ const plaque = plaqueBox(g, chrome, regions, tag, layout, win_w, win_h) orelse continue;
+ const b = plaque.border;
+ const sh = plaque.shadow;
+ const fill = if (tag.active) chrome.tag_focus_bg else chrome.tag_bg;
+ // The band's own ground, then the shadow, the frame and the fill.
+ try addDecor(g, gpa, plaque.band[0], plaque.band[1], plaque.band[2], plaque.band[3], page, 1, track, win_w, win_h);
+ const shadow_rgb = chrome.decor_box_shadow orelse border_rgb;
+ try addDecor(g, gpa, plaque.box[0] + sh[0], plaque.box[1] + sh[1], plaque.box[2] + sh[0], plaque.box[3] + sh[1], if (pane) shadow_rgb else mixRgb(shadow_rgb, page), 1, track, win_w, win_h);
+ try addDecor(g, gpa, plaque.box[0], plaque.box[1], plaque.box[2], plaque.box[3], border_rgb, 1, track, win_w, win_h);
+ const inner: [4]f32 = .{ plaque.box[0] + b, plaque.box[1] + b, plaque.box[2] - b, plaque.box[3] - b };
+ if (pane and tag.active and chrome.decor_box_stripe != null)
+ try addPattern(g, gpa, inner[0], inner[1], inner[2], inner[3], chrome.decor_box_stripe.?, fill, dp(@floatFromInt(@max(1, chrome.decor_box_stripe_px))), true, track, win_w, win_h)
+ else
+ try addDecor(g, gpa, inner[0], inner[1], inner[2], inner[3], fill, 1, track, win_w, win_h);
+ }
+}
+
+const Plaque = struct { band: [4]f32, box: [4]f32, border: f32, shadow: [2]f32 };
+
+/// A tag's plaque in window pixels: its band (the rows, from after the
+/// grip to the end), the framed box round the tagline's text, and the
+/// frame's and the shadow's widths, the shadow cut to what the band's slack
+/// below the text and its right end leave. Null where there is no room.
+fn plaqueBox(g: *const Gui, chrome: *const pardes.Chrome, regions: []const pardes.Region, tag: pardes.Region, layout: CellLayout, win_w: f32, win_h: f32) ?Plaque {
+ const pane = tag.kind == .tag;
+ const first = tag.rect.y;
+ const last = tag.rect.y + tag.rect.h - 1;
+ // From its start, right of its grip: the gap cell before its text is
+ // the plaque's margin, so its frame is never under the first letter.
+ _ = regions;
+ const x0 = @as(f32, @floatFromInt(tag.rect.x)) * layout.w;
+ const x1_cells = @as(f32, @floatFromInt(tag.rect.x + tag.rect.w)) * layout.w;
+ const x1 = if (win_w - x1_cells < layout.w) @max(x1_cells, win_w) else x1_cells;
+ const row_top = @as(f32, @floatFromInt(first)) * layout.h;
+ const row_bottom = @as(f32, @floatFromInt(last + 1)) * layout.h;
+ const text_top = row_top + @as(f32, @floatFromInt(taglineBandOffset(first, win_h, g.cell_h, g.tagline_height)));
+ const text_bottom = @as(f32, @floatFromInt(last)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(last, win_h, g.cell_h, g.tagline_height) + g.tagline_height));
+ const border = @min(if (pane) dp(@floatFromInt(@max(1, chrome.decor_box_border_px))) else dp(1), @floor(text_top - row_top));
+ if (border < 1) return null;
+ // The shadow's full offset to the right; down, what the slack under the
+ // text leaves (a pixel or three at a tagline under the body's size).
+ const want: f32 = if (pane) dp(@floatFromInt(chrome.decor_box_shadow_px)) else dp(2);
+ const drop = std.math.clamp(@floor(row_bottom - text_bottom - border), 0, want);
+ if (x1 - x0 < 4 * border + want) return null;
+ return .{
+ .band = .{ x0, row_top, x1, row_bottom },
+ .box = .{ x0, text_top - border, x1 - want, text_bottom + border },
+ .border = border,
+ .shadow = .{ want, drop },
+ };
+}
+
/// The two short bars on each row of a grip with no mode glyph there,
/// centered in its anchor. Only the panes of group `index`.
fn gripDecor(g: *Gui, gpa: std.mem.Allocator, regions: []const pardes.Region, cells: []const pardes.Cell, groups: *const Groups, index: usize, chrome: *const pardes.Chrome, layout: CellLayout, cols: u16, rows: u16, win_w: f32, win_h: f32, track: ?pardes.animation.Track) !void {
@@ -8903,7 +9225,8 @@ fn uploadOverlayGpu(g: *Gui, cmd: *c.SDL_GPUCommandBuffer, vertex_count: u32) bo
}
fn pushLayerOpacity(cmd: *c.SDL_GPUCommandBuffer, pass: ?*c.SDL_GPURenderPass, opacity: f32) void {
- const uniforms = [4]f32{ opacity, 0, 0, 0 };
+ var uniforms = [4]f32{ opacity, 0, 0, 0 } ++ @as([12]f32, @splat(0));
+ uniforms[4..].* = cell_decor.uniforms;
c.SDL_PushGPUFragmentUniformData(cmd, 0, &uniforms, @sizeOf(@TypeOf(uniforms)));
// The source alpha is geometric/image coverage, not window opacity. Its
// alpha contribution is multiplied by this constant separately, so a