diff --git a/CHANGELOG.md b/CHANGELOG.md index ca776bd10..1ee7da516 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ All notable changes to this project will be documented in this file. - One can now omit the type of a two way binding property - One can declare callback aliases - `abs()` function to get the absolute value + - Ability to name the root element of an `if` or `for` ### Fixed diff --git a/docs/langref.md b/docs/langref.md index c98b0a3e3..b7922543e 100644 --- a/docs/langref.md +++ b/docs/langref.md @@ -546,8 +546,19 @@ clicked => { ; } ## Repetition -The `for` syntax +The `for`-`in` syntax can be used to repeat an element. +The sytax look like this: `for name[index] in model : id := Element { ... }` + +The *model* can be of the following type: + - an integer, in which case the element will be repeated that amount of time + - an array type or a model declared natively, in which case the element will be instantiated for each element in the array or model. + +The *name* will be available for lookup within the element and is going to be like a pseudo-property set to the +value of the model. The *index* is optional and will be set to the index of this element in the model. +The *id* is also optional. + +### Examples ```60 Example := Window { @@ -562,6 +573,37 @@ Example := Window { } ``` +```60 +Example := Window { + height: 50px; + width: 50px; + property <[{foo: string, col: color}]> model: [ + {foo: "abc", col: #f00 }, + {foo: "def", col: #00f }, + ]; + VerticalLayout { + for data in root.model: my_repeated_text := Text { + color: data.col; + text: data.foo; + } + } +} +``` + +## Conditional element + +Similar to `for`, the `if` construct can instentiate element only if a given condition is true. +The syntax is `if (condition) : id := Element { ... }` + +```60 +Example := Window { + height: 50px; + width: 50px; + if (true) : foo := Rectangle { background: blue; } + if (false) : Rectangle { background: red; } +} +``` + ## Animations Simple animation that animates a property can be declared with `animate` like so: diff --git a/sixtyfps_compiler/object_tree.rs b/sixtyfps_compiler/object_tree.rs index ddb456aed..bd65e4fc3 100644 --- a/sixtyfps_compiler/object_tree.rs +++ b/sixtyfps_compiler/object_tree.rs @@ -728,26 +728,14 @@ impl Element { for se in node.children() { if se.kind() == SyntaxKind::SubElement { - let id = identifier_text(&se).unwrap_or_default(); - if matches!(id.as_ref(), "parent" | "self" | "root") { - diag.push_error( - format!("'{}' is a reserved id", id), - &se.child_token(SyntaxKind::Identifier).unwrap(), - ) - } - if let Some(element_node) = se.child_node(SyntaxKind::Element) { - let parent_type = r.borrow().base_type.clone(); - r.borrow_mut().children.push(Element::from_node( - element_node.into(), - id, - parent_type, - component_child_insertion_point, - diag, - tr, - )); - } else { - assert!(diag.has_error()); - } + let parent_type = r.borrow().base_type.clone(); + r.borrow_mut().children.push(Element::from_sub_element_node( + se.into(), + parent_type, + component_child_insertion_point, + diag, + tr, + )); } else if se.kind() == SyntaxKind::RepeatedElement { let rep = Element::from_repeated_node( se.into(), @@ -832,6 +820,30 @@ impl Element { r } + fn from_sub_element_node( + node: syntax_nodes::SubElement, + parent_type: Type, + component_child_insertion_point: &mut Option, + diag: &mut BuildDiagnostics, + tr: &TypeRegister, + ) -> ElementRc { + let id = identifier_text(&node).unwrap_or_default(); + if matches!(id.as_ref(), "parent" | "self" | "root") { + diag.push_error( + format!("'{}' is a reserved id", id), + &node.child_token(SyntaxKind::Identifier).unwrap(), + ) + } + Element::from_node( + node.Element(), + id, + parent_type, + component_child_insertion_point, + diag, + tr, + ) + } + fn from_repeated_node( node: syntax_nodes::RepeatedElement, parent: &ElementRc, @@ -860,10 +872,9 @@ impl Element { is_conditional_element: false, is_listview, }; - let e = Element::from_node( - node.Element(), - String::new(), - parent.borrow().base_type.to_owned(), + let e = Element::from_sub_element_node( + node.SubElement(), + parent.borrow().base_type.clone(), component_child_insertion_point, diag, tr, @@ -886,9 +897,8 @@ impl Element { is_conditional_element: true, is_listview: None, }; - let e = Element::from_node( - node.Element(), - String::new(), + let e = Element::from_sub_element_node( + node.SubElement(), parent_type, component_child_insertion_point, diag, diff --git a/sixtyfps_compiler/parser.rs b/sixtyfps_compiler/parser.rs index d7c6a642d..55c429921 100644 --- a/sixtyfps_compiler/parser.rs +++ b/sixtyfps_compiler/parser.rs @@ -323,9 +323,9 @@ declare_syntax! { Element -> [ ?QualifiedName, *PropertyDeclaration, *Binding, *CallbackConnection, *CallbackDeclaration, *SubElement, *RepeatedElement, *PropertyAnimation, *TwoWayBinding, *States, *Transitions, ?ChildrenPlaceholder ], - RepeatedElement -> [ ?DeclaredIdentifier, ?RepeatedIndex, Expression , Element], + RepeatedElement -> [ ?DeclaredIdentifier, ?RepeatedIndex, Expression , SubElement], RepeatedIndex -> [], - ConditionalElement -> [ Expression , Element], + ConditionalElement -> [ Expression , SubElement], CallbackDeclaration -> [ DeclaredIdentifier, *Type, ?ReturnType, ?TwoWayBinding ], /// `-> type` (but without the ->) ReturnType -> [Type], diff --git a/sixtyfps_compiler/parser/element.rs b/sixtyfps_compiler/parser/element.rs index a39f4e8ae..a91b61791 100644 --- a/sixtyfps_compiler/parser/element.rs +++ b/sixtyfps_compiler/parser/element.rs @@ -125,7 +125,7 @@ pub fn parse_element_content(p: &mut impl Parser) { fn parse_sub_element(p: &mut impl Parser) { let mut p = p.start_node(SyntaxKind::SubElement); if p.nth(1).kind() == SyntaxKind::ColonEqual { - assert!(p.expect(SyntaxKind::Identifier)); + p.expect(SyntaxKind::Identifier); p.expect(SyntaxKind::ColonEqual); } parse_element(&mut *p); @@ -136,6 +136,7 @@ fn parse_sub_element(p: &mut impl Parser) { /// for xx in mm: Elem { } /// for [idx] in mm: Elem { } /// for xx [idx] in foo.bar: Elem { } +/// for _ in (xxx()): blah := Elem { Elem{} } /// ``` /// Must consume at least one token fn parse_repeated_element(p: &mut impl Parser) { @@ -155,19 +156,20 @@ fn parse_repeated_element(p: &mut impl Parser) { if p.peek().as_str() != "in" { p.error("Invalid 'for' syntax: there should be a 'in' token"); drop(p.start_node(SyntaxKind::Expression)); - drop(p.start_node(SyntaxKind::Element)); + drop(p.start_node(SyntaxKind::SubElement).start_node(SyntaxKind::Element)); return; } p.consume(); // "in" parse_expression(&mut *p); p.expect(SyntaxKind::Colon); - parse_element(&mut *p); + parse_sub_element(&mut *p); } #[cfg_attr(test, parser_test)] /// ```test,ConditionalElement /// if (condition) : Elem { } /// if (foo ? bar : xx) : Elem { foo:bar; Elem {}} +/// if (true) : foo := Elem {} /// ``` /// Must consume at least one token fn parse_if_element(p: &mut impl Parser) { @@ -176,15 +178,15 @@ fn parse_if_element(p: &mut impl Parser) { p.consume(); // "if" if !p.expect(SyntaxKind::LParent) { drop(p.start_node(SyntaxKind::Expression)); - drop(p.start_node(SyntaxKind::Element)); + drop(p.start_node(SyntaxKind::SubElement).start_node(SyntaxKind::Element)); return; } parse_expression(&mut *p); if !p.expect(SyntaxKind::RParent) || !p.expect(SyntaxKind::Colon) { - drop(p.start_node(SyntaxKind::Element)); + drop(p.start_node(SyntaxKind::SubElement).start_node(SyntaxKind::Element)); return; } - parse_element(&mut *p); + parse_sub_element(&mut *p); } #[cfg_attr(test, parser_test)] diff --git a/sixtyfps_compiler/tests/syntax/basic/sub_elements.60 b/sixtyfps_compiler/tests/syntax/basic/sub_elements.60 index be953da9e..4e0c0854f 100644 --- a/sixtyfps_compiler/tests/syntax/basic/sub_elements.60 +++ b/sixtyfps_compiler/tests/syntax/basic/sub_elements.60 @@ -29,5 +29,12 @@ SubElements := Rectangle { self := Rectangle {} // ^error{'self' is a reserved id} } + + if (true) : root := Rectangle { +// ^error{'root' is a reserved id} + + for _ in 1 : self := Rectangle { } +// ^error{'self' is a reserved id} + } } diff --git a/sixtyfps_compiler/tests/syntax/lookup/for_lookup.60 b/sixtyfps_compiler/tests/syntax/lookup/for_lookup.60 index 52c3b98d3..7d8da95a7 100644 --- a/sixtyfps_compiler/tests/syntax/lookup/for_lookup.60 +++ b/sixtyfps_compiler/tests/syntax/lookup/for_lookup.60 @@ -31,8 +31,16 @@ Hello := Rectangle { // ^error{Cannot access id 'ccc'} } - Text { text: ccc.text; } - // ^error{Cannot access id 'ccc'} + for plop in 0 : named_for := Rectangle { + Text { color: named_for.background; } + } + + Text { + text: ccc.text; + // ^error{Cannot access id 'ccc'} + color: named_for.background; + // ^error{Cannot access id 'named_for'} + } for aaa in aaa.text: Rectangle { // ^error{Cannot convert string to model} diff --git a/tests/cases/models/for.60 b/tests/cases/models/for.60 index 6a42c3d4b..73ff2bd40 100644 --- a/tests/cases/models/for.60 +++ b/tests/cases/models/for.60 @@ -16,16 +16,18 @@ Extra2 := Rectangle { property top_level; property value; callback update_value; - for aaa[r] in [[10, top_level], [2, 3]] : Rectangle { + for aaa[r] in [[10, top_level], [2, 3]] : blah := Rectangle { width: parent.width; height: root.height; + property some_value: 1000; for bb[l] in aaa : TouchArea { + property some_value: 1515; width: 10phx; height: 10phx; x: r*10phx; y: l*10phx; clicked => { - root.value += bb; + root.value += bb + blah.some_value; update_value(); } } @@ -96,13 +98,13 @@ auto handle = TestCase::create(); const TestCase &instance = *handle; sixtyfps::testing::send_mouse_click(&instance, 5., 5.); -assert_eq(instance.get_value(), 10); +assert_eq(instance.get_value(), 1010); sixtyfps::testing::send_mouse_click(&instance, 15., 15.); -assert_eq(instance.get_value(), 13); +assert_eq(instance.get_value(), 2013); sixtyfps::testing::send_mouse_click(&instance, 5., 15.); -assert_eq(instance.get_value(), 13+42); +assert_eq(instance.get_value(), 3000+13+42); ``` @@ -110,28 +112,28 @@ assert_eq(instance.get_value(), 13+42); let instance = TestCase::new(); sixtyfps::testing::send_mouse_click(&instance, 5., 5.); -assert_eq!(instance.get_value(), 10); +assert_eq!(instance.get_value(), 1010); sixtyfps::testing::send_mouse_click(&instance, 15., 15.); -assert_eq!(instance.get_value(), 13); +assert_eq!(instance.get_value(), 2013); sixtyfps::testing::send_mouse_click(&instance, 5., 15.); -assert_eq!(instance.get_value(), 13+42); +assert_eq!(instance.get_value(), 3000+13+42); ``` ```js var instance = new sixtyfps.TestCase(); instance.send_mouse_click(5., 5.); -assert.equal(instance.value, 10); +assert.equal(instance.value, 1010); instance.cond1 = true; instance.send_mouse_click(15., 15.); -assert.equal(instance.value, 13); +assert.equal(instance.value, 2013); instance.cond1 = false; instance.send_mouse_click(5., 15.); -assert.equal(instance.value, 13+42); +assert.equal(instance.value, 3000+13+42); ``` */