use super::hud;
use super::registry::{self, PanelKey};
use super::theme;
use crate::components::{
DebugHud, EngineDefaults, Sprite, TextInput, TextLabel, Window, WindowMode,
};
use crate::ecs::FontHandle;
use crate::ecs::World;
use crate::ecs::asset_id::AssetId;
use concinnity_cook::authoring::spec::{AssetSpec, asset};
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);
pin_editor_window(world);
world.insert_resource(crate::ecs::PickIndex::default());
world.insert_resource(super::thumbs::capture_for_injection());
world.insert_resource(crate::ecs::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.add_component(button_sprite(
super::worlds::cinematic::FADE,
hidden,
[0.0, 0.0, 0.0, 0.0],
false,
));
for id in super::worlds::loading::all_sprite_ids() {
world.add_component(button_sprite(id, hidden, [0.0, 0.0, 0.0, 0.0], false));
}
for id in super::worlds::loading::all_label_ids() {
world.add_component(row_label(id, "", hidden, font, false));
}
for s in super::billboards::sprites() {
world.add_component(s);
}
for id in super::billboards::all_label_ids() {
let mut l = centered_label(id, "", [0.0; 4], font);
l.visible = false;
world.add_component(l);
}
for s in super::highlight::outline_sprites() {
world.add_component(s);
}
world.add_component(super::marquee::rect_sprite());
for s in super::gizmo::sprites() {
world.add_component(s);
}
world.add_component(super::cursor::sprite());
world.add_component(row_label(
super::gizmo::MODE_LABEL,
"",
[0.0, 0.0, 0.0, 0.0],
font,
false,
));
for key in PanelKey::ALL {
let p = registry::panel(key);
for id in p.sprite_ids() {
world.add_component(button_sprite(id, hidden, [0.1, 0.1, 0.12, 1.0], false));
}
for id in p.label_ids() {
world.add_component(row_label(id, "", hidden, font, false));
}
for (id, placeholder) in p.field_ids() {
world.add_component(text_field(id, placeholder, font));
}
}
for id in super::create_menu::all_sprite_ids() {
world.add_component(button_sprite(id, hidden, [0.1, 0.1, 0.12, 1.0], false));
}
for id in super::create_menu::all_label_ids() {
world.add_component(row_label(id, "", hidden, font, false));
}
for id in super::view_menu::all_sprite_ids() {
world.add_component(button_sprite(id, hidden, [0.1, 0.1, 0.12, 1.0], false));
}
for id in super::view_menu::all_label_ids() {
world.add_component(row_label(id, "", hidden, font, false));
}
inject_top_bar(world, font);
for id in super::toast_overlay::all_sprite_ids() {
world.add_component(button_sprite(id, hidden, [0.1, 0.1, 0.12, 1.0], false));
}
for id in super::toast_overlay::all_label_ids() {
world.add_component(row_label(id, "", hidden, font, false));
}
for id in super::modal::all_sprite_ids() {
world.add_component(button_sprite(id, hidden, [0.1, 0.1, 0.12, 1.0], false));
}
for id in super::modal::all_label_ids() {
world.add_component(row_label(id, "", hidden, font, false));
}
for id in super::modal::all_field_ids() {
world.add_component(text_field(id, "world name", 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.add_component(button_sprite(
hud::BAR_BG,
[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.add_component(button_sprite(id, rect, theme::BUTTON_TINT, true));
}
world.add_component(centered_label(hud::SAVE_LABEL, "Save", bar.save, font));
world.add_component(centered_label(hud::VIEW_LABEL, "View", bar.view, font));
world.add_component(centered_label(
hud::DISPLAY_LABEL,
"Display",
bar.display,
font,
));
world.add_component(centered_label(hud::UNDO_LABEL, "Undo", bar.undo, font));
world.add_component(centered_label(hud::REDO_LABEL, "Redo", bar.redo, font));
world.add_component(centered_label(hud::PLAY_LABEL, "Play", bar.play, font));
world.add_component(centered_label(hud::STEP_LABEL, "Step", bar.step, font));
world.add_component(centered_label(hud::STOP_LABEL, "Stop", bar.stop, font));
}
fn materialize<T: serde::de::DeserializeOwned>(spec: AssetSpec) -> T {
serde_json::from_value(crate::spec_args(&spec))
.expect("editor HUD spec deserializes into its component")
}
fn button_sprite(id: AssetId, rect: [f32; 4], tint: [f32; 4], visible: bool) -> Sprite {
let mut s: Sprite = materialize(asset::sprite("", rect, tint).set("visible", visible));
s.asset_id = id;
s
}
fn centered_label(
id: AssetId,
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.asset_id = id;
l.font = font;
l.scale = theme::TEXT_SCALE;
l
}
fn row_label(
id: AssetId,
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.asset_id = id;
l.font = font;
l.scale = theme::TEXT_SCALE;
l
}
fn text_field(id: AssetId, 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.asset_id = id;
t.font = font;
t.scale = theme::TEXT_SCALE;
t
}
#[cfg(test)]
mod tests {
use super::super::{form_panel, panel, preview, template_panel, templates, view};
use super::*;
#[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
.query::<Sprite>()
.find(|s| s.asset_id == id)
.unwrap()
.visible,
"{id:?} visible"
);
}
assert_eq!(
world
.query::<TextLabel>()
.find(|l| l.asset_id == hud::VIEW_LABEL)
.unwrap()
.content,
"View"
);
for id in [
panel::PANEL_BG,
panel::PLUS_BG,
panel::PICKER_BG,
panel::MENU_BG,
panel::row_bg(0),
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),
templates::PANEL_BG,
templates::row_bg(0),
template_panel::PANEL_BG,
template_panel::APPLY_BG,
template_panel::row_bg(0),
] {
assert!(
!world
.query::<Sprite>()
.find(|s| s.asset_id == id)
.unwrap()
.visible,
"{id:?} starts hidden"
);
}
let sprites: Vec<AssetId> = world.query::<Sprite>().map(|s| s.asset_id).collect();
let pos = |id: AssetId| sprites.iter().position(|&x| x == id).unwrap();
assert!(pos(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(templates::PANEL_BG) < pos(hud::SAVE_BUTTON));
assert!(pos(template_panel::PANEL_BG) < pos(hud::SAVE_BUTTON));
let fields: Vec<AssetId> = world.query::<TextInput>().map(|t| t.asset_id).collect();
assert!(fields.contains(&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 bakes_the_engine_hud_face_and_takes_over_the_debug_hud() {
let mut world = World::new();
world.add_component(TextLabel {
asset_id: AssetId(0),
font: Some(FontHandle(99)),
..Default::default()
});
world.add_component(DebugHud::default());
editor_hud(&mut world);
let fonts = world
.resource::<crate::resource::FontTable>()
.expect("the face was baked");
assert_eq!(fonts.len(), 1);
let baked = FontHandle(0);
let save = world
.query::<TextLabel>()
.find(|l| l.asset_id == hud::SAVE_LABEL)
.unwrap();
assert_eq!(save.font, Some(baked));
let field = world
.query::<TextInput>()
.find(|t| t.asset_id == 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 {
sky: 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.sky, "the world's own opt-out stands");
}
#[test]
fn constructors_materialize_expected_components() {
use crate::components::TextAlign;
let s = button_sprite(
AssetId(1),
[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);
assert_eq!(s.asset_id, AssetId(1));
let t = text_field(AssetId(2), "name", Some(FontHandle(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(9)));
let c = centered_label(AssetId(3), "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(AssetId(4), "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);
}
}