widgetry/widgets/
image.rs

1use crate::{
2    Color, ContentMode, CornerRounding, DrawWithTooltips, EdgeInsets, EventCtx, GeomBatch,
3    JustDraw, RewriteColor, ScreenDims, ScreenPt, Text, Widget,
4};
5use geom::{Bounds, Polygon, Pt2D};
6
7use std::borrow::Cow;
8
9/// A stylable UI component builder which presents vector graphics from an [`ImageSource`].
10#[derive(Clone, Debug, Default)]
11pub struct Image<'a, 'c> {
12    source: Option<Cow<'c, ImageSource<'a>>>,
13    tooltip: Option<Text>,
14    color: Option<RewriteColor>,
15    content_mode: Option<ContentMode>,
16    corner_rounding: Option<CornerRounding>,
17    padding: Option<EdgeInsets>,
18    bg_color: Option<Color>,
19    dims: Option<ScreenDims>,
20}
21
22/// The visual
23#[derive(Clone, Debug)]
24pub enum ImageSource<'a> {
25    /// Path to an SVG file
26    Path(&'a str),
27
28    /// UTF-8 encoded bytes of an SVG
29    Bytes { bytes: &'a [u8], cache_key: &'a str },
30
31    /// Previously rendered graphics, in the form of a [`GeomBatch`], can
32    /// be packaged as an `Image`.
33    GeomBatch(GeomBatch, geom::Bounds),
34}
35
36impl ImageSource<'_> {
37    /// Process `self` into a [`GeomBatch`].
38    ///
39    /// The underlying implementation makes use of caching to avoid re-parsing SVGs.
40    pub fn load(&self, prerender: &crate::Prerender) -> (GeomBatch, geom::Bounds) {
41        use crate::svg;
42        match self {
43            ImageSource::Path(image_path) => svg::load_svg(prerender, image_path),
44            ImageSource::Bytes { bytes, cache_key } => {
45                svg::load_svg_bytes(prerender, cache_key, bytes).unwrap_or_else(|_| {
46                    panic!("Failed to load svg from bytes. cache_key: {}", cache_key)
47                })
48            }
49            ImageSource::GeomBatch(geom_batch, bounds) => (geom_batch.clone(), *bounds),
50        }
51    }
52}
53
54impl<'a, 'c> Image<'a, 'c> {
55    /// An `Image` with no renderable content. Useful for starting a template for creating
56    /// several similar images using a builder pattern.
57    pub fn empty() -> Self {
58        Self {
59            ..Default::default()
60        }
61    }
62
63    /// Create an SVG `Image`, read from `filename`, which is colored to match `Style.icon_fg`
64    pub fn from_path(filename: &'a str) -> Self {
65        Self {
66            source: Some(Cow::Owned(ImageSource::Path(filename))),
67            ..Default::default()
68        }
69    }
70
71    /// Create a new SVG `Image` from bytes.
72    ///
73    /// * `labeled_bytes`: is a (`label`, `bytes`) tuple you can generate with
74    ///   [`include_labeled_bytes!`]
75    /// * `label`: a label to describe the bytes for debugging purposes
76    /// * `bytes`: UTF-8 encoded bytes of the SVG
77    pub fn from_bytes(labeled_bytes: (&'a str, &'a [u8])) -> Self {
78        Self {
79            source: Some(Cow::Owned(ImageSource::Bytes {
80                cache_key: labeled_bytes.0,
81                bytes: labeled_bytes.1,
82            })),
83            ..Default::default()
84        }
85    }
86
87    /// Create a new `Image` from a [`GeomBatch`].
88    ///
89    /// By default, the given `bounds` will be used for padding, background, etc.
90    pub fn from_batch(batch: GeomBatch, bounds: Bounds) -> Self {
91        Self {
92            source: Some(Cow::Owned(ImageSource::GeomBatch(batch, bounds))),
93            dims: Some(bounds.into()),
94            ..Default::default()
95        }
96    }
97
98    /// Set a new source for the `Image`'s data.
99    ///
100    /// This will replace any previously set source.
101    pub fn source(mut self, source: ImageSource<'a>) -> Self {
102        self.source = Some(Cow::Owned(source));
103        self
104    }
105
106    /// Set the path to an SVG file for the image.
107    ///
108    /// This will replace any image source previously set.
109    pub fn source_path(self, path: &'a str) -> Self {
110        self.source(ImageSource::Path(path))
111    }
112
113    /// Set the bytes for the image.
114    ///
115    /// This will replace any image source previously set.
116    ///
117    /// * `labeled_bytes`: is a (`label`, `bytes`) tuple you can generate with
118    ///   [`include_labeled_bytes!`]
119    /// * `label`: a label to describe the bytes for debugging purposes
120    /// * `bytes`: UTF-8 encoded bytes of the SVG
121    pub fn source_bytes(self, labeled_bytes: (&'a str, &'a [u8])) -> Self {
122        let (label, bytes) = labeled_bytes;
123        self.source(ImageSource::Bytes {
124            bytes,
125            cache_key: label,
126        })
127    }
128
129    /// Set the GeomBatch for the button.
130    ///
131    /// This will replace any image source previously set.
132    ///
133    /// This method is useful when doing more complex transforms. For example, to re-write more than
134    /// one color for your image, do so externally and pass in the resultant GeomBatch here.
135    pub fn source_batch(self, batch: GeomBatch, bounds: geom::Bounds) -> Self {
136        self.source(ImageSource::GeomBatch(batch, bounds))
137    }
138
139    /// Add a tooltip to appear when hovering over the image.
140    pub fn tooltip(mut self, tooltip: impl Into<Text>) -> Self {
141        self.tooltip = Some(tooltip.into());
142        self
143    }
144
145    /// Create a new `Image` based on `self`, but overriding with any values set on `other`.
146    pub fn merged_image_style(&'c self, other: &'c Self) -> Self {
147        let source_cow: Option<&Cow<'c, ImageSource>> =
148            other.source.as_ref().or_else(|| self.source.as_ref());
149        let source: Option<Cow<'c, ImageSource>> = source_cow.map(|source: &Cow<ImageSource>| {
150            let source: &ImageSource = source;
151            Cow::Borrowed(source)
152        });
153
154        Self {
155            source,
156            // PERF: we could make tooltip a cow to eliminate clone
157            tooltip: other.tooltip.clone().or_else(|| self.tooltip.clone()),
158            color: other.color.or(self.color),
159            content_mode: other.content_mode.or(self.content_mode),
160            corner_rounding: other.corner_rounding.or(self.corner_rounding),
161            padding: other.padding.or(self.padding),
162            bg_color: other.bg_color.or(self.bg_color),
163            dims: other.dims.or(self.dims),
164        }
165    }
166
167    /// Rewrite the color of the image.
168    pub fn color<RWC: Into<RewriteColor>>(mut self, value: RWC) -> Self {
169        self.color = Some(value.into());
170        self
171    }
172
173    /// Set a background color for the image. Has no effect unless custom `dims` are explicitly set.
174    pub fn bg_color(mut self, value: Color) -> Self {
175        self.bg_color = Some(value);
176        self
177    }
178
179    /// The image's intrinsic colors will be used, it will not be tinted like `Image::icon`
180    pub fn untinted(self) -> Self {
181        self.color(RewriteColor::NoOp)
182    }
183
184    /// Scale the bounds containing the image. If `dims` are not specified, the image's intrinsic
185    /// size will be used, but padding and background settings will be ignored.
186    ///
187    /// See [`Self::content_mode`] to control how the image scales to fit its custom bounds.
188    pub fn dims<D: Into<ScreenDims>>(mut self, dims: D) -> Self {
189        self.dims = Some(dims.into());
190        self
191    }
192
193    /// If a custom `dims` was set, control how the image should be scaled to its new bounds.
194    ///
195    /// If `dims` were not specified, the image will not be scaled, so content_mode has no
196    /// affect.
197    ///
198    /// The default, [`ContentMode::ScaleAspectFit`] will only grow as much as it can while
199    /// maintaining its aspect ratio and not exceeding its bounds
200    pub fn content_mode(mut self, value: ContentMode) -> Self {
201        self.content_mode = Some(value);
202        self
203    }
204
205    /// Set independent rounding for each of the image's corners. Has no effect unless custom
206    /// `dims` are explicitly set.
207    pub fn corner_rounding<R: Into<CornerRounding>>(mut self, value: R) -> Self {
208        self.corner_rounding = Some(value.into());
209        self
210    }
211
212    /// Set padding for the image. Has no effect unless custom `dims` are explicitly set.
213    pub fn padding<EI: Into<EdgeInsets>>(mut self, value: EI) -> Self {
214        self.padding = Some(value.into());
215        self
216    }
217
218    /// Padding above the image. Has no effect unless custom `dims` are explicitly set.
219    pub fn padding_top(mut self, new_value: f64) -> Self {
220        let mut padding = self.padding.unwrap_or_default();
221        padding.top = new_value;
222        self.padding = Some(padding);
223        self
224    }
225
226    /// Padding to the left of the image. Has no effect unless custom `dims` are explicitly set.
227    pub fn padding_left(mut self, new_value: f64) -> Self {
228        let mut padding = self.padding.unwrap_or_default();
229        padding.left = new_value;
230        self.padding = Some(padding);
231        self
232    }
233
234    /// Padding below the image. Has no effect unless custom `dims` are explicitly set.
235    pub fn padding_bottom(mut self, new_value: f64) -> Self {
236        let mut padding = self.padding.unwrap_or_default();
237        padding.bottom = new_value;
238        self.padding = Some(padding);
239        self
240    }
241
242    /// Padding to the right of the image. Has no effect unless custom `dims` are explicitly set.
243    pub fn padding_right(mut self, new_value: f64) -> Self {
244        let mut padding = self.padding.unwrap_or_default();
245        padding.right = new_value;
246        self.padding = Some(padding);
247        self
248    }
249
250    /// Render the `Image` and any styling (padding, background, etc.) to a `GeomBatch`.
251    pub fn build_batch(&self, ctx: &EventCtx) -> Option<(GeomBatch, Bounds)> {
252        // TODO: unwrap/panic if source is empty?
253        self.source.as_ref().map(|source| {
254            let (mut image_batch, image_bounds) = source.load(ctx.prerender);
255
256            image_batch = image_batch.color(
257                self.color
258                    .unwrap_or_else(|| RewriteColor::ChangeAll(ctx.style().icon_fg)),
259            );
260
261            match self.dims {
262                None => {
263                    // Preserve any padding intrinsic to the SVG.
264                    image_batch.push(Color::CLEAR, image_bounds.get_rectangle());
265                    (image_batch, image_bounds)
266                }
267                Some(image_dims) => {
268                    if image_bounds.width() != 0.0 && image_bounds.height() != 0.0 {
269                        let (x_factor, y_factor) = (
270                            image_dims.width / image_bounds.width(),
271                            image_dims.height / image_bounds.height(),
272                        );
273                        image_batch = match self.content_mode.unwrap_or_default() {
274                            ContentMode::ScaleToFill => image_batch.scale_xy(x_factor, y_factor),
275                            ContentMode::ScaleAspectFit => {
276                                image_batch.scale(x_factor.min(y_factor))
277                            }
278                            ContentMode::ScaleAspectFill => {
279                                image_batch.scale(x_factor.max(y_factor))
280                            }
281                        };
282                    }
283
284                    let image_corners = self.corner_rounding.unwrap_or_default();
285                    let padding = self.padding.unwrap_or_default();
286
287                    let mut container_batch = GeomBatch::new();
288                    let container_bounds = Bounds {
289                        min_x: 0.0,
290                        min_y: 0.0,
291                        max_x: image_dims.width + padding.left + padding.right,
292                        max_y: image_dims.height + padding.top + padding.bottom,
293                    };
294                    let container = match image_corners {
295                        CornerRounding::FullyRounded => {
296                            Polygon::pill(container_bounds.width(), container_bounds.height())
297                        }
298                        CornerRounding::CornerRadii(image_corners) => Polygon::rounded_rectangle(
299                            container_bounds.width(),
300                            container_bounds.height(),
301                            image_corners,
302                        ),
303                        CornerRounding::NoRounding => {
304                            Polygon::rectangle(container_bounds.width(), container_bounds.height())
305                        }
306                    };
307
308                    let image_bg = self.bg_color.unwrap_or(Color::CLEAR);
309                    container_batch.push(image_bg, container);
310
311                    let center = Pt2D::new(
312                        image_dims.width / 2.0 + padding.left,
313                        image_dims.height / 2.0 + padding.top,
314                    );
315                    image_batch = image_batch.autocrop().centered_on(center);
316                    container_batch.append(image_batch);
317
318                    (container_batch, container_bounds)
319                }
320            }
321        })
322    }
323
324    pub fn into_widget(self, ctx: &EventCtx) -> Widget {
325        match self.build_batch(ctx) {
326            None => Widget::nothing(),
327            Some((batch, bounds)) => {
328                if let Some(tooltip) = self.tooltip {
329                    DrawWithTooltips::new_widget(
330                        ctx,
331                        batch,
332                        vec![(bounds.get_rectangle(), tooltip, None)],
333                        Box::new(|_| GeomBatch::new()),
334                    )
335                } else {
336                    Widget::new(Box::new(JustDraw {
337                        dims: ScreenDims::new(bounds.width(), bounds.height()),
338                        draw: ctx.upload(batch),
339                        top_left: ScreenPt::new(0.0, 0.0),
340                    }))
341                }
342            }
343        }
344    }
345}