feat: update to new text rendering system
This commit is contained in:
parent
3cbcfc0154
commit
b8a928f7e9
|
@ -13,8 +13,8 @@
|
|||
// .hash = "1220685f1c039a53076ef169ef2e2959aeb054c86d4eda5aac736885daa0bf2f6910",
|
||||
//},
|
||||
.zenolith = .{
|
||||
.url = "https://git.mzte.de/zenolith/zenolith/archive/4705b425154b40a1718d2102ab6ad4eecf5135d9.tar.gz",
|
||||
.hash = "122039dbae23348c8d9320af685c4d43420b8f362d53898a1d57f7ec0ced04ab8711",
|
||||
.url = "https://git.mzte.de/zenolith/zenolith/archive/8908dbcc0d0f54bd942a2fd1cf973b5ef2ef9ffc.tar.gz",
|
||||
.hash = "122099a1bd57dee5e0a2b454ab435755b9b206fe0de4f7949edc2ae972af2663a1f4",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ pub fn main() !void {
|
|||
});
|
||||
defer platform.deinit();
|
||||
|
||||
var font = zenolith.font.Font.create(try platform.createFont(.{
|
||||
var font = zenolith.text.Font.create(try platform.createFont(.{
|
||||
.source = .{ .path = "/usr/share/fonts/noto/NotoSans-Regular.ttf" },
|
||||
}), {});
|
||||
defer font.deinit();
|
||||
|
@ -45,30 +45,68 @@ pub fn main() !void {
|
|||
},
|
||||
},
|
||||
.padding = 10,
|
||||
.font_size = 32,
|
||||
.text_color = zenolith.Color.fromInt(0xcdd6f4ff),
|
||||
.font_style = .{},
|
||||
};
|
||||
|
||||
root.data.attreebutes = attrs;
|
||||
}
|
||||
|
||||
root.downcast(zenolith.widget.Box).?.orth_expand = true;
|
||||
try root.downcast(zenolith.widget.Box).?.addChildPositioned(root, null, try zenolith.widget.Label.init(.{
|
||||
.alloc = alloc,
|
||||
.font = &font,
|
||||
.text = "Hello, Zenolith!",
|
||||
.size = 64,
|
||||
}), .center);
|
||||
try root.downcast(zenolith.widget.Box).?.addChildPositioned(
|
||||
root,
|
||||
null,
|
||||
try zenolith.widget.Label.init(alloc, .{
|
||||
.font = &font,
|
||||
.text = "Hello, Zenolith!",
|
||||
.style = .{ .size = 64 },
|
||||
}),
|
||||
.center,
|
||||
);
|
||||
|
||||
try root.addChild(null, try zenolith.widget.Label.init(.{
|
||||
.alloc = alloc,
|
||||
try root.addChild(null, try zenolith.widget.Label.init(alloc, .{
|
||||
.font = &font,
|
||||
.text = "Labels!",
|
||||
.size = 32,
|
||||
.style = .{ .size = 32 },
|
||||
}));
|
||||
|
||||
try root.addChild(null, try zenolith.widget.Button.init(alloc, "Click Me!"));
|
||||
|
||||
{
|
||||
var chunk = zenolith.text.Chunk.init(alloc);
|
||||
errdefer chunk.deinit();
|
||||
|
||||
try chunk.spans.append(.{ .span = try zenolith.text.Span.init(alloc, .{
|
||||
.font = &font,
|
||||
.text = "Span 1",
|
||||
.style = .{ .size = 32 },
|
||||
}) });
|
||||
try chunk.spans.append(.{ .span = try zenolith.text.Span.init(alloc, .{
|
||||
.font = &font,
|
||||
.text = "Span 2",
|
||||
.style = .{ .color = zenolith.Color.fromInt(0xff0000ff) },
|
||||
}) });
|
||||
try chunk.spans.append(.{ .span = try zenolith.text.Span.init(alloc, .{
|
||||
.font = &font,
|
||||
.text = "Span 3",
|
||||
}), .wrap_mode = .always });
|
||||
|
||||
try chunk.spans.append(.{ .span = try zenolith.text.Span.init(alloc, .{
|
||||
.font = &font,
|
||||
.text = "abcdefghijklmnopqrstuvwxyz",
|
||||
.style = .{ .size = 48 },
|
||||
}), .wrap_mode = .always });
|
||||
|
||||
for ("abcdefghijklmnopqrstuvwxyz") |ch| {
|
||||
try chunk.spans.append(.{ .span = try zenolith.text.Span.init(alloc, .{
|
||||
.font = &font,
|
||||
.text = &.{ch},
|
||||
.style = .{ .size = 48 },
|
||||
}) });
|
||||
}
|
||||
|
||||
try root.addChild(null, try zenolith.widget.ChunkView.init(alloc, chunk));
|
||||
}
|
||||
|
||||
try root.treevent(zenolith.treevent.Link{
|
||||
.parent = null,
|
||||
.platform = &zplatform,
|
||||
|
|
163
src/Sdl2Font.zig
163
src/Sdl2Font.zig
|
@ -12,61 +12,36 @@ atlas: *c.SDL_Texture,
|
|||
renderer: *c.SDL_Renderer,
|
||||
|
||||
/// This is an ArrayHashMap to speed up iteration which is requried for collision checking.
|
||||
glyphs: std.AutoArrayHashMap(GlyphProperties, Glyph),
|
||||
glyphs: std.AutoArrayHashMap(GlyphProperties, AtlasGlyph),
|
||||
|
||||
/// A buffer for creating glyph pixel data in for SDL2 textures.
|
||||
pixel_buf: std.ArrayList(u8),
|
||||
|
||||
const Sdl2Font = @This();
|
||||
|
||||
pub const Chunk = struct {
|
||||
font: *Sdl2Font,
|
||||
glyphs: std.ArrayList(PositionedGlyph),
|
||||
// Saving the size here to improve performance by not recalculating this.
|
||||
// This is an immutable data structure.
|
||||
size: zenolith.layout.Size,
|
||||
|
||||
pub fn deinit(self: Chunk) void {
|
||||
self.glyphs.deinit();
|
||||
}
|
||||
|
||||
pub fn getSize(self: Chunk) zenolith.layout.Size {
|
||||
return self.size;
|
||||
}
|
||||
};
|
||||
|
||||
const PositionedGlyph = struct {
|
||||
glyph: Glyph,
|
||||
pos: zenolith.layout.Position,
|
||||
};
|
||||
|
||||
pub const Glyph = struct {
|
||||
/// Rectangle in atlas-local coordinates of this glyph.
|
||||
pub const AtlasGlyph = struct {
|
||||
glyph: zenolith.text.Glyph,
|
||||
sprite: zenolith.layout.Rectangle,
|
||||
|
||||
/// Offset the glyph is rendered at.
|
||||
bearing: [2]i32,
|
||||
advance: usize,
|
||||
};
|
||||
|
||||
pub const GlyphProperties = struct {
|
||||
codepoint: u21,
|
||||
size: u32,
|
||||
size: usize,
|
||||
};
|
||||
|
||||
pub fn deinit(self_: Sdl2Font) void {
|
||||
var self = self_;
|
||||
pub fn deinit(self: *Sdl2Font) void {
|
||||
c.SDL_DestroyTexture(self.atlas);
|
||||
_ = c.FT_Done_Face(self.face);
|
||||
self.glyphs.deinit();
|
||||
self.pixel_buf.deinit();
|
||||
}
|
||||
|
||||
pub fn getGlyph(self: *Sdl2Font, props: GlyphProperties) !Glyph {
|
||||
if (self.glyphs.get(props)) |g| return g;
|
||||
pub fn getGlyph(self: *Sdl2Font, codepoint: u21, style: zenolith.text.Style) !zenolith.text.Glyph {
|
||||
const props = GlyphProperties{ .codepoint = codepoint, .size = style.size };
|
||||
if (self.glyphs.get(props)) |g| return g.glyph;
|
||||
|
||||
try ffi.handleFTError(c.FT_Set_Pixel_Sizes(self.face, 0, props.size));
|
||||
try ffi.handleFTError(c.FT_Load_Char(self.face, props.codepoint, c.FT_LOAD_RENDER));
|
||||
try ffi.handleFTError(c.FT_Set_Pixel_Sizes(self.face, 0, @intCast(style.size)));
|
||||
try ffi.handleFTError(c.FT_Load_Char(self.face, codepoint, c.FT_LOAD_RENDER));
|
||||
|
||||
const bmp = self.face.*.glyph.*.bitmap;
|
||||
|
||||
|
@ -75,20 +50,29 @@ pub fn getGlyph(self: *Sdl2Font, props: GlyphProperties) !Glyph {
|
|||
.size = .{ .width = 0, .height = 0 },
|
||||
} else try self.addAtlastSprite(bmp.buffer[0 .. bmp.rows * bmp.width], bmp.width);
|
||||
|
||||
const glyph = Glyph{
|
||||
.sprite = rect,
|
||||
const glyph = zenolith.text.Glyph{
|
||||
.codepoint = codepoint,
|
||||
.size = rect.size,
|
||||
.bearing = .{
|
||||
self.face.*.glyph.*.bitmap_left,
|
||||
self.face.*.glyph.*.bitmap_top,
|
||||
.x = self.face.*.glyph.*.bitmap_left,
|
||||
.y = -self.face.*.glyph.*.bitmap_top,
|
||||
},
|
||||
// I see no point in supporting negative glyph advance.
|
||||
.advance = @intCast(@max(0, self.face.*.glyph.*.advance.x) >> 6),
|
||||
};
|
||||
|
||||
try self.glyphs.put(props, glyph);
|
||||
try self.glyphs.put(props, .{ .glyph = glyph, .sprite = rect });
|
||||
return glyph;
|
||||
}
|
||||
|
||||
pub fn yOffset(self: *Sdl2Font, size: usize) usize {
|
||||
if (c.FT_Set_Pixel_Sizes(self.face, 0, @intCast(size)) != 0)
|
||||
// TODO: wonk
|
||||
@panic("Unable to FT_Set_Pixel_Sizes for determining y offset");
|
||||
|
||||
return @intCast(self.face.*.size.*.metrics.height >> 6);
|
||||
}
|
||||
|
||||
fn getSize(self: *Sdl2Font) zenolith.layout.Size {
|
||||
var w: c_int = 0;
|
||||
var h: c_int = 0;
|
||||
|
@ -110,8 +94,8 @@ pub fn addAtlastSprite(self: *Sdl2Font, data: []const u8, width: usize) !zenolit
|
|||
var collision = zenolith.layout.Rectangle{
|
||||
// start in bottom right
|
||||
.pos = .{
|
||||
.x = size.width - width,
|
||||
.y = size.height,
|
||||
.x = @intCast(size.width - width),
|
||||
.y = @intCast(size.height),
|
||||
},
|
||||
|
||||
// size of glyph + padding
|
||||
|
@ -146,7 +130,7 @@ pub fn addAtlastSprite(self: *Sdl2Font, data: []const u8, width: usize) !zenolit
|
|||
},
|
||||
};
|
||||
|
||||
if (collision.pos.y + collision.size.height - padding > size.height)
|
||||
if (@as(usize, @intCast(collision.pos.y)) + collision.size.height - padding > size.height)
|
||||
return error.AtlastTooSmall;
|
||||
|
||||
try self.pixel_buf.resize(data.len * 4);
|
||||
|
@ -174,93 +158,16 @@ pub fn addAtlastSprite(self: *Sdl2Font, data: []const u8, width: usize) !zenolit
|
|||
return rect;
|
||||
}
|
||||
|
||||
pub fn layout(
|
||||
self: *Sdl2Font,
|
||||
text: []const u8,
|
||||
size: usize,
|
||||
wrap: zenolith.font.TextWrap,
|
||||
) !zenolith.font.Chunk {
|
||||
_ = wrap;
|
||||
var glyphs = std.ArrayList(PositionedGlyph).init(self.pixel_buf.allocator);
|
||||
errdefer glyphs.deinit();
|
||||
|
||||
var cur_pos = zenolith.layout.Position{
|
||||
// We start in the middle of the possible range to leave as much space in all directions
|
||||
// as possible. This is all aligned to 0/0 in the last layout step.
|
||||
.x = std.math.maxInt(usize) / 2,
|
||||
.y = std.math.maxInt(usize) / 2,
|
||||
};
|
||||
|
||||
// This is the position of the top-left-most glyph. This is later subtracted from all positions.
|
||||
var min_pos = zenolith.layout.Position{
|
||||
.x = std.math.maxInt(usize),
|
||||
.y = std.math.maxInt(usize),
|
||||
};
|
||||
|
||||
// The height of the current line of text.
|
||||
var cur_line_height: usize = 0;
|
||||
|
||||
var iter = std.unicode.Utf8Iterator{ .i = 0, .bytes = text };
|
||||
while (iter.nextCodepoint()) |codepoint| {
|
||||
// TODO: correctly calculate size of next line first
|
||||
if (codepoint == '\n') {
|
||||
cur_pos.x = std.math.maxInt(usize) / 2;
|
||||
cur_pos.y += @intCast(self.face.*.size.*.metrics.height >> 6);
|
||||
|
||||
cur_line_height = 0;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const glyph = try self.getGlyph(.{
|
||||
.size = @intCast(size),
|
||||
.codepoint = codepoint,
|
||||
});
|
||||
const gpos = zenolith.layout.Position{
|
||||
// hacky addition with negative numbers
|
||||
.x = cur_pos.x +% @as(usize, @bitCast(@as(isize, glyph.bearing[0]))),
|
||||
.y = cur_pos.y -% @as(usize, @bitCast(@as(isize, glyph.bearing[1]))),
|
||||
};
|
||||
|
||||
try glyphs.append(.{
|
||||
.glyph = glyph,
|
||||
.pos = gpos,
|
||||
});
|
||||
|
||||
cur_pos.x += glyph.advance;
|
||||
|
||||
if (glyph.sprite.size.height > cur_line_height) cur_line_height = glyph.sprite.size.height;
|
||||
|
||||
if (gpos.x < min_pos.x) min_pos.x = gpos.x;
|
||||
if (gpos.y < min_pos.y) min_pos.y = gpos.y;
|
||||
}
|
||||
|
||||
var csize = zenolith.layout.Size.zero;
|
||||
|
||||
// Position glyphs at top left and calculate chunk size.
|
||||
for (glyphs.items) |*g| {
|
||||
const new_pos = g.pos.sub(min_pos);
|
||||
g.pos = new_pos;
|
||||
|
||||
const xmax = g.pos.x + g.glyph.sprite.size.width;
|
||||
const ymax = g.pos.y + g.glyph.sprite.size.height;
|
||||
if (xmax > csize.width) csize.width = xmax;
|
||||
if (ymax > csize.height) csize.height = ymax;
|
||||
}
|
||||
|
||||
return zenolith.font.Chunk.create(Chunk{
|
||||
.font = self,
|
||||
.glyphs = glyphs,
|
||||
.size = csize,
|
||||
}, {});
|
||||
pub fn getSprite(self: *Sdl2Font, codepoint: u21, size: usize) ?zenolith.layout.Rectangle {
|
||||
return (self.glyphs.get(.{ .codepoint = codepoint, .size = size }) orelse return null).sprite;
|
||||
}
|
||||
|
||||
fn isTouchingAnyOnTop(self: *Sdl2Font, rect: zenolith.layout.Rectangle) bool {
|
||||
if (rect.pos.y == 0) return true;
|
||||
for (self.glyphs.unmanaged.entries.items(.value)) |other| {
|
||||
if (other.sprite.pos.y + other.sprite.size.height == rect.pos.y and
|
||||
other.sprite.pos.x < rect.pos.x + rect.size.width and
|
||||
other.sprite.pos.x + other.sprite.size.width > rect.pos.x) return true;
|
||||
if (other.sprite.pos.y + @as(isize, @intCast(other.sprite.size.height)) == rect.pos.y and
|
||||
other.sprite.pos.x < rect.pos.x + @as(isize, @intCast(rect.size.width)) and
|
||||
other.sprite.pos.x + @as(isize, @intCast(other.sprite.size.width)) > rect.pos.x) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -268,9 +175,9 @@ fn isTouchingAnyOnTop(self: *Sdl2Font, rect: zenolith.layout.Rectangle) bool {
|
|||
fn isTouchingAnyOnLeft(self: *Sdl2Font, rect: zenolith.layout.Rectangle) bool {
|
||||
if (rect.pos.x == 0) return true;
|
||||
for (self.glyphs.unmanaged.entries.items(.value)) |other| {
|
||||
if (other.sprite.pos.x + other.sprite.size.width == rect.pos.x and
|
||||
other.sprite.pos.y < rect.pos.y + rect.size.height and
|
||||
other.sprite.pos.y + other.sprite.size.height > rect.pos.y) return true;
|
||||
if (other.sprite.pos.x + @as(isize, @intCast(other.sprite.size.width)) == rect.pos.x and
|
||||
other.sprite.pos.y < rect.pos.y + @as(isize, @intCast(rect.size.height)) and
|
||||
other.sprite.pos.y + @as(isize, @intCast(other.sprite.size.height)) > rect.pos.y) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -41,23 +41,33 @@ pub fn texturedRect(
|
|||
) != 0) return error.Render;
|
||||
}
|
||||
|
||||
pub fn text(
|
||||
pub fn span(
|
||||
self: *Sdl2Painter,
|
||||
pos: zenolith.layout.Position,
|
||||
zchunk: zenolith.font.Chunk,
|
||||
color: zenolith.Color,
|
||||
zspan: zenolith.text.Span,
|
||||
) !void {
|
||||
const chunk = zchunk.downcast(Sdl2Font.Chunk) orelse unreachable;
|
||||
if (c.SDL_SetTextureColorMod(chunk.font.atlas, color.r, color.g, color.b) != 0) return error.Render;
|
||||
if (c.SDL_SetTextureAlphaMod(chunk.font.atlas, color.a) != 0) return error.Render;
|
||||
for (chunk.glyphs.items) |g| {
|
||||
const font = zspan.font.downcast(Sdl2Font) orelse unreachable;
|
||||
|
||||
if (c.SDL_SetTextureColorMod(
|
||||
font.atlas,
|
||||
zspan.style.color.r,
|
||||
zspan.style.color.g,
|
||||
zspan.style.color.b,
|
||||
) != 0) return error.Render;
|
||||
if (c.SDL_SetTextureAlphaMod(font.atlas, zspan.style.color.a) != 0) return error.Render;
|
||||
|
||||
for (zspan.glyphs.items) |g| {
|
||||
if (c.SDL_RenderCopy(
|
||||
self.renderer,
|
||||
chunk.font.atlas,
|
||||
&util.toSdlRect(g.glyph.sprite),
|
||||
font.atlas,
|
||||
&util.toSdlRect(
|
||||
// This is sound as the span will have already gotten the glyph from the font,
|
||||
// causing it to add it to the map.
|
||||
font.getSprite(g.glyph.codepoint, zspan.style.size) orelse unreachable,
|
||||
),
|
||||
&util.toSdlRect(.{
|
||||
.pos = pos.add(g.pos),
|
||||
.size = g.glyph.sprite.size,
|
||||
.pos = pos.add(g.position),
|
||||
.size = g.glyph.size,
|
||||
}),
|
||||
) != 0) return error.Render;
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@ pub fn init(options: InitOptions) InitializeError!Sdl2Platform {
|
|||
window_pos[1],
|
||||
options.window_size[0],
|
||||
options.window_size[1],
|
||||
c.SDL_WINDOW_SHOWN, // TODO: add API to change this
|
||||
c.SDL_WINDOW_SHOWN | c.SDL_WINDOW_RESIZABLE, // TODO: add API to change this
|
||||
) orelse return error.CreateWindow;
|
||||
errdefer c.SDL_DestroyWindow(window);
|
||||
|
||||
|
@ -294,7 +294,7 @@ pub fn createFont(self: Sdl2Platform, opts: CreateFontOptions) CreateFontError!S
|
|||
.face = face,
|
||||
.atlas = atlas,
|
||||
.renderer = self.renderer,
|
||||
.glyphs = std.AutoArrayHashMap(Sdl2Font.GlyphProperties, Sdl2Font.Glyph).init(self.alloc),
|
||||
.glyphs = std.AutoArrayHashMap(Sdl2Font.GlyphProperties, Sdl2Font.AtlasGlyph).init(self.alloc),
|
||||
.pixel_buf = std.ArrayList(u8).init(self.alloc),
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue