Add support for SVG commands in Path elements

Using the commands property we can just paste SVG paths. This makes it
much easier to write examples/demos. A good online path designer is
for example https://codepen.io/anthonydugois/pen/mewdyZ
This commit is contained in:
Simon Hausmann 2020-07-09 11:37:47 +02:00
parent be60079f21
commit 251ef7fc97
14 changed files with 433 additions and 89 deletions

View file

@ -7,6 +7,12 @@ namespace sixtyfps {
using internal::types::PathArcTo; using internal::types::PathArcTo;
using internal::types::PathElement; using internal::types::PathElement;
using internal::types::PathEvent;
using internal::types::PathEventBegin;
using internal::types::PathEventCubic;
using internal::types::PathEventEnd;
using internal::types::PathEventLine;
using internal::types::PathEventQuadratic;
using internal::types::PathLineTo; using internal::types::PathLineTo;
struct PathElements struct PathElements
@ -20,6 +26,11 @@ public:
{ {
} }
PathElements(const PathEvent *firstEvent, size_t count)
: data(Data::PathEvents(events_from_array(firstEvent, count)))
{
}
private: private:
static SharedArray<PathElement> elements_from_array(const PathElement *firstElement, static SharedArray<PathElement> elements_from_array(const PathElement *firstElement,
size_t count) size_t count)
@ -29,6 +40,13 @@ private:
return tmp; return tmp;
} }
static SharedArray<PathEvent> events_from_array(const PathEvent *firstEvent, size_t count)
{
SharedArray<PathEvent> tmp;
sixtyfps_new_path_events(&tmp, firstEvent, count);
return tmp;
}
using Data = internal::types::PathElements; using Data = internal::types::PathElements;
Data data; Data data;
}; };

View file

@ -124,5 +124,13 @@ Hello := Rectangle {
} }
} }
} }
Path {
commands: "M 100 300 Q 150 50 250 150 C 250 300 300 300 300 450 A 50 50 0 1 0 450 450 L 550 300";
x: 100;
y: 500;
stroke_color: black;
stroke_width: 2.0;
}
} }

View file

@ -28,6 +28,7 @@ codemap-diagnostic = { version = "0.1.1", optional = true }
codemap = { version = "0.1", optional = true } codemap = { version = "0.1", optional = true }
quote = { version = "1.0", optional = true } quote = { version = "1.0", optional = true }
proc-macro2 = { version = "1.0.17", optional = true } proc-macro2 = { version = "1.0.17", optional = true }
lyon = { version = "0.15.8", features = ["svg"] }
[dev-dependencies] [dev-dependencies]

View file

@ -131,7 +131,7 @@ pub enum Expression {
}, },
PathElements { PathElements {
elements: Vec<PathElement>, elements: Path,
}, },
} }
@ -248,8 +248,10 @@ impl Expression {
} }
} }
Expression::PathElements { elements } => { Expression::PathElements { elements } => {
for element in elements { if let Path::Elements(elements) = elements {
element.bindings.values().for_each(|binding| visitor(binding)) for element in elements {
element.bindings.values().for_each(|binding| visitor(binding))
}
} }
} }
} }
@ -300,8 +302,10 @@ impl Expression {
} }
} }
Expression::PathElements { elements } => { Expression::PathElements { elements } => {
for element in elements { if let Path::Elements(elements) = elements {
element.bindings.values_mut().for_each(|binding| visitor(binding)) for element in elements {
element.bindings.values_mut().for_each(|binding| visitor(binding))
}
} }
} }
} }
@ -330,7 +334,13 @@ impl Expression {
Expression::Array { values, .. } => values.iter().all(Expression::is_constant), Expression::Array { values, .. } => values.iter().all(Expression::is_constant),
Expression::Object { values, .. } => values.iter().all(|(_, v)| v.is_constant()), Expression::Object { values, .. } => values.iter().all(|(_, v)| v.is_constant()),
Expression::PathElements { elements } => { Expression::PathElements { elements } => {
elements.iter().all(|element| element.bindings.values().all(|v| v.is_constant())) if let Path::Elements(elements) = elements {
elements
.iter()
.all(|element| element.bindings.values().all(|v| v.is_constant()))
} else {
true
}
} }
} }
} }
@ -356,6 +366,14 @@ impl Expression {
} }
} }
pub type PathEvents = Vec<lyon::path::Event<lyon::math::Point, lyon::math::Point>>;
#[derive(Debug, Clone)]
pub enum Path {
Elements(Vec<PathElement>),
Events(PathEvents),
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PathElement { pub struct PathElement {
pub element_type: Rc<BuiltinElement>, pub element_type: Rc<BuiltinElement>,

View file

@ -833,35 +833,50 @@ fn compile_expression(e: &crate::expression_tree::Expression, component: &Rc<Com
panic!("Expression::Object is not a Type::Object") panic!("Expression::Object is not a Type::Object")
} }
} }
PathElements { elements } => { PathElements { elements } => match elements {
let converted_elements: Vec<String> = elements crate::expression_tree::Path::Elements(elements) => {
.iter() let converted_elements: Vec<String> = elements
.map(|element| { .iter()
let element_initializer = element .map(|element| {
.element_type let element_initializer = element
.cpp_type .element_type
.as_ref() .cpp_type
.map(|cpp_type| { .as_ref()
new_struct_with_bindings(&cpp_type, &element.bindings, component) .map(|cpp_type| {
}) new_struct_with_bindings(&cpp_type, &element.bindings, component)
.unwrap_or_default(); })
format!( .unwrap_or_default();
"sixtyfps::PathElement::{}({})", format!(
element.element_type.class_name, element_initializer "sixtyfps::PathElement::{}({})",
) element.element_type.class_name, element_initializer
}) )
.collect(); })
format!( .collect();
r#"[&](){{ format!(
r#"[&](){{
sixtyfps::PathElement elements[{}] = {{ sixtyfps::PathElement elements[{}] = {{
{} {}
}}; }};
return sixtyfps::PathElements(&elements[0], sizeof(elements) / sizeof(elements[0])); return sixtyfps::PathElements(&elements[0], sizeof(elements) / sizeof(elements[0]));
}}()"#, }}()"#,
converted_elements.len(), converted_elements.len(),
converted_elements.join(",") converted_elements.join(",")
) )
} }
crate::expression_tree::Path::Events(events) => {
let converted_elements: Vec<String> = compile_path_events(events);
format!(
r#"[&](){{
sixtyfps::PathEvent events[{}] = {{
{}
}};
return sixtyfps::PathElements(&events[0], sizeof(events) / sizeof(events[0]));
}}()"#,
converted_elements.len(),
converted_elements.join(",")
)
}
},
Uncompiled(_) => panic!(), Uncompiled(_) => panic!(),
Invalid => format!("\n#error invalid expression\n"), Invalid => format!("\n#error invalid expression\n"),
} }
@ -930,3 +945,92 @@ fn compute_layout(component: &Rc<Component>) -> Vec<String> {
} }
res res
} }
fn new_path_event(event_name: &str, fields: &[(&str, String)]) -> String {
let fields_initialization: Vec<String> = fields
.iter()
.map(|(prop, initializer)| format!("var.{} = {};", prop, initializer))
.collect();
format!(
r#"[&](){{
{} var{{}};
{}
return var;
}}()"#,
event_name,
fields_initialization.join("\n")
)
}
fn compile_path_events(events: &crate::expression_tree::PathEvents) -> Vec<String> {
use lyon::path::Event;
events
.iter()
.map(|event| match event {
Event::Begin { at } => format!(
"sixtyfps::PathEvent::Begin({})",
new_path_event(
"sixtyfps::PathEventBegin",
&[("x", at.x.to_string()), ("y", at.y.to_string())]
)
),
Event::Line { from, to } => format!(
"sixtyfps::PathEvent::Line({})",
new_path_event(
"sixtyfps::PathEventLine",
&[
("from_x", from.x.to_string()),
("from_y", from.y.to_string()),
("to_x", to.x.to_string()),
("to_y", to.y.to_string())
]
)
),
Event::Quadratic { from, ctrl, to } => format!(
"sixtyfps::PathEvent::Quadratic({})",
new_path_event(
"sixtyfps::PathEventQuadratic",
&[
("from_x", from.x.to_string()),
("from_y", from.y.to_string()),
("control_x", ctrl.x.to_string()),
("control_y", ctrl.y.to_string()),
("to_x", to.x.to_string()),
("to_y", to.y.to_string())
]
)
),
Event::Cubic { from, ctrl1, ctrl2, to } => format!(
"sixtyfps::PathEvent::Cubic({})",
new_path_event(
"sixtyfps::PathEventCubic",
&[
("from_x", from.x.to_string()),
("from_y", from.y.to_string()),
("control1_x", ctrl1.x.to_string()),
("control1_y", ctrl1.y.to_string()),
("control2_x", ctrl2.x.to_string()),
("control2_y", ctrl2.y.to_string()),
("to_x", to.x.to_string()),
("to_y", to.y.to_string())
]
)
),
Event::End { last, first, close } => format!(
"sixtyfps::PathEvent::End({})",
new_path_event(
"sixtyfps::PathEventEnd",
&[
("first_x", first.x.to_string()),
("first_y", first.y.to_string()),
("last_x", last.x.to_string()),
("last_y", last.y.to_string()),
("close", close.to_string())
]
)
),
})
.collect()
}

View file

@ -678,28 +678,29 @@ fn compile_expression(e: &Expression, component: &Rc<Component>) -> TokenStream
panic!("Expression::Object is not a Type::Object") panic!("Expression::Object is not a Type::Object")
} }
} }
Expression::PathElements { elements } => { Expression::PathElements { elements } => match elements {
let converted_elements: Vec<TokenStream> = elements crate::expression_tree::Path::Elements(elements) => {
.iter() let converted_elements: Vec<TokenStream> = elements
.map(|element| { .iter()
let mut bindings = element .map(|element| {
.bindings let mut bindings = element
.iter() .bindings
.map(|(property, expr)| { .iter()
let prop_ident = quote::format_ident!("{}", property); .map(|(property, expr)| {
let binding_expr = compile_expression(expr, component); let prop_ident = quote::format_ident!("{}", property);
let binding_expr = compile_expression(expr, component);
quote!(#prop_ident: #binding_expr as _).to_string() quote!(#prop_ident: #binding_expr as _).to_string()
}) })
.collect::<Vec<String>>(); .collect::<Vec<String>>();
if bindings.len() < element.element_type.properties.len() { if bindings.len() < element.element_type.properties.len() {
bindings.push("..Default::default()".into()) bindings.push("..Default::default()".into())
} }
let bindings = bindings.join(","); let bindings = bindings.join(",");
let ctor_format_string = element let ctor_format_string = element
.element_type .element_type
.rust_type_constructor .rust_type_constructor
.as_ref() .as_ref()
@ -707,17 +708,22 @@ fn compile_expression(e: &Expression, component: &Rc<Component>) -> TokenStream
"Unexpected error in type registry: path element is lacking rust type name", "Unexpected error in type registry: path element is lacking rust type name",
); );
ctor_format_string ctor_format_string
.replace("{}", &bindings) .replace("{}", &bindings)
.parse() .parse()
.expect("Error parsing rust path element constructor") .expect("Error parsing rust path element constructor")
}) })
.collect(); .collect();
quote!(sixtyfps::re_exports::PathElements::SharedElements( quote!(sixtyfps::re_exports::PathElements::SharedElements(
sixtyfps::re_exports::SharedArray::<sixtyfps::re_exports::PathElement>::from(&[#(#converted_elements),*]) sixtyfps::re_exports::SharedArray::<sixtyfps::re_exports::PathElement>::from(&[#(#converted_elements),*])
)) ))
} }
crate::expression_tree::Path::Events(events) => {
let events = compile_path_events(events);
quote!(sixtyfps::re_exports::PathElements::PathEvents(#events))
}
},
} }
} }
@ -791,3 +797,54 @@ fn compute_layout(component: &Component) -> TokenStream {
} }
} }
} }
fn compile_path_events(events: &crate::expression_tree::PathEvents) -> TokenStream {
use lyon::path::Event;
let converted_events: Vec<proc_macro2::TokenStream> = events
.iter()
.map(|event| match event {
Event::Begin { at } => {
let x = at.x;
let y = at.y;
quote!(sixtyfps::re_exports::PathEvent::Begin(sixtyfps::re_exports::PathEventBegin{x: #x, y: #y}))
}
Event::Line { from, to } => {
let from_x = from.x;
let from_y = from.y;
let to_x = to.x;
let to_y = to.y;
quote!(sixtyfps::re_exports::PathEvent::Line(sixtyfps::re_exports::PathEventLine{from_x: #from_x, from_y: #from_y, to_x: #to_x, to_y: #to_y}))
}
Event::Quadratic { from, ctrl, to } => {
let from_x = from.x;
let from_y = from.y;
let control_x = ctrl.x;
let control_y = ctrl.y;
let to_x = to.x;
let to_y = to.y;
quote!(sixtyfps::re_exports::PathEvent::Quadratic(sixtyfps::re_exports::PathEventQuadratic{from_x: #from_x, from_y: #from_y, control_x: #control_x, control_y: #control_y, to_x: #to_x, to_y: #to_y}))
}
Event::Cubic { from, ctrl1, ctrl2, to } => {
let from_x = from.x;
let from_y = from.y;
let control1_x = ctrl1.x;
let control1_y = ctrl1.y;
let control2_x = ctrl2.x;
let control2_y = ctrl2.y;
let to_x = to.x;
let to_y = to.y;
quote!(sixtyfps::re_exports::PathEvent::Cubic(sixtyfps::re_exports::PathEventCubic{from_x: #from_x, from_y: #from_y, control1_x: #control1_x, control1_y: #control1_y, control2_x: #control2_x, control2_y: #control2_y, to_x: #to_x, to_y: #to_y}))
}
Event::End { last, first, close } => {
let first_x = first.x;
let first_y = first.y;
let last_x = last.x;
let last_y = last.y;
quote!(sixtyfps::re_exports::PathEvent::End(sixtyfps::re_exports::PathEventEnd{first_x: #first_x, first_y: #first_y, last_x: #last_x, last_y: #last_y, close: #close}))
}
})
.collect();
quote!(sixtyfps::re_exports::SharedArray::<sixtyfps::re_exports::PathEvent>::from(&[#(#converted_events),*]))
}

View file

@ -46,7 +46,7 @@ pub fn run_passes(
passes::resolving::resolve_expressions(doc, diag, tr); passes::resolving::resolve_expressions(doc, diag, tr);
passes::inlining::inline(doc); passes::inlining::inline(doc);
passes::lower_layout::lower_layouts(&doc.root_component, diag); passes::lower_layout::lower_layouts(&doc.root_component, diag);
passes::compile_paths::compile_paths(&doc.root_component, tr); passes::compile_paths::compile_paths(&doc.root_component, tr, diag);
passes::unique_id::assign_unique_id(&doc.root_component); passes::unique_id::assign_unique_id(&doc.root_component);
if compiler_config.embed_resources { if compiler_config.embed_resources {
passes::collect_resources::collect_resources(&doc.root_component); passes::collect_resources::collect_resources(&doc.root_component);

View file

@ -1,3 +1,4 @@
use crate::diagnostics::Diagnostics;
/// This pass converts the verbose markup used for paths, such as /// This pass converts the verbose markup used for paths, such as
/// Path { /// Path {
/// LineTo { ... } ArcTo { ... } /// LineTo { ... } ArcTo { ... }
@ -10,7 +11,11 @@ use crate::object_tree::*;
use crate::typeregister::Type; use crate::typeregister::Type;
use std::rc::Rc; use std::rc::Rc;
pub fn compile_paths(component: &Rc<Component>, tr: &mut crate::typeregister::TypeRegister) { pub fn compile_paths(
component: &Rc<Component>,
tr: &mut crate::typeregister::TypeRegister,
diag: &mut Diagnostics,
) {
let path_type = tr.lookup("Path"); let path_type = tr.lookup("Path");
let path_type = path_type.as_builtin(); let path_type = path_type.as_builtin();
@ -25,31 +30,70 @@ pub fn compile_paths(component: &Rc<Component>, tr: &mut crate::typeregister::Ty
} }
let mut elem = elem_.borrow_mut(); let mut elem = elem_.borrow_mut();
let mut children = std::mem::take(&mut elem.children);
let path_data: Vec<_> = children let path_data = if let Some(commands_expr) = elem.bindings.remove("commands") {
.iter_mut() if let Some(path_child) = elem.children.iter().find(|child| {
.filter_map(|child| { path_type
let mut child = child.borrow_mut(); .additional_accepted_child_types
let element_name = &child.base_type.as_builtin().class_name; .contains_key(&child.borrow().base_type.as_builtin().class_name)
}) {
diag.push_error(
"Path elements cannot be mixed with the use of the SVG commands property."
.into(),
path_child.borrow().span(),
);
return;
}
if let Some(path_element) = let commands = match commands_expr {
path_type.additional_accepted_child_types.get(element_name) Expression::StringLiteral(commands) => commands,
{ _ => {
let element_type = match path_element { diag.push_error(
Type::Builtin(b) => b.clone(), "The commands property only accepts string literals.".into(),
_ => panic!( elem.span(),
);
return;
}
};
let path_builder = lyon::path::Path::builder().with_svg();
let path = lyon::svg::path_utils::build_path(path_builder, &commands);
match path {
Ok(path) => Path::Events(path.into_iter().collect()),
Err(err) => {
diag.push_error(format!("Error parsing SVG commands: {:?}", err), elem.span());
return;
}
}
} else {
let mut children = std::mem::take(&mut elem.children);
crate::expression_tree::Path::Elements(
children
.iter_mut()
.filter_map(|child| {
let mut child = child.borrow_mut();
let element_name = &child.base_type.as_builtin().class_name;
if let Some(path_element) =
path_type.additional_accepted_child_types.get(element_name)
{
let element_type = match path_element {
Type::Builtin(b) => b.clone(),
_ => panic!(
"Incorrect type registry -- expected built-in type for path element {}", "Incorrect type registry -- expected built-in type for path element {}",
element_name element_name
), ),
}; };
let bindings = std::mem::take(&mut child.bindings); let bindings = std::mem::take(&mut child.bindings);
Some(PathElement { element_type, bindings }) Some(PathElement { element_type, bindings })
} else { } else {
None None
} }
}) })
.collect(); .collect(),
)
};
elem.bindings.insert("elements".into(), Expression::PathElements { elements: path_data }); elem.bindings.insert("elements".into(), Expression::PathElements { elements: path_data });
}); });

View file

@ -0,0 +1,18 @@
TestCase := Rectangle {
Path {
commands: "M 350 300 L 550 300 ";
LineTo { x: 10; y: 100; }
// ^error{Path elements cannot be mixed with the use of the SVG commands property.}
}
property<string> cmds: "M 350 300 L 550 300 ";
Path {
// ^error{The commands property only accepts string literals.}
commands: cmds;
}
}

View file

@ -281,6 +281,7 @@ impl TypeRegister {
path.properties.insert("fill_color".to_owned(), Type::Color); path.properties.insert("fill_color".to_owned(), Type::Color);
path.properties.insert("stroke_color".to_owned(), Type::Color); path.properties.insert("stroke_color".to_owned(), Type::Color);
path.properties.insert("stroke_width".to_owned(), Type::Float32); path.properties.insert("stroke_width".to_owned(), Type::Float32);
path.properties.insert("commands".to_owned(), Type::String);
path.disallow_global_types_as_child_elements = true; path.disallow_global_types_as_child_elements = true;
let path_elements = { let path_elements = {

View file

@ -467,6 +467,46 @@ impl From<&PathEvent> for lyon::path::Event<lyon::math::Point, lyon::math::Point
} }
} }
impl From<&lyon::path::Event<lyon::math::Point, lyon::math::Point>> for PathEvent {
fn from(event: &lyon::path::Event<lyon::math::Point, lyon::math::Point>) -> Self {
use lyon::path::Event;
match event {
Event::Begin { at } => PathEvent::Begin(PathEventBegin { x: at.x, y: at.y }),
Event::Line { from, to } => PathEvent::Line(PathEventLine {
from_x: from.x,
from_y: from.y,
to_x: to.x,
to_y: to.y,
}),
Event::Quadratic { from, ctrl, to } => PathEvent::Quadratic(PathEventQuadratic {
from_x: from.x,
from_y: from.y,
control_x: ctrl.x,
control_y: ctrl.y,
to_x: to.x,
to_y: to.y,
}),
Event::Cubic { from, ctrl1, ctrl2, to } => PathEvent::Cubic(PathEventCubic {
from_x: from.x,
from_y: from.y,
control1_x: ctrl1.x,
control1_y: ctrl1.y,
control2_x: ctrl2.x,
control2_y: ctrl2.y,
to_x: to.x,
to_y: to.y,
}),
Event::End { last, first, close } => PathEvent::End(PathEventEnd {
first_x: first.x,
first_y: first.y,
last_x: last.x,
last_y: last.y,
close: *close,
}),
}
}
}
#[repr(C)] #[repr(C)]
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
/// PathElements holds the elements of a path. /// PathElements holds the elements of a path.
@ -572,6 +612,17 @@ pub unsafe extern "C" fn sixtyfps_new_path_elements(
core::ptr::write(out as *mut crate::SharedArray<PathElement>, arr.clone()); core::ptr::write(out as *mut crate::SharedArray<PathElement>, arr.clone());
} }
#[no_mangle]
/// This function is used for the low-level C++ interface to allocate the backing vector for a shared path event array.
pub unsafe extern "C" fn sixtyfps_new_path_events(
out: *mut c_void,
first_event: *const PathEvent,
count: usize,
) {
let arr = crate::SharedArray::from(std::slice::from_raw_parts(first_event, count));
core::ptr::write(out as *mut crate::SharedArray<PathEvent>, arr.clone());
}
/// Each item return a RenderingPrimitive to the backend with information about what to draw. /// Each item return a RenderingPrimitive to the backend with information about what to draw.
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
#[repr(C)] #[repr(C)]

View file

@ -27,6 +27,7 @@ fn main() {
"PathElements", "PathElements",
"PathElement", "PathElement",
"sixtyfps_new_path_elements", "sixtyfps_new_path_elements",
"sixtyfps_new_path_events",
] ]
.iter() .iter()
.map(|x| x.to_string()) .map(|x| x.to_string())
@ -85,7 +86,12 @@ fn main() {
(vec!["Resource"], "sixtyfps_resource_internal.h"), (vec!["Resource"], "sixtyfps_resource_internal.h"),
(vec!["Color"], "sixtyfps_color_internal.h"), (vec!["Color"], "sixtyfps_color_internal.h"),
( (
vec!["PathElements", "PathElement", "sixtyfps_new_path_elements"], vec![
"PathElements",
"PathElement",
"sixtyfps_new_path_elements",
"sixtyfps_new_path_events",
],
"sixtyfps_pathelements_internal.h", "sixtyfps_pathelements_internal.h",
), ),
] ]
@ -98,6 +104,7 @@ fn main() {
"sixtyfps_component_window_drop", "sixtyfps_component_window_drop",
"sixtyfps_component_window_run", "sixtyfps_component_window_run",
"sixtyfps_new_path_elements", "sixtyfps_new_path_elements",
"sixtyfps_new_path_events",
] ]
.iter() .iter()
.filter(|exclusion| rust_types.iter().find(|inclusion| inclusion == exclusion).is_none()) .filter(|exclusion| rust_types.iter().find(|inclusion| inclusion == exclusion).is_none())

View file

@ -1,7 +1,7 @@
use core::convert::{TryFrom, TryInto}; use core::convert::{TryFrom, TryInto};
use core::pin::Pin; use core::pin::Pin;
use sixtyfps_compilerlib::expression_tree::{ use sixtyfps_compilerlib::expression_tree::{
Expression, NamedReference, PathElement as ExprPathElement, Expression, NamedReference, Path as ExprPath, PathElement as ExprPathElement,
}; };
use sixtyfps_compilerlib::{object_tree::ElementRc, typeregister::Type}; use sixtyfps_compilerlib::{object_tree::ElementRc, typeregister::Type};
use sixtyfps_corelib as corelib; use sixtyfps_corelib as corelib;
@ -323,13 +323,22 @@ pub fn eval_expression(
.collect(), .collect(),
), ),
Expression::PathElements { elements } => { Expression::PathElements { elements } => {
Value::PathElements(PathElements::SharedElements(sixtyfps_corelib::SharedArray::< match elements {
sixtyfps_corelib::abi::datastructures::PathElement, ExprPath::Elements(elements) => Value::PathElements(PathElements::SharedElements(
>::from_iter( sixtyfps_corelib::SharedArray::<
elements sixtyfps_corelib::abi::datastructures::PathElement,
.iter() >::from_iter(elements.iter().map(|element| {
.map(|element| convert_path_element(element, component_type, eval_context)), convert_path_element(element, component_type, eval_context)
))) })),
)),
ExprPath::Events(events) => {
Value::PathElements(PathElements::PathEvents(sixtyfps_corelib::SharedArray::<
sixtyfps_corelib::abi::datastructures::PathEvent,
>::from_iter(
events.iter().map(|event| event.into()),
)))
}
}
} }
} }
} }

View file

@ -127,5 +127,13 @@ Hello := Rectangle {
Close {} Close {}
} }
Path {
commands: "M 100 300 Q 150 50 250 150 C 250 300 300 300 300 450 A 50 50 0 1 0 450 450 L 550 300";
x: 100;
y: 500;
stroke_color: black;
stroke_width: 2.0;
}
} }