Skip to main content

folder_text/
folder-text.rs

1use mittens_engine::{engine, utils};
2
3#[path = "example_util/mod.rs"]
4mod example_util;
5
6use std::collections::BTreeMap;
7use std::path::{Path, PathBuf};
8
9fn measure_text_bounds(text: &str, wrap_at: usize) -> (usize, usize) {
10    const TAB_WIDTH: usize = 4;
11
12    let mut col: usize = 0;
13    let mut line_count: usize = 0;
14    let mut row: usize = 0;
15    let mut max_col: usize = 0;
16
17    for ch in text.chars() {
18        if ch == '\n' {
19            max_col = max_col.max(col);
20            row += 1;
21            col = 0;
22            line_count = 0;
23            continue;
24        }
25
26        if wrap_at != 0 && line_count >= wrap_at {
27            max_col = max_col.max(col);
28            row += 1;
29            col = 0;
30            line_count = 0;
31        }
32
33        let adv = if ch == '\t' { TAB_WIDTH } else { 1 };
34        col += adv;
35        line_count += adv;
36        max_col = max_col.max(col);
37    }
38
39    // Keep a minimum 1x1 so empty text still gets a panel.
40    (max_col.max(1), (row + 1).max(1))
41}
42
43fn collect_rs_files(dir: &Path, out: &mut Vec<PathBuf>) {
44    let Ok(read_dir) = std::fs::read_dir(dir) else {
45        return;
46    };
47
48    for entry in read_dir.flatten() {
49        let path = entry.path();
50        if path.is_dir() {
51            collect_rs_files(&path, out);
52            continue;
53        }
54        if path.extension().and_then(|s| s.to_str()) == Some("rs") {
55            out.push(path);
56        }
57    }
58}
59
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}