Merge main into #664

# Conflicts:
#	crates/engine/src/code_connect.rs
#	crates/engine/src/codegen.rs
#	crates/engine/src/component_codegen.rs
#	crates/engine/src/lib.rs
#	crates/engine/src/playground.rs
#	crates/ui-egui/locales/bn.toml
#	crates/ui-egui/locales/cs.toml
#	crates/ui-egui/locales/de.toml
#	crates/ui-egui/locales/en.toml
#	crates/ui-egui/locales/es.toml
#	crates/ui-egui/locales/fi.toml
#	crates/ui-egui/locales/fr.toml
#	crates/ui-egui/locales/id.toml
#	crates/ui-egui/locales/ja.toml
#	crates/ui-egui/locales/ka.toml
#	crates/ui-egui/locales/ko.toml
#	crates/ui-egui/locales/nl.toml
#	crates/ui-egui/locales/pl.toml
#	crates/ui-egui/locales/pt-BR.toml
#	crates/ui-egui/locales/ru.toml
#	crates/ui-egui/locales/sv.toml
#	crates/ui-egui/locales/tr.toml
#	crates/ui-egui/locales/zh-Hans.toml
#	crates/ui-egui/src/dev_mode.rs
#	docs/dev-mode-parity.md
#	docs/gaps.md
#	docs/mcp.md
#	docs/target-app-parity.md
This commit is contained in:
bflatastic
2026-10-26 23:52:51 +07:00
541 changed files with 53315 additions and 4270 deletions
+120 -18
View File
@@ -1,16 +1,17 @@
//! Component code: a component (or component set) becomes a React, Leptos, Vue, SwiftUI or Jetpack
//! Compose component whose properties are its parameters (a variant property → a union or enum, boolean →
//! a flag showing the layers bound to it, text → a string, instance swap → a view parameter, slot →
//! optional content), and an instance becomes a call passing the values it sets. Variants of a set
//! share one definition (the set's default variant); in React and Leptos their differences become CSS
//! rules on `data-*` attributes. Returned by `inspect.componentCode` and used by `inspect.code`. A component
//! connected to code (see [`crate::code_connect`]) shows its real snippet instead.
//! Component code: a component (or component set) becomes a React, Leptos, Vue, SwiftUI, Jetpack
//! Compose or ArkUI component whose properties are its parameters (a variant property → a union or
//! enum, boolean → a flag showing the layers bound to it, text → a string, instance swap → a view
//! parameter, slot → optional content), and an instance becomes a call passing the values it sets.
//! Variants of a set share one definition (the set's default variant); in React and Leptos their
//! differences become CSS rules on `data-*` attributes. Returned by `inspect.componentCode` and
//! used by `inspect.code`. A component connected to code (see [`crate::code_connect`]) shows its
//! real snippet instead.
use figcraft_doc::*;
use serde_json::{Value, json};
use crate::cmd::CommandSpec;
use crate::codegen::{CodeLang, Ctx, Markup, class_name, compose, indent, literal, swift};
use crate::codegen::{CodeLang, Ctx, Markup, arkui, class_name, compose, indent, literal, swift};
use crate::components::{prop_default_value, slot_edited, slots_of, variant_options, variant_props, variant_set};
use crate::params::*;
use crate::{bad, spec};
@@ -67,14 +68,14 @@ pub fn specs() -> Vec<CommandSpec> {
vec![spec!(
"inspect.componentCode",
"Component code",
"{id? (component, set or instance; default the selection), language?: react|leptos|vue|swiftui|compose (default react)} → {component, name, set, properties: [{name, param, type, default, value? (instances)}], definition, usage? (instances; the connected snippet when the component is connected to code), connected: {importPath, name, template, import, code} | null}",
"{id? (component, set or instance; default the selection), language?: react|leptos|vue|swiftui|compose|arkui (default react)} → {component, name, set, properties: [{name, param, type, default, value? (instances)}], definition, usage? (instances; the connected snippet when the component is connected to code), connected: {importPath, name, template, import, code} | null}",
false,
|s, p| {
let cmd = "inspect.componentCode";
let id = targets(s, p).first().copied().ok_or_else(|| bad(cmd, "select a component or instance"))?;
let lang = match string(p, "language") {
// One list of code languages, shared with codeConnect (see `code_connect::language`).
Some(l) => crate::code_connect::language(&l).ok_or_else(|| bad(cmd, "language: react|leptos|vue|swiftui|compose"))?,
Some(l) => crate::code_connect::language(&l).ok_or_else(|| bad(cmd, "language: react|leptos|vue|swiftui|compose|arkui"))?,
None => CodeLang::React,
};
let doc = s.doc()?;
@@ -107,16 +108,17 @@ pub fn specs() -> Vec<CommandSpec> {
)]
}
/// Code for any layer in React, Leptos, Vue, SwiftUI or Compose: an instance's call and its component's
/// definition, a component's (or set's) definition, or the layer tree (React and Leptos wrap it
/// in a function component).
/// Code for any layer in React, Leptos, Vue, SwiftUI, Compose or ArkUI: an instance's call and its
/// component's definition, a component's (or set's) definition, or the layer tree (React and
/// Leptos wrap it in a function component), with the imports of the connected components it calls
/// at the top.
pub fn code(doc: &Document, id: NodeId, lang: CodeLang) -> String {
let Some(n) = doc.node(id) else { return String::new() };
if let Some(code) = crate::code_connect::link(doc, id, lang).and_then(|l| crate::code_connect::connected_code(doc, id, lang, l)) {
return code;
}
let comp = main_of(doc, id).and_then(|m| describe(doc, m));
match (&n.kind, comp) {
let code = match (&n.kind, comp) {
(NodeKind::Instance { .. }, Some(c)) => format!("{}\n{}", usage(doc, id, lang, 0).unwrap_or_default(), definition(doc, &c, lang)),
(NodeKind::Component { .. } | NodeKind::ComponentSet { .. }, Some(c)) => definition(doc, &c, lang),
_ => match lang {
@@ -133,13 +135,19 @@ pub fn code(doc: &Document, id: NodeId, lang: CodeLang) -> String {
swift(&Ctx::new(doc, id), id, 0, &mut out, &[]);
out
}
CodeLang::ArkUi => {
let mut out = String::new();
arkui(&Ctx::new(doc, id), id, 0, &mut out, &[]);
out
}
_ => {
let mut out = String::new();
compose(&Ctx::new(doc, id), id, 0, &mut out);
out
}
},
}
};
crate::code_connect::with_imports(&crate::code_connect::tree_imports(doc, id, lang), code)
}
fn is_component(doc: &Document, id: NodeId) -> bool {
@@ -228,7 +236,9 @@ pub fn type_name(name: &str) -> String {
/// Words no generated parameter may be called (keywords of TypeScript, Swift and Kotlin, and
/// names the generated code uses).
const RESERVED: &[&str] = &[
"arguments",
"as",
"await",
"body",
"break",
"case",
@@ -242,6 +252,7 @@ const RESERVED: &[&str] = &[
"do",
"else",
"enum",
"eval",
"export",
"extends",
"extension",
@@ -252,6 +263,7 @@ const RESERVED: &[&str] = &[
"function",
"guard",
"if",
"implements",
"import",
"in",
"init",
@@ -267,6 +279,7 @@ const RESERVED: &[&str] = &[
"operator",
"package",
"private",
"protected",
"protocol",
"public",
"ref",
@@ -293,7 +306,7 @@ const RESERVED: &[&str] = &[
"yield",
];
/// Types the generated SwiftUI and Compose code names: a variant enum may not shadow them.
/// Types the generated SwiftUI, Compose and ArkUI code names: a variant enum may not shadow them.
const TYPES: &[&str] = &[
"Alignment",
"AnyView",
@@ -302,6 +315,7 @@ const TYPES: &[&str] = &[
"Boolean",
"Box",
"CaseIterable",
"Circle",
"CircleShape",
"Color",
"Column",
@@ -310,21 +324,30 @@ const TYPES: &[&str] = &[
"EdgeInsets",
"Ellipse",
"EmptyView",
"FlexAlign",
"Font",
"FontStyle",
"FontWeight",
"Group",
"HStack",
"HorizontalAlign",
"Image",
"Modifier",
"Rect",
"RectangleShape",
"RoundedCornerShape",
"RoundedRectangle",
"Row",
"Shape",
"Stack",
"String",
"Text",
"TextAlign",
"TextCase",
"TextDecorationType",
"Unit",
"VStack",
"VerticalAlign",
"View",
"ZStack",
];
@@ -511,6 +534,9 @@ pub(crate) fn value_expr(doc: &Document, c: &Component, p: &Param, value: &Value
(Some(t), CodeLang::Leptos) => format!("|| view! {{ <{} /> }}", rust_type(&t)),
(None, CodeLang::Leptos) => "|| ()".into(),
(Some(t), CodeLang::SwiftUi) => format!("AnyView({t}())"),
// ArkUI takes a builder: `@BuilderParam` is a `() => void`.
(Some(t), CodeLang::ArkUi) => format!("() => {{ {t}() }}"),
(None, CodeLang::ArkUi) => "() => {}".into(),
(Some(t), _) => format!("{{ {t}() }}"),
(None, _) if js => "null".into(),
(None, CodeLang::SwiftUi) => "AnyView(EmptyView())".into(),
@@ -569,8 +595,7 @@ pub(crate) fn usage_with_styles(doc: &Document, id: NodeId, lang: CodeLang, dept
let c = main_of(doc, id).and_then(|m| describe(doc, m))?;
let values = instance_values(doc, id, &c)?;
let pad = if matches!(lang, CodeLang::React | CodeLang::Vue) { " ".repeat(depth) } else { indent(depth) };
// A connected component: its real snippet. shortcut: inside a layer tree it brings no import
// line; collect them at the top of the tree's code when people ask for it.
// A connected component: its real snippet (its import is at the top of the code: `code`).
if let Some(link) = crate::code_connect::link(doc, id, lang) {
return Some((crate::code_connect::render(doc, &c, &values, link, lang, &pad), String::new()));
}
@@ -638,6 +663,11 @@ pub(crate) fn usage_with_styles(doc: &Document, id: NodeId, lang: CodeLang, dept
if slots.is_empty() { format!("{pad}<{t}{attrs} />\n") } else { format!("{pad}<{t}{attrs}>\n{slots}{pad}</{t}>\n") }
}
CodeLang::SwiftUi => format!("{pad}{t}({})\n", args.iter().map(|(n, e, _)| format!("{n}: {e}")).collect::<Vec<_>>().join(", ")),
CodeLang::ArkUi => {
let list = args.iter().map(|(n, e, _)| format!("{n}: {e}")).collect::<Vec<_>>().join(", ");
// ArkTS calls take an object literal, or nothing at all.
if list.is_empty() { format!("{pad}{t}()\n") } else { format!("{pad}{t}({{ {list} }})\n") }
}
_ => format!("{pad}{t}({})\n", args.iter().map(|(n, e, _)| format!("{n} = {e}")).collect::<Vec<_>>().join(", ")),
};
Some((code, slot_css))
@@ -684,6 +714,13 @@ fn slot_content(doc: &Document, slot: NodeId, lang: CodeLang, depth: usize, pref
}
(format!("AnyView(Group {{\n{out}{}}})", indent(depth + 1)), String::new())
}
CodeLang::ArkUi => {
let mut out = String::new();
for k in kids {
arkui(&ctx, k, depth + 2, &mut out, &[]);
}
(format!("() => {{\n{out}{}}}", indent(depth + 1)), String::new())
}
_ => {
let mut out = String::new();
for k in kids {
@@ -703,6 +740,7 @@ pub fn definition(doc: &Document, c: &Component, lang: CodeLang) -> String {
CodeLang::Compose => compose_definition(doc, c),
CodeLang::Leptos => leptos(doc, c),
CodeLang::Vue => vue(doc, c),
CodeLang::ArkUi => arkui_definition(doc, c),
_ => react(doc, c),
}
}
@@ -933,3 +971,67 @@ fn compose_definition(doc: &Document, c: &Component) -> String {
out.push_str("}\n");
out
}
/// An ArkUI (`@Component`) struct: variant enums, `@Prop` parameters, a default builder for each
/// instance swap and slot, and the `build` body.
fn arkui_definition(doc: &Document, c: &Component) -> String {
let ctx = Ctx { doc, root: c.comp, comp: Some(c), base: 1 };
let mut out = String::new();
for p in &c.params {
if let ParamKind::Variant(opts) = &p.kind {
out.push_str(&format!("export enum {} {{ {} }}\n\n", enum_name(c, p, false), cases(opts, false).join(", ")));
}
}
// A default builder per instance swap or slot, named apart from every parameter.
let mut used: Vec<String> = c.params.iter().map(|p| p.ident.clone()).collect();
let mut builders: Vec<(String, String)> = Vec::new();
for p in &c.params {
if !matches!(p.kind, ParamKind::InstanceSwap | ParamKind::Slot) {
continue;
}
let base = format!("default{}Builder", capitalized(&p.ident));
let name = (1..).map(|k| if k == 1 { base.clone() } else { format!("{base}{k}") }).find(|n| !used.contains(n)).unwrap_or(base);
used.push(name.clone());
builders.push((p.ident.clone(), name));
}
out.push_str("@Component\nexport struct ");
out.push_str(&c.type_name);
out.push_str(" {\n");
for p in &c.params {
match &p.kind {
ParamKind::Variant(_) => {
out.push_str(&format!(" @Prop {}: {} = {}\n", p.ident, enum_name(c, p, false), value_expr(doc, c, p, &p.default, CodeLang::ArkUi)))
}
ParamKind::Boolean => out.push_str(&format!(" @Prop {}: boolean = {}\n", p.ident, p.default.as_bool().unwrap_or(false))),
ParamKind::Text => out.push_str(&format!(" @Prop {}: string = {}\n", p.ident, value_expr(doc, c, p, &p.default, CodeLang::ArkUi))),
ParamKind::InstanceSwap | ParamKind::Slot => {
let name = builders.iter().find(|(ident, _)| *ident == p.ident).map_or("", |(_, name)| name.as_str());
out.push_str(&format!(" @BuilderParam {}: () => void = this.{name}\n", p.ident));
}
}
}
for (ident, name) in &builders {
let Some(p) = c.params.iter().find(|p| p.ident == *ident) else { continue };
out.push_str(&format!("\n @Builder\n {name}() {{\n"));
match &p.kind {
ParamKind::InstanceSwap => {
if let Some(t) = id_of(&p.default).and_then(|i| component_type(doc, i)) {
out.push_str(&format!(" {t}()\n"));
}
}
ParamKind::Slot => {
let layer = slots_of(doc, c.comp).into_iter().find(|(_, slot)| slot == &p.prop).map(|(layer, _)| layer);
let kids = layer.and_then(|l| doc.node(l)).map(|n| ctx.kids(n)).unwrap_or_default();
for k in kids {
arkui(&ctx, k, 2, &mut out, &[]);
}
}
_ => {}
}
out.push_str(" }\n");
}
out.push_str("\n build() {\n");
arkui(&ctx, c.comp, 2, &mut out, &[]);
out.push_str(" }\n}\n");
out
}