1use ui_layout::Style;
4
5use crate::engine::layouter::types::{Color, TextFlowStyle, TextStyle};
6use crate::engine::renderer_model::{Brush, DrawCommand, FillRule, Paint, rect_path};
7use crate::engine::ui::custom_node::{ContentSize, CustomNode};
8
9#[derive(Debug, Clone, PartialEq)]
10pub enum CanvasCommand {
11 FillRect {
12 color: Color,
13 x: f32,
14 y: f32,
15 width: f32,
16 height: f32,
17 },
18 ClearRect {
19 x: f32,
20 y: f32,
21 width: f32,
22 height: f32,
23 },
24 StrokeRect {
25 color: Color,
26 x: f32,
27 y: f32,
28 width: f32,
29 height: f32,
30 },
31}
32
33#[derive(Debug)]
34pub struct CanvasComponent {
35 width: f32,
36 height: f32,
37 commands: Vec<CanvasCommand>,
38}
39
40impl CanvasComponent {
41 pub fn new(width: f32, height: f32, source: &str) -> Self {
42 Self {
43 width,
44 height,
45 commands: parse_commands(source),
46 }
47 }
48}
49
50impl CustomNode for CanvasComponent {
51 fn draw_sized(
52 &self,
53 cmd_buf: &mut Vec<DrawCommand>,
54 _text_style: &TextStyle,
55 _text_flow_style: &TextFlowStyle,
56 _style: &Style,
57 size: ContentSize,
58 ) {
59 let scale_x = if self.width > 0.0 {
60 size.width / self.width
61 } else {
62 1.0
63 };
64 let scale_y = if self.height > 0.0 {
65 size.height / self.height
66 } else {
67 1.0
68 };
69 for command in &self.commands {
70 match *command {
71 CanvasCommand::FillRect {
72 color,
73 x,
74 y,
75 width,
76 height,
77 } => draw_rect(cmd_buf, color, x, y, width, height, scale_x, scale_y),
78 CanvasCommand::ClearRect { .. } => {
79 }
82 CanvasCommand::StrokeRect {
83 color,
84 x,
85 y,
86 width,
87 height,
88 } => {
89 let line = 1.0;
90 draw_rect(cmd_buf, color, x, y, width, line, scale_x, scale_y);
91 draw_rect(
92 cmd_buf,
93 color,
94 x,
95 y + height - line,
96 width,
97 line,
98 scale_x,
99 scale_y,
100 );
101 draw_rect(cmd_buf, color, x, y, line, height, scale_x, scale_y);
102 draw_rect(
103 cmd_buf,
104 color,
105 x + width - line,
106 y,
107 line,
108 height,
109 scale_x,
110 scale_y,
111 );
112 }
113 }
114 }
115 }
116
117 fn intrinsic_size(&self) -> ContentSize {
118 ContentSize {
119 width: self.width,
120 height: self.height,
121 }
122 }
123
124 fn role(&self) -> Option<&'static str> {
125 Some("img")
126 }
127}
128
129#[allow(clippy::too_many_arguments)]
130fn draw_rect(
131 cmd_buf: &mut Vec<DrawCommand>,
132 color: Color,
133 x: f32,
134 y: f32,
135 width: f32,
136 height: f32,
137 scale_x: f32,
138 scale_y: f32,
139) {
140 if width <= 0.0 || height <= 0.0 {
141 return;
142 }
143 cmd_buf.push(DrawCommand::Fill {
144 path: rect_path(x * scale_x, y * scale_y, width * scale_x, height * scale_y),
145 paint: Paint {
146 brush: Brush::Solid(color),
147 opacity: 1.0,
148 },
149 rule: FillRule::NonZero,
150 });
151}
152
153fn parse_commands(source: &str) -> Vec<CanvasCommand> {
154 source.lines().filter_map(parse_command).collect()
155}
156
157fn parse_command(source: &str) -> Option<CanvasCommand> {
158 let mut parts = source.split('|');
159 let name = parts.next()?;
160 let color = parse_color(parts.next().unwrap_or(""));
161 let x = parts.next()?.parse().ok()?;
162 let y = parts.next()?.parse().ok()?;
163 let width = parts.next()?.parse().ok()?;
164 let height = parts.next()?.parse().ok()?;
165 match name {
166 "fillRect" => Some(CanvasCommand::FillRect {
167 color: color?,
168 x,
169 y,
170 width,
171 height,
172 }),
173 "clearRect" => Some(CanvasCommand::ClearRect {
174 x,
175 y,
176 width,
177 height,
178 }),
179 "strokeRect" => Some(CanvasCommand::StrokeRect {
180 color: color?,
181 x,
182 y,
183 width,
184 height,
185 }),
186 _ => None,
187 }
188}
189
190fn parse_color(source: &str) -> Option<Color> {
191 let source = source.trim().to_ascii_lowercase();
192 match source.as_str() {
193 "black" => Some(Color(0, 0, 0, 255)),
194 "white" => Some(Color(255, 255, 255, 255)),
195 "red" => Some(Color(255, 0, 0, 255)),
196 "green" => Some(Color(0, 128, 0, 255)),
197 "blue" => Some(Color(0, 0, 255, 255)),
198 "orange" => Some(Color(255, 165, 0, 255)),
199 "transparent" => Some(Color(0, 0, 0, 0)),
200 _ if source.len() == 7 && source.starts_with('#') => Some(Color(
201 u8::from_str_radix(&source[1..3], 16).ok()?,
202 u8::from_str_radix(&source[3..5], 16).ok()?,
203 u8::from_str_radix(&source[5..7], 16).ok()?,
204 255,
205 )),
206 _ if source.len() == 4 && source.starts_with('#') => {
207 let mut digits = source[1..].chars();
208 let expand = |digit: char| u8::from_str_radix(&format!("{digit}{digit}"), 16).ok();
209 Some(Color(
210 expand(digits.next()?)?,
211 expand(digits.next()?)?,
212 expand(digits.next()?)?,
213 255,
214 ))
215 }
216 _ => None,
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn recorded_rectangles_parse_into_canvas_commands() {
226 let canvas = CanvasComponent::new(
227 150.0,
228 100.0,
229 "fillRect|orange|10|10|130|80\nstrokeRect|#0000ff|0|0|150|100",
230 );
231 assert_eq!(canvas.commands.len(), 2);
232 assert!(matches!(
233 canvas.commands[0],
234 CanvasCommand::FillRect {
235 color: Color(255, 165, 0, 255),
236 ..
237 }
238 ));
239 }
240}