rewrote assembler backend to support custom executable format (DSE) and also refactored (moving crates around)

This commit is contained in:
2026-07-16 00:34:13 +01:00
parent 7a84073bc4
commit a4d42bdad6
60 changed files with 1486 additions and 1669 deletions
+44
View File
@@ -0,0 +1,44 @@
[package]
name = "emulator_ui"
version = "0.3.0"
edition.workspace = true
authors.workspace = true
[[bin]]
name = "dsa"
path = "src/main.rs"
[lib]
name = "dsa"
path = "src/lib.rs"
[dev-dependencies]
criterion = { version = "0.8.2", features = ["html_reports"] }
[dependencies]
arc-swap = "1.8.2"
clap = { version = "4.5.60", features = ["derive"] }
fxhash = "0.2.1"
ron = "0.12.0"
serde = { version = "1.0.228", features = ["derive"] }
common = { path = "../../common" }
egui = "0.35.0"
eframe = "0.35.0"
egui_tiles = "0.16.0"
egui_code_editor = "0.3.7"
egui_file = "0.27.0"
dirs = "6.0.0"
assembler = { path = "../../assembler" }
egui_extras = "0.35.0"
emulator_core = { version = "0.3.0", path = "../emulator_core" }
[features]
default = ["mainstore-prealloc"]
# Memory Bank Features
mainstore-bulkalloc = [] # Fastest for Writes
mainstore-prealloc = [] # Fastest for Reads
mainstore-stackarray = [] # Slightly outperforms ArrayMap but requires a large stack
mainstore-arraymap = [] # Simple implementation
mainstore-hashmap = [] # Old implementation, no raw pointers. Uses a hashmap
+61
View File
@@ -0,0 +1,61 @@
use std::{fs, path::PathBuf};
use clap::Parser;
use emulator_core::config::{IoMap, IoMapping, MemoryMap};
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
pub struct DsaArgs {
/// Memory map to use on boot.
/// Overrides default value.
#[arg(long = "mmap")]
memory_map: Option<PathBuf>,
#[arg(long = "bin")]
binary: Option<PathBuf>,
#[arg(long = "iomap")]
io_map: Option<PathBuf>,
#[arg(long = "headless")]
headless: bool,
}
impl DsaArgs {
pub fn headless(&self) -> bool {
self.headless
}
pub fn get_memory_map(&self) -> MemoryMap {
self.memory_map
.as_ref()
.and_then(|m| {
fs::read_to_string(m)
.ok()
.and_then(|map| ron::from_str(&map).ok())
})
.unwrap_or_else(|| {
MemoryMap::default()
})
}
pub fn get_binary(&self) -> Option<Vec<u8>> {
self.binary
.clone()
.and_then(|path| Some(fs::read(path).unwrap()))
}
pub fn get_io_map(&self) -> IoMap {
self.io_map
.as_ref()
.and_then(|m| {
fs::read_to_string(m)
.ok()
.and_then(|map| ron::from_str(&map).ok())
})
.unwrap_or_else(|| {
IoMap::default()
})
}
}
+7
View File
@@ -0,0 +1,7 @@
#![feature(likely_unlikely)]
#![feature(inherent_associated_types)]
#![feature(str_as_str)]
pub mod ui;
pub mod args;
+68
View File
@@ -0,0 +1,68 @@
use clap::Parser;
use common::prelude::Instruction;
use std::thread;
use dsa::args::DsaArgs;
use dsa::ui::run_app;
use emulator_core::{
memory::ram::Page,
processor::processor::Emulator
};
const STACK_SIZE: usize = 1024 * 1024 * 16;
fn main() {
let args = DsaArgs::parse();
let mut emulator = Emulator::new().apply_memory_map(args.get_memory_map());
let mem_handle = emulator.memory_mut().clone();
let state = emulator.state_handle();
if let Some(bin) = args.get_binary() {
for (i, ch) in bin.chunks(4).enumerate() {
let instruction = Instruction(u32::from_le_bytes(ch.try_into().unwrap()));
let hex = ch
.iter()
.map(|b| format!("{:02X}", b))
.collect::<Vec<_>>()
.join(" ");
let chars = ch
.iter()
.map(|b| {
if b.is_ascii_graphic() {
*b as char
} else {
'.'
}
})
.collect::<String>();
println!("{:04X}: {:<12} {:4} {:?}", i * 4, hex, chars, instruction);
}
let program: Vec<Page> = Page::paginate(&bin).collect();
for (i, page) in program.iter().enumerate() {
emulator.memory_mut().write_page(i as u32 * 4096, &page);
}
}
let _runner = thread::Builder::new()
.stack_size(STACK_SIZE)
.spawn(move || emulator.run().unwrap())
.unwrap();
if args.headless() {
run_server(&args);
} else {
run_app(state, mem_handle).unwrap()
}
}
fn run_server(args: &DsaArgs) {
if args.headless() {
println!("Running in headless mode");
} else {
println!("Running in GUI mode");
}
}
@@ -0,0 +1,113 @@
use std::sync::{Arc, atomic::Ordering};
use super::Component;
use common::prelude::Register;
use emulator_core::{
memory::ram::MemoryBank,
processor::state::SharedState
};
pub struct Controller {
state: Arc<SharedState>,
mem: MemoryBank,
visible: bool,
}
impl Controller {
pub fn new(state: Arc<SharedState>, mem: MemoryBank) -> Self {
Self {
state,
mem,
visible: false,
}
}
}
impl Component for Controller {
fn title(&self) -> &str {
"Control Panel"
}
fn visible(&mut self) -> &mut bool {
&mut self.visible
}
fn ui(&mut self, ui: &mut egui::Ui) {
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);
}
// 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());
// });
// }
// 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);
}
ui.separator();
// 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
// }
// }
// Status info
ui.label(format!(
"Status: {}",
match paused {
true => "Paused",
false => "Running",
}
));
ui.label(format!("Halted: {}", state.halted));
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
};
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,127 @@
use egui::{Color32, FontId, Vec2};
use emulator_core::{
memory::PhysAddr,
memory::ram::{MemoryBank, Page}
};
use super::Component;
pub struct Display {
width: usize,
height: usize,
addr: PhysAddr,
mem: MemoryBank,
buffer: Vec<u8>,
visible: bool,
}
impl Display {
pub fn new(width: u32, height: u32, addr: PhysAddr, mem: MemoryBank) -> Self {
Self {
width: width as usize,
height: height as usize,
addr,
mem,
buffer: Vec::new(),
visible: true,
}
}
pub fn visible(&mut self) -> &mut bool {
&mut self.visible
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
fn internal_read(&self) -> Vec<u8> {
let pages = (0..((self.width * self.height).div_ceil(Page::SIZE)))
.map(|i| {
let page = self.mem.read_page(self.addr + i as u32 * 4096);
page.as_slice().to_owned()
})
.flatten()
.take((self.width * self.height) as usize)
.collect::<Vec<u8>>();
pages
}
pub fn changed(&mut self) -> bool {
let temp = self.internal_read();
if temp != self.buffer {
self.buffer = temp;
return true;
}
false
}
pub fn read(&mut self) -> Vec<u8> {
self.internal_read()
}
}
impl Component for Display {
fn title(&self) -> &str {
"Display"
}
fn visible(&mut self) -> &mut bool {
self.visible()
}
fn ui(&mut self, ui: &mut egui::Ui) {
let display_w = self.width();
let display_h = self.height();
let data = self.read();
let font_id = FontId::monospace(12.0);
let char_width = ui.fonts_mut(|f| f.glyph_width(&font_id, 'W'));
let line_height = ui.fonts_mut(|f| f.row_height(&font_id));
#[expect(clippy::cast_precision_loss)]
let display_size = Vec2::new(
char_width * display_w as f32,
line_height * display_h as f32,
);
let (rect, _response) = ui.allocate_exact_size(display_size, egui::Sense::all());
// ui.painter().rect_filled(rect, 0.0, Color32::BLACK);
// Draw text
for y in 0..display_h {
let mut row_text = String::with_capacity(display_w);
for x in 0..display_w {
let index = y * display_w + x;
if index < data.len() {
let byte = data[index];
let ch = if (32..=126).contains(&byte) {
byte as char
} else {
' '
};
row_text.push(ch);
} else {
row_text.push(' ');
}
}
#[expect(clippy::cast_precision_loss)]
let text_pos = rect.min + Vec2::new(0.0, y as f32 * line_height);
ui.painter().text(
text_pos,
egui::Align2::LEFT_TOP,
row_text,
font_id.clone(),
Color32::WHITE,
);
}
}
}
@@ -0,0 +1,695 @@
use std::fmt::Write;
use std::fs::{self};
use std::io::Cursor;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::{
ffi::OsStr,
path::{Path, PathBuf},
};
pub mod syntax;
use super::Component;
use assembler::prelude::Assembler;
use common::formats::binary_dse::DseExecutable;
use common::prelude::Instruction;
use egui::{Align, Context, Key, Layout, Ui};
use egui_code_editor::{CodeEditor, ColorTheme, Completer, Syntax};
use egui_extras::{Column, TableBuilder};
use egui_file::FileDialog;
use emulator_core::{
memory::ram::{MemoryBank, Page},
processor::state::SharedState
};
pub struct Editor {
state: Arc<SharedState>,
memory: MemoryBank,
// editor state
path: Option<PathBuf>,
unsaved: bool,
text: String,
buffer: String,
// editor widget
editor: CodeEditor,
completer: Completer,
syntax: Syntax,
// output / loading
output: Output,
load_offset: u32,
offset_str: String,
// file dialogs
open_file_dialog: Option<FileDialog>,
save_file_dialog: Option<FileDialog>,
// other
visible: bool,
show_output: bool,
error: Option<String>,
}
impl Component for Editor {
fn title(&self) -> &'static str {
"Editor"
}
fn visible(&mut self) -> &mut bool {
&mut self.visible
}
fn ui(&mut self, ui: &mut Ui) {
if self.buffer != self.text {
self.unsaved = true;
}
ui.vertical(|ui| {
// Top bar
if ui.input(|i| i.key_pressed(Key::S) && i.modifiers.ctrl) {
self.save();
}
self.render_toolbar(ui);
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| {
if self.show_output {
egui::Panel::right("Editor Output").resizable(false).show_inside(ui, |ui| {
self.output.ui(ui);
});
}
self.render_editor(ui);
},
);
self.render_bottom_bar(ui);
});
}
}
impl Editor {
#[must_use]
pub fn new(state: Arc<SharedState>, memory: MemoryBank) -> Self {
Self {
memory,
state,
path: None,
text: String::new(),
buffer: String::new(),
completer: Completer::new_with_syntax(&Syntax::default()).with_user_words(),
syntax: Syntax::default(),
editor: CodeEditor::default()
.id_source("editor")
.with_fontsize(12.0)
.with_rows(0)
.with_theme(THEME)
.with_numlines(true),
output: Output::new(),
unsaved: true,
visible: false,
load_offset: 0,
offset_str: String::new(),
error: None,
open_file_dialog: None,
save_file_dialog: None,
show_output: false,
}
}
fn filename(&self) -> &str {
if let Some(path) = &self.path {
return path
.file_name()
.unwrap_or_else(|| OsStr::new("Unnamed!"))
.to_str()
.unwrap_or_else(|| unreachable!("File name should be valid UTF-8."));
}
"Unnamed!"
}
fn extension(&self) -> &str {
if let Some(path) = &self.path {
return path
.extension()
.unwrap_or_else(|| OsStr::new("Unknown!"))
.to_str()
.unwrap_or_else(|| unreachable!("File name should be valid UTF-8."));
}
"Unknown!"
}
fn save(&mut self) {
if self.open_file_dialog.is_some() {
// TODO: Flash an error stating you can only have one menu open at once.
self.open_file_dialog = None;
}
if let Some(path) = &self.path {
// Save to existing path
self.buffer = self.text.clone();
let text = if path.extension().is_some_and(|ext| ext == "dsb") {
let mut res = Vec::new();
for line in self.text.lines() {
for line in line.split_whitespace() {
match u32::from_str_radix(line, 16) {
Ok(num) => res.push(num),
Err(e) => {
self.error = Some(format!("Failed to parse file: {e}"));
return;
}
}
}
}
res.into_iter()
.flat_map(u32::to_be_bytes)
.collect::<Vec<u8>>()
} else {
self.text.as_bytes().to_vec()
};
if let Err(why) = std::fs::write(path, text) {
self.error = Some(format!("Failed to save file: {why}"));
} else {
self.unsaved = false;
}
} else {
// Open the save dialog.
let work_dir = std::env::current_dir().unwrap_or_else(|_| {
dirs::home_dir()
.expect("Couldn't get your current working directory or your home directory.")
});
if self.save_file_dialog.is_none() {
let mut dialog = FileDialog::save_file().initial_path(work_dir);
dialog.open();
self.save_file_dialog = Some(dialog);
}
}
}
fn open(&mut self) {
let work_dir = std::env::current_dir().unwrap_or_else(|_| {
dirs::home_dir()
.expect("Couldn't get your current working directory or your home directory.")
});
if self.save_file_dialog.is_some() {
// TODO: Flash an error stating you can only have one menu open at once.
self.save_file_dialog = None;
}
if self.open_file_dialog.is_none() {
let mut dialog = FileDialog::open_file();
if let Some(p) = &self.path
&& let Some(path) = p.parent().map(Path::to_path_buf)
{
dialog.set_path(path);
} else {
dialog.set_path(work_dir);
}
dialog.open();
self.open_file_dialog = Some(dialog);
}
self.syntax = match self.extension() {
"dsa" => syntax::dsa(),
"dsc" => syntax::dsc(),
"rs" => Syntax::rust(),
_ => Syntax::default(),
};
self.completer = Completer::new_with_syntax(&self.syntax).with_user_words();
}
fn handle_file_dialogs(&mut self, ui: &Ui) {
// Handle open dialog
if let Some(dialog) = &mut self.open_file_dialog
&& dialog.show(ui).selected()
{
if let Some(file) = dialog.path() {
// check if the file is a binary file
if file.extension().is_some_and(|ext| ext == "dsb") {
match std::fs::read(file) {
Ok(content) => {
let mut res = String::new();
for (i, b) in content.iter().enumerate() {
_ = write!(res, "{b:02x}");
if i % 4 == 3 {
res.push('\n');
}
}
self.text = res.clone();
self.buffer = res;
self.path = Some(file.to_path_buf());
self.unsaved = false;
self.error = None;
}
Err(e) => {
self.error = Some(format!("Failed to read file: {e}"));
}
}
} else {
match std::fs::read_to_string(file) {
Ok(content) => {
self.text = content.clone();
self.buffer = content;
self.path = Some(file.to_path_buf());
self.unsaved = false;
self.error = None;
}
Err(e) => {
self.error = Some(format!("Failed to read file: {e}"));
}
}
}
}
self.open_file_dialog = None;
}
// Handle save dialog
if let Some(dialog) = &mut self.save_file_dialog
&& dialog.show(ui).selected()
{
if let Some(file) = dialog.path() {
self.buffer = self.text.clone();
let content = if file.extension().is_some_and(|ext| ext == "dsb") {
let mut res = Vec::new();
for line in self.text.lines() {
for line in line.split_whitespace() {
match u32::from_str_radix(line, 16) {
Ok(num) => res.push(num),
Err(e) => {
self.error = Some(format!("Failed to parse file: {e}"));
return;
}
}
}
}
res.into_iter()
.flat_map(u32::to_be_bytes)
.collect::<Vec<u8>>()
} else {
self.text.clone().as_bytes().to_vec()
};
match std::fs::write(file, content) {
Ok(()) => {
self.path = Some(file.to_path_buf());
self.unsaved = false;
self.error = None;
}
Err(e) => {
self.error = Some(format!("Failed to save file: {e}"));
}
}
}
self.save_file_dialog = None;
}
}
fn render_editor(&mut self, ui: &mut Ui) {
let available_width = ui.available_width();
let mut editor = self.editor.clone().desired_width(available_width);
editor.show_with_completer(ui, &mut self.text, &self.syntax, &mut self.completer);
}
fn render_bottom_bar(&self, ui: &mut Ui) {
ui.horizontal(|ui| {
// error display
ui.label(
egui::RichText::new(self.error.clone().unwrap_or_default())
.color(egui::Color32::RED),
);
});
}
fn build(&mut self) {
if let Some(path) = &self.path {
match path.extension().and_then(|ext| ext.to_str()) {
Some("dsa") => {
let mut assembler = Assembler::new(path);
assembler.start(());
// Or block until done
*self.output.mut_data() = match assembler.output() {
Ok(instructions) => instructions,
Err(e) => {
self.error = Some(e.to_string());
return;
}
};
}
// Some("dsc") => {
// let dsa_path = Path::new(path).with_extension("dsa");
// let mut compiler = Compiler::new(path);
// let is_lib = false;
// compiler.start(is_lib);
// if let Err(e) = compiler.write_result(&dsa_path) {
// self.error = Some(e.to_string());
// return;
// }
// let mut assembler = Assembler::new(&dsa_path);
// assembler.start(());
// // Or block until done
// self.output = match assembler.output() {
// Ok(instructions) => instructions,
// Err(e) => {
// self.error = Some(format!("Assembler error: {e}"));
// return;
// }
// };
// }
Some("dsb") => {
if let Ok(bytes) = fs::read(path) {
*self.output.mut_data() = bytes;
} else {
self.error = Some("Failed to read file".to_string());
}
}
_ => {
self.error = Some(format!("Invalid file type: {}", self.filename()));
}
}
}
}
fn render_toolbar(&mut self, ui: &mut Ui) {
self.handle_file_dialogs(ui);
ui.horizontal(|ui| {
ui.label(format!("File type: {}", self.extension()));
ui.label(format!("Filename: {}", self.filename()));
ui.label(format!("Unsaved: {}", self.unsaved));
// 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
// ui.add_enabled(false, egui::Button::new("Open"));
if ui.button("Open").clicked() {
self.open();
}
// Saves the current file
// ui.add_enabled(false, egui::Button::new("Save"));
if ui.button("Save").clicked() {
self.save();
}
// // builds the current file
// ui.add_enabled(false, egui::Button::new("Build"));
if ui.button("Build").clicked() && !self.unsaved {
self.build();
}
// // Loads the generated binary into the assembler at the provided offset
// ui.add_enabled(false, egui::Button::new("Load"));
if ui.button("Load").clicked() {
if self.error.is_some() {
self.error = Some("Can't load program at invalid offset!".to_string());
}
let mut reader = Cursor::new(self.output.data().clone());
let Ok(executable) = DseExecutable::read(&mut reader) else {
self.error = Some("Failed to read program!".to_string());
return
};
let image: Vec<Page> = Page::paginate(&executable.to_memory_image()).collect();
self.state.reseting.store(true, Ordering::Relaxed);
self.memory.clear();
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);
}
// 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());
}
}
ui.checkbox(&mut self.show_output, "Show Output");
});
}
}
fn parse_address(address: &str) -> Option<u32> {
address.strip_prefix("0x").map_or_else(
|| {
address.strip_prefix("0b").map_or_else(
|| {
address.strip_prefix("0o").map_or_else(
|| address.parse::<u32>().ok(),
|oct| u32::from_str_radix(oct, 8).ok(),
)
},
|bin| u32::from_str_radix(bin, 2).ok(),
)
},
|hex| u32::from_str_radix(hex, 16).ok(),
)
}
pub const THEME: ColorTheme = ColorTheme {
name: "Theme",
dark: true,
bg: "1b1b1b",
cursor: "de5252", // fg4
selection: "28323B", // bg2
comments: "444444", // gray1
functions: "7CCCC7", // green1
keywords: "6C81E0", // red1
literals: "A3ABFF", // fg1
numerics: "8A46CF", // purple1
punctuation: "99C9C9", // orange1
strs: "618c84", // aqua1
types: "B8B9D4", // yellow1
special: "de5252", // blue1
};
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)),
);
});
}
})
}
});
});
}
}
@@ -0,0 +1,52 @@
use std::collections::BTreeSet;
use egui_code_editor::{Patch, Syntax};
pub fn dsa() -> Syntax {
Syntax {
word_start: BTreeSet::new(),
quotes: BTreeSet::from(['"', '\'']),
language: "Assembly",
case_sensitive: false,
comment: "//",
comment_multiline: ["/*", "*/"],
hyperlinks: BTreeSet::from(["HTTP", "HTTPS"]),
keywords: BTreeSet::from([
"NOP", "MOV", "CMOV", "LDB", "LDBS", "LDH", "LDHS", "LDW", "STB", "STH", "STW", "LLI",
"LUI", "LWI", "JMP", "JNZ", "JEZ", "JNC", "JIC", "IEQ", "INE", "IGE", "IGT", "ILE",
"ILT", "SHL", "SHR", "ADD", "SUB", "AND", "OR", "NOT", "XOR", "NAND", "NOR", "XNOR",
"IRET", "RET", "CALL", "INT", "PUSH", "POP", "HLT",
]),
types: BTreeSet::from(["DB", "DW", "DH", "RESB", "RESH", "RESW", "INCLUDE"]),
special: BTreeSet::from([
"RG0", "RG1", "RG2", "RG3", "RG4", "RG5", "RG6", "RG7", "RG8", "RG9", "RGA", "RGB",
"RGC", "RGD", "RGE", "RGF", "ACC", "SPR", "BPR", "IDR", "MMR", "ZERO", "PCX", "STS",
"CIR",
]),
patch: Patch::default(),
}
}
pub fn dsc() -> Syntax {
Syntax {
word_start: BTreeSet::from(['_']),
quotes: BTreeSet::from(['"', '\'', '`']),
language: "Damn Simple Code",
case_sensitive: false,
comment: "//",
comment_multiline: ["/*", "*/"],
hyperlinks: BTreeSet::from(["HTTP", "HTTPS"]),
keywords: BTreeSet::from([
"INCLUDE", "FN", "LET", "CONST", "STATIC", "IF", "ELSE", "WHILE", "FOR", "BREAK",
"CONTINUE", "LOOP", "RETURN", "CLASS", "DEFER", "PUB", "CONST",
]),
types: BTreeSet::from([
"U32", "U16", "U8", "I32", "I16", "I8", "STR", "CHAR", "BOOL", "VOID",
]),
special: BTreeSet::from([
",", ";", ".", ":", "=", "+", "-", "*", "/", "%", "&", "|", "^", "~", "!", "?", "<",
">", "<<", ">>", "==", "!=", "<=", ">=", "&&", "||",
]),
patch: Patch::default(),
}
}
@@ -0,0 +1,138 @@
use egui::Vec2;
use emulator_core::{
memory::PhysAddr,
memory::ram::{MemoryBank, Page}
};
use super::Component;
pub struct FrameBuffer {
width: usize,
height: usize,
addr: PhysAddr,
mem: MemoryBank,
pub buffer: Vec<u32>, // RGBA pixels
visible: bool,
texture: Option<egui::TextureHandle>,
}
impl FrameBuffer {
pub fn new(width: u32, height: u32, addr: PhysAddr, mem: MemoryBank) -> Self {
Self {
width: width as usize,
height: height as usize,
addr,
mem,
buffer: vec![0u32; width as usize * height as usize],
visible: true,
texture: None,
}
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
pub fn visible(&mut self) -> &mut bool {
&mut self.visible
}
fn internal_read(&self) -> Vec<u32> {
let byte_size = self.width * self.height * 4;
let page_count = byte_size.div_ceil(Page::SIZE);
(0..page_count)
.map(|i| {
let page = self.mem.read_page(self.addr + i as u32 * 4096);
page.as_slice().to_owned()
})
.flatten()
.take(byte_size)
.collect::<Vec<u8>>()
.chunks_exact(4)
.map(|chunk| u32::from_le_bytes(chunk.try_into().unwrap()))
.collect()
}
pub fn changed(&mut self) -> bool {
let temp = self.internal_read();
if temp != self.buffer {
self.buffer = temp;
return true;
}
false
}
pub fn read(&self) -> &[u32] {
&self.buffer
}
/// Convert buffer to RGBA bytes for use with egui/wgpu textures
pub fn as_rgba_bytes(&self) -> Vec<u8> {
self.buffer
.iter()
.flat_map(|&pixel| pixel.to_le_bytes())
.collect()
}
}
impl Component for FrameBuffer {
fn title(&self) -> &str {
"Framebuffer"
}
fn visible(&mut self) -> &mut bool {
&mut self.visible
}
fn ui(&mut self, ui: &mut egui::Ui) {
let changed = self.changed();
// get or create the texture
let texture = self.texture.get_or_insert_with(|| {
ui.load_texture(
"framebuffer",
egui::ColorImage {
size: [self.width, self.height],
source_size: Vec2::from([self.width as f32, self.height as f32]),
pixels: vec![egui::Color32::BLACK; self.width * self.height],
},
egui::TextureOptions::NEAREST,
)
});
if changed {
let pixels: Vec<egui::Color32> = self
.buffer
.iter()
.map(|&p| {
let bytes = p.to_le_bytes();
egui::Color32::from_rgba_premultiplied(bytes[0], bytes[1], bytes[2], bytes[3])
})
.collect();
texture.set(
egui::ColorImage {
size: [self.width, self.height],
source_size: Vec2::from([self.width as f32, self.height as f32]),
pixels,
},
egui::TextureOptions::NEAREST,
);
}
// scale to fit available space while preserving aspect ratio
let available = ui.available_size();
let aspect = self.width as f32 / self.height as f32;
let size = if available.x / aspect <= available.y {
egui::vec2(available.x, available.x / aspect)
} else {
egui::vec2(available.y * aspect, available.y)
};
ui.image(egui::load::SizedTexture::new(texture.id(), size));
}
}
@@ -0,0 +1,264 @@
use std::num::ParseIntError;
use egui_extras::{Column, TableBuilder};
use common::prelude::Instruction;
use emulator_core::memory::ram::MemoryBank;
use super::Component;
pub struct MemoryInspector {
memory: MemoryBank,
view_size: u32,
view_addr: u32,
visible: bool,
addr_input: String,
// settings
show_bytes: bool,
show_words: bool,
show_ascii: bool,
show_instruction: bool,
show_decimal: bool,
}
impl MemoryInspector {
#[must_use]
pub fn new(memory: MemoryBank) -> Self {
Self {
memory,
view_size: 1024,
view_addr: 0,
visible: false,
addr_input: String::from("0x00"),
show_bytes: true,
show_words: true,
show_ascii: true,
show_instruction: true,
show_decimal: true,
}
}
}
impl Component for MemoryInspector {
fn title(&self) -> &'static str {
"Memory Inspector"
}
fn visible(&mut self) -> &mut bool {
&mut self.visible
}
fn ui(&mut self, ui: &mut egui::Ui) {
// Right column - Memory
// ui.vertical(|ui| {
// ui.add_space(10.0);
// });
// Memory table
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");
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(75.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, value) in (0..self.view_size / 4)
.map(|i| (i, self.memory.read_word(self.view_addr + i * 4))) {
body.row(18.0, |mut row| {
let address = line_num * 4 * self.view_addr;
let chunk = value.to_le_bytes();
// Address column
row.col(|ui| {
ui.label(
egui::RichText::new(format!("0x{address:08X}"))
.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)),
);
});
}
})
}
});
});
}
}
fn parse_address(address: &str) -> Result<u32, ParseIntError> {
if let Some(hex_part) = address.strip_prefix("0x") {
return u32::from_str_radix(hex_part, 16);
}
if let Some(bin_part) = address.strip_prefix("0b") {
return u32::from_str_radix(bin_part, 2);
}
if let Some(oct_part) = address.strip_prefix("0o") {
return u32::from_str_radix(oct_part, 8);
}
address.parse::<u32>()
}
@@ -0,0 +1,17 @@
pub mod controller;
pub mod display;
pub mod editor;
pub mod framebuffer;
pub mod memory;
pub mod registers;
pub mod serial;
pub trait Component {
/// The tab label shown in the tile header.
fn title(&self) -> &str;
fn visible(&mut self) -> &mut bool;
/// Draw the component's UI inside the provided `Ui`.
fn ui(&mut self, ui: &mut egui::Ui);
}
@@ -0,0 +1,127 @@
use std::{f32, sync::Arc};
use common::prelude::Register;
use emulator_core::processor::state::SharedState;
use super::Component;
pub struct Registers {
state: Arc<SharedState>,
visible: bool,
}
impl Registers {
fn section(
&mut self,
ui: &mut egui::Ui,
registers: Vec<(Register, u32)>,
col_pairs: usize,
name: &str,
) {
ui.collapsing(name, |ui| {
egui::Grid::new(name)
.num_columns(col_pairs * 2)
.spacing([40.0, 4.0])
.striped(true)
.show(ui, |ui| {
// only show header if there are multiple rows
if registers.len() > col_pairs {
for _ in 0..col_pairs {
ui.label("Register");
ui.label("Value");
}
ui.end_row();
}
for chunk in registers.chunks(col_pairs) {
for (reg, val) in chunk {
ui.label(reg.to_string());
ui.label(format!("0x{:08X} ({})", val, val));
}
for _ in chunk.len()..col_pairs {
ui.label("");
ui.label("");
}
ui.end_row();
}
});
});
}
}
impl Registers {
pub fn new(state: Arc<SharedState>) -> Self {
Self {
state,
visible: false,
}
}
}
impl Component for Registers {
fn title(&self) -> &str {
"Registers"
}
fn visible(&mut self) -> &mut bool {
&mut self.visible
}
fn ui(&mut self, ui: &mut egui::Ui) {
ui.set_min_width(200.0);
let state = self.state.proc.load();
let available = ui.available_width();
let col_pairs = match available {
0.0..350.0 => 1,
350.0..600.0 => 2,
600.0..900.0 => 3,
f32::MIN..0.0 => unreachable!(),
_ => 4,
};
ui.vertical(|ui| {
// ── General Purpose ──────────────────────────────────────
let gp_registers: Vec<(Register, u32)> = (0..=15u8)
.map(|i| (Register::from_u8(i).unwrap(), state.registers[i as usize]))
.collect();
self.section(
ui,
gp_registers,
col_pairs,
"General Purpose Registers",
);
ui.separator();
// ── Stack ─────────────────────────────────────────────────
let stack_registers = vec![
(Register::Spr, state.registers[Register::Spr as usize]),
(Register::Bpr, state.registers[Register::Bpr as usize]),
];
self.section(ui, stack_registers, col_pairs, "Stack Registers");
ui.separator();
// ── Special Purpose ───────────────────────────────────────
let special_registers = vec![
(Register::Acc, state.registers[Register::Acc as usize]),
(Register::Ret, state.registers[Register::Ret as usize]),
(Register::Idr, state.registers[Register::Idr as usize]),
(Register::Mmr, state.registers[Register::Mmr as usize]),
];
self.section(
ui,
special_registers,
col_pairs,
"Special Purpose Registers",
);
ui.separator();
// ── System ────────────────────────────────────────────────
let system_registers = vec![
(Register::Pcx, state.registers[Register::Pcx as usize]),
(Register::Sts, state.registers[Register::Sts as usize]),
(Register::Cir, state.registers[Register::Cir as usize]),
];
self.section(ui, system_registers, col_pairs, "System Registers");
});
}
}
@@ -0,0 +1,128 @@
use std::{
collections::VecDeque,
sync::{
Arc,
atomic::Ordering,
mpsc::{Receiver, Sender},
},
};
use emulator_core::{
memory::{
PhysAddr,
ram::MemoryBank
},
processor::{
interrupts::Interrupt,
state::SharedState
}
};
use super::Component;
/// ## SERIAL LAYOUT: (from the perspective of code inside the ui)
/// - [base+0x00]: ser_out valid flag - set by kernel after writing a byte to serial
/// - [base+0x01]: ser_out byte - data payload for ui to read
/// - [base+0x02]: ser_in valid flag - set by ui after writing a byte to serial
/// - [base+0x03]: ser_in byte - data payload for kernel to read
pub struct Serial {
pub read_rx: Receiver<u8>,
pub write_tx: Sender<u8>,
pub read_buff: Vec<u8>,
pub write_buff: String,
pub visible: bool,
}
impl Serial {
fn uart_io_thread(
mem: MemoryBank,
state: Arc<SharedState>,
uart_base: PhysAddr,
tx: Sender<u8>,
rx: Receiver<u8>,
) {
let mut write_buffer: VecDeque<u8> = VecDeque::new();
loop {
while let Ok(byte) = rx.try_recv() {
write_buffer.push_back(byte);
}
let ser_out_valid = mem.read_byte(uart_base + 0);
// Kernel => ui: valid flag in byte 0, data in byte 1
if ser_out_valid == 0x01 {
let ser_out = mem.read_byte(uart_base + 1);
let _ = tx.send(ser_out);
mem.write_byte(uart_base, 0x00); // clear the whole word
}
if !write_buffer.is_empty() {
// Emulator => kernel: valid flag in byte 2, data in byte 3
let ser_in_valid = mem.read_byte(uart_base + 2);
if ser_in_valid == 0x00 {
if let Some(byte) = write_buffer.pop_front() {
mem.write_byte(uart_base + 3, byte);
mem.write_byte(uart_base + 2, 0x01);
}
}
std::thread::sleep(std::time::Duration::from_millis(2));
state.interrupt_queue.send(Interrupt::SerialIn).unwrap();
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
}
pub fn new(uart_base: PhysAddr, state: Arc<SharedState>, mem_handle: MemoryBank) -> Self {
let (read_tx, read_rx) = std::sync::mpsc::channel();
let (write_tx, write_rx) = std::sync::mpsc::channel();
let _ = std::thread::spawn(move || {
Self::uart_io_thread(mem_handle, state, uart_base, read_tx, write_rx)
});
Self {
visible: false,
write_tx,
read_rx,
read_buff: Vec::new(),
write_buff: String::new(),
}
}
pub fn update(&mut self) {
while let Ok(byte) = self.read_rx.try_recv() {
self.read_buff.push(byte);
}
}
}
impl Component for Serial {
fn title(&self) -> &str {
"Serial"
}
fn visible(&mut self) -> &mut bool {
&mut self.visible
}
fn ui(&mut self, ui: &mut egui::Ui) {
self.update();
// Text field for input and a send button
ui.text_edit_multiline(&mut self.write_buff);
if ui.button("Send").clicked() {
self.write_buff.push(0 as char);
for byte in self.write_buff.bytes() {
self.write_tx.send(byte).unwrap();
}
self.write_buff.clear();
}
// Display received data
ui.label(String::from_utf8(self.read_buff.clone()).unwrap());
if ui.button("Clear").clicked() {
self.read_buff.clear();
}
}
}
+155
View File
@@ -0,0 +1,155 @@
use eframe::NativeOptions;
use eframe::egui;
use std::sync::Arc;
use std::sync::atomic::Ordering;
mod components;
use components::{
controller::Controller, display::Display, editor::Editor, framebuffer::FrameBuffer, memory::MemoryInspector,
registers::Registers, serial::Serial, Component,
};
use emulator_core::{
config::VERSION,
memory::ram::MemoryBank,
processor::state::SharedState
};
pub fn run_app(state: Arc<SharedState>, mem_handle: MemoryBank) -> eframe::Result<()> {
eframe::run_native(
"Damn Simple Architecture",
NativeOptions::default(),
Box::new(|cc| Ok(Box::new(DsaUi::new(cc, state, mem_handle)))),
)
}
impl eframe::App for DsaUi {
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
// request an update from ui every cycle
self.state.update_req.store(true, Ordering::Relaxed);
// title panel with dsa title
egui::containers::Panel::top("top_panel").show(ui, |ui| {
ui.with_layout(
egui::Layout::top_down_justified(egui::Align::Center)
.with_main_align(egui::Align::Min),
|ui| {
ui.allocate_space(egui::vec2(0.0, 5.0));
ui.heading(format!("Damn Simple Architecture v{} 🚀", VERSION));
ui.allocate_space(egui::vec2(0.0, 5.0));
},
);
});
// menu bar.
egui::Panel::top("menu_bar").show(ui, |ui| {
egui::MenuBar::new().ui(ui, |ui| {
ui.menu_button("Panels", |ui| {
ui.set_max_width(300.0);
ui.set_min_width(300.0);
ui.spacing_mut().button_padding = egui::vec2(10.0, 5.0);
for comp in &mut self.components {
let name = comp.title().to_string();
ui.toggle_value(comp.visible(), name);
}
});
});
});
egui::CentralPanel::default().show(ui, |ui| {
for c in &mut self.components {
let mut visible = *c.visible();
if visible {
egui::Window::new(c.title())
.open(&mut visible)
.show(ui, |ui| {
c.ui(ui);
});
}
*c.visible() = visible;
}
});
ui.request_repaint_after(std::time::Duration::from_millis(16)); // ~60fps
}
}
pub struct DsaUi {
state: Arc<SharedState>,
mem_handle: MemoryBank,
components: Vec<Box<dyn Component>>,
}
impl DsaUi {
pub fn new(
cc: &eframe::CreationContext,
state: Arc<SharedState>,
mem_handle: MemoryBank,
) -> Self {
// components
let mut components: Vec<Box<dyn Component>> = vec![
Box::new(Controller::new(state.clone(), mem_handle.clone())),
Box::new(Display::new(80, 25, 0x20000, mem_handle.clone())),
Box::new(FrameBuffer::new(320, 240, 0x30000, mem_handle.clone())),
Box::new(Registers::new(state.clone())),
Box::new(Serial::new(0x40000, state.clone(), mem_handle.clone())),
Box::new(Editor::new(state.clone(), mem_handle.clone())),
Box::new(MemoryInspector::new(mem_handle.clone())),
];
components.iter_mut().for_each(|c| *c.visible() = false);
Self::configure_appearance(&cc.egui_ctx);
let app = Self {
state,
mem_handle,
components,
};
app
}
fn configure_appearance(ctx: &egui::Context) {
// configure appearance of UI elements
let mut visuals = egui::Visuals::dark();
// --- your existing settings ---
visuals.window_fill = egui::Color32::from_rgb(20, 20, 20);
visuals.panel_fill = egui::Color32::from_rgb(20, 20, 20);
visuals.widgets.inactive.fg_stroke =
egui::Stroke::from((1.0, egui::Color32::from_rgb(255, 255, 255)));
visuals.widgets.inactive.bg_stroke =
egui::Stroke::from((1.0, egui::Color32::from_rgb(60, 60, 60)));
visuals.widgets.inactive.corner_radius = egui::CornerRadius::from(4);
visuals.widgets.inactive.bg_fill = egui::Color32::from_rgb(20, 20, 20);
visuals.widgets.inactive.weak_bg_fill = egui::Color32::from_rgb(20, 20, 20);
visuals.widgets.inactive.expansion = 1.0;
// tile resize handle
visuals.window_stroke = egui::Stroke::new(1.0, egui::Color32::from_rgb(45, 45, 45));
visuals.extreme_bg_color = egui::Color32::from_rgb(20, 20, 20);
ctx.set_visuals(visuals);
let mut fonts = egui::FontDefinitions::default();
fonts.font_data.insert(
"JetBrains Mono Nerd Font".to_string(),
std::sync::Arc::new(egui::FontData::from_static(include_bytes!(
"../../font/JetBrainsMonoNerdFontMono_Regular.ttf",
))),
);
fonts
.families
.entry(egui::FontFamily::Proportional)
.or_default()
.insert(0, "JetBrains Mono Nerd Font".to_string());
fonts
.families
.entry(egui::FontFamily::Monospace)
.or_default()
.insert(0, "JetBrains Mono Nerd Font".to_string());
ctx.set_fonts(fonts);
}
}