added notes, improved other features and removed most bugs
This commit is contained in:
Generated
+13
@@ -3267,6 +3267,8 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
|
"uuid",
|
||||||
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3611,6 +3613,17 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uuid"
|
||||||
|
version = "1.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.3",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "v_frame"
|
name = "v_frame"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
|
|||||||
@@ -20,3 +20,5 @@ serde_json = "1.0.140"
|
|||||||
chrono = { version = "0.4.41", features = ["serde"] }
|
chrono = { version = "0.4.41", features = ["serde"] }
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.12"
|
||||||
egui_commonmark = { version = "0.20.0", features = ["embedded_image"] }
|
egui_commonmark = { version = "0.20.0", features = ["embedded_image"] }
|
||||||
|
walkdir = "2.5.0"
|
||||||
|
uuid = { version = "1.17.0", features = ["v4"] }
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{"name":"Note","content":"this is the note! gfjh gfdhgj fgfjhghfd iughuifghuifghuifghuifghuifdg"}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"id": "1da3111e-717a-4452-8209-98c5443fc31a",
|
||||||
|
"template_id": "c96f5e87-7517-44cc-a5ab-42ffd537801d",
|
||||||
|
"name": "sword",
|
||||||
|
"fields": {
|
||||||
|
"description": {
|
||||||
|
"value": "idk, it probably looks cool"
|
||||||
|
},
|
||||||
|
"durability": {
|
||||||
|
"value": "13.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"id": "67e29043-9aa8-4dd9-807c-cc7385eb6847",
|
||||||
|
"template_id": "c96f5e87-7517-44cc-a5ab-42ffd537801d",
|
||||||
|
"name": "sword",
|
||||||
|
"fields": {
|
||||||
|
"durability": {
|
||||||
|
"value": "8.6"
|
||||||
|
},
|
||||||
|
"Icon": {
|
||||||
|
"value": "/home/zxq5/Projects/Minecraft/Packs/ZXQ5 projects/ZXQ5 x/ZXQ5 x-512/ZXQ5 x Release/classic.pack.png"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"value": "idk"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"id": "d1b10e0c-f6b4-4b9d-a96e-cb51213f0243",
|
||||||
|
"template_id": "a24b3ab7-2572-4af4-8457-df26937fd773",
|
||||||
|
"name": "zxq5",
|
||||||
|
"fields": {
|
||||||
|
"Appearance": {
|
||||||
|
"value": "taller than panic"
|
||||||
|
},
|
||||||
|
"Date of Birth": {
|
||||||
|
"value": "2025-07-15"
|
||||||
|
},
|
||||||
|
"Portrait / Image": {
|
||||||
|
"value": "/home/zxq5/Pictures/logos and pfps/YT profile picture background.png"
|
||||||
|
},
|
||||||
|
"Age": {
|
||||||
|
"value": "19"
|
||||||
|
},
|
||||||
|
"Personality": {
|
||||||
|
"value": "coder"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"template_name": "Character",
|
|
||||||
"template_path": "templates/character.json",
|
|
||||||
"name": "zxq5",
|
|
||||||
"fields": {
|
|
||||||
"Age": {
|
|
||||||
"value": "19"
|
|
||||||
},
|
|
||||||
"Birth Date": {
|
|
||||||
"value": "2025-07-11"
|
|
||||||
},
|
|
||||||
"Name": {
|
|
||||||
"value": "zxq5"
|
|
||||||
},
|
|
||||||
"Backstory": {
|
|
||||||
"value": "zxq5"
|
|
||||||
},
|
|
||||||
"Profile Picture": {
|
|
||||||
"value": "/home/zxq5/Pictures/logos and pfps/YT profile picture.png"
|
|
||||||
},
|
|
||||||
"Personality": {
|
|
||||||
"value": "zxq5"
|
|
||||||
},
|
|
||||||
"Appearance": {
|
|
||||||
"value": "zxq5"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "Character",
|
||||||
|
"id": "a24b3ab7-2572-4af4-8457-df26937fd773",
|
||||||
|
"description": "character",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "Portrait / Image",
|
||||||
|
"field_type": "Image",
|
||||||
|
"required": true,
|
||||||
|
"description": "image of character"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Date of Birth",
|
||||||
|
"field_type": "Date",
|
||||||
|
"required": false,
|
||||||
|
"description": "date of birth"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Age",
|
||||||
|
"field_type": "Number",
|
||||||
|
"required": false,
|
||||||
|
"description": "age"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Appearance",
|
||||||
|
"field_type": "MultiLine",
|
||||||
|
"required": false,
|
||||||
|
"description": "character's appearance"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Personality",
|
||||||
|
"field_type": "MultiLine",
|
||||||
|
"required": false,
|
||||||
|
"description": "character's personality"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "Item",
|
||||||
|
"id": "c96f5e87-7517-44cc-a5ab-42ffd537801d",
|
||||||
|
"description": "an in-game item",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "durability",
|
||||||
|
"field_type": "Number",
|
||||||
|
"required": false,
|
||||||
|
"description": "the item's durability"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "description",
|
||||||
|
"field_type": "MultiLine",
|
||||||
|
"required": true,
|
||||||
|
"description": "the item's description"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Icon",
|
||||||
|
"field_type": "Image",
|
||||||
|
"required": false,
|
||||||
|
"description": "yes"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Character",
|
|
||||||
"description": "character template!",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"name": "Profile Picture",
|
|
||||||
"field_type": "Image",
|
|
||||||
"required": false,
|
|
||||||
"description": "Path to character's profile picture"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Name",
|
|
||||||
"field_type": "SingleLine",
|
|
||||||
"required": true,
|
|
||||||
"description": "The full name of the character"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Age",
|
|
||||||
"field_type": "Number",
|
|
||||||
"required": false,
|
|
||||||
"description": "Character's age in years"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Birth Date",
|
|
||||||
"field_type": "Date",
|
|
||||||
"required": false,
|
|
||||||
"description": "Date of birth (YYYY-MM-DD)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Appearance",
|
|
||||||
"field_type": "MultiLine",
|
|
||||||
"required": false,
|
|
||||||
"description": "Physical description of the character"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Personality",
|
|
||||||
"field_type": "MultiLine",
|
|
||||||
"required": false,
|
|
||||||
"description": "Personality traits and behavior"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Backstory",
|
|
||||||
"field_type": "MultiLine",
|
|
||||||
"required": false,
|
|
||||||
"description": "Character's history and background"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
pub struct Error {
|
||||||
|
message: String,
|
||||||
|
visible: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error {
|
||||||
|
pub fn new(message: String) -> Self {
|
||||||
|
Self {
|
||||||
|
message,
|
||||||
|
visible: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show(&mut self, ui: &mut egui::Ui) {
|
||||||
|
egui::Window::new("Error")
|
||||||
|
.open(&mut self.visible)
|
||||||
|
.fixed_size([200.0, 100.0])
|
||||||
|
.show(ui.ctx(), |ui| {
|
||||||
|
ui.label(self.message.clone());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+159
@@ -0,0 +1,159 @@
|
|||||||
|
use egui::RichText;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
PROJECT_FOLDER, RightPanelContent,
|
||||||
|
note::Note,
|
||||||
|
object::ObjectInstance,
|
||||||
|
template::{FieldType, Template},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Explorer {}
|
||||||
|
|
||||||
|
impl Explorer {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui(&mut self, to_load: &mut Option<RightPanelContent>, ui: &mut egui::Ui) {
|
||||||
|
let (templates, objects) = match Self::load_templates() {
|
||||||
|
Ok((templates, objects)) => (templates, objects),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to load project: {e}");
|
||||||
|
ui.label(RichText::new("Failed to load project").color(egui::Color32::RED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
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 {
|
||||||
|
template: Box::new(Template::default()),
|
||||||
|
new_field_name: Default::default(),
|
||||||
|
new_field_type: FieldType::SingleLine,
|
||||||
|
new_field_required: false,
|
||||||
|
new_field_description: Default::default(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.body(|ui| {
|
||||||
|
for template in &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 {
|
||||||
|
object: Box::new(ObjectInstance::new(template)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.body(|ui| {
|
||||||
|
for object in &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 {
|
||||||
|
object: Box::new(object.clone()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let notes = Self::load_notes().unwrap();
|
||||||
|
|
||||||
|
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 {
|
||||||
|
note: Box::new(Note::default()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.body(|ui| {
|
||||||
|
for note in ¬es {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.add_space(10.0);
|
||||||
|
|
||||||
|
// load the note
|
||||||
|
if ui.selectable_label(false, ¬e.name).clicked() {
|
||||||
|
*to_load = Some(RightPanelContent::Note {
|
||||||
|
note: Box::new(note.clone()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
egui::CollapsingHeader::new("Projects").show(ui, |ui| {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_templates() -> std::io::Result<(Vec<Template>, Vec<ObjectInstance>)> {
|
||||||
|
let mut templates = Vec::new();
|
||||||
|
let mut objects = Vec::new();
|
||||||
|
|
||||||
|
for entry in std::fs::read_dir(PROJECT_FOLDER.join("templates")).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}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for entry in std::fs::read_dir(PROJECT_FOLDER.join("objects")).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}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((templates, objects))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_notes() -> std::io::Result<Vec<Note>> {
|
||||||
|
let mut notes = Vec::new();
|
||||||
|
|
||||||
|
for entry in std::fs::read_dir(PROJECT_FOLDER.join("notes")).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}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(notes)
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
-123
@@ -2,13 +2,19 @@ use std::{fs, path::PathBuf, sync::LazyLock};
|
|||||||
|
|
||||||
use egui::{RichText, ScrollArea};
|
use egui::{RichText, ScrollArea};
|
||||||
|
|
||||||
|
mod error;
|
||||||
|
mod explorer;
|
||||||
mod main_editor;
|
mod main_editor;
|
||||||
|
mod note;
|
||||||
mod object;
|
mod object;
|
||||||
|
mod scene;
|
||||||
mod template;
|
mod template;
|
||||||
use egui_file::DialogType;
|
use egui_file::DialogType;
|
||||||
use object::ObjectInstance;
|
use object::ObjectInstance;
|
||||||
use template::{FieldType, Template};
|
use template::{FieldType, Template};
|
||||||
|
|
||||||
|
use crate::{explorer::Explorer, note::Note};
|
||||||
|
|
||||||
static PROJECT_FOLDER: LazyLock<PathBuf> = LazyLock::new(|| {
|
static PROJECT_FOLDER: LazyLock<PathBuf> = LazyLock::new(|| {
|
||||||
let mut path = std::env::current_dir().unwrap();
|
let mut path = std::env::current_dir().unwrap();
|
||||||
path.push("project");
|
path.push("project");
|
||||||
@@ -27,98 +33,24 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct Interface {
|
pub struct Interface {
|
||||||
text: String,
|
|
||||||
dialog: Option<egui_file::FileDialog>,
|
dialog: Option<egui_file::FileDialog>,
|
||||||
right_panel_content: RightPanelContent,
|
right_panel_content: RightPanelContent,
|
||||||
editor: main_editor::MainEditor,
|
editor: main_editor::MainEditor,
|
||||||
|
scene: scene::EditorScene,
|
||||||
|
explorer: Explorer,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Interface {
|
impl Interface {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
text: "".to_string(),
|
|
||||||
dialog: None,
|
dialog: None,
|
||||||
right_panel_content: RightPanelContent::None,
|
right_panel_content: RightPanelContent::None,
|
||||||
editor: main_editor::MainEditor::new(),
|
editor: main_editor::MainEditor::new(),
|
||||||
|
scene: scene::EditorScene::new(),
|
||||||
|
explorer: Explorer::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_directory(
|
|
||||||
&mut self,
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
path: &PathBuf,
|
|
||||||
depth: usize,
|
|
||||||
) -> std::io::Result<()> {
|
|
||||||
if !path.exists() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let indent = " ".repeat(depth);
|
|
||||||
let entries = fs::read_dir(path)?;
|
|
||||||
let mut dirs = Vec::new();
|
|
||||||
let mut files = Vec::new();
|
|
||||||
|
|
||||||
for entry in entries {
|
|
||||||
let entry = entry?;
|
|
||||||
let path = entry.path();
|
|
||||||
if path.is_dir() {
|
|
||||||
dirs.push(path);
|
|
||||||
} else if let Some(ext) = path.extension() {
|
|
||||||
if ext == "json" {
|
|
||||||
// Only show JSON files
|
|
||||||
files.push(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort directories and files alphabetically
|
|
||||||
dirs.sort();
|
|
||||||
files.sort();
|
|
||||||
|
|
||||||
// Show directories first
|
|
||||||
for dir in dirs {
|
|
||||||
let dir_name = dir
|
|
||||||
.file_name()
|
|
||||||
.and_then(|n| n.to_str())
|
|
||||||
.unwrap_or("<invalid>")
|
|
||||||
.to_owned()
|
|
||||||
+ "/";
|
|
||||||
|
|
||||||
if egui::CollapsingHeader::new(dir_name.clone())
|
|
||||||
.default_open(depth < 1)
|
|
||||||
.show(ui, |ui| self.show_directory(ui, &dir, depth + 1))
|
|
||||||
.body_returned
|
|
||||||
.is_none()
|
|
||||||
{
|
|
||||||
ui.label(RichText::new(format!(
|
|
||||||
"{indent}❌ Error reading {dir_name}"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then show files
|
|
||||||
for file in files {
|
|
||||||
if let Some(file_name) = file.file_name().and_then(|n| n.to_str()) {
|
|
||||||
let response = ui.horizontal(|ui| {
|
|
||||||
ui.label(" ".repeat(depth));
|
|
||||||
ui.selectable_label(false, file_name)
|
|
||||||
});
|
|
||||||
|
|
||||||
if response.inner.clicked() {
|
|
||||||
if let Ok(instance) = ObjectInstance::load(file.clone()) {
|
|
||||||
self.right_panel_content =
|
|
||||||
RightPanelContent::instance(Some(file.clone()), Some(instance));
|
|
||||||
} else if let Ok(template) = Template::load(file.clone()) {
|
|
||||||
self.right_panel_content =
|
|
||||||
RightPanelContent::template(Some(file.clone()), Some(template));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Interface {
|
impl Default for Interface {
|
||||||
@@ -145,29 +77,25 @@ impl eframe::App for Interface {
|
|||||||
if let Some(path) = dialog.path() {
|
if let Some(path) = dialog.path() {
|
||||||
if dialog.dialog_type() == DialogType::OpenFile {
|
if dialog.dialog_type() == DialogType::OpenFile {
|
||||||
// Handle file dialog for loading templates/instances
|
// Handle file dialog for loading templates/instances
|
||||||
if let Ok(instance) = ObjectInstance::load(path.to_path_buf()) {
|
if let Ok(object) =
|
||||||
|
ObjectInstance::load(path.file_stem().unwrap().to_str().unwrap())
|
||||||
|
{
|
||||||
// Instance
|
// Instance
|
||||||
self.right_panel_content = RightPanelContent::instance(
|
self.right_panel_content = RightPanelContent::instance(Some(object));
|
||||||
Some(path.to_path_buf()),
|
|
||||||
Some(instance),
|
|
||||||
);
|
|
||||||
self.dialog = None;
|
self.dialog = None;
|
||||||
} else if let Ok(template) = Template::load(path.to_path_buf()) {
|
} else if let Ok(template) =
|
||||||
|
Template::load(path.file_stem().unwrap().to_str().unwrap())
|
||||||
|
{
|
||||||
// Template
|
// Template
|
||||||
self.right_panel_content = RightPanelContent::template(
|
self.right_panel_content = RightPanelContent::template(Some(template));
|
||||||
Some(path.to_path_buf()),
|
|
||||||
Some(template),
|
|
||||||
);
|
|
||||||
self.dialog = None;
|
self.dialog = None;
|
||||||
}
|
}
|
||||||
} else if dialog.dialog_type() == DialogType::SaveFile {
|
} else if dialog.dialog_type() == DialogType::SaveFile {
|
||||||
// Handle file dialog for saving templates/instances
|
// Handle file dialog for saving templates/instances
|
||||||
|
|
||||||
if let RightPanelContent::Template { template, .. } =
|
if let RightPanelContent::Template { template, .. } =
|
||||||
&mut self.right_panel_content
|
&mut self.right_panel_content
|
||||||
{
|
{
|
||||||
// set the save location and save
|
// set the save location and save
|
||||||
template.path = Some(path.to_path_buf());
|
|
||||||
if template.save().is_err() {
|
if template.save().is_err() {
|
||||||
eprintln!("Failed to save template");
|
eprintln!("Failed to save template");
|
||||||
} else {
|
} else {
|
||||||
@@ -218,14 +146,15 @@ impl eframe::App for Interface {
|
|||||||
ui.heading("Project Files");
|
ui.heading("Project Files");
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
|
let mut to_load: Option<RightPanelContent> = None;
|
||||||
|
|
||||||
ScrollArea::vertical().show(ui, |ui| {
|
ScrollArea::vertical().show(ui, |ui| {
|
||||||
if let Err(e) = self.show_directory(ui, &PROJECT_FOLDER, 0) {
|
self.explorer.ui(&mut to_load, ui);
|
||||||
ui.label(
|
|
||||||
RichText::new(format!("Error reading directory: {e}"))
|
|
||||||
.color(egui::Color32::RED),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if let Some(to_load) = to_load {
|
||||||
|
self.right_panel_content = to_load;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Main content area
|
// Main content area
|
||||||
@@ -234,19 +163,11 @@ impl eframe::App for Interface {
|
|||||||
|
|
||||||
match &mut self.right_panel_content {
|
match &mut self.right_panel_content {
|
||||||
// an instance of a template
|
// an instance of a template
|
||||||
RightPanelContent::Instance { instance, path: _ } => {
|
RightPanelContent::Object { object } => {
|
||||||
// load template from path
|
// load template from path
|
||||||
|
let template = Template::load(&object.template_id).unwrap();
|
||||||
let mut temp_path = instance.template_path.clone();
|
|
||||||
|
|
||||||
// check if path is relative
|
|
||||||
if !temp_path.is_absolute() {
|
|
||||||
temp_path = PROJECT_FOLDER.join(temp_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
let template = Template::load(temp_path).unwrap();
|
|
||||||
ScrollArea::vertical().show(ui, |ui| {
|
ScrollArea::vertical().show(ui, |ui| {
|
||||||
instance.ui(ui, &template);
|
object.ui(ui, &template);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,6 +187,8 @@ impl eframe::App for Interface {
|
|||||||
new_field_description,
|
new_field_description,
|
||||||
),
|
),
|
||||||
|
|
||||||
|
RightPanelContent::Note { note } => note.ui(ui),
|
||||||
|
|
||||||
RightPanelContent::None => {
|
RightPanelContent::None => {
|
||||||
ui.centered_and_justified(|ui| {
|
ui.centered_and_justified(|ui| {
|
||||||
ui.label("No template loaded to edit.");
|
ui.label("No template loaded to edit.");
|
||||||
@@ -283,6 +206,7 @@ impl eframe::App for Interface {
|
|||||||
});
|
});
|
||||||
|
|
||||||
self.editor.ui(ctx);
|
self.editor.ui(ctx);
|
||||||
|
self.scene.ui(ctx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,15 +219,17 @@ pub enum RightPanelContent {
|
|||||||
new_field_required: bool,
|
new_field_required: bool,
|
||||||
new_field_description: String,
|
new_field_description: String,
|
||||||
},
|
},
|
||||||
Instance {
|
Object {
|
||||||
instance: Box<ObjectInstance>,
|
object: Box<ObjectInstance>,
|
||||||
path: Option<PathBuf>,
|
},
|
||||||
|
Note {
|
||||||
|
note: Box<Note>,
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RightPanelContent {
|
impl RightPanelContent {
|
||||||
fn template(path: Option<PathBuf>, template: Option<Template>) -> Self {
|
fn template(template: Option<Template>) -> Self {
|
||||||
Self::Template {
|
Self::Template {
|
||||||
template: Box::new(template.unwrap_or_default()),
|
template: Box::new(template.unwrap_or_default()),
|
||||||
new_field_name: String::new(),
|
new_field_name: String::new(),
|
||||||
@@ -313,18 +239,9 @@ impl RightPanelContent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn instance(path: Option<PathBuf>, instance: Option<ObjectInstance>) -> Self {
|
fn instance(instance: Option<ObjectInstance>) -> Self {
|
||||||
Self::Instance {
|
Self::Object {
|
||||||
instance: Box::new(instance.unwrap_or_default()),
|
object: Box::new(instance.unwrap_or_default()),
|
||||||
path,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_saved(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
RightPanelContent::Instance { instance, path: _ } => instance.saved,
|
|
||||||
RightPanelContent::Template { template, .. } => template.path.is_some(),
|
|
||||||
RightPanelContent::None => false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ impl MainEditor {
|
|||||||
.resizable(true)
|
.resizable(true)
|
||||||
.default_width(1000.0)
|
.default_width(1000.0)
|
||||||
.default_height(800.0)
|
.default_height(800.0)
|
||||||
.max_height(800.0)
|
|
||||||
.open(&mut self.show_editor)
|
.open(&mut self.show_editor)
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
|
|||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
use std::fs;
|
||||||
|
|
||||||
|
use egui::RichText;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::PROJECT_FOLDER;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct Note {
|
||||||
|
pub name: String,
|
||||||
|
pub content: String,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub id: String,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub saved: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Note {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
name: "New Note".to_string(),
|
||||||
|
content: "".to_string(),
|
||||||
|
saved: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Note {
|
||||||
|
pub fn new(name: String, content: String) -> Self {
|
||||||
|
Self {
|
||||||
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
name,
|
||||||
|
content,
|
||||||
|
saved: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(&mut self) -> std::io::Result<()> {
|
||||||
|
let id = &self.id;
|
||||||
|
let path = PROJECT_FOLDER.join("notes").join(format!("{id}.json"));
|
||||||
|
fs::write(path, serde_json::to_string(&self)?)?;
|
||||||
|
self.saved = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(id: &str) -> std::io::Result<Self> {
|
||||||
|
let path = PROJECT_FOLDER.join("notes").join(format!("{id}.json"));
|
||||||
|
let content = fs::read_to_string(path)?;
|
||||||
|
let mut note: Note = serde_json::from_str(&content)?;
|
||||||
|
note.id = id.to_string();
|
||||||
|
note.saved = true;
|
||||||
|
Ok(note)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
|
if ui.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl) {
|
||||||
|
if let Err(e) = self.save() {
|
||||||
|
eprintln!("Failed to save: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.group(|ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if self.saved {
|
||||||
|
ui.label(RichText::new("✓ Saved").color(egui::Color32::GREEN));
|
||||||
|
} else {
|
||||||
|
ui.label(RichText::new("* Unsaved").color(egui::Color32::YELLOW));
|
||||||
|
}
|
||||||
|
ui.label(format!("id: {}", self.id));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if ui.button("Save").clicked() {
|
||||||
|
if let Err(e) = self.save() {
|
||||||
|
eprintln!("Failed to save: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
egui::Grid::new("note_grid")
|
||||||
|
.striped(true)
|
||||||
|
.num_columns(2)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.label("Name:");
|
||||||
|
if ui
|
||||||
|
.add(egui::TextEdit::singleline(&mut self.name).frame(false))
|
||||||
|
.changed()
|
||||||
|
{
|
||||||
|
self.saved = false;
|
||||||
|
}
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
ui.label("Content:");
|
||||||
|
if ui
|
||||||
|
.add(
|
||||||
|
egui::TextEdit::multiline(&mut self.content)
|
||||||
|
.desired_rows(1)
|
||||||
|
.frame(false),
|
||||||
|
)
|
||||||
|
.changed()
|
||||||
|
{
|
||||||
|
self.saved = false;
|
||||||
|
}
|
||||||
|
ui.end_row();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+63
-96
@@ -1,9 +1,7 @@
|
|||||||
use core::f32;
|
use core::f32;
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use egui::{CollapsingHeader, RichText, TextEdit, Ui, vec2};
|
use egui::{CollapsingHeader, RichText, TextEdit, Ui, vec2};
|
||||||
use egui_file::DialogType;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -14,32 +12,41 @@ use crate::{
|
|||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct ObjectInstance {
|
pub struct ObjectInstance {
|
||||||
// template info
|
// template info
|
||||||
pub template_name: String,
|
pub id: String,
|
||||||
pub template_path: PathBuf,
|
pub template_id: String,
|
||||||
|
|
||||||
// instance info
|
// instance info
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
pub name: String,
|
||||||
pub name: Option<String>,
|
|
||||||
pub fields: std::collections::HashMap<String, FieldValue>,
|
pub fields: std::collections::HashMap<String, FieldValue>,
|
||||||
|
|
||||||
// state (ignore)
|
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub saved: bool,
|
pub saved: bool,
|
||||||
#[serde(skip)]
|
|
||||||
pub path: Option<PathBuf>,
|
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub dialog: Option<egui_file::FileDialog>,
|
pub dialog: Option<egui_file::FileDialog>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Clone for ObjectInstance {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
id: self.id.clone(),
|
||||||
|
template_id: self.template_id.clone(),
|
||||||
|
name: self.name.clone(),
|
||||||
|
fields: self.fields.clone(),
|
||||||
|
saved: self.saved,
|
||||||
|
dialog: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for ObjectInstance {
|
impl Default for ObjectInstance {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
template_name: "New Template Instance".to_string(),
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
name: None,
|
template_id: "new_template_instance".to_string(),
|
||||||
template_path: PathBuf::new(),
|
name: "new_object".to_string(),
|
||||||
fields: std::collections::HashMap::new(),
|
fields: std::collections::HashMap::new(),
|
||||||
saved: false,
|
saved: false,
|
||||||
path: None,
|
|
||||||
dialog: None,
|
dialog: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,124 +61,84 @@ impl ObjectInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
template_name: template.name.clone(),
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
name: None,
|
template_id: template.id.clone(),
|
||||||
|
name: "new_object".to_string(),
|
||||||
fields,
|
fields,
|
||||||
template_path: template
|
|
||||||
.path
|
|
||||||
.clone()
|
|
||||||
.expect("expected the template to have a path!"),
|
|
||||||
saved: false,
|
saved: false,
|
||||||
path: None,
|
|
||||||
dialog: None,
|
dialog: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save(&self, path: PathBuf) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let path = PROJECT_FOLDER
|
||||||
|
.join("objects")
|
||||||
|
.join(format!("{}.json", &self.id));
|
||||||
|
|
||||||
let content = serde_json::to_string_pretty(self)?;
|
let content = serde_json::to_string_pretty(self)?;
|
||||||
std::fs::write(&path, content)?;
|
std::fs::write(&path, content)?;
|
||||||
|
self.saved = true;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load(path: PathBuf) -> Result<Self, Box<dyn std::error::Error>> {
|
pub fn load(id: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
|
let path = PROJECT_FOLDER.join("objects").join(format!("{id}.json"));
|
||||||
|
|
||||||
let content = std::fs::read_to_string(&path)?;
|
let content = std::fs::read_to_string(&path)?;
|
||||||
let mut instance: Self = serde_json::from_str(&content)?;
|
let mut instance: ObjectInstance = serde_json::from_str(&content)?;
|
||||||
|
|
||||||
// Set the name from the filename (without extension)
|
|
||||||
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
|
|
||||||
instance.name = Some(stem.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
instance.path = Some(path);
|
|
||||||
instance.saved = true;
|
instance.saved = true;
|
||||||
Ok(instance)
|
Ok(instance)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_dialogs(&mut self, ui: &mut Ui) {
|
|
||||||
let path = if let Some(dialog) = &mut self.dialog {
|
|
||||||
match dialog.dialog_type() {
|
|
||||||
DialogType::SaveFile => {
|
|
||||||
if dialog.show(ui.ctx()).selected() {
|
|
||||||
dialog.path().map(|path| (path.to_path_buf(), true))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Handle other dialog types...
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some((path, should_save)) = path {
|
|
||||||
if should_save {
|
|
||||||
if let Err(err) = self.save(path.clone()) {
|
|
||||||
println!("Save error: {err}");
|
|
||||||
} else {
|
|
||||||
// Update the name from the filename when saving
|
|
||||||
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
|
|
||||||
self.name = Some(stem.to_string());
|
|
||||||
}
|
|
||||||
self.path = Some(path);
|
|
||||||
self.saved = true;
|
|
||||||
self.dialog = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ui(&mut self, ui: &mut Ui, template: &Template) {
|
pub fn ui(&mut self, ui: &mut Ui, template: &Template) {
|
||||||
ui.vertical(|ui| {
|
if ui.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl) {
|
||||||
self.handle_dialogs(ui);
|
if let Err(e) = self.save() {
|
||||||
|
eprintln!("Failed to save: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.vertical(|ui| {
|
||||||
// Show save status and button
|
// Show save status and button
|
||||||
|
|
||||||
|
ui.group(|ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if self.saved {
|
if self.saved {
|
||||||
ui.label(RichText::new("✓ Saved").color(egui::Color32::GREEN));
|
ui.label(RichText::new("✓ Saved").color(egui::Color32::GREEN));
|
||||||
} else {
|
} else {
|
||||||
ui.label(RichText::new("* Unsaved").color(egui::Color32::YELLOW));
|
ui.label(RichText::new("* Unsaved").color(egui::Color32::YELLOW));
|
||||||
}
|
}
|
||||||
|
ui.label(format!("id: {}", self.id));
|
||||||
// Show current save path or "Not saved yet"
|
});
|
||||||
let path_display = self
|
});
|
||||||
.path
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|p| p.to_str())
|
|
||||||
.unwrap_or("Not saved yet");
|
|
||||||
ui.label(path_display);
|
|
||||||
|
|
||||||
// File picker button
|
|
||||||
if ui.button("Save As").clicked() && self.dialog.is_none() {
|
|
||||||
self.dialog = Some(egui_file::FileDialog::save_file(Some(
|
|
||||||
PROJECT_FOLDER.clone(),
|
|
||||||
)));
|
|
||||||
self.dialog.as_mut().unwrap().open();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ui.button("Save").clicked() {
|
if ui.button("Save").clicked() {
|
||||||
if let Some(path) = &self.path {
|
if let Err(e) = self.save() {
|
||||||
if let Err(e) = self.save(path.clone()) {
|
|
||||||
eprintln!("Failed to save: {e}");
|
eprintln!("Failed to save: {e}");
|
||||||
} else {
|
|
||||||
self.saved = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If no path is set, request one
|
|
||||||
if self.dialog.is_none() {
|
|
||||||
self.dialog = Some(egui_file::FileDialog::save_file(Some(
|
|
||||||
PROJECT_FOLDER.clone(),
|
|
||||||
)));
|
|
||||||
self.dialog.as_mut().unwrap().open();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||||
// Render each field
|
// Render each field
|
||||||
|
|
||||||
|
// allow name to be edited
|
||||||
|
CollapsingHeader::new("Name")
|
||||||
|
.default_open(true)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.heading(RichText::new("Name").size(14.0).strong());
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
let _ = TextEdit::singleline(&mut self.name)
|
||||||
|
.desired_width(f32::INFINITY)
|
||||||
|
.frame(false)
|
||||||
|
.show(ui)
|
||||||
|
.response;
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
});
|
||||||
|
|
||||||
for field_def in &template.fields {
|
for field_def in &template.fields {
|
||||||
if let Some(field_value) = self.fields.get_mut(&field_def.name) {
|
if let Some(field_value) = self.fields.get_mut(&field_def.name) {
|
||||||
CollapsingHeader::new(&field_def.name)
|
CollapsingHeader::new(&field_def.name)
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
pub struct EditorScene {
|
||||||
|
rect: egui::Rect,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EditorScene {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
rect: egui::Rect::ZERO,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui(&mut self, ctx: &egui::Context) {
|
||||||
|
egui::CentralPanel::default()
|
||||||
|
.frame(egui::Frame::NONE)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
egui::Scene::default()
|
||||||
|
.zoom_range(0.1..=10.0)
|
||||||
|
.show(ui, &mut self.rect, |ui| {
|
||||||
|
egui::Resize::default().auto_sized().show(ui, |ui| {
|
||||||
|
ui.group(|ui| {
|
||||||
|
ui.label("Scene");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+176
-140
@@ -1,8 +1,9 @@
|
|||||||
|
use core::fmt;
|
||||||
|
|
||||||
use egui::{RichText, ScrollArea};
|
use egui::{RichText, ScrollArea};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use crate::{PROJECT_FOLDER, RightPanelContent, object::ObjectInstance};
|
use crate::{PROJECT_FOLDER, RightPanelContent, error::Error, object::ObjectInstance};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub enum FieldType {
|
pub enum FieldType {
|
||||||
@@ -21,57 +22,77 @@ pub struct FieldDefinition {
|
|||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub enum EditorMode {
|
|
||||||
#[default]
|
|
||||||
View,
|
|
||||||
EditTemplate,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Template {
|
pub struct Template {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub id: String,
|
||||||
|
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub fields: Vec<FieldDefinition>,
|
pub fields: Vec<FieldDefinition>,
|
||||||
|
|
||||||
#[serde(skip)]
|
|
||||||
pub path: Option<PathBuf>,
|
|
||||||
|
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub saved: bool,
|
pub saved: bool,
|
||||||
|
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub editor_mode: EditorMode,
|
pub error: Option<Error>,
|
||||||
|
}
|
||||||
|
|
||||||
#[serde(skip)]
|
impl fmt::Debug for Template {
|
||||||
pub dialog: Option<egui_file::FileDialog>,
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("Template")
|
||||||
|
.field("name", &self.name)
|
||||||
|
.field("id", &self.id)
|
||||||
|
.field("description", &self.description)
|
||||||
|
.field("fields", &self.fields)
|
||||||
|
.field("saved", &self.saved)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for Template {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
name: self.name.clone(),
|
||||||
|
id: self.id.clone(),
|
||||||
|
description: self.description.clone(),
|
||||||
|
fields: self.fields.clone(),
|
||||||
|
saved: self.saved,
|
||||||
|
error: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Template {
|
impl Default for Template {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: "New Template".to_string(),
|
name: "New Template".to_string(),
|
||||||
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
description: Some(String::from("Placeholder description")),
|
description: Some(String::from("Placeholder description")),
|
||||||
fields: Vec::new(),
|
fields: Vec::new(),
|
||||||
saved: false,
|
saved: false,
|
||||||
path: None,
|
error: None,
|
||||||
editor_mode: EditorMode::default(),
|
|
||||||
dialog: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Template {
|
impl Template {
|
||||||
pub fn load(path: PathBuf) -> Result<Self, Box<dyn std::error::Error>> {
|
pub fn load(id: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
|
let path = PROJECT_FOLDER.join("templates").join(format!("{id}.json"));
|
||||||
|
|
||||||
let content = std::fs::read_to_string(&path)?;
|
let content = std::fs::read_to_string(&path)?;
|
||||||
let mut template: Self = serde_json::from_str(&content)?;
|
let mut template: Self = serde_json::from_str(&content)?;
|
||||||
template.path = Some(path);
|
template.saved = true;
|
||||||
Ok(template)
|
Ok(template)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let path = PROJECT_FOLDER
|
||||||
|
.join("templates")
|
||||||
|
.join(format!("{}.json", &self.id));
|
||||||
|
|
||||||
let content = serde_json::to_string_pretty(self)?;
|
let content = serde_json::to_string_pretty(self)?;
|
||||||
std::fs::write(self.path.as_ref().ok_or("no path")?, content)?;
|
std::fs::write(path, content)?;
|
||||||
|
self.saved = true;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,40 +105,56 @@ impl Template {
|
|||||||
new_field_required: &mut bool,
|
new_field_required: &mut bool,
|
||||||
new_field_description: &mut String,
|
new_field_description: &mut String,
|
||||||
) {
|
) {
|
||||||
match self.editor_mode {
|
if let Some(error) = &mut self.error {
|
||||||
EditorMode::View => {
|
error.show(ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl) {
|
||||||
|
if let Err(e) = self.save() {
|
||||||
|
eprintln!("Failed to save: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ScrollArea::vertical().show(ui, |ui| {
|
ScrollArea::vertical().show(ui, |ui| {
|
||||||
if ui.button("Edit Template").clicked() {
|
ui.vertical(|ui| {
|
||||||
self.editor_mode = EditorMode::EditTemplate;
|
ui.group(|ui| {
|
||||||
}
|
ui.horizontal(|ui| {
|
||||||
|
|
||||||
if ui.button("New Instance").clicked() {
|
|
||||||
*new_instance = RightPanelContent::Instance {
|
|
||||||
instance: Box::new(ObjectInstance::new(self)),
|
|
||||||
path: None,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.viewer_ui(ui, new_instance);
|
|
||||||
|
|
||||||
if self.saved {
|
if self.saved {
|
||||||
ui.label(RichText::new("✓ Saved").color(egui::Color32::GREEN));
|
ui.label(RichText::new("✓ Saved").color(egui::Color32::GREEN));
|
||||||
} else {
|
} else {
|
||||||
ui.label(RichText::new("* Unsaved").color(egui::Color32::YELLOW));
|
ui.label(RichText::new("* Unsaved").color(egui::Color32::YELLOW));
|
||||||
}
|
}
|
||||||
|
ui.label(format!("id: {}", self.id));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Show current save path or "Not saved yet"
|
// Save/Cancel buttons
|
||||||
let path_display = self
|
ui.horizontal(|ui| {
|
||||||
.path
|
if ui.button("Save Template").clicked() {
|
||||||
.as_ref()
|
if let Err(e) = self.save() {
|
||||||
.and_then(|p| p.to_str())
|
eprintln!("Failed to save: {e}");
|
||||||
.unwrap_or("Not saved yet");
|
|
||||||
ui.label(path_display);
|
|
||||||
}
|
}
|
||||||
EditorMode::EditTemplate => {
|
}
|
||||||
ScrollArea::vertical().show(ui, |ui| {
|
|
||||||
ui.vertical_centered(|ui| {
|
if ui.button("Cancel").clicked() {
|
||||||
|
// load default state
|
||||||
|
*self = Self::load(&self.id).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.button("Create New Instance").clicked() {
|
||||||
|
if self.saved {
|
||||||
|
*new_instance = RightPanelContent::Object {
|
||||||
|
object: Box::new(ObjectInstance::new(self)),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
self.error = Some(Error::new(
|
||||||
|
"You must save the template before creating a new instance!"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
self.editor_ui(
|
self.editor_ui(
|
||||||
ui,
|
ui,
|
||||||
new_field_name,
|
new_field_name,
|
||||||
@@ -125,70 +162,8 @@ impl Template {
|
|||||||
new_field_required,
|
new_field_required,
|
||||||
new_field_description,
|
new_field_description,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Save/Cancel buttons
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
if ui.button("Save Template").clicked() {
|
|
||||||
if let Some(_path) = &self.path {
|
|
||||||
if let Err(e) = self.save() {
|
|
||||||
eprintln!("Failed to save: {e}");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Open save dialog
|
|
||||||
let mut dialog = egui_file::FileDialog::save_file(Some(
|
|
||||||
PROJECT_FOLDER.clone(),
|
|
||||||
));
|
|
||||||
dialog.open();
|
|
||||||
self.dialog = Some(dialog);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ui.button("Cancel").clicked() {
|
|
||||||
self.editor_mode = EditorMode::View;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn viewer_ui(&self, ui: &mut egui::Ui, new_instance: &mut RightPanelContent) {
|
|
||||||
// Show template view
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
ui.heading(&self.name);
|
|
||||||
|
|
||||||
if let Some(description) = &self.description {
|
|
||||||
ui.separator();
|
|
||||||
ui.label(description);
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
ui.heading("Fields");
|
|
||||||
|
|
||||||
for field in &self.fields {
|
|
||||||
ui.separator();
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.strong(&field.name);
|
|
||||||
ui.label(format!("({:?})", field.field_type));
|
|
||||||
if field.required {
|
|
||||||
ui.label("*");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(desc) = &field.description {
|
|
||||||
ui.label(desc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
if ui.button("Create New Instance").clicked() {
|
|
||||||
*new_instance = RightPanelContent::Instance {
|
|
||||||
instance: Box::new(ObjectInstance::new(self)),
|
|
||||||
path: None,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn editor_ui(
|
pub fn editor_ui(
|
||||||
@@ -199,19 +174,57 @@ impl Template {
|
|||||||
new_field_required: &mut bool,
|
new_field_required: &mut bool,
|
||||||
new_field_description: &mut String,
|
new_field_description: &mut String,
|
||||||
) {
|
) {
|
||||||
// Template name and description
|
egui::Grid::new("template_grid")
|
||||||
ui.horizontal(|ui| {
|
.num_columns(2)
|
||||||
|
.striped(true)
|
||||||
|
.show(ui, |ui| {
|
||||||
ui.label("Template Name:");
|
ui.label("Template Name:");
|
||||||
ui.text_edit_singleline(&mut self.name);
|
if ui
|
||||||
});
|
.add(egui::TextEdit::singleline(&mut self.name).frame(false))
|
||||||
|
.changed()
|
||||||
|
{
|
||||||
|
self.saved = false;
|
||||||
|
}
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label("Description:");
|
ui.label("Description:");
|
||||||
ui.text_edit_multiline(self.description.get_or_insert_with(String::new));
|
if ui
|
||||||
|
.add(
|
||||||
|
egui::TextEdit::multiline(self.description.get_or_insert_with(String::new))
|
||||||
|
.desired_rows(1)
|
||||||
|
.frame(false),
|
||||||
|
)
|
||||||
|
.changed()
|
||||||
|
{
|
||||||
|
self.saved = false;
|
||||||
|
}
|
||||||
|
ui.end_row();
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.heading("Fields");
|
|
||||||
|
egui::CollapsingHeader::new("Name").show(ui, |ui: &mut egui::Ui| {
|
||||||
|
egui::Grid::new("field_grid")
|
||||||
|
.num_columns(2)
|
||||||
|
.striped(true)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.label("Name:");
|
||||||
|
ui.label("Name");
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
ui.label("Type:");
|
||||||
|
ui.label("SingleLine");
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
ui.label("Required");
|
||||||
|
ui.label("✓");
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
ui.label("Description:");
|
||||||
|
ui.label("Object name");
|
||||||
|
ui.end_row();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// List of fields
|
// List of fields
|
||||||
let mut to_remove = None;
|
let mut to_remove = None;
|
||||||
@@ -220,18 +233,23 @@ impl Template {
|
|||||||
|
|
||||||
egui::collapsing_header::CollapsingState::load_with_default_open(ui.ctx(), id, true)
|
egui::collapsing_header::CollapsingState::load_with_default_open(ui.ctx(), id, true)
|
||||||
.show_header(ui, |ui| {
|
.show_header(ui, |ui| {
|
||||||
ui.label(field.name.clone());
|
|
||||||
if ui.button("❌").clicked() {
|
if ui.button("❌").clicked() {
|
||||||
to_remove = Some(i);
|
to_remove = Some(i);
|
||||||
}
|
}
|
||||||
|
ui.label(field.name.clone());
|
||||||
})
|
})
|
||||||
.body(|ui| {
|
.body(|ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.separator();
|
||||||
|
egui::Grid::new("field_grid")
|
||||||
|
.num_columns(2)
|
||||||
|
.striped(true)
|
||||||
|
.show(ui, |ui| {
|
||||||
ui.label("Name:");
|
ui.label("Name:");
|
||||||
ui.text_edit_singleline(&mut field.name);
|
if ui.text_edit_singleline(&mut field.name).changed() {
|
||||||
});
|
self.saved = false;
|
||||||
|
}
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label("Type:");
|
ui.label("Type:");
|
||||||
egui::ComboBox::from_id_salt(format!("field_type_{i}"))
|
egui::ComboBox::from_id_salt(format!("field_type_{i}"))
|
||||||
.selected_text(format!("{:?}", field.field_type))
|
.selected_text(format!("{:?}", field.field_type))
|
||||||
@@ -243,22 +261,37 @@ impl Template {
|
|||||||
FieldType::Date,
|
FieldType::Date,
|
||||||
FieldType::Image,
|
FieldType::Image,
|
||||||
] {
|
] {
|
||||||
ui.selectable_value(
|
if ui
|
||||||
|
.selectable_value(
|
||||||
&mut field.field_type,
|
&mut field.field_type,
|
||||||
variant.clone(),
|
variant.clone(),
|
||||||
format!("{variant:?}"),
|
format!("{variant:?}"),
|
||||||
);
|
)
|
||||||
|
.changed()
|
||||||
|
{
|
||||||
|
self.saved = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
ui.end_row();
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.label("Required:");
|
||||||
ui.checkbox(&mut field.required, "Required");
|
if ui.checkbox(&mut field.required, "").clicked() {
|
||||||
});
|
self.saved = false;
|
||||||
|
}
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label("Description:");
|
ui.label("Description:");
|
||||||
ui.text_edit_singleline(field.description.get_or_insert_with(String::new));
|
if ui
|
||||||
|
.text_edit_singleline(
|
||||||
|
field.description.get_or_insert_with(String::new),
|
||||||
|
)
|
||||||
|
.changed()
|
||||||
|
{
|
||||||
|
self.saved = false;
|
||||||
|
}
|
||||||
|
ui.end_row();
|
||||||
|
ui.separator();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -266,6 +299,7 @@ impl Template {
|
|||||||
// Remove field if needed
|
// Remove field if needed
|
||||||
if let Some(index) = to_remove {
|
if let Some(index) = to_remove {
|
||||||
self.fields.remove(index);
|
self.fields.remove(index);
|
||||||
|
self.saved = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add new field
|
// Add new field
|
||||||
@@ -273,13 +307,14 @@ impl Template {
|
|||||||
ui.heading("Add New Field");
|
ui.heading("Add New Field");
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.vertical(|ui| {
|
egui::Grid::new("field_grid")
|
||||||
ui.horizontal(|ui| {
|
.num_columns(2)
|
||||||
|
.striped(true)
|
||||||
|
.show(ui, |ui| {
|
||||||
ui.label("Name:");
|
ui.label("Name:");
|
||||||
ui.text_edit_singleline(new_field_name);
|
ui.text_edit_singleline(new_field_name);
|
||||||
});
|
ui.end_row();
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label("Type:");
|
ui.label("Type:");
|
||||||
egui::ComboBox::from_id_salt("new_field_type")
|
egui::ComboBox::from_id_salt("new_field_type")
|
||||||
.selected_text(format!("{new_field_type:?}"))
|
.selected_text(format!("{new_field_type:?}"))
|
||||||
@@ -298,16 +333,15 @@ impl Template {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
ui.end_row();
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.label("Required:");
|
||||||
ui.checkbox(new_field_required, "Required");
|
ui.checkbox(new_field_required, "");
|
||||||
});
|
ui.end_row();
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label("Description:");
|
ui.label("Description:");
|
||||||
ui.text_edit_singleline(new_field_description);
|
ui.text_edit_singleline(new_field_description);
|
||||||
});
|
ui.end_row();
|
||||||
|
|
||||||
if ui.button("Add Field").clicked() && !new_field_name.is_empty() {
|
if ui.button("Add Field").clicked() && !new_field_name.is_empty() {
|
||||||
self.fields.push(FieldDefinition {
|
self.fields.push(FieldDefinition {
|
||||||
@@ -321,6 +355,8 @@ impl Template {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.saved = false;
|
||||||
|
|
||||||
// Reset new field form
|
// Reset new field form
|
||||||
new_field_name.clear();
|
new_field_name.clear();
|
||||||
*new_field_required = false;
|
*new_field_required = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user