mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-17 17:26: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.
|
/// Set to true once we're quitting. This never goes false again.
|
||||||
quit: bool,
|
quit: bool,
|
||||||
|
|
||||||
/// Font discovery mechanism. This is only safe to use from the main thread.
|
/// The set of font GroupCache instances shared by surfaces with the
|
||||||
/// This is lazily initialized on the first call to fontDiscover so do
|
/// same font configuration.
|
||||||
/// not access this directly.
|
font_grid_set: font.SharedGridSet,
|
||||||
font_discover: ?font.Discover = null,
|
|
||||||
|
|
||||||
/// Initialize the main app instance. This creates the main window, sets
|
/// Initialize the main app instance. This creates the main window, sets
|
||||||
/// up the renderer state, compiles the shaders, etc. This is the primary
|
/// 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);
|
var app = try alloc.create(App);
|
||||||
errdefer alloc.destroy(app);
|
errdefer alloc.destroy(app);
|
||||||
|
|
||||||
|
var font_grid_set = try font.SharedGridSet.init(alloc);
|
||||||
|
errdefer font_grid_set.deinit();
|
||||||
|
|
||||||
app.* = .{
|
app.* = .{
|
||||||
.alloc = alloc,
|
.alloc = alloc,
|
||||||
.surfaces = .{},
|
.surfaces = .{},
|
||||||
.mailbox = .{},
|
.mailbox = .{},
|
||||||
.quit = false,
|
.quit = false,
|
||||||
|
.font_grid_set = font_grid_set,
|
||||||
};
|
};
|
||||||
errdefer app.surfaces.deinit(alloc);
|
errdefer app.surfaces.deinit(alloc);
|
||||||
|
|
||||||
@ -71,9 +74,12 @@ pub fn destroy(self: *App) void {
|
|||||||
for (self.surfaces.items) |surface| surface.deinit();
|
for (self.surfaces.items) |surface| surface.deinit();
|
||||||
self.surfaces.deinit(self.alloc);
|
self.surfaces.deinit(self.alloc);
|
||||||
|
|
||||||
if (comptime font.Discover != void) {
|
// Clean up our font group cache
|
||||||
if (self.font_discover) |*v| v.deinit();
|
// 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);
|
self.alloc.destroy(self);
|
||||||
}
|
}
|
||||||
@ -166,20 +172,6 @@ pub fn needsConfirmQuit(self: *const App) bool {
|
|||||||
return false;
|
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.
|
/// Drain the mailbox.
|
||||||
fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
|
fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
|
||||||
while (self.mailbox.pop()) |message| {
|
while (self.mailbox.pop()) |message| {
|
||||||
|
232
src/Surface.zig
232
src/Surface.zig
@ -54,8 +54,7 @@ rt_app: *apprt.runtime.App,
|
|||||||
rt_surface: *apprt.runtime.Surface,
|
rt_surface: *apprt.runtime.Surface,
|
||||||
|
|
||||||
/// The font structures
|
/// The font structures
|
||||||
font_lib: font.Library,
|
font_grid_key: font.SharedGridSet.Key,
|
||||||
font_group: *font.GroupCache,
|
|
||||||
font_size: font.face.DesiredSize,
|
font_size: font.face.DesiredSize,
|
||||||
|
|
||||||
/// The renderer for this surface.
|
/// The renderer for this surface.
|
||||||
@ -206,6 +205,7 @@ const DerivedConfig = struct {
|
|||||||
confirm_close_surface: bool,
|
confirm_close_surface: bool,
|
||||||
cursor_click_to_move: bool,
|
cursor_click_to_move: bool,
|
||||||
desktop_notifications: bool,
|
desktop_notifications: bool,
|
||||||
|
font: font.SharedGridSet.DerivedConfig,
|
||||||
mouse_interval: u64,
|
mouse_interval: u64,
|
||||||
mouse_hide_while_typing: bool,
|
mouse_hide_while_typing: bool,
|
||||||
mouse_scroll_multiplier: f64,
|
mouse_scroll_multiplier: f64,
|
||||||
@ -263,6 +263,7 @@ const DerivedConfig = struct {
|
|||||||
.confirm_close_surface = config.@"confirm-close-surface",
|
.confirm_close_surface = config.@"confirm-close-surface",
|
||||||
.cursor_click_to_move = config.@"cursor-click-to-move",
|
.cursor_click_to_move = config.@"cursor-click-to-move",
|
||||||
.desktop_notifications = config.@"desktop-notifications",
|
.desktop_notifications = config.@"desktop-notifications",
|
||||||
|
.font = try font.SharedGridSet.DerivedConfig.init(alloc, config),
|
||||||
.mouse_interval = config.@"click-repeat-interval" * 1_000_000, // 500ms
|
.mouse_interval = config.@"click-repeat-interval" * 1_000_000, // 500ms
|
||||||
.mouse_hide_while_typing = config.@"mouse-hide-while-typing",
|
.mouse_hide_while_typing = config.@"mouse-hide-while-typing",
|
||||||
.mouse_scroll_multiplier = config.@"mouse-scroll-multiplier",
|
.mouse_scroll_multiplier = config.@"mouse-scroll-multiplier",
|
||||||
@ -298,6 +299,10 @@ pub fn init(
|
|||||||
rt_app: *apprt.runtime.App,
|
rt_app: *apprt.runtime.App,
|
||||||
rt_surface: *apprt.runtime.Surface,
|
rt_surface: *apprt.runtime.Surface,
|
||||||
) !void {
|
) !void {
|
||||||
|
// Get our configuration
|
||||||
|
var derived_config = try DerivedConfig.init(alloc, config);
|
||||||
|
errdefer derived_config.deinit();
|
||||||
|
|
||||||
// Initialize our renderer with our initialized surface.
|
// Initialize our renderer with our initialized surface.
|
||||||
try Renderer.surfaceInit(rt_surface);
|
try Renderer.surfaceInit(rt_surface);
|
||||||
|
|
||||||
@ -320,174 +325,15 @@ pub fn init(
|
|||||||
.ydpi = @intFromFloat(y_dpi),
|
.ydpi = @intFromFloat(y_dpi),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Find all the fonts for this surface
|
// Setup our font group. This will reuse an existing font group if
|
||||||
//
|
// it was already loaded.
|
||||||
// Future: we can share the font group amongst all surfaces to save
|
const font_grid_key, const font_grid = try app.font_grid_set.ref(
|
||||||
// some new surface init time and some memory. This will require making
|
&derived_config.font,
|
||||||
// thread-safe changes to font structs.
|
font_size,
|
||||||
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", .{});
|
|
||||||
|
|
||||||
// Pre-calculate our initial cell size ourselves.
|
// 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
|
// Convert our padding from points to pixels
|
||||||
const padding_x: u32 = padding_x: {
|
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 };
|
const app_mailbox: App.Mailbox = .{ .rt_app = rt_app, .mailbox = &app.mailbox };
|
||||||
var renderer_impl = try Renderer.init(alloc, .{
|
var renderer_impl = try Renderer.init(alloc, .{
|
||||||
.config = try Renderer.DerivedConfig.init(alloc, config),
|
.config = try Renderer.DerivedConfig.init(alloc, config),
|
||||||
.font_group = font_group,
|
.font_grid = font_grid,
|
||||||
.padding = .{
|
.padding = .{
|
||||||
.explicit = padding,
|
.explicit = padding,
|
||||||
.balance = config.@"window-padding-balance",
|
.balance = config.@"window-padding-balance",
|
||||||
@ -570,8 +416,7 @@ pub fn init(
|
|||||||
.app = app,
|
.app = app,
|
||||||
.rt_app = rt_app,
|
.rt_app = rt_app,
|
||||||
.rt_surface = rt_surface,
|
.rt_surface = rt_surface,
|
||||||
.font_lib = font_lib,
|
.font_grid_key = font_grid_key,
|
||||||
.font_group = font_group,
|
|
||||||
.font_size = font_size,
|
.font_size = font_size,
|
||||||
.renderer = renderer_impl,
|
.renderer = renderer_impl,
|
||||||
.renderer_thread = render_thread,
|
.renderer_thread = render_thread,
|
||||||
@ -588,7 +433,7 @@ pub fn init(
|
|||||||
.grid_size = .{},
|
.grid_size = .{},
|
||||||
.cell_size = cell_size,
|
.cell_size = cell_size,
|
||||||
.padding = padding,
|
.padding = padding,
|
||||||
.config = try DerivedConfig.init(alloc, config),
|
.config = derived_config,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Report initial cell size on surface creation
|
// Report initial cell size on surface creation
|
||||||
@ -686,15 +531,14 @@ pub fn deinit(self: *Surface) void {
|
|||||||
self.io_thread.deinit();
|
self.io_thread.deinit();
|
||||||
self.io.deinit();
|
self.io.deinit();
|
||||||
|
|
||||||
self.font_group.deinit(self.alloc);
|
|
||||||
self.font_lib.deinit();
|
|
||||||
self.alloc.destroy(self.font_group);
|
|
||||||
|
|
||||||
if (self.inspector) |v| {
|
if (self.inspector) |v| {
|
||||||
v.deinit();
|
v.deinit();
|
||||||
self.alloc.destroy(v);
|
self.alloc.destroy(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up our font grid
|
||||||
|
self.app.font_grid_set.deref(self.font_grid_key);
|
||||||
|
|
||||||
// Clean up our render state
|
// Clean up our render state
|
||||||
if (self.renderer_state.preedit) |p| self.alloc.free(p.codepoints);
|
if (self.renderer_state.preedit) |p| self.alloc.free(p.codepoints);
|
||||||
self.alloc.destroy(self.renderer_state.mutex);
|
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);
|
try self.rt_surface.setMouseShape(shape);
|
||||||
},
|
},
|
||||||
|
|
||||||
.cell_size => |size| try self.setCellSize(size),
|
|
||||||
|
|
||||||
.clipboard_read => |clipboard| {
|
.clipboard_read => |clipboard| {
|
||||||
if (self.config.clipboard_read == .deny) {
|
if (self.config.clipboard_read == .deny) {
|
||||||
log.info("application attempted to read clipboard, but 'clipboard-read' is set to 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.
|
/// Change the font size.
|
||||||
///
|
///
|
||||||
/// This can only be called from the main thread.
|
/// 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
|
// Update our font size so future changes work
|
||||||
self.font_size = size;
|
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(.{
|
_ = 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 = {} });
|
}, .{ .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
|
// Schedule render which also drains our mailbox
|
||||||
self.queueRender() catch unreachable;
|
self.queueRender() catch unreachable;
|
||||||
}
|
}
|
||||||
@ -1839,7 +1703,7 @@ pub fn contentScaleCallback(self: *Surface, content_scale: apprt.ContentScale) !
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.setFontSize(size);
|
try self.setFontSize(size);
|
||||||
|
|
||||||
// Update our padding which is dependent on DPI.
|
// Update our padding which is dependent on DPI.
|
||||||
self.padding = padding: {
|
self.padding = padding: {
|
||||||
@ -3138,7 +3002,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||||||
|
|
||||||
var size = self.font_size;
|
var size = self.font_size;
|
||||||
size.points +|= delta;
|
size.points +|= delta;
|
||||||
self.setFontSize(size);
|
try self.setFontSize(size);
|
||||||
},
|
},
|
||||||
|
|
||||||
.decrease_font_size => |delta| {
|
.decrease_font_size => |delta| {
|
||||||
@ -3146,7 +3010,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||||||
|
|
||||||
var size = self.font_size;
|
var size = self.font_size;
|
||||||
size.points = @max(1, size.points -| delta);
|
size.points = @max(1, size.points -| delta);
|
||||||
self.setFontSize(size);
|
try self.setFontSize(size);
|
||||||
},
|
},
|
||||||
|
|
||||||
.reset_font_size => {
|
.reset_font_size => {
|
||||||
@ -3154,7 +3018,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||||||
|
|
||||||
var size = self.font_size;
|
var size = self.font_size;
|
||||||
size.points = self.config.original_font_size;
|
size.points = self.config.original_font_size;
|
||||||
self.setFontSize(size);
|
try self.setFontSize(size);
|
||||||
},
|
},
|
||||||
|
|
||||||
.clear_screen => {
|
.clear_screen => {
|
||||||
|
@ -414,7 +414,7 @@ pub const Surface = struct {
|
|||||||
if (opts.font_size != 0) {
|
if (opts.font_size != 0) {
|
||||||
var font_size = self.core_surface.font_size;
|
var font_size = self.core_surface.font_size;
|
||||||
font_size.points = opts.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 we have a parent, inherit some properties
|
||||||
if (self.config.@"window-inherit-font-size") {
|
if (self.config.@"window-inherit-font-size") {
|
||||||
if (parent_) |parent| {
|
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 we have a font size we want, set that now
|
||||||
if (self.font_size) |size| {
|
if (self.font_size) |size| {
|
||||||
self.core_surface.setFontSize(size);
|
try self.core_surface.setFontSize(size);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the intial color scheme
|
// Set the intial color scheme
|
||||||
|
@ -21,9 +21,6 @@ pub const Message = union(enum) {
|
|||||||
/// Set the mouse shape.
|
/// Set the mouse shape.
|
||||||
set_mouse_shape: terminal.MouseShape,
|
set_mouse_shape: terminal.MouseShape,
|
||||||
|
|
||||||
/// Change the cell size.
|
|
||||||
cell_size: renderer.CellSize,
|
|
||||||
|
|
||||||
/// Read the clipboard and write to the pty.
|
/// Read the clipboard and write to the pty.
|
||||||
clipboard_read: apprt.Clipboard,
|
clipboard_read: apprt.Clipboard,
|
||||||
|
|
||||||
|
@ -8,14 +8,18 @@ pub const edit = @import("config/edit.zig");
|
|||||||
pub const url = @import("config/url.zig");
|
pub const url = @import("config/url.zig");
|
||||||
|
|
||||||
// Field types
|
// Field types
|
||||||
|
pub const ClipboardAccess = Config.ClipboardAccess;
|
||||||
pub const CopyOnSelect = Config.CopyOnSelect;
|
pub const CopyOnSelect = Config.CopyOnSelect;
|
||||||
|
pub const CustomShaderAnimation = Config.CustomShaderAnimation;
|
||||||
|
pub const FontStyle = Config.FontStyle;
|
||||||
pub const Keybinds = Config.Keybinds;
|
pub const Keybinds = Config.Keybinds;
|
||||||
pub const MouseShiftCapture = Config.MouseShiftCapture;
|
pub const MouseShiftCapture = Config.MouseShiftCapture;
|
||||||
pub const CustomShaderAnimation = Config.CustomShaderAnimation;
|
|
||||||
pub const NonNativeFullscreen = Config.NonNativeFullscreen;
|
pub const NonNativeFullscreen = Config.NonNativeFullscreen;
|
||||||
pub const OptionAsAlt = Config.OptionAsAlt;
|
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 ShellIntegrationFeatures = Config.ShellIntegrationFeatures;
|
||||||
pub const ClipboardAccess = Config.ClipboardAccess;
|
|
||||||
|
|
||||||
// Alternate APIs
|
// Alternate APIs
|
||||||
pub const CAPI = @import("config/CAPI.zig");
|
pub const CAPI = @import("config/CAPI.zig");
|
||||||
|
@ -2498,9 +2498,14 @@ pub const RepeatableString = struct {
|
|||||||
|
|
||||||
/// Deep copy of the struct. Required by Config.
|
/// Deep copy of the struct. Required by Config.
|
||||||
pub fn clone(self: *const Self, alloc: Allocator) !Self {
|
pub fn clone(self: *const Self, alloc: Allocator) !Self {
|
||||||
return .{
|
// Copy the list and all the strings in the list.
|
||||||
.list = try self.list.clone(alloc),
|
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
|
/// The number of itemsin the list
|
||||||
@ -2960,9 +2965,7 @@ pub const RepeatableCodepointMap = struct {
|
|||||||
|
|
||||||
/// Deep copy of the struct. Required by Config.
|
/// Deep copy of the struct. Required by Config.
|
||||||
pub fn clone(self: *const Self, alloc: Allocator) !Self {
|
pub fn clone(self: *const Self, alloc: Allocator) !Self {
|
||||||
return .{
|
return .{ .map = try self.map.clone(alloc) };
|
||||||
.map = .{ .list = try self.map.list.clone(alloc) },
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compare if two of our value are requal. Required by Config.
|
/// 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
|
/// Used by Formatter
|
||||||
pub fn formatEntry(self: Self, formatter: anytype) !void {
|
pub fn formatEntry(self: Self, formatter: anytype) !void {
|
||||||
switch (self) {
|
switch (self) {
|
||||||
|
@ -38,18 +38,16 @@ nodes: std.ArrayListUnmanaged(Node) = .{},
|
|||||||
/// different formats, you must use multiple atlases or convert the textures.
|
/// different formats, you must use multiple atlases or convert the textures.
|
||||||
format: Format = .greyscale,
|
format: Format = .greyscale,
|
||||||
|
|
||||||
/// This will be set to true when the atlas has data set on it. It is up
|
/// This will be incremented every time the atlas is modified. This is useful
|
||||||
/// to the user of the atlas to set this to false when they observe the value.
|
/// for knowing if the texture data has changed since the last time it was
|
||||||
/// This is a useful value to know if you need to send new data to the GPU or
|
/// sent to the GPU. It is up the user of the atlas to read this value atomically
|
||||||
/// not.
|
/// to observe it.
|
||||||
modified: bool = false,
|
modified: std.atomic.Value(usize) = .{ .raw = 0 },
|
||||||
|
|
||||||
/// This will be set to true when the atlas has been resized. It is up
|
/// This will be incremented every time the atlas is resized. This is useful
|
||||||
/// to the user of the atlas to set this to false when they observe the value.
|
/// for knowing if a GPU texture can be updated in-place or if it requires
|
||||||
/// The resized value is useful for sending textures to the GPU to know if
|
/// a resize operation.
|
||||||
/// a new texture needs to be allocated or if an existing one can be
|
resized: std.atomic.Value(usize) = .{ .raw = 0 },
|
||||||
/// updated in-place.
|
|
||||||
resized: bool = false,
|
|
||||||
|
|
||||||
pub const Format = enum(u8) {
|
pub const Format = enum(u8) {
|
||||||
greyscale = 0,
|
greyscale = 0,
|
||||||
@ -99,7 +97,6 @@ pub fn init(alloc: Allocator, size: u32, format: Format) !Atlas {
|
|||||||
|
|
||||||
// This sets up our initial state
|
// This sets up our initial state
|
||||||
result.clear();
|
result.clear();
|
||||||
result.modified = false;
|
|
||||||
|
|
||||||
return result;
|
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.
|
// 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() ..]);
|
}, data_old[size_old * self.format.depth() ..]);
|
||||||
|
|
||||||
// We are both modified and resized
|
// We are both modified and resized
|
||||||
self.modified = true;
|
_ = self.modified.fetchAdd(1, .monotonic);
|
||||||
self.resized = true;
|
_ = self.resized.fetchAdd(1, .monotonic);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty the atlas. This doesn't reclaim any previously allocated memory.
|
// Empty the atlas. This doesn't reclaim any previously allocated memory.
|
||||||
pub fn clear(self: *Atlas) void {
|
pub fn clear(self: *Atlas) void {
|
||||||
self.modified = true;
|
_ = self.modified.fetchAdd(1, .monotonic);
|
||||||
@memset(self.data, 0);
|
@memset(self.data, 0);
|
||||||
self.nodes.clearRetainingCapacity();
|
self.nodes.clearRetainingCapacity();
|
||||||
|
|
||||||
@ -475,8 +472,9 @@ test "exact fit" {
|
|||||||
var atlas = try init(alloc, 34, .greyscale); // +2 for 1px border
|
var atlas = try init(alloc, 34, .greyscale); // +2 for 1px border
|
||||||
defer atlas.deinit(alloc);
|
defer atlas.deinit(alloc);
|
||||||
|
|
||||||
|
const modified = atlas.modified.load(.monotonic);
|
||||||
_ = try atlas.reserve(alloc, 32, 32);
|
_ = 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));
|
try testing.expectError(Error.AtlasFull, atlas.reserve(alloc, 1, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -505,9 +503,10 @@ test "writing data" {
|
|||||||
defer atlas.deinit(alloc);
|
defer atlas.deinit(alloc);
|
||||||
|
|
||||||
const reg = try atlas.reserve(alloc, 2, 2);
|
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 });
|
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
|
// 33 because of the 1px border and so on
|
||||||
try testing.expectEqual(@as(u8, 1), atlas.data[33]);
|
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, 3), atlas.data[9]);
|
||||||
try testing.expectEqual(@as(u8, 4), atlas.data[10]);
|
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.
|
// 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 atlas.grow(alloc, atlas.size + 1);
|
||||||
try testing.expect(atlas.modified);
|
const new_modified = atlas.modified.load(.monotonic);
|
||||||
try testing.expect(atlas.resized);
|
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);
|
_ = try atlas.reserve(alloc, 1, 1);
|
||||||
|
|
||||||
// Ensure our data is still set. Not the offsets change due to size.
|
// 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);
|
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.
|
/// Add an entry to the map.
|
||||||
///
|
///
|
||||||
/// For conflicting codepoints, entries added later take priority over
|
/// 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;
|
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" {
|
test "codepointmap" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const alloc = testing.allocator;
|
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);
|
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
|
/// Fontconfig
|
||||||
fc: if (options.backend == .fontconfig_freetype) ?Fontconfig else void =
|
fc: if (options.backend == .fontconfig_freetype) ?Fontconfig else void =
|
||||||
if (options.backend == .fontconfig_freetype) null else {},
|
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.
|
/// will be preferred, but not guaranteed.
|
||||||
variations: []const Variation = &.{},
|
variations: []const Variation = &.{},
|
||||||
|
|
||||||
/// Returns a hash code that can be used to uniquely identify this
|
/// Hash the descriptor with the given hasher.
|
||||||
/// action.
|
pub fn hash(self: Descriptor, hasher: anytype) void {
|
||||||
pub fn hash(self: Descriptor) u64 {
|
|
||||||
const autoHash = std.hash.autoHash;
|
const autoHash = std.hash.autoHash;
|
||||||
var hasher = std.hash.Wyhash.init(0);
|
const autoHashStrat = std.hash.autoHashStrat;
|
||||||
autoHash(&hasher, self.family);
|
autoHashStrat(hasher, self.family, .Deep);
|
||||||
autoHash(&hasher, self.style);
|
autoHashStrat(hasher, self.style, .Deep);
|
||||||
autoHash(&hasher, self.codepoint);
|
autoHash(hasher, self.codepoint);
|
||||||
autoHash(&hasher, self.size);
|
autoHash(hasher, self.size);
|
||||||
autoHash(&hasher, self.bold);
|
autoHash(hasher, self.bold);
|
||||||
autoHash(&hasher, self.italic);
|
autoHash(hasher, self.italic);
|
||||||
autoHash(&hasher, self.monospace);
|
autoHash(hasher, self.monospace);
|
||||||
autoHash(&hasher, self.variations.len);
|
autoHash(hasher, self.variations.len);
|
||||||
for (self.variations) |variation| {
|
for (self.variations) |variation| {
|
||||||
autoHash(&hasher, variation.id);
|
autoHash(hasher, variation.id);
|
||||||
|
|
||||||
// This is not correct, but we don't currently depend on the
|
// This is not correct, but we don't currently depend on the
|
||||||
// hash value being different based on decimal values of variations.
|
// 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();
|
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
|
/// Convert to Fontconfig pattern to use for lookup. The pattern does
|
||||||
/// not have defaults filled/substituted (Fontconfig thing) so callers
|
/// not have defaults filled/substituted (Fontconfig thing) so callers
|
||||||
/// must still do this.
|
/// must still do this.
|
||||||
@ -552,7 +572,7 @@ test "descriptor hash" {
|
|||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
|
|
||||||
var d: Descriptor = .{};
|
var d: Descriptor = .{};
|
||||||
try testing.expect(d.hash() != 0);
|
try testing.expect(d.hashcode() != 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "descriptor hash familiy names" {
|
test "descriptor hash familiy names" {
|
||||||
@ -560,7 +580,7 @@ test "descriptor hash familiy names" {
|
|||||||
|
|
||||||
var d1: Descriptor = .{ .family = "A" };
|
var d1: Descriptor = .{ .family = "A" };
|
||||||
var d2: Descriptor = .{ .family = "B" };
|
var d2: Descriptor = .{ .family = "B" };
|
||||||
try testing.expect(d1.hash() != d2.hash());
|
try testing.expect(d1.hashcode() != d2.hashcode());
|
||||||
}
|
}
|
||||||
|
|
||||||
test "fontconfig" {
|
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" {
|
test "formatConfig percent" {
|
||||||
const configpkg = @import("../../config.zig");
|
const configpkg = @import("../../config.zig");
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
|
@ -6,15 +6,19 @@ pub const Atlas = @import("Atlas.zig");
|
|||||||
pub const discovery = @import("discovery.zig");
|
pub const discovery = @import("discovery.zig");
|
||||||
pub const face = @import("face.zig");
|
pub const face = @import("face.zig");
|
||||||
pub const CodepointMap = @import("CodepointMap.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 DeferredFace = @import("DeferredFace.zig");
|
||||||
pub const Face = face.Face;
|
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 Glyph = @import("Glyph.zig");
|
||||||
|
pub const Metrics = face.Metrics;
|
||||||
pub const shape = @import("shape.zig");
|
pub const shape = @import("shape.zig");
|
||||||
pub const Shaper = shape.Shaper;
|
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 = @import("sprite.zig");
|
||||||
pub const Sprite = sprite.Sprite;
|
pub const Sprite = sprite.Sprite;
|
||||||
|
pub const SpriteFace = sprite.Face;
|
||||||
pub const Descriptor = discovery.Descriptor;
|
pub const Descriptor = discovery.Descriptor;
|
||||||
pub const Discover = discovery.Discover;
|
pub const Discover = discovery.Discover;
|
||||||
pub usingnamespace @import("library.zig");
|
pub usingnamespace @import("library.zig");
|
||||||
@ -23,8 +27,6 @@ pub usingnamespace @import("library.zig");
|
|||||||
pub usingnamespace if (builtin.target.isWasm()) struct {
|
pub usingnamespace if (builtin.target.isWasm()) struct {
|
||||||
pub usingnamespace Atlas.Wasm;
|
pub usingnamespace Atlas.Wasm;
|
||||||
pub usingnamespace DeferredFace.Wasm;
|
pub usingnamespace DeferredFace.Wasm;
|
||||||
pub usingnamespace Group.Wasm;
|
|
||||||
pub usingnamespace GroupCache.Wasm;
|
|
||||||
pub usingnamespace face.web_canvas.Wasm;
|
pub usingnamespace face.web_canvas.Wasm;
|
||||||
pub usingnamespace shape.web_canvas.Wasm;
|
pub usingnamespace shape.web_canvas.Wasm;
|
||||||
} else struct {};
|
} else struct {};
|
||||||
@ -145,7 +147,7 @@ pub const Presentation = enum(u1) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// A FontIndex that can be used to use the sprite font directly.
|
/// 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 {
|
test {
|
||||||
// For non-wasm we want to test everything we can
|
// For non-wasm we want to test everything we can
|
||||||
|
@ -5,10 +5,12 @@ const macos = @import("macos");
|
|||||||
const trace = @import("tracy").trace;
|
const trace = @import("tracy").trace;
|
||||||
const font = @import("../main.zig");
|
const font = @import("../main.zig");
|
||||||
const Face = font.Face;
|
const Face = font.Face;
|
||||||
|
const Collection = font.Collection;
|
||||||
const DeferredFace = font.DeferredFace;
|
const DeferredFace = font.DeferredFace;
|
||||||
const Group = font.Group;
|
const Group = font.Group;
|
||||||
const GroupCache = font.GroupCache;
|
const GroupCache = font.GroupCache;
|
||||||
const Library = font.Library;
|
const Library = font.Library;
|
||||||
|
const SharedGrid = font.SharedGrid;
|
||||||
const Style = font.Style;
|
const Style = font.Style;
|
||||||
const Presentation = font.Presentation;
|
const Presentation = font.Presentation;
|
||||||
const terminal = @import("../../terminal/main.zig");
|
const terminal = @import("../../terminal/main.zig");
|
||||||
@ -189,7 +191,7 @@ pub const Shaper = struct {
|
|||||||
|
|
||||||
pub fn runIterator(
|
pub fn runIterator(
|
||||||
self: *Shaper,
|
self: *Shaper,
|
||||||
group: *GroupCache,
|
grid: *SharedGrid,
|
||||||
screen: *const terminal.Screen,
|
screen: *const terminal.Screen,
|
||||||
row: terminal.Pin,
|
row: terminal.Pin,
|
||||||
selection: ?terminal.Selection,
|
selection: ?terminal.Selection,
|
||||||
@ -197,7 +199,7 @@ pub const Shaper = struct {
|
|||||||
) font.shape.RunIterator {
|
) font.shape.RunIterator {
|
||||||
return .{
|
return .{
|
||||||
.hooks = .{ .shaper = self },
|
.hooks = .{ .shaper = self },
|
||||||
.group = group,
|
.grid = grid,
|
||||||
.screen = screen,
|
.screen = screen,
|
||||||
.row = row,
|
.row = row,
|
||||||
.selection = selection,
|
.selection = selection,
|
||||||
@ -231,7 +233,24 @@ pub const Shaper = struct {
|
|||||||
// Get our font. We have to apply the font features we want for
|
// Get our font. We have to apply the font features we want for
|
||||||
// the font here.
|
// the font here.
|
||||||
const run_font: *macos.text.Font = font: {
|
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 original = face.font;
|
||||||
|
|
||||||
const attrs = try self.features.attrsDict(face.quirks_disable_default_font_features);
|
const attrs = try self.features.attrsDict(face.quirks_disable_default_font_features);
|
||||||
@ -400,7 +419,7 @@ test "run iterator" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -419,7 +438,7 @@ test "run iterator" {
|
|||||||
|
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -439,7 +458,7 @@ test "run iterator" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -486,7 +505,7 @@ test "run iterator: empty cells with background set" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -527,7 +546,7 @@ test "shape" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -562,7 +581,7 @@ test "shape nerd fonts" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -590,7 +609,7 @@ test "shape inconsolata ligs" {
|
|||||||
|
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -614,7 +633,7 @@ test "shape inconsolata ligs" {
|
|||||||
|
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -646,7 +665,7 @@ test "shape monaspace ligs" {
|
|||||||
|
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -712,7 +731,7 @@ test "shape emoji width" {
|
|||||||
|
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -752,7 +771,7 @@ test "shape emoji width long" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -789,7 +808,7 @@ test "shape variation selector VS15" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -824,7 +843,7 @@ test "shape variation selector VS16" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -856,7 +875,7 @@ test "shape with empty cells in between" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -894,7 +913,7 @@ test "shape Chinese characters" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -921,13 +940,6 @@ test "shape box glyphs" {
|
|||||||
var testdata = try testShaper(alloc);
|
var testdata = try testShaper(alloc);
|
||||||
defer testdata.deinit();
|
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: [32]u8 = undefined;
|
||||||
var buf_idx: usize = 0;
|
var buf_idx: usize = 0;
|
||||||
buf_idx += try std.unicode.utf8Encode(0x2500, buf[buf_idx..]); // horiz line
|
buf_idx += try std.unicode.utf8Encode(0x2500, buf[buf_idx..]); // horiz line
|
||||||
@ -941,7 +953,7 @@ test "shape box glyphs" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -977,7 +989,7 @@ test "shape selection boundary" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
terminal.Selection.init(
|
terminal.Selection.init(
|
||||||
@ -1000,7 +1012,7 @@ test "shape selection boundary" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
terminal.Selection.init(
|
terminal.Selection.init(
|
||||||
@ -1023,7 +1035,7 @@ test "shape selection boundary" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
terminal.Selection.init(
|
terminal.Selection.init(
|
||||||
@ -1046,7 +1058,7 @@ test "shape selection boundary" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
terminal.Selection.init(
|
terminal.Selection.init(
|
||||||
@ -1069,7 +1081,7 @@ test "shape selection boundary" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
terminal.Selection.init(
|
terminal.Selection.init(
|
||||||
@ -1105,7 +1117,7 @@ test "shape cursor boundary" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -1124,7 +1136,7 @@ test "shape cursor boundary" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -1143,7 +1155,7 @@ test "shape cursor boundary" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -1162,7 +1174,7 @@ test "shape cursor boundary" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -1194,7 +1206,7 @@ test "shape cursor boundary and colored emoji" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -1213,7 +1225,7 @@ test "shape cursor boundary and colored emoji" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -1230,7 +1242,7 @@ test "shape cursor boundary and colored emoji" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -1260,7 +1272,7 @@ test "shape cell attribute change" {
|
|||||||
|
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -1284,7 +1296,7 @@ test "shape cell attribute change" {
|
|||||||
|
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -1309,7 +1321,7 @@ test "shape cell attribute change" {
|
|||||||
|
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -1334,7 +1346,7 @@ test "shape cell attribute change" {
|
|||||||
|
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -1358,7 +1370,7 @@ test "shape cell attribute change" {
|
|||||||
|
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -1376,13 +1388,13 @@ test "shape cell attribute change" {
|
|||||||
const TestShaper = struct {
|
const TestShaper = struct {
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
shaper: Shaper,
|
shaper: Shaper,
|
||||||
cache: *GroupCache,
|
grid: *SharedGrid,
|
||||||
lib: Library,
|
lib: Library,
|
||||||
|
|
||||||
pub fn deinit(self: *TestShaper) void {
|
pub fn deinit(self: *TestShaper) void {
|
||||||
self.shaper.deinit();
|
self.shaper.deinit();
|
||||||
self.cache.deinit(self.alloc);
|
self.grid.deinit(self.alloc);
|
||||||
self.alloc.destroy(self.cache);
|
self.alloc.destroy(self.grid);
|
||||||
self.lib.deinit();
|
self.lib.deinit();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -1412,17 +1424,11 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
|
|||||||
var lib = try Library.init();
|
var lib = try Library.init();
|
||||||
errdefer lib.deinit();
|
errdefer lib.deinit();
|
||||||
|
|
||||||
var cache_ptr = try alloc.create(GroupCache);
|
var c = try Collection.init(alloc);
|
||||||
errdefer alloc.destroy(cache_ptr);
|
c.load_options = .{ .library = lib };
|
||||||
cache_ptr.* = try GroupCache.init(alloc, try Group.init(
|
|
||||||
alloc,
|
|
||||||
lib,
|
|
||||||
.{ .points = 12 },
|
|
||||||
));
|
|
||||||
errdefer cache_ptr.*.deinit(alloc);
|
|
||||||
|
|
||||||
// Setup group
|
// Setup group
|
||||||
_ = try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init(
|
_ = try c.add(alloc, .regular, .{ .loaded = try Face.init(
|
||||||
lib,
|
lib,
|
||||||
testFont,
|
testFont,
|
||||||
.{ .size = .{ .points = 12 } },
|
.{ .size = .{ .points = 12 } },
|
||||||
@ -1430,7 +1436,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
|
|||||||
|
|
||||||
if (font.options.backend != .coretext) {
|
if (font.options.backend != .coretext) {
|
||||||
// Coretext doesn't support Noto's format
|
// 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,
|
lib,
|
||||||
testEmoji,
|
testEmoji,
|
||||||
.{ .size = .{ .points = 12 } },
|
.{ .size = .{ .points = 12 } },
|
||||||
@ -1447,21 +1453,26 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
|
|||||||
defer disco_it.deinit();
|
defer disco_it.deinit();
|
||||||
var face = (try disco_it.next()).?;
|
var face = (try disco_it.next()).?;
|
||||||
errdefer face.deinit();
|
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,
|
lib,
|
||||||
testEmojiText,
|
testEmojiText,
|
||||||
.{ .size = .{ .points = 12 } },
|
.{ .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, .{});
|
var shaper = try Shaper.init(alloc, .{});
|
||||||
errdefer shaper.deinit();
|
errdefer shaper.deinit();
|
||||||
|
|
||||||
return TestShaper{
|
return TestShaper{
|
||||||
.alloc = alloc,
|
.alloc = alloc,
|
||||||
.shaper = shaper,
|
.shaper = shaper,
|
||||||
.cache = cache_ptr,
|
.grid = grid_ptr,
|
||||||
.lib = lib,
|
.lib = lib,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,10 @@ const Allocator = std.mem.Allocator;
|
|||||||
const harfbuzz = @import("harfbuzz");
|
const harfbuzz = @import("harfbuzz");
|
||||||
const font = @import("../main.zig");
|
const font = @import("../main.zig");
|
||||||
const Face = font.Face;
|
const Face = font.Face;
|
||||||
|
const Collection = font.Collection;
|
||||||
const DeferredFace = font.DeferredFace;
|
const DeferredFace = font.DeferredFace;
|
||||||
const Group = font.Group;
|
|
||||||
const GroupCache = font.GroupCache;
|
|
||||||
const Library = font.Library;
|
const Library = font.Library;
|
||||||
|
const SharedGrid = font.SharedGrid;
|
||||||
const Style = font.Style;
|
const Style = font.Style;
|
||||||
const Presentation = font.Presentation;
|
const Presentation = font.Presentation;
|
||||||
const terminal = @import("../../terminal/main.zig");
|
const terminal = @import("../../terminal/main.zig");
|
||||||
@ -83,7 +83,7 @@ pub const Shaper = struct {
|
|||||||
/// and assume the y value matches.
|
/// and assume the y value matches.
|
||||||
pub fn runIterator(
|
pub fn runIterator(
|
||||||
self: *Shaper,
|
self: *Shaper,
|
||||||
group: *GroupCache,
|
grid: *SharedGrid,
|
||||||
screen: *const terminal.Screen,
|
screen: *const terminal.Screen,
|
||||||
row: terminal.Pin,
|
row: terminal.Pin,
|
||||||
selection: ?terminal.Selection,
|
selection: ?terminal.Selection,
|
||||||
@ -91,7 +91,7 @@ pub const Shaper = struct {
|
|||||||
) font.shape.RunIterator {
|
) font.shape.RunIterator {
|
||||||
return .{
|
return .{
|
||||||
.hooks = .{ .shaper = self },
|
.hooks = .{ .shaper = self },
|
||||||
.group = group,
|
.grid = grid,
|
||||||
.screen = screen,
|
.screen = screen,
|
||||||
.row = row,
|
.row = row,
|
||||||
.selection = selection,
|
.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
|
// 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.
|
// fonts, the codepoint == glyph_index so we don't need to run any shaping.
|
||||||
if (run.font_index.special() == null) {
|
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: {
|
const i = if (!face.quirks_disable_default_font_features) 0 else i: {
|
||||||
// If we are disabling default font features we just offset
|
// If we are disabling default font features we just offset
|
||||||
// our features by the hardcoded items because always
|
// our features by the hardcoded items because always
|
||||||
@ -251,7 +257,7 @@ test "run iterator" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -270,7 +276,7 @@ test "run iterator" {
|
|||||||
|
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -290,7 +296,7 @@ test "run iterator" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -342,7 +348,7 @@ test "run iterator: empty cells with background set" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -385,7 +391,7 @@ test "shape" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -414,7 +420,7 @@ test "shape inconsolata ligs" {
|
|||||||
|
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -439,7 +445,7 @@ test "shape inconsolata ligs" {
|
|||||||
|
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -473,7 +479,7 @@ test "shape monaspace ligs" {
|
|||||||
|
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -507,7 +513,7 @@ test "shape emoji width" {
|
|||||||
|
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -547,7 +553,7 @@ test "shape emoji width long" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -586,7 +592,7 @@ test "shape variation selector VS15" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -623,7 +629,7 @@ test "shape variation selector VS16" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -657,7 +663,7 @@ test "shape with empty cells in between" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -695,7 +701,7 @@ test "shape Chinese characters" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -722,13 +728,6 @@ test "shape box glyphs" {
|
|||||||
var testdata = try testShaper(alloc);
|
var testdata = try testShaper(alloc);
|
||||||
defer testdata.deinit();
|
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: [32]u8 = undefined;
|
||||||
var buf_idx: usize = 0;
|
var buf_idx: usize = 0;
|
||||||
buf_idx += try std.unicode.utf8Encode(0x2500, buf[buf_idx..]); // horiz line
|
buf_idx += try std.unicode.utf8Encode(0x2500, buf[buf_idx..]); // horiz line
|
||||||
@ -742,7 +741,7 @@ test "shape box glyphs" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -779,7 +778,7 @@ test "shape selection boundary" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
terminal.Selection.init(
|
terminal.Selection.init(
|
||||||
@ -802,7 +801,7 @@ test "shape selection boundary" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
terminal.Selection.init(
|
terminal.Selection.init(
|
||||||
@ -825,7 +824,7 @@ test "shape selection boundary" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
terminal.Selection.init(
|
terminal.Selection.init(
|
||||||
@ -848,7 +847,7 @@ test "shape selection boundary" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
terminal.Selection.init(
|
terminal.Selection.init(
|
||||||
@ -871,7 +870,7 @@ test "shape selection boundary" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
terminal.Selection.init(
|
terminal.Selection.init(
|
||||||
@ -907,7 +906,7 @@ test "shape cursor boundary" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -926,7 +925,7 @@ test "shape cursor boundary" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -945,7 +944,7 @@ test "shape cursor boundary" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -964,7 +963,7 @@ test "shape cursor boundary" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -996,7 +995,7 @@ test "shape cursor boundary and colored emoji" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -1015,7 +1014,7 @@ test "shape cursor boundary and colored emoji" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -1032,7 +1031,7 @@ test "shape cursor boundary and colored emoji" {
|
|||||||
// Get our run iterator
|
// Get our run iterator
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -1062,7 +1061,7 @@ test "shape cell attribute change" {
|
|||||||
|
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -1086,7 +1085,7 @@ test "shape cell attribute change" {
|
|||||||
|
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -1111,7 +1110,7 @@ test "shape cell attribute change" {
|
|||||||
|
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -1136,7 +1135,7 @@ test "shape cell attribute change" {
|
|||||||
|
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -1160,7 +1159,7 @@ test "shape cell attribute change" {
|
|||||||
|
|
||||||
var shaper = &testdata.shaper;
|
var shaper = &testdata.shaper;
|
||||||
var it = shaper.runIterator(
|
var it = shaper.runIterator(
|
||||||
testdata.cache,
|
testdata.grid,
|
||||||
&screen,
|
&screen,
|
||||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||||
null,
|
null,
|
||||||
@ -1178,13 +1177,13 @@ test "shape cell attribute change" {
|
|||||||
const TestShaper = struct {
|
const TestShaper = struct {
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
shaper: Shaper,
|
shaper: Shaper,
|
||||||
cache: *GroupCache,
|
grid: *SharedGrid,
|
||||||
lib: Library,
|
lib: Library,
|
||||||
|
|
||||||
pub fn deinit(self: *TestShaper) void {
|
pub fn deinit(self: *TestShaper) void {
|
||||||
self.shaper.deinit();
|
self.shaper.deinit();
|
||||||
self.cache.deinit(self.alloc);
|
self.grid.deinit(self.alloc);
|
||||||
self.alloc.destroy(self.cache);
|
self.alloc.destroy(self.grid);
|
||||||
self.lib.deinit();
|
self.lib.deinit();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -1210,17 +1209,11 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
|
|||||||
var lib = try Library.init();
|
var lib = try Library.init();
|
||||||
errdefer lib.deinit();
|
errdefer lib.deinit();
|
||||||
|
|
||||||
var cache_ptr = try alloc.create(GroupCache);
|
var c = try Collection.init(alloc);
|
||||||
errdefer alloc.destroy(cache_ptr);
|
c.load_options = .{ .library = lib };
|
||||||
cache_ptr.* = try GroupCache.init(alloc, try Group.init(
|
|
||||||
alloc,
|
|
||||||
lib,
|
|
||||||
.{ .points = 12 },
|
|
||||||
));
|
|
||||||
errdefer cache_ptr.*.deinit(alloc);
|
|
||||||
|
|
||||||
// Setup group
|
// Setup group
|
||||||
_ = try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init(
|
_ = try c.add(alloc, .regular, .{ .loaded = try Face.init(
|
||||||
lib,
|
lib,
|
||||||
testFont,
|
testFont,
|
||||||
.{ .size = .{ .points = 12 } },
|
.{ .size = .{ .points = 12 } },
|
||||||
@ -1228,7 +1221,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
|
|||||||
|
|
||||||
if (font.options.backend != .coretext) {
|
if (font.options.backend != .coretext) {
|
||||||
// Coretext doesn't support Noto's format
|
// 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,
|
lib,
|
||||||
testEmoji,
|
testEmoji,
|
||||||
.{ .size = .{ .points = 12 } },
|
.{ .size = .{ .points = 12 } },
|
||||||
@ -1245,21 +1238,26 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
|
|||||||
defer disco_it.deinit();
|
defer disco_it.deinit();
|
||||||
var face = (try disco_it.next()).?;
|
var face = (try disco_it.next()).?;
|
||||||
errdefer face.deinit();
|
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,
|
lib,
|
||||||
testEmojiText,
|
testEmojiText,
|
||||||
.{ .size = .{ .points = 12 } },
|
.{ .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, .{});
|
var shaper = try Shaper.init(alloc, .{});
|
||||||
errdefer shaper.deinit();
|
errdefer shaper.deinit();
|
||||||
|
|
||||||
return TestShaper{
|
return TestShaper{
|
||||||
.alloc = alloc,
|
.alloc = alloc,
|
||||||
.shaper = shaper,
|
.shaper = shaper,
|
||||||
.cache = cache_ptr,
|
.grid = grid_ptr,
|
||||||
.lib = lib,
|
.lib = lib,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -15,17 +15,17 @@ pub const TextRun = struct {
|
|||||||
/// The total number of cells produced by this run.
|
/// The total number of cells produced by this run.
|
||||||
cells: u16,
|
cells: u16,
|
||||||
|
|
||||||
/// The font group that built this run.
|
/// The font grid that built this run.
|
||||||
group: *font.GroupCache,
|
grid: *font.SharedGrid,
|
||||||
|
|
||||||
/// The font index to use for the glyphs of this run.
|
/// 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.
|
/// RunIterator is an iterator that yields text runs.
|
||||||
pub const RunIterator = struct {
|
pub const RunIterator = struct {
|
||||||
hooks: font.Shaper.RunIteratorHook,
|
hooks: font.Shaper.RunIteratorHook,
|
||||||
group: *font.GroupCache,
|
grid: *font.SharedGrid,
|
||||||
screen: *const terminal.Screen,
|
screen: *const terminal.Screen,
|
||||||
row: terminal.Pin,
|
row: terminal.Pin,
|
||||||
selection: ?terminal.Selection = null,
|
selection: ?terminal.Selection = null,
|
||||||
@ -49,7 +49,7 @@ pub const RunIterator = struct {
|
|||||||
if (self.i >= max) return null;
|
if (self.i >= max) return null;
|
||||||
|
|
||||||
// Track the font for our current run
|
// Track the font for our current run
|
||||||
var current_font: font.Group.FontIndex = .{};
|
var current_font: font.Collection.Index = .{};
|
||||||
|
|
||||||
// Allow the hook to prepare
|
// Allow the hook to prepare
|
||||||
try self.hooks.prepare();
|
try self.hooks.prepare();
|
||||||
@ -117,7 +117,7 @@ pub const RunIterator = struct {
|
|||||||
} else emoji: {
|
} else emoji: {
|
||||||
// If we're not a grapheme, our individual char could be
|
// If we're not a grapheme, our individual char could be
|
||||||
// an emoji so we want to check if we expect emoji presentation.
|
// 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.
|
// automatically.
|
||||||
break :emoji null;
|
break :emoji null;
|
||||||
};
|
};
|
||||||
@ -160,7 +160,7 @@ pub const RunIterator = struct {
|
|||||||
// grapheme, i.e. combining characters), we need to find a font
|
// grapheme, i.e. combining characters), we need to find a font
|
||||||
// that supports all of them.
|
// that supports all of them.
|
||||||
const font_info: struct {
|
const font_info: struct {
|
||||||
idx: font.Group.FontIndex,
|
idx: font.Collection.Index,
|
||||||
fallback: ?u32 = null,
|
fallback: ?u32 = null,
|
||||||
} = font_info: {
|
} = font_info: {
|
||||||
// If we find a font that supports this entire grapheme
|
// 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
|
// Otherwise we need a fallback character. Prefer the
|
||||||
// official replacement character.
|
// official replacement character.
|
||||||
if (try self.group.indexForCodepoint(
|
if (try self.grid.getIndex(
|
||||||
alloc,
|
alloc,
|
||||||
0xFFFD, // replacement char
|
0xFFFD, // replacement char
|
||||||
font_style,
|
font_style,
|
||||||
@ -182,7 +182,7 @@ pub const RunIterator = struct {
|
|||||||
)) |idx| break :font_info .{ .idx = idx, .fallback = 0xFFFD };
|
)) |idx| break :font_info .{ .idx = idx, .fallback = 0xFFFD };
|
||||||
|
|
||||||
// Fallback to space
|
// Fallback to space
|
||||||
if (try self.group.indexForCodepoint(
|
if (try self.grid.getIndex(
|
||||||
alloc,
|
alloc,
|
||||||
' ',
|
' ',
|
||||||
font_style,
|
font_style,
|
||||||
@ -231,7 +231,7 @@ pub const RunIterator = struct {
|
|||||||
return TextRun{
|
return TextRun{
|
||||||
.offset = @intCast(self.i),
|
.offset = @intCast(self.i),
|
||||||
.cells = @intCast(j - self.i),
|
.cells = @intCast(j - self.i),
|
||||||
.group = self.group,
|
.grid = self.grid,
|
||||||
.font_index = current_font,
|
.font_index = current_font,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -248,10 +248,10 @@ pub const RunIterator = struct {
|
|||||||
cell: *terminal.Cell,
|
cell: *terminal.Cell,
|
||||||
style: font.Style,
|
style: font.Style,
|
||||||
presentation: ?font.Presentation,
|
presentation: ?font.Presentation,
|
||||||
) !?font.Group.FontIndex {
|
) !?font.Collection.Index {
|
||||||
// Get the font index for the primary codepoint.
|
// Get the font index for the primary codepoint.
|
||||||
const primary_cp: u32 = if (cell.isEmpty() or cell.codepoint() == 0) ' ' else cell.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,
|
alloc,
|
||||||
primary_cp,
|
primary_cp,
|
||||||
style,
|
style,
|
||||||
@ -265,7 +265,7 @@ pub const RunIterator = struct {
|
|||||||
// If this is a grapheme, we need to find a font that supports
|
// If this is a grapheme, we need to find a font that supports
|
||||||
// all of the codepoints in the grapheme.
|
// all of the codepoints in the grapheme.
|
||||||
const cps = self.row.grapheme(cell) orelse return primary;
|
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();
|
defer candidates.deinit();
|
||||||
candidates.appendAssumeCapacity(primary);
|
candidates.appendAssumeCapacity(primary);
|
||||||
|
|
||||||
@ -275,7 +275,7 @@ pub const RunIterator = struct {
|
|||||||
|
|
||||||
// Find a font that supports this codepoint. If none support this
|
// Find a font that supports this codepoint. If none support this
|
||||||
// then the whole grapheme can't be rendered so we return null.
|
// 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,
|
alloc,
|
||||||
cp,
|
cp,
|
||||||
style,
|
style,
|
||||||
@ -286,11 +286,11 @@ pub const RunIterator = struct {
|
|||||||
|
|
||||||
// We need to find a candidate that has ALL of our codepoints
|
// We need to find a candidate that has ALL of our codepoints
|
||||||
for (candidates.items) |idx| {
|
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| {
|
for (cps) |cp| {
|
||||||
// Ignore Emoji ZWJs
|
// Ignore Emoji ZWJs
|
||||||
if (cp == 0xFE0E or cp == 0xFE0F or cp == 0x200D) continue;
|
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 {
|
} else {
|
||||||
// If the while completed, then we have a candidate that
|
// If the while completed, then we have a candidate that
|
||||||
// supports all of our codepoints.
|
// supports all of our codepoints.
|
||||||
|
@ -97,7 +97,7 @@ cells: std.ArrayListUnmanaged(mtl_shaders.Cell),
|
|||||||
uniforms: mtl_shaders.Uniforms,
|
uniforms: mtl_shaders.Uniforms,
|
||||||
|
|
||||||
/// The font structures.
|
/// The font structures.
|
||||||
font_group: *font.GroupCache,
|
font_grid: *font.SharedGrid,
|
||||||
font_shaper: font.Shaper,
|
font_shaper: font.Shaper,
|
||||||
|
|
||||||
/// The images that we may render.
|
/// The images that we may render.
|
||||||
@ -118,6 +118,8 @@ queue: objc.Object, // MTLCommandQueue
|
|||||||
layer: objc.Object, // CAMetalLayer
|
layer: objc.Object, // CAMetalLayer
|
||||||
texture_greyscale: objc.Object, // MTLTexture
|
texture_greyscale: objc.Object, // MTLTexture
|
||||||
texture_color: 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. This is only set if we have custom shaders.
|
||||||
custom_shader_state: ?CustomShaderState = null,
|
custom_shader_state: ?CustomShaderState = null,
|
||||||
@ -158,7 +160,7 @@ pub const DerivedConfig = struct {
|
|||||||
|
|
||||||
font_thicken: bool,
|
font_thicken: bool,
|
||||||
font_features: std.ArrayListUnmanaged([:0]const u8),
|
font_features: std.ArrayListUnmanaged([:0]const u8),
|
||||||
font_styles: font.Group.StyleStatus,
|
font_styles: font.CodepointResolver.StyleStatus,
|
||||||
cursor_color: ?terminal.color.RGB,
|
cursor_color: ?terminal.color.RGB,
|
||||||
cursor_opacity: f64,
|
cursor_opacity: f64,
|
||||||
cursor_text: ?terminal.color.RGB,
|
cursor_text: ?terminal.color.RGB,
|
||||||
@ -187,7 +189,7 @@ pub const DerivedConfig = struct {
|
|||||||
const font_features = try config.@"font-feature".list.clone(alloc);
|
const font_features = try config.@"font-feature".list.clone(alloc);
|
||||||
|
|
||||||
// Get our font styles
|
// 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(.bold, config.@"font-style-bold" != .false);
|
||||||
font_styles.set(.italic, config.@"font-style-italic" != .false);
|
font_styles.set(.italic, config.@"font-style-italic" != .false);
|
||||||
font_styles.set(.bold_italic, config.@"font-style-bold-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.
|
// to blurry rendering.
|
||||||
layer.setProperty("contentsScale", info.scaleFactor);
|
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
|
// 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
|
// a width of 160 which is a common width for modern screens to help
|
||||||
// avoid allocations later.
|
// 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);
|
var shaders = try Shaders.init(alloc, device, custom_shaders);
|
||||||
errdefer shaders.deinit(alloc);
|
errdefer shaders.deinit(alloc);
|
||||||
|
|
||||||
|
// 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
|
// Font atlas textures
|
||||||
const texture_greyscale = try initAtlasTexture(device, &options.font_group.atlas_greyscale);
|
const greyscale = try initAtlasTexture(device, &grid.atlas_greyscale);
|
||||||
const texture_color = try initAtlasTexture(device, &options.font_group.atlas_color);
|
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{
|
return Metal{
|
||||||
.alloc = alloc,
|
.alloc = alloc,
|
||||||
.config = options.config,
|
.config = options.config,
|
||||||
.surface_mailbox = options.surface_mailbox,
|
.surface_mailbox = options.surface_mailbox,
|
||||||
.grid_metrics = metrics,
|
.grid_metrics = font_critical.metrics,
|
||||||
.screen_size = null,
|
.screen_size = null,
|
||||||
.padding = options.padding,
|
.padding = options.padding,
|
||||||
.focused = true,
|
.focused = true,
|
||||||
@ -450,13 +452,13 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
|||||||
.uniforms = .{
|
.uniforms = .{
|
||||||
.projection_matrix = undefined,
|
.projection_matrix = undefined,
|
||||||
.cell_size = undefined,
|
.cell_size = undefined,
|
||||||
.strikethrough_position = @floatFromInt(metrics.strikethrough_position),
|
.strikethrough_position = @floatFromInt(font_critical.metrics.strikethrough_position),
|
||||||
.strikethrough_thickness = @floatFromInt(metrics.strikethrough_thickness),
|
.strikethrough_thickness = @floatFromInt(font_critical.metrics.strikethrough_thickness),
|
||||||
.min_contrast = options.config.min_contrast,
|
.min_contrast = options.config.min_contrast,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Fonts
|
// Fonts
|
||||||
.font_group = options.font_group,
|
.font_grid = options.font_grid,
|
||||||
.font_shaper = font_shaper,
|
.font_shaper = font_shaper,
|
||||||
|
|
||||||
// Shaders
|
// Shaders
|
||||||
@ -469,8 +471,8 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
|||||||
.device = device,
|
.device = device,
|
||||||
.queue = queue,
|
.queue = queue,
|
||||||
.layer = layer,
|
.layer = layer,
|
||||||
.texture_greyscale = texture_greyscale,
|
.texture_greyscale = font_critical.texture_greyscale,
|
||||||
.texture_color = texture_color,
|
.texture_color = font_critical.texture_color,
|
||||||
.custom_shader_state = custom_shader_state,
|
.custom_shader_state = custom_shader_state,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -564,18 +566,16 @@ pub fn setFocus(self: *Metal, focus: bool) !void {
|
|||||||
/// Set the new font size.
|
/// Set the new font size.
|
||||||
///
|
///
|
||||||
/// Must be called on the render thread.
|
/// Must be called on the render thread.
|
||||||
pub fn setFontSize(self: *Metal, size: font.face.DesiredSize) !void {
|
pub fn setFontGrid(self: *Metal, grid: *font.SharedGrid) void {
|
||||||
log.info("set font size={}", .{size});
|
// 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.
|
// Get our metrics from the grid. This doesn't require a lock because
|
||||||
try self.font_group.setSize(size);
|
// the metrics are never recalculated.
|
||||||
|
const metrics = grid.metrics;
|
||||||
// Recalculate our metrics
|
self.grid_metrics = 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;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update our uniforms
|
// Update our uniforms
|
||||||
self.uniforms = .{
|
self.uniforms = .{
|
||||||
@ -588,27 +588,6 @@ pub fn setFontSize(self: *Metal, size: font.face.DesiredSize) !void {
|
|||||||
.strikethrough_thickness = @floatFromInt(metrics.strikethrough_thickness),
|
.strikethrough_thickness = @floatFromInt(metrics.strikethrough_thickness),
|
||||||
.min_contrast = self.uniforms.min_contrast,
|
.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.
|
/// 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 our font atlas changed, sync the texture data
|
||||||
if (self.font_group.atlas_greyscale.modified) {
|
texture: {
|
||||||
try syncAtlasTexture(self.device, &self.font_group.atlas_greyscale, &self.texture_greyscale);
|
const modified = self.font_grid.atlas_greyscale.modified.load(.monotonic);
|
||||||
self.font_group.atlas_greyscale.modified = false;
|
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) {
|
texture: {
|
||||||
try syncAtlasTexture(self.device, &self.font_group.atlas_color, &self.texture_color);
|
const modified = self.font_grid.atlas_color.modified.load(.monotonic);
|
||||||
self.font_group.atlas_color.modified = false;
|
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)
|
// Command buffer (MTLCommandBuffer)
|
||||||
@ -1376,16 +1363,6 @@ fn prepKittyGraphics(
|
|||||||
|
|
||||||
/// Update the configuration.
|
/// Update the configuration.
|
||||||
pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void {
|
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
|
// 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
|
// could check to see if there was an actual config change but this is
|
||||||
// easier and rare enough to not cause performance issues.
|
// easier and rare enough to not cause performance issues.
|
||||||
@ -1643,7 +1620,7 @@ fn rebuildCells(
|
|||||||
|
|
||||||
// Split our row into runs and shape each one.
|
// Split our row into runs and shape each one.
|
||||||
var iter = self.font_shaper.runIterator(
|
var iter = self.font_shaper.runIterator(
|
||||||
self.font_group,
|
self.font_grid,
|
||||||
screen,
|
screen,
|
||||||
row,
|
row,
|
||||||
row_selection,
|
row_selection,
|
||||||
@ -1868,7 +1845,7 @@ fn updateCell(
|
|||||||
// If the cell has a character, draw it
|
// If the cell has a character, draw it
|
||||||
if (cell.hasText()) fg: {
|
if (cell.hasText()) fg: {
|
||||||
// Render
|
// Render
|
||||||
const glyph = try self.font_group.renderGlyph(
|
const render = try self.font_grid.renderGlyph(
|
||||||
self.alloc,
|
self.alloc,
|
||||||
shaper_run.font_index,
|
shaper_run.font_index,
|
||||||
shaper_cell.glyph_index orelse break :fg,
|
shaper_cell.glyph_index orelse break :fg,
|
||||||
@ -1879,9 +1856,8 @@ fn updateCell(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const mode: mtl_shaders.Cell.Mode = switch (try fgMode(
|
const mode: mtl_shaders.Cell.Mode = switch (try fgMode(
|
||||||
&self.font_group.group,
|
render.presentation,
|
||||||
cell_pin,
|
cell_pin,
|
||||||
shaper_run,
|
|
||||||
)) {
|
)) {
|
||||||
.normal => .fg,
|
.normal => .fg,
|
||||||
.color => .fg_color,
|
.color => .fg_color,
|
||||||
@ -1894,11 +1870,11 @@ fn updateCell(
|
|||||||
.cell_width = cell.gridWidth(),
|
.cell_width = cell.gridWidth(),
|
||||||
.color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha },
|
.color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha },
|
||||||
.bg_color = bg,
|
.bg_color = bg,
|
||||||
.glyph_pos = .{ glyph.atlas_x, glyph.atlas_y },
|
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
|
||||||
.glyph_size = .{ glyph.width, glyph.height },
|
.glyph_size = .{ render.glyph.width, render.glyph.height },
|
||||||
.glyph_offset = .{
|
.glyph_offset = .{
|
||||||
glyph.offset_x + shaper_cell.x_offset,
|
render.glyph.offset_x + shaper_cell.x_offset,
|
||||||
glyph.offset_y + shaper_cell.y_offset,
|
render.glyph.offset_y + shaper_cell.y_offset,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1913,7 +1889,7 @@ fn updateCell(
|
|||||||
.curly => .underline_curly,
|
.curly => .underline_curly,
|
||||||
};
|
};
|
||||||
|
|
||||||
const glyph = try self.font_group.renderGlyph(
|
const render = try self.font_grid.renderGlyph(
|
||||||
self.alloc,
|
self.alloc,
|
||||||
font.sprite_index,
|
font.sprite_index,
|
||||||
@intFromEnum(sprite),
|
@intFromEnum(sprite),
|
||||||
@ -1931,9 +1907,9 @@ fn updateCell(
|
|||||||
.cell_width = cell.gridWidth(),
|
.cell_width = cell.gridWidth(),
|
||||||
.color = .{ color.r, color.g, color.b, alpha },
|
.color = .{ color.r, color.g, color.b, alpha },
|
||||||
.bg_color = bg,
|
.bg_color = bg,
|
||||||
.glyph_pos = .{ glyph.atlas_x, glyph.atlas_y },
|
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
|
||||||
.glyph_size = .{ glyph.width, glyph.height },
|
.glyph_size = .{ render.glyph.width, render.glyph.height },
|
||||||
.glyph_offset = .{ glyph.offset_x, glyph.offset_y },
|
.glyph_offset = .{ render.glyph.offset_x, render.glyph.offset_y },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1982,7 +1958,7 @@ fn addCursor(
|
|||||||
.underline => .underline,
|
.underline => .underline,
|
||||||
};
|
};
|
||||||
|
|
||||||
const glyph = self.font_group.renderGlyph(
|
const render = self.font_grid.renderGlyph(
|
||||||
self.alloc,
|
self.alloc,
|
||||||
font.sprite_index,
|
font.sprite_index,
|
||||||
@intFromEnum(sprite),
|
@intFromEnum(sprite),
|
||||||
@ -2004,9 +1980,9 @@ fn addCursor(
|
|||||||
.cell_width = if (wide) 2 else 1,
|
.cell_width = if (wide) 2 else 1,
|
||||||
.color = .{ color.r, color.g, color.b, alpha },
|
.color = .{ color.r, color.g, color.b, alpha },
|
||||||
.bg_color = .{ 0, 0, 0, 0 },
|
.bg_color = .{ 0, 0, 0, 0 },
|
||||||
.glyph_pos = .{ glyph.atlas_x, glyph.atlas_y },
|
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
|
||||||
.glyph_size = .{ glyph.width, glyph.height },
|
.glyph_size = .{ render.glyph.width, render.glyph.height },
|
||||||
.glyph_offset = .{ glyph.offset_x, glyph.offset_y },
|
.glyph_offset = .{ render.glyph.offset_x, render.glyph.offset_y },
|
||||||
});
|
});
|
||||||
|
|
||||||
return &self.cells.items[self.cells.items.len - 1];
|
return &self.cells.items[self.cells.items.len - 1];
|
||||||
@ -2022,33 +1998,21 @@ fn addPreeditCell(
|
|||||||
const bg = self.foreground_color;
|
const bg = self.foreground_color;
|
||||||
const fg = self.background_color;
|
const fg = self.background_color;
|
||||||
|
|
||||||
// Get the font for this codepoint.
|
// Render the glyph for our preedit text
|
||||||
const font_index = if (self.font_group.indexForCodepoint(
|
const render_ = self.font_grid.renderCodepoint(
|
||||||
self.alloc,
|
self.alloc,
|
||||||
@intCast(cp.codepoint),
|
@intCast(cp.codepoint),
|
||||||
.regular,
|
.regular,
|
||||||
.text,
|
.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 },
|
.{ .grid_metrics = self.grid_metrics },
|
||||||
) catch |err| {
|
) catch |err| {
|
||||||
log.warn("error rendering preedit glyph err={}", .{err});
|
log.warn("error rendering preedit glyph err={}", .{err});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
const render = render_ orelse {
|
||||||
|
log.warn("failed to find font for preedit codepoint={X}", .{cp.codepoint});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
// Add our opaque background cell
|
// Add our opaque background cell
|
||||||
self.cells_bg.appendAssumeCapacity(.{
|
self.cells_bg.appendAssumeCapacity(.{
|
||||||
@ -2066,9 +2030,9 @@ fn addPreeditCell(
|
|||||||
.cell_width = if (cp.wide) 2 else 1,
|
.cell_width = if (cp.wide) 2 else 1,
|
||||||
.color = .{ fg.r, fg.g, fg.b, 255 },
|
.color = .{ fg.r, fg.g, fg.b, 255 },
|
||||||
.bg_color = .{ bg.r, bg.g, bg.b, 255 },
|
.bg_color = .{ bg.r, bg.g, bg.b, 255 },
|
||||||
.glyph_pos = .{ glyph.atlas_x, glyph.atlas_y },
|
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
|
||||||
.glyph_size = .{ glyph.width, glyph.height },
|
.glyph_size = .{ render.glyph.width, render.glyph.height },
|
||||||
.glyph_offset = .{ glyph.offset_x, glyph.offset_y },
|
.glyph_offset = .{ render.glyph.offset_x, render.glyph.offset_y },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,8 +72,12 @@ gl_cells_written: usize = 0,
|
|||||||
gl_state: ?GLState = null,
|
gl_state: ?GLState = null,
|
||||||
|
|
||||||
/// The font structures.
|
/// The font structures.
|
||||||
font_group: *font.GroupCache,
|
font_grid: *font.SharedGrid,
|
||||||
font_shaper: font.Shaper,
|
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
|
/// True if the window is focused
|
||||||
focused: bool,
|
focused: bool,
|
||||||
@ -239,7 +243,7 @@ pub const DerivedConfig = struct {
|
|||||||
|
|
||||||
font_thicken: bool,
|
font_thicken: bool,
|
||||||
font_features: std.ArrayListUnmanaged([:0]const u8),
|
font_features: std.ArrayListUnmanaged([:0]const u8),
|
||||||
font_styles: font.Group.StyleStatus,
|
font_styles: font.CodepointResolver.StyleStatus,
|
||||||
cursor_color: ?terminal.color.RGB,
|
cursor_color: ?terminal.color.RGB,
|
||||||
cursor_text: ?terminal.color.RGB,
|
cursor_text: ?terminal.color.RGB,
|
||||||
cursor_opacity: f64,
|
cursor_opacity: f64,
|
||||||
@ -268,7 +272,7 @@ pub const DerivedConfig = struct {
|
|||||||
const font_features = try config.@"font-feature".list.clone(alloc);
|
const font_features = try config.@"font-feature".list.clone(alloc);
|
||||||
|
|
||||||
// Get our font styles
|
// 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(.bold, config.@"font-style-bold" != .false);
|
||||||
font_styles.set(.italic, config.@"font-style-italic" != .false);
|
font_styles.set(.italic, config.@"font-style-italic" != .false);
|
||||||
font_styles.set(.bold_italic, config.@"font-style-bold-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();
|
errdefer shaper.deinit();
|
||||||
|
|
||||||
// Setup our font metrics uniform
|
// For the remainder of the setup we lock our font grid data because
|
||||||
const metrics = try resetFontMetrics(
|
// we're reading it.
|
||||||
alloc,
|
const grid = options.font_grid;
|
||||||
options.font_group,
|
grid.lock.lockShared();
|
||||||
options.config.font_thicken,
|
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();
|
errdefer gl_state.deinit();
|
||||||
|
|
||||||
return OpenGL{
|
return OpenGL{
|
||||||
@ -348,10 +351,10 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL {
|
|||||||
.config = options.config,
|
.config = options.config,
|
||||||
.cells_bg = .{},
|
.cells_bg = .{},
|
||||||
.cells = .{},
|
.cells = .{},
|
||||||
.grid_metrics = metrics,
|
.grid_metrics = grid.metrics,
|
||||||
.screen_size = null,
|
.screen_size = null,
|
||||||
.gl_state = gl_state,
|
.gl_state = gl_state,
|
||||||
.font_group = options.font_group,
|
.font_grid = grid,
|
||||||
.font_shaper = shaper,
|
.font_shaper = shaper,
|
||||||
.draw_background = options.config.background,
|
.draw_background = options.config.background,
|
||||||
.focused = true,
|
.focused = true,
|
||||||
@ -360,7 +363,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL {
|
|||||||
.cursor_color = options.config.cursor_color,
|
.cursor_color = options.config.cursor_color,
|
||||||
.padding = options.padding,
|
.padding = options.padding,
|
||||||
.surface_mailbox = options.surface_mailbox,
|
.surface_mailbox = options.surface_mailbox,
|
||||||
.deferred_font_size = .{ .metrics = metrics },
|
.deferred_font_size = .{ .metrics = grid.metrics },
|
||||||
.deferred_config = .{},
|
.deferred_config = .{},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -470,15 +473,16 @@ pub fn displayRealize(self: *OpenGL) !void {
|
|||||||
if (single_threaded_draw) self.draw_mutex.lock();
|
if (single_threaded_draw) self.draw_mutex.lock();
|
||||||
defer if (single_threaded_draw) self.draw_mutex.unlock();
|
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
|
// 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();
|
errdefer gl_state.deinit();
|
||||||
|
|
||||||
// Unrealize if we have to
|
// Unrealize if we have to
|
||||||
@ -491,14 +495,16 @@ pub fn displayRealize(self: *OpenGL) !void {
|
|||||||
// reflush everything
|
// reflush everything
|
||||||
self.gl_cells_size = 0;
|
self.gl_cells_size = 0;
|
||||||
self.gl_cells_written = 0;
|
self.gl_cells_written = 0;
|
||||||
self.font_group.atlas_greyscale.modified = true;
|
self.texture_greyscale_modified = 0;
|
||||||
self.font_group.atlas_color.modified = true;
|
self.texture_color_modified = 0;
|
||||||
|
self.texture_greyscale_resized = 0;
|
||||||
|
self.texture_color_resized = 0;
|
||||||
|
|
||||||
// We need to reset our uniforms
|
// We need to reset our uniforms
|
||||||
if (self.screen_size) |size| {
|
if (self.screen_size) |size| {
|
||||||
self.deferred_screen_size = .{ .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 = .{};
|
self.deferred_config = .{};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -584,65 +590,20 @@ pub fn setFocus(self: *OpenGL, focus: bool) !void {
|
|||||||
/// Set the new font size.
|
/// Set the new font size.
|
||||||
///
|
///
|
||||||
/// Must be called on the render thread.
|
/// 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();
|
if (single_threaded_draw) self.draw_mutex.lock();
|
||||||
defer if (single_threaded_draw) self.draw_mutex.unlock();
|
defer if (single_threaded_draw) self.draw_mutex.unlock();
|
||||||
|
|
||||||
log.info("set font size={}", .{size});
|
// Reset our font grid
|
||||||
|
self.font_grid = grid;
|
||||||
// Set our new size, this will also reset our font atlas.
|
self.grid_metrics = grid.metrics;
|
||||||
try self.font_group.setSize(size);
|
self.texture_greyscale_modified = 0;
|
||||||
|
self.texture_greyscale_resized = 0;
|
||||||
// Reset our GPU uniforms
|
self.texture_color_modified = 0;
|
||||||
const metrics = try resetFontMetrics(
|
self.texture_color_resized = 0;
|
||||||
self.alloc,
|
|
||||||
self.font_group,
|
|
||||||
self.config.font_thicken,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Defer our GPU updates
|
// Defer our GPU updates
|
||||||
self.deferred_font_size = .{ .metrics = metrics };
|
self.deferred_font_size = .{ .metrics = grid.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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The primary render callback that is completely thread-safe.
|
/// 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.
|
// Split our row into runs and shape each one.
|
||||||
var iter = self.font_shaper.runIterator(
|
var iter = self.font_shaper.runIterator(
|
||||||
self.font_group,
|
self.font_grid,
|
||||||
screen,
|
screen,
|
||||||
row,
|
row,
|
||||||
selection,
|
selection,
|
||||||
@ -1170,33 +1131,21 @@ fn addPreeditCell(
|
|||||||
const bg = self.foreground_color;
|
const bg = self.foreground_color;
|
||||||
const fg = self.background_color;
|
const fg = self.background_color;
|
||||||
|
|
||||||
// Get the font for this codepoint.
|
// Render the glyph for our preedit text
|
||||||
const font_index = if (self.font_group.indexForCodepoint(
|
const render_ = self.font_grid.renderCodepoint(
|
||||||
self.alloc,
|
self.alloc,
|
||||||
@intCast(cp.codepoint),
|
@intCast(cp.codepoint),
|
||||||
.regular,
|
.regular,
|
||||||
.text,
|
.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 },
|
.{ .grid_metrics = self.grid_metrics },
|
||||||
) catch |err| {
|
) catch |err| {
|
||||||
log.warn("error rendering preedit glyph err={}", .{err});
|
log.warn("error rendering preedit glyph err={}", .{err});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
const render = render_ orelse {
|
||||||
|
log.warn("failed to find font for preedit codepoint={X}", .{cp.codepoint});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
// Add our opaque background cell
|
// Add our opaque background cell
|
||||||
self.cells_bg.appendAssumeCapacity(.{
|
self.cells_bg.appendAssumeCapacity(.{
|
||||||
@ -1226,12 +1175,12 @@ fn addPreeditCell(
|
|||||||
.grid_col = @intCast(x),
|
.grid_col = @intCast(x),
|
||||||
.grid_row = @intCast(y),
|
.grid_row = @intCast(y),
|
||||||
.grid_width = if (cp.wide) 2 else 1,
|
.grid_width = if (cp.wide) 2 else 1,
|
||||||
.glyph_x = glyph.atlas_x,
|
.glyph_x = render.glyph.atlas_x,
|
||||||
.glyph_y = glyph.atlas_y,
|
.glyph_y = render.glyph.atlas_y,
|
||||||
.glyph_width = glyph.width,
|
.glyph_width = render.glyph.width,
|
||||||
.glyph_height = glyph.height,
|
.glyph_height = render.glyph.height,
|
||||||
.glyph_offset_x = glyph.offset_x,
|
.glyph_offset_x = render.glyph.offset_x,
|
||||||
.glyph_offset_y = glyph.offset_y,
|
.glyph_offset_y = render.glyph.offset_y,
|
||||||
.r = fg.r,
|
.r = fg.r,
|
||||||
.g = fg.g,
|
.g = fg.g,
|
||||||
.b = fg.b,
|
.b = fg.b,
|
||||||
@ -1275,13 +1224,13 @@ fn addCursor(
|
|||||||
.underline => .underline,
|
.underline => .underline,
|
||||||
};
|
};
|
||||||
|
|
||||||
const glyph = self.font_group.renderGlyph(
|
const render = self.font_grid.renderGlyph(
|
||||||
self.alloc,
|
self.alloc,
|
||||||
font.sprite_index,
|
font.sprite_index,
|
||||||
@intFromEnum(sprite),
|
@intFromEnum(sprite),
|
||||||
.{
|
.{
|
||||||
.grid_metrics = self.grid_metrics,
|
|
||||||
.cell_width = if (wide) 2 else 1,
|
.cell_width = if (wide) 2 else 1,
|
||||||
|
.grid_metrics = self.grid_metrics,
|
||||||
},
|
},
|
||||||
) catch |err| {
|
) catch |err| {
|
||||||
log.warn("error rendering cursor glyph err={}", .{err});
|
log.warn("error rendering cursor glyph err={}", .{err});
|
||||||
@ -1301,12 +1250,12 @@ fn addCursor(
|
|||||||
.bg_g = 0,
|
.bg_g = 0,
|
||||||
.bg_b = 0,
|
.bg_b = 0,
|
||||||
.bg_a = 0,
|
.bg_a = 0,
|
||||||
.glyph_x = glyph.atlas_x,
|
.glyph_x = render.glyph.atlas_x,
|
||||||
.glyph_y = glyph.atlas_y,
|
.glyph_y = render.glyph.atlas_y,
|
||||||
.glyph_width = glyph.width,
|
.glyph_width = render.glyph.width,
|
||||||
.glyph_height = glyph.height,
|
.glyph_height = render.glyph.height,
|
||||||
.glyph_offset_x = glyph.offset_x,
|
.glyph_offset_x = render.glyph.offset_x,
|
||||||
.glyph_offset_y = glyph.offset_y,
|
.glyph_offset_y = render.glyph.offset_y,
|
||||||
});
|
});
|
||||||
|
|
||||||
return &self.cells.items[self.cells.items.len - 1];
|
return &self.cells.items[self.cells.items.len - 1];
|
||||||
@ -1455,7 +1404,7 @@ fn updateCell(
|
|||||||
// If the cell has a character, draw it
|
// If the cell has a character, draw it
|
||||||
if (cell.hasText()) fg: {
|
if (cell.hasText()) fg: {
|
||||||
// Render
|
// Render
|
||||||
const glyph = try self.font_group.renderGlyph(
|
const render = try self.font_grid.renderGlyph(
|
||||||
self.alloc,
|
self.alloc,
|
||||||
shaper_run.font_index,
|
shaper_run.font_index,
|
||||||
shaper_cell.glyph_index orelse break :fg,
|
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
|
// If we're rendering a color font, we use the color atlas
|
||||||
const mode: CellProgram.CellMode = switch (try fgMode(
|
const mode: CellProgram.CellMode = switch (try fgMode(
|
||||||
&self.font_group.group,
|
render.presentation,
|
||||||
cell_pin,
|
cell_pin,
|
||||||
shaper_run,
|
|
||||||
)) {
|
)) {
|
||||||
.normal => .fg,
|
.normal => .fg,
|
||||||
.color => .fg_color,
|
.color => .fg_color,
|
||||||
@ -1481,12 +1429,12 @@ fn updateCell(
|
|||||||
.grid_col = @intCast(x),
|
.grid_col = @intCast(x),
|
||||||
.grid_row = @intCast(y),
|
.grid_row = @intCast(y),
|
||||||
.grid_width = cell.gridWidth(),
|
.grid_width = cell.gridWidth(),
|
||||||
.glyph_x = glyph.atlas_x,
|
.glyph_x = render.glyph.atlas_x,
|
||||||
.glyph_y = glyph.atlas_y,
|
.glyph_y = render.glyph.atlas_y,
|
||||||
.glyph_width = glyph.width,
|
.glyph_width = render.glyph.width,
|
||||||
.glyph_height = glyph.height,
|
.glyph_height = render.glyph.height,
|
||||||
.glyph_offset_x = glyph.offset_x + shaper_cell.x_offset,
|
.glyph_offset_x = render.glyph.offset_x + shaper_cell.x_offset,
|
||||||
.glyph_offset_y = glyph.offset_y + shaper_cell.y_offset,
|
.glyph_offset_y = render.glyph.offset_y + shaper_cell.y_offset,
|
||||||
.r = colors.fg.r,
|
.r = colors.fg.r,
|
||||||
.g = colors.fg.g,
|
.g = colors.fg.g,
|
||||||
.b = colors.fg.b,
|
.b = colors.fg.b,
|
||||||
@ -1508,13 +1456,13 @@ fn updateCell(
|
|||||||
.curly => .underline_curly,
|
.curly => .underline_curly,
|
||||||
};
|
};
|
||||||
|
|
||||||
const underline_glyph = try self.font_group.renderGlyph(
|
const render = try self.font_grid.renderGlyph(
|
||||||
self.alloc,
|
self.alloc,
|
||||||
font.sprite_index,
|
font.sprite_index,
|
||||||
@intFromEnum(sprite),
|
@intFromEnum(sprite),
|
||||||
.{
|
.{
|
||||||
.grid_metrics = self.grid_metrics,
|
|
||||||
.cell_width = if (cell.wide == .wide) 2 else 1,
|
.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_col = @intCast(x),
|
||||||
.grid_row = @intCast(y),
|
.grid_row = @intCast(y),
|
||||||
.grid_width = cell.gridWidth(),
|
.grid_width = cell.gridWidth(),
|
||||||
.glyph_x = underline_glyph.atlas_x,
|
.glyph_x = render.glyph.atlas_x,
|
||||||
.glyph_y = underline_glyph.atlas_y,
|
.glyph_y = render.glyph.atlas_y,
|
||||||
.glyph_width = underline_glyph.width,
|
.glyph_width = render.glyph.width,
|
||||||
.glyph_height = underline_glyph.height,
|
.glyph_height = render.glyph.height,
|
||||||
.glyph_offset_x = underline_glyph.offset_x,
|
.glyph_offset_x = render.glyph.offset_x,
|
||||||
.glyph_offset_y = underline_glyph.offset_y,
|
.glyph_offset_y = render.glyph.offset_y,
|
||||||
.r = color.r,
|
.r = color.r,
|
||||||
.g = color.g,
|
.g = color.g,
|
||||||
.b = color.b,
|
.b = color.b,
|
||||||
@ -1582,16 +1530,6 @@ fn gridSize(self: *const OpenGL, screen_size: renderer.ScreenSize) renderer.Grid
|
|||||||
|
|
||||||
/// Update the configuration.
|
/// Update the configuration.
|
||||||
pub fn changeConfig(self: *OpenGL, config: *DerivedConfig) !void {
|
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
|
// 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
|
// could check to see if there was an actual config change but this is
|
||||||
// easier and rare enough to not cause performance issues.
|
// easier and rare enough to not cause performance issues.
|
||||||
@ -1656,60 +1594,63 @@ pub fn setScreenSize(
|
|||||||
/// Updates the font texture atlas if it is dirty.
|
/// Updates the font texture atlas if it is dirty.
|
||||||
fn flushAtlas(self: *OpenGL) !void {
|
fn flushAtlas(self: *OpenGL) !void {
|
||||||
const gl_state = self.gl_state orelse return;
|
const gl_state = self.gl_state orelse return;
|
||||||
|
try flushAtlasSingle(
|
||||||
{
|
&self.font_grid.lock,
|
||||||
const atlas = &self.font_group.atlas_greyscale;
|
gl_state.texture,
|
||||||
if (atlas.modified) {
|
&self.font_grid.atlas_greyscale,
|
||||||
atlas.modified = false;
|
&self.texture_greyscale_modified,
|
||||||
var texbind = try gl_state.texture.bind(.@"2D");
|
&self.texture_greyscale_resized,
|
||||||
defer texbind.unbind();
|
|
||||||
|
|
||||||
if (atlas.resized) {
|
|
||||||
atlas.resized = false;
|
|
||||||
try texbind.image2D(
|
|
||||||
0,
|
|
||||||
.red,
|
.red,
|
||||||
@intCast(atlas.size),
|
|
||||||
@intCast(atlas.size),
|
|
||||||
0,
|
|
||||||
.red,
|
.red,
|
||||||
.UnsignedByte,
|
|
||||||
atlas.data.ptr,
|
|
||||||
);
|
);
|
||||||
} else {
|
try flushAtlasSingle(
|
||||||
try texbind.subImage2D(
|
&self.font_grid.lock,
|
||||||
0,
|
gl_state.texture_color,
|
||||||
0,
|
&self.font_grid.atlas_color,
|
||||||
0,
|
&self.texture_color_modified,
|
||||||
@intCast(atlas.size),
|
&self.texture_color_resized,
|
||||||
@intCast(atlas.size),
|
|
||||||
.red,
|
|
||||||
.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,
|
.rgba,
|
||||||
|
.bgra,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 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),
|
||||||
@intCast(atlas.size),
|
@intCast(atlas.size),
|
||||||
0,
|
0,
|
||||||
.bgra,
|
format,
|
||||||
.UnsignedByte,
|
.UnsignedByte,
|
||||||
atlas.data.ptr,
|
atlas.data.ptr,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Only update the resized number after successful resize
|
||||||
|
resized.* = new_resized;
|
||||||
} else {
|
} else {
|
||||||
try texbind.subImage2D(
|
try texbind.subImage2D(
|
||||||
0,
|
0,
|
||||||
@ -1717,13 +1658,14 @@ fn flushAtlas(self: *OpenGL) !void {
|
|||||||
0,
|
0,
|
||||||
@intCast(atlas.size),
|
@intCast(atlas.size),
|
||||||
@intCast(atlas.size),
|
@intCast(atlas.size),
|
||||||
.bgra,
|
format,
|
||||||
.UnsignedByte,
|
.UnsignedByte,
|
||||||
atlas.data.ptr,
|
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
|
/// Render renders the current cell state. This will not modify any of
|
||||||
@ -1999,7 +1941,7 @@ const GLState = struct {
|
|||||||
pub fn init(
|
pub fn init(
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
config: DerivedConfig,
|
config: DerivedConfig,
|
||||||
font_group: *font.GroupCache,
|
font_grid: *font.SharedGrid,
|
||||||
) !GLState {
|
) !GLState {
|
||||||
var arena = ArenaAllocator.init(alloc);
|
var arena = ArenaAllocator.init(alloc);
|
||||||
defer arena.deinit();
|
defer arena.deinit();
|
||||||
@ -2045,12 +1987,12 @@ const GLState = struct {
|
|||||||
try texbind.image2D(
|
try texbind.image2D(
|
||||||
0,
|
0,
|
||||||
.red,
|
.red,
|
||||||
@intCast(font_group.atlas_greyscale.size),
|
@intCast(font_grid.atlas_greyscale.size),
|
||||||
@intCast(font_group.atlas_greyscale.size),
|
@intCast(font_grid.atlas_greyscale.size),
|
||||||
0,
|
0,
|
||||||
.red,
|
.red,
|
||||||
.UnsignedByte,
|
.UnsignedByte,
|
||||||
font_group.atlas_greyscale.data.ptr,
|
font_grid.atlas_greyscale.data.ptr,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2066,12 +2008,12 @@ const GLState = struct {
|
|||||||
try texbind.image2D(
|
try texbind.image2D(
|
||||||
0,
|
0,
|
||||||
.rgba,
|
.rgba,
|
||||||
@intCast(font_group.atlas_color.size),
|
@intCast(font_grid.atlas_color.size),
|
||||||
@intCast(font_group.atlas_color.size),
|
@intCast(font_grid.atlas_color.size),
|
||||||
0,
|
0,
|
||||||
.bgra,
|
.bgra,
|
||||||
.UnsignedByte,
|
.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.
|
/// The derived configuration for this renderer implementation.
|
||||||
config: renderer.Renderer.DerivedConfig,
|
config: renderer.Renderer.DerivedConfig,
|
||||||
|
|
||||||
/// The font group that should be used.
|
/// The font grid that should be used along with the key for deref-ing.
|
||||||
font_group: *font.GroupCache,
|
font_grid: *font.SharedGrid,
|
||||||
|
|
||||||
/// Padding options for the viewport.
|
/// Padding options for the viewport.
|
||||||
padding: Padding,
|
padding: Padding,
|
||||||
|
@ -321,8 +321,9 @@ fn drainMailbox(self: *Thread) !void {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
.font_size => |size| {
|
.font_grid => |grid| {
|
||||||
try self.renderer.setFontSize(size);
|
self.renderer.setFontGrid(grid.grid);
|
||||||
|
grid.set.deref(grid.old_key);
|
||||||
},
|
},
|
||||||
|
|
||||||
.foreground_color => |color| {
|
.foreground_color => |color| {
|
||||||
|
@ -20,11 +20,9 @@ pub const FgMode = enum {
|
|||||||
/// meant to be called from the typical updateCell function within a
|
/// meant to be called from the typical updateCell function within a
|
||||||
/// renderer.
|
/// renderer.
|
||||||
pub fn fgMode(
|
pub fn fgMode(
|
||||||
group: *font.Group,
|
presentation: font.Presentation,
|
||||||
cell_pin: terminal.Pin,
|
cell_pin: terminal.Pin,
|
||||||
shaper_run: font.shape.TextRun,
|
|
||||||
) !FgMode {
|
) !FgMode {
|
||||||
const presentation = try group.presentationFromIndex(shaper_run.font_index);
|
|
||||||
return switch (presentation) {
|
return switch (presentation) {
|
||||||
// Emoji is always full size and color.
|
// Emoji is always full size and color.
|
||||||
.emoji => .color,
|
.emoji => .color,
|
||||||
|
@ -22,10 +22,21 @@ pub const Message = union(enum) {
|
|||||||
/// restarting the timer.
|
/// restarting the timer.
|
||||||
reset_cursor_blink: void,
|
reset_cursor_blink: void,
|
||||||
|
|
||||||
/// Change the font size. This should recalculate the grid size and
|
/// Change the font grid. This can happen for any number of reasons
|
||||||
/// send a grid size change message back to the window thread if
|
/// including a font size change, family change, etc.
|
||||||
/// the size changes.
|
font_grid: struct {
|
||||||
font_size: font.face.DesiredSize,
|
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
|
/// Change the foreground color. This can be done separately from changing
|
||||||
/// the config file in response to an OSC 10 command.
|
/// 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 {
|
pub const CellSize = struct {
|
||||||
width: u32,
|
width: u32,
|
||||||
height: 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
|
/// The dimensions of the screen that the grid is rendered to. This is the
|
||||||
|
Reference in New Issue
Block a user