Files
worldcoder/src/explorer.rs
T
zxq5 c891a8be58
Continuous integration / build (push) Failing after 4m9s
ui improvements and feature flags for AI integration
2025-08-18 01:06:30 +01:00

406 lines
14 KiB
Rust

use itertools::Itertools;
use std::fs::{self, DirEntry};
// use walkdir::{DirEntry, WalkDir};
use crate::{
PROJECT_FOLDER, RightPanelContent,
content_editor::MainEditor,
editors::{
asset_editor::Asset, content_editor::ContentSection, object_editor::ObjectInstance,
tags::Tag, template_editor::Template,
},
note_editor::Note,
};
pub struct Explorer {
templates: Vec<Template>,
objects: Vec<ObjectInstance>,
notes: Vec<Note>,
documents: Vec<MainEditor>,
tags: Vec<Tag>,
}
impl Explorer {
pub fn new() -> Self {
Self {
templates: Vec::new(),
objects: Vec::new(),
notes: Vec::new(),
documents: Vec::new(),
tags: Vec::new(),
}
}
pub fn objects(&self) -> Vec<ObjectInstance> {
self.objects.clone()
}
pub fn ui(
&mut self,
to_load: &mut Option<RightPanelContent>,
load_doc: &mut Option<MainEditor>,
ui: &mut egui::Ui,
) {
self.load_templates().expect("Failed to load templates");
self.load_objects().expect("Failed to load objects");
self.load_notes().expect("Failed to load notes");
self.load_documents().expect("Failed to load documents");
self.load_tags().expect("Failed to load tags");
ui.vertical(|ui| {
self.render_templates(ui, to_load);
self.render_notes(ui, to_load);
self.render_doc_root(ui, load_doc);
self.render_tags(ui, to_load);
self.render_assets(ui, to_load);
});
}
fn render_templates(&mut self, ui: &mut egui::Ui, to_load: &mut Option<RightPanelContent>) {
egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
ui.make_persistent_id("templates"),
true,
)
.show_header(ui, |ui| {
ui.horizontal(|ui| {
ui.label("Templates");
if ui.button("+").clicked() {
*to_load = Some(RightPanelContent::template(Some(Template::default())));
}
});
})
.body(|ui| {
for template in &self.templates {
let id = ui.make_persistent_id(template.name.clone());
egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
id,
true,
)
.show_header(ui, |ui| {
// load the template
if ui.selectable_label(false, template.name.clone()).clicked() {
*to_load = Some(RightPanelContent::template(Some(template.clone())));
}
// create a new object based on this template
if ui.button("+").clicked() {
*to_load = Some(RightPanelContent::object(Some(ObjectInstance::new(
template,
))));
}
})
.body(|ui| {
for object in &self.objects {
if object.template_id == template.id {
ui.horizontal(|ui| {
ui.add_space(10.0);
// load the object
if ui.selectable_label(false, &object.name).clicked() {
*to_load =
Some(RightPanelContent::object(Some(object.clone())));
}
});
}
}
});
}
});
}
fn render_notes(&mut self, ui: &mut egui::Ui, to_load: &mut Option<RightPanelContent>) {
egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
ui.make_persistent_id("notes"),
true,
)
.show_header(ui, |ui| {
ui.horizontal(|ui| {
ui.label("Notes");
if ui.button("+").clicked() {
*to_load = Some(RightPanelContent::note(Some(Note::default())));
}
});
})
.body(|ui| {
for note in &self.notes {
ui.horizontal(|ui| {
ui.add_space(10.0);
// load the note
if ui.selectable_label(false, &note.name).clicked() {
*to_load = Some(RightPanelContent::note(Some(note.clone())));
}
});
}
});
}
fn render_doc_root(&self, ui: &mut egui::Ui, load_doc: &mut Option<MainEditor>) {
egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
ui.make_persistent_id("projects"),
true,
)
.show_header(ui, |ui| {
ui.horizontal(|ui| {
ui.label("Projects");
if ui.button("+").clicked() {
*load_doc = Some(MainEditor::open(ContentSection::new()));
}
});
})
.body(|ui| {
// Convert MainEditor vec to ContentSection vec
let content_sections: Vec<ContentSection> = self
.documents
.iter()
.map(|doc| doc.content.clone())
.collect();
Self::render_doc_branch(ui, &content_sections, None, load_doc);
});
}
/// Recursively renders a tree of documents.
///
/// Each document is represented by a single element in the `documents` array.
/// The `parent_id` parameter is used to filter out documents that do not have the current
/// parent. If `parent_id` is `None`, all documents are rendered.
///
/// `load_doc` is a mutable reference to a `MainEditor`. When a document is clicked, it
/// is loaded into the `MainEditor` and returned as `Some`.
fn render_doc_branch(
ui: &mut egui::Ui,
documents: &[ContentSection],
parent_id: Option<&str>,
load_doc: &mut Option<MainEditor>,
) {
// Filter documents that have the current parent (or no parent if this is the root)
let child_docs: Vec<&ContentSection> = documents
.iter()
.filter(|doc| doc.parent.as_deref() == parent_id)
.collect();
for doc in child_docs {
egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
ui.make_persistent_id(&doc.id),
false,
)
.show_header(ui, |ui| {
ui.horizontal(|ui| {
// Document title
if ui.selectable_label(false, &doc.title).clicked() {
*load_doc = Some(MainEditor::open(doc.clone()));
}
// Add child button
if ui.button("+").clicked() {
let child = doc.create_child();
*load_doc = Some(MainEditor::open(child));
}
});
})
.body(|ui| {
// recursive call to render the next level of documents
Self::render_doc_branch(ui, documents, Some(&doc.id), load_doc);
});
}
}
fn render_tags(&mut self, ui: &mut egui::Ui, to_load: &mut Option<RightPanelContent>) {
egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
ui.make_persistent_id("tags"),
true,
)
.show_header(ui, |ui| {
ui.horizontal(|ui| {
ui.label("Tags");
if ui.button("+").clicked() {
*to_load = Some(RightPanelContent::Tag(Tag::default()));
}
});
})
.body(|ui| {
for tag in &mut self.tags {
ui.horizontal(|ui| {
ui.add_space(10.0);
// load the tag
if tag.list_ui(ui).clicked() {
*to_load = Some(RightPanelContent::Tag(tag.clone()));
}
});
}
});
}
fn render_assets(&mut self, ui: &mut egui::Ui, to_load: &mut Option<RightPanelContent>) {
egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
ui.make_persistent_id("assets"),
true,
)
.show_header(ui, |ui| {
ui.horizontal(|ui| {
ui.label("Assets");
});
})
.body(|ui| {
let entries = fs::read_dir(PROJECT_FOLDER.join("assets"))
.unwrap()
.filter_map(Result::ok)
.sorted_by(|a, b| {
// Directories first, then files
let a_is_dir = a.file_type().unwrap().is_dir();
let b_is_dir = b.file_type().unwrap().is_dir();
if a_is_dir == b_is_dir {
a.file_name().cmp(&b.file_name())
} else if a_is_dir {
std::cmp::Ordering::Less
} else {
std::cmp::Ordering::Greater
}
})
.collect::<Vec<_>>();
for entry in entries {
Self::render_entry(ui, to_load, &entry);
}
});
}
fn render_entry(ui: &mut egui::Ui, to_load: &mut Option<RightPanelContent>, entry: &DirEntry) {
let file_type = entry.file_type().unwrap();
let is_dir = file_type.is_dir();
let file_name = entry.file_name().to_str().unwrap().to_string();
let path = entry.path();
if is_dir {
let entries = fs::read_dir(path)
.unwrap()
.filter_map(Result::ok)
.collect::<Vec<_>>();
egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
ui.make_persistent_id(&file_name),
false,
)
.show_header(ui, |ui| {
ui.horizontal(|ui| {
ui.label(file_name);
let _clicked = ui.button("+").on_hover_text("Add new item").clicked();
});
})
.body(|ui| {
// recursive call to render the next level of documents
for entry in entries {
Self::render_entry(ui, to_load, &entry);
}
});
} else {
// Handle file
if ui
.selectable_label(false, format!("📄 {file_name}"))
.clicked()
{
// use asset::load to get the file at the path
let asset_path = path.strip_prefix(PROJECT_FOLDER.join("assets")).unwrap();
let asset = Asset::open(asset_path.to_string_lossy().to_string());
*to_load = Some(RightPanelContent::Asset(Box::new(asset)));
}
}
}
// load templates from the templates folder
fn load_templates(&mut self) -> std::io::Result<()> {
let templates_folder = PROJECT_FOLDER.join("templates");
if !templates_folder.exists() {
std::fs::create_dir_all(&templates_folder)?;
}
let mut templates = Vec::new();
for entry in std::fs::read_dir(&templates_folder).unwrap() {
let path = entry.unwrap().path();
match Template::load(path.file_stem().unwrap().to_str().unwrap()) {
Ok(t) => templates.push(t),
Err(err) => eprintln!("Could not parse file {path:?}: {err}"),
}
}
self.templates = templates;
Ok(())
}
// load objects from the objects folder
fn load_objects(&mut self) -> std::io::Result<()> {
let objects_folder = PROJECT_FOLDER.join("objects");
if !objects_folder.exists() {
std::fs::create_dir_all(&objects_folder)?;
}
let mut objects = Vec::new();
for entry in std::fs::read_dir(&objects_folder).unwrap() {
let path = entry.unwrap().path();
match ObjectInstance::load(path.file_stem().unwrap().to_str().unwrap()) {
Ok(o) => objects.push(o),
Err(err) => eprintln!("Could not parse file {path:?}: {err}"),
}
}
self.objects = objects;
Ok(())
}
// load notes from the notes folder
fn load_notes(&mut self) -> std::io::Result<()> {
let notes_folder = PROJECT_FOLDER.join("notes");
if !notes_folder.exists() {
std::fs::create_dir_all(&notes_folder)?;
}
let mut notes = Vec::new();
for entry in std::fs::read_dir(&notes_folder).unwrap() {
let path = entry.unwrap().path();
match Note::load(path.file_stem().unwrap().to_str().unwrap()) {
Ok(note) => notes.push(note),
Err(err) => eprintln!("Could not parse file {path:?}: {err}"),
}
}
self.notes = notes;
Ok(())
}
// load documents from the documents folder
fn load_documents(&mut self) -> std::io::Result<()> {
let documents_folder = PROJECT_FOLDER.join("documents");
if !documents_folder.exists() {
std::fs::create_dir_all(&documents_folder)?;
}
let mut documents = Vec::new();
for entry in std::fs::read_dir(&documents_folder).unwrap() {
let path = entry.unwrap().path();
match ContentSection::load(path.file_stem().unwrap().to_str().unwrap()) {
Ok(document) => documents.push(MainEditor::open(document)),
Err(err) => eprintln!("Could not parse file {path:?}: {err}"),
}
}
self.documents = documents;
Ok(())
}
fn load_tags(&mut self) -> std::io::Result<()> {
self.tags = Tag::load_all();
Ok(())
}
}