fix: vertical positioning of spans
This commit is contained in:
parent
78951ce42e
commit
6f318242d7
|
@ -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.
|
/// 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 {
|
pub fn span(self: *Self, pos: Position, text_span: Span) !void {
|
||||||
if (zenolith.debug_render) {
|
if (zenolith.debug_render) {
|
||||||
|
@ -174,7 +176,7 @@ fn Prototype(comptime Self: type) type {
|
||||||
// Baseline
|
// Baseline
|
||||||
try self.rect(
|
try self.rect(
|
||||||
.{
|
.{
|
||||||
.pos = pos.add(.{ .x = 0, .y = @intCast(text_span.baseline_y) }),
|
.pos = pos,
|
||||||
.size = .{ .width = text_span.baseline_width, .height = 2 },
|
.size = .{ .width = text_span.baseline_width, .height = 2 },
|
||||||
},
|
},
|
||||||
Color.fromInt(0x00ff00ff),
|
Color.fromInt(0x00ff00ff),
|
||||||
|
|
47
src/text.zig
47
src/text.zig
|
@ -1,13 +1,56 @@
|
||||||
|
const Position = @import("layout/Position.zig");
|
||||||
|
const Size = @import("layout/Size.zig");
|
||||||
|
|
||||||
test {
|
test {
|
||||||
_ = Chunk;
|
_ = Chunk;
|
||||||
_ = Font;
|
_ = Font;
|
||||||
_ = Glyph;
|
|
||||||
_ = Span;
|
_ = Span;
|
||||||
_ = Style;
|
_ = Style;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const Chunk = @import("text/Chunk.zig");
|
pub const Chunk = @import("text/Chunk.zig");
|
||||||
pub const Font = @import("text/font.zig").Font;
|
pub const Font = @import("text/font.zig").Font;
|
||||||
pub const Glyph = @import("text/Glyph.zig");
|
|
||||||
pub const Span = @import("text/Span.zig");
|
pub const Span = @import("text/Span.zig");
|
||||||
pub const Style = @import("text/Style.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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
//! It may do wrapping on span boundaries depending on the wrap_mode.
|
//! 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.
|
//! You may access the spans field to modify children, but layout must be called again afterwards.
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const zenolith = @import("../main.zig");
|
||||||
|
|
||||||
const Position = @import("../layout/Position.zig");
|
const Position = @import("../layout/Position.zig");
|
||||||
const Size = @import("../layout/Size.zig");
|
const Size = @import("../layout/Size.zig");
|
||||||
|
@ -114,7 +115,7 @@ pub fn layout(self: *Chunk, opts: LayoutOptions) void {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (should_wrap) {
|
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;
|
line_start_idx = i;
|
||||||
if (cursor.x > self.size.width) self.size.width = @intCast(cursor.x);
|
if (cursor.x > self.size.width) self.size.width = @intCast(cursor.x);
|
||||||
cursor.x = -span.span.origin_off.x;
|
cursor.x = -span.span.origin_off.x;
|
||||||
|
@ -122,28 +123,37 @@ pub fn layout(self: *Chunk, opts: LayoutOptions) void {
|
||||||
|
|
||||||
span.position = .{
|
span.position = .{
|
||||||
.x = cursor.x,
|
.x = cursor.x,
|
||||||
.y = cursor.y - span.span.baseline_y,
|
.y = cursor.y,
|
||||||
};
|
};
|
||||||
|
|
||||||
cursor.x += span.span.baseline_width;
|
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);
|
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.
|
/// Offsets all chunks in the given range downwards by their line y_offset and returns that line's
|
||||||
fn offsetLineByHeight(self: *const Chunk, start_idx: usize, end_idx: usize) u31 {
|
/// height metrics..
|
||||||
var max_height: u31 = 0;
|
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| {
|
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| {
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
|
|
@ -1,9 +1,9 @@
|
||||||
//! A span is a one-line piece of text. It consists of glyphs and performs single-line layout on them.
|
//! 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.
|
//! It also has information on the font, style, color as well as bounding boxes.
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const zenolith = @import("../main.zig");
|
||||||
|
|
||||||
const Font = @import("font.zig").Font;
|
const Font = @import("font.zig").Font;
|
||||||
const Glyph = @import("Glyph.zig");
|
|
||||||
const Position = @import("../layout/Position.zig");
|
const Position = @import("../layout/Position.zig");
|
||||||
const Size = @import("../layout/Size.zig");
|
const Size = @import("../layout/Size.zig");
|
||||||
const Style = @import("Style.zig");
|
const Style = @import("Style.zig");
|
||||||
|
@ -12,15 +12,12 @@ glyphs: std.ArrayList(PositionedGlyph),
|
||||||
font: *Font,
|
font: *Font,
|
||||||
style: Style,
|
style: Style,
|
||||||
|
|
||||||
/// The position offset from this span's position to it's origin.
|
/// This represents an offset from relative 0/0 to the start of the baseline.
|
||||||
/// Spans may have negative origins relative to their position when glyphs have a negative bearing.
|
/// This means that the y component is the height of the highest glyph (minus the below-baseline part).
|
||||||
/// The top left corner of this span would be calculated as position + origin_off.
|
/// 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,
|
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.
|
/// 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.
|
/// This does not always correspond to renderSize().width due to padding between glyphs.
|
||||||
baseline_width: u31 = 0,
|
baseline_width: u31 = 0,
|
||||||
|
@ -28,7 +25,7 @@ baseline_width: u31 = 0,
|
||||||
const Span = @This();
|
const Span = @This();
|
||||||
|
|
||||||
pub const PositionedGlyph = struct {
|
pub const PositionedGlyph = struct {
|
||||||
glyph: Glyph,
|
glyph: zenolith.text.Glyph,
|
||||||
position: Position,
|
position: Position,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -93,12 +90,13 @@ pub fn layout(self: *Span) void {
|
||||||
cursor.x += pglyph.glyph.advance;
|
cursor.x += pglyph.glyph.advance;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (self.glyphs.items) |*pglyph| {
|
//for (self.glyphs.items) |*pglyph| {
|
||||||
pglyph.position.y -= min_y;
|
// pglyph.position.y -= min_y;
|
||||||
}
|
//}
|
||||||
|
|
||||||
self.origin_off = if (self.glyphs.items.len > 0) self.glyphs.items[0].position else Position.zero;
|
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);
|
self.baseline_width = @intCast(cursor.x);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,3 +119,23 @@ pub fn renderSize(self: Span) Size {
|
||||||
|
|
||||||
return max.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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -5,23 +5,28 @@ const zenolith = @import("../main.zig");
|
||||||
|
|
||||||
const Size = @import("../layout/Size.zig");
|
const Size = @import("../layout/Size.zig");
|
||||||
const Style = @import("../text/Style.zig");
|
const Style = @import("../text/Style.zig");
|
||||||
const Glyph = @import("Glyph.zig");
|
|
||||||
|
|
||||||
fn FontPrototype(comptime Self: type) type {
|
fn FontPrototype(comptime Self: type) type {
|
||||||
std.debug.assert(Self == Font);
|
std.debug.assert(Self == Font);
|
||||||
|
|
||||||
return struct {
|
return struct {
|
||||||
/// For a given font size in pixels, returns the offset between lines.
|
/// For a given font size in pixels, returns the height metrics.
|
||||||
pub fn yOffset(self: *Self, size: u31) u31 {
|
pub fn heightMetrics(self: *Self, size: u31) zenolith.text.HeightMetrics {
|
||||||
return statspatch.implcall(self, .ptr, "yOffset", u31, .{size});
|
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(
|
return try statspatch.implcall(
|
||||||
self,
|
self,
|
||||||
.ptr,
|
.ptr,
|
||||||
"getGlyph",
|
"getGlyph",
|
||||||
anyerror!Glyph,
|
anyerror!zenolith.text.Glyph,
|
||||||
.{ codepoint, style },
|
.{ codepoint, style },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,10 +51,7 @@ pub fn treevent(self: *Button, selfw: *Widget, tv: anytype) !void {
|
||||||
self.span.?.layout();
|
self.span.?.layout();
|
||||||
}
|
}
|
||||||
|
|
||||||
selfw.data.size = layout.Size.two(style.padding * 2).add(.{
|
selfw.data.size = layout.Size.two(style.padding * 2).add(self.span.?.layoutSize());
|
||||||
.width = self.span.?.baseline_width,
|
|
||||||
.height = self.span.?.font.yOffset(self.span.?.style.size),
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
treev.Draw => {
|
treev.Draw => {
|
||||||
|
@ -82,10 +79,7 @@ pub fn treevent(self: *Button, selfw: *Widget, tv: anytype) !void {
|
||||||
);
|
);
|
||||||
|
|
||||||
try tv.painter.span(
|
try tv.painter.span(
|
||||||
selfw.data.position.add(layout.Position.two(style.padding)).add(.{
|
selfw.data.position.add(layout.Position.two(style.padding)).add(self.span.?.layoutOffset()),
|
||||||
.x = 0,
|
|
||||||
.y = self.span.?.font.yOffset(self.span.?.style.size) - self.span.?.baseline_y,
|
|
||||||
}),
|
|
||||||
self.span.?,
|
self.span.?,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -49,10 +49,7 @@ pub fn treevent(self: *Label, selfw: *Widget, tv: anytype) !void {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
treev.LayoutSize => {
|
treev.LayoutSize => {
|
||||||
selfw.data.size = tv.constraints.clamp(.{
|
selfw.data.size = tv.constraints.clamp(self.span.?.layoutSize());
|
||||||
.width = self.span.?.baseline_width,
|
|
||||||
.height = self.span.?.font.yOffset(self.span.?.style.size),
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
treev.Draw => {
|
treev.Draw => {
|
||||||
const style = selfw.getAttreebute(LabelStyle) orelse
|
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.?.style = style.font_style;
|
||||||
self.span.?.layout();
|
self.span.?.layout();
|
||||||
|
|
||||||
try tv.painter.span(selfw.data.position.add(.{
|
try tv.painter.span(selfw.data.position.add(self.span.?.layoutOffset()), self.span.?);
|
||||||
.x = 0,
|
|
||||||
.y = self.span.?.font.yOffset(self.span.?.style.size) - self.span.?.baseline_y,
|
|
||||||
}), self.span.?);
|
|
||||||
},
|
},
|
||||||
else => try tv.dispatch(selfw),
|
else => try tv.dispatch(selfw),
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue