Skip to main content

cranpose_ui_graphics/
geometry.rs

1//! Geometric primitives: Point, Size, Rect, Insets, Path
2
3use crate::{Brush, ColorFilter, ImageBitmap};
4use std::ops::AddAssign;
5
6#[derive(Clone, Copy, Debug, PartialEq, Default)]
7pub struct Point {
8    pub x: f32,
9    pub y: f32,
10}
11
12impl Point {
13    pub const fn new(x: f32, y: f32) -> Self {
14        Self { x, y }
15    }
16
17    pub const ZERO: Point = Point { x: 0.0, y: 0.0 };
18}
19
20#[derive(Clone, Copy, Debug, PartialEq, Default)]
21pub struct Size {
22    pub width: f32,
23    pub height: f32,
24}
25
26impl Size {
27    pub const fn new(width: f32, height: f32) -> Self {
28        Self { width, height }
29    }
30
31    pub const ZERO: Size = Size {
32        width: 0.0,
33        height: 0.0,
34    };
35}
36
37#[derive(Clone, Copy, Debug, PartialEq)]
38pub struct Rect {
39    pub x: f32,
40    pub y: f32,
41    pub width: f32,
42    pub height: f32,
43}
44
45impl Rect {
46    pub fn from_origin_size(origin: Point, size: Size) -> Self {
47        Self {
48            x: origin.x,
49            y: origin.y,
50            width: size.width,
51            height: size.height,
52        }
53    }
54
55    pub fn from_size(size: Size) -> Self {
56        Self {
57            x: 0.0,
58            y: 0.0,
59            width: size.width,
60            height: size.height,
61        }
62    }
63
64    pub fn translate(&self, dx: f32, dy: f32) -> Self {
65        Self {
66            x: self.x + dx,
67            y: self.y + dy,
68            width: self.width,
69            height: self.height,
70        }
71    }
72
73    pub fn contains(&self, x: f32, y: f32) -> bool {
74        x >= self.x && y >= self.y && x <= self.x + self.width && y <= self.y + self.height
75    }
76}
77
78/// Padding values for each edge of a rectangle.
79#[derive(Clone, Copy, Debug, Default, PartialEq)]
80pub struct EdgeInsets {
81    pub left: f32,
82    pub top: f32,
83    pub right: f32,
84    pub bottom: f32,
85}
86
87impl EdgeInsets {
88    pub fn uniform(all: f32) -> Self {
89        Self {
90            left: all,
91            top: all,
92            right: all,
93            bottom: all,
94        }
95    }
96
97    pub fn horizontal(horizontal: f32) -> Self {
98        Self {
99            left: horizontal,
100            right: horizontal,
101            ..Self::default()
102        }
103    }
104
105    pub fn vertical(vertical: f32) -> Self {
106        Self {
107            top: vertical,
108            bottom: vertical,
109            ..Self::default()
110        }
111    }
112
113    pub fn symmetric(horizontal: f32, vertical: f32) -> Self {
114        Self {
115            left: horizontal,
116            right: horizontal,
117            top: vertical,
118            bottom: vertical,
119        }
120    }
121
122    pub fn from_components(left: f32, top: f32, right: f32, bottom: f32) -> Self {
123        Self {
124            left,
125            top,
126            right,
127            bottom,
128        }
129    }
130
131    pub fn is_zero(&self) -> bool {
132        self.left == 0.0 && self.top == 0.0 && self.right == 0.0 && self.bottom == 0.0
133    }
134
135    pub fn horizontal_sum(&self) -> f32 {
136        self.left + self.right
137    }
138
139    pub fn vertical_sum(&self) -> f32 {
140        self.top + self.bottom
141    }
142}
143
144impl AddAssign for EdgeInsets {
145    fn add_assign(&mut self, rhs: Self) {
146        self.left += rhs.left;
147        self.top += rhs.top;
148        self.right += rhs.right;
149        self.bottom += rhs.bottom;
150    }
151}
152
153#[derive(Clone, Copy, Debug, Default, PartialEq)]
154pub struct CornerRadii {
155    pub top_left: f32,
156    pub top_right: f32,
157    pub bottom_right: f32,
158    pub bottom_left: f32,
159}
160
161impl CornerRadii {
162    pub fn uniform(radius: f32) -> Self {
163        Self {
164            top_left: radius,
165            top_right: radius,
166            bottom_right: radius,
167            bottom_left: radius,
168        }
169    }
170}
171
172#[derive(Clone, Copy, Debug, PartialEq)]
173pub struct RoundedCornerShape {
174    radii: CornerRadii,
175}
176
177impl RoundedCornerShape {
178    pub fn new(top_left: f32, top_right: f32, bottom_right: f32, bottom_left: f32) -> Self {
179        Self {
180            radii: CornerRadii {
181                top_left,
182                top_right,
183                bottom_right,
184                bottom_left,
185            },
186        }
187    }
188
189    pub fn uniform(radius: f32) -> Self {
190        Self {
191            radii: CornerRadii::uniform(radius),
192        }
193    }
194
195    pub fn with_radii(radii: CornerRadii) -> Self {
196        Self { radii }
197    }
198
199    pub fn resolve(&self, width: f32, height: f32) -> CornerRadii {
200        let mut resolved = self.radii;
201        let max_width = (width / 2.0).max(0.0);
202        let max_height = (height / 2.0).max(0.0);
203        resolved.top_left = resolved.top_left.clamp(0.0, max_width).min(max_height);
204        resolved.top_right = resolved.top_right.clamp(0.0, max_width).min(max_height);
205        resolved.bottom_right = resolved.bottom_right.clamp(0.0, max_width).min(max_height);
206        resolved.bottom_left = resolved.bottom_left.clamp(0.0, max_width).min(max_height);
207        resolved
208    }
209
210    pub fn radii(&self) -> CornerRadii {
211        self.radii
212    }
213}
214
215#[derive(Clone, Copy, Debug, PartialEq)]
216pub struct GraphicsLayer {
217    pub alpha: f32,
218    pub scale: f32,
219    pub translation_x: f32,
220    pub translation_y: f32,
221}
222
223impl Default for GraphicsLayer {
224    fn default() -> Self {
225        Self {
226            alpha: 1.0,
227            scale: 1.0,
228            translation_x: 0.0,
229            translation_y: 0.0,
230        }
231    }
232}
233
234#[derive(Clone, Debug, PartialEq)]
235pub enum DrawPrimitive {
236    Rect {
237        rect: Rect,
238        brush: Brush,
239    },
240    RoundRect {
241        rect: Rect,
242        brush: Brush,
243        radii: CornerRadii,
244    },
245    Image {
246        rect: Rect,
247        image: ImageBitmap,
248        alpha: f32,
249        color_filter: Option<ColorFilter>,
250        /// Optional source rectangle in image-pixel coordinates.
251        /// When `None`, the entire image is drawn. When `Some`, only the
252        /// specified sub-region of the source image is sampled.
253        src_rect: Option<Rect>,
254    },
255}
256
257pub trait DrawScope {
258    fn size(&self) -> Size;
259    fn draw_content(&self);
260    fn draw_rect(&mut self, brush: Brush);
261    /// Draws a rectangle at the specified position and size.
262    fn draw_rect_at(&mut self, rect: Rect, brush: Brush);
263    fn draw_round_rect(&mut self, brush: Brush, radii: CornerRadii);
264    fn draw_image(&mut self, image: ImageBitmap);
265    fn draw_image_at(
266        &mut self,
267        rect: Rect,
268        image: ImageBitmap,
269        alpha: f32,
270        color_filter: Option<ColorFilter>,
271    );
272    /// Draws a sub-region of an image. `src_rect` is in image-pixel
273    /// coordinates; `dst_rect` is in scope coordinates.
274    fn draw_image_src(
275        &mut self,
276        image: ImageBitmap,
277        src_rect: Rect,
278        dst_rect: Rect,
279        alpha: f32,
280        color_filter: Option<ColorFilter>,
281    );
282    fn into_primitives(self) -> Vec<DrawPrimitive>;
283}
284
285#[derive(Default)]
286pub struct DrawScopeDefault {
287    size: Size,
288    primitives: Vec<DrawPrimitive>,
289}
290
291impl DrawScopeDefault {
292    pub fn new(size: Size) -> Self {
293        Self {
294            size,
295            primitives: Vec::new(),
296        }
297    }
298}
299
300impl DrawScope for DrawScopeDefault {
301    fn size(&self) -> Size {
302        self.size
303    }
304
305    fn draw_content(&self) {}
306
307    fn draw_rect(&mut self, brush: Brush) {
308        self.primitives.push(DrawPrimitive::Rect {
309            rect: Rect::from_size(self.size),
310            brush,
311        });
312    }
313
314    fn draw_rect_at(&mut self, rect: Rect, brush: Brush) {
315        self.primitives.push(DrawPrimitive::Rect { rect, brush });
316    }
317
318    fn draw_round_rect(&mut self, brush: Brush, radii: CornerRadii) {
319        self.primitives.push(DrawPrimitive::RoundRect {
320            rect: Rect::from_size(self.size),
321            brush,
322            radii,
323        });
324    }
325
326    fn draw_image(&mut self, image: ImageBitmap) {
327        self.primitives.push(DrawPrimitive::Image {
328            rect: Rect::from_size(self.size),
329            image,
330            alpha: 1.0,
331            color_filter: None,
332            src_rect: None,
333        });
334    }
335
336    fn draw_image_at(
337        &mut self,
338        rect: Rect,
339        image: ImageBitmap,
340        alpha: f32,
341        color_filter: Option<ColorFilter>,
342    ) {
343        self.primitives.push(DrawPrimitive::Image {
344            rect,
345            image,
346            alpha: alpha.clamp(0.0, 1.0),
347            color_filter,
348            src_rect: None,
349        });
350    }
351
352    fn draw_image_src(
353        &mut self,
354        image: ImageBitmap,
355        src_rect: Rect,
356        dst_rect: Rect,
357        alpha: f32,
358        color_filter: Option<ColorFilter>,
359    ) {
360        self.primitives.push(DrawPrimitive::Image {
361            rect: dst_rect,
362            image,
363            alpha: alpha.clamp(0.0, 1.0),
364            color_filter,
365            src_rect: Some(src_rect),
366        });
367    }
368
369    fn into_primitives(self) -> Vec<DrawPrimitive> {
370        self.primitives
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377    use crate::{Color, ImageBitmap};
378
379    #[test]
380    fn draw_image_uses_scope_size_as_default_rect() {
381        let mut scope = DrawScopeDefault::new(Size::new(40.0, 24.0));
382        let image = ImageBitmap::from_rgba8(2, 2, vec![255; 16]).expect("image");
383        scope.draw_image(image.clone());
384        let primitives = scope.into_primitives();
385        assert_eq!(primitives.len(), 1);
386        match &primitives[0] {
387            DrawPrimitive::Image {
388                rect,
389                image: actual,
390                alpha,
391                color_filter,
392                src_rect,
393            } => {
394                assert_eq!(*rect, Rect::from_size(Size::new(40.0, 24.0)));
395                assert_eq!(*actual, image);
396                assert_eq!(*alpha, 1.0);
397                assert!(color_filter.is_none());
398                assert!(src_rect.is_none());
399            }
400            other => panic!("expected image primitive, got {other:?}"),
401        }
402    }
403
404    #[test]
405    fn draw_image_src_stores_src_rect() {
406        let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
407        let image = ImageBitmap::from_rgba8(64, 64, vec![255; 64 * 64 * 4]).expect("image");
408        let src = Rect {
409            x: 10.0,
410            y: 20.0,
411            width: 30.0,
412            height: 40.0,
413        };
414        let dst = Rect {
415            x: 0.0,
416            y: 0.0,
417            width: 60.0,
418            height: 80.0,
419        };
420        scope.draw_image_src(image.clone(), src, dst, 0.8, None);
421        let primitives = scope.into_primitives();
422        assert_eq!(primitives.len(), 1);
423        match &primitives[0] {
424            DrawPrimitive::Image {
425                rect,
426                image: actual,
427                alpha,
428                src_rect,
429                ..
430            } => {
431                assert_eq!(*rect, dst);
432                assert_eq!(*actual, image);
433                assert!((alpha - 0.8).abs() < 1e-5);
434                assert_eq!(*src_rect, Some(src));
435            }
436            other => panic!("expected image primitive, got {other:?}"),
437        }
438    }
439
440    #[test]
441    fn draw_image_at_clamps_alpha() {
442        let mut scope = DrawScopeDefault::new(Size::new(10.0, 10.0));
443        let image = ImageBitmap::from_rgba8(1, 1, vec![255, 255, 255, 255]).expect("image");
444        scope.draw_image_at(
445            Rect::from_origin_size(Point::new(2.0, 3.0), Size::new(5.0, 6.0)),
446            image,
447            3.0,
448            Some(ColorFilter::Tint(Color::from_rgba_u8(128, 128, 255, 255))),
449        );
450        match &scope.into_primitives()[0] {
451            DrawPrimitive::Image { alpha, .. } => assert_eq!(*alpha, 1.0),
452            other => panic!("expected image primitive, got {other:?}"),
453        }
454    }
455}