sixtyfps_compilerlib/
layout.rs

1// Copyright © SixtyFPS GmbH <info@sixtyfps.io>
2// SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
3
4//! Datastructures used to represent layouts in the compiler
5
6use crate::diagnostics::BuildDiagnostics;
7use crate::expression_tree::*;
8use crate::langtype::{PropertyLookupResult, Type};
9use crate::object_tree::{Component, ElementRc};
10
11use std::cell::RefCell;
12use std::rc::{Rc, Weak};
13
14#[derive(Clone, Debug, Copy, Eq, PartialEq)]
15pub enum Orientation {
16    Horizontal,
17    Vertical,
18}
19
20#[derive(Clone, Debug, derive_more::From)]
21pub enum Layout {
22    GridLayout(GridLayout),
23    PathLayout(PathLayout),
24    BoxLayout(BoxLayout),
25}
26
27impl Layout {
28    pub fn rect(&self) -> &LayoutRect {
29        match self {
30            Layout::GridLayout(g) => &g.geometry.rect,
31            Layout::BoxLayout(g) => &g.geometry.rect,
32            Layout::PathLayout(p) => &p.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            Layout::PathLayout(p) => &mut p.rect,
40        }
41    }
42    pub fn geometry(&self) -> Option<&LayoutGeometry> {
43        match self {
44            Layout::GridLayout(l) => Some(&l.geometry),
45            Layout::BoxLayout(l) => Some(&l.geometry),
46            Layout::PathLayout(_) => None,
47        }
48    }
49}
50
51impl Layout {
52    /// Call the visitor for each NamedReference stored in the layout
53    pub fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
54        match self {
55            Layout::GridLayout(grid) => grid.visit_named_references(visitor),
56            Layout::BoxLayout(l) => l.visit_named_references(visitor),
57            Layout::PathLayout(path) => path.visit_named_references(visitor),
58        }
59    }
60}
61
62/// An Item in the layout tree
63#[derive(Debug, Default, Clone)]
64pub struct LayoutItem {
65    pub element: ElementRc,
66    pub constraints: LayoutConstraints,
67}
68
69impl LayoutItem {
70    pub fn rect(&self) -> LayoutRect {
71        let p = |unresolved_name: &str| {
72            let PropertyLookupResult { resolved_name, property_type } =
73                self.element.borrow().lookup_property(unresolved_name);
74            if property_type == Type::LogicalLength {
75                Some(NamedReference::new(&self.element, resolved_name.as_ref()))
76            } else {
77                None
78            }
79        };
80        LayoutRect {
81            x_reference: p("x"),
82            y_reference: p("y"),
83            width_reference: if !self.constraints.fixed_width { p("width") } else { None },
84            height_reference: if !self.constraints.fixed_height { p("height") } else { None },
85        }
86    }
87}
88
89#[derive(Debug, Clone, Default)]
90pub struct LayoutRect {
91    pub width_reference: Option<NamedReference>,
92    pub height_reference: Option<NamedReference>,
93    pub x_reference: Option<NamedReference>,
94    pub y_reference: Option<NamedReference>,
95}
96
97impl LayoutRect {
98    pub fn install_on_element(element: &ElementRc) -> Self {
99        let install_prop = |name: &str| Some(NamedReference::new(element, name));
100
101        Self {
102            x_reference: install_prop("x"),
103            y_reference: install_prop("y"),
104            width_reference: install_prop("width"),
105            height_reference: install_prop("height"),
106        }
107    }
108
109    fn visit_named_references(&mut self, mut visitor: &mut impl FnMut(&mut NamedReference)) {
110        self.width_reference.as_mut().map(&mut visitor);
111        self.height_reference.as_mut().map(&mut visitor);
112        self.x_reference.as_mut().map(&mut visitor);
113        self.y_reference.as_mut().map(&mut visitor);
114    }
115
116    pub fn size_reference(&self, orientation: Orientation) -> Option<&NamedReference> {
117        match orientation {
118            Orientation::Horizontal => self.width_reference.as_ref(),
119            Orientation::Vertical => self.height_reference.as_ref(),
120        }
121    }
122}
123
124#[derive(Debug, Default, Clone)]
125pub struct LayoutConstraints {
126    pub min_width: Option<NamedReference>,
127    pub max_width: Option<NamedReference>,
128    pub min_height: Option<NamedReference>,
129    pub max_height: Option<NamedReference>,
130    pub preferred_width: Option<NamedReference>,
131    pub preferred_height: Option<NamedReference>,
132    pub horizontal_stretch: Option<NamedReference>,
133    pub vertical_stretch: Option<NamedReference>,
134    pub fixed_width: bool,
135    pub fixed_height: bool,
136}
137
138impl LayoutConstraints {
139    pub fn new(element: &ElementRc, diag: &mut BuildDiagnostics) -> Self {
140        let mut constraints = Self {
141            min_width: binding_reference(element, "min-width"),
142            max_width: binding_reference(element, "max-width"),
143            min_height: binding_reference(element, "min-height"),
144            max_height: binding_reference(element, "max-height"),
145            preferred_width: binding_reference(element, "preferred-width"),
146            preferred_height: binding_reference(element, "preferred-height"),
147            horizontal_stretch: binding_reference(element, "horizontal-stretch"),
148            vertical_stretch: binding_reference(element, "vertical-stretch"),
149            fixed_width: false,
150            fixed_height: false,
151        };
152        let mut apply_size_constraint =
153            |prop,
154             binding: &BindingExpression,
155             enclosing1: &Weak<Component>,
156             depth,
157             op: &mut Option<NamedReference>| {
158                if let Some(other_prop) = op {
159                    find_binding(
160                        &other_prop.element(),
161                        other_prop.name(),
162                        |old, enclosing2, d2| {
163                            if Weak::ptr_eq(enclosing1, enclosing2)
164                                && old.priority.saturating_add(d2)
165                                    <= binding.priority.saturating_add(depth)
166                            {
167                                diag.push_error(
168                                    format!(
169                                        "Cannot specify both '{}' and '{}'",
170                                        prop,
171                                        other_prop.name()
172                                    ),
173                                    binding,
174                                )
175                            }
176                        },
177                    );
178                }
179                *op = Some(NamedReference::new(element, prop))
180            };
181        find_binding(element, "height", |s, enclosing, depth| {
182            constraints.fixed_height = true;
183            apply_size_constraint("height", s, enclosing, depth, &mut constraints.min_height);
184            apply_size_constraint("height", s, enclosing, depth, &mut constraints.max_height);
185        });
186        find_binding(element, "width", |s, enclosing, depth| {
187            if s.expression.ty() == Type::Percent {
188                apply_size_constraint("width", s, enclosing, depth, &mut constraints.min_width);
189            } else {
190                constraints.fixed_width = true;
191                apply_size_constraint("width", s, enclosing, depth, &mut constraints.min_width);
192                apply_size_constraint("width", s, enclosing, depth, &mut constraints.max_width);
193            }
194        });
195
196        constraints
197    }
198
199    pub fn has_explicit_restrictions(&self) -> bool {
200        self.min_width.is_some()
201            || self.max_width.is_some()
202            || self.min_height.is_some()
203            || self.max_width.is_some()
204            || self.max_height.is_some()
205            || self.preferred_height.is_some()
206            || self.preferred_width.is_some()
207            || self.horizontal_stretch.is_some()
208            || self.vertical_stretch.is_some()
209    }
210
211    // Iterate over the constraint with a reference to a property, and the corresponding member in the sixtyfps_corelib::layout::LayoutInfo struct
212    pub fn for_each_restrictions<'a>(
213        &'a self,
214        orientation: Orientation,
215    ) -> impl Iterator<Item = (&NamedReference, &'static str)> {
216        let (min, max, preferred, stretch) = match orientation {
217            Orientation::Horizontal => {
218                (&self.min_width, &self.max_width, &self.preferred_width, &self.horizontal_stretch)
219            }
220            Orientation::Vertical => {
221                (&self.min_height, &self.max_height, &self.preferred_height, &self.vertical_stretch)
222            }
223        };
224        std::iter::empty()
225            .chain(min.as_ref().map(|x| {
226                if Expression::PropertyReference(x.clone()).ty() != Type::Percent {
227                    (x, "min")
228                } else {
229                    (x, "min_percent")
230                }
231            }))
232            .chain(max.as_ref().map(|x| {
233                if Expression::PropertyReference(x.clone()).ty() != Type::Percent {
234                    (x, "max")
235                } else {
236                    (x, "max_percent")
237                }
238            }))
239            .chain(preferred.as_ref().map(|x| (x, "preferred")))
240            .chain(stretch.as_ref().map(|x| (x, "stretch")))
241    }
242
243    pub fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
244        if let Some(e) = self.max_width.as_mut() {
245            visitor(&mut *e);
246        }
247        if let Some(e) = self.min_width.as_mut() {
248            visitor(&mut *e);
249        }
250        if let Some(e) = self.max_height.as_mut() {
251            visitor(&mut *e);
252        }
253        if let Some(e) = self.min_height.as_mut() {
254            visitor(&mut *e);
255        }
256        if let Some(e) = self.preferred_width.as_mut() {
257            visitor(&mut *e);
258        }
259        if let Some(e) = self.preferred_height.as_mut() {
260            visitor(&mut *e);
261        }
262        if let Some(e) = self.horizontal_stretch.as_mut() {
263            visitor(&mut *e);
264        }
265        if let Some(e) = self.vertical_stretch.as_mut() {
266            visitor(&mut *e);
267        }
268    }
269}
270
271/// An element in a GridLayout
272#[derive(Debug, Clone)]
273pub struct GridLayoutElement {
274    pub col: u16,
275    pub row: u16,
276    pub colspan: u16,
277    pub rowspan: u16,
278    pub item: LayoutItem,
279}
280
281impl GridLayoutElement {
282    pub fn col_or_row_and_span(&self, orientation: Orientation) -> (u16, u16) {
283        match orientation {
284            Orientation::Horizontal => (self.col, self.colspan),
285            Orientation::Vertical => (self.row, self.rowspan),
286        }
287    }
288}
289
290#[derive(Debug, Clone)]
291pub struct Padding {
292    pub left: Option<NamedReference>,
293    pub right: Option<NamedReference>,
294    pub top: Option<NamedReference>,
295    pub bottom: Option<NamedReference>,
296}
297
298impl Padding {
299    fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
300        if let Some(e) = self.left.as_mut() {
301            visitor(&mut *e)
302        }
303        if let Some(e) = self.right.as_mut() {
304            visitor(&mut *e)
305        }
306        if let Some(e) = self.top.as_mut() {
307            visitor(&mut *e)
308        }
309        if let Some(e) = self.bottom.as_mut() {
310            visitor(&mut *e)
311        }
312    }
313
314    // Return reference to the begin and end padding for a given orientation
315    pub fn begin_end(&self, o: Orientation) -> (Option<&NamedReference>, Option<&NamedReference>) {
316        match o {
317            Orientation::Horizontal => (self.left.as_ref(), self.right.as_ref()),
318            Orientation::Vertical => (self.top.as_ref(), self.bottom.as_ref()),
319        }
320    }
321}
322
323#[derive(Debug, Clone)]
324pub struct LayoutGeometry {
325    pub rect: LayoutRect,
326    pub spacing: Option<NamedReference>,
327    pub alignment: Option<NamedReference>,
328    pub padding: Padding,
329}
330
331impl LayoutGeometry {
332    pub fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
333        self.rect.visit_named_references(visitor);
334        if let Some(e) = self.spacing.as_mut() {
335            visitor(&mut *e)
336        }
337        if let Some(e) = self.alignment.as_mut() {
338            visitor(&mut *e)
339        }
340        self.padding.visit_named_references(visitor);
341    }
342
343    pub fn new(layout_element: &ElementRc) -> Self {
344        let spacing = binding_reference(layout_element, "spacing");
345        let alignment = binding_reference(layout_element, "alignment");
346
347        let padding = || binding_reference(layout_element, "padding");
348        init_fake_property(layout_element, "padding-left", padding);
349        init_fake_property(layout_element, "padding-right", padding);
350        init_fake_property(layout_element, "padding-top", padding);
351        init_fake_property(layout_element, "padding-bottom", padding);
352
353        let padding = Padding {
354            left: binding_reference(layout_element, "padding-left").or_else(padding),
355            right: binding_reference(layout_element, "padding-right").or_else(padding),
356            top: binding_reference(layout_element, "padding-top").or_else(padding),
357            bottom: binding_reference(layout_element, "padding-bottom").or_else(padding),
358        };
359
360        let rect = LayoutRect::install_on_element(layout_element);
361
362        Self { rect, spacing, padding, alignment }
363    }
364}
365
366/// If this element or any of the parent has a binding to the property, call the functor with that binding, and the depth.
367/// Return None if the binding does not exist in any of the sub component, or Some with the result of the functor otherwise
368fn find_binding<R>(
369    element: &ElementRc,
370    name: &str,
371    f: impl FnOnce(&BindingExpression, &Weak<Component>, i32) -> R,
372) -> Option<R> {
373    let mut element = element.clone();
374    let mut depth = 0;
375    loop {
376        if let Some(b) = element.borrow().bindings.get(name) {
377            return Some(f(&b.borrow(), &element.borrow().enclosing_component, depth));
378        }
379        let e = match &element.borrow().base_type {
380            Type::Component(base) => base.root_element.clone(),
381            _ => return None,
382        };
383        element = e;
384        depth += 1;
385    }
386}
387
388/// Return a named reference to a property if a binding is set on that property
389fn binding_reference(element: &ElementRc, name: &str) -> Option<NamedReference> {
390    find_binding(element, name, |_, _, _| NamedReference::new(element, name))
391}
392
393fn init_fake_property(
394    grid_layout_element: &ElementRc,
395    name: &str,
396    lazy_default: impl Fn() -> Option<NamedReference>,
397) {
398    if grid_layout_element.borrow().property_declarations.contains_key(name)
399        && !grid_layout_element.borrow().bindings.contains_key(name)
400    {
401        if let Some(e) = lazy_default() {
402            if e.name() == name && Rc::ptr_eq(&e.element(), grid_layout_element) {
403                // Don't reference self
404                return;
405            }
406            grid_layout_element
407                .borrow_mut()
408                .bindings
409                .insert(name.to_owned(), RefCell::new(Expression::PropertyReference(e).into()));
410        }
411    }
412}
413
414/// Internal representation of a grid layout
415#[derive(Debug, Clone)]
416pub struct GridLayout {
417    /// All the elements will be layout within that element.
418    pub elems: Vec<GridLayoutElement>,
419
420    pub geometry: LayoutGeometry,
421
422    /// When this GridLyout is actually the layout of a Dialog, then the cells start with all the buttons,
423    /// and this variable contains their roles. The string is actually one of the values from the sixtyfps_corelib::layout::DialogButtonRole
424    pub dialog_button_roles: Option<Vec<String>>,
425}
426
427impl GridLayout {
428    fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
429        for cell in &mut self.elems {
430            cell.item.constraints.visit_named_references(visitor);
431        }
432        self.geometry.visit_named_references(visitor);
433    }
434}
435
436/// Internal representation of a BoxLayout
437#[derive(Debug, Clone)]
438pub struct BoxLayout {
439    /// Whether, this is a HorizontalLayout, otherwise a VerticalLayout
440    pub orientation: Orientation,
441    pub elems: Vec<LayoutItem>,
442    pub geometry: LayoutGeometry,
443}
444
445impl BoxLayout {
446    fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
447        for cell in &mut self.elems {
448            cell.constraints.visit_named_references(visitor);
449        }
450        self.geometry.visit_named_references(visitor);
451    }
452}
453
454/// Internal representation of a path layout
455#[derive(Debug, Clone)]
456pub struct PathLayout {
457    pub path: Path,
458    pub elements: Vec<ElementRc>,
459    pub rect: LayoutRect,
460    pub offset_reference: Option<NamedReference>,
461}
462
463impl PathLayout {
464    fn visit_named_references(&mut self, visitor: &mut impl FnMut(&mut NamedReference)) {
465        self.rect.visit_named_references(visitor);
466        self.offset_reference.as_mut().map(visitor);
467    }
468}
469
470/// The [`Type`] for a runtime LayoutInfo structure
471pub fn layout_info_type() -> Type {
472    Type::Struct {
473        fields: ["min", "max", "preferred"]
474            .iter()
475            .map(|s| (s.to_string(), Type::LogicalLength))
476            .chain(
477                ["min_percent", "max_percent", "stretch"]
478                    .iter()
479                    .map(|s| (s.to_string(), Type::Float32)),
480            )
481            .collect(),
482        name: Some("LayoutInfo".into()),
483        node: None,
484    }
485}
486
487/// Get the implicit layout info of a particular element
488pub fn implicit_layout_info_call(elem: &ElementRc, orientation: Orientation) -> Expression {
489    let mut elem_it = elem.clone();
490    loop {
491        return match &elem_it.clone().borrow().base_type {
492            Type::Component(base_comp) => {
493                match base_comp.root_element.borrow().layout_info_prop(orientation) {
494                    Some(nr) => {
495                        // We cannot take nr as is because it is relative to the elem's component. We therefore need to
496                        // use `elem` as an element for the PropertyReference, not `root` within the base of elem
497                        debug_assert!(Rc::ptr_eq(&nr.element(), &base_comp.root_element));
498                        Expression::PropertyReference(NamedReference::new(elem, nr.name()))
499                    }
500                    None => {
501                        elem_it = base_comp.root_element.clone();
502                        continue;
503                    }
504                }
505            }
506            Type::Builtin(base_type) if base_type.name == "Rectangle" => {
507                // hard-code the value for rectangle because many rectangle end up optimized away and we
508                // don't want to depend on the element.
509                Expression::Struct {
510                    ty: layout_info_type(),
511                    values: [("min", 0.), ("max", f32::MAX), ("preferred", 0.)]
512                        .iter()
513                        .map(|(s, v)| (s.to_string(), Expression::NumberLiteral(*v as _, Unit::Px)))
514                        .chain(
515                            [("min_percent", 0.), ("max_percent", 100.), ("stretch", 1.)]
516                                .iter()
517                                .map(|(s, v)| {
518                                    (s.to_string(), Expression::NumberLiteral(*v, Unit::None))
519                                }),
520                        )
521                        .collect(),
522                }
523            }
524            _ => Expression::FunctionCall {
525                function: Box::new(Expression::BuiltinFunctionReference(
526                    BuiltinFunction::ImplicitLayoutInfo(orientation),
527                    None,
528                )),
529                arguments: vec![Expression::ElementReference(Rc::downgrade(elem))],
530                source_location: None,
531            },
532        };
533    }
534}