351 lines
13 KiB
Zig
351 lines
13 KiB
Zig
//! The font the tab bar draws its titles in: fontconfig picks it, FreeType
|
|
//! rasterises it.
|
|
//!
|
|
//! The bar is the only thing att_wm draws itself, and window titles are text
|
|
//! the user reads at a glance, so they are worth a real font — matched by
|
|
//! pattern (`config.tab_font`) rather than by path, hinted, and rendered to
|
|
//! antialiased coverage that `shm.Buffer` blends over the tab.
|
|
//!
|
|
//! Titles are whatever a client cares to set, so one font is rarely enough: a
|
|
//! codepoint the matched font has no glyph for sends us back to fontconfig for
|
|
//! one that does. That is as far as the text stack goes. There is no shaping,
|
|
//! so scripts that need it — Arabic, Devanagari — come out as unjoined letters
|
|
//! rather than correctly; a window manager is not the place to link HarfBuzz.
|
|
//!
|
|
//! One face is shared by every output. It is not thread safe, which is fine:
|
|
//! everything here happens on the one Wayland event loop.
|
|
|
|
const Face = @This();
|
|
|
|
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
const font = @import("font.zig");
|
|
|
|
const c = @cImport({
|
|
@cInclude("fontconfig/fontconfig.h");
|
|
@cInclude("freetype/freetype.h");
|
|
});
|
|
|
|
/// FreeType's flag constants come from macros; these are the two we need,
|
|
/// narrowed to the FT_Int32 the load functions take.
|
|
const load = struct {
|
|
const render: i32 = @intCast(c.FT_LOAD_RENDER);
|
|
const no_hinting: i32 = @intCast(c.FT_LOAD_NO_HINTING);
|
|
|
|
/// FT_LOAD_TARGET_(x): the render mode packed into bits 16..19.
|
|
fn target(mode: c_uint) i32 {
|
|
return @intCast((mode & 15) << 16);
|
|
}
|
|
};
|
|
|
|
const pixel_mode_gray: u8 = @intCast(c.FT_PIXEL_MODE_GRAY);
|
|
|
|
/// Below this size the antialiasing is doing all the work and the text is a
|
|
/// smear; a bar too short for it is better off overflowing a little.
|
|
const min_px = 6;
|
|
|
|
/// Enough for a title mixing Latin, CJK and emoji, and a bound on what a
|
|
/// client can make us open by cycling its title through the world's scripts.
|
|
const max_fonts = 8;
|
|
|
|
/// How many codepoints to remember the font for before starting over. Titles
|
|
/// repeat their characters constantly, so even a small cache turns almost every
|
|
/// lookup into a hash hit.
|
|
const cache_limit = 1024;
|
|
|
|
gpa: Allocator,
|
|
library: c.FT_Library,
|
|
/// Index 0 is what the pattern matched; the rest were opened for codepoints it
|
|
/// had no glyph for.
|
|
fonts: std.ArrayList(Font),
|
|
/// Which font in `fonts` covers a codepoint, `no_font` for none of them.
|
|
coverage: std.AutoHashMapUnmanaged(u21, u8),
|
|
/// The substituted request, kept to match fallbacks against so they follow the
|
|
/// size and weight that was asked for.
|
|
pattern: *c.FcPattern,
|
|
/// Pixel size fontconfig resolved the pattern to; the ceiling for `fitHeight`.
|
|
preferred_px: u32,
|
|
/// Pixel size the fonts are drawn at. Applied to each lazily, so a face that
|
|
/// is only used for one codepoint is not resized on every bar redraw.
|
|
px: u32,
|
|
/// FT_LOAD_TARGET_* and friends, from the pattern's hinting settings.
|
|
load_flags: i32,
|
|
|
|
const no_font: u8 = std.math.maxInt(u8);
|
|
|
|
const Font = struct {
|
|
face: c.FT_Face,
|
|
/// The file it came from, so the same one is not opened twice.
|
|
path: []u8,
|
|
/// Size this face is currently set to, which lags `Face.px`.
|
|
px: u32 = 0,
|
|
};
|
|
|
|
/// A rendered glyph: 8-bit coverage, plus where it sits relative to the pen on
|
|
/// the baseline. Borrowed from a face's glyph slot, so it is only valid until
|
|
/// the next `render` call.
|
|
pub const Glyph = struct {
|
|
coverage: []const u8,
|
|
width: u32,
|
|
height: u32,
|
|
/// Bytes per row, which is not always `width`.
|
|
pitch: usize,
|
|
left: i32,
|
|
top: i32,
|
|
};
|
|
|
|
pub const Error = error{
|
|
FontconfigInit,
|
|
BadPattern,
|
|
NoFontMatched,
|
|
FreetypeInit,
|
|
FontUnreadable,
|
|
OutOfMemory,
|
|
};
|
|
|
|
/// Match `pattern` — ordinary fontconfig syntax, e.g. "sans:size=11" — and open
|
|
/// what it resolves to.
|
|
pub fn open(gpa: Allocator, pattern: [:0]const u8) Error!Face {
|
|
if (c.FcInit() != c.FcTrue) return error.FontconfigInit;
|
|
|
|
var library: c.FT_Library = null;
|
|
if (c.FT_Init_FreeType(&library) != 0) return error.FreetypeInit;
|
|
|
|
const pat = c.FcNameParse(pattern.ptr) orelse {
|
|
_ = c.FT_Done_FreeType(library);
|
|
return error.BadPattern;
|
|
};
|
|
|
|
// From here `self` owns both the library and the pattern, so one errdefer
|
|
// releases everything however far this gets.
|
|
var self: Face = .{
|
|
.gpa = gpa,
|
|
.library = library,
|
|
.fonts = .empty,
|
|
.coverage = .empty,
|
|
.pattern = pat,
|
|
.preferred_px = 12,
|
|
.px = 0,
|
|
.load_flags = load.target(c.FT_RENDER_MODE_LIGHT),
|
|
};
|
|
errdefer self.deinit();
|
|
|
|
// Fill the pattern out the way every fontconfig client is expected to:
|
|
// configuration first, then the built-in defaults for whatever is still
|
|
// unset, or the match comes back without a size or a hint style.
|
|
if (c.FcConfigSubstitute(null, pat, c.FcMatchPattern) != c.FcTrue) return error.BadPattern;
|
|
c.FcDefaultSubstitute(pat);
|
|
|
|
var result: c.FcResult = undefined;
|
|
const matched = c.FcFontMatch(null, pat, &result) orelse return error.NoFontMatched;
|
|
defer c.FcPatternDestroy(matched);
|
|
if (result != c.FcResultMatch) return error.NoFontMatched;
|
|
|
|
var size: f64 = 0;
|
|
if (c.FcPatternGetDouble(matched, c.FC_PIXEL_SIZE, 0, &size) != c.FcResultMatch) size = 0;
|
|
if (size >= min_px) self.preferred_px = @intFromFloat(@round(size));
|
|
self.load_flags = loadFlags(matched);
|
|
|
|
if (try self.openMatched(matched) == null) return error.FontUnreadable;
|
|
self.setPixelSize(self.preferred_px);
|
|
return self;
|
|
}
|
|
|
|
pub fn deinit(self: *Face) void {
|
|
for (self.fonts.items) |*f| {
|
|
_ = c.FT_Done_Face(f.face);
|
|
self.gpa.free(f.path);
|
|
}
|
|
self.fonts.deinit(self.gpa);
|
|
self.coverage.deinit(self.gpa);
|
|
c.FcPatternDestroy(self.pattern);
|
|
_ = c.FT_Done_FreeType(self.library);
|
|
self.* = undefined;
|
|
}
|
|
|
|
/// Open the font a completed fontconfig match names, or return the one already
|
|
/// open for that file. Null when FreeType will not have it.
|
|
fn openMatched(self: *Face, matched: *c.FcPattern) Allocator.Error!?u8 {
|
|
var path: [*c]c.FcChar8 = undefined;
|
|
if (c.FcPatternGetString(matched, c.FC_FILE, 0, &path) != c.FcResultMatch) return null;
|
|
var index: c_int = 0;
|
|
_ = c.FcPatternGetInteger(matched, c.FC_INDEX, 0, &index);
|
|
|
|
const name = std.mem.span(@as([*:0]const u8, @ptrCast(path)));
|
|
for (self.fonts.items, 0..) |f, i| {
|
|
if (std.mem.eql(u8, f.path, name)) return @intCast(i);
|
|
}
|
|
|
|
var face: c.FT_Face = null;
|
|
if (c.FT_New_Face(self.library, @ptrCast(path), index, &face) != 0) return null;
|
|
errdefer _ = c.FT_Done_Face(face);
|
|
|
|
const owned = try self.gpa.dupe(u8, name);
|
|
errdefer self.gpa.free(owned);
|
|
|
|
try self.fonts.append(self.gpa, .{ .face = face, .path = owned });
|
|
return @intCast(self.fonts.items.len - 1);
|
|
}
|
|
|
|
/// Hinting as the pattern asks for it. Light is the default fontconfig hands
|
|
/// out and the one that suits us best: it straightens stems vertically and
|
|
/// leaves horizontal metrics alone, which is what keeps subpixel-positioned
|
|
/// text evenly spaced.
|
|
fn loadFlags(matched: *c.FcPattern) i32 {
|
|
var hinting: c.FcBool = c.FcTrue;
|
|
_ = c.FcPatternGetBool(matched, c.FC_HINTING, 0, &hinting);
|
|
if (hinting != c.FcTrue) return load.no_hinting | load.target(c.FT_RENDER_MODE_NORMAL);
|
|
|
|
var style: c_int = c.FC_HINT_SLIGHT;
|
|
_ = c.FcPatternGetInteger(matched, c.FC_HINT_STYLE, 0, &style);
|
|
return switch (style) {
|
|
c.FC_HINT_NONE => load.no_hinting | load.target(c.FT_RENDER_MODE_NORMAL),
|
|
c.FC_HINT_FULL => load.target(c.FT_RENDER_MODE_NORMAL),
|
|
// Medium and slight both mean "vertical only" to FreeType.
|
|
else => load.target(c.FT_RENDER_MODE_LIGHT),
|
|
};
|
|
}
|
|
|
|
fn setPixelSize(self: *Face, px: u32) void {
|
|
if (self.px == px) return;
|
|
self.px = px;
|
|
// The primary carries the metrics the bar lays text out with, so it is
|
|
// resized now; the rest wait until something needs them.
|
|
self.resize(&self.fonts.items[0]);
|
|
}
|
|
|
|
fn resize(self: *Face, f: *Font) void {
|
|
if (f.px == self.px) return;
|
|
if (c.FT_Set_Pixel_Sizes(f.face, 0, self.px) != 0) return;
|
|
f.px = self.px;
|
|
}
|
|
|
|
/// Shrink the text to fit a strip `height` pixels tall, up to the size the
|
|
/// pattern asked for. A tab bar thinner than the configured font is a bar
|
|
/// height the user chose deliberately; the text should give way, not overflow.
|
|
pub fn fitHeight(self: *Face, height: i32) void {
|
|
if (height <= 0) return;
|
|
var px = self.preferred_px;
|
|
while (true) {
|
|
self.setPixelSize(px);
|
|
// Two pixels of air above and below the line.
|
|
if (self.lineHeight() + 4 <= height or px <= min_px) return;
|
|
px -= 1;
|
|
}
|
|
}
|
|
|
|
/// Distance from the top of the ascenders to the bottom of the descenders, in
|
|
/// whole pixels.
|
|
pub fn lineHeight(self: *const Face) i32 {
|
|
const m = self.fonts.items[0].face.*.size.*.metrics;
|
|
return @intCast((m.ascender - m.descender + 63) >> 6);
|
|
}
|
|
|
|
/// Where the baseline goes in a strip `height` pixels tall, measured from its
|
|
/// top, so that the text sits centred in it.
|
|
pub fn baselineIn(self: *const Face, height: i32) i32 {
|
|
const m = self.fonts.items[0].face.*.size.*.metrics;
|
|
const ascent: i32 = @intCast((m.ascender + 63) >> 6);
|
|
return @divFloor(height - self.lineHeight(), 2) + ascent;
|
|
}
|
|
|
|
/// The face to draw a codepoint with, and its glyph index in that face. Falls
|
|
/// back to the primary's .notdef box when nothing on the system has the glyph,
|
|
/// which is at least honest about being a character we cannot draw.
|
|
fn lookup(self: *Face, cp: u21) struct { face: c.FT_Face, glyph: c_uint } {
|
|
const primary = &self.fonts.items[0];
|
|
const which = self.coverage.get(cp) orelse blk: {
|
|
const found = self.find(cp);
|
|
// A title cycling through scripts must not grow this without bound.
|
|
if (self.coverage.count() >= cache_limit) self.coverage.clearRetainingCapacity();
|
|
self.coverage.put(self.gpa, cp, found) catch {};
|
|
break :blk found;
|
|
};
|
|
if (which == no_font or which >= self.fonts.items.len) {
|
|
self.resize(primary);
|
|
return .{ .face = primary.face, .glyph = 0 };
|
|
}
|
|
|
|
const f = &self.fonts.items[which];
|
|
self.resize(f);
|
|
return .{ .face = f.face, .glyph = c.FT_Get_Char_Index(f.face, cp) };
|
|
}
|
|
|
|
/// Which open font has a glyph for `cp`, opening one more if none does.
|
|
fn find(self: *Face, cp: u21) u8 {
|
|
for (self.fonts.items, 0..) |f, i| {
|
|
if (c.FT_Get_Char_Index(f.face, cp) != 0) return @intCast(i);
|
|
}
|
|
if (self.fonts.items.len >= max_fonts) return no_font;
|
|
|
|
// Ask fontconfig the same question it answers for every other client: what
|
|
// font like this one has this character?
|
|
const charset = c.FcCharSetCreate() orelse return no_font;
|
|
defer c.FcCharSetDestroy(charset);
|
|
if (c.FcCharSetAddChar(charset, cp) != c.FcTrue) return no_font;
|
|
|
|
const pat = c.FcPatternDuplicate(self.pattern) orelse return no_font;
|
|
defer c.FcPatternDestroy(pat);
|
|
if (c.FcPatternAddCharSet(pat, c.FC_CHARSET, charset) != c.FcTrue) return no_font;
|
|
|
|
var result: c.FcResult = undefined;
|
|
const matched = c.FcFontMatch(null, pat, &result) orelse return no_font;
|
|
defer c.FcPatternDestroy(matched);
|
|
if (result != c.FcResultMatch) return no_font;
|
|
|
|
const opened = (self.openMatched(matched) catch return no_font) orelse return no_font;
|
|
// fontconfig answers with its best match whether or not it covers the
|
|
// character, so the answer still has to be checked.
|
|
const f = &self.fonts.items[opened];
|
|
if (c.FT_Get_Char_Index(f.face, cp) == 0) return no_font;
|
|
return opened;
|
|
}
|
|
|
|
/// Horizontal advance of a codepoint in 26.6 fixed point — the unhinted,
|
|
/// fractional one, so that a string's width does not depend on where in a pixel
|
|
/// it starts.
|
|
pub fn advance(self: *Face, cp: u21) i32 {
|
|
const l = self.lookup(cp);
|
|
if (c.FT_Load_Glyph(l.face, l.glyph, self.load_flags) != 0) return 0;
|
|
// linearHoriAdvance is 16.16; the pen counts in 26.6.
|
|
return @intCast(l.face.*.glyph.*.linearHoriAdvance >> 10);
|
|
}
|
|
|
|
/// Advance of the ellipsis, which `font.fit` needs to reserve room for.
|
|
pub fn ellipsisAdvance(self: *Face) i32 {
|
|
return self.advance(font.ellipsis);
|
|
}
|
|
|
|
/// Rasterise a codepoint positioned `subpixel` 26.6 units into its cell.
|
|
/// Returns null for glyphs with nothing to draw, spaces above all.
|
|
pub fn render(self: *Face, cp: u21, subpixel: i32) ?Glyph {
|
|
const l = self.lookup(cp);
|
|
|
|
// Shifting the outline instead of rounding the pen is what buys evenly
|
|
// spaced text: a glyph landing a third of a pixel along is rasterised a
|
|
// third of a pixel along.
|
|
var delta: c.FT_Vector = .{ .x = @intCast(subpixel & 63), .y = 0 };
|
|
c.FT_Set_Transform(l.face, null, &delta);
|
|
defer c.FT_Set_Transform(l.face, null, null);
|
|
|
|
if (c.FT_Load_Glyph(l.face, l.glyph, self.load_flags | load.render) != 0) return null;
|
|
|
|
const slot = l.face.*.glyph;
|
|
const bitmap = slot.*.bitmap;
|
|
if (bitmap.width == 0 or bitmap.rows == 0) return null;
|
|
// FT_RENDER_MODE_NORMAL means 8-bit coverage; a face carrying embedded
|
|
// bitmaps can hand back a 1-bit one instead, which this does not unpack.
|
|
if (bitmap.pixel_mode != pixel_mode_gray) return null;
|
|
|
|
const pitch: usize = @abs(bitmap.pitch);
|
|
const rows: usize = bitmap.rows;
|
|
return .{
|
|
.coverage = bitmap.buffer[0 .. pitch * rows],
|
|
.width = bitmap.width,
|
|
.height = bitmap.rows,
|
|
.pitch = pitch,
|
|
.left = slot.*.bitmap_left,
|
|
.top = slot.*.bitmap_top,
|
|
};
|
|
}
|