From 6f318242d7bb5dad079e5ad6421900744e5d03b4 Mon Sep 17 00:00:00 2001 From: LordMZTE Date: Fri, 9 Feb 2024 22:23:51 +0100 Subject: [PATCH] fix: vertical positioning of spans --- src/painter.zig | 6 ++++-- src/text.zig | 47 ++++++++++++++++++++++++++++++++++++++++-- src/text/Chunk.zig | 30 ++++++++++++++++++--------- src/text/Glyph.zig | 22 -------------------- src/text/Span.zig | 44 +++++++++++++++++++++++++++------------ src/text/font.zig | 17 +++++++++------ src/widgets/Button.zig | 10 ++------- src/widgets/Label.zig | 10 ++------- 8 files changed, 115 insertions(+), 71 deletions(-) delete mode 100644 src/text/Glyph.zig diff --git a/src/painter.zig b/src/painter.zig index 52a4b71..1c62f8f 100644 --- a/src/painter.zig +++ b/src/painter.zig @@ -139,7 +139,9 @@ fn Prototype(comptime Self: type) type { ); } - /// Draw the given span of text at the given position. + /// Draw the given span of text at the given position, where the position is the start + /// of the span baseline. To work with the top-left corner of the span, you should + /// use origin_off or layoutPosition() accordingly. /// The caller asserts that the font of the span is from the same platform as this painter. pub fn span(self: *Self, pos: Position, text_span: Span) !void { if (zenolith.debug_render) { @@ -174,7 +176,7 @@ fn Prototype(comptime Self: type) type { // Baseline try self.rect( .{ - .pos = pos.add(.{ .x = 0, .y = @intCast(text_span.baseline_y) }), + .pos = pos, .size = .{ .width = text_span.baseline_width, .height = 2 }, }, Color.fromInt(0x00ff00ff), diff --git a/src/text.zig b/src/text.zig index 55f3edf..58aa1ae 100644 --- a/src/text.zig +++ b/src/text.zig @@ -1,13 +1,56 @@ +const Position = @import("layout/Position.zig"); +const Size = @import("layout/Size.zig"); + test { _ = Chunk; _ = Font; - _ = Glyph; _ = Span; _ = Style; } pub const Chunk = @import("text/Chunk.zig"); pub const Font = @import("text/font.zig").Font; -pub const Glyph = @import("text/Glyph.zig"); pub const Span = @import("text/Span.zig"); pub const Style = @import("text/Style.zig"); + +/// A glyph represents the smallet possible unit of text with positioning information. +/// It is used for text layout and rendering. +pub const Glyph = struct { + /// The unicode codepoint this glyph corresponds to. + /// This is intentionally only one codepoint. While Unicode glyphs an consist of up to 7 + /// codepoints (citation needed), these are not supported. Support might be implemented in the + /// future, but this is currently not planned. + codepoint: u21, + + /// The size of this glyph. This is typically the boundig box of the glyph as drawn. + size: Size, + + /// The offset of the character's top-left corner from the baseline of the text span. + /// When performing span layout, this is added onto the position of the cursor. + /// This being 0/0 will thus result in the glyph being aligned below the baseline. + /// This is typically has a negative Y offset to align the glyph above the baseline. + bearing: Position, + + /// How much the cursor should move to the right after this glyph. + advance: u31, +}; + +/// Information about the height of a font at a certain size. You may obtain this using +/// Font.heightMetrics. +pub const HeightMetrics = struct { + /// The distance two baselines should be offset from another. + /// This thus also represents the maximum height a glyph may have ABOVE the baseline. + y_offset: u31, + + /// The maximum space a glyph may take up below the baseline. + /// Note that this is space is not inserted between lines, + /// but is instead used as padding below the last line of a chunk. + bottom_padding: u31, + + /// Returns the total height a line will have. + /// This is simply the sum of y_offset and bottom_padding. + pub inline fn totalHeight(self: HeightMetrics) u31 { + return self.y_offset + self.bottom_padding; + } +}; + diff --git a/src/text/Chunk.zig b/src/text/Chunk.zig index 0b8d6fd..f07fdf6 100644 --- a/src/text/Chunk.zig +++ b/src/text/Chunk.zig @@ -2,6 +2,7 @@ //! It may do wrapping on span boundaries depending on the wrap_mode. //! You may access the spans field to modify children, but layout must be called again afterwards. const std = @import("std"); +const zenolith = @import("../main.zig"); const Position = @import("../layout/Position.zig"); const Size = @import("../layout/Size.zig"); @@ -114,7 +115,7 @@ pub fn layout(self: *Chunk, opts: LayoutOptions) void { }; if (should_wrap) { - cursor.y += self.offsetLineByHeight(line_start_idx, i); + cursor.y += self.offsetLineByHeight(line_start_idx, i).y_offset; line_start_idx = i; if (cursor.x > self.size.width) self.size.width = @intCast(cursor.x); cursor.x = -span.span.origin_off.x; @@ -122,28 +123,37 @@ pub fn layout(self: *Chunk, opts: LayoutOptions) void { span.position = .{ .x = cursor.x, - .y = cursor.y - span.span.baseline_y, + .y = cursor.y, }; cursor.x += span.span.baseline_width; } - cursor.y += self.offsetLineByHeight(line_start_idx, self.spans.items.len); + const last_metrics = self.offsetLineByHeight(line_start_idx, self.spans.items.len); + cursor.y += last_metrics.y_offset; - self.size.height = @intCast(cursor.y); + self.size.height = @intCast(cursor.y + last_metrics.bottom_padding); if (cursor.x > self.size.width) self.size.width = @intCast(cursor.x); } -/// Offsets all chunks in the given range downwards by their line height and returns that line height. -fn offsetLineByHeight(self: *const Chunk, start_idx: usize, end_idx: usize) u31 { - var max_height: u31 = 0; +/// Offsets all chunks in the given range downwards by their line y_offset and returns that line's +/// height metrics.. +fn offsetLineByHeight(self: *const Chunk, start_idx: usize, end_idx: usize) zenolith.text.HeightMetrics { + var max = zenolith.text.HeightMetrics{ + .y_offset = 0, + .bottom_padding = 0, + }; for (self.spans.items[start_idx..end_idx]) |span| { - max_height = @max(max_height, span.span.font.yOffset(span.span.style.size)); + const metrics = span.span.font.heightMetrics(span.span.style.size); + max = .{ + .y_offset = @max(max.y_offset, metrics.y_offset), + .bottom_padding = @max(max.bottom_padding, metrics.bottom_padding), + }; } for (self.spans.items[start_idx..end_idx]) |*span| { - span.position.y += max_height; + span.position.y += max.y_offset; } - return max_height; + return max; } diff --git a/src/text/Glyph.zig b/src/text/Glyph.zig deleted file mode 100644 index 824fb8e..0000000 --- a/src/text/Glyph.zig +++ /dev/null @@ -1,22 +0,0 @@ -//! A glyph represents the smallet possible unit of text with positioning information. -//! It is used for text layout and rendering. -const Position = @import("../layout/Position.zig"); -const Size = @import("../layout/Size.zig"); - -/// The unicode codepoint this glyph corresponds to. -/// This is intentionally only one codepoint. While Unicode glyphs an consist of up to 7 -/// codepoints (citation needed), these are not supported. Support might be implemented in the -/// future, but this is currently not planned. -codepoint: u21, - -/// The size of this glyph. This is typically the boundig box of the glyph as drawn. -size: Size, - -/// The offset of the character's top-left corner from the baseline of the text span. -/// When performing span layout, this is added onto the position of the cursor. -/// This being 0/0 will thus result in the glyph being aligned below the baseline. -/// This is typically has a negative Y offset to align the glyph above the baseline. -bearing: Position, - -/// How much the cursor should move to the right after this glyph. -advance: u31, diff --git a/src/text/Span.zig b/src/text/Span.zig index e64edbd..bc53113 100644 --- a/src/text/Span.zig +++ b/src/text/Span.zig @@ -1,9 +1,9 @@ //! 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 Glyph = @import("Glyph.zig"); const Position = @import("../layout/Position.zig"); const Size = @import("../layout/Size.zig"); const Style = @import("Style.zig"); @@ -12,15 +12,12 @@ glyphs: std.ArrayList(PositionedGlyph), font: *Font, style: Style, -/// The position offset from this span's position to it's origin. -/// Spans may have negative origins relative to their position when glyphs have a negative bearing. -/// The top left corner of this span would be calculated as position + origin_off. +/// 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 Y-coordinate of the baseline of this span relative to it's top. -/// The chunker uses this for aligning spans. -baseline_y: u31 = 0, - /// 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, @@ -28,7 +25,7 @@ baseline_width: u31 = 0, const Span = @This(); pub const PositionedGlyph = struct { - glyph: Glyph, + glyph: zenolith.text.Glyph, position: Position, }; @@ -93,12 +90,13 @@ pub fn layout(self: *Span) void { cursor.x += pglyph.glyph.advance; } - for (self.glyphs.items) |*pglyph| { - pglyph.position.y -= min_y; - } + //for (self.glyphs.items) |*pglyph| { + // pglyph.position.y -= min_y; + //} self.origin_off = if (self.glyphs.items.len > 0) self.glyphs.items[0].position else Position.zero; - self.baseline_y = @intCast(-min_y); + self.origin_off.y = -min_y; + //self.baseline_y = @intCast(-min_y); self.baseline_width = @intCast(cursor.x); } @@ -121,3 +119,23 @@ pub fn renderSize(self: Span) Size { 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, + }; +} diff --git a/src/text/font.zig b/src/text/font.zig index a0831ce..f4d027d 100644 --- a/src/text/font.zig +++ b/src/text/font.zig @@ -5,23 +5,28 @@ const zenolith = @import("../main.zig"); const Size = @import("../layout/Size.zig"); const Style = @import("../text/Style.zig"); -const Glyph = @import("Glyph.zig"); fn FontPrototype(comptime Self: type) type { std.debug.assert(Self == Font); return struct { - /// For a given font size in pixels, returns the offset between lines. - pub fn yOffset(self: *Self, size: u31) u31 { - return statspatch.implcall(self, .ptr, "yOffset", u31, .{size}); + /// For a given font size in pixels, returns the height metrics. + pub fn heightMetrics(self: *Self, size: u31) zenolith.text.HeightMetrics { + return statspatch.implcall( + self, + .ptr, + "heightMetrics", + zenolith.text.HeightMetrics, + .{size}, + ); } - pub fn getGlyph(self: *Self, codepoint: u21, style: Style) !Glyph { + pub fn getGlyph(self: *Self, codepoint: u21, style: Style) !zenolith.text.Glyph { return try statspatch.implcall( self, .ptr, "getGlyph", - anyerror!Glyph, + anyerror!zenolith.text.Glyph, .{ codepoint, style }, ); } diff --git a/src/widgets/Button.zig b/src/widgets/Button.zig index 8577116..d3f5883 100644 --- a/src/widgets/Button.zig +++ b/src/widgets/Button.zig @@ -51,10 +51,7 @@ pub fn treevent(self: *Button, selfw: *Widget, tv: anytype) !void { self.span.?.layout(); } - selfw.data.size = layout.Size.two(style.padding * 2).add(.{ - .width = self.span.?.baseline_width, - .height = self.span.?.font.yOffset(self.span.?.style.size), - }); + selfw.data.size = layout.Size.two(style.padding * 2).add(self.span.?.layoutSize()); }, treev.Draw => { @@ -82,10 +79,7 @@ pub fn treevent(self: *Button, selfw: *Widget, tv: anytype) !void { ); try tv.painter.span( - selfw.data.position.add(layout.Position.two(style.padding)).add(.{ - .x = 0, - .y = self.span.?.font.yOffset(self.span.?.style.size) - self.span.?.baseline_y, - }), + selfw.data.position.add(layout.Position.two(style.padding)).add(self.span.?.layoutOffset()), self.span.?, ); }, diff --git a/src/widgets/Label.zig b/src/widgets/Label.zig index 20e94cb..a04500b 100644 --- a/src/widgets/Label.zig +++ b/src/widgets/Label.zig @@ -49,10 +49,7 @@ pub fn treevent(self: *Label, selfw: *Widget, tv: anytype) !void { }); }, treev.LayoutSize => { - selfw.data.size = tv.constraints.clamp(.{ - .width = self.span.?.baseline_width, - .height = self.span.?.font.yOffset(self.span.?.style.size), - }); + selfw.data.size = tv.constraints.clamp(self.span.?.layoutSize()); }, treev.Draw => { const style = selfw.getAttreebute(LabelStyle) orelse @@ -61,10 +58,7 @@ pub fn treevent(self: *Label, selfw: *Widget, tv: anytype) !void { self.span.?.style = style.font_style; self.span.?.layout(); - try tv.painter.span(selfw.data.position.add(.{ - .x = 0, - .y = self.span.?.font.yOffset(self.span.?.style.size) - self.span.?.baseline_y, - }), self.span.?); + try tv.painter.span(selfw.data.position.add(self.span.?.layoutOffset()), self.span.?); }, else => try tv.dispatch(selfw), }