editor works

This commit is contained in:
2025-06-15 21:21:02 +01:00
parent 5d1ea86cdd
commit 277f210b3e
8 changed files with 641 additions and 80 deletions
+326
View File
@@ -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
View File
@@ -1,4 +1,5 @@
pub mod control_unit;
pub mod editor;
pub mod interface;
pub mod memory_inspector;
pub mod menu;