1use geometry_core::{Point, Rect, Transform};
2
3use crate::DrawCommand;
4
5pub fn transform_clip_rect(m: [f32; 6], r: Rect) -> Rect {
7 let [a, b, c, d, e, f] = m;
8 let map = |x: f32, y: f32| (a * x + c * y + e, b * x + d * y + f);
9 let corners = [
10 map(r.x, r.y),
11 map(r.x + r.width, r.y),
12 map(r.x, r.y + r.height),
13 map(r.x + r.width, r.y + r.height),
14 ];
15 let mut min_x = f32::INFINITY;
16 let mut min_y = f32::INFINITY;
17 let mut max_x = f32::NEG_INFINITY;
18 let mut max_y = f32::NEG_INFINITY;
19 for (x, y) in corners {
20 min_x = min_x.min(x);
21 min_y = min_y.min(y);
22 max_x = max_x.max(x);
23 max_y = max_y.max(y);
24 }
25 Rect {
26 x: min_x,
27 y: min_y,
28 width: max_x - min_x,
29 height: max_y - min_y,
30 }
31}
32
33pub struct DrawState {
35 clip_stack: Vec<Rect>,
36 transform_stack: Vec<[f32; 6]>,
37 pub cumulative_matrix: [f32; 6],
38}
39
40impl DrawState {
41 pub fn new() -> Self {
42 Self {
43 clip_stack: Vec::with_capacity(16),
44 transform_stack: Vec::with_capacity(16),
45 cumulative_matrix: Transform::IDENTITY.to_array(),
46 }
47 }
48
49 #[inline]
62 pub fn push_clip(&mut self, rect: Rect) -> Rect {
63 let effective = match self.clip_stack.last() {
68 Some(¤t) => current
69 .intersect(rect)
70 .unwrap_or_else(|| Rect::new(current.x, current.y, 0.0, 0.0)),
71 None => rect,
72 };
73 self.clip_stack.push(effective);
74 effective
75 }
76
77 #[inline]
78 pub fn pop_clip(&mut self) -> Option<Rect> {
79 self.clip_stack.pop();
80 self.clip_stack.last().copied()
81 }
82
83 #[inline]
84 pub fn current_clip(&self) -> Option<Rect> {
85 self.clip_stack.last().copied()
86 }
87
88 #[inline]
89 pub fn push_matrix(&mut self, matrix: [f32; 6]) {
90 self.transform_stack.push(self.cumulative_matrix);
91 self.cumulative_matrix = Transform::from_array(matrix)
93 .then(Transform::from_array(self.cumulative_matrix))
94 .to_array();
95 }
96
97 #[inline]
98 pub fn pop_matrix(&mut self) {
99 if let Some(prev) = self.transform_stack.pop() {
100 self.cumulative_matrix = prev;
101 }
102 }
103
104 #[inline]
105 pub fn apply_point(&self, x: f32, y: f32) -> (f32, f32) {
106 let p = Transform::from_array(self.cumulative_matrix).apply(Point::new(x, y));
107 (p.x, p.y)
108 }
109
110 pub fn reset(&mut self) {
111 self.clip_stack.clear();
112 self.transform_stack.clear();
113 self.cumulative_matrix = Transform::IDENTITY.to_array();
114 }
115}
116
117impl Default for DrawState {
118 fn default() -> Self {
119 Self::new()
120 }
121}
122
123pub fn for_each_with_matrix<F>(cmds: &[DrawCommand], mut f: F)
125where
126 F: FnMut(&DrawCommand, [f32; 6]),
127{
128 let mut state = DrawState::new();
129 for cmd in cmds {
130 match cmd {
131 DrawCommand::PushMatrix { matrix } => state.push_matrix(*matrix),
132 DrawCommand::PopMatrix => state.pop_matrix(),
133 _ => {}
134 }
135 f(cmd, state.cumulative_matrix);
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
146 fn push_matrix_matches_legacy_compose_order() {
147 let cumulative = [2.0, 0.1, -0.2, 2.0, 10.0, 20.0];
148 let matrix = [1.0, 0.5, -0.5, 1.0, 5.0, 7.0];
149
150 let [a1, b1, c1, d1, e1, f1] = matrix;
152 let [a2, b2, c2, d2, e2, f2] = cumulative;
153 let expected = [
154 a2 * a1 + c2 * b1,
155 b2 * a1 + d2 * b1,
156 a2 * c1 + c2 * d1,
157 b2 * c1 + d2 * d1,
158 a2 * e1 + c2 * f1 + e2,
159 b2 * e1 + d2 * f1 + f2,
160 ];
161
162 let mut state = DrawState::new();
163 state.push_matrix(cumulative);
164 state.push_matrix(matrix);
165 assert_eq!(state.cumulative_matrix, expected);
166 }
167
168 #[test]
173 fn a_clip_outside_its_parent_clips_away_to_nothing() {
174 let viewport = Rect::new(0.0, 0.0, 340.0, 300.0);
175 let mut state = DrawState::new();
176 assert_eq!(
177 state.push_clip(viewport),
178 viewport,
179 "the outermost clip is itself"
180 );
181
182 let escaped = state.push_clip(Rect::new(0.0, 560.0, 56.0, 56.0));
184 assert_eq!(
185 (escaped.width, escaped.height),
186 (0.0, 0.0),
187 "an image scrolled out of its panel must not be drawn at all, got {escaped:?}"
188 );
189 assert!(
190 viewport
191 .intersect(Rect::new(escaped.x, escaped.y, 1.0, 1.0))
192 .is_some(),
193 "and the empty clip sits inside the parent, so a backend that rounds it up to 1×1 draws that \
194 pixel where it was already allowed to: {escaped:?}"
195 );
196
197 state.pop_clip();
198 let visible = state.push_clip(Rect::new(0.0, 280.0, 56.0, 56.0));
200 assert_eq!(visible, Rect::new(0.0, 280.0, 56.0, 20.0));
201
202 let mut fresh = DrawState::new();
204 let alone = Rect::new(12.0, 34.0, 56.0, 78.0);
205 assert_eq!(fresh.push_clip(alone), alone);
206 }
207}