1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
2 | // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial |
3 | |
4 | use crate::figmatypes::{self, *}; |
5 | use std::collections::HashMap; |
6 | use std::fmt::Display; |
7 | use std::fmt::Write; |
8 | |
9 | pub struct Document<'doc> { |
10 | pub nodeHash: HashMap<&'doc str, &'doc figmatypes::Node>, |
11 | //pub images: HashMap<String, Vec<u8>>, |
12 | } |
13 | |
14 | #[derive (Default, Clone, Copy, PartialEq, Eq)] |
15 | struct Indent(pub u32); |
16 | |
17 | impl std::ops::SubAssign<u32> for Indent { |
18 | fn sub_assign(&mut self, rhs: u32) { |
19 | self.0 -= rhs; |
20 | } |
21 | } |
22 | |
23 | impl std::ops::AddAssign<u32> for Indent { |
24 | fn add_assign(&mut self, rhs: u32) { |
25 | self.0 += rhs; |
26 | } |
27 | } |
28 | |
29 | impl Display for Indent { |
30 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
31 | for _ in 0..self.0 { |
32 | write!(f, " " )? |
33 | } |
34 | Ok(()) |
35 | } |
36 | } |
37 | |
38 | #[derive (Default)] |
39 | struct Ctx { |
40 | out: String, |
41 | indent: Indent, |
42 | offset: Vector, |
43 | } |
44 | |
45 | impl Ctx { |
46 | fn begin_element( |
47 | &mut self, |
48 | element: &str, |
49 | node: &NodeCommon, |
50 | absoluteBoundingBox: Option<&Rectangle>, |
51 | ) -> std::fmt::Result { |
52 | writeln!( |
53 | self, |
54 | "id_ {} := {} {{ /* {} */" , |
55 | node.id.replace(':' , "-" ).replace(';' , "_" ), |
56 | element, |
57 | node.name |
58 | )?; |
59 | self.indent += 1; |
60 | if let Some(bb) = absoluteBoundingBox { |
61 | writeln!(self, "width: {}px;" , bb.width)?; |
62 | writeln!(self, "height: {}px;" , bb.height)?; |
63 | writeln!(self, "x: {}px;" , bb.x - self.offset.x)?; |
64 | writeln!(self, "y: {}px;" , bb.y - self.offset.y)?; |
65 | } |
66 | Ok(()) |
67 | } |
68 | |
69 | fn end_element(&mut self) -> std::fmt::Result { |
70 | self.indent -= 1; |
71 | writeln!(self, " }}" ) |
72 | } |
73 | } |
74 | |
75 | impl Write for Ctx { |
76 | fn write_str(&mut self, s: &str) -> std::fmt::Result { |
77 | if self.out.as_bytes().last() == Some(&b' \n' ) { |
78 | write!(self.out, " {}" , self.indent)?; |
79 | } |
80 | self.out.push_str(string:s); |
81 | Ok(()) |
82 | } |
83 | } |
84 | |
85 | impl Display for Color { |
86 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
87 | write!( |
88 | f, |
89 | "# {:02x}{:02x}{:02x}{:02x}" , |
90 | (self.r * 255.0) as u8, |
91 | (self.g * 255.0) as u8, |
92 | (self.b * 255.0) as u8, |
93 | (self.a * 255.0) as u8 |
94 | ) |
95 | } |
96 | } |
97 | |
98 | pub fn render( |
99 | _name: &str, |
100 | node: &Node, |
101 | background: Color, |
102 | doc: &Document, |
103 | ) -> Result<String, Box<dyn std::error::Error>> { |
104 | /*= match node { |
105 | Node::FRAME(Frame { absoluteBoundingBox, .. }) => absoluteBoundingBox, |
106 | Node::GROUP(Frame { absoluteBoundingBox, .. }) => absoluteBoundingBox, |
107 | Node::COMPONENT(Frame { absoluteBoundingBox, .. }) => absoluteBoundingBox, |
108 | Node::INSTANCE { |
109 | frame: Frame { absoluteBoundingBox, .. }, |
110 | .. |
111 | } => absoluteBoundingBox, |
112 | _ => return Err(super::Error("Rendering not a frame".into()).into()) |
113 | };*/ |
114 | let frame = match node { |
115 | Node::FRAME(f) => f, |
116 | Node::GROUP(f) => f, |
117 | Node::COMPONENT(f) => f, |
118 | // Node::INSTANCE { frame } => frame, |
119 | _ => return Err(super::Error("Rendering not a frame" .into()).into()), |
120 | }; |
121 | |
122 | let mut ctx = Ctx::default(); |
123 | writeln!(ctx, "App := Window {{" )?; |
124 | ctx.indent += 1; |
125 | writeln!(ctx, "background: {};" , background)?; |
126 | writeln!(ctx, "width: {}px;" , frame.absoluteBoundingBox.width)?; |
127 | writeln!(ctx, "height: {}px;" , frame.absoluteBoundingBox.height)?; |
128 | ctx.offset = frame.absoluteBoundingBox.origin(); |
129 | render_node(node, &mut ctx, doc)?; |
130 | ctx.end_element()?; |
131 | |
132 | Ok(ctx.out) |
133 | } |
134 | |
135 | fn render_frame(frame: &Frame, rc: &mut Ctx) -> Result<bool, Box<dyn std::error::Error>> { |
136 | rc.begin_element(element:"Rectangle" , &frame.node, absoluteBoundingBox:Some(&frame.absoluteBoundingBox))?; |
137 | rc.offset = frame.absoluteBoundingBox.origin(); |
138 | let mut has_background: bool = false; |
139 | for p: &Paint in frame.background.iter() { |
140 | has_background |= handle_paint(p, rc, arg:"background" )?; |
141 | } |
142 | if !has_background && !frame.backgroundColor.is_transparent() { |
143 | writeln!(rc, "background: {};" , frame.backgroundColor)?; |
144 | } |
145 | if frame.clipsContent || frame.isMask { |
146 | writeln!(rc, "clip: true;" )?; |
147 | } |
148 | Ok(frame.isMask) |
149 | } |
150 | |
151 | fn render_vector( |
152 | vector: &VectorNode, |
153 | rc: &mut Ctx, |
154 | _doc: &Document, |
155 | ) -> Result<bool, Box<dyn std::error::Error>> { |
156 | if !vector.fillGeometry.is_empty() || !vector.strokeGeometry.is_empty() { |
157 | for p in vector.fillGeometry.iter().chain(vector.strokeGeometry.iter()) { |
158 | rc.begin_element("Path" , &vector.node, Some(&vector.absoluteBoundingBox))?; |
159 | writeln!(rc, "commands: \"{}\";" , p.path)?; |
160 | writeln!(rc, "fill-rule: {};" , p.windingRule.to_ascii_lowercase())?; |
161 | if vector.strokeWeight > 0. { |
162 | writeln!(rc, "stroke-width: {}px;" , vector.strokeWeight)?; |
163 | } |
164 | for p in vector.strokes.iter() { |
165 | handle_paint(p, rc, "stroke" )?; |
166 | } |
167 | for p in vector.fills.iter() { |
168 | handle_paint(p, rc, "fill" )?; |
169 | if let Some(_imr) = &p.imageRef { /* */ } |
170 | } |
171 | rc.end_element()?; |
172 | } |
173 | return Ok(false); |
174 | } |
175 | |
176 | for p in vector.fills.iter() { |
177 | if let Some(color) = &p.color { |
178 | if !color.is_transparent() { |
179 | rc.begin_element("Rectangle" , &vector.node, Some(&vector.absoluteBoundingBox))?; |
180 | handle_paint(p, rc, "background" )?; |
181 | rc.end_element()?; |
182 | } |
183 | } |
184 | if let Some(imr) = &p.imageRef { |
185 | rc.begin_element("Image" , &vector.node, Some(&vector.absoluteBoundingBox))?; |
186 | writeln!(rc, "source: @image-url( \"images/ {}\");" , imr.escape_debug())?; |
187 | rc.end_element()?; |
188 | } |
189 | } |
190 | Ok(false) |
191 | } |
192 | |
193 | fn render_text( |
194 | text: &str, |
195 | font: &TypeStyle, |
196 | vector: &VectorNode, |
197 | rc: &mut Ctx, |
198 | ) -> Result<(), Box<dyn std::error::Error>> { |
199 | rc.begin_element(element:"Text" , &vector.node, absoluteBoundingBox:Some(&vector.absoluteBoundingBox))?; |
200 | writeln!(rc, "text: \"{}\";" , text.escape_debug())?; |
201 | writeln!(rc, "font-family: \"{}\";" , font.fontFamily)?; |
202 | writeln!(rc, "font-size: {}px;" , font.fontSize)?; |
203 | writeln!(rc, "font-weight: {};" , font.fontWeight)?; |
204 | writeln!(rc, "horizontal-alignment: {};" , font.textAlignHorizontal.to_ascii_lowercase())?; |
205 | writeln!(rc, "vertical-alignment: {};" , font.textAlignVertical.to_ascii_lowercase())?; |
206 | writeln!(rc, "letter-spacing: {}px;" , font.letterSpacing)?; |
207 | for p: &Paint in vector.fills.iter() { |
208 | handle_paint(p, rc, arg:"color" )?; |
209 | } |
210 | rc.end_element()?; |
211 | Ok(()) |
212 | } |
213 | |
214 | fn render_rectangle( |
215 | vector: &VectorNode, |
216 | cornerRadius: &Option<f32>, |
217 | rc: &mut Ctx, |
218 | _doc: &Document, |
219 | ) -> Result<bool, Box<dyn std::error::Error>> { |
220 | rc.begin_element("Rectangle" , &vector.node, Some(&vector.absoluteBoundingBox))?; |
221 | rc.offset = vector.absoluteBoundingBox.origin(); |
222 | if let Some(cornerRadius) = cornerRadius { |
223 | // Note that figma rendering when the cornerRadius > min(height,width)/2 is different |
224 | // than ours, so we adjust it there |
225 | let min_edge = vector.absoluteBoundingBox.width.min(vector.absoluteBoundingBox.height); |
226 | writeln!(rc, "border-radius: {}px;" , cornerRadius.min(min_edge / 2.))?; |
227 | } |
228 | let mut has_border = false; |
229 | for p in vector.strokes.iter() { |
230 | has_border |= handle_paint(p, rc, "border-color" )?; |
231 | } |
232 | if vector.strokeWeight > 0. && has_border { |
233 | writeln!(rc, "border-width: {}px;" , vector.strokeWeight)?; |
234 | } |
235 | for p in vector.fills.iter() { |
236 | handle_paint(p, rc, "background" )?; |
237 | if let Some(imr) = &p.imageRef { |
238 | writeln!(rc, "Image {{" )?; |
239 | writeln!(rc, " width: 100%; height: 100%;" )?; |
240 | writeln!(rc, " source: @image-url( \"images/ {}\");" , imr.escape_debug())?; |
241 | match p.scaleMode.as_deref() { |
242 | Some("FIT" ) => writeln!(rc, " image-fit: contain;" )?, |
243 | _ => (), |
244 | } |
245 | writeln!(rc, " }}" )?; |
246 | } |
247 | } |
248 | if vector.isMask { |
249 | writeln!(rc, "Clip {{" )?; |
250 | rc.indent += 1; |
251 | } |
252 | Ok(vector.isMask) |
253 | } |
254 | |
255 | fn render_line( |
256 | vector: &VectorNode, |
257 | rc: &mut Ctx, |
258 | _doc: &Document, |
259 | ) -> Result<(), Box<dyn std::error::Error>> { |
260 | let mut bb: Rectangle = vector.absoluteBoundingBox; |
261 | if bb.height == 0. { |
262 | bb.y -= vector.strokeWeight; |
263 | bb.height += vector.strokeWeight; |
264 | } |
265 | if bb.width == 0. { |
266 | bb.x -= vector.strokeWeight; |
267 | bb.width += vector.strokeWeight; |
268 | } |
269 | |
270 | rc.begin_element(element:"Rectangle" , &vector.node, absoluteBoundingBox:Some(&bb))?; |
271 | for p: &Paint in vector.strokes.iter() { |
272 | handle_paint(p, rc, arg:"background" )?; |
273 | } |
274 | rc.end_element()?; |
275 | Ok(()) |
276 | } |
277 | |
278 | fn render_node( |
279 | node: &figmatypes::Node, |
280 | rc: &mut Ctx, |
281 | doc: &Document, |
282 | ) -> Result<(), Box<dyn std::error::Error>> { |
283 | let prev_ctx = (rc.indent, rc.offset); |
284 | let is_mask = match node { |
285 | Node::FRAME(f) => render_frame(f, rc)?, |
286 | Node::GROUP(f) => render_frame(f, rc)?, |
287 | Node::COMPONENT(f) => render_frame(f, rc)?, |
288 | // Node::INSTANCE { frame } => frame, |
289 | Node::VECTOR(vector) => render_vector(vector, rc, doc)?, |
290 | Node::BOOLEAN_OPERATION { vector, .. } => render_vector(vector, rc, doc)?, |
291 | Node::STAR(vector) => render_vector(vector, rc, doc)?, |
292 | Node::LINE(vector) => { |
293 | render_line(vector, rc, doc)?; |
294 | false |
295 | } |
296 | Node::ELLIPSE(vector) => render_vector(vector, rc, doc)?, |
297 | Node::REGULAR_POLYGON(vector) => render_vector(vector, rc, doc)?, |
298 | Node::RECTANGLE { vector, cornerRadius, .. } => { |
299 | render_rectangle(vector, cornerRadius, rc, doc)? |
300 | } |
301 | Node::TEXT { vector, characters, style, .. } => { |
302 | render_text(characters, style, vector, rc)?; |
303 | false |
304 | } |
305 | _ => false, |
306 | }; |
307 | |
308 | for x in node.common().children.iter() { |
309 | render_node(x, rc, doc)?; |
310 | } |
311 | |
312 | if is_mask { |
313 | return Ok(()); |
314 | } |
315 | |
316 | while rc.indent != prev_ctx.0 { |
317 | rc.indent -= 1; |
318 | writeln!(rc, " }}" )?; |
319 | } |
320 | rc.offset = prev_ctx.1; |
321 | |
322 | Ok(()) |
323 | } |
324 | |
325 | fn handle_paint(p: &Paint, rc: &mut Ctx, arg: &str) -> Result<bool, Box<dyn std::error::Error>> { |
326 | if !p.visible { |
327 | return Ok(false); |
328 | } |
329 | let mut has_something = false; |
330 | if !p.gradientStops.is_empty() { |
331 | if p.r#type != "GRADIENT_LINEAR" { |
332 | eprintln!("Warning: unsupported paint type {:?}" , p.r#type); |
333 | return Ok(false); |
334 | } |
335 | let p1 = *p |
336 | .gradientHandlePositions |
337 | .first() |
338 | .ok_or_else(|| "Gradient with missing 'gradientHandlePositions'" .to_string())?; |
339 | let p2 = *p |
340 | .gradientHandlePositions |
341 | .get(1) |
342 | .ok_or_else(|| "Gradient with missing 'gradientHandlePositions'" .to_string())?; |
343 | let sub = p1 - p2; |
344 | write!(rc, " {}: @linear-gradient( {}deg" , arg, -f32::atan2(sub.x, sub.y).to_degrees())?; |
345 | for stop in &p.gradientStops { |
346 | write!(rc, ", {} {}" , stop.color, stop.position)?; |
347 | } |
348 | writeln!(rc, ");" )?; |
349 | has_something = true; |
350 | } else if let Some(color) = &p.color { |
351 | if !color.is_transparent() { |
352 | writeln!(rc, " {}: {};" , arg, color)?; |
353 | has_something = true; |
354 | } |
355 | } |
356 | Ok(has_something) |
357 | } |
358 | |