i_slint_compiler/passes/
default_geometry.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/*! Set the width and height of Rectangle, TouchArea, ... to 100%,
5    the implicit width or aspect ratio preserving for Images.
6    Also set the Image.image-fit default depending on the presence of a
7    layout parent.
8
9    This pass must be run after lower_layout
10*/
11
12use std::cell::RefCell;
13use std::rc::Rc;
14
15use crate::diagnostics::{BuildDiagnostics, DiagnosticLevel, Spanned};
16use crate::expression_tree::{
17    BindingExpression, BuiltinFunction, Expression, MinMaxOp, NamedReference, Unit,
18};
19use crate::langtype::{BuiltinElement, DefaultSizeBinding, Type};
20use crate::layout::{implicit_layout_info_call, LayoutConstraints, Orientation};
21use crate::object_tree::{Component, ElementRc};
22use smol_str::{format_smolstr, SmolStr};
23use std::collections::HashMap;
24
25pub fn default_geometry(root_component: &Rc<Component>, diag: &mut BuildDiagnostics) {
26    crate::object_tree::recurse_elem_including_sub_components(
27        root_component,
28        &None,
29        &mut |elem: &ElementRc, parent: &Option<ElementRc>| {
30            if elem.borrow().repeated.is_some() {
31                return None;
32            }
33            if elem.borrow().geometry_props.is_none() {
34                // Not an element with geometry, skip it
35                return None;
36            }
37
38            // whether the width, or height, is filling the parent
39            let (mut w100, mut h100) = (false, false);
40
41            w100 |= fix_percent_size(elem, parent, "width", diag);
42            h100 |= fix_percent_size(elem, parent, "height", diag);
43
44            gen_layout_info_prop(elem, diag);
45
46            let builtin_type = match elem.borrow().builtin_type() {
47                Some(b) => b,
48                None => return Some(elem.clone()),
49            };
50
51            let is_image = builtin_type.name == "Image";
52            if is_image {
53                adjust_image_clip_rect(elem, &builtin_type);
54            }
55
56            if let Some(parent) = parent {
57                match builtin_type.default_size_binding {
58                    DefaultSizeBinding::None => {
59                        if elem.borrow().default_fill_parent.0 {
60                            let e_width =
61                                elem.borrow().geometry_props.as_ref().unwrap().width.clone();
62                            let p_width =
63                                parent.borrow().geometry_props.as_ref().unwrap().width.clone();
64                            w100 |= make_default_100(&e_width, &p_width);
65                        } else {
66                            make_default_implicit(elem, "width");
67                        }
68                        if elem.borrow().default_fill_parent.1 {
69                            let e_height =
70                                elem.borrow().geometry_props.as_ref().unwrap().height.clone();
71                            let p_height =
72                                parent.borrow().geometry_props.as_ref().unwrap().height.clone();
73                            h100 |= make_default_100(&e_height, &p_height);
74                        } else {
75                            make_default_implicit(elem, "height");
76                        }
77                    }
78                    DefaultSizeBinding::ExpandsToParentGeometry => {
79                        if !elem.borrow().child_of_layout {
80                            let (e_width, e_height) = elem
81                                .borrow()
82                                .geometry_props
83                                .as_ref()
84                                .map(|g| (g.width.clone(), g.height.clone()))
85                                .unwrap();
86                            let (p_width, p_height) = parent
87                                .borrow()
88                                .geometry_props
89                                .as_ref()
90                                .map(|g| (g.width.clone(), g.height.clone()))
91                                .unwrap();
92                            w100 |= make_default_100(&e_width, &p_width);
93                            h100 |= make_default_100(&e_height, &p_height);
94                        }
95                    }
96                    DefaultSizeBinding::ImplicitSize => {
97                        let has_length_property_binding = |elem: &ElementRc, property: &str| {
98                            debug_assert_eq!(
99                                elem.borrow().lookup_property(property).property_type,
100                                Type::LogicalLength
101                            );
102
103                            elem.borrow().is_binding_set(property, true)
104                        };
105
106                        let width_specified = has_length_property_binding(elem, "width");
107                        let height_specified = has_length_property_binding(elem, "height");
108
109                        if !elem.borrow().child_of_layout {
110                            // Add aspect-ratio preserving width or height bindings
111                            if is_image && width_specified && !height_specified {
112                                make_default_aspect_ratio_preserving_binding(
113                                    elem, "height", "width",
114                                )
115                            } else if is_image && height_specified && !width_specified {
116                                make_default_aspect_ratio_preserving_binding(
117                                    elem, "width", "height",
118                                )
119                            } else {
120                                make_default_implicit(elem, "width");
121                                make_default_implicit(elem, "height");
122                            }
123                        } else if is_image {
124                            // If an image is in a layout and has no explicit width or height specified, change the default for image-fit
125                            // to `contain`
126                            if !width_specified || !height_specified {
127                                let image_fit_lookup = elem.borrow().lookup_property("image-fit");
128
129                                elem.borrow_mut().set_binding_if_not_set(
130                                    image_fit_lookup.resolved_name.into(),
131                                    || {
132                                        Expression::EnumerationValue(
133                                            image_fit_lookup
134                                                .property_type
135                                                .as_enum()
136                                                .clone()
137                                                .try_value_from_string("contain")
138                                                .unwrap(),
139                                        )
140                                    },
141                                );
142                            }
143                        }
144                    }
145                }
146
147                if !elem.borrow().child_of_layout
148                    && !elem.borrow().is_legacy_syntax
149                    && builtin_type.name != "Window"
150                {
151                    if !w100 {
152                        maybe_center_in_parent(elem, parent, "x", "width");
153                    }
154                    if !h100 {
155                        maybe_center_in_parent(elem, parent, "y", "height");
156                    }
157                }
158            }
159
160            Some(elem.clone())
161        },
162    )
163}
164
165/// Generate a layout_info_prop based on the children layouts
166fn gen_layout_info_prop(elem: &ElementRc, diag: &mut BuildDiagnostics) {
167    if elem.borrow().layout_info_prop.is_some() || elem.borrow().is_flickable_viewport {
168        return;
169    }
170
171    let child_infos = elem
172        .borrow()
173        .children
174        .iter()
175        .filter(|c| {
176            !c.borrow().bindings.contains_key("x") && !c.borrow().bindings.contains_key("y")
177        })
178        .filter_map(|c| {
179            gen_layout_info_prop(c, diag);
180            c.borrow()
181                .layout_info_prop
182                .clone()
183                .map(|(h, v)| {
184                    (Some(Expression::PropertyReference(h)), Some(Expression::PropertyReference(v)))
185                })
186                .or_else(|| {
187                    if c.borrow().is_legacy_syntax {
188                        return None;
189                    }
190                    if c.borrow().repeated.is_some() {
191                        // FIXME: we should ideally add runtime code to merge layout info of all elements that are repeated (same as #407)
192                        return None;
193                    }
194                    let explicit_constraints =
195                        LayoutConstraints::new(c, diag, DiagnosticLevel::Error);
196                    let use_implicit_size = c.borrow().builtin_type().is_some_and(|b| {
197                        b.default_size_binding == DefaultSizeBinding::ImplicitSize
198                    });
199
200                    let compute = |orientation| {
201                        if !explicit_constraints.has_explicit_restrictions(orientation) {
202                            use_implicit_size.then(|| implicit_layout_info_call(c, orientation))
203                        } else {
204                            Some(explicit_layout_info(c, orientation))
205                        }
206                    };
207                    Some((compute(Orientation::Horizontal), compute(Orientation::Vertical)))
208                        .filter(|(a, b)| a.is_some() || b.is_some())
209                })
210        })
211        .collect::<Vec<_>>();
212
213    if child_infos.is_empty() {
214        return;
215    }
216
217    let li_v = crate::layout::create_new_prop(
218        elem,
219        SmolStr::new_static("layoutinfo-v"),
220        crate::typeregister::layout_info_type().into(),
221    );
222    let li_h = crate::layout::create_new_prop(
223        elem,
224        SmolStr::new_static("layoutinfo-h"),
225        crate::typeregister::layout_info_type().into(),
226    );
227    elem.borrow_mut().layout_info_prop = Some((li_h.clone(), li_v.clone()));
228    let mut expr_h = implicit_layout_info_call(elem, Orientation::Horizontal);
229    let mut expr_v = implicit_layout_info_call(elem, Orientation::Vertical);
230
231    let explicit_constraints = LayoutConstraints::new(elem, diag, DiagnosticLevel::Warning);
232    if !explicit_constraints.fixed_width {
233        merge_explicit_constraints(&mut expr_h, &explicit_constraints, Orientation::Horizontal);
234    }
235    if !explicit_constraints.fixed_height {
236        merge_explicit_constraints(&mut expr_v, &explicit_constraints, Orientation::Vertical);
237    }
238
239    for child_info in child_infos {
240        if let Some(h) = child_info.0 {
241            expr_h = Expression::BinaryExpression {
242                lhs: Box::new(std::mem::take(&mut expr_h)),
243                rhs: Box::new(h),
244                op: '+',
245            };
246        }
247        if let Some(v) = child_info.1 {
248            expr_v = Expression::BinaryExpression {
249                lhs: Box::new(std::mem::take(&mut expr_v)),
250                rhs: Box::new(v),
251                op: '+',
252            };
253        }
254    }
255
256    let expr_v = BindingExpression::new_with_span(expr_v, elem.borrow().to_source_location());
257    li_v.element().borrow_mut().bindings.insert(li_v.name().clone(), expr_v.into());
258    let expr_h = BindingExpression::new_with_span(expr_h, elem.borrow().to_source_location());
259    li_h.element().borrow_mut().bindings.insert(li_h.name().clone(), expr_h.into());
260}
261
262fn merge_explicit_constraints(
263    expr: &mut Expression,
264    constraints: &LayoutConstraints,
265    orientation: Orientation,
266) {
267    if constraints.has_explicit_restrictions(orientation) {
268        static COUNT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
269        let unique_name = format_smolstr!(
270            "layout_info_{}",
271            COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
272        );
273        let ty = expr.ty();
274        let store = Expression::StoreLocalVariable {
275            name: unique_name.clone(),
276            value: Box::new(std::mem::take(expr)),
277        };
278        let Type::Struct(s) = &ty else { unreachable!() };
279        let mut values = s
280            .fields
281            .keys()
282            .map(|p| {
283                (
284                    p.clone(),
285                    Expression::StructFieldAccess {
286                        base: Expression::ReadLocalVariable {
287                            name: unique_name.clone(),
288                            ty: ty.clone(),
289                        }
290                        .into(),
291                        name: p.clone(),
292                    },
293                )
294            })
295            .collect::<HashMap<_, _>>();
296
297        for (nr, s) in constraints.for_each_restrictions(orientation) {
298            let e = nr
299                .element()
300                .borrow()
301                .bindings
302                .get(nr.name())
303                .expect("constraint must have binding")
304                .borrow()
305                .expression
306                .clone();
307            debug_assert!(!matches!(e, Expression::Invalid));
308            values.insert(s.into(), e);
309        }
310        *expr = Expression::CodeBlock([store, Expression::Struct { ty: s.clone(), values }].into());
311    }
312}
313
314fn explicit_layout_info(e: &ElementRc, orientation: Orientation) -> Expression {
315    let mut values = HashMap::new();
316    let (size, orient) = match orientation {
317        Orientation::Horizontal => ("width", "horizontal"),
318        Orientation::Vertical => ("height", "vertical"),
319    };
320    for (k, v) in [
321        ("min", format_smolstr!("min-{size}")),
322        ("max", format_smolstr!("max-{size}")),
323        ("preferred", format_smolstr!("preferred-{size}")),
324        ("stretch", format_smolstr!("{orient}-stretch")),
325    ] {
326        values.insert(k.into(), Expression::PropertyReference(NamedReference::new(e, v)));
327    }
328    values.insert("min_percent".into(), Expression::NumberLiteral(0., Unit::None));
329    values.insert("max_percent".into(), Expression::NumberLiteral(100., Unit::None));
330    Expression::Struct { ty: crate::typeregister::layout_info_type(), values }
331}
332
333/// Replace expression such as  `"width: 30%;` with `width: 0.3 * parent.width;`
334///
335/// Returns true if the expression was 100%
336fn fix_percent_size(
337    elem: &ElementRc,
338    parent: &Option<ElementRc>,
339    property: &'static str,
340    diag: &mut BuildDiagnostics,
341) -> bool {
342    let elem = elem.borrow();
343    let binding = match elem.bindings.get(property) {
344        Some(b) => b,
345        None => return false,
346    };
347
348    if binding.borrow().ty() != Type::Percent {
349        let Some(parent) = parent.as_ref() else { return false };
350        // Pattern match to check it was already parent.<property>
351        return matches!(&binding.borrow().expression, Expression::PropertyReference(nr) if *nr.name() == property && Rc::ptr_eq(&nr.element(), parent));
352    }
353    let mut b = binding.borrow_mut();
354    if let Some(mut parent) = parent.clone() {
355        if parent.borrow().is_flickable_viewport {
356            // the `%` in a flickable need to refer to the size of the flickable, not the size of the viewport
357            parent = crate::object_tree::find_parent_element(&parent).unwrap_or(parent)
358        }
359        debug_assert_eq!(
360            parent.borrow().lookup_property(property).property_type,
361            Type::LogicalLength
362        );
363        let fill =
364            matches!(b.expression, Expression::NumberLiteral(x, _) if (x - 100.).abs() < 0.001);
365        b.expression = Expression::BinaryExpression {
366            lhs: Box::new(std::mem::take(&mut b.expression).maybe_convert_to(
367                Type::Float32,
368                &b.span,
369                diag,
370            )),
371            rhs: Box::new(Expression::PropertyReference(NamedReference::new(
372                &parent,
373                SmolStr::new_static(property),
374            ))),
375            op: '*',
376        };
377        fill
378    } else {
379        diag.push_error("Cannot find parent property to apply relative length".into(), &b.span);
380        false
381    }
382}
383
384/// Generate a size property that covers the parent.
385/// Return true if it was changed
386fn make_default_100(prop: &NamedReference, parent_prop: &NamedReference) -> bool {
387    prop.element().borrow_mut().set_binding_if_not_set(prop.name().clone(), || {
388        Expression::PropertyReference(parent_prop.clone())
389    })
390}
391
392fn make_default_implicit(elem: &ElementRc, property: &str) {
393    let e = crate::builtin_macros::min_max_expression(
394        Expression::PropertyReference(NamedReference::new(
395            elem,
396            format_smolstr!("preferred-{}", property),
397        )),
398        Expression::PropertyReference(NamedReference::new(
399            elem,
400            format_smolstr!("min-{}", property),
401        )),
402        MinMaxOp::Max,
403    );
404    elem.borrow_mut().set_binding_if_not_set(property.into(), || e);
405}
406
407// For an element with `width`, `height`, `preferred-width` and `preferred-height`, make an aspect
408// ratio preserving binding. This is currently only called for Image elements. For example when for an
409// image the `width` is specified and there is no `height` binding, it is called with `missing_size_property = height`
410// and `given_size_property = width` and install a binding like this:
411//
412//    height: self.width * self.preferred_height / self.preferred_width;
413//
414fn make_default_aspect_ratio_preserving_binding(
415    elem: &ElementRc,
416    missing_size_property: &'static str,
417    given_size_property: &'static str,
418) {
419    if elem.borrow().is_binding_set(missing_size_property, false) {
420        return;
421    }
422
423    debug_assert_eq!(elem.borrow().lookup_property("source").property_type, Type::Image);
424
425    let missing_size_property = SmolStr::new_static(missing_size_property);
426    let given_size_property = SmolStr::new_static(given_size_property);
427
428    let ratio = if elem.borrow().is_binding_set("source-clip-height", false) {
429        Expression::BinaryExpression {
430            lhs: Box::new(Expression::PropertyReference(NamedReference::new(
431                elem,
432                format_smolstr!("source-clip-{missing_size_property}"),
433            ))),
434            rhs: Box::new(Expression::PropertyReference(NamedReference::new(
435                elem,
436                format_smolstr!("source-clip-{given_size_property}"),
437            ))),
438            op: '/',
439        }
440    } else {
441        let implicit_size_var = Box::new(Expression::ReadLocalVariable {
442            name: "image_implicit_size".into(),
443            ty: BuiltinFunction::ImageSize.ty().return_type.clone(),
444        });
445
446        Expression::CodeBlock(vec![
447            Expression::StoreLocalVariable {
448                name: "image_implicit_size".into(),
449                value: Box::new(Expression::FunctionCall {
450                    function: BuiltinFunction::ImageSize.into(),
451                    arguments: vec![Expression::PropertyReference(NamedReference::new(
452                        elem,
453                        SmolStr::new_static("source"),
454                    ))],
455                    source_location: None,
456                }),
457            },
458            Expression::BinaryExpression {
459                lhs: Box::new(Expression::StructFieldAccess {
460                    base: implicit_size_var.clone(),
461                    name: missing_size_property.clone(),
462                }),
463                rhs: Box::new(Expression::StructFieldAccess {
464                    base: implicit_size_var,
465                    name: given_size_property.clone(),
466                }),
467                op: '/',
468            },
469        ])
470    };
471    let binding = Expression::BinaryExpression {
472        lhs: Box::new(ratio),
473        rhs: Expression::PropertyReference(NamedReference::new(elem, given_size_property)).into(),
474        op: '*',
475    };
476
477    elem.borrow_mut().bindings.insert(missing_size_property, RefCell::new(binding.into()));
478}
479
480fn maybe_center_in_parent(
481    elem: &ElementRc,
482    parent: &ElementRc,
483    pos_prop: &'static str,
484    size_prop: &'static str,
485) {
486    if elem.borrow().is_binding_set(pos_prop, false) {
487        return;
488    }
489
490    let size_prop = SmolStr::new_static(size_prop);
491    let diff = Expression::BinaryExpression {
492        lhs: Expression::PropertyReference(NamedReference::new(parent, size_prop.clone())).into(),
493        op: '-',
494        rhs: Expression::PropertyReference(NamedReference::new(elem, size_prop)).into(),
495    };
496
497    let pos_prop = SmolStr::new_static(pos_prop);
498    elem.borrow_mut().set_binding_if_not_set(pos_prop, || Expression::BinaryExpression {
499        lhs: diff.into(),
500        op: '/',
501        rhs: Expression::NumberLiteral(2., Unit::None).into(),
502    });
503}
504
505fn adjust_image_clip_rect(elem: &ElementRc, builtin: &Rc<BuiltinElement>) {
506    debug_assert_eq!(builtin.native_class.class_name, "ClippedImage");
507
508    if builtin.native_class.properties.keys().any(|p| {
509        elem.borrow().bindings.contains_key(p)
510            || elem.borrow().property_analysis.borrow().get(p).is_some_and(|a| a.is_used())
511    }) {
512        let source = NamedReference::new(elem, SmolStr::new_static("source"));
513        let x = NamedReference::new(elem, SmolStr::new_static("source-clip-x"));
514        let y = NamedReference::new(elem, SmolStr::new_static("source-clip-y"));
515        let make_expr = |dim: &str, prop: NamedReference| Expression::BinaryExpression {
516            lhs: Box::new(Expression::StructFieldAccess {
517                base: Box::new(Expression::FunctionCall {
518                    function: BuiltinFunction::ImageSize.into(),
519                    arguments: vec![Expression::PropertyReference(source.clone())],
520                    source_location: None,
521                }),
522                name: dim.into(),
523            }),
524            rhs: Expression::PropertyReference(prop).into(),
525            op: '-',
526        };
527
528        elem.borrow_mut()
529            .set_binding_if_not_set("source-clip-width".into(), || make_expr("width", x));
530        elem.borrow_mut()
531            .set_binding_if_not_set("source-clip-height".into(), || make_expr("height", y));
532    }
533}
534
535#[test]
536fn test_no_property_for_100pc() {
537    //! Test that we don't generate x or y property to center elements if the size is filling the parent
538    let mut compiler_config =
539        crate::CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
540    compiler_config.style = Some("fluent".into());
541    let mut test_diags = crate::diagnostics::BuildDiagnostics::default();
542    let doc_node = crate::parser::parse(
543        r#"
544        export component Foo inherits Window {
545            r1 := Rectangle {
546                r2 := Rectangle {
547                    width: 100%;
548                    background: blue;
549                }
550                r3 := Rectangle {
551                    height: parent.height;
552                    width: 50%;
553                    background: red;
554                }
555            }
556
557            out property <length> r2x: r2.x;
558            out property <length> r2y: r2.y;
559            out property <length> r3x: r3.x;
560            out property <length> r3y: r3.y;
561        }
562"#
563        .into(),
564        Some(std::path::Path::new("HELLO")),
565        &mut test_diags,
566    );
567    let (doc, diag, _) =
568        spin_on::spin_on(crate::compile_syntax_node(doc_node, test_diags, compiler_config));
569    assert!(!diag.has_errors(), "{:?}", diag.to_string_vec());
570
571    let root_elem = doc.inner_components.last().unwrap().root_element.borrow();
572
573    // const propagation must have seen that the x and y property are literal 0
574    assert!(matches!(
575        &root_elem.bindings.get("r2x").unwrap().borrow().expression,
576        Expression::NumberLiteral(v, _) if *v == 0.
577    ));
578    assert!(matches!(
579        &root_elem.bindings.get("r2y").unwrap().borrow().expression,
580        Expression::NumberLiteral(v, _) if *v == 0.
581    ));
582    assert!(matches!(
583        &root_elem.bindings.get("r3y").unwrap().borrow().expression,
584        Expression::NumberLiteral(v, _) if *v == 0.
585    ));
586    // this one is 50% so it should be set to be in the center
587    assert!(!matches!(
588        &root_elem.bindings.get("r3x").unwrap().borrow().expression,
589        Expression::BinaryExpression { .. }
590    ));
591}