Krolyxon
Commit 43502f8

Branches

Tags

Refs →

bench51cratesbench51-guisrcapp.rs

709 lines · 24.3 KB · Rust

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