Krolyxon
Commit 42b14d3

Branches

Tags

Refs →

bench51cratesbench51-guisrcmodals.rs

208 lines · 8.3 KB · Rust

1//! Dialogs: unsaved-changes confirmation, target options, shortcuts, about.
2
3use eframe::egui::{
4 self, Align, Color32, Context, CornerRadius, FontId, Key, Layout, RichText, Stroke, Ui,
5};
6
7use crate::state::{Action, Modal, State};
8use crate::theme::{BODY, HERO, palette};
9use crate::widgets::{self, PillKind};
10
11const SHORTCUTS: &[(&str, &[(&str, &str)])] = &[
12 (
13 "File",
14 &[
15 ("Ctrl+N", "new file"),
16 ("Ctrl+O", "open .asm / .a51, or .hex (disassembled)"),
17 ("Ctrl+S F2", "save"),
18 ("Ctrl+Shift+S", "save as"),
19 ("Ctrl+W", "close file"),
20 ("Ctrl+F", "find"),
21 ],
22 ),
23 (
24 "Build & debug",
25 &[
26 ("F7", "build target (writes .lst and .hex)"),
27 ("Ctrl+F5 F4", "start / stop debug session"),
28 ("F5", "run / stop"),
29 ("F11", "step into"),
30 ("F10", "step over"),
31 ("Ctrl+F11 F12", "step out"),
32 ("Ctrl+F10 F8", "run to cursor line"),
33 ("F9", "toggle breakpoint"),
34 ("Ctrl+Shift+F9", "kill all breakpoints"),
35 ("F6", "reset CPU"),
36 ("Esc", "stop running"),
37 ],
38 ),
39 (
40 "Windows",
41 &[
42 ("Ctrl+Tab", "next tab in the hovered group"),
43 ("Ctrl+ +/-/0", "zoom"),
44 ("double-click", "edit a memory byte"),
45 ],
46 ),
47];
48
49fn frame(ctx: &Context) -> egui::Frame {
50 let p = crate::theme::palette(ctx);
51 egui::Frame::new()
52 .fill(p.surface)
53 .stroke(Stroke::new(1.0, p.border_visible))
54 .corner_radius(CornerRadius::same(16))
55 .inner_margin(egui::Margin::same(24))
56}
57
58/// Title on the left, `[ X ]` close on the right.
59fn header(ui: &mut Ui, title: &str) -> bool {
60 let p = palette(ui.ctx());
61 let mut close = false;
62 ui.horizontal(|ui| {
63 ui.label(p.label(title));
64 ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
65 close = widgets::pill(ui, "[ X ]", PillKind::Ghost).clicked();
66 });
67 });
68 ui.add_space(12.0);
69 close
70}
71
72pub fn show(ctx: &Context, st: &mut State) {
73 let Some(modal) = st.modal.clone() else {
74 return;
75 };
76 let mut close = false;
77 let mut acts = Vec::new();
78 let resp = egui::Modal::new(egui::Id::unique("modal"))
79 .frame(frame(ctx))
80 .backdrop_color(Color32::from_black_alpha(204))
81 .show(ctx, |ui| {
82 let p = palette(ui.ctx());
83 ui.set_max_width(480.0);
84 match &modal {
85 Modal::ConfirmClose(i) => {
86 let name = st.docs.get(*i).map(|d| d.name()).unwrap_or_default();
87 close |= header(ui, "Unsaved changes");
88 ui.label(
89 RichText::new(format!("Save changes to {name} before closing?"))
90 .size(BODY)
91 .color(p.text_primary),
92 );
93 ui.add_space(24.0);
94 ui.horizontal(|ui| {
95 if widgets::pill(ui, "Save", PillKind::Primary).clicked() {
96 acts.push(Action::Activate(*i));
97 acts.push(Action::Save);
98 acts.push(Action::Close(*i));
99 close = true;
100 }
101 if widgets::pill(ui, "Discard", PillKind::Secondary).clicked() {
102 acts.push(Action::ForceClose(*i));
103 close = true;
104 }
105 close |= widgets::pill(ui, "Cancel", PillKind::Ghost).clicked();
106 });
107 }
108 Modal::ConfirmQuit => {
109 let n = st.docs.iter().filter(|d| d.dirty()).count();
110 close |= header(ui, "Unsaved changes");
111 ui.label(
112 RichText::new(format!(
113 "{n} file{} unsaved changes.",
114 if n == 1 { " has" } else { "s have" }
115 ))
116 .size(BODY)
117 .color(p.text_primary),
118 );
119 ui.add_space(24.0);
120 ui.horizontal(|ui| {
121 if widgets::pill(ui, "Quit without saving", PillKind::Secondary).clicked() {
122 acts.push(Action::ForceQuit);
123 close = true;
124 }
125 close |= widgets::pill(ui, "Cancel", PillKind::Ghost).clicked();
126 });
127 }
128 Modal::Options(text) => {
129 close |= header(ui, "Options for target");
130 let mut text = text.clone();
131 ui.label(p.label("Xtal (MHz)"));
132 ui.add_space(4.0);
133 let r = widgets::underline_input(ui, &mut text, "11.0592", 160.0);
134 if !r.has_focus() && !r.lost_focus() && text.is_empty() {
135 r.request_focus();
136 }
137 ui.add_space(4.0);
138 ui.label(
139 p.label("Used to turn machine cycles into elapsed time")
140 .color(p.text_disabled),
141 );
142 let valid = text.trim().parse::<f64>().ok().filter(|v| *v > 0.0);
143 ui.add_space(24.0);
144 let enter = r.lost_focus() && ui.input(|i| i.key_pressed(Key::Enter));
145 ui.horizontal(|ui| {
146 if (widgets::pill(ui, "OK", PillKind::Primary).clicked() || enter)
147 && let Some(v) = valid
148 {
149 acts.push(Action::SetClock(v));
150 close = true;
151 }
152 close |= widgets::pill(ui, "Cancel", PillKind::Ghost).clicked();
153 });
154 if !close {
155 st.modal = Some(Modal::Options(text));
156 }
157 }
158 Modal::Shortcuts => {
159 close |= header(ui, "Keyboard shortcuts");
160 egui::ScrollArea::vertical()
161 .max_height(420.0)
162 .show(ui, |ui| {
163 for (section, keys) in SHORTCUTS {
164 ui.label(p.label(section).color(p.text_disabled));
165 ui.add_space(4.0);
166 egui::Grid::new(section)
167 .num_columns(2)
168 .spacing([24.0, 4.0])
169 .show(ui, |ui| {
170 for (k, what) in *keys {
171 ui.label(
172 RichText::new(*k)
173 .font(FontId::monospace(12.0))
174 .color(p.text_display),
175 );
176 ui.label(RichText::new(*what).color(p.text_secondary));
177 ui.end_row();
178 }
179 });
180 ui.add_space(16.0);
181 }
182 });
183 }
184 Modal::About => {
185 close |= header(ui, "About");
186 ui.label(p.hero("BENCH51", HERO));
187 ui.add_space(4.0);
188 ui.label(p.label(format!("version {}", env!("CARGO_PKG_VERSION"))));
189 ui.add_space(16.0);
190 ui.label(
191 RichText::new(
192 "An Intel 8051 assembler, simulator and debugger laid out like \
193 Keil µVision. A learning tool, not a cycle-exact replacement \
194 for a vendor simulator.",
195 )
196 .color(p.text_primary),
197 );
198 }
199 }
200 });
201 if resp.should_close() {
202 close = true;
203 }
204 if close {
205 st.modal = None;
206 }
207 st.actions.extend(acts);
208}