1use std::sync::{Arc, Mutex};
23
24use khora_sdk::editor_ui::*;
25use khora_tool_ui::widgets::{
26 self,
27 paint::{icon, mono},
28 status_dot, Health,
29};
30
31pub const STATUS_HEIGHT: f32 = 26.0;
33
34pub struct StatusBarPanel {
36 state: Arc<Mutex<EditorState>>,
37 theme: UiTheme,
38}
39
40impl StatusBarPanel {
41 pub fn new(state: Arc<Mutex<EditorState>>, theme: UiTheme) -> Self {
43 Self { state, theme }
44 }
45}
46
47impl EditorPanel for StatusBarPanel {
48 fn id(&self) -> &str {
49 "khora.editor.status_bar"
50 }
51
52 fn title(&self) -> &str {
53 "Status Bar"
54 }
55
56 fn preferred_size(&self) -> Option<f32> {
57 Some(STATUS_HEIGHT)
58 }
59
60 fn ui(&mut self, ui: &mut dyn UiBuilder) {
61 let t = &self.theme;
62 let Ok(s) = self.state.lock() else { return };
63 let snap = Snapshot::from(&*s);
64 drop(s);
65
66 let r = ui.panel_rect();
67 ui.paint_rect_filled([r[0], r[1]], [r[2], r[3]], t.surface, 0.0);
68 ui.paint_line([r[0], r[1]], [widgets::right(r), r[1]], t.border, 1.0);
69
70 let size = t.font_size_caption;
71 let y = widgets::text_y(r, size);
72 let cy = r[1] + r[3] * 0.5;
73
74 let mut x = r[0] + 14.0;
76 status_dot(ui, t, [x, cy], Health::from_ratio(snap.health()));
77 x += 12.0;
78
79 if let Some(branch) = snap.git_branch.as_deref() {
80 mono(ui, [x, y], branch, size, t.text_dim);
81 x += ui.measure_text(branch, size, FontFamilyHint::Monospace)[0] + 14.0;
82 x = vsep(ui, t, x, r);
83 }
84
85 let cpu = (snap.cpu_load * 100.0).clamp(0.0, 100.0);
87 let cpu_label = format!("cpu {cpu:.0}%");
88 mono(
89 ui,
90 [x, y],
91 &cpu_label,
92 size,
93 if cpu > 70.0 { t.warning } else { t.text_dim },
94 );
95 x += ui.measure_text(&cpu_label, size, FontFamilyHint::Monospace)[0] + 12.0;
96
97 let heap = format!("heap {:.0}M", snap.memory_used_mb);
98 mono(ui, [x, y], &heap, size, t.text_dim);
99 x += ui.measure_text(&heap, size, FontFamilyHint::Monospace)[0] + 12.0;
100
101 if snap.vram_mb > 0.0 {
102 let vram = format!("vram {:.1}G", snap.vram_mb / 1024.0);
103 mono(ui, [x, y], &vram, size, t.text_dim);
104 }
105
106 let mut rx = widgets::right(r) - 14.0;
108
109 let project = format!(
110 "{} · v{}",
111 snap.project.as_deref().unwrap_or("untitled"),
112 snap.engine_version.as_deref().unwrap_or("dev"),
113 );
114 rx -= ui.measure_text(&project, size, FontFamilyHint::Monospace)[0];
115 mono(ui, [rx, y], &project, size, t.text_muted);
116 rx -= 14.0;
117 rx = vsep_left(ui, t, rx, r);
118
119 let frame = format!("{:.0} fps · {:.1}ms", snap.fps, snap.frame_time_ms);
121 let frame_color = if snap.fps > 55.0 {
122 t.success
123 } else if snap.fps > 30.0 {
124 t.warning
125 } else {
126 t.error
127 };
128 rx -= ui.measure_text(&frame, size, FontFamilyHint::Monospace)[0];
129 mono(ui, [rx, y], &frame, size, frame_color);
130 rx -= 8.0;
131
132 icon(
133 ui,
134 [rx - 13.0, y - 1.0],
135 Icon::Zap,
136 size + 1.0,
137 t.text_disabled,
138 );
139 }
140}
141
142fn vsep(ui: &mut dyn UiBuilder, t: &UiTheme, x: f32, r: [f32; 4]) -> f32 {
144 ui.paint_line([x, r[1] + 7.0], [x, r[1] + r[3] - 7.0], t.separator, 1.0);
145 x + 14.0
146}
147
148fn vsep_left(ui: &mut dyn UiBuilder, t: &UiTheme, x: f32, r: [f32; 4]) -> f32 {
150 ui.paint_line([x, r[1] + 7.0], [x, r[1] + r[3] - 7.0], t.separator, 1.0);
151 x - 14.0
152}
153
154struct Snapshot {
155 fps: f32,
156 frame_time_ms: f32,
157 memory_used_mb: f32,
158 cpu_load: f32,
159 vram_mb: f32,
160 project: Option<String>,
161 git_branch: Option<String>,
162 engine_version: Option<String>,
163}
164
165impl Snapshot {
166 fn health(&self) -> f32 {
169 (self.fps / 60.0).clamp(0.0, 1.0)
170 }
171}
172
173impl From<&EditorState> for Snapshot {
174 fn from(s: &EditorState) -> Self {
175 Self {
176 fps: s.status.fps,
177 frame_time_ms: s.status.frame_time_ms,
178 memory_used_mb: s.status.memory_used_mb,
179 cpu_load: s.status.cpu_load,
180 vram_mb: s.status.vram_mb,
181 project: s.project_name.clone(),
182 git_branch: s.current_git_branch.clone(),
183 engine_version: s.project_engine_version.clone(),
184 }
185 }
186}