| 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 | const 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 | |
| 49 | fn 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. |
| 59 | fn 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 | |
| 72 | pub 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 | } |