Compare commits

...

13 commits

13 changed files with 169 additions and 76 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_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 },

View file

@ -22,16 +22,16 @@
.hash = "thespian-0.0.1-owFOjlgiBgC8w4XqkCOegxz5vMy6kNErcssWQWf2QHeE",
},
.themes = .{
.url = "https://github.com/neurocyte/flow-themes/releases/download/master-80ac47f701dc4fab73dafb5c0da10b0c9e395052/flow-themes.tar.gz",
.hash = "N-V-__8AAIjCKwDJwoOzcI3233UAVqIaOTOOLMgQK1FnFDfx",
.url = "https://github.com/neurocyte/flow-themes/releases/download/master-9114e2d9ff4e01064ba1da4f780b0c5448b0f0ef/flow-themes.tar.gz",
.hash = "N-V-__8AAJZfLAAP6zSuW13nEIrBBHZM9pbeMRwhvQ-8qqx1",
},
.fuzzig = .{
.url = "https://github.com/fjebaker/fuzzig/archive/4251fe4230d38e721514394a485db62ee1667ff3.tar.gz",
.hash = "fuzzig-0.1.1-Ji0xivxIAQBD0g8O_NV_0foqoPf3elsg9Sc3pNfdVH4D",
},
.vaxis = .{
.url = "git+https://github.com/neurocyte/libvaxis?ref=main#cecc97d9ff8da9df13499da0d0b19c5cd18742c3",
.hash = "vaxis-0.5.1-BWNV_BcgCgDG3wpSPxCHxaRAZukEfnnKrBa-52zjnjex",
.url = "git+https://github.com/neurocyte/libvaxis?ref=main#9fc042aa0fc8b08d15fce1c897bc65ed4a4c65ed",
.hash = "vaxis-0.5.1-BWNV_GgiCgCimbrqWwG7XRgYXNFWFBcH9dTmxx-8Gs-2",
},
.zeit = .{
.url = "git+https://github.com/rockorager/zeit?ref=zig-0.15#ed2ca60db118414bda2b12df2039e33bad3b0b88",

View file

@ -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) };
}

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

@ -23,6 +23,8 @@ pub const FsParams = extern struct {
cursor_color: [4]f32, // RGBA normalized [0,1]
// Secondary cursor colour (positions encoded in ShaderCell._pad)
sec_cursor_color: [4]f32, // RGBA normalized [0,1]
// Background fill for border area outside the cell grid
bg_color: [4]f32, // RGBA normalized [0,1]
};
const vs_src =
@ -48,6 +50,7 @@ const fs_src =
\\uniform int cursor_vis;
\\uniform vec4 cursor_color;
\\uniform vec4 sec_cursor_color;
\\uniform vec4 bg_color;
\\uniform sampler2D glyph_tex_glyph_smp;
\\uniform usampler2D cell_tex_cell_smp;
\\out vec4 frag_color;
@ -69,7 +72,7 @@ const fs_src =
\\ int row = py / cell_size_y;
\\
\\ if (col >= col_count || row >= row_count || row < 0 || col < 0) {
\\ frag_color = vec4(0.0, 0.0, 0.0, 1.0);
\\ frag_color = vec4(bg_color.rgb, 1.0);
\\ return;
\\ }
\\
@ -142,6 +145,7 @@ pub fn shaderDesc(backend: sg.Backend) sg.ShaderDesc {
desc.uniform_blocks[0].glsl_uniforms[8] = .{ .type = .INT, .glsl_name = "cursor_vis" };
desc.uniform_blocks[0].glsl_uniforms[9] = .{ .type = .FLOAT4, .glsl_name = "cursor_color" };
desc.uniform_blocks[0].glsl_uniforms[10] = .{ .type = .FLOAT4, .glsl_name = "sec_cursor_color" };
desc.uniform_blocks[0].glsl_uniforms[11] = .{ .type = .FLOAT4, .glsl_name = "bg_color" };
// Glyph atlas texture: R8 sample_type = FLOAT
desc.views[0].texture = .{

View file

@ -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,10 +116,14 @@ 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;
}
pub fn getBackground() RGBA {
return global.background;
}
// WindowState
pub const WindowState = struct {
@ -387,15 +389,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 +450,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 +465,9 @@ 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(),
.bg_color = global.background.to_vec4(),
};
sg.applyUniforms(0, .{
.ptr = &fs_params,
@ -476,15 +479,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));
}

View file

@ -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)
@ -55,6 +55,11 @@ var font_backend: gpu.RasterizerBackend = .freetype;
var font_dirty: std.atomic.Value(bool) = .init(true);
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(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);
const config_arena = config_arena_instance.allocator();
@ -311,6 +316,13 @@ fn saveConfig() void {
log.err("failed to write gui config file", .{});
}
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);
wio.cancelWait();
}
pub fn requestAttention() void {
attention_pending.store(true, .release);
wio.cancelWait();
@ -359,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 => gpu.getBackground(),
.index => |idx| .from_u24(@import("xterm").colors[idx]),
.rgb => |rgb| .from_u8s(rgb),
};
}
@ -635,6 +639,16 @@ fn wioLoop() void {
state.size = .{ .x = win_size.width, .y = win_size.height };
const font = wio_font;
if (background_dirty.swap(false, .acq_rel)) {
const color_u32 = background_color.load(.acquire);
gpu.setBackground(.{
.r = @truncate(color_u32 >> 24),
.g = @truncate(color_u32 >> 16),
.b = @truncate(color_u32 >> 8),
.a = @truncate(color_u32),
});
}
// Regenerate glyph indices using the GPU state.
// For double-wide characters vaxis emits width=2 for the left
// cell and width=0 (continuation) for the right cell. The

View file

@ -46,7 +46,6 @@
["alt+shift+p", "open_command_palette"],
["alt+n", "goto_next_file_or_diagnostic"],
["alt+p", "goto_prev_file_or_diagnostic"],
["alt+l", "toggle_panel"],
["alt+i", "toggle_inputview"],
["alt+k", "toggle_keybindview"],
["alt+x", "open_command_palette"],
@ -56,7 +55,7 @@
["ctrl+k ctrl+s", "insert_command_name"],
["f9", "theme_prev"],
["f10", "theme_next"],
["f11", "toggle_panel"],
["f11", "show_logview"],
["alt+f11", "toggle_color_scheme"],
["alt+f9", "panel_next_widget_style"],
["shift+alt+f9", "hint_window_next_widget_style"],

View file

@ -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,
@ -378,6 +379,7 @@ pub fn process_renderer_event(self: *Self, msg: []const u8) Error!void {
})) {
self.window_ready = true;
self.dispatch_initialized(self.handler_ctx);
if (self.dispatch_event) |f| f(self.handler_ctx, try self.fmtmsg(.{"capability_detection_complete"}));
return;
}
}
@ -409,7 +411,7 @@ pub fn set_terminal_title(self: *Self, text: []const u8) void {
pub fn set_terminal_style(self: *Self, style_: Style) void {
_ = self;
_ = style_;
if (style_.bg) |bg| app.setBackground(themeColorToGpu(bg));
}
pub fn adjust_fontsize(self: *Self, amount: f32) void {
@ -545,7 +547,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),

View file

@ -76,6 +76,12 @@ pub fn run_cmd(self: *Self, ctx: command.Context) !void {
errdefer env.deinit();
if (env.get("TERM") == null)
try env.put("TERM", "xterm-256color");
try env.put("COLORTERM", "truecolor");
// COLORFGBG tells apps whether the terminal background is dark or light
try env.put("COLORFGBG", switch (tui.active_color_scheme()) {
.dark => "15;0",
.light => "0;15",
});
var cmd_arg: []const u8 = "";
var on_exit: TerminalOnExit = tui.config().terminal_on_exit;
@ -531,6 +537,10 @@ const Vt = struct {
&self.write_buf,
);
const theme = tui.active_theme();
if (theme.editor.fg) |fg| self.vt.fg_color = color.u24_to_u8s(fg.color);
if (theme.editor.bg) |bg| self.vt.bg_color = color.u24_to_u8s(bg.color);
try self.vt.spawn();
self.pty_pid = try pty.spawn(allocator, &self.vt);
}

View file

@ -85,7 +85,7 @@ query_cache_: *syntax.QueryCache,
frames_rendered_: usize = 0,
clipboard: ?std.ArrayList(ClipboardEntry) = null,
clipboard_current_group_number: usize = 0,
color_scheme: enum { dark, light } = .dark,
color_scheme: ColorScheme = .dark,
color_scheme_locked: bool = false,
hint_mode: HintMode = .prefix,
last_palette: ?LastPalette = null,
@ -97,6 +97,8 @@ jump_mode_: bool = false,
auto_run_timer: ?tp.Cancellable = null,
pub const ColorScheme = enum { dark, light };
const HintMode = enum { none, prefix, all };
const LastPalette = struct {
@ -1068,14 +1070,14 @@ fn set_theme_by_name(self: *Self, name: []const u8, action: enum { none, store }
}
}
fn force_color_scheme(self: *Self, color_scheme: @TypeOf(self.color_scheme)) void {
fn force_color_scheme(self: *Self, color_scheme: ColorScheme) void {
self.color_scheme = color_scheme;
self.color_scheme_locked = true;
self.set_terminal_style(self.current_theme());
self.logger.print("color scheme: {t} ({s})", .{ self.color_scheme, self.current_theme().name });
}
fn set_color_scheme(self: *Self, color_scheme: @TypeOf(self.color_scheme)) void {
fn set_color_scheme(self: *Self, color_scheme: ColorScheme) void {
if (self.color_scheme_locked) return;
self.color_scheme = color_scheme;
self.set_terminal_style(self.current_theme());
@ -1931,6 +1933,14 @@ pub fn config() *const @import("config") {
return &current().config_;
}
pub fn active_theme() *const Widget.Theme {
return current().current_theme();
}
pub fn active_color_scheme() ColorScheme {
return current().color_scheme;
}
pub fn get_tab_width() usize {
const self = current();
return self.session_tab_width orelse self.config_.tab_width;

View file

@ -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);
}