mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 08:46:08 +03:00
Merge pull request #60 from mitchellh/font-canvas
wasm: Canvas-based Rasterizer
This commit is contained in:
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -34,3 +34,6 @@
|
||||
[submodule "vendor/pixman"]
|
||||
path = vendor/pixman
|
||||
url = https://github.com/freedesktop/pixman.git
|
||||
[submodule "vendor/zig-js"]
|
||||
path = vendor/zig-js
|
||||
url = https://github.com/mitchellh/zig-js.git
|
||||
|
10
build.zig
10
build.zig
@ -7,6 +7,7 @@ const fontconfig = @import("pkg/fontconfig/build.zig");
|
||||
const freetype = @import("pkg/freetype/build.zig");
|
||||
const harfbuzz = @import("pkg/harfbuzz/build.zig");
|
||||
const imgui = @import("pkg/imgui/build.zig");
|
||||
const js = @import("vendor/zig-js/build.zig");
|
||||
const libxml2 = @import("vendor/zig-libxml2/libxml2.zig");
|
||||
const libuv = @import("pkg/libuv/build.zig");
|
||||
const libpng = @import("pkg/libpng/build.zig");
|
||||
@ -120,7 +121,10 @@ pub fn build(b: *std.build.Builder) !void {
|
||||
wasm.setBuildMode(mode);
|
||||
wasm.setOutputDir("zig-out");
|
||||
|
||||
wasm.addOptions("build_options", exe_options);
|
||||
|
||||
// Wasm-specific deps
|
||||
wasm.addPackage(js.pkg);
|
||||
wasm.addPackage(tracylib.pkg);
|
||||
wasm.addPackage(utf8proc.pkg);
|
||||
_ = try utf8proc.link(b, wasm);
|
||||
@ -135,6 +139,7 @@ pub fn build(b: *std.build.Builder) !void {
|
||||
const main_test = b.addTest("src/main_wasm.zig");
|
||||
main_test.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .wasi });
|
||||
main_test.addOptions("build_options", exe_options);
|
||||
main_test.addPackage(js.pkg);
|
||||
test_step.dependOn(&main_test.step);
|
||||
}
|
||||
|
||||
@ -235,6 +240,11 @@ fn addDeps(
|
||||
_ = try macos.link(b, step, .{});
|
||||
}
|
||||
|
||||
// Wasm
|
||||
if (step.target.getCpuArch() == .wasm32) {
|
||||
step.addPackage(js.pkg);
|
||||
}
|
||||
|
||||
// We always statically compile glad
|
||||
step.addIncludePath("vendor/glad/include/");
|
||||
step.addCSourceFile("vendor/glad/src/gl.c", &.{});
|
||||
|
4
example/.gitignore
vendored
Normal file
4
example/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
.parcel-cache/
|
||||
dist/
|
||||
node_modules/
|
||||
example.wasm*
|
69
example/app.ts
Normal file
69
example/app.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { ZigJS } from 'zig-js';
|
||||
|
||||
const zjs = new ZigJS();
|
||||
const importObject = {
|
||||
module: {},
|
||||
env: {
|
||||
log: (ptr: number, len: number) => {
|
||||
const view = new DataView(zjs.memory.buffer, ptr, Number(len));
|
||||
const str = new TextDecoder('utf-8').decode(view);
|
||||
console.log(str);
|
||||
},
|
||||
},
|
||||
|
||||
...zjs.importObject(),
|
||||
};
|
||||
|
||||
const url = new URL('ghostty-wasm.wasm', import.meta.url);
|
||||
fetch(url.href).then(response =>
|
||||
response.arrayBuffer()
|
||||
).then(bytes =>
|
||||
WebAssembly.instantiate(bytes, importObject)
|
||||
).then(results => {
|
||||
const {
|
||||
memory,
|
||||
malloc,
|
||||
free,
|
||||
face_new,
|
||||
face_free,
|
||||
face_render_glyph,
|
||||
face_debug_canvas,
|
||||
atlas_new,
|
||||
atlas_free,
|
||||
atlas_debug_canvas,
|
||||
} = results.instance.exports;
|
||||
// Give us access to the zjs value for debugging.
|
||||
globalThis.zjs = zjs;
|
||||
console.log(zjs);
|
||||
|
||||
// Initialize our zig-js memory
|
||||
zjs.memory = memory;
|
||||
|
||||
// Create our atlas
|
||||
const atlas = atlas_new(512, 0 /* greyscale */);
|
||||
|
||||
// Create some memory for our string
|
||||
const font = new TextEncoder().encode("monospace");
|
||||
const font_ptr = malloc(font.byteLength);
|
||||
new Uint8Array(memory.buffer, font_ptr).set(font);
|
||||
|
||||
// Initialize our font face
|
||||
const face = face_new(font_ptr, font.byteLength, 72 /* size in px */);
|
||||
free(font_ptr);
|
||||
|
||||
// Render a glyph
|
||||
for (let i = 33; i <= 126; i++) {
|
||||
face_render_glyph(face, atlas, i);
|
||||
}
|
||||
//face_render_glyph(face, atlas, "橋".codePointAt(0));
|
||||
//face_render_glyph(face, atlas, "p".codePointAt(0));
|
||||
|
||||
// Debug our canvas
|
||||
face_debug_canvas(face);
|
||||
|
||||
// Debug our atlas canvas
|
||||
const id = atlas_debug_canvas(atlas);
|
||||
document.getElementById("atlas-canvas").append(zjs.deleteValue(id));
|
||||
|
||||
//face_free(face);
|
||||
});
|
@ -1,36 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>WASM Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
const importObject = {
|
||||
module: {},
|
||||
env: {
|
||||
// memory: new WebAssembly.Memory({ initial: 256 }),
|
||||
}
|
||||
};
|
||||
|
||||
fetch('ghostty-wasm.wasm').then(response =>
|
||||
response.arrayBuffer()
|
||||
).then(bytes =>
|
||||
WebAssembly.instantiate(bytes, importObject)
|
||||
).then(results => {
|
||||
const {
|
||||
atlas_new,
|
||||
atlas_free,
|
||||
atlas_reserve,
|
||||
free,
|
||||
memory,
|
||||
} = results.instance.exports;
|
||||
|
||||
const atlas = atlas_new(512, 0);
|
||||
const reg = atlas_reserve(atlas, 10, 10);
|
||||
free(reg);
|
||||
atlas_free(atlas);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>Ghostty Example</title>
|
||||
<script type="module" src="app.ts"></script>
|
||||
</head>
|
||||
<body>
|
||||
<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><div id="face-canvas" style="display: inline-block; border: 1px solid red;"></div></div>
|
||||
<p>The current font atlas is rendered below.</p>
|
||||
<div><div id="atlas-canvas" style="display: inline-block; border: 1px solid green;"></div></div>
|
||||
</body>
|
||||
</html>
|
||||
|
4436
example/package-lock.json
generated
Normal file
4436
example/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
example/package.json
Normal file
22
example/package.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "ghostty example",
|
||||
"version": "0.1.0",
|
||||
"description": "Example showing ghostty and wasm.",
|
||||
"source": "index.html",
|
||||
"browserslist": "> 0.5%, last 2 versions, not dead",
|
||||
"scripts": {
|
||||
"start": "parcel",
|
||||
"build": "parcel build",
|
||||
"check": "tsc --noEmit"
|
||||
},
|
||||
"author": "Mitchell Hashimoto",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@parcel/transformer-inline-string": "^2.8.0",
|
||||
"parcel": "^2.8.0",
|
||||
"typescript": "^4.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"zig-js": "file:../vendor/zig-js/js"
|
||||
}
|
||||
}
|
@ -21,6 +21,8 @@ const Allocator = std.mem.Allocator;
|
||||
const testing = std.testing;
|
||||
const fastmem = @import("../fastmem.zig");
|
||||
|
||||
const log = std.log.scoped(.atlas);
|
||||
|
||||
/// Data is the raw texture data.
|
||||
data: []u8,
|
||||
|
||||
@ -307,8 +309,9 @@ pub fn clear(self: *Atlas) void {
|
||||
pub const Wasm = struct {
|
||||
// If you're copying this file (Atlas.zig) out to a separate project,
|
||||
// just replace this with the allocator you want to use.
|
||||
const wasm = @import("../wasm.zig");
|
||||
const wasm = @import("../os/wasm.zig");
|
||||
const alloc = wasm.alloc;
|
||||
const js = @import("zig-js");
|
||||
|
||||
export fn atlas_new(size: u32, format: u8) ?*Atlas {
|
||||
const atlas = init(
|
||||
@ -321,6 +324,13 @@ pub const Wasm = struct {
|
||||
return result;
|
||||
}
|
||||
|
||||
export fn atlas_free(ptr: ?*Atlas) void {
|
||||
if (ptr) |v| {
|
||||
v.deinit(alloc);
|
||||
alloc.destroy(v);
|
||||
}
|
||||
}
|
||||
|
||||
/// The return value for this should be freed by the caller with "free".
|
||||
export fn atlas_reserve(self: *Atlas, width: u32, height: u32) ?*Region {
|
||||
return atlas_reserve_(self, width, height) catch return null;
|
||||
@ -348,11 +358,91 @@ pub const Wasm = struct {
|
||||
self.clear();
|
||||
}
|
||||
|
||||
export fn atlas_free(ptr: ?*Atlas) void {
|
||||
if (ptr) |v| {
|
||||
v.deinit(alloc);
|
||||
alloc.destroy(v);
|
||||
/// This creates a Canvas element identified by the id returned that
|
||||
/// the caller can draw into the DOM to visualize the atlas. The returned
|
||||
/// ID must be freed from the JS runtime by calling "zigjs.deleteValue".
|
||||
export fn atlas_debug_canvas(self: *Atlas) u32 {
|
||||
return atlas_debug_canvas_(self) catch |err| {
|
||||
log.warn("error dumping atlas canvas err={}", .{err});
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
|
||||
fn atlas_debug_canvas_(self: *Atlas) !u32 {
|
||||
// Create our canvas
|
||||
const doc = try js.global.get(js.Object, "document");
|
||||
defer doc.deinit();
|
||||
const canvas = try doc.call(js.Object, "createElement", .{js.string("canvas")});
|
||||
errdefer canvas.deinit();
|
||||
|
||||
// Setup our canvas size
|
||||
{
|
||||
try canvas.set("width", self.size);
|
||||
try canvas.set("height", self.size);
|
||||
|
||||
const width_str = try std.fmt.allocPrint(alloc, "{d}px", .{self.size});
|
||||
defer alloc.free(width_str);
|
||||
|
||||
const style = try canvas.get(js.Object, "style");
|
||||
defer style.deinit();
|
||||
try style.set("width", js.string(width_str));
|
||||
try style.set("height", js.string(width_str));
|
||||
}
|
||||
|
||||
// This will return the same context on subsequent calls so it
|
||||
// is important to reset it.
|
||||
const ctx = try canvas.call(js.Object, "getContext", .{js.string("2d")});
|
||||
defer ctx.deinit();
|
||||
|
||||
// We need to draw pixels so this is format dependent.
|
||||
var buf: []u8 = switch (self.format) {
|
||||
// RGBA is the native ImageData format
|
||||
.rgba => self.data,
|
||||
|
||||
.greyscale => buf: {
|
||||
// Convert from A8 to RGBA so every 4th byte is set to a value.
|
||||
var buf: []u8 = try alloc.alloc(u8, self.data.len * 4);
|
||||
errdefer alloc.free(buf);
|
||||
std.mem.set(u8, buf, 0);
|
||||
for (self.data) |value, i| {
|
||||
buf[(i * 4) + 3] = value;
|
||||
}
|
||||
break :buf buf;
|
||||
},
|
||||
|
||||
else => return error.UnsupportedAtlasFormat,
|
||||
};
|
||||
defer if (buf.ptr != self.data.ptr) alloc.free(buf);
|
||||
|
||||
// Create an ImageData from our buffer and then write it to the canvas
|
||||
const image_data: js.Object = data: {
|
||||
// Get our runtime memory
|
||||
const mem = try js.runtime.get(js.Object, "memory");
|
||||
defer mem.deinit();
|
||||
const mem_buf = try mem.get(js.Object, "buffer");
|
||||
defer mem_buf.deinit();
|
||||
|
||||
// Create an array that points to our buffer
|
||||
const Uint8ClampedArray = try js.global.get(js.Object, "Uint8ClampedArray");
|
||||
defer Uint8ClampedArray.deinit();
|
||||
const arr = try Uint8ClampedArray.new(.{ mem_buf, buf.ptr, buf.len });
|
||||
defer arr.deinit();
|
||||
|
||||
// Create the image data from our array
|
||||
const ImageData = try js.global.get(js.Object, "ImageData");
|
||||
defer ImageData.deinit();
|
||||
const data = try ImageData.new(.{ arr, self.size, self.size });
|
||||
errdefer data.deinit();
|
||||
|
||||
break :data data;
|
||||
};
|
||||
defer image_data.deinit();
|
||||
|
||||
// Draw it
|
||||
try ctx.call(void, "putImageData", .{ image_data, 0, 0 });
|
||||
|
||||
const id = @bitCast(js.Ref, @enumToInt(canvas.value)).id;
|
||||
return id;
|
||||
}
|
||||
|
||||
test "happy path" {
|
||||
|
@ -24,8 +24,8 @@ fc: if (options.backend == .fontconfig_freetype) ?Fontconfig else void =
|
||||
if (options.backend == .fontconfig_freetype) null else {},
|
||||
|
||||
/// CoreText
|
||||
ct: if (options.backend == .coretext) ?CoreText else void =
|
||||
if (options.backend == .coretext) null else {},
|
||||
ct: if (font.Discover == font.discovery.CoreText) ?CoreText else void =
|
||||
if (font.Discover == font.discovery.CoreText) null else {},
|
||||
|
||||
/// Fontconfig specific data. This is only present if building with fontconfig.
|
||||
pub const Fontconfig = struct {
|
||||
@ -66,7 +66,7 @@ pub fn deinit(self: *DeferredFace) void {
|
||||
if (self.face) |*face| face.deinit();
|
||||
switch (options.backend) {
|
||||
.fontconfig_freetype => if (self.fc) |*fc| fc.deinit(),
|
||||
.coretext => if (self.ct) |*ct| ct.deinit(),
|
||||
.coretext, .coretext_freetype => if (self.ct) |*ct| ct.deinit(),
|
||||
.freetype => {},
|
||||
// TODO
|
||||
.web_canvas => unreachable,
|
||||
@ -86,7 +86,7 @@ pub fn name(self: DeferredFace) ![:0]const u8 {
|
||||
.fontconfig_freetype => if (self.fc) |fc|
|
||||
return (try fc.pattern.get(.fullname, 0)).string,
|
||||
|
||||
.coretext => if (self.ct) |ct| {
|
||||
.coretext, .coretext_freetype => if (self.ct) |ct| {
|
||||
const display_name = ct.font.copyDisplayName();
|
||||
return display_name.cstringPtr(.utf8) orelse "<unsupported internal encoding>";
|
||||
},
|
||||
@ -116,14 +116,12 @@ pub fn load(
|
||||
},
|
||||
|
||||
.coretext => {
|
||||
// It is possible to use CoreText with Freetype so we support
|
||||
// both here.
|
||||
switch (font.Face) {
|
||||
@import("face/freetype.zig").Face => try self.loadCoreTextFreetype(lib, size),
|
||||
@import("face/coretext.zig").Face => try self.loadCoreText(lib, size),
|
||||
else => unreachable,
|
||||
}
|
||||
try self.loadCoreText(lib, size);
|
||||
return;
|
||||
},
|
||||
|
||||
.coretext_freetype => {
|
||||
try self.loadCoreTextFreetype(lib, size);
|
||||
return;
|
||||
},
|
||||
|
||||
@ -239,7 +237,7 @@ pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool {
|
||||
}
|
||||
},
|
||||
|
||||
.coretext => {
|
||||
.coretext, .coretext_freetype => {
|
||||
// If we are using coretext, we check the loaded CT font.
|
||||
if (self.ct) |ct| {
|
||||
// Turn UTF-32 into UTF-16 for CT API
|
||||
|
@ -285,6 +285,26 @@ pub fn renderGlyph(
|
||||
return try face.face.?.renderGlyph(alloc, atlas, glyph_index, max_height);
|
||||
}
|
||||
|
||||
/// The wasm-compatible API.
|
||||
pub const Wasm = struct {
|
||||
const wasm = @import("../os/wasm.zig");
|
||||
const alloc = wasm.alloc;
|
||||
|
||||
export fn group_new(pts: u16) ?*Group {
|
||||
return group_new_(pts) catch null;
|
||||
}
|
||||
|
||||
fn group_new_(pts: u16) !*Group {
|
||||
var group = try Group.init(alloc, .{}, .{ .points = pts });
|
||||
errdefer group.deinit();
|
||||
|
||||
var result = try alloc.create(Group);
|
||||
errdefer alloc.destroy(result);
|
||||
result.* = group;
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
test {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
@ -1,19 +0,0 @@
|
||||
//! A library represents the shared state that the underlying font
|
||||
//! library implementation(s) require per-process.
|
||||
//!
|
||||
//! In the future, this will be abstracted so that the underlying text
|
||||
//! engine might not be Freetype and may be something like Core Text,
|
||||
//! but the API will remain the same.
|
||||
const Library = @This();
|
||||
|
||||
const freetype = @import("freetype");
|
||||
|
||||
lib: freetype.Library,
|
||||
|
||||
pub fn init() freetype.Error!Library {
|
||||
return Library{ .lib = try freetype.Library.init() };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Library) void {
|
||||
self.lib.deinit();
|
||||
}
|
@ -2,15 +2,17 @@ const builtin = @import("builtin");
|
||||
const options = @import("main.zig").options;
|
||||
const freetype = @import("face/freetype.zig");
|
||||
const coretext = @import("face/coretext.zig");
|
||||
const web_canvas = @import("face/web_canvas.zig");
|
||||
pub const web_canvas = @import("face/web_canvas.zig");
|
||||
|
||||
/// Face implementation for the compile options.
|
||||
pub const Face = switch (options.backend) {
|
||||
.fontconfig_freetype => freetype.Face,
|
||||
.coretext => freetype.Face,
|
||||
//.coretext => coretext.Face,
|
||||
.freetype,
|
||||
.fontconfig_freetype,
|
||||
.coretext_freetype,
|
||||
=> freetype.Face,
|
||||
|
||||
.coretext => coretext.Face,
|
||||
.web_canvas => web_canvas.Face,
|
||||
else => unreachable,
|
||||
};
|
||||
|
||||
/// If a DPI can't be calculated, this DPI is used. This is probably
|
||||
|
@ -113,7 +113,7 @@ pub const Face = struct {
|
||||
|
||||
/// Returns true if this font is colored. This can be used by callers to
|
||||
/// determine what kind of atlas to pass in.
|
||||
pub fn hasColor(self: Face) bool {
|
||||
fn hasColor(self: Face) bool {
|
||||
return self.face.hasColor();
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,390 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const testing = std.testing;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const js = @import("zig-js");
|
||||
const font = @import("../main.zig");
|
||||
|
||||
const log = std.log.scoped(.font_face);
|
||||
|
||||
pub const Face = struct {
|
||||
// TODO
|
||||
/// The web canvas face makes use of an allocator when interacting
|
||||
/// with the JS environment.
|
||||
alloc: Allocator,
|
||||
|
||||
/// The CSS "font" attribute, excluding size.
|
||||
font_str: []const u8,
|
||||
|
||||
/// The size we currently have set.
|
||||
size: font.face.DesiredSize,
|
||||
|
||||
/// The presentation for this font. This is a heuristic since fonts don't have
|
||||
/// a way to declare this. We just assume a font with color is an emoji font.
|
||||
presentation: font.Presentation,
|
||||
|
||||
/// Metrics for this font face. These are useful for renderers.
|
||||
metrics: font.face.Metrics,
|
||||
|
||||
/// The canvas element that we will reuse to render glyphs
|
||||
canvas: js.Object,
|
||||
|
||||
/// Initialize a web canvas font with a "raw" value. The "raw" value can
|
||||
/// be any valid value for a CSS "font" property EXCLUDING the size. The
|
||||
/// size is always added via the `size` parameter.
|
||||
///
|
||||
/// The raw value is copied so the caller can free it after it is gone.
|
||||
pub fn initNamed(
|
||||
alloc: Allocator,
|
||||
raw: []const u8,
|
||||
size: font.face.DesiredSize,
|
||||
) !Face {
|
||||
// Copy our font string because we're going to have to reuse it.
|
||||
const font_str = try alloc.dupe(u8, raw);
|
||||
errdefer alloc.free(font_str);
|
||||
|
||||
// Create our canvas that we're going to continue to reuse.
|
||||
const doc = try js.global.get(js.Object, "document");
|
||||
defer doc.deinit();
|
||||
const canvas = try doc.call(js.Object, "createElement", .{js.string("canvas")});
|
||||
errdefer canvas.deinit();
|
||||
|
||||
var result = Face{
|
||||
.alloc = alloc,
|
||||
.font_str = font_str,
|
||||
.size = size,
|
||||
// TODO: figure out how we're going to do emoji with web canvas
|
||||
.presentation = .text,
|
||||
|
||||
.canvas = canvas,
|
||||
|
||||
// We're going to calculate these right after initialization.
|
||||
.metrics = undefined,
|
||||
};
|
||||
try result.calcMetrics();
|
||||
|
||||
log.debug("face initialized: {s}", .{raw});
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Face) void {
|
||||
self.alloc.free(self.font_str);
|
||||
self.canvas.deinit();
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
/// Resize the font in-place. If this succeeds, the caller is responsible
|
||||
/// for clearing any glyph caches, font atlas data, etc.
|
||||
pub fn setSize(self: *Face, size: font.face.DesiredSize) !void {
|
||||
const old = self.size;
|
||||
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 {
|
||||
_ = max_height;
|
||||
|
||||
// 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");
|
||||
})) + 1;
|
||||
|
||||
// Height is our ascender + descender for this char
|
||||
const asc = try metrics.get(f32, "actualBoundingBoxAscent");
|
||||
const desc = try metrics.get(f32, "actualBoundingBoxDescent");
|
||||
const left = try metrics.get(f32, "actualBoundingBoxLeft");
|
||||
const height = @floatToInt(u32, @ceil(asc + desc)) + 1;
|
||||
|
||||
// Note: width and height both get "+ 1" added to them above. This
|
||||
// is important so that there is a 1px border around the glyph to avoid
|
||||
// any clipping in the atlas.
|
||||
|
||||
// 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();
|
||||
|
||||
// Draw background
|
||||
try ctx.set("fillStyle", js.string("transparent"));
|
||||
try ctx.call(void, "fillRect", .{
|
||||
@as(u32, 0),
|
||||
@as(u32, 0),
|
||||
width,
|
||||
height,
|
||||
});
|
||||
|
||||
// Draw glyph
|
||||
try ctx.set("fillStyle", js.string("black"));
|
||||
try ctx.call(void, "fillText", .{
|
||||
glyph_str,
|
||||
left + 1,
|
||||
asc + 1,
|
||||
});
|
||||
|
||||
// Read the image data and get it into a []u8 on our side
|
||||
const bitmap: []u8 = bitmap: {
|
||||
// Read the raw bitmap data and get the "data" value which is a
|
||||
// Uint8ClampedArray.
|
||||
const data = try ctx.call(js.Object, "getImageData", .{ 0, 0, width, height });
|
||||
defer data.deinit();
|
||||
const src_array = try data.get(js.Object, "data");
|
||||
defer src_array.deinit();
|
||||
|
||||
// Allocate our local memory to copy the data to.
|
||||
const len = try src_array.get(u32, "length");
|
||||
var bitmap = try alloc.alloc(u8, @intCast(usize, len));
|
||||
errdefer alloc.free(bitmap);
|
||||
|
||||
// Create our target Uint8Array that we can use to copy from src.
|
||||
const mem_array = mem_array: {
|
||||
// Get our runtime memory
|
||||
const mem = try js.runtime.get(js.Object, "memory");
|
||||
defer mem.deinit();
|
||||
const buf = try mem.get(js.Object, "buffer");
|
||||
defer buf.deinit();
|
||||
|
||||
// Construct our array to peer into our memory
|
||||
const Uint8Array = try js.global.get(js.Object, "Uint8Array");
|
||||
defer Uint8Array.deinit();
|
||||
const mem_array = try Uint8Array.new(.{ buf, bitmap.ptr });
|
||||
errdefer mem_array.deinit();
|
||||
|
||||
break :mem_array mem_array;
|
||||
};
|
||||
defer mem_array.deinit();
|
||||
|
||||
// Copy
|
||||
try mem_array.call(void, "set", .{src_array});
|
||||
|
||||
break :bitmap bitmap;
|
||||
};
|
||||
defer alloc.free(bitmap);
|
||||
|
||||
// The bitmap is in RGBA format and we just want alpha8.
|
||||
assert(@mod(bitmap.len, 4) == 0);
|
||||
var bitmap_a8 = try alloc.alloc(u8, bitmap.len / 4);
|
||||
defer alloc.free(bitmap_a8);
|
||||
var i: usize = 0;
|
||||
while (i < bitmap_a8.len) : (i += 1) {
|
||||
bitmap_a8[i] = bitmap[(i * 4) + 3];
|
||||
}
|
||||
|
||||
// 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_a8);
|
||||
|
||||
return font.Glyph{
|
||||
.width = width,
|
||||
.height = 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;
|
||||
}
|
||||
};
|
||||
|
||||
/// The wasm-compatible API.
|
||||
pub const Wasm = struct {
|
||||
const wasm = @import("../../os/wasm.zig");
|
||||
const alloc = wasm.alloc;
|
||||
|
||||
export fn face_new(ptr: [*]const u8, len: usize, pts: u16) ?*Face {
|
||||
return face_new_(ptr, len, pts) catch null;
|
||||
}
|
||||
|
||||
fn face_new_(ptr: [*]const u8, len: usize, pts: u16) !*Face {
|
||||
var face = try Face.initNamed(alloc, ptr[0..len], .{ .points = pts });
|
||||
errdefer face.deinit();
|
||||
|
||||
var result = try alloc.create(Face);
|
||||
errdefer alloc.destroy(result);
|
||||
result.* = face;
|
||||
return result;
|
||||
}
|
||||
|
||||
export fn face_free(ptr: ?*Face) void {
|
||||
if (ptr) |v| {
|
||||
v.deinit();
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
42
src/font/library.zig
Normal file
42
src/font/library.zig
Normal file
@ -0,0 +1,42 @@
|
||||
//! A library represents the shared state that the underlying font
|
||||
//! library implementation(s) require per-process.
|
||||
const builtin = @import("builtin");
|
||||
const options = @import("main.zig").options;
|
||||
const freetype = @import("freetype");
|
||||
const font = @import("main.zig");
|
||||
|
||||
/// Library implementation for the compile options.
|
||||
pub const Library = switch (options.backend) {
|
||||
// Freetype requires a state library
|
||||
.freetype,
|
||||
.fontconfig_freetype,
|
||||
.coretext_freetype,
|
||||
=> FreetypeLibrary,
|
||||
|
||||
// Some backends such as CT and Canvas don't have a "library"
|
||||
.coretext,
|
||||
.web_canvas,
|
||||
=> NoopLibrary,
|
||||
};
|
||||
|
||||
pub const FreetypeLibrary = struct {
|
||||
lib: freetype.Library,
|
||||
|
||||
pub fn init() freetype.Error!Library {
|
||||
return Library{ .lib = try freetype.Library.init() };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Library) void {
|
||||
self.lib.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
pub const NoopLibrary = struct {
|
||||
pub fn init() !Library {
|
||||
return Library{};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Library) void {
|
||||
_ = self;
|
||||
}
|
||||
};
|
@ -10,15 +10,18 @@ pub const Face = face.Face;
|
||||
pub const Group = @import("Group.zig");
|
||||
pub const GroupCache = @import("GroupCache.zig");
|
||||
pub const Glyph = @import("Glyph.zig");
|
||||
pub const Library = @import("Library.zig");
|
||||
pub const Shaper = @import("Shaper.zig");
|
||||
pub const sprite = @import("sprite.zig");
|
||||
pub const Sprite = sprite.Sprite;
|
||||
pub const Descriptor = discovery.Descriptor;
|
||||
pub const Discover = discovery.Discover;
|
||||
pub usingnamespace @import("library.zig");
|
||||
|
||||
/// If we're targeting wasm then we export some wasm APIs.
|
||||
pub usingnamespace if (builtin.target.isWasm()) struct {
|
||||
pub usingnamespace Atlas.Wasm;
|
||||
pub usingnamespace Group.Wasm;
|
||||
pub usingnamespace face.web_canvas.Wasm;
|
||||
} else struct {};
|
||||
|
||||
/// Build options
|
||||
@ -35,9 +38,12 @@ pub const Backend = enum {
|
||||
/// Fontconfig for font discovery and FreeType for font rendering.
|
||||
fontconfig_freetype,
|
||||
|
||||
/// CoreText for both font discovery and rendering (macOS).
|
||||
/// CoreText for both font discovery for rendering (macOS).
|
||||
coretext,
|
||||
|
||||
/// CoreText for font discovery and FreeType for rendering (macOS).
|
||||
coretext_freetype,
|
||||
|
||||
/// Use the browser font system and the Canvas API (wasm). This limits
|
||||
/// the available fonts to browser fonts (anything Canvas natively
|
||||
/// supports).
|
||||
|
@ -1,5 +1,17 @@
|
||||
// This is the main file for the WASM module. The WASM module has to
|
||||
// export a C ABI compatible API.
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
pub usingnamespace @import("wasm.zig");
|
||||
pub usingnamespace @import("os/wasm.zig");
|
||||
pub usingnamespace @import("os/wasm/log.zig");
|
||||
pub usingnamespace @import("font/main.zig");
|
||||
|
||||
// Set our log level. We try to get as much logging as possible but in
|
||||
// ReleaseSmall mode where we're optimizing for space, we elevate the
|
||||
// log level.
|
||||
pub const log_level: std.log.Level = switch (builtin.mode) {
|
||||
.Debug => .debug,
|
||||
.ReleaseSmall => .warn,
|
||||
else => .info,
|
||||
};
|
||||
|
@ -37,6 +37,18 @@ pub export fn malloc(len: usize) ?[*]u8 {
|
||||
return alloc_(len) catch return null;
|
||||
}
|
||||
|
||||
fn alloc_(len: usize) ![*]u8 {
|
||||
// Create the allocation
|
||||
const slice = try alloc.alloc(u8, len);
|
||||
errdefer alloc.free(slice);
|
||||
|
||||
// Store the size so we can deallocate later
|
||||
try allocs.putNoClobber(alloc, slice.ptr, slice.len);
|
||||
errdefer _ = allocs.remove(slice.ptr);
|
||||
|
||||
return slice.ptr;
|
||||
}
|
||||
|
||||
/// Free an allocation from malloc.
|
||||
pub export fn free(ptr: ?[*]u8) void {
|
||||
if (ptr) |v| {
|
||||
@ -79,18 +91,6 @@ pub fn toModuleOwned(ptr: anytype) void {
|
||||
_ = allocs.remove(casted);
|
||||
}
|
||||
|
||||
fn alloc_(len: usize) ![*]u8 {
|
||||
// Create the allocation
|
||||
const slice = try alloc.alloc(u8, len);
|
||||
errdefer alloc.free(slice);
|
||||
|
||||
// Store the size so we can deallocate later
|
||||
try allocs.putNoClobber(alloc, slice.ptr, slice.len);
|
||||
errdefer _ = allocs.remove(slice.ptr);
|
||||
|
||||
return slice.ptr;
|
||||
}
|
||||
|
||||
test "basics" {
|
||||
const testing = std.testing;
|
||||
var buf = malloc(32).?;
|
27
src/os/wasm/log.zig
Normal file
27
src/os/wasm/log.zig
Normal file
@ -0,0 +1,27 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const wasm = @import("../wasm.zig");
|
||||
|
||||
// The function std.log will call.
|
||||
pub fn log(
|
||||
comptime level: std.log.Level,
|
||||
comptime scope: @TypeOf(.EnumLiteral),
|
||||
comptime format: []const u8,
|
||||
args: anytype,
|
||||
) void {
|
||||
// Build the string
|
||||
const level_txt = comptime level.asText();
|
||||
const prefix = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): ";
|
||||
const txt = level_txt ++ prefix ++ format;
|
||||
const str = nosuspend std.fmt.allocPrint(wasm.alloc, txt, args) catch return;
|
||||
defer wasm.alloc.free(str);
|
||||
|
||||
// Send it over to the JS side
|
||||
JS.log(str.ptr, str.len);
|
||||
}
|
||||
|
||||
// We wrap our externs in this namespace so we can reuse symbols, otherwise
|
||||
// "log" would collide.
|
||||
const JS = struct {
|
||||
extern "env" fn log(ptr: [*]const u8, len: usize) void;
|
||||
};
|
1
vendor/zig-js
vendored
Submodule
1
vendor/zig-js
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 52eed4daddcf9fa974ad4457691de26ef2351c56
|
Reference in New Issue
Block a user