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>
This commit is contained in:
Mikayla Maki 2024-11-15 19:12:01 -08:00 committed by GitHub
parent f34877334e
commit 516f7b3642
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 1700 additions and 1041 deletions

1922
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -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(),

View file

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

View file

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

View file

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

View file

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

View file

@ -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<Arc<PathBuf>> for ImageSource {
fn from(value: Arc<PathBuf>) -> Self {
Self::File(value)
impl From<&Path> for ImageSource {
fn from(value: &Path) -> Self {
Self::Resource(value.to_path_buf().into())
}
}
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));
let mut replacement_id = None;
if elapsed >= frame_duration {
state.frame_index = (state.frame_index + 1) % frame_count;
state.last_frame_time =
Some(current_time - (elapsed - frame_duration));
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);
}
}
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()),
};
}
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();
}
}
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 {
state.last_frame_time = Some(current_time);
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));
}
}
}
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 global_id.is_some() && data.frame_count() > 1 {
cx.request_animation_frame();
}
}
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!(),
};
impl IntoElement for Img {
type Element = Self;
cx.use_asset::<ImageAsset>(&uri_or_path)?.log_err()
}
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>> {
impl FocusableElement for Img {}
impl StatefulInteractiveElement for Img {}
impl ImageSource {
pub(crate) fn use_data(
&self,
cx: &mut WindowContext,
) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
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.fetch_asset::<ImageAsset>(&uri_or_path).0.await.log_err()
}
ImageSource::Render(data) => Some(data.to_owned()),
ImageSource::Image(data) => cx.fetch_asset::<ImageDecoder>(data).0.await.log_err(),
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))
}
}

View file

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

View file

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

View file

@ -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"));
}

View file

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

View file

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

View file

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