Skip to main content

i_slint_compiler/passes/
lower_states.rs

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
6use crate::diagnostics::BuildDiagnostics;
7use crate::diagnostics::SourceLocation;
8use crate::diagnostics::Spanned;
9use crate::expression_tree::*;
10use crate::langtype::{PropertyLookupMode, Type};
11use crate::object_tree::forward_inherited_expression::{
12    ForwardedReferenceCache, InheritedExpression, forward_inherited_expression,
13};
14use crate::object_tree::*;
15use crate::symbol_counters::SymbolCounters;
16use smol_str::SmolStr;
17use std::collections::{HashMap, HashSet};
18use std::rc::Rc;
19
20pub fn lower_states(
21    component: &Rc<Component>,
22    symbol_counters: &SymbolCounters,
23    forwarded_references: &mut ForwardedReferenceCache,
24    diag: &mut BuildDiagnostics,
25) {
26    let state_info_type = crate::typeregister::BUILTIN.state_info_type.clone().into();
27    recurse_elem(&component.root_element, &(), &mut |elem, _| {
28        lower_state_in_element(elem, &state_info_type, symbol_counters, forwarded_references, diag)
29    });
30}
31
32fn lower_state_in_element(
33    root_element: &ElementRc,
34    state_info_type: &Type,
35    symbol_counters: &SymbolCounters,
36    forwarded_references: &mut ForwardedReferenceCache,
37    diag: &mut BuildDiagnostics,
38) {
39    if root_element.borrow().states.is_empty() {
40        return;
41    }
42    let has_transitions = !root_element.borrow().transitions.is_empty();
43    let state_property_nr = crate::layout::create_new_prop(
44        root_element,
45        SmolStr::new_static("state"),
46        if has_transitions { state_info_type.clone() } else { Type::Int32 },
47    );
48    let state_property = Expression::PropertyReference(state_property_nr.clone());
49    let state_property_ref = if has_transitions {
50        Expression::StructFieldAccess {
51            base: Box::new(state_property.clone()),
52            name: "current-state".into(),
53        }
54    } else {
55        state_property.clone()
56    };
57    let mut affected_properties = HashSet::new();
58    // Maps State name string -> integer id
59    let mut states_id = HashMap::new();
60    let mut state_value = Expression::NumberLiteral(0., Unit::None);
61    let states = std::mem::take(&mut root_element.borrow_mut().states);
62    for (idx, state) in states.into_iter().enumerate().rev() {
63        if let Some(condition) = &state.condition {
64            state_value = Expression::Condition {
65                condition: Box::new(condition.clone()),
66                true_expr: Box::new(Expression::NumberLiteral((idx + 1) as _, Unit::None)),
67                false_expr: Box::new(std::mem::take(&mut state_value)),
68                source_location: state.selection.clone(),
69            };
70        }
71        for (property_reference, expr, node) in state.property_changes {
72            affected_properties.insert(property_reference.clone());
73            let element = property_reference.element();
74            let property_expr = match expression_for_property(
75                &element,
76                property_reference.name(),
77                symbol_counters,
78                forwarded_references,
79            ) {
80                ExpressionForProperty::TwoWayBinding => {
81                    diag.push_error(
82                    format!("Cannot change the property '{}' in a state because it is initialized with a two-way binding", property_reference.name()),
83                    &node
84                );
85                    continue;
86                }
87                ExpressionForProperty::Expression(e) => e,
88            };
89            let new_expr = Expression::Condition {
90                condition: Box::new(Expression::BinaryExpression {
91                    source_location: None,
92                    lhs: Box::new(state_property_ref.clone()),
93                    rhs: Box::new(Expression::NumberLiteral((idx + 1) as _, Unit::None)),
94                    op: '=',
95                }),
96                true_expr: Box::new(expr),
97                false_expr: Box::new(property_expr),
98                source_location: Some(ConditionLocation::StateChange(
99                    node.QualifiedName().to_source_location(),
100                )),
101            };
102
103            let name = property_reference.name();
104            if let Some(cell) = element.borrow().binding_cell_including_synthetic(name) {
105                // A synthetic hook is upgraded in place; a real binding's hook survives inside
106                // `property_expr` (the false-branch of `new_expr`), so replacing it is correct.
107                cell.borrow_mut().set_value_expression(new_expr);
108            } else {
109                let mut r = BindingExpression::from(new_expr);
110                r.priority = 1;
111                element.borrow_mut().set_binding(name.clone(), r);
112            }
113        }
114        states_id.insert(state.id, idx as i32 + 1);
115    }
116
117    root_element.borrow_mut().set_binding(state_property_nr.name().clone(), state_value.into());
118
119    lower_transitions_in_element(
120        root_element,
121        state_property,
122        states_id,
123        affected_properties,
124        diag,
125    );
126}
127
128fn lower_transitions_in_element(
129    elem: &ElementRc,
130    state_property: Expression,
131    states_id: HashMap<SmolStr, i32>,
132    affected_properties: HashSet<NamedReference>,
133    diag: &mut BuildDiagnostics,
134) {
135    let transitions = std::mem::take(&mut elem.borrow_mut().transitions);
136    let mut props =
137        HashMap::<NamedReference, (SourceLocation, Vec<TransitionPropertyAnimation>)>::new();
138    for transition in transitions {
139        let state = states_id.get(&transition.state_id).unwrap_or_else(|| {
140            diag.push_error(
141                format!("State '{}' does not exist", transition.state_id),
142                transition
143                    .node
144                    .DeclaredIdentifier()
145                    .as_ref()
146                    .map(|x| x as &dyn Spanned)
147                    .unwrap_or(&transition.node as &dyn Spanned),
148            );
149            &0
150        });
151
152        for (p, span, animation) in transition.property_animations {
153            if !affected_properties.contains(&p) {
154                diag.push_error(
155                    "The property is not changed as part of this transition".into(),
156                    &span,
157                );
158                continue;
159            }
160
161            let t = TransitionPropertyAnimation {
162                state_id: *state,
163                direction: transition.direction,
164                animation,
165            };
166            props.entry(p).or_insert_with(|| (span.clone(), Vec::new())).1.push(t);
167        }
168    }
169    for (ne, (span, animations)) in props {
170        let e = ne.element();
171        // We check earlier that the property is in the set of changed properties, so a binding bust have been assigned
172        let old_anim = e.borrow().binding_mut(ne.name()).unwrap().animation.replace(
173            PropertyAnimation::Transition { state_ref: state_property.clone(), animations },
174        );
175        if old_anim.is_some() {
176            diag.push_error(
177                format!(
178                    "The property '{}' cannot have transition because it already has an animation",
179                    ne.name()
180                ),
181                &span,
182            );
183        }
184    }
185}
186
187enum ExpressionForProperty {
188    TwoWayBinding,
189    Expression(Expression),
190}
191
192/// Return the expression binding currently associated to the given property
193fn expression_for_property(
194    element: &ElementRc,
195    name: &str,
196    symbol_counters: &SymbolCounters,
197    forwarded_references: &mut ForwardedReferenceCache,
198) -> ExpressionForProperty {
199    let local_binding = element
200        .borrow()
201        .binding(name)
202        .map(|binding| (!binding.two_way_bindings.is_empty(), binding.expression.clone()));
203    if let Some((is_two_way_binding, expression)) = local_binding {
204        if is_two_way_binding {
205            return ExpressionForProperty::TwoWayBinding;
206        }
207        if !matches!(expression, Expression::Invalid) {
208            return ExpressionForProperty::Expression(expression);
209        }
210    }
211
212    match forward_inherited_expression(element, name, symbol_counters, forwarded_references) {
213        InheritedExpression::Expression(expression) => {
214            return ExpressionForProperty::Expression(expression);
215        }
216        InheritedExpression::TwoWayBinding => return ExpressionForProperty::TwoWayBinding,
217        InheritedExpression::Unbound => {}
218    }
219
220    let expression =
221        super::materialize_fake_properties::initialize(element, name).unwrap_or_else(|| {
222            Expression::default_value_for_type(
223                &element
224                    .borrow()
225                    .lookup_property(name, PropertyLookupMode::InternalName)
226                    .property_type,
227            )
228        });
229
230    ExpressionForProperty::Expression(expression)
231}