From 327522eb998d0a2e1cbf10ed1d5e6f9bc9581db1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 28 May 2025 19:58:43 +0200 Subject: [PATCH 01/83] Draft test recorder structure in `iced_devtools` --- Cargo.lock | 251 ++++++++++++++++++ core/src/renderer/null.rs | 1 - core/src/text.rs | 5 - devtools/Cargo.toml | 4 + devtools/build.rs | 7 + devtools/fonts/iced_devtools-icons.toml | 8 + devtools/fonts/iced_devtools-icons.ttf | Bin 0 -> 5888 bytes devtools/src/icon.rs | 40 +++ devtools/src/lib.rs | 339 +++++++++++++++++++----- program/src/lib.rs | 7 +- renderer/src/fallback.rs | 1 - tiny_skia/src/lib.rs | 1 - wgpu/src/lib.rs | 1 - widget/src/helpers.rs | 19 +- widget/src/pop.rs | 9 +- widget/src/themer.rs | 94 +++---- 16 files changed, 641 insertions(+), 146 deletions(-) create mode 100644 devtools/build.rs create mode 100644 devtools/fonts/iced_devtools-icons.toml create mode 100644 devtools/fonts/iced_devtools-icons.ttf create mode 100644 devtools/src/icon.rs diff --git a/Cargo.lock b/Cargo.lock index b90d4cec..265b802e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,6 +33,17 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.8.12" @@ -141,6 +152,9 @@ name = "arbitrary" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "arc" @@ -666,6 +680,25 @@ version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e93abca9e28e0a1b9877922aacb20576e05d4679ffa78c3d6dc22a26a216659" +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "cairo-sys-rs" version = "0.18.2" @@ -809,6 +842,16 @@ dependencies = [ "half", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.5.39" @@ -952,6 +995,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "core-foundation" version = "0.9.4" @@ -1066,6 +1115,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.4.2" @@ -1227,6 +1291,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" +[[package]] +name = "deflate64" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" + [[package]] name = "deranged" version = "0.4.0" @@ -1236,6 +1306,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "digest" version = "0.10.7" @@ -1244,6 +1325,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -1881,9 +1963,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -2193,6 +2277,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "home" version = "0.5.11" @@ -2462,11 +2555,27 @@ name = "iced_devtools" version = "0.14.0-dev" dependencies = [ "iced_debug", + "iced_fontello", "iced_program", + "iced_test", "iced_widget", "log", ] +[[package]] +name = "iced_fontello" +version = "0.14.0-dev" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1d2a83ec8063a28ddc818b018c2901a83fa7b773031b691dc80dd32cdb8f32" +dependencies = [ + "reqwest", + "serde", + "serde_json", + "sha2", + "toml", + "zip", +] + [[package]] name = "iced_futures" version = "0.14.0-dev" @@ -2785,6 +2894,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "integration" version = "0.1.0" @@ -3199,6 +3317,27 @@ dependencies = [ "num-traits", ] +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -4093,6 +4232,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -4687,6 +4836,7 @@ dependencies = [ "base64 0.22.1", "bytes", "encoding_rs", + "futures-channel", "futures-core", "futures-util", "h2 0.4.10", @@ -4701,6 +4851,7 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "native-tls", "once_cell", "percent-encoding", @@ -5905,9 +6056,16 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", + "toml_write", "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" + [[package]] name = "tooltip" version = "0.1.0" @@ -7427,6 +7585,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + [[package]] name = "yaml-rust" version = "0.4.5" @@ -7584,6 +7751,20 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zerotrie" @@ -7618,6 +7799,76 @@ dependencies = [ "syn", ] +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "getrandom 0.3.3", + "hmac", + "indexmap", + "lzma-rs", + "memchr", + "pbkdf2", + "sha1", + "thiserror 2.0.12", + "time", + "xz2", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zopfli" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.15+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zune-core" version = "0.4.12" diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 2251e527..8105f91a 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -31,7 +31,6 @@ impl text::Renderer for () { type Paragraph = (); type Editor = (); - const MONOSPACE_FONT: Font = Font::MONOSPACE; const ICON_FONT: Font = Font::DEFAULT; const CHECKMARK_ICON: char = '0'; const ARROW_DOWN_ICON: char = '0'; diff --git a/core/src/text.rs b/core/src/text.rs index 0b51f244..525381ef 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -276,11 +276,6 @@ pub trait Renderer: crate::Renderer { /// The [`Editor`] of this [`Renderer`]. type Editor: Editor + 'static; - /// A monospace font. - /// - /// It may be used by devtools. - const MONOSPACE_FONT: Self::Font; - /// The icon font of the backend. const ICON_FONT: Self::Font; diff --git a/devtools/Cargo.toml b/devtools/Cargo.toml index 04c3792b..fab9128d 100644 --- a/devtools/Cargo.toml +++ b/devtools/Cargo.toml @@ -19,6 +19,10 @@ time-travel = ["iced_program/time-travel"] [dependencies] iced_debug.workspace = true iced_program.workspace = true +iced_test.workspace = true iced_widget.workspace = true log.workspace = true + +[build-dependencies] +iced_fontello = "0.14.0-dev" diff --git a/devtools/build.rs b/devtools/build.rs new file mode 100644 index 00000000..5bcfd41b --- /dev/null +++ b/devtools/build.rs @@ -0,0 +1,7 @@ +#![allow(missing_docs)] +fn main() { + // println!("cargo::rerun-if-changed=fonts/iced_devtools-icons.toml"); + + // iced_fontello::build("fonts/iced_devtools-icons.toml") + // .expect("Build icons font"); +} diff --git a/devtools/fonts/iced_devtools-icons.toml b/devtools/fonts/iced_devtools-icons.toml new file mode 100644 index 00000000..69c8d86d --- /dev/null +++ b/devtools/fonts/iced_devtools-icons.toml @@ -0,0 +1,8 @@ +module = "icon" + +[glyphs] +play = "entypo-play" +stop = "entypo-stop" +record = "entypo-record" +import = "entypo-folder" +export = "entypo-floppy" diff --git a/devtools/fonts/iced_devtools-icons.ttf b/devtools/fonts/iced_devtools-icons.ttf new file mode 100644 index 0000000000000000000000000000000000000000..179db4f20f63ee12518bfbe6f661c660b1a8fb58 GIT binary patch literal 5888 zcmd^DU2Ggz6~1@&$IjYW+wo6AHw||)Uf1!?ZX7pp-Nbd=nO!@LQ@U}SbSI_FjCa>R z!S-(LO>kOT!cS?~wg?r$1L{K`cxxZ-*icf4s;Ux*ho}-f@PsNP5Gqd~Q9wv2;X5<4 zuAMqicx6_z_q*qO_uO;NJ@?+V8E1^Ou&XSf)uuGm!if@6=;w4h}r~-iyCt zjOWm|3X67S<=FWo`V#MJh09d|IiAK7_=?U{W*4W^|G@ahJi5)Z_DTivR`f@SG`nzR z=1=#HpJFV4m;HRMWEYb^JAN-?!4C8X=OAbp4m^%N>AU9^tE-Ve1F@iMXQ5oM*^5j^ z|0;x$MSHcv-ir>S|1$bw$zCjd|96dlME_09ak^4osh(SY?+2I~YBo|?E>)g?>)2b2 zbrsQnjZy3o*3U+mk{LTS#i9##b&1`>f<(td`puXx%+K1cG1748NXsGNEL`U+H~6<% zh%x0EEj0$$Tn7s zF%2)~=W{-18d73wXd|vX$Oh4G#0TRNuP5H>dE@r&%?Mi0f8r2Q$TFu`{@4b)!aoj!zutdTc*K6JH)MNjJ>f9lnn zH*fvs=1t|s&08P5|32S;)3*U1UVHE__g-ij{wr$@Q={a{-CEDd%A#@*g>|_AEjVV#n~&69f*3X!PqYL!G_Hv z{s!1YG5aP9Dw{W9|wV_eH=nwFZnnO`BfiBSdx9m$4!vGLu(q`fq5>nU;3D{ z9)8Tn3X5{v#{tO8J`MtZ*T*5&#eeGKFywVR5q6IM-Iq799_3M^T)DD5KRZ_yy~Y7C z&_8fQOkWXYh~}5<1z}&R&Xt!}#Hg4lFI7tm3*~g7yf|Md6)zM^m#gLS!b)nsP+nS@ zEX`h8u$Q;dY$G~XT3(qiFNs6x{%y45rKQrcT`d*KlaS)von}lu_ zRu^EKeEKwFJ{{{EER&b>m~{z$9)f56pK^wAj9qfa+iqk@}?!Q9GNLO2U4d zE}lX`osPxhl9`ejYGw^P(6IEBZWUh5_Vy0wJ zlYyk{H1af`>@p0W5EDfq-_6Nj&-t}pzSGDSvND{_$7LX4PCk(bIabRHnae>mYQ}^d zA~a;0!tvPfs23u?CuBeA`-%B(E-#Rcnk{4`m$x7hq>B(5Bs6HnEYmb&$d+t23X)Cc zC7U2}9R1jY+(&5Ngnhl06^L;?#HLNNXq%EJP1BFS6h*|N>SjuYG?5iDn6MF3gOSV2 z2341hs*V-F5Gy6aZn}{PQFI!nbwNrBH|Dud-;!C`f(-S=p)^EI)G(LRA4(uYr}I`W zW>1=V)r^}$j-JUw7o)uRuB2pxCU+RgHI$y4^+t46T}8R5x-FII8OaN9L^kxL6?$Rcv@=mar zF}vl?lDe9b?b^iY{KOeg7>h&R?#eqfhwU=X=AB);49RU>wk2uHp|o_Tg}z$xC3zRt zB9O@C9omOTmtMmPW7^ifxQbzQjCvqF4Z%((t%*p+;r}?KTNmptOXx7xt|G}OsF5|! zxmypN8grPEoyyBysxGoJit^ium9unV{dmtF&f0J_>$*-E=zxyfPDf)>zLbpZM|pQ4 z#?E9)?$#Vm?H=Tt+RtcCfZBUBCrE8fb3)Ya)toT3_i9c9wfi)uk=oB{&JJpm8nWT# zUxqDYS`}%@pQPQAlG-L^*M{Pr$GJ zh>uzo@lo52_^3@FK5BaqAGP-(K5BarAGHS%AGLjmkJ_{*hTV;HP!pEiV+kC3+@hMW z(aJQ{Y`-QCCgnlw$3xf(7G0sdTXaTL#K8B?szD>_o#^syKP5&k6C)Y#xUq zt>XQg(sMV?VND!yJwF0#o}KI`j|rSU+j>iK*7Za8-pY)qL(XB|NwE$h9|&OEF%~w9 zJ(Q9UXzAU&!ZqR}_P7^N8X=b`?GLofQS`}ir2AjvW(nQ*`C&Q{T>D@{}!z!vWgi95NUC^*&z?O;g;m$EOT*LXV zj@d&o6*G(>PBiW;u(!=5aDUS&uzbonS?F#IFsbb1Eonyo5P%OTw zUgD?x9@o}!8X7=(Al&Uhrnb&K*?AyO_5|@bX+KXq7d=Tl7d=Hh7hRw^v!Gct zCjp!0Brr{L5-1So1VE8E1WLpqFhd*yv#wtmpgGqsLi4U)gf6;%5&DAb7oi2$FG7p1 zUxb!izX+9)C+OrD^1tUj1dtdb+ z5x(X@B77a(;~Vx~aC@%3Z+MXCFM5#Z*D&(9XYZRHBtp%DM0iPCYjS_t%5ZEgs06b3 zqX~DJsV8M)Nd~%etMv;##qbB() -> Text<'a, Theme, Renderer> +where + Theme: text::Catalog + 'a, + Renderer: program::Renderer, +{ + icon("\u{25B6}") +} + +pub fn record<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer> +where + Theme: text::Catalog + 'a, + Renderer: program::Renderer, +{ + icon("\u{26AB}") +} + +pub fn stop<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer> +where + Theme: text::Catalog + 'a, + Renderer: program::Renderer, +{ + icon("\u{25A0}") +} + +fn icon<'a, Theme, Renderer>(codepoint: &'a str) -> Text<'a, Theme, Renderer> +where + Theme: text::Catalog + 'a, + Renderer: program::Renderer, +{ + text(codepoint).font(Font::with_name("iced_devtools-icons")) +} diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index e4c2e4d4..44923707 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -8,21 +8,26 @@ use iced_widget::runtime::futures; mod comet; mod executor; +mod icon; mod time_machine; +use crate::core::alignment::Horizontal::Right; use crate::core::border; use crate::core::keyboard; use crate::core::theme::{self, Base, Theme}; use crate::core::time::seconds; use crate::core::window; -use crate::core::{Alignment::Center, Color, Element, Length::Fill}; +use crate::core::{ + Alignment::Center, Color, Element, Font, Length::Fill, Size, +}; use crate::futures::Subscription; use crate::program::Program; use crate::runtime::Task; +use crate::runtime::font; use crate::time_machine::TimeMachine; use crate::widget::{ - bottom_right, button, center, column, container, horizontal_space, opaque, - row, scrollable, stack, text, themer, + Text, bottom_right, button, center, column, container, horizontal_space, + opaque, row, scrollable, stack, text, text_input, themer, }; use std::fmt; @@ -59,7 +64,11 @@ where ( devtools, - Task::batch([boot.map(Event::Program), task.map(Event::Message)]), + Task::batch([ + boot.map(Event::Program), + task.map(Event::Message), + font::load(icon::FONT).discard(), + ]), ) } @@ -110,6 +119,7 @@ where P: Program, { state: P::State, + size: Size, mode: Mode, show_notification: bool, time_machine: TimeMachine

, @@ -118,18 +128,28 @@ where #[derive(Debug, Clone)] pub enum Message { HideNotification, + Toggle, ToggleComet, CometLaunched(comet::launch::Result), InstallComet, Installing(comet::install::Result), CancelSetup, + ChangeWidth(String), + ChangeHeight(String), + Record, } enum Mode { - None, + Hidden, + Open { recorder: Recorder }, Setup(Setup), } +enum Recorder { + Idle { events: Vec }, + Recording { events: Vec }, +} + enum Setup { Idle { goal: Goal }, Running { logs: Vec }, @@ -148,7 +168,8 @@ where ( Self { state, - mode: Mode::None, + size: Size::new(512.0, 512.0), + mode: Mode::Hidden, show_notification: true, time_machine: TimeMachine::new(), }, @@ -172,10 +193,30 @@ where Task::none() } + Message::Toggle => { + match self.mode { + Mode::Hidden => { + self.mode = Mode::Open { + recorder: Recorder::Idle { events: Vec::new() }, + }; + } + Mode::Open { + recorder: Recorder::Idle { .. }, + } => { + self.mode = Mode::Hidden; + } + Mode::Setup(_) + | Mode::Open { + recorder: Recorder::Recording { .. }, + } => {} + } + + Task::none() + } Message::ToggleComet => { if let Mode::Setup(setup) = &self.mode { if matches!(setup, Setup::Idle { .. }) { - self.mode = Mode::None; + self.mode = Mode::Hidden; } Task::none() @@ -228,7 +269,7 @@ where Task::none() } comet::install::Event::Finished => { - self.mode = Mode::None; + self.mode = Mode::Hidden; comet::launch().discard() } } @@ -251,10 +292,34 @@ where Task::none() } Message::CancelSetup => { - self.mode = Mode::None; + self.mode = Mode::Hidden; Task::none() } + Message::ChangeWidth(width) => { + if let Ok(width) = width.parse() { + self.size.width = width; + } + + Task::none() + } + Message::ChangeHeight(height) => { + if let Ok(height) = height.parse() { + self.size.height = height; + } + + Task::none() + } + Message::Record => { + let (state, task) = program.boot(); + + self.state = state; + self.mode = Mode::Open { + recorder: Recorder::Recording { events: Vec::new() }, + }; + + task.map(Event::Program) + } }, Event::Program(message) => { self.time_machine.push(&message); @@ -299,6 +364,9 @@ where let view = { let view = program.view(state, window); + let theme = program.theme(state, window); + + let view: Element<'_, _, Theme, _> = themer(theme, view).into(); if self.time_machine.is_rewinding() { view.map(|_| Event::Discard) @@ -307,58 +375,177 @@ where } }; - let theme = program.theme(state, window); + let theme = program + .theme(state, window) + .palette() + .map(|palette| Theme::custom("iced devtools", palette)) + .unwrap_or_default(); - let derive_theme = move || { - theme - .palette() - .map(|palette| Theme::custom("iced devtools", palette)) - .unwrap_or_default() - }; + let setup = if let Mode::Setup(setup) = &self.mode { + let stage: Element<'_, _, Theme, P::Renderer> = match setup { + Setup::Idle { goal } => self::setup(goal), + Setup::Running { logs } => installation(logs), + }; - let mode = match &self.mode { - Mode::None => None, - Mode::Setup(setup) => { - let stage: Element<'_, _, Theme, P::Renderer> = match setup { - Setup::Idle { goal } => self::setup(goal), - Setup::Running { logs } => installation(logs), - }; + let setup = center( + container(stage) + .padding(20) + .max_width(500) + .style(container::bordered_box), + ) + .padding(10) + .style(|_theme| { + container::Style::default() + .background(Color::BLACK.scale_alpha(0.8)) + }); - let setup = center( - container(stage) - .padding(20) - .max_width(500) - .style(container::bordered_box), - ) - .padding(10) - .style(|_theme| { - container::Style::default() - .background(Color::BLACK.scale_alpha(0.8)) - }); - - Some(setup) - } + Some(setup) + } else { + None } - .map(|mode| { - themer(derive_theme(), Element::from(mode).map(Event::Message)) - }); + .map(|mode| Element::from(mode).map(Event::Message)); let notification = self.show_notification.then(|| { - themer( - derive_theme(), - bottom_right(opaque( - container(text("Press F12 to open debug metrics")) - .padding(10) - .style(container::dark), - )), - ) + bottom_right(opaque( + container(text("Press F12 to open developer tools")) + .padding(10) + .style(container::dark), + )) }); - stack![view] - .height(Fill) - .push_maybe(mode.map(opaque)) - .push_maybe(notification) - .into() + let sidebar = if let Mode::Open { recorder } = &self.mode { + let title = monospace("Developer Tools"); + + let recorder = { + let events = center(match recorder { + Recorder::Idle { events } if events.is_empty() => { + monospace("No events recorded yet!") + .size(14) + .width(Fill) + .center() + } + Recorder::Idle { events } + | Recorder::Recording { events } => { + monospace(format!("{} events recorded", events.len())) + } + }) + .style(container::bordered_box); + + let controls = { + row![ + button(icon::play().size(14).width(Fill).center()), + match recorder { + Recorder::Idle { .. } => { + button( + icon::record() + .size(14) + .width(Fill) + .center(), + ) + .on_press(Message::Record) + .style(button::danger) + } + Recorder::Recording { .. } => { + button( + icon::stop().size(14).width(Fill).center(), + ) + .on_press(Message::Record) + .style(button::success) + } + } + ] + .spacing(10) + }; + + column![events, controls].spacing(10).align_x(Center) + }; + + let viewport = row![ + text_input("Width", &self.size.width.to_string()) + .size(14) + .on_input(Message::ChangeWidth), + monospace("x"), + text_input("Height", &self.size.height.to_string()) + .size(14) + .on_input(Message::ChangeHeight), + ] + .spacing(10) + .align_y(Center); + + let tools = column![ + title, + labeled("Viewport", viewport), + labeled("Tester", recorder) + ] + .spacing(10); + + let sidebar = container(tools) + .padding(10) + .width(250) + .height(Fill) + .style(container::dark); + + Some(Element::from(sidebar).map(Event::Message)) + } else { + None + }; + + let content = row![if let Mode::Open { recorder } = &self.mode { + let is_recording = matches!(recorder, Recorder::Recording { .. }); + + let status = if is_recording { + monospace("Recording").style(|theme| text::Style { + color: Some(theme.palette().danger), + }) + } else { + monospace("Idle").style(|theme| text::Style { + color: Some( + theme.extended_palette().background.strongest.color, + ), + }) + }; + + let viewport = container( + scrollable( + container(view) + .width(self.size.width) + .height(self.size.height), + ) + .direction(scrollable::Direction::Both { + vertical: scrollable::Scrollbar::default(), + horizontal: scrollable::Scrollbar::default(), + }), + ) + .style(move |theme| { + let palette = theme.extended_palette(); + + container::Style { + border: border::width(2.0).color(if is_recording { + palette.danger.base.color + } else { + palette.background.strongest.color + }), + ..container::Style::default() + } + }) + .padding(10); + + center(column![status, viewport].spacing(10).align_x(Right)) + .padding(10) + .into() + } else { + view + }] + .push_maybe(sidebar); + + themer( + theme, + stack![content] + .height(Fill) + .push_maybe(setup.map(opaque)) + .push_maybe(notification), + ) + .into() } fn subscription(&self, program: &P) -> Subscription> { @@ -369,6 +556,9 @@ where let hotkeys = futures::keyboard::on_key_press(|key, _modifiers| match key { keyboard::Key::Named(keyboard::key::Named::F12) => { + Some(Message::Toggle) + } + keyboard::Key::Named(keyboard::key::Named::F11) => { Some(Message::ToggleComet) } _ => None, @@ -438,7 +628,7 @@ where fn setup(goal: &Goal) -> Element<'_, Message, Theme, Renderer> where - Renderer: core::text::Renderer + 'static, + Renderer: program::Renderer + 'static, { let controls = row![ button(text("Cancel").center().width(Fill)) @@ -460,14 +650,13 @@ where ]; let command = container( - text!( + monospace(format!( "cargo install --locked \\ --git https://github.com/iced-rs/comet.git \\ --rev {}", comet::COMPATIBLE_REVISION - ) - .size(14) - .font(Renderer::MONOSPACE_FONT), + )) + .size(14), ) .width(Fill) .padding(5) @@ -528,15 +717,15 @@ fn installation<'a, Renderer>( logs: &'a [String], ) -> Element<'a, Message, Theme, Renderer> where - Renderer: core::text::Renderer + 'a, + Renderer: program::Renderer + 'a, { column![ text("Installing comet...").size(20), container( scrollable( - column(logs.iter().map(|log| { - text(log).size(12).font(Renderer::MONOSPACE_FONT).into() - }),) + column( + logs.iter().map(|log| { monospace(log).size(12).into() }), + ) .spacing(3), ) .spacing(10) @@ -555,9 +744,9 @@ fn inline_code<'a, Renderer>( code: impl text::IntoFragment<'a>, ) -> Element<'a, Message, Theme, Renderer> where - Renderer: core::text::Renderer + 'a, + Renderer: program::Renderer + 'a, { - container(text(code).font(Renderer::MONOSPACE_FONT).size(12)) + container(monospace(code).size(12)) .style(|_theme| { container::Style::default() .background(Color::BLACK) @@ -566,3 +755,25 @@ where .padding([2, 4]) .into() } + +fn monospace<'a, Renderer>( + fragment: impl text::IntoFragment<'a>, +) -> Text<'a, Theme, Renderer> +where + Renderer: program::Renderer + 'a, +{ + text(fragment).font(Font::MONOSPACE) +} + +fn labeled<'a, Message, Renderer>( + fragment: impl text::IntoFragment<'a>, + content: impl Into>, +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Renderer: program::Renderer + 'a, +{ + column![monospace(fragment).size(14), content.into()] + .spacing(5) + .into() +} diff --git a/program/src/lib.rs b/program/src/lib.rs index e25cdb22..80e59330 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -4,10 +4,10 @@ pub use iced_runtime as runtime; pub use iced_runtime::core; pub use iced_runtime::futures; -use crate::core::Element; use crate::core::text; use crate::core::theme; use crate::core::window; +use crate::core::{Element, Font}; use crate::futures::{Executor, Subscription}; use crate::graphics::compositor; use crate::runtime::Task; @@ -585,9 +585,10 @@ pub fn with_executor( } /// The renderer of some [`Program`]. -pub trait Renderer: text::Renderer + compositor::Default {} +pub trait Renderer: text::Renderer + compositor::Default {} -impl Renderer for T where T: text::Renderer + compositor::Default {} +impl Renderer for T where T: text::Renderer + compositor::Default +{} /// A particular instance of a running [`Program`]. #[allow(missing_debug_implementations)] diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index 79e22c77..7b87e7e1 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -84,7 +84,6 @@ where type Paragraph = A::Paragraph; type Editor = A::Editor; - const MONOSPACE_FONT: Self::Font = A::MONOSPACE_FONT; const ICON_FONT: Self::Font = A::ICON_FONT; const CHECKMARK_ICON: char = A::CHECKMARK_ICON; const ARROW_DOWN_ICON: char = A::ARROW_DOWN_ICON; diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index 8f343277..a222e23c 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -235,7 +235,6 @@ impl core::text::Renderer for Renderer { type Paragraph = Paragraph; type Editor = Editor; - const MONOSPACE_FONT: Font = Font::MONOSPACE; const ICON_FONT: Font = Font::with_name("Iced-Icons"); const CHECKMARK_ICON: char = '\u{f00c}'; const ARROW_DOWN_ICON: char = '\u{e800}'; diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 7f74905b..13ed671d 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -655,7 +655,6 @@ impl core::text::Renderer for Renderer { type Paragraph = Paragraph; type Editor = Editor; - const MONOSPACE_FONT: Font = Font::MONOSPACE; const ICON_FONT: Font = Font::with_name("Iced-Icons"); const CHECKMARK_ICON: char = '\u{f00c}'; const ARROW_DOWN_ICON: char = '\u{e800}'; diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 5e7b30d7..429a597c 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -997,7 +997,6 @@ pub fn pop<'a, Message, Theme, Renderer>( ) -> Pop<'a, (), Message, Theme, Renderer> where Renderer: core::Renderer, - Message: Clone, { Pop::new(content) } @@ -2054,22 +2053,14 @@ where } /// A widget that applies any `Theme` to its contents. -pub fn themer<'a, Message, OldTheme, NewTheme, Renderer>( - new_theme: NewTheme, - content: impl Into>, -) -> Themer< - 'a, - Message, - OldTheme, - NewTheme, - impl Fn(&OldTheme) -> NewTheme, - Renderer, -> +pub fn themer<'a, Message, Theme, Renderer>( + theme: Theme, + content: impl Into>, +) -> Themer<'a, Message, Theme, Renderer> where Renderer: core::Renderer, - NewTheme: Clone, { - Themer::new(move |_| new_theme.clone(), content) + Themer::new(theme, content) } /// Creates a [`PaneGrid`] with the given [`pane_grid::State`] and view function. diff --git a/widget/src/pop.rs b/widget/src/pop.rs index 44da6a4e..8c6236bc 100644 --- a/widget/src/pop.rs +++ b/widget/src/pop.rs @@ -34,7 +34,6 @@ pub struct Pop< impl<'a, Message, Theme, Renderer> Pop<'a, (), Message, Theme, Renderer> where - Message: Clone, Renderer: core::Renderer, { /// Creates a new [`Pop`] widget with the given content. @@ -55,7 +54,6 @@ where impl<'a, Key, Message, Theme, Renderer> Pop<'a, Key, Message, Theme, Renderer> where - Message: Clone, Key: self::Key, Renderer: core::Renderer, { @@ -163,7 +161,6 @@ impl Widget for Pop<'_, Key, Message, Theme, Renderer> where Key: self::Key, - Message: Clone, Renderer: core::Renderer, { fn tag(&self) -> tree::Tag { @@ -243,8 +240,8 @@ where if let Some(on_show) = &self.on_show { shell.publish(on_show(layout.bounds().size())); } - } else if let Some(on_hide) = &self.on_hide { - shell.publish(on_hide.clone()); + } else if let Some(on_hide) = self.on_hide.take() { + shell.publish(on_hide); } state.should_notify_at = None; @@ -362,7 +359,7 @@ impl<'a, Key, Message, Theme, Renderer> From> for Element<'a, Message, Theme, Renderer> where - Message: Clone + 'a, + Message: 'a, Key: self::Key + 'a, Renderer: core::Renderer + 'a, Theme: 'a, diff --git a/widget/src/themer.rs b/widget/src/themer.rs index 693b8486..1b9e1c6a 100644 --- a/widget/src/themer.rs +++ b/widget/src/themer.rs @@ -10,63 +10,55 @@ use crate::core::{ Shell, Size, Vector, Widget, }; -use std::marker::PhantomData; - /// A widget that applies any `Theme` to its contents. /// /// This widget can be useful to leverage multiple `Theme` /// types in an application. #[allow(missing_debug_implementations)] -pub struct Themer<'a, Message, Theme, NewTheme, F, Renderer = crate::Renderer> +pub struct Themer<'a, Message, Theme, Renderer = crate::Renderer> where - F: Fn(&Theme) -> NewTheme, Renderer: crate::core::Renderer, { - content: Element<'a, Message, NewTheme, Renderer>, - to_theme: F, - text_color: Option Color>, - background: Option Background>, - old_theme: PhantomData, + content: Element<'a, Message, Theme, Renderer>, + theme: Theme, + text_color: Option Color>, + background: Option Background>, } -impl<'a, Message, Theme, NewTheme, F, Renderer> - Themer<'a, Message, Theme, NewTheme, F, Renderer> +impl<'a, Message, Theme, Renderer> Themer<'a, Message, Theme, Renderer> where - F: Fn(&Theme) -> NewTheme, Renderer: crate::core::Renderer, { /// Creates an empty [`Themer`] that applies the given `Theme` /// to the provided `content`. - pub fn new(to_theme: F, content: T) -> Self - where - T: Into>, - { + pub fn new( + theme: Theme, + content: impl Into>, + ) -> Self { Self { content: content.into(), - to_theme, + theme, text_color: None, background: None, - old_theme: PhantomData, } } /// Sets the default text [`Color`] of the [`Themer`]. - pub fn text_color(mut self, f: fn(&NewTheme) -> Color) -> Self { + pub fn text_color(mut self, f: fn(&Theme) -> Color) -> Self { self.text_color = Some(f); self } /// Sets the [`Background`] of the [`Themer`]. - pub fn background(mut self, f: fn(&NewTheme) -> Background) -> Self { + pub fn background(mut self, f: fn(&Theme) -> Background) -> Self { self.background = Some(f); self } } -impl Widget - for Themer<'_, Message, Theme, NewTheme, F, Renderer> +impl Widget + for Themer<'_, Message, Theme, Renderer> where - F: Fn(&Theme) -> NewTheme, Renderer: crate::core::Renderer, { fn tag(&self) -> tree::Tag { @@ -143,19 +135,17 @@ where &self, tree: &Tree, renderer: &mut Renderer, - theme: &Theme, + _theme: &AnyTheme, style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, ) { - let theme = (self.to_theme)(theme); - if let Some(background) = self.background { container::draw_background( renderer, &container::Style { - background: Some(background(&theme)), + background: Some(background(&self.theme)), ..container::Style::default() }, layout.bounds(), @@ -164,15 +154,21 @@ where let style = if let Some(text_color) = self.text_color { renderer::Style { - text_color: text_color(&theme), + text_color: text_color(&self.theme), } } else { *style }; - self.content - .as_widget() - .draw(tree, renderer, &theme, &style, layout, cursor, viewport); + self.content.as_widget().draw( + tree, + renderer, + &self.theme, + &style, + layout, + cursor, + viewport, + ); } fn overlay<'b>( @@ -182,15 +178,15 @@ where renderer: &Renderer, viewport: &Rectangle, translation: Vector, - ) -> Option> { - struct Overlay<'a, Message, Theme, NewTheme, Renderer> { - to_theme: &'a dyn Fn(&Theme) -> NewTheme, - content: overlay::Element<'a, Message, NewTheme, Renderer>, + ) -> Option> { + struct Overlay<'a, Message, Theme, Renderer> { + theme: &'a Theme, + content: overlay::Element<'a, Message, Theme, Renderer>, } - impl - overlay::Overlay - for Overlay<'_, Message, Theme, NewTheme, Renderer> + impl + overlay::Overlay + for Overlay<'_, Message, Theme, Renderer> where Renderer: crate::core::Renderer, { @@ -205,14 +201,14 @@ where fn draw( &self, renderer: &mut Renderer, - theme: &Theme, + _theme: &AnyTheme, style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, ) { self.content.as_overlay().draw( renderer, - &(self.to_theme)(theme), + &self.theme, style, layout, cursor, @@ -259,13 +255,13 @@ where &'b mut self, layout: Layout<'b>, renderer: &Renderer, - ) -> Option> + ) -> Option> { self.content .as_overlay_mut() .overlay(layout, renderer) .map(|content| Overlay { - to_theme: &self.to_theme, + theme: self.theme, content, }) .map(|overlay| overlay::Element::new(Box::new(overlay))) @@ -276,26 +272,24 @@ where .as_widget_mut() .overlay(tree, layout, renderer, viewport, translation) .map(|content| Overlay { - to_theme: &self.to_theme, + theme: &self.theme, content, }) .map(|overlay| overlay::Element::new(Box::new(overlay))) } } -impl<'a, Message, Theme, NewTheme, F, Renderer> - From> - for Element<'a, Message, Theme, Renderer> +impl<'a, Message, Theme, Renderer, AnyTheme> + From> + for Element<'a, Message, AnyTheme, Renderer> where Message: 'a, Theme: 'a, - NewTheme: 'a, - F: Fn(&Theme) -> NewTheme + 'a, Renderer: 'a + crate::core::Renderer, { fn from( - themer: Themer<'a, Message, Theme, NewTheme, F, Renderer>, - ) -> Element<'a, Message, Theme, Renderer> { + themer: Themer<'a, Message, Theme, Renderer>, + ) -> Element<'a, Message, AnyTheme, Renderer> { Element::new(themer) } } From 921467b5bec1568665e6f57b25de092d9c00e597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 29 May 2025 16:34:44 +0200 Subject: [PATCH 02/83] Draft `Instruction` DSL in `iced_test` --- Cargo.lock | 12 +- Cargo.toml | 1 + devtools/src/lib.rs | 157 ++++++--- devtools/src/widget.rs | 12 + devtools/src/widget/recorder.rs | 178 ++++++++++ test/Cargo.toml | 1 + test/src/error.rs | 39 +++ test/src/instruction.rs | 343 ++++++++++++++++++ test/src/lib.rs | 591 +------------------------------- test/src/simulator.rs | 531 ++++++++++++++++++++++++++++ widget/src/container.rs | 2 + widget/src/themer.rs | 10 +- 12 files changed, 1246 insertions(+), 631 deletions(-) create mode 100644 devtools/src/widget.rs create mode 100644 devtools/src/widget/recorder.rs create mode 100644 test/src/error.rs create mode 100644 test/src/instruction.rs create mode 100644 test/src/simulator.rs diff --git a/Cargo.lock b/Cargo.lock index 265b802e..9d7a5ed3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -476,7 +476,7 @@ dependencies = [ "anyhow", "arrayvec", "log", - "nom", + "nom 7.1.3", "num-rational", "v_frame", ] @@ -2656,6 +2656,7 @@ version = "0.14.0-dev" dependencies = [ "iced_renderer", "iced_runtime", + "nom 8.0.0", "png", "sha2", "thiserror 1.0.69", @@ -3613,6 +3614,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "noop_proc_macro" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index 3c19290b..1e7ef4e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -181,6 +181,7 @@ lilt = "0.8" log = "0.4" lyon = "1.0" lyon_path = "1.0" +nom = "8" num-traits = "0.2" ouroboros = "0.18" png = "0.17" diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index 44923707..fc6d1cdb 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -1,7 +1,7 @@ #![allow(missing_docs)] use iced_debug as debug; use iced_program as program; -use iced_widget as widget; +use iced_test as test; use iced_widget::core; use iced_widget::runtime; use iced_widget::runtime::futures; @@ -10,6 +10,7 @@ mod comet; mod executor; mod icon; mod time_machine; +mod widget; use crate::core::alignment::Horizontal::Right; use crate::core::border; @@ -24,6 +25,7 @@ use crate::futures::Subscription; use crate::program::Program; use crate::runtime::Task; use crate::runtime::font; +use crate::test::instruction; use crate::time_machine::TimeMachine; use crate::widget::{ Text, bottom_right, button, center, column, container, horizontal_space, @@ -137,6 +139,8 @@ pub enum Message { ChangeWidth(String), ChangeHeight(String), Record, + Stop, + Recorded(core::Event), } enum Mode { @@ -145,9 +149,9 @@ enum Mode { Setup(Setup), } -enum Recorder { - Idle { events: Vec }, - Recording { events: Vec }, +struct Recorder { + instructions: Vec, + is_recording: bool, } enum Setup { @@ -194,21 +198,19 @@ where Task::none() } Message::Toggle => { - match self.mode { + match &self.mode { Mode::Hidden => { self.mode = Mode::Open { - recorder: Recorder::Idle { events: Vec::new() }, + recorder: Recorder { + instructions: Vec::new(), + is_recording: false, + }, }; } - Mode::Open { - recorder: Recorder::Idle { .. }, - } => { + Mode::Open { recorder } if !recorder.is_recording => { self.mode = Mode::Hidden; } - Mode::Setup(_) - | Mode::Open { - recorder: Recorder::Recording { .. }, - } => {} + Mode::Setup(_) | Mode::Open { .. } => {} } Task::none() @@ -256,7 +258,6 @@ where .map(Message::Installing) .map(Event::Message) } - Message::Installing(Ok(installation)) => { let Mode::Setup(Setup::Running { logs }) = &mut self.mode else { @@ -311,15 +312,61 @@ where Task::none() } Message::Record => { - let (state, task) = program.boot(); - - self.state = state; - self.mode = Mode::Open { - recorder: Recorder::Recording { events: Vec::new() }, + let Mode::Open { recorder } = &mut self.mode else { + return Task::none(); }; + recorder.instructions.clear(); + recorder.is_recording = true; + + let (state, task) = program.boot(); + self.state = state; + task.map(Event::Program) } + Message::Recorded(event) => { + let Mode::Open { recorder } = &mut self.mode else { + return Task::none(); + }; + + let Some(interaction) = + instruction::Interaction::from_event(event) + else { + return Task::none(); + }; + + if let Some(test::Instruction::Interact(last_interaction)) = + recorder.instructions.pop() + { + let (last_interaction, new_interaction) = + last_interaction.merge(interaction); + + recorder.instructions.push( + test::Instruction::Interact(last_interaction), + ); + + if let Some(new_interaction) = new_interaction { + recorder.instructions.push( + test::Instruction::Interact(new_interaction), + ); + } + } else { + recorder + .instructions + .push(test::Instruction::Interact(interaction)); + } + + Task::none() + } + Message::Stop => { + let Mode::Open { recorder } = &mut self.mode else { + return Task::none(); + }; + + recorder.is_recording = false; + + Task::none() + } }, Event::Program(message) => { self.time_machine.push(&message); @@ -417,41 +464,43 @@ where let title = monospace("Developer Tools"); let recorder = { - let events = center(match recorder { - Recorder::Idle { events } if events.is_empty() => { - monospace("No events recorded yet!") + let events = container(if recorder.instructions.is_empty() { + Element::from(center( + monospace("No instructions recorded yet!") .size(14) .width(Fill) - .center() - } - Recorder::Idle { events } - | Recorder::Recording { events } => { - monospace(format!("{} events recorded", events.len())) - } + .center(), + )) + } else { + scrollable( + column(recorder.instructions.iter().map( + |instruction| { + monospace(instruction.to_string()) + .size(10) + .into() + }, + )) + .spacing(5), + ) + .spacing(5) + .into() }) - .style(container::bordered_box); + .width(Fill) + .height(Fill) + .style(container::rounded_box) + .padding(5); let controls = { row![ button(icon::play().size(14).width(Fill).center()), - match recorder { - Recorder::Idle { .. } => { - button( - icon::record() - .size(14) - .width(Fill) - .center(), - ) + if recorder.is_recording { + button(icon::stop().size(14).width(Fill).center()) + .on_press(Message::Stop) + .style(button::success) + } else { + button(icon::record().size(14).width(Fill).center()) .on_press(Message::Record) .style(button::danger) - } - Recorder::Recording { .. } => { - button( - icon::stop().size(14).width(Fill).center(), - ) - .on_press(Message::Record) - .style(button::success) - } } ] .spacing(10) @@ -491,7 +540,7 @@ where }; let content = row![if let Mode::Open { recorder } = &self.mode { - let is_recording = matches!(recorder, Recorder::Recording { .. }); + let is_recording = recorder.is_recording; let status = if is_recording { monospace("Recording").style(|theme| text::Style { @@ -507,9 +556,17 @@ where let viewport = container( scrollable( - container(view) - .width(self.size.width) - .height(self.size.height), + container(if recorder.is_recording { + widget::recorder(view) + .on_event(|event| { + Event::Message(Message::Recorded(event)) + }) + .into() + } else { + view + }) + .width(self.size.width) + .height(self.size.height), ) .direction(scrollable::Direction::Both { vertical: scrollable::Scrollbar::default(), @@ -673,7 +730,7 @@ where your iced applications.", column![ "Do you wish to install it with the \ - following command?", + following command?", command ] .spacing(10), diff --git a/devtools/src/widget.rs b/devtools/src/widget.rs new file mode 100644 index 00000000..3067a1ca --- /dev/null +++ b/devtools/src/widget.rs @@ -0,0 +1,12 @@ +mod recorder; + +pub use iced_widget::*; +pub use recorder::Recorder; + +use crate::core::Element; + +pub fn recorder<'a, Message, Theme, Renderer>( + content: impl Into>, +) -> Recorder<'a, Message, Theme, Renderer> { + Recorder::new(content) +} diff --git a/devtools/src/widget/recorder.rs b/devtools/src/widget/recorder.rs new file mode 100644 index 00000000..3fd88436 --- /dev/null +++ b/devtools/src/widget/recorder.rs @@ -0,0 +1,178 @@ +use crate::core::layout; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::widget; +use crate::core::widget::tree; +use crate::core::{ + self, Clipboard, Element, Event, Layout, Length, Point, Rectangle, Shell, + Size, Widget, +}; + +#[allow(missing_debug_implementations)] +pub struct Recorder<'a, Message, Theme, Renderer> { + content: Element<'a, Message, Theme, Renderer>, + on_event: Option Message + 'a>>, +} + +impl<'a, Message, Theme, Renderer> Recorder<'a, Message, Theme, Renderer> { + pub fn new( + content: impl Into>, + ) -> Self { + Self { + content: content.into(), + on_event: None, + } + } + + pub fn on_event( + mut self, + on_event: impl Fn(Event) -> Message + 'a, + ) -> Self { + self.on_event = Some(Box::new(on_event)); + self + } +} + +impl Widget + for Recorder<'_, Message, Theme, Renderer> +where + Renderer: core::Renderer, +{ + fn update( + &mut self, + state: &mut widget::Tree, + event: &Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) { + if shell.is_event_captured() { + return; + } + + self.content.as_widget_mut().update( + state, event, layout, cursor, renderer, clipboard, shell, viewport, + ); + + if let Some(on_event) = &self.on_event { + match event { + Event::Mouse(event) => { + if !cursor.is_over(layout.bounds()) { + return; + } + + match event { + mouse::Event::ButtonPressed(_) + | mouse::Event::ButtonReleased(_) + | mouse::Event::WheelScrolled { .. } => { + shell + .publish(on_event(Event::Mouse(event.clone()))); + } + mouse::Event::CursorMoved { position } => { + shell.publish(on_event(Event::Mouse( + mouse::Event::CursorMoved { + position: *position + - (layout.bounds().position() + - Point::ORIGIN), + }, + ))); + } + _ => {} + } + } + Event::Keyboard(event) => { + shell.publish(on_event(Event::Keyboard(event.clone()))); + } + _ => {} + } + } + } + + fn tag(&self) -> tree::Tag { + self.content.as_widget().tag() + } + + fn state(&self) -> tree::State { + self.content.as_widget().state() + } + + fn children(&self) -> Vec { + self.content.as_widget().children() + } + + fn diff(&self, tree: &mut tree::Tree) { + self.content.as_widget().diff(tree); + } + + fn size(&self) -> Size { + self.content.as_widget().size() + } + + fn size_hint(&self) -> Size { + self.content.as_widget().size_hint() + } + + fn layout( + &self, + tree: &mut widget::Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.content.as_widget().layout(tree, renderer, limits) + } + + fn draw( + &self, + tree: &widget::Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + self.content + .as_widget() + .draw(tree, renderer, theme, style, layout, cursor, viewport); + } + + fn mouse_interaction( + &self, + state: &widget::Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content + .as_widget() + .mouse_interaction(state, layout, cursor, viewport, renderer) + } + + fn operate( + &self, + state: &mut widget::Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + self.content + .as_widget() + .operate(state, layout, renderer, operation); + } +} + +impl<'a, Message, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: core::Renderer + 'a, +{ + fn from(recorder: Recorder<'a, Message, Theme, Renderer>) -> Self { + Element::new(recorder) + } +} diff --git a/test/Cargo.toml b/test/Cargo.toml index 2dd35e7f..af5795ed 100644 --- a/test/Cargo.toml +++ b/test/Cargo.toml @@ -19,6 +19,7 @@ iced_runtime.workspace = true iced_renderer.workspace = true iced_renderer.features = ["fira-sans"] +nom.workspace = true png.workspace = true sha2.workspace = true thiserror.workspace = true diff --git a/test/src/error.rs b/test/src/error.rs new file mode 100644 index 00000000..ae475f16 --- /dev/null +++ b/test/src/error.rs @@ -0,0 +1,39 @@ +use crate::Selector; + +use std::io; +use std::sync::Arc; + +/// A test error. +#[derive(Debug, Clone, thiserror::Error)] +pub enum Error { + /// No matching widget was found for the [`Selector`]. + #[error("no matching widget was found for the selector: {0:?}")] + NotFound(Selector), + /// An IO operation failed. + #[error("an IO operation failed: {0}")] + IOFailed(Arc), + /// The decoding of some PNG image failed. + #[error("the decoding of some PNG image failed: {0}")] + PngDecodingFailed(Arc), + /// The encoding of some PNG image failed. + #[error("the encoding of some PNG image failed: {0}")] + PngEncodingFailed(Arc), +} + +impl From for Error { + fn from(error: io::Error) -> Self { + Self::IOFailed(Arc::new(error)) + } +} + +impl From for Error { + fn from(error: png::DecodingError) -> Self { + Self::PngDecodingFailed(Arc::new(error)) + } +} + +impl From for Error { + fn from(error: png::EncodingError) -> Self { + Self::PngEncodingFailed(Arc::new(error)) + } +} diff --git a/test/src/instruction.rs b/test/src/instruction.rs new file mode 100644 index 00000000..1f8455f8 --- /dev/null +++ b/test/src/instruction.rs @@ -0,0 +1,343 @@ +use crate::core::keyboard; +use crate::core::mouse; +use crate::core::{Event, Point}; + +use std::fmt; + +#[derive(Debug, Clone)] +pub enum Instruction { + Interact(Interaction), +} + +impl fmt::Display for Instruction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Instruction::Interact(interaction) => interaction.fmt(f), + } + } +} + +#[derive(Debug, Clone)] +pub enum Interaction { + Mouse(Mouse), + Keyboard(Keyboard), +} + +impl Interaction { + pub fn from_event(event: Event) -> Option { + Some(match event { + Event::Mouse(mouse) => Self::Mouse(match mouse { + mouse::Event::CursorMoved { position } => Mouse::Move(position), + mouse::Event::ButtonPressed(button) => { + Mouse::Press { button, at: None } + } + mouse::Event::ButtonReleased(button) => { + Mouse::Release { button, at: None } + } + _ => None?, + }), + Event::Keyboard(keyboard) => Self::Keyboard(match keyboard { + keyboard::Event::KeyPressed { key, text, .. } => match key { + keyboard::Key::Named(keyboard::key::Named::Enter) => { + Keyboard::Press(Key::Enter) + } + keyboard::Key::Named(keyboard::key::Named::Escape) => { + Keyboard::Press(Key::Escape) + } + keyboard::Key::Named(keyboard::key::Named::Tab) => { + Keyboard::Press(Key::Tab) + } + keyboard::Key::Named(keyboard::key::Named::Backspace) => { + Keyboard::Press(Key::Backspace) + } + _ => Keyboard::Typewrite(text?.to_string()), + }, + keyboard::Event::KeyReleased { key, .. } => match key { + keyboard::Key::Named(keyboard::key::Named::Enter) => { + Keyboard::Release(Key::Enter) + } + keyboard::Key::Named(keyboard::key::Named::Escape) => { + Keyboard::Release(Key::Escape) + } + keyboard::Key::Named(keyboard::key::Named::Tab) => { + Keyboard::Release(Key::Tab) + } + keyboard::Key::Named(keyboard::key::Named::Backspace) => { + Keyboard::Release(Key::Backspace) + } + _ => None?, + }, + keyboard::Event::ModifiersChanged(_) => None?, + }), + _ => None?, + }) + } + + pub fn merge(self, next: Self) -> (Self, Option) { + match (self, next) { + (Self::Mouse(current), Self::Mouse(next)) => { + match (current, next) { + (Mouse::Move(_), Mouse::Move(to)) => { + (Self::Mouse(Mouse::Move(to)), None) + } + (Mouse::Move(to), Mouse::Press { button, at: None }) => ( + Self::Mouse(Mouse::Press { + button, + at: Some(to), + }), + None, + ), + (Mouse::Move(to), Mouse::Release { button, at: None }) => ( + Self::Mouse(Mouse::Release { + button, + at: Some(to), + }), + None, + ), + ( + Mouse::Press { + button: press, + at: press_at, + }, + Mouse::Release { + button: release, + at: release_at, + }, + ) if press == release + && release_at.is_none_or(|release_at| { + Some(release_at) == press_at + }) => + { + ( + Self::Mouse(Mouse::Click { + button: press, + at: press_at, + }), + None, + ) + } + (current, next) => { + (Self::Mouse(current), Some(Self::Mouse(next))) + } + } + } + (Self::Keyboard(current), Self::Keyboard(next)) => { + match (current, next) { + ( + Keyboard::Typewrite(current), + Keyboard::Typewrite(next), + ) => ( + Self::Keyboard(Keyboard::Typewrite(format!( + "{current}{next}" + ))), + None, + ), + (Keyboard::Press(current), Keyboard::Release(next)) + if current == next => + { + (Self::Keyboard(Keyboard::Type(current)), None) + } + (current, next) => { + (Self::Keyboard(current), Some(Self::Keyboard(next))) + } + } + } + (current, next) => (current, Some(next)), + } + } +} + +impl fmt::Display for Interaction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Interaction::Mouse(mouse) => mouse.fmt(f), + Interaction::Keyboard(keyboard) => keyboard.fmt(f), + } + } +} + +#[derive(Debug, Clone)] +pub enum Mouse { + Move(Point), + Press { + button: mouse::Button, + at: Option, + }, + Release { + button: mouse::Button, + at: Option, + }, + Click { + button: mouse::Button, + at: Option, + }, +} + +impl fmt::Display for Mouse { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Mouse::Move(point) => { + write!(f, "move cursor to ({:.2}, {:.2})", point.x, point.y) + } + Mouse::Press { button, at } => { + write!(f, "press {}", format::button_at(*button, *at)) + } + Mouse::Release { button, at } => { + write!(f, "release {}", format::button_at(*button, *at)) + } + Mouse::Click { button, at } => { + write!(f, "click {}", format::button_at(*button, *at)) + } + } + } +} + +#[derive(Debug, Clone)] +pub enum Keyboard { + Press(Key), + Release(Key), + Type(Key), + Typewrite(String), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Key { + Enter, + Escape, + Tab, + Backspace, +} + +impl fmt::Display for Keyboard { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Keyboard::Press(key) => { + write!(f, "press {}", format::key(*key)) + } + Keyboard::Release(key) => { + write!(f, "release {}", format::key(*key)) + } + Keyboard::Type(key) => { + write!(f, "type {}", format::key(*key)) + } + Keyboard::Typewrite(text) => { + write!(f, "type \"{text}\"") + } + } + } +} + +mod format { + use super::*; + + pub fn button_at(button: mouse::Button, at: Option) -> String { + if let Some(at) = at { + format!("{} at {}", self::button(button), point(at)) + } else { + self::button(button).to_owned() + } + } + + pub fn button(button: mouse::Button) -> &'static str { + match button { + mouse::Button::Left => "left", + mouse::Button::Right => "right", + mouse::Button::Middle => "middle", + mouse::Button::Back => "back", + mouse::Button::Forward => "forward", + mouse::Button::Other(_) => "other", + } + } + + pub fn point(point: Point) -> String { + format!("({:.2}, {:.2})", point.x, point.y) + } + + pub fn key(key: Key) -> &'static str { + match key { + Key::Enter => "enter", + Key::Escape => "escape", + Key::Tab => "tab", + Key::Backspace => "backspace", + } + } +} + +pub use parser::{Error as ParseError, run as parse}; + +mod parser { + use super::*; + + use nom::branch::alt; + use nom::bytes::complete::tag; + use nom::character::complete::{char, multispace0}; + use nom::combinator::{map, opt}; + use nom::number::float; + use nom::sequence::{delimited, preceded, separated_pair}; + use nom::{Finish, IResult, Parser}; + + #[derive(Debug, Clone, thiserror::Error)] + #[error("parse error: {0}")] + pub struct Error(nom::error::Error); + + pub fn run(input: &str) -> Result { + match instruction.parse_complete(input).finish() { + Ok((_rest, instruction)) => Ok(instruction), + Err(error) => Err(Error(error.cloned())), + } + } + + fn instruction(input: &str) -> IResult<&str, Instruction> { + map(interaction, Instruction::Interact).parse(input) + } + + fn interaction(input: &str) -> IResult<&str, Interaction> { + map(mouse, Interaction::Mouse).parse(input) + } + + fn mouse(input: &str) -> IResult<&str, Mouse> { + let mouse_move = + preceded(tag("move cursor to "), point).map(Mouse::Move); + + alt((mouse_move, mouse_click)).parse(input) + } + + fn mouse_click(input: &str) -> IResult<&str, Mouse> { + let (input, _) = tag("click ")(input)?; + + let (input, (button, at)) = mouse_button_at(input)?; + + Ok((input, Mouse::Click { button, at })) + } + + fn mouse_button_at( + input: &str, + ) -> IResult<&str, (mouse::Button, Option)> { + let (input, button) = mouse_button(input)?; + let (input, at) = opt(preceded(tag(" at "), point)).parse(input)?; + + Ok((input, (button, at))) + } + + fn mouse_button(input: &str) -> IResult<&str, mouse::Button> { + alt(( + tag("left").map(|_| mouse::Button::Left), + tag("right").map(|_| mouse::Button::Right), + )) + .parse(input) + } + + fn point(input: &str) -> IResult<&str, Point> { + let comma = (multispace0, char(','), multispace0); + + map( + delimited( + char('('), + separated_pair(float(), comma, float()), + char(')'), + ), + |(x, y)| Point { x, y }, + ) + .parse(input) + } +} diff --git a/test/src/lib.rs b/test/src/lib.rs index 636b8173..4ec0d8dd 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -24,15 +24,14 @@ //! # impl Counter { //! # pub fn view(&self) -> iced_runtime::core::Element<(), iced_runtime::core::Theme, iced_renderer::Renderer> { unimplemented!() } //! # } -//! use iced_test::selector::text; //! # use iced_test::simulator; //! # //! # let mut counter = Counter { value: 0 }; //! # let mut ui = simulator(counter.view()); //! -//! let _ = ui.click(text("+")); -//! let _ = ui.click(text("+")); -//! let _ = ui.click(text("-")); +//! let _ = ui.click("+"); +//! let _ = ui.click("+"); +//! let _ = ui.click("-"); //! ``` //! //! [`Simulator::click`] takes a [`Selector`]. A [`Selector`] describes a way to query the widgets of an interface. In this case, @@ -47,15 +46,14 @@ //! # pub fn update(&mut self, message: ()) {} //! # pub fn view(&self) -> iced_runtime::core::Element<(), iced_runtime::core::Theme, iced_renderer::Renderer> { unimplemented!() } //! # } -//! # use iced_test::selector::text; //! # use iced_test::simulator; //! # //! # let mut counter = Counter { value: 0 }; //! # let mut ui = simulator(counter.view()); //! # -//! # let _ = ui.click(text("+")); -//! # let _ = ui.click(text("+")); -//! # let _ = ui.click(text("-")); +//! # let _ = ui.click("+"); +//! # let _ = ui.click("+"); +//! # let _ = ui.click("-"); //! # //! for message in ui.into_messages() { //! counter.update(message); @@ -71,13 +69,12 @@ //! # impl Counter { //! # pub fn view(&self) -> iced_runtime::core::Element<(), iced_runtime::core::Theme, iced_renderer::Renderer> { unimplemented!() } //! # } -//! # use iced_test::selector::text; //! # use iced_test::simulator; //! # //! # let mut counter = Counter { value: 0 }; //! let mut ui = simulator(counter.view()); //! -//! assert!(ui.find(text("1")).is_ok(), "Counter should display 1!"); +//! assert!(ui.find("1").is_ok(), "Counter should display 1!"); //! ``` //! //! And that's it! That's the gist of testing `iced` applications! @@ -86,575 +83,23 @@ //! [`typewrite`](Simulator::typewrite)—and even perform [_snapshot testing_](Simulator::snapshot)! //! //! [the classical counter interface]: https://book.iced.rs/architecture.html#dissecting-an-interface -pub mod selector; - -pub use selector::Selector; - +#![allow(missing_docs)] use iced_renderer as renderer; use iced_runtime as runtime; use iced_runtime::core; -use crate::core::clipboard; -use crate::core::event; -use crate::core::keyboard; -use crate::core::mouse; -use crate::core::theme; -use crate::core::time; -use crate::core::widget; -use crate::core::window; -use crate::core::{ - Element, Event, Font, Point, Rectangle, Settings, Size, SmolStr, -}; -use crate::runtime::UserInterface; -use crate::runtime::user_interface; +pub mod instruction; +pub mod selector; +pub mod simulator; -use std::borrow::Cow; -use std::env; -use std::fs; -use std::io; -use std::path::{Path, PathBuf}; -use std::sync::Arc; +mod error; -/// Creates a new [`Simulator`]. -/// -/// This is just a function version of [`Simulator::new`]. -pub fn simulator<'a, Message, Theme, Renderer>( - element: impl Into>, -) -> Simulator<'a, Message, Theme, Renderer> -where - Theme: theme::Base, - Renderer: core::Renderer + core::renderer::Headless, -{ - Simulator::new(element) -} +pub use error::Error; +pub use instruction::Instruction; +pub use selector::Selector; +pub use simulator::{Simulator, simulator}; -/// A user interface that can be interacted with and inspected programmatically. -#[allow(missing_debug_implementations)] -pub struct Simulator< - 'a, - Message, - Theme = core::Theme, - Renderer = renderer::Renderer, -> { - raw: UserInterface<'a, Message, Theme, Renderer>, - renderer: Renderer, - size: Size, - cursor: mouse::Cursor, - messages: Vec, -} - -/// A specific area of a [`Simulator`], normally containing a widget. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Target { - /// The bounds of the area. - pub bounds: Rectangle, -} - -impl<'a, Message, Theme, Renderer> Simulator<'a, Message, Theme, Renderer> -where - Theme: theme::Base, - Renderer: core::Renderer + core::renderer::Headless, -{ - /// Creates a new [`Simulator`] with default [`Settings`] and a default size (1024x768). - pub fn new( - element: impl Into>, - ) -> Self { - Self::with_settings(Settings::default(), element) - } - - /// Creates a new [`Simulator`] with the given [`Settings`] and a default size (1024x768). - pub fn with_settings( - settings: Settings, - element: impl Into>, - ) -> Self { - Self::with_size(settings, window::Settings::default().size, element) - } - - /// Creates a new [`Simulator`] with the given [`Settings`] and size. - pub fn with_size( - settings: Settings, - size: impl Into, - element: impl Into>, - ) -> Self { - let size = size.into(); - - let default_font = match settings.default_font { - Font::DEFAULT => Font::with_name("Fira Sans"), - _ => settings.default_font, - }; - - for font in settings.fonts { - load_font(font).expect("Font must be valid"); - } - - let mut renderer = { - let backend = env::var("ICED_TEST_BACKEND").ok(); - - iced_runtime::futures::futures::executor::block_on(Renderer::new( - default_font, - settings.default_text_size, - backend.as_deref(), - )) - .expect("Create new headless renderer") - }; - - let raw = UserInterface::build( - element, - size, - user_interface::Cache::default(), - &mut renderer, - ); - - Simulator { - raw, - renderer, - size, - cursor: mouse::Cursor::Unavailable, - messages: Vec::new(), - } - } - - /// Finds the [`Target`] of the given widget [`Selector`] in the [`Simulator`]. - pub fn find( - &mut self, - selector: impl Into, - ) -> Result { - let selector = selector.into(); - - match &selector { - Selector::Id(id) => { - struct FindById<'a> { - id: &'a widget::Id, - target: Option, - } - - impl widget::Operation for FindById<'_> { - fn container( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - operate_on_children: &mut dyn FnMut( - &mut dyn widget::Operation<()>, - ), - ) { - if self.target.is_some() { - return; - } - - if Some(self.id) == id { - self.target = Some(Target { bounds }); - return; - } - - operate_on_children(self); - } - - fn scrollable( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - _content_bounds: Rectangle, - _translation: core::Vector, - _state: &mut dyn widget::operation::Scrollable, - ) { - if self.target.is_some() { - return; - } - - if Some(self.id) == id { - self.target = Some(Target { bounds }); - } - } - - fn text_input( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - _state: &mut dyn widget::operation::TextInput, - ) { - if self.target.is_some() { - return; - } - - if Some(self.id) == id { - self.target = Some(Target { bounds }); - } - } - - fn text( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - _text: &str, - ) { - if self.target.is_some() { - return; - } - - if Some(self.id) == id { - self.target = Some(Target { bounds }); - } - } - - fn custom( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - _state: &mut dyn std::any::Any, - ) { - if self.target.is_some() { - return; - } - - if Some(self.id) == id { - self.target = Some(Target { bounds }); - } - } - } - - let mut find = FindById { id, target: None }; - self.raw.operate(&self.renderer, &mut find); - - find.target.ok_or(Error::NotFound(selector)) - } - Selector::Text(text) => { - struct FindByText<'a> { - text: &'a str, - target: Option, - } - - impl widget::Operation for FindByText<'_> { - fn container( - &mut self, - _id: Option<&widget::Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut( - &mut dyn widget::Operation<()>, - ), - ) { - if self.target.is_some() { - return; - } - - operate_on_children(self); - } - - fn text( - &mut self, - _id: Option<&widget::Id>, - bounds: Rectangle, - text: &str, - ) { - if self.target.is_some() { - return; - } - - if self.text == text { - self.target = Some(Target { bounds }); - } - } - } - - let mut find = FindByText { text, target: None }; - self.raw.operate(&self.renderer, &mut find); - - find.target.ok_or(Error::NotFound(selector)) - } - } - } - - /// Points the mouse cursor at the given position in the [`Simulator`]. - /// - /// This does _not_ produce mouse movement events! - pub fn point_at(&mut self, position: impl Into) { - self.cursor = mouse::Cursor::Available(position.into()); - } - - /// Clicks the [`Target`] found by the given [`Selector`], if any. - /// - /// This consists in: - /// - Pointing the mouse cursor at the center of the [`Target`]. - /// - Simulating a [`click`]. - pub fn click( - &mut self, - selector: impl Into, - ) -> Result { - let target = self.find(selector)?; - self.point_at(target.bounds.center()); - - let _ = self.simulate(click()); - - Ok(target) - } - - /// Simulates a key press, followed by a release, in the [`Simulator`]. - pub fn tap_key(&mut self, key: impl Into) -> event::Status { - self.simulate(tap_key(key, None)) - .first() - .copied() - .unwrap_or(event::Status::Ignored) - } - - /// Simulates a user typing in the keyboard the given text in the [`Simulator`]. - pub fn typewrite(&mut self, text: &str) -> event::Status { - let statuses = self.simulate(typewrite(text)); - - statuses - .into_iter() - .fold(event::Status::Ignored, event::Status::merge) - } - - /// Simulates the given raw sequence of events in the [`Simulator`]. - pub fn simulate( - &mut self, - events: impl IntoIterator, - ) -> Vec { - let events: Vec = events.into_iter().collect(); - - let (_state, statuses) = self.raw.update( - &events, - self.cursor, - &mut self.renderer, - &mut clipboard::Null, - &mut self.messages, - ); - - statuses - } - - /// Draws and takes a [`Snapshot`] of the interface in the [`Simulator`]. - pub fn snapshot(&mut self, theme: &Theme) -> Result { - let base = theme.base(); - - let _ = self.raw.update( - &[Event::Window(window::Event::RedrawRequested( - time::Instant::now(), - ))], - self.cursor, - &mut self.renderer, - &mut clipboard::Null, - &mut self.messages, - ); - - self.raw.draw( - &mut self.renderer, - theme, - &core::renderer::Style { - text_color: base.text_color, - }, - self.cursor, - ); - - let scale_factor = 2.0; - - let physical_size = Size::new( - (self.size.width * scale_factor).round() as u32, - (self.size.height * scale_factor).round() as u32, - ); - - let rgba = self.renderer.screenshot( - physical_size, - scale_factor, - base.background_color, - ); - - Ok(Snapshot { - screenshot: window::Screenshot::new( - rgba, - physical_size, - f64::from(scale_factor), - ), - renderer: self.renderer.name(), - }) - } - - /// Turns the [`Simulator`] into the sequence of messages produced by any interactions. - pub fn into_messages( - self, - ) -> impl Iterator + use { - self.messages.into_iter() - } -} - -/// A frame of a user interface rendered by a [`Simulator`]. #[derive(Debug, Clone)] -pub struct Snapshot { - screenshot: window::Screenshot, - renderer: String, -} - -impl Snapshot { - /// Compares the [`Snapshot`] with the PNG image found in the given path, returning - /// `true` if they are identical. - /// - /// If the PNG image does not exist, it will be created by the [`Snapshot`] for future - /// testing and `true` will be returned. - pub fn matches_image(&self, path: impl AsRef) -> Result { - let path = self.path(path, "png"); - - if path.exists() { - let file = fs::File::open(&path)?; - let decoder = png::Decoder::new(file); - - let mut reader = decoder.read_info()?; - let mut bytes = vec![0; reader.output_buffer_size()]; - let info = reader.next_frame(&mut bytes)?; - - Ok(self.screenshot.bytes == bytes[..info.buffer_size()]) - } else { - if let Some(directory) = path.parent() { - fs::create_dir_all(directory)?; - } - - let file = fs::File::create(path)?; - - let mut encoder = png::Encoder::new( - file, - self.screenshot.size.width, - self.screenshot.size.height, - ); - encoder.set_color(png::ColorType::Rgba); - - let mut writer = encoder.write_header()?; - writer.write_image_data(&self.screenshot.bytes)?; - writer.finish()?; - - Ok(true) - } - } - - /// Compares the [`Snapshot`] with the SHA-256 hash file found in the given path, returning - /// `true` if they are identical. - /// - /// If the hash file does not exist, it will be created by the [`Snapshot`] for future - /// testing and `true` will be returned. - pub fn matches_hash(&self, path: impl AsRef) -> Result { - use sha2::{Digest, Sha256}; - - let path = self.path(path, "sha256"); - - let hash = { - let mut hasher = Sha256::new(); - hasher.update(&self.screenshot.bytes); - format!("{:x}", hasher.finalize()) - }; - - if path.exists() { - let saved_hash = fs::read_to_string(&path)?; - - Ok(hash == saved_hash) - } else { - if let Some(directory) = path.parent() { - fs::create_dir_all(directory)?; - } - - fs::write(path, hash)?; - Ok(true) - } - } - - fn path(&self, path: impl AsRef, extension: &str) -> PathBuf { - let path = path.as_ref(); - - path.with_file_name(format!( - "{name}-{renderer}", - name = path - .file_stem() - .map(std::ffi::OsStr::to_string_lossy) - .unwrap_or_default(), - renderer = self.renderer - )) - .with_extension(extension) - } -} - -/// Returns the sequence of events of a click. -pub fn click() -> impl Iterator { - [ - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)), - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)), - ] - .into_iter() -} - -/// Returns the sequence of events of a "key tap" (i.e. pressing and releasing a key). -pub fn tap_key( - key: impl Into, - text: Option, -) -> impl Iterator { - let key = key.into(); - - [ - Event::Keyboard(keyboard::Event::KeyPressed { - key: key.clone(), - modified_key: key.clone(), - physical_key: keyboard::key::Physical::Unidentified( - keyboard::key::NativeCode::Unidentified, - ), - location: keyboard::Location::Standard, - modifiers: keyboard::Modifiers::default(), - text, - }), - Event::Keyboard(keyboard::Event::KeyReleased { - key: key.clone(), - modified_key: key, - physical_key: keyboard::key::Physical::Unidentified( - keyboard::key::NativeCode::Unidentified, - ), - location: keyboard::Location::Standard, - modifiers: keyboard::Modifiers::default(), - }), - ] - .into_iter() -} - -/// Returns the sequence of events of typewriting the given text in a keyboard. -pub fn typewrite(text: &str) -> impl Iterator + '_ { - text.chars() - .map(|c| SmolStr::new_inline(&c.to_string())) - .flat_map(|c| tap_key(keyboard::Key::Character(c.clone()), Some(c))) -} - -/// A test error. -#[derive(Debug, Clone, thiserror::Error)] -pub enum Error { - /// No matching widget was found for the [`Selector`]. - #[error("no matching widget was found for the selector: {0:?}")] - NotFound(Selector), - /// An IO operation failed. - #[error("an IO operation failed: {0}")] - IOFailed(Arc), - /// The decoding of some PNG image failed. - #[error("the decoding of some PNG image failed: {0}")] - PngDecodingFailed(Arc), - /// The encoding of some PNG image failed. - #[error("the encoding of some PNG image failed: {0}")] - PngEncodingFailed(Arc), -} - -impl From for Error { - fn from(error: io::Error) -> Self { - Self::IOFailed(Arc::new(error)) - } -} - -impl From for Error { - fn from(error: png::DecodingError) -> Self { - Self::PngDecodingFailed(Arc::new(error)) - } -} - -impl From for Error { - fn from(error: png::EncodingError) -> Self { - Self::PngEncodingFailed(Arc::new(error)) - } -} - -fn load_font(font: impl Into>) -> Result<(), Error> { - renderer::graphics::text::font_system() - .write() - .expect("Write to font system") - .load_font(font.into()); - - Ok(()) +pub struct Test { + instructions: Vec, } diff --git a/test/src/simulator.rs b/test/src/simulator.rs new file mode 100644 index 00000000..e638e9a0 --- /dev/null +++ b/test/src/simulator.rs @@ -0,0 +1,531 @@ +//! Run a simulation of your application. +use crate::core; +use crate::core::clipboard; +use crate::core::event; +use crate::core::keyboard; +use crate::core::mouse; +use crate::core::theme; +use crate::core::time; +use crate::core::widget; +use crate::core::window; +use crate::core::{ + Element, Event, Font, Point, Rectangle, Settings, Size, SmolStr, +}; +use crate::renderer; +use crate::runtime::UserInterface; +use crate::runtime::user_interface; +use crate::{Error, Selector}; + +use std::borrow::Cow; +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; + +/// A user interface that can be interacted with and inspected programmatically. +#[allow(missing_debug_implementations)] +pub struct Simulator< + 'a, + Message, + Theme = core::Theme, + Renderer = renderer::Renderer, +> { + raw: UserInterface<'a, Message, Theme, Renderer>, + renderer: Renderer, + size: Size, + cursor: mouse::Cursor, + messages: Vec, +} + +/// A specific area of a [`Simulator`], normally containing a widget. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Target { + /// The bounds of the area. + pub bounds: Rectangle, +} + +impl<'a, Message, Theme, Renderer> Simulator<'a, Message, Theme, Renderer> +where + Theme: theme::Base, + Renderer: core::Renderer + core::renderer::Headless, +{ + /// Creates a new [`Simulator`] with default [`Settings`] and a default size (1024x768). + pub fn new( + element: impl Into>, + ) -> Self { + Self::with_settings(Settings::default(), element) + } + + /// Creates a new [`Simulator`] with the given [`Settings`] and a default size (1024x768). + pub fn with_settings( + settings: Settings, + element: impl Into>, + ) -> Self { + Self::with_size(settings, window::Settings::default().size, element) + } + + /// Creates a new [`Simulator`] with the given [`Settings`] and size. + pub fn with_size( + settings: Settings, + size: impl Into, + element: impl Into>, + ) -> Self { + let size = size.into(); + + let default_font = match settings.default_font { + Font::DEFAULT => Font::with_name("Fira Sans"), + _ => settings.default_font, + }; + + for font in settings.fonts { + load_font(font).expect("Font must be valid"); + } + + let mut renderer = { + let backend = env::var("ICED_TEST_BACKEND").ok(); + + iced_runtime::futures::futures::executor::block_on(Renderer::new( + default_font, + settings.default_text_size, + backend.as_deref(), + )) + .expect("Create new headless renderer") + }; + + let raw = UserInterface::build( + element, + size, + user_interface::Cache::default(), + &mut renderer, + ); + + Simulator { + raw, + renderer, + size, + cursor: mouse::Cursor::Unavailable, + messages: Vec::new(), + } + } + + /// Finds the [`Target`] of the given widget [`Selector`] in the [`Simulator`]. + pub fn find( + &mut self, + selector: impl Into, + ) -> Result { + let selector = selector.into(); + + match &selector { + Selector::Id(id) => { + struct FindById<'a> { + id: &'a widget::Id, + target: Option, + } + + impl widget::Operation for FindById<'_> { + fn container( + &mut self, + id: Option<&widget::Id>, + bounds: Rectangle, + operate_on_children: &mut dyn FnMut( + &mut dyn widget::Operation<()>, + ), + ) { + if self.target.is_some() { + return; + } + + if Some(self.id) == id { + self.target = Some(Target { bounds }); + return; + } + + operate_on_children(self); + } + + fn scrollable( + &mut self, + id: Option<&widget::Id>, + bounds: Rectangle, + _content_bounds: Rectangle, + _translation: core::Vector, + _state: &mut dyn widget::operation::Scrollable, + ) { + if self.target.is_some() { + return; + } + + if Some(self.id) == id { + self.target = Some(Target { bounds }); + } + } + + fn text_input( + &mut self, + id: Option<&widget::Id>, + bounds: Rectangle, + _state: &mut dyn widget::operation::TextInput, + ) { + if self.target.is_some() { + return; + } + + if Some(self.id) == id { + self.target = Some(Target { bounds }); + } + } + + fn text( + &mut self, + id: Option<&widget::Id>, + bounds: Rectangle, + _text: &str, + ) { + if self.target.is_some() { + return; + } + + if Some(self.id) == id { + self.target = Some(Target { bounds }); + } + } + + fn custom( + &mut self, + id: Option<&widget::Id>, + bounds: Rectangle, + _state: &mut dyn std::any::Any, + ) { + if self.target.is_some() { + return; + } + + if Some(self.id) == id { + self.target = Some(Target { bounds }); + } + } + } + + let mut find = FindById { id, target: None }; + self.raw.operate(&self.renderer, &mut find); + + find.target.ok_or(Error::NotFound(selector)) + } + Selector::Text(text) => { + struct FindByText<'a> { + text: &'a str, + target: Option, + } + + impl widget::Operation for FindByText<'_> { + fn container( + &mut self, + _id: Option<&widget::Id>, + _bounds: Rectangle, + operate_on_children: &mut dyn FnMut( + &mut dyn widget::Operation<()>, + ), + ) { + if self.target.is_some() { + return; + } + + operate_on_children(self); + } + + fn text( + &mut self, + _id: Option<&widget::Id>, + bounds: Rectangle, + text: &str, + ) { + if self.target.is_some() { + return; + } + + if self.text == text { + self.target = Some(Target { bounds }); + } + } + } + + let mut find = FindByText { text, target: None }; + self.raw.operate(&self.renderer, &mut find); + + find.target.ok_or(Error::NotFound(selector)) + } + } + } + + /// Points the mouse cursor at the given position in the [`Simulator`]. + /// + /// This does _not_ produce mouse movement events! + pub fn point_at(&mut self, position: impl Into) { + self.cursor = mouse::Cursor::Available(position.into()); + } + + /// Clicks the [`Target`] found by the given [`Selector`], if any. + /// + /// This consists in: + /// - Pointing the mouse cursor at the center of the [`Target`]. + /// - Simulating a [`click`]. + pub fn click( + &mut self, + selector: impl Into, + ) -> Result { + let target = self.find(selector)?; + self.point_at(target.bounds.center()); + + let _ = self.simulate(click()); + + Ok(target) + } + + /// Simulates a key press, followed by a release, in the [`Simulator`]. + pub fn tap_key(&mut self, key: impl Into) -> event::Status { + self.simulate(tap_key(key, None)) + .first() + .copied() + .unwrap_or(event::Status::Ignored) + } + + /// Simulates a user typing in the keyboard the given text in the [`Simulator`]. + pub fn typewrite(&mut self, text: &str) -> event::Status { + let statuses = self.simulate(typewrite(text)); + + statuses + .into_iter() + .fold(event::Status::Ignored, event::Status::merge) + } + + /// Simulates the given raw sequence of events in the [`Simulator`]. + pub fn simulate( + &mut self, + events: impl IntoIterator, + ) -> Vec { + let events: Vec = events.into_iter().collect(); + + let (_state, statuses) = self.raw.update( + &events, + self.cursor, + &mut self.renderer, + &mut clipboard::Null, + &mut self.messages, + ); + + statuses + } + + /// Draws and takes a [`Snapshot`] of the interface in the [`Simulator`]. + pub fn snapshot(&mut self, theme: &Theme) -> Result { + let base = theme.base(); + + let _ = self.raw.update( + &[Event::Window(window::Event::RedrawRequested( + time::Instant::now(), + ))], + self.cursor, + &mut self.renderer, + &mut clipboard::Null, + &mut self.messages, + ); + + self.raw.draw( + &mut self.renderer, + theme, + &core::renderer::Style { + text_color: base.text_color, + }, + self.cursor, + ); + + let scale_factor = 2.0; + + let physical_size = Size::new( + (self.size.width * scale_factor).round() as u32, + (self.size.height * scale_factor).round() as u32, + ); + + let rgba = self.renderer.screenshot( + physical_size, + scale_factor, + base.background_color, + ); + + Ok(Snapshot { + screenshot: window::Screenshot::new( + rgba, + physical_size, + f64::from(scale_factor), + ), + renderer: self.renderer.name(), + }) + } + + /// Turns the [`Simulator`] into the sequence of messages produced by any interactions. + pub fn into_messages( + self, + ) -> impl Iterator + use { + self.messages.into_iter() + } +} + +/// A frame of a user interface rendered by a [`Simulator`]. +#[derive(Debug, Clone)] +pub struct Snapshot { + screenshot: window::Screenshot, + renderer: String, +} + +impl Snapshot { + /// Compares the [`Snapshot`] with the PNG image found in the given path, returning + /// `true` if they are identical. + /// + /// If the PNG image does not exist, it will be created by the [`Snapshot`] for future + /// testing and `true` will be returned. + pub fn matches_image(&self, path: impl AsRef) -> Result { + let path = self.path(path, "png"); + + if path.exists() { + let file = fs::File::open(&path)?; + let decoder = png::Decoder::new(file); + + let mut reader = decoder.read_info()?; + let mut bytes = vec![0; reader.output_buffer_size()]; + let info = reader.next_frame(&mut bytes)?; + + Ok(self.screenshot.bytes == bytes[..info.buffer_size()]) + } else { + if let Some(directory) = path.parent() { + fs::create_dir_all(directory)?; + } + + let file = fs::File::create(path)?; + + let mut encoder = png::Encoder::new( + file, + self.screenshot.size.width, + self.screenshot.size.height, + ); + encoder.set_color(png::ColorType::Rgba); + + let mut writer = encoder.write_header()?; + writer.write_image_data(&self.screenshot.bytes)?; + writer.finish()?; + + Ok(true) + } + } + + /// Compares the [`Snapshot`] with the SHA-256 hash file found in the given path, returning + /// `true` if they are identical. + /// + /// If the hash file does not exist, it will be created by the [`Snapshot`] for future + /// testing and `true` will be returned. + pub fn matches_hash(&self, path: impl AsRef) -> Result { + use sha2::{Digest, Sha256}; + + let path = self.path(path, "sha256"); + + let hash = { + let mut hasher = Sha256::new(); + hasher.update(&self.screenshot.bytes); + format!("{:x}", hasher.finalize()) + }; + + if path.exists() { + let saved_hash = fs::read_to_string(&path)?; + + Ok(hash == saved_hash) + } else { + if let Some(directory) = path.parent() { + fs::create_dir_all(directory)?; + } + + fs::write(path, hash)?; + Ok(true) + } + } + + fn path(&self, path: impl AsRef, extension: &str) -> PathBuf { + let path = path.as_ref(); + + path.with_file_name(format!( + "{name}-{renderer}", + name = path + .file_stem() + .map(std::ffi::OsStr::to_string_lossy) + .unwrap_or_default(), + renderer = self.renderer + )) + .with_extension(extension) + } +} + +/// Creates a new [`Simulator`]. +/// +/// This is just a function version of [`Simulator::new`]. +pub fn simulator<'a, Message, Theme, Renderer>( + element: impl Into>, +) -> Simulator<'a, Message, Theme, Renderer> +where + Theme: theme::Base, + Renderer: core::Renderer + core::renderer::Headless, +{ + Simulator::new(element) +} + +/// Returns the sequence of events of a click. +pub fn click() -> impl Iterator { + [ + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)), + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)), + ] + .into_iter() +} + +/// Returns the sequence of events of a "key tap" (i.e. pressing and releasing a key). +pub fn tap_key( + key: impl Into, + text: Option, +) -> impl Iterator { + let key = key.into(); + + [ + Event::Keyboard(keyboard::Event::KeyPressed { + key: key.clone(), + modified_key: key.clone(), + physical_key: keyboard::key::Physical::Unidentified( + keyboard::key::NativeCode::Unidentified, + ), + location: keyboard::Location::Standard, + modifiers: keyboard::Modifiers::default(), + text, + }), + Event::Keyboard(keyboard::Event::KeyReleased { + key: key.clone(), + modified_key: key, + physical_key: keyboard::key::Physical::Unidentified( + keyboard::key::NativeCode::Unidentified, + ), + location: keyboard::Location::Standard, + modifiers: keyboard::Modifiers::default(), + }), + ] + .into_iter() +} + +/// Returns the sequence of events of typewriting the given text in a keyboard. +pub fn typewrite(text: &str) -> impl Iterator + '_ { + text.chars() + .map(|c| SmolStr::new_inline(&c.to_string())) + .flat_map(|c| tap_key(keyboard::Key::Character(c.clone()), Some(c))) +} + +fn load_font(font: impl Into>) -> Result<(), Error> { + renderer::graphics::text::font_system() + .write() + .expect("Write to font system") + .load_font(font.into()); + + Ok(()) +} diff --git a/widget/src/container.rs b/widget/src/container.rs index 4f6725b1..73b2502a 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -698,6 +698,7 @@ pub fn rounded_box(theme: &Theme) -> Style { Style { background: Some(palette.background.weak.color.into()), + text_color: Some(palette.background.weak.text), border: border::rounded(2), ..Style::default() } @@ -709,6 +710,7 @@ pub fn bordered_box(theme: &Theme) -> Style { Style { background: Some(palette.background.weakest.color.into()), + text_color: Some(palette.background.weakest.text), border: Border { width: 1.0, radius: 5.0.into(), diff --git a/widget/src/themer.rs b/widget/src/themer.rs index 1b9e1c6a..43de86f4 100644 --- a/widget/src/themer.rs +++ b/widget/src/themer.rs @@ -206,13 +206,9 @@ where layout: Layout<'_>, cursor: mouse::Cursor, ) { - self.content.as_overlay().draw( - renderer, - &self.theme, - style, - layout, - cursor, - ); + self.content + .as_overlay() + .draw(renderer, self.theme, style, layout, cursor); } fn update( From 16556b51bc712ca4ebc61dca89d412c49562744e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 31 May 2025 04:34:54 +0200 Subject: [PATCH 03/83] Draft `Emulator` in `iced_test` --- Cargo.lock | 1 + devtools/src/lib.rs | 4 + devtools/src/widget/recorder.rs | 3 +- program/src/lib.rs | 40 +++++++- src/application.rs | 73 +++++++++++++- src/application/timed.rs | 4 + src/daemon.rs | 72 +++++++++++++- test/Cargo.toml | 1 + test/src/emulator.rs | 169 ++++++++++++++++++++++++++++++++ test/src/instruction.rs | 84 ++++++++++++++-- test/src/lib.rs | 7 +- test/src/simulator.rs | 55 ++++++----- 12 files changed, 466 insertions(+), 47 deletions(-) create mode 100644 test/src/emulator.rs diff --git a/Cargo.lock b/Cargo.lock index 9d7a5ed3..dd3236ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2654,6 +2654,7 @@ dependencies = [ name = "iced_test" version = "0.14.0-dev" dependencies = [ + "iced_program", "iced_renderer", "iced_runtime", "nom 8.0.0", diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index fc6d1cdb..e8dcfa71 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -60,6 +60,10 @@ where P::name() } + fn settings(&self) -> core::Settings { + self.program.settings() + } + fn boot(&self) -> (Self::State, Task) { let (state, boot) = self.program.boot(); let (devtools, task) = DevTools::new(state); diff --git a/devtools/src/widget/recorder.rs b/devtools/src/widget/recorder.rs index 3fd88436..bae43478 100644 --- a/devtools/src/widget/recorder.rs +++ b/devtools/src/widget/recorder.rs @@ -68,8 +68,7 @@ where mouse::Event::ButtonPressed(_) | mouse::Event::ButtonReleased(_) | mouse::Event::WheelScrolled { .. } => { - shell - .publish(on_event(Event::Mouse(event.clone()))); + shell.publish(on_event(Event::Mouse(*event))); } mouse::Event::CursorMoved { position } => { shell.publish(on_event(Event::Mouse( diff --git a/program/src/lib.rs b/program/src/lib.rs index 80e59330..c75b02a1 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -4,10 +4,11 @@ pub use iced_runtime as runtime; pub use iced_runtime::core; pub use iced_runtime::futures; +use crate::core::renderer; use crate::core::text; use crate::core::theme; use crate::core::window; -use crate::core::{Element, Font}; +use crate::core::{Element, Font, Settings}; use crate::futures::{Executor, Subscription}; use crate::graphics::compositor; use crate::runtime::Task; @@ -36,6 +37,8 @@ pub trait Program: Sized { /// Returns the unique name of the [`Program`]. fn name() -> &'static str; + fn settings(&self) -> Settings; + fn boot(&self) -> (Self::State, Task); fn update( @@ -128,6 +131,10 @@ pub fn with_title( P::name() } + fn settings(&self) -> Settings { + self.program.settings() + } + fn boot(&self) -> (Self::State, Task) { self.program.boot() } @@ -210,6 +217,10 @@ pub fn with_subscription( P::name() } + fn settings(&self) -> Settings { + self.program.settings() + } + fn boot(&self) -> (Self::State, Task) { self.program.boot() } @@ -293,6 +304,10 @@ pub fn with_theme( P::name() } + fn settings(&self) -> Settings { + self.program.settings() + } + fn boot(&self) -> (Self::State, Task) { self.program.boot() } @@ -372,6 +387,10 @@ pub fn with_style( P::name() } + fn settings(&self) -> Settings { + self.program.settings() + } + fn boot(&self) -> (Self::State, Task) { self.program.boot() } @@ -447,6 +466,10 @@ pub fn with_scale_factor( P::name() } + fn settings(&self) -> Settings { + self.program.settings() + } + fn boot(&self) -> (Self::State, Task) { self.program.boot() } @@ -530,6 +553,10 @@ pub fn with_executor( P::name() } + fn settings(&self) -> Settings { + self.program.settings() + } + fn boot(&self) -> (Self::State, Task) { self.program.boot() } @@ -585,10 +612,15 @@ pub fn with_executor( } /// The renderer of some [`Program`]. -pub trait Renderer: text::Renderer + compositor::Default {} +pub trait Renderer: + text::Renderer + compositor::Default + renderer::Headless +{ +} -impl Renderer for T where T: text::Renderer + compositor::Default -{} +impl Renderer for T where + T: text::Renderer + compositor::Default + renderer::Headless +{ +} /// A particular instance of a running [`Program`]. #[allow(missing_debug_implementations)] diff --git a/src/application.rs b/src/application.rs index e87d89a2..8c8917fc 100644 --- a/src/application.rs +++ b/src/application.rs @@ -136,6 +136,10 @@ where ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { self.view.view(state) } + + fn settings(&self) -> Settings { + Settings::default() + } } Application { @@ -173,6 +177,9 @@ impl Application

{ where Self: 'static, { + let settings = self.settings.clone(); + let window = self.window.clone(); + #[cfg(all(feature = "debug", not(target_arch = "wasm32")))] let program = { iced_debug::init(iced_debug::Metadata { @@ -181,13 +188,13 @@ impl Application

{ can_time_travel: cfg!(feature = "time-travel"), }); - iced_devtools::attach(self.raw) + iced_devtools::attach(self) }; #[cfg(any(not(feature = "debug"), target_arch = "wasm32"))] - let program = self.raw; + let program = self; - Ok(shell::run(program, self.settings, Some(self.window))?) + Ok(shell::run(program, settings, Some(window))?) } /// Sets the [`Settings`] that will be used to run the [`Application`]. @@ -409,6 +416,66 @@ impl Application

{ } } +impl Program for Application

{ + type State = P::State; + type Message = P::Message; + type Theme = P::Theme; + type Renderer = P::Renderer; + type Executor = P::Executor; + + fn name() -> &'static str { + P::name() + } + + fn settings(&self) -> Settings { + self.settings.clone() + } + + fn boot(&self) -> (Self::State, Task) { + self.raw.boot() + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Task { + self.raw.update(state, message) + } + + fn view<'a>( + &self, + state: &'a Self::State, + window: window::Id, + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + self.raw.view(state, window) + } + + fn title(&self, state: &Self::State, window: window::Id) -> String { + self.raw.title(state, window) + } + + fn subscription(&self, state: &Self::State) -> Subscription { + self.raw.subscription(state) + } + + fn theme( + &self, + state: &Self::State, + window: iced_core::window::Id, + ) -> Self::Theme { + self.raw.theme(state, window) + } + + fn style(&self, state: &Self::State, theme: &Self::Theme) -> theme::Style { + self.raw.style(state, theme) + } + + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { + self.raw.scale_factor(state, window) + } +} + /// The logic to initialize the `State` of some [`Application`]. /// /// This trait is implemented for both `Fn() -> State` and diff --git a/src/application/timed.rs b/src/application/timed.rs index 606273c8..5ca45ec4 100644 --- a/src/application/timed.rs +++ b/src/application/timed.rs @@ -86,6 +86,10 @@ where name.split("::").next().unwrap_or("a_cool_application") } + fn settings(&self) -> Settings { + Settings::default() + } + fn boot(&self) -> (State, Task) { let (state, task) = self.boot.boot(); diff --git a/src/daemon.rs b/src/daemon.rs index 1b99da30..b1f6e5ab 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -63,6 +63,10 @@ where name.split("::").next().unwrap_or("a_cool_daemon") } + fn settings(&self) -> Settings { + Settings::default() + } + fn boot(&self) -> (Self::State, Task) { self.boot.boot() } @@ -117,6 +121,8 @@ impl Daemon

{ where Self: 'static, { + let settings = self.settings.clone(); + #[cfg(all(feature = "debug", not(target_arch = "wasm32")))] let program = { iced_debug::init(iced_debug::Metadata { @@ -125,13 +131,13 @@ impl Daemon

{ can_time_travel: cfg!(feature = "time-travel"), }); - iced_devtools::attach(self.raw) + iced_devtools::attach(self) }; #[cfg(any(not(feature = "debug"), target_arch = "wasm32"))] - let program = self.raw; + let program = self; - Ok(shell::run(program, self.settings, None)?) + Ok(shell::run(program, settings, None)?) } /// Sets the [`Settings`] that will be used to run the [`Daemon`]. @@ -250,6 +256,66 @@ impl Daemon

{ } } +impl Program for Daemon

{ + type State = P::State; + type Message = P::Message; + type Theme = P::Theme; + type Renderer = P::Renderer; + type Executor = P::Executor; + + fn name() -> &'static str { + P::name() + } + + fn settings(&self) -> Settings { + self.settings.clone() + } + + fn boot(&self) -> (Self::State, Task) { + self.raw.boot() + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Task { + self.raw.update(state, message) + } + + fn view<'a>( + &self, + state: &'a Self::State, + window: window::Id, + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + self.raw.view(state, window) + } + + fn title(&self, state: &Self::State, window: window::Id) -> String { + self.raw.title(state, window) + } + + fn subscription(&self, state: &Self::State) -> Subscription { + self.raw.subscription(state) + } + + fn theme( + &self, + state: &Self::State, + window: iced_core::window::Id, + ) -> Self::Theme { + self.raw.theme(state, window) + } + + fn style(&self, state: &Self::State, theme: &Self::Theme) -> theme::Style { + self.raw.style(state, theme) + } + + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { + self.raw.scale_factor(state, window) + } +} + /// The title logic of some [`Daemon`]. /// /// This trait is implemented both for `&static str` and diff --git a/test/Cargo.toml b/test/Cargo.toml index af5795ed..132a913b 100644 --- a/test/Cargo.toml +++ b/test/Cargo.toml @@ -15,6 +15,7 @@ workspace = true [dependencies] iced_runtime.workspace = true +iced_program.workspace = true iced_renderer.workspace = true iced_renderer.features = ["fira-sans"] diff --git a/test/src/emulator.rs b/test/src/emulator.rs new file mode 100644 index 00000000..9b2d2549 --- /dev/null +++ b/test/src/emulator.rs @@ -0,0 +1,169 @@ +use crate::Instruction; +use crate::core; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::window; +use crate::core::{Element, Size}; +use crate::program::Program; +use crate::runtime::futures::futures::StreamExt; +use crate::runtime::futures::futures::channel::mpsc; +use crate::runtime::futures::{Executor, Runtime}; +use crate::runtime::task; +use crate::runtime::user_interface; +use crate::runtime::{Action, UserInterface}; + +#[allow(missing_debug_implementations)] +pub struct Emulator { + state: P::State, + runtime: Runtime>, Event

>, + renderer: P::Renderer, + size: Size, + window: window::Id, + cursor: mouse::Cursor, + clipboard: Clipboard, + cache: Option, +} + +#[allow(missing_debug_implementations)] +pub enum Event { + Action(Action), +} + +impl Emulator

{ + pub fn new( + program: &P, + size: Size, + sender: mpsc::Sender>, + ) -> Emulator

{ + use renderer::Headless; + + let settings = program.settings(); + + // TODO: Error handling + let executor = P::Executor::new().expect("Create emulator executor"); + + let renderer = executor + .block_on(P::Renderer::new( + settings.default_font, + settings.default_text_size, + None, + )) + .expect("Create emulator renderer"); + + let mut runtime = Runtime::new(executor, sender); + + let (state, task) = program.boot(); + + if let Some(stream) = task::into_stream(task) { + runtime.run(stream.map(Event::Action).boxed()); + } + + Self { + state, + runtime, + renderer, + size, + clipboard: Clipboard { content: None }, + cursor: mouse::Cursor::Unavailable, + window: window::Id::unique(), + cache: Some(user_interface::Cache::default()), + } + } + + pub fn update(&mut self, program: &P, message: P::Message) { + let task = program.update(&mut self.state, message); + + if let Some(stream) = task::into_stream(task) { + self.runtime.run(stream.map(Event::Action).boxed()); + } + } + + pub fn perform(&mut self, program: &P, action: Action) { + match action { + Action::Output(message) => { + self.update(program, message); + } + Action::LoadFont { .. } => { + // TODO + } + Action::Widget(_operation) => { + // TODO + } + Action::Clipboard(action) => { + // TODO + dbg!(action); + } + Action::Window(_action) => { + // TODO + } + Action::System(action) => { + // TODO + dbg!(action); + } + Action::Exit => { + // TODO + } + } + } + + pub fn run(&mut self, program: &P, instruction: Instruction) { + let mut user_interface = UserInterface::build( + program.view(&self.state, self.window), + self.size, + self.cache.take().unwrap(), + &mut self.renderer, + ); + + let mut messages = Vec::new(); + + match instruction { + Instruction::Interact(interaction) => { + let events = interaction.events(); + + for event in &events { + if let core::Event::Mouse(mouse::Event::CursorMoved { + position, + }) = event + { + self.cursor = mouse::Cursor::Available(*position); + } + } + + let (_state, _status) = user_interface.update( + &events, + self.cursor, + &mut self.renderer, + &mut self.clipboard, + &mut messages, + ); + } + } + + self.cache = Some(user_interface.into_cache()); + + for message in messages { + self.update(program, message); + } + } + + pub fn view( + &self, + program: &P, + ) -> Element<'_, P::Message, P::Theme, P::Renderer> { + program.view(&self.state, self.window) + } +} + +struct Clipboard { + content: Option, +} + +impl core::Clipboard for Clipboard { + fn read(&self, _kind: core::clipboard::Kind) -> Option { + self.content.clone() + } + + fn write(&mut self, _kind: core::clipboard::Kind, contents: String) { + self.content = Some(contents); + } +} diff --git a/test/src/instruction.rs b/test/src/instruction.rs index 1f8455f8..d1323bcf 100644 --- a/test/src/instruction.rs +++ b/test/src/instruction.rs @@ -1,6 +1,7 @@ use crate::core::keyboard; use crate::core::mouse; use crate::core::{Event, Point}; +use crate::simulator; use std::fmt; @@ -145,6 +146,62 @@ impl Interaction { (current, next) => (current, Some(next)), } } + + pub fn events(&self) -> Vec { + let mouse_move_ = + |to| Event::Mouse(mouse::Event::CursorMoved { position: to }); + + let mouse_press = + |button| Event::Mouse(mouse::Event::ButtonPressed(button)); + + let mouse_release = + |button| Event::Mouse(mouse::Event::ButtonReleased(button)); + + let key_press = |key| simulator::press_key(key, None); + + let key_release = |key| simulator::release_key(key); + + match self { + Interaction::Mouse(mouse) => match mouse { + Mouse::Move(to) => vec![mouse_move_(*to)], + Mouse::Press { + button, + at: Some(at), + } => vec![mouse_move_(*at), mouse_press(*button)], + Mouse::Press { button, at: None } => { + vec![mouse_press(*button)] + } + Mouse::Release { + button, + at: Some(at), + } => vec![mouse_move_(*at), mouse_release(*button)], + Mouse::Release { button, at: None } => { + vec![mouse_release(*button)] + } + Mouse::Click { + button, + at: Some(at), + } => { + vec![ + mouse_move_(*at), + mouse_press(*button), + mouse_release(*button), + ] + } + Mouse::Click { button, at: None } => { + vec![mouse_press(*button), mouse_release(*button)] + } + }, + Interaction::Keyboard(keyboard) => match keyboard { + Keyboard::Press(key) => vec![key_press(*key)], + Keyboard::Release(key) => vec![key_release(*key)], + Keyboard::Type(key) => vec![key_press(*key), key_release(*key)], + Keyboard::Typewrite(text) => { + simulator::typewrite(text).collect() + } + }, + } + } } impl fmt::Display for Interaction { @@ -200,14 +257,6 @@ pub enum Keyboard { Typewrite(String), } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Key { - Enter, - Escape, - Tab, - Backspace, -} - impl fmt::Display for Keyboard { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -227,6 +276,25 @@ impl fmt::Display for Keyboard { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Key { + Enter, + Escape, + Tab, + Backspace, +} + +impl From for keyboard::Key { + fn from(key: Key) -> Self { + match key { + Key::Enter => Self::Named(keyboard::key::Named::Enter), + Key::Escape => Self::Named(keyboard::key::Named::Escape), + Key::Tab => Self::Named(keyboard::key::Named::Tab), + Key::Backspace => Self::Named(keyboard::key::Named::Backspace), + } + } +} + mod format { use super::*; diff --git a/test/src/lib.rs b/test/src/lib.rs index 4ec0d8dd..1aa69334 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -84,10 +84,12 @@ //! //! [the classical counter interface]: https://book.iced.rs/architecture.html#dissecting-an-interface #![allow(missing_docs)] +use iced_program as program; use iced_renderer as renderer; use iced_runtime as runtime; use iced_runtime::core; +pub mod emulator; pub mod instruction; pub mod selector; pub mod simulator; @@ -98,8 +100,3 @@ pub use error::Error; pub use instruction::Instruction; pub use selector::Selector; pub use simulator::{Simulator, simulator}; - -#[derive(Debug, Clone)] -pub struct Test { - instructions: Vec, -} diff --git a/test/src/simulator.rs b/test/src/simulator.rs index e638e9a0..cdff5040 100644 --- a/test/src/simulator.rs +++ b/test/src/simulator.rs @@ -483,6 +483,38 @@ pub fn click() -> impl Iterator { .into_iter() } +pub fn press_key( + key: impl Into, + text: Option, +) -> Event { + let key = key.into(); + + Event::Keyboard(keyboard::Event::KeyPressed { + key: key.clone(), + modified_key: key, + physical_key: keyboard::key::Physical::Unidentified( + keyboard::key::NativeCode::Unidentified, + ), + location: keyboard::Location::Standard, + modifiers: keyboard::Modifiers::default(), + text, + }) +} + +pub fn release_key(key: impl Into) -> Event { + let key = key.into(); + + Event::Keyboard(keyboard::Event::KeyReleased { + key: key.clone(), + modified_key: key, + physical_key: keyboard::key::Physical::Unidentified( + keyboard::key::NativeCode::Unidentified, + ), + location: keyboard::Location::Standard, + modifiers: keyboard::Modifiers::default(), + }) +} + /// Returns the sequence of events of a "key tap" (i.e. pressing and releasing a key). pub fn tap_key( key: impl Into, @@ -490,28 +522,7 @@ pub fn tap_key( ) -> impl Iterator { let key = key.into(); - [ - Event::Keyboard(keyboard::Event::KeyPressed { - key: key.clone(), - modified_key: key.clone(), - physical_key: keyboard::key::Physical::Unidentified( - keyboard::key::NativeCode::Unidentified, - ), - location: keyboard::Location::Standard, - modifiers: keyboard::Modifiers::default(), - text, - }), - Event::Keyboard(keyboard::Event::KeyReleased { - key: key.clone(), - modified_key: key, - physical_key: keyboard::key::Physical::Unidentified( - keyboard::key::NativeCode::Unidentified, - ), - location: keyboard::Location::Standard, - modifiers: keyboard::Modifiers::default(), - }), - ] - .into_iter() + [press_key(key.clone(), text), release_key(key)].into_iter() } /// Returns the sequence of events of typewriting the given text in a keyboard. From ed528c9c5394bef9c344f967ec72d59d7137d6ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 31 May 2025 05:50:25 +0200 Subject: [PATCH 04/83] Plug `Emulator` into `devtools` :tada: --- devtools/src/lib.rs | 172 +++++++++++++++++++++++++++++++++-------- futures/src/runtime.rs | 11 ++- test/src/emulator.rs | 6 ++ test/src/lib.rs | 1 + 4 files changed, 158 insertions(+), 32 deletions(-) diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index e8dcfa71..d516d075 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -25,6 +25,8 @@ use crate::futures::Subscription; use crate::program::Program; use crate::runtime::Task; use crate::runtime::font; +use crate::test::Emulator; +use crate::test::emulator; use crate::test::instruction; use crate::time_machine::TimeMachine; use crate::widget::{ @@ -126,7 +128,7 @@ where { state: P::State, size: Size, - mode: Mode, + mode: Mode

, show_notification: bool, time_machine: TimeMachine

, } @@ -145,17 +147,27 @@ pub enum Message { Record, Stop, Recorded(core::Event), + Play, } -enum Mode { +enum Mode { Hidden, - Open { recorder: Recorder }, + Open { recorder: Recorder

}, Setup(Setup), } -struct Recorder { +struct Recorder { instructions: Vec, - is_recording: bool, + state: State

, +} + +enum State { + Idle, + Recording, + Playing { + emulator: Emulator

, + current: usize, + }, } enum Setup { @@ -207,11 +219,16 @@ where self.mode = Mode::Open { recorder: Recorder { instructions: Vec::new(), - is_recording: false, + state: State::Idle, }, }; } - Mode::Open { recorder } if !recorder.is_recording => { + Mode::Open { + recorder: + Recorder { + state: State::Idle, .. + }, + } => { self.mode = Mode::Hidden; } Mode::Setup(_) | Mode::Open { .. } => {} @@ -321,7 +338,7 @@ where }; recorder.instructions.clear(); - recorder.is_recording = true; + recorder.state = State::Recording; let (state, task) = program.boot(); self.state = state; @@ -367,10 +384,27 @@ where return Task::none(); }; - recorder.is_recording = false; + recorder.state = State::Idle; Task::none() } + Message::Play => { + let Mode::Open { recorder } = &mut self.mode else { + return Task::none(); + }; + + let (sender, receiver) = + futures::futures::channel::mpsc::channel(1); + + let emulator = Emulator::new(program, self.size, sender); + + recorder.state = State::Playing { + emulator, + current: 0, + }; + + Task::run(receiver, Event::Emulator) + } }, Event::Program(message) => { self.time_machine.push(&message); @@ -402,6 +436,34 @@ where Task::none() } + Event::Emulator(event) => { + let Mode::Open { + recorder: + Recorder { + state: State::Playing { emulator, current }, + instructions, + }, + } = &mut self.mode + else { + return Task::none(); + }; + + match event { + emulator::Event::Action(action) => { + emulator.perform(program, action); + } + emulator::Event::Ready => { + if let Some(instruction) = + instructions.get(*current).cloned() + { + emulator.run(program, instruction); + *current += 1; + } + } + } + + Task::none() + } Event::Discard => Task::none(), } } @@ -414,7 +476,17 @@ where let state = self.state(); let view = { - let view = program.view(state, window); + let view = match &self.mode { + Mode::Open { + recorder: + Recorder { + state: State::Playing { emulator, .. }, + .. + }, + } => emulator.view(program), + _ => program.view(state, window), + }; + let theme = program.theme(state, window); let view: Element<'_, _, Theme, _> = themer(theme, view).into(); @@ -477,10 +549,34 @@ where )) } else { scrollable( - column(recorder.instructions.iter().map( - |instruction| { + column(recorder.instructions.iter().enumerate().map( + |(i, instruction)| { monospace(instruction.to_string()) .size(10) + .style(move |theme: &Theme| text::Style { + color: match &recorder.state { + State::Playing { + current, .. + } => { + if *current == i { + Some( + theme.palette().primary, + ) + } else if *current > i { + Some( + theme + .extended_palette() + .success + .strong + .color, + ) + } else { + None + } + } + _ => None, + }, + }) .into() }, )) @@ -491,19 +587,26 @@ where }) .width(Fill) .height(Fill) - .style(container::rounded_box) .padding(5); let controls = { row![ - button(icon::play().size(14).width(Fill).center()), - if recorder.is_recording { + button(icon::play().size(14).width(Fill).center()) + .on_press_maybe( + (!matches!(recorder.state, State::Recording) + && !recorder.instructions.is_empty()) + .then_some(Message::Play), + ), + if let State::Recording = &recorder.state { button(icon::stop().size(14).width(Fill).center()) .on_press(Message::Stop) .style(button::success) } else { button(icon::record().size(14).width(Fill).center()) - .on_press(Message::Record) + .on_press_maybe( + matches!(recorder.state, State::Idle) + .then_some(Message::Record), + ) .style(button::danger) } ] @@ -544,23 +647,27 @@ where }; let content = row![if let Mode::Open { recorder } = &self.mode { - let is_recording = recorder.is_recording; - - let status = if is_recording { - monospace("Recording").style(|theme| text::Style { - color: Some(theme.palette().danger), - }) - } else { - monospace("Idle").style(|theme| text::Style { + let status = match &recorder.state { + State::Idle => monospace("Idle").style(|theme| text::Style { color: Some( theme.extended_palette().background.strongest.color, ), - }) + }), + State::Recording => { + monospace("Recording").style(|theme| text::Style { + color: Some(theme.palette().danger), + }) + } + State::Playing { .. } => { + monospace("Playing").style(|theme| text::Style { + color: Some(theme.palette().primary), + }) + } }; let viewport = container( scrollable( - container(if recorder.is_recording { + container(if let State::Recording = &recorder.state { widget::recorder(view) .on_event(|event| { Event::Message(Message::Recorded(event)) @@ -577,14 +684,14 @@ where horizontal: scrollable::Scrollbar::default(), }), ) - .style(move |theme| { + .style(|theme| { let palette = theme.extended_palette(); container::Style { - border: border::width(2.0).color(if is_recording { - palette.danger.base.color - } else { - palette.background.strongest.color + border: border::width(2.0).color(match &recorder.state { + State::Idle => palette.background.strongest.color, + State::Recording => palette.danger.base.color, + State::Playing { .. } => palette.primary.base.color, }), ..container::Style::default() } @@ -654,6 +761,7 @@ where { Message(Message), Program(P::Message), + Emulator(emulator::Event

), Command(debug::Command), Discard, } @@ -666,6 +774,7 @@ where match self { Self::Message(message) => message.fmt(f), Self::Program(message) => message.fmt(f), + Self::Emulator(_) => f.write_str("Emulator"), Self::Command(command) => command.fmt(f), Self::Discard => f.write_str("Discard"), } @@ -682,6 +791,7 @@ where Self::Message(message) => Self::Message(message.clone()), Self::Program(message) => Self::Program(message.clone()), Self::Command(command) => Self::Command(*command), + Self::Emulator(_) => Self::Discard, // Time traveling an emulator?! Self::Discard => Self::Discard, } } diff --git a/futures/src/runtime.rs b/futures/src/runtime.rs index e25ba1d7..927b35db 100644 --- a/futures/src/runtime.rs +++ b/futures/src/runtime.rs @@ -2,7 +2,7 @@ use crate::subscription; use crate::{BoxStream, Executor, MaybeSend}; -use futures::{Sink, channel::mpsc}; +use futures::{Sink, SinkExt, channel::mpsc}; use std::marker::PhantomData; /// A batteries-included runtime of commands and subscriptions. @@ -79,6 +79,15 @@ where self.executor.spawn(future); } + /// Sends a message concurrently through the [`Runtime`]. + pub fn send(&mut self, message: Message) { + let mut sender = self.sender.clone(); + + self.executor.spawn(async move { + let _ = sender.send(message).await; + }); + } + /// Tracks a [`Subscription`] in the [`Runtime`]. /// /// It will spawn new streams or close old ones as necessary! See diff --git a/test/src/emulator.rs b/test/src/emulator.rs index 9b2d2549..c76301b5 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -27,6 +27,7 @@ pub struct Emulator { #[allow(missing_debug_implementations)] pub enum Event { Action(Action), + Ready, } impl Emulator

{ @@ -58,6 +59,9 @@ impl Emulator

{ runtime.run(stream.map(Event::Action).boxed()); } + // TODO: Async boot environments + runtime.send(Event::Ready); + Self { state, runtime, @@ -144,6 +148,8 @@ impl Emulator

{ for message in messages { self.update(program, message); } + + self.runtime.send(Event::Ready); } pub fn view( diff --git a/test/src/lib.rs b/test/src/lib.rs index 1aa69334..bdf06df1 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -96,6 +96,7 @@ pub mod simulator; mod error; +pub use emulator::Emulator; pub use error::Error; pub use instruction::Instruction; pub use selector::Selector; From 1821dc7ff0a0030d817315d0e486a863b720621b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 3 Jun 2025 07:23:56 +0200 Subject: [PATCH 05/83] Introduce `tester` feature flag --- Cargo.lock | 278 +------------- Cargo.toml | 2 + devtools/Cargo.toml | 7 +- devtools/build.rs | 7 - devtools/src/icon.rs | 4 +- devtools/src/lib.rs | 402 +++----------------- devtools/src/tester.rs | 346 +++++++++++++++++ devtools/src/tester/null.rs | 51 +++ devtools/src/{widget => tester}/recorder.rs | 6 + devtools/src/time_machine.rs | 2 +- devtools/src/widget.rs | 17 +- examples/todos/Cargo.toml | 2 +- test/src/emulator.rs | 4 + 13 files changed, 488 insertions(+), 640 deletions(-) delete mode 100644 devtools/build.rs create mode 100644 devtools/src/tester.rs create mode 100644 devtools/src/tester/null.rs rename devtools/src/{widget => tester}/recorder.rs (96%) diff --git a/Cargo.lock b/Cargo.lock index dd3236ca..9cb1393b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,17 +33,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - [[package]] name = "ahash" version = "0.8.12" @@ -152,9 +141,6 @@ name = "arbitrary" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" -dependencies = [ - "derive_arbitrary", -] [[package]] name = "arc" @@ -680,25 +666,6 @@ version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e93abca9e28e0a1b9877922aacb20576e05d4679ffa78c3d6dc22a26a216659" -[[package]] -name = "bzip2" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" -dependencies = [ - "bzip2-sys", -] - -[[package]] -name = "bzip2-sys" -version = "0.1.13+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" -dependencies = [ - "cc", - "pkg-config", -] - [[package]] name = "cairo-sys-rs" version = "0.18.2" @@ -743,9 +710,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.24" +version = "1.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" +checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" dependencies = [ "jobserver", "libc", @@ -842,16 +809,6 @@ dependencies = [ "half", ] -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - [[package]] name = "clap" version = "4.5.39" @@ -995,12 +952,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "constant_time_eq" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" - [[package]] name = "core-foundation" version = "0.9.4" @@ -1115,21 +1066,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crc" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - [[package]] name = "crc32fast" version = "1.4.2" @@ -1291,12 +1227,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" -[[package]] -name = "deflate64" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" - [[package]] name = "deranged" version = "0.4.0" @@ -1306,17 +1236,6 @@ dependencies = [ "powerfmt", ] -[[package]] -name = "derive_arbitrary" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "digest" version = "0.10.7" @@ -1325,7 +1244,6 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", - "subtle", ] [[package]] @@ -1963,11 +1881,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", - "wasm-bindgen", ] [[package]] @@ -2253,12 +2169,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - [[package]] name = "hermit-abi" version = "0.5.1" @@ -2277,15 +2187,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - [[package]] name = "home" version = "0.5.11" @@ -2555,27 +2456,12 @@ name = "iced_devtools" version = "0.14.0-dev" dependencies = [ "iced_debug", - "iced_fontello", "iced_program", "iced_test", "iced_widget", "log", ] -[[package]] -name = "iced_fontello" -version = "0.14.0-dev" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1d2a83ec8063a28ddc818b018c2901a83fa7b773031b691dc80dd32cdb8f32" -dependencies = [ - "reqwest", - "serde", - "serde_json", - "sha2", - "toml", - "zip", -] - [[package]] name = "iced_futures" version = "0.14.0-dev" @@ -2896,15 +2782,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - [[package]] name = "integration" version = "0.1.0" @@ -2962,7 +2839,7 @@ version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ - "hermit-abi 0.5.1", + "hermit-abi", "libc", "windows-sys 0.59.0", ] @@ -3319,27 +3196,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "lzma-rs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" -dependencies = [ - "byteorder", - "crc", -] - -[[package]] -name = "lzma-sys" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - [[package]] name = "malloc_buf" version = "0.0.6" @@ -3708,11 +3564,11 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi", "libc", ] @@ -4243,16 +4099,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest", - "hmac", -] - [[package]] name = "percent-encoding" version = "2.3.1" @@ -4437,7 +4283,7 @@ checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi 0.5.1", + "hermit-abi", "pin-project-lite", "rustix 1.0.7", "tracing", @@ -4840,14 +4686,13 @@ checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" [[package]] name = "reqwest" -version = "0.12.18" +version = "0.12.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e98ff6b0dbbe4d5a37318f433d4fc82babd21631f194d370409ceb2e40b2f0b5" +checksum = "a2f8e5513d63f2e5b386eb5106dc67eaf3f84e95258e210489136b8b92ad6119" dependencies = [ "base64 0.22.1", "bytes", "encoding_rs", - "futures-channel", "futures-core", "futures-util", "h2 0.4.10", @@ -4862,7 +4707,6 @@ dependencies = [ "js-sys", "log", "mime", - "mime_guess", "native-tls", "once_cell", "percent-encoding", @@ -6067,16 +5911,9 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_write", "winnow", ] -[[package]] -name = "toml_write" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" - [[package]] name = "tooltip" version = "0.1.0" @@ -6112,9 +5949,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e" +checksum = "5cc2d9e086a412a451384326f521c8123a99a466b329941a9403696bff9b0da2" dependencies = [ "bitflags 2.9.1", "bytes", @@ -7596,15 +7433,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" -[[package]] -name = "xz2" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" -dependencies = [ - "lzma-sys", -] - [[package]] name = "yaml-rust" version = "0.4.5" @@ -7762,20 +7590,6 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] [[package]] name = "zerotrie" @@ -7810,76 +7624,6 @@ dependencies = [ "syn", ] -[[package]] -name = "zip" -version = "2.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" -dependencies = [ - "aes", - "arbitrary", - "bzip2", - "constant_time_eq", - "crc32fast", - "crossbeam-utils", - "deflate64", - "displaydoc", - "flate2", - "getrandom 0.3.3", - "hmac", - "indexmap", - "lzma-rs", - "memchr", - "pbkdf2", - "sha1", - "thiserror 2.0.12", - "time", - "xz2", - "zeroize", - "zopfli", - "zstd", -] - -[[package]] -name = "zopfli" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" -dependencies = [ - "bumpalo", - "crc32fast", - "log", - "simd-adler32", -] - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.15+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" -dependencies = [ - "cc", - "pkg-config", -] - [[package]] name = "zune-core" version = "0.4.12" diff --git a/Cargo.toml b/Cargo.toml index 1e7ef4e1..2d9beb5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,8 @@ lazy = ["iced_widget/lazy"] debug = ["iced_winit/debug", "iced_devtools"] # Enables time-travel debugging (very experimental!) time-travel = ["debug", "iced_devtools/time-travel"] +# Enables the tester developer tool for recording and playing tests (press F12) +tester = ["debug", "iced_devtools/tester"] # Enables the `thread-pool` futures executor as the `executor::Default` on native platforms thread-pool = ["iced_futures/thread-pool"] # Enables `tokio` as the `executor::Default` on native platforms diff --git a/devtools/Cargo.toml b/devtools/Cargo.toml index fab9128d..98f83745 100644 --- a/devtools/Cargo.toml +++ b/devtools/Cargo.toml @@ -15,14 +15,13 @@ workspace = true [features] time-travel = ["iced_program/time-travel"] +tester = ["dep:iced_test"] [dependencies] iced_debug.workspace = true iced_program.workspace = true -iced_test.workspace = true iced_widget.workspace = true - log.workspace = true -[build-dependencies] -iced_fontello = "0.14.0-dev" +iced_test.workspace = true +iced_test.optional = true diff --git a/devtools/build.rs b/devtools/build.rs deleted file mode 100644 index 5bcfd41b..00000000 --- a/devtools/build.rs +++ /dev/null @@ -1,7 +0,0 @@ -#![allow(missing_docs)] -fn main() { - // println!("cargo::rerun-if-changed=fonts/iced_devtools-icons.toml"); - - // iced_fontello::build("fonts/iced_devtools-icons.toml") - // .expect("Build icons font"); -} diff --git a/devtools/src/icon.rs b/devtools/src/icon.rs index cd7f1b58..27ceb9b7 100644 --- a/devtools/src/icon.rs +++ b/devtools/src/icon.rs @@ -1,6 +1,4 @@ -// Generated automatically by iced_fontello at build time. -// Do not edit manually. Source: ../fonts/iced_devtools-icons.toml -// 3139a163a989c992b8f038da359b59e9292fc49f031e760b61a8d76e2037aee2 +#![allow(unused)] use crate::core::Font; use crate::program; use crate::widget::{Text, text}; diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index d516d075..151b5c93 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -1,6 +1,7 @@ #![allow(missing_docs)] use iced_debug as debug; use iced_program as program; +#[cfg(feature = "tester")] use iced_test as test; use iced_widget::core; use iced_widget::runtime; @@ -12,26 +13,29 @@ mod icon; mod time_machine; mod widget; -use crate::core::alignment::Horizontal::Right; +#[cfg(feature = "tester")] +mod tester; + +#[cfg(not(feature = "tester"))] +#[path = "tester/null.rs"] +mod tester; + +use crate::tester::Tester; + use crate::core::border; use crate::core::keyboard; use crate::core::theme::{self, Base, Theme}; use crate::core::time::seconds; use crate::core::window; -use crate::core::{ - Alignment::Center, Color, Element, Font, Length::Fill, Size, -}; +use crate::core::{Alignment::Center, Color, Element, Length::Fill}; use crate::futures::Subscription; use crate::program::Program; use crate::runtime::Task; use crate::runtime::font; -use crate::test::Emulator; -use crate::test::emulator; -use crate::test::instruction; use crate::time_machine::TimeMachine; use crate::widget::{ - Text, bottom_right, button, center, column, container, horizontal_space, - opaque, row, scrollable, stack, text, text_input, themer, + bottom_right, button, center, column, container, horizontal_space, + monospace, opaque, row, scrollable, stack, text, themer, }; use std::fmt; @@ -127,49 +131,29 @@ where P: Program, { state: P::State, - size: Size, - mode: Mode

, show_notification: bool, time_machine: TimeMachine

, + mode: Mode

, } #[derive(Debug, Clone)] pub enum Message { HideNotification, - Toggle, ToggleComet, CometLaunched(comet::launch::Result), InstallComet, Installing(comet::install::Result), CancelSetup, - ChangeWidth(String), - ChangeHeight(String), - Record, - Stop, - Recorded(core::Event), - Play, + Toggle, + Tester(tester::Message), } enum Mode { Hidden, - Open { recorder: Recorder

}, + Open { tester: Tester

}, Setup(Setup), } -struct Recorder { - instructions: Vec, - state: State

, -} - -enum State { - Idle, - Recording, - Playing { - emulator: Emulator

, - current: usize, - }, -} - enum Setup { Idle { goal: Goal }, Running { logs: Vec }, @@ -188,7 +172,6 @@ where ( Self { state, - size: Size::new(512.0, 512.0), mode: Mode::Hidden, show_notification: true, time_machine: TimeMachine::new(), @@ -217,18 +200,10 @@ where match &self.mode { Mode::Hidden => { self.mode = Mode::Open { - recorder: Recorder { - instructions: Vec::new(), - state: State::Idle, - }, + tester: Tester::new(), }; } - Mode::Open { - recorder: - Recorder { - state: State::Idle, .. - }, - } => { + Mode::Open { tester } if !tester.is_busy() => { self.mode = Mode::Hidden; } Mode::Setup(_) | Mode::Open { .. } => {} @@ -318,92 +293,12 @@ where Task::none() } - Message::ChangeWidth(width) => { - if let Ok(width) = width.parse() { - self.size.width = width; - } - - Task::none() - } - Message::ChangeHeight(height) => { - if let Ok(height) = height.parse() { - self.size.height = height; - } - - Task::none() - } - Message::Record => { - let Mode::Open { recorder } = &mut self.mode else { + Message::Tester(message) => { + let Mode::Open { tester } = &mut self.mode else { return Task::none(); }; - recorder.instructions.clear(); - recorder.state = State::Recording; - - let (state, task) = program.boot(); - self.state = state; - - task.map(Event::Program) - } - Message::Recorded(event) => { - let Mode::Open { recorder } = &mut self.mode else { - return Task::none(); - }; - - let Some(interaction) = - instruction::Interaction::from_event(event) - else { - return Task::none(); - }; - - if let Some(test::Instruction::Interact(last_interaction)) = - recorder.instructions.pop() - { - let (last_interaction, new_interaction) = - last_interaction.merge(interaction); - - recorder.instructions.push( - test::Instruction::Interact(last_interaction), - ); - - if let Some(new_interaction) = new_interaction { - recorder.instructions.push( - test::Instruction::Interact(new_interaction), - ); - } - } else { - recorder - .instructions - .push(test::Instruction::Interact(interaction)); - } - - Task::none() - } - Message::Stop => { - let Mode::Open { recorder } = &mut self.mode else { - return Task::none(); - }; - - recorder.state = State::Idle; - - Task::none() - } - Message::Play => { - let Mode::Open { recorder } = &mut self.mode else { - return Task::none(); - }; - - let (sender, receiver) = - futures::futures::channel::mpsc::channel(1); - - let emulator = Emulator::new(program, self.size, sender); - - recorder.state = State::Playing { - emulator, - current: 0, - }; - - Task::run(receiver, Event::Emulator) + tester.update(program, message).map(Event::Tester) } }, Event::Program(message) => { @@ -436,33 +331,12 @@ where Task::none() } - Event::Emulator(event) => { - let Mode::Open { - recorder: - Recorder { - state: State::Playing { emulator, current }, - instructions, - }, - } = &mut self.mode - else { + Event::Tester(tick) => { + let Mode::Open { tester } = &mut self.mode else { return Task::none(); }; - match event { - emulator::Event::Action(action) => { - emulator.perform(program, action); - } - emulator::Event::Ready => { - if let Some(instruction) = - instructions.get(*current).cloned() - { - emulator.run(program, instruction); - *current += 1; - } - } - } - - Task::none() + tester.tick(program, tick).map(Event::Tester) } Event::Discard => Task::none(), } @@ -476,25 +350,23 @@ where let state = self.state(); let view = { - let view = match &self.mode { - Mode::Open { - recorder: - Recorder { - state: State::Playing { emulator, .. }, - .. - }, - } => emulator.view(program), - _ => program.view(state, window), + let view = || { + let theme = program.theme(state, window); + let view: Element<'_, _, Theme, _> = + themer(theme, program.view(&self.state, window)).into(); + + if self.time_machine.is_rewinding() { + view.map(|_| Event::Discard) + } else { + view.map(Event::Program) + } }; - let theme = program.theme(state, window); - - let view: Element<'_, _, Theme, _> = themer(theme, view).into(); - - if self.time_machine.is_rewinding() { - view.map(|_| Event::Discard) - } else { - view.map(Event::Program) + match &self.mode { + Mode::Open { tester } => { + tester.view(program, window, view, Event::Tester) + } + _ => view(), } }; @@ -536,104 +408,11 @@ where )) }); - let sidebar = if let Mode::Open { recorder } = &self.mode { + let sidebar = if let Mode::Open { tester } = &self.mode { let title = monospace("Developer Tools"); + let tester = tester.controls().map(Message::Tester); - let recorder = { - let events = container(if recorder.instructions.is_empty() { - Element::from(center( - monospace("No instructions recorded yet!") - .size(14) - .width(Fill) - .center(), - )) - } else { - scrollable( - column(recorder.instructions.iter().enumerate().map( - |(i, instruction)| { - monospace(instruction.to_string()) - .size(10) - .style(move |theme: &Theme| text::Style { - color: match &recorder.state { - State::Playing { - current, .. - } => { - if *current == i { - Some( - theme.palette().primary, - ) - } else if *current > i { - Some( - theme - .extended_palette() - .success - .strong - .color, - ) - } else { - None - } - } - _ => None, - }, - }) - .into() - }, - )) - .spacing(5), - ) - .spacing(5) - .into() - }) - .width(Fill) - .height(Fill) - .padding(5); - - let controls = { - row![ - button(icon::play().size(14).width(Fill).center()) - .on_press_maybe( - (!matches!(recorder.state, State::Recording) - && !recorder.instructions.is_empty()) - .then_some(Message::Play), - ), - if let State::Recording = &recorder.state { - button(icon::stop().size(14).width(Fill).center()) - .on_press(Message::Stop) - .style(button::success) - } else { - button(icon::record().size(14).width(Fill).center()) - .on_press_maybe( - matches!(recorder.state, State::Idle) - .then_some(Message::Record), - ) - .style(button::danger) - } - ] - .spacing(10) - }; - - column![events, controls].spacing(10).align_x(Center) - }; - - let viewport = row![ - text_input("Width", &self.size.width.to_string()) - .size(14) - .on_input(Message::ChangeWidth), - monospace("x"), - text_input("Height", &self.size.height.to_string()) - .size(14) - .on_input(Message::ChangeHeight), - ] - .spacing(10) - .align_y(Center); - - let tools = column![ - title, - labeled("Viewport", viewport), - labeled("Tester", recorder) - ] - .spacing(10); + let tools = column![title, tester].spacing(10); let sidebar = container(tools) .padding(10) @@ -646,65 +425,7 @@ where None }; - let content = row![if let Mode::Open { recorder } = &self.mode { - let status = match &recorder.state { - State::Idle => monospace("Idle").style(|theme| text::Style { - color: Some( - theme.extended_palette().background.strongest.color, - ), - }), - State::Recording => { - monospace("Recording").style(|theme| text::Style { - color: Some(theme.palette().danger), - }) - } - State::Playing { .. } => { - monospace("Playing").style(|theme| text::Style { - color: Some(theme.palette().primary), - }) - } - }; - - let viewport = container( - scrollable( - container(if let State::Recording = &recorder.state { - widget::recorder(view) - .on_event(|event| { - Event::Message(Message::Recorded(event)) - }) - .into() - } else { - view - }) - .width(self.size.width) - .height(self.size.height), - ) - .direction(scrollable::Direction::Both { - vertical: scrollable::Scrollbar::default(), - horizontal: scrollable::Scrollbar::default(), - }), - ) - .style(|theme| { - let palette = theme.extended_palette(); - - container::Style { - border: border::width(2.0).color(match &recorder.state { - State::Idle => palette.background.strongest.color, - State::Recording => palette.danger.base.color, - State::Playing { .. } => palette.primary.base.color, - }), - ..container::Style::default() - } - }) - .padding(10); - - center(column![status, viewport].spacing(10).align_x(Right)) - .padding(10) - .into() - } else { - view - }] - .push_maybe(sidebar); + let content = row![view].push_maybe(sidebar); themer( theme, @@ -724,8 +445,13 @@ where let hotkeys = futures::keyboard::on_key_press(|key, _modifiers| match key { keyboard::Key::Named(keyboard::key::Named::F12) => { - Some(Message::Toggle) + Some(if cfg!(feature = "tester") { + Message::Toggle + } else { + Message::ToggleComet + }) } + #[cfg(feature = "tester")] keyboard::Key::Named(keyboard::key::Named::F11) => { Some(Message::ToggleComet) } @@ -761,7 +487,7 @@ where { Message(Message), Program(P::Message), - Emulator(emulator::Event

), + Tester(tester::Tick

), Command(debug::Command), Discard, } @@ -774,7 +500,7 @@ where match self { Self::Message(message) => message.fmt(f), Self::Program(message) => message.fmt(f), - Self::Emulator(_) => f.write_str("Emulator"), + Self::Tester(_) => f.write_str("Tester"), Self::Command(command) => command.fmt(f), Self::Discard => f.write_str("Discard"), } @@ -791,7 +517,7 @@ where Self::Message(message) => Self::Message(message.clone()), Self::Program(message) => Self::Program(message.clone()), Self::Command(command) => Self::Command(*command), - Self::Emulator(_) => Self::Discard, // Time traveling an emulator?! + Self::Tester(_) => Self::Discard, // Time traveling an emulator?! Self::Discard => Self::Discard, } } @@ -926,25 +652,3 @@ where .padding([2, 4]) .into() } - -fn monospace<'a, Renderer>( - fragment: impl text::IntoFragment<'a>, -) -> Text<'a, Theme, Renderer> -where - Renderer: program::Renderer + 'a, -{ - text(fragment).font(Font::MONOSPACE) -} - -fn labeled<'a, Message, Renderer>( - fragment: impl text::IntoFragment<'a>, - content: impl Into>, -) -> Element<'a, Message, Theme, Renderer> -where - Message: 'a, - Renderer: program::Renderer + 'a, -{ - column![monospace(fragment).size(14), content.into()] - .spacing(5) - .into() -} diff --git a/devtools/src/tester.rs b/devtools/src/tester.rs new file mode 100644 index 00000000..73a7793a --- /dev/null +++ b/devtools/src/tester.rs @@ -0,0 +1,346 @@ +mod recorder; + +use recorder::recorder; + +use crate::Program; +use crate::core::Alignment::Center; +use crate::core::Length::Fill; +use crate::core::alignment::Horizontal::Right; +use crate::core::border; +use crate::core::window; +use crate::core::{Element, Event, Size, Theme}; +use crate::futures::futures::channel::mpsc; +use crate::icon; +use crate::program; +use crate::runtime::Task; +use crate::test::emulator; +use crate::test::instruction; +use crate::test::{Emulator, Instruction}; +use crate::widget::{ + button, center, column, container, monospace, row, scrollable, text, + text_input, themer, +}; + +pub struct Tester { + viewport: Size, + instructions: Vec, + state: State

, +} + +enum State { + Idle, + Recording { + state: P::State, + }, + Playing { + emulator: Emulator

, + current: usize, + }, +} + +#[derive(Debug, Clone)] +pub enum Message { + ChangeViewport(Size), + Record, + Stop, + Play, +} + +#[allow(missing_debug_implementations)] +pub enum Tick { + Program(P::Message), + Recorder(Event), + Emulator(emulator::Event

), +} + +impl Tester

{ + pub fn new() -> Self { + Self { + viewport: Size::new(512.0, 512.0), + instructions: Vec::new(), + state: State::Idle, + } + } + + pub fn is_busy(&self) -> bool { + matches!(self.state, State::Idle | State::Playing { .. }) + } + + pub fn update(&mut self, program: &P, message: Message) -> Task> { + match message { + Message::ChangeViewport(viewport) => { + self.viewport = viewport; + + Task::none() + } + Message::Record => { + self.instructions.clear(); + + let (state, task) = program.boot(); + self.state = State::Recording { state }; + + task.map(Tick::Program) + } + Message::Stop => { + self.state = State::Idle; + + Task::none() + } + Message::Play => { + let (sender, receiver) = mpsc::channel(1); + let emulator = Emulator::new(program, self.viewport, sender); + + self.state = State::Playing { + emulator, + current: 0, + }; + + Task::run(receiver, Tick::Emulator) + } + } + } + + pub fn tick(&mut self, program: &P, tick: Tick

) -> Task> { + match tick { + Tick::Program(message) => { + let State::Recording { state } = &mut self.state else { + return Task::none(); + }; + + program.update(state, message).map(Tick::Program) + } + Tick::Recorder(event) => { + let Some(interaction) = + instruction::Interaction::from_event(event) + else { + return Task::none(); + }; + + if let Some(Instruction::Interact(last_interaction)) = + self.instructions.pop() + { + let (last_interaction, new_interaction) = + last_interaction.merge(interaction); + + self.instructions + .push(Instruction::Interact(last_interaction)); + + if let Some(new_interaction) = new_interaction { + self.instructions + .push(Instruction::Interact(new_interaction)); + } + } else { + self.instructions.push(Instruction::Interact(interaction)); + } + + Task::none() + } + Tick::Emulator(event) => { + let State::Playing { emulator, current } = &mut self.state + else { + return Task::none(); + }; + + match event { + emulator::Event::Action(action) => { + emulator.perform(program, action); + } + emulator::Event::Ready => { + if let Some(instruction) = + self.instructions.get(*current).cloned() + { + emulator.run(program, instruction); + *current += 1; + } + } + } + + Task::none() + } + } + } + + pub fn view<'a, T: 'static>( + &'a self, + program: &P, + window: window::Id, + current: impl FnOnce() -> Element<'a, T, Theme, P::Renderer>, + emulate: impl Fn(Tick

) -> T + 'a, + ) -> Element<'a, T, Theme, P::Renderer> { + let status = match &self.state { + State::Idle => monospace("Idle").style(|theme| text::Style { + color: Some( + theme.extended_palette().background.strongest.color, + ), + }), + State::Recording { .. } => { + monospace("Recording").style(|theme| text::Style { + color: Some(theme.palette().danger), + }) + } + State::Playing { .. } => { + monospace("Playing").style(|theme| text::Style { + color: Some(theme.palette().primary), + }) + } + }; + + let viewport = container( + scrollable( + container(match &self.state { + State::Idle => current(), + State::Recording { state } => { + let theme = program.theme(state, window); + let view = + program.view(state, window).map(Tick::Program); + + Element::from( + recorder(themer(theme, view)) + .on_event(Tick::Recorder), + ) + .map(emulate) + } + State::Playing { emulator, .. } => { + let theme = emulator.theme(program); + let view = emulator.view(program).map(Tick::Program); + + Element::from(themer(theme, view)).map(emulate) + } + }) + .width(self.viewport.width) + .height(self.viewport.height), + ) + .direction(scrollable::Direction::Both { + vertical: scrollable::Scrollbar::default(), + horizontal: scrollable::Scrollbar::default(), + }), + ) + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + + container::Style { + border: border::width(2.0).color(match &self.state { + State::Idle => palette.background.strongest.color, + State::Recording { .. } => palette.danger.base.color, + State::Playing { .. } => palette.primary.base.color, + }), + ..container::Style::default() + } + }) + .padding(10); + + center(column![status, viewport].spacing(10).align_x(Right)) + .padding(10) + .into() + } + + pub fn controls(&self) -> Element<'_, Message, Theme, P::Renderer> { + let viewport = row![ + text_input("Width", &self.viewport.width.to_string()) + .size(14) + .on_input(|width| Message::ChangeViewport(Size { + width: width.parse().unwrap_or(self.viewport.width), + ..self.viewport + })), + monospace("x"), + text_input("Height", &self.viewport.height.to_string()) + .size(14) + .on_input(|height| Message::ChangeViewport(Size { + height: height.parse().unwrap_or(self.viewport.height), + ..self.viewport + })), + ] + .spacing(10) + .align_y(Center); + + let player = { + let events = container(if self.instructions.is_empty() { + Element::from(center( + monospace("No instructions recorded yet!") + .size(14) + .width(Fill) + .center(), + )) + } else { + scrollable( + column(self.instructions.iter().enumerate().map( + |(i, instruction)| { + monospace(instruction.to_string()) + .size(10) + .style(move |theme: &Theme| text::Style { + color: match &self.state { + State::Playing { current, .. } => { + if *current == i { + Some(theme.palette().primary) + } else if *current > i { + Some( + theme + .extended_palette() + .success + .strong + .color, + ) + } else { + None + } + } + _ => None, + }, + }) + .into() + }, + )) + .spacing(5), + ) + .spacing(5) + .into() + }) + .width(Fill) + .height(Fill) + .padding(5); + + let controls = { + row![ + button(icon::play().size(14).width(Fill).center()) + .on_press_maybe( + (!matches!(self.state, State::Recording { .. }) + && !self.instructions.is_empty()) + .then_some(Message::Play), + ), + if let State::Recording { .. } = &self.state { + button(icon::stop().size(14).width(Fill).center()) + .on_press(Message::Stop) + .style(button::success) + } else { + button(icon::record().size(14).width(Fill).center()) + .on_press_maybe( + matches!(self.state, State::Idle) + .then_some(Message::Record), + ) + .style(button::danger) + } + ] + .spacing(10) + }; + + column![events, controls].spacing(10).align_x(Center) + }; + + column![labeled("Viewport", viewport), labeled("Tester", player)] + .spacing(10) + .into() + } +} + +fn labeled<'a, Message, Renderer>( + fragment: impl text::IntoFragment<'a>, + content: impl Into>, +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Renderer: program::Renderer + 'a, +{ + column![monospace(fragment).size(14), content.into()] + .spacing(5) + .into() +} diff --git a/devtools/src/tester/null.rs b/devtools/src/tester/null.rs new file mode 100644 index 00000000..3a7247ba --- /dev/null +++ b/devtools/src/tester/null.rs @@ -0,0 +1,51 @@ +use crate::Program; +use crate::core::window; +use crate::core::{Element, Theme}; +use crate::runtime::Task; +use crate::widget::horizontal_space; + +use std::marker::PhantomData; + +pub struct Tester { + _type: PhantomData, +} + +#[derive(Debug, Clone)] +pub enum Message {} + +#[allow(missing_debug_implementations)] +pub struct Tick { + _type: PhantomData, +} + +impl Tester

{ + pub fn new() -> Self { + Self { _type: PhantomData } + } + + pub fn is_busy(&self) -> bool { + false + } + + pub fn update(&mut self, _program: &P, _message: Message) -> Task> { + Task::none() + } + + pub fn tick(&mut self, _program: &P, _tick: Tick

) -> Task> { + Task::none() + } + + pub fn view<'a, T: 'static>( + &'a self, + _program: &P, + _window: window::Id, + _current: impl FnOnce() -> Element<'a, T, Theme, P::Renderer>, + _emulate: impl Fn(Tick

) -> T + 'a, + ) -> Element<'a, T, Theme, P::Renderer> { + horizontal_space().into() + } + + pub fn controls(&self) -> Element<'_, Message, Theme, P::Renderer> { + horizontal_space().into() + } +} diff --git a/devtools/src/widget/recorder.rs b/devtools/src/tester/recorder.rs similarity index 96% rename from devtools/src/widget/recorder.rs rename to devtools/src/tester/recorder.rs index bae43478..47191582 100644 --- a/devtools/src/widget/recorder.rs +++ b/devtools/src/tester/recorder.rs @@ -8,6 +8,12 @@ use crate::core::{ Size, Widget, }; +pub fn recorder<'a, Message, Theme, Renderer>( + content: impl Into>, +) -> Recorder<'a, Message, Theme, Renderer> { + Recorder::new(content) +} + #[allow(missing_debug_implementations)] pub struct Recorder<'a, Message, Theme, Renderer> { content: Element<'a, Message, Theme, Renderer>, diff --git a/devtools/src/time_machine.rs b/devtools/src/time_machine.rs index 6d8b80ae..f999c70e 100644 --- a/devtools/src/time_machine.rs +++ b/devtools/src/time_machine.rs @@ -30,6 +30,7 @@ where } pub fn rewind(&mut self, program: &P, message: usize) { + crate::debug::disable(); let (mut state, _) = program.boot(); if message < self.messages.len() { @@ -40,7 +41,6 @@ where } self.state = Some(state); - crate::debug::disable(); } pub fn go_to_present(&mut self) { diff --git a/devtools/src/widget.rs b/devtools/src/widget.rs index 3067a1ca..ef247508 100644 --- a/devtools/src/widget.rs +++ b/devtools/src/widget.rs @@ -1,12 +1,13 @@ -mod recorder; - pub use iced_widget::*; -pub use recorder::Recorder; -use crate::core::Element; +use crate::core::Font; +use crate::program; -pub fn recorder<'a, Message, Theme, Renderer>( - content: impl Into>, -) -> Recorder<'a, Message, Theme, Renderer> { - Recorder::new(content) +pub fn monospace<'a, Renderer>( + fragment: impl text::IntoFragment<'a>, +) -> Text<'a, Theme, Renderer> +where + Renderer: program::Renderer + 'a, +{ + text(fragment).font(Font::MONOSPACE) } diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml index 5e16a2ac..de23558e 100644 --- a/examples/todos/Cargo.toml +++ b/examples/todos/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["tokio", "debug", "time-travel"] +iced.features = ["tokio", "time-travel", "tester"] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/test/src/emulator.rs b/test/src/emulator.rs index c76301b5..e0170c1d 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -158,6 +158,10 @@ impl Emulator

{ ) -> Element<'_, P::Message, P::Theme, P::Renderer> { program.view(&self.state, self.window) } + + pub fn theme(&self, program: &P) -> P::Theme { + program.theme(&self.state, self.window) + } } struct Clipboard { From 9cd1a93e5aaf3b17922cce53f90110b65af5cae2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 3 Jun 2025 09:54:14 +0200 Subject: [PATCH 06/83] Enable `tester` feature in `websocket` example --- devtools/src/lib.rs | 27 +++++++++++++++++++-------- devtools/src/tester.rs | 16 +++++++++++++++- examples/todos/Cargo.toml | 2 +- examples/websocket/Cargo.toml | 2 +- examples/websocket/src/main.rs | 13 ++++++++----- 5 files changed, 44 insertions(+), 16 deletions(-) diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index 151b5c93..3a663384 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -104,10 +104,7 @@ where state.title(&self.program, window) } - fn subscription( - &self, - state: &Self::State, - ) -> runtime::futures::Subscription { + fn subscription(&self, state: &Self::State) -> Subscription { state.subscription(&self.program) } @@ -438,9 +435,19 @@ where } fn subscription(&self, program: &P) -> Subscription> { - let subscription = - program.subscription(&self.state).map(Event::Program); - debug::subscriptions_tracked(subscription.units()); + let subscription = match &self.mode { + Mode::Open { tester } if !tester.is_idle() => { + tester.subscription(program).map(Event::Tester) + } + _ => { + let subscription = + program.subscription(&self.state).map(Event::Program); + + debug::subscriptions_tracked(subscription.units()); + + subscription + } + }; let hotkeys = futures::keyboard::on_key_press(|key, _modifiers| match key { @@ -473,7 +480,11 @@ where } fn scale_factor(&self, program: &P, window: window::Id) -> f64 { - program.scale_factor(self.state(), window) + if let Mode::Open { .. } = &self.mode { + 1.0 + } else { + program.scale_factor(self.state(), window) + } } fn state(&self) -> &P::State { diff --git a/devtools/src/tester.rs b/devtools/src/tester.rs index 73a7793a..e5aa441b 100644 --- a/devtools/src/tester.rs +++ b/devtools/src/tester.rs @@ -9,6 +9,7 @@ use crate::core::alignment::Horizontal::Right; use crate::core::border; use crate::core::window; use crate::core::{Element, Event, Size, Theme}; +use crate::futures::Subscription; use crate::futures::futures::channel::mpsc; use crate::icon; use crate::program; @@ -62,8 +63,12 @@ impl Tester

{ } } + pub fn is_idle(&self) -> bool { + matches!(self.state, State::Idle) + } + pub fn is_busy(&self) -> bool { - matches!(self.state, State::Idle | State::Playing { .. }) + matches!(self.state, State::Recording { .. } | State::Playing { .. }) } pub fn update(&mut self, program: &P, message: Message) -> Task> { @@ -160,6 +165,15 @@ impl Tester

{ } } + pub fn subscription(&self, program: &P) -> Subscription> { + match &self.state { + State::Idle | State::Playing { .. } => Subscription::none(), + State::Recording { state } => { + program.subscription(state).map(Tick::Program) + } + } + } + pub fn view<'a, T: 'static>( &'a self, program: &P, diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml index de23558e..5e16a2ac 100644 --- a/examples/todos/Cargo.toml +++ b/examples/todos/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["tokio", "time-travel", "tester"] +iced.features = ["tokio", "debug", "time-travel"] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/examples/websocket/Cargo.toml b/examples/websocket/Cargo.toml index c47e3c93..b2053fa6 100644 --- a/examples/websocket/Cargo.toml +++ b/examples/websocket/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["debug", "tokio", "sipper"] +iced.features = ["tester", "tokio", "sipper"] warp = "0.3" diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index c52de37e..81083a22 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -1,9 +1,11 @@ mod echo; +use iced::futures::stream; use iced::widget::{ self, button, center, column, row, scrollable, text, text_input, }; use iced::{Center, Element, Fill, Subscription, Task, color}; + use std::sync::LazyLock; pub fn main() -> iced::Result { @@ -34,10 +36,7 @@ impl WebSocket { new_message: String::new(), state: State::Disconnected, }, - Task::batch([ - Task::perform(echo::server::run(), |_| Message::Server), - widget::focus_next(), - ]), + widget::focus_next(), ) } @@ -87,7 +86,11 @@ impl WebSocket { } fn subscription(&self) -> Subscription { - Subscription::run(echo::connect).map(Message::Echo) + Subscription::batch([ + Subscription::run(|| stream::once(echo::server::run())) + .map(|_| Message::Server), + Subscription::run(echo::connect).map(Message::Echo), + ]) } fn view(&self) -> Element { From ff0bbf52834af28299de94c72b2299e67f64700e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 3 Jun 2025 09:54:32 +0200 Subject: [PATCH 07/83] Track subscriptions in `iced_test::Emulator` --- test/src/emulator.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/src/emulator.rs b/test/src/emulator.rs index e0170c1d..cb1b7ed0 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -7,6 +7,7 @@ use crate::core::{Element, Size}; use crate::program::Program; use crate::runtime::futures::futures::StreamExt; use crate::runtime::futures::futures::channel::mpsc; +use crate::runtime::futures::subscription; use crate::runtime::futures::{Executor, Runtime}; use crate::runtime::task; use crate::runtime::user_interface; @@ -80,6 +81,12 @@ impl Emulator

{ if let Some(stream) = task::into_stream(task) { self.runtime.run(stream.map(Event::Action).boxed()); } + + self.runtime.track(subscription::into_recipes( + program + .subscription(&self.state) + .map(|message| Event::Action(Action::Output(message))), + )); } pub fn perform(&mut self, program: &P, action: Action) { From a7cf2642c77a6a803f4273f134f50388e473a5c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 3 Jun 2025 09:55:32 +0200 Subject: [PATCH 08/83] Fix missing methods in `null` tester --- devtools/src/tester/null.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/devtools/src/tester/null.rs b/devtools/src/tester/null.rs index 3a7247ba..52d7ae4b 100644 --- a/devtools/src/tester/null.rs +++ b/devtools/src/tester/null.rs @@ -1,6 +1,7 @@ use crate::Program; use crate::core::window; use crate::core::{Element, Theme}; +use crate::futures::Subscription; use crate::runtime::Task; use crate::widget::horizontal_space; @@ -23,6 +24,10 @@ impl Tester

{ Self { _type: PhantomData } } + pub fn is_idle(&self) -> bool { + true + } + pub fn is_busy(&self) -> bool { false } @@ -35,6 +40,10 @@ impl Tester

{ Task::none() } + pub fn subscription(&self, _program: &P) -> Subscription> { + Subscription::none() + } + pub fn view<'a, T: 'static>( &'a self, _program: &P, From 95e1efc6eaf3b038c124fbef8d12a52a71db98d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 3 Jun 2025 10:06:10 +0200 Subject: [PATCH 09/83] Update test snapshots of `styling` example --- examples/styling/snapshots/catppuccin_latte-tiny-skia.sha256 | 2 +- examples/styling/snapshots/solarized_dark-tiny-skia.sha256 | 2 +- examples/styling/snapshots/solarized_light-tiny-skia.sha256 | 2 +- examples/styling/snapshots/tokyo_night-tiny-skia.sha256 | 2 +- examples/styling/snapshots/tokyo_night_light-tiny-skia.sha256 | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/styling/snapshots/catppuccin_latte-tiny-skia.sha256 b/examples/styling/snapshots/catppuccin_latte-tiny-skia.sha256 index 1993b7c9..deb39ab6 100644 --- a/examples/styling/snapshots/catppuccin_latte-tiny-skia.sha256 +++ b/examples/styling/snapshots/catppuccin_latte-tiny-skia.sha256 @@ -1 +1 @@ -fa00d7e0ff95b366945d409712d7fe4ce44fff22425236cb56b8b96a88815ee6 \ No newline at end of file +78d3afadbdca4a992c662541d602850e0dec0abafa585e2a3078daa1be5998b8 \ No newline at end of file diff --git a/examples/styling/snapshots/solarized_dark-tiny-skia.sha256 b/examples/styling/snapshots/solarized_dark-tiny-skia.sha256 index 0b341569..a3988fa4 100644 --- a/examples/styling/snapshots/solarized_dark-tiny-skia.sha256 +++ b/examples/styling/snapshots/solarized_dark-tiny-skia.sha256 @@ -1 +1 @@ -15aa476c65304bde23e3648ceb424d6394f525f6c1d3e49ee1150376b6fd3068 \ No newline at end of file +be1f056f0decaee8c138cd038e3e34352f02448b77c1e251796a8f0be8086913 \ No newline at end of file diff --git a/examples/styling/snapshots/solarized_light-tiny-skia.sha256 b/examples/styling/snapshots/solarized_light-tiny-skia.sha256 index b62d6107..4b355311 100644 --- a/examples/styling/snapshots/solarized_light-tiny-skia.sha256 +++ b/examples/styling/snapshots/solarized_light-tiny-skia.sha256 @@ -1 +1 @@ -04fcb0da640c6e7c1d86c95c74570c9f9d8f56056c9802eef332187f52c003ca \ No newline at end of file +31a7a46ed7951d69e1a29ab595d241e689cacdf66cea53dda6609db4927070e5 \ No newline at end of file diff --git a/examples/styling/snapshots/tokyo_night-tiny-skia.sha256 b/examples/styling/snapshots/tokyo_night-tiny-skia.sha256 index b4afd06f..1288126c 100644 --- a/examples/styling/snapshots/tokyo_night-tiny-skia.sha256 +++ b/examples/styling/snapshots/tokyo_night-tiny-skia.sha256 @@ -1 +1 @@ -26e9660da3caa88e5a995739c986b142487f1449c1a5dc555abf7bc7cbca2345 \ No newline at end of file +5eab001ed1aeeea3f24fe18e4aab9b8522cb35ac1328d7ca3532dbdfdf95780f \ No newline at end of file diff --git a/examples/styling/snapshots/tokyo_night_light-tiny-skia.sha256 b/examples/styling/snapshots/tokyo_night_light-tiny-skia.sha256 index 29ba7bca..55e6a15c 100644 --- a/examples/styling/snapshots/tokyo_night_light-tiny-skia.sha256 +++ b/examples/styling/snapshots/tokyo_night_light-tiny-skia.sha256 @@ -1 +1 @@ -8d846a765ff96506ad4f26a672976cb1bfa997d4b09c5ecabf273d5e22ae0a3a \ No newline at end of file +2fed695daa4c3da56f744832a041060704310a97d65820c97d556d297dfb271a \ No newline at end of file From 149eb8a0246b618c90dca0e9265195a05f8c7d7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 4 Jun 2025 13:35:09 +0200 Subject: [PATCH 10/83] Wait for `Task` in `Emulator` automatically --- test/src/emulator.rs | 60 ++++++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/test/src/emulator.rs b/test/src/emulator.rs index cb1b7ed0..d6b5f939 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -7,11 +7,12 @@ use crate::core::{Element, Size}; use crate::program::Program; use crate::runtime::futures::futures::StreamExt; use crate::runtime::futures::futures::channel::mpsc; +use crate::runtime::futures::futures::stream; use crate::runtime::futures::subscription; use crate::runtime::futures::{Executor, Runtime}; use crate::runtime::task; use crate::runtime::user_interface; -use crate::runtime::{Action, UserInterface}; +use crate::runtime::{Action, Task, UserInterface}; #[allow(missing_debug_implementations)] pub struct Emulator { @@ -52,18 +53,10 @@ impl Emulator

{ )) .expect("Create emulator renderer"); - let mut runtime = Runtime::new(executor, sender); - + let runtime = Runtime::new(executor, sender); let (state, task) = program.boot(); - if let Some(stream) = task::into_stream(task) { - runtime.run(stream.map(Event::Action).boxed()); - } - - // TODO: Async boot environments - runtime.send(Event::Ready); - - Self { + let mut emulator = Self { state, runtime, renderer, @@ -72,7 +65,12 @@ impl Emulator

{ cursor: mouse::Cursor::Unavailable, window: window::Id::unique(), cache: Some(user_interface::Cache::default()), - } + }; + + // TODO: Configurable + emulator.wait_for(task); + + emulator } pub fn update(&mut self, program: &P, message: P::Message) { @@ -82,11 +80,7 @@ impl Emulator

{ self.runtime.run(stream.map(Event::Action).boxed()); } - self.runtime.track(subscription::into_recipes( - program - .subscription(&self.state) - .map(|message| Event::Action(Action::Output(message))), - )); + self.resubscribe(program); } pub fn perform(&mut self, program: &P, action: Action) { @@ -152,11 +146,35 @@ impl Emulator

{ self.cache = Some(user_interface.into_cache()); - for message in messages { - self.update(program, message); - } + let task = Task::batch( + messages + .into_iter() + .map(|message| program.update(&mut self.state, message)), + ); - self.runtime.send(Event::Ready); + // TODO: Configurable + self.wait_for(task); + + self.resubscribe(program); + } + + pub fn wait_for(&mut self, task: Task) { + if let Some(stream) = task::into_stream(task) { + self.runtime.run( + stream + .map(Event::Action) + .chain(stream::once(async { Event::Ready })) + .boxed(), + ); + } + } + + pub fn resubscribe(&mut self, program: &P) { + self.runtime.track(subscription::into_recipes( + program + .subscription(&self.state) + .map(|message| Event::Action(Action::Output(message))), + )); } pub fn view( From 927d5b7cba18f8c2f3e8cde26bd11dcb43e7720a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 4 Jun 2025 13:58:44 +0200 Subject: [PATCH 11/83] Perform widget operations in `Emulator` --- test/src/emulator.rs | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/test/src/emulator.rs b/test/src/emulator.rs index d6b5f939..592b866b 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -2,6 +2,7 @@ use crate::Instruction; use crate::core; use crate::core::mouse; use crate::core::renderer; +use crate::core::widget::operation; use crate::core::window; use crate::core::{Element, Size}; use crate::program::Program; @@ -91,8 +92,29 @@ impl Emulator

{ Action::LoadFont { .. } => { // TODO } - Action::Widget(_operation) => { - // TODO + Action::Widget(operation) => { + let mut user_interface = UserInterface::build( + program.view(&self.state, self.window), + self.size, + self.cache.take().unwrap(), + &mut self.renderer, + ); + + let mut operation = Some(operation); + + while let Some(mut current) = operation.take() { + user_interface.operate(&self.renderer, &mut current); + + match current.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(()) => {} + operation::Outcome::Chain(next) => { + operation = Some(next); + } + } + } + + self.cache = Some(user_interface.into_cache()); } Action::Clipboard(action) => { // TODO From 73f5569f28ac6dfa64dedc54b5d2f9d176e2a951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 4 Jun 2025 19:17:11 +0200 Subject: [PATCH 12/83] Add `program::Preset` and `emulator::Mode` --- Cargo.lock | 1 + Cargo.toml | 6 ++- devtools/src/lib.rs | 2 +- devtools/src/tester.rs | 91 +++++++++++++++++++++++++++++++++---- devtools/src/tester/null.rs | 2 +- examples/todos/Cargo.toml | 4 ++ examples/todos/src/main.rs | 43 ++++++++++++++++-- program/Cargo.toml | 1 + program/src/lib.rs | 11 +++++ program/src/preset.rs | 42 +++++++++++++++++ src/application.rs | 41 +++++++++++++++++ src/application/timed.rs | 3 ++ src/lib.rs | 2 +- test/Cargo.toml | 2 + test/src/emulator.rs | 81 ++++++++++++++++++++++++++------- widget/src/combo_box.rs | 11 +++-- winit/Cargo.toml | 1 - 17 files changed, 305 insertions(+), 39 deletions(-) create mode 100644 program/src/preset.rs diff --git a/Cargo.lock b/Cargo.lock index 9cb1393b..38d0f64b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2400,6 +2400,7 @@ dependencies = [ "iced_devtools", "iced_futures", "iced_highlighter", + "iced_program", "iced_renderer", "iced_runtime", "iced_wgpu", diff --git a/Cargo.toml b/Cargo.toml index 2d9beb5f..dbfe4cc5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,9 @@ debug = ["iced_winit/debug", "iced_devtools"] # Enables time-travel debugging (very experimental!) time-travel = ["debug", "iced_devtools/time-travel"] # Enables the tester developer tool for recording and playing tests (press F12) -tester = ["debug", "iced_devtools/tester"] +tester = ["debug", "test", "iced_devtools/tester"] +# Enables testing features (e.g. application presets) +test = ["iced_program/test"] # Enables the `thread-pool` futures executor as the `executor::Default` on native platforms thread-pool = ["iced_futures/thread-pool"] # Enables `tokio` as the `executor::Default` on native platforms @@ -80,10 +82,10 @@ sipper = ["iced_runtime/sipper"] iced_debug.workspace = true iced_core.workspace = true iced_futures.workspace = true +iced_program.workspace = true iced_renderer.workspace = true iced_runtime.workspace = true iced_widget.workspace = true -iced_winit.features = ["program"] iced_winit.workspace = true iced_devtools.workspace = true diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index 3a663384..9a9123fb 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -197,7 +197,7 @@ where match &self.mode { Mode::Hidden => { self.mode = Mode::Open { - tester: Tester::new(), + tester: Tester::new(program), }; } Mode::Open { tester } if !tester.is_busy() => { diff --git a/devtools/src/tester.rs b/devtools/src/tester.rs index e5aa441b..5831a13f 100644 --- a/devtools/src/tester.rs +++ b/devtools/src/tester.rs @@ -18,12 +18,15 @@ use crate::test::emulator; use crate::test::instruction; use crate::test::{Emulator, Instruction}; use crate::widget::{ - button, center, column, container, monospace, row, scrollable, text, - text_input, themer, + button, center, column, combo_box, container, monospace, pick_list, row, + scrollable, text, text_input, themer, }; pub struct Tester { viewport: Size, + mode: emulator::Mode, + presets: combo_box::State, + preset: Option, instructions: Vec, state: State

, } @@ -42,6 +45,8 @@ enum State { #[derive(Debug, Clone)] pub enum Message { ChangeViewport(Size), + ModeSelected(emulator::Mode), + PresetSelected(String), Record, Stop, Play, @@ -55,9 +60,19 @@ pub enum Tick { } impl Tester

{ - pub fn new() -> Self { + pub fn new(program: &P) -> Self { Self { + mode: emulator::Mode::default(), viewport: Size::new(512.0, 512.0), + presets: combo_box::State::new( + program + .presets() + .iter() + .map(program::Preset::name) + .map(str::to_owned) + .collect(), + ), + preset: None, instructions: Vec::new(), state: State::Idle, } @@ -78,10 +93,25 @@ impl Tester

{ Task::none() } + Message::ModeSelected(mode) => { + self.mode = mode; + + Task::none() + } + Message::PresetSelected(preset) => { + self.preset = Some(preset); + + Task::none() + } Message::Record => { self.instructions.clear(); - let (state, task) = program.boot(); + let (state, task) = if let Some(preset) = self.preset(program) { + preset.boot() + } else { + program.boot() + }; + self.state = State::Recording { state }; task.map(Tick::Program) @@ -93,7 +123,14 @@ impl Tester

{ } Message::Play => { let (sender, receiver) = mpsc::channel(1); - let emulator = Emulator::new(program, self.viewport, sender); + + let emulator = Emulator::with_preset( + sender, + program, + self.mode, + self.viewport, + self.preset(program), + ); self.state = State::Playing { emulator, @@ -105,6 +142,18 @@ impl Tester

{ } } + fn preset<'a>( + &self, + program: &'a P, + ) -> Option<&'a program::Preset> { + self.preset.as_ref().and_then(|preset| { + program + .presets() + .iter() + .find(|candidate| candidate.name() == preset) + }) + } + pub fn tick(&mut self, program: &P, tick: Tick

) -> Task> { match tick { Tick::Program(message) => { @@ -267,8 +316,25 @@ impl Tester

{ .spacing(10) .align_y(Center); + let preset = combo_box( + &self.presets, + "Default Preset", + self.preset.as_ref(), + Message::PresetSelected, + ) + .size(14) + .width(Fill); + + let mode = pick_list( + emulator::Mode::ALL, + Some(self.mode), + Message::ModeSelected, + ) + .text_size(14) + .width(Fill); + let player = { - let events = container(if self.instructions.is_empty() { + let instructions = container(if self.instructions.is_empty() { Element::from(center( monospace("No instructions recorded yet!") .size(14) @@ -337,12 +403,17 @@ impl Tester

{ .spacing(10) }; - column![events, controls].spacing(10).align_x(Center) + column![instructions, controls].spacing(10).align_x(Center) }; - column![labeled("Viewport", viewport), labeled("Tester", player)] - .spacing(10) - .into() + column![ + labeled("Viewport", viewport), + labeled("Mode", mode), + labeled("Preset", preset), + labeled("Instructions", player) + ] + .spacing(10) + .into() } } diff --git a/devtools/src/tester/null.rs b/devtools/src/tester/null.rs index 52d7ae4b..c2a4b4f3 100644 --- a/devtools/src/tester/null.rs +++ b/devtools/src/tester/null.rs @@ -20,7 +20,7 @@ pub struct Tick { } impl Tester

{ - pub fn new() -> Self { + pub fn new(_program: &P) -> Self { Self { _type: PhantomData } } diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml index 5e16a2ac..9869a2c5 100644 --- a/examples/todos/Cargo.toml +++ b/examples/todos/Cargo.toml @@ -5,6 +5,10 @@ authors = ["Héctor Ramón Jiménez "] edition = "2024" publish = false +[features] +test = ["iced/test"] +tester = ["test", "iced/tester"] + [dependencies] iced.workspace = true iced.features = ["tokio", "debug", "time-travel"] diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 8a4bc2eb..dc6dabbf 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -15,12 +15,16 @@ pub fn main() -> iced::Result { #[cfg(not(target_arch = "wasm32"))] tracing_subscriber::fmt::init(); - iced::application(Todos::new, Todos::update, Todos::view) + let todos = iced::application(Todos::new, Todos::update, Todos::view) .subscription(Todos::subscription) .title(Todos::title) .font(Todos::ICON_FONT) - .window_size((500.0, 800.0)) - .run() + .window_size((500.0, 800.0)); + + #[cfg(feature = "test")] + let todos = todos.presets(presets()); + + todos.run() } #[derive(Debug)] @@ -572,6 +576,39 @@ impl SavedState { } } +#[cfg(feature = "test")] +fn presets() -> impl Iterator> +{ + use iced::application::Preset; + + [ + Preset::new("Empty", || { + ( + Todos::Loading, + Command::done(Message::Loaded(Err(LoadError::File))), + ) + }), + Preset::new("Basic", || { + ( + Todos::Loaded(State { + input_value: "Bake an apple pie".to_owned(), + filter: Filter::All, + tasks: vec![Task { + id: Uuid::new_v4(), + description: "Create the universe".to_owned(), + completed: false, + state: TaskState::Idle, + }], + dirty: false, + saving: false, + }), + Command::none(), + ) + }), + ] + .into_iter() +} + #[cfg(test)] mod tests { use super::*; diff --git a/program/Cargo.toml b/program/Cargo.toml index 7aa6414d..4b49f464 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -15,6 +15,7 @@ workspace = true [features] time-travel = [] +test = [] [dependencies] iced_graphics.workspace = true diff --git a/program/src/lib.rs b/program/src/lib.rs index c75b02a1..04583bd3 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -4,6 +4,12 @@ pub use iced_runtime as runtime; pub use iced_runtime::core; pub use iced_runtime::futures; +#[cfg(feature = "test")] +mod preset; + +#[cfg(feature = "test")] +pub use preset::Preset; + use crate::core::renderer; use crate::core::text; use crate::core::theme; @@ -100,6 +106,11 @@ pub trait Program: Sized { fn scale_factor(&self, _state: &Self::State, _window: window::Id) -> f64 { 1.0 } + + #[cfg(feature = "test")] + fn presets(&self) -> &[Preset] { + &[] + } } /// Decorates a [`Program`] with the given title function. diff --git a/program/src/preset.rs b/program/src/preset.rs new file mode 100644 index 00000000..863278ab --- /dev/null +++ b/program/src/preset.rs @@ -0,0 +1,42 @@ +use crate::runtime::Task; + +use std::borrow::Cow; +use std::fmt; + +/// A specific boot strategy for a [`Program`]. +pub struct Preset { + name: Cow<'static, str>, + boot: Box (State, Task)>, +} + +impl Preset { + /// Creates a new [`Preset`] with the given name and boot strategy. + pub fn new( + name: impl Into>, + boot: impl Fn() -> (State, Task) + 'static, + ) -> Self { + Self { + name: name.into(), + boot: Box::new(boot), + } + } + + /// Returns the name of the [`Preset`]. + pub fn name(&self) -> &str { + &self.name + } + + /// Boots the [`Preset`], returning the initial [`Program`] state and + /// a [`Task`] for concurrent booting. + pub fn boot(&self) -> (State, Task) { + (self.boot)() + } +} + +impl fmt::Debug for Preset { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Preset") + .field("name", &self.name) + .finish_non_exhaustive() + } +} diff --git a/src/application.rs b/src/application.rs index 8c8917fc..459f3a68 100644 --- a/src/application.rs +++ b/src/application.rs @@ -42,6 +42,8 @@ use std::borrow::Cow; pub mod timed; +#[cfg(feature = "test")] +pub use program::Preset; pub use timed::timed; /// Creates an iced [`Application`] given its boot, update, and view logic. @@ -154,6 +156,9 @@ where }, settings: Settings::default(), window: window::Settings::default(), + + #[cfg(feature = "test")] + presets: Vec::new(), } } @@ -169,6 +174,9 @@ pub struct Application { raw: P, settings: Settings, window: window::Settings, + + #[cfg(feature = "test")] + presets: Vec>, } impl Application

{ @@ -338,6 +346,8 @@ impl Application

{ }), settings: self.settings, window: self.window, + #[cfg(feature = "test")] + presets: self.presets, } } @@ -352,6 +362,8 @@ impl Application

{ raw: program::with_subscription(self.raw, f), settings: self.settings, window: self.window, + #[cfg(feature = "test")] + presets: self.presets, } } @@ -366,6 +378,8 @@ impl Application

{ raw: program::with_theme(self.raw, move |state, _window| f(state)), settings: self.settings, window: self.window, + #[cfg(feature = "test")] + presets: self.presets, } } @@ -380,6 +394,8 @@ impl Application

{ raw: program::with_style(self.raw, f), settings: self.settings, window: self.window, + #[cfg(feature = "test")] + presets: self.presets, } } @@ -396,6 +412,8 @@ impl Application

{ }), settings: self.settings, window: self.window, + #[cfg(feature = "test")] + presets: self.presets, } } @@ -412,6 +430,24 @@ impl Application

{ raw: program::with_executor::(self.raw), settings: self.settings, window: self.window, + #[cfg(feature = "test")] + presets: self.presets, + } + } + + /// Sets the boot presets of the [`Application`]. + /// + /// Presets can be used to override the default booting strategy + /// of your application during testing to create reproducible + /// environments. + #[cfg(feature = "test")] + pub fn presets( + self, + presets: impl IntoIterator>, + ) -> Self { + Self { + presets: presets.into_iter().collect(), + ..self } } } @@ -474,6 +510,11 @@ impl Program for Application

{ fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { self.raw.scale_factor(state, window) } + + #[cfg(feature = "test")] + fn presets(&self) -> &[Preset] { + &self.presets + } } /// The logic to initialize the `State` of some [`Application`]. diff --git a/src/application/timed.rs b/src/application/timed.rs index 5ca45ec4..18eba2b5 100644 --- a/src/application/timed.rs +++ b/src/application/timed.rs @@ -138,6 +138,9 @@ where }, settings: Settings::default(), window: window::Settings::default(), + + #[cfg(feature = "test")] + presets: Vec::new(), } } diff --git a/src/lib.rs b/src/lib.rs index e9ca3a04..b77f0b21 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -475,11 +475,11 @@ )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))] +use iced_program as program; use iced_widget::graphics; use iced_widget::renderer; use iced_winit as shell; use iced_winit::core; -use iced_winit::program; use iced_winit::runtime; pub use iced_futures::futures; diff --git a/test/Cargo.toml b/test/Cargo.toml index 132a913b..dd262ffc 100644 --- a/test/Cargo.toml +++ b/test/Cargo.toml @@ -15,7 +15,9 @@ workspace = true [dependencies] iced_runtime.workspace = true + iced_program.workspace = true +iced_program.features = ["test"] iced_renderer.workspace = true iced_renderer.features = ["fira-sans"] diff --git a/test/src/emulator.rs b/test/src/emulator.rs index 592b866b..98841e57 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -2,9 +2,10 @@ use crate::Instruction; use crate::core; use crate::core::mouse; use crate::core::renderer; -use crate::core::widget::operation; +use crate::core::widget; use crate::core::window; use crate::core::{Element, Size}; +use crate::program; use crate::program::Program; use crate::runtime::futures::futures::StreamExt; use crate::runtime::futures::futures::channel::mpsc; @@ -15,11 +16,14 @@ use crate::runtime::task; use crate::runtime::user_interface; use crate::runtime::{Action, Task, UserInterface}; +use std::fmt; + #[allow(missing_debug_implementations)] pub struct Emulator { state: P::State, runtime: Runtime>, Event

>, renderer: P::Renderer, + mode: Mode, size: Size, window: window::Id, cursor: mouse::Cursor, @@ -35,9 +39,20 @@ pub enum Event { impl Emulator

{ pub fn new( - program: &P, - size: Size, sender: mpsc::Sender>, + program: &P, + mode: Mode, + size: Size, + ) -> Emulator

{ + Self::with_preset(sender, program, mode, size, None) + } + + pub fn with_preset( + sender: mpsc::Sender>, + program: &P, + mode: Mode, + size: Size, + preset: Option<&program::Preset>, ) -> Emulator

{ use renderer::Headless; @@ -55,12 +70,18 @@ impl Emulator

{ .expect("Create emulator renderer"); let runtime = Runtime::new(executor, sender); - let (state, task) = program.boot(); + + let (state, task) = if let Some(preset) = preset { + preset.boot() + } else { + program.boot() + }; let mut emulator = Self { state, runtime, renderer, + mode, size, clipboard: Clipboard { content: None }, cursor: mouse::Cursor::Unavailable, @@ -68,8 +89,8 @@ impl Emulator

{ cache: Some(user_interface::Cache::default()), }; - // TODO: Configurable emulator.wait_for(task); + emulator.resubscribe(program); emulator } @@ -106,9 +127,9 @@ impl Emulator

{ user_interface.operate(&self.renderer, &mut current); match current.finish() { - operation::Outcome::None => {} - operation::Outcome::Some(()) => {} - operation::Outcome::Chain(next) => { + widget::operation::Outcome::None => {} + widget::operation::Outcome::Some(()) => {} + widget::operation::Outcome::Chain(next) => { operation = Some(next); } } @@ -174,20 +195,28 @@ impl Emulator

{ .map(|message| program.update(&mut self.state, message)), ); - // TODO: Configurable self.wait_for(task); - self.resubscribe(program); } pub fn wait_for(&mut self, task: Task) { if let Some(stream) = task::into_stream(task) { - self.runtime.run( - stream - .map(Event::Action) - .chain(stream::once(async { Event::Ready })) - .boxed(), - ); + match self.mode { + Mode::Patient => { + self.runtime.run( + stream + .map(Event::Action) + .chain(stream::once(async { Event::Ready })) + .boxed(), + ); + } + Mode::Impatient => { + self.runtime.run(stream.map(Event::Action).boxed()); + self.runtime.send(Event::Ready); + } + } + } else { + self.runtime.send(Event::Ready); } } @@ -211,6 +240,26 @@ impl Emulator

{ } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Mode { + Patient, + #[default] + Impatient, +} + +impl Mode { + pub const ALL: &[Self] = &[Self::Patient, Self::Impatient]; +} + +impl fmt::Display for Mode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Mode::Patient => f.write_str("Patient"), + Mode::Impatient => f.write_str("Impatient"), + } + } +} + struct Clipboard { content: Option, } diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 5bf01eac..27f3db56 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -64,8 +64,8 @@ use crate::core::text; use crate::core::time::Instant; use crate::core::widget::{self, Widget}; use crate::core::{ - Clipboard, Element, Event, Length, Padding, Rectangle, Shell, Size, Theme, - Vector, + Clipboard, Element, Event, Length, Padding, Pixels, Rectangle, Shell, Size, + Theme, Vector, }; use crate::overlay::menu; use crate::text::LineHeight; @@ -249,9 +249,12 @@ where } /// Sets the text sixe of the [`ComboBox`]. - pub fn size(mut self, size: f32) -> Self { + pub fn size(mut self, size: impl Into) -> Self { + let size = size.into(); + self.text_input = self.text_input.size(size); - self.size = Some(size); + self.size = Some(size.0); + self } diff --git a/winit/Cargo.toml b/winit/Cargo.toml index f2157978..8827d9bb 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -17,7 +17,6 @@ workspace = true default = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"] debug = ["iced_debug/enable"] system = ["sysinfo"] -program = [] x11 = ["winit/x11"] wayland = ["winit/wayland"] wayland-dlopen = ["winit/wayland-dlopen"] From e548372fe042a1f4aa911ce279e839fbc8f63c64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 4 Jun 2025 22:40:32 +0200 Subject: [PATCH 13/83] Add `Failed` and `Success` states to `tester` devtool --- Cargo.lock | 37 +++--- devtools/fonts/iced_devtools-icons.toml | 8 +- devtools/fonts/iced_devtools-icons.ttf | Bin 5888 -> 7132 bytes devtools/src/icon.rs | 56 +++++++++ devtools/src/tester.rs | 145 +++++++++++++++++++----- examples/todos/src/main.rs | 4 +- test/src/emulator.rs | 1 + 7 files changed, 198 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 38d0f64b..cbe3b805 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,9 +117,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anyhow" @@ -2342,9 +2342,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" +checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" dependencies = [ "base64 0.22.1", "bytes", @@ -4613,9 +4613,9 @@ dependencies = [ [[package]] name = "read-fonts" -version = "0.29.2" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f96bfbb7df43d34a2b7b8582fcbcb676ba02a763265cb90bc8aabfd62b57d64" +checksum = "04ca636dac446b5664bd16c069c00a9621806895b8bb02c2dc68542b23b8f25d" dependencies = [ "bytemuck", "font-types", @@ -5950,9 +5950,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc2d9e086a412a451384326f521c8123a99a466b329941a9403696bff9b0da2" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ "bitflags 2.9.1", "bytes", @@ -6938,13 +6938,13 @@ checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" [[package]] name = "windows-registry" -version = "0.4.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820" dependencies = [ + "windows-link", "windows-result 0.3.4", - "windows-strings 0.3.1", - "windows-targets 0.53.0", + "windows-strings 0.4.2", ] [[package]] @@ -6984,15 +6984,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-strings" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-strings" version = "0.4.2" @@ -7642,9 +7633,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.4.14" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" +checksum = "3e4a518c0ea2576f4da876349d7f67a7be489297cd77c2cf9e04c2e05fcd3974" dependencies = [ "zune-core", ] diff --git a/devtools/fonts/iced_devtools-icons.toml b/devtools/fonts/iced_devtools-icons.toml index 69c8d86d..9444ede4 100644 --- a/devtools/fonts/iced_devtools-icons.toml +++ b/devtools/fonts/iced_devtools-icons.toml @@ -3,6 +3,10 @@ module = "icon" [glyphs] play = "entypo-play" stop = "entypo-stop" +pause = "entypo-pause" record = "entypo-record" -import = "entypo-folder" -export = "entypo-floppy" +lightbulb = "fontawesome-lightbulb" +check = "entypo-check" +cancel = "entypo-cancel" +folder = "entypo-folder" +floppy = "entypo-floppy" diff --git a/devtools/fonts/iced_devtools-icons.ttf b/devtools/fonts/iced_devtools-icons.ttf index 179db4f20f63ee12518bfbe6f661c660b1a8fb58..a9c302594e70ecc73a200689d1148fe3e7e6cc76 100644 GIT binary patch delta 1823 zcmaJ?Uu=_A6hG&FfBJ{^>(_5z|94&2c5SmQW$C(3wpl8oC=T7ipKU;ZuIpB$W6d@e zX3R?BcwmWUIvxlh62pTDJS;#6F~p!*3^B&=VD>$&Hi z-#z!-bI(2J_EhX)`@Fw2eJx`Lz#afNk{!(ymm~Xjk-P}NOxdYPKjGBOA?l>z;>hS= z?Ad!HCrC7nWG0G)%Orb=>qiRH!#nTK)BtdI0Qlv6E;H1$TwbH8voxQ~6QJwj_LKZQ z$)Wt{%edoFv;vGbf9vN}rP4LbCsOW;A#HSN$WBduWs1 z;@HIG+|>C20MiwkKT#ad6=!aB-U9HXKmPjwUR&6MU3~CNxx8=|H<#uCNef^OxXP%R zxRDM!4?G+qT&9l$2%c%U+;A&=HBu9?J^iCx{x76rXQRW)N^N!ZBa^_$5)T51pd(S# zR2Lf9%0dg(iWKW;!tt2`9E9b?wP` z9a3jkEr?4>D6Y)TDse@Y=jLRY2AUL7=Dtzl0r}jy3Uu*OMS-M1T)9-aj`AW)aVEO( z2poWY>D>ZF7I(e*S~4CJO9Icj>DGA!{uJ;6^1_S`1(AL;Ad0gTd5-D1qvSKP0bmR> zfDC*4`(8<>UheF0J3^{r5Is#uzK7cr;b4G*)f)GqBN1Z(iRsijW_PqE+f#Iqc2!4+ z-KK7kSPDuk9MM*`huf+YV-XsrI3L3cCX>XvYIP>2H+YlH{oz!1cXuj`VXO5GT}i(~ zw2R>#TU-G@=Qf*N`jBf=EZ$;phfwE|BsUBAT^;eBd_Mnnl3Ddequ%8;%0{KJHo86L zbjG$vw=|cOn!UZfdquawp?qoSkyX;H{I;s z+Sh(06=Knjp=!35f8hM=37EkF9(X)Sx=G9tVk)v1Y{!=S)LdAhza~* ziZ=gF$w{w=)8q8GUG^H;LRL%(z36B{hgB7W^?@+uaTRmMwLYGV*n(~BlaD$Y z*S?5$bwzPyz35Z1S@p$-*prKEKWiZCZ|qqA@%eMr=X-eX9y!3u>*VBPa#8{x4865z z?&(hl$wQti6$SE*k`Ye<;wXcqf&QpXgC}y`tX&dte;S z!V;CsbqdchLdgqym=9{t9yJCEw8Dop8j-3`ClK<}jqCgvx7$dC!_iU!MS4Z_Xh0)w z0-=~WF_GgZCdZ0=v5=V-$8*`S@gX6b&t;E^+05~5u3#t}9m!7)o+u28!-cV8aatT6 OD-7kv;YOE!N%|MgA5hc) delta 554 zcmY*WO(;ZB6h8OeYmA9uArxwq8TpBgNG5+9l0xJ+@w^%H7W2$)W|E)$>})6+D_JOZ zW1-ZnCTYUL(%Oy%D+-jX*SY6>-}%0K?tAa9d82;Ev%Zqqyd`1-L^NYqx{S3> z)B>L(4vdAk2RxRu2b+6NhOF88&t>2-z}%1?li&-0JCrCCUYdKZ>h2@r2)Z_ddZ6~I zy_`rC0ymi;vi*D%SoJGSD?YyHdO=CTJ}4Y9blRjY;0=&2OJ9_far=R{fIaiN72Lmf zJ^`O#$e@hG;v><_F=nGpZ892^YpK>0QBh#`W0Iv4!^|(dhrVQDk2MUL#FUI?iQ82j zvI{S2lAtE=PCOcs_t2a2CBMG@R<@7X3N?yB$B`UIof=ctTwBm+@3@(={)8Ij#f4O; z!+pu0Af6}_H#Hz3IB0n-pU(HcQ$BwLLl0A{=YKPQ)Y6FyXK}yXg$AB~ODLa)pTgdd z)4#n+d`SbfQVYuab!p_F3fcx&eb$?^!b=6y7-xcIk%+}3Qpm8r() -> Text<'a, Theme, Renderer> +where + Theme: text::Catalog + 'a, + Renderer: program::Renderer, +{ + icon("\u{2715}") +} + +pub fn check<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer> +where + Theme: text::Catalog + 'a, + Renderer: program::Renderer, +{ + icon("\u{2713}") +} + +pub fn floppy<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer> +where + Theme: text::Catalog + 'a, + Renderer: program::Renderer, +{ + icon("\u{1F4BE}") +} + +pub fn folder<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer> +where + Theme: text::Catalog + 'a, + Renderer: program::Renderer, +{ + icon("\u{1F4C1}") +} + +pub fn lightbulb<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer> +where + Theme: text::Catalog + 'a, + Renderer: program::Renderer, +{ + icon("\u{F0EB}") +} + +pub fn pause<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer> +where + Theme: text::Catalog + 'a, + Renderer: program::Renderer, +{ + icon("\u{2389}") +} + pub fn play<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer> where Theme: text::Catalog + 'a, @@ -29,6 +77,14 @@ where icon("\u{25A0}") } +pub fn tape<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer> +where + Theme: text::Catalog + 'a, + Renderer: program::Renderer, +{ + icon("\u{2707}") +} + fn icon<'a, Theme, Renderer>(codepoint: &'a str) -> Text<'a, Theme, Renderer> where Theme: text::Catalog + 'a, diff --git a/devtools/src/tester.rs b/devtools/src/tester.rs index 5831a13f..83358317 100644 --- a/devtools/src/tester.rs +++ b/devtools/src/tester.rs @@ -36,12 +36,22 @@ enum State { Recording { state: P::State, }, + Ready { + state: P::State, + }, Playing { emulator: Emulator

, current: usize, + outcome: Outcome, }, } +enum Outcome { + Running, + Failed, + Success, +} + #[derive(Debug, Clone)] pub enum Message { ChangeViewport(Size), @@ -83,7 +93,14 @@ impl Tester

{ } pub fn is_busy(&self) -> bool { - matches!(self.state, State::Recording { .. } | State::Playing { .. }) + matches!( + self.state, + State::Recording { .. } + | State::Playing { + outcome: Outcome::Running, + .. + } + ) } pub fn update(&mut self, program: &P, message: Message) -> Task> { @@ -117,7 +134,13 @@ impl Tester

{ task.map(Tick::Program) } Message::Stop => { - self.state = State::Idle; + let State::Recording { state } = + std::mem::replace(&mut self.state, State::Idle) + else { + return Task::none(); + }; + + self.state = State::Ready { state }; Task::none() } @@ -135,6 +158,7 @@ impl Tester

{ self.state = State::Playing { emulator, current: 0, + outcome: Outcome::Running, }; Task::run(receiver, Tick::Emulator) @@ -190,7 +214,11 @@ impl Tester

{ Task::none() } Tick::Emulator(event) => { - let State::Playing { emulator, current } = &mut self.state + let State::Playing { + emulator, + current, + outcome, + } = &mut self.state else { return Task::none(); }; @@ -199,6 +227,9 @@ impl Tester

{ emulator::Event::Action(action) => { emulator.perform(program, action); } + emulator::Event::Failed => { + *outcome = Outcome::Failed; + } emulator::Event::Ready => { if let Some(instruction) = self.instructions.get(*current).cloned() @@ -206,6 +237,10 @@ impl Tester

{ emulator.run(program, instruction); *current += 1; } + + if *current >= self.instructions.len() { + *outcome = Outcome::Success; + } } } @@ -216,7 +251,9 @@ impl Tester

{ pub fn subscription(&self, program: &P) -> Subscription> { match &self.state { - State::Idle | State::Playing { .. } => Subscription::none(), + State::Idle | State::Playing { .. } | State::Ready { .. } => { + Subscription::none() + } State::Recording { state } => { program.subscription(state).map(Tick::Program) } @@ -230,22 +267,44 @@ impl Tester

{ current: impl FnOnce() -> Element<'a, T, Theme, P::Renderer>, emulate: impl Fn(Tick

) -> T + 'a, ) -> Element<'a, T, Theme, P::Renderer> { - let status = match &self.state { - State::Idle => monospace("Idle").style(|theme| text::Style { - color: Some( - theme.extended_palette().background.strongest.color, - ), - }), - State::Recording { .. } => { - monospace("Recording").style(|theme| text::Style { - color: Some(theme.palette().danger), + let status = { + let (icon, label) = match &self.state { + State::Idle => (text(""), "Idle"), + State::Recording { .. } => (icon::record(), "Recording"), + State::Ready { .. } => (icon::lightbulb(), "Ready"), + State::Playing { outcome, .. } => match outcome { + Outcome::Running => (icon::play(), "Playing"), + Outcome::Failed => (icon::cancel(), "Failed"), + Outcome::Success => (icon::check(), "Success"), + }, + }; + + container(row![icon.size(14), label].align_y(Center).spacing(8)) + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + + container::Style { + text_color: Some(match &self.state { + State::Idle => palette.background.strongest.color, + State::Recording { .. } => { + palette.danger.base.color + } + State::Ready { .. } => palette.warning.base.color, + State::Playing { outcome, .. } => match outcome { + Outcome::Running => theme.palette().primary, + Outcome::Failed => theme.palette().danger, + Outcome::Success => { + theme + .extended_palette() + .success + .strong + .color + } + }, + }), + ..container::Style::default() + } }) - } - State::Playing { .. } => { - monospace("Playing").style(|theme| text::Style { - color: Some(theme.palette().primary), - }) - } }; let viewport = container( @@ -263,6 +322,13 @@ impl Tester

{ ) .map(emulate) } + State::Ready { state } => { + let theme = program.theme(state, window); + let view = + program.view(state, window).map(Tick::Program); + + Element::from(themer(theme, view)).map(emulate) + } State::Playing { emulator, .. } => { let theme = emulator.theme(program); let view = emulator.view(program).map(Tick::Program); @@ -285,7 +351,12 @@ impl Tester

{ border: border::width(2.0).color(match &self.state { State::Idle => palette.background.strongest.color, State::Recording { .. } => palette.danger.base.color, - State::Playing { .. } => palette.primary.base.color, + State::Ready { .. } => palette.warning.weak.color, + State::Playing { outcome, .. } => match outcome { + Outcome::Running => palette.primary.base.color, + Outcome::Failed => palette.danger.strong.color, + Outcome::Success => palette.success.strong.color, + }, }), ..container::Style::default() } @@ -305,7 +376,7 @@ impl Tester

{ width: width.parse().unwrap_or(self.viewport.width), ..self.viewport })), - monospace("x"), + monospace("x").size(14), text_input("Height", &self.viewport.height.to_string()) .size(14) .on_input(|height| Message::ChangeViewport(Size { @@ -318,7 +389,7 @@ impl Tester

{ let preset = combo_box( &self.presets, - "Default Preset", + "Default", self.preset.as_ref(), Message::PresetSelected, ) @@ -349,9 +420,32 @@ impl Tester

{ .size(10) .style(move |theme: &Theme| text::Style { color: match &self.state { - State::Playing { current, .. } => { + State::Playing { + current, + outcome, + .. + } => { if *current == i { - Some(theme.palette().primary) + Some(match outcome { + Outcome::Running => { + theme.palette().primary + } + + Outcome::Failed => { + theme + .extended_palette() + .danger + .strong + .color + } + Outcome::Success => { + theme + .extended_palette() + .success + .strong + .color + } + }) } else if *current > i { Some( theme @@ -394,8 +488,7 @@ impl Tester

{ } else { button(icon::record().size(14).width(Fill).center()) .on_press_maybe( - matches!(self.state, State::Idle) - .then_some(Message::Record), + (!self.is_busy()).then_some(Message::Record), ) .style(button::danger) } diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index dc6dabbf..2eb65157 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -588,10 +588,10 @@ fn presets() -> impl Iterator> Command::done(Message::Loaded(Err(LoadError::File))), ) }), - Preset::new("Basic", || { + Preset::new("Carl Sagan", || { ( Todos::Loaded(State { - input_value: "Bake an apple pie".to_owned(), + input_value: "Make an apple pie".to_owned(), filter: Filter::All, tasks: vec![Task { id: Uuid::new_v4(), diff --git a/test/src/emulator.rs b/test/src/emulator.rs index 98841e57..fa1c4b3d 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -34,6 +34,7 @@ pub struct Emulator { #[allow(missing_debug_implementations)] pub enum Event { Action(Action), + Failed, Ready, } From 1fd6980f91510363f3288b0b306aa5ab43d31598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 5 Jun 2025 00:23:36 +0200 Subject: [PATCH 14/83] Implement import/export for `tester` devtool --- Cargo.lock | 149 ++++++++++++++++++++++++---- Cargo.toml | 1 + devtools/Cargo.toml | 5 +- devtools/src/tester.rs | 123 +++++++++++++++++++---- examples/todos/tests/carl_sagan.ice | 8 ++ test/src/instruction.rs | 46 ++++++++- 6 files changed, 287 insertions(+), 45 deletions(-) create mode 100644 examples/todos/tests/carl_sagan.ice diff --git a/Cargo.lock b/Cargo.lock index cbe3b805..fe774c93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -205,6 +205,25 @@ dependencies = [ "zbus", ] +[[package]] +name = "ashpd" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.1", + "raw-window-handle 0.6.2", + "serde", + "serde_repr", + "url", + "zbus", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -585,6 +604,15 @@ dependencies = [ "objc2 0.5.2", ] +[[package]] +name = "block2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" +dependencies = [ + "objc2 0.6.1", +] + [[package]] name = "blocking" version = "1.6.1" @@ -850,7 +878,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b7f4aaa047ba3c3630b080bb9860894732ff23e2aee290a418909aa6d5df38f" dependencies = [ "objc2 0.5.2", - "objc2-app-kit", + "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", ] @@ -1207,7 +1235,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18e1a09f280e29a8b00bc7e81eca5ac87dca0575639c9422a5fa25a07bb884b8" dependencies = [ - "ashpd", + "ashpd 0.10.3", "async-std", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -1273,6 +1301,28 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "dispatch2" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a0d569e003ff27784e0e14e4a594048698e0c0f0b66cabcb51511be55a7caa0" +dependencies = [ + "bitflags 2.9.1", + "block2 0.6.1", + "libc", + "objc2 0.6.1", +] + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.6.1", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1365,7 +1415,7 @@ name = "editor" version = "0.1.0" dependencies = [ "iced", - "rfd", + "rfd 0.13.0", "tokio", ] @@ -2461,6 +2511,7 @@ dependencies = [ "iced_test", "iced_widget", "log", + "rfd 0.15.3", ] [[package]] @@ -3646,7 +3697,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ "bitflags 2.9.1", - "block2", + "block2 0.5.1", "libc", "objc2 0.5.2", "objc2-core-data", @@ -3655,6 +3706,18 @@ dependencies = [ "objc2-quartz-core", ] +[[package]] +name = "objc2-app-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" +dependencies = [ + "bitflags 2.9.1", + "block2 0.6.1", + "objc2 0.6.1", + "objc2-foundation 0.3.1", +] + [[package]] name = "objc2-cloud-kit" version = "0.2.2" @@ -3662,7 +3725,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ "bitflags 2.9.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", "objc2-foundation 0.2.2", @@ -3674,7 +3737,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -3686,18 +3749,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ "bitflags 2.9.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-core-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +dependencies = [ + "bitflags 2.9.1", + "dispatch2 0.3.0", + "objc2 0.6.1", +] + [[package]] name = "objc2-core-image" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", "objc2-metal", @@ -3709,7 +3783,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-contacts", "objc2-foundation 0.2.2", @@ -3728,7 +3802,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ "bitflags 2.9.1", - "block2", + "block2 0.5.1", "dispatch", "libc", "objc2 0.5.2", @@ -3742,6 +3816,7 @@ checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" dependencies = [ "bitflags 2.9.1", "objc2 0.6.1", + "objc2-core-foundation", ] [[package]] @@ -3750,9 +3825,9 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", - "objc2-app-kit", + "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", ] @@ -3763,7 +3838,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ "bitflags 2.9.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -3775,7 +3850,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ "bitflags 2.9.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", "objc2-metal", @@ -3798,7 +3873,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ "bitflags 2.9.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-cloud-kit", "objc2-core-data", @@ -3818,7 +3893,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -3830,7 +3905,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ "bitflags 2.9.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", "objc2-foundation 0.2.2", @@ -4291,6 +4366,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + [[package]] name = "potential_utf" version = "0.1.2" @@ -4769,6 +4850,30 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rfd" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80c844748fdc82aae252ee4594a89b6e7ebef1063de7951545564cbc4e57075d" +dependencies = [ + "ashpd 0.11.0", + "block2 0.6.1", + "dispatch2 0.2.0", + "js-sys", + "log", + "objc2 0.6.1", + "objc2-app-kit 0.3.1", + "objc2-core-foundation", + "objc2-foundation 0.3.1", + "pollster", + "raw-window-handle 0.6.2", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + [[package]] name = "rgb" version = "0.8.50" @@ -6195,6 +6300,12 @@ dependencies = [ "iced", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "usvg" version = "0.42.0" @@ -7280,7 +7391,7 @@ dependencies = [ "android-activity", "atomic-waker", "bitflags 2.9.1", - "block2", + "block2 0.5.1", "bytemuck", "calloop", "cfg_aliases", @@ -7294,7 +7405,7 @@ dependencies = [ "memmap2", "ndk", "objc2 0.5.2", - "objc2-app-kit", + "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", "objc2-ui-kit", "orbclient", diff --git a/Cargo.toml b/Cargo.toml index dbfe4cc5..54c10460 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -193,6 +193,7 @@ pulldown-cmark = "0.12" qrcode = { version = "0.13", default-features = false } raw-window-handle = "0.6" resvg = "0.42" +rfd = "0.15" rustc-hash = "2.0" serde = "1.0" semver = "1.0" diff --git a/devtools/Cargo.toml b/devtools/Cargo.toml index 98f83745..8e50978b 100644 --- a/devtools/Cargo.toml +++ b/devtools/Cargo.toml @@ -15,7 +15,7 @@ workspace = true [features] time-travel = ["iced_program/time-travel"] -tester = ["dep:iced_test"] +tester = ["dep:iced_test", "dep:rfd"] [dependencies] iced_debug.workspace = true @@ -25,3 +25,6 @@ log.workspace = true iced_test.workspace = true iced_test.optional = true + +rfd.workspace = true +rfd.optional = true diff --git a/devtools/src/tester.rs b/devtools/src/tester.rs index 83358317..ac52b5c4 100644 --- a/devtools/src/tester.rs +++ b/devtools/src/tester.rs @@ -9,6 +9,7 @@ use crate::core::alignment::Horizontal::Right; use crate::core::border; use crate::core::window; use crate::core::{Element, Event, Size, Theme}; +use crate::executor; use crate::futures::Subscription; use crate::futures::futures::channel::mpsc; use crate::icon; @@ -60,10 +61,14 @@ pub enum Message { Record, Stop, Play, + Import, + Export, + Imported(Option), } #[allow(missing_debug_implementations)] pub enum Tick { + Tester(Message), Program(P::Message), Recorder(Event), Emulator(emulator::Event

), @@ -163,6 +168,67 @@ impl Tester

{ Task::run(receiver, Tick::Emulator) } + Message::Import => { + use std::fs; + + let import = rfd::AsyncFileDialog::new() + .add_filter("ice", &["ice"]) + .pick_file(); + + Task::future(import) + .and_then(|file| { + executor::spawn_blocking(move |mut sender| { + let _ = sender + .try_send(fs::read_to_string(file.path()).ok()); + }) + }) + .map(Message::Imported) + .map(Tick::Tester) + } + Message::Export => { + use std::fs; + use std::thread; + + let test: Vec<_> = self + .instructions + .iter() + .map(Instruction::to_string) + .collect(); + + let export = rfd::AsyncFileDialog::new() + .add_filter("ice", &["ice"]) + .save_file(); + + Task::future(async move { + let Some(file) = export.await else { + return; + }; + + let _ = thread::spawn(move || { + fs::write(file.path(), test.join("\n")) + }); + }) + .discard() + } + Message::Imported(instructions) => { + let Some(instructions) = instructions else { + return Task::none(); + }; + + let instructions: Result, _> = + instructions.lines().map(Instruction::parse).collect(); + + match instructions { + Ok(instructions) => { + self.instructions = instructions; + } + Err(error) => { + log::error!("{error}"); + } + } + + Task::none() + } } } @@ -180,6 +246,7 @@ impl Tester

{ pub fn tick(&mut self, program: &P, tick: Tick

) -> Task> { match tick { + Tick::Tester(message) => self.update(program, message), Tick::Program(message) => { let State::Recording { state } = &mut self.state else { return Task::none(); @@ -473,29 +540,43 @@ impl Tester

{ .height(Fill) .padding(5); - let controls = { - row![ - button(icon::play().size(14).width(Fill).center()) - .on_press_maybe( - (!matches!(self.state, State::Recording { .. }) - && !self.instructions.is_empty()) - .then_some(Message::Play), - ), - if let State::Recording { .. } = &self.state { - button(icon::stop().size(14).width(Fill).center()) - .on_press(Message::Stop) - .style(button::success) - } else { - button(icon::record().size(14).width(Fill).center()) - .on_press_maybe( - (!self.is_busy()).then_some(Message::Record), - ) - .style(button::danger) - } - ] - .spacing(10) + let control = |icon: text::Text<'static, _, _>| { + button(icon.size(14).width(Fill).height(Fill).center()) }; + let play = control(icon::play()).on_press_maybe( + (!matches!(self.state, State::Recording { .. }) + && !self.instructions.is_empty()) + .then_some(Message::Play), + ); + + let record = if let State::Recording { .. } = &self.state { + control(icon::stop()) + .on_press(Message::Stop) + .style(button::success) + } else { + control(icon::record()) + .on_press_maybe( + (!self.is_busy()).then_some(Message::Record), + ) + .style(button::danger) + }; + + let import = control(icon::folder()) + .on_press_maybe((!self.is_busy()).then_some(Message::Import)) + .style(button::secondary); + + let export = control(icon::floppy()) + .on_press_maybe( + (!matches!(self.state, State::Recording { .. }) + && !self.instructions.is_empty()) + .then_some(Message::Export), + ) + .style(button::success); + + let controls = + row![import, export, play, record].height(30).spacing(10); + column![instructions, controls].spacing(10).align_x(Center) }; diff --git a/examples/todos/tests/carl_sagan.ice b/examples/todos/tests/carl_sagan.ice new file mode 100644 index 00000000..d536b9f1 --- /dev/null +++ b/examples/todos/tests/carl_sagan.ice @@ -0,0 +1,8 @@ +click left at (377.80, 236.50) +type "Create the universe" +type enter +type "Make an apple pie" +type enter +click left at (135.40, 351.70) +click left at (153.80, 398.10) +move cursor to (511.40, 448.50) \ No newline at end of file diff --git a/test/src/instruction.rs b/test/src/instruction.rs index d1323bcf..fd9ba2c9 100644 --- a/test/src/instruction.rs +++ b/test/src/instruction.rs @@ -10,6 +10,12 @@ pub enum Instruction { Interact(Interaction), } +impl Instruction { + pub fn parse(line: &str) -> Result { + parser::run(line) + } +} + impl fmt::Display for Instruction { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -331,15 +337,16 @@ mod format { } } -pub use parser::{Error as ParseError, run as parse}; +pub use parser::Error as ParseError; mod parser { use super::*; use nom::branch::alt; use nom::bytes::complete::tag; - use nom::character::complete::{char, multispace0}; - use nom::combinator::{map, opt}; + use nom::character::complete::{char, multispace0, satisfy}; + use nom::combinator::{map, opt, recognize}; + use nom::multi::many0; use nom::number::float; use nom::sequence::{delimited, preceded, separated_pair}; use nom::{Finish, IResult, Parser}; @@ -360,7 +367,11 @@ mod parser { } fn interaction(input: &str) -> IResult<&str, Interaction> { - map(mouse, Interaction::Mouse).parse(input) + alt(( + map(mouse, Interaction::Mouse), + map(keyboard, Interaction::Keyboard), + )) + .parse(input) } fn mouse(input: &str) -> IResult<&str, Mouse> { @@ -395,6 +406,33 @@ mod parser { .parse(input) } + fn keyboard(input: &str) -> IResult<&str, Keyboard> { + alt(( + map(preceded(tag("type "), string), Keyboard::Typewrite), + map(preceded(tag("type "), key), Keyboard::Type), + )) + .parse(input) + } + + fn key(input: &str) -> IResult<&str, Key> { + alt(( + map(tag("enter"), |_| Key::Enter), + map(tag("escape"), |_| Key::Escape), + map(tag("tab"), |_| Key::Tab), + map(tag("backspace"), |_| Key::Backspace), + )) + .parse(input) + } + + fn string(input: &str) -> IResult<&str, String> { + delimited( + char('"'), + map(recognize(many0(satisfy(|c| c != '"'))), str::to_owned), + char('"'), + ) + .parse(input) + } + fn point(input: &str) -> IResult<&str, Point> { let comma = (multispace0, char(','), multispace0); From 9cf97901981466cc61eb835fbdc699dae781eadd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 5 Jun 2025 00:35:53 +0200 Subject: [PATCH 15/83] Make methods of `DevTools` public Co-authored-by: ShootingStarDragons --- devtools/src/lib.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index 9a9123fb..d7d7e074 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -165,7 +165,7 @@ impl

DevTools

where P: Program + 'static, { - fn new(state: P::State) -> (Self, Task) { + pub fn new(state: P::State) -> (Self, Task) { ( Self { state, @@ -181,11 +181,11 @@ where ) } - fn title(&self, program: &P, window: window::Id) -> String { + pub fn title(&self, program: &P, window: window::Id) -> String { program.title(&self.state, window) } - fn update(&mut self, program: &P, event: Event

) -> Task> { + pub fn update(&mut self, program: &P, event: Event

) -> Task> { match event { Event::Message(message) => match message { Message::HideNotification => { @@ -339,7 +339,7 @@ where } } - fn view( + pub fn view( &self, program: &P, window: window::Id, @@ -434,7 +434,7 @@ where .into() } - fn subscription(&self, program: &P) -> Subscription> { + pub fn subscription(&self, program: &P) -> Subscription> { let subscription = match &self.mode { Mode::Open { tester } if !tester.is_idle() => { tester.subscription(program).map(Event::Tester) @@ -471,15 +471,15 @@ where Subscription::batch([subscription, hotkeys, commands]) } - fn theme(&self, program: &P, window: window::Id) -> P::Theme { + pub fn theme(&self, program: &P, window: window::Id) -> P::Theme { program.theme(self.state(), window) } - fn style(&self, program: &P, theme: &P::Theme) -> theme::Style { + pub fn style(&self, program: &P, theme: &P::Theme) -> theme::Style { program.style(self.state(), theme) } - fn scale_factor(&self, program: &P, window: window::Id) -> f64 { + pub fn scale_factor(&self, program: &P, window: window::Id) -> f64 { if let Mode::Open { .. } = &self.mode { 1.0 } else { @@ -487,7 +487,7 @@ where } } - fn state(&self) -> &P::State { + pub fn state(&self) -> &P::State { self.time_machine.state().unwrap_or(&self.state) } } From 76213a55f5eb0424dc89388fac02dfd9f3e2e858 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 5 Jun 2025 00:52:22 +0200 Subject: [PATCH 16/83] Update `thiserror` to `2.0` --- Cargo.lock | 22 +++++++++++----------- Cargo.toml | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fe774c93..a06bff0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -783,7 +783,7 @@ dependencies = [ "log", "reqwest", "serde", - "thiserror 1.0.69", + "thiserror 2.0.12", "tokio", "tracing-subscriber", "webbrowser", @@ -2457,7 +2457,7 @@ dependencies = [ "iced_widget", "iced_winit", "image", - "thiserror 1.0.69", + "thiserror 2.0.12", ] [[package]] @@ -2470,7 +2470,7 @@ dependencies = [ "log", "semver", "serde", - "thiserror 1.0.69", + "thiserror 2.0.12", "tokio", ] @@ -2488,7 +2488,7 @@ dependencies = [ "rustc-hash 2.1.1", "serde", "smol_str", - "thiserror 1.0.69", + "thiserror 2.0.12", "web-time", ] @@ -2544,7 +2544,7 @@ dependencies = [ "lyon_path", "raw-window-handle 0.6.2", "rustc-hash 2.1.1", - "thiserror 1.0.69", + "thiserror 2.0.12", "unicode-segmentation", ] @@ -2572,7 +2572,7 @@ dependencies = [ "iced_tiny_skia", "iced_wgpu", "log", - "thiserror 1.0.69", + "thiserror 2.0.12", ] [[package]] @@ -2585,7 +2585,7 @@ dependencies = [ "iced_futures", "raw-window-handle 0.6.2", "sipper", - "thiserror 1.0.69", + "thiserror 2.0.12", ] [[package]] @@ -2598,7 +2598,7 @@ dependencies = [ "nom 8.0.0", "png", "sha2", - "thiserror 1.0.69", + "thiserror 2.0.12", ] [[package]] @@ -2633,7 +2633,7 @@ dependencies = [ "lyon", "resvg", "rustc-hash 2.1.1", - "thiserror 1.0.69", + "thiserror 2.0.12", "wgpu", ] @@ -2650,7 +2650,7 @@ dependencies = [ "pulldown-cmark", "qrcode", "rustc-hash 2.1.1", - "thiserror 1.0.69", + "thiserror 2.0.12", "unicode-segmentation", "url", ] @@ -2664,7 +2664,7 @@ dependencies = [ "log", "rustc-hash 2.1.1", "sysinfo", - "thiserror 1.0.69", + "thiserror 2.0.12", "tracing", "wasm-bindgen-futures", "web-sys", diff --git a/Cargo.toml b/Cargo.toml index 54c10460..b38dfa2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -204,7 +204,7 @@ smol_str = "0.2" softbuffer = "0.4" syntect = "5.1" sysinfo = "0.33" -thiserror = "1.0" +thiserror = "2" tiny-skia = "0.11" tokio = "1.0" tracing = "0.1" From f878b59977c41269fe437b520d910ab1b2506fda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 5 Jun 2025 15:11:25 +0200 Subject: [PATCH 17/83] Implement instruction editor for `tester` --- devtools/fonts/iced_devtools-icons.toml | 3 + devtools/fonts/iced_devtools-icons.ttf | Bin 7132 -> 7880 bytes devtools/src/icon.rs | 24 +++++ devtools/src/tester.rs | 117 ++++++++++++++++++++++-- 4 files changed, 134 insertions(+), 10 deletions(-) diff --git a/devtools/fonts/iced_devtools-icons.toml b/devtools/fonts/iced_devtools-icons.toml index 9444ede4..44613c4e 100644 --- a/devtools/fonts/iced_devtools-icons.toml +++ b/devtools/fonts/iced_devtools-icons.toml @@ -10,3 +10,6 @@ check = "entypo-check" cancel = "entypo-cancel" folder = "entypo-folder" floppy = "entypo-floppy" +pencil = "entypo-pencil" +mouse_pointer = "fontawesome-mouse-pointer" +keyboard = "entypo-keyboard" diff --git a/devtools/fonts/iced_devtools-icons.ttf b/devtools/fonts/iced_devtools-icons.ttf index a9c302594e70ecc73a200689d1148fe3e7e6cc76..301915f493b618ef98f9e45888619871135cb52b 100644 GIT binary patch delta 1480 zcmZ`(T})eb6hG(Q_TIMKe$aAnFHp*bOKC?LU|T2?*otH^l4T;%sPh9$TMAO>4KM<_ z*cTTw5|rq&ePC`exTu+0dOf$@i(hSj=iJ}# z|8dUw|Ih8s`1OIenhGbAy#P1@057CwlA5hWTp@l1fH|33Tu?}-kIO~IF`;E=#yft0 ziMT>k$R_7CV~~Cp@|J9Fc|xkMX$LS7<%cIT$#nR}wK0II6D0IZlEC+~&k=u>c+2F> z!qQVMi~64=tdS7Qg0r=a`-+KmkRv*U!dq~&WQT;_Y#jeA})ps%Q#0DTu2Ur8P z{M1g~MpbW6;%7*gs3sOTHKd*kTncQpUJg2fo}jpY`|fdF2l#J&b&wG|k=FkEOIrg& zJDR`@9PnTPYdIeG{vd+|3Md%az@@S+Gc{y{Ax?>^=PIBrAV6~r&}I;zIt>`7-FmqK z3IIVz1r!Q`o(d=^1jPy{JnY}DfD$Rd-Q!RO<%GXnErU*+{IxPF>CvVXb&pvACwRaM z_0R|^1R)Ha5QRAO!7&(u^~6U$ggV99Smoy%EG$)>ZK-$II5Si05-cW@nK5PMYP;2B zmTDNc$L!7)RByA`WcDC>p1l9)!7)G&KAK4M_QqmeU7?Vo$g<0YkQhn~Jv4ai=s+7*pF(AjagJroXw+YSX=11*ZGsLf4{{sy01FW1-AdL^;O<#Bo3 z)m9Fi=m>C97$cHcU8v#Irsme}>R3po+n0w;CtUzUOOe z^A$fZEbgWw>2&FtEqN03}*irC7L!!=s2zY|}k~wR1>RCWs4ArYFR~tr&=fdcbI45$goyc=) zbI>^BcuzFqWtQAFtNgXb3HFquj+@@7a`3z)@x0T9Lbd2E{bu)>+i0}7QN`N4?8CB^ zt)e_O75)siy!rIb^JC8bU%|=lkqAFzkM7?0-{}W>mi>_(UH#0`X|@~VtNWIkQI=jX z<{kY2zaig8_=0*_A>7hS#!}dZJLtj=9Kkp6Dwdda<_>$5{jmJ+V25X+-2GJOFuVq@ z)BI02hpjKO1v@gxq6y8&(WzU|iZ-l50qy7j%XDUWJfECPo3-TW`HX3PA+MRVTymM4 z%cSyiX-><`rlxY{)MO?#&83pFsZ7qAo61fujGxYp+h_8$=&+WbnqA1uaTB?`rY&<5 S`CK|PSI(GH!%fA$?f47EeKzs{ delta 726 zcmZuuOK4L;6umR^UVcrJlG-YMFk;dOYAqE|T7ey3YxNuPr0)ngR!rDz1E%+fm&*a&iGn~0|&i%~XnfF7VFI0|| zt41#o8z!QgiL4sry zy$8U1vqnj>eSv4-1K{DiM%G;Z;@JWp#vxZ^u25W@S-wu>e*yor%$st)K3FF@bn)?z zaaOfP*ih$!ZHue+Z5k6*dSX3i(Q<_t!ig41w0EaqedxME8cjg&!W2Zu7oscCdf#fS zJJ!?Ow(b8Z?K*o4YiFu#XQCu=RMJQ%7r8-QD%3LhFH3UWniQ_`{eQ_`Ro#KQQ*^}v z@+pdj6x5yiRvn-w#qiP!s7|pS2RMhC+YWFd+j-pvUKsXm8+bic6+ax}>$L~^QC2(R zj`P|l?;ML%Hq&n=gYjJoif=0BYjUE0&)O|}O?(%AtJHC3+^76J1dO7u9a#K1e`>c_ z19lLX4Fg;DAC*l5G(ve=qIXPVFS*9=KvRFksf~ggXIjPBvG3&~kiClil+6eBi?FK&v{sQ4?o)7>4 diff --git a/devtools/src/icon.rs b/devtools/src/icon.rs index 63d32ecc..811b1d23 100644 --- a/devtools/src/icon.rs +++ b/devtools/src/icon.rs @@ -37,6 +37,14 @@ where icon("\u{1F4C1}") } +pub fn keyboard<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer> +where + Theme: text::Catalog + 'a, + Renderer: program::Renderer, +{ + icon("\u{2328}") +} + pub fn lightbulb<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer> where Theme: text::Catalog + 'a, @@ -45,6 +53,14 @@ where icon("\u{F0EB}") } +pub fn mouse_pointer<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer> +where + Theme: text::Catalog + 'a, + Renderer: program::Renderer, +{ + icon("\u{F245}") +} + pub fn pause<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer> where Theme: text::Catalog + 'a, @@ -53,6 +69,14 @@ where icon("\u{2389}") } +pub fn pencil<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer> +where + Theme: text::Catalog + 'a, + Renderer: program::Renderer, +{ + icon("\u{270E}") +} + pub fn play<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer> where Theme: text::Catalog + 'a, diff --git a/devtools/src/tester.rs b/devtools/src/tester.rs index ac52b5c4..b7824dbc 100644 --- a/devtools/src/tester.rs +++ b/devtools/src/tester.rs @@ -8,7 +8,7 @@ use crate::core::Length::Fill; use crate::core::alignment::Horizontal::Right; use crate::core::border; use crate::core::window; -use crate::core::{Element, Event, Size, Theme}; +use crate::core::{Element, Event, Font, Size, Theme}; use crate::executor; use crate::futures::Subscription; use crate::futures::futures::channel::mpsc; @@ -19,8 +19,8 @@ use crate::test::emulator; use crate::test::instruction; use crate::test::{Emulator, Instruction}; use crate::widget::{ - button, center, column, combo_box, container, monospace, pick_list, row, - scrollable, text, text_input, themer, + button, center, column, combo_box, container, horizontal_space, monospace, + pick_list, row, scrollable, text, text_editor, text_input, themer, }; pub struct Tester { @@ -30,6 +30,7 @@ pub struct Tester { preset: Option, instructions: Vec, state: State

, + edit: Option>, } enum State { @@ -64,6 +65,9 @@ pub enum Message { Import, Export, Imported(Option), + Edit, + Edited(text_editor::Action), + Confirm, } #[allow(missing_debug_implementations)] @@ -90,6 +94,7 @@ impl Tester

{ preset: None, instructions: Vec::new(), state: State::Idle, + edit: None, } } @@ -126,6 +131,7 @@ impl Tester

{ Task::none() } Message::Record => { + self.edit = None; self.instructions.clear(); let (state, task) = if let Some(preset) = self.preset(program) { @@ -150,6 +156,8 @@ impl Tester

{ Task::none() } Message::Play => { + self.confirm(); + let (sender, receiver) = mpsc::channel(1); let emulator = Emulator::with_preset( @@ -189,6 +197,8 @@ impl Tester

{ use std::fs; use std::thread; + self.confirm(); + let test: Vec<_> = self .instructions .iter() @@ -221,17 +231,60 @@ impl Tester

{ match instructions { Ok(instructions) => { self.instructions = instructions; + self.edit = None; } Err(error) => { log::error!("{error}"); } } + Task::none() + } + Message::Edit => { + if self.is_busy() { + return Task::none(); + } + + self.edit = Some(text_editor::Content::with_text( + &self + .instructions + .iter() + .map(Instruction::to_string) + .collect::>() + .join("\n"), + )); + + Task::none() + } + Message::Edited(action) => { + if let Some(edit) = &mut self.edit { + edit.perform(action); + } + + Task::none() + } + Message::Confirm => { + self.confirm(); + Task::none() } } } + fn confirm(&mut self) { + let Some(edit) = &mut self.edit else { + return; + }; + + self.instructions = edit + .lines() + .filter(|line| !line.text.trim().is_empty()) + .filter_map(|line| Instruction::parse(&line.text).ok()) + .collect(); + + self.edit = None; + } + fn preset<'a>( &self, program: &'a P, @@ -472,7 +525,14 @@ impl Tester

{ .width(Fill); let player = { - let instructions = container(if self.instructions.is_empty() { + let instructions = if let Some(edit) = &self.edit { + text_editor(edit) + .size(12) + .height(Fill) + .font(Font::MONOSPACE) + .on_action(Message::Edited) + .into() + } else if self.instructions.is_empty() { Element::from(center( monospace("No instructions recorded yet!") .size(14) @@ -497,7 +557,6 @@ impl Tester

{ Outcome::Running => { theme.palette().primary } - Outcome::Failed => { theme .extended_palette() @@ -533,12 +592,11 @@ impl Tester

{ )) .spacing(5), ) + .width(Fill) + .height(Fill) .spacing(5) .into() - }) - .width(Fill) - .height(Fill) - .padding(5); + }; let control = |icon: text::Text<'static, _, _>| { button(icon.size(14).width(Fill).height(Fill).center()) @@ -580,11 +638,27 @@ impl Tester

{ column![instructions, controls].spacing(10).align_x(Center) }; + let edit = if self.is_busy() { + Element::from(horizontal_space()) + } else if self.edit.is_none() { + button(icon::pencil().size(14)) + .padding(0) + .on_press(Message::Edit) + .style(button::text) + .into() + } else { + button(icon::check().size(14)) + .padding(0) + .on_press(Message::Confirm) + .style(button::text) + .into() + }; + column![ labeled("Viewport", viewport), labeled("Mode", mode), labeled("Preset", preset), - labeled("Instructions", player) + labeled_with("Instructions", edit, player) ] .spacing(10) .into() @@ -603,3 +677,26 @@ where .spacing(5) .into() } + +fn labeled_with<'a, Message, Renderer>( + fragment: impl text::IntoFragment<'a>, + control: impl Into>, + content: impl Into>, +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Renderer: program::Renderer + 'a, +{ + column![ + row![ + monospace(fragment).size(14), + horizontal_space(), + control.into() + ] + .spacing(5) + .align_y(Center), + content.into() + ] + .spacing(5) + .into() +} From d10b74054546641f54407a975f2dbdaa50cb953e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 5 Jun 2025 15:40:04 +0200 Subject: [PATCH 18/83] Add first `Expectation` to `instruction` --- devtools/src/tester.rs | 9 ++- test/src/emulator.rs | 45 ++++++++--- test/src/instruction.rs | 35 ++++++++- test/src/selector.rs | 167 ++++++++++++++++++++++++++++++++++++++++ test/src/simulator.rs | 164 ++++----------------------------------- 5 files changed, 254 insertions(+), 166 deletions(-) diff --git a/devtools/src/tester.rs b/devtools/src/tester.rs index b7824dbc..9f8419e1 100644 --- a/devtools/src/tester.rs +++ b/devtools/src/tester.rs @@ -351,11 +351,12 @@ impl Tester

{ *outcome = Outcome::Failed; } emulator::Event::Ready => { + *current += 1; + if let Some(instruction) = - self.instructions.get(*current).cloned() + self.instructions.get(*current - 1).cloned() { emulator.run(program, instruction); - *current += 1; } if *current >= self.instructions.len() { @@ -552,7 +553,7 @@ impl Tester

{ outcome, .. } => { - if *current == i { + if *current == i + 1 { Some(match outcome { Outcome::Running => { theme.palette().primary @@ -572,7 +573,7 @@ impl Tester

{ .color } }) - } else if *current > i { + } else if *current > i + 1 { Some( theme .extended_palette() diff --git a/test/src/emulator.rs b/test/src/emulator.rs index fa1c4b3d..cc557f37 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -5,6 +5,7 @@ use crate::core::renderer; use crate::core::widget; use crate::core::window; use crate::core::{Element, Size}; +use crate::instruction; use crate::program; use crate::program::Program; use crate::runtime::futures::futures::StreamExt; @@ -185,19 +186,41 @@ impl Emulator

{ &mut self.clipboard, &mut messages, ); + + self.cache = Some(user_interface.into_cache()); + + let task = + Task::batch(messages.into_iter().map(|message| { + program.update(&mut self.state, message) + })); + + self.wait_for(task); + self.resubscribe(program); } + Instruction::Expect(expectation) => match expectation { + instruction::Expectation::Presence(selector) => { + use widget::Operation; + + let mut operation = selector.operation(); + + user_interface.operate( + &self.renderer, + &mut widget::operation::black_box(&mut operation), + ); + + match operation.finish() { + widget::operation::Outcome::Some(Some(_)) => { + self.runtime.send(Event::Ready); + } + _ => { + self.runtime.send(Event::Failed); + } + } + + self.cache = Some(user_interface.into_cache()); + } + }, } - - self.cache = Some(user_interface.into_cache()); - - let task = Task::batch( - messages - .into_iter() - .map(|message| program.update(&mut self.state, message)), - ); - - self.wait_for(task); - self.resubscribe(program); } pub fn wait_for(&mut self, task: Task) { diff --git a/test/src/instruction.rs b/test/src/instruction.rs index fd9ba2c9..bdfa0d48 100644 --- a/test/src/instruction.rs +++ b/test/src/instruction.rs @@ -1,6 +1,8 @@ +use crate::Selector; use crate::core::keyboard; use crate::core::mouse; use crate::core::{Event, Point}; +use crate::selector; use crate::simulator; use std::fmt; @@ -8,6 +10,7 @@ use std::fmt; #[derive(Debug, Clone)] pub enum Instruction { Interact(Interaction), + Expect(Expectation), } impl Instruction { @@ -20,6 +23,7 @@ impl fmt::Display for Instruction { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Instruction::Interact(interaction) => interaction.fmt(f), + Instruction::Expect(expectation) => expectation.fmt(f), } } } @@ -337,6 +341,24 @@ mod format { } } +#[derive(Debug, Clone)] +pub enum Expectation { + Presence(Selector), +} + +impl fmt::Display for Expectation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Expectation::Presence(Selector::Id(_id)) => { + write!(f, "expect id") // TODO + } + Expectation::Presence(Selector::Text(text)) => { + write!(f, "expect text \"{text}\"") + } + } + } +} + pub use parser::Error as ParseError; mod parser { @@ -363,7 +385,11 @@ mod parser { } fn instruction(input: &str) -> IResult<&str, Instruction> { - map(interaction, Instruction::Interact).parse(input) + alt(( + map(interaction, Instruction::Interact), + map(expectation, Instruction::Expect), + )) + .parse(input) } fn interaction(input: &str) -> IResult<&str, Interaction> { @@ -414,6 +440,13 @@ mod parser { .parse(input) } + fn expectation(input: &str) -> IResult<&str, Expectation> { + map(preceded(tag("expect text "), string), |text| { + Expectation::Presence(selector::text(text)) + }) + .parse(input) + } + fn key(input: &str) -> IResult<&str, Key> { alt(( map(tag("enter"), |_| Key::Enter), diff --git a/test/src/selector.rs b/test/src/selector.rs index 58e0fca4..b6a5b294 100644 --- a/test/src/selector.rs +++ b/test/src/selector.rs @@ -1,6 +1,7 @@ //! Select widgets of a user interface. use crate::core::text; use crate::core::widget; +use crate::core::{Rectangle, Vector}; /// A selector describes a strategy to find a certain widget in a user interface. #[derive(Debug, Clone, PartialEq, Eq)] @@ -11,6 +12,165 @@ pub enum Selector { Text(text::Fragment<'static>), } +impl Selector { + pub fn operation<'a>(&self) -> impl widget::Operation> + 'a { + match self { + Selector::Id(id) => { + struct FindById { + id: widget::Id, + target: Option, + } + + impl widget::Operation> for FindById { + fn container( + &mut self, + id: Option<&widget::Id>, + bounds: Rectangle, + operate_on_children: &mut dyn FnMut( + &mut dyn widget::Operation>, + ), + ) { + if self.target.is_some() { + return; + } + + if Some(&self.id) == id { + self.target = Some(Target { bounds }); + return; + } + + operate_on_children(self); + } + + fn scrollable( + &mut self, + id: Option<&widget::Id>, + bounds: Rectangle, + _content_bounds: Rectangle, + _translation: Vector, + _state: &mut dyn widget::operation::Scrollable, + ) { + if self.target.is_some() { + return; + } + + if Some(&self.id) == id { + self.target = Some(Target { bounds }); + } + } + + fn text_input( + &mut self, + id: Option<&widget::Id>, + bounds: Rectangle, + _state: &mut dyn widget::operation::TextInput, + ) { + if self.target.is_some() { + return; + } + + if Some(&self.id) == id { + self.target = Some(Target { bounds }); + } + } + + fn text( + &mut self, + id: Option<&widget::Id>, + bounds: Rectangle, + _text: &str, + ) { + if self.target.is_some() { + return; + } + + if Some(&self.id) == id { + self.target = Some(Target { bounds }); + } + } + + fn custom( + &mut self, + id: Option<&widget::Id>, + bounds: Rectangle, + _state: &mut dyn std::any::Any, + ) { + if self.target.is_some() { + return; + } + + if Some(&self.id) == id { + self.target = Some(Target { bounds }); + } + } + + fn finish( + &self, + ) -> widget::operation::Outcome> + { + widget::operation::Outcome::Some(self.target) + } + } + + Box::new(FindById { + id: id.clone(), + target: None, + }) as Box> + } + Selector::Text(text) => { + struct FindByText { + text: text::Fragment<'static>, + target: Option, + } + + impl widget::Operation> for FindByText { + fn container( + &mut self, + _id: Option<&widget::Id>, + _bounds: Rectangle, + operate_on_children: &mut dyn FnMut( + &mut dyn widget::Operation>, + ), + ) { + if self.target.is_some() { + return; + } + + operate_on_children(self); + } + + fn text( + &mut self, + _id: Option<&widget::Id>, + bounds: Rectangle, + text: &str, + ) { + if self.target.is_some() { + return; + } + + if self.text == text { + self.target = Some(Target { bounds }); + } + } + + fn finish( + &self, + ) -> widget::operation::Outcome> + { + widget::operation::Outcome::Some(self.target) + } + } + + Box::new(FindByText { + text: text.clone(), + target: None, + }) + } + } + } +} + impl From for Selector { fn from(id: widget::Id) -> Self { Self::Id(id) @@ -32,3 +192,10 @@ pub fn id(id: impl Into) -> Selector { pub fn text(fragment: impl text::IntoFragment<'static>) -> Selector { Selector::Text(fragment.into_fragment()) } + +/// A specific area, normally containing a widget. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Target { + /// The bounds of the area. + pub bounds: Rectangle, +} diff --git a/test/src/simulator.rs b/test/src/simulator.rs index cdff5040..c5505d5b 100644 --- a/test/src/simulator.rs +++ b/test/src/simulator.rs @@ -8,12 +8,11 @@ use crate::core::theme; use crate::core::time; use crate::core::widget; use crate::core::window; -use crate::core::{ - Element, Event, Font, Point, Rectangle, Settings, Size, SmolStr, -}; +use crate::core::{Element, Event, Font, Point, Settings, Size, SmolStr}; use crate::renderer; use crate::runtime::UserInterface; use crate::runtime::user_interface; +use crate::selector; use crate::{Error, Selector}; use std::borrow::Cow; @@ -36,13 +35,6 @@ pub struct Simulator< messages: Vec, } -/// A specific area of a [`Simulator`], normally containing a widget. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Target { - /// The bounds of the area. - pub bounds: Rectangle, -} - impl<'a, Message, Theme, Renderer> Simulator<'a, Message, Theme, Renderer> where Theme: theme::Base, @@ -111,148 +103,20 @@ where pub fn find( &mut self, selector: impl Into, - ) -> Result { + ) -> Result { + use widget::Operation; + let selector = selector.into(); + let mut operation = selector.operation(); - match &selector { - Selector::Id(id) => { - struct FindById<'a> { - id: &'a widget::Id, - target: Option, - } + self.raw.operate( + &self.renderer, + &mut widget::operation::black_box(&mut operation), + ); - impl widget::Operation for FindById<'_> { - fn container( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - operate_on_children: &mut dyn FnMut( - &mut dyn widget::Operation<()>, - ), - ) { - if self.target.is_some() { - return; - } - - if Some(self.id) == id { - self.target = Some(Target { bounds }); - return; - } - - operate_on_children(self); - } - - fn scrollable( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - _content_bounds: Rectangle, - _translation: core::Vector, - _state: &mut dyn widget::operation::Scrollable, - ) { - if self.target.is_some() { - return; - } - - if Some(self.id) == id { - self.target = Some(Target { bounds }); - } - } - - fn text_input( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - _state: &mut dyn widget::operation::TextInput, - ) { - if self.target.is_some() { - return; - } - - if Some(self.id) == id { - self.target = Some(Target { bounds }); - } - } - - fn text( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - _text: &str, - ) { - if self.target.is_some() { - return; - } - - if Some(self.id) == id { - self.target = Some(Target { bounds }); - } - } - - fn custom( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - _state: &mut dyn std::any::Any, - ) { - if self.target.is_some() { - return; - } - - if Some(self.id) == id { - self.target = Some(Target { bounds }); - } - } - } - - let mut find = FindById { id, target: None }; - self.raw.operate(&self.renderer, &mut find); - - find.target.ok_or(Error::NotFound(selector)) - } - Selector::Text(text) => { - struct FindByText<'a> { - text: &'a str, - target: Option, - } - - impl widget::Operation for FindByText<'_> { - fn container( - &mut self, - _id: Option<&widget::Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut( - &mut dyn widget::Operation<()>, - ), - ) { - if self.target.is_some() { - return; - } - - operate_on_children(self); - } - - fn text( - &mut self, - _id: Option<&widget::Id>, - bounds: Rectangle, - text: &str, - ) { - if self.target.is_some() { - return; - } - - if self.text == text { - self.target = Some(Target { bounds }); - } - } - } - - let mut find = FindByText { text, target: None }; - self.raw.operate(&self.renderer, &mut find); - - find.target.ok_or(Error::NotFound(selector)) - } + match operation.finish() { + widget::operation::Outcome::Some(Some(target)) => Ok(target), + _ => Err(Error::NotFound(selector)), } } @@ -271,7 +135,7 @@ where pub fn click( &mut self, selector: impl Into, - ) -> Result { + ) -> Result { let target = self.find(selector)?; self.point_at(target.bounds.center()); From 623bae7fc0c674d5231a3a4c1c331d26a14f6882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 13 Jun 2025 15:46:37 +0200 Subject: [PATCH 19/83] Merge `Instruction` as many times as possible --- devtools/src/tester.rs | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/devtools/src/tester.rs b/devtools/src/tester.rs index 9f8419e1..0aa517c0 100644 --- a/devtools/src/tester.rs +++ b/devtools/src/tester.rs @@ -308,27 +308,30 @@ impl Tester

{ program.update(state, message).map(Tick::Program) } Tick::Recorder(event) => { - let Some(interaction) = - instruction::Interaction::from_event(event) - else { - return Task::none(); - }; + let mut interaction = + instruction::Interaction::from_event(event); - if let Some(Instruction::Interact(last_interaction)) = - self.instructions.pop() - { - let (last_interaction, new_interaction) = - last_interaction.merge(interaction); + while let Some(new_interaction) = interaction.take() { + if let Some(Instruction::Interact(last_interaction)) = + self.instructions.pop() + { + let (merged_interaction, new_interaction) = + last_interaction.merge(new_interaction); - self.instructions - .push(Instruction::Interact(last_interaction)); + if let Some(new_interaction) = new_interaction { + self.instructions.push(Instruction::Interact( + merged_interaction, + )); - if let Some(new_interaction) = new_interaction { + self.instructions + .push(Instruction::Interact(new_interaction)); + } else { + interaction = Some(merged_interaction); + } + } else { self.instructions .push(Instruction::Interact(new_interaction)); } - } else { - self.instructions.push(Instruction::Interact(interaction)); } Task::none() From df3ca3066025a7e910c7a3967c22b620364d59ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 13 Jun 2025 15:48:00 +0200 Subject: [PATCH 20/83] Make `Patient` the default `Mode` in `test::emulator` --- test/src/emulator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/src/emulator.rs b/test/src/emulator.rs index cc557f37..a2b6e6a4 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -266,8 +266,8 @@ impl Emulator

{ #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Mode { - Patient, #[default] + Patient, Impatient, } From baa7d2b96eaf199cc0ab8d3eefb533eebb7faa40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 13 Jun 2025 15:48:24 +0200 Subject: [PATCH 21/83] Merge consecutive `Mouse::Release` instructions --- test/src/instruction.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/src/instruction.rs b/test/src/instruction.rs index bdfa0d48..ba4867ba 100644 --- a/test/src/instruction.rs +++ b/test/src/instruction.rs @@ -127,6 +127,22 @@ impl Interaction { None, ) } + ( + current @ Mouse::Release { + button: button_a, + at: at_a, + } + | current @ Mouse::Click { + button: button_a, + at: at_a, + }, + Mouse::Release { + button: button_b, + at: at_b, + }, + ) if button_a == button_b && at_a == at_b => { + (Self::Mouse(current), None) + } (current, next) => { (Self::Mouse(current), Some(Self::Mouse(next))) } From a596abf431aa94e8a1296e77f12674d001656ca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 13 Jun 2025 15:52:23 +0200 Subject: [PATCH 22/83] Support overlays in `tester` devtool --- devtools/src/tester/recorder.rs | 186 ++++++++++++++++++++++++++------ 1 file changed, 156 insertions(+), 30 deletions(-) diff --git a/devtools/src/tester/recorder.rs b/devtools/src/tester/recorder.rs index 47191582..10760a1c 100644 --- a/devtools/src/tester/recorder.rs +++ b/devtools/src/tester/recorder.rs @@ -1,11 +1,12 @@ use crate::core::layout; use crate::core::mouse; +use crate::core::overlay; use crate::core::renderer; use crate::core::widget; use crate::core::widget::tree; use crate::core::{ self, Clipboard, Element, Event, Layout, Length, Point, Rectangle, Shell, - Size, Widget, + Size, Vector, Widget, }; pub fn recorder<'a, Message, Theme, Renderer>( @@ -64,35 +65,7 @@ where ); if let Some(on_event) = &self.on_event { - match event { - Event::Mouse(event) => { - if !cursor.is_over(layout.bounds()) { - return; - } - - match event { - mouse::Event::ButtonPressed(_) - | mouse::Event::ButtonReleased(_) - | mouse::Event::WheelScrolled { .. } => { - shell.publish(on_event(Event::Mouse(*event))); - } - mouse::Event::CursorMoved { position } => { - shell.publish(on_event(Event::Mouse( - mouse::Event::CursorMoved { - position: *position - - (layout.bounds().position() - - Point::ORIGIN), - }, - ))); - } - _ => {} - } - } - Event::Keyboard(event) => { - shell.publish(on_event(Event::Keyboard(event.clone()))); - } - _ => {} - } + record(event, cursor, shell, layout.bounds(), on_event); } } @@ -168,6 +141,26 @@ where .as_widget() .operate(state, layout, renderer, operation); } + + fn overlay<'a>( + &'a mut self, + state: &'a mut widget::Tree, + layout: Layout<'a>, + renderer: &Renderer, + _viewport: &Rectangle, + translation: Vector, + ) -> Option> { + self.content + .as_widget_mut() + .overlay(state, layout, renderer, &layout.bounds(), translation) + .map(|raw| { + overlay::Element::new(Box::new(Overlay { + raw, + bounds: layout.bounds(), + on_event: self.on_event.as_deref(), + })) + }) + } } impl<'a, Message, Theme, Renderer> From> @@ -181,3 +174,136 @@ where Element::new(recorder) } } + +struct Overlay<'a, Message, Theme, Renderer> { + raw: overlay::Element<'a, Message, Theme, Renderer>, + bounds: Rectangle, + on_event: Option<&'a dyn Fn(Event) -> Message>, +} + +impl<'a, Message, Theme, Renderer> core::Overlay + for Overlay<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer + 'a, +{ + fn layout(&mut self, renderer: &Renderer, _bounds: Size) -> layout::Node { + self.raw + .as_overlay_mut() + .layout(renderer, self.bounds.size()) + } + + fn draw( + &self, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + ) { + self.raw + .as_overlay() + .draw(renderer, theme, style, layout, cursor); + } + + fn operate( + &mut self, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + self.raw + .as_overlay_mut() + .operate(layout, renderer, operation); + } + + fn update( + &mut self, + event: &Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) { + if shell.is_event_captured() { + return; + } + + self.raw + .as_overlay_mut() + .update(event, layout, cursor, renderer, clipboard, shell); + + if let Some(on_event) = &self.on_event { + record(event, cursor, shell, self.bounds, on_event); + } + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + ) -> mouse::Interaction { + self.raw + .as_overlay() + .mouse_interaction(layout, cursor, renderer) + } + + fn overlay<'b>( + &'b mut self, + layout: Layout<'b>, + renderer: &Renderer, + ) -> Option> { + self.raw + .as_overlay_mut() + .overlay(layout, renderer) + .map(|raw| { + overlay::Element::new(Box::new(Overlay { + raw, + bounds: self.bounds, + on_event: self.on_event, + })) + }) + } + + fn index(&self) -> f32 { + self.raw.as_overlay().index() + } +} + +fn record( + event: &Event, + cursor: mouse::Cursor, + shell: &mut Shell<'_, Message>, + bounds: Rectangle, + on_event: impl Fn(Event) -> Message, +) { + match event { + Event::Mouse(event) => { + if !cursor.is_over(bounds) { + return; + } + + match event { + mouse::Event::ButtonPressed(_) + | mouse::Event::ButtonReleased(_) + | mouse::Event::WheelScrolled { .. } => { + shell.publish(on_event(Event::Mouse(*event))); + } + mouse::Event::CursorMoved { position } => { + shell.publish(on_event(Event::Mouse( + mouse::Event::CursorMoved { + position: *position + - (bounds.position() - Point::ORIGIN), + }, + ))); + } + _ => {} + } + } + Event::Keyboard(event) => { + shell.publish(on_event(Event::Keyboard(event.clone()))); + } + _ => {} + } +} From 1deb87694d48db46fca7c3d40b5ecbba2a4e2416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 8 Jul 2025 00:31:04 +0200 Subject: [PATCH 23/83] Make `window::open` pure again --- examples/multi_window/src/main.rs | 11 ++++------- runtime/src/window.rs | 15 +++++---------- winit/src/lib.rs | 5 +++-- 3 files changed, 12 insertions(+), 19 deletions(-) diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 77be505b..d940b9da 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -42,13 +42,12 @@ enum Message { impl Example { fn new() -> (Self, Task) { - let (_id, open) = window::open(window::Settings::default()); - ( Self { windows: BTreeMap::new(), }, - open.map(Message::WindowOpened), + window::open(window::Settings::default()) + .map(Message::WindowOpened), ) } @@ -77,12 +76,10 @@ impl Example { }, ); - let (_id, open) = window::open(window::Settings { + window::open(window::Settings { position, ..window::Settings::default() - }); - - open + }) }) .map(Message::WindowOpened) } diff --git a/runtime/src/window.rs b/runtime/src/window.rs index ccd8721b..19044628 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -18,7 +18,7 @@ use raw_window_handle::WindowHandle; #[allow(missing_debug_implementations)] pub enum Action { /// Opens a new window with some [`Settings`]. - Open(Id, Settings, oneshot::Sender), + Open(Settings, oneshot::Sender), /// Close the window and exits the application. Close(Id), @@ -249,15 +249,10 @@ pub fn close_requests() -> Subscription { /// Opens a new window with the given [`Settings`]; producing the [`Id`] /// of the new window on completion. -pub fn open(settings: Settings) -> (Id, Task) { - let id = Id::unique(); - - ( - id, - task::oneshot(|channel| { - crate::Action::Window(Action::Open(id, settings, channel)) - }), - ) +pub fn open(settings: Settings) -> Task { + task::oneshot(|channel| { + crate::Action::Window(Action::Open(settings, channel)) + }) } /// Closes the window with `id`. diff --git a/winit/src/lib.rs b/winit/src/lib.rs index c4e62863..5abdaa4d 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -108,7 +108,7 @@ where let task = if let Some(window_settings) = window_settings { let mut task = Some(task); - let (_id, open) = runtime::window::open(window_settings); + let open = runtime::window::open(window_settings); open.then(move |_| task.take().unwrap_or(Task::none())) } else { @@ -1121,7 +1121,8 @@ fn run_action<'a, P, C>( } }, Action::Window(action) => match action { - window::Action::Open(id, settings, channel) => { + window::Action::Open(settings, channel) => { + let id = core::window::Id::unique(); let monitor = window_manager.last_monitor(); control_sender From f3a4e4431485ea6bfde6068a9d7b12e9d81edb37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 8 Jul 2025 01:19:37 +0200 Subject: [PATCH 24/83] Make `tester` work with `daemon` (only 1 window for now!) --- devtools/src/lib.rs | 18 ++----- devtools/src/tester.rs | 120 ++++++++++++++++++++--------------------- test/src/emulator.rs | 59 +++++++++++++++++--- 3 files changed, 113 insertions(+), 84 deletions(-) diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index 4a50580d..fa002c8e 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -361,7 +361,7 @@ where match &self.mode { Mode::Open { tester } => { - tester.view(program, window, view, Event::Tester) + tester.view(program, view, Event::Tester) } _ => view(), } @@ -444,19 +444,9 @@ where } pub fn subscription(&self, program: &P) -> Subscription> { - let subscription = match &self.mode { - Mode::Open { tester } if !tester.is_idle() => { - tester.subscription(program).map(Event::Tester) - } - _ => { - let subscription = - program.subscription(&self.state).map(Event::Program); - - debug::subscriptions_tracked(subscription.units()); - - subscription - } - }; + let subscription = + program.subscription(&self.state).map(Event::Program); + debug::subscriptions_tracked(subscription.units()); let hotkeys = futures::keyboard::on_key_press(|key, _modifiers| match key { diff --git a/devtools/src/tester.rs b/devtools/src/tester.rs index 0aa517c0..8c7ac9ce 100644 --- a/devtools/src/tester.rs +++ b/devtools/src/tester.rs @@ -10,7 +10,6 @@ use crate::core::border; use crate::core::window; use crate::core::{Element, Event, Font, Size, Theme}; use crate::executor; -use crate::futures::Subscription; use crate::futures::futures::channel::mpsc; use crate::icon; use crate::program; @@ -36,10 +35,11 @@ pub struct Tester { enum State { Idle, Recording { - state: P::State, + emulator: Emulator

, }, Ready { state: P::State, + window: window::Id, }, Playing { emulator: Emulator

, @@ -98,10 +98,6 @@ impl Tester

{ } } - pub fn is_idle(&self) -> bool { - matches!(self.state, State::Idle) - } - pub fn is_busy(&self) -> bool { matches!( self.state, @@ -134,24 +130,30 @@ impl Tester

{ self.edit = None; self.instructions.clear(); - let (state, task) = if let Some(preset) = self.preset(program) { - preset.boot() - } else { - program.boot() - }; + let (sender, receiver) = mpsc::channel(1); - self.state = State::Recording { state }; + let emulator = Emulator::with_preset( + sender, + program, + self.mode, + self.viewport, + self.preset(program), + ); - task.map(Tick::Program) + self.state = State::Recording { emulator }; + + Task::run(receiver, Tick::Emulator) } Message::Stop => { - let State::Recording { state } = + let State::Recording { emulator } = std::mem::replace(&mut self.state, State::Idle) else { return Task::none(); }; - self.state = State::Ready { state }; + let (state, window) = emulator.into_state(); + + self.state = State::Ready { state, window }; Task::none() } @@ -301,11 +303,13 @@ impl Tester

{ match tick { Tick::Tester(message) => self.update(program, message), Tick::Program(message) => { - let State::Recording { state } = &mut self.state else { + let State::Recording { emulator } = &mut self.state else { return Task::none(); }; - program.update(state, message).map(Tick::Program) + emulator.update(program, message); + + Task::none() } Tick::Recorder(event) => { let mut interaction = @@ -337,35 +341,38 @@ impl Tester

{ Task::none() } Tick::Emulator(event) => { - let State::Playing { - emulator, - current, - outcome, - } = &mut self.state - else { - return Task::none(); - }; - - match event { - emulator::Event::Action(action) => { - emulator.perform(program, action); - } - emulator::Event::Failed => { - *outcome = Outcome::Failed; - } - emulator::Event::Ready => { - *current += 1; - - if let Some(instruction) = - self.instructions.get(*current - 1).cloned() - { - emulator.run(program, instruction); - } - - if *current >= self.instructions.len() { - *outcome = Outcome::Success; + match &mut self.state { + State::Recording { emulator } => { + if let emulator::Event::Action(action) = event { + emulator.perform(program, action); } } + State::Playing { + emulator, + current, + outcome, + } => match event { + emulator::Event::Action(action) => { + emulator.perform(program, action); + } + emulator::Event::Failed => { + *outcome = Outcome::Failed; + } + emulator::Event::Ready => { + *current += 1; + + if let Some(instruction) = + self.instructions.get(*current - 1).cloned() + { + emulator.run(program, instruction); + } + + if *current >= self.instructions.len() { + *outcome = Outcome::Success; + } + } + }, + State::Idle | State::Ready { .. } => {} } Task::none() @@ -373,21 +380,9 @@ impl Tester

{ } } - pub fn subscription(&self, program: &P) -> Subscription> { - match &self.state { - State::Idle | State::Playing { .. } | State::Ready { .. } => { - Subscription::none() - } - State::Recording { state } => { - program.subscription(state).map(Tick::Program) - } - } - } - pub fn view<'a, T: 'static>( &'a self, program: &P, - window: window::Id, current: impl FnOnce() -> Element<'a, T, Theme, P::Renderer>, emulate: impl Fn(Tick

) -> T + 'a, ) -> Element<'a, T, Theme, P::Renderer> { @@ -435,10 +430,9 @@ impl Tester

{ scrollable( container(match &self.state { State::Idle => current(), - State::Recording { state } => { - let theme = program.theme(state, window); - let view = - program.view(state, window).map(Tick::Program); + State::Recording { emulator } => { + let theme = emulator.theme(program); + let view = emulator.view(program).map(Tick::Program); Element::from( recorder(themer(theme, view)) @@ -446,10 +440,10 @@ impl Tester

{ ) .map(emulate) } - State::Ready { state } => { - let theme = program.theme(state, window); + State::Ready { state, window } => { + let theme = program.theme(state, *window); let view = - program.view(state, window).map(Tick::Program); + program.view(state, *window).map(Tick::Program); Element::from(themer(theme, view)).map(emulate) } diff --git a/test/src/emulator.rs b/test/src/emulator.rs index a1465a4f..89e0bd87 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -3,8 +3,7 @@ use crate::core; use crate::core::mouse; use crate::core::renderer; use crate::core::widget; -use crate::core::window; -use crate::core::{Element, Size}; +use crate::core::{Element, Point, Size}; use crate::instruction; use crate::program; use crate::program::Program; @@ -15,6 +14,7 @@ use crate::runtime::futures::subscription; use crate::runtime::futures::{Executor, Runtime}; use crate::runtime::task; use crate::runtime::user_interface; +use crate::runtime::window; use crate::runtime::{Action, Task, UserInterface}; use std::fmt; @@ -26,7 +26,7 @@ pub struct Emulator { renderer: P::Renderer, mode: Mode, size: Size, - window: window::Id, + window: core::window::Id, cursor: mouse::Cursor, clipboard: Clipboard, cache: Option, @@ -87,7 +87,7 @@ impl Emulator

{ size, clipboard: Clipboard { content: None }, cursor: mouse::Cursor::Unavailable, - window: window::Id::unique(), + window: core::window::Id::unique(), cache: Some(user_interface::Cache::default()), }; @@ -143,9 +143,50 @@ impl Emulator

{ // TODO dbg!(action); } - Action::Window(_action) => { - // TODO - } + Action::Window(action) => match action { + window::Action::Open(_settings, sender) => { + self.window = core::window::Id::unique(); + + let _ = sender.send(self.window); + } + window::Action::GetOldest(sender) + | window::Action::GetLatest(sender) => { + let _ = sender.send(Some(self.window)); + } + window::Action::GetSize(id, sender) => { + if id == self.window { + let _ = sender.send(self.size); + } + } + window::Action::GetMaximized(id, sender) => { + if id == self.window { + let _ = sender.send(false); + } + } + window::Action::GetMinimized(id, sender) => { + if id == self.window { + let _ = sender.send(None); + } + } + window::Action::GetPosition(id, sender) => { + if id == self.window { + let _ = sender.send(Some(Point::ORIGIN)); + } + } + window::Action::GetScaleFactor(id, sender) => { + if id == self.window { + let _ = sender.send(1.0); + } + } + window::Action::GetMode(id, sender) => { + if id == self.window { + let _ = sender.send(core::window::Mode::Windowed); + } + } + _ => { + // Ignored + } + }, Action::System(action) => { // TODO dbg!(action); @@ -265,6 +306,10 @@ impl Emulator

{ pub fn theme(&self, program: &P) -> P::Theme { program.theme(&self.state, self.window) } + + pub fn into_state(self) -> (P::State, core::window::Id) { + (self.state, self.window) + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] From 28a4c53f4387e8ce931d377dd90d8b7022abfd96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 12 Aug 2025 22:38:59 +0200 Subject: [PATCH 25/83] Fix `view` signature of `tester::null` --- devtools/src/tester/null.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/devtools/src/tester/null.rs b/devtools/src/tester/null.rs index c2a4b4f3..a2b7be47 100644 --- a/devtools/src/tester/null.rs +++ b/devtools/src/tester/null.rs @@ -47,7 +47,6 @@ impl Tester

{ pub fn view<'a, T: 'static>( &'a self, _program: &P, - _window: window::Id, _current: impl FnOnce() -> Element<'a, T, Theme, P::Renderer>, _emulate: impl Fn(Tick

) -> T + 'a, ) -> Element<'a, T, Theme, P::Renderer> { From f9755b0b7a99dfc0dd5eb7d9f020ab2673f5b87b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 15 Aug 2025 21:22:41 +0200 Subject: [PATCH 26/83] Introduce `Ice` format and save test metadata --- devtools/src/tester.rs | 59 +++++----- devtools/src/tester/null.rs | 10 -- examples/todos/tests/carl_sagan.ice | 8 +- test/src/ice.rs | 169 ++++++++++++++++++++++++++++ test/src/instruction.rs | 10 +- test/src/lib.rs | 2 + 6 files changed, 215 insertions(+), 43 deletions(-) create mode 100644 test/src/ice.rs diff --git a/devtools/src/tester.rs b/devtools/src/tester.rs index 8c7ac9ce..13b6e9e2 100644 --- a/devtools/src/tester.rs +++ b/devtools/src/tester.rs @@ -15,8 +15,9 @@ use crate::icon; use crate::program; use crate::runtime::Task; use crate::test::emulator; +use crate::test::ice; use crate::test::instruction; -use crate::test::{Emulator, Instruction}; +use crate::test::{Emulator, Ice, Instruction}; use crate::widget::{ button, center, column, combo_box, container, horizontal_space, monospace, pick_list, row, scrollable, text, text_editor, text_input, themer, @@ -64,7 +65,7 @@ pub enum Message { Play, Import, Export, - Imported(Option), + Imported(Result), Edit, Edited(text_editor::Action), Confirm, @@ -188,8 +189,10 @@ impl Tester

{ Task::future(import) .and_then(|file| { executor::spawn_blocking(move |mut sender| { - let _ = sender - .try_send(fs::read_to_string(file.path()).ok()); + let _ = sender.try_send(Ice::parse( + &fs::read_to_string(file.path()) + .unwrap_or_default(), + )); }) }) .map(Message::Imported) @@ -201,11 +204,15 @@ impl Tester

{ self.confirm(); - let test: Vec<_> = self - .instructions - .iter() - .map(Instruction::to_string) - .collect(); + let ice = Ice { + viewport: Size::new( + self.viewport.width as u32, + self.viewport.height as u32, + ), + mode: self.mode, + preset: self.preset.clone(), + instructions: self.instructions.clone(), + }; let export = rfd::AsyncFileDialog::new() .add_filter("ice", &["ice"]) @@ -217,28 +224,21 @@ impl Tester

{ }; let _ = thread::spawn(move || { - fs::write(file.path(), test.join("\n")) + fs::write(file.path(), ice.to_string()) }); }) .discard() } - Message::Imported(instructions) => { - let Some(instructions) = instructions else { - return Task::none(); - }; - - let instructions: Result, _> = - instructions.lines().map(Instruction::parse).collect(); - - match instructions { - Ok(instructions) => { - self.instructions = instructions; - self.edit = None; - } - Err(error) => { - log::error!("{error}"); - } - } + Message::Imported(Ok(ice)) => { + self.viewport = Size::new( + ice.viewport.width as f32, + ice.viewport.height as f32, + ); + self.mode = ice.mode; + self.preset = ice.preset; + self.instructions = ice.instructions; + self.edit = None; + self.state = State::Idle; Task::none() } @@ -268,6 +268,11 @@ impl Tester

{ Message::Confirm => { self.confirm(); + Task::none() + } + Message::Imported(Err(error)) => { + log::error!("{error}"); + Task::none() } } diff --git a/devtools/src/tester/null.rs b/devtools/src/tester/null.rs index a2b7be47..96b1dbfa 100644 --- a/devtools/src/tester/null.rs +++ b/devtools/src/tester/null.rs @@ -1,7 +1,5 @@ use crate::Program; -use crate::core::window; use crate::core::{Element, Theme}; -use crate::futures::Subscription; use crate::runtime::Task; use crate::widget::horizontal_space; @@ -24,10 +22,6 @@ impl Tester

{ Self { _type: PhantomData } } - pub fn is_idle(&self) -> bool { - true - } - pub fn is_busy(&self) -> bool { false } @@ -40,10 +34,6 @@ impl Tester

{ Task::none() } - pub fn subscription(&self, _program: &P) -> Subscription> { - Subscription::none() - } - pub fn view<'a, T: 'static>( &'a self, _program: &P, diff --git a/examples/todos/tests/carl_sagan.ice b/examples/todos/tests/carl_sagan.ice index d536b9f1..860f5d9e 100644 --- a/examples/todos/tests/carl_sagan.ice +++ b/examples/todos/tests/carl_sagan.ice @@ -1,3 +1,7 @@ +viewport: 512x768 +mode: impatient +preset: Empty +----- click left at (377.80, 236.50) type "Create the universe" type enter @@ -5,4 +9,6 @@ type "Make an apple pie" type enter click left at (135.40, 351.70) click left at (153.80, 398.10) -move cursor to (511.40, 448.50) \ No newline at end of file +move cursor to (511.40, 448.50) +expect text "Create the universe" +expect text "Make an apple pie" diff --git a/test/src/ice.rs b/test/src/ice.rs new file mode 100644 index 00000000..67037c14 --- /dev/null +++ b/test/src/ice.rs @@ -0,0 +1,169 @@ +use crate::Instruction; +use crate::core::Size; +use crate::emulator; +use crate::instruction; + +#[derive(Debug, Clone, PartialEq)] +pub struct Ice { + pub viewport: Size, + pub mode: emulator::Mode, + pub preset: Option, + pub instructions: Vec, +} + +impl Ice { + pub fn parse(content: &str) -> Result { + let Some((metadata, rest)) = content.split_once("-") else { + return Err(ParseError::NoMetadata); + }; + + let mut viewport = None; + let mut mode = None; + let mut preset = None; + + for (i, line) in metadata.lines().enumerate() { + if line.trim().is_empty() { + continue; + } + + let Some((field, value)) = line.split_once(':') else { + return Err(ParseError::InvalidMetadata { + line: i, + content: line.to_owned(), + }); + }; + + match field.trim() { + "viewport" => { + viewport = Some( + if let Some((width, height)) = + value.trim().split_once('x') + && let Ok(width) = width.parse() + && let Ok(height) = height.parse() + { + Size::new(width, height) + } else { + return Err(ParseError::InvalidViewport { + line: i, + value: value.to_owned(), + }); + }, + ); + } + "mode" => { + mode = Some(match value.trim() { + "patient" => emulator::Mode::Patient, + "impatient" => emulator::Mode::Impatient, + _ => { + return Err(ParseError::InvalidMode { + line: i, + value: value.to_owned(), + }); + } + }); + } + "preset" => { + preset = Some(value.trim().to_owned()); + } + field => { + return Err(ParseError::UnknownField { + line: i, + field: field.to_owned(), + }); + } + } + } + + let Some(viewport) = viewport else { + return Err(ParseError::MissingViewport); + }; + + let Some(mode) = mode else { + return Err(ParseError::MissingMode); + }; + + let instructions = rest + .lines() + .skip(1) + .enumerate() + .map(|(i, line)| { + Instruction::parse(line).map_err(|error| { + ParseError::InvalidInstruction { + line: metadata.lines().count() + 1 + i, + error, + } + }) + }) + .collect::, _>>()?; + + Ok(Self { + viewport, + mode, + preset, + instructions, + }) + } +} + +impl std::fmt::Display for Ice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!( + f, + "viewport: {width}x{height}", + width = self.viewport.width, + height = self.viewport.height + )?; + + writeln!( + f, + "mode: {}", + match self.mode { + emulator::Mode::Patient => "patient", + emulator::Mode::Impatient => "impatient", + } + )?; + + if let Some(preset) = &self.preset { + writeln!(f, "preset: {preset}")?; + } + + f.write_str("-----\n")?; + + for instruction in &self.instructions { + instruction.fmt(f)?; + f.write_str("\n")?; + } + + Ok(()) + } +} + +#[derive(Debug, Clone, thiserror::Error)] +pub enum ParseError { + #[error("the ice test has no metadata")] + NoMetadata, + + #[error("invalid metadata in line {line}: \"{content}\"")] + InvalidMetadata { line: usize, content: String }, + + #[error("invalid viewport in line {line}: \"{value}\"")] + InvalidViewport { line: usize, value: String }, + + #[error("invalid mode in line {line}: \"{value}\"")] + InvalidMode { line: usize, value: String }, + + #[error("unknown metadata field in line {line}: \"{field}\"")] + UnknownField { line: usize, field: String }, + + #[error("metadata is missing the viewport field")] + MissingViewport, + + #[error("metadata is missing the mode field")] + MissingMode, + + #[error("invalid instruction in line {line}: {error}")] + InvalidInstruction { + line: usize, + error: instruction::ParseError, + }, +} diff --git a/test/src/instruction.rs b/test/src/instruction.rs index ba4867ba..ba987072 100644 --- a/test/src/instruction.rs +++ b/test/src/instruction.rs @@ -7,7 +7,7 @@ use crate::simulator; use std::fmt; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum Instruction { Interact(Interaction), Expect(Expectation), @@ -28,7 +28,7 @@ impl fmt::Display for Instruction { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum Interaction { Mouse(Mouse), Keyboard(Keyboard), @@ -239,7 +239,7 @@ impl fmt::Display for Interaction { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum Mouse { Move(Point), Press { @@ -275,7 +275,7 @@ impl fmt::Display for Mouse { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum Keyboard { Press(Key), Release(Key), @@ -357,7 +357,7 @@ mod format { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum Expectation { Presence(Selector), } diff --git a/test/src/lib.rs b/test/src/lib.rs index bdf06df1..150521f0 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -90,6 +90,7 @@ use iced_runtime as runtime; use iced_runtime::core; pub mod emulator; +pub mod ice; pub mod instruction; pub mod selector; pub mod simulator; @@ -98,6 +99,7 @@ mod error; pub use emulator::Emulator; pub use error::Error; +pub use ice::Ice; pub use instruction::Instruction; pub use selector::Selector; pub use simulator::{Simulator, simulator}; From bdcaadbe0088568c197bedb7bc93ae7a38431b57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 20 Aug 2025 13:47:34 +0200 Subject: [PATCH 27/83] Introduce `instruction::Target` in `test` crate --- core/src/widget/operation/text_input.rs | 8 ++ examples/todos/Cargo.toml | 1 + examples/todos/tests/carl_sagan.ice | 10 +- test/src/emulator.rs | 38 +++++++- test/src/instruction.rs | 116 +++++++++++++----------- test/src/selector.rs | 47 ++++++---- test/src/simulator.rs | 4 +- widget/src/text_input.rs | 8 ++ 8 files changed, 150 insertions(+), 82 deletions(-) diff --git a/core/src/widget/operation/text_input.rs b/core/src/widget/operation/text_input.rs index efb2a4d3..6bcae385 100644 --- a/core/src/widget/operation/text_input.rs +++ b/core/src/widget/operation/text_input.rs @@ -5,12 +5,20 @@ use crate::widget::operation::Operation; /// The internal state of a widget that has text input. pub trait TextInput { + /// Returns the current _visible_ text of the text input + /// + /// Normally, this is either its value or its placeholder. + fn text(&self) -> &str; + /// Moves the cursor of the text input to the front of the input text. fn move_cursor_to_front(&mut self); + /// Moves the cursor of the text input to the end of the input text. fn move_cursor_to_end(&mut self); + /// Moves the cursor of the text input to an arbitrary location. fn move_cursor_to(&mut self, position: usize); + /// Selects all the content of the text input. fn select_all(&mut self); } diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml index 9869a2c5..3dca5c49 100644 --- a/examples/todos/Cargo.toml +++ b/examples/todos/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" publish = false [features] +default = ["tester"] test = ["iced/test"] tester = ["test", "iced/tester"] diff --git a/examples/todos/tests/carl_sagan.ice b/examples/todos/tests/carl_sagan.ice index 860f5d9e..7c94ff3a 100644 --- a/examples/todos/tests/carl_sagan.ice +++ b/examples/todos/tests/carl_sagan.ice @@ -2,13 +2,11 @@ viewport: 512x768 mode: impatient preset: Empty ----- -click left at (377.80, 236.50) +click left at "What needs to be done?" type "Create the universe" type enter type "Make an apple pie" type enter -click left at (135.40, 351.70) -click left at (153.80, 398.10) -move cursor to (511.40, 448.50) -expect text "Create the universe" -expect text "Make an apple pie" +click left at "Create the universe" +click left at "Make an apple pie" +expect "0 tasks left" diff --git a/test/src/emulator.rs b/test/src/emulator.rs index 89e0bd87..710e5b1b 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -16,6 +16,7 @@ use crate::runtime::task; use crate::runtime::user_interface; use crate::runtime::window; use crate::runtime::{Action, Task, UserInterface}; +use crate::selector; use std::fmt; @@ -212,7 +213,34 @@ impl Emulator

{ match instruction { Instruction::Interact(interaction) => { - let events = interaction.events(); + let Some(events) = interaction.events(|target| match target { + instruction::Target::Point(position) => Some(*position), + instruction::Target::Text(text) => { + use widget::Operation; + + let mut operation = + selector::text(text.to_owned()).operation(); + + user_interface.operate( + &self.renderer, + &mut widget::operation::black_box(&mut operation), + ); + + match operation.finish() { + widget::operation::Outcome::Some(matches) => { + matches + .first() + .copied() + .map(|target| target.bounds.center()) + } + _ => None, + } + } + }) else { + self.runtime.send(Event::Failed); + self.cache = Some(user_interface.into_cache()); + return; + }; for event in &events { if let core::Event::Mouse(mouse::Event::CursorMoved { @@ -242,10 +270,10 @@ impl Emulator

{ self.resubscribe(program); } Instruction::Expect(expectation) => match expectation { - instruction::Expectation::Presence(selector) => { + instruction::Expectation::Text(text) => { use widget::Operation; - let mut operation = selector.operation(); + let mut operation = selector::text(text).operation(); user_interface.operate( &self.renderer, @@ -253,7 +281,9 @@ impl Emulator

{ ); match operation.finish() { - widget::operation::Outcome::Some(Some(_)) => { + widget::operation::Outcome::Some(matches) + if matches.len() == 1 => + { self.runtime.send(Event::Ready); } _ => { diff --git a/test/src/instruction.rs b/test/src/instruction.rs index ba987072..539c61c5 100644 --- a/test/src/instruction.rs +++ b/test/src/instruction.rs @@ -1,8 +1,6 @@ -use crate::Selector; use crate::core::keyboard; use crate::core::mouse; use crate::core::{Event, Point}; -use crate::selector; use crate::simulator; use std::fmt; @@ -38,7 +36,9 @@ impl Interaction { pub fn from_event(event: Event) -> Option { Some(match event { Event::Mouse(mouse) => Self::Mouse(match mouse { - mouse::Event::CursorMoved { position } => Mouse::Move(position), + mouse::Event::CursorMoved { position } => { + Mouse::Move(Target::Point(position)) + } mouse::Event::ButtonPressed(button) => { Mouse::Press { button, at: None } } @@ -115,8 +115,8 @@ impl Interaction { at: release_at, }, ) if press == release - && release_at.is_none_or(|release_at| { - Some(release_at) == press_at + && release_at.as_ref().is_none_or(|release_at| { + Some(release_at) == press_at.as_ref() }) => { ( @@ -127,22 +127,6 @@ impl Interaction { None, ) } - ( - current @ Mouse::Release { - button: button_a, - at: at_a, - } - | current @ Mouse::Click { - button: button_a, - at: at_a, - }, - Mouse::Release { - button: button_b, - at: at_b, - }, - ) if button_a == button_b && at_a == at_b => { - (Self::Mouse(current), None) - } (current, next) => { (Self::Mouse(current), Some(Self::Mouse(next))) } @@ -173,7 +157,10 @@ impl Interaction { } } - pub fn events(&self) -> Vec { + pub fn events( + &self, + find_target: impl FnOnce(&Target) -> Option, + ) -> Option> { let mouse_move_ = |to| Event::Mouse(mouse::Event::CursorMoved { position: to }); @@ -187,20 +174,22 @@ impl Interaction { let key_release = |key| simulator::release_key(key); - match self { + Some(match self { Interaction::Mouse(mouse) => match mouse { - Mouse::Move(to) => vec![mouse_move_(*to)], + Mouse::Move(to) => vec![mouse_move_(find_target(to)?)], Mouse::Press { button, at: Some(at), - } => vec![mouse_move_(*at), mouse_press(*button)], + } => vec![mouse_move_(find_target(at)?), mouse_press(*button)], Mouse::Press { button, at: None } => { vec![mouse_press(*button)] } Mouse::Release { button, at: Some(at), - } => vec![mouse_move_(*at), mouse_release(*button)], + } => { + vec![mouse_move_(find_target(at)?), mouse_release(*button)] + } Mouse::Release { button, at: None } => { vec![mouse_release(*button)] } @@ -209,7 +198,7 @@ impl Interaction { at: Some(at), } => { vec![ - mouse_move_(*at), + mouse_move_(find_target(at)?), mouse_press(*button), mouse_release(*button), ] @@ -226,7 +215,7 @@ impl Interaction { simulator::typewrite(text).collect() } }, - } + }) } } @@ -241,40 +230,55 @@ impl fmt::Display for Interaction { #[derive(Debug, Clone, PartialEq)] pub enum Mouse { - Move(Point), + Move(Target), Press { button: mouse::Button, - at: Option, + at: Option, }, Release { button: mouse::Button, - at: Option, + at: Option, }, Click { button: mouse::Button, - at: Option, + at: Option, }, } impl fmt::Display for Mouse { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Mouse::Move(point) => { - write!(f, "move cursor to ({:.2}, {:.2})", point.x, point.y) + Mouse::Move(target) => { + write!(f, "move cursor to {}", target) } Mouse::Press { button, at } => { - write!(f, "press {}", format::button_at(*button, *at)) + write!(f, "press {}", format::button_at(*button, at.as_ref())) } Mouse::Release { button, at } => { - write!(f, "release {}", format::button_at(*button, *at)) + write!(f, "release {}", format::button_at(*button, at.as_ref())) } Mouse::Click { button, at } => { - write!(f, "click {}", format::button_at(*button, *at)) + write!(f, "click {}", format::button_at(*button, at.as_ref())) } } } } +#[derive(Debug, Clone, PartialEq)] +pub enum Target { + Point(Point), + Text(String), +} + +impl fmt::Display for Target { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Point(point) => f.write_str(&format::point(*point)), + Self::Text(text) => f.write_str(&format::string(text)), + } + } +} + #[derive(Debug, Clone, PartialEq)] pub enum Keyboard { Press(Key), @@ -324,9 +328,9 @@ impl From for keyboard::Key { mod format { use super::*; - pub fn button_at(button: mouse::Button, at: Option) -> String { + pub fn button_at(button: mouse::Button, at: Option<&Target>) -> String { if let Some(at) = at { - format!("{} at {}", self::button(button), point(at)) + format!("{} at {}", self::button(button), at) } else { self::button(button).to_owned() } @@ -355,21 +359,22 @@ mod format { Key::Backspace => "backspace", } } + + pub fn string(text: &str) -> String { + format!("\"{}\"", text.escape_default()) + } } #[derive(Debug, Clone, PartialEq)] pub enum Expectation { - Presence(Selector), + Text(String), } impl fmt::Display for Expectation { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Expectation::Presence(Selector::Id(_id)) => { - write!(f, "expect id") // TODO - } - Expectation::Presence(Selector::Text(text)) => { - write!(f, "expect text \"{text}\"") + Expectation::Text(text) => { + write!(f, "expect {}", format::string(text)) } } } @@ -383,7 +388,7 @@ mod parser { use nom::branch::alt; use nom::bytes::complete::tag; use nom::character::complete::{char, multispace0, satisfy}; - use nom::combinator::{map, opt, recognize}; + use nom::combinator::{cut, map, opt, recognize}; use nom::multi::many0; use nom::number::float; use nom::sequence::{delimited, preceded, separated_pair}; @@ -418,7 +423,7 @@ mod parser { fn mouse(input: &str) -> IResult<&str, Mouse> { let mouse_move = - preceded(tag("move cursor to "), point).map(Mouse::Move); + preceded(tag("move cursor to "), target).map(Mouse::Move); alt((mouse_move, mouse_click)).parse(input) } @@ -433,13 +438,21 @@ mod parser { fn mouse_button_at( input: &str, - ) -> IResult<&str, (mouse::Button, Option)> { + ) -> IResult<&str, (mouse::Button, Option)> { let (input, button) = mouse_button(input)?; - let (input, at) = opt(preceded(tag(" at "), point)).parse(input)?; + let (input, at) = opt(target).parse(input)?; Ok((input, (button, at))) } + fn target(input: &str) -> IResult<&str, Target> { + preceded( + tag(" at "), + cut(alt((string.map(Target::Text), point.map(Target::Point)))), + ) + .parse(input) + } + fn mouse_button(input: &str) -> IResult<&str, mouse::Button> { alt(( tag("left").map(|_| mouse::Button::Left), @@ -457,8 +470,8 @@ mod parser { } fn expectation(input: &str) -> IResult<&str, Expectation> { - map(preceded(tag("expect text "), string), |text| { - Expectation::Presence(selector::text(text)) + map(preceded(tag("expect "), string), |text| { + Expectation::Text(text) }) .parse(input) } @@ -474,6 +487,7 @@ mod parser { } fn string(input: &str) -> IResult<&str, String> { + // TODO: Proper string literal parsing delimited( char('"'), map(recognize(many0(satisfy(|c| c != '"'))), str::to_owned), diff --git a/test/src/selector.rs b/test/src/selector.rs index b6a5b294..fa6fce5a 100644 --- a/test/src/selector.rs +++ b/test/src/selector.rs @@ -13,7 +13,7 @@ pub enum Selector { } impl Selector { - pub fn operation<'a>(&self) -> impl widget::Operation> + 'a { + pub fn operation<'a>(&self) -> impl widget::Operation> + 'a { match self { Selector::Id(id) => { struct FindById { @@ -21,13 +21,13 @@ impl Selector { target: Option, } - impl widget::Operation> for FindById { + impl widget::Operation> for FindById { fn container( &mut self, id: Option<&widget::Id>, bounds: Rectangle, operate_on_children: &mut dyn FnMut( - &mut dyn widget::Operation>, + &mut dyn widget::Operation>, ), ) { if self.target.is_some() { @@ -106,9 +106,13 @@ impl Selector { fn finish( &self, - ) -> widget::operation::Outcome> + ) -> widget::operation::Outcome> { - widget::operation::Outcome::Some(self.target) + if let Some(target) = self.target { + widget::operation::Outcome::Some(vec![target]) + } else { + widget::operation::Outcome::None + } } } @@ -120,51 +124,54 @@ impl Selector { Selector::Text(text) => { struct FindByText { text: text::Fragment<'static>, - target: Option, + target: Vec, } - impl widget::Operation> for FindByText { + impl widget::Operation> for FindByText { fn container( &mut self, _id: Option<&widget::Id>, _bounds: Rectangle, operate_on_children: &mut dyn FnMut( - &mut dyn widget::Operation>, + &mut dyn widget::Operation>, ), ) { - if self.target.is_some() { - return; - } - operate_on_children(self); } + fn text_input( + &mut self, + _id: Option<&widget::Id>, + bounds: Rectangle, + state: &mut dyn widget::operation::TextInput, + ) { + if self.text == state.text() { + self.target.push(Target { bounds }); + } + } + fn text( &mut self, _id: Option<&widget::Id>, bounds: Rectangle, text: &str, ) { - if self.target.is_some() { - return; - } - if self.text == text { - self.target = Some(Target { bounds }); + self.target.push(Target { bounds }); } } fn finish( &self, - ) -> widget::operation::Outcome> + ) -> widget::operation::Outcome> { - widget::operation::Outcome::Some(self.target) + widget::operation::Outcome::Some(self.target.clone()) } } Box::new(FindByText { text: text.clone(), - target: None, + target: Vec::new(), }) } } diff --git a/test/src/simulator.rs b/test/src/simulator.rs index c5505d5b..4e643e5b 100644 --- a/test/src/simulator.rs +++ b/test/src/simulator.rs @@ -115,7 +115,9 @@ where ); match operation.finish() { - widget::operation::Outcome::Some(Some(target)) => Ok(target), + widget::operation::Outcome::Some(matches) => { + matches.first().copied().ok_or(Error::NotFound(selector)) + } _ => Err(Error::NotFound(selector)), } } diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index fa3dd770..c7ba113c 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -1631,6 +1631,14 @@ impl operation::Focusable for State

{ } impl operation::TextInput for State

{ + fn text(&self) -> &str { + if self.value.content().is_empty() { + self.placeholder.content() + } else { + self.value.content() + } + } + fn move_cursor_to_front(&mut self) { State::move_cursor_to_front(self); } From c8ccba5535f7853845f7ab0517f61d7e570db307 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 20 Aug 2025 14:03:51 +0200 Subject: [PATCH 28/83] Use `mouse::Button::Left` by default in ice tests --- examples/todos/tests/carl_sagan.ice | 6 +++--- test/src/instruction.rs | 30 ++++++++++++++++++++++------- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/examples/todos/tests/carl_sagan.ice b/examples/todos/tests/carl_sagan.ice index 7c94ff3a..1a1b7f08 100644 --- a/examples/todos/tests/carl_sagan.ice +++ b/examples/todos/tests/carl_sagan.ice @@ -2,11 +2,11 @@ viewport: 512x768 mode: impatient preset: Empty ----- -click left at "What needs to be done?" +click at "What needs to be done?" type "Create the universe" type enter type "Make an apple pie" type enter -click left at "Create the universe" -click left at "Make an apple pie" +click at "Create the universe" +click at "Make an apple pie" expect "0 tasks left" diff --git a/test/src/instruction.rs b/test/src/instruction.rs index 539c61c5..e356e028 100644 --- a/test/src/instruction.rs +++ b/test/src/instruction.rs @@ -329,16 +329,22 @@ mod format { use super::*; pub fn button_at(button: mouse::Button, at: Option<&Target>) -> String { + let button = self::button(button); + if let Some(at) = at { - format!("{} at {}", self::button(button), at) + if button.is_empty() { + format!("at {}", at) + } else { + format!("{} at {}", button, at) + } } else { - self::button(button).to_owned() + button.to_owned() } } pub fn button(button: mouse::Button) -> &'static str { match button { - mouse::Button::Left => "left", + mouse::Button::Left => "", mouse::Button::Right => "right", mouse::Button::Middle => "middle", mouse::Button::Back => "back", @@ -388,7 +394,8 @@ mod parser { use nom::branch::alt; use nom::bytes::complete::tag; use nom::character::complete::{char, multispace0, satisfy}; - use nom::combinator::{cut, map, opt, recognize}; + use nom::combinator::{cut, map, opt, recognize, success}; + use nom::error::ParseError; use nom::multi::many0; use nom::number::float; use nom::sequence::{delimited, preceded, separated_pair}; @@ -447,7 +454,7 @@ mod parser { fn target(input: &str) -> IResult<&str, Target> { preceded( - tag(" at "), + whitespace(tag("at ")), cut(alt((string.map(Target::Text), point.map(Target::Point)))), ) .parse(input) @@ -455,8 +462,8 @@ mod parser { fn mouse_button(input: &str) -> IResult<&str, mouse::Button> { alt(( - tag("left").map(|_| mouse::Button::Left), tag("right").map(|_| mouse::Button::Right), + success(mouse::Button::Left), )) .parse(input) } @@ -497,7 +504,7 @@ mod parser { } fn point(input: &str) -> IResult<&str, Point> { - let comma = (multispace0, char(','), multispace0); + let comma = whitespace(char(',')); map( delimited( @@ -509,4 +516,13 @@ mod parser { ) .parse(input) } + + pub fn whitespace<'a, O, E: ParseError<&'a str>, F>( + inner: F, + ) -> impl Parser<&'a str, Output = O, Error = E> + where + F: Parser<&'a str, Output = O, Error = E>, + { + delimited(multispace0, inner, multispace0) + } } From b20168b614deea8090d83225b9ce4694297b0c2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 20 Aug 2025 14:43:37 +0200 Subject: [PATCH 29/83] Capitalize `mode` value in ice metadata --- examples/todos/src/main.rs | 5 +---- examples/todos/tests/carl_sagan.ice | 4 +++- test/src/ice.rs | 6 +++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 10549117..e0c60e94 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -586,10 +586,7 @@ fn presets() -> impl Iterator> [ Preset::new("Empty", || { - ( - Todos::Loading, - Command::done(Message::Loaded(Err(LoadError::File))), - ) + (Todos::Loaded(State::default()), Command::none()) }), Preset::new("Carl Sagan", || { ( diff --git a/examples/todos/tests/carl_sagan.ice b/examples/todos/tests/carl_sagan.ice index 1a1b7f08..cfad7a60 100644 --- a/examples/todos/tests/carl_sagan.ice +++ b/examples/todos/tests/carl_sagan.ice @@ -1,5 +1,5 @@ viewport: 512x768 -mode: impatient +mode: Impatient preset: Empty ----- click at "What needs to be done?" @@ -7,6 +7,8 @@ type "Create the universe" type enter type "Make an apple pie" type enter +expect "2 tasks left" click at "Create the universe" +expect "1 task left" click at "Make an apple pie" expect "0 tasks left" diff --git a/test/src/ice.rs b/test/src/ice.rs index 67037c14..ab1a9c8f 100644 --- a/test/src/ice.rs +++ b/test/src/ice.rs @@ -51,7 +51,7 @@ impl Ice { ); } "mode" => { - mode = Some(match value.trim() { + mode = Some(match value.trim().to_lowercase().as_str() { "patient" => emulator::Mode::Patient, "impatient" => emulator::Mode::Impatient, _ => { @@ -118,8 +118,8 @@ impl std::fmt::Display for Ice { f, "mode: {}", match self.mode { - emulator::Mode::Patient => "patient", - emulator::Mode::Impatient => "impatient", + emulator::Mode::Patient => "Patient", + emulator::Mode::Impatient => "Impatient", } )?; From 2532099efc83a4f3a178ee4782aa9d385c7b3e68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 20 Aug 2025 15:07:08 +0200 Subject: [PATCH 30/83] Disable `tester` feature in `todos` by default --- examples/todos/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml index 3dca5c49..9869a2c5 100644 --- a/examples/todos/Cargo.toml +++ b/examples/todos/Cargo.toml @@ -6,7 +6,6 @@ edition = "2024" publish = false [features] -default = ["tester"] test = ["iced/test"] tester = ["test", "iced/tester"] From 8ca25d627f23ea46ada2cfe3291bf982c173f976 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 20 Aug 2025 15:31:12 +0200 Subject: [PATCH 31/83] Make `recorder` widget produce `Interaction` --- devtools/src/tester.rs | 11 +++--- devtools/src/tester/recorder.rs | 67 ++++++++++++++------------------- test/src/instruction.rs | 20 +++++----- 3 files changed, 45 insertions(+), 53 deletions(-) diff --git a/devtools/src/tester.rs b/devtools/src/tester.rs index 13b6e9e2..96715a4d 100644 --- a/devtools/src/tester.rs +++ b/devtools/src/tester.rs @@ -8,7 +8,7 @@ use crate::core::Length::Fill; use crate::core::alignment::Horizontal::Right; use crate::core::border; use crate::core::window; -use crate::core::{Element, Event, Font, Size, Theme}; +use crate::core::{Element, Font, Size, Theme}; use crate::executor; use crate::futures::futures::channel::mpsc; use crate::icon; @@ -75,7 +75,7 @@ pub enum Message { pub enum Tick { Tester(Message), Program(P::Message), - Recorder(Event), + Recorder(instruction::Interaction), Emulator(emulator::Event

), } @@ -316,9 +316,8 @@ impl Tester

{ Task::none() } - Tick::Recorder(event) => { - let mut interaction = - instruction::Interaction::from_event(event); + Tick::Recorder(interaction) => { + let mut interaction = Some(interaction); while let Some(new_interaction) = interaction.take() { if let Some(Instruction::Interact(last_interaction)) = @@ -441,7 +440,7 @@ impl Tester

{ Element::from( recorder(themer(theme, view)) - .on_event(Tick::Recorder), + .on_record(Tick::Recorder), ) .map(emulate) } diff --git a/devtools/src/tester/recorder.rs b/devtools/src/tester/recorder.rs index 10760a1c..e1aa5655 100644 --- a/devtools/src/tester/recorder.rs +++ b/devtools/src/tester/recorder.rs @@ -8,6 +8,7 @@ use crate::core::{ self, Clipboard, Element, Event, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget, }; +use crate::test::instruction::Interaction; pub fn recorder<'a, Message, Theme, Renderer>( content: impl Into>, @@ -18,7 +19,7 @@ pub fn recorder<'a, Message, Theme, Renderer>( #[allow(missing_debug_implementations)] pub struct Recorder<'a, Message, Theme, Renderer> { content: Element<'a, Message, Theme, Renderer>, - on_event: Option Message + 'a>>, + on_record: Option Message + 'a>>, } impl<'a, Message, Theme, Renderer> Recorder<'a, Message, Theme, Renderer> { @@ -27,15 +28,15 @@ impl<'a, Message, Theme, Renderer> Recorder<'a, Message, Theme, Renderer> { ) -> Self { Self { content: content.into(), - on_event: None, + on_record: None, } } - pub fn on_event( + pub fn on_record( mut self, - on_event: impl Fn(Event) -> Message + 'a, + on_record: impl Fn(Interaction) -> Message + 'a, ) -> Self { - self.on_event = Some(Box::new(on_event)); + self.on_record = Some(Box::new(on_record)); self } } @@ -64,8 +65,8 @@ where state, event, layout, cursor, renderer, clipboard, shell, viewport, ); - if let Some(on_event) = &self.on_event { - record(event, cursor, shell, layout.bounds(), on_event); + if let Some(on_record) = &self.on_record { + record(event, cursor, shell, layout.bounds(), on_record); } } @@ -157,7 +158,7 @@ where overlay::Element::new(Box::new(Overlay { raw, bounds: layout.bounds(), - on_event: self.on_event.as_deref(), + on_record: self.on_record.as_deref(), })) }) } @@ -178,7 +179,7 @@ where struct Overlay<'a, Message, Theme, Renderer> { raw: overlay::Element<'a, Message, Theme, Renderer>, bounds: Rectangle, - on_event: Option<&'a dyn Fn(Event) -> Message>, + on_record: Option<&'a dyn Fn(Interaction) -> Message>, } impl<'a, Message, Theme, Renderer> core::Overlay @@ -233,7 +234,7 @@ where .as_overlay_mut() .update(event, layout, cursor, renderer, clipboard, shell); - if let Some(on_event) = &self.on_event { + if let Some(on_event) = &self.on_record { record(event, cursor, shell, self.bounds, on_event); } } @@ -261,7 +262,7 @@ where overlay::Element::new(Box::new(Overlay { raw, bounds: self.bounds, - on_event: self.on_event, + on_record: self.on_record, })) }) } @@ -276,34 +277,24 @@ fn record( cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, bounds: Rectangle, - on_event: impl Fn(Event) -> Message, + on_record: impl Fn(Interaction) -> Message, ) { - match event { - Event::Mouse(event) => { - if !cursor.is_over(bounds) { - return; - } + if let Event::Mouse(_) = event + && !cursor.is_over(bounds) + { + return; + } - match event { - mouse::Event::ButtonPressed(_) - | mouse::Event::ButtonReleased(_) - | mouse::Event::WheelScrolled { .. } => { - shell.publish(on_event(Event::Mouse(*event))); - } - mouse::Event::CursorMoved { position } => { - shell.publish(on_event(Event::Mouse( - mouse::Event::CursorMoved { - position: *position - - (bounds.position() - Point::ORIGIN), - }, - ))); - } - _ => {} - } - } - Event::Keyboard(event) => { - shell.publish(on_event(Event::Keyboard(event.clone()))); - } - _ => {} + let interaction = + if let Event::Mouse(mouse::Event::CursorMoved { position }) = event { + Interaction::from_event(&Event::Mouse(mouse::Event::CursorMoved { + position: *position - (bounds.position() - Point::ORIGIN), + })) + } else { + Interaction::from_event(event) + }; + + if let Some(interaction) = interaction { + shell.publish(on_record(interaction)); } } diff --git a/test/src/instruction.rs b/test/src/instruction.rs index e356e028..1d3dd025 100644 --- a/test/src/instruction.rs +++ b/test/src/instruction.rs @@ -33,18 +33,20 @@ pub enum Interaction { } impl Interaction { - pub fn from_event(event: Event) -> Option { + pub fn from_event(event: &Event) -> Option { Some(match event { Event::Mouse(mouse) => Self::Mouse(match mouse { mouse::Event::CursorMoved { position } => { - Mouse::Move(Target::Point(position)) - } - mouse::Event::ButtonPressed(button) => { - Mouse::Press { button, at: None } - } - mouse::Event::ButtonReleased(button) => { - Mouse::Release { button, at: None } + Mouse::Move(Target::Point(*position)) } + mouse::Event::ButtonPressed(button) => Mouse::Press { + button: *button, + at: None, + }, + mouse::Event::ButtonReleased(button) => Mouse::Release { + button: *button, + at: None, + }, _ => None?, }), Event::Keyboard(keyboard) => Self::Keyboard(match keyboard { @@ -61,7 +63,7 @@ impl Interaction { keyboard::Key::Named(keyboard::key::Named::Backspace) => { Keyboard::Press(Key::Backspace) } - _ => Keyboard::Typewrite(text?.to_string()), + _ => Keyboard::Typewrite(text.as_ref()?.to_string()), }, keyboard::Event::KeyReleased { key, .. } => match key { keyboard::Key::Named(keyboard::key::Named::Enter) => { From 63142d34fc440b18106aee8b45de195487d98c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 23 Aug 2025 01:44:17 +0200 Subject: [PATCH 32/83] Introduce new `iced_selector` subcrate and refactor `Operation` --- Cargo.lock | 8 + Cargo.toml | 2 + core/src/overlay/group.rs | 2 +- core/src/widget/operation.rs | 132 ++++++------ core/src/widget/operation/focusable.rs | 60 ++---- core/src/widget/operation/scrollable.rs | 27 +-- core/src/widget/operation/text_input.rs | 36 +--- examples/toast/src/main.rs | 6 +- examples/todos/src/main.rs | 4 +- selector/Cargo.toml | 17 ++ selector/src/find.rs | 270 ++++++++++++++++++++++++ selector/src/lib.rs | 113 ++++++++++ selector/src/target.rs | 238 +++++++++++++++++++++ test/Cargo.toml | 1 + test/src/emulator.rs | 19 +- test/src/error.rs | 10 +- test/src/lib.rs | 3 +- test/src/selector.rs | 208 ------------------ test/src/simulator.rs | 44 ++-- widget/src/button.rs | 3 +- widget/src/column.rs | 3 +- widget/src/container.rs | 117 ++-------- widget/src/grid.rs | 3 +- widget/src/keyed/column.rs | 3 +- widget/src/lib.rs | 2 + widget/src/pane_grid.rs | 3 +- widget/src/row.rs | 3 +- widget/src/scrollable.rs | 20 +- widget/src/stack.rs | 3 +- 29 files changed, 839 insertions(+), 521 deletions(-) create mode 100644 selector/Cargo.toml create mode 100644 selector/src/find.rs create mode 100644 selector/src/lib.rs create mode 100644 selector/src/target.rs delete mode 100644 test/src/selector.rs diff --git a/Cargo.lock b/Cargo.lock index ecdcd3e0..cda9648a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2627,6 +2627,13 @@ dependencies = [ "thiserror 2.0.14", ] +[[package]] +name = "iced_selector" +version = "0.14.0-dev" +dependencies = [ + "iced_core", +] + [[package]] name = "iced_test" version = "0.14.0-dev" @@ -2634,6 +2641,7 @@ dependencies = [ "iced_program", "iced_renderer", "iced_runtime", + "iced_selector", "nom 8.0.0", "png", "sha2", diff --git a/Cargo.toml b/Cargo.toml index de46980a..432a9cd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -132,6 +132,7 @@ members = [ "program", "renderer", "runtime", + "selector", "test", "tiny_skia", "wgpu", @@ -163,6 +164,7 @@ iced_highlighter = { version = "0.14.0-dev", path = "highlighter" } iced_program = { version = "0.14.0-dev", path = "program" } iced_renderer = { version = "0.14.0-dev", path = "renderer" } iced_runtime = { version = "0.14.0-dev", path = "runtime" } +iced_selector = { version = "0.14.0-dev", path = "selector" } iced_test = { version = "0.14.0-dev", path = "test" } iced_tiny_skia = { version = "0.14.0-dev", path = "tiny_skia" } iced_wgpu = { version = "0.14.0-dev", path = "wgpu" } diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs index 145ee21d..cb734996 100644 --- a/core/src/overlay/group.rs +++ b/core/src/overlay/group.rs @@ -127,7 +127,7 @@ where renderer: &Renderer, operation: &mut dyn widget::Operation, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.traverse(&mut |operation| { self.children.iter_mut().zip(layout.children()).for_each( |(child, layout)| { child.as_overlay_mut().operate(layout, renderer, operation); diff --git a/core/src/widget/operation.rs b/core/src/widget/operation.rs index 8fc627bf..070b369c 100644 --- a/core/src/widget/operation.rs +++ b/core/src/widget/operation.rs @@ -18,25 +18,16 @@ use std::sync::Arc; /// A piece of logic that can traverse the widget tree of an application in /// order to query or update some widget state. pub trait Operation: Send { - /// Operates on a widget that contains other widgets. + /// Requests further traversal of the widget tree to keep operating. /// - /// The `operate_on_children` function can be called to return control to - /// the widget tree and keep traversing it. - fn container( - &mut self, - id: Option<&Id>, - bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ); + /// The provided `operate` closure may be called by an [`Operation`] + /// to return control to the widget tree and keep traversing it. If + /// the closure is not called, the children of the widget asking for + /// traversal will be skipped. + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)); - /// Operates on a widget that can be focused. - fn focusable( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - _state: &mut dyn Focusable, - ) { - } + /// Operates on a widget that contains other widgets. + fn container(&mut self, _id: Option<&Id>, _bounds: Rectangle) {} /// Operates on a widget that can be scrolled. fn scrollable( @@ -49,6 +40,15 @@ pub trait Operation: Send { ) { } + /// Operates on a widget that can be focused. + fn focusable( + &mut self, + _id: Option<&Id>, + _bounds: Rectangle, + _state: &mut dyn Focusable, + ) { + } + /// Operates on a widget that has text input. fn text_input( &mut self, @@ -80,13 +80,12 @@ impl Operation for Box where T: Operation + ?Sized, { - fn container( - &mut self, - id: Option<&Id>, - bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - self.as_mut().container(id, bounds, operate_on_children); + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { + self.as_mut().traverse(operate); + } + + fn container(&mut self, id: Option<&Id>, bounds: Rectangle) { + self.as_mut().container(id, bounds); } fn focusable( @@ -179,17 +178,19 @@ where } impl Operation for BlackBox<'_, T> { - fn container( - &mut self, - id: Option<&Id>, - bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - self.operation.container(id, bounds, &mut |operation| { - operate_on_children(&mut BlackBox { operation }); + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) + where + Self: Sized, + { + self.operation.traverse(&mut |operation| { + operate(&mut BlackBox { operation }); }); } + fn container(&mut self, id: Option<&Id>, bounds: Rectangle) { + self.operation.container(id, bounds); + } + fn focusable( &mut self, id: Option<&Id>, @@ -267,28 +268,25 @@ where A: 'static, B: 'static, { - fn container( - &mut self, - id: Option<&Id>, - bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { struct MapRef<'a, A> { operation: &'a mut dyn Operation, } impl Operation for MapRef<'_, A> { - fn container( + fn traverse( &mut self, - id: Option<&Id>, - bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), + operate: &mut dyn FnMut(&mut dyn Operation), ) { + self.operation.traverse(&mut |operation| { + operate(&mut MapRef { operation }); + }); + } + + fn container(&mut self, id: Option<&Id>, bounds: Rectangle) { let Self { operation, .. } = self; - operation.container(id, bounds, &mut |operation| { - operate_on_children(&mut MapRef { operation }); - }); + operation.container(id, bounds); } fn scrollable( @@ -345,9 +343,13 @@ where } } - let Self { operation, .. } = self; + self.operation.traverse(&mut |operation| { + operate(&mut MapRef { operation }); + }); + } - MapRef { operation }.container(id, bounds, operate_on_children); + fn container(&mut self, id: Option<&Id>, bounds: Rectangle) { + self.operation.container(id, bounds); } fn focusable( @@ -444,17 +446,16 @@ where A: 'static, B: Send + 'static, { - fn container( - &mut self, - id: Option<&Id>, - bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - self.operation.container(id, bounds, &mut |operation| { - operate_on_children(&mut black_box(operation)); + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { + self.operation.traverse(&mut |operation| { + operate(&mut black_box(operation)); }); } + fn container(&mut self, id: Option<&Id>, bounds: Rectangle) { + self.operation.container(id, bounds); + } + fn focusable( &mut self, id: Option<&Id>, @@ -531,21 +532,26 @@ pub fn scope( ) -> impl Operation { struct ScopedOperation { target: Id, + current: Option, operation: Box>, } impl Operation for ScopedOperation { - fn container( + fn traverse( &mut self, - id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), + operate: &mut dyn FnMut(&mut dyn Operation), ) { - if id == Some(&self.target) { - operate_on_children(self.operation.as_mut()); + if self.current.as_ref() == Some(&self.target) { + self.operation.as_mut().traverse(operate); } else { - operate_on_children(self); + operate(self); } + + self.current = None; + } + + fn container(&mut self, id: Option<&Id>, _bounds: Rectangle) { + self.current = id.cloned(); } fn finish(&self) -> Outcome { @@ -553,6 +559,7 @@ pub fn scope( Outcome::Chain(next) => { Outcome::Chain(Box::new(ScopedOperation { target: self.target.clone(), + current: None, operation: next, })) } @@ -563,6 +570,7 @@ pub fn scope( ScopedOperation { target, + current: None, operation: Box::new(operation), } } diff --git a/core/src/widget/operation/focusable.rs b/core/src/widget/operation/focusable.rs index 44c9d647..5b066f2d 100644 --- a/core/src/widget/operation/focusable.rs +++ b/core/src/widget/operation/focusable.rs @@ -48,13 +48,8 @@ pub fn focus(target: Id) -> impl Operation { } } - fn container( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self); + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { + operate(self); } } @@ -75,13 +70,8 @@ pub fn unfocus() -> impl Operation { state.unfocus(); } - fn container( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self); + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { + operate(self); } } @@ -109,13 +99,11 @@ pub fn count() -> impl Operation { self.count.total += 1; } - fn container( + fn traverse( &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), + operate: &mut dyn FnMut(&mut dyn Operation), ) { - operate_on_children(self); + operate(self); } fn finish(&self) -> Outcome { @@ -163,13 +151,8 @@ where self.current += 1; } - fn container( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self); + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { + operate(self); } } @@ -205,13 +188,8 @@ where self.current += 1; } - fn container( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self); + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { + operate(self); } } @@ -237,13 +215,11 @@ pub fn find_focused() -> impl Operation { } } - fn container( + fn traverse( &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), + operate: &mut dyn FnMut(&mut dyn Operation), ) { - operate_on_children(self); + operate(self); } fn finish(&self) -> Outcome { @@ -279,17 +255,15 @@ pub fn is_focused(target: Id) -> impl Operation { } } - fn container( + fn traverse( &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), + operate: &mut dyn FnMut(&mut dyn Operation), ) { if self.is_focused.is_some() { return; } - operate_on_children(self); + operate(self); } fn finish(&self) -> Outcome { diff --git a/core/src/widget/operation/scrollable.rs b/core/src/widget/operation/scrollable.rs index 7c78c087..25a03434 100644 --- a/core/src/widget/operation/scrollable.rs +++ b/core/src/widget/operation/scrollable.rs @@ -28,13 +28,8 @@ pub fn snap_to(target: Id, offset: RelativeOffset) -> impl Operation { } impl Operation for SnapTo { - fn container( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self); + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { + operate(self); } fn scrollable( @@ -63,13 +58,8 @@ pub fn scroll_to(target: Id, offset: AbsoluteOffset) -> impl Operation { } impl Operation for ScrollTo { - fn container( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self); + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { + operate(self); } fn scrollable( @@ -98,13 +88,8 @@ pub fn scroll_by(target: Id, offset: AbsoluteOffset) -> impl Operation { } impl Operation for ScrollBy { - fn container( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self); + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { + operate(self); } fn scrollable( diff --git a/core/src/widget/operation/text_input.rs b/core/src/widget/operation/text_input.rs index 6bcae385..de2ac1a0 100644 --- a/core/src/widget/operation/text_input.rs +++ b/core/src/widget/operation/text_input.rs @@ -45,13 +45,8 @@ pub fn move_cursor_to_front(target: Id) -> impl Operation { } } - fn container( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self); + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { + operate(self); } } @@ -80,13 +75,8 @@ pub fn move_cursor_to_end(target: Id) -> impl Operation { } } - fn container( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self); + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { + operate(self); } } @@ -116,13 +106,8 @@ pub fn move_cursor_to(target: Id, position: usize) -> impl Operation { } } - fn container( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self); + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { + operate(self); } } @@ -150,13 +135,8 @@ pub fn select_all(target: Id) -> impl Operation { } } - fn container( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self); + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) { + operate(self); } } diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index b4e93a32..e825f4f3 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -349,7 +349,8 @@ mod toast { renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { self.content.as_widget().operate( &mut state.children[0], layout, @@ -580,7 +581,8 @@ mod toast { renderer: &Renderer, operation: &mut dyn widget::Operation, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { self.toasts .iter() .zip(self.state.iter_mut()) diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index e0c60e94..5dab262c 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -613,8 +613,8 @@ fn presets() -> impl Iterator> mod tests { use super::*; + use iced::widget; use iced::{Settings, Theme}; - use iced_test::selector::id; use iced_test::{Error, Simulator}; fn simulator(todos: &Todos) -> Simulator<'_, Message> { @@ -633,7 +633,7 @@ mod tests { let _command = todos.update(Message::Loaded(Err(LoadError::File))); let mut ui = simulator(&todos); - let _input = ui.click(id("new-task"))?; + let _input = ui.click(widget::Id::new("new-task"))?; let _ = ui.typewrite("Create the universe"); let _ = ui.tap_key(keyboard::key::Named::Enter); diff --git a/selector/Cargo.toml b/selector/Cargo.toml new file mode 100644 index 00000000..67488d12 --- /dev/null +++ b/selector/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "iced_selector" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +categories.workspace = true +keywords.workspace = true +rust-version.workspace = true + +[dependencies] +iced_core.workspace = true + +[lints] +workspace = true diff --git a/selector/src/find.rs b/selector/src/find.rs new file mode 100644 index 00000000..15016933 --- /dev/null +++ b/selector/src/find.rs @@ -0,0 +1,270 @@ +use crate::core::widget::operation::{ + Focusable, Outcome, Scrollable, TextInput, +}; +use crate::core::widget::{Id, Operation}; +use crate::core::{Rectangle, Vector}; +use crate::{Selector, Target}; + +use std::any::Any; + +pub type Find = Finder>; +pub type FindAll = Finder>; + +#[derive(Debug)] +pub struct One +where + S: Selector, +{ + selector: S, + output: Option, +} + +impl One +where + S: Selector, +{ + pub fn new(selector: S) -> Self { + Self { + selector, + output: None, + } + } +} + +impl Strategy for One +where + S: Selector, + S::Output: Clone, +{ + type Output = Option; + + fn feed(&mut self, target: Target<'_>) { + if let Some(output) = self.selector.select(target) { + self.output = Some(output); + } + } + + fn is_done(&self) -> bool { + self.output.is_some() + } + + fn finish(&self) -> Self::Output { + self.output.clone() + } +} + +#[derive(Debug)] +pub struct All +where + S: Selector, +{ + selector: S, + outputs: Vec, +} + +impl All +where + S: Selector, +{ + pub fn new(selector: S) -> Self { + Self { + selector, + outputs: Vec::new(), + } + } +} + +impl Strategy for All +where + S: Selector, + S::Output: Clone, +{ + type Output = Vec; + + fn feed(&mut self, target: Target<'_>) { + if let Some(output) = self.selector.select(target) { + self.outputs.push(output); + } + } + + fn is_done(&self) -> bool { + false + } + + fn finish(&self) -> Self::Output { + self.outputs.clone() + } +} + +pub trait Strategy { + type Output; + + fn feed(&mut self, target: Target<'_>); + + fn is_done(&self) -> bool; + + fn finish(&self) -> Self::Output; +} + +#[derive(Debug)] +pub struct Finder { + strategy: S, + stack: Vec<(Rectangle, Vector)>, + viewport: Rectangle, + translation: Vector, +} + +impl Finder { + pub fn new(strategy: S) -> Self { + Self { + strategy, + stack: vec![(Rectangle::INFINITE, Vector::ZERO)], + viewport: Rectangle::INFINITE, + translation: Vector::ZERO, + } + } +} + +impl Operation for Finder +where + S: Strategy + Send, + S::Output: Send, +{ + fn traverse( + &mut self, + operate: &mut dyn FnMut(&mut dyn Operation), + ) { + if self.strategy.is_done() { + return; + } + + self.stack.push((self.viewport, self.translation)); + operate(self); + let _ = self.stack.pop(); + + let (viewport, translation) = self.stack.last().unwrap(); + self.viewport = *viewport; + self.translation = *translation; + } + + fn container(&mut self, id: Option<&Id>, bounds: Rectangle) { + if self.strategy.is_done() { + return; + } + + self.strategy.feed(Target::Container { + id, + bounds, + visible_bounds: self + .viewport + .intersection(&(bounds + self.translation)), + }); + } + + fn focusable( + &mut self, + id: Option<&Id>, + bounds: Rectangle, + state: &mut dyn Focusable, + ) { + if self.strategy.is_done() { + return; + } + + self.strategy.feed(Target::Focusable { + id, + bounds, + visible_bounds: self + .viewport + .intersection(&(bounds + self.translation)), + state, + }); + } + + fn scrollable( + &mut self, + id: Option<&Id>, + bounds: Rectangle, + content_bounds: Rectangle, + translation: Vector, + state: &mut dyn Scrollable, + ) { + if self.strategy.is_done() { + return; + } + + let visible_bounds = + self.viewport.intersection(&(bounds + self.translation)); + + self.strategy.feed(Target::Scrollable { + id, + bounds, + visible_bounds, + content_bounds, + translation, + state, + }); + + self.translation = self.translation - translation; + self.viewport = visible_bounds.unwrap_or_default(); + } + + fn text_input( + &mut self, + id: Option<&Id>, + bounds: Rectangle, + state: &mut dyn TextInput, + ) { + if self.strategy.is_done() { + return; + } + + self.strategy.feed(Target::TextInput { + id, + bounds, + visible_bounds: self + .viewport + .intersection(&(bounds + self.translation)), + state, + }); + } + + fn text(&mut self, id: Option<&Id>, bounds: Rectangle, text: &str) { + if self.strategy.is_done() { + return; + } + + self.strategy.feed(Target::Text { + id, + bounds, + visible_bounds: self + .viewport + .intersection(&(bounds + self.translation)), + content: text, + }); + } + + fn custom( + &mut self, + id: Option<&Id>, + bounds: Rectangle, + state: &mut dyn Any, + ) { + if self.strategy.is_done() { + return; + } + + self.strategy.feed(Target::Custom { + id, + bounds, + visible_bounds: self + .viewport + .intersection(&(bounds + self.translation)), + state, + }); + } + + fn finish(&self) -> Outcome { + Outcome::Some(self.strategy.finish()) + } +} diff --git a/selector/src/lib.rs b/selector/src/lib.rs new file mode 100644 index 00000000..2163c502 --- /dev/null +++ b/selector/src/lib.rs @@ -0,0 +1,113 @@ +#![allow(missing_docs)] +use iced_core as core; + +pub mod target; + +mod find; + +pub use find::{Find, FindAll}; +pub use target::Target; + +use crate::core::widget::Id; + +pub trait Selector { + type Output; + + fn select(&mut self, target: Target<'_>) -> Option; + + fn description(&self) -> String; + + fn find(self) -> Find + where + Self: Sized, + { + Find::new(find::One::new(self)) + } + + fn find_all(self) -> FindAll + where + Self: Sized, + { + FindAll::new(find::All::new(self)) + } +} + +impl Selector for &str { + type Output = target::Text; + + fn select(&mut self, target: Target<'_>) -> Option { + match target { + Target::TextInput { + id, + bounds, + visible_bounds, + state, + } if state.text() == *self => Some(target::Text::Input { + id: id.cloned(), + bounds, + visible_bounds, + }), + Target::Text { + id, + bounds, + visible_bounds, + content, + } if content == *self => Some(target::Text::Raw { + id: id.cloned(), + bounds, + visible_bounds, + }), + _ => None, + } + } + + fn description(&self) -> String { + format!("text == \"{}\"", self.escape_default()) + } +} + +impl Selector for Id { + type Output = target::Match; + + fn select(&mut self, target: Target<'_>) -> Option { + if target.id() != Some(self) { + return None; + } + + Some(target::Match::from_target(target)) + } + + fn description(&self) -> String { + format!("id == \"{:?}\"", self) + } +} + +impl Selector for F +where + F: FnMut(Target<'_>) -> Option, +{ + type Output = T; + + fn select(&mut self, target: Target<'_>) -> Option { + (self)(target) + } + + fn description(&self) -> String { + format!("custom selector: {}", std::any::type_name_of_val(self)) + } +} + +// pub fn inspect(position: Point) -> impl Selector { +// visible(move |target: Target<'_>, visible_bounds: Rectangle| { +// visible_bounds +// .contains(position) +// .then(|| Match::from_target(target)) +// }) +// } + +// pub fn visible( +// f: impl Fn(Target<'_>, Rectangle) -> Option, +// ) -> impl Selector { +// todo!() +// } +// diff --git a/selector/src/target.rs b/selector/src/target.rs new file mode 100644 index 00000000..fb2e6d0f --- /dev/null +++ b/selector/src/target.rs @@ -0,0 +1,238 @@ +use crate::core::widget::Id; +use crate::core::widget::operation::{Focusable, Scrollable, TextInput}; +use crate::core::{Rectangle, Vector}; + +use std::any::Any; + +#[derive(Clone)] +#[allow(missing_debug_implementations)] +pub enum Target<'a> { + Container { + id: Option<&'a Id>, + bounds: Rectangle, + visible_bounds: Option, + }, + Focusable { + id: Option<&'a Id>, + bounds: Rectangle, + visible_bounds: Option, + state: &'a dyn Focusable, + }, + Scrollable { + id: Option<&'a Id>, + bounds: Rectangle, + content_bounds: Rectangle, + visible_bounds: Option, + translation: Vector, + state: &'a dyn Scrollable, + }, + TextInput { + id: Option<&'a Id>, + bounds: Rectangle, + visible_bounds: Option, + state: &'a dyn TextInput, + }, + Text { + id: Option<&'a Id>, + bounds: Rectangle, + visible_bounds: Option, + content: &'a str, + }, + Custom { + id: Option<&'a Id>, + bounds: Rectangle, + visible_bounds: Option, + state: &'a dyn Any, + }, +} + +impl<'a> Target<'a> { + pub fn id(&self) -> Option<&'a Id> { + match self { + Target::Container { id, .. } + | Target::Focusable { id, .. } + | Target::Scrollable { id, .. } + | Target::TextInput { id, .. } + | Target::Text { id, .. } + | Target::Custom { id, .. } => *id, + } + } + + pub fn bounds(&self) -> Rectangle { + match self { + Target::Container { bounds, .. } + | Target::Focusable { bounds, .. } + | Target::Scrollable { bounds, .. } + | Target::TextInput { bounds, .. } + | Target::Text { bounds, .. } + | Target::Custom { bounds, .. } => *bounds, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Match { + Container { + id: Option, + bounds: Rectangle, + visible_bounds: Option, + }, + Focusable { + id: Option, + bounds: Rectangle, + visible_bounds: Option, + }, + Scrollable { + id: Option, + bounds: Rectangle, + visible_bounds: Option, + content_bounds: Rectangle, + translation: Vector, + }, + TextInput { + id: Option, + bounds: Rectangle, + visible_bounds: Option, + }, + Text { + id: Option, + bounds: Rectangle, + visible_bounds: Option, + content: String, + }, + Custom { + id: Option, + bounds: Rectangle, + visible_bounds: Option, + }, +} + +impl Match { + pub fn from_target(target: Target<'_>) -> Self { + match target { + Target::Container { + id, + bounds, + visible_bounds, + } => Self::Container { + id: id.cloned(), + bounds, + visible_bounds, + }, + Target::Focusable { + id, + bounds, + visible_bounds, + .. + } => Self::Focusable { + id: id.cloned(), + bounds, + visible_bounds, + }, + Target::Scrollable { + id, + bounds, + visible_bounds, + content_bounds, + translation, + .. + } => Self::Scrollable { + id: id.cloned(), + bounds, + visible_bounds, + content_bounds, + translation, + }, + Target::TextInput { + id, + bounds, + visible_bounds, + .. + } => Self::TextInput { + id: id.cloned(), + bounds, + visible_bounds, + }, + Target::Text { + id, + bounds, + visible_bounds, + content, + } => Self::Text { + id: id.cloned(), + bounds, + visible_bounds, + content: content.to_owned(), + }, + Target::Custom { + id, + bounds, + visible_bounds, + .. + } => Self::Custom { + id: id.cloned(), + bounds, + visible_bounds, + }, + } + } +} + +impl Bounded for Match { + fn bounds(&self) -> Rectangle { + match self { + Match::Container { bounds, .. } + | Match::Focusable { bounds, .. } + | Match::Scrollable { bounds, .. } + | Match::TextInput { bounds, .. } + | Match::Text { bounds, .. } + | Match::Custom { bounds, .. } => *bounds, + } + } + + fn visible_bounds(&self) -> Option { + match self { + Match::Container { visible_bounds, .. } + | Match::Focusable { visible_bounds, .. } + | Match::Scrollable { visible_bounds, .. } + | Match::TextInput { visible_bounds, .. } + | Match::Text { visible_bounds, .. } + | Match::Custom { visible_bounds, .. } => *visible_bounds, + } + } +} + +pub trait Bounded: std::fmt::Debug { + fn bounds(&self) -> Rectangle; + + fn visible_bounds(&self) -> Option; +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Text { + Raw { + id: Option, + bounds: Rectangle, + visible_bounds: Option, + }, + Input { + id: Option, + bounds: Rectangle, + visible_bounds: Option, + }, +} + +impl Bounded for Text { + fn bounds(&self) -> Rectangle { + match self { + Text::Raw { bounds, .. } | Text::Input { bounds, .. } => *bounds, + } + } + + fn visible_bounds(&self) -> Option { + match self { + Text::Raw { visible_bounds, .. } + | Text::Input { visible_bounds, .. } => *visible_bounds, + } + } +} diff --git a/test/Cargo.toml b/test/Cargo.toml index dd262ffc..af15020e 100644 --- a/test/Cargo.toml +++ b/test/Cargo.toml @@ -15,6 +15,7 @@ workspace = true [dependencies] iced_runtime.workspace = true +iced_selector.workspace = true iced_program.workspace = true iced_program.features = ["test"] diff --git a/test/src/emulator.rs b/test/src/emulator.rs index 710e5b1b..b51d4a5c 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -1,4 +1,3 @@ -use crate::Instruction; use crate::core; use crate::core::mouse; use crate::core::renderer; @@ -17,6 +16,7 @@ use crate::runtime::user_interface; use crate::runtime::window; use crate::runtime::{Action, Task, UserInterface}; use crate::selector; +use crate::{Instruction, Selector}; use std::fmt; @@ -216,10 +216,10 @@ impl Emulator

{ let Some(events) = interaction.events(|target| match target { instruction::Target::Point(position) => Some(*position), instruction::Target::Text(text) => { + use selector::target::Bounded; use widget::Operation; - let mut operation = - selector::text(text.to_owned()).operation(); + let mut operation = Selector::find(text.as_str()); user_interface.operate( &self.renderer, @@ -227,11 +227,8 @@ impl Emulator

{ ); match operation.finish() { - widget::operation::Outcome::Some(matches) => { - matches - .first() - .copied() - .map(|target| target.bounds.center()) + widget::operation::Outcome::Some(text) => { + Some(text?.visible_bounds()?.center()) } _ => None, } @@ -273,7 +270,7 @@ impl Emulator

{ instruction::Expectation::Text(text) => { use widget::Operation; - let mut operation = selector::text(text).operation(); + let mut operation = Selector::find(text.as_str()); user_interface.operate( &self.renderer, @@ -281,9 +278,7 @@ impl Emulator

{ ); match operation.finish() { - widget::operation::Outcome::Some(matches) - if matches.len() == 1 => - { + widget::operation::Outcome::Some(Some(_text)) => { self.runtime.send(Event::Ready); } _ => { diff --git a/test/src/error.rs b/test/src/error.rs index ae475f16..45269a81 100644 --- a/test/src/error.rs +++ b/test/src/error.rs @@ -1,5 +1,3 @@ -use crate::Selector; - use std::io; use std::sync::Arc; @@ -7,8 +5,12 @@ use std::sync::Arc; #[derive(Debug, Clone, thiserror::Error)] pub enum Error { /// No matching widget was found for the [`Selector`]. - #[error("no matching widget was found for the selector: {0:?}")] - NotFound(Selector), + #[error("no matching widget was found for the selector: {selector}")] + NotFound { selector: String }, + #[error("the matching target is not visible: {target:?}")] + NotVisible { + target: Arc, + }, /// An IO operation failed. #[error("an IO operation failed: {0}")] IOFailed(Arc), diff --git a/test/src/lib.rs b/test/src/lib.rs index 150521f0..4ba2fe9c 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -89,10 +89,11 @@ use iced_renderer as renderer; use iced_runtime as runtime; use iced_runtime::core; +pub use iced_selector as selector; + pub mod emulator; pub mod ice; pub mod instruction; -pub mod selector; pub mod simulator; mod error; diff --git a/test/src/selector.rs b/test/src/selector.rs deleted file mode 100644 index fa6fce5a..00000000 --- a/test/src/selector.rs +++ /dev/null @@ -1,208 +0,0 @@ -//! Select widgets of a user interface. -use crate::core::text; -use crate::core::widget; -use crate::core::{Rectangle, Vector}; - -/// A selector describes a strategy to find a certain widget in a user interface. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Selector { - /// Find the widget with the given [`widget::Id`]. - Id(widget::Id), - /// Find the widget containing the given [`text::Fragment`]. - Text(text::Fragment<'static>), -} - -impl Selector { - pub fn operation<'a>(&self) -> impl widget::Operation> + 'a { - match self { - Selector::Id(id) => { - struct FindById { - id: widget::Id, - target: Option, - } - - impl widget::Operation> for FindById { - fn container( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - operate_on_children: &mut dyn FnMut( - &mut dyn widget::Operation>, - ), - ) { - if self.target.is_some() { - return; - } - - if Some(&self.id) == id { - self.target = Some(Target { bounds }); - return; - } - - operate_on_children(self); - } - - fn scrollable( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - _content_bounds: Rectangle, - _translation: Vector, - _state: &mut dyn widget::operation::Scrollable, - ) { - if self.target.is_some() { - return; - } - - if Some(&self.id) == id { - self.target = Some(Target { bounds }); - } - } - - fn text_input( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - _state: &mut dyn widget::operation::TextInput, - ) { - if self.target.is_some() { - return; - } - - if Some(&self.id) == id { - self.target = Some(Target { bounds }); - } - } - - fn text( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - _text: &str, - ) { - if self.target.is_some() { - return; - } - - if Some(&self.id) == id { - self.target = Some(Target { bounds }); - } - } - - fn custom( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - _state: &mut dyn std::any::Any, - ) { - if self.target.is_some() { - return; - } - - if Some(&self.id) == id { - self.target = Some(Target { bounds }); - } - } - - fn finish( - &self, - ) -> widget::operation::Outcome> - { - if let Some(target) = self.target { - widget::operation::Outcome::Some(vec![target]) - } else { - widget::operation::Outcome::None - } - } - } - - Box::new(FindById { - id: id.clone(), - target: None, - }) as Box> - } - Selector::Text(text) => { - struct FindByText { - text: text::Fragment<'static>, - target: Vec, - } - - impl widget::Operation> for FindByText { - fn container( - &mut self, - _id: Option<&widget::Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut( - &mut dyn widget::Operation>, - ), - ) { - operate_on_children(self); - } - - fn text_input( - &mut self, - _id: Option<&widget::Id>, - bounds: Rectangle, - state: &mut dyn widget::operation::TextInput, - ) { - if self.text == state.text() { - self.target.push(Target { bounds }); - } - } - - fn text( - &mut self, - _id: Option<&widget::Id>, - bounds: Rectangle, - text: &str, - ) { - if self.text == text { - self.target.push(Target { bounds }); - } - } - - fn finish( - &self, - ) -> widget::operation::Outcome> - { - widget::operation::Outcome::Some(self.target.clone()) - } - } - - Box::new(FindByText { - text: text.clone(), - target: Vec::new(), - }) - } - } - } -} - -impl From for Selector { - fn from(id: widget::Id) -> Self { - Self::Id(id) - } -} - -impl From<&'static str> for Selector { - fn from(text: &'static str) -> Self { - Self::Text(text.into()) - } -} - -/// Creates a [`Selector`] that finds the widget with the given [`widget::Id`]. -pub fn id(id: impl Into) -> Selector { - Selector::Id(id.into()) -} - -/// Creates a [`Selector`] that finds the widget containing the given text fragment. -pub fn text(fragment: impl text::IntoFragment<'static>) -> Selector { - Selector::Text(fragment.into_fragment()) -} - -/// A specific area, normally containing a widget. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Target { - /// The bounds of the area. - pub bounds: Rectangle, -} diff --git a/test/src/simulator.rs b/test/src/simulator.rs index 4e643e5b..a01bf28b 100644 --- a/test/src/simulator.rs +++ b/test/src/simulator.rs @@ -12,13 +12,14 @@ use crate::core::{Element, Event, Font, Point, Settings, Size, SmolStr}; use crate::renderer; use crate::runtime::UserInterface; use crate::runtime::user_interface; -use crate::selector; +use crate::selector::target::Bounded; use crate::{Error, Selector}; use std::borrow::Cow; use std::env; use std::fs; use std::path::{Path, PathBuf}; +use std::sync::Arc; /// A user interface that can be interacted with and inspected programmatically. #[allow(missing_debug_implementations)] @@ -100,14 +101,15 @@ where } /// Finds the [`Target`] of the given widget [`Selector`] in the [`Simulator`]. - pub fn find( - &mut self, - selector: impl Into, - ) -> Result { + pub fn find(&mut self, selector: S) -> Result + where + S: Selector + Send, + S::Output: Clone + Send, + { use widget::Operation; - let selector = selector.into(); - let mut operation = selector.operation(); + let description = selector.description(); + let mut operation = selector.find(); self.raw.operate( &self.renderer, @@ -115,10 +117,14 @@ where ); match operation.finish() { - widget::operation::Outcome::Some(matches) => { - matches.first().copied().ok_or(Error::NotFound(selector)) + widget::operation::Outcome::Some(output) => { + output.ok_or(Error::NotFound { + selector: description, + }) } - _ => Err(Error::NotFound(selector)), + _ => Err(Error::NotFound { + selector: description, + }), } } @@ -134,12 +140,20 @@ where /// This consists in: /// - Pointing the mouse cursor at the center of the [`Target`]. /// - Simulating a [`click`]. - pub fn click( - &mut self, - selector: impl Into, - ) -> Result { + pub fn click(&mut self, selector: S) -> Result + where + S: Selector + Send, + S::Output: Bounded + Clone + Send + Sync + 'static, + { let target = self.find(selector)?; - self.point_at(target.bounds.center()); + + let Some(visible_bounds) = target.visible_bounds() else { + return Err(Error::NotVisible { + target: Arc::new(target), + }); + }; + + self.point_at(visible_bounds.center()); let _ = self.simulate(click()); diff --git a/widget/src/button.rs b/widget/src/button.rs index ac4a27cb..d9021597 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -262,7 +262,8 @@ where renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { self.content.as_widget().operate( &mut tree.children[0], layout.children().next().unwrap(), diff --git a/widget/src/column.rs b/widget/src/column.rs index 6c126048..51cb8bda 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -234,7 +234,8 @@ where renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { self.children .iter() .zip(&mut tree.children) diff --git a/widget/src/container.rs b/widget/src/container.rs index af789fec..7b73831e 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -31,10 +31,10 @@ use crate::core::widget::tree::{self, Tree}; use crate::core::widget::{self, Operation}; use crate::core::{ self, Background, Clipboard, Color, Element, Event, Layout, Length, - Padding, Pixels, Point, Rectangle, Shadow, Shell, Size, Theme, Vector, - Widget, color, + Padding, Pixels, Rectangle, Shadow, Shell, Size, Theme, Vector, Widget, + color, }; -use crate::runtime::task::{self, Task}; +use crate::runtime::Task; /// A widget that aligns its contents inside of its boundaries. /// @@ -284,18 +284,15 @@ where renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container( - self.id.as_ref().map(|id| &id.0), - layout.bounds(), - &mut |operation| { - self.content.as_widget().operate( - tree, - layout.children().next().unwrap(), - renderer, - operation, - ); - }, - ); + operation.container(self.id.as_ref().map(|id| &id.0), layout.bounds()); + operation.traverse(&mut |operation| { + self.content.as_widget().operate( + tree, + layout.children().next().unwrap(), + renderer, + operation, + ); + }); } fn update( @@ -492,94 +489,8 @@ impl From<&'static str> for Id { /// Produces a [`Task`] that queries the visible screen bounds of the /// [`Container`] with the given [`Id`]. -pub fn visible_bounds(id: impl Into) -> Task> { - let id = id.into(); - - struct VisibleBounds { - target: widget::Id, - depth: usize, - scrollables: Vec<(Vector, Rectangle, usize)>, - bounds: Option, - } - - impl Operation> for VisibleBounds { - fn scrollable( - &mut self, - _id: Option<&widget::Id>, - bounds: Rectangle, - _content_bounds: Rectangle, - translation: Vector, - _state: &mut dyn widget::operation::Scrollable, - ) { - match self.scrollables.last() { - Some((last_translation, last_viewport, _depth)) => { - let viewport = last_viewport - .intersection(&(bounds - *last_translation)) - .unwrap_or(Rectangle::new(Point::ORIGIN, Size::ZERO)); - - self.scrollables.push(( - translation + *last_translation, - viewport, - self.depth, - )); - } - None => { - self.scrollables.push((translation, bounds, self.depth)); - } - } - } - - fn container( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - operate_on_children: &mut dyn FnMut( - &mut dyn Operation>, - ), - ) { - if self.bounds.is_some() { - return; - } - - if id == Some(&self.target) { - match self.scrollables.last() { - Some((translation, viewport, _)) => { - self.bounds = - viewport.intersection(&(bounds - *translation)); - } - None => { - self.bounds = Some(bounds); - } - } - - return; - } - - self.depth += 1; - - operate_on_children(self); - - self.depth -= 1; - - match self.scrollables.last() { - Some((_, _, depth)) if self.depth == *depth => { - let _ = self.scrollables.pop(); - } - _ => {} - } - } - - fn finish(&self) -> widget::operation::Outcome> { - widget::operation::Outcome::Some(self.bounds) - } - } - - task::widget(VisibleBounds { - target: id.into(), - depth: 0, - scrollables: Vec::new(), - bounds: None, - }) +pub fn visible_bounds(_id: impl Into) -> Task> { + todo!() } /// The appearance of a container. diff --git a/widget/src/grid.rs b/widget/src/grid.rs index 4a08dc55..15ffa738 100644 --- a/widget/src/grid.rs +++ b/widget/src/grid.rs @@ -257,7 +257,8 @@ where renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { self.children .iter() .zip(&mut tree.children) diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs index a774c239..40605e14 100644 --- a/widget/src/keyed/column.rs +++ b/widget/src/keyed/column.rs @@ -284,7 +284,8 @@ where renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { self.children .iter() .zip(&mut tree.children) diff --git a/widget/src/lib.rs b/widget/src/lib.rs index d08a92f9..fc64d938 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -8,6 +8,8 @@ pub use iced_renderer::graphics; pub use iced_runtime as runtime; pub use iced_runtime::core; +pub use core::widget::Id; + mod action; mod column; mod mouse_area; diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 158c265b..e1cbe6c2 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -468,7 +468,8 @@ where renderer: &Renderer, operation: &mut dyn widget::Operation, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { self.panes .iter() .copied() diff --git a/widget/src/row.rs b/widget/src/row.rs index 101e51d8..9baa3bbd 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -234,7 +234,8 @@ where renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { self.children .iter() .zip(&mut tree.children) diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index baff578e..0b682a78 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -549,18 +549,14 @@ where state, ); - operation.container( - self.id.as_ref().map(|id| &id.0), - bounds, - &mut |operation| { - self.content.as_widget().operate( - &mut tree.children[0], - layout.children().next().unwrap(), - renderer, - operation, - ); - }, - ); + operation.traverse(&mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + operation, + ); + }); } fn update( diff --git a/widget/src/stack.rs b/widget/src/stack.rs index ee81e4de..28290fc2 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -188,7 +188,8 @@ where renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { self.children .iter() .zip(&mut tree.children) From fbe60feb7ebb27486ac90c40df4094098f7c74a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 23 Aug 2025 02:04:30 +0200 Subject: [PATCH 33/83] Remove `Id` for `container`, `scrollable`, and `text_input` --- core/src/widget/id.rs | 6 +++ examples/scrollable/src/main.rs | 33 ++++-------- examples/todos/src/main.rs | 4 +- examples/visible_bounds/src/main.rs | 16 +++--- examples/websocket/src/main.rs | 9 ++-- selector/src/lib.rs | 2 +- widget/src/container.rs | 38 ++----------- widget/src/scrollable.rs | 57 +++++++------------- widget/src/text_input.rs | 82 +++++++---------------------- 9 files changed, 68 insertions(+), 179 deletions(-) diff --git a/core/src/widget/id.rs b/core/src/widget/id.rs index e03ded9d..2e7331c0 100644 --- a/core/src/widget/id.rs +++ b/core/src/widget/id.rs @@ -29,6 +29,12 @@ impl From<&'static str> for Id { } } +impl From for Id { + fn from(value: String) -> Self { + Self::new(value) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] enum Internal { Unique(usize), diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index 087f8e51..7ebe46c7 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -4,11 +4,6 @@ use iced::widget::{ }; use iced::{Border, Center, Color, Element, Fill, Task, Theme}; -use std::sync::LazyLock; - -static SCROLLABLE_ID: LazyLock = - LazyLock::new(scrollable::Id::unique); - pub fn main() -> iced::Result { iced::application( ScrollableDemo::default, @@ -65,19 +60,13 @@ impl ScrollableDemo { self.current_scroll_offset = scrollable::RelativeOffset::START; self.scrollable_direction = direction; - scrollable::snap_to( - SCROLLABLE_ID.clone(), - self.current_scroll_offset, - ) + scrollable::snap_to(SCROLLABLE, self.current_scroll_offset) } Message::AlignmentChanged(alignment) => { self.current_scroll_offset = scrollable::RelativeOffset::START; self.anchor = alignment; - scrollable::snap_to( - SCROLLABLE_ID.clone(), - self.current_scroll_offset, - ) + scrollable::snap_to(SCROLLABLE, self.current_scroll_offset) } Message::ScrollbarWidthChanged(width) => { self.scrollbar_width = width; @@ -97,18 +86,12 @@ impl ScrollableDemo { Message::ScrollToBeginning => { self.current_scroll_offset = scrollable::RelativeOffset::START; - scrollable::snap_to( - SCROLLABLE_ID.clone(), - self.current_scroll_offset, - ) + scrollable::snap_to(SCROLLABLE, self.current_scroll_offset) } Message::ScrollToEnd => { self.current_scroll_offset = scrollable::RelativeOffset::END; - scrollable::snap_to( - SCROLLABLE_ID.clone(), - self.current_scroll_offset, - ) + scrollable::snap_to(SCROLLABLE, self.current_scroll_offset) } Message::Scrolled(viewport) => { self.current_scroll_offset = viewport.relative_offset(); @@ -226,7 +209,7 @@ impl ScrollableDemo { )) .width(Fill) .height(Fill) - .id(SCROLLABLE_ID.clone()) + .id(SCROLLABLE) .on_scroll(Message::Scrolled), Direction::Horizontal => scrollable( row![ @@ -252,7 +235,7 @@ impl ScrollableDemo { )) .width(Fill) .height(Fill) - .id(SCROLLABLE_ID.clone()) + .id(SCROLLABLE) .on_scroll(Message::Scrolled), Direction::Multi => scrollable( //horizontal content @@ -299,7 +282,7 @@ impl ScrollableDemo { }) .width(Fill) .height(Fill) - .id(SCROLLABLE_ID.clone()) + .id(SCROLLABLE) .on_scroll(Message::Scrolled), }); @@ -348,3 +331,5 @@ fn progress_bar_custom_style(theme: &Theme) -> progress_bar::Style { border: Border::default(), } } + +const SCROLLABLE: &str = "scrollable"; diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 5dab262c..78ed4c5e 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -305,8 +305,8 @@ pub enum TaskMessage { } impl Task { - fn text_input_id(i: usize) -> text_input::Id { - text_input::Id::new(format!("task-{i}")) + fn text_input_id(i: usize) -> widget::Id { + widget::Id::new(format!("task-{i}")) } fn new(description: String) -> Self { diff --git a/examples/visible_bounds/src/main.rs b/examples/visible_bounds/src/main.rs index 8e5e4a07..0029de55 100644 --- a/examples/visible_bounds/src/main.rs +++ b/examples/visible_bounds/src/main.rs @@ -41,9 +41,9 @@ impl Example { Task::none() } Message::Scrolled | Message::WindowResized => Task::batch(vec![ - container::visible_bounds(OUTER_CONTAINER.clone()) + container::visible_bounds(OUTER_CONTAINER) .map(Message::OuterBoundsFetched), - container::visible_bounds(INNER_CONTAINER.clone()) + container::visible_bounds(INNER_CONTAINER) .map(Message::InnerBoundsFetched), ]), Message::OuterBoundsFetched(outer_bounds) => { @@ -113,7 +113,7 @@ impl Example { text("Scroll me!"), vertical_space().height(400), container(text("I am the outer container!")) - .id(OUTER_CONTAINER.clone()) + .id(OUTER_CONTAINER) .padding(40) .style(container::rounded_box), vertical_space().height(400), @@ -122,7 +122,7 @@ impl Example { text("Scroll me!"), vertical_space().height(400), container(text("I am the inner container!")) - .id(INNER_CONTAINER.clone()) + .id(INNER_CONTAINER) .padding(40) .style(container::rounded_box), vertical_space().height(400), @@ -157,9 +157,5 @@ impl Example { } } -use std::sync::LazyLock; - -static OUTER_CONTAINER: LazyLock = - LazyLock::new(|| container::Id::new("outer")); -static INNER_CONTAINER: LazyLock = - LazyLock::new(|| container::Id::new("inner")); +const OUTER_CONTAINER: &str = "outer"; +const INNER_CONTAINER: &str = "inner"; diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index 055455de..4376a1a1 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -6,8 +6,6 @@ use iced::widget::{ }; use iced::{Center, Element, Fill, Subscription, Task, color}; -use std::sync::LazyLock; - pub fn main() -> iced::Result { iced::application(WebSocket::new, WebSocket::update, WebSocket::view) .subscription(WebSocket::subscription) @@ -76,7 +74,7 @@ impl WebSocket { self.messages.push(message); scrollable::snap_to( - MESSAGE_LOG.clone(), + MESSAGE_LOG, scrollable::RelativeOffset::END, ) } @@ -105,7 +103,7 @@ impl WebSocket { column(self.messages.iter().map(text).map(Element::from)) .spacing(10), ) - .id(MESSAGE_LOG.clone()) + .id(MESSAGE_LOG) .height(Fill) .spacing(10) .into() @@ -142,5 +140,4 @@ enum State { Connected(echo::Connection), } -static MESSAGE_LOG: LazyLock = - LazyLock::new(scrollable::Id::unique); +const MESSAGE_LOG: &str = "message_log"; diff --git a/selector/src/lib.rs b/selector/src/lib.rs index 2163c502..e9a36076 100644 --- a/selector/src/lib.rs +++ b/selector/src/lib.rs @@ -78,7 +78,7 @@ impl Selector for Id { } fn description(&self) -> String { - format!("id == \"{:?}\"", self) + format!("id == {:?}", self) } } diff --git a/widget/src/container.rs b/widget/src/container.rs index 7b73831e..f3b1c8d1 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -67,7 +67,7 @@ pub struct Container< Theme: Catalog, Renderer: core::Renderer, { - id: Option, + id: Option, padding: Padding, width: Length, height: Length, @@ -108,7 +108,7 @@ where } /// Sets the [`Id`] of the [`Container`]. - pub fn id(mut self, id: impl Into) -> Self { + pub fn id(mut self, id: impl Into) -> Self { self.id = Some(id.into()); self } @@ -284,7 +284,7 @@ where renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container(self.id.as_ref().map(|id| &id.0), layout.bounds()); + operation.container(self.id.as_ref(), layout.bounds()); operation.traverse(&mut |operation| { self.content.as_widget().operate( tree, @@ -457,39 +457,9 @@ pub fn draw_background( } } -/// The identifier of a [`Container`]. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Id(widget::Id); - -impl Id { - /// Creates a custom [`Id`]. - pub fn new(id: impl Into>) -> Self { - Self(widget::Id::new(id)) - } - - /// Creates a unique [`Id`]. - /// - /// This function produces a different [`Id`] every time it is called. - pub fn unique() -> Self { - Self(widget::Id::unique()) - } -} - -impl From for widget::Id { - fn from(id: Id) -> Self { - id.0 - } -} - -impl From<&'static str> for Id { - fn from(value: &'static str) -> Self { - Id::new(value) - } -} - /// Produces a [`Task`] that queries the visible screen bounds of the /// [`Container`] with the given [`Id`]. -pub fn visible_bounds(_id: impl Into) -> Task> { +pub fn visible_bounds(_id: impl Into) -> Task> { todo!() } diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 0b682a78..e5c0a728 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -74,7 +74,7 @@ pub struct Scrollable< Theme: Catalog, Renderer: core::Renderer, { - id: Option, + id: Option, width: Length, height: Length, direction: Direction, @@ -150,7 +150,7 @@ where } /// Sets the [`Id`] of the [`Scrollable`]. - pub fn id(mut self, id: impl Into) -> Self { + pub fn id(mut self, id: impl Into) -> Self { self.id = Some(id.into()); self } @@ -542,7 +542,7 @@ where state.translation(self.direction, bounds, content_bounds); operation.scrollable( - self.id.as_ref().map(|id| &id.0), + self.id.as_ref(), bounds, content_bounds, translation, @@ -1262,59 +1262,38 @@ where } } -/// The identifier of a [`Scrollable`]. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Id(widget::Id); - -impl Id { - /// Creates a custom [`Id`]. - pub fn new(id: impl Into>) -> Self { - Self(widget::Id::new(id)) - } - - /// Creates a unique [`Id`]. - /// - /// This function produces a different [`Id`] every time it is called. - pub fn unique() -> Self { - Self(widget::Id::unique()) - } -} - -impl From for widget::Id { - fn from(id: Id) -> Self { - id.0 - } -} - -impl From<&'static str> for Id { - fn from(id: &'static str) -> Self { - Self::new(id) - } -} - /// Produces a [`Task`] that snaps the [`Scrollable`] with the given [`Id`] /// to the provided [`RelativeOffset`]. -pub fn snap_to(id: impl Into, offset: RelativeOffset) -> Task { +pub fn snap_to( + id: impl Into, + offset: RelativeOffset, +) -> Task { task::effect(Action::widget(operation::scrollable::snap_to( - id.into().0, + id.into(), offset, ))) } /// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`Id`] /// to the provided [`AbsoluteOffset`]. -pub fn scroll_to(id: impl Into, offset: AbsoluteOffset) -> Task { +pub fn scroll_to( + id: impl Into, + offset: AbsoluteOffset, +) -> Task { task::effect(Action::widget(operation::scrollable::scroll_to( - id.into().0, + id.into(), offset, ))) } /// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`Id`] /// by the provided [`AbsoluteOffset`]. -pub fn scroll_by(id: impl Into, offset: AbsoluteOffset) -> Task { +pub fn scroll_by( + id: impl Into, + offset: AbsoluteOffset, +) -> Task { task::effect(Action::widget(operation::scrollable::scroll_by( - id.into().0, + id.into(), offset, ))) } diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index c7ba113c..a40cd149 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -106,7 +106,7 @@ pub struct TextInput< Theme: Catalog, Renderer: text::Renderer, { - id: Option, + id: Option, placeholder: String, value: Value, is_secure: bool, @@ -157,7 +157,7 @@ where } /// Sets the [`Id`] of the [`TextInput`]. - pub fn id(mut self, id: impl Into) -> Self { + pub fn id(mut self, id: impl Into) -> Self { self.id = Some(id.into()); self } @@ -688,17 +688,8 @@ where ) { let state = tree.state.downcast_mut::>(); - operation.focusable( - self.id.as_ref().map(|id| &id.0), - layout.bounds(), - state, - ); - - operation.text_input( - self.id.as_ref().map(|id| &id.0), - layout.bounds(), - state, - ); + operation.text_input(self.id.as_ref(), layout.bounds(), state); + operation.focusable(self.id.as_ref(), layout.bounds(), state); } fn update( @@ -1454,82 +1445,47 @@ pub enum Side { Right, } -/// The identifier of a [`TextInput`]. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Id(widget::Id); - -impl Id { - /// Creates a custom [`Id`]. - pub fn new(id: impl Into>) -> Self { - Self(widget::Id::new(id)) - } - - /// Creates a unique [`Id`]. - /// - /// This function produces a different [`Id`] every time it is called. - pub fn unique() -> Self { - Self(widget::Id::unique()) - } -} - -impl From for widget::Id { - fn from(id: Id) -> Self { - id.0 - } -} - -impl From<&'static str> for Id { - fn from(id: &'static str) -> Self { - Self::new(id) - } -} - -impl From for Id { - fn from(id: String) -> Self { - Self::new(id) - } -} - /// Produces a [`Task`] that returns whether the [`TextInput`] with the given [`Id`] is focused or not. -pub fn is_focused(id: impl Into) -> Task { - task::widget(operation::focusable::is_focused(id.into().into())) +pub fn is_focused(id: impl Into) -> Task { + task::widget(operation::focusable::is_focused(id.into())) } /// Produces a [`Task`] that focuses the [`TextInput`] with the given [`Id`]. -pub fn focus(id: impl Into) -> Task { - task::effect(Action::widget(operation::focusable::focus(id.into().0))) +pub fn focus(id: impl Into) -> Task { + task::effect(Action::widget(operation::focusable::focus(id.into()))) } /// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// end. -pub fn move_cursor_to_end(id: impl Into) -> Task { +pub fn move_cursor_to_end(id: impl Into) -> Task { task::effect(Action::widget(operation::text_input::move_cursor_to_end( - id.into().0, + id.into(), ))) } /// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// front. -pub fn move_cursor_to_front(id: impl Into) -> Task { +pub fn move_cursor_to_front(id: impl Into) -> Task { task::effect(Action::widget(operation::text_input::move_cursor_to_front( - id.into().0, + id.into(), ))) } /// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// provided position. -pub fn move_cursor_to(id: impl Into, position: usize) -> Task { +pub fn move_cursor_to( + id: impl Into, + position: usize, +) -> Task { task::effect(Action::widget(operation::text_input::move_cursor_to( - id.into().0, + id.into(), position, ))) } /// Produces a [`Task`] that selects all the content of the [`TextInput`] with the given [`Id`]. -pub fn select_all(id: impl Into) -> Task { - task::effect(Action::widget(operation::text_input::select_all( - id.into().0, - ))) +pub fn select_all(id: impl Into) -> Task { + task::effect(Action::widget(operation::text_input::select_all(id.into()))) } /// The state of a [`TextInput`]. From 885d45f435e5874edd66b2ec6ba0484b99e73245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 23 Aug 2025 02:41:52 +0200 Subject: [PATCH 34/83] Fix broken intra-doc links --- program/src/preset.rs | 4 ++-- test/src/error.rs | 2 +- test/src/lib.rs | 4 ++-- test/src/simulator.rs | 6 +++--- widget/src/container.rs | 4 ++-- widget/src/scrollable.rs | 8 ++++---- widget/src/text_input.rs | 14 +++++++------- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/program/src/preset.rs b/program/src/preset.rs index 863278ab..ba983f67 100644 --- a/program/src/preset.rs +++ b/program/src/preset.rs @@ -3,7 +3,7 @@ use crate::runtime::Task; use std::borrow::Cow; use std::fmt; -/// A specific boot strategy for a [`Program`]. +/// A specific boot strategy for a [`Program`](crate::Program). pub struct Preset { name: Cow<'static, str>, boot: Box (State, Task)>, @@ -26,7 +26,7 @@ impl Preset { &self.name } - /// Boots the [`Preset`], returning the initial [`Program`] state and + /// Boots the [`Preset`], returning the initial [`Program`](crate::Program) state and /// a [`Task`] for concurrent booting. pub fn boot(&self) -> (State, Task) { (self.boot)() diff --git a/test/src/error.rs b/test/src/error.rs index 45269a81..1e1de331 100644 --- a/test/src/error.rs +++ b/test/src/error.rs @@ -4,7 +4,7 @@ use std::sync::Arc; /// A test error. #[derive(Debug, Clone, thiserror::Error)] pub enum Error { - /// No matching widget was found for the [`Selector`]. + /// No matching widget was found for the [`Selector`](crate::Selector). #[error("no matching widget was found for the selector: {selector}")] NotFound { selector: String }, #[error("the matching target is not visible: {target:?}")] diff --git a/test/src/lib.rs b/test/src/lib.rs index 4ba2fe9c..baccc5e3 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -34,8 +34,8 @@ //! let _ = ui.click("-"); //! ``` //! -//! [`Simulator::click`] takes a [`Selector`]. A [`Selector`] describes a way to query the widgets of an interface. In this case, -//! [`selector::text`] lets us select a widget by the text it contains. +//! [`Simulator::click`] takes a type implementing the [`Selector`] trait. A [`Selector`] describes a way to query the widgets of an interface. +//! In this case, we leverage the [`Selector`] implementation of `&str`, which selects a widget by the text it contains. //! //! We can now process any messages produced by these interactions and then assert that the final value of our counter is //! indeed `1`! diff --git a/test/src/simulator.rs b/test/src/simulator.rs index a01bf28b..30f772a6 100644 --- a/test/src/simulator.rs +++ b/test/src/simulator.rs @@ -100,7 +100,7 @@ where } } - /// Finds the [`Target`] of the given widget [`Selector`] in the [`Simulator`]. + /// Finds the target of the given widget [`Selector`] in the [`Simulator`]. pub fn find(&mut self, selector: S) -> Result where S: Selector + Send, @@ -135,10 +135,10 @@ where self.cursor = mouse::Cursor::Available(position.into()); } - /// Clicks the [`Target`] found by the given [`Selector`], if any. + /// Clicks the [`Bounded`] target found by the given [`Selector`], if any. /// /// This consists in: - /// - Pointing the mouse cursor at the center of the [`Target`]. + /// - Pointing the mouse cursor at the center of the [`Bounded`] target. /// - Simulating a [`click`]. pub fn click(&mut self, selector: S) -> Result where diff --git a/widget/src/container.rs b/widget/src/container.rs index f3b1c8d1..a9883668 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -107,7 +107,7 @@ where } } - /// Sets the [`Id`] of the [`Container`]. + /// Sets the [`widget::Id`] of the [`Container`]. pub fn id(mut self, id: impl Into) -> Self { self.id = Some(id.into()); self @@ -458,7 +458,7 @@ pub fn draw_background( } /// Produces a [`Task`] that queries the visible screen bounds of the -/// [`Container`] with the given [`Id`]. +/// [`Container`] with the given [`widget::Id`]. pub fn visible_bounds(_id: impl Into) -> Task> { todo!() } diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index e5c0a728..67242334 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -149,7 +149,7 @@ where self.validate() } - /// Sets the [`Id`] of the [`Scrollable`]. + /// Sets the [`widget::Id`] of the [`Scrollable`]. pub fn id(mut self, id: impl Into) -> Self { self.id = Some(id.into()); self @@ -1262,7 +1262,7 @@ where } } -/// Produces a [`Task`] that snaps the [`Scrollable`] with the given [`Id`] +/// Produces a [`Task`] that snaps the [`Scrollable`] with the given [`widget::Id`] /// to the provided [`RelativeOffset`]. pub fn snap_to( id: impl Into, @@ -1274,7 +1274,7 @@ pub fn snap_to( ))) } -/// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`Id`] +/// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`widget::Id`] /// to the provided [`AbsoluteOffset`]. pub fn scroll_to( id: impl Into, @@ -1286,7 +1286,7 @@ pub fn scroll_to( ))) } -/// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`Id`] +/// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`widget::Id`] /// by the provided [`AbsoluteOffset`]. pub fn scroll_by( id: impl Into, diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index a40cd149..9d39e02c 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -156,7 +156,7 @@ where } } - /// Sets the [`Id`] of the [`TextInput`]. + /// Sets the [`widget::Id`] of the [`TextInput`]. pub fn id(mut self, id: impl Into) -> Self { self.id = Some(id.into()); self @@ -1445,17 +1445,17 @@ pub enum Side { Right, } -/// Produces a [`Task`] that returns whether the [`TextInput`] with the given [`Id`] is focused or not. +/// Produces a [`Task`] that returns whether the [`TextInput`] with the given [`widget::Id`] is focused or not. pub fn is_focused(id: impl Into) -> Task { task::widget(operation::focusable::is_focused(id.into())) } -/// Produces a [`Task`] that focuses the [`TextInput`] with the given [`Id`]. +/// Produces a [`Task`] that focuses the [`TextInput`] with the given [`widget::Id`]. pub fn focus(id: impl Into) -> Task { task::effect(Action::widget(operation::focusable::focus(id.into()))) } -/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the +/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`widget::Id`] to the /// end. pub fn move_cursor_to_end(id: impl Into) -> Task { task::effect(Action::widget(operation::text_input::move_cursor_to_end( @@ -1463,7 +1463,7 @@ pub fn move_cursor_to_end(id: impl Into) -> Task { ))) } -/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the +/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`widget::Id`] to the /// front. pub fn move_cursor_to_front(id: impl Into) -> Task { task::effect(Action::widget(operation::text_input::move_cursor_to_front( @@ -1471,7 +1471,7 @@ pub fn move_cursor_to_front(id: impl Into) -> Task { ))) } -/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the +/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`widget::Id`] to the /// provided position. pub fn move_cursor_to( id: impl Into, @@ -1483,7 +1483,7 @@ pub fn move_cursor_to( ))) } -/// Produces a [`Task`] that selects all the content of the [`TextInput`] with the given [`Id`]. +/// Produces a [`Task`] that selects all the content of the [`TextInput`] with the given [`widget::Id`]. pub fn select_all(id: impl Into) -> Task { task::effect(Action::widget(operation::text_input::select_all(id.into()))) } From 34a42b5ad42c72ab7fd0bba6273e6387d9024e0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 23 Aug 2025 03:54:54 +0200 Subject: [PATCH 35/83] Move all operations to `widget::operation` module --- examples/editor/src/main.rs | 6 +-- examples/markdown/src/main.rs | 11 ++-- examples/modal/src/main.rs | 10 ++-- examples/multi_window/src/main.rs | 6 +-- examples/scrollable/src/main.rs | 12 ++--- examples/toast/src/main.rs | 6 +-- examples/todos/src/main.rs | 14 ++--- examples/websocket/src/main.rs | 9 ++-- src/lib.rs | 2 +- widget/src/helpers.rs | 12 ----- widget/src/lib.rs | 1 + widget/src/operation.rs | 88 +++++++++++++++++++++++++++++++ widget/src/scrollable.rs | 38 ------------- widget/src/text_input.rs | 45 ---------------- 14 files changed, 124 insertions(+), 136 deletions(-) create mode 100644 widget/src/operation.rs diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index ad2337d8..622d700b 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -1,8 +1,8 @@ use iced::highlighter; use iced::keyboard; use iced::widget::{ - self, button, center_x, column, container, horizontal_space, pick_list, - row, text, text_editor, toggler, tooltip, + button, center_x, column, container, horizontal_space, operation, + pick_list, row, text, text_editor, toggler, tooltip, }; use iced::{Center, Element, Fill, Font, Task, Theme}; @@ -59,7 +59,7 @@ impl Editor { )), Message::FileOpened, ), - widget::focus_next(), + operation::focus_next(), ]), ) } diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs index b7195055..42302b9e 100644 --- a/examples/markdown/src/main.rs +++ b/examples/markdown/src/main.rs @@ -5,8 +5,8 @@ use iced::clipboard; use iced::highlighter; use iced::time::{self, Instant, milliseconds}; use iced::widget::{ - self, button, center_x, container, horizontal_space, hover, image, - markdown, right, row, scrollable, sensor, text_editor, toggler, + button, center_x, container, horizontal_space, hover, image, markdown, + operation, right, row, scrollable, sensor, text_editor, toggler, }; use iced::window; use iced::{ @@ -78,7 +78,7 @@ impl Markdown { theme: Theme::TokyoNight, now: Instant::now(), }, - widget::focus_next(), + operation::focus_next(), ) } @@ -140,10 +140,7 @@ impl Markdown { pending: self.raw.text(), }; - scrollable::snap_to( - "preview", - scrollable::RelativeOffset::END, - ) + operation::snap_to_end("preview") } else { self.mode = Mode::Preview; diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index b4355004..54fcb1af 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -2,8 +2,8 @@ use iced::event::{self, Event}; use iced::keyboard; use iced::keyboard::key; use iced::widget::{ - self, button, center, column, container, horizontal_space, mouse_area, - opaque, pick_list, row, stack, text, text_input, + button, center, column, container, horizontal_space, mouse_area, opaque, + operation, pick_list, row, stack, text, text_input, }; use iced::{Bottom, Color, Element, Fill, Subscription, Task}; @@ -43,7 +43,7 @@ impl App { match message { Message::ShowModal => { self.show_modal = true; - widget::focus_next() + operation::focus_next() } Message::HideModal => { self.hide_modal(); @@ -75,9 +75,9 @@ impl App { .. }) => { if modifiers.shift() { - widget::focus_previous() + operation::focus_previous() } else { - widget::focus_next() + operation::focus_next() } } Event::Keyboard(keyboard::Event::KeyPressed { diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index d940b9da..f7c57d2a 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -1,6 +1,6 @@ use iced::widget::{ - button, center, center_x, column, container, horizontal_space, scrollable, - text, text_input, + button, center, center_x, column, container, horizontal_space, operation, + scrollable, text, text_input, }; use iced::window; use iced::{ @@ -85,7 +85,7 @@ impl Example { } Message::WindowOpened(id) => { let window = Window::new(self.windows.len() + 1); - let focus_input = text_input::focus(format!("input-{id}")); + let focus_input = operation::focus(format!("input-{id}")); self.windows.insert(id, window); diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index 7ebe46c7..e354bac8 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -1,6 +1,6 @@ use iced::widget::{ - button, column, container, horizontal_space, progress_bar, radio, row, - scrollable, slider, text, vertical_space, + button, column, container, horizontal_space, operation, progress_bar, + radio, row, scrollable, slider, text, vertical_space, }; use iced::{Border, Center, Color, Element, Fill, Task, Theme}; @@ -60,13 +60,13 @@ impl ScrollableDemo { self.current_scroll_offset = scrollable::RelativeOffset::START; self.scrollable_direction = direction; - scrollable::snap_to(SCROLLABLE, self.current_scroll_offset) + operation::snap_to(SCROLLABLE, self.current_scroll_offset) } Message::AlignmentChanged(alignment) => { self.current_scroll_offset = scrollable::RelativeOffset::START; self.anchor = alignment; - scrollable::snap_to(SCROLLABLE, self.current_scroll_offset) + operation::snap_to(SCROLLABLE, self.current_scroll_offset) } Message::ScrollbarWidthChanged(width) => { self.scrollbar_width = width; @@ -86,12 +86,12 @@ impl ScrollableDemo { Message::ScrollToBeginning => { self.current_scroll_offset = scrollable::RelativeOffset::START; - scrollable::snap_to(SCROLLABLE, self.current_scroll_offset) + operation::snap_to(SCROLLABLE, self.current_scroll_offset) } Message::ScrollToEnd => { self.current_scroll_offset = scrollable::RelativeOffset::END; - scrollable::snap_to(SCROLLABLE, self.current_scroll_offset) + operation::snap_to(SCROLLABLE, self.current_scroll_offset) } Message::Scrolled(viewport) => { self.current_scroll_offset = viewport.relative_offset(); diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index e825f4f3..4fdd4cf9 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -2,7 +2,7 @@ use iced::event::{self, Event}; use iced::keyboard; use iced::keyboard::key; use iced::widget::{ - self, button, center, column, pick_list, row, slider, text, text_input, + button, center, column, operation, pick_list, row, slider, text, text_input, }; use iced::{Center, Element, Fill, Subscription, Task}; @@ -83,11 +83,11 @@ impl App { key: keyboard::Key::Named(key::Named::Tab), modifiers, .. - })) if modifiers.shift() => widget::focus_previous(), + })) if modifiers.shift() => operation::focus_previous(), Message::Event(Event::Keyboard(keyboard::Event::KeyPressed { key: keyboard::Key::Named(key::Named::Tab), .. - })) => widget::focus_next(), + })) => operation::focus_next(), Message::Event(_) => Task::none(), } } diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 78ed4c5e..3e15e492 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -1,7 +1,7 @@ use iced::keyboard; use iced::widget::{ - self, Text, button, center, center_x, checkbox, column, keyed_column, row, - scrollable, text, text_input, + self, Text, button, center, center_x, checkbox, column, keyed_column, + operation, row, scrollable, text, text_input, }; use iced::window; use iced::{ @@ -91,7 +91,7 @@ impl Todos { _ => {} } - text_input::focus("new-task") + operation::focus("new-task") } Todos::Loaded(state) => { let mut saved = false; @@ -132,8 +132,8 @@ impl Todos { if should_focus { let id = Task::text_input_id(i); Command::batch(vec![ - text_input::focus(id.clone()), - text_input::select_all(id), + operation::focus(id.clone()), + operation::select_all(id), ]) } else { Command::none() @@ -150,9 +150,9 @@ impl Todos { } Message::TabPressed { shift } => { if shift { - widget::focus_previous() + operation::focus_previous() } else { - widget::focus_next() + operation::focus_next() } } Message::ToggleFullscreen(mode) => window::get_latest() diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index 4376a1a1..a868be70 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -2,7 +2,7 @@ mod echo; use iced::futures::stream; use iced::widget::{ - self, button, center, column, row, scrollable, text, text_input, + button, center, column, operation, row, scrollable, text, text_input, }; use iced::{Center, Element, Fill, Subscription, Task, color}; @@ -34,7 +34,7 @@ impl WebSocket { new_message: String::new(), state: State::Disconnected, }, - widget::focus_next(), + operation::focus_next(), ) } @@ -73,10 +73,7 @@ impl WebSocket { echo::Event::MessageReceived(message) => { self.messages.push(message); - scrollable::snap_to( - MESSAGE_LOG, - scrollable::RelativeOffset::END, - ) + operation::snap_to_end(MESSAGE_LOG) } }, Message::Server => Task::none(), diff --git a/src/lib.rs b/src/lib.rs index 180db986..aa19c6ff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -326,7 +326,7 @@ //! //! Tasks can also be used to interact with the iced runtime. Some modules //! expose functions that create tasks for different purposes—like [changing -//! window settings](window#functions), [focusing a widget](widget::focus_next), or +//! window settings](window#functions), [focusing a widget](widget::operation::focus_next), or //! [querying its visible bounds](widget::container::visible_bounds). //! //! Like futures and streams, tasks expose [a monadic interface](Task::then)—but they can also be diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 60627c50..fbebde3b 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -15,8 +15,6 @@ use crate::pick_list::{self, PickList}; use crate::progress_bar::{self, ProgressBar}; use crate::radio::{self, Radio}; use crate::rule::{self, Rule}; -use crate::runtime::Action; -use crate::runtime::task::{self, Task}; use crate::scrollable::{self, Scrollable}; use crate::slider::{self, Slider}; use crate::text::{self, Text}; @@ -2036,16 +2034,6 @@ where crate::Shader::new(program) } -/// Focuses the previous focusable widget. -pub fn focus_previous() -> Task { - task::effect(Action::widget(operation::focusable::focus_previous())) -} - -/// Focuses the next focusable widget. -pub fn focus_next() -> Task { - task::effect(Action::widget(operation::focusable::focus_next())) -} - /// Creates a new [`MouseArea`]. pub fn mouse_area<'a, Message, Theme, Renderer>( widget: impl Into>, diff --git a/widget/src/lib.rs b/widget/src/lib.rs index fc64d938..ec480889 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -25,6 +25,7 @@ pub mod container; pub mod float; pub mod grid; pub mod keyed; +pub mod operation; pub mod overlay; pub mod pane_grid; pub mod pick_list; diff --git a/widget/src/operation.rs b/widget/src/operation.rs new file mode 100644 index 00000000..db24e1f2 --- /dev/null +++ b/widget/src/operation.rs @@ -0,0 +1,88 @@ +//! Change internal widget state. +use crate::Id; +use crate::core::widget::operation; +use crate::runtime::task; +use crate::runtime::{Action, Task}; + +pub use crate::core::widget::operation::scrollable::{ + AbsoluteOffset, RelativeOffset, +}; + +/// Snaps the scrollable with the given [`Id`] to the provided [`RelativeOffset`]. +pub fn snap_to(id: impl Into, offset: RelativeOffset) -> Task { + task::effect(Action::widget(operation::scrollable::snap_to( + id.into(), + offset, + ))) +} + +/// Snaps the scrollable with the given [`Id`] to the [`RelativeOffset::END`]. +pub fn snap_to_end(id: impl Into) -> Task { + task::effect(Action::widget(operation::scrollable::snap_to( + id.into(), + RelativeOffset::END, + ))) +} + +/// Scrolls the scrollable with the given [`Id`] to the provided [`AbsoluteOffset`]. +pub fn scroll_to(id: impl Into, offset: AbsoluteOffset) -> Task { + task::effect(Action::widget(operation::scrollable::scroll_to( + id.into(), + offset, + ))) +} + +/// Scrolls the scrollable with the given [`Id`] by the provided [`AbsoluteOffset`]. +pub fn scroll_by(id: impl Into, offset: AbsoluteOffset) -> Task { + task::effect(Action::widget(operation::scrollable::scroll_by( + id.into(), + offset, + ))) +} + +/// Focuses the previous focusable widget. +pub fn focus_previous() -> Task { + task::effect(Action::widget(operation::focusable::focus_previous())) +} + +/// Focuses the next focusable widget. +pub fn focus_next() -> Task { + task::effect(Action::widget(operation::focusable::focus_next())) +} + +/// Returns whether the widget with the given [`Id`] is focused or not. +pub fn is_focused(id: impl Into) -> Task { + task::widget(operation::focusable::is_focused(id.into())) +} + +/// Focuses the widget with the given [`Id`]. +pub fn focus(id: impl Into) -> Task { + task::effect(Action::widget(operation::focusable::focus(id.into()))) +} + +/// Moves the cursor of the widget with the given [`Id`] to the end. +pub fn move_cursor_to_end(id: impl Into) -> Task { + task::effect(Action::widget(operation::text_input::move_cursor_to_end( + id.into(), + ))) +} + +/// Moves the cursor of the widget with the given [`Id`] to the front. +pub fn move_cursor_to_front(id: impl Into) -> Task { + task::effect(Action::widget(operation::text_input::move_cursor_to_front( + id.into(), + ))) +} + +/// Moves the cursor of the widget with the given [`Id`] to the provided position. +pub fn move_cursor_to(id: impl Into, position: usize) -> Task { + task::effect(Action::widget(operation::text_input::move_cursor_to( + id.into(), + position, + ))) +} + +/// Selects all the content of the widget with the given [`Id`]. +pub fn select_all(id: impl Into) -> Task { + task::effect(Action::widget(operation::text_input::select_all(id.into()))) +} diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 67242334..07b56f5d 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -37,8 +37,6 @@ use crate::core::{ Length, Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; -use crate::runtime::Action; -use crate::runtime::task::{self, Task}; pub use operation::scrollable::{AbsoluteOffset, RelativeOffset}; @@ -1262,42 +1260,6 @@ where } } -/// Produces a [`Task`] that snaps the [`Scrollable`] with the given [`widget::Id`] -/// to the provided [`RelativeOffset`]. -pub fn snap_to( - id: impl Into, - offset: RelativeOffset, -) -> Task { - task::effect(Action::widget(operation::scrollable::snap_to( - id.into(), - offset, - ))) -} - -/// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`widget::Id`] -/// to the provided [`AbsoluteOffset`]. -pub fn scroll_to( - id: impl Into, - offset: AbsoluteOffset, -) -> Task { - task::effect(Action::widget(operation::scrollable::scroll_to( - id.into(), - offset, - ))) -} - -/// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`widget::Id`] -/// by the provided [`AbsoluteOffset`]. -pub fn scroll_by( - id: impl Into, - offset: AbsoluteOffset, -) -> Task { - task::effect(Action::widget(operation::scrollable::scroll_by( - id.into(), - offset, - ))) -} - fn notify_scroll( state: &mut State, on_scroll: &Option Message + '_>>, diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 9d39e02c..ea4de989 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -61,8 +61,6 @@ use crate::core::{ Length, Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; -use crate::runtime::Action; -use crate::runtime::task::{self, Task}; /// A field that can be filled with text. /// @@ -1445,49 +1443,6 @@ pub enum Side { Right, } -/// Produces a [`Task`] that returns whether the [`TextInput`] with the given [`widget::Id`] is focused or not. -pub fn is_focused(id: impl Into) -> Task { - task::widget(operation::focusable::is_focused(id.into())) -} - -/// Produces a [`Task`] that focuses the [`TextInput`] with the given [`widget::Id`]. -pub fn focus(id: impl Into) -> Task { - task::effect(Action::widget(operation::focusable::focus(id.into()))) -} - -/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`widget::Id`] to the -/// end. -pub fn move_cursor_to_end(id: impl Into) -> Task { - task::effect(Action::widget(operation::text_input::move_cursor_to_end( - id.into(), - ))) -} - -/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`widget::Id`] to the -/// front. -pub fn move_cursor_to_front(id: impl Into) -> Task { - task::effect(Action::widget(operation::text_input::move_cursor_to_front( - id.into(), - ))) -} - -/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`widget::Id`] to the -/// provided position. -pub fn move_cursor_to( - id: impl Into, - position: usize, -) -> Task { - task::effect(Action::widget(operation::text_input::move_cursor_to( - id.into(), - position, - ))) -} - -/// Produces a [`Task`] that selects all the content of the [`TextInput`] with the given [`widget::Id`]. -pub fn select_all(id: impl Into) -> Task { - task::effect(Action::widget(operation::text_input::select_all(id.into()))) -} - /// The state of a [`TextInput`]. #[derive(Debug, Default, Clone)] pub struct State { From 81d1eda7fecd1b2e25c6352f7acdf70054057a6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 23 Aug 2025 05:15:57 +0200 Subject: [PATCH 36/83] Introduce `selector` flag and decouple `iced_widget` from `iced_runtime` --- Cargo.lock | 2 +- Cargo.toml | 2 ++ core/src/overlay.rs | 2 ++ {runtime => core}/src/overlay/nested.rs | 14 ++++---- devtools/src/lib.rs | 4 +-- examples/visible_bounds/Cargo.toml | 2 +- examples/visible_bounds/src/main.rs | 25 +++++++------- runtime/Cargo.toml | 7 +++- runtime/src/lib.rs | 9 +++-- runtime/src/overlay.rs | 4 --- runtime/src/user_interface.rs | 2 +- runtime/src/widget.rs | 5 +++ .../src => runtime/src/widget}/operation.rs | 6 ++-- runtime/src/widget/selector.rs | 18 ++++++++++ selector/src/lib.rs | 34 +++++++++++++++++++ src/lib.rs | 6 ++-- widget/Cargo.toml | 1 - widget/src/container.rs | 7 ---- widget/src/lazy.rs | 9 +++-- widget/src/lazy/component.rs | 11 +++--- widget/src/lazy/responsive.rs | 11 +++--- widget/src/lib.rs | 4 +-- 22 files changed, 118 insertions(+), 67 deletions(-) rename {runtime => core}/src/overlay/nested.rs (97%) delete mode 100644 runtime/src/overlay.rs create mode 100644 runtime/src/widget.rs rename {widget/src => runtime/src/widget}/operation.rs (97%) create mode 100644 runtime/src/widget/selector.rs diff --git a/Cargo.lock b/Cargo.lock index cda9648a..6fd97456 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2622,6 +2622,7 @@ dependencies = [ "iced_core", "iced_debug", "iced_futures", + "iced_selector", "raw-window-handle 0.6.2", "sipper", "thiserror 2.0.14", @@ -2690,7 +2691,6 @@ version = "0.14.0-dev" dependencies = [ "iced_highlighter", "iced_renderer", - "iced_runtime", "log", "num-traits", "ouroboros", diff --git a/Cargo.toml b/Cargo.toml index 432a9cd4..c8087550 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,8 @@ crisp = ["iced_core/crisp", "iced_widget/crisp"] webgl = ["iced_renderer/webgl"] # Enables syntax highligthing highlighter = ["iced_highlighter", "iced_widget/highlighter"] +# Enables the `widget::selector` module +selector = ["iced_runtime/selector"] # Enables the advanced module advanced = ["iced_core/advanced", "iced_widget/advanced"] # Embeds Fira Sans into the final application; useful for testing and Wasm builds diff --git a/core/src/overlay.rs b/core/src/overlay.rs index 5f7ebce9..76fe321a 100644 --- a/core/src/overlay.rs +++ b/core/src/overlay.rs @@ -1,9 +1,11 @@ //! Display interactive elements on top of other widgets. mod element; mod group; +mod nested; pub use element::Element; pub use group::Group; +pub use nested::Nested; use crate::layout; use crate::mouse; diff --git a/runtime/src/overlay/nested.rs b/core/src/overlay/nested.rs similarity index 97% rename from runtime/src/overlay/nested.rs rename to core/src/overlay/nested.rs index 83c58804..25d94c91 100644 --- a/runtime/src/overlay/nested.rs +++ b/core/src/overlay/nested.rs @@ -1,10 +1,10 @@ -use crate::core::event; -use crate::core::layout; -use crate::core::mouse; -use crate::core::overlay; -use crate::core::renderer; -use crate::core::widget; -use crate::core::{Clipboard, Event, Layout, Shell, Size}; +use crate::event; +use crate::layout; +use crate::mouse; +use crate::overlay; +use crate::renderer; +use crate::widget; +use crate::{Clipboard, Event, Layout, Shell, Size}; /// An overlay container that displays nested overlays #[allow(missing_debug_implementations)] diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index 209eb100..18208a7a 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -1,11 +1,11 @@ #![allow(missing_docs)] use iced_debug as debug; use iced_program as program; +use iced_program::runtime; +use iced_program::runtime::futures; #[cfg(feature = "tester")] use iced_test as test; use iced_widget::core; -use iced_widget::runtime; -use iced_widget::runtime::futures; mod comet; mod executor; diff --git a/examples/visible_bounds/Cargo.toml b/examples/visible_bounds/Cargo.toml index a11af963..7cdccb60 100644 --- a/examples/visible_bounds/Cargo.toml +++ b/examples/visible_bounds/Cargo.toml @@ -7,4 +7,4 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["debug"] +iced.features = ["debug", "selector"] diff --git a/examples/visible_bounds/src/main.rs b/examples/visible_bounds/src/main.rs index 0029de55..3930b462 100644 --- a/examples/visible_bounds/src/main.rs +++ b/examples/visible_bounds/src/main.rs @@ -1,7 +1,8 @@ use iced::event::{self, Event}; use iced::mouse; use iced::widget::{ - column, container, horizontal_space, row, scrollable, text, vertical_space, + column, container, horizontal_space, row, scrollable, selector, text, + vertical_space, }; use iced::window; use iced::{ @@ -23,13 +24,13 @@ struct Example { inner_bounds: Option, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] enum Message { MouseMoved(Point), WindowResized, Scrolled, - OuterBoundsFetched(Option), - InnerBoundsFetched(Option), + OuterFound(Option), + InnerFound(Option), } impl Example { @@ -41,18 +42,18 @@ impl Example { Task::none() } Message::Scrolled | Message::WindowResized => Task::batch(vec![ - container::visible_bounds(OUTER_CONTAINER) - .map(Message::OuterBoundsFetched), - container::visible_bounds(INNER_CONTAINER) - .map(Message::InnerBoundsFetched), + selector::find_by_id(OUTER_CONTAINER).map(Message::OuterFound), + selector::find_by_id(INNER_CONTAINER).map(Message::InnerFound), ]), - Message::OuterBoundsFetched(outer_bounds) => { - self.outer_bounds = outer_bounds; + Message::OuterFound(outer) => { + self.outer_bounds = + outer.as_ref().and_then(selector::Bounded::visible_bounds); Task::none() } - Message::InnerBoundsFetched(inner_bounds) => { - self.inner_bounds = inner_bounds; + Message::InnerFound(inner) => { + self.inner_bounds = + inner.as_ref().and_then(selector::Bounded::visible_bounds); Task::none() } diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 2dc60474..baddbfa4 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -10,6 +10,9 @@ homepage.workspace = true categories.workspace = true keywords.workspace = true +[features] +selector = ["dep:iced_selector"] + [lints] workspace = true @@ -17,7 +20,6 @@ workspace = true bytes.workspace = true iced_core.workspace = true iced_debug.workspace = true - iced_futures.workspace = true raw-window-handle.workspace = true @@ -25,3 +27,6 @@ thiserror.workspace = true sipper.workspace = true sipper.optional = true + +iced_selector.workspace = true +iced_selector.optional = true diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 457f723c..af43edce 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -12,10 +12,10 @@ pub mod clipboard; pub mod font; pub mod keyboard; -pub mod overlay; pub mod system; pub mod task; pub mod user_interface; +pub mod widget; pub mod window; pub use iced_core as core; @@ -25,7 +25,6 @@ pub use iced_futures as futures; pub use task::Task; pub use user_interface::UserInterface; -use crate::core::widget; use crate::futures::futures::channel::oneshot; use std::borrow::Cow; @@ -45,7 +44,7 @@ pub enum Action { }, /// Run a widget operation. - Widget(Box), + Widget(Box), /// Run a clipboard action. Clipboard(clipboard::Action), @@ -67,8 +66,8 @@ pub enum Action { } impl Action { - /// Creates a new [`Action::Widget`] with the given [`widget::Operation`]. - pub fn widget(operation: impl widget::Operation + 'static) -> Self { + /// Creates a new [`Action::Widget`] with the given [`widget::Operation`](core::widget::Operation). + pub fn widget(operation: impl core::widget::Operation + 'static) -> Self { Self::Widget(Box::new(operation)) } diff --git a/runtime/src/overlay.rs b/runtime/src/overlay.rs deleted file mode 100644 index 03390980..00000000 --- a/runtime/src/overlay.rs +++ /dev/null @@ -1,4 +0,0 @@ -//! Overlays for user interfaces. -mod nested; - -pub use nested::Nested; diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index 95e7574f..631c3883 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -2,13 +2,13 @@ use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; +use crate::core::overlay; use crate::core::renderer; use crate::core::widget; use crate::core::window; use crate::core::{ Clipboard, Element, InputMethod, Layout, Rectangle, Shell, Size, Vector, }; -use crate::overlay; /// A set of interactive graphical elements with a specific [`Layout`]. /// diff --git a/runtime/src/widget.rs b/runtime/src/widget.rs new file mode 100644 index 00000000..e5ba2b0e --- /dev/null +++ b/runtime/src/widget.rs @@ -0,0 +1,5 @@ +//! Operate on widgets and query them at runtime. +pub mod operation; + +#[cfg(feature = "selector")] +pub mod selector; diff --git a/widget/src/operation.rs b/runtime/src/widget/operation.rs similarity index 97% rename from widget/src/operation.rs rename to runtime/src/widget/operation.rs index db24e1f2..ab03bbe0 100644 --- a/widget/src/operation.rs +++ b/runtime/src/widget/operation.rs @@ -1,8 +1,8 @@ //! Change internal widget state. -use crate::Id; +use crate::core::widget::Id; use crate::core::widget::operation; -use crate::runtime::task; -use crate::runtime::{Action, Task}; +use crate::task; +use crate::{Action, Task}; pub use crate::core::widget::operation::scrollable::{ AbsoluteOffset, RelativeOffset, diff --git a/runtime/src/widget/selector.rs b/runtime/src/widget/selector.rs new file mode 100644 index 00000000..d6fc6f9a --- /dev/null +++ b/runtime/src/widget/selector.rs @@ -0,0 +1,18 @@ +//! Find and query widgets in your applications. +pub use iced_selector::Selector; + +pub use iced_selector::target::{Bounded, Match, Target, Text}; + +use crate::Task; +use crate::core::widget; +use crate::task; + +/// Finds a widget by the given [`widget::Id`]. +pub fn find_by_id(id: impl Into) -> Task> { + task::widget(id.into().find()) +} + +/// Finds a widget that contains the given text. +pub fn find_by_text(text: impl Into) -> Task> { + task::widget(Selector::find(text.into())) +} diff --git a/selector/src/lib.rs b/selector/src/lib.rs index e9a36076..c32d065f 100644 --- a/selector/src/lib.rs +++ b/selector/src/lib.rs @@ -66,6 +66,40 @@ impl Selector for &str { } } +impl Selector for String { + type Output = target::Text; + + fn select(&mut self, target: Target<'_>) -> Option { + match target { + Target::TextInput { + id, + bounds, + visible_bounds, + state, + } if state.text() == *self => Some(target::Text::Input { + id: id.cloned(), + bounds, + visible_bounds, + }), + Target::Text { + id, + bounds, + visible_bounds, + content, + } if content == *self => Some(target::Text::Raw { + id: id.cloned(), + bounds, + visible_bounds, + }), + _ => None, + } + } + + fn description(&self) -> String { + format!("text == \"{}\"", self.escape_default()) + } +} + impl Selector for Id { type Output = target::Match; diff --git a/src/lib.rs b/src/lib.rs index aa19c6ff..79867909 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -327,7 +327,7 @@ //! Tasks can also be used to interact with the iced runtime. Some modules //! expose functions that create tasks for different purposes—like [changing //! window settings](window#functions), [focusing a widget](widget::operation::focus_next), or -//! [querying its visible bounds](widget::container::visible_bounds). +//! [querying its visible bounds](widget::selector::find_by_id). //! //! Like futures and streams, tasks expose [a monadic interface](Task::then)—but they can also be //! [mapped](Task::map), [chained](Task::chain), [batched](Task::batch), [canceled](Task::abortable), @@ -620,15 +620,13 @@ pub mod touch { #[allow(hidden_glob_reexports)] pub mod widget { //! Use the built-in widgets or create your own. + pub use iced_runtime::widget::*; pub use iced_widget::*; // We hide the re-exported modules by `iced_widget` mod core {} mod graphics {} - mod native {} mod renderer {} - mod style {} - mod runtime {} } pub use application::Application; diff --git a/widget/Cargo.toml b/widget/Cargo.toml index 024314ff..dc64b61d 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -31,7 +31,6 @@ crisp = [] [dependencies] iced_renderer.workspace = true -iced_runtime.workspace = true num-traits.workspace = true log.workspace = true diff --git a/widget/src/container.rs b/widget/src/container.rs index a9883668..16a827da 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -34,7 +34,6 @@ use crate::core::{ Padding, Pixels, Rectangle, Shadow, Shell, Size, Theme, Vector, Widget, color, }; -use crate::runtime::Task; /// A widget that aligns its contents inside of its boundaries. /// @@ -457,12 +456,6 @@ pub fn draw_background( } } -/// Produces a [`Task`] that queries the visible screen bounds of the -/// [`Container`] with the given [`widget::Id`]. -pub fn visible_bounds(_id: impl Into) -> Task> { - todo!() -} - /// The appearance of a container. #[derive(Debug, Clone, Copy, PartialEq, Default)] pub struct Style { diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index b538b460..88a648c6 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -20,7 +20,6 @@ use crate::core::widget::{self, Widget}; use crate::core::{ self, Clipboard, Event, Length, Rectangle, Shell, Size, Vector, }; -use crate::runtime::overlay::Nested; use ouroboros::self_referencing; use rustc_hash::FxHasher; @@ -286,7 +285,7 @@ where element .as_widget_mut() .overlay(tree, *layout, renderer, viewport, translation) - .map(|overlay| RefCell::new(Nested::new(overlay))) + .map(|overlay| RefCell::new(overlay::Nested::new(overlay))) }, } .build(); @@ -317,7 +316,7 @@ struct Inner<'a, Message: 'a, Theme: 'a, Renderer: 'a> { #[borrows(mut element, mut tree, layout)] #[not_covariant] - overlay: Option>>, + overlay: Option>>, } struct Overlay<'a, Message, Theme, Renderer>( @@ -334,7 +333,7 @@ impl Drop for Overlay<'_, Message, Theme, Renderer> { impl Overlay<'_, Message, Theme, Renderer> { fn with_overlay_maybe( &self, - f: impl FnOnce(&mut Nested<'_, Message, Theme, Renderer>) -> T, + f: impl FnOnce(&mut overlay::Nested<'_, Message, Theme, Renderer>) -> T, ) -> Option { self.0.as_ref().unwrap().with_overlay(|overlay| { overlay.as_ref().map(|nested| (f)(&mut nested.borrow_mut())) @@ -343,7 +342,7 @@ impl Overlay<'_, Message, Theme, Renderer> { fn with_overlay_mut_maybe( &mut self, - f: impl FnOnce(&mut Nested<'_, Message, Theme, Renderer>) -> T, + f: impl FnOnce(&mut overlay::Nested<'_, Message, Theme, Renderer>) -> T, ) -> Option { self.0.as_mut().unwrap().with_overlay_mut(|overlay| { overlay.as_mut().map(|nested| (f)(nested.get_mut())) diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index 8bd04d64..eedc1afa 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -9,7 +9,6 @@ use crate::core::widget::tree::{self, Tree}; use crate::core::{ self, Clipboard, Element, Length, Rectangle, Shell, Size, Vector, Widget, }; -use crate::runtime::overlay::Nested; use ouroboros::self_referencing; use std::cell::RefCell; @@ -472,7 +471,9 @@ where viewport, translation, ) - .map(|overlay| RefCell::new(Nested::new(overlay))) + .map(|overlay| { + RefCell::new(overlay::Nested::new(overlay)) + }) }, ) }, @@ -519,7 +520,7 @@ struct Inner<'a, 'b, Message, Theme, Renderer, Event, S> { #[borrows(mut instance, mut tree)] #[not_covariant] - overlay: Option>>, + overlay: Option>>, } struct OverlayInstance<'a, 'b, Message, Theme, Renderer, Event, S> { @@ -531,7 +532,7 @@ impl { fn with_overlay_maybe( &self, - f: impl FnOnce(&mut Nested<'_, Event, Theme, Renderer>) -> T, + f: impl FnOnce(&mut overlay::Nested<'_, Event, Theme, Renderer>) -> T, ) -> Option { self.overlay .as_ref() @@ -546,7 +547,7 @@ impl fn with_overlay_mut_maybe( &mut self, - f: impl FnOnce(&mut Nested<'_, Event, Theme, Renderer>) -> T, + f: impl FnOnce(&mut overlay::Nested<'_, Event, Theme, Renderer>) -> T, ) -> Option { self.overlay .as_mut() diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index 4e90a178..6c227a7a 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -9,7 +9,6 @@ use crate::core::{ Vector, Widget, }; use crate::horizontal_space; -use crate::runtime::overlay::Nested; use ouroboros::self_referencing; use std::cell::{RefCell, RefMut}; @@ -327,7 +326,9 @@ where viewport, translation, ) - .map(|overlay| RefCell::new(Nested::new(overlay))), + .map(|overlay| { + RefCell::new(overlay::Nested::new(overlay)) + }), is_layout_invalid, ) }, @@ -364,7 +365,7 @@ struct Overlay<'a, 'b, Message, Theme, Renderer> { #[borrows(mut content, mut tree)] #[not_covariant] overlay: ( - Option>>, + Option>>, &'this mut bool, ), } @@ -372,7 +373,7 @@ struct Overlay<'a, 'b, Message, Theme, Renderer> { impl Overlay<'_, '_, Message, Theme, Renderer> { fn with_overlay_maybe( &self, - f: impl FnOnce(&mut Nested<'_, Message, Theme, Renderer>) -> T, + f: impl FnOnce(&mut overlay::Nested<'_, Message, Theme, Renderer>) -> T, ) -> Option { self.with_overlay(|(overlay, _layout)| { overlay.as_ref().map(|nested| (f)(&mut nested.borrow_mut())) @@ -381,7 +382,7 @@ impl Overlay<'_, '_, Message, Theme, Renderer> { fn with_overlay_mut_maybe( &mut self, - f: impl FnOnce(&mut Nested<'_, Message, Theme, Renderer>) -> T, + f: impl FnOnce(&mut overlay::Nested<'_, Message, Theme, Renderer>) -> T, ) -> Option { self.with_overlay_mut(|(overlay, _layout)| { overlay.as_mut().map(|nested| (f)(nested.get_mut())) diff --git a/widget/src/lib.rs b/widget/src/lib.rs index ec480889..4f4db734 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -4,9 +4,8 @@ )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] pub use iced_renderer as renderer; +pub use iced_renderer::core; pub use iced_renderer::graphics; -pub use iced_runtime as runtime; -pub use iced_runtime::core; pub use core::widget::Id; @@ -25,7 +24,6 @@ pub mod container; pub mod float; pub mod grid; pub mod keyed; -pub mod operation; pub mod overlay; pub mod pane_grid; pub mod pick_list; From 1923d1db1eb1726b24ba1b240fea57843288f8cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 23 Aug 2025 06:37:02 +0200 Subject: [PATCH 37/83] Try to find text targets in `tester::recorder` --- devtools/src/tester.rs | 1 + devtools/src/tester/recorder.rs | 259 +++++++++++++++++++++++++------- selector/src/lib.rs | 16 ++ selector/src/target.rs | 15 +- test/src/instruction.rs | 13 ++ 5 files changed, 245 insertions(+), 59 deletions(-) diff --git a/devtools/src/tester.rs b/devtools/src/tester.rs index 96715a4d..fda621c6 100644 --- a/devtools/src/tester.rs +++ b/devtools/src/tester.rs @@ -546,6 +546,7 @@ impl Tester

{ column(self.instructions.iter().enumerate().map( |(i, instruction)| { monospace(instruction.to_string()) + .wrapping(text::Wrapping::None) // TODO: Ellipsize? .size(10) .style(move |theme: &Theme| text::Style { color: match &self.state { diff --git a/devtools/src/tester/recorder.rs b/devtools/src/tester/recorder.rs index e1aa5655..a15e460c 100644 --- a/devtools/src/tester/recorder.rs +++ b/devtools/src/tester/recorder.rs @@ -3,26 +3,29 @@ use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::widget; +use crate::core::widget::operation; use crate::core::widget::tree; use crate::core::{ self, Clipboard, Element, Event, Layout, Length, Point, Rectangle, Shell, - Size, Vector, Widget, + Size, Theme, Vector, Widget, }; -use crate::test::instruction::Interaction; +use crate::test::Selector; +use crate::test::instruction::{Interaction, Mouse, Target}; +use crate::test::selector::target; -pub fn recorder<'a, Message, Theme, Renderer>( +pub fn recorder<'a, Message, Renderer>( content: impl Into>, -) -> Recorder<'a, Message, Theme, Renderer> { +) -> Recorder<'a, Message, Renderer> { Recorder::new(content) } #[allow(missing_debug_implementations)] -pub struct Recorder<'a, Message, Theme, Renderer> { +pub struct Recorder<'a, Message, Renderer> { content: Element<'a, Message, Theme, Renderer>, on_record: Option Message + 'a>>, } -impl<'a, Message, Theme, Renderer> Recorder<'a, Message, Theme, Renderer> { +impl<'a, Message, Renderer> Recorder<'a, Message, Renderer> { pub fn new( content: impl Into>, ) -> Self { @@ -41,14 +44,46 @@ impl<'a, Message, Theme, Renderer> Recorder<'a, Message, Theme, Renderer> { } } -impl Widget - for Recorder<'_, Message, Theme, Renderer> +struct State { + last_hovered: Option, + last_hovered_overlay: Option, +} + +impl Widget + for Recorder<'_, Message, Renderer> where Renderer: core::Renderer, { + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State { + last_hovered: None, + last_hovered_overlay: None, + }) + } + + fn children(&self) -> Vec { + vec![widget::Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut tree::Tree) { + tree.diff_children(std::slice::from_ref(&self.content)); + } + + fn size(&self) -> Size { + self.content.as_widget().size() + } + + fn size_hint(&self) -> Size { + self.content.as_widget().size_hint() + } + fn update( &mut self, - state: &mut widget::Tree, + tree: &mut widget::Tree, event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, @@ -61,37 +96,43 @@ where return; } - self.content.as_widget_mut().update( - state, event, layout, cursor, renderer, clipboard, shell, viewport, - ); - if let Some(on_record) = &self.on_record { - record(event, cursor, shell, layout.bounds(), on_record); + let state = tree.state.downcast_mut::(); + + record( + event, + cursor, + shell, + layout.bounds(), + &mut state.last_hovered, + on_record, + |position| { + use widget::Operation; + + let mut selector = position.find_all(); + + self.content.as_widget_mut().operate( + &mut tree.children[0], + layout, + renderer, + &mut operation::black_box(&mut selector), + ); + + selector.finish() + }, + ); } - } - fn tag(&self) -> tree::Tag { - self.content.as_widget().tag() - } - - fn state(&self) -> tree::State { - self.content.as_widget().state() - } - - fn children(&self) -> Vec { - self.content.as_widget().children() - } - - fn diff(&self, tree: &mut tree::Tree) { - self.content.as_widget().diff(tree); - } - - fn size(&self) -> Size { - self.content.as_widget().size() - } - - fn size_hint(&self) -> Size { - self.content.as_widget().size_hint() + self.content.as_widget_mut().update( + &mut tree.children[0], + event, + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ); } fn layout( @@ -100,7 +141,9 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - self.content.as_widget().layout(tree, renderer, limits) + self.content + .as_widget() + .layout(&mut tree.children[0], renderer, limits) } fn draw( @@ -113,39 +156,70 @@ where cursor: mouse::Cursor, viewport: &Rectangle, ) { - self.content - .as_widget() - .draw(tree, renderer, theme, style, layout, cursor, viewport); + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor, + viewport, + ); + + let state = tree.state.downcast_ref::(); + + let Some(last_hovered) = &state.last_hovered else { + return; + }; + + let palette = theme.palette(); + + renderer.with_layer(*viewport, |renderer| { + renderer.fill_quad( + renderer::Quad { + bounds: *last_hovered, + ..renderer::Quad::default() + }, + palette.primary.scale_alpha(0.5), + ); + }); } fn mouse_interaction( &self, - state: &widget::Tree, + tree: &widget::Tree, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - self.content - .as_widget() - .mouse_interaction(state, layout, cursor, viewport, renderer) + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor, + viewport, + renderer, + ) } fn operate( &self, - state: &mut widget::Tree, + tree: &mut widget::Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn widget::Operation, ) { - self.content - .as_widget() - .operate(state, layout, renderer, operation); + self.content.as_widget().operate( + &mut tree.children[0], + layout, + renderer, + operation, + ); } fn overlay<'a>( &'a mut self, - state: &'a mut widget::Tree, + tree: &'a mut widget::Tree, layout: Layout<'a>, renderer: &Renderer, _viewport: &Rectangle, @@ -153,25 +227,34 @@ where ) -> Option> { self.content .as_widget_mut() - .overlay(state, layout, renderer, &layout.bounds(), translation) + .overlay( + &mut tree.children[0], + layout, + renderer, + &layout.bounds(), + translation, + ) .map(|raw| { + let state = tree.state.downcast_mut::(); + overlay::Element::new(Box::new(Overlay { raw, bounds: layout.bounds(), + last_hovered: &mut state.last_hovered_overlay, on_record: self.on_record.as_deref(), })) }) } } -impl<'a, Message, Theme, Renderer> From> +impl<'a, Message, Renderer> From> for Element<'a, Message, Theme, Renderer> where Message: 'a, Theme: 'a, Renderer: core::Renderer + 'a, { - fn from(recorder: Recorder<'a, Message, Theme, Renderer>) -> Self { + fn from(recorder: Recorder<'a, Message, Renderer>) -> Self { Element::new(recorder) } } @@ -179,6 +262,7 @@ where struct Overlay<'a, Message, Theme, Renderer> { raw: overlay::Element<'a, Message, Theme, Renderer>, bounds: Rectangle, + last_hovered: &'a mut Option, on_record: Option<&'a dyn Fn(Interaction) -> Message>, } @@ -230,13 +314,33 @@ where return; } + if let Some(on_event) = &self.on_record { + record( + event, + cursor, + shell, + self.bounds, + self.last_hovered, + on_event, + |position| { + use widget::Operation; + + let mut selector = position.find_all(); + + self.raw.as_overlay_mut().operate( + layout, + renderer, + &mut operation::black_box(&mut selector), + ); + + selector.finish() + }, + ); + } + self.raw .as_overlay_mut() .update(event, layout, cursor, renderer, clipboard, shell); - - if let Some(on_event) = &self.on_record { - record(event, cursor, shell, self.bounds, on_event); - } } fn mouse_interaction( @@ -262,6 +366,7 @@ where overlay::Element::new(Box::new(Overlay { raw, bounds: self.bounds, + last_hovered: self.last_hovered, on_record: self.on_record, })) }) @@ -277,7 +382,9 @@ fn record( cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, bounds: Rectangle, + last_hovered: &mut Option, on_record: impl Fn(Interaction) -> Message, + find: impl FnOnce(Point) -> operation::Outcome>, ) { if let Event::Mouse(_) = event && !cursor.is_over(bounds) @@ -294,7 +401,43 @@ fn record( Interaction::from_event(event) }; - if let Some(interaction) = interaction { + if let Some(mut interaction) = interaction { + if let Interaction::Mouse( + Mouse::Move(at) + | Mouse::Press { at: Some(at), .. } + | Mouse::Release { at: Some(at), .. } + | Mouse::Click { at: Some(at), .. }, + ) = &mut interaction + { + if let Target::Point(position) = *at + && let operation::Outcome::Some(targets) = + find(position + (bounds.position() - Point::ORIGIN)) + && let Some((content, visible_bounds)) = + targets.into_iter().rev().find_map(|target| { + if let target::Match::Text { + content, + visible_bounds, + .. + } + | target::Match::TextInput { + content, + visible_bounds, + .. + } = target + { + Some((content, visible_bounds)) + } else { + None + } + }) + { + *at = Target::Text(content); + *last_hovered = visible_bounds; + } else { + *last_hovered = None; + } + } + shell.publish(on_record(interaction)); } } diff --git a/selector/src/lib.rs b/selector/src/lib.rs index c32d065f..18142882 100644 --- a/selector/src/lib.rs +++ b/selector/src/lib.rs @@ -8,6 +8,7 @@ mod find; pub use find::{Find, FindAll}; pub use target::Target; +use crate::core::Point; use crate::core::widget::Id; pub trait Selector { @@ -116,6 +117,21 @@ impl Selector for Id { } } +impl Selector for Point { + type Output = target::Match; + + fn select(&mut self, target: Target<'_>) -> Option { + target + .visible_bounds() + .is_some_and(|visible_bounds| visible_bounds.contains(*self)) + .then(|| target::Match::from_target(target)) + } + + fn description(&self) -> String { + format!("bounds contains {:?}", self) + } +} + impl Selector for F where F: FnMut(Target<'_>) -> Option, diff --git a/selector/src/target.rs b/selector/src/target.rs index fb2e6d0f..0ae0a50f 100644 --- a/selector/src/target.rs +++ b/selector/src/target.rs @@ -68,6 +68,17 @@ impl<'a> Target<'a> { | Target::Custom { bounds, .. } => *bounds, } } + + pub fn visible_bounds(&self) -> Option { + match self { + Target::Container { visible_bounds, .. } + | Target::Focusable { visible_bounds, .. } + | Target::Scrollable { visible_bounds, .. } + | Target::TextInput { visible_bounds, .. } + | Target::Text { visible_bounds, .. } + | Target::Custom { visible_bounds, .. } => *visible_bounds, + } + } } #[derive(Debug, Clone, PartialEq)] @@ -93,6 +104,7 @@ pub enum Match { id: Option, bounds: Rectangle, visible_bounds: Option, + content: String, }, Text { id: Option, @@ -147,11 +159,12 @@ impl Match { id, bounds, visible_bounds, - .. + state, } => Self::TextInput { id: id.cloned(), bounds, visible_bounds, + content: state.text().to_owned(), }, Target::Text { id, diff --git a/test/src/instruction.rs b/test/src/instruction.rs index 1d3dd025..c7945e83 100644 --- a/test/src/instruction.rs +++ b/test/src/instruction.rs @@ -129,6 +129,19 @@ impl Interaction { None, ) } + ( + Mouse::Click { + button, + at: Some(click_at), + }, + Mouse::Move(move_at), + ) if click_at == move_at => ( + Self::Mouse(Mouse::Click { + button, + at: Some(click_at), + }), + None, + ), (current, next) => { (Self::Mouse(current), Some(Self::Mouse(next))) } From 0b00fcfff577c4d0d0a683fd9e061f0f43257252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 27 Aug 2025 02:33:58 +0200 Subject: [PATCH 38/83] Remove `test` feature and expose `Preset` unconditionally --- Cargo.lock | 1 - Cargo.toml | 5 +---- examples/todos/Cargo.toml | 3 +-- examples/todos/src/main.rs | 15 +++++---------- program/Cargo.toml | 1 - program/src/lib.rs | 3 --- src/application.rs | 16 +--------------- src/application/timed.rs | 2 -- src/daemon.rs | 31 ++++++++++++++++++++++++++++++- src/lib.rs | 3 ++- test/Cargo.toml | 4 +--- 11 files changed, 41 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6fd97456..40e459e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2488,7 +2488,6 @@ dependencies = [ "iced_devtools", "iced_futures", "iced_highlighter", - "iced_program", "iced_renderer", "iced_runtime", "iced_wgpu", diff --git a/Cargo.toml b/Cargo.toml index c8087550..0a8ac2a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,9 +48,7 @@ time-travel = ["debug", "iced_devtools/time-travel"] # Enables hot reloading (very experimental!) hot = ["debug", "iced_debug/hot"] # Enables the tester developer tool for recording and playing tests (press F12) -tester = ["debug", "test", "iced_devtools/tester"] -# Enables testing features (e.g. application presets) -test = ["iced_program/test"] +tester = ["debug", "iced_devtools/tester"] # Enables the `thread-pool` futures executor as the `executor::Default` on native platforms thread-pool = ["iced_futures/thread-pool"] # Enables `tokio` as the `executor::Default` on native platforms @@ -86,7 +84,6 @@ sipper = ["iced_runtime/sipper"] iced_debug.workspace = true iced_core.workspace = true iced_futures.workspace = true -iced_program.workspace = true iced_renderer.workspace = true iced_runtime.workspace = true iced_widget.workspace = true diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml index 9869a2c5..f47cd861 100644 --- a/examples/todos/Cargo.toml +++ b/examples/todos/Cargo.toml @@ -6,8 +6,7 @@ edition = "2024" publish = false [features] -test = ["iced/test"] -tester = ["test", "iced/tester"] +tester = ["iced/tester"] [dependencies] iced.workspace = true diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 3e15e492..ecaa99d4 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -5,7 +5,8 @@ use iced::widget::{ }; use iced::window; use iced::{ - Center, Element, Fill, Font, Function, Subscription, Task as Command, + Center, Element, Fill, Font, Function, Preset, Subscription, + Task as Command, }; use serde::{Deserialize, Serialize}; @@ -19,10 +20,8 @@ pub fn main() -> iced::Result { .subscription(Todos::subscription) .title(Todos::title) .font(Todos::ICON_FONT) - .window_size((500.0, 800.0)); - - #[cfg(feature = "test")] - let todos = todos.presets(presets()); + .window_size((500.0, 800.0)) + .presets(presets()); todos.run() } @@ -579,11 +578,7 @@ impl SavedState { } } -#[cfg(feature = "test")] -fn presets() -> impl Iterator> -{ - use iced::application::Preset; - +fn presets() -> impl Iterator> { [ Preset::new("Empty", || { (Todos::Loaded(State::default()), Command::none()) diff --git a/program/Cargo.toml b/program/Cargo.toml index 4b49f464..7aa6414d 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -15,7 +15,6 @@ workspace = true [features] time-travel = [] -test = [] [dependencies] iced_graphics.workspace = true diff --git a/program/src/lib.rs b/program/src/lib.rs index 04583bd3..c20afdfd 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -4,10 +4,8 @@ pub use iced_runtime as runtime; pub use iced_runtime::core; pub use iced_runtime::futures; -#[cfg(feature = "test")] mod preset; -#[cfg(feature = "test")] pub use preset::Preset; use crate::core::renderer; @@ -107,7 +105,6 @@ pub trait Program: Sized { 1.0 } - #[cfg(feature = "test")] fn presets(&self) -> &[Preset] { &[] } diff --git a/src/application.rs b/src/application.rs index e832834c..6b965502 100644 --- a/src/application.rs +++ b/src/application.rs @@ -35,7 +35,7 @@ use crate::shell; use crate::theme; use crate::window; use crate::{ - Element, Executor, Font, Result, Settings, Size, Subscription, Task, + Element, Executor, Font, Preset, Result, Settings, Size, Subscription, Task, }; use iced_debug as debug; @@ -44,8 +44,6 @@ use std::borrow::Cow; pub mod timed; -#[cfg(feature = "test")] -pub use program::Preset; pub use timed::timed; /// Creates an iced [`Application`] given its boot, update, and view logic. @@ -158,8 +156,6 @@ where }, settings: Settings::default(), window: window::Settings::default(), - - #[cfg(feature = "test")] presets: Vec::new(), } } @@ -176,8 +172,6 @@ pub struct Application { raw: P, settings: Settings, window: window::Settings, - - #[cfg(feature = "test")] presets: Vec>, } @@ -348,7 +342,6 @@ impl Application

{ }), settings: self.settings, window: self.window, - #[cfg(feature = "test")] presets: self.presets, } } @@ -366,7 +359,6 @@ impl Application

{ }), settings: self.settings, window: self.window, - #[cfg(feature = "test")] presets: self.presets, } } @@ -384,7 +376,6 @@ impl Application

{ }), settings: self.settings, window: self.window, - #[cfg(feature = "test")] presets: self.presets, } } @@ -402,7 +393,6 @@ impl Application

{ }), settings: self.settings, window: self.window, - #[cfg(feature = "test")] presets: self.presets, } } @@ -420,7 +410,6 @@ impl Application

{ }), settings: self.settings, window: self.window, - #[cfg(feature = "test")] presets: self.presets, } } @@ -438,7 +427,6 @@ impl Application

{ raw: program::with_executor::(self.raw), settings: self.settings, window: self.window, - #[cfg(feature = "test")] presets: self.presets, } } @@ -448,7 +436,6 @@ impl Application

{ /// Presets can be used to override the default booting strategy /// of your application during testing to create reproducible /// environments. - #[cfg(feature = "test")] pub fn presets( self, presets: impl IntoIterator>, @@ -519,7 +506,6 @@ impl Program for Application

{ self.raw.scale_factor(state, window) } - #[cfg(feature = "test")] fn presets(&self) -> &[Preset] { &self.presets } diff --git a/src/application/timed.rs b/src/application/timed.rs index 7deb3faa..02574b3a 100644 --- a/src/application/timed.rs +++ b/src/application/timed.rs @@ -147,8 +147,6 @@ where }, settings: Settings::default(), window: window::Settings::default(), - - #[cfg(feature = "test")] presets: Vec::new(), } } diff --git a/src/daemon.rs b/src/daemon.rs index d64b7712..fceb1486 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -4,7 +4,9 @@ use crate::program::{self, Program}; use crate::shell; use crate::theme; use crate::window; -use crate::{Element, Executor, Font, Result, Settings, Subscription, Task}; +use crate::{ + Element, Executor, Font, Preset, Result, Settings, Subscription, Task, +}; use iced_debug as debug; @@ -101,6 +103,7 @@ where _renderer: PhantomData, }, settings: Settings::default(), + presets: Vec::new(), } } @@ -115,6 +118,7 @@ where pub struct Daemon { raw: P, settings: Settings, + presets: Vec>, } impl Daemon

{ @@ -187,6 +191,7 @@ impl Daemon

{ debug::hot(|| title.title(state, window)) }), settings: self.settings, + presets: self.presets, } } @@ -202,6 +207,7 @@ impl Daemon

{ debug::hot(|| f(state)) }), settings: self.settings, + presets: self.presets, } } @@ -217,6 +223,7 @@ impl Daemon

{ debug::hot(|| f(state, window)) }), settings: self.settings, + presets: self.presets, } } @@ -232,6 +239,7 @@ impl Daemon

{ debug::hot(|| f(state, theme)) }), settings: self.settings, + presets: self.presets, } } @@ -247,6 +255,7 @@ impl Daemon

{ debug::hot(|| f(state, window)) }), settings: self.settings, + presets: self.presets, } } @@ -262,6 +271,22 @@ impl Daemon

{ Daemon { raw: program::with_executor::(self.raw), settings: self.settings, + presets: self.presets, + } + } + + /// Sets the boot presets of the [`Daemon`]. + /// + /// Presets can be used to override the default booting strategy + /// of your application during testing to create reproducible + /// environments. + pub fn presets( + self, + presets: impl IntoIterator>, + ) -> Self { + Self { + presets: presets.into_iter().collect(), + ..self } } } @@ -324,6 +349,10 @@ impl Program for Daemon

{ fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { self.raw.scale_factor(state, window) } + + fn presets(&self) -> &[Preset] { + &self.presets + } } /// The title logic of some [`Daemon`]. diff --git a/src/lib.rs b/src/lib.rs index 79867909..29dffb1a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -475,11 +475,11 @@ )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))] -use iced_program as program; use iced_widget::graphics; use iced_widget::renderer; use iced_winit as shell; use iced_winit::core; +use iced_winit::program; use iced_winit::runtime; pub use iced_futures::futures; @@ -525,6 +525,7 @@ pub use crate::core::{ Function, Gradient, Length, Padding, Pixels, Point, Radians, Rectangle, Rotation, Settings, Shadow, Size, Theme, Transformation, Vector, never, }; +pub use crate::program::Preset; pub use crate::runtime::exit; pub use iced_futures::Subscription; diff --git a/test/Cargo.toml b/test/Cargo.toml index af15020e..7c30cefd 100644 --- a/test/Cargo.toml +++ b/test/Cargo.toml @@ -15,10 +15,8 @@ workspace = true [dependencies] iced_runtime.workspace = true -iced_selector.workspace = true - iced_program.workspace = true -iced_program.features = ["test"] +iced_selector.workspace = true iced_renderer.workspace = true iced_renderer.features = ["fira-sans"] From 14de69704d205f7729a9fbdc669333c7a80bef35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 27 Aug 2025 02:42:28 +0200 Subject: [PATCH 39/83] Centralize `debug::hot` in `Program` implementations --- src/application.rs | 34 ++++++++++++++-------------------- src/daemon.rs | 36 ++++++++++++++---------------------- 2 files changed, 28 insertions(+), 42 deletions(-) diff --git a/src/application.rs b/src/application.rs index 6b965502..1792502f 100644 --- a/src/application.rs +++ b/src/application.rs @@ -128,7 +128,7 @@ where state: &mut Self::State, message: Self::Message, ) -> Task { - debug::hot(|| self.update.update(state, message)) + self.update.update(state, message) } fn view<'a>( @@ -136,7 +136,7 @@ where state: &'a Self::State, _window: window::Id, ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - debug::hot(|| self.view.view(state)) + self.view.view(state) } fn settings(&self) -> Settings { @@ -338,7 +338,7 @@ impl Application

{ > { Application { raw: program::with_title(self.raw, move |state, _window| { - debug::hot(|| title.title(state)) + title.title(state) }), settings: self.settings, window: self.window, @@ -354,9 +354,7 @@ impl Application

{ impl Program, > { Application { - raw: program::with_subscription(self.raw, move |state| { - debug::hot(|| f(state)) - }), + raw: program::with_subscription(self.raw, f), settings: self.settings, window: self.window, presets: self.presets, @@ -371,9 +369,7 @@ impl Application

{ impl Program, > { Application { - raw: program::with_theme(self.raw, move |state, _window| { - debug::hot(|| f(state)) - }), + raw: program::with_theme(self.raw, move |state, _window| f(state)), settings: self.settings, window: self.window, presets: self.presets, @@ -388,9 +384,7 @@ impl Application

{ impl Program, > { Application { - raw: program::with_style(self.raw, move |state, theme| { - debug::hot(|| f(state, theme)) - }), + raw: program::with_style(self.raw, f), settings: self.settings, window: self.window, presets: self.presets, @@ -406,7 +400,7 @@ impl Application

{ > { Application { raw: program::with_scale_factor(self.raw, move |state, _window| { - debug::hot(|| f(state)) + f(state) }), settings: self.settings, window: self.window, @@ -471,7 +465,7 @@ impl Program for Application

{ state: &mut Self::State, message: Self::Message, ) -> Task { - self.raw.update(state, message) + debug::hot(|| self.raw.update(state, message)) } fn view<'a>( @@ -479,15 +473,15 @@ impl Program for Application

{ state: &'a Self::State, window: window::Id, ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.raw.view(state, window) + debug::hot(|| self.raw.view(state, window)) } fn title(&self, state: &Self::State, window: window::Id) -> String { - self.raw.title(state, window) + debug::hot(|| self.raw.title(state, window)) } fn subscription(&self, state: &Self::State) -> Subscription { - self.raw.subscription(state) + debug::hot(|| self.raw.subscription(state)) } fn theme( @@ -495,15 +489,15 @@ impl Program for Application

{ state: &Self::State, window: iced_core::window::Id, ) -> Self::Theme { - self.raw.theme(state, window) + debug::hot(|| self.raw.theme(state, window)) } fn style(&self, state: &Self::State, theme: &Self::Theme) -> theme::Style { - self.raw.style(state, theme) + debug::hot(|| self.raw.style(state, theme)) } fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { - self.raw.scale_factor(state, window) + debug::hot(|| self.raw.scale_factor(state, window)) } fn presets(&self) -> &[Preset] { diff --git a/src/daemon.rs b/src/daemon.rs index fceb1486..a961eed5 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -80,7 +80,7 @@ where state: &mut Self::State, message: Self::Message, ) -> Task { - debug::hot(|| self.update.update(state, message)) + self.update.update(state, message) } fn view<'a>( @@ -88,7 +88,7 @@ where state: &'a Self::State, window: window::Id, ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - debug::hot(|| self.view.view(state, window)) + self.view.view(state, window) } } @@ -188,7 +188,7 @@ impl Daemon

{ > { Daemon { raw: program::with_title(self.raw, move |state, window| { - debug::hot(|| title.title(state, window)) + title.title(state, window) }), settings: self.settings, presets: self.presets, @@ -203,9 +203,7 @@ impl Daemon

{ impl Program, > { Daemon { - raw: program::with_subscription(self.raw, move |state| { - debug::hot(|| f(state)) - }), + raw: program::with_subscription(self.raw, f), settings: self.settings, presets: self.presets, } @@ -219,9 +217,7 @@ impl Daemon

{ impl Program, > { Daemon { - raw: program::with_theme(self.raw, move |state, window| { - debug::hot(|| f(state, window)) - }), + raw: program::with_theme(self.raw, f), settings: self.settings, presets: self.presets, } @@ -235,9 +231,7 @@ impl Daemon

{ impl Program, > { Daemon { - raw: program::with_style(self.raw, move |state, theme| { - debug::hot(|| f(state, theme)) - }), + raw: program::with_style(self.raw, f), settings: self.settings, presets: self.presets, } @@ -251,9 +245,7 @@ impl Daemon

{ impl Program, > { Daemon { - raw: program::with_scale_factor(self.raw, move |state, window| { - debug::hot(|| f(state, window)) - }), + raw: program::with_scale_factor(self.raw, f), settings: self.settings, presets: self.presets, } @@ -315,7 +307,7 @@ impl Program for Daemon

{ state: &mut Self::State, message: Self::Message, ) -> Task { - self.raw.update(state, message) + debug::hot(|| self.raw.update(state, message)) } fn view<'a>( @@ -323,15 +315,15 @@ impl Program for Daemon

{ state: &'a Self::State, window: window::Id, ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.raw.view(state, window) + debug::hot(|| self.raw.view(state, window)) } fn title(&self, state: &Self::State, window: window::Id) -> String { - self.raw.title(state, window) + debug::hot(|| self.raw.title(state, window)) } fn subscription(&self, state: &Self::State) -> Subscription { - self.raw.subscription(state) + debug::hot(|| self.raw.subscription(state)) } fn theme( @@ -339,15 +331,15 @@ impl Program for Daemon

{ state: &Self::State, window: iced_core::window::Id, ) -> Self::Theme { - self.raw.theme(state, window) + debug::hot(|| self.raw.theme(state, window)) } fn style(&self, state: &Self::State, theme: &Self::Theme) -> theme::Style { - self.raw.style(state, theme) + debug::hot(|| self.raw.style(state, theme)) } fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { - self.raw.scale_factor(state, window) + debug::hot(|| self.raw.scale_factor(state, window)) } fn presets(&self) -> &[Preset] { From d64cb0d9c1367c722b07d1131b7049fd476833d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 27 Aug 2025 04:19:08 +0200 Subject: [PATCH 40/83] Discard last recorded mouse movements in `tester` --- devtools/src/tester.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/devtools/src/tester.rs b/devtools/src/tester.rs index fda621c6..0c917680 100644 --- a/devtools/src/tester.rs +++ b/devtools/src/tester.rs @@ -152,6 +152,15 @@ impl Tester

{ return Task::none(); }; + while let Some(Instruction::Interact( + instruction::Interaction::Mouse(instruction::Mouse::Move( + _, + )), + )) = self.instructions.last() + { + let _ = self.instructions.pop(); + } + let (state, window) = emulator.into_state(); self.state = State::Ready { state, window }; From ebaf0238a9ba530962b3714272f6fe0687d99ccd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 27 Aug 2025 04:43:46 +0200 Subject: [PATCH 41/83] Disambiguate text selectors when recording with a coordinate fallback --- devtools/src/tester/recorder.rs | 137 +++++++++++++++++++------------- 1 file changed, 80 insertions(+), 57 deletions(-) diff --git a/devtools/src/tester/recorder.rs b/devtools/src/tester/recorder.rs index a15e460c..6df06589 100644 --- a/devtools/src/tester/recorder.rs +++ b/devtools/src/tester/recorder.rs @@ -106,19 +106,13 @@ where layout.bounds(), &mut state.last_hovered, on_record, - |position| { - use widget::Operation; - - let mut selector = position.find_all(); - + |operation| { self.content.as_widget_mut().operate( &mut tree.children[0], layout, renderer, - &mut operation::black_box(&mut selector), + operation, ); - - selector.finish() }, ); } @@ -322,18 +316,10 @@ where self.bounds, self.last_hovered, on_event, - |position| { - use widget::Operation; - - let mut selector = position.find_all(); - - self.raw.as_overlay_mut().operate( - layout, - renderer, - &mut operation::black_box(&mut selector), - ); - - selector.finish() + |operation| { + self.raw + .as_overlay_mut() + .operate(layout, renderer, operation); }, ); } @@ -384,7 +370,7 @@ fn record( bounds: Rectangle, last_hovered: &mut Option, on_record: impl Fn(Interaction) -> Message, - find: impl FnOnce(Point) -> operation::Outcome>, + operate: impl FnMut(&mut dyn widget::Operation), ) { if let Event::Mouse(_) = event && !cursor.is_over(bounds) @@ -401,43 +387,80 @@ fn record( Interaction::from_event(event) }; - if let Some(mut interaction) = interaction { - if let Interaction::Mouse( - Mouse::Move(at) - | Mouse::Press { at: Some(at), .. } - | Mouse::Release { at: Some(at), .. } - | Mouse::Click { at: Some(at), .. }, - ) = &mut interaction - { - if let Target::Point(position) = *at - && let operation::Outcome::Some(targets) = - find(position + (bounds.position() - Point::ORIGIN)) - && let Some((content, visible_bounds)) = - targets.into_iter().rev().find_map(|target| { - if let target::Match::Text { - content, - visible_bounds, - .. - } - | target::Match::TextInput { - content, - visible_bounds, - .. - } = target - { - Some((content, visible_bounds)) - } else { - None - } - }) - { - *at = Target::Text(content); - *last_hovered = visible_bounds; - } else { - *last_hovered = None; - } - } + let Some(mut interaction) = interaction else { + return; + }; + let Interaction::Mouse( + Mouse::Move(at) + | Mouse::Press { at: Some(at), .. } + | Mouse::Release { at: Some(at), .. } + | Mouse::Click { at: Some(at), .. }, + ) = &mut interaction + else { shell.publish(on_record(interaction)); + return; + }; + + let Target::Point(position) = *at else { + shell.publish(on_record(interaction)); + return; + }; + + if let Some((content, visible_bounds)) = + find_text(position + (bounds.position() - Point::ORIGIN), operate) + { + *at = Target::Text(content); + *last_hovered = visible_bounds; + } else { + *last_hovered = None; } + + shell.publish(on_record(interaction)); +} + +fn find_text( + position: Point, + mut operate: impl FnMut(&mut dyn widget::Operation), +) -> Option<(String, Option)> { + use widget::Operation; + + let mut by_position = position.find_all(); + operate(&mut operation::black_box(&mut by_position)); + + let operation::Outcome::Some(targets) = by_position.finish() else { + return None; + }; + + let (content, visible_bounds) = + targets.into_iter().rev().find_map(|target| { + if let target::Match::Text { + content, + visible_bounds, + .. + } + | target::Match::TextInput { + content, + visible_bounds, + .. + } = target + { + Some((content, visible_bounds)) + } else { + None + } + })?; + + let mut by_text = content.clone().find_all(); + operate(&mut operation::black_box(&mut by_text)); + + let operation::Outcome::Some(texts) = by_text.finish() else { + return None; + }; + + if texts.len() > 1 { + return None; + } + + Some((content, visible_bounds)) } From e136e14b7cc95edc45bd54e688f30d90d4f9d218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 27 Aug 2025 05:04:27 +0200 Subject: [PATCH 42/83] Introduce `Asserting` mode after `Recording` in `tester` --- devtools/src/tester.rs | 127 +++++++++++++++++++++++++++++------------ 1 file changed, 89 insertions(+), 38 deletions(-) diff --git a/devtools/src/tester.rs b/devtools/src/tester.rs index 0c917680..de1d3e25 100644 --- a/devtools/src/tester.rs +++ b/devtools/src/tester.rs @@ -7,6 +7,7 @@ use crate::core::Alignment::Center; use crate::core::Length::Fill; use crate::core::alignment::Horizontal::Right; use crate::core::border; +use crate::core::mouse; use crate::core::window; use crate::core::{Element, Font, Size, Theme}; use crate::executor; @@ -38,9 +39,10 @@ enum State { Recording { emulator: Emulator

, }, - Ready { + Asserting { state: P::State, window: window::Id, + last_interaction: Option, }, Playing { emulator: Emulator

, @@ -75,8 +77,9 @@ pub enum Message { pub enum Tick { Tester(Message), Program(P::Message), - Recorder(instruction::Interaction), Emulator(emulator::Event

), + Record(instruction::Interaction), + Assert(instruction::Interaction), } impl Tester

{ @@ -163,7 +166,11 @@ impl Tester

{ let (state, window) = emulator.into_state(); - self.state = State::Ready { state, window }; + self.state = State::Asserting { + state, + window, + last_interaction: None, + }; Task::none() } @@ -325,34 +332,6 @@ impl Tester

{ Task::none() } - Tick::Recorder(interaction) => { - let mut interaction = Some(interaction); - - while let Some(new_interaction) = interaction.take() { - if let Some(Instruction::Interact(last_interaction)) = - self.instructions.pop() - { - let (merged_interaction, new_interaction) = - last_interaction.merge(new_interaction); - - if let Some(new_interaction) = new_interaction { - self.instructions.push(Instruction::Interact( - merged_interaction, - )); - - self.instructions - .push(Instruction::Interact(new_interaction)); - } else { - interaction = Some(merged_interaction); - } - } else { - self.instructions - .push(Instruction::Interact(new_interaction)); - } - } - - Task::none() - } Tick::Emulator(event) => { match &mut self.state { State::Recording { emulator } => { @@ -385,9 +364,75 @@ impl Tester

{ } } }, - State::Idle | State::Ready { .. } => {} + State::Idle | State::Asserting { .. } => {} } + Task::none() + } + Tick::Record(interaction) => { + let mut interaction = Some(interaction); + + while let Some(new_interaction) = interaction.take() { + if let Some(Instruction::Interact(last_interaction)) = + self.instructions.pop() + { + let (merged_interaction, new_interaction) = + last_interaction.merge(new_interaction); + + if let Some(new_interaction) = new_interaction { + self.instructions.push(Instruction::Interact( + merged_interaction, + )); + + self.instructions + .push(Instruction::Interact(new_interaction)); + } else { + interaction = Some(merged_interaction); + } + } else { + self.instructions + .push(Instruction::Interact(new_interaction)); + } + } + + Task::none() + } + Tick::Assert(interaction) => { + let State::Asserting { + last_interaction, .. + } = &mut self.state + else { + return Task::none(); + }; + + *last_interaction = + if let Some(last_interaction) = last_interaction.take() { + let (merged, new) = last_interaction.merge(interaction); + + Some(new.unwrap_or(merged)) + } else { + Some(interaction) + }; + + let Some(interaction) = last_interaction.take() else { + return Task::none(); + }; + + let instruction::Interaction::Mouse( + instruction::Mouse::Click { + button: mouse::Button::Left, + at: Some(instruction::Target::Text(text)), + }, + ) = interaction + else { + *last_interaction = Some(interaction); + return Task::none(); + }; + + self.instructions.push(Instruction::Expect( + instruction::Expectation::Text(text), + )); + Task::none() } } @@ -403,7 +448,7 @@ impl Tester

{ let (icon, label) = match &self.state { State::Idle => (text(""), "Idle"), State::Recording { .. } => (icon::record(), "Recording"), - State::Ready { .. } => (icon::lightbulb(), "Ready"), + State::Asserting { .. } => (icon::lightbulb(), "Asserting"), State::Playing { outcome, .. } => match outcome { Outcome::Running => (icon::play(), "Playing"), Outcome::Failed => (icon::cancel(), "Failed"), @@ -421,7 +466,9 @@ impl Tester

{ State::Recording { .. } => { palette.danger.base.color } - State::Ready { .. } => palette.warning.base.color, + State::Asserting { .. } => { + palette.warning.base.color + } State::Playing { outcome, .. } => match outcome { Outcome::Running => theme.palette().primary, Outcome::Failed => theme.palette().danger, @@ -449,16 +496,20 @@ impl Tester

{ Element::from( recorder(themer(theme, view)) - .on_record(Tick::Recorder), + .on_record(Tick::Record), ) .map(emulate) } - State::Ready { state, window } => { + State::Asserting { state, window, .. } => { let theme = program.theme(state, *window); let view = program.view(state, *window).map(Tick::Program); - Element::from(themer(theme, view)).map(emulate) + Element::from( + recorder(themer(theme, view)) + .on_record(Tick::Assert), + ) + .map(emulate) } State::Playing { emulator, .. } => { let theme = emulator.theme(program); @@ -482,7 +533,7 @@ impl Tester

{ border: border::width(2.0).color(match &self.state { State::Idle => palette.background.strongest.color, State::Recording { .. } => palette.danger.base.color, - State::Ready { .. } => palette.warning.weak.color, + State::Asserting { .. } => palette.warning.weak.color, State::Playing { outcome, .. } => match outcome { Outcome::Running => palette.primary.base.color, Outcome::Failed => palette.danger.strong.color, From 6a6a2ac8c5d868893c8c2362502987d95c41aaa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 27 Aug 2025 06:02:02 +0200 Subject: [PATCH 43/83] Implement `iced_test::run` entrypoint for ice testing --- devtools/src/tester.rs | 12 ++--- examples/todos/src/main.rs | 23 ++++++--- test/src/emulator.rs | 8 +-- test/src/error.rs | 21 ++++++++ test/src/ice.rs | 6 +-- test/src/lib.rs | 101 +++++++++++++++++++++++++++++++++++++ 6 files changed, 149 insertions(+), 22 deletions(-) diff --git a/devtools/src/tester.rs b/devtools/src/tester.rs index de1d3e25..1f5397c0 100644 --- a/devtools/src/tester.rs +++ b/devtools/src/tester.rs @@ -221,10 +221,7 @@ impl Tester

{ self.confirm(); let ice = Ice { - viewport: Size::new( - self.viewport.width as u32, - self.viewport.height as u32, - ), + viewport: self.viewport, mode: self.mode, preset: self.preset.clone(), instructions: self.instructions.clone(), @@ -246,10 +243,7 @@ impl Tester

{ .discard() } Message::Imported(Ok(ice)) => { - self.viewport = Size::new( - ice.viewport.width as f32, - ice.viewport.height as f32, - ); + self.viewport = ice.viewport; self.mode = ice.mode; self.preset = ice.preset; self.instructions = ice.instructions; @@ -347,7 +341,7 @@ impl Tester

{ emulator::Event::Action(action) => { emulator.perform(program, action); } - emulator::Event::Failed => { + emulator::Event::Failed(_instruction) => { *outcome = Outcome::Failed; } emulator::Event::Ready => { diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index ecaa99d4..0f7c1009 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -5,8 +5,8 @@ use iced::widget::{ }; use iced::window; use iced::{ - Center, Element, Fill, Font, Function, Preset, Subscription, - Task as Command, + Application, Center, Element, Fill, Font, Function, Preset, Program, + Subscription, Task as Command, }; use serde::{Deserialize, Serialize}; @@ -16,14 +16,16 @@ pub fn main() -> iced::Result { #[cfg(not(target_arch = "wasm32"))] tracing_subscriber::fmt::init(); - let todos = iced::application(Todos::new, Todos::update, Todos::view) + application().run() +} + +fn application() -> Application { + iced::application(Todos::new, Todos::update, Todos::view) .subscription(Todos::subscription) .title(Todos::title) .font(Todos::ICON_FONT) .window_size((500.0, 800.0)) - .presets(presets()); - - todos.run() + .presets(presets()) } #[derive(Debug)] @@ -648,4 +650,13 @@ mod tests { Ok(()) } + + #[test] + #[ignore] + fn it_passes_the_ice_tests() -> Result<(), Error> { + iced_test::run( + application(), + format!("{}/tests", env!("CARGO_MANIFEST_DIR")), + ) + } } diff --git a/test/src/emulator.rs b/test/src/emulator.rs index b51d4a5c..ef358d7a 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -36,7 +36,7 @@ pub struct Emulator { #[allow(missing_debug_implementations)] pub enum Event { Action(Action), - Failed, + Failed(Instruction), Ready, } @@ -211,7 +211,7 @@ impl Emulator

{ let mut messages = Vec::new(); - match instruction { + match &instruction { Instruction::Interact(interaction) => { let Some(events) = interaction.events(|target| match target { instruction::Target::Point(position) => Some(*position), @@ -234,7 +234,7 @@ impl Emulator

{ } } }) else { - self.runtime.send(Event::Failed); + self.runtime.send(Event::Failed(instruction)); self.cache = Some(user_interface.into_cache()); return; }; @@ -282,7 +282,7 @@ impl Emulator

{ self.runtime.send(Event::Ready); } _ => { - self.runtime.send(Event::Failed); + self.runtime.send(Event::Failed(instruction)); } } diff --git a/test/src/error.rs b/test/src/error.rs index 1e1de331..4b898e95 100644 --- a/test/src/error.rs +++ b/test/src/error.rs @@ -1,4 +1,8 @@ +use crate::Instruction; +use crate::ice; + use std::io; +use std::path::PathBuf; use std::sync::Arc; /// A test error. @@ -20,6 +24,23 @@ pub enum Error { /// The encoding of some PNG image failed. #[error("the encoding of some PNG image failed: {0}")] PngEncodingFailed(Arc), + #[error("the ice test ({file}) is invalid: {error}")] + IceParsingFailed { + file: PathBuf, + error: ice::ParseError, + }, + #[error("the ice test ({file}) failed")] + IceFailed { + file: PathBuf, + instruction: Instruction, + }, + #[error( + "the preset \"{name}\" does not exist (available presets: {available:?})" + )] + PresetNotFound { + name: String, + available: Vec, + }, } impl From for Error { diff --git a/test/src/ice.rs b/test/src/ice.rs index ab1a9c8f..a8e44a93 100644 --- a/test/src/ice.rs +++ b/test/src/ice.rs @@ -5,7 +5,7 @@ use crate::instruction; #[derive(Debug, Clone, PartialEq)] pub struct Ice { - pub viewport: Size, + pub viewport: Size, pub mode: emulator::Mode, pub preset: Option, pub instructions: Vec, @@ -110,8 +110,8 @@ impl std::fmt::Display for Ice { writeln!( f, "viewport: {width}x{height}", - width = self.viewport.width, - height = self.viewport.height + width = self.viewport.width as u32, // TODO + height = self.viewport.height as u32, // TODO )?; writeln!( diff --git a/test/src/lib.rs b/test/src/lib.rs index baccc5e3..82f5db92 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -104,3 +104,104 @@ pub use ice::Ice; pub use instruction::Instruction; pub use selector::Selector; pub use simulator::{Simulator, simulator}; + +use std::path::Path; + +pub fn run( + program: impl program::Program + 'static, + tests_dir: impl AsRef, +) -> Result<(), Error> { + use crate::runtime::futures::futures::StreamExt; + use crate::runtime::futures::futures::channel::mpsc; + use crate::runtime::futures::futures::executor; + + use std::ffi::OsStr; + use std::fs; + + let tests = fs::read_dir(tests_dir)?; + + // TODO: Concurrent runtimes + for file in tests { + let file = file?; + + if file.path().extension().and_then(OsStr::to_str) != Some("ice") { + continue; + } + + let ice = { + let content = fs::read_to_string(file.path())?; + + match Ice::parse(&content) { + Ok(ice) => ice, + Err(error) => { + return Err(Error::IceParsingFailed { + file: file.path().to_path_buf(), + error, + }); + } + } + }; + + let (sender, mut receiver) = mpsc::channel(1); + + let preset = if let Some(preset) = ice.preset { + let Some(preset) = program + .presets() + .iter() + .find(|candidate| candidate.name() == preset) + else { + return Err(Error::PresetNotFound { + name: preset.to_owned(), + available: program + .presets() + .iter() + .map(program::Preset::name) + .map(str::to_owned) + .collect(), + }); + }; + + Some(preset) + } else { + None + }; + + let mut emulator = Emulator::with_preset( + sender, + &program, + ice.mode, + ice.viewport, + preset, + ); + + let mut instructions: Vec<_> = + ice.instructions.into_iter().rev().collect(); + + loop { + let Some(event) = executor::block_on(receiver.next()) else { + panic!("emulator runtime stopped unexpectedly"); + }; + + match event { + emulator::Event::Action(action) => { + emulator.perform(&program, action); + } + emulator::Event::Failed(instruction) => { + return Err(Error::IceFailed { + file: file.path().to_path_buf(), + instruction, + }); + } + emulator::Event::Ready => { + let Some(instruction) = instructions.pop() else { + break; + }; + + emulator.run(&program, instruction); + } + } + } + } + + Ok(()) +} From d8dbb7568ad59290f46a918a79375667b258f27b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 27 Aug 2025 06:04:12 +0200 Subject: [PATCH 44/83] Iterate instructions instead of collecting in `iced_test::run` --- test/src/lib.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/src/lib.rs b/test/src/lib.rs index 82f5db92..8680f885 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -174,8 +174,7 @@ pub fn run( preset, ); - let mut instructions: Vec<_> = - ice.instructions.into_iter().rev().collect(); + let mut instructions = ice.instructions.into_iter(); loop { let Some(event) = executor::block_on(receiver.next()) else { @@ -193,7 +192,7 @@ pub fn run( }); } emulator::Event::Ready => { - let Some(instruction) = instructions.pop() else { + let Some(instruction) = instructions.next() else { break; }; From ed873514e5630e113a22aeb4b2c058bdc860bac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 27 Aug 2025 06:12:38 +0200 Subject: [PATCH 45/83] Parse and validate all tests first in `iced_test::run` --- test/src/lib.rs | 75 +++++++++++++++++++++++++------------------------ 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/test/src/lib.rs b/test/src/lib.rs index 8680f885..cc74509a 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -118,53 +118,56 @@ pub fn run( use std::ffi::OsStr; use std::fs; - let tests = fs::read_dir(tests_dir)?; + let files = fs::read_dir(tests_dir)?; + let mut tests = Vec::new(); - // TODO: Concurrent runtimes - for file in tests { + for file in files { let file = file?; if file.path().extension().and_then(OsStr::to_str) != Some("ice") { continue; } - let ice = { - let content = fs::read_to_string(file.path())?; + let content = fs::read_to_string(file.path())?; - match Ice::parse(&content) { - Ok(ice) => ice, - Err(error) => { - return Err(Error::IceParsingFailed { - file: file.path().to_path_buf(), - error, - }); - } - } - }; - - let (sender, mut receiver) = mpsc::channel(1); - - let preset = if let Some(preset) = ice.preset { - let Some(preset) = program - .presets() - .iter() - .find(|candidate| candidate.name() == preset) - else { - return Err(Error::PresetNotFound { - name: preset.to_owned(), - available: program + match Ice::parse(&content) { + Ok(ice) => { + let preset = if let Some(preset) = &ice.preset { + let Some(preset) = program .presets() .iter() - .map(program::Preset::name) - .map(str::to_owned) - .collect(), - }); - }; + .find(|candidate| candidate.name() == preset) + else { + return Err(Error::PresetNotFound { + name: preset.to_owned(), + available: program + .presets() + .iter() + .map(program::Preset::name) + .map(str::to_owned) + .collect(), + }); + }; - Some(preset) - } else { - None - }; + Some(preset) + } else { + None + }; + + tests.push((file, ice, preset)); + } + Err(error) => { + return Err(Error::IceParsingFailed { + file: file.path().to_path_buf(), + error, + }); + } + } + } + + // TODO: Concurrent runtimes + for (file, ice, preset) in tests { + let (sender, mut receiver) = mpsc::channel(1); let mut emulator = Emulator::with_preset( sender, From 1c54f5e3e9df51aafd81151d2c1bbde9eec2478f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 27 Aug 2025 06:18:23 +0200 Subject: [PATCH 46/83] Improve naming of `Error` variants in `iced_test` --- test/src/error.rs | 6 +++--- test/src/lib.rs | 7 +++---- test/src/simulator.rs | 6 +++--- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/test/src/error.rs b/test/src/error.rs index 4b898e95..520fd1c6 100644 --- a/test/src/error.rs +++ b/test/src/error.rs @@ -10,9 +10,9 @@ use std::sync::Arc; pub enum Error { /// No matching widget was found for the [`Selector`](crate::Selector). #[error("no matching widget was found for the selector: {selector}")] - NotFound { selector: String }, + SelectorNotFound { selector: String }, #[error("the matching target is not visible: {target:?}")] - NotVisible { + TargetNotVisible { target: Arc, }, /// An IO operation failed. @@ -30,7 +30,7 @@ pub enum Error { error: ice::ParseError, }, #[error("the ice test ({file}) failed")] - IceFailed { + IceTestingFailed { file: PathBuf, instruction: Instruction, }, diff --git a/test/src/lib.rs b/test/src/lib.rs index cc74509a..5f4f61dd 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -180,16 +180,15 @@ pub fn run( let mut instructions = ice.instructions.into_iter(); loop { - let Some(event) = executor::block_on(receiver.next()) else { - panic!("emulator runtime stopped unexpectedly"); - }; + let event = executor::block_on(receiver.next()) + .expect("emulator runtime should never stop on its own"); match event { emulator::Event::Action(action) => { emulator.perform(&program, action); } emulator::Event::Failed(instruction) => { - return Err(Error::IceFailed { + return Err(Error::IceTestingFailed { file: file.path().to_path_buf(), instruction, }); diff --git a/test/src/simulator.rs b/test/src/simulator.rs index 30f772a6..93b933a5 100644 --- a/test/src/simulator.rs +++ b/test/src/simulator.rs @@ -118,11 +118,11 @@ where match operation.finish() { widget::operation::Outcome::Some(output) => { - output.ok_or(Error::NotFound { + output.ok_or(Error::SelectorNotFound { selector: description, }) } - _ => Err(Error::NotFound { + _ => Err(Error::SelectorNotFound { selector: description, }), } @@ -148,7 +148,7 @@ where let target = self.find(selector)?; let Some(visible_bounds) = target.visible_bounds() else { - return Err(Error::NotVisible { + return Err(Error::TargetNotVisible { target: Arc::new(target), }); }; From 445b6c86081e72a0c0175380f04f6f7cdfed4d48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 27 Aug 2025 10:54:17 +0200 Subject: [PATCH 47/83] Fix duplicate recording when `Overlay` present --- devtools/src/tester/recorder.rs | 40 +++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/devtools/src/tester/recorder.rs b/devtools/src/tester/recorder.rs index 6df06589..01b6e2e2 100644 --- a/devtools/src/tester/recorder.rs +++ b/devtools/src/tester/recorder.rs @@ -23,6 +23,7 @@ pub fn recorder<'a, Message, Renderer>( pub struct Recorder<'a, Message, Renderer> { content: Element<'a, Message, Theme, Renderer>, on_record: Option Message + 'a>>, + has_overlay: bool, } impl<'a, Message, Renderer> Recorder<'a, Message, Renderer> { @@ -32,6 +33,7 @@ impl<'a, Message, Renderer> Recorder<'a, Message, Renderer> { Self { content: content.into(), on_record: None, + has_overlay: false, } } @@ -96,7 +98,9 @@ where return; } - if let Some(on_record) = &self.on_record { + if !self.has_overlay + && let Some(on_record) = &self.on_record + { let state = tree.state.downcast_mut::(); record( @@ -174,7 +178,7 @@ where bounds: *last_hovered, ..renderer::Quad::default() }, - palette.primary.scale_alpha(0.5), + palette.primary.scale_alpha(0.7), ); }); } @@ -219,6 +223,8 @@ where _viewport: &Rectangle, translation: Vector, ) -> Option> { + self.has_overlay = false; + self.content .as_widget_mut() .overlay( @@ -229,6 +235,8 @@ where translation, ) .map(|raw| { + self.has_overlay = true; + let state = tree.state.downcast_mut::(); overlay::Element::new(Box::new(Overlay { @@ -253,22 +261,20 @@ where } } -struct Overlay<'a, Message, Theme, Renderer> { +struct Overlay<'a, Message, Renderer> { raw: overlay::Element<'a, Message, Theme, Renderer>, bounds: Rectangle, last_hovered: &'a mut Option, on_record: Option<&'a dyn Fn(Interaction) -> Message>, } -impl<'a, Message, Theme, Renderer> core::Overlay - for Overlay<'a, Message, Theme, Renderer> +impl<'a, Message, Renderer> core::Overlay + for Overlay<'a, Message, Renderer> where Renderer: core::Renderer + 'a, { - fn layout(&mut self, renderer: &Renderer, _bounds: Size) -> layout::Node { - self.raw - .as_overlay_mut() - .layout(renderer, self.bounds.size()) + fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node { + self.raw.as_overlay_mut().layout(renderer, bounds) } fn draw( @@ -282,6 +288,22 @@ where self.raw .as_overlay() .draw(renderer, theme, style, layout, cursor); + + let Some(last_hovered) = &self.last_hovered else { + return; + }; + + let palette = theme.palette(); + + renderer.with_layer(self.bounds, |renderer| { + renderer.fill_quad( + renderer::Quad { + bounds: *last_hovered, + ..renderer::Quad::default() + }, + palette.primary.scale_alpha(0.7), + ); + }); } fn operate( From 7c1f19354264ed2dfd4f5c99ba26a3ff4c3749bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 27 Aug 2025 10:54:53 +0200 Subject: [PATCH 48/83] Add `visible_bounds` helper to `widget::selector` --- runtime/src/widget/selector.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/runtime/src/widget/selector.rs b/runtime/src/widget/selector.rs index d6fc6f9a..fa7ff749 100644 --- a/runtime/src/widget/selector.rs +++ b/runtime/src/widget/selector.rs @@ -3,6 +3,8 @@ pub use iced_selector::Selector; pub use iced_selector::target::{Bounded, Match, Target, Text}; +use crate::core::Rectangle; + use crate::Task; use crate::core::widget; use crate::task; @@ -16,3 +18,13 @@ pub fn find_by_id(id: impl Into) -> Task> { pub fn find_by_text(text: impl Into) -> Task> { task::widget(Selector::find(text.into())) } + +/// Finds the visible bounds of the first [`Selector`] target. +pub fn visible_bounds(selector: S) -> Task> +where + S: Selector + Send + 'static, + S::Output: Bounded + Clone + Send + 'static, +{ + task::widget(selector.find()) + .map(|target| target.as_ref().and_then(Bounded::visible_bounds)) +} From d7aab6c4ec9618bcb75fc523f54bf3b1946bef82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 27 Aug 2025 10:55:34 +0200 Subject: [PATCH 49/83] Remove leftover code in `selector` module --- selector/src/lib.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/selector/src/lib.rs b/selector/src/lib.rs index 18142882..b8081178 100644 --- a/selector/src/lib.rs +++ b/selector/src/lib.rs @@ -146,18 +146,3 @@ where format!("custom selector: {}", std::any::type_name_of_val(self)) } } - -// pub fn inspect(position: Point) -> impl Selector { -// visible(move |target: Target<'_>, visible_bounds: Rectangle| { -// visible_bounds -// .contains(position) -// .then(|| Match::from_target(target)) -// }) -// } - -// pub fn visible( -// f: impl Fn(Target<'_>, Rectangle) -> Option, -// ) -> impl Selector { -// todo!() -// } -// From bf3ac04498924c73ea5c938deb5856108936322e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 27 Aug 2025 10:59:45 +0200 Subject: [PATCH 50/83] Keep `window::open` impure (for now) --- examples/multi_window/src/main.rs | 11 +++++++---- runtime/src/window.rs | 15 ++++++++++----- test/src/emulator.rs | 4 ++-- winit/src/lib.rs | 5 ++--- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index f7c57d2a..3183ecee 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -42,12 +42,13 @@ enum Message { impl Example { fn new() -> (Self, Task) { + let (_, open) = window::open(window::Settings::default()); + ( Self { windows: BTreeMap::new(), }, - window::open(window::Settings::default()) - .map(Message::WindowOpened), + open.map(Message::WindowOpened), ) } @@ -76,10 +77,12 @@ impl Example { }, ); - window::open(window::Settings { + let (_, open) = window::open(window::Settings { position, ..window::Settings::default() - }) + }); + + open }) .map(Message::WindowOpened) } diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 19044628..ccd8721b 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -18,7 +18,7 @@ use raw_window_handle::WindowHandle; #[allow(missing_debug_implementations)] pub enum Action { /// Opens a new window with some [`Settings`]. - Open(Settings, oneshot::Sender), + Open(Id, Settings, oneshot::Sender), /// Close the window and exits the application. Close(Id), @@ -249,10 +249,15 @@ pub fn close_requests() -> Subscription { /// Opens a new window with the given [`Settings`]; producing the [`Id`] /// of the new window on completion. -pub fn open(settings: Settings) -> Task { - task::oneshot(|channel| { - crate::Action::Window(Action::Open(settings, channel)) - }) +pub fn open(settings: Settings) -> (Id, Task) { + let id = Id::unique(); + + ( + id, + task::oneshot(|channel| { + crate::Action::Window(Action::Open(id, settings, channel)) + }), + ) } /// Closes the window with `id`. diff --git a/test/src/emulator.rs b/test/src/emulator.rs index ef358d7a..9b74dee3 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -145,8 +145,8 @@ impl Emulator

{ dbg!(action); } Action::Window(action) => match action { - window::Action::Open(_settings, sender) => { - self.window = core::window::Id::unique(); + window::Action::Open(id, _settings, sender) => { + self.window = id; let _ = sender.send(self.window); } diff --git a/winit/src/lib.rs b/winit/src/lib.rs index de1a9f9f..0ad2c507 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -109,7 +109,7 @@ where let task = if let Some(window_settings) = window_settings { let mut task = Some(task); - let open = runtime::window::open(window_settings); + let (_id, open) = runtime::window::open(window_settings); open.then(move |_| task.take().unwrap_or(Task::none())) } else { @@ -1122,8 +1122,7 @@ fn run_action<'a, P, C>( } }, Action::Window(action) => match action { - window::Action::Open(settings, channel) => { - let id = core::window::Id::unique(); + window::Action::Open(id, settings, channel) => { let monitor = window_manager.last_monitor(); control_sender From 11928ba66815977d9dbc18756f4730cc7ab91746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 28 Aug 2025 10:40:43 +0200 Subject: [PATCH 51/83] Merge `Mouse::Press` and `Mouse::Move` interactions --- test/src/instruction.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/src/instruction.rs b/test/src/instruction.rs index c7945e83..14cceb2f 100644 --- a/test/src/instruction.rs +++ b/test/src/instruction.rs @@ -129,6 +129,19 @@ impl Interaction { None, ) } + ( + Mouse::Press { + button, + at: Some(press_at), + }, + Mouse::Move(move_at), + ) if press_at == move_at => ( + Self::Mouse(Mouse::Press { + button, + at: Some(press_at), + }), + None, + ), ( Mouse::Click { button, From 751714388cd551c0c83c0ebc911824ab79d96cdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 28 Aug 2025 10:41:22 +0200 Subject: [PATCH 52/83] Use default `window::Settings::size` as `tester` viewport --- devtools/src/tester.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devtools/src/tester.rs b/devtools/src/tester.rs index 1f5397c0..b177d0cc 100644 --- a/devtools/src/tester.rs +++ b/devtools/src/tester.rs @@ -86,7 +86,7 @@ impl Tester

{ pub fn new(program: &P) -> Self { Self { mode: emulator::Mode::default(), - viewport: Size::new(512.0, 512.0), + viewport: window::Settings::default().size, presets: combo_box::State::new( program .presets() From 71ee026444a26d2779221832320be1ce71b86696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 28 Aug 2025 11:44:18 +0200 Subject: [PATCH 53/83] =?UTF-8?q?Add=20`Zen`=20variant=20to=20`emulator::M?= =?UTF-8?q?ode`=20=F0=9F=A7=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/src/emulator.rs | 213 ++++++++++++++++++++++++++----------------- test/src/ice.rs | 2 + 2 files changed, 130 insertions(+), 85 deletions(-) diff --git a/test/src/emulator.rs b/test/src/emulator.rs index 9b74dee3..63845b91 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -6,6 +6,7 @@ use crate::core::{Element, Point, Size}; use crate::instruction; use crate::program; use crate::program::Program; +use crate::runtime; use crate::runtime::futures::futures::StreamExt; use crate::runtime::futures::futures::channel::mpsc; use crate::runtime::futures::futures::stream; @@ -14,7 +15,7 @@ use crate::runtime::futures::{Executor, Runtime}; use crate::runtime::task; use crate::runtime::user_interface; use crate::runtime::window; -use crate::runtime::{Action, Task, UserInterface}; +use crate::runtime::{Task, UserInterface}; use crate::selector; use crate::{Instruction, Selector}; @@ -31,15 +32,22 @@ pub struct Emulator { cursor: mouse::Cursor, clipboard: Clipboard, cache: Option, + pending_tasks: usize, } #[allow(missing_debug_implementations)] pub enum Event { - Action(Action), + Action(Action

), Failed(Instruction), Ready, } +#[allow(missing_debug_implementations)] +pub enum Action { + Runtime(runtime::Action), + CountDown, +} + impl Emulator

{ pub fn new( sender: mpsc::Sender>, @@ -90,6 +98,7 @@ impl Emulator

{ cursor: mouse::Cursor::Unavailable, window: core::window::Id::unique(), cache: Some(user_interface::Cache::default()), + pending_tasks: 0, }; emulator.wait_for(task); @@ -101,103 +110,119 @@ impl Emulator

{ pub fn update(&mut self, program: &P, message: P::Message) { let task = program.update(&mut self.state, message); - if let Some(stream) = task::into_stream(task) { - self.runtime.run(stream.map(Event::Action).boxed()); + match self.mode { + Mode::Zen => self.wait_for(task), + _ => { + if let Some(stream) = task::into_stream(task) { + self.runtime.run( + stream.map(Action::Runtime).map(Event::Action).boxed(), + ); + } + } } self.resubscribe(program); } - pub fn perform(&mut self, program: &P, action: Action) { + pub fn perform(&mut self, program: &P, action: Action

) { match action { - Action::Output(message) => { - self.update(program, message); + Action::CountDown => { + self.pending_tasks -= 1; + + if self.pending_tasks == 0 { + self.runtime.send(Event::Ready); + } } - Action::LoadFont { .. } => { - // TODO - } - Action::Widget(operation) => { - let mut user_interface = UserInterface::build( - program.view(&self.state, self.window), - self.size, - self.cache.take().unwrap(), - &mut self.renderer, - ); + Action::Runtime(action) => match action { + runtime::Action::Output(message) => { + self.update(program, message); + } + runtime::Action::LoadFont { .. } => { + // TODO + } + runtime::Action::Widget(operation) => { + let mut user_interface = UserInterface::build( + program.view(&self.state, self.window), + self.size, + self.cache.take().unwrap(), + &mut self.renderer, + ); - let mut operation = Some(operation); + let mut operation = Some(operation); - while let Some(mut current) = operation.take() { - user_interface.operate(&self.renderer, &mut current); + while let Some(mut current) = operation.take() { + user_interface.operate(&self.renderer, &mut current); - match current.finish() { - widget::operation::Outcome::None => {} - widget::operation::Outcome::Some(()) => {} - widget::operation::Outcome::Chain(next) => { - operation = Some(next); + match current.finish() { + widget::operation::Outcome::None => {} + widget::operation::Outcome::Some(()) => {} + widget::operation::Outcome::Chain(next) => { + operation = Some(next); + } } } - } - self.cache = Some(user_interface.into_cache()); - } - Action::Clipboard(action) => { - // TODO - dbg!(action); - } - Action::Window(action) => match action { - window::Action::Open(id, _settings, sender) => { - self.window = id; + self.cache = Some(user_interface.into_cache()); + } + runtime::Action::Clipboard(action) => { + // TODO + dbg!(action); + } + runtime::Action::Window(action) => match action { + window::Action::Open(id, _settings, sender) => { + self.window = id; - let _ = sender.send(self.window); - } - window::Action::GetOldest(sender) - | window::Action::GetLatest(sender) => { - let _ = sender.send(Some(self.window)); - } - window::Action::GetSize(id, sender) => { - if id == self.window { - let _ = sender.send(self.size); + let _ = sender.send(self.window); } - } - window::Action::GetMaximized(id, sender) => { - if id == self.window { - let _ = sender.send(false); + window::Action::GetOldest(sender) + | window::Action::GetLatest(sender) => { + let _ = sender.send(Some(self.window)); } - } - window::Action::GetMinimized(id, sender) => { - if id == self.window { - let _ = sender.send(None); + window::Action::GetSize(id, sender) => { + if id == self.window { + let _ = sender.send(self.size); + } } - } - window::Action::GetPosition(id, sender) => { - if id == self.window { - let _ = sender.send(Some(Point::ORIGIN)); + window::Action::GetMaximized(id, sender) => { + if id == self.window { + let _ = sender.send(false); + } } - } - window::Action::GetScaleFactor(id, sender) => { - if id == self.window { - let _ = sender.send(1.0); + window::Action::GetMinimized(id, sender) => { + if id == self.window { + let _ = sender.send(None); + } } - } - window::Action::GetMode(id, sender) => { - if id == self.window { - let _ = sender.send(core::window::Mode::Windowed); + window::Action::GetPosition(id, sender) => { + if id == self.window { + let _ = sender.send(Some(Point::ORIGIN)); + } } + window::Action::GetScaleFactor(id, sender) => { + if id == self.window { + let _ = sender.send(1.0); + } + } + window::Action::GetMode(id, sender) => { + if id == self.window { + let _ = sender.send(core::window::Mode::Windowed); + } + } + _ => { + // Ignored + } + }, + runtime::Action::System(action) => { + // TODO + dbg!(action); } - _ => { - // Ignored + runtime::Action::Exit => { + // TODO + } + runtime::Action::Reload => { + // TODO } }, - Action::System(action) => { - // TODO - dbg!(action); - } - Action::Exit => { - // TODO - } - Action::Reload => { - // TODO - } } } @@ -295,16 +320,32 @@ impl Emulator

{ pub fn wait_for(&mut self, task: Task) { if let Some(stream) = task::into_stream(task) { match self.mode { + Mode::Zen => { + self.pending_tasks += 1; + + self.runtime.run( + stream + .map(Action::Runtime) + .map(Event::Action) + .chain(stream::once(async { + Event::Action(Action::CountDown) + })) + .boxed(), + ); + } Mode::Patient => { self.runtime.run( stream + .map(Action::Runtime) .map(Event::Action) .chain(stream::once(async { Event::Ready })) .boxed(), ); } Mode::Impatient => { - self.runtime.run(stream.map(Event::Action).boxed()); + self.runtime.run( + stream.map(Action::Runtime).map(Event::Action).boxed(), + ); self.runtime.send(Event::Ready); } } @@ -315,9 +356,9 @@ impl Emulator

{ pub fn resubscribe(&mut self, program: &P) { self.runtime.track(subscription::into_recipes( - program - .subscription(&self.state) - .map(|message| Event::Action(Action::Output(message))), + program.subscription(&self.state).map(|message| { + Event::Action(Action::Runtime(runtime::Action::Output(message))) + }), )); } @@ -340,20 +381,22 @@ impl Emulator

{ #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Mode { #[default] + Zen, Patient, Impatient, } impl Mode { - pub const ALL: &[Self] = &[Self::Patient, Self::Impatient]; + pub const ALL: &[Self] = &[Self::Zen, Self::Patient, Self::Impatient]; } impl fmt::Display for Mode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Mode::Patient => f.write_str("Patient"), - Mode::Impatient => f.write_str("Impatient"), - } + f.write_str(match self { + Self::Zen => "Zen", + Self::Patient => "Patient", + Self::Impatient => "Impatient", + }) } } diff --git a/test/src/ice.rs b/test/src/ice.rs index a8e44a93..ce0cd4a6 100644 --- a/test/src/ice.rs +++ b/test/src/ice.rs @@ -52,6 +52,7 @@ impl Ice { } "mode" => { mode = Some(match value.trim().to_lowercase().as_str() { + "zen" => emulator::Mode::Zen, "patient" => emulator::Mode::Patient, "impatient" => emulator::Mode::Impatient, _ => { @@ -118,6 +119,7 @@ impl std::fmt::Display for Ice { f, "mode: {}", match self.mode { + emulator::Mode::Zen => "Zen", emulator::Mode::Patient => "Patient", emulator::Mode::Impatient => "Impatient", } From 720ffa3e0b178e73dd67c0da08a66ab0b15a4b5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 28 Aug 2025 12:37:58 +0200 Subject: [PATCH 54/83] Fix `Zen` mode finishing early with `Task::none()` --- test/src/emulator.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/src/emulator.rs b/test/src/emulator.rs index 63845b91..3d0b1263 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -111,7 +111,7 @@ impl Emulator

{ let task = program.update(&mut self.state, message); match self.mode { - Mode::Zen => self.wait_for(task), + Mode::Zen if self.pending_tasks > 0 => self.wait_for(task), _ => { if let Some(stream) = task::into_stream(task) { self.runtime.run( @@ -127,10 +127,12 @@ impl Emulator

{ pub fn perform(&mut self, program: &P, action: Action

) { match action { Action::CountDown => { - self.pending_tasks -= 1; + if self.pending_tasks > 0 { + self.pending_tasks -= 1; - if self.pending_tasks == 0 { - self.runtime.send(Event::Ready); + if self.pending_tasks == 0 { + self.runtime.send(Event::Ready); + } } } Action::Runtime(action) => match action { @@ -349,7 +351,7 @@ impl Emulator

{ self.runtime.send(Event::Ready); } } - } else { + } else if self.pending_tasks == 0 { self.runtime.send(Event::Ready); } } From 2d83da799abddd7be3139b5c40ef8ba496d814aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 28 Aug 2025 13:10:35 +0200 Subject: [PATCH 55/83] Resubscribe before waiting for `Task` in `Emulator` --- test/src/emulator.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/src/emulator.rs b/test/src/emulator.rs index 3d0b1263..1c663bf8 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -101,8 +101,8 @@ impl Emulator

{ pending_tasks: 0, }; - emulator.wait_for(task); emulator.resubscribe(program); + emulator.wait_for(task); emulator } @@ -110,6 +110,8 @@ impl Emulator

{ pub fn update(&mut self, program: &P, message: P::Message) { let task = program.update(&mut self.state, message); + self.resubscribe(program); + match self.mode { Mode::Zen if self.pending_tasks > 0 => self.wait_for(task), _ => { @@ -120,8 +122,6 @@ impl Emulator

{ } } } - - self.resubscribe(program); } pub fn perform(&mut self, program: &P, action: Action

) { @@ -290,8 +290,8 @@ impl Emulator

{ program.update(&mut self.state, message) })); - self.wait_for(task); self.resubscribe(program); + self.wait_for(task); } Instruction::Expect(expectation) => match expectation { instruction::Expectation::Text(text) => { From a88e67105e47d4e281cea80ccb160e2fb7828628 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 28 Aug 2025 13:14:30 +0200 Subject: [PATCH 56/83] Use `Runtime::enter` in `Emulator` --- test/src/emulator.rs | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/test/src/emulator.rs b/test/src/emulator.rs index 1c663bf8..c77863ee 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -82,11 +82,13 @@ impl Emulator

{ let runtime = Runtime::new(executor, sender); - let (state, task) = if let Some(preset) = preset { - preset.boot() - } else { - program.boot() - }; + let (state, task) = runtime.enter(|| { + if let Some(preset) = preset { + preset.boot() + } else { + program.boot() + } + }); let mut emulator = Self { state, @@ -108,7 +110,9 @@ impl Emulator

{ } pub fn update(&mut self, program: &P, message: P::Message) { - let task = program.update(&mut self.state, message); + let task = self + .runtime + .enter(|| program.update(&mut self.state, message)); self.resubscribe(program); @@ -285,10 +289,11 @@ impl Emulator

{ self.cache = Some(user_interface.into_cache()); - let task = + let task = self.runtime.enter(|| { Task::batch(messages.into_iter().map(|message| { program.update(&mut self.state, message) - })); + })) + }); self.resubscribe(program); self.wait_for(task); @@ -357,11 +362,14 @@ impl Emulator

{ } pub fn resubscribe(&mut self, program: &P) { - self.runtime.track(subscription::into_recipes( - program.subscription(&self.state).map(|message| { - Event::Action(Action::Runtime(runtime::Action::Output(message))) - }), - )); + self.runtime + .track(subscription::into_recipes(self.runtime.enter(|| { + program.subscription(&self.state).map(|message| { + Event::Action(Action::Runtime(runtime::Action::Output( + message, + ))) + }) + }))); } pub fn view( From 4f7444bddfffac27dbd5e56d9748f698629fc7c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 29 Aug 2025 08:39:44 +0200 Subject: [PATCH 57/83] Move `tester` to a new `iced_tester` subcrate --- Cargo.lock | 13 +- Cargo.toml | 9 +- devtools/Cargo.toml | 9 +- devtools/src/comet.rs | 7 +- devtools/src/executor.rs | 43 ---- devtools/src/lib.rs | 170 ++++------------ devtools/src/tester/null.rs | 49 ----- devtools/src/time_machine.rs | 1 + devtools/src/widget.rs | 13 -- examples/todos/src/main.rs | 2 +- examples/todos/tests/carl_sagan.ice | 2 +- program/Cargo.toml | 1 + program/src/lib.rs | 46 +++-- program/src/message.rs | 33 ++++ runtime/src/task.rs | 45 +++++ src/application.rs | 40 ++-- src/application/timed.rs | 8 +- src/daemon.rs | 21 +- src/lib.rs | 4 +- test/src/lib.rs | 8 +- tester/Cargo.toml | 20 ++ .../fonts/iced_tester-icons.toml | 0 .../fonts/iced_tester-icons.ttf | Bin {devtools => tester}/src/icon.rs | 2 +- devtools/src/tester.rs => tester/src/lib.rs | 185 +++++++++++++----- .../src/tester => tester/src}/recorder.rs | 0 winit/src/lib.rs | 11 +- winit/src/proxy.rs | 5 +- 28 files changed, 392 insertions(+), 355 deletions(-) delete mode 100644 devtools/src/executor.rs delete mode 100644 devtools/src/tester/null.rs delete mode 100644 devtools/src/widget.rs create mode 100644 program/src/message.rs create mode 100644 tester/Cargo.toml rename devtools/fonts/iced_devtools-icons.toml => tester/fonts/iced_tester-icons.toml (100%) rename devtools/fonts/iced_devtools-icons.ttf => tester/fonts/iced_tester-icons.ttf (100%) rename {devtools => tester}/src/icon.rs (96%) rename devtools/src/tester.rs => tester/src/lib.rs (84%) rename {devtools/src/tester => tester/src}/recorder.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 786e115c..d455278b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2492,6 +2492,7 @@ dependencies = [ "iced_highlighter", "iced_renderer", "iced_runtime", + "iced_tester", "iced_wgpu", "iced_widget", "iced_winit", @@ -2548,10 +2549,8 @@ version = "0.14.0-dev" dependencies = [ "iced_debug", "iced_program", - "iced_test", "iced_widget", "log", - "rfd 0.15.4", ] [[package]] @@ -2650,6 +2649,16 @@ dependencies = [ "thiserror 2.0.16", ] +[[package]] +name = "iced_tester" +version = "0.14.0-dev" +dependencies = [ + "iced_test", + "iced_widget", + "log", + "rfd 0.15.4", +] + [[package]] name = "iced_tiny_skia" version = "0.14.0-dev" diff --git a/Cargo.toml b/Cargo.toml index 0a8ac2a6..a363b5c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,13 +42,13 @@ markdown = ["iced_widget/markdown"] # Enables lazy widgets lazy = ["iced_widget/lazy"] # Enables debug metrics in native platforms (press F12) -debug = ["iced_winit/debug", "iced_devtools"] +debug = ["iced_winit/debug", "dep:iced_devtools"] # Enables time-travel debugging (very experimental!) time-travel = ["debug", "iced_devtools/time-travel"] # Enables hot reloading (very experimental!) hot = ["debug", "iced_debug/hot"] # Enables the tester developer tool for recording and playing tests (press F12) -tester = ["debug", "iced_devtools/tester"] +tester = ["dep:iced_tester"] # Enables the `thread-pool` futures executor as the `executor::Default` on native platforms thread-pool = ["iced_futures/thread-pool"] # Enables `tokio` as the `executor::Default` on native platforms @@ -92,6 +92,9 @@ iced_winit.workspace = true iced_devtools.workspace = true iced_devtools.optional = true +iced_tester.workspace = true +iced_tester.optional = true + iced_highlighter.workspace = true iced_highlighter.optional = true @@ -133,6 +136,7 @@ members = [ "runtime", "selector", "test", + "tester", "tiny_skia", "wgpu", "widget", @@ -165,6 +169,7 @@ iced_renderer = { version = "0.14.0-dev", path = "renderer" } iced_runtime = { version = "0.14.0-dev", path = "runtime" } iced_selector = { version = "0.14.0-dev", path = "selector" } iced_test = { version = "0.14.0-dev", path = "test" } +iced_tester = { version = "0.14.0-dev", path = "tester" } iced_tiny_skia = { version = "0.14.0-dev", path = "tiny_skia" } iced_wgpu = { version = "0.14.0-dev", path = "wgpu" } iced_widget = { version = "0.14.0-dev", path = "widget" } diff --git a/devtools/Cargo.toml b/devtools/Cargo.toml index 8e50978b..f9923410 100644 --- a/devtools/Cargo.toml +++ b/devtools/Cargo.toml @@ -15,16 +15,11 @@ workspace = true [features] time-travel = ["iced_program/time-travel"] -tester = ["dep:iced_test", "dep:rfd"] [dependencies] iced_debug.workspace = true -iced_program.workspace = true iced_widget.workspace = true log.workspace = true -iced_test.workspace = true -iced_test.optional = true - -rfd.workspace = true -rfd.optional = true +iced_program.workspace = true +iced_program.features = ["debug"] diff --git a/devtools/src/comet.rs b/devtools/src/comet.rs index d5ba0802..d6975c26 100644 --- a/devtools/src/comet.rs +++ b/devtools/src/comet.rs @@ -1,5 +1,4 @@ -use crate::executor; -use crate::runtime::Task; +use crate::runtime::task::{self, Task}; use std::process; @@ -7,7 +6,7 @@ pub const COMPATIBLE_REVISION: &str = "20f9c9a897fecac5dce0977bbb5639fdce1f54b9"; pub fn launch() -> Task { - executor::try_spawn_blocking(|mut sender| { + task::try_blocking(|mut sender| { let cargo_install = process::Command::new("cargo") .args(["install", "--list"]) .output()?; @@ -48,7 +47,7 @@ pub fn launch() -> Task { } pub fn install() -> Task { - executor::try_spawn_blocking(|mut sender| { + task::try_blocking(|mut sender| { use std::io::{BufRead, BufReader}; use std::process::{Command, Stdio}; diff --git a/devtools/src/executor.rs b/devtools/src/executor.rs deleted file mode 100644 index 1e7317a2..00000000 --- a/devtools/src/executor.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::futures::futures::channel::mpsc; -use crate::futures::futures::channel::oneshot; -use crate::futures::futures::stream::{self, StreamExt}; -use crate::runtime::Task; - -use std::thread; - -pub fn spawn_blocking( - f: impl FnOnce(mpsc::Sender) + Send + 'static, -) -> Task -where - T: Send + 'static, -{ - let (sender, receiver) = mpsc::channel(1); - - let _ = thread::spawn(move || { - f(sender); - }); - - Task::stream(receiver) -} - -pub fn try_spawn_blocking( - f: impl FnOnce(mpsc::Sender) -> Result<(), E> + Send + 'static, -) -> Task> -where - T: Send + 'static, - E: Send + 'static, -{ - let (sender, receiver) = mpsc::channel(1); - let (error_sender, error_receiver) = oneshot::channel(); - - let _ = thread::spawn(move || { - if let Err(error) = f(sender) { - let _ = error_sender.send(Err(error)); - } - }); - - Task::stream(stream::select( - receiver.map(Ok), - stream::once(error_receiver).filter_map(async |result| result.ok()), - )) -} diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index 18208a7a..b859341c 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -3,39 +3,28 @@ use iced_debug as debug; use iced_program as program; use iced_program::runtime; use iced_program::runtime::futures; -#[cfg(feature = "tester")] -use iced_test as test; +use iced_widget as widget; use iced_widget::core; mod comet; -mod executor; -mod icon; mod time_machine; -mod widget; - -#[cfg(feature = "tester")] -mod tester; - -#[cfg(not(feature = "tester"))] -#[path = "tester/null.rs"] -mod tester; - -use crate::tester::Tester; use crate::core::border; use crate::core::keyboard; use crate::core::theme::{self, Base, Theme}; use crate::core::time::seconds; use crate::core::window; -use crate::core::{Alignment::Center, Color, Element, Length::Fill}; +use crate::core::{ + Alignment::Center, Color, Element, Font, Length::Fill, Settings, +}; use crate::futures::Subscription; use crate::program::Program; -use crate::runtime::Task; -use crate::runtime::font; +use crate::program::message; +use crate::runtime::task::{self, Task}; use crate::time_machine::TimeMachine; use crate::widget::{ - bottom_right, button, center, column, container, horizontal_space, - monospace, opaque, row, scrollable, stack, text, themer, + bottom_right, button, center, column, container, horizontal_space, opaque, + row, scrollable, stack, text, themer, }; use std::fmt; @@ -55,6 +44,7 @@ pub struct Attach

{ impl

Program for Attach

where P: Program + 'static, + P::Message: std::fmt::Debug + message::MaybeClone, { type State = DevTools

; type Message = Event

; @@ -66,21 +56,21 @@ where P::name() } - fn settings(&self) -> core::Settings { + fn settings(&self) -> Settings { self.program.settings() } + fn window(&self) -> Option { + self.program.window() + } + fn boot(&self) -> (Self::State, Task) { let (state, boot) = self.program.boot(); let (devtools, task) = DevTools::new(state); ( devtools, - Task::batch([ - boot.map(Event::Program), - task.map(Event::Message), - font::load(icon::FONT).discard(), - ]), + Task::batch([boot.map(Event::Program), task.map(Event::Message)]), ) } @@ -130,7 +120,7 @@ where state: P::State, show_notification: bool, time_machine: TimeMachine

, - mode: Mode

, + mode: Mode, } #[derive(Debug, Clone)] @@ -141,13 +131,10 @@ pub enum Message { InstallComet, Installing(comet::install::Result), CancelSetup, - Toggle, - Tester(tester::Message), } -enum Mode { +enum Mode { Hidden, - Open { tester: Tester

}, Setup(Setup), } @@ -164,6 +151,7 @@ enum Goal { impl

DevTools

where P: Program + 'static, + P::Message: std::fmt::Debug + message::MaybeClone, { pub fn new(state: P::State) -> (Self, Task) { ( @@ -173,7 +161,7 @@ where show_notification: true, time_machine: TimeMachine::new(), }, - executor::spawn_blocking(|mut sender| { + task::blocking(|mut sender| { thread::sleep(seconds(2)); let _ = sender.try_send(()); }) @@ -193,21 +181,6 @@ where Task::none() } - Message::Toggle => { - match &self.mode { - Mode::Hidden => { - self.mode = Mode::Open { - tester: Tester::new(program), - }; - } - Mode::Open { tester } if !tester.is_busy() => { - self.mode = Mode::Hidden; - } - Mode::Setup(_) | Mode::Open { .. } => {} - } - - Task::none() - } Message::ToggleComet => { if let Mode::Setup(setup) = &self.mode { if matches!(setup, Setup::Idle { .. }) { @@ -290,13 +263,6 @@ where Task::none() } - Message::Tester(message) => { - let Mode::Open { tester } = &mut self.mode else { - return Task::none(); - }; - - tester.update(program, message).map(Event::Tester) - } }, Event::Program(message) => { self.time_machine.push(&message); @@ -328,13 +294,6 @@ where Task::none() } - Event::Tester(tick) => { - let Mode::Open { tester } = &mut self.mode else { - return Task::none(); - }; - - tester.tick(program, tick).map(Event::Tester) - } Event::Discard => Task::none(), } } @@ -347,23 +306,15 @@ where let state = self.state(); let view = { - let view = || { - let theme = program.theme(state, window); - let view: Element<'_, _, Theme, _> = - themer(theme, program.view(&self.state, window)).into(); + let theme = program.theme(state, window); - if self.time_machine.is_rewinding() { - view.map(|_| Event::Discard) - } else { - view.map(Event::Program) - } - }; + let view: Element<'_, _, Theme, _> = + themer(theme, program.view(state, window)).into(); - match &self.mode { - Mode::Open { tester } => { - tester.view(program, view, Event::Tester) - } - _ => view(), + if self.time_machine.is_rewinding() { + view.map(|_| Event::Discard) + } else { + view.map(Event::Program) } }; @@ -408,28 +359,9 @@ where }) }); - let sidebar = if let Mode::Open { tester } = &self.mode { - let title = monospace("Developer Tools"); - let tester = tester.controls().map(Message::Tester); - - let tools = column![title, tester].spacing(10); - - let sidebar = container(tools) - .padding(10) - .width(250) - .height(Fill) - .style(container::dark); - - Some(Element::from(sidebar).map(Event::Message)) - } else { - None - }; - - let content = row![view, sidebar]; - themer( theme, - stack![content] + stack![view] .height(Fill) .push_maybe(setup.map(opaque)) .push_maybe(notification.map(|notification| { @@ -451,14 +383,6 @@ where let hotkeys = futures::keyboard::on_key_press(|key, _modifiers| match key { keyboard::Key::Named(keyboard::key::Named::F12) => { - Some(if cfg!(feature = "tester") { - Message::Toggle - } else { - Message::ToggleComet - }) - } - #[cfg(feature = "tester")] - keyboard::Key::Named(keyboard::key::Named::F11) => { Some(Message::ToggleComet) } _ => None, @@ -479,11 +403,7 @@ where } pub fn scale_factor(&self, program: &P, window: window::Id) -> f64 { - if let Mode::Open { .. } = &self.mode { - 1.0 - } else { - program.scale_factor(self.state(), window) - } + program.scale_factor(self.state(), window) } pub fn state(&self) -> &P::State { @@ -497,7 +417,6 @@ where { Message(Message), Program(P::Message), - Tester(tester::Tick

), Command(debug::Command), Discard, } @@ -505,34 +424,18 @@ where impl

fmt::Debug for Event

where P: Program, + P::Message: std::fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Message(message) => message.fmt(f), Self::Program(message) => message.fmt(f), - Self::Tester(_) => f.write_str("Tester"), Self::Command(command) => command.fmt(f), Self::Discard => f.write_str("Discard"), } } } -#[cfg(feature = "time-travel")] -impl

Clone for Event

-where - P: Program, -{ - fn clone(&self) -> Self { - match self { - Self::Message(message) => Self::Message(message.clone()), - Self::Program(message) => Self::Program(message.clone()), - Self::Command(command) => Self::Command(*command), - Self::Tester(_) => Self::Discard, // Time traveling an emulator?! - Self::Discard => Self::Discard, - } - } -} - fn setup(goal: &Goal) -> Element<'_, Message, Theme, Renderer> where Renderer: program::Renderer + 'static, @@ -557,13 +460,14 @@ where ]; let command = container( - monospace(format!( + text!( "cargo install --locked \\ --git https://github.com/iced-rs/comet.git \\ --rev {}", comet::COMPATIBLE_REVISION - )) - .size(14), + ) + .size(14) + .font(Font::MONOSPACE), ) .width(Fill) .padding(5) @@ -630,9 +534,9 @@ where text("Installing comet...").size(20), container( scrollable( - column( - logs.iter().map(|log| { monospace(log).size(12).into() }), - ) + column(logs.iter().map(|log| { + text(log).size(12).font(Font::MONOSPACE).into() + })) .spacing(3), ) .spacing(10) @@ -653,7 +557,7 @@ fn inline_code<'a, Renderer>( where Renderer: program::Renderer + 'a, { - container(monospace(code).size(12)) + container(text(code).size(12).font(Font::MONOSPACE)) .style(|_theme| { container::Style::default() .background(Color::BLACK) diff --git a/devtools/src/tester/null.rs b/devtools/src/tester/null.rs deleted file mode 100644 index 96b1dbfa..00000000 --- a/devtools/src/tester/null.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::Program; -use crate::core::{Element, Theme}; -use crate::runtime::Task; -use crate::widget::horizontal_space; - -use std::marker::PhantomData; - -pub struct Tester { - _type: PhantomData, -} - -#[derive(Debug, Clone)] -pub enum Message {} - -#[allow(missing_debug_implementations)] -pub struct Tick { - _type: PhantomData, -} - -impl Tester

{ - pub fn new(_program: &P) -> Self { - Self { _type: PhantomData } - } - - pub fn is_busy(&self) -> bool { - false - } - - pub fn update(&mut self, _program: &P, _message: Message) -> Task> { - Task::none() - } - - pub fn tick(&mut self, _program: &P, _tick: Tick

) -> Task> { - Task::none() - } - - pub fn view<'a, T: 'static>( - &'a self, - _program: &P, - _current: impl FnOnce() -> Element<'a, T, Theme, P::Renderer>, - _emulate: impl Fn(Tick

) -> T + 'a, - ) -> Element<'a, T, Theme, P::Renderer> { - horizontal_space().into() - } - - pub fn controls(&self) -> Element<'_, Message, Theme, P::Renderer> { - horizontal_space().into() - } -} diff --git a/devtools/src/time_machine.rs b/devtools/src/time_machine.rs index f999c70e..3c8c18a7 100644 --- a/devtools/src/time_machine.rs +++ b/devtools/src/time_machine.rs @@ -13,6 +13,7 @@ where impl

TimeMachine

where P: Program, + P::Message: std::fmt::Debug + Clone, { pub fn new() -> Self { Self { diff --git a/devtools/src/widget.rs b/devtools/src/widget.rs deleted file mode 100644 index ef247508..00000000 --- a/devtools/src/widget.rs +++ /dev/null @@ -1,13 +0,0 @@ -pub use iced_widget::*; - -use crate::core::Font; -use crate::program; - -pub fn monospace<'a, Renderer>( - fragment: impl text::IntoFragment<'a>, -) -> Text<'a, Theme, Renderer> -where - Renderer: program::Renderer + 'a, -{ - text(fragment).font(Font::MONOSPACE) -} diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 0f7c1009..2113c93d 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -19,7 +19,7 @@ pub fn main() -> iced::Result { application().run() } -fn application() -> Application { +fn application() -> Application> { iced::application(Todos::new, Todos::update, Todos::view) .subscription(Todos::subscription) .title(Todos::title) diff --git a/examples/todos/tests/carl_sagan.ice b/examples/todos/tests/carl_sagan.ice index cfad7a60..cb9b91ed 100644 --- a/examples/todos/tests/carl_sagan.ice +++ b/examples/todos/tests/carl_sagan.ice @@ -1,4 +1,4 @@ -viewport: 512x768 +viewport: 500x800 mode: Impatient preset: Empty ----- diff --git a/program/Cargo.toml b/program/Cargo.toml index 7aa6414d..76106c25 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -14,6 +14,7 @@ rust-version.workspace = true workspace = true [features] +debug = [] time-travel = [] [dependencies] diff --git a/program/src/lib.rs b/program/src/lib.rs index c20afdfd..0e473c3a 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -4,6 +4,8 @@ pub use iced_runtime as runtime; pub use iced_runtime::core; pub use iced_runtime::futures; +pub mod message; + mod preset; pub use preset::Preset; @@ -13,7 +15,7 @@ use crate::core::text; use crate::core::theme; use crate::core::window; use crate::core::{Element, Font, Settings}; -use crate::futures::{Executor, Subscription}; +use crate::futures::{Executor, MaybeSend, Subscription}; use crate::graphics::compositor; use crate::runtime::Task; @@ -27,7 +29,7 @@ pub trait Program: Sized { type State; /// The message of the program. - type Message: Message + 'static; + type Message: MaybeSend + 'static; /// The theme of the program. type Theme: Default + theme::Base; @@ -43,6 +45,8 @@ pub trait Program: Sized { fn settings(&self) -> Settings; + fn window(&self) -> Option; + fn boot(&self) -> (Self::State, Task); fn update( @@ -143,6 +147,10 @@ pub fn with_title( self.program.settings() } + fn window(&self) -> Option { + self.program.window() + } + fn boot(&self) -> (Self::State, Task) { self.program.boot() } @@ -229,6 +237,10 @@ pub fn with_subscription( self.program.settings() } + fn window(&self) -> Option { + self.program.window() + } + fn boot(&self) -> (Self::State, Task) { self.program.boot() } @@ -316,6 +328,10 @@ pub fn with_theme( self.program.settings() } + fn window(&self) -> Option { + self.program.window() + } + fn boot(&self) -> (Self::State, Task) { self.program.boot() } @@ -399,6 +415,10 @@ pub fn with_style( self.program.settings() } + fn window(&self) -> Option { + self.program.window() + } + fn boot(&self) -> (Self::State, Task) { self.program.boot() } @@ -478,6 +498,10 @@ pub fn with_scale_factor( self.program.settings() } + fn window(&self) -> Option { + self.program.window() + } + fn boot(&self) -> (Self::State, Task) { self.program.boot() } @@ -565,6 +589,10 @@ pub fn with_executor( self.program.settings() } + fn window(&self) -> Option { + self.program.window() + } + fn boot(&self) -> (Self::State, Task) { self.program.boot() } @@ -683,17 +711,3 @@ impl Instance

{ self.program.scale_factor(&self.state, window) } } - -/// A trait alias for the [`Message`](Program::Message) of a [`Program`]. -#[cfg(feature = "time-travel")] -pub trait Message: Send + std::fmt::Debug + Clone {} - -#[cfg(feature = "time-travel")] -impl Message for T {} - -/// A trait alias for the [`Message`](Program::Message) of a [`Program`]. -#[cfg(not(feature = "time-travel"))] -pub trait Message: Send + std::fmt::Debug {} - -#[cfg(not(feature = "time-travel"))] -impl Message for T {} diff --git a/program/src/message.rs b/program/src/message.rs new file mode 100644 index 00000000..15dfafed --- /dev/null +++ b/program/src/message.rs @@ -0,0 +1,33 @@ +//! Traits for the message type of a [`Program`](crate::Program). + +/// A trait alias for [`Clone`], but only when the `time-travel` +/// feature is enabled. +#[cfg(feature = "time-travel")] +pub trait MaybeClone: Clone {} + +#[cfg(feature = "time-travel")] +impl MaybeClone for T where T: Clone {} + +/// A trait alias for [`Clone`], but only when the `time-travel` +/// feature is enabled. +#[cfg(not(feature = "time-travel"))] +pub trait MaybeClone {} + +#[cfg(not(feature = "time-travel"))] +impl MaybeClone for T {} + +/// A trait alias for [`Debug`](std::fmt::Debug), but only when the +/// `debug` feature is enabled. +#[cfg(feature = "debug")] +pub trait MaybeDebug: std::fmt::Debug {} + +#[cfg(feature = "debug")] +impl MaybeDebug for T where T: std::fmt::Debug {} + +/// A trait alias for [`Debug`](std::fmt::Debug), but only when the +/// `debug` feature is enabled. +#[cfg(not(feature = "debug"))] +pub trait MaybeDebug {} + +#[cfg(not(feature = "debug"))] +impl MaybeDebug for T {} diff --git a/runtime/src/task.rs b/runtime/src/task.rs index 34ee1df7..7ac9befe 100644 --- a/runtime/src/task.rs +++ b/runtime/src/task.rs @@ -9,6 +9,7 @@ use crate::futures::{BoxStream, MaybeSend, boxed_stream}; use std::convert::Infallible; use std::sync::Arc; +use std::thread; #[cfg(feature = "sipper")] #[doc(no_inline)] @@ -466,3 +467,47 @@ pub fn effect(action: impl Into>) -> Task { pub fn into_stream(task: Task) -> Option>> { task.stream } + +/// Creates a new [`Task`] that will run the given closure in a new thread. +/// +/// Any data sent by the closure through the [`mpsc::Sender`] will be produced +/// by the [`Task`]. +pub fn blocking(f: impl FnOnce(mpsc::Sender) + Send + 'static) -> Task +where + T: Send + 'static, +{ + let (sender, receiver) = mpsc::channel(1); + + let _ = thread::spawn(move || { + f(sender); + }); + + Task::stream(receiver) +} + +/// Creates a new [`Task`] that will run the given closure that can fail in a new +/// thread. +/// +/// Any data sent by the closure through the [`mpsc::Sender`] will be produced +/// by the [`Task`]. +pub fn try_blocking( + f: impl FnOnce(mpsc::Sender) -> Result<(), E> + Send + 'static, +) -> Task> +where + T: Send + 'static, + E: Send + 'static, +{ + let (sender, receiver) = mpsc::channel(1); + let (error_sender, error_receiver) = oneshot::channel(); + + let _ = thread::spawn(move || { + if let Err(error) = f(sender) { + let _ = error_sender.send(Err(error)); + } + }); + + Task::stream(stream::select( + receiver.map(Ok), + stream::once(error_receiver).filter_map(async |result| result.ok()), + )) +} diff --git a/src/application.rs b/src/application.rs index 1792502f..38d95f79 100644 --- a/src/application.rs +++ b/src/application.rs @@ -30,12 +30,14 @@ //! ] //! } //! ``` +use crate::message; use crate::program::{self, Program}; use crate::shell; use crate::theme; use crate::window; use crate::{ - Element, Executor, Font, Preset, Result, Settings, Size, Subscription, Task, + Element, Executor, Font, MaybeSend, Preset, Result, Settings, Size, + Subscription, Task, }; use iced_debug as debug; @@ -81,7 +83,7 @@ pub fn application( ) -> Application> where State: 'static, - Message: program::Message + 'static, + Message: MaybeSend + 'static, Theme: Default + theme::Base, Renderer: program::Renderer, { @@ -100,7 +102,7 @@ where impl Program for Instance where - Message: program::Message + 'static, + Message: MaybeSend + 'static, Theme: Default + theme::Base, Renderer: program::Renderer, Boot: self::Boot, @@ -142,6 +144,10 @@ where fn settings(&self) -> Settings { Settings::default() } + + fn window(&self) -> Option { + Some(window::Settings::default()) + } } Application { @@ -180,25 +186,25 @@ impl Application

{ pub fn run(self) -> Result where Self: 'static, + P::Message: message::MaybeDebug + message::MaybeClone, { - let settings = self.settings.clone(); - let window = self.window.clone(); + #[cfg(feature = "debug")] + iced_debug::init(iced_debug::Metadata { + name: P::name(), + theme: None, + can_time_travel: cfg!(feature = "time-travel"), + }); - #[cfg(all(feature = "debug", not(target_arch = "wasm32")))] - let program = { - iced_debug::init(iced_debug::Metadata { - name: P::name(), - theme: None, - can_time_travel: cfg!(feature = "time-travel"), - }); + #[cfg(feature = "tester")] + let program = iced_tester::attach(self); - iced_devtools::attach(self) - }; + #[cfg(all(feature = "debug", not(feature = "tester")))] + let program = iced_devtools::attach(self); #[cfg(any(not(feature = "debug"), target_arch = "wasm32"))] let program = self; - Ok(shell::run(program, settings, Some(window))?) + Ok(shell::run(program)?) } /// Sets the [`Settings`] that will be used to run the [`Application`]. @@ -456,6 +462,10 @@ impl Program for Application

{ self.settings.clone() } + fn window(&self) -> Option { + Some(self.window.clone()) + } + fn boot(&self) -> (Self::State, Task) { self.raw.boot() } diff --git a/src/application/timed.rs b/src/application/timed.rs index 02574b3a..ade98a18 100644 --- a/src/application/timed.rs +++ b/src/application/timed.rs @@ -29,7 +29,7 @@ pub fn timed( > where State: 'static, - Message: program::Message + 'static, + Message: Send + 'static, Theme: Default + theme::Base + 'static, Renderer: program::Renderer + 'static, { @@ -68,7 +68,7 @@ where View, > where - Message: program::Message + 'static, + Message: Send + 'static, Theme: Default + theme::Base + 'static, Renderer: program::Renderer + 'static, Boot: self::Boot, @@ -92,6 +92,10 @@ where Settings::default() } + fn window(&self) -> Option { + Some(window::Settings::default()) + } + fn boot(&self) -> (State, Task) { let (state, task) = self.boot.boot(); diff --git a/src/daemon.rs b/src/daemon.rs index a961eed5..c848d8bd 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,11 +1,13 @@ //! Create and run daemons that run in the background. use crate::application; +use crate::message; use crate::program::{self, Program}; use crate::shell; use crate::theme; use crate::window; use crate::{ - Element, Executor, Font, Preset, Result, Settings, Subscription, Task, + Element, Executor, Font, MaybeSend, Preset, Result, Settings, Subscription, + Task, }; use iced_debug as debug; @@ -29,7 +31,7 @@ pub fn daemon( ) -> Daemon> where State: 'static, - Message: program::Message + 'static, + Message: MaybeSend + 'static, Theme: Default + theme::Base, Renderer: program::Renderer, { @@ -48,7 +50,7 @@ where impl Program for Instance where - Message: program::Message + 'static, + Message: MaybeSend + 'static, Theme: Default + theme::Base, Renderer: program::Renderer, Boot: application::Boot, @@ -71,6 +73,10 @@ where Settings::default() } + fn window(&self) -> Option { + None + } + fn boot(&self) -> (Self::State, Task) { self.boot.boot() } @@ -126,9 +132,8 @@ impl Daemon

{ pub fn run(self) -> Result where Self: 'static, + P::Message: message::MaybeDebug + message::MaybeClone, { - let settings = self.settings.clone(); - #[cfg(all(feature = "debug", not(target_arch = "wasm32")))] let program = { iced_debug::init(iced_debug::Metadata { @@ -143,7 +148,7 @@ impl Daemon

{ #[cfg(any(not(feature = "debug"), target_arch = "wasm32"))] let program = self; - Ok(shell::run(program, settings, None)?) + Ok(shell::run(program)?) } /// Sets the [`Settings`] that will be used to run the [`Daemon`]. @@ -298,6 +303,10 @@ impl Program for Daemon

{ self.settings.clone() } + fn window(&self) -> Option { + None + } + fn boot(&self) -> (Self::State, Task) { self.raw.boot() } diff --git a/src/lib.rs b/src/lib.rs index 29dffb1a..e4cc8807 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -526,7 +526,9 @@ pub use crate::core::{ Rotation, Settings, Shadow, Size, Theme, Transformation, Vector, never, }; pub use crate::program::Preset; +pub use crate::program::message; pub use crate::runtime::exit; +pub use crate::runtime::futures::MaybeSend; pub use iced_futures::Subscription; pub use Alignment::Center; @@ -696,7 +698,7 @@ pub fn run( ) -> Result where State: Default + 'static, - Message: program::Message + 'static, + Message: MaybeSend + message::MaybeDebug + message::MaybeClone + 'static, Theme: Default + theme::Base + 'static, Renderer: program::Renderer + 'static, { diff --git a/test/src/lib.rs b/test/src/lib.rs index 5f4f61dd..1cf8d81f 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -84,10 +84,10 @@ //! //! [the classical counter interface]: https://book.iced.rs/architecture.html#dissecting-an-interface #![allow(missing_docs)] -use iced_program as program; -use iced_renderer as renderer; -use iced_runtime as runtime; -use iced_runtime::core; +pub use iced_program as program; +pub use iced_renderer as renderer; +pub use iced_runtime as runtime; +pub use iced_runtime::core; pub use iced_selector as selector; diff --git a/tester/Cargo.toml b/tester/Cargo.toml new file mode 100644 index 00000000..4f97bac3 --- /dev/null +++ b/tester/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "iced_tester" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +categories.workspace = true +keywords.workspace = true +rust-version.workspace = true + +[dependencies] +iced_test.workspace = true +iced_widget.workspace = true +log.workspace = true +rfd.workspace = true + +[lints] +workspace = true diff --git a/devtools/fonts/iced_devtools-icons.toml b/tester/fonts/iced_tester-icons.toml similarity index 100% rename from devtools/fonts/iced_devtools-icons.toml rename to tester/fonts/iced_tester-icons.toml diff --git a/devtools/fonts/iced_devtools-icons.ttf b/tester/fonts/iced_tester-icons.ttf similarity index 100% rename from devtools/fonts/iced_devtools-icons.ttf rename to tester/fonts/iced_tester-icons.ttf diff --git a/devtools/src/icon.rs b/tester/src/icon.rs similarity index 96% rename from devtools/src/icon.rs rename to tester/src/icon.rs index 811b1d23..343f942f 100644 --- a/devtools/src/icon.rs +++ b/tester/src/icon.rs @@ -3,7 +3,7 @@ use crate::core::Font; use crate::program; use crate::widget::{Text, text}; -pub const FONT: &[u8] = include_bytes!("../fonts/iced_devtools-icons.ttf"); +pub const FONT: &[u8] = include_bytes!("../fonts/iced_tester-icons.ttf"); pub fn cancel<'a, Theme, Renderer>() -> Text<'a, Theme, Renderer> where diff --git a/devtools/src/tester.rs b/tester/src/lib.rs similarity index 84% rename from devtools/src/tester.rs rename to tester/src/lib.rs index b177d0cc..8e7593d6 100644 --- a/devtools/src/tester.rs +++ b/tester/src/lib.rs @@ -1,29 +1,97 @@ +//! Record, edit, and run end-to-end tests for your iced applications. +#![allow(missing_docs)] +pub use iced_test as test; +pub use iced_test::core; +pub use iced_test::program; +pub use iced_test::runtime; +pub use iced_test::runtime::futures; +pub use iced_widget as widget; + +mod icon; mod recorder; use recorder::recorder; -use crate::Program; use crate::core::Alignment::Center; use crate::core::Length::Fill; use crate::core::alignment::Horizontal::Right; use crate::core::border; use crate::core::mouse; use crate::core::window; -use crate::core::{Element, Font, Size, Theme}; -use crate::executor; +use crate::core::{Element, Font, Settings, Size, Theme}; use crate::futures::futures::channel::mpsc; -use crate::icon; -use crate::program; -use crate::runtime::Task; +use crate::program::Program; +use crate::runtime::task::{self, Task}; use crate::test::emulator; use crate::test::ice; use crate::test::instruction; use crate::test::{Emulator, Ice, Instruction}; use crate::widget::{ - button, center, column, combo_box, container, horizontal_space, monospace, - pick_list, row, scrollable, text, text_editor, text_input, themer, + button, center, column, combo_box, container, horizontal_space, pick_list, + row, scrollable, text, text_editor, text_input, themer, }; +/// Attaches a [`Tester`] to the given [`Program`]. +pub fn attach(program: P) -> Attach

{ + Attach { program } +} + +/// A [`Program`] with a [`Tester`] attached to it. +#[derive(Debug)] +pub struct Attach

{ + /// The original [`Program`] attatched to the [`Tester`]. + pub program: P, +} + +impl

Program for Attach

+where + P: Program + 'static, +{ + type State = Tester

; + type Message = Tick

; + type Theme = Theme; + type Renderer = P::Renderer; + type Executor = P::Executor; + + fn name() -> &'static str { + P::name() + } + + fn settings(&self) -> Settings { + let mut settings = self.program.settings(); + settings.fonts.push(icon::FONT.into()); + settings + } + + fn window(&self) -> Option { + self.program.window().map(|window| window::Settings { + size: window.size + Size::new(300.0, 80.0), + ..window + }) + } + + fn boot(&self) -> (Self::State, Task) { + (Tester::new(&self.program), Task::none()) + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Task { + state.tick(&self.program, message) + } + + fn view<'a>( + &self, + state: &'a Self::State, + window: window::Id, + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + state.view(&self.program, window) + } +} + +#[allow(missing_debug_implementations)] pub struct Tester { viewport: Size, mode: emulator::Mode, @@ -35,7 +103,10 @@ pub struct Tester { } enum State { - Idle, + Empty, + Idle { + state: P::State, + }, Recording { emulator: Emulator

, }, @@ -84,9 +155,12 @@ pub enum Tick { impl Tester

{ pub fn new(program: &P) -> Self { + let (state, _) = program.boot(); + let window = program.window().unwrap_or_default(); + Self { mode: emulator::Mode::default(), - viewport: window::Settings::default().size, + viewport: window.size, presets: combo_box::State::new( program .presets() @@ -97,7 +171,7 @@ impl Tester

{ ), preset: None, instructions: Vec::new(), - state: State::Idle, + state: State::Idle { state }, edit: None, } } @@ -150,7 +224,7 @@ impl Tester

{ } Message::Stop => { let State::Recording { emulator } = - std::mem::replace(&mut self.state, State::Idle) + std::mem::replace(&mut self.state, State::Empty) else { return Task::none(); }; @@ -204,7 +278,7 @@ impl Tester

{ Task::future(import) .and_then(|file| { - executor::spawn_blocking(move |mut sender| { + task::blocking(move |mut sender| { let _ = sender.try_send(Ice::parse( &fs::read_to_string(file.path()) .unwrap_or_default(), @@ -248,7 +322,13 @@ impl Tester

{ self.preset = ice.preset; self.instructions = ice.instructions; self.edit = None; - self.state = State::Idle; + + let (state, _) = self + .preset(program) + .map(program::Preset::boot) + .unwrap_or_else(|| program.boot()); + + self.state = State::Idle { state }; Task::none() } @@ -358,7 +438,9 @@ impl Tester

{ } } }, - State::Idle | State::Asserting { .. } => {} + State::Empty + | State::Idle { .. } + | State::Asserting { .. } => {} } Task::none() @@ -432,15 +514,14 @@ impl Tester

{ } } - pub fn view<'a, T: 'static>( + pub fn view<'a>( &'a self, program: &P, - current: impl FnOnce() -> Element<'a, T, Theme, P::Renderer>, - emulate: impl Fn(Tick

) -> T + 'a, - ) -> Element<'a, T, Theme, P::Renderer> { + window: window::Id, + ) -> Element<'a, Tick

, Theme, P::Renderer> { let status = { let (icon, label) = match &self.state { - State::Idle => (text(""), "Idle"), + State::Empty | State::Idle { .. } => (text(""), "Idle"), State::Recording { .. } => (icon::record(), "Recording"), State::Asserting { .. } => (icon::lightbulb(), "Asserting"), State::Playing { outcome, .. } => match outcome { @@ -456,7 +537,9 @@ impl Tester

{ container::Style { text_color: Some(match &self.state { - State::Idle => palette.background.strongest.color, + State::Empty | State::Idle { .. } => { + palette.background.strongest.color + } State::Recording { .. } => { palette.danger.base.color } @@ -483,33 +566,34 @@ impl Tester

{ let viewport = container( scrollable( container(match &self.state { - State::Idle => current(), + State::Empty => horizontal_space().into(), + State::Idle { state } => Element::from(themer( + program.theme(state, window), + program.view(state, window), + )) + .map(Tick::Program), State::Recording { emulator } => { let theme = emulator.theme(program); let view = emulator.view(program).map(Tick::Program); - Element::from( - recorder(themer(theme, view)) - .on_record(Tick::Record), - ) - .map(emulate) + recorder(themer(theme, view)) + .on_record(Tick::Record) + .into() } State::Asserting { state, window, .. } => { let theme = program.theme(state, *window); let view = program.view(state, *window).map(Tick::Program); - Element::from( - recorder(themer(theme, view)) - .on_record(Tick::Assert), - ) - .map(emulate) + recorder(themer(theme, view)) + .on_record(Tick::Assert) + .into() } State::Playing { emulator, .. } => { let theme = emulator.theme(program); let view = emulator.view(program).map(Tick::Program); - Element::from(themer(theme, view)).map(emulate) + themer(theme, view).into() } }) .width(self.viewport.width) @@ -525,7 +609,9 @@ impl Tester

{ container::Style { border: border::width(2.0).color(match &self.state { - State::Idle => palette.background.strongest.color, + State::Empty | State::Idle { .. } => { + palette.background.strongest.color + } State::Recording { .. } => palette.danger.base.color, State::Asserting { .. } => palette.warning.weak.color, State::Playing { outcome, .. } => match outcome { @@ -539,9 +625,15 @@ impl Tester

{ }) .padding(10); - center(column![status, viewport].spacing(10).align_x(Right)) - .padding(10) - .into() + row![ + center(column![status, viewport].spacing(10).align_x(Right)) + .padding(10), + container(self.controls().map(Tick::Tester)) + .width(250) + .padding(10) + .style(container::dark) + ] + .into() } pub fn controls(&self) -> Element<'_, Message, Theme, P::Renderer> { @@ -552,7 +644,7 @@ impl Tester

{ width: width.parse().unwrap_or(self.viewport.width), ..self.viewport })), - monospace("x").size(14), + text("x").size(14).font(Font::MONOSPACE), text_input("Height", &self.viewport.height.to_string()) .size(14) .on_input(|height| Message::ChangeViewport(Size { @@ -590,8 +682,9 @@ impl Tester

{ .into() } else if self.instructions.is_empty() { Element::from(center( - monospace("No instructions recorded yet!") + text("No instructions recorded yet!") .size(14) + .font(Font::MONOSPACE) .width(Fill) .center(), )) @@ -599,9 +692,10 @@ impl Tester

{ scrollable( column(self.instructions.iter().enumerate().map( |(i, instruction)| { - monospace(instruction.to_string()) + text(instruction.to_string()) .wrapping(text::Wrapping::None) // TODO: Ellipsize? .size(10) + .font(Font::MONOSPACE) .style(move |theme: &Theme| text::Style { color: match &self.state { State::Playing { @@ -730,9 +824,12 @@ where Message: 'a, Renderer: program::Renderer + 'a, { - column![monospace(fragment).size(14), content.into()] - .spacing(5) - .into() + column![ + text(fragment).size(14).font(Font::MONOSPACE), + content.into() + ] + .spacing(5) + .into() } fn labeled_with<'a, Message, Renderer>( @@ -746,7 +843,7 @@ where { column![ row![ - monospace(fragment).size(14), + text(fragment).size(14).font(Font::MONOSPACE), horizontal_space(), control.into() ] diff --git a/devtools/src/tester/recorder.rs b/tester/src/recorder.rs similarity index 100% rename from devtools/src/tester/recorder.rs rename to tester/src/recorder.rs diff --git a/winit/src/lib.rs b/winit/src/lib.rs index 0ad2c507..459f7215 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -45,7 +45,7 @@ use crate::core::renderer; use crate::core::theme; use crate::core::time::Instant; use crate::core::widget::operation; -use crate::core::{Point, Settings, Size}; +use crate::core::{Point, Size}; use crate::futures::futures::channel::mpsc; use crate::futures::futures::channel::oneshot; use crate::futures::futures::task; @@ -66,11 +66,7 @@ use std::slice; use std::sync::Arc; /// Runs a [`Program`] with the provided settings. -pub fn run

( - program: P, - settings: Settings, - window_settings: Option, -) -> Result<(), Error> +pub fn run

(program: P) -> Result<(), Error> where P: Program + 'static, P::Theme: theme::Base, @@ -78,6 +74,8 @@ where use winit::event_loop::EventLoop; let boot_span = debug::boot(); + let settings = program.settings(); + let window_settings = program.window(); let graphics_settings = settings.clone().into(); let event_loop = EventLoop::with_user_event() @@ -169,7 +167,6 @@ where impl winit::application::ApplicationHandler> for Runner where - Message: std::fmt::Debug, F: Future, { fn resumed( diff --git a/winit/src/proxy.rs b/winit/src/proxy.rs index dcfcacff..ef3ecec7 100644 --- a/winit/src/proxy.rs +++ b/winit/src/proxy.rs @@ -88,10 +88,7 @@ impl Proxy { /// /// Note: This skips the backpressure mechanism with an unbounded /// channel. Use sparingly! - pub fn send_action(&self, action: Action) - where - T: std::fmt::Debug, - { + pub fn send_action(&self, action: Action) { let _ = self.raw.send_event(action); } From 4991a1a7f3709d439101a3f06f5bb8956e0c341d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 29 Aug 2025 08:50:27 +0200 Subject: [PATCH 58/83] Remove `Debug` bound in `devtools::TimeMachine` --- devtools/src/time_machine.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devtools/src/time_machine.rs b/devtools/src/time_machine.rs index 3c8c18a7..e9e51874 100644 --- a/devtools/src/time_machine.rs +++ b/devtools/src/time_machine.rs @@ -13,7 +13,7 @@ where impl

TimeMachine

where P: Program, - P::Message: std::fmt::Debug + Clone, + P::Message: Clone, { pub fn new() -> Self { Self { From 5417b630a5bc97f4dc427574fa556ac7ca658d9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 29 Aug 2025 09:07:19 +0200 Subject: [PATCH 59/83] Fix `Send` requirements in WebAssembly builds --- debug/src/lib.rs | 6 +++--- program/src/lib.rs | 4 ++-- src/application.rs | 9 ++++----- src/daemon.rs | 7 +++---- src/lib.rs | 3 +-- winit/src/proxy.rs | 5 +---- 6 files changed, 14 insertions(+), 20 deletions(-) diff --git a/debug/src/lib.rs b/debug/src/lib.rs index f79dde55..e20c00e6 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -413,7 +413,7 @@ mod internal { } } -#[cfg(feature = "hot")] +#[cfg(all(feature = "hot", not(target_arch = "wasm32")))] mod hot { use std::collections::BTreeSet; use std::sync::atomic::{self, AtomicBool}; @@ -478,7 +478,7 @@ mod hot { } } -#[cfg(not(feature = "hot"))] +#[cfg(any(not(feature = "hot"), target_arch = "wasm32"))] mod hot { pub fn init() {} @@ -486,7 +486,7 @@ mod hot { f() } - pub fn on_hotpatch(_f: impl Fn() + Send + Sync + 'static) {} + pub fn on_hotpatch(_f: impl Fn()) {} pub fn is_stale() -> bool { false diff --git a/program/src/lib.rs b/program/src/lib.rs index 0e473c3a..3c49659f 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -15,7 +15,7 @@ use crate::core::text; use crate::core::theme; use crate::core::window; use crate::core::{Element, Font, Settings}; -use crate::futures::{Executor, MaybeSend, Subscription}; +use crate::futures::{Executor, Subscription}; use crate::graphics::compositor; use crate::runtime::Task; @@ -29,7 +29,7 @@ pub trait Program: Sized { type State; /// The message of the program. - type Message: MaybeSend + 'static; + type Message: Send + 'static; /// The theme of the program. type Theme: Default + theme::Base; diff --git a/src/application.rs b/src/application.rs index 38d95f79..e2de21b5 100644 --- a/src/application.rs +++ b/src/application.rs @@ -36,8 +36,7 @@ use crate::shell; use crate::theme; use crate::window; use crate::{ - Element, Executor, Font, MaybeSend, Preset, Result, Settings, Size, - Subscription, Task, + Element, Executor, Font, Preset, Result, Settings, Size, Subscription, Task, }; use iced_debug as debug; @@ -83,7 +82,7 @@ pub fn application( ) -> Application> where State: 'static, - Message: MaybeSend + 'static, + Message: Send + 'static, Theme: Default + theme::Base, Renderer: program::Renderer, { @@ -102,7 +101,7 @@ where impl Program for Instance where - Message: MaybeSend + 'static, + Message: Send + 'static, Theme: Default + theme::Base, Renderer: program::Renderer, Boot: self::Boot, @@ -201,7 +200,7 @@ impl Application

{ #[cfg(all(feature = "debug", not(feature = "tester")))] let program = iced_devtools::attach(self); - #[cfg(any(not(feature = "debug"), target_arch = "wasm32"))] + #[cfg(not(any(feature = "tester", feature = "debug")))] let program = self; Ok(shell::run(program)?) diff --git a/src/daemon.rs b/src/daemon.rs index c848d8bd..c58d8033 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -6,8 +6,7 @@ use crate::shell; use crate::theme; use crate::window; use crate::{ - Element, Executor, Font, MaybeSend, Preset, Result, Settings, Subscription, - Task, + Element, Executor, Font, Preset, Result, Settings, Subscription, Task, }; use iced_debug as debug; @@ -31,7 +30,7 @@ pub fn daemon( ) -> Daemon> where State: 'static, - Message: MaybeSend + 'static, + Message: Send + 'static, Theme: Default + theme::Base, Renderer: program::Renderer, { @@ -50,7 +49,7 @@ where impl Program for Instance where - Message: MaybeSend + 'static, + Message: Send + 'static, Theme: Default + theme::Base, Renderer: program::Renderer, Boot: application::Boot, diff --git a/src/lib.rs b/src/lib.rs index e4cc8807..d2772e62 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -528,7 +528,6 @@ pub use crate::core::{ pub use crate::program::Preset; pub use crate::program::message; pub use crate::runtime::exit; -pub use crate::runtime::futures::MaybeSend; pub use iced_futures::Subscription; pub use Alignment::Center; @@ -698,7 +697,7 @@ pub fn run( ) -> Result where State: Default + 'static, - Message: MaybeSend + message::MaybeDebug + message::MaybeClone + 'static, + Message: Send + message::MaybeDebug + message::MaybeClone + 'static, Theme: Default + theme::Base + 'static, Renderer: program::Renderer + 'static, { diff --git a/winit/src/proxy.rs b/winit/src/proxy.rs index ef3ecec7..92758c5f 100644 --- a/winit/src/proxy.rs +++ b/winit/src/proxy.rs @@ -77,10 +77,7 @@ impl Proxy { /// /// Note: This skips the backpressure mechanism with an unbounded /// channel. Use sparingly! - pub fn send(&self, value: T) - where - T: std::fmt::Debug, - { + pub fn send(&self, value: T) { self.send_action(Action::Output(value)); } From 8d27a3bc26b662d7a93465a1c352df113b24d920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 11 Sep 2025 05:06:36 +0200 Subject: [PATCH 60/83] Leverage `&str` implementation of `Selector` for `String` --- selector/src/lib.rs | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/selector/src/lib.rs b/selector/src/lib.rs index b8081178..3d60a127 100644 --- a/selector/src/lib.rs +++ b/selector/src/lib.rs @@ -71,33 +71,11 @@ impl Selector for String { type Output = target::Text; fn select(&mut self, target: Target<'_>) -> Option { - match target { - Target::TextInput { - id, - bounds, - visible_bounds, - state, - } if state.text() == *self => Some(target::Text::Input { - id: id.cloned(), - bounds, - visible_bounds, - }), - Target::Text { - id, - bounds, - visible_bounds, - content, - } if content == *self => Some(target::Text::Raw { - id: id.cloned(), - bounds, - visible_bounds, - }), - _ => None, - } + self.as_str().select(target) } fn description(&self) -> String { - format!("text == \"{}\"", self.escape_default()) + self.as_str().description() } } From faad8e2ed04140a95d7d6e104f0971f737df801f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 11 Sep 2025 05:08:10 +0200 Subject: [PATCH 61/83] Restore `debug` feature for `websocket` example --- examples/websocket/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/websocket/Cargo.toml b/examples/websocket/Cargo.toml index b2053fa6..c47e3c93 100644 --- a/examples/websocket/Cargo.toml +++ b/examples/websocket/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["tester", "tokio", "sipper"] +iced.features = ["debug", "tokio", "sipper"] warp = "0.3" From bc9951f84f2d78b7e304fd1466daf78bf386510c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 11 Sep 2025 06:20:04 +0200 Subject: [PATCH 62/83] Add proper `string` parser for `Instruction` --- selector/src/lib.rs | 6 +-- test/src/instruction.rs | 115 +++++++++++++++++++++++++++++++++++----- 2 files changed, 105 insertions(+), 16 deletions(-) diff --git a/selector/src/lib.rs b/selector/src/lib.rs index 3d60a127..ae1c733d 100644 --- a/selector/src/lib.rs +++ b/selector/src/lib.rs @@ -63,7 +63,7 @@ impl Selector for &str { } fn description(&self) -> String { - format!("text == \"{}\"", self.escape_default()) + format!("text == {self:?}") } } @@ -91,7 +91,7 @@ impl Selector for Id { } fn description(&self) -> String { - format!("id == {:?}", self) + format!("id == {self:?}") } } @@ -106,7 +106,7 @@ impl Selector for Point { } fn description(&self) -> String { - format!("bounds contains {:?}", self) + format!("bounds contains {self:?}") } } diff --git a/test/src/instruction.rs b/test/src/instruction.rs index 14cceb2f..6059be9c 100644 --- a/test/src/instruction.rs +++ b/test/src/instruction.rs @@ -421,10 +421,13 @@ mod parser { use nom::branch::alt; use nom::bytes::complete::tag; - use nom::character::complete::{char, multispace0, satisfy}; - use nom::combinator::{cut, map, opt, recognize, success}; + use nom::bytes::{is_not, take_while_m_n}; + use nom::character::complete::{char, multispace0, multispace1}; + use nom::combinator::{ + cut, map, map_opt, map_res, opt, success, value, verify, + }; use nom::error::ParseError; - use nom::multi::many0; + use nom::multi::fold; use nom::number::float; use nom::sequence::{delimited, preceded, separated_pair}; use nom::{Finish, IResult, Parser}; @@ -521,16 +524,6 @@ mod parser { .parse(input) } - fn string(input: &str) -> IResult<&str, String> { - // TODO: Proper string literal parsing - delimited( - char('"'), - map(recognize(many0(satisfy(|c| c != '"'))), str::to_owned), - char('"'), - ) - .parse(input) - } - fn point(input: &str) -> IResult<&str, Point> { let comma = whitespace(char(',')); @@ -553,4 +546,100 @@ mod parser { { delimited(multispace0, inner, multispace0) } + + // Taken from https://github.com/rust-bakery/nom/blob/51c3c4e44fa78a8a09b413419372b97b2cc2a787/examples/string.rs + // + // Copyright (c) 2014-2019 Geoffroy Couprie + // + // 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. + fn string(input: &str) -> IResult<&str, String> { + #[derive(Debug, Clone, Copy)] + enum Fragment<'a> { + Literal(&'a str), + EscapedChar(char), + EscapedWS, + } + + fn fragment(input: &str) -> IResult<&str, Fragment<'_>> { + alt(( + map(string_literal, Fragment::Literal), + map(escaped_char, Fragment::EscapedChar), + value(Fragment::EscapedWS, escaped_whitespace), + )) + .parse(input) + } + + fn string_literal<'a, E: ParseError<&'a str>>( + input: &'a str, + ) -> IResult<&'a str, &'a str, E> { + let not_quote_slash = is_not("\"\\"); + + verify(not_quote_slash, |s: &str| !s.is_empty()).parse(input) + } + + fn unicode(input: &str) -> IResult<&str, char> { + let parse_hex = + take_while_m_n(1, 6, |c: char| c.is_ascii_hexdigit()); + + let parse_delimited_hex = + preceded(char('u'), delimited(char('{'), parse_hex, char('}'))); + + let parse_u32 = map_res(parse_delimited_hex, move |hex| { + u32::from_str_radix(hex, 16) + }); + + map_opt(parse_u32, std::char::from_u32).parse(input) + } + + fn escaped_char(input: &str) -> IResult<&str, char> { + preceded( + char('\\'), + alt(( + unicode, + value('\n', char('n')), + value('\r', char('r')), + value('\t', char('t')), + value('\u{08}', char('b')), + value('\u{0C}', char('f')), + value('\\', char('\\')), + value('/', char('/')), + value('"', char('"')), + )), + ) + .parse(input) + } + + fn escaped_whitespace(input: &str) -> IResult<&str, &str> { + preceded(char('\\'), multispace1).parse(input) + } + + let build_string = + fold(0.., fragment, String::new, |mut string, fragment| { + match fragment { + Fragment::Literal(s) => string.push_str(s), + Fragment::EscapedChar(c) => string.push(c), + Fragment::EscapedWS => {} + } + string + }); + + delimited(char('"'), build_string, char('"')).parse(input) + } } From c70ce5af89e4e942eb81bc3b66327b2add4d9c78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 11 Sep 2025 06:24:13 +0200 Subject: [PATCH 63/83] Add `id` helper to `selector` module --- examples/todos/src/main.rs | 4 ++-- selector/src/lib.rs | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 98f6e90f..1aaaf614 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -617,8 +617,8 @@ fn presets() -> impl Iterator> { mod tests { use super::*; - use iced::widget; use iced::{Settings, Theme}; + use iced_test::selector::id; use iced_test::{Error, Simulator}; fn simulator(todos: &Todos) -> Simulator<'_, Message> { @@ -638,7 +638,7 @@ mod tests { let _command = todos.update(Message::Loaded(Err(LoadError::File))); let mut ui = simulator(&todos); - let _input = ui.click(widget::Id::new("new-task"))?; + let _input = ui.click(id("new-task"))?; let _ = ui.typewrite("Create the universe"); let _ = ui.tap_key(keyboard::key::Named::Enter); diff --git a/selector/src/lib.rs b/selector/src/lib.rs index ae1c733d..050d6a49 100644 --- a/selector/src/lib.rs +++ b/selector/src/lib.rs @@ -9,7 +9,7 @@ pub use find::{Find, FindAll}; pub use target::Target; use crate::core::Point; -use crate::core::widget::Id; +use crate::core::widget; pub trait Selector { type Output; @@ -79,7 +79,7 @@ impl Selector for String { } } -impl Selector for Id { +impl Selector for widget::Id { type Output = target::Match; fn select(&mut self, target: Target<'_>) -> Option { @@ -124,3 +124,8 @@ where format!("custom selector: {}", std::any::type_name_of_val(self)) } } + +/// Creates a new [`Selector`] that matches widgets with the given [`widget::Id`]. +pub fn id(id: impl Into) -> impl Selector { + id.into() +} From c684fbd6af6e02532328d2d966cb01031331e8dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 11 Sep 2025 06:51:53 +0200 Subject: [PATCH 64/83] Rename `horizontal_rule` to `rule` and introduce `Rule::vertical` --- examples/layout/src/main.rs | 7 ++-- examples/styling/src/main.rs | 10 ++--- examples/toast/src/main.rs | 4 +- tester/src/lib.rs | 7 +++- widget/src/helpers.rs | 33 ++-------------- widget/src/markdown.rs | 11 ++---- widget/src/rule.rs | 77 +++++++++++++++++++----------------- 7 files changed, 63 insertions(+), 86 deletions(-) diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index b930bdb0..a43bb117 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -3,8 +3,7 @@ use iced::keyboard; use iced::mouse; use iced::widget::{ button, canvas, center, center_y, checkbox, column, container, - horizontal_rule, horizontal_space, pick_list, pin, row, scrollable, stack, - text, vertical_rule, + horizontal_space, pick_list, pin, row, rule, scrollable, stack, text, }; use iced::{ Center, Element, Fill, Font, Length, Point, Rectangle, Renderer, Shrink, @@ -295,7 +294,7 @@ fn quotes<'a>() -> Element<'a, Message> { fn quote<'a>( content: impl Into>, ) -> Element<'a, Message> { - row![vertical_rule(1), content.into()] + row![rule(1).vertical(), content.into()] .spacing(10) .height(Shrink) .into() @@ -313,7 +312,7 @@ fn quotes<'a>() -> Element<'a, Message> { reply("This is the original message", "This is a reply"), "This is another reply", ), - horizontal_rule(1), + rule(1), text("A separator ↑"), ] .width(Shrink) diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index e5805532..3e48c5fe 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -1,8 +1,8 @@ use iced::keyboard; use iced::widget::{ - button, center_x, center_y, checkbox, column, container, horizontal_rule, - pick_list, progress_bar, row, scrollable, slider, text, text_input, - toggler, vertical_rule, vertical_space, + button, center_x, center_y, checkbox, column, container, pick_list, + progress_bar, row, rule, scrollable, slider, text, text_input, toggler, + vertical_space, }; use iced::{Center, Element, Fill, Shrink, Subscription, Theme}; @@ -162,14 +162,14 @@ impl Styling { let content = column![ choose_theme, - horizontal_rule(1), + rule(1), text_input, buttons, slider(), progress_bar(), row![ scroll_me, - vertical_rule(1), + rule(1).vertical(), column![check, check_disabled, toggle, disabled_toggle] .spacing(10) ] diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 1cc8a58a..db4e073b 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -172,7 +172,7 @@ mod toast { use iced::theme; use iced::time::{self, Duration, Instant}; use iced::widget::{ - button, column, container, horizontal_rule, horizontal_space, row, text, + button, column, container, horizontal_space, row, rule, text, }; use iced::window; use iced::{ @@ -254,7 +254,7 @@ mod toast { Status::Success => success, Status::Danger => danger, }), - horizontal_rule(1), + rule(1), container(text(toast.body.as_str())) .width(Fill) .padding(5) diff --git a/tester/src/lib.rs b/tester/src/lib.rs index 80d3d960..4021d8c2 100644 --- a/tester/src/lib.rs +++ b/tester/src/lib.rs @@ -28,7 +28,7 @@ use crate::test::instruction; use crate::test::{Emulator, Ice, Instruction}; use crate::widget::{ button, center, column, combo_box, container, horizontal_space, pick_list, - row, scrollable, text, text_editor, text_input, themer, + row, rule, scrollable, text, text_editor, text_input, themer, }; /// Attaches a [`Tester`] to the given [`Program`]. @@ -632,10 +632,13 @@ impl Tester

{ row![ center(column![status, viewport].spacing(10).align_x(Right)) .padding(10), + rule(1).vertical().style(rule::weak), container(self.controls().map(Tick::Tester)) .width(250) .padding(10) - .style(container::dark) + .style(|theme| container::Style::default().background( + theme.extended_palette().background.weakest.color + )), ] .into() } diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index fb26945d..89aa7231 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -1752,7 +1752,7 @@ pub fn vertical_space() -> Space { /// # mod iced { pub mod widget { pub use iced_widget::*; } } /// # pub type State = (); /// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; -/// use iced::widget::horizontal_rule; +/// use iced::widget::rule; /// /// #[derive(Clone)] /// enum Message { @@ -1760,39 +1760,14 @@ pub fn vertical_space() -> Space { /// } /// /// fn view(state: &State) -> Element<'_, Message> { -/// horizontal_rule(2).into() +/// rule(2).into() /// } /// ``` -pub fn horizontal_rule<'a, Theme>(height: impl Into) -> Rule<'a, Theme> +pub fn rule<'a, Theme>(height: impl Into) -> Rule<'a, Theme> where Theme: rule::Catalog + 'a, { - Rule::horizontal(height) -} - -/// Creates a vertical [`Rule`] with the given width. -/// -/// # Example -/// ```no_run -/// # mod iced { pub mod widget { pub use iced_widget::*; } } -/// # pub type State = (); -/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; -/// use iced::widget::vertical_rule; -/// -/// #[derive(Clone)] -/// enum Message { -/// // ..., -/// } -/// -/// fn view(state: &State) -> Element<'_, Message> { -/// vertical_rule(2).into() -/// } -/// ``` -pub fn vertical_rule<'a, Theme>(width: impl Into) -> Rule<'a, Theme> -where - Theme: rule::Catalog + 'a, -{ - Rule::vertical(width) + Rule::new(height) } /// Creates a new [`ProgressBar`]. diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index 35e7549c..f2114f1d 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -51,10 +51,7 @@ use crate::core::theme; use crate::core::{ self, Color, Element, Length, Padding, Pixels, Theme, color, }; -use crate::{ - column, container, horizontal_rule, rich_text, row, rule, scrollable, span, - text, vertical_rule, -}; +use crate::{column, container, rich_text, row, scrollable, span, text}; use std::borrow::BorrowMut; use std::cell::{Cell, RefCell}; @@ -1391,7 +1388,7 @@ where Renderer: core::text::Renderer + 'a, { row![ - vertical_rule(4), + crate::rule(4).vertical(), column( contents .iter() @@ -1413,7 +1410,7 @@ where Theme: Catalog + 'a, Renderer: core::text::Renderer + 'a, { - horizontal_rule(2).into() + crate::rule(2).into() } /// Displays a table using the default look. @@ -1637,8 +1634,8 @@ where pub trait Catalog: container::Catalog + scrollable::Catalog - + rule::Catalog + text::Catalog + + crate::rule::Catalog + crate::table::Catalog { /// The styling class of a Markdown code block. diff --git a/widget/src/rule.rs b/widget/src/rule.rs index 163d2757..b9cee136 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -5,7 +5,7 @@ //! # mod iced { pub mod widget { pub use iced_widget::*; } } //! # pub type State = (); //! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; -//! use iced::widget::horizontal_rule; +//! use iced::widget::rule; //! //! #[derive(Clone)] //! enum Message { @@ -13,7 +13,7 @@ //! } //! //! fn view(state: &State) -> Element<'_, Message> { -//! horizontal_rule(2).into() +//! rule(2).into() //! } //! ``` use crate::core; @@ -33,7 +33,7 @@ use crate::core::{ /// # mod iced { pub mod widget { pub use iced_widget::*; } } /// # pub type State = (); /// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; -/// use iced::widget::horizontal_rule; +/// use iced::widget::rule; /// /// #[derive(Clone)] /// enum Message { @@ -41,7 +41,7 @@ use crate::core::{ /// } /// /// fn view(state: &State) -> Element<'_, Message> { -/// horizontal_rule(2).into() +/// rule(2).into() /// } /// ``` #[allow(missing_debug_implementations)] @@ -49,9 +49,8 @@ pub struct Rule<'a, Theme = crate::Theme> where Theme: Catalog, { - width: Length, - height: Length, - is_horizontal: bool, + thickness: Length, + is_vertical: bool, class: Theme::Class<'a>, } @@ -59,24 +58,19 @@ impl<'a, Theme> Rule<'a, Theme> where Theme: Catalog, { - /// Creates a horizontal [`Rule`] with the given height. - pub fn horizontal(height: impl Into) -> Self { + /// Creates a horizontal [`Rule`] with the given thickness. + pub fn new(thickness: impl Into) -> Self { Rule { - width: Length::Fill, - height: Length::Fixed(height.into().0), - is_horizontal: true, + thickness: Length::Fixed(thickness.into().0), + is_vertical: false, class: Theme::default(), } } - /// Creates a vertical [`Rule`] with the given width. - pub fn vertical(width: impl Into) -> Self { - Rule { - width: Length::Fixed(width.into().0), - height: Length::Fill, - is_horizontal: false, - class: Theme::default(), - } + /// Turns the [`Rule`] into a vertical one. + pub fn vertical(mut self) -> Self { + self.is_vertical = true; + self } /// Sets the style of the [`Rule`]. @@ -105,9 +99,16 @@ where Theme: Catalog, { fn size(&self) -> Size { - Size { - width: self.width, - height: self.height, + if self.is_vertical { + Size { + width: self.thickness, + height: Length::Fill, + } + } else { + Size { + width: Length::Fill, + height: self.thickness, + } } } @@ -117,7 +118,9 @@ where _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - layout::atomic(limits, self.width, self.height) + let size = >::size(self); + + layout::atomic(limits, size.width, size.height) } fn draw( @@ -133,19 +136,7 @@ where let bounds = layout.bounds(); let style = theme.style(&self.class); - let bounds = if self.is_horizontal { - let line_y = bounds.y.round(); - - let (offset, line_width) = style.fill_mode.fill(bounds.width); - let line_x = bounds.x + offset; - - Rectangle { - x: line_x, - y: line_y, - width: line_width, - height: bounds.height, - } - } else { + let bounds = if self.is_vertical { let line_x = bounds.x.round(); let (offset, line_height) = style.fill_mode.fill(bounds.height); @@ -157,6 +148,18 @@ where width: bounds.width, height: line_height, } + } else { + let line_y = bounds.y.round(); + + let (offset, line_width) = style.fill_mode.fill(bounds.width); + let line_x = bounds.x + offset; + + Rectangle { + x: line_x, + y: line_y, + width: line_width, + height: bounds.height, + } }; renderer.fill_quad( From 89b7585465f57acb42c60083f94150bcb59a7661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 11 Sep 2025 07:10:53 +0200 Subject: [PATCH 65/83] Rename `*_space` to `space_x` and `space_y` --- devtools/src/lib.rs | 10 +++++----- examples/bezier_tool/src/main.rs | 4 ++-- examples/combo_box/src/main.rs | 6 ++---- examples/editor/src/main.rs | 8 ++++---- examples/gallery/src/main.rs | 14 +++++++------- examples/gradient/src/main.rs | 6 ++---- examples/layout/src/main.rs | 22 ++++++++-------------- examples/lazy/src/main.rs | 5 ++--- examples/markdown/src/main.rs | 6 +++--- examples/modal/src/main.rs | 16 ++++++---------- examples/multi_window/src/main.rs | 6 +++--- examples/pick_list/src/main.rs | 6 +++--- examples/scrollable/src/main.rs | 24 ++++++++++++------------ examples/styling/src/main.rs | 6 +++--- examples/toast/src/main.rs | 6 ++---- examples/tour/src/main.rs | 11 +++++------ examples/vectorial_text/src/main.rs | 6 ++---- examples/visible_bounds/src/main.rs | 13 ++++++------- tester/src/lib.rs | 10 +++++----- widget/src/helpers.rs | 8 ++++---- widget/src/responsive.rs | 4 ++-- widget/src/scrollable.rs | 8 ++++---- 22 files changed, 92 insertions(+), 113 deletions(-) diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index d548b55a..c28bf31a 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -23,8 +23,8 @@ use crate::program::message; use crate::runtime::task::{self, Task}; use crate::time_machine::TimeMachine; use crate::widget::{ - bottom_right, button, center, column, container, horizontal_space, opaque, - row, scrollable, stack, text, themer, + bottom_right, button, center, column, container, opaque, row, scrollable, + space_x, stack, text, themer, }; use std::fmt; @@ -448,7 +448,7 @@ where .width(100) .on_press(Message::CancelSetup) .style(button::danger), - horizontal_space(), + space_x(), button( text(match goal { Goal::Installation => "Install", @@ -498,13 +498,13 @@ where let comparison = column![ row![ "Installed revision:", - horizontal_space(), + space_x(), inline_code(revision.as_deref().unwrap_or("Unknown")) ] .align_y(Center), row![ "Compatible revision:", - horizontal_space(), + space_x(), inline_code(comet::COMPATIBLE_REVISION), ] .align_y(Center) diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 9a525210..0f006821 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -1,5 +1,5 @@ //! This example showcases an interactive `Canvas` for drawing Bézier curves. -use iced::widget::{button, container, horizontal_space, hover, right}; +use iced::widget::{button, container, hover, right, space_x}; use iced::{Element, Theme}; pub fn main() -> iced::Result { @@ -38,7 +38,7 @@ impl Example { container(hover( self.bezier.view(&self.curves).map(Message::AddCurve), if self.curves.is_empty() { - container(horizontal_space()) + container(space_x()) } else { right( button("Clear") diff --git a/examples/combo_box/src/main.rs b/examples/combo_box/src/main.rs index a7a68590..cc0e7fc0 100644 --- a/examples/combo_box/src/main.rs +++ b/examples/combo_box/src/main.rs @@ -1,6 +1,4 @@ -use iced::widget::{ - center, column, combo_box, scrollable, text, vertical_space, -}; +use iced::widget::{center, column, combo_box, scrollable, space_y, text}; use iced::{Center, Element, Fill}; pub fn main() -> iced::Result { @@ -62,7 +60,7 @@ impl Example { text(&self.text), "What is your language?", combo_box, - vertical_space().height(150), + space_y().height(150), ] .width(Fill) .align_x(Center) diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index a687eee4..6a987030 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -1,8 +1,8 @@ use iced::highlighter; use iced::keyboard; use iced::widget::{ - button, center_x, column, container, horizontal_space, operation, - pick_list, row, text, text_editor, toggler, tooltip, + button, center_x, column, container, operation, pick_list, row, space_x, + text, text_editor, toggler, tooltip, }; use iced::{Center, Element, Fill, Font, Task, Theme}; @@ -157,7 +157,7 @@ impl Editor { "Save file", self.is_dirty.then_some(Message::SaveFile) ), - horizontal_space(), + space_x(), toggler(self.word_wrap) .label("Word Wrap") .on_toggle(Message::WordWrapToggled), @@ -184,7 +184,7 @@ impl Editor { } else { String::from("New file") }), - horizontal_space(), + space_x(), text({ let (line, column) = self.content.cursor_position(); diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs index 14d20103..529c04d4 100644 --- a/examples/gallery/src/main.rs +++ b/examples/gallery/src/main.rs @@ -9,8 +9,8 @@ use crate::civitai::{Error, Id, Image, Rgba, Size}; use iced::animation; use iced::time::{Instant, milliseconds}; use iced::widget::{ - button, container, float, grid, horizontal_space, image, mouse_area, - opaque, scrollable, sensor, stack, + button, container, float, grid, image, mouse_area, opaque, scrollable, + sensor, space_x, stack, }; use iced::window; use iced::{ @@ -227,7 +227,7 @@ fn card<'a>( }) .into() } else { - horizontal_space().into() + space_x().into() }; if let Some(blurhash) = preview.blurhash(now) { @@ -241,7 +241,7 @@ fn card<'a>( thumbnail } } else { - horizontal_space().into() + space_x().into() }; let card = mouse_area(container(image).style(container::dark)) @@ -264,7 +264,7 @@ fn card<'a>( } fn placeholder<'a>() -> Element<'a, Message> { - container(horizontal_space()).style(container::dark).into() + container(space_x()).style(container::dark).into() } enum Preview { @@ -426,7 +426,7 @@ impl Viewer { .scale(self.image_fade_in.interpolate(1.5, 1.0, now)) .into() } else { - horizontal_space().into() + space_x().into() }; if opacity > 0.0 { @@ -443,7 +443,7 @@ impl Viewer { .on_press(Message::Close), ) } else { - horizontal_space().into() + space_x().into() } } } diff --git a/examples/gradient/src/main.rs b/examples/gradient/src/main.rs index 8c0c90d3..d927cd23 100644 --- a/examples/gradient/src/main.rs +++ b/examples/gradient/src/main.rs @@ -1,8 +1,6 @@ use iced::gradient; use iced::theme; -use iced::widget::{ - checkbox, column, container, horizontal_space, row, slider, text, -}; +use iced::widget::{checkbox, column, container, row, slider, space_x, text}; use iced::{Center, Color, Element, Fill, Radians, Theme, color}; pub fn main() -> iced::Result { @@ -59,7 +57,7 @@ impl Gradient { transparent, } = *self; - let gradient_box = container(horizontal_space()) + let gradient_box = container(space_x()) .style(move |_theme| { let gradient = gradient::Linear::new(angle) .add_stop(0.0, start) diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index a43bb117..dc311d75 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -2,8 +2,8 @@ use iced::border; use iced::keyboard; use iced::mouse; use iced::widget::{ - button, canvas, center, center_y, checkbox, column, container, - horizontal_space, pick_list, pin, row, rule, scrollable, stack, text, + button, canvas, center, center_y, checkbox, column, container, pick_list, + pin, row, rule, scrollable, space_x, stack, text, }; use iced::{ Center, Element, Fill, Font, Length, Point, Rectangle, Renderer, Shrink, @@ -70,7 +70,7 @@ impl Layout { fn view(&self) -> Element<'_, Message> { let header = row![ text(self.example.title).size(20).font(Font::MONOSPACE), - horizontal_space(), + space_x(), checkbox("Explain", self.explain) .on_toggle(Message::ExplainToggled), pick_list(Theme::ALL, self.theme.as_ref(), Message::ThemeSelected) @@ -99,7 +99,7 @@ impl Layout { .on_press(Message::Previous) .into(), ), - Some(horizontal_space().into()), + Some(space_x().into()), (!self.example.is_last()).then_some( button(text("Next →")) .padding([5, 10]) @@ -238,20 +238,14 @@ fn row_<'a>() -> Element<'a, Message> { } fn space<'a>() -> Element<'a, Message> { - row!["Left!", horizontal_space(), "Right!"].into() + row!["Left!", space_x(), "Right!"].into() } fn application<'a>() -> Element<'a, Message> { let header = container( - row![ - square(40), - horizontal_space(), - "Header!", - horizontal_space(), - square(40), - ] - .padding(10) - .align_y(Center), + row![square(40), space_x(), "Header!", space_x(), square(40),] + .padding(10) + .align_y(Center), ) .style(|theme| { let palette = theme.extended_palette(); diff --git a/examples/lazy/src/main.rs b/examples/lazy/src/main.rs index 585b8c0a..99348da7 100644 --- a/examples/lazy/src/main.rs +++ b/examples/lazy/src/main.rs @@ -1,6 +1,5 @@ use iced::widget::{ - button, column, horizontal_space, lazy, pick_list, row, scrollable, text, - text_input, + button, column, lazy, pick_list, row, scrollable, space_x, text, text_input, }; use iced::{Element, Fill}; @@ -174,7 +173,7 @@ impl App { row![ text(item.name.clone()).color(item.color), - horizontal_space(), + space_x(), pick_list(Color::ALL, Some(item.color), move |color| { Message::ItemColorChanged(item.clone(), color) }), diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs index 42302b9e..c6df9a0c 100644 --- a/examples/markdown/src/main.rs +++ b/examples/markdown/src/main.rs @@ -5,8 +5,8 @@ use iced::clipboard; use iced::highlighter; use iced::time::{self, Instant, milliseconds}; use iced::widget::{ - button, center_x, container, horizontal_space, hover, image, markdown, - operation, right, row, scrollable, sensor, text_editor, toggler, + button, center_x, container, hover, image, markdown, operation, right, row, + scrollable, sensor, space_x, text_editor, toggler, }; use iced::window; use iced::{ @@ -264,7 +264,7 @@ impl<'a> markdown::Viewer<'a, Message> for CustomViewer<'a> { ) .into() } else { - sensor(horizontal_space()) + sensor(space_x()) .key_ref(url.as_str()) .delay(milliseconds(500)) .on_show(|_size| Message::ImageShown(url.clone())) diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index 54fcb1af..82596130 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -2,8 +2,8 @@ use iced::event::{self, Event}; use iced::keyboard; use iced::keyboard::key; use iced::widget::{ - button, center, column, container, horizontal_space, mouse_area, opaque, - operation, pick_list, row, stack, text, text_input, + button, center, column, container, mouse_area, opaque, operation, + pick_list, row, space_x, stack, text, text_input, }; use iced::{Bottom, Color, Element, Fill, Subscription, Task}; @@ -95,16 +95,12 @@ impl App { fn view(&self) -> Element<'_, Message> { let content = container( column![ - row![text("Top Left"), horizontal_space(), text("Top Right")] + row![text("Top Left"), space_x(), text("Top Right")] .height(Fill), center(button(text("Show Modal")).on_press(Message::ShowModal)), - row![ - text("Bottom Left"), - horizontal_space(), - text("Bottom Right") - ] - .align_y(Bottom) - .height(Fill), + row![text("Bottom Left"), space_x(), text("Bottom Right")] + .align_y(Bottom) + .height(Fill), ] .height(Fill), ) diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index a6731023..a8076617 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -1,6 +1,6 @@ use iced::widget::{ - button, center, center_x, column, container, horizontal_space, operation, - scrollable, text, text_input, + button, center, center_x, column, container, operation, scrollable, + space_x, text, text_input, }; use iced::window; use iced::{ @@ -134,7 +134,7 @@ impl Example { if let Some(window) = self.windows.get(&window_id) { center(window.view(window_id)).into() } else { - horizontal_space().into() + space_x().into() } } diff --git a/examples/pick_list/src/main.rs b/examples/pick_list/src/main.rs index 1023a30a..a1e44ae8 100644 --- a/examples/pick_list/src/main.rs +++ b/examples/pick_list/src/main.rs @@ -1,4 +1,4 @@ -use iced::widget::{column, pick_list, scrollable, vertical_space}; +use iced::widget::{column, pick_list, scrollable, space_y}; use iced::{Center, Element, Fill}; pub fn main() -> iced::Result { @@ -33,10 +33,10 @@ impl Example { .placeholder("Choose a language..."); let content = column![ - vertical_space().height(600), + space_y().height(600), "Which is your favorite language?", pick_list, - vertical_space().height(600), + space_y().height(600), ] .width(Fill) .align_x(Center) diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index e354bac8..9c6bfa1c 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -1,6 +1,6 @@ use iced::widget::{ - button, column, container, horizontal_space, operation, progress_bar, - radio, row, scrollable, slider, text, vertical_space, + button, column, container, operation, progress_bar, radio, row, scrollable, + slider, space_x, space_y, text, }; use iced::{Border, Center, Color, Element, Fill, Task, Theme}; @@ -190,9 +190,9 @@ impl ScrollableDemo { column![ scroll_to_end_button(), text("Beginning!"), - vertical_space().height(1200), + space_y().height(1200), text("Middle!"), - vertical_space().height(1200), + space_y().height(1200), text("End!"), scroll_to_beginning_button(), ] @@ -215,9 +215,9 @@ impl ScrollableDemo { row![ scroll_to_end_button(), text("Beginning!"), - horizontal_space().width(1200), + space_x().width(1200), text("Middle!"), - horizontal_space().width(1200), + space_x().width(1200), text("End!"), scroll_to_beginning_button(), ] @@ -242,25 +242,25 @@ impl ScrollableDemo { row![ column![ text("Let's do some scrolling!"), - vertical_space().height(2400) + space_y().height(2400) ], scroll_to_end_button(), text("Horizontal - Beginning!"), - horizontal_space().width(1200), + space_x().width(1200), //vertical content column![ text("Horizontal - Middle!"), scroll_to_end_button(), text("Vertical - Beginning!"), - vertical_space().height(1200), + space_y().height(1200), text("Vertical - Middle!"), - vertical_space().height(1200), + space_y().height(1200), text("Vertical - End!"), scroll_to_beginning_button(), - vertical_space().height(40), + space_y().height(40), ] .spacing(40), - horizontal_space().width(1200), + space_x().width(1200), text("Horizontal - End!"), scroll_to_beginning_button(), ] diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index 3e48c5fe..996f80ce 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -1,8 +1,8 @@ use iced::keyboard; use iced::widget::{ button, center_x, center_y, checkbox, column, container, pick_list, - progress_bar, row, rule, scrollable, slider, text, text_input, toggler, - vertical_space, + progress_bar, row, rule, scrollable, slider, space_y, text, text_input, + toggler, }; use iced::{Center, Element, Fill, Shrink, Subscription, Theme}; @@ -127,7 +127,7 @@ impl Styling { let scroll_me = scrollable(column![ "Scroll me!", - vertical_space().height(800), + space_y().height(800), "You did it!" ]) .width(Fill) diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index db4e073b..8f91c9a5 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -171,9 +171,7 @@ mod toast { use iced::mouse; use iced::theme; use iced::time::{self, Duration, Instant}; - use iced::widget::{ - button, column, container, horizontal_space, row, rule, text, - }; + use iced::widget::{button, column, container, row, rule, space_x, text}; use iced::window; use iced::{ Alignment, Center, Element, Event, Fill, Length, Point, Rectangle, @@ -239,7 +237,7 @@ mod toast { container( row![ text(toast.title.as_str()), - horizontal_space(), + space_x(), button("X") .on_press((on_close)(index)) .padding(3), diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index 78c329b5..33ae3cca 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -1,8 +1,7 @@ use iced::widget::{Button, Column, Container, Slider}; use iced::widget::{ - button, center_x, center_y, checkbox, column, horizontal_space, image, - radio, rich_text, row, scrollable, slider, span, text, text_input, toggler, - vertical_space, + button, center_x, center_y, checkbox, column, image, radio, rich_text, row, + scrollable, slider, space_x, space_y, span, text, text_input, toggler, }; use iced::{Center, Color, Element, Fill, Font, Pixels, color}; @@ -147,7 +146,7 @@ impl Tour { .on_press(Message::BackPressed) .style(button::secondary) }), - horizontal_space(), + space_x(), self.can_continue().then(|| { padded_button("Next").on_press(Message::NextPressed) }) @@ -406,14 +405,14 @@ impl Tour { text("Tip: You can use the scrollbar to scroll down faster!") .size(16), ) - .push(vertical_space().height(4096)) + .push(space_y().height(4096)) .push( text("You are halfway there!") .width(Fill) .size(30) .align_x(Center), ) - .push(vertical_space().height(4096)) + .push(space_y().height(4096)) .push(ferris(300, image::FilterMethod::Linear)) .push(text("You made it!").width(Fill).size(50).align_x(Center)) } diff --git a/examples/vectorial_text/src/main.rs b/examples/vectorial_text/src/main.rs index 78349696..12d190a3 100644 --- a/examples/vectorial_text/src/main.rs +++ b/examples/vectorial_text/src/main.rs @@ -1,8 +1,6 @@ use iced::alignment; use iced::mouse; -use iced::widget::{ - canvas, checkbox, column, horizontal_space, row, slider, text, -}; +use iced::widget::{canvas, checkbox, column, row, slider, space_x, text}; use iced::{Center, Element, Fill, Point, Rectangle, Renderer, Theme, Vector}; pub fn main() -> iced::Result { @@ -51,7 +49,7 @@ impl VectorialText { fn view(&self) -> Element<'_, Message> { let slider_with_label = |label, range, value, message: fn(f32) -> _| { column![ - row![text(label), horizontal_space(), text!("{:.2}", value)], + row![text(label), space_x(), text!("{:.2}", value)], slider(range, value, message).step(0.01) ] .spacing(2) diff --git a/examples/visible_bounds/src/main.rs b/examples/visible_bounds/src/main.rs index 893337e4..7a20ba86 100644 --- a/examples/visible_bounds/src/main.rs +++ b/examples/visible_bounds/src/main.rs @@ -1,8 +1,7 @@ use iced::event::{self, Event}; use iced::mouse; use iced::widget::{ - column, container, horizontal_space, row, scrollable, selector, text, - vertical_space, + column, container, row, scrollable, selector, space_x, space_y, text, }; use iced::window; use iced::{ @@ -64,7 +63,7 @@ impl Example { let data_row = |label, value, color| { row![ text(label), - horizontal_space(), + space_x(), text(value) .font(Font::MONOSPACE) .size(14) @@ -112,21 +111,21 @@ impl Example { scrollable( column![ text("Scroll me!"), - vertical_space().height(400), + space_y().height(400), container(text("I am the outer container!")) .id(OUTER_CONTAINER) .padding(40) .style(container::rounded_box), - vertical_space().height(400), + space_y().height(400), scrollable( column![ text("Scroll me!"), - vertical_space().height(400), + space_y().height(400), container(text("I am the inner container!")) .id(INNER_CONTAINER) .padding(40) .style(container::rounded_box), - vertical_space().height(400), + space_y().height(400), ] .padding(20) ) diff --git a/tester/src/lib.rs b/tester/src/lib.rs index 4021d8c2..5c2965cb 100644 --- a/tester/src/lib.rs +++ b/tester/src/lib.rs @@ -27,8 +27,8 @@ use crate::test::ice; use crate::test::instruction; use crate::test::{Emulator, Ice, Instruction}; use crate::widget::{ - button, center, column, combo_box, container, horizontal_space, pick_list, - row, rule, scrollable, text, text_editor, text_input, themer, + button, center, column, combo_box, container, pick_list, row, rule, + scrollable, space_x, text, text_editor, text_input, themer, }; /// Attaches a [`Tester`] to the given [`Program`]. @@ -566,7 +566,7 @@ impl Tester

{ let viewport = container( scrollable( container(match &self.state { - State::Empty => Element::from(horizontal_space()), + State::Empty => Element::from(space_x()), State::Idle { state } => { let theme = program.theme(state, window); @@ -797,7 +797,7 @@ impl Tester

{ }; let edit = if self.is_busy() { - Element::from(horizontal_space()) + Element::from(space_x()) } else if self.edit.is_none() { button(icon::pencil().size(14)) .padding(0) @@ -851,7 +851,7 @@ where column![ row![ text(fragment).size(14).font(Font::MONOSPACE), - horizontal_space(), + space_x(), control.into() ] .spacing(5) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 89aa7231..a5d20222 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -1017,7 +1017,7 @@ where /// # mod iced { pub mod widget { pub use iced_widget::*; } } /// # pub type State = (); /// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; -/// use iced::widget::{column, scrollable, vertical_space}; +/// use iced::widget::{column, scrollable, space_y}; /// /// enum Message { /// // ... @@ -1026,7 +1026,7 @@ where /// fn view(state: &State) -> Element<'_, Message> { /// scrollable(column![ /// "Scroll me!", -/// vertical_space().height(3000), +/// space_y().height(3000), /// "You did it!", /// ]).into() /// } @@ -1733,7 +1733,7 @@ where /// horizontal space. /// /// This can be useful to separate widgets in a [`Row`]. -pub fn horizontal_space() -> Space { +pub fn space_x() -> Space { Space::with_width(Length::Fill) } @@ -1741,7 +1741,7 @@ pub fn horizontal_space() -> Space { /// vertical space. /// /// This can be useful to separate widgets in a [`Column`]. -pub fn vertical_space() -> Space { +pub fn space_y() -> Space { Space::with_height(Length::Fill) } diff --git a/widget/src/responsive.rs b/widget/src/responsive.rs index a987a430..814f8909 100644 --- a/widget/src/responsive.rs +++ b/widget/src/responsive.rs @@ -8,7 +8,7 @@ use crate::core::{ self, Clipboard, Element, Event, Length, Rectangle, Shell, Size, Vector, Widget, }; -use crate::horizontal_space; +use crate::space_x; /// A widget that is aware of its dimensions. /// @@ -44,7 +44,7 @@ where view: Box::new(view), width: Length::Fill, height: Length::Fill, - content: Element::new(horizontal_space().width(0)), + content: Element::new(space_x().width(0)), } } diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 5320a9cc..321aeb25 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -5,7 +5,7 @@ //! # mod iced { pub mod widget { pub use iced_widget::*; } } //! # pub type State = (); //! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; -//! use iced::widget::{column, scrollable, vertical_space}; +//! use iced::widget::{column, scrollable, space_y}; //! //! enum Message { //! // ... @@ -14,7 +14,7 @@ //! fn view(state: &State) -> Element<'_, Message> { //! scrollable(column![ //! "Scroll me!", -//! vertical_space().height(3000), +//! space_y().height(3000), //! "You did it!", //! ]).into() //! } @@ -48,7 +48,7 @@ pub use operation::scrollable::{AbsoluteOffset, RelativeOffset}; /// # mod iced { pub mod widget { pub use iced_widget::*; } } /// # pub type State = (); /// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; -/// use iced::widget::{column, scrollable, vertical_space}; +/// use iced::widget::{column, scrollable, space_y}; /// /// enum Message { /// // ... @@ -57,7 +57,7 @@ pub use operation::scrollable::{AbsoluteOffset, RelativeOffset}; /// fn view(state: &State) -> Element<'_, Message> { /// scrollable(column![ /// "Scroll me!", -/// vertical_space().height(3000), +/// space_y().height(3000), /// "You did it!", /// ]).into() /// } From 5796ba272e5fafd5436f39945d53e48c090c18e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 12 Sep 2025 05:25:09 +0200 Subject: [PATCH 66/83] Remove `missing_debug_implementations` lint --- Cargo.toml | 1 - core/src/element.rs | 1 - core/src/hasher.rs | 14 -------------- core/src/overlay/element.rs | 1 - core/src/overlay/group.rs | 1 - core/src/overlay/nested.rs | 1 - core/src/widget/operation.rs | 1 - core/src/widget/text.rs | 1 - devtools/src/lib.rs | 1 - examples/loading_spinners/src/circular.rs | 1 - examples/loading_spinners/src/linear.rs | 1 - graphics/src/geometry/frame.rs | 1 - graphics/src/geometry/path/builder.rs | 1 - graphics/src/text.rs | 1 - program/src/lib.rs | 1 - runtime/src/user_interface.rs | 1 - runtime/src/window.rs | 1 - selector/src/target.rs | 1 - test/src/emulator.rs | 3 --- test/src/simulator.rs | 1 - tester/src/lib.rs | 2 -- tester/src/recorder.rs | 1 - tiny_skia/src/window/compositor.rs | 2 -- wgpu/src/engine.rs | 1 - wgpu/src/geometry.rs | 1 - wgpu/src/lib.rs | 1 - wgpu/src/primitive.rs | 1 - wgpu/src/text.rs | 1 - wgpu/src/window/compositor.rs | 1 - widget/src/button.rs | 1 - widget/src/checkbox.rs | 1 - widget/src/column.rs | 1 - widget/src/combo_box.rs | 1 - widget/src/container.rs | 1 - widget/src/float.rs | 1 - widget/src/grid.rs | 1 - widget/src/image.rs | 1 - widget/src/image/viewer.rs | 1 - widget/src/keyed/column.rs | 1 - widget/src/lazy.rs | 1 - widget/src/mouse_area.rs | 1 - widget/src/overlay/menu.rs | 1 - widget/src/pane_grid.rs | 1 - widget/src/pane_grid/content.rs | 1 - widget/src/pane_grid/controls.rs | 1 - widget/src/pane_grid/title_bar.rs | 1 - widget/src/pick_list.rs | 1 - widget/src/pin.rs | 1 - widget/src/progress_bar.rs | 1 - widget/src/qr_code.rs | 1 - widget/src/radio.rs | 1 - widget/src/responsive.rs | 1 - widget/src/row.rs | 2 -- widget/src/rule.rs | 1 - widget/src/scrollable.rs | 1 - widget/src/sensor.rs | 1 - widget/src/shader.rs | 1 - widget/src/slider.rs | 1 - widget/src/stack.rs | 1 - widget/src/svg.rs | 1 - widget/src/table.rs | 2 -- widget/src/text/rich.rs | 1 - widget/src/text_editor.rs | 1 - widget/src/text_input.rs | 1 - widget/src/themer.rs | 1 - widget/src/toggler.rs | 1 - widget/src/tooltip.rs | 1 - widget/src/vertical_slider.rs | 1 - winit/src/clipboard.rs | 1 - winit/src/window.rs | 2 -- 70 files changed, 90 deletions(-) delete mode 100644 core/src/hasher.rs diff --git a/Cargo.toml b/Cargo.toml index 7971c150..637edf1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -233,7 +233,6 @@ winit = { git = "https://github.com/iced-rs/winit.git", rev = "11414b6aa45699f03 [workspace.lints.rust] rust_2018_idioms = { level = "deny", priority = -1 } -missing_debug_implementations = "deny" missing_docs = "deny" unsafe_code = "deny" unused_results = "deny" diff --git a/core/src/element.rs b/core/src/element.rs index 6a71296c..17731d0c 100644 --- a/core/src/element.rs +++ b/core/src/element.rs @@ -20,7 +20,6 @@ use std::borrow::Borrow; /// to turn it into an [`Element`]. /// /// [built-in widget]: crate::widget -#[allow(missing_debug_implementations)] pub struct Element<'a, Message, Theme, Renderer> { widget: Box + 'a>, } diff --git a/core/src/hasher.rs b/core/src/hasher.rs deleted file mode 100644 index 13180e41..00000000 --- a/core/src/hasher.rs +++ /dev/null @@ -1,14 +0,0 @@ -/// The hasher used to compare layouts. -#[allow(missing_debug_implementations)] // Doesn't really make sense to have debug on the hasher state anyways. -#[derive(Default)] -pub struct Hasher(rustc_hash::FxHasher); - -impl core::hash::Hasher for Hasher { - fn write(&mut self, bytes: &[u8]) { - self.0.write(bytes); - } - - fn finish(&self) -> u64 { - self.0.finish() - } -} diff --git a/core/src/overlay/element.rs b/core/src/overlay/element.rs index 561a18ab..e67a48bc 100644 --- a/core/src/overlay/element.rs +++ b/core/src/overlay/element.rs @@ -7,7 +7,6 @@ use crate::widget; use crate::{Clipboard, Event, Layout, Shell, Size}; /// A generic [`Overlay`]. -#[allow(missing_debug_implementations)] pub struct Element<'a, Message, Theme, Renderer> { overlay: Box + 'a>, } diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs index cb734996..1a4ce672 100644 --- a/core/src/overlay/group.rs +++ b/core/src/overlay/group.rs @@ -7,7 +7,6 @@ use crate::{Clipboard, Event, Layout, Overlay, Shell, Size}; /// An [`Overlay`] container that displays multiple overlay [`overlay::Element`] /// children. -#[allow(missing_debug_implementations)] pub struct Group<'a, Message, Theme, Renderer> { children: Vec>, } diff --git a/core/src/overlay/nested.rs b/core/src/overlay/nested.rs index 25d94c91..2b90a900 100644 --- a/core/src/overlay/nested.rs +++ b/core/src/overlay/nested.rs @@ -7,7 +7,6 @@ use crate::widget; use crate::{Clipboard, Event, Layout, Shell, Size}; /// An overlay container that displays nested overlays -#[allow(missing_debug_implementations)] pub struct Nested<'a, Message, Theme, Renderer> { overlay: overlay::Element<'a, Message, Theme, Renderer>, } diff --git a/core/src/widget/operation.rs b/core/src/widget/operation.rs index 070b369c..9e7b0d34 100644 --- a/core/src/widget/operation.rs +++ b/core/src/widget/operation.rs @@ -256,7 +256,6 @@ where A: 'static, B: 'static, { - #[allow(missing_debug_implementations)] struct Map { operation: O, f: Arc B + Send + Sync>, diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index c1f1e87e..72cb4319 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -55,7 +55,6 @@ pub use text::{Alignment, LineHeight, Shaping, Wrapping}; /// .into() /// } /// ``` -#[allow(missing_debug_implementations)] pub struct Text<'a, Theme, Renderer> where Theme: Catalog, diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index c28bf31a..ca0f739d 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -116,7 +116,6 @@ where } /// The state of the devtools. -#[allow(missing_debug_implementations)] pub struct DevTools

where P: Program, diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs index c5d6df6f..a05d4e0a 100644 --- a/examples/loading_spinners/src/circular.rs +++ b/examples/loading_spinners/src/circular.rs @@ -21,7 +21,6 @@ const MIN_ANGLE: Radians = Radians(PI / 8.0); const WRAP_ANGLE: Radians = Radians(2.0 * PI - PI / 4.0); const BASE_ROTATION_SPEED: u32 = u32::MAX / 80; -#[allow(missing_debug_implementations)] pub struct Circular<'a, Theme> where Theme: StyleSheet, diff --git a/examples/loading_spinners/src/linear.rs b/examples/loading_spinners/src/linear.rs index e9b4a20e..551d0111 100644 --- a/examples/loading_spinners/src/linear.rs +++ b/examples/loading_spinners/src/linear.rs @@ -12,7 +12,6 @@ use super::easing::{self, Easing}; use std::time::Duration; -#[allow(missing_debug_implementations)] pub struct Linear<'a, Theme> where Theme: StyleSheet, diff --git a/graphics/src/geometry/frame.rs b/graphics/src/geometry/frame.rs index 5372ec16..fb7aae82 100644 --- a/graphics/src/geometry/frame.rs +++ b/graphics/src/geometry/frame.rs @@ -3,7 +3,6 @@ use crate::core::{Point, Radians, Rectangle, Size, Vector}; use crate::geometry::{self, Fill, Image, Path, Stroke, Svg, Text}; /// The region of a surface that can be used to draw geometry. -#[allow(missing_debug_implementations)] pub struct Frame where Renderer: geometry::Renderer, diff --git a/graphics/src/geometry/path/builder.rs b/graphics/src/geometry/path/builder.rs index e814a3a7..2f389595 100644 --- a/graphics/src/geometry/path/builder.rs +++ b/graphics/src/geometry/path/builder.rs @@ -10,7 +10,6 @@ use lyon_path::math; /// A [`Path`] builder. /// /// Once a [`Path`] is built, it can no longer be mutated. -#[allow(missing_debug_implementations)] pub struct Builder { raw: builder::WithSvg, } diff --git a/graphics/src/text.rs b/graphics/src/text.rs index 9f932661..74ab6b84 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -134,7 +134,6 @@ pub fn font_system() -> &'static RwLock { } /// A set of system fonts. -#[allow(missing_debug_implementations)] pub struct FontSystem { raw: cosmic_text::FontSystem, loaded_fonts: HashSet, diff --git a/program/src/lib.rs b/program/src/lib.rs index aa51d959..800a1422 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -663,7 +663,6 @@ impl Renderer for T where } /// A particular instance of a running [`Program`]. -#[allow(missing_debug_implementations)] pub struct Instance { program: P, state: P::State, diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index 151a94a7..b63b4747 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -22,7 +22,6 @@ use crate::core::{ /// existing graphical application. /// /// [`integration`]: https://github.com/iced-rs/iced/tree/0.13/examples/integration -#[allow(missing_debug_implementations)] pub struct UserInterface<'a, Message, Theme, Renderer> { root: Element<'a, Message, Theme, Renderer>, base: layout::Node, diff --git a/runtime/src/window.rs b/runtime/src/window.rs index dde540b8..f2ed4e5e 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -15,7 +15,6 @@ pub use raw_window_handle; use raw_window_handle::WindowHandle; /// An operation to be performed on some window. -#[allow(missing_debug_implementations)] pub enum Action { /// Opens a new window with some [`Settings`]. Open(Id, Settings, oneshot::Sender), diff --git a/selector/src/target.rs b/selector/src/target.rs index 0ae0a50f..21c0c38e 100644 --- a/selector/src/target.rs +++ b/selector/src/target.rs @@ -5,7 +5,6 @@ use crate::core::{Rectangle, Vector}; use std::any::Any; #[derive(Clone)] -#[allow(missing_debug_implementations)] pub enum Target<'a> { Container { id: Option<&'a Id>, diff --git a/test/src/emulator.rs b/test/src/emulator.rs index ade88e23..4322343e 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -21,7 +21,6 @@ use crate::{Instruction, Selector}; use std::fmt; -#[allow(missing_debug_implementations)] pub struct Emulator { state: P::State, runtime: Runtime>, Event

>, @@ -35,14 +34,12 @@ pub struct Emulator { pending_tasks: usize, } -#[allow(missing_debug_implementations)] pub enum Event { Action(Action

), Failed(Instruction), Ready, } -#[allow(missing_debug_implementations)] pub enum Action { Runtime(runtime::Action), CountDown, diff --git a/test/src/simulator.rs b/test/src/simulator.rs index 68923dd7..009a2cc8 100644 --- a/test/src/simulator.rs +++ b/test/src/simulator.rs @@ -23,7 +23,6 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; /// A user interface that can be interacted with and inspected programmatically. -#[allow(missing_debug_implementations)] pub struct Simulator< 'a, Message, diff --git a/tester/src/lib.rs b/tester/src/lib.rs index 5c2965cb..16450b4e 100644 --- a/tester/src/lib.rs +++ b/tester/src/lib.rs @@ -91,7 +91,6 @@ where } } -#[allow(missing_debug_implementations)] pub struct Tester { viewport: Size, mode: emulator::Mode, @@ -144,7 +143,6 @@ pub enum Message { Confirm, } -#[allow(missing_debug_implementations)] pub enum Tick { Tester(Message), Program(P::Message), diff --git a/tester/src/recorder.rs b/tester/src/recorder.rs index 6eae0f61..b44b40f1 100644 --- a/tester/src/recorder.rs +++ b/tester/src/recorder.rs @@ -19,7 +19,6 @@ pub fn recorder<'a, Message, Renderer>( Recorder::new(content) } -#[allow(missing_debug_implementations)] pub struct Recorder<'a, Message, Renderer> { content: Element<'a, Message, Theme, Renderer>, on_record: Option Message + 'a>>, diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 02cace72..9ede982d 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -8,13 +8,11 @@ use crate::{Layer, Renderer, Settings}; use std::collections::VecDeque; use std::num::NonZeroU32; -#[allow(missing_debug_implementations)] pub struct Compositor { context: softbuffer::Context>, settings: Settings, } -#[allow(missing_debug_implementations)] pub struct Surface { window: softbuffer::Surface< Box, diff --git a/wgpu/src/engine.rs b/wgpu/src/engine.rs index 5125562c..574223a4 100644 --- a/wgpu/src/engine.rs +++ b/wgpu/src/engine.rs @@ -7,7 +7,6 @@ use crate::triangle; use std::sync::{Arc, RwLock}; #[derive(Clone)] -#[allow(missing_debug_implementations)] pub struct Engine { pub(crate) device: wgpu::Device, pub(crate) queue: wgpu::Queue, diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index 18e26794..872d9fe1 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -92,7 +92,6 @@ impl Cached for Geometry { } /// A frame for drawing some geometry. -#[allow(missing_debug_implementations)] pub struct Frame { clip_bounds: Rectangle, buffers: BufferStack, diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index f49fa1db..c1977fb4 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -72,7 +72,6 @@ use crate::graphics::text::{Editor, Paragraph}; /// /// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs /// [`iced`]: https://github.com/iced-rs/iced -#[allow(missing_debug_implementations)] pub struct Renderer { engine: Engine, diff --git a/wgpu/src/primitive.rs b/wgpu/src/primitive.rs index dd661e7e..cb2c02c7 100644 --- a/wgpu/src/primitive.rs +++ b/wgpu/src/primitive.rs @@ -197,7 +197,6 @@ pub trait Renderer: core::Renderer { /// Stores custom, user-provided types. #[derive(Default)] -#[allow(missing_debug_implementations)] pub struct Storage { pipelines: FxHashMap>, } diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 6fcc8e45..16efa092 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -269,7 +269,6 @@ impl Viewport { } #[derive(Clone)] -#[allow(missing_debug_implementations)] pub struct Pipeline { format: wgpu::TextureFormat, cache: cryoglyph::Cache, diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index c912ef1d..dab96531 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -8,7 +8,6 @@ use crate::settings::{self, Settings}; use crate::{Engine, Renderer}; /// A window graphics backend for iced powered by `wgpu`. -#[allow(missing_debug_implementations)] pub struct Compositor { instance: wgpu::Instance, adapter: wgpu::Adapter, diff --git a/widget/src/button.rs b/widget/src/button.rs index c0e16728..060762a2 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -68,7 +68,6 @@ use crate::core::{ /// button("I am disabled!").into() /// } /// ``` -#[allow(missing_debug_implementations)] pub struct Button<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> where Renderer: crate::core::Renderer, diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 32e81538..80be7180 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -77,7 +77,6 @@ use crate::core::{ /// } /// ``` /// ![Checkbox drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/checkbox.png?raw=true) -#[allow(missing_debug_implementations)] pub struct Checkbox< 'a, Message, diff --git a/widget/src/column.rs b/widget/src/column.rs index ccf49e20..8e9745bc 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -32,7 +32,6 @@ use crate::core::{ /// ].into() /// } /// ``` -#[allow(missing_debug_implementations)] pub struct Column<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> { spacing: f32, diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index cda3c402..dd87c5a1 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -130,7 +130,6 @@ use std::fmt::Display; /// } /// } /// ``` -#[allow(missing_debug_implementations)] pub struct ComboBox< 'a, T, diff --git a/widget/src/container.rs b/widget/src/container.rs index dfe26b21..3e054ea6 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -56,7 +56,6 @@ use crate::core::{ /// .into() /// } /// ``` -#[allow(missing_debug_implementations)] pub struct Container< 'a, Message, diff --git a/widget/src/float.rs b/widget/src/float.rs index 1ff6ab10..c04adee6 100644 --- a/widget/src/float.rs +++ b/widget/src/float.rs @@ -13,7 +13,6 @@ use crate::core::{ }; /// A widget that can make its contents float over other widgets. -#[allow(missing_debug_implementations)] pub struct Float<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> where Theme: Catalog, diff --git a/widget/src/grid.rs b/widget/src/grid.rs index 4b1d9d56..b1d73050 100644 --- a/widget/src/grid.rs +++ b/widget/src/grid.rs @@ -10,7 +10,6 @@ use crate::core::{ }; /// A container that distributes its contents on a responsive grid. -#[allow(missing_debug_implementations)] pub struct Grid<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> { spacing: f32, columns: Constraint, diff --git a/widget/src/image.rs b/widget/src/image.rs index 3b29e4f5..18b7a3a3 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -54,7 +54,6 @@ pub fn viewer(handle: Handle) -> Viewer { /// } /// ``` /// -#[allow(missing_debug_implementations)] pub struct Image { handle: Handle, width: Length, diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 205f84d5..29da5d6d 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -10,7 +10,6 @@ use crate::core::{ }; /// A frame that displays an image with the ability to zoom in/out and pan. -#[allow(missing_debug_implementations)] pub struct Viewer { padding: f32, width: Length, diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs index 45525e4a..bdfa82e1 100644 --- a/widget/src/keyed/column.rs +++ b/widget/src/keyed/column.rs @@ -29,7 +29,6 @@ use crate::core::{ /// })).into() /// } /// ``` -#[allow(missing_debug_implementations)] pub struct Column< 'a, Key, diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 106e239d..85d87f10 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -27,7 +27,6 @@ use std::rc::Rc; /// A widget that only rebuilds its contents when necessary. #[cfg(feature = "lazy")] -#[allow(missing_debug_implementations)] pub struct Lazy<'a, Message, Theme, Renderer, Dependency, View> { dependency: Dependency, view: Box View + 'a>, diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index f4a76d3b..8ca6f882 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -11,7 +11,6 @@ use crate::core::{ }; /// Emit messages on mouse events. -#[allow(missing_debug_implementations)] pub struct MouseArea< 'a, Message, diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index 6f8cc00b..fb2fe9e9 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -17,7 +17,6 @@ use crate::core::{Element, Shell, Widget}; use crate::scrollable::{self, Scrollable}; /// A list of selectable options. -#[allow(missing_debug_implementations)] pub struct Menu< 'a, 'b, diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 5a3c1f70..67cb20d4 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -147,7 +147,6 @@ const THICKNESS_RATIO: f32 = 25.0; /// .into() /// } /// ``` -#[allow(missing_debug_implementations)] pub struct PaneGrid< 'a, Message, diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs index 6592694b..b6f27203 100644 --- a/widget/src/pane_grid/content.rs +++ b/widget/src/pane_grid/content.rs @@ -13,7 +13,6 @@ use crate::pane_grid::{Draggable, TitleBar}; /// The content of a [`Pane`]. /// /// [`Pane`]: super::Pane -#[allow(missing_debug_implementations)] pub struct Content< 'a, Message, diff --git a/widget/src/pane_grid/controls.rs b/widget/src/pane_grid/controls.rs index 13b57acb..4ba32101 100644 --- a/widget/src/pane_grid/controls.rs +++ b/widget/src/pane_grid/controls.rs @@ -4,7 +4,6 @@ use crate::core::{self, Element}; /// The controls of a [`Pane`]. /// /// [`Pane`]: super::Pane -#[allow(missing_debug_implementations)] pub struct Controls< 'a, Message, diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs index 7d15bf80..14a15c5f 100644 --- a/widget/src/pane_grid/title_bar.rs +++ b/widget/src/pane_grid/title_bar.rs @@ -13,7 +13,6 @@ use crate::pane_grid::controls::Controls; /// The title bar of a [`Pane`]. /// /// [`Pane`]: super::Pane -#[allow(missing_debug_implementations)] pub struct TitleBar< 'a, Message, diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 86dbdaf6..06e938ec 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -142,7 +142,6 @@ use std::f32; /// } /// } /// ``` -#[allow(missing_debug_implementations)] pub struct PickList< 'a, T, diff --git a/widget/src/pin.rs b/widget/src/pin.rs index 04ed0324..77df188e 100644 --- a/widget/src/pin.rs +++ b/widget/src/pin.rs @@ -52,7 +52,6 @@ use crate::core::{ /// .into() /// } /// ``` -#[allow(missing_debug_implementations)] pub struct Pin<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> where Renderer: core::Renderer, diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs index d364f5df..25973027 100644 --- a/widget/src/progress_bar.rs +++ b/widget/src/progress_bar.rs @@ -52,7 +52,6 @@ use std::ops::RangeInclusive; /// progress_bar(0.0..=100.0, state.progress).into() /// } /// ``` -#[allow(missing_debug_implementations)] pub struct ProgressBar<'a, Theme = crate::Theme> where Theme: Catalog, diff --git a/widget/src/qr_code.rs b/widget/src/qr_code.rs index e8a278fd..6995cc9f 100644 --- a/widget/src/qr_code.rs +++ b/widget/src/qr_code.rs @@ -60,7 +60,6 @@ const QUIET_ZONE: usize = 2; /// qr_code(&state.data).into() /// } /// ``` -#[allow(missing_debug_implementations)] pub struct QRCode<'a, Theme = crate::Theme> where Theme: Catalog, diff --git a/widget/src/radio.rs b/widget/src/radio.rs index 3bb2e806..17d0426a 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -129,7 +129,6 @@ use crate::core::{ /// column![a, b, c, all].into() /// } /// ``` -#[allow(missing_debug_implementations)] pub struct Radio<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> where Theme: Catalog, diff --git a/widget/src/responsive.rs b/widget/src/responsive.rs index 814f8909..06757fbd 100644 --- a/widget/src/responsive.rs +++ b/widget/src/responsive.rs @@ -14,7 +14,6 @@ use crate::space_x; /// /// A [`Responsive`] widget will always try to fill all the available space of /// its parent. -#[allow(missing_debug_implementations)] pub struct Responsive< 'a, Message, diff --git a/widget/src/row.rs b/widget/src/row.rs index 19c6666a..6dabc926 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -32,7 +32,6 @@ use crate::core::{ /// ].into() /// } /// ``` -#[allow(missing_debug_implementations)] pub struct Row<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> { spacing: f32, padding: Padding, @@ -361,7 +360,6 @@ where /// obtain a [`Row`] that wraps its contents. /// /// The original alignment of the [`Row`] is preserved per row wrapped. -#[allow(missing_debug_implementations)] pub struct Wrapping< 'a, Message, diff --git a/widget/src/rule.rs b/widget/src/rule.rs index b9cee136..5ba2a6f0 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -44,7 +44,6 @@ use crate::core::{ /// rule(2).into() /// } /// ``` -#[allow(missing_debug_implementations)] pub struct Rule<'a, Theme = crate::Theme> where Theme: Catalog, diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 321aeb25..2535ecb8 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -62,7 +62,6 @@ pub use operation::scrollable::{AbsoluteOffset, RelativeOffset}; /// ]).into() /// } /// ``` -#[allow(missing_debug_implementations)] pub struct Scrollable< 'a, Message, diff --git a/widget/src/sensor.rs b/widget/src/sensor.rs index f18a1f1a..ff8a8f12 100644 --- a/widget/src/sensor.rs +++ b/widget/src/sensor.rs @@ -15,7 +15,6 @@ use crate::core::{ /// A widget that can generate messages when its content pops in and out of view. /// /// It can even notify you with anticipation at a given distance! -#[allow(missing_debug_implementations)] pub struct Sensor< 'a, Key, diff --git a/widget/src/shader.rs b/widget/src/shader.rs index cf165e59..7ce06964 100644 --- a/widget/src/shader.rs +++ b/widget/src/shader.rs @@ -22,7 +22,6 @@ pub use primitive::{Primitive, Storage}; /// /// Must be initialized with a [`Program`], which describes the internal widget state & how /// its [`Program::Primitive`]s are drawn. -#[allow(missing_debug_implementations)] pub struct Shader> { width: Length, height: Length, diff --git a/widget/src/slider.rs b/widget/src/slider.rs index db6bc175..2bace845 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -80,7 +80,6 @@ use std::ops::RangeInclusive; /// } /// } /// ``` -#[allow(missing_debug_implementations)] pub struct Slider<'a, T, Message, Theme = crate::Theme> where Theme: Catalog, diff --git a/widget/src/stack.rs b/widget/src/stack.rs index 5b6c8fab..6cfe0311 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -17,7 +17,6 @@ use crate::core::{ /// /// Keep in mind that too much layering will normally produce bad UX as well as /// introduce certain rendering overhead. Use this widget sparingly! -#[allow(missing_debug_implementations)] pub struct Stack<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> { width: Length, diff --git a/widget/src/svg.rs b/widget/src/svg.rs index ab69b2dc..dabe499f 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -51,7 +51,6 @@ pub use crate::core::svg::Handle; /// svg("tiger.svg").into() /// } /// ``` -#[allow(missing_debug_implementations)] pub struct Svg<'a, Theme = crate::Theme> where Theme: Catalog, diff --git a/widget/src/table.rs b/widget/src/table.rs index 06b47124..44faa86d 100644 --- a/widget/src/table.rs +++ b/widget/src/table.rs @@ -49,7 +49,6 @@ where } /// A grid-like visual representation of data distributed in columns and rows. -#[allow(missing_debug_implementations)] pub struct Table<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> where Theme: Catalog, @@ -630,7 +629,6 @@ where } /// A vertical visualization of some data with a header. -#[allow(missing_debug_implementations)] pub struct Column< 'a, 'b, diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs index e11f406b..648ffe82 100644 --- a/widget/src/text/rich.rs +++ b/widget/src/text/rich.rs @@ -13,7 +13,6 @@ use crate::core::{ }; /// A bunch of [`Rich`] text. -#[allow(missing_debug_implementations)] pub struct Rich< 'a, Link, diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 33666935..a8e2676b 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -93,7 +93,6 @@ pub use text::editor::{Action, Edit, Line, LineEnding, Motion}; /// } /// } /// ``` -#[allow(missing_debug_implementations)] pub struct TextEditor< 'a, Highlighter, diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index de47b145..432ff9a5 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -94,7 +94,6 @@ use crate::core::{ /// } /// } /// ``` -#[allow(missing_debug_implementations)] pub struct TextInput< 'a, Message, diff --git a/widget/src/themer.rs b/widget/src/themer.rs index 67b6c65b..ccc03484 100644 --- a/widget/src/themer.rs +++ b/widget/src/themer.rs @@ -15,7 +15,6 @@ use crate::core::{ /// /// This widget can be useful to leverage multiple `Theme` /// types in an application. -#[allow(missing_debug_implementations)] pub struct Themer<'a, Message, Theme, Renderer = crate::Renderer> where Renderer: crate::core::Renderer, diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index 0ad2d22c..7910c210 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -76,7 +76,6 @@ use crate::core::{ /// } /// } /// ``` -#[allow(missing_debug_implementations)] pub struct Toggler< 'a, Message, diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index 3f81270f..9333e1e1 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -56,7 +56,6 @@ use crate::core::{ /// ).into() /// } /// ``` -#[allow(missing_debug_implementations)] pub struct Tooltip< 'a, Message, diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index 8e8ca1c0..35e0217e 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -84,7 +84,6 @@ use crate::core::{ /// } /// } /// ``` -#[allow(missing_debug_implementations)] pub struct VerticalSlider<'a, T, Message, Theme = crate::Theme> where Theme: Catalog, diff --git a/winit/src/clipboard.rs b/winit/src/clipboard.rs index d54a1fe0..b1463fdc 100644 --- a/winit/src/clipboard.rs +++ b/winit/src/clipboard.rs @@ -6,7 +6,6 @@ use winit::window::{Window, WindowId}; /// A buffer for short-term storage and transfer within and between /// applications. -#[allow(missing_debug_implementations)] pub struct Clipboard { state: State, } diff --git a/winit/src/window.rs b/winit/src/window.rs index 419c50e1..a1e8c8b3 100644 --- a/winit/src/window.rs +++ b/winit/src/window.rs @@ -24,7 +24,6 @@ use winit::monitor::MonitorHandle; use std::collections::BTreeMap; use std::sync::Arc; -#[allow(missing_debug_implementations)] pub struct WindowManager where P: Program, @@ -157,7 +156,6 @@ where } } -#[allow(missing_debug_implementations)] pub struct Window where P: Program, From 59e2687146ad276cbc33fb043098e02a414349d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 12 Sep 2025 22:53:28 +0200 Subject: [PATCH 67/83] Write documentation for new `iced_test` APIs --- test/src/emulator.rs | 91 +++++++++++++++++++++++++++++++++++------ test/src/error.rs | 16 +++++++- test/src/ice.rs | 82 +++++++++++++++++++++++++++++++++++-- test/src/instruction.rs | 57 ++++++++++++++++++++++++++ test/src/lib.rs | 11 ++++- test/src/simulator.rs | 4 +- 6 files changed, 240 insertions(+), 21 deletions(-) diff --git a/test/src/emulator.rs b/test/src/emulator.rs index 4322343e..096949e2 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -1,3 +1,4 @@ +//! Run your application in a headless runtime. use crate::core; use crate::core::mouse; use crate::core::renderer; @@ -21,6 +22,15 @@ use crate::{Instruction, Selector}; use std::fmt; +/// A headless runtime that can run iced applications and execute +/// [instructions](crate::Instruction). +/// +/// An [`Emulator`] runs its program as close as possible to the real thing. +/// It will run subscriptions and tasks in the [`Executor`](Program::Executor) of +/// the [`Program`]. +/// +/// If you want to run a simulation without side effects, use a [`Simulator`](crate::Simulator) +/// instead. pub struct Emulator { state: P::State, runtime: Runtime>, Event

>, @@ -34,18 +44,30 @@ pub struct Emulator { pending_tasks: usize, } +/// An emulation event. pub enum Event { + /// An action that must be [performed](Emulator::perform) by the [`Emulator`]. Action(Action

), + /// An [`Instruction`] failed to be executed. Failed(Instruction), + /// The [`Emulator`] is ready. Ready, } -pub enum Action { +/// An action that must be [performed](Emulator::perform) by the [`Emulator`]. +pub struct Action(Action_

); + +enum Action_ { Runtime(runtime::Action), CountDown, } impl Emulator

{ + /// Creates a new [`Emulator`] of the [`Program`] with the given [`Mode`] and [`Size`]. + /// + /// The [`Emulator`] will send [`Event`] notifications through the provided [`mpsc::Sender`]. + /// + /// When the [`Emulator`] has finished booting, an [`Event::Ready`] will be produced. pub fn new( sender: mpsc::Sender>, program: &P, @@ -55,6 +77,10 @@ impl Emulator

{ Self::with_preset(sender, program, mode, size, None) } + /// Creates a new [`Emulator`] analogously to [`new`](Self::new), but it also takes a + /// [`program::Preset`] that will be used as the initial state. + /// + /// When the [`Emulator`] has finished booting, an [`Event::Ready`] will be produced. pub fn with_preset( sender: mpsc::Sender>, program: &P, @@ -106,6 +132,11 @@ impl Emulator

{ emulator } + /// Updates the state of the [`Emulator`] program. + /// + /// This is equivalent to calling the [`Program::update`] function, + /// resubscribing to any subscriptions, and running the resulting tasks + /// concurrently. pub fn update(&mut self, program: &P, message: P::Message) { let task = self .runtime @@ -118,16 +149,24 @@ impl Emulator

{ _ => { if let Some(stream) = task::into_stream(task) { self.runtime.run( - stream.map(Action::Runtime).map(Event::Action).boxed(), + stream + .map(Action_::Runtime) + .map(Action) + .map(Event::Action) + .boxed(), ); } } } } + /// Performs an [`Action`]. + /// + /// Whenever an [`Emulator`] sends an [`Event::Action`], this + /// method must be called to proceed with the execution. pub fn perform(&mut self, program: &P, action: Action

) { - match action { - Action::CountDown => { + match action.0 { + Action_::CountDown => { if self.pending_tasks > 0 { self.pending_tasks -= 1; @@ -136,7 +175,7 @@ impl Emulator

{ } } } - Action::Runtime(action) => match action { + Action_::Runtime(action) => match action { runtime::Action::Output(message) => { self.update(program, message); } @@ -229,6 +268,12 @@ impl Emulator

{ } } + /// Runs an [`Instruction`]. + /// + /// If the [`Instruction`] executes successfully, an [`Event::Ready`] will be + /// produced by the [`Emulator`]. + /// + /// Otherwise, an [`Event::Failed`] will be triggered. pub fn run(&mut self, program: &P, instruction: Instruction) { let mut user_interface = UserInterface::build( program.view(&self.state, self.window), @@ -321,7 +366,7 @@ impl Emulator

{ } } - pub fn wait_for(&mut self, task: Task) { + fn wait_for(&mut self, task: Task) { if let Some(stream) = task::into_stream(task) { match self.mode { Mode::Zen => { @@ -329,10 +374,11 @@ impl Emulator

{ self.runtime.run( stream - .map(Action::Runtime) + .map(Action_::Runtime) + .map(Action) .map(Event::Action) .chain(stream::once(async { - Event::Action(Action::CountDown) + Event::Action(Action(Action_::CountDown)) })) .boxed(), ); @@ -340,7 +386,8 @@ impl Emulator

{ Mode::Patient => { self.runtime.run( stream - .map(Action::Runtime) + .map(Action_::Runtime) + .map(Action) .map(Event::Action) .chain(stream::once(async { Event::Ready })) .boxed(), @@ -348,7 +395,11 @@ impl Emulator

{ } Mode::Impatient => { self.runtime.run( - stream.map(Action::Runtime).map(Event::Action).boxed(), + stream + .map(Action_::Runtime) + .map(Action) + .map(Event::Action) + .boxed(), ); self.runtime.send(Event::Ready); } @@ -358,17 +409,18 @@ impl Emulator

{ } } - pub fn resubscribe(&mut self, program: &P) { + fn resubscribe(&mut self, program: &P) { self.runtime .track(subscription::into_recipes(self.runtime.enter(|| { program.subscription(&self.state).map(|message| { - Event::Action(Action::Runtime(runtime::Action::Output( - message, + Event::Action(Action(Action_::Runtime( + runtime::Action::Output(message), ))) }) }))); } + /// Returns the current view of the [`Emulator`]. pub fn view( &self, program: &P, @@ -376,24 +428,37 @@ impl Emulator

{ program.view(&self.state, self.window) } + /// Returns the current theme of the [`Emulator`]. pub fn theme(&self, program: &P) -> Option { program.theme(&self.state, self.window) } + /// Turns the [`Emulator`] into its internal state. pub fn into_state(self) -> (P::State, core::window::Id) { (self.state, self.window) } } +/// The strategy used by an [`Emulator`] when waiting for tasks to finish. +/// +/// A [`Mode`] can be used to make an [`Emulator`] wait for side effects to finish before +/// continuing execution. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Mode { + /// Waits for all tasks spawned by an [`Instruction`], as well as all tasks indirectly + /// spawned by the the results of those tasks. + /// + /// This is the default. #[default] Zen, + /// Waits only for the tasks directly spawned by an [`Instruction`]. Patient, + /// Never waits for any tasks to finish. Impatient, } impl Mode { + /// A list of all the available modes. pub const ALL: &[Self] = &[Self::Zen, Self::Patient, Self::Impatient]; } diff --git a/test/src/error.rs b/test/src/error.rs index 520fd1c6..96ec47c4 100644 --- a/test/src/error.rs +++ b/test/src/error.rs @@ -10,9 +10,14 @@ use std::sync::Arc; pub enum Error { /// No matching widget was found for the [`Selector`](crate::Selector). #[error("no matching widget was found for the selector: {selector}")] - SelectorNotFound { selector: String }, + SelectorNotFound { + /// A description of the selector. + selector: String, + }, + /// A target matched, but is not visible. #[error("the matching target is not visible: {target:?}")] TargetNotVisible { + /// The target target: Arc, }, /// An IO operation failed. @@ -24,21 +29,30 @@ pub enum Error { /// The encoding of some PNG image failed. #[error("the encoding of some PNG image failed: {0}")] PngEncodingFailed(Arc), + /// The parsing of an [`Ice`](crate::Ice) test failed. #[error("the ice test ({file}) is invalid: {error}")] IceParsingFailed { + /// The path of the test. file: PathBuf, + /// The parse error. error: ice::ParseError, }, + /// The execution of an [`Ice`](crate::Ice) test failed. #[error("the ice test ({file}) failed")] IceTestingFailed { + /// The path of the test. file: PathBuf, + /// The [`Instruction`] that failed. instruction: Instruction, }, + /// The [`Preset`](crate::program::Preset) of a program could not be found. #[error( "the preset \"{name}\" does not exist (available presets: {available:?})" )] PresetNotFound { + /// The name of the [`Preset`](crate::program::Preset). name: String, + /// The available set of presets. available: Vec, }, } diff --git a/test/src/ice.rs b/test/src/ice.rs index ce0cd4a6..2777cf8e 100644 --- a/test/src/ice.rs +++ b/test/src/ice.rs @@ -1,17 +1,59 @@ +//! A shareable, simple format of end-to-end tests. use crate::Instruction; use crate::core::Size; use crate::emulator; use crate::instruction; +/// An end-to-end test for iced applications. +/// +/// Ice tests encode a certain configuration together with a sequence of instructions. +/// An ice test passes if all the instructions can be executed successfully. +/// +/// Normally, ice tests are run by an [`Emulator`](crate::Emulator) in continuous +/// integration pipelines. +/// +/// Ice tests can be easily run by saving them as `.ice` files in a folder and simply +/// calling [`run`](crate::run). These test files can be recorded by enabling the `tester` +/// feature flag in the root crate. #[derive(Debug, Clone, PartialEq)] pub struct Ice { + /// The viewport [`Size`] that must be used for the test. pub viewport: Size, + /// The [`emulator::Mode`] that must be used for the test. pub mode: emulator::Mode, + /// The name of the [`Preset`](crate::program::Preset) that must be used for the test. pub preset: Option, + /// The sequence of instructions of the test. pub instructions: Vec, } impl Ice { + /// Parses an [`Ice`] test from its textual representation. + /// + /// Here is an example of the [`Ice`] test syntax: + /// + /// ```text + /// viewport: 500x800 + /// mode: Impatient + /// preset: Empty + /// ----- + /// click at "What needs to be done?" + /// type "Create the universe" + /// type enter + /// type "Make an apple pie" + /// type enter + /// expect "2 tasks left" + /// click at "Create the universe" + /// expect "1 task left" + /// click at "Make an apple pie" + /// expect "0 tasks left" + /// ``` + /// + /// This syntax is _very_ experimental and extremely likely to change often. + /// For this reason, it is reserved for advanced users that want to early test it. + /// + /// Currently, in order to use it, you will need to earn the right and prove you understand + /// its experimental nature by reading the code! pub fn parse(content: &str) -> Result { let Some((metadata, rest)) = content.split_once("-") else { return Err(ParseError::NoMetadata); @@ -140,32 +182,64 @@ impl std::fmt::Display for Ice { } } +/// An error produced during [`Ice::parse`]. #[derive(Debug, Clone, thiserror::Error)] pub enum ParseError { + /// No metadata is present. #[error("the ice test has no metadata")] NoMetadata, + /// The metadata is invalid. #[error("invalid metadata in line {line}: \"{content}\"")] - InvalidMetadata { line: usize, content: String }, + InvalidMetadata { + /// The number of the invalid line. + line: usize, + /// The content of the invalid line. + content: String, + }, + /// The viewport is invalid. #[error("invalid viewport in line {line}: \"{value}\"")] - InvalidViewport { line: usize, value: String }, + InvalidViewport { + /// The number of the invalid line. + line: usize, + /// The invalid value. + value: String, + }, + + /// The [`emulator::Mode`] is invalid. #[error("invalid mode in line {line}: \"{value}\"")] - InvalidMode { line: usize, value: String }, + InvalidMode { + /// The number of the invalid line. + line: usize, + /// The invalid value. + value: String, + }, + /// A metadata field is unknown. #[error("unknown metadata field in line {line}: \"{field}\"")] - UnknownField { line: usize, field: String }, + UnknownField { + /// The number of the invalid line. + line: usize, + /// The name of the unknown field. + field: String, + }, + /// Viewport metadata is missing. #[error("metadata is missing the viewport field")] MissingViewport, + /// [`emulator::Mode`] metadata is missing. #[error("metadata is missing the mode field")] MissingMode, + /// An [`Instruction`] failed to parse. #[error("invalid instruction in line {line}: {error}")] InvalidInstruction { + /// The number of the invalid line. line: usize, + /// The parse error. error: instruction::ParseError, }, } diff --git a/test/src/instruction.rs b/test/src/instruction.rs index 6059be9c..ca6ed200 100644 --- a/test/src/instruction.rs +++ b/test/src/instruction.rs @@ -1,3 +1,4 @@ +//! A step in an end-to-end test. use crate::core::keyboard; use crate::core::mouse; use crate::core::{Event, Point}; @@ -5,13 +6,19 @@ use crate::simulator; use std::fmt; +/// A step in an end-to-end test. +/// +/// An [`Instruction`] can be run by an [`Emulator`](crate::Emulator). #[derive(Debug, Clone, PartialEq)] pub enum Instruction { + /// A user [`Interaction`]. Interact(Interaction), + /// A testing [`Expectation`]. Expect(Expectation), } impl Instruction { + /// Parses an [`Instruction`] from its textual representation. pub fn parse(line: &str) -> Result { parser::run(line) } @@ -26,13 +33,19 @@ impl fmt::Display for Instruction { } } +/// A user interaction. #[derive(Debug, Clone, PartialEq)] pub enum Interaction { + /// A mouse interaction. Mouse(Mouse), + /// A keyboard interaction. Keyboard(Keyboard), } impl Interaction { + /// Creates an [`Interaction`] from a runtime [`Event`]. + /// + /// This can be useful for recording tests during real usage. pub fn from_event(event: &Event) -> Option { Some(match event { Event::Mouse(mouse) => Self::Mouse(match mouse { @@ -86,6 +99,17 @@ impl Interaction { }) } + /// Merges two interactions together, if possible. + /// + /// This method can turn certain sequences of interactions into a single one. + /// For instance, a mouse movement, left button press, and left button release + /// can all be merged into a single click interaction. + /// + /// Merging is lossy and, therefore, it is not always desirable if you are recording + /// a test and want full reproducibility. + /// + /// If the interactions cannot be merged, the `next` interaction will be + /// returned as the second element of the tuple. pub fn merge(self, next: Self) -> (Self, Option) { match (self, next) { (Self::Mouse(current), Self::Mouse(next)) => { @@ -185,6 +209,10 @@ impl Interaction { } } + /// Returns a list of runtime events representing the [`Interaction`]. + /// + /// The `find_target` closure must convert a [`Target`] into its screen + /// coordinates. pub fn events( &self, find_target: impl FnOnce(&Target) -> Option, @@ -256,19 +284,30 @@ impl fmt::Display for Interaction { } } +/// A mouse interaction. #[derive(Debug, Clone, PartialEq)] pub enum Mouse { + /// The mouse was moved. Move(Target), + /// A button was pressed. Press { + /// The button. button: mouse::Button, + /// The location of the press. at: Option, }, + /// A button was released. Release { + /// The button. button: mouse::Button, + /// The location of the release. at: Option, }, + /// A button was clicked. Click { + /// The button. button: mouse::Button, + /// The location of the click. at: Option, }, } @@ -292,9 +331,12 @@ impl fmt::Display for Mouse { } } +/// The target of an interaction. #[derive(Debug, Clone, PartialEq)] pub enum Target { + /// A specific point of the viewport. Point(Point), + /// A UI element containing the given text. Text(String), } @@ -307,11 +349,16 @@ impl fmt::Display for Target { } } +/// A keyboard interaction. #[derive(Debug, Clone, PartialEq)] pub enum Keyboard { + /// A key was pressed. Press(Key), + /// A key was release. Release(Key), + /// A key was "typed" (press and released). Type(Key), + /// A bunch of text was typed. Typewrite(String), } @@ -334,7 +381,11 @@ impl fmt::Display for Keyboard { } } +/// A keyboard key. +/// +/// Only a small subset of keys is supported currently! #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(missing_docs)] pub enum Key { Enter, Escape, @@ -399,8 +450,13 @@ mod format { } } +/// A testing assertion. +/// +/// Expectations are instructions that verify the current state of +/// the user interface of an application. #[derive(Debug, Clone, PartialEq)] pub enum Expectation { + /// Expect some element to contain some text. Text(String), } @@ -432,6 +488,7 @@ mod parser { use nom::sequence::{delimited, preceded, separated_pair}; use nom::{Finish, IResult, Parser}; + /// A parsing error. #[derive(Debug, Clone, thiserror::Error)] #[error("parse error: {0}")] pub struct Error(nom::error::Error); diff --git a/test/src/lib.rs b/test/src/lib.rs index 1cf8d81f..5879fb04 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -28,7 +28,7 @@ //! # //! # let mut counter = Counter { value: 0 }; //! # let mut ui = simulator(counter.view()); -//! +//! # //! let _ = ui.click("+"); //! let _ = ui.click("+"); //! let _ = ui.click("-"); @@ -83,7 +83,6 @@ //! [`typewrite`](Simulator::typewrite)—and even perform [_snapshot testing_](Simulator::snapshot)! //! //! [the classical counter interface]: https://book.iced.rs/architecture.html#dissecting-an-interface -#![allow(missing_docs)] pub use iced_program as program; pub use iced_renderer as renderer; pub use iced_runtime as runtime; @@ -107,6 +106,14 @@ pub use simulator::{Simulator, simulator}; use std::path::Path; +/// Runs an [`Ice`] test suite for the given [`Program`](program::Program). +/// +/// Any `.ice` tests will be parsed from the given directory and executed in +/// an [`Emulator`] of the given [`Program`](program::Program). +/// +/// Remember that an [`Emulator`] executes the real thing! Side effects _will_ +/// be performed. It is up to you to ensure your tests have reproducible environments +/// by leveraging [`Preset`][program::Preset]. pub fn run( program: impl program::Program + 'static, tests_dir: impl AsRef, diff --git a/test/src/simulator.rs b/test/src/simulator.rs index 009a2cc8..b578e107 100644 --- a/test/src/simulator.rs +++ b/test/src/simulator.rs @@ -1,4 +1,4 @@ -//! Run a simulation of your application. +//! Run a simulation of your application without side effects. use crate::core; use crate::core::clipboard; use crate::core::event; @@ -366,6 +366,7 @@ pub fn click() -> impl Iterator { .into_iter() } +/// Returns the sequence of events of a key press. pub fn press_key( key: impl Into, text: Option, @@ -384,6 +385,7 @@ pub fn press_key( }) } +/// Returns the sequence of events of a key release. pub fn release_key(key: impl Into) -> Event { let key = key.into(); From 299eb54d6f14d79c659cd1c0ad8a8d3d1d258e41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 17 Sep 2025 22:56:58 +0200 Subject: [PATCH 68/83] Improve naming in `iced_selector` crate --- core/src/widget/id.rs | 8 +- examples/todos/src/main.rs | 2 +- examples/visible_bounds/src/main.rs | 22 +- runtime/src/widget/selector.rs | 6 +- selector/src/find.rs | 21 +- selector/src/lib.rs | 43 ++-- selector/src/target.rs | 334 +++++++++++++++------------- test/src/emulator.rs | 6 +- test/src/instruction.rs | 2 +- test/src/lib.rs | 2 +- test/src/simulator.rs | 2 +- tester/src/recorder.rs | 6 +- 12 files changed, 235 insertions(+), 219 deletions(-) diff --git a/core/src/widget/id.rs b/core/src/widget/id.rs index 2e7331c0..1d67086e 100644 --- a/core/src/widget/id.rs +++ b/core/src/widget/id.rs @@ -8,9 +8,9 @@ static NEXT_ID: AtomicUsize = AtomicUsize::new(0); pub struct Id(Internal); impl Id { - /// Creates a custom [`Id`]. - pub fn new(id: impl Into>) -> Self { - Self(Internal::Custom(id.into())) + /// Creates a new [`Id`] from a static `str`. + pub const fn new(id: &'static str) -> Self { + Self(Internal::Custom(borrow::Cow::Borrowed(id))) } /// Creates a unique [`Id`]. @@ -31,7 +31,7 @@ impl From<&'static str> for Id { impl From for Id { fn from(value: String) -> Self { - Self::new(value) + Self(Internal::Custom(borrow::Cow::Owned(value))) } } diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 1aaaf614..6f4e0808 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -307,7 +307,7 @@ pub enum TaskMessage { impl Task { fn text_input_id(i: usize) -> widget::Id { - widget::Id::new(format!("task-{i}")) + widget::Id::from(format!("task-{i}")) } fn new(description: String) -> Self { diff --git a/examples/visible_bounds/src/main.rs b/examples/visible_bounds/src/main.rs index 7a20ba86..4025dcf4 100644 --- a/examples/visible_bounds/src/main.rs +++ b/examples/visible_bounds/src/main.rs @@ -1,7 +1,7 @@ use iced::event::{self, Event}; use iced::mouse; use iced::widget::{ - column, container, row, scrollable, selector, space_x, space_y, text, + self, column, container, row, scrollable, selector, space_x, space_y, text, }; use iced::window; use iced::{ @@ -28,8 +28,8 @@ enum Message { MouseMoved(Point), WindowResized, Scrolled, - OuterFound(Option), - InnerFound(Option), + OuterFound(Option), + InnerFound(Option), } impl Example { @@ -41,18 +41,18 @@ impl Example { Task::none() } Message::Scrolled | Message::WindowResized => Task::batch(vec![ - selector::find_by_id(OUTER_CONTAINER).map(Message::OuterFound), - selector::find_by_id(INNER_CONTAINER).map(Message::InnerFound), + selector::visible_bounds(OUTER_CONTAINER) + .map(Message::OuterFound), + selector::visible_bounds(INNER_CONTAINER) + .map(Message::InnerFound), ]), Message::OuterFound(outer) => { - self.outer_bounds = - outer.as_ref().and_then(selector::Bounded::visible_bounds); + self.outer_bounds = outer; Task::none() } Message::InnerFound(inner) => { - self.inner_bounds = - inner.as_ref().and_then(selector::Bounded::visible_bounds); + self.inner_bounds = inner; Task::none() } @@ -157,5 +157,5 @@ impl Example { } } -const OUTER_CONTAINER: &str = "outer"; -const INNER_CONTAINER: &str = "inner"; +const OUTER_CONTAINER: widget::Id = widget::Id::new("outer"); +const INNER_CONTAINER: widget::Id = widget::Id::new("inner"); diff --git a/runtime/src/widget/selector.rs b/runtime/src/widget/selector.rs index fa7ff749..ee142246 100644 --- a/runtime/src/widget/selector.rs +++ b/runtime/src/widget/selector.rs @@ -1,7 +1,5 @@ //! Find and query widgets in your applications. -pub use iced_selector::Selector; - -pub use iced_selector::target::{Bounded, Match, Target, Text}; +pub use iced_selector::{Bounded, Candidate, Selector, Target, Text}; use crate::core::Rectangle; @@ -10,7 +8,7 @@ use crate::core::widget; use crate::task; /// Finds a widget by the given [`widget::Id`]. -pub fn find_by_id(id: impl Into) -> Task> { +pub fn find_by_id(id: impl Into) -> Task> { task::widget(id.into().find()) } diff --git a/selector/src/find.rs b/selector/src/find.rs index 15016933..eb3f0fa0 100644 --- a/selector/src/find.rs +++ b/selector/src/find.rs @@ -1,9 +1,10 @@ +use crate::Selector; use crate::core::widget::operation::{ Focusable, Outcome, Scrollable, TextInput, }; use crate::core::widget::{Id, Operation}; use crate::core::{Rectangle, Vector}; -use crate::{Selector, Target}; +use crate::target::Candidate; use std::any::Any; @@ -38,7 +39,7 @@ where { type Output = Option; - fn feed(&mut self, target: Target<'_>) { + fn feed(&mut self, target: Candidate<'_>) { if let Some(output) = self.selector.select(target) { self.output = Some(output); } @@ -81,7 +82,7 @@ where { type Output = Vec; - fn feed(&mut self, target: Target<'_>) { + fn feed(&mut self, target: Candidate<'_>) { if let Some(output) = self.selector.select(target) { self.outputs.push(output); } @@ -99,7 +100,7 @@ where pub trait Strategy { type Output; - fn feed(&mut self, target: Target<'_>); + fn feed(&mut self, target: Candidate<'_>); fn is_done(&self) -> bool; @@ -152,7 +153,7 @@ where return; } - self.strategy.feed(Target::Container { + self.strategy.feed(Candidate::Container { id, bounds, visible_bounds: self @@ -171,7 +172,7 @@ where return; } - self.strategy.feed(Target::Focusable { + self.strategy.feed(Candidate::Focusable { id, bounds, visible_bounds: self @@ -196,7 +197,7 @@ where let visible_bounds = self.viewport.intersection(&(bounds + self.translation)); - self.strategy.feed(Target::Scrollable { + self.strategy.feed(Candidate::Scrollable { id, bounds, visible_bounds, @@ -219,7 +220,7 @@ where return; } - self.strategy.feed(Target::TextInput { + self.strategy.feed(Candidate::TextInput { id, bounds, visible_bounds: self @@ -234,7 +235,7 @@ where return; } - self.strategy.feed(Target::Text { + self.strategy.feed(Candidate::Text { id, bounds, visible_bounds: self @@ -254,7 +255,7 @@ where return; } - self.strategy.feed(Target::Custom { + self.strategy.feed(Candidate::Custom { id, bounds, visible_bounds: self diff --git a/selector/src/lib.rs b/selector/src/lib.rs index 050d6a49..4e56172b 100644 --- a/selector/src/lib.rs +++ b/selector/src/lib.rs @@ -1,12 +1,11 @@ #![allow(missing_docs)] use iced_core as core; -pub mod target; - mod find; +mod target; pub use find::{Find, FindAll}; -pub use target::Target; +pub use target::{Bounded, Candidate, Target, Text}; use crate::core::Point; use crate::core::widget; @@ -14,7 +13,7 @@ use crate::core::widget; pub trait Selector { type Output; - fn select(&mut self, target: Target<'_>) -> Option; + fn select(&mut self, candidate: Candidate<'_>) -> Option; fn description(&self) -> String; @@ -36,9 +35,9 @@ pub trait Selector { impl Selector for &str { type Output = target::Text; - fn select(&mut self, target: Target<'_>) -> Option { - match target { - Target::TextInput { + fn select(&mut self, candidate: Candidate<'_>) -> Option { + match candidate { + Candidate::TextInput { id, bounds, visible_bounds, @@ -48,7 +47,7 @@ impl Selector for &str { bounds, visible_bounds, }), - Target::Text { + Candidate::Text { id, bounds, visible_bounds, @@ -70,8 +69,8 @@ impl Selector for &str { impl Selector for String { type Output = target::Text; - fn select(&mut self, target: Target<'_>) -> Option { - self.as_str().select(target) + fn select(&mut self, candidate: Candidate<'_>) -> Option { + self.as_str().select(candidate) } fn description(&self) -> String { @@ -80,14 +79,14 @@ impl Selector for String { } impl Selector for widget::Id { - type Output = target::Match; + type Output = Target; - fn select(&mut self, target: Target<'_>) -> Option { - if target.id() != Some(self) { + fn select(&mut self, candidate: Candidate<'_>) -> Option { + if candidate.id() != Some(self) { return None; } - Some(target::Match::from_target(target)) + Some(Target::from(candidate)) } fn description(&self) -> String { @@ -96,13 +95,13 @@ impl Selector for widget::Id { } impl Selector for Point { - type Output = target::Match; + type Output = Target; - fn select(&mut self, target: Target<'_>) -> Option { - target + fn select(&mut self, candidate: Candidate<'_>) -> Option { + candidate .visible_bounds() .is_some_and(|visible_bounds| visible_bounds.contains(*self)) - .then(|| target::Match::from_target(target)) + .then(|| Target::from(candidate)) } fn description(&self) -> String { @@ -112,12 +111,12 @@ impl Selector for Point { impl Selector for F where - F: FnMut(Target<'_>) -> Option, + F: FnMut(Candidate<'_>) -> Option, { type Output = T; - fn select(&mut self, target: Target<'_>) -> Option { - (self)(target) + fn select(&mut self, candidate: Candidate<'_>) -> Option { + (self)(candidate) } fn description(&self) -> String { @@ -126,6 +125,6 @@ where } /// Creates a new [`Selector`] that matches widgets with the given [`widget::Id`]. -pub fn id(id: impl Into) -> impl Selector { +pub fn id(id: impl Into) -> impl Selector { id.into() } diff --git a/selector/src/target.rs b/selector/src/target.rs index 21c0c38e..cd0c4bba 100644 --- a/selector/src/target.rs +++ b/selector/src/target.rs @@ -4,8 +4,152 @@ use crate::core::{Rectangle, Vector}; use std::any::Any; +#[derive(Debug, Clone, PartialEq)] +pub enum Target { + Container { + id: Option, + bounds: Rectangle, + visible_bounds: Option, + }, + Focusable { + id: Option, + bounds: Rectangle, + visible_bounds: Option, + }, + Scrollable { + id: Option, + bounds: Rectangle, + visible_bounds: Option, + content_bounds: Rectangle, + translation: Vector, + }, + TextInput { + id: Option, + bounds: Rectangle, + visible_bounds: Option, + content: String, + }, + Text { + id: Option, + bounds: Rectangle, + visible_bounds: Option, + content: String, + }, + Custom { + id: Option, + bounds: Rectangle, + visible_bounds: Option, + }, +} + +impl Target { + pub fn bounds(&self) -> Rectangle { + match self { + Target::Container { bounds, .. } + | Target::Focusable { bounds, .. } + | Target::Scrollable { bounds, .. } + | Target::TextInput { bounds, .. } + | Target::Text { bounds, .. } + | Target::Custom { bounds, .. } => *bounds, + } + } + + pub fn visible_bounds(&self) -> Option { + match self { + Target::Container { visible_bounds, .. } + | Target::Focusable { visible_bounds, .. } + | Target::Scrollable { visible_bounds, .. } + | Target::TextInput { visible_bounds, .. } + | Target::Text { visible_bounds, .. } + | Target::Custom { visible_bounds, .. } => *visible_bounds, + } + } +} + +impl From> for Target { + fn from(candidate: Candidate<'_>) -> Self { + match candidate { + Candidate::Container { + id, + bounds, + visible_bounds, + } => Self::Container { + id: id.cloned(), + bounds, + visible_bounds, + }, + Candidate::Focusable { + id, + bounds, + visible_bounds, + .. + } => Self::Focusable { + id: id.cloned(), + bounds, + visible_bounds, + }, + Candidate::Scrollable { + id, + bounds, + visible_bounds, + content_bounds, + translation, + .. + } => Self::Scrollable { + id: id.cloned(), + bounds, + visible_bounds, + content_bounds, + translation, + }, + Candidate::TextInput { + id, + bounds, + visible_bounds, + state, + } => Self::TextInput { + id: id.cloned(), + bounds, + visible_bounds, + content: state.text().to_owned(), + }, + Candidate::Text { + id, + bounds, + visible_bounds, + content, + } => Self::Text { + id: id.cloned(), + bounds, + visible_bounds, + content: content.to_owned(), + }, + Candidate::Custom { + id, + bounds, + visible_bounds, + .. + } => Self::Custom { + id: id.cloned(), + bounds, + visible_bounds, + }, + } + } +} + +impl Bounded for Target { + fn bounds(&self) -> Rectangle { + self.bounds() + } + + fn visible_bounds(&self) -> Option { + self.visible_bounds() + } +} + #[derive(Clone)] -pub enum Target<'a> { +pub enum Candidate<'a> { Container { id: Option<&'a Id>, bounds: Rectangle, @@ -45,171 +189,37 @@ pub enum Target<'a> { }, } -impl<'a> Target<'a> { +impl<'a> Candidate<'a> { pub fn id(&self) -> Option<&'a Id> { match self { - Target::Container { id, .. } - | Target::Focusable { id, .. } - | Target::Scrollable { id, .. } - | Target::TextInput { id, .. } - | Target::Text { id, .. } - | Target::Custom { id, .. } => *id, + Candidate::Container { id, .. } + | Candidate::Focusable { id, .. } + | Candidate::Scrollable { id, .. } + | Candidate::TextInput { id, .. } + | Candidate::Text { id, .. } + | Candidate::Custom { id, .. } => *id, } } pub fn bounds(&self) -> Rectangle { match self { - Target::Container { bounds, .. } - | Target::Focusable { bounds, .. } - | Target::Scrollable { bounds, .. } - | Target::TextInput { bounds, .. } - | Target::Text { bounds, .. } - | Target::Custom { bounds, .. } => *bounds, + Candidate::Container { bounds, .. } + | Candidate::Focusable { bounds, .. } + | Candidate::Scrollable { bounds, .. } + | Candidate::TextInput { bounds, .. } + | Candidate::Text { bounds, .. } + | Candidate::Custom { bounds, .. } => *bounds, } } pub fn visible_bounds(&self) -> Option { match self { - Target::Container { visible_bounds, .. } - | Target::Focusable { visible_bounds, .. } - | Target::Scrollable { visible_bounds, .. } - | Target::TextInput { visible_bounds, .. } - | Target::Text { visible_bounds, .. } - | Target::Custom { visible_bounds, .. } => *visible_bounds, - } - } -} - -#[derive(Debug, Clone, PartialEq)] -pub enum Match { - Container { - id: Option, - bounds: Rectangle, - visible_bounds: Option, - }, - Focusable { - id: Option, - bounds: Rectangle, - visible_bounds: Option, - }, - Scrollable { - id: Option, - bounds: Rectangle, - visible_bounds: Option, - content_bounds: Rectangle, - translation: Vector, - }, - TextInput { - id: Option, - bounds: Rectangle, - visible_bounds: Option, - content: String, - }, - Text { - id: Option, - bounds: Rectangle, - visible_bounds: Option, - content: String, - }, - Custom { - id: Option, - bounds: Rectangle, - visible_bounds: Option, - }, -} - -impl Match { - pub fn from_target(target: Target<'_>) -> Self { - match target { - Target::Container { - id, - bounds, - visible_bounds, - } => Self::Container { - id: id.cloned(), - bounds, - visible_bounds, - }, - Target::Focusable { - id, - bounds, - visible_bounds, - .. - } => Self::Focusable { - id: id.cloned(), - bounds, - visible_bounds, - }, - Target::Scrollable { - id, - bounds, - visible_bounds, - content_bounds, - translation, - .. - } => Self::Scrollable { - id: id.cloned(), - bounds, - visible_bounds, - content_bounds, - translation, - }, - Target::TextInput { - id, - bounds, - visible_bounds, - state, - } => Self::TextInput { - id: id.cloned(), - bounds, - visible_bounds, - content: state.text().to_owned(), - }, - Target::Text { - id, - bounds, - visible_bounds, - content, - } => Self::Text { - id: id.cloned(), - bounds, - visible_bounds, - content: content.to_owned(), - }, - Target::Custom { - id, - bounds, - visible_bounds, - .. - } => Self::Custom { - id: id.cloned(), - bounds, - visible_bounds, - }, - } - } -} - -impl Bounded for Match { - fn bounds(&self) -> Rectangle { - match self { - Match::Container { bounds, .. } - | Match::Focusable { bounds, .. } - | Match::Scrollable { bounds, .. } - | Match::TextInput { bounds, .. } - | Match::Text { bounds, .. } - | Match::Custom { bounds, .. } => *bounds, - } - } - - fn visible_bounds(&self) -> Option { - match self { - Match::Container { visible_bounds, .. } - | Match::Focusable { visible_bounds, .. } - | Match::Scrollable { visible_bounds, .. } - | Match::TextInput { visible_bounds, .. } - | Match::Text { visible_bounds, .. } - | Match::Custom { visible_bounds, .. } => *visible_bounds, + Candidate::Container { visible_bounds, .. } + | Candidate::Focusable { visible_bounds, .. } + | Candidate::Scrollable { visible_bounds, .. } + | Candidate::TextInput { visible_bounds, .. } + | Candidate::Text { visible_bounds, .. } + | Candidate::Custom { visible_bounds, .. } => *visible_bounds, } } } @@ -234,17 +244,27 @@ pub enum Text { }, } -impl Bounded for Text { - fn bounds(&self) -> Rectangle { +impl Text { + pub fn bounds(&self) -> Rectangle { match self { Text::Raw { bounds, .. } | Text::Input { bounds, .. } => *bounds, } } - fn visible_bounds(&self) -> Option { + pub fn visible_bounds(&self) -> Option { match self { Text::Raw { visible_bounds, .. } | Text::Input { visible_bounds, .. } => *visible_bounds, } } } + +impl Bounded for Text { + fn bounds(&self) -> Rectangle { + self.bounds() + } + + fn visible_bounds(&self) -> Option { + self.visible_bounds() + } +} diff --git a/test/src/emulator.rs b/test/src/emulator.rs index 096949e2..5f46caf2 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -17,7 +17,6 @@ use crate::runtime::task; use crate::runtime::user_interface; use crate::runtime::window; use crate::runtime::{Task, UserInterface}; -use crate::selector; use crate::{Instruction, Selector}; use std::fmt; @@ -25,8 +24,8 @@ use std::fmt; /// A headless runtime that can run iced applications and execute /// [instructions](crate::Instruction). /// -/// An [`Emulator`] runs its program as close as possible to the real thing. -/// It will run subscriptions and tasks in the [`Executor`](Program::Executor) of +/// An [`Emulator`] runs its program as faithfully as possible to the real thing. +/// It will run subscriptions and tasks with the [`Executor`](Program::Executor) of /// the [`Program`]. /// /// If you want to run a simulation without side effects, use a [`Simulator`](crate::Simulator) @@ -289,7 +288,6 @@ impl Emulator

{ let Some(events) = interaction.events(|target| match target { instruction::Target::Point(position) => Some(*position), instruction::Target::Text(text) => { - use selector::target::Bounded; use widget::Operation; let mut operation = Selector::find(text.as_str()); diff --git a/test/src/instruction.rs b/test/src/instruction.rs index ca6ed200..c9feeb40 100644 --- a/test/src/instruction.rs +++ b/test/src/instruction.rs @@ -354,7 +354,7 @@ impl fmt::Display for Target { pub enum Keyboard { /// A key was pressed. Press(Key), - /// A key was release. + /// A key was released. Release(Key), /// A key was "typed" (press and released). Type(Key), diff --git a/test/src/lib.rs b/test/src/lib.rs index 5879fb04..ba5c8c9c 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -112,7 +112,7 @@ use std::path::Path; /// an [`Emulator`] of the given [`Program`](program::Program). /// /// Remember that an [`Emulator`] executes the real thing! Side effects _will_ -/// be performed. It is up to you to ensure your tests have reproducible environments +/// take place. It is up to you to ensure your tests have reproducible environments /// by leveraging [`Preset`][program::Preset]. pub fn run( program: impl program::Program + 'static, diff --git a/test/src/simulator.rs b/test/src/simulator.rs index b578e107..b0a9a5d1 100644 --- a/test/src/simulator.rs +++ b/test/src/simulator.rs @@ -12,7 +12,7 @@ use crate::core::{Element, Event, Font, Point, Settings, Size, SmolStr}; use crate::renderer; use crate::runtime::UserInterface; use crate::runtime::user_interface; -use crate::selector::target::Bounded; +use crate::selector::Bounded; use crate::{Error, Selector}; use std::borrow::Cow; diff --git a/tester/src/recorder.rs b/tester/src/recorder.rs index b44b40f1..03a161e4 100644 --- a/tester/src/recorder.rs +++ b/tester/src/recorder.rs @@ -11,7 +11,7 @@ use crate::core::{ }; use crate::test::Selector; use crate::test::instruction::{Interaction, Mouse, Target}; -use crate::test::selector::target; +use crate::test::selector; pub fn recorder<'a, Message, Renderer>( content: impl Into>, @@ -457,12 +457,12 @@ fn find_text( let (content, visible_bounds) = targets.into_iter().rev().find_map(|target| { - if let target::Match::Text { + if let selector::Target::Text { content, visible_bounds, .. } - | target::Match::TextInput { + | selector::Target::TextInput { content, visible_bounds, .. From 611f89fc5956d51b54253a989ff023e56a936311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 17 Sep 2025 23:49:01 +0200 Subject: [PATCH 69/83] Rename `space_{x,y}` to `space::{horizontal,vertical}` --- devtools/src/lib.rs | 8 ++--- examples/bezier_tool/src/main.rs | 4 +-- examples/combo_box/src/main.rs | 4 +-- examples/editor/src/main.rs | 6 ++-- examples/gallery/src/civitai.rs | 2 +- examples/gallery/src/main.rs | 47 ++++++++++++++--------------- examples/gradient/src/main.rs | 4 +-- examples/layout/src/main.rs | 32 +++++++++++--------- examples/lazy/src/main.rs | 4 +-- examples/markdown/src/main.rs | 4 +-- examples/modal/src/main.rs | 14 ++++++--- examples/multi_window/src/main.rs | 6 ++-- examples/pick_list/src/main.rs | 6 ++-- examples/scrollable/src/main.rs | 22 +++++++------- examples/styling/src/main.rs | 4 +-- examples/toast/src/main.rs | 4 +-- examples/tour/src/main.rs | 8 ++--- examples/vectorial_text/src/main.rs | 4 +-- examples/visible_bounds/src/main.rs | 12 ++++---- tester/src/lib.rs | 8 ++--- widget/src/helpers.rs | 22 +++++--------- widget/src/lib.rs | 2 +- widget/src/responsive.rs | 4 +-- widget/src/scrollable.rs | 8 ++--- widget/src/space.rs | 46 ++++++++++++++++------------ 25 files changed, 143 insertions(+), 142 deletions(-) diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index ca0f739d..c806912d 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -24,7 +24,7 @@ use crate::runtime::task::{self, Task}; use crate::time_machine::TimeMachine; use crate::widget::{ bottom_right, button, center, column, container, opaque, row, scrollable, - space_x, stack, text, themer, + space, stack, text, themer, }; use std::fmt; @@ -447,7 +447,7 @@ where .width(100) .on_press(Message::CancelSetup) .style(button::danger), - space_x(), + space::horizontal(), button( text(match goal { Goal::Installation => "Install", @@ -497,13 +497,13 @@ where let comparison = column![ row![ "Installed revision:", - space_x(), + space::horizontal(), inline_code(revision.as_deref().unwrap_or("Unknown")) ] .align_y(Center), row![ "Compatible revision:", - space_x(), + space::horizontal(), inline_code(comet::COMPATIBLE_REVISION), ] .align_y(Center) diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 0f006821..2523924a 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -1,5 +1,5 @@ //! This example showcases an interactive `Canvas` for drawing Bézier curves. -use iced::widget::{button, container, hover, right, space_x}; +use iced::widget::{button, container, hover, right, space}; use iced::{Element, Theme}; pub fn main() -> iced::Result { @@ -38,7 +38,7 @@ impl Example { container(hover( self.bezier.view(&self.curves).map(Message::AddCurve), if self.curves.is_empty() { - container(space_x()) + container(space::horizontal()) } else { right( button("Clear") diff --git a/examples/combo_box/src/main.rs b/examples/combo_box/src/main.rs index cc0e7fc0..fc16d88b 100644 --- a/examples/combo_box/src/main.rs +++ b/examples/combo_box/src/main.rs @@ -1,4 +1,4 @@ -use iced::widget::{center, column, combo_box, scrollable, space_y, text}; +use iced::widget::{center, column, combo_box, scrollable, space, text}; use iced::{Center, Element, Fill}; pub fn main() -> iced::Result { @@ -60,7 +60,7 @@ impl Example { text(&self.text), "What is your language?", combo_box, - space_y().height(150), + space().height(150), ] .width(Fill) .align_x(Center) diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 6a987030..b854275f 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -1,7 +1,7 @@ use iced::highlighter; use iced::keyboard; use iced::widget::{ - button, center_x, column, container, operation, pick_list, row, space_x, + button, center_x, column, container, operation, pick_list, row, space, text, text_editor, toggler, tooltip, }; use iced::{Center, Element, Fill, Font, Task, Theme}; @@ -157,7 +157,7 @@ impl Editor { "Save file", self.is_dirty.then_some(Message::SaveFile) ), - space_x(), + space::horizontal(), toggler(self.word_wrap) .label("Word Wrap") .on_toggle(Message::WordWrapToggled), @@ -184,7 +184,7 @@ impl Editor { } else { String::from("New file") }), - space_x(), + space::horizontal(), text({ let (line, column) = self.content.cursor_position(); diff --git a/examples/gallery/src/civitai.rs b/examples/gallery/src/civitai.rs index b24b1031..97f0ad89 100644 --- a/examples/gallery/src/civitai.rs +++ b/examples/gallery/src/civitai.rs @@ -15,7 +15,7 @@ pub struct Image { } impl Image { - pub const LIMIT: usize = 99; + pub const LIMIT: usize = 96; pub async fn list() -> Result, Error> { let client = reqwest::Client::new(); diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs index 529c04d4..182fd606 100644 --- a/examples/gallery/src/main.rs +++ b/examples/gallery/src/main.rs @@ -10,7 +10,7 @@ use iced::animation; use iced::time::{Instant, milliseconds}; use iced::widget::{ button, container, float, grid, image, mouse_area, opaque, scrollable, - sensor, space_x, stack, + sensor, space, stack, }; use iced::window; use iced::{ @@ -227,7 +227,7 @@ fn card<'a>( }) .into() } else { - space_x().into() + space::horizontal().into() }; if let Some(blurhash) = preview.blurhash(now) { @@ -241,7 +241,7 @@ fn card<'a>( thumbnail } } else { - space_x().into() + space::horizontal().into() }; let card = mouse_area(container(image).style(container::dark)) @@ -264,7 +264,7 @@ fn card<'a>( } fn placeholder<'a>() -> Element<'a, Message> { - container(space_x()).style(container::dark).into() + container(space()).style(container::dark).into() } enum Preview { @@ -415,35 +415,32 @@ impl Viewer { || self.image_fade_in.is_animating(now) } - fn view(&self, now: Instant) -> Element<'_, Message> { + fn view(&self, now: Instant) -> Option> { let opacity = self.background_fade_in.interpolate(0.0, 0.8, now); - let image: Element<'_, _> = if let Some(handle) = &self.image { + if opacity <= 0.0 { + return None; + } + + let image = self.image.as_ref().map(|handle| { image(handle) .width(Fill) .height(Fill) .opacity(self.image_fade_in.interpolate(0.0, 1.0, now)) .scale(self.image_fade_in.interpolate(1.5, 1.0, now)) - .into() - } else { - space_x().into() - }; + }); - if opacity > 0.0 { - opaque( - mouse_area( - container(image) - .center(Fill) - .style(move |_theme| { - container::Style::default() - .background(color!(0x000000, opacity)) - }) - .padding(20), - ) - .on_press(Message::Close), + Some(opaque( + mouse_area( + container(image) + .center(Fill) + .style(move |_theme| { + container::Style::default() + .background(color!(0x000000, opacity)) + }) + .padding(20), ) - } else { - space_x().into() - } + .on_press(Message::Close), + )) } } diff --git a/examples/gradient/src/main.rs b/examples/gradient/src/main.rs index d927cd23..05505f35 100644 --- a/examples/gradient/src/main.rs +++ b/examples/gradient/src/main.rs @@ -1,6 +1,6 @@ use iced::gradient; use iced::theme; -use iced::widget::{checkbox, column, container, row, slider, space_x, text}; +use iced::widget::{checkbox, column, container, row, slider, space, text}; use iced::{Center, Color, Element, Fill, Radians, Theme, color}; pub fn main() -> iced::Result { @@ -57,7 +57,7 @@ impl Gradient { transparent, } = *self; - let gradient_box = container(space_x()) + let gradient_box = container(space()) .style(move |_theme| { let gradient = gradient::Linear::new(angle) .add_stop(0.0, start) diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index dc311d75..210f745d 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -3,7 +3,7 @@ use iced::keyboard; use iced::mouse; use iced::widget::{ button, canvas, center, center_y, checkbox, column, container, pick_list, - pin, row, rule, scrollable, space_x, stack, text, + pin, row, rule, scrollable, space, stack, text, }; use iced::{ Center, Element, Fill, Font, Length, Point, Rectangle, Renderer, Shrink, @@ -70,7 +70,7 @@ impl Layout { fn view(&self) -> Element<'_, Message> { let header = row![ text(self.example.title).size(20).font(Font::MONOSPACE), - space_x(), + space::horizontal(), checkbox("Explain", self.explain) .on_toggle(Message::ExplainToggled), pick_list(Theme::ALL, self.theme.as_ref(), Message::ThemeSelected) @@ -92,23 +92,19 @@ impl Layout { }) .padding(4); - let controls = row([ + let controls = row![ (!self.example.is_first()).then_some( button(text("← Previous")) .padding([5, 10]) .on_press(Message::Previous) - .into(), ), - Some(space_x().into()), + space::horizontal(), (!self.example.is_last()).then_some( button(text("Next →")) .padding([5, 10]) .on_press(Message::Next) - .into(), ), - ] - .into_iter() - .flatten()); + ]; column![header, example, controls] .spacing(10) @@ -143,7 +139,7 @@ impl Example { }, Self { title: "Space", - view: space, + view: space_, }, Self { title: "Application", @@ -237,15 +233,21 @@ fn row_<'a>() -> Element<'a, Message> { .into() } -fn space<'a>() -> Element<'a, Message> { - row!["Left!", space_x(), "Right!"].into() +fn space_<'a>() -> Element<'a, Message> { + row!["Left!", space::horizontal(), "Right!"].into() } fn application<'a>() -> Element<'a, Message> { let header = container( - row![square(40), space_x(), "Header!", space_x(), square(40),] - .padding(10) - .align_y(Center), + row![ + square(40), + space::horizontal(), + "Header!", + space::horizontal(), + square(40), + ] + .padding(10) + .align_y(Center), ) .style(|theme| { let palette = theme.extended_palette(); diff --git a/examples/lazy/src/main.rs b/examples/lazy/src/main.rs index 99348da7..6a48b655 100644 --- a/examples/lazy/src/main.rs +++ b/examples/lazy/src/main.rs @@ -1,5 +1,5 @@ use iced::widget::{ - button, column, lazy, pick_list, row, scrollable, space_x, text, text_input, + button, column, lazy, pick_list, row, scrollable, space, text, text_input, }; use iced::{Element, Fill}; @@ -173,7 +173,7 @@ impl App { row![ text(item.name.clone()).color(item.color), - space_x(), + space::horizontal(), pick_list(Color::ALL, Some(item.color), move |color| { Message::ItemColorChanged(item.clone(), color) }), diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs index c6df9a0c..6e2a79b9 100644 --- a/examples/markdown/src/main.rs +++ b/examples/markdown/src/main.rs @@ -6,7 +6,7 @@ use iced::highlighter; use iced::time::{self, Instant, milliseconds}; use iced::widget::{ button, center_x, container, hover, image, markdown, operation, right, row, - scrollable, sensor, space_x, text_editor, toggler, + scrollable, sensor, space, text_editor, toggler, }; use iced::window; use iced::{ @@ -264,7 +264,7 @@ impl<'a> markdown::Viewer<'a, Message> for CustomViewer<'a> { ) .into() } else { - sensor(space_x()) + sensor(space()) .key_ref(url.as_str()) .delay(milliseconds(500)) .on_show(|_size| Message::ImageShown(url.clone())) diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index 82596130..daf288e0 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -3,7 +3,7 @@ use iced::keyboard; use iced::keyboard::key; use iced::widget::{ button, center, column, container, mouse_area, opaque, operation, - pick_list, row, space_x, stack, text, text_input, + pick_list, row, space, stack, text, text_input, }; use iced::{Bottom, Color, Element, Fill, Subscription, Task}; @@ -95,12 +95,16 @@ impl App { fn view(&self) -> Element<'_, Message> { let content = container( column![ - row![text("Top Left"), space_x(), text("Top Right")] + row![text("Top Left"), space::horizontal(), text("Top Right")] .height(Fill), center(button(text("Show Modal")).on_press(Message::ShowModal)), - row![text("Bottom Left"), space_x(), text("Bottom Right")] - .align_y(Bottom) - .height(Fill), + row![ + text("Bottom Left"), + space::horizontal(), + text("Bottom Right") + ] + .align_y(Bottom) + .height(Fill), ] .height(Fill), ) diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index a8076617..a834a617 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -1,6 +1,6 @@ use iced::widget::{ - button, center, center_x, column, container, operation, scrollable, - space_x, text, text_input, + button, center, center_x, column, container, operation, scrollable, space, + text, text_input, }; use iced::window; use iced::{ @@ -134,7 +134,7 @@ impl Example { if let Some(window) = self.windows.get(&window_id) { center(window.view(window_id)).into() } else { - space_x().into() + space().into() } } diff --git a/examples/pick_list/src/main.rs b/examples/pick_list/src/main.rs index a1e44ae8..f67e3a6c 100644 --- a/examples/pick_list/src/main.rs +++ b/examples/pick_list/src/main.rs @@ -1,4 +1,4 @@ -use iced::widget::{column, pick_list, scrollable, space_y}; +use iced::widget::{column, pick_list, scrollable, space}; use iced::{Center, Element, Fill}; pub fn main() -> iced::Result { @@ -33,10 +33,10 @@ impl Example { .placeholder("Choose a language..."); let content = column![ - space_y().height(600), + space().height(600), "Which is your favorite language?", pick_list, - space_y().height(600), + space().height(600), ] .width(Fill) .align_x(Center) diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index 9c6bfa1c..17b1fad2 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -1,6 +1,6 @@ use iced::widget::{ button, column, container, operation, progress_bar, radio, row, scrollable, - slider, space_x, space_y, text, + slider, space, text, }; use iced::{Border, Center, Color, Element, Fill, Task, Theme}; @@ -190,9 +190,9 @@ impl ScrollableDemo { column![ scroll_to_end_button(), text("Beginning!"), - space_y().height(1200), + space().height(1200), text("Middle!"), - space_y().height(1200), + space().height(1200), text("End!"), scroll_to_beginning_button(), ] @@ -215,9 +215,9 @@ impl ScrollableDemo { row![ scroll_to_end_button(), text("Beginning!"), - space_x().width(1200), + space().width(1200), text("Middle!"), - space_x().width(1200), + space().width(1200), text("End!"), scroll_to_beginning_button(), ] @@ -242,25 +242,25 @@ impl ScrollableDemo { row![ column![ text("Let's do some scrolling!"), - space_y().height(2400) + space().height(2400) ], scroll_to_end_button(), text("Horizontal - Beginning!"), - space_x().width(1200), + space().width(1200), //vertical content column![ text("Horizontal - Middle!"), scroll_to_end_button(), text("Vertical - Beginning!"), - space_y().height(1200), + space().height(1200), text("Vertical - Middle!"), - space_y().height(1200), + space().height(1200), text("Vertical - End!"), scroll_to_beginning_button(), - space_y().height(40), + space().height(40), ] .spacing(40), - space_x().width(1200), + space().width(1200), text("Horizontal - End!"), scroll_to_beginning_button(), ] diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index 996f80ce..56e5bb27 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -1,7 +1,7 @@ use iced::keyboard; use iced::widget::{ button, center_x, center_y, checkbox, column, container, pick_list, - progress_bar, row, rule, scrollable, slider, space_y, text, text_input, + progress_bar, row, rule, scrollable, slider, space, text, text_input, toggler, }; use iced::{Center, Element, Fill, Shrink, Subscription, Theme}; @@ -127,7 +127,7 @@ impl Styling { let scroll_me = scrollable(column![ "Scroll me!", - space_y().height(800), + space().height(800), "You did it!" ]) .width(Fill) diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 8f91c9a5..b3f09dd6 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -171,7 +171,7 @@ mod toast { use iced::mouse; use iced::theme; use iced::time::{self, Duration, Instant}; - use iced::widget::{button, column, container, row, rule, space_x, text}; + use iced::widget::{button, column, container, row, rule, space, text}; use iced::window; use iced::{ Alignment, Center, Element, Event, Fill, Length, Point, Rectangle, @@ -237,7 +237,7 @@ mod toast { container( row![ text(toast.title.as_str()), - space_x(), + space::horizontal(), button("X") .on_press((on_close)(index)) .padding(3), diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index 33ae3cca..fbefa6ad 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -1,7 +1,7 @@ use iced::widget::{Button, Column, Container, Slider}; use iced::widget::{ button, center_x, center_y, checkbox, column, image, radio, rich_text, row, - scrollable, slider, space_x, space_y, span, text, text_input, toggler, + scrollable, slider, space, span, text, text_input, toggler, }; use iced::{Center, Color, Element, Fill, Font, Pixels, color}; @@ -146,7 +146,7 @@ impl Tour { .on_press(Message::BackPressed) .style(button::secondary) }), - space_x(), + space::horizontal(), self.can_continue().then(|| { padded_button("Next").on_press(Message::NextPressed) }) @@ -405,14 +405,14 @@ impl Tour { text("Tip: You can use the scrollbar to scroll down faster!") .size(16), ) - .push(space_y().height(4096)) + .push(space().height(4096)) .push( text("You are halfway there!") .width(Fill) .size(30) .align_x(Center), ) - .push(space_y().height(4096)) + .push(space().height(4096)) .push(ferris(300, image::FilterMethod::Linear)) .push(text("You made it!").width(Fill).size(50).align_x(Center)) } diff --git a/examples/vectorial_text/src/main.rs b/examples/vectorial_text/src/main.rs index 12d190a3..bf0fd657 100644 --- a/examples/vectorial_text/src/main.rs +++ b/examples/vectorial_text/src/main.rs @@ -1,6 +1,6 @@ use iced::alignment; use iced::mouse; -use iced::widget::{canvas, checkbox, column, row, slider, space_x, text}; +use iced::widget::{canvas, checkbox, column, row, slider, space, text}; use iced::{Center, Element, Fill, Point, Rectangle, Renderer, Theme, Vector}; pub fn main() -> iced::Result { @@ -49,7 +49,7 @@ impl VectorialText { fn view(&self) -> Element<'_, Message> { let slider_with_label = |label, range, value, message: fn(f32) -> _| { column![ - row![text(label), space_x(), text!("{:.2}", value)], + row![text(label), space::horizontal(), text!("{:.2}", value)], slider(range, value, message).step(0.01) ] .spacing(2) diff --git a/examples/visible_bounds/src/main.rs b/examples/visible_bounds/src/main.rs index 4025dcf4..4a900bf7 100644 --- a/examples/visible_bounds/src/main.rs +++ b/examples/visible_bounds/src/main.rs @@ -1,7 +1,7 @@ use iced::event::{self, Event}; use iced::mouse; use iced::widget::{ - self, column, container, row, scrollable, selector, space_x, space_y, text, + self, column, container, row, scrollable, selector, space, text, }; use iced::window; use iced::{ @@ -63,7 +63,7 @@ impl Example { let data_row = |label, value, color| { row![ text(label), - space_x(), + space::horizontal(), text(value) .font(Font::MONOSPACE) .size(14) @@ -111,21 +111,21 @@ impl Example { scrollable( column![ text("Scroll me!"), - space_y().height(400), + space().height(400), container(text("I am the outer container!")) .id(OUTER_CONTAINER) .padding(40) .style(container::rounded_box), - space_y().height(400), + space().height(400), scrollable( column![ text("Scroll me!"), - space_y().height(400), + space().height(400), container(text("I am the inner container!")) .id(INNER_CONTAINER) .padding(40) .style(container::rounded_box), - space_y().height(400), + space().height(400), ] .padding(20) ) diff --git a/tester/src/lib.rs b/tester/src/lib.rs index 16450b4e..5ac77968 100644 --- a/tester/src/lib.rs +++ b/tester/src/lib.rs @@ -28,7 +28,7 @@ use crate::test::instruction; use crate::test::{Emulator, Ice, Instruction}; use crate::widget::{ button, center, column, combo_box, container, pick_list, row, rule, - scrollable, space_x, text, text_editor, text_input, themer, + scrollable, space, text, text_editor, text_input, themer, }; /// Attaches a [`Tester`] to the given [`Program`]. @@ -564,7 +564,7 @@ impl Tester

{ let viewport = container( scrollable( container(match &self.state { - State::Empty => Element::from(space_x()), + State::Empty => Element::from(space()), State::Idle { state } => { let theme = program.theme(state, window); @@ -795,7 +795,7 @@ impl Tester

{ }; let edit = if self.is_busy() { - Element::from(space_x()) + Element::from(space::horizontal()) } else if self.edit.is_none() { button(icon::pencil().size(14)) .padding(0) @@ -849,7 +849,7 @@ where column![ row![ text(fragment).size(14).font(Font::MONOSPACE), - space_x(), + space::horizontal(), control.into() ] .spacing(5) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index a5d20222..4afb8cee 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -1017,7 +1017,7 @@ where /// # mod iced { pub mod widget { pub use iced_widget::*; } } /// # pub type State = (); /// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; -/// use iced::widget::{column, scrollable, space_y}; +/// use iced::widget::{column, scrollable, space}; /// /// enum Message { /// // ... @@ -1026,7 +1026,7 @@ where /// fn view(state: &State) -> Element<'_, Message> { /// scrollable(column![ /// "Scroll me!", -/// space_y().height(3000), +/// space().height(3000), /// "You did it!", /// ]).into() /// } @@ -1729,20 +1729,12 @@ where ComboBox::new(state, placeholder, selection, on_selected) } -/// Creates a new [`Space`] widget that fills the available -/// horizontal space. +/// Creates some empty [`Space`] with no size. /// -/// This can be useful to separate widgets in a [`Row`]. -pub fn space_x() -> Space { - Space::with_width(Length::Fill) -} - -/// Creates a new [`Space`] widget that fills the available -/// vertical space. -/// -/// This can be useful to separate widgets in a [`Column`]. -pub fn space_y() -> Space { - Space::with_height(Length::Fill) +/// This is considered the "identity" widget. It will take +/// no space and do nothing. +pub fn space() -> Space { + Space::new() } /// Creates a horizontal [`Rule`] with the given height. diff --git a/widget/src/lib.rs b/widget/src/lib.rs index c58220a8..73f1ed22 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -14,7 +14,6 @@ mod column; mod mouse_area; mod pin; mod responsive; -mod space; mod stack; mod themer; @@ -35,6 +34,7 @@ pub mod rule; pub mod scrollable; pub mod sensor; pub mod slider; +pub mod space; pub mod table; pub mod text; pub mod text_editor; diff --git a/widget/src/responsive.rs b/widget/src/responsive.rs index 06757fbd..76e9c62c 100644 --- a/widget/src/responsive.rs +++ b/widget/src/responsive.rs @@ -8,7 +8,7 @@ use crate::core::{ self, Clipboard, Element, Event, Length, Rectangle, Shell, Size, Vector, Widget, }; -use crate::space_x; +use crate::space; /// A widget that is aware of its dimensions. /// @@ -43,7 +43,7 @@ where view: Box::new(view), width: Length::Fill, height: Length::Fill, - content: Element::new(space_x().width(0)), + content: Element::new(space()), } } diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 2535ecb8..75fd249c 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -5,7 +5,7 @@ //! # mod iced { pub mod widget { pub use iced_widget::*; } } //! # pub type State = (); //! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; -//! use iced::widget::{column, scrollable, space_y}; +//! use iced::widget::{column, scrollable, space}; //! //! enum Message { //! // ... @@ -14,7 +14,7 @@ //! fn view(state: &State) -> Element<'_, Message> { //! scrollable(column![ //! "Scroll me!", -//! space_y().height(3000), +//! space().height(3000), //! "You did it!", //! ]).into() //! } @@ -48,7 +48,7 @@ pub use operation::scrollable::{AbsoluteOffset, RelativeOffset}; /// # mod iced { pub mod widget { pub use iced_widget::*; } } /// # pub type State = (); /// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; -/// use iced::widget::{column, scrollable, space_y}; +/// use iced::widget::{column, scrollable, space}; /// /// enum Message { /// // ... @@ -57,7 +57,7 @@ pub use operation::scrollable::{AbsoluteOffset, RelativeOffset}; /// fn view(state: &State) -> Element<'_, Message> { /// scrollable(column![ /// "Scroll me!", -/// space_y().height(3000), +/// space().height(3000), /// "You did it!", /// ]).into() /// } diff --git a/widget/src/space.rs b/widget/src/space.rs index 949ea3ce..231d4704 100644 --- a/widget/src/space.rs +++ b/widget/src/space.rs @@ -1,4 +1,4 @@ -//! Distribute content vertically. +//! Add some explicit spacing between elements. use crate::core; use crate::core::layout; use crate::core::mouse; @@ -6,6 +6,22 @@ use crate::core::renderer; use crate::core::widget::Tree; use crate::core::{Element, Layout, Length, Rectangle, Size, Widget}; +/// Creates a new [`Space`] widget that fills the available +/// horizontal space. +/// +/// This can be useful to separate widgets in a [`Row`](crate::Row). +pub fn horizontal() -> Space { + Space::new().width(Length::Fill) +} + +/// Creates a new [`Space`] widget that fills the available +/// vertical space. +/// +/// This can be useful to separate widgets in a [`Column`](crate::Column). +pub fn vertical() -> Space { + Space::new().height(Length::Fill) +} + /// An amount of empty space. /// /// It can be useful if you want to fill some space with nothing. @@ -16,27 +32,11 @@ pub struct Space { } impl Space { - /// Creates an amount of empty [`Space`] with the given width and height. - pub fn new(width: impl Into, height: impl Into) -> Self { - Space { - width: width.into(), - height: height.into(), - } - } - - /// Creates an amount of horizontal [`Space`]. - pub fn with_width(width: impl Into) -> Self { - Space { - width: width.into(), - height: Length::Shrink, - } - } - - /// Creates an amount of vertical [`Space`]. - pub fn with_height(height: impl Into) -> Self { + /// Creates some empty [`Space`] with no size. + pub fn new() -> Self { Space { width: Length::Shrink, - height: height.into(), + height: Length::Shrink, } } @@ -53,6 +53,12 @@ impl Space { } } +impl Default for Space { + fn default() -> Self { + Space::new() + } +} + impl Widget for Space where Renderer: core::Renderer, From afac7be7d12a5228adebd33ffafaf755a942642f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 17 Sep 2025 23:59:05 +0200 Subject: [PATCH 70/83] Rename `selector::visible_bounds` to `delineate` --- Cargo.lock | 14 +++++++------- examples/{visible_bounds => delineate}/Cargo.toml | 2 +- examples/{visible_bounds => delineate}/src/main.rs | 6 ++---- runtime/src/widget/selector.rs | 2 +- 4 files changed, 11 insertions(+), 13 deletions(-) rename examples/{visible_bounds => delineate}/Cargo.toml (89%) rename examples/{visible_bounds => delineate}/src/main.rs (95%) diff --git a/Cargo.lock b/Cargo.lock index 4d3dde36..efea5828 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1210,6 +1210,13 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" +[[package]] +name = "delineate" +version = "0.1.0" +dependencies = [ + "iced", +] + [[package]] name = "deranged" version = "0.5.3" @@ -6557,13 +6564,6 @@ version = "0.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" -[[package]] -name = "visible_bounds" -version = "0.1.0" -dependencies = [ - "iced", -] - [[package]] name = "voronator" version = "0.2.1" diff --git a/examples/visible_bounds/Cargo.toml b/examples/delineate/Cargo.toml similarity index 89% rename from examples/visible_bounds/Cargo.toml rename to examples/delineate/Cargo.toml index 7cdccb60..ff181689 100644 --- a/examples/visible_bounds/Cargo.toml +++ b/examples/delineate/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "visible_bounds" +name = "delineate" version = "0.1.0" authors = ["Héctor Ramón Jiménez "] edition = "2024" diff --git a/examples/visible_bounds/src/main.rs b/examples/delineate/src/main.rs similarity index 95% rename from examples/visible_bounds/src/main.rs rename to examples/delineate/src/main.rs index 4a900bf7..97bd7d62 100644 --- a/examples/visible_bounds/src/main.rs +++ b/examples/delineate/src/main.rs @@ -41,10 +41,8 @@ impl Example { Task::none() } Message::Scrolled | Message::WindowResized => Task::batch(vec![ - selector::visible_bounds(OUTER_CONTAINER) - .map(Message::OuterFound), - selector::visible_bounds(INNER_CONTAINER) - .map(Message::InnerFound), + selector::delineate(OUTER_CONTAINER).map(Message::OuterFound), + selector::delineate(INNER_CONTAINER).map(Message::InnerFound), ]), Message::OuterFound(outer) => { self.outer_bounds = outer; diff --git a/runtime/src/widget/selector.rs b/runtime/src/widget/selector.rs index ee142246..a6f306c5 100644 --- a/runtime/src/widget/selector.rs +++ b/runtime/src/widget/selector.rs @@ -18,7 +18,7 @@ pub fn find_by_text(text: impl Into) -> Task> { } /// Finds the visible bounds of the first [`Selector`] target. -pub fn visible_bounds(selector: S) -> Task> +pub fn delineate(selector: S) -> Task> where S: Selector + Send + 'static, S::Output: Bounded + Clone + Send + 'static, From 8f87a2bc2e33757be34549762c03c0f0bcc317d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 19 Sep 2025 18:09:59 +0200 Subject: [PATCH 71/83] Make `rule` API consistent with `space` --- examples/layout/src/main.rs | 4 ++-- examples/styling/src/main.rs | 4 ++-- examples/toast/src/main.rs | 2 +- tester/src/lib.rs | 2 +- widget/src/helpers.rs | 30 ++----------------------- widget/src/markdown.rs | 6 ++--- widget/src/rule.rs | 43 ++++++++++++++++++++++-------------- 7 files changed, 37 insertions(+), 54 deletions(-) diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index 210f745d..c4faf319 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -290,7 +290,7 @@ fn quotes<'a>() -> Element<'a, Message> { fn quote<'a>( content: impl Into>, ) -> Element<'a, Message> { - row![rule(1).vertical(), content.into()] + row![rule::vertical(1), content.into()] .spacing(10) .height(Shrink) .into() @@ -308,7 +308,7 @@ fn quotes<'a>() -> Element<'a, Message> { reply("This is the original message", "This is a reply"), "This is another reply", ), - rule(1), + rule::horizontal(1), text("A separator ↑"), ] .width(Shrink) diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index 56e5bb27..f979f6f2 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -162,14 +162,14 @@ impl Styling { let content = column![ choose_theme, - rule(1), + rule::horizontal(1), text_input, buttons, slider(), progress_bar(), row![ scroll_me, - rule(1).vertical(), + rule::vertical(1), column![check, check_disabled, toggle, disabled_toggle] .spacing(10) ] diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index b3f09dd6..cca0e789 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -252,7 +252,7 @@ mod toast { Status::Success => success, Status::Danger => danger, }), - rule(1), + rule::horizontal(1), container(text(toast.body.as_str())) .width(Fill) .padding(5) diff --git a/tester/src/lib.rs b/tester/src/lib.rs index 5ac77968..724aa28f 100644 --- a/tester/src/lib.rs +++ b/tester/src/lib.rs @@ -630,7 +630,7 @@ impl Tester

{ row![ center(column![status, viewport].spacing(10).align_x(Right)) .padding(10), - rule(1).vertical().style(rule::weak), + rule::vertical(1).style(rule::weak), container(self.controls().map(Tick::Tester)) .width(250) .padding(10) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 4afb8cee..2d696c84 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -7,7 +7,7 @@ use crate::core; use crate::core::theme; use crate::core::widget::operation::{self, Operation}; use crate::core::window; -use crate::core::{Element, Length, Pixels, Size, Widget}; +use crate::core::{Element, Length, Size, Widget}; use crate::float::{self, Float}; use crate::keyed; use crate::overlay; @@ -15,7 +15,6 @@ use crate::pane_grid::{self, PaneGrid}; use crate::pick_list::{self, PickList}; use crate::progress_bar::{self, ProgressBar}; use crate::radio::{self, Radio}; -use crate::rule::{self, Rule}; use crate::scrollable::{self, Scrollable}; use crate::slider::{self, Slider}; use crate::text::{self, Text}; @@ -1737,31 +1736,6 @@ pub fn space() -> Space { Space::new() } -/// Creates a horizontal [`Rule`] with the given height. -/// -/// # Example -/// ```no_run -/// # mod iced { pub mod widget { pub use iced_widget::*; } } -/// # pub type State = (); -/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; -/// use iced::widget::rule; -/// -/// #[derive(Clone)] -/// enum Message { -/// // ..., -/// } -/// -/// fn view(state: &State) -> Element<'_, Message> { -/// rule(2).into() -/// } -/// ``` -pub fn rule<'a, Theme>(height: impl Into) -> Rule<'a, Theme> -where - Theme: rule::Catalog + 'a, -{ - Rule::new(height) -} - /// Creates a new [`ProgressBar`]. /// /// Progress bars visualize the progression of an extended computer operation, such as a download, file transfer, or installation. @@ -1864,7 +1838,7 @@ where /// for instance. #[cfg(feature = "svg")] pub fn iced<'a, Message, Theme, Renderer>( - text_size: impl Into, + text_size: impl Into, ) -> Element<'a, Message, Theme, Renderer> where Message: 'a, diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index f2114f1d..d778dd6a 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -51,7 +51,7 @@ use crate::core::theme; use crate::core::{ self, Color, Element, Length, Padding, Pixels, Theme, color, }; -use crate::{column, container, rich_text, row, scrollable, span, text}; +use crate::{column, container, rich_text, row, rule, scrollable, span, text}; use std::borrow::BorrowMut; use std::cell::{Cell, RefCell}; @@ -1388,7 +1388,7 @@ where Renderer: core::text::Renderer + 'a, { row![ - crate::rule(4).vertical(), + rule::vertical(4), column( contents .iter() @@ -1410,7 +1410,7 @@ where Theme: Catalog + 'a, Renderer: core::text::Renderer + 'a, { - crate::rule(2).into() + rule::horizontal(2).into() } /// Displays a table using the default look. diff --git a/widget/src/rule.rs b/widget/src/rule.rs index 5ba2a6f0..1472caed 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -13,7 +13,7 @@ //! } //! //! fn view(state: &State) -> Element<'_, Message> { -//! rule(2).into() +//! rule::horizontal(2).into() //! } //! ``` use crate::core; @@ -26,6 +26,30 @@ use crate::core::{ Color, Element, Layout, Length, Pixels, Rectangle, Size, Theme, Widget, }; +/// Creates a new horizontal [`Rule`] with the given height. +pub fn horizontal<'a, Theme>(height: impl Into) -> Rule<'a, Theme> +where + Theme: Catalog, +{ + Rule { + thickness: Length::Fixed(height.into().0), + is_vertical: false, + class: Theme::default(), + } +} + +/// Creates a new vertical [`Rule`] with the given width. +pub fn vertical<'a, Theme>(width: impl Into) -> Rule<'a, Theme> +where + Theme: Catalog, +{ + Rule { + thickness: Length::Fixed(width.into().0), + is_vertical: true, + class: Theme::default(), + } +} + /// Display a horizontal or vertical rule for dividing content. /// /// # Example @@ -41,7 +65,7 @@ use crate::core::{ /// } /// /// fn view(state: &State) -> Element<'_, Message> { -/// rule(2).into() +/// rule::horizontal(2).into() /// } /// ``` pub struct Rule<'a, Theme = crate::Theme> @@ -57,21 +81,6 @@ impl<'a, Theme> Rule<'a, Theme> where Theme: Catalog, { - /// Creates a horizontal [`Rule`] with the given thickness. - pub fn new(thickness: impl Into) -> Self { - Rule { - thickness: Length::Fixed(thickness.into().0), - is_vertical: false, - class: Theme::default(), - } - } - - /// Turns the [`Rule`] into a vertical one. - pub fn vertical(mut self) -> Self { - self.is_vertical = true; - self - } - /// Sets the style of the [`Rule`]. #[must_use] pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self From c3f90fc420f5ae0fc56ab04d498f685db0d62c92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 19 Sep 2025 18:24:02 +0200 Subject: [PATCH 72/83] Update `Cargo.lock` --- Cargo.lock | 272 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 176 insertions(+), 96 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 24fcb729..c43fa494 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -129,9 +129,9 @@ checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "approx" @@ -255,9 +255,9 @@ dependencies = [ [[package]] name = "async-fs" -version = "2.1.3" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f7e37c0ed80b2a977691c47dae8625cfb21e205827106c64f7c588766b2e50" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" dependencies = [ "async-lock", "blocking", @@ -266,11 +266,11 @@ dependencies = [ [[package]] name = "async-io" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19634d6336019ef220f09fd31168ce5c184b295cbf80345437cc36094ef223ca" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" dependencies = [ - "async-lock", + "autocfg", "cfg-if", "concurrent-queue", "futures-io", @@ -279,7 +279,7 @@ dependencies = [ "polling", "rustix 1.1.2", "slab", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -306,9 +306,9 @@ dependencies = [ [[package]] name = "async-process" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65daa13722ad51e6ab1a1b9c01299142bc75135b337923cfa10e79bbbd669f00" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" dependencies = [ "async-channel", "async-io", @@ -335,9 +335,9 @@ dependencies = [ [[package]] name = "async-signal" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f567af260ef69e1d52c2b560ce0ea230763e6fbb9214a85d768760a920e3e3c1" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" dependencies = [ "async-io", "async-lock", @@ -348,7 +348,7 @@ dependencies = [ "rustix 1.1.2", "signal-hook-registry", "slab", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -706,9 +706,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.36" +version = "1.2.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5252b3d2648e5eedbc1a6f501e3c795e07025c1e93bbf8bbdd6eef7f447a6d54" +checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9" dependencies = [ "find-msvc-tools", "jobserver", @@ -1574,9 +1574,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" +checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" [[package]] name = "flate2" @@ -1925,7 +1925,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.5+wasi-0.2.4", + "wasi 0.14.7+wasi-0.2.4", ] [[package]] @@ -2058,7 +2058,7 @@ checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" dependencies = [ "bitflags 2.9.4", "gpu-descriptor-types", - "hashbrown", + "hashbrown 0.15.5", ] [[package]] @@ -2164,6 +2164,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + [[package]] name = "headers" version = "0.3.9" @@ -2341,10 +2347,10 @@ dependencies = [ "http 1.3.1", "hyper 1.7.0", "hyper-util", - "rustls 0.23.31", + "rustls 0.23.32", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.2", + "tokio-rustls 0.26.3", "tower-service", ] @@ -2366,9 +2372,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ "base64 0.22.1", "bytes", @@ -2392,9 +2398,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2402,7 +2408,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.2", + "windows-core 0.62.0", ] [[package]] @@ -2818,12 +2824,12 @@ checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" [[package]] name = "indexmap" -version = "2.11.1" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.0", ] [[package]] @@ -2973,9 +2979,9 @@ checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" [[package]] name = "js-sys" -version = "0.3.78" +version = "0.3.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738" +checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" dependencies = [ "once_cell", "wasm-bindgen", @@ -3088,9 +3094,9 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags 2.9.4", "libc", @@ -3190,9 +3196,9 @@ checksum = "bfe949189f46fabb938b3a9a0be30fdd93fd8a09260da863399a8cf3db756ec8" [[package]] name = "lyon" -version = "1.0.1" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7f9cda98b5430809e63ca5197b06c7d191bf7e26dfc467d5a3f0290e2a74f" +checksum = "dbcb7d54d54c8937364c9d41902d066656817dce1e03a44e5533afebd1ef4352" dependencies = [ "lyon_algorithms", "lyon_tessellation", @@ -3200,9 +3206,9 @@ dependencies = [ [[package]] name = "lyon_algorithms" -version = "1.0.5" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f13c9be19d257c7d37e70608ed858e8eab4b2afcea2e3c9a622e892acbf43c08" +checksum = "f4c0829e28c4f336396f250d850c3987e16ce6db057ffe047ce0dd54aab6b647" dependencies = [ "lyon_path", "num-traits", @@ -3210,9 +3216,9 @@ dependencies = [ [[package]] name = "lyon_geom" -version = "1.0.6" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8af69edc087272df438b3ee436c4bb6d7c04aa8af665cfd398feae627dbd8570" +checksum = "ce9333c02ea4517fd31207f126124352ad59975218c114c55dbb8f9d56fd4b45" dependencies = [ "arrayvec", "euclid", @@ -3221,9 +3227,9 @@ dependencies = [ [[package]] name = "lyon_path" -version = "1.0.7" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0047f508cd7a85ad6bad9518f68cce7b1bf6b943fb71f6da0ee3bc1e8cb75f25" +checksum = "1aeca86bcfd632a15984ba029b539ffb811e0a70bf55e814ef8b0f54f506fdeb" dependencies = [ "lyon_geom", "num-traits", @@ -3231,9 +3237,9 @@ dependencies = [ [[package]] name = "lyon_tessellation" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579d42360a4b09846eff2feef28f538696c7d6c7439bfa65874ff3cbe0951b2c" +checksum = "f3f586142e1280335b1bc89539f7c97dd80f08fc43e9ab1b74ef0a42b04aa353" dependencies = [ "float_next_after", "lyon_path", @@ -3465,7 +3471,7 @@ dependencies = [ "cfg_aliases", "codespan-reporting", "half", - "hashbrown", + "hashbrown 0.15.5", "hexf-parse", "indexmap", "libm", @@ -4388,9 +4394,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plist" -version = "1.7.4" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", "indexmap", @@ -4467,16 +4473,16 @@ dependencies = [ [[package]] name = "polling" -version = "3.10.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5bd19146350fe804f7cb2669c851c03d69da628803dab0d98018142aaa5d829" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi", "pin-project-lite", "rustix 1.1.2", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -4532,11 +4538,11 @@ checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" [[package]] name = "proc-macro-crate" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit", + "toml_edit 0.23.6", ] [[package]] @@ -5098,13 +5104,13 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.31" +version = "0.23.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" dependencies = [ "once_cell", "rustls-pki-types", - "rustls-webpki 0.103.4", + "rustls-webpki 0.103.6", "subtle", "zeroize", ] @@ -5131,9 +5137,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" dependencies = [ "ring", "rustls-pki-types", @@ -5260,27 +5266,38 @@ checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" dependencies = [ "serde", + "serde_core", ] [[package]] name = "serde" -version = "1.0.219" +version = "1.0.225" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.225" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.225" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" dependencies = [ "proc-macro2", "quote", @@ -5289,14 +5306,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.143" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -5933,11 +5951,12 @@ dependencies = [ [[package]] name = "time" -version = "0.3.43" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", + "itoa", "num-conv", "powerfmt", "serde", @@ -6111,11 +6130,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "05f63835928ca123f1bef57abbcd23bb2ba0ac9ae1235f1e65bda0d06e7786bd" dependencies = [ - "rustls 0.23.31", + "rustls 0.23.32", "tokio", ] @@ -6152,8 +6171,8 @@ checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", ] [[package]] @@ -6165,6 +6184,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -6174,7 +6202,28 @@ dependencies = [ "indexmap", "serde", "serde_spanned", - "toml_datetime", + "toml_datetime 0.6.11", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.23.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +dependencies = [ + "indexmap", + "toml_datetime 0.7.2", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +dependencies = [ "winnow", ] @@ -6629,27 +6678,27 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.5+wasi-0.2.4" +version = "0.14.7+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4494f6290a82f5fe584817a676a34b9d6763e8d9d18204009fb31dceca98fd4" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" dependencies = [ "wasip2", ] [[package]] name = "wasip2" -version = "1.0.0+wasi-0.2.4" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03fa2761397e5bd52002cd7e73110c71af2109aca4e521a9f40473fe685b0a24" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.101" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b" +checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" dependencies = [ "cfg-if", "once_cell", @@ -6660,9 +6709,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.101" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb" +checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" dependencies = [ "bumpalo", "log", @@ -6674,9 +6723,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.51" +version = "0.4.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe" +checksum = "a0b221ff421256839509adbb55998214a70d829d3a28c69b4a6672e9d2a42f67" dependencies = [ "cfg-if", "js-sys", @@ -6687,9 +6736,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.101" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d" +checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6697,9 +6746,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.101" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa" +checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" dependencies = [ "proc-macro2", "quote", @@ -6710,9 +6759,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.101" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1" +checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" dependencies = [ "unicode-ident", ] @@ -6855,9 +6904,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.78" +version = "0.3.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12" +checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc" dependencies = [ "js-sys", "wasm-bindgen", @@ -6934,7 +6983,7 @@ dependencies = [ "cfg-if", "cfg_aliases", "document-features", - "hashbrown", + "hashbrown 0.15.5", "js-sys", "log", "naga", @@ -6964,7 +7013,7 @@ dependencies = [ "bitflags 2.9.4", "cfg_aliases", "document-features", - "hashbrown", + "hashbrown 0.15.5", "indexmap", "log", "naga", @@ -7041,7 +7090,7 @@ dependencies = [ "gpu-alloc", "gpu-allocator", "gpu-descriptor", - "hashbrown", + "hashbrown 0.15.5", "js-sys", "khronos-egl", "libc", @@ -7207,6 +7256,19 @@ dependencies = [ "windows-strings 0.4.2", ] +[[package]] +name = "windows-core" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" +dependencies = [ + "windows-implement 0.60.0", + "windows-interface 0.59.1", + "windows-link 0.2.0", + "windows-result 0.4.0", + "windows-strings 0.5.0", +] + [[package]] name = "windows-future" version = "0.2.1" @@ -7344,6 +7406,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-result" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows-strings" version = "0.1.0" @@ -7363,6 +7434,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-strings" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -7731,9 +7811,9 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.45.1" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" From a0a2f3aa52ce4d837886652f445be64333a3d5fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 20 Sep 2025 13:51:20 +0200 Subject: [PATCH 73/83] Write documentation for `iced_tester` --- tester/src/lib.rs | 89 ++++++++++++++++++++++++----------------------- 1 file changed, 46 insertions(+), 43 deletions(-) diff --git a/tester/src/lib.rs b/tester/src/lib.rs index 724aa28f..2edf44bb 100644 --- a/tester/src/lib.rs +++ b/tester/src/lib.rs @@ -1,5 +1,4 @@ //! Record, edit, and run end-to-end tests for your iced applications. -#![allow(missing_docs)] pub use iced_test as test; pub use iced_test::core; pub use iced_test::program; @@ -39,7 +38,7 @@ pub fn attach(program: P) -> Attach

{ /// A [`Program`] with a [`Tester`] attached to it. #[derive(Debug)] pub struct Attach

{ - /// The original [`Program`] attatched to the [`Tester`]. + /// The original [`Program`] attached to the [`Tester`]. pub program: P, } @@ -48,7 +47,7 @@ where P: Program + 'static, { type State = Tester

; - type Message = Tick

; + type Message = Message

; type Theme = Theme; type Renderer = P::Renderer; type Executor = P::Executor; @@ -79,7 +78,7 @@ where state: &mut Self::State, message: Self::Message, ) -> Task { - state.tick(&self.program, message) + state.tick(&self.program, message.0).map(Message) } fn view<'a>( @@ -87,10 +86,13 @@ where state: &'a Self::State, window: window::Id, ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - state.view(&self.program, window) + state.view(&self.program, window).map(Message) } } +/// A tester decorates a [`Program`] definition and attaches a test recorder on top. +/// +/// It can be used to both record and play [`Ice`] tests. pub struct Tester { viewport: Size, mode: emulator::Mode, @@ -127,8 +129,11 @@ enum Outcome { Success, } +/// The message of a [`Tester`]. +pub struct Message(Tick

); + #[derive(Debug, Clone)] -pub enum Message { +enum Event { ChangeViewport(Size), ModeSelected(emulator::Mode), PresetSelected(String), @@ -143,8 +148,8 @@ pub enum Message { Confirm, } -pub enum Tick { - Tester(Message), +enum Tick { + Tester(Event), Program(P::Message), Emulator(emulator::Event

), Record(instruction::Interaction), @@ -152,7 +157,7 @@ pub enum Tick { } impl Tester

{ - pub fn new(program: &P) -> Self { + fn new(program: &P) -> Self { let (state, _) = program.boot(); let window = program.window().unwrap_or_default(); @@ -174,7 +179,7 @@ impl Tester

{ } } - pub fn is_busy(&self) -> bool { + fn is_busy(&self) -> bool { matches!( self.state, State::Recording { .. } @@ -185,24 +190,24 @@ impl Tester

{ ) } - pub fn update(&mut self, program: &P, message: Message) -> Task> { - match message { - Message::ChangeViewport(viewport) => { + fn update(&mut self, program: &P, event: Event) -> Task> { + match event { + Event::ChangeViewport(viewport) => { self.viewport = viewport; Task::none() } - Message::ModeSelected(mode) => { + Event::ModeSelected(mode) => { self.mode = mode; Task::none() } - Message::PresetSelected(preset) => { + Event::PresetSelected(preset) => { self.preset = Some(preset); Task::none() } - Message::Record => { + Event::Record => { self.edit = None; self.instructions.clear(); @@ -220,7 +225,7 @@ impl Tester

{ Task::run(receiver, Tick::Emulator) } - Message::Stop => { + Event::Stop => { let State::Recording { emulator } = std::mem::replace(&mut self.state, State::Empty) else { @@ -246,7 +251,7 @@ impl Tester

{ Task::none() } - Message::Play => { + Event::Play => { self.confirm(); let (sender, receiver) = mpsc::channel(1); @@ -267,7 +272,7 @@ impl Tester

{ Task::run(receiver, Tick::Emulator) } - Message::Import => { + Event::Import => { use std::fs; let import = rfd::AsyncFileDialog::new() @@ -283,10 +288,10 @@ impl Tester

{ )); }) }) - .map(Message::Imported) + .map(Event::Imported) .map(Tick::Tester) } - Message::Export => { + Event::Export => { use std::fs; use std::thread; @@ -314,7 +319,7 @@ impl Tester

{ }) .discard() } - Message::Imported(Ok(ice)) => { + Event::Imported(Ok(ice)) => { self.viewport = ice.viewport; self.mode = ice.mode; self.preset = ice.preset; @@ -330,7 +335,7 @@ impl Tester

{ Task::none() } - Message::Edit => { + Event::Edit => { if self.is_busy() { return Task::none(); } @@ -346,19 +351,19 @@ impl Tester

{ Task::none() } - Message::Edited(action) => { + Event::Edited(action) => { if let Some(edit) = &mut self.edit { edit.perform(action); } Task::none() } - Message::Confirm => { + Event::Confirm => { self.confirm(); Task::none() } - Message::Imported(Err(error)) => { + Event::Imported(Err(error)) => { log::error!("{error}"); Task::none() @@ -392,7 +397,7 @@ impl Tester

{ }) } - pub fn tick(&mut self, program: &P, tick: Tick

) -> Task> { + fn tick(&mut self, program: &P, tick: Tick

) -> Task> { match tick { Tick::Tester(message) => self.update(program, message), Tick::Program(message) => { @@ -512,7 +517,7 @@ impl Tester

{ } } - pub fn view<'a>( + fn view<'a>( &'a self, program: &P, window: window::Id, @@ -641,18 +646,18 @@ impl Tester

{ .into() } - pub fn controls(&self) -> Element<'_, Message, Theme, P::Renderer> { + fn controls(&self) -> Element<'_, Event, Theme, P::Renderer> { let viewport = row![ text_input("Width", &self.viewport.width.to_string()) .size(14) - .on_input(|width| Message::ChangeViewport(Size { + .on_input(|width| Event::ChangeViewport(Size { width: width.parse().unwrap_or(self.viewport.width), ..self.viewport })), text("x").size(14).font(Font::MONOSPACE), text_input("Height", &self.viewport.height.to_string()) .size(14) - .on_input(|height| Message::ChangeViewport(Size { + .on_input(|height| Event::ChangeViewport(Size { height: height.parse().unwrap_or(self.viewport.height), ..self.viewport })), @@ -664,7 +669,7 @@ impl Tester

{ &self.presets, "Default", self.preset.as_ref(), - Message::PresetSelected, + Event::PresetSelected, ) .size(14) .width(Fill); @@ -672,7 +677,7 @@ impl Tester

{ let mode = pick_list( emulator::Mode::ALL, Some(self.mode), - Message::ModeSelected, + Event::ModeSelected, ) .text_size(14) .width(Fill); @@ -683,7 +688,7 @@ impl Tester

{ .size(12) .height(Fill) .font(Font::MONOSPACE) - .on_action(Message::Edited) + .on_action(Event::Edited) .into() } else if self.instructions.is_empty() { Element::from(center( @@ -761,30 +766,28 @@ impl Tester

{ let play = control(icon::play()).on_press_maybe( (!matches!(self.state, State::Recording { .. }) && !self.instructions.is_empty()) - .then_some(Message::Play), + .then_some(Event::Play), ); let record = if let State::Recording { .. } = &self.state { control(icon::stop()) - .on_press(Message::Stop) + .on_press(Event::Stop) .style(button::success) } else { control(icon::record()) - .on_press_maybe( - (!self.is_busy()).then_some(Message::Record), - ) + .on_press_maybe((!self.is_busy()).then_some(Event::Record)) .style(button::danger) }; let import = control(icon::folder()) - .on_press_maybe((!self.is_busy()).then_some(Message::Import)) + .on_press_maybe((!self.is_busy()).then_some(Event::Import)) .style(button::secondary); let export = control(icon::floppy()) .on_press_maybe( (!matches!(self.state, State::Recording { .. }) && !self.instructions.is_empty()) - .then_some(Message::Export), + .then_some(Event::Export), ) .style(button::success); @@ -799,13 +802,13 @@ impl Tester

{ } else if self.edit.is_none() { button(icon::pencil().size(14)) .padding(0) - .on_press(Message::Edit) + .on_press(Event::Edit) .style(button::text) .into() } else { button(icon::check().size(14)) .padding(0) - .on_press(Message::Confirm) + .on_press(Event::Confirm) .style(button::text) .into() }; From 3a0c78a2c0ce0488fcf2d816ad3f668adc0590a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 20 Sep 2025 14:32:45 +0200 Subject: [PATCH 74/83] Write documentation for `iced_selector` --- selector/src/find.rs | 5 +++++ selector/src/lib.rs | 20 +++++++++++++++++++- selector/src/target.rs | 21 +++++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/selector/src/find.rs b/selector/src/find.rs index eb3f0fa0..37ffd713 100644 --- a/selector/src/find.rs +++ b/selector/src/find.rs @@ -8,7 +8,12 @@ use crate::target::Candidate; use std::any::Any; +/// An [`Operation`] that runs the [`Selector`] and stops after +/// the first [`Output`](Selector::Output) is produced. pub type Find = Finder>; + +/// An [`Operation`] that runs the [`Selector`] for the entire +/// widget tree and aggregates all of its [`Output`](Selector::Output). pub type FindAll = Finder>; #[derive(Debug)] diff --git a/selector/src/lib.rs b/selector/src/lib.rs index 4e56172b..5005dd66 100644 --- a/selector/src/lib.rs +++ b/selector/src/lib.rs @@ -1,4 +1,4 @@ -#![allow(missing_docs)] +//! Select data from the widget tree. use iced_core as core; mod find; @@ -10,13 +10,29 @@ pub use target::{Bounded, Candidate, Target, Text}; use crate::core::Point; use crate::core::widget; +/// A type that traverses the widget tree to "select" data and produce some output. pub trait Selector { + /// The output type of the [`Selector`]. + /// + /// For most selectors, this will normally be a [`Target`]. However, some + /// may selectors may want to return a more limited type to encode the selection + /// guarantees in the type system. + /// + /// For instance, the implementations of [`String`] and [`str`] of [`Selector`] + /// return a [`target::Text`] instead of a generic [`Target`], since they are + /// guaranteed to only select text. type Output; + /// Performs a selection of the given [`Candidate`], if applicable. + /// + /// This method traverses the widget tree in depth-first order. fn select(&mut self, candidate: Candidate<'_>) -> Option; + /// Returns a short description of the [`Selector`] for debugging purposes. fn description(&self) -> String; + /// Returns a [`widget::Operation`] that runs the [`Selector`] and stops after + /// the first [`Output`](Self::Output) is produced. fn find(self) -> Find where Self: Sized, @@ -24,6 +40,8 @@ pub trait Selector { Find::new(find::One::new(self)) } + /// Returns a [`widget::Operation`] that runs the [`Selector`] for the entire + /// widget tree and aggregates all of its [`Output`](Self::Output). fn find_all(self) -> FindAll where Self: Sized, diff --git a/selector/src/target.rs b/selector/src/target.rs index cd0c4bba..d2d20651 100644 --- a/selector/src/target.rs +++ b/selector/src/target.rs @@ -4,6 +4,8 @@ use crate::core::{Rectangle, Vector}; use std::any::Any; +/// A generic widget match produced during selection. +#[allow(missing_docs)] #[derive(Debug, Clone, PartialEq)] pub enum Target { Container { @@ -43,6 +45,7 @@ pub enum Target { } impl Target { + /// Returns the layout bounds of the [`Target`]. pub fn bounds(&self) -> Rectangle { match self { Target::Container { bounds, .. } @@ -54,6 +57,7 @@ impl Target { } } + /// Returns the visible bounds of the [`Target`], in screen coordinates. pub fn visible_bounds(&self) -> Option { match self { Target::Container { visible_bounds, .. } @@ -148,6 +152,10 @@ impl Bounded for Target { } } +/// A selection candidate. +/// +/// This is provided to [`Selector::select`](crate::Selector::select). +#[allow(missing_docs)] #[derive(Clone)] pub enum Candidate<'a> { Container { @@ -190,6 +198,7 @@ pub enum Candidate<'a> { } impl<'a> Candidate<'a> { + /// Returns the widget [`Id`] of the [`Candidate`]. pub fn id(&self) -> Option<&'a Id> { match self { Candidate::Container { id, .. } @@ -201,6 +210,7 @@ impl<'a> Candidate<'a> { } } + /// Returns the layout bounds of the [`Candidate`]. pub fn bounds(&self) -> Rectangle { match self { Candidate::Container { bounds, .. } @@ -212,6 +222,7 @@ impl<'a> Candidate<'a> { } } + /// Returns the visible bounds of the [`Candidate`], in screen coordinates. pub fn visible_bounds(&self) -> Option { match self { Candidate::Container { visible_bounds, .. } @@ -224,12 +235,20 @@ impl<'a> Candidate<'a> { } } +/// A bounded type has both layout bounds and visible bounds. +/// +/// This trait lets us write generic code over the [`Output`](crate::Selector::Output) +/// of a [`Selector`](crate::Selector). pub trait Bounded: std::fmt::Debug { + /// Returns the layout bounds. fn bounds(&self) -> Rectangle; + /// Returns the visible bounds, in screen coordinates. fn visible_bounds(&self) -> Option; } +/// A text match. +#[allow(missing_docs)] #[derive(Debug, Clone, PartialEq)] pub enum Text { Raw { @@ -245,12 +264,14 @@ pub enum Text { } impl Text { + /// Returns the layout bounds of the [`Text`]. pub fn bounds(&self) -> Rectangle { match self { Text::Raw { bounds, .. } | Text::Input { bounds, .. } => *bounds, } } + /// Returns the visible bounds of the [`Text`], in screen coordinates. pub fn visible_bounds(&self) -> Option { match self { Text::Raw { visible_bounds, .. } From c67ff2aadbce83a0a812c95a29c67da4d7416156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 20 Sep 2025 14:33:42 +0200 Subject: [PATCH 75/83] Fix typo in docs of `Selector::Output` --- selector/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selector/src/lib.rs b/selector/src/lib.rs index 5005dd66..e5703df5 100644 --- a/selector/src/lib.rs +++ b/selector/src/lib.rs @@ -15,7 +15,7 @@ pub trait Selector { /// The output type of the [`Selector`]. /// /// For most selectors, this will normally be a [`Target`]. However, some - /// may selectors may want to return a more limited type to encode the selection + /// selectors may want to return a more limited type to encode the selection /// guarantees in the type system. /// /// For instance, the implementations of [`String`] and [`str`] of [`Selector`] From 33256ac444ef6e9bbd9d6f51e03719af7492dd66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 20 Sep 2025 15:44:29 +0200 Subject: [PATCH 76/83] Use `labeled_slider` for `viewport` size in `tester` --- tester/src/lib.rs | 110 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 92 insertions(+), 18 deletions(-) diff --git a/tester/src/lib.rs b/tester/src/lib.rs index 2edf44bb..f89acce7 100644 --- a/tester/src/lib.rs +++ b/tester/src/lib.rs @@ -17,7 +17,7 @@ use crate::core::alignment::Horizontal::Right; use crate::core::border; use crate::core::mouse; use crate::core::window; -use crate::core::{Element, Font, Settings, Size, Theme}; +use crate::core::{Color, Element, Font, Settings, Size, Theme}; use crate::futures::futures::channel::mpsc; use crate::program::Program; use crate::runtime::task::{self, Task}; @@ -27,9 +27,11 @@ use crate::test::instruction; use crate::test::{Emulator, Ice, Instruction}; use crate::widget::{ button, center, column, combo_box, container, pick_list, row, rule, - scrollable, space, text, text_editor, text_input, themer, + scrollable, slider, space, stack, text, text_editor, themer, }; +use std::ops::RangeInclusive; + /// Attaches a [`Tester`] to the given [`Program`]. pub fn attach(program: P) -> Attach

{ Attach { program } @@ -134,7 +136,7 @@ pub struct Message(Tick

); #[derive(Debug, Clone)] enum Event { - ChangeViewport(Size), + ViewportChanged(Size), ModeSelected(emulator::Mode), PresetSelected(String), Record, @@ -192,7 +194,7 @@ impl Tester

{ fn update(&mut self, program: &P, event: Event) -> Task> { match event { - Event::ChangeViewport(viewport) => { + Event::ViewportChanged(viewport) => { self.viewport = viewport; Task::none() @@ -647,23 +649,29 @@ impl Tester

{ } fn controls(&self) -> Element<'_, Event, Theme, P::Renderer> { - let viewport = row![ - text_input("Width", &self.viewport.width.to_string()) - .size(14) - .on_input(|width| Event::ChangeViewport(Size { - width: width.parse().unwrap_or(self.viewport.width), + let viewport = column![ + labeled_slider( + "Width", + 100.0..=2000.0, + self.viewport.width, + |width| Event::ViewportChanged(Size { + width, ..self.viewport - })), - text("x").size(14).font(Font::MONOSPACE), - text_input("Height", &self.viewport.height.to_string()) - .size(14) - .on_input(|height| Event::ChangeViewport(Size { - height: height.parse().unwrap_or(self.viewport.height), + }), + |width| format!("{width:.0}"), + ), + labeled_slider( + "Height", + 100.0..=2000.0, + self.viewport.height, + |height| Event::ViewportChanged(Size { + height, ..self.viewport - })), + }), + |height| format!("{height:.0}"), + ), ] - .spacing(10) - .align_y(Center); + .spacing(10); let preset = combo_box( &self.presets, @@ -862,3 +870,69 @@ where .spacing(5) .into() } + +fn labeled_slider<'a, Message, Renderer>( + label: impl text::IntoFragment<'a>, + range: RangeInclusive, + current: f32, + on_change: impl Fn(f32) -> Message + 'a, + to_string: impl Fn(&f32) -> String, +) -> Element<'a, Message, Theme, Renderer> +where + Message: Clone + 'a, + Renderer: core::text::Renderer + 'a, +{ + stack![ + container( + slider(range, current, on_change) + .step(10.0) + .width(Fill) + .height(24) + .style(|theme: &core::Theme, status| { + let palette = theme.extended_palette(); + + slider::Style { + rail: slider::Rail { + backgrounds: ( + match status { + slider::Status::Active + | slider::Status::Dragged => { + palette.background.strongest.color + } + slider::Status::Hovered => { + palette.background.stronger.color + } + } + .into(), + Color::TRANSPARENT.into(), + ), + width: 24.0, + border: border::rounded(2), + }, + handle: slider::Handle { + shape: slider::HandleShape::Circle { radius: 0.0 }, + background: Color::TRANSPARENT.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + } + }) + ) + .style(|theme| container::Style::default() + .background(theme.extended_palette().background.weak.color) + .border(border::rounded(2))), + row![ + text(label).size(14).style(|theme: &core::Theme| { + text::Style { + color: Some(theme.extended_palette().background.weak.text), + } + }), + space::horizontal(), + text(to_string(¤t)).size(14) + ] + .padding([0, 10]) + .height(Fill) + .align_y(Center), + ] + .into() +} From 364b68f0304989145c740cdacffb96cc757a416d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 20 Sep 2025 15:50:07 +0200 Subject: [PATCH 77/83] Rename `Mode::Impatient` to `Immediate` --- examples/todos/tests/carl_sagan.ice | 2 +- test/src/emulator.rs | 8 ++++---- test/src/ice.rs | 14 +++----------- 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/examples/todos/tests/carl_sagan.ice b/examples/todos/tests/carl_sagan.ice index cb9b91ed..aaf8cb5c 100644 --- a/examples/todos/tests/carl_sagan.ice +++ b/examples/todos/tests/carl_sagan.ice @@ -1,5 +1,5 @@ viewport: 500x800 -mode: Impatient +mode: Immediate preset: Empty ----- click at "What needs to be done?" diff --git a/test/src/emulator.rs b/test/src/emulator.rs index 5f46caf2..0bd653d7 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -391,7 +391,7 @@ impl Emulator

{ .boxed(), ); } - Mode::Impatient => { + Mode::Immediate => { self.runtime.run( stream .map(Action_::Runtime) @@ -452,12 +452,12 @@ pub enum Mode { /// Waits only for the tasks directly spawned by an [`Instruction`]. Patient, /// Never waits for any tasks to finish. - Impatient, + Immediate, } impl Mode { /// A list of all the available modes. - pub const ALL: &[Self] = &[Self::Zen, Self::Patient, Self::Impatient]; + pub const ALL: &[Self] = &[Self::Zen, Self::Patient, Self::Immediate]; } impl fmt::Display for Mode { @@ -465,7 +465,7 @@ impl fmt::Display for Mode { f.write_str(match self { Self::Zen => "Zen", Self::Patient => "Patient", - Self::Impatient => "Impatient", + Self::Immediate => "Immediate", }) } } diff --git a/test/src/ice.rs b/test/src/ice.rs index 2777cf8e..cbad1822 100644 --- a/test/src/ice.rs +++ b/test/src/ice.rs @@ -34,7 +34,7 @@ impl Ice { /// /// ```text /// viewport: 500x800 - /// mode: Impatient + /// mode: Immediate /// preset: Empty /// ----- /// click at "What needs to be done?" @@ -96,7 +96,7 @@ impl Ice { mode = Some(match value.trim().to_lowercase().as_str() { "zen" => emulator::Mode::Zen, "patient" => emulator::Mode::Patient, - "impatient" => emulator::Mode::Impatient, + "immediate" => emulator::Mode::Immediate, _ => { return Err(ParseError::InvalidMode { line: i, @@ -157,15 +157,7 @@ impl std::fmt::Display for Ice { height = self.viewport.height as u32, // TODO )?; - writeln!( - f, - "mode: {}", - match self.mode { - emulator::Mode::Zen => "Zen", - emulator::Mode::Patient => "Patient", - emulator::Mode::Impatient => "Impatient", - } - )?; + writeln!(f, "mode: {}", self.mode)?; if let Some(preset) = &self.preset { writeln!(f, "preset: {preset}")?; From 79d501f9a512cf6c6804077f7f83dae663497979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 20 Sep 2025 17:33:57 +0200 Subject: [PATCH 78/83] Remove `at` from mouse instructions in `Ice` syntax --- examples/todos/src/main.rs | 5 +- examples/todos/tests/carl_sagan.ice | 6 +- test/src/instruction.rs | 107 +++++++++++++++++----------- tester/src/lib.rs | 2 +- tester/src/recorder.rs | 21 ++++-- 5 files changed, 89 insertions(+), 52 deletions(-) diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 6f4e0808..e71ee4d2 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -1,4 +1,5 @@ use iced::keyboard; +use iced::time::milliseconds; use iced::widget::{ self, Text, button, center, center_x, checkbox, column, keyed_column, operation, row, scrollable, text, text_input, @@ -544,8 +545,8 @@ impl SavedState { .map_err(|_| SaveError::Write)?; } - // This is a simple way to save at most once every couple seconds - tokio::time::sleep(std::time::Duration::from_secs(2)).await; + // This is a simple way to save at most twice every second + tokio::time::sleep(milliseconds(500)).await; Ok(()) } diff --git a/examples/todos/tests/carl_sagan.ice b/examples/todos/tests/carl_sagan.ice index aaf8cb5c..16d9bd4f 100644 --- a/examples/todos/tests/carl_sagan.ice +++ b/examples/todos/tests/carl_sagan.ice @@ -2,13 +2,13 @@ viewport: 500x800 mode: Immediate preset: Empty ----- -click at "What needs to be done?" +click "What needs to be done?" type "Create the universe" type enter type "Make an apple pie" type enter expect "2 tasks left" -click at "Create the universe" +click "Create the universe" expect "1 task left" -click at "Make an apple pie" +click "Make an apple pie" expect "0 tasks left" diff --git a/test/src/instruction.rs b/test/src/instruction.rs index c9feeb40..99d0d505 100644 --- a/test/src/instruction.rs +++ b/test/src/instruction.rs @@ -54,11 +54,11 @@ impl Interaction { } mouse::Event::ButtonPressed(button) => Mouse::Press { button: *button, - at: None, + target: None, }, mouse::Event::ButtonReleased(button) => Mouse::Release { button: *button, - at: None, + target: None, }, _ => None?, }), @@ -117,28 +117,40 @@ impl Interaction { (Mouse::Move(_), Mouse::Move(to)) => { (Self::Mouse(Mouse::Move(to)), None) } - (Mouse::Move(to), Mouse::Press { button, at: None }) => ( + ( + Mouse::Move(to), + Mouse::Press { + button, + target: None, + }, + ) => ( Self::Mouse(Mouse::Press { button, - at: Some(to), + target: Some(to), }), None, ), - (Mouse::Move(to), Mouse::Release { button, at: None }) => ( + ( + Mouse::Move(to), + Mouse::Release { + button, + target: None, + }, + ) => ( Self::Mouse(Mouse::Release { button, - at: Some(to), + target: Some(to), }), None, ), ( Mouse::Press { button: press, - at: press_at, + target: press_at, }, Mouse::Release { button: release, - at: release_at, + target: release_at, }, ) if press == release && release_at.as_ref().is_none_or(|release_at| { @@ -148,7 +160,7 @@ impl Interaction { ( Self::Mouse(Mouse::Click { button: press, - at: press_at, + target: press_at, }), None, ) @@ -156,26 +168,26 @@ impl Interaction { ( Mouse::Press { button, - at: Some(press_at), + target: Some(press_at), }, Mouse::Move(move_at), ) if press_at == move_at => ( Self::Mouse(Mouse::Press { button, - at: Some(press_at), + target: Some(press_at), }), None, ), ( Mouse::Click { button, - at: Some(click_at), + target: Some(click_at), }, Mouse::Move(move_at), ) if click_at == move_at => ( Self::Mouse(Mouse::Click { button, - at: Some(click_at), + target: Some(click_at), }), None, ), @@ -235,23 +247,29 @@ impl Interaction { Mouse::Move(to) => vec![mouse_move_(find_target(to)?)], Mouse::Press { button, - at: Some(at), + target: Some(at), } => vec![mouse_move_(find_target(at)?), mouse_press(*button)], - Mouse::Press { button, at: None } => { + Mouse::Press { + button, + target: None, + } => { vec![mouse_press(*button)] } Mouse::Release { button, - at: Some(at), + target: Some(at), } => { vec![mouse_move_(find_target(at)?), mouse_release(*button)] } - Mouse::Release { button, at: None } => { + Mouse::Release { + button, + target: None, + } => { vec![mouse_release(*button)] } Mouse::Click { button, - at: Some(at), + target: Some(at), } => { vec![ mouse_move_(find_target(at)?), @@ -259,7 +277,10 @@ impl Interaction { mouse_release(*button), ] } - Mouse::Click { button, at: None } => { + Mouse::Click { + button, + target: None, + } => { vec![mouse_press(*button), mouse_release(*button)] } }, @@ -294,21 +315,21 @@ pub enum Mouse { /// The button. button: mouse::Button, /// The location of the press. - at: Option, + target: Option, }, /// A button was released. Release { /// The button. button: mouse::Button, /// The location of the release. - at: Option, + target: Option, }, /// A button was clicked. Click { /// The button. button: mouse::Button, /// The location of the click. - at: Option, + target: Option, }, } @@ -318,14 +339,26 @@ impl fmt::Display for Mouse { Mouse::Move(target) => { write!(f, "move cursor to {}", target) } - Mouse::Press { button, at } => { - write!(f, "press {}", format::button_at(*button, at.as_ref())) + Mouse::Press { button, target } => { + write!( + f, + "press {}", + format::button_at(*button, target.as_ref()) + ) } - Mouse::Release { button, at } => { - write!(f, "release {}", format::button_at(*button, at.as_ref())) + Mouse::Release { button, target } => { + write!( + f, + "release {}", + format::button_at(*button, target.as_ref()) + ) } - Mouse::Click { button, at } => { - write!(f, "click {}", format::button_at(*button, at.as_ref())) + Mouse::Click { button, target } => { + write!( + f, + "click {}", + format::button_at(*button, target.as_ref()) + ) } } } @@ -412,9 +445,9 @@ mod format { if let Some(at) = at { if button.is_empty() { - format!("at {}", at) + at.to_string() } else { - format!("{} at {}", button, at) + format!("{} {}", button, at) } } else { button.to_owned() @@ -479,9 +512,7 @@ mod parser { use nom::bytes::complete::tag; use nom::bytes::{is_not, take_while_m_n}; use nom::character::complete::{char, multispace0, multispace1}; - use nom::combinator::{ - cut, map, map_opt, map_res, opt, success, value, verify, - }; + use nom::combinator::{map, map_opt, map_res, opt, success, value, verify}; use nom::error::ParseError; use nom::multi::fold; use nom::number::float; @@ -526,9 +557,9 @@ mod parser { fn mouse_click(input: &str) -> IResult<&str, Mouse> { let (input, _) = tag("click ")(input)?; - let (input, (button, at)) = mouse_button_at(input)?; + let (input, (button, target)) = mouse_button_at(input)?; - Ok((input, Mouse::Click { button, at })) + Ok((input, Mouse::Click { button, target })) } fn mouse_button_at( @@ -541,11 +572,7 @@ mod parser { } fn target(input: &str) -> IResult<&str, Target> { - preceded( - whitespace(tag("at ")), - cut(alt((string.map(Target::Text), point.map(Target::Point)))), - ) - .parse(input) + alt((string.map(Target::Text), point.map(Target::Point))).parse(input) } fn mouse_button(input: &str) -> IResult<&str, mouse::Button> { diff --git a/tester/src/lib.rs b/tester/src/lib.rs index f89acce7..ad393c98 100644 --- a/tester/src/lib.rs +++ b/tester/src/lib.rs @@ -502,7 +502,7 @@ impl Tester

{ let instruction::Interaction::Mouse( instruction::Mouse::Click { button: mouse::Button::Left, - at: Some(instruction::Target::Text(text)), + target: Some(instruction::Target::Text(text)), }, ) = interaction else { diff --git a/tester/src/recorder.rs b/tester/src/recorder.rs index 03a161e4..3baefd25 100644 --- a/tester/src/recorder.rs +++ b/tester/src/recorder.rs @@ -415,17 +415,26 @@ fn record( }; let Interaction::Mouse( - Mouse::Move(at) - | Mouse::Press { at: Some(at), .. } - | Mouse::Release { at: Some(at), .. } - | Mouse::Click { at: Some(at), .. }, + Mouse::Move(target) + | Mouse::Press { + target: Some(target), + .. + } + | Mouse::Release { + target: Some(target), + .. + } + | Mouse::Click { + target: Some(target), + .. + }, ) = &mut interaction else { shell.publish(on_record(interaction)); return; }; - let Target::Point(position) = *at else { + let Target::Point(position) = *target else { shell.publish(on_record(interaction)); return; }; @@ -433,7 +442,7 @@ fn record( if let Some((content, visible_bounds)) = find_text(position + (bounds.position() - Point::ORIGIN), operate) { - *at = Target::Text(content); + *target = Target::Text(content); *last_hovered = visible_bounds; } else { *last_hovered = None; From 4366a7cdb99dd5003c7fce0a41d17ee0ec5922be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 20 Sep 2025 17:41:39 +0200 Subject: [PATCH 79/83] Preview `Preset` in `tester` when selected --- tester/src/lib.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tester/src/lib.rs b/tester/src/lib.rs index ad393c98..d8a8df71 100644 --- a/tester/src/lib.rs +++ b/tester/src/lib.rs @@ -207,6 +207,13 @@ impl Tester

{ Event::PresetSelected(preset) => { self.preset = Some(preset); + let (state, _) = self + .preset(program) + .map(program::Preset::boot) + .unwrap_or_else(|| program.boot()); + + self.state = State::Idle { state }; + Task::none() } Event::Record => { From 8a1462b54c72f7bc22f3fa3392baf89691fd6272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 23 Sep 2025 02:11:02 +0200 Subject: [PATCH 80/83] Update `Ice` example in documentation --- examples/todos/src/main.rs | 2 +- test/src/ice.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index e71ee4d2..168c5c81 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -588,7 +588,7 @@ impl SavedState { } } -fn presets() -> impl Iterator> { +fn presets() -> impl IntoIterator> { [ Preset::new("Empty", || { (Todos::Loaded(State::default()), Command::none()) diff --git a/test/src/ice.rs b/test/src/ice.rs index cbad1822..bc26fd77 100644 --- a/test/src/ice.rs +++ b/test/src/ice.rs @@ -37,15 +37,15 @@ impl Ice { /// mode: Immediate /// preset: Empty /// ----- - /// click at "What needs to be done?" + /// click "What needs to be done?" /// type "Create the universe" /// type enter /// type "Make an apple pie" /// type enter /// expect "2 tasks left" - /// click at "Create the universe" + /// click "Create the universe" /// expect "1 task left" - /// click at "Make an apple pie" + /// click "Make an apple pie" /// expect "0 tasks left" /// ``` /// From 145534c92e3ea493a065abf47ef5b09b747b01ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 23 Sep 2025 02:23:12 +0200 Subject: [PATCH 81/83] Remove `push_maybe` from `stack` widget --- devtools/src/lib.rs | 19 +++++++++---------- widget/src/stack.rs | 24 +++++++----------------- 2 files changed, 16 insertions(+), 27 deletions(-) diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index c806912d..6ad7ae9a 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -344,11 +344,10 @@ where .background(Color::BLACK.scale_alpha(0.8)) }); - Some(setup) + Some(themer(theme(), Element::from(setup).map(Event::Message))) } else { None - } - .map(|setup| themer(theme(), Element::from(setup).map(Event::Message))); + }; let notification = self .show_notification @@ -359,12 +358,8 @@ where "Types have changed. Restart to re-enable hotpatching.", ) }) - }); - - stack![view] - .height(Fill) - .push_maybe(setup.map(opaque)) - .push_maybe(notification.map(|notification| { + }) + .map(|notification| { themer( theme(), bottom_right(opaque( @@ -373,7 +368,11 @@ where .style(container::dark), )), ) - })) + }); + + stack![view, setup, notification] + .width(Fill) + .height(Fill) .into() } diff --git a/widget/src/stack.rs b/widget/src/stack.rs index 6cfe0311..65c5aeb0 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -84,30 +84,20 @@ where child: impl Into>, ) -> Self { let child = child.into(); + let child_size = child.as_widget().size_hint(); - if self.children.is_empty() { - let child_size = child.as_widget().size_hint(); + if !child_size.is_void() { + if self.children.is_empty() { + self.width = self.width.enclose(child_size.width); + self.height = self.height.enclose(child_size.height); + } - self.width = self.width.enclose(child_size.width); - self.height = self.height.enclose(child_size.height); + self.children.push(child); } - self.children.push(child); self } - /// Adds an element to the [`Stack`], if `Some`. - pub fn push_maybe( - self, - child: Option>>, - ) -> Self { - if let Some(child) = child { - self.push(child) - } else { - self - } - } - /// Extends the [`Stack`] with the given children. pub fn extend( self, From 629e7957844a363e3ee4909323da39cd12deafdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 23 Sep 2025 02:23:32 +0200 Subject: [PATCH 82/83] Avoid underflow in `stack` widget Fixes #3062 --- widget/src/stack.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/widget/src/stack.rs b/widget/src/stack.rs index 65c5aeb0..2a062ee4 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -217,6 +217,10 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { + if self.children.is_empty() { + return; + } + let is_over = cursor.is_over(layout.bounds()); let end = self.children.len() - 1; From 714c4982d677b3c0d675aeef18b528a193942c99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 23 Sep 2025 02:27:42 +0200 Subject: [PATCH 83/83] Use `Task::discard` in `websocket` example --- examples/websocket/src/echo.rs | 1 - examples/websocket/src/main.rs | 14 +++++--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/examples/websocket/src/echo.rs b/examples/websocket/src/echo.rs index 1116d1ea..85b3caf6 100644 --- a/examples/websocket/src/echo.rs +++ b/examples/websocket/src/echo.rs @@ -30,7 +30,6 @@ pub fn connect() -> impl Sipper { tokio::time::sleep(tokio::time::Duration::from_secs(1)) .await; - output.send(Event::Disconnected).await; continue; } }; diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index a868be70..1968843a 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -1,6 +1,5 @@ mod echo; -use iced::futures::stream; use iced::widget::{ button, center, column, operation, row, scrollable, text, text_input, }; @@ -23,7 +22,6 @@ enum Message { NewMessageChanged(String), Send(echo::Message), Echo(echo::Event), - Server, } impl WebSocket { @@ -34,7 +32,10 @@ impl WebSocket { new_message: String::new(), state: State::Disconnected, }, - operation::focus_next(), + Task::batch([ + Task::future(echo::server::run()).discard(), + operation::focus_next(), + ]), ) } @@ -76,16 +77,11 @@ impl WebSocket { operation::snap_to_end(MESSAGE_LOG) } }, - Message::Server => Task::none(), } } fn subscription(&self) -> Subscription { - Subscription::batch([ - Subscription::run(|| stream::once(echo::server::run())) - .map(|_| Message::Server), - Subscription::run(echo::connect).map(Message::Echo), - ]) + Subscription::run(echo::connect).map(Message::Echo) } fn view(&self) -> Element<'_, Message> {