Krolyxon
Commit 43502f8

Branches

Tags

Refs →

bench51cratesbench51-guisrcchrome.rs

435 lines · 13.9 KB · Rust

1//! Window chrome: menu bar, toolbar and status bar. They sit directly on
2//! the page, without boxes.
3
4use bench51_core::session::SPEEDS;
5use eframe::egui::{self, Align, Button, FontId, Layout, RichText, Stroke, Ui};
6
7use crate::icons::Icon;
8use crate::layout::Pane;
9use crate::state::{Action, Keymap, Modal, Mode, State, ThemePref};
10use crate::theme::{ACCENT, LABEL, LABEL_TRACKING, palette};
11use crate::widgets;
12
13fn item(ui: &mut Ui, text: &str, shortcut: &str) -> bool {
14 let p = palette(ui.ctx());
15 let label = RichText::new(text.to_uppercase())
16 .font(FontId::monospace(12.0))
17 .extra_letter_spacing(0.5)
18 .color(p.text_primary);
19 let mut b = Button::new(label).min_size(egui::vec2(240.0, 24.0));
20 if !shortcut.is_empty() {
21 b = b.shortcut_text(
22 RichText::new(shortcut)
23 .font(FontId::monospace(11.0))
24 .color(p.text_disabled),
25 );
26 }
27 ui.add(b).clicked()
28}
29
30fn check(ui: &mut Ui, text: &str, on: bool) -> bool {
31 let mark = if on { "● " } else { " " };
32 item(ui, &format!("{mark}{text}"), "")
33}
34
35fn title(ui: &Ui, text: &str) -> RichText {
36 let p = palette(ui.ctx());
37 RichText::new(text.to_uppercase())
38 .font(FontId::monospace(LABEL))
39 .extra_letter_spacing(LABEL_TRACKING)
40 .color(p.text_secondary)
41}
42
43pub fn menu_bar(ui: &mut Ui, st: &mut State) {
44 let mut acts = Vec::new();
45 let has_doc = st.doc().is_some();
46 egui::MenuBar::new().ui(ui, |ui| {
47 ui.menu_button(title(ui, "File"), |ui| {
48 if item(ui, "New", "Ctrl+N") {
49 acts.push(Action::New);
50 }
51 if item(ui, "Open…", "Ctrl+O") {
52 acts.push(Action::Open);
53 }
54 ui.menu_button(title(ui, "Open recent"), |ui| {
55 if st.prefs.recent.is_empty() {
56 ui.label(palette(ui.ctx()).label("none"));
57 }
58 for path in &st.prefs.recent {
59 let name = path.display().to_string();
60 if ui
61 .button(RichText::new(name).font(FontId::monospace(12.0)))
62 .clicked()
63 {
64 acts.push(Action::OpenPath(path.clone()));
65 }
66 }
67 });
68 ui.separator();
69 if item(ui, "Save", "Ctrl+S") {
70 acts.push(Action::Save);
71 }
72 if item(ui, "Save as…", "Ctrl+Shift+S") {
73 acts.push(Action::SaveAs);
74 }
75 if has_doc && item(ui, "Close file", "Ctrl+W") {
76 acts.push(Action::Close(st.active));
77 }
78 ui.separator();
79 if item(ui, "Quit", "Ctrl+Q") {
80 acts.push(Action::Quit);
81 }
82 });
83 ui.menu_button(title(ui, "Edit"), |ui| {
84 if item(ui, "Find…", "Ctrl+F") {
85 acts.push(Action::Find);
86 }
87 ui.separator();
88 if item(ui, "Toggle breakpoint", "F9") {
89 acts.push(Action::ToggleBreakpoint);
90 }
91 if item(ui, "Kill all breakpoints", "Ctrl+Shift+F9") {
92 acts.push(Action::ClearBreakpoints);
93 }
94 });
95 ui.menu_button(title(ui, "View"), |ui| {
96 for pane in Pane::ALL {
97 if item(ui, pane.title(), "") {
98 acts.push(Action::ShowPane(pane));
99 }
100 }
101 ui.separator();
102 if item(ui, "Zoom in", "Ctrl++") {
103 acts.push(Action::Zoom(0.1));
104 }
105 if item(ui, "Zoom out", "Ctrl+-") {
106 acts.push(Action::Zoom(-0.1));
107 }
108 if item(ui, "Actual size", "Ctrl+0") {
109 acts.push(Action::ZoomReset);
110 }
111 ui.separator();
112 if item(ui, "Reset layout", "") {
113 acts.push(Action::ResetLayout);
114 }
115 });
116 ui.menu_button(title(ui, "Project"), |ui| {
117 if item(ui, "Build target", "F7") {
118 acts.push(Action::Build);
119 }
120 ui.separator();
121 if item(ui, "Options for target…", "") {
122 acts.push(Action::Modal(Modal::Options(
123 st.prefs.clock_mhz.to_string(),
124 )));
125 }
126 });
127 ui.menu_button(title(ui, "Debug"), |ui| {
128 let session = if st.mode == Mode::Debug {
129 "Stop debug session"
130 } else {
131 "Start debug session"
132 };
133 if item(ui, session, "Ctrl+F5") {
134 acts.push(Action::ToggleDebug);
135 }
136 ui.separator();
137 let run = if st.running() { "Stop" } else { "Run" };
138 if item(ui, run, "F5") {
139 acts.push(Action::RunPause);
140 }
141 if item(ui, "Step into", "F11") {
142 acts.push(Action::StepInto);
143 }
144 if item(ui, "Step over", "F10") {
145 acts.push(Action::StepOver);
146 }
147 if item(ui, "Step out", "Ctrl+F11") {
148 acts.push(Action::StepOut);
149 }
150 if item(ui, "Run to cursor line", "Ctrl+F10") {
151 acts.push(Action::RunToCursor);
152 }
153 if item(ui, "Reset CPU", "F6") {
154 acts.push(Action::Reset);
155 }
156 ui.separator();
157 ui.menu_button(title(ui, "Speed"), |ui| {
158 for (i, n) in SPEEDS.iter().enumerate() {
159 if check(ui, &format!("{n} / tick"), st.prefs.speed == i) {
160 acts.push(Action::SetSpeed(i));
161 }
162 }
163 });
164 });
165 ui.menu_button(title(ui, "Peripherals"), |ui| {
166 for (pane, name) in [
167 (Pane::Ports, "I/O ports"),
168 (Pane::Timers, "Timers"),
169 (Pane::Interrupts, "Interrupts"),
170 (Pane::Serial, "Serial (UART #1)"),
171 ] {
172 if item(ui, name, "") {
173 acts.push(Action::ShowPane(pane));
174 }
175 }
176 });
177 ui.menu_button(title(ui, "Settings"), |ui| {
178 ui.menu_button(title(ui, "Keybindings"), |ui| {
179 for (k, name) in [(Keymap::Vim, "Vim"), (Keymap::Standard, "Standard")] {
180 if check(ui, name, st.prefs.keymap == k) {
181 acts.push(Action::SetKeymap(k));
182 }
183 }
184 });
185 ui.menu_button(title(ui, "Theme"), |ui| {
186 for (t, name) in [
187 (ThemePref::System, "System"),
188 (ThemePref::Dark, "Dark"),
189 (ThemePref::Light, "Light"),
190 ] {
191 if check(ui, name, st.prefs.theme == t) {
192 acts.push(Action::SetTheme(t));
193 }
194 }
195 });
196 });
197 ui.menu_button(title(ui, "Help"), |ui| {
198 if item(ui, "Keyboard shortcuts", "F1") {
199 acts.push(Action::Modal(Modal::Shortcuts));
200 }
201 if item(ui, "About bench51", "") {
202 acts.push(Action::Modal(Modal::About));
203 }
204 });
205 ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
206 let p = palette(ui.ctx());
207 ui.label(title(ui, "bench51").color(p.text_disabled));
208 });
209 });
210 st.actions.extend(acts);
211}
212
213pub fn toolbar(ui: &mut Ui, st: &mut State) {
214 let p = palette(ui.ctx());
215 let has_doc = st.doc().is_some();
216 let running = st.running();
217 let debug = st.mode == Mode::Debug;
218 let mut acts = Vec::new();
219 ui.horizontal(|ui| {
220 ui.spacing_mut().item_spacing.x = 2.0;
221 let mut b = |ui: &mut Ui, icon, tip, enabled, on, act| {
222 if widgets::icon_button(ui, icon, tip, enabled, on).clicked() {
223 acts.push(act);
224 }
225 };
226 b(ui, Icon::New, "New file (Ctrl+N)", true, false, Action::New);
227 b(ui, Icon::Open, "Open (Ctrl+O)", true, false, Action::Open);
228 b(
229 ui,
230 Icon::Save,
231 "Save (Ctrl+S)",
232 has_doc,
233 false,
234 Action::Save,
235 );
236 ui.add_space(18.0);
237 b(
238 ui,
239 Icon::Build,
240 "Build target (F7)",
241 has_doc,
242 false,
243 Action::Build,
244 );
245 ui.add_space(18.0);
246 b(
247 ui,
248 Icon::Debug,
249 if debug {
250 "Stop debug session (Ctrl+F5)"
251 } else {
252 "Start debug session (Ctrl+F5)"
253 },
254 has_doc,
255 debug,
256 Action::ToggleDebug,
257 );
258 ui.add_space(18.0);
259 b(
260 ui,
261 Icon::Reset,
262 "Reset CPU (F6)",
263 has_doc,
264 false,
265 Action::Reset,
266 );
267 if running {
268 b(
269 ui,
270 Icon::Pause,
271 "Stop (F5)",
272 has_doc,
273 false,
274 Action::RunPause,
275 );
276 } else {
277 b(ui, Icon::Run, "Run (F5)", has_doc, false, Action::RunPause);
278 }
279 b(
280 ui,
281 Icon::StepInto,
282 "Step into (F11)",
283 has_doc,
284 false,
285 Action::StepInto,
286 );
287 b(
288 ui,
289 Icon::StepOver,
290 "Step over (F10)",
291 has_doc,
292 false,
293 Action::StepOver,
294 );
295 b(
296 ui,
297 Icon::StepOut,
298 "Step out (Ctrl+F11)",
299 has_doc,
300 false,
301 Action::StepOut,
302 );
303 b(
304 ui,
305 Icon::RunToCursor,
306 "Run to cursor line (Ctrl+F10)",
307 has_doc,
308 false,
309 Action::RunToCursor,
310 );
311 ui.add_space(18.0);
312 b(
313 ui,
314 Icon::Breakpoint,
315 "Toggle breakpoint (F9)",
316 has_doc,
317 false,
318 Action::ToggleBreakpoint,
319 );
320
321 ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
322 ui.spacing_mut().item_spacing.x = 10.0;
323 ui.add_space(6.0);
324 let speed = st.prefs.speed;
325 ui.label(
326 RichText::new(format!("{:>6} / TICK", SPEEDS[speed]))
327 .font(FontId::monospace(LABEL))
328 .color(p.text_secondary),
329 );
330 if let Some(i) = widgets::segment_bar(
331 ui,
332 136.0,
333 10.0,
334 SPEEDS.len(),
335 speed + 1,
336 p.text_display,
337 true,
338 ) {
339 acts.push(Action::SetSpeed(i));
340 }
341 ui.label(p.label("Speed"));
342 });
343 });
344 st.actions.extend(acts);
345}
346
347/// True for status messages that report a failure.
348fn is_error(msg: &str) -> bool {
349 let m = msg.to_ascii_lowercase();
350 [
351 "error(s)",
352 "cannot ",
353 "invalid ",
354 "failed",
355 "reserved opcode",
356 "out of range",
357 ]
358 .iter()
359 .any(|w| m.contains(w))
360}
361
362pub fn status_bar(ui: &mut Ui, st: &State) {
363 let p = palette(ui.ctx());
364 let text = |s: String| {
365 RichText::new(s.to_uppercase())
366 .font(FontId::monospace(LABEL))
367 .extra_letter_spacing(0.6)
368 };
369 ui.horizontal(|ui| {
370 ui.spacing_mut().item_spacing.x = 14.0;
371 let doc = st.doc();
372 let (state, live) = match doc {
373 None => ("no file", false),
374 Some(d) if d.session.running => ("running", true),
375 Some(d) if d.session.build.is_none() => ("not built", false),
376 Some(d) if d.stale() => ("source changed", false),
377 Some(_) if st.mode == Mode::Debug => ("stopped", false),
378 Some(_) => ("ready", false),
379 };
380 let mode = if st.mode == Mode::Debug {
381 "simulation"
382 } else {
383 "edit"
384 };
385 ui.label(text(mode.into()).color(p.text_disabled));
386 let keys = match st.prefs.keymap {
387 Keymap::Vim => "vim",
388 Keymap::Standard => "std",
389 };
390 ui.label(text(keys.into()).color(p.text_disabled));
391 // Running: a dot that blinks in hard 2 Hz steps.
392 let (r, _) = ui.allocate_exact_size(egui::vec2(8.0, 8.0), egui::Sense::hover());
393 let on = !live || (ui.input(|i| i.time) * 4.0) as i64 % 2 == 0;
394 if live && on {
395 ui.painter().circle_filled(r.center(), 3.5, p.text_display);
396 } else {
397 ui.painter()
398 .circle_stroke(r.center(), 3.0, Stroke::new(1.0, p.text_disabled));
399 }
400 ui.label(text(format!("[{state}]")).color(if live {
401 p.text_display
402 } else {
403 p.text_secondary
404 }));
405 if let Some(d) = doc {
406 let msg = &d.session.status;
407 if !msg.is_empty() {
408 let color = if is_error(msg) {
409 ACCENT
410 } else {
411 p.text_primary
412 };
413 let shown = if is_error(msg) {
414 format!("[error: {msg}]")
415 } else {
416 msg.clone()
417 };
418 ui.label(text(shown).color(color));
419 }
420 }
421 ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
422 ui.label(text(format!("{} MHz", st.prefs.clock_mhz)).color(p.text_disabled));
423 if let Some(d) = doc {
424 if d.session.build.is_some() {
425 ui.label(text(format!("cyc {}", d.session.sim.cycles)).color(p.text_secondary));
426 ui.label(text(format!("t {}", d.session.elapsed())).color(p.text_secondary));
427 }
428 ui.label(
429 text(format!("l {} c {}", d.cursor.0 + 1, d.cursor.1 + 1))
430 .color(p.text_secondary),
431 );
432 }
433 });
434 });
435}