Skip to main content

i_slint_compiler/
layout.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//! Datastructures used to represent layouts in the compiler
5
6use crate::diagnostics::{BuildDiagnostics, DiagnosticLevel, Spanned};
7use crate::expression_tree::*;
8use crate::langtype::{ElementType, PropertyLookupResult, Type};
9use crate::object_tree::{Component, ElementRc};
10
11use smol_str::{SmolStr, ToSmolStr, format_smolstr};
12
13use std::cell::RefCell;
14use std::rc::{Rc, Weak};
15
16#[derive(Clone, Debug, Copy, Eq, PartialEq)]
17pub enum Orientation {
18    Horizontal,
19    Vertical,
20}
21
22#[derive(Clone, Debug, derive_more::From)]
23pub enum Layout {
24    GridLayout(GridLayout),
25    BoxLayout(BoxLayout),
26}
27
28impl Layout {
29    pub fn rect(&self) -> &LayoutRect {
30        match self {
31            Layout::GridLayout(g) => &g.geometry.rect,
32            Layout::BoxLayout(g) => &g.geometry.rect,
33        }
34    }
35    pub fn rect_mut(&mut self) -> &mut LayoutRect {
36        match self {
37            Layout::GridLayout(g) => &mut g.geometry.rect,
38            Layout::BoxLayout(g) => &mut g.geometry.rect,
39        }
40    }
41    pub fn geometry(&self) -> &LayoutGeometry {
42        match self {
43            Layout::GridLayout(l) => &l.geometry,
44            Layout::BoxLayout(l) => &l.geometry,
45        }
46    }
47}
48
49impl Layout {
50    /// Call the visitor for each NamedReference stored in the layout
51    pub fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
52        match self {
53            Layout::GridLayout(grid) => grid.visit_named_references(visitor),
54            Layout::BoxLayout(l) => l.visit_named_references(visitor),
55        }
56    }
57}
58
59/// An Item in the layout tree
60#[derive(Debug, Default, Clone)]
61pub struct LayoutItem {
62    pub element: ElementRc,
63    pub constraints: LayoutConstraints,
64}
65
66impl LayoutItem {
67    pub fn rect(&self) -> LayoutRect {
68        let p = |unresolved_name: &str| {
69            let PropertyLookupResult { resolved_name, property_type, .. } =
70                self.element.borrow().lookup_property(unresolved_name);
71            if property_type == Type::LogicalLength {
72                Some(NamedReference::new(&self.element, resolved_name.to_smolstr()))
73            } else {
74                None
75            }
76        };
77        LayoutRect {
78            x_reference: p("x"),
79            y_reference: p("y"),
80            width_reference: if !self.constraints.fixed_width { p("width") } else { None },
81            height_reference: if !self.constraints.fixed_height { p("height") } else { None },
82        }
83    }
84}
85
86#[derive(Debug, Clone, Default)]
87pub struct LayoutRect {
88    pub width_reference: Option<NamedReference>,
89    pub height_reference: Option<NamedReference>,
90    pub x_reference: Option<NamedReference>,
91    pub y_reference: Option<NamedReference>,
92}
93
94impl LayoutRect {
95    pub fn install_on_element(element: &ElementRc) -> Self {
96        let install_prop =
97            |name: &'static str| Some(NamedReference::new(element, SmolStr::new_static(name)));
98
99        Self {
100            x_reference: install_prop("x"),
101            y_reference: install_prop("y"),
102            width_reference: install_prop("width"),
103            height_reference: install_prop("height"),
104        }
105    }
106
107    fn visit_named_references(&mut self, mut visitor: &mut impl FnMut(&mut NamedReference)) {
108        self.width_reference.as_mut().map(&mut visitor);
109        self.height_reference.as_mut().map(&mut visitor);
110        self.x_reference.as_mut().map(&mut visitor);
111        self.y_reference.as_mut().map(&mut visitor);
112    }
113
114    pub fn size_reference(&self, orientation: Orientation) -> Option<&NamedReference> {
115        match orientation {
116            Orientation::Horizontal => self.width_reference.as_ref(),
117            Orientation::Vertical => self.height_reference.as_ref(),
118        }
119    }
120}
121
122#[derive(Debug, Default, Clone)]
123pub struct LayoutConstraints {
124    pub min_width: Option<NamedReference>,
125    pub max_width: Option<NamedReference>,
126    pub min_height: Option<NamedReference>,
127    pub max_height: Option<NamedReference>,
128    pub preferred_width: Option<NamedReference>,
129    pub preferred_height: Option<NamedReference>,
130    pub horizontal_stretch: Option<NamedReference>,
131    pub vertical_stretch: Option<NamedReference>,
132    pub fixed_width: bool,
133    pub fixed_height: bool,
134}
135
136impl LayoutConstraints {
137    /// Build the constraints for the given element
138    ///
139    /// Report diagnostics when both constraints and fixed size are set
140    /// (one can set the level to warning to keep compatibility to old version of Slint)
141    pub fn new(element: &ElementRc, diag: &mut BuildDiagnostics, level: DiagnosticLevel) -> Self {
142        let mut constraints = Self {
143            min_width: binding_reference(element, "min-width"),
144            max_width: binding_reference(element, "max-width"),
145            min_height: binding_reference(element, "min-height"),
146            max_height: binding_reference(element, "max-height"),
147            preferred_width: binding_reference(element, "preferred-width"),
148            preferred_height: binding_reference(element, "preferred-height"),
149            horizontal_stretch: binding_reference(element, "horizontal-stretch"),
150            vertical_stretch: binding_reference(element, "vertical-stretch"),
151            fixed_width: false,
152            fixed_height: false,
153        };
154        let mut apply_size_constraint =
155            |prop: &'static str,
156             binding: &BindingExpression,
157             enclosing1: &Weak<Component>,
158             depth,
159             op: &mut Option<NamedReference>| {
160                if let Some(other_prop) = op {
161                    find_binding(
162                        &other_prop.element(),
163                        other_prop.name(),
164                        |old, enclosing2, d2| {
165                            if Weak::ptr_eq(enclosing1, enclosing2)
166                                && old.priority.saturating_add(d2)
167                                    <= binding.priority.saturating_add(depth)
168                            {
169                                diag.push_diagnostic_with_span(
170                                    format!(
171                                        "Cannot specify both '{prop}' and '{}'",
172                                        other_prop.name()
173                                    ),
174                                    binding.to_source_location(),
175                                    level,
176                                );
177                            }
178                        },
179                    );
180                }
181                *op = Some(NamedReference::new(element, SmolStr::new_static(prop)))
182            };
183        find_binding(element, "height", |s, enclosing, depth| {
184            constraints.fixed_height = true;
185            apply_size_constraint("height", s, enclosing, depth, &mut constraints.min_height);
186            apply_size_constraint("height", s, enclosing, depth, &mut constraints.max_height);
187        });
188        find_binding(element, "width", |s, enclosing, depth| {
189            constraints.fixed_width = true;
190            if s.expression.ty() == Type::Percent {
191                apply_size_constraint("width", s, enclosing, depth, &mut constraints.min_width);
192            } else {
193                apply_size_constraint("width", s, enclosing, depth, &mut constraints.min_width);
194                apply_size_constraint("width", s, enclosing, depth, &mut constraints.max_width);
195            }
196        });
197
198        constraints
199    }
200
201    pub fn has_explicit_restrictions(&self, orientation: Orientation) -> bool {
202        match orientation {
203            Orientation::Horizontal => {
204                self.min_width.is_some()
205                    || self.max_width.is_some()
206                    || self.preferred_width.is_some()
207                    || self.horizontal_stretch.is_some()
208            }
209            Orientation::Vertical => {
210                self.min_height.is_some()
211                    || self.max_height.is_some()
212                    || self.preferred_height.is_some()
213                    || self.vertical_stretch.is_some()
214            }
215        }
216    }
217
218    // Iterate over the constraint with a reference to a property, and the corresponding member in the i_slint_core::layout::LayoutInfo struct
219    pub fn for_each_restrictions(
220        &self,
221        orientation: Orientation,
222    ) -> impl Iterator<Item = (&NamedReference, &'static str)> {
223        let (min, max, preferred, stretch) = match orientation {
224            Orientation::Horizontal => {
225                (&self.min_width, &self.max_width, &self.preferred_width, &self.horizontal_stretch)
226            }
227            Orientation::Vertical => {
228                (&self.min_height, &self.max_height, &self.preferred_height, &self.vertical_stretch)
229            }
230        };
231        std::iter::empty()
232            .chain(min.as_ref().map(|x| {
233                if Expression::PropertyReference(x.clone()).ty() != Type::Percent {
234                    (x, "min")
235                } else {
236                    (x, "min_percent")
237                }
238            }))
239            .chain(max.as_ref().map(|x| {
240                if Expression::PropertyReference(x.clone()).ty() != Type::Percent {
241                    (x, "max")
242                } else {
243                    (x, "max_percent")
244                }
245            }))
246            .chain(preferred.as_ref().map(|x| (x, "preferred")))
247            .chain(stretch.as_ref().map(|x| (x, "stretch")))
248    }
249
250    pub fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
251        if let Some(e) = self.max_width.as_mut() {
252            visitor(&mut *e);
253        }
254        if let Some(e) = self.min_width.as_mut() {
255            visitor(&mut *e);
256        }
257        if let Some(e) = self.max_height.as_mut() {
258            visitor(&mut *e);
259        }
260        if let Some(e) = self.min_height.as_mut() {
261            visitor(&mut *e);
262        }
263        if let Some(e) = self.preferred_width.as_mut() {
264            visitor(&mut *e);
265        }
266        if let Some(e) = self.preferred_height.as_mut() {
267            visitor(&mut *e);
268        }
269        if let Some(e) = self.horizontal_stretch.as_mut() {
270            visitor(&mut *e);
271        }
272        if let Some(e) = self.vertical_stretch.as_mut() {
273            visitor(&mut *e);
274        }
275    }
276}
277
278#[derive(Debug, Clone)]
279pub enum RowColExpr {
280    Named(NamedReference),
281    Literal(u16),
282    Auto,
283}
284
285#[derive(Debug, Clone)]
286pub struct GridLayoutCell {
287    pub new_row: bool,
288    pub col_expr: RowColExpr,
289    pub row_expr: RowColExpr,
290    pub colspan_expr: RowColExpr,
291    pub rowspan_expr: RowColExpr,
292    pub child_items: Option<Vec<LayoutItem>>, // for repeated rows
293}
294
295impl GridLayoutCell {
296    pub fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
297        if let RowColExpr::Named(ref mut e) = self.col_expr {
298            visitor(e);
299        }
300        if let RowColExpr::Named(ref mut e) = self.row_expr {
301            visitor(e);
302        }
303        if let RowColExpr::Named(ref mut e) = self.colspan_expr {
304            visitor(e);
305        }
306        if let RowColExpr::Named(ref mut e) = self.rowspan_expr {
307            visitor(e);
308        }
309        if let Some(children) = &mut self.child_items {
310            for child in children {
311                child.constraints.visit_named_references(visitor);
312            }
313        }
314    }
315}
316
317/// An element in a GridLayout
318#[derive(Debug, Clone)]
319pub struct GridLayoutElement {
320    /// `Rc<RefCell<GridLayoutCell>>` because shared with the repeated component's element
321    pub cell: Rc<RefCell<GridLayoutCell>>,
322    pub item: LayoutItem,
323}
324
325impl GridLayoutElement {
326    pub fn span(&self, orientation: Orientation) -> RowColExpr {
327        let cell = self.cell.borrow();
328        match orientation {
329            Orientation::Horizontal => cell.colspan_expr.clone(),
330            Orientation::Vertical => cell.rowspan_expr.clone(),
331        }
332    }
333}
334
335#[derive(Debug, Clone)]
336pub struct Padding {
337    pub left: Option<NamedReference>,
338    pub right: Option<NamedReference>,
339    pub top: Option<NamedReference>,
340    pub bottom: Option<NamedReference>,
341}
342
343impl Padding {
344    fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
345        if let Some(e) = self.left.as_mut() {
346            visitor(&mut *e)
347        }
348        if let Some(e) = self.right.as_mut() {
349            visitor(&mut *e)
350        }
351        if let Some(e) = self.top.as_mut() {
352            visitor(&mut *e)
353        }
354        if let Some(e) = self.bottom.as_mut() {
355            visitor(&mut *e)
356        }
357    }
358
359    // Return reference to the begin and end padding for a given orientation
360    pub fn begin_end(&self, o: Orientation) -> (Option<&NamedReference>, Option<&NamedReference>) {
361        match o {
362            Orientation::Horizontal => (self.left.as_ref(), self.right.as_ref()),
363            Orientation::Vertical => (self.top.as_ref(), self.bottom.as_ref()),
364        }
365    }
366}
367
368#[derive(Debug, Clone)]
369pub struct Spacing {
370    pub horizontal: Option<NamedReference>,
371    pub vertical: Option<NamedReference>,
372}
373
374impl Spacing {
375    fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
376        if let Some(e) = self.horizontal.as_mut() {
377            visitor(&mut *e);
378        }
379        if let Some(e) = self.vertical.as_mut() {
380            visitor(&mut *e);
381        }
382    }
383
384    pub fn orientation(&self, o: Orientation) -> Option<&NamedReference> {
385        match o {
386            Orientation::Horizontal => self.horizontal.as_ref(),
387            Orientation::Vertical => self.vertical.as_ref(),
388        }
389    }
390}
391
392#[derive(Debug, Clone)]
393pub struct LayoutGeometry {
394    pub rect: LayoutRect,
395    pub spacing: Spacing,
396    pub alignment: Option<NamedReference>,
397    pub padding: Padding,
398}
399
400impl LayoutGeometry {
401    pub fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
402        self.rect.visit_named_references(visitor);
403        if let Some(e) = self.alignment.as_mut() {
404            visitor(&mut *e)
405        }
406        self.spacing.visit_named_references(visitor);
407        self.padding.visit_named_references(visitor);
408    }
409
410    pub fn new(layout_element: &ElementRc) -> Self {
411        let spacing = || binding_reference(layout_element, "spacing");
412        init_fake_property(layout_element, "spacing-horizontal", spacing);
413        init_fake_property(layout_element, "spacing-vertical", spacing);
414
415        let alignment = binding_reference(layout_element, "alignment");
416
417        let padding = || binding_reference(layout_element, "padding");
418        init_fake_property(layout_element, "padding-left", padding);
419        init_fake_property(layout_element, "padding-right", padding);
420        init_fake_property(layout_element, "padding-top", padding);
421        init_fake_property(layout_element, "padding-bottom", padding);
422
423        let padding = Padding {
424            left: binding_reference(layout_element, "padding-left").or_else(padding),
425            right: binding_reference(layout_element, "padding-right").or_else(padding),
426            top: binding_reference(layout_element, "padding-top").or_else(padding),
427            bottom: binding_reference(layout_element, "padding-bottom").or_else(padding),
428        };
429
430        let spacing = Spacing {
431            horizontal: binding_reference(layout_element, "spacing-horizontal").or_else(spacing),
432            vertical: binding_reference(layout_element, "spacing-vertical").or_else(spacing),
433        };
434
435        let rect = LayoutRect::install_on_element(layout_element);
436
437        Self { rect, spacing, padding, alignment }
438    }
439}
440
441/// If this element or any of the parent has a binding to the property, call the functor with that binding, and the depth.
442/// Return None if the binding does not exist in any of the sub component, or Some with the result of the functor otherwise
443fn find_binding<R>(
444    element: &ElementRc,
445    name: &str,
446    f: impl FnOnce(&BindingExpression, &Weak<Component>, i32) -> R,
447) -> Option<R> {
448    let mut element = element.clone();
449    let mut depth = 0;
450    loop {
451        if let Some(b) = element.borrow().bindings.get(name)
452            && b.borrow().has_binding()
453        {
454            return Some(f(&b.borrow(), &element.borrow().enclosing_component, depth));
455        }
456        let e = match &element.borrow().base_type {
457            ElementType::Component(base) => base.root_element.clone(),
458            _ => return None,
459        };
460        element = e;
461        depth += 1;
462    }
463}
464
465/// Return a named reference to a property if a binding is set on that property
466pub fn binding_reference(element: &ElementRc, name: &'static str) -> Option<NamedReference> {
467    find_binding(element, name, |_, _, _| NamedReference::new(element, SmolStr::new_static(name)))
468}
469
470fn init_fake_property(
471    grid_layout_element: &ElementRc,
472    name: &str,
473    lazy_default: impl Fn() -> Option<NamedReference>,
474) {
475    if grid_layout_element.borrow().property_declarations.contains_key(name)
476        && !grid_layout_element.borrow().bindings.contains_key(name)
477        && let Some(e) = lazy_default()
478    {
479        if e.name() == name && Rc::ptr_eq(&e.element(), grid_layout_element) {
480            // Don't reference self
481            return;
482        }
483        grid_layout_element
484            .borrow_mut()
485            .bindings
486            .insert(name.into(), RefCell::new(Expression::PropertyReference(e).into()));
487    }
488}
489
490/// Internal representation of a grid layout
491#[derive(Debug, Clone)]
492pub struct GridLayout {
493    /// All the elements which will be laid out within that element.
494    pub elems: Vec<GridLayoutElement>,
495
496    pub geometry: LayoutGeometry,
497
498    /// When this GridLayout is actually the layout of a Dialog, then the cells start with all the buttons,
499    /// and this variable contains their roles. The string is actually one of the values from the i_slint_core::layout::DialogButtonRole
500    pub dialog_button_roles: Option<Vec<SmolStr>>,
501
502    /// Whether any of the row/column expressions use 'auto'
503    pub uses_auto: bool,
504}
505
506impl GridLayout {
507    pub fn visit_rowcol_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
508        for elem in &mut self.elems {
509            let mut cell = elem.cell.borrow_mut();
510            if let RowColExpr::Named(ref mut e) = cell.col_expr {
511                visitor(e);
512            }
513            if let RowColExpr::Named(ref mut e) = cell.row_expr {
514                visitor(e);
515            }
516            if let RowColExpr::Named(ref mut e) = cell.colspan_expr {
517                visitor(e);
518            }
519            if let RowColExpr::Named(ref mut e) = cell.rowspan_expr {
520                visitor(e);
521            }
522        }
523    }
524
525    pub fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
526        self.visit_rowcol_named_references(visitor);
527        for layout_elem in &mut self.elems {
528            layout_elem.item.constraints.visit_named_references(visitor);
529            if let Some(child_items) = &mut layout_elem.cell.borrow_mut().child_items {
530                for child in child_items {
531                    child.constraints.visit_named_references(visitor);
532                }
533            }
534        }
535        self.geometry.visit_named_references(visitor);
536    }
537}
538
539/// Internal representation of a BoxLayout
540#[derive(Debug, Clone)]
541pub struct BoxLayout {
542    /// Whether this is a HorizontalLayout or a VerticalLayout
543    pub orientation: Orientation,
544    pub elems: Vec<LayoutItem>,
545    pub geometry: LayoutGeometry,
546}
547
548impl BoxLayout {
549    fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
550        for cell in &mut self.elems {
551            cell.constraints.visit_named_references(visitor);
552        }
553        self.geometry.visit_named_references(visitor);
554    }
555}
556
557/// Get the implicit layout info of a particular element
558pub fn implicit_layout_info_call(elem: &ElementRc, orientation: Orientation) -> Expression {
559    let mut elem_it = elem.clone();
560    loop {
561        return match &elem_it.clone().borrow().base_type {
562            ElementType::Component(base_comp) => {
563                match base_comp.root_element.borrow().layout_info_prop(orientation) {
564                    Some(nr) => {
565                        // We cannot take nr as is because it is relative to the elem's component. We therefore need to
566                        // use `elem` as an element for the PropertyReference, not `root` within the base of elem
567                        debug_assert!(Rc::ptr_eq(&nr.element(), &base_comp.root_element));
568                        Expression::PropertyReference(NamedReference::new(elem, nr.name().clone()))
569                    }
570                    None => {
571                        elem_it = base_comp.root_element.clone();
572                        continue;
573                    }
574                }
575            }
576            ElementType::Builtin(base_type)
577                if matches!(
578                    base_type.name.as_str(),
579                    "Rectangle"
580                        | "Empty"
581                        | "TouchArea"
582                        | "FocusScope"
583                        | "Opacity"
584                        | "Layer"
585                        | "BoxShadow"
586                        | "Clip"
587                ) =>
588            {
589                // hard-code the value for rectangle because many rectangle end up optimized away and we
590                // don't want to depend on the element.
591                Expression::Struct {
592                    ty: crate::typeregister::layout_info_type(),
593                    values: [("min", 0.), ("max", f32::MAX), ("preferred", 0.)]
594                        .iter()
595                        .map(|(s, v)| {
596                            (SmolStr::new_static(s), Expression::NumberLiteral(*v as _, Unit::Px))
597                        })
598                        .chain(
599                            [("min_percent", 0.), ("max_percent", 100.), ("stretch", 1.)]
600                                .iter()
601                                .map(|(s, v)| {
602                                    (
603                                        SmolStr::new_static(s),
604                                        Expression::NumberLiteral(*v, Unit::None),
605                                    )
606                                }),
607                        )
608                        .collect(),
609                }
610            }
611            _ => Expression::FunctionCall {
612                function: BuiltinFunction::ImplicitLayoutInfo(orientation).into(),
613                arguments: vec![Expression::ElementReference(Rc::downgrade(elem))],
614                source_location: None,
615            },
616        };
617    }
618}
619
620/// Create a new property based on the name. (it might get a different name if that property exist)
621pub fn create_new_prop(elem: &ElementRc, tentative_name: SmolStr, ty: Type) -> NamedReference {
622    let mut e = elem.borrow_mut();
623    if !e.lookup_property(&tentative_name).is_valid() {
624        e.property_declarations.insert(tentative_name.clone(), ty.into());
625        drop(e);
626        NamedReference::new(elem, tentative_name)
627    } else {
628        let mut counter = 0;
629        loop {
630            counter += 1;
631            let name = format_smolstr!("{}{}", tentative_name, counter);
632            if !e.lookup_property(&name).is_valid() {
633                e.property_declarations.insert(name.clone(), ty.into());
634                drop(e);
635                return NamedReference::new(elem, name);
636            }
637        }
638    }
639}
640
641/// Return true if this type is a layout that has constraints
642pub fn is_layout(base_type: &ElementType) -> bool {
643    match base_type {
644        ElementType::Component(c) => is_layout(&c.root_element.borrow().base_type),
645        ElementType::Builtin(be) => {
646            matches!(be.name.as_str(), "GridLayout" | "HorizontalLayout" | "VerticalLayout")
647        }
648        _ => false,
649    }
650}