Skip to main content

i_slint_compiler/passes/
materialize_fake_properties.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//! This pass creates properties that are used but are otherwise not real.
5//!
6//! Must be run after lower_layout and default_geometry passes
7
8use crate::diagnostics::Spanned;
9use crate::expression_tree::{BindingExpression, Expression, Unit};
10use crate::langtype::{ElementType, PropertyLookupMode, Type};
11use crate::layout::Orientation;
12use crate::namedreference::NamedReference;
13use crate::object_tree::*;
14use smol_str::SmolStr;
15use std::borrow::Cow;
16use std::collections::BTreeMap;
17use std::rc::Rc;
18
19pub fn materialize_fake_properties(component: &Rc<Component>) {
20    let mut to_materialize = std::collections::HashMap::new();
21
22    visit_all_named_references(component, &mut |nr| {
23        let elem = nr.element();
24        let elem = elem.borrow();
25        if !to_materialize.contains_key(nr)
26            && let Some(ty) =
27                should_materialize(&elem.property_declarations, &elem.base_type, nr.name())
28        {
29            // This only brings more trouble down the line
30            if elem.repeated.is_some() {
31                panic!(
32                    "Cannot materialize fake property {} on repeated element {}",
33                    nr.name(),
34                    elem.id
35                );
36            }
37            to_materialize.insert(nr.clone(), ty);
38        }
39    });
40
41    recurse_elem_including_sub_components_no_borrow(component, &(), &mut |elem, _| {
42        for prop in elem.borrow().real_bindings().map(|(name, _)| name) {
43            let nr = NamedReference::new(elem, prop.clone());
44            if let std::collections::hash_map::Entry::Vacant(entry) = to_materialize.entry(nr) {
45                let elem = elem.borrow();
46                if let Some(ty) =
47                    should_materialize(&elem.property_declarations, &elem.base_type, prop)
48                {
49                    entry.insert(ty);
50                }
51            }
52        }
53    });
54
55    for (nr, ty) in to_materialize {
56        let elem = nr.element();
57
58        // Note: a DebugHook binding must materialize like the binding it wraps would.
59        // A referenced property whose binding is a *synthetic* hook counts as uninitialized
60        // (see `must_initialize`), so `initialize` below upgrades the hook in place with the
61        // computed default — e.g. geometry_props references (geometry_props.x → img.x) reach
62        // this for unbound geometry, and the Transform element's two-way reference reaches it
63        // for the injected `transform-rotation`. Skipping hooked properties here instead would
64        // leave a binding on a property that never exists at runtime.
65        elem.borrow_mut().property_declarations.insert(
66            nr.name().clone(),
67            PropertyDeclaration { property_type: ty, ..PropertyDeclaration::default() },
68        );
69
70        if !must_initialize(&elem.borrow(), nr.name()) {
71            // One must check again if one really need to be initialized, because when
72            // we checked the first time, the element's binding were temporarily moved
73            // by visit_all_named_references_in_element
74            continue;
75        }
76        if let Some(init_expr) = initialize(&elem, nr.name()) {
77            let mut elem_mut = elem.borrow_mut();
78            if let Some(cell) = elem_mut.binding_cell_including_synthetic(nr.name()) {
79                // A synthetic debug hook may occupy the slot (must_initialize treats it as
80                // uninitialized): upgrade it in place, keeping the wrapper and id so the
81                // property stays live-editable.
82                cell.borrow_mut().set_value_expression(init_expr);
83            } else {
84                let span = elem_mut.to_source_location();
85                let mut binding = BindingExpression::new_with_span(init_expr, span);
86                binding.priority = i32::MAX;
87                elem_mut.set_binding(nr.name().clone(), binding);
88            }
89        }
90    }
91}
92
93// One must initialize if there is no real expression for that binding.
94fn must_initialize(elem: &Element, prop: &str) -> bool {
95    match elem.binding(prop) {
96        None => true,
97        Some(b) => {
98            matches!(b.value_expression(), Expression::Invalid)
99        }
100    }
101}
102
103/// Returns a type if the property needs to be materialized.
104pub(crate) fn should_materialize(
105    property_declarations: &BTreeMap<SmolStr, PropertyDeclaration>,
106    base_type: &ElementType,
107    prop: &str,
108) -> Option<Type> {
109    if property_declarations.contains_key(prop) {
110        return None;
111    }
112    let has_declared_property = match base_type {
113        ElementType::Component(c) => has_declared_property(&c.root_element.borrow(), prop),
114        ElementType::Builtin(b) => b.native_class.lookup_property(prop).is_some(),
115        ElementType::Native(n) => n.lookup_property(prop).is_some(),
116        ElementType::Global | ElementType::Interface | ElementType::Error => false,
117    };
118
119    if !has_declared_property {
120        let ty = crate::typeregister::reserved_property(Cow::Borrowed(prop)).property_type.clone();
121        if ty != Type::Invalid {
122            return Some(ty);
123        } else if prop == "close-on-click" {
124            // PopupWindow::close-on-click
125            return Some(Type::Bool);
126        } else if prop == "close-policy" {
127            // PopupWindow::close-policy
128            return Some(Type::Enumeration(
129                crate::typeregister::BUILTIN.enums.PopupClosePolicy.clone(),
130            ));
131        } else {
132            let ty = base_type
133                .lookup_property(prop, PropertyLookupMode::InternalName)
134                .property_type
135                .clone();
136            return (ty != Type::Invalid).then_some(ty);
137        }
138    }
139    None
140}
141
142/// Returns true if the property is declared in this element or parent
143/// (as opposed to being implicitly declared)
144pub fn has_declared_property(elem: &Element, prop: &str) -> bool {
145    if elem.property_declarations.contains_key(prop) {
146        return true;
147    }
148    match &elem.base_type {
149        ElementType::Component(c) => has_declared_property(&c.root_element.borrow(), prop),
150        ElementType::Builtin(b) => b.native_class.lookup_property(prop).is_some(),
151        ElementType::Native(n) => n.lookup_property(prop).is_some(),
152        ElementType::Global | ElementType::Interface | ElementType::Error => false,
153    }
154}
155
156/// Initialize a sensible default binding for the now materialized property
157pub fn initialize(elem: &ElementRc, name: &str) -> Option<Expression> {
158    let mut base_type = elem.borrow().base_type.clone();
159    loop {
160        base_type = match base_type {
161            ElementType::Component(ref c) => c.root_element.borrow().base_type.clone(),
162            ElementType::Builtin(b) => {
163                match b.properties.get(name).and_then(|prop| prop.default_value.expr(elem)) {
164                    Some(expr) => return Some(expr),
165                    None => break,
166                }
167            }
168            _ => break,
169        };
170    }
171
172    // Hardcode properties for images, because this is a very common call, and this allows
173    // later optimization steps to eliminate these properties.
174    // Note that Rectangles and Empties are similarly optimized in layout_constraint_prop, and
175    // we rely on struct field access simplification for those.
176    if elem.borrow().builtin_type().is_some_and(|n| n.name == "Image") {
177        if elem.borrow().effective_layout_info_prop(Orientation::Horizontal).is_none() {
178            match name {
179                "min-width" => return Some(Expression::NumberLiteral(0., Unit::Px)),
180                "max-width" => return Some(Expression::NumberLiteral(f32::MAX as _, Unit::Px)),
181                "horizontal-stretch" => return Some(Expression::NumberLiteral(0., Unit::None)),
182                _ => {}
183            }
184        }
185
186        if elem.borrow().effective_layout_info_prop(Orientation::Vertical).is_none() {
187            match name {
188                "min-height" => return Some(Expression::NumberLiteral(0., Unit::Px)),
189                "max-height" => return Some(Expression::NumberLiteral(f32::MAX as _, Unit::Px)),
190                "vertical-stretch" => return Some(Expression::NumberLiteral(0., Unit::None)),
191                _ => {}
192            }
193        }
194    }
195
196    let expr = match name {
197        "min-height" => layout_constraint_prop(elem, "min", Orientation::Vertical),
198        "min-width" => layout_constraint_prop(elem, "min", Orientation::Horizontal),
199        "max-height" => layout_constraint_prop(elem, "max", Orientation::Vertical),
200        "max-width" => layout_constraint_prop(elem, "max", Orientation::Horizontal),
201        "horizontal-stretch" => layout_constraint_prop(elem, "stretch", Orientation::Horizontal),
202        "vertical-stretch" => layout_constraint_prop(elem, "stretch", Orientation::Vertical),
203        "preferred-height" => layout_constraint_prop(elem, "preferred", Orientation::Vertical),
204        "preferred-width" => layout_constraint_prop(elem, "preferred", Orientation::Horizontal),
205        "opacity" => Expression::NumberLiteral(1., Unit::None),
206        "visible" => Expression::BoolLiteral(true),
207        "rowspan" => Expression::NumberLiteral(1., Unit::None),
208        "colspan" => Expression::NumberLiteral(1., Unit::None),
209        "rotation-origin-x" => size_div_2(elem, "width"),
210        "rotation-origin-y" => size_div_2(elem, "height"),
211        _ => return None,
212    };
213    Some(expr)
214}
215
216fn layout_constraint_prop(elem: &ElementRc, field: &str, orient: Orientation) -> Expression {
217    let expr = match elem.borrow().effective_layout_info_prop(orient) {
218        Some(e) => Expression::PropertyReference(e.clone()),
219        None => crate::layout::implicit_layout_info_call(
220            elem,
221            orient,
222            crate::layout::BuiltinFilter::All,
223            None,
224        )
225        .unwrap(),
226    };
227    Expression::StructFieldAccess { base: expr.into(), name: field.into() }
228}
229
230fn size_div_2(elem: &ElementRc, field: &str) -> Expression {
231    Expression::BinaryExpression {
232        lhs: Expression::PropertyReference(NamedReference::new(elem, field.into())).into(),
233        op: '/',
234        rhs: Expression::NumberLiteral(2., Unit::None).into(),
235        source_location: None,
236    }
237}