| 1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
| 2 | // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 |
| 3 | |
| 4 | //! Pass that create a state property, and change all the binding to depend on that property |
| 5 | |
| 6 | use crate::diagnostics::BuildDiagnostics; |
| 7 | use crate::diagnostics::SourceLocation; |
| 8 | use crate::diagnostics::Spanned; |
| 9 | use crate::expression_tree::*; |
| 10 | use crate::langtype::ElementType; |
| 11 | use crate::langtype::Type; |
| 12 | use crate::object_tree::*; |
| 13 | use smol_str::SmolStr; |
| 14 | use std::cell::RefCell; |
| 15 | use std::collections::{HashMap, HashSet}; |
| 16 | use std::rc::{Rc, Weak}; |
| 17 | |
| 18 | pub fn lower_states( |
| 19 | component: &Rc<Component>, |
| 20 | tr: &crate::typeregister::TypeRegister, |
| 21 | diag: &mut BuildDiagnostics, |
| 22 | ) { |
| 23 | let state_info_type: Type = tr.lookup(name:"StateInfo" ); |
| 24 | assert!(matches!(state_info_type, Type::Struct(ref s) if s.name.is_some())); |
| 25 | recurse_elem(&component.root_element, &(), &mut |elem: &Rc>, _| { |
| 26 | lower_state_in_element(root_element:elem, &state_info_type, diag) |
| 27 | }); |
| 28 | } |
| 29 | |
| 30 | fn lower_state_in_element( |
| 31 | root_element: &ElementRc, |
| 32 | state_info_type: &Type, |
| 33 | diag: &mut BuildDiagnostics, |
| 34 | ) { |
| 35 | if root_element.borrow().states.is_empty() { |
| 36 | return; |
| 37 | } |
| 38 | let has_transitions = !root_element.borrow().transitions.is_empty(); |
| 39 | let state_property_name = compute_state_property_name(root_element); |
| 40 | let state_property = Expression::PropertyReference(NamedReference::new( |
| 41 | root_element, |
| 42 | state_property_name.clone(), |
| 43 | )); |
| 44 | let state_property_ref = if has_transitions { |
| 45 | Expression::StructFieldAccess { |
| 46 | base: Box::new(state_property.clone()), |
| 47 | name: "current-state" .into(), |
| 48 | } |
| 49 | } else { |
| 50 | state_property.clone() |
| 51 | }; |
| 52 | let mut affected_properties = HashSet::new(); |
| 53 | // Maps State name string -> integer id |
| 54 | let mut states_id = HashMap::new(); |
| 55 | let mut state_value = Expression::NumberLiteral(0., Unit::None); |
| 56 | let states = std::mem::take(&mut root_element.borrow_mut().states); |
| 57 | for (idx, state) in states.into_iter().enumerate().rev() { |
| 58 | if let Some(condition) = &state.condition { |
| 59 | state_value = Expression::Condition { |
| 60 | condition: Box::new(condition.clone()), |
| 61 | true_expr: Box::new(Expression::NumberLiteral((idx + 1) as _, Unit::None)), |
| 62 | false_expr: Box::new(std::mem::take(&mut state_value)), |
| 63 | }; |
| 64 | } |
| 65 | for (ne, expr, node) in state.property_changes { |
| 66 | affected_properties.insert(ne.clone()); |
| 67 | let e = ne.element(); |
| 68 | let property_expr = match expression_for_property(&e, ne.name()) { |
| 69 | ExpressionForProperty::TwoWayBinding => { |
| 70 | diag.push_error( |
| 71 | format!("Cannot change the property ' {}' in a state because it is initialized with a two-way binding" , ne.name()), |
| 72 | &node |
| 73 | ); |
| 74 | continue; |
| 75 | } |
| 76 | ExpressionForProperty::Expression(e) => e, |
| 77 | ExpressionForProperty::InvalidBecauseOfIssue1461 => { |
| 78 | diag.push_error( |
| 79 | format!("Internal error: The expression for the default state currently cannot be represented: https://github.com/slint-ui/slint/issues/1461 \nAs a workaround, add a binding for property {}" , ne.name()), |
| 80 | &node |
| 81 | ); |
| 82 | continue; |
| 83 | } |
| 84 | }; |
| 85 | let new_expr = Expression::Condition { |
| 86 | condition: Box::new(Expression::BinaryExpression { |
| 87 | lhs: Box::new(state_property_ref.clone()), |
| 88 | rhs: Box::new(Expression::NumberLiteral((idx + 1) as _, Unit::None)), |
| 89 | op: '=' , |
| 90 | }), |
| 91 | true_expr: Box::new(expr), |
| 92 | false_expr: Box::new(property_expr), |
| 93 | }; |
| 94 | match e.borrow_mut().bindings.entry(ne.name().clone()) { |
| 95 | std::collections::btree_map::Entry::Occupied(mut e) => { |
| 96 | e.get_mut().get_mut().expression = new_expr |
| 97 | } |
| 98 | std::collections::btree_map::Entry::Vacant(e) => { |
| 99 | let mut r = BindingExpression::from(new_expr); |
| 100 | r.priority = 1; |
| 101 | e.insert(r.into()); |
| 102 | } |
| 103 | }; |
| 104 | } |
| 105 | states_id.insert(state.id, idx as i32 + 1); |
| 106 | } |
| 107 | |
| 108 | root_element.borrow_mut().property_declarations.insert( |
| 109 | state_property_name.clone(), |
| 110 | PropertyDeclaration { |
| 111 | property_type: if has_transitions { state_info_type.clone() } else { Type::Int32 }, |
| 112 | ..PropertyDeclaration::default() |
| 113 | }, |
| 114 | ); |
| 115 | root_element |
| 116 | .borrow_mut() |
| 117 | .bindings |
| 118 | .insert(state_property_name, RefCell::new(state_value.into())); |
| 119 | |
| 120 | lower_transitions_in_element( |
| 121 | root_element, |
| 122 | state_property, |
| 123 | states_id, |
| 124 | affected_properties, |
| 125 | diag, |
| 126 | ); |
| 127 | } |
| 128 | |
| 129 | fn lower_transitions_in_element( |
| 130 | elem: &ElementRc, |
| 131 | state_property: Expression, |
| 132 | states_id: HashMap<SmolStr, i32>, |
| 133 | affected_properties: HashSet<NamedReference>, |
| 134 | diag: &mut BuildDiagnostics, |
| 135 | ) { |
| 136 | let transitions = std::mem::take(&mut elem.borrow_mut().transitions); |
| 137 | let mut props = |
| 138 | HashMap::<NamedReference, (SourceLocation, Vec<TransitionPropertyAnimation>)>::new(); |
| 139 | for transition in transitions { |
| 140 | let state = states_id.get(&transition.state_id).unwrap_or_else(|| { |
| 141 | diag.push_error( |
| 142 | format!("State ' {}' does not exist" , transition.state_id), |
| 143 | transition |
| 144 | .node |
| 145 | .DeclaredIdentifier() |
| 146 | .as_ref() |
| 147 | .map(|x| x as &dyn Spanned) |
| 148 | .unwrap_or(&transition.node as &dyn Spanned), |
| 149 | ); |
| 150 | &0 |
| 151 | }); |
| 152 | |
| 153 | for (p, span, animation) in transition.property_animations { |
| 154 | if !affected_properties.contains(&p) { |
| 155 | diag.push_error( |
| 156 | "The property is not changed as part of this transition" .into(), |
| 157 | &span, |
| 158 | ); |
| 159 | continue; |
| 160 | } |
| 161 | |
| 162 | let t = TransitionPropertyAnimation { |
| 163 | state_id: *state, |
| 164 | is_out: transition.is_out, |
| 165 | animation, |
| 166 | }; |
| 167 | props.entry(p).or_insert_with(|| (span.clone(), vec![])).1.push(t); |
| 168 | } |
| 169 | } |
| 170 | for (ne, (span, animations)) in props { |
| 171 | let e = ne.element(); |
| 172 | // We check earlier that the property is in the set of changed properties, so a binding bust have been assigned |
| 173 | let old_anim = e.borrow().bindings.get(ne.name()).unwrap().borrow_mut().animation.replace( |
| 174 | PropertyAnimation::Transition { state_ref: state_property.clone(), animations }, |
| 175 | ); |
| 176 | if old_anim.is_some() { |
| 177 | diag.push_error( |
| 178 | format!( |
| 179 | "The property ' {}' cannot have transition because it already has an animation" , |
| 180 | ne.name() |
| 181 | ), |
| 182 | &span, |
| 183 | ); |
| 184 | } |
| 185 | } |
| 186 | } |
| 187 | |
| 188 | /// Returns a suitable unique name for the "state" property |
| 189 | fn compute_state_property_name(root_element: &ElementRc) -> SmolStr { |
| 190 | let mut property_name: String = "state" .to_owned(); |
| 191 | while root_element.borrow().lookup_property(property_name.as_ref()).property_type |
| 192 | != Type::Invalid |
| 193 | { |
| 194 | property_name += "-" ; |
| 195 | } |
| 196 | property_name.into() |
| 197 | } |
| 198 | |
| 199 | enum ExpressionForProperty { |
| 200 | TwoWayBinding, |
| 201 | Expression(Expression), |
| 202 | /// Workaround: the expression can't be represented with the current data structure, so make it an error for now. |
| 203 | InvalidBecauseOfIssue1461, |
| 204 | } |
| 205 | |
| 206 | /// Return the expression binding currently associated to the given property |
| 207 | fn expression_for_property(element: &ElementRc, name: &str) -> ExpressionForProperty { |
| 208 | let mut element_it = Some(element.clone()); |
| 209 | let mut in_base = false; |
| 210 | while let Some(elem) = element_it { |
| 211 | if let Some(e) = elem.borrow().bindings.get(name) { |
| 212 | let e = e.borrow(); |
| 213 | if !e.two_way_bindings.is_empty() { |
| 214 | return ExpressionForProperty::TwoWayBinding; |
| 215 | } |
| 216 | let mut expr = e.expression.clone(); |
| 217 | if !matches!(expr, Expression::Invalid) { |
| 218 | if in_base { |
| 219 | // Check that the expression is valid in the new scope |
| 220 | let mut has_invalid = false; |
| 221 | expr.visit_recursive_mut(&mut |ex| match ex { |
| 222 | Expression::PropertyReference(nr) |
| 223 | | Expression::FunctionCall { |
| 224 | function: Callable::Callback(nr) | Callable::Function(nr), |
| 225 | .. |
| 226 | } => { |
| 227 | let e = nr.element(); |
| 228 | if Rc::ptr_eq(&e, &elem) { |
| 229 | *nr = NamedReference::new(element, nr.name().clone()); |
| 230 | } else if Weak::ptr_eq( |
| 231 | &e.borrow().enclosing_component, |
| 232 | &elem.borrow().enclosing_component, |
| 233 | ) { |
| 234 | has_invalid = true; |
| 235 | } |
| 236 | } |
| 237 | _ => (), |
| 238 | }); |
| 239 | if has_invalid { |
| 240 | return ExpressionForProperty::InvalidBecauseOfIssue1461; |
| 241 | } |
| 242 | } |
| 243 | |
| 244 | return ExpressionForProperty::Expression(expr); |
| 245 | } |
| 246 | } |
| 247 | element_it = if let ElementType::Component(base) = &elem.borrow().base_type { |
| 248 | in_base = true; |
| 249 | Some(base.root_element.clone()) |
| 250 | } else { |
| 251 | None |
| 252 | }; |
| 253 | } |
| 254 | let expr = super::materialize_fake_properties::initialize(element, name).unwrap_or_else(|| { |
| 255 | Expression::default_value_for_type(&element.borrow().lookup_property(name).property_type) |
| 256 | }); |
| 257 | |
| 258 | ExpressionForProperty::Expression(expr) |
| 259 | } |
| 260 | |