1
0
Fork 0

fix: vertical positioning of spans

This commit is contained in:
LordMZTE 2024-02-09 22:23:51 +01:00
parent 78951ce42e
commit 6f318242d7
Signed by: LordMZTE
GPG key ID: B64802DC33A64FF6
8 changed files with 115 additions and 71 deletions

View file

@ -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),

View file

@ -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;
}
};

View file

@ -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;
}

View file

@ -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,

View file

@ -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,
};
}

View file

@ -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 },
);
}

View file

@ -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.?,
);
},

View file

@ -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),
}