added notes, improved other features and removed most bugs
This commit is contained in:
Generated
+13
@@ -3267,6 +3267,8 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.12",
|
||||
"uuid",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3611,6 +3613,17 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "v_frame"
|
||||
version = "0.3.9"
|
||||
|
||||
@@ -20,3 +20,5 @@ serde_json = "1.0.140"
|
||||
chrono = { version = "0.4.41", features = ["serde"] }
|
||||
thiserror = "2.0.12"
|
||||
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};
|
||||
|
||||
mod error;
|
||||
mod explorer;
|
||||
mod main_editor;
|
||||
mod note;
|
||||
mod object;
|
||||
mod scene;
|
||||
mod template;
|
||||
use egui_file::DialogType;
|
||||
use object::ObjectInstance;
|
||||
use template::{FieldType, Template};
|
||||
|
||||
use crate::{explorer::Explorer, note::Note};
|
||||
|
||||
static PROJECT_FOLDER: LazyLock<PathBuf> = LazyLock::new(|| {
|
||||
let mut path = std::env::current_dir().unwrap();
|
||||
path.push("project");
|
||||
@@ -27,98 +33,24 @@ fn main() {
|
||||
}
|
||||
|
||||
pub struct Interface {
|
||||
text: String,
|
||||
dialog: Option<egui_file::FileDialog>,
|
||||
right_panel_content: RightPanelContent,
|
||||
editor: main_editor::MainEditor,
|
||||
scene: scene::EditorScene,
|
||||
explorer: Explorer,
|
||||
}
|
||||
|
||||
impl Interface {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
text: "".to_string(),
|
||||
dialog: None,
|
||||
right_panel_content: RightPanelContent::None,
|
||||
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 {
|
||||
@@ -145,29 +77,25 @@ impl eframe::App for Interface {
|
||||
if let Some(path) = dialog.path() {
|
||||
if dialog.dialog_type() == DialogType::OpenFile {
|
||||
// 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
|
||||
self.right_panel_content = RightPanelContent::instance(
|
||||
Some(path.to_path_buf()),
|
||||
Some(instance),
|
||||
);
|
||||
self.right_panel_content = RightPanelContent::instance(Some(object));
|
||||
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
|
||||
self.right_panel_content = RightPanelContent::template(
|
||||
Some(path.to_path_buf()),
|
||||
Some(template),
|
||||
);
|
||||
self.right_panel_content = RightPanelContent::template(Some(template));
|
||||
self.dialog = None;
|
||||
}
|
||||
} else if dialog.dialog_type() == DialogType::SaveFile {
|
||||
// Handle file dialog for saving templates/instances
|
||||
|
||||
if let RightPanelContent::Template { template, .. } =
|
||||
&mut self.right_panel_content
|
||||
{
|
||||
// set the save location and save
|
||||
template.path = Some(path.to_path_buf());
|
||||
if template.save().is_err() {
|
||||
eprintln!("Failed to save template");
|
||||
} else {
|
||||
@@ -218,14 +146,15 @@ impl eframe::App for Interface {
|
||||
ui.heading("Project Files");
|
||||
ui.separator();
|
||||
|
||||
let mut to_load: Option<RightPanelContent> = None;
|
||||
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
if let Err(e) = self.show_directory(ui, &PROJECT_FOLDER, 0) {
|
||||
ui.label(
|
||||
RichText::new(format!("Error reading directory: {e}"))
|
||||
.color(egui::Color32::RED),
|
||||
);
|
||||
}
|
||||
self.explorer.ui(&mut to_load, ui);
|
||||
});
|
||||
|
||||
if let Some(to_load) = to_load {
|
||||
self.right_panel_content = to_load;
|
||||
}
|
||||
});
|
||||
|
||||
// Main content area
|
||||
@@ -234,19 +163,11 @@ impl eframe::App for Interface {
|
||||
|
||||
match &mut self.right_panel_content {
|
||||
// an instance of a template
|
||||
RightPanelContent::Instance { instance, path: _ } => {
|
||||
RightPanelContent::Object { object } => {
|
||||
// load template from path
|
||||
|
||||
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();
|
||||
let template = Template::load(&object.template_id).unwrap();
|
||||
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,
|
||||
),
|
||||
|
||||
RightPanelContent::Note { note } => note.ui(ui),
|
||||
|
||||
RightPanelContent::None => {
|
||||
ui.centered_and_justified(|ui| {
|
||||
ui.label("No template loaded to edit.");
|
||||
@@ -283,6 +206,7 @@ impl eframe::App for Interface {
|
||||
});
|
||||
|
||||
self.editor.ui(ctx);
|
||||
self.scene.ui(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,15 +219,17 @@ pub enum RightPanelContent {
|
||||
new_field_required: bool,
|
||||
new_field_description: String,
|
||||
},
|
||||
Instance {
|
||||
instance: Box<ObjectInstance>,
|
||||
path: Option<PathBuf>,
|
||||
Object {
|
||||
object: Box<ObjectInstance>,
|
||||
},
|
||||
Note {
|
||||
note: Box<Note>,
|
||||
},
|
||||
None,
|
||||
}
|
||||
|
||||
impl RightPanelContent {
|
||||
fn template(path: Option<PathBuf>, template: Option<Template>) -> Self {
|
||||
fn template(template: Option<Template>) -> Self {
|
||||
Self::Template {
|
||||
template: Box::new(template.unwrap_or_default()),
|
||||
new_field_name: String::new(),
|
||||
@@ -313,18 +239,9 @@ impl RightPanelContent {
|
||||
}
|
||||
}
|
||||
|
||||
fn instance(path: Option<PathBuf>, instance: Option<ObjectInstance>) -> Self {
|
||||
Self::Instance {
|
||||
instance: 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,
|
||||
fn instance(instance: Option<ObjectInstance>) -> Self {
|
||||
Self::Object {
|
||||
object: Box::new(instance.unwrap_or_default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ impl MainEditor {
|
||||
.resizable(true)
|
||||
.default_width(1000.0)
|
||||
.default_height(800.0)
|
||||
.max_height(800.0)
|
||||
.open(&mut self.show_editor)
|
||||
.show(ctx, |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();
|
||||
});
|
||||
}
|
||||
}
|
||||
+68
-101
@@ -1,9 +1,7 @@
|
||||
use core::f32;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use egui::{CollapsingHeader, RichText, TextEdit, Ui, vec2};
|
||||
use egui_file::DialogType;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
@@ -14,32 +12,41 @@ use crate::{
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ObjectInstance {
|
||||
// template info
|
||||
pub template_name: String,
|
||||
pub template_path: PathBuf,
|
||||
pub id: String,
|
||||
pub template_id: String,
|
||||
|
||||
// instance info
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
pub name: String,
|
||||
pub fields: std::collections::HashMap<String, FieldValue>,
|
||||
|
||||
// state (ignore)
|
||||
#[serde(skip)]
|
||||
pub saved: bool,
|
||||
#[serde(skip)]
|
||||
pub path: Option<PathBuf>,
|
||||
|
||||
#[serde(skip)]
|
||||
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 {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
template_name: "New Template Instance".to_string(),
|
||||
name: None,
|
||||
template_path: PathBuf::new(),
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
template_id: "new_template_instance".to_string(),
|
||||
name: "new_object".to_string(),
|
||||
fields: std::collections::HashMap::new(),
|
||||
saved: false,
|
||||
path: None,
|
||||
dialog: None,
|
||||
}
|
||||
}
|
||||
@@ -54,124 +61,84 @@ impl ObjectInstance {
|
||||
}
|
||||
|
||||
Self {
|
||||
template_name: template.name.clone(),
|
||||
name: None,
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
template_id: template.id.clone(),
|
||||
name: "new_object".to_string(),
|
||||
fields,
|
||||
template_path: template
|
||||
.path
|
||||
.clone()
|
||||
.expect("expected the template to have a path!"),
|
||||
saved: false,
|
||||
path: 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)?;
|
||||
std::fs::write(&path, content)?;
|
||||
self.saved = true;
|
||||
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 mut instance: Self = 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);
|
||||
let mut instance: ObjectInstance = serde_json::from_str(&content)?;
|
||||
instance.saved = true;
|
||||
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) {
|
||||
if ui.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl) {
|
||||
if let Err(e) = self.save() {
|
||||
eprintln!("Failed to save: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut Ui, template: &Template) {
|
||||
ui.vertical(|ui| {
|
||||
self.handle_dialogs(ui);
|
||||
|
||||
// Show save status and button
|
||||
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));
|
||||
}
|
||||
|
||||
// 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 let Some(path) = &self.path {
|
||||
if let Err(e) = self.save(path.clone()) {
|
||||
eprintln!("Failed to save: {e}");
|
||||
} else {
|
||||
self.saved = true;
|
||||
}
|
||||
ui.group(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
if self.saved {
|
||||
ui.label(RichText::new("✓ Saved").color(egui::Color32::GREEN));
|
||||
} 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.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}");
|
||||
}
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
// 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 {
|
||||
if let Some(field_value) = self.fields.get_mut(&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");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
+223
-187
@@ -1,8 +1,9 @@
|
||||
use core::fmt;
|
||||
|
||||
use egui::{RichText, ScrollArea};
|
||||
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)]
|
||||
pub enum FieldType {
|
||||
@@ -21,57 +22,77 @@ pub struct FieldDefinition {
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub enum EditorMode {
|
||||
#[default]
|
||||
View,
|
||||
EditTemplate,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Template {
|
||||
pub name: String,
|
||||
pub id: String,
|
||||
|
||||
pub description: Option<String>,
|
||||
pub fields: Vec<FieldDefinition>,
|
||||
|
||||
#[serde(skip)]
|
||||
pub path: Option<PathBuf>,
|
||||
|
||||
#[serde(skip)]
|
||||
pub saved: bool,
|
||||
|
||||
#[serde(skip)]
|
||||
pub editor_mode: EditorMode,
|
||||
pub error: Option<Error>,
|
||||
}
|
||||
|
||||
#[serde(skip)]
|
||||
pub dialog: Option<egui_file::FileDialog>,
|
||||
impl fmt::Debug for Template {
|
||||
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 {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: "New Template".to_string(),
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
description: Some(String::from("Placeholder description")),
|
||||
fields: Vec::new(),
|
||||
saved: false,
|
||||
path: None,
|
||||
editor_mode: EditorMode::default(),
|
||||
dialog: None,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 mut template: Self = serde_json::from_str(&content)?;
|
||||
template.path = Some(path);
|
||||
template.saved = true;
|
||||
Ok(template)
|
||||
}
|
||||
|
||||
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)?;
|
||||
std::fs::write(self.path.as_ref().ok_or("no path")?, content)?;
|
||||
std::fs::write(path, content)?;
|
||||
self.saved = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -84,110 +105,64 @@ impl Template {
|
||||
new_field_required: &mut bool,
|
||||
new_field_description: &mut String,
|
||||
) {
|
||||
match self.editor_mode {
|
||||
EditorMode::View => {
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
if ui.button("Edit Template").clicked() {
|
||||
self.editor_mode = EditorMode::EditTemplate;
|
||||
}
|
||||
if let Some(error) = &mut self.error {
|
||||
error.show(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 {
|
||||
ui.label(RichText::new("✓ Saved").color(egui::Color32::GREEN));
|
||||
} else {
|
||||
ui.label(RichText::new("* Unsaved").color(egui::Color32::YELLOW));
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
EditorMode::EditTemplate => {
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
self.editor_ui(
|
||||
ui,
|
||||
new_field_name,
|
||||
new_field_type,
|
||||
new_field_required,
|
||||
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;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
if ui.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl) {
|
||||
if let Err(e) = self.save() {
|
||||
eprintln!("Failed to save: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn viewer_ui(&self, ui: &mut egui::Ui, new_instance: &mut RightPanelContent) {
|
||||
// Show template view
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading(&self.name);
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.vertical(|ui| {
|
||||
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 let Some(description) = &self.description {
|
||||
ui.separator();
|
||||
ui.label(description);
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
ui.heading("Fields");
|
||||
|
||||
for field in &self.fields {
|
||||
ui.separator();
|
||||
// Save/Cancel buttons
|
||||
ui.horizontal(|ui| {
|
||||
ui.strong(&field.name);
|
||||
ui.label(format!("({:?})", field.field_type));
|
||||
if field.required {
|
||||
ui.label("*");
|
||||
if ui.button("Save Template").clicked() {
|
||||
if let Err(e) = self.save() {
|
||||
eprintln!("Failed to save: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
self.editor_ui(
|
||||
ui,
|
||||
new_field_name,
|
||||
new_field_type,
|
||||
new_field_required,
|
||||
new_field_description,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -199,19 +174,57 @@ impl Template {
|
||||
new_field_required: &mut bool,
|
||||
new_field_description: &mut String,
|
||||
) {
|
||||
// Template name and description
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Template Name:");
|
||||
ui.text_edit_singleline(&mut self.name);
|
||||
});
|
||||
egui::Grid::new("template_grid")
|
||||
.num_columns(2)
|
||||
.striped(true)
|
||||
.show(ui, |ui| {
|
||||
ui.label("Template 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.text_edit_multiline(self.description.get_or_insert_with(String::new));
|
||||
});
|
||||
ui.label("Description:");
|
||||
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.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
|
||||
let mut to_remove = None;
|
||||
@@ -220,52 +233,73 @@ impl Template {
|
||||
|
||||
egui::collapsing_header::CollapsingState::load_with_default_open(ui.ctx(), id, true)
|
||||
.show_header(ui, |ui| {
|
||||
ui.label(field.name.clone());
|
||||
if ui.button("❌").clicked() {
|
||||
to_remove = Some(i);
|
||||
}
|
||||
ui.label(field.name.clone());
|
||||
})
|
||||
.body(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Name:");
|
||||
ui.text_edit_singleline(&mut field.name);
|
||||
});
|
||||
ui.separator();
|
||||
egui::Grid::new("field_grid")
|
||||
.num_columns(2)
|
||||
.striped(true)
|
||||
.show(ui, |ui| {
|
||||
ui.label("Name:");
|
||||
if ui.text_edit_singleline(&mut field.name).changed() {
|
||||
self.saved = false;
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Type:");
|
||||
egui::ComboBox::from_id_salt(format!("field_type_{i}"))
|
||||
.selected_text(format!("{:?}", field.field_type))
|
||||
.show_ui(ui, |ui| {
|
||||
for variant in [
|
||||
FieldType::SingleLine,
|
||||
FieldType::MultiLine,
|
||||
FieldType::Number,
|
||||
FieldType::Date,
|
||||
FieldType::Image,
|
||||
] {
|
||||
ui.selectable_value(
|
||||
&mut field.field_type,
|
||||
variant.clone(),
|
||||
format!("{variant:?}"),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.label("Type:");
|
||||
egui::ComboBox::from_id_salt(format!("field_type_{i}"))
|
||||
.selected_text(format!("{:?}", field.field_type))
|
||||
.show_ui(ui, |ui| {
|
||||
for variant in [
|
||||
FieldType::SingleLine,
|
||||
FieldType::MultiLine,
|
||||
FieldType::Number,
|
||||
FieldType::Date,
|
||||
FieldType::Image,
|
||||
] {
|
||||
if ui
|
||||
.selectable_value(
|
||||
&mut field.field_type,
|
||||
variant.clone(),
|
||||
format!("{variant:?}"),
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
self.saved = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut field.required, "Required");
|
||||
});
|
||||
ui.label("Required:");
|
||||
if ui.checkbox(&mut field.required, "").clicked() {
|
||||
self.saved = false;
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Description:");
|
||||
ui.text_edit_singleline(field.description.get_or_insert_with(String::new));
|
||||
});
|
||||
ui.label("Description:");
|
||||
if ui
|
||||
.text_edit_singleline(
|
||||
field.description.get_or_insert_with(String::new),
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
self.saved = false;
|
||||
}
|
||||
ui.end_row();
|
||||
ui.separator();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Remove field if needed
|
||||
if let Some(index) = to_remove {
|
||||
self.fields.remove(index);
|
||||
self.saved = false;
|
||||
}
|
||||
|
||||
// Add new field
|
||||
@@ -273,13 +307,14 @@ impl Template {
|
||||
ui.heading("Add New Field");
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
egui::Grid::new("field_grid")
|
||||
.num_columns(2)
|
||||
.striped(true)
|
||||
.show(ui, |ui| {
|
||||
ui.label("Name:");
|
||||
ui.text_edit_singleline(new_field_name);
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Type:");
|
||||
egui::ComboBox::from_id_salt("new_field_type")
|
||||
.selected_text(format!("{new_field_type:?}"))
|
||||
@@ -298,35 +333,36 @@ impl Template {
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(new_field_required, "Required");
|
||||
});
|
||||
ui.label("Required:");
|
||||
ui.checkbox(new_field_required, "");
|
||||
ui.end_row();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Description:");
|
||||
ui.text_edit_singleline(new_field_description);
|
||||
ui.end_row();
|
||||
|
||||
if ui.button("Add Field").clicked() && !new_field_name.is_empty() {
|
||||
self.fields.push(FieldDefinition {
|
||||
name: new_field_name.clone(),
|
||||
field_type: new_field_type.clone(),
|
||||
required: *new_field_required,
|
||||
description: if new_field_description.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(new_field_description.clone())
|
||||
},
|
||||
});
|
||||
|
||||
self.saved = false;
|
||||
|
||||
// Reset new field form
|
||||
new_field_name.clear();
|
||||
*new_field_required = false;
|
||||
new_field_description.clear();
|
||||
}
|
||||
});
|
||||
|
||||
if ui.button("Add Field").clicked() && !new_field_name.is_empty() {
|
||||
self.fields.push(FieldDefinition {
|
||||
name: new_field_name.clone(),
|
||||
field_type: new_field_type.clone(),
|
||||
required: *new_field_required,
|
||||
description: if new_field_description.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(new_field_description.clone())
|
||||
},
|
||||
});
|
||||
|
||||
// Reset new field form
|
||||
new_field_name.clear();
|
||||
*new_field_required = false;
|
||||
new_field_description.clear();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user