//! Component code: components become React / SwiftUI / Compose components whose properties are //! parameters, and instances become calls passing the values they set. use crate::Session; use crate::codegen::{CodeLang, code_for}; use crate::component_codegen::{ParamKind, describe, main_of}; use crate::components::tests::{ex, idv}; use figcraft_doc::*; use serde_json::{Value, json}; fn code(s: &mut Session, id: NodeId, lang: &str) -> String { ex(s, "inspect.code", json!({"id": id, "language": lang}))["code"].as_str().unwrap_or_default().to_string() } fn component_code(s: &mut Session, id: NodeId, lang: &str) -> Value { ex(s, "inspect.componentCode", json!({"id": id, "language": lang})) } /// A "Card" component with a "Title" text (text property), a "Dot" ellipse (boolean property /// "Show dot") and a "Bg" rectangle. Returns (card, title, dot). fn card(s: &mut Session) -> (NodeId, NodeId, NodeId) { let f = idv(&ex(s, "frame.create", json!({"x": 0, "y": 0, "width": 200, "height": 100, "name": "Card"}))); ex(s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 200, "height": 100, "parent": f, "props": {"name": "Bg", "fill": "#eeeeee"}})); let title = idv(&ex(s, "text.create", json!({"text": "Hello", "x": 10, "y": 10, "parent": f, "props": {"name": "Title"}}))); let dot = idv(&ex(s, "shape.ellipse", json!({"x": 170, "y": 10, "width": 20, "height": 20, "parent": f, "props": {"name": "Dot"}}))); let c = idv(&ex(s, "component.create", json!({"ids": [f]}))); ex(s, "component.addProperty", json!({"id": dot, "name": "Show dot", "type": "boolean", "default": true})); ex(s, "component.addProperty", json!({"id": c, "name": "Title", "type": "text", "layer": title})); (c, title, dot) } /// A "Button" set with State=Default|Hover variants (the hover background blue). Returns (set, /// default variant, hover variant). fn button_set(s: &mut Session) -> (NodeId, NodeId, NodeId) { let mut variants = Vec::new(); for (k, (state, color)) in [("Default", "#ff0000"), ("Hover", "#0000ff")].iter().enumerate() { let f = idv(&ex(s, "frame.create", json!({"x": k as f64 * 150.0, "y": 300, "width": 100, "height": 40, "name": format!("State={state}")}))); ex(s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 100, "height": 40, "parent": f, "props": {"name": "Bg", "fill": color}})); variants.push(idv(&ex(s, "component.create", json!({"ids": [f]})))); } let set = idv(&ex(s, "component.combineVariants", json!({"ids": variants.clone()}))); ex(s, "node.set", json!({"id": set, "props": {"name": "Button"}})); (set, variants[0], variants[1]) } #[test] fn components_take_their_properties_as_parameters() { let mut s = Session::new(); let (c, _, _) = card(&mut s); let react = code(&mut s, c, "react"); for want in [ "export interface CardProps {", " showDot?: boolean;", " title?: string;", "export function Card({", " showDot = true,", " title = \"Hello\",", "}: CardProps) {", "{showDot && (", ">{title}

", "/* card.css */", "border-radius: 50%;", ] { assert!(react.contains(want), "React has {want:?}:\n{react}"); } let swift = code(&mut s, c, "swiftui"); for want in ["struct Card: View {", " var showDot: Bool = true", " var title: String = \"Hello\"", "if showDot {", "Text(title)"] { assert!(swift.contains(want), "SwiftUI has {want:?}:\n{swift}"); } let kt = code(&mut s, c, "compose"); 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}"); } // The command describes the properties. let v = component_code(&mut s, c, "react"); assert_eq!(v["name"], "Card"); assert_eq!( v["properties"], json!([ {"name": "Show dot", "param": "showDot", "type": "boolean", "default": true}, {"name": "Title", "param": "title", "type": "text", "default": "Hello"}, ]) ); assert_eq!(v["definition"].as_str(), Some(react.as_str())); assert!(v.get("usage").is_none(), "a main has no call"); // Deterministic. assert_eq!(code(&mut s, c, "react"), react); } #[test] fn instances_are_calls_passing_the_values_they_set() { let mut s = Session::new(); let (c, _, _) = card(&mut s); let i = idv(&ex(&mut s, "instance.create", json!({"component": c, "x": 300, "y": 0}))); let v = component_code(&mut s, i, "react"); assert_eq!(v["usage"], "\n", "defaults aren't passed"); ex(&mut s, "instance.setProperties", json!({"ids": [i], "properties": {"Show dot": false, "Title": "Buy"}})); let v = component_code(&mut s, i, "react"); assert_eq!(v["usage"], "\n"); assert_eq!(v["properties"][0]["value"], false); assert_eq!(v["properties"][1]["value"], "Buy"); // inspect.code on an instance: the call, then the definition. let react = code(&mut s, i, "react"); assert!(react.starts_with("\n"), "{react}"); assert!(react.contains("export function Card({")); 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"); // 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"}})); assert_eq!(component_code(&mut s, i, "react")["usage"], "\n"); 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"); } #[test] fn nested_instances_in_a_layer_tree_are_calls() { let mut s = Session::new(); let (c, _, _) = card(&mut s); let screen = idv(&ex(&mut s, "frame.create", json!({"x": 0, "y": 500, "width": 400, "height": 300, "name": "Home screen"}))); let i = idv(&ex(&mut s, "instance.create", json!({"component": c, "x": 20, "y": 30, "parent": screen}))); ex(&mut s, "instance.setProperties", json!({"ids": [i], "properties": {"Title": "Inbox"}})); let react = code(&mut s, screen, "react"); assert!(react.contains("export function HomeScreen() {"), "{react}"); assert!(react.contains(""), "{react}"); assert!(!react.contains("Hello"), "the instance's layers aren't spelled out: {react}"); assert!(code(&mut s, screen, "swiftui").contains("Card(title: \"Inbox\")")); assert!(code(&mut s, screen, "compose").contains("Card(title = \"Inbox\")")); // HTML and CSS stay plain markup. assert!(code(&mut s, screen, "html").contains("Inbox")); } #[test] fn variant_sets_share_one_definition() { let mut s = Session::new(); let (set, default, hover) = button_set(&mut s); let doc = s.doc().unwrap(); assert_eq!(main_of(doc, set), Some(default)); assert_eq!(main_of(doc, hover), Some(default), "every variant is defined by the set's first"); let c = describe(doc, default).unwrap(); assert_eq!(c.type_name, "Button"); assert_eq!(c.params.len(), 1); assert_eq!(c.params[0].ident, "state"); assert_eq!(c.params[0].kind, ParamKind::Variant(vec!["Default".into(), "Hover".into()])); let react = code(&mut s, set, "react"); assert!(react.contains(" state?: \"Default\" | \"Hover\";"), "{react}"); assert!(react.contains(" state = \"Default\","), "{react}"); assert!(react.contains("data-state={state}"), "{react}"); // The hover variant's difference is a rule on its data attribute. assert!(react.contains(".button[data-state=\"Hover\"] .bg {\n background: #0000FF;"), "{react}"); let swift = code(&mut s, set, "swiftui"); assert!( swift.contains(" enum State: String, CaseIterable {\n case `default` = \"Default\"\n case hover = \"Hover\""), "{swift}" ); assert!(swift.contains(" var state: State = .`default`"), "{swift}"); let kt = code(&mut s, set, "compose"); assert!(kt.contains("enum class ButtonState { Default, Hover }"), "{kt}"); assert!(kt.contains(" state: ButtonState = ButtonState.Default,"), "{kt}"); // An instance showing the hover variant passes it. let i = idv(&ex(&mut s, "instance.create", json!({"component": set, "x": 0, "y": 600}))); ex(&mut s, "instance.setProperties", json!({"ids": [i], "properties": {"State": "Hover"}})); assert_eq!(component_code(&mut s, i, "react")["usage"], "