615 lines
18 KiB
Zig
615 lines
18 KiB
Zig
const std = @import("std");
|
|
const assert = std.debug.assert;
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
const sokol = @import("sokol");
|
|
const sapp = sokol.app;
|
|
|
|
const Math = @import("./math.zig");
|
|
const Vec2 = Math.Vec2;
|
|
const Rect = Math.Rect;
|
|
const Vec4 = Math.Vec4;
|
|
|
|
const Gfx = @import("./graphics.zig");
|
|
|
|
const c = @cImport({
|
|
@cInclude("sokol/sokol_gfx.h");
|
|
@cInclude("fontstash.h");
|
|
@cInclude("sokol_fontstash.h");
|
|
});
|
|
|
|
// Struct definition copies from fonstash.h
|
|
// TODO: Create a getter, to avoid copying this struct by hand
|
|
const FONSstate = extern struct {
|
|
font: c_int,
|
|
@"align": c_int,
|
|
size: f32,
|
|
color: c_uint,
|
|
blur: f32,
|
|
spacing: f32
|
|
};
|
|
|
|
const FONSfont = opaque{};
|
|
const FONSglyph = opaque{};
|
|
|
|
extern fn zig_isTopLeft(stash: ?*c.FONScontext) bool;
|
|
extern fn zig_getGlyphIndex(glyph: ?*FONSglyph) c_int;
|
|
extern fn zig_fons__getState(stash: ?*c.struct_FONScontext) ?*FONSstate;
|
|
extern fn zig_getFont(stash: ?*c.FONScontext, index: c_int) ?*FONSfont;
|
|
extern fn zig_fons__tt_getPixelHeightScale(font: ?*FONSfont, size: f32) f32;
|
|
extern fn zig_fons__getVertAlign(stash: ?*c.FONScontext, font: ?*FONSfont, @"align": c_int, isize: i16) f32;
|
|
extern fn zig_fons__getGlyph(
|
|
stash: ?*c.FONScontext,
|
|
font: ?*FONSfont,
|
|
codepoint: c_uint,
|
|
@"isize": i16,
|
|
iblur: i16
|
|
) ?*FONSglyph;
|
|
|
|
extern fn zig_fons__getQuad(
|
|
stash: ?*c.FONScontext,
|
|
font: ?*FONSfont,
|
|
prevGlyphIndex: c_int, glyph: ?*FONSglyph,
|
|
scale: f32,
|
|
spacing: f32,
|
|
x: ?*f32, y: ?*f32,
|
|
q: ?*c.FONSquad
|
|
) void;
|
|
|
|
const Font = @This();
|
|
|
|
pub const Id = enum(c_int) {
|
|
_,
|
|
|
|
pub const invalid: Id = @enumFromInt(c.FONS_INVALID);
|
|
};
|
|
|
|
pub const AlignX = enum {
|
|
left,
|
|
right,
|
|
center,
|
|
|
|
fn toFONSAlignment(self: AlignX) c_int {
|
|
return switch (self) {
|
|
.left => c.FONS_ALIGN_LEFT,
|
|
.center => c.FONS_ALIGN_CENTER,
|
|
.right => c.FONS_ALIGN_RIGHT,
|
|
};
|
|
}
|
|
};
|
|
|
|
pub const AlignY = enum {
|
|
top,
|
|
middle,
|
|
bottom,
|
|
baseline,
|
|
|
|
fn toFONSAlignment(self: AlignY) c_int {
|
|
return switch (self) {
|
|
.top => c.FONS_ALIGN_TOP,
|
|
.middle => c.FONS_ALIGN_MIDDLE,
|
|
.bottom => c.FONS_ALIGN_BOTTOM,
|
|
.baseline => c.FONS_ALIGN_BASELINE,
|
|
};
|
|
}
|
|
};
|
|
|
|
pub const QuadIterator = struct {
|
|
ctx: Context,
|
|
|
|
x: f32,
|
|
y: f32,
|
|
next_x: f32,
|
|
next_y: f32,
|
|
spacing: f32,
|
|
scale: f32,
|
|
isize: c_short,
|
|
iblur: c_short,
|
|
prev_glyph_index: c_int,
|
|
font: *FONSfont,
|
|
|
|
pub fn init(ctx: Context, font: Font) QuadIterator {
|
|
const stash = ctx.fons_context;
|
|
ctx.setAsCurrent(font);
|
|
|
|
const x: f32 = 0;
|
|
var y: f32 = 0;
|
|
|
|
const state = zig_fons__getState(stash).?;
|
|
const fons_font = zig_getFont(stash, state.font) orelse @panic("Invalid font");
|
|
|
|
const font_isize: c_short = @intFromFloat(@trunc(state.size * 10.0));
|
|
const font_iblur: c_short = @intFromFloat(@trunc(state.blur));
|
|
const scale = zig_fons__tt_getPixelHeightScale(fons_font, @as(f32, @floatFromInt(font_isize)) / 10.0);
|
|
|
|
assert(state.@"align" & c.FONS_ALIGN_LEFT != 0);
|
|
assert(state.@"align" & c.FONS_ALIGN_RIGHT == 0);
|
|
assert(state.@"align" & c.FONS_ALIGN_CENTER == 0);
|
|
|
|
// Align vertically.
|
|
y += zig_fons__getVertAlign(stash, fons_font, @truncate(state.@"align"), font_isize);
|
|
|
|
return QuadIterator{
|
|
.ctx = ctx,
|
|
.x = x,
|
|
.y = y,
|
|
.next_x = x,
|
|
.next_y = y,
|
|
.isize = font_isize,
|
|
.iblur = font_iblur,
|
|
.scale = scale,
|
|
.spacing = state.spacing,
|
|
.font = fons_font,
|
|
.prev_glyph_index = -1
|
|
};
|
|
}
|
|
|
|
pub fn next(self: *QuadIterator, codepoint: u21) ?Rect {
|
|
const stash = self.ctx.fons_context;
|
|
|
|
self.x = self.next_x;
|
|
self.y = self.next_y;
|
|
|
|
const glyph = zig_fons__getGlyph(stash, self.font, codepoint, self.isize, self.iblur);
|
|
if (glyph != null) {
|
|
var q: c.FONSquad = .{};
|
|
zig_fons__getQuad(stash, self.font, self.prev_glyph_index, glyph, self.scale, self.spacing, &self.x, &self.y, &q);
|
|
self.prev_glyph_index = zig_getGlyphIndex(glyph);
|
|
return Rect.init(q.x0, q.y0, q.x1 - q.x0, q.y1 - q.y0);
|
|
} else {
|
|
self.prev_glyph_index = -1;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
pub const Ascii = struct {
|
|
iter: QuadIterator,
|
|
text: []const u8,
|
|
index: usize,
|
|
|
|
pub fn init(ctx: Context, font: Font, text: []const u8) Ascii {
|
|
return Ascii{
|
|
.iter = .init(ctx, font),
|
|
.text = text,
|
|
.index = 0
|
|
};
|
|
}
|
|
|
|
pub fn next(self: *Ascii) ?Rect {
|
|
while (self.index < self.text.len) {
|
|
const codepoint_len = std.unicode.utf8ByteSequenceLength(self.text[self.index]) catch {
|
|
self.index += 1;
|
|
continue;
|
|
};
|
|
|
|
if (self.index + codepoint_len > self.text.len) {
|
|
self.index = self.text.len;
|
|
return null;
|
|
}
|
|
|
|
const codepoint = std.unicode.utf8Decode(self.text[self.index..][0..codepoint_len]) catch {
|
|
self.index += 1;
|
|
continue;
|
|
};
|
|
self.index += codepoint_len;
|
|
|
|
if (self.iter.next(codepoint)) |rect| {
|
|
return rect;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
pub fn last(self: *Ascii) ?Rect {
|
|
var last_rect: ?Rect = null;
|
|
while (self.next()) |rect| {
|
|
last_rect = rect;
|
|
}
|
|
return last_rect;
|
|
}
|
|
};
|
|
|
|
pub const Unicode = struct {
|
|
iter: QuadIterator,
|
|
text: []const u21,
|
|
index: usize,
|
|
|
|
pub fn init(ctx: Context, font: Font, text: []const u21) Unicode {
|
|
return Unicode{
|
|
.iter = .init(ctx, font),
|
|
.text = text,
|
|
.index = 0
|
|
};
|
|
}
|
|
|
|
pub fn next(self: *Unicode) ?Rect {
|
|
while (self.index < self.text.len) {
|
|
const next_rect = self.iter.next(self.text[self.index]);
|
|
self.index += 1;
|
|
|
|
if (next_rect) |rect| {
|
|
return rect;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
pub fn last(self: *Unicode) ?Rect {
|
|
var last_rect: ?Rect = null;
|
|
while (self.next()) |rect| {
|
|
last_rect = rect;
|
|
}
|
|
return last_rect;
|
|
}
|
|
};
|
|
};
|
|
|
|
pub const Context = struct {
|
|
fons_context: *c.struct_FONScontext,
|
|
|
|
pub fn init(atlas_size: f32) !Context {
|
|
const dpi_scale = sapp.dpiScale();
|
|
|
|
const atlas_dim = std.math.ceilPowerOfTwoAssert(u32, @intFromFloat(atlas_size * dpi_scale));
|
|
const fons_context = c.sfons_create(&c.sfons_desc_t{
|
|
.width = @intCast(atlas_dim),
|
|
.height = @intCast(atlas_dim),
|
|
});
|
|
if (fons_context == null) {
|
|
return error.sfons_create;
|
|
}
|
|
|
|
return Context{
|
|
.fons_context = fons_context.?
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: Context) void {
|
|
c.sfons_destroy(self.fons_context);
|
|
}
|
|
|
|
pub fn add(self: Context, name: [*c]const u8, data: []const u8) !Id {
|
|
const font_id = c.fonsAddFontMem(
|
|
self.fons_context,
|
|
name,
|
|
@constCast(data.ptr),
|
|
@intCast(data.len),
|
|
0
|
|
);
|
|
if (font_id == c.FONS_INVALID) {
|
|
return error.fonsAddFontMem;
|
|
}
|
|
return @enumFromInt(font_id);
|
|
}
|
|
|
|
pub fn clearState(self: Context) void {
|
|
c.fonsClearState(self.fons_context);
|
|
}
|
|
|
|
pub fn flush(self: Context) void {
|
|
c.sfons_flush(self.fons_context);
|
|
}
|
|
|
|
fn setAsCurrent(self: Context, font: Font) void {
|
|
const fs = self.fons_context;
|
|
const dpi_scale = sapp.dpiScale();
|
|
|
|
c.fonsSetFont(fs, @intFromEnum(font.id));
|
|
c.fonsSetSize(fs, font.size * dpi_scale);
|
|
c.fonsSetAlign(fs, Font.AlignX.left.toFONSAlignment() | Font.AlignY.top.toFONSAlignment());
|
|
c.fonsSetSpacing(fs, font.spacing);
|
|
}
|
|
|
|
pub fn drawText(self: Context, font: Font, pos: Vec2, color: Vec4, text: []const u8) void {
|
|
const fs = self.fons_context;
|
|
|
|
// TODO: Test if calling `.setAsCurrent()` isn't expensive
|
|
self.setAsCurrent(font);
|
|
|
|
const fons_color = c.sfons_rgba(
|
|
@intFromFloat(color.x * 255),
|
|
@intFromFloat(color.y * 255),
|
|
@intFromFloat(color.z * 255),
|
|
@intFromFloat(color.w * 255)
|
|
);
|
|
c.fonsSetColor(fs, fons_color);
|
|
|
|
Gfx.pushTransform(.init(0, 0), 1.0/64.0);
|
|
defer Gfx.popTransform();
|
|
|
|
_ = c.fonsDrawText(
|
|
fs,
|
|
pos.x*64,
|
|
pos.y*64,
|
|
text.ptr,
|
|
text.ptr + text.len
|
|
);
|
|
}
|
|
|
|
pub fn getBounds(self: Context, font: Font, text: []const u8) Rect {
|
|
const fs = self.fons_context;
|
|
self.setAsCurrent(font);
|
|
|
|
var line_bounds: [4]f32 = undefined;
|
|
_ = c.fonsTextBounds(fs, 0, 0, text.ptr, text.ptr + text.len, &line_bounds);
|
|
|
|
const min_x = line_bounds[0];
|
|
const min_y = line_bounds[1];
|
|
const max_x = line_bounds[2];
|
|
const max_y = line_bounds[3];
|
|
|
|
const width = max_x - min_x;
|
|
const height = max_y - min_y;
|
|
|
|
return Rect.init(min_x, min_y, width, height);
|
|
}
|
|
|
|
pub fn getBoundsUtf8(self: Context, font: Font, text: []const u21) Rect {
|
|
const fs = self.fons_context;
|
|
self.setAsCurrent(font);
|
|
|
|
var line_bounds: [4]f32 = undefined;
|
|
_ = fonsTextBoundsUtf8(fs, 0, 0, text, &line_bounds);
|
|
|
|
const min_x = line_bounds[0];
|
|
const min_y = line_bounds[1];
|
|
const max_x = line_bounds[2];
|
|
const max_y = line_bounds[3];
|
|
|
|
const width = max_x - min_x;
|
|
const height = max_y - min_y;
|
|
|
|
return Rect.init(min_x, min_y, width, height);
|
|
}
|
|
|
|
pub fn measure(self: Context, font: Font, text: []const u8) Vec2 {
|
|
const bounds = self.getBounds(font, text);
|
|
return bounds.size;
|
|
}
|
|
|
|
pub fn quadIter(self: Context, font: Font, text: []const u8) QuadIterator.Ascii {
|
|
return QuadIterator.Ascii.init(self, font, text);
|
|
}
|
|
|
|
pub fn quadUnicodeIter(self: Context, font: Font, text: []const u21) QuadIterator.Unicode {
|
|
return QuadIterator.Unicode.init(self, font, text);
|
|
}
|
|
|
|
// A reimplementation of `fonsTextBounds` which uses an array of already decoded codepoints
|
|
fn fonsTextBoundsUtf8(stash: ?*c.struct_FONScontext, initial_x: f32, initial_y: f32, str: []const u21, bounds: [*c]f32) f32 {
|
|
if (stash == null) {
|
|
return 0;
|
|
}
|
|
|
|
var x = initial_x;
|
|
var y = initial_y;
|
|
|
|
const state = zig_fons__getState(stash).?;
|
|
const font = zig_getFont(stash, state.font) orelse return 0;
|
|
|
|
const font_isize: i16 = @intFromFloat(@trunc(state.size * 10.0));
|
|
const font_iblur: i16 = @intFromFloat(@trunc(state.blur));
|
|
|
|
const scale = zig_fons__tt_getPixelHeightScale(font, @as(f32, @floatFromInt(font_isize)) / 10.0);
|
|
|
|
// Align vertically.
|
|
y += zig_fons__getVertAlign(stash, font, @truncate(state.@"align"), font_isize);
|
|
|
|
var minx = x;
|
|
var maxx = x;
|
|
var miny = y;
|
|
var maxy = y;
|
|
const startx = x;
|
|
|
|
var prevGlyphIndex: c_int = -1;
|
|
for (str) |codepoint| {
|
|
const glyph = zig_fons__getGlyph(stash, font, codepoint, font_isize, font_iblur);
|
|
if (glyph != null) {
|
|
var q: c.FONSquad = .{};
|
|
zig_fons__getQuad(stash, font, prevGlyphIndex, glyph, scale, state.spacing, &x, &y, &q);
|
|
if (q.x0 < minx) minx = q.x0;
|
|
if (q.x1 > maxx) maxx = q.x1;
|
|
if (zig_isTopLeft(stash)) {
|
|
if (q.y0 < miny) miny = q.y0;
|
|
if (q.y1 > maxy) maxy = q.y1;
|
|
} else {
|
|
if (q.y1 < miny) miny = q.y1;
|
|
if (q.y0 > maxy) maxy = q.y0;
|
|
}
|
|
|
|
prevGlyphIndex = zig_getGlyphIndex(glyph);
|
|
} else {
|
|
prevGlyphIndex = -1;
|
|
}
|
|
}
|
|
|
|
const advance = x - startx;
|
|
|
|
// Align horizontally
|
|
if ((state.@"align" & c.FONS_ALIGN_LEFT) != 0) {
|
|
// empty
|
|
} else if ((state.@"align" & c.FONS_ALIGN_RIGHT) != 0) {
|
|
minx -= advance;
|
|
maxx -= advance;
|
|
} else if ((state.@"align" & c.FONS_ALIGN_CENTER) != 0) {
|
|
minx -= advance * 0.5;
|
|
maxx -= advance * 0.5;
|
|
}
|
|
|
|
if (bounds != null) {
|
|
bounds[0] = minx;
|
|
bounds[1] = miny;
|
|
bounds[2] = maxx;
|
|
bounds[3] = maxy;
|
|
}
|
|
|
|
return advance;
|
|
}
|
|
};
|
|
|
|
pub const DrawTextContext = struct {
|
|
pub const Line = struct {
|
|
// Assumes that the text won't be invalidted between `queue*` and `draw` functions
|
|
text: []const u8,
|
|
pos: Vec2,
|
|
bounds: Rect,
|
|
};
|
|
|
|
// TODO: Add support for multiple font in a single context.
|
|
// I.e. each command should be able to have a separate font
|
|
ctx: Context,
|
|
font: Font,
|
|
|
|
line_height: f32 = 0,
|
|
pos: Vec2 = Vec2.zero,
|
|
lines: std.ArrayListUnmanaged(Line) = .empty,
|
|
bounds: Rect = Rect.zero,
|
|
|
|
pub fn init(ctx: Context, font: Font) DrawTextContext {
|
|
var self = DrawTextContext{
|
|
.ctx = ctx,
|
|
.font = font
|
|
};
|
|
|
|
const fs = ctx.fons_context;
|
|
self.ctx.setAsCurrent(font);
|
|
c.fonsVertMetrics(fs, null, null, &self.line_height);
|
|
|
|
return self;
|
|
}
|
|
|
|
pub fn deinit(self: *DrawTextContext, allocator: Allocator) void {
|
|
self.lines.deinit(allocator);
|
|
}
|
|
|
|
pub fn queueText(self: *DrawTextContext, allocator: Allocator, text: []const u8) !void {
|
|
var line_iter = std.mem.splitScalar(u8, text, '\n');
|
|
while (line_iter.next()) |line| {
|
|
try self.queueLine(allocator, line);
|
|
}
|
|
}
|
|
|
|
pub fn measureText(self: *const DrawTextContext, text: []const u8) Rect {
|
|
const fs = self.ctx.fons_context;
|
|
self.ctx.setAsCurrent(self.font);
|
|
|
|
var min_x: f32 = 0;
|
|
var max_x: f32 = 0;
|
|
var min_y: f32 = 0;
|
|
var max_y: f32 = 0;
|
|
|
|
var pos = Vec2.zero;
|
|
var line_iter = std.mem.splitScalar(u8, text, '\n');
|
|
while (line_iter.next()) |line| {
|
|
var line_bounds: [4]f32 = undefined;
|
|
_ = c.fonsTextBounds(fs, pos.x, pos.y, line.ptr, line.ptr + line.len, &line_bounds);
|
|
min_x = @min(min_x, line_bounds[0]);
|
|
min_y = @min(min_y, line_bounds[1]);
|
|
max_x = @max(max_x, line_bounds[2]);
|
|
max_y = @max(max_y, line_bounds[3]);
|
|
|
|
pos.y += self.line_height * self.font.line_spacing;
|
|
}
|
|
|
|
return Rect.init(min_x, min_y, max_x - min_x, max_y - min_y);
|
|
}
|
|
|
|
pub fn queueLine(self: *DrawTextContext, allocator: Allocator, line: []const u8) !void {
|
|
const fs = self.ctx.fons_context;
|
|
self.ctx.setAsCurrent(self.font);
|
|
|
|
var line_bounds: [4]f32 = undefined;
|
|
_ = c.fonsTextBounds(fs, self.pos.x, self.pos.y, line.ptr, line.ptr + line.len, &line_bounds);
|
|
|
|
try self.lines.append(allocator, .{
|
|
.bounds = Rect.init(
|
|
line_bounds[0],
|
|
line_bounds[1],
|
|
line_bounds[2] - line_bounds[0],
|
|
line_bounds[3] - line_bounds[1],
|
|
),
|
|
.pos = self.pos,
|
|
.text = line
|
|
});
|
|
self.pos.y += self.line_height * self.font.line_spacing;
|
|
|
|
var min_x = self.bounds.pos.x;
|
|
var max_x = self.bounds.pos.x + self.bounds.size.x;
|
|
var min_y = self.bounds.pos.y;
|
|
var max_y = self.bounds.pos.y + self.bounds.size.y;
|
|
|
|
min_x = @min(min_x, line_bounds[0]);
|
|
min_y = @min(min_y, line_bounds[1]);
|
|
max_x = @max(max_x, line_bounds[2]);
|
|
max_y = @max(max_y, line_bounds[3]);
|
|
|
|
self.bounds = Rect.init(min_x, min_y, max_x - min_x, max_y - min_y);
|
|
}
|
|
|
|
pub fn draw(self: *DrawTextContext, pos: Vec2, color: Vec4) void {
|
|
for (self.lines.items) |cmd| {
|
|
self.ctx.drawText(self.font, pos.add(cmd.pos), color, cmd.text);
|
|
}
|
|
}
|
|
|
|
pub fn isEmpty(self: *DrawTextContext) bool {
|
|
return self.lines.items.len == 0;
|
|
}
|
|
|
|
// pub fn drawLine(self: *DrawTextContext, line: []const u8) void {
|
|
// const fs = fons_context;
|
|
//
|
|
// _ = c.fonsDrawText(fs, self.pos.x, self.pos.y, line.ptr, line.ptr + line.len);
|
|
// self.pos.y += self.line_height * self.font.line_spacing;
|
|
// }
|
|
//
|
|
// pub fn drawLines(self: *DrawTextContext, lines: []const []const u8) void {
|
|
// for (lines) |line| {
|
|
// self.drawLine(line);
|
|
// }
|
|
// }
|
|
//
|
|
// pub fn drawText(self: *DrawTextContext, text: []const u8) void {
|
|
// var line_iter = std.mem.splitScalar(u8, text, '\n');
|
|
// while (line_iter.next()) |line| {
|
|
// self.drawLine(line);
|
|
// }
|
|
// }
|
|
//
|
|
// pub fn measureLine(self: *DrawTextContext, line: []const u8) void {
|
|
// const fs = fons_context;
|
|
//
|
|
// var min_x: f32 = 0;
|
|
// var min_y: f32 = 0;
|
|
// var max_x: f32 = 0;
|
|
// var max_y: f32 = 0;
|
|
//
|
|
// var line_iter = std.mem.splitScalar(u8, text, '\n');
|
|
// var y: f32 = 0;
|
|
// while (line_iter.next()) |line| {
|
|
// var line_bounds: [4]f32 = undefined;
|
|
//
|
|
// _ = c.fonsTextBounds(fs, 0, y, line.ptr, line.ptr + line.len, &line_bounds);
|
|
// y += line_height * font.line_spacing;
|
|
//
|
|
// min_x = @min(min_x, line_bounds[0]);
|
|
// min_y = @min(min_y, line_bounds[1]);
|
|
// max_x = @max(max_x, line_bounds[2]);
|
|
// max_y = @max(max_y, line_bounds[3]);
|
|
// }
|
|
//
|
|
// return Rect.init(min_x, min_y, max_x - min_x, max_y - min_y);
|
|
// }
|
|
|
|
pub fn end(self: *DrawTextContext) void {
|
|
_ = self;
|
|
}
|
|
};
|
|
|
|
id: Id,
|
|
size: f32,
|
|
spacing: f32 = 0,
|
|
line_spacing: f32 = 1,
|