mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-25 13:16:11 +03:00
font: handle presentation at glyph layer
This commit is contained in:
@ -294,13 +294,14 @@ fn getIndexCodepointOverride(
|
|||||||
pub fn getPresentation(
|
pub fn getPresentation(
|
||||||
self: *CodepointResolver,
|
self: *CodepointResolver,
|
||||||
index: Collection.Index,
|
index: Collection.Index,
|
||||||
|
glyph_index: u32,
|
||||||
) !Presentation {
|
) !Presentation {
|
||||||
if (index.special()) |sp| return switch (sp) {
|
if (index.special()) |sp| return switch (sp) {
|
||||||
.sprite => .text,
|
.sprite => .text,
|
||||||
};
|
};
|
||||||
|
|
||||||
const face = try self.collection.getFace(index);
|
const face = try self.collection.getFace(index);
|
||||||
return face.presentation;
|
return if (face.isColorGlyph(glyph_index)) .emoji else .text;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render a glyph by glyph index into the given font atlas and return
|
/// Render a glyph by glyph index into the given font atlas and return
|
||||||
|
@ -190,16 +190,23 @@ pub fn autoItalicize(self: *Collection, alloc: Allocator) !void {
|
|||||||
const list = self.faces.get(.regular);
|
const list = self.faces.get(.regular);
|
||||||
if (list.items.len == 0) return;
|
if (list.items.len == 0) return;
|
||||||
|
|
||||||
// Find our first font that is text. This will force
|
// Find our first regular face that has text glyphs.
|
||||||
// loading any deferred faces but we only load them until
|
|
||||||
// we find a text face. A text face is almost always the
|
|
||||||
// first face in the list.
|
|
||||||
for (0..list.items.len) |i| {
|
for (0..list.items.len) |i| {
|
||||||
const face = try self.getFace(.{
|
const face = try self.getFace(.{
|
||||||
.style = .regular,
|
.style = .regular,
|
||||||
.idx = @intCast(i),
|
.idx = @intCast(i),
|
||||||
});
|
});
|
||||||
if (face.presentation == .text) break :regular face;
|
|
||||||
|
// We have two conditionals here. The color check is obvious:
|
||||||
|
// we want to auto-italicize a normal text font. The second
|
||||||
|
// check is less obvious... for mixed color/non-color fonts, we
|
||||||
|
// accept the regular font if it has basic ASCII. This may not
|
||||||
|
// be strictly correct (especially with international fonts) but
|
||||||
|
// it's a reasonable heuristic and the first case will match 99%
|
||||||
|
// of the time.
|
||||||
|
if (!face.hasColor() or face.glyphIndex('A') != null) {
|
||||||
|
break :regular face;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No regular text face found.
|
// No regular text face found.
|
||||||
@ -344,7 +351,13 @@ pub const Entry = union(enum) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
.loaded => |face| switch (p_mode) {
|
.loaded => |face| switch (p_mode) {
|
||||||
.explicit => |p| face.presentation == p and face.glyphIndex(cp) != null,
|
.explicit => |p| explicit: {
|
||||||
|
const index = face.glyphIndex(cp) orelse break :explicit false;
|
||||||
|
break :explicit switch (p) {
|
||||||
|
.text => !face.isColorGlyph(index),
|
||||||
|
.emoji => face.isColorGlyph(index),
|
||||||
|
};
|
||||||
|
},
|
||||||
.default, .any => face.glyphIndex(cp) != null,
|
.default, .any => face.glyphIndex(cp) != null,
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -357,7 +370,13 @@ pub const Entry = union(enum) {
|
|||||||
.fallback_loaded => |face| switch (p_mode) {
|
.fallback_loaded => |face| switch (p_mode) {
|
||||||
.explicit,
|
.explicit,
|
||||||
.default,
|
.default,
|
||||||
=> |p| face.presentation == p and face.glyphIndex(cp) != null,
|
=> |p| explicit: {
|
||||||
|
const index = face.glyphIndex(cp) orelse break :explicit false;
|
||||||
|
break :explicit switch (p) {
|
||||||
|
.text => !face.isColorGlyph(index),
|
||||||
|
.emoji => face.isColorGlyph(index),
|
||||||
|
};
|
||||||
|
},
|
||||||
.any => face.glyphIndex(cp) != null,
|
.any => face.glyphIndex(cp) != null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -371,7 +390,7 @@ pub const PresentationMode = union(enum) {
|
|||||||
explicit: Presentation,
|
explicit: Presentation,
|
||||||
|
|
||||||
/// The codepoint has no explicit presentation and we should use
|
/// The codepoint has no explicit presentation and we should use
|
||||||
/// the presentation from the UCd.
|
/// the presentation from the UCD.
|
||||||
default: Presentation,
|
default: Presentation,
|
||||||
|
|
||||||
/// The codepoint can be any presentation.
|
/// The codepoint can be any presentation.
|
||||||
|
@ -281,6 +281,7 @@ pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool {
|
|||||||
=> {
|
=> {
|
||||||
// If we are using coretext, we check the loaded CT font.
|
// If we are using coretext, we check the loaded CT font.
|
||||||
if (self.ct) |ct| {
|
if (self.ct) |ct| {
|
||||||
|
// TODO(mixed-fonts): handle presentation on a glyph level
|
||||||
if (p) |desired_p| {
|
if (p) |desired_p| {
|
||||||
const traits = ct.font.getSymbolicTraits();
|
const traits = ct.font.getSymbolicTraits();
|
||||||
const actual_p: Presentation = if (traits.color_glyphs) .emoji else .text;
|
const actual_p: Presentation = if (traits.color_glyphs) .emoji else .text;
|
||||||
|
@ -253,7 +253,7 @@ pub fn renderGlyph(
|
|||||||
if (gop.found_existing) return gop.value_ptr.*;
|
if (gop.found_existing) return gop.value_ptr.*;
|
||||||
|
|
||||||
// Get the presentation to determine what atlas to use
|
// Get the presentation to determine what atlas to use
|
||||||
const p = try self.resolver.getPresentation(index);
|
const p = try self.resolver.getPresentation(index, glyph_index);
|
||||||
const atlas: *font.Atlas = switch (p) {
|
const atlas: *font.Atlas = switch (p) {
|
||||||
.text => &self.atlas_greyscale,
|
.text => &self.atlas_greyscale,
|
||||||
.emoji => &self.atlas_color,
|
.emoji => &self.atlas_color,
|
||||||
|
@ -19,9 +19,6 @@ pub const Face = struct {
|
|||||||
/// if we're using Harfbuzz.
|
/// if we're using Harfbuzz.
|
||||||
hb_font: if (harfbuzz_shaper) harfbuzz.Font else void,
|
hb_font: if (harfbuzz_shaper) harfbuzz.Font else void,
|
||||||
|
|
||||||
/// The presentation for this font.
|
|
||||||
presentation: font.Presentation,
|
|
||||||
|
|
||||||
/// Metrics for this font face. These are useful for renderers.
|
/// Metrics for this font face. These are useful for renderers.
|
||||||
metrics: font.face.Metrics,
|
metrics: font.face.Metrics,
|
||||||
|
|
||||||
@ -111,26 +108,11 @@ pub const Face = struct {
|
|||||||
var result: Face = .{
|
var result: Face = .{
|
||||||
.font = ct_font,
|
.font = ct_font,
|
||||||
.hb_font = hb_font,
|
.hb_font = hb_font,
|
||||||
.presentation = if (traits.color_glyphs) .emoji else .text,
|
|
||||||
.metrics = metrics,
|
.metrics = metrics,
|
||||||
.color = color,
|
.color = color,
|
||||||
};
|
};
|
||||||
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
|
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
|
||||||
|
|
||||||
// If our presentation is emoji, we also check for the presence of
|
|
||||||
// emoji codepoints. This forces fonts with colorized glyphs that aren't
|
|
||||||
// emoji font to be treated as text. Long term, this isn't what we want
|
|
||||||
// but this fixes some bugs in the short term. See:
|
|
||||||
// https://github.com/mitchellh/ghostty/issues/1768
|
|
||||||
//
|
|
||||||
// Longer term, we'd like to detect mixed color/non-color fonts and
|
|
||||||
// handle them correctly by rendering the color glyphs as color and the
|
|
||||||
// non-color glyphs as text.
|
|
||||||
if (result.presentation == .emoji and result.glyphIndex('🥸') == null) {
|
|
||||||
log.warn("font has colorized glyphs but isn't emoji, treating as text", .{});
|
|
||||||
result.presentation = .text;
|
|
||||||
}
|
|
||||||
|
|
||||||
// In debug mode, we output information about available variation axes,
|
// In debug mode, we output information about available variation axes,
|
||||||
// if they exist.
|
// if they exist.
|
||||||
if (comptime builtin.mode == .Debug) {
|
if (comptime builtin.mode == .Debug) {
|
||||||
@ -244,15 +226,15 @@ pub const Face = struct {
|
|||||||
|
|
||||||
/// Returns true if the face has any glyphs that are colorized.
|
/// Returns true if the face has any glyphs that are colorized.
|
||||||
/// To determine if an individual glyph is colorized you must use
|
/// To determine if an individual glyph is colorized you must use
|
||||||
/// isColored.
|
/// isColorGlyph.
|
||||||
pub fn hasColor(self: *const Face) bool {
|
pub fn hasColor(self: *const Face) bool {
|
||||||
return self.color != null;
|
return self.color != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if the given glyph ID is colorized.
|
/// Returns true if the given glyph ID is colorized.
|
||||||
pub fn isColored(self: *const Face, glyph_id: u16) bool {
|
pub fn isColorGlyph(self: *const Face, glyph_id: u32) bool {
|
||||||
const c = self.color orelse return false;
|
const c = self.color orelse return false;
|
||||||
return c.isColored(glyph_id);
|
return c.isColorGlyph(glyph_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the glyph index for the given Unicode code point. If this
|
/// Returns the glyph index for the given Unicode code point. If this
|
||||||
@ -328,7 +310,7 @@ pub const Face = struct {
|
|||||||
depth: u32,
|
depth: u32,
|
||||||
space: *macos.graphics.ColorSpace,
|
space: *macos.graphics.ColorSpace,
|
||||||
context_opts: c_uint,
|
context_opts: c_uint,
|
||||||
} = if (self.presentation == .text) .{
|
} = if (!self.isColorGlyph(glyph_index)) .{
|
||||||
.color = false,
|
.color = false,
|
||||||
.depth = 1,
|
.depth = 1,
|
||||||
.space = try macos.graphics.ColorSpace.createDeviceGray(),
|
.space = try macos.graphics.ColorSpace.createDeviceGray(),
|
||||||
@ -669,13 +651,18 @@ const ColorState = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if the given glyph ID is colored.
|
/// Returns true if the given glyph ID is colored.
|
||||||
pub fn isColored(self: *const ColorState, glyph_id: u16) bool {
|
pub fn isColorGlyph(self: *const ColorState, glyph_id: u32) bool {
|
||||||
|
// Our font system uses 32-bit glyph IDs for special values but
|
||||||
|
// actual fonts only contain 16-bit glyph IDs so if we can't cast
|
||||||
|
// into it it must be false.
|
||||||
|
const glyph_u16 = std.math.cast(u16, glyph_id) orelse return false;
|
||||||
|
|
||||||
// sbix is always true for now
|
// sbix is always true for now
|
||||||
if (self.sbix) return true;
|
if (self.sbix) return true;
|
||||||
|
|
||||||
// if we have svg data, check it
|
// if we have svg data, check it
|
||||||
if (self.svg) |svg| {
|
if (self.svg) |svg| {
|
||||||
if (svg.hasGlyph(glyph_id)) return true;
|
if (svg.hasGlyph(glyph_u16)) return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -699,8 +686,6 @@ test {
|
|||||||
var face = try Face.initFontCopy(ct_font, .{ .size = .{ .points = 12 } });
|
var face = try Face.initFontCopy(ct_font, .{ .size = .{ .points = 12 } });
|
||||||
defer face.deinit();
|
defer face.deinit();
|
||||||
|
|
||||||
try testing.expectEqual(font.Presentation.text, face.presentation);
|
|
||||||
|
|
||||||
// Generate all visible ASCII
|
// Generate all visible ASCII
|
||||||
var i: u8 = 32;
|
var i: u8 = 32;
|
||||||
while (i < 127) : (i += 1) {
|
while (i < 127) : (i += 1) {
|
||||||
@ -722,8 +707,6 @@ test "name" {
|
|||||||
var face = try Face.initFontCopy(ct_font, .{ .size = .{ .points = 12 } });
|
var face = try Face.initFontCopy(ct_font, .{ .size = .{ .points = 12 } });
|
||||||
defer face.deinit();
|
defer face.deinit();
|
||||||
|
|
||||||
try testing.expectEqual(font.Presentation.text, face.presentation);
|
|
||||||
|
|
||||||
var buf: [1024]u8 = undefined;
|
var buf: [1024]u8 = undefined;
|
||||||
const font_name = try face.name(&buf);
|
const font_name = try face.name(&buf);
|
||||||
try testing.expect(std.mem.eql(u8, font_name, "Menlo"));
|
try testing.expect(std.mem.eql(u8, font_name, "Menlo"));
|
||||||
@ -742,13 +725,10 @@ test "emoji" {
|
|||||||
var face = try Face.initFontCopy(ct_font, .{ .size = .{ .points = 18 } });
|
var face = try Face.initFontCopy(ct_font, .{ .size = .{ .points = 18 } });
|
||||||
defer face.deinit();
|
defer face.deinit();
|
||||||
|
|
||||||
// Presentation
|
|
||||||
try testing.expectEqual(font.Presentation.emoji, face.presentation);
|
|
||||||
|
|
||||||
// Glyph index check
|
// Glyph index check
|
||||||
{
|
{
|
||||||
const id = face.glyphIndex('🥸').?;
|
const id = face.glyphIndex('🥸').?;
|
||||||
try testing.expect(face.isColored(id));
|
try testing.expect(face.isColorGlyph(id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -766,8 +746,6 @@ test "in-memory" {
|
|||||||
var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
|
var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
|
||||||
defer face.deinit();
|
defer face.deinit();
|
||||||
|
|
||||||
try testing.expectEqual(font.Presentation.text, face.presentation);
|
|
||||||
|
|
||||||
// Generate all visible ASCII
|
// Generate all visible ASCII
|
||||||
var i: u8 = 32;
|
var i: u8 = 32;
|
||||||
while (i < 127) : (i += 1) {
|
while (i < 127) : (i += 1) {
|
||||||
@ -790,8 +768,6 @@ test "variable" {
|
|||||||
var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
|
var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
|
||||||
defer face.deinit();
|
defer face.deinit();
|
||||||
|
|
||||||
try testing.expectEqual(font.Presentation.text, face.presentation);
|
|
||||||
|
|
||||||
// Generate all visible ASCII
|
// Generate all visible ASCII
|
||||||
var i: u8 = 32;
|
var i: u8 = 32;
|
||||||
while (i < 127) : (i += 1) {
|
while (i < 127) : (i += 1) {
|
||||||
@ -814,8 +790,6 @@ test "variable set variation" {
|
|||||||
var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
|
var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
|
||||||
defer face.deinit();
|
defer face.deinit();
|
||||||
|
|
||||||
try testing.expectEqual(font.Presentation.text, face.presentation);
|
|
||||||
|
|
||||||
try face.setVariations(&.{
|
try face.setVariations(&.{
|
||||||
.{ .id = font.face.Variation.Id.init("wght"), .value = 400 },
|
.{ .id = font.face.Variation.Id.init("wght"), .value = 400 },
|
||||||
}, .{ .size = .{ .points = 12 } });
|
}, .{ .size = .{ .points = 12 } });
|
||||||
@ -828,19 +802,6 @@ test "variable set variation" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test "mixed color/non-color font treated as text" {
|
|
||||||
const testing = std.testing;
|
|
||||||
const testFont = @import("../test.zig").fontJuliaMono;
|
|
||||||
|
|
||||||
var lib = try font.Library.init();
|
|
||||||
defer lib.deinit();
|
|
||||||
|
|
||||||
var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
|
|
||||||
defer face.deinit();
|
|
||||||
|
|
||||||
try testing.expect(face.presentation == .text);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "svg font table" {
|
test "svg font table" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
@ -871,12 +832,12 @@ test "glyphIndex colored vs text" {
|
|||||||
{
|
{
|
||||||
const glyph = face.glyphIndex('A').?;
|
const glyph = face.glyphIndex('A').?;
|
||||||
try testing.expectEqual(4, glyph);
|
try testing.expectEqual(4, glyph);
|
||||||
try testing.expect(!face.isColored(glyph));
|
try testing.expect(!face.isColorGlyph(glyph));
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const glyph = face.glyphIndex(0xE800).?;
|
const glyph = face.glyphIndex(0xE800).?;
|
||||||
try testing.expectEqual(11482, glyph);
|
try testing.expectEqual(11482, glyph);
|
||||||
try testing.expect(face.isColored(glyph));
|
try testing.expect(face.isColorGlyph(glyph));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user