| 1 | //! Application state shared by every pane, and the actions panes ask for. |
| 2 | |
| 3 | use std::path::{Path, PathBuf}; |
| 4 | |
| 5 | use bench51_core::session::{MemSpace, Session}; |
| 6 | use serde::{Deserialize, Serialize}; |
| 7 | |
| 8 | use crate::layout::Pane; |
| 9 | use crate::vim::{self, Vim}; |
| 10 | |
| 11 | /// Keil's two window layouts: building and debugging. |
| 12 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 13 | pub enum Mode { |
| 14 | Edit, |
| 15 | Debug, |
| 16 | } |
| 17 | |
| 18 | /// How the code editor takes keys. |
| 19 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)] |
| 20 | pub enum Keymap { |
| 21 | Vim, |
| 22 | Standard, |
| 23 | } |
| 24 | |
| 25 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)] |
| 26 | pub enum ThemePref { |
| 27 | System, |
| 28 | Dark, |
| 29 | Light, |
| 30 | } |
| 31 | |
| 32 | /// Settings that survive restarts. |
| 33 | #[derive(Clone, Serialize, Deserialize)] |
| 34 | #[serde(default)] |
| 35 | pub struct Prefs { |
| 36 | pub theme: ThemePref, |
| 37 | pub keymap: Keymap, |
| 38 | pub recent: Vec<PathBuf>, |
| 39 | pub speed: usize, |
| 40 | pub clock_mhz: f64, |
| 41 | pub zoom: f32, |
| 42 | } |
| 43 | |
| 44 | impl Default for Prefs { |
| 45 | fn default() -> Self { |
| 46 | Self { |
| 47 | theme: ThemePref::System, |
| 48 | keymap: Keymap::Vim, |
| 49 | recent: Vec::new(), |
| 50 | speed: 5, |
| 51 | clock_mhz: 11.0592, |
| 52 | zoom: 1.0, |
| 53 | } |
| 54 | } |
| 55 | } |
| 56 | |
| 57 | impl Prefs { |
| 58 | pub fn add_recent(&mut self, path: &Path) { |
| 59 | let path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf()); |
| 60 | self.recent.retain(|p| *p != path); |
| 61 | self.recent.insert(0, path); |
| 62 | self.recent.truncate(10); |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | /// An open source file and the simulator running it. |
| 67 | pub struct Document { |
| 68 | /// Stable id for egui widget state. |
| 69 | pub id: u64, |
| 70 | pub path: Option<PathBuf>, |
| 71 | /// Name shown for documents without a path (e.g. a disassembled HEX file). |
| 72 | pub label: Option<String>, |
| 73 | pub text: String, |
| 74 | pub saved: String, |
| 75 | pub session: Session, |
| 76 | /// Cursor position as (line, column), updated by the editor. |
| 77 | pub cursor: (usize, usize), |
| 78 | /// Cursor position as a char index into `text`. |
| 79 | pub cursor_char: usize, |
| 80 | /// Move the cursor here and scroll to it on the next frame. |
| 81 | pub goto: Option<(usize, usize)>, |
| 82 | /// Select this char range and scroll to it on the next frame, keeping |
| 83 | /// keyboard focus where it is (the find bar). |
| 84 | pub select: Option<(usize, usize)>, |
| 85 | /// Byte range of the current find match. |
| 86 | pub find_hit: Option<(usize, usize)>, |
| 87 | /// Scroll the editor and disassembly to the PC on the next frame. |
| 88 | pub follow_pc: bool, |
| 89 | /// Vim undo history. |
| 90 | pub history: vim::History, |
| 91 | } |
| 92 | |
| 93 | impl Document { |
| 94 | pub fn new(id: u64, path: Option<PathBuf>, text: &str, clock_mhz: f64) -> Self { |
| 95 | let text = text.replace("\r\n", "\n"); |
| 96 | Self { |
| 97 | id, |
| 98 | path, |
| 99 | label: None, |
| 100 | saved: text.clone(), |
| 101 | text, |
| 102 | session: Session::new(clock_mhz), |
| 103 | cursor: (0, 0), |
| 104 | cursor_char: 0, |
| 105 | goto: None, |
| 106 | select: None, |
| 107 | find_hit: None, |
| 108 | follow_pc: false, |
| 109 | history: vim::History::default(), |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | pub fn name(&self) -> String { |
| 114 | if let Some(name) = self.path.as_ref().and_then(|p| p.file_name()) { |
| 115 | return name.to_string_lossy().into_owned(); |
| 116 | } |
| 117 | self.label.clone().unwrap_or_else(|| "untitled.asm".into()) |
| 118 | } |
| 119 | |
| 120 | /// File name without extension, Keil's "target" name. |
| 121 | pub fn target(&self) -> String { |
| 122 | let name = self.name(); |
| 123 | match name.rsplit_once('.') { |
| 124 | Some((stem, _)) if !stem.is_empty() => stem.to_string(), |
| 125 | _ => name, |
| 126 | } |
| 127 | } |
| 128 | |
| 129 | pub fn dirty(&self) -> bool { |
| 130 | self.text != self.saved |
| 131 | } |
| 132 | |
| 133 | /// The editor text differs from the program in the simulator. |
| 134 | pub fn stale(&self) -> bool { |
| 135 | self.session.is_stale(self.text.split('\n')) |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 140 | pub enum LogKind { |
| 141 | Info, |
| 142 | Ok, |
| 143 | Error, |
| 144 | } |
| 145 | |
| 146 | /// A line in the Build Output messages. |
| 147 | pub struct LogLine { |
| 148 | pub text: String, |
| 149 | pub kind: LogKind, |
| 150 | /// Clicking the line jumps here: (document id, line, column). |
| 151 | pub target: Option<(u64, usize, usize)>, |
| 152 | } |
| 153 | |
| 154 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 155 | pub enum BuildTab { |
| 156 | Messages, |
| 157 | Listing, |
| 158 | Hex, |
| 159 | } |
| 160 | |
| 161 | /// Everything the menus, toolbar, shortcuts and panes can ask for. They |
| 162 | /// queue actions; `GuiApp::apply` runs them once the frame's UI is done. |
| 163 | #[derive(Debug, Clone, PartialEq)] |
| 164 | pub enum Action { |
| 165 | New, |
| 166 | Open, |
| 167 | OpenPath(PathBuf), |
| 168 | Save, |
| 169 | SaveAs, |
| 170 | /// Save the active document to this path (`:w file`). |
| 171 | SaveTo(PathBuf), |
| 172 | /// Close a document, asking first if it has unsaved changes. |
| 173 | Close(usize), |
| 174 | /// Close without asking. |
| 175 | ForceClose(usize), |
| 176 | Activate(usize), |
| 177 | Quit, |
| 178 | /// Quit even with unsaved changes. |
| 179 | ForceQuit, |
| 180 | Build, |
| 181 | ToggleDebug, |
| 182 | RunPause, |
| 183 | StepInto, |
| 184 | StepOver, |
| 185 | StepOut, |
| 186 | RunToCursor, |
| 187 | Reset, |
| 188 | ToggleBreakpoint, |
| 189 | ClearBreakpoints, |
| 190 | SetSpeed(usize), |
| 191 | SetClock(f64), |
| 192 | Goto { |
| 193 | doc: u64, |
| 194 | line: usize, |
| 195 | col: usize, |
| 196 | }, |
| 197 | ShowPane(Pane), |
| 198 | ResetLayout, |
| 199 | SetTheme(ThemePref), |
| 200 | SetKeymap(Keymap), |
| 201 | Zoom(f32), |
| 202 | ZoomReset, |
| 203 | Find, |
| 204 | Modal(Modal), |
| 205 | } |
| 206 | |
| 207 | #[derive(Debug, Clone, PartialEq)] |
| 208 | pub enum Modal { |
| 209 | /// Unsaved changes in this document. |
| 210 | ConfirmClose(usize), |
| 211 | ConfirmQuit, |
| 212 | Shortcuts, |
| 213 | /// Options for Target: the oscillator frequency, being edited. |
| 214 | Options(String), |
| 215 | About, |
| 216 | } |
| 217 | |
| 218 | pub struct MemView { |
| 219 | pub space: MemSpace, |
| 220 | pub goto: String, |
| 221 | /// Scroll so this address is visible on the next frame. |
| 222 | pub scroll_to: Option<u32>, |
| 223 | /// Byte being edited inline: (address, text). |
| 224 | pub edit: Option<(u32, String)>, |
| 225 | } |
| 226 | |
| 227 | pub struct FindBar { |
| 228 | pub open: bool, |
| 229 | pub query: String, |
| 230 | /// Give the query field keyboard focus on the next frame. |
| 231 | pub focus: bool, |
| 232 | } |
| 233 | |
| 234 | pub struct State { |
| 235 | pub docs: Vec<Document>, |
| 236 | pub active: usize, |
| 237 | next_doc_id: u64, |
| 238 | pub mode: Mode, |
| 239 | pub prefs: Prefs, |
| 240 | pub log: Vec<LogLine>, |
| 241 | pub actions: Vec<Action>, |
| 242 | pub modal: Option<Modal>, |
| 243 | pub mem: MemView, |
| 244 | pub build_tab: BuildTab, |
| 245 | pub serial_input: String, |
| 246 | pub find: FindBar, |
| 247 | pub vim: Vim, |
| 248 | /// Lines visible in the editor last frame, for Vim's `H M L` and paging. |
| 249 | pub vim_view: vim::View, |
| 250 | /// Give the editor keyboard focus on the next frame. |
| 251 | pub focus_editor: bool, |
| 252 | } |
| 253 | |
| 254 | impl State { |
| 255 | pub fn new(prefs: Prefs) -> Self { |
| 256 | Self { |
| 257 | docs: Vec::new(), |
| 258 | active: 0, |
| 259 | next_doc_id: 1, |
| 260 | mode: Mode::Edit, |
| 261 | prefs, |
| 262 | log: Vec::new(), |
| 263 | actions: Vec::new(), |
| 264 | modal: None, |
| 265 | mem: MemView { |
| 266 | space: MemSpace::Iram, |
| 267 | goto: String::new(), |
| 268 | scroll_to: None, |
| 269 | edit: None, |
| 270 | }, |
| 271 | build_tab: BuildTab::Messages, |
| 272 | serial_input: String::new(), |
| 273 | find: FindBar { |
| 274 | open: false, |
| 275 | query: String::new(), |
| 276 | focus: false, |
| 277 | }, |
| 278 | vim: Vim::default(), |
| 279 | vim_view: vim::View::default(), |
| 280 | focus_editor: false, |
| 281 | } |
| 282 | } |
| 283 | |
| 284 | pub fn doc(&self) -> Option<&Document> { |
| 285 | self.docs.get(self.active) |
| 286 | } |
| 287 | |
| 288 | pub fn doc_mut(&mut self) -> Option<&mut Document> { |
| 289 | self.docs.get_mut(self.active) |
| 290 | } |
| 291 | |
| 292 | pub fn add_doc(&mut self, path: Option<PathBuf>, text: &str) -> &mut Document { |
| 293 | let id = self.next_doc_id; |
| 294 | self.next_doc_id += 1; |
| 295 | let mut doc = Document::new(id, path, text, self.prefs.clock_mhz); |
| 296 | doc.session.speed = self.prefs.speed; |
| 297 | self.docs.push(doc); |
| 298 | self.active = self.docs.len() - 1; |
| 299 | self.vim.abandon(); |
| 300 | self.focus_editor = true; |
| 301 | self.docs.last_mut().unwrap() |
| 302 | } |
| 303 | |
| 304 | pub fn log(&mut self, kind: LogKind, text: impl Into<String>) { |
| 305 | self.log.push(LogLine { |
| 306 | text: text.into(), |
| 307 | kind, |
| 308 | target: None, |
| 309 | }); |
| 310 | if self.log.len() > 2000 { |
| 311 | self.log.drain(..500); |
| 312 | } |
| 313 | } |
| 314 | |
| 315 | /// The active document's simulator is free running. |
| 316 | pub fn running(&self) -> bool { |
| 317 | self.doc().is_some_and(|d| d.session.running) |
| 318 | } |
| 319 | } |