diff options
| author | pml68 <contact@pml68.dev> | 2024-12-30 02:15:10 +0100 |
|---|---|---|
| committer | pml68 <contact@pml68.dev> | 2024-12-30 02:15:10 +0100 |
| commit | 0dad6dd5b8395d3089bed022a4b8830f7cae7d9f (patch) | |
| tree | e28ccc225c07f9b49a1c233fb81c8eddd75a01c0 | |
| parent | refactor!: switch to `uuid` for uuid generation (diff) | |
| download | iced-builder-0dad6dd5b8395d3089bed022a4b8830f7cae7d9f.tar.gz | |
feat: add config loading, with theming support, limited to Palette for now
Diffstat (limited to '')
| -rw-r--r-- | Cargo.lock | 130 | ||||
| -rw-r--r-- | iced_builder/Cargo.toml | 12 | ||||
| -rw-r--r-- | iced_builder/assets/config.toml | 1 | ||||
| -rw-r--r-- | iced_builder/assets/themes/Rose Pine.toml | 5 | ||||
| -rw-r--r-- | iced_builder/src/config.rs | 123 | ||||
| -rw-r--r-- | iced_builder/src/dialogs.rs | 9 | ||||
| -rw-r--r-- | iced_builder/src/environment.rs | 51 | ||||
| -rw-r--r-- | iced_builder/src/error.rs | 19 | ||||
| -rw-r--r-- | iced_builder/src/lib.rs | 3 | ||||
| -rw-r--r-- | iced_builder/src/main.rs | 71 | ||||
| -rw-r--r-- | iced_builder/src/panes/code_view.rs | 6 | ||||
| -rw-r--r-- | iced_builder/src/panes/element_list.rs | 8 | ||||
| -rw-r--r-- | iced_builder/src/theme.rs | 124 | ||||
| -rw-r--r-- | iced_builder/src/types/project.rs | 53 | ||||
| -rwxr-xr-x | iced_builder/src/types/rendered_element.rs | 20 |
15 files changed, 518 insertions, 117 deletions
@@ -293,7 +293,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", ] [[package]] @@ -328,7 +328,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", ] [[package]] @@ -484,7 +484,7 @@ checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", ] [[package]] @@ -558,9 +558,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.5" +version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" +checksum = "8d6dbb628b8f8555f86d0323c2eb39e3ec81901f4b83e091db8a6a76d316a333" dependencies = [ "jobserver", "libc", @@ -967,7 +967,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", ] [[package]] @@ -997,6 +997,16 @@ dependencies = [ ] [[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] name = "dirs-sys" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1008,6 +1018,17 @@ dependencies = [ ] [[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] name = "dispatch" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1021,7 +1042,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", ] [[package]] @@ -1157,7 +1178,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", ] [[package]] @@ -1359,7 +1380,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", ] [[package]] @@ -1453,7 +1474,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", ] [[package]] @@ -1969,13 +1990,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b89898a5300d2800451406a3d6cfaed89ef08797dc392143f7c16c5c747e95" dependencies = [ "quote", - "syn 2.0.91", + "syn 2.0.93", ] [[package]] name = "iced_builder" version = "0.1.0" dependencies = [ + "dirs-next", "embed-resource 3.0.1", "iced", "iced_anim", @@ -1987,8 +2009,11 @@ dependencies = [ "serde_json", "thiserror 2.0.9", "tokio", + "tokio-stream", + "toml", "uuid", "windows_exe_info", + "xdg", ] [[package]] @@ -2311,7 +2336,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", ] [[package]] @@ -2882,7 +2907,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", ] [[package]] @@ -3167,7 +3192,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", ] [[package]] @@ -3247,7 +3272,7 @@ dependencies = [ "by_address", "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", ] [[package]] @@ -3368,7 +3393,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", ] [[package]] @@ -3403,7 +3428,7 @@ checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", ] [[package]] @@ -3693,9 +3718,9 @@ checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" [[package]] name = "reqwest" -version = "0.12.10" +version = "0.12.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3536321cfc54baa8cf3e273d5e1f63f889067829c4b410fcdbac8ca7b80994" +checksum = "7fe060fe50f524be480214aba758c71f99f90ee8c83c5a36b5e9e1d568eb4eb3" dependencies = [ "base64", "bytes", @@ -4004,22 +4029,22 @@ checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" [[package]] name = "serde" -version = "1.0.216" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.216" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", ] [[package]] @@ -4042,7 +4067,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", ] [[package]] @@ -4333,9 +4358,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.91" +version = "2.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035" +checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058" dependencies = [ "proc-macro2", "quote", @@ -4359,7 +4384,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", ] [[package]] @@ -4481,7 +4506,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", ] [[package]] @@ -4492,7 +4517,7 @@ checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", ] [[package]] @@ -4637,6 +4662,17 @@ dependencies = [ ] [[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] name = "tokio-util" version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4729,7 +4765,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", ] [[package]] @@ -5005,7 +5041,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", "wasm-bindgen-shared", ] @@ -5040,7 +5076,7 @@ checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5762,6 +5798,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" [[package]] +name = "xdg" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" + +[[package]] name = "xdg-home" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5837,7 +5879,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", "synstructure", ] @@ -5924,7 +5966,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", "zvariant_utils 2.1.0", ] @@ -5937,7 +5979,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", "zbus_names 4.1.0", "zvariant 5.1.0", "zvariant_utils 3.0.2", @@ -5990,7 +6032,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", ] [[package]] @@ -6010,7 +6052,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", "synstructure", ] @@ -6031,7 +6073,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", ] [[package]] @@ -6053,7 +6095,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", ] [[package]] @@ -6174,7 +6216,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", "zvariant_utils 2.1.0", ] @@ -6187,7 +6229,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", "zvariant_utils 3.0.2", ] @@ -6199,7 +6241,7 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ "proc-macro2", "quote", - "syn 2.0.91", + "syn 2.0.93", ] [[package]] @@ -6212,6 +6254,6 @@ dependencies = [ "quote", "serde", "static_assertions", - "syn 2.0.91", + "syn 2.0.93", "winnow", ] diff --git a/iced_builder/Cargo.toml b/iced_builder/Cargo.toml index c2437ec..638006b 100644 --- a/iced_builder/Cargo.toml +++ b/iced_builder/Cargo.toml @@ -13,13 +13,17 @@ iced = { version = "0.13.1", features = [ "image","svg","canvas","qr_code","adva # iced_aw = { version = "0.11.0", default-features = false, features = ["menu","color_picker"] } iced_anim = { version = "0.1.4", features = ["derive", "serde"] } iced_drop = { path = "../iced_drop" } -serde = { version = "1.0.216", features = ["derive"] } -serde_json = "1.0.133" -tokio = { version = "1.42.0", features = ["fs"] } +serde = { version = "1.0.217", features = ["derive"] } +serde_json = "1.0.134" +toml = "0.8.19" +tokio = { version = "1.42", features = ["fs"] } +tokio-stream = { version = "0.1", features = ["fs"] } rfd = { version = "0.15.1", default-features = false, features = ["async-std", "gtk3"] } rust-format = "0.3.4" uuid = { version = "1.11.0", features = ["v4", "serde"] } -thiserror = "2.0.6" +thiserror = "2.0.9" +xdg = "2.5.2" +dirs-next = "2.0.0" [build-dependencies] iced_fontello = "0.13.1" diff --git a/iced_builder/assets/config.toml b/iced_builder/assets/config.toml new file mode 100644 index 0000000..8ef1bb3 --- /dev/null +++ b/iced_builder/assets/config.toml @@ -0,0 +1 @@ +theme = "Rose Pine" diff --git a/iced_builder/assets/themes/Rose Pine.toml b/iced_builder/assets/themes/Rose Pine.toml new file mode 100644 index 0000000..a4eeeeb --- /dev/null +++ b/iced_builder/assets/themes/Rose Pine.toml @@ -0,0 +1,5 @@ +background = "#26233a" +text = "#e0def4" +primary = "#9ccfd8" +success = "#f6c177" +danger = "#eb6f92" diff --git a/iced_builder/src/config.rs b/iced_builder/src/config.rs new file mode 100644 index 0000000..75aee1d --- /dev/null +++ b/iced_builder/src/config.rs @@ -0,0 +1,123 @@ +use std::path::PathBuf; + +use serde::Deserialize; +use tokio_stream::wrappers::ReadDirStream; +use tokio_stream::StreamExt; + +use crate::theme::{theme_from_str, theme_index, Theme, ThemePalette}; +use crate::{environment, Error, Result}; + +#[derive(Debug, Default)] +pub struct Config { + pub theme: Theme, + pub last_project: Option<PathBuf>, +} + +impl Config { + pub fn selected_theme(&self) -> iced::Theme { + self.theme.selected.clone() + } + + pub fn config_dir() -> PathBuf { + let dir = environment::config_dir(); + + if !dir.exists() { + std::fs::create_dir_all(dir.as_path()) + .expect("expected permissions to create config folder"); + } + println!("{}", dir.to_string_lossy()); + dir + } + + pub fn themes_dir() -> PathBuf { + let dir = Self::config_dir().join("themes"); + + if !dir.exists() { + std::fs::create_dir_all(dir.as_path()) + .expect("expected permissions to create themes folder"); + } + println!("{}", dir.to_string_lossy()); + dir + } + + pub fn config_file_path() -> PathBuf { + Self::config_dir().join(environment::CONFIG_FILE_NAME) + } + + pub async fn load() -> Result<Self> { + use tokio::fs; + + #[derive(Deserialize)] + pub struct Configuration { + pub theme: String, + pub last_project: Option<PathBuf>, + } + + let path = Self::config_file_path(); + if !path.try_exists()? { + return Err(Error::ConfigMissing); + } + + let content = fs::read_to_string(path).await?; + + let Configuration { + theme, + last_project, + } = toml::from_str(content.as_ref())?; + + let theme = Self::load_theme(theme).await.unwrap_or_default(); + + Ok(Self { + theme, + last_project, + }) + } + + pub async fn load_theme(theme_name: String) -> Result<Theme> { + use tokio::fs; + + let read_entry = |entry: fs::DirEntry| async move { + let content = fs::read_to_string(entry.path()).await.ok()?; + + let palette: ThemePalette = + toml::from_str(content.as_ref()).ok()?; + let name = entry.path().file_stem()?.to_string_lossy().to_string(); + + Some(iced::Theme::custom(name, palette.into())) + }; + + let mut all = iced::Theme::ALL.to_owned(); + let mut selected = iced::Theme::default(); + + if theme_index(theme_name.clone(), iced::Theme::ALL).is_some() { + selected = theme_from_str(None, &theme_name); + } + + let mut stream = + ReadDirStream::new(fs::read_dir(Self::themes_dir()).await?); + while let Some(entry) = stream.next().await { + let Ok(entry) = entry else { + continue; + }; + + let Some(file_name) = entry.file_name().to_str().map(String::from) + else { + continue; + }; + + if let Some(file_name) = file_name.strip_suffix(".toml") { + if let Some(theme) = read_entry(entry).await { + if file_name == theme_name { + selected = theme.clone(); + } + all.push(theme); + } + } + } + + Ok(Theme { + selected, + all: all.into(), + }) + } +} diff --git a/iced_builder/src/dialogs.rs b/iced_builder/src/dialogs.rs index 047ffd2..c9a5ba2 100644 --- a/iced_builder/src/dialogs.rs +++ b/iced_builder/src/dialogs.rs @@ -9,6 +9,15 @@ pub fn error_dialog(description: impl Into<String>) { .show(); } +pub fn warning_dialog(description: impl Into<String>) { + let _ = MessageDialog::new() + .set_level(MessageLevel::Warning) + .set_buttons(MessageButtons::Ok) + .set_title("Heads up!") + .set_description(description) + .show(); +} + pub fn unsaved_changes_dialog( description: impl Into<String>, ) -> MessageDialogResult { diff --git a/iced_builder/src/environment.rs b/iced_builder/src/environment.rs new file mode 100644 index 0000000..52e7ca5 --- /dev/null +++ b/iced_builder/src/environment.rs @@ -0,0 +1,51 @@ +use std::env; +use std::path::PathBuf; + +pub const CONFIG_FILE_NAME: &str = "config.toml"; + +pub fn config_dir() -> PathBuf { + portable_dir().unwrap_or_else(platform_specific_config_dir) +} + +pub fn data_dir() -> PathBuf { + portable_dir().unwrap_or_else(|| { + dirs_next::data_dir() + .expect("expected valid data dir") + .join("iced-builder") + }) +} + +fn portable_dir() -> Option<PathBuf> { + let exe = env::current_exe().ok()?; + let dir = exe.parent()?; + + dir.join(CONFIG_FILE_NAME) + .is_file() + .then(|| dir.to_path_buf()) +} + +fn platform_specific_config_dir() -> PathBuf { + #[cfg(target_os = "macos")] + { + xdg_config_dir().unwrap_or_else(|| { + dirs_next::config_dir() + .expect("expected valid config dir") + .join("iced-builder") + }) + } + #[cfg(not(target_os = "macos"))] + { + dirs_next::config_dir() + .expect("expected valid config dir") + .join("iced-builder") + } +} + +#[cfg(target_os = "macos")] +fn xdg_config_dir() -> Option<PathBuf> { + let config_dir = xdg::BaseDirectories::with_prefix("iced-builder") + .ok() + .and_then(|xdg| xdg.find_config_file(CONFIG_FILE_NAME))?; + + config_dir.parent().map(|p| p.to_path_buf()) +} diff --git a/iced_builder/src/error.rs b/iced_builder/src/error.rs index 8876016..9cbb6ee 100644 --- a/iced_builder/src/error.rs +++ b/iced_builder/src/error.rs @@ -7,12 +7,17 @@ use thiserror::Error; #[error(transparent)] pub enum Error { IOError(Arc<io::Error>), - SerdeError(Arc<serde_json::Error>), + #[error("config does not exist")] + ConfigMissing, + #[error("JSON parsing error: {0}")] + SerdeJSONError(Arc<serde_json::Error>), + #[error("TOML parsing error: {0}")] + SerdeTOMLError(#[from] toml::de::Error), FormatError(Arc<rust_format::Error>), - #[error("The element tree contains no matching element")] + #[error("the element tree contains no matching element")] NonExistentElement, #[error( - "The file dialog has been closed without selecting a valid option" + "the file dialog has been closed without selecting a valid option" )] DialogClosed, #[error("{0}")] @@ -27,7 +32,7 @@ impl From<io::Error> for Error { impl From<serde_json::Error> for Error { fn from(value: serde_json::Error) -> Self { - Self::SerdeError(Arc::new(value)) + Self::SerdeJSONError(Arc::new(value)) } } @@ -42,3 +47,9 @@ impl From<&str> for Error { Self::Other(value.to_owned()) } } + +impl From<String> for Error { + fn from(value: String) -> Self { + Self::Other(value) + } +} diff --git a/iced_builder/src/lib.rs b/iced_builder/src/lib.rs index f3165f5..847e01e 100644 --- a/iced_builder/src/lib.rs +++ b/iced_builder/src/lib.rs @@ -1,7 +1,10 @@ +pub mod config; pub mod dialogs; +pub mod environment; pub mod error; pub mod icon; pub mod panes; +pub mod theme; pub mod types; pub mod widget; diff --git a/iced_builder/src/main.rs b/iced_builder/src/main.rs index a041c6f..02e1ae0 100644 --- a/iced_builder/src/main.rs +++ b/iced_builder/src/main.rs @@ -5,22 +5,34 @@ use iced::widget::pane_grid::{self, Pane, PaneGrid}; use iced::widget::{container, pick_list, row, text_editor, Column}; use iced::{clipboard, keyboard, Alignment, Element, Length, Task, Theme}; use iced_anim::{Animation, Spring}; -use iced_builder::dialogs::{error_dialog, unsaved_changes_dialog}; -use iced_builder::icon; +use iced_builder::config::Config; +use iced_builder::dialogs::{ + error_dialog, unsaved_changes_dialog, warning_dialog, +}; use iced_builder::panes::{code_view, designer_view, element_list}; use iced_builder::types::{ Action, DesignerPage, ElementName, Message, Project, }; +use iced_builder::{icon, Error}; use rfd::MessageDialogResult; +use tokio::runtime; + +fn main() -> Result<(), Box<dyn std::error::Error>> { + let config_load = { + let rt = runtime::Builder::new_current_thread() + .enable_all() + .build()?; -const THEMES: &'static [Theme] = &[Theme::SolarizedDark, Theme::SolarizedLight]; + rt.block_on(Config::load()) + }; -fn main() -> iced::Result { iced::application(App::title, App::update, App::view) .font(icon::FONT) .theme(|state| state.theme.value().clone()) .subscription(App::subscription) - .run_with(App::new) + .run_with(move || App::new(config_load))?; + + Ok(()) } struct App { @@ -28,6 +40,7 @@ struct App { is_loading: bool, project_path: Option<PathBuf>, project: Project, + config: Config, theme: Spring<Theme>, pane_state: pane_grid::State<Panes>, focus: Option<Pane>, @@ -43,7 +56,7 @@ enum Panes { } impl App { - fn new() -> (Self, Task<Message>) { + fn new(config_load: Result<Config, Error>) -> (Self, Task<Message>) { let state = pane_grid::State::with_configuration( pane_grid::Configuration::Split { axis: pane_grid::Axis::Vertical, @@ -52,20 +65,48 @@ impl App { b: Box::new(pane_grid::Configuration::Pane(Panes::ElementList)), }, ); + + let config = match config_load { + Ok(config) => { + println!("{config:?}"); + config + } + Err(_) => Config::default(), + }; + + let theme = config.selected_theme(); + + let mut task = Task::none(); + + if let Some(path) = config.last_project.clone() { + if path.exists() && path.is_file() { + task = Task::perform( + Project::from_path(path), + Message::FileOpened, + ); + } else { + warning_dialog(format!( + "The file {} does not exist, or isn't a file.", + path.to_string_lossy().to_string() + )); + } + } + ( Self { is_dirty: false, is_loading: false, project_path: None, project: Project::new(), - theme: Spring::new(Theme::SolarizedDark), + config, + theme: Spring::new(theme), pane_state: state, focus: None, designer_page: DesignerPage::DesignerView, element_list: ElementName::ALL, editor_content: text_editor::Content::new(), }, - Task::none(), + task, ) } @@ -104,7 +145,7 @@ impl App { } } Message::RefreshEditorContent => { - match self.project.clone().app_code() { + match self.project.clone().app_code(&self.config) { Ok(code) => { self.editor_content = text_editor::Content::with_text(&code); @@ -209,14 +250,14 @@ impl App { self.is_loading = true; return Task::perform( - Project::from_path(), + Project::from_file(), Message::FileOpened, ); } else { if let MessageDialogResult::Ok = unsaved_changes_dialog("You have unsaved changes. Do you wish to discard these and open another project?") { self.is_dirty = false; self.is_loading = true; - return Task::perform(Project::from_path(), Message::FileOpened); + return Task::perform(Project::from_file(), Message::FileOpened); } } } @@ -231,7 +272,7 @@ impl App { self.project_path = Some(path); self.editor_content = text_editor::Content::with_text( &project - .app_code() + .app_code(&self.config) .unwrap_or_else(|err| err.to_string()), ); } @@ -299,7 +340,7 @@ impl App { fn view(&self) -> Element<'_, Message> { let header = row![pick_list( - THEMES, + self.config.theme.all.clone(), Some(self.theme.target().clone()), |theme| { Message::ToggleTheme(theme.into()) } )] @@ -311,12 +352,12 @@ impl App { Panes::Designer => match &self.designer_page { DesignerPage::DesignerView => designer_view::view( &self.project.element_tree, - self.project.get_theme(), + self.project.get_theme(&self.config), is_focused, ), DesignerPage::CodeView => code_view::view( &self.editor_content, - self.theme.value().clone(), + self.theme.target().clone(), is_focused, ), }, diff --git a/iced_builder/src/panes/code_view.rs b/iced_builder/src/panes/code_view.rs index fe7801c..b95b653 100644 --- a/iced_builder/src/panes/code_view.rs +++ b/iced_builder/src/panes/code_view.rs @@ -5,11 +5,11 @@ use crate::icon::copy; use crate::types::{DesignerPage, Message}; use crate::widget::tip; -pub fn view<'a>( - editor_content: &'a text_editor::Content, +pub fn view( + editor_content: &text_editor::Content, theme: Theme, is_focused: bool, -) -> pane_grid::Content<'a, Message> { +) -> pane_grid::Content<'_, Message> { let title = row![ text("Generated Code"), Space::with_width(Length::Fill), diff --git a/iced_builder/src/panes/element_list.rs b/iced_builder/src/panes/element_list.rs index 74188af..8a1c6eb 100644 --- a/iced_builder/src/panes/element_list.rs +++ b/iced_builder/src/panes/element_list.rs @@ -5,7 +5,7 @@ use iced_drop::droppable; use super::style; use crate::types::{ElementName, Message}; -fn items_list_view<'a>(items: &'a [ElementName]) -> Element<'a, Message> { +fn items_list_view(items: &[ElementName]) -> Element<'_, Message> { let mut column = Column::new() .spacing(20) .align_x(Alignment::Center) @@ -26,10 +26,10 @@ fn items_list_view<'a>(items: &'a [ElementName]) -> Element<'a, Message> { .into() } -pub fn view<'a>( - element_list: &'a [ElementName], +pub fn view( + element_list: &[ElementName], is_focused: bool, -) -> pane_grid::Content<'a, Message> { +) -> pane_grid::Content<'_, Message> { let items_list = items_list_view(element_list); let content = column![items_list] .align_x(Alignment::Center) diff --git a/iced_builder/src/theme.rs b/iced_builder/src/theme.rs new file mode 100644 index 0000000..21ec6e3 --- /dev/null +++ b/iced_builder/src/theme.rs @@ -0,0 +1,124 @@ +use std::sync::Arc; + +use iced::Color; + +use crate::config::Config; + +pub fn theme_index(theme_name: String, slice: &[iced::Theme]) -> Option<usize> { + slice + .iter() + .position(|theme| theme.to_string() == theme_name) +} + +pub fn theme_from_str( + config: Option<&Config>, + theme_name: &str, +) -> iced::Theme { + match theme_name { + "Light" => iced::Theme::Light, + "Dark" => iced::Theme::Dark, + "Dracula" => iced::Theme::Dracula, + "Nord" => iced::Theme::Nord, + "Solarized Light" => iced::Theme::SolarizedLight, + "Solarized Dark" => iced::Theme::SolarizedDark, + "Gruvbox Light" => iced::Theme::GruvboxLight, + "Gruvbox Dark" => iced::Theme::GruvboxDark, + "Catppuccin Latte" => iced::Theme::CatppuccinLatte, + "Catppuccin Frappé" => iced::Theme::CatppuccinFrappe, + "Catppuccin Macchiato" => iced::Theme::CatppuccinMacchiato, + "Catppuccin Mocha" => iced::Theme::CatppuccinMocha, + "Tokyo Night" => iced::Theme::TokyoNight, + "Tokyo Night Storm" => iced::Theme::TokyoNightStorm, + "Tokyo Night Light" => iced::Theme::TokyoNightLight, + "Kanagawa Wave" => iced::Theme::KanagawaWave, + "Kanagawa Dragon" => iced::Theme::KanagawaDragon, + "Kanagawa Lotus" => iced::Theme::KanagawaLotus, + "Moonfly" => iced::Theme::Moonfly, + "Nightfly" => iced::Theme::Nightfly, + "Oxocarbon" => iced::Theme::Oxocarbon, + "Ferra" => iced::Theme::Ferra, + _ => { + if let Some(config) = config { + if theme_name == config.theme.selected.to_string() { + config.theme.selected.clone() + } else if let Some(index) = + theme_index(theme_name.into(), &config.theme.all) + { + config.theme.all[index].clone() + } else { + iced::Theme::default() + } + } else { + iced::Theme::default() + } + } + } +} + +#[derive(Debug)] +pub struct Theme { + pub selected: iced::Theme, + pub all: Arc<[iced::Theme]>, +} + +impl Default for Theme { + fn default() -> Self { + Self { + selected: iced::Theme::default(), + all: iced::Theme::ALL.into(), + } + } +} + +#[derive(Debug, serde::Deserialize)] +pub struct ThemePalette { + #[serde(with = "color_serde")] + background: Color, + #[serde(with = "color_serde")] + text: Color, + #[serde(with = "color_serde")] + primary: Color, + #[serde(with = "color_serde")] + success: Color, + #[serde(with = "color_serde")] + danger: Color, +} + +impl Default for ThemePalette { + fn default() -> Self { + let palette = iced::Theme::default().palette(); + Self { + background: palette.background, + text: palette.text, + primary: palette.primary, + success: palette.success, + danger: palette.danger, + } + } +} + +impl From<ThemePalette> for iced::theme::Palette { + fn from(palette: ThemePalette) -> Self { + iced::theme::Palette { + background: palette.background, + text: palette.text, + primary: palette.primary, + success: palette.success, + danger: palette.danger, + } + } +} + +mod color_serde { + use iced::Color; + use serde::{Deserialize, Deserializer}; + + pub fn deserialize<'de, D>(deserializer: D) -> Result<Color, D::Error> + where + D: Deserializer<'de>, + { + Ok(String::deserialize(deserializer) + .map(|hex| Color::parse(&hex))? + .unwrap_or(Color::TRANSPARENT)) + } +} diff --git a/iced_builder/src/types/project.rs b/iced_builder/src/types/project.rs index f4dbcc4..6f3b7ed 100644 --- a/iced_builder/src/types/project.rs +++ b/iced_builder/src/types/project.rs @@ -5,6 +5,7 @@ use rust_format::{Config, Edition, Formatter, RustFmt}; use serde::{Deserialize, Serialize}; use super::rendered_element::RenderedElement; +use crate::theme::theme_from_str; use crate::{Error, Result}; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -14,6 +15,12 @@ pub struct Project { pub element_tree: Option<RenderedElement>, } +impl Default for Project { + fn default() -> Self { + Self::new() + } +} + impl Project { pub fn new() -> Self { Self { @@ -23,38 +30,21 @@ impl Project { } } - pub fn get_theme(&self) -> Theme { + pub fn get_theme(&self, config: &crate::config::Config) -> Theme { match &self.theme { - Some(theme) => match theme.as_str() { - "Light" => Theme::Light, - "Dark" => Theme::Dark, - "Dracula" => Theme::Dracula, - "Nord" => Theme::Nord, - "Solarized Light" => Theme::SolarizedLight, - "Solarized Dark" => Theme::SolarizedDark, - "Gruvbox Light" => Theme::GruvboxLight, - "Gruvbox Dark" => Theme::GruvboxDark, - "Catppuccin Latte" => Theme::CatppuccinLatte, - "Catppuccin Frappé" => Theme::CatppuccinFrappe, - "Catppuccin Macchiato" => Theme::CatppuccinMacchiato, - "Catppuccin Mocha" => Theme::CatppuccinMocha, - "Tokyo Night" => Theme::TokyoNight, - "Tokyo Night Storm" => Theme::TokyoNightStorm, - "Tokyo Night Light" => Theme::TokyoNightLight, - "Kanagawa Wave" => Theme::KanagawaWave, - "Kanagawa Dragon" => Theme::KanagawaDragon, - "Kanagawa Lotus" => Theme::KanagawaLotus, - "Moonfly" => Theme::Moonfly, - "Nightfly" => Theme::Nightfly, - "Oxocarbon" => Theme::Oxocarbon, - "Ferra" => Theme::Ferra, - _ => Theme::Dark, - }, + Some(theme) => theme_from_str(Some(config), theme), None => Theme::Dark, } } - pub async fn from_path() -> Result<(PathBuf, Self)> { + pub async fn from_path(path: PathBuf) -> Result<(PathBuf, Self)> { + let contents = tokio::fs::read_to_string(&path).await?; + let element: Self = serde_json::from_str(&contents)?; + + Ok((path, element)) + } + + pub async fn from_file() -> Result<(PathBuf, Self)> { let picked_file = rfd::AsyncFileDialog::new() .set_title("Open a JSON file...") .add_filter("*.json, *.JSON", &["json", "JSON"]) @@ -64,10 +54,7 @@ impl Project { let path = picked_file.path().to_owned(); - let contents = tokio::fs::read_to_string(&path).await?; - let element: Self = serde_json::from_str(&contents)?; - - Ok((path, element)) + Self::from_path(path).await } pub async fn write_to_file(self, path: Option<PathBuf>) -> Result<PathBuf> { @@ -91,7 +78,7 @@ impl Project { Ok(path) } - pub fn app_code(&self) -> Result<String> { + pub fn app_code(&self, config: &crate::config::Config) -> Result<String> { match self.element_tree { Some(ref element_tree) => { let (imports, view) = element_tree.codegen(); @@ -127,7 +114,7 @@ impl Project { Some(ref t) => t, None => "New app", }, - self.get_theme().to_string().replace(" ", "") + self.get_theme(config).to_string().replace(" ", "") ); let config = Config::new_str() .edition(Edition::Rust2021) diff --git a/iced_builder/src/types/rendered_element.rs b/iced_builder/src/types/rendered_element.rs index ccc8668..5270e5a 100755 --- a/iced_builder/src/types/rendered_element.rs +++ b/iced_builder/src/types/rendered_element.rs @@ -43,7 +43,7 @@ impl RenderedElement { pub fn find_by_id(&mut self, id: Id) -> Option<&mut Self> { if self.get_id() == id.clone() { - return Some(self); + Some(self) } else if let Some(child_elements) = self.child_elements.as_mut() { for element in child_elements { let element = element.find_by_id(id.clone()); @@ -51,9 +51,9 @@ impl RenderedElement { return element; } } - return None; + None } else { - return None; + None } } @@ -62,12 +62,12 @@ impl RenderedElement { child_element: &RenderedElement, ) -> Option<&mut Self> { if child_element == self { - return Some(self); + Some(self) } else if self.child_elements.is_some() { if self .child_elements .clone() - .unwrap_or(vec![]) + .unwrap_or_default() .contains(child_element) { return Some(self); @@ -160,7 +160,7 @@ impl RenderedElement { } } - fn preset_options<'a>(mut self, options: &[&'a str]) -> Self { + fn preset_options(mut self, options: &[&str]) -> Self { for opt in options { let _ = self.options.insert(opt.to_string(), None); } @@ -410,10 +410,10 @@ impl Action { .find_by_id(id.clone()) .unwrap(); - // Element IS a parent but ISN'T a non-empty container - match element.is_parent() - && !(element.name == ElementName::Container - && !element.is_empty()) + // Element is a parent and isn't a non-empty container + match (element.is_empty() + || !(element.name == ElementName::Container)) + && element.is_parent() { true => { action = Self::PushFront(id); |
