a73x

src/gui/quads.zig

Ref:   Size: 16.9 KiB   History

//! Grid cells to ordered background and foreground instance streams.
const std = @import("std");
const client = @import("client");
const term = @import("term");
const grid = term.grid;
const proto = term.protocol;
const atlas = @import("atlas.zig");
const theme_mod = @import("native_core").theme;
pub const Instance = extern struct {
    x: f32,
    y: f32,
    w: f32,
    h: f32,
    u0: f32,
    v0: f32,
    u1: f32,
    v1: f32,
    rgba: u32,
    kind: u32,
    pub const solid: u32 = 0;
    pub const glyph: u32 = 1;
};
pub const PositionedGlyph = struct { entry: atlas.Entry, x_advance: i32 = 0, y_advance: i32 = 0, x_offset: i32 = 0, y_offset: i32 = 0 };
pub const Glyphs = struct { ctx: *anyopaque, resolve: *const fn (*anyopaque, []const u8, atlas.Variant) anyerror![]const PositionedGlyph };
pub const Lists = struct {
    backgrounds: std.ArrayListUnmanaged(Instance) = .empty,
    foregrounds: std.ArrayListUnmanaged(Instance) = .empty,
    pub fn deinit(s: *Lists, a: std.mem.Allocator) void {
        s.backgrounds.deinit(a);
        s.foregrounds.deinit(a);
    }
    pub fn flatten(s: *const Lists, out: *std.ArrayListUnmanaged(Instance), a: std.mem.Allocator) !void {
        try out.appendSlice(a, s.backgrounds.items);
        try out.appendSlice(a, s.foregrounds.items);
    }
};
pub const Ctx = struct { cell_w: u16, cell_h: u16, ascent: u16, x0: f32 = 0, y0: f32 = 0, atlas_w: f32, atlas_h: f32, glyphs: Glyphs, blink_visible: bool = true, theme: *const theme_mod.Theme = &theme_mod.legacy, fg: ?u32 = null, bg: ?u32 = null, selection: ?client.selection.Span = null };
pub fn rgbaOf(c: u32, d: u32, appearance: *const theme_mod.Theme) u32 {
    return switch (c >> 24) {
        1 => appearance.palette[@intCast(c & 0xff)],
        2 => (c << 8) | 0xff,
        else => d,
    };
}
pub fn solid(x: f32, y: f32, w: f32, h: f32, color: u32) Instance {
    return .{ .x = x, .y = y, .w = w, .h = h, .u0 = 0, .v0 = 0, .u1 = 0, .v1 = 0, .rgba = color, .kind = Instance.solid };
}
pub fn variantOf(flags: u16) atlas.Variant {
    return if (flags & 3 == 3) .bold_italic else if (flags & 1 != 0) .bold else if (flags & 2 != 0) .italic else .regular;
}
fn decoration(out: *std.ArrayListUnmanaged(Instance), a: std.mem.Allocator, x: f32, y: f32, w: f32, h: f32, color: u32, kind: u16) !void {
    if (kind == 0) return;
    const unit: @TypeOf(h) = @max(h / 16, 1);
    switch (kind) {
        1 => try out.append(a, solid(x, y + h - unit * 2, w, unit, color)),
        2 => {
            try out.append(a, solid(x, y + h - unit * 3, w, unit, color));
            try out.append(a, solid(x, y + h - unit, w, unit, color));
        },
        3 => {
            var p: f32 = 0;
            var segment: usize = 0;
            while (p < w) : ({
                p += unit * 2;
                segment += 1;
            }) try out.append(a, solid(x + p, y + h - unit * (1 + @as(f32, @floatFromInt(segment % 2))), @min(unit * 2, w - p), unit, color));
        },
        4 => {
            var p: f32 = 0;
            while (p < w) : (p += unit * 2) try out.append(a, solid(x + p, y + h - unit, @min(unit, w - p), unit, color));
        },
        5 => {
            var p: f32 = 0;
            while (p < w) : (p += unit * 4) try out.append(a, solid(x + p, y + h - unit, @min(unit * 3, w - p), unit, color));
        },
        else => {},
    }
}
pub fn rowInstances(out: *Lists, a: std.mem.Allocator, row: *const grid.Row, cols: u16, col_off: u16, y: u16, ctx: Ctx) !bool {
    const n = @min(@as(usize, cols), row.cells.len);
    const cw: f32 = @floatFromInt(ctx.cell_w);
    const ch: f32 = @floatFromInt(ctx.cell_h);
    const top = ctx.y0 + @as(f32, @floatFromInt(y)) * ch;
    var has_blink = false;
    for (row.cells[0..n], 0..) |cell, x| {
        if (cell.wide == .spacer_tail) continue;
        const span_cols: usize = @min(if (cell.wide == .wide) @as(usize, 2) else 1, n - x);
        const selected = if (ctx.selection) |s| x <= s.to and x + span_cols - 1 >= s.from else false;
        const inv = cell.style.flags & (1 << 4) != 0;
        const bg = if (selected) ctx.theme.chrome_focus_bg else if (inv) rgbaOf(cell.style.fg, ctx.fg orelse ctx.theme.terminal_fg, ctx.theme) else rgbaOf(cell.style.bg, ctx.bg orelse ctx.theme.terminal_bg, ctx.theme);
        if (selected or inv or cell.style.bg != proto.color_none) try out.backgrounds.append(a, solid(ctx.x0 + @as(f32, @floatFromInt(col_off + x)) * cw, top, cw * @as(f32, @floatFromInt(span_cols)), ch, bg));
    }
    for (row.cells[0..n], 0..) |cell, x| {
        if (cell.wide == .spacer_tail) continue;
        const flags = cell.style.flags;
        const decorated = flags & ((1 << 6) | (1 << 7) | (7 << 8)) != 0;
        if (flags & (1 << 3) != 0 and flags & (1 << 5) == 0 and (cell.text_len > 0 or decorated)) has_blink = true;
        if (flags & (1 << 5) != 0 or (flags & (1 << 3) != 0 and !ctx.blink_visible)) continue;
        const inv = flags & (1 << 4) != 0;
        var fg = if (inv) rgbaOf(cell.style.bg, ctx.bg orelse ctx.theme.terminal_bg, ctx.theme) else rgbaOf(cell.style.fg, ctx.fg orelse ctx.theme.terminal_fg, ctx.theme);
        if (flags & (1 << 2) != 0) fg = (fg & 0xffffff00) | 0x80;
        const left = ctx.x0 + @as(f32, @floatFromInt(col_off + x)) * cw;
        const span_cols: usize = @min(if (cell.wide == .wide) @as(usize, 2) else 1, n - x);
        const span = cw * @as(f32, @floatFromInt(span_cols));
        const selected = if (ctx.selection) |s| x <= s.to and x + span_cols - 1 >= s.from else false;
        if (selected) fg = ctx.theme.chrome_focus_fg;
        if (cell.text_len > 0) {
            const run = try ctx.glyphs.resolve(ctx.glyphs.ctx, row.textOf(cell), variantOf(flags));
            var pen_x: i32 = 0;
            var pen_y: i32 = 0;
            for (run) |g| {
                const e = g.entry;
                const gx = left + @as(f32, @floatFromInt(e.left)) + @as(f32, @floatFromInt(pen_x + g.x_offset)) / 64;
                const gy = top + @as(f32, @floatFromInt(@as(i32, ctx.ascent) - e.top)) - @as(f32, @floatFromInt(pen_y + g.y_offset)) / 64;
                const gw: f32 = @floatFromInt(e.w);
                const gh: f32 = @floatFromInt(e.h);
                const x0 = @max(gx, left);
                const y0 = @max(gy, top);
                const x1 = @min(gx + gw, left + span);
                const y1 = @min(gy + gh, top + ch);
                if (x1 > x0 and y1 > y0 and gw > 0 and gh > 0) {
                    const eu0: f32 = @floatFromInt(e.x);
                    const ev0: f32 = @floatFromInt(e.y);
                    try out.foregrounds.append(a, .{ .x = x0, .y = y0, .w = x1 - x0, .h = y1 - y0, .u0 = (eu0 + (x0 - gx)) / ctx.atlas_w, .v0 = (ev0 + (y0 - gy)) / ctx.atlas_h, .u1 = (eu0 + (x1 - gx)) / ctx.atlas_w, .v1 = (ev0 + (y1 - gy)) / ctx.atlas_h, .rgba = fg, .kind = Instance.glyph });
                }
                pen_x += g.x_advance;
                pen_y += g.y_advance;
            }
        }
        if (flags & (1 << 6) != 0) try out.foregrounds.append(a, solid(left, top + ch / 2, span, @max(ch / 16, 1), fg));
        if (flags & (1 << 7) != 0) try out.foregrounds.append(a, solid(left, top, span, @max(ch / 16, 1), fg));
        try decoration(&out.foregrounds, a, left, top, span, ch, rgbaOf(cell.style.ul, fg, ctx.theme), (flags >> 8) & 7);
    }
    return has_blink;
}
pub fn cursorInstance(x: u16, y: u16, ctx: Ctx) Instance {
    return solid(ctx.x0 + @as(f32, @floatFromInt(x)) * @as(f32, @floatFromInt(ctx.cell_w)), ctx.y0 + @as(f32, @floatFromInt(y)) * @as(f32, @floatFromInt(ctx.cell_h)), @floatFromInt(ctx.cell_w), @floatFromInt(ctx.cell_h), ctx.theme.cursor);
}

test "palette and cursor preserve origin" {
    try std.testing.expectEqual(@as(u32, 0xcc0000ff), rgbaOf(proto.colorPalette(1), 0, &theme_mod.legacy));
    const dummy = Ctx{ .cell_w = 8, .cell_h = 16, .ascent = 12, .x0 = 3, .y0 = 4, .atlas_w = 1, .atlas_h = 1, .glyphs = undefined };
    const q = cursorInstance(2, 1, dummy);
    try std.testing.expectEqual(@as(f32, 19), q.x);
    try std.testing.expectEqual(@as(f32, 20), q.y);
    try std.testing.expectEqual(@as(f32, 16), q.h);
    try std.testing.expectEqual(theme_mod.legacy.cursor, q.rgba);
}

test "theme resolves indexed, default, RGB and inverse colours independently" {
    var appearance = theme_mod.trial;
    appearance.palette[1] = 0x010203ff;
    appearance.palette[200] = 0xa0b0c0ff;
    try std.testing.expectEqual(@as(u32, 0x010203ff), rgbaOf(proto.colorPalette(1), appearance.terminal_fg, &appearance));
    try std.testing.expectEqual(@as(u32, 0xa0b0c0ff), rgbaOf(proto.colorPalette(200), appearance.terminal_fg, &appearance));
    try std.testing.expectEqual(appearance.terminal_bg, rgbaOf(proto.color_none, appearance.terminal_bg, &appearance));
    try std.testing.expectEqual(@as(u32, 0x090807ff), rgbaOf(proto.colorRgb(9, 8, 7), appearance.terminal_fg, &appearance));

    var cells = [_]grid.Cell{.{ .text_len = 1, .style = .{ .flags = 1 << 4, .fg = proto.colorRgb(9, 8, 7), .bg = proto.colorPalette(200) } }};
    var row: grid.Row = .{ .cells = &cells, .text = .{ .items = @constCast("x"), .capacity = 1 } };
    var lists: Lists = .{};
    defer lists.deinit(std.testing.allocator);
    var ctx = testCtx(true);
    ctx.theme = &appearance;
    _ = try rowInstances(&lists, std.testing.allocator, &row, 1, 0, 0, ctx);
    try std.testing.expectEqual(@as(u32, 0xa0b0c0ff), lists.foregrounds.items[0].rgba);
    try std.testing.expectEqual(@as(u32, 0x090807ff), lists.backgrounds.items[0].rgba);
}

const Fake = struct {
    var last_variant: atlas.Variant = .regular;
    var run = [_]PositionedGlyph{
        .{ .entry = .{ .x = 0, .y = 0, .w = 4, .h = 8, .left = 1, .top = 7 }, .x_advance = 6 * 64 },
        .{ .entry = .{ .x = 4, .y = 0, .w = 2, .h = 3, .left = -1, .top = 9 }, .x_offset = -2 * 64, .y_offset = 2 * 64 },
    };
    fn resolve(_: *anyopaque, _: []const u8, v: atlas.Variant) anyerror![]const PositionedGlyph {
        last_variant = v;
        return &run;
    }
};

const Oversize = struct {
    var run = [_]PositionedGlyph{.{ .entry = .{ .x = 2, .y = 3, .w = 20, .h = 24, .left = -4, .top = 20 } }};
    fn resolve(_: *anyopaque, _: []const u8, _: atlas.Variant) anyerror![]const PositionedGlyph {
        return &run;
    }
};

fn testCtx(blink_visible: bool) Ctx {
    return .{ .cell_w = 8, .cell_h = 16, .ascent = 12, .x0 = 5, .atlas_w = 16, .atlas_h = 16, .blink_visible = blink_visible, .glyphs = .{ .ctx = undefined, .resolve = Fake.resolve } };
}

test "full cluster, wide origin, styles, underline variants and blink" {
    var cells = [_]grid.Cell{ .{ .wide = .wide, .text_len = 3 }, .{ .wide = .spacer_tail } };
    var row: grid.Row = .{ .cells = &cells, .text = .{ .items = @constCast("e\xcc\x81"), .capacity = 3 } };
    const a = std.testing.allocator;
    for (1..6) |underline| {
        cells[0].style.flags = @as(u16, @intCast(underline)) << 8 | 1 | 2 | (1 << 6) | (1 << 7);
        cells[0].style.ul = proto.colorRgb(1, 2, 3);
        var lists: Lists = .{};
        defer lists.deinit(a);
        _ = try rowInstances(&lists, a, &row, 2, 3, 0, testCtx(true));
        try std.testing.expect(lists.foregrounds.items.len >= 5);
        try std.testing.expectEqual(@as(f32, 29 + 1), lists.foregrounds.items[0].x);
        try std.testing.expectEqual(@as(f32, 32), lists.foregrounds.items[1].x);
        try std.testing.expectEqual(atlas.Variant.bold_italic, Fake.last_variant);
        try std.testing.expectEqual(@as(u32, 0x010203ff), lists.foregrounds.items[lists.foregrounds.items.len - 1].rgba);
        if (underline == 3) {
            const tail = lists.foregrounds.items[lists.foregrounds.items.len - 4 ..];
            try std.testing.expect(tail[0].y != tail[1].y);
        }
    }
    cells[0].style.flags = (1 << 5) | (1 << 6) | (1 << 8);
    var hidden: Lists = .{};
    defer hidden.deinit(a);
    _ = try rowInstances(&hidden, a, &row, 2, 0, 0, testCtx(true));
    try std.testing.expectEqual(@as(usize, 0), hidden.foregrounds.items.len);
    cells[0].style.flags = 1 << 3;
    var blink: Lists = .{};
    defer blink.deinit(a);
    try std.testing.expect(try rowInstances(&blink, a, &row, 2, 0, 0, testCtx(false)));
    try std.testing.expectEqual(@as(usize, 0), blink.foregrounds.items.len);
}

test "each non-underline style flag has a visible deterministic effect" {
    var cells = [_]grid.Cell{.{ .text_len = 1 }};
    var row: grid.Row = .{ .cells = &cells, .text = .{ .items = @constCast("x"), .capacity = 1 } };
    const a = std.testing.allocator;
    for ([_]u16{ 1 << 0, 1 << 1, 1 << 2, 1 << 4, 1 << 6, 1 << 7 }) |flag| {
        cells[0].style = .{ .flags = flag, .fg = proto.colorRgb(100, 80, 60), .bg = proto.colorRgb(5, 6, 7) };
        var lists: Lists = .{};
        defer lists.deinit(a);
        _ = try rowInstances(&lists, a, &row, 1, 0, 0, testCtx(true));
        try std.testing.expect(lists.foregrounds.items.len >= 2);
        if (flag == 1 << 0) try std.testing.expectEqual(atlas.Variant.bold, Fake.last_variant);
        if (flag == 1 << 1) try std.testing.expectEqual(atlas.Variant.italic, Fake.last_variant);
        if (flag == 1 << 2) try std.testing.expectEqual(@as(u8, 0x80), @as(u8, @truncate(lists.foregrounds.items[0].rgba)));
        if (flag == 1 << 4) try std.testing.expectEqual(@as(u32, 0x050607ff), lists.foregrounds.items[0].rgba);
    }
}

test "glyphs and a clipped wide edge stay inside the authoritative span" {
    var cells = [_]grid.Cell{.{ .wide = .wide, .text_len = 1, .style = .{ .bg = proto.colorRgb(1, 1, 1) } }};
    var row: grid.Row = .{ .cells = &cells, .text = .{ .items = @constCast("x"), .capacity = 1 } };
    var lists: Lists = .{};
    defer lists.deinit(std.testing.allocator);
    var ctx = testCtx(true);
    ctx.glyphs.resolve = Oversize.resolve;
    _ = try rowInstances(&lists, std.testing.allocator, &row, 1, 2, 0, ctx);
    try std.testing.expectEqual(@as(f32, 8), lists.backgrounds.items[0].w);
    const glyph = lists.foregrounds.items[0];
    try std.testing.expect(glyph.x >= 21 and glyph.x + glyph.w <= 29);
    try std.testing.expect(glyph.y >= 0 and glyph.y + glyph.h <= 16);
    try std.testing.expect(glyph.u0 > @as(f32, 2) / ctx.atlas_w);
    try std.testing.expect(glyph.u1 < @as(f32, 22) / ctx.atlas_w);
}

test "selection highlight covers wide cells without changing grid styles" {
    var cells = [_]grid.Cell{
        .{ .wide = .wide, .text_len = 1, .style = .{ .fg = proto.colorRgb(1, 2, 3), .bg = proto.colorRgb(4, 5, 6) } },
        .{ .wide = .spacer_tail },
    };
    var row: grid.Row = .{ .cells = &cells, .text = .{ .items = @constCast("界"), .capacity = 3 } };
    var lists: Lists = .{};
    defer lists.deinit(std.testing.allocator);
    var ctx = testCtx(true);
    ctx.selection = .{ .from = 1, .to = 1 };
    _ = try rowInstances(&lists, std.testing.allocator, &row, 2, 0, 0, ctx);
    // Selecting the continuation still paints the owning wide cell as one
    // complete two-column rectangle, while the source style remains intact.
    try std.testing.expectEqual(@as(f32, 16), lists.backgrounds.items[0].w);
    try std.testing.expectEqual(theme_mod.legacy.chrome_focus_bg, lists.backgrounds.items[0].rgba);
    try std.testing.expectEqual(proto.colorRgb(1, 2, 3), cells[0].style.fg);
}

/// Clip only the newly emitted range, adjusting glyph UVs proportionally.
/// Removed instances are compacted without changing background/foreground order.
pub fn clip(list: *std.ArrayListUnmanaged(Instance), start: usize, x: f32, y: f32, w: f32, h: f32) void {
    var out = start;
    for (list.items[start..]) |old| {
        const left = @max(x, old.x);
        const top = @max(y, old.y);
        const right = @min(x + w, old.x + old.w);
        const bottom = @min(y + h, old.y + old.h);
        if (right <= left or bottom <= top) continue;
        var v = old;
        v.x = left;
        v.y = top;
        v.w = right - left;
        v.h = bottom - top;
        v.u0 = old.u0 + (old.u1 - old.u0) * (left - old.x) / old.w;
        v.u1 = old.u0 + (old.u1 - old.u0) * (right - old.x) / old.w;
        v.v0 = old.v0 + (old.v1 - old.v0) * (top - old.y) / old.h;
        v.v1 = old.v0 + (old.v1 - old.v0) * (bottom - old.y) / old.h;
        list.items[out] = v;
        out += 1;
    }
    list.items.len = out;
}

test "pane clipping preserves adjacent stream and crops glyph UVs on both axes" {
    const a = std.testing.allocator;
    var list: std.ArrayListUnmanaged(Instance) = .empty;
    defer list.deinit(a);
    try list.append(a, solid(0, 0, 5, 5, 0));
    var glyph = solid(10, 10, 20, 20, 0);
    glyph.kind = Instance.glyph;
    glyph.u1 = 1;
    glyph.v1 = 1;
    try list.append(a, glyph);
    try list.append(a, solid(100, 100, 5, 5, 0));
    clip(&list, 1, 15, 20, 10, 5);
    try std.testing.expectEqual(@as(usize, 2), list.items.len);
    try std.testing.expectEqual(@as(f32, 0), list.items[0].x);
    const v = list.items[1];
    try std.testing.expectEqual(@as(f32, 15), v.x);
    try std.testing.expectEqual(@as(f32, 5), v.h);
    try std.testing.expectEqual(@as(f32, 0.25), v.u0);
    try std.testing.expectEqual(@as(f32, 0.75), v.u1);
    try std.testing.expectEqual(@as(f32, 0.5), v.v0);
    try std.testing.expectEqual(@as(f32, 0.75), v.v1);
}