diff --git a/Cargo.lock b/Cargo.lock index 9b64572e18..48c31dd50d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,7 +85,7 @@ dependencies = [ "ndk-context", "ndk-sys 0.6.0+11769913", "num_enum", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -747,7 +747,7 @@ dependencies = [ "polling 3.7.4", "rustix 0.38.43", "slab", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -821,7 +821,7 @@ dependencies = [ "log", "reqwest", "serde", - "thiserror 1.0.69", + "thiserror", "tokio", "tracing-subscriber", "webbrowser", @@ -935,7 +935,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4274ea815e013e0f9f04a2633423e14194e408a0576c943ce3d14ca56c50031c" dependencies = [ - "thiserror 1.0.69", + "thiserror", "x11rb", ] @@ -1053,7 +1053,7 @@ dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", "core-graphics-types 0.1.3", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -1066,7 +1066,7 @@ dependencies = [ "bitflags 2.8.0", "core-foundation 0.10.0", "core-graphics-types 0.2.0", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -1690,6 +1690,15 @@ dependencies = [ "ttf-parser 0.21.1", ] +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -1697,7 +1706,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared", + "foreign-types-shared 0.3.1", ] [[package]] @@ -1711,6 +1720,12 @@ dependencies = [ "syn", ] +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -1844,6 +1859,18 @@ dependencies = [ "slab", ] +[[package]] +name = "gallery" +version = "0.1.0" +dependencies = [ + "bytes", + "iced", + "image", + "reqwest", + "serde", + "tokio", +] + [[package]] name = "game_of_life" version = "0.1.0" @@ -2076,7 +2103,7 @@ checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" dependencies = [ "log", "presser", - "thiserror 1.0.69", + "thiserror", "windows 0.58.0", ] @@ -2155,6 +2182,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.2.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "2.4.1" @@ -2321,7 +2367,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -2344,6 +2390,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2 0.4.7", "http 1.2.0", "http-body 1.0.1", "httparse", @@ -2369,7 +2416,22 @@ dependencies = [ "tokio", "tokio-rustls 0.26.1", "tower-service", - "webpki-roots", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.5.2", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", ] [[package]] @@ -2427,7 +2489,7 @@ dependencies = [ "iced_widget", "iced_winit", "image", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -2439,12 +2501,13 @@ dependencies = [ "bytes", "dark-light", "glam", + "lilt", "log", "num-traits", "palette", "rustc-hash 2.1.0", "smol_str", - "thiserror 1.0.69", + "thiserror", "web-time", ] @@ -2479,7 +2542,7 @@ dependencies = [ "lyon_path", "raw-window-handle 0.6.2", "rustc-hash 2.1.0", - "thiserror 1.0.69", + "thiserror", "unicode-segmentation", ] @@ -2499,7 +2562,7 @@ dependencies = [ "iced_tiny_skia", "iced_wgpu", "log", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -2510,7 +2573,7 @@ dependencies = [ "iced_core", "iced_futures", "raw-window-handle 0.6.2", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -2521,7 +2584,7 @@ dependencies = [ "iced_runtime", "png", "sha2", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -2554,7 +2617,7 @@ dependencies = [ "lyon", "resvg", "rustc-hash 2.1.0", - "thiserror 1.0.69", + "thiserror", "wgpu", ] @@ -2570,7 +2633,7 @@ dependencies = [ "pulldown-cmark", "qrcode", "rustc-hash 2.1.0", - "thiserror 1.0.69", + "thiserror", "unicode-segmentation", "url", ] @@ -2585,7 +2648,7 @@ dependencies = [ "log", "rustc-hash 2.1.0", "sysinfo", - "thiserror 1.0.69", + "thiserror", "tracing", "wasm-bindgen-futures", "web-sys", @@ -2904,7 +2967,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror 1.0.69", + "thiserror", "walkdir", "windows-sys 0.45.0", ] @@ -3064,6 +3127,12 @@ dependencies = [ "redox_syscall 0.5.8", ] +[[package]] +name = "lilt" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a095f60643436f4a6bddb82f7f37c1a9247990a2ee3421cf4faa43715edd5896" + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -3270,7 +3339,7 @@ dependencies = [ "bitflags 2.8.0", "block", "core-graphics-types 0.1.3", - "foreign-types", + "foreign-types 0.5.0", "log", "objc", "paste", @@ -3383,10 +3452,27 @@ dependencies = [ "rustc-hash 1.1.0", "spirv", "termcolor", - "thiserror 1.0.69", + "thiserror", "unicode-xid", ] +[[package]] +name = "native-tls" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "ndk" version = "0.9.0" @@ -3399,7 +3485,7 @@ dependencies = [ "ndk-sys 0.6.0+11769913", "num_enum", "raw-window-handle 0.6.2", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -3854,6 +3940,50 @@ dependencies = [ "pathdiff", ] +[[package]] +name = "openssl" +version = "0.10.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e534d133a060a3c19daec1eb3e98ec6f4685978834f2dbadfe2ec215bab64e" +dependencies = [ + "bitflags 2.8.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "orbclient" version = "0.3.48" @@ -4371,58 +4501,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "quinn" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" -dependencies = [ - "bytes", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash 2.1.0", - "rustls 0.23.21", - "socket2 0.5.8", - "thiserror 2.0.11", - "tokio", - "tracing", -] - -[[package]] -name = "quinn-proto" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" -dependencies = [ - "bytes", - "getrandom", - "rand", - "ring", - "rustc-hash 2.1.0", - "rustls 0.23.21", - "rustls-pki-types", - "slab", - "thiserror 2.0.11", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c40286217b4ba3a71d644d752e6a0b71f13f1b6a2c5311acfcbe0c2418ed904" -dependencies = [ - "cfg_aliases 0.2.1", - "libc", - "once_cell", - "socket2 0.5.8", - "tracing", - "windows-sys 0.59.0", -] - [[package]] name = "quote" version = "1.0.38" @@ -4504,7 +4582,7 @@ dependencies = [ "rand_chacha", "simd_helpers", "system-deps", - "thiserror 1.0.69", + "thiserror", "v_frame", "wasm-bindgen", ] @@ -4601,7 +4679,7 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom", "libredox", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -4647,31 +4725,33 @@ checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" dependencies = [ "base64 0.22.1", "bytes", + "encoding_rs", "futures-core", "futures-util", + "h2 0.4.7", "http 1.2.0", "http-body 1.0.1", "http-body-util", "hyper 1.5.2", "hyper-rustls", + "hyper-tls", "hyper-util", "ipnet", "js-sys", "log", "mime", + "native-tls", "once_cell", "percent-encoding", "pin-project-lite", - "quinn", - "rustls 0.23.21", "rustls-pemfile", - "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", + "system-configuration", "tokio", - "tokio-rustls 0.26.1", + "tokio-native-tls", "tokio-util", "tower", "tower-service", @@ -4680,7 +4760,6 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots", "windows-registry", ] @@ -4819,7 +4898,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" dependencies = [ "once_cell", - "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -4840,9 +4918,6 @@ name = "rustls-pki-types" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" -dependencies = [ - "web-time", -] [[package]] name = "rustls-webpki" @@ -4893,6 +4968,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -4935,6 +5019,29 @@ dependencies = [ "tiny-skia", ] +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.8.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "self_cell" version = "1.1.0" @@ -5144,7 +5251,7 @@ dependencies = [ "log", "memmap2", "rustix 0.38.43", - "thiserror 1.0.69", + "thiserror", "wayland-backend", "wayland-client", "wayland-csd-frame", @@ -5224,7 +5331,7 @@ dependencies = [ "core-graphics 0.24.0", "drm", "fastrand 2.3.0", - "foreign-types", + "foreign-types 0.5.0", "js-sys", "log", "memmap2", @@ -5391,7 +5498,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "thiserror 1.0.69", + "thiserror", "walkdir", "yaml-rust", ] @@ -5420,6 +5527,27 @@ dependencies = [ "windows 0.52.0", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.8.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -5485,16 +5613,7 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" -dependencies = [ - "thiserror-impl 2.0.11", + "thiserror-impl", ] [[package]] @@ -5508,17 +5627,6 @@ dependencies = [ "syn", ] -[[package]] -name = "thiserror-impl" -version = "2.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "thread_local" version = "1.1.8" @@ -5684,6 +5792,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.25.0" @@ -5906,7 +6024,7 @@ dependencies = [ "rustls 0.22.4", "rustls-pki-types", "sha1", - "thiserror 1.0.69", + "thiserror", "url", "utf-8", ] @@ -6105,6 +6223,12 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "vectorial_text" version = "0.1.0" @@ -6516,7 +6640,7 @@ dependencies = [ "raw-window-handle 0.6.2", "rustc-hash 1.1.0", "smallvec", - "thiserror 1.0.69", + "thiserror", "wgpu-hal", "wgpu-types", ] @@ -6558,7 +6682,7 @@ dependencies = [ "renderdoc-sys", "rustc-hash 1.1.0", "smallvec", - "thiserror 1.0.69", + "thiserror", "wasm-bindgen", "web-sys", "wgpu-types", @@ -6619,7 +6743,7 @@ dependencies = [ "clipboard_wayland", "clipboard_x11", "raw-window-handle 0.6.2", - "thiserror 1.0.69", + "thiserror", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 89c28139fe..58b54897fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -158,6 +158,7 @@ half = "2.2" image = { version = "0.25", default-features = false } kamadak-exif = "0.5" kurbo = "0.10" +lilt = "0.7" log = "0.4" lyon = "1.0" lyon_path = "1.0" @@ -192,7 +193,7 @@ window_clipboard = "0.4.1" winit = { git = "https://github.com/iced-rs/winit.git", rev = "11414b6aa45699f038114e61b4ddf5102b2d3b4b" } [workspace.lints.rust] -rust_2018_idioms = { level = "forbid", priority = -1 } +rust_2018_idioms = { level = "deny", priority = -1 } missing_debug_implementations = "deny" missing_docs = "deny" unsafe_code = "deny" diff --git a/core/Cargo.toml b/core/Cargo.toml index a3bc6745fb..1ca7260c22 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -21,6 +21,7 @@ advanced = [] bitflags.workspace = true bytes.workspace = true glam.workspace = true +lilt.workspace = true log.workspace = true num-traits.workspace = true palette.workspace = true diff --git a/core/src/animation.rs b/core/src/animation.rs new file mode 100644 index 0000000000..258fd084b7 --- /dev/null +++ b/core/src/animation.rs @@ -0,0 +1,136 @@ +//! Animate your applications. +use crate::time::{Duration, Instant}; + +pub use lilt::{Easing, FloatRepresentable as Float, Interpolable}; + +/// The animation of some particular state. +/// +/// It tracks state changes and allows projecting interpolated values +/// through time. +#[derive(Debug, Clone)] +pub struct Animation +where + T: Clone + Copy + PartialEq + Float, +{ + raw: lilt::Animated, +} + +impl Animation +where + T: Clone + Copy + PartialEq + Float, +{ + /// Creates a new [`Animation`] with the given initial state. + pub fn new(state: T) -> Self { + Self { + raw: lilt::Animated::new(state), + } + } + + /// Sets the [`Easing`] function of the [`Animation`]. + /// + /// See the [Easing Functions Cheat Sheet](https://easings.net) for + /// details! + pub fn easing(mut self, easing: Easing) -> Self { + self.raw = self.raw.easing(easing); + self + } + + /// Sets the duration of the [`Animation`] to 100ms. + pub fn very_quick(self) -> Self { + self.duration(Duration::from_millis(100)) + } + + /// Sets the duration of the [`Animation`] to 200ms. + pub fn quick(self) -> Self { + self.duration(Duration::from_millis(200)) + } + + /// Sets the duration of the [`Animation`] to 400ms. + pub fn slow(self) -> Self { + self.duration(Duration::from_millis(400)) + } + + /// Sets the duration of the [`Animation`] to 500ms. + pub fn very_slow(self) -> Self { + self.duration(Duration::from_millis(500)) + } + + /// Sets the duration of the [`Animation`] to the given value. + pub fn duration(mut self, duration: Duration) -> Self { + self.raw = self.raw.duration(duration.as_secs_f32() * 1_000.0); + self + } + + /// Sets a delay for the [`Animation`]. + pub fn delay(mut self, duration: Duration) -> Self { + self.raw = self.raw.delay(duration.as_secs_f64() as f32 * 1000.0); + self + } + + /// Makes the [`Animation`] repeat a given amount of times. + /// + /// Providing 1 repetition plays the animation twice in total. + pub fn repeat(mut self, repetitions: u32) -> Self { + self.raw = self.raw.repeat(repetitions); + self + } + + /// Makes the [`Animation`] repeat forever. + pub fn repeat_forever(mut self) -> Self { + self.raw = self.raw.repeat_forever(); + self + } + + /// Makes the [`Animation`] automatically reverse when repeating. + pub fn auto_reverse(mut self) -> Self { + self.raw = self.raw.auto_reverse(); + self + } + + /// Transitions the [`Animation`] from its current state to the given new state. + pub fn go(mut self, new_state: T) -> Self { + self.go_mut(new_state); + self + } + + /// Transitions the [`Animation`] from its current state to the given new state, by reference. + pub fn go_mut(&mut self, new_state: T) { + self.raw.transition(new_state, Instant::now()); + } + + /// Returns true if the [`Animation`] is currently in progress. + /// + /// An [`Animation`] is in progress when it is transitioning to a different state. + pub fn is_animating(&self, at: Instant) -> bool { + self.raw.in_progress(at) + } + + /// Projects the [`Animation`] into an interpolated value at the given [`Instant`]; using the + /// closure provided to calculate the different keyframes of interpolated values. + /// + /// If the [`Animation`] state is a `bool`, you can use the simpler [`interpolate`] method. + /// + /// [`interpolate`]: Animation::interpolate + pub fn interpolate_with(&self, f: impl Fn(T) -> I, at: Instant) -> I + where + I: Interpolable, + { + self.raw.animate(f, at) + } + + /// Retuns the current state of the [`Animation`]. + pub fn value(&self) -> T { + self.raw.value + } +} + +impl Animation { + /// Projects the [`Animation`] into an interpolated value at the given [`Instant`]; using the + /// `start` and `end` values as the origin and destination keyframes. + pub fn interpolate(&self, start: I, end: I, at: Instant) -> I + where + I: Interpolable + Clone, + { + self.raw.animate_bool(start, end, at) + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs index 645f7a902e..3a61a5ee34 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -10,6 +10,8 @@ html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] pub mod alignment; +#[cfg(not(target_arch = "wasm32"))] +pub mod animation; pub mod border; pub mod clipboard; pub mod event; @@ -49,6 +51,8 @@ mod vector; pub use alignment::Alignment; pub use angle::{Degrees, Radians}; +#[cfg(not(target_arch = "wasm32"))] +pub use animation::Animation; pub use background::Background; pub use border::Border; pub use clipboard::Clipboard; diff --git a/examples/changelog/Cargo.toml b/examples/changelog/Cargo.toml index eeb7b52664..6e314947fa 100644 --- a/examples/changelog/Cargo.toml +++ b/examples/changelog/Cargo.toml @@ -23,5 +23,4 @@ tracing-subscriber = "0.3" [dependencies.reqwest] version = "0.12" -default-features = false -features = ["json", "rustls-tls"] +features = ["json"] diff --git a/examples/download_progress/Cargo.toml b/examples/download_progress/Cargo.toml index 61a1b2572c..8632c8b88b 100644 --- a/examples/download_progress/Cargo.toml +++ b/examples/download_progress/Cargo.toml @@ -11,5 +11,4 @@ iced.features = ["tokio"] [dependencies.reqwest] version = "0.12" -default-features = false -features = ["stream", "rustls-tls"] +features = ["stream"] diff --git a/examples/gallery/Cargo.toml b/examples/gallery/Cargo.toml new file mode 100644 index 0000000000..573389b138 --- /dev/null +++ b/examples/gallery/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "gallery" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2021" +publish = false + +[dependencies] +iced.workspace = true +iced.features = ["tokio", "image", "web-colors", "debug"] + +reqwest.version = "0.12" +reqwest.features = ["json"] + +serde.version = "1.0" +serde.features = ["derive"] + +bytes.workspace = true +image.workspace = true +tokio.workspace = true + +[lints] +workspace = true diff --git a/examples/gallery/src/civitai.rs b/examples/gallery/src/civitai.rs new file mode 100644 index 0000000000..986b6bf2cf --- /dev/null +++ b/examples/gallery/src/civitai.rs @@ -0,0 +1,144 @@ +use bytes::Bytes; +use serde::Deserialize; +use tokio::task; + +use std::fmt; +use std::io; +use std::sync::Arc; + +#[derive(Debug, Clone, Deserialize)] +pub struct Image { + pub id: Id, + url: String, +} + +impl Image { + pub const LIMIT: usize = 99; + + pub async fn list() -> Result, Error> { + let client = reqwest::Client::new(); + + #[derive(Deserialize)] + struct Response { + items: Vec, + } + + let response: Response = client + .get("https://civitai.com/api/v1/images") + .query(&[ + ("sort", "Most Reactions"), + ("period", "Week"), + ("nsfw", "None"), + ("limit", &Image::LIMIT.to_string()), + ]) + .send() + .await? + .error_for_status()? + .json() + .await?; + + Ok(response.items) + } + + pub async fn download(self, size: Size) -> Result { + let client = reqwest::Client::new(); + + let bytes = client + .get(match size { + Size::Original => self.url, + Size::Thumbnail => self + .url + .split("/") + .map(|part| { + if part.starts_with("width=") { + "width=640" + } else { + part + } + }) + .collect::>() + .join("/"), + }) + .send() + .await? + .error_for_status()? + .bytes() + .await?; + + let image = task::spawn_blocking(move || { + Ok::<_, Error>( + image::ImageReader::new(io::Cursor::new(bytes)) + .with_guessed_format()? + .decode()? + .to_rgba8(), + ) + }) + .await??; + + Ok(Rgba { + width: image.width(), + height: image.height(), + pixels: Bytes::from(image.into_raw()), + }) + } +} + +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, +)] +pub struct Id(u32); + +#[derive(Clone)] +pub struct Rgba { + pub width: u32, + pub height: u32, + pub pixels: Bytes, +} + +impl fmt::Debug for Rgba { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Rgba") + .field("width", &self.width) + .field("height", &self.height) + .finish() + } +} + +#[derive(Debug, Clone, Copy)] +pub enum Size { + Original, + Thumbnail, +} + +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub enum Error { + RequestFailed(Arc), + IOFailed(Arc), + JoinFailed(Arc), + ImageDecodingFailed(Arc), +} + +impl From for Error { + fn from(error: reqwest::Error) -> Self { + Self::RequestFailed(Arc::new(error)) + } +} + +impl From for Error { + fn from(error: io::Error) -> Self { + Self::IOFailed(Arc::new(error)) + } +} + +impl From for Error { + fn from(error: task::JoinError) -> Self { + Self::JoinFailed(Arc::new(error)) + } +} + +impl From for Error { + fn from(error: image::ImageError) -> Self { + Self::ImageDecodingFailed(Arc::new(error)) + } +} diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs new file mode 100644 index 0000000000..9092cf9e6d --- /dev/null +++ b/examples/gallery/src/main.rs @@ -0,0 +1,326 @@ +//! A simple gallery that displays the daily featured images of Civitai. +//! +//! Showcases lazy loading of images in the background, as well as +//! some smooth animations. +mod civitai; + +use crate::civitai::{Error, Id, Image, Rgba, Size}; + +use iced::animation; +use iced::time::Instant; +use iced::widget::{ + button, center_x, container, horizontal_space, image, mouse_area, opaque, + pop, row, scrollable, stack, +}; +use iced::window; +use iced::{ + color, Animation, ContentFit, Element, Fill, Subscription, Task, Theme, +}; + +use std::collections::HashMap; + +fn main() -> iced::Result { + iced::application("Gallery - Iced", Gallery::update, Gallery::view) + .subscription(Gallery::subscription) + .theme(Gallery::theme) + .run_with(Gallery::new) +} + +struct Gallery { + images: Vec, + thumbnails: HashMap, + viewer: Viewer, + now: Instant, +} + +#[derive(Debug, Clone)] +enum Message { + ImagesListed(Result, Error>), + ImagePoppedIn(Id), + ImageDownloaded(Result), + ThumbnailDownloaded(Id, Result), + ThumbnailHovered(Id, bool), + Open(Id), + Close, + Animate(Instant), +} + +impl Gallery { + pub fn new() -> (Self, Task) { + ( + Self { + images: Vec::new(), + thumbnails: HashMap::new(), + viewer: Viewer::new(), + now: Instant::now(), + }, + Task::perform(Image::list(), Message::ImagesListed), + ) + } + + pub fn theme(&self) -> Theme { + Theme::TokyoNight + } + + pub fn subscription(&self) -> Subscription { + let is_animating = self + .thumbnails + .values() + .any(|thumbnail| thumbnail.is_animating(self.now)) + || self.viewer.is_animating(self.now); + + if is_animating { + window::frames().map(Message::Animate) + } else { + Subscription::none() + } + } + + pub fn update(&mut self, message: Message) -> Task { + match message { + Message::ImagesListed(Ok(images)) => { + self.images = images; + + Task::none() + } + Message::ImagePoppedIn(id) => { + let Some(image) = self + .images + .iter() + .find(|candidate| candidate.id == id) + .cloned() + else { + return Task::none(); + }; + + Task::perform(image.download(Size::Thumbnail), move |result| { + Message::ThumbnailDownloaded(id, result) + }) + } + Message::ImageDownloaded(Ok(rgba)) => { + self.viewer.show(rgba); + + Task::none() + } + Message::ThumbnailDownloaded(id, Ok(rgba)) => { + let thumbnail = Thumbnail::new(rgba); + let _ = self.thumbnails.insert(id, thumbnail); + + Task::none() + } + Message::ThumbnailHovered(id, is_hovered) => { + if let Some(thumbnail) = self.thumbnails.get_mut(&id) { + thumbnail.zoom.go_mut(is_hovered); + } + + Task::none() + } + Message::Open(id) => { + let Some(image) = self + .images + .iter() + .find(|candidate| candidate.id == id) + .cloned() + else { + return Task::none(); + }; + + self.viewer.open(); + + Task::perform( + image.download(Size::Original), + Message::ImageDownloaded, + ) + } + Message::Close => { + self.viewer.close(); + + Task::none() + } + Message::Animate(now) => { + self.now = now; + + Task::none() + } + Message::ImagesListed(Err(error)) + | Message::ImageDownloaded(Err(error)) + | Message::ThumbnailDownloaded(_, Err(error)) => { + dbg!(error); + + Task::none() + } + } + } + + pub fn view(&self) -> Element<'_, Message> { + let gallery = if self.images.is_empty() { + row((0..=Image::LIMIT).map(|_| placeholder())) + } else { + row(self.images.iter().map(|image| { + card(image, self.thumbnails.get(&image.id), self.now) + })) + } + .spacing(10) + .wrap(); + + let content = + container(scrollable(center_x(gallery)).spacing(10)).padding(10); + + let viewer = self.viewer.view(self.now); + + stack![content, viewer].into() + } +} + +fn card<'a>( + metadata: &'a Image, + thumbnail: Option<&'a Thumbnail>, + now: Instant, +) -> Element<'a, Message> { + let image: Element<'_, _> = if let Some(thumbnail) = thumbnail { + image(&thumbnail.handle) + .width(Fill) + .height(Fill) + .content_fit(ContentFit::Cover) + .opacity(thumbnail.fade_in.interpolate(0.0, 1.0, now)) + .scale(thumbnail.zoom.interpolate(1.0, 1.1, now)) + .into() + } else { + horizontal_space().into() + }; + + let card = mouse_area( + container(image) + .width(Thumbnail::WIDTH) + .height(Thumbnail::HEIGHT) + .style(container::dark), + ) + .on_enter(Message::ThumbnailHovered(metadata.id, true)) + .on_exit(Message::ThumbnailHovered(metadata.id, false)); + + if thumbnail.is_some() { + button(card) + .on_press(Message::Open(metadata.id)) + .padding(0) + .style(button::text) + .into() + } else { + pop(card) + .on_show(Message::ImagePoppedIn(metadata.id)) + .into() + } +} + +fn placeholder<'a>() -> Element<'a, Message> { + container(horizontal_space()) + .width(Thumbnail::WIDTH) + .height(Thumbnail::HEIGHT) + .style(container::dark) + .into() +} + +struct Thumbnail { + handle: image::Handle, + fade_in: Animation, + zoom: Animation, +} + +impl Thumbnail { + const WIDTH: u16 = 320; + const HEIGHT: u16 = 410; + + fn new(rgba: Rgba) -> Self { + Self { + handle: image::Handle::from_rgba( + rgba.width, + rgba.height, + rgba.pixels, + ), + fade_in: Animation::new(false).slow().go(true), + zoom: Animation::new(false) + .quick() + .easing(animation::Easing::EaseInOut), + } + } + + fn is_animating(&self, now: Instant) -> bool { + self.fade_in.is_animating(now) || self.zoom.is_animating(now) + } +} + +struct Viewer { + image: Option, + background_fade_in: Animation, + image_fade_in: Animation, +} + +impl Viewer { + fn new() -> Self { + Self { + image: None, + background_fade_in: Animation::new(false) + .very_slow() + .easing(animation::Easing::EaseInOut), + image_fade_in: Animation::new(false) + .very_slow() + .easing(animation::Easing::EaseInOut), + } + } + + fn open(&mut self) { + self.image = None; + self.background_fade_in.go_mut(true); + } + + fn show(&mut self, rgba: Rgba) { + self.image = Some(image::Handle::from_rgba( + rgba.width, + rgba.height, + rgba.pixels, + )); + self.background_fade_in.go_mut(true); + self.image_fade_in.go_mut(true); + } + + fn close(&mut self) { + self.background_fade_in.go_mut(false); + self.image_fade_in.go_mut(false); + } + + fn is_animating(&self, now: Instant) -> bool { + self.background_fade_in.is_animating(now) + || self.image_fade_in.is_animating(now) + } + + fn view(&self, now: Instant) -> Element<'_, Message> { + let opacity = self.background_fade_in.interpolate(0.0, 0.8, now); + + let image: Element<'_, _> = if let Some(handle) = &self.image { + 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 { + horizontal_space().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), + ) + } else { + horizontal_space().into() + } + } +} diff --git a/examples/pokedex/Cargo.toml b/examples/pokedex/Cargo.toml index 1a6d54453e..b3be4e140d 100644 --- a/examples/pokedex/Cargo.toml +++ b/examples/pokedex/Cargo.toml @@ -17,8 +17,7 @@ features = ["derive"] [dependencies.reqwest] version = "0.12" -default-features = false -features = ["json", "rustls-tls"] +features = ["json"] [dependencies.rand] version = "0.8" diff --git a/src/lib.rs b/src/lib.rs index 939943a9db..34e1e37deb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -511,6 +511,12 @@ pub use crate::core::{ pub use crate::runtime::exit; pub use iced_futures::Subscription; +#[cfg(not(target_arch = "wasm32"))] +pub use crate::core::animation; + +#[cfg(not(target_arch = "wasm32"))] +pub use crate::core::Animation; + pub use alignment::Horizontal::{Left, Right}; pub use alignment::Vertical::{Bottom, Top}; pub use Alignment::Center;