Krolyxon
Tag v0.3.0

Branches

Tags

Refs →

bench51cratesbench51-guisrcstate.rs

319 lines · 8.1 KB · Rust

1//! Application state shared by every pane, and the actions panes ask for.
2
3use std::path::{Path, PathBuf};
4
5use bench51_core::session::{MemSpace, Session};
6use serde::{Deserialize, Serialize};
7
8use crate::layout::Pane;
9use crate::vim::{self, Vim};
10
11/// Keil's two window layouts: building and debugging.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub 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)]
20pub enum Keymap {
21 Vim,
22 Standard,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
26pub enum ThemePref {
27 System,
28 Dark,
29 Light,
30}
31
32/// Settings that survive restarts.
33#[derive(Clone, Serialize, Deserialize)]
34#[serde(default)]
35pub 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
44impl 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
57impl 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.
67pub 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
93impl 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)]
140pub enum LogKind {
141 Info,
142 Ok,
143 Error,
144}
145
146/// A line in the Build Output messages.
147pub 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)]
155pub 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)]
164pub 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)]
208pub 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
218pub 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
227pub 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
234pub 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
254impl 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}