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#[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#[derive(Clone, Debug)]
24pub enum ImageSource<'a> {
25 Path(&'a str),
27
28 Bytes { bytes: &'a [u8], cache_key: &'a str },
30
31 GeomBatch(GeomBatch, geom::Bounds),
34}
35
36impl ImageSource<'_> {
37 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 pub fn empty() -> Self {
58 Self {
59 ..Default::default()
60 }
61 }
62
63 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 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 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 pub fn source(mut self, source: ImageSource<'a>) -> Self {
102 self.source = Some(Cow::Owned(source));
103 self
104 }
105
106 pub fn source_path(self, path: &'a str) -> Self {
110 self.source(ImageSource::Path(path))
111 }
112
113 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 pub fn source_batch(self, batch: GeomBatch, bounds: geom::Bounds) -> Self {
136 self.source(ImageSource::GeomBatch(batch, bounds))
137 }
138
139 pub fn tooltip(mut self, tooltip: impl Into<Text>) -> Self {
141 self.tooltip = Some(tooltip.into());
142 self
143 }
144
145 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 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 pub fn color<RWC: Into<RewriteColor>>(mut self, value: RWC) -> Self {
169 self.color = Some(value.into());
170 self
171 }
172
173 pub fn bg_color(mut self, value: Color) -> Self {
175 self.bg_color = Some(value);
176 self
177 }
178
179 pub fn untinted(self) -> Self {
181 self.color(RewriteColor::NoOp)
182 }
183
184 pub fn dims<D: Into<ScreenDims>>(mut self, dims: D) -> Self {
189 self.dims = Some(dims.into());
190 self
191 }
192
193 pub fn content_mode(mut self, value: ContentMode) -> Self {
201 self.content_mode = Some(value);
202 self
203 }
204
205 pub fn corner_rounding<R: Into<CornerRounding>>(mut self, value: R) -> Self {
208 self.corner_rounding = Some(value.into());
209 self
210 }
211
212 pub fn padding<EI: Into<EdgeInsets>>(mut self, value: EI) -> Self {
214 self.padding = Some(value.into());
215 self
216 }
217
218 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 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 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 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 pub fn build_batch(&self, ctx: &EventCtx) -> Option<(GeomBatch, Bounds)> {
252 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 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}