Skip to main content

simular/renderers/
tui.rs

1//! Generic TUI Renderer for `DemoEngine` Implementations
2//!
3//! Per specification SIMULAR-DEMO-002: This renderer is engine-agnostic.
4//! Any `DemoEngine` implementation can be rendered through this interface.
5//!
6//! # Usage
7//!
8//! ```ignore
9//! use simular::demos::{DemoEngine, OrbitalEngine};
10//! use simular::renderers::DemoRenderer;
11//!
12//! let yaml = std::fs::read_to_string("config.yaml")?;
13//! let engine = OrbitalEngine::from_yaml(&yaml)?;
14//! let renderer = DemoRenderer::new(engine);
15//! renderer.run()?;
16//! ```
17
18use crate::demos::{CriterionResult, DemoEngine, DemoMeta};
19use serde::Serialize;
20
21/// Trait for renderable demo data.
22///
23/// Engines that want TUI rendering implement this to provide
24/// display-friendly data without coupling to ratatui.
25pub trait RenderableDemo {
26    /// Get demo title for display.
27    fn title(&self) -> String;
28
29    /// Get current status line.
30    fn status_line(&self) -> String;
31
32    /// Get key metrics as (label, value) pairs.
33    fn metrics(&self) -> Vec<(String, String)>;
34
35    /// Get current step count.
36    fn current_step(&self) -> u64;
37
38    /// Check if demo is paused/complete.
39    fn is_running(&self) -> bool;
40}
41
42/// Generic demo renderer that works with any `DemoEngine`.
43///
44/// This is the unified renderer that replaces separate `OrbitApp`, `TspApp`, etc.
45#[derive(Debug)]
46pub struct DemoRenderer<E: DemoEngine> {
47    engine: E,
48    running: bool,
49    paused: bool,
50    step_count: u64,
51}
52
53impl<E: DemoEngine> DemoRenderer<E> {
54    /// Create a new renderer for the given engine.
55    #[must_use]
56    pub fn new(engine: E) -> Self {
57        Self {
58            engine,
59            running: true,
60            paused: false,
61            step_count: 0,
62        }
63    }
64
65    /// Get reference to the engine.
66    #[must_use]
67    pub fn engine(&self) -> &E {
68        &self.engine
69    }
70
71    /// Get mutable reference to the engine.
72    pub fn engine_mut(&mut self) -> &mut E {
73        &mut self.engine
74    }
75
76    /// Check if renderer is running.
77    #[must_use]
78    pub fn is_running(&self) -> bool {
79        self.running && !self.engine.is_complete()
80    }
81
82    /// Check if renderer is paused.
83    #[must_use]
84    pub fn is_paused(&self) -> bool {
85        self.paused
86    }
87
88    /// Toggle pause state.
89    pub fn toggle_pause(&mut self) {
90        self.paused = !self.paused;
91    }
92
93    /// Stop the renderer.
94    pub fn stop(&mut self) {
95        self.running = false;
96    }
97
98    /// Reset the engine.
99    pub fn reset(&mut self) {
100        self.engine.reset();
101        self.step_count = 0;
102        self.paused = false;
103    }
104
105    /// Advance the simulation by one step.
106    pub fn step(&mut self) -> E::StepResult {
107        let result = self.engine.step();
108        self.step_count += 1;
109        result
110    }
111
112    /// Get demo metadata.
113    #[must_use]
114    pub fn meta(&self) -> &DemoMeta {
115        self.engine.meta()
116    }
117
118    /// Get current state.
119    #[must_use]
120    pub fn state(&self) -> E::State {
121        self.engine.state()
122    }
123
124    /// Evaluate falsification criteria.
125    #[must_use]
126    pub fn evaluate_criteria(&self) -> Vec<CriterionResult> {
127        self.engine.evaluate_criteria()
128    }
129
130    /// Get step count.
131    #[must_use]
132    pub fn step_count(&self) -> u64 {
133        self.step_count
134    }
135
136    /// Get seed for reproducibility.
137    #[must_use]
138    pub fn seed(&self) -> u64 {
139        self.engine.seed()
140    }
141}
142
143/// Render data for TUI display.
144///
145/// This struct contains all data needed to render a frame,
146/// decoupled from the actual rendering implementation.
147#[derive(Debug, Clone, Serialize)]
148pub struct RenderFrame {
149    /// Demo title.
150    pub title: String,
151    /// Demo type (e.g., "orbit", "tsp").
152    pub demo_type: String,
153    /// Current step number.
154    pub step: u64,
155    /// Seed for reproducibility.
156    pub seed: u64,
157    /// Whether demo is paused.
158    pub paused: bool,
159    /// Whether demo is complete.
160    pub complete: bool,
161    /// Key-value metrics for display.
162    pub metrics: Vec<(String, String)>,
163    /// Falsification criteria results.
164    pub criteria: Vec<CriterionResult>,
165}
166
167impl<E: DemoEngine> DemoRenderer<E>
168where
169    E::State: Serialize,
170{
171    /// Generate a render frame from current state.
172    #[must_use]
173    pub fn render_frame(&self) -> RenderFrame {
174        let meta = self.engine.meta();
175
176        RenderFrame {
177            title: format!("{} ({})", meta.id, meta.version),
178            demo_type: meta.demo_type.clone(),
179            step: self.step_count,
180            seed: self.engine.seed(),
181            paused: self.paused,
182            complete: self.engine.is_complete(),
183            metrics: Vec::new(), // Engines can provide custom metrics
184            criteria: self.engine.evaluate_criteria(),
185        }
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use crate::demos::OrbitalEngine;
193
194    const TEST_YAML: &str = r#"
195simulation:
196  type: orbit
197  name: "Test Orbit"
198
199meta:
200  id: "TEST-001"
201  version: "1.0.0"
202  demo_type: orbit
203
204reproducibility:
205  seed: 42
206
207scenario:
208  type: kepler
209  central_body:
210    name: "Sun"
211    mass_kg: 1.989e30
212    position: [0.0, 0.0, 0.0]
213  orbiter:
214    name: "Earth"
215    mass_kg: 5.972e24
216    semi_major_axis_m: 1.496e11
217    eccentricity: 0.0167
218
219integrator:
220  type: stormer_verlet
221  dt_seconds: 3600.0
222"#;
223
224    #[test]
225    fn test_renderer_creation() {
226        let engine = OrbitalEngine::from_yaml(TEST_YAML).unwrap();
227        let renderer = DemoRenderer::new(engine);
228
229        assert!(renderer.is_running());
230        assert!(!renderer.is_paused());
231        assert_eq!(renderer.step_count(), 0);
232    }
233
234    #[test]
235    fn test_renderer_step() {
236        let engine = OrbitalEngine::from_yaml(TEST_YAML).unwrap();
237        let mut renderer = DemoRenderer::new(engine);
238
239        renderer.step();
240        assert_eq!(renderer.step_count(), 1);
241
242        renderer.step();
243        assert_eq!(renderer.step_count(), 2);
244    }
245
246    #[test]
247    fn test_renderer_pause() {
248        let engine = OrbitalEngine::from_yaml(TEST_YAML).unwrap();
249        let mut renderer = DemoRenderer::new(engine);
250
251        assert!(!renderer.is_paused());
252        renderer.toggle_pause();
253        assert!(renderer.is_paused());
254        renderer.toggle_pause();
255        assert!(!renderer.is_paused());
256    }
257
258    #[test]
259    fn test_renderer_reset() {
260        let engine = OrbitalEngine::from_yaml(TEST_YAML).unwrap();
261        let mut renderer = DemoRenderer::new(engine);
262
263        renderer.step();
264        renderer.step();
265        assert_eq!(renderer.step_count(), 2);
266
267        renderer.reset();
268        assert_eq!(renderer.step_count(), 0);
269    }
270
271    #[test]
272    fn test_renderer_meta() {
273        let engine = OrbitalEngine::from_yaml(TEST_YAML).unwrap();
274        let renderer = DemoRenderer::new(engine);
275
276        let meta = renderer.meta();
277        assert_eq!(meta.id, "TEST-001");
278        assert_eq!(meta.demo_type, "orbit");
279    }
280
281    #[test]
282    fn test_render_frame() {
283        let engine = OrbitalEngine::from_yaml(TEST_YAML).unwrap();
284        let mut renderer = DemoRenderer::new(engine);
285
286        renderer.step();
287        let frame = renderer.render_frame();
288
289        assert!(frame.title.contains("TEST-001"));
290        assert_eq!(frame.demo_type, "orbit");
291        assert_eq!(frame.step, 1);
292        assert_eq!(frame.seed, 42);
293        assert!(!frame.paused);
294    }
295
296    #[test]
297    fn test_renderer_stop() {
298        let engine = OrbitalEngine::from_yaml(TEST_YAML).unwrap();
299        let mut renderer = DemoRenderer::new(engine);
300
301        assert!(renderer.is_running());
302        renderer.stop();
303        assert!(!renderer.is_running());
304    }
305
306    #[test]
307    fn test_renderer_criteria() {
308        let engine = OrbitalEngine::from_yaml(TEST_YAML).unwrap();
309        let renderer = DemoRenderer::new(engine);
310
311        let criteria = renderer.evaluate_criteria();
312        assert!(!criteria.is_empty());
313    }
314}