use concinnity_cook::authoring::spec::{AssetSpec, asset};
use concinnity_core::components::{
DebugHud, EngineDefaults, Sprite, TextInput, TextLabel, Window, WindowMode,
};
use concinnity_core::ecs::FontHandle;
use concinnity_core::ecs::PickIndex;
use concinnity_core::ecs::TransientSaves;
use concinnity_core::ecs::World;
use super::hud;
use super::hud_ids::HudIds;
use super::panels::registry::{self, PanelKey};
use super::theme;
const REF_W: f32 = 1280.0;
fn hud_font(world: &mut World) -> Option<FontHandle> {
concinnity_core::defaults::hud_font(&mut world.context()).ok()
}
pub(crate) fn editor_hud(world: &mut World) {
let font = hud_font(world);
let code_font = super::code_font::inject(world);
pin_editor_window(world);
world.insert_resource(PickIndex::default());
world.insert_resource(super::thumbs::capture_for_injection());
world.insert_resource(TransientSaves(true));
world.remove_all::<DebugHud>();
match world.query_mut::<EngineDefaults>().next() {
Some(declared) => declared.debug_hud = false,
None => world.add_component(EngineDefaults {
debug_hud: false,
..Default::default()
}),
}
let hidden = [0.0, 0.0, 0.0, 0.0];
world.push_identified(
super::worlds::cinematic::FADE,
button_sprite(hidden, [0.0, 0.0, 0.0, 0.0], false),
);
let loading = super::worlds::loading::ids();
for &id in &loading.sprites {
world.push_identified(id, button_sprite(hidden, [0.0, 0.0, 0.0, 0.0], false));
}
for &id in &loading.labels {
world.push_identified(id, row_label("", hidden, font, false));
}
for (id, s) in super::viewport::billboards::sprites() {
world.push_identified(id, s);
}
for &id in &super::viewport::billboards::ids().labels {
let mut l = centered_label("", [0.0; 4], font);
l.visible = false;
world.push_identified(id, l);
}
for (id, s) in super::viewport::highlight::outline_sprites() {
world.push_identified(id, s);
}
let (id, rect) = super::viewport::marquee::rect_sprite();
world.push_identified(id, rect);
for (id, s) in super::viewport::gizmo::sprites() {
world.push_identified(id, s);
}
let (id, cursor) = super::viewport::cursor::sprite();
world.push_identified(id, cursor);
world.push_identified(
super::viewport::gizmo::MODE_LABEL,
row_label("", [0.0, 0.0, 0.0, 0.0], font, false),
);
for key in PanelKey::ALL {
inject_ids(world, registry::panel(key).ids(), font, code_font);
}
inject_ids(world, super::create_menu::ids(), font, code_font);
inject_ids(world, super::view_menu::ids(), font, code_font);
inject_top_bar(world, font);
inject_ids(world, super::toast_overlay::ids(), font, code_font);
inject_ids(world, super::modal::ids(), font, code_font);
}
fn inject_ids(
world: &mut World,
ids: &HudIds,
font: Option<FontHandle>,
code_font: Option<FontHandle>,
) {
let hidden = [0.0, 0.0, 0.0, 0.0];
for &id in &ids.sprites {
world.push_identified(id, button_sprite(hidden, [0.1, 0.1, 0.12, 1.0], false));
}
for &id in &ids.labels {
world.push_identified(id, row_label("", hidden, font, false));
}
for &id in &ids.code_labels {
world.push_identified(id, code_label(code_font));
}
for &(id, placeholder) in &ids.fields {
world.push_identified(id, text_field(placeholder, font));
}
}
const HIDES_TITLE_BAR: bool = cfg!(target_os = "macos");
fn pin_editor_window(world: &mut World) {
let mut authored = false;
for w in world.query_mut::<Window>() {
w.mode = WindowMode::Windowed;
if HIDES_TITLE_BAR {
w.title_bar = false;
}
authored = true;
}
if !authored && HIDES_TITLE_BAR {
world.add_component(Window {
title_bar: false,
..Default::default()
});
}
}
fn inject_top_bar(world: &mut World, font: Option<FontHandle>) {
let bar = hud::layout(REF_W);
world.push_identified(
hud::BAR_BG,
button_sprite([0.0, 0.0, REF_W, hud::BAR_H], theme::CHROME_TINT, true),
);
for (id, rect) in [
(hud::SAVE_BUTTON, bar.save),
(hud::VIEW_BUTTON, bar.view),
(hud::DISPLAY_BUTTON, bar.display),
(hud::UNDO_BUTTON, bar.undo),
(hud::REDO_BUTTON, bar.redo),
(hud::PLAY_BUTTON, bar.play),
(hud::STEP_BUTTON, bar.step),
(hud::STOP_BUTTON, bar.stop),
] {
world.push_identified(id, button_sprite(rect, theme::BUTTON_TINT, true));
}
world.push_identified(hud::SAVE_LABEL, centered_label("Save", bar.save, font));
world.push_identified(hud::VIEW_LABEL, centered_label("View", bar.view, font));
world.push_identified(
hud::DISPLAY_LABEL,
centered_label("Display", bar.display, font),
);
world.push_identified(hud::UNDO_LABEL, centered_label("Undo", bar.undo, font));
world.push_identified(hud::REDO_LABEL, centered_label("Redo", bar.redo, font));
world.push_identified(hud::PLAY_LABEL, centered_label("Play", bar.play, font));
world.push_identified(hud::STEP_LABEL, centered_label("Step", bar.step, font));
world.push_identified(hud::STOP_LABEL, centered_label("Stop", bar.stop, font));
}
fn materialize<T: serde::de::DeserializeOwned>(spec: AssetSpec) -> T {
serde_json::from_value(crate::authoring::spec_args(&spec))
.expect("editor HUD spec deserializes into its component")
}
fn button_sprite(rect: [f32; 4], tint: [f32; 4], visible: bool) -> Sprite {
materialize(asset::sprite("", rect, tint).set("visible", visible))
}
fn centered_label(content: &str, rect: [f32; 4], font: Option<FontHandle>) -> TextLabel {
let pos = [rect[0] + rect[2] * 0.5, rect[1] + hud::LABEL_TOP];
let mut l: TextLabel = materialize(asset::text_label(
"",
content,
pos,
[1.0, 1.0, 1.0],
"center",
));
l.font = font;
l.scale = theme::TEXT_SCALE;
l
}
fn row_label(content: &str, rect: [f32; 4], font: Option<FontHandle>, visible: bool) -> TextLabel {
let pos = [rect[0] + 12.0, rect[1] + 10.0];
let mut l: TextLabel = materialize(
asset::text_label("", content, pos, [0.9, 0.9, 0.92], "left").set("visible", visible),
);
l.font = font;
l.scale = theme::TEXT_SCALE;
l
}
fn code_label(font: Option<FontHandle>) -> TextLabel {
let mut l = row_label("", [0.0; 4], font, false);
l.scale = super::code_font::SCALE;
l
}
fn text_field(placeholder: &str, font: Option<FontHandle>) -> TextInput {
let mut t: TextInput = materialize(
asset::text_input("", placeholder)
.set("background", [0.14, 0.15, 0.20, 1.0])
.set("max_len", 48u32)
.set("visible", false),
);
t.font = font;
t.scale = theme::TEXT_SCALE;
t
}
#[cfg(test)]
mod tests {
use super::super::panels::{assets_panel, form_panel, preview, template, template_panel, view};
use super::*;
use concinnity_core::components::Identity;
use concinnity_core::ecs::asset_id::AssetId;
use concinnity_core::resource::FontTable;
#[test]
fn injection_overrides_an_authored_title_bar_only_where_the_chrome_survives() {
let mut world = World::new();
world.add_component(Window {
title: "My Game".into(),
width: 1600,
title_bar: true,
..Default::default()
});
editor_hud(&mut world);
let w = world.query::<Window>().next().unwrap();
assert_eq!(w.title_bar, !HIDES_TITLE_BAR);
assert_eq!(w.title, "My Game");
assert_eq!(w.width, 1600);
assert_eq!(world.query::<Window>().count(), 1);
}
#[test]
fn injection_materializes_a_window_to_override_only_where_the_chrome_survives() {
let mut world = World::new();
editor_hud(&mut world);
let windows: Vec<&Window> = world.query::<Window>().collect();
if HIDES_TITLE_BAR {
assert_eq!(windows.len(), 1);
assert!(!windows[0].title_bar);
} else {
assert!(windows.is_empty());
}
}
#[test]
fn injection_pins_a_non_windowed_mode_to_windowed() {
for mode in [WindowMode::Borderless, WindowMode::Fullscreen] {
let mut world = World::new();
world.add_component(Window {
title: "My Game".into(),
width: 1600,
mode,
..Default::default()
});
editor_hud(&mut world);
let w = world.query::<Window>().next().unwrap();
assert_eq!(
w.mode,
WindowMode::Windowed,
"editor pins {mode:?} to windowed"
);
assert_eq!(w.title, "My Game");
assert_eq!(w.width, 1600);
assert_eq!(world.query::<Window>().count(), 1);
}
}
#[test]
fn injects_top_bar_and_hidden_panels() {
let mut world = World::new();
editor_hud(&mut world);
for id in [hud::SAVE_BUTTON, hud::VIEW_BUTTON] {
assert!(
world.get_by_id::<Sprite>(id).unwrap().visible,
"{id:?} visible"
);
}
assert_eq!(
world
.get_by_id::<TextLabel>(hud::VIEW_LABEL)
.unwrap()
.content,
"View"
);
for id in [
assets_panel::PANEL_BG,
assets_panel::PLUS_BG,
assets_panel::PICKER_BG,
assets_panel::MENU_BG,
assets_panel::row_bg(0),
assets_panel::picker_row_bg(0),
preview::PANEL_BG,
preview::ROW_BG,
preview::CHECK_BOX,
view::PANEL_BG,
view::row_bg(0),
view::check_box(0),
template::PANEL_BG,
template::row_bg(0),
template_panel::PANEL_BG,
template_panel::APPLY_BG,
template_panel::row_bg(0),
] {
assert!(
!world.get_by_id::<Sprite>(id).unwrap().visible,
"{id:?} starts hidden"
);
}
let sprites: Vec<AssetId> = world
.join2::<Sprite, Identity>()
.map(|(_, _, identity)| identity.id())
.collect();
let pos = |id: AssetId| sprites.iter().position(|&x| x == id).unwrap();
assert!(pos(assets_panel::PANEL_BG) < pos(hud::SAVE_BUTTON));
assert!(pos(preview::PANEL_BG) < pos(hud::SAVE_BUTTON));
assert!(pos(view::PANEL_BG) < pos(hud::SAVE_BUTTON));
assert!(pos(template::PANEL_BG) < pos(hud::SAVE_BUTTON));
assert!(pos(template_panel::PANEL_BG) < pos(hud::SAVE_BUTTON));
let fields: Vec<AssetId> = world
.join2::<TextInput, Identity>()
.map(|(_, _, identity)| identity.id())
.collect();
assert!(fields.contains(&assets_panel::SEARCH_INPUT));
assert!(fields.contains(&form_panel::NAME_INPUT));
assert!(world.query::<TextInput>().all(|t| !t.visible));
assert!(world.query::<Sprite>().all(|s| s.screen.is_none()));
assert!(world.query::<TextLabel>().all(|l| l.screen.is_none()));
assert!(world.query::<TextInput>().all(|t| t.screen.is_none()));
}
#[test]
fn every_injected_element_is_identified() {
let mut world = World::new();
editor_hud(&mut world);
assert_eq!(
world.join2::<Sprite, Identity>().count(),
world.query::<Sprite>().count()
);
assert_eq!(
world.join2::<TextLabel, Identity>().count(),
world.query::<TextLabel>().count()
);
assert_eq!(
world.join2::<TextInput, Identity>().count(),
world.query::<TextInput>().count()
);
}
#[test]
fn bakes_the_hud_and_code_faces_and_takes_over_the_debug_hud() {
let mut world = World::new();
world.push_identified(
AssetId(0),
TextLabel {
font: Some(FontHandle::new(99)),
..Default::default()
},
);
world.add_component(DebugHud::default());
editor_hud(&mut world);
let fonts = world.resource::<FontTable>().expect("the faces were baked");
assert_eq!(fonts.len(), 2);
let baked = FontHandle::new(0);
let code = super::super::panels::story_panel::ids().code_labels[0];
let code = world.get_by_id::<TextLabel>(code).unwrap();
assert_eq!(code.font, Some(FontHandle::new(1)));
assert_eq!(code.scale, super::super::code_font::SCALE);
let save = world.get_by_id::<TextLabel>(hud::SAVE_LABEL).unwrap();
assert_eq!(save.font, Some(baked));
let field = world
.get_by_id::<TextInput>(form_panel::NAME_INPUT)
.unwrap();
assert_eq!(field.font, Some(baked));
assert_eq!(world.query::<DebugHud>().count(), 0, "DebugHud dropped");
let defaults = world
.query::<EngineDefaults>()
.next()
.expect("the debug HUD default is turned off");
assert!(!defaults.debug_hud);
assert!(defaults.hud, "every other default is left alone");
}
#[test]
fn a_declared_directive_is_amended_rather_than_duplicated() {
let mut world = World::new();
world.add_component(EngineDefaults {
loading_overlay: false,
..Default::default()
});
editor_hud(&mut world);
assert_eq!(world.query::<EngineDefaults>().count(), 1);
let defaults = world.query::<EngineDefaults>().next().unwrap();
assert!(!defaults.debug_hud);
assert!(!defaults.loading_overlay, "the world's own opt-out stands");
}
#[test]
fn constructors_materialize_expected_components() {
use concinnity_core::components::TextAlign;
let s = button_sprite([10.0, 20.0, 100.0, 40.0], [1.0, 0.0, 0.0, 1.0], true);
assert_eq!((s.x, s.y, s.width, s.height), (10.0, 20.0, 100.0, 40.0));
assert_eq!(s.tint, [1.0, 0.0, 0.0, 1.0]);
assert!(s.visible);
let t = text_field("name", Some(FontHandle::new(9)));
assert_eq!(t.placeholder, "name");
assert_eq!(t.background, [0.14, 0.15, 0.20, 1.0]);
assert_eq!(t.max_len, 48);
assert!(!t.visible);
assert_eq!(t.font, Some(FontHandle::new(9)));
let c = centered_label("SAVE", [0.0, 0.0, 88.0, 88.0], None);
assert_eq!(c.content, "SAVE");
assert_eq!(c.align, TextAlign::Center);
assert_eq!(c.x, 44.0, "centered on the button width");
assert_eq!(c.color, [1.0, 1.0, 1.0]);
assert!(c.visible);
let r = row_label("row", [0.0, 0.0, 100.0, 40.0], None, false);
assert_eq!(r.align, TextAlign::Left);
assert_eq!(r.color, [0.9, 0.9, 0.92]);
assert!(!r.visible);
}
}