Skip to main content

shuyi_ui/
platform_facts.rs

1//! [`PlatformFacts`]: where the game runs, as its screens see it
2//! ([`Ctx::platform`](crate::Ctx::platform)). A fact is constant for a
3//! run: the platform's backend says them before the game loads, read from
4//! its target's profile ([`TargetProfile`]) or from the system.
5//!
6//! A screen asks a fact instead of a `cfg!`, so a test can run it as any
7//! target: offer Quit only where [`can_quit`](PlatformFacts::can_quit).
8//! The facts are the screens'. A game's rules never read them, so a
9//! replay plays the same on every target.
10
11use crate::prelude::*;
12
13pub use shuyi_assets::target::Quit;
14use shuyi_assets::target::TargetProfile;
15
16use crate::ctx::Keyboard;
17use crate::input::state::Device;
18
19/// Where the game runs: its target's id, and what that target lets a
20/// game do and the player press.
21#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
22#[serde(deny_unknown_fields)]
23#[non_exhaustive]
24pub struct PlatformFacts {
25    /// The target's id, as its profile names it (`desktop`, `web`,
26    /// `steam-deck`, `playdate`).
27    pub target: String,
28    /// How the player leaves the game: by its own Quit, or through the
29    /// system ([`can_quit`](Self::can_quit)).
30    pub quit: Quit,
31    /// Whether the game is in a window the player can switch to full
32    /// screen and back. Where it isn't, the Fullscreen setting does
33    /// nothing, and the engine's options screen leaves its row out.
34    pub windowed: bool,
35    /// Whether the device has a keyboard: without one the game's
36    /// first-key screen never opens, and the key bindings screen shows the
37    /// controller's alone.
38    pub keyboard: Keyboard,
39    /// Whether a mouse or another pointer exists.
40    pub pointer: bool,
41    /// Whether a touch screen exists.
42    pub touch: bool,
43    /// The device the player holds, where the target has just the one: help
44    /// text names its buttons from the first frame, before any press
45    /// ([`Ctx::device`](crate::Ctx::device)). `None` where it isn't known
46    /// until something is pressed.
47    pub device: Option<Device>,
48}
49
50impl PlatformFacts {
51    /// The facts of the target `profile` describes.
52    pub fn from_profile(profile: &TargetProfile) -> Self {
53        let input = &profile.input;
54        Self {
55            target: profile.id.clone(),
56            quit: profile.quit,
57            windowed: profile.display.windowed,
58            keyboard: if input.keyboard {
59                Keyboard::Present
60            } else {
61                Keyboard::Absent
62            },
63            pointer: input.pointer,
64            touch: input.touch,
65            device: None,
66        }
67    }
68
69    /// The same facts on a target whose player holds `device`.
70    #[must_use]
71    pub fn with_device(mut self, device: Device) -> Self {
72        self.device = Some(device);
73        self
74    }
75
76    /// Whether the game may offer its own Quit: where the player leaves
77    /// through the system (a browser's tab, a console's home button), a
78    /// Quit would do nothing, so a game leaves it out.
79    pub fn can_quit(&self) -> bool {
80        self.quit == Quit::Game
81    }
82}
83
84impl Default for PlatformFacts {
85    /// The engine's `desktop` target's facts: a window that can go full
86    /// screen, the game's own Quit, a keyboard and a pointer, no touch.
87    fn default() -> Self {
88        Self {
89            target: "desktop".to_owned(),
90            quit: Quit::Game,
91            windowed: true,
92            keyboard: Keyboard::Present,
93            pointer: true,
94            touch: false,
95            device: None,
96        }
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn a_target_says_the_device_its_player_holds() {
106        let device = Device::Pad(crate::input::pad::PadKind::Playdate);
107        assert_eq!(PlatformFacts::default().device, None);
108        assert_eq!(
109            PlatformFacts::default().with_device(device).device,
110            Some(device)
111        );
112    }
113
114    /// The engine's profile `id`, from `targets/`.
115    fn engine(id: &str, source: &str) -> TargetProfile {
116        let file = format!("targets/{id}.ron");
117        TargetProfile::from_source(&file, id, source).unwrap()
118    }
119
120    fn desktop() -> TargetProfile {
121        engine("desktop", include_str!("../../../targets/desktop.ron"))
122    }
123
124    #[test]
125    fn the_default_facts_are_the_desktops() {
126        assert_eq!(
127            PlatformFacts::default(),
128            PlatformFacts::from_profile(&desktop())
129        );
130    }
131
132    #[test]
133    fn each_engine_profile_gives_its_facts() {
134        let facts = |id, source| PlatformFacts::from_profile(&engine(id, source));
135        let web = facts("web", include_str!("../../../targets/web.ron"));
136        assert_eq!(web.target, "web");
137        assert!(!web.can_quit());
138        assert!(web.windowed && web.pointer && !web.touch);
139        assert_eq!(web.keyboard, Keyboard::Present);
140        let deck = facts(
141            "steam-deck",
142            include_str!("../../../targets/steam-deck.ron"),
143        );
144        assert!(deck.can_quit());
145        assert!(!deck.windowed && !deck.pointer && deck.touch);
146        assert_eq!(deck.keyboard, Keyboard::Absent);
147        let playdate = facts("playdate", include_str!("../../../targets/playdate.ron"));
148        assert!(!playdate.can_quit() && !playdate.windowed);
149        assert!(!playdate.pointer && !playdate.touch);
150        assert_eq!(playdate.keyboard, Keyboard::Absent);
151        let desktop = PlatformFacts::from_profile(&desktop());
152        assert!(desktop.can_quit());
153    }
154}