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