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())
}
fn injected_world() -> World {
let mut world = World::new();
inject::editor_hud(&mut world);
world
}
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(),
));
}
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()
}
#[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");
}
}
}
#[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:?}"
);
}
}
#[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}"
);
}
}
}
}
#[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:?}"
);
}
}
}
}
#[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");
}
}
#[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");
}
}
#[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"
);
}
}
#[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);
}
}
}
#[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"
);
}
}
#[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"
);
}
}
}
#[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");
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"
);
}
#[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");
h.open_template_detail(0);
h.apply_template_detail(TemplateAction::Apply);
assert_eq!(h.entries.len(), first, "re-apply is idempotent");
}
#[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");
}
#[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"
);
}
#[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());
world.add_component(FrameInput {
viewport: vp,
..Default::default()
});
h.tick(&mut world);
assert!(!vis(&world, view::PANEL_BG) && !vis(&world, template::PANEL_BG));
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"
);
let row = rect(&world, view::row_bg(2));
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");
}
#[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());
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"
);
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");
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"
);
}