From 4b8b5c5fc1131967a0191926edf5adcbf8434003 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 24 Nov 2022 08:33:32 -0800 Subject: [PATCH] font: skeleton for box drawing and hook up to Group --- src/font/BoxFont.zig | 168 +++++++++++++++++++++++++++++++++++++++++++ src/font/Group.zig | 72 +++++++++++++------ src/font/main.zig | 1 + 3 files changed, 220 insertions(+), 21 deletions(-) create mode 100644 src/font/BoxFont.zig diff --git a/src/font/BoxFont.zig b/src/font/BoxFont.zig new file mode 100644 index 000000000..9084a7841 --- /dev/null +++ b/src/font/BoxFont.zig @@ -0,0 +1,168 @@ +//! This file contains functions for drawing the box drawing characters +//! (https://en.wikipedia.org/wiki/Box-drawing_character) and related +//! characters that are provided by the terminal. +const BoxFont = @This(); + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const pixman = @import("pixman"); +const font = @import("main.zig"); +const Atlas = @import("../Atlas.zig"); + +/// The cell width and height because the boxes are fit perfectly +/// into a cell so that they all properly connect with zero spacing. +width: u32, +height: u32, + +/// Base thickness value for lines of the box. This is in points. +thickness: u32, + +/// We use alpha-channel-only images for the box font so white causes +/// a pixel to be shown. +const white: pixman.Color = .{ + .red = 0xFFFF, + .green = 0xFFFF, + .blue = 0xFFFF, + .alpha = 0xFFFF, +}; + +/// The thickness of a line. +const Thickness = enum { + light, + heavy, + + /// Calculate the real height of a line based on its thickness + /// and a base thickness value. The base thickness value is expected + /// to be in pixels. + fn height(self: Thickness, base: u32) u32 { + return switch (self) { + .light => base, + .heavy => base * 3, + }; + } +}; + +pub fn renderGlyph( + self: BoxFont, + alloc: Allocator, + atlas: *Atlas, + cp: u32, +) !font.Glyph { + assert(atlas.format == .greyscale); + + // TODO: render depending on cp + _ = cp; + + // Determine the config for our image buffer. The images we draw + // for boxes are always 8bpp + const format: pixman.FormatCode = .a8; + const stride = format.strideForWidth(self.width); + const len = @intCast(usize, stride * @intCast(c_int, self.height)); + + // Allocate our buffer + var data = try alloc.alloc(u32, len); + defer alloc.free(data); + std.mem.set(u32, data, 0); + + // Create the image we'll draw to + const img = try pixman.Image.createBitsNoClear( + format, + @intCast(c_int, self.width), + @intCast(c_int, self.height), + data.ptr, + stride, + ); + defer _ = img.unref(); + + self.draw_box_drawings_light_horizontal(img); + + // Reserve our region in the atlas and render the glyph to it. + const region = try atlas.reserve(alloc, self.width, self.height); + if (region.width > 0 and region.height > 0) { + // Convert our []u32 to []u8 since we use 8bpp formats + assert(format.bpp() == 8); + const len_u8 = len * 4; + const data_u8 = @alignCast(@alignOf(u8), @ptrCast([*]u8, data.ptr)[0..len_u8]); + + const depth = atlas.format.depth(); + + // We can avoid a buffer copy if our atlas width and bitmap + // width match and the bitmap pitch is just the width (meaning + // the data is tightly packed). + const needs_copy = !(self.width * depth == stride); + + // If we need to copy the data, we copy it into a temporary buffer. + const buffer = if (needs_copy) buffer: { + var temp = try alloc.alloc(u8, self.width * self.height * depth); + var dst_ptr = temp; + var src_ptr = data_u8.ptr; + var i: usize = 0; + while (i < self.height) : (i += 1) { + std.mem.copy(u8, dst_ptr, src_ptr[0 .. self.width * depth]); + dst_ptr = dst_ptr[self.width * depth ..]; + src_ptr += @intCast(usize, stride); + } + break :buffer temp; + } else data_u8[0..(self.width * self.height * depth)]; + defer if (buffer.ptr != data_u8.ptr) alloc.free(buffer); + + // Write the glyph information into the atlas + assert(region.width == self.width); + assert(region.height == self.height); + atlas.set(region, buffer); + } + + return font.Glyph{ + .width = self.width, + .height = self.height, + .offset_x = 0, + .offset_y = 0, + .atlas_x = region.x, + .atlas_y = region.y, + .advance_x = @intToFloat(f32, self.width), + }; +} + +fn draw_box_drawings_light_horizontal(self: BoxFont, img: *pixman.Image) void { + self.hline_middle(img, .light); +} + +fn hline_middle(self: BoxFont, img: *pixman.Image, thickness: Thickness) void { + const height = thickness.height(self.thickness); + self.hline(img, 0, self.width, (self.height - height) / 2, height); +} + +fn hline( + self: BoxFont, + img: *pixman.Image, + x1: u32, + x2: u32, + y: u32, + thickness_px: u32, +) void { + const boxes = &[_]pixman.Box32{ + .{ + .x1 = @intCast(i32, @min(@max(x1, 0), self.width)), + .x2 = @intCast(i32, @min(@max(x2, 0), self.width)), + .y1 = @intCast(i32, @min(@max(y, 0), self.height)), + .y2 = @intCast(i32, @min(@max(y + thickness_px, 0), self.height)), + }, + }; + + img.fillBoxes(.src, white, boxes) catch {}; +} + +test "all" { + const testing = std.testing; + const alloc = testing.allocator; + + var atlas_greyscale = try Atlas.init(alloc, 512, .greyscale); + defer atlas_greyscale.deinit(alloc); + + const face: BoxFont = .{ .width = 18, .height = 36, .thickness = 2 }; + const glyph = try face.renderGlyph(alloc, &atlas_greyscale, 0x2500); + try testing.expectEqual(@as(u32, face.width), glyph.width); + try testing.expectEqual(@as(u32, face.height), glyph.height); +} diff --git a/src/font/Group.zig b/src/font/Group.zig index c7ea8bd48..6d9766cf8 100644 --- a/src/font/Group.zig +++ b/src/font/Group.zig @@ -47,9 +47,14 @@ size: font.face.DesiredSize, faces: StyleArray, /// If discovery is available, we'll look up fonts where we can't find -/// the codepoint. +/// the codepoint. This can be set after initialization. discover: ?font.Discover = null, +/// Set this to a non-null value to enable box font glyph drawing. If this +/// isn't enabled we'll just fall through to trying to use regular fonts +/// to render box glyphs. +box_font: ?font.BoxFont = null, + pub fn init( alloc: Allocator, lib: Library, @@ -89,7 +94,7 @@ pub fn addFace(self: *Group, alloc: Allocator, style: Style, face: DeferredFace) const list = self.faces.getPtr(style); // We have some special indexes so we must never pass those. - if (list.items.len >= FontIndex.special_start - 1) return error.GroupFull; + if (list.items.len >= FontIndex.Special.start - 1) return error.GroupFull; try list.append(alloc, face); } @@ -119,17 +124,23 @@ pub const FontIndex = packed struct { const idx_bits = 8 - @typeInfo(@typeInfo(Style).Enum.tag_type).Int.bits; pub const IndexInt = @Type(.{ .Int = .{ .signedness = .unsigned, .bits = idx_bits } }); - /// Special indexes. They start at "special_start" and are guaranteed - /// to not be less than that. - pub const special_start = std.math.maxInt(IndexInt); + /// The special-case fonts that we support. + pub const Special = enum(IndexInt) { + // We start all special fonts at this index so they can be detected. + pub const start = std.math.maxInt(IndexInt); - /// Our box drawing font is always specified by box_index. This font - /// is rendered just-in-time using 2D graphics APIs. - pub const box: FontIndex = .{ .idx = special_start }; + /// Box drawing, this is rendered JIT using 2D graphics APIs. + box = start, + }; style: Style = .regular, idx: IndexInt = 0, + /// Initialize a special font index. + pub fn initSpecial(v: Special) FontIndex { + return .{ .style = .regular, .idx = @enumToInt(v) }; + } + /// Convert to int pub fn int(self: FontIndex) u8 { return @bitCast(u8, self); @@ -138,8 +149,9 @@ pub const FontIndex = packed struct { /// Returns true if this is a "special" index which doesn't map to /// a real font face. We can still render it but there is no face for /// this font. - pub fn special(self: FontIndex) bool { - return self.idx >= special_start; + pub fn special(self: FontIndex) ?Special { + if (self.idx < Special.start) return null; + return @intToEnum(Special, self.idx); } test { @@ -167,13 +179,16 @@ pub fn indexForCodepoint( p: ?Presentation, ) ?FontIndex { // If this is a box drawing glyph, we use the special font index. This - // will force special logic where we'll render this ourselves. - if (switch (cp) { - // "Box Drawing" block - 0x2500...0x257F => true, - else => false, - }) { - return FontIndex.box; + // will force special logic where we'll render this ourselves. If we don't + // have a box font set, then we just try to use regular fonts. + if (self.box_font != null) { + if (switch (cp) { + // "Box Drawing" block + 0x2500...0x257F => true, + else => false, + }) { + return FontIndex.initSpecial(.box); + } } // If we can find the exact value, then return that. @@ -224,7 +239,7 @@ fn indexForCodepointExact(self: Group, cp: u32, style: Style, p: ?Presentation) /// Return the Face represented by a given FontIndex. pub fn faceFromIndex(self: Group, index: FontIndex) !Face { - if (index.special()) return error.SpecialHasNoFace; + if (index.special() != null) return error.SpecialHasNoFace; const deferred = &self.faces.get(index.style).items[@intCast(usize, index.idx)]; try deferred.load(self.lib, self.size); return deferred.face.?; @@ -249,7 +264,14 @@ pub fn renderGlyph( glyph_index: u32, max_height: ?u16, ) !Glyph { - // TODO: render special faces here + // Special-case fonts are rendered directly. + if (index.special()) |sp| switch (sp) { + .box => return try self.box_font.?.renderGlyph( + alloc, + atlas, + glyph_index, + ), + }; const face = &self.faces.get(index.style).items[@intCast(usize, index.idx)]; try face.load(self.lib, self.size); @@ -313,6 +335,11 @@ test { try testing.expectEqual(Style.regular, idx.style); try testing.expectEqual(@as(FontIndex.IndexInt, 1), idx.idx); } + + // Box glyph should be null since we didn't set a box font + { + try testing.expect(group.indexForCodepoint(0x1FB00, .regular, null) == null); + } } test "box glyph" { @@ -328,10 +355,13 @@ test "box glyph" { var group = try init(alloc, lib, .{ .points = 12 }); defer group.deinit(); + // Set box font + group.box_font = font.BoxFont{ .width = 18, .height = 36, .thickness = 2 }; + // Should find a box glyph const idx = group.indexForCodepoint(0x2500, .regular, null).?; try testing.expectEqual(Style.regular, idx.style); - try testing.expectEqual(@as(FontIndex.IndexInt, FontIndex.box.idx), idx.idx); + try testing.expectEqual(@enumToInt(FontIndex.Special.box), idx.idx); // Should render it const glyph = try group.renderGlyph( @@ -341,7 +371,7 @@ test "box glyph" { 0x2500, null, ); - _ = glyph; + try testing.expectEqual(@as(u32, 36), glyph.height); } test "resize" { diff --git a/src/font/main.zig b/src/font/main.zig index 69e6a9431..ed21e2f09 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -1,6 +1,7 @@ const std = @import("std"); const build_options = @import("build_options"); +pub const BoxFont = @import("BoxFont.zig"); pub const discovery = @import("discovery.zig"); pub const face = @import("face.zig"); pub const DeferredFace = @import("DeferredFace.zig");