| 1 | //! The application: window chrome around a tree of docked panes, the run |
| 2 | //! loop, file handling, and every action the UI can ask for. |
| 3 | |
| 4 | use std::path::{Path, PathBuf}; |
| 5 | |
| 6 | use bench51_core::files::{self, Loaded}; |
| 7 | use eframe::egui::{ |
| 8 | self, CentralPanel, Context, Key, Modifiers, Panel, Shape, ThemePreference, Ui, ViewportCommand, |
| 9 | }; |
| 10 | use egui_tiles::Tree; |
| 11 | |
| 12 | use crate::layout::{self, Pane, PaneBehavior}; |
| 13 | use crate::state::{Action, BuildTab, LogKind, LogLine, Modal, Mode, Prefs, State, ThemePref}; |
| 14 | use crate::theme::{self, palette}; |
| 15 | use crate::{chrome, modals}; |
| 16 | |
| 17 | /// One simulation chunk, as in the TUI's frame loop. |
| 18 | const TICK: f64 = 1.0 / 30.0; |
| 19 | |
| 20 | const SOURCE_EXTENSIONS: [&str; 4] = ["asm", "a51", "s", "src"]; |
| 21 | const HEX_EXTENSIONS: [&str; 2] = ["hex", "ihx"]; |
| 22 | |
| 23 | const TEMPLATE: &str = "\ |
| 24 | ; New 8051 program. F7 builds, Ctrl+F5 starts debugging, F11 steps. |
| 25 | ORG 0000h |
| 26 | LJMP main |
| 27 | |
| 28 | ORG 0030h |
| 29 | main: MOV A,#01h ; walking LED pattern on P1 |
| 30 | loop: MOV P1,A |
| 31 | RL A |
| 32 | SJMP loop |
| 33 | |
| 34 | END |
| 35 | "; |
| 36 | |
| 37 | pub struct GuiApp { |
| 38 | st: State, |
| 39 | edit_tree: Tree<Pane>, |
| 40 | debug_tree: Tree<Pane>, |
| 41 | last_time: Option<f64>, |
| 42 | tick_debt: f64, |
| 43 | /// The user confirmed quitting with unsaved changes. |
| 44 | allow_close: bool, |
| 45 | title: String, |
| 46 | } |
| 47 | |
| 48 | fn theme_preference(t: ThemePref) -> ThemePreference { |
| 49 | match t { |
| 50 | ThemePref::System => ThemePreference::System, |
| 51 | ThemePref::Dark => ThemePreference::Dark, |
| 52 | ThemePref::Light => ThemePreference::Light, |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | fn load_tree( |
| 57 | cc: &eframe::CreationContext<'_>, |
| 58 | key: &str, |
| 59 | default: fn() -> Tree<Pane>, |
| 60 | ) -> Tree<Pane> { |
| 61 | let mut tree = cc |
| 62 | .storage |
| 63 | .and_then(|s| eframe::get_value::<Tree<Pane>>(s, key)) |
| 64 | .filter(|t| t.tiles.find_pane(&Pane::Editor).is_some()) |
| 65 | .unwrap_or_else(default); |
| 66 | // Panes added in newer versions are simply missing; nothing to fix up. |
| 67 | if tree.root.is_none() { |
| 68 | tree = default(); |
| 69 | } |
| 70 | tree |
| 71 | } |
| 72 | |
| 73 | impl GuiApp { |
| 74 | pub fn new( |
| 75 | cc: &eframe::CreationContext<'_>, |
| 76 | files: Vec<PathBuf>, |
| 77 | clock: Option<f64>, |
| 78 | theme: Option<ThemePref>, |
| 79 | ) -> Self { |
| 80 | let ctx = &cc.egui_ctx; |
| 81 | theme::install(ctx); |
| 82 | let mut prefs: Prefs = cc |
| 83 | .storage |
| 84 | .and_then(|s| eframe::get_value(s, "prefs")) |
| 85 | .unwrap_or_default(); |
| 86 | if let Some(c) = clock { |
| 87 | prefs.clock_mhz = c; |
| 88 | } |
| 89 | if let Some(t) = theme { |
| 90 | prefs.theme = t; |
| 91 | } |
| 92 | prefs.speed = prefs.speed.min(bench51_core::session::SPEEDS.len() - 1); |
| 93 | ctx.set_theme(theme_preference(prefs.theme)); |
| 94 | ctx.set_zoom_factor(prefs.zoom.clamp(0.5, 3.0)); |
| 95 | let mut app = Self { |
| 96 | st: State::new(prefs), |
| 97 | edit_tree: load_tree(cc, "edit_layout", layout::edit_tree), |
| 98 | debug_tree: load_tree(cc, "debug_layout", layout::debug_tree), |
| 99 | last_time: None, |
| 100 | tick_debt: 0.0, |
| 101 | allow_close: false, |
| 102 | title: String::new(), |
| 103 | }; |
| 104 | for f in files { |
| 105 | app.open_path(&f); |
| 106 | } |
| 107 | app |
| 108 | } |
| 109 | |
| 110 | fn tree(&mut self) -> &mut Tree<Pane> { |
| 111 | match self.st.mode { |
| 112 | Mode::Edit => &mut self.edit_tree, |
| 113 | Mode::Debug => &mut self.debug_tree, |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | // ---- Input ---- |
| 118 | |
| 119 | fn shortcuts(&mut self, ctx: &Context) { |
| 120 | if self.st.modal.is_some() { |
| 121 | return; |
| 122 | } |
| 123 | let cmd = Modifiers::COMMAND; |
| 124 | let cmd_shift = Modifiers::COMMAND | Modifiers::SHIFT; |
| 125 | let none = Modifiers::NONE; |
| 126 | let running = self.st.running(); |
| 127 | let active = self.st.active; |
| 128 | // Most specific first: `consume_key` ignores extra Shift. |
| 129 | let table: [(Modifiers, Key, Action); 25] = [ |
| 130 | (cmd_shift, Key::S, Action::SaveAs), |
| 131 | (cmd, Key::S, Action::Save), |
| 132 | (none, Key::F2, Action::Save), |
| 133 | (cmd, Key::N, Action::New), |
| 134 | (cmd, Key::O, Action::Open), |
| 135 | (cmd, Key::W, Action::Close(active)), |
| 136 | (cmd, Key::Q, Action::Quit), |
| 137 | (cmd, Key::F, Action::Find), |
| 138 | (none, Key::F1, Action::Modal(Modal::Shortcuts)), |
| 139 | (none, Key::F7, Action::Build), |
| 140 | (cmd, Key::F5, Action::ToggleDebug), |
| 141 | (none, Key::F4, Action::ToggleDebug), |
| 142 | (none, Key::F5, Action::RunPause), |
| 143 | (cmd, Key::F11, Action::StepOut), |
| 144 | (none, Key::F12, Action::StepOut), |
| 145 | (none, Key::F11, Action::StepInto), |
| 146 | (cmd, Key::F10, Action::RunToCursor), |
| 147 | (none, Key::F10, Action::StepOver), |
| 148 | (none, Key::F8, Action::RunToCursor), |
| 149 | (cmd_shift, Key::F9, Action::ClearBreakpoints), |
| 150 | (none, Key::F9, Action::ToggleBreakpoint), |
| 151 | (none, Key::F6, Action::Reset), |
| 152 | (cmd, Key::Plus, Action::Zoom(0.1)), |
| 153 | (cmd, Key::Minus, Action::Zoom(-0.1)), |
| 154 | (cmd, Key::Num0, Action::ZoomReset), |
| 155 | ]; |
| 156 | ctx.input_mut(|i| { |
| 157 | for (mods, key, action) in table { |
| 158 | if i.consume_key(mods, key) { |
| 159 | self.st.actions.push(action); |
| 160 | } |
| 161 | } |
| 162 | if running && i.consume_key(none, Key::Escape) { |
| 163 | self.st.actions.push(Action::RunPause); |
| 164 | } |
| 165 | }); |
| 166 | } |
| 167 | |
| 168 | fn handle_drops(&mut self, ctx: &Context) { |
| 169 | let dropped: Vec<PathBuf> = ctx.input(|i| { |
| 170 | i.raw |
| 171 | .dropped_files |
| 172 | .iter() |
| 173 | .map(|f| f.path().to_path_buf()) |
| 174 | .filter(|p| !p.as_os_str().is_empty()) |
| 175 | .collect() |
| 176 | }); |
| 177 | for path in dropped { |
| 178 | self.open_path(&path); |
| 179 | } |
| 180 | } |
| 181 | |
| 182 | fn handle_close(&mut self, ctx: &Context) { |
| 183 | if ctx.input(|i| i.viewport().close_requested()) |
| 184 | && !self.allow_close |
| 185 | && self.st.docs.iter().any(|d| d.dirty()) |
| 186 | { |
| 187 | ctx.send_viewport_cmd(ViewportCommand::CancelClose); |
| 188 | self.st.modal = Some(Modal::ConfirmQuit); |
| 189 | } |
| 190 | } |
| 191 | |
| 192 | /// Advances the running program in fixed 33 ms chunks of wall time. |
| 193 | fn pace(&mut self, ctx: &Context) { |
| 194 | let now = ctx.input(|i| i.time); |
| 195 | let dt = self.last_time.map_or(0.0, |t| now - t); |
| 196 | self.last_time = Some(now); |
| 197 | let speed = self.st.prefs.speed; |
| 198 | let Some(doc) = self.st.doc_mut() else { |
| 199 | return; |
| 200 | }; |
| 201 | doc.session.speed = speed; |
| 202 | if !doc.session.running { |
| 203 | self.tick_debt = 0.0; |
| 204 | return; |
| 205 | } |
| 206 | self.tick_debt = (self.tick_debt + dt).min(TICK * 3.0); |
| 207 | while self.tick_debt >= TICK && doc.session.running { |
| 208 | doc.session.tick(); |
| 209 | self.tick_debt -= TICK; |
| 210 | } |
| 211 | if !doc.session.running || speed <= 2 { |
| 212 | doc.follow_pc = true; |
| 213 | } |
| 214 | if doc.session.running { |
| 215 | ctx.request_repaint(); |
| 216 | } |
| 217 | } |
| 218 | |
| 219 | // ---- Files ---- |
| 220 | |
| 221 | fn open_path(&mut self, path: &Path) { |
| 222 | let canon = path.canonicalize().ok(); |
| 223 | if let Some(i) = self.st.docs.iter().position(|d| { |
| 224 | d.path.as_ref().and_then(|p| p.canonicalize().ok()) == canon && canon.is_some() |
| 225 | }) { |
| 226 | self.activate(i); |
| 227 | return; |
| 228 | } |
| 229 | // An untouched new document is replaced, as editors usually do. |
| 230 | if self.st.docs.len() == 1 { |
| 231 | let d = &self.st.docs[0]; |
| 232 | if d.path.is_none() && d.label.is_none() && !d.dirty() && d.text == TEMPLATE { |
| 233 | self.st.docs.clear(); |
| 234 | } |
| 235 | } |
| 236 | match files::load(path) { |
| 237 | Err(e) => { |
| 238 | self.st.log(LogKind::Error, e.clone()); |
| 239 | if let Some(d) = self.st.doc_mut() { |
| 240 | d.session.status = e; |
| 241 | } |
| 242 | } |
| 243 | Ok(Loaded::Source(text)) => { |
| 244 | self.pause_active(); |
| 245 | let doc = self.st.add_doc(Some(path.to_path_buf()), &text); |
| 246 | doc.session.status = format!("opened {}", path.display()); |
| 247 | self.st.prefs.add_recent(path); |
| 248 | self.st |
| 249 | .log(LogKind::Info, format!("opened {}", path.display())); |
| 250 | } |
| 251 | Ok(Loaded::Hex { source, bytes }) => { |
| 252 | self.pause_active(); |
| 253 | let name = path.file_name().map(|n| n.to_string_lossy().into_owned()); |
| 254 | let doc = self.st.add_doc(None, &source); |
| 255 | doc.label = name; |
| 256 | // Not saved anywhere yet: Save asks for an .asm name. |
| 257 | doc.saved.clear(); |
| 258 | self.st.prefs.add_recent(path); |
| 259 | self.build(); |
| 260 | if let Some(d) = self.st.doc_mut() { |
| 261 | d.session.status = format!( |
| 262 | "loaded {} ({bytes} bytes) and disassembled it", |
| 263 | path.display() |
| 264 | ); |
| 265 | } |
| 266 | } |
| 267 | } |
| 268 | } |
| 269 | |
| 270 | fn start_dir(&self) -> Option<PathBuf> { |
| 271 | self.st |
| 272 | .doc() |
| 273 | .and_then(|d| d.path.as_ref()) |
| 274 | .and_then(|p| p.parent().map(Path::to_path_buf)) |
| 275 | .or_else(|| { |
| 276 | self.st |
| 277 | .prefs |
| 278 | .recent |
| 279 | .first() |
| 280 | .and_then(|p| p.parent().map(Path::to_path_buf)) |
| 281 | }) |
| 282 | .or_else(|| std::env::current_dir().ok()) |
| 283 | } |
| 284 | |
| 285 | fn open_dialog(&mut self) { |
| 286 | let mut all: Vec<&str> = SOURCE_EXTENSIONS.to_vec(); |
| 287 | all.extend(HEX_EXTENSIONS); |
| 288 | let mut dialog = rfd::FileDialog::new() |
| 289 | .set_title("Open") |
| 290 | .add_filter("8051 source or Intel HEX", &all) |
| 291 | .add_filter("8051 assembly", &SOURCE_EXTENSIONS) |
| 292 | .add_filter("Intel HEX", &HEX_EXTENSIONS) |
| 293 | .add_filter("All files", &["*"]); |
| 294 | if let Some(dir) = self.start_dir() { |
| 295 | dialog = dialog.set_directory(dir); |
| 296 | } |
| 297 | for path in dialog.pick_files().unwrap_or_default() { |
| 298 | self.open_path(&path); |
| 299 | } |
| 300 | } |
| 301 | |
| 302 | fn save(&mut self, ask: bool) { |
| 303 | let dir = self.start_dir(); |
| 304 | let Some(doc) = self.st.doc_mut() else { |
| 305 | return; |
| 306 | }; |
| 307 | let path = match (&doc.path, ask) { |
| 308 | (Some(p), false) => Some(p.clone()), |
| 309 | _ => { |
| 310 | let name = doc.name(); |
| 311 | let stem = name.rsplit_once('.').map_or(name.as_str(), |(s, _)| s); |
| 312 | let mut dialog = rfd::FileDialog::new() |
| 313 | .set_title("Save as") |
| 314 | .add_filter("8051 assembly", &SOURCE_EXTENSIONS) |
| 315 | .set_file_name(format!("{stem}.asm")); |
| 316 | if let Some(dir) = dir { |
| 317 | dialog = dialog.set_directory(dir); |
| 318 | } |
| 319 | dialog.save_file() |
| 320 | } |
| 321 | }; |
| 322 | let Some(path) = path else { |
| 323 | return; |
| 324 | }; |
| 325 | let mut contents = doc.text.clone(); |
| 326 | if !contents.ends_with('\n') { |
| 327 | contents.push('\n'); |
| 328 | } |
| 329 | match std::fs::write(&path, contents) { |
| 330 | Ok(()) => { |
| 331 | doc.saved = doc.text.clone(); |
| 332 | doc.session.status = format!("saved {}", path.display()); |
| 333 | doc.path = Some(path.clone()); |
| 334 | doc.label = None; |
| 335 | self.st.prefs.add_recent(&path); |
| 336 | } |
| 337 | Err(e) => { |
| 338 | let msg = format!("cannot save {}: {e}", path.display()); |
| 339 | doc.session.status = msg.clone(); |
| 340 | self.st.log(LogKind::Error, msg); |
| 341 | } |
| 342 | } |
| 343 | } |
| 344 | |
| 345 | fn close(&mut self, i: usize) { |
| 346 | if i >= self.st.docs.len() { |
| 347 | return; |
| 348 | } |
| 349 | self.st.docs.remove(i); |
| 350 | if self.st.active >= i && self.st.active > 0 { |
| 351 | self.st.active -= 1; |
| 352 | } |
| 353 | if self.st.docs.is_empty() { |
| 354 | self.st.mode = Mode::Edit; |
| 355 | self.st.active = 0; |
| 356 | } |
| 357 | } |
| 358 | |
| 359 | fn pause_active(&mut self) { |
| 360 | if let Some(d) = self.st.doc_mut() |
| 361 | && d.session.running |
| 362 | { |
| 363 | d.session.pause(); |
| 364 | } |
| 365 | } |
| 366 | |
| 367 | fn activate(&mut self, i: usize) { |
| 368 | if i < self.st.docs.len() && i != self.st.active { |
| 369 | self.pause_active(); |
| 370 | self.st.active = i; |
| 371 | } |
| 372 | } |
| 373 | |
| 374 | // ---- Build & debug ---- |
| 375 | |
| 376 | /// Assembles the active document and reports Keil-style in the Build |
| 377 | /// Output. Returns whether it built without errors. |
| 378 | fn build(&mut self) -> bool { |
| 379 | let Some(doc) = self.st.docs.get_mut(self.st.active) else { |
| 380 | return false; |
| 381 | }; |
| 382 | let (name, target, id) = (doc.name(), doc.target(), doc.id); |
| 383 | let ok = doc.session.assemble(&doc.text, doc.path.as_deref()); |
| 384 | let mut lines = vec![ |
| 385 | (LogKind::Info, format!("Build target '{target}'"), None), |
| 386 | (LogKind::Info, format!("assembling {name}..."), None), |
| 387 | ]; |
| 388 | let out = &doc.session.last_asm.as_ref().unwrap().1; |
| 389 | if ok { |
| 390 | lines.push(( |
| 391 | LogKind::Info, |
| 392 | format!("Program Size: code={} bytes", out.code_size()), |
| 393 | None, |
| 394 | )); |
| 395 | if doc.path.is_some() { |
| 396 | lines.push(( |
| 397 | LogKind::Info, |
| 398 | format!("creating hex file from \"{target}\"..."), |
| 399 | None, |
| 400 | )); |
| 401 | } |
| 402 | if doc.session.status.contains("failed") { |
| 403 | lines.push((LogKind::Error, doc.session.status.clone(), None)); |
| 404 | } |
| 405 | lines.push((LogKind::Ok, format!("\"{target}\" - 0 Error(s)"), None)); |
| 406 | doc.follow_pc = true; |
| 407 | } else { |
| 408 | for d in &out.diagnostics { |
| 409 | lines.push(( |
| 410 | LogKind::Error, |
| 411 | format!("{name}({}:{}): error: {}", d.line + 1, d.col + 1, d.msg), |
| 412 | Some((id, d.line, d.col)), |
| 413 | )); |
| 414 | } |
| 415 | lines.push(( |
| 416 | LogKind::Error, |
| 417 | format!("\"{target}\" - {} Error(s)", out.diagnostics.len()), |
| 418 | None, |
| 419 | )); |
| 420 | let first = &out.diagnostics[0]; |
| 421 | doc.goto = Some((first.line, first.col)); |
| 422 | } |
| 423 | for (kind, text, target) in lines { |
| 424 | self.st.log.push(LogLine { text, kind, target }); |
| 425 | } |
| 426 | self.st.log(LogKind::Info, ""); |
| 427 | if !ok { |
| 428 | self.st.mode = Mode::Edit; |
| 429 | self.st.build_tab = BuildTab::Messages; |
| 430 | layout::show_pane(&mut self.edit_tree, Pane::Build); |
| 431 | } |
| 432 | ok |
| 433 | } |
| 434 | |
| 435 | fn ensure_built(&mut self) -> bool { |
| 436 | match self.st.doc() { |
| 437 | None => false, |
| 438 | Some(d) if d.stale() => self.build(), |
| 439 | Some(_) => true, |
| 440 | } |
| 441 | } |
| 442 | |
| 443 | /// Runs a debugger command on the active document, building first and |
| 444 | /// entering the debug layout, as Keil does. |
| 445 | fn debug_cmd(&mut self, f: impl FnOnce(&mut bench51_core::session::Session, (usize, usize))) { |
| 446 | if !self.ensure_built() { |
| 447 | return; |
| 448 | } |
| 449 | self.st.mode = Mode::Debug; |
| 450 | if let Some(doc) = self.st.doc_mut() { |
| 451 | f(&mut doc.session, doc.cursor); |
| 452 | doc.follow_pc = true; |
| 453 | } |
| 454 | } |
| 455 | |
| 456 | fn apply(&mut self, ctx: &Context, action: Action) { |
| 457 | match action { |
| 458 | Action::New => { |
| 459 | self.pause_active(); |
| 460 | self.st.add_doc(None, TEMPLATE); |
| 461 | } |
| 462 | Action::Open => self.open_dialog(), |
| 463 | Action::OpenPath(p) => self.open_path(&p), |
| 464 | Action::Save => self.save(false), |
| 465 | Action::SaveAs => self.save(true), |
| 466 | Action::Close(i) => { |
| 467 | if self.st.docs.get(i).is_some_and(|d| d.dirty()) { |
| 468 | self.st.modal = Some(Modal::ConfirmClose(i)); |
| 469 | } else { |
| 470 | self.close(i); |
| 471 | } |
| 472 | } |
| 473 | Action::ForceClose(i) => self.close(i), |
| 474 | Action::Activate(i) => self.activate(i), |
| 475 | Action::Quit => ctx.send_viewport_cmd(ViewportCommand::Close), |
| 476 | Action::ForceQuit => { |
| 477 | self.allow_close = true; |
| 478 | ctx.send_viewport_cmd(ViewportCommand::Close); |
| 479 | } |
| 480 | Action::Build => { |
| 481 | if self.build() && self.st.mode == Mode::Edit { |
| 482 | self.st.build_tab = BuildTab::Messages; |
| 483 | } |
| 484 | } |
| 485 | Action::ToggleDebug => match self.st.mode { |
| 486 | Mode::Edit => { |
| 487 | if self.ensure_built() { |
| 488 | self.st.mode = Mode::Debug; |
| 489 | if let Some(d) = self.st.doc_mut() { |
| 490 | d.follow_pc = true; |
| 491 | } |
| 492 | } |
| 493 | } |
| 494 | Mode::Debug => { |
| 495 | self.pause_active(); |
| 496 | self.st.mode = Mode::Edit; |
| 497 | } |
| 498 | }, |
| 499 | Action::RunPause => { |
| 500 | if self.st.running() { |
| 501 | self.pause_active(); |
| 502 | if let Some(d) = self.st.doc_mut() { |
| 503 | d.follow_pc = true; |
| 504 | } |
| 505 | } else { |
| 506 | self.debug_cmd(|s, _| s.start()); |
| 507 | } |
| 508 | } |
| 509 | Action::StepInto => self.debug_cmd(|s, _| s.step_into()), |
| 510 | Action::StepOver => self.debug_cmd(|s, _| s.step_over()), |
| 511 | Action::StepOut => self.debug_cmd(|s, _| s.step_out()), |
| 512 | Action::RunToCursor => self.debug_cmd(|s, (line, _)| { |
| 513 | s.run_to_line(line); |
| 514 | }), |
| 515 | Action::Reset => { |
| 516 | if let Some(d) = self.st.doc_mut() { |
| 517 | d.session.reset(); |
| 518 | d.follow_pc = true; |
| 519 | } |
| 520 | } |
| 521 | Action::ToggleBreakpoint => { |
| 522 | if let Some(d) = self.st.doc_mut() { |
| 523 | d.session.toggle_breakpoint(d.cursor.0); |
| 524 | } |
| 525 | } |
| 526 | Action::ClearBreakpoints => { |
| 527 | if let Some(d) = self.st.doc_mut() { |
| 528 | d.session.breakpoints.clear(); |
| 529 | d.session.status = "all breakpoints removed".into(); |
| 530 | } |
| 531 | } |
| 532 | Action::SetSpeed(i) => self.st.prefs.speed = i, |
| 533 | Action::SetClock(mhz) => { |
| 534 | self.st.prefs.clock_mhz = mhz; |
| 535 | for d in &mut self.st.docs { |
| 536 | d.session.clock_mhz = mhz; |
| 537 | } |
| 538 | } |
| 539 | Action::Goto { doc, line, col } => { |
| 540 | if let Some(i) = self.st.docs.iter().position(|d| d.id == doc) { |
| 541 | self.activate(i); |
| 542 | self.st.docs[i].goto = Some((line, col)); |
| 543 | layout::show_pane(self.tree(), Pane::Editor); |
| 544 | } |
| 545 | } |
| 546 | Action::ShowPane(p) => layout::show_pane(self.tree(), p), |
| 547 | Action::ResetLayout => { |
| 548 | *self.tree() = match self.st.mode { |
| 549 | Mode::Edit => layout::edit_tree(), |
| 550 | Mode::Debug => layout::debug_tree(), |
| 551 | } |
| 552 | } |
| 553 | Action::SetTheme(t) => { |
| 554 | self.st.prefs.theme = t; |
| 555 | ctx.set_theme(theme_preference(t)); |
| 556 | } |
| 557 | Action::Zoom(d) => ctx.set_zoom_factor((ctx.zoom_factor() + d).clamp(0.5, 3.0)), |
| 558 | Action::ZoomReset => ctx.set_zoom_factor(1.0), |
| 559 | Action::Find => { |
| 560 | if self.st.doc().is_some() { |
| 561 | self.st.find.open = true; |
| 562 | self.st.find.focus = true; |
| 563 | } |
| 564 | } |
| 565 | Action::Modal(m) => self.st.modal = Some(m), |
| 566 | } |
| 567 | } |
| 568 | |
| 569 | fn update_title(&mut self, ctx: &Context) { |
| 570 | let title = match self.st.doc() { |
| 571 | Some(d) => format!( |
| 572 | "{}{} — bench51", |
| 573 | d.name(), |
| 574 | if d.dirty() { " •" } else { "" } |
| 575 | ), |
| 576 | None => "bench51".to_string(), |
| 577 | }; |
| 578 | if title != self.title { |
| 579 | ctx.send_viewport_cmd(ViewportCommand::Title(title.clone())); |
| 580 | self.title = title; |
| 581 | } |
| 582 | } |
| 583 | } |
| 584 | |
| 585 | impl eframe::App for GuiApp { |
| 586 | fn ui(&mut self, ui: &mut Ui, _frame: &mut eframe::Frame) { |
| 587 | let ctx = ui.ctx().clone(); |
| 588 | let p = palette(&ctx); |
| 589 | self.shortcuts(&ctx); |
| 590 | self.handle_drops(&ctx); |
| 591 | self.handle_close(&ctx); |
| 592 | self.pace(&ctx); |
| 593 | self.st.prefs.zoom = ctx.zoom_factor(); |
| 594 | |
| 595 | let bar = |top: i8, bottom: i8| { |
| 596 | egui::Frame::NONE.fill(p.black).inner_margin(egui::Margin { |
| 597 | left: 10, |
| 598 | right: 10, |
| 599 | top, |
| 600 | bottom, |
| 601 | }) |
| 602 | }; |
| 603 | Panel::top("menu") |
| 604 | .frame(bar(4, 0)) |
| 605 | .show_separator_line(false) |
| 606 | .show(ui, |ui| chrome::menu_bar(ui, &mut self.st)); |
| 607 | Panel::top("toolbar") |
| 608 | .frame(bar(4, 6)) |
| 609 | .show_separator_line(false) |
| 610 | .show(ui, |ui| chrome::toolbar(ui, &mut self.st)); |
| 611 | Panel::bottom("status") |
| 612 | .frame(bar(6, 6)) |
| 613 | .show_separator_line(false) |
| 614 | .show(ui, |ui| chrome::status_bar(ui, &self.st)); |
| 615 | |
| 616 | CentralPanel::default() |
| 617 | .frame( |
| 618 | egui::Frame::NONE |
| 619 | .fill(p.black) |
| 620 | .inner_margin(egui::Margin::symmetric(6, 0)), |
| 621 | ) |
| 622 | .show(ui, |ui| { |
| 623 | let tree = match self.st.mode { |
| 624 | Mode::Edit => &mut self.edit_tree, |
| 625 | Mode::Debug => &mut self.debug_tree, |
| 626 | }; |
| 627 | let cards = ui.painter().add(Shape::Noop); |
| 628 | let editor_tile = tree.tiles.find_pane(&Pane::Editor); |
| 629 | let mut behavior = PaneBehavior { |
| 630 | st: &mut self.st, |
| 631 | editor_tile, |
| 632 | }; |
| 633 | tree.ui(&mut behavior, ui); |
| 634 | ui.painter() |
| 635 | .set(cards, Shape::Vec(layout::card_shapes(tree, p.surface))); |
| 636 | }); |
| 637 | |
| 638 | modals::show(&ctx, &mut self.st); |
| 639 | for action in std::mem::take(&mut self.st.actions) { |
| 640 | self.apply(&ctx, action); |
| 641 | } |
| 642 | self.update_title(&ctx); |
| 643 | } |
| 644 | |
| 645 | fn save(&mut self, storage: &mut dyn eframe::Storage) { |
| 646 | eframe::set_value(storage, "prefs", &self.st.prefs); |
| 647 | eframe::set_value(storage, "edit_layout", &self.edit_tree); |
| 648 | eframe::set_value(storage, "debug_layout", &self.debug_tree); |
| 649 | } |
| 650 | |
| 651 | /// Layouts, theme and zoom are saved above. egui's own memory would |
| 652 | /// restore scroll positions keyed by document number onto whatever file |
| 653 | /// gets that number next session. |
| 654 | fn persist_egui_memory(&self) -> bool { |
| 655 | false |
| 656 | } |
| 657 | |
| 658 | fn clear_color(&self, visuals: &egui::Visuals) -> [f32; 4] { |
| 659 | visuals.panel_fill.to_normalized_gamma_f32() |
| 660 | } |
| 661 | } |