initial commit - v0.1.0
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
[build]
|
||||||
|
rustc-wrapper = "sccache"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
*/target
|
||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"rust-analyzer.check.command": "clippy",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"rust-analyzer.cargo.features": "all",
|
||||||
|
"files.eol": "\n",
|
||||||
|
"files.insertFinalNewline": true,
|
||||||
|
"files.trimFinalNewlines": true,
|
||||||
|
"files.trimTrailingWhitespace": true
|
||||||
|
}
|
||||||
Generated
+4817
File diff suppressed because it is too large
Load Diff
+22
@@ -0,0 +1,22 @@
|
|||||||
|
[package]
|
||||||
|
name = "somewhatusefultool"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
eframe = "0.31.1"
|
||||||
|
egui = "0.31.1"
|
||||||
|
editor = { path = "./editor" }
|
||||||
|
egui_extras = { version = "0.31.1", features = [
|
||||||
|
"chrono",
|
||||||
|
"datepicker",
|
||||||
|
"file",
|
||||||
|
"image",
|
||||||
|
] }
|
||||||
|
egui_file = "0.22.1"
|
||||||
|
image = { version = "0.25.6", features = ["jpeg", "png"] }
|
||||||
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
|
serde_json = "1.0.140"
|
||||||
|
chrono = { version = "0.4.41", features = ["serde"] }
|
||||||
|
thiserror = "2.0.12"
|
||||||
|
egui_commonmark = "0.20.0"
|
||||||
Generated
+354
@@ -0,0 +1,354 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ab_glyph"
|
||||||
|
version = "0.2.30"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e0f4f6fbdc5ee39f2ede9f5f3ec79477271a6d6a2baff22310d51736bda6cea"
|
||||||
|
dependencies = [
|
||||||
|
"ab_glyph_rasterizer",
|
||||||
|
"owned_ttf_parser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ab_glyph_rasterizer"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b2187590a23ab1e3df8681afdf0987c48504d80291f002fcdb651f0ef5e25169"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ahash"
|
||||||
|
version = "0.8.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"version_check",
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ecolor"
|
||||||
|
version = "0.32.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4a631732d995184114016fab22fc7e3faf73d6841c2d7650395fe251fbcd9285"
|
||||||
|
dependencies = [
|
||||||
|
"emath",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "editor"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"egui",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "egui"
|
||||||
|
version = "0.32.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8470210c95a42cc985d9ffebfd5067eea55bdb1c3f7611484907db9639675e28"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
"bitflags",
|
||||||
|
"emath",
|
||||||
|
"epaint",
|
||||||
|
"nohash-hasher",
|
||||||
|
"profiling",
|
||||||
|
"smallvec",
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "emath"
|
||||||
|
version = "0.32.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "45f057b141e7e46340c321400be74b793543b1b213036f0f989c35d35957c32e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "epaint"
|
||||||
|
version = "0.32.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94cca02195f0552c17cabdc02f39aa9ab6fbd815dac60ab1cd3d5b0aa6f9551c"
|
||||||
|
dependencies = [
|
||||||
|
"ab_glyph",
|
||||||
|
"ahash",
|
||||||
|
"ecolor",
|
||||||
|
"emath",
|
||||||
|
"epaint_default_fonts",
|
||||||
|
"nohash-hasher",
|
||||||
|
"parking_lot",
|
||||||
|
"profiling",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "epaint_default_fonts"
|
||||||
|
version = "0.32.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8495e11ed527dff39663b8c36b6c2b2799d7e4287fb90556e455d72eca0b4d3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.174"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lock_api"
|
||||||
|
version = "0.4.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nohash-hasher"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "owned_ttf_parser"
|
||||||
|
version = "0.25.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4"
|
||||||
|
dependencies = [
|
||||||
|
"ttf-parser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot"
|
||||||
|
version = "0.12.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
|
||||||
|
dependencies = [
|
||||||
|
"lock_api",
|
||||||
|
"parking_lot_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot_core"
|
||||||
|
version = "0.9.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
|
"smallvec",
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.95"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "profiling"
|
||||||
|
version = "1.0.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.40"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.5.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scopeguard"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.219"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.219"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.104"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ttf-parser"
|
||||||
|
version = "0.25.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-segmentation"
|
||||||
|
version = "1.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version_check"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||||
|
dependencies = [
|
||||||
|
"windows_aarch64_gnullvm",
|
||||||
|
"windows_aarch64_msvc",
|
||||||
|
"windows_i686_gnu",
|
||||||
|
"windows_i686_gnullvm",
|
||||||
|
"windows_i686_msvc",
|
||||||
|
"windows_x86_64_gnu",
|
||||||
|
"windows_x86_64_gnullvm",
|
||||||
|
"windows_x86_64_msvc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.8.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.8.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "editor"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
description = "a basic text editor widget with line numbers"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
egui = "0.31.1"
|
||||||
|
serde = "1"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "editor"
|
||||||
|
path = "src/lib.rs"
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
|
||||||
|
|
||||||
|
use egui::{text::LayoutJob, Color32};
|
||||||
|
use egui::widgets::text_edit::TextEditOutput;
|
||||||
|
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
/// CodeEditor struct which stores settings for highlighting.
|
||||||
|
pub struct CodeEditor {
|
||||||
|
id: String,
|
||||||
|
numlines: bool,
|
||||||
|
numlines_shift: isize,
|
||||||
|
numlines_only_natural: bool,
|
||||||
|
fontsize: f32,
|
||||||
|
stick_to_bottom: bool,
|
||||||
|
desired_width: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for CodeEditor {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||||
|
(self.fontsize as u32).hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CodeEditor {
|
||||||
|
fn default() -> CodeEditor {
|
||||||
|
CodeEditor {
|
||||||
|
id: String::from("Code Editor"),
|
||||||
|
numlines: true,
|
||||||
|
numlines_shift: 0,
|
||||||
|
numlines_only_natural: false,
|
||||||
|
fontsize: 10.0,
|
||||||
|
stick_to_bottom: false,
|
||||||
|
desired_width: f32::INFINITY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CodeEditor {
|
||||||
|
pub fn id_source(self, id_source: impl Into<String>) -> Self {
|
||||||
|
CodeEditor {
|
||||||
|
id: id_source.into(),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Use custom font size
|
||||||
|
///
|
||||||
|
/// **Default: 10.0**
|
||||||
|
pub fn with_fontsize(self, fontsize: f32) -> Self {
|
||||||
|
CodeEditor { fontsize, ..self }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Use UI font size
|
||||||
|
pub fn with_ui_fontsize(self, ui: &mut egui::Ui) -> Self {
|
||||||
|
CodeEditor {
|
||||||
|
fontsize: egui::TextStyle::Monospace.resolve(ui.style()).size,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show or hide lines numbering
|
||||||
|
///
|
||||||
|
/// **Default: true**
|
||||||
|
pub fn with_numlines(self, numlines: bool) -> Self {
|
||||||
|
CodeEditor { numlines, ..self }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shift lines numbering by this value
|
||||||
|
///
|
||||||
|
/// **Default: 0**
|
||||||
|
pub fn with_numlines_shift(self, numlines_shift: isize) -> Self {
|
||||||
|
CodeEditor {
|
||||||
|
numlines_shift,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show lines numbering only above zero, useful for enabling numbering since nth row
|
||||||
|
///
|
||||||
|
/// **Default: false**
|
||||||
|
pub fn with_numlines_only_natural(self, numlines_only_natural: bool) -> Self {
|
||||||
|
CodeEditor {
|
||||||
|
numlines_only_natural,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Should the containing area shrink if the content is small?
|
||||||
|
///
|
||||||
|
/// **Default: false**
|
||||||
|
pub fn auto_shrink(self, shrink: bool) -> Self {
|
||||||
|
CodeEditor {
|
||||||
|
desired_width: if shrink { 0.0 } else { self.desired_width },
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the desired width of the code editor
|
||||||
|
///
|
||||||
|
/// **Default: `f32::INFINITY`**
|
||||||
|
pub fn desired_width(self, width: f32) -> Self {
|
||||||
|
CodeEditor {
|
||||||
|
desired_width: width,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stick to bottom
|
||||||
|
/// The scroll handle will stick to the bottom position even while the content size
|
||||||
|
/// changes dynamically. This can be useful to simulate terminal UIs or log/info scrollers.
|
||||||
|
/// The scroll handle remains stuck until user manually changes position. Once "unstuck"
|
||||||
|
/// it will remain focused on whatever content viewport the user left it on. If the scroll
|
||||||
|
/// handle is dragged to the bottom it will again become stuck and remain there until manually
|
||||||
|
/// pulled from the end position.
|
||||||
|
///
|
||||||
|
/// **Default: false**
|
||||||
|
pub fn stick_to_bottom(self, stick_to_bottom: bool) -> Self {
|
||||||
|
CodeEditor {
|
||||||
|
stick_to_bottom,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format(&self, string: &str) -> egui::text::TextFormat {
|
||||||
|
let font_id = egui::FontId::monospace(self.fontsize);
|
||||||
|
let color = Color32::WHITE;
|
||||||
|
egui::text::TextFormat::simple(font_id, color)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn numlines_show(&self, ui: &mut egui::Ui, text: &str) {
|
||||||
|
let total = if text.ends_with('\n') || text.is_empty() {
|
||||||
|
text.lines().count() + 1
|
||||||
|
} else {
|
||||||
|
text.lines().count()
|
||||||
|
} as isize;
|
||||||
|
|
||||||
|
let max_indent = total.to_string().len().max(
|
||||||
|
!self.numlines_only_natural as usize * self.numlines_shift.to_string().len(),
|
||||||
|
);
|
||||||
|
let mut counter = (1..=total)
|
||||||
|
.map(|i| {
|
||||||
|
let num = i + self.numlines_shift;
|
||||||
|
if num <= 0 && self.numlines_only_natural {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
let label = num.to_string();
|
||||||
|
format!(
|
||||||
|
"{}{label}",
|
||||||
|
" ".repeat(max_indent.saturating_sub(label.len()))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
#[allow(clippy::cast_precision_loss)]
|
||||||
|
let width = max_indent as f32
|
||||||
|
* self.fontsize
|
||||||
|
* 0.5
|
||||||
|
* !(total + self.numlines_shift <= 0 && self.numlines_only_natural) as u8
|
||||||
|
as f32;
|
||||||
|
|
||||||
|
let mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| {
|
||||||
|
let layout_job = egui::text::LayoutJob::single_section(
|
||||||
|
string.to_string(),
|
||||||
|
egui::TextFormat::simple(
|
||||||
|
egui::FontId::monospace(self.fontsize),
|
||||||
|
Color32::WHITE,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
ui.fonts(|f| f.layout_job(layout_job))
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.add(
|
||||||
|
egui::TextEdit::multiline(&mut counter)
|
||||||
|
.id_source(format!("{}_numlines", self.id))
|
||||||
|
.font(egui::TextStyle::Monospace)
|
||||||
|
.interactive(false)
|
||||||
|
.frame(false)
|
||||||
|
.desired_width(width)
|
||||||
|
.layouter(&mut layouter),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show Code Editor
|
||||||
|
pub fn show(
|
||||||
|
&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
text: &mut dyn egui::TextBuffer,
|
||||||
|
) -> TextEditOutput {
|
||||||
|
let mut text_edit_output: Option<TextEditOutput> = None;
|
||||||
|
let code_editor = |ui: &mut egui::Ui| {
|
||||||
|
ui.horizontal_top(|h| {
|
||||||
|
if self.numlines {
|
||||||
|
self.numlines_show(h, text.as_str());
|
||||||
|
}
|
||||||
|
egui::ScrollArea::horizontal()
|
||||||
|
.hscroll(true)
|
||||||
|
.id_salt(format!("{}_inner_scroll", self.id))
|
||||||
|
.show(h, |ui| {
|
||||||
|
let output = egui::TextEdit::multiline(text)
|
||||||
|
.id_source(&self.id)
|
||||||
|
.lock_focus(true)
|
||||||
|
.frame(false)
|
||||||
|
.desired_width(self.desired_width)
|
||||||
|
.show(ui);
|
||||||
|
text_edit_output = Some(output);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
egui::ScrollArea::vertical()
|
||||||
|
.id_salt(format!("{}_outer_scroll", self.id))
|
||||||
|
.stick_to_bottom(self.stick_to_bottom)
|
||||||
|
.show(ui, code_editor);
|
||||||
|
|
||||||
|
text_edit_output.expect("TextEditOutput should exist at this point")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"template_name": "Character",
|
||||||
|
"template_path": "templates/character.json",
|
||||||
|
"name": "zxq5",
|
||||||
|
"fields": {
|
||||||
|
"Age": {
|
||||||
|
"value": "19"
|
||||||
|
},
|
||||||
|
"Birth Date": {
|
||||||
|
"value": "2025-07-11"
|
||||||
|
},
|
||||||
|
"Name": {
|
||||||
|
"value": "zxq5"
|
||||||
|
},
|
||||||
|
"Backstory": {
|
||||||
|
"value": "zxq5"
|
||||||
|
},
|
||||||
|
"Profile Picture": {
|
||||||
|
"value": "/home/zxq5/Pictures/logos and pfps/YT profile picture.png"
|
||||||
|
},
|
||||||
|
"Personality": {
|
||||||
|
"value": "zxq5"
|
||||||
|
},
|
||||||
|
"Appearance": {
|
||||||
|
"value": "zxq5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"name": "Character",
|
||||||
|
"description": "character template!",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "Profile Picture",
|
||||||
|
"field_type": "Image",
|
||||||
|
"required": false,
|
||||||
|
"description": "Path to character's profile picture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Name",
|
||||||
|
"field_type": "SingleLine",
|
||||||
|
"required": true,
|
||||||
|
"description": "The full name of the character"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Age",
|
||||||
|
"field_type": "Number",
|
||||||
|
"required": false,
|
||||||
|
"description": "Character's age in years"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Birth Date",
|
||||||
|
"field_type": "Date",
|
||||||
|
"required": false,
|
||||||
|
"description": "Date of birth (YYYY-MM-DD)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Appearance",
|
||||||
|
"field_type": "MultiLine",
|
||||||
|
"required": false,
|
||||||
|
"description": "Physical description of the character"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Personality",
|
||||||
|
"field_type": "MultiLine",
|
||||||
|
"required": false,
|
||||||
|
"description": "Personality traits and behavior"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Backstory",
|
||||||
|
"field_type": "MultiLine",
|
||||||
|
"required": false,
|
||||||
|
"description": "Character's history and background"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
# Project Roadmap
|
||||||
|
|
||||||
|
## v0.1 - Minimum Viable Product
|
||||||
|
|
||||||
|
### v0.1.0 - basic structure
|
||||||
|
|
||||||
|
- [x] Templates
|
||||||
|
- [x] Create template
|
||||||
|
- [x] View template
|
||||||
|
- [x] Edit template
|
||||||
|
- [x] Save template
|
||||||
|
- [x] Load template
|
||||||
|
- [ ] Delete template
|
||||||
|
- [x] Objects
|
||||||
|
- [x] Create object from template
|
||||||
|
- [x] Save object
|
||||||
|
- [x] Load object
|
||||||
|
- [x] View & Edit Object
|
||||||
|
- [ ] Delete object
|
||||||
|
- [x] Projects
|
||||||
|
- [x] Single project folder (hardcoded at current directory)
|
||||||
|
|
||||||
|
### v0.1.1 - Editor
|
||||||
|
|
||||||
|
- [ ] Basic editor (markdown formatting)
|
||||||
|
- [x] Basic text editing
|
||||||
|
- [ ] Load & Save text file
|
||||||
|
- [x] Editor preview
|
||||||
|
- [x] Preview text in markdown
|
||||||
|
|
||||||
|
## v0.2 - Projects, Organisation & Linking
|
||||||
|
|
||||||
|
### v0.2.0 - links & context building
|
||||||
|
|
||||||
|
- [ ] Links between objects
|
||||||
|
- [ ] Links in templates
|
||||||
|
|
||||||
|
### v0.2.1 - writing projects & organisation
|
||||||
|
|
||||||
|
- [ ] Project management
|
||||||
|
- [ ] Chapters/organisation
|
||||||
|
|
||||||
|
## v0.3 - Workflows & AI integration
|
||||||
|
|
||||||
|
### v0.3.0 - Build project
|
||||||
|
|
||||||
|
- [ ] Create a project build tool
|
||||||
|
- [ ] be able to store and process project metadata
|
||||||
|
- [ ] projects need a clear and defined structure
|
||||||
|
- [ ] Turn story of several chapters arranged as json files of metadata and content into a final story presented as markdown
|
||||||
|
|
||||||
|
### v0.3.1 - Basic AI tooling
|
||||||
|
|
||||||
|
- [ ] Creating objects from templates
|
||||||
|
- [ ] Integrate an AI model that can take data in any format and extract any relevant data
|
||||||
|
- [ ] Store this data as a new object
|
||||||
|
|
||||||
|
### v0.3.2 - Content generation AI
|
||||||
|
|
||||||
|
- [ ] Written Content
|
||||||
|
- [ ] Initial Context Gathering
|
||||||
|
- [ ] Tokenise the request
|
||||||
|
- [ ] Fuzzy search for relevant objects and load them into context
|
||||||
|
|
||||||
|
- [ ] Content Summarisation
|
||||||
|
- [ ] Summarise entire written work with a low level of detail and extract key details as well as the overall theme and style
|
||||||
|
- [ ] Summarise current section with a high level of detail as well as the written style etc.
|
||||||
|
|
||||||
|
- [ ] Content generation AI
|
||||||
|
- [ ] Collect context from across a story or project
|
||||||
|
- [ ] Generate content for stories
|
||||||
|
- [ ] Create original content in the form of objects using templates
|
||||||
|
- [ ] Create a new template
|
||||||
|
|
||||||
|
### v0.3.3 - AI flows & deep integration
|
||||||
|
|
||||||
|
- [ ] AI flows & deep integration
|
||||||
|
- [ ] System for the user to review and approve actions before they are run
|
||||||
|
- [ ] Tooling to perform workflows that may take many steps
|
||||||
|
- [ ] Figure out what actions it needs to do, such as creating new objects and templates in real time
|
||||||
+323
@@ -0,0 +1,323 @@
|
|||||||
|
use std::{fs, path::PathBuf, sync::LazyLock};
|
||||||
|
|
||||||
|
use egui::{RichText, ScrollArea};
|
||||||
|
|
||||||
|
mod main_editor;
|
||||||
|
mod object;
|
||||||
|
mod template;
|
||||||
|
use egui_file::DialogType;
|
||||||
|
use object::ObjectInstance;
|
||||||
|
use template::{FieldType, Template};
|
||||||
|
|
||||||
|
static PROJECT_FOLDER: LazyLock<PathBuf> = LazyLock::new(|| {
|
||||||
|
let mut path = std::env::current_dir().unwrap();
|
||||||
|
path.push("project");
|
||||||
|
path
|
||||||
|
});
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let app = Interface::new();
|
||||||
|
|
||||||
|
let options = eframe::NativeOptions {
|
||||||
|
viewport: egui::ViewportBuilder::default().with_inner_size([800.0, 600.0]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = eframe::run_native("Code Editor", options, Box::new(|_cc| Ok(Box::new(app))));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Interface {
|
||||||
|
text: String,
|
||||||
|
dialog: Option<egui_file::FileDialog>,
|
||||||
|
right_panel_content: RightPanelContent,
|
||||||
|
editor: main_editor::MainEditor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Interface {
|
||||||
|
#[must_use]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
text: "".to_string(),
|
||||||
|
dialog: None,
|
||||||
|
right_panel_content: RightPanelContent::None,
|
||||||
|
editor: main_editor::MainEditor::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_directory(
|
||||||
|
&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
path: &PathBuf,
|
||||||
|
depth: usize,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let indent = " ".repeat(depth);
|
||||||
|
let entries = fs::read_dir(path)?;
|
||||||
|
let mut dirs = Vec::new();
|
||||||
|
let mut files = Vec::new();
|
||||||
|
|
||||||
|
for entry in entries {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
dirs.push(path);
|
||||||
|
} else if let Some(ext) = path.extension() {
|
||||||
|
if ext == "json" {
|
||||||
|
// Only show JSON files
|
||||||
|
files.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort directories and files alphabetically
|
||||||
|
dirs.sort();
|
||||||
|
files.sort();
|
||||||
|
|
||||||
|
// Show directories first
|
||||||
|
for dir in dirs {
|
||||||
|
let dir_name = dir
|
||||||
|
.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.unwrap_or("<invalid>")
|
||||||
|
.to_owned()
|
||||||
|
+ "/";
|
||||||
|
|
||||||
|
if egui::CollapsingHeader::new(dir_name.clone())
|
||||||
|
.default_open(depth < 1)
|
||||||
|
.show(ui, |ui| self.show_directory(ui, &dir, depth + 1))
|
||||||
|
.body_returned
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
ui.label(RichText::new(format!(
|
||||||
|
"{indent}❌ Error reading {dir_name}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then show files
|
||||||
|
for file in files {
|
||||||
|
if let Some(file_name) = file.file_name().and_then(|n| n.to_str()) {
|
||||||
|
let response = ui.horizontal(|ui| {
|
||||||
|
ui.label(" ".repeat(depth));
|
||||||
|
ui.selectable_label(false, file_name)
|
||||||
|
});
|
||||||
|
|
||||||
|
if response.inner.clicked() {
|
||||||
|
if let Ok(instance) = ObjectInstance::load(file.clone()) {
|
||||||
|
self.right_panel_content =
|
||||||
|
RightPanelContent::instance(Some(file.clone()), Some(instance));
|
||||||
|
} else if let Ok(template) = Template::load(file.clone()) {
|
||||||
|
self.right_panel_content =
|
||||||
|
RightPanelContent::template(Some(file.clone()), Some(template));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Interface {
|
||||||
|
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(instance) = ObjectInstance::load(path.to_path_buf()) {
|
||||||
|
// Instance
|
||||||
|
self.right_panel_content = RightPanelContent::instance(
|
||||||
|
Some(path.to_path_buf()),
|
||||||
|
Some(instance),
|
||||||
|
);
|
||||||
|
self.dialog = None;
|
||||||
|
} else if let Ok(template) = Template::load(path.to_path_buf()) {
|
||||||
|
// Template
|
||||||
|
self.right_panel_content = RightPanelContent::template(
|
||||||
|
Some(path.to_path_buf()),
|
||||||
|
Some(template),
|
||||||
|
);
|
||||||
|
self.dialog = None;
|
||||||
|
}
|
||||||
|
} else if dialog.dialog_type() == DialogType::SaveFile {
|
||||||
|
// Handle file dialog for saving templates/instances
|
||||||
|
|
||||||
|
if let RightPanelContent::Template { template, .. } =
|
||||||
|
&mut self.right_panel_content
|
||||||
|
{
|
||||||
|
// set the save location and save
|
||||||
|
template.path = Some(path.to_path_buf());
|
||||||
|
if template.save().is_err() {
|
||||||
|
eprintln!("Failed to save template");
|
||||||
|
} else {
|
||||||
|
self.dialog = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top bar with actions
|
||||||
|
egui::TopBottomPanel::top("top").show(ctx, |ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
// create new template
|
||||||
|
if ui.button("New Template").clicked() {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Left panel - File browser
|
||||||
|
egui::SidePanel::left("file_browser")
|
||||||
|
.resizable(true)
|
||||||
|
.default_width(250.0)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
ui.heading("Project Files");
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
ScrollArea::vertical().show(ui, |ui| {
|
||||||
|
if let Err(e) = self.show_directory(ui, &PROJECT_FOLDER, 0) {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(format!("Error reading directory: {e}"))
|
||||||
|
.color(egui::Color32::RED),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Main content area
|
||||||
|
egui::SidePanel::right("templates").show(ctx, |ui| {
|
||||||
|
let mut new_instance: RightPanelContent = RightPanelContent::None;
|
||||||
|
|
||||||
|
match &mut self.right_panel_content {
|
||||||
|
// an instance of a template
|
||||||
|
RightPanelContent::Instance { instance, path: _ } => {
|
||||||
|
// load template from path
|
||||||
|
|
||||||
|
let mut temp_path = instance.template_path.clone();
|
||||||
|
|
||||||
|
// check if path is relative
|
||||||
|
if !temp_path.is_absolute() {
|
||||||
|
temp_path = PROJECT_FOLDER.join(temp_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let template = Template::load(temp_path).unwrap();
|
||||||
|
ScrollArea::vertical().show(ui, |ui| {
|
||||||
|
instance.ui(ui, &template);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// an editable template
|
||||||
|
RightPanelContent::Template {
|
||||||
|
template,
|
||||||
|
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::None => {
|
||||||
|
ui.centered_and_justified(|ui| {
|
||||||
|
ui.label("No template loaded to edit.");
|
||||||
|
if ui.button("Back").clicked() {
|
||||||
|
self.right_panel_content = RightPanelContent::None;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let RightPanelContent::None = new_instance {
|
||||||
|
} else {
|
||||||
|
self.right_panel_content = new_instance;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.editor.ui(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum RightPanelContent {
|
||||||
|
Template {
|
||||||
|
template: Box<Template>,
|
||||||
|
// fields to edit
|
||||||
|
new_field_name: String,
|
||||||
|
new_field_type: FieldType,
|
||||||
|
new_field_required: bool,
|
||||||
|
new_field_description: String,
|
||||||
|
},
|
||||||
|
Instance {
|
||||||
|
instance: Box<ObjectInstance>,
|
||||||
|
path: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RightPanelContent {
|
||||||
|
fn template(path: Option<PathBuf>, 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(path: Option<PathBuf>, instance: Option<ObjectInstance>) -> Self {
|
||||||
|
Self::Instance {
|
||||||
|
instance: Box::new(instance.unwrap_or_default()),
|
||||||
|
path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_saved(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
RightPanelContent::Instance { instance, path: _ } => instance.saved,
|
||||||
|
RightPanelContent::Template { template, .. } => template.path.is_some(),
|
||||||
|
RightPanelContent::None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
use egui_commonmark::{CommonMarkCache, CommonMarkViewer};
|
||||||
|
|
||||||
|
pub struct MainEditor {
|
||||||
|
preview: bool,
|
||||||
|
text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MainEditor {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
preview: true,
|
||||||
|
text: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui(&mut self, ctx: &egui::Context) {
|
||||||
|
if self.preview {
|
||||||
|
egui::TopBottomPanel::bottom("bottom_panel")
|
||||||
|
.resizable(true)
|
||||||
|
.default_height(250.0)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
egui::ScrollArea::vertical()
|
||||||
|
.stick_to_bottom(true)
|
||||||
|
.auto_shrink(false)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
let mut cache = CommonMarkCache::default();
|
||||||
|
CommonMarkViewer::new().show(ui, &mut cache, &self.text);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
egui::ScrollArea::vertical()
|
||||||
|
.enable_scrolling(true)
|
||||||
|
.auto_shrink(false)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.add(
|
||||||
|
egui::TextEdit::multiline(&mut self.text)
|
||||||
|
.id_source("MainEditor_numlines")
|
||||||
|
.font(egui::TextStyle::Monospace)
|
||||||
|
.interactive(true)
|
||||||
|
.frame(false)
|
||||||
|
.lock_focus(true)
|
||||||
|
.hint_text("Type here...")
|
||||||
|
.desired_width(256.0),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+278
@@ -0,0 +1,278 @@
|
|||||||
|
use core::f32;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
use egui::{CollapsingHeader, RichText, TextEdit, Ui, vec2};
|
||||||
|
use egui_file::DialogType;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
PROJECT_FOLDER,
|
||||||
|
template::{FieldType, FieldValue, Template},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ObjectInstance {
|
||||||
|
// template info
|
||||||
|
pub template_name: String,
|
||||||
|
pub template_path: PathBuf,
|
||||||
|
|
||||||
|
// instance info
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub fields: std::collections::HashMap<String, FieldValue>,
|
||||||
|
|
||||||
|
// state (ignore)
|
||||||
|
#[serde(skip)]
|
||||||
|
pub saved: bool,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub path: Option<PathBuf>,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub dialog: Option<egui_file::FileDialog>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ObjectInstance {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
template_name: "New Template Instance".to_string(),
|
||||||
|
name: None,
|
||||||
|
template_path: PathBuf::new(),
|
||||||
|
fields: std::collections::HashMap::new(),
|
||||||
|
saved: false,
|
||||||
|
path: None,
|
||||||
|
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 {
|
||||||
|
template_name: template.name.clone(),
|
||||||
|
name: None,
|
||||||
|
fields,
|
||||||
|
template_path: template
|
||||||
|
.path
|
||||||
|
.clone()
|
||||||
|
.expect("expected the template to have a path!"),
|
||||||
|
saved: false,
|
||||||
|
path: None,
|
||||||
|
dialog: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(&self, path: PathBuf) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let content = serde_json::to_string_pretty(self)?;
|
||||||
|
std::fs::write(&path, content)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(path: PathBuf) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
|
let content = std::fs::read_to_string(&path)?;
|
||||||
|
let mut instance: Self = serde_json::from_str(&content)?;
|
||||||
|
|
||||||
|
// Set the name from the filename (without extension)
|
||||||
|
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
|
||||||
|
instance.name = Some(stem.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.path = Some(path);
|
||||||
|
instance.saved = true;
|
||||||
|
Ok(instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_dialogs(&mut self, ui: &mut Ui) {
|
||||||
|
let path = if let Some(dialog) = &mut self.dialog {
|
||||||
|
match dialog.dialog_type() {
|
||||||
|
DialogType::SaveFile => {
|
||||||
|
if dialog.show(ui.ctx()).selected() {
|
||||||
|
dialog.path().map(|path| (path.to_path_buf(), true))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handle other dialog types...
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some((path, should_save)) = path {
|
||||||
|
if should_save {
|
||||||
|
if let Err(err) = self.save(path.clone()) {
|
||||||
|
println!("Save error: {err}");
|
||||||
|
} else {
|
||||||
|
// Update the name from the filename when saving
|
||||||
|
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
|
||||||
|
self.name = Some(stem.to_string());
|
||||||
|
}
|
||||||
|
self.path = Some(path);
|
||||||
|
self.saved = true;
|
||||||
|
self.dialog = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui(&mut self, ui: &mut Ui, template: &Template) {
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
self.handle_dialogs(ui);
|
||||||
|
|
||||||
|
// Show save status and button
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if self.saved {
|
||||||
|
ui.label(RichText::new("✓ Saved").color(egui::Color32::GREEN));
|
||||||
|
} else {
|
||||||
|
ui.label(RichText::new("* Unsaved").color(egui::Color32::YELLOW));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show current save path or "Not saved yet"
|
||||||
|
let path_display = self
|
||||||
|
.path
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|p| p.to_str())
|
||||||
|
.unwrap_or("Not saved yet");
|
||||||
|
ui.label(path_display);
|
||||||
|
|
||||||
|
// File picker button
|
||||||
|
if ui.button("Save As").clicked() && self.dialog.is_none() {
|
||||||
|
self.dialog = Some(egui_file::FileDialog::save_file(Some(
|
||||||
|
PROJECT_FOLDER.clone(),
|
||||||
|
)));
|
||||||
|
self.dialog.as_mut().unwrap().open();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.button("Save").clicked() {
|
||||||
|
if let Some(path) = &self.path {
|
||||||
|
if let Err(e) = self.save(path.clone()) {
|
||||||
|
eprintln!("Failed to save: {e}");
|
||||||
|
} else {
|
||||||
|
self.saved = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If no path is set, request one
|
||||||
|
if self.dialog.is_none() {
|
||||||
|
self.dialog = Some(egui_file::FileDialog::save_file(Some(
|
||||||
|
PROJECT_FOLDER.clone(),
|
||||||
|
)));
|
||||||
|
self.dialog.as_mut().unwrap().open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||||
|
// Render each field
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+339
@@ -0,0 +1,339 @@
|
|||||||
|
use egui::{RichText, ScrollArea};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::{PROJECT_FOLDER, RightPanelContent, object::ObjectInstance};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub enum FieldType {
|
||||||
|
Image,
|
||||||
|
SingleLine,
|
||||||
|
MultiLine,
|
||||||
|
Date,
|
||||||
|
Number,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct FieldDefinition {
|
||||||
|
pub name: String,
|
||||||
|
pub field_type: FieldType,
|
||||||
|
pub required: bool,
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub enum EditorMode {
|
||||||
|
#[default]
|
||||||
|
View,
|
||||||
|
EditTemplate,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Template {
|
||||||
|
pub name: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub fields: Vec<FieldDefinition>,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub path: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub saved: bool,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub editor_mode: EditorMode,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub dialog: Option<egui_file::FileDialog>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Template {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
name: "New Template".to_string(),
|
||||||
|
description: Some(String::from("Placeholder description")),
|
||||||
|
fields: Vec::new(),
|
||||||
|
saved: false,
|
||||||
|
path: None,
|
||||||
|
editor_mode: EditorMode::default(),
|
||||||
|
dialog: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Template {
|
||||||
|
pub fn load(path: PathBuf) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
|
let content = std::fs::read_to_string(&path)?;
|
||||||
|
let mut template: Self = serde_json::from_str(&content)?;
|
||||||
|
template.path = Some(path);
|
||||||
|
Ok(template)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let content = serde_json::to_string_pretty(self)?;
|
||||||
|
std::fs::write(self.path.as_ref().ok_or("no path")?, content)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui(
|
||||||
|
&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
new_instance: &mut RightPanelContent,
|
||||||
|
new_field_name: &mut String,
|
||||||
|
new_field_type: &mut FieldType,
|
||||||
|
new_field_required: &mut bool,
|
||||||
|
new_field_description: &mut String,
|
||||||
|
) {
|
||||||
|
match self.editor_mode {
|
||||||
|
EditorMode::View => {
|
||||||
|
ScrollArea::vertical().show(ui, |ui| {
|
||||||
|
if ui.button("Edit Template").clicked() {
|
||||||
|
self.editor_mode = EditorMode::EditTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.button("New Instance").clicked() {
|
||||||
|
*new_instance = RightPanelContent::Instance {
|
||||||
|
instance: Box::new(ObjectInstance::new(self)),
|
||||||
|
path: None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.viewer_ui(ui, new_instance);
|
||||||
|
|
||||||
|
if self.saved {
|
||||||
|
ui.label(RichText::new("✓ Saved").color(egui::Color32::GREEN));
|
||||||
|
} else {
|
||||||
|
ui.label(RichText::new("* Unsaved").color(egui::Color32::YELLOW));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show current save path or "Not saved yet"
|
||||||
|
let path_display = self
|
||||||
|
.path
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|p| p.to_str())
|
||||||
|
.unwrap_or("Not saved yet");
|
||||||
|
ui.label(path_display);
|
||||||
|
}
|
||||||
|
EditorMode::EditTemplate => {
|
||||||
|
ScrollArea::vertical().show(ui, |ui| {
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
self.editor_ui(
|
||||||
|
ui,
|
||||||
|
new_field_name,
|
||||||
|
new_field_type,
|
||||||
|
new_field_required,
|
||||||
|
new_field_description,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save/Cancel buttons
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if ui.button("Save Template").clicked() {
|
||||||
|
if let Some(_path) = &self.path {
|
||||||
|
if let Err(e) = self.save() {
|
||||||
|
eprintln!("Failed to save: {e}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Open save dialog
|
||||||
|
let mut dialog = egui_file::FileDialog::save_file(Some(
|
||||||
|
PROJECT_FOLDER.clone(),
|
||||||
|
));
|
||||||
|
dialog.open();
|
||||||
|
self.dialog = Some(dialog);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.button("Cancel").clicked() {
|
||||||
|
self.editor_mode = EditorMode::View;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn viewer_ui(&self, ui: &mut egui::Ui, new_instance: &mut RightPanelContent) {
|
||||||
|
// Show template view
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.heading(&self.name);
|
||||||
|
|
||||||
|
if let Some(description) = &self.description {
|
||||||
|
ui.separator();
|
||||||
|
ui.label(description);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
ui.heading("Fields");
|
||||||
|
|
||||||
|
for field in &self.fields {
|
||||||
|
ui.separator();
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.strong(&field.name);
|
||||||
|
ui.label(format!("({:?})", field.field_type));
|
||||||
|
if field.required {
|
||||||
|
ui.label("*");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(desc) = &field.description {
|
||||||
|
ui.label(desc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
if ui.button("Create New Instance").clicked() {
|
||||||
|
*new_instance = RightPanelContent::Instance {
|
||||||
|
instance: Box::new(ObjectInstance::new(self)),
|
||||||
|
path: None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn editor_ui(
|
||||||
|
&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,
|
||||||
|
) {
|
||||||
|
// Template name and description
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Template Name:");
|
||||||
|
ui.text_edit_singleline(&mut self.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Description:");
|
||||||
|
ui.text_edit_multiline(self.description.get_or_insert_with(String::new));
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
ui.heading("Fields");
|
||||||
|
|
||||||
|
// List of fields
|
||||||
|
let mut to_remove = None;
|
||||||
|
for (i, field) in self.fields.iter_mut().enumerate() {
|
||||||
|
let id = ui.make_persistent_id(format!("field_{i}"));
|
||||||
|
|
||||||
|
egui::collapsing_header::CollapsingState::load_with_default_open(ui.ctx(), id, true)
|
||||||
|
.show_header(ui, |ui| {
|
||||||
|
ui.label(field.name.clone());
|
||||||
|
if ui.button("❌").clicked() {
|
||||||
|
to_remove = Some(i);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.body(|ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Name:");
|
||||||
|
ui.text_edit_singleline(&mut field.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Type:");
|
||||||
|
egui::ComboBox::from_id_salt(format!("field_type_{i}"))
|
||||||
|
.selected_text(format!("{:?}", field.field_type))
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
for variant in [
|
||||||
|
FieldType::SingleLine,
|
||||||
|
FieldType::MultiLine,
|
||||||
|
FieldType::Number,
|
||||||
|
FieldType::Date,
|
||||||
|
FieldType::Image,
|
||||||
|
] {
|
||||||
|
ui.selectable_value(
|
||||||
|
&mut field.field_type,
|
||||||
|
variant.clone(),
|
||||||
|
format!("{variant:?}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.checkbox(&mut field.required, "Required");
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Description:");
|
||||||
|
ui.text_edit_singleline(field.description.get_or_insert_with(String::new));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove field if needed
|
||||||
|
if let Some(index) = to_remove {
|
||||||
|
self.fields.remove(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new field
|
||||||
|
ui.separator();
|
||||||
|
ui.heading("Add New Field");
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Name:");
|
||||||
|
ui.text_edit_singleline(new_field_name);
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Type:");
|
||||||
|
egui::ComboBox::from_id_salt("new_field_type")
|
||||||
|
.selected_text(format!("{new_field_type:?}"))
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
for variant in [
|
||||||
|
FieldType::SingleLine,
|
||||||
|
FieldType::MultiLine,
|
||||||
|
FieldType::Number,
|
||||||
|
FieldType::Date,
|
||||||
|
FieldType::Image,
|
||||||
|
] {
|
||||||
|
ui.selectable_value(
|
||||||
|
new_field_type,
|
||||||
|
variant.clone(),
|
||||||
|
format!("{variant:?}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.checkbox(new_field_required, "Required");
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Description:");
|
||||||
|
ui.text_edit_singleline(new_field_description);
|
||||||
|
});
|
||||||
|
|
||||||
|
if ui.button("Add Field").clicked() && !new_field_name.is_empty() {
|
||||||
|
self.fields.push(FieldDefinition {
|
||||||
|
name: new_field_name.clone(),
|
||||||
|
field_type: new_field_type.clone(),
|
||||||
|
required: *new_field_required,
|
||||||
|
description: if new_field_description.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(new_field_description.clone())
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset new field form
|
||||||
|
new_field_name.clear();
|
||||||
|
*new_field_required = false;
|
||||||
|
new_field_description.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
pub struct FieldValue {
|
||||||
|
pub value: String,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub modified: bool,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user