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::devtools::Probed;
26use crate::theme::{theme, Size};
27
28/// An image element.
29///
30/// The source accepts anything gpui's [`ImageSource`] converts from: `&str` /
31/// `String` (an `http(s)://` URI, else an embedded-asset path), a
32/// `Path`/`PathBuf` (local file), or decoded/raw image data.
33#[derive(IntoElement)]
34pub struct Image {
35  source: ImageSource,
36  width: Option<f32>,
37  height: Option<f32>,
38  radius: Option<Size>,
39  circle: bool,
40  fit: ObjectFit,
41  fallback: Option<Box<dyn Fn() -> AnyElement + 'static>>,
42}
43
44impl Image {
45  pub fn new(source: impl Into<ImageSource>) -> Self {
46    Image {
47      source: source.into(),
48      width: None,
49      height: None,
50      radius: None,
51      circle: false,
52      fit: ObjectFit::Cover,
53      fallback: None,
54    }
55  }
56
57  /// Fixed width in px. Give the element a size — an unsized image lays
58  /// out at zero.
59  pub fn width(mut self, width: f32) -> Self {
60    self.width = Some(width);
61    self
62  }
63
64  /// Fixed height in px.
65  pub fn height(mut self, height: f32) -> Self {
66    self.height = Some(height);
67    self
68  }
69
70  /// Corner radius from the theme scale (images are square-cornered by
71  /// default).
72  pub fn radius(mut self, radius: Size) -> Self {
73    self.radius = Some(radius);
74    self
75  }
76
77  /// Clip to a circle (an avatar). Pair with equal `width`/`height`.
78  pub fn circle(mut self) -> Self {
79    self.circle = true;
80    self
81  }
82
83  /// How the picture fills its box (default [`ObjectFit::Cover`]).
84  pub fn fit(mut self, fit: ObjectFit) -> Self {
85    self.fit = fit;
86    self
87  }
88
89  /// Element shown while the source is loading or failed to resolve.
90  pub fn fallback<E: IntoElement>(mut self, fallback: impl Fn() -> E + 'static) -> Self {
91    self.fallback = Some(Box::new(move || fallback().into_any_element()));
92    self
93  }
94}
95
96impl RenderOnce for Image {
97  fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
98    let t = theme(cx);
99    let mut el = img(self.source).object_fit(self.fit);
100    if let Some(fallback) = self.fallback {
101      el = el.with_fallback(fallback);
102    }
103    if let Some(width) = self.width {
104      el = el.w(px(width));
105    }
106    if let Some(height) = self.height {
107      el = el.h(px(height));
108    }
109    if self.circle {
110      el = el.rounded_full();
111    } else if let Some(radius) = self.radius {
112      el = el.rounded(px(t.radius(radius)));
113    }
114    el.probe("Image")
115  }
116}