mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
Merge pull request #1662 from mitchellh/fontmem
Dedupe font stack for terminals with identical font configuration
This commit is contained in:
34
src/App.zig
34
src/App.zig
@ -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| {
|
||||
|
234
src/Surface.zig
234
src/Surface.zig
@ -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 => {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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) {
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
|
522
src/font/CodepointResolver.zig
Normal file
522
src/font/CodepointResolver.zig
Normal 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
649
src/font/Collection.zig
Normal 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
|
||||
}
|
@ -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 {},
|
||||
|
1173
src/font/Group.zig
1173
src/font/Group.zig
File diff suppressed because it is too large
Load Diff
@ -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
359
src/font/SharedGrid.zig
Normal 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
632
src/font/SharedGridSet.zig
Normal 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());
|
||||
}
|
@ -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" {
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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 },
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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| {
|
||||
|
@ -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,
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user