use freetype for font display
This commit is contained in:
@@ -191,9 +191,13 @@ onto a tag nobody is viewing leaves both the order and focus alone.
|
|||||||
**tabbed** — same geometry as monocle, minus a strip at the top where att_wm
|
**tabbed** — same geometry as monocle, minus a strip at the top where att_wm
|
||||||
draws one tab per window, the focused one highlighted. Each tab is clickable and
|
draws one tab per window, the focused one highlighted. Each tab is clickable and
|
||||||
carries the window's title, elided with an ellipsis when the tab is too narrow.
|
carries the window's title, elided with an ellipsis when the tab is too narrow.
|
||||||
Titles are drawn in a 5x8 bitmap font built into the binary (`src/font.zig`) —
|
Titles are drawn with FreeType in whatever font the fontconfig pattern
|
||||||
a font stack for one strip of pixels would be a poor trade — scaled by whole
|
`tab_font` matches (`"sans:size=11"` by default, so `fc-match 'sans:size=11'`
|
||||||
pixels to suit `tabbar_height`, or by `tab_font_scale` if you would rather say.
|
shows what you will get), antialiased and subpixel positioned, shrunk to fit
|
||||||
|
when `tabbar_height` leaves no room for the size asked for. Codepoints the
|
||||||
|
matched font has no glyph for send att_wm back to fontconfig for one that does,
|
||||||
|
so a CJK or emoji title is not a row of boxes; there is no shaping, so scripts
|
||||||
|
that need it come out as unjoined letters.
|
||||||
The same window list is published over IPC, so a bar can draw its own tab strip
|
The same window list is published over IPC, so a bar can draw its own tab strip
|
||||||
alongside (the bundled quickshell config does). Set `tabbar_height = 0` to drop
|
alongside (the bundled quickshell config does). Set `tabbar_height = 0` to drop
|
||||||
the built-in strip entirely and let the bar own it.
|
the built-in strip entirely and let the bar own it.
|
||||||
@@ -492,7 +496,8 @@ river's identifier is shared between them.
|
|||||||
| `src/config.zig` | Compile-time configuration |
|
| `src/config.zig` | Compile-time configuration |
|
||||||
| `src/ipc.zig` | Socket server and JSON encoding |
|
| `src/ipc.zig` | Socket server and JSON encoding |
|
||||||
| `src/shm.zig` | memfd buffers for the tab bar, and drawing into them |
|
| `src/shm.zig` | memfd buffers for the tab bar, and drawing into them |
|
||||||
| `src/font.zig` | The built-in 5x8 bitmap font — no Wayland, unit tested |
|
| `src/Face.zig` | The tab bar's font: fontconfig matching, FreeType rasterising |
|
||||||
|
| `src/font.zig` | Measuring and eliding titles — no font needed, unit tested |
|
||||||
| `src/sys.zig` | Thin Linux syscall wrappers |
|
| `src/sys.zig` | Thin Linux syscall wrappers |
|
||||||
|
|
||||||
The single most important invariant: **river only permits window management
|
The single most important invariant: **river only permits window management
|
||||||
|
|||||||
@@ -84,6 +84,10 @@ pub fn build(b: *Build) void {
|
|||||||
exe.root_module.addImport("config", config);
|
exe.root_module.addImport("config", config);
|
||||||
exe.root_module.linkSystemLibrary("wayland-client", .{});
|
exe.root_module.linkSystemLibrary("wayland-client", .{});
|
||||||
exe.root_module.linkSystemLibrary("xkbcommon", .{});
|
exe.root_module.linkSystemLibrary("xkbcommon", .{});
|
||||||
|
// The tab bar draws window titles in a real font: fontconfig picks it,
|
||||||
|
// FreeType rasterises it. See src/Face.zig.
|
||||||
|
exe.root_module.linkSystemLibrary("freetype2", .{});
|
||||||
|
exe.root_module.linkSystemLibrary("fontconfig", .{});
|
||||||
exe.pie = pie;
|
exe.pie = pie;
|
||||||
|
|
||||||
b.installArtifact(exe);
|
b.installArtifact(exe);
|
||||||
|
|||||||
@@ -38,6 +38,8 @@
|
|||||||
wayland
|
wayland
|
||||||
wayland-protocols
|
wayland-protocols
|
||||||
libxkbcommon
|
libxkbcommon
|
||||||
|
freetype
|
||||||
|
fontconfig
|
||||||
];
|
];
|
||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
river
|
river
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
wayland-protocols,
|
wayland-protocols,
|
||||||
wayland-scanner,
|
wayland-scanner,
|
||||||
libxkbcommon,
|
libxkbcommon,
|
||||||
|
freetype,
|
||||||
|
fontconfig,
|
||||||
# Path to a replacement src/config.zig. dwm-style compile-time configuration:
|
# Path to a replacement src/config.zig. dwm-style compile-time configuration:
|
||||||
# att_wm.override { configFile = ./my-config.zig; }
|
# att_wm.override { configFile = ./my-config.zig; }
|
||||||
# Deliberately not called `config`: callPackage would fill that from the
|
# Deliberately not called `config`: callPackage would fill that from the
|
||||||
@@ -38,6 +40,8 @@ stdenv.mkDerivation {
|
|||||||
wayland-protocols
|
wayland-protocols
|
||||||
wayland-scanner
|
wayland-scanner
|
||||||
libxkbcommon
|
libxkbcommon
|
||||||
|
freetype
|
||||||
|
fontconfig
|
||||||
];
|
];
|
||||||
|
|
||||||
zigBuildFlags = [
|
zigBuildFlags = [
|
||||||
|
|||||||
+350
@@ -0,0 +1,350 @@
|
|||||||
|
//! 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
+19
-25
@@ -22,6 +22,7 @@ const Window = @import("Window.zig");
|
|||||||
const shm = @import("shm.zig");
|
const shm = @import("shm.zig");
|
||||||
const color = @import("color.zig");
|
const color = @import("color.zig");
|
||||||
const font = @import("font.zig");
|
const font = @import("font.zig");
|
||||||
|
const Face = @import("Face.zig");
|
||||||
const layout = @import("layout.zig");
|
const layout = @import("layout.zig");
|
||||||
const Box = layout.Box;
|
const Box = layout.Box;
|
||||||
|
|
||||||
@@ -242,7 +243,10 @@ pub const TabBar = struct {
|
|||||||
const sep = color.toArgb8888(config.tab_separator);
|
const sep = color.toArgb8888(config.tab_separator);
|
||||||
buffer.fill(local, sep);
|
buffer.fill(local, sep);
|
||||||
|
|
||||||
const scale = textScale(box.height);
|
// One face, sized once for the whole bar: every tab is the same height.
|
||||||
|
const face = self.wm.tabFace();
|
||||||
|
if (face) |f| f.fitHeight(box.height);
|
||||||
|
|
||||||
for (self.rects.items, windows) |rect, win| {
|
for (self.rects.items, windows) |rect, win| {
|
||||||
const is_focused = focused != null and focused.? == win;
|
const is_focused = focused != null and focused.? == win;
|
||||||
const argb = color.toArgb8888(if (is_focused) config.tab_focused else config.tab_normal);
|
const argb = color.toArgb8888(if (is_focused) config.tab_focused else config.tab_normal);
|
||||||
@@ -256,8 +260,10 @@ pub const TabBar = struct {
|
|||||||
};
|
};
|
||||||
buffer.fill(inner, argb);
|
buffer.fill(inner, argb);
|
||||||
|
|
||||||
const ink = color.toArgb8888(if (is_focused) config.tab_text_focused else config.tab_text_normal);
|
if (face) |f| {
|
||||||
drawLabel(buffer, inner, label(win), scale, ink);
|
const ink = color.toArgb8888(if (is_focused) config.tab_text_focused else config.tab_text_normal);
|
||||||
|
drawLabel(buffer, f, inner, label(win), ink);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Translate the hit rectangles into global coordinates for click
|
// Translate the hit rectangles into global coordinates for click
|
||||||
@@ -304,34 +310,22 @@ pub const TabBar = struct {
|
|||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whole-pixel scale for the font: the largest that leaves a little air
|
|
||||||
/// above and below in a bar this tall. `config.tab_font_scale` overrides
|
|
||||||
/// it for anyone wanting big letters in a thin bar, or the reverse.
|
|
||||||
fn textScale(height: i32) i32 {
|
|
||||||
if (config.tab_font_scale > 0) return config.tab_font_scale;
|
|
||||||
var s: i32 = 1;
|
|
||||||
while (s < 4 and font.cell_height * (s + 1) + 4 <= height) s += 1;
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Centre a title in one tab, eliding it with an ellipsis when the tab is
|
/// Centre a title in one tab, eliding it with an ellipsis when the tab is
|
||||||
/// too narrow to hold it.
|
/// too narrow to hold it.
|
||||||
fn drawLabel(buffer: *shm.Buffer, tab: Box, text: []const u8, scale: i32, argb: u32) void {
|
fn drawLabel(buffer: *shm.Buffer, face: *Face, tab: Box, text: []const u8, argb: u32) void {
|
||||||
const pad = 2 * scale;
|
// Enough air either side that neighbouring titles do not read as one.
|
||||||
const avail = tab.width - 2 * pad;
|
const pad = @divFloor(face.px, 2) + 2;
|
||||||
|
const avail = tab.width - 2 * @as(i32, @intCast(pad));
|
||||||
if (avail <= 0) return;
|
if (avail <= 0) return;
|
||||||
|
|
||||||
const f = font.fit(text, font.cellsForWidth(avail, scale));
|
const f = font.fit(text, font.fixed(avail), face);
|
||||||
const cells = f.cells + @intFromBool(f.elided);
|
if (f.end == 0 and !f.elided) return;
|
||||||
if (cells == 0) return;
|
|
||||||
|
|
||||||
const x = tab.x + @divFloor(tab.width - font.textWidth(cells, scale), 2);
|
const x = tab.x + @divFloor(tab.width - font.pixels(f.width), 2);
|
||||||
// Centre the part above the baseline; the descender row may hang into
|
const y = tab.y + face.baselineIn(tab.height);
|
||||||
// the padding, which is what it is for.
|
|
||||||
const y = tab.y + @divFloor(tab.height - (font.cell_height - 1) * scale, 2);
|
|
||||||
|
|
||||||
const pen = buffer.drawText(text[0..f.end], x, y, scale, argb, tab);
|
const pen = buffer.drawText(face, text[0..f.end], x, y, argb, tab);
|
||||||
if (f.elided) buffer.drawGlyph(font.ellipsis, pen, y, scale, argb, tab);
|
if (f.elided) _ = buffer.drawGlyph(face, font.ellipsis, pen, y, argb, tab);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Which window's tab covers this global coordinate, if any.
|
/// Which window's tab covers this global coordinate, if any.
|
||||||
|
|||||||
+24
@@ -28,6 +28,7 @@ const Output = @import("Output.zig");
|
|||||||
const Seat = @import("Seat.zig");
|
const Seat = @import("Seat.zig");
|
||||||
const InputManager = @import("InputManager.zig");
|
const InputManager = @import("InputManager.zig");
|
||||||
const Ipc = @import("ipc.zig").Ipc;
|
const Ipc = @import("ipc.zig").Ipc;
|
||||||
|
const Face = @import("Face.zig");
|
||||||
const color = @import("color.zig");
|
const color = @import("color.zig");
|
||||||
const layout = @import("layout.zig");
|
const layout = @import("layout.zig");
|
||||||
const Box = layout.Box;
|
const Box = layout.Box;
|
||||||
@@ -97,6 +98,11 @@ repeat_fd: sys.fd_t,
|
|||||||
repeat_seat: ?*Seat = null,
|
repeat_seat: ?*Seat = null,
|
||||||
repeat_index: ?usize = null,
|
repeat_index: ?usize = null,
|
||||||
|
|
||||||
|
/// The font the tab bar draws titles in, opened on the first draw that needs
|
||||||
|
/// one so that a configuration with no tab bar never touches fontconfig.
|
||||||
|
tab_face: ?Face = null,
|
||||||
|
tab_face_tried: bool = false,
|
||||||
|
|
||||||
/// Scratch buffers reused by the layout pass to avoid per-frame allocation.
|
/// Scratch buffers reused by the layout pass to avoid per-frame allocation.
|
||||||
scratch_windows: std.ArrayList(*Window) = .empty,
|
scratch_windows: std.ArrayList(*Window) = .empty,
|
||||||
scratch_cells: std.ArrayList(Box) = .empty,
|
scratch_cells: std.ArrayList(Box) = .empty,
|
||||||
@@ -221,8 +227,26 @@ fn freeGlobals(self: *Wm) void {
|
|||||||
self.shm = null;
|
self.shm = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The tab bar font, or null if there is none to be had. Opening it is tried
|
||||||
|
/// exactly once: a machine with no fonts installed gets bare tabs and one line
|
||||||
|
/// in the log, not a failure to start and not a message every frame.
|
||||||
|
pub fn tabFace(self: *Wm) ?*Face {
|
||||||
|
if (!self.tab_face_tried) {
|
||||||
|
self.tab_face_tried = true;
|
||||||
|
self.tab_face = Face.open(self.gpa, config.tab_font) catch |err| blk: {
|
||||||
|
std.log.err("no font for \"{s}\" ({s}); tab titles will not be drawn", .{
|
||||||
|
config.tab_font,
|
||||||
|
@errorName(err),
|
||||||
|
});
|
||||||
|
break :blk null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return if (self.tab_face) |*f| f else null;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Wm) void {
|
pub fn deinit(self: *Wm) void {
|
||||||
const gpa = self.gpa;
|
const gpa = self.gpa;
|
||||||
|
if (self.tab_face) |*f| f.deinit();
|
||||||
|
|
||||||
for (self.windows.items) |win| win.destroy();
|
for (self.windows.items) |win| win.destroy();
|
||||||
// Outputs hold references into wl_outputs, so they must go first.
|
// Outputs hold references into wl_outputs, so they must go first.
|
||||||
|
|||||||
+4
-4
@@ -44,10 +44,10 @@ pub const tab_separator: u32 = 0x1a1a1aff;
|
|||||||
/// Window titles are drawn on the tabs in these colours.
|
/// Window titles are drawn on the tabs in these colours.
|
||||||
pub const tab_text_focused: u32 = 0xffffffff;
|
pub const tab_text_focused: u32 = 0xffffffff;
|
||||||
pub const tab_text_normal: u32 = 0xbbbbbbff;
|
pub const tab_text_normal: u32 = 0xbbbbbbff;
|
||||||
/// Whole-pixel scale for the 5x8 built-in font the titles are drawn in: 2
|
/// The font titles are drawn in, as a fontconfig pattern — the same syntax
|
||||||
/// gives 10x16 letters. Zero picks the largest scale `tabbar_height` has room
|
/// `fc-match` takes, so `fc-match 'sans:size=11'` shows what this will resolve
|
||||||
/// for, which is what you want after changing the bar height.
|
/// to. The size is trimmed to whatever `tabbar_height` has room for.
|
||||||
pub const tab_font_scale: i32 = 0;
|
pub const tab_font: [:0]const u8 = "sans:size=11";
|
||||||
|
|
||||||
// ─── Layout ──────────────────────────────────────────────────────────────────
|
// ─── Layout ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
+53
-190
@@ -1,92 +1,86 @@
|
|||||||
//! A 5x8 bitmap font, and the text measuring that goes with it.
|
//! Text measuring for the tab bar: how much of a title fits, and where it has
|
||||||
|
//! to be cut short.
|
||||||
//!
|
//!
|
||||||
//! The tab bar has to label its tabs, and a window manager that draws exactly
|
//! Deliberately free of FreeType — it asks whatever it is handed how wide a
|
||||||
//! one strip of pixels has no business linking a font stack for it. So the
|
//! codepoint is and does the arithmetic, so the eliding rules can be tested
|
||||||
//! glyphs live here as data: printable ASCII plus an ellipsis for elided
|
//! without a font on the machine running the tests. `Face` is what supplies the
|
||||||
//! titles, drawn as bit rows and expanded by whole pixels when scaled.
|
//! widths in the running window manager.
|
||||||
//!
|
//!
|
||||||
//! Anything else — every non-ASCII codepoint a title may hold — renders as the
|
//! Widths are 26.6 fixed point, FreeType's unit: whole pixels are too coarse to
|
||||||
//! fallback box, which is at least honest about being a character we cannot
|
//! accumulate a string's width in without the spacing drifting.
|
||||||
//! draw.
|
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
pub const cell_width = 5;
|
/// Cut titles are ended with this rather than three periods, which at tab-bar
|
||||||
pub const cell_height = 8;
|
/// sizes look like a smudge.
|
||||||
/// Columns from one cell to the next: the glyph plus a one column gap.
|
|
||||||
pub const advance = cell_width + 1;
|
|
||||||
|
|
||||||
/// One glyph: `cell_height` rows, bit `cell_width - 1` the leftmost column.
|
|
||||||
pub const Glyph = [cell_height]u8;
|
|
||||||
|
|
||||||
pub const first_char = 0x20;
|
|
||||||
pub const last_char = 0x7e;
|
|
||||||
|
|
||||||
pub const ellipsis: u21 = '…';
|
pub const ellipsis: u21 = '…';
|
||||||
|
|
||||||
/// Rows for a codepoint. Control characters come back blank rather than as a
|
/// Whole pixels to 26.6.
|
||||||
/// box, so a stray newline in a title does not turn into visible noise.
|
pub fn fixed(px: i32) i32 {
|
||||||
pub fn glyph(cp: u21) *const Glyph {
|
return px * 64;
|
||||||
if (cp < first_char or cp == 0x7f) return &glyphs[0];
|
|
||||||
if (cp <= last_char) return &glyphs[cp - first_char];
|
|
||||||
if (cp == ellipsis) return &ellipsis_glyph;
|
|
||||||
return &fallback_glyph;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Width in pixels of `cells` characters, without the trailing gap.
|
/// 26.6 to whole pixels, rounded up: a width is how much room the text needs.
|
||||||
pub fn textWidth(cells: usize, scale: i32) i32 {
|
pub fn pixels(width: i32) i32 {
|
||||||
if (cells == 0) return 0;
|
return @divFloor(width + 63, 64);
|
||||||
const n: i32 = @intCast(cells);
|
|
||||||
return (n * advance - 1) * scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// How many characters fit in a strip that wide.
|
|
||||||
pub fn cellsForWidth(width: i32, scale: i32) usize {
|
|
||||||
if (width <= 0 or scale <= 0) return 0;
|
|
||||||
// The last cell needs no trailing gap, so lend it one.
|
|
||||||
const cells = @divFloor(width + scale, advance * scale);
|
|
||||||
return if (cells <= 0) 0 else @intCast(cells);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const Fit = struct {
|
pub const Fit = struct {
|
||||||
/// Bytes of the string to draw.
|
/// Bytes of the string to draw.
|
||||||
end: usize,
|
end: usize,
|
||||||
/// Characters those bytes come to, an ellipsis not counted.
|
/// Width in 26.6 of what will be drawn, the ellipsis included.
|
||||||
cells: usize,
|
width: i32,
|
||||||
/// The string was too long; draw an ellipsis after `end`.
|
/// The string was too long; draw an ellipsis after `end`.
|
||||||
elided: bool,
|
elided: bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Fit a string into `max_cells` character cells, giving up the last cell to
|
/// Fit a string into `max_width` 26.6 units, giving up as many trailing
|
||||||
/// an ellipsis when the string does not fit whole.
|
/// characters as it takes to leave room for an ellipsis when the string does
|
||||||
pub fn fit(text: []const u8, max_cells: usize) Fit {
|
/// not fit whole.
|
||||||
if (max_cells == 0) return .{ .end = 0, .cells = 0, .elided = false };
|
///
|
||||||
|
/// `measure` is anything with `advance(u21) i32` and `ellipsisAdvance() i32`.
|
||||||
|
pub fn fit(text: []const u8, max_width: i32, measure: anytype) Fit {
|
||||||
|
const empty: Fit = .{ .end = 0, .width = 0, .elided = false };
|
||||||
|
if (max_width <= 0) return empty;
|
||||||
|
|
||||||
|
const dots = measure.ellipsisAdvance();
|
||||||
|
|
||||||
var it: Iterator = .{ .bytes = text };
|
var it: Iterator = .{ .bytes = text };
|
||||||
var cells: usize = 0;
|
var width: i32 = 0;
|
||||||
// Where the string is cut if it turns out not to fit: one cell short, so
|
// The longest prefix seen so far that still leaves room for an ellipsis.
|
||||||
// the ellipsis has somewhere to go.
|
|
||||||
var cut: usize = 0;
|
var cut: usize = 0;
|
||||||
|
var cut_width: i32 = 0;
|
||||||
|
var cut_fits = dots <= max_width;
|
||||||
|
|
||||||
while (it.next()) |_| {
|
while (it.next()) |cp| {
|
||||||
cells += 1;
|
const adv = measure.advance(cp);
|
||||||
if (cells + 1 == max_cells) cut = it.i;
|
if (width + adv > max_width) {
|
||||||
// One character past the end is what proves it does not fit.
|
// Not even an ellipsis fits: better to draw nothing than a lone
|
||||||
if (cells > max_cells) {
|
// pair of dots where a title should be.
|
||||||
return .{ .end = cut, .cells = max_cells - 1, .elided = true };
|
if (!cut_fits) return empty;
|
||||||
|
return .{ .end = cut, .width = cut_width + dots, .elided = true };
|
||||||
|
}
|
||||||
|
width += adv;
|
||||||
|
if (width + dots <= max_width) {
|
||||||
|
cut = it.i;
|
||||||
|
cut_width = width;
|
||||||
|
cut_fits = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return .{ .end = text.len, .cells = cells, .elided = false };
|
return .{ .end = text.len, .width = width, .elided = false };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// UTF-8 iteration that never fails: anything malformed comes back as the
|
/// UTF-8 iteration that never fails: anything malformed comes back as the
|
||||||
/// replacement character, which draws as the fallback box. Titles come from
|
/// replacement character, which draws as whatever the face has for it. Titles
|
||||||
/// arbitrary clients, so refusing to render one is not an option.
|
/// come from arbitrary clients, so refusing to render one is not an option.
|
||||||
|
///
|
||||||
|
/// Control characters come back as spaces, so that a stray newline in a title
|
||||||
|
/// is a gap rather than a .notdef box.
|
||||||
pub const Iterator = struct {
|
pub const Iterator = struct {
|
||||||
bytes: []const u8,
|
bytes: []const u8,
|
||||||
i: usize = 0,
|
i: usize = 0,
|
||||||
|
|
||||||
const replacement: u21 = 0xfffd;
|
pub const replacement: u21 = 0xfffd;
|
||||||
|
|
||||||
pub fn next(self: *Iterator) ?u21 {
|
pub fn next(self: *Iterator) ?u21 {
|
||||||
if (self.i >= self.bytes.len) return null;
|
if (self.i >= self.bytes.len) return null;
|
||||||
@@ -104,137 +98,6 @@ pub const Iterator = struct {
|
|||||||
return replacement;
|
return replacement;
|
||||||
};
|
};
|
||||||
self.i += len;
|
self.i += len;
|
||||||
return cp;
|
return if (cp < 0x20 or cp == 0x7f) ' ' else cp;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── Glyph data ──────────────────────────────────────────────────────────────
|
|
||||||
//
|
|
||||||
// Written as pictures rather than hex so a wrong pixel is visible in review.
|
|
||||||
// Rows 0..6 hold the glyph, with the baseline under row 6; row 7 is there for
|
|
||||||
// the descenders of g, j, p, q and y.
|
|
||||||
|
|
||||||
fn parse(comptime picture: [cell_height][]const u8) Glyph {
|
|
||||||
var rows: Glyph = @splat(0);
|
|
||||||
for (picture, 0..) |line, y| {
|
|
||||||
if (line.len != cell_width) @compileError("glyph row must be " ++
|
|
||||||
std.fmt.comptimePrint("{d}", .{cell_width}) ++ " columns wide");
|
|
||||||
for (line, 0..) |ch, x| {
|
|
||||||
if (ch == '#') rows[y] |= @as(u8, 1) << @intCast(cell_width - 1 - x);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fallback_glyph = parse(.{ ".....", "#####", "#...#", "#...#", "#...#", "#...#", "#####", "....." });
|
|
||||||
const ellipsis_glyph = parse(.{ ".....", ".....", ".....", ".....", ".....", ".....", "#.#.#", "....." });
|
|
||||||
|
|
||||||
const glyphs = blk: {
|
|
||||||
// Ninety-five glyphs of forty pixels each is a lot of comptime looping.
|
|
||||||
@setEvalBranchQuota(20_000);
|
|
||||||
var table: [last_char - first_char + 1]Glyph = undefined;
|
|
||||||
for (art, 0..) |a, i| table[i] = parse(a);
|
|
||||||
break :blk table;
|
|
||||||
};
|
|
||||||
|
|
||||||
const art = [_][cell_height][]const u8{
|
|
||||||
.{ ".....", ".....", ".....", ".....", ".....", ".....", ".....", "....." }, // space
|
|
||||||
.{ "..#..", "..#..", "..#..", "..#..", "..#..", ".....", "..#..", "....." }, // !
|
|
||||||
.{ ".#.#.", ".#.#.", ".#.#.", ".....", ".....", ".....", ".....", "....." }, // "
|
|
||||||
.{ ".#.#.", ".#.#.", "#####", ".#.#.", "#####", ".#.#.", ".#.#.", "....." }, // #
|
|
||||||
.{ "..#..", ".####", "#.#..", ".###.", "..#.#", "####.", "..#..", "....." }, // $
|
|
||||||
.{ "##...", "##..#", "...#.", "..#..", ".#...", "#..##", "...##", "....." }, // %
|
|
||||||
.{ ".##..", "#..#.", "#.#..", ".#...", "#.#.#", "#..#.", ".##.#", "....." }, // &
|
|
||||||
.{ "..#..", "..#..", "..#..", ".....", ".....", ".....", ".....", "....." }, // '
|
|
||||||
.{ "...#.", "..#..", ".#...", ".#...", ".#...", "..#..", "...#.", "....." }, // (
|
|
||||||
.{ ".#...", "..#..", "...#.", "...#.", "...#.", "..#..", ".#...", "....." }, // )
|
|
||||||
.{ ".....", "#.#.#", ".###.", "#####", ".###.", "#.#.#", ".....", "....." }, // *
|
|
||||||
.{ ".....", "..#..", "..#..", "#####", "..#..", "..#..", ".....", "....." }, // +
|
|
||||||
.{ ".....", ".....", ".....", ".....", ".....", "..##.", "..##.", ".#..." }, // ,
|
|
||||||
.{ ".....", ".....", ".....", "#####", ".....", ".....", ".....", "....." }, // -
|
|
||||||
.{ ".....", ".....", ".....", ".....", ".....", "..##.", "..##.", "....." }, // .
|
|
||||||
.{ "....#", "...#.", "...#.", "..#..", ".#...", ".#...", "#....", "....." }, // /
|
|
||||||
.{ ".###.", "#...#", "#..##", "#.#.#", "##..#", "#...#", ".###.", "....." }, // 0
|
|
||||||
.{ "..#..", ".##..", "..#..", "..#..", "..#..", "..#..", ".###.", "....." }, // 1
|
|
||||||
.{ ".###.", "#...#", "....#", "...#.", "..#..", ".#...", "#####", "....." }, // 2
|
|
||||||
.{ "#####", "...#.", "..#..", "...#.", "....#", "#...#", ".###.", "....." }, // 3
|
|
||||||
.{ "...#.", "..##.", ".#.#.", "#..#.", "#####", "...#.", "...#.", "....." }, // 4
|
|
||||||
.{ "#####", "#....", "####.", "....#", "....#", "#...#", ".###.", "....." }, // 5
|
|
||||||
.{ "..##.", ".#...", "#....", "####.", "#...#", "#...#", ".###.", "....." }, // 6
|
|
||||||
.{ "#####", "....#", "...#.", "..#..", ".#...", ".#...", ".#...", "....." }, // 7
|
|
||||||
.{ ".###.", "#...#", "#...#", ".###.", "#...#", "#...#", ".###.", "....." }, // 8
|
|
||||||
.{ ".###.", "#...#", "#...#", ".####", "....#", "...#.", ".##..", "....." }, // 9
|
|
||||||
.{ ".....", ".....", "..##.", "..##.", ".....", "..##.", "..##.", "....." }, // :
|
|
||||||
.{ ".....", ".....", "..##.", "..##.", ".....", "..##.", "..##.", ".#..." }, // ;
|
|
||||||
.{ ".....", "...#.", "..#..", ".#...", "..#..", "...#.", ".....", "....." }, // <
|
|
||||||
.{ ".....", ".....", "#####", ".....", "#####", ".....", ".....", "....." }, // =
|
|
||||||
.{ ".....", ".#...", "..#..", "...#.", "..#..", ".#...", ".....", "....." }, // >
|
|
||||||
.{ ".###.", "#...#", "....#", "...#.", "..#..", ".....", "..#..", "....." }, // ?
|
|
||||||
.{ ".###.", "#...#", "#.###", "#.#.#", "#.###", "#....", ".###.", "....." }, // @
|
|
||||||
.{ ".###.", "#...#", "#...#", "#####", "#...#", "#...#", "#...#", "....." }, // A
|
|
||||||
.{ "####.", "#...#", "#...#", "####.", "#...#", "#...#", "####.", "....." }, // B
|
|
||||||
.{ ".###.", "#...#", "#....", "#....", "#....", "#...#", ".###.", "....." }, // C
|
|
||||||
.{ "###..", "#..#.", "#...#", "#...#", "#...#", "#..#.", "###..", "....." }, // D
|
|
||||||
.{ "#####", "#....", "#....", "####.", "#....", "#....", "#####", "....." }, // E
|
|
||||||
.{ "#####", "#....", "#....", "####.", "#....", "#....", "#....", "....." }, // F
|
|
||||||
.{ ".###.", "#...#", "#....", "#.###", "#...#", "#...#", ".####", "....." }, // G
|
|
||||||
.{ "#...#", "#...#", "#...#", "#####", "#...#", "#...#", "#...#", "....." }, // H
|
|
||||||
.{ ".###.", "..#..", "..#..", "..#..", "..#..", "..#..", ".###.", "....." }, // I
|
|
||||||
.{ "..###", "...#.", "...#.", "...#.", "...#.", "#..#.", ".##..", "....." }, // J
|
|
||||||
.{ "#...#", "#..#.", "#.#..", "##...", "#.#..", "#..#.", "#...#", "....." }, // K
|
|
||||||
.{ "#....", "#....", "#....", "#....", "#....", "#....", "#####", "....." }, // L
|
|
||||||
.{ "#...#", "##.##", "#.#.#", "#.#.#", "#...#", "#...#", "#...#", "....." }, // M
|
|
||||||
.{ "#...#", "#...#", "##..#", "#.#.#", "#..##", "#...#", "#...#", "....." }, // N
|
|
||||||
.{ ".###.", "#...#", "#...#", "#...#", "#...#", "#...#", ".###.", "....." }, // O
|
|
||||||
.{ "####.", "#...#", "#...#", "####.", "#....", "#....", "#....", "....." }, // P
|
|
||||||
.{ ".###.", "#...#", "#...#", "#...#", "#.#.#", "#..#.", ".##.#", "....." }, // Q
|
|
||||||
.{ "####.", "#...#", "#...#", "####.", "#.#..", "#..#.", "#...#", "....." }, // R
|
|
||||||
.{ ".####", "#....", "#....", ".###.", "....#", "....#", "####.", "....." }, // S
|
|
||||||
.{ "#####", "..#..", "..#..", "..#..", "..#..", "..#..", "..#..", "....." }, // T
|
|
||||||
.{ "#...#", "#...#", "#...#", "#...#", "#...#", "#...#", ".###.", "....." }, // U
|
|
||||||
.{ "#...#", "#...#", "#...#", "#...#", "#...#", ".#.#.", "..#..", "....." }, // V
|
|
||||||
.{ "#...#", "#...#", "#...#", "#.#.#", "#.#.#", "##.##", "#...#", "....." }, // W
|
|
||||||
.{ "#...#", "#...#", ".#.#.", "..#..", ".#.#.", "#...#", "#...#", "....." }, // X
|
|
||||||
.{ "#...#", "#...#", ".#.#.", "..#..", "..#..", "..#..", "..#..", "....." }, // Y
|
|
||||||
.{ "#####", "....#", "...#.", "..#..", ".#...", "#....", "#####", "....." }, // Z
|
|
||||||
.{ ".###.", ".#...", ".#...", ".#...", ".#...", ".#...", ".###.", "....." }, // [
|
|
||||||
.{ "#....", "#....", ".#...", "..#..", "...#.", "....#", "....#", "....." }, // backslash
|
|
||||||
.{ ".###.", "...#.", "...#.", "...#.", "...#.", "...#.", ".###.", "....." }, // ]
|
|
||||||
.{ "..#..", ".#.#.", "#...#", ".....", ".....", ".....", ".....", "....." }, // ^
|
|
||||||
.{ ".....", ".....", ".....", ".....", ".....", ".....", ".....", "#####" }, // _
|
|
||||||
.{ ".#...", "..#..", ".....", ".....", ".....", ".....", ".....", "....." }, // `
|
|
||||||
.{ ".....", ".....", ".###.", "....#", ".####", "#...#", ".####", "....." }, // a
|
|
||||||
.{ "#....", "#....", "####.", "#...#", "#...#", "#...#", "####.", "....." }, // b
|
|
||||||
.{ ".....", ".....", ".###.", "#...#", "#....", "#...#", ".###.", "....." }, // c
|
|
||||||
.{ "....#", "....#", ".####", "#...#", "#...#", "#...#", ".####", "....." }, // d
|
|
||||||
.{ ".....", ".....", ".###.", "#...#", "#####", "#....", ".###.", "....." }, // e
|
|
||||||
.{ "..##.", ".#..#", ".#...", "####.", ".#...", ".#...", ".#...", "....." }, // f
|
|
||||||
.{ ".....", ".....", ".####", "#...#", "#...#", ".####", "....#", ".###." }, // g
|
|
||||||
.{ "#....", "#....", "####.", "#...#", "#...#", "#...#", "#...#", "....." }, // h
|
|
||||||
.{ "..#..", ".....", ".##..", "..#..", "..#..", "..#..", ".###.", "....." }, // i
|
|
||||||
.{ "...#.", ".....", "..##.", "...#.", "...#.", "...#.", "#..#.", ".##.." }, // j
|
|
||||||
.{ "#....", "#....", "#..#.", "#.#..", "##...", "#.#..", "#..#.", "....." }, // k
|
|
||||||
.{ ".##..", "..#..", "..#..", "..#..", "..#..", "..#..", ".###.", "....." }, // l
|
|
||||||
.{ ".....", ".....", "##.#.", "#.#.#", "#.#.#", "#...#", "#...#", "....." }, // m
|
|
||||||
.{ ".....", ".....", "####.", "#...#", "#...#", "#...#", "#...#", "....." }, // n
|
|
||||||
.{ ".....", ".....", ".###.", "#...#", "#...#", "#...#", ".###.", "....." }, // o
|
|
||||||
.{ ".....", ".....", "####.", "#...#", "#...#", "####.", "#....", "#...." }, // p
|
|
||||||
.{ ".....", ".....", ".####", "#...#", "#...#", ".####", "....#", "....#" }, // q
|
|
||||||
.{ ".....", ".....", "#.##.", "##..#", "#....", "#....", "#....", "....." }, // r
|
|
||||||
.{ ".....", ".....", ".####", "#....", ".###.", "....#", "####.", "....." }, // s
|
|
||||||
.{ ".#...", ".#...", "####.", ".#...", ".#...", ".#..#", "..##.", "....." }, // t
|
|
||||||
.{ ".....", ".....", "#...#", "#...#", "#...#", "#...#", ".####", "....." }, // u
|
|
||||||
.{ ".....", ".....", "#...#", "#...#", "#...#", ".#.#.", "..#..", "....." }, // v
|
|
||||||
.{ ".....", ".....", "#...#", "#...#", "#.#.#", "#.#.#", ".#.#.", "....." }, // w
|
|
||||||
.{ ".....", ".....", "#...#", ".#.#.", "..#..", ".#.#.", "#...#", "....." }, // x
|
|
||||||
.{ ".....", ".....", "#...#", "#...#", "#...#", ".####", "....#", ".###." }, // y
|
|
||||||
.{ ".....", ".....", "#####", "...#.", "..#..", ".#...", "#####", "....." }, // z
|
|
||||||
.{ "...##", "..#..", "..#..", ".#...", "..#..", "..#..", "...##", "....." }, // {
|
|
||||||
.{ "..#..", "..#..", "..#..", "..#..", "..#..", "..#..", "..#..", "....." }, // |
|
|
||||||
.{ "##...", "..#..", "..#..", "...#.", "..#..", "..#..", "##...", "....." }, // }
|
|
||||||
.{ ".....", ".....", ".##..", "#..##", ".....", ".....", ".....", "....." }, // ~
|
|
||||||
};
|
|
||||||
|
|
||||||
comptime {
|
|
||||||
std.debug.assert(art.len == last_char - first_char + 1);
|
|
||||||
}
|
|
||||||
|
|||||||
+63
-27
@@ -1,8 +1,8 @@
|
|||||||
//! Minimal wl_shm buffer pool for the tab bar.
|
//! Minimal wl_shm buffer pool for the tab bar.
|
||||||
//!
|
//!
|
||||||
//! The tab bar is the only thing att_wm draws itself, and it draws nothing but
|
//! The tab bar is the only thing att_wm draws itself, and it draws nothing but
|
||||||
//! solid rectangles and text in the bitmap font from `font.zig`, so this
|
//! solid rectangles and antialiased text, so this deliberately stops at
|
||||||
//! deliberately stops at "memfd, mmap, fill" rather than pulling in pixman.
|
//! "memfd, mmap, fill, blend" rather than pulling in pixman.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const posix = std.posix;
|
const posix = std.posix;
|
||||||
@@ -13,6 +13,7 @@ const wayland = @import("wayland");
|
|||||||
const wl = wayland.client.wl;
|
const wl = wayland.client.wl;
|
||||||
|
|
||||||
const font = @import("font.zig");
|
const font = @import("font.zig");
|
||||||
|
const Face = @import("Face.zig");
|
||||||
const layout = @import("layout.zig");
|
const layout = @import("layout.zig");
|
||||||
const Box = layout.Box;
|
const Box = layout.Box;
|
||||||
|
|
||||||
@@ -69,41 +70,76 @@ pub const Buffer = struct {
|
|||||||
self.fill(.{ .x = x0, .y = y0, .width = x1 - x0, .height = y1 - y0 }, argb);
|
self.fill(.{ .x = x0, .y = y0, .width = x1 - x0, .height = y1 - y0 }, argb);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw text with the top left of its first cell at (x, y), clipped to
|
/// Draw text along the baseline `y`, starting at `x`, clipped to `clip`,
|
||||||
/// `clip`, and return the x the next cell would start at. The font has no
|
/// and return the 26.6 pen position the next glyph would start at.
|
||||||
/// antialiasing, so lit pixels are simply written: nothing to blend.
|
///
|
||||||
pub fn drawText(self: *Buffer, text: []const u8, x: i32, y: i32, scale: i32, argb: u32, clip: Box) i32 {
|
/// The pen is carried in 26.6 and handed to the face as a subpixel offset
|
||||||
var pen = x;
|
/// rather than rounded per glyph: rounding is what makes rendered text look
|
||||||
|
/// unevenly spaced, and it is cheap to avoid.
|
||||||
|
pub fn drawText(self: *Buffer, face: *Face, text: []const u8, x: i32, y: i32, argb: u32, clip: Box) i32 {
|
||||||
|
var pen = font.fixed(x);
|
||||||
var it: font.Iterator = .{ .bytes = text };
|
var it: font.Iterator = .{ .bytes = text };
|
||||||
while (it.next()) |cp| {
|
while (it.next()) |cp| {
|
||||||
// Stop as soon as the pen leaves the clip rather than walking the
|
// Stop as soon as the pen leaves the clip rather than rasterising
|
||||||
// rest of a long title a pixel at a time.
|
// the rest of a long title into nothing.
|
||||||
if (pen >= clip.x + clip.width) break;
|
if (pen >= font.fixed(clip.x + clip.width)) break;
|
||||||
self.drawGlyph(cp, pen, y, scale, argb, clip);
|
pen = self.drawGlyph(face, cp, pen, y, argb, clip);
|
||||||
pen += font.advance * scale;
|
|
||||||
}
|
}
|
||||||
return pen;
|
return pen;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn drawGlyph(self: *Buffer, cp: u21, x: i32, y: i32, scale: i32, argb: u32, clip: Box) void {
|
/// Draw one glyph with its origin at the 26.6 pen position `pen` on the
|
||||||
const rows = font.glyph(cp);
|
/// baseline `y`, and return the pen position after it.
|
||||||
for (rows, 0..) |bits, row| {
|
pub fn drawGlyph(self: *Buffer, face: *Face, cp: u21, pen: i32, y: i32, argb: u32, clip: Box) i32 {
|
||||||
if (bits == 0) continue;
|
const advance = face.advance(cp);
|
||||||
const py = y + @as(i32, @intCast(row)) * scale;
|
if (face.render(cp, pen & 63)) |g| {
|
||||||
var col: u3 = 0;
|
self.blendCoverage(g, @divFloor(pen, 64) + g.left, y - g.top, argb, clip);
|
||||||
while (col < font.cell_width) : (col += 1) {
|
}
|
||||||
const mask = @as(u8, 1) << @intCast(font.cell_width - 1 - col);
|
return pen + advance;
|
||||||
if (bits & mask == 0) continue;
|
}
|
||||||
self.fillClipped(.{
|
|
||||||
.x = x + @as(i32, col) * scale,
|
/// Composite a glyph's coverage over what is already in the buffer, with
|
||||||
.y = py,
|
/// (x, y) the top left of its bitmap.
|
||||||
.width = scale,
|
fn blendCoverage(self: *Buffer, g: Face.Glyph, x: i32, y: i32, argb: u32, clip: Box) void {
|
||||||
.height = scale,
|
const x0 = @max(@max(0, clip.x), x);
|
||||||
}, clip, argb);
|
const y0 = @max(@max(0, clip.y), y);
|
||||||
|
const x1 = @min(@min(self.width, clip.x + clip.width), x + @as(i32, @intCast(g.width)));
|
||||||
|
const y1 = @min(@min(self.height, clip.y + clip.height), y + @as(i32, @intCast(g.height)));
|
||||||
|
if (x1 <= x0 or y1 <= y0) return;
|
||||||
|
|
||||||
|
const px = self.pixels();
|
||||||
|
const stride: usize = @intCast(self.width);
|
||||||
|
var row = y0;
|
||||||
|
while (row < y1) : (row += 1) {
|
||||||
|
const src = g.coverage[@as(usize, @intCast(row - y)) * g.pitch ..];
|
||||||
|
const dst_row = @as(usize, @intCast(row)) * stride;
|
||||||
|
var col = x0;
|
||||||
|
while (col < x1) : (col += 1) {
|
||||||
|
const cov = src[@intCast(col - x)];
|
||||||
|
if (cov == 0) continue;
|
||||||
|
const dst = &px[dst_row + @as(usize, @intCast(col))];
|
||||||
|
dst.* = if (cov == 0xff) argb else blend(argb, dst.*, cov);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `src` over `dst` at coverage `cov`. Both are premultiplied, so this is
|
||||||
|
/// the same arithmetic on all four channels, alpha included.
|
||||||
|
fn blend(src: u32, dst: u32, cov: u8) u32 {
|
||||||
|
const a: u32 = cov;
|
||||||
|
const inv: u32 = 255 - a;
|
||||||
|
var out: u32 = 0;
|
||||||
|
for ([_]u5{ 0, 8, 16, 24 }) |shift| {
|
||||||
|
const s = (src >> shift) & 0xff;
|
||||||
|
const d = (dst >> shift) & 0xff;
|
||||||
|
// +127 to round to nearest; text over a dark tab loses a level to
|
||||||
|
// truncation otherwise, and thin stems are made of those levels.
|
||||||
|
const v = (s * a + d * inv + 127) / 255;
|
||||||
|
out |= v << shift;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
fn deinit(self: *Buffer, gpa: Allocator) void {
|
fn deinit(self: *Buffer, gpa: Allocator) void {
|
||||||
self.wl_buffer.destroy();
|
self.wl_buffer.destroy();
|
||||||
posix.munmap(self.data);
|
posix.munmap(self.data);
|
||||||
|
|||||||
+73
-42
@@ -426,44 +426,87 @@ test "map_to_output merges like any other setting" {
|
|||||||
|
|
||||||
// ─── tab titles ──────────────────────────────────────────────────────────────
|
// ─── tab titles ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Stands in for a `Face`, so the eliding rules can be checked without a font
|
||||||
|
/// on the machine running the tests. One cell wide per character, in the 26.6
|
||||||
|
/// units `font.fit` counts in.
|
||||||
|
const Mono = struct {
|
||||||
|
pub fn advance(_: Mono, _: u21) i32 {
|
||||||
|
return font.fixed(1);
|
||||||
|
}
|
||||||
|
pub fn ellipsisAdvance(self: Mono) i32 {
|
||||||
|
return self.advance(font.ellipsis);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A proportional face: 'i' is half a cell, the ellipsis two. Real faces are
|
||||||
|
/// like this, and it is where fixed-cell arithmetic goes wrong.
|
||||||
|
const Prop = struct {
|
||||||
|
pub fn advance(_: Prop, cp: u21) i32 {
|
||||||
|
return switch (cp) {
|
||||||
|
'i' => @divExact(font.fixed(1), 2),
|
||||||
|
font.ellipsis => font.fixed(2),
|
||||||
|
else => font.fixed(1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
pub fn ellipsisAdvance(self: Prop) i32 {
|
||||||
|
return self.advance(font.ellipsis);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
test "a title that fits is drawn whole" {
|
test "a title that fits is drawn whole" {
|
||||||
const f = font.fit("foot", 10);
|
const f = font.fit("foot", font.fixed(10), Mono{});
|
||||||
try testing.expectEqual(@as(usize, 4), f.end);
|
try testing.expectEqual(@as(usize, 4), f.end);
|
||||||
try testing.expectEqual(@as(usize, 4), f.cells);
|
try testing.expectEqual(font.fixed(4), f.width);
|
||||||
try testing.expect(!f.elided);
|
try testing.expect(!f.elided);
|
||||||
|
|
||||||
// Exactly full is still whole: the ellipsis costs a cell, so only spend it
|
// Exactly full is still whole: the ellipsis costs room, so only spend it
|
||||||
// when something is actually cut.
|
// when something is actually cut.
|
||||||
const exact = font.fit("foot", 4);
|
const exact = font.fit("foot", font.fixed(4), Mono{});
|
||||||
try testing.expectEqual(@as(usize, 4), exact.cells);
|
try testing.expectEqual(@as(usize, 4), exact.end);
|
||||||
try testing.expect(!exact.elided);
|
try testing.expect(!exact.elided);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "a title too long for the tab loses its last cell to the ellipsis" {
|
test "a title too long for the tab is cut short for the ellipsis" {
|
||||||
const f = font.fit("firefox", 4);
|
const f = font.fit("firefox", font.fixed(4), Mono{});
|
||||||
try testing.expectEqualStrings("fir", "firefox"[0..f.end]);
|
try testing.expectEqualStrings("fir", "firefox"[0..f.end]);
|
||||||
try testing.expectEqual(@as(usize, 3), f.cells);
|
try testing.expectEqual(font.fixed(4), f.width);
|
||||||
try testing.expect(f.elided);
|
try testing.expect(f.elided);
|
||||||
|
|
||||||
// One cell of room leaves nothing but the ellipsis, and no room at all
|
// Room for the ellipsis alone leaves nothing but the ellipsis, and no room
|
||||||
// draws nothing rather than indexing past the end.
|
// at all draws nothing rather than indexing past the end.
|
||||||
const one = font.fit("firefox", 1);
|
const one = font.fit("firefox", font.fixed(1), Mono{});
|
||||||
try testing.expectEqual(@as(usize, 0), one.end);
|
try testing.expectEqual(@as(usize, 0), one.end);
|
||||||
try testing.expect(one.elided);
|
try testing.expect(one.elided);
|
||||||
|
|
||||||
const none = font.fit("firefox", 0);
|
const none = font.fit("firefox", 0, Mono{});
|
||||||
try testing.expectEqual(@as(usize, 0), none.end);
|
try testing.expectEqual(@as(usize, 0), none.end);
|
||||||
try testing.expect(!none.elided);
|
try testing.expect(!none.elided);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "eliding gives up as many characters as the ellipsis costs" {
|
||||||
|
// "wiki" alone would fit in three cells; the two-cell ellipsis means all
|
||||||
|
// but the first character has to go, which counting cells would miss.
|
||||||
|
const f = font.fit("wikiw", font.fixed(3), Prop{});
|
||||||
|
try testing.expect(f.elided);
|
||||||
|
try testing.expectEqualStrings("w", "wikiw"[0..f.end]);
|
||||||
|
try testing.expect(f.width <= font.fixed(3));
|
||||||
|
|
||||||
|
// Narrow characters earn their place: three of them survive where two wide
|
||||||
|
// ones would not.
|
||||||
|
const narrow = font.fit("wiiwwww", font.fixed(4), Prop{});
|
||||||
|
try testing.expect(narrow.elided);
|
||||||
|
try testing.expectEqualStrings("wii", "wiiwwww"[0..narrow.end]);
|
||||||
|
try testing.expect(narrow.width <= font.fixed(4));
|
||||||
|
}
|
||||||
|
|
||||||
test "titles are measured in characters, not bytes" {
|
test "titles are measured in characters, not bytes" {
|
||||||
// Three codepoints, six bytes: a tab four cells wide holds them all.
|
// Four codepoints, six bytes: a tab four cells wide holds them all.
|
||||||
const f = font.fit("ünïx", 4);
|
const f = font.fit("ünïx", font.fixed(4), Mono{});
|
||||||
try testing.expectEqual(@as(usize, 4), f.cells);
|
try testing.expectEqual("ünïx".len, f.end);
|
||||||
try testing.expect(!f.elided);
|
try testing.expect(!f.elided);
|
||||||
|
|
||||||
// And a cut lands on a codepoint boundary, never mid-sequence.
|
// And a cut lands on a codepoint boundary, never mid-sequence.
|
||||||
const cut = font.fit("ünïx", 3);
|
const cut = font.fit("ünïx", font.fixed(3), Mono{});
|
||||||
try testing.expect(cut.elided);
|
try testing.expect(cut.elided);
|
||||||
try testing.expectEqualStrings("ün", "ünïx"[0..cut.end]);
|
try testing.expectEqualStrings("ün", "ünïx"[0..cut.end]);
|
||||||
}
|
}
|
||||||
@@ -478,32 +521,20 @@ test "malformed utf-8 in a title yields one glyph per bad byte" {
|
|||||||
try testing.expectEqual(@as(?u21, null), it.next());
|
try testing.expectEqual(@as(?u21, null), it.next());
|
||||||
}
|
}
|
||||||
|
|
||||||
test "text measuring and cell fitting agree" {
|
test "control characters in a title draw as spaces" {
|
||||||
const scale = 2;
|
var it: font.Iterator = .{ .bytes = "a\nb\tc\x7f" };
|
||||||
// Whatever cellsForWidth says fits must actually fit.
|
try testing.expectEqual(@as(?u21, 'a'), it.next());
|
||||||
var width: i32 = 0;
|
try testing.expectEqual(@as(?u21, ' '), it.next());
|
||||||
while (width < 200) : (width += 1) {
|
try testing.expectEqual(@as(?u21, 'b'), it.next());
|
||||||
const cells = font.cellsForWidth(width, scale);
|
try testing.expectEqual(@as(?u21, ' '), it.next());
|
||||||
try testing.expect(font.textWidth(cells, scale) <= width);
|
try testing.expectEqual(@as(?u21, 'c'), it.next());
|
||||||
try testing.expect(font.textWidth(cells + 1, scale) > width);
|
try testing.expectEqual(@as(?u21, ' '), it.next());
|
||||||
}
|
try testing.expectEqual(@as(?u21, null), it.next());
|
||||||
}
|
}
|
||||||
|
|
||||||
test "printable ascii all have glyphs, and unknown codepoints fall back" {
|
test "fixed point widths round up to whole pixels" {
|
||||||
// The space is blank, and nothing else printable is.
|
try testing.expectEqual(@as(i32, 0), font.pixels(0));
|
||||||
var c: u8 = 0x21;
|
try testing.expectEqual(@as(i32, 1), font.pixels(1));
|
||||||
while (c <= 0x7e) : (c += 1) {
|
try testing.expectEqual(@as(i32, 1), font.pixels(font.fixed(1)));
|
||||||
const rows = font.glyph(c);
|
try testing.expectEqual(@as(i32, 2), font.pixels(font.fixed(1) + 1));
|
||||||
var lit = false;
|
|
||||||
for (rows) |r| lit = lit or r != 0;
|
|
||||||
try testing.expect(lit);
|
|
||||||
// Nothing may spill outside the five column cell.
|
|
||||||
for (rows) |r| try testing.expectEqual(@as(u8, 0), r & ~@as(u8, 0b11111));
|
|
||||||
}
|
|
||||||
for (font.glyph(' ')) |r| try testing.expectEqual(@as(u8, 0), r);
|
|
||||||
|
|
||||||
// Control characters stay blank; other codepoints get the fallback box.
|
|
||||||
for (font.glyph('\n')) |r| try testing.expectEqual(@as(u8, 0), r);
|
|
||||||
try testing.expect(!std.mem.eql(u8, font.glyph('?'), font.glyph('あ')));
|
|
||||||
try testing.expectEqualSlices(u8, font.glyph(0xfffd), font.glyph('あ'));
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user