refactor: fully moved to tile-based UI layout and removed most remnants of old layout (aside from editor which needs to be implemented properly before it's fully replaced.

This commit is contained in:
2026-07-26 19:55:06 +01:00
parent d73ec47410
commit 708743ef64
30 changed files with 1421 additions and 1362 deletions
+1 -1
View File
@@ -3,4 +3,4 @@
#![feature(str_as_str)]
pub mod ui;
pub mod args;
pub mod args;
+34 -45
View File
@@ -1,61 +1,50 @@
use clap::Parser;
use common::prelude::Instruction;
use std::thread;
use dsa::args::DsaArgs;
use dsa::ui::run_app;
use emulator_core::{
memory::ram::Page,
processor::processor::Emulator
};
const STACK_SIZE: usize = 1024 * 1024 * 16;
use emulator_core::{DsaImplementation, EmulatorController, processor::Emulator};
fn main() {
let args = DsaArgs::parse();
let mut emulator = Emulator::new().apply_memory_map(args.get_memory_map());
let mut emulator = Emulator::new();
emulator.apply_memory_map(args.get_memory_map());
let mem_handle = emulator.memory_mut().clone();
let state = emulator.state_handle();
if let Some(bin) = args.get_binary() {
for (i, ch) in bin.chunks(4).enumerate() {
let instruction = Instruction(u32::from_le_bytes(ch.try_into().unwrap()));
let hex = ch
.iter()
.map(|b| format!("{:02X}", b))
.collect::<Vec<_>>()
.join(" ");
let chars = ch
.iter()
.map(|b| {
if b.is_ascii_graphic() {
*b as char
} else {
'.'
}
})
.collect::<String>();
println!("{:04X}: {:<12} {:4} {:?}", i * 4, hex, chars, instruction);
}
let program: Vec<Page> = Page::paginate(&bin).collect();
for (i, page) in program.iter().enumerate() {
emulator.memory_mut().write_page(i as u32 * 4096, &page);
}
}
let _runner = thread::Builder::new()
.stack_size(STACK_SIZE)
.spawn(move || emulator.run().unwrap())
.unwrap();
let controller = EmulatorController::new(Box::new(emulator));
//
// if let Some(bin) = args.get_binary() {
// for (i, ch) in bin.chunks(4).enumerate() {
// let instruction = Instruction(u32::from_le_bytes(ch.try_into().unwrap()));
// let hex = ch
// .iter()
// .map(|b| format!("{:02X}", b))
// .collect::<Vec<_>>()
// .join(" ");
// let chars = ch
// .iter()
// .map(|b| {
// if b.is_ascii_graphic() {
// *b as char
// } else {
// '.'
// }
// })
// .collect::<String>();
// println!("{:04X}: {:<12} {:4} {:?}", i * 4, hex, chars, instruction);
// }
//
// let program: Vec<Page> = Page::paginate(&bin).collect();
//
// for (i, page) in program.iter().enumerate() {
// emulator.memory_mut().write_page(i as u32 * 4096, &page);
// }
// }
if args.headless() {
run_server(&args);
} else {
run_app(state, mem_handle).unwrap()
run_app(controller).unwrap()
}
}
@@ -1,219 +0,0 @@
use std::any::Any;
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
use egui::{ScrollArea, Ui, WidgetText};
use egui_extras::{Size, StripBuilder};
use egui_file::FileDialog;
use egui_tiles::TileId;
use common::formats::binary_dse::DseExecutable;
use disassembler::{disassemble, Disassembly};
use crate::ui::components::Component;
use crate::ui::debugger::Action;
use crate::ui::debugger::panels::TilePane;
pub struct Disassembler {
visible: bool,
dialog: Option<FileDialog>,
error: Option<String>,
program: Option<DseExecutable>,
disassembled: Option<Disassembly>,
}
impl TilePane for Disassembler {
fn title(&self) -> WidgetText {
"Disassembler".into()
}
fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option<Action> {
Component::ui(self, ui);
None
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl Disassembler {
pub(crate) fn new() -> Self {
Self {
visible: true,
dialog: None,
error: None,
program: None,
disassembled: None,
}
}
pub fn new_open(path: &Path) -> Self {
let reader = File::open(path).unwrap();
let bin = DseExecutable::read(&mut BufReader::new(reader)).unwrap();
Self {
visible: true,
dialog: None,
error: None,
program: Some(bin),
disassembled: None,
}
}
fn open(&mut self) {
let Ok(cwd) = std::env::current_dir() else {
self.error = Some("Couldn't get CWD to open file!".to_string());
return;
};
if self.dialog.is_none() {
let mut dialog = FileDialog::open_file();
dialog.set_path(cwd);
dialog.open();
self.dialog = Some(dialog);
}
}
fn handle_file_dialog(&mut self, ui: &mut Ui) {
let Some(dialog) = &mut self.dialog else {
return;
};
if !dialog.show(ui).selected() { return };
let Some(file) = dialog.path() else {
return
};
if !file.extension().is_some_and(|ext| ext == "dsb") {
return
}
let reader = match File::open(file) {
Ok(f) => f,
Err(e) => {
self.error = Some(e.to_string());
return;
}
};
match DseExecutable::read(&mut BufReader::new(reader)) {
Ok(bin) => self.program = Some(bin),
Err(e) => {
self.error = Some(e.to_string());
return;
}
};
self.dialog = None;
}
}
impl Component for Disassembler {
fn title(&self) -> &'static str {
"Disassembler"
}
fn visible(&mut self) -> &mut bool {
&mut self.visible
}
fn ui(&mut self, ui: &mut egui::Ui) {
self.handle_file_dialog(ui);
ui.horizontal(|ui| {
ui.spacing_mut().button_padding = egui::vec2(8.0, 4.0);
ui.spacing_mut().item_spacing.x = 6.0;
// Opens a file
// ui.add_enabled(false, egui::Button::new("Open"));
if ui.button("Open").clicked() {
self.open();
}
});
if let Some(binary) = &self.program {
if let Some(output) = &self.disassembled {
StripBuilder::new(ui)
.size(Size::exact(350.0))
.size(Size::remainder().at_most(500.0))
.horizontal(|mut strip| {
strip.cell(|ui| {
ScrollArea::vertical()
.id_salt("symbols")
.show(ui, |ui| {
egui::Grid::new("symbols_grid")
.striped(true)
.spacing([12.0, 2.0]) // horizontal, vertical spacing
.show(ui, |ui| {
// Header
ui.strong("Addr");
ui.strong("Name");
ui.strong("Section");
ui.strong("Bind");
ui.end_row();
// Rows
for symbol in &output.symbols {
ui.monospace(format!("{:#06x}", symbol.address));
ui.monospace(&symbol.name);
ui.label(format!("{:?}", symbol.section));
ui.label(format!("{:?}", symbol.binding));
ui.end_row();
}
});
});
});
strip.cell(|ui| {
ScrollArea::vertical()
.id_salt("asm")
.show(ui, |ui| {
egui::Grid::new("asm_grid")
.striped(true)
.spacing([12.0, 2.0])
.show(ui, |ui| {
for (addr, line) in output.text_lines.iter().enumerate() {
ui.label(
egui::RichText::new(format!("{:04X}", addr))
.monospace()
.weak(),
);
ui.monospace(line);
ui.end_row();
}
});
});
});
});
// ui.horizontal(|ui| {
//
// egui::ScrollArea::vertical()
// .id_salt("symbols_scroll")
// .show(ui, |ui| {
//
// });
//
// ui.separator();
//
// egui::ScrollArea::vertical()
// .id_salt("asm_scroll")
// .show(ui, |ui| {
//
// });
// });
} else {
self.disassembled = Some(disassemble(&binary))
}
}
}
}
@@ -1,79 +1,58 @@
use crate::ui::debugger::ui_helpers::{ PaddedWidget, StatusIndicator};
use std::sync::{Arc, atomic::Ordering};
use std::thread;
use std::time::Duration;
use crate::ui::ui_helpers::StatusIndicator;
use eframe::emath::Vec2;
use eframe::epaint::Color32;
use egui::Widget;
use super::Component;
use common::prelude::Register;
use emulator_core::{
memory::ram::MemoryBank,
processor::state::SharedState
};
use emulator_core::processor::state::RunningState;
use emulator_core::EmulatorHandle;
use emulator_core::processor::RunningState;
use crate::ui::ui_helpers::PaddedWidget;
pub struct Controller {
state: Arc<SharedState>,
mem: MemoryBank,
emulator: EmulatorHandle,
visible: bool,
}
impl Controller {
pub fn new(state: Arc<SharedState>, mem: MemoryBank) -> Self {
pub fn new(state: EmulatorHandle) -> Self {
Self {
state,
mem,
emulator: state,
visible: false,
}
}
}
impl Component for Controller {
fn title(&self) -> &str {
"Control Panel"
}
pub fn ui(&mut self, ui: &mut egui::Ui) {
fn visible(&mut self) -> &mut bool {
&mut self.visible
}
fn ui(&mut self, ui: &mut egui::Ui) {
self.state.update_req.store(true, Ordering::Relaxed);
thread::sleep(Duration::from_millis(5));
let state = self.state.proc.load();
let paused = self.state.paused.load(Ordering::Relaxed);
self.emulator.request_update();
egui::Frame::new()
.inner_margin(10.0)
.show(ui, |ui| {
ui.horizontal(|ui| {
let status = self.emulator.status();
let padding = Vec2::new(14.0, 8.0);
if match state.running {
RunningState::Paused | RunningState::Halted | RunningState::Breakpoint(_)
=> PaddedWidget::button("▶ Run").color(Color32::GREEN).ui(ui),
RunningState::Running
=> PaddedWidget::button("⏸ Pause").color(Color32::YELLOW).ui(ui),
match status {
RunningState::Paused | RunningState::Halted | RunningState::Breakpoint(_) => {
if PaddedWidget::button("▶ Run").color(Color32::GREEN).ui(ui).clicked() {
self.emulator.resume()
}
},
RunningState::Running => {
if PaddedWidget::button("⏸ Pause").color(Color32::YELLOW).ui(ui).clicked() {
self.emulator.pause()
}
}
RunningState::Shutdown => {
ui.add_enabled_ui(false, |ui| {
PaddedWidget::button("▶ Run").color(Color32::GREEN).ui(ui)
}).response
},
}.clicked()
{
self.state.paused.store(!paused, Ordering::Relaxed);
PaddedWidget::button("▶ Run").color(Color32::GREEN).ui(ui);
});
}
}
if PaddedWidget::button("⟳ Reset All").color(Color32::YELLOW).ui(ui).clicked() {
self.state.reseting.store(true, Ordering::Relaxed);
self.mem.clear();
self.state.reseting.store(false, Ordering::Relaxed);
self.emulator.reset();
}
// Step
@@ -100,16 +79,17 @@ impl Component for Controller {
// }
// }
state.running.clone().ui(ui, padding);
status.clone().ui(ui, padding);
ui.separator();
let pcx = state.registers[Register::Pcx as usize];
let clock = state.clock;
let time = state.time.as_micros();
let pcx = self.emulator.get_register(Register::Pcx);
let clock = self.emulator.clock();
let time = self.emulator.time().as_micros();
let mips = if time != 0 {
state.clock as u128 / time
clock as u128 / time
} else {
0
};
@@ -1,299 +0,0 @@
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;
use emulator_core::processor::state::SharedState;
use crate::ui::components::controller::Controller;
use crate::ui::components::disassembler::Disassembler;
use crate::ui::components::memory::MemoryInspector;
use crate::ui::DsaUi;
use crate::ui::components::Component;
use dialog::confirm_dialog::ConfirmDialog;
use panels::documentation::Documentation;
use panels::editor::Editor;
use dialog::file_dialog::DsaFileDialog;
use emulator_core::memory::PhysAddr;
use panels::PanelFactory;
use crate::ui::components::display::Display;
use crate::ui::components::framebuffer::FrameBuffer;
use crate::ui::components::registers::Registers;
use crate::ui::components::serial::Serial;
use crate::ui::debugger::dialog::error_dialog::ErrorDialog;
use crate::ui::debugger::panels::{CentralTiles, TilePane};
use crate::ui::debugger::panels::about::AboutScreen;
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),
ShowError(String),
}
struct EmulatorHandle {
state: Arc<SharedState>,
memory: MemoryBank,
}
impl EmulatorHandle {
pub fn new(state: Arc<SharedState>, memory: MemoryBank) -> Self {
Self { state, memory }
}
pub fn write_bytes(&mut self, offset: PhysAddr, data: &[u8]) {
self.memory.write_bytes(offset, data);
}
}
pub struct DebuggerUi {
emulator: EmulatorHandle,
central_tiles: CentralTiles,
controller: Controller,
confirm_dialog: ConfirmDialog<Action>,
file_dialog: DsaFileDialog<Action>,
error_dialog: ErrorDialog,
tools: Vec<PanelFactory>
}
impl DebuggerUi {
fn open_or_focus<T: TilePane + 'static>(
central_tiles: &mut CentralTiles,
slot: &mut Option<TileId>,
make_pane: impl FnOnce() -> T,
) {
match slot {
Some(tile_id) => central_tiles.activate_tab(*tile_id),
None => *slot = Some(central_tiles.add_tab(make_pane())),
}
}
pub fn new(
cc: &eframe::CreationContext,
state: Arc<SharedState>,
mem_handle: MemoryBank,
) -> Self {
DsaUi::configure_appearance(&cc.egui_ctx);
Self {
emulator: EmulatorHandle::new(state.clone(), mem_handle.clone()),
central_tiles: CentralTiles::new(),
controller: Controller::new(state.clone(), mem_handle.clone()),
confirm_dialog: ConfirmDialog::new(),
file_dialog: DsaFileDialog::new(),
error_dialog: ErrorDialog::default(),
tools: vec![
PanelFactory::register("Memory Inspector", {
let mem_handle = mem_handle.clone();
move || MemoryInspector::new(mem_handle.clone())
}),
PanelFactory::register("Documentation", Documentation::default),
PanelFactory::register("Display", {
let mem_handle = mem_handle.clone();
move || Display::new(80, 25, 0x20000, mem_handle.clone())
}),
PanelFactory::register("Framebuffer", {
let mem_handle = mem_handle.clone();
move || FrameBuffer::new(320, 240, 0x30000, mem_handle.clone())
}),
PanelFactory::register("Serial", {
let mem_handle = mem_handle.clone();
let state = state.clone();
move || Serial::new(0x40000, state.clone(), mem_handle.clone())
}),
PanelFactory::register("Registers", {
let state = state.clone();
move || Registers::new(state.clone())
})
]
}
}
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" | "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!(),
None => {},
}
}
Action::ShowError(err) => {
self.error_dialog.open(err);
}
Action::LoadBinary(path) => {
if let Some(ext) = path.extension().map(|s| s.to_str()).flatten() && ext == "dsb" {
let Ok(file) = File::open(path) else {
self.perform_action(Action::ShowError("Failed to open file".into()));
return
};
let Ok(binary) = DseExecutable::read(&mut BufReader::new(file)) else {
self.perform_action(Action::ShowError("Failed to read binary".into()));
return;
};
self.emulator.write_bytes(0, &binary.to_memory_image())
}
}
Action::SaveFile(path, tile) => {
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()))
}
}
}
Action::RequestSaveAs(tile) => {
self.file_dialog.open_save(move |path| Action::SaveFile(path, tile));
}
};
}
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 {
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
ui.request_repaint_after(Duration::from_millis(16));
if let Some(action) = self.confirm_dialog.show(ui) {
self.perform_action(action);
}
if let Some(action) = self.file_dialog.show(ui) {
self.perform_action(action);
}
self.error_dialog.show(ui);
Panel::top("menu_bar").resizable(false).show(ui, |ui| {
self.toolbar(ui);
});
Panel::top("control_panel").resizable(false).show(ui, |ui| {
self.controller.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);
}
});
}
}
@@ -1,5 +1,5 @@
use egui::{Modal, Id, Layout, Align, RichText};
use crate::ui::debugger::ui_helpers::PaddedWidget;
use crate::ui::ui_helpers::PaddedWidget;
pub struct ConfirmDialog<T> {
open: bool,
@@ -1,8 +1,5 @@
use std::path::PathBuf;
use egui::Ui;
use egui_file_dialog::FileDialog;
use crate::ui::debugger::Action;
pub struct DsaFileDialog<T> {
inner: FileDialog, // or whatever backend you're using
+310 -132
View File
@@ -1,171 +1,349 @@
use std::fmt::Debug;
use std::fs::File;
use std::io::BufReader;
use std::path::PathBuf;
use std::time::Duration;
use eframe::NativeOptions;
use eframe::egui;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use egui::Order::Debug;
use egui::{CentralPanel, Panel, Ui};
use egui_tiles::TileId;
use common::formats::binary_dse::DseExecutable;
use dialog::confirm_dialog::ConfirmDialog;
use panels::documentation::Documentation;
use panels::editor::Editor;
use dialog::file_dialog::DsaFileDialog;
use emulator_core::{EmulatorController};
use panels::PanelFactory;
use crate::ui::controller::Controller;
use crate::ui::dialog::error_dialog::ErrorDialog;
use crate::ui::panels::{CentralTiles, TilePane};
use crate::ui::panels::about::AboutScreen;
use crate::ui::panels::disassembler::BinaryViewer;
use crate::ui::panels::display::Display;
use crate::ui::panels::framebuffer::FrameBuffer;
use crate::ui::panels::memory::MemoryInspector;
use crate::ui::panels::registers::Registers;
use crate::ui::panels::serial::Serial;
mod components;
pub mod debugger;
mod dialog;
mod panels;
mod ui_helpers;
mod controller;
use components::{
controller::Controller, display::Display, editor::Editor, framebuffer::FrameBuffer, memory::MemoryInspector,
registers::Registers, serial::Serial, Component,
};
use emulator_core::{
config::VERSION,
memory::ram::MemoryBank,
processor::state::SharedState
};
use crate::ui::components::disassembler::Disassembler;
use crate::ui::debugger::DebuggerUi;
pub fn run_app(state: Arc<SharedState>, mem_handle: MemoryBank) -> eframe::Result<()> {
let e = e();
pub fn run_app(mut controller: EmulatorController) -> eframe::Result<()> {
// initialise emulator
controller.run().unwrap();
eframe::run_native(
"Damn Simple Architecture",
NativeOptions::default(),
// Box::new(|cc| Ok(Box::new(DsaUi::new(cc, state, mem_handle)))),
Box::new(|cc| Ok(Box::new(DebuggerUi::new(cc, state, mem_handle)))),
Box::new(|cc| Ok(Box::new(DamnSimpleUI::new(cc, controller)))),
)
}
fn e() -> i32 {
return 5
trait FileFormat: Debug + Clone {
fn extension(&self) -> &'static str;
}
impl eframe::App for DsaUi {
impl FileFormat for DseExecutable {
fn extension(&self) -> &'static str {
"dse"
}
}
#[derive(Debug, Clone)]
pub enum DataSource {
File(PathBuf),
Data(Vec<u8>),
}
#[derive(Debug, Clone)]
pub enum Action {
RequestCloseTab(TileId),
CloseTab(TileId),
Quit,
OpenFile(PathBuf),
// OpenCachedFile(Box<dyn FileFormat>),
LoadBinary(DataSource),
SaveFile(PathBuf, TileId),
RequestSaveAs(TileId),
ShowError(String),
}
pub struct DamnSimpleUI {
emulator: EmulatorController,
central_tiles: CentralTiles,
controller: Controller,
confirm_dialog: ConfirmDialog<Action>,
file_dialog: DsaFileDialog<Action>,
error_dialog: ErrorDialog,
tools: Vec<PanelFactory>,
}
impl eframe::App for DamnSimpleUI {
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
// request an update from ui every cycle
self.state.update_req.store(true, Ordering::Relaxed);
ui.request_repaint_after(Duration::from_millis(16));
// title panel with dsa title
egui::containers::Panel::top("top_panel").show(ui, |ui| {
ui.with_layout(
egui::Layout::top_down_justified(egui::Align::Center)
.with_main_align(egui::Align::Min),
|ui| {
ui.allocate_space(egui::vec2(0.0, 5.0));
ui.heading(format!("Damn Simple Architecture v{} 🚀", VERSION));
ui.allocate_space(egui::vec2(0.0, 5.0));
},
);
if let Some(action) = self.confirm_dialog.show(ui) {
self.perform_action(action);
}
if let Some(action) = self.file_dialog.show(ui) {
self.perform_action(action);
}
self.error_dialog.show(ui);
Panel::top("menu_bar").resizable(false).show(ui, |ui| {
self.toolbar(ui);
});
// menu bar.
egui::Panel::top("menu_bar").show(ui, |ui| {
egui::MenuBar::new().ui(ui, |ui| {
ui.menu_button("Panels", |ui| {
ui.set_max_width(300.0);
ui.set_min_width(300.0);
ui.spacing_mut().button_padding = egui::vec2(10.0, 5.0);
for comp in &mut self.components {
let name = comp.title().to_string();
ui.toggle_value(comp.visible(), name);
}
});
});
Panel::top("control_panel").resizable(false).show(ui, |ui| {
self.controller.ui(ui);
});
egui::CentralPanel::default().show(ui, |ui| {
for c in &mut self.components {
let mut visible = *c.visible();
if visible {
egui::Window::new(c.title())
.open(&mut visible)
.show(ui, |ui| {
c.ui(ui);
});
}
*c.visible() = visible;
Panel::left("left_panel").resizable(false).show(ui, |ui| {
// TODO: File Explorer!
});
Panel::bottom("bottom_panel").resizable(false).show(ui, |ui| {
// self.process_manager.ui(ui);
});
CentralPanel::default().show(ui, |ui| {
if let Some(action) = self.central_tiles.ui(ui) {
self.perform_action(action);
}
});
ui.request_repaint_after(std::time::Duration::from_millis(16)); // ~60fps
}
}
pub struct DsaUi {
state: Arc<SharedState>,
mem_handle: MemoryBank,
components: Vec<Box<dyn Component>>,
}
impl DsaUi {
impl DamnSimpleUI {
fn open_or_focus<T: TilePane + 'static>(
central_tiles: &mut CentralTiles,
slot: &mut Option<TileId>,
make_pane: impl FnOnce() -> T,
) {
match slot {
Some(tile_id) => central_tiles.activate_tab(*tile_id),
None => *slot = Some(central_tiles.add_tab(make_pane())),
}
}
pub fn new(
cc: &eframe::CreationContext,
state: Arc<SharedState>,
mem_handle: MemoryBank,
mut emulator: EmulatorController,
) -> Self {
// components
let mut components: Vec<Box<dyn Component>> = vec![
Box::new(Controller::new(state.clone(), mem_handle.clone())),
Box::new(Display::new(80, 25, 0x20000, mem_handle.clone())),
Box::new(FrameBuffer::new(320, 240, 0x30000, mem_handle.clone())),
Box::new(Registers::new(state.clone())),
Box::new(Serial::new(0x40000, state.clone(), mem_handle.clone())),
Box::new(Editor::new(state.clone(), mem_handle.clone())),
Box::new(MemoryInspector::new(mem_handle.clone())),
Box::new(Disassembler::new())
];
components.iter_mut().for_each(|c| *c.visible() = false);
Self::configure_appearance(&cc.egui_ctx);
let app = Self {
state,
mem_handle,
components,
};
app
configure_appearance(&cc.egui_ctx);
let handle = emulator.get_handle().clone();
Self {
central_tiles: CentralTiles::new(),
controller: Controller::new(handle.clone()),
confirm_dialog: ConfirmDialog::new(),
file_dialog: DsaFileDialog::new(),
error_dialog: ErrorDialog::default(),
tools: vec![
PanelFactory::register("Memory Inspector", {
let handle = handle.clone();
move || MemoryInspector::new(handle.clone())
}),
PanelFactory::register("Documentation", Documentation::default),
PanelFactory::register("Display", {
let handle = handle.clone();
move || Display::new(80, 25, 0x20000, handle.clone())
}),
PanelFactory::register("Framebuffer", {
let handle = handle.clone();
move || FrameBuffer::new(320, 240, 0x30000, handle.clone())
}),
PanelFactory::register("Serial", {
let handle = handle.clone();
move || Serial::new(0x40000, handle.clone())
}),
PanelFactory::register("Registers", {
let handle = handle.clone();
move || Registers::new(handle.clone())
})
],
emulator,
}
}
pub fn configure_appearance(ctx: &egui::Context) {
// configure appearance of UI elements
let mut visuals = egui::Visuals::dark();
pub fn perform_action(&mut self, action: Action) {
match action {
// --- your existing settings ---
visuals.window_fill = egui::Color32::from_rgb(20, 20, 20);
visuals.panel_fill = egui::Color32::from_rgb(20, 20, 20);
visuals.widgets.inactive.fg_stroke =
egui::Stroke::from((1.0, egui::Color32::from_rgb(255, 255, 255)));
visuals.widgets.inactive.bg_stroke =
egui::Stroke::from((1.0, egui::Color32::from_rgb(60, 60, 60)));
visuals.widgets.inactive.corner_radius = egui::CornerRadius::from(4);
visuals.widgets.inactive.bg_fill = egui::Color32::from_rgb(20, 20, 20);
visuals.widgets.inactive.weak_bg_fill = egui::Color32::from_rgb(20, 20, 20);
visuals.widgets.inactive.expansion = 1.0;
Action::RequestCloseTab(tile_id) => {
self.confirm_dialog.open(
"Close Tab",
"Closing this tab may result in losing unsaved work",
Action::CloseTab(tile_id)
);
}
// tile resize handle
visuals.window_stroke = egui::Stroke::new(1.0, egui::Color32::from_rgb(45, 45, 45));
visuals.extreme_bg_color = egui::Color32::from_rgb(20, 20, 20);
Action::CloseTab(tile_id) => {
self.central_tiles.close_tab(tile_id);
}
ctx.set_visuals(visuals);
Action::Quit => std::process::exit(0),
let mut fonts = egui::FontDefinitions::default();
fonts.font_data.insert(
"JetBrains Mono Nerd Font".to_string(),
std::sync::Arc::new(egui::FontData::from_static(include_bytes!(
"../../font/JetBrainsMonoNerdFontMono_Regular.ttf",
))),
);
// Opens a file (usually constructed by the file dialogue)
Action::OpenFile(path) => {
match path.extension().map(|s| s.to_str()).flatten() {
Some("dsa" | "dsc") => { self.central_tiles.add_tab(Editor::new_open(&path)); },
Some("dsb") => { self.central_tiles.add_tab(BinaryViewer::new_open(&path)); },
Some("md") => { self.central_tiles.add_tab(Documentation::open(&path)); },
Some(_) => todo!(),
None => {},
}
}
fonts
.families
.entry(egui::FontFamily::Proportional)
.or_default()
.insert(0, "JetBrains Mono Nerd Font".to_string());
Action::ShowError(err) => {
self.error_dialog.open(err);
}
fonts
.families
.entry(egui::FontFamily::Monospace)
.or_default()
.insert(0, "JetBrains Mono Nerd Font".to_string());
Action::LoadBinary(DataSource::Data(exe)) => {
self.emulator.get_handle().mem_write(0, &exe);
}
ctx.set_fonts(fonts);
Action::LoadBinary(DataSource::File(path)) => {
if let Some(ext) = path.extension().map(|s| s.to_str()).flatten() && ext == "dsb" {
let Ok(file) = File::open(path) else {
self.perform_action(Action::ShowError("Failed to open file".into()));
return
};
let Ok(binary) = DseExecutable::read(&mut BufReader::new(file)) else {
self.perform_action(Action::ShowError("Failed to read binary".into()));
return;
};
self.emulator.get_handle().mem_write(0, &binary.to_memory_image())
}
}
Action::SaveFile(path, tile) => {
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()))
}
}
}
Action::RequestSaveAs(tile) => {
self.file_dialog.open_save(move |path| Action::SaveFile(path, tile));
}
};
}
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(|path| Action::LoadBinary(DataSource::File(path)));
}
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);
}
});
});
}
}
pub fn configure_appearance(ctx: &egui::Context) {
// configure appearance of UI elements
let mut visuals = egui::Visuals::dark();
// --- your existing settings ---
visuals.window_fill = egui::Color32::from_rgb(20, 20, 20);
visuals.panel_fill = egui::Color32::from_rgb(20, 20, 20);
visuals.widgets.inactive.fg_stroke =
egui::Stroke::from((1.0, egui::Color32::from_rgb(255, 255, 255)));
visuals.widgets.inactive.bg_stroke =
egui::Stroke::from((1.0, egui::Color32::from_rgb(60, 60, 60)));
visuals.widgets.inactive.corner_radius = egui::CornerRadius::from(4);
visuals.widgets.inactive.bg_fill = egui::Color32::from_rgb(20, 20, 20);
visuals.widgets.inactive.weak_bg_fill = egui::Color32::from_rgb(20, 20, 20);
visuals.widgets.inactive.expansion = 1.0;
// tile resize handle
visuals.window_stroke = egui::Stroke::new(1.0, egui::Color32::from_rgb(45, 45, 45));
visuals.extreme_bg_color = egui::Color32::from_rgb(20, 20, 20);
ctx.set_visuals(visuals);
let mut fonts = egui::FontDefinitions::default();
fonts.font_data.insert(
"JetBrains Mono Nerd Font".to_string(),
std::sync::Arc::new(egui::FontData::from_static(include_bytes!(
"../../font/JetBrainsMonoNerdFontMono_Regular.ttf",
))),
);
fonts
.families
.entry(egui::FontFamily::Proportional)
.or_default()
.insert(0, "JetBrains Mono Nerd Font".to_string());
fonts
.families
.entry(egui::FontFamily::Monospace)
.or_default()
.insert(0, "JetBrains Mono Nerd Font".to_string());
ctx.set_fonts(fonts);
}
@@ -1,13 +1,10 @@
use std::default::Default;
use std::any::Any;
use std::path::Path;
use crate::ui::Action;
use crate::ui::panels::TilePane;
use eframe::epaint::Direction;
use egui::{Ui, Widget, WidgetText};
use egui_commonmark::{CommonMarkCache, CommonMarkViewer};
use egui::{Ui, WidgetText};
use egui_tiles::TileId;
use emulator_core::config::{VERSION};
use crate::ui::debugger::Action;
use crate::ui::debugger::panels::TilePane;
use emulator_core::config::VERSION;
use std::any::Any;
pub struct AboutScreen;
@@ -0,0 +1,283 @@
use std::any::Any;
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
use std::sync::mpsc;
use std::sync::Arc;
use std::thread;
use egui::{ScrollArea, Ui, WidgetText};
use egui_extras::{Size, StripBuilder};
use egui_file::FileDialog;
use egui_tiles::TileId;
use common::formats::binary_dse::DseExecutable;
use disassembler::{disassemble, Disassembly};
use crate::ui::panels::TilePane;
use crate::ui::{Action, DataSource};
use crate::ui::panels::memory::MemoryViewer;
#[derive(PartialEq, Clone, Copy)]
enum ViewMode {
Memory,
Disassembly,
}
enum DisassemblyState {
Idle,
Running(mpsc::Receiver<Disassembly>),
Done(Disassembly),
}
pub struct BinaryViewer {
dialog: Option<FileDialog>,
error: Option<String>,
program: Option<Arc<DseExecutable>>,
memory_image: Option<Vec<u8>>,
view: ViewMode,
memory_viewer: MemoryViewer,
disassembly: DisassemblyState,
}
impl BinaryViewer {
pub fn new() -> Self {
Self {
dialog: None,
error: None,
program: None,
memory_image: None,
view: ViewMode::Memory,
memory_viewer: MemoryViewer::new(),
disassembly: DisassemblyState::Idle,
}
}
pub fn new_open(path: &Path) -> Self {
let mut viewer = Self::new();
match File::open(path).map(|f| DseExecutable::read(&mut BufReader::new(f))) {
Ok(Ok(bin)) => viewer.load_program(bin),
Ok(Err(e)) => viewer.error = Some(format!("Failed to load {}: {e}", path.display())),
Err(e) => viewer.error = Some(format!("Failed to load {}: {e}", path.display())),
}
viewer
}
fn load_program(&mut self, bin: DseExecutable) {
// TODO: replace with however DseExecutable actually exposes its raw memory image.
// Assuming a method like this exists — swap for the real accessor.
self.memory_image = Some(bin.to_memory_image());
self.program = Some(Arc::new(bin));
self.disassembly = DisassemblyState::Idle;
self.view = ViewMode::Memory;
self.error = None;
}
fn open(&mut self) {
let Ok(cwd) = std::env::current_dir() else {
self.error = Some("Couldn't get CWD to open file!".to_string());
return;
};
if self.dialog.is_none() {
let mut dialog = FileDialog::open_file();
dialog.set_path(cwd);
dialog.open();
self.dialog = Some(dialog);
}
}
fn handle_file_dialog(&mut self, ui: &mut Ui) {
let Some(dialog) = &mut self.dialog else { return };
if !dialog.show(ui).selected() { return }
let Some(file) = dialog.path() else { return };
let file = file.to_path_buf();
self.dialog = None;
if !file.extension().is_some_and(|ext| ext == "dsb") {
self.error = Some("Selected file is not a .dsb binary".into());
return;
}
let reader = match File::open(&file) {
Ok(f) => f,
Err(e) => { self.error = Some(e.to_string()); return; }
};
match DseExecutable::read(&mut BufReader::new(reader)) {
Ok(bin) => self.load_program(bin),
Err(e) => self.error = Some(e.to_string()),
}
}
/// Kicks disassembly off on a background thread so a large binary
/// doesn't stall the UI. `DseExecutable` is wrapped in `Arc` so the
/// thread can borrow it without cloning the whole binary image.
fn start_disassembly(&mut self) {
let Some(program) = self.program.clone() else { return };
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let result = disassemble(&program);
let _ = tx.send(result);
});
self.disassembly = DisassemblyState::Running(rx);
self.view = ViewMode::Disassembly;
}
fn poll_disassembly(&mut self) {
if let DisassemblyState::Running(rx) = &self.disassembly {
if let Ok(result) = rx.try_recv() {
self.disassembly = DisassemblyState::Done(result);
}
}
}
fn toolbar(&mut self, ui: &mut Ui) -> Option<Action> {
let mut action = None;
ui.horizontal(|ui| {
ui.spacing_mut().button_padding = egui::vec2(8.0, 4.0);
ui.spacing_mut().item_spacing.x = 6.0;
if ui.button("Open").clicked() {
self.open();
}
let has_program = self.program.is_some();
if ui.add_enabled(has_program, egui::Button::new("Load")).clicked() {
if let Some(data) = &self.memory_image {
action = Some(Action::LoadBinary(DataSource::Data(data.clone())));
}
}
let disassembling = matches!(self.disassembly, DisassemblyState::Running(_));
if ui
.add_enabled(has_program && !disassembling, egui::Button::new("Disassemble"))
.clicked()
{
self.start_disassembly();
}
ui.separator();
ui.selectable_value(&mut self.view, ViewMode::Memory, "Memory");
let has_disassembly = !matches!(self.disassembly, DisassemblyState::Idle);
ui.add_enabled_ui(has_disassembly, |ui| {
ui.selectable_value(&mut self.view, ViewMode::Disassembly, "Disassembly");
});
if let Some(err) = &self.error {
ui.separator();
ui.colored_label(egui::Color32::from_rgb(220, 80, 80), err);
}
});
action
}
}
impl TilePane for BinaryViewer {
fn title(&self) -> WidgetText {
"Binary Viewer".into()
}
fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option<Action> {
self.handle_file_dialog(ui);
self.poll_disassembly();
if matches!(self.disassembly, DisassemblyState::Running(_)) {
ui.ctx().request_repaint(); // keep polling promptly while the background thread works
}
let action = self.toolbar(ui);
ui.separator();
match self.view {
ViewMode::Memory => match &self.memory_image {
Some(memory) => self.memory_viewer.ui(ui, memory, 0),
None => {
ui.centered_and_justified(|ui| {
ui.weak("Open a binary to inspect its memory image.");
});
}
},
ViewMode::Disassembly => match &self.disassembly {
DisassemblyState::Idle => {
ui.centered_and_justified(|ui| {
ui.weak("Click Disassemble to generate a disassembly.");
});
}
DisassemblyState::Running(_) => {
ui.centered_and_justified(|ui| {
ui.horizontal(|ui| {
ui.spinner();
ui.label("Disassembling…");
});
});
}
DisassemblyState::Done(output) => render_disassembly(ui, output),
},
}
action
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
fn render_disassembly(ui: &mut Ui, output: &Disassembly) {
StripBuilder::new(ui)
.size(Size::exact(350.0))
.size(Size::remainder().at_most(500.0))
.horizontal(|mut strip| {
strip.cell(|ui| {
ScrollArea::vertical().id_salt("symbols").show(ui, |ui| {
egui::Grid::new("symbols_grid")
.striped(true)
.spacing([12.0, 2.0])
.show(ui, |ui| {
ui.strong("Addr");
ui.strong("Name");
ui.strong("Section");
ui.strong("Bind");
ui.end_row();
for symbol in &output.symbols {
ui.monospace(format!("{:#06x}", symbol.address));
ui.monospace(&symbol.name);
ui.label(format!("{:?}", symbol.section));
ui.label(format!("{:?}", symbol.binding));
ui.end_row();
}
});
});
});
strip.cell(|ui| {
ScrollArea::vertical().id_salt("asm").show(ui, |ui| {
egui::Grid::new("asm_grid")
.striped(true)
.spacing([12.0, 2.0])
.show(ui, |ui| {
for (addr, line) in output.text_lines.iter().enumerate() {
ui.label(
egui::RichText::new(format!("{:04X}", addr))
.monospace()
.weak(),
);
ui.monospace(line);
ui.end_row();
}
});
});
});
});
}
@@ -1,62 +1,41 @@
use std::any::Any;
use egui::{Color32, FontId, Ui, Vec2, WidgetText};
use egui_tiles::TileId;
use emulator_core::{
memory::PhysAddr,
memory::ram::{MemoryBank, Page}
};
use crate::ui::debugger::Action;
use crate::ui::debugger::panels::TilePane;
use super::Component;
use emulator_core::{memory::ram::{MemoryBank, Page}, memory::PhysAddr, EmulatorHandle};
use crate::ui::Action;
use crate::ui::panels::TilePane;
pub struct Display {
width: usize,
height: usize,
width: u32,
height: u32,
addr: PhysAddr,
mem: MemoryBank,
emulator: EmulatorHandle,
buffer: Vec<u8>,
visible: bool,
}
impl Display {
pub fn new(width: u32, height: u32, addr: PhysAddr, mem: MemoryBank) -> Self {
pub fn new(width: u32, height: u32, addr: PhysAddr, emulator: EmulatorHandle) -> Self {
Self {
width: width as usize,
height: height as usize,
width,
height,
addr,
mem,
emulator,
buffer: Vec::new(),
visible: true,
}
}
pub fn visible(&mut self) -> &mut bool {
&mut self.visible
}
pub fn width(&self) -> usize {
self.width
self.width as usize
}
pub fn height(&self) -> usize {
self.height
self.height as usize
}
fn internal_read(&self) -> Vec<u8> {
let pages = (0..((self.width * self.height).div_ceil(Page::SIZE)))
.map(|i| {
let page = self.mem.read_page(self.addr + i as u32 * 4096);
page.as_slice().to_owned()
})
.flatten()
.take((self.width * self.height) as usize)
.collect::<Vec<u8>>();
pages
}
pub fn changed(&mut self) -> bool {
let temp = self.internal_read();
let temp = self.read();
if temp != self.buffer {
self.buffer = temp;
return true;
@@ -65,7 +44,7 @@ impl Display {
}
pub fn read(&mut self) -> Vec<u8> {
self.internal_read()
self.emulator.mem_read(self.addr, self.width * self.height)
}
}
@@ -76,25 +55,6 @@ impl TilePane for Display {
}
fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option<Action> {
Component::ui(self, ui);
None
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl Component for Display {
fn title(&self) -> &str {
"Display"
}
fn visible(&mut self) -> &mut bool {
self.visible()
}
fn ui(&mut self, ui: &mut egui::Ui) {
let display_w = self.width();
let display_h = self.height();
let data = self.read();
@@ -143,5 +103,10 @@ impl Component for Display {
Color32::WHITE,
);
}
None
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
@@ -4,8 +4,8 @@ use std::path::Path;
use egui::{Ui, Widget, WidgetText};
use egui_commonmark::{CommonMarkCache, CommonMarkViewer};
use egui_tiles::TileId;
use crate::ui::debugger::Action;
use crate::ui::debugger::panels::TilePane;
use crate::ui::Action;
use crate::ui::panels::TilePane;
pub struct Documentation {
buffer: Option<String>,
@@ -1,22 +1,18 @@
use crate::ui::Action;
use crate::ui::panels::TilePane;
use egui::{Key, Ui};
use egui_code_editor::CodeEditor;
use egui_tiles::TileId;
use std::any::Any;
use std::path::{Path, PathBuf};
use egui::accesskit::Role::Code;
use egui::{Key, Ui};
use egui_code_editor::{CodeEditor, Syntax};
use egui_file_dialog::FileDialog;
use egui_tiles::TileId;
use crate::ui::components::Component;
use crate::ui::components::editor::{syntax, THEME};
use crate::ui::debugger::Action;
use crate::ui::debugger::panels::TilePane;
use theme::THEME;
mod syntax;
mod theme;
pub struct Editor {
buffer: String,
filename: Option<PathBuf>,
editor: CodeEditor,
dialog: FileDialog,
visible: bool,
}
impl Default for Editor {
@@ -25,8 +21,6 @@ impl Default for Editor {
buffer: String::new(),
filename: None,
editor: CodeEditor::default().with_fontsize(12.0).with_theme(THEME).with_numlines(true),
dialog: FileDialog::new(),
visible: false,
}
}
}
@@ -90,4 +84,5 @@ impl TilePane for Editor {
fn close_requires_confirmation(&mut self, _tile_id: TileId) -> bool {
true
}
}
}
@@ -1,7 +1,6 @@
use std::collections::BTreeSet;
use egui_code_editor::{Patch, Syntax};
pub fn dsa() -> Syntax {
Syntax {
word_start: BTreeSet::new(),
@@ -0,0 +1,18 @@
use egui_code_editor::ColorTheme;
pub const THEME: ColorTheme = ColorTheme {
name: "Theme",
dark: true,
bg: "1b1b1b",
cursor: "de5252", // fg4
selection: "28323B", // bg2
comments: "444444", // gray1
functions: "7CCCC7", // green1
keywords: "6C81E0", // red1
literals: "A3ABFF", // fg1
numerics: "8A46CF", // purple1
punctuation: "99C9C9", // orange1
strs: "618c84", // aqua1
types: "B8B9D4", // yellow1
special: "de5252", // blue1
};
@@ -1,35 +1,30 @@
use std::any::Any;
use egui::{Ui, Vec2, WidgetText};
use egui_tiles::TileId;
use emulator_core::{
memory::PhysAddr,
memory::ram::{MemoryBank, Page}
};
use crate::ui::debugger::Action;
use crate::ui::debugger::panels::TilePane;
use super::Component;
use emulator_core::{memory::PhysAddr, EmulatorHandle};
use crate::ui::Action;
use crate::ui::panels::TilePane;
pub struct FrameBuffer {
width: usize,
height: usize,
addr: PhysAddr,
mem: MemoryBank,
pub buffer: Vec<u32>, // RGBA pixels
visible: bool,
emulator: EmulatorHandle,
pub buffer: Vec<u8>,
texture: Option<egui::TextureHandle>,
}
impl FrameBuffer {
pub fn new(width: u32, height: u32, addr: PhysAddr, mem: MemoryBank) -> Self {
pub fn new(width: u32, height: u32, addr: PhysAddr, emulator: EmulatorHandle) -> Self {
let width = width as usize;
let height = height as usize;
Self {
width: width as usize,
height: height as usize,
width,
height,
addr,
mem,
buffer: vec![0u32; width as usize * height as usize],
visible: true,
emulator,
buffer: vec![0; width * height * 4],
texture: None,
}
}
@@ -40,25 +35,10 @@ impl FrameBuffer {
pub fn height(&self) -> usize {
self.height
}
pub fn visible(&mut self) -> &mut bool {
&mut self.visible
}
fn internal_read(&self) -> Vec<u32> {
fn internal_read(&mut self) -> Vec<u8> {
let byte_size = self.width * self.height * 4;
let page_count = byte_size.div_ceil(Page::SIZE);
(0..page_count)
.map(|i| {
let page = self.mem.read_page(self.addr + i as u32 * 4096);
page.as_slice().to_owned()
})
.flatten()
.take(byte_size)
.collect::<Vec<u8>>()
.chunks_exact(4)
.map(|chunk| u32::from_le_bytes(chunk.try_into().unwrap()))
.collect()
self.emulator.mem_read(self.addr, byte_size as u32)
}
pub fn changed(&mut self) -> bool {
@@ -70,7 +50,7 @@ impl FrameBuffer {
false
}
pub fn read(&self) -> &[u32] {
pub fn read(&self) -> &[u8] {
&self.buffer
}
@@ -87,27 +67,12 @@ impl TilePane for FrameBuffer {
fn title(&self) -> WidgetText {
"Framebuffer".into()
}
fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option<Action> {
Component::ui(self, ui);
None
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl Component for FrameBuffer {
fn title(&self) -> &str {
"Framebuffer"
}
fn visible(&mut self) -> &mut bool {
&mut self.visible
}
fn ui(&mut self, ui: &mut egui::Ui) {
fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option<Action> {
let changed = self.changed();
// get or create the texture
@@ -126,10 +91,10 @@ impl Component for FrameBuffer {
if changed {
let pixels: Vec<egui::Color32> = self
.buffer
.as_chunks::<4>().0
.iter()
.map(|&p| {
let bytes = p.to_le_bytes();
egui::Color32::from_rgba_premultiplied(bytes[0], bytes[1], bytes[2], bytes[3])
.map(|&[red, green, blue, alpha]| {
egui::Color32::from_rgba_premultiplied(red, green, blue, alpha)
})
.collect();
@@ -153,5 +118,6 @@ impl Component for FrameBuffer {
};
ui.image(egui::load::SizedTexture::new(texture.id(), size));
None
}
}
@@ -4,17 +4,16 @@ use egui::{Ui, WidgetText};
use egui_extras::{Column, TableBuilder};
use egui_tiles::{Tile, TileId};
use common::prelude::Instruction;
use emulator_core::EmulatorHandle;
use emulator_core::memory::ram::MemoryBank;
use crate::ui::debugger::Action;
use crate::ui::debugger::panels::TilePane;
use super::Component;
use crate::ui::Action;
use crate::ui::panels::TilePane;
pub struct MemoryInspector {
memory: MemoryBank,
emulator: EmulatorHandle,
view_size: u32,
view_addr: u32,
visible: bool,
addr_input: String,
// settings
@@ -23,14 +22,12 @@ pub struct MemoryInspector {
impl MemoryInspector {
#[must_use]
pub fn new(memory: MemoryBank) -> Self {
pub fn new(emulator: EmulatorHandle) -> Self {
Self {
memory,
emulator,
view_size: 1024,
view_addr: 0,
visible: false,
addr_input: String::from("0x00"),
memory_viewer: MemoryViewer::new()
}
}
@@ -41,27 +38,8 @@ impl TilePane for MemoryInspector {
fn title(&self) -> WidgetText {
"Memory Inspector".into()
}
fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option<Action> {
Component::ui(self, ui);
None
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl Component for MemoryInspector {
fn title(&self) -> &'static str {
"Memory Inspector"
}
fn visible(&mut self) -> &mut bool {
&mut self.visible
}
fn ui(&mut self, ui: &mut egui::Ui) {
ui.horizontal(|ui| {
ui.label("Address:");
@@ -96,12 +74,14 @@ impl Component for MemoryInspector {
ui.label("(hex or decimal)");
});
let mut window = vec![0u8; self.view_size as usize];
for i in 0..self.view_size {
window[i as usize] = self.memory.read_byte(self.view_addr + i as u32);
}
let window = self.emulator.mem_read(self.view_addr, self.view_size);
self.memory_viewer.ui(ui, &window, self.view_addr as usize);
None
}
self.memory_viewer.ui(ui, &window, self.view_addr as usize)
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
@@ -1,12 +1,17 @@
pub mod documentation;
pub mod editor;
pub mod about;
pub mod disassembler;
pub mod display;
pub mod framebuffer;
pub mod memory;
pub mod registers;
pub mod serial;
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;
use crate::ui::Action;
use crate::ui::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
@@ -1,38 +1,22 @@
use std::{f32, sync::Arc};
use std::f32;
use std::any::Any;
use egui::{Ui, WidgetText};
use egui_tiles::TileId;
use common::prelude::Register;
use emulator_core::processor::state::SharedState;
use crate::ui::components::memory::MemoryInspector;
use crate::ui::debugger::Action;
use crate::ui::debugger::panels::TilePane;
use super::Component;
use emulator_core::EmulatorHandle;
use crate::ui::Action;
use crate::ui::panels::TilePane;
pub struct Registers {
state: Arc<SharedState>,
visible: bool,
state: EmulatorHandle,
}
impl TilePane for Registers {
fn title(&self) -> WidgetText {
"Registers".into()
}
fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option<Action> {
Component::ui(self, ui);
None
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl Registers {
fn section(
&mut self,
ui: &mut egui::Ui,
ui: &mut Ui,
registers: Vec<(Register, u32)>,
col_pairs: usize,
name: &str,
@@ -54,7 +38,7 @@ impl Registers {
for chunk in registers.chunks(col_pairs) {
for (reg, val) in chunk {
ui.label(reg.to_string());
ui.label(format!("0x{:08X} ({})", val, val));
ui.label(format!("0x{:08X} ({:012})", val, val));
}
for _ in chunk.len()..col_pairs {
ui.label("");
@@ -68,27 +52,21 @@ impl Registers {
}
impl Registers {
pub fn new(state: Arc<SharedState>) -> Self {
pub fn new(state: EmulatorHandle) -> Self {
Self {
state,
visible: false,
}
}
}
impl Component for Registers {
fn title(&self) -> &str {
"Registers"
impl TilePane for Registers {
fn title(&self) -> WidgetText {
"Registers".into()
}
fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option<Action> {
fn visible(&mut self) -> &mut bool {
&mut self.visible
}
fn ui(&mut self, ui: &mut egui::Ui) {
ui.set_min_width(200.0);
let state = self.state.proc.load();
let available = ui.available_width();
let col_pairs = match available {
0.0..350.0 => 1,
@@ -101,7 +79,7 @@ impl Component for Registers {
ui.vertical(|ui| {
// ── General Purpose ──────────────────────────────────────
let gp_registers: Vec<(Register, u32)> = (0..=15u8)
.map(|i| (Register::from_u8(i).unwrap(), state.registers[i as usize]))
.map(|i| (Register::from_u8(i).unwrap(), self.state.registers()[i as usize]))
.collect();
self.section(
ui,
@@ -113,18 +91,18 @@ impl Component for Registers {
// ── Stack ─────────────────────────────────────────────────
let stack_registers = vec![
(Register::Spr, state.registers[Register::Spr as usize]),
(Register::Bpr, state.registers[Register::Bpr as usize]),
(Register::Spr, self.state.get_register(Register::Spr)),
(Register::Bpr, self.state.get_register(Register::Bpr)),
];
self.section(ui, stack_registers, col_pairs, "Stack Registers");
ui.separator();
// ── Special Purpose ───────────────────────────────────────
let special_registers = vec![
(Register::Acc, state.registers[Register::Acc as usize]),
(Register::Ret, state.registers[Register::Ret as usize]),
(Register::Idr, state.registers[Register::Idr as usize]),
(Register::Mmr, state.registers[Register::Mmr as usize]),
(Register::Acc, self.state.get_register(Register::Acc)),
(Register::Ret, self.state.get_register(Register::Ret)),
(Register::Idr, self.state.get_register(Register::Idr)),
(Register::Mmr, self.state.get_register(Register::Mmr)),
];
self.section(
ui,
@@ -136,11 +114,16 @@ impl Component for Registers {
// ── System ────────────────────────────────────────────────
let system_registers = vec![
(Register::Pcx, state.registers[Register::Pcx as usize]),
(Register::Sts, state.registers[Register::Sts as usize]),
(Register::Cir, state.registers[Register::Cir as usize]),
(Register::Pcx, self.state.get_register(Register::Pcx)),
(Register::Sts, self.state.get_register(Register::Sts)),
(Register::Cir, self.state.get_register(Register::Cir)),
];
self.section(ui, system_registers, col_pairs, "System Registers");
});
None
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
@@ -1,28 +1,18 @@
use std::{
collections::VecDeque,
sync::{
Arc,
atomic::Ordering,
mpsc::{Receiver, Sender},
},
};
use std::any::Any;
use egui::{Ui, WidgetText};
use egui_tiles::TileId;
use emulator_core::{
memory::{
PhysAddr,
ram::MemoryBank
},
processor::{
interrupts::Interrupt,
state::SharedState
}
};
use crate::ui::components::registers::Registers;
use crate::ui::debugger::Action;
use crate::ui::debugger::panels::TilePane;
use super::Component;
use emulator_core::{memory::{
PhysAddr,
}, EmulatorHandle};
use emulator_core::processor::Interrupt;
use crate::ui::Action;
use crate::ui::panels::TilePane;
/// ## SERIAL LAYOUT: (from the perspective of code inside the ui)
/// - [base+0x00]: ser_out valid flag - set by kernel after writing a byte to serial
@@ -34,29 +24,11 @@ pub struct Serial {
pub write_tx: Sender<u8>,
pub read_buff: Vec<u8>,
pub write_buff: String,
pub visible: bool,
}
impl TilePane for Serial {
fn title(&self) -> WidgetText {
"Serial".into()
}
fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option<Action> {
Component::ui(self, ui);
None
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl Serial {
fn uart_io_thread(
mem: MemoryBank,
state: Arc<SharedState>,
mut handle: EmulatorHandle,
uart_base: PhysAddr,
tx: Sender<u8>,
rx: Receiver<u8>,
@@ -68,42 +40,41 @@ impl Serial {
write_buffer.push_back(byte);
}
let ser_out_valid = mem.read_byte(uart_base + 0);
let ser_out_valid = handle.read_byte(uart_base + 0);
// Kernel => ui: valid flag in byte 0, data in byte 1
if ser_out_valid == 0x01 {
let ser_out = mem.read_byte(uart_base + 1);
let ser_out = handle.read_byte(uart_base + 1);
let _ = tx.send(ser_out);
mem.write_byte(uart_base, 0x00); // clear the whole word
handle.write_byte(uart_base, 0x00); // clear the whole word
}
if !write_buffer.is_empty() {
// Emulator => kernel: valid flag in byte 2, data in byte 3
let ser_in_valid = mem.read_byte(uart_base + 2);
let ser_in_valid = handle.read_byte(uart_base + 2);
if ser_in_valid == 0x00 {
if let Some(byte) = write_buffer.pop_front() {
mem.write_byte(uart_base + 3, byte);
mem.write_byte(uart_base + 2, 0x01);
handle.write_byte(uart_base + 3, byte);
handle.write_byte(uart_base + 2, 0x01);
}
}
std::thread::sleep(std::time::Duration::from_millis(2));
state.interrupt_queue.send(Interrupt::SerialIn).unwrap();
handle.interrupt(Interrupt::SerialIn).unwrap();
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
}
pub fn new(uart_base: PhysAddr, state: Arc<SharedState>, mem_handle: MemoryBank) -> Self {
pub fn new(uart_base: PhysAddr, handle: EmulatorHandle) -> Self {
let (read_tx, read_rx) = std::sync::mpsc::channel();
let (write_tx, write_rx) = std::sync::mpsc::channel();
let _ = std::thread::spawn(move || {
Self::uart_io_thread(mem_handle, state, uart_base, read_tx, write_rx)
Self::uart_io_thread(handle, uart_base, read_tx, write_rx)
});
Self {
visible: false,
write_tx,
read_rx,
read_buff: Vec::new(),
@@ -118,16 +89,11 @@ impl Serial {
}
}
impl Component for Serial {
fn title(&self) -> &str {
"Serial"
impl TilePane for Serial {
fn title(&self) -> WidgetText {
"Serial".into()
}
fn visible(&mut self) -> &mut bool {
&mut self.visible
}
fn ui(&mut self, ui: &mut egui::Ui) {
fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option<Action> {
self.update();
// Text field for input and a send button
@@ -145,5 +111,11 @@ impl Component for Serial {
if ui.button("Clear").clicked() {
self.read_buff.clear();
}
None
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
@@ -1,8 +1,7 @@
use eframe::emath::Vec2;
use egui::{Color32, Frame, Stroke};
use egui::widget_style::{Classes, WidgetState};
use emulator_core::processor::state::RunningState;
use emulator_core::processor::RunningState;
pub struct PaddedWidget {
text: String,