Files
figcraft/crates/engine/src/dev/codegen/swift.rs
T
BFlat 2e92d36af0 M6: SwiftUI and Compose code name a bound variable by its code syntax (#1716)
Bound variables with an iOS or Android code syntax are named in generated SwiftUI and Compose in place of their value: paints, stroke weight, sizes, corner radius, gaps, padding, opacity and text. A variable with none for the platform keeps the value.
2026-10-31 06:49:41 +07:00

182 lines
7.5 KiB
Rust

use super::{
CodeLang, Color, Ctx, LayoutMode, Node, NodeId, NodeKind, PADDING, drop_shadow, first_fill, indent, literal, named, num, radius, stroke_color,
};
// ------------------------------------------------------------------ SwiftUI
fn swift_color(c: Color) -> String {
format!("Color(red: {}, green: {}, blue: {}, opacity: {})", num(f64::from(c.r)), num(f64::from(c.g)), num(f64::from(c.b)), num(f64::from(c.a)))
}
fn swift_weight(w: u16) -> &'static str {
match w {
0..=150 => ".thin",
151..=250 => ".ultraLight",
251..=350 => ".light",
351..=450 => ".regular",
451..=550 => ".medium",
551..=650 => ".semibold",
651..=750 => ".bold",
751..=850 => ".heavy",
_ => ".black",
}
}
/// SwiftUI for a layer and its subtree at `depth`; `extra` modifiers go last (a free child's
/// offset). A layer a boolean parameter shows sits in an `if`.
pub(crate) fn swift(ctx: &Ctx, id: NodeId, depth: usize, out: &mut String, extra: &[String]) {
let Some(n) = ctx.doc.node(id) else { return };
if id != ctx.root
&& let Some(p) = ctx.param(n, "visible")
{
let pad = indent(depth);
out.push_str(&format!("{pad}if {} {{\n", p.ident));
swift_layer(ctx, n, depth + 1, out, extra);
out.push_str(&format!("{pad}}}\n"));
return;
}
swift_layer(ctx, n, depth, out, extra);
}
/// A layer name safe for a single-line `//` comment: line breaks become spaces.
pub(super) fn one_line(name: &str) -> String {
name.replace(['\n', '\r', '\u{85}', '\u{2028}', '\u{2029}'], " ")
}
fn swift_layer(ctx: &Ctx, n: &Node, depth: usize, out: &mut String, extra: &[String]) {
let doc = ctx.doc;
let pad = indent(depth);
if ctx.too_deep(depth) {
out.push_str(&format!("{pad}// {} …\n", one_line(&n.name)));
return;
}
// Nested instances are calls; an instance swap parameter is a view.
let call = match ctx.param(n, "mainComponent").filter(|_| n.id != ctx.root) {
Some(p) => Some(format!("{pad}{}\n", p.ident)),
None => ctx.call(n, CodeLang::SwiftUi, depth).map(|(code, _)| code),
};
if let Some(call) = call {
out.push_str(&call);
for m in extra {
out.push_str(&format!("{pad} {m}\n"));
}
return;
}
// A bound variable's iOS code syntax in place of the value.
let bound = |fields: &[&str], value: String| named(doc, n, "iOS", fields, value);
let fill = first_fill(n).map(|c| bound(&["fill"], swift_color(c)));
let (w, h) = (bound(&["width"], num(n.width)), bound(&["height"], num(n.height)));
let corner = bound(&["cornerRadius"], num(radius(n)));
let mut mods: Vec<String> = Vec::new();
match &n.kind {
NodeKind::Text(t) => {
let text = ctx.param(n, "characters").map_or_else(|| bound(&["characters"], literal(&t.characters, false)), |p| p.ident.clone());
out.push_str(&format!("{pad}Text({text})\n"));
mods.push(format!(
".font(.custom({}, size: {}).weight({}))",
bound(&["fontFamily"], literal(&t.style.font_family, false)),
bound(&["fontSize"], num(t.style.font_size)),
swift_weight(t.style.weight())
));
if let Some(c) = &fill {
mods.push(format!(".foregroundColor({c})"));
}
}
NodeKind::Ellipse { .. } => {
out.push_str(&format!("{pad}Ellipse()\n"));
if let Some(c) = &fill {
mods.push(format!(".fill({c})"));
}
mods.push(format!(".frame(width: {w}, height: {h})"));
}
NodeKind::Rectangle => {
out.push_str(&format!("{pad}RoundedRectangle(cornerRadius: {corner})\n"));
if let Some(c) = &fill {
mods.push(format!(".fill({c})"));
}
mods.push(format!(".frame(width: {w}, height: {h})"));
}
k if k.is_frame_like() || matches!(k, NodeKind::Group) => {
let children = ctx.kids(n);
let open = match n.layout.as_ref().map(|l| (l.mode, l)) {
Some((LayoutMode::Vertical, l)) => format!("VStack(alignment: .leading, spacing: {}) {{", bound(&["gap"], num(l.gap))),
Some((_, l)) => format!("HStack(alignment: .center, spacing: {}) {{", bound(&["gap"], num(l.gap))),
None => "ZStack(alignment: .topLeading) {".to_string(),
};
out.push_str(&format!("{pad}{open}\n"));
// A slot shows the instance's content, or else the main's.
let slot = ctx.param(n, "slot");
let inner = match slot {
Some(p) => {
out.push_str(&format!("{pad} if let {0} {{\n{pad} {0}\n{pad} }} else {{\n", p.ident));
depth + 2
}
None => depth + 1,
};
for c in &children {
// Free children sit at their offset inside a ZStack.
let offset: Vec<String> = match doc.node(*c).filter(|_| n.layout.is_none()) {
Some(cn) => {
let [_, _, _, _, x, y] = cn.transform.as_coeffs();
vec![format!(".offset(x: {}, y: {})", num(x), num(y))]
}
None => Vec::new(),
};
swift(ctx, *c, inner, out, &offset);
}
if slot.is_some() {
out.push_str(&format!("{pad} }}\n"));
}
out.push_str(&format!("{pad}}}\n"));
if let Some(l) = &n.layout {
let [t, r, b, lft] = l.padding;
if [t, r, b, lft].iter().any(|v| *v > 0.0) {
let side = |at: usize, v: f64| bound(&PADDING[at], num(v));
mods.push(format!(
".padding(EdgeInsets(top: {}, leading: {}, bottom: {}, trailing: {}))",
side(0, t),
side(3, lft),
side(2, b),
side(1, r)
));
}
}
mods.push(format!(".frame(width: {w}, height: {h}, alignment: .topLeading)"));
if let Some(c) = &fill {
mods.push(format!(".background({c})"));
}
if radius(n) > 0.0 {
mods.push(format!(".clipShape(RoundedRectangle(cornerRadius: {corner}))"));
}
}
_ => {
out.push_str(&format!("{pad}// {}: export as an image asset\n{pad}Image({})\n", n.kind.label(), literal(&n.name, false)));
mods.push(".resizable()".into());
mods.push(format!(".frame(width: {w}, height: {h})"));
}
}
if let Some(c) = stroke_color(n) {
mods.push(format!(
".overlay(RoundedRectangle(cornerRadius: {corner}).stroke({}, lineWidth: {}))",
bound(&["stroke"], swift_color(c)),
bound(&["strokeWeight"], num(n.stroke.weight))
));
}
if let Some(s) = drop_shadow(n) {
mods.push(format!(
".shadow(color: {}, radius: {}, x: {}, y: {})",
swift_color(s.color),
num(s.radius / 2.0),
num(s.offset.0),
num(s.offset.1)
));
}
if n.opacity < 1.0 {
mods.push(format!(".opacity({})", bound(&["opacity"], num(f64::from(n.opacity)))));
}
mods.extend(extra.iter().cloned());
for m in mods {
out.push_str(&format!("{pad} {m}\n"));
}
}