slint/sixtyfps_runtime/interpreter/eval.rs

597 lines
24 KiB
Rust

/* LICENSE BEGIN
This file is part of the SixtyFPS Project -- https://sixtyfps.io
Copyright (c) 2020 Olivier Goffart <olivier.goffart@sixtyfps.io>
Copyright (c) 2020 Simon Hausmann <simon.hausmann@sixtyfps.io>
SPDX-License-Identifier: GPL-3.0-only
This file is also available under commercial licensing terms.
Please contact info@sixtyfps.io for more information.
LICENSE END */
use crate::dynamic_component::InstanceRef;
use core::convert::{TryFrom, TryInto};
use core::iter::FromIterator;
use core::pin::Pin;
use sixtyfps_compilerlib::expression_tree::{
BuiltinFunction, EasingCurve, Expression, ExpressionSpanned, NamedReference, Path as ExprPath,
PathElement as ExprPathElement,
};
use sixtyfps_compilerlib::{object_tree::ElementRc, typeregister::Type};
use sixtyfps_corelib as corelib;
use sixtyfps_corelib::{
graphics::PathElement, items::ItemRef, items::PropertyAnimation, Color, PathData, Resource,
SharedArray, SharedString, Signal,
};
use std::collections::HashMap;
use std::rc::Rc;
pub trait ErasedPropertyInfo {
fn get(&self, item: Pin<ItemRef>) -> Value;
fn set(&self, item: Pin<ItemRef>, value: Value, animation: Option<PropertyAnimation>);
fn set_binding(
&self,
item: Pin<ItemRef>,
binding: Box<dyn Fn() -> Value>,
animation: Option<PropertyAnimation>,
);
fn offset(&self) -> usize;
}
impl<Item: vtable::HasStaticVTable<corelib::items::ItemVTable>> ErasedPropertyInfo
for &'static dyn corelib::rtti::PropertyInfo<Item, Value>
{
fn get(&self, item: Pin<ItemRef>) -> Value {
(*self).get(ItemRef::downcast_pin(item).unwrap()).unwrap()
}
fn set(&self, item: Pin<ItemRef>, value: Value, animation: Option<PropertyAnimation>) {
(*self).set(ItemRef::downcast_pin(item).unwrap(), value, animation).unwrap()
}
fn set_binding(
&self,
item: Pin<ItemRef>,
binding: Box<dyn Fn() -> Value>,
animation: Option<PropertyAnimation>,
) {
(*self).set_binding(ItemRef::downcast_pin(item).unwrap(), binding, animation).unwrap();
}
fn offset(&self) -> usize {
(*self).offset()
}
}
#[derive(Debug, Clone, PartialEq)]
/// This is a dynamically typed Value used in the interpreter, it need to be able
/// to be converted from and to anything that can be stored in a Property
pub enum Value {
/// There is nothing in this value. That's the default.
/// For example, a function that do not return a result would return a Value::Void
Void,
/// An i32 or a float
Number(f64),
/// String
String(SharedString),
/// Bool
Bool(bool),
/// A resource (typically an image)
Resource(Resource),
/// An Array
Array(Vec<Value>),
/// An object
Object(HashMap<String, Value>),
/// A color
Color(Color),
/// The elements of a path
PathElements(PathData),
/// An easing curve
EasingCurve(corelib::animations::EasingCurve),
/// An enumation, like TextHorizontalAlignment::align_center
EnumerationValue(String, String),
}
impl Default for Value {
fn default() -> Self {
Value::Void
}
}
impl corelib::rtti::ValueType for Value {}
/// Helper macro to implement the TryFrom / TryInto for Value
///
/// For example
/// `declare_value_conversion!(Number => [u32, u64, i32, i64, f32, f64] );`
/// means that Value::Number can be converted to / from each of the said rust types
macro_rules! declare_value_conversion {
( $value:ident => [$($ty:ty),*] ) => {
$(
impl TryFrom<$ty> for Value {
type Error = ();
fn try_from(v: $ty) -> Result<Self, ()> {
//Ok(Value::$value(v.try_into().map_err(|_|())?))
Ok(Value::$value(v as _))
}
}
impl TryInto<$ty> for Value {
type Error = ();
fn try_into(self) -> Result<$ty, ()> {
match self {
//Self::$value(x) => x.try_into().map_err(|_|()),
Self::$value(x) => Ok(x as _),
_ => Err(())
}
}
}
)*
};
}
declare_value_conversion!(Number => [u32, u64, i32, i64, f32, f64, usize, isize] );
declare_value_conversion!(String => [SharedString] );
declare_value_conversion!(Bool => [bool] );
declare_value_conversion!(Resource => [Resource] );
declare_value_conversion!(Object => [HashMap<String, Value>] );
declare_value_conversion!(Color => [Color] );
declare_value_conversion!(PathElements => [PathData]);
declare_value_conversion!(EasingCurve => [corelib::animations::EasingCurve]);
macro_rules! declare_value_enum_conversion {
($ty:ty, $n:ident) => {
impl TryFrom<$ty> for Value {
type Error = ();
fn try_from(v: $ty) -> Result<Self, ()> {
Ok(Value::EnumerationValue(stringify!($n).to_owned(), v.to_string()))
}
}
impl TryInto<$ty> for Value {
type Error = ();
fn try_into(self) -> Result<$ty, ()> {
use std::str::FromStr;
match self {
Self::EnumerationValue(enumeration, value) => {
if enumeration != stringify!($n) {
return Err(());
}
<$ty>::from_str(value.as_str()).map_err(|_| ())
}
_ => Err(()),
}
}
}
};
}
declare_value_enum_conversion!(corelib::items::TextHorizontalAlignment, TextHorizontalAlignment);
declare_value_enum_conversion!(corelib::items::TextVerticalAlignment, TextVerticalAlignment);
/// The local variable needed for binding evaluation
#[derive(Default)]
pub struct EvalLocalContext {
local_variables: HashMap<String, Value>,
function_arguments: Vec<Value>,
}
impl EvalLocalContext {
/// Create a context for a function and passing the arguments
pub fn from_function_arguments(function_arguments: Vec<Value>) -> Self {
Self { function_arguments, ..Default::default() }
}
}
/// Evaluate an expression and return a Value as the result of this expression
pub fn eval_expression(
e: &Expression,
component: InstanceRef,
local_context: &mut EvalLocalContext,
) -> Value {
match e {
Expression::Invalid => panic!("invalid expression while evaluating"),
Expression::Uncompiled(_) => panic!("uncompiled expression while evaluating"),
Expression::TwoWayBinding(_) => panic!("invalid expression while evaluating"),
Expression::StringLiteral(s) => Value::String(s.into()),
Expression::NumberLiteral(n, unit) => Value::Number(unit.normalize(*n)),
Expression::BoolLiteral(b) => Value::Bool(*b),
Expression::SignalReference { .. } => panic!("signal in expression"),
Expression::BuiltinFunctionReference(_) => panic!(
"naked builtin function reference not allowed, should be handled by function call"
),
Expression::PropertyReference(NamedReference { element, name }) => {
load_property(component, &element.upgrade().unwrap(), name.as_ref()).unwrap()
}
Expression::RepeaterIndexReference { element } => load_property(
component,
&element.upgrade().unwrap().borrow().base_type.as_component().root_element,
"index",
)
.unwrap(),
Expression::RepeaterModelReference { element } => load_property(
component,
&element.upgrade().unwrap().borrow().base_type.as_component().root_element,
"model_data",
)
.unwrap(),
Expression::FunctionParameterReference { index, .. } => {
local_context.function_arguments[*index].clone()
}
Expression::ObjectAccess { base, name } => {
if let Value::Object(mut o) = eval_expression(base, component, local_context) {
o.remove(name).unwrap_or(Value::Void)
} else {
Value::Void
}
}
Expression::Cast { from, to } => {
let v = eval_expression(&*from, component, local_context);
match (v, to) {
(Value::Number(n), Type::Int32) => Value::Number(n.round()),
(Value::Number(n), Type::String) => {
Value::String(SharedString::from(format!("{}", n).as_str()))
}
(Value::Number(n), Type::Color) => Value::Color(Color::from_argb_encoded(n as u32)),
(v, _) => v,
}
}
Expression::CodeBlock(sub) => {
let mut v = Value::Void;
for e in sub {
v = eval_expression(e, component, local_context);
}
v
}
Expression::FunctionCall { function, arguments } => {
let a = arguments.iter().map(|e| eval_expression(e, component, local_context));
if let Expression::SignalReference(NamedReference { element, name }) = &**function {
let element = element.upgrade().unwrap();
generativity::make_guard!(guard);
let enclosing_component =
enclosing_component_for_element(&element, component, guard);
let component_type = enclosing_component.component_type;
let item_info = &component_type.items[element.borrow().id.as_str()];
let item = unsafe { item_info.item_from_component(enclosing_component.as_ptr()) };
if let Some(signal_offset) = item_info.rtti.signals.get(name.as_str()) {
let signal =
unsafe { &*(item.as_ptr().add(*signal_offset) as *const Signal<()>) };
signal.emit(&());
} else if let Some(signal_offset) = component_type.custom_signals.get(name.as_str())
{
let signal = signal_offset.apply(&*enclosing_component.instance);
signal.emit(a.collect::<Vec<_>>().as_slice())
} else {
panic!("unkown signal {}", name)
}
Value::Void
} else if let Expression::BuiltinFunctionReference(funcref) = &**function {
match funcref {
BuiltinFunction::GetWindowScaleFactor => {
Value::Number(window_ref(component).unwrap().scale_factor() as _)
}
BuiltinFunction::Debug => {
println!("{:?}", a);
Value::Void
}
}
} else {
panic!("call of something not a signal")
}
}
Expression::SelfAssignment { lhs, rhs, op } => match &**lhs {
Expression::PropertyReference(NamedReference { element, name }) => {
let rhs = eval_expression(&**rhs, component, local_context);
if *op == '=' {
store_property(component, &element.upgrade().unwrap(), name.as_ref(), rhs)
.unwrap();
return Value::Void;
}
let eval = |lhs| match (lhs, rhs, op) {
(Value::Number(a), Value::Number(b), '+') => Value::Number(a + b),
(Value::Number(a), Value::Number(b), '-') => Value::Number(a - b),
(Value::Number(a), Value::Number(b), '/') => Value::Number(a / b),
(Value::Number(a), Value::Number(b), '*') => Value::Number(a * b),
(lhs, rhs, op) => panic!("unsupported {:?} {} {:?}", lhs, op, rhs),
};
let element = element.upgrade().unwrap();
generativity::make_guard!(guard);
let enclosing_component =
enclosing_component_for_element(&element, component, guard);
let component = element.borrow().enclosing_component.upgrade().unwrap();
if element.borrow().id == component.root_element.borrow().id {
if let Some(x) = enclosing_component.component_type.custom_properties.get(name)
{
unsafe {
let p =
Pin::new_unchecked(&*enclosing_component.as_ptr().add(x.offset));
x.prop.set(p, eval(x.prop.get(p).unwrap()), None).unwrap();
}
return Value::Void;
}
};
let item_info =
&enclosing_component.component_type.items[element.borrow().id.as_str()];
let item = unsafe { item_info.item_from_component(enclosing_component.as_ptr()) };
let p = &item_info.rtti.properties[name.as_str()];
p.set(item, eval(p.get(item)), None);
Value::Void
}
_ => panic!("typechecking should make sure this was a PropertyReference"),
},
Expression::BinaryExpression { lhs, rhs, op } => {
let lhs = eval_expression(&**lhs, component, local_context);
let rhs = eval_expression(&**rhs, component, local_context);
match (op, lhs, rhs) {
('+', Value::Number(a), Value::Number(b)) => Value::Number(a + b),
('-', Value::Number(a), Value::Number(b)) => Value::Number(a - b),
('/', Value::Number(a), Value::Number(b)) => Value::Number(a / b),
('*', Value::Number(a), Value::Number(b)) => Value::Number(a * b),
('<', Value::Number(a), Value::Number(b)) => Value::Bool(a < b),
('>', Value::Number(a), Value::Number(b)) => Value::Bool(a > b),
('≤', Value::Number(a), Value::Number(b)) => Value::Bool(a <= b),
('≥', Value::Number(a), Value::Number(b)) => Value::Bool(a >= b),
('=', a, b) => Value::Bool(a == b),
('!', a, b) => Value::Bool(a != b),
('&', Value::Bool(a), Value::Bool(b)) => Value::Bool(a && b),
('|', Value::Bool(a), Value::Bool(b)) => Value::Bool(a || b),
(op, lhs, rhs) => panic!("unsupported {:?} {} {:?}", lhs, op, rhs),
}
}
Expression::UnaryOp { sub, op } => {
let sub = eval_expression(&**sub, component, local_context);
match (sub, op) {
(Value::Number(a), '+') => Value::Number(a),
(Value::Number(a), '-') => Value::Number(-a),
(Value::Bool(a), '!') => Value::Bool(!a),
(sub, op) => panic!("unsupported {} {:?}", op, sub),
}
}
Expression::ResourceReference { absolute_source_path } => {
Value::Resource(Resource::AbsoluteFilePath(absolute_source_path.into()))
}
Expression::Condition { condition, true_expr, false_expr } => {
match eval_expression(&**condition, component, local_context).try_into()
as Result<bool, _>
{
Ok(true) => eval_expression(&**true_expr, component, local_context),
Ok(false) => eval_expression(&**false_expr, component, local_context),
_ => panic!("conditional expression did not evaluate to boolean"),
}
}
Expression::Array { values, .. } => Value::Array(
values.iter().map(|e| eval_expression(e, component, local_context)).collect(),
),
Expression::Object { values, .. } => Value::Object(
values
.iter()
.map(|(k, v)| (k.clone(), eval_expression(v, component, local_context)))
.collect(),
),
Expression::PathElements { elements } => {
Value::PathElements(convert_path(elements, component, local_context))
}
Expression::StoreLocalVariable { name, value } => {
let value = eval_expression(value, component, local_context);
local_context.local_variables.insert(name.clone(), value);
Value::Void
}
Expression::ReadLocalVariable { name, .. } => {
local_context.local_variables.get(name).unwrap().clone()
}
Expression::EasingCurve(curve) => Value::EasingCurve(match curve {
EasingCurve::Linear => corelib::animations::EasingCurve::Linear,
EasingCurve::CubicBezier(a, b, c, d) => {
corelib::animations::EasingCurve::CubicBezier([*a, *b, *c, *d])
}
}),
Expression::EnumerationValue(value) => {
Value::EnumerationValue(value.enumeration.name.clone(), value.to_string())
}
}
}
pub fn load_property(component: InstanceRef, element: &ElementRc, name: &str) -> Result<Value, ()> {
generativity::make_guard!(guard);
let enclosing_component = enclosing_component_for_element(&element, component, guard);
let element = element.borrow();
if element.id == element.enclosing_component.upgrade().unwrap().root_element.borrow().id {
if let Some(x) = enclosing_component.component_type.custom_properties.get(name) {
return unsafe {
x.prop.get(Pin::new_unchecked(&*enclosing_component.as_ptr().add(x.offset)))
};
}
};
let item_info = enclosing_component
.component_type
.items
.get(element.id.as_str())
.unwrap_or_else(|| panic!("Unkown element for {}.{}", element.id, name));
core::mem::drop(element);
let item = unsafe { item_info.item_from_component(enclosing_component.as_ptr()) };
Ok(item_info.rtti.properties.get(name).ok_or(())?.get(item))
}
pub fn store_property(
component_instance: InstanceRef,
element: &ElementRc,
name: &str,
value: Value,
) -> Result<(), ()> {
generativity::make_guard!(guard);
let enclosing_component = enclosing_component_for_element(&element, component_instance, guard);
let maybe_animation = crate::dynamic_component::animation_for_property(
enclosing_component,
&element.borrow().property_animations,
name,
);
let component = element.borrow().enclosing_component.upgrade().unwrap();
if element.borrow().id == component.root_element.borrow().id {
if let Some(x) = enclosing_component.component_type.custom_properties.get(name) {
unsafe {
let p = Pin::new_unchecked(&*enclosing_component.as_ptr().add(x.offset));
return x.prop.set(p, value, maybe_animation);
}
}
};
let item_info = &enclosing_component.component_type.items[element.borrow().id.as_str()];
let item = unsafe { item_info.item_from_component(enclosing_component.as_ptr()) };
let p = &item_info.rtti.properties.get(name).ok_or(())?;
p.set(item, value, maybe_animation);
Ok(())
}
pub fn window_ref(component: InstanceRef) -> Option<sixtyfps_corelib::eventloop::ComponentWindow> {
if let Some(parent_offset) = component.component_type.parent_component_offset {
let parent_component =
if let Some(parent) = parent_offset.apply(&*component.instance.as_ref()) {
*parent
} else {
return None;
};
generativity::make_guard!(guard);
window_ref(unsafe { InstanceRef::from_pin_ref(parent_component, guard) })
} else {
component
.component_type
.extra_data_offset
.apply(&*component.instance.as_ref())
.window
.borrow()
.as_ref()
.map(|w| w.clone())
}
}
fn enclosing_component_for_element<'a, 'old_id, 'new_id>(
element: &'a ElementRc,
component: InstanceRef<'a, 'old_id>,
guard: generativity::Guard<'new_id>,
) -> InstanceRef<'a, 'new_id> {
if Rc::ptr_eq(
&element.borrow().enclosing_component.upgrade().unwrap(),
&component.component_type.original,
) {
// Safety: new_id is an unique id
unsafe {
std::mem::transmute::<InstanceRef<'a, 'old_id>, InstanceRef<'a, 'new_id>>(component)
}
} else {
let parent_component = component
.component_type
.parent_component_offset
.unwrap()
.apply(component.as_ref())
.unwrap();
generativity::make_guard!(new_guard);
let parent_instance = unsafe { InstanceRef::from_pin_ref(parent_component, new_guard) };
let parent_instance = unsafe {
core::mem::transmute::<InstanceRef, InstanceRef<'a, 'static>>(parent_instance)
};
enclosing_component_for_element(element, parent_instance, guard)
}
}
pub fn new_struct_with_bindings<
ElementType: 'static + Default + sixtyfps_corelib::rtti::BuiltinItem,
>(
bindings: &HashMap<String, ExpressionSpanned>,
component: InstanceRef,
local_context: &mut EvalLocalContext,
) -> ElementType {
let mut element = ElementType::default();
for (prop, info) in ElementType::fields::<Value>().into_iter() {
if let Some(binding) = &bindings.get(prop) {
let value = eval_expression(&binding, component, local_context);
info.set_field(&mut element, value).unwrap();
}
}
element
}
fn convert_from_lyon_path<'a>(
it: impl IntoIterator<Item = &'a lyon::path::Event<lyon::math::Point, lyon::math::Point>>,
) -> PathData {
use lyon::path::Event;
use sixtyfps_corelib::graphics::PathEvent;
let mut coordinates = Vec::new();
let events = it
.into_iter()
.map(|event| match event {
Event::Begin { at } => {
coordinates.push(at);
PathEvent::Begin
}
Event::Line { from, to } => {
coordinates.push(from);
coordinates.push(to);
PathEvent::Line
}
Event::Quadratic { from, ctrl, to } => {
coordinates.push(from);
coordinates.push(ctrl);
coordinates.push(to);
PathEvent::Quadratic
}
Event::Cubic { from, ctrl1, ctrl2, to } => {
coordinates.push(from);
coordinates.push(ctrl1);
coordinates.push(ctrl2);
coordinates.push(to);
PathEvent::Cubic
}
Event::End { last, first, close } => {
debug_assert_eq!(coordinates.first(), Some(&first));
debug_assert_eq!(coordinates.last(), Some(&last));
if *close {
PathEvent::EndClosed
} else {
PathEvent::EndOpen
}
}
})
.collect::<Vec<_>>();
PathData::Events(
SharedArray::from(events.as_slice()),
SharedArray::from_iter(coordinates.into_iter().cloned()),
)
}
pub fn convert_path(
path: &ExprPath,
component: InstanceRef,
local_context: &mut EvalLocalContext,
) -> PathData {
match path {
ExprPath::Elements(elements) => PathData::Elements(SharedArray::<PathElement>::from_iter(
elements.iter().map(|element| convert_path_element(element, component, local_context)),
)),
ExprPath::Events(events) => convert_from_lyon_path(events.iter()),
}
}
fn convert_path_element(
expr_element: &ExprPathElement,
component: InstanceRef,
local_context: &mut EvalLocalContext,
) -> PathElement {
match expr_element.element_type.native_class.class_name.as_str() {
"LineTo" => PathElement::LineTo(new_struct_with_bindings(
&expr_element.bindings,
component,
local_context,
)),
"ArcTo" => PathElement::ArcTo(new_struct_with_bindings(
&expr_element.bindings,
component,
local_context,
)),
"Close" => PathElement::Close,
_ => panic!(
"Cannot create unsupported path element {}",
expr_element.element_type.native_class.class_name
),
}
}