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| {
|
||||
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();
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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(),
|
||||
|
|
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
|
||||
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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue