Skip to main content

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