gfx: individual styles for text segments
Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
parent
fc4c3d06a3
commit
e1ce5c5662
2 changed files with 95 additions and 89 deletions
181
src/gfx/Text.zig
181
src/gfx/Text.zig
|
|
@ -11,6 +11,7 @@ const vec2 = math.vec2;
|
||||||
const Vec2 = math.Vec2;
|
const Vec2 = math.Vec2;
|
||||||
const Vec3 = math.Vec3;
|
const Vec3 = math.Vec3;
|
||||||
const Vec4 = math.Vec4;
|
const Vec4 = math.Vec4;
|
||||||
|
const vec4 = math.vec4;
|
||||||
const Mat3x3 = math.Mat3x3;
|
const Mat3x3 = math.Mat3x3;
|
||||||
const Mat4x4 = math.Mat4x4;
|
const Mat4x4 = math.Mat4x4;
|
||||||
|
|
||||||
|
|
@ -19,14 +20,6 @@ pipelines: std.AutoArrayHashMapUnmanaged(u32, Pipeline),
|
||||||
|
|
||||||
pub const name = .mach_gfx_text;
|
pub const name = .mach_gfx_text;
|
||||||
|
|
||||||
/// Converts points to pixels. e.g. a 12pt font size `12.0 * points_to_pixels == 16.0`
|
|
||||||
pub const points_to_pixels = 4.0 / 3.0;
|
|
||||||
|
|
||||||
// TODO: italics/bold
|
|
||||||
//
|
|
||||||
// TODO: should users use multiple text entities for different italic/bold/color regions, or should
|
|
||||||
// we handle that internally?
|
|
||||||
//
|
|
||||||
// TODO: better/proper text layout, shaping
|
// TODO: better/proper text layout, shaping
|
||||||
//
|
//
|
||||||
// TODO: integrate freetype integration
|
// TODO: integrate freetype integration
|
||||||
|
|
@ -44,18 +37,30 @@ pub const components = struct {
|
||||||
/// origin (0, 0) lives at the center of the window.
|
/// origin (0, 0) lives at the center of the window.
|
||||||
pub const transform = Mat4x4;
|
pub const transform = Mat4x4;
|
||||||
|
|
||||||
|
/// Segments of text to render.
|
||||||
|
pub const text = []const Segment;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Segment = struct {
|
||||||
/// A string of UTF-8 encoded text.
|
/// A string of UTF-8 encoded text.
|
||||||
pub const text = []const u8;
|
string: []const u8,
|
||||||
|
|
||||||
/// The font to be rendered.
|
/// Which style this text should use.
|
||||||
pub const font_name = []const u8;
|
style: *const Style,
|
||||||
|
};
|
||||||
|
|
||||||
/// Font size in pixels. To convert from points to pixels, multiply by `points_to_pixels`.
|
pub const Style = struct {
|
||||||
pub const font_size = f32;
|
/// The desired font to render text with.
|
||||||
|
font_name: []const u8, // TODO: ship a default font
|
||||||
|
|
||||||
/// Text color.
|
/// Font size in pixels.
|
||||||
// TODO: actually respect color
|
font_size: f32 = 12 * mach.gfx.px_per_pt, // 12pt
|
||||||
pub const color = Vec4;
|
|
||||||
|
font_weight: u16 = mach.gfx.font_weight_normal,
|
||||||
|
|
||||||
|
italic: bool = false,
|
||||||
|
|
||||||
|
color: Vec4 = vec4(0, 0, 0, 1.0),
|
||||||
};
|
};
|
||||||
|
|
||||||
const Uniforms = extern struct {
|
const Uniforms = extern struct {
|
||||||
|
|
@ -354,9 +359,6 @@ pub fn machGfxTextUpdated(
|
||||||
.pipeline,
|
.pipeline,
|
||||||
.transform,
|
.transform,
|
||||||
.text,
|
.text,
|
||||||
.font_name,
|
|
||||||
.font_size,
|
|
||||||
.color,
|
|
||||||
} },
|
} },
|
||||||
} });
|
} });
|
||||||
|
|
||||||
|
|
@ -367,104 +369,105 @@ pub fn machGfxTextUpdated(
|
||||||
pipeline.num_glyphs = 0;
|
pipeline.num_glyphs = 0;
|
||||||
var glyphs = std.ArrayListUnmanaged(Glyph){};
|
var glyphs = std.ArrayListUnmanaged(Glyph){};
|
||||||
var transforms_offset: usize = 0;
|
var transforms_offset: usize = 0;
|
||||||
var colors_offset: usize = 0;
|
// var colors_offset: usize = 0;
|
||||||
var texture_update = false;
|
var texture_update = false;
|
||||||
while (archetypes_iter.next()) |archetype| {
|
while (archetypes_iter.next()) |archetype| {
|
||||||
var transforms = archetype.slice(.mach_gfx_text, .transform);
|
var transforms = archetype.slice(.mach_gfx_text, .transform);
|
||||||
var colors = archetype.slice(.mach_gfx_text, .color);
|
// var colors = archetype.slice(.mach_gfx_text, .color);
|
||||||
|
|
||||||
// TODO: confirm the lifetime of these slices is OK for writeBuffer, how long do they need
|
// TODO: confirm the lifetime of these slices is OK for writeBuffer, how long do they need
|
||||||
// to live?
|
// to live?
|
||||||
encoder.writeBuffer(pipeline.transforms, transforms_offset, transforms);
|
encoder.writeBuffer(pipeline.transforms, transforms_offset, transforms);
|
||||||
encoder.writeBuffer(pipeline.colors, colors_offset, colors);
|
// encoder.writeBuffer(pipeline.colors, colors_offset, colors);
|
||||||
|
|
||||||
transforms_offset += transforms.len;
|
transforms_offset += transforms.len;
|
||||||
colors_offset += colors.len;
|
// colors_offset += colors.len;
|
||||||
pipeline.num_texts += @intCast(transforms.len);
|
pipeline.num_texts += @intCast(transforms.len);
|
||||||
|
|
||||||
// Render texts
|
// Render texts
|
||||||
// TODO: this is very expensive and shouldn't be done here, should be done only on detected
|
// TODO: this is very expensive and shouldn't be done here, should be done only on detected
|
||||||
// text change.
|
// text change.
|
||||||
const px_density = 2.0;
|
const px_density = 2.0;
|
||||||
var font_names = archetype.slice(.mach_gfx_text, .font_name);
|
// var font_names = archetype.slice(.mach_gfx_text, .font_name);
|
||||||
var font_sizes = archetype.slice(.mach_gfx_text, .font_size);
|
// var font_sizes = archetype.slice(.mach_gfx_text, .font_size);
|
||||||
var texts = archetype.slice(.mach_gfx_text, .text);
|
var texts = archetype.slice(.mach_gfx_text, .text);
|
||||||
for (font_names, font_sizes, texts) |font_name, font_size, text| {
|
for (texts) |text| {
|
||||||
_ = font_name;
|
|
||||||
var origin_x: f32 = 0.0;
|
var origin_x: f32 = 0.0;
|
||||||
var origin_y: f32 = 0.0;
|
var origin_y: f32 = 0.0;
|
||||||
|
|
||||||
// Load a font
|
for (text) |segment| {
|
||||||
// TODO: resolve font by name, not hard-code
|
// Load a font
|
||||||
const font_bytes = @import("font-assets").fira_sans_regular_ttf;
|
// TODO: resolve font by name, not hard-code
|
||||||
var font = try gfx.Font.initBytes(font_bytes);
|
const font_bytes = @import("font-assets").fira_sans_regular_ttf;
|
||||||
defer font.deinit(engine.allocator);
|
var font = try gfx.Font.initBytes(font_bytes);
|
||||||
|
defer font.deinit(engine.allocator);
|
||||||
|
|
||||||
// Create a text shaper
|
// Create a text shaper
|
||||||
var run = try gfx.TextRun.init();
|
var run = try gfx.TextRun.init();
|
||||||
run.font_size_px = font_size;
|
run.font_size_px = segment.style.font_size;
|
||||||
run.px_density = 2; // TODO
|
run.px_density = 2; // TODO
|
||||||
|
|
||||||
defer run.deinit();
|
defer run.deinit();
|
||||||
|
|
||||||
run.addText(text);
|
run.addText(segment.string);
|
||||||
try font.shape(&run);
|
try font.shape(&run);
|
||||||
|
|
||||||
while (run.next()) |glyph| {
|
while (run.next()) |glyph| {
|
||||||
const codepoint = text[glyph.cluster];
|
const codepoint = segment.string[glyph.cluster];
|
||||||
// TODO: use flags(?) to detect newline, or at least something more reliable?
|
// TODO: use flags(?) to detect newline, or at least something more reliable?
|
||||||
if (codepoint != '\n') {
|
if (codepoint != '\n') {
|
||||||
var region = try pipeline.regions.getOrPut(engine.allocator, .{
|
var region = try pipeline.regions.getOrPut(engine.allocator, .{
|
||||||
.index = glyph.glyph_index,
|
.index = glyph.glyph_index,
|
||||||
.size = @bitCast(font_size),
|
.size = @bitCast(segment.style.font_size),
|
||||||
});
|
|
||||||
if (!region.found_existing) {
|
|
||||||
const rendered_glyph = try font.render(engine.allocator, glyph.glyph_index, .{
|
|
||||||
.font_size_px = run.font_size_px,
|
|
||||||
});
|
});
|
||||||
if (rendered_glyph.bitmap) |bitmap| {
|
if (!region.found_existing) {
|
||||||
var glyph_atlas_region = try pipeline.texture_atlas.reserve(engine.allocator, rendered_glyph.width, rendered_glyph.height);
|
const rendered_glyph = try font.render(engine.allocator, glyph.glyph_index, .{
|
||||||
pipeline.texture_atlas.set(glyph_atlas_region, @as([*]const u8, @ptrCast(bitmap.ptr))[0 .. bitmap.len * 4]);
|
.font_size_px = run.font_size_px,
|
||||||
texture_update = true;
|
});
|
||||||
|
if (rendered_glyph.bitmap) |bitmap| {
|
||||||
|
var glyph_atlas_region = try pipeline.texture_atlas.reserve(engine.allocator, rendered_glyph.width, rendered_glyph.height);
|
||||||
|
pipeline.texture_atlas.set(glyph_atlas_region, @as([*]const u8, @ptrCast(bitmap.ptr))[0 .. bitmap.len * 4]);
|
||||||
|
texture_update = true;
|
||||||
|
|
||||||
// Exclude the 1px blank space margin when describing the region of the texture
|
// Exclude the 1px blank space margin when describing the region of the texture
|
||||||
// that actually represents the glyph.
|
// that actually represents the glyph.
|
||||||
const margin = 1;
|
const margin = 1;
|
||||||
glyph_atlas_region.x += margin;
|
glyph_atlas_region.x += margin;
|
||||||
glyph_atlas_region.y += margin;
|
glyph_atlas_region.y += margin;
|
||||||
glyph_atlas_region.width -= margin * 2;
|
glyph_atlas_region.width -= margin * 2;
|
||||||
glyph_atlas_region.height -= margin * 2;
|
glyph_atlas_region.height -= margin * 2;
|
||||||
region.value_ptr.* = glyph_atlas_region;
|
region.value_ptr.* = glyph_atlas_region;
|
||||||
} else {
|
} else {
|
||||||
// whitespace
|
// whitespace
|
||||||
region.value_ptr.* = gfx.Atlas.Region{
|
region.value_ptr.* = gfx.Atlas.Region{
|
||||||
.width = 0,
|
.width = 0,
|
||||||
.height = 0,
|
.height = 0,
|
||||||
.x = 0,
|
.x = 0,
|
||||||
.y = 0,
|
.y = 0,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const r = region.value_ptr.*;
|
||||||
|
const size = vec2(@floatFromInt(r.width), @floatFromInt(r.height));
|
||||||
|
try glyphs.append(engine.allocator, .{
|
||||||
|
.pos = vec2(
|
||||||
|
origin_x + glyph.offset.x(),
|
||||||
|
origin_y - (size.y() - glyph.offset.y()),
|
||||||
|
).divScalar(px_density),
|
||||||
|
.size = size.divScalar(px_density),
|
||||||
|
.text_index = 0,
|
||||||
|
.uv_pos = vec2(@floatFromInt(r.x), @floatFromInt(r.y)),
|
||||||
|
});
|
||||||
|
pipeline.num_glyphs += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const r = region.value_ptr.*;
|
if (codepoint == '\n') {
|
||||||
const size = vec2(@floatFromInt(r.width), @floatFromInt(r.height));
|
origin_x = 0;
|
||||||
try glyphs.append(engine.allocator, .{
|
origin_y -= segment.style.font_size;
|
||||||
.pos = vec2(
|
} else {
|
||||||
origin_x + glyph.offset.x(),
|
origin_x += glyph.advance.x();
|
||||||
origin_y - (size.y() - glyph.offset.y()),
|
}
|
||||||
).divScalar(px_density),
|
|
||||||
.size = size.divScalar(px_density),
|
|
||||||
.text_index = 0,
|
|
||||||
.uv_pos = vec2(@floatFromInt(r.x), @floatFromInt(r.y)),
|
|
||||||
});
|
|
||||||
pipeline.num_glyphs += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (codepoint == '\n') {
|
|
||||||
origin_x = 0;
|
|
||||||
origin_y -= font_size;
|
|
||||||
} else {
|
|
||||||
origin_x += glyph.advance.x();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@ pub const Text = @import("Text.zig");
|
||||||
pub const Font = @import("font/main.zig").Font;
|
pub const Font = @import("font/main.zig").Font;
|
||||||
pub const TextRun = @import("font/main.zig").TextRun;
|
pub const TextRun = @import("font/main.zig").TextRun;
|
||||||
pub const Glyph = @import("font/main.zig").Glyph;
|
pub const Glyph = @import("font/main.zig").Glyph;
|
||||||
|
pub const px_per_pt = @import("font/main.zig").px_per_pt;
|
||||||
|
pub const font_weight_normal = 400;
|
||||||
|
pub const font_weight_bold = 700;
|
||||||
|
|
||||||
test {
|
test {
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue