mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
font: working on rendering glyphs in canvas
This commit is contained in:
@ -26,6 +26,10 @@ fetch(url.href).then(response =>
|
|||||||
free,
|
free,
|
||||||
face_new,
|
face_new,
|
||||||
face_free,
|
face_free,
|
||||||
|
face_render_glyph,
|
||||||
|
face_debug_canvas,
|
||||||
|
atlas_new,
|
||||||
|
atlas_free,
|
||||||
} = results.instance.exports;
|
} = results.instance.exports;
|
||||||
// Give us access to the zjs value for debugging.
|
// Give us access to the zjs value for debugging.
|
||||||
globalThis.zjs = zjs;
|
globalThis.zjs = zjs;
|
||||||
@ -34,16 +38,23 @@ fetch(url.href).then(response =>
|
|||||||
// Initialize our zig-js memory
|
// Initialize our zig-js memory
|
||||||
zjs.memory = memory;
|
zjs.memory = memory;
|
||||||
|
|
||||||
|
// Create our atlas
|
||||||
|
const atlas = atlas_new(512, 0 /* greyscale */);
|
||||||
|
|
||||||
// Create some memory for our string
|
// Create some memory for our string
|
||||||
const font = new TextEncoder().encode("monospace");
|
const font = new TextEncoder().encode("monospace");
|
||||||
const font_ptr = malloc(font.byteLength);
|
const font_ptr = malloc(font.byteLength);
|
||||||
try {
|
|
||||||
new Uint8Array(memory.buffer, font_ptr).set(font);
|
new Uint8Array(memory.buffer, font_ptr).set(font);
|
||||||
|
|
||||||
// Call whatever example you want:
|
// Call whatever example you want:
|
||||||
const face = face_new(font_ptr, font.byteLength, 14);
|
const face = face_new(font_ptr, font.byteLength, 144);
|
||||||
|
free(font_ptr);
|
||||||
|
|
||||||
|
// Render a glyph
|
||||||
|
face_render_glyph(face, atlas, "A".codePointAt(0));
|
||||||
|
|
||||||
|
// Debug our canvas
|
||||||
|
face_debug_canvas(face);
|
||||||
|
|
||||||
//face_free(face);
|
//face_free(face);
|
||||||
} finally {
|
|
||||||
free(font_ptr);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
@ -6,6 +6,8 @@
|
|||||||
<script type="module" src="app.ts"></script>
|
<script type="module" src="app.ts"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
Open your console, we are just debugging here.
|
<p>Open your console, we are just debugging here.</p>
|
||||||
|
<p>The font rendering canvas should show below. This shows a single glyph.</p>
|
||||||
|
<div id="face-canvas" style="display: inline-block; border: 1px solid red;"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -113,7 +113,7 @@ pub const Face = struct {
|
|||||||
|
|
||||||
/// Returns true if this font is colored. This can be used by callers to
|
/// Returns true if this font is colored. This can be used by callers to
|
||||||
/// determine what kind of atlas to pass in.
|
/// determine what kind of atlas to pass in.
|
||||||
pub fn hasColor(self: Face) bool {
|
fn hasColor(self: Face) bool {
|
||||||
return self.face.hasColor();
|
return self.face.hasColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,41 +73,120 @@ pub const Face = struct {
|
|||||||
self.* = undefined;
|
self.* = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate the metrics associated with a given face.
|
/// Resize the font in-place. If this succeeds, the caller is responsible
|
||||||
fn calcMetrics(self: *Face) !void {
|
/// for clearing any glyph caches, font atlas data, etc.
|
||||||
// This will return the same context on subsequent calls so it
|
pub fn setSize(self: *Face, size: font.face.DesiredSize) !void {
|
||||||
// is important to reset it.
|
const old = self.size;
|
||||||
const ctx = try self.canvas.call(js.Object, "getContext", .{js.string("2d")});
|
self.size = size;
|
||||||
|
errdefer self.size = old;
|
||||||
|
try self.calcMetrics();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the glyph index for the given Unicode code point. For canvas,
|
||||||
|
/// we support every glyph and the ID is just the codepoint since we don't
|
||||||
|
/// have access to the underlying tables anyways. We let the browser deal
|
||||||
|
/// with bad codepoints.
|
||||||
|
pub fn glyphIndex(self: Face, cp: u32) ?u32 {
|
||||||
|
_ = self;
|
||||||
|
return cp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render a glyph using the glyph index. The rendered glyph is stored in the
|
||||||
|
/// given texture atlas.
|
||||||
|
pub fn renderGlyph(
|
||||||
|
self: Face,
|
||||||
|
alloc: Allocator,
|
||||||
|
atlas: *font.Atlas,
|
||||||
|
glyph_index: u32,
|
||||||
|
max_height: ?u16,
|
||||||
|
) !font.Glyph {
|
||||||
|
// Encode our glyph into UTF-8 so we can build a JS string out of it.
|
||||||
|
var utf8: [4]u8 = undefined;
|
||||||
|
const utf8_len = try std.unicode.utf8Encode(@intCast(u21, glyph_index), &utf8);
|
||||||
|
const glyph_str = js.string(utf8[0..utf8_len]);
|
||||||
|
|
||||||
|
// Get our drawing context
|
||||||
|
const measure_ctx = try self.context();
|
||||||
|
defer measure_ctx.deinit();
|
||||||
|
|
||||||
|
// Get the width and height of the render
|
||||||
|
const metrics = try measure_ctx.call(js.Object, "measureText", .{glyph_str});
|
||||||
|
defer metrics.deinit();
|
||||||
|
const width: u32 = @floatToInt(u32, @ceil(width: {
|
||||||
|
// 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 :width bounding_right;
|
||||||
|
break :width try metrics.get(f32, "width");
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Height is our ascender + descender for this char
|
||||||
|
const asc = try metrics.get(f32, "actualBoundingBoxAscent");
|
||||||
|
const desc = try metrics.get(f32, "actualBoundingBoxDescent");
|
||||||
|
const height = @floatToInt(u32, @ceil(asc + desc));
|
||||||
|
|
||||||
|
// Resize canvas to match the glyph size exactly
|
||||||
|
{
|
||||||
|
try self.canvas.set("width", width);
|
||||||
|
try self.canvas.set("height", height);
|
||||||
|
|
||||||
|
const width_str = try std.fmt.allocPrint(alloc, "{d}px", .{width});
|
||||||
|
defer alloc.free(width_str);
|
||||||
|
const height_str = try std.fmt.allocPrint(alloc, "{d}px", .{height});
|
||||||
|
defer alloc.free(height_str);
|
||||||
|
|
||||||
|
const style = try self.canvas.get(js.Object, "style");
|
||||||
|
defer style.deinit();
|
||||||
|
try style.set("width", js.string(width_str));
|
||||||
|
try style.set("height", js.string(height_str));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload our context since we resized the canvas
|
||||||
|
const ctx = try self.context();
|
||||||
defer ctx.deinit();
|
defer ctx.deinit();
|
||||||
|
|
||||||
// Set our context font
|
// Set our alignment different since we want it centered exactly
|
||||||
var font_val = try std.fmt.allocPrint(
|
try ctx.set("textBaseline", js.string("top"));
|
||||||
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.
|
// Draw background
|
||||||
// We do this check because it is very easy to put an invalid font
|
try ctx.set("fillStyle", js.string("transparent"));
|
||||||
// in and this at least makes it show up in the logs.
|
try ctx.call(void, "fillRect", .{
|
||||||
{
|
@as(u32, 0),
|
||||||
const check = try ctx.getAlloc(js.String, self.alloc, "font");
|
@as(u32, 0),
|
||||||
defer self.alloc.free(check);
|
width,
|
||||||
if (!std.mem.eql(u8, font_val, check)) {
|
height,
|
||||||
log.warn("canvas font didn't set, fonts may be broken, expected={s} got={s}", .{
|
});
|
||||||
font_val,
|
|
||||||
check,
|
// Draw glyph
|
||||||
});
|
try ctx.set("fillStyle", js.string("black"));
|
||||||
}
|
try ctx.call(void, "fillText", .{
|
||||||
}
|
glyph_str,
|
||||||
|
width / 2,
|
||||||
|
height / 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
_ = atlas;
|
||||||
|
_ = max_height;
|
||||||
|
return error.Unimplemented;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
// Cell width is the width of our M text
|
||||||
const cell_width: f32 = cell_width: {
|
const cell_width: f32 = cell_width: {
|
||||||
const metrics = try ctx.call(js.Object, "measureText", .{js.string("M")});
|
const metrics = try ctx.call(js.Object, "measureText", .{js.string("M")});
|
||||||
defer metrics.deinit();
|
defer metrics.deinit();
|
||||||
break :cell_width try metrics.get(f32, "actualBoundingBoxRight");
|
|
||||||
|
// 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
|
// To get the cell height we render a high and low character and get
|
||||||
@ -136,7 +215,45 @@ pub const Face = struct {
|
|||||||
.strikethrough_thickness = underline_thickness,
|
.strikethrough_thickness = underline_thickness,
|
||||||
};
|
};
|
||||||
|
|
||||||
log.debug("metrics font={s} value={}", .{ font_val, self.metrics });
|
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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -165,4 +282,46 @@ pub const Wasm = struct {
|
|||||||
alloc.destroy(v);
|
alloc.destroy(v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resulting pointer must be freed using the global "free".
|
||||||
|
export fn face_render_glyph(
|
||||||
|
face: *Face,
|
||||||
|
atlas: *font.Atlas,
|
||||||
|
codepoint: u32,
|
||||||
|
) ?*font.Glyph {
|
||||||
|
return face_render_glyph_(face, atlas, codepoint) catch |err| {
|
||||||
|
log.warn("error rendering glyph err={}", .{err});
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn face_debug_canvas(face: *Face) void {
|
||||||
|
face_debug_canvas_(face) catch |err| {
|
||||||
|
log.warn("error adding debug canvas err={}", .{err});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn face_debug_canvas_(face: *Face) !void {
|
||||||
|
const doc = try js.global.get(js.Object, "document");
|
||||||
|
defer doc.deinit();
|
||||||
|
|
||||||
|
const elem = try doc.call(
|
||||||
|
?js.Object,
|
||||||
|
"getElementById",
|
||||||
|
.{js.string("face-canvas")},
|
||||||
|
) orelse return error.CanvasContainerNotFound;
|
||||||
|
defer elem.deinit();
|
||||||
|
|
||||||
|
try elem.call(void, "append", .{face.canvas});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn face_render_glyph_(face: *Face, atlas: *font.Atlas, codepoint: u32) !*font.Glyph {
|
||||||
|
const glyph = try face.renderGlyph(alloc, atlas, codepoint, null);
|
||||||
|
|
||||||
|
const result = try alloc.create(font.Glyph);
|
||||||
|
errdefer alloc.destroy(result);
|
||||||
|
_ = try wasm.toHostOwned(result);
|
||||||
|
result.* = glyph;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
2
vendor/zig-js
vendored
2
vendor/zig-js
vendored
@ -1 +1 @@
|
|||||||
Subproject commit a70f5da4f51b643be47fa39ba697dcd9444204c8
|
Subproject commit 3aebb1cbee374025368e223ef23b0c488ba612eb
|
Reference in New Issue
Block a user