refactor(gui): move RGBA to color module

This commit is contained in:
CJ van den Berg 2026-04-10 12:17:47 +02:00
parent 76ed87f87b
commit 347ce61f5d
Signed by: neurocyte
GPG key ID: 8EB1E1BB660E3FB9
8 changed files with 109 additions and 67 deletions

View file

@ -492,7 +492,12 @@ pub fn build_exe(
const win32_dep = b.lazyDependency("win32", .{}) orelse break :blk tui_renderer_mod; const win32_dep = b.lazyDependency("win32", .{}) orelse break :blk tui_renderer_mod;
const win32_mod = win32_dep.module("win32"); const win32_mod = win32_dep.module("win32");
const gui_xy_mod = b.createModule(.{ .root_source_file = b.path("src/gui/xy.zig") }); const gui_xy_mod = b.createModule(.{ .root_source_file = b.path("src/gui/xy.zig") });
const gui_cell_mod = b.createModule(.{ .root_source_file = b.path("src/gui/Cell.zig") }); const gui_cell_mod = b.createModule(.{
.root_source_file = b.path("src/gui/cell.zig"),
.imports = &.{
.{ .name = "color", .module = color_mod },
},
});
const gui_glyph_cache_mod = b.createModule(.{ .root_source_file = b.path("src/gui/GlyphIndexCache.zig") }); const gui_glyph_cache_mod = b.createModule(.{ .root_source_file = b.path("src/gui/GlyphIndexCache.zig") });
const gui_xterm_mod = b.createModule(.{ .root_source_file = b.path("src/gui/xterm.zig") }); const gui_xterm_mod = b.createModule(.{ .root_source_file = b.path("src/gui/xterm.zig") });
const gui_mod = b.createModule(.{ const gui_mod = b.createModule(.{
@ -509,7 +514,7 @@ pub fn build_exe(
.{ .name = "gui_config", .module = gui_config_mod }, .{ .name = "gui_config", .module = gui_config_mod },
.{ .name = "tracy", .module = tracy_mod }, .{ .name = "tracy", .module = tracy_mod },
.{ .name = "xy", .module = gui_xy_mod }, .{ .name = "xy", .module = gui_xy_mod },
.{ .name = "Cell", .module = gui_cell_mod }, .{ .name = "cell", .module = gui_cell_mod },
.{ .name = "GlyphIndexCache", .module = gui_glyph_cache_mod }, .{ .name = "GlyphIndexCache", .module = gui_glyph_cache_mod },
.{ .name = "xterm", .module = gui_xterm_mod }, .{ .name = "xterm", .module = gui_xterm_mod },
}, },
@ -550,7 +555,12 @@ pub fn build_exe(
const sokol_mod = sokol_dep.module("sokol"); const sokol_mod = sokol_dep.module("sokol");
const gui_xy_mod = b.createModule(.{ .root_source_file = b.path("src/gui/xy.zig") }); const gui_xy_mod = b.createModule(.{ .root_source_file = b.path("src/gui/xy.zig") });
const gui_cell_mod = b.createModule(.{ .root_source_file = b.path("src/gui/Cell.zig") }); const gui_cell_mod = b.createModule(.{
.root_source_file = b.path("src/gui/cell.zig"),
.imports = &.{
.{ .name = "color", .module = color_mod },
},
});
const gui_glyph_cache_mod = b.createModule(.{ .root_source_file = b.path("src/gui/GlyphIndexCache.zig") }); const gui_glyph_cache_mod = b.createModule(.{ .root_source_file = b.path("src/gui/GlyphIndexCache.zig") });
const gui_xterm_mod = b.createModule(.{ .root_source_file = b.path("src/gui/xterm.zig") }); const gui_xterm_mod = b.createModule(.{ .root_source_file = b.path("src/gui/xterm.zig") });
@ -609,10 +619,11 @@ pub fn build_exe(
const gpu_mod = b.createModule(.{ const gpu_mod = b.createModule(.{
.root_source_file = b.path("src/gui/gpu/gpu.zig"), .root_source_file = b.path("src/gui/gpu/gpu.zig"),
.imports = &.{ .imports = &.{
.{ .name = "color", .module = color_mod },
.{ .name = "sokol", .module = sokol_mod }, .{ .name = "sokol", .module = sokol_mod },
.{ .name = "rasterizer", .module = combined_rasterizer_mod }, .{ .name = "rasterizer", .module = combined_rasterizer_mod },
.{ .name = "xy", .module = gui_xy_mod }, .{ .name = "xy", .module = gui_xy_mod },
.{ .name = "Cell", .module = gui_cell_mod }, .{ .name = "cell", .module = gui_cell_mod },
.{ .name = "GlyphIndexCache", .module = gui_glyph_cache_mod }, .{ .name = "GlyphIndexCache", .module = gui_glyph_cache_mod },
}, },
}); });
@ -620,6 +631,7 @@ pub fn build_exe(
const app_mod = b.createModule(.{ const app_mod = b.createModule(.{
.root_source_file = b.path("src/gui/wio/app.zig"), .root_source_file = b.path("src/gui/wio/app.zig"),
.imports = &.{ .imports = &.{
.{ .name = "color", .module = color_mod },
.{ .name = "wio", .module = wio_mod }, .{ .name = "wio", .module = wio_mod },
.{ .name = "sokol", .module = sokol_mod }, .{ .name = "sokol", .module = sokol_mod },
.{ .name = "gpu", .module = gpu_mod }, .{ .name = "gpu", .module = gpu_mod },
@ -635,6 +647,7 @@ pub fn build_exe(
const mod = b.createModule(.{ const mod = b.createModule(.{
.root_source_file = b.path("src/renderer/gui/renderer.zig"), .root_source_file = b.path("src/renderer/gui/renderer.zig"),
.imports = &.{ .imports = &.{
.{ .name = "color", .module = color_mod },
.{ .name = "theme", .module = themes_dep.module("theme") }, .{ .name = "theme", .module = themes_dep.module("theme") },
.{ .name = "cbor", .module = cbor_mod }, .{ .name = "cbor", .module = cbor_mod },
.{ .name = "thespian", .module = thespian_mod }, .{ .name = "thespian", .module = thespian_mod },

View file

@ -106,6 +106,47 @@ pub const RGBf = struct {
const GAMMA = 2.4; const GAMMA = 2.4;
}; };
// Packed RGBA color type used by GPUs
pub const RGBA = packed struct(u32) {
a: u8,
b: u8,
g: u8,
r: u8,
pub fn init(r: u8, g: u8, b: u8, a: u8) RGBA {
return .{ .r = r, .g = g, .b = b, .a = a };
}
pub fn from_RGB(v: RGB) RGBA {
return .{ .r = v.r, .g = v.g, .b = v.b, .a = 255 };
}
pub inline fn from_u32(v: u32) RGBA {
return @bitCast(v);
}
pub inline fn from_u24(v: u24) RGBA {
return from_RGB(RGB.from_u24(v));
}
pub inline fn from_u8s(v: [3]u8) RGBA {
return .{ .r = v[0], .g = v[1], .b = v[2], .a = 255 };
}
pub inline fn to_u32(v: RGBA) u32 {
return @bitCast(v);
}
pub fn to_vec4(c: RGBA) [4]f32 {
return .{
@as(f32, @floatFromInt(c.r)) / 255.0,
@as(f32, @floatFromInt(c.g)) / 255.0,
@as(f32, @floatFromInt(c.b)) / 255.0,
@as(f32, @floatFromInt(c.a)) / 255.0,
};
}
};
pub fn u24_to_u8s(v: u24) [3]u8 { pub fn u24_to_u8s(v: u24) [3]u8 {
return .{ @truncate(v >> 16), @truncate(v >> 8), @truncate(v) }; return .{ @truncate(v >> 16), @truncate(v >> 8), @truncate(v) };
} }

View file

@ -1,20 +0,0 @@
// Shared GPU cell types used by all GPU renderer backends.
pub const Rgba8 = packed struct(u32) {
a: u8,
b: u8,
g: u8,
r: u8,
pub fn initRgb(r: u8, g: u8, b: u8) Rgba8 {
return .{ .r = r, .g = g, .b = b, .a = 255 };
}
pub fn initRgba(r: u8, g: u8, b: u8, a: u8) Rgba8 {
return .{ .r = r, .g = g, .b = b, .a = a };
}
};
pub const Cell = extern struct {
glyph_index: u32,
background: Rgba8,
foreground: Rgba8,
};

9
src/gui/cell.zig Normal file
View file

@ -0,0 +1,9 @@
// Shared GPU cell types used by all GPU renderer backends.
const RGBA = @import("color").RGBA;
pub const Cell = extern struct {
glyph_index: u32,
background: RGBA,
foreground: RGBA,
};

View file

@ -7,16 +7,14 @@ const std = @import("std");
const sg = @import("sokol").gfx; const sg = @import("sokol").gfx;
const Rasterizer = @import("rasterizer"); const Rasterizer = @import("rasterizer");
const GlyphIndexCache = @import("GlyphIndexCache"); const GlyphIndexCache = @import("GlyphIndexCache");
const gui_cell = @import("Cell");
const XY = @import("xy").XY; const XY = @import("xy").XY;
const builtin_shader = @import("builtin.glsl.zig"); const builtin_shader = @import("builtin.glsl.zig");
pub const Font = Rasterizer.Font; pub const Font = Rasterizer.Font;
pub const GlyphKind = Rasterizer.GlyphKind; pub const GlyphKind = Rasterizer.GlyphKind;
pub const RasterizerBackend = Rasterizer.Backend; pub const RasterizerBackend = Rasterizer.Backend;
pub const Cell = gui_cell.Cell; pub const Cell = @import("cell").Cell;
pub const Color = gui_cell.Rgba8; pub const RGBA = @import("color").RGBA;
const Rgba8 = gui_cell.Rgba8;
pub const CursorShape = enum(i32) { block = 0, beam = 1, underline = 2 }; pub const CursorShape = enum(i32) { block = 0, beam = 1, underline = 2 };
@ -25,7 +23,7 @@ pub const CursorInfo = struct {
row: u16 = 0, row: u16 = 0,
col: u16 = 0, col: u16 = 0,
shape: CursorShape = .block, shape: CursorShape = .block,
color: Color = Color.initRgb(255, 255, 255), color: RGBA = .init(255, 255, 255, 255),
}; };
const log = std.log.scoped(.gpu); const log = std.log.scoped(.gpu);
@ -44,7 +42,7 @@ fn getAtlasCellCount(cell_size: XY(u16)) XY(u16) {
// Shader cell layout for the RGBA32UI cell texture. // Shader cell layout for the RGBA32UI cell texture.
// Each texel encodes one terminal cell: // Each texel encodes one terminal cell:
// .r = glyph_index (u32) // .r = glyph_index (u32)
// .g = bg packed (Rgba8 bit-cast to u32: r<<24|g<<16|b<<8|a) // .g = bg packed (RGBA bit-cast to u32: r<<24|g<<16|b<<8|a)
// .b = fg packed (same) // .b = fg packed (same)
// .a = 0 (reserved) // .a = 0 (reserved)
const ShaderCell = extern struct { const ShaderCell = extern struct {
@ -61,7 +59,7 @@ const global = struct {
var glyph_sampler: sg.Sampler = .{}; var glyph_sampler: sg.Sampler = .{};
var cell_sampler: sg.Sampler = .{}; var cell_sampler: sg.Sampler = .{};
var glyph_cache_arena: std.heap.ArenaAllocator = undefined; var glyph_cache_arena: std.heap.ArenaAllocator = undefined;
var background: Rgba8 = .{ .r = 19, .g = 19, .b = 19, .a = 255 }; var background: RGBA = .init(0, 255, 255, 255); // default is warning yellow
}; };
pub fn init(allocator: std.mem.Allocator) !void { pub fn init(allocator: std.mem.Allocator) !void {
@ -118,7 +116,7 @@ pub fn setFontWeight(font: *Font, w: u8) void {
Rasterizer.setFontWeight(font, w); Rasterizer.setFontWeight(font, w);
} }
pub fn setBackground(color: Rgba8) void { pub fn setBackground(color: RGBA) void {
global.background = color; global.background = color;
} }
@ -387,15 +385,15 @@ pub fn paint(
const src = cells[src_row_offset + ci]; const src = cells[src_row_offset + ci];
shader_cells[dst_row_offset + ci] = .{ shader_cells[dst_row_offset + ci] = .{
.glyph_index = src.glyph_index, .glyph_index = src.glyph_index,
.bg = @bitCast(src.background), .bg = src.background.to_u32(),
.fg = @bitCast(src.foreground), .fg = src.foreground.to_u32(),
}; };
} }
for (copy_len..shader_col_count) |ci| { for (copy_len..shader_col_count) |ci| {
shader_cells[dst_row_offset + ci] = .{ shader_cells[dst_row_offset + ci] = .{
.glyph_index = blank_glyph_index, .glyph_index = blank_glyph_index,
.bg = @bitCast(global.background), .bg = global.background.to_u32(),
.fg = @bitCast(global.background), .fg = global.background.to_u32(),
}; };
} }
} }
@ -448,10 +446,10 @@ pub fn paint(
bindings.samplers[1] = global.cell_sampler; bindings.samplers[1] = global.cell_sampler;
sg.applyBindings(bindings); sg.applyBindings(bindings);
const sec_color: Color = if (secondary_cursors.len > 0) const sec_color: RGBA = if (secondary_cursors.len > 0)
secondary_cursors[0].color secondary_cursors[0].color
else else
Color.initRgb(255, 255, 255); .init(255, 255, 255, 255);
const fs_params = builtin_shader.FsParams{ const fs_params = builtin_shader.FsParams{
.cell_size_x = font.cell_size.x, .cell_size_x = font.cell_size.x,
@ -463,8 +461,8 @@ pub fn paint(
.cursor_row = cursor.row, .cursor_row = cursor.row,
.cursor_shape = @intFromEnum(cursor.shape), .cursor_shape = @intFromEnum(cursor.shape),
.cursor_vis = if (cursor.vis) 1 else 0, .cursor_vis = if (cursor.vis) 1 else 0,
.cursor_color = colorToVec4(cursor.color), .cursor_color = cursor.color.to_vec4(),
.sec_cursor_color = colorToVec4(sec_color), .sec_cursor_color = sec_color.to_vec4(),
}; };
sg.applyUniforms(0, .{ sg.applyUniforms(0, .{
.ptr = &fs_params, .ptr = &fs_params,
@ -476,15 +474,6 @@ pub fn paint(
// Note: caller (app.zig) calls sg.commit() and window.swapBuffers() // Note: caller (app.zig) calls sg.commit() and window.swapBuffers()
} }
fn colorToVec4(c: Color) [4]f32 {
return .{
@as(f32, @floatFromInt(c.r)) / 255.0,
@as(f32, @floatFromInt(c.g)) / 255.0,
@as(f32, @floatFromInt(c.b)) / 255.0,
@as(f32, @floatFromInt(c.a)) / 255.0,
};
}
fn oom(e: error{OutOfMemory}) noreturn { fn oom(e: error{OutOfMemory}) noreturn {
@panic(@errorName(e)); @panic(@errorName(e));
} }

View file

@ -15,6 +15,7 @@ const gpu = @import("gpu");
const thespian = @import("thespian"); const thespian = @import("thespian");
const cbor = @import("cbor"); const cbor = @import("cbor");
const vaxis = @import("vaxis"); const vaxis = @import("vaxis");
const RGBA = @import("color").RGBA;
const input_translate = @import("input.zig"); const input_translate = @import("input.zig");
const root = @import("soft_root").root; const root = @import("soft_root").root;
@ -26,7 +27,6 @@ const log = std.log.scoped(.wio_app);
// can use them without a direct dependency on the gpu module. // can use them without a direct dependency on the gpu module.
pub const CursorInfo = gpu.CursorInfo; pub const CursorInfo = gpu.CursorInfo;
pub const CursorShape = gpu.CursorShape; pub const CursorShape = gpu.CursorShape;
pub const GpuColor = gpu.Color;
// Shared state (protected by screen_mutex) // Shared state (protected by screen_mutex)
@ -57,7 +57,7 @@ var stop_requested: std.atomic.Value(bool) = .init(false);
// Background color (written from TUI thread, applied by wio thread on each paint). // Background color (written from TUI thread, applied by wio thread on each paint).
// Stored as packed RGBA u32 to allow atomic reads/writes. // Stored as packed RGBA u32 to allow atomic reads/writes.
var background_color: std.atomic.Value(u32) = .init(0x131313ff); // matches gpu.zig default var background_color: std.atomic.Value(u32) = .init(RGBA.init(0, 255, 255, 255).to_u32()); // warning yellow, we should never see the default
var background_dirty: std.atomic.Value(bool) = .init(false); var background_dirty: std.atomic.Value(bool) = .init(false);
var config_arena_instance: std.heap.ArenaAllocator = std.heap.ArenaAllocator.init(std.heap.page_allocator); var config_arena_instance: std.heap.ArenaAllocator = std.heap.ArenaAllocator.init(std.heap.page_allocator);
@ -316,7 +316,7 @@ fn saveConfig() void {
log.err("failed to write gui config file", .{}); log.err("failed to write gui config file", .{});
} }
pub fn setBackground(color: gpu.Color) void { pub fn setBackground(color: RGBA) void {
const color_u32: u32 = (@as(u32, color.r) << 24) | (@as(u32, color.g) << 16) | (@as(u32, color.b) << 8) | color.a; const color_u32: u32 = (@as(u32, color.r) << 24) | (@as(u32, color.g) << 16) | (@as(u32, color.b) << 8) | color.a;
background_color.store(color_u32, .release); background_color.store(color_u32, .release);
background_dirty.store(true, .release); background_dirty.store(true, .release);
@ -371,19 +371,11 @@ fn maybeReloadFont(win_size: wio.Size, state: *gpu.WindowState, cell_width: *u16
} }
} }
fn colorFromVaxis(color: vaxis.Cell.Color) gpu.Color { fn colorFromVaxis(color: vaxis.Cell.Color) RGBA {
return switch (color) { return switch (color) {
.default => gpu.Color.initRgb(0, 0, 0), .default => .init(0, 0, 0, 255),
.index => |idx| blk: { .index => |idx| .from_u24(@import("xterm").colors[idx]),
const xterm = @import("xterm"); .rgb => |rgb| .from_u8s(rgb),
const rgb24 = xterm.colors[idx];
break :blk gpu.Color.initRgb(
@truncate(rgb24 >> 16),
@truncate(rgb24 >> 8),
@truncate(rgb24),
);
},
.rgb => |rgb| gpu.Color.initRgb(rgb[0], rgb[1], rgb[2]),
}; };
} }

View file

@ -3,6 +3,7 @@ pub const log_name = "renderer";
const std = @import("std"); const std = @import("std");
const cbor = @import("cbor"); const cbor = @import("cbor");
const RGBA = @import("color").RGBA;
pub const vaxis = @import("vaxis"); pub const vaxis = @import("vaxis");
const Style = @import("theme").Style; const Style = @import("theme").Style;
const Color = @import("theme").Color; const Color = @import("theme").Color;
@ -57,9 +58,9 @@ thread: ?std.Thread = null,
window_ready: bool = false, window_ready: bool = false,
cursor_info: app.CursorInfo = .{}, cursor_info: app.CursorInfo = .{},
cursor_color: app.GpuColor = app.GpuColor.initRgb(255, 255, 255), cursor_color: RGBA = .init(255, 255, 255, 255),
secondary_cursors: std.ArrayListUnmanaged(app.CursorInfo) = .{}, secondary_cursors: std.ArrayListUnmanaged(app.CursorInfo) = .{},
secondary_color: app.GpuColor = app.GpuColor.initRgb(255, 255, 255), secondary_color: RGBA = .init(255, 255, 255, 255),
cursor_blink: bool = false, cursor_blink: bool = false,
blink_on: bool = true, blink_on: bool = true,
@ -545,7 +546,7 @@ pub fn show_multi_cursor_yx(self: *Self, y: i32, x: i32) !void {
}); });
} }
fn themeColorToGpu(color: Color) app.GpuColor { fn themeColorToGpu(color: Color) RGBA {
return .{ return .{
.r = @truncate(color.color >> 16), .r = @truncate(color.color >> 16),
.g = @truncate(color.color >> 8), .g = @truncate(color.color >> 8),

View file

@ -2,6 +2,7 @@ const std = @import("std");
const color = @import("color"); const color = @import("color");
const RGB = color.RGB; const RGB = color.RGB;
const RGBA = color.RGBA;
const rgb = RGB.from_u24; const rgb = RGB.from_u24;
test "contrast white/yellow" { test "contrast white/yellow" {
@ -41,3 +42,19 @@ test "best contrast black/white to blue" {
const best = color.max_contrast(0x0000FF, 0xFFFFFF, 0x000000); const best = color.max_contrast(0x0000FF, 0xFFFFFF, 0x000000);
try std.testing.expectEqual(best, 0xFFFFFF); try std.testing.expectEqual(best, 0xFFFFFF);
} }
test "verify RGBA byte order" {
const v1: RGBA = .init(0xA, 0xB, 0xC, 0xD);
const v1_u32: u32 = @bitCast(v1);
const v2: RGBA = .{
.r = @truncate(v1_u32 >> 24),
.g = @truncate(v1_u32 >> 16),
.b = @truncate(v1_u32 >> 8),
.a = @truncate(v1_u32),
};
const v3: RGBA = @bitCast(v1_u32);
const testing = @import("std").testing;
try testing.expectEqual(v1, v2);
try testing.expectEqual(v1, v3);
}