Skip to main content

guise/
image.rs

1//! `Image` — a themed wrapper around gpui's `img()` element.
2//!
3//! Shows a picture from a remote URI, an asset path, or a filesystem path,
4//! with guise's sizing/radius vocabulary and an optional fallback slot that
5//! renders while the source is loading or unavailable.
6//!
7//! Note: gpui also exports a *type* named `gpui::Image` — that one is raw
8//! encoded bytes (an `Arc<gpui::Image>` is itself a valid source), not an
9//! element. This component is the element.
10//!
11//! ```ignore
12//! Image::new("https://example.com/cat.png")
13//!     .width(240.0)
14//!     .height(160.0)
15//!     .radius(Size::Md)
16//!     .fit(ObjectFit::Cover)
17//!     .fallback(|| Text::new("no image").dimmed())
18//! ```
19
20use gpui::prelude::*;
21use gpui::{img, px, AnyElement, App, ImageSource, IntoElement, Window};
22
23pub use gpui::ObjectFit;
24
25use crate::theme::{theme, Size};
26
27/// An image element. The Mantine `Image`.
28///
29/// The source accepts anything gpui's [`ImageSource`] converts from: `&str` /
30/// `String` (an `http(s)://` URI, else an embedded-asset path), a
31/// `Path`/`PathBuf` (local file), or decoded/raw image data.
32#[derive(IntoElement)]
33pub struct Image {
34    source: ImageSource,
35    width: Option<f32>,
36    height: Option<f32>,
37    radius: Option<Size>,
38    circle: bool,
39    fit: ObjectFit,
40    fallback: Option<Box<dyn Fn() -> AnyElement + 'static>>,
41}
42
43impl Image {
44    pub fn new(source: impl Into<ImageSource>) -> Self {
45        Image {
46            source: source.into(),
47            width: None,
48            height: None,
49            radius: None,
50            circle: false,
51            fit: ObjectFit::Cover,
52            fallback: None,
53        }
54    }
55
56    /// Fixed width in px. Give the element a size — an unsized image lays
57    /// out at zero.
58    pub fn width(mut self, width: f32) -> Self {
59        self.width = Some(width);
60        self
61    }
62
63    /// Fixed height in px.
64    pub fn height(mut self, height: f32) -> Self {
65        self.height = Some(height);
66        self
67    }
68
69    /// Corner radius from the theme scale (Mantine images are square-cornered
70    /// by default).
71    pub fn radius(mut self, radius: Size) -> Self {
72        self.radius = Some(radius);
73        self
74    }
75
76    /// Clip to a circle (an avatar). Pair with equal `width`/`height`.
77    pub fn circle(mut self) -> Self {
78        self.circle = true;
79        self
80    }
81
82    /// How the picture fills its box (default [`ObjectFit::Cover`]).
83    pub fn fit(mut self, fit: ObjectFit) -> Self {
84        self.fit = fit;
85        self
86    }
87
88    /// Element shown while the source is loading or failed to resolve.
89    pub fn fallback<E: IntoElement>(mut self, fallback: impl Fn() -> E + 'static) -> Self {
90        self.fallback = Some(Box::new(move || fallback().into_any_element()));
91        self
92    }
93}
94
95impl RenderOnce for Image {
96    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
97        let t = theme(cx);
98        let mut el = img(self.source).object_fit(self.fit);
99        if let Some(fallback) = self.fallback {
100            el = el.with_fallback(fallback);
101        }
102        if let Some(width) = self.width {
103            el = el.w(px(width));
104        }
105        if let Some(height) = self.height {
106            el = el.h(px(height));
107        }
108        if self.circle {
109            el = el.rounded_full();
110        } else if let Some(radius) = self.radius {
111            el = el.rounded(px(t.radius(radius)));
112        }
113        el
114    }
115}