mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-21 03:06:15 +03:00
font: start SharedGrid
This commit is contained in:
120
src/font/SharedGrid.zig
Normal file
120
src/font/SharedGrid.zig
Normal file
@ -0,0 +1,120 @@
|
||||
//! 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.
|
||||
const SharedGrid = @This();
|
||||
|
||||
// TODO(fontmem):
|
||||
// - consider config changes and how they affect the shared grid.
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const font = @import("main.zig");
|
||||
const Atlas = font.Atlas;
|
||||
const CodepointResolver = font.CodepointResolver;
|
||||
const Collection = font.Collection;
|
||||
const Glyph = font.Glyph;
|
||||
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, Glyph) = .{},
|
||||
|
||||
/// 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,
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
const CodepointKey = struct {
|
||||
style: Style,
|
||||
codepoint: u32,
|
||||
presentation: ?Presentation,
|
||||
};
|
||||
|
||||
const GlyphKey = struct {
|
||||
index: Collection.Index,
|
||||
glyph: u32,
|
||||
opts: RenderOptions,
|
||||
};
|
@ -16,6 +16,7 @@ pub const GroupCacheSet = @import("GroupCacheSet.zig");
|
||||
pub const Glyph = @import("Glyph.zig");
|
||||
pub const shape = @import("shape.zig");
|
||||
pub const Shaper = shape.Shaper;
|
||||
pub const SharedGrid = @import("SharedGrid.zig");
|
||||
pub const sprite = @import("sprite.zig");
|
||||
pub const Sprite = sprite.Sprite;
|
||||
pub const SpriteFace = sprite.Face;
|
||||
|
Reference in New Issue
Block a user