added zxq5_egui crate, a crate for packaging together my UI components so i can reuse them across projects.

- added buttons/tabs
- added a simple file tree (work in progress)
This commit is contained in:
2026-08-02 02:58:28 +01:00
parent 43f8340f0d
commit 59c26564d3
12 changed files with 446 additions and 287 deletions
+4 -4
View File
@@ -1,8 +1,9 @@
use eframe::epaint::Color32;
use common::prelude::Register;
use dsa_emulator::EmulatorHandle;
use dsa_emulator::processor::RunningState;
use crate::ui::ui_helpers::PaddedWidget;
use eframe::epaint::Color32;
use zxq5_egui::buttons::PaddedWidget;
use crate::ui::ui_helpers::StateWrapper;
pub struct Controller {
emulator: EmulatorHandle,
@@ -23,7 +24,6 @@ impl Controller {
.inner_margin(10.0)
.show(ui, |ui| {
ui.horizontal(|ui| {
use RunningState::*;
let status = self.emulator.status();
@@ -43,7 +43,7 @@ impl Controller {
}
}
PaddedWidget::indicator(status).ui(ui);
PaddedWidget::indicator(StateWrapper(status)).ui(ui);
if PaddedWidget::button("⟳ Reset All").color(Color32::ORANGE).ui(ui).clicked() {
self.emulator.when_paused(|emu| emu.reset());
@@ -1,5 +1,5 @@
use egui::{Modal, Id, Layout, Align, RichText};
use crate::ui::ui_helpers::PaddedWidget;
use egui::{Align, Id, Layout, Modal, RichText};
use zxq5_egui::buttons::PaddedWidget;
pub struct ConfirmDialog<T> {
open: bool,
+90 -55
View File
@@ -8,27 +8,29 @@ use crate::ui::tiles::registers::Registers;
use crate::ui::tiles::{CentralTiles, TilePane};
use dialogs::confirm_dialog::ConfirmDialog;
use dialogs::file_dialog::DsaFileDialog;
use dsa_formats::binary_dse::DseExecutable;
use egui::{CentralPanel, Panel, Ui};
use egui_tiles::TileId;
use dsa_emulator::EmulatorController;
use dsa_emulator::devices::display::Display;
use dsa_emulator::devices::serial::Serial;
use dsa_formats::binary_dse::DseExecutable;
use egui::{CentralPanel, Panel, Ui};
use egui_tiles::TileId;
use process_manager::ProcessManager;
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
use std::time::Duration;
use tiles::PanelFactory;
use tiles::documentation::Documentation;
use tiles::editor::Editor;
use tiles::io_devices::framebuffer::FrameBufferUi;
use process_manager::ProcessManager;
use std::fs::File;
use std::io::BufReader;
use std::time::Duration;
use zxq5_egui::file_tree::{FileTree, FileTreeEvent};
pub mod appearance;
mod controller;
mod dialogs;
pub mod process_manager;
mod tiles;
mod ui_helpers;
mod controller;
pub mod process_manager;
pub mod appearance;
pub struct DamnSimpleUI {
emulator: EmulatorController,
@@ -42,10 +44,10 @@ pub struct DamnSimpleUI {
tools: Vec<PanelFactory>,
show_process_manager: bool,
process_manager: ProcessManager
process_manager: ProcessManager,
file_tree: FileTree,
}
impl eframe::App for DamnSimpleUI {
fn ui(&mut self, ui: &mut Ui, frame: &mut eframe::Frame) {
ui.request_repaint_after(Duration::from_millis(16));
@@ -72,13 +74,26 @@ impl eframe::App for DamnSimpleUI {
self.controller.ui(ui);
});
Panel::left("left_panel").resizable(false).show(ui, |ui| {
// TODO: File Explorer!
Panel::left("left_panel").resizable(true).show(ui, |ui| {
let inner_res = self.file_tree.ui(ui, &mut |ui: &mut Ui, path: &Path| {
ui.button("Load Binary")
.clicked()
.then_some(Action::LoadBinary(DataSource::File(path.to_path_buf())))
});
if let Some(event) = inner_res.inner {
match event {
FileTreeEvent::Clicked(path) => self.perform_action(Action::OpenFile(path)),
FileTreeEvent::ContextAction(action) => self.perform_action(action),
}
}
});
Panel::bottom("bottom_panel").resizable(true).show_collapsible(ui, &mut self.show_process_manager,|ui| {
self.process_manager.ui(ui);
});
Panel::bottom("bottom_panel")
.resizable(true)
.show(ui, |ui| {
self.process_manager.ui(ui);
});
CentralPanel::default().show(ui, |ui| {
if let Some(action) = self.central_tiles.ui(ui) {
@@ -88,11 +103,7 @@ impl eframe::App for DamnSimpleUI {
}
}
impl DamnSimpleUI {
fn open_or_focus<T: TilePane + 'static>(
central_tiles: &mut CentralTiles,
slot: &mut Option<TileId>,
@@ -104,10 +115,7 @@ impl DamnSimpleUI {
}
}
pub fn new(
cc: &eframe::CreationContext,
mut emulator: EmulatorController,
) -> Self {
pub fn new(cc: &eframe::CreationContext, mut emulator: EmulatorController) -> Self {
appearance::configure_appearance(&cc.egui_ctx);
let handle = emulator.get_handle().clone();
Self {
@@ -116,7 +124,7 @@ impl DamnSimpleUI {
confirm_dialog: ConfirmDialog::new(),
file_dialog: DsaFileDialog::new(),
error_dialog: ErrorDialog::default(),
tools: vec![
PanelFactory::register("Memory Inspector", {
let handle = handle.clone();
@@ -131,31 +139,32 @@ impl DamnSimpleUI {
let handle = handle.clone();
move || FrameBufferUi::new(320, 240, 0x30000, handle.clone())
}),
PanelFactory::register("Serial", {
PanelFactory::register("Serial", {
let handle = handle.clone();
move || Serial::new(0x40000, handle.clone())
}),
PanelFactory::register("Registers", {
PanelFactory::register("Registers", {
let handle = handle.clone();
move || Registers::new(handle.clone())
})
}),
],
emulator,
show_process_manager: true,
process_manager: ProcessManager::new()
process_manager: ProcessManager::new(),
file_tree: FileTree::build(&std::env::current_dir().unwrap()),
}
}
pub fn perform_action(&mut self, action: Action) {
match action {
Action::None => {},
Action::None => {}
Action::SpawnProcess(spawner) => spawner(&mut self.process_manager),
Action::RequestCloseTab(tile_id) => {
self.confirm_dialog.open(
"Close Tab",
"Closing this tab may result in losing unsaved work",
Action::CloseTab(tile_id)
Action::CloseTab(tile_id),
);
}
@@ -166,15 +175,24 @@ impl DamnSimpleUI {
Action::Quit => std::process::exit(0),
// Opens a file (usually constructed by the file dialogue)
Action::OpenFile(path) => {
match path.extension().map(|s| s.to_str()).flatten() {
Some("dsa" | "dsc") => { self.central_tiles.add_tab(Editor::new_open(&path)); },
Some("dsb") => { self.central_tiles.add_tab(BinaryViewer::new_open(&path)); },
Some("md") => { self.central_tiles.add_tab(Documentation::open(&path)); },
Some(_) => todo!(),
None => {},
Action::OpenFile(path) => match path.extension().map(|s| s.to_str()).flatten() {
Some("dsa" | "dsc") => {
self.central_tiles.add_tab(Editor::new_open(&path));
}
}
Some("dsb") => {
self.central_tiles.add_tab(BinaryViewer::new_open(&path));
}
Some("md") => {
self.central_tiles.add_tab(Documentation::open(&path));
}
Some(f) => {
self.perform_action(Action::ShowError(format!(
"Unsupported file extension: {}",
f
)));
}
None => {}
},
Action::ShowError(err) => {
self.error_dialog.open(err);
@@ -187,11 +205,12 @@ impl DamnSimpleUI {
}
Action::LoadBinary(DataSource::File(path)) => {
if let Some(ext) = path.extension().map(|s| s.to_str()).flatten() && ext == "dsb" {
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
return;
};
let Ok(binary) = DseExecutable::read(&mut BufReader::new(file)) else {
@@ -215,50 +234,67 @@ impl DamnSimpleUI {
}
Action::RequestSaveAs(tile) => {
self.file_dialog.open_save(move |path| Action::SaveFile(path, tile));
self.file_dialog
.open_save(move |path| Action::SaveFile(path, tile));
}
};
}
fn toolbar(&mut self, ui: &mut Ui) {
egui::MenuBar::new().ui(ui, |ui| {
let save_enabled = self.central_tiles.active_tile()
let save_enabled = self
.central_tiles
.active_tile()
.and_then(|id| self.central_tiles.pane_mut::<Editor>(id))
.is_some();
ui.set_height(30.0);
ui.menu_button("File", |ui| {
if ui.button("Open").clicked() {
self.file_dialog.open_pick(Action::OpenFile);
}
if ui.button("Load Binary").clicked() {
self.file_dialog.open_pick(|path| Action::LoadBinary(DataSource::File(path)));
self.file_dialog
.open_pick(|path| Action::LoadBinary(DataSource::File(path)));
}
if ui.add_enabled(save_enabled, egui::Button::new("Save")).clicked() {
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)),
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 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))
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)
self.confirm_dialog.open(
"Exit",
"Exit Application\n(you will lose unsaved work)",
Action::Quit,
)
}
});
@@ -278,4 +314,3 @@ impl DamnSimpleUI {
});
}
}
+1 -1
View File
@@ -1,10 +1,10 @@
use crate::state::Action;
use crate::ui::ui_helpers::painted_tab;
use crate::ui::ui_helpers::text::strip_ansi;
use dsa_tasks::{ProcessAgent, TraceableProcess};
use std::collections::VecDeque;
use std::sync::mpsc;
use std::thread::JoinHandle;
use zxq5_egui::tabs::painted_tab;
pub type ProcessSpawner = Box<dyn FnOnce(&mut ProcessManager) + Send>;
+1 -1
View File
@@ -2,7 +2,6 @@ use crate::state::Action;
use crate::state::DataSource::Data;
use crate::ui::tiles::TilePane;
use crate::ui::tiles::editor::syntax::SyntaxFrom;
use crate::ui::ui_helpers::PaddedWidget;
use assembler::prelude::Assembler;
use dsa_formats::binary_dse::DseExecutable;
use dsa_tasks::TraceableProcess;
@@ -13,6 +12,7 @@ use std::any::Any;
use std::fs::File;
use std::path::{Path, PathBuf};
use theme::THEME;
use zxq5_egui::buttons::PaddedWidget;
mod syntax;
mod theme;
+1 -1
View File
@@ -8,11 +8,11 @@ pub mod io_devices;
use crate::state::Action;
use crate::state::Action::RequestCloseTab;
use crate::ui::ui_helpers::painted_tab;
use eframe::epaint::{Color32, CornerRadius};
use egui::{Id, Response, Stroke, Ui};
use egui_tiles::{Behavior, Container, SimplificationOptions, TabState, Tile, TileId, Tiles, Tree, UiResponse};
use std::any::Any;
use zxq5_egui::tabs::painted_tab;
/// 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
+5 -223
View File
@@ -1,115 +1,13 @@
pub mod text;
use eframe::emath::Vec2;
use egui::{Color32};
use dsa_emulator::processor::RunningState;
use zxq5_egui::buttons::StatusIndicator;
pub struct PaddedWidget {
text: String,
color: Option<Color32>,
padding: Option<Vec2>,
widget_type: WidgetType
}
pub struct StateWrapper(pub RunningState);
enum WidgetType {
Button,
Indicator,
Label
}
impl PaddedWidget {
pub fn button(text: impl Into<String>) -> Self {
Self {
text: text.into(),
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: impl Into<String>) -> Self {
Self {
text: text.into(),
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 = if let WidgetType::Indicator = self.widget_type {
galley.size() + egui::vec2(12.0, 0.0) + self.padding.unwrap_or(Vec2::new(14.0, 8.0)) * 2.0
} else {
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,
);
if let WidgetType::Indicator = self.widget_type {
let size = egui::vec2(galley.size().clone().x + 6.0, 0.0);
let text_pos = rect.center() - galley.size() / 2.0 + egui::vec2(8.0, 0.0);
ui.painter().galley(text_pos, galley, self.color.unwrap_or(visuals.fg_stroke.color));
ui.painter().circle_filled(rect.center() - size / 2.0, 8.0 / 2.0, self.color.unwrap());
}
else {
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 trait StatusIndicator {
fn color(&self) -> egui::Color32;
fn label(&self) -> String;
}
impl StatusIndicator for RunningState {
impl StatusIndicator for StateWrapper {
fn color(&self) -> egui::Color32 {
match self {
match self.0 {
RunningState::Running => egui::Color32::GREEN,
RunningState::Paused => egui::Color32::YELLOW,
RunningState::Halted => egui::Color32::ORANGE,
@@ -119,7 +17,7 @@ impl StatusIndicator for RunningState {
}
fn label(&self) -> String {
match self {
match self.0 {
RunningState::Running => "Running".into(),
RunningState::Paused => "Paused".into(),
RunningState::Halted => "Halted".into(),
@@ -129,119 +27,3 @@ impl StatusIndicator for RunningState {
}
}
/// Shared paint logic — draws the crossed lines, using whatever
/// interact-state visuals (hover/press color, expansion) egui's style
/// would normally pick for this response.
fn paint_close_x(ui: &egui::Ui, rect: egui::Rect, response: &egui::Response) {
if !ui.is_rect_visible(rect) {
return;
}
let visuals = ui.style().interact(response);
let inner = rect.shrink(4.0).expand(visuals.expansion);
let stroke = visuals.fg_stroke;
ui.painter().line_segment([inner.left_top(), inner.right_bottom()], stroke);
ui.painter().line_segment([inner.right_top(), inner.left_bottom()], stroke);
}
/// Standalone close button — allocates its own space in the current
/// layout, like a normal widget. Use this in a `ui.horizontal(...)` row,
/// e.g. next to a label in a flat list (process manager tabs, a file
/// list with per-row close, etc).
pub fn close_button(ui: &mut egui::Ui, size: f32) -> egui::Response {
let (rect, response) = ui.allocate_exact_size(egui::vec2(size, size), egui::Sense::click());
let response = response.on_hover_cursor(egui::CursorIcon::Default);
response.widget_info(|| {
egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), "Close")
});
paint_close_x(ui, rect, &response);
response
}
/// Close button at an explicit rect you've already computed — for
/// cases like tab_ui, where the button needs to sit inside a larger
/// region you're laying out manually rather than in normal flow.
pub fn close_button_at(ui: &mut egui::Ui, rect: egui::Rect, id: egui::Id) -> egui::Response {
let response = ui
.interact(rect, id, egui::Sense::click_and_drag())
.on_hover_cursor(egui::CursorIcon::Default);
response.widget_info(|| {
egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), "Close")
});
paint_close_x(ui, rect, &response);
response
}
pub struct TabResponse {
pub response: egui::Response,
pub close_clicked: bool,
}
pub fn painted_tab(
ui: &mut egui::Ui,
id: egui::Id,
title: impl Into<egui::WidgetText>,
selected: bool,
closable: bool,
draggable: bool,
) -> TabResponse {
let padding = egui::vec2(14.0, 8.0);
let close_size = 14.0;
let close_gap = 6.0;
let font_id = egui::TextStyle::Button.resolve(ui.style());
let galley = title.into().into_galley(ui, Some(egui::TextWrapMode::Extend), f32::INFINITY, font_id);
let extra_width = if closable { close_size + close_gap } else { 0.0 };
let desired = egui::vec2(
galley.size().x + padding.x * 2.0 + extra_width,
ui.available_height().max(galley.size().y + padding.y * 2.0),
);
let (_, rect) = ui.allocate_space(desired);
let sense = if draggable { egui::Sense::click_and_drag() } else { egui::Sense::click() };
let response = ui.interact(rect, id, sense);
let response = if draggable {
response.on_hover_cursor(egui::CursorIcon::Grab)
} else {
response
};
response.widget_info(|| {
egui::WidgetInfo::selected(egui::WidgetType::Button, ui.is_enabled(), selected, galley.text())
});
let mut close_clicked = false;
if ui.is_rect_visible(rect) {
let visuals = ui.style().interact(&response);
ui.painter().rect(rect, 4.0, visuals.bg_fill, visuals.bg_stroke, egui::StrokeKind::Inside);
let text_color = if selected {
egui::Color32::WHITE
} else {
egui::Color32::DARK_GRAY
};
let text_pos = egui::pos2(rect.min.x + padding.x, rect.center().y - galley.size().y / 2.0);
ui.painter().galley(text_pos, galley, text_color);
if closable {
let close_rect = egui::Rect::from_min_size(
egui::pos2(rect.max.x - padding.x - close_size, rect.center().y - close_size / 2.0),
egui::vec2(close_size, close_size),
);
let close_id = id.with("close");
let close_response = close_button_at(ui, close_rect, close_id);
close_clicked = close_response.clicked() || response.clicked_by(egui::PointerButton::Middle);
}
}
TabResponse { response, close_clicked }
}