gfx: make Text ECS module use style entities (cleaner API design)
Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
parent
0c3ae9a048
commit
aa33896935
2 changed files with 83 additions and 70 deletions
|
|
@ -27,6 +27,7 @@ frame_count: usize,
|
||||||
texts: usize,
|
texts: usize,
|
||||||
rand: std.rand.DefaultPrng,
|
rand: std.rand.DefaultPrng,
|
||||||
time: f32,
|
time: f32,
|
||||||
|
style1: mach.ecs.EntityID,
|
||||||
|
|
||||||
const d0 = 0.000001;
|
const d0 = 0.000001;
|
||||||
|
|
||||||
|
|
@ -49,45 +50,13 @@ pub const Pipeline = enum(u32) {
|
||||||
|
|
||||||
const upscale = 1.0;
|
const upscale = 1.0;
|
||||||
|
|
||||||
const style1 = Text.Style{
|
const text1: []const []const u8 = &.{
|
||||||
.font_name = "Roboto Medium", // TODO
|
"Text but with spaces 😊\nand\n",
|
||||||
.font_size = 48 * gfx.px_per_pt, // 48pt
|
"italics\nand\n",
|
||||||
.font_weight = gfx.font_weight_normal,
|
"bold\nand\n",
|
||||||
.italic = false,
|
|
||||||
.color = vec4(0.6, 1.0, 0.6, 1.0),
|
|
||||||
};
|
|
||||||
const style2 = blk: {
|
|
||||||
var v = style1;
|
|
||||||
v.italic = true;
|
|
||||||
break :blk v;
|
|
||||||
};
|
|
||||||
const style3 = blk: {
|
|
||||||
var v = style1;
|
|
||||||
v.font_weight = gfx.font_weight_bold;
|
|
||||||
break :blk v;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const segment1: []const @import("mach").gfx.Text.Segment = &.{
|
const text2: []const []const u8 = &.{"!$?😊"};
|
||||||
.{
|
|
||||||
.string = "Text but with spaces 😊\nand\n",
|
|
||||||
.style = &style1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.string = "italics\nand\n",
|
|
||||||
.style = &style2,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.string = "bold\nand\n",
|
|
||||||
.style = &style3,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const segment2: []const @import("mach").gfx.Text.Segment = &.{
|
|
||||||
.{
|
|
||||||
.string = "!$?😊",
|
|
||||||
.style = &style1,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn init(
|
pub fn init(
|
||||||
engine: *mach.Engine.Mod,
|
engine: *mach.Engine.Mod,
|
||||||
|
|
@ -98,11 +67,41 @@ pub fn init(
|
||||||
// The Mach .core is where we set window options, etc.
|
// The Mach .core is where we set window options, etc.
|
||||||
core.setTitle("gfx.Text example");
|
core.setTitle("gfx.Text example");
|
||||||
|
|
||||||
|
// TODO: a better way to initialize entities with default values
|
||||||
|
const style1 = try engine.newEntity();
|
||||||
|
try text_mod.set(style1, .font_name, "Roboto Medium"); // TODO
|
||||||
|
try text_mod.set(style1, .font_size, 48 * gfx.px_per_pt); // 48pt
|
||||||
|
try text_mod.set(style1, .font_weight, gfx.font_weight_normal);
|
||||||
|
try text_mod.set(style1, .italic, false);
|
||||||
|
try text_mod.set(style1, .color, vec4(0.6, 1.0, 0.6, 1.0));
|
||||||
|
|
||||||
|
const style2 = try engine.newEntity();
|
||||||
|
try text_mod.set(style2, .font_name, "Roboto Medium"); // TODO
|
||||||
|
try text_mod.set(style2, .font_size, 48 * gfx.px_per_pt); // 48pt
|
||||||
|
try text_mod.set(style2, .font_weight, gfx.font_weight_normal);
|
||||||
|
try text_mod.set(style2, .italic, true);
|
||||||
|
try text_mod.set(style2, .color, vec4(0.6, 1.0, 0.6, 1.0));
|
||||||
|
|
||||||
|
const style3 = try engine.newEntity();
|
||||||
|
try text_mod.set(style3, .font_name, "Roboto Medium"); // TODO
|
||||||
|
try text_mod.set(style3, .font_size, 48 * gfx.px_per_pt); // 48pt
|
||||||
|
try text_mod.set(style3, .font_weight, gfx.font_weight_bold);
|
||||||
|
try text_mod.set(style3, .italic, false);
|
||||||
|
try text_mod.set(style3, .color, vec4(0.6, 1.0, 0.6, 1.0));
|
||||||
|
|
||||||
// Create some text
|
// Create some text
|
||||||
const player = try engine.newEntity();
|
const player = try engine.newEntity();
|
||||||
try text_mod.set(player, .pipeline, @intFromEnum(Pipeline.default));
|
try text_mod.set(player, .pipeline, @intFromEnum(Pipeline.default));
|
||||||
try text_mod.set(player, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(vec3(0, 0, 0))));
|
try text_mod.set(player, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(vec3(0, 0, 0))));
|
||||||
try text_mod.set(player, .text, segment1);
|
|
||||||
|
// TODO: better storage mechanism for this
|
||||||
|
// TODO: this is a leak
|
||||||
|
const styles = try engine.allocator.alloc(mach.ecs.EntityID, 3);
|
||||||
|
styles[0] = style1;
|
||||||
|
styles[1] = style2;
|
||||||
|
styles[2] = style3;
|
||||||
|
try text_mod.set(player, .text, text1);
|
||||||
|
try text_mod.set(player, .style, styles);
|
||||||
|
|
||||||
text_mod.send(.init, .{});
|
text_mod.send(.init, .{});
|
||||||
text_mod.send(.initPipeline, .{Text.PipelineOptions{
|
text_mod.send(.initPipeline, .{Text.PipelineOptions{
|
||||||
|
|
@ -119,6 +118,7 @@ pub fn init(
|
||||||
.texts = 0,
|
.texts = 0,
|
||||||
.rand = std.rand.DefaultPrng.init(1337),
|
.rand = std.rand.DefaultPrng.init(1337),
|
||||||
.time = 0,
|
.time = 0,
|
||||||
|
.style1 = style1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -177,7 +177,13 @@ pub fn tick(
|
||||||
const new_entity = try engine.newEntity();
|
const new_entity = try engine.newEntity();
|
||||||
try text_mod.set(new_entity, .pipeline, @intFromEnum(Pipeline.default));
|
try text_mod.set(new_entity, .pipeline, @intFromEnum(Pipeline.default));
|
||||||
try text_mod.set(new_entity, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(new_pos)));
|
try text_mod.set(new_entity, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(new_pos)));
|
||||||
try text_mod.set(new_entity, .text, segment2);
|
|
||||||
|
// TODO: better storage mechanism for this
|
||||||
|
// TODO: this is a leak
|
||||||
|
const styles = try engine.allocator.alloc(mach.ecs.EntityID, 1);
|
||||||
|
styles[0] = game.state.style1;
|
||||||
|
try text_mod.set(new_entity, .text, text2);
|
||||||
|
try text_mod.set(new_entity, .style, styles);
|
||||||
|
|
||||||
game.state.texts += 1;
|
game.state.texts += 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,30 +38,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.
|
/// String segments of UTF-8 encoded text to render.
|
||||||
pub const text = []const Segment;
|
///
|
||||||
};
|
/// Expected to match the length of the style component.
|
||||||
|
pub const text = []const []const u8;
|
||||||
|
|
||||||
pub const Segment = struct {
|
/// The style to apply to each segment of text.
|
||||||
/// A string of UTF-8 encoded text.
|
///
|
||||||
string: []const u8,
|
/// Expected to match the length of the text component.
|
||||||
|
pub const style = []const mach.ecs.EntityID;
|
||||||
|
|
||||||
/// Which style this text should use.
|
/// Style component: desired font to render text with.
|
||||||
style: *const Style,
|
pub const font_name = []const u8; // TODO: ship a default font
|
||||||
};
|
|
||||||
|
|
||||||
pub const Style = struct {
|
/// Style component: font size in pixels
|
||||||
/// The desired font to render text with.
|
pub const font_size = f32; // e.g. 12 * mach.gfx.px_per_pt // 12pt
|
||||||
font_name: []const u8, // TODO: ship a default font
|
|
||||||
|
|
||||||
/// Font size in pixels.
|
/// Style component: font weight
|
||||||
font_size: f32 = 12 * mach.gfx.px_per_pt, // 12pt
|
pub const font_weight = u16; // e.g. mach.gfx.font_weight_normal
|
||||||
|
|
||||||
font_weight: u16 = mach.gfx.font_weight_normal,
|
/// Style component: italic text
|
||||||
|
pub const italic = bool; // e.g. false
|
||||||
|
|
||||||
italic: bool = false,
|
/// Style component: fill color
|
||||||
|
pub const color = Vec4; // e.g. vec4(0, 0, 0, 1.0),
|
||||||
color: Vec4 = vec4(0, 0, 0, 1.0),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Uniforms = extern struct {
|
const Uniforms = extern struct {
|
||||||
|
|
@ -371,11 +371,9 @@ pub const local = struct {
|
||||||
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 texture_update = false;
|
var texture_update = false;
|
||||||
while (archetypes_iter.next()) |archetype| {
|
while (archetypes_iter.next()) |archetype| {
|
||||||
const transforms = archetype.slice(.mach_gfx_text, .transform);
|
const transforms = archetype.slice(.mach_gfx_text, .transform);
|
||||||
// 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?
|
||||||
|
|
@ -390,37 +388,46 @@ pub const local = struct {
|
||||||
// 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);
|
const segment_lists = archetype.slice(.mach_gfx_text, .text);
|
||||||
// var font_sizes = archetype.slice(.mach_gfx_text, .font_size);
|
const style_lists = archetype.slice(.mach_gfx_text, .style);
|
||||||
const texts = archetype.slice(.mach_gfx_text, .text);
|
for (segment_lists, style_lists) |segments, styles| {
|
||||||
for (texts) |text| {
|
|
||||||
var origin_x: f32 = 0.0;
|
var origin_x: f32 = 0.0;
|
||||||
var origin_y: f32 = 0.0;
|
var origin_y: f32 = 0.0;
|
||||||
|
|
||||||
for (text) |segment| {
|
for (segments, styles) |segment, style| {
|
||||||
// Load a font
|
// Load a font
|
||||||
// TODO: resolve font by name, not hard-code
|
const font_name = engine.entities.getComponent(style, .mach_gfx_text, .font_name).?;
|
||||||
|
_ = font_name; // TODO: actually use font name
|
||||||
const font_bytes = @import("font-assets").fira_sans_regular_ttf;
|
const font_bytes = @import("font-assets").fira_sans_regular_ttf;
|
||||||
var font = try gfx.Font.initBytes(font_bytes);
|
var font = try gfx.Font.initBytes(font_bytes);
|
||||||
defer font.deinit(engine.allocator);
|
defer font.deinit(engine.allocator);
|
||||||
|
|
||||||
|
const font_size = engine.entities.getComponent(style, .mach_gfx_text, .font_size).?;
|
||||||
|
const font_weight = engine.entities.getComponent(style, .mach_gfx_text, .font_weight);
|
||||||
|
const italic = engine.entities.getComponent(style, .mach_gfx_text, .italic);
|
||||||
|
const color = engine.entities.getComponent(style, .mach_gfx_text, .color);
|
||||||
|
// TODO: actually apply these
|
||||||
|
_ = font_weight;
|
||||||
|
_ = italic;
|
||||||
|
_ = color;
|
||||||
|
|
||||||
// Create a text shaper
|
// Create a text shaper
|
||||||
var run = try gfx.TextRun.init();
|
var run = try gfx.TextRun.init();
|
||||||
run.font_size_px = segment.style.font_size;
|
run.font_size_px = font_size;
|
||||||
run.px_density = 2; // TODO
|
run.px_density = 2; // TODO
|
||||||
|
|
||||||
defer run.deinit();
|
defer run.deinit();
|
||||||
|
|
||||||
run.addText(segment.string);
|
run.addText(segment);
|
||||||
try font.shape(&run);
|
try font.shape(&run);
|
||||||
|
|
||||||
while (run.next()) |glyph| {
|
while (run.next()) |glyph| {
|
||||||
const codepoint = segment.string[glyph.cluster];
|
const codepoint = segment[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') {
|
||||||
const region = try pipeline.regions.getOrPut(engine.allocator, .{
|
const region = try pipeline.regions.getOrPut(engine.allocator, .{
|
||||||
.index = glyph.glyph_index,
|
.index = glyph.glyph_index,
|
||||||
.size = @bitCast(segment.style.font_size),
|
.size = @bitCast(font_size),
|
||||||
});
|
});
|
||||||
if (!region.found_existing) {
|
if (!region.found_existing) {
|
||||||
const rendered_glyph = try font.render(engine.allocator, glyph.glyph_index, .{
|
const rendered_glyph = try font.render(engine.allocator, glyph.glyph_index, .{
|
||||||
|
|
@ -466,7 +473,7 @@ pub const local = struct {
|
||||||
|
|
||||||
if (codepoint == '\n') {
|
if (codepoint == '\n') {
|
||||||
origin_x = 0;
|
origin_x = 0;
|
||||||
origin_y -= segment.style.font_size;
|
origin_y -= font_size;
|
||||||
} else {
|
} else {
|
||||||
origin_x += glyph.advance.x();
|
origin_x += glyph.advance.x();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue