Better Glyph Constraint Logic (#7809)

This is a big'un.

- **Glyph constraint logic is now done fully on the CPU** at the
rasterization stage, so it only needs to be done once per glyph instead
of every frame. This also lets us eliminate padding between glyphs on
the atlas because we're doing nearest-neighbor sampling instead of
interpolating-- which ever so slightly increases our packing efficiency.
- **Special constraints for nerd font glyphs** are applied based roughly
on the constraints they use in their patcher. It's a simplification of
what they do, the largest difference being that they scale groups of
glyphs based on a shared bounding box so that they maintain relative
size to one another, but that would require loading all glyphs on the
group and I'd want to do that on font load TBH and at that point I'd
basically be re-implementing the nerd fonts patcher in Zig to patch
fonts at load time which is way beyond the scope I want to have. (Fixes
#7069)
- These constraints allow for **perfectly sized and centered emojis**,
this is very nice.
- **Changed the default embedded fonts** from 4 copies (regular, italic,
bold, bold italic) of a patched (and outdated) JetBrains Mono to a
single JetBrains Mono variable font and a single Nerd Fonts Symbols Only
font. This cuts the weight of those down from 9MB to 3MB!
- **FreeType's `renderGlyph` is significantly reworked**, and the new
code is, IMO, much cleaner- although there are probably some edge case
behavior differences I've introduced.

> [!NOTE]
> One breaking change I definitely introduced is changing the
`monochrome` freetype load flag config from its previous completely
backwards meaning to instead the correct one (I also changed the
default, so this won't affect any user who hasn't touched it, but users
who set the `monochrome` flag will find their fonts quite crispy after
this change because they will have no anti-aliasing anymore)

### Future work

Following this change I want to get to work on automatic font size
matching (a la CSS
[`font-size-adjust`](https://developer.mozilla.org/en-US/docs/Web/CSS/font-size-adjust)).
I set the stage for that quite some time ago so it shouldn't be too much
work and it will be a big benefit for users who regularly use multiple
writing systems and so have multiple fonts for them that aren't
necessarily size-compatible.
This commit is contained in:
Mitchell Hashimoto
2025-07-05 14:37:46 -07:00
committed by GitHub
30 changed files with 3959 additions and 685 deletions

View File

@ -99,6 +99,16 @@
.lazy = true,
},
// Fonts
.jetbrains_mono = .{
.url = "https://deps.files.ghostty.org/JetBrainsMono-2.304.tar.gz",
.hash = "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x",
},
.nerd_fonts_symbols_only = .{
.url = "https://deps.files.ghostty.org/NerdFontsSymbolsOnly-3.4.0.tar.gz",
.hash = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO26s",
},
// Other
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
.iterm2_themes = .{

10
build.zig.zon.json generated
View File

@ -54,6 +54,11 @@
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025cb960ffa3e7373a2ed8.tar.gz",
"hash": "sha256-g9o2CIc/TfWYoUS/l/HP5KZECD7qNsdQUlFruaKkVz4="
},
"N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": {
"name": "jetbrains_mono",
"url": "https://deps.files.ghostty.org/JetBrainsMono-2.304.tar.gz",
"hash": "sha256-xXppHouCrQmLWWPzlZAy5AOPORCHr3cViFulkEYQXMQ="
},
"N-V-__8AAJrvXQCqAT8Mg9o_tk6m0yf5Fz-gCNEOKLyTSerD": {
"name": "libpng",
"url": "https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz",
@ -69,6 +74,11 @@
"url": "https://deps.files.ghostty.org/libxml2-2.11.5.tar.gz",
"hash": "sha256-bCgFni4+60K1tLFkieORamNGwQladP7jvGXNxdiaYhU="
},
"N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO26s": {
"name": "nerd_fonts_symbols_only",
"url": "https://deps.files.ghostty.org/NerdFontsSymbolsOnly-3.4.0.tar.gz",
"hash": "sha256-EWTRuVbUveJI17LwmYxDzJT1ICQxoVZKeTiVsec7DQQ="
},
"N-V-__8AAHjwMQDBXnLq3Q2QhaivE0kE2aD138vtX2Bq1g7c": {
"name": "oniguruma",
"url": "https://deps.files.ghostty.org/oniguruma-1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb.tar.gz",

16
build.zig.zon.nix generated
View File

@ -169,6 +169,14 @@ in
hash = "sha256-g9o2CIc/TfWYoUS/l/HP5KZECD7qNsdQUlFruaKkVz4=";
};
}
{
name = "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x";
path = fetchZigArtifact {
name = "jetbrains_mono";
url = "https://deps.files.ghostty.org/JetBrainsMono-2.304.tar.gz";
hash = "sha256-xXppHouCrQmLWWPzlZAy5AOPORCHr3cViFulkEYQXMQ=";
};
}
{
name = "N-V-__8AAJrvXQCqAT8Mg9o_tk6m0yf5Fz-gCNEOKLyTSerD";
path = fetchZigArtifact {
@ -193,6 +201,14 @@ in
hash = "sha256-bCgFni4+60K1tLFkieORamNGwQladP7jvGXNxdiaYhU=";
};
}
{
name = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO26s";
path = fetchZigArtifact {
name = "nerd_fonts_symbols_only";
url = "https://deps.files.ghostty.org/NerdFontsSymbolsOnly-3.4.0.tar.gz";
hash = "sha256-EWTRuVbUveJI17LwmYxDzJT1ICQxoVZKeTiVsec7DQQ=";
};
}
{
name = "N-V-__8AAHjwMQDBXnLq3Q2QhaivE0kE2aD138vtX2Bq1g7c";
path = fetchZigArtifact {

2
build.zig.zon.txt generated
View File

@ -2,6 +2,8 @@ git+https://codeberg.org/atman/zg#4a002763419a34d61dcbb1f415821b83b9bf8ddc
git+https://github.com/TUSF/zigimg#31268548fe3276c0e95f318a6c0d2ab10565b58d
git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23
https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz
https://deps.files.ghostty.org/JetBrainsMono-2.304.tar.gz
https://deps.files.ghostty.org/NerdFontsSymbolsOnly-3.4.0.tar.gz
https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz
https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz
https://deps.files.ghostty.org/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d.tar.gz

View File

@ -65,6 +65,12 @@
"dest": "vendor/p/N-V-__8AAGHcWgTaKLjwmFkxToNT4jgz5VXUHR7hz8TQ2_AS",
"sha256": "83da3608873f4df598a144bf97f1cfe4a644083eea36c75052516bb9a2a4573e"
},
{
"type": "archive",
"url": "https://deps.files.ghostty.org/JetBrainsMono-2.304.tar.gz",
"dest": "vendor/p/N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x",
"sha256": "c57a691e8b82ad098b5963f3959032e4038f391087af7715885ba59046105cc4"
},
{
"type": "archive",
"url": "https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz",
@ -83,6 +89,12 @@
"dest": "vendor/p/N-V-__8AAG3RoQEyRC2Vw7Qoro5SYBf62IHn3HjqtNVY6aWK",
"sha256": "6c28059e2e3eeb42b5b4b16489e3916a6346c1095a74fee3bc65cdc5d89a6215"
},
{
"type": "archive",
"url": "https://deps.files.ghostty.org/NerdFontsSymbolsOnly-3.4.0.tar.gz",
"dest": "vendor/p/N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO26s",
"sha256": "1164d1b956d4bde248d7b2f0998c43cc94f5202431a1564a793895b1e73b0d04"
},
{
"type": "archive",
"url": "https://deps.files.ghostty.org/oniguruma-1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb.tar.gz",

View File

@ -5,3 +5,5 @@
#include <freetype/ftoutln.h>
#include <freetype/ftsnames.h>
#include <freetype/ttnameid.h>
#include <freetype/ftbitmap.h>
#include <freetype/ftbbox.h>

View File

@ -141,6 +141,22 @@ pub fn Context(comptime T: type) type {
@bitCast(rect),
);
}
pub fn scaleCTM(self: *T, sx: c.CGFloat, sy: c.CGFloat) void {
c.CGContextScaleCTM(
@ptrCast(self),
sx,
sy,
);
}
pub fn translateCTM(self: *T, tx: c.CGFloat, ty: c.CGFloat) void {
c.CGContextTranslateCTM(
@ptrCast(self),
tx,
ty,
);
}
};
}

View File

@ -500,6 +500,43 @@ pub fn add(
try static_libs.append(utfcpp_dep.artifact("utfcpp").getEmittedBin());
}
// Fonts
{
// JetBrains Mono
const jb_mono = b.dependency("jetbrains_mono", .{});
step.root_module.addAnonymousImport(
"jetbrains_mono_regular",
.{ .root_source_file = jb_mono.path("fonts/ttf/JetBrainsMono-Regular.ttf") },
);
step.root_module.addAnonymousImport(
"jetbrains_mono_bold",
.{ .root_source_file = jb_mono.path("fonts/ttf/JetBrainsMono-Bold.ttf") },
);
step.root_module.addAnonymousImport(
"jetbrains_mono_italic",
.{ .root_source_file = jb_mono.path("fonts/ttf/JetBrainsMono-Italic.ttf") },
);
step.root_module.addAnonymousImport(
"jetbrains_mono_bold_italic",
.{ .root_source_file = jb_mono.path("fonts/ttf/JetBrainsMono-BoldItalic.ttf") },
);
step.root_module.addAnonymousImport(
"jetbrains_mono_variable",
.{ .root_source_file = jb_mono.path("fonts/variable/JetBrainsMono[wght].ttf") },
);
step.root_module.addAnonymousImport(
"jetbrains_mono_variable_italic",
.{ .root_source_file = jb_mono.path("fonts/variable/JetBrainsMono-Italic[wght].ttf") },
);
// Symbols-only nerd font
const nf_symbols = b.dependency("nerd_fonts_symbols_only", .{});
step.root_module.addAnonymousImport(
"nerd_fonts_symbols_only",
.{ .root_source_file = nf_symbols.path("SymbolsNerdFontMono-Regular.ttf") },
);
}
// If we're building an exe then we have additional dependencies.
if (step.kind != .lib) {
// We always statically compile glad

View File

@ -432,13 +432,16 @@ pub const compatibility = std.StaticStringMap(
///
/// Available flags:
///
/// * `hinting` - Enable or disable hinting, enabled by default.
/// * `force-autohint` - Use the freetype auto-hinter rather than the
/// font's native hinter. Enabled by default.
/// * `monochrome` - Instructs renderer to use 1-bit monochrome
/// rendering. This option doesn't impact the hinter.
/// Enabled by default.
/// * `autohint` - Use the freetype auto-hinter. Enabled by default.
/// * `hinting` - Enable or disable hinting. Enabled by default.
///
/// * `force-autohint` - Always use the freetype auto-hinter instead of
/// the font's native hinter. Enabled by default.
///
/// * `monochrome` - Instructs renderer to use 1-bit monochrome rendering.
/// This will disable anti-aliasing, and probably not look very good unless
/// you're using a pixel font. Disabled by default.
///
/// * `autohint` - Enable the freetype auto-hinter. Enabled by default.
///
/// Example: `hinting`, `no-hinting`, `force-autohint`, `no-force-autohint`
@"freetype-load-flags": FreetypeLoadFlags = .{},
@ -7082,7 +7085,7 @@ pub const FreetypeLoadFlags = packed struct {
// to these defaults.
hinting: bool = true,
@"force-autohint": bool = true,
monochrome: bool = true,
monochrome: bool = false,
autohint: bool = true,
};

View File

@ -265,13 +265,35 @@ pub fn renderGlyph(
.emoji => &self.atlas_color,
};
var render_opts = opts;
// Always use these constraints for emoji.
if (p == .emoji) {
render_opts.constraint = .{
// Make the emoji as wide as possible, scaling proportionally,
// but then scale it down as necessary if its new size exceeds
// the cell height.
.size_horizontal = .cover,
.size_vertical = .fit,
// Center the emoji in its cells.
.align_horizontal = .center,
.align_vertical = .center,
// Add a small bit of padding so the emoji
// doesn't quite touch the edges of the cells.
.pad_left = 0.025,
.pad_right = 0.025,
};
}
// Render into the atlas
const glyph = self.resolver.renderGlyph(
alloc,
atlas,
index,
glyph_index,
opts,
render_opts,
) catch |err| switch (err) {
// If the atlas is full, we resize it
error.AtlasFull => blk: {
@ -281,7 +303,7 @@ pub fn renderGlyph(
atlas,
index,
glyph_index,
opts,
render_opts,
);
},
@ -325,7 +347,8 @@ const GlyphKey = struct {
cell_width: u2,
thicken: bool,
thicken_strength: u8,
_padding: u5 = 0,
constraint_width: u2,
_padding: u3 = 0,
},
inline fn from(key: GlyphKey) Packed {
@ -336,6 +359,7 @@ const GlyphKey = struct {
.cell_width = key.opts.cell_width orelse 0,
.thicken = key.opts.thicken,
.thicken_strength = key.opts.thicken_strength,
.constraint_width = key.opts.constraint_width,
},
};
}

View File

@ -260,34 +260,51 @@ fn collection(
.regular,
.{ .fallback_loaded = try .init(
self.font_lib,
font.embedded.regular,
font.embedded.variable,
load_options.faceOptions(),
) },
);
_ = try c.add(
try (try c.getFace(try c.add(
self.alloc,
.bold,
.{ .fallback_loaded = try .init(
self.font_lib,
font.embedded.bold,
font.embedded.variable,
load_options.faceOptions(),
) },
))).setVariations(
&.{.{ .id = .init("wght"), .value = 700 }},
load_options.faceOptions(),
);
_ = try c.add(
self.alloc,
.italic,
.{ .fallback_loaded = try .init(
self.font_lib,
font.embedded.italic,
font.embedded.variable_italic,
load_options.faceOptions(),
) },
);
_ = try c.add(
try (try c.getFace(try c.add(
self.alloc,
.bold_italic,
.{ .fallback_loaded = try .init(
self.font_lib,
font.embedded.bold_italic,
font.embedded.variable_italic,
load_options.faceOptions(),
) },
))).setVariations(
&.{.{ .id = .init("wght"), .value = 700 }},
load_options.faceOptions(),
);
// Nerd-font symbols fallback.
_ = try c.add(
self.alloc,
.regular,
.{ .fallback_loaded = try Face.init(
self.font_lib,
font.embedded.symbols_nerd_font,
load_options.faceOptions(),
) },
);

View File

@ -6,19 +6,29 @@
//! redistribution and include their license as necessary.
/// Default fonts that we prefer for Ghostty.
pub const regular = @embedFile("res/JetBrainsMonoNerdFont-Regular.ttf");
pub const bold = @embedFile("res/JetBrainsMonoNerdFont-Bold.ttf");
pub const italic = @embedFile("res/JetBrainsMonoNerdFont-Italic.ttf");
pub const bold_italic = @embedFile("res/JetBrainsMonoNerdFont-BoldItalic.ttf");
pub const variable = @embedFile("jetbrains_mono_variable");
pub const variable_italic = @embedFile("jetbrains_mono_variable_italic");
/// Symbols-only nerd font.
pub const symbols_nerd_font = @embedFile("nerd_fonts_symbols_only");
/// Static jetbrains mono faces, currently unused.
pub const regular = @embedFile("jetbrains_mono_regular");
pub const bold = @embedFile("jetbrains_mono_bold");
pub const italic = @embedFile("jetbrains_mono_italic");
pub const bold_italic = @embedFile("jetbrains_mono_bold_italic");
/// Emoji fonts
pub const emoji = @embedFile("res/NotoColorEmoji.ttf");
pub const emoji_text = @embedFile("res/NotoEmoji-Regular.ttf");
// Fonts below are ONLY used for testing.
/// Fonts with general properties
pub const arabic = @embedFile("res/KawkabMono-Regular.ttf");
pub const variable = @embedFile("res/Lilex-VF.ttf");
/// Font with nerd fonts embedded.
pub const nerd_font = @embedFile("res/JetBrainsMonoNerdFont-Regular.ttf");
/// A font for testing which is patched with nerd font symbols.
pub const test_nerd_font = @embedFile("res/JetBrainsMonoNerdFont-Regular.ttf");
/// Specific font families below:
pub const code_new_roman = @embedFile("res/CodeNewRoman-Regular.otf");

View File

@ -94,6 +94,17 @@ pub const RenderOptions = struct {
/// 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.
///
@ -108,6 +119,198 @@ pub const RenderOptions = struct {
///
/// 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 {

View File

@ -291,22 +291,29 @@ pub const Face = struct {
// in the bottom left and +Y pointing up.
var rect = self.font.getBoundingRectsForGlyphs(.horizontal, &glyphs, null);
// Determine whether this is a color glyph.
const is_color = self.isColorGlyph(glyph_index);
// And whether it's (probably) a bitmap (sbix).
const sbix = is_color and self.color != null and self.color.?.sbix;
// If we're rendering a synthetic bold then we will gain 50% of
// the line width on every edge, which means we should increase
// our width and height by the line width and subtract half from
// our origin points.
if (self.synthetic_bold) |line_width| {
//
// We don't add extra size if it's a sbix color font though,
// since bitmaps aren't affected by synthetic bold.
if (!sbix) if (self.synthetic_bold) |line_width| {
rect.size.width += line_width;
rect.size.height += line_width;
rect.origin.x -= line_width / 2;
rect.origin.y -= line_width / 2;
}
};
// We make an assumption that font smoothing ("thicken")
// adds no more than 1 extra pixel to any edge. We don't
// add extra size if it's a sbix color font though, since
// bitmaps aren't affected by smoothing.
const sbix = self.color != null and self.color.?.sbix;
if (opts.thicken and !sbix) {
rect.size.width += 2.0;
rect.size.height += 2.0;
@ -314,18 +321,12 @@ pub const Face = struct {
rect.origin.y -= 1.0;
}
// We compute the minimum and maximum x and y values.
// We round our min points down and max points up.
const x0: i32, const x1: i32, const y0: i32, const y1: i32 = .{
@intFromFloat(@floor(rect.origin.x)),
@intFromFloat(@ceil(rect.origin.x) + @ceil(rect.size.width)),
@intFromFloat(@floor(rect.origin.y)),
@intFromFloat(@ceil(rect.origin.y) + @ceil(rect.size.height)),
};
// This bitmap is blank. I've seen it happen in a font, I don't know why.
// If it is empty, we just return a valid glyph struct that does nothing.
if (x1 <= x0 or y1 <= y0) return font.Glyph{
// If our rect is smaller than a quarter pixel in either axis
// then it has no outlines or they're too small to render.
//
// In this case we just return 0-sized glyph struct.
if (rect.size.width < 0.25 or rect.size.height < 0.25)
return font.Glyph{
.width = 0,
.height = 0,
.offset_x = 0,
@ -335,8 +336,28 @@ pub const Face = struct {
.advance_x = 0,
};
const width: u32 = @intCast(x1 - x0);
const height: u32 = @intCast(y1 - y0);
const metrics = opts.grid_metrics;
const cell_width: f64 = @floatFromInt(metrics.cell_width * opts.constraint_width);
const cell_height: f64 = @floatFromInt(metrics.cell_height);
const glyph_size = opts.constraint.constrain(
.{
.width = rect.size.width,
.height = rect.size.height,
.x = rect.origin.x,
.y = rect.origin.y + @as(f64, @floatFromInt(metrics.cell_baseline)),
},
cell_width,
cell_height,
);
const width = glyph_size.width;
const height = glyph_size.height;
const x = glyph_size.x;
const y = glyph_size.y;
const px_width: u32 = @intFromFloat(@ceil(width));
const px_height: u32 = @intFromFloat(@ceil(height));
// Settings that are specific to if we are rendering text or emoji.
const color: struct {
@ -344,7 +365,7 @@ pub const Face = struct {
depth: u32,
space: *macos.graphics.ColorSpace,
context_opts: c_uint,
} = if (!self.isColorGlyph(glyph_index)) .{
} = if (!is_color) .{
.color = false,
.depth = 1,
.space = try macos.graphics.ColorSpace.createNamed(.linearGray),
@ -371,17 +392,17 @@ pub const Face = struct {
// usually stabilizes pretty quickly and is very infrequent so I think
// the allocation overhead is acceptable compared to the cost of
// caching it forever or having to deal with a cache lifetime.
const buf = try alloc.alloc(u8, width * height * color.depth);
const buf = try alloc.alloc(u8, px_width * px_height * color.depth);
defer alloc.free(buf);
@memset(buf, 0);
const context = macos.graphics.BitmapContext.context;
const ctx = try macos.graphics.BitmapContext.create(
buf,
width,
height,
px_width,
px_height,
8,
width * color.depth,
px_width * color.depth,
color.space,
color.context_opts,
);
@ -390,14 +411,14 @@ pub const Face = struct {
// Perform an initial fill. This ensures that we don't have any
// uninitialized pixels in the bitmap.
if (color.color)
context.setRGBFillColor(ctx, 1, 1, 1, 0)
context.setRGBFillColor(ctx, 0, 0, 0, 0)
else
context.setGrayFillColor(ctx, 1, 0);
context.setGrayFillColor(ctx, 0, 0);
context.fillRect(ctx, .{
.origin = .{ .x = 0, .y = 0 },
.size = .{
.width = @floatFromInt(width),
.height = @floatFromInt(height),
.width = @floatFromInt(px_width),
.height = @floatFromInt(px_height),
},
});
@ -427,49 +448,34 @@ pub const Face = struct {
context.setLineWidth(ctx, line_width);
}
context.scaleCTM(
ctx,
width / rect.size.width,
height / rect.size.height,
);
// We want to render the glyphs at (0,0), but the glyphs themselves
// are offset by bearings, so we have to undo those bearings in order
// to get them to 0,0.
self.font.drawGlyphs(&glyphs, &.{
.{
.x = @floatFromInt(-x0),
.y = @floatFromInt(-y0),
},
}, ctx);
self.font.drawGlyphs(&glyphs, &.{.{
.x = -@floor(rect.origin.x),
.y = -@floor(rect.origin.y),
}}, ctx);
const region = region: {
// We reserve a region that's 1px wider and taller than we need
// in order to create a 1px separation between adjacent glyphs
// to prevent interpolation with adjacent glyphs while sampling
// from the atlas.
var region = try atlas.reserve(
alloc,
width + 1,
height + 1,
);
// We adjust the region width and height back down since we
// don't need the extra pixel, we just needed to reserve it
// so that it isn't used for other glyphs in the future.
region.width -= 1;
region.height -= 1;
break :region region;
};
// Write our rasterized glyph to the atlas.
const region = try atlas.reserve(alloc, px_width, px_height);
atlas.set(region, buf);
const metrics = opts.grid_metrics;
// This should be the distance from the bottom of
// the cell to the top of the glyph's bounding box.
//
// The calculation is distance from bottom of cell to
// baseline plus distance from baseline to top of glyph.
const offset_y: i32 = @as(i32, @intCast(metrics.cell_baseline)) + y1;
const offset_y: i32 =
@as(i32, @intFromFloat(@floor(y))) +
@as(i32, @intCast(px_height));
// This should be the distance from the left of
// the cell to the left of the glyph's bounding box.
const offset_x: i32 = offset_x: {
var result: i32 = x0;
var result: i32 = @intFromFloat(@round(x));
// If our cell was resized then we adjust our glyph's
// position relative to the new center. This keeps glyphs
@ -490,8 +496,8 @@ pub const Face = struct {
_ = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, &advances);
return .{
.width = width,
.height = height,
.width = px_width,
.height = px_height,
.offset_x = offset_x,
.offset_y = offset_y,
.atlas_x = region.x,

View File

@ -15,12 +15,13 @@ const Allocator = std.mem.Allocator;
const font = @import("../main.zig");
const Glyph = font.Glyph;
const Library = font.Library;
const convert = @import("freetype_convert.zig");
const opentype = @import("../opentype.zig");
const fastmem = @import("../../fastmem.zig");
const quirks = @import("../../quirks.zig");
const config = @import("../../config.zig");
const F26Dot6 = opentype.sfnt.F26Dot6;
const log = std.log.scoped(.font_face);
pub const Face = struct {
@ -58,14 +59,6 @@ pub const Face = struct {
bold: bool = false,
} = .{},
/// The matrix applied to a regular font to create a synthetic italic.
const italic_matrix: freetype.c.FT_Matrix = .{
.xx = 0x10000,
.xy = 0x044ED, // approx. tan(15)
.yx = 0,
.yy = 0x10000,
};
/// Initialize a new font face with the given source in-memory.
pub fn initFile(
lib: Library,
@ -330,26 +323,32 @@ pub const Face = struct {
self.ft_mutex.lock();
defer self.ft_mutex.unlock();
const metrics = opts.grid_metrics;
// We enable hinting by default, and disable it if either of the
// constraint alignments are not center or none, since this means
// that the glyph needs to be aligned flush to the cell edge, and
// hinting can mess that up.
const do_hinting = self.load_flags.hinting and
switch (opts.constraint.align_horizontal) {
.start, .end => false,
.center, .none => true,
} and
switch (opts.constraint.align_vertical) {
.start, .end => false,
.center, .none => true,
};
// If we have synthetic italic, then we apply a transformation matrix.
// We have to undo this because synthetic italic works by increasing
// the ref count of the base face.
if (self.synthetic.italic) self.face.setTransform(&italic_matrix, null);
defer if (self.synthetic.italic) self.face.setTransform(null, null);
// If our glyph has color, we want to render the color
// Load the glyph.
try self.face.loadGlyph(glyph_index, .{
// If our glyph has color, we want to render the color
.color = self.face.hasColor(),
// If we have synthetic bold, we have to set some additional
// glyph properties before render so we don't render here.
.render = !self.synthetic.bold,
// We don't render, because we'll invoke the render
// manually after applying constraints further down.
.render = false,
// use options from config
.no_hinting = !self.load_flags.hinting,
.no_hinting = !do_hinting,
.force_autohint = !self.load_flags.@"force-autohint",
.monochrome = !self.load_flags.monochrome,
.no_autohint = !self.load_flags.autohint,
// NO_SVG set to true because we don't currently support rendering
@ -359,22 +358,15 @@ pub const Face = struct {
});
const glyph = self.face.handle.*.glyph;
// For synthetic bold, we embolden the glyph and render it.
if (self.synthetic.bold) {
// We need to scale the embolden amount based on the font size.
// This is a heuristic I found worked well across a variety of
// founts: 1 pixel per 64 units of height.
const height: f64 = @floatFromInt(self.face.handle.*.size.*.metrics.height);
const ratio: f64 = 64.0 / 2048.0;
const amount = @ceil(height * ratio);
_ = freetype.c.FT_Outline_Embolden(&glyph.*.outline, @intFromFloat(amount));
try self.face.renderGlyph(.normal);
}
const glyph_width: f64 = f26dot6ToF64(glyph.*.metrics.width);
const glyph_height: f64 = f26dot6ToF64(glyph.*.metrics.height);
// This bitmap is blank. I've seen it happen in a font, I don't know why.
// If it is empty, we just return a valid glyph struct that does nothing.
const bitmap_ft = glyph.*.bitmap;
if (bitmap_ft.rows == 0) return .{
// If our glyph is smaller than a quarter pixel in either axis
// then it has no outlines or they're too small to render.
//
// In this case we just return 0-sized glyph struct.
if (glyph_width < 0.25 or glyph_height < 0.25)
return font.Glyph{
.width = 0,
.height = 0,
.offset_x = 0,
@ -384,235 +376,292 @@ pub const Face = struct {
.advance_x = 0,
};
// Ensure we know how to work with the font format. And assure that
// or color depth is as expected on the texture atlas. If format is null
// it means there is no native color format for our Atlas and we must try
// conversion.
const format: ?font.Atlas.Format = switch (bitmap_ft.pixel_mode) {
freetype.c.FT_PIXEL_MODE_MONO => null,
freetype.c.FT_PIXEL_MODE_GRAY => .grayscale,
freetype.c.FT_PIXEL_MODE_BGRA => .bgra,
else => {
log.warn("glyph={} pixel mode={}", .{ glyph_index, bitmap_ft.pixel_mode });
@panic("unsupported pixel mode");
// For synthetic bold, we embolden the glyph.
if (self.synthetic.bold) {
// We need to scale the embolden amount based on the font size.
// This is a heuristic I found worked well across a variety of
// founts: 1 pixel per 64 units of height.
const font_height: f64 = @floatFromInt(self.face.handle.*.size.*.metrics.height);
const ratio: f64 = 64.0 / 2048.0;
const amount = @ceil(font_height * ratio);
_ = freetype.c.FT_Outline_Embolden(&glyph.*.outline, @intFromFloat(amount));
}
// Next we need to apply any constraints.
const metrics = opts.grid_metrics;
const cell_width: f64 = @floatFromInt(metrics.cell_width * opts.constraint_width);
const cell_height: f64 = @floatFromInt(metrics.cell_height);
const glyph_x: f64 = f26dot6ToF64(glyph.*.metrics.horiBearingX);
const glyph_y: f64 = f26dot6ToF64(glyph.*.metrics.horiBearingY) - glyph_height;
const glyph_size = opts.constraint.constrain(
.{
.width = glyph_width,
.height = glyph_height,
.x = glyph_x,
.y = glyph_y + @as(f64, @floatFromInt(metrics.cell_baseline)),
},
};
// If our atlas format doesn't match, look for conversions if possible.
const bitmap_converted = if (format == null or atlas.format != format.?) blk: {
const func = convert.map[bitmap_ft.pixel_mode].get(atlas.format) orelse {
log.warn("glyph={} pixel mode={}", .{ glyph_index, bitmap_ft.pixel_mode });
return error.UnsupportedPixelMode;
};
log.debug("converting from pixel_mode={} to atlas_format={}", .{
bitmap_ft.pixel_mode,
atlas.format,
});
break :blk try func(alloc, bitmap_ft);
} else null;
defer if (bitmap_converted) |bm| {
const len = @as(usize, @intCast(bm.pitch)) * @as(usize, @intCast(bm.rows));
alloc.free(bm.buffer[0..len]);
};
// Now we need to see if we need to resize this bitmap. This can happen
// in scenarios where we have fixed size glyphs. For example, emoji
// can be quite large (i.e. 128x128) when we have a cell width of 24!
// The issue with large bitmaps is they take a huge amount of space in
// the atlas and force resizes quite frequently. We pay some CPU cost
// up front to resize the glyph to avoid significant CPU cost to resize
// and copy the atlas.
const bitmap_original = bitmap_converted orelse bitmap_ft;
const bitmap_resized: ?freetype.c.struct_FT_Bitmap_ = resized: {
const original_width = bitmap_original.width;
const original_height = bitmap_original.rows;
var result = bitmap_original;
// TODO: We are limiting this to only color glyphs, so mainly emoji.
// We can rework this after a future improvement (promised by Qwerasd)
// which implements more flexible resizing rules.
if (atlas.format != .grayscale and opts.cell_width != null) {
const cell_width = opts.cell_width orelse unreachable;
// If we have a cell_width, we constrain
// the glyph to fit within the cell(s).
result.width = metrics.cell_width * @as(u32, cell_width);
result.rows = (result.width * original_height) / original_width;
} else {
// If we don't have a cell_width, we scale to fill vertically
result.rows = metrics.cell_height;
result.width = (metrics.cell_height * original_width) / original_height;
}
// If we already fit, we don't need to resize
if (original_height <= result.rows and original_width <= result.width) {
break :resized null;
}
result.pitch = @as(c_int, @intCast(result.width)) * atlas.format.depth();
const buf = try alloc.alloc(
u8,
@as(usize, @intCast(result.pitch)) * @as(usize, @intCast(result.rows)),
cell_width,
cell_height,
);
result.buffer = buf.ptr;
errdefer alloc.free(buf);
const width = glyph_size.width;
const height = glyph_size.height;
// This may need to be adjusted later on.
var x = glyph_size.x;
const y = glyph_size.y;
// Now we can render the glyph.
var bitmap: freetype.c.FT_Bitmap = undefined;
_ = freetype.c.FT_Bitmap_Init(&bitmap);
defer _ = freetype.c.FT_Bitmap_Done(self.lib.lib.handle, &bitmap);
switch (glyph.*.format) {
freetype.c.FT_GLYPH_FORMAT_OUTLINE => {
// Manually adjust the glyph outline with this transform.
//
// This offers better precision than using the freetype transform
// matrix, since that has 16.16 coefficients, and also I was having
// weird issues that I can only assume where due to freetype doing
// some bad caching or something when I did this using the matrix.
const scale_x = width / glyph_width;
const scale_y = height / glyph_height;
const skew: f64 =
if (self.synthetic.italic)
// We skew by 12 degrees to synthesize italics.
@tan(std.math.degreesToRadians(12))
else
0.0;
var bbox_before: freetype.c.FT_BBox = undefined;
_ = freetype.c.FT_Outline_Get_BBox(&glyph.*.outline, &bbox_before);
const outline = &glyph.*.outline;
for (outline.points[0..@intCast(outline.n_points)]) |*p| {
// Convert to f64 for processing
var px = f26dot6ToF64(p.x);
var py = f26dot6ToF64(p.y);
// Scale
px *= scale_x;
py *= scale_y;
// Skew
px += py * skew;
// Convert back and store
p.x = @as(i32, @bitCast(F26Dot6.from(px)));
p.y = @as(i32, @bitCast(F26Dot6.from(py)));
}
var bbox_after: freetype.c.FT_BBox = undefined;
_ = freetype.c.FT_Outline_Get_BBox(&glyph.*.outline, &bbox_after);
// If our bounding box changed, account for the lsb difference.
//
// This can happen when we skew glyphs that have a bit sticking
// out to the left higher up, like the top of the T or the serif
// on the lower case l in many monospace fonts.
x += f26dot6ToF64(bbox_after.xMin) - f26dot6ToF64(bbox_before.xMin);
try self.face.renderGlyph(
if (self.load_flags.monochrome)
.mono
else
.normal,
);
// Copy the glyph's bitmap, making sure
// that it's 8bpp and densely packed.
if (freetype.c.FT_Bitmap_Convert(
self.lib.lib.handle,
&glyph.*.bitmap,
&bitmap,
1,
) != 0) {
return error.BitmapHandlingError;
}
},
freetype.c.FT_GLYPH_FORMAT_BITMAP => {
// If our glyph has a non-color bitmap, we need
// to convert it to dense 8bpp so that the scale
// operation works correctly.
switch (glyph.*.bitmap.pixel_mode) {
freetype.c.FT_PIXEL_MODE_BGRA,
freetype.c.FT_PIXEL_MODE_GRAY,
=> {},
else => {
var converted: freetype.c.FT_Bitmap = undefined;
freetype.c.FT_Bitmap_Init(&converted);
if (freetype.c.FT_Bitmap_Convert(
self.lib.lib.handle,
&glyph.*.bitmap,
&converted,
1,
) != 0) {
return error.BitmapHandlingError;
}
// Free the existing glyph bitmap and
// replace it with the converted one.
_ = freetype.c.FT_Bitmap_Done(
self.lib.lib.handle,
&glyph.*.bitmap,
);
glyph.*.bitmap = converted;
},
}
const glyph_bitmap = glyph.*.bitmap;
// Round our target width and height
// as the size for our scaled bitmap.
const w: u32 = @intFromFloat(@round(width));
const h: u32 = @intFromFloat(@round(height));
const pitch = w * atlas.format.depth();
// Allocate a buffer for our scaled bitmap.
//
// We'll copy this to the original bitmap once we're
// done so we can free it at the end of this scope.
const buf = try alloc.alloc(u8, pitch * h);
defer alloc.free(buf);
// Resize
if (stb.stbir_resize_uint8(
bitmap_original.buffer,
@intCast(original_width),
@intCast(original_height),
bitmap_original.pitch,
result.buffer,
@intCast(result.width),
@intCast(result.rows),
result.pitch,
glyph_bitmap.buffer,
@intCast(glyph_bitmap.width),
@intCast(glyph_bitmap.rows),
glyph_bitmap.pitch,
buf.ptr,
@intCast(w),
@intCast(h),
@intCast(pitch),
atlas.format.depth(),
) == 0) {
// This should never fail because this is a fairly straightforward
// in-memory operation...
// This should never fail because this is a
// fairly straightforward in-memory operation...
return error.GlyphResizeFailed;
}
break :resized result;
};
defer if (bitmap_resized) |bm| {
const len = @as(usize, @intCast(bm.pitch)) * @as(usize, @intCast(bm.rows));
alloc.free(bm.buffer[0..len]);
const scaled_bitmap: freetype.c.FT_Bitmap = .{
.buffer = buf.ptr,
.width = @intCast(w),
.rows = @intCast(h),
.pitch = @intCast(pitch),
.pixel_mode = glyph_bitmap.pixel_mode,
.num_grays = glyph_bitmap.num_grays,
};
const bitmap = bitmap_resized orelse (bitmap_converted orelse bitmap_ft);
const tgt_w = bitmap.width;
const tgt_h = bitmap.rows;
// Replace the bitmap's buffer and size info.
if (freetype.c.FT_Bitmap_Copy(
self.lib.lib.handle,
&scaled_bitmap,
&bitmap,
) != 0) {
return error.BitmapHandlingError;
}
},
// Must have non-empty bitmap because we return earlier
// if zero. We assume the rest of this that it is nont-zero so
// this is important.
assert(tgt_w > 0 and tgt_h > 0);
else => |f| {
// Glyph formats are tags, so we can
// output a semi-readable error here.
log.err(
"Can't render glyph with unsupported glyph format \"{s}\"",
.{[4]u8{
@truncate(f >> 24),
@truncate(f >> 16),
@truncate(f >> 8),
@truncate(f >> 0),
}},
);
return error.UnsupportedGlyphFormat;
},
}
// If we resized our bitmap, we need to recalculate some metrics that
// we use such as the top/left offsets. These need to be scaled by the
// same ratio as the resize.
const glyph_metrics = if (bitmap_resized) |bm| metrics: {
// Our ratio for the resize
const ratio = ratio: {
const new: f64 = @floatFromInt(bm.rows);
const old: f64 = @floatFromInt(bitmap_original.rows);
break :ratio new / old;
};
// If this is a color glyph but we're trying to render it to the
// grayscale atlas, or vice versa, then we throw and error. Maybe
// in the future we could convert, but for now it should be fine.
switch (bitmap.pixel_mode) {
freetype.c.FT_PIXEL_MODE_GRAY => if (atlas.format != .grayscale) {
return error.WrongAtlas;
},
freetype.c.FT_PIXEL_MODE_BGRA => if (atlas.format != .bgra) {
return error.WrongAtlas;
},
else => {
log.warn("glyph={} pixel mode={}", .{ glyph_index, bitmap.pixel_mode });
@panic("unsupported pixel mode");
},
}
var copy = glyph.*;
copy.bitmap_top = @as(c_int, @intFromFloat(@round(@as(f64, @floatFromInt(copy.bitmap_top)) * ratio)));
copy.bitmap_left = @as(c_int, @intFromFloat(@round(@as(f64, @floatFromInt(copy.bitmap_left)) * ratio)));
break :metrics copy;
} else glyph.*;
// Allocate our texture atlas region
const region = region: {
// We need to add a 1px padding to the font so that we don't
// get fuzzy issues when blending textures.
const padding = 1;
// Get the full padded region
var region = try atlas.reserve(
alloc,
tgt_w + (padding * 2), // * 2 because left+right
tgt_h + (padding * 2), // * 2 because top+bottom
const px_width = bitmap.width;
const px_height = bitmap.rows;
const len: usize = @intCast(
@as(c_uint, @intCast(@abs(bitmap.pitch))) * bitmap.rows,
);
// Modify the region so that we remove the padding so that
// we write to the non-zero location. The data in an Altlas
// is always initialized to zero (Atlas.clear) so we don't
// need to worry about zero-ing that.
region.x += padding;
region.y += padding;
region.width -= padding * 2;
region.height -= padding * 2;
break :region region;
};
// Copy the image into the region.
assert(region.width > 0 and region.height > 0);
{
const depth = atlas.format.depth();
// We can avoid a buffer copy if our atlas width and bitmap
// width match and the bitmap pitch is just the width (meaning
// the data is tightly packed).
const needs_copy = !(tgt_w == bitmap.width and (bitmap.width * depth) == bitmap.pitch);
// If we need to copy the data, we copy it into a temporary buffer.
const buffer = if (needs_copy) buffer: {
const temp = try alloc.alloc(u8, tgt_w * tgt_h * depth);
var dst_ptr = temp;
var src_ptr = bitmap.buffer;
var i: usize = 0;
while (i < bitmap.rows) : (i += 1) {
fastmem.copy(u8, dst_ptr, src_ptr[0 .. bitmap.width * depth]);
dst_ptr = dst_ptr[tgt_w * depth ..];
src_ptr += @as(usize, @intCast(bitmap.pitch));
}
break :buffer temp;
} else bitmap.buffer[0..(tgt_w * tgt_h * depth)];
defer if (buffer.ptr != bitmap.buffer) alloc.free(buffer);
// Write the glyph information into the atlas
assert(region.width == tgt_w);
assert(region.height == tgt_h);
atlas.set(region, buffer);
}
const offset_y: c_int = offset_y: {
// For non-scalable colorized fonts, we assume they are pictographic
// and just center the glyph. So far this has only applied to emoji
// fonts. Emoji fonts don't always report a correct ascender/descender
// (mainly Apple Emoji) so we just center them. Also, since emoji font
// aren't scalable, cell_baseline is incorrect anyways.
// If our bitmap is grayscale, make sure to multiply all pixel
// values by the right factor to bring `num_grays` up to 256.
//
// NOTE(mitchellh): I don't know if this is right, this doesn't
// _feel_ right, but it makes all my limited test cases work.
if (self.face.hasColor() and !self.face.isScalable()) {
break :offset_y @intCast(tgt_h + (metrics.cell_height -| tgt_h) / 2);
// This is necessary because FT_Bitmap_Convert doesn't do this,
// it just sets num_grays to the correct number and uses the
// original smaller pixel values.
if (bitmap.pixel_mode == freetype.c.FT_PIXEL_MODE_GRAY and
bitmap.num_grays < 256)
{
const factor: u8 = @intCast(255 / (bitmap.num_grays - 1));
for (bitmap.buffer[0..len]) |*p| {
p.* *= factor;
}
bitmap.num_grays = 256;
}
// The Y offset is the offset of the top of our bitmap PLUS our
// baseline calculation. The baseline calculation is so that everything
// is properly centered when we render it out into a monospace grid.
// Note: we add here because our X/Y is actually reversed, adding goes UP.
break :offset_y glyph_metrics.bitmap_top + @as(c_int, @intCast(metrics.cell_baseline));
};
// Must have non-empty bitmap because we return earlier if zero.
// We assume the rest of this that it is non-zero so this is important.
assert(px_width > 0 and px_height > 0);
// If this doesn't match then something is wrong.
assert(px_width * atlas.format.depth() == bitmap.pitch);
// Allocate our texture atlas region and copy our bitmap in to it.
const region = try atlas.reserve(alloc, px_width, px_height);
atlas.set(region, bitmap.buffer[0..len]);
// This should be the distance from the bottom of
// the cell to the top of the glyph's bounding box.
const offset_y: i32 =
@as(i32, @intFromFloat(@floor(y))) +
@as(i32, @intCast(px_height));
// This should be the distance from the left of
// the cell to the left of the glyph's bounding box.
const offset_x: i32 = offset_x: {
var result: i32 = glyph_metrics.bitmap_left;
var result: i32 = @intFromFloat(@floor(x));
// If our cell was resized to be wider then we center our
// glyph in the cell.
// If our cell was resized then we adjust our glyph's
// position relative to the new center. This keeps glyphs
// centered in the cell whether it was made wider or narrower.
if (metrics.original_cell_width) |original_width| {
if (original_width < metrics.cell_width) {
const diff = (metrics.cell_width - original_width) / 2;
result += @intCast(diff);
}
const before: i32 = @intCast(original_width);
const after: i32 = @intCast(metrics.cell_width);
// Increase the offset by half of the difference
// between the widths to keep things centered.
result += @divTrunc(after - before, 2);
}
break :offset_x result;
};
// log.warn("renderGlyph width={} height={} offset_x={} offset_y={} glyph_metrics={}", .{
// tgt_w,
// tgt_h,
// glyph_metrics.bitmap_left,
// offset_y,
// glyph_metrics,
// });
// Store glyph metadata
return Glyph{
.width = tgt_w,
.height = tgt_h,
.width = px_width,
.height = px_height,
.offset_x = offset_x,
.offset_y = offset_y,
.atlas_x = region.x,
.atlas_y = region.y,
.advance_x = f26dot6ToFloat(glyph_metrics.advance.x),
.advance_x = f26dot6ToFloat(glyph.*.advance.x),
};
}
@ -631,7 +680,7 @@ pub const Face = struct {
}
fn f26dot6ToF64(v: freetype.c.FT_F26Dot6) f64 {
return @as(opentype.sfnt.F26Dot6, @bitCast(@as(u32, @intCast(v)))).to(f64);
return @as(F26Dot6, @bitCast(@as(i32, @intCast(v)))).to(f64);
}
pub const GetMetricsError = error{
@ -950,13 +999,15 @@ test "color emoji" {
}
// resize
// TODO: Comprehensive tests for constraints,
// this is just an adapted legacy test.
{
const glyph = try ft_font.renderGlyph(
alloc,
&atlas,
ft_font.glyphIndex('🥸').?,
.{ .grid_metrics = .{
.cell_width = 10,
.cell_width = 13,
.cell_height = 24,
.cell_baseline = 0,
.underline_position = 0,
@ -967,6 +1018,11 @@ test "color emoji" {
.overline_thickness = 0,
.box_thickness = 0,
.cursor_height = 0,
}, .constraint_width = 2, .constraint = .{
.size_horizontal = .cover,
.size_vertical = .cover,
.align_horizontal = .center,
.align_vertical = .center,
} },
);
try testing.expectEqual(@as(u32, 24), glyph.height);

View File

@ -1,88 +0,0 @@
//! Various conversions from Freetype formats to Atlas formats. These are
//! currently implemented naively. There are definitely MUCH faster ways
//! to do this (likely using SIMD), but I started simple.
const std = @import("std");
const freetype = @import("freetype");
const font = @import("../main.zig");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
/// The mapping from freetype format to atlas format.
pub const map = genMap();
/// The map type.
pub const Map = [freetype.c.FT_PIXEL_MODE_MAX]AtlasArray;
/// Conversion function type. The returning bitmap buffer is guaranteed
/// to be exactly `width * rows * depth` long for freeing it. The caller must
/// free the bitmap buffer. The depth is the depth of the atlas format in the
/// map.
pub const Func = *const fn (Allocator, Bitmap) Allocator.Error!Bitmap;
/// Alias for the freetype FT_Bitmap type to make it easier to type.
pub const Bitmap = freetype.c.struct_FT_Bitmap_;
const AtlasArray = std.EnumArray(font.Atlas.Format, ?Func);
fn genMap() Map {
var result: Map = undefined;
// Initialize to no converter
var i: usize = 0;
while (i < freetype.c.FT_PIXEL_MODE_MAX) : (i += 1) {
result[i] = .initFill(null);
}
// Map our converters
result[freetype.c.FT_PIXEL_MODE_MONO].set(.grayscale, monoToGrayscale);
return result;
}
pub fn monoToGrayscale(alloc: Allocator, bm: Bitmap) Allocator.Error!Bitmap {
var buf = try alloc.alloc(u8, bm.width * bm.rows);
errdefer alloc.free(buf);
for (0..bm.rows) |y| {
const row_offset = y * @as(usize, @intCast(bm.pitch));
for (0..bm.width) |x| {
const byte_offset = row_offset + @divTrunc(x, 8);
const mask = @as(u8, 1) << @intCast(7 - (x % 8));
const bit: u8 = @intFromBool((bm.buffer[byte_offset] & mask) != 0);
buf[y * bm.width + x] = bit * 255;
}
}
var copy = bm;
copy.buffer = buf.ptr;
copy.pixel_mode = freetype.c.FT_PIXEL_MODE_GRAY;
copy.pitch = @as(c_int, @intCast(bm.width));
return copy;
}
test {
// Force comptime to run
_ = map;
}
test "mono to grayscale" {
const testing = std.testing;
const alloc = testing.allocator;
var mono_data = [_]u8{0b1010_0101};
const source: Bitmap = .{
.rows = 1,
.width = 8,
.pitch = 1,
.buffer = @ptrCast(&mono_data),
.num_grays = 0,
.pixel_mode = freetype.c.FT_PIXEL_MODE_MONO,
.palette_mode = 0,
.palette = null,
};
const result = try monoToGrayscale(alloc, source);
defer alloc.free(result.buffer[0..(result.width * result.rows)]);
try testing.expect(result.pixel_mode == freetype.c.FT_PIXEL_MODE_GRAY);
try testing.expectEqual(@as(u8, 255), result.buffer[0]);
}

View File

@ -0,0 +1,349 @@
//! This is a generate file, produced by nerd_font_codegen.py
//! DO NOT EDIT BY HAND!
//!
//! This file provides info extracted from the nerd fonts patcher script,
//! specifying the scaling/positioning attributes of various glyphs.
const Constraint = @import("face.zig").RenderOptions.Constraint;
/// Get the a constraints for the provided codepoint.
pub fn getConstraint(cp: u21) Constraint {
return switch (cp) {
0x2500...0x259f,
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
.align_horizontal = .center,
.align_vertical = .center,
.pad_left = -0.02,
.pad_right = -0.02,
.pad_top = -0.01,
.pad_bottom = -0.01,
},
0x2630,
=> .{
.size_horizontal = .cover,
.size_vertical = .fit,
.align_horizontal = .center,
.align_vertical = .center,
.pad_left = 0.1,
.pad_right = 0.1,
.pad_top = 0.01,
.pad_bottom = 0.01,
},
0x276c...0x2771,
=> .{
.size_horizontal = .cover,
.size_vertical = .fit,
.align_horizontal = .center,
.align_vertical = .center,
},
0xe0b0,
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
.align_horizontal = .start,
.align_vertical = .center,
.pad_left = -0.06,
.pad_right = -0.06,
.pad_top = -0.01,
.pad_bottom = -0.01,
.max_xy_ratio = 0.7,
},
0xe0b1,
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
.align_horizontal = .start,
.align_vertical = .center,
.max_xy_ratio = 0.7,
},
0xe0b2,
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
.align_horizontal = .end,
.align_vertical = .center,
.pad_left = -0.06,
.pad_right = -0.06,
.pad_top = -0.01,
.pad_bottom = -0.01,
.max_xy_ratio = 0.7,
},
0xe0b3,
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
.align_horizontal = .end,
.align_vertical = .center,
.max_xy_ratio = 0.7,
},
0xe0b4,
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
.align_horizontal = .start,
.align_vertical = .center,
.pad_left = -0.06,
.pad_right = -0.06,
.pad_top = -0.01,
.pad_bottom = -0.01,
.max_xy_ratio = 0.59,
},
0xe0b5,
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
.align_horizontal = .start,
.align_vertical = .center,
.max_xy_ratio = 0.5,
},
0xe0b6,
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
.align_horizontal = .end,
.align_vertical = .center,
.pad_left = -0.06,
.pad_right = -0.06,
.pad_top = -0.01,
.pad_bottom = -0.01,
.max_xy_ratio = 0.59,
},
0xe0b7,
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
.align_horizontal = .end,
.align_vertical = .center,
.max_xy_ratio = 0.5,
},
0xe0b8,
0xe0bc,
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
.align_horizontal = .start,
.align_vertical = .center,
.pad_left = -0.05,
.pad_right = -0.05,
.pad_top = -0.01,
.pad_bottom = -0.01,
},
0xe0b9,
0xe0bd,
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
.align_horizontal = .start,
.align_vertical = .center,
},
0xe0ba,
0xe0be,
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
.align_horizontal = .end,
.align_vertical = .center,
.pad_left = -0.05,
.pad_right = -0.05,
.pad_top = -0.01,
.pad_bottom = -0.01,
},
0xe0bb,
0xe0bf,
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
.align_horizontal = .end,
.align_vertical = .center,
},
0xe0c0,
0xe0c8,
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
.align_horizontal = .start,
.align_vertical = .center,
.pad_left = -0.05,
.pad_right = -0.05,
.pad_top = -0.01,
.pad_bottom = -0.01,
},
0xe0c1,
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
.align_horizontal = .start,
.align_vertical = .center,
},
0xe0c2,
0xe0ca,
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
.align_horizontal = .end,
.align_vertical = .center,
.pad_left = -0.05,
.pad_right = -0.05,
.pad_top = -0.01,
.pad_bottom = -0.01,
},
0xe0c3,
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
.align_horizontal = .end,
.align_vertical = .center,
},
0xe0c4,
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
.align_horizontal = .start,
.align_vertical = .center,
.pad_left = 0.03,
.pad_right = 0.03,
.pad_top = 0.01,
.pad_bottom = 0.01,
.max_xy_ratio = 0.86,
},
0xe0c5,
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
.align_horizontal = .end,
.align_vertical = .center,
.pad_left = 0.03,
.pad_right = 0.03,
.pad_top = 0.01,
.pad_bottom = 0.01,
.max_xy_ratio = 0.86,
},
0xe0c6,
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
.align_horizontal = .start,
.align_vertical = .center,
.pad_left = 0.03,
.pad_right = 0.03,
.pad_top = 0.01,
.pad_bottom = 0.01,
.max_xy_ratio = 0.78,
},
0xe0c7,
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
.align_horizontal = .end,
.align_vertical = .center,
.pad_left = 0.03,
.pad_right = 0.03,
.pad_top = 0.01,
.pad_bottom = 0.01,
.max_xy_ratio = 0.78,
},
0xe0cc,
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
.align_horizontal = .start,
.align_vertical = .center,
.pad_left = -0.02,
.pad_right = -0.02,
.pad_top = -0.01,
.pad_bottom = -0.01,
.max_xy_ratio = 0.85,
},
0xe0cd,
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
.align_horizontal = .start,
.align_vertical = .center,
.max_xy_ratio = 0.865,
},
0xe0ce,
0xe0d0...0xe0d1,
=> .{
.size_horizontal = .cover,
.size_vertical = .cover,
.align_horizontal = .start,
.align_vertical = .center,
},
0xe0cf,
0xe0d3,
0xe0d5,
=> .{
.size_horizontal = .cover,
.size_vertical = .cover,
.align_horizontal = .center,
.align_vertical = .center,
},
0xe0d2,
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
.align_horizontal = .start,
.align_vertical = .center,
.pad_left = -0.02,
.pad_right = -0.02,
.pad_top = -0.01,
.pad_bottom = -0.01,
.max_xy_ratio = 0.7,
},
0xe0d4,
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
.align_horizontal = .end,
.align_vertical = .center,
.pad_left = -0.02,
.pad_right = -0.02,
.pad_top = -0.01,
.pad_bottom = -0.01,
.max_xy_ratio = 0.7,
},
0xe0d6,
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
.align_horizontal = .start,
.align_vertical = .center,
.pad_left = -0.05,
.pad_right = -0.05,
.pad_top = -0.01,
.pad_bottom = -0.01,
.max_xy_ratio = 0.7,
},
0xe0d7,
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
.align_horizontal = .end,
.align_vertical = .center,
.pad_left = -0.05,
.pad_right = -0.05,
.pad_top = -0.01,
.pad_bottom = -0.01,
.max_xy_ratio = 0.7,
},
0x23fb...0x23fe,
0x2665,
0x26a1,
0x2b58,
0xe000...0xe0a9,
0xe4fa...0xe7ef,
0xea60...0xec1e,
0xed00...0xf847,
0xf0001...0xf1af0,
=> .{
.size_horizontal = .fit,
.size_vertical = .fit,
.align_horizontal = .center,
.align_vertical = .center,
},
else => .none,
};
}

View File

@ -0,0 +1,259 @@
"""
This file is mostly vibe coded because I don't like Python. It extracts the
patch sets from the nerd fonts font patcher file in order to extract scaling
rules and attributes for different codepoint ranges which it then codegens
in to a Zig file with a function that switches over codepoints and returns
the attributes and scaling rules.
This does include an `eval` call! This is spooky, but we trust
the nerd fonts code to be safe and not malicious or anything.
"""
import ast
import math
from pathlib import Path
from collections import defaultdict
class PatchSetExtractor(ast.NodeVisitor):
def __init__(self):
self.symbol_table = {}
self.patch_set_values = []
def visit_ClassDef(self, node):
if node.name == "font_patcher":
for item in node.body:
if isinstance(item, ast.FunctionDef) and item.name == "setup_patch_set":
self.visit_setup_patch_set(item)
def visit_setup_patch_set(self, node):
# First pass: gather variable assignments
for stmt in node.body:
if isinstance(stmt, ast.Assign):
# Store simple variable assignments in the symbol table
if len(stmt.targets) == 1 and isinstance(stmt.targets[0], ast.Name):
var_name = stmt.targets[0].id
self.symbol_table[var_name] = stmt.value
# Second pass: process self.patch_set
for stmt in node.body:
if isinstance(stmt, ast.Assign):
for target in stmt.targets:
if isinstance(target, ast.Attribute) and target.attr == "patch_set":
if isinstance(stmt.value, ast.List):
for elt in stmt.value.elts:
if isinstance(elt, ast.Dict):
self.process_patch_entry(elt)
def resolve_symbol(self, node):
"""Resolve named variables to their actual values from the symbol table."""
if isinstance(node, ast.Name) and node.id in self.symbol_table:
return self.safe_literal_eval(self.symbol_table[node.id])
return self.safe_literal_eval(node)
def safe_literal_eval(self, node):
"""Try to evaluate or stringify an AST node."""
try:
return ast.literal_eval(node)
except Exception:
# Spooky eval! But we trust nerd fonts to be safe...
if hasattr(ast, "unparse"):
return eval(
ast.unparse(node), {"box_keep": True}, {"self": SpoofSelf()}
)
else:
return f"<cannot eval: {type(node).__name__}>"
def process_patch_entry(self, dict_node):
entry = {}
for key_node, value_node in zip(dict_node.keys, dict_node.values):
if isinstance(key_node, ast.Constant) and key_node.value in (
"Enabled",
"Name",
"Filename",
"Exact",
):
continue
key = ast.literal_eval(key_node)
value = self.resolve_symbol(value_node)
entry[key] = value
self.patch_set_values.append(entry)
def extract_patch_set_values(source_code):
tree = ast.parse(source_code)
extractor = PatchSetExtractor()
extractor.visit(tree)
return extractor.patch_set_values
# We have to spoof `self` and `self.args` for the eval.
class SpoofArgs:
careful = True
class SpoofSelf:
args = SpoofArgs()
def parse_alignment(val):
return {
"l": ".start",
"r": ".end",
"c": ".center",
"": None,
}.get(val, ".none")
def get_param(d, key, default):
return float(d.get(key, default))
def attr_key(attr):
"""Convert attributes to a hashable key for grouping."""
stretch = attr.get("stretch", "")
return (
parse_alignment(attr.get("align", "")),
parse_alignment(attr.get("valign", "")),
stretch,
float(attr.get("params", {}).get("overlap", 0.0)),
float(attr.get("params", {}).get("xy-ratio", -1.0)),
float(attr.get("params", {}).get("ypadding", 0.0)),
)
def coalesce_codepoints_to_ranges(codepoints):
"""Convert a sorted list of integers to a list of single values and ranges."""
ranges = []
cp_iter = iter(sorted(codepoints))
try:
start = prev = next(cp_iter)
for cp in cp_iter:
if cp == prev + 1:
prev = cp
else:
ranges.append((start, prev))
start = prev = cp
ranges.append((start, prev))
except StopIteration:
pass
return ranges
def emit_zig_entry_multikey(codepoints, attr):
align = parse_alignment(attr.get("align", ""))
valign = parse_alignment(attr.get("valign", ""))
stretch = attr.get("stretch", "")
params = attr.get("params", {})
overlap = get_param(params, "overlap", 0.0)
xy_ratio = get_param(params, "xy-ratio", -1.0)
y_padding = get_param(params, "ypadding", 0.0)
ranges = coalesce_codepoints_to_ranges(codepoints)
keys = "\n".join(
f" 0x{start:x}...0x{end:x}," if start != end else f" 0x{start:x},"
for start, end in ranges
)
s = f"""{keys}
=> .{{\n"""
# These translations don't quite capture the way
# the actual patcher does scaling, but they're a
# good enough compromise.
if ("xy" in stretch):
s += " .size_horizontal = .stretch,\n"
s += " .size_vertical = .stretch,\n"
elif ("!" in stretch):
s += " .size_horizontal = .cover,\n"
s += " .size_vertical = .fit,\n"
elif ("^" in stretch):
s += " .size_horizontal = .cover,\n"
s += " .size_vertical = .cover,\n"
else:
s += " .size_horizontal = .fit,\n"
s += " .size_vertical = .fit,\n"
if (align is not None):
s += f" .align_horizontal = {align},\n"
if (valign is not None):
s += f" .align_vertical = {valign},\n"
if (overlap != 0.0):
pad = -overlap
s += f" .pad_left = {pad},\n"
s += f" .pad_right = {pad},\n"
v_pad = y_padding - math.copysign(min(0.01, abs(overlap)), overlap)
s += f" .pad_top = {v_pad},\n"
s += f" .pad_bottom = {v_pad},\n"
if (xy_ratio > 0):
s += f" .max_xy_ratio = {xy_ratio},\n"
s += " },"
return s
def generate_zig_switch_arms(patch_set):
entries = {}
for entry in patch_set:
attributes = entry["Attributes"]
for cp in range(entry["SymStart"], entry["SymEnd"] + 1):
entries[cp] = attributes["default"]
for k, v in attributes.items():
if isinstance(k, int):
entries[k] = v
del entries[0]
# Group codepoints by attribute key
grouped = defaultdict(list)
for cp, attr in entries.items():
grouped[attr_key(attr)].append(cp)
# Emit zig switch arms
result = []
for _, codepoints in sorted(grouped.items(), key=lambda x: x[1]):
# Use one of the attrs in the group to emit the value
attr = entries[codepoints[0]]
result.append(emit_zig_entry_multikey(codepoints, attr))
return "\n".join(result)
if __name__ == "__main__":
path = (
Path(__file__).resolve().parent
/ ".."
/ ".."
/ "vendor"
/ "nerd-fonts"
/ "font-patcher.py"
)
with open(path, "r", encoding="utf-8") as f:
source = f.read()
patch_set = extract_patch_set_values(source)
out_path = Path(__file__).resolve().parent / "nerd_font_attributes.zig"
with open(out_path, "w", encoding="utf-8") as f:
f.write("""//! This is a generate file, produced by nerd_font_codegen.py
//! DO NOT EDIT BY HAND!
//!
//! This file provides info extracted from the nerd fonts patcher script,
//! specifying the scaling/positioning attributes of various glyphs.
const Constraint = @import("face.zig").RenderOptions.Constraint;
/// Get the a constraints for the provided codepoint.
pub fn getConstraint(cp: u21) Constraint {
return switch (cp) {
""")
f.write(generate_zig_switch_arms(patch_set))
f.write("\n")
f.write(" else => .none,\n };\n}\n")

View File

@ -76,24 +76,22 @@ fn FixedPoint(comptime T: type, int_bits: u64, frac_bits: u64) type {
));
const half = @as(T, 1) << @intCast(frac_bits - 1);
frac: std.meta.Int(.unsigned, frac_bits),
int: std.meta.Int(type_info.signedness, int_bits),
const Frac = std.meta.Int(.unsigned, frac_bits);
const Int = std.meta.Int(type_info.signedness, int_bits);
frac: Frac,
int: Int,
pub fn to(self: Self, comptime FloatType: type) FloatType {
const i: FloatType = @floatFromInt(self.int);
const f: FloatType = @floatFromInt(self.frac);
return i + f / frac_factor;
return @as(FloatType, @floatFromInt(
@as(T, @bitCast(self)),
)) / frac_factor;
}
pub fn from(float: anytype) Self {
const int = @floor(float);
const frac = @abs(float - int);
return .{
.int = @intFromFloat(int),
.frac = @intFromFloat(@round(frac * frac_factor)),
};
return @bitCast(
@as(T, @intFromFloat(@round(float * frac_factor))),
);
}
/// Round to the nearest integer, .5 rounds away from 0.

View File

@ -1769,7 +1769,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
.geist_mono => font.embedded.geist_mono,
.jetbrains_mono => font.embedded.jetbrains_mono,
.monaspace_neon => font.embedded.monaspace_neon,
.nerd_font => font.embedded.nerd_font,
.nerd_font => font.embedded.test_nerd_font,
};
var lib = try Library.init(alloc);

View File

@ -218,63 +218,20 @@ pub fn isCovering(cp: u21) bool {
};
}
pub const FgMode = enum {
/// Normal non-colored text rendering. The text can leave the cell
/// size if it is larger than the cell to allow for ligatures.
normal,
/// Colored text rendering, specifically Emoji.
color,
/// Similar to normal but the text must be constrained to the cell
/// size. If a glyph is larger than the cell then it must be resized
/// to fit.
constrained,
/// Similar to normal, but the text consists of Powerline glyphs and is
/// optionally exempt from padding color extension and minimum contrast requirements.
powerline,
};
/// Returns the appropriate foreground mode for the given cell. This is
/// meant to be called from the typical updateCell function within a
/// renderer.
pub fn fgMode(
presentation: font.Presentation,
cell_pin: terminal.Pin,
) FgMode {
return switch (presentation) {
// Emoji is always full size and color.
.emoji => .color,
// If it is text it is slightly more complex. If we are a codepoint
// in the private use area and we are at the end or the next cell
// is not empty, we need to constrain rendering.
//
// We do this specifically so that Nerd Fonts can render their
// icons without overlapping with subsequent characters. But if
// the subsequent character is empty, then we allow it to use
// the full glyph size. See #1071.
.text => text: {
/// Returns the appropriate `constraint_width` for
/// the provided cell when rendering its glyph(s).
pub fn constraintWidth(cell_pin: terminal.Pin) u2 {
const cell = cell_pin.rowAndCell().cell;
const cp = cell.codepoint();
if (!ziglyph.general_category.isPrivateUse(cp) and
!ziglyph.blocks.isDingbats(cp))
{
break :text .normal;
return cell.gridWidth();
}
// Special-case Powerline glyphs. They exhibit box drawing behavior
// and should not be constrained. They have their own special category
// though because they're used for other logic (i.e. disabling
// min contrast).
if (isPowerline(cp)) {
break :text .powerline;
}
// If we are at the end of the screen its definitely constrained
if (cell_pin.x == cell_pin.node.data.size.cols - 1) break :text .constrained;
// If we are at the end of the screen it must be constrained to one cell.
if (cell_pin.x == cell_pin.node.data.size.cols - 1) return 1;
// If we have a previous cell and it was PUA then we need to
// also constrain. This is so that multiple PUA glyphs align.
@ -288,16 +245,16 @@ pub fn fgMode(
break :prev_cp prev_cell.codepoint();
};
// Powerline is whitespace
// We consider powerline glyphs whitespace.
if (isPowerline(prev_cp)) break :prev;
if (ziglyph.general_category.isPrivateUse(prev_cp)) {
break :text .constrained;
return 1;
}
}
// If the next cell is empty, then we allow it to use the
// full glyph size.
// If the next cell is whitespace, then
// we allow it to be up to two cells wide.
const next_cp = next_cp: {
var copy = cell_pin;
copy.x += 1;
@ -308,13 +265,17 @@ pub fn fgMode(
isSpace(next_cp) or
isPowerline(next_cp))
{
break :text .normal;
return 2;
}
// Must be constrained
break :text .constrained;
},
};
return 1;
}
/// Whether min contrast should be disabled for a given glyph.
pub fn noMinContrast(cp: u21) bool {
// TODO: We should disable for all box drawing type characters.
return isPowerline(cp);
}
// Some general spaces, others intentionally kept
@ -361,7 +322,7 @@ test Contents {
// Add some contents.
const bg_cell: shaderpkg.CellBg = .{ 0, 0, 0, 1 };
const fg_cell: shaderpkg.CellText = .{
.mode = .fg,
.atlas = .grayscale,
.grid_pos = .{ 4, 1 },
.color = .{ 0, 0, 0, 1 },
};
@ -382,7 +343,8 @@ test Contents {
// Add a block cursor.
const cursor_cell: shaderpkg.CellText = .{
.mode = .cursor,
.atlas = .grayscale,
.bools = .{ .is_cursor_glyph = true },
.grid_pos = .{ 2, 3 },
.color = .{ 0, 0, 0, 1 },
};
@ -413,7 +375,7 @@ test "Contents clear retains other content" {
// bg and fg cells in row 1
const bg_cell_1: shaderpkg.CellBg = .{ 0, 0, 0, 1 };
const fg_cell_1: shaderpkg.CellText = .{
.mode = .fg,
.atlas = .grayscale,
.grid_pos = .{ 4, 1 },
.color = .{ 0, 0, 0, 1 },
};
@ -422,7 +384,7 @@ test "Contents clear retains other content" {
// bg and fg cells in row 2
const bg_cell_2: shaderpkg.CellBg = .{ 0, 0, 0, 1 };
const fg_cell_2: shaderpkg.CellText = .{
.mode = .fg,
.atlas = .grayscale,
.grid_pos = .{ 4, 2 },
.color = .{ 0, 0, 0, 1 },
};
@ -453,7 +415,7 @@ test "Contents clear last added content" {
// bg and fg cells in row 1
const bg_cell_1: shaderpkg.CellBg = .{ 0, 0, 0, 1 };
const fg_cell_1: shaderpkg.CellText = .{
.mode = .fg,
.atlas = .grayscale,
.grid_pos = .{ 4, 1 },
.color = .{ 0, 0, 0, 1 },
};
@ -462,7 +424,7 @@ test "Contents clear last added content" {
// bg and fg cells in row 2
const bg_cell_2: shaderpkg.CellBg = .{ 0, 0, 0, 1 };
const fg_cell_2: shaderpkg.CellText = .{
.mode = .fg,
.atlas = .grayscale,
.grid_pos = .{ 4, 2 },
.color = .{ 0, 0, 0, 1 },
};

View File

@ -12,7 +12,8 @@ const math = @import("../math.zig");
const Surface = @import("../Surface.zig");
const link = @import("link.zig");
const cellpkg = @import("cell.zig");
const fgMode = cellpkg.fgMode;
const noMinContrast = cellpkg.noMinContrast;
const constraintWidth = cellpkg.constraintWidth;
const isCovering = cellpkg.isCovering;
const imagepkg = @import("image.zig");
const Image = imagepkg.Image;
@ -25,6 +26,8 @@ const ArenaAllocator = std.heap.ArenaAllocator;
const Terminal = terminal.Terminal;
const Health = renderer.Health;
const getConstraint = @import("../font/nerd_font_attributes.zig").getConstraint;
const FileType = @import("../file_type.zig").FileType;
const macos = switch (builtin.os.tag) {
@ -2924,9 +2927,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
);
try self.cells.add(self.alloc, .underline, .{
.mode = .fg,
.atlas = .grayscale,
.grid_pos = .{ @intCast(x), @intCast(y) },
.constraint_width = 1,
.color = .{ color.r, color.g, color.b, alpha },
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
.glyph_size = .{ render.glyph.width, render.glyph.height },
@ -2956,9 +2958,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
);
try self.cells.add(self.alloc, .overline, .{
.mode = .fg,
.atlas = .grayscale,
.grid_pos = .{ @intCast(x), @intCast(y) },
.constraint_width = 1,
.color = .{ color.r, color.g, color.b, alpha },
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
.glyph_size = .{ render.glyph.width, render.glyph.height },
@ -2988,9 +2989,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
);
try self.cells.add(self.alloc, .strikethrough, .{
.mode = .fg,
.atlas = .grayscale,
.grid_pos = .{ @intCast(x), @intCast(y) },
.constraint_width = 1,
.color = .{ color.r, color.g, color.b, alpha },
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
.glyph_size = .{ render.glyph.width, render.glyph.height },
@ -3015,6 +3015,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
const rac = cell_pin.rowAndCell();
const cell = rac.cell;
const cp = cell.codepoint();
// Render
const render = try self.font_grid.renderGlyph(
self.alloc,
@ -3024,6 +3026,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.grid_metrics = self.grid_metrics,
.thicken = self.config.font_thicken,
.thicken_strength = self.config.font_thicken_strength,
.cell_width = cell.gridWidth(),
.constraint = getConstraint(cp),
.constraint_width = constraintWidth(cell_pin),
},
);
@ -3033,27 +3038,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
return;
}
// We always use fg mode for sprite glyphs, since we know we never
// need to constrain them, and we don't have any color sprites.
//
// Otherwise we defer to `fgMode`.
const mode: shaderpkg.CellText.Mode =
if (render.glyph.sprite)
.fg
else switch (fgMode(
render.presentation,
cell_pin,
)) {
.normal => .fg,
.color => .fg_color,
.constrained => .fg_constrained,
.powerline => .fg_powerline,
};
try self.cells.add(self.alloc, .text, .{
.mode = mode,
.atlas = switch (render.presentation) {
.emoji => .color,
.text => .grayscale,
},
.bools = .{ .no_min_contrast = noMinContrast(cp) },
.grid_pos = .{ @intCast(x), @intCast(y) },
.constraint_width = cell.gridWidth(),
.color = .{ color.r, color.g, color.b, alpha },
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
.glyph_size = .{ render.glyph.width, render.glyph.height },
@ -3138,7 +3129,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
};
self.cells.setCursor(.{
.mode = .cursor,
.atlas = .grayscale,
.bools = .{ .is_cursor_glyph = true },
.grid_pos = .{ x, screen.cursor.y },
.color = .{ cursor_color.r, cursor_color.g, cursor_color.b, alpha },
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
@ -3187,7 +3179,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// Add our text
try self.cells.add(self.alloc, .text, .{
.mode = .fg,
.atlas = .grayscale,
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
.color = .{ fg.r, fg.g, fg.b, 255 },
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },

View File

@ -269,15 +269,16 @@ pub const CellText = extern struct {
bearings: [2]i16 align(4) = .{ 0, 0 },
grid_pos: [2]u16 align(4),
color: [4]u8 align(4),
mode: Mode align(1),
constraint_width: u8 align(1) = 0,
atlas: Atlas align(1),
bools: packed struct(u8) {
no_min_contrast: bool = false,
is_cursor_glyph: bool = false,
_padding: u6 = 0,
} align(1) = .{},
pub const Mode = enum(u8) {
fg = 1,
fg_constrained = 2,
fg_color = 3,
cursor = 4,
fg_powerline = 5,
pub const Atlas = enum(u8) {
grayscale = 0,
color = 1,
};
test {

View File

@ -237,15 +237,16 @@ pub const CellText = extern struct {
bearings: [2]i16 align(4) = .{ 0, 0 },
grid_pos: [2]u16 align(4),
color: [4]u8 align(4),
mode: Mode align(4),
constraint_width: u32 align(4) = 0,
atlas: Atlas align(1),
bools: packed struct(u8) {
no_min_contrast: bool = false,
is_cursor_glyph: bool = false,
_padding: u6 = 0,
} align(1) = .{},
pub const Mode = enum(u32) {
fg = 1,
fg_constrained = 2,
fg_color = 3,
cursor = 4,
fg_powerline = 5,
pub const Atlas = enum(u8) {
grayscale = 0,
color = 1,
};
// test {

View File

@ -4,21 +4,15 @@ layout(binding = 0) uniform sampler2DRect atlas_grayscale;
layout(binding = 1) uniform sampler2DRect atlas_color;
in CellTextVertexOut {
flat uint mode;
flat uint atlas;
flat vec4 color;
flat vec4 bg_color;
vec2 tex_coord;
} in_data;
// These are the possible modes that "mode" can be set to. This is
// used to multiplex multiple render modes into a single shader.
//
// NOTE: this must be kept in sync with the fragment shader
const uint MODE_TEXT = 1u;
const uint MODE_TEXT_CONSTRAINED = 2u;
const uint MODE_TEXT_COLOR = 3u;
const uint MODE_TEXT_CURSOR = 4u;
const uint MODE_TEXT_POWERLINE = 5u;
// Values `atlas` can take.
const uint ATLAS_GRAYSCALE = 0u;
const uint ATLAS_COLOR = 1u;
// Must declare this output for some versions of OpenGL.
layout(location = 0) out vec4 out_FragColor;
@ -27,12 +21,9 @@ void main() {
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
bool use_linear_correction = (bools & USE_LINEAR_CORRECTION) != 0;
switch (in_data.mode) {
switch (in_data.atlas) {
default:
case MODE_TEXT_CURSOR:
case MODE_TEXT_CONSTRAINED:
case MODE_TEXT_POWERLINE:
case MODE_TEXT:
case ATLAS_GRAYSCALE:
{
// Our input color is always linear.
vec4 color = in_data.color;
@ -84,7 +75,7 @@ void main() {
return;
}
case MODE_TEXT_COLOR:
case ATLAS_COLOR:
{
// For now, we assume that color glyphs
// are already premultiplied linear colors.

View File

@ -15,22 +15,22 @@ layout(location = 3) in uvec2 grid_pos;
// The color of the rendered text glyph.
layout(location = 4) in uvec4 color;
// The mode for this cell.
layout(location = 5) in uint mode;
// Which atlas this glyph is in.
layout(location = 5) in uint atlas;
// The width to constrain the glyph to, in cells, or 0 for no constraint.
layout(location = 6) in uint constraint_width;
// Misc glyph properties.
layout(location = 6) in uint glyph_bools;
// These are the possible modes that "mode" can be set to. This is
// used to multiplex multiple render modes into a single shader.
const uint MODE_TEXT = 1u;
const uint MODE_TEXT_CONSTRAINED = 2u;
const uint MODE_TEXT_COLOR = 3u;
const uint MODE_TEXT_CURSOR = 4u;
const uint MODE_TEXT_POWERLINE = 5u;
// Values `atlas` can take.
const uint ATLAS_GRAYSCALE = 0u;
const uint ATLAS_COLOR = 1u;
// Masks for the `glyph_bools` attribute
const uint NO_MIN_CONTRAST = 1u;
const uint IS_CURSOR_GLYPH = 2u;
out CellTextVertexOut {
flat uint mode;
flat uint atlas;
flat vec4 color;
flat vec4 bg_color;
vec2 tex_coord;
@ -69,7 +69,7 @@ void main() {
corner.x = float(vid == 1 || vid == 3);
corner.y = float(vid == 2 || vid == 3);
out_data.mode = mode;
out_data.atlas = atlas;
// === Grid Cell ===
// +X
@ -102,25 +102,6 @@ void main() {
offset.y = cell_size.y - offset.y;
// If we're constrained then we need to scale the glyph.
if (mode == MODE_TEXT_CONSTRAINED) {
float max_width = cell_size.x * constraint_width;
// If this glyph is wider than the constraint width,
// fit it to the width and remove its horizontal offset.
if (size.x > max_width) {
float new_y = size.y * (max_width / size.x);
offset.y += (size.y - new_y) / 2.0;
offset.x = 0.0;
size.y = new_y;
size.x = max_width;
} else if (max_width - size.x > offset.x) {
// However, if it does fit in the constraint width, make
// sure the offset is small enough to not push it over the
// right edge of the constraint width.
offset.x = max_width - size.x;
}
}
// Calculate the final position of the cell which uses our glyph size
// and glyph offset to create the correct bounding box for the glyph.
cell_pos = cell_pos + size * corner + offset;
@ -149,11 +130,7 @@ void main() {
// If we have a minimum contrast, we need to check if we need to
// change the color of the text to ensure it has enough contrast
// with the background.
// We only apply this adjustment to "normal" text with MODE_TEXT,
// since we want color glyphs to appear in their original color
// and Powerline glyphs to be unaffected (else parts of the line would
// have different colors as some parts are displayed via background colors).
if (min_contrast > 1.0f && mode == MODE_TEXT) {
if (min_contrast > 1.0f && (glyph_bools & NO_MIN_CONTRAST) == 0) {
// Ensure our minimum contrast
out_data.color = contrasted_color(min_contrast, out_data.color, out_data.bg_color);
}
@ -161,8 +138,9 @@ void main() {
// Check if current position is under cursor (including wide cursor)
bool is_cursor_pos = ((grid_pos.x == cursor_pos.x) || (cursor_wide && (grid_pos.x == (cursor_pos.x + 1)))) && (grid_pos.y == cursor_pos.y);
// If this cell is the cursor cell, then we need to change the color.
if (mode != MODE_TEXT_CURSOR && is_cursor_pos) {
// If this cell is the cursor cell, but we're not processing
// the cursor glyph itself, then we need to change the color.
if ((glyph_bools & IS_CURSOR_GLYPH) == 0 && is_cursor_pos) {
out_data.color = load_color(unpack4u8(cursor_color_packed_4u8), use_linear_blending);
}
}

View File

@ -509,13 +509,17 @@ fragment float4 cell_bg_fragment(
//-------------------------------------------------------------------
#pragma mark - Cell Text Shader
// The possible modes that a cell fg entry can take.
enum CellTextMode : uint8_t {
MODE_TEXT = 1u,
MODE_TEXT_CONSTRAINED = 2u,
MODE_TEXT_COLOR = 3u,
MODE_TEXT_CURSOR = 4u,
MODE_TEXT_POWERLINE = 5u,
enum CellTextAtlas : uint8_t {
ATLAS_GRAYSCALE = 0u,
ATLAS_COLOR = 1u,
};
// We use a packed struct of bools for misc properties of the glyph.
enum CellTextBools : uint8_t {
// Don't apply min contrast to this glyph.
NO_MIN_CONTRAST = 1u,
// This is the cursor glyph.
IS_CURSOR_GLYPH = 2u,
};
struct CellTextVertexIn {
@ -534,16 +538,16 @@ struct CellTextVertexIn {
// The color of the rendered text glyph.
uchar4 color [[attribute(4)]];
// The mode for this cell.
uint8_t mode [[attribute(5)]];
// Which atlas to sample for our glyph.
uint8_t atlas [[attribute(5)]];
// The width to constrain the glyph to, in cells, or 0 for no constraint.
uint8_t constraint_width [[attribute(6)]];
// Misc properties of the glyph.
uint8_t bools [[attribute(6)]];
};
struct CellTextVertexOut {
float4 position [[position]];
uint8_t mode [[flat]];
uint8_t atlas [[flat]];
float4 color [[flat]];
float4 bg_color [[flat]];
float2 tex_coord;
@ -577,7 +581,7 @@ vertex CellTextVertexOut cell_text_vertex(
corner.y = float(vid == 2 || vid == 3);
CellTextVertexOut out;
out.mode = in.mode;
out.atlas = in.atlas;
// === Grid Cell ===
// +X
@ -610,25 +614,6 @@ vertex CellTextVertexOut cell_text_vertex(
offset.y = uniforms.cell_size.y - offset.y;
// If we're constrained then we need to scale the glyph.
if (in.mode == MODE_TEXT_CONSTRAINED) {
float max_width = uniforms.cell_size.x * in.constraint_width;
// If this glyph is wider than the constraint width,
// fit it to the width and remove its horizontal offset.
if (size.x > max_width) {
float new_y = size.y * (max_width / size.x);
offset.y += (size.y - new_y) / 2;
offset.x = 0;
size.y = new_y;
size.x = max_width;
} else if (max_width - size.x > offset.x) {
// However, if it does fit in the constraint width, make
// sure the offset is small enough to not push it over the
// right edge of the constraint width.
offset.x = max_width - size.x;
}
}
// Calculate the final position of the cell which uses our glyph size
// and glyph offset to create the correct bounding box for the glyph.
cell_pos = cell_pos + size * corner + offset;
@ -665,11 +650,7 @@ vertex CellTextVertexOut cell_text_vertex(
// If we have a minimum contrast, we need to check if we need to
// change the color of the text to ensure it has enough contrast
// with the background.
// We only apply this adjustment to "normal" text with MODE_TEXT,
// since we want color glyphs to appear in their original color
// and Powerline glyphs to be unaffected (else parts of the line would
// have different colors as some parts are displayed via background colors).
if (uniforms.min_contrast > 1.0f && in.mode == MODE_TEXT) {
if (uniforms.min_contrast > 1.0f && (in.bools & NO_MIN_CONTRAST) == 0) {
// Ensure our minimum contrast
out.color = contrasted_color(uniforms.min_contrast, out.color, out.bg_color);
}
@ -681,8 +662,9 @@ vertex CellTextVertexOut cell_text_vertex(
in.grid_pos.x == uniforms.cursor_pos.x + 1
) && in.grid_pos.y == uniforms.cursor_pos.y;
// If this cell is the cursor cell, then we need to change the color.
if (in.mode != MODE_TEXT_CURSOR && is_cursor_pos) {
// If this cell is the cursor cell, but we're not processing
// the cursor glyph itself, then we need to change the color.
if ((in.bools & IS_CURSOR_GLYPH) == 0 && is_cursor_pos) {
out.color = load_color(
uniforms.cursor_color,
uniforms.use_display_p3,
@ -702,19 +684,12 @@ fragment float4 cell_text_fragment(
constexpr sampler textureSampler(
coord::pixel,
address::clamp_to_edge,
// TODO(qwerasd): This can be changed back to filter::nearest when
// we move the constraint logic out of the GPU code
// which should once again guarantee pixel perfect
// sizing.
filter::linear
filter::nearest
);
switch (in.mode) {
switch (in.atlas) {
default:
case MODE_TEXT_CURSOR:
case MODE_TEXT_CONSTRAINED:
case MODE_TEXT_POWERLINE:
case MODE_TEXT: {
case ATLAS_GRAYSCALE: {
// Our input color is always linear.
float4 color = in.color;
@ -764,7 +739,7 @@ fragment float4 cell_text_fragment(
return color;
}
case MODE_TEXT_COLOR: {
case ATLAS_COLOR: {
// For now, we assume that color glyphs
// are already premultiplied linear colors.
float4 color = textureColor.sample(textureSampler, in.tex_coord);

126
vendor/nerd-fonts/LICENSE vendored Normal file
View File

@ -0,0 +1,126 @@
# Nerd Fonts Licensing
There are various sources used under various licenses:
* Nerd Fonts source fonts, patched fonts, and folders with explict OFL SIL files are licensed under SIL OPEN FONT LICENSE Version 1.1 (see below).
* Nerd Fonts original source code files (such as `.sh`, `.py`, `font-patcher` and others) are licensed under the MIT License (MIT) (see below).
* Many other licenses are present in this project for even more detailed breakdown see: [License Audit](https://github.com/ryanoasis/nerd-fonts/blob/-/license-audit.md).
## Source files not in folders containing an explicit license are using the MIT License (MIT)
The MIT License (MIT)
Copyright (c) 2014 Ryan L McIntyre
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## Various Fonts, Patched Fonts, SVGs, Glyph Fonts, and any files in a folder with explicit SIL OFL 1.1 License
Copyright (c) 2014, Ryan L McIntyre (https://ryanlmcintyre.com).
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

10
vendor/nerd-fonts/README.md vendored Normal file
View File

@ -0,0 +1,10 @@
We have a copy of the `font-patcher` file from `nerd-fonts` here, fetched from
https://github.com/ryanoasis/nerd-fonts/blob/master/font-patcher.
This is MIT licensed, see `LICENSE` in this directory.
We use parse a section of this file to codegen a lookup table of the nerd font
scaling rules. See `src/font/nerd_font_codegen.py` in the main Ghostty source
tree for more info.
Last fetched commit: ebc376cbd43f609d8084f47dd348646595ce066e

2296
vendor/nerd-fonts/font-patcher.py vendored Normal file

File diff suppressed because it is too large Load Diff