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}