Skip to main content

TextComponent

Struct TextComponent 

Source
pub struct TextComponent {
    pub text: String,
    pub font_size: f32,
    pub authored_font_size: f32,
    pub wrap_at: usize,
    pub authored_wrap_at: usize,
    pub word_wrap: bool,
    pub authored_word_wrap: bool,
    pub word_wrap_tokens: Vec<String>,
    pub authored_word_wrap_tokens: Vec<String>,
    /* private fields */
}
Expand description

Text component.

On registration, TextSystem expands this into per-glyph component trees.

Fields§

§text: String§font_size: f32

Effective visual scale applied to spawned glyph quads.

This affects glyph rendering only; layout still measures text in glyph columns. Style.font_size may temporarily override this value during layout, while authored_font_size preserves the authored/default size.

§authored_font_size: f32

Author-provided font size. Preserved when layout applies or removes a style-driven override so the effective font_size can be restored.

§wrap_at: usize

Wrap after this many characters. This is the effective value used by TextSystem for glyph layout. The layout pass may narrow it to fit the containing block, but it never exceeds Self::authored_wrap_at.

§authored_wrap_at: usize

Author-provided wrap cap. Captured at construction (and on decode) and never modified by the layout pass. Layout uses this as the upper bound when re-deriving wrap_at from the current container width — so when the container grows again, wrap_at can be widened back up to (but not past) authored_wrap_at.

Invariant: any future “set the wrap cap” mutation path (an MMS wrap_at(N) setter, a Rust set_wrap helper, etc.) MUST update both wrap_at and authored_wrap_at. Updating only wrap_at will be silently undone by the next layout pass.

§word_wrap: bool

If true, wrap only at whitespace boundaries (avoid breaking words). When false, wraps strictly by character count.

The layout pass may override this when the containing styled TC has a word_wrap style — see Self::authored_word_wrap for the authored/default value.

§authored_word_wrap: bool

Author-provided word-wrap mode. Preserved when layout applies or removes a style-driven override so the effective word_wrap can be restored.

§word_wrap_tokens: Vec<String>

Tokens after which wrapping is allowed when word_wrap == true.

This always includes whitespace tokens (space + tab) by default. The layout pass may override this when the containing styled TC has a word_wrap_tokens style — see Self::authored_word_wrap_tokens.

§authored_word_wrap_tokens: Vec<String>

Author-provided word-wrap tokens. Preserved alongside Self::authored_word_wrap for the same reason.

Implementations§

Source§

impl TextComponent

Source

pub const DEFAULT_FONT_SIZE: f32 = 1.0

Source

pub const DEFAULT_WRAP_AT: usize = 0

Default authored wrap cap: 0 = no author cap. Layout still wraps to fit the containing block; this default just means the author didn’t impose an additional column limit. To set an explicit cap, use with_wrap / with_word_wrap.

Source

pub const DEFAULT_WORD_WRAP_TOKENS: [&'static str; 2]

Source

pub fn new(text: impl Into<String>) -> Self

Examples found in repository?
examples/opacity-example.rs (line 54)
43fn spawn_text_label(universe: &mut engine::Universe, position: (f32, f32, f32), text: &str) {
44    // T_root { T_scale { TXT { filtering } } }
45    let text_root = universe
46        .world
47        .add_component(TransformComponent::new().with_position(position.0, position.1, position.2));
48
49    let text_scale = universe
50        .world
51        .add_component(TransformComponent::new().with_scale(0.18, 0.18, 1.0));
52    let _ = universe.attach(text_root, text_scale);
53
54    let text_c = universe.world.add_component(TextComponent::new(text));
55    let _ = universe.attach(text_scale, text_c);
56
57    // Keep it crisp.
58    let filtering = universe
59        .world
60        .add_component(TextureFilteringComponent::nearest());
61    let _ = universe.attach(text_c, filtering);
62
63    // White label.
64    let color = universe
65        .world
66        .add_component(ColorComponent::rgba(1.0, 1.0, 1.0, 1.0));
67    let _ = universe.attach(text_c, color);
68
69    universe.add(text_root);
70}
71
72fn text_block_dimensions(text: &str) -> (usize, usize) {
73    let mut max_cols = 0usize;
74    let mut rows = 1usize;
75    let mut cur = 0usize;
76
77    for ch in text.chars() {
78        if ch == '\n' {
79            max_cols = max_cols.max(cur);
80            cur = 0;
81            rows += 1;
82        } else {
83            cur += 1;
84        }
85    }
86    max_cols = max_cols.max(cur);
87
88    (max_cols.max(1), rows.max(1))
89}
90
91fn spawn_text_label_with_bg(
92    universe: &mut engine::Universe,
93    position: (f32, f32, f32),
94    text: &str,
95    bg_opacity: f32,
96) {
97    // T_root {
98    //   T_bg { R_bg { Color black, Opacity } }
99    //   T_scale { TXT { filtering } }
100    // }
101
102    let text_root = universe
103        .world
104        .add_component(TransformComponent::new().with_position(position.0, position.1, position.2));
105
106    // Background quad (slightly behind the glyph quads).
107    let (cols, rows) = text_block_dimensions(text);
108    let text_scale = 0.18_f32;
109    let pad_x = 0.55_f32;
110    let pad_y = 0.45_f32;
111    let bg_w = cols as f32 * text_scale + pad_x;
112    let bg_h = rows as f32 * text_scale + pad_y;
113
114    let bg_t = universe.world.add_component(
115        TransformComponent::new()
116            .with_position(1.5, 0.0, -0.02)
117            .with_scale(bg_w, bg_h, 1.0),
118    );
119    let bg_r = universe.world.add_component(RenderableComponent::square());
120    let _ = universe.attach(text_root, bg_t);
121    let _ = universe.attach(bg_t, bg_r);
122
123    let bg_c = universe
124        .world
125        .add_component(ColorComponent::rgba(0.0, 0.0, 0.0, 1.0));
126    let _ = universe.attach(bg_r, bg_c);
127
128    let bg_o = universe
129        .world
130        .add_component(OpacityComponent::new().with_opacity(bg_opacity));
131    let _ = universe.attach(bg_r, bg_o);
132
133    let text_scale_t = universe
134        .world
135        .add_component(TransformComponent::new().with_scale(text_scale, text_scale, 1.0));
136    let _ = universe.attach(text_root, text_scale_t);
137
138    let text_c = universe.world.add_component(TextComponent::new(text));
139    let _ = universe.attach(text_scale_t, text_c);
140
141    // Keep it crisp.
142    let filtering = universe
143        .world
144        .add_component(TextureFilteringComponent::nearest());
145    let _ = universe.attach(text_c, filtering);
146
147    // Force the label into the transparent pass so it layers correctly with the background.
148    // (Pass selection currently does not consider texture alpha.)
149    let color = universe
150        .world
151        .add_component(ColorComponent::rgba(1.0, 1.0, 1.0, 0.998));
152    let _ = universe.attach(text_c, color);
153
154    universe.add(text_root);
155}
More examples
Hide additional examples
examples/pointer-events.rs (line 325)
309fn spawn_label(
310    universe: &mut engine::Universe,
311    parent: engine::ecs::ComponentId,
312    pos: [f32; 3],
313    text: &str,
314) {
315    use engine::ecs::component::{
316        ColorComponent, EmissiveComponent, TextComponent, TextureFilteringComponent,
317        TransformComponent, TransparentCutoutComponent,
318    };
319
320    let root = universe.world.add_component(
321        TransformComponent::new()
322            .with_position(pos[0], pos[1], pos[2])
323            .with_scale(0.055, 0.055, 1.0),
324    );
325    let txt = universe.world.add_component(TextComponent::new(text));
326    let color = universe
327        .world
328        .add_component(ColorComponent::rgba(0.05, 0.05, 0.08, 1.0));
329    let emissive = universe.world.add_component(EmissiveComponent::on());
330    let filtering = universe
331        .world
332        .add_component(TextureFilteringComponent::nearest_magnification());
333    let cutout = universe
334        .world
335        .add_component(TransparentCutoutComponent::new());
336
337    let _ = universe.attach(parent, root);
338    let _ = universe.attach(root, txt);
339    let _ = universe.attach(txt, color);
340    let _ = universe.attach(txt, emissive);
341    let _ = universe.attach(txt, filtering);
342    let _ = universe.attach(txt, cutout);
343}
examples/router.rs (line 29)
3fn spawn_runtime_text(
4    universe: &mut engine::Universe,
5    owner: engine::ecs::ComponentId,
6    label: &str,
7) {
8    use engine::ecs::component::{
9        ColorComponent, EdgeInsets, SizeDimension, StyleComponent, TextComponent,
10        TransformComponent,
11    };
12
13    let root = universe
14        .world
15        .add_component_boxed_named(label, Box::new(TransformComponent::new()));
16    let style = universe.world.add_component_boxed_named(
17        format!("{label}_style"),
18        Box::new({
19            let mut style = StyleComponent::new();
20            style.margin = EdgeInsets::all(0.5);
21            style.padding = EdgeInsets::axes(0.75, 0.5);
22            style.width = SizeDimension::Auto;
23            style.background_color = Some([0.93, 0.88, 0.98, 1.0]);
24            style
25        }),
26    );
27    let text = universe
28        .world
29        .add_component_boxed_named(format!("{label}_text"), Box::new(TextComponent::new(label)));
30    let color = universe
31        .world
32        .add_component(ColorComponent::rgba(0.38, 0.14, 0.62, 1.0));
33
34    let _ = universe.world.add_child(root, style);
35    let _ = universe.world.add_child(root, text);
36    let _ = universe.world.add_child(text, color);
37
38    universe.attach(owner, root).expect("attach routed child");
39}
examples/text-example.rs (line 129)
108    fn spawn_text_style(
109        universe: &mut engine::Universe,
110        pos: [f32; 3],
111        scale: f32,
112        text: &str,
113        color: [f32; 4],
114        shadow: TextShadowComponent,
115        filtering: TextureFilteringComponent,
116    ) {
117        let root = universe.world.add_component(
118            TransformComponent::new()
119                .with_position(pos[0], pos[1], pos[2])
120                .with_scale(scale, scale, 1.0),
121        );
122
123        // Color must be an ancestor of the glyph renderables.
124        let color_id = universe
125            .world
126            .add_component(ColorComponent::rgba(color[0], color[1], color[2], color[3]));
127        let _ = universe.attach(root, color_id);
128
129        let text_id = universe.world.add_component(TextComponent::new(text));
130        let _ = universe.attach(color_id, text_id);
131
132        // Route into cutout pass for cleaner edges.
133        let cutout = universe
134            .world
135            .add_component(TransparentCutoutComponent::new());
136        let _ = universe.attach(text_id, cutout);
137
138        // Use the same atlas as the debug quad.
139        let tex = universe
140            .world
141            .add_component(TextureComponent::with_uri("assets/textures/font.dds"));
142        let _ = universe.attach(text_id, tex);
143
144        let shadow_id = universe.world.add_component(shadow);
145        let _ = universe.attach(text_id, shadow_id);
146
147        let filtering_id = universe.world.add_component(filtering);
148        let _ = universe.attach(text_id, filtering_id);
149
150        universe.add(root);
151    }
examples/diy-panel.rs (line 37)
5fn spawn_runtime_text(
6    universe: &mut engine::Universe,
7    owner: engine::ecs::ComponentId,
8    label: &str,
9) {
10    use engine::ecs::component::{
11        ColorComponent, EdgeInsets, SizeDimension, StyleComponent, TextComponent,
12        TransformComponent,
13    };
14
15    let root = universe.world.add_component_boxed_named(
16        label,
17        Box::new(TransformComponent::new().with_position(0.0, 0.0, 0.2)),
18    );
19    let style = universe.world.add_component_boxed_named(
20        format!("{label}_style"),
21        Box::new({
22            let mut style = StyleComponent::new();
23            style.margin = EdgeInsets::axes(0.25, 0.25);
24            style.padding = EdgeInsets::axes(0.5, 0.5);
25            style.height = SizeDimension::GlyphUnits(2.5);
26            style.width = SizeDimension::Auto;
27            style.background_color = Some([1.0, 0.80, 0.80, 1.0]);
28            style
29        }),
30    );
31    let text_root = universe.world.add_component_boxed_named(
32        format!("{label}_text_root"),
33        Box::new(TransformComponent::new().with_position(0.0, 0.0, 0.2)),
34    );
35    let text = universe
36        .world
37        .add_component_boxed_named(format!("{label}_text"), Box::new(TextComponent::new(label)));
38    let color = universe
39        .world
40        .add_component(ColorComponent::rgba(0.40, 0.05, 0.05, 1.0));
41
42    let _ = universe.world.add_child(root, style);
43    let _ = universe.world.add_child(root, text_root);
44    let _ = universe.world.add_child(text_root, text);
45    let _ = universe.world.add_child(text, color);
46
47    universe.attach(owner, root).expect("attach routed child");
48}
examples/triage/panel-pierce.rs (line 56)
16fn spawn_row(
17    universe: &mut engine::Universe,
18    scrolling: engine::ecs::ComponentId,
19    index: usize,
20) -> engine::ecs::ComponentId {
21    use engine::ecs::component::{
22        ColorComponent, RenderableComponent, TextComponent, TransformComponent,
23    };
24
25    let label = format!("row_{index:02}");
26    let row = universe.world.add_component_boxed_named(
27        label.clone(),
28        Box::new(TransformComponent::new().with_position(0.0, -(index as f32) * 1.15, 0.05)),
29    );
30    let panel = universe.world.add_component_boxed_named(
31        format!("{label}_panel"),
32        Box::new(
33            TransformComponent::new()
34                .with_position(2.3, -0.45, 0.0)
35                .with_scale(4.6, 0.9, 1.0),
36        ),
37    );
38    let panel_renderable = universe.world.add_component_boxed_named(
39        format!("{label}_rend"),
40        Box::new(RenderableComponent::square()),
41    );
42    let panel_color = universe
43        .world
44        .add_component(ColorComponent::rgba(0.98, 0.94, 0.78, 1.0));
45
46    let text_anchor = universe.world.add_component_boxed_named(
47        format!("{label}_text_anchor"),
48        Box::new(
49            TransformComponent::new()
50                .with_position(0.35, -0.38, 0.02)
51                .with_scale(0.11, 0.11, 0.11),
52        ),
53    );
54    let text = universe.world.add_component_boxed_named(
55        format!("{label}_text"),
56        Box::new(TextComponent::new(label.clone())),
57    );
58    let text_color = universe
59        .world
60        .add_component(ColorComponent::rgba(0.20, 0.16, 0.08, 1.0));
61
62    let _ = universe.world.add_child(row, panel);
63    let _ = universe.world.add_child(panel, panel_renderable);
64    let _ = universe.world.add_child(panel_renderable, panel_color);
65    let _ = universe.world.add_child(row, text_anchor);
66    let _ = universe.world.add_child(text_anchor, text);
67    let _ = universe.world.add_child(text, text_color);
68
69    universe.attach(scrolling, row).expect("attach scroll row");
70    row
71}
Source

pub fn with_wrap(text: impl Into<String>, wrap_at: usize) -> Self

Examples found in repository?
examples/folder-text.rs (lines 398-401)
60fn main() {
61    mittens_engine::example_support::ensure_model_assets();
62    utils::logger::init();
63
64    // Usage:
65    //   cargo run --example folder-text -- [folder] [spacing]
66    // Defaults:
67    //   folder=src  spacing=0.3
68    let mut args = std::env::args().skip(1);
69    let folder = args.next().unwrap_or_else(|| "src".to_string());
70    let spacing: f32 = args
71        .next()
72        .and_then(|s| s.parse::<f32>().ok())
73        .unwrap_or(1.0);
74
75    // Safety caps: this demo can explode the ECS if we load huge projects.
76    const MAX_FILES: usize = 42;
77    const MAX_CHARS_PER_FILE: usize = 10_000;
78    const WRAP_AT: usize = 90;
79    const TEXT_SCALE: f32 = 0.01;
80
81    let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
82    let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
83    let scan_root = {
84        let p = PathBuf::from(&folder);
85        if p.is_absolute() {
86            p
87        } else {
88            // Resolve relative paths from the crate root, not the process CWD.
89            manifest_dir.join(p)
90        }
91    };
92    eprintln!("[folder-text] cwd={:?}", cwd);
93    eprintln!("[folder-text] manifest_dir={:?}", manifest_dir);
94    eprintln!("[folder-text] scan_root={:?}", scan_root);
95
96    let mut files = Vec::new();
97    collect_rs_files(&scan_root, &mut files);
98    files.sort();
99
100    if files.is_empty() {
101        eprintln!(
102            "[folder-text] No .rs files found under folder={:?} (resolved={:?})",
103            folder, scan_root
104        );
105    }
106
107    let files: Vec<PathBuf> = files.into_iter().take(MAX_FILES).collect();
108    eprintln!(
109        "[folder-text] Loaded {} file(s) from {:?} (spacing={})",
110        files.len(),
111        folder,
112        spacing
113    );
114
115    #[derive(Debug, Clone)]
116    struct FileEntry {
117        path: PathBuf,
118        display_text: String,
119        max_cols: usize,
120        rows: usize,
121    }
122
123    // Group files by folder.
124    // Layout:
125    // - each folder becomes a column along +X
126    // - within a folder, files stack along +Y (8 high)
127    // - after 8, additional files start a new stack further along +Z
128    let mut files_by_folder: BTreeMap<PathBuf, Vec<FileEntry>> = BTreeMap::new();
129    for path in files {
130        let Ok(content) = std::fs::read_to_string(&path) else {
131            eprintln!("[folder-text] Failed to read {:?}", path);
132            continue;
133        };
134
135        let mut display_text = format!(
136            "// {}\n\n{}",
137            path.strip_prefix(&manifest_dir).unwrap_or(&path).display(),
138            content
139        );
140
141        if display_text.chars().count() > MAX_CHARS_PER_FILE {
142            display_text = display_text
143                .chars()
144                .take(MAX_CHARS_PER_FILE)
145                .collect::<String>();
146            display_text.push_str("\n\n// ... truncated ...\n");
147        }
148
149        let (max_cols, rows) = measure_text_bounds(&display_text, WRAP_AT);
150
151        let folder_key = path.parent().unwrap_or(&scan_root).to_path_buf();
152        files_by_folder
153            .entry(folder_key)
154            .or_default()
155            .push(FileEntry {
156                path,
157                display_text,
158                max_cols,
159                rows,
160            });
161    }
162
163    let column_count = files_by_folder.len().max(1);
164
165    let world = engine::ecs::World::default();
166    let mut universe = engine::Universe::new(world);
167
168    let bg_r = 0.25;
169    let bg_g = 0.25;
170    let bg_b = 0.25;
171    let background = universe
172        .world
173        .add_component(engine::ecs::component::BackgroundColorComponent::new());
174    let background_c = universe
175        .world
176        .add_component(engine::ecs::component::ColorComponent::rgba(
177            bg_r, bg_g, bg_b, 1.00,
178        ));
179    let _ = universe.world.add_child(background, background_c);
180    universe.add(background);
181
182    let ambient = universe
183        .world
184        .add_component(engine::ecs::component::AmbientLightComponent::rgb(
185            bg_r, bg_g, bg_b,
186        ));
187    universe.add(ambient);
188
189    // Input-driven camera rig.
190    // Topology: I { T { C3D } }
191    let input = universe
192        .world
193        .add_component(engine::ecs::component::InputComponent::new().with_speed(1.5));
194
195    // Center the camera horizontally over the spawned columns.
196    let center_x = if column_count <= 1 {
197        0.0
198    } else {
199        ((column_count - 1) as f32) * spacing * 0.5
200    };
201
202    // Bring camera closer: these text blocks are tiny.
203    let rig_transform = universe.world.add_component(
204        engine::ecs::component::TransformComponent::new().with_position(center_x, 0.0, 1.2),
205    );
206    let camera3d = universe
207        .world
208        .add_component(engine::ecs::component::Camera3DComponent::new());
209    let input_mode = universe.world.add_component(
210        engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
211    );
212    let _ = universe.attach(input, input_mode);
213    let _ = universe.attach(input, rig_transform);
214    let _ = universe.attach(rig_transform, camera3d);
215
216    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
217    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
218    universe.add(input);
219
220    // Big floor plane under everything.
221    // RenderableComponent::square() is an XY quad facing +Z; rotate it to XZ facing +Y.
222    let max_stacks = files_by_folder
223        .values()
224        .map(|v| (v.len() + 7) / 8)
225        .max()
226        .unwrap_or(1)
227        .max(1);
228    let total_width = (column_count as f32) * spacing;
229    let total_depth = (max_stacks as f32) * spacing;
230    let floor_w = total_width.max(10.0);
231    let floor_h = total_depth.max(10.0);
232    let floor_transform = universe.world.add_component(
233        engine::ecs::component::TransformComponent::new()
234            .with_position(0.0, -2.0, 0.0)
235            .with_rotation_euler(-std::f32::consts::FRAC_PI_2, 0.0, 0.0)
236            .with_scale(floor_w, floor_h, 1.0),
237    );
238    let floor_renderable = universe
239        .world
240        .add_component(engine::ecs::component::RenderableComponent::square());
241    let floor_color = universe
242        .world
243        .add_component(engine::ecs::component::ColorComponent::rgba(
244            0.88, 0.88, 0.88, 1.0,
245        ));
246    let _ = universe.attach(floor_transform, floor_renderable);
247    let _ = universe.attach(floor_renderable, floor_color);
248    universe.add(floor_transform);
249
250    // 4 red cubes around the perimeter of the world (easy visual anchors).
251    fn spawn_red_cube(universe: &mut engine::Universe, x: f32, y: f32, z: f32, s: f32) {
252        let t = universe.world.add_component(
253            engine::ecs::component::TransformComponent::new()
254                .with_position(x, y, z)
255                .with_scale(s, s, s),
256        );
257        let r = universe
258            .world
259            .add_component(engine::ecs::component::RenderableComponent::cube());
260        let c = universe
261            .world
262            .add_component(engine::ecs::component::ColorComponent::rgba(
263                1.0, 0.0, 0.0, 1.0,
264            ));
265
266        let light_transform = universe.world.add_component(
267            engine::ecs::component::TransformComponent::new().with_position(0.0, s * 0.75, 0.0),
268        );
269        let light = universe.world.add_component(
270            engine::ecs::component::PointLightComponent::new()
271                .with_intensity(1.5)
272                .with_distance(20.0)
273                .with_color(1.0, 0.0, 0.0),
274        );
275
276        let _ = universe.attach(t, r);
277        let _ = universe.attach(r, c);
278        let _ = universe.attach(t, light_transform);
279        let _ = universe.attach(light_transform, light);
280
281        universe.add(t);
282    }
283
284    let p = 1.5;
285    let s = 0.25;
286    spawn_red_cube(&mut universe, -p, -p, 0.0, s);
287    spawn_red_cube(&mut universe, p, -p, 0.0, s);
288    spawn_red_cube(&mut universe, -p, p, 0.0, s);
289    spawn_red_cube(&mut universe, p, p, 0.0, s);
290
291    let start_x = -center_x;
292    let stack_depth = spacing;
293    let row_gap_world = 0.35;
294
295    // One global point light at the top of the overall text "tower".
296    let pad_y = 4.0;
297    let global_max_panel_h_world = files_by_folder
298        .values()
299        .flat_map(|v| v.iter())
300        .map(|e| ((e.rows as f32) + pad_y) * TEXT_SCALE)
301        .fold(0.0_f32, f32::max)
302        .max(0.6);
303    let global_slot_h_world = global_max_panel_h_world + row_gap_world;
304    let tower_top_y = (7.0 * global_slot_h_world) + 4.0;
305    let tower_center_z = total_depth * 0.5;
306
307    let tower_light_transform = universe.world.add_component(
308        engine::ecs::component::TransformComponent::new().with_position(
309            0.0,
310            tower_top_y,
311            tower_center_z,
312        ),
313    );
314    let tower_light = universe.world.add_component(
315        engine::ecs::component::PointLightComponent::new()
316            .with_intensity(2.0)
317            .with_distance(120.0)
318            .with_color(1.0, 1.0, 1.0),
319    );
320    let _ = universe.attach(tower_light_transform, tower_light);
321    universe.add(tower_light_transform);
322
323    for (col_idx, (_folder, mut entries)) in files_by_folder.into_iter().enumerate() {
324        entries.sort_by(|a, b| a.path.cmp(&b.path));
325
326        // Slot height (world units) based on the tallest panel in this folder.
327        let pad_y = 4.0;
328        let max_panel_h_world = entries
329            .iter()
330            .map(|e| ((e.rows as f32) + pad_y) * TEXT_SCALE)
331            .fold(0.0_f32, f32::max)
332            .max(0.6);
333        let slot_h_world = max_panel_h_world + row_gap_world;
334
335        let col_x = start_x + (col_idx as f32) * spacing;
336
337        for (i, entry) in entries.into_iter().enumerate() {
338            let row = (i % 8) as f32;
339            let stack = (i / 8) as f32;
340            let file_y = row * slot_h_world;
341            let file_z = stack * stack_depth;
342
343            let file_group = universe.world.add_component(
344                engine::ecs::component::TransformComponent::new()
345                    .with_position(col_x, file_y, file_z),
346            );
347
348            // Text subtree (tiny scale).
349            let file_root = universe.world.add_component(
350                engine::ecs::component::TransformComponent::new()
351                    .with_position(0.0, 1.0, 0.0)
352                    .with_scale(TEXT_SCALE, TEXT_SCALE, 1.0)
353                    .with_rotation_euler(0.0, std::f32::consts::PI / 6.0, 0.0),
354            );
355            let _ = universe.attach(file_group, file_root);
356
357            // Background panel behind the text.
358            // Size is based on wrapped line count (matches TextSystem's strict wrapping behavior).
359            let (max_cols, rows) = (entry.max_cols, entry.rows);
360            let pad_x = 4.0;
361            let pad_y = 4.0;
362            let w = (max_cols as f32) + pad_x;
363            let h = (rows as f32) + pad_y;
364
365            // Text glyph quads are centered at integer (col,row) positions and are 1x1, so the
366            // text's AABB (in text-space) is roughly:
367            //   x: [-0.5, max_cols-0.5]
368            //   y: [-(rows-1)-0.5, 0.5]
369            // Centering the background quad at those midpoints keeps it aligned as text grows.
370            let bg_x = (max_cols as f32 - 1.0) * 0.5;
371            let bg_y = -((rows as f32 - 1.0) * 0.5);
372
373            let bg_transform = universe.world.add_component(
374                engine::ecs::component::TransformComponent::new()
375                    .with_position(bg_x, bg_y, -0.05)
376                    .with_scale(w, h, 1.0),
377            );
378            let bg_renderable = universe
379                .world
380                .add_component(engine::ecs::component::RenderableComponent::square());
381            let bg_quant = universe.world.add_component(
382                engine::ecs::component::LightQuantizationComponent::steps(5.0),
383            );
384            let bg_color =
385                universe
386                    .world
387                    .add_component(engine::ecs::component::ColorComponent::rgba(
388                        0.2, 0.2, 0.2, 1.0,
389                    ));
390            let _ = universe.attach(file_root, bg_transform);
391            let _ = universe.attach(bg_transform, bg_renderable);
392            let _ = universe.attach(bg_renderable, bg_quant);
393            let _ = universe.attach(bg_renderable, bg_color);
394
395            let text =
396                universe
397                    .world
398                    .add_component(engine::ecs::component::TextComponent::with_wrap(
399                        entry.display_text,
400                        WRAP_AT,
401                    ));
402            let cutout = universe
403                .world
404                .add_component(engine::ecs::component::TransparentCutoutComponent::new());
405            let filtering = universe.world.add_component(
406                engine::ecs::component::TextureFilteringComponent::nearest_magnification(),
407            );
408            // let color = universe
409            //     .world
410            //     .add_component(engine::ecs::component::ColorComponent::rgba(0.7, 0.7, 1.0, 1.0));
411            let emissive = universe
412                .world
413                .add_component(engine::ecs::component::EmissiveComponent::on());
414            let _ = universe.attach(file_root, text);
415            let _ = universe.attach(text, cutout);
416            let _ = universe.attach(text, filtering);
417            //let _ = universe.world.add_child(text, color);
418            let _ = universe.attach(text, emissive);
419
420            universe.add(file_group);
421        }
422    }
423
424    // Process init-time registrations (Text expands into glyph subtrees here).
425    universe.systems.process_commands(
426        &mut universe.world,
427        &mut universe.visuals,
428        &mut universe.render_assets,
429        &mut universe.command_queue,
430    );
431
432    engine::Windowing::run_app(universe).expect("Windowing failed");
433}
Source

pub fn with_word_wrap(text: impl Into<String>, wrap_at: usize) -> Self

Word-wrap (prefer wrapping at whitespace) aiming for wrap_at characters.

If the line exceeds wrap_at and there was no whitespace to wrap at, the line will continue (words are not broken).

Examples found in repository?
examples/animation-example.rs (lines 110-112)
101    fn spawn_text(universe: &mut engine::Universe, pos: (f32, f32, f32), scale: f32, text: &str) {
102        let tx = universe.world.add_component(
103            engine::ecs::component::TransformComponent::new()
104                .with_position(pos.0, pos.1, pos.2)
105                .with_scale(scale, scale, 1.0),
106        );
107        let t =
108            universe
109                .world
110                .add_component(engine::ecs::component::TextComponent::with_word_wrap(
111                    text, 38,
112                ));
113        let _ = universe.attach(tx, t);
114        universe.add(tx);
115    }
More examples
Hide additional examples
examples/audio-graph-example.rs (lines 214-216)
199    fn spawn_text(
200        universe: &mut engine::Universe,
201        pos: (f32, f32, f32),
202        scale: f32,
203        wrap_cols: usize,
204        text: &str,
205    ) {
206        let tx = universe.world.add_component(
207            engine::ecs::component::TransformComponent::new()
208                .with_position(pos.0, pos.1, pos.2)
209                .with_scale(scale, scale, 1.0),
210        );
211        let t =
212            universe
213                .world
214                .add_component(engine::ecs::component::TextComponent::with_word_wrap(
215                    text, wrap_cols,
216                ));
217        let _ = universe.attach(tx, t);
218
219        // TextSystem looks for an immediate TextureFilteringComponent child.
220        let filtering = universe
221            .world
222            .add_component(engine::ecs::component::TextureFilteringComponent::nearest());
223        let _ = universe.attach(t, filtering);
224
225        // TextSystem also supports styling from immediate Emissive children.
226        let emissive = universe
227            .world
228            .add_component(engine::ecs::component::EmissiveComponent::on());
229        let _ = universe.attach(t, emissive);
230
231        universe.add(tx);
232    }
233
234    fn spawn_emissive_cube(
235        universe: &mut engine::Universe,
236        parent: engine::ecs::ComponentId,
237        pos: (f32, f32, f32),
238        scale: f32,
239        rgba: [f32; 4],
240    ) {
241        let tx = universe.world.add_component(
242            engine::ecs::component::TransformComponent::new()
243                .with_position(pos.0, pos.1, pos.2)
244                .with_scale(scale, scale, scale),
245        );
246        let r = universe
247            .world
248            .add_component(engine::ecs::component::RenderableComponent::cube());
249        let c = universe
250            .world
251            .add_component(engine::ecs::component::ColorComponent::rgba(
252                rgba[0], rgba[1], rgba[2], rgba[3],
253            ));
254        let e = universe
255            .world
256            .add_component(engine::ecs::component::EmissiveComponent::on());
257        let _ = universe.attach(parent, tx);
258        let _ = universe.attach(tx, r);
259        let _ = universe.attach(r, c);
260        let _ = universe.attach(r, e);
261    }
262
263    fn spawn_op_cube(
264        universe: &mut engine::Universe,
265        parent: engine::ecs::ComponentId,
266        pos: (f32, f32, f32),
267        scale: f32,
268        base_rgba: [f32; 4],
269    ) -> engine::ecs::ComponentId {
270        let tx = universe.world.add_component(
271            engine::ecs::component::TransformComponent::new()
272                .with_position(pos.0, pos.1, pos.2)
273                .with_scale(scale, scale, scale),
274        );
275        let r = universe
276            .world
277            .add_component(engine::ecs::component::RenderableComponent::cube());
278        let c = universe
279            .world
280            .add_component(engine::ecs::component::ColorComponent::rgba(
281                base_rgba[0],
282                base_rgba[1],
283                base_rgba[2],
284                base_rgba[3],
285            ));
286        let e = universe
287            .world
288            .add_component(engine::ecs::component::EmissiveComponent::on());
289
290        let _ = universe.attach(parent, tx);
291        let _ = universe.attach(tx, r);
292        let _ = universe.attach(r, c);
293        let _ = universe.attach(r, e);
294
295        tx
296    }
297
298    // --- HUD / lane (single track) ---
299    let lane_x = -2.6_f32;
300    let lane_title_z = -0.4_f32;
301    let lane_cfg_z = -0.4_f32;
302    let lane_pat_z = -1.3_f32;
303    let lane_graph_z = -1.15_f32;
304
305    // Content for pattern/chain/graph starts at x=0; keep section labels aligned there.
306    // This also keeps them from overlapping the config block (which is anchored at lane_x).
307    let lane_labels_x = lane_x + 1.6_f32;
308
309    let track_a_y = 0.9_f32;
310
311    spawn_text(
312        &mut universe,
313        (lane_x, track_a_y + 0.55, lane_title_z),
314        0.09,
315        42,
316        "Track A: AudioOscillator::square()",
317    );
318    spawn_text(
319        &mut universe,
320        (lane_x, track_a_y + 0.25, lane_cfg_z),
321        0.07,
322        48,
323        "oscillators=1\nfrequency_hz=110.0\namplitude=0.12\nenabled=false\nlookahead=0.10s",
324    );
325
326    let viz_root = universe.world.add_component(
327        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
328    );
329    universe.add(viz_root);
330
331    println!("[audio-graph-example] viz root added");
332
333    // Small identifier cubes next to titles.
334    spawn_emissive_cube(
335        &mut universe,
336        viz_root,
337        (lane_x - 0.28, track_a_y + 0.55, lane_title_z),
338        0.16,
339        [0.85, 0.40, 1.00, 1.0],
340    );
341
342    // Pattern roots so we can reset via one SetColor action.
343    let track_a_pattern_root = universe.world.add_component(
344        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
345    );
346    let _ = universe.attach(viz_root, track_a_pattern_root);
347
348    // Labels for pattern/chain/graph sections.
349    spawn_text(
350        &mut universe,
351        (lane_labels_x, track_a_y, lane_title_z),
352        0.06,
353        42,
354        "pattern",
355    );
356    spawn_text(
357        &mut universe,
358        (lane_labels_x, track_a_y - 0.40, lane_title_z),
359        0.06,
360        42,
361        "graph",
362    );
363
364    // --- Processing chain + graph visualization (compiled graph → cubes + labels) ---
365    use engine::ecs::system::audio_graph_compiler::{
366        AudioGraphCompiler, AudioGraphNode, AudioGraphNodeKind,
367    };
368
369    fn effect_grey_for_depth(depth: usize) -> f32 {
370        // depth=1 => medium grey; deeper => lighter, capped.
371        let base = 0.55;
372        let step = 0.13;
373        let d = depth.saturating_sub(1) as f32;
374        (base + step * d).min(0.92)
375    }
376
377    fn node_rgba(node: &AudioGraphNode, depth: usize) -> [f32; 4] {
378        match node.kind {
379            AudioGraphNodeKind::OscillatorSource { .. } => [1.0, 0.78, 0.22, 1.0],
380            _ => {
381                let g = effect_grey_for_depth(depth);
382                [g, g, g, 1.0]
383            }
384        }
385    }
386
387    fn node_label(node: &AudioGraphNode) -> String {
388        match &node.kind {
389            AudioGraphNodeKind::OscillatorSource { voices } => {
390                format!("OscillatorSource voices={voices}")
391            }
392            AudioGraphNodeKind::Gain { gain } => {
393                format!("Gain gain={gain:.3}")
394            }
395            AudioGraphNodeKind::LowPass {
396                cutoff_hz,
397                resonance,
398            } => {
399                format!("LowPass cutoff={cutoff_hz:.1}Hz res={resonance:.3}")
400            }
401            AudioGraphNodeKind::BandPass {
402                center_hz,
403                bandwidth_octaves,
404                resonance,
405            } => {
406                format!(
407                    "BandPass center={center_hz:.1}Hz bw={bandwidth_octaves:.3}oct res={resonance:.3}"
408                )
409            }
410            AudioGraphNodeKind::HighPass {
411                cutoff_hz,
412                resonance,
413            } => {
414                format!("HighPass cutoff={cutoff_hz:.1}Hz res={resonance:.3}")
415            }
416            AudioGraphNodeKind::Limiter {
417                attack_ms,
418                release_ms,
419                threshold,
420            } => {
421                format!("Limiter atk={attack_ms:.1}ms rel={release_ms:.1}ms thr={threshold:.3}")
422            }
423            AudioGraphNodeKind::ClipSource => "ClipSource".to_string(),
424        }
425    }
426
427    fn compute_layout(
428        node: &AudioGraphNode,
429        depth: usize,
430        x_cursor: &mut i32,
431        out: &mut std::collections::HashMap<*const AudioGraphNode, (i32, usize)>,
432    ) -> i32 {
433        // Depth-only layout:
434        // - keep children directly below their parent on X
435        // - use Z sibling offsets (in spawn_graph_tree) to show branching
436        let _ = x_cursor;
437        for ch in node.children.iter() {
438            let _ = compute_layout(ch, depth + 1, x_cursor, out);
439        }
440
441        let my_x = 0;
442        out.insert(node as *const AudioGraphNode, (my_x, depth));
443        my_x
444    }
445
446    fn spawn_graph_tree(
447        universe: &mut engine::Universe,
448        parent: engine::ecs::ComponentId,
449        node: &AudioGraphNode,
450        bp_component: Option<engine::ecs::ComponentId>,
451        bp_label_out: &mut Option<engine::ecs::ComponentId>,
452        origin: (f32, f32, f32),
453        layout: &std::collections::HashMap<*const AudioGraphNode, (i32, usize)>,
454        dx: f32,
455        dy: f32,
456        cube_scale: f32,
457        sibling_index: usize,
458        sibling_count: usize,
459    ) {
460        let Some((x_unit, depth)) = layout.get(&(node as *const AudioGraphNode)).copied() else {
461            return;
462        };
463
464        let x = origin.0 + (x_unit as f32) * dx;
465        let y = origin.1 - (depth as f32) * dy;
466
467        // Push siblings “behind” each other along Z to reduce overlap between
468        // a sibling's cube and another node's label.
469        let dz_sibling = 0.18;
470        let z = origin.2 - (sibling_index as f32) * dz_sibling;
471
472        // Keep text slightly in front of its cube.
473        let z_text = z + 0.03;
474
475        let rgba = node_rgba(node, depth);
476        let cube_tx = spawn_op_cube(universe, parent, (x, y, z), cube_scale, rgba);
477        let _ = cube_tx;
478
479        // Label next to the cube.
480        let label = node_label(node);
481        let tx = universe.world.add_component(
482            engine::ecs::component::TransformComponent::new()
483                .with_position(x + 0.14, y + 0.02, z_text)
484                .with_scale(0.06, 0.06, 1.0),
485        );
486        let t =
487            universe
488                .world
489                .add_component(engine::ecs::component::TextComponent::with_word_wrap(
490                    &label, 25,
491                ));
492        let _ = universe.attach(parent, tx);
493        let _ = universe.attach(tx, t);
494
495        if bp_component == Some(node.component) {
496            *bp_label_out = Some(t);
497        }
498
499        let filtering = universe
500            .world
501            .add_component(engine::ecs::component::TextureFilteringComponent::nearest());
502        let _ = universe.attach(t, filtering);
503
504        let emissive = universe
505            .world
506            .add_component(engine::ecs::component::EmissiveComponent::on());
507        let _ = universe.attach(t, emissive);
508
509        universe.add(tx);
510
511        // Mix/branch label if branching.
512        if node.children.len() > 1 {
513            let mut weights: Vec<f32> = Vec::with_capacity(node.children.len());
514            for i in 0..node.children.len() {
515                let w = node
516                    .mix
517                    .as_ref()
518                    .map(|m| m.weights.get(i).copied().unwrap_or(1.0))
519                    .unwrap_or(1.0);
520                weights.push(w);
521            }
522            let mix_label = if node.mix.is_some() {
523                format!("Mix weights={weights:?}")
524            } else {
525                format!("Mix <implicit> w={weights:?}")
526            };
527            let mix_tx = universe.world.add_component(
528                engine::ecs::component::TransformComponent::new()
529                    .with_position(x + 0.14, y - 0.11, z_text)
530                    .with_scale(0.05, 0.05, 1.0),
531            );
532            let mix_t = universe.world.add_component(
533                engine::ecs::component::TextComponent::with_word_wrap(&mix_label, 25),
534            );
535            let _ = universe.attach(parent, mix_tx);
536            let _ = universe.attach(mix_tx, mix_t);
537
538            let filtering = universe
539                .world
540                .add_component(engine::ecs::component::TextureFilteringComponent::nearest());
541            let _ = universe.attach(mix_t, filtering);
542
543            let emissive = universe
544                .world
545                .add_component(engine::ecs::component::EmissiveComponent::on());
546            let _ = universe.attach(mix_t, emissive);
547
548            universe.add(mix_tx);
549        }
550
551        let child_count = node.children.len().max(1);
552        for (i, ch) in node.children.iter().enumerate() {
553            // Each node's children form a sibling group; offset them in Z.
554            // For the root node, sibling_index is 0.
555            let _ = sibling_count;
556            spawn_graph_tree(
557                universe,
558                parent,
559                ch,
560                bp_component,
561                bp_label_out,
562                origin,
563                layout,
564                dx,
565                dy,
566                cube_scale,
567                i,
568                child_count,
569            );
570        }
571    }
examples/button-press.rs (line 388)
276fn spawn_button(
277    universe: &mut engine::Universe,
278    pos: [f32; 3],
279    middle_wh: [f32; 2],
280    border: ButtonBorder,
281    text: &str,
282    text_scale: f32,
283    color: [f32; 4],
284    background_color: [f32; 4],
285) -> engine::ecs::ComponentId {
286    use engine::ecs::component::{
287        ColorComponent, TextComponent, TextureFilteringComponent, TransformComponent,
288        TransparentCutoutComponent,
289    };
290
291    let button_root = universe
292        .world
293        .add_component(TransformComponent::new().with_position(pos[0], pos[1], pos[2]));
294
295    // Frame: static border around the button.
296    let frame_root = universe.world.add_component(TransformComponent::new());
297    let frame_color = universe
298        .world
299        .add_component(ColorComponent::rgba(color[0], color[1], color[2], color[3]));
300    let _ = universe.attach(button_root, frame_root);
301    let _ = universe.attach(frame_root, frame_color);
302
303    // Cap: pressable face + text.
304    let cap_raised_z = 0.030;
305    let cap_pressed_z = 0.000;
306    let cap_root = universe
307        .world
308        .add_component(TransformComponent::new().with_position(0.0, 0.0, cap_raised_z));
309    let _ = universe.attach(button_root, cap_root);
310
311    let face_color = universe.world.add_component(ColorComponent::rgba(
312        background_color[0],
313        background_color[1],
314        background_color[2],
315        background_color[3],
316    ));
317    let _ = universe.attach(cap_root, face_color);
318
319    // Geometry.
320    let depth = 0.025;
321    let middle_w = middle_wh[0];
322    let middle_h = middle_wh[1];
323
324    // Border pieces.
325    let full_w = middle_w + border.left + border.right;
326    let full_h = middle_h + border.top + border.bottom;
327
328    // Left / right.
329    let _left = spawn_raycastable_cube(
330        universe,
331        frame_color,
332        [-(middle_w * 0.5 + border.left * 0.5), 0.0, 0.0],
333        [border.left, full_h, depth],
334    );
335    let _right = spawn_raycastable_cube(
336        universe,
337        frame_color,
338        [(middle_w * 0.5 + border.right * 0.5), 0.0, 0.0],
339        [border.right, full_h, depth],
340    );
341
342    // Top / bottom.
343    let _top = spawn_raycastable_cube(
344        universe,
345        frame_color,
346        [0.0, (middle_h * 0.5 + border.top * 0.5), 0.0],
347        [full_w, border.top, depth],
348    );
349    let _bottom = spawn_raycastable_cube(
350        universe,
351        frame_color,
352        [0.0, -(middle_h * 0.5 + border.bottom * 0.5), 0.0],
353        [full_w, border.bottom, depth],
354    );
355
356    // Face (pressable center).
357    let _face = spawn_raycastable_cube(
358        universe,
359        face_color,
360        [0.0, 0.0, 0.0],
361        [middle_w, middle_h, depth],
362    );
363
364    // Text. (Heuristic centering; TextComponent is top-left anchored today.)
365    let text_root = universe.world.add_component(
366        TransformComponent::new()
367            .with_position(0.0, 0.0, depth * 0.5 + 0.006)
368            .with_scale(text_scale, text_scale, 1.0),
369    );
370    let _ = universe.attach(cap_root, text_root);
371
372    let wrap_at = wrap_at_for_button_middle_width(middle_w, text_scale);
373    let (cols, rows) = measure_word_wrapped_block(text, wrap_at);
374    let cols_f = cols.max(1) as f32;
375    let rows_f = rows.max(1) as f32;
376
377    // Center the text block around (0,0) in glyph-local units.
378    // X is left-to-right (0..cols-1), Y is down (0, -1, -2...).
379    let x_center = -0.5 * (cols_f - 1.0);
380    let y_center = 0.5 * (rows_f - 1.0);
381    let text_offset = universe
382        .world
383        .add_component(TransformComponent::new().with_position(x_center, y_center, 0.0));
384    let _ = universe.attach(text_root, text_offset);
385
386    let text_id = universe
387        .world
388        .add_component(TextComponent::with_word_wrap(text, wrap_at));
389    let _ = universe.attach(text_offset, text_id);
390
391    let cutout = universe
392        .world
393        .add_component(TransparentCutoutComponent::new());
394    let _ = universe.attach(text_id, cutout);
395
396    let filtering = universe
397        .world
398        .add_component(TextureFilteringComponent::nearest_magnification());
399    let _ = universe.attach(text_id, filtering);
400
401    universe.add(button_root);
402
403    // Attach text color *after* text has been built/initialized.
404    // We intentionally initialize the ColorComponent before attachment so that its `init()` will
405    // NOT re-run on attach; this exercises the TextSystem ParentChanged refresh behavior.
406    let text_color = universe
407        .world
408        .add_component(ColorComponent::rgba(0.0, 0.0, 0.0, 1.0));
409    universe.add(text_color);
410    let _ = universe.attach(text_id, text_color);
411
412    // Stash state for the signal handler.
413    let state = ButtonState {
414        pressed: false,
415        cap_root,
416        cap_raised_z,
417        cap_pressed_z,
418        frame_color,
419        face_color,
420        text_id,
421        text_color,
422        frame_color_up: color,
423        face_color_up: background_color,
424        text_color_up: [0.0, 0.0, 0.0, 1.0],
425        frame_color_down: darken(color, 0.75),
426        face_color_down: darken(background_color, 0.75),
427        text_color_down: [1.0, 1.0, 1.0, 1.0],
428    };
429
430    BUTTONS
431        .get_or_init(|| Mutex::new(HashMap::new()))
432        .lock()
433        .expect("button state lock")
434        .insert(button_root, state);
435
436    button_root
437}
Source

pub fn with_word_wrap_tokens<T, I>( text: impl Into<String>, wrap_at: usize, tokens: I, ) -> Self
where I: IntoIterator<Item = T>, T: Into<String>,

Word-wrap (prefer wrapping at whitespace / tokens) aiming for wrap_at characters.

tokens are additional “wrap-allowed-after” sequences (e.g. “::”, “.”, “,”). Whitespace tokens (space + tab) are always included.

Examples found in repository?
examples/mesh-factory-example.rs (lines 130-134)
55    fn spawn_labeled_mesh(
56        universe: &mut engine::Universe,
57        x: f32,
58        y: f32,
59        label: &str,
60        mesh: engine::graphics::primitives::CpuMeshHandle,
61        scale: [f32; 3],
62        color: [f32; 4],
63    ) {
64        use engine::ecs::component::{
65            ActionComponent, AnimationComponent, AnimationState, ColorComponent, EmissiveComponent,
66            KeyframeComponent, RenderableComponent, TextComponent, TransformComponent,
67        };
68        use engine::graphics::primitives::{MaterialHandle, Renderable};
69
70        // Mesh.
71        let root = universe.world.add_component(
72            TransformComponent::new()
73                .with_position(x, y, 0.0)
74                .with_scale(scale[0], scale[1], scale[2]),
75        );
76
77        // Spin each shape around its own +Y axis using AnimationComponent + keyframes.
78        // We fill [0, 2) beats densely so it looks smooth.
79        let anim = universe
80            .world
81            .add_component(AnimationComponent::new().with_state(AnimationState::Looping));
82        let _ = universe.attach(root, anim);
83
84        let steps: usize = 64;
85        for i in 0..steps {
86            let beat = (i as f64) * (2.0 / (steps as f64));
87            let kf = universe.world.add_component(KeyframeComponent::new(beat));
88            let _ = universe.attach(anim, kf);
89
90            // Full turn over 2 beats.
91            let angle = (std::f64::consts::TAU * (beat / 2.0)) as f32;
92            let rotation = utils::math::quat_from_axis_angle([0.0, 1.0, 0.0], angle);
93
94            let action_cid = universe.world.add_component(ActionComponent::new(
95                engine::ecs::IntentValue::UpdateTransform {
96                    component_ids: vec![root],
97                    translation: [x, y, 0.0],
98                    rotation_quat_xyzw: rotation,
99                    scale,
100                },
101            ));
102            let _ = universe.attach(kf, action_cid);
103        }
104
105        let renderable = universe
106            .world
107            .add_component(RenderableComponent::new(Renderable::new(
108                mesh,
109                MaterialHandle::TOON_MESH,
110            )));
111        let color_c = universe
112            .world
113            .add_component(ColorComponent::rgba(color[0], color[1], color[2], color[3]));
114        let emissive = universe.world.add_component(EmissiveComponent::on());
115
116        let _ = universe.attach(root, renderable);
117        let _ = universe.attach(renderable, color_c);
118        let _ = universe.attach(renderable, emissive);
119
120        universe.add(root);
121
122        // Label (separate transform so we can scale text independently).
123        let text_root = universe.world.add_component(
124            TransformComponent::new()
125                .with_position(x, y + 0.75, 0.05)
126                .with_scale(0.09, 0.09, 1.0),
127        );
128        let text = universe
129            .world
130            .add_component(TextComponent::with_word_wrap_tokens(
131                label,
132                LABEL_WRAP_AT,
133                ["::", "(", ")", ",", "."],
134            ));
135        let text_color = universe
136            .world
137            .add_component(ColorComponent::rgba(1.0, 1.0, 1.0, 1.0));
138        let text_emissive = universe.world.add_component(EmissiveComponent::on());
139        let _ = universe.attach(text_root, text);
140        let _ = universe.attach(text, text_color);
141        let _ = universe.attach(text, text_emissive);
142        universe.add(text_root);
143    }
Source

pub fn with_font_size(self, font_size: f32) -> Self

Source

pub fn set_font_size(&mut self, font_size: f32)

Source

pub fn set_effective_font_size(&mut self, font_size: f32)

Trait Implementations§

Source§

impl Clone for TextComponent

Source§

fn clone(&self) -> TextComponent

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Component for TextComponent

Source§

fn name(&self) -> &'static str

Short debug/type name for this component kind (e.g. “transform”, “camera”).
Source§

fn set_id(&mut self, component: ComponentId)

Source§

fn as_any(&self) -> &dyn Any

Source§

fn as_any_mut(&mut self) -> &mut dyn Any

Source§

fn init(&mut self, emit: &mut dyn SignalEmitter, component: ComponentId)

Called when component is added to the World
Source§

fn to_mms_ast(&self, _world: &World) -> ComponentExpression

Encode this component as an MMS Component Expression AST node. Read more
Source§

fn cleanup(&mut self, _emit: &mut dyn SignalEmitter, _component: ComponentId)

Called when component is removed from the World.
Source§

fn encode(&self) -> HashMap<String, Value>

Encode component data to a HashMap for serialization. Read more
Source§

fn decode(&mut self, _data: &HashMap<String, Value>) -> Result<(), String>

Decode component data from a HashMap after deserialization. Read more
Source§

impl Debug for TextComponent

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> Downcast for T
where T: Any,

Source§

fn into_any(self: Box<T>) -> Box<dyn Any>

Convert Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.
Source§

fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>

Convert Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be further downcast into Rc<ConcreteType> where ConcreteType implements Trait.
Source§

fn as_any(&self) -> &(dyn Any + 'static)

Convert &Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &Any’s vtable from &Trait’s.
Source§

fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)

Convert &mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &mut Any’s vtable from &mut Trait’s.
Source§

impl<T> DowncastSync for T
where T: Any + Send + Sync,

Source§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Sync + Send>

Convert Arc<Trait> (where Trait: Downcast) to Arc<Any>. Arc<Any> can then be further downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<S, T> Duplex<S> for T
where T: FromSample<S> + ToSample<S>,

Source§

impl<T> DynClone for T
where T: Clone,

Source§

fn __clone_box(&self, _: Private) -> *mut ()

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<S> FromSample<S> for S

Source§

fn from_sample_(s: S) -> S

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<F, T> IntoSample<T> for F
where T: FromSample<F>,

Source§

fn into_sample(self) -> T

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> ToSample<U> for T
where U: FromSample<T>,

Source§

fn to_sample_(self) -> U

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more