Skip to main content

i_slint_compiler/passes/
flickable.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//! Flickable pass
5//!
6//! The Flickable element is special in the sense that it has a content element
7//! which is not exposed. This passes create the content element and fixes all property access
8//!
9//! It will also initialize proper geometry
10//! This pass must be called before the materialize_fake_properties as it going to be generate
11//! binding reference to fake properties
12
13use crate::expression_tree::{BindingExpression, Expression, MinMaxOp, NamedReference};
14use crate::langtype::{ElementType, NativeClass, Type};
15use crate::layout::is_layout;
16use crate::object_tree::{Component, Element, ElementRc};
17use crate::typeregister::TypeRegister;
18use smol_str::{SmolStr, format_smolstr};
19use std::rc::Rc;
20use std::sync::Arc;
21
22pub fn is_flickable_element(element: &ElementRc) -> bool {
23    matches!(&element.borrow().base_type, ElementType::Builtin(n) if n.name == "Flickable")
24}
25
26pub fn handle_flickable(root_component: &Rc<Component>, tr: &TypeRegister) {
27    let mut native_empty = tr.empty_type().as_builtin().native_class.clone();
28    while let Some(p) = native_empty.parent.clone() {
29        native_empty = p;
30    }
31
32    crate::object_tree::recurse_elem_including_sub_components(
33        root_component,
34        &(),
35        &mut |elem: &ElementRc, _| {
36            if !is_flickable_element(elem) {
37                return;
38            }
39
40            fixup_geometry(elem);
41            create_content_element(elem, &native_empty);
42        },
43    )
44}
45
46fn create_content_element(flickable: &ElementRc, native_empty: &Arc<NativeClass>) {
47    let children = std::mem::take(&mut flickable.borrow_mut().children);
48    let is_listview = children
49        .iter()
50        .find_map(|c| c.borrow().repeated.as_ref().and_then(|r| r.is_listview.clone()));
51
52    if let Some(listview) = &is_listview {
53        // Fox Listview, we don't bind the y property to the geometry because for large listview, we want to support coordinate with more precision than f32
54        // so the actual geometry is relative to the Flickable instead of the content element
55        // We still assign a binding to the y property in case it is read by someone
56        for c in &children {
57            if c.borrow().repeated.is_none() {
58                // Normally should not happen, listview should only have one children, and it should be repeated
59                continue;
60            }
61            let ElementType::Component(base) = c.borrow().base_type.clone() else { continue };
62            let inner_elem = &base.root_element;
63            let new_y = crate::layout::create_new_prop(
64                inner_elem,
65                SmolStr::new_static("actual-y"),
66                Type::LogicalLength,
67            );
68            new_y.mark_as_set();
69            inner_elem.borrow_mut().set_binding(
70                "y".into(),
71                Expression::BinaryExpression {
72                    lhs: Expression::PropertyReference(new_y.clone()).into(),
73                    rhs: Expression::PropertyReference(listview.content_y.clone()).into(),
74                    op: '-',
75                    source_location: None,
76                }
77                .into(),
78            );
79            inner_elem.borrow_mut().geometry_props.as_mut().unwrap().y = new_y;
80        }
81    }
82
83    let content = Element::make_rc(Element {
84        id: format_smolstr!("{}-content", flickable.borrow().id),
85        base_type: ElementType::Native(native_empty.clone()),
86        children,
87        enclosing_component: flickable.borrow().enclosing_component.clone(),
88        is_flickable_content: true,
89        ..Element::default()
90    });
91    let element_type = flickable.borrow().base_type.clone();
92    for prop in element_type.as_builtin().properties.keys() {
93        // bind the content's property to the flickable property, such as:  `width <=> parent.content-width`
94        if let Some(content_prop) = prop.strip_prefix("content-") {
95            if is_listview.is_some() && matches!(content_prop, "y" | "height") {
96                //don't bind content-y for ListView because the layout is handled by the runtime
97                continue;
98            }
99            content.borrow_mut().set_binding(
100                content_prop.into(),
101                BindingExpression::new_two_way(NamedReference::new(flickable, prop.clone()).into()),
102            );
103        }
104    }
105    content
106        .borrow()
107        .property_analysis
108        .borrow_mut()
109        .entry("y".into())
110        .or_default()
111        .is_set_externally = true;
112    content
113        .borrow()
114        .property_analysis
115        .borrow_mut()
116        .entry("x".into())
117        .or_default()
118        .is_set_externally = true;
119
120    let enclosing_component = flickable.borrow().enclosing_component.upgrade().unwrap();
121    for insertion_point in enclosing_component.child_insertion_points.borrow_mut().values_mut() {
122        if std::rc::Rc::ptr_eq(&insertion_point.parent, flickable) {
123            insertion_point.parent = content.clone()
124        }
125    }
126
127    flickable.borrow_mut().children.push(content);
128}
129
130fn fixup_geometry(flickable_elem: &ElementRc) {
131    let forward_minmax_of = |prop: &'static str, op: MinMaxOp| {
132        set_binding_if_not_explicit(flickable_elem, prop, || {
133            flickable_elem
134                .borrow()
135                .children
136                .iter()
137                .filter(|x| is_layout(&x.borrow().base_type))
138                // FIXME: we should ideally add runtime code to merge layout info of all elements that are repeated (#407)
139                .filter(|x| x.borrow().repeated.is_none())
140                .map(|x| {
141                    Expression::PropertyReference(NamedReference::new(x, SmolStr::new_static(prop)))
142                })
143                .reduce(|lhs, rhs| crate::builtin_macros::min_max_expression(lhs, rhs, op))
144        })
145    };
146
147    if !flickable_elem.borrow().is_binding_set("height", false) {
148        forward_minmax_of("max-height", MinMaxOp::Min);
149        forward_minmax_of("preferred-height", MinMaxOp::Min);
150    }
151    if !flickable_elem.borrow().is_binding_set("width", false) {
152        forward_minmax_of("max-width", MinMaxOp::Min);
153        forward_minmax_of("preferred-width", MinMaxOp::Min);
154    }
155    set_binding_if_not_explicit(flickable_elem, "content-width", || {
156        Some(
157            flickable_elem
158                .borrow()
159                .children
160                .iter()
161                .filter(|x| is_layout(&x.borrow().base_type))
162                // FIXME: (#407)
163                .filter(|x| x.borrow().repeated.is_none())
164                .map(|x| {
165                    Expression::PropertyReference(NamedReference::new(
166                        x,
167                        SmolStr::new_static("min-width"),
168                    ))
169                })
170                .fold(
171                    Expression::PropertyReference(NamedReference::new(
172                        flickable_elem,
173                        SmolStr::new_static("width"),
174                    )),
175                    |lhs, rhs| crate::builtin_macros::min_max_expression(lhs, rhs, MinMaxOp::Max),
176                ),
177        )
178    });
179    set_binding_if_not_explicit(flickable_elem, "content-height", || {
180        Some(
181            flickable_elem
182                .borrow()
183                .children
184                .iter()
185                .filter(|x| is_layout(&x.borrow().base_type))
186                // FIXME: (#407)
187                .filter(|x| x.borrow().repeated.is_none())
188                .map(|x| {
189                    Expression::PropertyReference(NamedReference::new(
190                        x,
191                        SmolStr::new_static("min-height"),
192                    ))
193                })
194                .fold(
195                    Expression::PropertyReference(NamedReference::new(
196                        flickable_elem,
197                        SmolStr::new_static("height"),
198                    )),
199                    |lhs, rhs| crate::builtin_macros::min_max_expression(lhs, rhs, MinMaxOp::Max),
200                ),
201        )
202    });
203}
204
205/// Set the property binding on the given element to the given expression (computed lazily).
206/// The parameter to the lazily calculation is the element's children
207fn set_binding_if_not_explicit(
208    elem: &ElementRc,
209    property: &str,
210    expression: impl FnOnce() -> Option<Expression>,
211) {
212    // we can't use `set_binding_if_not_set` directly because `expression()` may borrow `elem`
213    //
214    // Be careful to check both that a binding exists and that the BindingExpression actually has a
215    // binding by using is_binding_set instead of binding().is_none().
216    // Otherwise an animation on the property would prevent setting the binding, even if the binding
217    // is not set, but just animated.
218    if !elem.borrow().is_binding_set(property, false)
219        && let Some(e) = expression()
220    {
221        elem.borrow_mut().set_binding_if_not_set(property.into(), || e);
222    }
223}