Add Loading and Fallback States to Image Elements (via StyledImage) (#20371)

Mikayla Maki , nate , michael , Nate Butler , and Antonio Scandurra created

@iamnbutler edit:

This pull request enhances the image element by introducing the ability
to display loading and fallback states.

Changes:

- Implemented the loading and fallback states for image elements using
`.with_loading` and `.with_fallback` respectively.
- Introduced the `StyledImage` trait and `ImageStyle` to enable a fluent
API for changing image styles across image types (`Img`,
`Stateful<Img>`, etc).

Example Usage:

```rust
fn loading_element() -> impl IntoElement {
    div().size_full().flex_none().p_0p5().rounded_sm().child(
        div().size_full().with_animation(
            "loading-bg",
            Animation::new(Duration::from_secs(3))
                .repeat()
                .with_easing(pulsating_between(0.04, 0.24)),
            move |this, delta| this.bg(black().opacity(delta)),
        ),
    )
}

fn fallback_element() -> impl IntoElement {
    let fallback_color: Hsla = black().opacity(0.5);

    div().size_full().flex_none().p_0p5().child(
        div()
            .size_full()
            .flex()
            .items_center()
            .justify_center()
            .rounded_sm()
            .text_sm()
            .text_color(fallback_color)
            .border_1()
            .border_color(fallback_color)
            .child("?"),
    )
}

impl Render for ImageLoadingExample {
    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
        img("some/image/path")
            .id("image-1")
            .with_fallback(|| Self::fallback_element().into_any_element())
            .with_loading(|| Self::loading_element().into_any_element())
    }
}
```

Note:

An `Img` must have an `id` to be able to add a loading state.

Release Notes:

- N/A

---------

Co-authored-by: nate <nate@zed.dev>
Co-authored-by: michael <michael@zed.dev>
Co-authored-by: Nate Butler <iamnbutler@gmail.com>
Co-authored-by: Antonio Scandurra <me@as-cii.com>

Change summary

Cargo.lock                              | 337 +++++++++----------
crates/assistant/src/assistant_panel.rs |   3 
crates/gpui/examples/gif_viewer.rs      |   6 
crates/gpui/examples/image/image.rs     |   9 
crates/gpui/examples/image_loading.rs   | 214 ++++++++++++
crates/gpui/src/app.rs                  |  11 
crates/gpui/src/asset_cache.rs          |  52 ++
crates/gpui/src/elements/div.rs         |   2 
crates/gpui/src/elements/img.rs         | 452 +++++++++++++++++---------
crates/gpui/src/platform.rs             |  21 
crates/gpui/src/prelude.rs              |   2 
crates/gpui/src/svg_renderer.rs         |   4 
crates/gpui/src/window.rs               |  33 -
crates/ui/src/components/scrollbar.rs   |   4 
crates/workspace/src/workspace.rs       |   2 
15 files changed, 750 insertions(+), 402 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -10,7 +10,7 @@ dependencies = [
  "auto_update",
  "editor",
  "extension_host",
- "futures 0.3.30",
+ "futures 0.3.31",
  "gpui",
  "language",
  "lsp",
@@ -23,19 +23,13 @@ dependencies = [
 
 [[package]]
 name = "addr2line"
-version = "0.24.1"
+version = "0.24.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375"
+checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
 dependencies = [
- "gimli 0.31.0",
+ "gimli 0.31.1",
 ]
 
-[[package]]
-name = "adler"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
-
 [[package]]
 name = "adler2"
 version = "2.0.0"
@@ -100,8 +94,8 @@ dependencies = [
  "miow",
  "parking_lot",
  "piper",
- "polling 3.7.3",
- "regex-automata 0.4.7",
+ "polling 3.7.4",
+ "regex-automata 0.4.9",
  "rustix-openpty",
  "serde",
  "signal-hook",
@@ -124,9 +118,9 @@ checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1"
 
 [[package]]
 name = "allocator-api2"
-version = "0.2.18"
+version = "0.2.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
+checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9"
 
 [[package]]
 name = "alsa"
@@ -192,9 +186,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
 
 [[package]]
 name = "anstream"
-version = "0.6.15"
+version = "0.6.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
+checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
 dependencies = [
  "anstyle",
  "anstyle-parse",
@@ -207,36 +201,36 @@ dependencies = [
 
 [[package]]
 name = "anstyle"
-version = "1.0.8"
+version = "1.0.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
+checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
 
 [[package]]
 name = "anstyle-parse"
-version = "0.2.5"
+version = "0.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
+checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
 dependencies = [
  "utf8parse",
 ]
 
 [[package]]
 name = "anstyle-query"
-version = "1.1.1"
+version = "1.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
+checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
 dependencies = [
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
 name = "anstyle-wincon"
-version = "3.0.4"
+version = "3.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
+checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
 dependencies = [
  "anstyle",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -245,13 +239,13 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "chrono",
- "futures 0.3.30",
+ "futures 0.3.31",
  "http_client",
  "schemars",
  "serde",
  "serde_json",
  "strum 0.25.0",
- "thiserror",
+ "thiserror 1.0.69",
  "util",
 ]
 
@@ -278,9 +272,9 @@ dependencies = [
 
 [[package]]
 name = "arbitrary"
-version = "1.3.2"
+version = "1.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
+checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
 
 [[package]]
 name = "arg_enum_proc_macro"
@@ -301,9 +295,9 @@ checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
 
 [[package]]
 name = "arrayref"
-version = "0.3.8"
+version = "0.3.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a"
+checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
 
 [[package]]
 name = "arrayvec"
@@ -396,7 +390,7 @@ dependencies = [
  "env_logger 0.11.5",
  "feature_flags",
  "fs",
- "futures 0.3.30",
+ "futures 0.3.31",
  "fuzzy",
  "globset",
  "gpui",
@@ -463,7 +457,7 @@ dependencies = [
  "collections",
  "derive_more",
  "extension",
- "futures 0.3.30",
+ "futures 0.3.31",
  "gpui",
  "language",
  "language_model",
@@ -573,14 +567,14 @@ dependencies = [
 
 [[package]]
 name = "async-executor"
-version = "1.13.0"
+version = "1.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7ebdfa2ebdab6b1760375fa7d6f382b9f486eac35fc994625a00e89280bdbb7"
+checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec"
 dependencies = [
  "async-task",
  "concurrent-queue",
- "fastrand 2.1.1",
- "futures-lite 2.3.0",
+ "fastrand 2.2.0",
+ "futures-lite 2.5.0",
  "slab",
 ]
 
@@ -604,7 +598,7 @@ checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a"
 dependencies = [
  "async-lock 3.4.0",
  "blocking",
- "futures-lite 2.3.0",
+ "futures-lite 2.5.0",
 ]
 
 [[package]]
@@ -615,10 +609,10 @@ checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c"
 dependencies = [
  "async-channel 2.3.1",
  "async-executor",
- "async-io 2.3.4",
+ "async-io 2.4.0",
  "async-lock 3.4.0",
  "blocking",
- "futures-lite 2.3.0",
+ "futures-lite 2.5.0",
  "once_cell",
 ]
 
@@ -644,18 +638,18 @@ dependencies = [
 
 [[package]]
 name = "async-io"
-version = "2.3.4"
+version = "2.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8"
+checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059"
 dependencies = [
  "async-lock 3.4.0",
  "cfg-if",
  "concurrent-queue",
  "futures-io",
- "futures-lite 2.3.0",
+ "futures-lite 2.5.0",
  "parking",
- "polling 3.7.3",
- "rustix 0.38.35",
+ "polling 3.7.4",
+ "rustix 0.38.40",
  "slab",
  "tracing",
  "windows-sys 0.59.0",
@@ -689,7 +683,7 @@ checksum = "9343dc5acf07e79ff82d0c37899f079db3534d99f189a1837c8e549c99405bec"
 dependencies = [
  "futures-util",
  "native-tls",
- "thiserror",
+ "thiserror 1.0.69",
  "url",
 ]
 
@@ -710,9 +704,9 @@ version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7"
 dependencies = [
- "async-io 2.3.4",
+ "async-io 2.4.0",
  "blocking",
- "futures-lite 2.3.0",
+ "futures-lite 2.5.0",
 ]
 
 [[package]]
@@ -720,7 +714,7 @@ name = "async-pipe"
 version = "0.1.3"
 source = "git+https://github.com/zed-industries/async-pipe-rs?rev=82d00a04211cf4e1236029aa03e6b6ce2a74c553#82d00a04211cf4e1236029aa03e6b6ce2a74c553"
 dependencies = [
- "futures 0.3.30",
+ "futures 0.3.31",
  "log",
 ]
 
@@ -737,28 +731,27 @@ dependencies = [
  "cfg-if",
  "event-listener 3.1.0",
  "futures-lite 1.13.0",
- "rustix 0.38.35",
+ "rustix 0.38.40",
  "windows-sys 0.48.0",
 ]
 
 [[package]]
 name = "async-process"
-version = "2.2.4"
+version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8a07789659a4d385b79b18b9127fc27e1a59e1e89117c78c5ea3b806f016374"
+checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb"
 dependencies = [
  "async-channel 2.3.1",
- "async-io 2.3.4",
+ "async-io 2.4.0",
  "async-lock 3.4.0",
  "async-signal",
  "async-task",
  "blocking",
  "cfg-if",
  "event-listener 5.3.1",
- "futures-lite 2.3.0",
- "rustix 0.38.35",
+ "futures-lite 2.5.0",
+ "rustix 0.38.40",
  "tracing",
- "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -789,13 +782,13 @@ version = "0.2.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3"
 dependencies = [
- "async-io 2.3.4",
+ "async-io 2.4.0",
  "async-lock 3.4.0",
  "atomic-waker",
  "cfg-if",
  "futures-core",
  "futures-io",
- "rustix 0.38.35",
+ "rustix 0.38.40",
  "signal-hook-registry",
  "slab",
  "windows-sys 0.59.0",
@@ -803,21 +796,21 @@ dependencies = [
 
 [[package]]
 name = "async-std"
-version = "1.12.0"
+version = "1.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d"
+checksum = "c634475f29802fde2b8f0b505b1bd00dfe4df7d4a000f0b36f7671197d5c3615"
 dependencies = [
  "async-attributes",
  "async-channel 1.9.0",
  "async-global-executor",
- "async-io 1.13.0",
- "async-lock 2.8.0",
- "async-process 1.8.1",
+ "async-io 2.4.0",
+ "async-lock 3.4.0",
+ "async-process 2.3.0",
  "crossbeam-utils",
  "futures-channel",
  "futures-core",
  "futures-io",
- "futures-lite 1.13.0",
+ "futures-lite 2.5.0",
  "gloo-timers",
  "kv-log-macro",
  "log",
@@ -831,9 +824,9 @@ dependencies = [
 
 [[package]]
 name = "async-stream"
-version = "0.3.5"
+version = "0.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
+checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
 dependencies = [
  "async-stream-impl",
  "futures-core",
@@ -842,9 +835,9 @@ dependencies = [
 
 [[package]]
 name = "async-stream-impl"
-version = "0.3.5"
+version = "0.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
+checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -867,7 +860,7 @@ dependencies = [
  "serde_qs 0.10.1",
  "smart-default",
  "smol_str",
- "thiserror",
+ "thiserror 1.0.69",
  "tokio",
 ]
 
@@ -963,9 +956,9 @@ checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52"
 dependencies = [
  "async-compression",
  "crc32fast",
- "futures-lite 2.3.0",
+ "futures-lite 2.5.0",
  "pin-project",
- "thiserror",
+ "thiserror 1.0.69",
 ]
 
 [[package]]
@@ -974,7 +967,7 @@ version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a860072022177f903e59730004fb5dc13db9275b79bb2aef7ba8ce831956c233"
 dependencies = [
- "bytes 1.7.2",
+ "bytes 1.8.0",
  "futures-sink",
  "futures-util",
  "memchr",
@@ -1044,9 +1037,9 @@ dependencies = [
 
 [[package]]
 name = "autocfg"
-version = "1.3.0"
+version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
 
 [[package]]
 name = "av1-grain"
@@ -1064,18 +1057,18 @@ dependencies = [
 
 [[package]]
 name = "avif-serialize"
-version = "0.8.1"
+version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "876c75a42f6364451a033496a14c44bffe41f5f4a8236f697391f11024e596d2"
+checksum = "e335041290c43101ca215eed6f43ec437eb5a42125573f600fc3fa42b9bddd62"
 dependencies = [
  "arrayvec",
 ]
 
 [[package]]
 name = "aws-config"
-version = "1.5.5"
+version = "1.5.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e95816a168520d72c0e7680c405a5a8c1fb6a035b4bc4b9d7b0de8e1a941697"
+checksum = "9b49afaa341e8dd8577e1a2200468f98956d6eda50bcf4a53246cc00174ba924"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -1089,8 +1082,8 @@ dependencies = [
  "aws-smithy-runtime-api",
  "aws-smithy-types",
  "aws-types",
- "bytes 1.7.2",
- "fastrand 2.1.1",
+ "bytes 1.8.0",
+ "fastrand 2.2.0",
  "hex",
  "http 0.2.12",
  "ring",
@@ -1128,8 +1121,8 @@ dependencies = [
  "aws-smithy-runtime-api",
  "aws-smithy-types",
  "aws-types",
- "bytes 1.7.2",
- "fastrand 2.1.1",
+ "bytes 1.8.0",
+ "fastrand 2.2.0",
  "http 0.2.12",
  "http-body 0.4.6",
  "once_cell",
@@ -1154,7 +1147,7 @@ dependencies = [
  "aws-smithy-runtime-api",
  "aws-smithy-types",
  "aws-types",
- "bytes 1.7.2",
+ "bytes 1.8.0",
  "http 0.2.12",
  "once_cell",
  "regex-lite",
@@ -1163,11 +1156,10 @@ dependencies = [
 
 [[package]]
 name = "aws-sdk-s3"
-version = "1.47.0"
+version = "1.61.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cca49303c05d2a740b8a4552fac63a4db6ead84f7e7eeed04761fd3014c26f25"
+checksum = "0e531658a0397d22365dfe26c3e1c0c8448bf6a3a2d8a098ded802f2b1261615"
 dependencies = [
- "ahash 0.8.11",
  "aws-credential-types",
  "aws-runtime",
  "aws-sigv4",
@@ -1181,8 +1173,8 @@ dependencies = [
  "aws-smithy-types",
  "aws-smithy-xml",
  "aws-types",
- "bytes 1.7.2",
- "fastrand 2.1.1",
+ "bytes 1.8.0",
+ "fastrand 2.2.0",
  "hex",
  "hmac",
  "http 0.2.12",
@@ -1198,9 +1190,9 @@ dependencies = [
 
 [[package]]
 name = "aws-sdk-sso"
-version = "1.40.0"
+version = "1.49.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5879bec6e74b648ce12f6085e7245417bc5f6d672781028384d2e494be3eb6d"
+checksum = "09677244a9da92172c8dc60109b4a9658597d4d298b188dd0018b6a66b410ca4"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -1211,7 +1203,7 @@ dependencies = [
  "aws-smithy-runtime-api",
  "aws-smithy-types",
  "aws-types",
- "bytes 1.7.2",
+ "bytes 1.8.0",
  "http 0.2.12",
  "once_cell",
  "regex-lite",
@@ -1220,9 +1212,9 @@ dependencies = [
 
 [[package]]
 name = "aws-sdk-ssooidc"
-version = "1.41.0"
+version = "1.50.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ef4cd9362f638c22a3b959fd8df292e7e47fdf170270f86246b97109b5f2f7d"
+checksum = "81fea2f3a8bb3bd10932ae7ad59cc59f65f270fc9183a7e91f501dc5efbef7ee"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -1233,7 +1225,7 @@ dependencies = [
  "aws-smithy-runtime-api",
  "aws-smithy-types",
  "aws-types",
- "bytes 1.7.2",
+ "bytes 1.8.0",
  "http 0.2.12",
  "once_cell",
  "regex-lite",
@@ -1242,9 +1234,9 @@ dependencies = [
 
 [[package]]
 name = "aws-sdk-sts"
-version = "1.40.0"
+version = "1.50.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b1e2735d2ab28b35ecbb5496c9d41857f52a0d6a0075bbf6a8af306045ea6f6"
+checksum = "6ada54e5f26ac246dc79727def52f7f8ed38915cb47781e2a72213957dc3a7d5"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -1274,7 +1266,7 @@ dependencies = [
  "aws-smithy-http",
  "aws-smithy-runtime-api",
  "aws-smithy-types",
- "bytes 1.7.2",
+ "bytes 1.8.0",
  "crypto-bigint 0.5.5",
  "form_urlencoded",
  "hex",
@@ -1305,13 +1297,13 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-checksums"
-version = "0.60.12"
+version = "0.60.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "598b1689d001c4d4dc3cb386adb07d37786783aee3ac4b324bcadac116bf3d23"
+checksum = "ba1a71073fca26775c8b5189175ea8863afb1c9ea2cceb02a5de5ad9dfbaa795"
 dependencies = [
  "aws-smithy-http",
  "aws-smithy-types",
- "bytes 1.7.2",
+ "bytes 1.8.0",
  "crc32c",
  "crc32fast",
  "hex",
@@ -1331,7 +1323,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cef7d0a272725f87e51ba2bf89f8c21e4df61b9e49ae1ac367a6d69916ef7c90"
 dependencies = [
  "aws-smithy-types",
- "bytes 1.7.2",
+ "bytes 1.8.0",
  "crc32fast",
 ]
 
@@ -1344,7 +1336,7 @@ dependencies = [
  "aws-smithy-eventstream",
  "aws-smithy-runtime-api",
  "aws-smithy-types",
- "bytes 1.7.2",
+ "bytes 1.8.0",
  "bytes-utils",
  "futures-core",
  "http 0.2.12",
@@ -1385,8 +1377,8 @@ dependencies = [
  "aws-smithy-http",
  "aws-smithy-runtime-api",
  "aws-smithy-types",
- "bytes 1.7.2",
- "fastrand 2.1.1",
+ "bytes 1.8.0",
+ "fastrand 2.2.0",
  "h2 0.3.26",
  "http 0.2.12",
  "http-body 0.4.6",
@@ -1410,7 +1402,7 @@ checksum = "92165296a47a812b267b4f41032ff8069ab7ff783696d217f0994a0d7ab585cd"
 dependencies = [
  "aws-smithy-async",
  "aws-smithy-types",
- "bytes 1.7.2",
+ "bytes 1.8.0",
  "http 0.2.12",
  "http 1.1.0",
  "pin-project-lite",
@@ -1426,7 +1418,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4fbd94a32b3a7d55d3806fe27d98d3ad393050439dd05eb53ece36ec5e3d3510"
 dependencies = [
  "base64-simd",
- "bytes 1.7.2",
+ "bytes 1.8.0",
  "bytes-utils",
  "futures-core",
  "http 0.2.12",
@@ -1447,9 +1439,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-xml"
-version = "0.60.8"
+version = "0.60.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d123fbc2a4adc3c301652ba8e149bf4bc1d1725affb9784eb20c953ace06bf55"
+checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc"
 dependencies = [
  "xmlparser",
 ]
@@ -1478,7 +1470,7 @@ dependencies = [
  "axum-core",
  "base64 0.21.7",
  "bitflags 1.3.2",
- "bytes 1.7.2",
+ "bytes 1.8.0",
  "futures-util",
  "headers",
  "http 0.2.12",
@@ -1511,7 +1503,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c"
 dependencies = [
  "async-trait",
- "bytes 1.7.2",
+ "bytes 1.8.0",
  "futures-util",
  "http 0.2.12",
  "http-body 0.4.6",
@@ -1528,7 +1520,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f9a320103719de37b7b4da4c8eb629d4573f6bcfd3dfe80d3208806895ccf81d"
 dependencies = [
  "axum",
- "bytes 1.7.2",
+ "bytes 1.8.0",
  "futures-util",
  "http 0.2.12",
  "mime",
@@ -1551,7 +1543,7 @@ dependencies = [
  "addr2line",
  "cfg-if",
  "libc",
- "miniz_oxide 0.8.0",
+ "miniz_oxide",
  "object",
  "rustc-demangle",
  "windows-targets 0.52.6",
@@ -1599,9 +1591,9 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
 
 [[package]]
 name = "bigdecimal"
-version = "0.4.5"
+version = "0.4.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "51d712318a27c7150326677b321a5fa91b55f6d9034ffd67f20319e147d40cee"
+checksum = "8f850665a0385e070b64c38d2354e6c104c8479c59868d1e48a0c13ee2c7a1c1"
 dependencies = [
  "autocfg",
  "libm",
@@ -1620,26 +1612,6 @@ dependencies = [
  "serde",
 ]
 
-[[package]]
-name = "bindgen"
-version = "0.69.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0"
-dependencies = [
- "bitflags 2.6.0",
- "cexpr",
- "clang-sys",
- "itertools 0.12.1",
- "lazy_static",
- "lazycell",
- "proc-macro2",
- "quote",
- "regex",
- "rustc-hash 1.1.0",
- "shlex",
- "syn 2.0.87",
-]
-
 [[package]]
 name = "bindgen"
 version = "0.70.1"
@@ -1728,9 +1700,9 @@ dependencies = [
 
 [[package]]
 name = "bitstream-io"
-version = "2.5.3"
+version = "2.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b81e1519b0d82120d2fd469d5bfb2919a9361c48b02d82d04befc1cdd2002452"
+checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2"
 
 [[package]]
 name = "bitvec"
@@ -1841,15 +1813,15 @@ dependencies = [
  "async-channel 2.3.1",
  "async-task",
  "futures-io",
- "futures-lite 2.3.0",
+ "futures-lite 2.5.0",
  "piper",
 ]
 
 [[package]]
 name = "borsh"
-version = "1.5.1"
+version = "1.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed"
+checksum = "2506947f73ad44e344215ccd6403ac2ae18cd8e046e581a441bf8d199f257f03"
 dependencies = [
  "borsh-derive",
  "cfg_aliases 0.2.1",
@@ -1857,16 +1829,15 @@ dependencies = [
 
 [[package]]
 name = "borsh-derive"
-version = "1.5.1"
+version = "1.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b"
+checksum = "c2593a3b8b938bd68373196c9832f516be11fa487ef4ae745eb282e6a56a7244"
 dependencies = [
  "once_cell",
  "proc-macro-crate",
  "proc-macro2",
  "quote",
  "syn 2.0.87",
- "syn_derive",
 ]
 
 [[package]]
@@ -1884,20 +1855,20 @@ dependencies = [
 
 [[package]]
 name = "bstr"
-version = "1.10.0"
+version = "1.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c"
+checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22"
 dependencies = [
  "memchr",
- "regex-automata 0.4.7",
+ "regex-automata 0.4.9",
  "serde",
 ]
 
 [[package]]
 name = "built"
-version = "0.7.4"
+version = "0.7.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "236e6289eda5a812bc6b53c3b024039382a2895fbbeef2d748b2931546d392c4"
+checksum = "c360505aed52b7ec96a3636c3f039d99103c37d1d9b4f7a8c743d3ea9ffcd03b"
 
 [[package]]
 name = "bumpalo"
@@ -1935,18 +1906,18 @@ dependencies = [
 
 [[package]]
 name = "bytemuck"
-version = "1.17.1"
+version = "1.19.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "773d90827bc3feecfb67fab12e24de0749aad83c74b9504ecde46237b5cd24e2"
+checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d"
 dependencies = [
  "bytemuck_derive",
 ]
 
 [[package]]
 name = "bytemuck_derive"
-version = "1.7.1"
+version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0cc8b54b395f2fcfbb3d90c47b01c7f444d94d05bdeb775811dec868ac3bbc26"
+checksum = "bcfcc3cd946cb52f0bbfdbbcfa2f4e24f75ebb6c0e1002f7c25904fada18b9ec"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1977,9 +1948,9 @@ dependencies = [
 
 [[package]]
 name = "bytes"
-version = "1.7.2"
+version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
+checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
 
 [[package]]
 name = "bytes-utils"
@@ -1987,7 +1958,7 @@ version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35"
 dependencies = [
- "bytes 1.7.2",
+ "bytes 1.8.0",
  "either",
 ]
 
@@ -2022,7 +1993,7 @@ dependencies = [
  "collections",
  "feature_flags",
  "fs",
- "futures 0.3.30",
+ "futures 0.3.31",
  "gpui",
  "http_client",
  "language",
@@ -2045,10 +2016,10 @@ checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
 dependencies = [
  "bitflags 2.6.0",
  "log",
- "polling 3.7.3",
- "rustix 0.38.35",
+ "polling 3.7.4",
+ "rustix 0.38.40",
  "slab",
- "thiserror",
+ "thiserror 1.0.69",
 ]
 
 [[package]]
@@ -2058,7 +2029,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20"
 dependencies = [
  "calloop",
- "rustix 0.38.35",
+ "rustix 0.38.40",
  "wayland-backend",
  "wayland-client",
 ]
@@ -2074,9 +2045,9 @@ dependencies = [
 
 [[package]]
 name = "cap-fs-ext"
-version = "3.2.0"
+version = "3.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eb23061fc1c4ead4e45ca713080fe768e6234e959f5a5c399c39eb41aa34e56e"
+checksum = "e16619ada836f12897a72011fe99b03f0025b87a8dbbea4f3c9f89b458a23bf3"
 dependencies = [
  "cap-primitives",
  "cap-std",
@@ -2086,21 +2057,21 @@ dependencies = [
 
 [[package]]
 name = "cap-net-ext"
-version = "3.2.0"
+version = "3.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f83ae11f116bcbafc5327c6af250341db96b5930046732e1905f7dc65887e0e1"
+checksum = "710b0eb776410a22c89a98f2f80b2187c2ac3a8206b99f3412332e63c9b09de0"
 dependencies = [
  "cap-primitives",
  "cap-std",
- "rustix 0.38.35",
+ "rustix 0.38.40",
  "smallvec",
 ]
 
 [[package]]
 name = "cap-primitives"
-version = "3.2.0"
+version = "3.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d00bd8d26c4270d950eaaa837387964a2089a1c3c349a690a1fa03221d29531"
+checksum = "82fa6c3f9773feab88d844aa50035a33fb6e7e7426105d2f4bb7aadc42a5f89a"
 dependencies = [
  "ambient-authority",
  "fs-set-times",
@@ -2108,16 +2079,16 @@ dependencies = [
  "io-lifetimes 2.0.3",
  "ipnet",
  "maybe-owned",
- "rustix 0.38.35",
+ "rustix 0.38.40",
  "windows-sys 0.52.0",
  "winx",
 ]
 
 [[package]]
 name = "cap-rand"
-version = "3.2.0"
+version = "3.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dbcb16a619d8b8211ed61f42bd290d2a1ac71277a69cf8417ec0996fa92f5211"
+checksum = "53774d49369892b70184f8312e50c1b87edccb376691de4485b0ff554b27c36c"
 dependencies = [
  "ambient-authority",
  "rand 0.8.5",
@@ -2125,27 +2096,27 @@ dependencies = [
 
 [[package]]
 name = "cap-std"
-version = "3.2.0"
+version = "3.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19eb8e3d71996828751c1ed3908a439639752ac6bdc874e41469ef7fc15fbd7f"
+checksum = "7f71b70818556b4fe2a10c7c30baac3f5f45e973f49fc2673d7c75c39d0baf5b"
 dependencies = [
  "cap-primitives",
  "io-extras",
  "io-lifetimes 2.0.3",
- "rustix 0.38.35",
+ "rustix 0.38.40",
 ]
 
 [[package]]
 name = "cap-time-ext"
-version = "3.2.0"
+version = "3.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61142dc51e25b7acc970ca578ce2c3695eac22bbba46c1073f5f583e78957725"
+checksum = "69dd48afa2363f746c93f961c211f6f099fb594a3446b8097bc5f79db51b6816"
 dependencies = [
  "ambient-authority",
  "cap-primitives",
  "iana-time-zone",
  "once_cell",
- "rustix 0.38.35",
+ "rustix 0.38.40",
  "winx",
 ]
 
@@ -2169,7 +2140,7 @@ dependencies = [
  "semver",
  "serde",
  "serde_json",
- "thiserror",
+ "thiserror 1.0.69",
 ]
 
 [[package]]
@@ -2204,7 +2175,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3fce8dd7fcfcbf3a0a87d8f515194b49d6135acab73e18bd380d1d93bb1a15eb"
 dependencies = [
  "heck 0.4.1",
- "indexmap 2.4.0",
+ "indexmap 2.6.0",
  "log",
  "proc-macro2",
  "quote",
@@ -2217,9 +2188,9 @@ dependencies = [
 
 [[package]]
 name = "cc"
-version = "1.1.15"
+version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6"
+checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47"
 dependencies = [
  "jobserver",
  "libc",
@@ -2277,7 +2248,7 @@ dependencies = [
  "client",
  "clock",
  "collections",
- "futures 0.3.30",
+ "futures 0.3.31",
  "gpui",
  "http_client",
  "language",
@@ -2387,9 +2358,9 @@ dependencies = [
 
 [[package]]
 name = "clap_complete"
-version = "4.5.24"
+version = "4.5.38"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d7db6eca8c205649e8d3ccd05aa5042b1800a784e56bc7c43524fde8abbfa9b"
+checksum = "d9647a559c112175f17cf724dc72d3645680a883c58481332779192b0d8e7a01"
 dependencies = [
  "clap",
 ]

crates/assistant/src/assistant_panel.rs 🔗

@@ -3333,7 +3333,8 @@ impl ContextEditor {
 
             self.context.update(cx, |context, cx| {
                 for image in images {
-                    let Some(render_image) = image.to_image_data(cx).log_err() else {
+                    let Some(render_image) = image.to_image_data(cx.svg_renderer()).log_err()
+                    else {
                         continue;
                     };
                     let image_id = image.id();

crates/gpui/examples/gif_viewer.rs 🔗

@@ -1,6 +1,4 @@
-use gpui::{
-    div, img, prelude::*, App, AppContext, ImageSource, Render, ViewContext, WindowOptions,
-};
+use gpui::{div, img, prelude::*, App, AppContext, Render, ViewContext, WindowOptions};
 use std::path::PathBuf;
 
 struct GifViewer {
@@ -16,7 +14,7 @@ impl GifViewer {
 impl Render for GifViewer {
     fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
         div().size_full().child(
-            img(ImageSource::File(self.gif_path.clone().into()))
+            img(self.gif_path.clone())
                 .size_full()
                 .object_fit(gpui::ObjectFit::Contain)
                 .id("gif"),

crates/gpui/examples/image/image.rs 🔗

@@ -61,7 +61,7 @@ impl RenderOnce for ImageContainer {
 }
 
 struct ImageShowcase {
-    local_resource: Arc<PathBuf>,
+    local_resource: Arc<std::path::Path>,
     remote_resource: SharedUri,
     asset_resource: SharedString,
 }
@@ -153,9 +153,10 @@ fn main() {
             cx.open_window(window_options, |cx| {
                 cx.new_view(|_cx| ImageShowcase {
                     // Relative path to your root project path
-                    local_resource: Arc::new(
-                        PathBuf::from_str("crates/gpui/examples/image/app-icon.png").unwrap(),
-                    ),
+                    local_resource: PathBuf::from_str("crates/gpui/examples/image/app-icon.png")
+                        .unwrap()
+                        .into(),
+
                     remote_resource: "https://picsum.photos/512/512".into(),
 
                     asset_resource: "image/color.svg".into(),

crates/gpui/examples/image_loading.rs 🔗

@@ -0,0 +1,214 @@
+use std::{path::Path, sync::Arc, time::Duration};
+
+use anyhow::anyhow;
+use gpui::{
+    black, div, img, prelude::*, pulsating_between, px, red, size, Animation, AnimationExt, App,
+    AppContext, Asset, AssetLogger, AssetSource, Bounds, Hsla, ImageAssetLoader, ImageCacheError,
+    ImgResourceLoader, Length, Pixels, RenderImage, Resource, SharedString, ViewContext,
+    WindowBounds, WindowContext, WindowOptions, LOADING_DELAY,
+};
+
+struct Assets {}
+
+impl AssetSource for Assets {
+    fn load(&self, path: &str) -> anyhow::Result<Option<std::borrow::Cow<'static, [u8]>>> {
+        std::fs::read(path)
+            .map(Into::into)
+            .map_err(Into::into)
+            .map(Some)
+    }
+
+    fn list(&self, path: &str) -> anyhow::Result<Vec<SharedString>> {
+        Ok(std::fs::read_dir(path)?
+            .filter_map(|entry| {
+                Some(SharedString::from(
+                    entry.ok()?.path().to_string_lossy().to_string(),
+                ))
+            })
+            .collect::<Vec<_>>())
+    }
+}
+
+const IMAGE: &str = "examples/image/app-icon.png";
+
+#[derive(Copy, Clone, Hash)]
+struct LoadImageParameters {
+    timeout: Duration,
+    fail: bool,
+}
+
+struct LoadImageWithParameters {}
+
+impl Asset for LoadImageWithParameters {
+    type Source = LoadImageParameters;
+
+    type Output = Result<Arc<RenderImage>, ImageCacheError>;
+
+    fn load(
+        parameters: Self::Source,
+        cx: &mut AppContext,
+    ) -> impl std::future::Future<Output = Self::Output> + Send + 'static {
+        let timer = cx.background_executor().timer(parameters.timeout);
+        let data = AssetLogger::<ImageAssetLoader>::load(
+            Resource::Path(Path::new(IMAGE).to_path_buf().into()),
+            cx,
+        );
+        async move {
+            timer.await;
+            if parameters.fail {
+                log::error!("Intentionally failed to load image");
+                Err(anyhow!("Failed to load image").into())
+            } else {
+                data.await
+            }
+        }
+    }
+}
+
+struct ImageLoadingExample {}
+
+impl ImageLoadingExample {
+    fn loading_element() -> impl IntoElement {
+        div().size_full().flex_none().p_0p5().rounded_sm().child(
+            div().size_full().with_animation(
+                "loading-bg",
+                Animation::new(Duration::from_secs(3))
+                    .repeat()
+                    .with_easing(pulsating_between(0.04, 0.24)),
+                move |this, delta| this.bg(black().opacity(delta)),
+            ),
+        )
+    }
+
+    fn fallback_element() -> impl IntoElement {
+        let fallback_color: Hsla = black().opacity(0.5);
+
+        div().size_full().flex_none().p_0p5().child(
+            div()
+                .size_full()
+                .flex()
+                .items_center()
+                .justify_center()
+                .rounded_sm()
+                .text_sm()
+                .text_color(fallback_color)
+                .border_1()
+                .border_color(fallback_color)
+                .child("?"),
+        )
+    }
+}
+
+impl Render for ImageLoadingExample {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
+        div().flex().flex_col().size_full().justify_around().child(
+            div().flex().flex_row().w_full().justify_around().child(
+                div()
+                    .flex()
+                    .bg(gpui::white())
+                    .size(Length::Definite(Pixels(300.0).into()))
+                    .justify_center()
+                    .items_center()
+                    .child({
+                        let image_source = LoadImageParameters {
+                            timeout: LOADING_DELAY.saturating_sub(Duration::from_millis(25)),
+                            fail: false,
+                        };
+
+                        // Load within the 'loading delay', should not show loading fallback
+                        img(move |cx: &mut WindowContext| {
+                            cx.use_asset::<LoadImageWithParameters>(&image_source)
+                        })
+                        .id("image-1")
+                        .border_1()
+                        .size_12()
+                        .with_fallback(|| Self::fallback_element().into_any_element())
+                        .border_color(red())
+                        .with_loading(|| Self::loading_element().into_any_element())
+                        .on_click(move |_, cx| {
+                            cx.remove_asset::<LoadImageWithParameters>(&image_source);
+                        })
+                    })
+                    .child({
+                        // Load after a long delay
+                        let image_source = LoadImageParameters {
+                            timeout: Duration::from_secs(5),
+                            fail: false,
+                        };
+
+                        img(move |cx: &mut WindowContext| {
+                            cx.use_asset::<LoadImageWithParameters>(&image_source)
+                        })
+                        .id("image-2")
+                        .with_fallback(|| Self::fallback_element().into_any_element())
+                        .with_loading(|| Self::loading_element().into_any_element())
+                        .size_12()
+                        .border_1()
+                        .border_color(red())
+                        .on_click(move |_, cx| {
+                            cx.remove_asset::<LoadImageWithParameters>(&image_source);
+                        })
+                    })
+                    .child({
+                        // Fail to load image after a long delay
+                        let image_source = LoadImageParameters {
+                            timeout: Duration::from_secs(5),
+                            fail: true,
+                        };
+
+                        // Fail to load after a long delay
+                        img(move |cx: &mut WindowContext| {
+                            cx.use_asset::<LoadImageWithParameters>(&image_source)
+                        })
+                        .id("image-3")
+                        .with_fallback(|| Self::fallback_element().into_any_element())
+                        .with_loading(|| Self::loading_element().into_any_element())
+                        .size_12()
+                        .border_1()
+                        .border_color(red())
+                        .on_click(move |_, cx| {
+                            cx.remove_asset::<LoadImageWithParameters>(&image_source);
+                        })
+                    })
+                    .child({
+                        // Ensure that the normal image loader doesn't spam logs
+                        let image_source = Path::new(
+                            "this/file/really/shouldn't/exist/or/won't/be/an/image/I/hope",
+                        )
+                        .to_path_buf();
+                        img(image_source.clone())
+                            .id("image-1")
+                            .border_1()
+                            .size_12()
+                            .with_fallback(|| Self::fallback_element().into_any_element())
+                            .border_color(red())
+                            .with_loading(|| Self::loading_element().into_any_element())
+                            .on_click(move |_, cx| {
+                                cx.remove_asset::<ImgResourceLoader>(&image_source.clone().into());
+                            })
+                    }),
+            ),
+        )
+    }
+}
+
+fn main() {
+    env_logger::init();
+    App::new()
+        .with_assets(Assets {})
+        .run(|cx: &mut AppContext| {
+            let options = WindowOptions {
+                window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
+                    None,
+                    size(px(300.), Pixels(300.)),
+                    cx,
+                ))),
+                ..Default::default()
+            };
+            cx.open_window(options, |cx| {
+                cx.activate(false);
+                cx.new_view(|_cx| ImageLoadingExample {})
+            })
+            .unwrap();
+        });
+}

crates/gpui/src/app.rs 🔗

@@ -747,7 +747,7 @@ impl AppContext {
     }
 
     /// Returns the SVG renderer GPUI uses
-    pub(crate) fn svg_renderer(&self) -> SvgRenderer {
+    pub fn svg_renderer(&self) -> SvgRenderer {
         self.svg_renderer.clone()
     }
 
@@ -1369,7 +1369,7 @@ impl AppContext {
     }
 
     /// Remove an asset from GPUI's cache
-    pub fn remove_cached_asset<A: Asset + 'static>(&mut self, source: &A::Source) {
+    pub fn remove_asset<A: Asset>(&mut self, source: &A::Source) {
         let asset_id = (TypeId::of::<A>(), hash(source));
         self.loading_assets.remove(&asset_id);
     }
@@ -1378,12 +1378,7 @@ impl AppContext {
     ///
     /// Note that the multiple calls to this method will only result in one `Asset::load` call at a
     /// time, and the results of this call will be cached
-    ///
-    /// This asset will not be cached by default, see [Self::use_cached_asset]
-    pub fn fetch_asset<A: Asset + 'static>(
-        &mut self,
-        source: &A::Source,
-    ) -> (Shared<Task<A::Output>>, bool) {
+    pub fn fetch_asset<A: Asset>(&mut self, source: &A::Source) -> (Shared<Task<A::Output>>, bool) {
         let asset_id = (TypeId::of::<A>(), hash(source));
         let mut is_first = false;
         let task = self

crates/gpui/src/asset_cache.rs 🔗

@@ -1,30 +1,43 @@
 use crate::{AppContext, SharedString, SharedUri};
 use futures::Future;
+
+use std::fmt::Debug;
 use std::hash::{Hash, Hasher};
-use std::path::PathBuf;
+use std::marker::PhantomData;
+use std::path::{Path, PathBuf};
 use std::sync::Arc;
 
+/// An enum representing
 #[derive(Debug, PartialEq, Eq, Hash, Clone)]
-pub(crate) enum UriOrPath {
+pub enum Resource {
+    /// This resource is at a given URI
     Uri(SharedUri),
-    Path(Arc<PathBuf>),
+    /// This resource is at a given path in the file system
+    Path(Arc<Path>),
+    /// This resource is embedded in the application binary
     Embedded(SharedString),
 }
 
-impl From<SharedUri> for UriOrPath {
+impl From<SharedUri> for Resource {
     fn from(value: SharedUri) -> Self {
         Self::Uri(value)
     }
 }
 
-impl From<Arc<PathBuf>> for UriOrPath {
-    fn from(value: Arc<PathBuf>) -> Self {
+impl From<PathBuf> for Resource {
+    fn from(value: PathBuf) -> Self {
+        Self::Path(value.into())
+    }
+}
+
+impl From<Arc<Path>> for Resource {
+    fn from(value: Arc<Path>) -> Self {
         Self::Path(value)
     }
 }
 
 /// A trait for asynchronous asset loading.
-pub trait Asset {
+pub trait Asset: 'static {
     /// The source of the asset.
     type Source: Clone + Hash + Send;
 
@@ -38,6 +51,31 @@ pub trait Asset {
     ) -> impl Future<Output = Self::Output> + Send + 'static;
 }
 
+/// An asset Loader that logs whatever passes through it
+pub enum AssetLogger<T> {
+    #[doc(hidden)]
+    _Phantom(PhantomData<T>, &'static dyn crate::seal::Sealed),
+}
+
+impl<R: Clone + Send, E: Clone + Send + std::error::Error, T: Asset<Output = Result<R, E>>> Asset
+    for AssetLogger<T>
+{
+    type Source = T::Source;
+
+    type Output = T::Output;
+
+    fn load(
+        source: Self::Source,
+        cx: &mut AppContext,
+    ) -> impl Future<Output = Self::Output> + Send + 'static {
+        let load = T::load(source, cx);
+        async {
+            load.await
+                .inspect_err(|e| log::error!("Failed to load asset: {}", e))
+        }
+    }
+}
+
 /// Use a quick, non-cryptographically secure hash function to get an identifier from data
 pub fn hash<T: Hash>(data: &T) -> u64 {
     let mut hasher = collections::FxHasher::default();

crates/gpui/src/elements/div.rs 🔗

@@ -2383,7 +2383,7 @@ where
 
 /// A wrapper around an element that can store state, produced after assigning an ElementId.
 pub struct Stateful<E> {
-    element: E,
+    pub(crate) element: E,
 }
 
 impl<E> Styled for Stateful<E>

crates/gpui/src/elements/img.rs 🔗

@@ -1,9 +1,11 @@
 use crate::{
-    px, AbsoluteLength, AppContext, Asset, Bounds, DefiniteLength, Element, ElementId,
-    GlobalElementId, Hitbox, Image, InteractiveElement, Interactivity, IntoElement, LayoutId,
-    Length, ObjectFit, Pixels, RenderImage, SharedString, SharedUri, StyleRefinement, Styled,
-    SvgSize, UriOrPath, WindowContext,
+    px, AbsoluteLength, AnyElement, AppContext, Asset, AssetLogger, Bounds, DefiniteLength,
+    Element, ElementId, GlobalElementId, Hitbox, Image, InteractiveElement, Interactivity,
+    IntoElement, LayoutId, Length, ObjectFit, Pixels, RenderImage, Resource, SharedString,
+    SharedUri, StyleRefinement, Styled, SvgSize, Task, WindowContext,
 };
+use anyhow::{anyhow, Result};
+
 use futures::{AsyncReadExt, Future};
 use image::{
     codecs::gif::GifDecoder, AnimationDecoder, Frame, ImageBuffer, ImageError, ImageFormat,
@@ -11,45 +13,56 @@ use image::{
 use smallvec::SmallVec;
 use std::{
     fs,
-    io::Cursor,
-    path::PathBuf,
+    io::{self, Cursor},
+    ops::{Deref, DerefMut},
+    path::{Path, PathBuf},
+    str::FromStr,
     sync::Arc,
     time::{Duration, Instant},
 };
 use thiserror::Error;
 use util::ResultExt;
 
+use super::{FocusableElement, Stateful, StatefulInteractiveElement};
+
+/// The delay before showing the loading state.
+pub const LOADING_DELAY: Duration = Duration::from_millis(200);
+
+/// A type alias to the resource loader that the `img()` element uses.
+///
+/// Note: that this is only for Resources, like URLs or file paths.
+/// Custom loaders, or external images will not use this asset loader
+pub type ImgResourceLoader = AssetLogger<ImageAssetLoader>;
+
 /// A source of image content.
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone)]
 pub enum ImageSource {
-    /// Image content will be loaded from provided URI at render time.
-    Uri(SharedUri),
-    /// Image content will be loaded from the provided file at render time.
-    File(Arc<PathBuf>),
+    /// The image content will be loaded from some resource location
+    Resource(Resource),
     /// Cached image data
     Render(Arc<RenderImage>),
     /// Cached image data
     Image(Arc<Image>),
-    /// Image content will be loaded from Asset at render time.
-    Embedded(SharedString),
+    /// A custom loading function to use
+    Custom(Arc<dyn Fn(&mut WindowContext) -> Option<Result<Arc<RenderImage>, ImageCacheError>>>),
 }
 
 fn is_uri(uri: &str) -> bool {
-    uri.contains("://")
+    http_client::Uri::from_str(uri).is_ok()
 }
 
 impl From<SharedUri> for ImageSource {
     fn from(value: SharedUri) -> Self {
-        Self::Uri(value)
+        Self::Resource(Resource::Uri(value))
     }
 }
 
-impl From<&'static str> for ImageSource {
-    fn from(s: &'static str) -> Self {
+impl<'a> From<&'a str> for ImageSource {
+    fn from(s: &'a str) -> Self {
         if is_uri(s) {
-            Self::Uri(s.into())
+            Self::Resource(Resource::Uri(s.to_string().into()))
         } else {
-            Self::Embedded(s.into())
+            Self::Resource(Resource::Embedded(s.to_string().into()))
         }
     }
 }
@@ -57,32 +70,34 @@ impl From<&'static str> for ImageSource {
 impl From<String> for ImageSource {
     fn from(s: String) -> Self {
         if is_uri(&s) {
-            Self::Uri(s.into())
+            Self::Resource(Resource::Uri(s.into()))
         } else {
-            Self::Embedded(s.into())
+            Self::Resource(Resource::Embedded(s.into()))
         }
     }
 }
 
 impl From<SharedString> for ImageSource {
     fn from(s: SharedString) -> Self {
-        if is_uri(&s) {
-            Self::Uri(s.into())
-        } else {
-            Self::Embedded(s)
-        }
+        s.as_ref().into()
+    }
+}
+
+impl From<&Path> for ImageSource {
+    fn from(value: &Path) -> Self {
+        Self::Resource(value.to_path_buf().into())
     }
 }
 
-impl From<Arc<PathBuf>> for ImageSource {
-    fn from(value: Arc<PathBuf>) -> Self {
-        Self::File(value)
+impl From<Arc<Path>> for ImageSource {
+    fn from(value: Arc<Path>) -> Self {
+        Self::Resource(value.into())
     }
 }
 
 impl From<PathBuf> for ImageSource {
     fn from(value: PathBuf) -> Self {
-        Self::File(value.into())
+        Self::Resource(value.into())
     }
 }
 
@@ -98,12 +113,80 @@ impl From<Arc<Image>> for ImageSource {
     }
 }
 
+impl<F: Fn(&mut WindowContext) -> Option<Result<Arc<RenderImage>, ImageCacheError>> + 'static>
+    From<F> for ImageSource
+{
+    fn from(value: F) -> Self {
+        Self::Custom(Arc::new(value))
+    }
+}
+
+/// The style of an image element.
+pub struct ImageStyle {
+    grayscale: bool,
+    object_fit: ObjectFit,
+    loading: Option<Box<dyn Fn() -> AnyElement>>,
+    fallback: Option<Box<dyn Fn() -> AnyElement>>,
+}
+
+impl Default for ImageStyle {
+    fn default() -> Self {
+        Self {
+            grayscale: false,
+            object_fit: ObjectFit::Contain,
+            loading: None,
+            fallback: None,
+        }
+    }
+}
+
+/// Style an image element.
+pub trait StyledImage: Sized {
+    /// Get a mutable [ImageStyle] from the element.
+    fn image_style(&mut self) -> &mut ImageStyle;
+
+    /// Set the image to be displayed in grayscale.
+    fn grayscale(mut self, grayscale: bool) -> Self {
+        self.image_style().grayscale = grayscale;
+        self
+    }
+
+    /// Set the object fit for the image.
+    fn object_fit(mut self, object_fit: ObjectFit) -> Self {
+        self.image_style().object_fit = object_fit;
+        self
+    }
+
+    /// Set the object fit for the image.
+    fn with_fallback(mut self, fallback: impl Fn() -> AnyElement + 'static) -> Self {
+        self.image_style().fallback = Some(Box::new(fallback));
+        self
+    }
+
+    /// Set the object fit for the image.
+    fn with_loading(mut self, loading: impl Fn() -> AnyElement + 'static) -> Self {
+        self.image_style().loading = Some(Box::new(loading));
+        self
+    }
+}
+
+impl StyledImage for Img {
+    fn image_style(&mut self) -> &mut ImageStyle {
+        &mut self.style
+    }
+}
+
+impl StyledImage for Stateful<Img> {
+    fn image_style(&mut self) -> &mut ImageStyle {
+        &mut self.element.style
+    }
+}
+
 /// An image element.
 pub struct Img {
     interactivity: Interactivity,
     source: ImageSource,
-    grayscale: bool,
-    object_fit: ObjectFit,
+    style: ImageStyle,
 }
 
 /// Create a new image element.
@@ -111,8 +194,7 @@ pub fn img(source: impl Into<ImageSource>) -> Img {
     Img {
         interactivity: Interactivity::default(),
         source: source.into(),
-        grayscale: false,
-        object_fit: ObjectFit::Contain,
+        style: ImageStyle::default(),
     }
 }
 
@@ -125,16 +207,19 @@ impl Img {
             "hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg",
         ]
     }
+}
 
-    /// Set the image to be displayed in grayscale.
-    pub fn grayscale(mut self, grayscale: bool) -> Self {
-        self.grayscale = grayscale;
-        self
+impl Deref for Stateful<Img> {
+    type Target = Img;
+
+    fn deref(&self) -> &Self::Target {
+        &self.element
     }
-    /// Set the object fit for the image.
-    pub fn object_fit(mut self, object_fit: ObjectFit) -> Self {
-        self.object_fit = object_fit;
-        self
+}
+
+impl DerefMut for Stateful<Img> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.element
     }
 }
 
@@ -142,10 +227,17 @@ impl Img {
 struct ImgState {
     frame_index: usize,
     last_frame_time: Option<Instant>,
+    started_loading: Option<(Instant, Task<()>)>,
+}
+
+/// The image layout state between frames
+pub struct ImgLayoutState {
+    frame_index: usize,
+    replacement: Option<AnyElement>,
 }
 
 impl Element for Img {
-    type RequestLayoutState = usize;
+    type RequestLayoutState = ImgLayoutState;
     type PrepaintState = Option<Hitbox>;
 
     fn id(&self) -> Option<ElementId> {
@@ -157,11 +249,17 @@ impl Element for Img {
         global_id: Option<&GlobalElementId>,
         cx: &mut WindowContext,
     ) -> (LayoutId, Self::RequestLayoutState) {
+        let mut layout_state = ImgLayoutState {
+            frame_index: 0,
+            replacement: None,
+        };
+
         cx.with_optional_element_state(global_id, |state, cx| {
             let mut state = state.map(|state| {
                 state.unwrap_or(ImgState {
                     frame_index: 0,
                     last_frame_time: None,
+                    started_loading: None,
                 })
             });
 
@@ -170,64 +268,105 @@ impl Element for Img {
             let layout_id = self
                 .interactivity
                 .request_layout(global_id, cx, |mut style, cx| {
-                    if let Some(data) = self.source.use_data(cx) {
-                        if let Some(state) = &mut state {
-                            let frame_count = data.frame_count();
-                            if frame_count > 1 {
-                                let current_time = Instant::now();
-                                if let Some(last_frame_time) = state.last_frame_time {
-                                    let elapsed = current_time - last_frame_time;
-                                    let frame_duration =
-                                        Duration::from(data.delay(state.frame_index));
-
-                                    if elapsed >= frame_duration {
-                                        state.frame_index = (state.frame_index + 1) % frame_count;
-                                        state.last_frame_time =
-                                            Some(current_time - (elapsed - frame_duration));
+                    let mut replacement_id = None;
+
+                    match self.source.use_data(cx) {
+                        Some(Ok(data)) => {
+                            if let Some(state) = &mut state {
+                                let frame_count = data.frame_count();
+                                if frame_count > 1 {
+                                    let current_time = Instant::now();
+                                    if let Some(last_frame_time) = state.last_frame_time {
+                                        let elapsed = current_time - last_frame_time;
+                                        let frame_duration =
+                                            Duration::from(data.delay(state.frame_index));
+
+                                        if elapsed >= frame_duration {
+                                            state.frame_index =
+                                                (state.frame_index + 1) % frame_count;
+                                            state.last_frame_time =
+                                                Some(current_time - (elapsed - frame_duration));
+                                        }
+                                    } else {
+                                        state.last_frame_time = Some(current_time);
                                     }
-                                } else {
-                                    state.last_frame_time = Some(current_time);
                                 }
+                                state.started_loading = None;
                             }
-                        }
 
-                        let image_size = data.size(frame_index);
-
-                        if let Length::Auto = style.size.width {
-                            style.size.width = match style.size.height {
-                                Length::Definite(DefiniteLength::Absolute(
-                                    AbsoluteLength::Pixels(height),
-                                )) => Length::Definite(
-                                    px(image_size.width.0 as f32 * height.0
-                                        / image_size.height.0 as f32)
-                                    .into(),
-                                ),
-                                _ => Length::Definite(px(image_size.width.0 as f32).into()),
-                            };
-                        }
+                            let image_size = data.size(frame_index);
+
+                            if let Length::Auto = style.size.width {
+                                style.size.width = match style.size.height {
+                                    Length::Definite(DefiniteLength::Absolute(
+                                        AbsoluteLength::Pixels(height),
+                                    )) => Length::Definite(
+                                        px(image_size.width.0 as f32 * height.0
+                                            / image_size.height.0 as f32)
+                                        .into(),
+                                    ),
+                                    _ => Length::Definite(px(image_size.width.0 as f32).into()),
+                                };
+                            }
 
-                        if let Length::Auto = style.size.height {
-                            style.size.height = match style.size.width {
-                                Length::Definite(DefiniteLength::Absolute(
-                                    AbsoluteLength::Pixels(width),
-                                )) => Length::Definite(
-                                    px(image_size.height.0 as f32 * width.0
-                                        / image_size.width.0 as f32)
-                                    .into(),
-                                ),
-                                _ => Length::Definite(px(image_size.height.0 as f32).into()),
-                            };
-                        }
+                            if let Length::Auto = style.size.height {
+                                style.size.height = match style.size.width {
+                                    Length::Definite(DefiniteLength::Absolute(
+                                        AbsoluteLength::Pixels(width),
+                                    )) => Length::Definite(
+                                        px(image_size.height.0 as f32 * width.0
+                                            / image_size.width.0 as f32)
+                                        .into(),
+                                    ),
+                                    _ => Length::Definite(px(image_size.height.0 as f32).into()),
+                                };
+                            }
 
-                        if global_id.is_some() && data.frame_count() > 1 {
-                            cx.request_animation_frame();
+                            if global_id.is_some() && data.frame_count() > 1 {
+                                cx.request_animation_frame();
+                            }
+                        }
+                        Some(_err) => {
+                            if let Some(fallback) = self.style.fallback.as_ref() {
+                                let mut element = fallback();
+                                replacement_id = Some(element.request_layout(cx));
+                                layout_state.replacement = Some(element);
+                            }
+                            if let Some(state) = &mut state {
+                                state.started_loading = None;
+                            }
+                        }
+                        None => {
+                            if let Some(state) = &mut state {
+                                if let Some((started_loading, _)) = state.started_loading {
+                                    if started_loading.elapsed() > LOADING_DELAY {
+                                        if let Some(loading) = self.style.loading.as_ref() {
+                                            let mut element = loading();
+                                            replacement_id = Some(element.request_layout(cx));
+                                            layout_state.replacement = Some(element);
+                                        }
+                                    }
+                                } else {
+                                    let parent_view_id = cx.parent_view_id();
+                                    let task = cx.spawn(|mut cx| async move {
+                                        cx.background_executor().timer(LOADING_DELAY).await;
+                                        cx.update(|cx| {
+                                            cx.notify(parent_view_id);
+                                        })
+                                        .ok();
+                                    });
+                                    state.started_loading = Some((Instant::now(), task));
+                                }
+                            }
                         }
                     }
 
-                    cx.request_layout(style, [])
+                    cx.request_layout(style, replacement_id)
                 });
 
-            ((layout_id, frame_index), state)
+            layout_state.frame_index = frame_index;
+
+            ((layout_id, layout_state), state)
         })
     }
 
@@ -235,18 +374,24 @@ impl Element for Img {
         &mut self,
         global_id: Option<&GlobalElementId>,
         bounds: Bounds<Pixels>,
-        _request_layout: &mut Self::RequestLayoutState,
+        request_layout: &mut Self::RequestLayoutState,
         cx: &mut WindowContext,
-    ) -> Option<Hitbox> {
+    ) -> Self::PrepaintState {
         self.interactivity
-            .prepaint(global_id, bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
+            .prepaint(global_id, bounds, bounds.size, cx, |_, _, hitbox, cx| {
+                if let Some(replacement) = &mut request_layout.replacement {
+                    replacement.prepaint(cx);
+                }
+
+                hitbox
+            })
     }
 
     fn paint(
         &mut self,
         global_id: Option<&GlobalElementId>,
         bounds: Bounds<Pixels>,
-        frame_index: &mut Self::RequestLayoutState,
+        layout_state: &mut Self::RequestLayoutState,
         hitbox: &mut Self::PrepaintState,
         cx: &mut WindowContext,
     ) {
@@ -255,29 +400,26 @@ impl Element for Img {
             .paint(global_id, bounds, hitbox.as_ref(), cx, |style, cx| {
                 let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
 
-                if let Some(data) = source.use_data(cx) {
-                    let new_bounds = self.object_fit.get_bounds(bounds, data.size(*frame_index));
+                if let Some(Ok(data)) = source.use_data(cx) {
+                    let new_bounds = self
+                        .style
+                        .object_fit
+                        .get_bounds(bounds, data.size(layout_state.frame_index));
                     cx.paint_image(
                         new_bounds,
                         corner_radii,
                         data.clone(),
-                        *frame_index,
-                        self.grayscale,
+                        layout_state.frame_index,
+                        self.style.grayscale,
                     )
                     .log_err();
+                } else if let Some(replacement) = &mut layout_state.replacement {
+                    replacement.paint(cx);
                 }
             })
     }
 }
 
-impl IntoElement for Img {
-    type Element = Self;
-
-    fn into_element(self) -> Self::Element {
-        self
-    }
-}
-
 impl Styled for Img {
     fn style(&mut self) -> &mut StyleRefinement {
         &mut self.interactivity.base_style
@@ -290,41 +432,28 @@ impl InteractiveElement for Img {
     }
 }
 
-impl ImageSource {
-    pub(crate) fn use_data(&self, cx: &mut WindowContext) -> Option<Arc<RenderImage>> {
-        match self {
-            ImageSource::Uri(_) | ImageSource::Embedded(_) | ImageSource::File(_) => {
-                let uri_or_path: UriOrPath = match self {
-                    ImageSource::Uri(uri) => uri.clone().into(),
-                    ImageSource::File(path) => path.clone().into(),
-                    ImageSource::Embedded(path) => UriOrPath::Embedded(path.clone()),
-                    _ => unreachable!(),
-                };
-
-                cx.use_asset::<ImageAsset>(&uri_or_path)?.log_err()
-            }
+impl IntoElement for Img {
+    type Element = Self;
 
-            ImageSource::Render(data) => Some(data.to_owned()),
-            ImageSource::Image(data) => cx.use_asset::<ImageDecoder>(data)?.log_err(),
-        }
+    fn into_element(self) -> Self::Element {
+        self
     }
+}
 
-    /// Fetch the data associated with this source, using GPUI's asset caching
-    pub async fn data(&self, cx: &mut AppContext) -> Option<Arc<RenderImage>> {
-        match self {
-            ImageSource::Uri(_) | ImageSource::Embedded(_) | ImageSource::File(_) => {
-                let uri_or_path: UriOrPath = match self {
-                    ImageSource::Uri(uri) => uri.clone().into(),
-                    ImageSource::File(path) => path.clone().into(),
-                    ImageSource::Embedded(path) => UriOrPath::Embedded(path.clone()),
-                    _ => unreachable!(),
-                };
+impl FocusableElement for Img {}
 
-                cx.fetch_asset::<ImageAsset>(&uri_or_path).0.await.log_err()
-            }
+impl StatefulInteractiveElement for Img {}
 
-            ImageSource::Render(data) => Some(data.to_owned()),
-            ImageSource::Image(data) => cx.fetch_asset::<ImageDecoder>(data).0.await.log_err(),
+impl ImageSource {
+    pub(crate) fn use_data(
+        &self,
+        cx: &mut WindowContext,
+    ) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
+        match self {
+            ImageSource::Resource(resource) => cx.use_asset::<ImgResourceLoader>(&resource),
+            ImageSource::Custom(loading_fn) => loading_fn(cx),
+            ImageSource::Render(data) => Some(Ok(data.to_owned())),
+            ImageSource::Image(data) => cx.use_asset::<AssetLogger<ImageDecoder>>(data),
         }
     }
 }
@@ -334,22 +463,23 @@ enum ImageDecoder {}
 
 impl Asset for ImageDecoder {
     type Source = Arc<Image>;
-    type Output = Result<Arc<RenderImage>, Arc<anyhow::Error>>;
+    type Output = Result<Arc<RenderImage>, ImageCacheError>;
 
     fn load(
         source: Self::Source,
         cx: &mut AppContext,
     ) -> impl Future<Output = Self::Output> + Send + 'static {
-        let result = source.to_image_data(cx).map_err(Arc::new);
-        async { result }
+        let renderer = cx.svg_renderer();
+        async move { source.to_image_data(renderer).map_err(Into::into) }
     }
 }
 
+/// An image loader for the GPUI asset system
 #[derive(Clone)]
-enum ImageAsset {}
+pub enum ImageAssetLoader {}
 
-impl Asset for ImageAsset {
-    type Source = UriOrPath;
+impl Asset for ImageAssetLoader {
+    type Source = Resource;
     type Output = Result<Arc<RenderImage>, ImageCacheError>;
 
     fn load(
@@ -363,12 +493,12 @@ impl Asset for ImageAsset {
         let asset_source = cx.asset_source().clone();
         async move {
             let bytes = match source.clone() {
-                UriOrPath::Path(uri) => fs::read(uri.as_ref())?,
-                UriOrPath::Uri(uri) => {
+                Resource::Path(uri) => fs::read(uri.as_ref())?,
+                Resource::Uri(uri) => {
                     let mut response = client
                         .get(uri.as_ref(), ().into(), true)
                         .await
-                        .map_err(|e| ImageCacheError::Client(Arc::new(e)))?;
+                        .map_err(|e| anyhow!(e))?;
                     let mut body = Vec::new();
                     response.body_mut().read_to_end(&mut body).await?;
                     if !response.status().is_success() {
@@ -383,13 +513,13 @@ impl Asset for ImageAsset {
                     }
                     body
                 }
-                UriOrPath::Embedded(path) => {
+                Resource::Embedded(path) => {
                     let data = asset_source.load(&path).ok().flatten();
                     if let Some(data) = data {
                         data.to_vec()
                     } else {
                         return Err(ImageCacheError::Asset(
-                            format!("not found: {}", path).into(),
+                            format!("Embedded resource not found: {}", path).into(),
                         ));
                     }
                 }
@@ -450,9 +580,9 @@ impl Asset for ImageAsset {
 /// An error that can occur when interacting with the image cache.
 #[derive(Debug, Error, Clone)]
 pub enum ImageCacheError {
-    /// An error that occurred while fetching an image from a remote source.
-    #[error("http error: {0}")]
-    Client(#[from] Arc<anyhow::Error>),
+    /// Some other kind of error occurred
+    #[error("error: {0}")]
+    Other(#[from] Arc<anyhow::Error>),
     /// An error that occurred while reading the image from disk.
     #[error("IO error: {0}")]
     Io(Arc<std::io::Error>),
@@ -477,20 +607,26 @@ pub enum ImageCacheError {
     Usvg(Arc<usvg::Error>),
 }
 
-impl From<std::io::Error> for ImageCacheError {
-    fn from(error: std::io::Error) -> Self {
-        Self::Io(Arc::new(error))
+impl From<anyhow::Error> for ImageCacheError {
+    fn from(value: anyhow::Error) -> Self {
+        Self::Other(Arc::new(value))
     }
 }
 
-impl From<ImageError> for ImageCacheError {
-    fn from(error: ImageError) -> Self {
-        Self::Image(Arc::new(error))
+impl From<io::Error> for ImageCacheError {
+    fn from(value: io::Error) -> Self {
+        Self::Io(Arc::new(value))
     }
 }
 
 impl From<usvg::Error> for ImageCacheError {
-    fn from(error: usvg::Error) -> Self {
-        Self::Usvg(Arc::new(error))
+    fn from(value: usvg::Error) -> Self {
+        Self::Usvg(Arc::new(value))
+    }
+}
+
+impl From<image::ImageError> for ImageCacheError {
+    fn from(value: image::ImageError) -> Self {
+        Self::Image(Arc::new(value))
     }
 }

crates/gpui/src/platform.rs 🔗

@@ -27,11 +27,11 @@ mod test;
 mod windows;
 
 use crate::{
-    point, Action, AnyWindowHandle, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds,
-    DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun, ForegroundExecutor,
-    GPUSpecs, GlyphId, ImageSource, Keymap, LineLayout, Pixels, PlatformInput, Point,
-    RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, ScaledPixels, Scene,
-    SharedString, Size, SvgSize, Task, TaskLabel, WindowContext, DEFAULT_WINDOW_SIZE,
+    point, Action, AnyWindowHandle, AsyncWindowContext, BackgroundExecutor, Bounds, DevicePixels,
+    DispatchEventResult, Font, FontId, FontMetrics, FontRun, ForegroundExecutor, GPUSpecs, GlyphId,
+    ImageSource, Keymap, LineLayout, Pixels, PlatformInput, Point, RenderGlyphParams, RenderImage,
+    RenderImageParams, RenderSvgParams, ScaledPixels, Scene, SharedString, Size, SvgRenderer,
+    SvgSize, Task, TaskLabel, WindowContext, DEFAULT_WINDOW_SIZE,
 };
 use anyhow::{anyhow, Result};
 use async_task::Runnable;
@@ -1290,11 +1290,13 @@ impl Image {
 
     /// Use the GPUI `use_asset` API to make this image renderable
     pub fn use_render_image(self: Arc<Self>, cx: &mut WindowContext) -> Option<Arc<RenderImage>> {
-        ImageSource::Image(self).use_data(cx)
+        ImageSource::Image(self)
+            .use_data(cx)
+            .and_then(|result| result.ok())
     }
 
     /// Convert the clipboard image to an `ImageData` object.
-    pub fn to_image_data(&self, cx: &AppContext) -> Result<Arc<RenderImage>> {
+    pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
         fn frames_for_image(
             bytes: &[u8],
             format: image::ImageFormat,
@@ -1331,10 +1333,7 @@ impl Image {
             ImageFormat::Bmp => frames_for_image(&self.bytes, image::ImageFormat::Bmp)?,
             ImageFormat::Tiff => frames_for_image(&self.bytes, image::ImageFormat::Tiff)?,
             ImageFormat::Svg => {
-                // TODO: Fix this
-                let pixmap = cx
-                    .svg_renderer()
-                    .render_pixmap(&self.bytes, SvgSize::ScaleFactor(1.0))?;
+                let pixmap = svg_renderer.render_pixmap(&self.bytes, SvgSize::ScaleFactor(1.0))?;
 
                 let buffer =
                     image::ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take())

crates/gpui/src/prelude.rs 🔗

@@ -5,5 +5,5 @@
 pub use crate::{
     util::FluentBuilder, BorrowAppContext, BorrowWindow, Context, Element, FocusableElement,
     InteractiveElement, IntoElement, ParentElement, Refineable, Render, RenderOnce,
-    StatefulInteractiveElement, Styled, VisualContext,
+    StatefulInteractiveElement, Styled, StyledImage, VisualContext,
 };

crates/gpui/src/svg_renderer.rs 🔗

@@ -10,7 +10,7 @@ pub(crate) struct RenderSvgParams {
 }
 
 #[derive(Clone)]
-pub(crate) struct SvgRenderer {
+pub struct SvgRenderer {
     asset_source: Arc<dyn AssetSource>,
 }
 
@@ -24,7 +24,7 @@ impl SvgRenderer {
         Self { asset_source }
     }
 
-    pub fn render(&self, params: &RenderSvgParams) -> Result<Option<Vec<u8>>> {
+    pub(crate) fn render(&self, params: &RenderSvgParams) -> Result<Option<Vec<u8>>> {
         if params.size.is_zero() {
             return Err(anyhow!("can't render at a zero size"));
         }

crates/gpui/src/window.rs 🔗

@@ -900,7 +900,13 @@ impl<'a> WindowContext<'a> {
 
     /// Indicate that this view has changed, which will invoke any observers and also mark the window as dirty.
     /// If this view or any of its ancestors are *cached*, notifying it will cause it or its ancestors to be redrawn.
-    pub fn notify(&mut self, view_id: EntityId) {
+    /// Note that this method will always cause a redraw, the entire window is refreshed if view_id is None.
+    pub fn notify(&mut self, view_id: Option<EntityId>) {
+        let Some(view_id) = view_id else {
+            self.refresh();
+            return;
+        };
+
         for view_id in self
             .window
             .rendered_frame
@@ -1165,13 +1171,7 @@ impl<'a> WindowContext<'a> {
     /// If called from within a view, it will notify that view on the next frame. Otherwise, it will refresh the entire window.
     pub fn request_animation_frame(&self) {
         let parent_id = self.parent_view_id();
-        self.on_next_frame(move |cx| {
-            if let Some(parent_id) = parent_id {
-                cx.notify(parent_id)
-            } else {
-                cx.refresh()
-            }
-        });
+        self.on_next_frame(move |cx| cx.notify(parent_id));
     }
 
     /// Spawn the future returned by the given closure on the application thread pool.
@@ -1982,9 +1982,7 @@ impl<'a> WindowContext<'a> {
     ///
     /// Note that the multiple calls to this method will only result in one `Asset::load` call at a
     /// time.
-    ///
-    /// This asset will not be cached by default, see [Self::use_cached_asset]
-    pub fn use_asset<A: Asset + 'static>(&mut self, source: &A::Source) -> Option<A::Output> {
+    pub fn use_asset<A: Asset>(&mut self, source: &A::Source) -> Option<A::Output> {
         let (task, is_first) = self.fetch_asset::<A>(source);
         task.clone().now_or_never().or_else(|| {
             if is_first {
@@ -1994,13 +1992,7 @@ impl<'a> WindowContext<'a> {
                     |mut cx| async move {
                         task.await;
 
-                        cx.on_next_frame(move |cx| {
-                            if let Some(parent_id) = parent_id {
-                                cx.notify(parent_id)
-                            } else {
-                                cx.refresh()
-                            }
-                        });
+                        cx.on_next_frame(move |cx| cx.notify(parent_id));
                     }
                 })
                 .detach();
@@ -2163,6 +2155,9 @@ impl<'a> WindowContext<'a> {
     /// A variant of `with_element_state` that allows the element's id to be optional. This is a convenience
     /// method for elements where the element id may or may not be assigned. Prefer using `with_element_state`
     /// when the element is guaranteed to have an id.
+    ///
+    /// The first option means 'no ID provided'
+    /// The second option means 'not yet initialized'
     pub fn with_optional_element_state<S, R>(
         &mut self,
         global_id: Option<&GlobalElementId>,
@@ -4227,7 +4222,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     /// Indicate that this view has changed, which will invoke any observers and also mark the window as dirty.
     /// If this view or any of its ancestors are *cached*, notifying it will cause it or its ancestors to be redrawn.
     pub fn notify(&mut self) {
-        self.window_cx.notify(self.view.entity_id());
+        self.window_cx.notify(Some(self.view.entity_id()));
     }
 
     /// Register a callback to be invoked when the window is resized.

crates/ui/src/components/scrollbar.rs 🔗

@@ -370,7 +370,7 @@ impl Element for Scrollbar {
                         };
 
                         if let Some(id) = state.parent_id {
-                            cx.notify(id);
+                            cx.notify(Some(id));
                         }
                     }
                 } else {
@@ -382,7 +382,7 @@ impl Element for Scrollbar {
                 if phase.bubble() {
                     state.drag.take();
                     if let Some(id) = state.parent_id {
-                        cx.notify(id);
+                        cx.notify(Some(id));
                     }
                 }
             });

crates/workspace/src/workspace.rs 🔗

@@ -5896,7 +5896,7 @@ pub fn client_side_decorations(element: impl IntoElement, cx: &mut WindowContext
                     let edge = cx.try_global::<GlobalResizeEdge>();
                     if new_edge != edge.map(|edge| edge.0) {
                         cx.window_handle()
-                            .update(cx, |workspace, cx| cx.notify(workspace.entity_id()))
+                            .update(cx, |workspace, cx| cx.notify(Some(workspace.entity_id())))
                             .ok();
                     }
                 })