//! 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, }; }