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
4use crate::figmatypes::{self, *};
5use std::collections::HashMap;
6use std::fmt::Display;
7use std::fmt::Write;
8
9pub 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)]
15struct Indent(pub u32);
16
17impl std::ops::SubAssign<u32> for Indent {
18 fn sub_assign(&mut self, rhs: u32) {
19 self.0 -= rhs;
20 }
21}
22
23impl std::ops::AddAssign<u32> for Indent {
24 fn add_assign(&mut self, rhs: u32) {
25 self.0 += rhs;
26 }
27}
28
29impl 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)]
39struct Ctx {
40 out: String,
41 indent: Indent,
42 offset: Vector,
43}
44
45impl 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
75impl 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
85impl 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
98pub 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
135fn 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
151fn 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
193fn 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
214fn 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
255fn 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
278fn 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
325fn 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