zed/crates/gpui/examples/image_loading.rs
Mikayla Maki 516f7b3642
Some checks are pending
CI / Check Postgres and Protobuf migrations, mergability (push) Waiting to run
CI / Check formatting and spelling (push) Waiting to run
CI / (macOS) Run Clippy and tests (push) Waiting to run
CI / (Linux) Run Clippy and tests (push) Waiting to run
CI / (Linux) Build Remote Server (push) Waiting to run
CI / (Windows) Run Clippy and tests (push) Waiting to run
CI / Create a macOS bundle (push) Blocked by required conditions
CI / Create a Linux bundle (push) Blocked by required conditions
CI / Create arm64 Linux bundle (push) Blocked by required conditions
Deploy Docs / Deploy Docs (push) Waiting to run
Docs / Check formatting (push) Waiting to run
Add Loading and Fallback States to Image Elements (via StyledImage) (#20371)
@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>
2024-11-15 19:12:01 -08:00

214 lines
7.9 KiB
Rust

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();
});
}