egui_snarl/ui/
background_pattern.rs1use egui::{emath::Rot2, vec2, Painter, Rect, Style, Vec2};
2
3use super::SnarlStyle;
4
5#[derive(Clone, Copy, Debug, PartialEq)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9#[cfg_attr(feature = "egui-probe", derive(egui_probe::EguiProbe))]
10pub struct Grid {
11 pub spacing: Vec2,
13
14 #[cfg_attr(feature = "egui-probe", egui_probe(as egui_probe::angle))]
16 pub angle: f32,
17}
18
19const DEFAULT_GRID_SPACING: Vec2 = vec2(50.0, 50.0);
20macro_rules! default_grid_spacing {
21 () => {
22 stringify!(vec2(50.0, 50.0))
23 };
24}
25
26const DEFAULT_GRID_ANGLE: f32 = 1.0;
27macro_rules! default_grid_angle {
28 () => {
29 stringify!(1.0)
30 };
31}
32
33impl Default for Grid {
34 fn default() -> Self {
35 Self {
36 spacing: DEFAULT_GRID_SPACING,
37 angle: DEFAULT_GRID_ANGLE,
38 }
39 }
40}
41
42impl Grid {
43 #[must_use]
45 pub const fn new(spacing: Vec2, angle: f32) -> Self {
46 Self { spacing, angle }
47 }
48
49 fn draw(&self, viewport: &Rect, snarl_style: &SnarlStyle, style: &Style, painter: &Painter) {
50 let bg_stroke = snarl_style.get_bg_pattern_stroke(style);
51
52 let spacing = vec2(self.spacing.x.max(1.0), self.spacing.y.max(1.0));
53
54 let rot = Rot2::from_angle(self.angle);
55 let rot_inv = rot.inverse();
56
57 let pattern_bounds = viewport.rotate_bb(rot_inv);
58
59 let min_x = (pattern_bounds.min.x / spacing.x).ceil();
60 let max_x = (pattern_bounds.max.x / spacing.x).floor();
61
62 #[allow(clippy::cast_possible_truncation)]
63 for x in 0..=f32::ceil(max_x - min_x) as i64 {
64 #[allow(clippy::cast_precision_loss)]
65 let x = (x as f32 + min_x) * spacing.x;
66
67 let top = (rot * vec2(x, pattern_bounds.min.y)).to_pos2();
68 let bottom = (rot * vec2(x, pattern_bounds.max.y)).to_pos2();
69
70 painter.line_segment([top, bottom], bg_stroke);
71 }
72
73 let min_y = (pattern_bounds.min.y / spacing.y).ceil();
74 let max_y = (pattern_bounds.max.y / spacing.y).floor();
75
76 #[allow(clippy::cast_possible_truncation)]
77 for y in 0..=f32::ceil(max_y - min_y) as i64 {
78 #[allow(clippy::cast_precision_loss)]
79 let y = (y as f32 + min_y) * spacing.y;
80
81 let top = (rot * vec2(pattern_bounds.min.x, y)).to_pos2();
82 let bottom = (rot * vec2(pattern_bounds.max.x, y)).to_pos2();
83
84 painter.line_segment([top, bottom], bg_stroke);
85 }
86 }
87}
88
89#[derive(Clone, Copy, Debug, PartialEq)]
91#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
92#[cfg_attr(feature = "egui-probe", derive(egui_probe::EguiProbe))]
93pub enum BackgroundPattern {
94 NoPattern,
96
97 #[cfg_attr(feature = "egui-probe", egui_probe(transparent))]
99 Grid(Grid),
100}
101
102impl Default for BackgroundPattern {
103 fn default() -> Self {
104 BackgroundPattern::new()
105 }
106}
107
108impl BackgroundPattern {
109 #[doc = default_grid_spacing!()]
113 #[doc = default_grid_angle!()]
115 #[must_use]
117 pub const fn new() -> Self {
118 Self::Grid(Grid::new(DEFAULT_GRID_SPACING, DEFAULT_GRID_ANGLE))
119 }
120
121 #[must_use]
123 pub const fn grid(spacing: Vec2, angle: f32) -> Self {
124 Self::Grid(Grid::new(spacing, angle))
125 }
126
127 pub fn draw(
129 &self,
130 viewport: &Rect,
131 snarl_style: &SnarlStyle,
132 style: &Style,
133 painter: &Painter,
134 ) {
135 match self {
136 BackgroundPattern::Grid(g) => g.draw(viewport, snarl_style, style, painter),
137 BackgroundPattern::NoPattern => {}
138 }
139 }
140}