Compare commits
6 commits
b3e1f22aa3
...
091345fd02
| Author | SHA1 | Date | |
|---|---|---|---|
| 091345fd02 | |||
| 4ca31b0f75 | |||
| 6faea2ef02 | |||
| f484ea0b57 | |||
| 45db14f894 | |||
| ce85a73063 |
7 changed files with 164 additions and 36 deletions
|
|
@ -606,6 +606,7 @@ pub fn build_exe(
|
||||||
.{ .name = "app", .module = app_mod },
|
.{ .name = "app", .module = app_mod },
|
||||||
.{ .name = "tuirenderer", .module = tui_renderer_mod },
|
.{ .name = "tuirenderer", .module = tui_renderer_mod },
|
||||||
.{ .name = "vaxis", .module = vaxis_mod },
|
.{ .name = "vaxis", .module = vaxis_mod },
|
||||||
|
.{ .name = "rasterizer", .module = truetype_rasterizer_mod },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
break :blk mod;
|
break :blk mod;
|
||||||
|
|
|
||||||
|
|
@ -213,6 +213,9 @@ pub const WindowState = struct {
|
||||||
c.deinit(global.glyph_cache_arena.allocator());
|
c.deinit(global.glyph_cache_arena.allocator());
|
||||||
_ = global.glyph_cache_arena.reset(.retain_capacity);
|
_ = global.glyph_cache_arena.reset(.retain_capacity);
|
||||||
state.glyph_index_cache = null;
|
state.glyph_index_cache = null;
|
||||||
|
// cell_buf was allocated from the arena; clear it so the next
|
||||||
|
// resize doesn't memcpy from the now-freed memory.
|
||||||
|
state.cell_buf = .{};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,12 @@ pub fn findFont(allocator: std.mem.Allocator, name: []const u8) ![]u8 {
|
||||||
else => error.FontFinderNotSupported,
|
else => error.FontFinderNotSupported,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a sorted, deduplicated list of monospace font family names.
|
||||||
|
/// Caller owns the returned slice and each string within it.
|
||||||
|
pub fn listFonts(allocator: std.mem.Allocator) ![][]u8 {
|
||||||
|
return switch (builtin.os.tag) {
|
||||||
|
.linux => @import("font_finder/linux.zig").list(allocator),
|
||||||
|
else => error.FontFinderNotSupported,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,56 @@ const fc = @cImport({
|
||||||
@cInclude("fontconfig/fontconfig.h");
|
@cInclude("fontconfig/fontconfig.h");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Returns a sorted, deduplicated list of monospace font family names.
|
||||||
|
/// Caller owns the returned slice and each string within it.
|
||||||
|
pub fn list(allocator: std.mem.Allocator) ![][]u8 {
|
||||||
|
const config = fc.FcInitLoadConfigAndFonts() orelse return error.FontconfigInit;
|
||||||
|
defer fc.FcConfigDestroy(config);
|
||||||
|
|
||||||
|
const pat = fc.FcPatternCreate() orelse return error.OutOfMemory;
|
||||||
|
defer fc.FcPatternDestroy(pat);
|
||||||
|
_ = fc.FcPatternAddInteger(pat, fc.FC_SPACING, fc.FC_MONO);
|
||||||
|
|
||||||
|
const os = fc.FcObjectSetCreate() orelse return error.OutOfMemory;
|
||||||
|
defer fc.FcObjectSetDestroy(os);
|
||||||
|
_ = fc.FcObjectSetAdd(os, fc.FC_FAMILY);
|
||||||
|
|
||||||
|
const font_set = fc.FcFontList(config, pat, os) orelse return error.OutOfMemory;
|
||||||
|
defer fc.FcFontSetDestroy(font_set);
|
||||||
|
|
||||||
|
var names: std.ArrayList([]u8) = .empty;
|
||||||
|
errdefer {
|
||||||
|
for (names.items) |n| allocator.free(n);
|
||||||
|
names.deinit(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (0..@intCast(font_set.*.nfont)) |i| {
|
||||||
|
var family: [*c]fc.FcChar8 = undefined;
|
||||||
|
if (fc.FcPatternGetString(font_set.*.fonts[i], fc.FC_FAMILY, 0, &family) != fc.FcResultMatch)
|
||||||
|
continue;
|
||||||
|
try names.append(allocator, try allocator.dupe(u8, std.mem.sliceTo(family, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = try names.toOwnedSlice(allocator);
|
||||||
|
std.mem.sort([]u8, result, {}, struct {
|
||||||
|
fn lessThan(_: void, a: []u8, b: []u8) bool {
|
||||||
|
return std.ascii.lessThanIgnoreCase(a, b);
|
||||||
|
}
|
||||||
|
}.lessThan);
|
||||||
|
|
||||||
|
// Remove adjacent duplicates that survived the sort.
|
||||||
|
var w: usize = 0;
|
||||||
|
for (result) |name| {
|
||||||
|
if (w == 0 or !std.ascii.eqlIgnoreCase(result[w - 1], name)) {
|
||||||
|
result[w] = name;
|
||||||
|
w += 1;
|
||||||
|
} else {
|
||||||
|
allocator.free(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result[0..w];
|
||||||
|
}
|
||||||
|
|
||||||
pub fn find(allocator: std.mem.Allocator, name: []const u8) ![]u8 {
|
pub fn find(allocator: std.mem.Allocator, name: []const u8) ![]u8 {
|
||||||
const config = fc.FcInitLoadConfigAndFonts() orelse return error.FontconfigInit;
|
const config = fc.FcInitLoadConfigAndFonts() orelse return error.FontconfigInit;
|
||||||
defer fc.FcConfigDestroy(config);
|
defer fc.FcConfigDestroy(config);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const TrueType = @import("TrueType");
|
const TrueType = @import("TrueType");
|
||||||
const XY = @import("xy").XY;
|
const XY = @import("xy").XY;
|
||||||
const font_finder = @import("font_finder.zig");
|
pub const font_finder = @import("font_finder.zig");
|
||||||
|
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,10 @@ var font_name_len: usize = 0;
|
||||||
var font_dirty: std.atomic.Value(bool) = .init(true);
|
var font_dirty: std.atomic.Value(bool) = .init(true);
|
||||||
var stop_requested: std.atomic.Value(bool) = .init(false);
|
var stop_requested: std.atomic.Value(bool) = .init(false);
|
||||||
|
|
||||||
|
// HiDPI scale factor (logical → physical pixels). Updated from wio .scale events.
|
||||||
|
// Only read/written on the wio thread after initialisation.
|
||||||
|
var dpi_scale: f32 = 1.0;
|
||||||
|
|
||||||
// Window title (written from TUI thread, applied by wio thread)
|
// Window title (written from TUI thread, applied by wio thread)
|
||||||
var title_mutex: std.Thread.Mutex = .{};
|
var title_mutex: std.Thread.Mutex = .{};
|
||||||
var title_buf: [512]u8 = undefined;
|
var title_buf: [512]u8 = undefined;
|
||||||
|
|
@ -198,6 +202,10 @@ pub fn setMouseCursor(shape: vaxis.Mouse.Shape) void {
|
||||||
wio.cancelWait();
|
wio.cancelWait();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getFontName() []const u8 {
|
||||||
|
return if (font_name_len > 0) font_name_buf[0..font_name_len] else "monospace";
|
||||||
|
}
|
||||||
|
|
||||||
pub fn requestAttention() void {
|
pub fn requestAttention() void {
|
||||||
attention_pending.store(true, .release);
|
attention_pending.store(true, .release);
|
||||||
wio.cancelWait();
|
wio.cancelWait();
|
||||||
|
|
@ -205,10 +213,34 @@ pub fn requestAttention() void {
|
||||||
|
|
||||||
// ── Internal helpers (wio thread only) ────────────────────────────────────
|
// ── Internal helpers (wio thread only) ────────────────────────────────────
|
||||||
|
|
||||||
|
const CellPos = struct {
|
||||||
|
col: i32,
|
||||||
|
row: i32,
|
||||||
|
xoff: i32,
|
||||||
|
yoff: i32,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn pixelToCellPos(pos: wio.Position) CellPos {
|
||||||
|
// Mouse positions are in logical pixels; cell_size is in physical pixels.
|
||||||
|
// Scale up to physical before dividing so that col/row and sub-cell offsets
|
||||||
|
// are all expressed in physical pixels, matching the GPU coordinate space.
|
||||||
|
const x: i32 = @intFromFloat(@as(f32, @floatFromInt(pos.x)) * dpi_scale);
|
||||||
|
const y: i32 = @intFromFloat(@as(f32, @floatFromInt(pos.y)) * dpi_scale);
|
||||||
|
const cw: i32 = wio_font.cell_size.x;
|
||||||
|
const ch: i32 = wio_font.cell_size.y;
|
||||||
|
return .{
|
||||||
|
.col = @divTrunc(x, cw),
|
||||||
|
.row = @divTrunc(y, ch),
|
||||||
|
.xoff = @mod(x, cw),
|
||||||
|
.yoff = @mod(y, ch),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Reload wio_font from current settings. Called only from the wio thread.
|
// Reload wio_font from current settings. Called only from the wio thread.
|
||||||
fn reloadFont() void {
|
fn reloadFont() void {
|
||||||
const name = if (font_name_len > 0) font_name_buf[0..font_name_len] else "monospace";
|
const name = if (font_name_len > 0) font_name_buf[0..font_name_len] else "monospace";
|
||||||
wio_font = gpu.loadFont(name, font_size_px) catch return;
|
const size_physical: u16 = @intFromFloat(@round(@as(f32, @floatFromInt(font_size_px)) * dpi_scale));
|
||||||
|
wio_font = gpu.loadFont(name, @max(size_physical, 4)) catch return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check dirty flag and reload if needed.
|
// Check dirty flag and reload if needed.
|
||||||
|
|
@ -278,19 +310,28 @@ fn wioLoop() void {
|
||||||
};
|
};
|
||||||
defer gpu.deinit();
|
defer gpu.deinit();
|
||||||
|
|
||||||
// Load the initial font on the wio thread (gpu.init must be done first).
|
|
||||||
reloadFont();
|
|
||||||
|
|
||||||
var state = gpu.WindowState.init();
|
var state = gpu.WindowState.init();
|
||||||
defer state.deinit();
|
defer state.deinit();
|
||||||
|
|
||||||
// Current window size in pixels (updated by size_physical events)
|
// Current window sizes (updated by size_* events).
|
||||||
var win_size: wio.Size = .{ .width = 1280, .height = 720 };
|
var win_size: wio.Size = .{ .width = 1280, .height = 720 };
|
||||||
// Cell grid dimensions (updated on resize)
|
// Cell grid dimensions (updated on resize)
|
||||||
var cell_width: u16 = 80;
|
var cell_width: u16 = 80;
|
||||||
var cell_height: u16 = 24;
|
var cell_height: u16 = 24;
|
||||||
|
|
||||||
|
// Drain the initial wio events (scale + size_*) that are queued synchronously
|
||||||
|
// during createWindow. This ensures dpi_scale and win_size are correct before
|
||||||
|
// the first reloadFont / sendResize, avoiding a brief render at the wrong scale.
|
||||||
|
while (window.getEvent()) |event| {
|
||||||
|
switch (event) {
|
||||||
|
.scale => |s| dpi_scale = s,
|
||||||
|
.size_physical => |sz| win_size = sz,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Notify the tui that the window is ready
|
// Notify the tui that the window is ready
|
||||||
|
reloadFont();
|
||||||
sendResize(win_size, &state, &cell_width, &cell_height);
|
sendResize(win_size, &state, &cell_width, &cell_height);
|
||||||
tui_pid.send(.{ "RDR", "WindowCreated", @as(usize, 0) }) catch {};
|
tui_pid.send(.{ "RDR", "WindowCreated", @as(usize, 0) }) catch {};
|
||||||
|
|
||||||
|
|
@ -310,6 +351,10 @@ fn wioLoop() void {
|
||||||
.close => {
|
.close => {
|
||||||
running = false;
|
running = false;
|
||||||
},
|
},
|
||||||
|
.scale => |s| {
|
||||||
|
dpi_scale = s;
|
||||||
|
font_dirty.store(true, .release);
|
||||||
|
},
|
||||||
.size_physical => |sz| {
|
.size_physical => |sz| {
|
||||||
win_size = sz;
|
win_size = sz;
|
||||||
sendResize(sz, &state, &cell_width, &cell_height);
|
sendResize(sz, &state, &cell_width, &cell_height);
|
||||||
|
|
@ -318,20 +363,15 @@ fn wioLoop() void {
|
||||||
held_buttons.press(btn);
|
held_buttons.press(btn);
|
||||||
const mods = input_translate.Mods.fromButtons(held_buttons);
|
const mods = input_translate.Mods.fromButtons(held_buttons);
|
||||||
if (input_translate.mouseButtonId(btn)) |mb_id| {
|
if (input_translate.mouseButtonId(btn)) |mb_id| {
|
||||||
const col: i32 = @intCast(mouse_pos.x);
|
const cp = pixelToCellPos(mouse_pos);
|
||||||
const row: i32 = @intCast(mouse_pos.y);
|
|
||||||
const col_cell: i32 = @intCast(@divTrunc(col, wio_font.cell_size.x));
|
|
||||||
const row_cell: i32 = @intCast(@divTrunc(row, wio_font.cell_size.y));
|
|
||||||
const xoff: i32 = @intCast(@mod(col, wio_font.cell_size.x));
|
|
||||||
const yoff: i32 = @intCast(@mod(row, wio_font.cell_size.y));
|
|
||||||
tui_pid.send(.{
|
tui_pid.send(.{
|
||||||
"RDR", "B",
|
"RDR", "B",
|
||||||
@as(u8, 1), // press
|
@as(u8, 1), // press
|
||||||
mb_id,
|
mb_id,
|
||||||
col_cell,
|
cp.col,
|
||||||
row_cell,
|
cp.row,
|
||||||
xoff,
|
cp.xoff,
|
||||||
yoff,
|
cp.yoff,
|
||||||
}) catch {};
|
}) catch {};
|
||||||
} else {
|
} else {
|
||||||
const base_cp = input_translate.codepointFromButton(btn, .{});
|
const base_cp = input_translate.codepointFromButton(btn, .{});
|
||||||
|
|
@ -351,20 +391,15 @@ fn wioLoop() void {
|
||||||
held_buttons.release(btn);
|
held_buttons.release(btn);
|
||||||
const mods = input_translate.Mods.fromButtons(held_buttons);
|
const mods = input_translate.Mods.fromButtons(held_buttons);
|
||||||
if (input_translate.mouseButtonId(btn)) |mb_id| {
|
if (input_translate.mouseButtonId(btn)) |mb_id| {
|
||||||
const col: i32 = @intCast(mouse_pos.x);
|
const cp = pixelToCellPos(mouse_pos);
|
||||||
const row: i32 = @intCast(mouse_pos.y);
|
|
||||||
const col_cell: i32 = @intCast(@divTrunc(col, wio_font.cell_size.x));
|
|
||||||
const row_cell: i32 = @intCast(@divTrunc(row, wio_font.cell_size.y));
|
|
||||||
const xoff: i32 = @intCast(@mod(col, wio_font.cell_size.x));
|
|
||||||
const yoff: i32 = @intCast(@mod(row, wio_font.cell_size.y));
|
|
||||||
tui_pid.send(.{
|
tui_pid.send(.{
|
||||||
"RDR", "B",
|
"RDR", "B",
|
||||||
@as(u8, 3), // release
|
@as(u8, 3), // release
|
||||||
mb_id,
|
mb_id,
|
||||||
col_cell,
|
cp.col,
|
||||||
row_cell,
|
cp.row,
|
||||||
xoff,
|
cp.xoff,
|
||||||
yoff,
|
cp.yoff,
|
||||||
}) catch {};
|
}) catch {};
|
||||||
} else {
|
} else {
|
||||||
const base_cp = input_translate.codepointFromButton(btn, .{});
|
const base_cp = input_translate.codepointFromButton(btn, .{});
|
||||||
|
|
@ -383,21 +418,22 @@ fn wioLoop() void {
|
||||||
},
|
},
|
||||||
.mouse => |pos| {
|
.mouse => |pos| {
|
||||||
mouse_pos = pos;
|
mouse_pos = pos;
|
||||||
const col_cell: i32 = @intCast(@divTrunc(@as(i32, @intCast(pos.x)), wio_font.cell_size.x));
|
const cp = pixelToCellPos(pos);
|
||||||
const row_cell: i32 = @intCast(@divTrunc(@as(i32, @intCast(pos.y)), wio_font.cell_size.y));
|
|
||||||
const xoff: i32 = @intCast(@mod(@as(i32, @intCast(pos.x)), wio_font.cell_size.x));
|
|
||||||
const yoff: i32 = @intCast(@mod(@as(i32, @intCast(pos.y)), wio_font.cell_size.y));
|
|
||||||
if (input_translate.heldMouseButtonId(held_buttons)) |mb_id| {
|
if (input_translate.heldMouseButtonId(held_buttons)) |mb_id| {
|
||||||
tui_pid.send(.{ "RDR", "D", mb_id, col_cell, row_cell, xoff, yoff }) catch {};
|
tui_pid.send(.{ "RDR", "D", mb_id, cp.col, cp.row, cp.xoff, cp.yoff }) catch {};
|
||||||
} else {
|
} else {
|
||||||
tui_pid.send(.{ "RDR", "M", col_cell, row_cell, xoff, yoff }) catch {};
|
tui_pid.send(.{ "RDR", "M", cp.col, cp.row, cp.xoff, cp.yoff }) catch {};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.scroll_vertical => |dy| {
|
.scroll_vertical => |dy| {
|
||||||
const btn_id: u8 = if (dy < 0) 64 else 65; // up / down scroll
|
const btn_id: u8 = if (dy < 0) 64 else 65; // up / down scroll
|
||||||
const col_cell: i32 = @intCast(@divTrunc(@as(i32, @intCast(mouse_pos.x)), wio_font.cell_size.x));
|
const cp = pixelToCellPos(mouse_pos);
|
||||||
const row_cell: i32 = @intCast(@divTrunc(@as(i32, @intCast(mouse_pos.y)), wio_font.cell_size.y));
|
tui_pid.send(.{ "RDR", "B", @as(u8, 1), btn_id, cp.col, cp.row, cp.xoff, cp.yoff }) catch {};
|
||||||
tui_pid.send(.{ "RDR", "B", @as(u8, 1), btn_id, col_cell, row_cell, @as(i32, 0), @as(i32, 0) }) catch {};
|
},
|
||||||
|
.scroll_horizontal => |dx| {
|
||||||
|
const btn_id: u8 = if (dx < 0) 66 else 67; // left / right scroll
|
||||||
|
const cp = pixelToCellPos(mouse_pos);
|
||||||
|
tui_pid.send(.{ "RDR", "B", @as(u8, 1), btn_id, cp.col, cp.row, cp.xoff, cp.yoff }) catch {};
|
||||||
},
|
},
|
||||||
.focused => window.enableTextInput(.{}),
|
.focused => window.enableTextInput(.{}),
|
||||||
.unfocused => window.disableTextInput(),
|
.unfocused => window.disableTextInput(),
|
||||||
|
|
|
||||||
|
|
@ -381,7 +381,36 @@ pub fn reset_fontface(self: *Self) void {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_fontfaces(self: *Self) void {
|
pub fn get_fontfaces(self: *Self) void {
|
||||||
_ = self;
|
const font_finder = @import("rasterizer").font_finder;
|
||||||
|
const dispatch = self.dispatch_event orelse return;
|
||||||
|
|
||||||
|
// Report the current font first.
|
||||||
|
if (self.fmtmsg(.{ "fontface", "current", app.getFontName() })) |msg|
|
||||||
|
dispatch(self.handler_ctx, msg)
|
||||||
|
else |_| {}
|
||||||
|
|
||||||
|
// Enumerate all available monospace fonts and report each one.
|
||||||
|
const names = font_finder.listFonts(self.allocator) catch {
|
||||||
|
// If enumeration fails, still close the palette with "done".
|
||||||
|
if (self.fmtmsg(.{ "fontface", "done" })) |msg|
|
||||||
|
dispatch(self.handler_ctx, msg)
|
||||||
|
else |_| {}
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
defer {
|
||||||
|
for (names) |n| self.allocator.free(n);
|
||||||
|
self.allocator.free(names);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (names) |name| {
|
||||||
|
if (self.fmtmsg(.{ "fontface", name })) |msg|
|
||||||
|
dispatch(self.handler_ctx, msg)
|
||||||
|
else |_| {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.fmtmsg(.{ "fontface", "done" })) |msg|
|
||||||
|
dispatch(self.handler_ctx, msg)
|
||||||
|
else |_| {}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_terminal_cursor_color(self: *Self, color: Color) void {
|
pub fn set_terminal_cursor_color(self: *Self, color: Color) void {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue