Krolyxon
Commit 3ec5af1

Branches

Tags

Refs →

bench51cratesbench51-guisrcchrome.rs

421 lines · 13.4 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, 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 ui.menu_button(title(ui, "Theme"), |ui| {
103 for (t, name) in [
104 (ThemePref::System, "System"),
105 (ThemePref::Dark, "Dark"),
106 (ThemePref::Light, "Light"),
107 ] {
108 if check(ui, name, st.prefs.theme == t) {
109 acts.push(Action::SetTheme(t));
110 }
111 }
112 });
113 if item(ui, "Zoom in", "Ctrl++") {
114 acts.push(Action::Zoom(0.1));
115 }
116 if item(ui, "Zoom out", "Ctrl+-") {
117 acts.push(Action::Zoom(-0.1));
118 }
119 if item(ui, "Actual size", "Ctrl+0") {
120 acts.push(Action::ZoomReset);
121 }
122 ui.separator();
123 if item(ui, "Reset layout", "") {
124 acts.push(Action::ResetLayout);
125 }
126 });
127 ui.menu_button(title(ui, "Project"), |ui| {
128 if item(ui, "Build target", "F7") {
129 acts.push(Action::Build);
130 }
131 ui.separator();
132 if item(ui, "Options for target…", "") {
133 acts.push(Action::Modal(Modal::Options(
134 st.prefs.clock_mhz.to_string(),
135 )));
136 }
137 });
138 ui.menu_button(title(ui, "Debug"), |ui| {
139 let session = if st.mode == Mode::Debug {
140 "Stop debug session"
141 } else {
142 "Start debug session"
143 };
144 if item(ui, session, "Ctrl+F5") {
145 acts.push(Action::ToggleDebug);
146 }
147 ui.separator();
148 let run = if st.running() { "Stop" } else { "Run" };
149 if item(ui, run, "F5") {
150 acts.push(Action::RunPause);
151 }
152 if item(ui, "Step into", "F11") {
153 acts.push(Action::StepInto);
154 }
155 if item(ui, "Step over", "F10") {
156 acts.push(Action::StepOver);
157 }
158 if item(ui, "Step out", "Ctrl+F11") {
159 acts.push(Action::StepOut);
160 }
161 if item(ui, "Run to cursor line", "Ctrl+F10") {
162 acts.push(Action::RunToCursor);
163 }
164 if item(ui, "Reset CPU", "F6") {
165 acts.push(Action::Reset);
166 }
167 ui.separator();
168 ui.menu_button(title(ui, "Speed"), |ui| {
169 for (i, n) in SPEEDS.iter().enumerate() {
170 if check(ui, &format!("{n} / tick"), st.prefs.speed == i) {
171 acts.push(Action::SetSpeed(i));
172 }
173 }
174 });
175 });
176 ui.menu_button(title(ui, "Peripherals"), |ui| {
177 for (pane, name) in [
178 (Pane::Ports, "I/O ports"),
179 (Pane::Timers, "Timers"),
180 (Pane::Interrupts, "Interrupts"),
181 (Pane::Serial, "Serial (UART #1)"),
182 ] {
183 if item(ui, name, "") {
184 acts.push(Action::ShowPane(pane));
185 }
186 }
187 });
188 ui.menu_button(title(ui, "Help"), |ui| {
189 if item(ui, "Keyboard shortcuts", "F1") {
190 acts.push(Action::Modal(Modal::Shortcuts));
191 }
192 if item(ui, "About bench51", "") {
193 acts.push(Action::Modal(Modal::About));
194 }
195 });
196 ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
197 let p = palette(ui.ctx());
198 ui.label(title(ui, "bench51").color(p.text_disabled));
199 });
200 });
201 st.actions.extend(acts);
202}
203
204pub fn toolbar(ui: &mut Ui, st: &mut State) {
205 let p = palette(ui.ctx());
206 let has_doc = st.doc().is_some();
207 let running = st.running();
208 let debug = st.mode == Mode::Debug;
209 let mut acts = Vec::new();
210 ui.horizontal(|ui| {
211 ui.spacing_mut().item_spacing.x = 2.0;
212 let mut b = |ui: &mut Ui, icon, tip, enabled, on, act| {
213 if widgets::icon_button(ui, icon, tip, enabled, on).clicked() {
214 acts.push(act);
215 }
216 };
217 b(ui, Icon::New, "New file (Ctrl+N)", true, false, Action::New);
218 b(ui, Icon::Open, "Open (Ctrl+O)", true, false, Action::Open);
219 b(
220 ui,
221 Icon::Save,
222 "Save (Ctrl+S)",
223 has_doc,
224 false,
225 Action::Save,
226 );
227 ui.add_space(18.0);
228 b(
229 ui,
230 Icon::Build,
231 "Build target (F7)",
232 has_doc,
233 false,
234 Action::Build,
235 );
236 ui.add_space(18.0);
237 b(
238 ui,
239 Icon::Debug,
240 if debug {
241 "Stop debug session (Ctrl+F5)"
242 } else {
243 "Start debug session (Ctrl+F5)"
244 },
245 has_doc,
246 debug,
247 Action::ToggleDebug,
248 );
249 ui.add_space(18.0);
250 b(
251 ui,
252 Icon::Reset,
253 "Reset CPU (F6)",
254 has_doc,
255 false,
256 Action::Reset,
257 );
258 if running {
259 b(
260 ui,
261 Icon::Pause,
262 "Stop (F5)",
263 has_doc,
264 false,
265 Action::RunPause,
266 );
267 } else {
268 b(ui, Icon::Run, "Run (F5)", has_doc, false, Action::RunPause);
269 }
270 b(
271 ui,
272 Icon::StepInto,
273 "Step into (F11)",
274 has_doc,
275 false,
276 Action::StepInto,
277 );
278 b(
279 ui,
280 Icon::StepOver,
281 "Step over (F10)",
282 has_doc,
283 false,
284 Action::StepOver,
285 );
286 b(
287 ui,
288 Icon::StepOut,
289 "Step out (Ctrl+F11)",
290 has_doc,
291 false,
292 Action::StepOut,
293 );
294 b(
295 ui,
296 Icon::RunToCursor,
297 "Run to cursor line (Ctrl+F10)",
298 has_doc,
299 false,
300 Action::RunToCursor,
301 );
302 ui.add_space(18.0);
303 b(
304 ui,
305 Icon::Breakpoint,
306 "Toggle breakpoint (F9)",
307 has_doc,
308 false,
309 Action::ToggleBreakpoint,
310 );
311
312 ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
313 ui.spacing_mut().item_spacing.x = 10.0;
314 ui.add_space(6.0);
315 let speed = st.prefs.speed;
316 ui.label(
317 RichText::new(format!("{:>6} / TICK", SPEEDS[speed]))
318 .font(FontId::monospace(LABEL))
319 .color(p.text_secondary),
320 );
321 if let Some(i) = widgets::segment_bar(
322 ui,
323 136.0,
324 10.0,
325 SPEEDS.len(),
326 speed + 1,
327 p.text_display,
328 true,
329 ) {
330 acts.push(Action::SetSpeed(i));
331 }
332 ui.label(p.label("Speed"));
333 });
334 });
335 st.actions.extend(acts);
336}
337
338/// True for status messages that report a failure.
339fn is_error(msg: &str) -> bool {
340 let m = msg.to_ascii_lowercase();
341 [
342 "error(s)",
343 "cannot ",
344 "invalid ",
345 "failed",
346 "reserved opcode",
347 "out of range",
348 ]
349 .iter()
350 .any(|w| m.contains(w))
351}
352
353pub fn status_bar(ui: &mut Ui, st: &State) {
354 let p = palette(ui.ctx());
355 let text = |s: String| {
356 RichText::new(s.to_uppercase())
357 .font(FontId::monospace(LABEL))
358 .extra_letter_spacing(0.6)
359 };
360 ui.horizontal(|ui| {
361 ui.spacing_mut().item_spacing.x = 14.0;
362 let doc = st.doc();
363 let (state, live) = match doc {
364 None => ("no file", false),
365 Some(d) if d.session.running => ("running", true),
366 Some(d) if d.session.build.is_none() => ("not built", false),
367 Some(d) if d.stale() => ("source changed", false),
368 Some(_) if st.mode == Mode::Debug => ("stopped", false),
369 Some(_) => ("ready", false),
370 };
371 let mode = if st.mode == Mode::Debug {
372 "simulation"
373 } else {
374 "edit"
375 };
376 ui.label(text(mode.into()).color(p.text_disabled));
377 // Running: a dot that blinks in hard 2 Hz steps.
378 let (r, _) = ui.allocate_exact_size(egui::vec2(8.0, 8.0), egui::Sense::hover());
379 let on = !live || (ui.input(|i| i.time) * 4.0) as i64 % 2 == 0;
380 if live && on {
381 ui.painter().circle_filled(r.center(), 3.5, p.text_display);
382 } else {
383 ui.painter()
384 .circle_stroke(r.center(), 3.0, Stroke::new(1.0, p.text_disabled));
385 }
386 ui.label(text(format!("[{state}]")).color(if live {
387 p.text_display
388 } else {
389 p.text_secondary
390 }));
391 if let Some(d) = doc {
392 let msg = &d.session.status;
393 if !msg.is_empty() {
394 let color = if is_error(msg) {
395 ACCENT
396 } else {
397 p.text_primary
398 };
399 let shown = if is_error(msg) {
400 format!("[error: {msg}]")
401 } else {
402 msg.clone()
403 };
404 ui.label(text(shown).color(color));
405 }
406 }
407 ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
408 ui.label(text(format!("{} MHz", st.prefs.clock_mhz)).color(p.text_disabled));
409 if let Some(d) = doc {
410 if d.session.build.is_some() {
411 ui.label(text(format!("cyc {}", d.session.sim.cycles)).color(p.text_secondary));
412 ui.label(text(format!("t {}", d.session.elapsed())).color(p.text_secondary));
413 }
414 ui.label(
415 text(format!("l {} c {}", d.cursor.0 + 1, d.cursor.1 + 1))
416 .color(p.text_secondary),
417 );
418 }
419 });
420 });
421}