Skip to main content

concinnity_core/components/
screen.rs

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