mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-23 04:06:13 +03:00
font: web canvas face has to render to check presentation for cp
Since we have no way to detect our presentation (text/emoji), we need to actually render the glyph that is being requested to double-check that the glyph matches our supported presentation. We do this because the browser will render fallback fonts for a glyph if it can't find one in the named font.
This commit is contained in:
@ -286,12 +286,29 @@ pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool {
|
|||||||
|
|
||||||
// Canvas always has the codepoint because we have no way of
|
// Canvas always has the codepoint because we have no way of
|
||||||
// really checking and we let the browser handle it.
|
// really checking and we let the browser handle it.
|
||||||
.web_canvas => {
|
.web_canvas => if (self.wc) |wc| {
|
||||||
if (self.wc) |wc| {
|
// Fast-path if we have a specific presentation and we
|
||||||
if (p) |desired| if (wc.presentation != desired) return false;
|
// don't match, then it is definitely not this face.
|
||||||
}
|
if (p) |desired| if (wc.presentation != desired) return false;
|
||||||
|
|
||||||
return true;
|
// Slow-path: we initialize the font, render it, and check
|
||||||
|
// if it works and the presentation matches.
|
||||||
|
var face = Face.initNamed(
|
||||||
|
wc.alloc,
|
||||||
|
wc.font_str,
|
||||||
|
.{ .points = 12 },
|
||||||
|
wc.presentation,
|
||||||
|
) catch |err| {
|
||||||
|
log.warn("failed to init face for codepoint check " ++
|
||||||
|
"face={s} err={}", .{
|
||||||
|
wc.font_str,
|
||||||
|
err,
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
defer face.deinit();
|
||||||
|
return face.glyphIndex(cp) != null;
|
||||||
},
|
},
|
||||||
|
|
||||||
.freetype => {},
|
.freetype => {},
|
||||||
|
@ -90,12 +90,50 @@ pub const Face = struct {
|
|||||||
/// have access to the underlying tables anyways. We let the browser deal
|
/// have access to the underlying tables anyways. We let the browser deal
|
||||||
/// with bad codepoints.
|
/// with bad codepoints.
|
||||||
pub fn glyphIndex(self: Face, cp: u32) ?u32 {
|
pub fn glyphIndex(self: Face, cp: u32) ?u32 {
|
||||||
_ = self;
|
// Render the glyph to determine if it is colored or not. We
|
||||||
|
// have to do this because the browser will always try to render
|
||||||
|
// whatever we give it and we have no API to determine color.
|
||||||
|
//
|
||||||
|
// We don't want to say yes to the wrong presentation because
|
||||||
|
// it will go into the wrong Atlas.
|
||||||
|
const p: font.Presentation = if (cp <= 255) .text else p: {
|
||||||
|
break :p self.glyphPresentation(cp) catch {
|
||||||
|
// In this case, we assume we are unable to render
|
||||||
|
// this glyph and therefore jus say we don't support it.
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
if (p != self.presentation) return null;
|
||||||
|
|
||||||
return cp;
|
return cp;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render a glyph using the glyph index. The rendered glyph is stored in the
|
/// This determines the presentation of the glyph by literally
|
||||||
/// given texture atlas.
|
/// inspecting the image data to look for any color. This isn't
|
||||||
|
/// super performant but we don't have a better choice given the
|
||||||
|
/// canvas APIs.
|
||||||
|
fn glyphPresentation(
|
||||||
|
self: Face,
|
||||||
|
cp: u32,
|
||||||
|
) !font.Presentation {
|
||||||
|
// Render the glyph
|
||||||
|
var render = try self.renderGlyphInternal(self.alloc, cp);
|
||||||
|
defer render.deinit();
|
||||||
|
|
||||||
|
// Inspect the image data for any non-zeros in the RGB value.
|
||||||
|
// NOTE(perf): this is an easy candidate for SIMD.
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < render.bitmap.len) : (i += 4) {
|
||||||
|
if (render.bitmap[i] > 0 or
|
||||||
|
render.bitmap[i + 1] > 0 or
|
||||||
|
render.bitmap[i + 2] > 0) return .emoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
return .text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render a glyph using the glyph index. The rendered glyph is stored
|
||||||
|
/// in the given texture atlas.
|
||||||
pub fn renderGlyph(
|
pub fn renderGlyph(
|
||||||
self: Face,
|
self: Face,
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
@ -105,7 +143,159 @@ pub const Face = struct {
|
|||||||
) !font.Glyph {
|
) !font.Glyph {
|
||||||
_ = max_height;
|
_ = max_height;
|
||||||
|
|
||||||
// Encode our glyph into UTF-8 so we can build a JS string out of it.
|
var render = try self.renderGlyphInternal(alloc, glyph_index);
|
||||||
|
defer render.deinit();
|
||||||
|
|
||||||
|
// Convert the format of the bitmap if necessary
|
||||||
|
const bitmap_formatted: []u8 = switch (atlas.format) {
|
||||||
|
// Bitmap is already in RGBA
|
||||||
|
.rgba => render.bitmap,
|
||||||
|
|
||||||
|
// Convert down to A8
|
||||||
|
.greyscale => a8: {
|
||||||
|
assert(@mod(render.bitmap.len, 4) == 0);
|
||||||
|
var bitmap_a8 = try alloc.alloc(u8, render.bitmap.len / 4);
|
||||||
|
errdefer alloc.free(bitmap_a8);
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < bitmap_a8.len) : (i += 1) {
|
||||||
|
bitmap_a8[i] = render.bitmap[(i * 4) + 3];
|
||||||
|
}
|
||||||
|
|
||||||
|
break :a8 bitmap_a8;
|
||||||
|
},
|
||||||
|
|
||||||
|
else => return error.UnsupportedAtlasFormat,
|
||||||
|
};
|
||||||
|
defer if (bitmap_formatted.ptr != render.bitmap.ptr) {
|
||||||
|
alloc.free(bitmap_formatted);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Put it in our atlas
|
||||||
|
const region = try atlas.reserve(alloc, render.width, render.height);
|
||||||
|
if (region.width > 0 and region.height > 0) {
|
||||||
|
atlas.set(region, bitmap_formatted);
|
||||||
|
}
|
||||||
|
|
||||||
|
return font.Glyph{
|
||||||
|
.width = render.width,
|
||||||
|
.height = render.height,
|
||||||
|
// TODO: this can't be right
|
||||||
|
.offset_x = 0,
|
||||||
|
.offset_y = 0,
|
||||||
|
.atlas_x = region.x,
|
||||||
|
.atlas_y = region.y,
|
||||||
|
.advance_x = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate the metrics associated with a given face.
|
||||||
|
fn calcMetrics(self: *Face) !void {
|
||||||
|
const ctx = try self.context();
|
||||||
|
defer ctx.deinit();
|
||||||
|
|
||||||
|
// Cell width is the width of our M text
|
||||||
|
const cell_width: f32 = cell_width: {
|
||||||
|
const metrics = try ctx.call(js.Object, "measureText", .{js.string("M")});
|
||||||
|
defer metrics.deinit();
|
||||||
|
|
||||||
|
// We prefer the bounding box since it is tighter but certain
|
||||||
|
// text such as emoji do not have a bounding box set so we use
|
||||||
|
// the full run width instead.
|
||||||
|
const bounding_right = try metrics.get(f32, "actualBoundingBoxRight");
|
||||||
|
if (bounding_right > 0) break :cell_width bounding_right;
|
||||||
|
break :cell_width try metrics.get(f32, "width");
|
||||||
|
};
|
||||||
|
|
||||||
|
// To get the cell height we render a high and low character and get
|
||||||
|
// the total of the ascent and descent. This should equal our
|
||||||
|
// pixel height but this is a more surefire way to get it.
|
||||||
|
const height_metrics = try ctx.call(js.Object, "measureText", .{js.string("M_")});
|
||||||
|
defer height_metrics.deinit();
|
||||||
|
const asc = try height_metrics.get(f32, "actualBoundingBoxAscent");
|
||||||
|
const desc = try height_metrics.get(f32, "actualBoundingBoxDescent");
|
||||||
|
const cell_height = asc + desc;
|
||||||
|
const cell_baseline = desc;
|
||||||
|
|
||||||
|
// There isn't a declared underline position for canvas measurements
|
||||||
|
// so we just go 1 under the cell height to match freetype logic
|
||||||
|
// at this time (our freetype logic).
|
||||||
|
const underline_position = cell_height - 1;
|
||||||
|
const underline_thickness: f32 = 1;
|
||||||
|
|
||||||
|
self.metrics = .{
|
||||||
|
.cell_width = cell_width,
|
||||||
|
.cell_height = cell_height,
|
||||||
|
.cell_baseline = cell_baseline,
|
||||||
|
.underline_position = underline_position,
|
||||||
|
.underline_thickness = underline_thickness,
|
||||||
|
.strikethrough_position = underline_position,
|
||||||
|
.strikethrough_thickness = underline_thickness,
|
||||||
|
};
|
||||||
|
|
||||||
|
log.debug("metrics font={s} value={}", .{ self.font_str, self.metrics });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the 2d context configured for drawing
|
||||||
|
fn context(self: Face) !js.Object {
|
||||||
|
// This will return the same context on subsequent calls so it
|
||||||
|
// is important to reset it.
|
||||||
|
const ctx = try self.canvas.call(js.Object, "getContext", .{js.string("2d")});
|
||||||
|
errdefer ctx.deinit();
|
||||||
|
|
||||||
|
// Clear the canvas
|
||||||
|
{
|
||||||
|
const width = try self.canvas.get(f64, "width");
|
||||||
|
const height = try self.canvas.get(f64, "height");
|
||||||
|
try ctx.call(void, "clearRect", .{ 0, 0, width, height });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set our context font
|
||||||
|
var font_val = try std.fmt.allocPrint(
|
||||||
|
self.alloc,
|
||||||
|
"{d}px {s}",
|
||||||
|
.{ self.size.points, self.font_str },
|
||||||
|
);
|
||||||
|
defer self.alloc.free(font_val);
|
||||||
|
try ctx.set("font", js.string(font_val));
|
||||||
|
|
||||||
|
// If the font property didn't change, then the font set didn't work.
|
||||||
|
// We do this check because it is very easy to put an invalid font
|
||||||
|
// in and this at least makes it show up in the logs.
|
||||||
|
const check = try ctx.getAlloc(js.String, self.alloc, "font");
|
||||||
|
defer self.alloc.free(check);
|
||||||
|
if (!std.mem.eql(u8, font_val, check)) {
|
||||||
|
log.warn("canvas font didn't set, fonts may be broken, expected={s} got={s}", .{
|
||||||
|
font_val,
|
||||||
|
check,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An internal (web-canvas-only) format for rendered glyphs
|
||||||
|
/// since we do render passes in multiple different situations.
|
||||||
|
const RenderedGlyph = struct {
|
||||||
|
alloc: Allocator,
|
||||||
|
metrics: js.Object,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
bitmap: []u8,
|
||||||
|
|
||||||
|
pub fn deinit(self: *RenderedGlyph) void {
|
||||||
|
self.metrics.deinit();
|
||||||
|
self.alloc.free(self.bitmap);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Shared logic for rendering a glyph.
|
||||||
|
fn renderGlyphInternal(
|
||||||
|
self: Face,
|
||||||
|
alloc: Allocator,
|
||||||
|
glyph_index: u32,
|
||||||
|
) !RenderedGlyph {
|
||||||
|
// Encode our glyph to UTF-8 so we can build a JS string out of it.
|
||||||
var utf8: [4]u8 = undefined;
|
var utf8: [4]u8 = undefined;
|
||||||
const utf8_len = try std.unicode.utf8Encode(@intCast(u21, glyph_index), &utf8);
|
const utf8_len = try std.unicode.utf8Encode(@intCast(u21, glyph_index), &utf8);
|
||||||
const glyph_str = js.string(utf8[0..utf8_len]);
|
const glyph_str = js.string(utf8[0..utf8_len]);
|
||||||
@ -116,7 +306,7 @@ pub const Face = struct {
|
|||||||
|
|
||||||
// Get the width and height of the render
|
// Get the width and height of the render
|
||||||
const metrics = try measure_ctx.call(js.Object, "measureText", .{glyph_str});
|
const metrics = try measure_ctx.call(js.Object, "measureText", .{glyph_str});
|
||||||
defer metrics.deinit();
|
errdefer metrics.deinit();
|
||||||
const width: u32 = @floatToInt(u32, @ceil(width: {
|
const width: u32 = @floatToInt(u32, @ceil(width: {
|
||||||
// We prefer the bounding box since it is tighter but certain
|
// We prefer the bounding box since it is tighter but certain
|
||||||
// text such as emoji do not have a bounding box set so we use
|
// text such as emoji do not have a bounding box set so we use
|
||||||
@ -222,130 +412,16 @@ pub const Face = struct {
|
|||||||
|
|
||||||
break :bitmap bitmap;
|
break :bitmap bitmap;
|
||||||
};
|
};
|
||||||
defer alloc.free(bitmap);
|
errdefer alloc.free(bitmap);
|
||||||
|
|
||||||
// Convert the format of the bitmap if necessary
|
return RenderedGlyph{
|
||||||
const bitmap_formatted: []u8 = switch (atlas.format) {
|
.alloc = alloc,
|
||||||
// Bitmap is already in RGBA
|
.metrics = metrics,
|
||||||
.rgba => bitmap,
|
|
||||||
|
|
||||||
// Convert down to A8
|
|
||||||
.greyscale => a8: {
|
|
||||||
assert(@mod(bitmap.len, 4) == 0);
|
|
||||||
var bitmap_a8 = try alloc.alloc(u8, bitmap.len / 4);
|
|
||||||
errdefer alloc.free(bitmap_a8);
|
|
||||||
var i: usize = 0;
|
|
||||||
while (i < bitmap_a8.len) : (i += 1) {
|
|
||||||
bitmap_a8[i] = bitmap[(i * 4) + 3];
|
|
||||||
}
|
|
||||||
|
|
||||||
break :a8 bitmap_a8;
|
|
||||||
},
|
|
||||||
|
|
||||||
else => return error.UnsupportedAtlasFormat,
|
|
||||||
};
|
|
||||||
defer if (bitmap_formatted.ptr != bitmap.ptr) alloc.free(bitmap_formatted);
|
|
||||||
|
|
||||||
// Put it in our atlas
|
|
||||||
const region = try atlas.reserve(alloc, width, height);
|
|
||||||
if (region.width > 0 and region.height > 0) atlas.set(region, bitmap_formatted);
|
|
||||||
|
|
||||||
return font.Glyph{
|
|
||||||
.width = width,
|
.width = width,
|
||||||
.height = height,
|
.height = height,
|
||||||
// TODO: this can't be right
|
.bitmap = bitmap,
|
||||||
.offset_x = 0,
|
|
||||||
.offset_y = 0,
|
|
||||||
.atlas_x = region.x,
|
|
||||||
.atlas_y = region.y,
|
|
||||||
.advance_x = 0,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate the metrics associated with a given face.
|
|
||||||
fn calcMetrics(self: *Face) !void {
|
|
||||||
const ctx = try self.context();
|
|
||||||
defer ctx.deinit();
|
|
||||||
|
|
||||||
// Cell width is the width of our M text
|
|
||||||
const cell_width: f32 = cell_width: {
|
|
||||||
const metrics = try ctx.call(js.Object, "measureText", .{js.string("M")});
|
|
||||||
defer metrics.deinit();
|
|
||||||
|
|
||||||
// We prefer the bounding box since it is tighter but certain
|
|
||||||
// text such as emoji do not have a bounding box set so we use
|
|
||||||
// the full run width instead.
|
|
||||||
const bounding_right = try metrics.get(f32, "actualBoundingBoxRight");
|
|
||||||
if (bounding_right > 0) break :cell_width bounding_right;
|
|
||||||
break :cell_width try metrics.get(f32, "width");
|
|
||||||
};
|
|
||||||
|
|
||||||
// To get the cell height we render a high and low character and get
|
|
||||||
// the total of the ascent and descent. This should equal our
|
|
||||||
// pixel height but this is a more surefire way to get it.
|
|
||||||
const height_metrics = try ctx.call(js.Object, "measureText", .{js.string("M_")});
|
|
||||||
defer height_metrics.deinit();
|
|
||||||
const asc = try height_metrics.get(f32, "actualBoundingBoxAscent");
|
|
||||||
const desc = try height_metrics.get(f32, "actualBoundingBoxDescent");
|
|
||||||
const cell_height = asc + desc;
|
|
||||||
const cell_baseline = desc;
|
|
||||||
|
|
||||||
// There isn't a declared underline position for canvas measurements
|
|
||||||
// so we just go 1 under the cell height to match freetype logic
|
|
||||||
// at this time (our freetype logic).
|
|
||||||
const underline_position = cell_height - 1;
|
|
||||||
const underline_thickness: f32 = 1;
|
|
||||||
|
|
||||||
self.metrics = .{
|
|
||||||
.cell_width = cell_width,
|
|
||||||
.cell_height = cell_height,
|
|
||||||
.cell_baseline = cell_baseline,
|
|
||||||
.underline_position = underline_position,
|
|
||||||
.underline_thickness = underline_thickness,
|
|
||||||
.strikethrough_position = underline_position,
|
|
||||||
.strikethrough_thickness = underline_thickness,
|
|
||||||
};
|
|
||||||
|
|
||||||
log.debug("metrics font={s} value={}", .{ self.font_str, self.metrics });
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the 2d context configured for drawing
|
|
||||||
fn context(self: Face) !js.Object {
|
|
||||||
// This will return the same context on subsequent calls so it
|
|
||||||
// is important to reset it.
|
|
||||||
const ctx = try self.canvas.call(js.Object, "getContext", .{js.string("2d")});
|
|
||||||
errdefer ctx.deinit();
|
|
||||||
|
|
||||||
// Clear the canvas
|
|
||||||
{
|
|
||||||
const width = try self.canvas.get(f64, "width");
|
|
||||||
const height = try self.canvas.get(f64, "height");
|
|
||||||
try ctx.call(void, "clearRect", .{ 0, 0, width, height });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set our context font
|
|
||||||
var font_val = try std.fmt.allocPrint(
|
|
||||||
self.alloc,
|
|
||||||
"{d}px {s}",
|
|
||||||
.{ self.size.points, self.font_str },
|
|
||||||
);
|
|
||||||
defer self.alloc.free(font_val);
|
|
||||||
try ctx.set("font", js.string(font_val));
|
|
||||||
|
|
||||||
// If the font property didn't change, then the font set didn't work.
|
|
||||||
// We do this check because it is very easy to put an invalid font
|
|
||||||
// in and this at least makes it show up in the logs.
|
|
||||||
const check = try ctx.getAlloc(js.String, self.alloc, "font");
|
|
||||||
defer self.alloc.free(check);
|
|
||||||
if (!std.mem.eql(u8, font_val, check)) {
|
|
||||||
log.warn("canvas font didn't set, fonts may be broken, expected={s} got={s}", .{
|
|
||||||
font_val,
|
|
||||||
check,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The wasm-compatible API.
|
/// The wasm-compatible API.
|
||||||
|
@ -129,7 +129,7 @@ pub const Wasm = struct {
|
|||||||
|
|
||||||
var iter = self.runIterator(group, row);
|
var iter = self.runIterator(group, row);
|
||||||
while (try iter.next(alloc)) |run| {
|
while (try iter.next(alloc)) |run| {
|
||||||
log.info("y={} run={}", .{ y, run });
|
log.info("y={} run={d} idx={}", .{ y, run.cells, run.font_index });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user