mod rows;
mod screen;
mod settings_tab;
#[cfg(test)]
mod tests;
use std::collections::HashSet;
use super::expand::{asset_name, type_norm};
use super::ui_spec::font_sizes;
use crate::authoring::registry::build_only::MainMenu;
use crate::authoring::spec::{asset, spec_to_value};
use concinnity_core::gfx::overlay::UI_REFERENCE_SIZE;
use rows::settings_tabs;
use screen::{MenuMetrics, emit_menu_screen};
use settings_tab::emit_settings_tab;
const TOP_MARGIN_FRAC: f32 = 0.07;
fn opaque(rgb: [f32; 3]) -> [f32; 4] {
[rgb[0], rgb[1], rgb[2], 1.0]
}
fn cursor_sprite(name: &str, style: &MainMenu) -> serde_json::Value {
spec_to_value(
&asset::sprite(
name,
[0.0, 0.0, style.cursor_size, style.cursor_size],
style.cursor_color,
)
.set("follow_cursor", true),
)
}
pub(crate) fn expand_main_menus(assets: &mut Vec<serde_json::Value>) -> Result<(), String> {
if !assets.iter().any(|v| type_norm(v) == "mainmenu") {
return Ok(());
}
let (win_w, win_h) = (UI_REFERENCE_SIZE[0], UI_REFERENCE_SIZE[1]);
let font_px_by_name = font_sizes(assets);
let mut taken: HashSet<String> = assets
.iter()
.filter(|v| type_norm(v) != "mainmenu")
.map(asset_name)
.filter(|n| !n.is_empty())
.collect();
let mut result: Vec<serde_json::Value> = Vec::new();
for value in assets.drain(..) {
if type_norm(&value) != "mainmenu" {
result.push(value);
continue;
}
let menu_name = asset_name(&value);
if menu_name.is_empty() {
return Err("MainMenu: missing `name`".to_string());
}
let args = value
.get("args")
.cloned()
.unwrap_or_else(|| serde_json::json!({}));
let menu: MainMenu = serde_json::from_value(args)
.map_err(|e| format!("MainMenu '{}': invalid args: {}", menu_name, e))?;
let font_px = if menu.font.is_empty() {
menu.font_px
} else {
*font_px_by_name.get(&menu.font).unwrap_or(&menu.font_px)
};
for entry in expand_one(&menu_name, &menu, win_w, win_h, font_px) {
let name = asset_name(&entry);
if !name.is_empty() && !taken.insert(name.clone()) {
return Err(format!(
"MainMenu '{}': generated asset name '{}' collides with an existing \
asset; rename the menu or the conflicting asset",
menu_name, name
));
}
result.push(entry);
}
}
*assets = result;
Ok(())
}
fn expand_one(
menu_name: &str,
menu: &MainMenu,
win_w: f32,
win_h: f32,
font_px: f32,
) -> Vec<serde_json::Value> {
let mut out = Vec::new();
let mut wants_settings = false;
let font_name = if menu.font.is_empty() {
let name = format!("{}_font", menu_name);
out.push(spec_to_value(&asset::font(&name, font_px as u32)));
name
} else {
menu.font.clone()
};
let items: Vec<(String, String)> = menu
.items
.iter()
.map(|item| {
let action = match item.action.trim().to_lowercase().as_str() {
"return" | "close" => "screen:hide".to_string(),
"settings" => {
wants_settings = true;
format!("screen:show:{}_settings_video", menu_name)
}
_ => item.action.clone(),
};
(item.label.clone(), action)
})
.collect();
out.extend(emit_menu_screen(
menu_name,
&menu.title,
&items,
menu,
&font_name,
MenuMetrics {
win_w,
win_h,
font_px,
},
menu.initial,
));
if !menu.toggle_key.is_empty() {
out.push(serde_json::json!({
"name": format!("{}_toggle", menu_name),
"type": "KeyBinding",
"args": { "key": menu.toggle_key, "action": format!("screen:toggle:{}", menu_name) }
}));
}
if wants_settings || !menu.settings_back_action.is_empty() {
for (suffix, _) in settings_tabs(menu.settings_profile) {
out.extend(emit_settings_tab(
menu_name, suffix, menu, &font_name, win_w, win_h, font_px,
));
}
}
out
}