| 1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
| 2 | // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 |
| 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 | if let Some("FIT" ) = p.scaleMode.as_deref() { |
| 242 | writeln!(rc, " image-fit: contain;" )? |
| 243 | } |
| 244 | writeln!(rc, " }}" )?; |
| 245 | } |
| 246 | } |
| 247 | if vector.isMask { |
| 248 | writeln!(rc, "Clip {{" )?; |
| 249 | rc.indent += 1; |
| 250 | } |
| 251 | Ok(vector.isMask) |
| 252 | } |
| 253 | |
| 254 | fn render_line( |
| 255 | vector: &VectorNode, |
| 256 | rc: &mut Ctx, |
| 257 | _doc: &Document, |
| 258 | ) -> Result<(), Box<dyn std::error::Error>> { |
| 259 | let mut bb: Rectangle = vector.absoluteBoundingBox; |
| 260 | if bb.height == 0. { |
| 261 | bb.y -= vector.strokeWeight; |
| 262 | bb.height += vector.strokeWeight; |
| 263 | } |
| 264 | if bb.width == 0. { |
| 265 | bb.x -= vector.strokeWeight; |
| 266 | bb.width += vector.strokeWeight; |
| 267 | } |
| 268 | |
| 269 | rc.begin_element(element:"Rectangle" , &vector.node, absoluteBoundingBox:Some(&bb))?; |
| 270 | for p: &Paint in vector.strokes.iter() { |
| 271 | handle_paint(p, rc, arg:"background" )?; |
| 272 | } |
| 273 | rc.end_element()?; |
| 274 | Ok(()) |
| 275 | } |
| 276 | |
| 277 | fn render_node( |
| 278 | node: &figmatypes::Node, |
| 279 | rc: &mut Ctx, |
| 280 | doc: &Document, |
| 281 | ) -> Result<(), Box<dyn std::error::Error>> { |
| 282 | let prev_ctx = (rc.indent, rc.offset); |
| 283 | let is_mask = match node { |
| 284 | Node::FRAME(f) => render_frame(f, rc)?, |
| 285 | Node::GROUP(f) => render_frame(f, rc)?, |
| 286 | Node::COMPONENT(f) => render_frame(f, rc)?, |
| 287 | // Node::INSTANCE { frame } => frame, |
| 288 | Node::VECTOR(vector) => render_vector(vector, rc, doc)?, |
| 289 | Node::BOOLEAN_OPERATION { vector, .. } => render_vector(vector, rc, doc)?, |
| 290 | Node::STAR(vector) => render_vector(vector, rc, doc)?, |
| 291 | Node::LINE(vector) => { |
| 292 | render_line(vector, rc, doc)?; |
| 293 | false |
| 294 | } |
| 295 | Node::ELLIPSE(vector) => render_vector(vector, rc, doc)?, |
| 296 | Node::REGULAR_POLYGON(vector) => render_vector(vector, rc, doc)?, |
| 297 | Node::RECTANGLE { vector, cornerRadius, .. } => { |
| 298 | render_rectangle(vector, cornerRadius, rc, doc)? |
| 299 | } |
| 300 | Node::TEXT { vector, characters, style, .. } => { |
| 301 | render_text(characters, style, vector, rc)?; |
| 302 | false |
| 303 | } |
| 304 | _ => false, |
| 305 | }; |
| 306 | |
| 307 | for x in node.common().children.iter() { |
| 308 | render_node(x, rc, doc)?; |
| 309 | } |
| 310 | |
| 311 | if is_mask { |
| 312 | return Ok(()); |
| 313 | } |
| 314 | |
| 315 | while rc.indent != prev_ctx.0 { |
| 316 | rc.indent -= 1; |
| 317 | writeln!(rc, " }}" )?; |
| 318 | } |
| 319 | rc.offset = prev_ctx.1; |
| 320 | |
| 321 | Ok(()) |
| 322 | } |
| 323 | |
| 324 | fn handle_paint(p: &Paint, rc: &mut Ctx, arg: &str) -> Result<bool, Box<dyn std::error::Error>> { |
| 325 | if !p.visible { |
| 326 | return Ok(false); |
| 327 | } |
| 328 | let mut has_something = false; |
| 329 | if !p.gradientStops.is_empty() { |
| 330 | if p.r#type != "GRADIENT_LINEAR" { |
| 331 | eprintln!("Warning: unsupported paint type {:?}" , p.r#type); |
| 332 | return Ok(false); |
| 333 | } |
| 334 | let p1 = *p |
| 335 | .gradientHandlePositions |
| 336 | .first() |
| 337 | .ok_or_else(|| "Gradient with missing 'gradientHandlePositions'" .to_string())?; |
| 338 | let p2 = *p |
| 339 | .gradientHandlePositions |
| 340 | .get(1) |
| 341 | .ok_or_else(|| "Gradient with missing 'gradientHandlePositions'" .to_string())?; |
| 342 | let sub = p1 - p2; |
| 343 | write!(rc, " {}: @linear-gradient( {}deg" , arg, -f32::atan2(sub.x, sub.y).to_degrees())?; |
| 344 | for stop in &p.gradientStops { |
| 345 | write!(rc, ", {} {}" , stop.color, stop.position)?; |
| 346 | } |
| 347 | writeln!(rc, ");" )?; |
| 348 | has_something = true; |
| 349 | } else if let Some(color) = &p.color { |
| 350 | if !color.is_transparent() { |
| 351 | writeln!(rc, " {arg}: {color};" )?; |
| 352 | has_something = true; |
| 353 | } |
| 354 | } |
| 355 | Ok(has_something) |
| 356 | } |
| 357 | |