From acf42d56f2eaaf0c13e8baa0cb467926558ab3df Mon Sep 17 00:00:00 2001 From: zxq5 Date: Wed, 22 Jul 2026 00:10:42 +0100 Subject: [PATCH] added close-confirmation for editor. TODO: make this behaviour dependent on the current file being saved (buffer is modified etc. maybe use a hash for memory efficiency) --- .../src/ui/debugger/dialog/confirm_dialog.rs | 7 +- .../emulator_ui/src/ui/debugger/mod.rs | 163 +++++++++++------- .../src/ui/debugger/panels/editor.rs | 4 + .../emulator_ui/src/ui/debugger/panels/mod.rs | 62 +++++-- 4 files changed, 152 insertions(+), 84 deletions(-) diff --git a/dsa/emulator/emulator_ui/src/ui/debugger/dialog/confirm_dialog.rs b/dsa/emulator/emulator_ui/src/ui/debugger/dialog/confirm_dialog.rs index bb3cb05..9b29e26 100644 --- a/dsa/emulator/emulator_ui/src/ui/debugger/dialog/confirm_dialog.rs +++ b/dsa/emulator/emulator_ui/src/ui/debugger/dialog/confirm_dialog.rs @@ -1,4 +1,5 @@ use egui::{Modal, Id, Layout, Align, RichText}; +use crate::ui::debugger::ui_helpers::PaddedWidget; pub struct ConfirmDialog { open: bool, @@ -46,11 +47,13 @@ impl ConfirmDialog { ui.separator(); ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - if ui.button(RichText::new("Confirm").strong()).clicked() { + + if PaddedWidget::button("Confirm").ui(ui).clicked() { result = Some(true); } + ui.add_space(6.0); - if ui.button("Cancel").clicked() { + if PaddedWidget::button("Cancel").ui(ui).clicked() { result = Some(false); } }); diff --git a/dsa/emulator/emulator_ui/src/ui/debugger/mod.rs b/dsa/emulator/emulator_ui/src/ui/debugger/mod.rs index 9e59b00..d9483f5 100644 --- a/dsa/emulator/emulator_ui/src/ui/debugger/mod.rs +++ b/dsa/emulator/emulator_ui/src/ui/debugger/mod.rs @@ -1,8 +1,10 @@ +use std::fmt::Debug; use std::fs::File; use std::io::BufReader; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; +use egui::{CentralPanel, Panel, Ui}; use egui_tiles::TileId; use common::formats::binary_dse::DseExecutable; use emulator_core::memory::ram::MemoryBank; @@ -30,10 +32,23 @@ pub mod dialog; pub mod panels; pub mod ui_helpers; +trait FileFormat: Debug + Clone { + fn extension(&self) -> &'static str; +} + +impl FileFormat for DseExecutable { + fn extension(&self) -> &'static str { + "dse" + } +} + #[derive(Debug, Clone)] pub enum Action { + RequestCloseTab(TileId), + CloseTab(TileId), Quit, OpenFile(PathBuf), + // OpenCachedFile(Box), LoadBinary(PathBuf), SaveFile(PathBuf, TileId), RequestSaveAs(TileId), @@ -125,12 +140,25 @@ impl DebuggerUi { pub fn perform_action(&mut self, action: Action) { match action { + + Action::RequestCloseTab(tile_id) => { + self.confirm_dialog.open( + "Close Tab", + "Closing this tab may result in losing unsaved work", + Action::CloseTab(tile_id) + ); + } + + Action::CloseTab(tile_id) => { + self.central_tiles.close_tab(tile_id); + } + Action::Quit => std::process::exit(0), // Opens a file (usually constructed by the file dialogue) Action::OpenFile(path) => { match path.extension().map(|s| s.to_str()).flatten() { - Some("dsa") => { self.central_tiles.add_tab(Editor::new_open(&path)); }, + Some("dsa" | "dsc") => { self.central_tiles.add_tab(Editor::new_open(&path)); }, Some("dsb") => { self.central_tiles.add_tab(Disassembler::new_open(&path)); }, Some("md") => { self.central_tiles.add_tab(Documentation::open(&path)); }, Some(_) => todo!(), @@ -160,8 +188,6 @@ impl DebuggerUi { } Action::SaveFile(path, tile) => { - println!("ACTION: saving file {} from tile {tile:?}", path.display()); - if let Some(editor) = self.central_tiles.pane_mut::(tile) { if let Err(e) = std::fs::write(path, editor.get_buffer().as_bytes()) { self.perform_action(Action::ShowError(e.to_string())) @@ -174,6 +200,64 @@ impl DebuggerUi { } }; } + + fn toolbar(&mut self, ui: &mut Ui) { + egui::MenuBar::new().ui(ui, |ui| { + + let save_enabled = self.central_tiles.active_tile() + .and_then(|id| self.central_tiles.pane_mut::(id)) + .is_some(); + + ui.set_height(30.0); + ui.menu_button("File", |ui| { + + if ui.button("Open").clicked() { + self.file_dialog.open_pick(Action::OpenFile); + } + + if ui.button("Load Binary").clicked() { + self.file_dialog.open_pick(Action::LoadBinary); + } + + if ui.add_enabled(save_enabled, egui::Button::new("Save")).clicked() { + if let Some(active) = self.central_tiles.active_tile() { + if let Some(editor) = self.central_tiles.pane_mut::(active) { + match editor.filename().cloned() { + Some(path) => std::fs::write(path, editor.get_buffer().as_bytes()).unwrap(), + None => self.file_dialog.open_save(move |path| Action::SaveFile(path, active)), + } + } + } + } + + if ui.add_enabled(save_enabled, egui::Button::new("Save As")).clicked() { + if let Some(active) = self.central_tiles.active_tile() { + if let Some(editor) = self.central_tiles.pane_mut::(active) { + self.file_dialog.open_save(move |path| Action::SaveFile(path, active)) + } + } + } + + if ui.button("Exit").clicked() { + self.confirm_dialog.open("Exit","Exit Application\n(you will lose unsaved work)", Action::Quit) + } + }); + + ui.menu_button("Tools", |ui| { + for PanelFactory(name, constructor) in &self.tools { + if ui.button(name).clicked() { + self.central_tiles.add_dyn_tab(constructor()); + } + } + }); + + ui.menu_button("Help", |ui| { + if ui.button("About").clicked() { + self.central_tiles.add_tab(AboutScreen); + } + }); + }); + } } impl eframe::App for DebuggerUi { @@ -190,70 +274,23 @@ impl eframe::App for DebuggerUi { self.error_dialog.show(ui); - egui::Panel::top("toolbar").resizable(false).show(ui, |ui| { - egui::MenuBar::new().ui(ui, |ui| { - - let save_enabled = self.central_tiles.active_tile() - .and_then(|id| self.central_tiles.pane_mut::(id)) - .is_some(); - - ui.set_height(30.0); - ui.menu_button("File", |ui| { - - if ui.button("Open").clicked() { - self.file_dialog.open_pick(Action::OpenFile); - } - - if ui.button("Load Binary").clicked() { - self.file_dialog.open_pick(Action::LoadBinary); - } - - if ui.add_enabled(save_enabled, egui::Button::new("Save")).clicked() { - if let Some(active) = self.central_tiles.active_tile() { - if let Some(editor) = self.central_tiles.pane_mut::(active) { - match editor.filename().cloned() { - Some(path) => std::fs::write(path, editor.get_buffer().as_bytes()).unwrap(), - None => self.file_dialog.open_save(move |path| Action::SaveFile(path, active)), - } - } - } - } - - if ui.add_enabled(save_enabled, egui::Button::new("Save As")).clicked() { - if let Some(active) = self.central_tiles.active_tile() { - if let Some(editor) = self.central_tiles.pane_mut::(active) { - self.file_dialog.open_save(move |path| Action::SaveFile(path, active)) - } - } - } - - if ui.button("Exit").clicked() { - self.confirm_dialog.open("Exit","Exit Application\n(you will lose unsaved work)", Action::Quit) - } - }); - - ui.menu_button("Tools", |ui| { - for PanelFactory(name, constructor) in &self.tools { - if ui.button(name).clicked() { - self.central_tiles.add_dyn_tab(constructor()); - } - } - }); - - ui.menu_button("Help", |ui| { - if ui.button("About").clicked() { - self.central_tiles.add_tab(AboutScreen); - } - }); - }); + Panel::top("menu_bar").resizable(false).show(ui, |ui| { + self.toolbar(ui); }); - egui::Panel::top("info_panel").resizable(false).show(ui, |ui| { + + Panel::top("control_panel").resizable(false).show(ui, |ui| { self.controller.ui(ui); }); - egui::Panel::left("left_panel").resizable(false).show(ui, |ui| {}); - egui::Panel::right("right_panel").resizable(false).show(ui, |ui| {}); - egui::CentralPanel::default().show(ui, |ui| { + Panel::left("left_panel").resizable(false).show(ui, |ui| { + // TODO: File Explorer! + }); + + // Panel::right("right_panel").resizable(false).show(ui, |ui| { + // // TODO: not sure what to put here! + // }); + + CentralPanel::default().show(ui, |ui| { if let Some(action) = self.central_tiles.ui(ui) { self.perform_action(action); } diff --git a/dsa/emulator/emulator_ui/src/ui/debugger/panels/editor.rs b/dsa/emulator/emulator_ui/src/ui/debugger/panels/editor.rs index e682d0c..1376a0d 100644 --- a/dsa/emulator/emulator_ui/src/ui/debugger/panels/editor.rs +++ b/dsa/emulator/emulator_ui/src/ui/debugger/panels/editor.rs @@ -86,4 +86,8 @@ impl TilePane for Editor { } fn as_any_mut(&mut self) -> &mut dyn Any { self } + + fn close_requires_confirmation(&mut self, _tile_id: TileId) -> bool { + true + } } \ No newline at end of file diff --git a/dsa/emulator/emulator_ui/src/ui/debugger/panels/mod.rs b/dsa/emulator/emulator_ui/src/ui/debugger/panels/mod.rs index 24f87ea..23db5c8 100644 --- a/dsa/emulator/emulator_ui/src/ui/debugger/panels/mod.rs +++ b/dsa/emulator/emulator_ui/src/ui/debugger/panels/mod.rs @@ -6,19 +6,31 @@ use std::any::Any; use egui::{Id, Response, Stroke, StrokeKind, Ui, Widget}; use egui_tiles::{Behavior, Container, SimplificationOptions, TabState, Tile, TileId, Tiles, Tree, UiResponse}; use crate::ui::debugger::Action; +use crate::ui::debugger::Action::RequestCloseTab; /// Anything that can live inside a tab in the central tiled area. /// `as_any_mut` lets callers reach back into a specific pane's concrete /// type after it's been handed off to the tree (e.g. to write into the /// editor's buffer from an `Action` handler). pub trait TilePane: Any { + + /// the title that's shown on the tab in the UI. fn title(&self) -> egui::WidgetText; + + /// main UI function. returns an action in order to communicate with the parent UI fn ui(&mut self, ui: &mut egui::Ui, _tile_id: TileId) -> Option; + + /// required in order to be used with a pane fn as_any_mut(&mut self) -> &mut dyn Any; + /// the menu options exposed by the tile. these inform the main application menu based on the current focused tile fn menu(&mut self, _ui: &mut egui::Ui, _tile_id: TileId) -> Option { None } + + fn close_requires_confirmation(&mut self, _tile_id: TileId) -> bool { + false + } } type Pane = Box; @@ -29,24 +41,6 @@ struct TileBehavior { } impl Behavior for TileBehavior { - fn is_tab_closable(&self, _tiles: &Tiles, _tile_id: TileId) -> bool { - true - } - - fn on_tab_close(&mut self, tiles: &mut Tiles, tile_id: TileId) -> bool { - tiles.remove(tile_id); - true - } - - fn tab_bar_height(&self, _style: &egui::Style) -> f32 { - 36.0 // taller tab row = more vertical padding around the label - } - - fn tab_bg_color(&self, visuals: &egui::Visuals, _tiles: &egui_tiles::Tiles>, _tile_id: egui_tiles::TileId, tab_state: &egui_tiles::TabState) -> egui::Color32 { - visuals.widgets.inactive.bg_fill - } - - fn pane_ui( &mut self, ui: &mut egui::Ui, @@ -65,11 +59,34 @@ impl Behavior for TileBehavior { UiResponse::None } - fn tab_title_for_pane(&mut self, pane: &Pane) -> egui::WidgetText { pane.title() } + fn is_tab_closable(&self, _tiles: &Tiles, _tile_id: TileId) -> bool { + true + } + + fn on_tab_close(&mut self, tiles: &mut Tiles, tile_id: TileId) -> bool { + let Some(Tile::Pane(pane)) = tiles.get_mut(tile_id) else { + return false + }; + + if pane.close_requires_confirmation(tile_id) { + self.pending_action = Some(RequestCloseTab(tile_id)); + return false; + } + + tiles.remove(tile_id); + true + } + + + fn tab_bar_height(&self, _style: &egui::Style) -> f32 { + 36.0 // taller tab row = more vertical padding around the label + } + + fn simplification_options(&self) -> SimplificationOptions { SimplificationOptions { // Keep the tab bar around even with only one tab, and don't let @@ -78,6 +95,10 @@ impl Behavior for TileBehavior { ..Default::default() } } + + fn tab_bg_color(&self, visuals: &egui::Visuals, _tiles: &Tiles>, _tile_id: TileId, tab_state: &TabState) -> egui::Color32 { + visuals.widgets.inactive.bg_fill + } } /// A single top-level tab strip that tools can add tabs to at runtime. @@ -111,6 +132,9 @@ impl CentralTiles { self.add_dyn_tab(Box::new(pane)) } + pub fn close_tab(&mut self, tile_id: TileId) { + self.tree.tiles.remove(tile_id); + } /// Add a new tab and make it active. Returns the `TileId` so the /// caller can hold onto it for later access via `pane_mut`.