Make it possible to use a string binding for Path commands

This was requested a few days ago in the chat and might also present an alternative for #754
This commit is contained in:
Simon Hausmann 2022-01-04 14:16:01 +01:00 committed by Simon Hausmann
parent ac031a45f8
commit 36e07b8801
11 changed files with 139 additions and 111 deletions

View file

@ -37,6 +37,11 @@ public:
{ {
} }
PathData(const SharedString &commands)
: data(cbindgen_private::types::PathData::Commands(commands))
{
}
friend bool operator==(const PathData &a, const PathData &b) friend bool operator==(const PathData &a, const PathData &b)
{ {
if (a.data.tag != b.data.tag) if (a.data.tag != b.data.tag)
@ -46,6 +51,8 @@ public:
return a.data.elements._0 == b.data.elements._0; return a.data.elements._0 == b.data.elements._0;
case cbindgen_private::types::PathData::Tag::Events: case cbindgen_private::types::PathData::Tag::Events:
return a.data.events._0 == b.data.events._0 && b.data.events._0 == b.data.events._0; return a.data.events._0 == b.data.events._0 && b.data.events._0 == b.data.events._0;
case cbindgen_private::types::PathData::Tag::Commands:
return a.data.commands._0 == b.data.commands._0;
case cbindgen_private::types::PathData::Tag::None: case cbindgen_private::types::PathData::Tag::None:
return true; return true;
} }

View file

@ -277,7 +277,7 @@ accordingly.
SVG is a popular file format for defining scalable graphics, which are often composed of paths. In SVG SVG is a popular file format for defining scalable graphics, which are often composed of paths. In SVG
paths are composed using [commands](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#path_commands), paths are composed using [commands](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#path_commands),
which in turn are written in a string literal. In `.60` the path commands are provided to the `commands` which in turn are written in a string. In `.60` the path commands are provided to the `commands`
property. The following example renders a shape consists of an arc and a rectangle, composed of `line-to`, property. The following example renders a shape consists of an arc and a rectangle, composed of `line-to`,
`move-to` and `arc` commands: `move-to` and `arc` commands:
@ -293,7 +293,7 @@ Example := Path {
The commands are provided in a property: The commands are provided in a property:
* **`commands`** (*string): A string literal providing the commands according to the SVG path specification. * **`commands`** (*string): A string providing the commands according to the SVG path specification.
#### Path Using SVG Path Elements #### Path Using SVG Path Elements

View file

@ -675,6 +675,7 @@ impl Expression {
Path::Events(events, coordinates) => { Path::Events(events, coordinates) => {
events.iter().chain(coordinates.iter()).for_each(visitor); events.iter().chain(coordinates.iter()).for_each(visitor);
} }
Path::Commands(commands) => visitor(commands),
}, },
Expression::StoreLocalVariable { value, .. } => visitor(&**value), Expression::StoreLocalVariable { value, .. } => visitor(&**value),
Expression::ReadLocalVariable { .. } => {} Expression::ReadLocalVariable { .. } => {}
@ -767,6 +768,7 @@ impl Expression {
Path::Events(events, coordinates) => { Path::Events(events, coordinates) => {
events.iter_mut().chain(coordinates.iter_mut()).for_each(visitor); events.iter_mut().chain(coordinates.iter_mut()).for_each(visitor);
} }
Path::Commands(commands) => visitor(commands),
}, },
Expression::StoreLocalVariable { value, .. } => visitor(&mut **value), Expression::StoreLocalVariable { value, .. } => visitor(&mut **value),
Expression::ReadLocalVariable { .. } => {} Expression::ReadLocalVariable { .. } => {}
@ -835,15 +837,13 @@ impl Expression {
Expression::UnaryOp { sub, .. } => sub.is_constant(), Expression::UnaryOp { sub, .. } => sub.is_constant(),
Expression::Array { values, .. } => values.iter().all(Expression::is_constant), Expression::Array { values, .. } => values.iter().all(Expression::is_constant),
Expression::Struct { values, .. } => values.iter().all(|(_, v)| v.is_constant()), Expression::Struct { values, .. } => values.iter().all(|(_, v)| v.is_constant()),
Expression::PathData(data) => { Expression::PathData(data) => match data {
if let Path::Elements(elements) = data { Path::Elements(elements) => elements
elements .iter()
.iter() .all(|element| element.bindings.values().all(|v| v.borrow().is_constant())),
.all(|element| element.bindings.values().all(|v| v.borrow().is_constant())) Path::Events(_, _) => true,
} else { Path::Commands(_) => false,
true },
}
}
Expression::StoreLocalVariable { .. } => false, Expression::StoreLocalVariable { .. } => false,
// we should somehow find out if this is constant or not // we should somehow find out if this is constant or not
Expression::ReadLocalVariable { .. } => false, Expression::ReadLocalVariable { .. } => false,
@ -1210,6 +1210,7 @@ pub struct BindingAnalysis {
pub enum Path { pub enum Path {
Elements(Vec<PathElement>), Elements(Vec<PathElement>),
Events(Vec<Expression>, Vec<Expression>), Events(Vec<Expression>, Vec<Expression>),
Commands(Box<Expression>), // expr must evaluate to string
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View file

@ -1340,6 +1340,9 @@ fn compile_expression(expr: &Expression, ctx: &EvaluationContext) -> TokenStream
}; };
quote!(sixtyfps::re_exports::PathData::Events(sixtyfps::re_exports::SharedVector::<_>::from_slice(&#events), sixtyfps::re_exports::SharedVector::<_>::from_slice(&#points))) quote!(sixtyfps::re_exports::PathData::Events(sixtyfps::re_exports::SharedVector::<_>::from_slice(&#events), sixtyfps::re_exports::SharedVector::<_>::from_slice(&#points)))
} }
(Type::String, Type::PathData) => {
quote!(sixtyfps::re_exports::PathData::Commands(#f))
}
_ => f, _ => f,
} }
} }

View file

@ -962,6 +962,10 @@ fn compile_path(path: &crate::expression_tree::Path, ctx: &ExpressionContext) ->
to: Type::PathData, to: Type::PathData,
} }
} }
crate::expression_tree::Path::Commands(commands) => llr_Expression::Cast {
from: lower_expression(&commands, &ctx).into(),
to: Type::PathData,
},
} }
} }

View file

@ -45,7 +45,7 @@ pub fn compile_paths(
let mut elem = elem_.borrow_mut(); let mut elem = elem_.borrow_mut();
let path_data = if let Some(commands_expr) = let path_data_binding = if let Some(commands_expr) =
elem.bindings.remove("commands").map(RefCell::into_inner) elem.bindings.remove("commands").map(RefCell::into_inner)
{ {
if let Some(path_child) = elem.children.iter().find(|child| { if let Some(path_child) = elem.children.iter().find(|child| {
@ -60,100 +60,21 @@ pub fn compile_paths(
return; return;
} }
let commands = match &commands_expr.expression { match &commands_expr.expression {
Expression::StringLiteral(commands) => commands, Expression::StringLiteral(commands) => {
if let Some(binding) = compile_path_from_string_literal(commands) {
binding
} else {
diag.push_error("Error parsing SVG commands".into(), &commands_expr);
return;
}
}
expr @ _ if expr.ty() == Type::String => Expression::PathData(
crate::expression_tree::Path::Commands(Box::new(commands_expr.expression)),
)
.into(),
_ => { _ => {
diag.push_error( diag.push_error("The commands property only accepts strings".into(), &*elem);
"The commands property only accepts string literals".into(),
&*elem,
);
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) => {
let event_enum = crate::typeregister::PATH_EVENT_ENUM.with(|e| e.clone());
let point_type = Type::Struct {
fields: IntoIterator::into_iter([
("x".to_owned(), Type::Float32),
("y".to_owned(), Type::Float32),
])
.collect(),
name: Some("Point".into()),
node: None,
};
let mut points = Vec::new();
let events = path
.into_iter()
.map(|event| {
Expression::EnumerationValue(match event {
lyon_path::Event::Begin { at } => {
points.push(at);
event_enum.clone().try_value_from_string("begin").unwrap()
}
lyon_path::Event::Line { from, to } => {
points.push(from);
points.push(to);
event_enum.clone().try_value_from_string("line").unwrap()
}
lyon_path::Event::Quadratic { from, ctrl, to } => {
points.push(from);
points.push(ctrl);
points.push(to);
event_enum.clone().try_value_from_string("quadratic").unwrap()
}
lyon_path::Event::Cubic { from, ctrl1, ctrl2, to } => {
points.push(from);
points.push(ctrl1);
points.push(ctrl2);
points.push(to);
event_enum.clone().try_value_from_string("cubic").unwrap()
}
lyon_path::Event::End { first: _, last: _, close } => {
if close {
event_enum
.clone()
.try_value_from_string("end_closed")
.unwrap()
} else {
event_enum
.clone()
.try_value_from_string("end_open")
.unwrap()
}
}
})
})
.collect();
let points = points
.into_iter()
.map(|point| Expression::Struct {
ty: point_type.clone(),
values: IntoIterator::into_iter([
(
"x".to_owned(),
Expression::NumberLiteral(point.x as _, Unit::None),
),
(
"y".to_owned(),
Expression::NumberLiteral(point.y as _, Unit::None),
),
])
.collect(),
})
.collect();
Path::Events(events, points)
}
Err(_) => {
diag.push_error("Error parsing SVG commands".into(), &commands_expr);
return; return;
} }
} }
@ -189,10 +110,79 @@ pub fn compile_paths(
elem.children.push(child); elem.children.push(child);
} }
} }
crate::expression_tree::Path::Elements(path_data) Expression::PathData(crate::expression_tree::Path::Elements(path_data)).into()
}; };
elem.bindings elem.bindings.insert("elements".into(), RefCell::new(path_data_binding));
.insert("elements".into(), RefCell::new(Expression::PathData(path_data).into()));
}); });
} }
fn compile_path_from_string_literal(commands: &str) -> Option<BindingExpression> {
let path_builder = lyon_path::Path::builder().with_svg();
lyon_svg::path_utils::build_path(path_builder, commands).ok().map(|path| {
let event_enum = crate::typeregister::PATH_EVENT_ENUM.with(|e| e.clone());
let point_type = Type::Struct {
fields: IntoIterator::into_iter([
("x".to_owned(), Type::Float32),
("y".to_owned(), Type::Float32),
])
.collect(),
name: Some("Point".into()),
node: None,
};
let mut points = Vec::new();
let events = path
.into_iter()
.map(|event| {
Expression::EnumerationValue(match event {
lyon_path::Event::Begin { at } => {
points.push(at);
event_enum.clone().try_value_from_string("begin").unwrap()
}
lyon_path::Event::Line { from, to } => {
points.push(from);
points.push(to);
event_enum.clone().try_value_from_string("line").unwrap()
}
lyon_path::Event::Quadratic { from, ctrl, to } => {
points.push(from);
points.push(ctrl);
points.push(to);
event_enum.clone().try_value_from_string("quadratic").unwrap()
}
lyon_path::Event::Cubic { from, ctrl1, ctrl2, to } => {
points.push(from);
points.push(ctrl1);
points.push(ctrl2);
points.push(to);
event_enum.clone().try_value_from_string("cubic").unwrap()
}
lyon_path::Event::End { first: _, last: _, close } => {
if close {
event_enum.clone().try_value_from_string("end_closed").unwrap()
} else {
event_enum.clone().try_value_from_string("end_open").unwrap()
}
}
})
})
.collect();
let points = points
.into_iter()
.map(|point| Expression::Struct {
ty: point_type.clone(),
values: IntoIterator::into_iter([
("x".to_owned(), Expression::NumberLiteral(point.x as _, Unit::None)),
("y".to_owned(), Expression::NumberLiteral(point.y as _, Unit::None)),
])
.collect(),
})
.collect();
Expression::PathData(Path::Events(events, points)).into()
})
}

View file

@ -12,7 +12,6 @@ TestCase := Rectangle {
property<string> cmds: "M 350 300 L 550 300 "; property<string> cmds: "M 350 300 L 550 300 ";
Path { Path {
// ^error{The commands property only accepts string literals}
commands: cmds; commands: cmds;
} }

View file

@ -22,7 +22,7 @@ rtti = []
# Expose C ABI # Expose C ABI
ffi = [] ffi = []
# Use the standard library # Use the standard library
std = ["euclid/std", "once_cell/std", "scoped-tls-hkt", "lyon_path", "lyon_algorithms", "lyon_geom", "instant"] std = ["euclid/std", "once_cell/std", "scoped-tls-hkt", "lyon_path", "lyon_algorithms", "lyon_geom", "lyon_svg", "instant"]
default = ["std"] default = ["std"]
@ -40,6 +40,7 @@ sixtyfps-corelib-macros = { version = "=0.2.0", path = "../corelib_macros" }
lyon_path = { version = "0.17.3" , optional = true } lyon_path = { version = "0.17.3" , optional = true }
lyon_algorithms = { version = "0.17.1" , optional = true } lyon_algorithms = { version = "0.17.1" , optional = true }
lyon_geom = { version = "0.17.0", optional = true } lyon_geom = { version = "0.17.0", optional = true }
lyon_svg = { version = "0.17.2", optional = true }
euclid = { version = "0.22.1", default-features = false } euclid = { version = "0.22.1", default-features = false }
once_cell = { version = "1.5", default-features = false } once_cell = { version = "1.5", default-features = false }
instant = { version = "0.1", features = [ "now" ], optional = true } instant = { version = "0.1", features = [ "now" ], optional = true }

View file

@ -292,6 +292,8 @@ pub enum PathData {
/// The Events variant describes the path as a series of low-level events and /// The Events variant describes the path as a series of low-level events and
/// associated coordinates. /// associated coordinates.
Events(crate::SharedVector<PathEvent>, crate::SharedVector<Point>), Events(crate::SharedVector<PathEvent>, crate::SharedVector<Point>),
/// The Commands variant describes the path as a series of SVG encoded path commands.
Commands(crate::SharedString),
} }
impl Default for PathData { impl Default for PathData {
@ -312,6 +314,13 @@ impl PathData {
PathData::Events(events, coordinates) => { PathData::Events(events, coordinates) => {
LyonPathIteratorVariant::FromEvents(events, coordinates) LyonPathIteratorVariant::FromEvents(events, coordinates)
} }
PathData::Commands(commands) => {
let path_builder = lyon_path::Path::builder().with_svg();
LyonPathIteratorVariant::FromPath(
lyon_svg::path_utils::build_path(path_builder, &commands)
.unwrap_or_default(),
)
}
}, },
transform: Default::default(), transform: Default::default(),
} }

View file

@ -1101,6 +1101,13 @@ pub fn convert_path(path: &ExprPath, local_context: &mut EvalLocalContext) -> Pa
ExprPath::Events(events, points) => { ExprPath::Events(events, points) => {
convert_from_lyon_path(events.iter(), points.iter(), local_context) convert_from_lyon_path(events.iter(), points.iter(), local_context)
} }
ExprPath::Commands(commands) => {
if let Value::String(commands) = eval_expression(commands, local_context) {
PathData::Commands(commands)
} else {
panic!("binding to path commands does not evalutate to string");
}
}
} }
} }

View file

@ -156,10 +156,17 @@ Hello := Rectangle {
} }
Path { 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"; commands: ta.pressed ? root.arrow_down_commands : root.funky-shape-commands;
x: 100phx; x: 100phx;
y: 500phx; y: 500phx;
stroke: black; stroke: black;
stroke_width: 2px; stroke_width: 2px;
} }
ta := TouchArea {
}
property <string> arrow_down_commands: "M21.8,311.1l84.2-82.1c15.7-15.2,41-15.2,56.7,0l341.1,304.1l333.7-297.5c15.5-15.2,41-15.2,56.6,0l84.3,82.1c15.6,15.2,15.6,40,0,55.2L531.7,771c-15.7,15.3-41,15.3-56.7,0l-6.9-6.7L21.8,366.3C6.1,351,6.1,326.3,21.8,311.1z";
property <string> funky_shape_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";
} }