mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-24 19:10:24 +00:00
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
@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>
214 lines
7.9 KiB
Rust
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();
|
|
});
|
|
}
|