Add png image loading to gpui

Mikayla Maki and Nathan created

add zed logo into welcome experience

Co-authored-by: Nathan <nathan@zed.dev>

Change summary

assets/images/zed-logo-90x90.png                    |  0 
crates/collab_ui/src/collab_titlebar_item.rs        |  2 
crates/collab_ui/src/contact_finder.rs              |  2 
crates/collab_ui/src/contact_list.rs                |  6 
crates/collab_ui/src/incoming_call_notification.rs  |  2 
crates/collab_ui/src/notifications.rs               |  2 
crates/collab_ui/src/project_shared_notification.rs |  2 
crates/gpui/src/assets.rs                           | 21 +++++
crates/gpui/src/elements/image.rs                   | 56 ++++++++++----
crates/project_panel/src/project_panel.rs           |  2 
crates/welcome/src/welcome.rs                       | 14 +--
11 files changed, 76 insertions(+), 33 deletions(-)

Detailed changes

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -823,7 +823,7 @@ impl CollabTitlebarItem {
         avatar_style: AvatarStyle,
         background_color: Color,
     ) -> ElementBox {
-        Image::new(avatar)
+        Image::from_data(avatar)
             .with_style(avatar_style.image)
             .aligned()
             .contained()

crates/collab_ui/src/contact_finder.rs 🔗

@@ -128,7 +128,7 @@ impl PickerDelegate for ContactFinder {
             .style_for(mouse_state, selected);
         Flex::row()
             .with_children(user.avatar.clone().map(|avatar| {
-                Image::new(avatar)
+                Image::from_data(avatar)
                     .with_style(theme.contact_finder.contact_avatar)
                     .aligned()
                     .left()

crates/collab_ui/src/contact_list.rs 🔗

@@ -726,7 +726,7 @@ impl ContactList {
     ) -> ElementBox {
         Flex::row()
             .with_children(user.avatar.clone().map(|avatar| {
-                Image::new(avatar)
+                Image::from_data(avatar)
                     .with_style(theme.contact_avatar)
                     .aligned()
                     .left()
@@ -1080,7 +1080,7 @@ impl ContactList {
                         };
                         Stack::new()
                             .with_child(
-                                Image::new(avatar)
+                                Image::from_data(avatar)
                                     .with_style(theme.contact_avatar)
                                     .aligned()
                                     .left()
@@ -1173,7 +1173,7 @@ impl ContactList {
 
         let mut row = Flex::row()
             .with_children(user.avatar.clone().map(|avatar| {
-                Image::new(avatar)
+                Image::from_data(avatar)
                     .with_style(theme.contact_avatar)
                     .aligned()
                     .left()

crates/collab_ui/src/incoming_call_notification.rs 🔗

@@ -108,7 +108,7 @@ impl IncomingCallNotification {
             .unwrap_or(&default_project);
         Flex::row()
             .with_children(self.call.calling_user.avatar.clone().map(|avatar| {
-                Image::new(avatar)
+                Image::from_data(avatar)
                     .with_style(theme.caller_avatar)
                     .aligned()
                     .boxed()

crates/collab_ui/src/notifications.rs 🔗

@@ -24,7 +24,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
         .with_child(
             Flex::row()
                 .with_children(user.avatar.clone().map(|avatar| {
-                    Image::new(avatar)
+                    Image::from_data(avatar)
                         .with_style(theme.header_avatar)
                         .aligned()
                         .constrained()

crates/collab_ui/src/project_shared_notification.rs 🔗

@@ -108,7 +108,7 @@ impl ProjectSharedNotification {
         let theme = &cx.global::<Settings>().theme.project_shared_notification;
         Flex::row()
             .with_children(self.owner.avatar.clone().map(|avatar| {
-                Image::new(avatar)
+                Image::from_data(avatar)
                     .with_style(theme.owner_avatar)
                     .aligned()
                     .boxed()

crates/gpui/src/assets.rs 🔗

@@ -1,5 +1,8 @@
 use anyhow::{anyhow, Result};
-use std::{borrow::Cow, cell::RefCell, collections::HashMap};
+use image::ImageFormat;
+use std::{borrow::Cow, cell::RefCell, collections::HashMap, sync::Arc};
+
+use crate::ImageData;
 
 pub trait AssetSource: 'static + Send + Sync {
     fn load(&self, path: &str) -> Result<Cow<[u8]>>;
@@ -22,6 +25,7 @@ impl AssetSource for () {
 pub struct AssetCache {
     source: Box<dyn AssetSource>,
     svgs: RefCell<HashMap<String, usvg::Tree>>,
+    pngs: RefCell<HashMap<String, Arc<ImageData>>>,
 }
 
 impl AssetCache {
@@ -29,6 +33,7 @@ impl AssetCache {
         Self {
             source: Box::new(source),
             svgs: RefCell::new(HashMap::new()),
+            pngs: RefCell::new(HashMap::new()),
         }
     }
 
@@ -43,4 +48,18 @@ impl AssetCache {
             Ok(svg)
         }
     }
+
+    pub fn png(&self, path: &str) -> Result<Arc<ImageData>> {
+        let mut pngs = self.pngs.borrow_mut();
+        if let Some(png) = pngs.get(path) {
+            Ok(png.clone())
+        } else {
+            let bytes = self.source.load(path)?;
+            let image = ImageData::new(
+                image::load_from_memory_with_format(&bytes, ImageFormat::Png)?.into_bgra8(),
+            );
+            pngs.insert(path.to_string(), image.clone());
+            Ok(image)
+        }
+    }
 }

crates/gpui/src/elements/image.rs 🔗

@@ -11,8 +11,13 @@ use crate::{
 use serde::Deserialize;
 use std::{ops::Range, sync::Arc};
 
+enum ImageSource {
+    Path(&'static str),
+    Data(Arc<ImageData>),
+}
+
 pub struct Image {
-    data: Arc<ImageData>,
+    source: ImageSource,
     style: ImageStyle,
 }
 
@@ -31,9 +36,16 @@ pub struct ImageStyle {
 }
 
 impl Image {
-    pub fn new(data: Arc<ImageData>) -> Self {
+    pub fn new(asset_path: &'static str) -> Self {
+        Self {
+            source: ImageSource::Path(asset_path),
+            style: Default::default(),
+        }
+    }
+
+    pub fn from_data(data: Arc<ImageData>) -> Self {
         Self {
-            data,
+            source: ImageSource::Data(data),
             style: Default::default(),
         }
     }
@@ -45,39 +57,53 @@ impl Image {
 }
 
 impl Element for Image {
-    type LayoutState = ();
+    type LayoutState = Option<Arc<ImageData>>;
     type PaintState = ();
 
     fn layout(
         &mut self,
         constraint: SizeConstraint,
-        _: &mut LayoutContext,
+        cx: &mut LayoutContext,
     ) -> (Vector2F, Self::LayoutState) {
+        let data = match &self.source {
+            ImageSource::Path(path) => match cx.asset_cache.png(path) {
+                Ok(data) => data,
+                Err(error) => {
+                    log::error!("could not load image: {}", error);
+                    return (Vector2F::zero(), None);
+                }
+            },
+            ImageSource::Data(data) => data.clone(),
+        };
+
         let desired_size = vec2f(
             self.style.width.unwrap_or_else(|| constraint.max.x()),
             self.style.height.unwrap_or_else(|| constraint.max.y()),
         );
         let size = constrain_size_preserving_aspect_ratio(
             constraint.constrain(desired_size),
-            self.data.size().to_f32(),
+            data.size().to_f32(),
         );
-        (size, ())
+
+        (size, Some(data))
     }
 
     fn paint(
         &mut self,
         bounds: RectF,
         _: RectF,
-        _: &mut Self::LayoutState,
+        layout: &mut Self::LayoutState,
         cx: &mut PaintContext,
     ) -> Self::PaintState {
-        cx.scene.push_image(scene::Image {
-            bounds,
-            border: self.style.border,
-            corner_radius: self.style.corner_radius,
-            grayscale: self.style.grayscale,
-            data: self.data.clone(),
-        });
+        if let Some(data) = layout {
+            cx.scene.push_image(scene::Image {
+                bounds,
+                border: self.style.border,
+                corner_radius: self.style.corner_radius,
+                grayscale: self.style.grayscale,
+                data: data.clone(),
+            });
+        }
     }
 
     fn rect_for_text_range(

crates/project_panel/src/project_panel.rs 🔗

@@ -1325,7 +1325,7 @@ impl View for ProjectPanel {
                                 Canvas::new(|bounds, _visible_bounds, cx| {
                                     cx.scene.push_quad(gpui::Quad {
                                         bounds,
-                                        background: Some(Color::red()),
+                                        background: Some(Color::transparent_black()),
                                         ..Default::default()
                                     })
                                 })

crates/welcome/src/welcome.rs 🔗

@@ -1,14 +1,13 @@
 use std::borrow::Cow;
 
 use gpui::{
-    color::Color,
-    elements::{Canvas, Empty, Flex, Label, MouseEventHandler, ParentElement, Stack, Svg},
+    elements::{Canvas, Empty, Flex, Image, Label, MouseEventHandler, ParentElement, Stack},
     geometry::rect::RectF,
     Action, Element, ElementBox, Entity, MouseButton, MouseRegion, MutableAppContext,
     RenderContext, Subscription, View, ViewContext,
 };
 use settings::{settings_file::SettingsFile, Settings, SettingsFileContent};
-use theme::{CheckboxStyle, ContainedText, Interactive};
+use theme::CheckboxStyle;
 use workspace::{item::Item, Welcome, Workspace};
 
 pub fn init(cx: &mut MutableAppContext) {
@@ -72,15 +71,14 @@ impl View for WelcomePage {
                     .with_children([
                         Flex::row()
                             .with_children([
-                                Svg::new("icons/terminal_16.svg")
-                                    .with_color(Color::red())
+                                Image::new("images/zed-logo-90x90.png")
                                     .constrained()
-                                    .with_width(100.)
-                                    .with_height(100.)
+                                    .with_width(90.)
+                                    .with_height(90.)
                                     .aligned()
                                     .contained()
                                     .boxed(),
-                                Label::new("Zed", theme.editor.hover_popover.prose.clone()).boxed(),
+                                // Label::new("Zed", theme.editor.hover_popover.prose.clone()).boxed(),
                             ])
                             .boxed(),
                         Label::new(