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::{format_smolstr, SmolStr, ToSmolStr};
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/// An element in a GridLayout
279#[derive(Debug, Clone)]
280pub struct GridLayoutElement {
281    pub col: u16,
282    pub row: u16,
283    pub colspan: u16,
284    pub rowspan: u16,
285    pub item: LayoutItem,
286}
287
288impl GridLayoutElement {
289    pub fn col_or_row_and_span(&self, orientation: Orientation) -> (u16, u16) {
290        match orientation {
291            Orientation::Horizontal => (self.col, self.colspan),
292            Orientation::Vertical => (self.row, self.rowspan),
293        }
294    }
295}
296
297#[derive(Debug, Clone)]
298pub struct Padding {
299    pub left: Option<NamedReference>,
300    pub right: Option<NamedReference>,
301    pub top: Option<NamedReference>,
302    pub bottom: Option<NamedReference>,
303}
304
305impl Padding {
306    fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
307        if let Some(e) = self.left.as_mut() {
308            visitor(&mut *e)
309        }
310        if let Some(e) = self.right.as_mut() {
311            visitor(&mut *e)
312        }
313        if let Some(e) = self.top.as_mut() {
314            visitor(&mut *e)
315        }
316        if let Some(e) = self.bottom.as_mut() {
317            visitor(&mut *e)
318        }
319    }
320
321    // Return reference to the begin and end padding for a given orientation
322    pub fn begin_end(&self, o: Orientation) -> (Option<&NamedReference>, Option<&NamedReference>) {
323        match o {
324            Orientation::Horizontal => (self.left.as_ref(), self.right.as_ref()),
325            Orientation::Vertical => (self.top.as_ref(), self.bottom.as_ref()),
326        }
327    }
328}
329
330#[derive(Debug, Clone)]
331pub struct Spacing {
332    pub horizontal: Option<NamedReference>,
333    pub vertical: Option<NamedReference>,
334}
335
336impl Spacing {
337    fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
338        if let Some(e) = self.horizontal.as_mut() {
339            visitor(&mut *e);
340        }
341        if let Some(e) = self.vertical.as_mut() {
342            visitor(&mut *e);
343        }
344    }
345
346    pub fn orientation(&self, o: Orientation) -> Option<&NamedReference> {
347        match o {
348            Orientation::Horizontal => self.horizontal.as_ref(),
349            Orientation::Vertical => self.vertical.as_ref(),
350        }
351    }
352}
353
354#[derive(Debug, Clone)]
355pub struct LayoutGeometry {
356    pub rect: LayoutRect,
357    pub spacing: Spacing,
358    pub alignment: Option<NamedReference>,
359    pub padding: Padding,
360}
361
362impl LayoutGeometry {
363    pub fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
364        self.rect.visit_named_references(visitor);
365        if let Some(e) = self.alignment.as_mut() {
366            visitor(&mut *e)
367        }
368        self.spacing.visit_named_references(visitor);
369        self.padding.visit_named_references(visitor);
370    }
371
372    pub fn new(layout_element: &ElementRc) -> Self {
373        let spacing = || binding_reference(layout_element, "spacing");
374        init_fake_property(layout_element, "spacing-horizontal", spacing);
375        init_fake_property(layout_element, "spacing-vertical", spacing);
376
377        let alignment = binding_reference(layout_element, "alignment");
378
379        let padding = || binding_reference(layout_element, "padding");
380        init_fake_property(layout_element, "padding-left", padding);
381        init_fake_property(layout_element, "padding-right", padding);
382        init_fake_property(layout_element, "padding-top", padding);
383        init_fake_property(layout_element, "padding-bottom", padding);
384
385        let padding = Padding {
386            left: binding_reference(layout_element, "padding-left").or_else(padding),
387            right: binding_reference(layout_element, "padding-right").or_else(padding),
388            top: binding_reference(layout_element, "padding-top").or_else(padding),
389            bottom: binding_reference(layout_element, "padding-bottom").or_else(padding),
390        };
391
392        let spacing = Spacing {
393            horizontal: binding_reference(layout_element, "spacing-horizontal").or_else(spacing),
394            vertical: binding_reference(layout_element, "spacing-vertical").or_else(spacing),
395        };
396
397        let rect = LayoutRect::install_on_element(layout_element);
398
399        Self { rect, spacing, padding, alignment }
400    }
401}
402
403/// If this element or any of the parent has a binding to the property, call the functor with that binding, and the depth.
404/// Return None if the binding does not exist in any of the sub component, or Some with the result of the functor otherwise
405fn find_binding<R>(
406    element: &ElementRc,
407    name: &str,
408    f: impl FnOnce(&BindingExpression, &Weak<Component>, i32) -> R,
409) -> Option<R> {
410    let mut element = element.clone();
411    let mut depth = 0;
412    loop {
413        if let Some(b) = element.borrow().bindings.get(name) {
414            if b.borrow().has_binding() {
415                return Some(f(&b.borrow(), &element.borrow().enclosing_component, depth));
416            }
417        }
418        let e = match &element.borrow().base_type {
419            ElementType::Component(base) => base.root_element.clone(),
420            _ => return None,
421        };
422        element = e;
423        depth += 1;
424    }
425}
426
427/// Return a named reference to a property if a binding is set on that property
428fn binding_reference(element: &ElementRc, name: &'static str) -> Option<NamedReference> {
429    find_binding(element, name, |_, _, _| NamedReference::new(element, SmolStr::new_static(name)))
430}
431
432fn init_fake_property(
433    grid_layout_element: &ElementRc,
434    name: &str,
435    lazy_default: impl Fn() -> Option<NamedReference>,
436) {
437    if grid_layout_element.borrow().property_declarations.contains_key(name)
438        && !grid_layout_element.borrow().bindings.contains_key(name)
439    {
440        if let Some(e) = lazy_default() {
441            if e.name() == name && Rc::ptr_eq(&e.element(), grid_layout_element) {
442                // Don't reference self
443                return;
444            }
445            grid_layout_element
446                .borrow_mut()
447                .bindings
448                .insert(name.into(), RefCell::new(Expression::PropertyReference(e).into()));
449        }
450    }
451}
452
453/// Internal representation of a grid layout
454#[derive(Debug, Clone)]
455pub struct GridLayout {
456    /// All the elements will be layout within that element.
457    pub elems: Vec<GridLayoutElement>,
458
459    pub geometry: LayoutGeometry,
460
461    /// When this GridLayout is actually the layout of a Dialog, then the cells start with all the buttons,
462    /// and this variable contains their roles. The string is actually one of the values from the i_slint_core::layout::DialogButtonRole
463    pub dialog_button_roles: Option<Vec<SmolStr>>,
464}
465
466impl GridLayout {
467    fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
468        for cell in &mut self.elems {
469            cell.item.constraints.visit_named_references(visitor);
470        }
471        self.geometry.visit_named_references(visitor);
472    }
473}
474
475/// Internal representation of a BoxLayout
476#[derive(Debug, Clone)]
477pub struct BoxLayout {
478    /// Whether, this is a HorizontalLayout, otherwise a VerticalLayout
479    pub orientation: Orientation,
480    pub elems: Vec<LayoutItem>,
481    pub geometry: LayoutGeometry,
482}
483
484impl BoxLayout {
485    fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
486        for cell in &mut self.elems {
487            cell.constraints.visit_named_references(visitor);
488        }
489        self.geometry.visit_named_references(visitor);
490    }
491}
492
493/// Get the implicit layout info of a particular element
494pub fn implicit_layout_info_call(elem: &ElementRc, orientation: Orientation) -> Expression {
495    let mut elem_it = elem.clone();
496    loop {
497        return match &elem_it.clone().borrow().base_type {
498            ElementType::Component(base_comp) => {
499                match base_comp.root_element.borrow().layout_info_prop(orientation) {
500                    Some(nr) => {
501                        // We cannot take nr as is because it is relative to the elem's component. We therefore need to
502                        // use `elem` as an element for the PropertyReference, not `root` within the base of elem
503                        debug_assert!(Rc::ptr_eq(&nr.element(), &base_comp.root_element));
504                        Expression::PropertyReference(NamedReference::new(elem, nr.name().clone()))
505                    }
506                    None => {
507                        elem_it = base_comp.root_element.clone();
508                        continue;
509                    }
510                }
511            }
512            ElementType::Builtin(base_type)
513                if matches!(
514                    base_type.name.as_str(),
515                    "Rectangle"
516                        | "Empty"
517                        | "TouchArea"
518                        | "FocusScope"
519                        | "Opacity"
520                        | "Layer"
521                        | "BoxShadow"
522                        | "Clip"
523                ) =>
524            {
525                // hard-code the value for rectangle because many rectangle end up optimized away and we
526                // don't want to depend on the element.
527                Expression::Struct {
528                    ty: crate::typeregister::layout_info_type(),
529                    values: [("min", 0.), ("max", f32::MAX), ("preferred", 0.)]
530                        .iter()
531                        .map(|(s, v)| {
532                            (SmolStr::new_static(s), Expression::NumberLiteral(*v as _, Unit::Px))
533                        })
534                        .chain(
535                            [("min_percent", 0.), ("max_percent", 100.), ("stretch", 1.)]
536                                .iter()
537                                .map(|(s, v)| {
538                                    (
539                                        SmolStr::new_static(s),
540                                        Expression::NumberLiteral(*v, Unit::None),
541                                    )
542                                }),
543                        )
544                        .collect(),
545                }
546            }
547            _ => Expression::FunctionCall {
548                function: BuiltinFunction::ImplicitLayoutInfo(orientation).into(),
549                arguments: vec![Expression::ElementReference(Rc::downgrade(elem))],
550                source_location: None,
551            },
552        };
553    }
554}
555
556/// Create a new property based on the name. (it might get a different name if that property exist)
557pub fn create_new_prop(elem: &ElementRc, tentative_name: SmolStr, ty: Type) -> NamedReference {
558    let mut e = elem.borrow_mut();
559    if !e.lookup_property(&tentative_name).is_valid() {
560        e.property_declarations.insert(tentative_name.clone(), ty.into());
561        drop(e);
562        NamedReference::new(elem, tentative_name)
563    } else {
564        let mut counter = 0;
565        loop {
566            counter += 1;
567            let name = format_smolstr!("{}{}", tentative_name, counter);
568            if !e.lookup_property(&name).is_valid() {
569                e.property_declarations.insert(name.clone(), ty.into());
570                drop(e);
571                return NamedReference::new(elem, name);
572            }
573        }
574    }
575}
576
577/// Return true if this type is a layout that has constraints
578pub fn is_layout(base_type: &ElementType) -> bool {
579    match base_type {
580        ElementType::Component(c) => is_layout(&c.root_element.borrow().base_type),
581        ElementType::Builtin(be) => {
582            matches!(be.name.as_str(), "GridLayout" | "HorizontalLayout" | "VerticalLayout")
583        }
584        _ => false,
585    }
586}