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:
parent
f34877334e
commit
516f7b3642
15 changed files with 1700 additions and 1041 deletions
1922
Cargo.lock
generated
1922
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -3333,7 +3333,8 @@ impl ContextEditor {
|
||||||
|
|
||||||
self.context.update(cx, |context, cx| {
|
self.context.update(cx, |context, cx| {
|
||||||
for image in images {
|
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;
|
continue;
|
||||||
};
|
};
|
||||||
let image_id = image.id();
|
let image_id = image.id();
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
use gpui::{
|
use gpui::{div, img, prelude::*, App, AppContext, Render, ViewContext, WindowOptions};
|
||||||
div, img, prelude::*, App, AppContext, ImageSource, Render, ViewContext, WindowOptions,
|
|
||||||
};
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
struct GifViewer {
|
struct GifViewer {
|
||||||
|
@ -16,7 +14,7 @@ impl GifViewer {
|
||||||
impl Render for GifViewer {
|
impl Render for GifViewer {
|
||||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
div().size_full().child(
|
div().size_full().child(
|
||||||
img(ImageSource::File(self.gif_path.clone().into()))
|
img(self.gif_path.clone())
|
||||||
.size_full()
|
.size_full()
|
||||||
.object_fit(gpui::ObjectFit::Contain)
|
.object_fit(gpui::ObjectFit::Contain)
|
||||||
.id("gif"),
|
.id("gif"),
|
||||||
|
|
|
@ -61,7 +61,7 @@ impl RenderOnce for ImageContainer {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ImageShowcase {
|
struct ImageShowcase {
|
||||||
local_resource: Arc<PathBuf>,
|
local_resource: Arc<std::path::Path>,
|
||||||
remote_resource: SharedUri,
|
remote_resource: SharedUri,
|
||||||
asset_resource: SharedString,
|
asset_resource: SharedString,
|
||||||
}
|
}
|
||||||
|
@ -153,9 +153,10 @@ fn main() {
|
||||||
cx.open_window(window_options, |cx| {
|
cx.open_window(window_options, |cx| {
|
||||||
cx.new_view(|_cx| ImageShowcase {
|
cx.new_view(|_cx| ImageShowcase {
|
||||||
// Relative path to your root project path
|
// Relative path to your root project path
|
||||||
local_resource: Arc::new(
|
local_resource: PathBuf::from_str("crates/gpui/examples/image/app-icon.png")
|
||||||
PathBuf::from_str("crates/gpui/examples/image/app-icon.png").unwrap(),
|
.unwrap()
|
||||||
),
|
.into(),
|
||||||
|
|
||||||
remote_resource: "https://picsum.photos/512/512".into(),
|
remote_resource: "https://picsum.photos/512/512".into(),
|
||||||
|
|
||||||
asset_resource: "image/color.svg".into(),
|
asset_resource: "image/color.svg".into(),
|
||||||
|
|
214
crates/gpui/examples/image_loading.rs
Normal file
214
crates/gpui/examples/image_loading.rs
Normal 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();
|
||||||
|
});
|
||||||
|
}
|
|
@ -747,7 +747,7 @@ impl AppContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the SVG renderer GPUI uses
|
/// Returns the SVG renderer GPUI uses
|
||||||
pub(crate) fn svg_renderer(&self) -> SvgRenderer {
|
pub fn svg_renderer(&self) -> SvgRenderer {
|
||||||
self.svg_renderer.clone()
|
self.svg_renderer.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1369,7 +1369,7 @@ impl AppContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove an asset from GPUI's cache
|
/// 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));
|
let asset_id = (TypeId::of::<A>(), hash(source));
|
||||||
self.loading_assets.remove(&asset_id);
|
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
|
/// 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
|
/// time, and the results of this call will be cached
|
||||||
///
|
pub fn fetch_asset<A: Asset>(&mut self, source: &A::Source) -> (Shared<Task<A::Output>>, bool) {
|
||||||
/// 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) {
|
|
||||||
let asset_id = (TypeId::of::<A>(), hash(source));
|
let asset_id = (TypeId::of::<A>(), hash(source));
|
||||||
let mut is_first = false;
|
let mut is_first = false;
|
||||||
let task = self
|
let task = self
|
||||||
|
|
|
@ -1,30 +1,43 @@
|
||||||
use crate::{AppContext, SharedString, SharedUri};
|
use crate::{AppContext, SharedString, SharedUri};
|
||||||
use futures::Future;
|
use futures::Future;
|
||||||
|
|
||||||
|
use std::fmt::Debug;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::path::PathBuf;
|
use std::marker::PhantomData;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// An enum representing
|
||||||
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
|
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
|
||||||
pub(crate) enum UriOrPath {
|
pub enum Resource {
|
||||||
|
/// This resource is at a given URI
|
||||||
Uri(SharedUri),
|
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),
|
Embedded(SharedString),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<SharedUri> for UriOrPath {
|
impl From<SharedUri> for Resource {
|
||||||
fn from(value: SharedUri) -> Self {
|
fn from(value: SharedUri) -> Self {
|
||||||
Self::Uri(value)
|
Self::Uri(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Arc<PathBuf>> for UriOrPath {
|
impl From<PathBuf> for Resource {
|
||||||
fn from(value: Arc<PathBuf>) -> Self {
|
fn from(value: PathBuf) -> Self {
|
||||||
|
Self::Path(value.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Arc<Path>> for Resource {
|
||||||
|
fn from(value: Arc<Path>) -> Self {
|
||||||
Self::Path(value)
|
Self::Path(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A trait for asynchronous asset loading.
|
/// A trait for asynchronous asset loading.
|
||||||
pub trait Asset {
|
pub trait Asset: 'static {
|
||||||
/// The source of the asset.
|
/// The source of the asset.
|
||||||
type Source: Clone + Hash + Send;
|
type Source: Clone + Hash + Send;
|
||||||
|
|
||||||
|
@ -38,6 +51,31 @@ pub trait Asset {
|
||||||
) -> impl Future<Output = Self::Output> + Send + 'static;
|
) -> 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
|
/// Use a quick, non-cryptographically secure hash function to get an identifier from data
|
||||||
pub fn hash<T: Hash>(data: &T) -> u64 {
|
pub fn hash<T: Hash>(data: &T) -> u64 {
|
||||||
let mut hasher = collections::FxHasher::default();
|
let mut hasher = collections::FxHasher::default();
|
||||||
|
|
|
@ -2383,7 +2383,7 @@ where
|
||||||
|
|
||||||
/// A wrapper around an element that can store state, produced after assigning an ElementId.
|
/// A wrapper around an element that can store state, produced after assigning an ElementId.
|
||||||
pub struct Stateful<E> {
|
pub struct Stateful<E> {
|
||||||
element: E,
|
pub(crate) element: E,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E> Styled for Stateful<E>
|
impl<E> Styled for Stateful<E>
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
px, AbsoluteLength, AppContext, Asset, Bounds, DefiniteLength, Element, ElementId,
|
px, AbsoluteLength, AnyElement, AppContext, Asset, AssetLogger, Bounds, DefiniteLength,
|
||||||
GlobalElementId, Hitbox, Image, InteractiveElement, Interactivity, IntoElement, LayoutId,
|
Element, ElementId, GlobalElementId, Hitbox, Image, InteractiveElement, Interactivity,
|
||||||
Length, ObjectFit, Pixels, RenderImage, SharedString, SharedUri, StyleRefinement, Styled,
|
IntoElement, LayoutId, Length, ObjectFit, Pixels, RenderImage, Resource, SharedString,
|
||||||
SvgSize, UriOrPath, WindowContext,
|
SharedUri, StyleRefinement, Styled, SvgSize, Task, WindowContext,
|
||||||
};
|
};
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
|
||||||
use futures::{AsyncReadExt, Future};
|
use futures::{AsyncReadExt, Future};
|
||||||
use image::{
|
use image::{
|
||||||
codecs::gif::GifDecoder, AnimationDecoder, Frame, ImageBuffer, ImageError, ImageFormat,
|
codecs::gif::GifDecoder, AnimationDecoder, Frame, ImageBuffer, ImageError, ImageFormat,
|
||||||
|
@ -11,45 +13,56 @@ use image::{
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::{
|
use std::{
|
||||||
fs,
|
fs,
|
||||||
io::Cursor,
|
io::{self, Cursor},
|
||||||
path::PathBuf,
|
ops::{Deref, DerefMut},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
str::FromStr,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use util::ResultExt;
|
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.
|
/// A source of image content.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone)]
|
||||||
pub enum ImageSource {
|
pub enum ImageSource {
|
||||||
/// Image content will be loaded from provided URI at render time.
|
/// The image content will be loaded from some resource location
|
||||||
Uri(SharedUri),
|
Resource(Resource),
|
||||||
/// Image content will be loaded from the provided file at render time.
|
|
||||||
File(Arc<PathBuf>),
|
|
||||||
/// Cached image data
|
/// Cached image data
|
||||||
Render(Arc<RenderImage>),
|
Render(Arc<RenderImage>),
|
||||||
/// Cached image data
|
/// Cached image data
|
||||||
Image(Arc<Image>),
|
Image(Arc<Image>),
|
||||||
/// Image content will be loaded from Asset at render time.
|
/// A custom loading function to use
|
||||||
Embedded(SharedString),
|
Custom(Arc<dyn Fn(&mut WindowContext) -> Option<Result<Arc<RenderImage>, ImageCacheError>>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_uri(uri: &str) -> bool {
|
fn is_uri(uri: &str) -> bool {
|
||||||
uri.contains("://")
|
http_client::Uri::from_str(uri).is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<SharedUri> for ImageSource {
|
impl From<SharedUri> for ImageSource {
|
||||||
fn from(value: SharedUri) -> Self {
|
fn from(value: SharedUri) -> Self {
|
||||||
Self::Uri(value)
|
Self::Resource(Resource::Uri(value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&'static str> for ImageSource {
|
impl<'a> From<&'a str> for ImageSource {
|
||||||
fn from(s: &'static str) -> Self {
|
fn from(s: &'a str) -> Self {
|
||||||
if is_uri(s) {
|
if is_uri(s) {
|
||||||
Self::Uri(s.into())
|
Self::Resource(Resource::Uri(s.to_string().into()))
|
||||||
} else {
|
} 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 {
|
impl From<String> for ImageSource {
|
||||||
fn from(s: String) -> Self {
|
fn from(s: String) -> Self {
|
||||||
if is_uri(&s) {
|
if is_uri(&s) {
|
||||||
Self::Uri(s.into())
|
Self::Resource(Resource::Uri(s.into()))
|
||||||
} else {
|
} else {
|
||||||
Self::Embedded(s.into())
|
Self::Resource(Resource::Embedded(s.into()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<SharedString> for ImageSource {
|
impl From<SharedString> for ImageSource {
|
||||||
fn from(s: SharedString) -> Self {
|
fn from(s: SharedString) -> Self {
|
||||||
if is_uri(&s) {
|
s.as_ref().into()
|
||||||
Self::Uri(s.into())
|
|
||||||
} else {
|
|
||||||
Self::Embedded(s)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Arc<PathBuf>> for ImageSource {
|
impl From<&Path> for ImageSource {
|
||||||
fn from(value: Arc<PathBuf>) -> Self {
|
fn from(value: &Path) -> Self {
|
||||||
Self::File(value)
|
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 {
|
impl From<PathBuf> for ImageSource {
|
||||||
fn from(value: PathBuf) -> Self {
|
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.
|
/// An image element.
|
||||||
pub struct Img {
|
pub struct Img {
|
||||||
interactivity: Interactivity,
|
interactivity: Interactivity,
|
||||||
source: ImageSource,
|
source: ImageSource,
|
||||||
grayscale: bool,
|
style: ImageStyle,
|
||||||
object_fit: ObjectFit,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new image element.
|
/// Create a new image element.
|
||||||
|
@ -111,8 +194,7 @@ pub fn img(source: impl Into<ImageSource>) -> Img {
|
||||||
Img {
|
Img {
|
||||||
interactivity: Interactivity::default(),
|
interactivity: Interactivity::default(),
|
||||||
source: source.into(),
|
source: source.into(),
|
||||||
grayscale: false,
|
style: ImageStyle::default(),
|
||||||
object_fit: ObjectFit::Contain,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,16 +207,19 @@ impl Img {
|
||||||
"hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg",
|
"hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the image to be displayed in grayscale.
|
impl Deref for Stateful<Img> {
|
||||||
pub fn grayscale(mut self, grayscale: bool) -> Self {
|
type Target = Img;
|
||||||
self.grayscale = grayscale;
|
|
||||||
self
|
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;
|
impl DerefMut for Stateful<Img> {
|
||||||
self
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.element
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,10 +227,17 @@ impl Img {
|
||||||
struct ImgState {
|
struct ImgState {
|
||||||
frame_index: usize,
|
frame_index: usize,
|
||||||
last_frame_time: Option<Instant>,
|
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 {
|
impl Element for Img {
|
||||||
type RequestLayoutState = usize;
|
type RequestLayoutState = ImgLayoutState;
|
||||||
type PrepaintState = Option<Hitbox>;
|
type PrepaintState = Option<Hitbox>;
|
||||||
|
|
||||||
fn id(&self) -> Option<ElementId> {
|
fn id(&self) -> Option<ElementId> {
|
||||||
|
@ -157,11 +249,17 @@ impl Element for Img {
|
||||||
global_id: Option<&GlobalElementId>,
|
global_id: Option<&GlobalElementId>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> (LayoutId, Self::RequestLayoutState) {
|
) -> (LayoutId, Self::RequestLayoutState) {
|
||||||
|
let mut layout_state = ImgLayoutState {
|
||||||
|
frame_index: 0,
|
||||||
|
replacement: None,
|
||||||
|
};
|
||||||
|
|
||||||
cx.with_optional_element_state(global_id, |state, cx| {
|
cx.with_optional_element_state(global_id, |state, cx| {
|
||||||
let mut state = state.map(|state| {
|
let mut state = state.map(|state| {
|
||||||
state.unwrap_or(ImgState {
|
state.unwrap_or(ImgState {
|
||||||
frame_index: 0,
|
frame_index: 0,
|
||||||
last_frame_time: None,
|
last_frame_time: None,
|
||||||
|
started_loading: None,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -170,64 +268,105 @@ impl Element for Img {
|
||||||
let layout_id = self
|
let layout_id = self
|
||||||
.interactivity
|
.interactivity
|
||||||
.request_layout(global_id, cx, |mut style, cx| {
|
.request_layout(global_id, cx, |mut style, cx| {
|
||||||
if let Some(data) = self.source.use_data(cx) {
|
let mut replacement_id = None;
|
||||||
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 {
|
match self.source.use_data(cx) {
|
||||||
state.frame_index = (state.frame_index + 1) % frame_count;
|
Some(Ok(data)) => {
|
||||||
state.last_frame_time =
|
if let Some(state) = &mut state {
|
||||||
Some(current_time - (elapsed - frame_duration));
|
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 {
|
} 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,
|
&mut self,
|
||||||
global_id: Option<&GlobalElementId>,
|
global_id: Option<&GlobalElementId>,
|
||||||
bounds: Bounds<Pixels>,
|
bounds: Bounds<Pixels>,
|
||||||
_request_layout: &mut Self::RequestLayoutState,
|
request_layout: &mut Self::RequestLayoutState,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Option<Hitbox> {
|
) -> Self::PrepaintState {
|
||||||
self.interactivity
|
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(
|
fn paint(
|
||||||
&mut self,
|
&mut self,
|
||||||
global_id: Option<&GlobalElementId>,
|
global_id: Option<&GlobalElementId>,
|
||||||
bounds: Bounds<Pixels>,
|
bounds: Bounds<Pixels>,
|
||||||
frame_index: &mut Self::RequestLayoutState,
|
layout_state: &mut Self::RequestLayoutState,
|
||||||
hitbox: &mut Self::PrepaintState,
|
hitbox: &mut Self::PrepaintState,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) {
|
) {
|
||||||
|
@ -255,29 +400,26 @@ impl Element for Img {
|
||||||
.paint(global_id, bounds, hitbox.as_ref(), cx, |style, cx| {
|
.paint(global_id, bounds, hitbox.as_ref(), cx, |style, cx| {
|
||||||
let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
|
let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
|
||||||
|
|
||||||
if let Some(data) = source.use_data(cx) {
|
if let Some(Ok(data)) = source.use_data(cx) {
|
||||||
let new_bounds = self.object_fit.get_bounds(bounds, data.size(*frame_index));
|
let new_bounds = self
|
||||||
|
.style
|
||||||
|
.object_fit
|
||||||
|
.get_bounds(bounds, data.size(layout_state.frame_index));
|
||||||
cx.paint_image(
|
cx.paint_image(
|
||||||
new_bounds,
|
new_bounds,
|
||||||
corner_radii,
|
corner_radii,
|
||||||
data.clone(),
|
data.clone(),
|
||||||
*frame_index,
|
layout_state.frame_index,
|
||||||
self.grayscale,
|
self.style.grayscale,
|
||||||
)
|
)
|
||||||
.log_err();
|
.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 {
|
impl Styled for Img {
|
||||||
fn style(&mut self) -> &mut StyleRefinement {
|
fn style(&mut self) -> &mut StyleRefinement {
|
||||||
&mut self.interactivity.base_style
|
&mut self.interactivity.base_style
|
||||||
|
@ -290,41 +432,28 @@ impl InteractiveElement for Img {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImageSource {
|
impl IntoElement for Img {
|
||||||
pub(crate) fn use_data(&self, cx: &mut WindowContext) -> Option<Arc<RenderImage>> {
|
type Element = Self;
|
||||||
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()
|
fn into_element(self) -> Self::Element {
|
||||||
}
|
self
|
||||||
|
|
||||||
ImageSource::Render(data) => Some(data.to_owned()),
|
|
||||||
ImageSource::Image(data) => cx.use_asset::<ImageDecoder>(data)?.log_err(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Fetch the data associated with this source, using GPUI's asset caching
|
impl FocusableElement for Img {}
|
||||||
pub async fn data(&self, cx: &mut AppContext) -> Option<Arc<RenderImage>> {
|
|
||||||
|
impl StatefulInteractiveElement for Img {}
|
||||||
|
|
||||||
|
impl ImageSource {
|
||||||
|
pub(crate) fn use_data(
|
||||||
|
&self,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
|
||||||
match self {
|
match self {
|
||||||
ImageSource::Uri(_) | ImageSource::Embedded(_) | ImageSource::File(_) => {
|
ImageSource::Resource(resource) => cx.use_asset::<ImgResourceLoader>(&resource),
|
||||||
let uri_or_path: UriOrPath = match self {
|
ImageSource::Custom(loading_fn) => loading_fn(cx),
|
||||||
ImageSource::Uri(uri) => uri.clone().into(),
|
ImageSource::Render(data) => Some(Ok(data.to_owned())),
|
||||||
ImageSource::File(path) => path.clone().into(),
|
ImageSource::Image(data) => cx.use_asset::<AssetLogger<ImageDecoder>>(data),
|
||||||
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(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -334,22 +463,23 @@ enum ImageDecoder {}
|
||||||
|
|
||||||
impl Asset for ImageDecoder {
|
impl Asset for ImageDecoder {
|
||||||
type Source = Arc<Image>;
|
type Source = Arc<Image>;
|
||||||
type Output = Result<Arc<RenderImage>, Arc<anyhow::Error>>;
|
type Output = Result<Arc<RenderImage>, ImageCacheError>;
|
||||||
|
|
||||||
fn load(
|
fn load(
|
||||||
source: Self::Source,
|
source: Self::Source,
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
) -> impl Future<Output = Self::Output> + Send + 'static {
|
) -> impl Future<Output = Self::Output> + Send + 'static {
|
||||||
let result = source.to_image_data(cx).map_err(Arc::new);
|
let renderer = cx.svg_renderer();
|
||||||
async { result }
|
async move { source.to_image_data(renderer).map_err(Into::into) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An image loader for the GPUI asset system
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
enum ImageAsset {}
|
pub enum ImageAssetLoader {}
|
||||||
|
|
||||||
impl Asset for ImageAsset {
|
impl Asset for ImageAssetLoader {
|
||||||
type Source = UriOrPath;
|
type Source = Resource;
|
||||||
type Output = Result<Arc<RenderImage>, ImageCacheError>;
|
type Output = Result<Arc<RenderImage>, ImageCacheError>;
|
||||||
|
|
||||||
fn load(
|
fn load(
|
||||||
|
@ -363,12 +493,12 @@ impl Asset for ImageAsset {
|
||||||
let asset_source = cx.asset_source().clone();
|
let asset_source = cx.asset_source().clone();
|
||||||
async move {
|
async move {
|
||||||
let bytes = match source.clone() {
|
let bytes = match source.clone() {
|
||||||
UriOrPath::Path(uri) => fs::read(uri.as_ref())?,
|
Resource::Path(uri) => fs::read(uri.as_ref())?,
|
||||||
UriOrPath::Uri(uri) => {
|
Resource::Uri(uri) => {
|
||||||
let mut response = client
|
let mut response = client
|
||||||
.get(uri.as_ref(), ().into(), true)
|
.get(uri.as_ref(), ().into(), true)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| ImageCacheError::Client(Arc::new(e)))?;
|
.map_err(|e| anyhow!(e))?;
|
||||||
let mut body = Vec::new();
|
let mut body = Vec::new();
|
||||||
response.body_mut().read_to_end(&mut body).await?;
|
response.body_mut().read_to_end(&mut body).await?;
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
|
@ -383,13 +513,13 @@ impl Asset for ImageAsset {
|
||||||
}
|
}
|
||||||
body
|
body
|
||||||
}
|
}
|
||||||
UriOrPath::Embedded(path) => {
|
Resource::Embedded(path) => {
|
||||||
let data = asset_source.load(&path).ok().flatten();
|
let data = asset_source.load(&path).ok().flatten();
|
||||||
if let Some(data) = data {
|
if let Some(data) = data {
|
||||||
data.to_vec()
|
data.to_vec()
|
||||||
} else {
|
} else {
|
||||||
return Err(ImageCacheError::Asset(
|
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.
|
/// An error that can occur when interacting with the image cache.
|
||||||
#[derive(Debug, Error, Clone)]
|
#[derive(Debug, Error, Clone)]
|
||||||
pub enum ImageCacheError {
|
pub enum ImageCacheError {
|
||||||
/// An error that occurred while fetching an image from a remote source.
|
/// Some other kind of error occurred
|
||||||
#[error("http error: {0}")]
|
#[error("error: {0}")]
|
||||||
Client(#[from] Arc<anyhow::Error>),
|
Other(#[from] Arc<anyhow::Error>),
|
||||||
/// An error that occurred while reading the image from disk.
|
/// An error that occurred while reading the image from disk.
|
||||||
#[error("IO error: {0}")]
|
#[error("IO error: {0}")]
|
||||||
Io(Arc<std::io::Error>),
|
Io(Arc<std::io::Error>),
|
||||||
|
@ -477,20 +607,26 @@ pub enum ImageCacheError {
|
||||||
Usvg(Arc<usvg::Error>),
|
Usvg(Arc<usvg::Error>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<std::io::Error> for ImageCacheError {
|
impl From<anyhow::Error> for ImageCacheError {
|
||||||
fn from(error: std::io::Error) -> Self {
|
fn from(value: anyhow::Error) -> Self {
|
||||||
Self::Io(Arc::new(error))
|
Self::Other(Arc::new(value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ImageError> for ImageCacheError {
|
impl From<io::Error> for ImageCacheError {
|
||||||
fn from(error: ImageError) -> Self {
|
fn from(value: io::Error) -> Self {
|
||||||
Self::Image(Arc::new(error))
|
Self::Io(Arc::new(value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<usvg::Error> for ImageCacheError {
|
impl From<usvg::Error> for ImageCacheError {
|
||||||
fn from(error: usvg::Error) -> Self {
|
fn from(value: usvg::Error) -> Self {
|
||||||
Self::Usvg(Arc::new(error))
|
Self::Usvg(Arc::new(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<image::ImageError> for ImageCacheError {
|
||||||
|
fn from(value: image::ImageError) -> Self {
|
||||||
|
Self::Image(Arc::new(value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,11 +27,11 @@ mod test;
|
||||||
mod windows;
|
mod windows;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
point, Action, AnyWindowHandle, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds,
|
point, Action, AnyWindowHandle, AsyncWindowContext, BackgroundExecutor, Bounds, DevicePixels,
|
||||||
DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun, ForegroundExecutor,
|
DispatchEventResult, Font, FontId, FontMetrics, FontRun, ForegroundExecutor, GPUSpecs, GlyphId,
|
||||||
GPUSpecs, GlyphId, ImageSource, Keymap, LineLayout, Pixels, PlatformInput, Point,
|
ImageSource, Keymap, LineLayout, Pixels, PlatformInput, Point, RenderGlyphParams, RenderImage,
|
||||||
RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, ScaledPixels, Scene,
|
RenderImageParams, RenderSvgParams, ScaledPixels, Scene, SharedString, Size, SvgRenderer,
|
||||||
SharedString, Size, SvgSize, Task, TaskLabel, WindowContext, DEFAULT_WINDOW_SIZE,
|
SvgSize, Task, TaskLabel, WindowContext, DEFAULT_WINDOW_SIZE,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use async_task::Runnable;
|
use async_task::Runnable;
|
||||||
|
@ -1290,11 +1290,13 @@ impl Image {
|
||||||
|
|
||||||
/// Use the GPUI `use_asset` API to make this image renderable
|
/// 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>> {
|
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.
|
/// 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(
|
fn frames_for_image(
|
||||||
bytes: &[u8],
|
bytes: &[u8],
|
||||||
format: image::ImageFormat,
|
format: image::ImageFormat,
|
||||||
|
@ -1331,10 +1333,7 @@ impl Image {
|
||||||
ImageFormat::Bmp => frames_for_image(&self.bytes, image::ImageFormat::Bmp)?,
|
ImageFormat::Bmp => frames_for_image(&self.bytes, image::ImageFormat::Bmp)?,
|
||||||
ImageFormat::Tiff => frames_for_image(&self.bytes, image::ImageFormat::Tiff)?,
|
ImageFormat::Tiff => frames_for_image(&self.bytes, image::ImageFormat::Tiff)?,
|
||||||
ImageFormat::Svg => {
|
ImageFormat::Svg => {
|
||||||
// TODO: Fix this
|
let pixmap = svg_renderer.render_pixmap(&self.bytes, SvgSize::ScaleFactor(1.0))?;
|
||||||
let pixmap = cx
|
|
||||||
.svg_renderer()
|
|
||||||
.render_pixmap(&self.bytes, SvgSize::ScaleFactor(1.0))?;
|
|
||||||
|
|
||||||
let buffer =
|
let buffer =
|
||||||
image::ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take())
|
image::ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take())
|
||||||
|
|
|
@ -5,5 +5,5 @@
|
||||||
pub use crate::{
|
pub use crate::{
|
||||||
util::FluentBuilder, BorrowAppContext, BorrowWindow, Context, Element, FocusableElement,
|
util::FluentBuilder, BorrowAppContext, BorrowWindow, Context, Element, FocusableElement,
|
||||||
InteractiveElement, IntoElement, ParentElement, Refineable, Render, RenderOnce,
|
InteractiveElement, IntoElement, ParentElement, Refineable, Render, RenderOnce,
|
||||||
StatefulInteractiveElement, Styled, VisualContext,
|
StatefulInteractiveElement, Styled, StyledImage, VisualContext,
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,7 +10,7 @@ pub(crate) struct RenderSvgParams {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct SvgRenderer {
|
pub struct SvgRenderer {
|
||||||
asset_source: Arc<dyn AssetSource>,
|
asset_source: Arc<dyn AssetSource>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ impl SvgRenderer {
|
||||||
Self { asset_source }
|
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() {
|
if params.size.is_zero() {
|
||||||
return Err(anyhow!("can't render at a zero size"));
|
return Err(anyhow!("can't render at a zero size"));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
/// 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.
|
/// 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
|
for view_id in self
|
||||||
.window
|
.window
|
||||||
.rendered_frame
|
.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.
|
/// 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) {
|
pub fn request_animation_frame(&self) {
|
||||||
let parent_id = self.parent_view_id();
|
let parent_id = self.parent_view_id();
|
||||||
self.on_next_frame(move |cx| {
|
self.on_next_frame(move |cx| cx.notify(parent_id));
|
||||||
if let Some(parent_id) = parent_id {
|
|
||||||
cx.notify(parent_id)
|
|
||||||
} else {
|
|
||||||
cx.refresh()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawn the future returned by the given closure on the application thread pool.
|
/// 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
|
/// Note that the multiple calls to this method will only result in one `Asset::load` call at a
|
||||||
/// time.
|
/// time.
|
||||||
///
|
pub fn use_asset<A: Asset>(&mut self, source: &A::Source) -> Option<A::Output> {
|
||||||
/// 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> {
|
|
||||||
let (task, is_first) = self.fetch_asset::<A>(source);
|
let (task, is_first) = self.fetch_asset::<A>(source);
|
||||||
task.clone().now_or_never().or_else(|| {
|
task.clone().now_or_never().or_else(|| {
|
||||||
if is_first {
|
if is_first {
|
||||||
|
@ -1994,13 +1992,7 @@ impl<'a> WindowContext<'a> {
|
||||||
|mut cx| async move {
|
|mut cx| async move {
|
||||||
task.await;
|
task.await;
|
||||||
|
|
||||||
cx.on_next_frame(move |cx| {
|
cx.on_next_frame(move |cx| cx.notify(parent_id));
|
||||||
if let Some(parent_id) = parent_id {
|
|
||||||
cx.notify(parent_id)
|
|
||||||
} else {
|
|
||||||
cx.refresh()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.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
|
/// 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`
|
/// 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.
|
/// 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>(
|
pub fn with_optional_element_state<S, R>(
|
||||||
&mut self,
|
&mut self,
|
||||||
global_id: Option<&GlobalElementId>,
|
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.
|
/// 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.
|
/// 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) {
|
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.
|
/// Register a callback to be invoked when the window is resized.
|
||||||
|
|
|
@ -370,7 +370,7 @@ impl Element for Scrollbar {
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(id) = state.parent_id {
|
if let Some(id) = state.parent_id {
|
||||||
cx.notify(id);
|
cx.notify(Some(id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -382,7 +382,7 @@ impl Element for Scrollbar {
|
||||||
if phase.bubble() {
|
if phase.bubble() {
|
||||||
state.drag.take();
|
state.drag.take();
|
||||||
if let Some(id) = state.parent_id {
|
if let Some(id) = state.parent_id {
|
||||||
cx.notify(id);
|
cx.notify(Some(id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -5896,7 +5896,7 @@ pub fn client_side_decorations(element: impl IntoElement, cx: &mut WindowContext
|
||||||
let edge = cx.try_global::<GlobalResizeEdge>();
|
let edge = cx.try_global::<GlobalResizeEdge>();
|
||||||
if new_edge != edge.map(|edge| edge.0) {
|
if new_edge != edge.map(|edge| edge.0) {
|
||||||
cx.window_handle()
|
cx.window_handle()
|
||||||
.update(cx, |workspace, cx| cx.notify(workspace.entity_id()))
|
.update(cx, |workspace, cx| cx.notify(Some(workspace.entity_id())))
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue