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)

This commit is contained in:
2026-07-22 00:10:42 +01:00
parent adfba876d2
commit acf42d56f2
4 changed files with 152 additions and 84 deletions
@@ -1,4 +1,5 @@
use egui::{Modal, Id, Layout, Align, RichText};
use crate::ui::debugger::ui_helpers::PaddedWidget;
pub struct ConfirmDialog<T> {
open: bool,
@@ -46,11 +47,13 @@ impl<T> ConfirmDialog<T> {
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);
}
});
+100 -63
View File
@@ -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<dyn FileFormat>),
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::<Editor>(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::<Editor>(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::<Editor>(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::<Editor>(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::<Editor>(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::<Editor>(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::<Editor>(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);
}
@@ -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
}
}
@@ -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<Action>;
/// 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<Action> {
None
}
fn close_requires_confirmation(&mut self, _tile_id: TileId) -> bool {
false
}
}
type Pane = Box<dyn TilePane>;
@@ -29,24 +41,6 @@ struct TileBehavior {
}
impl Behavior<Pane> for TileBehavior {
fn is_tab_closable(&self, _tiles: &Tiles<Pane>, _tile_id: TileId) -> bool {
true
}
fn on_tab_close(&mut self, tiles: &mut Tiles<Pane>, 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<Box<dyn TilePane>>, _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<Pane> for TileBehavior {
UiResponse::None
}
fn tab_title_for_pane(&mut self, pane: &Pane) -> egui::WidgetText {
pane.title()
}
fn is_tab_closable(&self, _tiles: &Tiles<Pane>, _tile_id: TileId) -> bool {
true
}
fn on_tab_close(&mut self, tiles: &mut Tiles<Pane>, 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<Pane> for TileBehavior {
..Default::default()
}
}
fn tab_bg_color(&self, visuals: &egui::Visuals, _tiles: &Tiles<Box<dyn TilePane>>, _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`.