mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
font: skeleton for box drawing and hook up to Group
This commit is contained in:
168
src/font/BoxFont.zig
Normal file
168
src/font/BoxFont.zig
Normal file
@ -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);
|
||||
}
|
@ -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" {
|
||||
|
@ -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");
|
||||
|
Reference in New Issue
Block a user