editor works
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use egui::{Align, Context, Layout, Ui};
|
||||
use rfd::FileDialog;
|
||||
|
||||
use crate::emulator::{
|
||||
system::model::{Command, State},
|
||||
ui::interface::Component,
|
||||
};
|
||||
|
||||
pub struct Editor {
|
||||
filename: String,
|
||||
text: String,
|
||||
output: Vec<u8>,
|
||||
sender: Sender<Command>,
|
||||
cursor_col: usize,
|
||||
cursor_line: usize,
|
||||
visible: bool,
|
||||
load_offset: u32,
|
||||
offset_str: String,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
impl Component for Editor {
|
||||
fn name(&self) -> &'static str {
|
||||
"Editor"
|
||||
}
|
||||
|
||||
fn visible(&mut self) -> &mut bool {
|
||||
&mut self.visible
|
||||
}
|
||||
|
||||
fn category(&self) -> super::interface::Category {
|
||||
super::interface::Category::Programming
|
||||
}
|
||||
|
||||
fn render(&mut self, state: &mut State, ui: &mut Ui, ctx: &Context) {
|
||||
ui.vertical(|ui| {
|
||||
// Top bar
|
||||
self.render_toolbar(state, ui, ctx);
|
||||
|
||||
ui.add_space(4.0); // Add some spacing instead of just a separator
|
||||
ui.separator();
|
||||
|
||||
let remaining_height = f32::max(ui.available_height() - 100.0, 100.0);
|
||||
|
||||
ui.allocate_ui_with_layout(
|
||||
egui::Vec2::new(ui.available_width(), remaining_height),
|
||||
Layout::left_to_right(Align::Min),
|
||||
|ui| {
|
||||
self.render_editor(state, ui, ctx);
|
||||
ui.separator();
|
||||
self.render_output(state, ui, ctx);
|
||||
},
|
||||
);
|
||||
|
||||
ui.label(format!("Ln {}, Col {}", self.cursor_line, self.cursor_col));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
pub fn new(sender: Sender<Command>) -> Self {
|
||||
Self {
|
||||
filename: String::new(),
|
||||
text: String::new(),
|
||||
output: Vec::new(),
|
||||
sender,
|
||||
cursor_col: 1,
|
||||
cursor_line: 1,
|
||||
visible: false,
|
||||
load_offset: 0,
|
||||
offset_str: String::new(),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_output(&mut self, _state: &mut State, ui: &mut Ui, _ctx: &Context) {
|
||||
// Output area with synchronized scrolling
|
||||
egui::ScrollArea::vertical()
|
||||
.id_salt("output_scroll")
|
||||
.max_width(300.0)
|
||||
.show(ui, |ui| {
|
||||
if self.output.is_empty() {
|
||||
ui.label(
|
||||
egui::RichText::new("No output data")
|
||||
.font(egui::FontId::monospace(12.0))
|
||||
.color(egui::Color32::GRAY),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
egui::Grid::new("output_grid")
|
||||
.num_columns(4)
|
||||
.spacing([20.0, 2.0]) // Horizontal and vertical spacing
|
||||
.striped(false)
|
||||
.show(ui, |ui| {
|
||||
// Process bytes in chunks of 4
|
||||
for (line_num, chunk) in self.output.chunks(4).enumerate() {
|
||||
let address = line_num * 4;
|
||||
|
||||
// Convert chunk to u32 (little-endian)
|
||||
let mut bytes = [0u8; 4];
|
||||
for (i, &byte) in chunk.iter().enumerate() {
|
||||
if i < 4 {
|
||||
bytes[i] = byte;
|
||||
}
|
||||
}
|
||||
let value = u32::from_le_bytes(bytes);
|
||||
|
||||
// Address column
|
||||
ui.with_layout(
|
||||
egui::Layout::left_to_right(egui::Align::Center),
|
||||
|ui| {
|
||||
ui.set_min_width(80.0);
|
||||
let style = ui.style_mut();
|
||||
style.visuals.widgets.inactive.bg_fill =
|
||||
egui::Color32::from_gray(30);
|
||||
ui.label(
|
||||
egui::RichText::new(format!("0x{:04X}", address))
|
||||
.font(egui::FontId::monospace(12.0)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// Individual bytes column
|
||||
let byte_str = chunk
|
||||
.iter()
|
||||
.map(|b| format!("{:02X}", b))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
|
||||
ui.label(
|
||||
egui::RichText::new(format!("{:<11}", byte_str))
|
||||
.font(egui::FontId::monospace(12.0))
|
||||
.color(egui::Color32::from_rgb(200, 200, 255)),
|
||||
);
|
||||
|
||||
// Hex column
|
||||
ui.label(
|
||||
egui::RichText::new(format!("0x{:08X}", value))
|
||||
.font(egui::FontId::monospace(12.0))
|
||||
.color(egui::Color32::from_rgb(255, 200, 200)),
|
||||
);
|
||||
|
||||
// Decimal column
|
||||
ui.label(
|
||||
egui::RichText::new(format!("{:10}", value))
|
||||
.font(egui::FontId::monospace(12.0))
|
||||
.color(egui::Color32::from_rgb(200, 255, 200)),
|
||||
);
|
||||
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn render_editor(&mut self, _state: &mut State, ui: &mut Ui, _ctx: &Context) {
|
||||
let available_width = ui.available_width();
|
||||
|
||||
// Main editor area with synchronized scrolling
|
||||
egui::ScrollArea::vertical()
|
||||
.max_width(available_width - 400.0)
|
||||
.id_salt("editor_scroll")
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
// Line numbers column
|
||||
let line_count = self.text.lines().count();
|
||||
ui.vertical(|ui| {
|
||||
ui.set_width(50.0);
|
||||
ui.style_mut().visuals.widgets.inactive.bg_fill =
|
||||
egui::Color32::from_gray(30);
|
||||
|
||||
// Calculate line height to match text editor
|
||||
let line_height = ui.text_style_height(&egui::TextStyle::Monospace);
|
||||
|
||||
for line_num in 1..=line_count {
|
||||
let line_response = ui.allocate_response(
|
||||
egui::vec2(50.0, line_height),
|
||||
egui::Sense::hover(),
|
||||
);
|
||||
|
||||
ui.painter().text(
|
||||
line_response.rect.left_center() + egui::vec2(5.0, 0.0),
|
||||
egui::Align2::LEFT_CENTER,
|
||||
format!("{:3}", line_num),
|
||||
egui::FontId::monospace(12.0),
|
||||
ui.style().visuals.text_color(),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
// Text editor area
|
||||
ui.vertical(|ui| {
|
||||
let available_size = ui.available_size();
|
||||
let response = ui.add_sized(
|
||||
available_size,
|
||||
egui::TextEdit::multiline(&mut self.text)
|
||||
.font(egui::TextStyle::Monospace)
|
||||
.margin(egui::vec2(5.0, 0.0))
|
||||
.code_editor(),
|
||||
);
|
||||
|
||||
// Update cursor position when text changes
|
||||
if response.changed() {
|
||||
// Simple but functional cursor tracking
|
||||
let lines = self.text.lines().collect::<Vec<_>>();
|
||||
self.cursor_line = lines.len().max(1);
|
||||
|
||||
// Get the length of the last line for column position
|
||||
if let Some(last_line) = lines.last() {
|
||||
self.cursor_col = last_line.chars().count() + 1;
|
||||
} else {
|
||||
self.cursor_col = 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn render_toolbar(&mut self, _state: &mut State, ui: &mut Ui, _ctx: &Context) {
|
||||
ui.horizontal(|ui| {
|
||||
// current filename
|
||||
ui.label(format!("File: {}", self.filename));
|
||||
|
||||
// error display
|
||||
ui.label(
|
||||
egui::RichText::new(self.error.clone().unwrap_or("".to_string()))
|
||||
.color(egui::Color32::RED),
|
||||
);
|
||||
|
||||
// number of lines in the file
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let line_count = self.text.lines().count();
|
||||
ui.label(format!("Lines: {}", line_count));
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
if ui.button("Open").clicked() {
|
||||
if let Some(path) = FileDialog::new()
|
||||
.add_filter("dsafiles", &["dsa", "dsb", "dsc", "dsd"])
|
||||
.add_filter("all", &["*"])
|
||||
.set_directory(std::env::current_dir().unwrap())
|
||||
.pick_file()
|
||||
{
|
||||
if let Ok(content) = std::fs::read_to_string(&path) {
|
||||
self.text = content;
|
||||
self.filename = path.display().to_string();
|
||||
}
|
||||
}
|
||||
|
||||
self.output = Vec::new();
|
||||
}
|
||||
|
||||
// Saves the current file
|
||||
if ui.button("Save").clicked() {
|
||||
if let Some(path) = FileDialog::new()
|
||||
.add_filter("dsafiles", &["dsa", "dsb", "dsc", "dsd"])
|
||||
.add_filter("all", &["*"])
|
||||
.set_directory(std::env::current_dir().unwrap())
|
||||
.save_file()
|
||||
{
|
||||
if let Err(e) = std::fs::write(&path, &self.text) {
|
||||
self.error = Some(format!("Failed to save file: {}", e));
|
||||
} else {
|
||||
self.filename = path.display().to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// builds the current file
|
||||
if ui.button("Build").clicked() {
|
||||
self.output = vec![0x00; 256];
|
||||
}
|
||||
|
||||
// Loads the generated binary into the assembler at the provided offset
|
||||
if ui.button("Load").clicked() {
|
||||
if self.error.is_some() {
|
||||
self.error = Some("Can't load program at invalid offset!".to_string());
|
||||
}
|
||||
|
||||
self.sender
|
||||
.send(Command::Write(self.load_offset, self.output.clone()))
|
||||
.unwrap_or_else(|_| self.error = Some("Failed to send command".to_string()));
|
||||
}
|
||||
|
||||
// Entry widget to enter a load offset
|
||||
if ui.text_edit_singleline(&mut self.offset_str).changed() {
|
||||
if let Some(offset) = parse_address(&self.offset_str) {
|
||||
self.load_offset = offset;
|
||||
self.error = None;
|
||||
} else {
|
||||
self.error = Some("Invalid offset".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Resets the emulator and all attached devices
|
||||
if ui.button("Reset Emulator").clicked() {
|
||||
self.sender
|
||||
.send(Command::Reset)
|
||||
.unwrap_or_else(|_| self.error = Some("Failed to send command".to_string()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_address(address: &str) -> Option<u32> {
|
||||
if address.starts_with("0x") {
|
||||
u32::from_str_radix(&address[2..], 16).ok()
|
||||
} else if address.starts_with("0b") {
|
||||
u32::from_str_radix(&address[2..], 2).ok()
|
||||
} else if address.starts_with("0o") {
|
||||
u32::from_str_radix(&address[2..], 8).ok()
|
||||
} else {
|
||||
address.parse::<u32>().ok()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod control_unit;
|
||||
pub mod editor;
|
||||
pub mod interface;
|
||||
pub mod memory_inspector;
|
||||
pub mod menu;
|
||||
|
||||
Reference in New Issue
Block a user