From 714ab9d4376fe890f269e47493691518a4e96289 Mon Sep 17 00:00:00 2001 From: pml68 Date: Sat, 28 Dec 2024 00:46:11 +0100 Subject: refactor!: switch to `uuid` for uuid generation Existing project files can only be opened after deleting all ids --- iced_builder/src/types/rendered_element.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'iced_builder/src') diff --git a/iced_builder/src/types/rendered_element.rs b/iced_builder/src/types/rendered_element.rs index d4d1a6c..ccc8668 100755 --- a/iced_builder/src/types/rendered_element.rs +++ b/iced_builder/src/types/rendered_element.rs @@ -1,9 +1,9 @@ use std::collections::BTreeMap; -use blob_uuid::random_blob; use iced::advanced::widget::Id; use iced::{widget, Element, Length}; use serde::{Deserialize, Serialize}; +use uuid::Uuid; use super::ElementName; use crate::types::Message; @@ -11,7 +11,8 @@ use crate::Result; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RenderedElement { - id: String, + #[serde(skip)] + id: Uuid, child_elements: Option>, name: ElementName, options: BTreeMap>, @@ -20,7 +21,7 @@ pub struct RenderedElement { impl RenderedElement { fn new(name: ElementName) -> Self { Self { - id: random_blob(), + id: Uuid::new_v4(), child_elements: None, name, options: BTreeMap::new(), @@ -29,7 +30,7 @@ impl RenderedElement { fn with(name: ElementName, child_elements: Vec) -> Self { Self { - id: random_blob(), + id: Uuid::new_v4(), child_elements: Some(child_elements), name, options: BTreeMap::new(), @@ -37,7 +38,7 @@ impl RenderedElement { } pub fn get_id(&self) -> Id { - Id::new(self.id.clone()) + Id::new(self.id.to_string()) } pub fn find_by_id(&mut self, id: Id) -> Option<&mut Self> { -- cgit v1.2.3 From 0dad6dd5b8395d3089bed022a4b8830f7cae7d9f Mon Sep 17 00:00:00 2001 From: pml68 Date: Mon, 30 Dec 2024 02:15:10 +0100 Subject: feat: add config loading, with theming support, limited to Palette for now --- Cargo.lock | 130 +++++++++++++++++++---------- iced_builder/Cargo.toml | 12 ++- iced_builder/assets/config.toml | 1 + iced_builder/assets/themes/Rose Pine.toml | 5 ++ iced_builder/src/config.rs | 123 +++++++++++++++++++++++++++ iced_builder/src/dialogs.rs | 9 ++ iced_builder/src/environment.rs | 51 +++++++++++ iced_builder/src/error.rs | 19 ++++- iced_builder/src/lib.rs | 3 + iced_builder/src/main.rs | 71 ++++++++++++---- iced_builder/src/panes/code_view.rs | 6 +- iced_builder/src/panes/element_list.rs | 8 +- iced_builder/src/theme.rs | 124 +++++++++++++++++++++++++++ iced_builder/src/types/project.rs | 53 +++++------- iced_builder/src/types/rendered_element.rs | 20 ++--- 15 files changed, 518 insertions(+), 117 deletions(-) create mode 100644 iced_builder/assets/config.toml create mode 100644 iced_builder/assets/themes/Rose Pine.toml create mode 100644 iced_builder/src/config.rs create mode 100644 iced_builder/src/environment.rs create mode 100644 iced_builder/src/theme.rs (limited to 'iced_builder/src') diff --git a/Cargo.lock b/Cargo.lock index 0eed251..1577101 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] @@ -996,6 +996,16 @@ dependencies = [ "dirs-sys", ] +[[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" @@ -1007,6 +1017,17 @@ dependencies = [ "winapi", ] +[[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" @@ -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]] @@ -4636,6 +4661,17 @@ dependencies = [ "tokio", ] +[[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" @@ -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", ] @@ -5761,6 +5797,12 @@ version = "0.3.8" 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" @@ -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, +} + +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 { + use tokio::fs; + + #[derive(Deserialize)] + pub struct Configuration { + pub theme: String, + pub last_project: Option, + } + + 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 { + 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) { .show(); } +pub fn warning_dialog(description: impl Into) { + 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, ) -> 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 { + 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 { + 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), - SerdeError(Arc), + #[error("config does not exist")] + ConfigMissing, + #[error("JSON parsing error: {0}")] + SerdeJSONError(Arc), + #[error("TOML parsing error: {0}")] + SerdeTOMLError(#[from] toml::de::Error), FormatError(Arc), - #[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 for Error { impl From 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 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> { + 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, project: Project, + config: Config, theme: Spring, pane_state: pane_grid::State, focus: Option, @@ -43,7 +56,7 @@ enum Panes { } impl App { - fn new() -> (Self, Task) { + fn new(config_load: Result) -> (Self, Task) { 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 { + 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 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 + 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, } +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) -> Result { @@ -91,7 +78,7 @@ impl Project { Ok(path) } - pub fn app_code(&self) -> Result { + pub fn app_code(&self, config: &crate::config::Config) -> Result { 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); -- cgit v1.2.3 From e39c7d037d3fa4b57a2ca80c3fc26ff900309e91 Mon Sep 17 00:00:00 2001 From: pml68 Date: Tue, 31 Dec 2024 00:00:09 +0100 Subject: refactor: unsaved_changes_dialog emits bool instead of MessageDialogResult --- iced_builder/src/dialogs.rs | 14 +++++++++----- iced_builder/src/main.rs | 5 ++--- 2 files changed, 11 insertions(+), 8 deletions(-) (limited to 'iced_builder/src') diff --git a/iced_builder/src/dialogs.rs b/iced_builder/src/dialogs.rs index c9a5ba2..56d22b2 100644 --- a/iced_builder/src/dialogs.rs +++ b/iced_builder/src/dialogs.rs @@ -18,13 +18,17 @@ pub fn warning_dialog(description: impl Into) { .show(); } -pub fn unsaved_changes_dialog( - description: impl Into, -) -> MessageDialogResult { - MessageDialog::new() +pub fn unsaved_changes_dialog(description: impl Into) -> bool { + let result = MessageDialog::new() .set_level(MessageLevel::Warning) .set_buttons(MessageButtons::OkCancel) .set_title("Unsaved changes") .set_description(description) - .show() + .show(); + + if let MessageDialogResult::Ok = result { + true + } else { + false + } } diff --git a/iced_builder/src/main.rs b/iced_builder/src/main.rs index 02e1ae0..86e478d 100644 --- a/iced_builder/src/main.rs +++ b/iced_builder/src/main.rs @@ -14,7 +14,6 @@ use iced_builder::types::{ Action, DesignerPage, ElementName, Message, Project, }; use iced_builder::{icon, Error}; -use rfd::MessageDialogResult; use tokio::runtime; fn main() -> Result<(), Box> { @@ -235,7 +234,7 @@ impl App { self.project_path = None; self.editor_content = text_editor::Content::new(); } else { - if let MessageDialogResult::Ok = unsaved_changes_dialog("You have unsaved changes. Do you wish to discard these and create a new project?") { + if unsaved_changes_dialog("You have unsaved changes. Do you wish to discard these and create a new project?") { self.is_dirty = false; self.project = Project::new(); self.project_path = None; @@ -254,7 +253,7 @@ impl App { Message::FileOpened, ); } else { - if let MessageDialogResult::Ok = unsaved_changes_dialog("You have unsaved changes. Do you wish to discard these and open another project?") { + if 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_file(), Message::FileOpened); -- cgit v1.2.3 From 4b6de2a4f8e793baf2caf7daedb9e401b24242a7 Mon Sep 17 00:00:00 2001 From: pml68 Date: Tue, 31 Dec 2024 03:01:43 +0100 Subject: feat: optionally define Extended Palette in custom theme --- iced_builder/assets/themes/Rose Pine.toml | 28 +++++ iced_builder/src/config.rs | 16 ++- iced_builder/src/main.rs | 5 +- iced_builder/src/theme.rs | 162 ++++++++++++++++++++++++++++- iced_builder/src/types/rendered_element.rs | 2 +- 5 files changed, 196 insertions(+), 17 deletions(-) (limited to 'iced_builder/src') diff --git a/iced_builder/assets/themes/Rose Pine.toml b/iced_builder/assets/themes/Rose Pine.toml index a4eeeeb..df7342b 100644 --- a/iced_builder/assets/themes/Rose Pine.toml +++ b/iced_builder/assets/themes/Rose Pine.toml @@ -1,5 +1,33 @@ +is_dark = true + +[palette] background = "#26233a" text = "#e0def4" primary = "#9ccfd8" success = "#f6c177" danger = "#eb6f92" + +[background] +base = { color = "#191724", text = "#e0def4" } +weak = { color = "#1f1d2e", text = "#e0def4" } +strong = { color = "#26233a", text = "#f4ebd3" } + +[primary] +base = { color = "#eb6f92", text = "#000000" } +weak = { color = "#f6c177", text = "#000000" } +strong = { color = "#ebbcba", text = "#000000" } + +[secondary] +base = { color = "#31748f", text = "#ffffff" } +weak = { color = "#9ccfd8", text = "#000000" } +strong = { color = "#c4a7e7", text = "#000000" } + +[success] +base = { color = "#9ccfd8", text = "#000000" } +weak = { color = "#6e6a86", text = "#ffffff" } +strong = { color = "#908caa", text = "#000000" } + +[danger] +base = { color = "#eb6f92", text = "#ffffff" } +weak = { color = "#f6c177", text = "#ffffff" } +strong = { color = "#524f67", text = "#ffffff" } diff --git a/iced_builder/src/config.rs b/iced_builder/src/config.rs index 75aee1d..631b35a 100644 --- a/iced_builder/src/config.rs +++ b/iced_builder/src/config.rs @@ -4,12 +4,12 @@ use serde::Deserialize; use tokio_stream::wrappers::ReadDirStream; use tokio_stream::StreamExt; -use crate::theme::{theme_from_str, theme_index, Theme, ThemePalette}; +use crate::theme::{theme_from_str, theme_index, Appearance, Theme}; use crate::{environment, Error, Result}; #[derive(Debug, Default)] pub struct Config { - pub theme: Theme, + pub theme: Appearance, pub last_project: Option, } @@ -25,7 +25,6 @@ impl Config { std::fs::create_dir_all(dir.as_path()) .expect("expected permissions to create config folder"); } - println!("{}", dir.to_string_lossy()); dir } @@ -36,7 +35,6 @@ impl Config { std::fs::create_dir_all(dir.as_path()) .expect("expected permissions to create themes folder"); } - println!("{}", dir.to_string_lossy()); dir } @@ -49,6 +47,7 @@ impl Config { #[derive(Deserialize)] pub struct Configuration { + #[serde(default)] pub theme: String, pub last_project: Option, } @@ -73,17 +72,16 @@ impl Config { }) } - pub async fn load_theme(theme_name: String) -> Result { + pub async fn load_theme(theme_name: String) -> Result { 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 theme: Theme = 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())) + Some(theme.into_iced_theme(name)) }; let mut all = iced::Theme::ALL.to_owned(); @@ -115,7 +113,7 @@ impl Config { } } - Ok(Theme { + Ok(Appearance { selected, all: all.into(), }) diff --git a/iced_builder/src/main.rs b/iced_builder/src/main.rs index 86e478d..8f3c3fb 100644 --- a/iced_builder/src/main.rs +++ b/iced_builder/src/main.rs @@ -66,10 +66,7 @@ impl App { ); let config = match config_load { - Ok(config) => { - println!("{config:?}"); - config - } + Ok(config) => config, Err(_) => Config::default(), }; diff --git a/iced_builder/src/theme.rs b/iced_builder/src/theme.rs index 21ec6e3..e41474c 100644 --- a/iced_builder/src/theme.rs +++ b/iced_builder/src/theme.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use iced::theme::palette::Extended; use iced::Color; use crate::config::Config; @@ -56,12 +57,12 @@ pub fn theme_from_str( } #[derive(Debug)] -pub struct Theme { +pub struct Appearance { pub selected: iced::Theme, pub all: Arc<[iced::Theme]>, } -impl Default for Theme { +impl Default for Appearance { fn default() -> Self { Self { selected: iced::Theme::default(), @@ -70,7 +71,15 @@ impl Default for Theme { } } -#[derive(Debug, serde::Deserialize)] +#[derive(Debug, Default, serde::Deserialize)] +pub struct Theme { + palette: ThemePalette, + is_dark: Option, + #[serde(flatten)] + extended: Option, +} + +#[derive(Debug, Clone, serde::Deserialize)] pub struct ThemePalette { #[serde(with = "color_serde")] background: Color, @@ -84,6 +93,14 @@ pub struct ThemePalette { danger: Color, } +impl Theme { + pub fn into_iced_theme(self, name: String) -> iced::Theme { + iced::Theme::custom_with_fn(name, self.palette.clone().into(), |_| { + self.into() + }) + } +} + impl Default for ThemePalette { fn default() -> Self { let palette = iced::Theme::default().palette(); @@ -109,6 +126,145 @@ impl From for iced::theme::Palette { } } +impl From for Extended { + fn from(theme: Theme) -> Self { + let mut extended = Extended::generate(theme.palette.into()); + + if let Some(is_dark) = theme.is_dark { + extended.is_dark = is_dark; + } + + if let Some(extended_palette) = theme.extended { + if let Some(background) = extended_palette.background { + if let Some(base) = background.base { + extended.background.base = base.into(); + } + if let Some(weak) = background.weak { + extended.background.weak = weak.into(); + } + if let Some(strong) = background.strong { + extended.background.strong = strong.into(); + } + } + + // Handle primary + if let Some(primary) = extended_palette.primary { + if let Some(base) = primary.base { + extended.primary.base = base.into(); + } + if let Some(weak) = primary.weak { + extended.primary.weak = weak.into(); + } + if let Some(strong) = primary.strong { + extended.primary.strong = strong.into(); + } + } + + // Handle secondary + if let Some(secondary) = extended_palette.secondary { + if let Some(base) = secondary.base { + extended.secondary.base = base.into(); + } + if let Some(weak) = secondary.weak { + extended.secondary.weak = weak.into(); + } + if let Some(strong) = secondary.strong { + extended.secondary.strong = strong.into(); + } + } + + // Handle success + if let Some(success) = extended_palette.success { + if let Some(base) = success.base { + extended.success.base = base.into(); + } + if let Some(weak) = success.weak { + extended.success.weak = weak.into(); + } + if let Some(strong) = success.strong { + extended.success.strong = strong.into(); + } + } + + // Handle danger + if let Some(danger) = extended_palette.danger { + if let Some(base) = danger.base { + extended.danger.base = base.into(); + } + if let Some(weak) = danger.weak { + extended.danger.weak = weak.into(); + } + if let Some(strong) = danger.strong { + extended.danger.strong = strong.into(); + } + } + } + + extended + } +} + +#[derive(Debug, Default, serde::Deserialize)] +struct ExtendedThemePalette { + background: Option, + primary: Option, + secondary: Option, + success: Option, + danger: Option, +} + +#[derive(Debug, Default, serde::Deserialize)] +struct ThemeBackground { + base: Option, + weak: Option, + strong: Option, +} + +#[derive(Debug, Default, serde::Deserialize)] +struct ThemePrimary { + base: Option, + weak: Option, + strong: Option, +} + +#[derive(Debug, Default, serde::Deserialize)] +struct ThemeSecondary { + base: Option, + weak: Option, + strong: Option, +} + +#[derive(Debug, Default, serde::Deserialize)] +struct ThemeSuccess { + base: Option, + weak: Option, + strong: Option, +} + +#[derive(Debug, Default, serde::Deserialize)] +struct ThemeDanger { + base: Option, + weak: Option, + strong: Option, +} + +#[derive(Debug, Default, serde::Deserialize)] +struct ThemePair { + #[serde(with = "color_serde")] + color: Color, + #[serde(with = "color_serde")] + text: Color, +} + +impl From for iced::theme::palette::Pair { + fn from(pair: ThemePair) -> Self { + Self { + color: pair.color, + text: pair.text, + } + } +} + mod color_serde { use iced::Color; use serde::{Deserialize, Deserializer}; diff --git a/iced_builder/src/types/rendered_element.rs b/iced_builder/src/types/rendered_element.rs index 5270e5a..e837abe 100755 --- a/iced_builder/src/types/rendered_element.rs +++ b/iced_builder/src/types/rendered_element.rs @@ -11,7 +11,7 @@ use crate::Result; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RenderedElement { - #[serde(skip)] + #[serde(skip, default = "Uuid::new_v4")] id: Uuid, child_elements: Option>, name: ElementName, -- cgit v1.2.3 From 8084412472f455d40fe5a84ee084b0b637ab818f Mon Sep 17 00:00:00 2001 From: pml68 Date: Tue, 31 Dec 2024 18:55:08 +0100 Subject: feat: use monospace font for `TextEditor` --- iced_builder/src/panes/code_view.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'iced_builder/src') diff --git a/iced_builder/src/panes/code_view.rs b/iced_builder/src/panes/code_view.rs index b95b653..f545157 100644 --- a/iced_builder/src/panes/code_view.rs +++ b/iced_builder/src/panes/code_view.rs @@ -1,5 +1,5 @@ use iced::widget::{button, pane_grid, row, text, text_editor, Space}; -use iced::{Alignment, Length, Theme}; +use iced::{Alignment, Font, Length, Theme}; use super::style; use crate::icon::copy; use crate::types::{DesignerPage, Message}; @@ -36,6 +36,7 @@ pub fn view( } else { highlighter::Theme::InspiredGitHub }, + .font(Font::MONOSPACE) ) .height(Length::Fill) .padding(20), -- cgit v1.2.3 From ef680ae14a52e79619d786138cb9ad9ef4902416 Mon Sep 17 00:00:00 2001 From: pml68 Date: Tue, 31 Dec 2024 18:56:01 +0100 Subject: refactor: apply clippy suggestions --- iced_builder/src/dialogs.rs | 6 +----- iced_builder/src/main.rs | 24 +++++++----------------- iced_builder/src/types/rendered_element.rs | 4 ++-- 3 files changed, 10 insertions(+), 24 deletions(-) (limited to 'iced_builder/src') diff --git a/iced_builder/src/dialogs.rs b/iced_builder/src/dialogs.rs index 56d22b2..2d916b1 100644 --- a/iced_builder/src/dialogs.rs +++ b/iced_builder/src/dialogs.rs @@ -26,9 +26,5 @@ pub fn unsaved_changes_dialog(description: impl Into) -> bool { .set_description(description) .show(); - if let MessageDialogResult::Ok = result { - true - } else { - false - } + matches!(result, MessageDialogResult::Ok) } diff --git a/iced_builder/src/main.rs b/iced_builder/src/main.rs index 8f3c3fb..437410a 100644 --- a/iced_builder/src/main.rs +++ b/iced_builder/src/main.rs @@ -65,11 +65,7 @@ impl App { }, ); - let config = match config_load { - Ok(config) => config, - Err(_) => Config::default(), - }; - + let config = config_load.unwrap_or_default(); let theme = config.selected_theme(); let mut task = Task::none(); @@ -83,7 +79,7 @@ impl App { } else { warning_dialog(format!( "The file {} does not exist, or isn't a file.", - path.to_string_lossy().to_string() + path.to_string_lossy() )); } } @@ -156,11 +152,10 @@ impl App { None, None, ) - .into() } Message::HandleNew(name, zones) => { let ids: Vec = zones.into_iter().map(|z| z.0).collect(); - if ids.len() > 0 { + if !ids.is_empty() { let action = Action::new( ids, &mut self.project.element_tree.clone(), @@ -172,7 +167,7 @@ impl App { ); match result { Ok(Some(ref element)) => { - self.project.element_tree = Some(element.clone()) + self.project.element_tree = Some(element.clone()); } Err(error) => error_dialog(error.to_string()), _ => {} @@ -189,11 +184,10 @@ impl App { None, None, ) - .into() } Message::HandleMove(element, zones) => { let ids: Vec = zones.into_iter().map(|z| z.0).collect(); - if ids.len() > 0 { + if !ids.is_empty() { let action = Action::new( ids, &mut self.project.element_tree.clone(), @@ -230,13 +224,11 @@ impl App { self.project = Project::new(); self.project_path = None; self.editor_content = text_editor::Content::new(); - } else { - if unsaved_changes_dialog("You have unsaved changes. Do you wish to discard these and create a new project?") { + } else if unsaved_changes_dialog("You have unsaved changes. Do you wish to discard these and create a new project?") { self.is_dirty = false; self.project = Project::new(); self.project_path = None; self.editor_content = text_editor::Content::new(); - } } } } @@ -249,12 +241,10 @@ impl App { Project::from_file(), Message::FileOpened, ); - } else { - if unsaved_changes_dialog("You have unsaved changes. Do you wish to discard these and open another project?") { + } else if 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_file(), Message::FileOpened); - } } } } diff --git a/iced_builder/src/types/rendered_element.rs b/iced_builder/src/types/rendered_element.rs index e837abe..d7efed1 100755 --- a/iced_builder/src/types/rendered_element.rs +++ b/iced_builder/src/types/rendered_element.rs @@ -349,12 +349,12 @@ impl<'a> From for Element<'a, Message> { .into() } ElementName::Row => widget::Row::from_iter( - child_elements.into_iter().map(|el| el.into()), + child_elements.into_iter().map(Into::into), ) .padding(20) .into(), ElementName::Column => widget::Column::from_iter( - child_elements.into_iter().map(|el| el.into()), + child_elements.into_iter().map(Into::into), ) .padding(20) .into(), -- cgit v1.2.3 From e8d36bd018177e4ccc0ae27ce8f21984d31d948a Mon Sep 17 00:00:00 2001 From: pml68 Date: Sat, 4 Jan 2025 14:25:14 +0100 Subject: feat: add custom theme codegen for `Project` --- Cargo.lock | 19 +++-- iced_builder/Cargo.toml | 7 +- iced_builder/src/config.rs | 10 +-- iced_builder/src/environment.rs | 8 --- iced_builder/src/error.rs | 14 ++-- iced_builder/src/lib.rs | 12 ---- iced_builder/src/main.rs | 47 ++++++++----- iced_builder/src/theme.rs | 107 ++++++++++++++++++++++++++++- iced_builder/src/types.rs | 10 +-- iced_builder/src/types/element_name.rs | 12 ++-- iced_builder/src/types/project.rs | 73 +++++++++++++++----- iced_builder/src/types/rendered_element.rs | 33 ++++----- 12 files changed, 244 insertions(+), 108 deletions(-) delete mode 100644 iced_builder/src/lib.rs (limited to 'iced_builder/src') diff --git a/Cargo.lock b/Cargo.lock index 1577101..dee5504 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1507,6 +1507,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "gdk-pixbuf-sys" version = "0.18.0" @@ -1974,20 +1983,19 @@ dependencies = [ [[package]] name = "iced_anim" -version = "0.1.4" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0436df8d38e8b2bde6f3666b015640934b5d40649108387baaaac14605b38192" +checksum = "386fbcdd1584feb1b11bb8c32b44bc5849663ed39c38a0ac8b191fa78773de44" dependencies = [ "iced", "iced_anim_derive", - "serde", ] [[package]] name = "iced_anim_derive" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b89898a5300d2800451406a3d6cfaed89ef08797dc392143f7c16c5c747e95" +checksum = "c22810793dcb18c4dedbf7d72f391f84877836f727917c3491df8141fe651b85" dependencies = [ "quote", "syn 2.0.93", @@ -1999,6 +2007,7 @@ version = "0.1.0" dependencies = [ "dirs-next", "embed-resource 3.0.1", + "fxhash", "iced", "iced_anim", "iced_drop", diff --git a/iced_builder/Cargo.toml b/iced_builder/Cargo.toml index 638006b..ab453a2 100644 --- a/iced_builder/Cargo.toml +++ b/iced_builder/Cargo.toml @@ -11,7 +11,7 @@ keywords = ["gui", "iced"] [dependencies] iced = { version = "0.13.1", features = [ "image","svg","canvas","qr_code","advanced","tokio","highlighter"] } # iced_aw = { version = "0.11.0", default-features = false, features = ["menu","color_picker"] } -iced_anim = { version = "0.1.4", features = ["derive", "serde"] } +iced_anim = { version = "0.2.0", features = ["derive"] } iced_drop = { path = "../iced_drop" } serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.134" @@ -21,10 +21,13 @@ 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"] } +fxhash = "0.2.1" thiserror = "2.0.9" -xdg = "2.5.2" dirs-next = "2.0.0" +[target.'cfg(macos)'.dependencies] +xdg = "2.5.2" + [build-dependencies] iced_fontello = "0.13.1" diff --git a/iced_builder/src/config.rs b/iced_builder/src/config.rs index 631b35a..9d29af7 100644 --- a/iced_builder/src/config.rs +++ b/iced_builder/src/config.rs @@ -5,9 +5,9 @@ use tokio_stream::wrappers::ReadDirStream; use tokio_stream::StreamExt; use crate::theme::{theme_from_str, theme_index, Appearance, Theme}; -use crate::{environment, Error, Result}; +use crate::{environment, Error}; -#[derive(Debug, Default)] +#[derive(Debug, Clone, Default)] pub struct Config { pub theme: Appearance, pub last_project: Option, @@ -42,7 +42,7 @@ impl Config { Self::config_dir().join(environment::CONFIG_FILE_NAME) } - pub async fn load() -> Result { + pub async fn load() -> Result { use tokio::fs; #[derive(Deserialize)] @@ -72,7 +72,7 @@ impl Config { }) } - pub async fn load_theme(theme_name: String) -> Result { + pub async fn load_theme(theme_name: String) -> Result { use tokio::fs; let read_entry = |entry: fs::DirEntry| async move { @@ -87,7 +87,7 @@ impl Config { 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() { + if theme_index(&theme_name, iced::Theme::ALL).is_some() { selected = theme_from_str(None, &theme_name); } diff --git a/iced_builder/src/environment.rs b/iced_builder/src/environment.rs index 52e7ca5..3ecb790 100644 --- a/iced_builder/src/environment.rs +++ b/iced_builder/src/environment.rs @@ -7,14 +7,6 @@ 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 { let exe = env::current_exe().ok()?; let dir = exe.parent()?; diff --git a/iced_builder/src/error.rs b/iced_builder/src/error.rs index 9cbb6ee..f4011bd 100644 --- a/iced_builder/src/error.rs +++ b/iced_builder/src/error.rs @@ -6,14 +6,14 @@ use thiserror::Error; #[derive(Debug, Clone, Error)] #[error(transparent)] pub enum Error { - IOError(Arc), + IO(Arc), #[error("config does not exist")] ConfigMissing, #[error("JSON parsing error: {0}")] - SerdeJSONError(Arc), + SerdeJSON(Arc), #[error("TOML parsing error: {0}")] - SerdeTOMLError(#[from] toml::de::Error), - FormatError(Arc), + SerdeTOML(#[from] toml::de::Error), + RustFmt(Arc), #[error("the element tree contains no matching element")] NonExistentElement, #[error( @@ -26,19 +26,19 @@ pub enum Error { impl From for Error { fn from(value: io::Error) -> Self { - Self::IOError(Arc::new(value)) + Self::IO(Arc::new(value)) } } impl From for Error { fn from(value: serde_json::Error) -> Self { - Self::SerdeJSONError(Arc::new(value)) + Self::SerdeJSON(Arc::new(value)) } } impl From for Error { fn from(value: rust_format::Error) -> Self { - Self::FormatError(Arc::new(value)) + Self::RustFmt(Arc::new(value)) } } diff --git a/iced_builder/src/lib.rs b/iced_builder/src/lib.rs deleted file mode 100644 index 847e01e..0000000 --- a/iced_builder/src/lib.rs +++ /dev/null @@ -1,12 +0,0 @@ -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; - -pub use error::Error; -pub type Result = core::result::Result; diff --git a/iced_builder/src/main.rs b/iced_builder/src/main.rs index 437410a..b30afaa 100644 --- a/iced_builder/src/main.rs +++ b/iced_builder/src/main.rs @@ -1,20 +1,30 @@ +#![feature(test)] +mod config; +mod dialogs; +mod environment; +mod error; +mod icon; +mod panes; +mod theme; +mod types; +mod widget; + use std::path::PathBuf; +use config::Config; +use dialogs::{error_dialog, unsaved_changes_dialog, warning_dialog}; +use error::Error; use iced::advanced::widget::Id; 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::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 iced_anim::transition::Easing; +use iced_anim::{Animated, Animation}; +use panes::{code_view, designer_view, element_list}; use tokio::runtime; +use types::{Action, DesignerPage, ElementName, Message, Project}; + +//pub(crate) type Result = core::result::Result; fn main() -> Result<(), Box> { let config_load = { @@ -40,7 +50,7 @@ struct App { project_path: Option, project: Project, config: Config, - theme: Spring, + theme: Animated, pane_state: pane_grid::State, focus: Option, designer_page: DesignerPage, @@ -73,7 +83,7 @@ impl App { if let Some(path) = config.last_project.clone() { if path.exists() && path.is_file() { task = Task::perform( - Project::from_path(path), + Project::from_path(path, config.clone()), Message::FileOpened, ); } else { @@ -91,7 +101,7 @@ impl App { project_path: None, project: Project::new(), config, - theme: Spring::new(theme), + theme: Animated::new(theme, Easing::EASE_IN), pane_state: state, focus: None, designer_page: DesignerPage::DesignerView, @@ -137,7 +147,7 @@ impl App { } } Message::RefreshEditorContent => { - match self.project.clone().app_code(&self.config) { + match self.project.app_code(&self.config) { Ok(code) => { self.editor_content = text_editor::Content::with_text(&code); @@ -238,13 +248,13 @@ impl App { self.is_loading = true; return Task::perform( - Project::from_file(), + Project::from_file(self.config.clone()), Message::FileOpened, ); } else if 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_file(), Message::FileOpened); + return Task::perform(Project::from_file(self.config.clone()), Message::FileOpened); } } } @@ -254,10 +264,11 @@ impl App { match result { Ok((path, project)) => { - self.project = project.clone(); + self.project = project; self.project_path = Some(path); self.editor_content = text_editor::Content::with_text( - &project + &self + .project .app_code(&self.config) .unwrap_or_else(|err| err.to_string()), ); diff --git a/iced_builder/src/theme.rs b/iced_builder/src/theme.rs index e41474c..e128162 100644 --- a/iced_builder/src/theme.rs +++ b/iced_builder/src/theme.rs @@ -5,10 +5,10 @@ use iced::Color; use crate::config::Config; -pub fn theme_index(theme_name: String, slice: &[iced::Theme]) -> Option { +pub fn theme_index(theme_name: &str, slice: &[iced::Theme]) -> Option { slice .iter() - .position(|theme| theme.to_string() == theme_name) + .position(|theme| &theme.to_string() == theme_name) } pub fn theme_from_str( @@ -56,7 +56,108 @@ pub fn theme_from_str( } } -#[derive(Debug)] +fn palette_to_string(palette: &iced::theme::Palette) -> String { + format!( + r#"Palette {{ + background: color!(0x{}), + text: color!(0x{}), + primary: color!(0x{}), + success: color!(0x{}), + danger: color!(0x{}), + }}"#, + color_to_hex(palette.background), + color_to_hex(palette.text), + color_to_hex(palette.primary), + color_to_hex(palette.success), + color_to_hex(palette.danger), + ) +} + +fn extended_to_string(extended: &Extended) -> String { + format!( + r#" +Extended{{background:Background{{base:Pair{{color:color!(0x{}),text:color!(0x{}),}},weak:Pair{{color:color!(0x{}),text:color!(0x{}),}},strong:Pair{{color:color!(0x{}),text:color!(0x{}),}},}},primary:Primary{{base:Pair{{color:color!(0x{}),text:color!(0x{}),}},weak:Pair{{color:color!(0x{}),text:color!(0x{}),}},strong:Pair{{color:color!(0x{}),text:color!(0x{}),}},}},secondary:Secondary{{base:Pair{{color:color!(0x{}),text:color!(0x{}),}},weak:Pair{{color:color!(0x{}),text:color!(0x{}),}},strong:Pair{{color:color!(0x{}),text:color!(0x{}),}},}},success:Success{{base:Pair{{color:color!(0x{}),text:color!(0x{}),}},weak:Pair{{color:color!(0x{}),text:color!(0x{}),}},strong:Pair{{color:color!(0x{}),text:color!(0x{}),}},}},danger:Danger{{base:Pair{{color:color!(0x{}),text:color!(0x{}),}},weak:Pair{{color:color!(0x{}),text:color!(0x{}),}},strong:Pair{{color:color!(0x{}),text:color!(0x{}),}},}},is_dark:true,}}"#, + color_to_hex(extended.background.base.color), + color_to_hex(extended.background.base.text), + color_to_hex(extended.background.weak.color), + color_to_hex(extended.background.weak.text), + color_to_hex(extended.background.strong.color), + color_to_hex(extended.background.strong.text), + color_to_hex(extended.primary.base.color), + color_to_hex(extended.primary.base.text), + color_to_hex(extended.primary.weak.color), + color_to_hex(extended.primary.weak.text), + color_to_hex(extended.primary.strong.color), + color_to_hex(extended.primary.strong.text), + color_to_hex(extended.secondary.base.color), + color_to_hex(extended.secondary.base.text), + color_to_hex(extended.secondary.weak.color), + color_to_hex(extended.secondary.weak.text), + color_to_hex(extended.secondary.strong.color), + color_to_hex(extended.secondary.strong.text), + color_to_hex(extended.success.base.color), + color_to_hex(extended.success.base.text), + color_to_hex(extended.success.weak.color), + color_to_hex(extended.success.weak.text), + color_to_hex(extended.success.strong.color), + color_to_hex(extended.success.strong.text), + color_to_hex(extended.danger.base.color), + color_to_hex(extended.danger.base.text), + color_to_hex(extended.danger.weak.color), + color_to_hex(extended.danger.weak.text), + color_to_hex(extended.danger.strong.color), + color_to_hex(extended.danger.strong.text), + ) +} + +pub fn theme_to_string(theme: &iced::Theme) -> String { + let palette = theme.palette(); + let extended = theme.extended_palette(); + + let generated_extended = Extended::generate(palette); + + if &generated_extended == extended { + format!( + r#"custom( + "{}".to_string(), + {} + )"#, + theme, + palette_to_string(&palette) + ) + } else { + format!( + r#"custom_with_fn( + "{}".to_string(), + {}, + |_| {} + )"#, + theme, + palette_to_string(&palette), + extended_to_string(extended) + ) + } +} + +fn color_to_hex(color: Color) -> String { + use std::fmt::Write; + + let mut hex = String::with_capacity(16); + + let [r, g, b, a] = color.into_rgba8(); + + let _ = write!(&mut hex, "{:02X}", r); + let _ = write!(&mut hex, "{:02X}", g); + let _ = write!(&mut hex, "{:02X}", b); + + if a < u8::MAX { + let _ = write!(&mut hex, ", {:.2}", a as f32 / 255.0); + } + + hex +} + +#[derive(Debug, Clone)] pub struct Appearance { pub selected: iced::Theme, pub all: Arc<[iced::Theme]>, diff --git a/iced_builder/src/types.rs b/iced_builder/src/types.rs index 161b5e1..ac9d039 100644 --- a/iced_builder/src/types.rs +++ b/iced_builder/src/types.rs @@ -7,15 +7,15 @@ use std::path::PathBuf; pub use element_name::ElementName; use iced::widget::{pane_grid, text_editor}; use iced::Theme; -use iced_anim::SpringEvent; +use iced_anim::Event; pub use project::Project; pub use rendered_element::*; -use crate::Result; +use crate::Error; #[derive(Debug, Clone)] pub enum Message { - ToggleTheme(SpringEvent), + ToggleTheme(Event), CopyCode, SwitchPage(DesignerPage), EditorAction(text_editor::Action), @@ -35,10 +35,10 @@ pub enum Message { PaneDragged(pane_grid::DragEvent), NewFile, OpenFile, - FileOpened(Result<(PathBuf, Project)>), + FileOpened(Result<(PathBuf, Project), Error>), SaveFile, SaveFileAs, - FileSaved(Result), + FileSaved(Result), } #[derive(Debug, Clone)] diff --git a/iced_builder/src/types/element_name.rs b/iced_builder/src/types/element_name.rs index e172227..0e8aa65 100644 --- a/iced_builder/src/types/element_name.rs +++ b/iced_builder/src/types/element_name.rs @@ -3,13 +3,13 @@ use serde::{Deserialize, Serialize}; use super::rendered_element::{ button, column, container, image, row, svg, text, Action, RenderedElement, }; -use crate::{Error, Result}; +use crate::Error; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum ElementName { Text(String), Button(String), - SVG(String), + Svg(String), Image(String), Container, Row, @@ -20,7 +20,7 @@ impl ElementName { pub const ALL: &'static [Self; 7] = &[ Self::Text(String::new()), Self::Button(String::new()), - Self::SVG(String::new()), + Self::Svg(String::new()), Self::Image(String::new()), Self::Container, Self::Row, @@ -31,11 +31,11 @@ impl ElementName { &self, element_tree: Option<&mut RenderedElement>, action: Action, - ) -> Result> { + ) -> Result, Error> { let element = match self { Self::Text(_) => text(""), Self::Button(_) => button(""), - Self::SVG(_) => svg(""), + Self::Svg(_) => svg(""), Self::Image(_) => image(""), Self::Container => container(None), Self::Row => row(None), @@ -75,7 +75,7 @@ impl std::fmt::Display for ElementName { match self { Self::Text(_) => "Text", Self::Button(_) => "Button", - Self::SVG(_) => "SVG", + Self::Svg(_) => "SVG", Self::Image(_) => "Image", Self::Container => "Container", Self::Row => "Row", diff --git a/iced_builder/src/types/project.rs b/iced_builder/src/types/project.rs index 6f3b7ed..6479b1d 100644 --- a/iced_builder/src/types/project.rs +++ b/iced_builder/src/types/project.rs @@ -1,18 +1,23 @@ use std::path::{Path, PathBuf}; +extern crate fxhash; +use fxhash::FxHashMap; use iced::Theme; -use rust_format::{Config, Edition, Formatter, RustFmt}; +use rust_format::{Edition, Formatter, RustFmt}; use serde::{Deserialize, Serialize}; use super::rendered_element::RenderedElement; -use crate::theme::theme_from_str; -use crate::{Error, Result}; +use crate::config::Config; +use crate::theme::{theme_from_str, theme_index, theme_to_string}; +use crate::Error; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Project { pub title: Option, pub theme: Option, pub element_tree: Option, + #[serde(skip)] + theme_cache: FxHashMap, } impl Default for Project { @@ -27,24 +32,42 @@ impl Project { title: None, theme: None, element_tree: None, + theme_cache: FxHashMap::default(), } } - pub fn get_theme(&self, config: &crate::config::Config) -> Theme { + pub fn get_theme(&self, config: &Config) -> Theme { match &self.theme { Some(theme) => theme_from_str(Some(config), theme), - None => Theme::Dark, + None => Theme::default(), } } - pub async fn from_path(path: PathBuf) -> Result<(PathBuf, Self)> { + fn theme_code(&mut self, theme: &Theme) -> String { + let theme_name = theme.to_string(); + if theme_index(&theme_name, Theme::ALL).is_none() { + self.theme_cache + .entry(theme_name) + .or_insert(theme_to_string(theme)) + .to_string() + } else { + theme_name.replace(" ", "") + } + } + + pub async fn from_path( + path: PathBuf, + config: Config, + ) -> Result<(PathBuf, Self), Error> { let contents = tokio::fs::read_to_string(&path).await?; - let element: Self = serde_json::from_str(&contents)?; + let mut project: Self = serde_json::from_str(&contents)?; + + let _ = project.theme_code(&project.get_theme(&config)); - Ok((path, element)) + Ok((path, project)) } - pub async fn from_file() -> Result<(PathBuf, Self)> { + pub async fn from_file(config: Config) -> Result<(PathBuf, Self), Error> { let picked_file = rfd::AsyncFileDialog::new() .set_title("Open a JSON file...") .add_filter("*.json, *.JSON", &["json", "JSON"]) @@ -54,10 +77,13 @@ impl Project { let path = picked_file.path().to_owned(); - Self::from_path(path).await + Self::from_path(path, config).await } - pub async fn write_to_file(self, path: Option) -> Result { + pub async fn write_to_file( + self, + path: Option, + ) -> Result { let path = if let Some(p) = path { p } else { @@ -78,16 +104,25 @@ impl Project { Ok(path) } - pub fn app_code(&self, config: &crate::config::Config) -> Result { + pub fn app_code(&mut self, config: &Config) -> Result { match self.element_tree { Some(ref element_tree) => { let (imports, view) = element_tree.codegen(); - let mut app_code = - format!("use iced::{{widget::{{{imports}}},Element}};"); - - app_code = format!( + let theme = self.get_theme(config); + let theme_code = self.theme_code(&theme); + let mut theme_imports = ""; + if theme_index(&theme.to_string(), Theme::ALL).is_none() { + if theme_code.contains("Extended") { + theme_imports = "use iced::{{color,theme::{{Palette,palette::{{Extended,Background,Primary,Secondary,Success,Danger,Pair}}}}}};\n"; + } else { + theme_imports = "use iced::{{color,theme::Palette}};\n" + } + } + + let app_code = format!( r#"// Automatically generated by iced Builder - {app_code} + use iced::{{widget::{{{imports}}},Element}}; + {theme_imports} fn main() -> iced::Result {{ iced::application("{}", State::update, State::view).theme(State::theme).run() @@ -114,9 +149,9 @@ impl Project { Some(ref t) => t, None => "New app", }, - self.get_theme(config).to_string().replace(" ", "") + theme_code ); - let config = Config::new_str() + let config = rust_format::Config::new_str() .edition(Edition::Rust2021) .option("trailing_comma", "Never") .option("imports_granularity", "Crate"); diff --git a/iced_builder/src/types/rendered_element.rs b/iced_builder/src/types/rendered_element.rs index d7efed1..177b43d 100755 --- a/iced_builder/src/types/rendered_element.rs +++ b/iced_builder/src/types/rendered_element.rs @@ -7,7 +7,7 @@ use uuid::Uuid; use super::ElementName; use crate::types::Message; -use crate::Result; +use crate::Error; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RenderedElement { @@ -62,7 +62,7 @@ impl RenderedElement { child_element: &RenderedElement, ) -> Option<&mut Self> { if child_element == self { - Some(self) + return Some(self); } else if self.child_elements.is_some() { if self .child_elements @@ -71,20 +71,17 @@ impl RenderedElement { .contains(child_element) { return Some(self); - } else { - if let Some(child_elements) = self.child_elements.as_mut() { - for element in child_elements { - let element = element.find_parent(child_element); - if element.is_some() { - return element; - } + } + if let Some(child_elements) = self.child_elements.as_mut() { + for element in child_elements { + let element = element.find_parent(child_element); + if element.is_some() { + return element; } } - return None; } - } else { - return None; } + return None; } pub fn is_parent(&self) -> bool { @@ -128,7 +125,7 @@ impl RenderedElement { &self, element_tree: Option<&mut RenderedElement>, action: Action, - ) -> Result<()> { + ) -> Result<(), Error> { let element_tree = element_tree.unwrap(); match action { @@ -175,12 +172,12 @@ impl RenderedElement { self } - pub fn as_element<'a>(self) -> Element<'a, Message> { + pub fn into_element<'a>(self) -> Element<'a, Message> { let mut children = widget::column![]; if let Some(els) = self.child_elements.clone() { for el in els { - children = children.push(el.clone().as_element()); + children = children.push(el.clone().into_element()); } } iced_drop::droppable( @@ -263,7 +260,7 @@ impl RenderedElement { imports = format!("{imports}image,"); view = format!("{view}\nimage(\"{path}\"){options}"); } - ElementName::SVG(path) => { + ElementName::Svg(path) => { imports = format!("{imports}svg,"); view = format!("{view}\nsvg(\"{path}\"){options}"); } @@ -337,7 +334,7 @@ impl<'a> From for Element<'a, Message> { widget::button(widget::text(s)).into() } } - ElementName::SVG(p) => widget::svg(p).into(), + ElementName::Svg(p) => widget::svg(p).into(), ElementName::Image(p) => widget::image(p).into(), ElementName::Container => { widget::container(if child_elements.len() == 1 { @@ -457,7 +454,7 @@ pub fn button(text: &str) -> RenderedElement { } pub fn svg(path: &str) -> RenderedElement { - RenderedElement::new(ElementName::SVG(path.to_owned())) + RenderedElement::new(ElementName::Svg(path.to_owned())) } pub fn image(path: &str) -> RenderedElement { -- cgit v1.2.3 From eaaaf2ac7182de1be043b223337e4d8a56e458e1 Mon Sep 17 00:00:00 2001 From: pml68 Date: Sat, 4 Jan 2025 22:45:52 +0100 Subject: refactor: apply clippy suggestions --- iced_builder/src/main.rs | 16 +++--- iced_builder/src/theme.rs | 6 +- iced_builder/src/types/element_name.rs | 7 +-- iced_builder/src/types/project.rs | 9 +-- iced_builder/src/types/rendered_element.rs | 88 +++++++++++++----------------- 5 files changed, 56 insertions(+), 70 deletions(-) (limited to 'iced_builder/src') diff --git a/iced_builder/src/main.rs b/iced_builder/src/main.rs index b30afaa..5b95b94 100644 --- a/iced_builder/src/main.rs +++ b/iced_builder/src/main.rs @@ -113,7 +113,7 @@ impl App { } fn title(&self) -> String { - let saved_state = if !self.is_dirty { "" } else { " *" }; + let saved_state = if self.is_dirty { " *" } else { "" }; let project_name = match &self.project.title { Some(n) => { @@ -126,7 +126,7 @@ impl App { } ) } - None => "".to_owned(), + None => String::new(), }; format!("iced Builder{project_name}{saved_state}") @@ -166,11 +166,8 @@ impl App { Message::HandleNew(name, zones) => { let ids: Vec = zones.into_iter().map(|z| z.0).collect(); if !ids.is_empty() { - let action = Action::new( - ids, - &mut self.project.element_tree.clone(), - None, - ); + let eltree_clone = self.project.element_tree.clone(); + let action = Action::new(&ids, &eltree_clone, None); let result = name.handle_action( self.project.element_tree.as_mut(), action, @@ -198,9 +195,10 @@ impl App { Message::HandleMove(element, zones) => { let ids: Vec = zones.into_iter().map(|z| z.0).collect(); if !ids.is_empty() { + let eltree_clone = self.project.element_tree.clone(); let action = Action::new( - ids, - &mut self.project.element_tree.clone(), + &ids, + &eltree_clone, Some(element.get_id()), ); let result = element.handle_action( diff --git a/iced_builder/src/theme.rs b/iced_builder/src/theme.rs index e128162..7d18aa9 100644 --- a/iced_builder/src/theme.rs +++ b/iced_builder/src/theme.rs @@ -8,7 +8,7 @@ use crate::config::Config; pub fn theme_index(theme_name: &str, slice: &[iced::Theme]) -> Option { slice .iter() - .position(|theme| &theme.to_string() == theme_name) + .position(|theme| theme.to_string() == theme_name) } pub fn theme_from_str( @@ -43,7 +43,7 @@ pub fn theme_from_str( 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) + theme_index(theme_name, &config.theme.all) { config.theme.all[index].clone() } else { @@ -142,7 +142,7 @@ pub fn theme_to_string(theme: &iced::Theme) -> String { fn color_to_hex(color: Color) -> String { use std::fmt::Write; - let mut hex = String::with_capacity(16); + let mut hex = String::with_capacity(12); let [r, g, b, a] = color.into_rgba8(); diff --git a/iced_builder/src/types/element_name.rs b/iced_builder/src/types/element_name.rs index 0e8aa65..2687673 100644 --- a/iced_builder/src/types/element_name.rs +++ b/iced_builder/src/types/element_name.rs @@ -42,12 +42,11 @@ impl ElementName { Self::Column => column(None), }; match action { - Action::Stop => Ok(None), - Action::Drop => Ok(None), + Action::Stop | Action::Drop => Ok(None), Action::AddNew => Ok(Some(element)), Action::PushFront(id) => { element_tree - .ok_or("The action was of kind `PushFront`, but no element tree was provided.")? + .ok_or("the action was of kind `PushFront`, but no element tree was provided.")? .find_by_id(id) .ok_or(Error::NonExistentElement)? .push_front(&element); @@ -56,7 +55,7 @@ impl ElementName { Action::InsertAfter(parent_id, child_id) => { element_tree .ok_or( - "The action was of kind `InsertAfter`, but no element tree was provided.", + "the action was of kind `InsertAfter`, but no element tree was provided.", )? .find_by_id(parent_id) .ok_or(Error::NonExistentElement)? diff --git a/iced_builder/src/types/project.rs b/iced_builder/src/types/project.rs index 6479b1d..27c576b 100644 --- a/iced_builder/src/types/project.rs +++ b/iced_builder/src/types/project.rs @@ -46,10 +46,11 @@ impl Project { fn theme_code(&mut self, theme: &Theme) -> String { let theme_name = theme.to_string(); if theme_index(&theme_name, Theme::ALL).is_none() { - self.theme_cache + (*self + .theme_cache .entry(theme_name) - .or_insert(theme_to_string(theme)) - .to_string() + .or_insert(theme_to_string(theme))) + .to_string() } else { theme_name.replace(" ", "") } @@ -115,7 +116,7 @@ impl Project { if theme_code.contains("Extended") { theme_imports = "use iced::{{color,theme::{{Palette,palette::{{Extended,Background,Primary,Secondary,Success,Danger,Pair}}}}}};\n"; } else { - theme_imports = "use iced::{{color,theme::Palette}};\n" + theme_imports = "use iced::{{color,theme::Palette}};\n"; } } diff --git a/iced_builder/src/types/rendered_element.rs b/iced_builder/src/types/rendered_element.rs index 177b43d..b001556 100755 --- a/iced_builder/src/types/rendered_element.rs +++ b/iced_builder/src/types/rendered_element.rs @@ -41,12 +41,12 @@ impl RenderedElement { Id::new(self.id.to_string()) } - pub fn find_by_id(&mut self, id: Id) -> Option<&mut Self> { - if self.get_id() == id.clone() { + pub fn find_by_id(&mut self, id: &Id) -> Option<&mut Self> { + if &self.get_id() == id { 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()); + let element = element.find_by_id(id); if element.is_some() { return element; } @@ -81,7 +81,7 @@ impl RenderedElement { } } } - return None; + None } pub fn is_parent(&self) -> bool { @@ -109,10 +109,10 @@ impl RenderedElement { } } - pub fn insert_after(&mut self, id: Id, element: &RenderedElement) { + pub fn insert_after(&mut self, id: &Id, element: &RenderedElement) { if let Some(child_elements) = self.child_elements.as_mut() { if let Some(index) = - child_elements.iter().position(|x| x.get_id() == id) + child_elements.iter().position(|x| &x.get_id() == id) { child_elements.insert(index + 1, element.clone()); } else { @@ -217,7 +217,7 @@ impl RenderedElement { for element in els { let (c_imports, children) = element.codegen(); imports = format!("{imports}{c_imports}"); - elements = format!("{elements}{},", children); + elements = format!("{elements}{children},"); } } @@ -345,13 +345,13 @@ impl<'a> From for Element<'a, Message> { .padding(20) .into() } - ElementName::Row => widget::Row::from_iter( - child_elements.into_iter().map(Into::into), + ElementName::Row => widget::Row::from_vec( + child_elements.into_iter().map(Into::into).collect(), ) .padding(20) .into(), - ElementName::Column => widget::Column::from_iter( - child_elements.into_iter().map(Into::into), + ElementName::Column => widget::Column::from_vec( + child_elements.into_iter().map(Into::into).collect(), ) .padding(20) .into(), @@ -367,18 +367,18 @@ impl<'a> From for Element<'a, Message> { } #[derive(Debug, Clone)] -pub enum Action { +pub enum Action<'a> { AddNew, - PushFront(Id), - InsertAfter(Id, Id), + PushFront(&'a Id), + InsertAfter(&'a Id, &'a Id), Drop, Stop, } -impl Action { +impl<'a> Action<'a> { pub fn new( - ids: Vec, - element_tree: &mut Option, + ids: &'a [Id], + element_tree: &'a Option, source_id: Option, ) -> Self { let mut action = Self::Stop; @@ -389,51 +389,39 @@ impl Action { action = Self::Drop; } } else { - let id: Id = match source_id { + let id: &Id = match source_id { Some(id) if ids.contains(&id) => { let element_id = - ids[ids.iter().position(|x| *x == id).unwrap()].clone(); - if ids.len() > 2 && ids[ids.clone().len() - 1] == element_id - { + &ids[ids.iter().position(|x| *x == id).unwrap()]; + if ids.len() > 2 && &ids[ids.len() - 1] == element_id { return Self::Stop; } element_id } - _ => ids.last().cloned().unwrap(), + _ => ids.last().unwrap(), }; - let element = element_tree - .as_mut() - .unwrap() - .find_by_id(id.clone()) - .unwrap(); + let mut element_tree = element_tree.clone().unwrap(); + let element = element_tree.find_by_id(id).unwrap(); // Element is a parent and isn't a non-empty container - match (element.is_empty() - || !(element.name == ElementName::Container)) + if (element.is_empty() || !(element.name == ElementName::Container)) && element.is_parent() { - true => { - action = Self::PushFront(id); - } - false if ids.len() > 2 => { - let parent = element_tree - .as_mut() - .unwrap() - .find_by_id(ids[&ids.len() - 2].clone()) - .unwrap(); - - if parent.name == ElementName::Container - && parent.child_elements != Some(vec![]) - { - action = Self::Stop; - } else { - action = Self::InsertAfter( - ids[&ids.len() - 2].clone(), - ids[&ids.len() - 1].clone(), - ); - } + action = Self::PushFront(id); + } else if ids.len() > 2 { + let parent = + element_tree.find_by_id(&ids[ids.len() - 2]).unwrap(); + + if parent.name == ElementName::Container + && parent.child_elements != Some(vec![]) + { + action = Self::Stop; + } else { + action = Self::InsertAfter( + &ids[ids.len() - 2], + &ids[ids.len() - 1], + ); } - _ => {} } } action -- cgit v1.2.3 From 61926598ce96bee00aafe5340af4a905759b122a Mon Sep 17 00:00:00 2001 From: pml68 Date: Sat, 11 Jan 2025 01:50:16 +0100 Subject: refactor: remove iced_drop & workspace --- Cargo.lock | 286 ++++--------- Cargo.toml | 79 +++- assets/config.toml | 1 + assets/themes/Rose Pine.toml | 33 ++ assets/windows/iced_builder.manifest | 8 + assets/windows/iced_builder.rc | 3 + build.rs | 12 + fonts/icons.toml | 6 + fonts/icons.ttf | Bin 0 -> 6348 bytes iced_builder/Cargo.toml | 64 --- iced_builder/assets/config.toml | 1 - iced_builder/assets/themes/Rose Pine.toml | 33 -- iced_builder/assets/windows/iced_builder.manifest | 8 - iced_builder/assets/windows/iced_builder.rc | 3 - iced_builder/build.rs | 12 - iced_builder/fonts/icons.toml | 6 - iced_builder/fonts/icons.ttf | Bin 6348 -> 0 bytes iced_builder/rustfmt.toml | 4 - iced_builder/src/config.rs | 121 ------ iced_builder/src/dialogs.rs | 30 -- iced_builder/src/environment.rs | 43 -- iced_builder/src/error.rs | 55 --- iced_builder/src/icon.rs | 23 - iced_builder/src/main.rs | 382 ----------------- iced_builder/src/panes.rs | 4 - iced_builder/src/panes/code_view.rs | 50 --- iced_builder/src/panes/designer_view.rs | 37 -- iced_builder/src/panes/element_list.rs | 49 --- iced_builder/src/panes/style.rs | 40 -- iced_builder/src/theme.rs | 381 ----------------- iced_builder/src/types.rs | 48 --- iced_builder/src/types/element_name.rs | 85 ---- iced_builder/src/types/project.rs | 165 ------- iced_builder/src/types/rendered_element.rs | 468 -------------------- iced_builder/src/widget.rs | 21 - iced_drop/.gitignore | 3 - iced_drop/Cargo.toml | 8 - iced_drop/LICENSE | 21 - iced_drop/README.md | 73 ---- iced_drop/src/lib.rs | 53 --- iced_drop/src/widget.rs | 2 - iced_drop/src/widget/droppable.rs | 499 ---------------------- iced_drop/src/widget/operation.rs | 1 - iced_drop/src/widget/operation/drop.rs | 90 ---- rustfmt.toml | 4 + src/config.rs | 121 ++++++ src/dialogs.rs | 30 ++ src/environment.rs | 43 ++ src/error.rs | 55 +++ src/icon.rs | 23 + src/main.rs | 382 +++++++++++++++++ src/panes.rs | 4 + src/panes/code_view.rs | 50 +++ src/panes/designer_view.rs | 37 ++ src/panes/element_list.rs | 49 +++ src/panes/style.rs | 40 ++ src/theme.rs | 381 +++++++++++++++++ src/types.rs | 48 +++ src/types/element_name.rs | 85 ++++ src/types/project.rs | 165 +++++++ src/types/rendered_element.rs | 468 ++++++++++++++++++++ src/widget.rs | 21 + 62 files changed, 2218 insertions(+), 3099 deletions(-) create mode 100644 assets/config.toml create mode 100644 assets/themes/Rose Pine.toml create mode 100644 assets/windows/iced_builder.manifest create mode 100644 assets/windows/iced_builder.rc create mode 100644 build.rs create mode 100644 fonts/icons.toml create mode 100644 fonts/icons.ttf delete mode 100644 iced_builder/Cargo.toml delete mode 100644 iced_builder/assets/config.toml delete mode 100644 iced_builder/assets/themes/Rose Pine.toml delete mode 100644 iced_builder/assets/windows/iced_builder.manifest delete mode 100644 iced_builder/assets/windows/iced_builder.rc delete mode 100644 iced_builder/build.rs delete mode 100644 iced_builder/fonts/icons.toml delete mode 100644 iced_builder/fonts/icons.ttf delete mode 100644 iced_builder/rustfmt.toml delete mode 100644 iced_builder/src/config.rs delete mode 100644 iced_builder/src/dialogs.rs delete mode 100644 iced_builder/src/environment.rs delete mode 100644 iced_builder/src/error.rs delete mode 100644 iced_builder/src/icon.rs delete mode 100644 iced_builder/src/main.rs delete mode 100644 iced_builder/src/panes.rs delete mode 100644 iced_builder/src/panes/code_view.rs delete mode 100644 iced_builder/src/panes/designer_view.rs delete mode 100644 iced_builder/src/panes/element_list.rs delete mode 100644 iced_builder/src/panes/style.rs delete mode 100644 iced_builder/src/theme.rs delete mode 100644 iced_builder/src/types.rs delete mode 100644 iced_builder/src/types/element_name.rs delete mode 100644 iced_builder/src/types/project.rs delete mode 100755 iced_builder/src/types/rendered_element.rs delete mode 100644 iced_builder/src/widget.rs delete mode 100644 iced_drop/.gitignore delete mode 100644 iced_drop/Cargo.toml delete mode 100644 iced_drop/LICENSE delete mode 100644 iced_drop/README.md delete mode 100644 iced_drop/src/lib.rs delete mode 100644 iced_drop/src/widget.rs delete mode 100644 iced_drop/src/widget/droppable.rs delete mode 100644 iced_drop/src/widget/operation.rs delete mode 100644 iced_drop/src/widget/operation/drop.rs create mode 100644 rustfmt.toml create mode 100644 src/config.rs create mode 100644 src/dialogs.rs create mode 100644 src/environment.rs create mode 100644 src/error.rs create mode 100644 src/icon.rs create mode 100644 src/main.rs create mode 100644 src/panes.rs create mode 100644 src/panes/code_view.rs create mode 100644 src/panes/designer_view.rs create mode 100644 src/panes/element_list.rs create mode 100644 src/panes/style.rs create mode 100644 src/theme.rs create mode 100644 src/types.rs create mode 100644 src/types/element_name.rs create mode 100644 src/types/project.rs create mode 100755 src/types/rendered_element.rs create mode 100644 src/widget.rs (limited to 'iced_builder/src') diff --git a/Cargo.lock b/Cargo.lock index dee5504..a638ff4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ab_glyph" @@ -293,7 +293,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", ] [[package]] @@ -322,13 +322,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.83" +version = "0.1.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", ] [[package]] @@ -484,7 +484,7 @@ checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", ] [[package]] @@ -558,9 +558,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.6" +version = "1.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d6dbb628b8f8555f86d0323c2eb39e3ec81901f4b83e091db8a6a76d316a333" +checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7" dependencies = [ "jobserver", "libc", @@ -967,7 +967,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", ] [[package]] @@ -1042,7 +1042,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", ] [[package]] @@ -1178,7 +1178,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", ] [[package]] @@ -1295,12 +1295,6 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" -[[package]] -name = "float_next_after" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" - [[package]] name = "fnv" version = "1.0.7" @@ -1380,7 +1374,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", ] [[package]] @@ -1474,7 +1468,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", ] [[package]] @@ -1973,7 +1967,6 @@ checksum = "88acfabc84ec077eaf9ede3457ffa3a104626d79022a9bf7f296093b1d60c73f" dependencies = [ "iced_core", "iced_futures", - "iced_highlighter", "iced_renderer", "iced_widget", "iced_winit", @@ -1998,7 +1991,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c22810793dcb18c4dedbf7d72f391f84877836f727917c3491df8141fe651b85" dependencies = [ "quote", - "syn 2.0.93", + "syn 2.0.95", ] [[package]] @@ -2048,6 +2041,7 @@ dependencies = [ [[package]] name = "iced_drop" version = "0.1.0" +source = "git+https://github.com/jhannyj/iced_drop?rev=d259ec4dff098852d995d3bcaa5551a88330636f#d259ec4dff098852d995d3bcaa5551a88330636f" dependencies = [ "iced", ] @@ -2109,7 +2103,6 @@ dependencies = [ "image", "kamadak-exif", "log", - "lyon_path", "once_cell", "raw-window-handle", "rustc-hash 2.1.0", @@ -2117,17 +2110,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "iced_highlighter" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad88b25a1328cd4bb0b72d8e20f8207c0433649dc788f67e911423b9406f45c" -dependencies = [ - "iced_core", - "once_cell", - "syntect", -] - [[package]] name = "iced_renderer" version = "0.13.0" @@ -2185,7 +2167,6 @@ dependencies = [ "iced_glyphon", "iced_graphics", "log", - "lyon", "once_cell", "resvg", "rustc-hash 2.1.0", @@ -2199,12 +2180,10 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81429e1b950b0e4bca65be4c4278fea6678ea782030a411778f26fa9f8983e1d" dependencies = [ - "iced_highlighter", "iced_renderer", "iced_runtime", "num-traits", "once_cell", - "qrcode", "rustc-hash 2.1.0", "thiserror 1.0.69", "unicode-segmentation", @@ -2345,7 +2324,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", ] [[package]] @@ -2578,12 +2557,6 @@ dependencies = [ "redox_syscall 0.5.8", ] -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -2630,58 +2603,6 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -[[package]] -name = "lyon" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7f9cda98b5430809e63ca5197b06c7d191bf7e26dfc467d5a3f0290e2a74f" -dependencies = [ - "lyon_algorithms", - "lyon_tessellation", -] - -[[package]] -name = "lyon_algorithms" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f13c9be19d257c7d37e70608ed858e8eab4b2afcea2e3c9a622e892acbf43c08" -dependencies = [ - "lyon_path", - "num-traits", -] - -[[package]] -name = "lyon_geom" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8af69edc087272df438b3ee436c4bb6d7c04aa8af665cfd398feae627dbd8570" -dependencies = [ - "arrayvec", - "euclid", - "num-traits", -] - -[[package]] -name = "lyon_path" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e0b8aec2f58586f6eef237985b9a9b7cb3a3aff4417c575075cf95bf925252e" -dependencies = [ - "lyon_geom", - "num-traits", -] - -[[package]] -name = "lyon_tessellation" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579d42360a4b09846eff2feef28f538696c7d6c7439bfa65874ff3cbe0951b2c" -dependencies = [ - "float_next_after", - "lyon_path", - "num-traits", -] - [[package]] name = "lzma-rs" version = "0.3.0" @@ -2885,7 +2806,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -2916,7 +2836,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", ] [[package]] @@ -3201,7 +3121,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", ] [[package]] @@ -3281,7 +3201,7 @@ dependencies = [ "by_address", "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", ] [[package]] @@ -3374,9 +3294,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "phf" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ "phf_macros", "phf_shared", @@ -3384,9 +3304,9 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", "rand", @@ -3394,24 +3314,24 @@ dependencies = [ [[package]] name = "phf_macros" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ "phf_generator", "phf_shared", "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", ] [[package]] name = "phf_shared" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ - "siphasher 0.3.11", + "siphasher", ] [[package]] @@ -3422,29 +3342,29 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "pin-project" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" +checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" +checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", ] [[package]] name = "pin-project-lite" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -3469,19 +3389,6 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" -[[package]] -name = "plist" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" -dependencies = [ - "base64", - "indexmap", - "quick-xml 0.32.0", - "serde", - "time", -] - [[package]] name = "png" version = "0.17.16" @@ -3564,21 +3471,6 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "qrcode" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "166f136dfdb199f98186f3649cf7a0536534a61417a1a30221b492b4fb60ce3f" - -[[package]] -name = "quick-xml" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" -dependencies = [ - "memchr", -] - [[package]] name = "quick-xml" version = "0.36.2" @@ -3727,9 +3619,9 @@ checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" [[package]] name = "reqwest" -version = "0.12.11" +version = "0.12.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe060fe50f524be480214aba758c71f99f90ee8c83c5a36b5e9e1d568eb4eb3" +checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" dependencies = [ "base64", "bytes", @@ -3789,12 +3681,14 @@ dependencies = [ [[package]] name = "rfd" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f6f80a9b882647d9014673ca9925d30ffc9750f2eed2b4490e189eaebd01e8" +checksum = "6a24763657bff09769a8ccf12c8b8a50416fb035fe199263b4c5071e4e3f006f" dependencies = [ "ashpd", "block2", + "core-foundation 0.10.0", + "core-foundation-sys", "glib-sys", "gobject-sys", "gtk-sys", @@ -3807,7 +3701,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -4053,7 +3947,7 @@ checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", ] [[package]] @@ -4076,7 +3970,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", ] [[package]] @@ -4145,19 +4039,13 @@ checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "simplecss" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" dependencies = [ "log", ] -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - [[package]] name = "siphasher" version = "1.0.1" @@ -4340,7 +4228,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "794de53cc48eaabeed0ab6a3404a65f40b3e38c067e4435883a65d2aa4ca000e" dependencies = [ "kurbo 0.11.1", - "siphasher 1.0.1", + "siphasher", ] [[package]] @@ -4367,9 +4255,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.93" +version = "2.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058" +checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" dependencies = [ "proc-macro2", "quote", @@ -4393,7 +4281,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", ] [[package]] @@ -4408,14 +4296,12 @@ dependencies = [ "fnv", "once_cell", "onig", - "plist", "regex-syntax", "serde", "serde_derive", "serde_json", "thiserror 1.0.69", "walkdir", - "yaml-rust", ] [[package]] @@ -4469,12 +4355,13 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" -version = "3.14.0" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", "fastrand", + "getrandom", "once_cell", "rustix", "windows-sys 0.59.0", @@ -4515,7 +4402,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", ] [[package]] @@ -4526,7 +4413,7 @@ checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", ] [[package]] @@ -4547,12 +4434,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", "serde", "time-core", - "time-macros", ] [[package]] @@ -4561,16 +4446,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" -[[package]] -name = "time-macros" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" -dependencies = [ - "num-conv", - "time-core", -] - [[package]] name = "tiny-skia" version = "0.11.4" @@ -4774,7 +4649,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", ] [[package]] @@ -4934,7 +4809,7 @@ dependencies = [ "roxmltree", "rustybuzz", "simplecss", - "siphasher 1.0.1", + "siphasher", "strict-num", "svgtypes", "tiny-skia-path", @@ -5050,7 +4925,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", "wasm-bindgen-shared", ] @@ -5085,7 +4960,7 @@ checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5204,7 +5079,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3" dependencies = [ "proc-macro2", - "quick-xml 0.36.2", + "quick-xml", "quote", ] @@ -5678,9 +5553,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winit" -version = "0.30.7" +version = "0.30.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dba50bc8ef4b6f1a75c9274fb95aa9a8f63fbc66c56f391bd85cf68d51e7b1a3" +checksum = "f5d74280aabb958072864bff6cfbcf9025cf8bfacdde5e32b5e12920ef703b0f" dependencies = [ "ahash 0.8.11", "android-activity", @@ -5730,9 +5605,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.20" +version = "0.6.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980" dependencies = [ "memchr", ] @@ -5853,15 +5728,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] - [[package]] name = "yazi" version = "0.1.6" @@ -5888,7 +5754,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", "synstructure", ] @@ -5975,7 +5841,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", "zvariant_utils 2.1.0", ] @@ -5988,7 +5854,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", "zbus_names 4.1.0", "zvariant 5.1.0", "zvariant_utils 3.0.2", @@ -6041,7 +5907,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", ] [[package]] @@ -6061,7 +5927,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", "synstructure", ] @@ -6082,7 +5948,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", ] [[package]] @@ -6104,7 +5970,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", ] [[package]] @@ -6225,7 +6091,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", "zvariant_utils 2.1.0", ] @@ -6238,7 +6104,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", "zvariant_utils 3.0.2", ] @@ -6250,7 +6116,7 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.95", ] [[package]] @@ -6263,6 +6129,6 @@ dependencies = [ "quote", "serde", "static_assertions", - "syn 2.0.93", + "syn 2.0.95", "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index 64f9037..1f97ff3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,73 @@ -[workspace] -resolver = "2" -members = [ - "iced_drop", - "iced_builder" -] +[package] +name = "iced_builder" +description = "GUI builder for iced, built with iced." +version = "0.1.0" +edition = "2021" +authors = ["pml68 "] +repository = "https://github.com/pml68/iced-builder" +license = "GPL-3.0-or-later" +keywords = ["gui", "iced"] + +[dependencies] +iced = { version = "0.13.1", features = [ "image","svg","advanced","tokio"] } +# iced_aw = { version = "0.11.0", default-features = false, features = ["menu","color_picker"] } +iced_anim = { version = "0.2.0", features = ["derive"] } +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"] } +fxhash = "0.2.1" +thiserror = "2.0.9" +dirs-next = "2.0.0" + +[dependencies.iced_drop] +git = "https://github.com/jhannyj/iced_drop" +rev = "d259ec4dff098852d995d3bcaa5551a88330636f" + +[target.'cfg(target_os = "macos")'.dependencies] +xdg = "2.5.2" + +[build-dependencies] +iced_fontello = "0.13.1" + +[target.'cfg(windows)'.build-dependencies] +embed-resource = "3.0.1" +windows_exe_info = "0.4" + +[profile.dev] +opt-level = 1 + +[profile.dev.package."*"] +opt-level = 3 + +[[bin]] +name = "iced-builder" +path = "src/main.rs" + +[lints.rust] +missing_debug_implementations = "deny" +# missing_docs = "deny" +unsafe_code = "deny" +unused_results = "deny" + +[lints.clippy] +type-complexity = "allow" +semicolon_if_nothing_returned = "deny" +trivially-copy-pass-by-ref = "deny" +default_trait_access = "deny" +match-wildcard-for-single-variants = "deny" +redundant-closure-for-method-calls = "deny" +filter_map_next = "deny" +manual_let_else = "deny" +unused_async = "deny" +from_over_into = "deny" +needless_borrow = "deny" +new_without_default = "deny" +useless_conversion = "deny" + +[lints.rustdoc] +broken_intra_doc_links = "forbid" diff --git a/assets/config.toml b/assets/config.toml new file mode 100644 index 0000000..8ef1bb3 --- /dev/null +++ b/assets/config.toml @@ -0,0 +1 @@ +theme = "Rose Pine" diff --git a/assets/themes/Rose Pine.toml b/assets/themes/Rose Pine.toml new file mode 100644 index 0000000..df7342b --- /dev/null +++ b/assets/themes/Rose Pine.toml @@ -0,0 +1,33 @@ +is_dark = true + +[palette] +background = "#26233a" +text = "#e0def4" +primary = "#9ccfd8" +success = "#f6c177" +danger = "#eb6f92" + +[background] +base = { color = "#191724", text = "#e0def4" } +weak = { color = "#1f1d2e", text = "#e0def4" } +strong = { color = "#26233a", text = "#f4ebd3" } + +[primary] +base = { color = "#eb6f92", text = "#000000" } +weak = { color = "#f6c177", text = "#000000" } +strong = { color = "#ebbcba", text = "#000000" } + +[secondary] +base = { color = "#31748f", text = "#ffffff" } +weak = { color = "#9ccfd8", text = "#000000" } +strong = { color = "#c4a7e7", text = "#000000" } + +[success] +base = { color = "#9ccfd8", text = "#000000" } +weak = { color = "#6e6a86", text = "#ffffff" } +strong = { color = "#908caa", text = "#000000" } + +[danger] +base = { color = "#eb6f92", text = "#ffffff" } +weak = { color = "#f6c177", text = "#ffffff" } +strong = { color = "#524f67", text = "#ffffff" } diff --git a/assets/windows/iced_builder.manifest b/assets/windows/iced_builder.manifest new file mode 100644 index 0000000..82039bf --- /dev/null +++ b/assets/windows/iced_builder.manifest @@ -0,0 +1,8 @@ + + + + + PerMonitorV2, unaware + + + diff --git a/assets/windows/iced_builder.rc b/assets/windows/iced_builder.rc new file mode 100644 index 0000000..7255b65 --- /dev/null +++ b/assets/windows/iced_builder.rc @@ -0,0 +1,3 @@ +#define RT_MANIFEST 24 + +1 RT_MANIFEST "iced_builder.manifest" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..438ce37 --- /dev/null +++ b/build.rs @@ -0,0 +1,12 @@ +fn main() { + println!("cargo::rerun-if-changed=fonts/icons.toml"); + iced_fontello::build("fonts/icons.toml").expect("Build icons font"); + #[cfg(windows)] + { + embed_resource::compile( + "assets/windows/iced_builder.rc", + embed_resource::NONE, + ); + windows_exe_info::versioninfo::link_cargo_env(); + } +} diff --git a/fonts/icons.toml b/fonts/icons.toml new file mode 100644 index 0000000..a70c0e7 --- /dev/null +++ b/fonts/icons.toml @@ -0,0 +1,6 @@ +module = "icon" + +[glyphs] +save = "entypo-floppy" +open = "fontawesome-folder-open-empty" +copy = "fontawesome-file-code" diff --git a/fonts/icons.ttf b/fonts/icons.ttf new file mode 100644 index 0000000..7af6b0e Binary files /dev/null and b/fonts/icons.ttf differ diff --git a/iced_builder/Cargo.toml b/iced_builder/Cargo.toml deleted file mode 100644 index ab453a2..0000000 --- a/iced_builder/Cargo.toml +++ /dev/null @@ -1,64 +0,0 @@ -[package] -name = "iced_builder" -description = "GUI builder for iced, built with iced." -version = "0.1.0" -edition = "2021" -authors = ["pml68 "] -repository = "https://github.com/pml68/iced-builder" -license = "GPL-3.0-or-later" -keywords = ["gui", "iced"] - -[dependencies] -iced = { version = "0.13.1", features = [ "image","svg","canvas","qr_code","advanced","tokio","highlighter"] } -# iced_aw = { version = "0.11.0", default-features = false, features = ["menu","color_picker"] } -iced_anim = { version = "0.2.0", features = ["derive"] } -iced_drop = { path = "../iced_drop" } -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"] } -fxhash = "0.2.1" -thiserror = "2.0.9" -dirs-next = "2.0.0" - -[target.'cfg(macos)'.dependencies] -xdg = "2.5.2" - -[build-dependencies] -iced_fontello = "0.13.1" - -[target.'cfg(windows)'.build-dependencies] -embed-resource = "3.0.1" -windows_exe_info = "0.4" - -[[bin]] -name = "iced-builder" -path = "src/main.rs" - -[lints.rust] -missing_debug_implementations = "deny" -# missing_docs = "deny" -unsafe_code = "deny" -unused_results = "deny" - -[lints.clippy] -type-complexity = "allow" -semicolon_if_nothing_returned = "deny" -trivially-copy-pass-by-ref = "deny" -default_trait_access = "deny" -match-wildcard-for-single-variants = "deny" -redundant-closure-for-method-calls = "deny" -filter_map_next = "deny" -manual_let_else = "deny" -unused_async = "deny" -from_over_into = "deny" -needless_borrow = "deny" -new_without_default = "deny" -useless_conversion = "deny" - -[lints.rustdoc] -broken_intra_doc_links = "forbid" diff --git a/iced_builder/assets/config.toml b/iced_builder/assets/config.toml deleted file mode 100644 index 8ef1bb3..0000000 --- a/iced_builder/assets/config.toml +++ /dev/null @@ -1 +0,0 @@ -theme = "Rose Pine" diff --git a/iced_builder/assets/themes/Rose Pine.toml b/iced_builder/assets/themes/Rose Pine.toml deleted file mode 100644 index df7342b..0000000 --- a/iced_builder/assets/themes/Rose Pine.toml +++ /dev/null @@ -1,33 +0,0 @@ -is_dark = true - -[palette] -background = "#26233a" -text = "#e0def4" -primary = "#9ccfd8" -success = "#f6c177" -danger = "#eb6f92" - -[background] -base = { color = "#191724", text = "#e0def4" } -weak = { color = "#1f1d2e", text = "#e0def4" } -strong = { color = "#26233a", text = "#f4ebd3" } - -[primary] -base = { color = "#eb6f92", text = "#000000" } -weak = { color = "#f6c177", text = "#000000" } -strong = { color = "#ebbcba", text = "#000000" } - -[secondary] -base = { color = "#31748f", text = "#ffffff" } -weak = { color = "#9ccfd8", text = "#000000" } -strong = { color = "#c4a7e7", text = "#000000" } - -[success] -base = { color = "#9ccfd8", text = "#000000" } -weak = { color = "#6e6a86", text = "#ffffff" } -strong = { color = "#908caa", text = "#000000" } - -[danger] -base = { color = "#eb6f92", text = "#ffffff" } -weak = { color = "#f6c177", text = "#ffffff" } -strong = { color = "#524f67", text = "#ffffff" } diff --git a/iced_builder/assets/windows/iced_builder.manifest b/iced_builder/assets/windows/iced_builder.manifest deleted file mode 100644 index 82039bf..0000000 --- a/iced_builder/assets/windows/iced_builder.manifest +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PerMonitorV2, unaware - - - diff --git a/iced_builder/assets/windows/iced_builder.rc b/iced_builder/assets/windows/iced_builder.rc deleted file mode 100644 index 7255b65..0000000 --- a/iced_builder/assets/windows/iced_builder.rc +++ /dev/null @@ -1,3 +0,0 @@ -#define RT_MANIFEST 24 - -1 RT_MANIFEST "iced_builder.manifest" diff --git a/iced_builder/build.rs b/iced_builder/build.rs deleted file mode 100644 index 438ce37..0000000 --- a/iced_builder/build.rs +++ /dev/null @@ -1,12 +0,0 @@ -fn main() { - println!("cargo::rerun-if-changed=fonts/icons.toml"); - iced_fontello::build("fonts/icons.toml").expect("Build icons font"); - #[cfg(windows)] - { - embed_resource::compile( - "assets/windows/iced_builder.rc", - embed_resource::NONE, - ); - windows_exe_info::versioninfo::link_cargo_env(); - } -} diff --git a/iced_builder/fonts/icons.toml b/iced_builder/fonts/icons.toml deleted file mode 100644 index a70c0e7..0000000 --- a/iced_builder/fonts/icons.toml +++ /dev/null @@ -1,6 +0,0 @@ -module = "icon" - -[glyphs] -save = "entypo-floppy" -open = "fontawesome-folder-open-empty" -copy = "fontawesome-file-code" diff --git a/iced_builder/fonts/icons.ttf b/iced_builder/fonts/icons.ttf deleted file mode 100644 index 7af6b0e..0000000 Binary files a/iced_builder/fonts/icons.ttf and /dev/null differ diff --git a/iced_builder/rustfmt.toml b/iced_builder/rustfmt.toml deleted file mode 100644 index 197262a..0000000 --- a/iced_builder/rustfmt.toml +++ /dev/null @@ -1,4 +0,0 @@ -edition = "2021" -imports_granularity = "Module" -group_imports = "StdExternalCrate" -max_width = 80 diff --git a/iced_builder/src/config.rs b/iced_builder/src/config.rs deleted file mode 100644 index 9d29af7..0000000 --- a/iced_builder/src/config.rs +++ /dev/null @@ -1,121 +0,0 @@ -use std::path::PathBuf; - -use serde::Deserialize; -use tokio_stream::wrappers::ReadDirStream; -use tokio_stream::StreamExt; - -use crate::theme::{theme_from_str, theme_index, Appearance, Theme}; -use crate::{environment, Error}; - -#[derive(Debug, Clone, Default)] -pub struct Config { - pub theme: Appearance, - pub last_project: Option, -} - -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"); - } - 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"); - } - dir - } - - pub fn config_file_path() -> PathBuf { - Self::config_dir().join(environment::CONFIG_FILE_NAME) - } - - pub async fn load() -> Result { - use tokio::fs; - - #[derive(Deserialize)] - pub struct Configuration { - #[serde(default)] - pub theme: String, - pub last_project: Option, - } - - 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 { - use tokio::fs; - - let read_entry = |entry: fs::DirEntry| async move { - let content = fs::read_to_string(entry.path()).await.ok()?; - - let theme: Theme = toml::from_str(content.as_ref()).ok()?; - let name = entry.path().file_stem()?.to_string_lossy().to_string(); - - Some(theme.into_iced_theme(name)) - }; - - let mut all = iced::Theme::ALL.to_owned(); - let mut selected = iced::Theme::default(); - - if theme_index(&theme_name, 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(Appearance { - selected, - all: all.into(), - }) - } -} diff --git a/iced_builder/src/dialogs.rs b/iced_builder/src/dialogs.rs deleted file mode 100644 index 2d916b1..0000000 --- a/iced_builder/src/dialogs.rs +++ /dev/null @@ -1,30 +0,0 @@ -use rfd::{MessageButtons, MessageDialog, MessageDialogResult, MessageLevel}; - -pub fn error_dialog(description: impl Into) { - let _ = MessageDialog::new() - .set_level(MessageLevel::Error) - .set_buttons(MessageButtons::Ok) - .set_title("Oops! Something went wrong.") - .set_description(description) - .show(); -} - -pub fn warning_dialog(description: impl Into) { - 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) -> bool { - let result = MessageDialog::new() - .set_level(MessageLevel::Warning) - .set_buttons(MessageButtons::OkCancel) - .set_title("Unsaved changes") - .set_description(description) - .show(); - - matches!(result, MessageDialogResult::Ok) -} diff --git a/iced_builder/src/environment.rs b/iced_builder/src/environment.rs deleted file mode 100644 index 3ecb790..0000000 --- a/iced_builder/src/environment.rs +++ /dev/null @@ -1,43 +0,0 @@ -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) -} - -fn portable_dir() -> Option { - 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 { - 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 deleted file mode 100644 index f4011bd..0000000 --- a/iced_builder/src/error.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::io; -use std::sync::Arc; - -use thiserror::Error; - -#[derive(Debug, Clone, Error)] -#[error(transparent)] -pub enum Error { - IO(Arc), - #[error("config does not exist")] - ConfigMissing, - #[error("JSON parsing error: {0}")] - SerdeJSON(Arc), - #[error("TOML parsing error: {0}")] - SerdeTOML(#[from] toml::de::Error), - RustFmt(Arc), - #[error("the element tree contains no matching element")] - NonExistentElement, - #[error( - "the file dialog has been closed without selecting a valid option" - )] - DialogClosed, - #[error("{0}")] - Other(String), -} - -impl From for Error { - fn from(value: io::Error) -> Self { - Self::IO(Arc::new(value)) - } -} - -impl From for Error { - fn from(value: serde_json::Error) -> Self { - Self::SerdeJSON(Arc::new(value)) - } -} - -impl From for Error { - fn from(value: rust_format::Error) -> Self { - Self::RustFmt(Arc::new(value)) - } -} - -impl From<&str> for Error { - fn from(value: &str) -> Self { - Self::Other(value.to_owned()) - } -} - -impl From for Error { - fn from(value: String) -> Self { - Self::Other(value) - } -} diff --git a/iced_builder/src/icon.rs b/iced_builder/src/icon.rs deleted file mode 100644 index f6760d5..0000000 --- a/iced_builder/src/icon.rs +++ /dev/null @@ -1,23 +0,0 @@ -// Generated automatically by iced_fontello at build time. -// Do not edit manually. Source: ../fonts/icons.toml -// 02c7558d187cdc056fdd0e6a638ef805fa10f5955f834575e51d75acd35bc70e -use iced::widget::{text, Text}; -use iced::Font; - -pub const FONT: &[u8] = include_bytes!("../fonts/icons.ttf"); - -pub fn copy<'a>() -> Text<'a> { - icon("\u{F1C9}") -} - -pub fn open<'a>() -> Text<'a> { - icon("\u{F115}") -} - -pub fn save<'a>() -> Text<'a> { - icon("\u{1F4BE}") -} - -fn icon<'a>(codepoint: &'a str) -> Text<'a> { - text(codepoint).font(Font::with_name("icons")) -} diff --git a/iced_builder/src/main.rs b/iced_builder/src/main.rs deleted file mode 100644 index 5b95b94..0000000 --- a/iced_builder/src/main.rs +++ /dev/null @@ -1,382 +0,0 @@ -#![feature(test)] -mod config; -mod dialogs; -mod environment; -mod error; -mod icon; -mod panes; -mod theme; -mod types; -mod widget; - -use std::path::PathBuf; - -use config::Config; -use dialogs::{error_dialog, unsaved_changes_dialog, warning_dialog}; -use error::Error; -use iced::advanced::widget::Id; -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::transition::Easing; -use iced_anim::{Animated, Animation}; -use panes::{code_view, designer_view, element_list}; -use tokio::runtime; -use types::{Action, DesignerPage, ElementName, Message, Project}; - -//pub(crate) type Result = core::result::Result; - -fn main() -> Result<(), Box> { - let config_load = { - let rt = runtime::Builder::new_current_thread() - .enable_all() - .build()?; - - rt.block_on(Config::load()) - }; - - iced::application(App::title, App::update, App::view) - .font(icon::FONT) - .theme(|state| state.theme.value().clone()) - .subscription(App::subscription) - .run_with(move || App::new(config_load))?; - - Ok(()) -} - -struct App { - is_dirty: bool, - is_loading: bool, - project_path: Option, - project: Project, - config: Config, - theme: Animated, - pane_state: pane_grid::State, - focus: Option, - designer_page: DesignerPage, - element_list: &'static [ElementName], - editor_content: text_editor::Content, -} - -#[derive(Clone, Copy, Debug)] -enum Panes { - Designer, - ElementList, -} - -impl App { - fn new(config_load: Result) -> (Self, Task) { - let state = pane_grid::State::with_configuration( - pane_grid::Configuration::Split { - axis: pane_grid::Axis::Vertical, - ratio: 0.8, - a: Box::new(pane_grid::Configuration::Pane(Panes::Designer)), - b: Box::new(pane_grid::Configuration::Pane(Panes::ElementList)), - }, - ); - - let config = config_load.unwrap_or_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, config.clone()), - Message::FileOpened, - ); - } else { - warning_dialog(format!( - "The file {} does not exist, or isn't a file.", - path.to_string_lossy() - )); - } - } - - ( - Self { - is_dirty: false, - is_loading: false, - project_path: None, - project: Project::new(), - config, - theme: Animated::new(theme, Easing::EASE_IN), - pane_state: state, - focus: None, - designer_page: DesignerPage::DesignerView, - element_list: ElementName::ALL, - editor_content: text_editor::Content::new(), - }, - task, - ) - } - - fn title(&self) -> String { - let saved_state = if self.is_dirty { " *" } else { "" }; - - let project_name = match &self.project.title { - Some(n) => { - format!( - " - {}", - if n.len() > 60 { - format!("...{}", &n[n.len() - 40..]) - } else { - n.to_owned() - } - ) - } - None => String::new(), - }; - - format!("iced Builder{project_name}{saved_state}") - } - - fn update(&mut self, message: Message) -> Task { - match message { - Message::ToggleTheme(event) => { - self.theme.update(event); - } - Message::CopyCode => { - return clipboard::write(self.editor_content.text()) - } - Message::SwitchPage(page) => self.designer_page = page, - Message::EditorAction(action) => { - if let text_editor::Action::Scroll { lines: _ } = action { - self.editor_content.perform(action); - } - } - Message::RefreshEditorContent => { - match self.project.app_code(&self.config) { - Ok(code) => { - self.editor_content = - text_editor::Content::with_text(&code); - } - Err(error) => error_dialog(error.to_string()), - } - } - Message::DropNewElement(name, point, _) => { - return iced_drop::zones_on_point( - move |zones| Message::HandleNew(name.clone(), zones), - point, - None, - None, - ) - } - Message::HandleNew(name, zones) => { - let ids: Vec = zones.into_iter().map(|z| z.0).collect(); - if !ids.is_empty() { - let eltree_clone = self.project.element_tree.clone(); - let action = Action::new(&ids, &eltree_clone, None); - let result = name.handle_action( - self.project.element_tree.as_mut(), - action, - ); - match result { - Ok(Some(ref element)) => { - self.project.element_tree = Some(element.clone()); - } - Err(error) => error_dialog(error.to_string()), - _ => {} - } - - self.is_dirty = true; - return Task::done(Message::RefreshEditorContent); - } - } - Message::MoveElement(element, point, _) => { - return iced_drop::zones_on_point( - move |zones| Message::HandleMove(element.clone(), zones), - point, - None, - None, - ) - } - Message::HandleMove(element, zones) => { - let ids: Vec = zones.into_iter().map(|z| z.0).collect(); - if !ids.is_empty() { - let eltree_clone = self.project.element_tree.clone(); - let action = Action::new( - &ids, - &eltree_clone, - Some(element.get_id()), - ); - let result = element.handle_action( - self.project.element_tree.as_mut(), - action, - ); - if let Err(error) = result { - error_dialog(error.to_string()); - } - - self.is_dirty = true; - return Task::done(Message::RefreshEditorContent); - } - } - Message::PaneResized(pane_grid::ResizeEvent { split, ratio }) => { - self.pane_state.resize(split, ratio); - } - Message::PaneClicked(pane) => { - self.focus = Some(pane); - } - Message::PaneDragged(pane_grid::DragEvent::Dropped { - pane, - target, - }) => { - self.pane_state.drop(pane, target); - } - Message::PaneDragged(_) => {} - Message::NewFile => { - if !self.is_loading { - if !self.is_dirty { - self.project = Project::new(); - self.project_path = None; - self.editor_content = text_editor::Content::new(); - } else if unsaved_changes_dialog("You have unsaved changes. Do you wish to discard these and create a new project?") { - self.is_dirty = false; - self.project = Project::new(); - self.project_path = None; - self.editor_content = text_editor::Content::new(); - } - } - } - Message::OpenFile => { - if !self.is_loading { - if !self.is_dirty { - self.is_loading = true; - - return Task::perform( - Project::from_file(self.config.clone()), - Message::FileOpened, - ); - } else if 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_file(self.config.clone()), Message::FileOpened); - } - } - } - Message::FileOpened(result) => { - self.is_loading = false; - self.is_dirty = false; - - match result { - Ok((path, project)) => { - self.project = project; - self.project_path = Some(path); - self.editor_content = text_editor::Content::with_text( - &self - .project - .app_code(&self.config) - .unwrap_or_else(|err| err.to_string()), - ); - } - Err(error) => error_dialog(error.to_string()), - } - } - Message::SaveFile => { - if !self.is_loading { - self.is_loading = true; - - return Task::perform( - self.project - .clone() - .write_to_file(self.project_path.clone()), - Message::FileSaved, - ); - } - } - Message::SaveFileAs => { - if !self.is_loading { - self.is_loading = true; - - return Task::perform( - self.project.clone().write_to_file(None), - Message::FileSaved, - ); - } - } - Message::FileSaved(result) => { - self.is_loading = false; - - match result { - Ok(path) => { - self.project_path = Some(path); - self.is_dirty = false; - } - Err(error) => error_dialog(error.to_string()), - } - } - } - - Task::none() - } - - fn subscription(&self) -> iced::Subscription { - keyboard::on_key_press(|key, modifiers| { - if modifiers.command() { - match key.as_ref() { - keyboard::Key::Character("o") => Some(Message::OpenFile), - keyboard::Key::Character("s") => { - Some(if modifiers.shift() { - Message::SaveFileAs - } else { - Message::SaveFile - }) - } - keyboard::Key::Character("n") => Some(Message::NewFile), - _ => None, - } - } else { - None - } - }) - } - - fn view(&self) -> Element<'_, Message> { - let header = row![pick_list( - self.config.theme.all.clone(), - Some(self.theme.target().clone()), - |theme| { Message::ToggleTheme(theme.into()) } - )] - .width(200); - let pane_grid = - PaneGrid::new(&self.pane_state, |id, pane, _is_maximized| { - let is_focused = Some(id) == self.focus; - match pane { - Panes::Designer => match &self.designer_page { - DesignerPage::DesignerView => designer_view::view( - &self.project.element_tree, - self.project.get_theme(&self.config), - is_focused, - ), - DesignerPage::CodeView => code_view::view( - &self.editor_content, - self.theme.target().clone(), - is_focused, - ), - }, - Panes::ElementList => { - element_list::view(self.element_list, is_focused) - } - } - }) - .width(Length::Fill) - .height(Length::Fill) - .spacing(10) - .on_resize(10, Message::PaneResized) - .on_click(Message::PaneClicked) - .on_drag(Message::PaneDragged); - - let content = Column::new() - .push(header) - .push(pane_grid) - .spacing(5) - .align_x(Alignment::Center) - .width(Length::Fill); - - Animation::new(&self.theme, container(content).height(Length::Fill)) - .on_update(Message::ToggleTheme) - .into() - } -} diff --git a/iced_builder/src/panes.rs b/iced_builder/src/panes.rs deleted file mode 100644 index 387662a..0000000 --- a/iced_builder/src/panes.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod code_view; -pub mod designer_view; -pub mod element_list; -mod style; diff --git a/iced_builder/src/panes/code_view.rs b/iced_builder/src/panes/code_view.rs deleted file mode 100644 index f545157..0000000 --- a/iced_builder/src/panes/code_view.rs +++ /dev/null @@ -1,50 +0,0 @@ -use iced::widget::{button, pane_grid, row, text, text_editor, Space}; -use iced::{Alignment, Font, Length, Theme}; -use super::style; -use crate::icon::copy; -use crate::types::{DesignerPage, Message}; -use crate::widget::tip; - -pub fn view( - editor_content: &text_editor::Content, - theme: Theme, - is_focused: bool, -) -> pane_grid::Content<'_, Message> { - let title = row![ - text("Generated Code"), - Space::with_width(Length::Fill), - tip( - button(copy()).on_press(Message::CopyCode), - "Copy code to clipboard", - tip::Position::FollowCursor - ), - Space::with_width(20), - button("Switch to Designer view") - .on_press(Message::SwitchPage(DesignerPage::DesignerView)) - ] - .align_y(Alignment::Center); - let title_bar = pane_grid::TitleBar::new(title) - .padding(10) - .style(style::title_bar); - pane_grid::Content::new( - text_editor(editor_content) - .on_action(Message::EditorAction) - .highlight( - "rs", - if theme.to_string().contains("Dark") { - highlighter::Theme::SolarizedDark - } else { - highlighter::Theme::InspiredGitHub - }, - .font(Font::MONOSPACE) - ) - .height(Length::Fill) - .padding(20), - ) - .title_bar(title_bar) - .style(if is_focused { - style::pane_focused - } else { - style::pane_active - }) -} diff --git a/iced_builder/src/panes/designer_view.rs b/iced_builder/src/panes/designer_view.rs deleted file mode 100644 index 76456db..0000000 --- a/iced_builder/src/panes/designer_view.rs +++ /dev/null @@ -1,37 +0,0 @@ -use iced::widget::{button, container, pane_grid, row, text, themer, Space}; -use iced::{Alignment, Element, Length}; - -use super::style; -use crate::types::{DesignerPage, Message, RenderedElement}; - -pub fn view<'a>( - element_tree: &Option, - designer_theme: iced::Theme, - is_focused: bool, -) -> pane_grid::Content<'a, Message> { - let el_tree: Element<'a, Message> = match element_tree { - Some(tree) => tree.clone().into(), - None => text("Open a project or begin creating one").into(), - }; - let content = container(themer(designer_theme, el_tree)) - .id(iced::widget::container::Id::new("drop_zone")) - .height(Length::Fill) - .width(Length::Fill); - let title = row![ - text("Designer"), - Space::with_width(Length::Fill), - button("Switch to Code view") - .on_press(Message::SwitchPage(DesignerPage::CodeView)), - ] - .align_y(Alignment::Center); - let title_bar = pane_grid::TitleBar::new(title) - .padding(10) - .style(style::title_bar); - pane_grid::Content::new(content) - .title_bar(title_bar) - .style(if is_focused { - style::pane_focused - } else { - style::pane_active - }) -} diff --git a/iced_builder/src/panes/element_list.rs b/iced_builder/src/panes/element_list.rs deleted file mode 100644 index 8a1c6eb..0000000 --- a/iced_builder/src/panes/element_list.rs +++ /dev/null @@ -1,49 +0,0 @@ -use iced::widget::{column, container, pane_grid, text, Column}; -use iced::{Alignment, Element, Length}; -use iced_drop::droppable; - -use super::style; -use crate::types::{ElementName, Message}; - -fn items_list_view(items: &[ElementName]) -> Element<'_, Message> { - let mut column = Column::new() - .spacing(20) - .align_x(Alignment::Center) - .width(Length::Fill); - - for item in items { - column = - column.push(droppable(text(item.clone().to_string())).on_drop( - move |point, rect| { - Message::DropNewElement(item.clone(), point, rect) - }, - )); - } - - container(column) - .width(Length::Fill) - .height(Length::Fill) - .into() -} - -pub fn view( - element_list: &[ElementName], - is_focused: bool, -) -> pane_grid::Content<'_, Message> { - let items_list = items_list_view(element_list); - let content = column![items_list] - .align_x(Alignment::Center) - .height(Length::Fill) - .width(Length::Fill); - let title = text("Element List"); - let title_bar = pane_grid::TitleBar::new(title) - .padding(10) - .style(style::title_bar); - pane_grid::Content::new(content) - .title_bar(title_bar) - .style(if is_focused { - style::pane_focused - } else { - style::pane_active - }) -} diff --git a/iced_builder/src/panes/style.rs b/iced_builder/src/panes/style.rs deleted file mode 100644 index 1eefb2d..0000000 --- a/iced_builder/src/panes/style.rs +++ /dev/null @@ -1,40 +0,0 @@ -use iced::widget::container::Style; -use iced::{Border, Theme}; - -pub fn title_bar(theme: &Theme) -> Style { - let palette = theme.extended_palette(); - - Style { - text_color: Some(palette.background.strong.text), - background: Some(palette.background.strong.color.into()), - ..Default::default() - } -} - -pub fn pane_active(theme: &Theme) -> Style { - let palette = theme.extended_palette(); - - Style { - background: Some(palette.background.weak.color.into()), - border: Border { - width: 1.0, - color: palette.background.strong.color, - ..Border::default() - }, - ..Default::default() - } -} - -pub fn pane_focused(theme: &Theme) -> Style { - let palette = theme.extended_palette(); - - Style { - background: Some(palette.background.weak.color.into()), - border: Border { - width: 4.0, - color: palette.background.strong.color, - ..Border::default() - }, - ..Default::default() - } -} diff --git a/iced_builder/src/theme.rs b/iced_builder/src/theme.rs deleted file mode 100644 index 7d18aa9..0000000 --- a/iced_builder/src/theme.rs +++ /dev/null @@ -1,381 +0,0 @@ -use std::sync::Arc; - -use iced::theme::palette::Extended; -use iced::Color; - -use crate::config::Config; - -pub fn theme_index(theme_name: &str, slice: &[iced::Theme]) -> Option { - 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, &config.theme.all) - { - config.theme.all[index].clone() - } else { - iced::Theme::default() - } - } else { - iced::Theme::default() - } - } - } -} - -fn palette_to_string(palette: &iced::theme::Palette) -> String { - format!( - r#"Palette {{ - background: color!(0x{}), - text: color!(0x{}), - primary: color!(0x{}), - success: color!(0x{}), - danger: color!(0x{}), - }}"#, - color_to_hex(palette.background), - color_to_hex(palette.text), - color_to_hex(palette.primary), - color_to_hex(palette.success), - color_to_hex(palette.danger), - ) -} - -fn extended_to_string(extended: &Extended) -> String { - format!( - r#" -Extended{{background:Background{{base:Pair{{color:color!(0x{}),text:color!(0x{}),}},weak:Pair{{color:color!(0x{}),text:color!(0x{}),}},strong:Pair{{color:color!(0x{}),text:color!(0x{}),}},}},primary:Primary{{base:Pair{{color:color!(0x{}),text:color!(0x{}),}},weak:Pair{{color:color!(0x{}),text:color!(0x{}),}},strong:Pair{{color:color!(0x{}),text:color!(0x{}),}},}},secondary:Secondary{{base:Pair{{color:color!(0x{}),text:color!(0x{}),}},weak:Pair{{color:color!(0x{}),text:color!(0x{}),}},strong:Pair{{color:color!(0x{}),text:color!(0x{}),}},}},success:Success{{base:Pair{{color:color!(0x{}),text:color!(0x{}),}},weak:Pair{{color:color!(0x{}),text:color!(0x{}),}},strong:Pair{{color:color!(0x{}),text:color!(0x{}),}},}},danger:Danger{{base:Pair{{color:color!(0x{}),text:color!(0x{}),}},weak:Pair{{color:color!(0x{}),text:color!(0x{}),}},strong:Pair{{color:color!(0x{}),text:color!(0x{}),}},}},is_dark:true,}}"#, - color_to_hex(extended.background.base.color), - color_to_hex(extended.background.base.text), - color_to_hex(extended.background.weak.color), - color_to_hex(extended.background.weak.text), - color_to_hex(extended.background.strong.color), - color_to_hex(extended.background.strong.text), - color_to_hex(extended.primary.base.color), - color_to_hex(extended.primary.base.text), - color_to_hex(extended.primary.weak.color), - color_to_hex(extended.primary.weak.text), - color_to_hex(extended.primary.strong.color), - color_to_hex(extended.primary.strong.text), - color_to_hex(extended.secondary.base.color), - color_to_hex(extended.secondary.base.text), - color_to_hex(extended.secondary.weak.color), - color_to_hex(extended.secondary.weak.text), - color_to_hex(extended.secondary.strong.color), - color_to_hex(extended.secondary.strong.text), - color_to_hex(extended.success.base.color), - color_to_hex(extended.success.base.text), - color_to_hex(extended.success.weak.color), - color_to_hex(extended.success.weak.text), - color_to_hex(extended.success.strong.color), - color_to_hex(extended.success.strong.text), - color_to_hex(extended.danger.base.color), - color_to_hex(extended.danger.base.text), - color_to_hex(extended.danger.weak.color), - color_to_hex(extended.danger.weak.text), - color_to_hex(extended.danger.strong.color), - color_to_hex(extended.danger.strong.text), - ) -} - -pub fn theme_to_string(theme: &iced::Theme) -> String { - let palette = theme.palette(); - let extended = theme.extended_palette(); - - let generated_extended = Extended::generate(palette); - - if &generated_extended == extended { - format!( - r#"custom( - "{}".to_string(), - {} - )"#, - theme, - palette_to_string(&palette) - ) - } else { - format!( - r#"custom_with_fn( - "{}".to_string(), - {}, - |_| {} - )"#, - theme, - palette_to_string(&palette), - extended_to_string(extended) - ) - } -} - -fn color_to_hex(color: Color) -> String { - use std::fmt::Write; - - let mut hex = String::with_capacity(12); - - let [r, g, b, a] = color.into_rgba8(); - - let _ = write!(&mut hex, "{:02X}", r); - let _ = write!(&mut hex, "{:02X}", g); - let _ = write!(&mut hex, "{:02X}", b); - - if a < u8::MAX { - let _ = write!(&mut hex, ", {:.2}", a as f32 / 255.0); - } - - hex -} - -#[derive(Debug, Clone)] -pub struct Appearance { - pub selected: iced::Theme, - pub all: Arc<[iced::Theme]>, -} - -impl Default for Appearance { - fn default() -> Self { - Self { - selected: iced::Theme::default(), - all: iced::Theme::ALL.into(), - } - } -} - -#[derive(Debug, Default, serde::Deserialize)] -pub struct Theme { - palette: ThemePalette, - is_dark: Option, - #[serde(flatten)] - extended: Option, -} - -#[derive(Debug, Clone, 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 Theme { - pub fn into_iced_theme(self, name: String) -> iced::Theme { - iced::Theme::custom_with_fn(name, self.palette.clone().into(), |_| { - self.into() - }) - } -} - -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 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, - } - } -} - -impl From for Extended { - fn from(theme: Theme) -> Self { - let mut extended = Extended::generate(theme.palette.into()); - - if let Some(is_dark) = theme.is_dark { - extended.is_dark = is_dark; - } - - if let Some(extended_palette) = theme.extended { - if let Some(background) = extended_palette.background { - if let Some(base) = background.base { - extended.background.base = base.into(); - } - if let Some(weak) = background.weak { - extended.background.weak = weak.into(); - } - if let Some(strong) = background.strong { - extended.background.strong = strong.into(); - } - } - - // Handle primary - if let Some(primary) = extended_palette.primary { - if let Some(base) = primary.base { - extended.primary.base = base.into(); - } - if let Some(weak) = primary.weak { - extended.primary.weak = weak.into(); - } - if let Some(strong) = primary.strong { - extended.primary.strong = strong.into(); - } - } - - // Handle secondary - if let Some(secondary) = extended_palette.secondary { - if let Some(base) = secondary.base { - extended.secondary.base = base.into(); - } - if let Some(weak) = secondary.weak { - extended.secondary.weak = weak.into(); - } - if let Some(strong) = secondary.strong { - extended.secondary.strong = strong.into(); - } - } - - // Handle success - if let Some(success) = extended_palette.success { - if let Some(base) = success.base { - extended.success.base = base.into(); - } - if let Some(weak) = success.weak { - extended.success.weak = weak.into(); - } - if let Some(strong) = success.strong { - extended.success.strong = strong.into(); - } - } - - // Handle danger - if let Some(danger) = extended_palette.danger { - if let Some(base) = danger.base { - extended.danger.base = base.into(); - } - if let Some(weak) = danger.weak { - extended.danger.weak = weak.into(); - } - if let Some(strong) = danger.strong { - extended.danger.strong = strong.into(); - } - } - } - - extended - } -} - -#[derive(Debug, Default, serde::Deserialize)] -struct ExtendedThemePalette { - background: Option, - primary: Option, - secondary: Option, - success: Option, - danger: Option, -} - -#[derive(Debug, Default, serde::Deserialize)] -struct ThemeBackground { - base: Option, - weak: Option, - strong: Option, -} - -#[derive(Debug, Default, serde::Deserialize)] -struct ThemePrimary { - base: Option, - weak: Option, - strong: Option, -} - -#[derive(Debug, Default, serde::Deserialize)] -struct ThemeSecondary { - base: Option, - weak: Option, - strong: Option, -} - -#[derive(Debug, Default, serde::Deserialize)] -struct ThemeSuccess { - base: Option, - weak: Option, - strong: Option, -} - -#[derive(Debug, Default, serde::Deserialize)] -struct ThemeDanger { - base: Option, - weak: Option, - strong: Option, -} - -#[derive(Debug, Default, serde::Deserialize)] -struct ThemePair { - #[serde(with = "color_serde")] - color: Color, - #[serde(with = "color_serde")] - text: Color, -} - -impl From for iced::theme::palette::Pair { - fn from(pair: ThemePair) -> Self { - Self { - color: pair.color, - text: pair.text, - } - } -} - -mod color_serde { - use iced::Color; - use serde::{Deserialize, Deserializer}; - - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - Ok(String::deserialize(deserializer) - .map(|hex| Color::parse(&hex))? - .unwrap_or(Color::TRANSPARENT)) - } -} diff --git a/iced_builder/src/types.rs b/iced_builder/src/types.rs deleted file mode 100644 index ac9d039..0000000 --- a/iced_builder/src/types.rs +++ /dev/null @@ -1,48 +0,0 @@ -pub mod element_name; -pub mod project; -pub mod rendered_element; - -use std::path::PathBuf; - -pub use element_name::ElementName; -use iced::widget::{pane_grid, text_editor}; -use iced::Theme; -use iced_anim::Event; -pub use project::Project; -pub use rendered_element::*; - -use crate::Error; - -#[derive(Debug, Clone)] -pub enum Message { - ToggleTheme(Event), - CopyCode, - SwitchPage(DesignerPage), - EditorAction(text_editor::Action), - RefreshEditorContent, - DropNewElement(ElementName, iced::Point, iced::Rectangle), - HandleNew( - ElementName, - Vec<(iced::advanced::widget::Id, iced::Rectangle)>, - ), - MoveElement(RenderedElement, iced::Point, iced::Rectangle), - HandleMove( - RenderedElement, - Vec<(iced::advanced::widget::Id, iced::Rectangle)>, - ), - PaneResized(pane_grid::ResizeEvent), - PaneClicked(pane_grid::Pane), - PaneDragged(pane_grid::DragEvent), - NewFile, - OpenFile, - FileOpened(Result<(PathBuf, Project), Error>), - SaveFile, - SaveFileAs, - FileSaved(Result), -} - -#[derive(Debug, Clone)] -pub enum DesignerPage { - DesignerView, - CodeView, -} diff --git a/iced_builder/src/types/element_name.rs b/iced_builder/src/types/element_name.rs deleted file mode 100644 index 2687673..0000000 --- a/iced_builder/src/types/element_name.rs +++ /dev/null @@ -1,85 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use super::rendered_element::{ - button, column, container, image, row, svg, text, Action, RenderedElement, -}; -use crate::Error; - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum ElementName { - Text(String), - Button(String), - Svg(String), - Image(String), - Container, - Row, - Column, -} - -impl ElementName { - pub const ALL: &'static [Self; 7] = &[ - Self::Text(String::new()), - Self::Button(String::new()), - Self::Svg(String::new()), - Self::Image(String::new()), - Self::Container, - Self::Row, - Self::Column, - ]; - - pub fn handle_action( - &self, - element_tree: Option<&mut RenderedElement>, - action: Action, - ) -> Result, Error> { - let element = match self { - Self::Text(_) => text(""), - Self::Button(_) => button(""), - Self::Svg(_) => svg(""), - Self::Image(_) => image(""), - Self::Container => container(None), - Self::Row => row(None), - Self::Column => column(None), - }; - match action { - Action::Stop | Action::Drop => Ok(None), - Action::AddNew => Ok(Some(element)), - Action::PushFront(id) => { - element_tree - .ok_or("the action was of kind `PushFront`, but no element tree was provided.")? - .find_by_id(id) - .ok_or(Error::NonExistentElement)? - .push_front(&element); - Ok(None) - } - Action::InsertAfter(parent_id, child_id) => { - element_tree - .ok_or( - "the action was of kind `InsertAfter`, but no element tree was provided.", - )? - .find_by_id(parent_id) - .ok_or(Error::NonExistentElement)? - .insert_after(child_id, &element); - Ok(None) - } - } - } -} - -impl std::fmt::Display for ElementName { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Self::Text(_) => "Text", - Self::Button(_) => "Button", - Self::Svg(_) => "SVG", - Self::Image(_) => "Image", - Self::Container => "Container", - Self::Row => "Row", - Self::Column => "Column", - } - ) - } -} diff --git a/iced_builder/src/types/project.rs b/iced_builder/src/types/project.rs deleted file mode 100644 index 27c576b..0000000 --- a/iced_builder/src/types/project.rs +++ /dev/null @@ -1,165 +0,0 @@ -use std::path::{Path, PathBuf}; - -extern crate fxhash; -use fxhash::FxHashMap; -use iced::Theme; -use rust_format::{Edition, Formatter, RustFmt}; -use serde::{Deserialize, Serialize}; - -use super::rendered_element::RenderedElement; -use crate::config::Config; -use crate::theme::{theme_from_str, theme_index, theme_to_string}; -use crate::Error; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Project { - pub title: Option, - pub theme: Option, - pub element_tree: Option, - #[serde(skip)] - theme_cache: FxHashMap, -} - -impl Default for Project { - fn default() -> Self { - Self::new() - } -} - -impl Project { - pub fn new() -> Self { - Self { - title: None, - theme: None, - element_tree: None, - theme_cache: FxHashMap::default(), - } - } - - pub fn get_theme(&self, config: &Config) -> Theme { - match &self.theme { - Some(theme) => theme_from_str(Some(config), theme), - None => Theme::default(), - } - } - - fn theme_code(&mut self, theme: &Theme) -> String { - let theme_name = theme.to_string(); - if theme_index(&theme_name, Theme::ALL).is_none() { - (*self - .theme_cache - .entry(theme_name) - .or_insert(theme_to_string(theme))) - .to_string() - } else { - theme_name.replace(" ", "") - } - } - - pub async fn from_path( - path: PathBuf, - config: Config, - ) -> Result<(PathBuf, Self), Error> { - let contents = tokio::fs::read_to_string(&path).await?; - let mut project: Self = serde_json::from_str(&contents)?; - - let _ = project.theme_code(&project.get_theme(&config)); - - Ok((path, project)) - } - - pub async fn from_file(config: Config) -> Result<(PathBuf, Self), Error> { - let picked_file = rfd::AsyncFileDialog::new() - .set_title("Open a JSON file...") - .add_filter("*.json, *.JSON", &["json", "JSON"]) - .pick_file() - .await - .ok_or(Error::DialogClosed)?; - - let path = picked_file.path().to_owned(); - - Self::from_path(path, config).await - } - - pub async fn write_to_file( - self, - path: Option, - ) -> Result { - let path = if let Some(p) = path { - p - } else { - rfd::AsyncFileDialog::new() - .set_title("Save to JSON file...") - .add_filter("*.json, *.JSON", &["json", "JSON"]) - .save_file() - .await - .as_ref() - .map(rfd::FileHandle::path) - .map(Path::to_owned) - .ok_or(Error::DialogClosed)? - }; - - let contents = serde_json::to_string(&self)?; - tokio::fs::write(&path, contents).await?; - - Ok(path) - } - - pub fn app_code(&mut self, config: &Config) -> Result { - match self.element_tree { - Some(ref element_tree) => { - let (imports, view) = element_tree.codegen(); - let theme = self.get_theme(config); - let theme_code = self.theme_code(&theme); - let mut theme_imports = ""; - if theme_index(&theme.to_string(), Theme::ALL).is_none() { - if theme_code.contains("Extended") { - theme_imports = "use iced::{{color,theme::{{Palette,palette::{{Extended,Background,Primary,Secondary,Success,Danger,Pair}}}}}};\n"; - } else { - theme_imports = "use iced::{{color,theme::Palette}};\n"; - } - } - - let app_code = format!( - r#"// Automatically generated by iced Builder - use iced::{{widget::{{{imports}}},Element}}; - {theme_imports} - - fn main() -> iced::Result {{ - iced::application("{}", State::update, State::view).theme(State::theme).run() - }} - - #[derive(Default)] - struct State; - - #[derive(Debug, Clone)] - enum Message {{}} - - impl State {{ - fn update(&mut self, _message: Message) {{}} - - fn theme(&self) -> iced::Theme {{ - iced::Theme::{} - }} - - fn view(&self) -> Element {{ - {view}.into() - }} - }}"#, - match self.title { - Some(ref t) => t, - None => "New app", - }, - theme_code - ); - let config = rust_format::Config::new_str() - .edition(Edition::Rust2021) - .option("trailing_comma", "Never") - .option("imports_granularity", "Crate"); - let rustfmt = RustFmt::from_config(config); - Ok(rustfmt.format_str(app_code)?) - } - None => Err("No element tree present".into()), - } - } -} diff --git a/iced_builder/src/types/rendered_element.rs b/iced_builder/src/types/rendered_element.rs deleted file mode 100755 index b001556..0000000 --- a/iced_builder/src/types/rendered_element.rs +++ /dev/null @@ -1,468 +0,0 @@ -use std::collections::BTreeMap; - -use iced::advanced::widget::Id; -use iced::{widget, Element, Length}; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use super::ElementName; -use crate::types::Message; -use crate::Error; - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct RenderedElement { - #[serde(skip, default = "Uuid::new_v4")] - id: Uuid, - child_elements: Option>, - name: ElementName, - options: BTreeMap>, -} - -impl RenderedElement { - fn new(name: ElementName) -> Self { - Self { - id: Uuid::new_v4(), - child_elements: None, - name, - options: BTreeMap::new(), - } - } - - fn with(name: ElementName, child_elements: Vec) -> Self { - Self { - id: Uuid::new_v4(), - child_elements: Some(child_elements), - name, - options: BTreeMap::new(), - } - } - - pub fn get_id(&self) -> Id { - Id::new(self.id.to_string()) - } - - pub fn find_by_id(&mut self, id: &Id) -> Option<&mut Self> { - if &self.get_id() == id { - 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); - if element.is_some() { - return element; - } - } - None - } else { - None - } - } - - pub fn find_parent( - &mut self, - child_element: &RenderedElement, - ) -> Option<&mut Self> { - if child_element == self { - return Some(self); - } else if self.child_elements.is_some() { - if self - .child_elements - .clone() - .unwrap_or_default() - .contains(child_element) - { - return Some(self); - } - if let Some(child_elements) = self.child_elements.as_mut() { - for element in child_elements { - let element = element.find_parent(child_element); - if element.is_some() { - return element; - } - } - } - } - None - } - - pub fn is_parent(&self) -> bool { - self.child_elements.is_some() - } - - pub fn is_empty(&self) -> bool { - self.child_elements == Some(vec![]) - } - - pub fn remove(&mut self, element: &RenderedElement) { - let parent = self.find_parent(element).unwrap(); - if let Some(child_elements) = parent.child_elements.as_mut() { - if let Some(index) = - child_elements.iter().position(|x| x == element) - { - let _ = child_elements.remove(index); - } - } - } - - pub fn push_front(&mut self, element: &RenderedElement) { - if let Some(child_elements) = self.child_elements.as_mut() { - child_elements.insert(0, element.clone()); - } - } - - pub fn insert_after(&mut self, id: &Id, element: &RenderedElement) { - if let Some(child_elements) = self.child_elements.as_mut() { - if let Some(index) = - child_elements.iter().position(|x| &x.get_id() == id) - { - child_elements.insert(index + 1, element.clone()); - } else { - child_elements.push(element.clone()); - } - } - } - - pub fn handle_action( - &self, - element_tree: Option<&mut RenderedElement>, - action: Action, - ) -> Result<(), Error> { - let element_tree = element_tree.unwrap(); - - match action { - Action::Stop => Ok(()), - Action::Drop => { - element_tree.remove(self); - - Ok(()) - } - Action::AddNew => Err( - "the action was of kind `AddNew`, but invoking it on an existing element tree is not possible".into(), - ), - Action::PushFront(id) => { - element_tree.remove(self); - - let new_parent = element_tree.find_by_id(id).unwrap(); - new_parent.push_front(self); - - Ok(()) - } - Action::InsertAfter(parent_id, target_id) => { - element_tree.remove(self); - - let new_parent = element_tree.find_by_id(parent_id).unwrap(); - new_parent.insert_after(target_id, self); - - Ok(()) - } - } - } - - fn preset_options(mut self, options: &[&str]) -> Self { - for opt in options { - let _ = self.options.insert(opt.to_string(), None); - } - self - } - - pub fn option<'a>(mut self, option: &'a str, value: &'a str) -> Self { - let _ = self - .options - .entry(option.to_owned()) - .and_modify(|opt| *opt = Some(value.to_owned())); - self - } - - pub fn into_element<'a>(self) -> Element<'a, Message> { - let mut children = widget::column![]; - - if let Some(els) = self.child_elements.clone() { - for el in els { - children = children.push(el.clone().into_element()); - } - } - iced_drop::droppable( - widget::container( - widget::column![ - widget::text(self.name.clone().to_string()), - children - ] - .width(Length::Fill) - .spacing(10), - ) - .padding(10) - .style(widget::container::bordered_box), - ) - .id(self.get_id()) - .drag_hide(true) - .on_drop(move |point, rect| { - Message::MoveElement(self.clone(), point, rect) - }) - .into() - } - - pub fn codegen(&self) -> (String, String) { - let mut imports = String::new(); - let mut view = String::new(); - let mut options = String::new(); - - for (k, v) in self.options.clone() { - if let Some(v) = v { - options = format!("{options}.{k}({v})"); - } - } - - let mut elements = String::new(); - - if let Some(els) = &self.child_elements { - for element in els { - let (c_imports, children) = element.codegen(); - imports = format!("{imports}{c_imports}"); - elements = format!("{elements}{children},"); - } - } - - match &self.name { - ElementName::Container => { - imports = format!("{imports}container,"); - view = format!("{view}\ncontainer({elements}){options}"); - } - ElementName::Row => { - imports = format!("{imports}row,"); - view = format!("{view}\nrow![{elements}]{options}"); - } - ElementName::Column => { - imports = format!("{imports}column,"); - view = format!("{view}\ncolumn![{elements}]{options}"); - } - ElementName::Text(string) => { - imports = format!("{imports}text,"); - view = format!( - "{view}\ntext(\"{}\"){options}", - if *string == String::new() { - "New Text" - } else { - string - } - ); - } - ElementName::Button(string) => { - imports = format!("{imports}button,"); - view = format!( - "{view}\nbutton(\"{}\"){options}", - if *string == String::new() { - "New Button" - } else { - string - } - ); - } - ElementName::Image(path) => { - imports = format!("{imports}image,"); - view = format!("{view}\nimage(\"{path}\"){options}"); - } - ElementName::Svg(path) => { - imports = format!("{imports}svg,"); - view = format!("{view}\nsvg(\"{path}\"){options}"); - } - } - - (imports, view) - } -} - -impl std::fmt::Display for RenderedElement { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut has_options = false; - f.pad("")?; - f.write_fmt(format_args!("{:?}\n", self.name))?; - f.pad("")?; - f.write_str("Options: (")?; - for (k, v) in &self.options { - if let Some(value) = v { - has_options = true; - f.write_fmt(format_args!( - "\n{:width$.precision$}{}: {}", - "", - k, - value, - width = f.width().unwrap_or(0) + f.precision().unwrap_or(0), - precision = f.precision().unwrap_or(0) - ))?; - } - } - if has_options { - f.write_str("\n")?; - f.pad("")?; - } - f.write_str(")")?; - if let Some(els) = &self.child_elements { - f.write_str(" {\n")?; - for el in els { - f.write_fmt(format_args!( - "\n{:width$.precision$}\n", - el, - width = f.width().unwrap_or(0) + f.precision().unwrap_or(0), - precision = f.precision().unwrap_or(0) - ))?; - } - f.pad("")?; - f.write_str("}")?; - } - Ok(()) - } -} - -impl<'a> From for Element<'a, Message> { - fn from(value: RenderedElement) -> Self { - let child_elements = match value.child_elements { - Some(ref elements) => elements.clone(), - None => vec![], - }; - - let content: Element<'a, Message> = match value.name.clone() { - ElementName::Text(s) => { - if s == String::new() { - widget::text("New Text").into() - } else { - widget::text(s).into() - } - } - ElementName::Button(s) => { - if s == String::new() { - widget::button(widget::text("New Button")).into() - } else { - widget::button(widget::text(s)).into() - } - } - ElementName::Svg(p) => widget::svg(p).into(), - ElementName::Image(p) => widget::image(p).into(), - ElementName::Container => { - widget::container(if child_elements.len() == 1 { - child_elements[0].clone().into() - } else { - Element::from("") - }) - .padding(20) - .into() - } - ElementName::Row => widget::Row::from_vec( - child_elements.into_iter().map(Into::into).collect(), - ) - .padding(20) - .into(), - ElementName::Column => widget::Column::from_vec( - child_elements.into_iter().map(Into::into).collect(), - ) - .padding(20) - .into(), - }; - iced_drop::droppable(content) - .id(value.get_id()) - .drag_hide(true) - .on_drop(move |point, rect| { - Message::MoveElement(value.clone(), point, rect) - }) - .into() - } -} - -#[derive(Debug, Clone)] -pub enum Action<'a> { - AddNew, - PushFront(&'a Id), - InsertAfter(&'a Id, &'a Id), - Drop, - Stop, -} - -impl<'a> Action<'a> { - pub fn new( - ids: &'a [Id], - element_tree: &'a Option, - source_id: Option, - ) -> Self { - let mut action = Self::Stop; - if ids.len() == 1 { - if element_tree.is_none() { - action = Self::AddNew; - } else { - action = Self::Drop; - } - } else { - let id: &Id = match source_id { - Some(id) if ids.contains(&id) => { - let element_id = - &ids[ids.iter().position(|x| *x == id).unwrap()]; - if ids.len() > 2 && &ids[ids.len() - 1] == element_id { - return Self::Stop; - } - element_id - } - _ => ids.last().unwrap(), - }; - let mut element_tree = element_tree.clone().unwrap(); - let element = element_tree.find_by_id(id).unwrap(); - - // Element is a parent and isn't a non-empty container - if (element.is_empty() || !(element.name == ElementName::Container)) - && element.is_parent() - { - action = Self::PushFront(id); - } else if ids.len() > 2 { - let parent = - element_tree.find_by_id(&ids[ids.len() - 2]).unwrap(); - - if parent.name == ElementName::Container - && parent.child_elements != Some(vec![]) - { - action = Self::Stop; - } else { - action = Self::InsertAfter( - &ids[ids.len() - 2], - &ids[ids.len() - 1], - ); - } - } - } - action - } -} - -pub fn text(text: &str) -> RenderedElement { - RenderedElement::new(ElementName::Text(text.to_owned())).preset_options(&[ - "size", - "line_height", - "width", - "height", - ]) -} - -pub fn button(text: &str) -> RenderedElement { - RenderedElement::new(ElementName::Button(text.to_owned())) -} - -pub fn svg(path: &str) -> RenderedElement { - RenderedElement::new(ElementName::Svg(path.to_owned())) -} - -pub fn image(path: &str) -> RenderedElement { - RenderedElement::new(ElementName::Image(path.to_owned())) -} - -pub fn container(content: Option) -> RenderedElement { - match content { - Some(el) => RenderedElement::with(ElementName::Container, vec![el]), - None => RenderedElement::with(ElementName::Container, vec![]), - } -} - -pub fn row(child_elements: Option>) -> RenderedElement { - RenderedElement::with(ElementName::Row, child_elements.unwrap_or_default()) -} - -pub fn column(child_elements: Option>) -> RenderedElement { - RenderedElement::with( - ElementName::Column, - child_elements.unwrap_or_default(), - ) -} diff --git a/iced_builder/src/widget.rs b/iced_builder/src/widget.rs deleted file mode 100644 index ed2073a..0000000 --- a/iced_builder/src/widget.rs +++ /dev/null @@ -1,21 +0,0 @@ -use iced::widget::{container, text, tooltip}; -use iced::Element; - -pub mod tip { - pub use super::tooltip::Position; -} - -pub fn tip<'a, Message: 'a>( - target: impl Into>, - tip: &'a str, - position: tip::Position, -) -> Element<'a, Message> { - tooltip( - target, - container(text(tip).size(14)) - .padding(5) - .style(container::rounded_box), - position, - ) - .into() -} diff --git a/iced_drop/.gitignore b/iced_drop/.gitignore deleted file mode 100644 index ff0d847..0000000 --- a/iced_drop/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/target -/.vscode -Cargo.lock diff --git a/iced_drop/Cargo.toml b/iced_drop/Cargo.toml deleted file mode 100644 index 40beeb0..0000000 --- a/iced_drop/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "iced_drop" -version = "0.1.0" -edition = "2021" - -[dependencies.iced] -version = "0.13.1" -features = ["advanced"] diff --git a/iced_drop/LICENSE b/iced_drop/LICENSE deleted file mode 100644 index 89d9fee..0000000 --- a/iced_drop/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024 jhannyj - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/iced_drop/README.md b/iced_drop/README.md deleted file mode 100644 index 41b637b..0000000 --- a/iced_drop/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# iced_drop - -A small library which provides a custom widget and operation to make drag and drop easier to implement in [iced](https://github.com/iced-rs/iced/tree/master) - -## Usage - -To add drag and drog functionality, first define two messages with the following format - -```rust -enum Message { - Drop(iced::Point, iced::Rectangle) - HandleZones(Vec<(iced::advanced::widget::Id, iced::Rectangle)>) -} -``` - -The `Drop` message will be sent when the droppable is being dragged, and the left mouse button is released. This message provides the mouse position and layout boundaries of the droppable at the release point. - -The `HandleZones` message will be sent after an operation that finds the drop zones under the mouse position. It provides the Id and bounds for each drop zone. - -Next, create create a droppable in the view method and assign the on_drop message. The dropopable function takes an `impl Into` object, so it's easy to make a droppable from any iced widget. - -```rust -iced_drop::droppable("Drop me!").on_drop(Message::Drop); -``` - -Next, create a "drop zone." A drop zone is any widget that operates like a container andhas some assigned Id. It's important that the widget is assigned some Id or it won't be recognized as a drop zone. - -```rust -iced::widget::container("Drop zone") - .id(iced::widget::container::Id::new("drop_zone")); -``` - -Finally, handle the updates of the drop messages - -```rust -match message { - Message::Drop(cursor_pos, _) => { - return iced_drop::zones_on_point( - Message::HandleZonesFound, - point, - None, - None, - ); - } - Message::HandleZones(zones) => { - println!("{:?}", zones) - } -} -``` - -On Drop, we return a widget operation that looks for drop zones under the cursor_pos. When this operation finishes, it returns the zones found and sends the `HandleZones` message. In this example, we only defined one zone, so the zones vector will either be empty if the droppable was not dropped on the zone, or it will contain the `drop_zone` - -## Examples - -There are two examples: color, todo. - -The color example is a very basic drag/drop showcase where the user can drag colors into zones and change the zone's color. I would start here. - -[Link to video](https://drive.google.com/file/d/1K1CCi2Lc90IUyDufsvoUBZmUCbeg6_Fi/view?usp=sharing) - -To run this examples: `cargo run -p color` - -The todo example is a basic todo board application similar to Trello. This is a much much more complex example as it handles custom highlighting and nested droppables, but it just shows you can make some pretty cool things with iced. - -[Link to video](https://drive.google.com/file/d/1MLOCk4Imd_oUnrTj_psbpYbwua976HmR/view?usp=sharing) - -To run this example try: `cargo run -p todo` - -Note: the todo example might also be a good example on how one can use operations. Check examples/todo/src/operation.rs. I didn't find any other examples of this in the iced repo except for the built in focus operations. - -## Future Development - -Right now it's a little annoying having to work with iced's Id type. At some point, I will work on a drop_zone widget that can take some generic clonable type as an id, and I will create a seperate find_zones operation that will return a list of this custom Id. This should make it easier to determine which drop zones were found. diff --git a/iced_drop/src/lib.rs b/iced_drop/src/lib.rs deleted file mode 100644 index 9906cbe..0000000 --- a/iced_drop/src/lib.rs +++ /dev/null @@ -1,53 +0,0 @@ -pub mod widget; - -use iced::{ - advanced::widget::{operate, Id}, - advanced::{graphics::futures::MaybeSend, renderer}, - task::Task, - Element, Point, Rectangle, -}; - -use widget::droppable::*; -use widget::operation::drop; - -pub fn droppable<'a, Message, Theme, Renderer>( - content: impl Into>, -) -> Droppable<'a, Message, Theme, Renderer> -where - Message: Clone, - Renderer: renderer::Renderer, -{ - Droppable::new(content) -} - -pub fn zones_on_point( - msg: MF, - point: Point, - options: Option>, - depth: Option, -) -> Task -where - T: Send + 'static, - MF: Fn(Vec<(Id, Rectangle)>) -> T + MaybeSend + Sync + Clone + 'static, -{ - operate(drop::find_zones( - move |bounds| bounds.contains(point), - options, - depth, - )) - .map(move |id| msg(id)) -} - -pub fn find_zones( - msg: MF, - filter: F, - options: Option>, - depth: Option, -) -> Task -where - Message: Send + 'static, - MF: Fn(Vec<(Id, Rectangle)>) -> Message + MaybeSend + Sync + Clone + 'static, - F: Fn(&Rectangle) -> bool + Send + 'static, -{ - operate(drop::find_zones(filter, options, depth)).map(move |id| msg(id)) -} diff --git a/iced_drop/src/widget.rs b/iced_drop/src/widget.rs deleted file mode 100644 index 6b3fed2..0000000 --- a/iced_drop/src/widget.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod droppable; -pub mod operation; diff --git a/iced_drop/src/widget/droppable.rs b/iced_drop/src/widget/droppable.rs deleted file mode 100644 index 80d8600..0000000 --- a/iced_drop/src/widget/droppable.rs +++ /dev/null @@ -1,499 +0,0 @@ -//! Encapsulates a widget that can be dragged and dropped. -use std::fmt::Debug; -use std::vec; - -use iced::advanced::widget::{Operation, Tree, Widget}; -use iced::advanced::{self, layout, mouse, overlay, renderer, Layout}; -use iced::event::Status; -use iced::{Element, Point, Rectangle, Size, Vector}; - -/// An element that can be dragged and dropped on a [`DropZone`] -pub struct Droppable<'a, Message, Theme = iced::Theme, Renderer = iced::Renderer> -where - Message: Clone, - Renderer: renderer::Renderer, -{ - content: Element<'a, Message, Theme, Renderer>, - id: Option, - on_click: Option, - on_drop: Option Message + 'a>>, - on_drag: Option Message + 'a>>, - on_cancel: Option, - drag_mode: Option<(bool, bool)>, - drag_overlay: bool, - drag_hide: bool, - drag_center: bool, - drag_size: Option, - reset_delay: usize, -} - -impl<'a, Message, Theme, Renderer> Droppable<'a, Message, Theme, Renderer> -where - Message: Clone, - Renderer: renderer::Renderer, -{ - /// Creates a new [`Droppable`]. - pub fn new(content: impl Into>) -> Self { - Self { - content: content.into(), - id: None, - on_click: None, - on_drop: None, - on_drag: None, - on_cancel: None, - drag_mode: Some((true, true)), - drag_overlay: true, - drag_hide: false, - drag_center: false, - drag_size: None, - reset_delay: 0, - } - } - - /// Sets the unique identifier of the [`Droppable`]. - pub fn id(mut self, id: iced::advanced::widget::Id) -> Self { - self.id = Some(id); - self - } - - /// Sets the message that will be produced when the [`Droppable`] is clicked. - pub fn on_click(mut self, message: Message) -> Self { - self.on_click = Some(message); - self - } - - /// Sets the message that will be produced when the [`Droppable`] is dropped on a [`DropZone`]. - /// - /// Unless this is set, the [`Droppable`] will be disabled. - pub fn on_drop(mut self, message: F) -> Self - where - F: Fn(Point, Rectangle) -> Message + 'a, - { - self.on_drop = Some(Box::new(message)); - self - } - - /// Sets the message that will be produced when the [`Droppable`] is dragged. - pub fn on_drag(mut self, message: F) -> Self - where - F: Fn(Point, Rectangle) -> Message + 'a, - { - self.on_drag = Some(Box::new(message)); - self - } - - /// Sets the message that will be produced when the user right clicks while dragging the [`Droppable`]. - pub fn on_cancel(mut self, message: Message) -> Self { - self.on_cancel = Some(message); - self - } - - /// Sets whether the [`Droppable`] should be drawn under the cursor while dragging. - pub fn drag_overlay(mut self, drag_overlay: bool) -> Self { - self.drag_overlay = drag_overlay; - self - } - - /// Sets whether the [`Droppable`] should be hidden while dragging. - pub fn drag_hide(mut self, drag_hide: bool) -> Self { - self.drag_hide = drag_hide; - self - } - - /// Sets whether the [`Droppable`] should be centered on the cursor while dragging. - pub fn drag_center(mut self, drag_center: bool) -> Self { - self.drag_center = drag_center; - self - } - - // Sets whether the [`Droppable`] can be dragged along individual axes. - pub fn drag_mode(mut self, drag_x: bool, drag_y: bool) -> Self { - self.drag_mode = Some((drag_x, drag_y)); - self - } - - /// Sets whether the [`Droppable`] should be be resized to a given size while dragging. - pub fn drag_size(mut self, hide_size: Size) -> Self { - self.drag_size = Some(hide_size); - self - } - - /// Sets the number of frames/layout calls to wait before resetting the size of the [`Droppable`] after dropping. - /// - /// This is useful for cases where the [`Droppable`] is being moved to a new location after some widget operation. - /// In this case, the [`Droppable`] will mainting the 'drag_size' for the given number of frames before resetting to its original size. - /// This prevents the [`Droppable`] from 'jumping' back to its original size before the new location is rendered which - /// prevents flickering. - /// - /// Warning: this should only be set if there's is some noticeble flickering when the [`Droppable`] is dropped. That is, if the - /// [`Droppable`] returns to its original size before it's moved to it's new location. - pub fn reset_delay(mut self, reset_delay: usize) -> Self { - self.reset_delay = reset_delay; - self - } -} - -impl<'a, Message, Theme, Renderer> Widget - for Droppable<'a, Message, Theme, Renderer> -where - Message: Clone, - Renderer: renderer::Renderer, -{ - fn state(&self) -> iced::advanced::widget::tree::State { - advanced::widget::tree::State::new(State::default()) - } - - fn tag(&self) -> iced::advanced::widget::tree::Tag { - advanced::widget::tree::Tag::of::() - } - - fn children(&self) -> Vec { - vec![advanced::widget::Tree::new(&self.content)] - } - - fn diff(&self, tree: &mut iced::advanced::widget::Tree) { - tree.diff_children(std::slice::from_ref(&self.content)) - } - - fn size(&self) -> iced::Size { - self.content.as_widget().size() - } - - fn on_event( - &mut self, - tree: &mut iced::advanced::widget::Tree, - event: iced::Event, - layout: iced::advanced::Layout<'_>, - cursor: iced::advanced::mouse::Cursor, - _renderer: &Renderer, - _clipboard: &mut dyn iced::advanced::Clipboard, - shell: &mut iced::advanced::Shell<'_, Message>, - _viewport: &iced::Rectangle, - ) -> iced::advanced::graphics::core::event::Status { - // handle the on event of the content first, in case that the droppable is nested - let status = self.content.as_widget_mut().on_event( - &mut tree.children[0], - event.clone(), - layout, - cursor, - _renderer, - _clipboard, - shell, - _viewport, - ); - // this should really only be captured if the droppable is nested or it contains some other - // widget that captures the event - if status == Status::Captured { - return status; - }; - - if let Some(on_drop) = self.on_drop.as_deref() { - let state = tree.state.downcast_mut::(); - if let iced::Event::Mouse(mouse) = event { - match mouse { - mouse::Event::ButtonPressed(btn) => { - if btn == mouse::Button::Left && cursor.is_over(layout.bounds()) { - // select the droppable and store the position of the widget before dragging - state.action = Action::Select(cursor.position().unwrap()); - let bounds = layout.bounds(); - state.widget_pos = bounds.position(); - state.overlay_bounds.width = bounds.width; - state.overlay_bounds.height = bounds.height; - - if let Some(on_click) = self.on_click.clone() { - shell.publish(on_click); - } - return Status::Captured; - } else if btn == mouse::Button::Right { - if let Action::Drag(_, _) = state.action { - shell.invalidate_layout(); - state.action = Action::None; - if let Some(on_cancel) = self.on_cancel.clone() { - shell.publish(on_cancel); - } - } - } - } - mouse::Event::CursorMoved { mut position } => match state.action { - Action::Select(start) | Action::Drag(start, _) => { - // calculate the new position of the widget after dragging - - if let Some((drag_x, drag_y)) = self.drag_mode { - position = Point { - x: if drag_x { position.x } else { start.x }, - y: if drag_y { position.y } else { start.y }, - }; - } - - state.action = Action::Drag(start, position); - // update the position of the overlay since the cursor was moved - if self.drag_center { - state.overlay_bounds.x = - position.x - state.overlay_bounds.width / 2.0; - state.overlay_bounds.y = - position.y - state.overlay_bounds.height / 2.0; - } else { - state.overlay_bounds.x = state.widget_pos.x + position.x - start.x; - state.overlay_bounds.y = state.widget_pos.y + position.y - start.y; - } - // send on drag msg - if let Some(on_drag) = self.on_drag.as_deref() { - let message = (on_drag)(position, state.overlay_bounds); - shell.publish(message); - } - } - _ => (), - }, - mouse::Event::ButtonReleased(btn) => { - if btn == mouse::Button::Left { - match state.action { - Action::Select(_) => { - state.action = Action::None; - } - Action::Drag(_, current) => { - // send on drop msg - let message = (on_drop)(current, state.overlay_bounds); - shell.publish(message); - - if self.reset_delay == 0 { - state.action = Action::None; - } else { - state.action = Action::Wait(self.reset_delay); - } - } - _ => (), - } - } - } - _ => {} - } - } - } - Status::Ignored - } - - fn layout( - &self, - tree: &mut iced::advanced::widget::Tree, - renderer: &Renderer, - limits: &iced::advanced::layout::Limits, - ) -> iced::advanced::layout::Node { - let state: &mut State = tree.state.downcast_mut::(); - let content_node = self - .content - .as_widget() - .layout(&mut tree.children[0], renderer, limits); - - // Adjust the size of the original widget if it's being dragged or we're wating to reset the size - if let Some(new_size) = self.drag_size { - match state.action { - Action::Drag(_, _) => { - return iced::advanced::layout::Node::with_children( - new_size, - content_node.children().to_vec(), - ); - } - Action::Wait(reveal_index) => { - if reveal_index <= 1 { - state.action = Action::None; - } else { - state.action = Action::Wait(reveal_index - 1); - } - - return iced::advanced::layout::Node::with_children( - new_size, - content_node.children().to_vec(), - ); - } - _ => (), - } - } - - content_node - } - - fn operate( - &self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn Operation, - ) { - let state = tree.state.downcast_mut::(); - operation.custom(state, self.id.as_ref()); - operation.container(self.id.as_ref(), layout.bounds(), &mut |operation| { - self.content - .as_widget() - .operate(&mut tree.children[0], layout, renderer, operation); - }); - } - - fn draw( - &self, - tree: &iced::advanced::widget::Tree, - renderer: &mut Renderer, - theme: &Theme, - style: &renderer::Style, - layout: iced::advanced::Layout<'_>, - cursor: iced::advanced::mouse::Cursor, - viewport: &iced::Rectangle, - ) { - let state: &State = tree.state.downcast_ref::(); - if let Action::Drag(_, _) = state.action { - if self.drag_hide { - return; - } - } - - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - style, - layout, - cursor, - &viewport, - ); - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - _translation: Vector, - ) -> Option> { - let state: &mut State = tree.state.downcast_mut::(); - let mut children = tree.children.iter_mut(); - if self.drag_overlay { - if let Action::Drag(_, _) = state.action { - return Some(overlay::Element::new(Box::new(Overlay { - content: &self.content, - tree: children.next().unwrap(), - overlay_bounds: state.overlay_bounds, - }))); - } - } - self.content.as_widget_mut().overlay( - children.next().unwrap(), - layout, - renderer, - _translation, - ) - } - - fn mouse_interaction( - &self, - tree: &iced::advanced::widget::Tree, - layout: iced::advanced::Layout<'_>, - cursor: iced::advanced::mouse::Cursor, - _viewport: &iced::Rectangle, - _renderer: &Renderer, - ) -> iced::advanced::mouse::Interaction { - let child_interact = self.content.as_widget().mouse_interaction( - &tree.children[0], - layout, - cursor, - _viewport, - _renderer, - ); - if child_interact != mouse::Interaction::default() { - return child_interact; - } - - let state = tree.state.downcast_ref::(); - - if self.on_drop.is_none() { - return mouse::Interaction::NotAllowed; - } - if let Action::Drag(_, _) = state.action { - return mouse::Interaction::Grabbing; - } - if cursor.is_over(layout.bounds()) { - return mouse::Interaction::Pointer; - } - mouse::Interaction::default() - } -} - -impl<'a, Message, Theme, Renderer> From> - for Element<'a, Message, Theme, Renderer> -where - Message: 'a + Clone, - Theme: 'a, - Renderer: 'a + renderer::Renderer, -{ - fn from( - droppable: Droppable<'a, Message, Theme, Renderer>, - ) -> Element<'a, Message, Theme, Renderer> { - Element::new(droppable) - } -} - -#[derive(Default, Clone, Copy, PartialEq, Debug)] -pub struct State { - widget_pos: Point, - overlay_bounds: Rectangle, - action: Action, -} - -#[derive(Default, Clone, Copy, PartialEq, Debug)] -pub enum Action { - #[default] - None, - /// (point clicked) - Select(Point), - /// (start pos, current pos) - Drag(Point, Point), - /// (frames to wait) - Wait(usize), -} - -struct Overlay<'a, 'b, Message, Theme, Renderer> -where - Renderer: renderer::Renderer, -{ - content: &'b Element<'a, Message, Theme, Renderer>, - tree: &'b mut advanced::widget::Tree, - overlay_bounds: Rectangle, -} - -impl<'a, 'b, Message, Theme, Renderer> overlay::Overlay - for Overlay<'a, 'b, Message, Theme, Renderer> -where - Renderer: renderer::Renderer, -{ - fn layout(&mut self, renderer: &Renderer, _bounds: Size) -> layout::Node { - Widget::::layout( - self.content.as_widget(), - self.tree, - renderer, - &layout::Limits::new(Size::ZERO, self.overlay_bounds.size()), - ) - .move_to(self.overlay_bounds.position()) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Theme, - inherited_style: &renderer::Style, - layout: Layout<'_>, - cursor_position: mouse::Cursor, - ) { - Widget::::draw( - self.content.as_widget(), - self.tree, - renderer, - theme, - inherited_style, - layout, - cursor_position, - &Rectangle::with_size(Size::INFINITY), - ); - } - - fn is_over(&self, _layout: Layout<'_>, _renderer: &Renderer, _cursor_position: Point) -> bool { - false - } -} diff --git a/iced_drop/src/widget/operation.rs b/iced_drop/src/widget/operation.rs deleted file mode 100644 index 3d7dcff..0000000 --- a/iced_drop/src/widget/operation.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod drop; diff --git a/iced_drop/src/widget/operation/drop.rs b/iced_drop/src/widget/operation/drop.rs deleted file mode 100644 index a76181c..0000000 --- a/iced_drop/src/widget/operation/drop.rs +++ /dev/null @@ -1,90 +0,0 @@ -use iced::{ - advanced::widget::{ - operation::{Outcome, Scrollable}, - Id, Operation, - }, - Rectangle, Vector, -}; - -/// Produces an [`Operation`] that will find the drop zones that pass a filter on the zone's bounds. -/// For any drop zone to be considered, the Element must have some Id. -/// If `options` is `None`, all drop zones will be considered. -/// Depth determines how how deep into nested drop zones to go. -/// If 'depth' is `None`, nested dropzones will be fully explored -pub fn find_zones( - filter: F, - options: Option>, - depth: Option, -) -> impl Operation> -where - F: Fn(&Rectangle) -> bool + Send + 'static, -{ - struct FindDropZone { - filter: F, - options: Option>, - zones: Vec<(Id, Rectangle)>, - max_depth: Option, - c_depth: usize, - offset: Vector, - } - - impl Operation> for FindDropZone - where - F: Fn(&Rectangle) -> bool + Send + 'static, - { - fn container( - &mut self, - id: Option<&Id>, - bounds: iced::Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation>), - ) { - match id { - Some(id) => { - let is_option = match &self.options { - Some(options) => options.contains(id), - None => true, - }; - let bounds = bounds - self.offset; - if is_option && (self.filter)(&bounds) { - self.c_depth += 1; - self.zones.push((id.clone(), bounds)); - } - } - None => (), - } - let goto_next = match &self.max_depth { - Some(m_depth) => self.c_depth < *m_depth, - None => true, - }; - if goto_next { - operate_on_children(self); - } - } - - fn finish(&self) -> Outcome> { - Outcome::Some(self.zones.clone()) - } - - fn scrollable( - &mut self, - _state: &mut dyn Scrollable, - _id: Option<&Id>, - bounds: Rectangle, - _content_bounds: Rectangle, - translation: Vector, - ) { - if (self.filter)(&bounds) { - self.offset = self.offset + translation; - } - } - } - - FindDropZone { - filter, - options, - zones: vec![], - max_depth: depth, - c_depth: 0, - offset: Vector { x: 0.0, y: 0.0 }, - } -} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..197262a --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,4 @@ +edition = "2021" +imports_granularity = "Module" +group_imports = "StdExternalCrate" +max_width = 80 diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..9d29af7 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,121 @@ +use std::path::PathBuf; + +use serde::Deserialize; +use tokio_stream::wrappers::ReadDirStream; +use tokio_stream::StreamExt; + +use crate::theme::{theme_from_str, theme_index, Appearance, Theme}; +use crate::{environment, Error}; + +#[derive(Debug, Clone, Default)] +pub struct Config { + pub theme: Appearance, + pub last_project: Option, +} + +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"); + } + 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"); + } + dir + } + + pub fn config_file_path() -> PathBuf { + Self::config_dir().join(environment::CONFIG_FILE_NAME) + } + + pub async fn load() -> Result { + use tokio::fs; + + #[derive(Deserialize)] + pub struct Configuration { + #[serde(default)] + pub theme: String, + pub last_project: Option, + } + + 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 { + use tokio::fs; + + let read_entry = |entry: fs::DirEntry| async move { + let content = fs::read_to_string(entry.path()).await.ok()?; + + let theme: Theme = toml::from_str(content.as_ref()).ok()?; + let name = entry.path().file_stem()?.to_string_lossy().to_string(); + + Some(theme.into_iced_theme(name)) + }; + + let mut all = iced::Theme::ALL.to_owned(); + let mut selected = iced::Theme::default(); + + if theme_index(&theme_name, 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(Appearance { + selected, + all: all.into(), + }) + } +} diff --git a/src/dialogs.rs b/src/dialogs.rs new file mode 100644 index 0000000..2d916b1 --- /dev/null +++ b/src/dialogs.rs @@ -0,0 +1,30 @@ +use rfd::{MessageButtons, MessageDialog, MessageDialogResult, MessageLevel}; + +pub fn error_dialog(description: impl Into) { + let _ = MessageDialog::new() + .set_level(MessageLevel::Error) + .set_buttons(MessageButtons::Ok) + .set_title("Oops! Something went wrong.") + .set_description(description) + .show(); +} + +pub fn warning_dialog(description: impl Into) { + 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) -> bool { + let result = MessageDialog::new() + .set_level(MessageLevel::Warning) + .set_buttons(MessageButtons::OkCancel) + .set_title("Unsaved changes") + .set_description(description) + .show(); + + matches!(result, MessageDialogResult::Ok) +} diff --git a/src/environment.rs b/src/environment.rs new file mode 100644 index 0000000..3ecb790 --- /dev/null +++ b/src/environment.rs @@ -0,0 +1,43 @@ +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) +} + +fn portable_dir() -> Option { + 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 { + 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/src/error.rs b/src/error.rs new file mode 100644 index 0000000..f4011bd --- /dev/null +++ b/src/error.rs @@ -0,0 +1,55 @@ +use std::io; +use std::sync::Arc; + +use thiserror::Error; + +#[derive(Debug, Clone, Error)] +#[error(transparent)] +pub enum Error { + IO(Arc), + #[error("config does not exist")] + ConfigMissing, + #[error("JSON parsing error: {0}")] + SerdeJSON(Arc), + #[error("TOML parsing error: {0}")] + SerdeTOML(#[from] toml::de::Error), + RustFmt(Arc), + #[error("the element tree contains no matching element")] + NonExistentElement, + #[error( + "the file dialog has been closed without selecting a valid option" + )] + DialogClosed, + #[error("{0}")] + Other(String), +} + +impl From for Error { + fn from(value: io::Error) -> Self { + Self::IO(Arc::new(value)) + } +} + +impl From for Error { + fn from(value: serde_json::Error) -> Self { + Self::SerdeJSON(Arc::new(value)) + } +} + +impl From for Error { + fn from(value: rust_format::Error) -> Self { + Self::RustFmt(Arc::new(value)) + } +} + +impl From<&str> for Error { + fn from(value: &str) -> Self { + Self::Other(value.to_owned()) + } +} + +impl From for Error { + fn from(value: String) -> Self { + Self::Other(value) + } +} diff --git a/src/icon.rs b/src/icon.rs new file mode 100644 index 0000000..f6760d5 --- /dev/null +++ b/src/icon.rs @@ -0,0 +1,23 @@ +// Generated automatically by iced_fontello at build time. +// Do not edit manually. Source: ../fonts/icons.toml +// 02c7558d187cdc056fdd0e6a638ef805fa10f5955f834575e51d75acd35bc70e +use iced::widget::{text, Text}; +use iced::Font; + +pub const FONT: &[u8] = include_bytes!("../fonts/icons.ttf"); + +pub fn copy<'a>() -> Text<'a> { + icon("\u{F1C9}") +} + +pub fn open<'a>() -> Text<'a> { + icon("\u{F115}") +} + +pub fn save<'a>() -> Text<'a> { + icon("\u{1F4BE}") +} + +fn icon<'a>(codepoint: &'a str) -> Text<'a> { + text(codepoint).font(Font::with_name("icons")) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..5b95b94 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,382 @@ +#![feature(test)] +mod config; +mod dialogs; +mod environment; +mod error; +mod icon; +mod panes; +mod theme; +mod types; +mod widget; + +use std::path::PathBuf; + +use config::Config; +use dialogs::{error_dialog, unsaved_changes_dialog, warning_dialog}; +use error::Error; +use iced::advanced::widget::Id; +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::transition::Easing; +use iced_anim::{Animated, Animation}; +use panes::{code_view, designer_view, element_list}; +use tokio::runtime; +use types::{Action, DesignerPage, ElementName, Message, Project}; + +//pub(crate) type Result = core::result::Result; + +fn main() -> Result<(), Box> { + let config_load = { + let rt = runtime::Builder::new_current_thread() + .enable_all() + .build()?; + + rt.block_on(Config::load()) + }; + + iced::application(App::title, App::update, App::view) + .font(icon::FONT) + .theme(|state| state.theme.value().clone()) + .subscription(App::subscription) + .run_with(move || App::new(config_load))?; + + Ok(()) +} + +struct App { + is_dirty: bool, + is_loading: bool, + project_path: Option, + project: Project, + config: Config, + theme: Animated, + pane_state: pane_grid::State, + focus: Option, + designer_page: DesignerPage, + element_list: &'static [ElementName], + editor_content: text_editor::Content, +} + +#[derive(Clone, Copy, Debug)] +enum Panes { + Designer, + ElementList, +} + +impl App { + fn new(config_load: Result) -> (Self, Task) { + let state = pane_grid::State::with_configuration( + pane_grid::Configuration::Split { + axis: pane_grid::Axis::Vertical, + ratio: 0.8, + a: Box::new(pane_grid::Configuration::Pane(Panes::Designer)), + b: Box::new(pane_grid::Configuration::Pane(Panes::ElementList)), + }, + ); + + let config = config_load.unwrap_or_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, config.clone()), + Message::FileOpened, + ); + } else { + warning_dialog(format!( + "The file {} does not exist, or isn't a file.", + path.to_string_lossy() + )); + } + } + + ( + Self { + is_dirty: false, + is_loading: false, + project_path: None, + project: Project::new(), + config, + theme: Animated::new(theme, Easing::EASE_IN), + pane_state: state, + focus: None, + designer_page: DesignerPage::DesignerView, + element_list: ElementName::ALL, + editor_content: text_editor::Content::new(), + }, + task, + ) + } + + fn title(&self) -> String { + let saved_state = if self.is_dirty { " *" } else { "" }; + + let project_name = match &self.project.title { + Some(n) => { + format!( + " - {}", + if n.len() > 60 { + format!("...{}", &n[n.len() - 40..]) + } else { + n.to_owned() + } + ) + } + None => String::new(), + }; + + format!("iced Builder{project_name}{saved_state}") + } + + fn update(&mut self, message: Message) -> Task { + match message { + Message::ToggleTheme(event) => { + self.theme.update(event); + } + Message::CopyCode => { + return clipboard::write(self.editor_content.text()) + } + Message::SwitchPage(page) => self.designer_page = page, + Message::EditorAction(action) => { + if let text_editor::Action::Scroll { lines: _ } = action { + self.editor_content.perform(action); + } + } + Message::RefreshEditorContent => { + match self.project.app_code(&self.config) { + Ok(code) => { + self.editor_content = + text_editor::Content::with_text(&code); + } + Err(error) => error_dialog(error.to_string()), + } + } + Message::DropNewElement(name, point, _) => { + return iced_drop::zones_on_point( + move |zones| Message::HandleNew(name.clone(), zones), + point, + None, + None, + ) + } + Message::HandleNew(name, zones) => { + let ids: Vec = zones.into_iter().map(|z| z.0).collect(); + if !ids.is_empty() { + let eltree_clone = self.project.element_tree.clone(); + let action = Action::new(&ids, &eltree_clone, None); + let result = name.handle_action( + self.project.element_tree.as_mut(), + action, + ); + match result { + Ok(Some(ref element)) => { + self.project.element_tree = Some(element.clone()); + } + Err(error) => error_dialog(error.to_string()), + _ => {} + } + + self.is_dirty = true; + return Task::done(Message::RefreshEditorContent); + } + } + Message::MoveElement(element, point, _) => { + return iced_drop::zones_on_point( + move |zones| Message::HandleMove(element.clone(), zones), + point, + None, + None, + ) + } + Message::HandleMove(element, zones) => { + let ids: Vec = zones.into_iter().map(|z| z.0).collect(); + if !ids.is_empty() { + let eltree_clone = self.project.element_tree.clone(); + let action = Action::new( + &ids, + &eltree_clone, + Some(element.get_id()), + ); + let result = element.handle_action( + self.project.element_tree.as_mut(), + action, + ); + if let Err(error) = result { + error_dialog(error.to_string()); + } + + self.is_dirty = true; + return Task::done(Message::RefreshEditorContent); + } + } + Message::PaneResized(pane_grid::ResizeEvent { split, ratio }) => { + self.pane_state.resize(split, ratio); + } + Message::PaneClicked(pane) => { + self.focus = Some(pane); + } + Message::PaneDragged(pane_grid::DragEvent::Dropped { + pane, + target, + }) => { + self.pane_state.drop(pane, target); + } + Message::PaneDragged(_) => {} + Message::NewFile => { + if !self.is_loading { + if !self.is_dirty { + self.project = Project::new(); + self.project_path = None; + self.editor_content = text_editor::Content::new(); + } else if unsaved_changes_dialog("You have unsaved changes. Do you wish to discard these and create a new project?") { + self.is_dirty = false; + self.project = Project::new(); + self.project_path = None; + self.editor_content = text_editor::Content::new(); + } + } + } + Message::OpenFile => { + if !self.is_loading { + if !self.is_dirty { + self.is_loading = true; + + return Task::perform( + Project::from_file(self.config.clone()), + Message::FileOpened, + ); + } else if 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_file(self.config.clone()), Message::FileOpened); + } + } + } + Message::FileOpened(result) => { + self.is_loading = false; + self.is_dirty = false; + + match result { + Ok((path, project)) => { + self.project = project; + self.project_path = Some(path); + self.editor_content = text_editor::Content::with_text( + &self + .project + .app_code(&self.config) + .unwrap_or_else(|err| err.to_string()), + ); + } + Err(error) => error_dialog(error.to_string()), + } + } + Message::SaveFile => { + if !self.is_loading { + self.is_loading = true; + + return Task::perform( + self.project + .clone() + .write_to_file(self.project_path.clone()), + Message::FileSaved, + ); + } + } + Message::SaveFileAs => { + if !self.is_loading { + self.is_loading = true; + + return Task::perform( + self.project.clone().write_to_file(None), + Message::FileSaved, + ); + } + } + Message::FileSaved(result) => { + self.is_loading = false; + + match result { + Ok(path) => { + self.project_path = Some(path); + self.is_dirty = false; + } + Err(error) => error_dialog(error.to_string()), + } + } + } + + Task::none() + } + + fn subscription(&self) -> iced::Subscription { + keyboard::on_key_press(|key, modifiers| { + if modifiers.command() { + match key.as_ref() { + keyboard::Key::Character("o") => Some(Message::OpenFile), + keyboard::Key::Character("s") => { + Some(if modifiers.shift() { + Message::SaveFileAs + } else { + Message::SaveFile + }) + } + keyboard::Key::Character("n") => Some(Message::NewFile), + _ => None, + } + } else { + None + } + }) + } + + fn view(&self) -> Element<'_, Message> { + let header = row![pick_list( + self.config.theme.all.clone(), + Some(self.theme.target().clone()), + |theme| { Message::ToggleTheme(theme.into()) } + )] + .width(200); + let pane_grid = + PaneGrid::new(&self.pane_state, |id, pane, _is_maximized| { + let is_focused = Some(id) == self.focus; + match pane { + Panes::Designer => match &self.designer_page { + DesignerPage::DesignerView => designer_view::view( + &self.project.element_tree, + self.project.get_theme(&self.config), + is_focused, + ), + DesignerPage::CodeView => code_view::view( + &self.editor_content, + self.theme.target().clone(), + is_focused, + ), + }, + Panes::ElementList => { + element_list::view(self.element_list, is_focused) + } + } + }) + .width(Length::Fill) + .height(Length::Fill) + .spacing(10) + .on_resize(10, Message::PaneResized) + .on_click(Message::PaneClicked) + .on_drag(Message::PaneDragged); + + let content = Column::new() + .push(header) + .push(pane_grid) + .spacing(5) + .align_x(Alignment::Center) + .width(Length::Fill); + + Animation::new(&self.theme, container(content).height(Length::Fill)) + .on_update(Message::ToggleTheme) + .into() + } +} diff --git a/src/panes.rs b/src/panes.rs new file mode 100644 index 0000000..387662a --- /dev/null +++ b/src/panes.rs @@ -0,0 +1,4 @@ +pub mod code_view; +pub mod designer_view; +pub mod element_list; +mod style; diff --git a/src/panes/code_view.rs b/src/panes/code_view.rs new file mode 100644 index 0000000..f545157 --- /dev/null +++ b/src/panes/code_view.rs @@ -0,0 +1,50 @@ +use iced::widget::{button, pane_grid, row, text, text_editor, Space}; +use iced::{Alignment, Font, Length, Theme}; +use super::style; +use crate::icon::copy; +use crate::types::{DesignerPage, Message}; +use crate::widget::tip; + +pub fn view( + editor_content: &text_editor::Content, + theme: Theme, + is_focused: bool, +) -> pane_grid::Content<'_, Message> { + let title = row![ + text("Generated Code"), + Space::with_width(Length::Fill), + tip( + button(copy()).on_press(Message::CopyCode), + "Copy code to clipboard", + tip::Position::FollowCursor + ), + Space::with_width(20), + button("Switch to Designer view") + .on_press(Message::SwitchPage(DesignerPage::DesignerView)) + ] + .align_y(Alignment::Center); + let title_bar = pane_grid::TitleBar::new(title) + .padding(10) + .style(style::title_bar); + pane_grid::Content::new( + text_editor(editor_content) + .on_action(Message::EditorAction) + .highlight( + "rs", + if theme.to_string().contains("Dark") { + highlighter::Theme::SolarizedDark + } else { + highlighter::Theme::InspiredGitHub + }, + .font(Font::MONOSPACE) + ) + .height(Length::Fill) + .padding(20), + ) + .title_bar(title_bar) + .style(if is_focused { + style::pane_focused + } else { + style::pane_active + }) +} diff --git a/src/panes/designer_view.rs b/src/panes/designer_view.rs new file mode 100644 index 0000000..76456db --- /dev/null +++ b/src/panes/designer_view.rs @@ -0,0 +1,37 @@ +use iced::widget::{button, container, pane_grid, row, text, themer, Space}; +use iced::{Alignment, Element, Length}; + +use super::style; +use crate::types::{DesignerPage, Message, RenderedElement}; + +pub fn view<'a>( + element_tree: &Option, + designer_theme: iced::Theme, + is_focused: bool, +) -> pane_grid::Content<'a, Message> { + let el_tree: Element<'a, Message> = match element_tree { + Some(tree) => tree.clone().into(), + None => text("Open a project or begin creating one").into(), + }; + let content = container(themer(designer_theme, el_tree)) + .id(iced::widget::container::Id::new("drop_zone")) + .height(Length::Fill) + .width(Length::Fill); + let title = row![ + text("Designer"), + Space::with_width(Length::Fill), + button("Switch to Code view") + .on_press(Message::SwitchPage(DesignerPage::CodeView)), + ] + .align_y(Alignment::Center); + let title_bar = pane_grid::TitleBar::new(title) + .padding(10) + .style(style::title_bar); + pane_grid::Content::new(content) + .title_bar(title_bar) + .style(if is_focused { + style::pane_focused + } else { + style::pane_active + }) +} diff --git a/src/panes/element_list.rs b/src/panes/element_list.rs new file mode 100644 index 0000000..8a1c6eb --- /dev/null +++ b/src/panes/element_list.rs @@ -0,0 +1,49 @@ +use iced::widget::{column, container, pane_grid, text, Column}; +use iced::{Alignment, Element, Length}; +use iced_drop::droppable; + +use super::style; +use crate::types::{ElementName, Message}; + +fn items_list_view(items: &[ElementName]) -> Element<'_, Message> { + let mut column = Column::new() + .spacing(20) + .align_x(Alignment::Center) + .width(Length::Fill); + + for item in items { + column = + column.push(droppable(text(item.clone().to_string())).on_drop( + move |point, rect| { + Message::DropNewElement(item.clone(), point, rect) + }, + )); + } + + container(column) + .width(Length::Fill) + .height(Length::Fill) + .into() +} + +pub fn view( + element_list: &[ElementName], + is_focused: bool, +) -> pane_grid::Content<'_, Message> { + let items_list = items_list_view(element_list); + let content = column![items_list] + .align_x(Alignment::Center) + .height(Length::Fill) + .width(Length::Fill); + let title = text("Element List"); + let title_bar = pane_grid::TitleBar::new(title) + .padding(10) + .style(style::title_bar); + pane_grid::Content::new(content) + .title_bar(title_bar) + .style(if is_focused { + style::pane_focused + } else { + style::pane_active + }) +} diff --git a/src/panes/style.rs b/src/panes/style.rs new file mode 100644 index 0000000..1eefb2d --- /dev/null +++ b/src/panes/style.rs @@ -0,0 +1,40 @@ +use iced::widget::container::Style; +use iced::{Border, Theme}; + +pub fn title_bar(theme: &Theme) -> Style { + let palette = theme.extended_palette(); + + Style { + text_color: Some(palette.background.strong.text), + background: Some(palette.background.strong.color.into()), + ..Default::default() + } +} + +pub fn pane_active(theme: &Theme) -> Style { + let palette = theme.extended_palette(); + + Style { + background: Some(palette.background.weak.color.into()), + border: Border { + width: 1.0, + color: palette.background.strong.color, + ..Border::default() + }, + ..Default::default() + } +} + +pub fn pane_focused(theme: &Theme) -> Style { + let palette = theme.extended_palette(); + + Style { + background: Some(palette.background.weak.color.into()), + border: Border { + width: 4.0, + color: palette.background.strong.color, + ..Border::default() + }, + ..Default::default() + } +} diff --git a/src/theme.rs b/src/theme.rs new file mode 100644 index 0000000..7d18aa9 --- /dev/null +++ b/src/theme.rs @@ -0,0 +1,381 @@ +use std::sync::Arc; + +use iced::theme::palette::Extended; +use iced::Color; + +use crate::config::Config; + +pub fn theme_index(theme_name: &str, slice: &[iced::Theme]) -> Option { + 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, &config.theme.all) + { + config.theme.all[index].clone() + } else { + iced::Theme::default() + } + } else { + iced::Theme::default() + } + } + } +} + +fn palette_to_string(palette: &iced::theme::Palette) -> String { + format!( + r#"Palette {{ + background: color!(0x{}), + text: color!(0x{}), + primary: color!(0x{}), + success: color!(0x{}), + danger: color!(0x{}), + }}"#, + color_to_hex(palette.background), + color_to_hex(palette.text), + color_to_hex(palette.primary), + color_to_hex(palette.success), + color_to_hex(palette.danger), + ) +} + +fn extended_to_string(extended: &Extended) -> String { + format!( + r#" +Extended{{background:Background{{base:Pair{{color:color!(0x{}),text:color!(0x{}),}},weak:Pair{{color:color!(0x{}),text:color!(0x{}),}},strong:Pair{{color:color!(0x{}),text:color!(0x{}),}},}},primary:Primary{{base:Pair{{color:color!(0x{}),text:color!(0x{}),}},weak:Pair{{color:color!(0x{}),text:color!(0x{}),}},strong:Pair{{color:color!(0x{}),text:color!(0x{}),}},}},secondary:Secondary{{base:Pair{{color:color!(0x{}),text:color!(0x{}),}},weak:Pair{{color:color!(0x{}),text:color!(0x{}),}},strong:Pair{{color:color!(0x{}),text:color!(0x{}),}},}},success:Success{{base:Pair{{color:color!(0x{}),text:color!(0x{}),}},weak:Pair{{color:color!(0x{}),text:color!(0x{}),}},strong:Pair{{color:color!(0x{}),text:color!(0x{}),}},}},danger:Danger{{base:Pair{{color:color!(0x{}),text:color!(0x{}),}},weak:Pair{{color:color!(0x{}),text:color!(0x{}),}},strong:Pair{{color:color!(0x{}),text:color!(0x{}),}},}},is_dark:true,}}"#, + color_to_hex(extended.background.base.color), + color_to_hex(extended.background.base.text), + color_to_hex(extended.background.weak.color), + color_to_hex(extended.background.weak.text), + color_to_hex(extended.background.strong.color), + color_to_hex(extended.background.strong.text), + color_to_hex(extended.primary.base.color), + color_to_hex(extended.primary.base.text), + color_to_hex(extended.primary.weak.color), + color_to_hex(extended.primary.weak.text), + color_to_hex(extended.primary.strong.color), + color_to_hex(extended.primary.strong.text), + color_to_hex(extended.secondary.base.color), + color_to_hex(extended.secondary.base.text), + color_to_hex(extended.secondary.weak.color), + color_to_hex(extended.secondary.weak.text), + color_to_hex(extended.secondary.strong.color), + color_to_hex(extended.secondary.strong.text), + color_to_hex(extended.success.base.color), + color_to_hex(extended.success.base.text), + color_to_hex(extended.success.weak.color), + color_to_hex(extended.success.weak.text), + color_to_hex(extended.success.strong.color), + color_to_hex(extended.success.strong.text), + color_to_hex(extended.danger.base.color), + color_to_hex(extended.danger.base.text), + color_to_hex(extended.danger.weak.color), + color_to_hex(extended.danger.weak.text), + color_to_hex(extended.danger.strong.color), + color_to_hex(extended.danger.strong.text), + ) +} + +pub fn theme_to_string(theme: &iced::Theme) -> String { + let palette = theme.palette(); + let extended = theme.extended_palette(); + + let generated_extended = Extended::generate(palette); + + if &generated_extended == extended { + format!( + r#"custom( + "{}".to_string(), + {} + )"#, + theme, + palette_to_string(&palette) + ) + } else { + format!( + r#"custom_with_fn( + "{}".to_string(), + {}, + |_| {} + )"#, + theme, + palette_to_string(&palette), + extended_to_string(extended) + ) + } +} + +fn color_to_hex(color: Color) -> String { + use std::fmt::Write; + + let mut hex = String::with_capacity(12); + + let [r, g, b, a] = color.into_rgba8(); + + let _ = write!(&mut hex, "{:02X}", r); + let _ = write!(&mut hex, "{:02X}", g); + let _ = write!(&mut hex, "{:02X}", b); + + if a < u8::MAX { + let _ = write!(&mut hex, ", {:.2}", a as f32 / 255.0); + } + + hex +} + +#[derive(Debug, Clone)] +pub struct Appearance { + pub selected: iced::Theme, + pub all: Arc<[iced::Theme]>, +} + +impl Default for Appearance { + fn default() -> Self { + Self { + selected: iced::Theme::default(), + all: iced::Theme::ALL.into(), + } + } +} + +#[derive(Debug, Default, serde::Deserialize)] +pub struct Theme { + palette: ThemePalette, + is_dark: Option, + #[serde(flatten)] + extended: Option, +} + +#[derive(Debug, Clone, 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 Theme { + pub fn into_iced_theme(self, name: String) -> iced::Theme { + iced::Theme::custom_with_fn(name, self.palette.clone().into(), |_| { + self.into() + }) + } +} + +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 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, + } + } +} + +impl From for Extended { + fn from(theme: Theme) -> Self { + let mut extended = Extended::generate(theme.palette.into()); + + if let Some(is_dark) = theme.is_dark { + extended.is_dark = is_dark; + } + + if let Some(extended_palette) = theme.extended { + if let Some(background) = extended_palette.background { + if let Some(base) = background.base { + extended.background.base = base.into(); + } + if let Some(weak) = background.weak { + extended.background.weak = weak.into(); + } + if let Some(strong) = background.strong { + extended.background.strong = strong.into(); + } + } + + // Handle primary + if let Some(primary) = extended_palette.primary { + if let Some(base) = primary.base { + extended.primary.base = base.into(); + } + if let Some(weak) = primary.weak { + extended.primary.weak = weak.into(); + } + if let Some(strong) = primary.strong { + extended.primary.strong = strong.into(); + } + } + + // Handle secondary + if let Some(secondary) = extended_palette.secondary { + if let Some(base) = secondary.base { + extended.secondary.base = base.into(); + } + if let Some(weak) = secondary.weak { + extended.secondary.weak = weak.into(); + } + if let Some(strong) = secondary.strong { + extended.secondary.strong = strong.into(); + } + } + + // Handle success + if let Some(success) = extended_palette.success { + if let Some(base) = success.base { + extended.success.base = base.into(); + } + if let Some(weak) = success.weak { + extended.success.weak = weak.into(); + } + if let Some(strong) = success.strong { + extended.success.strong = strong.into(); + } + } + + // Handle danger + if let Some(danger) = extended_palette.danger { + if let Some(base) = danger.base { + extended.danger.base = base.into(); + } + if let Some(weak) = danger.weak { + extended.danger.weak = weak.into(); + } + if let Some(strong) = danger.strong { + extended.danger.strong = strong.into(); + } + } + } + + extended + } +} + +#[derive(Debug, Default, serde::Deserialize)] +struct ExtendedThemePalette { + background: Option, + primary: Option, + secondary: Option, + success: Option, + danger: Option, +} + +#[derive(Debug, Default, serde::Deserialize)] +struct ThemeBackground { + base: Option, + weak: Option, + strong: Option, +} + +#[derive(Debug, Default, serde::Deserialize)] +struct ThemePrimary { + base: Option, + weak: Option, + strong: Option, +} + +#[derive(Debug, Default, serde::Deserialize)] +struct ThemeSecondary { + base: Option, + weak: Option, + strong: Option, +} + +#[derive(Debug, Default, serde::Deserialize)] +struct ThemeSuccess { + base: Option, + weak: Option, + strong: Option, +} + +#[derive(Debug, Default, serde::Deserialize)] +struct ThemeDanger { + base: Option, + weak: Option, + strong: Option, +} + +#[derive(Debug, Default, serde::Deserialize)] +struct ThemePair { + #[serde(with = "color_serde")] + color: Color, + #[serde(with = "color_serde")] + text: Color, +} + +impl From for iced::theme::palette::Pair { + fn from(pair: ThemePair) -> Self { + Self { + color: pair.color, + text: pair.text, + } + } +} + +mod color_serde { + use iced::Color; + use serde::{Deserialize, Deserializer}; + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Ok(String::deserialize(deserializer) + .map(|hex| Color::parse(&hex))? + .unwrap_or(Color::TRANSPARENT)) + } +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..ac9d039 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,48 @@ +pub mod element_name; +pub mod project; +pub mod rendered_element; + +use std::path::PathBuf; + +pub use element_name::ElementName; +use iced::widget::{pane_grid, text_editor}; +use iced::Theme; +use iced_anim::Event; +pub use project::Project; +pub use rendered_element::*; + +use crate::Error; + +#[derive(Debug, Clone)] +pub enum Message { + ToggleTheme(Event), + CopyCode, + SwitchPage(DesignerPage), + EditorAction(text_editor::Action), + RefreshEditorContent, + DropNewElement(ElementName, iced::Point, iced::Rectangle), + HandleNew( + ElementName, + Vec<(iced::advanced::widget::Id, iced::Rectangle)>, + ), + MoveElement(RenderedElement, iced::Point, iced::Rectangle), + HandleMove( + RenderedElement, + Vec<(iced::advanced::widget::Id, iced::Rectangle)>, + ), + PaneResized(pane_grid::ResizeEvent), + PaneClicked(pane_grid::Pane), + PaneDragged(pane_grid::DragEvent), + NewFile, + OpenFile, + FileOpened(Result<(PathBuf, Project), Error>), + SaveFile, + SaveFileAs, + FileSaved(Result), +} + +#[derive(Debug, Clone)] +pub enum DesignerPage { + DesignerView, + CodeView, +} diff --git a/src/types/element_name.rs b/src/types/element_name.rs new file mode 100644 index 0000000..2687673 --- /dev/null +++ b/src/types/element_name.rs @@ -0,0 +1,85 @@ +use serde::{Deserialize, Serialize}; + +use super::rendered_element::{ + button, column, container, image, row, svg, text, Action, RenderedElement, +}; +use crate::Error; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum ElementName { + Text(String), + Button(String), + Svg(String), + Image(String), + Container, + Row, + Column, +} + +impl ElementName { + pub const ALL: &'static [Self; 7] = &[ + Self::Text(String::new()), + Self::Button(String::new()), + Self::Svg(String::new()), + Self::Image(String::new()), + Self::Container, + Self::Row, + Self::Column, + ]; + + pub fn handle_action( + &self, + element_tree: Option<&mut RenderedElement>, + action: Action, + ) -> Result, Error> { + let element = match self { + Self::Text(_) => text(""), + Self::Button(_) => button(""), + Self::Svg(_) => svg(""), + Self::Image(_) => image(""), + Self::Container => container(None), + Self::Row => row(None), + Self::Column => column(None), + }; + match action { + Action::Stop | Action::Drop => Ok(None), + Action::AddNew => Ok(Some(element)), + Action::PushFront(id) => { + element_tree + .ok_or("the action was of kind `PushFront`, but no element tree was provided.")? + .find_by_id(id) + .ok_or(Error::NonExistentElement)? + .push_front(&element); + Ok(None) + } + Action::InsertAfter(parent_id, child_id) => { + element_tree + .ok_or( + "the action was of kind `InsertAfter`, but no element tree was provided.", + )? + .find_by_id(parent_id) + .ok_or(Error::NonExistentElement)? + .insert_after(child_id, &element); + Ok(None) + } + } + } +} + +impl std::fmt::Display for ElementName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Text(_) => "Text", + Self::Button(_) => "Button", + Self::Svg(_) => "SVG", + Self::Image(_) => "Image", + Self::Container => "Container", + Self::Row => "Row", + Self::Column => "Column", + } + ) + } +} diff --git a/src/types/project.rs b/src/types/project.rs new file mode 100644 index 0000000..27c576b --- /dev/null +++ b/src/types/project.rs @@ -0,0 +1,165 @@ +use std::path::{Path, PathBuf}; + +extern crate fxhash; +use fxhash::FxHashMap; +use iced::Theme; +use rust_format::{Edition, Formatter, RustFmt}; +use serde::{Deserialize, Serialize}; + +use super::rendered_element::RenderedElement; +use crate::config::Config; +use crate::theme::{theme_from_str, theme_index, theme_to_string}; +use crate::Error; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Project { + pub title: Option, + pub theme: Option, + pub element_tree: Option, + #[serde(skip)] + theme_cache: FxHashMap, +} + +impl Default for Project { + fn default() -> Self { + Self::new() + } +} + +impl Project { + pub fn new() -> Self { + Self { + title: None, + theme: None, + element_tree: None, + theme_cache: FxHashMap::default(), + } + } + + pub fn get_theme(&self, config: &Config) -> Theme { + match &self.theme { + Some(theme) => theme_from_str(Some(config), theme), + None => Theme::default(), + } + } + + fn theme_code(&mut self, theme: &Theme) -> String { + let theme_name = theme.to_string(); + if theme_index(&theme_name, Theme::ALL).is_none() { + (*self + .theme_cache + .entry(theme_name) + .or_insert(theme_to_string(theme))) + .to_string() + } else { + theme_name.replace(" ", "") + } + } + + pub async fn from_path( + path: PathBuf, + config: Config, + ) -> Result<(PathBuf, Self), Error> { + let contents = tokio::fs::read_to_string(&path).await?; + let mut project: Self = serde_json::from_str(&contents)?; + + let _ = project.theme_code(&project.get_theme(&config)); + + Ok((path, project)) + } + + pub async fn from_file(config: Config) -> Result<(PathBuf, Self), Error> { + let picked_file = rfd::AsyncFileDialog::new() + .set_title("Open a JSON file...") + .add_filter("*.json, *.JSON", &["json", "JSON"]) + .pick_file() + .await + .ok_or(Error::DialogClosed)?; + + let path = picked_file.path().to_owned(); + + Self::from_path(path, config).await + } + + pub async fn write_to_file( + self, + path: Option, + ) -> Result { + let path = if let Some(p) = path { + p + } else { + rfd::AsyncFileDialog::new() + .set_title("Save to JSON file...") + .add_filter("*.json, *.JSON", &["json", "JSON"]) + .save_file() + .await + .as_ref() + .map(rfd::FileHandle::path) + .map(Path::to_owned) + .ok_or(Error::DialogClosed)? + }; + + let contents = serde_json::to_string(&self)?; + tokio::fs::write(&path, contents).await?; + + Ok(path) + } + + pub fn app_code(&mut self, config: &Config) -> Result { + match self.element_tree { + Some(ref element_tree) => { + let (imports, view) = element_tree.codegen(); + let theme = self.get_theme(config); + let theme_code = self.theme_code(&theme); + let mut theme_imports = ""; + if theme_index(&theme.to_string(), Theme::ALL).is_none() { + if theme_code.contains("Extended") { + theme_imports = "use iced::{{color,theme::{{Palette,palette::{{Extended,Background,Primary,Secondary,Success,Danger,Pair}}}}}};\n"; + } else { + theme_imports = "use iced::{{color,theme::Palette}};\n"; + } + } + + let app_code = format!( + r#"// Automatically generated by iced Builder + use iced::{{widget::{{{imports}}},Element}}; + {theme_imports} + + fn main() -> iced::Result {{ + iced::application("{}", State::update, State::view).theme(State::theme).run() + }} + + #[derive(Default)] + struct State; + + #[derive(Debug, Clone)] + enum Message {{}} + + impl State {{ + fn update(&mut self, _message: Message) {{}} + + fn theme(&self) -> iced::Theme {{ + iced::Theme::{} + }} + + fn view(&self) -> Element {{ + {view}.into() + }} + }}"#, + match self.title { + Some(ref t) => t, + None => "New app", + }, + theme_code + ); + let config = rust_format::Config::new_str() + .edition(Edition::Rust2021) + .option("trailing_comma", "Never") + .option("imports_granularity", "Crate"); + let rustfmt = RustFmt::from_config(config); + Ok(rustfmt.format_str(app_code)?) + } + None => Err("No element tree present".into()), + } + } +} diff --git a/src/types/rendered_element.rs b/src/types/rendered_element.rs new file mode 100755 index 0000000..b001556 --- /dev/null +++ b/src/types/rendered_element.rs @@ -0,0 +1,468 @@ +use std::collections::BTreeMap; + +use iced::advanced::widget::Id; +use iced::{widget, Element, Length}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::ElementName; +use crate::types::Message; +use crate::Error; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RenderedElement { + #[serde(skip, default = "Uuid::new_v4")] + id: Uuid, + child_elements: Option>, + name: ElementName, + options: BTreeMap>, +} + +impl RenderedElement { + fn new(name: ElementName) -> Self { + Self { + id: Uuid::new_v4(), + child_elements: None, + name, + options: BTreeMap::new(), + } + } + + fn with(name: ElementName, child_elements: Vec) -> Self { + Self { + id: Uuid::new_v4(), + child_elements: Some(child_elements), + name, + options: BTreeMap::new(), + } + } + + pub fn get_id(&self) -> Id { + Id::new(self.id.to_string()) + } + + pub fn find_by_id(&mut self, id: &Id) -> Option<&mut Self> { + if &self.get_id() == id { + 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); + if element.is_some() { + return element; + } + } + None + } else { + None + } + } + + pub fn find_parent( + &mut self, + child_element: &RenderedElement, + ) -> Option<&mut Self> { + if child_element == self { + return Some(self); + } else if self.child_elements.is_some() { + if self + .child_elements + .clone() + .unwrap_or_default() + .contains(child_element) + { + return Some(self); + } + if let Some(child_elements) = self.child_elements.as_mut() { + for element in child_elements { + let element = element.find_parent(child_element); + if element.is_some() { + return element; + } + } + } + } + None + } + + pub fn is_parent(&self) -> bool { + self.child_elements.is_some() + } + + pub fn is_empty(&self) -> bool { + self.child_elements == Some(vec![]) + } + + pub fn remove(&mut self, element: &RenderedElement) { + let parent = self.find_parent(element).unwrap(); + if let Some(child_elements) = parent.child_elements.as_mut() { + if let Some(index) = + child_elements.iter().position(|x| x == element) + { + let _ = child_elements.remove(index); + } + } + } + + pub fn push_front(&mut self, element: &RenderedElement) { + if let Some(child_elements) = self.child_elements.as_mut() { + child_elements.insert(0, element.clone()); + } + } + + pub fn insert_after(&mut self, id: &Id, element: &RenderedElement) { + if let Some(child_elements) = self.child_elements.as_mut() { + if let Some(index) = + child_elements.iter().position(|x| &x.get_id() == id) + { + child_elements.insert(index + 1, element.clone()); + } else { + child_elements.push(element.clone()); + } + } + } + + pub fn handle_action( + &self, + element_tree: Option<&mut RenderedElement>, + action: Action, + ) -> Result<(), Error> { + let element_tree = element_tree.unwrap(); + + match action { + Action::Stop => Ok(()), + Action::Drop => { + element_tree.remove(self); + + Ok(()) + } + Action::AddNew => Err( + "the action was of kind `AddNew`, but invoking it on an existing element tree is not possible".into(), + ), + Action::PushFront(id) => { + element_tree.remove(self); + + let new_parent = element_tree.find_by_id(id).unwrap(); + new_parent.push_front(self); + + Ok(()) + } + Action::InsertAfter(parent_id, target_id) => { + element_tree.remove(self); + + let new_parent = element_tree.find_by_id(parent_id).unwrap(); + new_parent.insert_after(target_id, self); + + Ok(()) + } + } + } + + fn preset_options(mut self, options: &[&str]) -> Self { + for opt in options { + let _ = self.options.insert(opt.to_string(), None); + } + self + } + + pub fn option<'a>(mut self, option: &'a str, value: &'a str) -> Self { + let _ = self + .options + .entry(option.to_owned()) + .and_modify(|opt| *opt = Some(value.to_owned())); + self + } + + pub fn into_element<'a>(self) -> Element<'a, Message> { + let mut children = widget::column![]; + + if let Some(els) = self.child_elements.clone() { + for el in els { + children = children.push(el.clone().into_element()); + } + } + iced_drop::droppable( + widget::container( + widget::column![ + widget::text(self.name.clone().to_string()), + children + ] + .width(Length::Fill) + .spacing(10), + ) + .padding(10) + .style(widget::container::bordered_box), + ) + .id(self.get_id()) + .drag_hide(true) + .on_drop(move |point, rect| { + Message::MoveElement(self.clone(), point, rect) + }) + .into() + } + + pub fn codegen(&self) -> (String, String) { + let mut imports = String::new(); + let mut view = String::new(); + let mut options = String::new(); + + for (k, v) in self.options.clone() { + if let Some(v) = v { + options = format!("{options}.{k}({v})"); + } + } + + let mut elements = String::new(); + + if let Some(els) = &self.child_elements { + for element in els { + let (c_imports, children) = element.codegen(); + imports = format!("{imports}{c_imports}"); + elements = format!("{elements}{children},"); + } + } + + match &self.name { + ElementName::Container => { + imports = format!("{imports}container,"); + view = format!("{view}\ncontainer({elements}){options}"); + } + ElementName::Row => { + imports = format!("{imports}row,"); + view = format!("{view}\nrow![{elements}]{options}"); + } + ElementName::Column => { + imports = format!("{imports}column,"); + view = format!("{view}\ncolumn![{elements}]{options}"); + } + ElementName::Text(string) => { + imports = format!("{imports}text,"); + view = format!( + "{view}\ntext(\"{}\"){options}", + if *string == String::new() { + "New Text" + } else { + string + } + ); + } + ElementName::Button(string) => { + imports = format!("{imports}button,"); + view = format!( + "{view}\nbutton(\"{}\"){options}", + if *string == String::new() { + "New Button" + } else { + string + } + ); + } + ElementName::Image(path) => { + imports = format!("{imports}image,"); + view = format!("{view}\nimage(\"{path}\"){options}"); + } + ElementName::Svg(path) => { + imports = format!("{imports}svg,"); + view = format!("{view}\nsvg(\"{path}\"){options}"); + } + } + + (imports, view) + } +} + +impl std::fmt::Display for RenderedElement { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut has_options = false; + f.pad("")?; + f.write_fmt(format_args!("{:?}\n", self.name))?; + f.pad("")?; + f.write_str("Options: (")?; + for (k, v) in &self.options { + if let Some(value) = v { + has_options = true; + f.write_fmt(format_args!( + "\n{:width$.precision$}{}: {}", + "", + k, + value, + width = f.width().unwrap_or(0) + f.precision().unwrap_or(0), + precision = f.precision().unwrap_or(0) + ))?; + } + } + if has_options { + f.write_str("\n")?; + f.pad("")?; + } + f.write_str(")")?; + if let Some(els) = &self.child_elements { + f.write_str(" {\n")?; + for el in els { + f.write_fmt(format_args!( + "\n{:width$.precision$}\n", + el, + width = f.width().unwrap_or(0) + f.precision().unwrap_or(0), + precision = f.precision().unwrap_or(0) + ))?; + } + f.pad("")?; + f.write_str("}")?; + } + Ok(()) + } +} + +impl<'a> From for Element<'a, Message> { + fn from(value: RenderedElement) -> Self { + let child_elements = match value.child_elements { + Some(ref elements) => elements.clone(), + None => vec![], + }; + + let content: Element<'a, Message> = match value.name.clone() { + ElementName::Text(s) => { + if s == String::new() { + widget::text("New Text").into() + } else { + widget::text(s).into() + } + } + ElementName::Button(s) => { + if s == String::new() { + widget::button(widget::text("New Button")).into() + } else { + widget::button(widget::text(s)).into() + } + } + ElementName::Svg(p) => widget::svg(p).into(), + ElementName::Image(p) => widget::image(p).into(), + ElementName::Container => { + widget::container(if child_elements.len() == 1 { + child_elements[0].clone().into() + } else { + Element::from("") + }) + .padding(20) + .into() + } + ElementName::Row => widget::Row::from_vec( + child_elements.into_iter().map(Into::into).collect(), + ) + .padding(20) + .into(), + ElementName::Column => widget::Column::from_vec( + child_elements.into_iter().map(Into::into).collect(), + ) + .padding(20) + .into(), + }; + iced_drop::droppable(content) + .id(value.get_id()) + .drag_hide(true) + .on_drop(move |point, rect| { + Message::MoveElement(value.clone(), point, rect) + }) + .into() + } +} + +#[derive(Debug, Clone)] +pub enum Action<'a> { + AddNew, + PushFront(&'a Id), + InsertAfter(&'a Id, &'a Id), + Drop, + Stop, +} + +impl<'a> Action<'a> { + pub fn new( + ids: &'a [Id], + element_tree: &'a Option, + source_id: Option, + ) -> Self { + let mut action = Self::Stop; + if ids.len() == 1 { + if element_tree.is_none() { + action = Self::AddNew; + } else { + action = Self::Drop; + } + } else { + let id: &Id = match source_id { + Some(id) if ids.contains(&id) => { + let element_id = + &ids[ids.iter().position(|x| *x == id).unwrap()]; + if ids.len() > 2 && &ids[ids.len() - 1] == element_id { + return Self::Stop; + } + element_id + } + _ => ids.last().unwrap(), + }; + let mut element_tree = element_tree.clone().unwrap(); + let element = element_tree.find_by_id(id).unwrap(); + + // Element is a parent and isn't a non-empty container + if (element.is_empty() || !(element.name == ElementName::Container)) + && element.is_parent() + { + action = Self::PushFront(id); + } else if ids.len() > 2 { + let parent = + element_tree.find_by_id(&ids[ids.len() - 2]).unwrap(); + + if parent.name == ElementName::Container + && parent.child_elements != Some(vec![]) + { + action = Self::Stop; + } else { + action = Self::InsertAfter( + &ids[ids.len() - 2], + &ids[ids.len() - 1], + ); + } + } + } + action + } +} + +pub fn text(text: &str) -> RenderedElement { + RenderedElement::new(ElementName::Text(text.to_owned())).preset_options(&[ + "size", + "line_height", + "width", + "height", + ]) +} + +pub fn button(text: &str) -> RenderedElement { + RenderedElement::new(ElementName::Button(text.to_owned())) +} + +pub fn svg(path: &str) -> RenderedElement { + RenderedElement::new(ElementName::Svg(path.to_owned())) +} + +pub fn image(path: &str) -> RenderedElement { + RenderedElement::new(ElementName::Image(path.to_owned())) +} + +pub fn container(content: Option) -> RenderedElement { + match content { + Some(el) => RenderedElement::with(ElementName::Container, vec![el]), + None => RenderedElement::with(ElementName::Container, vec![]), + } +} + +pub fn row(child_elements: Option>) -> RenderedElement { + RenderedElement::with(ElementName::Row, child_elements.unwrap_or_default()) +} + +pub fn column(child_elements: Option>) -> RenderedElement { + RenderedElement::with( + ElementName::Column, + child_elements.unwrap_or_default(), + ) +} diff --git a/src/widget.rs b/src/widget.rs new file mode 100644 index 0000000..ed2073a --- /dev/null +++ b/src/widget.rs @@ -0,0 +1,21 @@ +use iced::widget::{container, text, tooltip}; +use iced::Element; + +pub mod tip { + pub use super::tooltip::Position; +} + +pub fn tip<'a, Message: 'a>( + target: impl Into>, + tip: &'a str, + position: tip::Position, +) -> Element<'a, Message> { + tooltip( + target, + container(text(tip).size(14)) + .padding(5) + .style(container::rounded_box), + position, + ) + .into() +} -- cgit v1.2.3