ghostty/src/font/face.zig
Qwerasd e441094af0 font: add constraint logic to rasterizers
This is in preparation to move constraint off the GPU to simplify our
shaders, instead we only need to constrain once at raster time and never
again.

This also significantly reworks the freetype renderGlyph function to be
generally much cleaner and more straightforward.

This commit doesn't actually apply the constraints to anything yet, that
will be in following commits.
2025-07-04 15:47:28 -06:00

333 lines
11 KiB
Zig

const std = @import("std");
const builtin = @import("builtin");
const options = @import("main.zig").options;
const Metrics = @import("main.zig").Metrics;
const config = @import("../config.zig");
const freetype = @import("face/freetype.zig");
const coretext = @import("face/coretext.zig");
pub const web_canvas = @import("face/web_canvas.zig");
/// Face implementation for the compile options.
pub const Face = switch (options.backend) {
.freetype,
.fontconfig_freetype,
.coretext_freetype,
=> freetype.Face,
.coretext,
.coretext_harfbuzz,
.coretext_noshape,
=> coretext.Face,
.web_canvas => web_canvas.Face,
};
/// If a DPI can't be calculated, this DPI is used. This is probably
/// wrong on modern devices so it is highly recommended you get the DPI
/// using whatever platform method you can.
pub const default_dpi = if (builtin.os.tag == .macos) 72 else 96;
/// These are the flags to customize how freetype loads fonts. This is
/// only non-void if the freetype backend is enabled.
pub const FreetypeLoadFlags = if (options.backend.hasFreetype())
config.FreetypeLoadFlags
else
void;
pub const freetype_load_flags_default: FreetypeLoadFlags = if (FreetypeLoadFlags != void) .{} else {};
/// Options for initializing a font face.
pub const Options = struct {
size: DesiredSize,
freetype_load_flags: FreetypeLoadFlags = freetype_load_flags_default,
};
/// The desired size for loading a font.
pub const DesiredSize = struct {
// Desired size in points
points: f32,
// The DPI of the screen so we can convert points to pixels.
xdpi: u16 = default_dpi,
ydpi: u16 = default_dpi,
// Converts points to pixels
pub fn pixels(self: DesiredSize) u16 {
// 1 point = 1/72 inch
return @intFromFloat(@round((self.points * @as(f32, @floatFromInt(self.ydpi))) / 72));
}
};
/// A font variation setting. The best documentation for this I know of
/// is actually the CSS font-variation-settings property on MDN:
/// https://developer.mozilla.org/en-US/docs/Web/CSS/font-variation-settings
pub const Variation = struct {
id: Id,
value: f64,
pub const Id = packed struct(u32) {
d: u8,
c: u8,
b: u8,
a: u8,
pub fn init(v: *const [4]u8) Id {
return .{ .a = v[0], .b = v[1], .c = v[2], .d = v[3] };
}
/// Converts the ID to a string. The return value is only valid
/// for the lifetime of the self pointer.
pub fn str(self: Id) [4]u8 {
return .{ self.a, self.b, self.c, self.d };
}
};
};
/// Additional options for rendering glyphs.
pub const RenderOptions = struct {
/// The metrics that are defining the grid layout. These are usually
/// the metrics of the primary font face. The grid metrics are used
/// by the font face to better layout the glyph in situations where
/// the font is not exactly the same size as the grid.
grid_metrics: Metrics,
/// The number of grid cells this glyph will take up. This can be used
/// optionally by the rasterizer to better layout the glyph.
cell_width: ?u2 = null,
/// Constraint and alignment properties for the glyph. The rasterizer
/// should call the `constrain` function on this with the original size
/// and bearings of the glyph to get remapped values that the glyph
/// should be scaled/moved to.
constraint: Constraint = .none,
/// The number of cells, horizontally that the glyph is free to take up
/// when resized and aligned by `constraint`. This is usually 1, but if
/// there's whitespace to the right of the cell then it can be 2.
constraint_width: u2 = 1,
/// Thicken the glyph. This draws the glyph with a thicker stroke width.
/// This is purely an aesthetic setting.
///
/// This only works with CoreText currently.
thicken: bool = false,
/// "Strength" of the thickening, between `0` and `255`.
/// Only has an effect when `thicken` is enabled.
///
/// `0` does not correspond to *no* thickening,
/// just the *lightest* thickening available.
///
/// CoreText only.
thicken_strength: u8 = 255,
/// See the `constraint` field.
pub const Constraint = struct {
/// Don't constrain the glyph in any way.
pub const none: Constraint = .{};
/// Vertical sizing rule.
size_vertical: Size = .none,
/// Horizontal sizing rule.
size_horizontal: Size = .none,
/// Vertical alignment rule.
align_vertical: Align = .none,
/// Horizontal alignment rule.
align_horizontal: Align = .none,
/// Top padding when resizing.
pad_top: f64 = 0.0,
/// Left padding when resizing.
pad_left: f64 = 0.0,
/// Right padding when resizing.
pad_right: f64 = 0.0,
/// Bottom padding when resizing.
pad_bottom: f64 = 0.0,
/// Maximum ratio of width to height when resizing.
max_xy_ratio: ?f64 = null,
pub const Size = enum {
/// Don't change the size of this glyph.
none,
/// Move the glyph and optionally scale it down
/// proportionally to fit within the given axis.
fit,
/// Move and resize the glyph proportionally to
/// cover the given axis.
cover,
/// Same as `cover` but not proportional.
stretch,
};
pub const Align = enum {
/// Don't move the glyph on this axis.
none,
/// Move the glyph so that its leading (bottom/left)
/// edge aligns with the leading edge of the axis.
start,
/// Move the glyph so that its trailing (top/right)
/// edge aligns with the trailing edge of the axis.
end,
/// Move the glyph so that it is centered on this axis.
center,
};
/// The size and position of a glyph.
pub const GlyphSize = struct {
width: f64,
height: f64,
x: f64,
y: f64,
};
/// Apply this constraint to the provided glyph
/// size, given the available width and height.
pub fn constrain(
self: Constraint,
glyph: GlyphSize,
/// Available width
cell_width: f64,
/// Available height
cell_height: f64,
) GlyphSize {
var g = glyph;
const w = cell_width -
self.pad_left * cell_width -
self.pad_right * cell_width;
const h = cell_height -
self.pad_top * cell_height -
self.pad_bottom * cell_height;
// Subtract padding from the bearings so that our
// alignment and sizing code works correctly. We
// re-add before returning.
g.x -= self.pad_left * cell_width;
g.y -= self.pad_bottom * cell_height;
switch (self.size_horizontal) {
.none => {},
.fit => if (g.width > w) {
const orig_height = g.height;
// Adjust our height and width to proportionally
// scale them to fit the glyph to the cell width.
g.height *= w / g.width;
g.width = w;
// Set our x to 0 since anything else would mean
// the glyph extends outside of the cell width.
g.x = 0;
// Compensate our y to keep things vertically
// centered as they're scaled down.
g.y += (orig_height - g.height) / 2;
} else if (g.width + g.x > w) {
// If the width of the glyph can fit in the cell but
// is currently outside due to the left bearing, then
// we reduce the left bearing just enough to fit it
// back in the cell.
g.x = w - g.width;
} else if (g.x < 0) {
g.x = 0;
},
.cover => {
const orig_height = g.height;
g.height *= w / g.width;
g.width = w;
g.x = 0;
g.y += (orig_height - g.height) / 2;
},
.stretch => {
g.width = w;
g.x = 0;
},
}
switch (self.size_vertical) {
.none => {},
.fit => if (g.height > h) {
const orig_width = g.width;
// Adjust our height and width to proportionally
// scale them to fit the glyph to the cell height.
g.width *= h / g.height;
g.height = h;
// Set our y to 0 since anything else would mean
// the glyph extends outside of the cell height.
g.y = 0;
// Compensate our x to keep things horizontally
// centered as they're scaled down.
g.x += (orig_width - g.width) / 2;
} else if (g.height + g.y > h) {
// If the height of the glyph can fit in the cell but
// is currently outside due to the bottom bearing, then
// we reduce the bottom bearing just enough to fit it
// back in the cell.
g.y = h - g.height;
} else if (g.y < 0) {
g.y = 0;
},
.cover => {
const orig_width = g.width;
g.width *= h / g.height;
g.height = h;
g.y = 0;
g.x += (orig_width - g.width) / 2;
},
.stretch => {
g.height = h;
g.y = 0;
},
}
if (self.max_xy_ratio) |ratio| if (g.width > g.height * ratio) {
const orig_width = g.width;
g.width = g.height * ratio;
g.x += (orig_width - g.width) / 2;
};
switch (self.align_horizontal) {
.none => {},
.start => g.x = 0,
.end => g.x = w - g.width,
.center => g.x = (w - g.width) / 2,
}
switch (self.align_vertical) {
.none => {},
.start => g.y = 0,
.end => g.y = h - g.height,
.center => g.y = (h - g.height) / 2,
}
// Re-add our padding before returning.
g.x += self.pad_left * cell_width;
g.y += self.pad_bottom * cell_height;
return g;
}
};
};
test {
@import("std").testing.refAllDecls(@This());
}
test "Variation.Id: wght should be 2003265652" {
const testing = std.testing;
const id = Variation.Id.init("wght");
try testing.expectEqual(@as(u32, 2003265652), @as(u32, @bitCast(id)));
try testing.expectEqualStrings("wght", &(id.str()));
}
test "Variation.Id: slnt should be 1936486004" {
const testing = std.testing;
const id: Variation.Id = .{ .a = 's', .b = 'l', .c = 'n', .d = 't' };
try testing.expectEqual(@as(u32, 1936486004), @as(u32, @bitCast(id)));
try testing.expectEqualStrings("slnt", &(id.str()));
}