Compare commits
3 Commits
editor
...
59f3a24d2c
| Author | SHA1 | Date | |
|---|---|---|---|
| 59f3a24d2c | |||
| ba468cafa7 | |||
| 224300f3ea |
@@ -1,2 +1,3 @@
|
|||||||
/target
|
/target
|
||||||
*/target
|
*/target
|
||||||
|
/project
|
||||||
|
|||||||
Generated
+842
-393
File diff suppressed because it is too large
Load Diff
+6
-5
@@ -4,24 +4,25 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
eframe = "0.31.1"
|
eframe = "0.32.0"
|
||||||
egui = "0.31.1"
|
egui = { version = "0.32.0", features = ["serde"] }
|
||||||
editor = { path = "./editor" }
|
editor = { path = "./editor" }
|
||||||
egui_extras = { version = "0.31.1", features = [
|
egui_extras = { version = "0.32.0", features = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"datepicker",
|
"datepicker",
|
||||||
"file",
|
"file",
|
||||||
"image",
|
"image",
|
||||||
] }
|
] }
|
||||||
egui_file = "0.22.1"
|
egui_file = "0.23.0"
|
||||||
image = { version = "0.25.6", features = ["jpeg", "png"] }
|
image = { version = "0.25.6", features = ["jpeg", "png"] }
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.140"
|
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.21.1", features = ["embedded_image"] }
|
||||||
walkdir = "2.5.0"
|
walkdir = "2.5.0"
|
||||||
uuid = { version = "1.17.0", features = ["v4"] }
|
uuid = { version = "1.17.0", features = ["v4"] }
|
||||||
|
reqwest = { version = "0.12.22", features = ["blocking", "json"] }
|
||||||
|
|
||||||
|
|
||||||
[target.x86_64-pc-windows-gnu]
|
[target.x86_64-pc-windows-gnu]
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ edition = "2024"
|
|||||||
description = "a basic text editor widget with line numbers"
|
description = "a basic text editor widget with line numbers"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
egui = "0.31.1"
|
egui = "0.32.0"
|
||||||
serde = "1"
|
serde = "1"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
|||||||
+11
-19
@@ -1,7 +1,6 @@
|
|||||||
|
use egui::TextBuffer;
|
||||||
|
|
||||||
use egui::{text::LayoutJob, Color32};
|
|
||||||
use egui::widgets::text_edit::TextEditOutput;
|
use egui::widgets::text_edit::TextEditOutput;
|
||||||
|
use egui::{Color32, text::LayoutJob};
|
||||||
|
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
||||||
@@ -137,9 +136,10 @@ impl CodeEditor {
|
|||||||
text.lines().count()
|
text.lines().count()
|
||||||
} as isize;
|
} as isize;
|
||||||
|
|
||||||
let max_indent = total.to_string().len().max(
|
let max_indent = total
|
||||||
!self.numlines_only_natural as usize * self.numlines_shift.to_string().len(),
|
.to_string()
|
||||||
);
|
.len()
|
||||||
|
.max(!self.numlines_only_natural as usize * self.numlines_shift.to_string().len());
|
||||||
let mut counter = (1..=total)
|
let mut counter = (1..=total)
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
let num = i + self.numlines_shift;
|
let num = i + self.numlines_shift;
|
||||||
@@ -160,16 +160,12 @@ impl CodeEditor {
|
|||||||
let width = max_indent as f32
|
let width = max_indent as f32
|
||||||
* self.fontsize
|
* self.fontsize
|
||||||
* 0.5
|
* 0.5
|
||||||
* !(total + self.numlines_shift <= 0 && self.numlines_only_natural) as u8
|
* !(total + self.numlines_shift <= 0 && self.numlines_only_natural) as u8 as f32;
|
||||||
as f32;
|
|
||||||
|
|
||||||
let mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| {
|
let mut layouter = |ui: &egui::Ui, string: &dyn TextBuffer, _wrap_width: f32| {
|
||||||
let layout_job = egui::text::LayoutJob::single_section(
|
let layout_job = egui::text::LayoutJob::single_section(
|
||||||
string.to_string(),
|
string.as_str().to_string(), // Convert TextBuffer to String
|
||||||
egui::TextFormat::simple(
|
egui::TextFormat::simple(egui::FontId::monospace(self.fontsize), Color32::WHITE),
|
||||||
egui::FontId::monospace(self.fontsize),
|
|
||||||
Color32::WHITE,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
ui.fonts(|f| f.layout_job(layout_job))
|
ui.fonts(|f| f.layout_job(layout_job))
|
||||||
};
|
};
|
||||||
@@ -186,11 +182,7 @@ impl CodeEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Show Code Editor
|
/// Show Code Editor
|
||||||
pub fn show(
|
pub fn show(&mut self, ui: &mut egui::Ui, text: &mut dyn egui::TextBuffer) -> TextEditOutput {
|
||||||
&mut self,
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
text: &mut dyn egui::TextBuffer,
|
|
||||||
) -> TextEditOutput {
|
|
||||||
let mut text_edit_output: Option<TextEditOutput> = None;
|
let mut text_edit_output: Option<TextEditOutput> = None;
|
||||||
let code_editor = |ui: &mut egui::Ui| {
|
let code_editor = |ui: &mut egui::Ui| {
|
||||||
ui.horizontal_top(|h| {
|
ui.horizontal_top(|h| {
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 254 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 145 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 157 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"date": "2025-07-17",
|
||||||
|
"project_name": "New Project",
|
||||||
|
"project_author": "Your Name",
|
||||||
|
"project_description": "Description of your project"
|
||||||
|
}
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "New Document",
|
|
||||||
"text": "# The effects of Whimsum dust:\nmore aggression, higher damage, higher rate of fire, lower accuracy"
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"title": "test",
|
||||||
|
"id": "83592caa-f97d-427e-9d6a-50a586c30e6e",
|
||||||
|
"description": "ee",
|
||||||
|
"tags": [],
|
||||||
|
"content": "# Test project\n\n- this project is a test to ensure that this tool can be integrated with AI models correctly\n- I’m testing various prompts and parameters to evaluate its capabilities. The initial focus is on simple tasks like list generation, text summarization, and question answering. More complex scenarios involving code generation and creative writing will follow in subsequent phases. A key aspect of this test project involves documenting all interactions – both the prompts used and the AI’s responses – for later analysis. This allows us to identify patterns, biases, and areas where the tool can be improved. ",
|
||||||
|
"parent": null
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"name":"Note","content":"this is the note! gfjh gfdhgj fgfjhghfd iughuifghuifghuifghuifghuifdg"}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "0078bb24-2fb0-4ecb-b5cb-20d29c5f2f77",
|
|
||||||
"template_id": "b5745688-3c1c-40de-bc3a-2a3e354dd19d",
|
|
||||||
"name": "The SPOONS!",
|
|
||||||
"fields": {
|
|
||||||
"description": {
|
|
||||||
"value": "full of smilers\n"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"id": "161227ef-ba29-41a7-b40a-ed4ac550a8ea",
|
||||||
|
"template_id": "69cf7e1d-96a1-4d2a-9f08-bd3386d4bc69",
|
||||||
|
"name": "nucleus",
|
||||||
|
"fields": {
|
||||||
|
"age": {
|
||||||
|
"Number": 0.0
|
||||||
|
},
|
||||||
|
"parent": {
|
||||||
|
"Link": ""
|
||||||
|
},
|
||||||
|
"dob": {
|
||||||
|
"Date": "1970-01-01"
|
||||||
|
},
|
||||||
|
"pfp": {
|
||||||
|
"Image": "characters/nucleus.png"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"MultiLine": "an AI"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "20beeb2f-363c-49bf-9621-f156d7c7cdd7",
|
|
||||||
"template_id": "b5745688-3c1c-40de-bc3a-2a3e354dd19d",
|
|
||||||
"name": "the brewdog.",
|
|
||||||
"fields": {
|
|
||||||
"description": {
|
|
||||||
"value": "full of smilers\n"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"id": "3ce0e977-9f65-4f4c-a036-67f3d5c25fdc",
|
||||||
|
"template_id": "69cf7e1d-96a1-4d2a-9f08-bd3386d4bc69",
|
||||||
|
"name": "ZXQ5",
|
||||||
|
"fields": {
|
||||||
|
"dob": {
|
||||||
|
"Date": "1970-01-01"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"MultiLine": "yes"
|
||||||
|
},
|
||||||
|
"age": {
|
||||||
|
"Number": 19.1
|
||||||
|
},
|
||||||
|
"parent": {
|
||||||
|
"Link": ""
|
||||||
|
},
|
||||||
|
"pfp": {
|
||||||
|
"Image": "characters/zxq5.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"id": "57429207-5fc1-4bab-a524-c550773c3d45",
|
||||||
|
"template_id": "69cf7e1d-96a1-4d2a-9f08-bd3386d4bc69",
|
||||||
|
"name": "Tayles",
|
||||||
|
"fields": {
|
||||||
|
"pfp": {
|
||||||
|
"Image": "characters/tayles.png"
|
||||||
|
},
|
||||||
|
"parent": {
|
||||||
|
"Link": ""
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"MultiLine": "trainspotter"
|
||||||
|
},
|
||||||
|
"age": {
|
||||||
|
"Number": 17.5
|
||||||
|
},
|
||||||
|
"dob": {
|
||||||
|
"Date": "1970-01-01"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "bd13d252-3f19-4618-bb10-cc45e9f7d301",
|
|
||||||
"template_id": "c96f5e87-7517-44cc-a5ab-42ffd537801d",
|
|
||||||
"name": "Cast Iron Pan",
|
|
||||||
"fields": {
|
|
||||||
"durability": {
|
|
||||||
"value": "9999999"
|
|
||||||
},
|
|
||||||
"Icon": {
|
|
||||||
"value": ""
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"value": "An unburnt pan for bitchslapping, comes with a free punchcard."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"id": "be24e58f-3f79-4c5a-9224-9037eea5f51f",
|
||||||
|
"template_id": "69cf7e1d-96a1-4d2a-9f08-bd3386d4bc69",
|
||||||
|
"name": "The Order",
|
||||||
|
"fields": {
|
||||||
|
"pfp": {
|
||||||
|
"Image": "characters/the order.png"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"MultiLine": "yes"
|
||||||
|
},
|
||||||
|
"dob": {
|
||||||
|
"Date": "1970-01-29"
|
||||||
|
},
|
||||||
|
"parent": {
|
||||||
|
"Link": ""
|
||||||
|
},
|
||||||
|
"age": {
|
||||||
|
"Number": 20.6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"bbeddabd-914c-4648-8262-bf14bfcf8fff"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"id": "deeaee92-bdec-4eb3-bb3b-ee760fc83d45",
|
||||||
|
"template_id": "69cf7e1d-96a1-4d2a-9f08-bd3386d4bc69",
|
||||||
|
"name": "The Chancellor",
|
||||||
|
"fields": {
|
||||||
|
"age": {
|
||||||
|
"Number": 37.0
|
||||||
|
},
|
||||||
|
"parent": {
|
||||||
|
"Link": ""
|
||||||
|
},
|
||||||
|
"dob": {
|
||||||
|
"Date": "1970-01-01"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"MultiLine": "a tall ahh american"
|
||||||
|
},
|
||||||
|
"pfp": {
|
||||||
|
"Image": "characters/the chancellor.jpg"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"bbeddabd-914c-4648-8262-bf14bfcf8fff"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"id": "bbeddabd-914c-4648-8262-bf14bfcf8fff",
|
||||||
|
"name": "American",
|
||||||
|
"description": "an american smh",
|
||||||
|
"color": [
|
||||||
|
0,
|
||||||
|
32,
|
||||||
|
207,
|
||||||
|
255
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "Species",
|
||||||
|
"id": "353649f9-e1f3-46d9-b723-8e56b510b2cc",
|
||||||
|
"description": "A classification system for living or digital entities.",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "Diverged from",
|
||||||
|
"field_type": {
|
||||||
|
"Link": {
|
||||||
|
"template_id": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": false,
|
||||||
|
"on_preview": false,
|
||||||
|
"description": "did this diverge from another documented species?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Appearance / Features",
|
||||||
|
"field_type": "MultiLine",
|
||||||
|
"required": true,
|
||||||
|
"on_preview": true,
|
||||||
|
"description": "anatomy etc."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "behaviour",
|
||||||
|
"field_type": "MultiLine",
|
||||||
|
"required": true,
|
||||||
|
"on_preview": true,
|
||||||
|
"description": "aggressive, collaborative, etc.."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"name": "Character",
|
||||||
|
"id": "69cf7e1d-96a1-4d2a-9f08-bd3386d4bc69",
|
||||||
|
"description": "a character",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "description",
|
||||||
|
"field_type": "MultiLine",
|
||||||
|
"required": true,
|
||||||
|
"on_preview": true,
|
||||||
|
"description": "yes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "age",
|
||||||
|
"field_type": "Number",
|
||||||
|
"required": true,
|
||||||
|
"on_preview": false,
|
||||||
|
"description": "yes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dob",
|
||||||
|
"field_type": "Date",
|
||||||
|
"required": true,
|
||||||
|
"on_preview": false,
|
||||||
|
"description": "yes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "parent",
|
||||||
|
"field_type": {
|
||||||
|
"Link": {
|
||||||
|
"template_id": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"on_preview": false,
|
||||||
|
"description": "yes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "pfp",
|
||||||
|
"field_type": "Image",
|
||||||
|
"required": true,
|
||||||
|
"on_preview": true,
|
||||||
|
"description": "yes"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Location",
|
|
||||||
"id": "b5745688-3c1c-40de-bc3a-2a3e354dd19d",
|
|
||||||
"description": "a place",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"name": "description",
|
|
||||||
"field_type": "MultiLine",
|
|
||||||
"required": true,
|
|
||||||
"description": "what is it like?"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
use egui::{TextEdit, vec2};
|
||||||
|
|
||||||
|
use crate::{PROJECT_FOLDER, util};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Asset {
|
||||||
|
pub new_name: String,
|
||||||
|
pub name: String,
|
||||||
|
pub saved: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Asset {
|
||||||
|
pub fn open(name: String) -> Self {
|
||||||
|
Self {
|
||||||
|
new_name: name.clone(),
|
||||||
|
name,
|
||||||
|
saved: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(&mut self) {
|
||||||
|
let old_path = Self::path(&self.name);
|
||||||
|
let new_path = Self::path(&self.new_name);
|
||||||
|
|
||||||
|
println!("old_path: {old_path:?}");
|
||||||
|
println!("new_path: {new_path:?}");
|
||||||
|
|
||||||
|
// move from src dir to name path
|
||||||
|
if let Err(err) = std::fs::rename(&old_path, &new_path) {
|
||||||
|
match err.kind() {
|
||||||
|
std::io::ErrorKind::NotFound => {
|
||||||
|
let dir = new_path.parent().unwrap();
|
||||||
|
if !dir.exists() {
|
||||||
|
std::fs::create_dir_all(dir).unwrap();
|
||||||
|
}
|
||||||
|
std::fs::rename(&old_path, &new_path).unwrap();
|
||||||
|
}
|
||||||
|
_ => panic!("Failed to rename file: {err}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.saved = true;
|
||||||
|
self.name = self.new_name.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path(name: &str) -> std::path::PathBuf {
|
||||||
|
PROJECT_FOLDER.join("assets").join(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
util::saved_status(ui, self.saved, &self.name, &self.new_name);
|
||||||
|
|
||||||
|
if ui.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl)
|
||||||
|
|| ui.button("Save").clicked()
|
||||||
|
{
|
||||||
|
self.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.strong("Filename:");
|
||||||
|
if TextEdit::singleline(&mut self.new_name)
|
||||||
|
.desired_width(f32::INFINITY)
|
||||||
|
.frame(false)
|
||||||
|
.show(ui)
|
||||||
|
.response
|
||||||
|
.changed()
|
||||||
|
{
|
||||||
|
self.saved = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
if let Ok(bytes) = std::fs::read(Self::path(&self.name)) {
|
||||||
|
let image_source = egui::ImageSource::Bytes {
|
||||||
|
uri: std::borrow::Cow::Owned(self.name.clone()),
|
||||||
|
bytes: bytes.into(),
|
||||||
|
};
|
||||||
|
ui.add(
|
||||||
|
egui::Image::new(image_source)
|
||||||
|
.max_size(vec2(ui.available_width(), f32::INFINITY)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,331 @@
|
|||||||
|
use egui::{TextEdit, text};
|
||||||
|
use egui_commonmark::{CommonMarkCache, CommonMarkViewer};
|
||||||
|
use serde::{self, Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{PROJECT_FOLDER, editors::tags::Tag, llm_integration::content_llm::ai_enabled, util};
|
||||||
|
|
||||||
|
pub struct MainEditor {
|
||||||
|
pub content: ContentSection,
|
||||||
|
pub show_editor: bool,
|
||||||
|
pub show_preview: bool,
|
||||||
|
preview_cache: CommonMarkCache,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for MainEditor {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
content: self.content.clone(),
|
||||||
|
|
||||||
|
show_editor: self.show_editor,
|
||||||
|
show_preview: self.show_preview,
|
||||||
|
preview_cache: CommonMarkCache::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct ContentSection {
|
||||||
|
#[serde(default)]
|
||||||
|
pub title: String,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: String,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub description: String,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub tags: Vec<String>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub content: String,
|
||||||
|
|
||||||
|
// parent id
|
||||||
|
#[serde(default)]
|
||||||
|
pub parent: Option<String>,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub saved: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContentSection {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
title: String::new(),
|
||||||
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
description: String::new(),
|
||||||
|
tags: Vec::new(),
|
||||||
|
content: String::new(),
|
||||||
|
parent: None,
|
||||||
|
saved: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let path = PROJECT_FOLDER
|
||||||
|
.join("documents")
|
||||||
|
.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(id: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
|
let path = PROJECT_FOLDER.join("documents").join(format!("{id}.json"));
|
||||||
|
|
||||||
|
let content = std::fs::read_to_string(&path)?;
|
||||||
|
let mut section: Self = serde_json::from_str(&content)?;
|
||||||
|
section.saved = true;
|
||||||
|
section.id = id.to_string();
|
||||||
|
Ok(section)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_child(&self) -> Self {
|
||||||
|
let mut child = Self::new();
|
||||||
|
child.title = format!("{} (Child)", self.title);
|
||||||
|
child.parent = Some(self.id.clone());
|
||||||
|
child
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MainEditor {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
content: ContentSection::new(),
|
||||||
|
show_editor: false, // Start with editor hidden
|
||||||
|
show_preview: false,
|
||||||
|
preview_cache: CommonMarkCache::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open(content: ContentSection) -> Self {
|
||||||
|
Self {
|
||||||
|
content,
|
||||||
|
show_editor: true,
|
||||||
|
show_preview: false,
|
||||||
|
preview_cache: CommonMarkCache::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui(&mut self, ctx: &egui::Context) {
|
||||||
|
// Show the editor window if enabled
|
||||||
|
let mut show = self.show_editor;
|
||||||
|
if show {
|
||||||
|
egui::Window::new("Markdown Editor")
|
||||||
|
.resizable(true)
|
||||||
|
.default_width(1000.0)
|
||||||
|
.default_height(800.0)
|
||||||
|
.open(&mut show)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
// check for Ctrl+S to save
|
||||||
|
if ui.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl) {
|
||||||
|
if let Err(e) = self.content.save() {
|
||||||
|
eprintln!("Failed to save: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// display save state
|
||||||
|
util::saved_status(
|
||||||
|
ui,
|
||||||
|
self.content.saved,
|
||||||
|
&self.content.id,
|
||||||
|
&self.content.title,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save/Cancel buttons
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
// save button
|
||||||
|
if ui.button("Save").clicked() {
|
||||||
|
if let Err(e) = self.content.save() {
|
||||||
|
eprintln!("Failed to save: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create copy button
|
||||||
|
if ui.button("Create Copy").clicked() {
|
||||||
|
let mut copy = self.clone();
|
||||||
|
copy.content.id = uuid::Uuid::new_v4().to_string();
|
||||||
|
copy.content.title = format!("{} (Copy)", self.content.title);
|
||||||
|
copy.content.save().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete button
|
||||||
|
if ui.button("Delete").clicked() {
|
||||||
|
std::fs::remove_file(
|
||||||
|
PROJECT_FOLDER
|
||||||
|
.join("documents")
|
||||||
|
.join(format!("{}.json", self.content.id)),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
*self = Self::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
// revert changes button
|
||||||
|
if ui.button("Revert changes").clicked() {
|
||||||
|
self.content = ContentSection::load(&self.content.id).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// preview toggle
|
||||||
|
ui.checkbox(&mut self.show_preview, "Preview");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
// Name and description grid
|
||||||
|
egui::Grid::new("top_grid")
|
||||||
|
.striped(true)
|
||||||
|
.num_columns(2)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.strong("Name");
|
||||||
|
if ui
|
||||||
|
.add(
|
||||||
|
TextEdit::singleline(&mut self.content.title)
|
||||||
|
.desired_width(f32::INFINITY)
|
||||||
|
.frame(false),
|
||||||
|
)
|
||||||
|
.changed()
|
||||||
|
{
|
||||||
|
self.content.saved = false;
|
||||||
|
}
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
ui.strong("Description");
|
||||||
|
if ui
|
||||||
|
.add(
|
||||||
|
TextEdit::singleline(&mut self.content.description)
|
||||||
|
.desired_width(f32::INFINITY)
|
||||||
|
.frame(false),
|
||||||
|
)
|
||||||
|
.changed()
|
||||||
|
{
|
||||||
|
self.content.saved = false;
|
||||||
|
}
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
ui.strong("Tags");
|
||||||
|
Tag::selector_ui(
|
||||||
|
&mut self.content.tags,
|
||||||
|
ui,
|
||||||
|
Some(&mut self.content.saved),
|
||||||
|
);
|
||||||
|
ui.end_row();
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
if self.show_preview {
|
||||||
|
self.preview_ui(ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.editor_ui(ui);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.show_editor = show;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preview_ui(&mut self, ui: &mut egui::Ui) {
|
||||||
|
// Preview area
|
||||||
|
egui::SidePanel::right("preview_panel")
|
||||||
|
.resizable(true)
|
||||||
|
.default_width(ui.available_width() / 2.0)
|
||||||
|
.show_inside(ui, |ui| {
|
||||||
|
// Preview area with centered content and max width
|
||||||
|
egui::ScrollArea::both()
|
||||||
|
.auto_shrink([false, false])
|
||||||
|
.id_salt("preview_scroll")
|
||||||
|
.show(ui, |ui| {
|
||||||
|
let max_width = 600;
|
||||||
|
let available_width = ui.available_width();
|
||||||
|
let content_width = (max_width as f32).min(available_width);
|
||||||
|
let padding = (available_width - content_width) / 2.0;
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.add_space(padding);
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.set_width(content_width);
|
||||||
|
ui.add_space(15.0);
|
||||||
|
|
||||||
|
ui.set_min_width(max_width as f32);
|
||||||
|
|
||||||
|
CommonMarkViewer::new()
|
||||||
|
.default_width(Some(max_width))
|
||||||
|
.max_image_width(Some(512))
|
||||||
|
.show(ui, &mut self.preview_cache, &self.content.content);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn editor_ui(&mut self, ui: &mut egui::Ui) {
|
||||||
|
let response = egui::ScrollArea::both()
|
||||||
|
.auto_shrink([false, false])
|
||||||
|
.id_salt("editor_scroll")
|
||||||
|
.show(ui, |ui| {
|
||||||
|
let max_width = 600;
|
||||||
|
let available_width = ui.available_width();
|
||||||
|
let content_width = (max_width as f32).min(available_width);
|
||||||
|
let padding = (available_width - content_width).max(30.0) / 2.0;
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.add_space(padding);
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.set_width(content_width);
|
||||||
|
ui.add_space(15.0);
|
||||||
|
|
||||||
|
ui.set_min_width(max_width as f32);
|
||||||
|
|
||||||
|
let text_edit = TextEdit::multiline(&mut self.content.content)
|
||||||
|
.id_source("MainEditor_editor")
|
||||||
|
.font(egui::TextStyle::Monospace)
|
||||||
|
.interactive(true)
|
||||||
|
.frame(false)
|
||||||
|
.lock_focus(true)
|
||||||
|
.hint_text("Type here...")
|
||||||
|
.desired_width(max_width as f32);
|
||||||
|
|
||||||
|
let mut ctx_menu = false;
|
||||||
|
let response = ui
|
||||||
|
.add_sized(
|
||||||
|
egui::vec2(max_width as f32 - 30.0, ui.available_height()),
|
||||||
|
text_edit,
|
||||||
|
)
|
||||||
|
.on_hover_text("Right click to open context menu")
|
||||||
|
.context_menu(|ui| {
|
||||||
|
ctx_menu = true;
|
||||||
|
|
||||||
|
ui.menu_button("AI Actions", |ui| {
|
||||||
|
ui.add_enabled_ui(ai_enabled(), |ui| {
|
||||||
|
if ui.button("Summarise").clicked() {
|
||||||
|
println!("Summarise");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.button("Continue").clicked() {
|
||||||
|
let content = self.content.content.clone();
|
||||||
|
let response =
|
||||||
|
crate::llm_integration::content_llm::continue_content(
|
||||||
|
&content, "", 1024,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
self.content.content.push_str(&response);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(response) = response {
|
||||||
|
if response.response.changed() || ctx_menu {
|
||||||
|
self.content.saved = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
use std::io::Read;
|
||||||
|
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
use egui_extras::DatePickerButton;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::PROJECT_FOLDER;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct ProjectContext {
|
||||||
|
date: NaiveDate,
|
||||||
|
project_name: String,
|
||||||
|
project_author: String,
|
||||||
|
project_description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProjectContext {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load() -> Self {
|
||||||
|
let path = PROJECT_FOLDER.join("context.json");
|
||||||
|
if let Ok(mut file) = std::fs::File::open(path) {
|
||||||
|
let mut contents = String::new();
|
||||||
|
file.read_to_string(&mut contents).unwrap();
|
||||||
|
if let Ok(proj) = serde_json::from_str(&contents) {
|
||||||
|
return proj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(&self) {
|
||||||
|
let path = PROJECT_FOLDER.join("context.json");
|
||||||
|
let content = serde_json::to_string_pretty(self).unwrap();
|
||||||
|
std::fs::write(path, content).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
|
// table
|
||||||
|
egui::Grid::new("context_editor")
|
||||||
|
.striped(true)
|
||||||
|
.num_columns(2)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.label("Project Name");
|
||||||
|
ui.text_edit_singleline(&mut self.project_name);
|
||||||
|
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
ui.label("Project Author");
|
||||||
|
ui.text_edit_singleline(&mut self.project_author);
|
||||||
|
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
ui.label("Project Description");
|
||||||
|
ui.text_edit_singleline(&mut self.project_description);
|
||||||
|
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
ui.label("Date");
|
||||||
|
ui.add(DatePickerButton::new(&mut self.date));
|
||||||
|
|
||||||
|
ui.end_row();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ProjectContext {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
date: chrono::Local::now().naive_local().into(),
|
||||||
|
project_name: "New Project".to_string(),
|
||||||
|
project_author: "Your Name".to_string(),
|
||||||
|
project_description: "Description of your project".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
pub mod asset_editor;
|
||||||
|
pub mod content_editor;
|
||||||
|
pub mod context_editor;
|
||||||
|
pub mod note_editor;
|
||||||
|
pub mod object_editor;
|
||||||
|
pub mod tags;
|
||||||
|
pub mod template_editor;
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
use std::fs;
|
||||||
|
|
||||||
|
use egui::TextEdit;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{PROJECT_FOLDER, editors::tags::Tag, util};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct Note {
|
||||||
|
pub name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub content: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub subject: String,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub tags: Vec<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(),
|
||||||
|
subject: "".to_string(),
|
||||||
|
content: "".to_string(),
|
||||||
|
tags: Vec::new(),
|
||||||
|
saved: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Note {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
name: "New Note".to_string(),
|
||||||
|
subject: "".to_string(),
|
||||||
|
content: "".to_string(),
|
||||||
|
tags: Vec::new(),
|
||||||
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
util::saved_status(ui, self.saved, &self.id, &self.name);
|
||||||
|
|
||||||
|
if ui.button("Save").clicked() {
|
||||||
|
if let Err(e) = self.save() {
|
||||||
|
eprintln!("Failed to save: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = ui.make_persistent_id("note_name");
|
||||||
|
egui::collapsing_header::CollapsingState::load_with_default_open(ui.ctx(), id, true)
|
||||||
|
.show_header(ui, |ui| {
|
||||||
|
ui.strong("Name");
|
||||||
|
})
|
||||||
|
.body(|ui| {
|
||||||
|
ui.separator();
|
||||||
|
if TextEdit::singleline(&mut self.name)
|
||||||
|
.desired_width(f32::INFINITY)
|
||||||
|
.frame(false)
|
||||||
|
.show(ui)
|
||||||
|
.response
|
||||||
|
.changed()
|
||||||
|
{
|
||||||
|
self.saved = false;
|
||||||
|
}
|
||||||
|
ui.separator();
|
||||||
|
});
|
||||||
|
|
||||||
|
let id = ui.make_persistent_id("note_tags");
|
||||||
|
egui::collapsing_header::CollapsingState::load_with_default_open(ui.ctx(), id, true)
|
||||||
|
.show_header(ui, |ui| {
|
||||||
|
ui.strong("Tags");
|
||||||
|
})
|
||||||
|
.body(|ui| {
|
||||||
|
ui.separator();
|
||||||
|
Tag::selector_ui(&mut self.tags, ui, Some(&mut self.saved));
|
||||||
|
ui.separator();
|
||||||
|
});
|
||||||
|
|
||||||
|
let id = ui.make_persistent_id("note_subject");
|
||||||
|
egui::collapsing_header::CollapsingState::load_with_default_open(ui.ctx(), id, true)
|
||||||
|
.show_header(ui, |ui| {
|
||||||
|
ui.strong("Subject");
|
||||||
|
})
|
||||||
|
.body(|ui| {
|
||||||
|
ui.separator();
|
||||||
|
if TextEdit::singleline(&mut self.subject)
|
||||||
|
.desired_width(f32::INFINITY)
|
||||||
|
.frame(false)
|
||||||
|
.show(ui)
|
||||||
|
.response
|
||||||
|
.changed()
|
||||||
|
{
|
||||||
|
self.saved = false;
|
||||||
|
}
|
||||||
|
ui.separator();
|
||||||
|
});
|
||||||
|
|
||||||
|
let id = ui.make_persistent_id("note_content");
|
||||||
|
egui::collapsing_header::CollapsingState::load_with_default_open(ui.ctx(), id, true)
|
||||||
|
.show_header(ui, |ui| {
|
||||||
|
ui.strong("Content");
|
||||||
|
})
|
||||||
|
.body(|ui| {
|
||||||
|
ui.separator();
|
||||||
|
if TextEdit::multiline(&mut self.content)
|
||||||
|
.desired_width(f32::INFINITY)
|
||||||
|
.desired_rows(5)
|
||||||
|
.frame(false)
|
||||||
|
.show(ui)
|
||||||
|
.response
|
||||||
|
.changed()
|
||||||
|
{
|
||||||
|
self.saved = false;
|
||||||
|
}
|
||||||
|
ui.separator();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
use core::f32;
|
||||||
|
use egui::{CollapsingHeader, RichText, Sense, TextEdit, Ui, UiBuilder, vec2};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
PROJECT_FOLDER, RightPanelContent,
|
||||||
|
editors::{
|
||||||
|
tags::Tag,
|
||||||
|
template_editor::{FieldValue, Template},
|
||||||
|
},
|
||||||
|
util,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub type ObjectId = String;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ObjectInstance {
|
||||||
|
// template info
|
||||||
|
pub id: ObjectId,
|
||||||
|
pub template_id: String,
|
||||||
|
|
||||||
|
// instance info
|
||||||
|
pub name: String,
|
||||||
|
pub fields: std::collections::HashMap<String, FieldValue>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub tags: Vec<String>,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub saved: bool,
|
||||||
|
|
||||||
|
#[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(),
|
||||||
|
tags: self.tags.clone(),
|
||||||
|
saved: self.saved,
|
||||||
|
dialog: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ObjectInstance {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
template_id: "new_template_instance".to_string(),
|
||||||
|
name: "new_object".to_string(),
|
||||||
|
fields: std::collections::HashMap::new(),
|
||||||
|
tags: Vec::new(),
|
||||||
|
saved: false,
|
||||||
|
dialog: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectInstance {
|
||||||
|
pub fn new(template: &Template) -> Self {
|
||||||
|
let mut fields = std::collections::HashMap::new();
|
||||||
|
|
||||||
|
for field in &template.fields {
|
||||||
|
fields.insert(field.name.clone(), FieldValue::from_type(&field.field_type));
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
template_id: template.id.clone(),
|
||||||
|
name: "new_object".to_string(),
|
||||||
|
fields,
|
||||||
|
tags: Vec::new(),
|
||||||
|
saved: false,
|
||||||
|
dialog: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(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: ObjectInstance = serde_json::from_str(&content)?;
|
||||||
|
instance.saved = true;
|
||||||
|
Ok(instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui(
|
||||||
|
&mut self,
|
||||||
|
ui: &mut Ui,
|
||||||
|
template: &Template,
|
||||||
|
right_panel: &mut Option<RightPanelContent>,
|
||||||
|
objects: &mut [ObjectInstance],
|
||||||
|
) {
|
||||||
|
let _ = right_panel;
|
||||||
|
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.vertical(|ui| {
|
||||||
|
// Show save status and button
|
||||||
|
|
||||||
|
util::saved_status(ui, self.saved, &self.id, &self.name);
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if ui.button("Save").clicked() {
|
||||||
|
if let Err(e) = self.save() {
|
||||||
|
eprintln!("Failed to save: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.button("Create Copy").clicked() {
|
||||||
|
let mut copy = self.clone();
|
||||||
|
copy.id = uuid::Uuid::new_v4().to_string();
|
||||||
|
copy.dialog = None;
|
||||||
|
copy.name = format!("{} (Copy)", self.name);
|
||||||
|
copy.save().unwrap();
|
||||||
|
|
||||||
|
*right_panel = Some(RightPanelContent::Object(Box::new(copy)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.button("Delete").clicked() {
|
||||||
|
std::fs::remove_file(
|
||||||
|
PROJECT_FOLDER
|
||||||
|
.join("objects")
|
||||||
|
.join(format!("{}.json", self.id)),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
*right_panel = Some(RightPanelContent::None);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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.separator();
|
||||||
|
let _ = TextEdit::singleline(&mut self.name)
|
||||||
|
.desired_width(f32::INFINITY)
|
||||||
|
.frame(false)
|
||||||
|
.show(ui)
|
||||||
|
.response;
|
||||||
|
ui.separator();
|
||||||
|
});
|
||||||
|
|
||||||
|
CollapsingHeader::new("Tags")
|
||||||
|
.default_open(true)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.separator();
|
||||||
|
Tag::selector_ui(&mut self.tags, ui, Some(&mut self.saved));
|
||||||
|
ui.separator();
|
||||||
|
});
|
||||||
|
|
||||||
|
for field_def in &template.fields {
|
||||||
|
if let Some(field_value) = self.fields.get_mut(&field_def.name) {
|
||||||
|
let id = ui.make_persistent_id(format!("field_{}", field_def.name));
|
||||||
|
egui::collapsing_header::CollapsingState::load_with_default_open(
|
||||||
|
ui.ctx(),
|
||||||
|
id,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.show_header(ui, |ui| {
|
||||||
|
ui.strong(&field_def.name);
|
||||||
|
})
|
||||||
|
.body(|ui| {
|
||||||
|
if let Some(desc) = &field_def.description {
|
||||||
|
ui.label(RichText::new(desc).italics().weak());
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
Self::render_field(field_value, ui, &mut self.saved, objects);
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_field(
|
||||||
|
field_value: &mut FieldValue,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
saved: &mut bool,
|
||||||
|
objects: &mut [ObjectInstance],
|
||||||
|
) {
|
||||||
|
match field_value {
|
||||||
|
FieldValue::SingleLine(value) => {
|
||||||
|
if TextEdit::singleline(value)
|
||||||
|
.desired_width(f32::INFINITY)
|
||||||
|
.frame(false)
|
||||||
|
.show(ui)
|
||||||
|
.response
|
||||||
|
.changed()
|
||||||
|
{
|
||||||
|
*saved = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FieldValue::MultiLine(value) => {
|
||||||
|
if TextEdit::multiline(value)
|
||||||
|
.desired_width(f32::INFINITY)
|
||||||
|
.desired_rows(5)
|
||||||
|
.frame(false)
|
||||||
|
.show(ui)
|
||||||
|
.response
|
||||||
|
.changed()
|
||||||
|
{
|
||||||
|
*saved = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FieldValue::Date(value) => {
|
||||||
|
let response = ui.add(egui_extras::DatePickerButton::new(value));
|
||||||
|
if response.changed() {
|
||||||
|
*saved = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FieldValue::Number(value) => {
|
||||||
|
let response = ui.add(egui::DragValue::new(value).speed(0.1));
|
||||||
|
if response.changed() {
|
||||||
|
*saved = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FieldValue::Image(value) => {
|
||||||
|
ui.scope_builder(UiBuilder::new().sense(Sense::HOVER), |ui| {
|
||||||
|
let id = ui.make_persistent_id("is_hovered");
|
||||||
|
let should_show = value.is_empty()
|
||||||
|
|| ui.response().hovered()
|
||||||
|
|| ui.memory(|mem| mem.data.get_temp(id).unwrap_or(false))
|
||||||
|
|| !PROJECT_FOLDER.join("assets").join(&value).exists();
|
||||||
|
|
||||||
|
// Simple path input for now
|
||||||
|
if should_show {
|
||||||
|
let response = TextEdit::singleline(value)
|
||||||
|
.hint_text("Asset name (ignore file extension)")
|
||||||
|
.desired_width(f32::INFINITY)
|
||||||
|
.frame(false)
|
||||||
|
.show(ui)
|
||||||
|
.response;
|
||||||
|
|
||||||
|
if response.changed() {
|
||||||
|
*saved = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.memory_mut(|mem| {
|
||||||
|
*mem.data.get_temp_mut_or_insert_with(id, || true) = response.hovered();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a valid path, try to display a preview
|
||||||
|
if !value.is_empty() {
|
||||||
|
let path = PROJECT_FOLDER.join("assets").join(&value);
|
||||||
|
|
||||||
|
if let Ok(bytes) = std::fs::read(&path) {
|
||||||
|
let image_source = egui::ImageSource::Bytes {
|
||||||
|
uri: std::borrow::Cow::Owned(path.to_str().unwrap().to_string()),
|
||||||
|
bytes: bytes.into(),
|
||||||
|
};
|
||||||
|
ui.add(
|
||||||
|
egui::Image::new(image_source).max_size(vec2(256.0, f32::INFINITY)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
FieldValue::Link(template_id) => {
|
||||||
|
ObjectInstance::selector_ui(template_id, objects, ui, saved)
|
||||||
|
}
|
||||||
|
FieldValue::Links(_template_ids) => {
|
||||||
|
let mut value = String::new();
|
||||||
|
if ui.text_edit_singleline(&mut value).changed() {
|
||||||
|
*saved = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selector_ui(
|
||||||
|
selected: &mut ObjectId,
|
||||||
|
objects: &mut [ObjectInstance],
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
saved: &mut bool,
|
||||||
|
) {
|
||||||
|
if !selected.is_empty() {
|
||||||
|
if let Ok(object) = ObjectInstance::load(selected) {
|
||||||
|
ui.strong(&object.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
let id = ui.make_persistent_id("new_object");
|
||||||
|
|
||||||
|
let ctx = ui.ctx();
|
||||||
|
let mut object_selection: usize =
|
||||||
|
ctx.memory(|mem| mem.data.get_temp::<usize>(id).unwrap_or(0));
|
||||||
|
|
||||||
|
if objects.is_empty() {
|
||||||
|
ui.label("No objects available");
|
||||||
|
} else {
|
||||||
|
egui::ComboBox::from_id_salt(id)
|
||||||
|
.selected_text(&objects[object_selection].name)
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
for (i, obj) in objects.iter().enumerate() {
|
||||||
|
ui.selectable_value(&mut object_selection, i, &obj.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let ctx = ui.ctx();
|
||||||
|
ctx.memory_mut(|mem| {
|
||||||
|
*mem.data.get_temp_mut_or_default::<usize>(id) = object_selection;
|
||||||
|
});
|
||||||
|
|
||||||
|
if ui.button("Set").clicked() && object_selection < objects.len() {
|
||||||
|
*selected = objects[object_selection].id.clone();
|
||||||
|
*saved = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.button("Remove").clicked() {
|
||||||
|
*selected = String::new();
|
||||||
|
*saved = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
use egui::{Response, RichText, TextEdit, UiBuilder};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{PROJECT_FOLDER, util};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct Tag {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub color: egui::Color32,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub saved: bool,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub error: Option<util::Error>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Tag {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
name: String::new(),
|
||||||
|
description: String::new(),
|
||||||
|
color: egui::Color32::from_rgb(20, 20, 20),
|
||||||
|
saved: false,
|
||||||
|
error: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for Tag {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
id: self.id.clone(),
|
||||||
|
name: self.name.clone(),
|
||||||
|
description: self.description.clone(),
|
||||||
|
color: self.color,
|
||||||
|
saved: self.saved,
|
||||||
|
error: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tag {
|
||||||
|
pub fn display_ui(&mut self, ui: &mut egui::Ui) -> bool {
|
||||||
|
let mut remove = false;
|
||||||
|
|
||||||
|
egui::Frame::new()
|
||||||
|
.shadow(egui::Shadow {
|
||||||
|
offset: [2, 2],
|
||||||
|
blur: 16,
|
||||||
|
spread: 0,
|
||||||
|
color: egui::Color32::from_black_alpha(180),
|
||||||
|
})
|
||||||
|
.stroke(egui::Stroke::new(2.0, self.color))
|
||||||
|
.corner_radius(4.0)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if ui.add(egui::Button::new("").frame(false)).clicked() {
|
||||||
|
remove = true;
|
||||||
|
}
|
||||||
|
ui.strong(&self.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
remove
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_ui(&mut self, ui: &mut egui::Ui) -> Response {
|
||||||
|
ui.add(
|
||||||
|
egui::Button::new(RichText::new(self.name.clone()).strong())
|
||||||
|
.frame(false)
|
||||||
|
.stroke(egui::Stroke::new(2.0, self.color))
|
||||||
|
.corner_radius(4.0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
|
util::saved_status(ui, self.saved, &self.id, &self.name);
|
||||||
|
|
||||||
|
if let Some(error) = &mut self.error {
|
||||||
|
error.show(ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl) {
|
||||||
|
if let Err(e) = self.save() {
|
||||||
|
self.error = Some(util::Error::new(format!("Failed to save tag: {e}")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
egui::Grid::new("tag_grid")
|
||||||
|
.striped(true)
|
||||||
|
.num_columns(2)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.strong("Name");
|
||||||
|
if ui
|
||||||
|
.add(
|
||||||
|
TextEdit::singleline(&mut self.name)
|
||||||
|
.desired_width(f32::INFINITY)
|
||||||
|
.frame(false),
|
||||||
|
)
|
||||||
|
.changed()
|
||||||
|
{
|
||||||
|
self.saved = false;
|
||||||
|
}
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
ui.strong("Description");
|
||||||
|
if ui
|
||||||
|
.add(
|
||||||
|
TextEdit::singleline(&mut self.description)
|
||||||
|
.desired_width(f32::INFINITY)
|
||||||
|
.frame(false),
|
||||||
|
)
|
||||||
|
.changed()
|
||||||
|
{
|
||||||
|
self.saved = false;
|
||||||
|
}
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
ui.strong("Color");
|
||||||
|
if ui.color_edit_button_srgba(&mut self.color).changed() {
|
||||||
|
self.saved = false;
|
||||||
|
}
|
||||||
|
ui.end_row();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selector_ui(tag_ids: &mut Vec<String>, ui: &mut egui::Ui, saved: Option<&mut bool>) {
|
||||||
|
// remove duplicate tag ids
|
||||||
|
tag_ids.sort();
|
||||||
|
tag_ids.dedup();
|
||||||
|
|
||||||
|
let mut remove: Vec<usize> = Vec::new();
|
||||||
|
let mut modified = false;
|
||||||
|
let id = ui.make_persistent_id("new_tag");
|
||||||
|
let available_tags = Self::load_all();
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
let ctx = ui.ctx();
|
||||||
|
let mut tag_selection: usize =
|
||||||
|
ctx.memory_mut(|mem| *mem.data.get_temp_mut_or_default::<usize>(id));
|
||||||
|
|
||||||
|
if available_tags.is_empty() {
|
||||||
|
ui.label("No tags available");
|
||||||
|
} else {
|
||||||
|
egui::ComboBox::from_id_salt(id)
|
||||||
|
.selected_text(&available_tags[tag_selection].name)
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
for (i, tag) in available_tags.iter().enumerate() {
|
||||||
|
if ui
|
||||||
|
.add(
|
||||||
|
egui::Button::new(RichText::new(tag.name.clone()).strong())
|
||||||
|
.frame(false)
|
||||||
|
.stroke(egui::Stroke::new(2.0, tag.color))
|
||||||
|
.corner_radius(4.0),
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
tag_selection = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.button("Add").clicked() && tag_selection < available_tags.len() {
|
||||||
|
tag_ids.push(available_tags[tag_selection].id.clone());
|
||||||
|
tag_selection = 0;
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ctx = ui.ctx();
|
||||||
|
ctx.memory_mut(|mem| {
|
||||||
|
*mem.data.get_temp_mut_or_default::<usize>(id) = tag_selection;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (i, tag_id) in tag_ids.iter().enumerate() {
|
||||||
|
if let Ok(mut tag) = Self::load(tag_id) {
|
||||||
|
if tag.display_ui(ui) {
|
||||||
|
remove.push(i)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if the tag doesn't exist (AKA it's been deleted)
|
||||||
|
remove.push(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !remove.is_empty() {
|
||||||
|
modified = true;
|
||||||
|
for i in remove {
|
||||||
|
tag_ids.remove(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(saved) = saved {
|
||||||
|
if modified {
|
||||||
|
*saved = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(id: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
|
let path = PROJECT_FOLDER.join("tags").join(format!("{id}.json"));
|
||||||
|
Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
if self.name.is_empty() {
|
||||||
|
self.error = Some(util::Error::new("Tag name cannot be empty".to_string()));
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.error = None;
|
||||||
|
|
||||||
|
let path = PROJECT_FOLDER
|
||||||
|
.join("tags")
|
||||||
|
.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_all() -> Vec<Self> {
|
||||||
|
let mut tags = Vec::new();
|
||||||
|
|
||||||
|
// scan tags folder. load tag json files
|
||||||
|
let tags_folder = PROJECT_FOLDER.join("tags");
|
||||||
|
if tags_folder.exists() {
|
||||||
|
for entry in std::fs::read_dir(tags_folder).unwrap() {
|
||||||
|
let path = entry.unwrap().path();
|
||||||
|
if path.is_file() && path.extension().unwrap() == "json" {
|
||||||
|
let tag =
|
||||||
|
serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
|
||||||
|
tags.push(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tags
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
use core::fmt;
|
use core::fmt;
|
||||||
|
|
||||||
use egui::{RichText, ScrollArea};
|
use chrono::NaiveDate;
|
||||||
|
use egui::ScrollArea;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{PROJECT_FOLDER, RightPanelContent, error::Error, object::ObjectInstance};
|
use crate::{
|
||||||
|
PROJECT_FOLDER, RightPanelContent,
|
||||||
|
editors::object_editor::ObjectInstance,
|
||||||
|
util::{self, Error},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub enum FieldType {
|
pub enum FieldType {
|
||||||
@@ -12,6 +17,62 @@ pub enum FieldType {
|
|||||||
MultiLine,
|
MultiLine,
|
||||||
Date,
|
Date,
|
||||||
Number,
|
Number,
|
||||||
|
Link { template_id: Option<String> },
|
||||||
|
Links,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FieldType {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::SingleLine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FieldType {
|
||||||
|
fn types() -> Vec<FieldType> {
|
||||||
|
vec![
|
||||||
|
FieldType::Image,
|
||||||
|
FieldType::SingleLine,
|
||||||
|
FieldType::MultiLine,
|
||||||
|
FieldType::Date,
|
||||||
|
FieldType::Number,
|
||||||
|
FieldType::Link { template_id: None },
|
||||||
|
FieldType::Links,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum FieldValue {
|
||||||
|
Image(String),
|
||||||
|
SingleLine(String),
|
||||||
|
MultiLine(String),
|
||||||
|
Date(NaiveDate),
|
||||||
|
Number(f64),
|
||||||
|
Link(String),
|
||||||
|
Links(Vec<String>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FieldValue {
|
||||||
|
pub fn from_type(_type: &FieldType) -> Self {
|
||||||
|
match _type {
|
||||||
|
FieldType::Image => Self::Image(String::new()),
|
||||||
|
FieldType::SingleLine => Self::SingleLine(String::new()),
|
||||||
|
FieldType::MultiLine => Self::MultiLine(String::new()),
|
||||||
|
FieldType::Date => Self::Date(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()),
|
||||||
|
FieldType::Number => Self::Number(0.0),
|
||||||
|
FieldType::Link { template_id: None } => Self::Link(String::new()),
|
||||||
|
FieldType::Link {
|
||||||
|
template_id: Some(template_id),
|
||||||
|
} => Self::Link(template_id.clone()),
|
||||||
|
FieldType::Links => Self::Links(Vec::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FieldValue {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::SingleLine(String::new())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -19,6 +80,9 @@ pub struct FieldDefinition {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub field_type: FieldType,
|
pub field_type: FieldType,
|
||||||
pub required: bool,
|
pub required: bool,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub on_preview: bool,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,6 +99,21 @@ pub struct Template {
|
|||||||
|
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub error: Option<Error>,
|
pub error: Option<Error>,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub new_field_name: String,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub new_field_type: FieldType,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub new_field_required: bool,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub new_field_description: String,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub new_field_on_preview: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for Template {
|
impl fmt::Debug for Template {
|
||||||
@@ -45,6 +124,10 @@ impl fmt::Debug for Template {
|
|||||||
.field("description", &self.description)
|
.field("description", &self.description)
|
||||||
.field("fields", &self.fields)
|
.field("fields", &self.fields)
|
||||||
.field("saved", &self.saved)
|
.field("saved", &self.saved)
|
||||||
|
.field("new_field_name", &self.new_field_name)
|
||||||
|
.field("new_field_type", &self.new_field_type)
|
||||||
|
.field("new_field_required", &self.new_field_required)
|
||||||
|
.field("new_field_description", &self.new_field_description)
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,6 +141,12 @@ impl Clone for Template {
|
|||||||
fields: self.fields.clone(),
|
fields: self.fields.clone(),
|
||||||
saved: self.saved,
|
saved: self.saved,
|
||||||
error: None,
|
error: None,
|
||||||
|
|
||||||
|
new_field_name: "".to_string(),
|
||||||
|
new_field_type: FieldType::default(),
|
||||||
|
new_field_required: false,
|
||||||
|
new_field_description: "".to_string(),
|
||||||
|
new_field_on_preview: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,6 +160,12 @@ impl Default for Template {
|
|||||||
fields: Vec::new(),
|
fields: Vec::new(),
|
||||||
saved: false,
|
saved: false,
|
||||||
error: None,
|
error: None,
|
||||||
|
|
||||||
|
new_field_name: "".to_string(),
|
||||||
|
new_field_type: FieldType::default(),
|
||||||
|
new_field_required: false,
|
||||||
|
new_field_description: "".to_string(),
|
||||||
|
new_field_on_preview: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,15 +191,7 @@ impl Template {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ui(
|
pub fn ui(&mut self, ui: &mut egui::Ui, new_instance: &mut Option<RightPanelContent>) {
|
||||||
&mut self,
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
new_instance: &mut Option<RightPanelContent>,
|
|
||||||
new_field_name: &mut String,
|
|
||||||
new_field_type: &mut FieldType,
|
|
||||||
new_field_required: &mut bool,
|
|
||||||
new_field_description: &mut String,
|
|
||||||
) {
|
|
||||||
if let Some(error) = &mut self.error {
|
if let Some(error) = &mut self.error {
|
||||||
error.show(ui);
|
error.show(ui);
|
||||||
}
|
}
|
||||||
@@ -117,16 +204,17 @@ impl Template {
|
|||||||
|
|
||||||
ScrollArea::vertical().show(ui, |ui| {
|
ScrollArea::vertical().show(ui, |ui| {
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
ui.group(|ui| {
|
// 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));
|
// ui.label(format!("id: {}", self.id));
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
|
util::saved_status(ui, self.saved, &self.id, &self.name);
|
||||||
|
|
||||||
// Save/Cancel buttons
|
// Save/Cancel buttons
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
@@ -161,9 +249,9 @@ impl Template {
|
|||||||
|
|
||||||
if ui.button("Use Template").clicked() {
|
if ui.button("Use Template").clicked() {
|
||||||
if self.saved {
|
if self.saved {
|
||||||
*new_instance = Some(RightPanelContent::Object {
|
*new_instance = Some(RightPanelContent::Object(Box::new(
|
||||||
object: Box::new(ObjectInstance::new(self)),
|
ObjectInstance::new(self),
|
||||||
});
|
)));
|
||||||
} else {
|
} else {
|
||||||
self.error = Some(Error::new(
|
self.error = Some(Error::new(
|
||||||
"You must save the template before creating a new instance!"
|
"You must save the template before creating a new instance!"
|
||||||
@@ -173,25 +261,12 @@ impl Template {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
self.editor_ui(
|
self.editor_ui(ui);
|
||||||
ui,
|
|
||||||
new_field_name,
|
|
||||||
new_field_type,
|
|
||||||
new_field_required,
|
|
||||||
new_field_description,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn editor_ui(
|
pub fn editor_ui(&mut self, ui: &mut egui::Ui) {
|
||||||
&mut self,
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
new_field_name: &mut String,
|
|
||||||
new_field_type: &mut FieldType,
|
|
||||||
new_field_required: &mut bool,
|
|
||||||
new_field_description: &mut String,
|
|
||||||
) {
|
|
||||||
egui::Grid::new("template_grid")
|
egui::Grid::new("template_grid")
|
||||||
.num_columns(2)
|
.num_columns(2)
|
||||||
.striped(true)
|
.striped(true)
|
||||||
@@ -254,7 +329,7 @@ impl Template {
|
|||||||
if ui.button("❌").clicked() {
|
if ui.button("❌").clicked() {
|
||||||
to_remove = Some(i);
|
to_remove = Some(i);
|
||||||
}
|
}
|
||||||
ui.label(field.name.clone());
|
ui.strong(field.name.clone());
|
||||||
})
|
})
|
||||||
.body(|ui| {
|
.body(|ui| {
|
||||||
ui.separator();
|
ui.separator();
|
||||||
@@ -272,13 +347,7 @@ impl Template {
|
|||||||
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))
|
||||||
.show_ui(ui, |ui| {
|
.show_ui(ui, |ui| {
|
||||||
for variant in [
|
for variant in FieldType::types() {
|
||||||
FieldType::SingleLine,
|
|
||||||
FieldType::MultiLine,
|
|
||||||
FieldType::Number,
|
|
||||||
FieldType::Date,
|
|
||||||
FieldType::Image,
|
|
||||||
] {
|
|
||||||
if ui
|
if ui
|
||||||
.selectable_value(
|
.selectable_value(
|
||||||
&mut field.field_type,
|
&mut field.field_type,
|
||||||
@@ -299,6 +368,12 @@ impl Template {
|
|||||||
}
|
}
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
|
||||||
|
ui.label("On Preview:");
|
||||||
|
if ui.checkbox(&mut field.on_preview, "").clicked() {
|
||||||
|
self.saved = false;
|
||||||
|
}
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
ui.label("Description:");
|
ui.label("Description:");
|
||||||
if ui
|
if ui
|
||||||
.text_edit_singleline(
|
.text_edit_singleline(
|
||||||
@@ -330,22 +405,16 @@ impl Template {
|
|||||||
.striped(true)
|
.striped(true)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.label("Name:");
|
ui.label("Name:");
|
||||||
ui.text_edit_singleline(new_field_name);
|
ui.text_edit_singleline(&mut self.new_field_name);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
|
||||||
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!("{:?}", self.new_field_type))
|
||||||
.show_ui(ui, |ui| {
|
.show_ui(ui, |ui| {
|
||||||
for variant in [
|
for variant in FieldType::types() {
|
||||||
FieldType::SingleLine,
|
|
||||||
FieldType::MultiLine,
|
|
||||||
FieldType::Number,
|
|
||||||
FieldType::Date,
|
|
||||||
FieldType::Image,
|
|
||||||
] {
|
|
||||||
ui.selectable_value(
|
ui.selectable_value(
|
||||||
new_field_type,
|
&mut self.new_field_type,
|
||||||
variant.clone(),
|
variant.clone(),
|
||||||
format!("{variant:?}"),
|
format!("{variant:?}"),
|
||||||
);
|
);
|
||||||
@@ -354,40 +423,38 @@ impl Template {
|
|||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
|
||||||
ui.label("Required:");
|
ui.label("Required:");
|
||||||
ui.checkbox(new_field_required, "");
|
ui.checkbox(&mut self.new_field_required, "");
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
ui.label("On Preview:");
|
||||||
|
ui.checkbox(&mut self.new_field_on_preview, "");
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
|
||||||
ui.label("Description:");
|
ui.label("Description:");
|
||||||
ui.text_edit_singleline(new_field_description);
|
ui.text_edit_singleline(&mut self.new_field_description);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
|
||||||
if ui.button("Add Field").clicked() && !new_field_name.is_empty() {
|
if ui.button("Add Field").clicked() && !self.new_field_name.is_empty() {
|
||||||
self.fields.push(FieldDefinition {
|
self.fields.push(FieldDefinition {
|
||||||
name: new_field_name.clone(),
|
name: self.new_field_name.clone(),
|
||||||
field_type: new_field_type.clone(),
|
field_type: self.new_field_type.clone(),
|
||||||
required: *new_field_required,
|
on_preview: self.new_field_on_preview,
|
||||||
description: if new_field_description.is_empty() {
|
required: self.new_field_required,
|
||||||
|
description: if self.new_field_description.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(new_field_description.clone())
|
Some(self.new_field_description.clone())
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
self.saved = false;
|
self.saved = false;
|
||||||
|
|
||||||
// Reset new field form
|
// Reset new field form
|
||||||
new_field_name.clear();
|
self.new_field_name.clear();
|
||||||
*new_field_required = false;
|
self.new_field_required = false;
|
||||||
new_field_description.clear();
|
self.new_field_description.clear();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
||||||
pub struct FieldValue {
|
|
||||||
pub value: String,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub modified: bool,
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
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());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+280
-60
@@ -1,18 +1,36 @@
|
|||||||
use egui::RichText;
|
use walkdir::{DirEntry, WalkDir};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
PROJECT_FOLDER, RightPanelContent,
|
PROJECT_FOLDER, RightPanelContent,
|
||||||
main_editor::MainEditor,
|
content_editor::MainEditor,
|
||||||
note::Note,
|
editors::{
|
||||||
object::ObjectInstance,
|
asset_editor::Asset, content_editor::ContentSection, object_editor::ObjectInstance,
|
||||||
template::{FieldType, Template},
|
tags::Tag, template_editor::Template,
|
||||||
|
},
|
||||||
|
note_editor::Note,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Explorer {}
|
pub struct Explorer {
|
||||||
|
templates: Vec<Template>,
|
||||||
|
objects: Vec<ObjectInstance>,
|
||||||
|
notes: Vec<Note>,
|
||||||
|
documents: Vec<MainEditor>,
|
||||||
|
tags: Vec<Tag>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Explorer {
|
impl Explorer {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {}
|
Self {
|
||||||
|
templates: Vec::new(),
|
||||||
|
objects: Vec::new(),
|
||||||
|
notes: Vec::new(),
|
||||||
|
documents: Vec::new(),
|
||||||
|
tags: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn objects(&self) -> Vec<ObjectInstance> {
|
||||||
|
self.objects.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ui(
|
pub fn ui(
|
||||||
@@ -21,16 +39,22 @@ impl Explorer {
|
|||||||
load_doc: &mut Option<MainEditor>,
|
load_doc: &mut Option<MainEditor>,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
) {
|
) {
|
||||||
let (templates, objects) = match Self::load_templates() {
|
self.load_templates().expect("Failed to load templates");
|
||||||
Ok((templates, objects)) => (templates, objects),
|
self.load_objects().expect("Failed to load objects");
|
||||||
Err(e) => {
|
self.load_notes().expect("Failed to load notes");
|
||||||
eprintln!("Failed to load project: {e}");
|
self.load_documents().expect("Failed to load documents");
|
||||||
ui.label(RichText::new("Failed to load project").color(egui::Color32::RED));
|
self.load_tags().expect("Failed to load tags");
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
|
self.render_templates(ui, to_load);
|
||||||
|
self.render_notes(ui, to_load);
|
||||||
|
self.render_doc_root(ui, load_doc);
|
||||||
|
self.render_tags(ui, to_load);
|
||||||
|
self.render_assets(ui, to_load);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_templates(&mut self, ui: &mut egui::Ui, to_load: &mut Option<RightPanelContent>) {
|
||||||
egui::collapsing_header::CollapsingState::load_with_default_open(
|
egui::collapsing_header::CollapsingState::load_with_default_open(
|
||||||
ui.ctx(),
|
ui.ctx(),
|
||||||
ui.make_persistent_id("templates"),
|
ui.make_persistent_id("templates"),
|
||||||
@@ -40,18 +64,12 @@ impl Explorer {
|
|||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label("Templates");
|
ui.label("Templates");
|
||||||
if ui.button("+").clicked() {
|
if ui.button("+").clicked() {
|
||||||
*to_load = Some(RightPanelContent::Template {
|
*to_load = Some(RightPanelContent::template(Some(Template::default())));
|
||||||
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| {
|
.body(|ui| {
|
||||||
for template in &templates {
|
for template in &self.templates {
|
||||||
let id = ui.make_persistent_id(template.name.clone());
|
let id = ui.make_persistent_id(template.name.clone());
|
||||||
egui::collapsing_header::CollapsingState::load_with_default_open(
|
egui::collapsing_header::CollapsingState::load_with_default_open(
|
||||||
ui.ctx(),
|
ui.ctx(),
|
||||||
@@ -66,22 +84,21 @@ impl Explorer {
|
|||||||
|
|
||||||
// create a new object based on this template
|
// create a new object based on this template
|
||||||
if ui.button("+").clicked() {
|
if ui.button("+").clicked() {
|
||||||
*to_load = Some(RightPanelContent::Object {
|
*to_load = Some(RightPanelContent::object(Some(ObjectInstance::new(
|
||||||
object: Box::new(ObjectInstance::new(template)),
|
template,
|
||||||
});
|
))));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.body(|ui| {
|
.body(|ui| {
|
||||||
for object in &objects {
|
for object in &self.objects {
|
||||||
if object.template_id == template.id {
|
if object.template_id == template.id {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.add_space(10.0);
|
ui.add_space(10.0);
|
||||||
|
|
||||||
// load the object
|
// load the object
|
||||||
if ui.selectable_label(false, &object.name).clicked() {
|
if ui.selectable_label(false, &object.name).clicked() {
|
||||||
*to_load = Some(RightPanelContent::Object {
|
*to_load =
|
||||||
object: Box::new(object.clone()),
|
Some(RightPanelContent::object(Some(object.clone())));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -89,9 +106,9 @@ impl Explorer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let notes = Self::load_notes().unwrap();
|
fn render_notes(&mut self, ui: &mut egui::Ui, to_load: &mut Option<RightPanelContent>) {
|
||||||
|
|
||||||
egui::collapsing_header::CollapsingState::load_with_default_open(
|
egui::collapsing_header::CollapsingState::load_with_default_open(
|
||||||
ui.ctx(),
|
ui.ctx(),
|
||||||
ui.make_persistent_id("notes"),
|
ui.make_persistent_id("notes"),
|
||||||
@@ -101,71 +118,260 @@ impl Explorer {
|
|||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label("Notes");
|
ui.label("Notes");
|
||||||
if ui.button("+").clicked() {
|
if ui.button("+").clicked() {
|
||||||
*to_load = Some(RightPanelContent::Note {
|
*to_load = Some(RightPanelContent::note(Some(Note::default())));
|
||||||
note: Box::new(Note::default()),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.body(|ui| {
|
.body(|ui| {
|
||||||
for note in ¬es {
|
for note in &self.notes {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.add_space(10.0);
|
ui.add_space(10.0);
|
||||||
|
|
||||||
// load the note
|
// load the note
|
||||||
if ui.selectable_label(false, ¬e.name).clicked() {
|
if ui.selectable_label(false, ¬e.name).clicked() {
|
||||||
*to_load = Some(RightPanelContent::Note {
|
*to_load = Some(RightPanelContent::note(Some(note.clone())));
|
||||||
note: Box::new(note.clone()),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let documents = Self::load_documents().unwrap();
|
fn render_doc_root(&self, ui: &mut egui::Ui, load_doc: &mut Option<MainEditor>) {
|
||||||
|
egui::collapsing_header::CollapsingState::load_with_default_open(
|
||||||
|
ui.ctx(),
|
||||||
|
ui.make_persistent_id("projects"),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.show_header(ui, |ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Projects");
|
||||||
|
if ui.button("+").clicked() {
|
||||||
|
*load_doc = Some(MainEditor::open(ContentSection::new()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.body(|ui| {
|
||||||
|
// Convert MainEditor vec to ContentSection vec
|
||||||
|
let content_sections: Vec<ContentSection> = self
|
||||||
|
.documents
|
||||||
|
.iter()
|
||||||
|
.map(|doc| doc.content.clone())
|
||||||
|
.collect();
|
||||||
|
|
||||||
egui::CollapsingHeader::new("Projects").show(ui, |ui| {
|
Self::render_doc_branch(ui, &content_sections, None, load_doc);
|
||||||
for document in &documents {
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively renders a tree of documents.
|
||||||
|
///
|
||||||
|
/// Each document is represented by a single element in the `documents` array.
|
||||||
|
/// The `parent_id` parameter is used to filter out documents that do not have the current
|
||||||
|
/// parent. If `parent_id` is `None`, all documents are rendered.
|
||||||
|
///
|
||||||
|
/// `load_doc` is a mutable reference to a `MainEditor`. When a document is clicked, it
|
||||||
|
/// is loaded into the `MainEditor` and returned as `Some`.
|
||||||
|
fn render_doc_branch(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
documents: &[ContentSection],
|
||||||
|
parent_id: Option<&str>,
|
||||||
|
load_doc: &mut Option<MainEditor>,
|
||||||
|
) {
|
||||||
|
// Filter documents that have the current parent (or no parent if this is the root)
|
||||||
|
let child_docs: Vec<&ContentSection> = documents
|
||||||
|
.iter()
|
||||||
|
.filter(|doc| doc.parent.as_deref() == parent_id)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for doc in child_docs {
|
||||||
|
egui::collapsing_header::CollapsingState::load_with_default_open(
|
||||||
|
ui.ctx(),
|
||||||
|
ui.make_persistent_id(&doc.id),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.show_header(ui, |ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
// Document title
|
||||||
|
if ui.selectable_label(false, &doc.title).clicked() {
|
||||||
|
*load_doc = Some(MainEditor::open(doc.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add child button
|
||||||
|
if ui.button("+").clicked() {
|
||||||
|
let child = doc.create_child();
|
||||||
|
*load_doc = Some(MainEditor::open(child));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.body(|ui| {
|
||||||
|
// recursive call to render the next level of documents
|
||||||
|
Self::render_doc_branch(ui, documents, Some(&doc.id), load_doc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_tags(&mut self, ui: &mut egui::Ui, to_load: &mut Option<RightPanelContent>) {
|
||||||
|
egui::collapsing_header::CollapsingState::load_with_default_open(
|
||||||
|
ui.ctx(),
|
||||||
|
ui.make_persistent_id("tags"),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.show_header(ui, |ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Tags");
|
||||||
|
if ui.button("+").clicked() {
|
||||||
|
*to_load = Some(RightPanelContent::Tag(Tag::default()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.body(|ui| {
|
||||||
|
for tag in &mut self.tags {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.add_space(10.0);
|
ui.add_space(10.0);
|
||||||
|
|
||||||
// load the document
|
// load the tag
|
||||||
if ui.selectable_label(false, &document.name).clicked() {
|
if tag.list_ui(ui).clicked() {
|
||||||
*load_doc = Some(document.clone());
|
*to_load = Some(RightPanelContent::Tag(tag.clone()));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_templates() -> std::io::Result<(Vec<Template>, Vec<ObjectInstance>)> {
|
fn render_assets(&mut self, ui: &mut egui::Ui, to_load: &mut Option<RightPanelContent>) {
|
||||||
|
egui::collapsing_header::CollapsingState::load_with_default_open(
|
||||||
|
ui.ctx(),
|
||||||
|
ui.make_persistent_id("assets"),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.show_header(ui, |ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Assets");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.body(|ui| {
|
||||||
|
let mut entries: Vec<_> = WalkDir::new(PROJECT_FOLDER.join("assets"))
|
||||||
|
.min_depth(1)
|
||||||
|
.max_depth(1) // Only immediate children
|
||||||
|
.sort_by(|a, b| {
|
||||||
|
// Directories first, then files
|
||||||
|
let a_is_dir = a.file_type().is_dir();
|
||||||
|
let b_is_dir = b.file_type().is_dir();
|
||||||
|
if a_is_dir == b_is_dir {
|
||||||
|
a.file_name().cmp(b.file_name())
|
||||||
|
} else if a_is_dir {
|
||||||
|
std::cmp::Ordering::Less
|
||||||
|
} else {
|
||||||
|
std::cmp::Ordering::Greater
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for entry in entries {
|
||||||
|
self.render_entry(ui, to_load, &entry);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_entry(
|
||||||
|
&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
to_load: &mut Option<RightPanelContent>,
|
||||||
|
entry: &DirEntry,
|
||||||
|
) {
|
||||||
|
let file_type = entry.file_type();
|
||||||
|
let is_dir = file_type.is_dir();
|
||||||
|
let file_name = entry.file_name().to_string_lossy();
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
if is_dir {
|
||||||
|
let entries: Vec<_> = WalkDir::new(path)
|
||||||
|
.min_depth(1)
|
||||||
|
.max_depth(1)
|
||||||
|
.sort_by(|a, b| a.file_name().cmp(b.file_name()))
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
egui::collapsing_header::CollapsingState::load_with_default_open(
|
||||||
|
ui.ctx(),
|
||||||
|
ui.make_persistent_id(&file_name),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.show_header(ui, |ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label(file_name);
|
||||||
|
let clicked = ui.button("+").on_hover_text("Add new item").clicked();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.body(|ui| {
|
||||||
|
// recursive call to render the next level of documents
|
||||||
|
for entry in entries {
|
||||||
|
self.render_entry(ui, to_load, &entry);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Handle file
|
||||||
|
if ui
|
||||||
|
.selectable_label(false, format!("📄 {file_name}"))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
// use asset::load to get the file at the path
|
||||||
|
let asset_path = path.strip_prefix(PROJECT_FOLDER.join("assets")).unwrap();
|
||||||
|
let asset = Asset::open(asset_path.to_string_lossy().to_string());
|
||||||
|
*to_load = Some(RightPanelContent::Asset(Box::new(asset)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// load templates from the templates folder
|
||||||
|
fn load_templates(&mut self) -> std::io::Result<()> {
|
||||||
|
let templates_folder = PROJECT_FOLDER.join("templates");
|
||||||
|
if !templates_folder.exists() {
|
||||||
|
std::fs::create_dir_all(&templates_folder)?;
|
||||||
|
}
|
||||||
let mut templates = Vec::new();
|
let mut templates = Vec::new();
|
||||||
let mut objects = Vec::new();
|
for entry in std::fs::read_dir(&templates_folder).unwrap() {
|
||||||
|
|
||||||
for entry in std::fs::read_dir(PROJECT_FOLDER.join("templates")).unwrap() {
|
|
||||||
let path = entry.unwrap().path();
|
let path = entry.unwrap().path();
|
||||||
match Template::load(path.file_stem().unwrap().to_str().unwrap()) {
|
match Template::load(path.file_stem().unwrap().to_str().unwrap()) {
|
||||||
Ok(t) => templates.push(t),
|
Ok(t) => templates.push(t),
|
||||||
Err(err) => eprintln!("Could not parse file {path:?}: {err}"),
|
Err(err) => eprintln!("Could not parse file {path:?}: {err}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.templates = templates;
|
||||||
|
|
||||||
for entry in std::fs::read_dir(PROJECT_FOLDER.join("objects")).unwrap() {
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// load objects from the objects folder
|
||||||
|
fn load_objects(&mut self) -> std::io::Result<()> {
|
||||||
|
let objects_folder = PROJECT_FOLDER.join("objects");
|
||||||
|
if !objects_folder.exists() {
|
||||||
|
std::fs::create_dir_all(&objects_folder)?;
|
||||||
|
}
|
||||||
|
let mut objects = Vec::new();
|
||||||
|
for entry in std::fs::read_dir(&objects_folder).unwrap() {
|
||||||
let path = entry.unwrap().path();
|
let path = entry.unwrap().path();
|
||||||
match ObjectInstance::load(path.file_stem().unwrap().to_str().unwrap()) {
|
match ObjectInstance::load(path.file_stem().unwrap().to_str().unwrap()) {
|
||||||
Ok(o) => objects.push(o),
|
Ok(o) => objects.push(o),
|
||||||
Err(err) => eprintln!("Could not parse file {path:?}: {err}"),
|
Err(err) => eprintln!("Could not parse file {path:?}: {err}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.objects = objects;
|
||||||
|
|
||||||
Ok((templates, objects))
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_notes() -> std::io::Result<Vec<Note>> {
|
// load notes from the notes folder
|
||||||
|
fn load_notes(&mut self) -> std::io::Result<()> {
|
||||||
|
let notes_folder = PROJECT_FOLDER.join("notes");
|
||||||
|
if !notes_folder.exists() {
|
||||||
|
std::fs::create_dir_all(¬es_folder)?;
|
||||||
|
}
|
||||||
let mut notes = Vec::new();
|
let mut notes = Vec::new();
|
||||||
|
|
||||||
for entry in std::fs::read_dir(PROJECT_FOLDER.join("notes")).unwrap() {
|
for entry in std::fs::read_dir(¬es_folder).unwrap() {
|
||||||
let path = entry.unwrap().path();
|
let path = entry.unwrap().path();
|
||||||
match Note::load(path.file_stem().unwrap().to_str().unwrap()) {
|
match Note::load(path.file_stem().unwrap().to_str().unwrap()) {
|
||||||
Ok(note) => notes.push(note),
|
Ok(note) => notes.push(note),
|
||||||
@@ -173,20 +379,34 @@ impl Explorer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(notes)
|
self.notes = notes;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_documents() -> std::io::Result<Vec<MainEditor>> {
|
// load documents from the documents folder
|
||||||
|
fn load_documents(&mut self) -> std::io::Result<()> {
|
||||||
|
let documents_folder = PROJECT_FOLDER.join("documents");
|
||||||
|
if !documents_folder.exists() {
|
||||||
|
std::fs::create_dir_all(&documents_folder)?;
|
||||||
|
}
|
||||||
let mut documents = Vec::new();
|
let mut documents = Vec::new();
|
||||||
|
|
||||||
for entry in std::fs::read_dir(PROJECT_FOLDER.join("documents")).unwrap() {
|
for entry in std::fs::read_dir(&documents_folder).unwrap() {
|
||||||
let path = entry.unwrap().path();
|
let path = entry.unwrap().path();
|
||||||
match MainEditor::load(path.file_stem().unwrap().to_str().unwrap()) {
|
match ContentSection::load(path.file_stem().unwrap().to_str().unwrap()) {
|
||||||
Ok(document) => documents.push(document),
|
Ok(document) => documents.push(MainEditor::open(document)),
|
||||||
Err(err) => eprintln!("Could not parse file {path:?}: {err}"),
|
Err(err) => eprintln!("Could not parse file {path:?}: {err}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(documents)
|
self.documents = documents;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_tags(&mut self) -> std::io::Result<()> {
|
||||||
|
self.tags = Tag::load_all();
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub fn continue_content(
|
||||||
|
context: &str,
|
||||||
|
instruction: &str,
|
||||||
|
max_tokens: usize,
|
||||||
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
let client = reqwest::blocking::Client::new();
|
||||||
|
|
||||||
|
let messages = vec![
|
||||||
|
Message {
|
||||||
|
role: "system".to_string(),
|
||||||
|
content: "
|
||||||
|
Please generate content that is a direct continuation of the given text.
|
||||||
|
Your response should be a logical next step in the content and should not repeat any of the text from the instruction or the content.
|
||||||
|
Do not generate any text that is not a direct continuation of the content.
|
||||||
|
if extra instructions are provided, follow them exactly, otherwise continue the text in a logical way.
|
||||||
|
".to_string(),
|
||||||
|
},
|
||||||
|
Message {
|
||||||
|
role: "user".to_string(),
|
||||||
|
content: context.to_string(),
|
||||||
|
},
|
||||||
|
Message {
|
||||||
|
role: "user".to_string(),
|
||||||
|
content: format!("Instructions: {instruction}"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let request = ChatRequest {
|
||||||
|
messages,
|
||||||
|
temperature: 0.7,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post("http://localhost:1234/v1/chat/completions")
|
||||||
|
.json(&request)
|
||||||
|
.send()?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(format!("Request failed: {}", response.text()?).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let response: ChatResponse = response.json()?;
|
||||||
|
|
||||||
|
if let Some(choice) = response.choices.into_iter().next() {
|
||||||
|
Ok(choice.message.content)
|
||||||
|
} else {
|
||||||
|
Err("No response from model".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ai_enabled() -> bool {
|
||||||
|
let client = reqwest::blocking::Client::new();
|
||||||
|
client.get("http://localhost:1234/v1/models").send().is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple request structure
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ChatRequest {
|
||||||
|
messages: Vec<Message>,
|
||||||
|
temperature: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
struct Message {
|
||||||
|
role: String,
|
||||||
|
content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct ChatResponse {
|
||||||
|
choices: Vec<Choice>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct Choice {
|
||||||
|
message: Message,
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod content_llm;
|
||||||
+133
-146
@@ -1,20 +1,23 @@
|
|||||||
use std::{fs, path::PathBuf, sync::LazyLock};
|
use std::{path::PathBuf, sync::LazyLock};
|
||||||
|
|
||||||
use egui::{RichText, ScrollArea};
|
use egui::ScrollArea;
|
||||||
|
|
||||||
mod error;
|
mod editors;
|
||||||
mod explorer;
|
mod explorer;
|
||||||
mod main_editor;
|
mod llm_integration;
|
||||||
mod note;
|
|
||||||
mod object;
|
|
||||||
mod scene;
|
mod scene;
|
||||||
mod template;
|
|
||||||
use egui_file::DialogType;
|
|
||||||
use object::ObjectInstance;
|
|
||||||
use template::{FieldType, Template};
|
|
||||||
|
|
||||||
use crate::{explorer::Explorer, main_editor::MainEditor, note::Note};
|
mod util;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
editors::{
|
||||||
|
asset_editor::Asset, content_editor, context_editor::ProjectContext, note_editor,
|
||||||
|
object_editor::ObjectInstance, tags::Tag, template_editor::Template,
|
||||||
|
},
|
||||||
|
explorer::Explorer,
|
||||||
|
};
|
||||||
|
|
||||||
|
static VERSION: &str = "0.1.0";
|
||||||
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");
|
||||||
@@ -23,7 +26,6 @@ static PROJECT_FOLDER: LazyLock<PathBuf> = LazyLock::new(|| {
|
|||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let app = Interface::new();
|
let app = Interface::new();
|
||||||
|
|
||||||
let options = eframe::NativeOptions {
|
let options = eframe::NativeOptions {
|
||||||
viewport: egui::ViewportBuilder::default().with_inner_size([800.0, 600.0]),
|
viewport: egui::ViewportBuilder::default().with_inner_size([800.0, 600.0]),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -33,111 +35,76 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct Interface {
|
pub struct Interface {
|
||||||
dialog: Option<egui_file::FileDialog>,
|
// dialog: Option<egui_file::FileDialog>,
|
||||||
right_panel_content: RightPanelContent,
|
right_panel_content: RightPanelContent,
|
||||||
editor: main_editor::MainEditor,
|
editor: content_editor::MainEditor,
|
||||||
scene: scene::EditorScene,
|
scene: scene::EditorScene,
|
||||||
explorer: Explorer,
|
explorer: Explorer,
|
||||||
|
project: ProjectContext,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl eframe::App for Interface {
|
||||||
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
|
egui_extras::install_image_loaders(ctx);
|
||||||
|
|
||||||
|
self.configure_appearance(ctx);
|
||||||
|
self.render_top_panel(ctx);
|
||||||
|
self.render_left_panel(ctx);
|
||||||
|
self.render_right_panel(ctx);
|
||||||
|
self.render_main_content(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum RightPanelContent {
|
||||||
|
Template(Box<Template>),
|
||||||
|
Object(Box<ObjectInstance>),
|
||||||
|
Note(Box<note_editor::Note>),
|
||||||
|
Tag(Tag),
|
||||||
|
Asset(Box<Asset>),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RightPanelContent {
|
||||||
|
fn template(template: Option<Template>) -> Self {
|
||||||
|
Self::Template(Box::new(template.unwrap_or_default()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn object(instance: Option<ObjectInstance>) -> Self {
|
||||||
|
Self::Object(Box::new(instance.unwrap_or_default()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn note(note: Option<note_editor::Note>) -> Self {
|
||||||
|
Self::Note(Box::new(note.unwrap_or_default()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Interface {
|
impl Interface {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
dialog: None,
|
|
||||||
right_panel_content: RightPanelContent::None,
|
right_panel_content: RightPanelContent::None,
|
||||||
editor: main_editor::MainEditor::new(),
|
editor: content_editor::MainEditor::new(),
|
||||||
scene: scene::EditorScene::new(),
|
scene: scene::EditorScene::new(),
|
||||||
explorer: Explorer::new(),
|
explorer: Explorer::new(),
|
||||||
}
|
project: ProjectContext::load(),
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Interface {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// /home/zxq5/Pictures/logos and pfps/YT profile picture background.png
|
|
||||||
|
|
||||||
impl eframe::App for Interface {
|
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
|
||||||
egui_extras::install_image_loaders(ctx);
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut visuals = egui::Visuals::dark();
|
|
||||||
visuals.window_fill = egui::Color32::from_rgb(20, 20, 20);
|
|
||||||
visuals.panel_fill = egui::Color32::from_rgb(20, 20, 20);
|
|
||||||
ctx.set_visuals(visuals);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(dialog) = &mut self.dialog {
|
|
||||||
if dialog.show(ctx).selected() {
|
|
||||||
if let Some(path) = dialog.path() {
|
|
||||||
if dialog.dialog_type() == DialogType::OpenFile {
|
|
||||||
// Handle file dialog for loading templates/instances
|
|
||||||
if let Ok(object) =
|
|
||||||
ObjectInstance::load(path.file_stem().unwrap().to_str().unwrap())
|
|
||||||
{
|
|
||||||
// Instance
|
|
||||||
self.right_panel_content = RightPanelContent::instance(Some(object));
|
|
||||||
self.dialog = None;
|
|
||||||
} else if let Ok(template) =
|
|
||||||
Template::load(path.file_stem().unwrap().to_str().unwrap())
|
|
||||||
{
|
|
||||||
// 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
|
|
||||||
if template.save().is_err() {
|
|
||||||
eprintln!("Failed to save template");
|
|
||||||
} else {
|
|
||||||
self.dialog = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_top_panel(&self, ctx: &egui::Context) {
|
||||||
// Top bar with actions
|
// Top bar with actions
|
||||||
egui::TopBottomPanel::top("top").show(ctx, |ui| {
|
egui::TopBottomPanel::top("top").show(ctx, |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
// Open Markdown Editor button
|
// title widget
|
||||||
if ui.button("📝 Open Editor").clicked() {
|
ui.heading("WorldCoder");
|
||||||
self.editor.show_editor = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
// create new template
|
// version
|
||||||
if ui.button("New Template").clicked() {
|
ui.label(VERSION)
|
||||||
self.right_panel_content = RightPanelContent::Template {
|
|
||||||
template: Box::new(Template::default()),
|
|
||||||
new_field_name: String::new(),
|
|
||||||
new_field_type: FieldType::SingleLine,
|
|
||||||
new_field_required: false,
|
|
||||||
new_field_description: String::new(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// load instance or template from file
|
|
||||||
if ui.button("Load Template/Instance").clicked() {
|
|
||||||
self.dialog = Some(egui_file::FileDialog::open_file(Some(
|
|
||||||
PROJECT_FOLDER.clone(),
|
|
||||||
)));
|
|
||||||
self.dialog.as_mut().unwrap().open();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_left_panel(&mut self, ctx: &egui::Context) {
|
||||||
// Left panel - File browser
|
// Left panel - File browser
|
||||||
egui::SidePanel::left("file_browser")
|
egui::SidePanel::left("file_browser")
|
||||||
.resizable(true)
|
.resizable(true)
|
||||||
@@ -147,7 +114,7 @@ impl eframe::App for Interface {
|
|||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
let mut to_load: Option<RightPanelContent> = None;
|
let mut to_load: Option<RightPanelContent> = None;
|
||||||
let mut load_doc: Option<MainEditor> = None;
|
let mut load_doc: Option<content_editor::MainEditor> = None;
|
||||||
|
|
||||||
ScrollArea::vertical().show(ui, |ui| {
|
ScrollArea::vertical().show(ui, |ui| {
|
||||||
self.explorer.ui(&mut to_load, &mut load_doc, ui);
|
self.explorer.ui(&mut to_load, &mut load_doc, ui);
|
||||||
@@ -163,21 +130,28 @@ impl eframe::App for Interface {
|
|||||||
self.editor.show_preview = true;
|
self.editor.show_preview = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_right_panel(&mut self, ctx: &egui::Context) {
|
||||||
// Main content area
|
// Main content area
|
||||||
egui::SidePanel::right("templates").show(ctx, |ui| {
|
egui::SidePanel::right("templates").show(ctx, |ui| {
|
||||||
let mut new_instance: Option<RightPanelContent> = None;
|
let mut new_instance: Option<RightPanelContent> = None;
|
||||||
|
|
||||||
match &mut self.right_panel_content {
|
match &mut self.right_panel_content {
|
||||||
// an instance of a template
|
// an instance of a template
|
||||||
RightPanelContent::Object { object } => {
|
RightPanelContent::Object(object) => {
|
||||||
// load template from path
|
// load template from path
|
||||||
|
|
||||||
let mut right_panel = None;
|
let mut right_panel = None;
|
||||||
|
|
||||||
let template = Template::load(&object.template_id).unwrap();
|
let template = Template::load(&object.template_id).unwrap();
|
||||||
ScrollArea::vertical().show(ui, |ui| {
|
ScrollArea::vertical().show(ui, |ui| {
|
||||||
object.ui(ui, &template, &mut right_panel);
|
object.ui(
|
||||||
|
ui,
|
||||||
|
&template,
|
||||||
|
&mut right_panel,
|
||||||
|
&mut self.explorer.objects(),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(right_panel) = right_panel {
|
if let Some(right_panel) = right_panel {
|
||||||
@@ -186,22 +160,15 @@ impl eframe::App for Interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// an editable template
|
// an editable template
|
||||||
RightPanelContent::Template {
|
RightPanelContent::Template(template) => {
|
||||||
template,
|
template.ui(ui, &mut new_instance);
|
||||||
new_field_name,
|
}
|
||||||
new_field_type,
|
|
||||||
new_field_required,
|
|
||||||
new_field_description,
|
|
||||||
} => template.ui(
|
|
||||||
ui,
|
|
||||||
&mut new_instance,
|
|
||||||
new_field_name,
|
|
||||||
new_field_type,
|
|
||||||
new_field_required,
|
|
||||||
new_field_description,
|
|
||||||
),
|
|
||||||
|
|
||||||
RightPanelContent::Note { note } => note.ui(ui),
|
RightPanelContent::Note(note) => note.ui(ui),
|
||||||
|
|
||||||
|
RightPanelContent::Tag(tag) => tag.ui(ui),
|
||||||
|
|
||||||
|
RightPanelContent::Asset(asset) => asset.ui(ui),
|
||||||
|
|
||||||
RightPanelContent::None => {
|
RightPanelContent::None => {
|
||||||
ui.centered_and_justified(|ui| {
|
ui.centered_and_justified(|ui| {
|
||||||
@@ -217,44 +184,64 @@ impl eframe::App for Interface {
|
|||||||
self.right_panel_content = new;
|
self.right_panel_content = new;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// render main content area
|
||||||
|
fn render_main_content(&mut self, ctx: &egui::Context) {
|
||||||
self.editor.ui(ctx);
|
self.editor.ui(ctx);
|
||||||
self.scene.ui(ctx);
|
self.scene.ui(ctx, &mut self.explorer.objects());
|
||||||
|
}
|
||||||
|
|
||||||
|
// configure appearance of UI elements
|
||||||
|
fn configure_appearance(&self, ctx: &egui::Context) {
|
||||||
|
// configure appearance of UI elements
|
||||||
|
let mut visuals = egui::Visuals::dark();
|
||||||
|
visuals.window_fill = egui::Color32::from_rgb(20, 20, 20);
|
||||||
|
visuals.panel_fill = egui::Color32::from_rgb(20, 20, 20);
|
||||||
|
visuals.widgets.inactive.fg_stroke =
|
||||||
|
egui::Stroke::from((1.0, egui::Color32::from_rgb(255, 255, 255)));
|
||||||
|
visuals.widgets.inactive.bg_stroke =
|
||||||
|
egui::Stroke::from((1.0, egui::Color32::from_rgb(60, 60, 60)));
|
||||||
|
visuals.widgets.inactive.corner_radius = egui::CornerRadius::from(4);
|
||||||
|
visuals.widgets.inactive.bg_fill = egui::Color32::from_rgb(20, 20, 20);
|
||||||
|
visuals.widgets.inactive.weak_bg_fill = egui::Color32::from_rgb(20, 20, 20);
|
||||||
|
visuals.widgets.inactive.expansion = 1.0;
|
||||||
|
|
||||||
|
ctx.set_visuals(visuals);
|
||||||
|
|
||||||
|
// setup fonts.
|
||||||
|
let mut fonts = egui::FontDefinitions::default();
|
||||||
|
fonts.font_data.insert(
|
||||||
|
"JetBrains Mono Nerd Font".to_string(),
|
||||||
|
std::sync::Arc::new(egui::FontData::from_static(include_bytes!(
|
||||||
|
"/usr/local/share/fonts/j/JetBrainsMonoNerdFont_Regular.ttf",
|
||||||
|
))),
|
||||||
|
);
|
||||||
|
|
||||||
|
fonts
|
||||||
|
.families
|
||||||
|
.entry(egui::FontFamily::Proportional)
|
||||||
|
.or_default()
|
||||||
|
.insert(0, "JetBrains Mono Nerd Font".to_string());
|
||||||
|
|
||||||
|
fonts
|
||||||
|
.families
|
||||||
|
.entry(egui::FontFamily::Monospace)
|
||||||
|
.or_default()
|
||||||
|
.insert(0, "JetBrains Mono Nerd Font".to_string());
|
||||||
|
|
||||||
|
ctx.set_fonts(fonts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum RightPanelContent {
|
impl Default for Interface {
|
||||||
Template {
|
fn default() -> Self {
|
||||||
template: Box<Template>,
|
Self::new()
|
||||||
// fields to edit
|
}
|
||||||
new_field_name: String,
|
}
|
||||||
new_field_type: FieldType,
|
|
||||||
new_field_required: bool,
|
impl Drop for Interface {
|
||||||
new_field_description: String,
|
fn drop(&mut self) {
|
||||||
},
|
self.project.save();
|
||||||
Object {
|
|
||||||
object: Box<ObjectInstance>,
|
|
||||||
},
|
|
||||||
Note {
|
|
||||||
note: Box<Note>,
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RightPanelContent {
|
|
||||||
fn template(template: Option<Template>) -> Self {
|
|
||||||
Self::Template {
|
|
||||||
template: Box::new(template.unwrap_or_default()),
|
|
||||||
new_field_name: String::new(),
|
|
||||||
new_field_type: FieldType::SingleLine,
|
|
||||||
new_field_required: false,
|
|
||||||
new_field_description: String::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn instance(instance: Option<ObjectInstance>) -> Self {
|
|
||||||
Self::Object {
|
|
||||||
object: Box::new(instance.unwrap_or_default()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,211 +0,0 @@
|
|||||||
use egui::{RichText, TextEdit, Ui};
|
|
||||||
use egui_commonmark::{CommonMarkCache, CommonMarkViewer};
|
|
||||||
use serde::{self, Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::PROJECT_FOLDER;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct MainEditor {
|
|
||||||
pub name: String,
|
|
||||||
pub text: String,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub id: String,
|
|
||||||
|
|
||||||
#[serde(skip)]
|
|
||||||
saved: bool,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub show_editor: bool,
|
|
||||||
#[serde(skip)]
|
|
||||||
pub show_preview: bool,
|
|
||||||
#[serde(skip)]
|
|
||||||
preview_cache: CommonMarkCache,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Clone for MainEditor {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self {
|
|
||||||
name: self.name.clone(),
|
|
||||||
text: self.text.clone(),
|
|
||||||
id: self.id.clone(),
|
|
||||||
saved: self.saved,
|
|
||||||
show_editor: self.show_editor,
|
|
||||||
show_preview: self.show_preview,
|
|
||||||
preview_cache: CommonMarkCache::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MainEditor {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
text: String::new(),
|
|
||||||
id: uuid::Uuid::new_v4().to_string(),
|
|
||||||
name: "New Document".to_string(),
|
|
||||||
saved: false,
|
|
||||||
show_editor: false, // Start with editor hidden
|
|
||||||
show_preview: true,
|
|
||||||
preview_cache: CommonMarkCache::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load(id: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
|
||||||
let path = PROJECT_FOLDER
|
|
||||||
.join("documents")
|
|
||||||
.join(format!("{}.json", id));
|
|
||||||
|
|
||||||
let content = std::fs::read_to_string(&path)?;
|
|
||||||
let mut editor: Self = serde_json::from_str(&content)?;
|
|
||||||
editor.saved = true;
|
|
||||||
editor.id = id.to_string();
|
|
||||||
Ok(editor)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let path = PROJECT_FOLDER
|
|
||||||
.join("documents")
|
|
||||||
.join(format!("{}.json", &self.id));
|
|
||||||
|
|
||||||
let content = serde_json::to_string_pretty(self)?;
|
|
||||||
std::fs::write(path, content)?;
|
|
||||||
self.saved = true;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ui(&mut self, ctx: &egui::Context) {
|
|
||||||
// Show the editor window if enabled
|
|
||||||
let mut show = self.show_editor;
|
|
||||||
if show {
|
|
||||||
egui::Window::new("Markdown Editor")
|
|
||||||
.resizable(true)
|
|
||||||
.default_width(1000.0)
|
|
||||||
.default_height(800.0)
|
|
||||||
.open(&mut show)
|
|
||||||
.show(ctx, |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));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Save/Cancel buttons
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
if ui.button("Save").clicked() {
|
|
||||||
if let Err(e) = self.save() {
|
|
||||||
eprintln!("Failed to save: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ui.button("Create Copy").clicked() {
|
|
||||||
let mut copy = self.clone();
|
|
||||||
copy.id = uuid::Uuid::new_v4().to_string();
|
|
||||||
copy.name = format!("{} (Copy)", self.name);
|
|
||||||
copy.save().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ui.button("Delete").clicked() {
|
|
||||||
std::fs::remove_file(
|
|
||||||
PROJECT_FOLDER
|
|
||||||
.join("documents")
|
|
||||||
.join(format!("{}.json", self.id)),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
*self = Self::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ui.button("Revert changes").clicked() {
|
|
||||||
// load default state
|
|
||||||
*self = Self::load(&self.id).unwrap();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label("Content Editor");
|
|
||||||
ui.checkbox(&mut self.show_preview, "Preview");
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
if self.show_preview {
|
|
||||||
// Preview area
|
|
||||||
egui::SidePanel::right("preview_panel")
|
|
||||||
.resizable(true)
|
|
||||||
.default_width(ui.available_width() / 2.0)
|
|
||||||
.show_inside(ui, |ui| {
|
|
||||||
// Preview area with centered content and max width
|
|
||||||
egui::ScrollArea::both()
|
|
||||||
.auto_shrink([false, false])
|
|
||||||
.id_salt("preview_scroll")
|
|
||||||
.show(ui, |ui| {
|
|
||||||
let max_width = 600;
|
|
||||||
let available_width = ui.available_width();
|
|
||||||
let content_width = (max_width as f32).min(available_width);
|
|
||||||
let padding = (available_width - content_width) / 2.0;
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.add_space(padding);
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.set_width(content_width);
|
|
||||||
ui.add_space(15.0);
|
|
||||||
|
|
||||||
ui.set_min_width(max_width as f32);
|
|
||||||
|
|
||||||
CommonMarkViewer::new()
|
|
||||||
.default_width(Some(max_width))
|
|
||||||
.max_image_width(Some(512))
|
|
||||||
.show(ui, &mut self.preview_cache, &self.text);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Editor area with centered content and max width
|
|
||||||
egui::ScrollArea::both()
|
|
||||||
.auto_shrink([false, false])
|
|
||||||
.id_salt("editor_scroll")
|
|
||||||
.show(ui, |ui| {
|
|
||||||
let max_width = 600;
|
|
||||||
let available_width = ui.available_width();
|
|
||||||
let content_width = (max_width as f32).min(available_width);
|
|
||||||
let padding = (available_width - content_width) / 2.0;
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.add_space(padding);
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.set_width(content_width);
|
|
||||||
ui.add_space(15.0);
|
|
||||||
|
|
||||||
ui.set_min_width(max_width as f32);
|
|
||||||
|
|
||||||
let text_edit = TextEdit::multiline(&mut self.text)
|
|
||||||
.id_source("MainEditor_editor")
|
|
||||||
.font(egui::TextStyle::Monospace)
|
|
||||||
.interactive(true)
|
|
||||||
.frame(false)
|
|
||||||
.lock_focus(true)
|
|
||||||
.hint_text("Type here...")
|
|
||||||
.desired_width(max_width as f32);
|
|
||||||
|
|
||||||
ui.add_sized(
|
|
||||||
egui::vec2(max_width as f32, ui.available_height()),
|
|
||||||
text_edit,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
self.show_editor = show;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-109
@@ -1,109 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-276
@@ -1,276 +0,0 @@
|
|||||||
use core::f32;
|
|
||||||
|
|
||||||
use chrono::NaiveDate;
|
|
||||||
use egui::{CollapsingHeader, RichText, TextEdit, Ui, vec2};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
PROJECT_FOLDER, RightPanelContent,
|
|
||||||
template::{FieldType, FieldValue, Template},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct ObjectInstance {
|
|
||||||
// template info
|
|
||||||
pub id: String,
|
|
||||||
pub template_id: String,
|
|
||||||
|
|
||||||
// instance info
|
|
||||||
pub name: String,
|
|
||||||
pub fields: std::collections::HashMap<String, FieldValue>,
|
|
||||||
|
|
||||||
#[serde(skip)]
|
|
||||||
pub saved: bool,
|
|
||||||
|
|
||||||
#[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 {
|
|
||||||
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,
|
|
||||||
dialog: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ObjectInstance {
|
|
||||||
pub fn new(template: &Template) -> Self {
|
|
||||||
let mut fields = std::collections::HashMap::new();
|
|
||||||
|
|
||||||
for field in &template.fields {
|
|
||||||
fields.insert(field.name.clone(), FieldValue::default());
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
|
||||||
id: uuid::Uuid::new_v4().to_string(),
|
|
||||||
template_id: template.id.clone(),
|
|
||||||
name: "new_object".to_string(),
|
|
||||||
fields,
|
|
||||||
saved: false,
|
|
||||||
dialog: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(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: ObjectInstance = serde_json::from_str(&content)?;
|
|
||||||
instance.saved = true;
|
|
||||||
Ok(instance)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ui(
|
|
||||||
&mut self,
|
|
||||||
ui: &mut Ui,
|
|
||||||
template: &Template,
|
|
||||||
right_panel: &mut Option<RightPanelContent>,
|
|
||||||
) {
|
|
||||||
let _ = right_panel;
|
|
||||||
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.vertical(|ui| {
|
|
||||||
// Show save status and button
|
|
||||||
|
|
||||||
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));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
if ui.button("Save").clicked() {
|
|
||||||
if let Err(e) = self.save() {
|
|
||||||
eprintln!("Failed to save: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ui.button("Create Copy").clicked() {
|
|
||||||
let mut copy = self.clone();
|
|
||||||
copy.id = uuid::Uuid::new_v4().to_string();
|
|
||||||
copy.dialog = None;
|
|
||||||
copy.name = format!("{} (Copy)", self.name);
|
|
||||||
copy.save().unwrap();
|
|
||||||
|
|
||||||
*right_panel = Some(RightPanelContent::Object {
|
|
||||||
object: Box::new(copy),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if ui.button("Delete").clicked() {
|
|
||||||
std::fs::remove_file(
|
|
||||||
PROJECT_FOLDER
|
|
||||||
.join("objects")
|
|
||||||
.join(format!("{}.json", self.id)),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
*right_panel = Some(RightPanelContent::None);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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)
|
|
||||||
.default_open(true)
|
|
||||||
.show(ui, |ui| {
|
|
||||||
ui.heading(RichText::new(&field_def.name).size(14.0).strong());
|
|
||||||
|
|
||||||
if let Some(desc) = &field_def.description {
|
|
||||||
ui.label(RichText::new(desc).italics().weak());
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
let response = match field_def.field_type {
|
|
||||||
FieldType::SingleLine => {
|
|
||||||
TextEdit::singleline(&mut field_value.value)
|
|
||||||
.desired_width(f32::INFINITY)
|
|
||||||
.frame(false)
|
|
||||||
.show(ui)
|
|
||||||
.response
|
|
||||||
}
|
|
||||||
FieldType::MultiLine => {
|
|
||||||
TextEdit::multiline(&mut field_value.value)
|
|
||||||
.desired_width(f32::INFINITY)
|
|
||||||
.desired_rows(5)
|
|
||||||
.frame(false)
|
|
||||||
.show(ui)
|
|
||||||
.response
|
|
||||||
}
|
|
||||||
FieldType::Date => {
|
|
||||||
let date_str = &field_value.value;
|
|
||||||
let mut date =
|
|
||||||
NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
|
|
||||||
.unwrap_or_else(|_| {
|
|
||||||
chrono::Local::now().date_naive()
|
|
||||||
});
|
|
||||||
|
|
||||||
let response =
|
|
||||||
ui.add(egui_extras::DatePickerButton::new(&mut date));
|
|
||||||
|
|
||||||
if response.changed() {
|
|
||||||
field_value.value = date.format("%Y-%m-%d").to_string();
|
|
||||||
field_value.modified = true;
|
|
||||||
self.saved = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
response
|
|
||||||
}
|
|
||||||
FieldType::Number => {
|
|
||||||
let mut num =
|
|
||||||
field_value.value.parse::<f64>().unwrap_or(0.0);
|
|
||||||
let response =
|
|
||||||
ui.add(egui::DragValue::new(&mut num).speed(0.1));
|
|
||||||
|
|
||||||
if response.changed() {
|
|
||||||
field_value.value = num.to_string();
|
|
||||||
field_value.modified = true;
|
|
||||||
self.saved = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
response
|
|
||||||
}
|
|
||||||
FieldType::Image => {
|
|
||||||
// Simple path input for now
|
|
||||||
let response = TextEdit::singleline(&mut field_value.value)
|
|
||||||
.hint_text("Path to image")
|
|
||||||
.desired_width(f32::INFINITY)
|
|
||||||
.frame(false)
|
|
||||||
.show(ui)
|
|
||||||
.response;
|
|
||||||
|
|
||||||
// If we have a valid path, try to display a preview
|
|
||||||
if !field_value.value.is_empty() {
|
|
||||||
if let Ok(bytes) = std::fs::read(&field_value.value) {
|
|
||||||
let image_source = egui::ImageSource::Bytes {
|
|
||||||
uri: std::borrow::Cow::Owned(
|
|
||||||
field_value.value.clone(),
|
|
||||||
),
|
|
||||||
bytes: bytes.into(),
|
|
||||||
};
|
|
||||||
ui.add(
|
|
||||||
egui::Image::new(image_source)
|
|
||||||
.max_size(vec2(256.0, f32::INFINITY)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if response.changed() {
|
|
||||||
field_value.modified = true;
|
|
||||||
self.saved = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+124
-4
@@ -1,3 +1,13 @@
|
|||||||
|
use egui::{RichText, vec2};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
PROJECT_FOLDER,
|
||||||
|
editors::{
|
||||||
|
object_editor::ObjectInstance,
|
||||||
|
template_editor::{FieldType, FieldValue, Template},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
pub struct EditorScene {
|
pub struct EditorScene {
|
||||||
rect: egui::Rect,
|
rect: egui::Rect,
|
||||||
}
|
}
|
||||||
@@ -9,18 +19,128 @@ impl EditorScene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ui(&mut self, ctx: &egui::Context) {
|
pub fn ui(&mut self, ctx: &egui::Context, objects: &mut [ObjectInstance]) {
|
||||||
egui::CentralPanel::default()
|
egui::CentralPanel::default()
|
||||||
.frame(egui::Frame::NONE)
|
.frame(egui::Frame::NONE)
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
egui::Scene::default()
|
egui::Scene::default()
|
||||||
.zoom_range(0.1..=10.0)
|
.zoom_range(0.1..=10.0)
|
||||||
.show(ui, &mut self.rect, |ui| {
|
.show(ui, &mut self.rect, |ui| {
|
||||||
egui::Resize::default().auto_sized().show(ui, |ui| {
|
ui.horizontal_wrapped(|ui| {
|
||||||
ui.group(|ui| {
|
ui.set_max_width(5000.0);
|
||||||
ui.label("Scene");
|
// Group objects by their template_id
|
||||||
|
use std::collections::HashMap;
|
||||||
|
let mut objects_by_template: HashMap<String, Vec<&ObjectInstance>> =
|
||||||
|
HashMap::new();
|
||||||
|
|
||||||
|
for obj in objects {
|
||||||
|
objects_by_template
|
||||||
|
.entry(obj.template_id.clone())
|
||||||
|
.or_default()
|
||||||
|
.push(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each template with objects, create cards
|
||||||
|
for (template_id, template_objects) in objects_by_template {
|
||||||
|
// Try to load the template to get field definitions
|
||||||
|
if let Ok(mut template) = Template::load(&template_id) {
|
||||||
|
for obj in template_objects {
|
||||||
|
// Create a card for each object
|
||||||
|
egui::Frame::group(ui.style())
|
||||||
|
.fill(egui::Color32::from_rgba_premultiplied(
|
||||||
|
30, 30, 30, 200,
|
||||||
|
))
|
||||||
|
.corner_radius(4.0)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.set_max_width(512.0);
|
||||||
|
ui.set_min_width(512.0);
|
||||||
|
|
||||||
|
// Object name as header
|
||||||
|
ui.heading(RichText::new(&obj.name).strong());
|
||||||
|
|
||||||
|
// Show fields with on_preview = true
|
||||||
|
template.fields.sort_by_key(|field| field.field_type != FieldType::Image);
|
||||||
|
for field_def in &template.fields {
|
||||||
|
if field_def.on_preview {
|
||||||
|
if let Some(field_value) =
|
||||||
|
obj.fields.get(&field_def.name)
|
||||||
|
{
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
match field_value {
|
||||||
|
FieldValue::SingleLine(
|
||||||
|
text,
|
||||||
|
) => {
|
||||||
|
ui.strong(&field_def.name);
|
||||||
|
ui.label(text);
|
||||||
|
}
|
||||||
|
FieldValue::MultiLine(
|
||||||
|
text,
|
||||||
|
) => {
|
||||||
|
ui.strong(&field_def.name);
|
||||||
|
ui.label(text);
|
||||||
|
}
|
||||||
|
FieldValue::Number(n) => {
|
||||||
|
ui.strong(&field_def.name);
|
||||||
|
ui.label(n.to_string());
|
||||||
|
}
|
||||||
|
FieldValue::Date(date) => {
|
||||||
|
ui.strong(&field_def.name);
|
||||||
|
ui.label(
|
||||||
|
date.format(
|
||||||
|
"%Y-%m-%d",
|
||||||
|
)
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
FieldValue::Image(value) => {
|
||||||
|
if !value.is_empty() {
|
||||||
|
let path = PROJECT_FOLDER.join("assets").join(value);
|
||||||
|
|
||||||
|
if let Ok(bytes) = std::fs::read(&path) {
|
||||||
|
let image_source = egui::ImageSource::Bytes {
|
||||||
|
uri: std::borrow::Cow::Owned(path.to_str().unwrap().to_string()),
|
||||||
|
bytes: bytes.into(),
|
||||||
|
};
|
||||||
|
ui.add(
|
||||||
|
egui::Image::new(image_source).fit_to_exact_size(vec2(512.0, 512.0)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FieldValue::Link(
|
||||||
|
target_id,
|
||||||
|
) => {
|
||||||
|
ui.strong(&field_def.name);
|
||||||
|
ui.label(format!(
|
||||||
|
"→ {target_id}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
FieldValue::Links(
|
||||||
|
links,
|
||||||
|
) => {
|
||||||
|
ui.strong(&field_def.name);
|
||||||
|
ui.label(format!(
|
||||||
|
"{} links",
|
||||||
|
links.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add some spacing between cards
|
||||||
|
ui.add_space(8.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
use egui::{
|
||||||
|
RichText,
|
||||||
|
scroll_area::{ScrollBarVisibility, ScrollSource},
|
||||||
|
};
|
||||||
|
|
||||||
|
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());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn saved_status(ui: &mut egui::Ui, saved: bool, id: &str, name: &str) {
|
||||||
|
ui.group(|ui| {
|
||||||
|
ui.set_max_width(ui.available_width());
|
||||||
|
|
||||||
|
// Create a container that will take up the full width
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
// Add a ScrollArea that will contain our content
|
||||||
|
egui::ScrollArea::horizontal()
|
||||||
|
.scroll_source(ScrollSource::ALL)
|
||||||
|
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||||
|
.auto_shrink([false, false]) // Don't shrink in either direction
|
||||||
|
.show(ui, |ui| {
|
||||||
|
// Create a horizontal layout that will contain our content
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
// Now add your content
|
||||||
|
ui.strong(name);
|
||||||
|
if saved {
|
||||||
|
ui.label(RichText::new("✓ Saved").color(egui::Color32::GREEN));
|
||||||
|
} else {
|
||||||
|
ui.label(RichText::new("* Unsaved").color(egui::Color32::YELLOW));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui
|
||||||
|
.add(egui::Button::new(format!("[id: {id}]")).frame(false))
|
||||||
|
.on_hover_text(" Click id to copy!")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
ui.ctx().copy_text(id.to_string());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user