cranpose_ui_graphics/draw_group.rs
1//! Groups of draws a [`DrawScope`](crate::DrawScope) moves, scales, turns or
2//! cuts as one block, as Compose's `withTransform` does.
3
4use crate::{
5 DrawPrimitive, GraphicsLayer, Path, Point, Rect, Size, TransformOrigin,
6 layer_transform::layer_transform_to_parent,
7};
8
9/// How a group of draws lands in the scope that holds it: scaled, then
10/// turned about the group's origin, then moved, as a [`GraphicsLayer`] with
11/// its transform origin at the top left places its content.
12#[derive(Clone, Copy, Debug, PartialEq)]
13pub struct DrawGroup {
14 /// Where the group's origin lands in the holding scope.
15 pub translation: Point,
16 /// Horizontal scale.
17 pub scale_x: f32,
18 /// Vertical scale.
19 pub scale_y: f32,
20 /// Clockwise turn on screen, in degrees.
21 pub rotation: f32,
22 /// The group's scope rect in its own coordinates: what its draws see as
23 /// the scope, and what `clip` cuts to.
24 pub bounds: Rect,
25 /// Whether nothing the group draws outside `bounds` shows.
26 pub clip: bool,
27 /// Whether the group draws into a surface of its own before it is
28 /// composited, so a mask drawn last in the group cuts only its draws.
29 pub isolated: bool,
30}
31
32impl DrawGroup {
33 /// A group that draws in the holding scope's coordinates.
34 pub fn plain(bounds: Rect) -> Self {
35 Self {
36 translation: Point::default(),
37 scale_x: 1.0,
38 scale_y: 1.0,
39 rotation: 0.0,
40 bounds,
41 clip: false,
42 isolated: false,
43 }
44 }
45
46 /// A [`DrawGroup::plain`] group cut to `bounds`.
47 pub fn clipped(bounds: Rect) -> Self {
48 Self {
49 clip: true,
50 ..Self::plain(bounds)
51 }
52 }
53
54 /// A group of `size` moved by `dx`, `dy`.
55 pub(crate) fn moved(dx: f32, dy: f32, size: Size) -> Self {
56 Self {
57 translation: Point::new(dx, dy),
58 ..Self::plain(Rect::from_size(size))
59 }
60 }
61
62 /// A group that cuts its draws to the mask drawn last in it, over `rect`.
63 pub(crate) fn masked(rect: Rect) -> Self {
64 Self {
65 isolated: true,
66 ..Self::clipped(rect)
67 }
68 }
69
70 /// A group of `size` scaled and turned about `pivot`, a point of the
71 /// holding scope that stays where it is.
72 pub(crate) fn about(
73 pivot: Point,
74 scale_x: f32,
75 scale_y: f32,
76 rotation: f32,
77 size: Size,
78 ) -> Self {
79 let turned = Self {
80 scale_x,
81 scale_y,
82 rotation,
83 ..Self::plain(Rect::from_size(size))
84 };
85 let moved = turned.transform_to_parent().map_point(pivot);
86 Self {
87 translation: Point::new(pivot.x - moved.x, pivot.y - moved.y),
88 ..turned
89 }
90 }
91
92 /// Whether the group scales or turns its draws, beyond moving them.
93 pub fn scales_or_turns(&self) -> bool {
94 self.scale_x != 1.0 || self.scale_y != 1.0 || self.rotation != 0.0
95 }
96
97 /// The graphics layer that places the group's draws as the group does.
98 pub fn layer(&self) -> GraphicsLayer {
99 GraphicsLayer {
100 scale_x: self.scale_x,
101 scale_y: self.scale_y,
102 rotation_z: self.rotation,
103 translation_x: self.translation.x,
104 translation_y: self.translation.y,
105 transform_origin: TransformOrigin::new(0.0, 0.0),
106 ..GraphicsLayer::DEFAULT
107 }
108 }
109
110 /// Maps the group's coordinates to the holding scope's.
111 pub fn transform_to_parent(&self) -> crate::ProjectiveTransform {
112 let origin = Rect {
113 x: 0.0,
114 y: 0.0,
115 ..self.bounds
116 };
117 layer_transform_to_parent(origin, Point::default(), &self.layer())
118 }
119
120 /// The group moved by `dx`, `dy` in the holding scope, its draws with it.
121 pub fn translate(self, dx: f32, dy: f32) -> Self {
122 Self {
123 translation: Point::new(self.translation.x + dx, self.translation.y + dy),
124 ..self
125 }
126 }
127}
128
129/// A group's start or end among recorded primitives. The primitives between
130/// a start and its end are in the group's coordinates.
131#[derive(Clone, Copy, Debug, PartialEq)]
132pub enum GroupMarker {
133 /// The group's draws start here.
134 Start(DrawGroup),
135 /// The group opened last ends here.
136 End,
137}
138
139/// One step of [`DrawScope::with_transform`](crate::DrawScope::with_transform),
140/// as Compose's `DrawTransform` calls. Each step applies inside the steps
141/// before it.
142#[derive(Clone, Copy, Debug, PartialEq)]
143pub enum DrawTransform<'a> {
144 /// Moves the draws by `dx`, `dy`.
145 Translate {
146 /// Horizontal move, in logical pixels.
147 dx: f32,
148 /// Vertical move, in logical pixels.
149 dy: f32,
150 },
151 /// Scales the draws about `pivot`, which stays where it is.
152 Scale {
153 /// Horizontal scale.
154 scale_x: f32,
155 /// Vertical scale.
156 scale_y: f32,
157 /// The point of the scope the scale keeps in place.
158 pivot: Point,
159 },
160 /// Turns the draws clockwise about `pivot`.
161 Rotate {
162 /// The turn, in degrees.
163 degrees: f32,
164 /// The point of the scope the turn keeps in place.
165 pivot: Point,
166 },
167 /// Shrinks the scope by the four insets: the draws move by `left` and
168 /// `top` and see the smaller size. Nothing is cut.
169 Inset {
170 /// Inset from the left edge.
171 left: f32,
172 /// Inset from the top edge.
173 top: f32,
174 /// Inset from the right edge.
175 right: f32,
176 /// Inset from the bottom edge.
177 bottom: f32,
178 },
179 /// Cuts the draws to `rect`, in scope coordinates.
180 ClipRect(Rect),
181 /// Cuts the draws to the fill of a path, in scope coordinates.
182 ClipPath(&'a Path),
183}
184
185/// Moves recorded primitives by `dx`, `dy` as one block: the primitives
186/// outside every group and the outermost groups move, and what a group
187/// holds moves with its group.
188pub fn translate_recorded(
189 primitives: impl IntoIterator<Item = DrawPrimitive>,
190 dx: f32,
191 dy: f32,
192) -> impl Iterator<Item = DrawPrimitive> {
193 let mut depth = 0u32;
194 primitives
195 .into_iter()
196 .map(move |primitive| match primitive {
197 DrawPrimitive::Group(GroupMarker::Start(group)) => {
198 depth += 1;
199 if depth == 1 {
200 DrawPrimitive::Group(GroupMarker::Start(group.translate(dx, dy)))
201 } else {
202 primitive
203 }
204 }
205 DrawPrimitive::Group(GroupMarker::End) => {
206 depth = depth.saturating_sub(1);
207 primitive
208 }
209 primitive if depth == 0 => primitive.translate(dx, dy),
210 primitive => primitive,
211 })
212}
213
214#[cfg(test)]
215#[path = "tests/draw_group_tests.rs"]
216mod tests;