mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-30 21:36:46 -06:00
refactor(engine): simplify asset and code generation helpers
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
use super::swift::one_line;
|
||||
use super::*;
|
||||
use super::{
|
||||
CodeLang, Color, CrossAlign, Ctx, LayoutMode, LetterSpacing, LineHeight, MainAlign, Node, NodeId, NodeKind, TextAlignH, TextCase, TextDecoration,
|
||||
drop_shadow, first_fill, indent, literal, num, radius, stroke_color,
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------ ArkUI (ArkTS)
|
||||
|
||||
@@ -37,8 +40,121 @@ pub(crate) fn arkui(ctx: &Ctx, id: NodeId, depth: usize, out: &mut String, extra
|
||||
arkui_layer(ctx, n, depth, out, extra);
|
||||
}
|
||||
|
||||
fn arkui_layer(ctx: &Ctx, n: &Node, depth: usize, out: &mut String, extra: &[String]) {
|
||||
fn text_layer(ctx: &Ctx, n: &Node, t: &figcraft_doc::TextData, pad: &str, out: &mut String, mods: &mut Vec<String>) {
|
||||
let text = ctx.param(n, "characters").map_or_else(|| literal(&t.characters, false), |p| format!("this.{}", p.ident));
|
||||
out.push_str(&format!("{pad}Text({text})\n"));
|
||||
let s = &t.style;
|
||||
mods.push(format!(".fontSize({})", num(s.font_size)));
|
||||
mods.push(format!(".fontWeight({})", s.weight().clamp(100, 900)));
|
||||
mods.push(format!(".fontFamily({})", literal(&s.font_family, false)));
|
||||
if s.italic() {
|
||||
mods.push(".fontStyle(FontStyle.Italic)".into());
|
||||
}
|
||||
if let Some(c) = first_fill(n) {
|
||||
mods.push(format!(".fontColor({})", ark_color(c)));
|
||||
}
|
||||
match s.line_height {
|
||||
LineHeight::Pixels(v) => mods.push(format!(".lineHeight({})", num(v))),
|
||||
LineHeight::Percent(p) => mods.push(format!(".lineHeight('{}%')", num(p))),
|
||||
LineHeight::Auto => {}
|
||||
}
|
||||
match s.letter_spacing {
|
||||
LetterSpacing::Pixels(v) if v != 0.0 => mods.push(format!(".letterSpacing({})", num(v))),
|
||||
LetterSpacing::Percent(p) if p != 0.0 => mods.push(format!(".letterSpacing('{}%')", num(p))),
|
||||
_ => {}
|
||||
}
|
||||
match t.align_h {
|
||||
TextAlignH::Left => {}
|
||||
TextAlignH::Center => mods.push(".textAlign(TextAlign.Center)".into()),
|
||||
TextAlignH::Right => mods.push(".textAlign(TextAlign.End)".into()),
|
||||
TextAlignH::Justified => mods.push(".textAlign(TextAlign.JUSTIFY)".into()),
|
||||
}
|
||||
// Aligning needs a box to align in: the text layer's own width.
|
||||
if !matches!(t.align_h, TextAlignH::Left) {
|
||||
mods.push(format!(".width({})", num(n.width)));
|
||||
}
|
||||
match s.case {
|
||||
TextCase::Upper => mods.push(".textCase(TextCase.UpperCase)".into()),
|
||||
TextCase::Lower => mods.push(".textCase(TextCase.LowerCase)".into()),
|
||||
_ => {}
|
||||
}
|
||||
match s.decoration {
|
||||
TextDecoration::Underline => mods.push(".decoration({ type: TextDecorationType.Underline })".into()),
|
||||
TextDecoration::Strikethrough => mods.push(".decoration({ type: TextDecorationType.LineThrough })".into()),
|
||||
TextDecoration::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn frame_layer(ctx: &Ctx, n: &Node, depth: usize, pad: &str, out: &mut String, mods: &mut Vec<String>) {
|
||||
let doc = ctx.doc;
|
||||
let open = match n.layout.as_ref().map(|l| (l.mode, l)) {
|
||||
Some((LayoutMode::Vertical, l)) => format!("Column({{ space: {} }})", num(l.gap)),
|
||||
Some((_, l)) => format!("Row({{ space: {} }})", num(l.gap)),
|
||||
None => "Stack({ alignContent: Alignment.TopStart })".to_string(),
|
||||
};
|
||||
out.push_str(&format!("{pad}{open} {{\n"));
|
||||
match ctx.param(n, "slot") {
|
||||
// A slot shows the instance's content, or else the main's (its default builder).
|
||||
Some(p) => out.push_str(&format!("{pad} this.{}()\n", p.ident)),
|
||||
None => {
|
||||
for c in ctx.kids(n) {
|
||||
// Free children sit at their position inside a Stack, and so does a child
|
||||
// that ignores its auto layout parent.
|
||||
let pos: Vec<String> = match doc.node(c).filter(|cn| n.layout.is_none() || cn.child_layout.absolute) {
|
||||
Some(cn) => {
|
||||
let [_, _, _, _, x, y] = cn.transform.as_coeffs();
|
||||
vec![format!(".position({{ x: {}, y: {} }})", num(x), num(y))]
|
||||
}
|
||||
None => Vec::new(),
|
||||
};
|
||||
arkui(ctx, c, depth + 1, out, &pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push_str(&format!("{pad}}}\n"));
|
||||
if let Some(l) = &n.layout {
|
||||
layout_mods(l, mods);
|
||||
}
|
||||
mods.push(format!(".width({})", num(n.width)));
|
||||
mods.push(format!(".height({})", num(n.height)));
|
||||
if let Some(c) = first_fill(n) {
|
||||
mods.push(format!(".backgroundColor({})", ark_color(c)));
|
||||
}
|
||||
if radius(n) > 0.0 {
|
||||
mods.push(".clip(true)".into());
|
||||
}
|
||||
ark_radius(n, mods);
|
||||
}
|
||||
|
||||
fn layout_mods(l: &figcraft_doc::AutoLayout, mods: &mut Vec<String>) {
|
||||
let [t, r, b, lft] = l.padding;
|
||||
if [t, r, b, lft].iter().any(|v| *v > 0.0) {
|
||||
mods.push(format!(".padding({{ top: {}, right: {}, bottom: {}, left: {} }})", num(t), num(r), num(b), num(lft)));
|
||||
}
|
||||
let justify = match l.main_align {
|
||||
MainAlign::Min => None,
|
||||
MainAlign::Center => Some("FlexAlign.Center"),
|
||||
MainAlign::Max => Some("FlexAlign.End"),
|
||||
MainAlign::SpaceBetween => Some("FlexAlign.SpaceBetween"),
|
||||
MainAlign::SpaceAround => Some("FlexAlign.SpaceAround"),
|
||||
MainAlign::SpaceEvenly => Some("FlexAlign.SpaceEvenly"),
|
||||
};
|
||||
if let Some(j) = justify {
|
||||
mods.push(format!(".justifyContent({j})"));
|
||||
}
|
||||
let cross = match l.cross_align {
|
||||
CrossAlign::Min => None,
|
||||
CrossAlign::Center => Some("Center"),
|
||||
CrossAlign::Max => Some(if l.mode == LayoutMode::Vertical { "End" } else { "Bottom" }),
|
||||
CrossAlign::Baseline => None,
|
||||
};
|
||||
if let Some(c) = cross {
|
||||
let ty = if l.mode == LayoutMode::Vertical { "HorizontalAlign" } else { "VerticalAlign" };
|
||||
mods.push(format!(".alignItems({ty}.{c})"));
|
||||
}
|
||||
}
|
||||
|
||||
fn arkui_layer(ctx: &Ctx, n: &Node, depth: usize, out: &mut String, extra: &[String]) {
|
||||
let pad = indent(depth);
|
||||
if ctx.too_deep(depth) {
|
||||
out.push_str(&format!("{pad}// {} …\n", one_line(&n.name)));
|
||||
@@ -68,48 +184,7 @@ fn arkui_layer(ctx: &Ctx, n: &Node, depth: usize, out: &mut String, extra: &[Str
|
||||
let mut mods: Vec<String> = Vec::new();
|
||||
match &n.kind {
|
||||
NodeKind::Text(t) => {
|
||||
let text = ctx.param(n, "characters").map_or_else(|| literal(&t.characters, false), |p| format!("this.{}", p.ident));
|
||||
out.push_str(&format!("{pad}Text({text})\n"));
|
||||
let s = &t.style;
|
||||
mods.push(format!(".fontSize({})", num(s.font_size)));
|
||||
mods.push(format!(".fontWeight({})", s.weight().clamp(100, 900)));
|
||||
mods.push(format!(".fontFamily({})", literal(&s.font_family, false)));
|
||||
if s.italic() {
|
||||
mods.push(".fontStyle(FontStyle.Italic)".into());
|
||||
}
|
||||
if let Some(c) = first_fill(n) {
|
||||
mods.push(format!(".fontColor({})", ark_color(c)));
|
||||
}
|
||||
match s.line_height {
|
||||
LineHeight::Pixels(v) => mods.push(format!(".lineHeight({})", num(v))),
|
||||
LineHeight::Percent(p) => mods.push(format!(".lineHeight('{}%')", num(p))),
|
||||
LineHeight::Auto => {}
|
||||
}
|
||||
match s.letter_spacing {
|
||||
LetterSpacing::Pixels(v) if v != 0.0 => mods.push(format!(".letterSpacing({})", num(v))),
|
||||
LetterSpacing::Percent(p) if p != 0.0 => mods.push(format!(".letterSpacing('{}%')", num(p))),
|
||||
_ => {}
|
||||
}
|
||||
match t.align_h {
|
||||
TextAlignH::Left => {}
|
||||
TextAlignH::Center => mods.push(".textAlign(TextAlign.Center)".into()),
|
||||
TextAlignH::Right => mods.push(".textAlign(TextAlign.End)".into()),
|
||||
TextAlignH::Justified => mods.push(".textAlign(TextAlign.JUSTIFY)".into()),
|
||||
}
|
||||
// Aligning needs a box to align in: the text layer's own width.
|
||||
if !matches!(t.align_h, TextAlignH::Left) {
|
||||
mods.push(format!(".width({})", num(n.width)));
|
||||
}
|
||||
match s.case {
|
||||
TextCase::Upper => mods.push(".textCase(TextCase.UpperCase)".into()),
|
||||
TextCase::Lower => mods.push(".textCase(TextCase.LowerCase)".into()),
|
||||
_ => {}
|
||||
}
|
||||
match s.decoration {
|
||||
TextDecoration::Underline => mods.push(".decoration({ type: TextDecorationType.Underline })".into()),
|
||||
TextDecoration::Strikethrough => mods.push(".decoration({ type: TextDecorationType.LineThrough })".into()),
|
||||
TextDecoration::None => {}
|
||||
}
|
||||
text_layer(ctx, n, t, &pad, out, &mut mods);
|
||||
}
|
||||
NodeKind::Ellipse { .. } => {
|
||||
out.push_str(&format!("{pad}Ellipse({{ width: {}, height: {} }})\n", num(n.width), num(n.height)));
|
||||
@@ -131,67 +206,7 @@ fn arkui_layer(ctx: &Ctx, n: &Node, depth: usize, out: &mut String, extra: &[Str
|
||||
mods.push(format!(".height({})", num(n.height)));
|
||||
}
|
||||
k if k.is_frame_like() || matches!(k, NodeKind::Group) => {
|
||||
let open = match n.layout.as_ref().map(|l| (l.mode, l)) {
|
||||
Some((LayoutMode::Vertical, l)) => format!("Column({{ space: {} }})", num(l.gap)),
|
||||
Some((_, l)) => format!("Row({{ space: {} }})", num(l.gap)),
|
||||
None => "Stack({ alignContent: Alignment.TopStart })".to_string(),
|
||||
};
|
||||
out.push_str(&format!("{pad}{open} {{\n"));
|
||||
match ctx.param(n, "slot") {
|
||||
// A slot shows the instance's content, or else the main's (its default builder).
|
||||
Some(p) => out.push_str(&format!("{pad} this.{}()\n", p.ident)),
|
||||
None => {
|
||||
for c in ctx.kids(n) {
|
||||
// Free children sit at their position inside a Stack, and so does a child
|
||||
// that ignores its auto layout parent.
|
||||
let pos: Vec<String> = match doc.node(c).filter(|cn| n.layout.is_none() || cn.child_layout.absolute) {
|
||||
Some(cn) => {
|
||||
let [_, _, _, _, x, y] = cn.transform.as_coeffs();
|
||||
vec![format!(".position({{ x: {}, y: {} }})", num(x), num(y))]
|
||||
}
|
||||
None => Vec::new(),
|
||||
};
|
||||
arkui(ctx, c, depth + 1, out, &pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
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) {
|
||||
mods.push(format!(".padding({{ top: {}, right: {}, bottom: {}, left: {} }})", num(t), num(r), num(b), num(lft)));
|
||||
}
|
||||
let justify = match l.main_align {
|
||||
MainAlign::Min => None,
|
||||
MainAlign::Center => Some("FlexAlign.Center"),
|
||||
MainAlign::Max => Some("FlexAlign.End"),
|
||||
MainAlign::SpaceBetween => Some("FlexAlign.SpaceBetween"),
|
||||
MainAlign::SpaceAround => Some("FlexAlign.SpaceAround"),
|
||||
MainAlign::SpaceEvenly => Some("FlexAlign.SpaceEvenly"),
|
||||
};
|
||||
if let Some(j) = justify {
|
||||
mods.push(format!(".justifyContent({j})"));
|
||||
}
|
||||
let cross = match l.cross_align {
|
||||
CrossAlign::Min => None,
|
||||
CrossAlign::Center => Some("Center"),
|
||||
CrossAlign::Max => Some(if l.mode == LayoutMode::Vertical { "End" } else { "Bottom" }),
|
||||
CrossAlign::Baseline => None,
|
||||
};
|
||||
if let Some(c) = cross {
|
||||
let ty = if l.mode == LayoutMode::Vertical { "HorizontalAlign" } else { "VerticalAlign" };
|
||||
mods.push(format!(".alignItems({ty}.{c})"));
|
||||
}
|
||||
}
|
||||
mods.push(format!(".width({})", num(n.width)));
|
||||
mods.push(format!(".height({})", num(n.height)));
|
||||
if let Some(c) = first_fill(n) {
|
||||
mods.push(format!(".backgroundColor({})", ark_color(c)));
|
||||
}
|
||||
if radius(n) > 0.0 {
|
||||
mods.push(".clip(true)".into());
|
||||
}
|
||||
ark_radius(n, &mut mods);
|
||||
frame_layer(ctx, n, depth, &pad, out, &mut mods);
|
||||
}
|
||||
_ => {
|
||||
out.push_str(&format!("{pad}// {}: export as an image asset\n{pad}Image($r('app.media.asset'))\n", n.kind.label()));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::swift::one_line;
|
||||
use super::*;
|
||||
use super::{CodeLang, Color, Ctx, LayoutMode, Node, NodeId, NodeKind, drop_shadow, first_fill, indent, literal, num, radius, stroke_color};
|
||||
|
||||
// ------------------------------------------------------------------ Jetpack Compose
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use super::*;
|
||||
use super::{
|
||||
ContentAlign, CrossAlign, Document, EffectKind, LayoutMode, LetterSpacing, LineHeight, MainAlign, Node, NodeId, NodeKind, PaintKind, TextAlignH,
|
||||
TextCase, TextDecoration, Variable, css_borders, num,
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------ CSS
|
||||
|
||||
@@ -85,51 +88,7 @@ pub(crate) fn css_decls(doc: &Document, n: &Node, t: &mut Tokens, hug: (bool, bo
|
||||
put("height", tok(doc, n, &["height"], px(n.height), t));
|
||||
}
|
||||
if let Some(l) = &n.layout {
|
||||
put("display", "flex".into());
|
||||
put("flex-direction", if l.mode == LayoutMode::Vertical { "column" } else { "row" }.into());
|
||||
if l.wrap {
|
||||
put("flex-wrap", "wrap".into());
|
||||
}
|
||||
put("gap", tok(doc, n, &["gap"], px(l.gap), t));
|
||||
let [pt, pr, pb, pl] = l.padding;
|
||||
let sides = [
|
||||
tok(doc, n, &["paddingTop", "paddingVertical", "padding"], px(pt), t),
|
||||
tok(doc, n, &["paddingRight", "paddingHorizontal", "padding"], px(pr), t),
|
||||
tok(doc, n, &["paddingBottom", "paddingVertical", "padding"], px(pb), t),
|
||||
tok(doc, n, &["paddingLeft", "paddingHorizontal", "padding"], px(pl), t),
|
||||
];
|
||||
put("padding", sides.join(" "));
|
||||
let justify = match l.main_align {
|
||||
MainAlign::Min => "flex-start",
|
||||
MainAlign::Center => "center",
|
||||
MainAlign::Max => "flex-end",
|
||||
MainAlign::SpaceBetween => "space-between",
|
||||
MainAlign::SpaceAround => "space-around",
|
||||
MainAlign::SpaceEvenly => "space-evenly",
|
||||
};
|
||||
put("justify-content", justify.into());
|
||||
let align = match l.cross_align {
|
||||
CrossAlign::Min => "flex-start",
|
||||
CrossAlign::Center => "center",
|
||||
CrossAlign::Max => "flex-end",
|
||||
CrossAlign::Baseline => "baseline",
|
||||
};
|
||||
put("align-items", align.into());
|
||||
if l.wrap {
|
||||
// The lines: spread, or packed where the items align (baseline: at the start).
|
||||
let spread = l.content_align == ContentAlign::SpaceBetween;
|
||||
put(
|
||||
"align-content",
|
||||
if spread {
|
||||
"space-between"
|
||||
} else if align == "baseline" {
|
||||
"flex-start"
|
||||
} else {
|
||||
align
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
layout_decls(doc, n, l, t, &mut put);
|
||||
}
|
||||
if matches!(n.kind, NodeKind::Ellipse { .. }) {
|
||||
put("border-radius", "50%".into());
|
||||
@@ -169,6 +128,65 @@ pub(crate) fn css_decls(doc: &Document, n: &Node, t: &mut Tokens, hug: (bool, bo
|
||||
for (key, value) in css_borders(doc, n, t) {
|
||||
put(&key, value);
|
||||
}
|
||||
effect_decls(n, &mut put);
|
||||
if let NodeKind::Text(tx) = &n.kind {
|
||||
text_decls(doc, n, tx, t, &mut put);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The flex declarations of auto layout `l` on `n`.
|
||||
fn layout_decls(doc: &Document, n: &Node, l: &figcraft_doc::AutoLayout, t: &mut Tokens, put: &mut impl FnMut(&str, String)) {
|
||||
let px = |v: f64| format!("{}px", num(v));
|
||||
put("display", "flex".into());
|
||||
put("flex-direction", if l.mode == LayoutMode::Vertical { "column" } else { "row" }.into());
|
||||
if l.wrap {
|
||||
put("flex-wrap", "wrap".into());
|
||||
}
|
||||
put("gap", tok(doc, n, &["gap"], px(l.gap), t));
|
||||
let [pt, pr, pb, pl] = l.padding;
|
||||
let sides = [
|
||||
tok(doc, n, &["paddingTop", "paddingVertical", "padding"], px(pt), t),
|
||||
tok(doc, n, &["paddingRight", "paddingHorizontal", "padding"], px(pr), t),
|
||||
tok(doc, n, &["paddingBottom", "paddingVertical", "padding"], px(pb), t),
|
||||
tok(doc, n, &["paddingLeft", "paddingHorizontal", "padding"], px(pl), t),
|
||||
];
|
||||
put("padding", sides.join(" "));
|
||||
let justify = match l.main_align {
|
||||
MainAlign::Min => "flex-start",
|
||||
MainAlign::Center => "center",
|
||||
MainAlign::Max => "flex-end",
|
||||
MainAlign::SpaceBetween => "space-between",
|
||||
MainAlign::SpaceAround => "space-around",
|
||||
MainAlign::SpaceEvenly => "space-evenly",
|
||||
};
|
||||
put("justify-content", justify.into());
|
||||
let align = match l.cross_align {
|
||||
CrossAlign::Min => "flex-start",
|
||||
CrossAlign::Center => "center",
|
||||
CrossAlign::Max => "flex-end",
|
||||
CrossAlign::Baseline => "baseline",
|
||||
};
|
||||
put("align-items", align.into());
|
||||
if l.wrap {
|
||||
// The lines: spread, or packed where the items align (baseline: at the start).
|
||||
let spread = l.content_align == ContentAlign::SpaceBetween;
|
||||
put(
|
||||
"align-content",
|
||||
if spread {
|
||||
"space-between"
|
||||
} else if align == "baseline" {
|
||||
"flex-start"
|
||||
} else {
|
||||
align
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Shadows and blurs of `n`.
|
||||
fn effect_decls(n: &Node, put: &mut impl FnMut(&str, String)) {
|
||||
let shadows: Vec<String> = n
|
||||
.effects
|
||||
.iter()
|
||||
@@ -196,75 +214,82 @@ pub(crate) fn css_decls(doc: &Document, n: &Node, t: &mut Tokens, hug: (bool, bo
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if let NodeKind::Text(tx) = &n.kind {
|
||||
let s = &tx.style;
|
||||
put("font-family", tok(doc, n, &["fontFamily"], format!("\"{}\"", s.font_family.replace('"', "'")), t));
|
||||
put("font-size", tok(doc, n, &["fontSize"], px(s.font_size), t));
|
||||
put("font-weight", s.weight().to_string());
|
||||
if s.italic() {
|
||||
put("font-style", "italic".into());
|
||||
}
|
||||
|
||||
/// The type declarations of text layer `n`.
|
||||
fn text_decls(doc: &Document, n: &Node, tx: &figcraft_doc::TextData, t: &mut Tokens, put: &mut impl FnMut(&str, String)) {
|
||||
let px = |v: f64| format!("{}px", num(v));
|
||||
let s = &tx.style;
|
||||
put("font-family", tok(doc, n, &["fontFamily"], format!("\"{}\"", s.font_family.replace('"', "'")), t));
|
||||
put("font-size", tok(doc, n, &["fontSize"], px(s.font_size), t));
|
||||
put("font-weight", s.weight().to_string());
|
||||
if s.italic() {
|
||||
put("font-style", "italic".into());
|
||||
}
|
||||
match s.line_height {
|
||||
LineHeight::Pixels(v) => put("line-height", tok(doc, n, &["lineHeight"], px(v), t)),
|
||||
LineHeight::Percent(p) => put("line-height", format!("{}%", num(p))),
|
||||
LineHeight::Auto => {}
|
||||
}
|
||||
match s.letter_spacing {
|
||||
LetterSpacing::Pixels(v) if v != 0.0 => put("letter-spacing", tok(doc, n, &["letterSpacing"], px(v), t)),
|
||||
LetterSpacing::Percent(p) if p != 0.0 => put("letter-spacing", format!("{}em", num(p / 100.0))),
|
||||
_ => {}
|
||||
}
|
||||
match tx.align_h {
|
||||
TextAlignH::Left => {}
|
||||
TextAlignH::Center => put("text-align", "center".into()),
|
||||
TextAlignH::Right => put("text-align", "right".into()),
|
||||
TextAlignH::Justified => put("text-align", "justify".into()),
|
||||
}
|
||||
match s.case {
|
||||
TextCase::Original => {}
|
||||
TextCase::Upper => put("text-transform", "uppercase".into()),
|
||||
TextCase::Lower => put("text-transform", "lowercase".into()),
|
||||
TextCase::Title => put("text-transform", "capitalize".into()),
|
||||
TextCase::SmallCaps => put("font-variant", "small-caps".into()),
|
||||
}
|
||||
match s.decoration {
|
||||
TextDecoration::None => {}
|
||||
TextDecoration::Underline => put("text-decoration", "underline".into()),
|
||||
TextDecoration::Strikethrough => put("text-decoration", "line-through".into()),
|
||||
}
|
||||
decoration_decls(s, put);
|
||||
if tx.leading_trim == figcraft_doc::LeadingTrim::CapHeight {
|
||||
put("text-box", "trim-both cap alphabetic".into());
|
||||
}
|
||||
}
|
||||
|
||||
fn decoration_decls(s: &figcraft_doc::TextStyle, put: &mut impl FnMut(&str, String)) {
|
||||
let px = |v: f64| format!("{}px", num(v));
|
||||
if s.decoration != TextDecoration::None {
|
||||
let l = s.look();
|
||||
let len = |m: LineHeight| match m {
|
||||
LineHeight::Auto => None,
|
||||
LineHeight::Pixels(v) => Some(px(v)),
|
||||
LineHeight::Percent(p) => Some(format!("{}%", num(p))),
|
||||
};
|
||||
match l.style {
|
||||
figcraft_doc::DecorationStyle::Solid => {}
|
||||
figcraft_doc::DecorationStyle::Dotted => put("text-decoration-style", "dotted".into()),
|
||||
figcraft_doc::DecorationStyle::Wavy => put("text-decoration-style", "wavy".into()),
|
||||
}
|
||||
match s.line_height {
|
||||
LineHeight::Pixels(v) => put("line-height", tok(doc, n, &["lineHeight"], px(v), t)),
|
||||
LineHeight::Percent(p) => put("line-height", format!("{}%", num(p))),
|
||||
LineHeight::Auto => {}
|
||||
if let Some(v) = len(l.thickness) {
|
||||
put("text-decoration-thickness", v);
|
||||
}
|
||||
match s.letter_spacing {
|
||||
LetterSpacing::Pixels(v) if v != 0.0 => put("letter-spacing", tok(doc, n, &["letterSpacing"], px(v), t)),
|
||||
LetterSpacing::Percent(p) if p != 0.0 => put("letter-spacing", format!("{}em", num(p / 100.0))),
|
||||
_ => {}
|
||||
if let Some(c) = l.color {
|
||||
put("text-decoration-color", c.to_css());
|
||||
}
|
||||
match tx.align_h {
|
||||
TextAlignH::Left => {}
|
||||
TextAlignH::Center => put("text-align", "center".into()),
|
||||
TextAlignH::Right => put("text-align", "right".into()),
|
||||
TextAlignH::Justified => put("text-align", "justify".into()),
|
||||
}
|
||||
match s.case {
|
||||
TextCase::Original => {}
|
||||
TextCase::Upper => put("text-transform", "uppercase".into()),
|
||||
TextCase::Lower => put("text-transform", "lowercase".into()),
|
||||
TextCase::Title => put("text-transform", "capitalize".into()),
|
||||
TextCase::SmallCaps => put("font-variant", "small-caps".into()),
|
||||
}
|
||||
match s.decoration {
|
||||
TextDecoration::None => {}
|
||||
TextDecoration::Underline => put("text-decoration", "underline".into()),
|
||||
TextDecoration::Strikethrough => put("text-decoration", "line-through".into()),
|
||||
}
|
||||
if s.decoration != TextDecoration::None {
|
||||
let l = s.look();
|
||||
let len = |m: LineHeight| match m {
|
||||
LineHeight::Auto => None,
|
||||
LineHeight::Pixels(v) => Some(px(v)),
|
||||
LineHeight::Percent(p) => Some(format!("{}%", num(p))),
|
||||
};
|
||||
match l.style {
|
||||
figcraft_doc::DecorationStyle::Solid => {}
|
||||
figcraft_doc::DecorationStyle::Dotted => put("text-decoration-style", "dotted".into()),
|
||||
figcraft_doc::DecorationStyle::Wavy => put("text-decoration-style", "wavy".into()),
|
||||
if s.decoration == TextDecoration::Underline {
|
||||
if let Some(v) = len(l.offset) {
|
||||
put("text-underline-offset", v);
|
||||
}
|
||||
if let Some(v) = len(l.thickness) {
|
||||
put("text-decoration-thickness", v);
|
||||
// Browsers skip ink by default; the canvas only when asked.
|
||||
if !l.skip_ink {
|
||||
put("text-decoration-skip-ink", "none".into());
|
||||
}
|
||||
if let Some(c) = l.color {
|
||||
put("text-decoration-color", c.to_css());
|
||||
}
|
||||
if s.decoration == TextDecoration::Underline {
|
||||
if let Some(v) = len(l.offset) {
|
||||
put("text-underline-offset", v);
|
||||
}
|
||||
// Browsers skip ink by default; the canvas only when asked.
|
||||
if !l.skip_ink {
|
||||
put("text-decoration-skip-ink", "none".into());
|
||||
}
|
||||
}
|
||||
}
|
||||
if tx.leading_trim == figcraft_doc::LeadingTrim::CapHeight {
|
||||
put("text-box", "trim-both cap alphabetic".into());
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A gradient fill on a `w`×`h` box as a CSS background: linear along its handles' direction
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::css::{Tokens, css_decls};
|
||||
use super::*;
|
||||
use super::{CodeLang, Ctx, Document, LayoutMode, Node, NodeId, NodeKind, Param, Sizing, indent, num, with_v_if};
|
||||
|
||||
// ------------------------------------------------------------------ HTML and React markup
|
||||
|
||||
@@ -127,6 +127,32 @@ pub(crate) struct Markup<'c, 'a> {
|
||||
pub fluid_leaves: f64,
|
||||
}
|
||||
|
||||
/// How `n` sits in its parent: in the flex flow, or placed at its position.
|
||||
fn placement_decls(n: &Node, parent: Option<&Node>, root: bool, in_flow: bool) -> String {
|
||||
let mut decls = String::new();
|
||||
if root {
|
||||
decls.push_str("position: relative;\n");
|
||||
} else if in_flow {
|
||||
let vertical = parent.and_then(|p| p.layout.as_ref()).is_some_and(|l| l.mode == LayoutMode::Vertical);
|
||||
let (main, cross) =
|
||||
if vertical { (n.child_layout.sizing_v, n.child_layout.sizing_h) } else { (n.child_layout.sizing_h, n.child_layout.sizing_v) };
|
||||
if main == Sizing::Fill {
|
||||
decls.push_str("flex: 1 1 0;\n");
|
||||
}
|
||||
if cross == Sizing::Fill {
|
||||
decls.push_str("align-self: stretch;\n");
|
||||
}
|
||||
} else {
|
||||
let c = n.transform.as_coeffs();
|
||||
decls.push_str(&format!("position: absolute;\nleft: {}px;\ntop: {}px;\n", num(c[4]), num(c[5])));
|
||||
let rot = figcraft_geom::rotation_degrees(n.transform);
|
||||
if rot.abs() > 0.01 {
|
||||
decls.push_str(&format!("transform: rotate({}deg);\ntransform-origin: 0 0;\n", num(-rot)));
|
||||
}
|
||||
}
|
||||
decls
|
||||
}
|
||||
|
||||
impl<'c, 'a> Markup<'c, 'a> {
|
||||
pub fn new(ctx: &'c Ctx<'a>, lang: CodeLang) -> Self {
|
||||
Markup {
|
||||
@@ -196,6 +222,52 @@ impl<'c, 'a> Markup<'c, 'a> {
|
||||
self.rules.push((path.to_string(), class.to_string(), decls));
|
||||
}
|
||||
|
||||
/// An instance as a call to its component (see `element`); true when it was written.
|
||||
fn instance_call(&mut self, n: &Node, depth: usize, path: &str, class: &str, decls: &str, root: bool) -> bool {
|
||||
let pad = self.pad(depth);
|
||||
let attr = if self.lang == CodeLang::React { "className" } else { "class" };
|
||||
let connected = self.inline && crate::code_connect::link(self.ctx.doc, n.id, CodeLang::React).is_some();
|
||||
if self.code() && (!self.inline || connected) && !root && matches!(n.kind, NodeKind::Instance { .. }) {
|
||||
let at = depth + usize::from(!decls.is_empty());
|
||||
let run = if self.lang == CodeLang::Leptos { ".run()" } else { "" };
|
||||
let (inner, css) = match self.ctx.param(n, "mainComponent") {
|
||||
Some(p) if self.lang == CodeLang::Vue => (Some(format!("{}<component :is=\"{}\" />\n", self.pad(at), self.ident(p))), String::new()),
|
||||
Some(p) => (Some(format!("{}{{{}{run}}}\n", self.pad(at), self.ident(p))), String::new()),
|
||||
None => self.ctx.call(n, self.lang, at).map_or((None, String::new()), |(code, css)| (Some(code), css)),
|
||||
};
|
||||
self.styles.push_str(&css);
|
||||
if let Some(inner) = inner {
|
||||
// A Vue guard set by `node` rides on the call when there is no wrapper.
|
||||
let guard = if self.lang == CodeLang::Vue { self.v_if.take() } else { None };
|
||||
if decls.is_empty() {
|
||||
self.markup.push_str(&with_v_if(&inner, guard.as_deref()));
|
||||
} else {
|
||||
let guard = guard.map(|g| format!(" v-if=\"{g}\"")).unwrap_or_default();
|
||||
self.markup.push_str(&format!("{pad}<div {attr}=\"{class}\"{guard}>\n{inner}{pad}</div>\n"));
|
||||
self.rule(path, class, decls.to_string());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn text_body(&self, n: &Node, t: &figcraft_doc::TextData) -> String {
|
||||
match (self.ctx.param(n, "characters").filter(|_| self.code()), self.lang) {
|
||||
(Some(p), CodeLang::Leptos) => format!("{{{}.clone()}}", self.ident(p)),
|
||||
(Some(p), CodeLang::Vue) => format!("{{{{ {} }}}}", self.ident(p)),
|
||||
(Some(p), _) => format!("{{{}}}", p.ident),
|
||||
(None, CodeLang::React) => jsx_text(&t.characters),
|
||||
// `view!` takes text as string literals and void elements self-closed.
|
||||
(None, CodeLang::Leptos) => {
|
||||
t.characters.chars().take(10_000).collect::<String>().lines().map(|l| format!("{l:?}")).collect::<Vec<_>>().join("<br/>")
|
||||
}
|
||||
// A Vue template reads `{{ }}` as interpolation, so braces are escaped.
|
||||
(None, CodeLang::Vue) => html_escape(&t.characters).replace('{', "{").replace('}', "}"),
|
||||
(None, _) => html_escape(&t.characters),
|
||||
}
|
||||
}
|
||||
|
||||
fn element(&mut self, n: &Node, parent: Option<&Node>, depth: usize, path: &str) {
|
||||
let doc = self.ctx.doc;
|
||||
let root = n.id == self.ctx.root;
|
||||
@@ -215,53 +287,13 @@ impl<'c, 'a> Markup<'c, 'a> {
|
||||
let pad = self.pad(depth);
|
||||
let attr = if self.lang == CodeLang::React { "className" } else { "class" };
|
||||
// How it sits in its parent: in the flex flow, or placed at its position.
|
||||
let mut decls = String::new();
|
||||
let in_flow = parent.is_some_and(|p| p.layout.is_some()) && !n.child_layout.absolute;
|
||||
if root {
|
||||
decls.push_str("position: relative;\n");
|
||||
} else if in_flow {
|
||||
let vertical = parent.and_then(|p| p.layout.as_ref()).is_some_and(|l| l.mode == LayoutMode::Vertical);
|
||||
let (main, cross) =
|
||||
if vertical { (n.child_layout.sizing_v, n.child_layout.sizing_h) } else { (n.child_layout.sizing_h, n.child_layout.sizing_v) };
|
||||
if main == Sizing::Fill {
|
||||
decls.push_str("flex: 1 1 0;\n");
|
||||
}
|
||||
if cross == Sizing::Fill {
|
||||
decls.push_str("align-self: stretch;\n");
|
||||
}
|
||||
} else {
|
||||
let c = n.transform.as_coeffs();
|
||||
decls.push_str(&format!("position: absolute;\nleft: {}px;\ntop: {}px;\n", num(c[4]), num(c[5])));
|
||||
let rot = figcraft_geom::rotation_degrees(n.transform);
|
||||
if rot.abs() > 0.01 {
|
||||
decls.push_str(&format!("transform: rotate({}deg);\ntransform-origin: 0 0;\n", num(-rot)));
|
||||
}
|
||||
}
|
||||
let mut decls = placement_decls(n, parent, root, in_flow);
|
||||
// Nested instances are calls (component code), in a wrapper that places them when needed.
|
||||
// Page code inlines instances, except those connected to code (codeConnect.set): their
|
||||
// snippet, e.g. a design-system `<Button>`.
|
||||
let connected = self.inline && crate::code_connect::link(doc, n.id, CodeLang::React).is_some();
|
||||
if self.code() && (!self.inline || connected) && !root && matches!(n.kind, NodeKind::Instance { .. }) {
|
||||
let at = depth + usize::from(!decls.is_empty());
|
||||
let run = if self.lang == CodeLang::Leptos { ".run()" } else { "" };
|
||||
let (inner, css) = match self.ctx.param(n, "mainComponent") {
|
||||
Some(p) if self.lang == CodeLang::Vue => (Some(format!("{}<component :is=\"{}\" />\n", self.pad(at), self.ident(p))), String::new()),
|
||||
Some(p) => (Some(format!("{}{{{}{run}}}\n", self.pad(at), self.ident(p))), String::new()),
|
||||
None => self.ctx.call(n, self.lang, at).map_or((None, String::new()), |(code, css)| (Some(code), css)),
|
||||
};
|
||||
self.styles.push_str(&css);
|
||||
if let Some(inner) = inner {
|
||||
// A Vue guard set by `node` rides on the call when there is no wrapper.
|
||||
let guard = if self.lang == CodeLang::Vue { self.v_if.take() } else { None };
|
||||
if decls.is_empty() {
|
||||
self.markup.push_str(&with_v_if(&inner, guard.as_deref()));
|
||||
} else {
|
||||
let guard = guard.map(|g| format!(" v-if=\"{g}\"")).unwrap_or_default();
|
||||
self.markup.push_str(&format!("{pad}<div {attr}=\"{class}\"{guard}>\n{inner}{pad}</div>\n"));
|
||||
self.rule(path, &class, decls);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if self.instance_call(n, depth, path, &class, &decls, root) {
|
||||
return;
|
||||
}
|
||||
let hugs = n.layout.is_some() || matches!(n.kind, NodeKind::Text(_));
|
||||
let hug = (hugs && n.child_layout.sizing_h == Sizing::Hug, hugs && n.child_layout.sizing_v == Sizing::Hug);
|
||||
@@ -286,19 +318,7 @@ impl<'c, 'a> Markup<'c, 'a> {
|
||||
let open = |tag: &str| format!("{pad}<{tag} {attr}=\"{class}\"{attrs}>");
|
||||
match &n.kind {
|
||||
NodeKind::Text(t) => {
|
||||
let body = match (self.ctx.param(n, "characters").filter(|_| self.code()), self.lang) {
|
||||
(Some(p), CodeLang::Leptos) => format!("{{{}.clone()}}", self.ident(p)),
|
||||
(Some(p), CodeLang::Vue) => format!("{{{{ {} }}}}", self.ident(p)),
|
||||
(Some(p), _) => format!("{{{}}}", p.ident),
|
||||
(None, CodeLang::React) => jsx_text(&t.characters),
|
||||
// `view!` takes text as string literals and void elements self-closed.
|
||||
(None, CodeLang::Leptos) => {
|
||||
t.characters.chars().take(10_000).collect::<String>().lines().map(|l| format!("{l:?}")).collect::<Vec<_>>().join("<br/>")
|
||||
}
|
||||
// A Vue template reads `{{ }}` as interpolation, so braces are escaped.
|
||||
(None, CodeLang::Vue) => html_escape(&t.characters).replace('{', "{").replace('}', "}"),
|
||||
(None, _) => html_escape(&t.characters),
|
||||
};
|
||||
let body = self.text_body(n, t);
|
||||
self.markup.push_str(&format!("{}{body}</p>\n", open("p")));
|
||||
}
|
||||
_ => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::{CodeLang, Color, Ctx, LayoutMode, Node, NodeId, NodeKind, drop_shadow, first_fill, indent, literal, num, radius, stroke_color};
|
||||
|
||||
// ------------------------------------------------------------------ SwiftUI
|
||||
|
||||
|
||||
Reference in New Issue
Block a user