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::rc::Rc;
13
14use crate::diagnostics::{BuildDiagnostics, DiagnosticLevel, SourceLocation, Spanned};
15use crate::expression_tree::{
16    BindingExpression, BuiltinFunction, Expression, MinMaxOp, NamedReference, Unit,
17};
18use crate::langtype::{BuiltinElement, DefaultSizeBinding, PropertyLookupMode, Type};
19use crate::layout::{BuiltinFilter, LayoutConstraints, Orientation, implicit_layout_info_call};
20use crate::object_tree::{Component, ElementRc};
21use crate::symbol_counters::SymbolCounters;
22use smol_str::{SmolStr, format_smolstr};
23use std::collections::BTreeMap;
24
25pub fn default_geometry(
26    root_component: &Rc<Component>,
27    diag: &mut BuildDiagnostics,
28    symbol_counters: &SymbolCounters,
29) {
30    crate::object_tree::recurse_elem_including_sub_components(
31        root_component,
32        &None,
33        &mut |elem: &ElementRc, parent: &Option<ElementRc>| {
34            if elem.borrow().repeated.is_some() {
35                return None;
36            }
37            elem.borrow().geometry_props.as_ref()?;
38
39            // whether the width, or height, is filling the parent
40            let (mut w100, mut h100) = (false, false);
41
42            w100 |= fix_percent_size(elem, parent, "width", diag, symbol_counters);
43            h100 |= fix_percent_size(elem, parent, "height", diag, symbol_counters);
44
45            gen_layout_info_prop(elem, diag, symbol_counters);
46
47            let builtin_type = match elem.borrow().builtin_type() {
48                Some(b) => b,
49                None => return Some(elem.clone()),
50            };
51
52            let is_image = builtin_type.name == "Image";
53            if is_image {
54                adjust_image_clip_rect(elem, &builtin_type);
55            }
56
57            if let Some(parent) = parent {
58                match builtin_type.default_size_binding {
59                    DefaultSizeBinding::None => {
60                        if elem.borrow().default_fill_parent.0 {
61                            let e_width =
62                                elem.borrow().geometry_props.as_ref().unwrap().width.clone();
63                            let p_width =
64                                parent.borrow().geometry_props.as_ref().unwrap().width.clone();
65                            w100 |= make_default_100(&e_width, &p_width);
66                        } else {
67                            make_default_implicit(elem, "width");
68                        }
69                        if elem.borrow().default_fill_parent.1 {
70                            let e_height =
71                                elem.borrow().geometry_props.as_ref().unwrap().height.clone();
72                            let p_height =
73                                parent.borrow().geometry_props.as_ref().unwrap().height.clone();
74                            h100 |= make_default_100(&e_height, &p_height);
75                        } else {
76                            make_default_implicit(elem, "height");
77                        }
78                    }
79                    DefaultSizeBinding::ExpandsToParentGeometry => {
80                        if !elem.borrow().child_of_layout {
81                            let (e_width, e_height) = elem
82                                .borrow()
83                                .geometry_props
84                                .as_ref()
85                                .map(|g| (g.width.clone(), g.height.clone()))
86                                .unwrap();
87                            let (p_width, p_height) = parent
88                                .borrow()
89                                .geometry_props
90                                .as_ref()
91                                .map(|g| (g.width.clone(), g.height.clone()))
92                                .unwrap();
93                            w100 |= make_default_100(&e_width, &p_width);
94                            h100 |= make_default_100(&e_height, &p_height);
95                        }
96                    }
97                    // In Slint SC an Image is always the size of its source
98                    // image: setting width or height was rejected when
99                    // resolving, so bind them to the source's dimensions
100                    // instead of the implicit-size machinery below. The
101                    // centering further down still applies, like any element.
102                    #[cfg(feature = "slint-sc")]
103                    DefaultSizeBinding::ImplicitSize if diag.slint_sc => {
104                        if is_image {
105                            bind_size_to_source_image(elem);
106                        }
107                    }
108                    DefaultSizeBinding::ImplicitSize => {
109                        let has_length_property_binding = |elem: &ElementRc, property: &str| {
110                            debug_assert_eq!(
111                                elem.borrow()
112                                    .lookup_property(property, PropertyLookupMode::ComponentLocal)
113                                    .property_type,
114                                Type::LogicalLength
115                            );
116
117                            elem.borrow().is_binding_set(property, true)
118                        };
119
120                        let width_specified = has_length_property_binding(elem, "width");
121                        let height_specified = has_length_property_binding(elem, "height");
122
123                        if !elem.borrow().child_of_layout {
124                            // Add aspect-ratio preserving width or height bindings
125                            if is_image && width_specified && !height_specified {
126                                make_default_aspect_ratio_preserving_binding(
127                                    elem, "height", "width",
128                                )
129                            } else if is_image && height_specified && !width_specified {
130                                make_default_aspect_ratio_preserving_binding(
131                                    elem, "width", "height",
132                                )
133                            } else {
134                                make_default_implicit(elem, "width");
135                                make_default_implicit(elem, "height");
136                            }
137                        } else if is_image {
138                            // If an image is in a layout and has no explicit width or height specified, change the default for image-fit
139                            // to `contain`
140                            if !width_specified || !height_specified {
141                                let image_fit_lookup = elem.borrow().lookup_property(
142                                    "image-fit",
143                                    PropertyLookupMode::ComponentLocal,
144                                );
145
146                                elem.borrow_mut().set_binding_if_not_set(
147                                    image_fit_lookup.resolved_name.into(),
148                                    || {
149                                        Expression::EnumerationValue(
150                                            image_fit_lookup
151                                                .property_type
152                                                .as_enum()
153                                                .clone()
154                                                .try_value_from_string("contain")
155                                                .unwrap(),
156                                        )
157                                    },
158                                );
159                            }
160                        }
161                    }
162                }
163
164                if !elem.borrow().child_of_layout
165                    && !elem.borrow().is_legacy_syntax
166                    && builtin_type.name != "Window"
167                {
168                    if !w100 {
169                        maybe_center_in_parent(elem, parent, "x", "width");
170                    }
171                    if !h100 {
172                        maybe_center_in_parent(elem, parent, "y", "height");
173                    }
174                }
175            }
176
177            Some(elem.clone())
178        },
179    )
180}
181
182/// Generate a layout_info_prop based on the children layouts
183fn gen_layout_info_prop(
184    elem: &ElementRc,
185    diag: &mut BuildDiagnostics,
186    symbol_counters: &SymbolCounters,
187) {
188    if elem.borrow().layout_info_prop.is_some() || elem.borrow().is_flickable_content {
189        return;
190    }
191
192    let child_infos = elem
193        .borrow()
194        .children
195        .iter()
196        .filter(|c| {
197            !c.borrow().is_binding_set("x", false) && !c.borrow().is_binding_set("y", false)
198        })
199        .filter_map(|c| {
200            gen_layout_info_prop(c, diag, symbol_counters);
201            let cb = c.borrow();
202            cb.effective_layout_info_prop(Orientation::Horizontal)
203                .cloned()
204                .zip(cb.effective_layout_info_prop(Orientation::Vertical).cloned())
205                .map(|(h, v)| {
206                    (Some(Expression::PropertyReference(h)), Some(Expression::PropertyReference(v)))
207                })
208                .or_else(|| {
209                    if c.borrow().is_legacy_syntax {
210                        return None;
211                    }
212                    if c.borrow().repeated.is_some() {
213                        // FIXME: we should ideally add runtime code to merge layout info of all elements that are repeated (same as #407)
214                        return None;
215                    }
216                    let explicit_constraints =
217                        LayoutConstraints::new(c, Some((&mut *diag, DiagnosticLevel::Error)));
218
219                    let compute = |orientation| {
220                        if explicit_constraints.has_explicit_restrictions(orientation) {
221                            Some(explicit_layout_info(c, orientation))
222                        } else {
223                            implicit_layout_info_call(
224                                c,
225                                orientation,
226                                BuiltinFilter::SkipNonImplicit,
227                                None,
228                            )
229                        }
230                    };
231                    Some((compute(Orientation::Horizontal), compute(Orientation::Vertical)))
232                        .filter(|(a, b)| a.is_some() || b.is_some())
233                })
234        })
235        .collect::<Vec<_>>();
236
237    if child_infos.is_empty() {
238        return;
239    }
240
241    let li_v = crate::layout::create_new_prop(
242        elem,
243        SmolStr::new_static("layoutinfo-v"),
244        crate::typeregister::layout_info_type().into(),
245    );
246    let li_h = crate::layout::create_new_prop(
247        elem,
248        SmolStr::new_static("layoutinfo-h"),
249        crate::typeregister::layout_info_type().into(),
250    );
251    elem.borrow_mut().layout_info_prop = Some((li_h.clone(), li_v.clone()));
252    let mut expr_h =
253        implicit_layout_info_call(elem, Orientation::Horizontal, BuiltinFilter::All, None).unwrap();
254    let mut expr_v =
255        implicit_layout_info_call(elem, Orientation::Vertical, BuiltinFilter::All, None).unwrap();
256
257    // The redundant-size-constraint diagnostic of a component root is reported by the lower_layouts
258    // pass, so only report here for non-root elements.
259    let is_root = elem
260        .borrow()
261        .enclosing_component
262        .upgrade()
263        .is_some_and(|c| Rc::ptr_eq(elem, &c.root_element));
264    let explicit_constraints =
265        LayoutConstraints::new(elem, (!is_root).then_some((&mut *diag, DiagnosticLevel::Warning)));
266    if !explicit_constraints.fixed_width {
267        merge_explicit_constraints(
268            &mut expr_h,
269            &explicit_constraints,
270            Orientation::Horizontal,
271            symbol_counters,
272        );
273    }
274    if !explicit_constraints.fixed_height {
275        merge_explicit_constraints(
276            &mut expr_v,
277            &explicit_constraints,
278            Orientation::Vertical,
279            symbol_counters,
280        );
281    }
282
283    for child_info in child_infos {
284        if let Some(h) = child_info.0 {
285            expr_h = Expression::BinaryExpression {
286                lhs: Box::new(std::mem::take(&mut expr_h)),
287                rhs: Box::new(h),
288                op: '+',
289                source_location: None,
290            };
291        }
292        if let Some(v) = child_info.1 {
293            expr_v = Expression::BinaryExpression {
294                lhs: Box::new(std::mem::take(&mut expr_v)),
295                rhs: Box::new(v),
296                op: '+',
297                source_location: None,
298            };
299        }
300    }
301
302    let expr_v = BindingExpression::new_with_span(expr_v, elem.borrow().to_source_location());
303    li_v.element().borrow_mut().set_binding(li_v.name().clone(), expr_v);
304    let expr_h = BindingExpression::new_with_span(expr_h, elem.borrow().to_source_location());
305    li_h.element().borrow_mut().set_binding(li_h.name().clone(), expr_h);
306}
307
308fn merge_explicit_constraints(
309    expr: &mut Expression,
310    constraints: &LayoutConstraints,
311    orientation: Orientation,
312    symbol_counters: &SymbolCounters,
313) {
314    if constraints.has_explicit_restrictions(orientation) {
315        let unique_name = symbol_counters.generate_name("layout_info_");
316        let ty = expr.ty();
317        let store = Expression::StoreLocalVariable {
318            name: unique_name.clone(),
319            value: Box::new(std::mem::take(expr)),
320        };
321        let Type::Struct(s) = &ty else { unreachable!() };
322        let mut values = s
323            .fields
324            .keys()
325            .map(|p| {
326                (
327                    p.clone(),
328                    Expression::StructFieldAccess {
329                        base: Expression::ReadLocalVariable {
330                            name: unique_name.clone(),
331                            ty: ty.clone(),
332                        }
333                        .into(),
334                        name: p.clone(),
335                    },
336                )
337            })
338            .collect::<BTreeMap<_, _>>();
339
340        for (nr, s) in constraints.for_each_restrictions(orientation) {
341            let e = nr
342                .element()
343                .borrow()
344                .binding(nr.name())
345                .expect("constraint must have binding")
346                .expression
347                .clone();
348            debug_assert!(!matches!(e, Expression::Invalid));
349            values.insert(s.into(), e);
350        }
351        *expr = Expression::CodeBlock([store, Expression::Struct { ty: s.clone(), values }].into());
352    }
353}
354
355fn explicit_layout_info(e: &ElementRc, orientation: Orientation) -> Expression {
356    let mut values = BTreeMap::new();
357    let (size, orient) = match orientation {
358        Orientation::Horizontal => ("width", "horizontal"),
359        Orientation::Vertical => ("height", "vertical"),
360    };
361    for (k, v) in [
362        ("min", format_smolstr!("min-{size}")),
363        ("max", format_smolstr!("max-{size}")),
364        ("preferred", format_smolstr!("preferred-{size}")),
365        ("stretch", format_smolstr!("{orient}-stretch")),
366    ] {
367        values.insert(k.into(), Expression::PropertyReference(NamedReference::new(e, v)));
368    }
369    values.insert("min_percent".into(), Expression::NumberLiteral(0., Unit::None));
370    values.insert("max_percent".into(), Expression::NumberLiteral(100., Unit::None));
371    Expression::Struct { ty: crate::typeregister::layout_info_type(), values }
372}
373
374/// Replace expression such as  `"width: 30%;` with `width: 0.3 * parent.width;`
375///
376/// Returns true if the expression was 100%
377fn fix_percent_size(
378    elem: &ElementRc,
379    parent: &Option<ElementRc>,
380    property: &'static str,
381    diag: &mut BuildDiagnostics,
382    symbol_counters: &SymbolCounters,
383) -> bool {
384    fn inner(
385        expression: &mut Expression,
386        span: &Option<SourceLocation>,
387        parent: &Option<ElementRc>,
388        property: &'static str,
389        diag: &mut BuildDiagnostics,
390        symbol_counters: &SymbolCounters,
391    ) -> bool {
392        if let Expression::DebugHook { expression, .. } = expression {
393            // If the Condition is inside a DebugHook we still need to visit it to fix the
394            // percentages, but ignore the result because the debug hook may override the
395            // expression.
396            inner(expression, span, parent, property, diag, symbol_counters);
397
398            false
399        } else if let Expression::Condition { true_expr, false_expr, .. } = expression
400            && true_expr.ty() != false_expr.ty()
401        {
402            // The lower_states pass can generate a Condition with mixed types of percents and
403            // lengths. The conversion of percents to lengths here can't happen before states
404            // lowering because it depends on inlining and states before lowering can't easily be
405            // inlined because they effectively form a single enum per component.
406            inner(true_expr, span, parent, property, diag, symbol_counters)
407                && inner(false_expr, span, parent, property, diag, symbol_counters)
408        } else {
409            if expression.ty() != Type::Percent {
410                let Some(parent) = parent.as_ref() else { return false };
411                // Pattern match to check it was already parent.<property>
412                //
413                // Note: do not ignore debug hooks here, the debug hook may overwrite the
414                // expression so it may not fill after all.
415                return matches!(
416                    expression,
417                    Expression::PropertyReference(nr)
418                        if *nr.name() == property && Rc::ptr_eq(&nr.element(), parent),
419                );
420            }
421            if let Some(mut parent) = parent.clone() {
422                let flickable = parent.borrow().is_flickable_content;
423                if parent.borrow().is_flickable_content {
424                    // the `%` in a flickable need to refer to the size of the flickable, not
425                    // the size of the content element
426                    parent = crate::object_tree::find_parent_element(&parent).unwrap_or(parent)
427                }
428                debug_assert_eq!(
429                    parent
430                        .borrow()
431                        .lookup_property(property, PropertyLookupMode::ComponentLocal)
432                        .property_type,
433                    Type::LogicalLength
434                );
435                // do not ignore debug hooks here, the debug hook may overwrite the expression
436                // so it may not fill after all.
437                let fill = matches!(
438                    *expression,
439                    Expression::NumberLiteral(x, _) if (x - 100.).abs() < 0.001,
440                );
441
442                *expression = Expression::BinaryExpression {
443                    lhs: Box::new(std::mem::take(expression).maybe_convert_to(
444                        Type::Float32,
445                        span,
446                        diag,
447                        symbol_counters,
448                    )),
449                    rhs: Box::new(Expression::PropertyReference(NamedReference::new(
450                        &parent,
451                        SmolStr::new_static(property),
452                    ))),
453                    op: '*',
454                    source_location: None,
455                };
456
457                // 100% of the outer size of the flickable does not mean the flickable is
458                // filled. We don't want to trigger the elimination of centering in this case.
459                fill && !flickable
460            } else {
461                diag.push_error(
462                    "Cannot find parent property to apply relative length".into(),
463                    span,
464                );
465                false
466            }
467        }
468    }
469
470    let elem = elem.borrow();
471    let Some(mut binding) = elem.binding_mut(property) else {
472        return false;
473    };
474
475    let binding = &mut *binding;
476
477    inner(&mut binding.expression, &binding.span, parent, property, diag, symbol_counters)
478}
479
480/// Generate a size property that covers the parent.
481/// Return true if it was changed
482fn make_default_100(prop: &NamedReference, parent_prop: &NamedReference) -> bool {
483    prop.element().borrow_mut().set_binding_if_not_set(prop.name().clone(), || {
484        Expression::PropertyReference(parent_prop.clone())
485    })
486}
487
488/// Bind the width and height of an Image element to the dimensions of its
489/// `source` image, for Slint SC.
490#[cfg(feature = "slint-sc")]
491fn bind_size_to_source_image(elem: &ElementRc) {
492    let source = NamedReference::new(elem, SmolStr::new_static("source"));
493    for prop in ["width", "height"] {
494        let size_field = Expression::Cast {
495            from: Box::new(Expression::StructFieldAccess {
496                base: Box::new(Expression::FunctionCall {
497                    function: BuiltinFunction::ImageSize.into(),
498                    arguments: vec![Expression::PropertyReference(source.clone())],
499                    source_location: None,
500                }),
501                name: prop.into(),
502            }),
503            to: Type::LogicalLength,
504        };
505        elem.borrow_mut().set_binding_if_not_set(prop.into(), || size_field);
506    }
507}
508
509fn make_default_implicit(elem: &ElementRc, property: &str) {
510    let e = crate::builtin_macros::min_max_expression(
511        Expression::PropertyReference(NamedReference::new(
512            elem,
513            format_smolstr!("preferred-{}", property),
514        )),
515        Expression::PropertyReference(NamedReference::new(
516            elem,
517            format_smolstr!("min-{}", property),
518        )),
519        MinMaxOp::Max,
520    );
521    elem.borrow_mut().set_binding_if_not_set(property.into(), || e);
522}
523
524// For an element with `width`, `height`, `preferred-width` and `preferred-height`, make an aspect
525// ratio preserving binding. This is currently only called for Image elements. For example when for an
526// image the `width` is specified and there is no `height` binding, it is called with `missing_size_property = height`
527// and `given_size_property = width` and install a binding like this:
528//
529//    height: self.width * self.preferred_height / self.preferred_width;
530//
531fn make_default_aspect_ratio_preserving_binding(
532    elem: &ElementRc,
533    missing_size_property: &'static str,
534    given_size_property: &'static str,
535) {
536    if elem.borrow().is_binding_set(missing_size_property, false) {
537        return;
538    }
539
540    debug_assert_eq!(
541        elem.borrow().lookup_property("source", PropertyLookupMode::ComponentLocal).property_type,
542        Type::Image
543    );
544
545    let missing_size_property = SmolStr::new_static(missing_size_property);
546    let given_size_property = SmolStr::new_static(given_size_property);
547
548    let ratio = if elem.borrow().is_binding_set("source-clip-height", false) {
549        Expression::BinaryExpression {
550            lhs: Box::new(Expression::PropertyReference(NamedReference::new(
551                elem,
552                format_smolstr!("source-clip-{missing_size_property}"),
553            ))),
554            rhs: Box::new(Expression::PropertyReference(NamedReference::new(
555                elem,
556                format_smolstr!("source-clip-{given_size_property}"),
557            ))),
558            op: '/',
559            source_location: None,
560        }
561    } else {
562        let implicit_size_var = Box::new(Expression::ReadLocalVariable {
563            name: "image_implicit_size".into(),
564            ty: BuiltinFunction::ImageSize.ty().return_type.clone(),
565        });
566
567        Expression::CodeBlock(vec![
568            Expression::StoreLocalVariable {
569                name: "image_implicit_size".into(),
570                value: Box::new(Expression::FunctionCall {
571                    function: BuiltinFunction::ImageSize.into(),
572                    arguments: vec![Expression::PropertyReference(NamedReference::new(
573                        elem,
574                        SmolStr::new_static("source"),
575                    ))],
576                    source_location: None,
577                }),
578            },
579            Expression::BinaryExpression {
580                lhs: Box::new(Expression::StructFieldAccess {
581                    base: implicit_size_var.clone(),
582                    name: missing_size_property.clone(),
583                }),
584                rhs: Box::new(Expression::StructFieldAccess {
585                    base: implicit_size_var,
586                    name: given_size_property.clone(),
587                }),
588                op: '/',
589                source_location: None,
590            },
591        ])
592    };
593    let binding = Expression::BinaryExpression {
594        lhs: Box::new(ratio),
595        rhs: Expression::PropertyReference(NamedReference::new(elem, given_size_property)).into(),
596        op: '*',
597        source_location: None,
598    };
599
600    let binding_expr = binding;
601    elem.borrow_mut().set_binding_if_not_set(missing_size_property, || binding_expr);
602}
603
604fn maybe_center_in_parent(
605    elem: &ElementRc,
606    parent: &ElementRc,
607    pos_prop: &'static str,
608    size_prop: &'static str,
609) {
610    if elem.borrow().is_binding_set(pos_prop, false) {
611        return;
612    }
613
614    let size_prop = SmolStr::new_static(size_prop);
615    let diff = Expression::BinaryExpression {
616        lhs: Expression::PropertyReference(NamedReference::new(parent, size_prop.clone())).into(),
617        op: '-',
618        rhs: Expression::PropertyReference(NamedReference::new(elem, size_prop)).into(),
619        source_location: None,
620    };
621
622    let pos_prop = SmolStr::new_static(pos_prop);
623    elem.borrow_mut().set_binding_if_not_set(pos_prop, || Expression::BinaryExpression {
624        lhs: diff.into(),
625        op: '/',
626        rhs: Expression::NumberLiteral(2., Unit::None).into(),
627        source_location: None,
628    });
629}
630
631fn adjust_image_clip_rect(elem: &ElementRc, builtin: &Rc<BuiltinElement>) {
632    debug_assert_eq!(builtin.native_class.class_name, "ClippedImage");
633
634    if builtin.native_class.properties.keys().any(|p| {
635        // Deliberately count synthetic debug hooks here (via binding_cell_including_synthetic): they also count as
636        // "used" in resolve_native_classes, so the ClippedImage native class gets selected —
637        // and a ClippedImage without the synthesized clip defaults renders/measures as a
638        // zero-size clip. This condition must match the class-selection semantics.
639        elem.borrow().binding_cell_including_synthetic(p).is_some()
640            || elem.borrow().property_analysis.borrow().get(p).is_some_and(|a| a.is_used())
641    }) {
642        let source = NamedReference::new(elem, SmolStr::new_static("source"));
643        let x = NamedReference::new(elem, SmolStr::new_static("source-clip-x"));
644        let y = NamedReference::new(elem, SmolStr::new_static("source-clip-y"));
645        let make_expr = |dim: &str, prop: NamedReference| Expression::BinaryExpression {
646            source_location: None,
647            lhs: Box::new(Expression::StructFieldAccess {
648                base: Box::new(Expression::FunctionCall {
649                    function: BuiltinFunction::ImageSize.into(),
650                    arguments: vec![Expression::PropertyReference(source.clone())],
651                    source_location: None,
652                }),
653                name: dim.into(),
654            }),
655            rhs: Expression::PropertyReference(prop).into(),
656            op: '-',
657        };
658
659        elem.borrow_mut()
660            .set_binding_if_not_set("source-clip-width".into(), || make_expr("width", x));
661        elem.borrow_mut()
662            .set_binding_if_not_set("source-clip-height".into(), || make_expr("height", y));
663    }
664}
665
666#[test]
667fn test_no_property_for_100pc() {
668    //! Test that we don't generate x or y property to center elements if the size is filling the parent
669    let mut compiler_config =
670        crate::CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
671    compiler_config.style = Some("fluent".into());
672    let mut test_diags = crate::diagnostics::BuildDiagnostics::default();
673    let doc_node = crate::parser::parse(
674        r#"
675        export component Foo inherits Window {
676            r1 := Rectangle {
677                r2 := Rectangle {
678                    width: 100%;
679                    background: blue;
680                }
681                r3 := Rectangle {
682                    height: parent.height;
683                    width: 50%;
684                    background: red;
685                }
686            }
687
688            out property <length> r2x: r2.x;
689            out property <length> r2y: r2.y;
690            out property <length> r3x: r3.x;
691            out property <length> r3y: r3.y;
692        }
693"#
694        .into(),
695        Some(std::path::Path::new("HELLO")),
696        &mut test_diags,
697    );
698    let (doc, diag, _) =
699        spin_on::spin_on(crate::compile_syntax_node(doc_node, test_diags, compiler_config));
700    assert!(!diag.has_errors(), "{:?}", diag.to_string_vec());
701
702    let root_elem = doc.inner_components.last().unwrap().root_element.borrow();
703
704    // const propagation must have seen that the x and y property are literal 0
705    assert!(matches!(
706        root_elem.binding("r2x").unwrap().value_expression(),
707        Expression::NumberLiteral(v, _) if *v == 0.
708    ));
709    assert!(matches!(
710        root_elem.binding("r2y").unwrap().value_expression(),
711        Expression::NumberLiteral(v, _) if *v == 0.
712    ));
713    assert!(matches!(
714        root_elem.binding("r3y").unwrap().value_expression(),
715        Expression::NumberLiteral(v, _) if *v == 0.
716    ));
717    // this one is 50% so it should be set to be in the center
718    assert!(!matches!(
719        root_elem.binding("r3x").unwrap().value_expression(),
720        Expression::BinaryExpression { .. }
721    ));
722}