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::{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, diag, DiagnosticLevel::Error);
193                    let use_implicit_size = c.borrow().builtin_type().is_some_and(|b| {
194                        b.default_size_binding == DefaultSizeBinding::ImplicitSize
195                    });
196
197                    let compute = |orientation| {
198                        if !explicit_constraints.has_explicit_restrictions(orientation) {
199                            use_implicit_size.then(|| implicit_layout_info_call(c, orientation))
200                        } else {
201                            Some(explicit_layout_info(c, orientation))
202                        }
203                    };
204                    Some((compute(Orientation::Horizontal), compute(Orientation::Vertical)))
205                        .filter(|(a, b)| a.is_some() || b.is_some())
206                })
207        })
208        .collect::<Vec<_>>();
209
210    if child_infos.is_empty() {
211        return;
212    }
213
214    let li_v = crate::layout::create_new_prop(
215        elem,
216        SmolStr::new_static("layoutinfo-v"),
217        crate::typeregister::layout_info_type().into(),
218    );
219    let li_h = crate::layout::create_new_prop(
220        elem,
221        SmolStr::new_static("layoutinfo-h"),
222        crate::typeregister::layout_info_type().into(),
223    );
224    elem.borrow_mut().layout_info_prop = Some((li_h.clone(), li_v.clone()));
225    let mut expr_h = implicit_layout_info_call(elem, Orientation::Horizontal);
226    let mut expr_v = implicit_layout_info_call(elem, Orientation::Vertical);
227
228    let explicit_constraints = LayoutConstraints::new(elem, diag, DiagnosticLevel::Warning);
229    if !explicit_constraints.fixed_width {
230        merge_explicit_constraints(&mut expr_h, &explicit_constraints, Orientation::Horizontal);
231    }
232    if !explicit_constraints.fixed_height {
233        merge_explicit_constraints(&mut expr_v, &explicit_constraints, Orientation::Vertical);
234    }
235
236    for child_info in child_infos {
237        if let Some(h) = child_info.0 {
238            expr_h = Expression::BinaryExpression {
239                lhs: Box::new(std::mem::take(&mut expr_h)),
240                rhs: Box::new(h),
241                op: '+',
242            };
243        }
244        if let Some(v) = child_info.1 {
245            expr_v = Expression::BinaryExpression {
246                lhs: Box::new(std::mem::take(&mut expr_v)),
247                rhs: Box::new(v),
248                op: '+',
249            };
250        }
251    }
252
253    let expr_v = BindingExpression::new_with_span(expr_v, elem.borrow().to_source_location());
254    li_v.element().borrow_mut().bindings.insert(li_v.name().clone(), expr_v.into());
255    let expr_h = BindingExpression::new_with_span(expr_h, elem.borrow().to_source_location());
256    li_h.element().borrow_mut().bindings.insert(li_h.name().clone(), expr_h.into());
257}
258
259fn merge_explicit_constraints(
260    expr: &mut Expression,
261    constraints: &LayoutConstraints,
262    orientation: Orientation,
263) {
264    if constraints.has_explicit_restrictions(orientation) {
265        static COUNT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
266        let unique_name = format_smolstr!(
267            "layout_info_{}",
268            COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
269        );
270        let ty = expr.ty();
271        let store = Expression::StoreLocalVariable {
272            name: unique_name.clone(),
273            value: Box::new(std::mem::take(expr)),
274        };
275        let Type::Struct(s) = &ty else { unreachable!() };
276        let mut values = s
277            .fields
278            .keys()
279            .map(|p| {
280                (
281                    p.clone(),
282                    Expression::StructFieldAccess {
283                        base: Expression::ReadLocalVariable {
284                            name: unique_name.clone(),
285                            ty: ty.clone(),
286                        }
287                        .into(),
288                        name: p.clone(),
289                    },
290                )
291            })
292            .collect::<HashMap<_, _>>();
293
294        for (nr, s) in constraints.for_each_restrictions(orientation) {
295            let e = nr
296                .element()
297                .borrow()
298                .bindings
299                .get(nr.name())
300                .expect("constraint must have binding")
301                .borrow()
302                .expression
303                .clone();
304            debug_assert!(!matches!(e, Expression::Invalid));
305            values.insert(s.into(), e);
306        }
307        *expr = Expression::CodeBlock([store, Expression::Struct { ty: s.clone(), values }].into());
308    }
309}
310
311fn explicit_layout_info(e: &ElementRc, orientation: Orientation) -> Expression {
312    let mut values = HashMap::new();
313    let (size, orient) = match orientation {
314        Orientation::Horizontal => ("width", "horizontal"),
315        Orientation::Vertical => ("height", "vertical"),
316    };
317    for (k, v) in [
318        ("min", format_smolstr!("min-{size}")),
319        ("max", format_smolstr!("max-{size}")),
320        ("preferred", format_smolstr!("preferred-{size}")),
321        ("stretch", format_smolstr!("{orient}-stretch")),
322    ] {
323        values.insert(k.into(), Expression::PropertyReference(NamedReference::new(e, v)));
324    }
325    values.insert("min_percent".into(), Expression::NumberLiteral(0., Unit::None));
326    values.insert("max_percent".into(), Expression::NumberLiteral(100., Unit::None));
327    Expression::Struct { ty: crate::typeregister::layout_info_type(), values }
328}
329
330/// Replace expression such as  `"width: 30%;` with `width: 0.3 * parent.width;`
331///
332/// Returns true if the expression was 100%
333fn fix_percent_size(
334    elem: &ElementRc,
335    parent: &Option<ElementRc>,
336    property: &'static str,
337    diag: &mut BuildDiagnostics,
338) -> bool {
339    let elem = elem.borrow();
340    let binding = match elem.bindings.get(property) {
341        Some(b) => b,
342        None => return false,
343    };
344
345    if binding.borrow().ty() != Type::Percent {
346        let Some(parent) = parent.as_ref() else { return false };
347        // Pattern match to check it was already parent.<property>
348        return matches!(&binding.borrow().expression, Expression::PropertyReference(nr) if *nr.name() == property && Rc::ptr_eq(&nr.element(), parent));
349    }
350    let mut b = binding.borrow_mut();
351    if let Some(mut parent) = parent.clone() {
352        if parent.borrow().is_flickable_viewport {
353            // the `%` in a flickable need to refer to the size of the flickable, not the size of the viewport
354            parent = crate::object_tree::find_parent_element(&parent).unwrap_or(parent)
355        }
356        debug_assert_eq!(
357            parent.borrow().lookup_property(property).property_type,
358            Type::LogicalLength
359        );
360        let fill =
361            matches!(b.expression, Expression::NumberLiteral(x, _) if (x - 100.).abs() < 0.001);
362        b.expression = Expression::BinaryExpression {
363            lhs: Box::new(std::mem::take(&mut b.expression).maybe_convert_to(
364                Type::Float32,
365                &b.span,
366                diag,
367            )),
368            rhs: Box::new(Expression::PropertyReference(NamedReference::new(
369                &parent,
370                SmolStr::new_static(property),
371            ))),
372            op: '*',
373        };
374        fill
375    } else {
376        diag.push_error("Cannot find parent property to apply relative length".into(), &b.span);
377        false
378    }
379}
380
381/// Generate a size property that covers the parent.
382/// Return true if it was changed
383fn make_default_100(prop: &NamedReference, parent_prop: &NamedReference) -> bool {
384    prop.element().borrow_mut().set_binding_if_not_set(prop.name().clone(), || {
385        Expression::PropertyReference(parent_prop.clone())
386    })
387}
388
389fn make_default_implicit(elem: &ElementRc, property: &str) {
390    let e = crate::builtin_macros::min_max_expression(
391        Expression::PropertyReference(NamedReference::new(
392            elem,
393            format_smolstr!("preferred-{}", property),
394        )),
395        Expression::PropertyReference(NamedReference::new(
396            elem,
397            format_smolstr!("min-{}", property),
398        )),
399        MinMaxOp::Max,
400    );
401    elem.borrow_mut().set_binding_if_not_set(property.into(), || e);
402}
403
404// For an element with `width`, `height`, `preferred-width` and `preferred-height`, make an aspect
405// ratio preserving binding. This is currently only called for Image elements. For example when for an
406// image the `width` is specified and there is no `height` binding, it is called with `missing_size_property = height`
407// and `given_size_property = width` and install a binding like this:
408//
409//    height: self.width * self.preferred_height / self.preferred_width;
410//
411fn make_default_aspect_ratio_preserving_binding(
412    elem: &ElementRc,
413    missing_size_property: &'static str,
414    given_size_property: &'static str,
415) {
416    if elem.borrow().is_binding_set(missing_size_property, false) {
417        return;
418    }
419
420    debug_assert_eq!(elem.borrow().lookup_property("source").property_type, Type::Image);
421
422    let missing_size_property = SmolStr::new_static(missing_size_property);
423    let given_size_property = SmolStr::new_static(given_size_property);
424
425    let ratio = if elem.borrow().is_binding_set("source-clip-height", false) {
426        Expression::BinaryExpression {
427            lhs: Box::new(Expression::PropertyReference(NamedReference::new(
428                elem,
429                format_smolstr!("source-clip-{missing_size_property}"),
430            ))),
431            rhs: Box::new(Expression::PropertyReference(NamedReference::new(
432                elem,
433                format_smolstr!("source-clip-{given_size_property}"),
434            ))),
435            op: '/',
436        }
437    } else {
438        let implicit_size_var = Box::new(Expression::ReadLocalVariable {
439            name: "image_implicit_size".into(),
440            ty: BuiltinFunction::ImageSize.ty().return_type.clone(),
441        });
442
443        Expression::CodeBlock(vec![
444            Expression::StoreLocalVariable {
445                name: "image_implicit_size".into(),
446                value: Box::new(Expression::FunctionCall {
447                    function: BuiltinFunction::ImageSize.into(),
448                    arguments: vec![Expression::PropertyReference(NamedReference::new(
449                        elem,
450                        SmolStr::new_static("source"),
451                    ))],
452                    source_location: None,
453                }),
454            },
455            Expression::BinaryExpression {
456                lhs: Box::new(Expression::StructFieldAccess {
457                    base: implicit_size_var.clone(),
458                    name: missing_size_property.clone(),
459                }),
460                rhs: Box::new(Expression::StructFieldAccess {
461                    base: implicit_size_var,
462                    name: given_size_property.clone(),
463                }),
464                op: '/',
465            },
466        ])
467    };
468    let binding = Expression::BinaryExpression {
469        lhs: Box::new(ratio),
470        rhs: Expression::PropertyReference(NamedReference::new(elem, given_size_property)).into(),
471        op: '*',
472    };
473
474    elem.borrow_mut().bindings.insert(missing_size_property, RefCell::new(binding.into()));
475}
476
477fn maybe_center_in_parent(
478    elem: &ElementRc,
479    parent: &ElementRc,
480    pos_prop: &'static str,
481    size_prop: &'static str,
482) {
483    if elem.borrow().is_binding_set(pos_prop, false) {
484        return;
485    }
486
487    let size_prop = SmolStr::new_static(size_prop);
488    let diff = Expression::BinaryExpression {
489        lhs: Expression::PropertyReference(NamedReference::new(parent, size_prop.clone())).into(),
490        op: '-',
491        rhs: Expression::PropertyReference(NamedReference::new(elem, size_prop)).into(),
492    };
493
494    let pos_prop = SmolStr::new_static(pos_prop);
495    elem.borrow_mut().set_binding_if_not_set(pos_prop, || Expression::BinaryExpression {
496        lhs: diff.into(),
497        op: '/',
498        rhs: Expression::NumberLiteral(2., Unit::None).into(),
499    });
500}
501
502fn adjust_image_clip_rect(elem: &ElementRc, builtin: &Rc<BuiltinElement>) {
503    debug_assert_eq!(builtin.native_class.class_name, "ClippedImage");
504
505    if builtin.native_class.properties.keys().any(|p| {
506        elem.borrow().bindings.contains_key(p)
507            || elem.borrow().property_analysis.borrow().get(p).is_some_and(|a| a.is_used())
508    }) {
509        let source = NamedReference::new(elem, SmolStr::new_static("source"));
510        let x = NamedReference::new(elem, SmolStr::new_static("source-clip-x"));
511        let y = NamedReference::new(elem, SmolStr::new_static("source-clip-y"));
512        let make_expr = |dim: &str, prop: NamedReference| Expression::BinaryExpression {
513            lhs: Box::new(Expression::StructFieldAccess {
514                base: Box::new(Expression::FunctionCall {
515                    function: BuiltinFunction::ImageSize.into(),
516                    arguments: vec![Expression::PropertyReference(source.clone())],
517                    source_location: None,
518                }),
519                name: dim.into(),
520            }),
521            rhs: Expression::PropertyReference(prop).into(),
522            op: '-',
523        };
524
525        elem.borrow_mut()
526            .set_binding_if_not_set("source-clip-width".into(), || make_expr("width", x));
527        elem.borrow_mut()
528            .set_binding_if_not_set("source-clip-height".into(), || make_expr("height", y));
529    }
530}
531
532#[test]
533fn test_no_property_for_100pc() {
534    //! Test that we don't generate x or y property to center elements if the size is filling the parent
535    let mut compiler_config =
536        crate::CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
537    compiler_config.style = Some("fluent".into());
538    let mut test_diags = crate::diagnostics::BuildDiagnostics::default();
539    let doc_node = crate::parser::parse(
540        r#"
541        export component Foo inherits Window {
542            r1 := Rectangle {
543                r2 := Rectangle {
544                    width: 100%;
545                    background: blue;
546                }
547                r3 := Rectangle {
548                    height: parent.height;
549                    width: 50%;
550                    background: red;
551                }
552            }
553
554            out property <length> r2x: r2.x;
555            out property <length> r2y: r2.y;
556            out property <length> r3x: r3.x;
557            out property <length> r3y: r3.y;
558        }
559"#
560        .into(),
561        Some(std::path::Path::new("HELLO")),
562        &mut test_diags,
563    );
564    let (doc, diag, _) =
565        spin_on::spin_on(crate::compile_syntax_node(doc_node, test_diags, compiler_config));
566    assert!(!diag.has_errors(), "{:?}", diag.to_string_vec());
567
568    let root_elem = doc.inner_components.last().unwrap().root_element.borrow();
569
570    // const propagation must have seen that the x and y property are literal 0
571    assert!(matches!(
572        &root_elem.bindings.get("r2x").unwrap().borrow().expression,
573        Expression::NumberLiteral(v, _) if *v == 0.
574    ));
575    assert!(matches!(
576        &root_elem.bindings.get("r2y").unwrap().borrow().expression,
577        Expression::NumberLiteral(v, _) if *v == 0.
578    ));
579    assert!(matches!(
580        &root_elem.bindings.get("r3y").unwrap().borrow().expression,
581        Expression::NumberLiteral(v, _) if *v == 0.
582    ));
583    // this one is 50% so it should be set to be in the center
584    assert!(!matches!(
585        &root_elem.bindings.get("r3x").unwrap().borrow().expression,
586        Expression::BinaryExpression { .. }
587    ));
588}