1use std::sync::Arc;
2
3use rosace_core::types::{Point, Rect, Size};
4
5use crate::canvas::Color;
6use crate::font::FontWeight;
7
8#[derive(Debug, Clone)]
14pub enum DrawCommand {
15 FillRect { rect: Rect, color: Color },
16 StrokeRect { rect: Rect, color: Color, width: f32 },
17 FillRRect { rect: Rect, radius: f32, color: Color },
19 StrokeRRect { rect: Rect, radius: f32, color: Color, width: f32 },
22 FillCircle { center: Point, radius: f32, color: Color },
23 FillGradient { rect: Rect, radius: f32, from: Color, to: Color, vertical: bool },
26 FillArc { center: Point, radius: f32, thickness: f32, start_deg: f32, sweep_deg: f32, color: Color },
30 DrawText { text: String, origin: Point, color: Color, px: f32, weight: FontWeight },
31 DrawShadow { rect: Rect, radius: f32, color: Color, blur: f32 },
35 BlitRgba { pixels: Arc<Vec<u8>>, src_width: u32, src_height: u32, dest_rect: Rect, opacity: f32 },
39 PushClip { rect: Rect },
42 PopClip,
44 BackdropBlur { rect: Rect, radius: f32, blur: f32, tint: Color },
50 ShaderFill { pipeline_id: u64, rect: Rect, uniforms: Vec<u8>, animate_time: bool },
66}
67
68impl DrawCommand {
69 pub fn offset(&self, dx: f32, dy: f32) -> Self {
71 fn shift(r: Rect, dx: f32, dy: f32) -> Rect {
72 Rect {
73 origin: Point { x: r.origin.x + dx, y: r.origin.y + dy },
74 size: r.size,
75 }
76 }
77 match self.clone() {
78 Self::FillRect { rect, color } => Self::FillRect { rect: shift(rect, dx, dy), color },
79 Self::StrokeRect { rect, color, width } => Self::StrokeRect { rect: shift(rect, dx, dy), color, width },
80 Self::FillRRect { rect, radius, color } => Self::FillRRect { rect: shift(rect, dx, dy), radius, color },
81 Self::StrokeRRect { rect, radius, color, width } => Self::StrokeRRect { rect: shift(rect, dx, dy), radius, color, width },
82 Self::FillCircle { center, radius, color } => Self::FillCircle { center: Point { x: center.x + dx, y: center.y + dy }, radius, color },
83 Self::FillGradient { rect, radius, from, to, vertical } => Self::FillGradient { rect: shift(rect, dx, dy), radius, from, to, vertical },
84 Self::FillArc { center, radius, thickness, start_deg, sweep_deg, color } => Self::FillArc { center: Point { x: center.x + dx, y: center.y + dy }, radius, thickness, start_deg, sweep_deg, color },
85 Self::DrawText { text, origin, color, px, weight } => Self::DrawText { text, origin: Point { x: origin.x + dx, y: origin.y + dy }, color, px, weight },
86 Self::DrawShadow { rect, radius, color, blur } => Self::DrawShadow { rect: shift(rect, dx, dy), radius, color, blur },
87 Self::BlitRgba { pixels, src_width, src_height, dest_rect, opacity } =>
88 Self::BlitRgba { pixels, src_width, src_height, dest_rect: shift(dest_rect, dx, dy), opacity },
89 Self::PushClip { rect } => Self::PushClip { rect: shift(rect, dx, dy) },
90 Self::PopClip => Self::PopClip,
91 Self::ShaderFill { pipeline_id, rect, uniforms, animate_time } =>
92 Self::ShaderFill { pipeline_id, rect: shift(rect, dx, dy), uniforms, animate_time },
93 Self::BackdropBlur { rect, radius, blur, tint } =>
94 Self::BackdropBlur { rect: shift(rect, dx, dy), radius, blur, tint },
95 }
96 }
97
98 pub fn morph(&self, src_origin: Point, dst_origin: Point, sx: f32, sy: f32) -> Self {
108 let s = (sx + sy) * 0.5;
109 fn pt(p: Point, so: Point, do_: Point, sx: f32, sy: f32) -> Point {
110 Point { x: do_.x + (p.x - so.x) * sx, y: do_.y + (p.y - so.y) * sy }
111 }
112 fn rc(r: Rect, so: Point, do_: Point, sx: f32, sy: f32) -> Rect {
113 Rect {
114 origin: pt(r.origin, so, do_, sx, sy),
115 size: Size { width: r.size.width * sx, height: r.size.height * sy },
116 }
117 }
118 match self.clone() {
119 Self::FillRect { rect, color } => Self::FillRect { rect: rc(rect, src_origin, dst_origin, sx, sy), color },
120 Self::StrokeRect { rect, color, width } => Self::StrokeRect { rect: rc(rect, src_origin, dst_origin, sx, sy), color, width: width * s },
121 Self::FillRRect { rect, radius, color } => Self::FillRRect { rect: rc(rect, src_origin, dst_origin, sx, sy), radius: radius * s, color },
122 Self::StrokeRRect { rect, radius, color, width } => Self::StrokeRRect { rect: rc(rect, src_origin, dst_origin, sx, sy), radius: radius * s, color, width: width * s },
123 Self::FillCircle { center, radius, color } => Self::FillCircle { center: pt(center, src_origin, dst_origin, sx, sy), radius: radius * s, color },
124 Self::FillGradient { rect, radius, from, to, vertical } => Self::FillGradient { rect: rc(rect, src_origin, dst_origin, sx, sy), radius: radius * s, from, to, vertical },
125 Self::FillArc { center, radius, thickness, start_deg, sweep_deg, color } => Self::FillArc { center: pt(center, src_origin, dst_origin, sx, sy), radius: radius * s, thickness: thickness * s, start_deg, sweep_deg, color },
126 Self::DrawText { text, origin, color, px, weight } => Self::DrawText { text, origin: pt(origin, src_origin, dst_origin, sx, sy), color, px: px * s, weight },
127 Self::DrawShadow { rect, radius, color, blur } => Self::DrawShadow { rect: rc(rect, src_origin, dst_origin, sx, sy), radius: radius * s, color, blur: blur * s },
128 Self::BlitRgba { pixels, src_width, src_height, dest_rect, opacity } =>
129 Self::BlitRgba { pixels, src_width, src_height, dest_rect: rc(dest_rect, src_origin, dst_origin, sx, sy), opacity },
130 Self::PushClip { rect } => Self::PushClip { rect: rc(rect, src_origin, dst_origin, sx, sy) },
131 Self::PopClip => Self::PopClip,
132 Self::ShaderFill { pipeline_id, rect, uniforms, animate_time } =>
137 Self::ShaderFill { pipeline_id, rect: rc(rect, src_origin, dst_origin, sx, sy), uniforms, animate_time },
138 Self::BackdropBlur { rect, radius, blur, tint } =>
139 Self::BackdropBlur { rect: rc(rect, src_origin, dst_origin, sx, sy), radius: radius * s, blur: blur * s, tint },
140 }
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 fn rect(x: f32, y: f32, w: f32, h: f32) -> Rect {
149 Rect { origin: Point { x, y }, size: Size { width: w, height: h } }
150 }
151
152 #[test]
153 fn morph_maps_a_rect_from_its_source_origin_to_the_destination_origin_and_size() {
154 let cmd = DrawCommand::FillRect { rect: rect(0.0, 0.0, 100.0, 100.0), color: Color::RED };
157 let morphed = cmd.morph(Point { x: 0.0, y: 0.0 }, Point { x: 200.0, y: 50.0 }, 0.4, 0.4);
158 match morphed {
159 DrawCommand::FillRect { rect: r, .. } => {
160 assert!((r.origin.x - 200.0).abs() < 0.001);
161 assert!((r.origin.y - 50.0).abs() < 0.001);
162 assert!((r.size.width - 40.0).abs() < 0.001, "100 * 0.4 = 40, got {}", r.size.width);
163 assert!((r.size.height - 40.0).abs() < 0.001);
164 }
165 other => panic!("expected FillRect, got {other:?}"),
166 }
167 }
168
169 #[test]
170 fn morph_scales_geometry_nested_inside_the_captured_rect_proportionally() {
171 let cmd = DrawCommand::FillCircle { center: Point { x: 50.0, y: 50.0 }, radius: 10.0, color: Color::RED };
175 let morphed = cmd.morph(Point { x: 0.0, y: 0.0 }, Point { x: 200.0, y: 50.0 }, 0.4, 0.4);
176 match morphed {
177 DrawCommand::FillCircle { center, radius, .. } => {
178 assert!((center.x - 220.0).abs() < 0.001, "expected 200 + 50*0.4 = 220, got {}", center.x);
179 assert!((center.y - 70.0).abs() < 0.001, "expected 50 + 50*0.4 = 70, got {}", center.y);
180 assert!((radius - 4.0).abs() < 0.001, "10 * 0.4 = 4, got {}", radius);
181 }
182 other => panic!("expected FillCircle, got {other:?}"),
183 }
184 }
185
186 #[test]
187 fn morph_at_identity_scale_and_matching_origins_is_a_no_op() {
188 let cmd = DrawCommand::FillRect { rect: rect(10.0, 20.0, 30.0, 40.0), color: Color::RED };
189 let morphed = cmd.morph(Point { x: 10.0, y: 20.0 }, Point { x: 10.0, y: 20.0 }, 1.0, 1.0);
190 match morphed {
191 DrawCommand::FillRect { rect: r, .. } => {
192 assert_eq!(r.origin.x, 10.0);
193 assert_eq!(r.origin.y, 20.0);
194 assert_eq!(r.size.width, 30.0);
195 assert_eq!(r.size.height, 40.0);
196 }
197 other => panic!("expected FillRect, got {other:?}"),
198 }
199 }
200}