megacommit:
- setup new UI layout using tiling and an action/message based system for updating global state based on interactions with individual tiles and vice versa - added custom dialogs and UI helpers to create a more cohesive UI - added a disassembler widget (WIP) - added a documentation widget (WIP) - refactored backend halt/pause logic to fix bugs. - started emulator implementation documentation (to complement the ISA spec)
This commit is contained in:
+1
-1
@@ -5,4 +5,4 @@ resolver = "3"
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
version = "0.3.0"
|
||||
authors = ["zxq5"]
|
||||
authors = ["zxq5", "zxq4"]
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# States
|
||||
|
||||
### Definitions
|
||||
- Model
|
||||
- Private - internal state of the processor, always up to date.
|
||||
- time: Duration,
|
||||
- state: **Running | Paused | Halted | Shutdown | Breakpoint**
|
||||
- Shared - shared across threads. updated periodically
|
||||
- time: Duration
|
||||
- cycles: Integer
|
||||
|
||||
|
||||
## Running
|
||||
the running state is the main loop of the emulator.
|
||||
|
||||
### Responsibilities:
|
||||
- run fetch-decode-execute cycle
|
||||
- handle interrupts
|
||||
- go to paused state if requested by UI.
|
||||
- go to the halted state if it hits that instruction
|
||||
- keep the UI updated occasionally, but prioritise extremely high throughput
|
||||
|
||||
### Transitions:
|
||||
- to `Paused`
|
||||
- when the `pause` signal is received
|
||||
- to `Halted`
|
||||
- when hitting the `Hlt` instruction.
|
||||
- to `Shutdown`
|
||||
- when the `shutdown` signal is received
|
||||
|
||||
***DEBUG IMPLEMENTATION ONLY***
|
||||
- to `Breakpoint`
|
||||
- when hitting a breakpoint
|
||||
|
||||
## Paused
|
||||
the paused state is started and ended by the UI thread.
|
||||
|
||||
### Responsibilities:
|
||||
- update the UI when requested
|
||||
- reset when requested by the UI
|
||||
- save the running-time and executed instructions since the last pause state.
|
||||
- add `private.time` to `shared.time`
|
||||
- reset `private.time, shared.time, shared.cycles` on state exit
|
||||
|
||||
### Transitions
|
||||
- to `Halted`
|
||||
- when the `run` signal is received and the previous state was `Halted`
|
||||
- to `Running`
|
||||
- when the `run` signal is received
|
||||
- to `Shutdown`
|
||||
- when the `shutdown` signal is received
|
||||
|
||||
## Halted
|
||||
the state reached when the emulator has hit a `Hlt` instruction and is waiting to continue
|
||||
|
||||
### Responsibilities:
|
||||
- update the UI when requested
|
||||
- unpause when an interrupt is received
|
||||
- keep track of the time spent in the running state across multiple hlt/interrupt cycles
|
||||
- add `private.time` to `shared.time`
|
||||
- reset `private.time` on state exit
|
||||
- preserve `shared` on state exit
|
||||
|
||||
### Transitions
|
||||
- to `Running`
|
||||
- when the `interrupt` signal is received
|
||||
- to `Paused`
|
||||
- when the `pause` signal is received
|
||||
- to `Shutdown`
|
||||
- when the `shutdown` signal is received
|
||||
|
||||
## Shutdown
|
||||
the shutdown state is a terminal state. the emulator cleans up its memory and exits.
|
||||
|
||||
## Breakpoint
|
||||
debug state used only in the debugger implementation of the emulator
|
||||
|
||||
### Transitions
|
||||
- to `Running`
|
||||
- when the `run` signal is received
|
||||
@@ -5,7 +5,7 @@ version.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "dsa-a"
|
||||
name = "dsa-d"
|
||||
path = "src/main.rs"
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -148,6 +148,13 @@ impl MemoryBank {
|
||||
unsafe { self.heap.add(addr as usize).write_volatile(value) }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn write_bytes(&self, addr: PhysAddr, bytes: &[u8]) {
|
||||
unsafe {
|
||||
self.heap.add(addr as usize).copy_from(bytes.as_ptr(), bytes.len());
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn write_word(&self, addr: PhysAddr, value: u32) {
|
||||
unsafe { (self.heap.add(addr as usize) as *mut u32).write_volatile(value) };
|
||||
|
||||
@@ -26,6 +26,8 @@ use crate::{
|
||||
};
|
||||
use crate::config::{MemoryMap, RegionType};
|
||||
use crate::memory::ram::Page;
|
||||
use crate::processor::state::{ProcessorSnapshot, RunningState};
|
||||
use crate::processor::state::RunningState::{Halted, Paused, Running, Shutdown};
|
||||
|
||||
pub struct Emulator {
|
||||
internal_state: ProcessorSnapshot,
|
||||
@@ -124,7 +126,7 @@ impl Emulator {
|
||||
}
|
||||
|
||||
#[cold]
|
||||
fn update(&mut self) {
|
||||
fn share_state(&mut self) {
|
||||
self.shared_state
|
||||
.proc
|
||||
.store(Arc::new(self.internal_state.clone()));
|
||||
@@ -140,90 +142,87 @@ impl Emulator {
|
||||
MemoryMap::identity_map(&mut self.mmu, map.regions.get(0).unwrap());
|
||||
}
|
||||
|
||||
self.internal_state.halted = false;
|
||||
self.internal_state.running = Running;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cold]
|
||||
fn shutdown(&mut self) {
|
||||
self.internal_state.halted = true;
|
||||
}
|
||||
|
||||
pub fn halt(&mut self) {
|
||||
self.internal_state.halted = true;
|
||||
|
||||
// wait until we're un-halted.
|
||||
while self.internal_state.halted {
|
||||
self.wait_for_instructions();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_update(&mut self) {
|
||||
// UI requested a state update.
|
||||
if self.shared_state.update_req.load(Ordering::Relaxed) {
|
||||
self.update();
|
||||
}
|
||||
|
||||
if let x = self.shared_state.goto.load(Ordering::Relaxed) && x != -1 {
|
||||
*self.mut_reg(Register::Pcx) = x as u32;
|
||||
self.shared_state.goto.store(-1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
if self.shared_state.reseting.load(Ordering::Relaxed) {
|
||||
self.internal_state.registers = [0; Register::COUNT as usize];
|
||||
self.internal_state.clock = 0;
|
||||
|
||||
// sit in a wait loop while resetting.
|
||||
while self.shared_state.reseting.load(Ordering::Relaxed) {
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
self.internal_state.halted = false;
|
||||
}
|
||||
}
|
||||
|
||||
#[cold]
|
||||
pub fn wait_for_instructions(&mut self) {
|
||||
{
|
||||
// record time for previous execution.
|
||||
self.internal_state.time = self.time.elapsed();
|
||||
self.update();
|
||||
self.time = Instant::now();
|
||||
}
|
||||
pub fn run_state_halted(&mut self, prior: RunningState) {
|
||||
self.internal_state.running = Halted;
|
||||
|
||||
// if paused from the UI thread, just wait.
|
||||
while self.shared_state.paused.load(Ordering::Relaxed) {
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
// UI is making changes / requesting updates.
|
||||
self.check_update();
|
||||
}
|
||||
// Responsibility: keep track of the time spent in the running state across multiple hlt/interrupt cycles
|
||||
self.internal_state.time += self.time.elapsed();
|
||||
|
||||
loop {
|
||||
// Check for interrupts.
|
||||
// Transition: interrupt received
|
||||
if let Ok(int) = self.interrupts.recv_timeout(Duration::from_millis(100)) {
|
||||
self.internal_state.halted = false;
|
||||
self.internal_state.running = Running;
|
||||
self.interrupt(int);
|
||||
break;
|
||||
}
|
||||
|
||||
// if the cpu didn't halt on it's own then it's ready to run again.
|
||||
if !self.internal_state.halted {
|
||||
return;
|
||||
// Transition: pause signal received
|
||||
if self.shared_state.paused.load(Ordering::Relaxed) {
|
||||
self.run_state_paused(Halted);
|
||||
}
|
||||
|
||||
// UI is resetting the ui
|
||||
self.check_update();
|
||||
// Responsibility: update UI when requested
|
||||
if self.shared_state.update_req.load(Ordering::Relaxed) == true {
|
||||
self.share_state();
|
||||
}
|
||||
}
|
||||
|
||||
// Responsibility: reset `private.time` on state exit
|
||||
self.time = Instant::now();
|
||||
self.internal_state.running = prior;
|
||||
}
|
||||
|
||||
#[cold]
|
||||
pub fn run_state_paused(&mut self, prior: RunningState) {
|
||||
self.internal_state.running = Paused;
|
||||
|
||||
// Responsibility: save the running-time since last pause state.
|
||||
self.internal_state.time = self.time.elapsed();
|
||||
self.share_state();
|
||||
|
||||
loop {
|
||||
// Responsibility: update UI when requested
|
||||
if self.shared_state.update_req.load(Ordering::Relaxed) == true {
|
||||
self.share_state();
|
||||
}
|
||||
|
||||
// Responsibility: reset when requested by the UI
|
||||
if self.shared_state.reseting.load(Ordering::Relaxed) == true {
|
||||
self.internal_state.registers = [0; Register::COUNT as usize];
|
||||
self.internal_state.clock = 0;
|
||||
|
||||
while self.shared_state.reseting.load(Ordering::Relaxed) == true {
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
|
||||
// Transition: run signal received
|
||||
if self.shared_state.paused.load(Ordering::Relaxed) == false {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsibility: reset private.time, shared.time, shared.cycles on state exit
|
||||
self.time = Instant::now();
|
||||
self.internal_state.time = Duration::new(0, 0);
|
||||
self.internal_state.clock = 0;
|
||||
|
||||
self.internal_state.running = prior;
|
||||
}
|
||||
|
||||
pub fn run(&mut self) -> Result<(), String> {
|
||||
// wait for UI to initialise ui.
|
||||
// wait for UI to initialise.
|
||||
if self.shared_state.paused.load(Ordering::Relaxed) {
|
||||
self.wait_for_instructions();
|
||||
self.run_state_paused(Running)
|
||||
}
|
||||
|
||||
self.boot()?;
|
||||
self.time = Instant::now();
|
||||
|
||||
'emu: loop {
|
||||
// so that it always returns zero. this is more performance friendly than checking
|
||||
@@ -234,17 +233,21 @@ impl Emulator {
|
||||
// do not change anything about this loop. it's fully optimised afaik.
|
||||
// Check for commands or hardware Interrupts every 32k cycles
|
||||
if unlikely(self.internal_state.clock & 0x7FFF == 0) {
|
||||
|
||||
// Responsibility: keep the UI thread up to date periodically
|
||||
// Update UI thread (roughly every 512k cycles targeting 60 UPS)
|
||||
if unlikely(self.internal_state.clock & 0x7FFFF == 0) {
|
||||
if self.shared_state.update_req.load(Ordering::Relaxed) {
|
||||
self.update();
|
||||
self.share_state();
|
||||
}
|
||||
}
|
||||
|
||||
// Transition: Paused
|
||||
if unlikely(self.shared_state.paused.load(Ordering::Relaxed)) {
|
||||
self.wait_for_instructions();
|
||||
self.run_state_paused(Running)
|
||||
}
|
||||
|
||||
// Responsibility: Handle interrupts
|
||||
match self.interrupts.try_recv() {
|
||||
Ok(int) => self.interrupt(int),
|
||||
Err(TryRecvError::Disconnected) => break 'emu,
|
||||
@@ -252,6 +255,7 @@ impl Emulator {
|
||||
}
|
||||
}
|
||||
|
||||
// Responsibility: run fetch-decode-execute cycle.
|
||||
let pc = self.reg(Register::Pcx);
|
||||
let instruction = self.mem_read_word(pc);
|
||||
|
||||
@@ -280,7 +284,7 @@ impl Emulator {
|
||||
self.internal_state.clock += 1;
|
||||
}
|
||||
|
||||
self.shutdown();
|
||||
self.internal_state.running = Shutdown;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -512,7 +516,9 @@ impl Emulator {
|
||||
Some(Opcode::Int) => {
|
||||
self.interrupt(Interrupt::Software(word.imm16() as u8));
|
||||
}
|
||||
Some(Opcode::Hlt) => self.halt(),
|
||||
Some(Opcode::Hlt) => {
|
||||
self.run_state_halted(Running);
|
||||
},
|
||||
Some(Opcode::IRet) => {
|
||||
self.pop(Register::Ret);
|
||||
*self.mut_reg(Register::Pcx) = self.reg(Register::Ret);
|
||||
@@ -569,21 +575,3 @@ impl Emulator {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProcessorSnapshot {
|
||||
pub halted: bool,
|
||||
pub clock: usize,
|
||||
pub time: Duration,
|
||||
pub registers: [u32; Register::COUNT as usize],
|
||||
}
|
||||
|
||||
impl Default for ProcessorSnapshot {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
halted: false,
|
||||
clock: 0,
|
||||
time: Duration::ZERO,
|
||||
registers: [0; Register::COUNT as usize],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
use std::sync::{Arc, atomic::AtomicBool, mpsc};
|
||||
use std::sync::atomic::AtomicIsize;
|
||||
use std::time::Duration;
|
||||
use arc_swap::ArcSwap;
|
||||
|
||||
use crate::processor::processor::ProcessorSnapshot;
|
||||
use common::prelude::Register;
|
||||
|
||||
use super::interrupts::Interrupt;
|
||||
|
||||
pub struct SharedState {
|
||||
pub proc: ArcSwap<ProcessorSnapshot>,
|
||||
|
||||
pub goto: AtomicIsize,
|
||||
pub reseting: AtomicBool,
|
||||
pub paused: AtomicBool,
|
||||
pub update_req: AtomicBool,
|
||||
@@ -22,10 +20,37 @@ impl SharedState {
|
||||
reseting: AtomicBool::new(false),
|
||||
proc: ArcSwap::new(Arc::new(ProcessorSnapshot::default())),
|
||||
|
||||
goto: AtomicIsize::new(-1),
|
||||
paused: AtomicBool::new(true),
|
||||
interrupt_queue: sender,
|
||||
update_req: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum RunningState {
|
||||
Paused,
|
||||
Halted,
|
||||
Running,
|
||||
Shutdown,
|
||||
Breakpoint(u32),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProcessorSnapshot {
|
||||
pub running: RunningState,
|
||||
pub clock: usize,
|
||||
pub time: Duration,
|
||||
pub registers: [u32; Register::COUNT as usize],
|
||||
}
|
||||
|
||||
impl Default for ProcessorSnapshot {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
running: RunningState::Paused,
|
||||
clock: 0,
|
||||
time: Duration::ZERO,
|
||||
registers: [0; Register::COUNT as usize],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,9 @@ dirs = "6.0.0"
|
||||
assembler = { path = "../../assembler" }
|
||||
egui_extras = "0.35.0"
|
||||
emulator_core = { version = "0.3.0", path = "../emulator_core" }
|
||||
disassembler = { version = "0.3.0", path = "../../disassembler" }
|
||||
egui-file-dialog = "0.14.1"
|
||||
egui_commonmark = { version = "0.24.0", features = ["better_syntax_highlighting", "macros"] }
|
||||
|
||||
[features]
|
||||
default = ["mainstore-prealloc"]
|
||||
|
||||
@@ -4,4 +4,3 @@
|
||||
|
||||
pub mod ui;
|
||||
pub mod args;
|
||||
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
use crate::ui::debugger::ui_helpers::{ PaddedWidget, StatusIndicator};
|
||||
use std::sync::{Arc, atomic::Ordering};
|
||||
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
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;
|
||||
|
||||
pub struct Controller {
|
||||
state: Arc<SharedState>,
|
||||
@@ -33,81 +39,89 @@ impl Component for Controller {
|
||||
}
|
||||
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
let state = self.state.proc.load();
|
||||
|
||||
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);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
// Pause / Run
|
||||
if ui
|
||||
.button(match paused {
|
||||
false => "Pause",
|
||||
true => "Resume",
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
self.state.paused.store(!paused, Ordering::Relaxed);
|
||||
}
|
||||
egui::Frame::new()
|
||||
.inner_margin(10.0)
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
|
||||
// Step
|
||||
// if ui.button("Step").clicked() {
|
||||
// state
|
||||
// .cmd_sender
|
||||
// .send(Command::Step(self.step_amount))
|
||||
// .unwrap_or_else(|_| {
|
||||
// state.error_log.push("Failed to send command".to_string());
|
||||
// });
|
||||
// }
|
||||
let padding = Vec2::new(14.0, 8.0);
|
||||
|
||||
// Resets the ui and all attached devices
|
||||
if ui.button("Reset All").clicked() {
|
||||
self.state.reseting.store(true, Ordering::Relaxed);
|
||||
self.mem.clear();
|
||||
self.state.reseting.store(false, Ordering::Relaxed);
|
||||
}
|
||||
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),
|
||||
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);
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
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);
|
||||
}
|
||||
|
||||
// if ui
|
||||
// .text_edit_singleline(&mut self.step_amount_input)
|
||||
// .changed()
|
||||
// {
|
||||
// self.step_amount = if let Ok(amount) = self.step_amount_input.parse() {
|
||||
// amount
|
||||
// } else {
|
||||
// state
|
||||
// .error_log
|
||||
// .push("Unable to parse step amount".to_string());
|
||||
// 1
|
||||
// }
|
||||
// }
|
||||
// Step
|
||||
// if ui.button("Step").clicked() {
|
||||
// state
|
||||
// .cmd_sender
|
||||
// .send(Command::Step(self.step_amount))
|
||||
// .unwrap_or_else(|_| {
|
||||
// state.error_log.push("Failed to send command".to_string());
|
||||
// });
|
||||
// }
|
||||
|
||||
// Status info
|
||||
ui.label(format!(
|
||||
"Status: {}",
|
||||
match paused {
|
||||
true => "Paused",
|
||||
false => "Running",
|
||||
}
|
||||
));
|
||||
// if ui
|
||||
// .text_edit_singleline(&mut self.step_amount_input)
|
||||
// .changed()
|
||||
// {
|
||||
// self.step_amount = if let Ok(amount) = self.step_amount_input.parse() {
|
||||
// amount
|
||||
// } else {
|
||||
// state
|
||||
// .error_log
|
||||
// .push("Unable to parse step amount".to_string());
|
||||
// 1
|
||||
// }
|
||||
// }
|
||||
|
||||
ui.label(format!("Halted: {}", state.halted));
|
||||
ui.separator();
|
||||
state.running.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 mips = if time != 0 {
|
||||
state.clock as u128 / time
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
PaddedWidget::label(format!("Clock: {clock}")).ui(ui);
|
||||
PaddedWidget::label( format!("Pcx: 0x{pcx:08X}")).ui(ui);
|
||||
PaddedWidget::label( format!("Elapsed: {} micros", time)).ui(ui);
|
||||
PaddedWidget::label(format!("Freq: {}MHz", mips)).ui(ui);
|
||||
});
|
||||
});
|
||||
|
||||
let pcx = state.registers[Register::Pcx as usize];
|
||||
let clock = state.clock;
|
||||
let time = state.time.as_micros();
|
||||
|
||||
let mips = if time != 0 {
|
||||
state.clock as u128 / time
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
ui.label(format!("Clock: {clock}"));
|
||||
ui.label(format!("Pcx: 0x{pcx:08X}"));
|
||||
ui.label(format!("Elapsed: {} micros", time));
|
||||
ui.label(format!("Freq: {}MHz", mips));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
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,8 +1,12 @@
|
||||
use egui::{Color32, FontId, Vec2};
|
||||
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;
|
||||
|
||||
pub struct Display {
|
||||
@@ -65,6 +69,22 @@ impl Display {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl TilePane for Display {
|
||||
fn title(&self) -> WidgetText {
|
||||
"Display".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 Display {
|
||||
fn title(&self) -> &str {
|
||||
"Display"
|
||||
|
||||
@@ -22,6 +22,7 @@ use emulator_core::{
|
||||
memory::ram::{MemoryBank, Page},
|
||||
processor::state::SharedState
|
||||
};
|
||||
use crate::ui::components::memory::MemoryViewer;
|
||||
|
||||
pub struct Editor {
|
||||
state: Arc<SharedState>,
|
||||
@@ -39,7 +40,8 @@ pub struct Editor {
|
||||
syntax: Syntax,
|
||||
|
||||
// output / loading
|
||||
output: Output,
|
||||
output: Vec<u8>,
|
||||
viewer: MemoryViewer,
|
||||
load_offset: u32,
|
||||
offset_str: String,
|
||||
|
||||
@@ -87,7 +89,7 @@ impl Component for Editor {
|
||||
|ui| {
|
||||
if self.show_output {
|
||||
egui::Panel::right("Editor Output").resizable(false).show_inside(ui, |ui| {
|
||||
self.output.ui(ui);
|
||||
self.viewer.ui(ui, self.output.as_slice(), 0);
|
||||
});
|
||||
}
|
||||
self.render_editor(ui);
|
||||
@@ -116,7 +118,8 @@ impl Editor {
|
||||
.with_rows(0)
|
||||
.with_theme(THEME)
|
||||
.with_numlines(true),
|
||||
output: Output::new(),
|
||||
output: Vec::new(),
|
||||
viewer: MemoryViewer::new(),
|
||||
unsaved: true,
|
||||
visible: false,
|
||||
load_offset: 0,
|
||||
@@ -346,7 +349,7 @@ impl Editor {
|
||||
assembler.start(());
|
||||
|
||||
// Or block until done
|
||||
*self.output.mut_data() = match assembler.output() {
|
||||
self.output = match assembler.output() {
|
||||
Ok(instructions) => instructions,
|
||||
Err(e) => {
|
||||
self.error = Some(e.to_string());
|
||||
@@ -380,7 +383,7 @@ impl Editor {
|
||||
// }
|
||||
Some("dsb") => {
|
||||
if let Ok(bytes) = fs::read(path) {
|
||||
*self.output.mut_data() = bytes;
|
||||
self.output = bytes;
|
||||
} else {
|
||||
self.error = Some("Failed to read file".to_string());
|
||||
}
|
||||
@@ -436,7 +439,7 @@ impl Editor {
|
||||
self.error = Some("Can't load program at invalid offset!".to_string());
|
||||
}
|
||||
|
||||
let mut reader = Cursor::new(self.output.data().clone());
|
||||
let mut reader = Cursor::new(self.output.clone());
|
||||
let Ok(executable) = DseExecutable::read(&mut reader) else {
|
||||
self.error = Some("Failed to read program!".to_string());
|
||||
return
|
||||
@@ -450,7 +453,6 @@ impl Editor {
|
||||
for (i, page) in image.iter().enumerate() {
|
||||
self.memory.write_page(i as u32 * 4096, &page);
|
||||
}
|
||||
self.state.goto.store(executable.entry_point(0) as isize, Ordering::Relaxed);
|
||||
self.state.reseting.store(false, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
@@ -502,194 +504,194 @@ pub const THEME: ColorTheme = ColorTheme {
|
||||
types: "B8B9D4", // yellow1
|
||||
special: "de5252", // blue1
|
||||
};
|
||||
|
||||
struct Output {
|
||||
pub data: Vec<u8>,
|
||||
|
||||
show_bytes: bool,
|
||||
show_words: bool,
|
||||
show_ascii: bool,
|
||||
show_instruction: bool,
|
||||
show_decimal: bool,
|
||||
}
|
||||
|
||||
impl Output {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
data: vec![],
|
||||
show_bytes: true,
|
||||
show_words: true,
|
||||
show_ascii: true,
|
||||
show_instruction: true,
|
||||
show_decimal: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn mut_data(&mut self) -> &mut Vec<u8> {
|
||||
&mut self.data
|
||||
}
|
||||
|
||||
fn data(&self) -> &Vec<u8> {
|
||||
&self.data
|
||||
}
|
||||
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
// Output area with synchronized scrolling
|
||||
egui::ScrollArea::vertical()
|
||||
.id_salt("output_scroll")
|
||||
.max_width(400.0)
|
||||
.show(ui, |ui| {
|
||||
if self.data.is_empty() {
|
||||
ui.label(
|
||||
egui::RichText::new("No output data")
|
||||
.font(egui::FontId::monospace(12.0))
|
||||
.color(egui::Color32::GRAY),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.toggle_value(&mut self.show_bytes, "Bytes");
|
||||
ui.toggle_value(&mut self.show_words, "Words");
|
||||
ui.toggle_value(&mut self.show_decimal, "Decimal");
|
||||
ui.toggle_value(&mut self.show_ascii, "Ascii");
|
||||
ui.toggle_value(&mut self.show_instruction, "Instructions");
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
let mut table = TableBuilder::new(ui)
|
||||
.resizable(false) // makes all columns resizable at once
|
||||
.column(Column::initial(55.0)); // addr
|
||||
|
||||
if self.show_bytes {
|
||||
table = table.column(Column::initial(90.0));
|
||||
}
|
||||
if self.show_words {
|
||||
table = table.column(Column::initial(75.0));
|
||||
}
|
||||
if self.show_decimal {
|
||||
table = table.column(Column::initial(80.0));
|
||||
}
|
||||
if self.show_ascii {
|
||||
table = table.column(Column::initial(50.0));
|
||||
}
|
||||
if self.show_instruction {
|
||||
table = table.column(Column::initial(200.0).clip(true));
|
||||
}
|
||||
|
||||
table
|
||||
.header(20.0, |mut header| {
|
||||
header.col(|ui| {
|
||||
ui.label("Addr");
|
||||
});
|
||||
if self.show_bytes {
|
||||
header.col(|ui| {
|
||||
ui.label("Bytes");
|
||||
});
|
||||
}
|
||||
if self.show_words {
|
||||
header.col(|ui| {
|
||||
ui.label("Word");
|
||||
});
|
||||
}
|
||||
if self.show_decimal {
|
||||
header.col(|ui| {
|
||||
ui.label("Decimal");
|
||||
});
|
||||
}
|
||||
if self.show_ascii {
|
||||
header.col(|ui| {
|
||||
ui.label("Ascii");
|
||||
});
|
||||
}
|
||||
if self.show_instruction {
|
||||
header.col(|ui| {
|
||||
ui.label("Instruction");
|
||||
});
|
||||
}
|
||||
})
|
||||
.body(|mut body| {
|
||||
// Process bytes in chunks of 4
|
||||
for (line_num, chunk) in self.data.chunks(4).enumerate() {
|
||||
body.row(18.0, |mut row| {
|
||||
let address = line_num * 4;
|
||||
let mut buf = [0u8; 4];
|
||||
buf[..chunk.len()].copy_from_slice(chunk);
|
||||
let value = u32::from_le_bytes(buf);
|
||||
|
||||
// Address column
|
||||
row.col(|ui| {
|
||||
ui.label(
|
||||
egui::RichText::new(format!("0x{address:04X}"))
|
||||
.font(egui::FontId::monospace(12.0)),
|
||||
);
|
||||
});
|
||||
|
||||
if self.show_bytes {
|
||||
row.col(|ui| {
|
||||
// Individual bytes column
|
||||
ui.label(
|
||||
egui::RichText::new(
|
||||
chunk
|
||||
.iter()
|
||||
.map(|b| format!("{b:02X}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" "),
|
||||
)
|
||||
.font(egui::FontId::monospace(12.0))
|
||||
.color(egui::Color32::from_rgb(200, 200, 255)),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if self.show_words {
|
||||
row.col(|ui| {
|
||||
// Hex column
|
||||
ui.label(
|
||||
egui::RichText::new(format!("0x{value:08X}"))
|
||||
.font(egui::FontId::monospace(12.0))
|
||||
.color(egui::Color32::from_rgb(255, 100, 100)),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if self.show_decimal {
|
||||
row.col(|ui| {
|
||||
ui.label(
|
||||
egui::RichText::new(format!("{value}"))
|
||||
.font(egui::FontId::monospace(12.0))
|
||||
.color(egui::Color32::from_rgb(100, 255, 100)),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if self.show_ascii {
|
||||
let ascii = value.to_le_bytes();
|
||||
row.col(|ui| {
|
||||
ui.label(
|
||||
egui::RichText::new(String::from_utf8_lossy(&ascii))
|
||||
.font(egui::FontId::monospace(12.0))
|
||||
.color(egui::Color32::from_rgb(100, 255, 255)),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if self.show_instruction {
|
||||
row.col(|ui| {
|
||||
ui.label(
|
||||
egui::RichText::new(format!(
|
||||
"{:?}",
|
||||
Instruction(value)
|
||||
))
|
||||
.font(egui::FontId::monospace(12.0))
|
||||
.color(egui::Color32::from_rgb(255, 100, 255)),
|
||||
);
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
//
|
||||
// struct Output {
|
||||
// pub data: Vec<u8>,
|
||||
//
|
||||
// show_bytes: bool,
|
||||
// show_words: bool,
|
||||
// show_ascii: bool,
|
||||
// show_instruction: bool,
|
||||
// show_decimal: bool,
|
||||
// }
|
||||
//
|
||||
// impl Output {
|
||||
// fn new() -> Self {
|
||||
// Self {
|
||||
// data: vec![],
|
||||
// show_bytes: true,
|
||||
// show_words: true,
|
||||
// show_ascii: true,
|
||||
// show_instruction: true,
|
||||
// show_decimal: true,
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// fn mut_data(&mut self) -> &mut Vec<u8> {
|
||||
// &mut self.data
|
||||
// }
|
||||
//
|
||||
// fn data(&self) -> &Vec<u8> {
|
||||
// &self.data
|
||||
// }
|
||||
//
|
||||
// fn ui(&mut self, ui: &mut Ui) {
|
||||
// // Output area with synchronized scrolling
|
||||
// egui::ScrollArea::vertical()
|
||||
// .id_salt("output_scroll")
|
||||
// .max_width(400.0)
|
||||
// .show(ui, |ui| {
|
||||
// if self.data.is_empty() {
|
||||
// ui.label(
|
||||
// egui::RichText::new("No output data")
|
||||
// .font(egui::FontId::monospace(12.0))
|
||||
// .color(egui::Color32::GRAY),
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// ui.horizontal(|ui| {
|
||||
// ui.toggle_value(&mut self.show_bytes, "Bytes");
|
||||
// ui.toggle_value(&mut self.show_words, "Words");
|
||||
// ui.toggle_value(&mut self.show_decimal, "Decimal");
|
||||
// ui.toggle_value(&mut self.show_ascii, "Ascii");
|
||||
// ui.toggle_value(&mut self.show_instruction, "Instructions");
|
||||
// });
|
||||
//
|
||||
// ui.separator();
|
||||
//
|
||||
// let mut table = TableBuilder::new(ui)
|
||||
// .resizable(false) // makes all columns resizable at once
|
||||
// .column(Column::initial(55.0)); // addr
|
||||
//
|
||||
// if self.show_bytes {
|
||||
// table = table.column(Column::initial(90.0));
|
||||
// }
|
||||
// if self.show_words {
|
||||
// table = table.column(Column::initial(75.0));
|
||||
// }
|
||||
// if self.show_decimal {
|
||||
// table = table.column(Column::initial(80.0));
|
||||
// }
|
||||
// if self.show_ascii {
|
||||
// table = table.column(Column::initial(50.0));
|
||||
// }
|
||||
// if self.show_instruction {
|
||||
// table = table.column(Column::initial(200.0).clip(true));
|
||||
// }
|
||||
//
|
||||
// table
|
||||
// .header(20.0, |mut header| {
|
||||
// header.col(|ui| {
|
||||
// ui.label("Addr");
|
||||
// });
|
||||
// if self.show_bytes {
|
||||
// header.col(|ui| {
|
||||
// ui.label("Bytes");
|
||||
// });
|
||||
// }
|
||||
// if self.show_words {
|
||||
// header.col(|ui| {
|
||||
// ui.label("Word");
|
||||
// });
|
||||
// }
|
||||
// if self.show_decimal {
|
||||
// header.col(|ui| {
|
||||
// ui.label("Decimal");
|
||||
// });
|
||||
// }
|
||||
// if self.show_ascii {
|
||||
// header.col(|ui| {
|
||||
// ui.label("Ascii");
|
||||
// });
|
||||
// }
|
||||
// if self.show_instruction {
|
||||
// header.col(|ui| {
|
||||
// ui.label("Instruction");
|
||||
// });
|
||||
// }
|
||||
// })
|
||||
// .body(|mut body| {
|
||||
// // Process bytes in chunks of 4
|
||||
// for (line_num, chunk) in self.data.chunks(4).enumerate() {
|
||||
// body.row(18.0, |mut row| {
|
||||
// let address = line_num * 4;
|
||||
// let mut buf = [0u8; 4];
|
||||
// buf[..chunk.len()].copy_from_slice(chunk);
|
||||
// let value = u32::from_le_bytes(buf);
|
||||
//
|
||||
// // Address column
|
||||
// row.col(|ui| {
|
||||
// ui.label(
|
||||
// egui::RichText::new(format!("0x{address:04X}"))
|
||||
// .font(egui::FontId::monospace(12.0)),
|
||||
// );
|
||||
// });
|
||||
//
|
||||
// if self.show_bytes {
|
||||
// row.col(|ui| {
|
||||
// // Individual bytes column
|
||||
// ui.label(
|
||||
// egui::RichText::new(
|
||||
// chunk
|
||||
// .iter()
|
||||
// .map(|b| format!("{b:02X}"))
|
||||
// .collect::<Vec<_>>()
|
||||
// .join(" "),
|
||||
// )
|
||||
// .font(egui::FontId::monospace(12.0))
|
||||
// .color(egui::Color32::from_rgb(200, 200, 255)),
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// if self.show_words {
|
||||
// row.col(|ui| {
|
||||
// // Hex column
|
||||
// ui.label(
|
||||
// egui::RichText::new(format!("0x{value:08X}"))
|
||||
// .font(egui::FontId::monospace(12.0))
|
||||
// .color(egui::Color32::from_rgb(255, 100, 100)),
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// if self.show_decimal {
|
||||
// row.col(|ui| {
|
||||
// ui.label(
|
||||
// egui::RichText::new(format!("{value}"))
|
||||
// .font(egui::FontId::monospace(12.0))
|
||||
// .color(egui::Color32::from_rgb(100, 255, 100)),
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// if self.show_ascii {
|
||||
// let ascii = value.to_le_bytes();
|
||||
// row.col(|ui| {
|
||||
// ui.label(
|
||||
// egui::RichText::new(String::from_utf8_lossy(&ascii))
|
||||
// .font(egui::FontId::monospace(12.0))
|
||||
// .color(egui::Color32::from_rgb(100, 255, 255)),
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// if self.show_instruction {
|
||||
// row.col(|ui| {
|
||||
// ui.label(
|
||||
// egui::RichText::new(format!(
|
||||
// "{:?}",
|
||||
// Instruction(value)
|
||||
// ))
|
||||
// .font(egui::FontId::monospace(12.0))
|
||||
// .color(egui::Color32::from_rgb(255, 100, 255)),
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
use egui::Vec2;
|
||||
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;
|
||||
|
||||
pub struct FrameBuffer {
|
||||
@@ -79,6 +83,21 @@ impl FrameBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
use std::any::Any;
|
||||
use std::num::ParseIntError;
|
||||
use egui::{Ui, WidgetText};
|
||||
use egui_extras::{Column, TableBuilder};
|
||||
use egui_tiles::{Tile, TileId};
|
||||
use common::prelude::Instruction;
|
||||
use emulator_core::memory::ram::MemoryBank;
|
||||
use crate::ui::debugger::Action;
|
||||
use crate::ui::debugger::panels::TilePane;
|
||||
use super::Component;
|
||||
|
||||
pub struct MemoryInspector {
|
||||
@@ -13,11 +18,7 @@ pub struct MemoryInspector {
|
||||
addr_input: String,
|
||||
|
||||
// settings
|
||||
show_bytes: bool,
|
||||
show_words: bool,
|
||||
show_ascii: bool,
|
||||
show_instruction: bool,
|
||||
show_decimal: bool,
|
||||
memory_viewer: MemoryViewer
|
||||
}
|
||||
|
||||
impl MemoryInspector {
|
||||
@@ -30,15 +31,27 @@ impl MemoryInspector {
|
||||
visible: false,
|
||||
addr_input: String::from("0x00"),
|
||||
|
||||
show_bytes: true,
|
||||
show_words: true,
|
||||
show_ascii: true,
|
||||
show_instruction: true,
|
||||
show_decimal: true,
|
||||
memory_viewer: MemoryViewer::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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"
|
||||
@@ -49,56 +62,74 @@ impl Component for MemoryInspector {
|
||||
}
|
||||
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
// Right column - Memory
|
||||
// ui.vertical(|ui| {
|
||||
// ui.add_space(10.0);
|
||||
// });
|
||||
// Memory table
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Address:");
|
||||
|
||||
let address_response = ui.add(
|
||||
egui::TextEdit::singleline(&mut self.addr_input)
|
||||
.hint_text("0x1000 or 4096")
|
||||
.desired_width(150.0),
|
||||
);
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
||||
// Search button
|
||||
let search_clicked = ui.button("🔍 Search").clicked();
|
||||
|
||||
// Handle Enter key in text field
|
||||
let enter_pressed =
|
||||
address_response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
|
||||
|
||||
if search_clicked || enter_pressed {
|
||||
if let Ok(new) = parse_address(&self.addr_input) {
|
||||
if new % 4 == 0 {
|
||||
self.view_addr = new;
|
||||
} else {
|
||||
println!("Address must be 4-byte aligned");
|
||||
}
|
||||
} else {
|
||||
println!("Invalid address");
|
||||
// state.error_log.push("Invalid address".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
self.memory_viewer.ui(ui, &window, self.view_addr as usize)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub struct MemoryViewer {
|
||||
show_bytes: bool,
|
||||
show_words: bool,
|
||||
show_ascii: bool,
|
||||
show_instruction: bool,
|
||||
show_decimal: bool,
|
||||
}
|
||||
|
||||
impl MemoryViewer {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
show_bytes: true,
|
||||
show_words: true,
|
||||
show_ascii: true,
|
||||
show_instruction: true,
|
||||
show_decimal: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, memory: &[u8], offset: usize) {
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink([false; 2])
|
||||
.id_salt("memory_inspector_scroll")
|
||||
.show(ui, |ui| {
|
||||
|
||||
// ui.heading("Memory Inspector");
|
||||
// ui.add_space(10.0);
|
||||
//
|
||||
// // Address input section
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Address:");
|
||||
|
||||
let address_response = ui.add(
|
||||
egui::TextEdit::singleline(&mut self.addr_input)
|
||||
.hint_text("0x1000 or 4096")
|
||||
.desired_width(150.0),
|
||||
);
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
||||
// Search button
|
||||
let search_clicked = ui.button("🔍 Search").clicked();
|
||||
|
||||
// Handle Enter key in text field
|
||||
let enter_pressed =
|
||||
address_response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
|
||||
|
||||
if search_clicked || enter_pressed {
|
||||
if let Ok(new) = parse_address(&self.addr_input) {
|
||||
if new % 4 == 0 {
|
||||
self.view_addr = new;
|
||||
} else {
|
||||
println!("Address must be 4-byte aligned");
|
||||
}
|
||||
} else {
|
||||
println!("Invalid address");
|
||||
// state.error_log.push("Invalid address".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
ui.label("(hex or decimal)");
|
||||
});
|
||||
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.toggle_value(&mut self.show_bytes, "Bytes");
|
||||
ui.toggle_value(&mut self.show_words, "Words");
|
||||
@@ -162,12 +193,11 @@ impl Component for MemoryInspector {
|
||||
})
|
||||
.body(|mut body| {
|
||||
// Process bytes in chunks of 4
|
||||
for (line_num, value) in (0..self.view_size / 4)
|
||||
.map(|i| (i, self.memory.read_word(self.view_addr + i * 4))) {
|
||||
for (line_num, chunk) in memory.as_chunks::<4>().0.iter().enumerate() {
|
||||
body.row(18.0, |mut row| {
|
||||
let address = line_num * 4 * self.view_addr;
|
||||
let address = line_num * 4 + offset;
|
||||
|
||||
let chunk = value.to_le_bytes();
|
||||
let value = u32::from_le_bytes(*chunk);
|
||||
|
||||
// Address column
|
||||
row.col(|ui| {
|
||||
@@ -216,10 +246,9 @@ impl Component for MemoryInspector {
|
||||
}
|
||||
|
||||
if self.show_ascii {
|
||||
let ascii = value.to_le_bytes();
|
||||
row.col(|ui| {
|
||||
ui.label(
|
||||
egui::RichText::new(String::from_utf8_lossy(&ascii))
|
||||
egui::RichText::new(String::from_utf8_lossy(chunk))
|
||||
.font(egui::FontId::monospace(12.0))
|
||||
.color(egui::Color32::from_rgb(100, 255, 255)),
|
||||
);
|
||||
@@ -242,7 +271,6 @@ impl Component for MemoryInspector {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ pub mod framebuffer;
|
||||
pub mod memory;
|
||||
pub mod registers;
|
||||
pub mod serial;
|
||||
pub mod disassembler;
|
||||
|
||||
pub trait Component {
|
||||
/// The tab label shown in the tile header.
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
use std::{f32, sync::Arc};
|
||||
|
||||
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;
|
||||
|
||||
pub struct Registers {
|
||||
@@ -10,6 +14,21 @@ pub struct Registers {
|
||||
visible: bool,
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -6,6 +6,9 @@ use std::{
|
||||
mpsc::{Receiver, Sender},
|
||||
},
|
||||
};
|
||||
use std::any::Any;
|
||||
use egui::{Ui, WidgetText};
|
||||
use egui_tiles::TileId;
|
||||
use emulator_core::{
|
||||
memory::{
|
||||
PhysAddr,
|
||||
@@ -16,6 +19,9 @@ use emulator_core::{
|
||||
state::SharedState
|
||||
}
|
||||
};
|
||||
use crate::ui::components::registers::Registers;
|
||||
use crate::ui::debugger::Action;
|
||||
use crate::ui::debugger::panels::TilePane;
|
||||
use super::Component;
|
||||
|
||||
/// ## SERIAL LAYOUT: (from the perspective of code inside the ui)
|
||||
@@ -32,6 +38,21 @@ pub struct Serial {
|
||||
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,
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
use egui::{Modal, Id, Layout, Align, RichText};
|
||||
|
||||
pub struct ConfirmDialog<T> {
|
||||
open: bool,
|
||||
title: String,
|
||||
message: String,
|
||||
action: Option<T>,
|
||||
}
|
||||
|
||||
impl<T> ConfirmDialog<T> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
open: false,
|
||||
title: String::new(),
|
||||
message: String::new(),
|
||||
action: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open(&mut self, title: impl Into<String>, message: impl Into<String>, action: T) {
|
||||
self.open = true;
|
||||
self.title = title.into();
|
||||
self.message = message.into();
|
||||
self.action = Some(action);
|
||||
}
|
||||
|
||||
pub fn show(&mut self, ctx: &egui::Context) -> Option<T> {
|
||||
if !self.open {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut result = None;
|
||||
|
||||
let modal = Modal::new(Id::new("confirm_dialog")).show(ctx, |ui| {
|
||||
ui.set_width(300.0);
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(&self.title).strong().size(16.0));
|
||||
ui.add_space(6.0);
|
||||
ui.separator();
|
||||
ui.add_space(8.0);
|
||||
ui.label(&self.message);
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.separator();
|
||||
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if ui.button(RichText::new("Confirm").strong()).clicked() {
|
||||
result = Some(true);
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
if ui.button("Cancel").clicked() {
|
||||
result = Some(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if modal.should_close() {
|
||||
self.open = false;
|
||||
self.action = None;
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(confirmed) = result {
|
||||
self.open = false;
|
||||
if confirmed {
|
||||
return self.action.take();
|
||||
}
|
||||
self.action = None;
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for ConfirmDialog<T> {
|
||||
fn default() -> Self { Self::new() }
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use egui::{Modal, Id, Layout, Align, RichText};
|
||||
|
||||
pub struct ErrorDialog {
|
||||
open: bool,
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl ErrorDialog {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
open: false,
|
||||
message: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open(&mut self, message: impl Into<String>) {
|
||||
self.open = true;
|
||||
self.message = message.into();
|
||||
}
|
||||
|
||||
pub fn show(&mut self, ctx: &egui::Context) {
|
||||
if !self.open {
|
||||
return
|
||||
}
|
||||
|
||||
let modal = Modal::new(Id::new("error_dialog")).show(ctx, |ui| {
|
||||
ui.set_width(300.0);
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new("Error").strong().size(16.0));
|
||||
ui.add_space(6.0);
|
||||
ui.separator();
|
||||
ui.add_space(8.0);
|
||||
ui.label(&self.message);
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.separator();
|
||||
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if ui.button(RichText::new("Ok").strong()).clicked() {
|
||||
self.open = false;
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ErrorDialog {
|
||||
fn default() -> Self { Self::new() }
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
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
|
||||
constructor: Option<Box<dyn FnOnce(PathBuf) -> T>>,
|
||||
}
|
||||
|
||||
impl<T> DsaFileDialog<T> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: egui_file_dialog::FileDialog::new()
|
||||
.add_save_extension("DSA source", "dsa")
|
||||
.add_save_extension("DSA binary", "dsb")
|
||||
.add_save_extension("DSC source", "dsc"),
|
||||
constructor: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_pick(&mut self, constructor: impl FnOnce(PathBuf) -> T + 'static) {
|
||||
self.constructor = Some(Box::new(constructor));
|
||||
self.inner.pick_file();
|
||||
}
|
||||
|
||||
pub fn open_save(&mut self, constructor: impl FnOnce(PathBuf) -> T + 'static) {
|
||||
self.constructor = Some(Box::new(constructor));
|
||||
self.inner.save_file();
|
||||
}
|
||||
|
||||
/// Call every frame. Returns Some(action) the frame a file is picked.
|
||||
pub fn show(&mut self, ctx: &egui::Context) -> Option<T> {
|
||||
self.inner.update(ctx);
|
||||
|
||||
if let Some(path) = self.inner.take_picked() {
|
||||
return self.constructor.take().map(|f| f(path));
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod file_dialog;
|
||||
pub mod confirm_dialog;
|
||||
pub mod error_dialog;
|
||||
@@ -0,0 +1,262 @@
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
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;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Action {
|
||||
Quit,
|
||||
OpenFile(PathBuf),
|
||||
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::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("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) => {
|
||||
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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Action::RequestSaveAs(tile) => {
|
||||
self.file_dialog.open_save(move |path| Action::SaveFile(path, tile));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
egui::Panel::top("info_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| {
|
||||
if let Some(action) = self.central_tiles.ui(ui) {
|
||||
self.perform_action(action);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use std::default::Default;
|
||||
use std::any::Any;
|
||||
use std::path::Path;
|
||||
use eframe::epaint::Direction;
|
||||
use egui::{Ui, Widget, WidgetText};
|
||||
use egui_commonmark::{CommonMarkCache, CommonMarkViewer};
|
||||
use egui_tiles::TileId;
|
||||
use emulator_core::config::{VERSION};
|
||||
use crate::ui::debugger::Action;
|
||||
use crate::ui::debugger::panels::TilePane;
|
||||
|
||||
pub struct AboutScreen;
|
||||
|
||||
impl TilePane for AboutScreen {
|
||||
fn title(&self) -> WidgetText {
|
||||
"About DSA".into()
|
||||
}
|
||||
|
||||
fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option<Action> {
|
||||
|
||||
ui.with_layout(
|
||||
egui::Layout::centered_and_justified(Direction::TopDown)
|
||||
.with_main_align(egui::Align::Center),
|
||||
|ui| {
|
||||
|
||||
// title
|
||||
ui.heading(format!("Damn Simple Architecture v{} 🚀", VERSION));
|
||||
},
|
||||
);
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use std::default::Default;
|
||||
use std::any::Any;
|
||||
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;
|
||||
|
||||
pub struct Documentation {
|
||||
buffer: Option<String>,
|
||||
cache: CommonMarkCache,
|
||||
}
|
||||
|
||||
impl TilePane for Documentation {
|
||||
fn title(&self) -> WidgetText {
|
||||
"Documentation".into()
|
||||
}
|
||||
|
||||
fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option<Action> {
|
||||
egui::ScrollArea::vertical()
|
||||
.id_salt("documentation")
|
||||
.max_width(1000.0)
|
||||
.show(ui, |ui| {
|
||||
if let Some(buffer) = &self.buffer {
|
||||
CommonMarkViewer::new().show(ui, &mut self.cache, buffer);
|
||||
} else {
|
||||
CommonMarkViewer::new().show(ui, &mut self.cache, "# No content loaded\nOpen a markdown file here!");
|
||||
}
|
||||
});
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Documentation {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
buffer: None,
|
||||
cache: CommonMarkCache::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Documentation {
|
||||
pub fn open(path: &Path) -> Self {
|
||||
Self {
|
||||
buffer: Some(std::fs::read_to_string(path).unwrap()),
|
||||
cache: CommonMarkCache::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self) -> &mut Option<String> {
|
||||
&mut self.buffer
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
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;
|
||||
|
||||
pub struct Editor {
|
||||
buffer: String,
|
||||
filename: Option<PathBuf>,
|
||||
|
||||
editor: CodeEditor,
|
||||
dialog: FileDialog,
|
||||
visible: bool,
|
||||
}
|
||||
|
||||
impl Default for Editor {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
buffer: String::new(),
|
||||
filename: None,
|
||||
editor: CodeEditor::default().with_fontsize(12.0).with_theme(THEME).with_numlines(true),
|
||||
dialog: FileDialog::new(),
|
||||
visible: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
pub fn new_open(path: &Path) -> Self {
|
||||
Self {
|
||||
buffer: std::fs::read_to_string(path).unwrap(),
|
||||
filename: Some(path.to_path_buf()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn filename_mut(&mut self) -> &mut Option<PathBuf> {
|
||||
&mut self.filename
|
||||
}
|
||||
|
||||
pub fn filename(&self) -> Option<&PathBuf> {
|
||||
self.filename.as_ref()
|
||||
}
|
||||
|
||||
pub fn get_buffer(&mut self) -> &mut String {
|
||||
&mut self.buffer
|
||||
}
|
||||
}
|
||||
|
||||
impl TilePane for Editor {
|
||||
fn title(&self) -> egui::WidgetText {
|
||||
self.filename.clone()
|
||||
.map(|f| f.file_name().unwrap().to_str().unwrap().into())
|
||||
.unwrap_or("New File*".into())
|
||||
}
|
||||
|
||||
fn ui(&mut self, ui: &mut Ui, tile_id: TileId) -> Option<Action> {
|
||||
|
||||
egui::Panel::top("panel").show(ui, |ui| {
|
||||
if let Some(filename) = self.filename.as_ref() {
|
||||
ui.label(filename.display().to_string());
|
||||
} else {
|
||||
ui.label("New File*");
|
||||
}
|
||||
});
|
||||
|
||||
let mut action = None;
|
||||
|
||||
self.editor.show(ui, &mut self.buffer, &syntax::dsa());
|
||||
|
||||
if ui.input(|i| i.key_pressed(Key::S) && i.modifiers.ctrl) {
|
||||
if let Some(filename) = self.filename.clone() {
|
||||
action = Some(Action::SaveFile(filename, tile_id));
|
||||
} else {
|
||||
action = Some(Action::RequestSaveAs(tile_id));
|
||||
}
|
||||
};
|
||||
|
||||
action
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any { self }
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
pub mod documentation;
|
||||
pub mod editor;
|
||||
pub mod about;
|
||||
|
||||
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;
|
||||
|
||||
/// 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 {
|
||||
fn title(&self) -> egui::WidgetText;
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _tile_id: TileId) -> Option<Action>;
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
|
||||
fn menu(&mut self, _ui: &mut egui::Ui, _tile_id: TileId) -> Option<Action> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
type Pane = Box<dyn TilePane>;
|
||||
|
||||
#[derive(Default)]
|
||||
struct TileBehavior {
|
||||
pending_action: Option<Action>,
|
||||
}
|
||||
|
||||
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,
|
||||
tile_id: TileId,
|
||||
pane: &mut Box<dyn TilePane>,
|
||||
) -> UiResponse {
|
||||
egui::Frame::new()
|
||||
.inner_margin(egui::Margin::same(10))
|
||||
.stroke(ui.visuals().widgets.active.bg_stroke)
|
||||
.show(ui, |ui| {
|
||||
if let Some(action) = pane.ui(ui, tile_id) {
|
||||
self.pending_action = Some(action);
|
||||
}
|
||||
});
|
||||
|
||||
UiResponse::None
|
||||
}
|
||||
|
||||
|
||||
fn tab_title_for_pane(&mut self, pane: &Pane) -> egui::WidgetText {
|
||||
pane.title()
|
||||
}
|
||||
|
||||
fn simplification_options(&self) -> SimplificationOptions {
|
||||
SimplificationOptions {
|
||||
// Keep the tab bar around even with only one tab, and don't let
|
||||
// panes get dragged out into their own floating windows.
|
||||
all_panes_must_have_tabs: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A single top-level tab strip that tools can add tabs to at runtime.
|
||||
pub struct CentralTiles {
|
||||
tree: Tree<Pane>,
|
||||
behavior: TileBehavior,
|
||||
}
|
||||
|
||||
impl CentralTiles {
|
||||
pub fn new() -> Self {
|
||||
let mut tiles = Tiles::<Pane>::default();
|
||||
let root = tiles.insert_tab_tile(vec![]);
|
||||
Self {
|
||||
tree: Tree::empty("central_tiles"),
|
||||
behavior: TileBehavior::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn active_tile(&self) -> Option<TileId> {
|
||||
match self.tree.root.and_then(|root| self.tree.tiles.get(root)) {
|
||||
Some(Tile::Container(Container::Tabs(tabs))) => tabs.active,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains(&self, tile_id: TileId) -> bool {
|
||||
self.tree.tiles.get(tile_id).is_some()
|
||||
}
|
||||
|
||||
pub fn add_tab(&mut self, pane: impl TilePane) -> TileId {
|
||||
self.add_dyn_tab(Box::new(pane))
|
||||
}
|
||||
|
||||
|
||||
/// Add a new tab and make it active. Returns the `TileId` so the
|
||||
/// caller can hold onto it for later access via `pane_mut`.
|
||||
pub fn add_dyn_tab(&mut self, pane: Box<dyn TilePane>) -> TileId {
|
||||
let tile_id = self.tree.tiles.insert_pane(pane);
|
||||
|
||||
match self.tree.root {
|
||||
Some(root) => {
|
||||
if let Some(Tile::Container(Container::Tabs(tabs))) = self.tree.tiles.get_mut(root) {
|
||||
tabs.children.push(tile_id);
|
||||
tabs.active = Some(tile_id);
|
||||
} else {
|
||||
// Root exists but isn't a Tabs container for some reason —
|
||||
// wrap both under a fresh tab container rather than losing it.
|
||||
let new_root = self.tree.tiles.insert_tab_tile(vec![root, tile_id]);
|
||||
self.tree.root = Some(new_root);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Tree was empty (or got simplified down to nothing) —
|
||||
// this is the first tab, so create the root container now.
|
||||
let root = self.tree.tiles.insert_tab_tile(vec![tile_id]);
|
||||
self.tree.root = Some(root);
|
||||
}
|
||||
}
|
||||
|
||||
tile_id
|
||||
}
|
||||
|
||||
/// Bring an already-open tab to the front (e.g. re-focus "Docs"
|
||||
/// instead of opening a duplicate tab).
|
||||
pub fn activate_tab(&mut self, tile_id: TileId) {
|
||||
self.tree.make_active(|id, _tile| id == tile_id);
|
||||
}
|
||||
|
||||
/// Reach into a specific pane's concrete type by id.
|
||||
pub fn pane_mut<T: 'static>(&mut self, tile_id: TileId) -> Option<&mut T> {
|
||||
match self.tree.tiles.get_mut(tile_id) {
|
||||
Some(Tile::Pane(pane)) => pane.as_any_mut().downcast_mut::<T>(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<Action> {
|
||||
self.behavior.pending_action = None;
|
||||
self.tree.ui(&mut self.behavior, ui);
|
||||
self.behavior.pending_action.take()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PanelFactory(pub String, pub Box<dyn Fn() -> Box<dyn TilePane>>);
|
||||
|
||||
impl PanelFactory {
|
||||
pub fn register<T, F>(name: &str, constructor: F) -> Self
|
||||
where
|
||||
T: TilePane + 'static,
|
||||
F: Fn() -> T + 'static,
|
||||
{
|
||||
Self(name.to_string(), Box::new(move || Box::new(constructor()) as Box<dyn TilePane>))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
use eframe::emath::Vec2;
|
||||
use egui::{Color32, Frame, Stroke};
|
||||
use egui::widget_style::{Classes, WidgetState};
|
||||
use emulator_core::processor::state::RunningState;
|
||||
|
||||
|
||||
pub struct PaddedWidget {
|
||||
text: String,
|
||||
color: Option<Color32>,
|
||||
padding: Option<Vec2>,
|
||||
widget_type: WidgetType
|
||||
}
|
||||
|
||||
enum WidgetType {
|
||||
Button,
|
||||
Indicator,
|
||||
Label
|
||||
}
|
||||
|
||||
impl PaddedWidget {
|
||||
pub fn button(text: impl AsRef<str>) -> Self {
|
||||
Self {
|
||||
text: text.as_ref().to_string(),
|
||||
color: None,
|
||||
padding: None,
|
||||
widget_type: WidgetType::Button
|
||||
}
|
||||
}
|
||||
|
||||
pub fn indicator(i: impl StatusIndicator) -> Self {
|
||||
Self {
|
||||
text: i.label().to_string(),
|
||||
color: Some(i.color()),
|
||||
padding: None,
|
||||
widget_type: WidgetType::Indicator
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(text: String) -> Self {
|
||||
Self {
|
||||
text,
|
||||
color: None,
|
||||
padding: None,
|
||||
widget_type: WidgetType::Label
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color(&mut self, color: Color32) -> &mut Self {
|
||||
self.color = Some(color);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn padding(&mut self, padding: Vec2) -> &mut Self {
|
||||
self.padding = Some(padding);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) -> egui::Response {
|
||||
let text: egui::WidgetText = self.text.clone().into();
|
||||
let galley = text.into_galley(ui, None, f32::INFINITY, egui::TextStyle::Button);
|
||||
let desired_size = galley.size() + self.padding.unwrap_or(Vec2::new(14.0, 8.0)) * 2.0;
|
||||
|
||||
let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
|
||||
|
||||
if ui.is_rect_visible(rect) {
|
||||
// picks noninteractive/inactive/hovered/active automatically based on `response`
|
||||
|
||||
let visuals = match self.widget_type {
|
||||
WidgetType::Button => ui.style().interact(&response),
|
||||
_ => &ui.visuals().widgets.inactive,
|
||||
};
|
||||
|
||||
ui.painter().rect(
|
||||
rect,
|
||||
visuals.corner_radius,
|
||||
visuals.bg_fill,
|
||||
visuals.bg_stroke,
|
||||
egui::StrokeKind::Inside,
|
||||
);
|
||||
|
||||
let text_pos = rect.center() - galley.size() / 2.0;
|
||||
ui.painter().galley(text_pos, galley, self.color.unwrap_or(visuals.fg_stroke.color));
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn frame_button(ui: &mut egui::Ui, text: impl Into<egui::WidgetText>, color: egui::Color32, padding: Vec2) -> egui::Response {
|
||||
// let text: egui::WidgetText = text.into();
|
||||
// let galley = text.into_galley(ui, None, f32::INFINITY, egui::TextStyle::Button);
|
||||
// let desired_size = galley.size() + padding * 2.0;
|
||||
//
|
||||
// let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
|
||||
//
|
||||
// if ui.is_rect_visible(rect) {
|
||||
// // picks noninteractive/inactive/hovered/active automatically based on `response`
|
||||
// let visuals = ui.style().interact(&response);
|
||||
//
|
||||
// ui.painter().rect(
|
||||
// rect,
|
||||
// visuals.corner_radius,
|
||||
// visuals.bg_fill,
|
||||
// visuals.bg_stroke,
|
||||
// egui::StrokeKind::Inside,
|
||||
// );
|
||||
//
|
||||
// let text_pos = rect.center() - galley.size() / 2.0;
|
||||
// ui.painter().galley(text_pos, galley, color);
|
||||
// }
|
||||
//
|
||||
// response
|
||||
// }
|
||||
//
|
||||
// pub fn info_field(ui: &mut egui::Ui, text: impl Into<egui::WidgetText>, padding: egui::Vec2) -> egui::Response {
|
||||
// let text: egui::WidgetText = text.into();
|
||||
// let galley = text.into_galley(ui, None, f32::INFINITY, egui::TextStyle::Button);
|
||||
// let desired_size = galley.size() + padding * 2.0;
|
||||
//
|
||||
// let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
|
||||
//
|
||||
// if ui.is_rect_visible(rect) {
|
||||
// // Always use `inactive` — no hover highlight, no active/pressed feedback either.
|
||||
// // Response is still fully clickable; this only affects what gets drawn.
|
||||
// let visuals = &ui.visuals().widgets.inactive;
|
||||
//
|
||||
// ui.painter().rect(
|
||||
// rect,
|
||||
// visuals.corner_radius,
|
||||
// visuals.bg_fill,
|
||||
// visuals.bg_stroke,
|
||||
// egui::StrokeKind::Inside,
|
||||
// );
|
||||
//
|
||||
// let text_pos = rect.center() - galley.size() / 2.0;
|
||||
// ui.painter().galley(text_pos, galley, visuals.fg_stroke.color);
|
||||
// }
|
||||
//
|
||||
// response
|
||||
// }
|
||||
|
||||
|
||||
pub fn bordered_frame<R>(
|
||||
ui: &mut egui::Ui,
|
||||
padding: egui::Vec2,
|
||||
add_contents: impl FnOnce(&mut egui::Ui) -> R,
|
||||
) -> egui::InnerResponse<R> {
|
||||
let visuals = ui.visuals().widgets.inactive; // same visuals a normal button uses
|
||||
|
||||
egui::Frame::new()
|
||||
.inner_margin(egui::Margin::symmetric(padding.x as i8, padding.y as i8))
|
||||
.stroke(visuals.bg_stroke)
|
||||
.corner_radius(visuals.corner_radius)
|
||||
.fill(visuals.bg_fill)
|
||||
.show(ui, add_contents)
|
||||
}
|
||||
|
||||
pub trait StatusIndicator {
|
||||
fn ui(&mut self, ui: &mut egui::Ui, padding: egui::Vec2) -> egui::Response {
|
||||
bordered_frame(ui, padding, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
let dot_size = 8.0;
|
||||
let (rect, _) = ui.allocate_exact_size(
|
||||
egui::vec2(dot_size, dot_size),
|
||||
egui::Sense::hover(),
|
||||
);
|
||||
ui.painter().circle_filled(rect.center(), dot_size / 2.0, self.color());
|
||||
ui.label( self.label() );
|
||||
})
|
||||
.response
|
||||
})
|
||||
.response
|
||||
}
|
||||
|
||||
|
||||
fn color(&self) -> egui::Color32;
|
||||
|
||||
fn label(&self) -> String;
|
||||
}
|
||||
|
||||
|
||||
|
||||
impl StatusIndicator for RunningState {
|
||||
fn color(&self) -> egui::Color32 {
|
||||
match self {
|
||||
RunningState::Running => egui::Color32::from_rgb(0x4c, 0xaf, 0x50),
|
||||
RunningState::Paused => egui::Color32::from_rgb(0xff, 0xc1, 0x07),
|
||||
RunningState::Halted => egui::Color32::from_rgb(0xff, 0xc1, 0x07),
|
||||
RunningState::Breakpoint(_) => egui::Color32::from_rgb(0xe5, 0x39, 0x35),
|
||||
RunningState::Shutdown => egui::Color32::from_rgb(0xff, 0xc1, 0x07),
|
||||
}
|
||||
}
|
||||
|
||||
fn label(&self) -> String {
|
||||
match self {
|
||||
RunningState::Running => "Running".into(),
|
||||
RunningState::Paused => "Paused".into(),
|
||||
RunningState::Halted => "Halted".into(),
|
||||
RunningState::Breakpoint(addr) => format!("Breakpoint @ {addr:#06x}"),
|
||||
RunningState::Shutdown => "Shutdown".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,11 @@ use eframe::NativeOptions;
|
||||
use eframe::egui;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
use egui::Order::Debug;
|
||||
|
||||
mod components;
|
||||
pub mod debugger;
|
||||
|
||||
use components::{
|
||||
controller::Controller, display::Display, editor::Editor, framebuffer::FrameBuffer, memory::MemoryInspector,
|
||||
registers::Registers, serial::Serial, Component,
|
||||
@@ -13,15 +16,27 @@ use emulator_core::{
|
||||
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();
|
||||
|
||||
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(DsaUi::new(cc, state, mem_handle)))),
|
||||
Box::new(|cc| Ok(Box::new(DebuggerUi::new(cc, state, mem_handle)))),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
fn e() -> i32 {
|
||||
return 5
|
||||
}
|
||||
|
||||
impl eframe::App for DsaUi {
|
||||
|
||||
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
|
||||
@@ -96,6 +111,7 @@ impl DsaUi {
|
||||
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);
|
||||
|
||||
@@ -108,7 +124,7 @@ impl DsaUi {
|
||||
app
|
||||
}
|
||||
|
||||
fn configure_appearance(ctx: &egui::Context) {
|
||||
pub fn configure_appearance(ctx: &egui::Context) {
|
||||
// configure appearance of UI elements
|
||||
let mut visuals = egui::Visuals::dark();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
include print: "./print.dsa"
|
||||
db msg: "hello world"
|
||||
dw stack: 0x1000
|
||||
dw stack: 0x10000
|
||||
_init:
|
||||
ldw stack, bpr
|
||||
mov bpr, spr
|
||||
|
||||
Binary file not shown.
@@ -121,10 +121,11 @@ print_byte: func
|
||||
|
||||
stb rg0, rg1
|
||||
addi rg1, 1
|
||||
stw rg1, current
|
||||
|
||||
pop rg1
|
||||
pop rg0
|
||||
jmp _end
|
||||
return
|
||||
|
||||
// ------------------------------------------
|
||||
// prints the value of arg[0] to the screen in hex.
|
||||
|
||||
Reference in New Issue
Block a user