use egui_arbor::{
ActionIcon, DropPosition, IconType, Outliner, OutlinerActions, OutlinerNode,
};
use std::collections::{HashSet, VecDeque};
use std::time::SystemTime;
fn main() -> eframe::Result {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([800.0, 600.0])
.with_title("egui-arbor Basic Example"),
..Default::default()
};
eframe::run_native(
"egui-arbor Example",
options,
Box::new(|cc| {
let mut fonts = egui::FontDefinitions::default();
let font_loaded = load_unicode_font(&mut fonts);
if font_loaded {
cc.egui_ctx.set_fonts(fonts);
}
Ok(Box::new(ExampleApp::new()))
}),
)
}
fn load_unicode_font(fonts: &mut egui::FontDefinitions) -> bool {
let font_paths = [
"/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
"/System/Library/Fonts/Supplemental/Arial.ttf",
"C:\\Windows\\Fonts\\arial.ttf",
"C:\\Windows\\Fonts\\segoeui.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/TTF/DejaVuSans.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
];
for font_path in &font_paths {
if let Ok(font_data) = std::fs::read(font_path) {
fonts.font_data.insert(
"unicode_font".to_owned(),
std::sync::Arc::new(egui::FontData::from_owned(font_data)),
);
fonts.families.get_mut(&egui::FontFamily::Proportional)
.unwrap()
.insert(0, "unicode_font".to_owned());
return true;
}
}
false
}
#[derive(Clone, Debug)]
struct TreeNode {
id: u64,
name: String,
is_collection: bool,
children: Vec<TreeNode>,
}
impl TreeNode {
fn collection(id: u64, name: impl Into<String>, children: Vec<TreeNode>) -> Self {
Self {
id,
name: name.into(),
is_collection: true,
children,
}
}
fn entity(id: u64, name: impl Into<String>) -> Self {
Self {
id,
name: name.into(),
is_collection: false,
children: Vec::new(),
}
}
fn rename_node(&mut self, id: u64, new_name: String) -> bool {
if self.id == id {
self.name = new_name;
return true;
}
for child in &mut self.children {
if child.rename_node(id, new_name.clone()) {
return true;
}
}
false
}
fn remove_node(&mut self, id: u64) -> Option<TreeNode> {
for i in 0..self.children.len() {
if self.children[i].id == id {
return Some(self.children.remove(i));
}
if let Some(node) = self.children[i].remove_node(id) {
return Some(node);
}
}
None
}
fn insert_node(&mut self, target_id: u64, node: TreeNode, position: DropPosition) -> bool {
if self.id == target_id {
match position {
DropPosition::Inside => {
if self.is_collection {
self.children.push(node);
return true;
}
}
_ => {
return false;
}
}
}
for i in 0..self.children.len() {
if self.children[i].id == target_id {
match position {
DropPosition::Before => {
self.children.insert(i, node);
return true;
}
DropPosition::After => {
self.children.insert(i + 1, node);
return true;
}
DropPosition::Inside => {
if self.children[i].is_collection {
self.children[i].children.push(node);
return true;
}
}
}
}
if self.children[i].insert_node(target_id, node.clone(), position) {
return true;
}
}
false
}
}
impl OutlinerNode for TreeNode {
type Id = u64;
fn id(&self) -> Self::Id {
self.id
}
fn name(&self) -> &str {
&self.name
}
fn is_collection(&self) -> bool {
self.is_collection
}
fn children(&self) -> &[Self] {
&self.children
}
fn children_mut(&mut self) -> &mut Vec<Self> {
&mut self.children
}
fn icon(&self) -> Option<IconType> {
if self.is_collection {
Some(IconType::Collection)
} else {
Some(IconType::Entity)
}
}
fn action_icons(&self) -> Vec<ActionIcon> {
vec![
ActionIcon::Visibility, ActionIcon::Lock, ActionIcon::Selection, ]
}
}
#[derive(Clone, Debug)]
struct LogEntry {
timestamp: SystemTime,
message: String,
event_type: EventType,
}
#[derive(Clone, Debug, PartialEq)]
enum EventType {
Selection,
Visibility,
Lock,
DragDrop,
Rename,
}
impl LogEntry {
fn new(message: String, event_type: EventType) -> Self {
Self {
timestamp: SystemTime::now(),
message,
event_type,
}
}
}
struct TreeActions {
selected: HashSet<u64>,
visible: HashSet<u64>,
locked: HashSet<u64>,
event_log: VecDeque<LogEntry>,
max_log_entries: usize,
}
impl TreeActions {
fn new() -> Self {
let mut visible = HashSet::new();
for id in 0..46 {
visible.insert(id);
}
Self {
selected: HashSet::new(),
visible,
locked: HashSet::new(),
event_log: VecDeque::new(),
max_log_entries: 10,
}
}
fn log_event(&mut self, message: String, event_type: EventType) {
self.event_log.push_front(LogEntry::new(message, event_type));
if self.event_log.len() > self.max_log_entries {
self.event_log.pop_back();
}
}
fn get_stats(&self) -> NodeStats {
NodeStats {
total_nodes: 46,
visible_count: self.visible.len(),
hidden_count: 46 - self.visible.len(),
locked_count: self.locked.len(),
selected_count: self.selected.len(),
}
}
}
#[derive(Debug)]
struct NodeStats {
total_nodes: usize,
visible_count: usize,
hidden_count: usize,
locked_count: usize,
selected_count: usize,
}
impl OutlinerActions<TreeNode> for TreeActions {
fn on_rename(&mut self, id: &u64, new_name: String) {
self.log_event(
format!("Renamed node {} to '{}'", id, new_name),
EventType::Rename,
);
}
fn on_move(&mut self, id: &u64, target: &u64, position: DropPosition) {
self.log_event(
format!("Move: node {} → target {} ({:?})", id, target, position),
EventType::DragDrop,
);
}
fn on_select(&mut self, id: &u64, selected: bool) {
if selected {
self.selected.insert(*id);
self.log_event(
format!("Selected node {}", id),
EventType::Selection,
);
} else {
self.selected.remove(id);
self.log_event(
format!("Deselected node {}", id),
EventType::Selection,
);
}
}
fn is_selected(&self, id: &u64) -> bool {
self.selected.contains(id)
}
fn is_visible(&self, id: &u64) -> bool {
self.visible.contains(id)
}
fn is_locked(&self, id: &u64) -> bool {
self.locked.contains(id)
}
fn on_visibility_toggle(&mut self, id: &u64) {
let was_visible = self.visible.contains(id);
let new_state = !was_visible;
if new_state {
self.visible.insert(*id);
self.log_event(
format!("Shown node {}", id),
EventType::Visibility,
);
} else {
self.visible.remove(id);
self.log_event(
format!("Hidden node {}", id),
EventType::Visibility,
);
}
}
fn on_lock_toggle(&mut self, id: &u64) {
let was_locked = self.locked.contains(id);
if was_locked {
self.locked.remove(id);
self.log_event(
format!("Unlocked node {}", id),
EventType::Lock,
);
} else {
self.locked.insert(*id);
self.log_event(
format!("Locked node {}", id),
EventType::Lock,
);
}
}
fn on_selection_toggle(&mut self, id: &u64) {
let is_selected = self.is_selected(id);
self.on_select(id, !is_selected);
}
fn on_custom_action(&mut self, _id: &u64, _icon: &str) {
}
}
struct ExampleApp {
tree: Vec<TreeNode>,
actions: TreeActions,
show_help: bool,
show_stats: bool,
show_log: bool,
}
impl ExampleApp {
fn new() -> Self {
let tree = vec![
TreeNode::collection(
0,
"Project",
vec![
TreeNode::collection(
1,
"src",
vec![
TreeNode::entity(2, "main.rs"),
TreeNode::entity(3, "lib.rs"),
TreeNode::collection(
4,
"components",
vec![
TreeNode::entity(5, "button.rs"),
TreeNode::entity(6, "input.rs"),
TreeNode::entity(7, "layout.rs"),
TreeNode::entity(8, "modal.rs"),
TreeNode::entity(9, "dropdown.rs"),
],
),
TreeNode::collection(
10,
"utils",
vec![
TreeNode::entity(11, "helpers.rs"),
TreeNode::entity(12, "validators.rs"),
TreeNode::entity(13, "formatters.rs"),
],
),
],
),
TreeNode::collection(
14,
"examples",
vec![
TreeNode::entity(15, "basic.rs"),
TreeNode::entity(16, "advanced.rs"),
TreeNode::entity(17, "custom_styling.rs"),
],
),
TreeNode::collection(
18,
"tests",
vec![
TreeNode::entity(19, "integration_test.rs"),
TreeNode::entity(20, "unit_test.rs"),
TreeNode::entity(21, "ui_test.rs"),
],
),
TreeNode::collection(
22,
"assets",
vec![
TreeNode::collection(
23,
"images",
vec![
TreeNode::entity(24, "logo.png"),
TreeNode::entity(25, "icon.svg"),
],
),
TreeNode::collection(
26,
"fonts",
vec![
TreeNode::entity(27, "roboto.ttf"),
TreeNode::entity(28, "monospace.ttf"),
],
),
],
),
TreeNode::entity(29, "Cargo.toml"),
TreeNode::entity(30, "README.md"),
TreeNode::entity(31, ".gitignore"),
TreeNode::entity(32, "LICENSE"),
],
),
TreeNode::collection(
33,
"Documentation",
vec![
TreeNode::entity(34, "getting_started.md"),
TreeNode::entity(35, "api_reference.md"),
TreeNode::entity(36, "examples.md"),
TreeNode::entity(37, "contributing.md"),
TreeNode::collection(
38,
"guides",
vec![
TreeNode::entity(39, "installation.md"),
TreeNode::entity(40, "configuration.md"),
TreeNode::entity(41, "troubleshooting.md"),
],
),
],
),
TreeNode::collection(
42,
"Scripts",
vec![
TreeNode::entity(43, "build.sh"),
TreeNode::entity(44, "test.sh"),
TreeNode::entity(45, "deploy.sh"),
],
),
];
Self {
tree,
actions: TreeActions::new(),
show_help: true,
show_stats: true,
show_log: true,
}
}
}
impl eframe::App for ExampleApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
ui.horizontal(|ui| {
ui.heading("🌳 egui-arbor Comprehensive Example");
ui.separator();
ui.checkbox(&mut self.show_help, "📖 Help");
ui.checkbox(&mut self.show_stats, "📊 Stats");
ui.checkbox(&mut self.show_log, "📋 Event Log");
});
});
if self.show_help {
egui::SidePanel::left("help_panel")
.default_width(250.0)
.show(ctx, |ui| {
ui.heading("📖 Instructions");
ui.separator();
ui.label(egui::RichText::new("Basic Interactions:").strong());
ui.label("• Click to select nodes");
ui.label("• Double-click to rename");
ui.label("• Click ▶/▼ to expand/collapse");
ui.add_space(8.0);
ui.label(egui::RichText::new("Action Icons:").strong());
ui.label("• 👁 Toggle visibility");
ui.label("• 🔒 Toggle lock state");
ui.label("• ☑ Toggle selection");
ui.add_space(8.0);
ui.label(egui::RichText::new("Drag & Drop:").strong());
ui.label("• Drag nodes to reorder");
ui.label("• Drop Before target");
ui.label("• Drop After target");
ui.label("• Drop Inside collections");
ui.add_space(8.0);
ui.label(egui::RichText::new("Visual Indicators:").strong());
ui.label("• 🔵 Selected node");
ui.label("• 👁🗨 Hidden node (dimmed)");
ui.label("• 🔒 Locked node");
ui.add_space(8.0);
ui.label(egui::RichText::new("Tips:").strong());
ui.label("• Try hiding a folder");
ui.label("• Lock a node, then try to drag it");
ui.label("• Drag files into folders");
ui.label("• Reorder items with Before/After");
});
}
if self.show_stats || self.show_log {
egui::SidePanel::right("info_panel")
.default_width(300.0)
.show(ctx, |ui| {
if self.show_stats {
ui.heading("📊 Node Statistics");
ui.separator();
let stats = self.actions.get_stats();
egui::Grid::new("stats_grid")
.num_columns(2)
.spacing([40.0, 4.0])
.striped(true)
.show(ui, |ui| {
ui.label("Total Nodes:");
ui.label(stats.total_nodes.to_string());
ui.end_row();
ui.label("Visible:");
ui.label(format!("{} 👁", stats.visible_count));
ui.end_row();
ui.label("Hidden:");
ui.label(format!("{} 👁🗨", stats.hidden_count));
ui.end_row();
ui.label("Locked:");
ui.label(format!("{} 🔒", stats.locked_count));
ui.end_row();
ui.label("Selected:");
ui.label(format!("{} 🔵", stats.selected_count));
ui.end_row();
});
ui.add_space(8.0);
if !self.actions.selected.is_empty() {
ui.label(egui::RichText::new("Selected Node IDs:")
.color(egui::Color32::from_rgb(100, 150, 255)));
let mut selected_ids: Vec<_> = self.actions.selected.iter().collect();
selected_ids.sort();
for id in selected_ids.iter().take(5) {
ui.label(format!(" • {}", id));
}
if selected_ids.len() > 5 {
ui.label(format!(" ... and {} more", selected_ids.len() - 5));
}
} else {
ui.label(egui::RichText::new("No nodes selected")
.color(egui::Color32::GRAY));
}
ui.separator();
}
if self.show_log {
ui.heading("📋 Event Log");
ui.separator();
egui::ScrollArea::vertical()
.max_height(400.0)
.show(ui, |ui| {
if self.actions.event_log.is_empty() {
ui.label(egui::RichText::new("No events yet...")
.italics()
.color(egui::Color32::GRAY));
} else {
for entry in &self.actions.event_log {
let color = match entry.event_type {
EventType::Selection => egui::Color32::from_rgb(100, 150, 255),
EventType::Visibility => egui::Color32::from_rgb(255, 200, 100),
EventType::Lock => egui::Color32::from_rgb(255, 150, 150),
EventType::DragDrop => egui::Color32::from_rgb(150, 255, 150),
EventType::Rename => egui::Color32::from_rgb(200, 150, 255),
};
ui.horizontal(|ui| {
ui.label(egui::RichText::new("•").color(color));
if let Ok(duration) = entry.timestamp.elapsed() {
let secs = duration.as_secs();
let time_str = if secs < 60 {
format!("{}s ago", secs)
} else {
format!("{}m ago", secs / 60)
};
ui.label(egui::RichText::new(time_str)
.small()
.color(egui::Color32::GRAY));
}
ui.label(&entry.message);
});
}
}
});
}
});
}
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Tree Structure");
ui.separator();
let response = Outliner::new("example_outliner")
.show(ui, &self.tree, &mut self.actions);
if let Some((id, new_name)) = response.renamed() {
for root in &mut self.tree {
if root.rename_node(*id, new_name.to_string()) {
break;
}
}
}
if let Some(drop_event) = response.drop_event() {
let target_id = &drop_event.target;
let position = drop_event.position;
let dragging_ids = response.dragging_nodes();
if !dragging_ids.is_empty() {
let mut removed_nodes = Vec::new();
for drag_id in dragging_ids {
for root in &mut self.tree {
if let Some(node) = root.remove_node(*drag_id) {
removed_nodes.push(node);
break;
}
}
}
let mut all_inserted = true;
for node in removed_nodes {
let mut inserted = false;
for root in &mut self.tree {
if root.insert_node(*target_id, node.clone(), position) {
inserted = true;
break;
}
}
if !inserted {
all_inserted = false;
}
}
if all_inserted {
self.actions.log_event(
format!("✓ Successfully moved {} node(s) to target {} ({:?})",
dragging_ids.len(), target_id, position),
EventType::DragDrop,
);
} else {
self.actions.log_event(
format!("✗ Failed to move some nodes to target {}", target_id),
EventType::DragDrop,
);
}
}
}
ui.separator();
ui.horizontal(|ui| {
if response.changed() {
ui.label(egui::RichText::new("✓ State changed this frame")
.color(egui::Color32::from_rgb(100, 255, 100)));
}
if let Some(id) = response.double_clicked() {
ui.separator();
ui.label(format!("Double-clicked: {}", id));
}
if let Some(id) = response.context_menu() {
ui.separator();
ui.label(format!("Context menu: {}", id));
}
});
});
}
}