diff --git a/build.zig b/build.zig index b2c46749..44cc96ef 100644 --- a/build.zig +++ b/build.zig @@ -492,7 +492,12 @@ pub fn build_exe( const win32_dep = b.lazyDependency("win32", .{}) orelse break :blk tui_renderer_mod; const win32_mod = win32_dep.module("win32"); 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_xterm_mod = b.createModule(.{ .root_source_file = b.path("src/gui/xterm.zig") }); const gui_mod = b.createModule(.{ @@ -509,7 +514,7 @@ pub fn build_exe( .{ .name = "gui_config", .module = gui_config_mod }, .{ .name = "tracy", .module = tracy_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 = "xterm", .module = gui_xterm_mod }, }, @@ -550,7 +555,12 @@ pub fn build_exe( const sokol_mod = sokol_dep.module("sokol"); 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_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(.{ .root_source_file = b.path("src/gui/gpu/gpu.zig"), .imports = &.{ + .{ .name = "color", .module = color_mod }, .{ .name = "sokol", .module = sokol_mod }, .{ .name = "rasterizer", .module = combined_rasterizer_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 }, }, }); @@ -620,6 +631,7 @@ pub fn build_exe( const app_mod = b.createModule(.{ .root_source_file = b.path("src/gui/wio/app.zig"), .imports = &.{ + .{ .name = "color", .module = color_mod }, .{ .name = "wio", .module = wio_mod }, .{ .name = "sokol", .module = sokol_mod }, .{ .name = "gpu", .module = gpu_mod }, @@ -635,6 +647,7 @@ pub fn build_exe( const mod = b.createModule(.{ .root_source_file = b.path("src/renderer/gui/renderer.zig"), .imports = &.{ + .{ .name = "color", .module = color_mod }, .{ .name = "theme", .module = themes_dep.module("theme") }, .{ .name = "cbor", .module = cbor_mod }, .{ .name = "thespian", .module = thespian_mod }, diff --git a/src/color.zig b/src/color.zig index c97c300e..e978fe96 100644 --- a/src/color.zig +++ b/src/color.zig @@ -106,6 +106,47 @@ pub const RGBf = struct { 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 { return .{ @truncate(v >> 16), @truncate(v >> 8), @truncate(v) }; } diff --git a/src/gui/Cell.zig b/src/gui/Cell.zig deleted file mode 100644 index 242d534a..00000000 --- a/src/gui/Cell.zig +++ /dev/null @@ -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, -}; diff --git a/src/gui/cell.zig b/src/gui/cell.zig new file mode 100644 index 00000000..39e8b502 --- /dev/null +++ b/src/gui/cell.zig @@ -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, +}; diff --git a/src/gui/gpu/gpu.zig b/src/gui/gpu/gpu.zig index 7c75b181..fb23273e 100644 --- a/src/gui/gpu/gpu.zig +++ b/src/gui/gpu/gpu.zig @@ -7,16 +7,14 @@ const std = @import("std"); const sg = @import("sokol").gfx; const Rasterizer = @import("rasterizer"); const GlyphIndexCache = @import("GlyphIndexCache"); -const gui_cell = @import("Cell"); const XY = @import("xy").XY; const builtin_shader = @import("builtin.glsl.zig"); pub const Font = Rasterizer.Font; pub const GlyphKind = Rasterizer.GlyphKind; pub const RasterizerBackend = Rasterizer.Backend; -pub const Cell = gui_cell.Cell; -pub const Color = gui_cell.Rgba8; -const Rgba8 = gui_cell.Rgba8; +pub const Cell = @import("cell").Cell; +pub const RGBA = @import("color").RGBA; pub const CursorShape = enum(i32) { block = 0, beam = 1, underline = 2 }; @@ -25,7 +23,7 @@ pub const CursorInfo = struct { row: u16 = 0, col: u16 = 0, shape: CursorShape = .block, - color: Color = Color.initRgb(255, 255, 255), + color: RGBA = .init(255, 255, 255, 255), }; 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. // Each texel encodes one terminal cell: // .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) // .a = 0 (reserved) const ShaderCell = extern struct { @@ -61,7 +59,7 @@ const global = struct { var glyph_sampler: sg.Sampler = .{}; var cell_sampler: sg.Sampler = .{}; 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 { @@ -118,7 +116,7 @@ pub fn setFontWeight(font: *Font, w: u8) void { Rasterizer.setFontWeight(font, w); } -pub fn setBackground(color: Rgba8) void { +pub fn setBackground(color: RGBA) void { global.background = color; } @@ -387,15 +385,15 @@ pub fn paint( const src = cells[src_row_offset + ci]; shader_cells[dst_row_offset + ci] = .{ .glyph_index = src.glyph_index, - .bg = @bitCast(src.background), - .fg = @bitCast(src.foreground), + .bg = src.background.to_u32(), + .fg = src.foreground.to_u32(), }; } for (copy_len..shader_col_count) |ci| { shader_cells[dst_row_offset + ci] = .{ .glyph_index = blank_glyph_index, - .bg = @bitCast(global.background), - .fg = @bitCast(global.background), + .bg = global.background.to_u32(), + .fg = global.background.to_u32(), }; } } @@ -448,10 +446,10 @@ pub fn paint( bindings.samplers[1] = global.cell_sampler; 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 else - Color.initRgb(255, 255, 255); + .init(255, 255, 255, 255); const fs_params = builtin_shader.FsParams{ .cell_size_x = font.cell_size.x, @@ -463,8 +461,8 @@ pub fn paint( .cursor_row = cursor.row, .cursor_shape = @intFromEnum(cursor.shape), .cursor_vis = if (cursor.vis) 1 else 0, - .cursor_color = colorToVec4(cursor.color), - .sec_cursor_color = colorToVec4(sec_color), + .cursor_color = cursor.color.to_vec4(), + .sec_cursor_color = sec_color.to_vec4(), }; sg.applyUniforms(0, .{ .ptr = &fs_params, @@ -476,15 +474,6 @@ pub fn paint( // 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 { @panic(@errorName(e)); } diff --git a/src/gui/wio/app.zig b/src/gui/wio/app.zig index cc08edff..0cdd97c8 100644 --- a/src/gui/wio/app.zig +++ b/src/gui/wio/app.zig @@ -15,6 +15,7 @@ const gpu = @import("gpu"); const thespian = @import("thespian"); const cbor = @import("cbor"); const vaxis = @import("vaxis"); +const RGBA = @import("color").RGBA; const input_translate = @import("input.zig"); 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. pub const CursorInfo = gpu.CursorInfo; pub const CursorShape = gpu.CursorShape; -pub const GpuColor = gpu.Color; // ── 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). // 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 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", .{}); } -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; background_color.store(color_u32, .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) { - .default => gpu.Color.initRgb(0, 0, 0), - .index => |idx| blk: { - const xterm = @import("xterm"); - 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]), + .default => .init(0, 0, 0, 255), + .index => |idx| .from_u24(@import("xterm").colors[idx]), + .rgb => |rgb| .from_u8s(rgb), }; } diff --git a/src/renderer/gui/renderer.zig b/src/renderer/gui/renderer.zig index fffce5db..4bbd1aff 100644 --- a/src/renderer/gui/renderer.zig +++ b/src/renderer/gui/renderer.zig @@ -3,6 +3,7 @@ pub const log_name = "renderer"; const std = @import("std"); const cbor = @import("cbor"); +const RGBA = @import("color").RGBA; pub const vaxis = @import("vaxis"); const Style = @import("theme").Style; const Color = @import("theme").Color; @@ -57,9 +58,9 @@ thread: ?std.Thread = null, window_ready: bool = false, 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_color: app.GpuColor = app.GpuColor.initRgb(255, 255, 255), +secondary_color: RGBA = .init(255, 255, 255, 255), cursor_blink: bool = false, 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 .{ .r = @truncate(color.color >> 16), .g = @truncate(color.color >> 8), diff --git a/test/tests_color.zig b/test/tests_color.zig index e9843773..cb99179e 100644 --- a/test/tests_color.zig +++ b/test/tests_color.zig @@ -2,6 +2,7 @@ const std = @import("std"); const color = @import("color"); const RGB = color.RGB; +const RGBA = color.RGBA; const rgb = RGB.from_u24; test "contrast white/yellow" { @@ -41,3 +42,19 @@ test "best contrast black/white to blue" { const best = color.max_contrast(0x0000FF, 0xFFFFFF, 0x000000); 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); +}