1
0
Fork 0
zenolith/src/text/Span.zig

140 lines
4.5 KiB
Zig

//! A span is a one-line piece of text. It consists of glyphs and performs single-line layout on them.
//! It also has information on the font, style, color as well as bounding boxes.
const std = @import("std");
const zenolith = @import("../main.zig");
const Font = @import("font.zig").Font;
const Position = @import("../layout/Position.zig");
const Size = @import("../layout/Size.zig");
const Style = @import("Style.zig");
glyphs: std.ArrayList(PositionedGlyph),
font: *Font,
style: Style,
/// This represents an offset from relative 0/0 to the start of the baseline.
/// This means that the y component is the height of the highest glyph (minus the below-baseline part).
/// The x component is a horizontal offset the first glyph may have. This is commonly the case with
/// letters such as 'j' where the hook would be a little to the left of where the text should be aligned.
origin_off: Position = Position.zero,
/// The width of the baseline. This is calculated as the distance the cursor moved during layout.
/// This does not always correspond to renderSize().width due to padding between glyphs.
baseline_width: u31 = 0,
const Span = @This();
pub const PositionedGlyph = struct {
glyph: zenolith.text.Glyph,
position: Position,
};
pub const InitOptions = struct {
font: *Font,
style: Style = .{},
text: []const u8,
};
/// Initializes this span with some text. This performs layout.
/// The caller must call deinit when done to free memory.
pub fn init(alloc: std.mem.Allocator, opts: InitOptions) !Span {
var self = Span{
.glyphs = std.ArrayList(PositionedGlyph).init(alloc),
.font = opts.font,
.style = opts.style,
};
try self.updateGlyphs(.{ .text = opts.text });
self.layout();
return self;
}
pub const UpdateGlyphsOptions = struct {
font: ?*Font = null,
style: ?Style = null,
text: []const u8,
};
/// Update the glyphs in this span to those of the given string.
/// Note that this does not recalculate positions! The caller must assure that `layout` is called
/// after this.
pub fn updateGlyphs(
self: *Span,
opts: UpdateGlyphsOptions,
) !void {
if (opts.style) |style| self.style = style;
if (opts.font) |font| self.font = font;
self.glyphs.clearRetainingCapacity();
var iter = std.unicode.Utf8Iterator{ .i = 0, .bytes = opts.text };
while (iter.nextCodepoint()) |codepoint| {
try self.glyphs.append(.{
.glyph = try self.font.getGlyph(codepoint, self.style),
.position = Position.zero,
});
}
}
/// Positions the glyphs of the span and sets origin_off.
pub fn layout(self: *Span) void {
var cursor = Position.zero;
var min_y: i32 = 0;
for (self.glyphs.items) |*pglyph| {
pglyph.position = cursor.add(pglyph.glyph.bearing);
min_y = @min(min_y, pglyph.glyph.bearing.y);
cursor.x += pglyph.glyph.advance;
}
self.origin_off = .{
.x = if (self.glyphs.items.len > 0) self.glyphs.items[0].position.x else 0,
.y = -min_y,
};
self.baseline_width = @intCast(cursor.x);
}
/// Free owned data.
pub fn deinit(self: Span) void {
self.glyphs.deinit();
}
/// This determines the size of the span as rendered. This fully contains the glyphs.
/// The given size is to be treated as relative to the span's origin, with origin_off calculated in.
pub fn renderSize(self: Span) Size {
if (self.glyphs.items.len == 0) return Size.zero;
var max = Position.two(std.math.minInt(i32));
for (self.glyphs.items) |glyph| {
max.y = @max(max.y, glyph.position.y + glyph.glyph.size.height);
max.x = @max(max.x, glyph.position.x + glyph.glyph.size.width);
}
return max.size();
}
/// The size to be used when this span is laid out stand-alone. It will always fully contain all
/// glayphs, but unlike render size, it will include padding as required by the font.
/// Use this if you want to include spans in widgets.
pub fn layoutSize(self: Span) Size {
return .{
.width = @intCast(@as(i32, self.baseline_width) + self.origin_off.x),
.height = self.font.heightMetrics(self.style.size).totalHeight(),
};
}
/// This is similar to self.origin_off, with the difference that the the baseline won't be
/// positioned in accordance with the largest glyph but instead using the font height metrics.
/// This is typically preferred, but may overly complex in some situations.
pub fn layoutOffset(self: Span) Position {
return .{
.x = self.origin_off.x,
.y = self.font.heightMetrics(self.style.size).y_offset,
};
}