Skip to main content

faststep/
image_view.rs

1use embedded_graphics::{
2    Drawable,
3    draw_target::{DrawTarget, DrawTargetExt},
4    image::{Image, ImageDrawable},
5    prelude::{OriginDimensions, PixelColor, Point, Primitive, Size},
6    primitives::{PrimitiveStyleBuilder, Rectangle, RoundedRectangle},
7};
8
9use crate::EdgeInsets;
10
11/// Alignment used when placing an image inside its frame.
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum ImageAlignment {
14    /// Align to the top-left corner.
15    TopLeading,
16    /// Align to the top edge, centered horizontally.
17    Top,
18    /// Align to the top-right corner.
19    TopTrailing,
20    /// Align to the leading edge, centered vertically.
21    Leading,
22    /// Center the image.
23    Center,
24    /// Align to the trailing edge, centered vertically.
25    Trailing,
26    /// Align to the bottom-left corner.
27    BottomLeading,
28    /// Align to the bottom edge, centered horizontally.
29    Bottom,
30    /// Align to the bottom-right corner.
31    BottomTrailing,
32}
33
34/// Visual configuration for an [`ImageView`].
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub struct ImageViewStyle<C> {
37    /// Image alignment inside the content rect.
38    pub alignment: ImageAlignment,
39    /// Inner padding applied before image placement.
40    pub insets: EdgeInsets,
41    /// Optional background fill for the shell.
42    pub background: Option<C>,
43    /// Optional border color for the shell.
44    pub border: Option<C>,
45    /// Border width in pixels.
46    pub border_width: u32,
47    /// Corner radius for the shell.
48    pub corner_radius: u32,
49}
50
51impl<C> ImageViewStyle<C> {
52    /// Creates the default image view style.
53    pub const fn new() -> Self {
54        Self {
55            alignment: ImageAlignment::Center,
56            insets: EdgeInsets::all(0),
57            background: None,
58            border: None,
59            border_width: 0,
60            corner_radius: 0,
61        }
62    }
63
64    /// Sets image alignment.
65    pub fn with_alignment(mut self, alignment: ImageAlignment) -> Self {
66        self.alignment = alignment;
67        self
68    }
69
70    /// Sets content insets.
71    pub fn with_insets(mut self, insets: EdgeInsets) -> Self {
72        self.insets = insets;
73        self
74    }
75
76    /// Sets the shell background color.
77    pub fn with_background(mut self, background: C) -> Self {
78        self.background = Some(background);
79        self
80    }
81
82    /// Sets border color and width.
83    pub fn with_border(mut self, border: C, border_width: u32) -> Self {
84        self.border = Some(border);
85        self.border_width = border_width;
86        self
87    }
88
89    /// Sets the shell corner radius.
90    pub fn with_corner_radius(mut self, corner_radius: u32) -> Self {
91        self.corner_radius = corner_radius;
92        self
93    }
94}
95
96impl<C> Default for ImageViewStyle<C> {
97    fn default() -> Self {
98        Self::new()
99    }
100}
101
102/// Reusable bitmap view similar to UIKit's `UIImageView`.
103pub struct ImageView<'a, T, C>
104where
105    T: ImageDrawable<Color = C> + OriginDimensions,
106    C: PixelColor,
107{
108    /// Outer frame for the view.
109    pub frame: Rectangle,
110    /// Referenced image content.
111    pub image: &'a T,
112    /// Shell and alignment style.
113    pub style: ImageViewStyle<C>,
114}
115
116impl<'a, T, C> ImageView<'a, T, C>
117where
118    T: ImageDrawable<Color = C> + OriginDimensions,
119    C: PixelColor,
120{
121    /// Creates a new image view with default styling.
122    pub const fn new(frame: Rectangle, image: &'a T) -> Self {
123        Self {
124            frame,
125            image,
126            style: ImageViewStyle::new(),
127        }
128    }
129
130    /// Replaces the view style.
131    pub fn with_style(mut self, style: ImageViewStyle<C>) -> Self {
132        self.style = style;
133        self
134    }
135
136    /// Draws the image view.
137    pub fn draw<D>(&self, display: &mut D)
138    where
139        D: DrawTarget<Color = C>,
140    {
141        draw_shell(
142            display,
143            self.frame,
144            self.style.background,
145            self.style.border,
146            self.style.border_width,
147            self.style.corner_radius,
148        );
149        let content = self.style.insets.inset_rect(self.frame);
150        if content.size.width == 0 || content.size.height == 0 {
151            return;
152        }
153
154        let image_size = self.image.size();
155        let origin = aligned_origin(content, image_size, self.style.alignment);
156        let mut clipped = display.clipped(&content);
157        Image::new(self.image, origin).draw(&mut clipped).ok();
158    }
159}
160
161fn draw_shell<D, C>(
162    display: &mut D,
163    frame: Rectangle,
164    background: Option<C>,
165    border: Option<C>,
166    border_width: u32,
167    corner_radius: u32,
168) where
169    D: DrawTarget<Color = C>,
170    C: PixelColor,
171{
172    if background.is_none() && border.is_none() {
173        return;
174    }
175
176    let mut style = PrimitiveStyleBuilder::new();
177    if let Some(background) = background {
178        style = style.fill_color(background);
179    }
180    if let Some(border) = border {
181        style = style.stroke_color(border).stroke_width(border_width.max(1));
182    }
183
184    RoundedRectangle::with_equal_corners(frame, Size::new(corner_radius, corner_radius))
185        .into_styled(style.build())
186        .draw(display)
187        .ok();
188}
189
190fn aligned_origin(frame: Rectangle, image_size: Size, alignment: ImageAlignment) -> Point {
191    let dx = frame.size.width as i32 - image_size.width as i32;
192    let dy = frame.size.height as i32 - image_size.height as i32;
193    let offset = match alignment {
194        ImageAlignment::TopLeading => Point::new(0, 0),
195        ImageAlignment::Top => Point::new(dx / 2, 0),
196        ImageAlignment::TopTrailing => Point::new(dx, 0),
197        ImageAlignment::Leading => Point::new(0, dy / 2),
198        ImageAlignment::Center => Point::new(dx / 2, dy / 2),
199        ImageAlignment::Trailing => Point::new(dx, dy / 2),
200        ImageAlignment::BottomLeading => Point::new(0, dy),
201        ImageAlignment::Bottom => Point::new(dx / 2, dy),
202        ImageAlignment::BottomTrailing => Point::new(dx, dy),
203    };
204    frame.top_left + offset
205}