1use minui::Window;
9use minui::prelude::*;
10use std::sync::{Arc, Mutex};
11use std::time::Duration;
12
13#[derive(Clone)]
14struct DemoState {
15 frame_count: u64,
16 latest_profile: Arc<Mutex<Option<FrameProfile>>>,
17}
18
19fn main() -> minui::Result<()> {
20 let profile_store = Arc::new(Mutex::new(None));
21
22 let mut app = App::new(DemoState {
23 frame_count: 0,
24 latest_profile: Arc::clone(&profile_store),
25 })?
26 .with_frame_rate(Duration::from_millis(16))
27 .with_frame_budget(Duration::from_millis(16));
28
29 {
30 let store = Arc::clone(&profile_store);
31 app.set_frame_profile_hook(move |profile| {
32 if let Ok(mut slot) = store.lock() {
33 *slot = Some(*profile);
34 }
35 });
36 }
37
38 app.run(
39 |state, event| {
40 match event {
41 Event::KeyWithModifiers(k) if matches!(k.key, KeyKind::Char('q')) => return false,
42 Event::Character('q') => return false,
43 Event::Frame => {
44 state.frame_count = state.frame_count.saturating_add(1);
45 }
46 _ => {}
47 }
48 true
49 },
50 |state, window| {
51 draw_header(window)?;
52 draw_code(window)?;
53 draw_profile_footer(state, window)?;
54 window.flush()?;
55 Ok(())
56 },
57 )
58}
59
60fn draw_header(window: &mut dyn Window) -> minui::Result<()> {
61 window.write_str_colored(
62 0,
63 0,
64 "MinUI syntax + frame profiling demo (press q to quit)",
65 Color::LightCyan.fg(),
66 )?;
67 window.write_str_colored(
68 1,
69 0,
70 "write_spans_colored() is used below for token-based colouring.",
71 Color::DarkGray.fg(),
72 )?;
73 Ok(())
74}
75
76fn draw_code(window: &mut dyn Window) -> minui::Result<()> {
77 let kw = Color::LightMagenta.fg();
78 let ident = Color::LightBlue.fg();
79 let punct = Color::LightGray.fg();
80 let number = Color::LightYellow.fg();
81 let string = Color::LightGreen.fg();
82 let comment = Color::DarkGray.fg();
83 let plain = Color::White.fg();
84
85 window.write_spans_colored(
86 3,
87 0,
88 &[
89 ColoredSpan::new("fn", kw),
90 ColoredSpan::new(" ", plain),
91 ColoredSpan::new("render_status", ident),
92 ColoredSpan::new("(", punct),
93 ColoredSpan::new("fps", ident),
94 ColoredSpan::new(": ", punct),
95 ColoredSpan::new("u32", ident),
96 ColoredSpan::new(") {", punct),
97 ],
98 )?;
99
100 window.write_spans_colored(
101 4,
102 0,
103 &[
104 ColoredSpan::new(" ", plain),
105 ColoredSpan::new("let", kw),
106 ColoredSpan::new(" budget_ms = ", plain),
107 ColoredSpan::new("16", number),
108 ColoredSpan::new(";", punct),
109 ColoredSpan::new(" // ~60 FPS target", comment),
110 ],
111 )?;
112
113 window.write_spans_colored(
114 5,
115 0,
116 &[
117 ColoredSpan::new(" ", plain),
118 ColoredSpan::new("println!", ident),
119 ColoredSpan::new("(", punct),
120 ColoredSpan::new("\"fps={} budget={}ms\"", string),
121 ColoredSpan::new(", fps, budget_ms", plain),
122 ColoredSpan::new(");", punct),
123 ],
124 )?;
125
126 window.write_spans_colored(6, 0, &[ColoredSpan::new("}", punct)])?;
127 Ok(())
128}
129
130fn draw_profile_footer(state: &DemoState, window: &mut dyn Window) -> minui::Result<()> {
131 let (w, h) = window.get_size();
132 if h == 0 {
133 return Ok(());
134 }
135
136 let y = h.saturating_sub(1);
137 let profile = state.latest_profile.lock().ok().and_then(|g| *g);
138
139 if let Some(p) = profile {
140 let colour = if p.over_budget {
141 Color::LightRed.fg()
142 } else {
143 Color::LightGreen.fg()
144 };
145 let budget_ms = p.budget.map(|d| d.as_millis()).unwrap_or(0);
146 let line = format!(
147 "frame={} events={} total={}ms input={}ms update={}ms draw={}ms budget={}ms over_budget={}",
148 state.frame_count,
149 p.events_processed,
150 p.frame_time.as_millis(),
151 p.input_poll_time.as_millis(),
152 p.update_time.as_millis(),
153 p.draw_time.as_millis(),
154 budget_ms,
155 p.over_budget
156 );
157 let clipped = clip_to_cells(&line, w, TabPolicy::SingleCell);
158 window.write_str_colored(y, 0, &clipped, colour)?;
159 } else {
160 window.write_str_colored(y, 0, "Waiting for frame profile...", Color::DarkGray.fg())?;
161 }
162
163 Ok(())
164}