diff --git a/crates/gpui/examples/image/image.rs b/crates/gpui/examples/image/image.rs index f184c33c7f..cc8e1a686c 100644 --- a/crates/gpui/examples/image/image.rs +++ b/crates/gpui/examples/image/image.rs @@ -3,6 +3,34 @@ use std::str::FromStr; use std::sync::Arc; use gpui::*; +use std::fs; + +struct Assets { + base: PathBuf, +} + +impl AssetSource for Assets { + fn load(&self, path: &str) -> Result>> { + fs::read(self.base.join(path)) + .map(|data| Some(std::borrow::Cow::Owned(data))) + .map_err(|e| e.into()) + } + + fn list(&self, path: &str) -> Result> { + fs::read_dir(self.base.join(path)) + .map(|entries| { + entries + .filter_map(|entry| { + entry + .ok() + .and_then(|entry| entry.file_name().into_string().ok()) + .map(SharedString::from) + }) + .collect() + }) + .map_err(|e| e.into()) + } +} #[derive(IntoElement)] struct ImageContainer { @@ -27,7 +55,7 @@ impl RenderOnce for ImageContainer { .size_full() .gap_4() .child(self.text) - .child(img(self.src).w(px(512.0)).h(px(512.0))), + .child(img(self.src).w(px(256.0)).h(px(256.0))), ) } } @@ -35,6 +63,7 @@ impl RenderOnce for ImageContainer { struct ImageShowcase { local_resource: Arc, remote_resource: SharedUri, + asset_resource: SharedString, } impl Render for ImageShowcase { @@ -55,6 +84,10 @@ impl Render for ImageShowcase { "Image loaded from a remote resource", self.remote_resource.clone(), )) + .child(ImageContainer::new( + "Image loaded from an asset", + self.asset_resource.clone(), + )) } } @@ -63,37 +96,44 @@ actions!(image, [Quit]); fn main() { env_logger::init(); - App::new().run(|cx: &mut AppContext| { - cx.activate(true); - cx.on_action(|_: &Quit, cx| cx.quit()); - cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); - cx.set_menus(vec![Menu { - name: "Image".into(), - items: vec![MenuItem::action("Quit", Quit)], - }]); - - let window_options = WindowOptions { - titlebar: Some(TitlebarOptions { - title: Some(SharedString::from("Image Example")), - appears_transparent: false, - ..Default::default() - }), - - window_bounds: Some(WindowBounds::Windowed(Bounds { - size: size(px(1100.), px(600.)), - origin: Point::new(px(200.), px(200.)), - })), - - ..Default::default() - }; - - 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("examples/image/app-icon.png").unwrap()), - remote_resource: "https://picsum.photos/512/512".into(), - }) + App::new() + .with_assets(Assets { + base: PathBuf::from("crates/gpui/examples"), }) - .unwrap(); - }); + .run(|cx: &mut AppContext| { + cx.activate(true); + cx.on_action(|_: &Quit, cx| cx.quit()); + cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); + cx.set_menus(vec![Menu { + name: "Image".into(), + items: vec![MenuItem::action("Quit", Quit)], + }]); + + let window_options = WindowOptions { + titlebar: Some(TitlebarOptions { + title: Some(SharedString::from("Image Example")), + appears_transparent: false, + ..Default::default() + }), + + window_bounds: Some(WindowBounds::Windowed(Bounds { + size: size(px(1100.), px(600.)), + origin: Point::new(px(200.), px(200.)), + })), + + ..Default::default() + }; + + 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(), + ), + remote_resource: "https://picsum.photos/512/512".into(), + asset_resource: "image/app-icon.png".into(), + }) + }) + .unwrap(); + }); } diff --git a/crates/gpui/src/asset_cache.rs b/crates/gpui/src/asset_cache.rs index 070aff32f6..e8b101f560 100644 --- a/crates/gpui/src/asset_cache.rs +++ b/crates/gpui/src/asset_cache.rs @@ -1,4 +1,4 @@ -use crate::{SharedUri, WindowContext}; +use crate::{SharedString, SharedUri, WindowContext}; use collections::FxHashMap; use futures::Future; use parking_lot::Mutex; @@ -11,6 +11,7 @@ use std::{any::Any, path::PathBuf}; pub(crate) enum UriOrPath { Uri(SharedUri), Path(Arc), + Asset(SharedString), } impl From for UriOrPath { diff --git a/crates/gpui/src/elements/img.rs b/crates/gpui/src/elements/img.rs index f1d7d4ff8d..5213597a32 100644 --- a/crates/gpui/src/elements/img.rs +++ b/crates/gpui/src/elements/img.rs @@ -1,8 +1,8 @@ use crate::{ point, px, size, AbsoluteLength, Asset, Bounds, DefiniteLength, DevicePixels, Element, ElementId, GlobalElementId, Hitbox, ImageData, InteractiveElement, Interactivity, IntoElement, - LayoutId, Length, Pixels, SharedUri, Size, StyleRefinement, Styled, SvgSize, UriOrPath, - WindowContext, + LayoutId, Length, Pixels, SharedString, SharedUri, Size, StyleRefinement, Styled, SvgSize, + UriOrPath, WindowContext, }; use futures::{AsyncReadExt, Future}; use http_client; @@ -31,12 +31,18 @@ pub enum ImageSource { File(Arc), /// Cached image data Data(Arc), + /// Image content will be loaded from Asset at render time. + Asset(SharedString), // TODO: move surface definitions into mac platform module /// A CoreVideo image buffer #[cfg(target_os = "macos")] Surface(CVImageBuffer), } +fn is_uri(uri: &str) -> bool { + uri.contains("://") +} + impl From for ImageSource { fn from(value: SharedUri) -> Self { Self::Uri(value) @@ -44,14 +50,32 @@ impl From for ImageSource { } impl From<&'static str> for ImageSource { - fn from(uri: &'static str) -> Self { - Self::Uri(uri.into()) + fn from(s: &'static str) -> Self { + if is_uri(&s) { + Self::Uri(s.into()) + } else { + Self::Asset(s.into()) + } } } impl From for ImageSource { - fn from(uri: String) -> Self { - Self::Uri(uri.into()) + fn from(s: String) -> Self { + if is_uri(&s) { + Self::Uri(s.into()) + } else { + Self::Asset(s.into()) + } + } +} + +impl From for ImageSource { + fn from(s: SharedString) -> Self { + if is_uri(&s) { + Self::Uri(s.into()) + } else { + Self::Asset(s) + } } } @@ -388,10 +412,11 @@ impl InteractiveElement for Img { impl ImageSource { fn data(&self, cx: &mut WindowContext) -> Option> { match self { - ImageSource::Uri(_) | ImageSource::File(_) => { + ImageSource::Uri(_) | ImageSource::Asset(_) | ImageSource::File(_) => { let uri_or_path: UriOrPath = match self { ImageSource::Uri(uri) => uri.clone().into(), ImageSource::File(path) => path.clone().into(), + ImageSource::Asset(path) => UriOrPath::Asset(path.clone()), _ => unreachable!(), }; @@ -419,6 +444,7 @@ impl Asset for Image { let client = cx.http_client(); let scale_factor = cx.scale_factor(); let svg_renderer = cx.svg_renderer(); + let asset_source = cx.asset_source().clone(); async move { let bytes = match source.clone() { UriOrPath::Path(uri) => fs::read(uri.as_ref())?, @@ -435,6 +461,16 @@ impl Asset for Image { } body } + UriOrPath::Asset(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(), + )); + } + } }; let data = if let Ok(format) = image::guess_format(&bytes) { @@ -502,6 +538,9 @@ pub enum ImageCacheError { /// The HTTP response body. body: String, }, + /// An error that occurred while processing an asset. + #[error("asset error: {0}")] + Asset(SharedString), /// An error that occurred while processing an image. #[error("image error: {0}")] Image(Arc),