1use std::sync::Arc;
2
3use rustc_hash::FxHashMap;
4
5use crate::style::Scale;
6use crate::{Color, DrawCommand, Paint, PathData, PathStyle, RectStyle, TextStyle};
7
8fn fill_layer_alpha(style: &RectStyle) -> Option<f32> {
9 if style.radius.is_zero() || style.shadow.is_some() {
11 return None;
12 }
13 match style.fill {
14 Some(Paint::Solid(c)) if c.a > 0.0 && c.a < 1.0 => Some(c.a),
15 _ => None,
16 }
17}
18
19fn would_expand_fill_layers(commands: &[DrawCommand]) -> bool {
20 commands.iter().any(|cmd| match cmd {
21 DrawCommand::Rect { style, .. } => fill_layer_alpha(style).is_some(),
22 _ => false,
23 })
24}
25
26pub fn expand_fill_layers(commands: &[DrawCommand]) -> Option<Vec<DrawCommand>> {
27 if !would_expand_fill_layers(commands) {
28 return None;
29 }
30 let mut result = Vec::with_capacity(commands.len() + 4);
31 for cmd in commands {
32 if let DrawCommand::Rect { rect, style } = cmd {
33 if let Some(alpha) = fill_layer_alpha(style) {
34 let mut opaque = **style;
35 if let Some(Paint::Solid(c)) = opaque.fill {
36 opaque.fill = Some(Paint::Solid(Color { a: 1.0, ..c }));
37 }
38 result.push(DrawCommand::PushLayer {
39 opacity: alpha,
40 backdrop_blur: 0.0,
41 });
42 result.push(DrawCommand::Rect {
43 rect: *rect,
44 style: Arc::new(opaque),
45 });
46 result.push(DrawCommand::PopLayer);
47 continue;
48 }
49 }
50 result.push(cmd.clone());
51 }
52 Some(result)
53}
54
55pub fn blur_sigma(blur_radius: f32) -> f32 {
56 blur_radius / 2.0
57}
58
59pub fn blur_padding(sigma: f32) -> i32 {
60 (sigma * 3.0).ceil() as i32 + 1
61}
62
63fn scale_path_data(data: &crate::PathData, sf: f32) -> crate::PathData {
64 let mut out = crate::PathData::new();
65 for verb in data.verbs() {
66 out = match verb {
67 crate::PathVerb::MoveTo(p) => out.move_to(p.scale(sf)),
68 crate::PathVerb::LineTo(p) => out.line_to(p.scale(sf)),
69 crate::PathVerb::QuadTo { ctrl, to } => out.quad_to(ctrl.scale(sf), to.scale(sf)),
70 crate::PathVerb::CubicTo { ctrl1, ctrl2, to } => {
71 out.cubic_to(ctrl1.scale(sf), ctrl2.scale(sf), to.scale(sf))
72 }
73 crate::PathVerb::Close => out.close(),
74 };
75 }
76 out
77}
78
79fn scale_command(cmd: &DrawCommand, sf: f32) -> DrawCommand {
80 match cmd {
81 DrawCommand::Rect { rect, style } => DrawCommand::Rect {
82 rect: rect.scale(sf),
83 style: Arc::new((**style).scale(sf)),
84 },
85 DrawCommand::Text { text, rect, style } => DrawCommand::Text {
86 text: text.clone(),
87 rect: rect.scale(sf),
88 style: Arc::new((**style).scale(sf)),
89 },
90 DrawCommand::RichText { runs, rect, base } => DrawCommand::RichText {
91 runs: runs.clone(),
92 rect: rect.scale(sf),
93 base: Arc::new((**base).scale(sf)),
94 },
95 DrawCommand::Image { data, rect, filter } => DrawCommand::Image {
96 data: data.clone(),
97 rect: rect.scale(sf),
98 filter: *filter,
99 },
100 DrawCommand::Line { p1, p2, style } => DrawCommand::Line {
101 p1: p1.scale(sf),
102 p2: p2.scale(sf),
103 style: (*style).scale(sf),
104 },
105 DrawCommand::Path { data, style } => DrawCommand::Path {
106 data: Arc::new(scale_path_data(data, sf)),
107 style: Arc::new((**style).scale(sf)),
108 },
109 DrawCommand::PushClip { rect, radius } => DrawCommand::PushClip {
110 rect: rect.scale(sf),
111 radius: (*radius).scale(sf),
112 },
113 DrawCommand::PopClip => DrawCommand::PopClip,
114 DrawCommand::PushMatrix { matrix } => DrawCommand::PushMatrix {
115 matrix: [
117 matrix[0],
118 matrix[1],
119 matrix[2],
120 matrix[3],
121 matrix[4] * sf,
122 matrix[5] * sf,
123 ],
124 },
125 DrawCommand::PopMatrix => DrawCommand::PopMatrix,
126 DrawCommand::PushLayer {
127 opacity,
128 backdrop_blur,
129 } => DrawCommand::PushLayer {
130 opacity: *opacity,
131 backdrop_blur: backdrop_blur * sf,
132 },
133 DrawCommand::PopLayer => DrawCommand::PopLayer,
134 }
135}
136
137#[derive(Default)]
142pub struct ScaleScratch {
143 storage: Vec<DrawCommand>,
144 rect_styles: FxHashMap<usize, Arc<RectStyle>>,
145 text_styles: FxHashMap<usize, Arc<TextStyle>>,
146 path_styles: FxHashMap<usize, Arc<PathStyle>>,
147 path_data: FxHashMap<usize, Arc<PathData>>,
148}
149
150impl ScaleScratch {
151 pub fn new() -> Self {
152 Self::default()
153 }
154
155 pub fn scale_into(&mut self, commands: &[DrawCommand], sf: f32) -> &[DrawCommand] {
159 let Self {
161 storage,
162 rect_styles,
163 text_styles,
164 path_styles,
165 path_data,
166 } = self;
167 storage.clear();
168 rect_styles.clear();
169 text_styles.clear();
170 path_styles.clear();
171 path_data.clear();
172 storage.reserve(commands.len());
173 for cmd in commands {
174 storage.push(scale_command_cached(
175 cmd,
176 sf,
177 rect_styles,
178 text_styles,
179 path_styles,
180 path_data,
181 ));
182 }
183 storage
184 }
185}
186
187#[inline]
188fn scaled_style_arc<T: Scale + Copy>(
189 cache: &mut FxHashMap<usize, Arc<T>>,
190 style: &Arc<T>,
191 sf: f32,
192) -> Arc<T> {
193 let key = Arc::as_ptr(style) as usize;
194 cache
195 .entry(key)
196 .or_insert_with(|| Arc::new((**style).scale(sf)))
197 .clone()
198}
199
200#[inline]
201fn scaled_path_arc(
202 cache: &mut FxHashMap<usize, Arc<PathData>>,
203 data: &Arc<PathData>,
204 sf: f32,
205) -> Arc<PathData> {
206 let key = Arc::as_ptr(data) as usize;
207 cache
208 .entry(key)
209 .or_insert_with(|| Arc::new(scale_path_data(data, sf)))
210 .clone()
211}
212
213fn scale_command_cached(
214 cmd: &DrawCommand,
215 sf: f32,
216 rect_styles: &mut FxHashMap<usize, Arc<RectStyle>>,
217 text_styles: &mut FxHashMap<usize, Arc<TextStyle>>,
218 path_styles: &mut FxHashMap<usize, Arc<PathStyle>>,
219 path_data: &mut FxHashMap<usize, Arc<PathData>>,
220) -> DrawCommand {
221 match cmd {
222 DrawCommand::Rect { rect, style } => DrawCommand::Rect {
223 rect: rect.scale(sf),
224 style: scaled_style_arc(rect_styles, style, sf),
225 },
226 DrawCommand::Text { text, rect, style } => DrawCommand::Text {
227 text: text.clone(),
228 rect: rect.scale(sf),
229 style: scaled_style_arc(text_styles, style, sf),
230 },
231 DrawCommand::Path { data, style } => DrawCommand::Path {
232 data: scaled_path_arc(path_data, data, sf),
233 style: scaled_style_arc(path_styles, style, sf),
234 },
235 other => scale_command(other, sf),
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use crate::{BorderRadius, RectStyle};
244 use geometry_core::Rect;
245
246 fn rect_cmd(style: &Arc<RectStyle>, x: f32) -> DrawCommand {
247 DrawCommand::Rect {
248 rect: Rect::new(x, 0.0, 10.0, 10.0),
249 style: style.clone(),
250 }
251 }
252
253 #[test]
254 fn scale_into_correctly_scales_and_shares_arcs() {
255 let style = Arc::new(RectStyle::default().with_radius(BorderRadius::all(4.0)));
256 let cmds = vec![
257 rect_cmd(&style, 0.0),
258 rect_cmd(&style, 20.0),
259 rect_cmd(&style, 40.0),
260 ];
261 let sf = 3.0;
262
263 let mut scratch = ScaleScratch::new();
264 let got = scratch.scale_into(&cmds, sf);
265
266 assert_eq!(got.len(), 3);
267
268 for (i, cmd) in got.iter().enumerate() {
269 match cmd {
270 DrawCommand::Rect { rect, style: _ } => {
271 assert_eq!(rect.x, (i as f32) * 20.0 * sf);
272 assert_eq!(rect.y, 0.0);
273 assert_eq!(rect.width, 30.0);
274 assert_eq!(rect.height, 30.0);
275 }
276 other => panic!("expected Rect, got {other:?}"),
277 }
278 }
279
280 let style_of = |c: &DrawCommand| match c {
282 DrawCommand::Rect { style, .. } => style.clone(),
283 _ => unreachable!(),
284 };
285 let a0 = style_of(&got[0]);
286 let a1 = style_of(&got[1]);
287 let a2 = style_of(&got[2]);
288 assert!(Arc::ptr_eq(&a0, &a1));
289 assert!(Arc::ptr_eq(&a1, &a2));
290 assert_eq!(a0.radius.top_left, 12.0);
291 }
292
293 #[test]
294 fn scale_into_reuses_buffer_across_frames() {
295 let style = Arc::new(RectStyle::default());
296 let cmds = vec![rect_cmd(&style, 0.0), rect_cmd(&style, 10.0)];
297 let mut scratch = ScaleScratch::new();
298 let _ = scratch.scale_into(&cmds, 2.0);
299 let cap = scratch.storage.capacity();
300 assert!(cap >= cmds.len());
301 let _ = scratch.scale_into(&cmds, 2.0);
302 assert_eq!(scratch.storage.capacity(), cap);
304 }
305}