Skip to main content

ashlar_control/
clock.rs

1//! `app.clock`: pause, step and resume the time a windowed app's frames see.
2//!
3//! Params, at most one of: `{pause: true}`, `{step_ms: N}` (pause if running, then run exactly one
4//! frame N ms later), `{resume: true}`; `{}` only reads. The reply is `{paused, time}` (the egui
5//! time the next frame sees, in seconds). Input still runs frames while paused, at the frozen
6//! time. See `docs/control-protocol.md`.
7
8use serde_json::Value;
9
10/// The clock method. Answered by the windowed runtime (`ashlar-app`), which lists it in
11/// `app.version`; a headless session refuses it.
12pub const APP_CLOCK: &str = "app.clock";
13
14/// Longest single `step_ms`: one minute.
15pub const MAX_STEP_MS: f64 = 60_000.0;
16
17/// What an `app.clock` request asks for.
18#[derive(Debug, Clone, Copy, PartialEq)]
19pub enum ClockOp {
20    /// `{}`: report the clock.
21    Read,
22    /// `{pause: true}`: freeze time at the current frame's.
23    Pause,
24    /// `{step_ms: N}`: freeze (if running), then move the frozen time on by `N` ms for one frame.
25    Step {
26        /// Milliseconds, `0 < ms <= MAX_STEP_MS`.
27        ms: f64,
28    },
29    /// `{resume: true}`: let time run again from the frozen value (no jump).
30    Resume,
31}
32
33impl ClockOp {
34    /// Read the request's params. `pause: false` and `resume: false` mean nothing; asking for two
35    /// things at once is an error.
36    pub fn from_params(p: &Value) -> Result<ClockOp, String> {
37        if !p.is_null() && !p.is_object() {
38            return Err("app.clock takes {pause: true}, {step_ms: N}, {resume: true} or {}".into());
39        }
40        let yes = |k: &str| p.get(k).and_then(Value::as_bool) == Some(true);
41        let mut ops = Vec::new();
42        if yes("pause") {
43            ops.push(ClockOp::Pause);
44        }
45        if yes("resume") {
46            ops.push(ClockOp::Resume);
47        }
48        if let Some(v) = p.get("step_ms").filter(|v| !v.is_null()) {
49            let ms = v.as_f64().filter(|m| m.is_finite() && *m > 0.0 && *m <= MAX_STEP_MS);
50            let Some(ms) = ms else { return Err(format!("step_ms is a number of milliseconds above 0 and at most {MAX_STEP_MS}")) };
51            ops.push(ClockOp::Step { ms });
52        }
53        match ops.as_slice() {
54            [] => Ok(ClockOp::Read),
55            [op] => Ok(*op),
56            _ => Err("app.clock does one of pause, step_ms or resume per request".into()),
57        }
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    use serde_json::json;
65
66    #[test]
67    fn params_name_one_operation() {
68        assert_eq!(ClockOp::from_params(&json!({})), Ok(ClockOp::Read));
69        assert_eq!(ClockOp::from_params(&Value::Null), Ok(ClockOp::Read));
70        assert_eq!(ClockOp::from_params(&json!({"pause": true})), Ok(ClockOp::Pause));
71        assert_eq!(ClockOp::from_params(&json!({"pause": false})), Ok(ClockOp::Read));
72        assert_eq!(ClockOp::from_params(&json!({"resume": true})), Ok(ClockOp::Resume));
73        assert_eq!(ClockOp::from_params(&json!({"step_ms": 16.7})), Ok(ClockOp::Step { ms: 16.7 }));
74        assert!(ClockOp::from_params(&json!({"pause": true, "resume": true})).is_err());
75        assert!(ClockOp::from_params(&json!({"step_ms": 0})).is_err());
76        assert!(ClockOp::from_params(&json!({"step_ms": -5})).is_err());
77        assert!(ClockOp::from_params(&json!({"step_ms": "16"})).is_err());
78        assert!(ClockOp::from_params(&json!({"step_ms": 1e9})).is_err());
79        assert!(ClockOp::from_params(&json!([1])).is_err());
80    }
81}