From aa338969352b23ad3386f0c9574f970be809a2e2 Mon Sep 17 00:00:00 2001 From: Stephen Gutekanst Date: Wed, 20 Mar 2024 09:32:22 -0700 Subject: [PATCH] gfx: make Text ECS module use style entities (cleaner API design) Signed-off-by: Stephen Gutekanst --- examples/text/Game.zig | 84 ++++++++++++++++++++++-------------------- src/gfx/Text.zig | 69 ++++++++++++++++++---------------- 2 files changed, 83 insertions(+), 70 deletions(-) diff --git a/examples/text/Game.zig b/examples/text/Game.zig index c1f07832..57179c8f 100644 --- a/examples/text/Game.zig +++ b/examples/text/Game.zig @@ -27,6 +27,7 @@ frame_count: usize, texts: usize, rand: std.rand.DefaultPrng, time: f32, +style1: mach.ecs.EntityID, const d0 = 0.000001; @@ -49,45 +50,13 @@ pub const Pipeline = enum(u32) { const upscale = 1.0; -const style1 = Text.Style{ - .font_name = "Roboto Medium", // TODO - .font_size = 48 * gfx.px_per_pt, // 48pt - .font_weight = gfx.font_weight_normal, - .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 text1: []const []const u8 = &.{ + "Text but with spaces 😊\nand\n", + "italics\nand\n", + "bold\nand\n", }; -const segment1: []const @import("mach").gfx.Text.Segment = &.{ - .{ - .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, - }, -}; +const text2: []const []const u8 = &.{"!$?😊"}; pub fn init( engine: *mach.Engine.Mod, @@ -98,11 +67,41 @@ pub fn init( // The Mach .core is where we set window options, etc. 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 const player = try engine.newEntity(); 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, .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(.initPipeline, .{Text.PipelineOptions{ @@ -119,6 +118,7 @@ pub fn init( .texts = 0, .rand = std.rand.DefaultPrng.init(1337), .time = 0, + .style1 = style1, }; } @@ -177,7 +177,13 @@ pub fn tick( const new_entity = try engine.newEntity(); 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, .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; } diff --git a/src/gfx/Text.zig b/src/gfx/Text.zig index 38361c09..e2513b30 100644 --- a/src/gfx/Text.zig +++ b/src/gfx/Text.zig @@ -38,30 +38,30 @@ pub const components = struct { /// origin (0, 0) lives at the center of the window. pub const transform = Mat4x4; - /// Segments of text to render. - pub const text = []const Segment; -}; + /// String segments of UTF-8 encoded text to render. + /// + /// Expected to match the length of the style component. + pub const text = []const []const u8; -pub const Segment = struct { - /// A string of UTF-8 encoded text. - string: []const u8, + /// The style to apply to each segment of text. + /// + /// Expected to match the length of the text component. + pub const style = []const mach.ecs.EntityID; - /// Which style this text should use. - style: *const Style, -}; + /// Style component: desired font to render text with. + pub const font_name = []const u8; // TODO: ship a default font -pub const Style = struct { - /// The desired font to render text with. - font_name: []const u8, // TODO: ship a default font + /// Style component: font size in pixels + pub const font_size = f32; // e.g. 12 * mach.gfx.px_per_pt // 12pt - /// Font size in pixels. - font_size: f32 = 12 * mach.gfx.px_per_pt, // 12pt + /// Style component: font weight + 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, - - color: Vec4 = vec4(0, 0, 0, 1.0), + /// Style component: fill color + pub const color = Vec4; // e.g. vec4(0, 0, 0, 1.0), }; const Uniforms = extern struct { @@ -371,11 +371,9 @@ pub const local = struct { pipeline.num_glyphs = 0; var glyphs = std.ArrayListUnmanaged(Glyph){}; var transforms_offset: usize = 0; - // var colors_offset: usize = 0; var texture_update = false; while (archetypes_iter.next()) |archetype| { 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 // 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 // text change. const px_density = 2.0; - // var font_names = archetype.slice(.mach_gfx_text, .font_name); - // var font_sizes = archetype.slice(.mach_gfx_text, .font_size); - const texts = archetype.slice(.mach_gfx_text, .text); - for (texts) |text| { + const segment_lists = archetype.slice(.mach_gfx_text, .text); + const style_lists = archetype.slice(.mach_gfx_text, .style); + for (segment_lists, style_lists) |segments, styles| { var origin_x: f32 = 0.0; var origin_y: f32 = 0.0; - for (text) |segment| { + for (segments, styles) |segment, style| { // 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; var font = try gfx.Font.initBytes(font_bytes); 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 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 defer run.deinit(); - run.addText(segment.string); + run.addText(segment); try font.shape(&run); 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? if (codepoint != '\n') { const region = try pipeline.regions.getOrPut(engine.allocator, .{ .index = glyph.glyph_index, - .size = @bitCast(segment.style.font_size), + .size = @bitCast(font_size), }); if (!region.found_existing) { const rendered_glyph = try font.render(engine.allocator, glyph.glyph_index, .{ @@ -466,7 +473,7 @@ pub const local = struct { if (codepoint == '\n') { origin_x = 0; - origin_y -= segment.style.font_size; + origin_y -= font_size; } else { origin_x += glyph.advance.x(); }