gpui: Add `SharedUrl` type (#3975)

Marshall Bowers created

This PR adds a `SharedUrl` type to GPUI.

It's just like a `SharedString`, but for denoting that the contained
value is a URL.

Mainlined from @nathansobo's GPUI blog post:
https://github.com/zed-industries/zed/pull/3968/files#diff-7ee75937e2daf7dd53f71b17698d8bd6d46993d06928d411781b9bd739b5f231R9-R12

Release Notes:

- N/A

Change summary

crates/client/src/user.rs                                 |  4 
crates/collab_ui/src/notifications/collab_notification.rs |  6 +-
crates/gpui/src/elements/img.rs                           |  8 +-
crates/gpui/src/gpui.rs                                   |  2 
crates/gpui/src/image_cache.rs                            |  6 +-
crates/gpui/src/shared_url.rs                             | 25 +++++++++
crates/ui/src/components/stories/list_item.rs             | 18 +++---
7 files changed, 48 insertions(+), 21 deletions(-)

Detailed changes

crates/client/src/user.rs 🔗

@@ -3,7 +3,7 @@ use anyhow::{anyhow, Context, Result};
 use collections::{hash_map::Entry, HashMap, HashSet};
 use feature_flags::FeatureFlagAppExt;
 use futures::{channel::mpsc, Future, StreamExt};
-use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, SharedString, Task};
+use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, SharedUrl, Task};
 use postage::{sink::Sink, watch};
 use rpc::proto::{RequestMessage, UsersResponse};
 use std::sync::{Arc, Weak};
@@ -19,7 +19,7 @@ pub struct ParticipantIndex(pub u32);
 pub struct User {
     pub id: UserId,
     pub github_login: String,
-    pub avatar_uri: SharedString,
+    pub avatar_uri: SharedUrl,
 }
 
 #[derive(Clone, Debug, PartialEq, Eq)]

crates/collab_ui/src/notifications/collab_notification.rs 🔗

@@ -1,10 +1,10 @@
-use gpui::{img, prelude::*, AnyElement};
+use gpui::{img, prelude::*, AnyElement, SharedUrl};
 use smallvec::SmallVec;
 use ui::prelude::*;
 
 #[derive(IntoElement)]
 pub struct CollabNotification {
-    avatar_uri: SharedString,
+    avatar_uri: SharedUrl,
     accept_button: Button,
     dismiss_button: Button,
     children: SmallVec<[AnyElement; 2]>,
@@ -12,7 +12,7 @@ pub struct CollabNotification {
 
 impl CollabNotification {
     pub fn new(
-        avatar_uri: impl Into<SharedString>,
+        avatar_uri: impl Into<SharedUrl>,
         accept_button: Button,
         dismiss_button: Button,
     ) -> Self {

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

@@ -2,7 +2,7 @@ use std::sync::Arc;
 
 use crate::{
     point, size, BorrowWindow, Bounds, DevicePixels, Element, ImageData, InteractiveElement,
-    InteractiveElementState, Interactivity, IntoElement, LayoutId, Pixels, SharedString, Size,
+    InteractiveElementState, Interactivity, IntoElement, LayoutId, Pixels, SharedUrl, Size,
     StyleRefinement, Styled, WindowContext,
 };
 use futures::FutureExt;
@@ -12,13 +12,13 @@ use util::ResultExt;
 #[derive(Clone, Debug)]
 pub enum ImageSource {
     /// Image content will be loaded from provided URI at render time.
-    Uri(SharedString),
+    Uri(SharedUrl),
     Data(Arc<ImageData>),
     Surface(CVImageBuffer),
 }
 
-impl From<SharedString> for ImageSource {
-    fn from(value: SharedString) -> Self {
+impl From<SharedUrl> for ImageSource {
+    fn from(value: SharedUrl) -> Self {
         Self::Uri(value)
     }
 }

crates/gpui/src/gpui.rs 🔗

@@ -18,6 +18,7 @@ mod platform;
 pub mod prelude;
 mod scene;
 mod shared_string;
+mod shared_url;
 mod style;
 mod styled;
 mod subscription;
@@ -67,6 +68,7 @@ pub use refineable::*;
 pub use scene::*;
 use seal::Sealed;
 pub use shared_string::*;
+pub use shared_url::*;
 pub use smol::Timer;
 pub use style::*;
 pub use styled::*;

crates/gpui/src/image_cache.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{ImageData, ImageId, SharedString};
+use crate::{ImageData, ImageId, SharedUrl};
 use collections::HashMap;
 use futures::{
     future::{BoxFuture, Shared},
@@ -44,7 +44,7 @@ impl From<ImageError> for Error {
 
 pub struct ImageCache {
     client: Arc<dyn HttpClient>,
-    images: Arc<Mutex<HashMap<SharedString, FetchImageFuture>>>,
+    images: Arc<Mutex<HashMap<SharedUrl, FetchImageFuture>>>,
 }
 
 type FetchImageFuture = Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>>;
@@ -59,7 +59,7 @@ impl ImageCache {
 
     pub fn get(
         &self,
-        uri: impl Into<SharedString>,
+        uri: impl Into<SharedUrl>,
     ) -> Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>> {
         let uri = uri.into();
         let mut images = self.images.lock();

crates/gpui/src/shared_url.rs 🔗

@@ -0,0 +1,25 @@
+use derive_more::{Deref, DerefMut};
+
+use crate::SharedString;
+
+/// A [`SharedString`] containing a URL.
+#[derive(Deref, DerefMut, Default, PartialEq, Eq, Hash, Clone)]
+pub struct SharedUrl(SharedString);
+
+impl std::fmt::Debug for SharedUrl {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+impl std::fmt::Display for SharedUrl {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.0.as_ref())
+    }
+}
+
+impl<T: Into<SharedString>> From<T> for SharedUrl {
+    fn from(value: T) -> Self {
+        Self(value.into())
+    }
+}

crates/ui/src/components/stories/list_item.rs 🔗

@@ -1,4 +1,4 @@
-use gpui::Render;
+use gpui::{Render, SharedUrl};
 use story::Story;
 
 use crate::{prelude::*, Avatar};
@@ -43,7 +43,7 @@ impl Render for ListItemStory {
             .child(
                 ListItem::new("with_start slot avatar")
                     .child("Hello, world!")
-                    .start_slot(Avatar::new(SharedString::from(
+                    .start_slot(Avatar::new(SharedUrl::from(
                         "https://avatars.githubusercontent.com/u/1714999?v=4",
                     ))),
             )
@@ -51,7 +51,7 @@ impl Render for ListItemStory {
             .child(
                 ListItem::new("with_left_avatar")
                     .child("Hello, world!")
-                    .end_slot(Avatar::new(SharedString::from(
+                    .end_slot(Avatar::new(SharedUrl::from(
                         "https://avatars.githubusercontent.com/u/1714999?v=4",
                     ))),
             )
@@ -62,23 +62,23 @@ impl Render for ListItemStory {
                     .end_slot(
                         h_stack()
                             .gap_2()
-                            .child(Avatar::new(SharedString::from(
+                            .child(Avatar::new(SharedUrl::from(
                                 "https://avatars.githubusercontent.com/u/1789?v=4",
                             )))
-                            .child(Avatar::new(SharedString::from(
+                            .child(Avatar::new(SharedUrl::from(
                                 "https://avatars.githubusercontent.com/u/1789?v=4",
                             )))
-                            .child(Avatar::new(SharedString::from(
+                            .child(Avatar::new(SharedUrl::from(
                                 "https://avatars.githubusercontent.com/u/1789?v=4",
                             )))
-                            .child(Avatar::new(SharedString::from(
+                            .child(Avatar::new(SharedUrl::from(
                                 "https://avatars.githubusercontent.com/u/1789?v=4",
                             )))
-                            .child(Avatar::new(SharedString::from(
+                            .child(Avatar::new(SharedUrl::from(
                                 "https://avatars.githubusercontent.com/u/1789?v=4",
                             ))),
                     )
-                    .end_hover_slot(Avatar::new(SharedString::from(
+                    .end_hover_slot(Avatar::new(SharedUrl::from(
                         "https://avatars.githubusercontent.com/u/1714999?v=4",
                     ))),
             )