mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-30 21:36:46 -06:00
M7: land Flutter code generation on main's code targets
main split `codegen.rs` into `dev/codegen.rs` (the shell: `CodeLang`, `code_for`, `Ctx`, the shared helpers) and one module per native target (`css`, `html`, `swift`, `compose`, `arkui`), and added Vue and ArkUI from other PRs. Flutter arrives as the tenth target, in its own module: `dev/codegen/flutter.rs`, wired from `codegen.rs` (`CodeLang::Flutter`, `dart` as an alias, rem left to the web targets), with `flutter_definition` / `flutter_type` / `flutter_default` beside main's `arkui_definition`. Every language list in the tree is a union of the two sides now: the `inspect.code` and `inspect.componentCode` specs, `codeConnect` (parse, import line, expression, template, path checks, messages), the playground CSS/HTML assets, the 21 UI catalogues (main added eight after this branch: bn, fi, it, ka, nl, pl, th, vi, each with Flutter in its own conjunction) and the docs. Flutter is an extra code target; nothing main gained is dropped. The reviewer's absolute-child bug (#1382-1403: a child that ignores its auto layout parent fell through to `Place::Flow`) is fixed in `flutter_children`: flow and absolute children are split, the absolute ones go in a `Stack` around the Row/Column at the parent's own box (so their offsets stay the parent's and the padding moves onto the line instead of insetting them), the line fills that Stack (`Positioned.fill`, so `Expanded` still has a bounded width), and only flow children now decide `CrossAxisAlignment.stretch` and `Expanded` — the absolute child no longer joins the line's spacing or loses its offsets. `flutter_lifts_a_child_that_ignores_its_auto_layout_parent_out_of_the_line` covers both kinds of child in one auto-layout frame: a 50×20 flow child and a 30×30 absolute one asking for `fill` on both axes, in a parent with 4 pt padding. It checks the offset, that the absolute child is out of the Row, that no `Expanded`/stretch comes from it, and that the padding insets the line only. ROADMAP and dev-mode-parity keep main's header (2026-10-26) and take this branch's history row in date order (2026-10-16). `cargo test`: engine 1391 pass, ui-egui 814 + 8 + 1 pass; fmt and clippy clean. The one engine failure, `board_tables_tests::a_double_click…`, fails the same way on a pristine `origin/main` here: this sandbox has no `craft-fonts` checkout, so its text doesn't wrap and the row can't grow.
This commit is contained in:
@@ -43,6 +43,18 @@ fn button_set(s: &mut Session) -> (NodeId, NodeId, NodeId) {
|
||||
(set, variants[0], variants[1])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strict_mode_reserved_words_are_not_parameter_names() {
|
||||
let mut s = Session::new();
|
||||
let (c, title, _) = card(&mut s);
|
||||
ex(&mut s, "component.addProperty", json!({"id": c, "name": "Protected", "type": "text", "layer": title}));
|
||||
let react = code(&mut s, c, "react");
|
||||
assert!(react.contains("protectedValue"), "React renames the property:\n{react}");
|
||||
for bad in [" protected?", " protected =", "{protected}"] {
|
||||
assert!(!react.contains(bad), "reserved word used as a binding ({bad:?}):\n{react}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn components_take_their_properties_as_parameters() {
|
||||
let mut s = Session::new();
|
||||
@@ -71,6 +83,17 @@ fn components_take_their_properties_as_parameters() {
|
||||
for want in ["@Composable\nfun Card(", " showDot: Boolean = true,", " title: String = \"Hello\",", "if (showDot) {", "text = title,"] {
|
||||
assert!(kt.contains(want), "Compose has {want:?}:\n{kt}");
|
||||
}
|
||||
let ark = code(&mut s, c, "arkui");
|
||||
for want in [
|
||||
"@Component\nexport struct Card {",
|
||||
" @Prop showDot: boolean = true",
|
||||
" @Prop title: string = \"Hello\"",
|
||||
" build() {",
|
||||
" if (this.showDot) {",
|
||||
"Text(this.title)",
|
||||
] {
|
||||
assert!(ark.contains(want), "ArkUI has {want:?}:\n{ark}");
|
||||
}
|
||||
let leptos = code(&mut s, c, "leptos");
|
||||
for want in [
|
||||
"use leptos::prelude::*;\n\n#[component]\npub fn Card(\n",
|
||||
@@ -129,6 +152,7 @@ fn instances_are_calls_passing_the_values_they_set() {
|
||||
assert_eq!(component_code(&mut s, i, "swiftui")["usage"], "Card(showDot: false, title: \"Buy\")\n");
|
||||
assert_eq!(component_code(&mut s, i, "compose")["usage"], "Card(showDot = false, title = \"Buy\")\n");
|
||||
assert_eq!(component_code(&mut s, i, "flutter")["usage"], "Card(showDot: false, title: \"Buy\")\n");
|
||||
assert_eq!(component_code(&mut s, i, "arkui")["usage"], "Card({ showDot: false, title: \"Buy\" })\n");
|
||||
assert_eq!(component_code(&mut s, i, "leptos")["usage"], "<Card show_dot={false} title=\"Buy\" />\n");
|
||||
// Text that needs escaping is an expression in JSX and escaped per language.
|
||||
ex(&mut s, "instance.setProperties", json!({"ids": [i], "properties": {"Title": "Say \"hi\" $5\nnow"}}));
|
||||
@@ -136,7 +160,19 @@ fn instances_are_calls_passing_the_values_they_set() {
|
||||
assert_eq!(component_code(&mut s, i, "swiftui")["usage"], "Card(showDot: false, title: \"Say \\\"hi\\\" $5\\nnow\")\n");
|
||||
assert_eq!(component_code(&mut s, i, "compose")["usage"], "Card(showDot = false, title = \"Say \\\"hi\\\" \\$5\\nnow\")\n");
|
||||
assert_eq!(component_code(&mut s, i, "flutter")["usage"], "Card(showDot: false, title: \"Say \\\"hi\\\" \\$5\\nnow\")\n");
|
||||
assert_eq!(component_code(&mut s, i, "arkui")["usage"], "Card({ showDot: false, title: \"Say \\\"hi\\\" $5\\nnow\" })\n");
|
||||
assert_eq!(component_code(&mut s, i, "leptos")["usage"], "<Card show_dot={false} title={\"Say \\\"hi\\\" $5\\nnow\"} />\n");
|
||||
// A quoted title becomes a binding whose attribute value is escaped: the expression itself
|
||||
// keeps its JS quotes, and the attribute's own quotes are entities.
|
||||
assert_eq!(component_code(&mut s, i, "vue")["usage"], "<Card :showDot=\"false\" :title=\""Say \\"hi\\" $5\\nnow"\" />\n");
|
||||
let vue = code(&mut s, i, "vue");
|
||||
assert!(vue.starts_with("<Card :showDot=\"false\" :title=\""Say \\"hi\\" $5\\nnow"\" />\n"), "{vue}");
|
||||
// A plain attribute keeps HTML entities intact: a literal ampersand is written as `&`,
|
||||
// and a literal `&` as `&amp;`.
|
||||
ex(&mut s, "instance.setProperties", json!({"ids": [i], "properties": {"Title": "Fish & Chips"}}));
|
||||
assert_eq!(component_code(&mut s, i, "vue")["usage"], "<Card :showDot=\"false\" title=\"Fish & Chips\" />\n");
|
||||
ex(&mut s, "instance.setProperties", json!({"ids": [i], "properties": {"Title": "Fish & Chips"}}));
|
||||
assert_eq!(component_code(&mut s, i, "vue")["usage"], "<Card :showDot=\"false\" title=\"Fish &amp; Chips\" />\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -192,6 +228,9 @@ fn variant_sets_share_one_definition() {
|
||||
assert!(dart.contains("enum ButtonState { defaultValue, hover }\n"), "{dart}");
|
||||
assert!(dart.contains(" this.state = ButtonState.defaultValue,\n"), "{dart}");
|
||||
assert!(dart.contains(" final ButtonState state;\n"), "{dart}");
|
||||
let ark = code(&mut s, set, "arkui");
|
||||
assert!(ark.contains("export enum ButtonState { Default, Hover }"), "{ark}");
|
||||
assert!(ark.contains(" @Prop state: ButtonState = ButtonState.Default"), "{ark}");
|
||||
let leptos = code(&mut s, set, "leptos");
|
||||
for want in [
|
||||
"#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]\npub enum ButtonState {\n #[default]\n Default,\n Hover,\n}",
|
||||
@@ -209,6 +248,7 @@ fn variant_sets_share_one_definition() {
|
||||
assert_eq!(component_code(&mut s, i, "swiftui")["usage"], "Button(state: .hover)\n");
|
||||
assert_eq!(component_code(&mut s, i, "compose")["usage"], "Button(state = ButtonState.Hover)\n");
|
||||
assert_eq!(component_code(&mut s, i, "flutter")["usage"], "Button(state: ButtonState.hover)\n");
|
||||
assert_eq!(component_code(&mut s, i, "arkui")["usage"], "Button({ state: ButtonState.Hover })\n");
|
||||
assert_eq!(component_code(&mut s, i, "leptos")["usage"], "<Button state=ButtonState::Hover />\n");
|
||||
}
|
||||
|
||||
@@ -236,16 +276,30 @@ fn instance_swap_properties_take_views() {
|
||||
assert!(dart.contains(" final Widget icon;\n") && dart.contains(" this.icon = const HeartIcon(),\n"), "{dart}");
|
||||
assert!(dart.contains("child: icon\n"), "the nested instance is the parameter: {dart}");
|
||||
assert!(!dart.contains("child: Container("), "the swap's widget tree isn't spelled out: {dart}");
|
||||
let ark = code(&mut s, chip, "arkui");
|
||||
for want in [
|
||||
" @BuilderParam icon: () => void = this.defaultIconBuilder",
|
||||
" @Builder\n defaultIconBuilder() {\n HeartIcon()\n }",
|
||||
" this.icon()",
|
||||
] {
|
||||
assert!(ark.contains(want), "ArkUI has {want:?}:\n{ark}");
|
||||
}
|
||||
let leptos = code(&mut s, chip, "leptos");
|
||||
assert!(leptos.contains(" #[prop(into, default = (|| view! { <HeartIcon /> }).into())] icon: ViewFn,"), "{leptos}");
|
||||
assert!(leptos.contains("{icon.run()}"), "{leptos}");
|
||||
let vue = code(&mut s, chip, "vue");
|
||||
assert!(vue.contains(" icon?: unknown;"), "{vue}");
|
||||
assert!(vue.contains(" icon: HeartIcon,"), "the swap defaults to its component: {vue}");
|
||||
assert!(vue.contains("<component :is=\"icon\" />"), "a swap is a component, not an element: {vue}");
|
||||
let i = idv(&ex(&mut s, "instance.create", json!({"component": chip, "x": 0, "y": 100})));
|
||||
ex(&mut s, "instance.setProperties", json!({"ids": [i], "properties": {"Icon": star}}));
|
||||
assert_eq!(component_code(&mut s, i, "react")["usage"], "<Chip icon={<StarIcon />} />\n");
|
||||
assert_eq!(component_code(&mut s, i, "swiftui")["usage"], "Chip(icon: AnyView(StarIcon()))\n");
|
||||
assert_eq!(component_code(&mut s, i, "compose")["usage"], "Chip(icon = { StarIcon() })\n");
|
||||
assert_eq!(component_code(&mut s, i, "flutter")["usage"], "Chip(icon: StarIcon())\n");
|
||||
assert_eq!(component_code(&mut s, i, "arkui")["usage"], "Chip({ icon: () => { StarIcon() } })\n");
|
||||
assert_eq!(component_code(&mut s, i, "leptos")["usage"], "<Chip icon={|| view! { <StarIcon /> }} />\n");
|
||||
assert_eq!(component_code(&mut s, i, "vue")["usage"], "<Chip :icon=\"StarIcon\" />\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -263,6 +317,14 @@ fn slots_take_optional_content() {
|
||||
let dart = code(&mut s, panel, "flutter");
|
||||
assert!(dart.contains(" final List<Widget>? content;\n") && dart.contains(" this.content,\n"), "{dart}");
|
||||
assert!(dart.contains("...?content,\n") && dart.contains("if (content == null) ...[\n"), "{dart}");
|
||||
let ark = code(&mut s, panel, "arkui");
|
||||
for want in [
|
||||
" @BuilderParam content: () => void = this.defaultContentBuilder",
|
||||
" @Builder\n defaultContentBuilder() {\n",
|
||||
" this.content()",
|
||||
] {
|
||||
assert!(ark.contains(want), "ArkUI has {want:?}:\n{ark}");
|
||||
}
|
||||
let leptos = code(&mut s, panel, "leptos");
|
||||
assert!(leptos.contains(" #[prop(optional, into)] content: Option<ViewFn>,"), "{leptos}");
|
||||
assert!(leptos.contains("{match &content { Some(c) => c.run(), None => view! {\n") && leptos.contains("}.into_any() }}\n"), "{leptos}");
|
||||
@@ -279,6 +341,8 @@ fn slots_take_optional_content() {
|
||||
assert!(usage.starts_with("<Panel content={|| view! {\n") && usage.contains("<Badge />") && usage.ends_with("}} />\n"), "{usage}");
|
||||
let usage = component_code(&mut s, i, "flutter")["usage"].as_str().unwrap_or_default().to_string();
|
||||
assert!(usage.starts_with("Panel(content: [\n") && usage.contains("Badge()"), "{usage}");
|
||||
let usage = component_code(&mut s, i, "arkui")["usage"].as_str().unwrap_or_default().to_string();
|
||||
assert!(usage.starts_with("Panel({ content: () => {\n") && usage.contains("Badge()") && usage.ends_with("} })\n"), "{usage}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user