concinnity_asset/screen.rs
1// Overlay-screen schema.
2
3use crate::{AssetId, de_opt_asset_ref};
4use alloc::string::String;
5
6/// How a [Screen](#screen) treats input while it is active.
7#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
8#[serde(rename_all = "lowercase")]
9pub enum ScreenInput {
10 /// The screen owns input while it is the topmost capturing screen:
11 /// gameplay input is suppressed and lower screens' [HitRegion](#hitregion)s
12 /// stop firing.
13 #[default]
14 Capture,
15 /// The screen only draws; input passes through to whatever is beneath it.
16 Passthrough,
17}
18
19/// A named full-screen layer of UI drawn over the world: a pause menu, a
20/// settings page, a console, a score overlay.
21///
22/// UI elements ([Sprite](#sprite), [TextLabel](#textlabel),
23/// [TextInput](#textinput), [HitRegion](#hitregion)) belong to a screen by
24/// name prefix `<screen_name>_*`, mirroring the [Scene](#scene) →
25/// [Prop](#prop) convention. Active screens form a stack; each is shown /
26/// hidden via [HitRegion](#hitregion) or [KeyBinding](#keybinding) actions:
27/// - `screen:show:<name>` replaces the top of the stack (menu navigation)
28/// - `screen:push:<name>` opens on top of what is already showing
29/// - `screen:hide` closes the top screen, revealing what was beneath
30/// - `screen:toggle:<name>` closes the screen if it is on top, opens it otherwise
31///
32/// Screens draw in stack order (later on top); `layer` orders a screen
33/// against the always-on HUD and other screens independent of stack position.
34/// While any active screen has `pauses_world` set, the world freezes exactly
35/// as today's pause menu does. A `toggle_key` opens and closes the screen from
36/// anywhere. `focus` names a [TextInput](#textinput) that receives keyboard
37/// focus whenever the screen reaches the top of the stack. Worlds that need no
38/// menus simply declare no screens.
39///
40/// ```rust
41/// # use concinnity_asset::Screen;
42/// Screen {
43/// toggle_key: "Escape".into(),
44/// ..Default::default()
45/// };
46/// ```
47#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
48#[serde(default)]
49pub struct Screen {
50 /// Assigned by the loader; not authored.
51 #[serde(skip)]
52 pub asset_id: AssetId,
53 /// When true, this screen is shown as soon as the world loads.
54 pub initial: bool,
55 /// Seconds to fade the screen in when it's shown. 0 shows it instantly.
56 pub fade_in_secs: f32,
57 /// InputKey that toggles this screen open / closed from anywhere, by the same
58 /// canonical key names a [KeyBinding](#keybinding) uses (e.g. "Escape",
59 /// "Backtick"). Empty leaves the screen action-driven only.
60 pub toggle_key: String,
61 /// Input policy while the screen is active.
62 pub input: ScreenInput,
63 /// When true (the default), the world pauses beneath this screen while it
64 /// is active: gameplay input, physics, and animation freeze.
65 pub pauses_world: bool,
66 /// [TextInput](#textinput) that receives keyboard focus whenever this
67 /// screen reaches the top of the stack.
68 #[serde(deserialize_with = "de_opt_asset_ref")]
69 pub focus: Option<AssetId>,
70 /// Draw-order bias against the always-on HUD and other screens. Screens
71 /// default above the HUD in stack order; a negative layer draws beneath
72 /// the HUD, a higher layer stays above later-pushed screens.
73 pub layer: i32,
74}
75
76impl Default for Screen {
77 fn default() -> Self {
78 Self {
79 asset_id: AssetId::default(),
80 initial: false,
81 fade_in_secs: 0.0,
82 toggle_key: String::new(),
83 input: ScreenInput::Capture,
84 pauses_world: true,
85 focus: None,
86 layer: 0,
87 }
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn a_blank_screen_captures_input_and_pauses_the_world() {
97 // An overlay is modal by default: the world underneath neither ticks nor
98 // sees the input the screen is consuming.
99 let s = Screen::default();
100 assert_eq!(s.input, ScreenInput::Capture);
101 assert!(s.pauses_world);
102 assert!(!s.initial);
103 assert_eq!(s.fade_in_secs, 0.0);
104 assert_eq!(s.layer, 0);
105 assert!(s.toggle_key.is_empty());
106 assert!(s.focus.is_none());
107 assert_eq!(ScreenInput::default(), ScreenInput::Capture);
108 }
109
110 #[test]
111 fn a_passthrough_hud_parses_and_round_trips_through_postcard() {
112 crate::test_support::install_resolvers();
113 let s: Screen = serde_json::from_str(
114 r#"{"initial":true,"fade_in_secs":0.5,"toggle_key":"Tab","input":"passthrough",
115 "pauses_world":false,"focus":"first_button","layer":-1}"#,
116 )
117 .unwrap();
118 assert_eq!(s.input, ScreenInput::Passthrough);
119 assert!(!s.pauses_world);
120 assert!(s.initial);
121 assert_eq!(s.focus, Some(AssetId(12)));
122 assert_eq!(
123 serde_json::to_string(&ScreenInput::Passthrough).unwrap(),
124 r#""passthrough""#
125 );
126
127 let bytes = postcard::to_allocvec(&s).unwrap();
128 let back: Screen = postcard::from_bytes(&bytes).unwrap();
129 assert_eq!(back.fade_in_secs, 0.5);
130 assert_eq!(back.toggle_key, "Tab");
131 assert_eq!(back.input, ScreenInput::Passthrough);
132 // A negative layer sits below the default overlays.
133 assert_eq!(back.layer, -1);
134 assert_eq!(back.asset_id, AssetId::default());
135 }
136}