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 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