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 viewport
7//! which is not exposed. This passes create the viewport 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 core::cell::RefCell;
19use smol_str::{format_smolstr, SmolStr};
20use std::rc::Rc;
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_viewport_element(elem, &native_empty);
42        },
43    )
44}
45
46fn create_viewport_element(flickable: &ElementRc, native_empty: &Rc<NativeClass>) {
47    let children = std::mem::take(&mut flickable.borrow_mut().children);
48    let is_listview = children
49        .iter()
50        .any(|c| c.borrow().repeated.as_ref().is_some_and(|r| r.is_listview.is_some()));
51
52    if 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 viewport
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().bindings.insert(
70                "y".into(),
71                RefCell::new(
72                    Expression::BinaryExpression {
73                        lhs: Expression::PropertyReference(new_y.clone()).into(),
74                        rhs: Expression::PropertyReference(NamedReference::new(
75                            flickable,
76                            SmolStr::new_static("viewport-y"),
77                        ))
78                        .into(),
79                        op: '-',
80                    }
81                    .into(),
82                ),
83            );
84            inner_elem.borrow_mut().geometry_props.as_mut().unwrap().y = new_y;
85        }
86    }
87
88    let viewport = Element::make_rc(Element {
89        id: format_smolstr!("{}-viewport", flickable.borrow().id),
90        base_type: ElementType::Native(native_empty.clone()),
91        children,
92        enclosing_component: flickable.borrow().enclosing_component.clone(),
93        is_flickable_viewport: true,
94        ..Element::default()
95    });
96    let element_type = flickable.borrow().base_type.clone();
97    for prop in element_type.as_builtin().properties.keys() {
98        // bind the viewport's property to the flickable property, such as:  `width <=> parent.viewport-width`
99        if let Some(vp_prop) = prop.strip_prefix("viewport-") {
100            if is_listview && matches!(vp_prop, "y" | "height") {
101                //don't bind viewport-y for ListView because the layout is handled by the runtime
102                continue;
103            }
104            viewport.borrow_mut().bindings.insert(
105                vp_prop.into(),
106                BindingExpression::new_two_way(NamedReference::new(flickable, prop.clone())).into(),
107            );
108        }
109    }
110    viewport
111        .borrow()
112        .property_analysis
113        .borrow_mut()
114        .entry("y".into())
115        .or_default()
116        .is_set_externally = true;
117    viewport
118        .borrow()
119        .property_analysis
120        .borrow_mut()
121        .entry("x".into())
122        .or_default()
123        .is_set_externally = true;
124
125    let enclosing_component = flickable.borrow().enclosing_component.upgrade().unwrap();
126    if let Some(insertion_point) = &mut *enclosing_component.child_insertion_point.borrow_mut() {
127        if std::rc::Rc::ptr_eq(&insertion_point.parent, flickable) {
128            insertion_point.parent = viewport.clone()
129        }
130    }
131
132    flickable.borrow_mut().children.push(viewport);
133}
134
135fn fixup_geometry(flickable_elem: &ElementRc) {
136    let forward_minmax_of = |prop: &'static str, op: MinMaxOp| {
137        set_binding_if_not_explicit(flickable_elem, prop, || {
138            flickable_elem
139                .borrow()
140                .children
141                .iter()
142                .filter(|x| is_layout(&x.borrow().base_type))
143                // FIXME: we should ideally add runtime code to merge layout info of all elements that are repeated (#407)
144                .filter(|x| x.borrow().repeated.is_none())
145                .map(|x| {
146                    Expression::PropertyReference(NamedReference::new(x, SmolStr::new_static(prop)))
147                })
148                .reduce(|lhs, rhs| crate::builtin_macros::min_max_expression(lhs, rhs, op))
149        })
150    };
151
152    if !flickable_elem.borrow().bindings.contains_key("height") {
153        forward_minmax_of("max-height", MinMaxOp::Min);
154        forward_minmax_of("preferred-height", MinMaxOp::Min);
155    }
156    if !flickable_elem.borrow().bindings.contains_key("width") {
157        forward_minmax_of("max-width", MinMaxOp::Min);
158        forward_minmax_of("preferred-width", MinMaxOp::Min);
159    }
160    set_binding_if_not_explicit(flickable_elem, "viewport-width", || {
161        Some(
162            flickable_elem
163                .borrow()
164                .children
165                .iter()
166                .filter(|x| is_layout(&x.borrow().base_type))
167                // FIXME: (#407)
168                .filter(|x| x.borrow().repeated.is_none())
169                .map(|x| {
170                    Expression::PropertyReference(NamedReference::new(
171                        x,
172                        SmolStr::new_static("min-width"),
173                    ))
174                })
175                .fold(
176                    Expression::PropertyReference(NamedReference::new(
177                        flickable_elem,
178                        SmolStr::new_static("width"),
179                    )),
180                    |lhs, rhs| crate::builtin_macros::min_max_expression(lhs, rhs, MinMaxOp::Max),
181                ),
182        )
183    });
184    set_binding_if_not_explicit(flickable_elem, "viewport-height", || {
185        Some(
186            flickable_elem
187                .borrow()
188                .children
189                .iter()
190                .filter(|x| is_layout(&x.borrow().base_type))
191                // FIXME: (#407)
192                .filter(|x| x.borrow().repeated.is_none())
193                .map(|x| {
194                    Expression::PropertyReference(NamedReference::new(
195                        x,
196                        SmolStr::new_static("min-height"),
197                    ))
198                })
199                .fold(
200                    Expression::PropertyReference(NamedReference::new(
201                        flickable_elem,
202                        SmolStr::new_static("height"),
203                    )),
204                    |lhs, rhs| crate::builtin_macros::min_max_expression(lhs, rhs, MinMaxOp::Max),
205                ),
206        )
207    });
208}
209
210/// Set the property binding on the given element to the given expression (computed lazily).
211/// The parameter to the lazily calculation is the element's children
212fn set_binding_if_not_explicit(
213    elem: &ElementRc,
214    property: &str,
215    expression: impl FnOnce() -> Option<Expression>,
216) {
217    // we can't use `set_binding_if_not_set` directly because `expression()` may borrow `elem`
218    if elem.borrow().bindings.get(property).map_or(true, |b| !b.borrow().has_binding()) {
219        if let Some(e) = expression() {
220            elem.borrow_mut().set_binding_if_not_set(property.into(), || e);
221        }
222    }
223}