Krolyxon
Branch master

Branches

Tags

Refs →

bench51cratesbench51-guisrcmodals.rs

235 lines · 9.6 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
11/// Shown first while the editor uses Vim keys.
12const VIM: (&str, &[(&str, &str)]) = (
13 "Editor (Vim)",
14 &[
15 ("i a I A o O", "insert · append · open a line"),
16 ("Esc", "back to Normal mode"),
17 ("h j k l w b e", "move; counts work: 5j, 3w"),
18 ("0 ^ $ gg G :42", "line start / end, top, bottom, line 42"),
19 ("f t % { }", "find char, matching bracket, paragraph"),
20 ("d c y > < + motion", "delete · change · yank · indent"),
21 ("dd yy cc ciw ci\"", "lines, words, inside quotes"),
22 ("x p P J ~ r", "delete char, put, join, case, replace"),
23 ("u Ctrl-R .", "undo · redo · repeat"),
24 ("v V", "visual · visual line"),
25 ("/ ? n N * #", "search; :noh clears the highlight"),
26 ("gd", "go to the label's definition"),
27 ("qa … q @a", "record and play a macro"),
28 (":w :q :wq :e :make", "write · close · open · build"),
29 ("gt gT", "next / previous file"),
30 ("Ctrl+V", "paste from the clipboard (y and d copy to it)"),
31 ("Settings → Keybindings", "switch to standard keys"),
32 ],
33);
34
35const SHORTCUTS: &[(&str, &[(&str, &str)])] = &[
36 (
37 "File",
38 &[
39 ("Ctrl+N", "new file"),
40 ("Ctrl+O", "open .asm / .a51, or .hex (disassembled)"),
41 ("Ctrl+S F2", "save"),
42 ("Ctrl+Shift+S", "save as"),
43 ("Ctrl+W", "close file"),
44 ("Ctrl+F", "find"),
45 ],
46 ),
47 (
48 "Build & debug",
49 &[
50 ("F7", "build target (writes .lst and .hex)"),
51 ("Ctrl+F5 F4", "start / stop debug session"),
52 ("F5", "run / stop"),
53 ("F11", "step into"),
54 ("F10", "step over"),
55 ("Ctrl+F11 F12", "step out"),
56 ("Ctrl+F10 F8", "run to cursor line"),
57 ("F9", "toggle breakpoint"),
58 ("Ctrl+Shift+F9", "kill all breakpoints"),
59 ("F6", "reset CPU"),
60 ("Esc", "stop running"),
61 ],
62 ),
63 (
64 "Windows",
65 &[
66 ("Ctrl+Tab", "next tab in the hovered group"),
67 ("Ctrl+ +/-/0", "zoom"),
68 ("double-click", "edit a memory byte"),
69 ],
70 ),
71];
72
73fn frame(ctx: &Context) -> egui::Frame {
74 let p = crate::theme::palette(ctx);
75 egui::Frame::new()
76 .fill(p.surface)
77 .stroke(Stroke::new(1.0, p.border_visible))
78 .corner_radius(CornerRadius::same(16))
79 .inner_margin(egui::Margin::same(24))
80}
81
82/// Title on the left, `[ X ]` close on the right.
83fn header(ui: &mut Ui, title: &str) -> bool {
84 let p = palette(ui.ctx());
85 let mut close = false;
86 ui.horizontal(|ui| {
87 ui.label(p.label(title));
88 ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
89 close = widgets::pill(ui, "[ X ]", PillKind::Ghost).clicked();
90 });
91 });
92 ui.add_space(12.0);
93 close
94}
95
96pub fn show(ctx: &Context, st: &mut State) {
97 let Some(modal) = st.modal.clone() else {
98 return;
99 };
100 let mut close = false;
101 let mut acts = Vec::new();
102 let resp = egui::Modal::new(egui::Id::unique("modal"))
103 .frame(frame(ctx))
104 .backdrop_color(Color32::from_black_alpha(204))
105 .show(ctx, |ui| {
106 let p = palette(ui.ctx());
107 ui.set_max_width(480.0);
108 match &modal {
109 Modal::ConfirmClose(i) => {
110 let name = st.docs.get(*i).map(|d| d.name()).unwrap_or_default();
111 close |= header(ui, "Unsaved changes");
112 ui.label(
113 RichText::new(format!("Save changes to {name} before closing?"))
114 .size(BODY)
115 .color(p.text_primary),
116 );
117 ui.add_space(24.0);
118 ui.horizontal(|ui| {
119 if widgets::pill(ui, "Save", PillKind::Primary).clicked() {
120 acts.push(Action::Activate(*i));
121 acts.push(Action::Save);
122 acts.push(Action::Close(*i));
123 close = true;
124 }
125 if widgets::pill(ui, "Discard", PillKind::Secondary).clicked() {
126 acts.push(Action::ForceClose(*i));
127 close = true;
128 }
129 close |= widgets::pill(ui, "Cancel", PillKind::Ghost).clicked();
130 });
131 }
132 Modal::ConfirmQuit => {
133 let n = st.docs.iter().filter(|d| d.dirty()).count();
134 close |= header(ui, "Unsaved changes");
135 ui.label(
136 RichText::new(format!(
137 "{n} file{} unsaved changes.",
138 if n == 1 { " has" } else { "s have" }
139 ))
140 .size(BODY)
141 .color(p.text_primary),
142 );
143 ui.add_space(24.0);
144 ui.horizontal(|ui| {
145 if widgets::pill(ui, "Quit without saving", PillKind::Secondary).clicked() {
146 acts.push(Action::ForceQuit);
147 close = true;
148 }
149 close |= widgets::pill(ui, "Cancel", PillKind::Ghost).clicked();
150 });
151 }
152 Modal::Options(text) => {
153 close |= header(ui, "Options for target");
154 let mut text = text.clone();
155 ui.label(p.label("Xtal (MHz)"));
156 ui.add_space(4.0);
157 let r = widgets::underline_input(ui, &mut text, "11.0592", 160.0);
158 if !r.has_focus() && !r.lost_focus() && text.is_empty() {
159 r.request_focus();
160 }
161 ui.add_space(4.0);
162 ui.label(
163 p.label("Used to turn machine cycles into elapsed time")
164 .color(p.text_disabled),
165 );
166 let valid = text.trim().parse::<f64>().ok().filter(|v| *v > 0.0);
167 ui.add_space(24.0);
168 let enter = r.lost_focus() && ui.input(|i| i.key_pressed(Key::Enter));
169 ui.horizontal(|ui| {
170 if (widgets::pill(ui, "OK", PillKind::Primary).clicked() || enter)
171 && let Some(v) = valid
172 {
173 acts.push(Action::SetClock(v));
174 close = true;
175 }
176 close |= widgets::pill(ui, "Cancel", PillKind::Ghost).clicked();
177 });
178 if !close {
179 st.modal = Some(Modal::Options(text));
180 }
181 }
182 Modal::Shortcuts => {
183 close |= header(ui, "Keyboard shortcuts");
184 egui::ScrollArea::vertical()
185 .max_height(420.0)
186 .show(ui, |ui| {
187 let vim_on = st.prefs.keymap == crate::state::Keymap::Vim;
188 let sections =
189 vim_on.then_some(&VIM).into_iter().chain(SHORTCUTS.iter());
190 for (section, keys) in sections {
191 ui.label(p.label(section).color(p.text_disabled));
192 ui.add_space(4.0);
193 egui::Grid::new(section)
194 .num_columns(2)
195 .spacing([24.0, 4.0])
196 .show(ui, |ui| {
197 for (k, what) in *keys {
198 ui.label(
199 RichText::new(*k)
200 .font(FontId::monospace(12.0))
201 .color(p.text_display),
202 );
203 ui.label(RichText::new(*what).color(p.text_secondary));
204 ui.end_row();
205 }
206 });
207 ui.add_space(16.0);
208 }
209 });
210 }
211 Modal::About => {
212 close |= header(ui, "About");
213 ui.label(p.hero("BENCH51", HERO));
214 ui.add_space(4.0);
215 ui.label(p.label(format!("version {}", env!("CARGO_PKG_VERSION"))));
216 ui.add_space(16.0);
217 ui.label(
218 RichText::new(
219 "An Intel 8051 assembler, simulator and debugger laid out like \
220 Keil µVision. A learning tool, not a cycle-exact replacement \
221 for a vendor simulator.",
222 )
223 .color(p.text_primary),
224 );
225 }
226 }
227 });
228 if resp.should_close() {
229 close = true;
230 }
231 if close {
232 st.modal = None;
233 }
234 st.actions.extend(acts);
235}