Skip to main content

khora_editor/chrome/
status_bar.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Status bar — one monospace strip of live facts.
16//!
17//! Every value here changes every frame, so every value is monospace: numbers
18//! stay in their columns instead of jittering as digits change width. The
19//! colours are semantic — a red frame time means the frame is late, not that
20//! red looked nice there.
21
22use 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
31/// Height of the status bar.
32pub const STATUS_HEIGHT: f32 = 26.0;
33
34/// The bottom status strip.
35pub struct StatusBarPanel {
36    state: Arc<Mutex<EditorState>>,
37    theme: UiTheme,
38}
39
40impl StatusBarPanel {
41    /// Creates a new status bar.
42    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        // ── Left: is it alive, and where are we ──
75        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        // Load: cpu, then the two memory pools.
86        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        // ── Right: what we are editing, and how fast ──
107        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        // The frame budget, in the colour of whether we are meeting it.
120        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
142/// A vertical rule; returns the x cursor past it.
143fn 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
148/// A vertical rule laid out right-to-left; returns the x cursor left of it.
149fn 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    /// How healthy the frame loop is, as a `0..=1` ratio — drives the liveness
167    /// dot through the same thresholds every other health indicator uses.
168    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}