diff options
Diffstat (limited to 'src/gui')
| -rw-r--r-- | src/gui/gui.zig | 335 |
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 |
