Merge pull request #1662 from mitchellh/fontmem

Dedupe font stack for terminals with identical font configuration
This commit is contained in:
Mitchell Hashimoto
2024-04-08 16:57:58 -04:00
committed by GitHub
30 changed files with 2758 additions and 2322 deletions

View File

@ -41,10 +41,9 @@ mailbox: Mailbox.Queue,
/// Set to true once we're quitting. This never goes false again.
quit: bool,
/// Font discovery mechanism. This is only safe to use from the main thread.
/// This is lazily initialized on the first call to fontDiscover so do
/// not access this directly.
font_discover: ?font.Discover = null,
/// The set of font GroupCache instances shared by surfaces with the
/// same font configuration.
font_grid_set: font.SharedGridSet,
/// Initialize the main app instance. This creates the main window, sets
/// up the renderer state, compiles the shaders, etc. This is the primary
@ -55,11 +54,15 @@ pub fn create(
var app = try alloc.create(App);
errdefer alloc.destroy(app);
var font_grid_set = try font.SharedGridSet.init(alloc);
errdefer font_grid_set.deinit();
app.* = .{
.alloc = alloc,
.surfaces = .{},
.mailbox = .{},
.quit = false,
.font_grid_set = font_grid_set,
};
errdefer app.surfaces.deinit(alloc);
@ -71,9 +74,12 @@ pub fn destroy(self: *App) void {
for (self.surfaces.items) |surface| surface.deinit();
self.surfaces.deinit(self.alloc);
if (comptime font.Discover != void) {
if (self.font_discover) |*v| v.deinit();
}
// Clean up our font group cache
// We should have zero items in the grid set at this point because
// destroy only gets called when the app is shutting down and this
// should gracefully close all surfaces.
assert(self.font_grid_set.count() == 0);
self.font_grid_set.deinit();
self.alloc.destroy(self);
}
@ -166,20 +172,6 @@ pub fn needsConfirmQuit(self: *const App) bool {
return false;
}
/// Initialize once and return the font discovery mechanism. This remains
/// initialized throughout the lifetime of the application because some
/// font discovery mechanisms (i.e. fontconfig) are unsafe to reinit.
pub fn fontDiscover(self: *App) !?*font.Discover {
// If we're built without a font discovery mechanism, return null
if (comptime font.Discover == void) return null;
// If we initialized, use it
if (self.font_discover) |*v| return v;
self.font_discover = font.Discover.init();
return &self.font_discover.?;
}
/// Drain the mailbox.
fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
while (self.mailbox.pop()) |message| {

View File

@ -54,8 +54,7 @@ rt_app: *apprt.runtime.App,
rt_surface: *apprt.runtime.Surface,
/// The font structures
font_lib: font.Library,
font_group: *font.GroupCache,
font_grid_key: font.SharedGridSet.Key,
font_size: font.face.DesiredSize,
/// The renderer for this surface.
@ -206,6 +205,7 @@ const DerivedConfig = struct {
confirm_close_surface: bool,
cursor_click_to_move: bool,
desktop_notifications: bool,
font: font.SharedGridSet.DerivedConfig,
mouse_interval: u64,
mouse_hide_while_typing: bool,
mouse_scroll_multiplier: f64,
@ -263,6 +263,7 @@ const DerivedConfig = struct {
.confirm_close_surface = config.@"confirm-close-surface",
.cursor_click_to_move = config.@"cursor-click-to-move",
.desktop_notifications = config.@"desktop-notifications",
.font = try font.SharedGridSet.DerivedConfig.init(alloc, config),
.mouse_interval = config.@"click-repeat-interval" * 1_000_000, // 500ms
.mouse_hide_while_typing = config.@"mouse-hide-while-typing",
.mouse_scroll_multiplier = config.@"mouse-scroll-multiplier",
@ -298,6 +299,10 @@ pub fn init(
rt_app: *apprt.runtime.App,
rt_surface: *apprt.runtime.Surface,
) !void {
// Get our configuration
var derived_config = try DerivedConfig.init(alloc, config);
errdefer derived_config.deinit();
// Initialize our renderer with our initialized surface.
try Renderer.surfaceInit(rt_surface);
@ -320,174 +325,15 @@ pub fn init(
.ydpi = @intFromFloat(y_dpi),
};
// Find all the fonts for this surface
//
// Future: we can share the font group amongst all surfaces to save
// some new surface init time and some memory. This will require making
// thread-safe changes to font structs.
var font_lib = try font.Library.init();
errdefer font_lib.deinit();
var font_group = try alloc.create(font.GroupCache);
errdefer alloc.destroy(font_group);
font_group.* = try font.GroupCache.init(alloc, group: {
var group = try font.Group.init(alloc, font_lib, font_size);
errdefer group.deinit();
// Setup our font metric modifiers if we have any.
group.metric_modifiers = set: {
var set: font.face.Metrics.ModifierSet = .{};
errdefer set.deinit(alloc);
if (config.@"adjust-cell-width") |m| try set.put(alloc, .cell_width, m);
if (config.@"adjust-cell-height") |m| try set.put(alloc, .cell_height, m);
if (config.@"adjust-font-baseline") |m| try set.put(alloc, .cell_baseline, m);
if (config.@"adjust-underline-position") |m| try set.put(alloc, .underline_position, m);
if (config.@"adjust-underline-thickness") |m| try set.put(alloc, .underline_thickness, m);
if (config.@"adjust-strikethrough-position") |m| try set.put(alloc, .strikethrough_position, m);
if (config.@"adjust-strikethrough-thickness") |m| try set.put(alloc, .strikethrough_thickness, m);
break :set set;
};
// If we have codepoint mappings, set those.
if (config.@"font-codepoint-map".map.list.len > 0) {
group.codepoint_map = config.@"font-codepoint-map".map;
}
// Set our styles
group.styles.set(.bold, config.@"font-style-bold" != .false);
group.styles.set(.italic, config.@"font-style-italic" != .false);
group.styles.set(.bold_italic, config.@"font-style-bold-italic" != .false);
// Search for fonts
if (font.Discover != void) discover: {
const disco = try app.fontDiscover() orelse {
log.warn("font discovery not available, cannot search for fonts", .{});
break :discover;
};
group.discover = disco;
// A buffer we use to store the font names for logging.
var name_buf: [256]u8 = undefined;
for (config.@"font-family".list.items) |family| {
var disco_it = try disco.discover(alloc, .{
.family = family,
.style = config.@"font-style".nameValue(),
.size = font_size.points,
.variations = config.@"font-variation".list.items,
});
defer disco_it.deinit();
if (try disco_it.next()) |face| {
log.info("font regular: {s}", .{try face.name(&name_buf)});
_ = try group.addFace(.regular, .{ .deferred = face });
} else log.warn("font-family not found: {s}", .{family});
}
// In all the styled cases below, we prefer to specify an exact
// style via the `font-style` configuration. If a style is not
// specified, we use the discovery mechanism to search for a
// style category such as bold, italic, etc. We can't specify both
// because the latter will restrict the search to only that. If
// a user says `font-style = italic` for the bold face for example,
// no results would be found if we restrict to ALSO searching for
// italic.
for (config.@"font-family-bold".list.items) |family| {
const style = config.@"font-style-bold".nameValue();
var disco_it = try disco.discover(alloc, .{
.family = family,
.style = style,
.size = font_size.points,
.bold = style == null,
.variations = config.@"font-variation-bold".list.items,
});
defer disco_it.deinit();
if (try disco_it.next()) |face| {
log.info("font bold: {s}", .{try face.name(&name_buf)});
_ = try group.addFace(.bold, .{ .deferred = face });
} else log.warn("font-family-bold not found: {s}", .{family});
}
for (config.@"font-family-italic".list.items) |family| {
const style = config.@"font-style-italic".nameValue();
var disco_it = try disco.discover(alloc, .{
.family = family,
.style = style,
.size = font_size.points,
.italic = style == null,
.variations = config.@"font-variation-italic".list.items,
});
defer disco_it.deinit();
if (try disco_it.next()) |face| {
log.info("font italic: {s}", .{try face.name(&name_buf)});
_ = try group.addFace(.italic, .{ .deferred = face });
} else log.warn("font-family-italic not found: {s}", .{family});
}
for (config.@"font-family-bold-italic".list.items) |family| {
const style = config.@"font-style-bold-italic".nameValue();
var disco_it = try disco.discover(alloc, .{
.family = family,
.style = style,
.size = font_size.points,
.bold = style == null,
.italic = style == null,
.variations = config.@"font-variation-bold-italic".list.items,
});
defer disco_it.deinit();
if (try disco_it.next()) |face| {
log.info("font bold+italic: {s}", .{try face.name(&name_buf)});
_ = try group.addFace(.bold_italic, .{ .deferred = face });
} else log.warn("font-family-bold-italic not found: {s}", .{family});
}
}
// Our built-in font will be used as a backup
_ = try group.addFace(
.regular,
.{ .fallback_loaded = try font.Face.init(font_lib, face_ttf, group.faceOptions()) },
);
_ = try group.addFace(
.bold,
.{ .fallback_loaded = try font.Face.init(font_lib, face_bold_ttf, group.faceOptions()) },
);
// Auto-italicize if we have to.
try group.italicize();
// On macOS, always search for and add the Apple Emoji font
// as our preferred emoji font for fallback. We do this in case
// people add other emoji fonts to their system, we always want to
// prefer the official one. Users can override this by explicitly
// specifying a font-family for emoji.
if (comptime builtin.target.isDarwin()) apple_emoji: {
const disco = group.discover orelse break :apple_emoji;
var disco_it = try disco.discover(alloc, .{
.family = "Apple Color Emoji",
});
defer disco_it.deinit();
if (try disco_it.next()) |face| {
_ = try group.addFace(.regular, .{ .fallback_deferred = face });
}
}
// Emoji fallback. We don't include this on Mac since Mac is expected
// to always have the Apple Emoji available on the system.
if (comptime !builtin.target.isDarwin() or font.Discover == void) {
_ = try group.addFace(
.regular,
.{ .fallback_loaded = try font.Face.init(font_lib, face_emoji_ttf, group.faceOptions()) },
);
_ = try group.addFace(
.regular,
.{ .fallback_loaded = try font.Face.init(font_lib, face_emoji_text_ttf, group.faceOptions()) },
);
}
break :group group;
});
errdefer font_group.deinit(alloc);
log.info("font loading complete, any non-logged faces are using the built-in font", .{});
// Setup our font group. This will reuse an existing font group if
// it was already loaded.
const font_grid_key, const font_grid = try app.font_grid_set.ref(
&derived_config.font,
font_size,
);
// Pre-calculate our initial cell size ourselves.
const cell_size = try renderer.CellSize.init(alloc, font_group);
const cell_size = font_grid.cellSize();
// Convert our padding from points to pixels
const padding_x: u32 = padding_x: {
@ -509,7 +355,7 @@ pub fn init(
const app_mailbox: App.Mailbox = .{ .rt_app = rt_app, .mailbox = &app.mailbox };
var renderer_impl = try Renderer.init(alloc, .{
.config = try Renderer.DerivedConfig.init(alloc, config),
.font_group = font_group,
.font_grid = font_grid,
.padding = .{
.explicit = padding,
.balance = config.@"window-padding-balance",
@ -570,8 +416,7 @@ pub fn init(
.app = app,
.rt_app = rt_app,
.rt_surface = rt_surface,
.font_lib = font_lib,
.font_group = font_group,
.font_grid_key = font_grid_key,
.font_size = font_size,
.renderer = renderer_impl,
.renderer_thread = render_thread,
@ -588,7 +433,7 @@ pub fn init(
.grid_size = .{},
.cell_size = cell_size,
.padding = padding,
.config = try DerivedConfig.init(alloc, config),
.config = derived_config,
};
// Report initial cell size on surface creation
@ -686,15 +531,14 @@ pub fn deinit(self: *Surface) void {
self.io_thread.deinit();
self.io.deinit();
self.font_group.deinit(self.alloc);
self.font_lib.deinit();
self.alloc.destroy(self.font_group);
if (self.inspector) |v| {
v.deinit();
self.alloc.destroy(v);
}
// Clean up our font grid
self.app.font_grid_set.deref(self.font_grid_key);
// Clean up our render state
if (self.renderer_state.preedit) |p| self.alloc.free(p.codepoints);
self.alloc.destroy(self.renderer_state.mutex);
@ -798,8 +642,6 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
try self.rt_surface.setMouseShape(shape);
},
.cell_size => |size| try self.setCellSize(size),
.clipboard_read => |clipboard| {
if (self.config.clipboard_read == .deny) {
log.info("application attempted to read clipboard, but 'clipboard-read' is set to deny", .{});
@ -1122,15 +964,37 @@ fn setCellSize(self: *Surface, size: renderer.CellSize) !void {
/// Change the font size.
///
/// This can only be called from the main thread.
pub fn setFontSize(self: *Surface, size: font.face.DesiredSize) void {
pub fn setFontSize(self: *Surface, size: font.face.DesiredSize) !void {
// Update our font size so future changes work
self.font_size = size;
// Notify our render thread of the font size. This triggers everything else.
// We need to build up a new font stack for this font size.
const font_grid_key, const font_grid = try self.app.font_grid_set.ref(
&self.config.font,
self.font_size,
);
errdefer self.app.font_grid_set.deref(font_grid_key);
// Set our cell size
try self.setCellSize(.{
.width = font_grid.metrics.cell_width,
.height = font_grid.metrics.cell_height,
});
// Notify our render thread of the new font stack. The renderer
// MUST accept the new font grid and deref the old.
_ = self.renderer_thread.mailbox.push(.{
.font_size = size,
.font_grid = .{
.grid = font_grid,
.set = &self.app.font_grid_set,
.old_key = self.font_grid_key,
.new_key = font_grid_key,
},
}, .{ .forever = {} });
// Once we've sent the key we can replace our key
self.font_grid_key = font_grid_key;
// Schedule render which also drains our mailbox
self.queueRender() catch unreachable;
}
@ -1839,7 +1703,7 @@ pub fn contentScaleCallback(self: *Surface, content_scale: apprt.ContentScale) !
return;
}
self.setFontSize(size);
try self.setFontSize(size);
// Update our padding which is dependent on DPI.
self.padding = padding: {
@ -3138,7 +3002,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
var size = self.font_size;
size.points +|= delta;
self.setFontSize(size);
try self.setFontSize(size);
},
.decrease_font_size => |delta| {
@ -3146,7 +3010,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
var size = self.font_size;
size.points = @max(1, size.points -| delta);
self.setFontSize(size);
try self.setFontSize(size);
},
.reset_font_size => {
@ -3154,7 +3018,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
var size = self.font_size;
size.points = self.config.original_font_size;
self.setFontSize(size);
try self.setFontSize(size);
},
.clear_screen => {

View File

@ -414,7 +414,7 @@ pub const Surface = struct {
if (opts.font_size != 0) {
var font_size = self.core_surface.font_size;
font_size.points = opts.font_size;
self.core_surface.setFontSize(font_size);
try self.core_surface.setFontSize(font_size);
}
}

View File

@ -246,7 +246,7 @@ pub const App = struct {
// If we have a parent, inherit some properties
if (self.config.@"window-inherit-font-size") {
if (parent_) |parent| {
surface.core_surface.setFontSize(parent.font_size);
try surface.core_surface.setFontSize(parent.font_size);
}
}

View File

@ -416,7 +416,7 @@ fn realize(self: *Surface) !void {
// If we have a font size we want, set that now
if (self.font_size) |size| {
self.core_surface.setFontSize(size);
try self.core_surface.setFontSize(size);
}
// Set the intial color scheme

View File

@ -21,9 +21,6 @@ pub const Message = union(enum) {
/// Set the mouse shape.
set_mouse_shape: terminal.MouseShape,
/// Change the cell size.
cell_size: renderer.CellSize,
/// Read the clipboard and write to the pty.
clipboard_read: apprt.Clipboard,

View File

@ -8,14 +8,18 @@ pub const edit = @import("config/edit.zig");
pub const url = @import("config/url.zig");
// Field types
pub const ClipboardAccess = Config.ClipboardAccess;
pub const CopyOnSelect = Config.CopyOnSelect;
pub const CustomShaderAnimation = Config.CustomShaderAnimation;
pub const FontStyle = Config.FontStyle;
pub const Keybinds = Config.Keybinds;
pub const MouseShiftCapture = Config.MouseShiftCapture;
pub const CustomShaderAnimation = Config.CustomShaderAnimation;
pub const NonNativeFullscreen = Config.NonNativeFullscreen;
pub const OptionAsAlt = Config.OptionAsAlt;
pub const RepeatableCodepointMap = Config.RepeatableCodepointMap;
pub const RepeatableFontVariation = Config.RepeatableFontVariation;
pub const RepeatableString = Config.RepeatableString;
pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures;
pub const ClipboardAccess = Config.ClipboardAccess;
// Alternate APIs
pub const CAPI = @import("config/CAPI.zig");

View File

@ -2498,9 +2498,14 @@ pub const RepeatableString = struct {
/// Deep copy of the struct. Required by Config.
pub fn clone(self: *const Self, alloc: Allocator) !Self {
return .{
.list = try self.list.clone(alloc),
};
// Copy the list and all the strings in the list.
const list = try self.list.clone(alloc);
for (list.items) |*item| {
const copy = try alloc.dupeZ(u8, item.*);
item.* = copy;
}
return .{ .list = list };
}
/// The number of itemsin the list
@ -2960,9 +2965,7 @@ pub const RepeatableCodepointMap = struct {
/// Deep copy of the struct. Required by Config.
pub fn clone(self: *const Self, alloc: Allocator) !Self {
return .{
.map = .{ .list = try self.map.list.clone(alloc) },
};
return .{ .map = try self.map.clone(alloc) };
}
/// Compare if two of our value are requal. Required by Config.
@ -3243,6 +3246,14 @@ pub const FontStyle = union(enum) {
};
}
/// Deep copy of the struct. Required by Config.
pub fn clone(self: Self, alloc: Allocator) !Self {
return switch (self) {
.default, .false => self,
.name => |v| .{ .name = try alloc.dupeZ(u8, v) },
};
}
/// Used by Formatter
pub fn formatEntry(self: Self, formatter: anytype) !void {
switch (self) {

View File

@ -38,18 +38,16 @@ nodes: std.ArrayListUnmanaged(Node) = .{},
/// different formats, you must use multiple atlases or convert the textures.
format: Format = .greyscale,
/// This will be set to true when the atlas has data set on it. It is up
/// to the user of the atlas to set this to false when they observe the value.
/// This is a useful value to know if you need to send new data to the GPU or
/// not.
modified: bool = false,
/// This will be incremented every time the atlas is modified. This is useful
/// for knowing if the texture data has changed since the last time it was
/// sent to the GPU. It is up the user of the atlas to read this value atomically
/// to observe it.
modified: std.atomic.Value(usize) = .{ .raw = 0 },
/// This will be set to true when the atlas has been resized. It is up
/// to the user of the atlas to set this to false when they observe the value.
/// The resized value is useful for sending textures to the GPU to know if
/// a new texture needs to be allocated or if an existing one can be
/// updated in-place.
resized: bool = false,
/// This will be incremented every time the atlas is resized. This is useful
/// for knowing if a GPU texture can be updated in-place or if it requires
/// a resize operation.
resized: std.atomic.Value(usize) = .{ .raw = 0 },
pub const Format = enum(u8) {
greyscale = 0,
@ -99,7 +97,6 @@ pub fn init(alloc: Allocator, size: u32, format: Format) !Atlas {
// This sets up our initial state
result.clear();
result.modified = false;
return result;
}
@ -243,7 +240,7 @@ pub fn set(self: *Atlas, reg: Region, data: []const u8) void {
);
}
self.modified = true;
_ = self.modified.fetchAdd(1, .monotonic);
}
// Grow the texture to the new size, preserving all previously written data.
@ -284,13 +281,13 @@ pub fn grow(self: *Atlas, alloc: Allocator, size_new: u32) Allocator.Error!void
}, data_old[size_old * self.format.depth() ..]);
// We are both modified and resized
self.modified = true;
self.resized = true;
_ = self.modified.fetchAdd(1, .monotonic);
_ = self.resized.fetchAdd(1, .monotonic);
}
// Empty the atlas. This doesn't reclaim any previously allocated memory.
pub fn clear(self: *Atlas) void {
self.modified = true;
_ = self.modified.fetchAdd(1, .monotonic);
@memset(self.data, 0);
self.nodes.clearRetainingCapacity();
@ -475,8 +472,9 @@ test "exact fit" {
var atlas = try init(alloc, 34, .greyscale); // +2 for 1px border
defer atlas.deinit(alloc);
const modified = atlas.modified.load(.monotonic);
_ = try atlas.reserve(alloc, 32, 32);
try testing.expect(!atlas.modified);
try testing.expectEqual(modified, atlas.modified.load(.monotonic));
try testing.expectError(Error.AtlasFull, atlas.reserve(alloc, 1, 1));
}
@ -505,9 +503,10 @@ test "writing data" {
defer atlas.deinit(alloc);
const reg = try atlas.reserve(alloc, 2, 2);
try testing.expect(!atlas.modified);
const old = atlas.modified.load(.monotonic);
atlas.set(reg, &[_]u8{ 1, 2, 3, 4 });
try testing.expect(atlas.modified);
const new = atlas.modified.load(.monotonic);
try testing.expect(new > old);
// 33 because of the 1px border and so on
try testing.expectEqual(@as(u8, 1), atlas.data[33]);
@ -531,14 +530,14 @@ test "grow" {
try testing.expectEqual(@as(u8, 3), atlas.data[9]);
try testing.expectEqual(@as(u8, 4), atlas.data[10]);
// Reset our state
atlas.modified = false;
atlas.resized = false;
// Expand by exactly 1 should fit our new 1x1 block.
const old_modified = atlas.modified.load(.monotonic);
const old_resized = atlas.resized.load(.monotonic);
try atlas.grow(alloc, atlas.size + 1);
try testing.expect(atlas.modified);
try testing.expect(atlas.resized);
const new_modified = atlas.modified.load(.monotonic);
const new_resized = atlas.resized.load(.monotonic);
try testing.expect(new_modified > old_modified);
try testing.expect(new_resized > old_resized);
_ = try atlas.reserve(alloc, 1, 1);
// Ensure our data is still set. Not the offsets change due to size.

View File

@ -30,6 +30,18 @@ pub fn deinit(self: *CodepointMap, alloc: Allocator) void {
self.list.deinit(alloc);
}
/// Deep copy of the struct. The given allocator is expected to
/// be an arena allocator of some sort since the struct itself
/// doesn't support fine-grained deallocation of fields.
pub fn clone(self: *const CodepointMap, alloc: Allocator) !CodepointMap {
var list = try self.list.clone(alloc);
for (list.items(.descriptor)) |*d| {
d.* = try d.clone(alloc);
}
return .{ .list = list };
}
/// Add an entry to the map.
///
/// For conflicting codepoints, entries added later take priority over
@ -53,6 +65,26 @@ pub fn get(self: *const CodepointMap, cp: u21) ?discovery.Descriptor {
return null;
}
/// Hash with the given hasher.
pub fn hash(self: *const CodepointMap, hasher: anytype) void {
const autoHash = std.hash.autoHash;
autoHash(hasher, self.list.len);
const slice = self.list.slice();
for (0..slice.len) |i| {
const entry = slice.get(i);
autoHash(hasher, entry.range);
entry.descriptor.hash(hasher);
}
}
/// Returns a hash code that can be used to uniquely identify this
/// action.
pub fn hashcode(self: *const CodepointMap) u64 {
var hasher = std.hash.Wyhash.init(0);
self.hash(&hasher);
return hasher.final();
}
test "codepointmap" {
const testing = std.testing;
const alloc = testing.allocator;

View File

@ -0,0 +1,522 @@
//! CodepointResolver maps a codepoint to a font. It is more dynamic
//! than "Collection" since it supports mapping codepoint ranges to
//! specific fonts, searching for fallback fonts, and more.
//!
//! To initialize the codepoint resolver, manually initialize using
//! Zig initialization syntax: .{}-style. Set the fields you want set,
//! and begin using the resolver.
//!
//! Deinit must still be called on the resolver to free any memory
//! allocated during use. All functions that take allocators should use
//! the same allocator.
const CodepointResolver = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const ziglyph = @import("ziglyph");
const font = @import("main.zig");
const Atlas = font.Atlas;
const CodepointMap = font.CodepointMap;
const Collection = font.Collection;
const Discover = font.Discover;
const DiscoveryDescriptor = font.discovery.Descriptor;
const Face = font.Face;
const Glyph = font.Glyph;
const Library = font.Library;
const Presentation = font.Presentation;
const RenderOptions = font.face.RenderOptions;
const SpriteFace = font.SpriteFace;
const Style = font.Style;
const log = std.log.scoped(.font_codepoint_resolver);
/// The underlying collection of fonts. This will be modified as
/// new fonts are found via the resolver. The resolver takes ownership
/// of the collection and will deinit it when it is deinitialized.
collection: Collection,
/// The set of statuses and whether they're enabled or not. This defaults
/// to true. This can be changed at runtime with no ill effect.
styles: StyleStatus = StyleStatus.initFill(true),
/// If discovery is available, we'll look up fonts where we can't find
/// the codepoint. This can be set after initialization.
discover: ?*Discover = null,
/// A map of codepoints to font requests for codepoint-level overrides.
/// The memory associated with the map is owned by the caller and is not
/// modified or freed by Group.
codepoint_map: ?CodepointMap = null,
/// The descriptor cache is used to cache the descriptor to font face
/// mapping for codepoint maps.
descriptor_cache: DescriptorCache = .{},
/// Set this to a non-null value to enable sprite glyph drawing. If this
/// isn't enabled we'll just fall through to trying to use regular fonts
/// to render sprite glyphs. But more than likely, if this isn't set then
/// terminal rendering will look wrong.
sprite: ?SpriteFace = null,
pub fn deinit(self: *CodepointResolver, alloc: Allocator) void {
self.collection.deinit(alloc);
self.descriptor_cache.deinit(alloc);
}
/// Looks up the font that should be used for a specific codepoint.
/// The font index is valid as long as font faces aren't removed. This
/// isn't cached; it is expected that downstream users handle caching if
/// that is important.
///
/// Optionally, a presentation format can be specified. This presentation
/// format will be preferred but if it can't be found in this format,
/// any format will be accepted. If presentation is null, the UCD
/// (Unicode Character Database) will be used to determine the default
/// presentation for the codepoint.
/// a code point.
///
/// An allocator is required because certain functionality (codepoint
/// mapping, fallback fonts, etc.) may require memory allocation. Curiously,
/// this function cannot error! If an error occurs for any reason, including
/// memory allocation, the associated functionality is ignored and the
/// resolver attempts to use a different method to satisfy the codepoint.
/// This behavior is intentional to make the resolver apply best-effort
/// logic to satisfy the codepoint since its better to render something
/// than nothing.
///
/// This logic is relatively complex so the exact algorithm is documented
/// here. If this gets out of sync with the code, ask questions.
///
/// 1. If a font style is requested that is disabled (i.e. bold),
/// we start over with the regular font style. The regular font style
/// cannot be disabled, but it can be replaced with a stylized font
/// face.
///
/// 2. If there is a codepoint override for the codepoint, we satisfy
/// that requirement if we can, no matter what style or presentation.
///
/// 3. If this is a sprite codepoint (such as an underline), then the
/// sprite font always is the result.
///
/// 4. If the exact style and presentation request can be satisfied by
/// one of our loaded fonts, we return that value. We search loaded
/// fonts in the order they're added to the group, so the caller must
/// set the priority order.
///
/// 5. If the style isn't regular, we restart this process at this point
/// but with the regular style. This lets us fall back to regular with
/// our loaded fonts before trying a fallback. We'd rather show a regular
/// version of a codepoint from a loaded font than find a new font in
/// the correct style because styles in other fonts often change
/// metrics like glyph widths.
///
/// 6. If the style is regular, and font discovery is enabled, we look
/// for a fallback font to satisfy our request.
///
/// 7. Finally, as a last resort, we fall back to restarting this whole
/// process with a regular font face satisfying ANY presentation for
/// the codepoint. If this fails, we return null.
///
pub fn getIndex(
self: *CodepointResolver,
alloc: Allocator,
cp: u32,
style: Style,
p: ?Presentation,
) ?Collection.Index {
// If we've disabled a font style, then fall back to regular.
if (style != .regular and !self.styles.get(style)) {
return self.getIndex(alloc, cp, .regular, p);
}
// Codepoint overrides.
if (self.getIndexCodepointOverride(alloc, cp)) |idx_| {
if (idx_) |idx| return idx;
} else |err| {
log.warn("codepoint override failed codepoint={} err={}", .{ cp, err });
}
// If we have sprite drawing enabled, check if our sprite face can
// handle this.
if (self.sprite) |sprite| {
if (sprite.hasCodepoint(cp, p)) {
return Collection.Index.initSpecial(.sprite);
}
}
// Build our presentation mode. If we don't have an explicit presentation
// given then we use the UCD (Unicode Character Database) to determine
// the default presentation. Note there is some inefficiency here because
// we'll do this muliple times if we recurse, but this is a cached function
// call higher up (GroupCache) so this should be rare.
const p_mode: Collection.PresentationMode = if (p) |v| .{ .explicit = v } else .{
.default = if (ziglyph.emoji.isEmojiPresentation(@intCast(cp)))
.emoji
else
.text,
};
// If we can find the exact value, then return that.
if (self.collection.getIndex(cp, style, p_mode)) |value| return value;
// If we're not a regular font style, try looking for a regular font
// that will satisfy this request. Blindly looking for unmatched styled
// fonts to satisfy one codepoint results in some ugly rendering.
if (style != .regular) {
if (self.getIndex(alloc, cp, .regular, p)) |value| return value;
}
// If we are regular, try looking for a fallback using discovery.
if (style == .regular and font.Discover != void) {
log.debug("searching for a fallback font for cp={X}", .{cp});
if (self.discover) |disco| discover: {
const load_opts = self.collection.load_options orelse
break :discover;
var disco_it = disco.discover(alloc, .{
.codepoint = cp,
.size = load_opts.size.points,
.bold = style == .bold or style == .bold_italic,
.italic = style == .italic or style == .bold_italic,
.monospace = false,
}) catch break :discover;
defer disco_it.deinit();
while (true) {
var deferred_face = (disco_it.next() catch |err| {
log.warn("fallback search failed with error err={}", .{err});
break;
}) orelse break;
// Discovery is supposed to only return faces that have our
// codepoint but we can't search presentation in discovery so
// we have to check it here.
const face: Collection.Entry = .{ .fallback_deferred = deferred_face };
if (!face.hasCodepoint(cp, p_mode)) {
deferred_face.deinit();
continue;
}
var buf: [256]u8 = undefined;
log.info("found codepoint 0x{X} in fallback face={s}", .{
cp,
deferred_face.name(&buf) catch "<error>",
});
return self.collection.add(alloc, style, face) catch {
deferred_face.deinit();
break :discover;
};
}
log.debug("no fallback face found for cp={X}", .{cp});
}
}
// If this is already regular, we're done falling back.
if (style == .regular and p == null) return null;
// For non-regular fonts, we fall back to regular with any presentation
return self.collection.getIndex(cp, .regular, .{ .any = {} });
}
/// Checks if the codepoint is in the map of codepoint overrides,
/// finds the override font, and returns it.
fn getIndexCodepointOverride(
self: *CodepointResolver,
alloc: Allocator,
cp: u32,
) !?Collection.Index {
// If discovery is disabled then we can't do codepoint overrides
// since the override is based on discovery to find the font.
if (comptime font.Discover == void) return null;
// Get our codepoint map. If we have no map set then we have no
// codepoint overrides and we're done.
const map = self.codepoint_map orelse return null;
// If we have a codepoint too large or isn't in the map, then we
// don't have an override. The map returns a descriptor that can be
// used with font discovery to search for a matching font.
const cp_u21 = std.math.cast(u21, cp) orelse return null;
const desc = map.get(cp_u21) orelse return null;
// Fast path: the descriptor is already loaded. This means that we
// already did the search before and we have an exact font for this
// codepoint.
const idx_: ?Collection.Index = self.descriptor_cache.get(desc) orelse idx: {
// Slow path: we have to find this descriptor and load the font
const discover = self.discover orelse return null;
var disco_it = try discover.discover(alloc, desc);
defer disco_it.deinit();
const face = (try disco_it.next()) orelse {
log.warn(
"font lookup for codepoint map failed codepoint={} err=FontNotFound",
.{cp},
);
// Add null to the cache so we don't do a lookup again later.
try self.descriptor_cache.put(alloc, desc, null);
return null;
};
// Add the font to our list of fonts so we can get an index for it,
// and ensure the index is stored in the descriptor cache for next time.
const idx = try self.collection.add(
alloc,
.regular,
.{ .deferred = face },
);
try self.descriptor_cache.put(alloc, desc, idx);
break :idx idx;
};
// The descriptor cache will populate null if the descriptor is not found
// to avoid expensive discoveries later, so if it is null then we already
// searched and found nothing.
const idx = idx_ orelse return null;
// We need to verify that this index has the codepoint we want.
if (self.collection.hasCodepoint(idx, cp, .{ .any = {} })) {
log.debug("codepoint override based on config codepoint={} family={s}", .{
cp,
desc.family orelse "",
});
return idx;
}
return null;
}
/// Returns the presentation for a specific font index. This is useful for
/// determining what atlas is needed.
pub fn getPresentation(self: *CodepointResolver, index: Collection.Index) !Presentation {
if (index.special()) |sp| return switch (sp) {
.sprite => .text,
};
const face = try self.collection.getFace(index);
return face.presentation;
}
/// Render a glyph by glyph index into the given font atlas and return
/// metadata about it.
///
/// This performs no caching, it is up to the caller to cache calls to this
/// if they want. This will also not resize the atlas if it is full.
///
/// IMPORTANT: this renders by /glyph index/ and not by /codepoint/. The caller
/// is expected to translate codepoints to glyph indexes in some way. The most
/// trivial way to do this is to get the Face and call glyphIndex. If you're
/// doing text shaping, the text shaping library (i.e. HarfBuzz) will automatically
/// determine glyph indexes for a text run.
pub fn renderGlyph(
self: *CodepointResolver,
alloc: Allocator,
atlas: *Atlas,
index: Collection.Index,
glyph_index: u32,
opts: RenderOptions,
) !Glyph {
// Special-case fonts are rendered directly.
if (index.special()) |sp| switch (sp) {
.sprite => return try self.sprite.?.renderGlyph(
alloc,
atlas,
glyph_index,
opts,
),
};
const face = try self.collection.getFace(index);
const glyph = try face.renderGlyph(alloc, atlas, glyph_index, opts);
// log.warn("GLYPH={}", .{glyph});
return glyph;
}
/// Packed array of booleans to indicate if a style is enabled or not.
pub const StyleStatus = std.EnumArray(Style, bool);
/// Map of descriptors to faces. This is used with manual codepoint maps
/// to ensure that we don't load the same font multiple times.
///
/// Note that the current implementation will load the same font multiple
/// times if the font used for a codepoint map is identical to a font used
/// for a regular style. That's just an inefficient choice made now because
/// the implementation is simpler and codepoint maps matching a regular
/// font is a rare case.
const DescriptorCache = std.HashMapUnmanaged(
DiscoveryDescriptor,
?Collection.Index,
struct {
const KeyType = DiscoveryDescriptor;
pub fn hash(ctx: @This(), k: KeyType) u64 {
_ = ctx;
return k.hashcode();
}
pub fn eql(ctx: @This(), a: KeyType, b: KeyType) bool {
// Note that this means its possible to have two different
// descriptors match when there is a hash collision so we
// should button this up later.
return ctx.hash(a) == ctx.hash(b);
}
},
std.hash_map.default_max_load_percentage,
);
test getIndex {
const testing = std.testing;
const alloc = testing.allocator;
const testFont = @import("test.zig").fontRegular;
const testEmoji = @import("test.zig").fontEmoji;
const testEmojiText = @import("test.zig").fontEmojiText;
var lib = try Library.init();
defer lib.deinit();
var c = try Collection.init(alloc);
c.load_options = .{ .library = lib };
_ = try c.add(alloc, .regular, .{ .loaded = try Face.init(
lib,
testFont,
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
) });
if (font.options.backend != .coretext) {
// Coretext doesn't support Noto's format
_ = try c.add(
alloc,
.regular,
.{ .loaded = try Face.init(
lib,
testEmoji,
.{ .size = .{ .points = 12 } },
) },
);
}
_ = try c.add(
alloc,
.regular,
.{ .loaded = try Face.init(
lib,
testEmojiText,
.{ .size = .{ .points = 12 } },
) },
);
var r: CodepointResolver = .{ .collection = c };
defer r.deinit(alloc);
// Should find all visible ASCII
var i: u32 = 32;
while (i < 127) : (i += 1) {
const idx = r.getIndex(alloc, i, .regular, null).?;
try testing.expectEqual(Style.regular, idx.style);
try testing.expectEqual(@as(Collection.Index.IndexInt, 0), idx.idx);
}
// Try emoji
{
const idx = r.getIndex(alloc, '🥸', .regular, null).?;
try testing.expectEqual(Style.regular, idx.style);
try testing.expectEqual(@as(Collection.Index.IndexInt, 1), idx.idx);
}
// Try text emoji
{
const idx = r.getIndex(alloc, 0x270C, .regular, .text).?;
try testing.expectEqual(Style.regular, idx.style);
const text_idx = if (font.options.backend == .coretext) 1 else 2;
try testing.expectEqual(@as(Collection.Index.IndexInt, text_idx), idx.idx);
}
{
const idx = r.getIndex(alloc, 0x270C, .regular, .emoji).?;
try testing.expectEqual(Style.regular, idx.style);
try testing.expectEqual(@as(Collection.Index.IndexInt, 1), idx.idx);
}
// Box glyph should be null since we didn't set a box font
{
try testing.expect(r.getIndex(alloc, 0x1FB00, .regular, null) == null);
}
}
test "getIndex disabled font style" {
const testing = std.testing;
const alloc = testing.allocator;
const testFont = @import("test.zig").fontRegular;
var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale);
defer atlas_greyscale.deinit(alloc);
var lib = try Library.init();
defer lib.deinit();
var c = try Collection.init(alloc);
c.load_options = .{ .library = lib };
_ = try c.add(alloc, .regular, .{ .loaded = try Face.init(
lib,
testFont,
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
) });
_ = try c.add(alloc, .bold, .{ .loaded = try Face.init(
lib,
testFont,
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
) });
_ = try c.add(alloc, .italic, .{ .loaded = try Face.init(
lib,
testFont,
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
) });
var r: CodepointResolver = .{ .collection = c };
defer r.deinit(alloc);
r.styles.set(.bold, false); // Disable bold
// Regular should work fine
{
const idx = r.getIndex(alloc, 'A', .regular, null).?;
try testing.expectEqual(Style.regular, idx.style);
try testing.expectEqual(@as(Collection.Index.IndexInt, 0), idx.idx);
}
// Bold should go to regular
{
const idx = r.getIndex(alloc, 'A', .bold, null).?;
try testing.expectEqual(Style.regular, idx.style);
try testing.expectEqual(@as(Collection.Index.IndexInt, 0), idx.idx);
}
// Italic should still work
{
const idx = r.getIndex(alloc, 'A', .italic, null).?;
try testing.expectEqual(Style.italic, idx.style);
try testing.expectEqual(@as(Collection.Index.IndexInt, 0), idx.idx);
}
}
test "getIndex box glyph" {
const testing = std.testing;
const alloc = testing.allocator;
var lib = try Library.init();
defer lib.deinit();
const c = try Collection.init(alloc);
var r: CodepointResolver = .{
.collection = c,
.sprite = .{ .width = 18, .height = 36, .thickness = 2 },
};
defer r.deinit(alloc);
// Should find a box glyph
const idx = r.getIndex(alloc, 0x2500, .regular, null).?;
try testing.expectEqual(Style.regular, idx.style);
try testing.expectEqual(@intFromEnum(Collection.Index.Special.sprite), idx.idx);
}

649
src/font/Collection.zig Normal file
View File

@ -0,0 +1,649 @@
//! A font collection is a list of faces of different styles. The list is
//! ordered by priority (per style). All fonts in a collection share the same
//! size so they can be used interchangeably in cases a glyph is missing in one
//! and present in another.
//!
//! The purpose of a collection is to store a list of fonts by style
//! and priority order. A collection does not handle searching for font
//! callbacks, rasterization, etc. For this, see CodepointResolver.
//!
//! The collection can contain both loaded and deferred faces. Deferred faces
//! typically use less memory while still providing some necessary information
//! such as codepoint support, presentation, etc. This is useful for looking
//! for fallback fonts as efficiently as possible. For example, when the glyph
//! "X" is not found, we can quickly search through deferred fonts rather
//! than loading the font completely.
const Collection = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const font = @import("main.zig");
const options = font.options;
const DeferredFace = font.DeferredFace;
const DesiredSize = font.face.DesiredSize;
const Face = font.Face;
const Library = font.Library;
const Metrics = font.face.Metrics;
const Presentation = font.Presentation;
const Style = font.Style;
const log = std.log.scoped(.font_collection);
/// The available faces we have. This shouldn't be modified manually.
/// Instead, use the functions available on Collection.
faces: StyleArray,
/// The load options for deferred faces in the face list. If this
/// is not set, then deferred faces will not be loaded. Attempting to
/// add a deferred face will result in an error.
load_options: ?LoadOptions = null,
/// Initialize an empty collection.
pub fn init(
alloc: Allocator,
) !Collection {
// Initialize our styles array, preallocating some space that is
// likely to be used.
var faces = StyleArray.initFill(.{});
for (&faces.values) |*v| try v.ensureTotalCapacityPrecise(alloc, 2);
return .{ .faces = faces };
}
pub fn deinit(self: *Collection, alloc: Allocator) void {
var it = self.faces.iterator();
while (it.next()) |entry| {
for (entry.value.items) |*item| item.deinit();
entry.value.deinit(alloc);
}
if (self.load_options) |*v| v.deinit(alloc);
}
pub const AddError = Allocator.Error || error{
CollectionFull,
DeferredLoadingUnavailable,
};
/// Add a face to the collection for the given style. This face will be added
/// next in priority if others exist already, i.e. it'll be the _last_ to be
/// searched for a glyph in that list.
///
/// The collection takes ownership of the face. The face will be deallocated
/// when the collection is deallocated.
///
/// If a loaded face is added to the collection, it should be the same
/// size as all the other faces in the collection. This function will not
/// verify or modify the size until the size of the entire collection is
/// changed.
pub fn add(
self: *Collection,
alloc: Allocator,
style: Style,
face: Entry,
) AddError!Index {
const list = self.faces.getPtr(style);
// We have some special indexes so we must never pass those.
if (list.items.len >= Index.Special.start - 1)
return error.CollectionFull;
// If this is deferred and we don't have load options, we can't.
if (face.isDeferred() and self.load_options == null)
return error.DeferredLoadingUnavailable;
const idx = list.items.len;
try list.append(alloc, face);
return .{ .style = style, .idx = @intCast(idx) };
}
/// Return the Face represented by a given Index. The returned pointer
/// is only valid as long as this collection is not modified.
///
/// This will initialize the face if it is deferred and not yet loaded,
/// which can fail.
pub fn getFace(self: *Collection, index: Index) !*Face {
if (index.special() != null) return error.SpecialHasNoFace;
const list = self.faces.getPtr(index.style);
const item = &list.items[index.idx];
return switch (item.*) {
inline .deferred, .fallback_deferred => |*d, tag| deferred: {
const opts = self.load_options orelse
return error.DeferredLoadingUnavailable;
const face = try d.load(opts.library, opts.faceOptions());
d.deinit();
item.* = switch (tag) {
.deferred => .{ .loaded = face },
.fallback_deferred => .{ .fallback_loaded = face },
else => unreachable,
};
break :deferred switch (tag) {
.deferred => &item.loaded,
.fallback_deferred => &item.fallback_loaded,
else => unreachable,
};
},
.loaded, .fallback_loaded => |*f| f,
};
}
/// Return the index of the font in this collection that contains
/// the given codepoint, style, and presentation. If no font is found,
/// null is returned.
///
/// This does not trigger font loading; deferred fonts can be
/// searched for codepoints.
pub fn getIndex(
self: *const Collection,
cp: u32,
style: Style,
p_mode: PresentationMode,
) ?Index {
for (self.faces.get(style).items, 0..) |elem, i| {
if (elem.hasCodepoint(cp, p_mode)) {
return .{
.style = style,
.idx = @intCast(i),
};
}
}
// Not found
return null;
}
/// Check if a specific font index has a specific codepoint. This does not
/// necessarily force the font to load. The presentation value "p" will
/// verify the Emoji representation matches if it is non-null. If "p" is
/// null then any presentation will be accepted.
pub fn hasCodepoint(
self: *const Collection,
index: Index,
cp: u32,
p_mode: PresentationMode,
) bool {
const list = self.faces.get(index.style);
if (index.idx >= list.items.len) return false;
return list.items[index.idx].hasCodepoint(cp, p_mode);
}
/// Automatically create an italicized font from the regular
/// font face if we don't have one already. If we already have
/// an italicized font face, this does nothing.
pub fn autoItalicize(self: *Collection, alloc: Allocator) !void {
// If we have an italic font, do nothing.
const italic_list = self.faces.getPtr(.italic);
if (italic_list.items.len > 0) return;
// Not all font backends support auto-italicization.
if (comptime !@hasDecl(Face, "italicize")) {
log.warn(
"no italic font face available, italics will not render",
.{},
);
return;
}
// Our regular font. If we have no regular font we also do nothing.
const regular = regular: {
const list = self.faces.get(.regular);
if (list.items.len == 0) return;
// Find our first font that is text. This will force
// loading any deferred faces but we only load them until
// we find a text face. A text face is almost always the
// first face in the list.
for (0..list.items.len) |i| {
const face = try self.getFace(.{
.style = .regular,
.idx = @intCast(i),
});
if (face.presentation == .text) break :regular face;
}
// No regular text face found.
return;
};
// We require loading options to auto-italicize.
const opts = self.load_options orelse return error.DeferredLoadingUnavailable;
// Try to italicize it.
const face = try regular.italicize(opts.faceOptions());
try italic_list.append(alloc, .{ .loaded = face });
var buf: [256]u8 = undefined;
if (face.name(&buf)) |name| {
log.info("font auto-italicized: {s}", .{name});
} else |_| {}
}
/// Update the size of all faces in the collection. This will
/// also update the size in the load options for future deferred
/// face loading.
///
/// This requires load options to be set.
pub fn setSize(self: *Collection, size: DesiredSize) !void {
// Get a pointer to our options so we can modify the size.
const opts = if (self.load_options) |*v|
v
else
return error.DeferredLoadingUnavailable;
opts.size = size;
// Resize all our faces that are loaded
var it = self.faces.iterator();
while (it.next()) |entry| {
for (entry.value.items) |*elem| switch (elem.*) {
.deferred, .fallback_deferred => continue,
.loaded, .fallback_loaded => |*f| try f.setSize(
opts.faceOptions(),
),
};
}
}
/// Packed array of all Style enum cases mapped to a growable list of faces.
///
/// We use this data structure because there aren't many styles and all
/// styles are typically loaded for a terminal session. The overhead per
/// style even if it is not used or barely used is minimal given the
/// small style count.
const StyleArray = std.EnumArray(Style, std.ArrayListUnmanaged(Entry));
/// Load options are used to configure all the details a Collection
/// needs to load deferred faces.
pub const LoadOptions = struct {
/// The library to use for loading faces. This is not owned by
/// the collection and can be used by multiple collections. When
/// deinitializing the collection, the library is not deinitialized.
library: Library,
/// The desired font size for all loaded faces.
size: DesiredSize = .{ .points = 12 },
/// The metric modifiers to use for all loaded faces. The memory
/// for this is owned by the user and is not freed by the collection.
metric_modifiers: Metrics.ModifierSet = .{},
pub fn deinit(self: *LoadOptions, alloc: Allocator) void {
_ = self;
_ = alloc;
}
/// The options to use for loading faces.
pub fn faceOptions(self: *const LoadOptions) font.face.Options {
return .{
.size = self.size,
.metric_modifiers = &self.metric_modifiers,
};
}
};
/// A entry in a collection can be deferred or loaded. A deferred face
/// is not yet fully loaded and only represents the font descriptor
/// and usually uses less resources. A loaded face is fully parsed,
/// ready to rasterize, and usually uses more resources than a
/// deferred version.
///
/// A face can also be a "fallback" variant that is still either
/// deferred or loaded. Today, there is only one difference between
/// fallback and non-fallback (or "explicit") faces: the handling
/// of emoji presentation.
///
/// For explicit faces, when an explicit emoji presentation is
/// not requested, we will use any glyph for that codepoint found
/// even if the font presentation does not match the UCD
/// (Unicode Character Database) value. When an explicit presentation
/// is requested (via either VS15/V16), that is always honored.
/// The reason we do this is because we assume that if a user
/// explicitly chosen a font face (hence it is "explicit" and
/// not "fallback"), they want to use any glyphs possible within that
/// font face. Fallback fonts on the other hand are picked as a
/// last resort, so we should prefer exactness if possible.
pub const Entry = union(enum) {
deferred: DeferredFace, // Not loaded
loaded: Face, // Loaded, explicit use
// The same as deferred/loaded but fallback font semantics (see large
// comment above Entry).
fallback_deferred: DeferredFace,
fallback_loaded: Face,
pub fn deinit(self: *Entry) void {
switch (self.*) {
inline .deferred,
.loaded,
.fallback_deferred,
.fallback_loaded,
=> |*v| v.deinit(),
}
}
/// True if the entry is deferred.
fn isDeferred(self: Entry) bool {
return switch (self) {
.deferred, .fallback_deferred => true,
.loaded, .fallback_loaded => false,
};
}
/// True if this face satisfies the given codepoint and presentation.
pub fn hasCodepoint(
self: Entry,
cp: u32,
p_mode: PresentationMode,
) bool {
return switch (self) {
// Non-fallback fonts require explicit presentation matching but
// otherwise don't care about presentation
.deferred => |v| switch (p_mode) {
.explicit => |p| v.hasCodepoint(cp, p),
.default, .any => v.hasCodepoint(cp, null),
},
.loaded => |face| switch (p_mode) {
.explicit => |p| face.presentation == p and face.glyphIndex(cp) != null,
.default, .any => face.glyphIndex(cp) != null,
},
// Fallback fonts require exact presentation matching.
.fallback_deferred => |v| switch (p_mode) {
.explicit, .default => |p| v.hasCodepoint(cp, p),
.any => v.hasCodepoint(cp, null),
},
.fallback_loaded => |face| switch (p_mode) {
.explicit,
.default,
=> |p| face.presentation == p and face.glyphIndex(cp) != null,
.any => face.glyphIndex(cp) != null,
},
};
}
};
/// The requested presentation for a codepoint.
pub const PresentationMode = union(enum) {
/// The codepoint has an explicit presentation that is required,
/// i.e. VS15/V16.
explicit: Presentation,
/// The codepoint has no explicit presentation and we should use
/// the presentation from the UCd.
default: Presentation,
/// The codepoint can be any presentation.
any: void,
};
/// This represents a specific font in the collection.
///
/// The backing size of this packed struct represents the total number
/// of possible usable fonts in a collection. And the number of bits
/// used for the index and not the style represents the total number
/// of possible usable fonts for a given style.
///
/// The goal is to keep the size of this struct as small as practical. We
/// accept the limitations that this imposes so long as they're reasonable.
/// At the time of writing this comment, this is a 16-bit struct with 13
/// bits used for the index, supporting up to 8192 fonts per style. This
/// seems more than reasonable. There are synthetic scenarios where this
/// could be a limitation but I can't think of any that are practical.
///
/// If you somehow need more fonts per style, you can increase the size of
/// the Backing type and everything should just work fine.
pub const Index = packed struct(Index.Backing) {
const Backing = u16;
const backing_bits = @typeInfo(Backing).Int.bits;
/// The number of bits we use for the index.
const idx_bits = backing_bits - @typeInfo(@typeInfo(Style).Enum.tag_type).Int.bits;
pub const IndexInt = @Type(.{ .Int = .{ .signedness = .unsigned, .bits = idx_bits } });
/// The special-case fonts that we support.
pub const Special = enum(IndexInt) {
// We start all special fonts at this index so they can be detected.
pub const start = std.math.maxInt(IndexInt);
/// Sprite drawing, this is rendered JIT using 2D graphics APIs.
sprite = start,
};
style: Style = .regular,
idx: IndexInt = 0,
/// Initialize a special font index.
pub fn initSpecial(v: Special) Index {
return .{ .style = .regular, .idx = @intFromEnum(v) };
}
/// Convert to int
pub fn int(self: Index) Backing {
return @bitCast(self);
}
/// Returns true if this is a "special" index which doesn't map to
/// a real font face. We can still render it but there is no face for
/// this font.
pub fn special(self: Index) ?Special {
if (self.idx < Special.start) return null;
return @enumFromInt(self.idx);
}
test {
// We never want to take up more than a byte since font indexes are
// everywhere so if we increase the size of this we'll dramatically
// increase our memory usage.
try std.testing.expectEqual(@sizeOf(Backing), @sizeOf(Index));
// Just so we're aware when this changes. The current maximum number
// of fonts for a style is 13 bits or 8192 fonts.
try std.testing.expectEqual(13, idx_bits);
}
};
test init {
const testing = std.testing;
const alloc = testing.allocator;
var c = try init(alloc);
defer c.deinit(alloc);
}
test "add full" {
const testing = std.testing;
const alloc = testing.allocator;
const testFont = @import("test.zig").fontRegular;
var lib = try Library.init();
defer lib.deinit();
var c = try init(alloc);
defer c.deinit(alloc);
for (0..Index.Special.start - 1) |_| {
_ = try c.add(alloc, .regular, .{ .loaded = try Face.init(
lib,
testFont,
.{ .size = .{ .points = 12 } },
) });
}
try testing.expectError(error.CollectionFull, c.add(
alloc,
.regular,
.{ .loaded = try Face.init(
lib,
testFont,
.{ .size = .{ .points = 12 } },
) },
));
}
test "add deferred without loading options" {
const testing = std.testing;
const alloc = testing.allocator;
var c = try init(alloc);
defer c.deinit(alloc);
try testing.expectError(error.DeferredLoadingUnavailable, c.add(
alloc,
.regular,
// This can be undefined because it should never be accessed.
.{ .deferred = undefined },
));
}
test getFace {
const testing = std.testing;
const alloc = testing.allocator;
const testFont = @import("test.zig").fontRegular;
var lib = try Library.init();
defer lib.deinit();
var c = try init(alloc);
defer c.deinit(alloc);
const idx = try c.add(alloc, .regular, .{ .loaded = try Face.init(
lib,
testFont,
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
) });
{
const face1 = try c.getFace(idx);
const face2 = try c.getFace(idx);
try testing.expectEqual(@intFromPtr(face1), @intFromPtr(face2));
}
}
test getIndex {
const testing = std.testing;
const alloc = testing.allocator;
const testFont = @import("test.zig").fontRegular;
var lib = try Library.init();
defer lib.deinit();
var c = try init(alloc);
defer c.deinit(alloc);
_ = try c.add(alloc, .regular, .{ .loaded = try Face.init(
lib,
testFont,
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
) });
// Should find all visible ASCII
var i: u32 = 32;
while (i < 127) : (i += 1) {
const idx = c.getIndex(i, .regular, .{ .any = {} });
try testing.expect(idx != null);
}
// Should not find emoji
{
const idx = c.getIndex('🥸', .regular, .{ .any = {} });
try testing.expect(idx == null);
}
}
test autoItalicize {
if (comptime !@hasDecl(Face, "italicize")) return error.SkipZigTest;
const testing = std.testing;
const alloc = testing.allocator;
const testFont = @import("test.zig").fontRegular;
var lib = try Library.init();
defer lib.deinit();
var c = try init(alloc);
defer c.deinit(alloc);
c.load_options = .{ .library = lib };
_ = try c.add(alloc, .regular, .{ .loaded = try Face.init(
lib,
testFont,
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
) });
try testing.expect(c.getIndex('A', .italic, .{ .any = {} }) == null);
try c.autoItalicize(alloc);
try testing.expect(c.getIndex('A', .italic, .{ .any = {} }) != null);
}
test setSize {
const testing = std.testing;
const alloc = testing.allocator;
const testFont = @import("test.zig").fontRegular;
var lib = try Library.init();
defer lib.deinit();
var c = try init(alloc);
defer c.deinit(alloc);
c.load_options = .{ .library = lib };
_ = try c.add(alloc, .regular, .{ .loaded = try Face.init(
lib,
testFont,
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
) });
try testing.expectEqual(@as(u32, 12), c.load_options.?.size.points);
try c.setSize(.{ .points = 24 });
try testing.expectEqual(@as(u32, 24), c.load_options.?.size.points);
}
test hasCodepoint {
const testing = std.testing;
const alloc = testing.allocator;
const testFont = @import("test.zig").fontRegular;
var lib = try Library.init();
defer lib.deinit();
var c = try init(alloc);
defer c.deinit(alloc);
c.load_options = .{ .library = lib };
const idx = try c.add(alloc, .regular, .{ .loaded = try Face.init(
lib,
testFont,
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
) });
try testing.expect(c.hasCodepoint(idx, 'A', .{ .any = {} }));
try testing.expect(!c.hasCodepoint(idx, '🥸', .{ .any = {} }));
}
test "hasCodepoint emoji default graphical" {
if (options.backend != .fontconfig_freetype) return error.SkipZigTest;
const testing = std.testing;
const alloc = testing.allocator;
const testEmoji = @import("test.zig").fontEmoji;
var lib = try Library.init();
defer lib.deinit();
var c = try init(alloc);
defer c.deinit(alloc);
c.load_options = .{ .library = lib };
const idx = try c.add(alloc, .regular, .{ .loaded = try Face.init(
lib,
testEmoji,
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
) });
try testing.expect(!c.hasCodepoint(idx, 'A', .{ .any = {} }));
try testing.expect(c.hasCodepoint(idx, '🥸', .{ .any = {} }));
// TODO(fontmem): test explicit/implicit
}

View File

@ -19,17 +19,6 @@ const Presentation = @import("main.zig").Presentation;
const log = std.log.scoped(.deferred_face);
/// The struct used for deferred face state.
///
/// TODO: Change the "fc", "ct", "wc" fields in @This to just use one field
/// with the state since there should be no world in which multiple are used.
const FaceState = switch (options.backend) {
.freetype => void,
.fontconfig_freetype => Fontconfig,
.coretext_freetype, .coretext => CoreText,
.web_canvas => WebCanvas,
};
/// Fontconfig
fc: if (options.backend == .fontconfig_freetype) ?Fontconfig else void =
if (options.backend == .fontconfig_freetype) null else {},

File diff suppressed because it is too large Load Diff

View File

@ -1,381 +0,0 @@
//! A glyph cache sits on top of a Group and caches the results from it.
const GroupCache = @This();
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const font = @import("main.zig");
const Face = @import("main.zig").Face;
const DeferredFace = @import("main.zig").DeferredFace;
const Library = @import("main.zig").Library;
const Glyph = @import("main.zig").Glyph;
const Style = @import("main.zig").Style;
const Group = @import("main.zig").Group;
const Presentation = @import("main.zig").Presentation;
const log = std.log.scoped(.font_groupcache);
/// Cache for codepoints to font indexes in a group.
codepoints: std.AutoHashMapUnmanaged(CodepointKey, ?Group.FontIndex) = .{},
/// Cache for glyph renders.
glyphs: std.AutoHashMapUnmanaged(GlyphKey, Glyph) = .{},
/// The underlying font group. Users are expected to use this directly
/// to setup the group or make changes. Beware some changes require a reset
/// (see reset).
group: Group,
/// The texture atlas to store renders in. The GroupCache has to store these
/// because the cached Glyph result is dependent on the Atlas.
atlas_greyscale: font.Atlas,
atlas_color: font.Atlas,
const CodepointKey = struct {
style: Style,
codepoint: u32,
presentation: ?Presentation,
};
const GlyphKey = struct {
index: Group.FontIndex,
glyph: u32,
opts: font.face.RenderOptions,
};
/// The GroupCache takes ownership of Group and will free it.
pub fn init(alloc: Allocator, group: Group) !GroupCache {
var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale);
errdefer atlas_greyscale.deinit(alloc);
var atlas_color = try font.Atlas.init(alloc, 512, .rgba);
errdefer atlas_color.deinit(alloc);
var result: GroupCache = .{
.group = group,
.atlas_greyscale = atlas_greyscale,
.atlas_color = atlas_color,
};
// We set an initial capacity that can fit a good number of characters.
// This number was picked empirically based on my own terminal usage.
try result.codepoints.ensureTotalCapacity(alloc, 128);
try result.glyphs.ensureTotalCapacity(alloc, 128);
return result;
}
pub fn deinit(self: *GroupCache, alloc: Allocator) void {
self.codepoints.deinit(alloc);
self.glyphs.deinit(alloc);
self.atlas_greyscale.deinit(alloc);
self.atlas_color.deinit(alloc);
self.group.deinit();
}
/// Reset the cache. This should be called:
///
/// - If an Atlas was reset
/// - If a font group font size was changed
/// - If a font group font set was changed
///
pub fn reset(self: *GroupCache) void {
self.codepoints.clearRetainingCapacity();
self.glyphs.clearRetainingCapacity();
}
/// Resize the fonts in the group. This will clear the cache.
pub fn setSize(self: *GroupCache, size: font.face.DesiredSize) !void {
try self.group.setSize(size);
// Reset our internal state
self.reset();
// Clear our atlases
self.atlas_greyscale.clear();
self.atlas_color.clear();
}
/// Get the font index for a given codepoint. This is cached.
pub fn indexForCodepoint(
self: *GroupCache,
alloc: Allocator,
cp: u32,
style: Style,
p: ?Presentation,
) !?Group.FontIndex {
const key: CodepointKey = .{ .style = style, .codepoint = cp, .presentation = p };
const gop = try self.codepoints.getOrPut(alloc, key);
// If it is in the cache, use it.
if (gop.found_existing) return gop.value_ptr.*;
// Load a value and cache it. This even caches negative matches.
const value = self.group.indexForCodepoint(cp, style, p);
gop.value_ptr.* = value;
return value;
}
/// Render a glyph. This automatically determines the correct texture
/// atlas to use and caches the result.
pub fn renderGlyph(
self: *GroupCache,
alloc: Allocator,
index: Group.FontIndex,
glyph_index: u32,
opts: font.face.RenderOptions,
) !Glyph {
const key: GlyphKey = .{ .index = index, .glyph = glyph_index, .opts = opts };
const gop = try self.glyphs.getOrPut(alloc, key);
// If it is in the cache, use it.
if (gop.found_existing) return gop.value_ptr.*;
// Uncached, render it
const atlas: *font.Atlas = switch (try self.group.presentationFromIndex(index)) {
.text => &self.atlas_greyscale,
.emoji => &self.atlas_color,
};
const glyph = self.group.renderGlyph(
alloc,
atlas,
index,
glyph_index,
opts,
) catch |err| switch (err) {
// If the atlas is full, we resize it
error.AtlasFull => blk: {
try atlas.grow(alloc, atlas.size * 2);
break :blk try self.group.renderGlyph(
alloc,
atlas,
index,
glyph_index,
opts,
);
},
else => return err,
};
// Cache and return
gop.value_ptr.* = glyph;
return glyph;
}
test {
const testing = std.testing;
const alloc = testing.allocator;
const testFont = @import("test.zig").fontRegular;
// const testEmoji = @import("test.zig").fontEmoji;
var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale);
defer atlas_greyscale.deinit(alloc);
var lib = try Library.init();
defer lib.deinit();
var cache = try init(alloc, try Group.init(
alloc,
lib,
.{ .points = 12 },
));
defer cache.deinit(alloc);
// Setup group
_ = try cache.group.addFace(
.regular,
.{ .loaded = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } }) },
);
var group = cache.group;
// Visible ASCII. Do it twice to verify cache.
var i: u32 = 32;
while (i < 127) : (i += 1) {
const idx = (try cache.indexForCodepoint(alloc, i, .regular, null)).?;
try testing.expectEqual(Style.regular, idx.style);
try testing.expectEqual(@as(Group.FontIndex.IndexInt, 0), idx.idx);
// Render
const face = try cache.group.faceFromIndex(idx);
const glyph_index = face.glyphIndex(i).?;
_ = try cache.renderGlyph(
alloc,
idx,
glyph_index,
.{},
);
}
// Do it again, but reset the group so that we know for sure its not hitting it
{
cache.group = undefined;
defer cache.group = group;
i = 32;
while (i < 127) : (i += 1) {
const idx = (try cache.indexForCodepoint(alloc, i, .regular, null)).?;
try testing.expectEqual(Style.regular, idx.style);
try testing.expectEqual(@as(Group.FontIndex.IndexInt, 0), idx.idx);
// Render
const face = try group.faceFromIndex(idx);
const glyph_index = face.glyphIndex(i).?;
_ = try cache.renderGlyph(
alloc,
idx,
glyph_index,
.{},
);
}
}
}
/// The wasm-compatible API.
pub const Wasm = struct {
const wasm = @import("../os/wasm.zig");
const alloc = wasm.alloc;
export fn group_cache_new(group: *Group) ?*GroupCache {
return group_cache_new_(group) catch null;
}
fn group_cache_new_(group: *Group) !*GroupCache {
var gc = try GroupCache.init(alloc, group.*);
errdefer gc.deinit(alloc);
const result = try alloc.create(GroupCache);
errdefer alloc.destroy(result);
result.* = gc;
return result;
}
export fn group_cache_free(ptr: ?*GroupCache) void {
if (ptr) |v| {
v.deinit(alloc);
alloc.destroy(v);
}
}
export fn group_cache_set_size(self: *GroupCache, size: u16) void {
return self.setSize(.{ .points = size }) catch |err| {
log.warn("error setting group cache size err={}", .{err});
return;
};
}
/// Presentation is negative for doesn't matter.
export fn group_cache_index_for_codepoint(self: *GroupCache, cp: u32, style: u16, p: i16) i16 {
const presentation: ?Presentation = if (p < 0) null else @enumFromInt(p);
if (self.indexForCodepoint(
alloc,
cp,
@enumFromInt(style),
presentation,
)) |idx| {
return @intCast(@as(u8, @bitCast(idx orelse return -1)));
} else |err| {
log.warn("error getting index for codepoint from group cache size err={}", .{err});
return -1;
}
}
export fn group_cache_render_glyph(
self: *GroupCache,
idx: i16,
cp: u32,
max_height: u16,
) ?*Glyph {
return group_cache_render_glyph_(self, idx, cp, max_height) catch |err| {
log.warn("error rendering group cache glyph err={}", .{err});
return null;
};
}
fn group_cache_render_glyph_(
self: *GroupCache,
idx_: i16,
cp: u32,
max_height_: u16,
) !*Glyph {
const idx = @as(Group.FontIndex, @bitCast(@as(u8, @intCast(idx_))));
const max_height = if (max_height_ <= 0) null else max_height_;
const glyph = try self.renderGlyph(alloc, idx, cp, .{
.max_height = max_height,
});
const result = try alloc.create(Glyph);
errdefer alloc.destroy(result);
result.* = glyph;
return result;
}
export fn group_cache_atlas_greyscale(self: *GroupCache) *font.Atlas {
return &self.atlas_greyscale;
}
export fn group_cache_atlas_color(self: *GroupCache) *font.Atlas {
return &self.atlas_color;
}
};
test "resize" {
const testing = std.testing;
const alloc = testing.allocator;
const testFont = @import("test.zig").fontRegular;
// const testEmoji = @import("test.zig").fontEmoji;
var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale);
defer atlas_greyscale.deinit(alloc);
var lib = try Library.init();
defer lib.deinit();
var cache = try init(alloc, try Group.init(
alloc,
lib,
.{ .points = 12 },
));
defer cache.deinit(alloc);
// Setup group
_ = try cache.group.addFace(
.regular,
.{ .loaded = try Face.init(
lib,
testFont,
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
) },
);
// Load a letter
{
const idx = (try cache.indexForCodepoint(alloc, 'A', .regular, null)).?;
const face = try cache.group.faceFromIndex(idx);
const glyph_index = face.glyphIndex('A').?;
const glyph = try cache.renderGlyph(
alloc,
idx,
glyph_index,
.{},
);
try testing.expectEqual(@as(u32, 11), glyph.height);
}
// Resize
try cache.setSize(.{ .points = 24, .xdpi = 96, .ydpi = 96 });
{
const idx = (try cache.indexForCodepoint(alloc, 'A', .regular, null)).?;
const face = try cache.group.faceFromIndex(idx);
const glyph_index = face.glyphIndex('A').?;
const glyph = try cache.renderGlyph(
alloc,
idx,
glyph_index,
.{},
);
try testing.expectEqual(@as(u32, 21), glyph.height);
}
}

359
src/font/SharedGrid.zig Normal file
View File

@ -0,0 +1,359 @@
//! This structure represents the state required to render a terminal
//! grid using the font subsystem. It is "shared" because it is able to
//! be shared across multiple surfaces.
//!
//! It is desirable for the grid state to be shared because the font
//! configuration for a set of surfaces is almost always the same and
//! font data is relatively memory intensive. Further, the font subsystem
//! should be read-heavy compared to write-heavy, so it handles concurrent
//! reads well. Going even further, the font subsystem should be very rarely
//! read at all since it should only be necessary when the grid actively
//! changes.
//!
//! SharedGrid does NOT support resizing, font-family changes, font removals
//! in collections, etc. Because the Grid is shared this would cause a
//! major disruption in the rendering of multiple surfaces (i.e. increasing
//! the font size in one would increase it in all). In many cases this isn't
//! desirable so to implement configuration changes the grid should be
//! reinitialized and all surfaces should switch over to using that one.
const SharedGrid = @This();
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const renderer = @import("../renderer.zig");
const font = @import("main.zig");
const Atlas = font.Atlas;
const CodepointResolver = font.CodepointResolver;
const Collection = font.Collection;
const Face = font.Face;
const Glyph = font.Glyph;
const Library = font.Library;
const Metrics = font.face.Metrics;
const Presentation = font.Presentation;
const Style = font.Style;
const RenderOptions = font.face.RenderOptions;
const log = std.log.scoped(.font_shared_grid);
/// Cache for codepoints to font indexes in a group.
codepoints: std.AutoHashMapUnmanaged(CodepointKey, ?Collection.Index) = .{},
/// Cache for glyph renders into the atlas.
glyphs: std.AutoHashMapUnmanaged(GlyphKey, Render) = .{},
/// The texture atlas to store renders in. The Glyph data in the glyphs
/// cache is dependent on the atlas matching.
atlas_greyscale: Atlas,
atlas_color: Atlas,
/// The underlying resolver for font data, fallbacks, etc. The shared
/// grid takes ownership of the resolver and will free it.
resolver: CodepointResolver,
/// The currently active grid metrics dictating the layout of the grid.
/// This is calculated based on the resolver and current fonts.
metrics: Metrics,
/// The RwLock used to protect the shared grid. Callers are expected to use
/// this directly if they need to i.e. access the atlas directly. Because
/// callers can use this lock directly, maintainers need to be extra careful
/// to review call sites to ensure they are using the lock correctly.
lock: std.Thread.RwLock,
/// Initialize the grid.
///
/// The resolver must have a collection that supports deferred loading
/// (collection.load_options != null). This is because we need the load
/// options data to determine grid metrics and setup our sprite font.
///
/// SharedGrid always configures the sprite font. This struct is expected to be
/// used with a terminal grid and therefore the sprite font is always
/// necessary for correct rendering.
pub fn init(
alloc: Allocator,
resolver: CodepointResolver,
thicken: bool,
) !SharedGrid {
// We need to support loading options since we use the size data
assert(resolver.collection.load_options != null);
var atlas_greyscale = try Atlas.init(alloc, 512, .greyscale);
errdefer atlas_greyscale.deinit(alloc);
var atlas_color = try Atlas.init(alloc, 512, .rgba);
errdefer atlas_color.deinit(alloc);
var result: SharedGrid = .{
.resolver = resolver,
.atlas_greyscale = atlas_greyscale,
.atlas_color = atlas_color,
.lock = .{},
.metrics = undefined, // Loaded below
};
// We set an initial capacity that can fit a good number of characters.
// This number was picked empirically based on my own terminal usage.
try result.codepoints.ensureTotalCapacity(alloc, 128);
try result.glyphs.ensureTotalCapacity(alloc, 128);
// Initialize our metrics.
try result.reloadMetrics(thicken);
return result;
}
/// Deinit. Assumes no concurrent access so no lock is taken.
pub fn deinit(self: *SharedGrid, alloc: Allocator) void {
self.codepoints.deinit(alloc);
self.glyphs.deinit(alloc);
self.atlas_greyscale.deinit(alloc);
self.atlas_color.deinit(alloc);
self.resolver.deinit(alloc);
}
fn reloadMetrics(self: *SharedGrid, thicken: bool) !void {
// Get our cell metrics based on a regular font ascii 'M'. Why 'M'?
// Doesn't matter, any normal ASCII will do we're just trying to make
// sure we use the regular font.
// We don't go through our caching layer because we want to minimize
// possible failures.
const collection = &self.resolver.collection;
const index = collection.getIndex('M', .regular, .{ .any = {} }).?;
const face = try collection.getFace(index);
self.metrics = face.metrics;
// Setup our sprite font.
self.resolver.sprite = .{
.width = self.metrics.cell_width,
.height = self.metrics.cell_height,
.thickness = self.metrics.underline_thickness *
@as(u32, if (thicken) 2 else 1),
.underline_position = self.metrics.underline_position,
};
}
/// Returns the grid cell size.
///
/// This is not thread safe.
pub fn cellSize(self: *SharedGrid) renderer.CellSize {
return .{
.width = self.metrics.cell_width,
.height = self.metrics.cell_height,
};
}
/// Get the font index for a given codepoint. This is cached.
pub fn getIndex(
self: *SharedGrid,
alloc: Allocator,
cp: u32,
style: Style,
p: ?Presentation,
) !?Collection.Index {
const key: CodepointKey = .{ .style = style, .codepoint = cp, .presentation = p };
// Fast path: the cache has the value. This is almost always true and
// only requires a read lock.
{
self.lock.lockShared();
defer self.lock.unlockShared();
if (self.codepoints.get(key)) |v| return v;
}
// Slow path: we need to search this codepoint
self.lock.lock();
defer self.lock.unlock();
// Try to get it, if it is now in the cache another thread beat us to it.
const gop = try self.codepoints.getOrPut(alloc, key);
if (gop.found_existing) return gop.value_ptr.*;
errdefer self.codepoints.removeByPtr(gop.key_ptr);
// Load a value and cache it. This even caches negative matches.
const value = self.resolver.getIndex(alloc, cp, style, p);
gop.value_ptr.* = value;
return value;
}
/// Returns true if the given font index has the codepoint and presentation.
pub fn hasCodepoint(
self: *SharedGrid,
idx: Collection.Index,
cp: u32,
p: ?Presentation,
) bool {
self.lock.lockShared();
defer self.lock.unlockShared();
return self.resolver.collection.hasCodepoint(
idx,
cp,
if (p) |v| .{ .explicit = v } else .{ .any = {} },
);
}
pub const Render = struct {
glyph: Glyph,
presentation: Presentation,
};
/// Render a codepoint. This uses the first font index that has the codepoint
/// and matches the presentation requested. If the codepoint cannot be found
/// in any font, an null render is returned.
pub fn renderCodepoint(
self: *SharedGrid,
alloc: Allocator,
cp: u32,
style: Style,
p: ?Presentation,
opts: RenderOptions,
) !?Render {
// Note: we could optimize the below to use way less locking, but
// at the time of writing this codepath is only called for preedit
// text which is relatively rare and almost non-existent in multiple
// surfaces at the same time.
// Get the font that has the codepoint
const index = try self.getIndex(alloc, cp, style, p) orelse return null;
// Get the glyph for the font
const glyph_index = glyph_index: {
self.lock.lockShared();
defer self.lock.unlockShared();
const face = try self.resolver.collection.getFace(index);
break :glyph_index face.glyphIndex(cp) orelse return null;
};
// Render
return try self.renderGlyph(alloc, index, glyph_index, opts);
}
/// Render a glyph index. This automatically determines the correct texture
/// atlas to use and caches the result.
pub fn renderGlyph(
self: *SharedGrid,
alloc: Allocator,
index: Collection.Index,
glyph_index: u32,
opts: RenderOptions,
) !Render {
const key: GlyphKey = .{ .index = index, .glyph = glyph_index, .opts = opts };
// Fast path: the cache has the value. This is almost always true and
// only requires a read lock.
{
self.lock.lockShared();
defer self.lock.unlockShared();
if (self.glyphs.get(key)) |v| return v;
}
// Slow path: we need to search this codepoint
self.lock.lock();
defer self.lock.unlock();
const gop = try self.glyphs.getOrPut(alloc, key);
if (gop.found_existing) return gop.value_ptr.*;
// Get the presentation to determine what atlas to use
const p = try self.resolver.getPresentation(index);
const atlas: *font.Atlas = switch (p) {
.text => &self.atlas_greyscale,
.emoji => &self.atlas_color,
};
// Render into the atlas
const glyph = self.resolver.renderGlyph(
alloc,
atlas,
index,
glyph_index,
opts,
) catch |err| switch (err) {
// If the atlas is full, we resize it
error.AtlasFull => blk: {
try atlas.grow(alloc, atlas.size * 2);
break :blk try self.resolver.renderGlyph(
alloc,
atlas,
index,
glyph_index,
opts,
);
},
else => return err,
};
// Cache and return
gop.value_ptr.* = .{
.glyph = glyph,
.presentation = p,
};
return gop.value_ptr.*;
}
const CodepointKey = struct {
style: Style,
codepoint: u32,
presentation: ?Presentation,
};
const GlyphKey = struct {
index: Collection.Index,
glyph: u32,
opts: RenderOptions,
};
const TestMode = enum { normal };
fn testGrid(mode: TestMode, alloc: Allocator, lib: Library) !SharedGrid {
const testFont = @import("test.zig").fontRegular;
var c = try Collection.init(alloc);
c.load_options = .{ .library = lib };
switch (mode) {
.normal => {
_ = try c.add(alloc, .regular, .{ .loaded = try Face.init(
lib,
testFont,
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
) });
},
}
var r: CodepointResolver = .{ .collection = c };
errdefer r.deinit(alloc);
return try init(alloc, r, false);
}
test getIndex {
const testing = std.testing;
const alloc = testing.allocator;
// const testEmoji = @import("test.zig").fontEmoji;
var lib = try Library.init();
defer lib.deinit();
var grid = try testGrid(.normal, alloc, lib);
defer grid.deinit(alloc);
// Visible ASCII.
for (32..127) |i| {
const idx = (try grid.getIndex(alloc, @intCast(i), .regular, null)).?;
try testing.expectEqual(Style.regular, idx.style);
try testing.expectEqual(@as(Collection.Index.IndexInt, 0), idx.idx);
try testing.expect(grid.hasCodepoint(idx, @intCast(i), null));
}
// Do it again without a resolver set to ensure we only hit the cache
const old_resolver = grid.resolver;
grid.resolver = undefined;
defer grid.resolver = old_resolver;
for (32..127) |i| {
const idx = (try grid.getIndex(alloc, @intCast(i), .regular, null)).?;
try testing.expectEqual(Style.regular, idx.style);
try testing.expectEqual(@as(Collection.Index.IndexInt, 0), idx.idx);
}
}

632
src/font/SharedGridSet.zig Normal file
View File

@ -0,0 +1,632 @@
//! This structure contains a set of SharedGrid structures keyed by
//! unique font configuration.
//!
//! Most terminals (surfaces) will share the same font configuration.
//! This structure allows expensive font information such as
//! the font atlas, glyph cache, font faces, etc. to be shared.
//!
//! This structure is thread-safe when the operations are documented
//! as thread-safe.
const SharedGridSet = @This();
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const font = @import("main.zig");
const CodepointResolver = font.CodepointResolver;
const Collection = font.Collection;
const Discover = font.Discover;
const Style = font.Style;
const Library = font.Library;
const Metrics = font.face.Metrics;
const CodepointMap = font.CodepointMap;
const DesiredSize = font.face.DesiredSize;
const Face = font.Face;
const SharedGrid = font.SharedGrid;
const discovery = @import("discovery.zig");
const configpkg = @import("../config.zig");
const Config = configpkg.Config;
const log = std.log.scoped(.font_shared_grid_set);
/// The allocator to use for all heap allocations.
alloc: Allocator,
/// The map of font configurations to SharedGrid instances.
map: Map = .{},
/// The font library that is used for all font groups.
font_lib: Library,
/// Font discovery mechanism.
font_discover: ?Discover = null,
/// Lock to protect multi-threaded access to the map.
lock: std.Thread.Mutex = .{},
/// Initialize a new SharedGridSet.
pub fn init(alloc: Allocator) !SharedGridSet {
var font_lib = try Library.init();
errdefer font_lib.deinit();
return .{
.alloc = alloc,
.map = .{},
.font_lib = font_lib,
};
}
pub fn deinit(self: *SharedGridSet) void {
var it = self.map.iterator();
while (it.next()) |entry| {
entry.key_ptr.deinit();
const v = entry.value_ptr.*;
v.grid.deinit(self.alloc);
self.alloc.destroy(v.grid);
}
self.map.deinit(self.alloc);
if (comptime Discover != void) {
if (self.font_discover) |*v| v.deinit();
}
self.font_lib.deinit();
}
/// Returns the number of cached grids.
pub fn count(self: *SharedGridSet) usize {
self.lock.lock();
defer self.lock.unlock();
return self.map.count();
}
/// Initialize a SharedGrid for the given font configuration. If the
/// SharedGrid is not present it will be initialized with a ref count of
/// 1. If it is present, the ref count will be incremented.
///
/// This is NOT thread-safe.
///
/// The returned data (key and grid) should never be freed. The memory is
/// owned by the set and will be freed when the ref count reaches zero.
pub fn ref(
self: *SharedGridSet,
config: *const DerivedConfig,
font_size: DesiredSize,
) !struct { Key, *SharedGrid } {
var key = try Key.init(self.alloc, config, font_size);
errdefer key.deinit();
self.lock.lock();
defer self.lock.unlock();
const gop = try self.map.getOrPut(self.alloc, key);
if (gop.found_existing) {
log.debug("found cached grid for font config", .{});
// We can deinit the key because we found a cached value.
key.deinit();
// Increment our ref count and return the cache
gop.value_ptr.ref += 1;
return .{ gop.key_ptr.*, gop.value_ptr.grid };
}
errdefer self.map.removeByPtr(gop.key_ptr);
log.debug("initializing new grid for font config", .{});
// A new font config, initialize the cache.
const grid = try self.alloc.create(SharedGrid);
errdefer self.alloc.destroy(grid);
gop.value_ptr.* = .{
.grid = grid,
.ref = 1,
};
grid.* = try SharedGrid.init(self.alloc, resolver: {
// Build our collection. This is the expensive operation that
// involves finding fonts, loading them (maybe, some are deferred),
// etc.
var c = try self.collection(&key, font_size);
errdefer c.deinit(self.alloc);
// Setup our enabled/disabled styles
var styles = CodepointResolver.StyleStatus.initFill(true);
styles.set(.bold, config.@"font-style-bold" != .false);
styles.set(.italic, config.@"font-style-italic" != .false);
styles.set(.bold_italic, config.@"font-style-bold-italic" != .false);
// Init our resolver which just requires setting fields.
break :resolver .{
.collection = c,
.styles = styles,
.discover = try self.discover(),
.codepoint_map = key.codepoint_map,
};
}, config.@"font-thicken");
errdefer grid.deinit(self.alloc);
return .{ gop.key_ptr.*, gop.value_ptr.grid };
}
/// Builds the Collection for the given configuration key and
/// initial font size.
fn collection(
self: *SharedGridSet,
key: *const Key,
size: DesiredSize,
) !Collection {
// A quick note on memory management:
// - font_lib is owned by the SharedGridSet
// - metric_modifiers is owned by the key which is freed only when
// the ref count for this grid reaches zero.
const load_options: Collection.LoadOptions = .{
.library = self.font_lib,
.size = size,
.metric_modifiers = key.metric_modifiers,
};
var c = try Collection.init(self.alloc);
errdefer c.deinit(self.alloc);
c.load_options = load_options;
// Search for fonts
if (Discover != void) discover: {
const disco = try self.discover() orelse {
log.warn(
"font discovery not available, cannot search for fonts",
.{},
);
break :discover;
};
// A buffer we use to store the font names for logging.
var name_buf: [256]u8 = undefined;
inline for (@typeInfo(Style).Enum.fields) |field| {
const style = @field(Style, field.name);
for (key.descriptorsForStyle(style)) |desc| {
var disco_it = try disco.discover(self.alloc, desc);
defer disco_it.deinit();
if (try disco_it.next()) |face| {
log.info("font {s}: {s}", .{
field.name,
try face.name(&name_buf),
});
_ = try c.add(
self.alloc,
style,
.{ .deferred = face },
);
} else log.warn("font-family {s} not found: {s}", .{
field.name,
desc.family.?,
});
}
}
}
// Our built-in font will be used as a backup
_ = try c.add(
self.alloc,
.regular,
.{ .fallback_loaded = try Face.init(
self.font_lib,
face_ttf,
load_options.faceOptions(),
) },
);
_ = try c.add(
self.alloc,
.bold,
.{ .fallback_loaded = try Face.init(
self.font_lib,
face_bold_ttf,
load_options.faceOptions(),
) },
);
// On macOS, always search for and add the Apple Emoji font
// as our preferred emoji font for fallback. We do this in case
// people add other emoji fonts to their system, we always want to
// prefer the official one. Users can override this by explicitly
// specifying a font-family for emoji.
if (comptime builtin.target.isDarwin()) apple_emoji: {
const disco = try self.discover() orelse break :apple_emoji;
var disco_it = try disco.discover(self.alloc, .{
.family = "Apple Color Emoji",
});
defer disco_it.deinit();
if (try disco_it.next()) |face| {
_ = try c.add(
self.alloc,
.regular,
.{ .fallback_deferred = face },
);
}
}
// Emoji fallback. We don't include this on Mac since Mac is expected
// to always have the Apple Emoji available on the system.
if (comptime !builtin.target.isDarwin() or Discover == void) {
_ = try c.add(
self.alloc,
.regular,
.{ .fallback_loaded = try Face.init(
self.font_lib,
face_emoji_ttf,
load_options.faceOptions(),
) },
);
_ = try c.add(
self.alloc,
.regular,
.{ .fallback_loaded = try Face.init(
self.font_lib,
face_emoji_text_ttf,
load_options.faceOptions(),
) },
);
}
// Auto-italicize
try c.autoItalicize(self.alloc);
return c;
}
/// Decrement the ref count for the given key. If the ref count is zero,
/// the grid will be deinitialized and removed from the map.j:w
pub fn deref(self: *SharedGridSet, key: Key) void {
self.lock.lock();
defer self.lock.unlock();
const entry = self.map.getEntry(key) orelse return;
assert(entry.value_ptr.ref >= 1);
// If we have more than one reference, decrement and return.
if (entry.value_ptr.ref > 1) {
entry.value_ptr.ref -= 1;
return;
}
// We are at a zero ref count so deinit the group and remove.
entry.key_ptr.deinit();
entry.value_ptr.grid.deinit(self.alloc);
self.alloc.destroy(entry.value_ptr.grid);
self.map.removeByPtr(entry.key_ptr);
}
/// Map of font configurations to grid instances. The grid
/// instances are pointers that are heap allocated so that they're
/// stable pointers across hash map resizes.
pub const Map = std.HashMapUnmanaged(
Key,
ReffedGrid,
struct {
const KeyType = Key;
pub fn hash(ctx: @This(), k: KeyType) u64 {
_ = ctx;
return k.hashcode();
}
pub fn eql(ctx: @This(), a: KeyType, b: KeyType) bool {
return ctx.hash(a) == ctx.hash(b);
}
},
std.hash_map.default_max_load_percentage,
);
/// Initialize once and return the font discovery mechanism. This remains
/// initialized throughout the lifetime of the application because some
/// font discovery mechanisms (i.e. fontconfig) are unsafe to reinit.
fn discover(self: *SharedGridSet) !?*Discover {
// If we're built without a font discovery mechanism, return null
if (comptime Discover == void) return null;
// If we initialized, use it
if (self.font_discover) |*v| return v;
self.font_discover = Discover.init();
return &self.font_discover.?;
}
/// Ref-counted SharedGrid.
const ReffedGrid = struct {
grid: *SharedGrid,
ref: u32 = 0,
};
/// This is the configuration required to create a key without having
/// to keep the full Ghostty configuration around.
pub const DerivedConfig = struct {
arena: ArenaAllocator,
@"font-family": configpkg.RepeatableString,
@"font-family-bold": configpkg.RepeatableString,
@"font-family-italic": configpkg.RepeatableString,
@"font-family-bold-italic": configpkg.RepeatableString,
@"font-style": configpkg.FontStyle,
@"font-style-bold": configpkg.FontStyle,
@"font-style-italic": configpkg.FontStyle,
@"font-style-bold-italic": configpkg.FontStyle,
@"font-variation": configpkg.RepeatableFontVariation,
@"font-variation-bold": configpkg.RepeatableFontVariation,
@"font-variation-italic": configpkg.RepeatableFontVariation,
@"font-variation-bold-italic": configpkg.RepeatableFontVariation,
@"font-codepoint-map": configpkg.RepeatableCodepointMap,
@"font-thicken": bool,
@"adjust-cell-width": ?Metrics.Modifier,
@"adjust-cell-height": ?Metrics.Modifier,
@"adjust-font-baseline": ?Metrics.Modifier,
@"adjust-underline-position": ?Metrics.Modifier,
@"adjust-underline-thickness": ?Metrics.Modifier,
@"adjust-strikethrough-position": ?Metrics.Modifier,
@"adjust-strikethrough-thickness": ?Metrics.Modifier,
pub fn init(alloc_gpa: Allocator, config: *const Config) !DerivedConfig {
var arena = ArenaAllocator.init(alloc_gpa);
errdefer arena.deinit();
const alloc = arena.allocator();
return .{
.@"font-family" = try config.@"font-family".clone(alloc),
.@"font-family-bold" = try config.@"font-family-bold".clone(alloc),
.@"font-family-italic" = try config.@"font-family-italic".clone(alloc),
.@"font-family-bold-italic" = try config.@"font-family-bold-italic".clone(alloc),
.@"font-style" = try config.@"font-style".clone(alloc),
.@"font-style-bold" = try config.@"font-style-bold".clone(alloc),
.@"font-style-italic" = try config.@"font-style-italic".clone(alloc),
.@"font-style-bold-italic" = try config.@"font-style-bold-italic".clone(alloc),
.@"font-variation" = try config.@"font-variation".clone(alloc),
.@"font-variation-bold" = try config.@"font-variation-bold".clone(alloc),
.@"font-variation-italic" = try config.@"font-variation-italic".clone(alloc),
.@"font-variation-bold-italic" = try config.@"font-variation-bold-italic".clone(alloc),
.@"font-codepoint-map" = try config.@"font-codepoint-map".clone(alloc),
.@"font-thicken" = config.@"font-thicken",
.@"adjust-cell-width" = config.@"adjust-cell-width",
.@"adjust-cell-height" = config.@"adjust-cell-height",
.@"adjust-font-baseline" = config.@"adjust-font-baseline",
.@"adjust-underline-position" = config.@"adjust-underline-position",
.@"adjust-underline-thickness" = config.@"adjust-underline-thickness",
.@"adjust-strikethrough-position" = config.@"adjust-strikethrough-position",
.@"adjust-strikethrough-thickness" = config.@"adjust-strikethrough-thickness",
// This must be last so the arena contains all our allocations
// from above since Zig does assignment in order.
.arena = arena,
};
}
pub fn deinit(self: *DerivedConfig) void {
self.arena.deinit();
}
};
/// The key used to uniquely identify a font configuration.
pub const Key = struct {
arena: ArenaAllocator,
/// The descriptors used for all the fonts added to the
/// initial group, including all styles. This is hashed
/// in order so the order matters. All users of the struct
/// should ensure that the order is consistent.
descriptors: []const discovery.Descriptor = &.{},
/// These are the offsets into the descriptors array for
/// each style. For example, bold is from
/// offsets[@intFromEnum(.bold) - 1] to
/// offsets[@intFromEnum(.bold)].
style_offsets: StyleOffsets = .{0} ** style_offsets_len,
/// The codepoint map configuration.
codepoint_map: CodepointMap = .{},
/// The metric modifier set configuration.
metric_modifiers: Metrics.ModifierSet = .{},
const style_offsets_len = std.enums.directEnumArrayLen(Style, 0);
const StyleOffsets = [style_offsets_len]usize;
comptime {
// We assume this throughout this structure. If this changes
// we may need to change this structure.
assert(@intFromEnum(Style.regular) == 0);
assert(@intFromEnum(Style.bold) == 1);
assert(@intFromEnum(Style.italic) == 2);
assert(@intFromEnum(Style.bold_italic) == 3);
}
pub fn init(
alloc_gpa: Allocator,
config: *const DerivedConfig,
font_size: DesiredSize,
) !Key {
var arena = ArenaAllocator.init(alloc_gpa);
errdefer arena.deinit();
const alloc = arena.allocator();
var descriptors = std.ArrayList(discovery.Descriptor).init(alloc);
defer descriptors.deinit();
for (config.@"font-family".list.items) |family| {
try descriptors.append(.{
.family = family,
.style = config.@"font-style".nameValue(),
.size = font_size.points,
.variations = config.@"font-variation".list.items,
});
}
// In all the styled cases below, we prefer to specify an exact
// style via the `font-style` configuration. If a style is not
// specified, we use the discovery mechanism to search for a
// style category such as bold, italic, etc. We can't specify both
// because the latter will restrict the search to only that. If
// a user says `font-style = italic` for the bold face for example,
// no results would be found if we restrict to ALSO searching for
// italic.
for (config.@"font-family-bold".list.items) |family| {
const style = config.@"font-style-bold".nameValue();
try descriptors.append(.{
.family = family,
.style = style,
.size = font_size.points,
.bold = style == null,
.variations = config.@"font-variation".list.items,
});
}
for (config.@"font-family-italic".list.items) |family| {
const style = config.@"font-style-italic".nameValue();
try descriptors.append(.{
.family = family,
.style = style,
.size = font_size.points,
.italic = style == null,
.variations = config.@"font-variation".list.items,
});
}
for (config.@"font-family-bold-italic".list.items) |family| {
const style = config.@"font-style-bold-italic".nameValue();
try descriptors.append(.{
.family = family,
.style = style,
.size = font_size.points,
.bold = style == null,
.italic = style == null,
.variations = config.@"font-variation".list.items,
});
}
// Setup the codepoint map
const codepoint_map: CodepointMap = map: {
const map = config.@"font-codepoint-map";
if (map.map.list.len == 0) break :map .{};
const clone = try config.@"font-codepoint-map".clone(alloc);
break :map clone.map;
};
// Metric modifiers
const metric_modifiers: Metrics.ModifierSet = set: {
var set: Metrics.ModifierSet = .{};
if (config.@"adjust-cell-width") |m| try set.put(alloc, .cell_width, m);
if (config.@"adjust-cell-height") |m| try set.put(alloc, .cell_height, m);
if (config.@"adjust-font-baseline") |m| try set.put(alloc, .cell_baseline, m);
if (config.@"adjust-underline-position") |m| try set.put(alloc, .underline_position, m);
if (config.@"adjust-underline-thickness") |m| try set.put(alloc, .underline_thickness, m);
if (config.@"adjust-strikethrough-position") |m| try set.put(alloc, .strikethrough_position, m);
if (config.@"adjust-strikethrough-thickness") |m| try set.put(alloc, .strikethrough_thickness, m);
break :set set;
};
return .{
.arena = arena,
.descriptors = try descriptors.toOwnedSlice(),
.style_offsets = .{
config.@"font-family".list.items.len,
config.@"font-family-bold".list.items.len,
config.@"font-family-italic".list.items.len,
config.@"font-family-bold-italic".list.items.len,
},
.codepoint_map = codepoint_map,
.metric_modifiers = metric_modifiers,
};
}
pub fn deinit(self: *Key) void {
self.arena.deinit();
}
/// Get the descriptors for the given font style that can be
/// used with discovery.
pub fn descriptorsForStyle(
self: Key,
style: Style,
) []const discovery.Descriptor {
const idx = @intFromEnum(style);
const start: usize = if (idx == 0) 0 else self.style_offsets[idx - 1];
const end = self.style_offsets[idx];
return self.descriptors[start..end];
}
/// Hash the key with the given hasher.
pub fn hash(self: Key, hasher: anytype) void {
const autoHash = std.hash.autoHash;
autoHash(hasher, self.descriptors.len);
for (self.descriptors) |d| d.hash(hasher);
self.codepoint_map.hash(hasher);
autoHash(hasher, self.metric_modifiers.count());
if (self.metric_modifiers.count() > 0) {
inline for (@typeInfo(Metrics.Key).Enum.fields) |field| {
const key = @field(Metrics.Key, field.name);
if (self.metric_modifiers.get(key)) |value| {
autoHash(hasher, key);
value.hash(hasher);
}
}
}
}
/// Returns a hash code that can be used to uniquely identify this
/// action.
pub fn hashcode(self: Key) u64 {
var hasher = std.hash.Wyhash.init(0);
self.hash(&hasher);
return hasher.final();
}
};
const face_ttf = @embedFile("res/JetBrainsMono-Regular.ttf");
const face_bold_ttf = @embedFile("res/JetBrainsMono-Bold.ttf");
const face_emoji_ttf = @embedFile("res/NotoColorEmoji.ttf");
const face_emoji_text_ttf = @embedFile("res/NotoEmoji-Regular.ttf");
test "Key" {
const testing = std.testing;
const alloc = testing.allocator;
var cfg = try Config.default(alloc);
defer cfg.deinit();
var keycfg = try DerivedConfig.init(alloc, &cfg);
defer keycfg.deinit();
var k = try Key.init(alloc, &keycfg, .{ .points = 12 });
defer k.deinit();
try testing.expect(k.hashcode() > 0);
}
test SharedGridSet {
const testing = std.testing;
const alloc = testing.allocator;
var set = try SharedGridSet.init(alloc);
defer set.deinit();
var cfg = try Config.default(alloc);
defer cfg.deinit();
var keycfg = try DerivedConfig.init(alloc, &cfg);
defer keycfg.deinit();
// Get a grid for the given config
const key1, const grid1 = try set.ref(&keycfg, .{ .points = 12 });
try testing.expectEqual(@as(usize, 1), set.count());
// Get another
const key2, const grid2 = try set.ref(&keycfg, .{ .points = 12 });
try testing.expectEqual(@as(usize, 1), set.count());
// They should be pointer equivalent
try testing.expectEqual(@intFromPtr(grid1), @intFromPtr(grid2));
// If I deref grid2 then we should still have a count of 1
set.deref(key2);
try testing.expectEqual(@as(usize, 1), set.count());
// If I deref grid1 then we should have a count of 0
set.deref(key1);
try testing.expectEqual(@as(usize, 0), set.count());
}

View File

@ -57,30 +57,50 @@ pub const Descriptor = struct {
/// will be preferred, but not guaranteed.
variations: []const Variation = &.{},
/// Returns a hash code that can be used to uniquely identify this
/// action.
pub fn hash(self: Descriptor) u64 {
/// Hash the descriptor with the given hasher.
pub fn hash(self: Descriptor, hasher: anytype) void {
const autoHash = std.hash.autoHash;
var hasher = std.hash.Wyhash.init(0);
autoHash(&hasher, self.family);
autoHash(&hasher, self.style);
autoHash(&hasher, self.codepoint);
autoHash(&hasher, self.size);
autoHash(&hasher, self.bold);
autoHash(&hasher, self.italic);
autoHash(&hasher, self.monospace);
autoHash(&hasher, self.variations.len);
const autoHashStrat = std.hash.autoHashStrat;
autoHashStrat(hasher, self.family, .Deep);
autoHashStrat(hasher, self.style, .Deep);
autoHash(hasher, self.codepoint);
autoHash(hasher, self.size);
autoHash(hasher, self.bold);
autoHash(hasher, self.italic);
autoHash(hasher, self.monospace);
autoHash(hasher, self.variations.len);
for (self.variations) |variation| {
autoHash(&hasher, variation.id);
autoHash(hasher, variation.id);
// This is not correct, but we don't currently depend on the
// hash value being different based on decimal values of variations.
autoHash(&hasher, @as(u64, @intFromFloat(variation.value)));
autoHash(hasher, @as(u64, @intFromFloat(variation.value)));
}
}
/// Returns a hash code that can be used to uniquely identify this
/// action.
pub fn hashcode(self: Descriptor) u64 {
var hasher = std.hash.Wyhash.init(0);
self.hash(&hasher);
return hasher.final();
}
/// Deep copy of the struct. The given allocator is expected to
/// be an arena allocator of some sort since the descriptor
/// itself doesn't support fine-grained deallocation of fields.
pub fn clone(self: *const Descriptor, alloc: Allocator) !Descriptor {
// We can't do any errdefer cleanup in here. As documented we
// expect the allocator to be an arena so any errors should be
// cleaned up somewhere else.
var copy = self.*;
copy.family = if (self.family) |src| try alloc.dupeZ(u8, src) else null;
copy.style = if (self.style) |src| try alloc.dupeZ(u8, src) else null;
copy.variations = try alloc.dupe(Variation, self.variations);
return copy;
}
/// Convert to Fontconfig pattern to use for lookup. The pattern does
/// not have defaults filled/substituted (Fontconfig thing) so callers
/// must still do this.
@ -552,7 +572,7 @@ test "descriptor hash" {
const testing = std.testing;
var d: Descriptor = .{};
try testing.expect(d.hash() != 0);
try testing.expect(d.hashcode() != 0);
}
test "descriptor hash familiy names" {
@ -560,7 +580,7 @@ test "descriptor hash familiy names" {
var d1: Descriptor = .{ .family = "A" };
var d2: Descriptor = .{ .family = "B" };
try testing.expect(d1.hash() != d2.hash());
try testing.expect(d1.hashcode() != d2.hashcode());
}
test "fontconfig" {

View File

@ -169,6 +169,19 @@ pub const Modifier = union(enum) {
};
}
/// Hash using the hasher.
pub fn hash(self: Modifier, hasher: anytype) void {
const autoHash = std.hash.autoHash;
autoHash(hasher, std.meta.activeTag(self));
switch (self) {
// floats can't be hashed directly so we bitcast to i64.
// for the purpose of what we're trying to do this seems
// good enough but I would prefer value hashing.
.percent => |v| autoHash(hasher, @as(i64, @bitCast(v))),
.absolute => |v| autoHash(hasher, v),
}
}
test "formatConfig percent" {
const configpkg = @import("../../config.zig");
const testing = std.testing;

View File

@ -6,15 +6,19 @@ pub const Atlas = @import("Atlas.zig");
pub const discovery = @import("discovery.zig");
pub const face = @import("face.zig");
pub const CodepointMap = @import("CodepointMap.zig");
pub const CodepointResolver = @import("CodepointResolver.zig");
pub const Collection = @import("Collection.zig");
pub const DeferredFace = @import("DeferredFace.zig");
pub const Face = face.Face;
pub const Group = @import("Group.zig");
pub const GroupCache = @import("GroupCache.zig");
pub const Glyph = @import("Glyph.zig");
pub const Metrics = face.Metrics;
pub const shape = @import("shape.zig");
pub const Shaper = shape.Shaper;
pub const SharedGrid = @import("SharedGrid.zig");
pub const SharedGridSet = @import("SharedGridSet.zig");
pub const sprite = @import("sprite.zig");
pub const Sprite = sprite.Sprite;
pub const SpriteFace = sprite.Face;
pub const Descriptor = discovery.Descriptor;
pub const Discover = discovery.Discover;
pub usingnamespace @import("library.zig");
@ -23,8 +27,6 @@ pub usingnamespace @import("library.zig");
pub usingnamespace if (builtin.target.isWasm()) struct {
pub usingnamespace Atlas.Wasm;
pub usingnamespace DeferredFace.Wasm;
pub usingnamespace Group.Wasm;
pub usingnamespace GroupCache.Wasm;
pub usingnamespace face.web_canvas.Wasm;
pub usingnamespace shape.web_canvas.Wasm;
} else struct {};
@ -145,7 +147,7 @@ pub const Presentation = enum(u1) {
};
/// A FontIndex that can be used to use the sprite font directly.
pub const sprite_index = Group.FontIndex.initSpecial(.sprite);
pub const sprite_index = Collection.Index.initSpecial(.sprite);
test {
// For non-wasm we want to test everything we can

View File

@ -5,10 +5,12 @@ const macos = @import("macos");
const trace = @import("tracy").trace;
const font = @import("../main.zig");
const Face = font.Face;
const Collection = font.Collection;
const DeferredFace = font.DeferredFace;
const Group = font.Group;
const GroupCache = font.GroupCache;
const Library = font.Library;
const SharedGrid = font.SharedGrid;
const Style = font.Style;
const Presentation = font.Presentation;
const terminal = @import("../../terminal/main.zig");
@ -189,7 +191,7 @@ pub const Shaper = struct {
pub fn runIterator(
self: *Shaper,
group: *GroupCache,
grid: *SharedGrid,
screen: *const terminal.Screen,
row: terminal.Pin,
selection: ?terminal.Selection,
@ -197,7 +199,7 @@ pub const Shaper = struct {
) font.shape.RunIterator {
return .{
.hooks = .{ .shaper = self },
.group = group,
.grid = grid,
.screen = screen,
.row = row,
.selection = selection,
@ -231,7 +233,24 @@ pub const Shaper = struct {
// Get our font. We have to apply the font features we want for
// the font here.
const run_font: *macos.text.Font = font: {
const face = try run.group.group.faceFromIndex(run.font_index);
// The CoreText shaper relies on CoreText and CoreText claims
// that CTFonts are threadsafe. See:
// https://developer.apple.com/documentation/coretext/
//
// Quote:
// All individual functions in Core Text are thread-safe. Font
// objects (CTFont, CTFontDescriptor, and associated objects) can
// be used simultaneously by multiple operations, work queues, or
// threads. However, the layout objects (CTTypesetter,
// CTFramesetter, CTRun, CTLine, CTFrame, and associated objects)
// should be used in a single operation, work queue, or thread.
//
// Because of this, we only acquire the read lock to grab the
// face and set it up, then release it.
run.grid.lock.lockShared();
defer run.grid.lock.unlockShared();
const face = try run.grid.resolver.collection.getFace(run.font_index);
const original = face.font;
const attrs = try self.features.attrsDict(face.quirks_disable_default_font_features);
@ -400,7 +419,7 @@ test "run iterator" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -419,7 +438,7 @@ test "run iterator" {
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -439,7 +458,7 @@ test "run iterator" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -486,7 +505,7 @@ test "run iterator: empty cells with background set" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -527,7 +546,7 @@ test "shape" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -562,7 +581,7 @@ test "shape nerd fonts" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -590,7 +609,7 @@ test "shape inconsolata ligs" {
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -614,7 +633,7 @@ test "shape inconsolata ligs" {
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -646,7 +665,7 @@ test "shape monaspace ligs" {
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -712,7 +731,7 @@ test "shape emoji width" {
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -752,7 +771,7 @@ test "shape emoji width long" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -789,7 +808,7 @@ test "shape variation selector VS15" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -824,7 +843,7 @@ test "shape variation selector VS16" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -856,7 +875,7 @@ test "shape with empty cells in between" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -894,7 +913,7 @@ test "shape Chinese characters" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -921,13 +940,6 @@ test "shape box glyphs" {
var testdata = try testShaper(alloc);
defer testdata.deinit();
// Setup the box font
testdata.cache.group.sprite = font.sprite.Face{
.width = 18,
.height = 36,
.thickness = 2,
};
var buf: [32]u8 = undefined;
var buf_idx: usize = 0;
buf_idx += try std.unicode.utf8Encode(0x2500, buf[buf_idx..]); // horiz line
@ -941,7 +953,7 @@ test "shape box glyphs" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -977,7 +989,7 @@ test "shape selection boundary" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init(
@ -1000,7 +1012,7 @@ test "shape selection boundary" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init(
@ -1023,7 +1035,7 @@ test "shape selection boundary" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init(
@ -1046,7 +1058,7 @@ test "shape selection boundary" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init(
@ -1069,7 +1081,7 @@ test "shape selection boundary" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init(
@ -1105,7 +1117,7 @@ test "shape cursor boundary" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -1124,7 +1136,7 @@ test "shape cursor boundary" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -1143,7 +1155,7 @@ test "shape cursor boundary" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -1162,7 +1174,7 @@ test "shape cursor boundary" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -1194,7 +1206,7 @@ test "shape cursor boundary and colored emoji" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -1213,7 +1225,7 @@ test "shape cursor boundary and colored emoji" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -1230,7 +1242,7 @@ test "shape cursor boundary and colored emoji" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -1260,7 +1272,7 @@ test "shape cell attribute change" {
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -1284,7 +1296,7 @@ test "shape cell attribute change" {
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -1309,7 +1321,7 @@ test "shape cell attribute change" {
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -1334,7 +1346,7 @@ test "shape cell attribute change" {
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -1358,7 +1370,7 @@ test "shape cell attribute change" {
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -1376,13 +1388,13 @@ test "shape cell attribute change" {
const TestShaper = struct {
alloc: Allocator,
shaper: Shaper,
cache: *GroupCache,
grid: *SharedGrid,
lib: Library,
pub fn deinit(self: *TestShaper) void {
self.shaper.deinit();
self.cache.deinit(self.alloc);
self.alloc.destroy(self.cache);
self.grid.deinit(self.alloc);
self.alloc.destroy(self.grid);
self.lib.deinit();
}
};
@ -1412,17 +1424,11 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
var lib = try Library.init();
errdefer lib.deinit();
var cache_ptr = try alloc.create(GroupCache);
errdefer alloc.destroy(cache_ptr);
cache_ptr.* = try GroupCache.init(alloc, try Group.init(
alloc,
lib,
.{ .points = 12 },
));
errdefer cache_ptr.*.deinit(alloc);
var c = try Collection.init(alloc);
c.load_options = .{ .library = lib };
// Setup group
_ = try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init(
_ = try c.add(alloc, .regular, .{ .loaded = try Face.init(
lib,
testFont,
.{ .size = .{ .points = 12 } },
@ -1430,7 +1436,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
if (font.options.backend != .coretext) {
// Coretext doesn't support Noto's format
_ = try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init(
_ = try c.add(alloc, .regular, .{ .loaded = try Face.init(
lib,
testEmoji,
.{ .size = .{ .points = 12 } },
@ -1447,21 +1453,26 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
defer disco_it.deinit();
var face = (try disco_it.next()).?;
errdefer face.deinit();
_ = try cache_ptr.group.addFace(.regular, .{ .deferred = face });
_ = try c.add(alloc, .regular, .{ .deferred = face });
}
_ = try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init(
_ = try c.add(alloc, .regular, .{ .loaded = try Face.init(
lib,
testEmojiText,
.{ .size = .{ .points = 12 } },
) });
const grid_ptr = try alloc.create(SharedGrid);
errdefer alloc.destroy(grid_ptr);
grid_ptr.* = try SharedGrid.init(alloc, .{ .collection = c }, false);
errdefer grid_ptr.*.deinit(alloc);
var shaper = try Shaper.init(alloc, .{});
errdefer shaper.deinit();
return TestShaper{
.alloc = alloc,
.shaper = shaper,
.cache = cache_ptr,
.grid = grid_ptr,
.lib = lib,
};
}

View File

@ -4,10 +4,10 @@ const Allocator = std.mem.Allocator;
const harfbuzz = @import("harfbuzz");
const font = @import("../main.zig");
const Face = font.Face;
const Collection = font.Collection;
const DeferredFace = font.DeferredFace;
const Group = font.Group;
const GroupCache = font.GroupCache;
const Library = font.Library;
const SharedGrid = font.SharedGrid;
const Style = font.Style;
const Presentation = font.Presentation;
const terminal = @import("../../terminal/main.zig");
@ -83,7 +83,7 @@ pub const Shaper = struct {
/// and assume the y value matches.
pub fn runIterator(
self: *Shaper,
group: *GroupCache,
grid: *SharedGrid,
screen: *const terminal.Screen,
row: terminal.Pin,
selection: ?terminal.Selection,
@ -91,7 +91,7 @@ pub const Shaper = struct {
) font.shape.RunIterator {
return .{
.hooks = .{ .shaper = self },
.group = group,
.grid = grid,
.screen = screen,
.row = row,
.selection = selection,
@ -110,7 +110,13 @@ pub const Shaper = struct {
// We only do shaping if the font is not a special-case. For special-case
// fonts, the codepoint == glyph_index so we don't need to run any shaping.
if (run.font_index.special() == null) {
const face = try run.group.group.faceFromIndex(run.font_index);
// We have to lock the grid to get the face and unfortunately
// freetype faces (typically used with harfbuzz) are not thread
// safe so this has to be an exclusive lock.
run.grid.lock.lock();
defer run.grid.lock.unlock();
const face = try run.grid.resolver.collection.getFace(run.font_index);
const i = if (!face.quirks_disable_default_font_features) 0 else i: {
// If we are disabling default font features we just offset
// our features by the hardcoded items because always
@ -251,7 +257,7 @@ test "run iterator" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -270,7 +276,7 @@ test "run iterator" {
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -290,7 +296,7 @@ test "run iterator" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -342,7 +348,7 @@ test "run iterator: empty cells with background set" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -385,7 +391,7 @@ test "shape" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -414,7 +420,7 @@ test "shape inconsolata ligs" {
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -439,7 +445,7 @@ test "shape inconsolata ligs" {
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -473,7 +479,7 @@ test "shape monaspace ligs" {
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -507,7 +513,7 @@ test "shape emoji width" {
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -547,7 +553,7 @@ test "shape emoji width long" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -586,7 +592,7 @@ test "shape variation selector VS15" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -623,7 +629,7 @@ test "shape variation selector VS16" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -657,7 +663,7 @@ test "shape with empty cells in between" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -695,7 +701,7 @@ test "shape Chinese characters" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -722,13 +728,6 @@ test "shape box glyphs" {
var testdata = try testShaper(alloc);
defer testdata.deinit();
// Setup the box font
testdata.cache.group.sprite = font.sprite.Face{
.width = 18,
.height = 36,
.thickness = 2,
};
var buf: [32]u8 = undefined;
var buf_idx: usize = 0;
buf_idx += try std.unicode.utf8Encode(0x2500, buf[buf_idx..]); // horiz line
@ -742,7 +741,7 @@ test "shape box glyphs" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -779,7 +778,7 @@ test "shape selection boundary" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init(
@ -802,7 +801,7 @@ test "shape selection boundary" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init(
@ -825,7 +824,7 @@ test "shape selection boundary" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init(
@ -848,7 +847,7 @@ test "shape selection boundary" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init(
@ -871,7 +870,7 @@ test "shape selection boundary" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init(
@ -907,7 +906,7 @@ test "shape cursor boundary" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -926,7 +925,7 @@ test "shape cursor boundary" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -945,7 +944,7 @@ test "shape cursor boundary" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -964,7 +963,7 @@ test "shape cursor boundary" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -996,7 +995,7 @@ test "shape cursor boundary and colored emoji" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -1015,7 +1014,7 @@ test "shape cursor boundary and colored emoji" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -1032,7 +1031,7 @@ test "shape cursor boundary and colored emoji" {
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -1062,7 +1061,7 @@ test "shape cell attribute change" {
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -1086,7 +1085,7 @@ test "shape cell attribute change" {
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -1111,7 +1110,7 @@ test "shape cell attribute change" {
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -1136,7 +1135,7 @@ test "shape cell attribute change" {
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -1160,7 +1159,7 @@ test "shape cell attribute change" {
var shaper = &testdata.shaper;
var it = shaper.runIterator(
testdata.cache,
testdata.grid,
&screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null,
@ -1178,13 +1177,13 @@ test "shape cell attribute change" {
const TestShaper = struct {
alloc: Allocator,
shaper: Shaper,
cache: *GroupCache,
grid: *SharedGrid,
lib: Library,
pub fn deinit(self: *TestShaper) void {
self.shaper.deinit();
self.cache.deinit(self.alloc);
self.alloc.destroy(self.cache);
self.grid.deinit(self.alloc);
self.alloc.destroy(self.grid);
self.lib.deinit();
}
};
@ -1210,17 +1209,11 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
var lib = try Library.init();
errdefer lib.deinit();
var cache_ptr = try alloc.create(GroupCache);
errdefer alloc.destroy(cache_ptr);
cache_ptr.* = try GroupCache.init(alloc, try Group.init(
alloc,
lib,
.{ .points = 12 },
));
errdefer cache_ptr.*.deinit(alloc);
var c = try Collection.init(alloc);
c.load_options = .{ .library = lib };
// Setup group
_ = try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init(
_ = try c.add(alloc, .regular, .{ .loaded = try Face.init(
lib,
testFont,
.{ .size = .{ .points = 12 } },
@ -1228,7 +1221,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
if (font.options.backend != .coretext) {
// Coretext doesn't support Noto's format
_ = try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init(
_ = try c.add(alloc, .regular, .{ .loaded = try Face.init(
lib,
testEmoji,
.{ .size = .{ .points = 12 } },
@ -1245,21 +1238,26 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
defer disco_it.deinit();
var face = (try disco_it.next()).?;
errdefer face.deinit();
_ = try cache_ptr.group.addFace(.regular, .{ .deferred = face });
_ = try c.add(alloc, .regular, .{ .deferred = face });
}
_ = try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init(
_ = try c.add(alloc, .regular, .{ .loaded = try Face.init(
lib,
testEmojiText,
.{ .size = .{ .points = 12 } },
) });
const grid_ptr = try alloc.create(SharedGrid);
errdefer alloc.destroy(grid_ptr);
grid_ptr.* = try SharedGrid.init(alloc, .{ .collection = c }, false);
errdefer grid_ptr.*.deinit(alloc);
var shaper = try Shaper.init(alloc, .{});
errdefer shaper.deinit();
return TestShaper{
.alloc = alloc,
.shaper = shaper,
.cache = cache_ptr,
.grid = grid_ptr,
.lib = lib,
};
}

View File

@ -15,17 +15,17 @@ pub const TextRun = struct {
/// The total number of cells produced by this run.
cells: u16,
/// The font group that built this run.
group: *font.GroupCache,
/// The font grid that built this run.
grid: *font.SharedGrid,
/// The font index to use for the glyphs of this run.
font_index: font.Group.FontIndex,
font_index: font.Collection.Index,
};
/// RunIterator is an iterator that yields text runs.
pub const RunIterator = struct {
hooks: font.Shaper.RunIteratorHook,
group: *font.GroupCache,
grid: *font.SharedGrid,
screen: *const terminal.Screen,
row: terminal.Pin,
selection: ?terminal.Selection = null,
@ -49,7 +49,7 @@ pub const RunIterator = struct {
if (self.i >= max) return null;
// Track the font for our current run
var current_font: font.Group.FontIndex = .{};
var current_font: font.Collection.Index = .{};
// Allow the hook to prepare
try self.hooks.prepare();
@ -117,7 +117,7 @@ pub const RunIterator = struct {
} else emoji: {
// If we're not a grapheme, our individual char could be
// an emoji so we want to check if we expect emoji presentation.
// The font group indexForCodepoint we use below will do this
// The font grid indexForCodepoint we use below will do this
// automatically.
break :emoji null;
};
@ -160,7 +160,7 @@ pub const RunIterator = struct {
// grapheme, i.e. combining characters), we need to find a font
// that supports all of them.
const font_info: struct {
idx: font.Group.FontIndex,
idx: font.Collection.Index,
fallback: ?u32 = null,
} = font_info: {
// If we find a font that supports this entire grapheme
@ -174,7 +174,7 @@ pub const RunIterator = struct {
// Otherwise we need a fallback character. Prefer the
// official replacement character.
if (try self.group.indexForCodepoint(
if (try self.grid.getIndex(
alloc,
0xFFFD, // replacement char
font_style,
@ -182,7 +182,7 @@ pub const RunIterator = struct {
)) |idx| break :font_info .{ .idx = idx, .fallback = 0xFFFD };
// Fallback to space
if (try self.group.indexForCodepoint(
if (try self.grid.getIndex(
alloc,
' ',
font_style,
@ -231,7 +231,7 @@ pub const RunIterator = struct {
return TextRun{
.offset = @intCast(self.i),
.cells = @intCast(j - self.i),
.group = self.group,
.grid = self.grid,
.font_index = current_font,
};
}
@ -248,10 +248,10 @@ pub const RunIterator = struct {
cell: *terminal.Cell,
style: font.Style,
presentation: ?font.Presentation,
) !?font.Group.FontIndex {
) !?font.Collection.Index {
// Get the font index for the primary codepoint.
const primary_cp: u32 = if (cell.isEmpty() or cell.codepoint() == 0) ' ' else cell.codepoint();
const primary = try self.group.indexForCodepoint(
const primary = try self.grid.getIndex(
alloc,
primary_cp,
style,
@ -265,7 +265,7 @@ pub const RunIterator = struct {
// If this is a grapheme, we need to find a font that supports
// all of the codepoints in the grapheme.
const cps = self.row.grapheme(cell) orelse return primary;
var candidates = try std.ArrayList(font.Group.FontIndex).initCapacity(alloc, cps.len + 1);
var candidates = try std.ArrayList(font.Collection.Index).initCapacity(alloc, cps.len + 1);
defer candidates.deinit();
candidates.appendAssumeCapacity(primary);
@ -275,7 +275,7 @@ pub const RunIterator = struct {
// Find a font that supports this codepoint. If none support this
// then the whole grapheme can't be rendered so we return null.
const idx = try self.group.indexForCodepoint(
const idx = try self.grid.getIndex(
alloc,
cp,
style,
@ -286,11 +286,11 @@ pub const RunIterator = struct {
// We need to find a candidate that has ALL of our codepoints
for (candidates.items) |idx| {
if (!self.group.group.hasCodepoint(idx, primary_cp, presentation)) continue;
if (!self.grid.hasCodepoint(idx, primary_cp, presentation)) continue;
for (cps) |cp| {
// Ignore Emoji ZWJs
if (cp == 0xFE0E or cp == 0xFE0F or cp == 0x200D) continue;
if (!self.group.group.hasCodepoint(idx, cp, presentation)) break;
if (!self.grid.hasCodepoint(idx, cp, presentation)) break;
} else {
// If the while completed, then we have a candidate that
// supports all of our codepoints.

View File

@ -97,7 +97,7 @@ cells: std.ArrayListUnmanaged(mtl_shaders.Cell),
uniforms: mtl_shaders.Uniforms,
/// The font structures.
font_group: *font.GroupCache,
font_grid: *font.SharedGrid,
font_shaper: font.Shaper,
/// The images that we may render.
@ -118,6 +118,8 @@ queue: objc.Object, // MTLCommandQueue
layer: objc.Object, // CAMetalLayer
texture_greyscale: objc.Object, // MTLTexture
texture_color: objc.Object, // MTLTexture
texture_greyscale_modified: usize = 0,
texture_color_modified: usize = 0,
/// Custom shader state. This is only set if we have custom shaders.
custom_shader_state: ?CustomShaderState = null,
@ -158,7 +160,7 @@ pub const DerivedConfig = struct {
font_thicken: bool,
font_features: std.ArrayListUnmanaged([:0]const u8),
font_styles: font.Group.StyleStatus,
font_styles: font.CodepointResolver.StyleStatus,
cursor_color: ?terminal.color.RGB,
cursor_opacity: f64,
cursor_text: ?terminal.color.RGB,
@ -187,7 +189,7 @@ pub const DerivedConfig = struct {
const font_features = try config.@"font-feature".list.clone(alloc);
// Get our font styles
var font_styles = font.Group.StyleStatus.initFill(true);
var font_styles = font.CodepointResolver.StyleStatus.initFill(true);
font_styles.set(.bold, config.@"font-style-bold" != .false);
font_styles.set(.italic, config.@"font-style-italic" != .false);
font_styles.set(.bold_italic, config.@"font-style-bold-italic" != .false);
@ -343,25 +345,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
// to blurry rendering.
layer.setProperty("contentsScale", info.scaleFactor);
// Get our cell metrics based on a regular font ascii 'M'. Why 'M'?
// Doesn't matter, any normal ASCII will do we're just trying to make
// sure we use the regular font.
const metrics = metrics: {
const index = (try options.font_group.indexForCodepoint(alloc, 'M', .regular, .text)).?;
const face = try options.font_group.group.faceFromIndex(index);
break :metrics face.metrics;
};
log.debug("cell dimensions={}", .{metrics});
// Set the sprite font up
options.font_group.group.sprite = font.sprite.Face{
.width = metrics.cell_width,
.height = metrics.cell_height,
.thickness = metrics.underline_thickness *
@as(u32, if (options.config.font_thicken) 2 else 1),
.underline_position = metrics.underline_position,
};
// Create the font shaper. We initially create a shaper that can support
// a width of 160 which is a common width for modern screens to help
// avoid allocations later.
@ -427,15 +410,34 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
var shaders = try Shaders.init(alloc, device, custom_shaders);
errdefer shaders.deinit(alloc);
// Font atlas textures
const texture_greyscale = try initAtlasTexture(device, &options.font_group.atlas_greyscale);
const texture_color = try initAtlasTexture(device, &options.font_group.atlas_color);
// Initialize all the data that requires a critical font section.
const font_critical: struct {
metrics: font.Metrics,
texture_greyscale: objc.Object,
texture_color: objc.Object,
} = font_critical: {
const grid = options.font_grid;
grid.lock.lockShared();
defer grid.lock.unlockShared();
// Font atlas textures
const greyscale = try initAtlasTexture(device, &grid.atlas_greyscale);
errdefer deinitMTLResource(greyscale);
const color = try initAtlasTexture(device, &grid.atlas_color);
errdefer deinitMTLResource(color);
break :font_critical .{
.metrics = grid.metrics,
.texture_greyscale = greyscale,
.texture_color = color,
};
};
return Metal{
.alloc = alloc,
.config = options.config,
.surface_mailbox = options.surface_mailbox,
.grid_metrics = metrics,
.grid_metrics = font_critical.metrics,
.screen_size = null,
.padding = options.padding,
.focused = true,
@ -450,13 +452,13 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
.uniforms = .{
.projection_matrix = undefined,
.cell_size = undefined,
.strikethrough_position = @floatFromInt(metrics.strikethrough_position),
.strikethrough_thickness = @floatFromInt(metrics.strikethrough_thickness),
.strikethrough_position = @floatFromInt(font_critical.metrics.strikethrough_position),
.strikethrough_thickness = @floatFromInt(font_critical.metrics.strikethrough_thickness),
.min_contrast = options.config.min_contrast,
},
// Fonts
.font_group = options.font_group,
.font_grid = options.font_grid,
.font_shaper = font_shaper,
// Shaders
@ -469,8 +471,8 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
.device = device,
.queue = queue,
.layer = layer,
.texture_greyscale = texture_greyscale,
.texture_color = texture_color,
.texture_greyscale = font_critical.texture_greyscale,
.texture_color = font_critical.texture_color,
.custom_shader_state = custom_shader_state,
};
}
@ -564,18 +566,16 @@ pub fn setFocus(self: *Metal, focus: bool) !void {
/// Set the new font size.
///
/// Must be called on the render thread.
pub fn setFontSize(self: *Metal, size: font.face.DesiredSize) !void {
log.info("set font size={}", .{size});
pub fn setFontGrid(self: *Metal, grid: *font.SharedGrid) void {
// Update our grid
self.font_grid = grid;
self.texture_greyscale_modified = 0;
self.texture_color_modified = 0;
// Set our new size, this will also reset our font atlas.
try self.font_group.setSize(size);
// Recalculate our metrics
const metrics = metrics: {
const index = (try self.font_group.indexForCodepoint(self.alloc, 'M', .regular, .text)).?;
const face = try self.font_group.group.faceFromIndex(index);
break :metrics face.metrics;
};
// Get our metrics from the grid. This doesn't require a lock because
// the metrics are never recalculated.
const metrics = grid.metrics;
self.grid_metrics = metrics;
// Update our uniforms
self.uniforms = .{
@ -588,27 +588,6 @@ pub fn setFontSize(self: *Metal, size: font.face.DesiredSize) !void {
.strikethrough_thickness = @floatFromInt(metrics.strikethrough_thickness),
.min_contrast = self.uniforms.min_contrast,
};
// Recalculate our cell size. If it is the same as before, then we do
// nothing since the grid size couldn't have possibly changed.
if (std.meta.eql(self.grid_metrics, metrics)) return;
self.grid_metrics = metrics;
// Set the sprite font up
self.font_group.group.sprite = font.sprite.Face{
.width = metrics.cell_width,
.height = metrics.cell_height,
.thickness = metrics.underline_thickness * @as(u32, if (self.config.font_thicken) 2 else 1),
.underline_position = metrics.underline_position,
};
// Notify the window that the cell size changed.
_ = self.surface_mailbox.push(.{
.cell_size = .{
.width = metrics.cell_width,
.height = metrics.cell_height,
},
}, .{ .forever = {} });
}
/// Update the frame data.
@ -773,13 +752,21 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
};
// If our font atlas changed, sync the texture data
if (self.font_group.atlas_greyscale.modified) {
try syncAtlasTexture(self.device, &self.font_group.atlas_greyscale, &self.texture_greyscale);
self.font_group.atlas_greyscale.modified = false;
texture: {
const modified = self.font_grid.atlas_greyscale.modified.load(.monotonic);
if (modified <= self.texture_greyscale_modified) break :texture;
self.font_grid.lock.lockShared();
defer self.font_grid.lock.unlockShared();
self.texture_greyscale_modified = self.font_grid.atlas_greyscale.modified.load(.monotonic);
try syncAtlasTexture(self.device, &self.font_grid.atlas_greyscale, &self.texture_greyscale);
}
if (self.font_group.atlas_color.modified) {
try syncAtlasTexture(self.device, &self.font_group.atlas_color, &self.texture_color);
self.font_group.atlas_color.modified = false;
texture: {
const modified = self.font_grid.atlas_color.modified.load(.monotonic);
if (modified <= self.texture_color_modified) break :texture;
self.font_grid.lock.lockShared();
defer self.font_grid.lock.unlockShared();
self.texture_color_modified = self.font_grid.atlas_color.modified.load(.monotonic);
try syncAtlasTexture(self.device, &self.font_grid.atlas_color, &self.texture_color);
}
// Command buffer (MTLCommandBuffer)
@ -1376,16 +1363,6 @@ fn prepKittyGraphics(
/// Update the configuration.
pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void {
// On configuration change we always reset our font group. There
// are a variety of configurations that can change font settings
// so to be safe we just always reset it. This has a performance hit
// when its not necessary but config reloading shouldn't be so
// common to cause a problem.
self.font_group.reset();
self.font_group.group.styles = config.font_styles;
self.font_group.atlas_greyscale.clear();
self.font_group.atlas_color.clear();
// We always redo the font shaper in case font features changed. We
// could check to see if there was an actual config change but this is
// easier and rare enough to not cause performance issues.
@ -1643,7 +1620,7 @@ fn rebuildCells(
// Split our row into runs and shape each one.
var iter = self.font_shaper.runIterator(
self.font_group,
self.font_grid,
screen,
row,
row_selection,
@ -1868,7 +1845,7 @@ fn updateCell(
// If the cell has a character, draw it
if (cell.hasText()) fg: {
// Render
const glyph = try self.font_group.renderGlyph(
const render = try self.font_grid.renderGlyph(
self.alloc,
shaper_run.font_index,
shaper_cell.glyph_index orelse break :fg,
@ -1879,9 +1856,8 @@ fn updateCell(
);
const mode: mtl_shaders.Cell.Mode = switch (try fgMode(
&self.font_group.group,
render.presentation,
cell_pin,
shaper_run,
)) {
.normal => .fg,
.color => .fg_color,
@ -1894,11 +1870,11 @@ fn updateCell(
.cell_width = cell.gridWidth(),
.color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha },
.bg_color = bg,
.glyph_pos = .{ glyph.atlas_x, glyph.atlas_y },
.glyph_size = .{ glyph.width, glyph.height },
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
.glyph_size = .{ render.glyph.width, render.glyph.height },
.glyph_offset = .{
glyph.offset_x + shaper_cell.x_offset,
glyph.offset_y + shaper_cell.y_offset,
render.glyph.offset_x + shaper_cell.x_offset,
render.glyph.offset_y + shaper_cell.y_offset,
},
});
}
@ -1913,7 +1889,7 @@ fn updateCell(
.curly => .underline_curly,
};
const glyph = try self.font_group.renderGlyph(
const render = try self.font_grid.renderGlyph(
self.alloc,
font.sprite_index,
@intFromEnum(sprite),
@ -1931,9 +1907,9 @@ fn updateCell(
.cell_width = cell.gridWidth(),
.color = .{ color.r, color.g, color.b, alpha },
.bg_color = bg,
.glyph_pos = .{ glyph.atlas_x, glyph.atlas_y },
.glyph_size = .{ glyph.width, glyph.height },
.glyph_offset = .{ glyph.offset_x, glyph.offset_y },
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
.glyph_size = .{ render.glyph.width, render.glyph.height },
.glyph_offset = .{ render.glyph.offset_x, render.glyph.offset_y },
});
}
@ -1982,7 +1958,7 @@ fn addCursor(
.underline => .underline,
};
const glyph = self.font_group.renderGlyph(
const render = self.font_grid.renderGlyph(
self.alloc,
font.sprite_index,
@intFromEnum(sprite),
@ -2004,9 +1980,9 @@ fn addCursor(
.cell_width = if (wide) 2 else 1,
.color = .{ color.r, color.g, color.b, alpha },
.bg_color = .{ 0, 0, 0, 0 },
.glyph_pos = .{ glyph.atlas_x, glyph.atlas_y },
.glyph_size = .{ glyph.width, glyph.height },
.glyph_offset = .{ glyph.offset_x, glyph.offset_y },
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
.glyph_size = .{ render.glyph.width, render.glyph.height },
.glyph_offset = .{ render.glyph.offset_x, render.glyph.offset_y },
});
return &self.cells.items[self.cells.items.len - 1];
@ -2022,33 +1998,21 @@ fn addPreeditCell(
const bg = self.foreground_color;
const fg = self.background_color;
// Get the font for this codepoint.
const font_index = if (self.font_group.indexForCodepoint(
// Render the glyph for our preedit text
const render_ = self.font_grid.renderCodepoint(
self.alloc,
@intCast(cp.codepoint),
.regular,
.text,
)) |index| index orelse return else |_| return;
// Get the font face so we can get the glyph
const face = self.font_group.group.faceFromIndex(font_index) catch |err| {
log.warn("error getting face for font_index={} err={}", .{ font_index, err });
return;
};
// Use the face to now get the glyph index
const glyph_index = face.glyphIndex(@intCast(cp.codepoint)) orelse return;
// Render the glyph for our preedit text
const glyph = self.font_group.renderGlyph(
self.alloc,
font_index,
glyph_index,
.{ .grid_metrics = self.grid_metrics },
) catch |err| {
log.warn("error rendering preedit glyph err={}", .{err});
return;
};
const render = render_ orelse {
log.warn("failed to find font for preedit codepoint={X}", .{cp.codepoint});
return;
};
// Add our opaque background cell
self.cells_bg.appendAssumeCapacity(.{
@ -2066,9 +2030,9 @@ fn addPreeditCell(
.cell_width = if (cp.wide) 2 else 1,
.color = .{ fg.r, fg.g, fg.b, 255 },
.bg_color = .{ bg.r, bg.g, bg.b, 255 },
.glyph_pos = .{ glyph.atlas_x, glyph.atlas_y },
.glyph_size = .{ glyph.width, glyph.height },
.glyph_offset = .{ glyph.offset_x, glyph.offset_y },
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
.glyph_size = .{ render.glyph.width, render.glyph.height },
.glyph_offset = .{ render.glyph.offset_x, render.glyph.offset_y },
});
}

View File

@ -72,8 +72,12 @@ gl_cells_written: usize = 0,
gl_state: ?GLState = null,
/// The font structures.
font_group: *font.GroupCache,
font_grid: *font.SharedGrid,
font_shaper: font.Shaper,
texture_greyscale_modified: usize = 0,
texture_greyscale_resized: usize = 0,
texture_color_modified: usize = 0,
texture_color_resized: usize = 0,
/// True if the window is focused
focused: bool,
@ -239,7 +243,7 @@ pub const DerivedConfig = struct {
font_thicken: bool,
font_features: std.ArrayListUnmanaged([:0]const u8),
font_styles: font.Group.StyleStatus,
font_styles: font.CodepointResolver.StyleStatus,
cursor_color: ?terminal.color.RGB,
cursor_text: ?terminal.color.RGB,
cursor_opacity: f64,
@ -268,7 +272,7 @@ pub const DerivedConfig = struct {
const font_features = try config.@"font-feature".list.clone(alloc);
// Get our font styles
var font_styles = font.Group.StyleStatus.initFill(true);
var font_styles = font.CodepointResolver.StyleStatus.initFill(true);
font_styles.set(.bold, config.@"font-style-bold" != .false);
font_styles.set(.italic, config.@"font-style-italic" != .false);
font_styles.set(.bold_italic, config.@"font-style-bold-italic" != .false);
@ -333,14 +337,13 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL {
});
errdefer shaper.deinit();
// Setup our font metrics uniform
const metrics = try resetFontMetrics(
alloc,
options.font_group,
options.config.font_thicken,
);
// For the remainder of the setup we lock our font grid data because
// we're reading it.
const grid = options.font_grid;
grid.lock.lockShared();
defer grid.lock.unlockShared();
var gl_state = try GLState.init(alloc, options.config, options.font_group);
var gl_state = try GLState.init(alloc, options.config, grid);
errdefer gl_state.deinit();
return OpenGL{
@ -348,10 +351,10 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL {
.config = options.config,
.cells_bg = .{},
.cells = .{},
.grid_metrics = metrics,
.grid_metrics = grid.metrics,
.screen_size = null,
.gl_state = gl_state,
.font_group = options.font_group,
.font_grid = grid,
.font_shaper = shaper,
.draw_background = options.config.background,
.focused = true,
@ -360,7 +363,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL {
.cursor_color = options.config.cursor_color,
.padding = options.padding,
.surface_mailbox = options.surface_mailbox,
.deferred_font_size = .{ .metrics = metrics },
.deferred_font_size = .{ .metrics = grid.metrics },
.deferred_config = .{},
};
}
@ -470,15 +473,16 @@ pub fn displayRealize(self: *OpenGL) !void {
if (single_threaded_draw) self.draw_mutex.lock();
defer if (single_threaded_draw) self.draw_mutex.unlock();
// Reset our GPU uniforms
const metrics = try resetFontMetrics(
self.alloc,
self.font_group,
self.config.font_thicken,
);
// Make our new state
var gl_state = try GLState.init(self.alloc, self.config, self.font_group);
var gl_state = gl_state: {
self.font_grid.lock.lockShared();
defer self.font_grid.lock.unlockShared();
break :gl_state try GLState.init(
self.alloc,
self.config,
self.font_grid,
);
};
errdefer gl_state.deinit();
// Unrealize if we have to
@ -491,14 +495,16 @@ pub fn displayRealize(self: *OpenGL) !void {
// reflush everything
self.gl_cells_size = 0;
self.gl_cells_written = 0;
self.font_group.atlas_greyscale.modified = true;
self.font_group.atlas_color.modified = true;
self.texture_greyscale_modified = 0;
self.texture_color_modified = 0;
self.texture_greyscale_resized = 0;
self.texture_color_resized = 0;
// We need to reset our uniforms
if (self.screen_size) |size| {
self.deferred_screen_size = .{ .size = size };
}
self.deferred_font_size = .{ .metrics = metrics };
self.deferred_font_size = .{ .metrics = self.grid_metrics };
self.deferred_config = .{};
}
@ -584,65 +590,20 @@ pub fn setFocus(self: *OpenGL, focus: bool) !void {
/// Set the new font size.
///
/// Must be called on the render thread.
pub fn setFontSize(self: *OpenGL, size: font.face.DesiredSize) !void {
pub fn setFontGrid(self: *OpenGL, grid: *font.SharedGrid) void {
if (single_threaded_draw) self.draw_mutex.lock();
defer if (single_threaded_draw) self.draw_mutex.unlock();
log.info("set font size={}", .{size});
// Set our new size, this will also reset our font atlas.
try self.font_group.setSize(size);
// Reset our GPU uniforms
const metrics = try resetFontMetrics(
self.alloc,
self.font_group,
self.config.font_thicken,
);
// Reset our font grid
self.font_grid = grid;
self.grid_metrics = grid.metrics;
self.texture_greyscale_modified = 0;
self.texture_greyscale_resized = 0;
self.texture_color_modified = 0;
self.texture_color_resized = 0;
// Defer our GPU updates
self.deferred_font_size = .{ .metrics = metrics };
// Recalculate our cell size. If it is the same as before, then we do
// nothing since the grid size couldn't have possibly changed.
if (std.meta.eql(self.grid_metrics, metrics)) return;
self.grid_metrics = metrics;
// Notify the window that the cell size changed.
_ = self.surface_mailbox.push(.{
.cell_size = .{
.width = metrics.cell_width,
.height = metrics.cell_height,
},
}, .{ .forever = {} });
}
/// Reload the font metrics, recalculate cell size, and send that all
/// down to the GPU.
fn resetFontMetrics(
alloc: Allocator,
font_group: *font.GroupCache,
font_thicken: bool,
) !font.face.Metrics {
// Get our cell metrics based on a regular font ascii 'M'. Why 'M'?
// Doesn't matter, any normal ASCII will do we're just trying to make
// sure we use the regular font.
const metrics = metrics: {
const index = (try font_group.indexForCodepoint(alloc, 'M', .regular, .text)).?;
const face = try font_group.group.faceFromIndex(index);
break :metrics face.metrics;
};
log.debug("cell dimensions={}", .{metrics});
// Set details for our sprite font
font_group.group.sprite = font.sprite.Face{
.width = metrics.cell_width,
.height = metrics.cell_height,
.thickness = metrics.underline_thickness * @as(u32, if (font_thicken) 2 else 1),
.underline_position = metrics.underline_position,
};
return metrics;
self.deferred_font_size = .{ .metrics = grid.metrics };
}
/// The primary render callback that is completely thread-safe.
@ -1056,7 +1017,7 @@ pub fn rebuildCells(
// Split our row into runs and shape each one.
var iter = self.font_shaper.runIterator(
self.font_group,
self.font_grid,
screen,
row,
selection,
@ -1170,33 +1131,21 @@ fn addPreeditCell(
const bg = self.foreground_color;
const fg = self.background_color;
// Get the font for this codepoint.
const font_index = if (self.font_group.indexForCodepoint(
// Render the glyph for our preedit text
const render_ = self.font_grid.renderCodepoint(
self.alloc,
@intCast(cp.codepoint),
.regular,
.text,
)) |index| index orelse return else |_| return;
// Get the font face so we can get the glyph
const face = self.font_group.group.faceFromIndex(font_index) catch |err| {
log.warn("error getting face for font_index={} err={}", .{ font_index, err });
return;
};
// Use the face to now get the glyph index
const glyph_index = face.glyphIndex(@intCast(cp.codepoint)) orelse return;
// Render the glyph for our preedit text
const glyph = self.font_group.renderGlyph(
self.alloc,
font_index,
glyph_index,
.{ .grid_metrics = self.grid_metrics },
) catch |err| {
log.warn("error rendering preedit glyph err={}", .{err});
return;
};
const render = render_ orelse {
log.warn("failed to find font for preedit codepoint={X}", .{cp.codepoint});
return;
};
// Add our opaque background cell
self.cells_bg.appendAssumeCapacity(.{
@ -1226,12 +1175,12 @@ fn addPreeditCell(
.grid_col = @intCast(x),
.grid_row = @intCast(y),
.grid_width = if (cp.wide) 2 else 1,
.glyph_x = glyph.atlas_x,
.glyph_y = glyph.atlas_y,
.glyph_width = glyph.width,
.glyph_height = glyph.height,
.glyph_offset_x = glyph.offset_x,
.glyph_offset_y = glyph.offset_y,
.glyph_x = render.glyph.atlas_x,
.glyph_y = render.glyph.atlas_y,
.glyph_width = render.glyph.width,
.glyph_height = render.glyph.height,
.glyph_offset_x = render.glyph.offset_x,
.glyph_offset_y = render.glyph.offset_y,
.r = fg.r,
.g = fg.g,
.b = fg.b,
@ -1275,13 +1224,13 @@ fn addCursor(
.underline => .underline,
};
const glyph = self.font_group.renderGlyph(
const render = self.font_grid.renderGlyph(
self.alloc,
font.sprite_index,
@intFromEnum(sprite),
.{
.grid_metrics = self.grid_metrics,
.cell_width = if (wide) 2 else 1,
.grid_metrics = self.grid_metrics,
},
) catch |err| {
log.warn("error rendering cursor glyph err={}", .{err});
@ -1301,12 +1250,12 @@ fn addCursor(
.bg_g = 0,
.bg_b = 0,
.bg_a = 0,
.glyph_x = glyph.atlas_x,
.glyph_y = glyph.atlas_y,
.glyph_width = glyph.width,
.glyph_height = glyph.height,
.glyph_offset_x = glyph.offset_x,
.glyph_offset_y = glyph.offset_y,
.glyph_x = render.glyph.atlas_x,
.glyph_y = render.glyph.atlas_y,
.glyph_width = render.glyph.width,
.glyph_height = render.glyph.height,
.glyph_offset_x = render.glyph.offset_x,
.glyph_offset_y = render.glyph.offset_y,
});
return &self.cells.items[self.cells.items.len - 1];
@ -1455,7 +1404,7 @@ fn updateCell(
// If the cell has a character, draw it
if (cell.hasText()) fg: {
// Render
const glyph = try self.font_group.renderGlyph(
const render = try self.font_grid.renderGlyph(
self.alloc,
shaper_run.font_index,
shaper_cell.glyph_index orelse break :fg,
@ -1467,9 +1416,8 @@ fn updateCell(
// If we're rendering a color font, we use the color atlas
const mode: CellProgram.CellMode = switch (try fgMode(
&self.font_group.group,
render.presentation,
cell_pin,
shaper_run,
)) {
.normal => .fg,
.color => .fg_color,
@ -1481,12 +1429,12 @@ fn updateCell(
.grid_col = @intCast(x),
.grid_row = @intCast(y),
.grid_width = cell.gridWidth(),
.glyph_x = glyph.atlas_x,
.glyph_y = glyph.atlas_y,
.glyph_width = glyph.width,
.glyph_height = glyph.height,
.glyph_offset_x = glyph.offset_x + shaper_cell.x_offset,
.glyph_offset_y = glyph.offset_y + shaper_cell.y_offset,
.glyph_x = render.glyph.atlas_x,
.glyph_y = render.glyph.atlas_y,
.glyph_width = render.glyph.width,
.glyph_height = render.glyph.height,
.glyph_offset_x = render.glyph.offset_x + shaper_cell.x_offset,
.glyph_offset_y = render.glyph.offset_y + shaper_cell.y_offset,
.r = colors.fg.r,
.g = colors.fg.g,
.b = colors.fg.b,
@ -1508,13 +1456,13 @@ fn updateCell(
.curly => .underline_curly,
};
const underline_glyph = try self.font_group.renderGlyph(
const render = try self.font_grid.renderGlyph(
self.alloc,
font.sprite_index,
@intFromEnum(sprite),
.{
.grid_metrics = self.grid_metrics,
.cell_width = if (cell.wide == .wide) 2 else 1,
.grid_metrics = self.grid_metrics,
},
);
@ -1525,12 +1473,12 @@ fn updateCell(
.grid_col = @intCast(x),
.grid_row = @intCast(y),
.grid_width = cell.gridWidth(),
.glyph_x = underline_glyph.atlas_x,
.glyph_y = underline_glyph.atlas_y,
.glyph_width = underline_glyph.width,
.glyph_height = underline_glyph.height,
.glyph_offset_x = underline_glyph.offset_x,
.glyph_offset_y = underline_glyph.offset_y,
.glyph_x = render.glyph.atlas_x,
.glyph_y = render.glyph.atlas_y,
.glyph_width = render.glyph.width,
.glyph_height = render.glyph.height,
.glyph_offset_x = render.glyph.offset_x,
.glyph_offset_y = render.glyph.offset_y,
.r = color.r,
.g = color.g,
.b = color.b,
@ -1582,16 +1530,6 @@ fn gridSize(self: *const OpenGL, screen_size: renderer.ScreenSize) renderer.Grid
/// Update the configuration.
pub fn changeConfig(self: *OpenGL, config: *DerivedConfig) !void {
// On configuration change we always reset our font group. There
// are a variety of configurations that can change font settings
// so to be safe we just always reset it. This has a performance hit
// when its not necessary but config reloading shouldn't be so
// common to cause a problem.
self.font_group.reset();
self.font_group.group.styles = config.font_styles;
self.font_group.atlas_greyscale.clear();
self.font_group.atlas_color.clear();
// We always redo the font shaper in case font features changed. We
// could check to see if there was an actual config change but this is
// easier and rare enough to not cause performance issues.
@ -1656,74 +1594,78 @@ pub fn setScreenSize(
/// Updates the font texture atlas if it is dirty.
fn flushAtlas(self: *OpenGL) !void {
const gl_state = self.gl_state orelse return;
try flushAtlasSingle(
&self.font_grid.lock,
gl_state.texture,
&self.font_grid.atlas_greyscale,
&self.texture_greyscale_modified,
&self.texture_greyscale_resized,
.red,
.red,
);
try flushAtlasSingle(
&self.font_grid.lock,
gl_state.texture_color,
&self.font_grid.atlas_color,
&self.texture_color_modified,
&self.texture_color_resized,
.rgba,
.bgra,
);
}
{
const atlas = &self.font_group.atlas_greyscale;
if (atlas.modified) {
atlas.modified = false;
var texbind = try gl_state.texture.bind(.@"2D");
defer texbind.unbind();
/// Flush a single atlas, grabbing all necessary locks, checking for
/// changes, etc.
fn flushAtlasSingle(
lock: *std.Thread.RwLock,
texture: gl.Texture,
atlas: *font.Atlas,
modified: *usize,
resized: *usize,
interal_format: gl.Texture.InternalFormat,
format: gl.Texture.Format,
) !void {
// If the texture isn't modified we do nothing
const new_modified = atlas.modified.load(.monotonic);
if (new_modified <= modified.*) return;
if (atlas.resized) {
atlas.resized = false;
try texbind.image2D(
0,
.red,
@intCast(atlas.size),
@intCast(atlas.size),
0,
.red,
.UnsignedByte,
atlas.data.ptr,
);
} else {
try texbind.subImage2D(
0,
0,
0,
@intCast(atlas.size),
@intCast(atlas.size),
.red,
.UnsignedByte,
atlas.data.ptr,
);
}
}
// If it is modified we need to grab a read-lock
lock.lockShared();
defer lock.unlockShared();
var texbind = try texture.bind(.@"2D");
defer texbind.unbind();
const new_resized = atlas.resized.load(.monotonic);
if (new_resized > resized.*) {
try texbind.image2D(
0,
interal_format,
@intCast(atlas.size),
@intCast(atlas.size),
0,
format,
.UnsignedByte,
atlas.data.ptr,
);
// Only update the resized number after successful resize
resized.* = new_resized;
} else {
try texbind.subImage2D(
0,
0,
0,
@intCast(atlas.size),
@intCast(atlas.size),
format,
.UnsignedByte,
atlas.data.ptr,
);
}
{
const atlas = &self.font_group.atlas_color;
if (atlas.modified) {
atlas.modified = false;
var texbind = try gl_state.texture_color.bind(.@"2D");
defer texbind.unbind();
if (atlas.resized) {
atlas.resized = false;
try texbind.image2D(
0,
.rgba,
@intCast(atlas.size),
@intCast(atlas.size),
0,
.bgra,
.UnsignedByte,
atlas.data.ptr,
);
} else {
try texbind.subImage2D(
0,
0,
0,
@intCast(atlas.size),
@intCast(atlas.size),
.bgra,
.UnsignedByte,
atlas.data.ptr,
);
}
}
}
// Update our modified tracker after successful update
modified.* = atlas.modified.load(.monotonic);
}
/// Render renders the current cell state. This will not modify any of
@ -1999,7 +1941,7 @@ const GLState = struct {
pub fn init(
alloc: Allocator,
config: DerivedConfig,
font_group: *font.GroupCache,
font_grid: *font.SharedGrid,
) !GLState {
var arena = ArenaAllocator.init(alloc);
defer arena.deinit();
@ -2045,12 +1987,12 @@ const GLState = struct {
try texbind.image2D(
0,
.red,
@intCast(font_group.atlas_greyscale.size),
@intCast(font_group.atlas_greyscale.size),
@intCast(font_grid.atlas_greyscale.size),
@intCast(font_grid.atlas_greyscale.size),
0,
.red,
.UnsignedByte,
font_group.atlas_greyscale.data.ptr,
font_grid.atlas_greyscale.data.ptr,
);
}
@ -2066,12 +2008,12 @@ const GLState = struct {
try texbind.image2D(
0,
.rgba,
@intCast(font_group.atlas_color.size),
@intCast(font_group.atlas_color.size),
@intCast(font_grid.atlas_color.size),
@intCast(font_grid.atlas_color.size),
0,
.bgra,
.UnsignedByte,
font_group.atlas_color.data.ptr,
font_grid.atlas_color.data.ptr,
);
}

View File

@ -8,8 +8,8 @@ const Config = @import("../config.zig").Config;
/// The derived configuration for this renderer implementation.
config: renderer.Renderer.DerivedConfig,
/// The font group that should be used.
font_group: *font.GroupCache,
/// The font grid that should be used along with the key for deref-ing.
font_grid: *font.SharedGrid,
/// Padding options for the viewport.
padding: Padding,

View File

@ -321,8 +321,9 @@ fn drainMailbox(self: *Thread) !void {
}
},
.font_size => |size| {
try self.renderer.setFontSize(size);
.font_grid => |grid| {
self.renderer.setFontGrid(grid.grid);
grid.set.deref(grid.old_key);
},
.foreground_color => |color| {

View File

@ -20,11 +20,9 @@ pub const FgMode = enum {
/// meant to be called from the typical updateCell function within a
/// renderer.
pub fn fgMode(
group: *font.Group,
presentation: font.Presentation,
cell_pin: terminal.Pin,
shaper_run: font.shape.TextRun,
) !FgMode {
const presentation = try group.presentationFromIndex(shaper_run.font_index);
return switch (presentation) {
// Emoji is always full size and color.
.emoji => .color,

View File

@ -22,10 +22,21 @@ pub const Message = union(enum) {
/// restarting the timer.
reset_cursor_blink: void,
/// Change the font size. This should recalculate the grid size and
/// send a grid size change message back to the window thread if
/// the size changes.
font_size: font.face.DesiredSize,
/// Change the font grid. This can happen for any number of reasons
/// including a font size change, family change, etc.
font_grid: struct {
grid: *font.SharedGrid,
set: *font.SharedGridSet,
// The key for the new grid. If adopting the new grid fails for any
// reason, the old grid should be kept but the new key should be
// dereferenced.
new_key: font.SharedGridSet.Key,
// After accepting the new grid, the old grid must be dereferenced
// using the fields below.
old_key: font.SharedGridSet.Key,
},
/// Change the foreground color. This can be done separately from changing
/// the config file in response to an OSC 10 command.

View File

@ -17,26 +17,6 @@ const log = std.log.scoped(.renderer_size);
pub const CellSize = struct {
width: u32,
height: u32,
/// Initialize the cell size information from a font group. This ensures
/// that all renderers use the same cell sizing information for the same
/// fonts.
pub fn init(alloc: Allocator, group: *font.GroupCache) !CellSize {
// Get our cell metrics based on a regular font ascii 'M'. Why 'M'?
// Doesn't matter, any normal ASCII will do we're just trying to make
// sure we use the regular font.
const metrics = metrics: {
const index = (try group.indexForCodepoint(alloc, 'M', .regular, .text)).?;
const face = try group.group.faceFromIndex(index);
break :metrics face.metrics;
};
log.debug("cell dimensions={}", .{metrics});
return CellSize{
.width = metrics.cell_width,
.height = metrics.cell_height,
};
}
};
/// The dimensions of the screen that the grid is rendered to. This is the