Krolyxon
Commit 815d036

Branches

Tags

Refs →

bench51cratesbench51-guisrcstate.rs

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