From 43e9d6942ca7d339e44e0f6053ace693809df5b7 Mon Sep 17 00:00:00 2001 From: zxq5 Date: Mon, 3 Aug 2026 23:02:09 +0100 Subject: [PATCH] improved tree widget and separatated "FileTree" implementation out into separate src file --- emulator/dsa_gui/src/ui/mod.rs | 33 +++--- libraries/zxq5_egui/src/context_tree.rs | 150 ------------------------ libraries/zxq5_egui/src/file_tree.rs | 125 -------------------- libraries/zxq5_egui/src/lib.rs | 3 +- libraries/zxq5_egui/src/tree/file.rs | 56 +++++++++ libraries/zxq5_egui/src/tree/mod.rs | 110 +++++++++++++++++ 6 files changed, 185 insertions(+), 292 deletions(-) delete mode 100644 libraries/zxq5_egui/src/context_tree.rs delete mode 100644 libraries/zxq5_egui/src/file_tree.rs create mode 100644 libraries/zxq5_egui/src/tree/file.rs create mode 100644 libraries/zxq5_egui/src/tree/mod.rs diff --git a/emulator/dsa_gui/src/ui/mod.rs b/emulator/dsa_gui/src/ui/mod.rs index 797bbef..dfc0795 100644 --- a/emulator/dsa_gui/src/ui/mod.rs +++ b/emulator/dsa_gui/src/ui/mod.rs @@ -22,7 +22,7 @@ use tiles::PanelFactory; use tiles::documentation::Documentation; use tiles::editor::Editor; use tiles::io_devices::framebuffer::FrameBufferUi; -use zxq5_egui::context_tree::{ContextTree, NewFileTree}; +use zxq5_egui::tree::{MenuTree, TreeEvent, file::FileTree}; pub mod appearance; mod controller; @@ -44,7 +44,7 @@ pub struct DamnSimpleUI { show_process_manager: bool, process_manager: ProcessManager, - file_tree: ContextTree, + file_tree: MenuTree, } impl eframe::App for DamnSimpleUI { @@ -74,13 +74,17 @@ impl eframe::App for DamnSimpleUI { }); Panel::left("left_panel").resizable(true).show(ui, |ui| { - let inner_res = self.file_tree.ui(ui); + let response = self.file_tree.ui(ui); + let Some(event) = response.inner else { + return; + }; - if let Some(event) = inner_res.inner { - // match event { - // ContextTreeEvent::Clicked(node) => self.perform_action(Action::OpenFile(node)), - // ContextTreeEvent::ContextAction(action, node) => self.perform_action(action), - // } + match event { + TreeEvent::ContextAction(action, _tree) => self.perform_action(action), + TreeEvent::Clicked(tree) => { + let filename = tree.filename().to_owned(); + self.perform_action(Action::OpenFile(filename)); + } } }); @@ -147,16 +151,15 @@ impl DamnSimpleUI { show_process_manager: true, process_manager: ProcessManager::new(), - file_tree: ContextTree::new( - NewFileTree::build(&std::env::current_dir().unwrap()), - |ui, node| match node { - NewFileTree::Leaf { filename } => ui + file_tree: MenuTree::new() + .root(Some(FileTree::build(&std::env::current_dir().unwrap()))) + .ctx(|ui, node| match node { + FileTree::File { path: filename } => ui .button("Open Binary") .clicked() .then_some(Action::LoadBinary(DataSource::File(filename.clone()))), - NewFileTree::Branch { .. } => None, - }, - ), + FileTree::Directory { .. } => None, + }), } } diff --git a/libraries/zxq5_egui/src/context_tree.rs b/libraries/zxq5_egui/src/context_tree.rs deleted file mode 100644 index 5241fae..0000000 --- a/libraries/zxq5_egui/src/context_tree.rs +++ /dev/null @@ -1,150 +0,0 @@ -use egui::{InnerResponse, TextBuffer, Widget}; -use std::borrow::Cow; -use std::path::{Path, PathBuf}; - -#[derive(Clone, PartialOrd, Ord, Eq, PartialEq)] -pub enum NewFileTree { - Branch { - path: PathBuf, - children: Vec, - }, - Leaf { - filename: PathBuf, - }, -} - -impl NewFileTree { - pub fn build(path: &Path) -> Self { - match path.is_dir() { - true => Self::Branch { - path: path.to_path_buf(), - children: { - let mut children: Vec = std::fs::read_dir(path) - .map(|ok| { - ok.filter_map(|file| file.ok()) - .map(|file| Self::build(&path.join(file.file_name()))) - .collect() - }) - .unwrap_or_default(); - children.sort_unstable(); - children - }, - }, - false => Self::Leaf { - filename: path.to_path_buf(), - }, - } - } -} - -impl TreeView for NewFileTree { - fn node(&self) -> Node<'_, Self> { - match self { - Self::Branch { path, children } => Node::Branch { - name: path.file_name().unwrap().to_string_lossy(), - children, - }, - Self::Leaf { filename } => Node::Leaf { - name: filename.file_name().unwrap().to_string_lossy(), - }, - } - } -} - -pub enum ContextTreeEvent<'t, A, T: TreeView> { - Clicked(Node<'t, T>), - ContextAction(A, Node<'t, T>), -} - -#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] -pub enum Node<'t, T: TreeView> { - Leaf { - name: Cow<'t, str>, - }, - Branch { - name: Cow<'t, str>, - children: &'t Vec, - }, -} - -pub trait TreeView: Sized + Clone { - fn node(&self) -> Node<'_, Self>; -} - -pub struct ContextTree { - root: T, - context: Box Option>, -} - -impl ContextTree -where - Tree: TreeView, -{ - type Ctx = Box Option>; - - pub fn new Option + 'static>( - root: Tree, - context: T, - ) -> Self { - Self { - root, - context: Box::new(context), - } - } - - fn ctx_menu<'t>( - ui: &mut egui::Ui, - ctx: &mut Self::Ctx, - tree: &'t Tree, - ) -> Option> { - ctx(ui, tree).and_then(|a| Some(ContextTreeEvent::ContextAction(a, tree.node()))) - } - - pub fn ui( - &mut self, - ui: &mut egui::Ui, - ) -> InnerResponse>> { - let mut action = None; - let frame_response = egui::Frame::new().show(ui, |ui| { - egui::ScrollArea::both() - .auto_shrink([false; 2]) - .show(ui, |ui| { - Self::ui_inner(ui, &mut action, &mut self.context, &self.root); - }) - }); - InnerResponse::new(action, frame_response.response) - } - - fn ui_inner<'ctx, 't>( - ui: &mut egui::Ui, - action: &mut Option>, - ctx: &mut Self::Ctx, - tree: &'t Tree, - ) { - match tree.node() { - Node::Leaf { ref name } => { - let response = egui::Button::new(name.as_str()).frame(false).ui(ui); - - response.context_menu(|ui| *action = Self::ctx_menu(ui, ctx, tree)); - - if response.clicked() { - *action = Some(ContextTreeEvent::Clicked(tree.node())); - } - } - Node::Branch { - ref name, - ref children, - } => { - let show_response = egui::CollapsingHeader::new(name.as_str()).show(ui, |ui| { - for child in children.iter() { - Self::ui_inner(ui, action, ctx, child); - } - }); - - show_response - .header_response - .context_menu(|ui| *action = Self::ctx_menu(ui, ctx, tree)); - } - } - } -} diff --git a/libraries/zxq5_egui/src/file_tree.rs b/libraries/zxq5_egui/src/file_tree.rs deleted file mode 100644 index 791d3f4..0000000 --- a/libraries/zxq5_egui/src/file_tree.rs +++ /dev/null @@ -1,125 +0,0 @@ -use egui::{CollapsingHeader, InnerResponse, Widget}; -use std::cmp::Ordering; -use std::path::{Path, PathBuf}; - -pub enum FileTree { - Branch { - path: PathBuf, - children: Vec, - }, - Leaf { - filename: PathBuf, - }, -} - -impl Eq for FileTree {} - -impl PartialEq for FileTree { - fn eq(&self, other: &Self) -> bool { - matches!( - (self, other), - (Self::Branch { .. }, Self::Branch { .. }) | (Self::Leaf { .. }, Self::Leaf { .. }) - ) - } -} - -impl PartialOrd for FileTree { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for FileTree { - fn cmp(&self, other: &Self) -> Ordering { - match (self, other) { - (Self::Leaf { filename: a }, Self::Leaf { filename: b }) => a.cmp(b), - (Self::Branch { path: a, .. }, Self::Branch { path: b, .. }) => a.cmp(b), - (Self::Branch { .. }, _) => Ordering::Less, - (_, Self::Branch { .. }) => Ordering::Greater, - } - } -} - -pub enum FileTreeEvent { - Clicked(PathBuf), - ContextAction(A), -} - -impl FileTree { - pub fn ui( - &self, - ui: &mut egui::Ui, - ctx: &mut impl FnMut(&mut egui::Ui, &Path) -> Option, - ) -> InnerResponse>> { - let mut action = None; - let frame_response = egui::Frame::new().show(ui, |ui| { - egui::ScrollArea::both() - .auto_shrink([false; 2]) - .show(ui, |ui| { - self.ui_inner(ui, &mut action, ctx); - }) - }); - InnerResponse::new(action, frame_response.response) - } - - fn ui_inner( - &self, - ui: &mut egui::Ui, - action: &mut Option>, - ctx: &mut impl FnMut(&mut egui::Ui, &Path) -> Option, - ) { - match self { - Self::Leaf { filename } => { - let response = egui::Button::new(filename.file_name().unwrap().to_string_lossy()) - .frame(false) - .ui(ui); - - response.context_menu(|ui| { - if let Some(a) = ctx(ui, filename) { - *action = Some(FileTreeEvent::ContextAction(a)); - } - }); - - if response.clicked() { - *action = Some(FileTreeEvent::Clicked(PathBuf::from(filename))); - } - } - Self::Branch { path, children } => { - CollapsingHeader::new(path.file_name().unwrap().to_string_lossy()) - .show(ui, |ui| { - for child in children { - child.ui_inner(ui, action, ctx); - } - }) - .header_response - .context_menu(|ui| { - if let Some(a) = ctx(ui, path) { - *action = Some(FileTreeEvent::ContextAction(a)); - } - }); - } - } - } - - pub fn build(path: &Path) -> Self { - match path.is_dir() { - true => Self::Branch { - path: path.to_path_buf(), - children: { - let mut children: Vec = std::fs::read_dir(path) - .map(|ok| { - ok.filter_map(|file| file.ok()) - .map(|file| Self::build(&path.join(file.file_name()))) - .collect() - }) - .unwrap_or_default(); - children.sort_unstable(); - children - }, - }, - false => Self::Leaf { - filename: path.to_path_buf(), - }, - } - } -} diff --git a/libraries/zxq5_egui/src/lib.rs b/libraries/zxq5_egui/src/lib.rs index 8736b9a..85b8083 100644 --- a/libraries/zxq5_egui/src/lib.rs +++ b/libraries/zxq5_egui/src/lib.rs @@ -3,6 +3,5 @@ #![feature(inherent_associated_types)] pub mod buttons; -pub mod context_tree; -pub mod file_tree; pub mod tabs; +pub mod tree; diff --git a/libraries/zxq5_egui/src/tree/file.rs b/libraries/zxq5_egui/src/tree/file.rs new file mode 100644 index 0000000..2dafb6e --- /dev/null +++ b/libraries/zxq5_egui/src/tree/file.rs @@ -0,0 +1,56 @@ +use super::{Node, TreeView}; +use std::path::{Path, PathBuf}; + +#[derive(Clone, PartialOrd, Ord, Eq, PartialEq)] +pub enum FileTree { + Directory { + path: PathBuf, + children: Vec, + }, + File { + path: PathBuf, + }, +} + +impl FileTree { + pub fn build(path: &Path) -> Self { + match path.is_dir() { + true => Self::Directory { + path: path.to_path_buf(), + children: { + let mut children: Vec = std::fs::read_dir(path) + .map(|ok| { + ok.filter_map(|file| file.ok()) + .map(|file| Self::build(&path.join(file.file_name()))) + .collect() + }) + .unwrap_or_default(); + children.sort_unstable(); + children + }, + }, + false => Self::File { + path: path.to_path_buf(), + }, + } + } + + pub fn filename(&self) -> &Path { + match self { + Self::Directory { path, .. } | Self::File { path } => path, + } + } +} + +impl TreeView for FileTree { + fn node(&self) -> Node<'_, Self> { + match self { + Self::Directory { path, children } => { + Node::Branch(path.file_name().unwrap().to_string_lossy(), children) + } + Self::File { path: filename } => { + Node::Leaf(filename.file_name().unwrap().to_string_lossy()) + } + } + } +} diff --git a/libraries/zxq5_egui/src/tree/mod.rs b/libraries/zxq5_egui/src/tree/mod.rs new file mode 100644 index 0000000..68c0ab2 --- /dev/null +++ b/libraries/zxq5_egui/src/tree/mod.rs @@ -0,0 +1,110 @@ +use egui::response::InnerResponse; +use egui::{Ui, Widget}; +use std::borrow::Cow; + +pub mod file; + +pub trait TreeView: Sized + Clone { + fn node(&self) -> Node<'_, Self>; +} + +pub enum TreeEvent<'t, A, T: TreeView> { + Clicked(&'t T), + ContextAction(A, &'t T), +} + +#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub enum Node<'t, T: TreeView> { + Leaf(Cow<'t, str>), + Branch(Cow<'t, str>, &'t Vec), +} + +struct CtxMenu(pub Box Option>); + +impl CtxMenu { + pub fn new(f: impl FnMut(&mut Ui, &Tree) -> Option + 'static) -> Self { + Self(Box::new(f)) + } + + pub fn ui<'t>(&mut self, ui: &mut Ui, tree: &'t Tree) -> Option> { + self.0(ui, tree).and_then(|a| Some(TreeEvent::ContextAction(a, tree))) + } +} + +pub struct MenuTree { + root: Option, + context: Option>, +} + +impl MenuTree +where + Tree: TreeView, +{ + pub fn new() -> Self { + Self { + root: None, + context: None, + } + } + + pub fn root(mut self, tree: Option) -> Self { + self.root = tree.clone(); + self + } + + pub fn ctx(mut self, ctx: impl FnMut(&mut Ui, &Tree) -> Option + 'static) -> Self { + self.context = Some(CtxMenu::new(ctx)); + self + } + + pub fn ui(&mut self, ui: &mut Ui) -> InnerResponse>> { + let mut action = None; + + let frame_response = egui::Frame::new().show(ui, |ui| { + egui::ScrollArea::both() + .auto_shrink([false; 2]) + .show(ui, |ui| { + let Some(root) = &self.root else { + return; + }; + + let Some(ctx) = &mut self.context else { + return; + }; + + Self::ui_inner(ui, &mut action, ctx, root); + }) + }); + InnerResponse::new(action, frame_response.response) + } + + fn ui_inner<'ctx, 't>( + ui: &mut Ui, + action: &mut Option>, + ctx: &mut CtxMenu, + tree: &'t Tree, + ) { + match tree.node() { + Node::Leaf(name) => { + let response = egui::Button::new(name.as_str()).frame(false).ui(ui); + + response.context_menu(|ui| *action = ctx.ui(ui, tree)); + + if response.clicked() { + *action = Some(TreeEvent::Clicked(tree)); + } + } + Node::Branch(name, children) => { + let show_response = egui::CollapsingHeader::new(name.as_str()).show(ui, |ui| { + for child in children.iter() { + Self::ui_inner(ui, action, ctx, child); + } + }); + + show_response + .header_response + .context_menu(|ui| *action = ctx.ui(ui, tree)); + } + } + } +}