This commit is contained in:
Tsuki 2024-04-23 11:51:16 +08:00
parent 4418ee0e4b
commit cb43165e53
312 changed files with 152 additions and 59917 deletions

2
.idea/.gitignore generated vendored
View File

@ -6,3 +6,5 @@
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# GitHub Copilot persisted chat sessions
/copilot/chatSessions

234
Cargo.lock generated
View File

@ -129,23 +129,22 @@ checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
[[package]]
name = "cairo-rs"
version = "0.17.10"
version = "0.19.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab3603c4028a5e368d09b51c8b624b9a46edcd7c3778284077a6125af73c9f0a"
checksum = "b2ac2a4d0e69036cf0062976f6efcba1aaee3e448594e6514bb2ddf87acce562"
dependencies = [
"bitflags 1.3.2",
"bitflags 2.4.2",
"cairo-sys-rs",
"glib",
"libc",
"once_cell",
"thiserror",
]
[[package]]
name = "cairo-sys-rs"
version = "0.17.10"
version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "691d0c66b1fb4881be80a760cb8fe76ea97218312f9dfe2c9cc0f496ca279cb1"
checksum = "fd3bb3119664efbd78b5e6c93957447944f16bdbced84c17a9f41c7829b81e64"
dependencies = [
"glib-sys",
"libc",
@ -406,14 +405,13 @@ dependencies = [
[[package]]
name = "flume"
version = "0.10.14"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577"
checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
dependencies = [
"futures-core",
"futures-sink",
"nanorand",
"pin-project",
"spin",
]
@ -529,23 +527,21 @@ dependencies = [
[[package]]
name = "gdk-pixbuf"
version = "0.17.10"
version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "695d6bc846438c5708b07007537b9274d883373dd30858ca881d7d71b5540717"
checksum = "f6a23f8a0b5090494fd04924662d463f8386cc678dd3915015a838c1a3679b92"
dependencies = [
"bitflags 1.3.2",
"gdk-pixbuf-sys",
"gio",
"glib",
"libc",
"once_cell",
]
[[package]]
name = "gdk-pixbuf-sys"
version = "0.17.10"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9285ec3c113c66d7d0ab5676599176f1f42f4944ca1b581852215bf5694870cb"
checksum = "3dcbd04c1b2c4834cc008b4828bc917d062483b88d26effde6342e5622028f96"
dependencies = [
"gio-sys",
"glib-sys",
@ -556,11 +552,10 @@ dependencies = [
[[package]]
name = "gdk4"
version = "0.6.3"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3abf96408a26e3eddf881a7f893a1e111767137136e347745e8ea6ed12731ff"
checksum = "9100b25604183f2fd97f55ef087fae96ab4934d7215118a35303e422688e6e4b"
dependencies = [
"bitflags 1.3.2",
"cairo-rs",
"gdk-pixbuf",
"gdk4-sys",
@ -572,9 +567,9 @@ dependencies = [
[[package]]
name = "gdk4-sys"
version = "0.6.3"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc92aa1608c089c49393d014c38ac0390d01e4841e1fedaa75dbcef77aaed64"
checksum = "d0b76874c40bb8d1c7d03a7231e23ac75fa577a456cd53af32ec17ec8f121626"
dependencies = [
"cairo-sys-rs",
"gdk-pixbuf-sys",
@ -617,11 +612,10 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "gio"
version = "0.17.10"
version = "0.19.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6973e92937cf98689b6a054a9e56c657ed4ff76de925e36fc331a15f0c5d30a"
checksum = "3f91a0518c2ec539f099d3f945ab2d6a83ec372a9ef40a21906343b191182845"
dependencies = [
"bitflags 1.3.2",
"futures-channel",
"futures-core",
"futures-io",
@ -629,7 +623,6 @@ dependencies = [
"gio-sys",
"glib",
"libc",
"once_cell",
"pin-project-lite",
"smallvec",
"thiserror",
@ -637,15 +630,15 @@ dependencies = [
[[package]]
name = "gio-sys"
version = "0.17.10"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ccf87c30a12c469b6d958950f6a9c09f2be20b7773f7e70d20b867fdf2628c3"
checksum = "bcf8e1d9219bb294636753d307b030c1e8a032062cba74f493c431a5c8b81ce4"
dependencies = [
"glib-sys",
"gobject-sys",
"libc",
"system-deps",
"winapi",
"windows-sys 0.52.0",
]
[[package]]
@ -691,11 +684,11 @@ dependencies = [
[[package]]
name = "glib"
version = "0.17.10"
version = "0.19.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fad45ba8d4d2cea612b432717e834f48031cd8853c8aaf43b2c79fec8d144b"
checksum = "ae1407b2ce171e654720be10d57d4054d3ff2f10a13d5b37e6819b41439832f7"
dependencies = [
"bitflags 1.3.2",
"bitflags 2.4.2",
"futures-channel",
"futures-core",
"futures-executor",
@ -707,7 +700,6 @@ dependencies = [
"gobject-sys",
"libc",
"memchr",
"once_cell",
"smallvec",
"thiserror",
]
@ -720,24 +712,22 @@ checksum = "7a65d79efe318ef2cbbbb37032b125866fd82c34ea44c816132621bbc552e716"
[[package]]
name = "glib-macros"
version = "0.17.10"
version = "0.19.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eca5c79337338391f1ab8058d6698125034ce8ef31b72a442437fa6c8580de26"
checksum = "d8bba315e8ce8aa59631545358450f4962557e89b5f7db7442e7153b47037f71"
dependencies = [
"anyhow",
"heck",
"proc-macro-crate",
"proc-macro-error",
"heck 0.5.0",
"proc-macro-crate 3.1.0",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 2.0.50",
]
[[package]]
name = "glib-sys"
version = "0.17.10"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d80aa6ea7bba0baac79222204aa786a6293078c210abe69ef1336911d4bdc4f0"
checksum = "630f097773d7c7a0bb3258df4e8157b47dc98bbfa0e60ad9ab56174813feced4"
dependencies = [
"libc",
"system-deps",
@ -745,9 +735,9 @@ dependencies = [
[[package]]
name = "gobject-sys"
version = "0.17.10"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd34c3317740a6358ec04572c1bcfd3ac0b5b6529275fae255b237b314bb8062"
checksum = "c85e2b1080b9418dd0c58b498da3a5c826030343e0ef07bde6a955d28de54979"
dependencies = [
"glib-sys",
"libc",
@ -756,9 +746,9 @@ dependencies = [
[[package]]
name = "graphene-rs"
version = "0.17.10"
version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "def4bb01265b59ed548b05455040d272d989b3012c42d4c1bbd39083cb9b40d9"
checksum = "99e4d388e96c5f29e2b2f67045d229ddf826d0a8d6d282f94ed3b34452222c91"
dependencies = [
"glib",
"graphene-sys",
@ -767,9 +757,9 @@ dependencies = [
[[package]]
name = "graphene-sys"
version = "0.17.10"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1856fc817e6a6675e36cea0bd9a3afe296f5d9709d1e2d3182803ac77f0ab21d"
checksum = "236ed66cc9b18d8adf233716f75de803d0bf6fc806f60d14d948974a12e240d0"
dependencies = [
"glib-sys",
"libc",
@ -815,7 +805,7 @@ checksum = "f290ecfa3bea3e8a157899dc8a1d96ee7dd6405c18c8ddd213fc58939d18a0e9"
dependencies = [
"graphql-introspection-query",
"graphql-parser",
"heck",
"heck 0.4.1",
"lazy_static",
"proc-macro2",
"quote",
@ -837,11 +827,10 @@ dependencies = [
[[package]]
name = "gsk4"
version = "0.6.3"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f01ef44fa7cac15e2da9978529383e6bee03e570ba5bf7036b4c10a15cc3a3c"
checksum = "c65036fc8f99579e8cb37b12487969b707ab23ec8ab953682ff347cbd15d396e"
dependencies = [
"bitflags 1.3.2",
"cairo-rs",
"gdk4",
"glib",
@ -853,9 +842,9 @@ dependencies = [
[[package]]
name = "gsk4-sys"
version = "0.6.3"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c07a84fb4dcf1323d29435aa85e2f5f58bef564342bef06775ec7bd0da1f01b0"
checksum = "bd24c814379f9c3199dc53e52253ee8d0f657eae389ab282c330505289d24738"
dependencies = [
"cairo-sys-rs",
"gdk4-sys",
@ -869,11 +858,10 @@ dependencies = [
[[package]]
name = "gtk4"
version = "0.6.6"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b28a32a04cd75cef14a0983f8b0c669e0fe152a0a7725accdeb594e2c764c88b"
checksum = "aa82753b8c26277e4af1446c70e35b19aad4fb794a7b143859e7eeb9a4025d83"
dependencies = [
"bitflags 1.3.2",
"cairo-rs",
"field-offset",
"futures-channel",
@ -886,18 +874,17 @@ dependencies = [
"gtk4-macros",
"gtk4-sys",
"libc",
"once_cell",
"pango",
]
[[package]]
name = "gtk4-macros"
version = "0.6.6"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a4d6b61570f76d3ee542d984da443b1cd69b6105264c61afec3abed08c2500f"
checksum = "40300bf071d2fcd4c94eacc09e84ec6fe73129d2ceb635cf7e55b026b5443567"
dependencies = [
"anyhow",
"proc-macro-crate",
"proc-macro-crate 3.1.0",
"proc-macro-error",
"proc-macro2",
"quote",
@ -906,9 +893,9 @@ dependencies = [
[[package]]
name = "gtk4-sys"
version = "0.6.3"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f8283f707b07e019e76c7f2934bdd4180c277e08aa93f4c0d8dd07b7a34e22f"
checksum = "0db1b104138f087ccdc81d2c332de5dd049b89de3d384437cc1093b17cd2da18"
dependencies = [
"cairo-sys-rs",
"gdk-pixbuf-sys",
@ -925,14 +912,12 @@ dependencies = [
[[package]]
name = "gvdb"
version = "0.4.2"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7139233c0ecb66f285c47a3c1c02b35c8d52a42ca4c7448d0163e5637bb4bd3"
checksum = "0bb9136c388a1e7b3017d18fe7c2f263b0a2b13f215c48e8eb44935d413ce0f9"
dependencies = [
"byteorder",
"flate2",
"lazy_static",
"memmap2",
"quick-xml",
"safe-transmute",
"serde",
@ -972,6 +957,12 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.3.6"
@ -1153,9 +1144,10 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libadwaita"
version = "0.4.4"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91b4990248b9e1ec5e72094a2ccaea70ec3809f88f6fd52192f2af306b87c5d9"
dependencies = [
"bitflags 1.3.2",
"gdk-pixbuf",
"gdk4",
"gio",
@ -1168,9 +1160,9 @@ dependencies = [
[[package]]
name = "libadwaita-sys"
version = "0.4.4"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4231cb2499a9f0c4cdfa4885414b33e39901ddcac61150bc0bb4ff8a57ede404"
checksum = "23a748e4e92be1265cd9e93d569c0b5dfc7814107985aa6743d670ab281ea1a8"
dependencies = [
"gdk4-sys",
"gio-sys",
@ -1261,15 +1253,6 @@ version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
[[package]]
name = "memmap2"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f49388d20533534cd19360ad3d6a7dadc885944aa802ba3995040c5ec11288c6"
dependencies = [
"libc",
]
[[package]]
name = "memoffset"
version = "0.9.0"
@ -1400,23 +1383,21 @@ dependencies = [
[[package]]
name = "pango"
version = "0.17.10"
version = "0.19.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35be456fc620e61f62dff7ff70fbd54dcbaf0a4b920c0f16de1107c47d921d48"
checksum = "b1264d13deb823cc652f26cfe59afb1ec4b9db2a5bd27c41b738c879cc1bfaa1"
dependencies = [
"bitflags 1.3.2",
"gio",
"glib",
"libc",
"once_cell",
"pango-sys",
]
[[package]]
name = "pango-sys"
version = "0.17.10"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3da69f9f3850b0d8990d462f8c709561975e95f689c1cdf0fecdebde78b35195"
checksum = "f52ef6a881c19fbfe3b1484df5cad411acaaba29dbec843941c3110d19f340ea"
dependencies = [
"glib-sys",
"gobject-sys",
@ -1453,26 +1434,6 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pin-project"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.50",
]
[[package]]
name = "pin-project-lite"
version = "0.2.13"
@ -1501,6 +1462,15 @@ dependencies = [
"toml_edit 0.19.15",
]
[[package]]
name = "proc-macro-crate"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284"
dependencies = [
"toml_edit 0.21.1",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
@ -1555,9 +1525,9 @@ checksum = "d5d8f9aa0e3cbcfaf8bf00300004ee3b72f74770f9cbac93f6928771f613276b"
[[package]]
name = "quick-xml"
version = "0.29.0"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81b9228215d82c7b61490fec1de287136b5de6f5700f6e58ea9ad61a7964ca51"
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
dependencies = [
"memchr",
"serde",
@ -1623,9 +1593,10 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "relm4"
version = "0.6.2"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e0e187b58db367305e8486d3228158251da1c8ba1e18baa9de61894e822649"
dependencies = [
"async-trait",
"flume",
"fragile",
"futures",
@ -1639,17 +1610,21 @@ dependencies = [
[[package]]
name = "relm4-icons"
version = "0.6.0"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e28bcc718a587bcfa31b034e0b8f4efe5b70e945b7de9d7d154b45357a0dadc"
checksum = "8603f50e9ed5ca2e3759a9c6033e4058c7b984f1bd22b1fc3b1a162c5612eb64"
dependencies = [
"gtk4",
"gvdb",
"serde",
"toml",
]
[[package]]
name = "relm4-macros"
version = "0.6.2"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0774e846889823aa5766f5b62cface3189a5b36280e65b2faaa6df0319da1726"
dependencies = [
"proc-macro2",
"quote",
@ -2001,7 +1976,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331"
dependencies = [
"cfg-expr",
"heck",
"heck 0.4.1",
"pkg-config",
"toml",
"version-compare",
@ -2113,14 +2088,14 @@ dependencies = [
[[package]]
name = "toml"
version = "0.8.10"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290"
checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit 0.22.6",
"toml_edit 0.22.12",
]
[[package]]
@ -2145,9 +2120,20 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.22.6"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6"
checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
dependencies = [
"indexmap",
"toml_datetime",
"winnow 0.5.40",
]
[[package]]
name = "toml_edit"
version = "0.22.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef"
dependencies = [
"indexmap",
"serde",
@ -2349,9 +2335,9 @@ checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
[[package]]
name = "walkdir"
version = "2.4.0"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
@ -2656,9 +2642,9 @@ dependencies = [
[[package]]
name = "zvariant"
version = "3.15.0"
version = "3.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44b291bee0d960c53170780af148dca5fa260a63cdd24f1962fa82e03e53338c"
checksum = "4eef2be88ba09b358d3b58aca6e41cd853631d44787f319a1383ca83424fb2db"
dependencies = [
"byteorder",
"libc",
@ -2669,11 +2655,11 @@ dependencies = [
[[package]]
name = "zvariant_derive"
version = "3.15.0"
version = "3.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "934d7a7dfc310d6ee06c87ffe88ef4eca7d3e37bb251dece2ef93da8f17d8ecd"
checksum = "37c24dc0bed72f5f90d1f8bb5b07228cbf63b3c6e9f82d82559d4bae666e7ed9"
dependencies = [
"proc-macro-crate",
"proc-macro-crate 1.3.1",
"proc-macro2",
"quote",
"syn 1.0.109",

View File

@ -6,11 +6,16 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
gtk = { version = "0.6.6", package = "gtk4", features = ["v4_8"] }
libadwaita = {package="libadwaita", version = "0.4.4", path="./libadwaita-0.4.4", features = ["v1_4"]}
#gtk = { version = "0.6.6", package = "gtk4", features = ["v4_8"] }
gtk = {version = "0.8.1",package = "gtk4", features = ["v4_12"]}
adw = {version = "0.6.0", package = "libadwaita", features = ["v1_4"]}
#libadwaita = {package="libadwaita", version = "0.4.4", path="./libadwaita-0.4.4", features = ["v1_4"]}
# relm4 = { version = "0.6.2", features = ["libadwaita"]}
relm4 = {package = "relm4", version="0.6.2", path="./Relm4-0.6.2/relm4", features=["libadwaita"]}
relm4-icons = { version = "0.6.0", features = ["add-filled"] }
#relm4 = {package = "relm4", version="0.6.2", path="./Relm4-0.6.2/relm4", features=["libadwaita"]}
relm4 = { version = "0.8.1", features = ["libadwaita"]}
relm4-icons = "0.8.2"
#relm4-icons = { version = "0.6.0", features = ["add-filled"] }
chrono = "0.4.34"
tracker = "0.2.1"
git2 = "0.18.2"

View File

@ -1,558 +0,0 @@
# Changelog
## Unreleased
## 0.6.2 - 2023-9-27
### Fixed
+ macros: Add support for accessing nested template children
## 0.6.1 - 2023-8-9
### Added
+ core: Add `TypedColumnView` as a typed wrapper for `gtk::ColumnView`
+ core: Add `set_margin_vertical` and `set_margin_horizontal` to RelmWidgetExt
### Fixed
+ core: Don't panic when dropping components from asynchronous contexts
+ core: Fix an issue with using `connect_open` on `gtk::Application`
+ core: Use GNOME 42 as baseline feature to help with Ubuntu 22.04
+ core: Fix compiler error when not using the "macros" feature
+ macros: Allow trailing commas in view!
+ macros: Allow multiple instances of the same template children
+ components: Disable default features of relm4
+ examples: Fix libadwaita tab examples
### Changed
+ examples: Add a separator to the libadwaita leaflet example.
## 0.6.0 - 2023-5-31
### Added
+ core: Implemented `RelmRemoveExt` for `adw::ExpanderRow`.
+ core: Implemented `ContainerChild` for `adw::ExpanderRow`.
+ core: Add `TypedListView` as idiomatic wrapper over `gtk::ListView`
### Fixed
+ macros: Improve error messages for non-identifier parameter patterns
## 0.6.0-beta.1 - 2023-4-19
### Added
+ core: Introduce setting and action safeties
+ core: Implement `RelmSetChildExt` for `gtk::AspectFrame`
+ core: Add `FactoryHashMap` as alternative to `FactoryVecDeque`
+ core: Add gnome_44 feature flag for GNOME 44
+ core: Documentation and better support for data bindings
+ core: Add `set_tooltip` method to `RelmWidgetExt`
+ core: Add `main_adw_application` method to retrieve the `adw::Application` when the libadwaita feature is enabled
+ macros: Add `skip_init` option for watch and track attributes to skip their initialization
+ examples: Introduce setting and action safeties
+ examples: Example for using relm4-icons
### Changed
+ core: Replace `FactoryVecDeque`'s associated function `from_vec` with `from_iter`
+ core: Added `Index` type to the `FactoryComponent` trait
+ core: Rename factory component traits `output_to_parent_input` method to `forward_to_parent`
+ core: Improved `RelmActionGroup` API
+ all: Increase MSRV to 1.65 to match the dependencies
### Fixed
+ all: Fix doc links
+ core: Improve worker docs
### Removed
+ core: Remove the deprecated `send!` macro
## 0.6.0-alpha.2 - 2023-3-13
### Added
+ core: Add data bindings
+ core: Implement `FactoryView` for `adw::Leaflet`
+ components: WebImage as component for easily loading images from the web
### Fixed
+ macros: Support template children of templates that are root widgets
+ macros: Fix order of assignment and connect statements
## 0.6.0-alpha.1 - 2023-2-29
### Added
+ core: Add `RelmApp` builder methods `with_args` and `with_broker`
+ core: Add `MessageBroker` support for `AsyncComponent`
### Changed
+ core: Change `MessageBroker` generic type parameter to message type
+ core: Rename `RelmApp` method `with_app` to `from_app`
+ core: Deprecate `RelmApp` methods `run_with_args` and `run_async_with_args`
## 0.5.0 - 2023-2-17
### Added
+ core: Implement `RelmSetChildExt` for `gtk::Expander`
+ macros: Support submenus in menu! macro
+ macros: Support widget templates as root widgets of components and factories
+ macros: Implement `Clone` for widget templates
### Changed
+ all: Use docs.rs to host the documentation. The documentation on the website will be deprecated.
### Fixed
+ core: Call shutdown on components even on application shutdown
+ core: Clearing and dropping a factory properly calls the shutdown method of its elements
+ components: Fix doc links to examples on GitHub
+ macros: Fix panic on incorrect root type syntax
+ macros: Fix incorrect type generation for generics
+ macros: Allow mutably root widgets
## 0.5.0-rc.2 - 2023-2-5
### Changed
+ core: Increase MSRV to 1.64 to match the gtk4 crate
+ all: Move examples into the corresponding crates
### Added
+ core: Add `broadcast` to `FactoryVecDeque`
### Fixed
+ core: Don't crash when the application is started multiple times
+ core: Support tokio also on local futures
+ core: Prevent leaking `CommandSenderInner` struct
+ core: Improve error message when sending input messages to dropped components
+ core: Fix scaping of examples on docs.rs
+ core: Fix crash caused by UID overflow with very large or frequently changing factories
+ macros: Fix clippy warning triggered by the view macro in some edge cases
+ macros: Import `relm4::ComponentSender` isnt longer required
## 0.5.0-rc.1 - 2022-12-21
### Added
+ core: Add `try_read` and `try_write` methods to `SharedState`
+ core: Allow initializing `FactoryVecDeque` from a `Vec` and make it cloneable
+ core: Support factories with `adw::PreferencePage`
### Changed
+ core: Pass `&self` to the `Position::position()` function for positioning widgets
+ core: Take `&str` instead of `&[u8]` in `set_global_css()`
+ macros: Allow expressions for names of menu entries
### Fixed
+ core: Initialize GTK when calling `RelmApp::new()`
## 0.5.0-beta.5 - 2022-11-26
### Added
+ core: Add asynchronous components including macro support
+ core: Add asynchronous factories including macro support
+ core: Temporary widget initialization for async components and factories
+ core: Add `LoadingWidgets` to help with temporary loading widgets in async factories and components
+ core: Add `Reducer` as message based alternative to `SharedState`
+ core: Synchronous API for commands
+ core: Remove async-broadcast dependency
+ core: Runtimes of `Component`s and `AsyncComponents` can now be detached for a static lifetime
+ core: Add `ComponentStream` as alternative to `Controller` that implements `Stream` for async message handling
+ core: Add `gnome_42` and `gnome_43` feature flags
+ core: Implement `RelmContainerExt` for `adw::Squeezer`
+ core: Implement `RelmSetChildExt` for `gtk::WindowHandle`
+ macros: Auto-generate the name of the `Widgets` type if possible
### Changed
+ core: Rename `FactoryComponentSender` to `FactorySender` and `AsyncFactoryComponentSender` to `AsyncFactorySender`
+ core: The sender API now supports proper error handling
+ core: Pass `Root` during `update` and `update_cmd` for `Component` and `AsyncComponent`
+ core: Rename `OnDestroy` to `RelmObjectExt`
+ core: Remove `EmptyRoot` in favor of the unit type
+ macros: Allow using methods calls as widget initializers in the `view` macro
+ macros: Explicitly using `visibility` as attribute name is no longer supported
### Fixed
+ all: Fix doc builds on docs.rs and add a CI job to prevent future doc failures
+ core: Fix various bugs related to component shutdown
+ core: `shutdown` on `Component` now works as expected
+ core: `shutdown` on `FactoryComponent` now works as expected
+ core: `transient_for` on `ComponentBuilder` now works properly when called after the application has been initialized
+ macros: Mark template children of public widget templates as public as well
+ macros: Only get template children in scope when they are actually used
+ macros: Fix type parsing after arrow operator in widget assignments
## 0.5.0-beta.4 - 2022-10-24
### Added
+ core: Added `dox` feature to be able to build the docs without the dependencies
+ core: Added widget templates
+ core: Allow changing the priority of event loops of components
+ core: Impl `ContainerChild` and `RelmSetChildExt` for `adw::ToastOverlay`
+ components: Added `dox` feature to be able to build the docs without the dependencies
+ examples: Add libadwaita Leaflet sidebar example
+ examples: Port entry, actions and popover examples to 0.5
### Changed
+ core: Improved `DrawHandler`
+ core: Made the `macros` feature a default feature
+ core: Remove async-oneshot dependency and replace it with tokio's oneshot channel
+ core: Remove WidgetPlus in favor of RelmWidgetExt
+ core: Add convenience getter-methods to Controller
+ core: `add_action` of `RelmActionGroup` now takes a reference to a `RelmAction` as a parameter
+ examples: Many improvements
+ macros: `parse_with_path`, `update_stream`, `inject_view_code` and `generate_tokens` take references for some of their parameters
+ artwork: Update logo
### Fixed
+ macros: Fix usage of RelmContainerExt with local_ref attribute
+ macros: Report RelmContainerExt error at the correct span
## 0.5.0-beta.3 - 2022-9-28
### Added
+ core: Add `iter_mut` to `FactoryVecDeque`
+ core: Impl extension traits and `FactoryView` for `adw::PreferencesGroup`
+ core: Add a `prelude` module that contains commonly imported traits and types
+ core: Implement RelmContainerExt for Leaflet, Carousel and TabView
+ core: Add `iter()` method to `FactoryVecDeque`
+ core: Add getter for global application to simplify graceful shutdown of applications
+ core: Add MessageBroker type to allow communication between components on different levels
+ core: Return a clone of the `DynamicIndex` after inserting into a factory
+ macros: Add shorthand syntax for simple input messages
+ macros: Add chain attribute for properties
+ components: Add `SimpleComboBox` type as a more idiomatic wrapper around `gtk::ComboBoxText`
+ components: Port `OpenButton` to 0.5
+ book: Many chapters ported to 0.5
### Changed
+ core: Improve `SharedState` interface and prefer method names related to `RwLock`
+ core: Remove Debug requirement for FactoryComponent
+ core: Remove `input` and `output` fields on `ComponentSender` and `FactoryComponentSender` in favor of `input_sender` and `output_sender` methods
+ core: Make `ComponentSender` and `FactoryComponentSender` structs instead of type aliases
+ core: Increase MSRV to 1.63 to match the gtk4 crate
+ core: Rename `ParentMsg` and `output_to_parent_msg` to `ParentInput` and `output_to_parent_input`, respectively.
+ core: Do not call `gtk_init` and `adw_init` in favor of the application startup handler
+ core: Remove `Application` type alias in favor of `gtk::Application`
+ core: Make `app` field on `RelmApp` private
+ core: Use late initialization for transient_for and its native variant
+ core: Rename InitParams to Init in SimpleComponent and Worker too
+ macros: Don't generate dead code in the widgets struct
+ macros: Improve error reporting on invalid trait implementations
### Fixed
+ core: Append children for `gtk::Dialog` to its content area instead of using `set_child`
+ macros: Fix returned widgets assignment in the view macro
### Misc
+ all: Use more clippy lints and clean up the code in general
## 0.5.0-beta.2 - 2022-8-12
### Added
+ core: Add oneshot_command method to ComponentSender
+ core: Implement FactoryView for adw::Carousel
+ components: Complete port to 0.5
+ examples: More examples ported to 0.5
### Changed
+ core: Rename InitParams to Init
+ core: Pass senders by value
+ core: Make factories use FactoryComponentSender instead of individual senders for input and output
+ core: Remove generics from FactoryComponent
### Fixed
+ macros: Fix unsoundness with thread local memory
## 0.5.0-beta.1 - 2022-7-26
### Added
+ core: Introduce commands
### Changed
+ core: The Component trait replaces AppUpdate, ComponentUpdate, AsyncComponentUpdate, MessageHandler, MicroModel, MicroWidgets, Components and Widgets
+ core: Replace FactoryPrototype with FactoryComponent
+ core: Drop FactoryVec and make FactoryVecDeque easier to use
+ core: Improved component initialization and lifecycle
+ macros: Replace iterate, track and watch with attributes
+ macros: Replace args! with only parenthesis
+ macros: Improved macro syntax
+ examples: Many rewrites for the new version
## 0.4.4 - 2022-3-30
### Changed
+ all: Repositories were transferred to the Relm4 organization
### Fixed
+ macros: Don't interpret expr != expr as macro
+ core: Always initialize GTK/Libadwaita before running apps
+ macros: Some doc link fixes
## 0.4.3 - 2022-3-13
### Added
+ core: Add WidgetRef trait to make AsRef easier accessible for widgets
+ macros: Destructure widgets in Widgets::view
### Fixed
+ macros: Use correct widget type in derive macro for components
+ macros: Fix parsing of `property: value == other,` expressions
+ core: Fixed the position type for TabView
+ core: Fixed state changes in FactoryVec (by [V02460](https://github.com/V02460))
+ macros: Parse whole expressions instead of just literals
## 0.4.2 - 2022-2-4
### Added
+ macros: The view macro now allows dereferencing widgets with *
### Fixed
+ core: Fixed clear method of FactoryVec
+ macros: The micro_component macro now parses post_view correctly
+ macros: Fix the ordering of properties in the view macro
+ macros: Fix the ordering of widget assignments in the view macro
## 0.4.1 - 2022-1-17
### Added
+ macros: Improved documentation
### Fixed
+ core: Action macros now include the required traits themselves
+ macros: Allow connecting events in the view macro
## 0.4.0 - 2022-1-16
### Added
+ all: Update gtk4-rs to v0.4
+ core: Introduce the "macro" feature as alternative to using relm4-macros separately
+ macros: Add a macros for MicroComponents and Factories
+ macros: Add a post_view function to execute code after the view code of the macro
+ macros: Allow using the view and menu macros independently from the widget macro
+ macros: Allow using mutable widgets in view
+ macros: Improve error messages for anonymous widgets
### Changed
+ core: Renamed methods of the FactoryPrototype trait to better match with the rest of Relm4
+ macros: manual_view is now called pre_view
+ book: Reworked introduction and first chapter
### Fixed
+ core: Fix panic caused by the clear method of FactoryVecDeque
## 0.4.0-beta.3 - 2021-12-28
### Added
+ core: A factory view implementation for libadwaita's StackView
+ macros: Allow early returns in manual_view (by [euclio](https://github.com/euclio))
### Changed
+ core: Make GTK's command line argument handling optional (by [euclio](https://github.com/euclio))
+ core: DynamicIndex now implements Send but panics when used on other threads
## 0.4.0-beta.2 - 2021-11-26
+ macros: Add optional returned widget syntax
## 0.4.0-beta.1 - 2021-11-21
### Added
+ core: Micro components
+ core: Type safe actions API
+ macros: Menu macro for creating menus
+ macros: New returned widget syntax
+ examples Micro components example
### Changed
+ core: Initialize widgets from the outermost components to the app
+ macros: component! removed and parent! was added instead
### Removed
+ core: RelmComponent::with_new_thread
## 0.2.1 - 2021-10-17
### Added
+ core: Added sender method to RelmComponent
+ macros: New shorthand tracker syntax
+ macros: Allow generic function parameters in properties
### Changed
+ core: Use adw::Application when "libadwaita" feature is active
## 0.2.0 - 2021-10-09
### Changed
+ core: Pass model in connect_components function of the Widgets trait
+ core: Mini rework of factories
+ core: Removed DefaultWidgets trait in favor of Default implementations in gkt4-rs
+ book: Many book improvements by [tronta](https://github.com/tronta)
### Added
+ core: Added with_app method that allows passing an existing gtk::Application to Relm4
+ core: Methods to access the widgets of components
+ core: Re-export for gtk
+ macros: Support named arguments in the widget macro (by [mskorkowski](https://github.com/mskorkowski))
+ macros: Support usage of re-export paths in the widget macro (by [mskorkowski](https://github.com/mskorkowski))
+ macros: Added error message when confusing `=` and `:`
+ macros: Allow usage of visibilities other than pub
+ macros: New pre_connect_components and post_connect_components for manual components code
### Fixed
+ macros: Parsing the first widget should now always work as expected
+ macros: [#20](https://github.com/Relm4/relm4/issues/20) Fix wrong order when using components in the widget macro
## 0.1.0 - 2021-09-06
### Added
+ core: Added message handler type
+ core: More methods for factory data structures
+ macros: Add syntax for connecting events with components
+ examples: Stack example
+ book: Added macro expansion chapter
### Changed
+ book: Added message handler chapter and reworked the threads and async chapter
+ book: Many book improvements by [tronta](https://github.com/tronta)
+ core: The send! macro no longer clones the sender
+ macros: Make fields of public widgets public
+ components: Use &'static str instead of String for configurations
+ examples: Many improvements
### Fixed
+ macros: Use fully qualified syntax for factories
+ macros: Passing additional arguments now works for components and other properties, too.
## 0.1.0-beta.9 - 2021-08-24
### Added
+ components: Open button with automatic recent files list
+ components: Removed trait duplication and added more docs
+ core: Iterators added to factory data structures
+ core: More widgets added as FactoryView
### Changed
+ book: Many book improvements by [tronta](https://github.com/tronta)
+ core: Removed class name methods from WidgetPlus [#14](https://github.com/Relm4/relm4/pull/14)
### Fixed
+ macros: Parsing additional fields should be more stable now
+ macros: Widgets can not include comments at the top
## 0.1.0-beta.8 - 2021-08-20
### Added
+ core: Support for libadwaita 🎉
+ macros: Fully qualified syntax for trait disambiguation
+ macros: Allow passing additional arguments to widget initialization (useful e.g. for grids)
+ book: Reusable components and widget macro reference chapters
### Changed
+ macros: Improved error messages
## 0.1.0-beta.7 - 2021-08-19
### Added
+ book: Factory, components, worker and thread + async chapters
### Changed
+ core: get and get_mut of FactoryVec and FactoryVecDeque now return an Option to prevent panics
### Fixed
+ macros: Fixed components
+ core: Fixed unsound removal of elements in FactoryVecDeque
## 0.1.0-beta.6 - 2021-08-18
### Changed
+ core: Improved and adjusted the FactoryPrototype trait
### Added
+ core: Added the FactoryListView trait for more flexibility
+ core: Added a FactoryVecDeque container
+ core: Implemented FactoryView and FactoryListView for more widgets
+ examples: More examples
### Fixed
+ macros: Fixed the factory! macro
## 0.1.0-beta.5 - 2021-08-15
### Added
+ core: Drawing handler for gtk::DrawingArea
+ core: New CSS methods in WidgetPlus trait
+ examples: Many new examples
### Changed
+ core: Many doc improvements
+ macros: Improved tracker! macro

View File

@ -1,26 +0,0 @@
[workspace]
resolver = "2"
members = [
"relm4",
"relm4-components",
"relm4-macros",
]
exclude = [
"examples",
"examples/libadwaita",
]
[workspace.package]
version = "0.6.2"
authors = ["Aaron Erhardt <aaron.erhardt@t-online.de>"]
edition = "2021"
rust-version = "1.65"
readme = "README.md"
license = "Apache-2.0 OR MIT"
description = "An idiomatic GUI library inspired by Elm and based on gtk4-rs"
homepage = "https://relm4.org"
repository = "https://github.com/Relm4/Relm4"
keywords = ["gui", "gtk", "gtk4", "elm"]
categories = ["gui"]

View File

@ -1,176 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View File

@ -1,23 +0,0 @@
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.

View File

@ -1,191 +0,0 @@
<h1>
<a href="https://relm4.org">
<img src="assets/Relm_logo_with_text.png" width="200" alt="Relm4">
</a>
</h1>
[![CI](https://github.com/Relm4/Relm4/actions/workflows/rust.yml/badge.svg)](https://github.com/Relm4/Relm4/actions/workflows/rust.yml)
[![Matrix](https://img.shields.io/matrix/relm4:matrix.org?label=matrix%20chat)](https://matrix.to/#/#relm4:matrix.org)
[![Relm4 on crates.io](https://img.shields.io/crates/v/relm4.svg)](https://crates.io/crates/relm4)
[![Relm4 docs](https://img.shields.io/badge/rust-documentation-blue)](https://docs.rs/relm4/)
[![Relm4 book](https://img.shields.io/badge/rust-book-fc0060)](https://relm4.org/book/stable/)
![Minimum Rust version 1.65](https://img.shields.io/badge/rustc-1.65+-06a096.svg)
[![dependency status](https://deps.rs/repo/github/Relm4/Relm4/status.svg)](https://deps.rs/repo/github/Relm4/Relm4)
An idiomatic GUI library inspired by [Elm](https://elm-lang.org/) and based on [gtk4-rs](https://crates.io/crates/gtk4).
Relm4 is a new version of [relm](https://github.com/antoyo/relm) that's built from scratch and is compatible with [GTK4](https://www.gtk.org/) and [libadwaita](https://gitlab.gnome.org/GNOME/libadwaita).
## Why Relm4
We believe that GUI development should be easy, productive and delightful.
The [gtk4-rs](https://crates.io/crates/gtk4) crate already provides everything you need to write modern, beautiful and cross-platform applications.
Built on top of this foundation, Relm4 makes developing more idiomatic, simpler and faster and enables you to become productive in just a few hours.
## Our goals
+ ⏱️ **Productivity**
+ ✨ **Simplicity**
+ 📎 **Outstanding documentation**
+ 🔧 **Maintainability**
## Documentation
+ 📖 **[The Relm4 book](https://relm4.org/book/stable/)**
+ 📜 **[Rust documentation](https://docs.rs/relm4/)**
## Dependencies
Relm4 depends on GTK4: [How to install GTK4 and Rust](https://gtk-rs.org/gtk4-rs/git/book/installation.html)
## Ecosystem
+ [relm4-macros](https://crates.io/crates/relm4-macros) - several macros for declarative UI definitions.
+ [relm4-components](https://crates.io/crates/relm4-components) - a collections of reusable components.
+ [relm4-icons](https://crates.io/crates/relm4-icons) - icons for your application.
+ [relm4-template](https://github.com/Relm4/relm4-template) - a starter template for creating Relm4 applications in the Flatpak package format.
+ [relm4-snippets](https://github.com/Relm4/vscode-relm4-snippets) - code snippets to speed up your development.
Use this in to your `Cargo.toml`:
```toml
# Core library
relm4 = "0.6.2"
# Optional: reusable components
relm4-components = "0.6.2"
# Optional: icons
relm4-icons = { version = "0.6.0", features = ["plus"] }
```
### Features
The `relm4` crate has four feature flags:
| Flag | Purpose | Default |
| :--- | :------ | :-----: |
| `macros` | Enable macros by re-exporting [`relm4-macros`](https://crates.io/crates/relm4-macros) | ✅ |
| `libadwaita` | Improved support for [libadwaita](https://gitlab.gnome.org/World/Rust/libadwaita-rs) | - |
| `libpanel` | Improved support for [libpanel](https://gitlab.gnome.org/World/Rust/libpanel-rs) | - |
| `dox` | Linking to the underlying C libraries is skipped to allow building the docs without dependencies | - |
| `gnome_44` | Enable all version feature flags of all dependencies to match the GNOME 44 SDK | - |
| `gnome_43` | Enable all version feature flags of all dependencies to match the GNOME 43 SDK | - |
| `gnome_42` | Enable all version feature flags of all dependencies to match the GNOME 42 SDK | - |
The `macros` feature is a default feature.
## Examples
Several example applications are available at [examples/](examples/).
#### [📸 Screenshots from the example apps](assets/screenshots)
### A simple counter app
![Simple app screenshot light](assets/screenshots/simple-light.png)
![Simple app screenshot dark](assets/screenshots/simple-dark.png)
```rust
use gtk::prelude::*;
use relm4::prelude::*;
struct App {
counter: u8,
}
#[derive(Debug)]
enum Msg {
Increment,
Decrement,
}
#[relm4::component]
impl SimpleComponent for App {
type Init = u8;
type Input = Msg;
type Output = ();
view! {
gtk::Window {
set_title: Some("Simple app"),
set_default_size: (300, 100),
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 5,
set_margin_all: 5,
gtk::Button {
set_label: "Increment",
connect_clicked => Msg::Increment,
},
gtk::Button {
set_label: "Decrement",
connect_clicked => Msg::Decrement,
},
gtk::Label {
#[watch]
set_label: &format!("Counter: {}", model.counter),
set_margin_all: 5,
}
}
}
}
// Initialize the component.
fn init(
counter: Self::Init,
root: &Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = App { counter };
// Insert the code generation of the view! macro here
let widgets = view_output!();
ComponentParts { model, widgets }
}
fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>) {
match msg {
Msg::Increment => {
self.counter = self.counter.wrapping_add(1);
}
Msg::Decrement => {
self.counter = self.counter.wrapping_sub(1);
}
}
}
}
fn main() {
let app = RelmApp::new("relm4.example.simple");
app.run::<App>(0);
}
```
## Projects using Relm4
- [fm](https://github.com/euclio/fm) — A small, general-purpose file manager.
- [Done](https://github.com/edfloreshz/done) - A simple and versatile to do app.
- [Reovim](https://github.com/songww/reovim) - GUI frontend for neovim.
- [NixOS Configuration Editor](https://github.com/vlinkz/nixos-conf-editor) - A graphical configuration editor for [NixOS](https://nixos.org).
- [Rhino Setup](https://github.com/rhino-linux/rhino-setup) - Setup wizard for [Rolling Rhino](https://rhinolinux.org/)
- [Lemoa](https://github.com/lemmy-gtk/lemoa) - Desktop client for Lemmy
## License
Licensed under either of
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.
### Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any
additional terms or conditions.
**Feedback and contributions are highly appreciated!**

View File

@ -1,17 +0,0 @@
#!/bin/bash
# Exit on error
set -e
# Check the code
cargo update
cargo fmt --all -- --check
cargo clippy --all-targets -- --deny warnings
cargo clippy --features "all" -- --deny warnings
cargo clippy --examples -- --deny warnings
cargo test
# Publish and pass all arguments to cargo
cargo publish -p relm4-macros
cargo publish -p relm4
cargo publish -p relm4-components

View File

@ -1,38 +0,0 @@
[package]
name = "relm4-components"
readme = "README.md"
documentation = "https://docs.rs/relm4_components/"
version.workspace = true
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
description.workspace = true
homepage.workspace = true
repository.workspace = true
keywords.workspace = true
categories.workspace = true
[package.metadata.docs.rs]
all-features = true
# enable unstable features in the documentation
rustc-args = ["--cfg", "docsrs"]
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
[dependencies]
once_cell = "1.18"
relm4 = { version = "0.6.2", path = "../relm4", default-features = false, features = ["macros"] }
reqwest = { version = "0.11.18", optional = true }
tracker = "0.2.1"
[features]
default = []
dox = ["relm4/dox", "web"]
web = ["reqwest"]
[[example]]
name = "web_image"
required-features = ["web"]

View File

@ -1,6 +0,0 @@
# Relm4 components
[![Relm4-components on crates.io](https://img.shields.io/crates/v/relm4-components.svg)](https://crates.io/crates/relm4-components)
[![Relm4-components docs](https://img.shields.io/badge/rust-documentation-blue)](https://docs.rs/relm4_components/)
A collection of reusable components that can easily be integrated into Relm4 applications.

View File

@ -1 +0,0 @@
.recent_files

View File

@ -1,148 +0,0 @@
use gtk::prelude::*;
use relm4::{
gtk, Component, ComponentController, ComponentParts, ComponentSender, Controller, RelmApp,
RelmWidgetExt, SimpleComponent,
};
use relm4_components::alert::{Alert, AlertMsg, AlertResponse, AlertSettings};
struct App {
counter: u8,
alert_toggle: bool,
dialog: Controller<Alert>,
second_dialog: Controller<Alert>,
}
#[derive(Debug)]
enum AppMsg {
Increment,
Decrement,
CloseRequest,
Save,
Close,
Ignore,
}
#[relm4::component]
impl SimpleComponent for App {
type Init = ();
type Input = AppMsg;
type Output = ();
view! {
main_window = gtk::ApplicationWindow {
set_title: Some("Alert example"),
set_default_size: (300, 100),
connect_close_request[sender] => move |_| {
sender.input(AppMsg::CloseRequest);
gtk::Inhibit(true)
},
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_margin_all: 5,
set_spacing: 5,
append = &gtk::Button {
set_label: "Increment",
connect_clicked => AppMsg::Increment,
},
append = &gtk::Button {
set_label: "Decrement",
connect_clicked => AppMsg::Decrement,
},
append = &gtk::Label {
set_margin_all: 5,
#[watch]
set_label: &format!("Counter: {}", model.counter),
},
append = &gtk::Button {
set_label: "Close",
connect_clicked => AppMsg::CloseRequest,
},
},
}
}
fn update(&mut self, msg: AppMsg, _sender: ComponentSender<Self>) {
match msg {
AppMsg::Increment => {
self.counter = self.counter.wrapping_add(1);
}
AppMsg::Decrement => {
self.counter = self.counter.wrapping_sub(1);
}
AppMsg::CloseRequest => {
if self.counter == 42 {
relm4::main_application().quit();
} else {
self.alert_toggle = !self.alert_toggle;
if self.alert_toggle {
self.dialog.emit(AlertMsg::Show);
} else {
self.second_dialog.emit(AlertMsg::Show);
}
}
}
AppMsg::Save => {
println!("* Open save dialog here *");
}
AppMsg::Close => {
relm4::main_application().quit();
}
AppMsg::Ignore => (),
}
}
fn init(
_: Self::Init,
root: &Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = App {
counter: 0,
alert_toggle: false,
dialog: Alert::builder()
.transient_for(root)
.launch(AlertSettings {
text: String::from("Do you want to quit without saving? (First alert)"),
secondary_text: Some(String::from("Your counter hasn't reached 42 yet")),
confirm_label: String::from("Close without saving"),
cancel_label: String::from("Cancel"),
option_label: Some(String::from("Save")),
is_modal: true,
destructive_accept: true,
})
.forward(sender.input_sender(), convert_alert_response),
second_dialog: Alert::builder()
.transient_for(root)
.launch(AlertSettings {
text: String::from("Do you want to quit without saving? (Second alert)"),
secondary_text: Some(String::from("Your counter hasn't reached 42 yet")),
confirm_label: String::from("Close without saving"),
cancel_label: String::from("Cancel"),
option_label: Some(String::from("Save")),
is_modal: true,
destructive_accept: true,
})
.forward(sender.input_sender(), convert_alert_response),
};
let widgets = view_output!();
ComponentParts { model, widgets }
}
}
fn convert_alert_response(response: AlertResponse) -> AppMsg {
match response {
AlertResponse::Confirm => AppMsg::Close,
AlertResponse::Cancel => AppMsg::Ignore,
AlertResponse::Option => AppMsg::Save,
}
}
fn main() {
let app = RelmApp::new("relm4.example.alert");
app.run::<App>(());
}

View File

@ -1,176 +0,0 @@
use gtk::prelude::*;
use relm4::{
gtk, Component, ComponentController, ComponentParts, ComponentSender, Controller, RelmApp,
RelmWidgetExt, SimpleComponent,
};
use relm4_components::{open_dialog::*, save_dialog::*};
use std::path::PathBuf;
struct App {
open_dialog: Controller<OpenDialog>,
save_dialog: Controller<SaveDialog>,
buffer: gtk::TextBuffer,
file_name: Option<String>,
message: Option<String>,
}
#[derive(Debug)]
enum Input {
OpenRequest,
OpenResponse(PathBuf),
SaveRequest,
SaveResponse(PathBuf),
ShowMessage(String),
ResetMessage,
Ignore,
}
#[relm4::component]
impl SimpleComponent for App {
type Init = ();
type Input = Input;
type Output = ();
view! {
root = gtk::ApplicationWindow {
set_default_size: (600, 400),
#[watch]
set_title: Some(model.file_name.as_deref().unwrap_or_default()),
#[wrap(Some)]
set_titlebar = &gtk::HeaderBar {
pack_start = &gtk::Button {
set_label: "Open",
connect_clicked => Input::OpenRequest,
},
pack_end = &gtk::Button {
set_label: "Save As",
connect_clicked => Input::SaveRequest,
#[watch]
set_sensitive: model.file_name.is_some(),
}
},
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_margin_all: 5,
gtk::ScrolledWindow {
set_min_content_height: 380,
#[wrap(Some)]
set_child = &gtk::TextView {
set_buffer: Some(&model.buffer),
#[watch]
set_visible: model.file_name.is_some(),
},
},
}
}
}
fn post_view() {
if let Some(text) = &model.message {
let dialog = gtk::MessageDialog::builder()
.text(text)
.use_markup(true)
.transient_for(&widgets.root)
.modal(true)
.buttons(gtk::ButtonsType::Ok)
.build();
dialog.connect_response(|dialog, _| dialog.destroy());
dialog.set_visible(true);
sender.input(Input::ResetMessage);
}
}
fn init(
_: Self::Init,
root: &Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let open_dialog = OpenDialog::builder()
.transient_for_native(root)
.launch(OpenDialogSettings::default())
.forward(sender.input_sender(), |response| match response {
OpenDialogResponse::Accept(path) => Input::OpenResponse(path),
OpenDialogResponse::Cancel => Input::Ignore,
});
let save_dialog = SaveDialog::builder()
.transient_for_native(root)
.launch(SaveDialogSettings::default())
.forward(sender.input_sender(), |response| match response {
SaveDialogResponse::Accept(path) => Input::SaveResponse(path),
SaveDialogResponse::Cancel => Input::Ignore,
});
let model = App {
open_dialog,
save_dialog,
buffer: gtk::TextBuffer::new(None),
file_name: None,
message: None,
};
let widgets = view_output!();
sender.input(Input::ShowMessage(String::from(
"A simple text editor showing the usage of\n<b>OpenFileDialog</b> and <b>SaveFileDialog</b> components.\n\nStart by clicking <b>Open</b> on the header bar.",
)));
ComponentParts { model, widgets }
}
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
match message {
Input::OpenRequest => self.open_dialog.emit(OpenDialogMsg::Open),
Input::OpenResponse(path) => match std::fs::read_to_string(&path) {
Ok(contents) => {
self.buffer.set_text(&contents);
self.file_name = Some(
path.file_name()
.expect("The path has no file name")
.to_str()
.expect("Cannot convert file name to string")
.to_string(),
);
}
Err(e) => sender.input(Input::ShowMessage(e.to_string())),
},
Input::SaveRequest => self
.save_dialog
.emit(SaveDialogMsg::SaveAs(self.file_name.clone().unwrap())),
Input::SaveResponse(path) => match std::fs::write(
&path,
self.buffer
.text(&self.buffer.start_iter(), &self.buffer.end_iter(), false),
) {
Ok(_) => {
sender.input(Input::ShowMessage(format!(
"File saved successfully at {path:?}"
)));
self.buffer.set_text("");
self.file_name = None;
}
Err(e) => sender.input(Input::ShowMessage(e.to_string())),
},
Input::ShowMessage(message) => {
self.message = Some(message);
}
Input::ResetMessage => {
self.message = None;
}
Input::Ignore => {}
}
}
}
fn main() {
let app = RelmApp::new("relm4.example.file_dialogs");
app.run::<App>(());
}

View File

@ -1,69 +0,0 @@
use std::path::PathBuf;
use gtk::prelude::*;
use relm4::{
gtk, Component, ComponentController, ComponentParts, ComponentSender, Controller, RelmApp,
SimpleComponent,
};
use relm4_components::open_button::{OpenButton, OpenButtonSettings};
use relm4_components::open_dialog::OpenDialogSettings;
#[derive(Debug)]
enum AppMsg {
Open(PathBuf),
}
struct App {
open_button: Controller<OpenButton>,
}
#[relm4::component]
impl SimpleComponent for App {
type Init = ();
type Input = AppMsg;
type Output = ();
view! {
gtk::ApplicationWindow {
set_default_size: (300, 100),
#[wrap(Some)]
set_titlebar = &gtk::HeaderBar {
pack_start: model.open_button.widget(),
}
}
}
fn update(&mut self, msg: Self::Input, _: ComponentSender<Self>) {
match msg {
AppMsg::Open(path) => {
println!("* Opened file {path:?} *");
}
}
}
fn init(
_: Self::Init,
root: &Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let open_button = OpenButton::builder()
.launch(OpenButtonSettings {
dialog_settings: OpenDialogSettings::default(),
text: "Open file",
recently_opened_files: Some(".recent_files"),
max_recent_files: 10,
})
.forward(sender.input_sender(), AppMsg::Open);
let model = App { open_button };
let widgets = view_output!();
ComponentParts { model, widgets }
}
}
fn main() {
let app = RelmApp::new("relm4.example.open_button");
app.run::<App>(());
}

View File

@ -1,84 +0,0 @@
use gtk::prelude::*;
use relm4::{
gtk, Component, ComponentController, ComponentParts, ComponentSender, Controller, RelmApp,
SimpleComponent,
};
use relm4_components::web_image::{WebImage, WebImageMsg};
const IMAGES: &[&str] = &[
"https://raw.githubusercontent.com/Relm4/Relm4/main/assets/Relm_logo_with_text.png",
"https://raw.githubusercontent.com/Relm4/Relm4/main/assets/Relm_logo.png",
"https://raw.githubusercontent.com/gtk-rs/gtk-rs.github.io/master/logo/gtk-rs.ico",
"https://avatars.githubusercontent.com/u/5430905",
];
#[derive(Debug)]
enum AppMsg {
Next,
Unload,
}
struct App {
image: Controller<WebImage>,
idx: usize,
}
#[relm4::component]
impl SimpleComponent for App {
type Init = ();
type Input = AppMsg;
type Output = ();
view! {
gtk::ApplicationWindow {
set_default_size: (300, 300),
#[wrap(Some)]
set_titlebar = &gtk::HeaderBar {
pack_start = &gtk::Button {
set_label: "Next image",
connect_clicked => AppMsg::Next,
},
pack_start = &gtk::Button {
set_label: "Unload image",
connect_clicked => AppMsg::Unload,
}
},
gtk::Box {
#[local_ref]
image -> gtk::Box {}
}
}
}
fn update(&mut self, msg: Self::Input, _: ComponentSender<Self>) {
match msg {
AppMsg::Next => {
self.idx = (self.idx + 1) % IMAGES.len();
self.image
.emit(WebImageMsg::LoadImage(IMAGES[self.idx].to_owned()));
}
AppMsg::Unload => self.image.emit(WebImageMsg::Unload),
}
}
fn init(
_: Self::Init,
root: &Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let image = WebImage::builder().launch(IMAGES[0].to_owned()).detach();
let model = App { image, idx: 0 };
let image = model.image.widget();
let widgets = view_output!();
ComponentParts { model, widgets }
}
}
fn main() {
let app = RelmApp::new("relm4.example.open_button");
app.run::<App>(());
}

View File

@ -1,129 +0,0 @@
//! Reusable and easily configurable alert component.
//!
//! **[Example implementation](https://github.com/AaronErhardt/relm4/blob/main/relm4-examples/examples/alert.rs)**
use gtk::prelude::{DialogExt, GtkWindowExt, WidgetExt};
use relm4::{gtk, ComponentParts, ComponentSender, SimpleComponent};
/// Configuration for the alert dialog component
#[derive(Debug)]
pub struct AlertSettings {
/// Large text
pub text: String,
/// Optional secondary, smaller text
pub secondary_text: Option<String>,
/// Modal dialogs freeze other windows as long they are visible
pub is_modal: bool,
/// Sets color of the accept button to red if the theme supports it
pub destructive_accept: bool,
/// Text for confirm button
pub confirm_label: String,
/// Text for cancel button
pub cancel_label: String,
/// Text for third option button. If [`None`] the third button won't be created.
pub option_label: Option<String>,
}
/// Alert dialog component.
#[derive(Debug)]
pub struct Alert {
settings: AlertSettings,
is_active: bool,
}
/// Messages that can be sent to the alert dialog component
#[derive(Debug)]
pub enum AlertMsg {
/// Message sent by the parent to view the dialog
Show,
#[doc(hidden)]
Response(gtk::ResponseType),
}
/// User action performed on the alert dialog.
#[derive(Debug)]
pub enum AlertResponse {
/// User clicked confirm button.
Confirm,
/// User clicked cancel button.
Cancel,
/// User clicked user-supplied option.
Option,
}
/// Widgets of the alert dialog component.
#[relm4::component(pub)]
impl SimpleComponent for Alert {
type Init = AlertSettings;
type Input = AlertMsg;
type Output = AlertResponse;
view! {
#[name(dialog)]
gtk::MessageDialog {
set_message_type: gtk::MessageType::Question,
#[watch]
set_visible: model.is_active,
connect_response[sender] => move |_, response| {
sender.input(AlertMsg::Response(response));
},
// Apply configuration
set_text: Some(&model.settings.text),
set_secondary_text: model.settings.secondary_text.as_deref(),
set_modal: model.settings.is_modal,
add_button: (&model.settings.confirm_label, gtk::ResponseType::Accept),
add_button: (&model.settings.cancel_label, gtk::ResponseType::Cancel),
}
}
fn init(
settings: AlertSettings,
root: &Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = Alert {
settings,
is_active: false,
};
let widgets = view_output!();
if let Some(option_label) = &model.settings.option_label {
widgets
.dialog
.add_button(option_label, gtk::ResponseType::Other(0));
}
if model.settings.destructive_accept {
let accept_widget = widgets
.dialog
.widget_for_response(gtk::ResponseType::Accept)
.expect("No button for accept response set");
accept_widget.add_css_class("destructive-action");
}
ComponentParts { model, widgets }
}
fn update(&mut self, input: AlertMsg, sender: ComponentSender<Self>) {
match input {
AlertMsg::Show => {
self.is_active = true;
}
AlertMsg::Response(ty) => {
self.is_active = false;
sender
.output(match ty {
gtk::ResponseType::Accept => AlertResponse::Confirm,
gtk::ResponseType::Other(_) => AlertResponse::Option,
_ => AlertResponse::Cancel,
})
.unwrap();
}
}
}
}

View File

@ -1,43 +0,0 @@
//! Collection of reusable and easily configurable components for Relm4.
//!
//! Docs of related crates:
//! [relm4](https://docs.rs/relm4)
//! | [relm4-macros](https://docs.rs/relm4_macros)
//! | [relm4-components](https://docs.rs/relm4_components)
//! | [gtk4-rs](https://gtk-rs.org/gtk4-rs/git/docs)
//! | [gtk-rs-core](https://gtk-rs.org/gtk-rs-core/git/docs)
//! | [libadwaita-rs](https://world.pages.gitlab.gnome.org/Rust/libadwaita-rs/git/docs/libadwaita)
//! | [libpanel-rs](https://world.pages.gitlab.gnome.org/Rust/libpanel-rs/git/docs/libpanel)
//!
//! [GitHub](https://github.com/Relm4/Relm4)
//! | [Website](https://relm4.org)
//! | [Book](https://relm4.org/book/stable/)
//! | [Blog](https://relm4.org/blog)
#![doc(html_logo_url = "https://relm4.org/icons/relm4_logo.svg")]
#![doc(html_favicon_url = "https://relm4.org/icons/relm4_org.svg")]
#![warn(
missing_debug_implementations,
missing_docs,
rust_2018_idioms,
unreachable_pub,
unused_qualifications,
clippy::cargo,
clippy::must_use_candidate
)]
// Configuration for doc builds on the nightly toolchain.
#![cfg_attr(docsrs, feature(doc_cfg))]
// Ignore GTK 4.10 deprecations.
// Most deprecated features can only be replaced with new 4.10 APIs and
// we don't want to lift the minimum requirement GTK4 version for Relm4 yet.
#![allow(deprecated)]
pub mod alert;
pub mod open_button;
pub mod open_dialog;
pub mod save_dialog;
pub mod simple_combo_box;
#[cfg(feature = "web")]
#[cfg_attr(docsrs, doc(cfg(feature = "web")))]
pub mod web_image;

View File

@ -1,42 +0,0 @@
use gtk::prelude::*;
use relm4::factory::{DynamicIndex, FactoryComponent, FactorySender};
use relm4::{gtk, RelmWidgetExt};
use super::OpenButtonMsg;
use std::path::PathBuf;
#[derive(Debug)]
pub(crate) struct FileListItem {
pub(crate) path: PathBuf,
}
#[relm4::factory(pub(crate))]
impl FactoryComponent for FileListItem {
type ParentInput = OpenButtonMsg;
type CommandOutput = ();
type Input = ();
type Init = PathBuf;
type ParentWidget = gtk::Box;
type Output = OpenButtonMsg;
view! {
gtk::ListBoxRow {
gtk::Button {
set_label: self.path.iter().last().expect("Empty path").to_str().unwrap(),
set_margin_all: 0,
connect_clicked[sender, index] => move |_| {
sender.output(OpenButtonMsg::OpenRecent(index.clone()));
}
}
}
}
fn forward_to_parent(output: Self::Output) -> Option<Self::ParentInput> {
Some(output)
}
fn init_model(init: Self::Init, _: &DynamicIndex, _: FactorySender<Self>) -> Self {
Self { path: init }
}
}

View File

@ -1,192 +0,0 @@
//! Reusable and easily configurable open button dialog component.
//!
//! **[Example implementation](https://github.com/Relm4/Relm4/blob/main/relm4-components/examples/open_button.rs)**
use relm4::factory::{DynamicIndex, FactoryVecDeque};
use relm4::gtk::prelude::*;
use relm4::{
gtk, Component, ComponentController, ComponentParts, ComponentSender, Controller,
SimpleComponent,
};
use crate::open_dialog::{OpenDialog, OpenDialogMsg, OpenDialogResponse, OpenDialogSettings};
use std::fs;
use std::path::PathBuf;
mod factory;
use factory::FileListItem;
/// Open button component.
///
/// Creates a button with custom text that can be used to open a file chooser dialog. If a file is
/// chosen, then it will be emitted as an output. The component can also optionally display a
/// popover list of open files if [`OpenButtonSettings::recently_opened_files`] is set to a value.
#[tracker::track]
#[derive(Debug)]
pub struct OpenButton {
#[do_not_track]
config: OpenButtonSettings,
#[do_not_track]
dialog: Controller<OpenDialog>,
#[do_not_track]
recent_files: Option<FactoryVecDeque<FileListItem>>,
initialized: bool,
#[do_not_track]
reset_popover: bool,
}
#[derive(Debug)]
/// Configuration for the open button component
pub struct OpenButtonSettings {
/// Settings for the open file dialog.
pub dialog_settings: OpenDialogSettings,
/// Text of the open button.
pub text: &'static str,
/// Path to a file where recent files should be stored.
/// This list is updated fully automatically.
pub recently_opened_files: Option<&'static str>,
/// Maximum amount of recent files to store.
/// This is only used if a path for storing the recently opened files was set.
pub max_recent_files: usize,
}
#[doc(hidden)]
#[derive(Debug)]
pub enum OpenButtonMsg {
Open(PathBuf),
OpenRecent(DynamicIndex),
ShowDialog,
Ignore,
}
/// Widgets of the open button component
#[relm4::component(pub)]
impl SimpleComponent for OpenButton {
type Init = OpenButtonSettings;
type Input = OpenButtonMsg;
type Output = PathBuf;
view! {
gtk::Box {
add_css_class: "linked",
gtk::Button {
set_label: model.config.text,
connect_clicked => OpenButtonMsg::ShowDialog,
},
gtk::MenuButton {
set_visible: model.config.recently_opened_files.is_some(),
#[wrap(Some)]
#[name(popover)]
set_popover = &gtk::Popover {
gtk::ScrolledWindow {
set_hscrollbar_policy: gtk::PolicyType::Never,
set_min_content_width: 100,
set_min_content_height: 100,
set_min_content_height: 300,
#[name(recent_files_list)]
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_vexpand: true,
set_hexpand: true,
}
}
}
}
}
}
fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) {
self.reset_popover = false;
match msg {
OpenButtonMsg::ShowDialog => {
self.dialog.emit(OpenDialogMsg::Open);
}
OpenButtonMsg::Open(path) => {
sender.output(path.clone()).unwrap();
self.reset_popover = true;
if let Some(recent_files) = &mut self.recent_files {
let index = recent_files.iter().position(|item| item.path == path);
if let Some(index) = index {
recent_files.guard().remove(index);
}
if recent_files.len() < self.config.max_recent_files {
recent_files.guard().push_front(path);
}
let contents = recent_files
.iter()
.filter_map(|recent_path| {
recent_path.path.to_str().map(|s| format!("{s}\n"))
})
.collect::<String>();
let _ = fs::write(self.config.recently_opened_files.unwrap(), contents);
}
}
OpenButtonMsg::OpenRecent(index) => {
if let Some(item) = self
.recent_files
.as_ref()
.and_then(|recent_files| recent_files.get(index.current_index()))
{
sender.input(OpenButtonMsg::Open(PathBuf::from(&item.path)));
}
}
OpenButtonMsg::Ignore => (),
}
}
fn pre_view() {
if self.reset_popover {
popover.popdown();
}
}
fn init(
settings: Self::Init,
root: &Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let dialog = OpenDialog::builder()
.transient_for_native(root)
.launch(settings.dialog_settings.clone())
.forward(sender.input_sender(), |response| match response {
OpenDialogResponse::Accept(path) => OpenButtonMsg::Open(path),
OpenDialogResponse::Cancel => OpenButtonMsg::Ignore,
});
let mut model = Self {
config: settings,
dialog,
initialized: false,
recent_files: None,
reset_popover: false,
tracker: 0,
};
let widgets = view_output!();
if let Some(filename) = model.config.recently_opened_files {
let mut factory =
FactoryVecDeque::new(widgets.recent_files_list.clone(), sender.input_sender());
if let Ok(entries) = fs::read_to_string(filename) {
let mut guard = factory.guard();
for entry in entries.lines() {
guard.push_back(PathBuf::from(entry));
}
}
model.recent_files = Some(factory);
}
ComponentParts { model, widgets }
}
}

View File

@ -1,181 +0,0 @@
//! Reusable and easily configurable open dialog component.
//!
//! **[Example implementation](https://github.com/Relm4/Relm4/blob/main/relm4-components/examples/file_dialogs.rs)**
use gtk::prelude::{Cast, FileChooserExt, FileExt, ListModelExt, NativeDialogExt};
use relm4::{gtk, ComponentParts, ComponentSender, SimpleComponent};
use std::{fmt::Debug, marker::PhantomData, path::PathBuf};
/// A component that prompts the user to choose a file.
///
/// The user would be able to select a single file. If you'd like to select multiple, use [`OpenDialogMulti`].
pub type OpenDialog = OpenDialogInner<SingleSelection>;
/// A component that prompts the user to choose a file.
///
/// The user would be able to select multiple files. If you'd like to select just one, use [`OpenDialog`].
pub type OpenDialogMulti = OpenDialogInner<MultiSelection>;
/// Type of selection used for the open dialog.
pub trait Select: Debug {
/// Output of the selection.
type Selection: Debug;
/// Whether to select multiple files inside the dialog.
const SELECT_MULTIPLE: bool;
/// Construct selection from the file chooser.
fn select(dialog: &gtk::FileChooserNative) -> Self::Selection;
}
/// A type of selection where only one file can be chosen at a time.
#[derive(Debug)]
pub struct SingleSelection;
impl Select for SingleSelection {
type Selection = PathBuf;
const SELECT_MULTIPLE: bool = false;
fn select(dialog: &gtk::FileChooserNative) -> Self::Selection {
dialog
.file()
.expect("No file selected")
.path()
.expect("No path")
}
}
/// A type of selection where multiple types can be chosen at a time.
#[derive(Debug)]
pub struct MultiSelection;
impl Select for MultiSelection {
type Selection = Vec<PathBuf>;
const SELECT_MULTIPLE: bool = true;
fn select(dialog: &gtk::FileChooserNative) -> Self::Selection {
let list_model = dialog.files();
(0..list_model.n_items())
.filter_map(|index| list_model.item(index))
.filter_map(|obj| obj.downcast::<gtk::gio::File>().ok())
.filter_map(|file| file.path())
.collect()
}
}
#[derive(Clone, Debug)]
/// Configuration for the open dialog component
pub struct OpenDialogSettings {
/// Select folders instead of files
pub folder_mode: bool,
/// Label for cancel button
pub cancel_label: String,
/// Label for accept button
pub accept_label: String,
/// Allow or disallow creating folders
pub create_folders: bool,
/// Freeze other windows while the dialog is open
pub is_modal: bool,
/// Filter for MIME types or other patterns
pub filters: Vec<gtk::FileFilter>,
}
impl Default for OpenDialogSettings {
fn default() -> Self {
OpenDialogSettings {
folder_mode: false,
accept_label: String::from("Open"),
cancel_label: String::from("Cancel"),
create_folders: true,
is_modal: true,
filters: Vec::new(),
}
}
}
#[derive(Debug)]
/// Model for the open dialog component
pub struct OpenDialogInner<S: Select> {
visible: bool,
_phantom: PhantomData<S>,
}
/// Messages that can be sent to the open dialog component
#[derive(Debug, Clone)]
pub enum OpenDialogMsg {
/// Show the dialog
Open,
#[doc(hidden)]
Hide,
}
/// Messages that can be sent from the open dialog component
#[derive(Debug, Clone)]
pub enum OpenDialogResponse<S: Select> {
/// User clicked accept button.
Accept(S::Selection),
/// User clicked cancel button.
Cancel,
}
/// Widgets of the open dialog component.
#[relm4::component(pub)]
impl<S: Select + 'static> SimpleComponent for OpenDialogInner<S> {
type Init = OpenDialogSettings;
type Input = OpenDialogMsg;
type Output = OpenDialogResponse<S>;
view! {
gtk::FileChooserNative {
set_action: if settings.folder_mode {
gtk::FileChooserAction::SelectFolder
} else {
gtk::FileChooserAction::Open
},
set_select_multiple: S::SELECT_MULTIPLE,
set_create_folders: settings.create_folders,
set_modal: settings.is_modal,
set_accept_label: Some(&settings.accept_label),
set_cancel_label: Some(&settings.cancel_label),
#[iterate]
add_filter: &settings.filters,
#[watch]
set_visible: model.visible,
connect_response[sender] => move |dialog, res_ty| {
match res_ty {
gtk::ResponseType::Accept => {
let selection = S::select(dialog);
sender.output(OpenDialogResponse::Accept(selection)).unwrap();
}
_ => sender.output(OpenDialogResponse::Cancel).unwrap(),
}
sender.input(OpenDialogMsg::Hide);
}
}
}
fn init(
settings: Self::Init,
root: &Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = OpenDialogInner {
visible: false,
_phantom: PhantomData,
};
let widgets = view_output!();
ComponentParts { model, widgets }
}
fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>) {
match message {
OpenDialogMsg::Open => {
self.visible = true;
}
OpenDialogMsg::Hide => {
self.visible = false;
}
}
}
}

View File

@ -1,136 +0,0 @@
//! Reusable and easily configurable save dialog component.
//!
//! **[Example implementation](https://github.com/Relm4/Relm4/blob/main/relm4-components/examples/file_dialogs.rs)**
use gtk::prelude::{FileChooserExt, FileExt, NativeDialogExt};
use relm4::{gtk, ComponentParts, ComponentSender, SimpleComponent};
use std::path::PathBuf;
#[derive(Clone, Debug)]
/// Configuration for the save dialog component
pub struct SaveDialogSettings {
/// Label for cancel button
pub cancel_label: String,
/// Label for accept button
pub accept_label: String,
/// Allow or disallow creating folders
pub create_folders: bool,
/// Freeze other windows while the dialog is open
pub is_modal: bool,
/// Filter for MIME types or other patterns
pub filters: Vec<gtk::FileFilter>,
}
impl Default for SaveDialogSettings {
fn default() -> Self {
SaveDialogSettings {
accept_label: String::from("Save"),
cancel_label: String::from("Cancel"),
create_folders: true,
is_modal: true,
filters: Vec::new(),
}
}
}
#[derive(Debug)]
/// A model for the save dialog component
pub struct SaveDialog {
current_name: String,
visible: bool,
}
/// Messages that can be sent to the save dialog component
#[derive(Debug, Clone)]
pub enum SaveDialogMsg {
/// Show the dialog
Save,
/// Show the dialog, with a suggested file name
SaveAs(String),
#[doc(hidden)]
Hide,
}
/// Messages that can be sent from the save dialog component
#[derive(Debug, Clone)]
pub enum SaveDialogResponse {
/// User clicked accept button.
Accept(PathBuf),
/// User clicked cancel button.
Cancel,
}
/// Widgets of the save dialog component.
#[relm4::component(pub)]
impl SimpleComponent for SaveDialog {
type Init = SaveDialogSettings;
type Input = SaveDialogMsg;
type Output = SaveDialogResponse;
view! {
gtk::FileChooserNative {
set_action: gtk::FileChooserAction::Save,
set_create_folders: settings.create_folders,
set_modal: settings.is_modal,
set_accept_label: Some(&settings.accept_label),
set_cancel_label: Some(&settings.cancel_label),
#[iterate]
add_filter: &settings.filters,
#[watch]
set_current_name: &model.current_name,
#[watch]
set_visible: model.visible,
connect_response[sender] => move |dialog, res_ty| {
match res_ty {
gtk::ResponseType::Accept => {
if let Some(file) = dialog.file() {
if let Some(path) = file.path() {
sender.output(SaveDialogResponse::Accept(path)).unwrap();
sender.input(SaveDialogMsg::Hide);
return;
}
}
sender.output(SaveDialogResponse::Cancel).unwrap();
}
_ => sender.output(SaveDialogResponse::Cancel).unwrap(),
}
sender.input(SaveDialogMsg::Hide);
}
}
}
fn init(
settings: Self::Init,
root: &Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = SaveDialog {
current_name: String::new(),
visible: false,
};
let widgets = view_output!();
ComponentParts { model, widgets }
}
fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>) {
match message {
SaveDialogMsg::Save => {
self.current_name = String::new();
self.visible = true;
}
SaveDialogMsg::SaveAs(file_name) => {
self.current_name = file_name;
self.visible = true;
}
SaveDialogMsg::Hide => {
self.visible = false;
}
}
}
}

View File

@ -1,117 +0,0 @@
//! A wrapper around [`gtk::ComboBoxText`] that makes it easier to use
//! from regular Rust code.
use std::fmt::Debug;
use relm4::gtk::prelude::{ComboBoxExt, ComboBoxExtManual};
use relm4::{gtk, ComponentSender};
use relm4::{Component, ComponentParts};
#[derive(Debug, Clone, PartialEq, Eq)]
/// A simple wrapper around [`gtk::ComboBox`].
///
/// This can be used with enums, [`String`]s or any custom type you want.
/// The only requirement is that the inner type implements [`ToString`] and [`Debug`].
///
/// To get notified when the selection changed, you can use
/// [`Connector::forward()`](relm4::component::Connector::forward())
/// after launching the component.
pub struct SimpleComboBox<E: ToString> {
/// The variants that can be selected.
pub variants: Vec<E>,
/// The index of the active element or [`None`] is nothing is selected.
pub active_index: Option<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
/// The message type of [`SimpleComboBox`].
pub enum SimpleComboBoxMsg<E: ToString> {
/// Overwrite the current values.
UpdateData(SimpleComboBox<E>),
/// Set the index of the active element.
SetActiveIdx(usize),
#[doc(hidden)]
UpdateIndex(usize),
}
impl<E> Component for SimpleComboBox<E>
where
E: ToString + 'static + Debug,
{
type CommandOutput = ();
type Input = SimpleComboBoxMsg<E>;
type Output = usize;
type Init = Self;
type Root = gtk::ComboBoxText;
type Widgets = gtk::ComboBoxText;
fn init_root() -> Self::Root {
gtk::ComboBoxText::default()
}
fn init(
model: Self::Init,
root: &Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let widgets = root.clone();
model.render(&widgets);
widgets.connect_changed(move |combo_box| {
if let Some(active_idx) = combo_box.active() {
sender.input(Self::Input::UpdateIndex(active_idx as usize));
}
});
ComponentParts { model, widgets }
}
fn update_with_view(
&mut self,
widgets: &mut Self::Widgets,
input: Self::Input,
sender: ComponentSender<Self>,
_root: &Self::Root,
) {
match input {
SimpleComboBoxMsg::UpdateIndex(idx) => {
// Ignore send errors because the component might
// be detached.
sender.output(idx).ok();
self.active_index = Some(idx);
}
SimpleComboBoxMsg::SetActiveIdx(idx) => {
if idx < self.variants.len() {
self.active_index = Some(idx);
widgets.set_active(u32::try_from(idx).ok());
}
}
SimpleComboBoxMsg::UpdateData(data) => {
*self = data;
self.render(widgets);
}
}
}
}
impl<E> SimpleComboBox<E>
where
E: ToString,
{
fn render(&self, combo_box: &gtk::ComboBoxText) {
combo_box.remove_all();
for (idx, e) in self.variants.iter().enumerate() {
combo_box.insert_text(idx as i32, &e.to_string());
}
combo_box.set_active(self.active_index.and_then(|val| u32::try_from(val).ok()));
}
/// Return the value of the currently selected element or [`None`] if nothing is selected.
#[must_use]
pub fn get_active_elem(&self) -> Option<&E> {
self.active_index.map(|idx| &self.variants[idx])
}
}

View File

@ -1,143 +0,0 @@
//! Reusable and easily configurable component for loading images from the web.
use std::collections::VecDeque;
use std::fmt::Debug;
use relm4::gtk::prelude::{BoxExt, Cast, WidgetExt};
use relm4::{gtk, Component, ComponentParts, ComponentSender};
#[derive(Debug, Clone, PartialEq, Eq)]
/// Reusable component for loading images from the web.
pub struct WebImage {
current_id: usize,
current_widget: gtk::Widget,
}
#[derive(Debug, Clone, PartialEq, Eq)]
/// Load or unload a web image.
pub enum WebImageMsg {
/// Load an image from an url.
LoadImage(String),
/// Unload the current image.
Unload,
}
impl Component for WebImage {
type CommandOutput = Option<(usize, VecDeque<u8>)>;
type Input = WebImageMsg;
type Output = ();
type Init = String;
type Root = gtk::Box;
type Widgets = ();
fn init_root() -> Self::Root {
gtk::Box::default()
}
fn init(
url: Self::Init,
root: &Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let widget = gtk::Box::default();
root.append(&widget);
let current_widget = Self::set_spinner(root, widget.upcast_ref());
let model = Self {
current_id: 0,
current_widget,
};
model.load_image(&sender, url);
ComponentParts { model, widgets: () }
}
fn update(&mut self, input: Self::Input, sender: ComponentSender<Self>, root: &Self::Root) {
self.current_widget = Self::set_spinner(root, &self.current_widget);
self.current_id = self.current_id.wrapping_add(1);
match input {
WebImageMsg::LoadImage(url) => {
self.load_image(&sender, url);
}
WebImageMsg::Unload => (),
}
}
fn update_cmd(
&mut self,
message: Self::CommandOutput,
sender: ComponentSender<Self>,
root: &Self::Root,
) {
if let Some((id, data)) = message {
if id == self.current_id {
if let Some(img) = Self::generate_image(data) {
self.current_widget = Self::set_image(root, &self.current_widget, &img);
sender.output(()).ok();
}
}
}
}
}
impl WebImage {
#[must_use]
fn set_spinner(root: &<Self as Component>::Root, widget: &gtk::Widget) -> gtk::Widget {
root.remove(widget);
relm4::view! {
#[local_ref]
root -> gtk::Box {
set_halign: gtk::Align::Center,
set_valign: gtk::Align::Center,
#[name(spinner)]
gtk::Spinner {
start: (),
set_hexpand: true,
set_vexpand: true,
}
}
}
spinner.upcast()
}
#[must_use]
fn set_image(
root: &<Self as Component>::Root,
widget: &gtk::Widget,
img: &gtk::Image,
) -> gtk::Widget {
root.remove(widget);
relm4::view! {
#[local_ref]
root -> gtk::Box {
set_halign: gtk::Align::Fill,
set_valign: gtk::Align::Fill,
#[local_ref]
img -> gtk::Image {
set_hexpand: true,
set_vexpand: true,
}
}
}
img.clone().upcast()
}
fn load_image(&self, sender: &ComponentSender<Self>, url: String) {
sender.oneshot_command(Self::get_img_data(self.current_id, url));
}
fn generate_image(data: VecDeque<u8>) -> Option<gtk::Image> {
let pixbuf = gtk::gdk_pixbuf::Pixbuf::from_read(data).ok()?;
Some(gtk::Image::from_pixbuf(Some(&pixbuf)))
}
async fn get_img_data(id: usize, url: String) -> Option<(usize, VecDeque<u8>)> {
let response = reqwest::get(url).await.ok()?;
let bytes = response.bytes().await.ok()?;
Some((id, bytes.into_iter().collect()))
}
}

View File

@ -1,43 +0,0 @@
[package]
name = "relm4-macros"
readme = "README.md"
keywords = ["gui", "gtk", "gtk4", "elm", "view"]
documentation = "https://docs.rs/relm4_macros/"
version.workspace = true
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
description.workspace = true
homepage.workspace = true
repository.workspace = true
categories.workspace = true
[lib]
proc-macro = true
[features]
default = ["relm4"]
# Without the default "relm4" feature, all imports of gtk will
# be `use gtk;` instead of `use relm4::gtk;` thus making it
# easier to use this crate without Relm4.
relm4 = []
[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = [
"full",
"extra-traits",
"visit",
"visit-mut",
] }
[dev-dependencies]
relm4 = { path = "../relm4" }
rustversion = "1"
trybuild = "1.0.80"

View File

@ -1,6 +0,0 @@
# Relm4-macros
[![Relm4-macros on crates.io](https://img.shields.io/crates/v/relm4-macros.svg)](https://crates.io/crates/relm4-macros)
[![Relm4-macros docs](https://img.shields.io/badge/rust-documentation-blue)](https://docs.rs/relm4_macros/)
A macro to easily generate UIs for Relm4 applications.

View File

@ -1,24 +0,0 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::ToTokens;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{Field, Result, Token};
#[derive(Debug)]
pub(super) struct AdditionalFields {
pub(super) inner: Punctuated<Field, Token![,]>,
}
impl Parse for AdditionalFields {
fn parse(input: ParseStream<'_>) -> Result<Self> {
Ok(AdditionalFields {
inner: input.parse_terminated(Field::parse_named, Token![,])?,
})
}
}
impl ToTokens for AdditionalFields {
fn to_tokens(&self, tokens: &mut TokenStream2) {
tokens.extend(self.inner.to_token_stream());
}
}

View File

@ -1,49 +0,0 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote_spanned, ToTokens};
use syn::parse::{Parse, ParseStream};
use syn::punctuated::{Pair, Punctuated};
use syn::spanned::Spanned;
use syn::{Error, Result, Token};
#[derive(Debug)]
pub(super) struct Args<T>
where
T: Parse + ToTokens,
{
pub(super) inner: Vec<T>,
}
impl<T> Parse for Args<T>
where
T: Parse + ToTokens,
{
fn parse(input: ParseStream<'_>) -> Result<Self> {
let punct: Punctuated<T, Token![,]> = input.call(Punctuated::parse_terminated)?;
if punct.is_empty() {
return Err(Error::new(input.span(), "Expected at least one element. This is probably caused by empty arguments and macros."));
}
let inner = punct.into_pairs().map(Pair::into_value).collect();
Ok(Args { inner })
}
}
impl<T> ToTokens for Args<T>
where
T: Parse + ToTokens,
{
fn to_tokens(&self, out: &mut TokenStream2) {
let mut iter = self.inner.iter();
let first = iter.next().unwrap();
out.extend(quote_spanned! {
first.span() => #first
});
for expr in iter {
out.extend(quote_spanned! {
expr.span() => ,#expr
});
}
}
}

View File

@ -1,77 +0,0 @@
use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned;
use syn::token::Async;
use syn::{Error, Result, Token, Visibility};
pub(super) struct Attrs {
/// Keeps information about visibility of the widget
pub(super) visibility: Option<Visibility>,
/// Whether an async trait is used or not
pub(super) asyncness: Option<Async>,
}
pub(super) struct SyncOnlyAttrs {
/// Keeps information about visibility of the widget
pub(super) visibility: Option<Visibility>,
}
impl Parse for SyncOnlyAttrs {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let Attrs {
visibility,
asyncness,
} = input.parse()?;
if let Some(async_token) = asyncness {
Err(syn::Error::new(
async_token.span,
"this macro doesn't support async traits",
))
} else {
Ok(Self { visibility })
}
}
}
impl Parse for Attrs {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let mut attrs = Attrs {
visibility: None,
asyncness: None,
};
while !input.is_empty() {
if input.peek(Async) {
let new_asyncness: Async = input.parse()?;
if attrs.asyncness.is_some() {
return Err(syn::Error::new(
new_asyncness.span,
"cannot specify asyncness twice",
));
} else {
attrs.asyncness = Some(new_asyncness);
}
} else {
let new_vis: Visibility = input.parse()?;
if attrs.visibility.is_some() {
return Err(syn::Error::new(
new_vis.span(),
"cannot specify visibility twice",
));
} else {
attrs.visibility = Some(new_vis);
}
}
if input.peek(Token![,]) {
let comma: Token![,] = input.parse()?;
if input.is_empty() {
// We've just consumed last token in stream (which is comma) and that's wrong
return Err(Error::new(comma.span, "expected visibility or `async`"));
}
}
}
Ok(attrs)
}
}

View File

@ -1,192 +0,0 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, quote_spanned, ToTokens};
use syn::parse_quote;
use syn::visit_mut::VisitMut;
use crate::attrs::Attrs;
use crate::token_streams::{TokenStreams, TraitImplDetails};
use crate::util;
use crate::visitors::{ComponentVisitor, PreAndPostView, ViewOutputExpander};
pub(crate) fn generate_tokens(
global_attributes: Attrs,
mut component_impl: syn::ItemImpl,
) -> TokenStream2 {
let Attrs {
visibility,
asyncness,
} = global_attributes;
let mut errors = vec![];
let mut component_visitor = ComponentVisitor::new(&mut errors);
component_visitor.visit_item_impl_mut(&mut component_impl);
let additional_fields = component_visitor.additional_fields.take();
let menus_stream = component_visitor
.menus
.take()
.map(|menus| menus.menus_stream());
let mut struct_fields = None;
match &component_visitor.view_widgets {
None => component_visitor.errors.push(syn::Error::new_spanned(
&component_impl,
"expected `view!` macro invocation",
)),
Some(Err(e)) => component_visitor.errors.push(e.clone()),
_ => (),
}
if let ComponentVisitor {
view_widgets: Some(Ok(view_widgets)),
model_name: Some(model_name),
root_name: Some(root_name),
sender_name: Some(sender_name),
errors,
..
} = component_visitor
{
let trait_impl_details = TraitImplDetails {
vis: visibility.clone(),
model_name,
sender_name,
root_name: Some(root_name),
};
let TokenStreams {
error,
init_root,
rename_root,
struct_fields: struct_fields_stream,
init: init_widgets,
assign,
return_fields,
destructure_fields,
update_view,
} = view_widgets.generate_streams(&trait_impl_details, false);
let model_name = trait_impl_details.model_name;
struct_fields = Some(struct_fields_stream);
let root_widget_type = view_widgets.root_type();
// Extract identifiers from additional fields for struct initialization: "test: u8" => "test"
let additional_fields_return_stream = if let Some(fields) = &additional_fields {
let mut tokens = TokenStream2::new();
for field in fields.inner.pairs() {
tokens.extend(field.value().ident.to_token_stream());
tokens.extend(quote! {,});
}
tokens
} else {
TokenStream2::new()
};
let view_code = quote! {
#rename_root
#menus_stream
#init_widgets
#assign
{
#error
}
};
let widgets_return_code = parse_quote! {
Self::Widgets {
#return_fields
#additional_fields_return_stream
}
};
ViewOutputExpander::expand(&mut component_impl, view_code, widgets_return_code, errors);
component_impl.items.push(parse_quote! {
type Root = #root_widget_type;
});
let ty: syn::Type = parse_quote!(Self::Root);
let init_root = util::verbatim_impl_item_fn("init_root", Vec::new(), ty, init_root);
component_impl.items.push(init_root);
let PreAndPostView {
pre_view,
post_view,
..
} = PreAndPostView::extract(&mut component_impl, errors);
let sender_ty: syn::TypePath = if asyncness.is_some() {
parse_quote! { relm4::AsyncComponentSender }
} else {
parse_quote! { relm4::ComponentSender }
};
component_impl.items.push(parse_quote! {
/// Update the view to represent the updated model.
fn update_view(
&self,
widgets: &mut Self::Widgets,
sender: #sender_ty<Self>,
) {
struct __DoNotReturnManually;
let _no_manual_return: __DoNotReturnManually = (move || {
#[allow(unused_variables)]
let Self::Widgets {
#destructure_fields
#additional_fields_return_stream
} = widgets;
#[allow(unused_variables)]
let #model_name = self;
#(#pre_view)*
#update_view
// In post_view returning early is ok
(move || { #(#post_view)* })();
__DoNotReturnManually
})();
}
});
}
// Use the widget type if used.
let widgets_name = util::generate_widgets_type(
component_visitor.widgets_ty,
&mut component_impl,
&mut errors,
);
let widgets_struct = widgets_name.map(|widgets_name| {
let outer_attrs = &component_impl.attrs;
quote! {
#[allow(dead_code)]
#(#outer_attrs)*
#[derive(Debug)]
#visibility struct #widgets_name {
#struct_fields
#additional_fields
}
}
});
let errors = errors.iter().map(syn::Error::to_compile_error);
let async_trait = asyncness.map(
|async_token| quote_spanned!(async_token.span => #[relm4::async_trait::async_trait(?Send)]),
);
quote! {
#widgets_struct
#async_trait
#component_impl
#(#errors)*
}
}

View File

@ -1,216 +0,0 @@
use proc_macro2::{Span as Span2, TokenStream as TokenStream2};
use quote::{quote, quote_spanned, ToTokens};
use syn::visit_mut::VisitMut;
use syn::{parse_quote, Ident};
use crate::attrs::Attrs;
use crate::token_streams::{TokenStreams, TraitImplDetails};
use crate::util;
use crate::visitors::{FactoryComponentVisitor, PreAndPostView, ViewOutputExpander};
pub(crate) fn generate_tokens(
global_attributes: Attrs,
mut factory_impl: syn::ItemImpl,
) -> TokenStream2 {
let Attrs {
visibility,
asyncness,
} = global_attributes;
let mut errors = vec![];
let mut factory_visitor = FactoryComponentVisitor::new(&mut errors);
factory_visitor.visit_item_impl_mut(&mut factory_impl);
let additional_fields = factory_visitor.additional_fields.take();
let menus_stream = factory_visitor.menus.take().map(|m| m.menus_stream());
let mut struct_fields = None;
match &factory_visitor.view_widgets {
None => factory_visitor.errors.push(syn::Error::new_spanned(
&factory_impl,
"expected `view!` macro invocation",
)),
Some(Err(e)) => factory_visitor.errors.push(e.clone()),
_ => (),
}
// Insert default index type for sync variants
// if it wasn't specified by the user.
if factory_visitor.index_ty.is_none() && asyncness.is_none() {
factory_impl.items.push(parse_quote! {
type Index = relm4::factory::DynamicIndex;
});
}
if let FactoryComponentVisitor {
view_widgets: Some(Ok(view_widgets)),
root_name,
init_widgets,
errors,
..
} = factory_visitor
{
let TokenStreams {
error,
init_root,
rename_root,
struct_fields: struct_fields_stream,
init,
assign,
return_fields,
destructure_fields,
update_view,
} = view_widgets.generate_streams(
&TraitImplDetails {
vis: visibility.clone(),
model_name: Ident::new("self", Span2::call_site()),
root_name: Some(
root_name.unwrap_or_else(|| Ident::new("root", Span2::call_site())),
),
sender_name: Ident::new("sender", Span2::call_site()),
},
false,
);
struct_fields = Some(struct_fields_stream);
let root_widget_type = view_widgets.root_type();
// Extract identifiers from additional fields for struct initialization: "test: u8" => "test"
let additional_fields_return_stream = if let Some(fields) = &additional_fields {
let mut tokens = TokenStream2::new();
for field in fields.inner.pairs() {
tokens.extend(field.value().ident.to_token_stream());
tokens.extend(quote! {,});
}
tokens
} else {
TokenStream2::new()
};
let view_code = quote! {
#rename_root
#menus_stream
#init
#assign
{
#error
}
};
let widgets_return_code = parse_quote! {
Self::Widgets {
#return_fields
#additional_fields_return_stream
}
};
let sender_ty: Ident = if asyncness.is_some() {
parse_quote! { AsyncFactorySender }
} else {
parse_quote! { FactorySender }
};
let index_ty: syn::TypePath = if asyncness.is_some() {
parse_quote! { relm4::factory::DynamicIndex }
} else {
parse_quote! { Self::Index }
};
if init_widgets.is_some() {
ViewOutputExpander::expand(&mut factory_impl, view_code, widgets_return_code, errors);
} else {
factory_impl.items.push(parse_quote! {
fn init_widgets(
&mut self,
index: & #index_ty,
root: &Self::Root,
returned_widget: &<Self::ParentWidget as relm4::factory::FactoryView>::ReturnedWidget,
sender: relm4::factory::#sender_ty<Self>,
) -> Self::Widgets {
#view_code
#widgets_return_code
}
});
}
factory_impl.items.push(parse_quote! {
type Root = #root_widget_type;
});
let ty: syn::Type = parse_quote!(Self::Root);
factory_impl.items.push(if asyncness.is_some() {
util::verbatim_impl_item_fn("init_root", Vec::new(), ty, init_root)
} else {
let args = vec![parse_quote! { &self}];
util::verbatim_impl_item_fn("init_root", args, ty, init_root)
});
let PreAndPostView {
pre_view,
post_view,
..
} = PreAndPostView::extract(&mut factory_impl, errors);
factory_impl.items.push(parse_quote! {
// Update the view to represent the updated model.
fn update_view(
&self,
widgets: &mut Self::Widgets,
sender: relm4::factory::#sender_ty<Self>,
) {
struct __DoNotReturnManually;
let _no_manual_return: __DoNotReturnManually = (move || {
#[allow(unused_variables)]
let Self::Widgets {
#destructure_fields
#additional_fields_return_stream
} = widgets;
#(#pre_view)*
#update_view
// In post_view returning early is ok
(move || { #(#post_view)* })();
__DoNotReturnManually
})();
}
});
}
// Use the widget type if used.
let widgets_name =
util::generate_widgets_type(factory_visitor.widgets_ty, &mut factory_impl, &mut errors);
let widgets_struct = widgets_name.map(|ty| {
let outer_attrs = &factory_impl.attrs;
quote! {
#[allow(dead_code)]
#(#outer_attrs)*
#[derive(Debug)]
#visibility struct #ty {
#struct_fields
#additional_fields
}
}
});
let errors = errors.iter().map(syn::Error::to_compile_error);
let async_trait = asyncness.map(
|async_token| quote_spanned!(async_token.span => #[relm4::async_trait::async_trait(?Send)]),
);
quote! {
#widgets_struct
#async_trait
#factory_impl
#(#errors)*
}
}

View File

@ -1,648 +0,0 @@
//! A collection of macros for gtk-rs, Relm4 and Rust in general.
//!
//! Docs of related crates:
//! [relm4](https://docs.rs/relm4)
//! | [relm4-macros](https://docs.rs/relm4_macros)
//! | [relm4-components](https://docs.rs/relm4_components)
//! | [gtk4-rs](https://gtk-rs.org/gtk4-rs/git/docs)
//! | [gtk-rs-core](https://gtk-rs.org/gtk-rs-core/git/docs)
//! | [libadwaita-rs](https://world.pages.gitlab.gnome.org/Rust/libadwaita-rs/git/docs/libadwaita)
//! | [libpanel-rs](https://world.pages.gitlab.gnome.org/Rust/libpanel-rs/git/docs/libpanel)
//!
//! [GitHub](https://github.com/Relm4/Relm4)
//! | [Website](https://relm4.org)
//! | [Book](https://relm4.org/book/stable/)
//! | [Blog](https://relm4.org/blog)
#![doc(html_logo_url = "https://relm4.org/icons/relm4_logo.svg")]
#![doc(html_favicon_url = "https://relm4.org/icons/relm4_org.svg")]
#![warn(
missing_debug_implementations,
missing_docs,
rust_2018_idioms,
unreachable_pub,
unused_qualifications,
clippy::cargo,
clippy::must_use_candidate
)]
use proc_macro::TokenStream;
use syn::{parse_macro_input, ItemImpl};
mod additional_fields;
mod args;
mod attrs;
mod component;
mod menu;
mod view;
mod visitors;
mod widgets;
#[macro_use]
mod util;
mod factory;
mod token_streams;
mod widget_template;
use attrs::{Attrs, SyncOnlyAttrs};
use menu::Menus;
fn gtk_import() -> syn::Path {
if cfg!(feature = "relm4") {
util::strings_to_path(&["relm4", "gtk"])
} else {
util::strings_to_path(&["gtk"])
}
}
/// Macro that implements `relm4::Component` or `relm4::SimpleComponent`
/// and generates the corresponding widgets struct.
///
/// # Attributes
///
/// To create public struct use `#[component(pub)]` or `#[component(visibility = pub)]`.
///
/// # Example
///
/// ```
/// use relm4::prelude::*;
/// use gtk::prelude::*;
///
/// #[derive(Default)]
/// struct App {
/// counter: u8,
/// }
///
/// #[derive(Debug)]
/// enum Msg {
/// Increment,
/// Decrement,
/// }
///
/// #[relm4_macros::component(pub)]
/// impl SimpleComponent for App {
/// type Init = u8;
/// type Input = Msg;
/// type Output = ();
///
/// view! {
/// gtk::Window {
/// set_title: Some("Simple app"),
/// set_default_size: (300, 100),
/// gtk::Box {
/// set_orientation: gtk::Orientation::Vertical,
/// set_margin_all: 5,
/// set_spacing: 5,
///
/// gtk::Button {
/// set_label: "Increment",
/// connect_clicked => Msg::Increment,
/// },
/// gtk::Button {
/// set_label: "Decrement",
/// connect_clicked[sender] => move |_| {
/// sender.input(Msg::Decrement);
/// },
/// },
/// gtk::Label {
/// set_margin_all: 5,
/// #[watch]
/// set_label: &format!("Counter: {}", model.counter),
/// }
/// },
/// }
/// }
///
/// fn init(
/// counter: Self::Init,
/// root: &Self::Root,
/// sender: ComponentSender<Self>,
/// ) -> ComponentParts<Self> {
/// let model = Self { counter };
///
/// let widgets = view_output!();
///
/// ComponentParts { model, widgets }
/// }
///
/// fn update(&mut self, msg: Msg, _sender: ComponentSender<Self>) {
/// match msg {
/// Msg::Increment => {
/// self.counter = self.counter.wrapping_add(1);
/// }
/// Msg::Decrement => {
/// self.counter = self.counter.wrapping_sub(1);
/// }
/// }
/// }
/// }
/// ```
///
/// # Notes on `pre_view`
///
/// Using `return` in `pre_view` will cause a compiler warning.
/// In general, you don't want to use `return` in `pre_view` as it will
/// cause all following update functionality to be skipped.
///
/// ```compile_fail
/// # use gtk::prelude::{BoxExt, ButtonExt, GtkWindowExt, OrientableExt};
/// # use relm4::{gtk, ComponentParts, ComponentSender, SimpleComponent, RelmWidgetExt};
/// #
/// struct App {}
///
/// #[relm4_macros::component]
/// impl SimpleComponent for App {
/// /* Code omitted */
/// # type Init = ();
/// # type Input = ();
/// # type Output = ();
/// #
/// # view! {
/// # gtk::Window {}
/// # }
///
/// fn pre_view() {
/// return;
/// }
/// #
/// # fn init(
/// # counter: Self::Init,
/// # root: &Self::Root,
/// # sender: ComponentSender<Self>,
/// # ) -> ComponentParts<Self> {
/// # let model = Self {};
/// #
/// # let widgets = view_output!();
/// #
/// # ComponentParts { model, widgets }
/// # }
/// }
/// ```
#[proc_macro_attribute]
pub fn component(attributes: TokenStream, input: TokenStream) -> TokenStream {
let global_attributes: Attrs = parse_macro_input!(attributes);
let backup_input = input.clone();
let component_impl_res = syn::parse::<ItemImpl>(input);
match component_impl_res {
Ok(component_impl) => component::generate_tokens(global_attributes, component_impl).into(),
Err(_) => util::item_impl_error(backup_input),
}
}
/// Macro that implements `relm4::factory::FactoryComponent` and generates the corresponding widgets struct.
///
/// # Attributes
///
/// To create public struct use `#[factory(pub)]` or `#[factory(visibility = pub)]`.
///
/// # Example
///
/// ```
/// use relm4::prelude::*;
/// use relm4::factory::*;
/// use gtk::prelude::*;
///
/// # #[derive(Debug)]
/// # enum AppMsg {
/// # AddCounter,
/// # RemoveCounter,
/// # SendFront(DynamicIndex)
/// # }
///
/// #[derive(Debug)]
/// struct Counter {
/// value: u8,
/// }
///
/// #[derive(Debug)]
/// enum CounterMsg {
/// Increment,
/// Decrement,
/// }
///
/// #[derive(Debug)]
/// enum CounterOutput {
/// SendFront(DynamicIndex),
/// }
///
/// #[relm4_macros::factory(pub)]
/// impl FactoryComponent for Counter {
/// type CommandOutput = ();
/// type Init = u8;
/// type Input = CounterMsg;
/// type Output = CounterOutput;
/// type ParentInput = AppMsg;
/// type ParentWidget = gtk::Box;
///
///
/// view! {
/// root = gtk::Box {
/// set_orientation: gtk::Orientation::Horizontal,
/// set_spacing: 10,
///
/// #[name(label)]
/// gtk::Label {
/// #[watch]
/// set_label: &self.value.to_string(),
/// set_width_chars: 3,
/// },
///
/// #[name(add_button)]
/// gtk::Button {
/// set_label: "+",
/// connect_clicked => CounterMsg::Increment,
/// },
///
/// #[name(remove_button)]
/// gtk::Button {
/// set_label: "-",
/// connect_clicked => CounterMsg::Decrement,
/// },
///
/// #[name(to_front_button)]
/// gtk::Button {
/// set_label: "To start",
/// connect_clicked[sender, index] => move |_| {
/// sender.output(CounterOutput::SendFront(index.clone()))
/// }
/// }
/// }
/// }
///
/// fn forward_to_parent(output: Self::Output) -> Option<AppMsg> {
/// Some(match output {
/// CounterOutput::SendFront(index) => AppMsg::SendFront(index),
/// })
/// }
///
/// fn init_model(
/// value: Self::Init,
/// _index: &DynamicIndex,
/// _sender: FactorySender<Self>,
/// ) -> Self {
/// Self { value }
/// }
///
/// fn update(&mut self, msg: Self::Input, _sender: FactorySender<Self>) {
/// match msg {
/// CounterMsg::Increment => {
/// self.value = self.value.wrapping_add(1);
/// }
/// CounterMsg::Decrement => {
/// self.value = self.value.wrapping_sub(1);
/// }
/// }
/// }
/// }
/// ```
#[proc_macro_attribute]
pub fn factory(attributes: TokenStream, input: TokenStream) -> TokenStream {
let attrs = parse_macro_input!(attributes);
let backup_input = input.clone();
let factory_impl_res = syn::parse::<ItemImpl>(input);
match factory_impl_res {
Ok(factory_impl) => factory::generate_tokens(attrs, factory_impl).into(),
Err(_) => util::item_impl_error(backup_input),
}
}
/// A macro to create menus.
///
/// Use
///
/// + `"Label text" => ActionType,` to create new entries.
/// + `"Label text" => ActionType(value),` to create new entries with action value.
/// + `custom => "widget_id",` add a placeholder for custom widgets you can add later with [`set_attribute_name`](https://gtk-rs.org/gtk-rs-core/stable/0.15/docs/gio/struct.MenuItem.html#method.set_attribute_value).
/// + `section! { ... }` to create new sections.
///
/// # Example
///
/// ```
/// # fn gettext(string: &str) -> String {
/// # string.to_owned()
/// # }
/// #
/// // Define some actions
/// relm4::new_action_group!(WindowActionGroup, "win");
/// relm4::new_stateless_action!(TestAction, WindowActionGroup, "test");
/// relm4::new_stateful_action!(TestU8Action, WindowActionGroup, "test2", u8, u8);
///
/// // Create a `MenuModel` called `menu_model`
/// relm4_macros::menu! {
/// main_menu: {
/// custom: "my_widget",
/// // Translate with gettext-rs, for example.
/// &gettext("Test") => TestAction,
/// "Test2" => TestAction,
/// "Test toggle" => TestU8Action(1_u8),
/// section! {
/// "Section test" => TestAction,
/// "Test toggle" => TestU8Action(1_u8),
/// },
/// section! {
/// "Test" => TestAction,
/// "Test2" => TestAction,
/// "Test Value" => TestU8Action(1_u8),
/// }
/// }
/// };
/// ```
///
/// # Macro expansion
///
/// The code generation for the example above looks like this (plus comments):
///
/// ```
/// # fn gettext(string: &str) -> String {
/// # string.to_owned()
/// # }
/// #
/// struct WindowActionGroup;
/// impl relm4::actions::ActionGroupName for WindowActionGroup {
/// const NAME: &'static str = "win";
/// }
///
/// struct TestAction;
/// impl relm4::actions::ActionName for TestAction {
/// type Group = WindowActionGroup;
/// type State = ();
/// type Target = ();
///
/// const NAME: &'static str = "test";
/// }
///
/// struct TestU8Action;
/// impl relm4::actions::ActionName for TestU8Action {
/// type Group = WindowActionGroup;
/// type State = u8;
/// type Target = u8;
///
/// const NAME: &'static str = "test2";
/// }
///
/// // Main menu
/// let main_menu = relm4::gtk::gio::Menu::new();
///
/// // Placeholder for custom widget
/// let new_entry = relm4::gtk::gio::MenuItem::new(None, None);
/// let variant = relm4::gtk::glib::variant::ToVariant::to_variant("my_widget");
/// new_entry.set_attribute_value("custom", Some(&variant));
/// main_menu.append_item(&new_entry);
///
/// let new_entry = relm4::actions::RelmAction::<TestAction>::to_menu_item(&gettext("Test"));
/// main_menu.append_item(&new_entry);
/// let new_entry = relm4::actions::RelmAction::<TestAction>::to_menu_item("Test2");
/// main_menu.append_item(&new_entry);
/// let new_entry = relm4::actions::RelmAction::<TestU8Action>::to_menu_item_with_target_value(
/// "Test toggle",
/// &1_u8,
/// );
/// main_menu.append_item(&new_entry);
///
/// // Section 0
/// let _section_0 = relm4::gtk::gio::Menu::new();
/// main_menu.append_section(None, &_section_0);
/// let new_entry = relm4::actions::RelmAction::<TestAction>::to_menu_item("Section test");
/// _section_0.append_item(&new_entry);
/// let new_entry = relm4::actions::RelmAction::<TestU8Action>::to_menu_item_with_target_value(
/// "Test toggle",
/// &1_u8,
/// );
/// _section_0.append_item(&new_entry);
///
/// // Section 1
/// let _section_1 = relm4::gtk::gio::Menu::new();
/// main_menu.append_section(None, &_section_1);
/// let new_entry = relm4::actions::RelmAction::<TestAction>::to_menu_item("Test");
/// _section_1.append_item(&new_entry);
/// let new_entry = relm4::actions::RelmAction::<TestAction>::to_menu_item("Test2");
/// _section_1.append_item(&new_entry);
/// let new_entry = relm4::actions::RelmAction::<TestU8Action>::to_menu_item_with_target_value(
/// "Test Value",
/// &1_u8,
/// );
/// _section_1.append_item(&new_entry);
/// ```
#[proc_macro]
pub fn menu(input: TokenStream) -> TokenStream {
let menus = parse_macro_input!(input as Menus);
menus.menus_stream().into()
}
/// The [`view!`] macro allows you to construct your UI easily and cleanly.
///
/// It does the same as inside the [`macro@component`] attribute macro,
/// but with less features.
///
/// You can even use the `relm4-macros` crate independently from Relm4 to build your GTK4 UI.
///
/// ```no_run
/// use gtk::prelude::{BoxExt, ButtonExt};
/// use relm4::gtk;
///
/// // Creating a box with a button inside.
/// relm4_macros::view! {
/// vbox = gtk::Box {
/// gtk::Button {
/// set_label: "Click me!",
/// connect_clicked => |_| {
/// println!("Hello world!");
/// }
/// },
/// prepend: my_label = &gtk::Label::builder()
/// .label("The view macro works!")
/// .build(),
/// }
/// }
///
/// // You can simply use the vbox created in the macro.
/// let spacing = vbox.spacing();
/// ```
///
/// Also, the macro doesn't rely on any special gtk4-rs features
/// so you can even use the macro for other purposes.
///
/// In this example, we use it to construct a [`Command`](std::process::Command).
///
/// ```
/// use std::process::Command;
///
/// let path = "/";
///
/// relm4_macros::view! {
/// mut process = Command::new("ls") {
/// args: ["-la"],
/// current_dir = mut &String {
/// push_str: path,
/// },
/// env: ("HOME", "/home/relm4"),
/// }
/// }
///
/// // Output of "ls -la" at "/"
/// dbg!(process.output());
/// ```
/// # Macro expansion
///
/// Let's have a look the this example:
///
/// ```no_run
/// # use gtk::prelude::{BoxExt, ButtonExt};
/// # use relm4::gtk;
/// // Creating a box with a button inside.
/// relm4_macros::view! {
/// vbox = gtk::Box {
/// gtk::Button {
/// set_label: "Click me!",
/// connect_clicked => |_| {
/// println!("Hello world!");
/// }
/// },
/// prepend: my_label = &gtk::Label::builder()
/// .label("The view macro works!")
/// .build(),
/// }
/// }
/// ```
///
/// The code generation for this example looks like this (plus comments):
///
/// ```no_run
/// # use gtk::prelude::{BoxExt, ButtonExt};
/// # use relm4::gtk;
///
/// // We've just used `gtk::Box` so we assume it has a `default()` method
/// let vbox = gtk::Box::default();
/// // `vbox` was named, yet the button doesn't have an explicit name and gets a generated one instead.
/// let _gtk_button_5 = gtk::Button::default();
/// // For the label, we used a manual constructor method, so no `default()` method is required.
/// let my_label = gtk::Label::builder().label("The view macro works!").build();
///
/// // Connect the signal
/// {
/// _gtk_button_5.connect_clicked(|_| {
/// println!("Hello world!");
/// });
/// }
///
/// // The button was added without any further instructions, so we assume `container_add()` will work.
/// relm4::RelmContainerExt::container_add(&vbox, &_gtk_button_5);
/// _gtk_button_5.set_label("Click me!");
/// // For the label, we used the `prepend` method, so we don't need `container_add()` here.
/// vbox.prepend(&my_label);
/// ```
///
/// The widgets are first initialized, then signals are connected and then
/// properties and widgets are assigned to each other.
///
/// The nested structure of the UI is translated into regular Rust code.
#[proc_macro]
pub fn view(input: TokenStream) -> TokenStream {
view::generate_tokens(input)
}
/// A macro to generate widget templates.
///
/// This macro generates a new type that implements `relm4::WidgetTemplate`.
///
/// # Example
///
/// ```
/// use relm4::prelude::*;
/// use gtk::prelude::*;
///
/// #[relm4::widget_template]
/// impl WidgetTemplate for MyBox {
/// view! {
/// gtk::Box {
/// set_margin_all: 10,
/// // Make the boxes visible
/// inline_css: "border: 2px solid blue",
/// }
/// }
/// }
/// ```
///
/// The template allows you the generate deeply nested
/// structures. All named items will be directly accessible
/// as a child of the template, even if they are nested.
/// In this example the "child_label" is a template child.
///
/// ```
/// # use relm4::prelude::*;
/// # use gtk::prelude::*;
/// #
/// # #[relm4::widget_template]
/// # impl WidgetTemplate for MyBox {
/// # view! {
/// # gtk::Box {
/// # set_margin_all: 10,
/// # // Make the boxes visible
/// # inline_css: "border: 2px solid blue",
/// # }
/// # }
/// # }
/// #
/// #[relm4::widget_template]
/// impl WidgetTemplate for MySpinner {
/// view! {
/// gtk::Spinner {
/// set_spinning: true,
/// }
/// }
/// }
///
/// #[relm4::widget_template]
/// impl WidgetTemplate for CustomBox {
/// view! {
/// gtk::Box {
/// set_orientation: gtk::Orientation::Vertical,
/// set_margin_all: 5,
/// set_spacing: 5,
///
/// #[template]
/// MyBox {
/// #[template]
/// MySpinner,
///
/// #[template]
/// MyBox {
/// #[template]
/// MySpinner,
///
/// #[template]
/// MyBox {
/// #[template]
/// MySpinner,
///
/// // Deeply nested!
/// #[name = "child_label"]
/// gtk::Label {
/// set_label: "This is a test",
/// }
/// }
/// }
/// }
/// }
/// }
/// }
/// ```
#[proc_macro_attribute]
pub fn widget_template(attributes: TokenStream, input: TokenStream) -> TokenStream {
let SyncOnlyAttrs { visibility } = parse_macro_input!(attributes);
let item_impl = parse_macro_input!(input as ItemImpl);
widget_template::generate_tokens(visibility, item_impl).into()
}
#[cfg(test)]
#[rustversion::all(stable, since(1.69))]
mod test {
#[test]
fn ui() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/compile-fail/**/*.rs");
}
}

View File

@ -1,136 +0,0 @@
use proc_macro2::{Span as Span2, TokenStream as TokenStream2};
use quote::{quote, quote_spanned};
use syn::{spanned::Spanned, Ident, LitStr};
use super::{Menu, MenuElement, MenuEntry, MenuItem, MenuSection, Menus, SubMenu};
impl Menus {
pub(crate) fn menus_stream(&self) -> TokenStream2 {
let mut menu_stream = TokenStream2::new();
for item in &self.items {
menu_stream.extend(item.menu_stream());
}
menu_stream
}
}
impl Menu {
fn menu_stream(&self) -> TokenStream2 {
let name = &self.name;
let gtk_import = crate::gtk_import();
// Create new menu
let mut menu_stream = quote_spanned! {
name.span() =>
let #name = #gtk_import ::gio::Menu::new();
};
// Add items
for item in &self.items {
menu_stream.extend(item.item_stream(name));
}
menu_stream
}
}
impl MenuElement {
fn item_stream(&self, parent_ident: &Ident) -> TokenStream2 {
let mut item_stream = TokenStream2::new();
item_stream.extend(match self {
Self::Item(entry) => entry.item_stream(parent_ident),
Self::Section(section) => section.section_stream(parent_ident),
Self::Custom(id) => custom_stream(parent_ident, id),
});
item_stream
}
}
fn custom_stream(parent_ident: &Ident, id: &LitStr) -> TokenStream2 {
let gtk_import = crate::gtk_import();
quote_spanned! {
id.span() =>
let new_entry = #gtk_import::gio::MenuItem::new(None, None);
let variant = #gtk_import::glib::variant::ToVariant::to_variant(#id);
new_entry.set_attribute_value("custom", Some(&variant));
#parent_ident.append_item(&new_entry);
}
}
impl MenuItem {
fn item_stream(&self, parent_ident: &Ident) -> TokenStream2 {
match self {
Self::Entry(entry) => entry.entry_stream(parent_ident),
Self::SubMenu(sub_menu) => sub_menu.submenu_stream(parent_ident),
}
}
}
impl SubMenu {
fn submenu_stream(&self, parent_ident: &Ident) -> TokenStream2 {
let name = Ident::new(&format!("_{parent_ident}"), Span2::call_site());
let gtk_import = crate::gtk_import();
let expr = &self.expr;
// Create new sub-menu
let mut item_stream = quote_spanned! {
expr.span() =>
let #name = #gtk_import ::gio::Menu::new();
#parent_ident.append_submenu(Some(#expr), &#name);
};
// Add items
for item in &self.items {
item_stream.extend(item.item_stream(&name));
}
// Wrap the generated code in a new scope to avoid side-effects
quote! {
{
#item_stream
}
}
}
}
impl MenuEntry {
fn entry_stream(&self, parent_ident: &Ident) -> TokenStream2 {
let expr = &self.expr;
let ty = &self.action_ty;
if let Some(value) = &self.value {
quote_spanned! {
expr.span() =>
let new_entry = relm4::actions::RelmAction::<#ty>::to_menu_item_with_target_value(#expr, &#value);
#parent_ident.append_item(&new_entry);
}
} else {
quote_spanned! {
expr.span() =>
let new_entry = relm4::actions::RelmAction::<#ty>::to_menu_item(#expr);
#parent_ident.append_item(&new_entry);
}
}
}
}
impl MenuSection {
fn section_stream(&self, parent_ident: &Ident) -> TokenStream2 {
let name = &self.name;
let gtk_import = crate::gtk_import();
let mut section_stream = quote! {
let #name = #gtk_import::gio::Menu::new();
#parent_ident.append_section(None, &#name);
};
for item in &self.items {
section_stream.extend(item.item_stream(name));
}
section_stream
}
}

View File

@ -1,49 +0,0 @@
use syn::punctuated::Punctuated;
use syn::token::Comma;
use syn::{Expr, Ident, LitStr, Path};
mod gen;
mod parse;
#[derive(Debug)]
pub(crate) struct Menus {
items: Punctuated<Menu, Comma>,
}
#[derive(Debug)]
struct Menu {
name: Ident,
items: Punctuated<MenuElement, Comma>,
}
#[derive(Debug)]
enum MenuElement {
Item(Box<MenuItem>),
Custom(LitStr),
Section(MenuSection),
}
#[derive(Debug)]
enum MenuItem {
Entry(Box<MenuEntry>),
SubMenu(Box<SubMenu>),
}
#[derive(Debug)]
struct MenuEntry {
expr: Expr,
action_ty: Path,
value: Option<Expr>,
}
#[derive(Debug)]
struct SubMenu {
expr: Expr,
items: Punctuated<MenuElement, Comma>,
}
#[derive(Debug)]
struct MenuSection {
name: Ident,
items: Punctuated<MenuElement, Comma>,
}

View File

@ -1,103 +0,0 @@
use proc_macro2::Span as Span2;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{braced, parenthesized, token, Ident, Path, Result, Token};
use crate::menu::SubMenu;
use super::{Menu, MenuElement, MenuEntry, MenuItem, MenuSection, Menus};
syn::custom_keyword!(custom);
impl Parse for Menus {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let items = input.call(Punctuated::parse_separated_nonempty)?;
Ok(Menus { items })
}
}
impl Parse for Menu {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let name = input.parse()?;
let _colon: Token![:] = input.parse()?;
let braced_input;
braced!(braced_input in input);
let items = braced_input.call(Punctuated::parse_terminated)?;
Ok(Menu { name, items })
}
}
impl Parse for MenuItem {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let expr = input.parse()?;
Ok(if input.peek(Token![=>]) {
let _arrow: Token![=>] = input.parse()?;
let action_ty = input.call(Path::parse_mod_style)?;
let value = if input.peek(token::Paren) {
let paren_input;
parenthesized!(paren_input in input);
Some(paren_input.parse()?)
} else {
None
};
Self::Entry(Box::new(MenuEntry {
expr,
action_ty,
value,
}))
} else {
let braced_input;
braced!(braced_input in input);
let items = braced_input.call(Punctuated::parse_terminated)?;
Self::SubMenu(Box::new(SubMenu { expr, items }))
})
}
}
impl Parse for MenuElement {
fn parse(input: ParseStream<'_>) -> Result<Self> {
Ok(if input.peek(custom) {
let _custom: custom = input.parse()?;
let _colon: Token![:] = input.parse()?;
input.parse().map(MenuElement::Custom)?
} else if input.peek2(Token![!]) {
input.parse().map(MenuElement::Section)?
} else {
input.parse().map(MenuElement::Item)?
})
}
}
impl Parse for MenuSection {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let name: Ident = input.parse()?;
assert!(name == "section");
let _excl: Token![!] = input.parse()?;
let braced_input;
braced!(braced_input in input);
let items = braced_input.call(Punctuated::parse_terminated)?;
let name = section_name();
Ok(MenuSection { name, items })
}
}
fn section_name() -> Ident {
use std::sync::atomic::{AtomicU8, Ordering};
static COUNTER: AtomicU8 = AtomicU8::new(0);
let value = COUNTER.fetch_add(1, Ordering::Relaxed);
Ident::new(&format!("_section_{value}"), Span2::call_site())
}

View File

@ -1,147 +0,0 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens};
use syn::{Error, Ident, Visibility};
use crate::widgets::{TopLevelWidget, ViewWidgets, Widget};
#[derive(Default)]
pub(super) struct TokenStreams {
/// Parsing errors
pub(super) error: TokenStream2,
/// Initialize the root widget.
pub(super) init_root: TokenStream2,
/// Rename root to the actual widget name.
pub(super) rename_root: TokenStream2,
/// The tokens for the struct fields -> name: Type,
pub(super) struct_fields: TokenStream2,
/// The tokens initializing the widgets.
pub(super) init: TokenStream2,
/// The tokens initializing the properties.
pub(super) assign: TokenStream2,
/// The tokens for the returned struct fields -> name,
pub(super) return_fields: TokenStream2,
/// For destructuring the widget struct field
pub(super) destructure_fields: TokenStream2,
/// The view tokens (watch! macro)
pub(super) update_view: TokenStream2,
}
pub(super) struct TraitImplDetails {
/// The visibility of the widgets struct.
pub(super) vis: Option<Visibility>,
/// The name of the model.
pub(super) model_name: Ident,
/// The name of the root widget.
pub(super) root_name: Option<Ident>,
/// The name of the sender used in the init function.
pub(super) sender_name: Ident,
}
impl ViewWidgets {
pub(super) fn generate_streams(
&self,
trait_impl_details: &TraitImplDetails,
standalone_view: bool,
) -> TokenStreams {
let mut streams = TokenStreams::default();
for top_level_widget in &self.top_level_widgets {
top_level_widget.generate_streams(&mut streams, trait_impl_details, standalone_view);
}
streams
}
/// Get the root widget
pub(super) fn get_root_widget(&self) -> syn::Result<&Widget> {
self.top_level_widgets
.iter()
.find(|w| w.root_attr.is_some())
.map(|w| &w.inner)
.ok_or_else(|| {
Error::new(
self.span,
"You need to specify the root widget using the `#[root]` attribute.",
)
})
}
/// Generate root type for `Root` parameter in `Component` impl
pub(super) fn root_type(&self) -> TokenStream2 {
match self.get_root_widget() {
Ok(root_widget) => root_widget.func_type_token_stream(),
Err(err) => err.to_compile_error(),
}
}
/// Get the name of the root widget
pub(super) fn root_name(&self) -> TokenStream2 {
match self.get_root_widget() {
Ok(root_widget) => root_widget.name.to_token_stream(),
Err(err) => err.to_compile_error(),
}
}
}
impl TopLevelWidget {
fn generate_streams(
&self,
streams: &mut TokenStreams,
trait_impl_details: &TraitImplDetails,
standalone_view: bool,
) {
let generate_init_root_stream = !standalone_view && self.root_attr.is_some();
self.inner
.init_token_generation(streams, trait_impl_details, generate_init_root_stream);
}
}
impl Widget {
pub(super) fn init_token_generation(
&self,
streams: &mut TokenStreams,
trait_impl_details: &TraitImplDetails,
generate_root_init_stream: bool,
) {
let TraitImplDetails {
vis,
model_name,
root_name,
sender_name,
} = trait_impl_details;
let name = &self.name;
// Initialize the root
if generate_root_init_stream {
// For the `component` macro
self.init_root_init_streams(&mut streams.init_root, &mut streams.init);
} else {
// For the `view!` macro
self.init_stream(&mut streams.init);
}
#[cfg(feature = "relm4")]
streams.assign.extend(quote::quote! {
use relm4::RelmContainerExt as _;
});
self.error_stream(&mut streams.error);
self.start_assign_stream(&mut streams.assign, sender_name);
self.init_conditional_init_stream(&mut streams.assign, model_name);
self.struct_fields_stream(&mut streams.struct_fields, vis);
self.return_stream(&mut streams.return_fields);
self.destructure_stream(&mut streams.destructure_fields);
self.init_update_view_stream(&mut streams.update_view, model_name);
// Rename the `root` to the actual widget name
if generate_root_init_stream {
let mut_token = self.mutable.as_ref();
if let Some(root_name) = root_name {
streams.rename_root.extend(quote! {
let #mut_token #name = #root_name.clone();
});
}
}
}
}

View File

@ -1,135 +0,0 @@
use proc_macro::TokenStream;
use proc_macro2::{Span as Span2, TokenStream as TokenStream2};
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::{FnArg, Ident, ImplItem, ItemImpl, Path, PathArguments, PathSegment, Type, TypePath};
pub(super) fn generate_widgets_type(
widgets_ty: Option<Type>,
component_impl: &mut ItemImpl,
errors: &mut Vec<syn::Error>,
) -> Option<Type> {
// Use the widget type if used.
if let Some(ty) = widgets_ty {
Some(ty)
}
// Try to generate the type from the self type name.
else if let Type::Path(self_ty) = &*component_impl.self_ty {
let (path, impl_item) = self_ty_to_widgets_ty(self_ty);
component_impl.items.push(impl_item);
Some(path)
}
// Error: No Widget type found or generated.
else {
let msg = "no `Widgets` type found and the type if `Self` in not a path. \
Please use a path for `Self` or use `type Widgets = WidgetsName;` to name the widgets type.";
errors.push(syn::Error::new(
component_impl
.items
.first()
.map(|i| i.span())
.unwrap_or_else(|| component_impl.self_ty.span()),
msg,
));
None
}
}
pub(super) fn self_ty_to_widgets_ty(self_ty: &TypePath) -> (Type, ImplItem) {
// Retrieve path, remove any generics and append "Widgets" to the last segment.
let mut self_path = self_ty.clone();
let last_seg = self_path.path.segments.last_mut().unwrap();
last_seg.arguments = Default::default();
last_seg.ident = Ident::new(&format!("{}Widgets", last_seg.ident), last_seg.span());
// Generate impl item for the trait impl
let impl_item = syn::parse_quote_spanned! {
self_path.span() => type Widgets = #self_path;
};
(Type::Path(self_path), impl_item)
}
pub(super) fn strings_to_path(strings: &[&str]) -> Path {
let path_segments: Vec<PathSegment> = strings
.iter()
.map(|string| -> PathSegment {
PathSegment {
ident: Ident::new(string, Span2::call_site()),
arguments: PathArguments::None,
}
})
.collect();
Path {
leading_colon: None,
segments: Punctuated::from_iter(path_segments),
}
}
pub(super) fn item_impl_error(original_input: TokenStream) -> TokenStream {
let macro_impls = quote::quote! {
macro_rules! view_output {
() => { () };
}
macro_rules! view {
() => {};
($tt:tt) => {};
($tt:tt $($y:tt)+) => {}
}
}
.into();
vec![macro_impls, original_input].into_iter().collect()
}
pub(super) fn verbatim_impl_item_fn(
name: &str,
args: Vec<FnArg>,
ty: Type,
tokens: TokenStream2,
) -> ImplItem {
ImplItem::Fn(syn::ImplItemFn {
attrs: Vec::new(),
vis: syn::Visibility::Inherited,
defaultness: None,
sig: syn::Signature {
constness: None,
asyncness: None,
unsafety: None,
abi: None,
fn_token: syn::token::Fn::default(),
ident: Ident::new(name, Span2::call_site()),
generics: syn::Generics {
lt_token: None,
params: Punctuated::default(),
gt_token: None,
where_clause: None,
},
paren_token: syn::token::Paren::default(),
inputs: Punctuated::from_iter(args),
variadic: None,
output: syn::ReturnType::Type(syn::token::RArrow::default(), Box::new(ty)),
},
block: syn::Block {
brace_token: syn::token::Brace::default(),
stmts: vec![syn::Stmt::Expr(syn::Expr::Verbatim(tokens), None)],
},
})
}
/// Returns the identifier for a parameter if it is a binding with an identifier pattern.
pub(super) fn extract_arg_ident(arg: &FnArg) -> syn::Result<&Ident> {
match arg {
syn::FnArg::Typed(pat_type) => match &*pat_type.pat {
syn::Pat::Ident(ident) => Ok(&ident.ident),
pat => Err(syn::Error::new_spanned(
pat,
"parameter binding must be an identifier",
)),
},
syn::FnArg::Receiver(_) => Err(syn::Error::new_spanned(
arg,
"parameter binding must be an identifier",
)),
}
}

View File

@ -1,35 +0,0 @@
use proc_macro::TokenStream;
use proc_macro2::Span as Span2;
use quote::quote;
use syn::{parse_macro_input, Ident};
use crate::token_streams::{TokenStreams, TraitImplDetails};
use crate::widgets::ViewWidgets;
pub(super) fn generate_tokens(input: TokenStream) -> TokenStream {
let view_widgets: ViewWidgets = parse_macro_input!(input);
let TokenStreams {
error,
init,
assign,
..
} = view_widgets.generate_streams(
&TraitImplDetails {
vis: None,
model_name: Ident::new("_", Span2::call_site()),
sender_name: Ident::new("sender", Span2::call_site()),
root_name: None,
},
true,
);
let output = quote! {
#init
#assign
{
#error
}
};
output.into()
}

View File

@ -1,479 +0,0 @@
use std::mem;
use proc_macro2::TokenStream;
use quote::quote;
use syn::visit::{self, Visit};
use syn::visit_mut::{self, VisitMut};
use syn::LocalInit;
use crate::additional_fields::AdditionalFields;
use crate::menu::Menus;
use crate::util;
use crate::widgets::ViewWidgets;
#[derive(Debug)]
pub(super) struct ComponentVisitor<'errors> {
pub(super) view_widgets: Option<syn::Result<ViewWidgets>>,
pub(super) widgets_ty: Option<syn::Type>,
pub(super) root_name: Option<syn::Ident>,
pub(super) model_name: Option<syn::Ident>,
pub(super) sender_name: Option<syn::Ident>,
pub(super) additional_fields: Option<AdditionalFields>,
pub(super) menus: Option<Menus>,
pub(super) errors: &'errors mut Vec<syn::Error>,
}
impl<'errors> ComponentVisitor<'errors> {
pub(super) fn new(errors: &'errors mut Vec<syn::Error>) -> Self {
ComponentVisitor {
view_widgets: None,
widgets_ty: None,
root_name: None,
model_name: None,
sender_name: None,
additional_fields: None,
menus: None,
errors,
}
}
}
impl VisitMut for ComponentVisitor<'_> {
fn visit_impl_item_mut(&mut self, item: &mut syn::ImplItem) {
let mut remove = false;
match item {
syn::ImplItem::Macro(mac) => {
match mac.mac.path.get_ident().map(ToString::to_string).as_deref() {
Some("view") => {
if self.view_widgets.is_some() {
self.errors
.push(syn::Error::new_spanned(&mac, "duplicate view macro"));
}
self.view_widgets.replace(mac.mac.parse_body());
remove = true;
}
Some("additional_fields") => {
match mac.mac.parse_body::<AdditionalFields>() {
Ok(fields) => {
let existing = self.additional_fields.replace(fields);
if existing.is_some() {
self.errors.push(syn::Error::new_spanned(
mac,
"duplicate additional_fields macro",
));
}
}
Err(e) => {
self.errors.push(e);
}
};
remove = true;
}
Some("menu") => {
match mac.mac.parse_body::<Menus>() {
Ok(menu) => {
let existing = self.menus.replace(menu);
if existing.is_some() {
self.errors
.push(syn::Error::new_spanned(mac, "duplicate menu macro"));
}
}
Err(e) => {
self.errors.push(e);
}
};
remove = true;
}
_ => (),
}
}
syn::ImplItem::Fn(func) => {
if &*func.sig.ident.to_string() == "init" {
let mut init_fn_visitor = InitFnVisitor::default();
init_fn_visitor.visit_impl_item_fn(func);
self.model_name = init_fn_visitor.model_name;
self.sender_name = init_fn_visitor.sender_name;
self.root_name = init_fn_visitor.root_name;
self.errors.append(&mut init_fn_visitor.errors);
}
}
_ => (),
}
if remove {
*item = null_item();
}
visit_mut::visit_impl_item_mut(self, item);
}
fn visit_impl_item_type_mut(&mut self, ty: &mut syn::ImplItemType) {
if ty.ident == "Widgets" {
self.widgets_ty = Some(ty.ty.clone());
}
}
}
#[derive(Debug)]
pub(super) struct FactoryComponentVisitor<'errors> {
pub(super) view_widgets: Option<syn::Result<ViewWidgets>>,
pub(super) widgets_ty: Option<syn::Type>,
pub(super) index_ty: Option<syn::Type>,
pub(super) init_widgets: Option<syn::ImplItemFn>,
pub(super) root_name: Option<syn::Ident>,
pub(super) additional_fields: Option<AdditionalFields>,
pub(super) menus: Option<Menus>,
pub(super) errors: &'errors mut Vec<syn::Error>,
}
impl<'errors> FactoryComponentVisitor<'errors> {
pub(super) fn new(errors: &'errors mut Vec<syn::Error>) -> Self {
FactoryComponentVisitor {
view_widgets: None,
widgets_ty: None,
index_ty: None,
init_widgets: None,
root_name: None,
additional_fields: None,
menus: None,
errors,
}
}
}
impl VisitMut for FactoryComponentVisitor<'_> {
fn visit_impl_item_mut(&mut self, item: &mut syn::ImplItem) {
let mut remove = false;
match item {
syn::ImplItem::Macro(mac) => {
match mac.mac.path.get_ident().map(ToString::to_string).as_deref() {
Some("view") => {
if self.view_widgets.is_some() {
self.errors
.push(syn::Error::new_spanned(&mac, "duplicate view macro"));
}
self.view_widgets.replace(mac.mac.parse_body());
remove = true;
}
Some("additional_fields") => {
match mac.mac.parse_body::<AdditionalFields>() {
Ok(fields) => {
let existing = self.additional_fields.replace(fields);
if existing.is_some() {
self.errors.push(syn::Error::new_spanned(
mac,
"duplicate additional_fields macro",
));
}
}
Err(e) => {
self.errors.push(e);
}
};
remove = true;
}
Some("menu") => {
match mac.mac.parse_body::<Menus>() {
Ok(menu) => {
let existing = self.menus.replace(menu);
if existing.is_some() {
self.errors
.push(syn::Error::new_spanned(mac, "duplicate menu macro"));
}
}
Err(e) => {
self.errors.push(e);
}
};
remove = true;
}
_ => (),
}
}
syn::ImplItem::Fn(func) => {
if &*func.sig.ident.to_string() == "init_widgets" {
let mut init_fn_visitor = InitWidgetsFnVisitor::default();
init_fn_visitor.visit_impl_item_fn(func);
self.root_name = init_fn_visitor.root_name;
self.errors.append(&mut init_fn_visitor.errors);
let existing = self.init_widgets.replace(func.clone());
if existing.is_some() {
self.errors.push(syn::Error::new_spanned(
func,
"duplicate init_widgets function",
));
}
}
}
_ => (),
}
if remove {
*item = null_item();
}
visit_mut::visit_impl_item_mut(self, item);
}
fn visit_impl_item_type_mut(&mut self, ty: &mut syn::ImplItemType) {
if ty.ident == "Widgets" {
self.widgets_ty = Some(ty.ty.clone());
} else if ty.ident == "Root" {
self.errors.push(syn::Error::new_spanned(
ty,
"`Root` type is defined by `view!` macro",
));
} else if ty.ident == "Index" {
self.index_ty = Some(ty.ty.clone());
}
}
}
#[derive(Debug, Default)]
struct InitFnVisitor {
root_name: Option<syn::Ident>,
model_name: Option<syn::Ident>,
sender_name: Option<syn::Ident>,
errors: Vec<syn::Error>,
}
impl<'ast> Visit<'ast> for InitFnVisitor {
fn visit_impl_item_fn(&mut self, func: &'ast syn::ImplItemFn) {
let Some(root_arg) = func.sig.inputs.iter().nth(1) else {
return;
};
let root_name = util::extract_arg_ident(root_arg);
match root_name {
Ok(root_name) => self.root_name = Some(root_name.clone()),
Err(e) => self.errors.push(e),
}
let Some(sender_arg) = func.sig.inputs.iter().nth(2) else {
return;
};
let sender_name = util::extract_arg_ident(sender_arg);
match sender_name {
Ok(sender_name) => self.sender_name = Some(sender_name.clone()),
Err(e) => self.errors.push(e),
}
visit::visit_impl_item_fn(self, func);
}
fn visit_expr_struct(&mut self, expr_struct: &'ast syn::ExprStruct) {
let ident = &expr_struct.path.segments.last().unwrap().ident;
if ident == "ComponentParts" || ident == "AsyncComponentParts" {
for field in &expr_struct.fields {
let member_name = match &field.member {
syn::Member::Named(ident) => Some(ident.to_string()),
syn::Member::Unnamed(_) => None,
};
if member_name.as_deref() == Some("model") {
let model_name = match &field.expr {
syn::Expr::Path(path) => {
if let Some(ident) = path.path.get_ident() {
Ok(ident.clone())
} else {
Err(syn::Error::new_spanned(
path,
"unable to determine model name",
))
}
}
_ => Err(syn::Error::new_spanned(
&field.expr,
"unable to determine model name",
)),
};
match model_name {
Ok(model_name) => self.model_name = Some(model_name),
Err(e) => self.errors.push(e),
}
}
}
}
visit::visit_expr_struct(self, expr_struct);
}
}
#[derive(Debug, Default)]
struct InitWidgetsFnVisitor {
root_name: Option<syn::Ident>,
errors: Vec<syn::Error>,
}
impl<'ast> Visit<'ast> for InitWidgetsFnVisitor {
fn visit_impl_item_fn(&mut self, func: &'ast syn::ImplItemFn) {
let Some(root_arg) = func.sig.inputs.iter().nth(2) else {
return;
};
let root_name = util::extract_arg_ident(root_arg);
match root_name {
Ok(root_name) => self.root_name = Some(root_name.clone()),
Err(e) => self.errors.push(e),
}
visit::visit_impl_item_fn(self, func);
}
}
#[derive(Debug)]
pub(super) struct PreAndPostView<'errors> {
pub(super) pre_view: Vec<syn::Stmt>,
pub(super) post_view: Vec<syn::Stmt>,
errors: &'errors mut Vec<syn::Error>,
}
impl<'errors> PreAndPostView<'errors> {
pub(super) fn extract(impl_: &mut syn::ItemImpl, errors: &'errors mut Vec<syn::Error>) -> Self {
let mut visitor = PreAndPostView {
pre_view: vec![],
post_view: vec![],
errors,
};
visitor.visit_item_impl_mut(impl_);
visitor
}
}
impl VisitMut for PreAndPostView<'_> {
fn visit_impl_item_mut(&mut self, item: &mut syn::ImplItem) {
if let syn::ImplItem::Fn(func) = item {
match &*func.sig.ident.to_string() {
"pre_view" => {
if !self.pre_view.is_empty() {
self.errors.push(syn::Error::new_spanned(
&func,
"duplicate pre_view function",
));
}
self.pre_view = func.block.stmts.clone();
*item = null_item();
}
"post_view" => {
if !self.post_view.is_empty() {
self.errors.push(syn::Error::new_spanned(
&func,
"duplicate post_view function",
));
}
self.post_view = func.block.stmts.clone();
*item = null_item();
}
_ => (),
}
}
visit_mut::visit_impl_item_mut(self, item);
}
}
/// Expands the `view_output!` macro expression in the `init` function.
pub(crate) struct ViewOutputExpander<'errors> {
/// Whether a `view_output!` macro expression has been successfully expanded.
expanded: bool,
/// View initialization code to inject before the view output.
view_code: TokenStream,
/// Widgets struct initialization.
widgets_init: Box<syn::Expr>,
errors: &'errors mut Vec<syn::Error>,
}
impl ViewOutputExpander<'_> {
pub(crate) fn expand(
item_impl: &mut syn::ItemImpl,
view_code: TokenStream,
widgets_init: Box<syn::Expr>,
errors: &mut Vec<syn::Error>,
) {
let mut expander = ViewOutputExpander {
expanded: false,
view_code,
widgets_init,
errors,
};
expander.visit_item_impl_mut(item_impl);
}
}
impl VisitMut for ViewOutputExpander<'_> {
fn visit_impl_item_fn_mut(&mut self, method: &mut syn::ImplItemFn) {
if method.sig.ident == "init" || method.sig.ident == "init_widgets" {
visit_mut::visit_impl_item_fn_mut(self, method);
if !self.expanded {
self.errors.push(syn::Error::new_spanned(method, "expected an injection point for the view macro. Try using `let widgets = view_output!();`"));
}
}
}
fn visit_stmt_mut(&mut self, stmt: &mut syn::Stmt) {
let mut expand = false;
if let syn::Stmt::Local(syn::Local {
init: Some(LocalInit { expr, .. }),
..
}) = stmt
{
if let syn::Expr::Macro(mac) = &**expr {
if mac.mac.path.is_ident("view_output") {
expand = true;
}
}
if expand {
// Replace the macro invocation with the widget initialization code. Perform the
// swap in-place to avoid a clone.
mem::swap(expr, &mut self.widgets_init);
}
}
if expand {
let view_code = &self.view_code;
*stmt = syn::Stmt::Expr(
syn::Expr::Verbatim(quote! {
#view_code
#stmt
}),
None,
);
self.expanded = true;
}
}
}
/// Returns an empty impl item that can be used to remove an existing item in a mutable visitor.
fn null_item() -> syn::ImplItem {
syn::ImplItem::Verbatim(quote! {})
}

View File

@ -1,95 +0,0 @@
use proc_macro2::{Span as Span2, TokenStream as TokenStream2};
use quote::quote;
use syn::{spanned::Spanned, Error, Ident, ImplItem, ItemImpl, Visibility};
use crate::{
token_streams::{TokenStreams, TraitImplDetails},
widgets::ViewWidgets,
};
pub(crate) fn generate_tokens(vis: Option<Visibility>, mut item_impl: ItemImpl) -> TokenStream2 {
if item_impl.items.len() != 1 {
return Error::new(
item_impl.span(),
"Expected only one view macro and nothing else",
)
.into_compile_error();
}
let item = item_impl.items.pop().unwrap();
if let ImplItem::Macro(mac) = item {
if Some("view") == mac.mac.path.get_ident().map(|i| i.to_string()).as_deref() {
match syn::parse::<ViewWidgets>(mac.mac.tokens.into()) {
Ok(mut view_widgets) => {
view_widgets.mark_root_as_used();
let TokenStreams {
error,
init,
assign,
struct_fields,
return_fields,
..
} = view_widgets.generate_streams(
&TraitImplDetails {
vis: vis.clone(),
model_name: Ident::new("_", Span2::call_site()),
sender_name: Ident::new("sender", Span2::call_site()),
root_name: None,
},
true,
);
let view_output = quote! {
#init
#assign
{
#error
}
};
let root_widget_type = view_widgets.root_type();
item_impl.items.push(ImplItem::Verbatim(quote! {
type Root = #root_widget_type;
}));
let root_name = view_widgets.root_name();
item_impl.items.push(ImplItem::Verbatim(quote! {
fn init() -> Self {
#view_output
Self {
#return_fields
}
}
}));
let type_name = &item_impl.self_ty;
quote! {
#[derive(Debug, Clone)]
#vis struct #type_name {
#struct_fields
}
impl ::std::ops::Deref for #type_name {
type Target = #root_widget_type;
fn deref(&self) -> &Self::Target {
&self.#root_name
}
}
#item_impl
}
}
Err(err) => err.to_compile_error(),
}
} else {
Error::new(mac.mac.path.span(), "Expected a view macro").into_compile_error()
}
} else {
Error::new(item.span(), "Expected a macro").into_compile_error()
}
}

View File

@ -1,135 +0,0 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, quote_spanned, ToTokens};
use syn::spanned::Spanned;
use syn::Expr;
use crate::widgets::{AssignProperty, AssignPropertyAttr, PropertyName};
use super::AssignInfo;
impl AssignProperty {
pub(crate) fn conditional_assign_stream(
&self,
info: &mut AssignInfo<'_>,
p_name: &PropertyName,
init: bool,
) {
// If the code gen path is behind a conditional widgets, handle `watch` and `track` later.
// Normally, those would be initialized right away, but they might need access to
// variables from a pattern, for example `Some(variable)` so they are moved inside the
// match arm or if expression.
if !info.is_conditional
|| !matches!(
self.attr,
AssignPropertyAttr::Track { .. } | AssignPropertyAttr::Watch { .. }
)
{
self.assign_stream(info, p_name, init);
}
}
pub(crate) fn assign_stream(
&self,
info: &mut AssignInfo<'_>,
p_name: &PropertyName,
init: bool,
) {
if init && self.attr.should_skip_init() {
return;
}
let assign_fn = p_name.assign_fn_stream(info);
let self_assign_args = p_name.assign_args_stream(info.widget_name);
let span = p_name.span();
let args = self.args.as_ref().map(|args| {
quote! {
, #args
}
});
// Destructure tuples
let assign = if let Expr::Tuple(tuple) = &self.expr {
tuple.elems.to_token_stream()
} else {
self.expr.to_token_stream()
};
let chain = self.chain.as_ref().map(|chain| {
quote_spanned! {
chain.span() => .#chain
}
});
let (block_stream, unblock_stream) = if init || self.block_signals.is_empty() {
(None, None)
} else {
let mut block_stream = TokenStream2::default();
let mut unblock_stream = TokenStream2::default();
let gtk_import = crate::gtk_import();
let w_name = info.widget_name;
for signal_handler in &self.block_signals {
block_stream.extend(quote_spanned! {
signal_handler.span() =>
{
use relm4::WidgetRef;
#[allow(clippy::needless_borrow)]
#gtk_import::prelude::ObjectExt::block_signal(#w_name.widget_ref(), &#signal_handler);
}
});
unblock_stream.extend(quote_spanned! {
signal_handler.span() =>
{
use relm4::WidgetRef;
#[allow(clippy::needless_borrow)]
#gtk_import::prelude::ObjectExt::unblock_signal(#w_name.widget_ref(), &#signal_handler);
}
});
}
(Some(block_stream), Some(unblock_stream))
};
info.stream
.extend(match (self.optional_assign, self.iterative) {
(false, false) => {
quote_spanned! { span =>
#block_stream
#assign_fn(#self_assign_args #assign #args) #chain;
#unblock_stream
}
}
(true, false) => {
quote_spanned! {
span => if let Some(__p_assign) = #assign {
#block_stream
#assign_fn(#self_assign_args __p_assign #args) #chain;
#unblock_stream
}
}
}
(false, true) => {
quote_spanned! {
span =>
#block_stream
for __elem in #assign {
#assign_fn(#self_assign_args __elem #args) #chain;
}
#unblock_stream
}
}
(true, true) => {
quote_spanned! {
span =>
#block_stream
for __elem in #assign {
if let Some(__p_assign) = __elem {
#assign_fn(#self_assign_args __p_assign #args) #chain;
}
}
#unblock_stream
}
}
});
}
}

View File

@ -1,57 +0,0 @@
use quote::{quote, quote_spanned};
use syn::Ident;
use crate::widgets::{ConditionalBranches, ConditionalWidget, PropertyName};
use super::AssignInfo;
impl ConditionalWidget {
pub(super) fn assign_stream<'a>(
&'a self,
info: &mut AssignInfo<'a>,
p_name: &PropertyName,
sender_name: &'a Ident,
) {
let assign_fn = p_name.assign_fn_stream(info);
let self_assign_args = p_name.assign_args_stream(info.widget_name);
let span = p_name.span();
let args = self.args.as_ref().map(|args| {
quote! {
, #args
}
});
let w_name = &self.name;
let assign_args = if let Some(assign_wrapper) = &self.assign_wrapper {
quote! { #assign_wrapper (&#w_name ) }
} else {
quote_spanned! { w_name.span() => &#w_name }
};
info.stream.extend(quote_spanned! {
span => #assign_fn(#self_assign_args #assign_args #args);
});
let mut info = AssignInfo {
stream: info.stream,
widget_name: &self.name,
template_path: None,
is_conditional: true,
};
match &self.branches {
ConditionalBranches::If(if_branches) => {
for branch in if_branches {
let p_name = PropertyName::Ident(Ident::new("add_named", p_name.span()));
branch.widget.assign_stream(&mut info, &p_name, sender_name);
}
}
ConditionalBranches::Match((_, _, match_arms)) => {
for arm in match_arms {
let p_name = PropertyName::Ident(Ident::new("add_named", p_name.span()));
arm.widget.assign_stream(&mut info, &p_name, sender_name);
}
}
}
}
}

View File

@ -1,37 +0,0 @@
use proc_macro2::TokenStream as TokenStream2;
use syn::{punctuated::Punctuated, token, Ident};
use crate::widgets::{Property, PropertyType};
mod assign_property;
mod conditional_widget;
mod properties;
mod signal_handler;
mod widgets;
pub(crate) struct AssignInfo<'a> {
pub(crate) stream: &'a mut TokenStream2,
pub(crate) widget_name: &'a Ident,
pub(crate) template_path: Option<Punctuated<Ident, token::Dot>>,
pub(crate) is_conditional: bool,
}
impl Property {
fn assign_stream<'a>(&'a self, info: &mut AssignInfo<'a>, sender_name: &'a Ident) {
match &self.ty {
PropertyType::Assign(assign) => {
assign.conditional_assign_stream(info, &self.name, true);
}
PropertyType::Widget(widget) => {
widget.assign_stream(info, &self.name, sender_name);
}
PropertyType::ConditionalWidget(cond_widget) => {
cond_widget.assign_stream(info, &self.name, sender_name);
}
PropertyType::SignalHandler(signal_handler) => {
signal_handler.connect_signals_stream(info, &self.name, sender_name);
}
PropertyType::ParseError(_) => (),
}
}
}

View File

@ -1,13 +0,0 @@
use syn::Ident;
use crate::widgets::Properties;
use super::AssignInfo;
impl Properties {
pub(super) fn assign_stream<'a>(&'a self, info: &mut AssignInfo<'a>, sender_name: &'a Ident) {
for prop in &self.properties {
prop.assign_stream(info, sender_name);
}
}
}

View File

@ -1,77 +0,0 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote_spanned, ToTokens};
use syn::Expr;
use syn::{spanned::Spanned, Ident};
use crate::widgets::{PropertyName, SignalHandler, SignalHandlerVariant};
use super::AssignInfo;
impl SignalHandler {
pub(super) fn connect_signals_stream(
&self,
info: &mut AssignInfo<'_>,
p_name: &PropertyName,
sender_name: &Ident,
) {
let span = p_name.span();
let assign_fn = p_name.assign_fn_stream(info);
let self_assign_args = p_name.assign_args_stream(info.widget_name);
let (clone_stream, assignment) = match &self.inner {
SignalHandlerVariant::Expr(expr) => (
quote_spanned! { span =>
#[allow(clippy::redundant_clone)]
let sender = #sender_name.clone();
},
quote_spanned! {
span => move |_| {
sender.input(#expr)
}
},
),
SignalHandlerVariant::Closure(inner) => {
let mut clone_stream = TokenStream2::new();
if let Some(args) = &inner.args {
for arg in &args.inner {
if let Expr::Path(path) = arg {
if let Some(ident) = path.path.get_ident() {
// Just an ident was used. Simply clone it.
clone_stream.extend(quote_spanned! { arg.span() =>
#[allow(clippy::redundant_clone)]
#[allow(clippy::clone_on_copy)]
let #ident = #ident.clone();
});
continue;
}
}
// Allow more complex expressions such as `value = data.sender()`
clone_stream.extend(quote_spanned! { arg.span() =>
#[allow(clippy::redundant_clone)]
#[allow(clippy::clone_on_copy)]
let #arg;
});
}
}
(clone_stream, inner.closure.to_token_stream())
}
};
info.stream
.extend(if let Some(signal_handler_id) = &self.handler_id {
quote_spanned! {
span => let #signal_handler_id = {
#clone_stream
#assign_fn(#self_assign_args #assignment)
};
}
} else {
quote_spanned! {
span => {
#clone_stream
#assign_fn(#self_assign_args #assignment);
}
}
});
}
}

View File

@ -1,100 +0,0 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, quote_spanned};
use syn::spanned::Spanned;
use syn::Ident;
use crate::widgets::{PropertyName, ReturnedWidget, Widget, WidgetTemplateAttr};
use super::AssignInfo;
impl ReturnedWidget {
fn return_assign_tokens(&self) -> TokenStream2 {
let name = &self.name;
if let Some(ty) = &self.ty {
quote! {
let #name : #ty
}
} else {
quote! {
let #name
}
}
}
}
impl Widget {
pub(crate) fn start_assign_stream<'a>(
&'a self,
stream: &'a mut TokenStream2,
sender_name: &'a Ident,
) {
let w_name = &self.name;
let mut info = AssignInfo {
stream,
widget_name: w_name,
template_path: None,
is_conditional: false,
};
self.properties.assign_stream(&mut info, sender_name);
}
pub(super) fn assign_stream<'a>(
&'a self,
info: &mut AssignInfo<'a>,
p_name: &PropertyName,
sender_name: &'a Ident,
) {
// Recursively generate code for properties
{
let template_path = (self.template_attr == WidgetTemplateAttr::TemplateChild)
.then_some(self.func.widget_template_path(info.widget_name, &self.name));
let mut info = AssignInfo {
stream: info.stream,
widget_name: &self.name,
template_path,
is_conditional: info.is_conditional,
};
self.properties.assign_stream(&mut info, sender_name);
}
// Template children are already assigned by the template.
if self.template_attr != WidgetTemplateAttr::TemplateChild {
let assign_fn = p_name.assign_fn_stream(info);
let self_assign_args = p_name.assign_args_stream(info.widget_name);
let assign = self.widget_assignment();
let span = p_name.span();
let args = self.args.as_ref().map(|args| {
quote_spanned! {
args.span() => ,#args
}
});
info.stream.extend(if let Some(ret_widget) = &self.returned_widget {
let return_assign_stream = ret_widget.return_assign_tokens();
let unwrap = ret_widget.is_optional.then(|| quote! { .unwrap() });
quote_spanned! {
span => #return_assign_stream = #assign_fn(#self_assign_args #assign #args) #unwrap;
}
} else {
quote_spanned! {
span => #assign_fn(#self_assign_args #assign #args);
}
});
}
if let Some(returned_widget) = &self.returned_widget {
let mut info = AssignInfo {
stream: info.stream,
widget_name: &returned_widget.name,
template_path: None,
is_conditional: info.is_conditional,
};
returned_widget
.properties
.assign_stream(&mut info, sender_name);
}
}
}

View File

@ -1,207 +0,0 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, quote_spanned};
use syn::spanned::Spanned;
use syn::Ident;
use crate::widgets::{
AssignProperty, AssignPropertyAttr, ConditionalBranches, ConditionalWidget, MatchArm,
Properties, Property, PropertyName, PropertyType, ReturnedWidget, Widget,
};
use super::assign::AssignInfo;
impl Property {
fn conditional_init_stream(
&self,
stream: &mut TokenStream2,
w_name: &Ident,
model_name: &Ident,
is_conditional: bool,
) {
match &self.ty {
PropertyType::Assign(assign) => assign.conditional_init_stream(
stream,
&self.name,
w_name,
model_name,
is_conditional,
),
PropertyType::Widget(widget) => {
widget.conditional_init_stream(stream, model_name, is_conditional);
}
PropertyType::ConditionalWidget(cond_widget) => {
cond_widget.conditional_init_stream(stream, model_name);
}
PropertyType::SignalHandler(_) | PropertyType::ParseError(_) => (),
}
}
}
impl Properties {
fn conditional_init_stream(
&self,
stream: &mut TokenStream2,
w_name: &Ident,
model_name: &Ident,
is_conditional: bool,
) {
for prop in &self.properties {
prop.conditional_init_stream(stream, w_name, model_name, is_conditional);
}
}
}
impl Widget {
pub(crate) fn init_conditional_init_stream(
&self,
stream: &mut TokenStream2,
model_name: &Ident,
) {
self.conditional_init_stream(stream, model_name, false);
}
fn conditional_init_stream(
&self,
stream: &mut TokenStream2,
model_name: &Ident,
is_conditional: bool,
) {
let w_name = &self.name;
self.properties
.conditional_init_stream(stream, w_name, model_name, is_conditional);
if let Some(returned_widget) = &self.returned_widget {
returned_widget.conditional_init_stream(stream, model_name, is_conditional);
}
}
}
impl ConditionalWidget {
fn conditional_init_stream(&self, stream: &mut TokenStream2, model_name: &Ident) {
let brach_stream = match &self.branches {
ConditionalBranches::If(if_branches) => {
let mut stream = TokenStream2::new();
for (index, branch) in if_branches.iter().enumerate() {
let mut inner_update_stream = TokenStream2::new();
branch.widget.conditional_init_stream(
&mut inner_update_stream,
model_name,
true,
);
branch.update_stream(&mut stream, &inner_update_stream, index);
}
stream
}
ConditionalBranches::Match((match_token, expr, match_arms)) => {
let mut inner_tokens = TokenStream2::new();
for (index, match_arm) in match_arms.iter().enumerate() {
let mut inner_update_stream = TokenStream2::new();
match_arm.widget.conditional_init_stream(
&mut inner_update_stream,
model_name,
true,
);
let MatchArm {
pattern,
guard,
arrow,
..
} = match_arm;
let (guard_if, guard_expr) = if let Some((guard_if, guard_expr)) = guard {
(Some(guard_if), Some(guard_expr))
} else {
(None, None)
};
let index = index.to_string();
inner_tokens.extend(quote! {
#pattern #guard_if #guard_expr #arrow {
#inner_update_stream
#index
},
});
}
quote! {
#match_token #expr {
#inner_tokens
}
}
}
};
let w_name = &self.name;
stream.extend(quote! {
let __current_page = "";
#w_name.set_visible_child_name(#brach_stream);
});
}
}
impl ReturnedWidget {
fn conditional_init_stream(
&self,
stream: &mut TokenStream2,
model_name: &Ident,
is_conditional: bool,
) {
let w_name = &self.name;
self.properties
.conditional_init_stream(stream, w_name, model_name, is_conditional);
}
}
impl AssignProperty {
fn conditional_init_stream(
&self,
stream: &mut TokenStream2,
p_name: &PropertyName,
widget_name: &Ident,
model_name: &Ident,
is_conditional: bool,
) {
// Unconditional code is handled in the "normal" init stream
if is_conditional {
match &self.attr {
AssignPropertyAttr::None => (),
AssignPropertyAttr::Watch { skip_init } => {
if skip_init.is_none() {
let mut info = AssignInfo {
stream,
widget_name,
template_path: None,
is_conditional,
};
self.assign_stream(&mut info, p_name, true);
}
}
AssignPropertyAttr::Track {
track_expr,
paste_model,
skip_init,
} => {
if skip_init.is_none() {
let mut assign_stream = TokenStream2::new();
let mut info = AssignInfo {
stream: &mut assign_stream,
widget_name,
template_path: None,
is_conditional,
};
self.assign_stream(&mut info, p_name, true);
let model = paste_model.then(|| model_name);
stream.extend(quote_spanned! {
track_expr.span() => if #model #track_expr {
#assign_stream
}
});
}
}
}
}
}
}

View File

@ -1,86 +0,0 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use crate::widgets::{
ConditionalBranches, ConditionalWidget, Properties, Property, PropertyType, ReturnedWidget,
SignalHandler, Widget,
};
impl Property {
fn destructure_stream(&self, stream: &mut TokenStream2) {
match &self.ty {
PropertyType::Widget(widget) => widget.destructure_stream(stream),
PropertyType::SignalHandler(signal_handler) => {
signal_handler.destructure_stream(stream);
}
PropertyType::ConditionalWidget(cond_widget) => {
cond_widget.destructure_stream(stream);
}
PropertyType::Assign(_) | PropertyType::ParseError(_) => (),
}
}
}
impl Properties {
fn destructure_stream(&self, stream: &mut TokenStream2) {
for prop in &self.properties {
prop.destructure_stream(stream);
}
}
}
impl Widget {
pub(crate) fn destructure_stream(&self, stream: &mut TokenStream2) {
if self.has_struct_field() {
let name = &self.name;
stream.extend(quote! { #name, });
}
self.properties.destructure_stream(stream);
}
}
impl ConditionalWidget {
fn destructure_stream(&self, stream: &mut TokenStream2) {
let name = &self.name;
stream.extend(quote! { #name, });
match &self.branches {
ConditionalBranches::If(if_branches) => {
for branch in if_branches {
branch.widget.destructure_stream(stream);
}
}
ConditionalBranches::Match((_, _, match_arms)) => {
for arm in match_arms {
arm.widget.destructure_stream(stream);
}
}
}
}
}
impl ReturnedWidget {
pub(super) fn destructure_stream(&self, stream: &mut TokenStream2) {
if self.ty.is_some() {
let name = &self.name;
stream.extend(quote! {
#name,
});
}
self.properties.destructure_stream(stream);
}
}
impl SignalHandler {
pub(super) fn destructure_stream(&self, stream: &mut TokenStream2) {
if let Some(signal_handler_id) = &self.handler_id {
stream.extend(quote! {
#signal_handler_id,
});
}
}
}

View File

@ -1,77 +0,0 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens};
use syn::Ident;
use crate::widgets::{
ConditionalBranches, ConditionalWidget, ParseError, Properties, Property, PropertyType,
ReturnedWidget, Widget,
};
impl Property {
fn error_stream(&self, stream: &mut TokenStream2, w_name: &Ident) {
match &self.ty {
PropertyType::ParseError(error) => error.error_stream(stream, w_name),
PropertyType::SignalHandler(_) | PropertyType::Assign(_) => (),
PropertyType::Widget(widget) => widget.error_stream(stream),
PropertyType::ConditionalWidget(cond_widget) => cond_widget.error_stream(stream),
}
}
}
impl Properties {
fn error_stream(&self, stream: &mut TokenStream2, w_name: &Ident) {
for prop in &self.properties {
prop.error_stream(stream, w_name);
}
}
}
impl ParseError {
fn error_stream(&self, stream: &mut TokenStream2, w_name: &Ident) {
match self {
ParseError::Ident((ident, tokens)) => stream.extend(quote! {
#tokens
#w_name.#ident ;
}),
ParseError::Path((path, tokens)) => stream.extend(quote! {
#tokens
#path ;
}),
ParseError::Generic(generic_error) => generic_error.to_tokens(stream),
}
}
}
impl Widget {
pub(crate) fn error_stream(&self, stream: &mut TokenStream2) {
let w_name = &self.name;
self.properties.error_stream(stream, w_name);
if let Some(returned_widget) = &self.returned_widget {
returned_widget.error_stream(stream);
}
}
}
impl ConditionalWidget {
fn error_stream(&self, stream: &mut TokenStream2) {
match &self.branches {
ConditionalBranches::If(if_branches) => {
for branch in if_branches {
branch.widget.error_stream(stream);
}
}
ConditionalBranches::Match((_, _, match_arms)) => {
for arm in match_arms {
arm.widget.error_stream(stream);
}
}
}
}
}
impl ReturnedWidget {
fn error_stream(&self, stream: &mut TokenStream2) {
let w_name = &self.name;
self.properties.error_stream(stream, w_name);
}
}

View File

@ -1,118 +0,0 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, quote_spanned};
use crate::widgets::{
ConditionalBranches, ConditionalWidget, Properties, Property, PropertyType, Widget, WidgetAttr,
WidgetTemplateAttr,
};
impl Property {
fn init_stream(&self, stream: &mut TokenStream2) {
match &self.ty {
PropertyType::Widget(widget) => {
widget.init_stream(stream);
}
PropertyType::ConditionalWidget(cond_widget) => {
cond_widget.init_stream(stream);
}
_ => (),
}
}
}
impl Properties {
fn init_stream(&self, stream: &mut TokenStream2) {
for prop in &self.properties {
prop.init_stream(stream);
}
}
}
impl Widget {
pub(crate) fn init_stream(&self, stream: &mut TokenStream2) {
self.self_init_stream(stream);
self.other_init_stream(stream);
}
pub(crate) fn init_root_init_streams(
&self,
init_root_stream: &mut TokenStream2,
init_stream: &mut TokenStream2,
) {
// Init function as return value
init_root_stream.extend(match self.template_attr {
WidgetTemplateAttr::None | WidgetTemplateAttr::TemplateChild => {
self.func.func_token_stream()
}
WidgetTemplateAttr::Template => {
let widget_ty = &self.func.path;
quote! {
<#widget_ty as relm4::WidgetTemplate>::init()
}
}
});
self.other_init_stream(init_stream);
}
fn self_init_stream(&self, stream: &mut TokenStream2) {
let mutability = &self.mutable;
let name = &self.name;
let ty = self.func.ty.as_ref().map(|ty| quote! {: #ty});
if self.attr == WidgetAttr::None {
match self.template_attr {
WidgetTemplateAttr::None => {
let func = self.func.func_token_stream();
stream.extend(quote! {
let #mutability #name #ty = #func;
});
}
WidgetTemplateAttr::Template => {
let widget_ty = &self.func.path;
stream.extend(quote! {
let #mutability #name #ty = <#widget_ty as relm4::WidgetTemplate>::init();
});
}
// Template children are already initialized by their template.
WidgetTemplateAttr::TemplateChild => (),
}
}
}
fn other_init_stream(&self, stream: &mut TokenStream2) {
self.properties.init_stream(stream);
}
}
impl ConditionalWidget {
fn init_stream(&self, stream: &mut TokenStream2) {
let name = &self.name;
let gtk_import = crate::gtk_import();
stream.extend(quote_spanned! {
name.span() =>
let #name = #gtk_import::Stack::default();
});
if let Some(transition) = &self.transition {
stream.extend(quote_spanned! {
transition.span() =>
#name.set_transition_type(#gtk_import::StackTransitionType:: #transition);
});
}
match &self.branches {
ConditionalBranches::If(if_branches) => {
for branch in if_branches {
branch.widget.init_stream(stream);
}
}
ConditionalBranches::Match((_, _, match_arms)) => {
for arm in match_arms {
arm.widget.init_stream(stream);
}
}
}
}
}

View File

@ -1,42 +0,0 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, quote_spanned};
use syn::{spanned::Spanned, token};
use super::{PropertyName, ReturnedWidget, Widget, WidgetTemplateAttr};
/// Utility methods and functions.
mod util;
/// Generate struct fields.
mod struct_fields;
/// Fields of the returned widget struct.
mod return_fields;
mod assign;
mod conditional_init;
mod destructure_fields;
mod error;
mod init;
mod update_view;
impl Widget {
pub(super) fn widget_assignment(&self) -> TokenStream2 {
let w_name = &self.name;
let ref_token = &self.ref_token;
let deref_token = &self.deref_token;
let template_deref =
(self.template_attr == WidgetTemplateAttr::Template).then(token::Star::default);
let out_stream = quote! { #ref_token #deref_token #template_deref #w_name };
if let Some(wrapper) = &self.assign_wrapper {
quote_spanned! {
wrapper.span() => #wrapper(#out_stream)
}
} else {
out_stream
}
}
}

View File

@ -1,80 +0,0 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use crate::widgets::{
ConditionalBranches, ConditionalWidget, Properties, Property, PropertyType, ReturnedWidget,
SignalHandler, Widget, WidgetAttr,
};
impl Property {
fn return_stream(&self, stream: &mut TokenStream2) {
match &self.ty {
PropertyType::Widget(widget) => widget.return_stream(stream),
PropertyType::SignalHandler(signal_handler) => signal_handler.return_stream(stream),
PropertyType::ConditionalWidget(cond_widget) => cond_widget.return_stream(stream),
PropertyType::Assign(_) | PropertyType::ParseError(_) => (),
}
}
}
impl Properties {
fn return_stream(&self, stream: &mut TokenStream2) {
for prop in &self.properties {
prop.return_stream(stream);
}
}
}
impl Widget {
pub(crate) fn return_stream(&self, stream: &mut TokenStream2) {
if self.has_struct_field() {
let name = &self.name;
stream.extend(if self.attr == WidgetAttr::LocalRef {
// The local reference must be cloned first
quote! { #name: #name.clone(), }
} else {
quote! { #name, }
});
}
self.properties.return_stream(stream);
if let Some(returned_widget) = &self.returned_widget {
returned_widget.return_stream(stream);
}
}
}
impl ConditionalWidget {
fn return_stream(&self, stream: &mut TokenStream2) {
let name = &self.name;
stream.extend(quote! { #name, });
match &self.branches {
ConditionalBranches::If(if_branches) => {
for branch in if_branches {
branch.widget.return_stream(stream);
}
}
ConditionalBranches::Match((_, _, match_arms)) => {
for arm in match_arms {
arm.widget.return_stream(stream);
}
}
}
}
}
impl ReturnedWidget {
fn return_stream(&self, stream: &mut TokenStream2) {
self.destructure_stream(stream);
self.properties.return_stream(stream);
}
}
impl SignalHandler {
fn return_stream(&self, stream: &mut TokenStream2) {
self.destructure_stream(stream);
}
}

View File

@ -1,117 +0,0 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, quote_spanned};
use syn::Visibility;
use super::{ReturnedWidget, Widget};
use crate::widgets::{
ConditionalBranches, ConditionalWidget, Properties, Property, PropertyType, SignalHandler,
};
impl Property {
fn struct_fields_stream(&self, stream: &mut TokenStream2, vis: &Option<Visibility>) {
match &self.ty {
PropertyType::Widget(widget) => widget.struct_fields_stream(stream, vis),
PropertyType::SignalHandler(signal_handler) => {
signal_handler.struct_fields_stream(stream, vis);
}
PropertyType::ConditionalWidget(cond_widget) => {
cond_widget.struct_fields_stream(stream, vis);
}
PropertyType::Assign(_) | PropertyType::ParseError(_) => (),
}
}
}
impl Properties {
fn struct_fields_stream(&self, stream: &mut TokenStream2, vis: &Option<Visibility>) {
for prop in &self.properties {
prop.struct_fields_stream(stream, vis);
}
}
}
impl Widget {
pub(crate) fn struct_fields_stream(&self, stream: &mut TokenStream2, vis: &Option<Visibility>) {
if self.has_struct_field() {
let name = &self.name;
let ty = self.func_type_token_stream();
stream.extend(if let Some(docs) = &self.doc_attr {
quote! {
#[doc = #docs]
#vis #name: #ty,
}
} else {
quote! {
#[allow(missing_docs)]
#vis #name: #ty,
}
});
}
self.properties.struct_fields_stream(stream, vis);
if let Some(returned_widget) = &self.returned_widget {
returned_widget.struct_fields_stream(stream, vis);
}
}
}
impl ConditionalWidget {
fn struct_fields_stream(&self, stream: &mut TokenStream2, vis: &Option<Visibility>) {
let name = &self.name;
let gtk_import = crate::gtk_import();
stream.extend(if let Some(docs) = &self.doc_attr {
quote_spanned! {
name.span() =>
#[doc = #docs]
#vis #name: #gtk_import::Stack,
}
} else {
quote_spanned! {
name.span() =>
#[allow(missing_docs)]
#vis #name: #gtk_import::Stack,
}
});
match &self.branches {
ConditionalBranches::If(if_branches) => {
for branch in if_branches {
branch.widget.struct_fields_stream(stream, vis);
}
}
ConditionalBranches::Match((_, _, match_arms)) => {
for arm in match_arms {
arm.widget.struct_fields_stream(stream, vis);
}
}
}
}
}
impl ReturnedWidget {
fn struct_fields_stream(&self, stream: &mut TokenStream2, vis: &Option<Visibility>) {
if let Some(ty) = &self.ty {
let name = &self.name;
stream.extend(quote! {
#[allow(missing_docs)]
#vis #name: #ty,
});
}
self.properties.struct_fields_stream(stream, vis);
}
}
impl SignalHandler {
fn struct_fields_stream(&self, stream: &mut TokenStream2, vis: &Option<Visibility>) {
if let Some(signal_handler_id) = &self.handler_id {
let gtk_import = crate::gtk_import();
stream.extend(quote_spanned! {
signal_handler_id.span() =>
#[allow(missing_docs)]
#vis #signal_handler_id: #gtk_import::glib::signal::SignalHandlerId,
});
}
}
}

View File

@ -1,231 +0,0 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, quote_spanned};
use syn::{punctuated::Punctuated, token, Ident};
use crate::widgets::{
AssignProperty, AssignPropertyAttr, ConditionalBranches, ConditionalWidget, MatchArm,
Properties, Property, PropertyName, PropertyType, ReturnedWidget, Widget, WidgetTemplateAttr,
};
use super::assign::AssignInfo;
impl Property {
fn update_view_stream(
&self,
stream: &mut TokenStream2,
widget_name: &Ident,
template_path: Option<Punctuated<Ident, token::Dot>>,
model_name: &Ident,
conditional_branch: bool,
) {
match &self.ty {
PropertyType::Assign(assign) => assign.update_view_stream(
stream,
&self.name,
widget_name,
template_path,
model_name,
conditional_branch,
),
PropertyType::Widget(widget) => {
widget.update_view_stream(
stream,
Some(widget_name),
model_name,
conditional_branch,
);
}
PropertyType::ConditionalWidget(cond_widget) => {
cond_widget.update_view_stream(stream, model_name);
}
PropertyType::SignalHandler(_) | PropertyType::ParseError(_) => (),
}
}
}
impl Properties {
fn update_view_stream(
&self,
stream: &mut TokenStream2,
widget_name: &Ident,
template_path: Option<Punctuated<Ident, token::Dot>>,
model_name: &Ident,
conditional_branch: bool,
) {
for prop in &self.properties {
prop.update_view_stream(
stream,
widget_name,
template_path.clone(),
model_name,
conditional_branch,
);
}
}
}
impl Widget {
pub(crate) fn init_update_view_stream(&self, stream: &mut TokenStream2, model_name: &Ident) {
self.update_view_stream(stream, None, model_name, false);
}
fn update_view_stream(
&self,
stream: &mut TokenStream2,
parent_widget_name: Option<&Ident>,
model_name: &Ident,
conditional_branch: bool,
) {
let widget_name = &self.name;
let template_path = if self.template_attr == WidgetTemplateAttr::TemplateChild {
parent_widget_name.map(|parent_widget_name| {
self.func
.widget_template_path(parent_widget_name, &self.name)
})
} else {
None
};
self.properties.update_view_stream(
stream,
widget_name,
template_path,
model_name,
conditional_branch,
);
if let Some(returned_widget) = &self.returned_widget {
returned_widget.update_view_stream(stream, model_name, conditional_branch);
}
}
}
impl ConditionalWidget {
fn update_view_stream(&self, stream: &mut TokenStream2, model_name: &Ident) {
let brach_stream = match &self.branches {
ConditionalBranches::If(if_branches) => {
let mut stream = TokenStream2::new();
for (index, branch) in if_branches.iter().enumerate() {
let mut inner_update_stream = TokenStream2::new();
branch.widget.update_view_stream(
&mut inner_update_stream,
None,
model_name,
true,
);
branch.update_stream(&mut stream, &inner_update_stream, index);
}
stream
}
ConditionalBranches::Match((match_token, expr, match_arms)) => {
let mut inner_tokens = TokenStream2::new();
for (index, match_arm) in match_arms.iter().enumerate() {
let mut inner_update_stream = TokenStream2::new();
match_arm.widget.update_view_stream(
&mut inner_update_stream,
None,
model_name,
true,
);
let MatchArm {
pattern,
guard,
arrow,
..
} = match_arm;
let (guard_if, guard_expr) = if let Some((guard_if, guard_expr)) = guard {
(Some(guard_if), Some(guard_expr))
} else {
(None, None)
};
let index = index.to_string();
inner_tokens.extend(quote! {
#pattern #guard_if #guard_expr #arrow {
let __page_active: bool = (__current_page == #index);
#inner_update_stream
#index
},
});
}
quote! {
#match_token #expr {
#inner_tokens
}
}
}
};
let w_name = &self.name;
stream.extend(quote_spanned! {
w_name.span() =>
let __current_page = #w_name.visible_child_name().map_or("".to_string(), |s| s.as_str().to_string());
#w_name.set_visible_child_name(#brach_stream);
});
}
}
impl ReturnedWidget {
fn update_view_stream(
&self,
stream: &mut TokenStream2,
model_name: &Ident,
conditional_branch: bool,
) {
let w_name = &self.name;
self.properties
.update_view_stream(stream, w_name, None, model_name, conditional_branch);
}
}
impl AssignProperty {
fn update_view_stream(
&self,
stream: &mut TokenStream2,
p_name: &PropertyName,
widget_name: &Ident,
template_path: Option<Punctuated<Ident, token::Dot>>,
model_name: &Ident,
conditional_branch: bool,
) {
match &self.attr {
AssignPropertyAttr::None => (),
AssignPropertyAttr::Watch { .. } => {
let mut info = AssignInfo {
stream,
widget_name,
template_path,
is_conditional: false,
};
self.assign_stream(&mut info, p_name, false);
}
AssignPropertyAttr::Track {
track_expr,
paste_model,
..
} => {
let mut assign_stream = TokenStream2::new();
let mut info = AssignInfo {
stream: &mut assign_stream,
widget_name,
template_path: None,
is_conditional: false,
};
self.assign_stream(&mut info, p_name, false);
let model = paste_model.then(|| model_name);
let page_switch = conditional_branch.then(|| {
quote_spanned! {
p_name.span() =>
!__page_active ||
}
});
stream.extend(quote! {
if #page_switch (#model #track_expr) {
#assign_stream
}
});
}
}
}
}

View File

@ -1,28 +0,0 @@
use crate::widgets::{AssignPropertyAttr, Properties, PropertyType, Widget, WidgetTemplateAttr};
impl Widget {
/// Don't generate any fields if the widget wasn't named by the user and
/// isn't used for any property updates either.
pub(crate) fn has_struct_field(&self) -> bool {
match self.template_attr {
WidgetTemplateAttr::None => {
self.name_assigned_by_user || self.properties.are_properties_updated()
}
WidgetTemplateAttr::Template => true,
WidgetTemplateAttr::TemplateChild => false,
}
}
}
impl Properties {
pub(crate) fn are_properties_updated(&self) -> bool {
// Is there any property with watch or track attribute?
self.properties.iter().any(|prop| match &prop.ty {
PropertyType::Assign(assign_prop) => matches!(
&assign_prop.attr,
AssignPropertyAttr::Track { .. } | AssignPropertyAttr::Watch { .. }
),
_ => false,
})
}
}

View File

@ -1,33 +0,0 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use crate::widgets::{IfBranch, IfCondition};
impl IfBranch {
pub(crate) fn update_stream(
&self,
stream: &mut TokenStream2,
inner_update_tokens: &TokenStream2,
index: usize,
) {
let index = index.to_string();
stream.extend(match &self.cond {
IfCondition::If(if_token, expr) => quote! {
#if_token #expr
},
IfCondition::ElseIf(else_token, if_token, expr) => quote! {
#else_token #if_token #expr
},
IfCondition::Else(else_token) => quote! {
#else_token
},
});
stream.extend(quote! {
{
let __page_active: bool = (__current_page == #index);
#inner_update_tokens
#index
}
});
}
}

View File

@ -1,5 +0,0 @@
mod has_struct_field;
mod if_branch;
mod property_name;
mod widget;
mod widget_func;

View File

@ -1,37 +0,0 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, quote_spanned, ToTokens};
use syn::Ident;
use crate::widgets::gen::{assign::AssignInfo, PropertyName};
impl PropertyName {
pub(crate) fn assign_fn_stream(&self, info: &mut AssignInfo<'_>) -> TokenStream2 {
let AssignInfo {
widget_name,
template_path,
..
} = info;
let widget_name = if let Some(template_path) = template_path {
quote! { #template_path }
} else {
quote! { #widget_name }
};
match self {
PropertyName::Ident(ident) => {
quote! { #widget_name.#ident }
}
PropertyName::Path(path) => path.to_token_stream(),
PropertyName::RelmContainerExtAssign(span) => {
quote_spanned! { *span => #widget_name.container_add }
}
}
}
pub(crate) fn assign_args_stream(&self, w_name: &Ident) -> Option<TokenStream2> {
match self {
PropertyName::RelmContainerExtAssign(_) | PropertyName::Ident(_) => None,
PropertyName::Path(_) => Some(quote_spanned! { w_name.span() => & #w_name, }),
}
}
}

View File

@ -1,14 +0,0 @@
use crate::widgets::ViewWidgets;
impl ViewWidgets {
/// Get a mutable reference to the root widget
pub(crate) fn mark_root_as_used(&mut self) {
if let Some(root_widget) = self
.top_level_widgets
.iter_mut()
.find(|w| w.root_attr.is_some())
{
root_widget.inner.name_assigned_by_user = true;
}
}
}

View File

@ -1,112 +0,0 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, quote_spanned, ToTokens};
use syn::punctuated::Punctuated;
use syn::{spanned::Spanned, Ident};
use syn::{token, Error};
use crate::widgets::{Widget, WidgetFunc};
impl Widget {
/// Get tokens for the widget's type.
pub(crate) fn func_type_token_stream(&self) -> TokenStream2 {
let is_local = self.attr.is_local_attr();
let func = &self.func;
let path = &self.func.path;
let mut tokens = TokenStream2::new();
// If type was specified, use it
let (type_segments, num_of_segments) = if let Some(ty) = &func.ty {
return ty.to_token_stream();
} else if is_local {
return Error::new(func.span().unwrap().into(),
format!("You need to specify the type of the local variable. Use this instead: {} -> Type {{ ...",
self.name)).into_compile_error();
} else if func.args.is_some() {
// If for example gtk::Box::new() was used, ignore ::new()
// and use gtk::Box as type.
let len = path.segments.len();
if len == 0 {
unreachable!("Path can't be empty");
} else if len == 1 {
return Error::new(func.span().unwrap().into(),
format!("You need to specify a type of your function. Use this instead: {}() -> Type {{ ...",
path.to_token_stream())).into_compile_error();
} else {
(&path.segments, len - 1)
}
} else {
(&path.segments, path.segments.len())
};
let mut seg_iter = type_segments.iter().take(num_of_segments);
let first = if let Some(first) = seg_iter.next() {
first
} else {
return Error::new(
func.span().unwrap().into(),
"No path segments in WidgetFunc.",
)
.into_compile_error();
};
tokens.extend(first.to_token_stream());
for segment in seg_iter {
tokens.extend(quote! {::});
tokens.extend(segment.to_token_stream());
}
tokens
}
}
impl WidgetFunc {
/// Get the tokens of the widget's function.
pub(crate) fn func_token_stream(&self) -> TokenStream2 {
let WidgetFunc {
path,
args,
method_chain,
..
} = &self;
let mut stream = if let Some(args) = args {
quote! { #path(#args) }
} else if method_chain.is_some() {
path.to_token_stream()
} else {
quote_spanned! {
path.span() => #path::default()
}
};
if let Some(method_chain) = method_chain {
stream.extend(quote! {
.#method_chain
});
}
stream
}
pub(crate) fn widget_template_path(
&self,
template_widget_name: &Ident,
widget_name: &Ident,
) -> Punctuated<Ident, token::Dot> {
let mut template_path = Punctuated::new();
template_path.push(template_widget_name.clone());
template_path.push(widget_name.clone());
if let Some(chain) = &self.method_chain {
for method in chain {
if method.turbofish.is_some() || method.args.is_some() {
break;
} else {
template_path.push(method.ident.clone());
}
}
template_path
} else {
template_path
}
}
}

View File

@ -1,218 +0,0 @@
use proc_macro2::{Span as Span2, TokenStream as TokenStream2};
use syn::punctuated::Punctuated;
use syn::token::{Else, FatArrow, If, Match, Mut};
use syn::{token, AngleBracketedGenericArguments, Expr, ExprClosure, Ident, Pat, Path, Type};
use crate::args::Args;
mod gen;
mod parse;
mod parse_util;
mod span;
#[derive(Debug)]
pub(super) struct ViewWidgets {
pub(super) span: Span2,
pub(super) top_level_widgets: Vec<TopLevelWidget>,
}
#[derive(Debug)]
pub(super) struct TopLevelWidget {
pub(super) root_attr: Option<Ident>,
pub(super) inner: Widget,
}
#[derive(Debug)]
enum PropertyType {
Assign(AssignProperty),
SignalHandler(SignalHandler),
Widget(Widget),
ConditionalWidget(ConditionalWidget),
ParseError(ParseError),
}
#[derive(Debug)]
enum ParseError {
Ident((Ident, TokenStream2)),
Path((Path, TokenStream2)),
Generic(TokenStream2),
}
#[derive(Debug)]
enum AssignPropertyAttr {
None,
Watch {
skip_init: Option<Ident>,
},
Track {
track_expr: TokenStream2,
skip_init: Option<Ident>,
paste_model: bool,
},
}
#[derive(Debug)]
struct AssignProperty {
attr: AssignPropertyAttr,
/// Optional arguments like param_name[arg1, arg2, ...]
args: Option<Args<Expr>>,
expr: Expr,
/// Assign with an ?
optional_assign: bool,
/// Iterate through elements to generate tokens
iterative: bool,
block_signals: Vec<Ident>,
chain: Option<Box<Expr>>,
}
#[derive(Debug)]
struct SignalHandler {
inner: SignalHandlerVariant,
handler_id: Option<Ident>,
}
#[derive(Debug)]
enum SignalHandlerVariant {
Expr(Expr),
Closure(ClosureSignalHandler),
}
#[derive(Debug)]
struct ClosureSignalHandler {
closure: ExprClosure,
args: Option<Args<Expr>>,
}
#[derive(Debug)]
enum PropertyName {
Ident(Ident),
Path(Path),
RelmContainerExtAssign(Span2),
}
#[derive(Debug)]
struct Property {
/// Either a path or just an ident
name: PropertyName,
ty: PropertyType,
}
#[derive(Debug, Default)]
struct Properties {
properties: Vec<Property>,
}
/// The function that initializes the widget.
///
/// This might be a real function or just something like `gtk::Label`.
#[derive(Debug)]
struct WidgetFunc {
path: Path,
args: Option<Punctuated<Expr, token::Comma>>,
method_chain: Option<Punctuated<WidgetFuncMethod, token::Dot>>,
ty: Option<Box<Type>>,
}
#[derive(Debug)]
struct WidgetFuncMethod {
ident: Ident,
turbofish: Option<AngleBracketedGenericArguments>,
args: Option<Punctuated<Expr, token::Comma>>,
}
#[derive(Debug)]
pub(super) struct Widget {
doc_attr: Option<TokenStream2>,
attr: WidgetAttr,
template_attr: WidgetTemplateAttr,
pub(super) mutable: Option<Mut>,
pub(super) name: Ident,
name_assigned_by_user: bool,
func: WidgetFunc,
args: Option<Args<Expr>>,
properties: Properties,
assign_wrapper: Option<Path>,
ref_token: Option<token::And>,
deref_token: Option<token::Star>,
returned_widget: Option<ReturnedWidget>,
}
#[derive(Debug, PartialEq)]
enum WidgetAttr {
None,
Local,
LocalRef,
}
#[derive(Debug, PartialEq)]
enum WidgetTemplateAttr {
None,
Template,
TemplateChild,
}
#[derive(Debug)]
struct ReturnedWidget {
name: Ident,
ty: Option<Path>,
properties: Properties,
is_optional: bool,
}
#[derive(Debug)]
struct ConditionalWidget {
doc_attr: Option<TokenStream2>,
transition: Option<Ident>,
assign_wrapper: Option<Path>,
name: Ident,
args: Option<Args<Expr>>,
branches: ConditionalBranches,
}
#[derive(Debug)]
enum ConditionalBranches {
If(Vec<IfBranch>),
Match((Match, Box<Expr>, Vec<MatchArm>)),
}
#[derive(Debug)]
enum IfCondition {
If(If, Expr),
ElseIf(Else, If, Expr),
Else(Else),
}
#[derive(Debug)]
struct IfBranch {
cond: IfCondition,
widget: Widget,
}
#[derive(Debug)]
struct MatchArm {
pattern: Pat,
guard: Option<(If, Box<Expr>)>,
arrow: FatArrow,
widget: Widget,
}
enum Attr {
Doc(TokenStream2),
Local(Ident),
LocalRef(Ident),
Root(Ident),
Iterate(Ident),
Watch(Ident, Option<Ident>),
Track(Ident, Option<Ident>, Option<Box<Expr>>),
BlockSignal(Ident, Vec<Ident>),
Name(Ident, Ident),
Transition(Ident, Ident),
Wrap(Ident, Path),
Chain(Ident, Box<Expr>),
Template(Ident),
TemplateChild(Ident),
}
struct Attrs {
inner: Vec<Attr>,
}

View File

@ -1,190 +0,0 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote_spanned, ToTokens};
use syn::parse::ParseStream;
use syn::spanned::Spanned;
use syn::{Error, Expr, ExprCall, ExprField, Ident, Member, Result, Token};
use crate::args::Args;
use crate::widgets::parse_util::attr_twice_error;
use crate::widgets::{AssignProperty, AssignPropertyAttr, Attr, Attrs};
struct ProcessedAttrs {
watch: AssignPropertyAttr,
iterative: bool,
block_signals: Vec<Ident>,
chain: Option<Box<Expr>>,
}
impl AssignProperty {
pub(super) fn parse(
input: ParseStream<'_>,
attributes: Option<Attrs>,
args: Option<Args<Expr>>,
) -> Result<Self> {
let optional_assign = input.parse::<Token![?]>().is_ok();
let colon: Token! [:] = input.parse()?;
let colon_span = colon.span();
let expr = match input.parse() {
Ok(expr) => expr,
Err(parse_err) => {
let mut err = Error::new(colon_span, "Did you confuse `=` with`:`?");
err.combine(parse_err);
return Err(err);
}
};
let ProcessedAttrs {
watch,
iterative,
block_signals,
chain,
} = Self::process_attributes(&expr, attributes)?;
Ok(Self {
attr: watch,
expr,
args,
optional_assign,
iterative,
block_signals,
chain,
})
}
fn process_attributes(assign_expr: &Expr, attrs: Option<Attrs>) -> Result<ProcessedAttrs> {
if let Some(attrs) = attrs {
let mut iterative = false;
let mut watch = AssignPropertyAttr::None;
let mut block_signals = Vec::with_capacity(0);
let mut chain = None;
for attr in attrs.inner {
let span = attr.span();
match attr {
Attr::Iterate(_) => {
if iterative {
return Err(attr_twice_error(span));
}
iterative = true;
}
Attr::Watch(_, skip_init) => {
if watch == AssignPropertyAttr::None {
watch = AssignPropertyAttr::Watch { skip_init }
} else {
return Err(attr_twice_error(span));
}
}
Attr::Track(_, skip_init, expr) => {
if watch == AssignPropertyAttr::None {
watch = if let Some(expr) = expr {
AssignPropertyAttr::Track {
track_expr: expr.to_token_stream(),
skip_init,
paste_model: false,
}
} else {
AssignPropertyAttr::Track {
track_expr: generate_tracker_from_expression(assign_expr)?,
skip_init,
paste_model: true,
}
};
} else {
return Err(attr_twice_error(span));
}
}
Attr::BlockSignal(_, idents) => {
if block_signals.is_empty() {
block_signals = idents;
} else {
return Err(attr_twice_error(span));
}
}
Attr::Chain(_, expr) => {
if chain.is_none() {
chain = Some(expr);
} else {
return Err(attr_twice_error(span));
}
}
_ => {
return Err(Error::new(
attr.span(),
"Properties can only have `watch`, `track` or `iterative` as attribute.",
));
}
}
}
Ok(ProcessedAttrs {
watch,
iterative,
block_signals,
chain,
})
} else {
Ok(ProcessedAttrs {
watch: AssignPropertyAttr::None,
iterative: false,
block_signals: Vec::with_capacity(0),
chain: None,
})
}
}
}
/// Helper function for the tracker attribute.
fn expr_field_from_expr_call(call_expr: &ExprCall) -> Option<&ExprField> {
let first_expr = call_expr.args.iter().next()?;
if let Expr::Field(expr_field) = first_expr {
Some(expr_field)
} else {
None
}
}
fn generate_tracker_from_expression(expression: &Expr) -> Result<TokenStream2> {
let error_fn = move |span, msg: &str| {
let error_msg =
"Unable to generate tracker function. Please pass a condition as string value of the `track` attribute.\n\
Usage: #[track = \"TRACK_CONDITION\"]";
Err(Error::new(span, format!("{error_msg}\nHint: {msg}")))
};
let unref_expr: &Expr = if let Expr::Reference(expr_ref) = expression {
&expr_ref.expr
} else {
expression
};
let expr_field_opt = match unref_expr {
Expr::Call(call_expr) => expr_field_from_expr_call(call_expr),
Expr::MethodCall(expr_method_call) => {
if let Expr::Field(ref expr_field) = *expr_method_call.receiver {
Some(expr_field)
} else {
None
}
}
Expr::Field(field_expr) => Some(field_expr),
_ => None,
};
let expr_field = if let Some(expr_field) = expr_field_opt {
expr_field
} else {
return error_fn(
unref_expr.span(),
"Couldn't find find a call or method expression.",
);
};
let ident = if let Member::Named(ident) = &expr_field.member {
ident.clone()
} else {
return error_fn(expr_field.member.span(), "Expected a named member");
};
let bool_stream = quote_spanned! { expr_field.span() => .changed(Self::#ident()) };
Ok(bool_stream)
}

View File

@ -1,214 +0,0 @@
use quote::ToTokens;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::{bracketed, parenthesized, token, Error, Expr, Ident, Lit, LitStr, Path, Result, Token};
use crate::widgets::{Attr, Attrs};
impl Parse for Attrs {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let mut attrs = Vec::new();
while input.peek(Token![#]) {
let _sharp: Token![#] = input.parse()?;
let attr_tokens;
bracketed!(attr_tokens in input);
let path: Path = attr_tokens.parse()?;
// Name attribute
attrs.push(if attr_tokens.is_empty() {
if let Some(ident) = path.get_ident() {
if ident == "local" {
Attr::Local(ident.clone())
} else if ident == "local_ref" {
Attr::LocalRef(ident.clone())
} else if ident == "root" {
Attr::Root(ident.clone())
} else if ident == "watch" {
Attr::Watch(ident.clone(), None)
} else if ident == "track" {
Attr::Track(ident.clone(), None, None)
} else if ident == "iterate" {
Attr::Iterate(ident.clone())
} else if ident == "template" {
Attr::Template(ident.clone())
} else if ident == "template_child" {
Attr::TemplateChild(ident.clone())
} else {
return Err(unexpected_attr_name(ident));
}
} else {
return Err(Error::new(path.span(), "Expected identifier."));
}
// List attribute: `#[name(item1, item2)]
} else if attr_tokens.peek(token::Paren) {
let paren_input;
parenthesized!(paren_input in attr_tokens);
let nested: Punctuated<Expr, token::Comma> =
Punctuated::parse_terminated(&paren_input)?;
if let Some(ident) = path.get_ident() {
if ident == "block_signal" {
let mut signal_idents = Vec::with_capacity(nested.len());
for expr in nested {
let ident = expect_ident_from_expr(&expr)?;
signal_idents.push(ident);
}
Attr::BlockSignal(ident.clone(), signal_idents)
} else if ident == "watch" {
let expr = expect_one_nested_expr(&nested)?;
if let Some(skip_init) = expr_to_skip_init_ident(expr) {
Attr::Watch(ident.clone(), Some(skip_init))
} else {
return Err(Error::new(nested.span(), "Expected `skip_init`."));
}
} else if ident == "track" {
let (skip_init, expr) = parse_track(&nested)?;
Attr::Track(ident.clone(), skip_init, expr.map(Box::new))
} else if ident == "transition" {
let expr = expect_one_nested_expr(&nested)?;
let ident = expect_ident_from_expr(expr)?;
Attr::Transition(ident.clone(), ident)
} else if ident == "name" {
let expr = expect_one_nested_expr(&nested)?;
let ident = expect_ident_from_expr(expr)?;
Attr::Name(ident.clone(), ident)
} else if ident == "wrap" {
let expr = expect_one_nested_expr(&nested)?;
let path = expect_path_from_expr(expr)?;
Attr::Wrap(ident.clone(), path)
} else if ident == "chain" {
let expr = expect_one_nested_expr(&nested)?;
Attr::Chain(ident.clone(), Box::new(expr.clone()))
} else {
return Err(unexpected_attr_name(ident));
}
} else {
return Err(Error::new(path.span(), "Expected identifier."));
}
// Value attribute: `#[name = literal)]
} else if attr_tokens.peek(Token![=]) {
let _eq: Token![=] = attr_tokens.parse()?;
let lit = attr_tokens.parse()?;
if let Some(ident) = path.get_ident() {
if ident == "track" {
let string = expect_string_lit(&lit)?;
Attr::Track(ident.clone(), None, Some(string.parse()?))
} else if ident == "doc" {
Attr::Doc(lit.into_token_stream())
} else if ident == "transition" {
let string = expect_string_lit(&lit)?;
Attr::Transition(ident.clone(), string.parse()?)
} else if ident == "name" {
let string = expect_string_lit(&lit)?;
Attr::Name(ident.clone(), string.parse()?)
} else {
return Err(unexpected_attr_name(ident));
}
} else {
return Err(Error::new(path.span(), "Expected identifier."));
}
} else {
return Err(Error::new(attr_tokens.span(), "Expected `]`, `(` or `=`."));
});
}
Ok(Attrs { inner: attrs })
}
}
fn unexpected_attr_name(ident: &Ident) -> Error {
Error::new(
ident.span(),
format!("Unexpected attribute name `{ident}`."),
)
}
fn expect_string_lit(lit: &Lit) -> Result<&LitStr> {
if let Lit::Str(string) = lit {
Ok(string)
} else {
Err(Error::new(
lit.span(),
"Expected string literal. Try this: `\"value\"`.",
))
}
}
fn expect_path_from_expr(expr: &Expr) -> Result<Path> {
if let Expr::Path(path) = expr {
Ok(path.path.clone())
} else {
Err(Error::new(
expr.span(),
format!("Expected path `{}`.", expr.to_token_stream()),
))
}
}
fn expect_ident_from_expr(expr: &Expr) -> Result<Ident> {
if let Expr::Path(path) = expr {
expect_ident_from_path(&path.path)
} else {
Err(Error::new(
expr.span(),
format!("Expected identifier `{}`.", expr.to_token_stream()),
))
}
}
fn expect_ident_from_path(path: &Path) -> Result<Ident> {
if let Some(ident) = path.get_ident() {
Ok(ident.clone())
} else {
Err(Error::new(path.span(), "Expected identifier."))
}
}
fn expect_one_nested_expr(nested: &Punctuated<Expr, token::Comma>) -> Result<&Expr> {
if nested.len() == 1 {
Ok(nested.first().unwrap())
} else {
Err(Error::new(nested.span(), "Expected only one expression."))
}
}
fn parse_track(nested: &Punctuated<Expr, token::Comma>) -> Result<(Option<Ident>, Option<Expr>)> {
let len = nested.len();
if len == 1 {
if let Some(skip_ident) = expr_to_skip_init_ident(&nested[0]) {
Ok((Some(skip_ident), None))
} else {
Ok((None, Some(nested[0].clone())))
}
} else if len == 2 {
if let Some(skip_ident) = expr_to_skip_init_ident(&nested[0]) {
Ok((Some(skip_ident), Some(nested.last().unwrap().clone())))
} else {
Err(Error::new(
nested.span(),
"Expected `skip_init` and an expression.",
))
}
} else {
Err(Error::new(
nested.span(),
"Expected exactly one or two expressions.",
))
}
}
fn expr_to_skip_init_ident(expr: &Expr) -> Option<Ident> {
if let Expr::Path(path) = &expr {
if let Some(ident) = path.path.get_ident() {
if ident == "skip_init" {
return Some(ident.clone());
}
}
}
None
}

View File

@ -1,31 +0,0 @@
use syn::parse::ParseStream;
use syn::{Expr, Token};
use crate::widgets::{parse_util, ConditionalBranches, IfBranch, MatchArm, ParseError};
impl ConditionalBranches {
pub(super) fn parse_if(input: ParseStream<'_>) -> Result<Self, ParseError> {
let mut if_branches = Vec::new();
let mut index = 0_usize;
while input.peek(Token![if]) || input.peek(Token![else]) {
if_branches.push(IfBranch::parse(input, index)?);
index += 1;
}
Ok(Self::If(if_branches))
}
pub(super) fn parse_match(input: ParseStream<'_>) -> Result<Self, ParseError> {
let match_token = input.parse()?;
let expr = Box::new(Expr::parse_without_eager_brace(input)?);
let braced = parse_util::braces(input)?;
let mut match_arms = Vec::new();
let mut index = 0_usize;
while !braced.is_empty() {
match_arms.push(MatchArm::parse(&braced, index)?);
index += 1;
}
Ok(Self::Match((match_token, expr, match_arms)))
}
}

View File

@ -1,134 +0,0 @@
use proc_macro2::TokenStream as TokenStream2;
use syn::parse::ParseStream;
use syn::{Error, Expr, Ident, Path, Token};
use crate::args::Args;
use crate::widgets::parse_util::{self, attr_twice_error};
use crate::widgets::{Attr, Attrs, ConditionalBranches, ConditionalWidget, ParseError};
type ConditionalAttrs = (
Option<Ident>,
Option<Ident>,
Option<TokenStream2>,
Option<Path>,
);
impl ConditionalWidget {
pub(super) fn parse(
input: ParseStream<'_>,
attrs: Option<Attrs>,
args: Option<Args<Expr>>,
) -> Result<Self, ParseError> {
let name = if input.peek2(Token![=]) && !input.peek2(Token![==]) && !input.peek(Token![!]) {
let name = input.parse()?;
let _assign: Token![=] = input.parse()?;
Some(name)
} else {
None
};
Self::parse_with_name(input, name, attrs, args)
}
pub(super) fn parse_with_name(
input: ParseStream<'_>,
name: Option<Ident>,
attrs: Option<Attrs>,
args: Option<Args<Expr>>,
) -> Result<Self, ParseError> {
let (transition, attr_name, doc_attr, assign_wrapper) = Self::process_attrs(attrs)?;
if attr_name.is_some() {
if let Some(name) = &name {
return Err(Error::new(
name.span(),
"Name defined as attribute and redefined here.",
)
.into());
}
}
let name = if let Some(name) = name {
name
} else if let Some(name) = attr_name {
name
} else {
parse_util::idents_to_snake_case(
[Ident::new("conditional_widget", input.span())].iter(),
input.span(),
)
};
if input.peek(Token![if]) {
let branches = ConditionalBranches::parse_if(input)?;
Ok(Self {
doc_attr,
transition,
assign_wrapper,
name,
args,
branches,
})
} else if input.peek(Token![match]) {
let branches = ConditionalBranches::parse_match(input)?;
Ok(Self {
doc_attr,
transition,
assign_wrapper,
name,
args,
branches,
})
} else {
Err(input.error("Expected `if` or `match`").into())
}
}
fn process_attrs(attrs: Option<Attrs>) -> Result<ConditionalAttrs, ParseError> {
let mut transition = None;
let mut name = None;
let mut doc_attr: Option<TokenStream2> = None;
let mut assign_wrapper = None;
if let Some(attrs) = attrs {
for attr in attrs.inner {
let span = attr.span();
match attr {
Attr::Transition(_, transition_value) => {
if transition.is_none() {
transition = Some(transition_value);
} else {
return Err(attr_twice_error(span).into());
}
}
Attr::Name(_, name_value) => {
if name.is_none() {
name = Some(name_value);
} else {
return Err(attr_twice_error(span).into());
}
}
Attr::Wrap(_, path) => {
if assign_wrapper.is_some() {
return Err(attr_twice_error(span).into());
}
assign_wrapper = Some(path.clone());
}
Attr::Doc(tokens) => {
if let Some(doc_tokens) = &mut doc_attr {
doc_tokens.extend(tokens);
} else {
doc_attr = Some(tokens);
}
}
_ => {
return Err(Error::new(
attr.span(),
"Conditional widgets can only have docs and `name` or `transition` as attribute.",
).into());
}
}
}
}
Ok((transition, name, doc_attr, assign_wrapper))
}
}

View File

@ -1,43 +0,0 @@
use proc_macro2::Span as Span2;
use syn::parse::ParseStream;
use syn::punctuated::Punctuated;
use syn::token::{And, Paren};
use syn::{Expr, ExprCall, ExprLit, ExprPath, Ident, Lit, LitStr};
use crate::args::Args;
use crate::widgets::{parse_util, IfBranch, ParseError, Widget};
impl IfBranch {
pub(super) fn parse(input: ParseStream<'_>, index: usize) -> Result<Self, ParseError> {
let cond = input.parse()?;
let braced = parse_util::braces(input)?;
let attributes = braced.parse().ok();
let args = args_from_index(index, input.span());
let mut widget = Widget::parse(&braced, attributes, Some(args))?;
widget.ref_token = Some(And {
spans: [Span2::call_site()],
});
Ok(Self { cond, widget })
}
}
pub(super) fn args_from_index(index: usize, span: Span2) -> Args<Expr> {
Args {
inner: vec![Expr::Call(ExprCall {
attrs: Vec::new(),
func: Box::new(Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: Ident::new("Some", span).into(),
})),
paren_token: Paren(span),
args: Punctuated::from_iter(vec![Expr::Lit(ExprLit {
attrs: Vec::new(),
lit: Lit::Str(LitStr::new(&format!("{index}"), span)),
})]),
})],
}
}

View File

@ -1,21 +0,0 @@
use syn::parse::{Parse, ParseStream};
use syn::{Result, Token};
use crate::widgets::IfCondition;
impl Parse for IfCondition {
fn parse(input: ParseStream<'_>) -> Result<Self> {
if input.peek(Token![if]) {
Ok(Self::If(input.parse()?, input.parse()?))
} else if input.peek(Token![else]) {
let else_token = input.parse()?;
if input.peek(Token![if]) {
Ok(Self::ElseIf(else_token, input.parse()?, input.parse()?))
} else {
Ok(Self::Else(else_token))
}
} else {
Err(input.error("Expected `if`, `if else` or `else`"))
}
}
}

View File

@ -1,45 +0,0 @@
use syn::parse::ParseStream;
use syn::token::And;
use syn::{token, Token};
use crate::widgets::parse::if_branch::args_from_index;
use crate::widgets::{parse_util, MatchArm, ParseError, Widget};
impl MatchArm {
pub(super) fn parse(input: ParseStream<'_>, index: usize) -> Result<Self, ParseError> {
if input.peek(Token![,]) {
let _comma: Token![,] = input.parse()?;
}
let pattern = syn::Pat::parse_multi_with_leading_vert(input)?;
let guard = if input.peek(token::FatArrow) {
None
} else {
Some((input.parse()?, input.parse()?))
};
let arrow = input.parse()?;
let braced;
let inner_tokens = if input.peek(token::Brace) {
braced = parse_util::braces(input)?;
&braced
} else {
input
};
let attributes = inner_tokens.parse().ok();
let args = args_from_index(index, input.span());
let ref_span = input.span();
let mut widget = Widget::parse(inner_tokens, attributes, Some(args))?;
widget.ref_token = Some(And { spans: [ref_span] });
Ok(Self {
pattern,
guard,
arrow,
widget,
})
}
}

View File

@ -1,17 +0,0 @@
mod assign_property;
mod attributes;
mod conditional_branches;
mod conditional_widget;
mod if_branch;
mod if_condition;
mod match_arm;
mod properties;
mod property;
mod property_name;
mod returned_widget;
mod signal_handler;
mod top_level_widget;
mod view_widgets;
mod widget;
mod widget_func;
mod widget_func_method;

View File

@ -1,143 +0,0 @@
use proc_macro2::{Literal, Punct};
use syn::ext::IdentExt;
use syn::parse::discouraged::Speculative;
use syn::parse::ParseStream;
use syn::punctuated::{Pair, Punctuated};
use syn::token::{And, At, Caret, Colon, Dot, Gt, Lt, Or, Question, Slash, Tilde, Underscore};
use syn::{braced, bracketed, parenthesized, token, Ident, Lifetime, Token};
use crate::widgets::{parse_util, ParseError, Properties, Property, PropertyName, PropertyType};
impl Properties {
pub(super) fn parse(input: ParseStream<'_>) -> Self {
let mut props: Punctuated<Property, Token![,]> = Punctuated::new();
loop {
if input.is_empty() {
break;
}
let parse_input = input.fork();
let (prop, contains_error) = Property::parse(&parse_input);
props.push(prop);
// Everything worked, advance input
if !contains_error {
input.advance_to(&parse_input);
}
if input.is_empty() {
break;
}
if let Err(prop) = parse_comma_error(input) {
// If there's already an error, ignore the additional comma error
if contains_error {
// Skip to next token to start with "fresh" and hopefully correct syntax.
while !parse_next_token(input).unwrap() {
let next_input = input.fork();
let (prop, contains_error) = Property::parse(&next_input);
if !contains_error {
// Point with correct syntax was found!
props.push(prop);
input.advance_to(&next_input);
// Now we should definitely have a comma
if let Err(prop) = parse_comma_error(input) {
props.push(prop);
}
break;
}
}
} else {
props.push(prop);
}
}
}
let properties = props.into_pairs().map(Pair::into_value).collect();
Properties { properties }
}
}
fn parse_comma_error(input: ParseStream<'_>) -> Result<(), Property> {
let lookahead = input.lookahead1();
if lookahead.peek(Token![,]) {
input.parse::<Token![,]>().unwrap();
Ok(())
} else {
let err = lookahead.error();
Err(Property {
name: PropertyName::Ident(parse_util::string_to_snake_case("comma_error")),
ty: PropertyType::ParseError(ParseError::Generic(err.to_compile_error())),
})
}
}
macro_rules! parse_type {
($input:ident, $ty:ty) => {
let _: $ty = $input.parse()?;
return Ok(false);
};
}
fn skip_inner_tokens(input: ParseStream<'_>) -> Result<(), syn::Error> {
while !input.is_empty() {
parse_next_token(input)?;
}
Ok(())
}
fn parse_next_token(input: ParseStream<'_>) -> Result<bool, syn::Error> {
let inner_tokens;
if input.is_empty() {
Ok(true)
} else if input.peek(Token![,]) {
let _comma: Token![,] = input.parse()?;
Ok(true)
} else if input.peek(token::Paren) {
parenthesized!(inner_tokens in input);
skip_inner_tokens(&inner_tokens)?;
Ok(false)
} else if input.peek(token::Bracket) {
bracketed!(inner_tokens in input);
skip_inner_tokens(&inner_tokens)?;
Ok(false)
} else if input.peek(token::Brace) {
braced!(inner_tokens in input);
skip_inner_tokens(&inner_tokens)?;
Ok(false)
} else if Ident::parse_any(input).is_ok() {
Ok(false)
} else if input.peek(And) {
parse_type!(input, And);
} else if input.peek(At) {
parse_type!(input, At);
} else if input.peek(Colon) {
parse_type!(input, Colon);
} else if input.peek(Slash) {
parse_type!(input, Slash);
} else if input.peek(syn::token::Eq) {
parse_type!(input, syn::token::Eq);
} else if input.peek(Gt) {
parse_type!(input, Gt);
} else if input.peek(Lt) {
parse_type!(input, Lt);
} else if input.peek(Or) {
parse_type!(input, Or);
} else if input.peek(Tilde) {
parse_type!(input, Tilde);
} else if input.peek(Caret) {
parse_type!(input, Caret);
} else if input.peek(Underscore) {
parse_type!(input, Underscore);
} else if input.peek(Question) {
parse_type!(input, Question);
} else if input.peek(Dot) {
parse_type!(input, Dot);
} else if input.peek(Lifetime) {
parse_type!(input, Lifetime);
} else if input.parse::<Punct>().is_ok() || input.parse::<Literal>().is_ok() {
Ok(false)
} else {
unreachable!("Every possible token should be covered. Please report this error at Relm4! \nContext: '''{input}''' \n");
}
}

View File

@ -1,142 +0,0 @@
use syn::parse::ParseStream;
use syn::spanned::Spanned;
use syn::{token, Error, Ident, Token};
use crate::widgets::{
parse_util, AssignProperty, Attrs, ConditionalWidget, ParseError, Property, PropertyName,
PropertyType, SignalHandler, Widget, WidgetFunc,
};
impl Property {
pub(super) fn parse(input: ParseStream<'_>) -> (Self, bool) {
match Self::parse_failing(input) {
Ok(prop) => (prop, false),
Err(err) => (
Self {
name: PropertyName::Ident(parse_util::string_to_snake_case("invalid_property")),
ty: PropertyType::ParseError(err),
},
true,
),
}
}
fn parse_failing(input: ParseStream<'_>) -> Result<Self, ParseError> {
// Handle `#[attrs]`
let mut attributes: Option<Attrs> = if input.peek(Token![#]) {
Some(input.parse()?)
} else {
None
};
// parse `if something { WIDGET } else { WIDGET}` or a similar match expression.
if input.peek(Token![if]) || input.peek(Token![match]) {
return Ok(Property {
name: PropertyName::RelmContainerExtAssign(input.span()),
ty: PropertyType::ConditionalWidget(ConditionalWidget::parse_with_name(
input,
None,
attributes.take(),
None,
)?),
});
}
// Parse path, ident or function
let func = WidgetFunc::parse(input)?;
// `gtk::Box { ... }`, `data.init_widget() -> gtk::Button { ... }` or `gtk::Box,`
if input.peek(token::Brace) || input.peek(Token![->]) || input.peek(Token![,]) {
let span = func.span();
let ty =
PropertyType::Widget(Widget::parse_for_container_ext(input, func, attributes)?);
Ok(Property {
name: PropertyName::RelmContainerExtAssign(span),
ty,
})
} else {
let name = func.into_property_name()?;
// check for property[a, b, c]: ...
let mut args = if input.peek(token::Bracket) {
let paren_input = parse_util::brackets(input)?;
Some(paren_input.parse()?)
} else {
None
};
// look for event handlers: signal[cloned_data, ...] => move |a, ...| { ... }
let ty = if input.peek(Token! [=>]) {
let _arrow: Token![=>] = input.parse()?;
PropertyType::SignalHandler(SignalHandler::parse_with_args(input, args.take())?)
}
// look for widgets
else if (input.peek(Token![=])
|| input.peek3(Token![=])
|| (input.peek(Token![:]) && input.peek2(Token![mut]) && input.peek3(Ident)))
// Don't interpret `property: value == other,` as a widget
&& !input.peek3(Token![==])
{
let is_conditional = if input.peek(Token![=]) {
let _token: Token![=] = input.parse()?;
input.peek(Token![if]) || input.peek(Token![match])
} else {
let _colon: Token![:] = input.parse()?;
input.peek3(Token![if]) || input.peek3(Token![match])
};
// match expression
if is_conditional {
PropertyType::ConditionalWidget(ConditionalWidget::parse(
input,
attributes.take(),
args.take(),
)?)
} else {
PropertyType::Widget(Widget::parse(input, attributes.take(), args.take())?)
}
}
// look for properties or optional properties (?)
else if input.peek(Token! [:]) || input.peek(Token! [?]) {
// look for ? at beginning for optional assign
PropertyType::Assign(AssignProperty::parse(
input,
attributes.take(),
args.take(),
)?)
} else {
return Err(input.error("Unexpected syntax.").into());
};
// Attributes must have been set to `None` by `take()`
if let Some(attrs) = attributes {
if let Some(first_attr) = attrs.inner.first() {
return Err(Error::new(
first_attr.span(),
"No attributes allowed in the following expression.",
)
.into());
}
}
// Arguments must have been set to `None` by `take()`
if let Some(args) = args {
if let Some(first_arg) = args.inner.first() {
return Err(Error::new(
first_arg.span(),
"No arguments allowed in this expression.",
)
.into());
}
}
if !input.is_empty() && !input.peek(Token![,]) {
Err(input
.error("expected `,`. Did you confuse `=` with`:`?")
.into())
} else {
Ok(Property { name, ty })
}
}
}
}

View File

@ -1,14 +0,0 @@
use syn::parse::{Parse, ParseStream};
use syn::{Result, Token};
use crate::widgets::PropertyName;
impl Parse for PropertyName {
fn parse(input: ParseStream<'_>) -> Result<Self> {
Ok(if input.peek(Token![::]) || input.peek2(Token! [::]) {
PropertyName::Path(input.parse()?)
} else {
PropertyName::Ident(input.parse()?)
})
}
}

View File

@ -1,50 +0,0 @@
use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned;
use syn::{braced, Ident, Result, Token};
use crate::widgets::{parse_util, Properties, ReturnedWidget};
impl Parse for ReturnedWidget {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let mut is_optional = false;
let (name, ty) = if input.peek(Ident) {
let name = input.parse()?;
let _colon: Token![:] = input.parse()?;
let ty = input.parse()?;
if input.peek(Token![?]) {
let _mark: Token![?] = input.parse()?;
is_optional = true;
}
(Some(name), Some(ty))
} else {
if input.peek(Token![?]) {
let _mark: Token![?] = input.parse()?;
is_optional = true;
}
(None, None)
};
let name = name.unwrap_or_else(|| {
parse_util::idents_to_snake_case(
[Ident::new("_returned_widget", input.span())].iter(),
ty.span(),
)
});
let inner;
let _token = braced!(inner in input);
let properties = Properties::parse(&inner);
Ok(ReturnedWidget {
name,
ty,
properties,
is_optional,
})
}
}

View File

@ -1,37 +0,0 @@
use syn::parse::ParseStream;
use syn::{Expr, Result, Token};
use crate::widgets::{Args, ClosureSignalHandler, SignalHandler, SignalHandlerVariant};
impl SignalHandler {
pub(super) fn parse_with_args(
input: ParseStream<'_>,
args: Option<Args<Expr>>,
) -> Result<Self> {
let inner = if args.is_some() || input.peek(Token![move]) || input.peek(Token![|]) {
ClosureSignalHandler::parse_with_args(input, args).map(SignalHandlerVariant::Closure)
} else {
input.parse().map(SignalHandlerVariant::Expr)
}?;
let handler_id = if input.peek(Token![@]) {
let _arrow: Token![@] = input.parse()?;
input.parse()?
} else {
None
};
Ok(Self { inner, handler_id })
}
}
impl ClosureSignalHandler {
pub(super) fn parse_with_args(
input: ParseStream<'_>,
args: Option<Args<Expr>>,
) -> Result<Self> {
let closure = input.parse()?;
Ok(Self { closure, args })
}
}

View File

@ -1,66 +0,0 @@
use syn::parse::ParseStream;
use crate::util;
use crate::widgets::{
parse_util, Attr, Attrs, Properties, Property, PropertyName, PropertyType, TopLevelWidget,
Widget, WidgetAttr, WidgetFunc, WidgetTemplateAttr,
};
impl TopLevelWidget {
pub(super) fn parse(input: ParseStream<'_>) -> Self {
let attributes: Option<Attrs> = input.parse().ok();
// Look for #[root] attribute and remove it from the list if it exists
let (attributes, root_attr) = if let Some(prev_attributes) = attributes {
let mut attributes = Attrs {
inner: Vec::with_capacity(prev_attributes.inner.len()),
};
let mut root_attr = None;
for attr in prev_attributes.inner {
match attr {
Attr::Root(ident) => {
// Save root attribute and don't push it to the new list
root_attr = Some(ident);
}
_ => attributes.inner.push(attr),
}
}
(Some(attributes), root_attr)
} else {
(None, None)
};
let inner = match Widget::parse(input, attributes, None) {
Ok(inner) => inner,
Err(err) => Widget {
doc_attr: None,
attr: WidgetAttr::None,
template_attr: WidgetTemplateAttr::None,
mutable: None,
name: parse_util::string_to_snake_case("incorrect_top_level_widget"),
name_assigned_by_user: false,
func: WidgetFunc {
path: util::strings_to_path(&["gtk", "Box"]),
args: None,
method_chain: None,
ty: None,
},
args: None,
properties: Properties {
properties: vec![Property {
name: PropertyName::Ident(parse_util::string_to_snake_case(
"invalid_property",
)),
ty: PropertyType::ParseError(err),
}],
},
assign_wrapper: None,
ref_token: None,
deref_token: None,
returned_widget: None,
},
};
Self { root_attr, inner }
}
}

View File

@ -1,39 +0,0 @@
use syn::parse::{Parse, ParseStream, Result};
use syn::{Error, Ident, Token};
use crate::widgets::{TopLevelWidget, ViewWidgets};
impl Parse for ViewWidgets {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let span = input.span();
let first_widget = TopLevelWidget::parse(input);
let mut root_exists = first_widget.root_attr.is_some();
let mut top_level_widgets = vec![first_widget];
// Parse colon between widgets and look for more
while input.parse::<Token![,]>().is_ok() && !input.is_empty() {
let widget = TopLevelWidget::parse(input);
if let Some(root_attr) = &widget.root_attr {
if root_exists {
return Err(Error::new(root_attr.span(), "You cannot have two roots."));
}
root_exists = true;
}
top_level_widgets.push(widget);
}
if !root_exists && top_level_widgets.len() == 1 {
top_level_widgets[0].root_attr = Some(Ident::new("root", input.span()));
}
if input.is_empty() {
Ok(Self {
span,
top_level_widgets,
})
} else {
Err(input.error("Unexpected end of input. Maybe a missing comma `,`?"))
}
}
}

View File

@ -1,285 +0,0 @@
use proc_macro2::TokenStream as TokenStream2;
use syn::parse::ParseStream;
use syn::spanned::Spanned;
use syn::token::{And, Star};
use syn::{Error, Expr, Ident, Path, Token};
use crate::args::Args;
use crate::widgets::parse_util::{self, attr_twice_error};
use crate::widgets::{
Attr, Attrs, ParseError, Properties, PropertyType, Widget, WidgetAttr, WidgetFunc,
WidgetTemplateAttr,
};
type WidgetFuncInfo = (Option<And>, Option<Star>, WidgetFunc, Properties);
type AttributeInfo = (
WidgetAttr,
Option<TokenStream2>,
Option<Ident>,
Option<Path>,
WidgetTemplateAttr,
);
impl Widget {
pub(super) fn parse(
input: ParseStream<'_>,
attributes: Option<Attrs>,
args: Option<Args<Expr>>,
) -> Result<Self, ParseError> {
let (attr, doc_attr, new_name, assign_wrapper, template_attr) =
Self::process_attributes(attributes)?;
// Check if first token is `mut`
let mutable = input.parse().ok();
// Look for name = Widget syntax
let name_opt: Option<Ident> = if input.peek2(Token![=]) {
if attr.is_local_attr() || template_attr == WidgetTemplateAttr::TemplateChild {
return Err(input.error("When using the `local`, `local_ref` or `template_child` attributes you cannot rename the existing local variable.").into());
}
let name = input.parse()?;
let _token: Token![=] = input.parse()?;
Some(name)
} else {
None
};
let (ref_token, deref_token, func, properties) = Self::parse_widget_func(input)?;
// Make sure that the name is only defined one.
let mut name_set = name_opt.is_some();
if new_name.is_some() {
if name_set {
return Err(Error::new(name_opt.unwrap().span(), "Widget name is specified more than once (attribute, assignment or local attribute).").into());
}
name_set = true;
}
if name_set && (attr.is_local_attr() || template_attr == WidgetTemplateAttr::TemplateChild)
{
return Err(Error::new(input.span(), "Widget name is specified more than once (attribute, assignment or local attribute).").into());
}
// Generate a name if no name was given.
let (name, name_assigned_by_user) = if let Some(name) = name_opt.or(new_name) {
(name, true)
} else if attr.is_local_attr() || template_attr == WidgetTemplateAttr::TemplateChild {
(Self::local_attr_name(&func)?, true)
} else {
(func.snake_case_name(), false)
};
let returned_widget = if input.peek(Token![->]) {
let _arrow: Token![->] = input.parse()?;
Some(input.parse()?)
} else {
None
};
Self::check_props(&properties, &template_attr)?;
Ok(Widget {
doc_attr,
attr,
template_attr,
mutable,
name,
name_assigned_by_user,
func,
args,
properties,
assign_wrapper,
ref_token,
deref_token,
returned_widget,
})
}
pub(super) fn parse_for_container_ext(
input: ParseStream<'_>,
func: WidgetFunc,
attributes: Option<Attrs>,
) -> Result<Self, ParseError> {
let (attr, doc_attr, new_name, assign_wrapper, template_attr) =
Self::process_attributes(attributes)?;
if let Some(wrapper) = assign_wrapper {
return Err(Error::new(
wrapper.span(),
"Can't use wrapper types in container assignment.",
)
.into());
}
let properties = if input.peek(Token![,]) {
Properties::default()
} else {
let inner = parse_util::braces(input)?;
Properties::parse(&inner)
};
// Make sure that the name is only defined one.
if attr.is_local_attr() || template_attr == WidgetTemplateAttr::TemplateChild {
if let Some(name) = &new_name {
return Err(Error::new(name.span(), "Widget name is specified more than once (attribute, assignment or local attribute).").into());
}
}
// Generate a name
let (name, name_assigned_by_user) = if let Some(name) = new_name {
(name, true)
} else if attr.is_local_attr() || template_attr == WidgetTemplateAttr::TemplateChild {
(Self::local_attr_name(&func)?, true)
} else {
(func.snake_case_name(), false)
};
let ref_token = Some(And::default());
Self::check_props(&properties, &template_attr)?;
Ok(Widget {
doc_attr,
attr,
template_attr,
mutable: None,
name,
name_assigned_by_user,
func,
args: None,
properties,
assign_wrapper,
ref_token,
deref_token: None,
returned_widget: None,
})
}
fn check_props(
props: &Properties,
template_attr: &WidgetTemplateAttr,
) -> Result<(), ParseError> {
// Make sure template_child is only used in a valid context.
if template_attr != &WidgetTemplateAttr::Template {
for prop in &props.properties {
if let PropertyType::Widget(widget) = &prop.ty {
if widget.template_attr == WidgetTemplateAttr::TemplateChild {
return Err(ParseError::Generic(
syn::Error::new(
widget.name.span(),
"You can't use a template child if the parent is not a template.",
)
.to_compile_error(),
));
}
}
}
}
Ok(())
}
fn process_attributes(attrs: Option<Attrs>) -> Result<AttributeInfo, ParseError> {
if let Some(attrs) = attrs {
let mut widget_attr = WidgetAttr::None;
let mut doc_attr: Option<TokenStream2> = None;
let mut name = None;
let mut assign_wrapper = None;
let mut template_attr = WidgetTemplateAttr::None;
for attr in attrs.inner {
let span = attr.span();
match attr {
Attr::Local(_) => {
if widget_attr == WidgetAttr::None {
widget_attr = WidgetAttr::Local;
} else {
return Err(attr_twice_error(span).into());
}
}
Attr::LocalRef(_) => {
if widget_attr == WidgetAttr::None {
widget_attr = WidgetAttr::LocalRef;
} else {
return Err(attr_twice_error(span).into());
}
}
Attr::Doc(tokens) => {
if let Some(doc_tokens) = &mut doc_attr {
doc_tokens.extend(tokens);
} else {
doc_attr = Some(tokens);
}
}
Attr::Name(_, name_value) => {
if name.is_some() {
return Err(attr_twice_error(span).into());
}
name = Some(name_value);
}
Attr::Wrap(_, path) => {
if assign_wrapper.is_some() {
return Err(attr_twice_error(span).into());
}
assign_wrapper = Some(path.clone());
}
Attr::Template(_) => {
if template_attr != WidgetTemplateAttr::None {
return Err(attr_twice_error(span).into());
}
template_attr = WidgetTemplateAttr::Template;
}
Attr::TemplateChild(_) => {
if template_attr != WidgetTemplateAttr::None {
return Err(attr_twice_error(span).into());
}
template_attr = WidgetTemplateAttr::TemplateChild;
}
_ => {
return Err(Error::new(
attr.span(),
"Widgets can only have docs and `local`, `local_ref`, `wrap`, `name`, `template`, `template_child` or `root` as attribute.",
).into());
}
}
}
Ok((widget_attr, doc_attr, name, assign_wrapper, template_attr))
} else {
Ok((WidgetAttr::None, None, None, None, WidgetTemplateAttr::None))
}
}
// Make sure that the widget function is just a single identifier of the
// local variable if a local attribute was set.
fn local_attr_name(func: &WidgetFunc) -> Result<Ident, ParseError> {
if let Some(name) = func.path.get_ident() {
Ok(name.clone())
} else {
Err(Error::new(
func.path.span(),
"Expected identifier due to the `local`, `local_ref` or `template_child` attribute.",
)
.into())
}
}
/// Parse information related to the widget function.
fn parse_widget_func(input: ParseStream<'_>) -> Result<WidgetFuncInfo, ParseError> {
// Look for &
let ref_token = input.parse().ok();
// Look for *
let deref_token = input.parse().ok();
let func = WidgetFunc::parse(input)?;
let properties = if input.peek(Token![,]) {
Properties::default()
} else {
let inner = parse_util::braces(input)?;
Properties::parse(&inner)
};
Ok((ref_token, deref_token, func, properties))
}
}

View File

@ -1,65 +0,0 @@
use syn::parse::ParseStream;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::{token, Ident, Path, Token};
use crate::widgets::{parse_util, ParseError, WidgetFunc};
impl WidgetFunc {
pub(super) fn parse_with_path(input: ParseStream<'_>, path: &Path) -> Result<Self, ParseError> {
match Self::parse_with_path_internal(input, path) {
Ok(func) => Ok(func),
Err(err) => Err(err.add_path(path)),
}
}
fn parse_with_path_internal(input: ParseStream<'_>, path: &Path) -> Result<Self, ParseError> {
if input.peek(Ident) {
return Err(ParseError::Generic(
syn::Error::new(
path.span()
.join(input.span())
.unwrap_or_else(|| input.span()),
"A path must not be followed by an identifier",
)
.into_compile_error(),
)
.add_path(path));
}
let args = if input.peek(token::Paren) {
let paren_input = parse_util::parens(input)?;
Some(paren_input.call(Punctuated::parse_terminated)?)
} else {
None
};
let method_chain = if input.peek(token::Dot) {
let _dot: token::Dot = input.parse()?;
Some(Punctuated::parse_separated_nonempty(input)?)
} else {
None
};
let ty = if input.peek(Token! [->]) {
let _token: Token! [->] = input.parse()?;
Some(input.parse()?)
} else {
None
};
Ok(WidgetFunc {
path: path.clone(),
args,
method_chain,
ty,
})
}
}
impl WidgetFunc {
pub(super) fn parse(input: ParseStream<'_>) -> Result<Self, ParseError> {
let path = &input.parse()?;
Self::parse_with_path(input, path)
}
}

View File

@ -1,25 +0,0 @@
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{parenthesized, token, Result};
use crate::widgets::WidgetFuncMethod;
impl Parse for WidgetFuncMethod {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let ident = input.parse()?;
let turbofish = input.parse().ok();
let args = if input.peek(token::Paren) {
let inner_input;
parenthesized!(inner_input in input);
Some(Punctuated::parse_terminated(&inner_input)?)
} else {
None
};
Ok(Self {
ident,
turbofish,
args,
})
}
}

View File

@ -1,162 +0,0 @@
use std::sync::atomic::{AtomicU16, Ordering};
use proc_macro2::Span as Span2;
use syn::parse::ParseBuffer;
use syn::spanned::Spanned;
use syn::{braced, bracketed, parenthesized, Error, Ident, Path};
use super::{ParseError, PropertyName};
use crate::widgets::{AssignPropertyAttr, WidgetAttr, WidgetFunc};
pub(super) fn attr_twice_error(span: Span2) -> Error {
Error::new(span, "Cannot use the same attribute twice.")
}
impl From<Error> for ParseError {
fn from(error: Error) -> Self {
Self::Generic(error.to_compile_error())
}
}
impl ParseError {
pub(super) fn add_path(self, path: &Path) -> Self {
if let ParseError::Generic(tokens) = self {
if let Some(ident) = path.get_ident() {
ParseError::Ident((ident.clone(), tokens))
} else {
ParseError::Path((path.clone(), tokens))
}
} else {
self
}
}
}
impl WidgetFunc {
pub(super) fn into_property_name(self) -> Result<PropertyName, Error> {
if let Some(methods) = &self.method_chain {
Err(Error::new(
methods.span(),
"Can't use method calls in property assignments",
))
} else if let Some(args) = &self.args {
Err(Error::new(
args.span(),
"Can't use function arguments in property assignments",
))
} else {
Ok(if let Some(ident) = self.path.get_ident() {
PropertyName::Ident(ident.clone())
} else {
PropertyName::Path(self.path)
})
}
}
}
impl WidgetFunc {
pub(super) fn snake_case_name(&self) -> Ident {
idents_to_snake_case(
self.path.segments.iter().map(|seg| &seg.ident),
self.path.span(),
)
}
}
impl WidgetAttr {
pub(super) fn is_local_attr(&self) -> bool {
matches!(self, Self::Local | Self::LocalRef)
}
}
impl AssignPropertyAttr {
pub(super) fn should_skip_init(&self) -> bool {
match self {
Self::None => false,
Self::Watch { skip_init } | Self::Track { skip_init, .. } => skip_init.is_some(),
}
}
}
impl PartialEq for AssignPropertyAttr {
fn eq(&self, other: &Self) -> bool {
core::mem::discriminant(self) == core::mem::discriminant(other)
}
}
pub(crate) fn string_to_snake_case(string: &str) -> Ident {
idents_to_snake_case(
[Ident::new(string, Span2::call_site())].iter(),
Span2::call_site(),
)
}
pub(crate) fn idents_to_snake_case<'a, I: Iterator<Item = &'a Ident>>(
idents: I,
span: Span2,
) -> Ident {
static COUNTER: AtomicU16 = AtomicU16::new(0);
let val = COUNTER.fetch_add(1, Ordering::Relaxed);
let index_str = val.to_string();
let segments: Vec<String> = idents
.map(|ident| ident.to_string().to_lowercase())
.collect();
let length: usize =
segments.iter().map(|seg| seg.len() + 1).sum::<usize>() + index_str.len() + 1;
let mut name: String = String::with_capacity(length);
for seg in &segments {
name.push('_');
name.push_str(seg);
}
name.push('_');
name.push_str(&index_str);
Ident::new(&name, span)
}
/// Weird hack to work around syn's awkward macros
/// that always return [`syn::Error`] and are worse
/// in every aspect compared to regular Rust code.
///
/// Sadly, the regular Rust API won't be made public,
/// see [#1190](https://github.com/dtolnay/syn/issues/1190).
pub(super) fn parens<'a>(input: &'a ParseBuffer<'_>) -> Result<ParseBuffer<'a>, ParseError> {
let content = (move || {
let content;
parenthesized!(content in input);
Ok(content)
})();
Ok(content?)
}
/// Weird hack to work around syn's awkward macros
/// that always return [`syn::Error`] and are worse
/// in every aspect compared to regular Rust code.
///
/// Sadly, the regular Rust API won't be made public,
/// see [#1190](https://github.com/dtolnay/syn/issues/1190).
pub(super) fn braces<'a>(input: &'a ParseBuffer<'_>) -> Result<ParseBuffer<'a>, ParseError> {
let content = (move || {
let content;
braced!(content in input);
Ok(content)
})();
Ok(content?)
}
/// Weird hack to work around syn's awkward macros
/// that always return [`syn::Error`] and are worse
/// in every aspect compared to regular Rust code.
///
/// Sadly, the regular Rust API won't be made public,
/// see [#1190](https://github.com/dtolnay/syn/issues/1190).
pub(super) fn brackets<'a>(input: &'a ParseBuffer<'_>) -> Result<ParseBuffer<'a>, ParseError> {
let content = (move || {
let content;
bracketed!(content in input);
Ok(content)
})();
Ok(content?)
}

View File

@ -1,25 +0,0 @@
use proc_macro2::Span as Span2;
use syn::spanned::Spanned;
use crate::widgets::Attr;
impl Attr {
pub(crate) fn span(&self) -> Span2 {
match self {
Self::Doc(tokens) => tokens.span(),
Self::Local(ident)
| Self::LocalRef(ident)
| Self::Root(ident)
| Self::Iterate(ident)
| Self::Watch(ident, _)
| Self::Track(ident, _, _)
| Self::BlockSignal(ident, _)
| Self::Name(ident, _)
| Self::Transition(ident, _)
| Self::Chain(ident, _)
| Self::Template(ident)
| Self::TemplateChild(ident)
| Self::Wrap(ident, _) => ident.span(),
}
}
}

View File

@ -1,6 +0,0 @@
//! Span implementations
mod attr;
mod property_name;
mod widget_func;
mod widget_func_method;

View File

@ -1,14 +0,0 @@
use proc_macro2::Span as Span2;
use syn::spanned::Spanned;
use crate::widgets::PropertyName;
impl PropertyName {
pub(crate) fn span(&self) -> Span2 {
match self {
PropertyName::Ident(ident) => ident.span(),
PropertyName::Path(path) => path.span(),
PropertyName::RelmContainerExtAssign(span) => *span,
}
}
}

View File

@ -1,10 +0,0 @@
use proc_macro2::Span as Span2;
use syn::spanned::Spanned;
use crate::widgets::WidgetFunc;
impl WidgetFunc {
pub(crate) fn span(&self) -> Span2 {
self.path.span()
}
}

View File

@ -1,23 +0,0 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens};
use crate::widgets::WidgetFuncMethod;
impl ToTokens for WidgetFuncMethod {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let WidgetFuncMethod {
ident,
turbofish,
args,
} = &self;
tokens.extend(if let Some(args) = args {
quote! {
#ident #turbofish (#args)
}
} else {
quote! {
#ident #turbofish
}
});
}
}

View File

@ -1,40 +0,0 @@
use gtk::prelude::GtkWindowExt;
use relm4::{gtk, ComponentParts, ComponentSender, SimpleComponent};
#[derive(Default)]
struct App;
#[relm4_macros::component]
impl SimpleComponent for App {
type Init = ();
type Input = ();
type Output = ();
type Widgets = AppWidgets;
view! {
gtk::Window {
set_title: Some("Simple app"),
set_default_size: (300, 100),
gtk::Box {
gtk::Builder::from_string("<Label id=\"label\"></Label>")
.object::<gtk::Label>("label")
.unwrap() -> gtk::Label {},
},
}
}
fn init(
_init: Self::Init,
_root: &Self::Root,
_sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = Self;
let widgets = view_output!();
ComponentParts { model, widgets }
}
fn update(&mut self, _msg: Self::Input, _sender: ComponentSender<Self>) {}
}

View File

@ -1,34 +0,0 @@
use relm4::{ComponentParts, ComponentSender, SimpleComponent};
#[derive(Default)]
struct App;
#[relm4_macros::component]
impl SimpleComponent for App {
type Init = ();
type Input = ();
type Output = ();
view! {
mut Vec::<Vec<u8>> {
push = mut Vec {
push: 0,
push: 1,
}
}
}
fn init(
_init: Self::Init,
_root: &Self::Root,
_sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = Self;
let widgets = view_output!();
ComponentParts { model, widgets }
}
fn update(&mut self, _msg: Self::Input, _sender: ComponentSender<Self>) {}
}

View File

@ -1,46 +0,0 @@
use gtk::prelude::{GtkWindowExt, OrientableExt};
use relm4::{gtk, ComponentParts, ComponentSender, SimpleComponent};
#[derive(Default)]
struct App;
trait TestType {
type Test;
}
impl TestType for App {
type Test = gtk::Box;
}
#[relm4_macros::component]
impl SimpleComponent for App {
type Init = ();
type Input = ();
type Output = ();
type Widgets = AppWidgets;
view! {
gtk::Window {
set_title: Some("Simple app"),
set_default_size: (300, 100),
gtk::Box::default() -> <App as TestType>::Test {
set_orientation: gtk::Orientation::Vertical,
},
}
}
fn init(
_: Self::Init,
root: &Self::Root,
_sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = Self;
let widgets = view_output!();
ComponentParts { model, widgets }
}
fn update(&mut self, _msg: (), _sender: ComponentSender<Self>) {}
}

View File

@ -1,28 +0,0 @@
use relm4::{gtk, ComponentParts, ComponentSender, SimpleComponent};
struct TestComponent;
#[relm4_macros::component]
impl SimpleComponent for TestComponent {
type Init = ();
type Input = ();
type Output = ();
view! {
gtk::Window {}
}
fn init(
_: Self::Init,
root: &Self::Root,
_sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = Self;
let widgets = view_output!();
ComponentParts { model, widgets }
}
}
fn main() {}

View File

@ -1,39 +0,0 @@
use relm4::{gtk, ComponentParts, ComponentSender, SimpleComponent};
struct App;
#[relm4_macros::component]
impl SimpleComponent for App {
type Init = ();
type Input = ();
type Output = ();
view! {
gtk::Window {
gtk::Label {
#[watch]
set_label: &format!("Counter: {counter}"),
}
}
}
fn pre_view() {
// Only works if pre_view isn't wrapped inside an IIFE
// because the local variable counter is used in the
// update_view method.
let counter = 1;
}
fn init(
_counter: Self::Init,
root: &Self::Root,
_sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = Self;
let counter = 1;
let widgets = view_output!();
ComponentParts { model, widgets }
}
}

View File

@ -1,44 +0,0 @@
use gtk::prelude::GtkWindowExt;
use relm4::{gtk, ComponentParts, ComponentSender, SimpleComponent};
#[derive(Default)]
struct AppModel;
#[relm4_macros::component]
impl SimpleComponent for AppModel {
type Init = ();
type Input = ();
type Output = ();
view! {
gtk::Window {
set_title: Some("Simple app"),
set_default_width: 300,
set_default_height: 100,
#[local_ref]
my_box_ref -> gtk::Box {
gtk::Label {
set_label: "This should compile",
}
},
}
}
fn init(
_init: Self::Init,
_root: &Self::Root,
_sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = Self;
let my_box = gtk::Box::default();
let my_box_ref = &my_box;
let widgets = view_output!();
ComponentParts { model, widgets }
}
fn update(&mut self, _msg: (), _sender: ComponentSender<Self>) {}
}

View File

@ -1,56 +0,0 @@
use gtk::prelude::GtkWindowExt;
use relm4::{gtk, ComponentParts, ComponentSender, SimpleComponent};
pub mod templates {
use relm4::{gtk, WidgetTemplate};
#[relm4::widget_template(pub)]
impl WidgetTemplate for TestTemplate {
view! {
gtk::Box {
#[name(test_child)]
gtk::Label {}
}
}
}
}
#[derive(Default)]
struct AppModel;
#[relm4_macros::component]
impl SimpleComponent for AppModel {
type Init = ();
type Input = ();
type Output = ();
type Widgets = AppWidgets;
view! {
gtk::Window {
set_title: Some("Simple app"),
set_default_size: (300, 100),
#[template]
templates::TestTemplate {
#[template_child]
test_child -> gtk::Label {
set_label: "It works!",
}
}
}
}
fn init(
_: Self::Init,
root: &Self::Root,
_sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = Self;
let widgets = view_output!();
ComponentParts { model, widgets }
}
fn update(&mut self, _msg: (), _sender: ComponentSender<Self>) {}
}

View File

@ -1,39 +0,0 @@
use relm4::{
component::{AsyncComponent, AsyncComponentParts, AsyncComponentSender},
gtk,
};
struct App;
#[relm4::component(pub async)]
impl AsyncComponent for App {
type Init = ();
type Input = ();
type Output = ();
type CommandOutput = ();
view! {
gtk::Window {}
}
// Initialize the component.
async fn init(
_init: Self::Init,
root: Self::Root,
_sender: AsyncComponentSender<Self>,
) -> AsyncComponentParts<Self> {
let model = Self;
let widgets = view_output!();
AsyncComponentParts { model, widgets }
}
async fn update(
&mut self,
_msg: Self::Input,
_sender: AsyncComponentSender<Self>,
_root: &Self::Root,
) {
}
}

View File

@ -1,26 +0,0 @@
use relm4::gtk;
fn widget() -> gtk::Separator {
// Mimic component.widget() call
gtk::Separator::default()
}
fn main() {
let local_widget = widget();
relm4_macros::view! {
gtk::Stack {
add_child = &gtk::Separator::default() { }
-> { set_needs_attention: false },
add_child = &gtk::Separator::default() { }
-> page: gtk::StackPage { set_needs_attention: false },
add_child = &widget() {} -> {
set_needs_attention: false
},
add_child: &widget(),
#[local_ref]
add_child = &local_widget {} -> {
set_needs_attention: false
},
}
}
}

View File

@ -1,77 +0,0 @@
use gtk::prelude::{BoxExt, ButtonExt, GtkWindowExt, OrientableExt};
use relm4::{gtk, ComponentParts, ComponentSender, RelmWidgetExt, SimpleComponent};
#[derive(Default)]
struct App {
counter: u8,
}
#[derive(Debug)]
enum AppMsg {
Increment,
Decrement,
}
#[relm4_macros::component]
impl SimpleComponent for App {
type Init = u8;
type Input = AppMsg;
type Output = ();
view! {
gtk::Window {
set_title: Some("Simple app"),
set_default_size: (300, 100),
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_margin_all: 5,
set_spacing: 5,
append = &gtk::Button {
set_label: "Increment",
connect_clicked => AppMsg::Increment,
},
append = &gtk::Button {
set_label: "Decrement",
connect_clicked => AppMsg::Decrement,
},
append = &gtk::Label {
set_margin_all: 5,
#[watch]
set_label: &format!("Counter: {}", model.counter),
}
},
}
}
fn init(
counter: Self::Init,
root: &Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = Self { counter };
let widgets = view_output!();
ComponentParts { model, widgets }
}
fn update(&mut self, msg: AppMsg, _sender: ComponentSender<Self>) {
match msg {
AppMsg::Increment => {
self.counter = self.counter.wrapping_add(1);
}
AppMsg::Decrement => {
self.counter = self.counter.wrapping_sub(1);
}
}
}
}
fn assert_debug_impl<T: std::fmt::Debug>() {}
#[test]
fn assert_widgets_impl_debug() {
assert_debug_impl::<AppWidgets>();
}

View File

@ -1,80 +0,0 @@
use gtk::prelude::{BoxExt, ButtonExt, GtkWindowExt, OrientableExt};
use relm4::{gtk, ComponentParts, ComponentSender, RelmWidgetExt, SimpleComponent};
#[derive(Default)]
struct App {
counter: u8,
}
#[derive(Debug)]
enum AppMsg {
Increment,
Decrement,
}
#[relm4_macros::component]
impl SimpleComponent for App {
type Init = u8;
type Input = AppMsg;
type Output = ();
view! {
gtk::Window {
set_title: Some("Simple app"),
set_default_size: (300, 100),
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_margin_all: 5,
set_spacing: 5,
append = &gtk::Button {
set_label: "Increment",
connect_clicked => AppMsg::Increment,
},
append = &gtk::Button {
set_label: "Decrement",
connect_clicked => AppMsg::Decrement,
},
append = &gtk::Label {
set_margin_all: 5,
#[watch(skip_init)]
set_label: &format!("Counter: {}{value}", model.counter),
#[track(skip_init, test)]
set_label: value,
}
},
}
}
fn pre_view() {
// These values are not available in init
// so we can make sure "skip_init" works
// by accessing the variables.
let test = true;
let value = "value";
}
fn init(
counter: Self::Init,
root: &Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = Self { counter };
let widgets = view_output!();
ComponentParts { model, widgets }
}
fn update(&mut self, msg: AppMsg, _sender: ComponentSender<Self>) {
match msg {
AppMsg::Increment => {
self.counter = self.counter.wrapping_add(1);
}
AppMsg::Decrement => {
self.counter = self.counter.wrapping_sub(1);
}
}
}
}

View File

@ -1,32 +0,0 @@
use relm4::{gtk, ComponentParts, ComponentSender, SimpleComponent};
#[derive(Default)]
struct TestComponent;
#[relm4_macros::component]
impl SimpleComponent for TestComponent {
type Init = ();
type Input = ();
type Output = ();
view! {
gtk::Window {}
}
fn init(
_init: Self::Init,
_root: &Self::Root,
_sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = Self::default();
let widgets = view_output!();
ComponentParts { model, widgets }
}
// Incorrect impl
fn
}
fn main() {}

View File

@ -1,29 +0,0 @@
error: expected identifier, found `}`
--> tests/ui/compile-fail/bad_impl.rs:30:1
|
7 | impl SimpleComponent for TestComponent {
| - while parsing this item list starting here
...
30 | }
| ^
| |
| expected identifier
| the item list ends here
warning: unused import: `gtk`
--> tests/ui/compile-fail/bad_impl.rs:1:13
|
1 | use relm4::{gtk, ComponentParts, ComponentSender, SimpleComponent};
| ^^^
|
= note: `#[warn(unused_imports)]` on by default
error[E0046]: not all trait items implemented, missing: `Root`, `Widgets`, `init_root`
--> tests/ui/compile-fail/bad_impl.rs:7:1
|
7 | impl SimpleComponent for TestComponent {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `Root`, `Widgets`, `init_root` in implementation
|
= help: implement the missing item: `type Root = /* Type */;`
= help: implement the missing item: `type Widgets = /* Type */;`
= help: implement the missing item: `fn init_root() -> <Self as SimpleComponent>::Root { todo!() }`

View File

@ -1,10 +0,0 @@
use relm4::SimpleComponent;
struct TestComponent;
#[relm4_macros::component]
impl SimpleComponent for TestComponent {
}
fn main() {}

View File

@ -1,20 +0,0 @@
error: expected `view!` macro invocation
--> tests/ui/compile-fail/component-empty.rs:6:1
|
6 | / impl SimpleComponent for TestComponent {
7 | |
8 | | }
| |_^
error[E0046]: not all trait items implemented, missing: `Input`, `Output`, `Init`, `Root`, `init_root`, `init`
--> tests/ui/compile-fail/component-empty.rs:6:1
|
6 | impl SimpleComponent for TestComponent {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `Input`, `Output`, `Init`, `Root`, `init_root`, `init` in implementation
|
= help: implement the missing item: `type Input = /* Type */;`
= help: implement the missing item: `type Output = /* Type */;`
= help: implement the missing item: `type Init = /* Type */;`
= help: implement the missing item: `type Root = /* Type */;`
= help: implement the missing item: `fn init_root() -> <Self as SimpleComponent>::Root { todo!() }`
= help: implement the missing item: `fn init(_: <Self as SimpleComponent>::Init, _: &<Self as SimpleComponent>::Root, _: ComponentSender<Self>) -> ComponentParts<Self> { todo!() }`

Some files were not shown because too many files have changed in this diff Show More