| 1 | //! Dialogs: unsaved-changes confirmation, target options, shortcuts, about. |
| 2 | |
| 3 | use eframe::egui::{ |
| 4 | self, Align, Color32, Context, CornerRadius, FontId, Key, Layout, RichText, Stroke, Ui, |
| 5 | }; |
| 6 | |
| 7 | use crate::state::{Action, Modal, State}; |
| 8 | use crate::theme::{BODY, HERO, palette}; |
| 9 | use crate::widgets::{self, PillKind}; |
| 10 | |
| 11 | /// Shown first while the editor uses Vim keys. |
| 12 | const 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 | |
| 35 | const 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 | |
| 73 | fn 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. |
| 83 | fn 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 | |
| 96 | pub 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 | } |