concinnity-dev 0.19.119

The Concinnity dev tooling library: world authoring, the in-engine editor, the debug server, docs and packaging
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
//! Contract tests over the panel registry. Each floating panel supplies only its
//! own `Panel` impl (`hook/panels.rs`) and the shared machinery drives the rest,
//! so the invariants that machinery relies on -- a footprint inside its own max,
//! an on-screen origin, a toggle that round-trips, a `hide` that blanks exactly
//! what the panel declared for injection, presses and wheels that fall through
//! outside the footprint, and scrolling that stays in bounds at either end --
//! are asserted here once for every registered panel rather than per panel.
//!
//! Beside them are the drives of the two panels that are only lists of the
//! others: the View panel's rows toggling what shows, and the Templates panel's
//! pick opening a detail panel whose apply adds the template's entries.

use concinnity_core::components::{FrameInput, ShaderStage, Sprite, TextInput, TextLabel};
use concinnity_core::ecs::World;
use concinnity_core::ecs::asset_id::AssetId;

use super::fixtures::{hook, set_input, world_with_input};
use crate::editor::hook::EditorHook;
use crate::editor::hook::edit::shaders_state::SourceState;
use crate::editor::hud::{self, HudAction};
use crate::editor::panels::registry::{self, Panel, PanelKey};
use crate::editor::panels::shader_source::SourceKey;
use crate::editor::panels::template_panel::{self, TemplateAction};
use crate::editor::panels::{list_panel, template, view};
use crate::editor::{inject, widget};
use crate::frame_hook::FrameHook;
use crate::test_support::isolate_state_dir;

fn empty_hook() -> EditorHook {
    EditorHook::new("unused.jsonl".to_string(), Vec::new())
}

// A world carrying every panel's injected elements, which is what the panels'
// `draw` / `hide` mutate.
fn injected_world() -> World {
    let mut world = World::new();
    inject::editor_hud(&mut world);
    world
}

// Open every registered panel, including the ones that have no View row: the
// edit form (gated on a picked type plus the Assets panel), the View panel
// itself, the template detail (gated on an open template), and the command
// palette (opened by its shortcut).
fn open_every_panel(h: &mut EditorHook, world: &mut World) {
    for p in registry::view_toggles() {
        if !p.is_open(h) {
            p.toggle(h, world);
        }
    }
    h.form.selected_type = Some("Sprite".to_string());
    h.open[PanelKey::View] = true;
    h.templates.detail = Some(0);
    h.open[PanelKey::Palette] = true;
    h.shaders.source = Some(SourceState::new(
        SourceKey::shader("lit", ShaderStage::Fragment),
        "/cn-none/lit.hlsl".to_string(),
        "float4 shade(VertexOut v, GpuObjectData od) { return 1.0; }".to_string(),
    ));
}

// Every declared element of `p`, forced visible so a `hide` that misses one is
// visible as a leftover rather than passing on an already-blank world.
fn show_every_element(p: &dyn Panel, world: &mut World) {
    let ids = p.ids();
    for &id in &ids.sprites {
        widget::set_sprite_visible(world, id, true);
    }
    for &id in ids.labels.iter().chain(&ids.code_labels) {
        widget::set_label_visible(world, id, true);
    }
    for id in ids.field_ids() {
        widget::show_field(world, id, [0.0, 0.0, 10.0, 10.0], false);
    }
}

fn visible_sprites(p: &dyn Panel, world: &World) -> Vec<AssetId> {
    let ids = &p.ids().sprites;
    world
        .join2::<Sprite, concinnity_core::components::Identity>()
        .filter(|(_, s, identity)| s.visible && ids.contains(&identity.id()))
        .map(|(_, _, identity)| identity.id())
        .collect()
}

// A panel blanks every element it declared for injection. The declared lists are
// what `inject::editor_hud` materializes, so an id a panel lists but never blanks
// would survive the F1 hidden pass (and a toggle-off) as a stranded element.
#[test]
fn hide_blanks_every_declared_element() {
    let mut world = injected_world();
    for key in PanelKey::ALL {
        let p = registry::panel(key);
        show_every_element(p, &mut world);
        p.hide(&mut world);

        let ids = p.ids();
        for &id in &ids.sprites {
            let s = world
                .get_by_id::<Sprite>(id)
                .unwrap_or_else(|| panic!("{key:?} declares sprite {id:?} but injection has none"));
            assert!(!s.visible, "{key:?} left sprite {id:?} visible after hide");
        }
        for &id in ids.labels.iter().chain(&ids.code_labels) {
            let l = world
                .get_by_id::<TextLabel>(id)
                .unwrap_or_else(|| panic!("{key:?} declares label {id:?} but injection has none"));
            assert!(!l.visible, "{key:?} left label {id:?} visible after hide");
        }
        for id in ids.field_ids() {
            let t = world
                .get_by_id::<TextInput>(id)
                .unwrap_or_else(|| panic!("{key:?} declares field {id:?} but injection has none"));
            assert!(!t.visible, "{key:?} left field {id:?} visible after hide");
        }
    }
}

// A panel's default footprint is also its minimum, so it can never start out
// larger than the maximum the resize drag will clamp it to.
#[test]
fn default_size_fits_within_max_size() {
    let h = empty_hook();
    for key in PanelKey::ALL {
        let p = registry::panel(key);
        let s = p.size(&h);
        let max = p.max_size(&h);
        assert!(
            s[0] > 0.0 && s[1] > 0.0,
            "{key:?} has a degenerate default size {s:?}"
        );
        assert!(
            s[0] <= max[0] && s[1] <= max[1],
            "{key:?} default size {s:?} exceeds its max {max:?}"
        );
    }
}

// The stored resize override is clamped into `[default, max]` on read, so a
// stale override from a session where the panel held more content can neither
// shrink it below its content nor grow it past its element pool.
#[test]
fn effective_size_clamps_a_stored_override() {
    for key in PanelKey::ALL {
        let mut h = empty_hook();
        let d = h.default_size(key);
        let max = registry::panel(key).max_size(&h);

        h.sizes[key] = Some([1.0, 1.0]);
        assert_eq!(
            h.effective_size(key),
            d,
            "{key:?} let an override shrink it below its default"
        );

        h.sizes[key] = Some([1.0e6, 1.0e6]);
        let grown = h.effective_size(key);
        assert!(
            grown[0] >= d[0] && grown[1] >= d[1],
            "{key:?} grew below its default"
        );
        for axis in 0..2 {
            if max[axis].is_finite() {
                assert_eq!(
                    grown[axis],
                    max[axis].max(d[axis]),
                    "{key:?} grew past its max on axis {axis}"
                );
            }
        }
    }
}

// Every panel's resting place is fully on screen and clear of the top bar, at
// any window shape -- the clamp is what keeps a panel reachable after a resize.
#[test]
fn every_panel_origin_stays_on_screen() {
    let h = empty_hook();
    for vp in [[1280.0, 720.0], [800.0, 600.0], [3840.0, 2160.0]] {
        for key in PanelKey::ALL {
            let o = h.origin(key, vp);
            let s = h.effective_size(key);
            assert!(o[0] >= 0.0, "{key:?} sits off the left edge at {vp:?}");
            assert!(
                o[1] >= hud::BAR_H,
                "{key:?} sits under the top bar at {vp:?}"
            );
            if s[0] <= vp[0] {
                assert!(
                    o[0] + s[0] <= vp[0],
                    "{key:?} overhangs the right edge at {vp:?}"
                );
            }
            if s[1] <= vp[1] - hud::BAR_H {
                assert!(
                    o[1] + s[1] <= vp[1],
                    "{key:?} overhangs the bottom edge at {vp:?}"
                );
            }
        }
    }
}

// Every View-panel row round-trips its panel in both directions, from whichever
// state that panel defaults to (the Preview panel starts open, the rest closed).
#[test]
fn every_view_toggle_opens_and_closes_its_panel() {
    let _guard = crate::test_support::lock();
    isolate_state_dir();
    let mut world = injected_world();
    let mut h = empty_hook();
    for p in registry::view_toggles() {
        let key = p.key();
        p.close(&mut h, &mut world);
        assert!(!p.is_open(&h), "{key:?} did not close on its title-bar X");

        p.toggle(&mut h, &mut world);
        assert!(p.is_open(&h), "{key:?} did not open on its View row");

        p.toggle(&mut h, &mut world);
        assert!(!p.is_open(&h), "{key:?} did not close on its View row");
    }
}

// The title-bar X shuts every panel, including the ones with no View row. Each
// is re-opened first because the compound gates chain: closing the Assets panel
// also takes the edit form with it, and closing Templates the detail panel.
#[test]
fn close_shuts_every_open_panel() {
    let _guard = crate::test_support::lock();
    isolate_state_dir();
    let mut world = injected_world();
    let mut h = empty_hook();
    for key in PanelKey::ALL {
        open_every_panel(&mut h, &mut world);
        let p = registry::panel(key);
        assert!(p.is_open(&h), "{key:?} did not open for the close sweep");
        p.close(&mut h, &mut world);
        assert!(!p.is_open(&h), "{key:?} stayed open after close");
    }
}

// A press or a wheel clear of a panel's footprint is not the panel's: it falls
// through so the panel behind it (or the 3D viewport) gets the event.
#[test]
fn a_press_clear_of_the_footprint_falls_through() {
    let _guard = crate::test_support::lock();
    isolate_state_dir();
    let mut world = injected_world();
    let mut h = empty_hook();
    open_every_panel(&mut h, &mut world);
    let vp = [1280.0, 720.0];
    for key in PanelKey::ALL {
        let p = registry::panel(key);
        let o = h.origin(key, vp);
        let s = h.effective_size(key);
        let (mx, my) = (o[0] + s[0] + 40.0, o[1] + s[1] + 40.0);
        assert!(
            !p.press(&mut h, &mut world, mx, my, o),
            "{key:?} claimed a press outside its footprint"
        );
        assert!(
            !p.wheel_over(&h, &world, mx, my, o),
            "{key:?} claimed a wheel outside its footprint"
        );
    }
}

// Scrolling past either end of a panel's list is a no-op rather than an
// underflow: the offsets are unsigned, so an unclamped step would panic here.
#[test]
fn scrolling_past_either_end_stays_in_bounds() {
    let _guard = crate::test_support::lock();
    isolate_state_dir();
    let mut world = injected_world();
    let mut h = empty_hook();
    open_every_panel(&mut h, &mut world);
    for key in PanelKey::ALL {
        let p = registry::panel(key);
        for _ in 0..32 {
            p.scroll(&mut h, &mut world, 1.0);
        }
        for _ in 0..64 {
            p.scroll(&mut h, &mut world, -1.0);
        }
        for _ in 0..64 {
            p.scroll(&mut h, &mut world, 1.0);
        }
    }
}

// A panel shows chrome when drawn and blanks it again when hidden, so the
// per-frame draw and the F1 hidden pass agree on the same element set.
#[test]
fn draw_shows_chrome_that_hide_takes_back() {
    let _guard = crate::test_support::lock();
    isolate_state_dir();
    let mut world = injected_world();
    let mut h = empty_hook();
    open_every_panel(&mut h, &mut world);
    let vp = [1280.0, 720.0];
    for key in PanelKey::ALL {
        let p = registry::panel(key);
        let o = h.origin(key, vp);
        p.hide(&mut world);
        p.draw(&h, &mut world, o, [o[0] + 4.0, o[1] + 4.0]);
        assert!(
            !visible_sprites(p, &world).is_empty(),
            "{key:?} drew no chrome while open"
        );
        p.hide(&mut world);
        assert!(
            visible_sprites(p, &world).is_empty(),
            "{key:?} left chrome behind after hide"
        );
    }
}

// A panel's floating overlay ids are drawn above its body, so they must come
// from the same declared element set injection materialized.
#[test]
fn overlay_ids_are_declared_elements() {
    let mut h = empty_hook();
    h.form.field_dropdown = None;
    for key in PanelKey::ALL {
        let p = registry::panel(key);
        for id in p.overlay_ids(&h) {
            assert!(
                p.ids().all().any(|declared| declared == id),
                "{key:?} overlay id {id:?} is not among its injected elements"
            );
        }
    }
}

// The top-bar View button toggles the View panel; the View panel's rows toggle
// the Assets, Preview, and Templates panels independently (no mutual exclusion).
#[test]
fn view_button_and_view_rows_toggle_the_panels() {
    let mut h = hook(Vec::new());
    let mut world = World::new();
    h.apply_top(HudAction::ToggleView, &mut world);
    assert!(
        h.open[PanelKey::View],
        "the View button shows the View panel"
    );
    h.apply_top(HudAction::ToggleView, &mut world);
    assert!(!h.open[PanelKey::View], "a second click hides it");
    // Row 0 -> Assets, row 1 -> Preview, row 2 -> Templates.
    h.toggle_view_row(0, &mut world);
    assert!(h.open[PanelKey::Assets], "row 0 shows the Assets panel");
    h.toggle_view_row(1, &mut world);
    assert!(
        !h.open[PanelKey::Preview],
        "row 1 hides the (default-shown) Preview panel"
    );
    h.toggle_view_row(2, &mut world);
    assert!(
        h.open[PanelKey::Templates],
        "row 2 shows the Templates panel"
    );
    assert!(
        h.open[PanelKey::Assets],
        "Assets stayed shown -- panels are independent"
    );
}

// Picking a template opens its detail panel (nothing is added yet); Apply from
// the detail layers the assets and closes it; re-applying is idempotent.
#[test]
fn template_pick_opens_detail_then_apply_adds_idempotently() {
    let mut h = hook(Vec::new());
    h.open_template_detail(0);
    assert_eq!(
        h.templates.detail,
        Some(0),
        "the detail panel opens on pick"
    );
    assert!(h.entries.is_empty(), "picking adds nothing on its own");

    h.apply_template_detail(TemplateAction::Apply);
    let first = concinnity_cook::authoring::template::TEMPLATES[0]
        .assets()
        .len();
    assert_eq!(h.entries.len(), first, "Apply adds all template entries");
    assert_eq!(h.templates.detail, None, "Apply closes the detail panel");

    // Re-open and Apply again: no duplicate entries.
    h.open_template_detail(0);
    h.apply_template_detail(TemplateAction::Apply);
    assert_eq!(h.entries.len(), first, "re-apply is idempotent");
}

// The detail panel's grouped rows come from the shared list model, so they
// match what the template would add (one row per asset plus a type header
// each), and the "X" closes the panel without adding anything.
#[test]
fn template_detail_rows_and_close() {
    let mut h = hook(Vec::new());
    h.open_template_detail(0);
    let rows = h.template_rows(0);
    let names = rows.iter().filter(|r| !r.is_header).count();
    assert_eq!(
        names,
        concinnity_cook::authoring::template::TEMPLATES[0]
            .assets()
            .len(),
        "one name row per template asset"
    );
    assert!(
        rows.iter().any(|r| r.is_header),
        "grouped under type headers"
    );
    h.apply_template_detail(TemplateAction::Close);
    assert_eq!(h.templates.detail, None);
    assert!(h.entries.is_empty(), "closing adds nothing");
}

// Clicking the Preview panel's capture row hands the cursor to the world
// (clicking again takes it back); the fly row below toggles the fly camera,
// and the two never hold the cursor together.
#[test]
fn preview_rows_toggle_play_mode_and_fly() {
    let mut h = hook(Vec::new());
    let vp = [1280.0, 720.0];
    let o = h.origin(PanelKey::Preview, vp);
    let row_mid = |i: usize| {
        let r = list_panel::row_rect(o, 200.0, i);
        [r[0] + 10.0, r[1] + r[3] * 0.5]
    };
    let click = |h: &mut EditorHook, world: &mut World, pos: [f32; 2]| {
        set_input(
            world,
            FrameInput {
                viewport: vp,
                mouse_x: pos[0],
                mouse_y: pos[1],
                left_click: true,
                left_button_down: true,
                ..Default::default()
            },
        );
        h.tick(world);
    };
    let mut world = world_with_input(FrameInput::default());

    click(&mut h, &mut world, row_mid(0));
    assert!(h.sim.playing(), "the checkbox click enters play mode");
    click(&mut h, &mut world, row_mid(0));
    assert!(!h.sim.playing(), "a second click leaves it");

    click(&mut h, &mut world, row_mid(1));
    assert!(h.fly, "the fly row starts the fly camera");
    assert!(!h.sim.playing());
    click(&mut h, &mut world, row_mid(0));
    assert!(
        h.sim.playing() && !h.fly,
        "entering play mode ends the fly camera"
    );
}

// End-to-end through `tick` against a fully injected HUD: the top-bar View
// button opens the View panel, and clicking its "Templates" row opens the
// Templates panel (the same click path a real session drives).
#[test]
fn tick_view_button_opens_view_then_a_row_opens_templates() {
    let vis = |w: &World, id: AssetId| {
        w.get_by_id::<Sprite>(id)
            .map(|s| s.visible)
            .unwrap_or(false)
    };
    let rect = |w: &World, id: AssetId| {
        let s = w.get_by_id::<Sprite>(id).unwrap();
        [s.x, s.y, s.width, s.height]
    };
    let mut world = World::new();
    inject::editor_hud(&mut world);
    let vp = [1280.0, 720.0];
    let mut h = hook(Vec::new());

    // Frame 1: no interaction. View + Templates start hidden.
    world.add_component(FrameInput {
        viewport: vp,
        ..Default::default()
    });
    h.tick(&mut world);
    assert!(!vis(&world, view::PANEL_BG) && !vis(&world, template::PANEL_BG));

    // Frame 2: click the top-bar View button -> the View panel opens.
    let view_btn = hud::layout(vp[0]).view;
    set_input(
        &mut world,
        FrameInput {
            viewport: vp,
            mouse_x: view_btn[0] + view_btn[2] * 0.5,
            mouse_y: view_btn[1] + view_btn[3] * 0.5,
            left_click: true,
            left_button_down: true,
            ..Default::default()
        },
    );
    h.tick(&mut world);
    assert!(
        h.open[PanelKey::View] && vis(&world, view::PANEL_BG),
        "View panel opened"
    );
    // Its "Templates" row (index 2) is laid out; grab its rect to click it.
    let row = rect(&world, view::row_bg(2));

    // Frame 3: click that row -> the Templates panel opens.
    set_input(
        &mut world,
        FrameInput {
            viewport: vp,
            mouse_x: row[0] + row[2] * 0.5,
            mouse_y: row[1] + row[3] * 0.5,
            left_click: true,
            left_button_down: true,
            ..Default::default()
        },
    );
    h.tick(&mut world);
    assert!(
        h.open[PanelKey::Templates],
        "the Templates row toggled the panel on"
    );
    assert!(vis(&world, template::PANEL_BG), "Templates panel shown");
}

// Picking a template row spawns the detail panel (title "Template <name>",
// hidden until then); its Apply button layers the template's assets and closes
// the detail. Drives the whole flow through `tick` end to end.
#[test]
fn tick_picking_a_template_spawns_the_detail_panel_then_apply_adds() {
    let vis = |w: &World, id: AssetId| {
        w.get_by_id::<Sprite>(id)
            .map(|s| s.visible)
            .unwrap_or(false)
    };
    let rect = |w: &World, id: AssetId| {
        let s = w.get_by_id::<Sprite>(id).unwrap();
        [s.x, s.y, s.width, s.height]
    };
    let mut world = World::new();
    inject::editor_hud(&mut world);
    let vp = [1280.0, 720.0];
    let mut h = hook(Vec::new());
    // Start with the Templates list already open.
    h.open[PanelKey::Templates] = true;
    world.add_component(FrameInput {
        viewport: vp,
        ..Default::default()
    });
    h.tick(&mut world);
    assert!(
        vis(&world, template::PANEL_BG) && !vis(&world, template_panel::PANEL_BG),
        "Templates list shown; detail panel still hidden"
    );

    // Click the first template row -> the detail panel spawns.
    let row = rect(&world, template::row_bg(0));
    set_input(
        &mut world,
        FrameInput {
            viewport: vp,
            mouse_x: row[0] + row[2] * 0.5,
            mouse_y: row[1] + row[3] * 0.5,
            left_click: true,
            left_button_down: true,
            ..Default::default()
        },
    );
    h.tick(&mut world);
    assert_eq!(
        h.templates.detail,
        Some(0),
        "the detail panel opened on pick"
    );
    assert!(vis(&world, template_panel::PANEL_BG), "detail panel shown");
    let title = world
        .get_by_id::<TextLabel>(template_panel::TITLE_LABEL)
        .unwrap();
    assert!(
        title.content.starts_with("Template "),
        "title bar reads 'Template <name>': {}",
        title.content
    );
    assert!(h.entries.is_empty(), "picking adds nothing yet");

    // Click the detail's Apply button -> the template's assets are added and
    // the detail closes.
    let apply = template_panel::apply_rect(
        h.origin(PanelKey::TemplateDetail, vp),
        h.effective_size(PanelKey::TemplateDetail)[0],
    );
    set_input(
        &mut world,
        FrameInput {
            viewport: vp,
            mouse_x: apply[0] + apply[2] * 0.5,
            mouse_y: apply[1] + apply[3] * 0.5,
            left_click: true,
            left_button_down: true,
            ..Default::default()
        },
    );
    h.tick(&mut world);
    assert_eq!(h.templates.detail, None, "Apply closed the detail panel");
    assert!(
        !vis(&world, template_panel::PANEL_BG),
        "detail panel hidden"
    );
    assert_eq!(
        h.entries.len(),
        concinnity_cook::authoring::template::TEMPLATES[0]
            .assets()
            .len(),
        "Apply layered the template's assets"
    );
}