Merge pull request #2757 from iced-rs/feature/animation-api

`Animation` API for application code
This commit is contained in:
Héctor 2025-01-28 03:47:13 +01:00 committed by GitHub
commit 00a048677f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 883 additions and 121 deletions

352
Cargo.lock generated
View file

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

View file

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

View file

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

136
core/src/animation.rs Normal file
View file

@ -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<T>
where
T: Clone + Copy + PartialEq + Float,
{
raw: lilt::Animated<T, Instant>,
}
impl<T> Animation<T>
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<I>(&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<bool> {
/// 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<I>(&self, start: I, end: I, at: Instant) -> I
where
I: Interpolable + Clone,
{
self.raw.animate_bool(start, end, at)
}
}

View file

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

View file

@ -23,5 +23,4 @@ tracing-subscriber = "0.3"
[dependencies.reqwest]
version = "0.12"
default-features = false
features = ["json", "rustls-tls"]
features = ["json"]

View file

@ -11,5 +11,4 @@ iced.features = ["tokio"]
[dependencies.reqwest]
version = "0.12"
default-features = false
features = ["stream", "rustls-tls"]
features = ["stream"]

View file

@ -0,0 +1,23 @@
[package]
name = "gallery"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
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

View file

@ -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<Vec<Self>, Error> {
let client = reqwest::Client::new();
#[derive(Deserialize)]
struct Response {
items: Vec<Image>,
}
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<Rgba, Error> {
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::<Vec<_>>()
.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<reqwest::Error>),
IOFailed(Arc<io::Error>),
JoinFailed(Arc<task::JoinError>),
ImageDecodingFailed(Arc<image::ImageError>),
}
impl From<reqwest::Error> for Error {
fn from(error: reqwest::Error) -> Self {
Self::RequestFailed(Arc::new(error))
}
}
impl From<io::Error> for Error {
fn from(error: io::Error) -> Self {
Self::IOFailed(Arc::new(error))
}
}
impl From<task::JoinError> for Error {
fn from(error: task::JoinError) -> Self {
Self::JoinFailed(Arc::new(error))
}
}
impl From<image::ImageError> for Error {
fn from(error: image::ImageError) -> Self {
Self::ImageDecodingFailed(Arc::new(error))
}
}

View file

@ -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<Image>,
thumbnails: HashMap<Id, Thumbnail>,
viewer: Viewer,
now: Instant,
}
#[derive(Debug, Clone)]
enum Message {
ImagesListed(Result<Vec<Image>, Error>),
ImagePoppedIn(Id),
ImageDownloaded(Result<Rgba, Error>),
ThumbnailDownloaded(Id, Result<Rgba, Error>),
ThumbnailHovered(Id, bool),
Open(Id),
Close,
Animate(Instant),
}
impl Gallery {
pub fn new() -> (Self, Task<Message>) {
(
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<Message> {
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<Message> {
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<bool>,
zoom: Animation<bool>,
}
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<image::Handle>,
background_fade_in: Animation<bool>,
image_fade_in: Animation<bool>,
}
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()
}
}
}

View file

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

View file

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