Skip to main content

slint_interpreter/
eval_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//! Dispatch for `Expression::ExtraBuiltinFunctionCall` — layout helper
5//! functions generated by the LLR's layout lowering pass.
6
7use crate::Value;
8use crate::eval::{EvalContext, eval_expression};
9use i_slint_compiler::llr::{Expression, FlexboxMeasureCell, FlexboxMeasureCellKind};
10use i_slint_core::SharedVector;
11use i_slint_core::layout::{
12    BoxLayoutData, FlexboxLayoutData, FlexboxLayoutItemInfo, GridLayoutData, GridLayoutInputData,
13    LayoutInfo, LayoutItemInfo, Padding,
14};
15use i_slint_core::model::Model;
16use i_slint_core::slice::Slice;
17
18// ── Value → layout-type converters ──────────────────────────────────────────
19
20fn to_f32(v: &Value) -> f32 {
21    match v {
22        Value::Number(n) => *n as f32,
23        _ => 0.,
24    }
25}
26
27fn to_padding(v: &Value) -> Padding {
28    let Value::Struct(s) = v else { return Padding::default() };
29    let f = |k| match s.get_field(k) {
30        Some(Value::Number(n)) => *n as f32,
31        _ => 0.,
32    };
33    Padding { begin: f("begin"), end: f("end") }
34}
35
36fn to_enum<T: std::str::FromStr + Default>(v: &Value) -> T {
37    match v {
38        Value::EnumerationValue(_, n) => n.parse().unwrap_or_default(),
39        _ => T::default(),
40    }
41}
42
43fn to_cells(v: &Value) -> Vec<LayoutItemInfo> {
44    let Value::Model(m) = v else { return Vec::new() };
45    (0..m.row_count())
46        .filter_map(|i| {
47            let Value::Struct(s) = m.row_data(i)? else { return None };
48            let c = s.get_field("constraint")?;
49            Some(LayoutItemInfo {
50                constraint: c.clone().try_into().unwrap_or_default(),
51                // Only set for a box layout's cross-axis cells; absent means `auto`.
52                cross_axis_self_alignment: s
53                    .get_field("cross-axis-self-alignment")
54                    .map(to_enum)
55                    .unwrap_or_default(),
56            })
57        })
58        .collect()
59}
60
61/// Convert one `Value::Struct` produced by the LLR's flexbox lowering:
62/// a `FlexboxLayoutItemInfo` with a `constraint` and a nested `props` field.
63/// `Struct::get_field` normalizes identifiers, so the kebab-case keys the
64/// lowering emits match regardless of spelling.
65pub(crate) fn flexbox_item_info_from_struct(s: &crate::api::Struct) -> FlexboxLayoutItemInfo {
66    let constraint: LayoutInfo =
67        s.get_field("constraint").cloned().and_then(|v| v.try_into().ok()).unwrap_or_default();
68    let props = match s.get_field("props") {
69        Some(Value::Struct(p)) => flex_props_from_struct(p),
70        _ => Default::default(),
71    };
72    FlexboxLayoutItemInfo { constraint, props }
73}
74
75/// Convert one `Value::Struct` produced by the LLR's flexbox lowering for a
76/// `FlexItemProps`.
77pub(crate) fn flex_props_from_struct(
78    s: &crate::api::Struct,
79) -> i_slint_core::layout::FlexItemProps {
80    let f = |k: &str| -> f32 {
81        match s.get_field(k) {
82            Some(Value::Number(n)) => *n as f32,
83            _ => 0.,
84        }
85    };
86    i_slint_core::layout::FlexItemProps {
87        flex_grow: f("flex-grow"),
88        flex_shrink: f("flex-shrink"),
89        cross_axis_self_alignment: s
90            .get_field("cross-axis-self-alignment")
91            .map(to_enum)
92            .unwrap_or_default(),
93        flex_order: match s.get_field("flex-order") {
94            Some(Value::Number(n)) => *n as i32,
95            _ => 0,
96        },
97    }
98}
99
100fn to_flex_props(v: &Value) -> Vec<i_slint_core::layout::FlexItemProps> {
101    let Value::Model(m) = v else { return Vec::new() };
102    (0..m.row_count())
103        .filter_map(|i| {
104            let Value::Struct(s) = m.row_data(i)? else { return None };
105            Some(flex_props_from_struct(&s))
106        })
107        .collect()
108}
109
110fn to_u32_vec(v: &Value) -> Vec<u32> {
111    let Value::Model(m) = v else { return Vec::new() };
112    (0..m.row_count())
113        .filter_map(|i| match m.row_data(i)? {
114            Value::Number(n) => Some(n as u32),
115            _ => None,
116        })
117        .collect()
118}
119
120fn to_grid_input_data(v: &Value) -> Vec<GridLayoutInputData> {
121    let Value::Model(m) = v else { return Vec::new() };
122    (0..m.row_count())
123        .filter_map(|i| {
124            let Value::Struct(s) = m.row_data(i)? else { return None };
125            let f = |k: &str| match s.get_field(k) {
126                Some(Value::Number(n)) => *n as f32,
127                _ => 0.,
128            };
129            Some(GridLayoutInputData {
130                new_row: matches!(s.get_field("new_row"), Some(Value::Bool(true))),
131                col: f("col"),
132                row: f("row"),
133                colspan: f("colspan"),
134                rowspan: f("rowspan"),
135            })
136        })
137        .collect()
138}
139
140fn to_array_of_u16(v: &Value) -> SharedVector<u16> {
141    match v {
142        Value::ArrayOfU16(v) => v.clone(),
143        _ => Default::default(),
144    }
145}
146
147fn to_dialog_roles(v: &Value) -> Vec<i_slint_core::items::DialogButtonRole> {
148    let Value::Model(m) = v else { return Vec::new() };
149    (0..m.row_count())
150        .filter_map(|i| match m.row_data(i)? {
151            Value::EnumerationValue(_, n) => n.parse().ok(),
152            _ => None,
153        })
154        .collect()
155}
156
157fn sf32(s: &crate::api::Struct, k: &str) -> f32 {
158    match s.get_field(k) {
159        Some(Value::Number(n)) => *n as f32,
160        _ => 0.,
161    }
162}
163
164// ── Dispatch ────────────────────────────────────────────────────────────────
165
166pub(crate) fn call_extra_builtin(
167    ctx: &mut EvalContext,
168    name: &str,
169    arguments: &[Expression],
170) -> Value {
171    let a: Vec<Value> = arguments.iter().map(|e| eval_expression(ctx, e)).collect();
172
173    match name {
174        "box_layout_info" => {
175            let c = to_cells(&a[0]);
176            i_slint_core::layout::box_layout_info(
177                Slice::from_slice(&c),
178                to_f32(&a[1]),
179                &to_padding(&a[2]),
180                to_enum(&a[3]),
181            )
182            .into()
183        }
184        "box_layout_info_ortho" => {
185            let c = to_cells(&a[0]);
186            i_slint_core::layout::box_layout_info_ortho(Slice::from_slice(&c), &to_padding(&a[1]))
187                .into()
188        }
189        "organize_dialog_button_layout" => {
190            let input = to_grid_input_data(&a[0]);
191            let roles = to_dialog_roles(&a[1]);
192            Value::ArrayOfU16(i_slint_core::layout::organize_dialog_button_layout(
193                Slice::from_slice(&input),
194                Slice::from_slice(&roles),
195            ))
196        }
197        "organize_grid_layout" => {
198            let (input, ri, rs) = (to_grid_input_data(&a[0]), to_u32_vec(&a[1]), to_u32_vec(&a[2]));
199            Value::ArrayOfU16(i_slint_core::layout::organize_grid_layout(
200                Slice::from_slice(&input),
201                Slice::from_slice(&ri),
202                Slice::from_slice(&rs),
203            ))
204        }
205        "grid_layout_info" => {
206            let (c, ri, rs) = (to_cells(&a[1]), to_u32_vec(&a[2]), to_u32_vec(&a[3]));
207            i_slint_core::layout::grid_layout_info(
208                to_array_of_u16(&a[0]),
209                Slice::from_slice(&c),
210                Slice::from_slice(&ri),
211                Slice::from_slice(&rs),
212                to_f32(&a[4]),
213                &to_padding(&a[5]),
214                to_enum(&a[6]),
215            )
216            .into()
217        }
218        "solve_grid_layout" => {
219            let (c, ri, rs) = (to_cells(&a[1]), to_u32_vec(&a[3]), to_u32_vec(&a[4]));
220            let Value::Struct(s) = &a[0] else { return Value::LayoutCache(Default::default()) };
221            Value::LayoutCache(i_slint_core::layout::solve_grid_layout(
222                &GridLayoutData {
223                    size: sf32(s, "size"),
224                    spacing: sf32(s, "spacing"),
225                    padding: s.get_field("padding").map(to_padding).unwrap_or_default(),
226                    organized_data: s
227                        .get_field("organized_data")
228                        .map(to_array_of_u16)
229                        .unwrap_or_default(),
230                },
231                Slice::from_slice(&c),
232                to_enum(&a[2]),
233                Slice::from_slice(&ri),
234                Slice::from_slice(&rs),
235            ))
236        }
237        "solve_box_layout" => {
238            let ri = to_u32_vec(&a[1]);
239            let Value::Struct(s) = &a[0] else { return Value::LayoutCache(Default::default()) };
240            let cells = s.get_field("cells").map(to_cells).unwrap_or_default();
241            Value::LayoutCache(i_slint_core::layout::solve_box_layout(
242                &BoxLayoutData {
243                    size: sf32(s, "size"),
244                    spacing: sf32(s, "spacing"),
245                    padding: s.get_field("padding").map(to_padding).unwrap_or_default(),
246                    alignment: s.get_field("alignment").map(to_enum).unwrap_or_default(),
247                    cells: Slice::from_slice(&cells),
248                },
249                Slice::from_slice(&ri),
250            ))
251        }
252        "solve_box_layout_ortho" => {
253            let ri = to_u32_vec(&a[1]);
254            let Value::Struct(s) = &a[0] else { return Value::LayoutCache(Default::default()) };
255            let cells = s.get_field("cells").map(to_cells).unwrap_or_default();
256            Value::LayoutCache(i_slint_core::layout::solve_box_layout_ortho(
257                &i_slint_core::layout::BoxLayoutOrthoData {
258                    size: sf32(s, "size"),
259                    padding: s.get_field("padding").map(to_padding).unwrap_or_default(),
260                    cross_axis_alignment: s
261                        .get_field("cross_axis_alignment")
262                        .map(to_enum)
263                        .unwrap_or_default(),
264                    cells: Slice::from_slice(&cells),
265                },
266                Slice::from_slice(&ri),
267            ))
268        }
269        "solve_flexbox_layout" => {
270            let ri = to_u32_vec(&a[1]);
271            let Value::Struct(s) = &a[0] else { return Value::LayoutCache(Default::default()) };
272            let (ch, cv) = (
273                s.get_field("cells_h").map(to_cells).unwrap_or_default(),
274                s.get_field("cells_v").map(to_cells).unwrap_or_default(),
275            );
276            let fp = s.get_field("flex_props").map(to_flex_props).unwrap_or_default();
277            Value::LayoutCache(i_slint_core::layout::solve_flexbox_layout(
278                &FlexboxLayoutData {
279                    width: sf32(s, "width"),
280                    height: sf32(s, "height"),
281                    spacing_h: sf32(s, "spacing_h"),
282                    spacing_v: sf32(s, "spacing_v"),
283                    padding_h: s.get_field("padding_h").map(to_padding).unwrap_or_default(),
284                    padding_v: s.get_field("padding_v").map(to_padding).unwrap_or_default(),
285                    alignment: s.get_field("alignment").map(to_enum).unwrap_or_default(),
286                    direction: s.get_field("direction").map(to_enum).unwrap_or_default(),
287                    cross_axis_line_alignment: s
288                        .get_field("cross_axis_line_alignment")
289                        .map(to_enum)
290                        .unwrap_or_default(),
291                    cross_axis_alignment: s
292                        .get_field("cross_axis_alignment")
293                        .map(to_enum)
294                        .unwrap_or_default(),
295                    flex_wrap: s.get_field("flex_wrap").map(to_enum).unwrap_or_default(),
296                    cells_h: Slice::from_slice(&ch),
297                    cells_v: Slice::from_slice(&cv),
298                    flex_props: Slice::from_slice(&fp),
299                },
300                Slice::from_slice(&ri),
301            ))
302        }
303        "flexbox_layout_info_main_axis" => {
304            let cells = to_cells(&a[0]);
305            let fp = to_flex_props(&a[1]);
306            i_slint_core::layout::flexbox_layout_info_main_axis(
307                Slice::from_slice(&cells),
308                Slice::from_slice(&fp),
309                to_f32(&a[2]),
310                &to_padding(&a[3]),
311                to_enum(&a[4]),
312            )
313            .into()
314        }
315        "flexbox_layout_unwrapped_main" => {
316            let cells = to_cells(&a[0]);
317            Value::Number(i_slint_core::layout::flexbox_layout_unwrapped_main(
318                Slice::from_slice(&cells),
319                to_f32(&a[1]),
320                &to_padding(&a[2]),
321            ) as f64)
322        }
323        "flexbox_layout_info_cross_axis" => {
324            let (ch, cv) = (to_cells(&a[0]), to_cells(&a[1]));
325            let fp = to_flex_props(&a[2]);
326            i_slint_core::layout::flexbox_layout_info_cross_axis(
327                Slice::from_slice(&ch),
328                Slice::from_slice(&cv),
329                Slice::from_slice(&fp),
330                to_f32(&a[3]),
331                to_f32(&a[4]),
332                &to_padding(&a[5]),
333                &to_padding(&a[6]),
334                to_enum(&a[7]),
335                to_enum(&a[8]),
336                to_f32(&a[9]),
337            )
338            .into()
339        }
340        other => unimplemented!("ExtraBuiltinFunctionCall `{other}`"),
341    }
342}
343
344fn eval_info(ctx: &mut EvalContext, e: &Expression) -> LayoutInfo {
345    eval_expression(ctx, e).try_into().unwrap_or_default()
346}
347
348/// One flexbox cell as seen by the measure callback, after expanding
349/// repeaters (a repeater contributes one entry per instance).
350struct FlatCell<'a> {
351    kind: FlatCellKind<'a>,
352    w4h_only: bool,
353}
354
355enum FlatCellKind<'a> {
356    Static {
357        h_info: &'a Expression,
358        v_info: &'a Expression,
359    },
360    Repeated(vtable::VRc<i_slint_core::item_tree::ItemTreeVTable, crate::instance::Instance>),
361    /// No constrained layout info: the pre-resolved sizes are already correct.
362    Fixed,
363}
364
365/// Flatten `measure_cells` into one entry per taffy cell. Static cells carry
366/// their `(h_info, v_info)` expressions; a repeater expands to one instance
367/// per row (re-measured through its own item tree at the assigned cross size).
368fn flatten_measure_cells<'a>(
369    ctx: &mut EvalContext,
370    measure_cells: &'a [FlexboxMeasureCell],
371) -> Vec<FlatCell<'a>> {
372    let mut flat: Vec<FlatCell> = Vec::with_capacity(measure_cells.len());
373    for item in measure_cells {
374        match &item.kind {
375            FlexboxMeasureCellKind::Static { h_info, v_info } => flat.push(FlatCell {
376                kind: FlatCellKind::Static { h_info, v_info },
377                w4h_only: item.w4h_only,
378            }),
379            FlexboxMeasureCellKind::Repeated(repeater) => {
380                if let Some(current) = ctx.current.as_ref() {
381                    let rep = &current.repeaters[repeater.repeater_index];
382                    rep.track_instance_changes();
383                    flat.extend(rep.instances_vec().into_iter().map(|instance| FlatCell {
384                        kind: FlatCellKind::Repeated(instance),
385                        w4h_only: item.w4h_only,
386                    }));
387                }
388            }
389            FlexboxMeasureCellKind::Fixed => {
390                flat.push(FlatCell { kind: FlatCellKind::Fixed, w4h_only: item.w4h_only })
391            }
392        }
393    }
394    flat
395}
396
397/// Measure callback body shared by the solve and cross-axis-info paths:
398/// re-evaluate the cell's perpendicular layout info with the
399/// `measure_known_w` / `measure_known_h` local set to the dimension taffy
400/// assigned (a dimension it did not assign, `known_* == false`, arrives
401/// pre-resolved to the cell's preferred size). A probe with neither dimension
402/// known measures the cell's free axis at the default size (see
403/// `FlexboxMeasureFn` in i-slint-core).
404fn measure_flexbox_cell(
405    ctx: &mut EvalContext,
406    flat: &[FlatCell],
407    index: usize,
408    w: f32,
409    h: f32,
410    known_w: bool,
411    known_h: bool,
412) -> (f32, f32) {
413    let Some(cell) = flat.get(index) else { return (w, h) };
414    // measure the height at the width `w`
415    let measure_height = |ctx: &mut EvalContext| match &cell.kind {
416        FlatCellKind::Static { v_info, .. } => {
417            let prev = ctx.locals.insert("measure_known_w".into(), Value::Number(w as f64));
418            let info = eval_info(ctx, v_info);
419            crate::eval::restore_local(ctx, "measure_known_w", prev);
420            (w, info.preferred_bounded())
421        }
422        FlatCellKind::Repeated(instance) => (
423            w,
424            instance
425                .as_pin_ref()
426                .flexbox_layout_item_info_at_cross_width(w)
427                .constraint
428                .preferred_bounded(),
429        ),
430        FlatCellKind::Fixed => (w, h),
431    };
432    // measure the width at the height `h`
433    let measure_width = |ctx: &mut EvalContext| match &cell.kind {
434        FlatCellKind::Static { h_info, .. } => {
435            let prev = ctx.locals.insert("measure_known_h".into(), Value::Number(h as f64));
436            let info = eval_info(ctx, h_info);
437            crate::eval::restore_local(ctx, "measure_known_h", prev);
438            (info.preferred_bounded(), h)
439        }
440        FlatCellKind::Repeated(instance) => (
441            instance
442                .as_pin_ref()
443                .flexbox_layout_item_info_at_cross_height(h)
444                .constraint
445                .preferred_bounded(),
446            h,
447        ),
448        FlatCellKind::Fixed => (w, h),
449    };
450    match (known_w, known_h) {
451        (true, true) => (w, h),
452        (true, false) => measure_height(ctx),
453        (false, true) => measure_width(ctx),
454        (false, false) => {
455            if cell.w4h_only {
456                measure_width(ctx)
457            } else {
458                measure_height(ctx)
459            }
460        }
461    }
462}
463
464/// Interpret [`Expression::SolveFlexboxLayoutWithMeasure`].
465pub(crate) fn solve_flexbox_layout_with_measure(ctx: &mut EvalContext, expr: &Expression) -> Value {
466    let Expression::SolveFlexboxLayoutWithMeasure { data, repeater_indices, measure_cells } = expr
467    else {
468        return Value::Void;
469    };
470    let ri = to_u32_vec(&eval_expression(ctx, repeater_indices));
471    let data = eval_expression(ctx, data);
472    let Value::Struct(s) = &data else { return Value::LayoutCache(Default::default()) };
473    let (ch, cv) = (
474        s.get_field("cells_h").map(to_cells).unwrap_or_default(),
475        s.get_field("cells_v").map(to_cells).unwrap_or_default(),
476    );
477    let fp = s.get_field("flex_props").map(to_flex_props).unwrap_or_default();
478
479    let flat = flatten_measure_cells(ctx, measure_cells);
480    let mut measure = |index: usize, w: f32, h: f32, known_w: bool, known_h: bool| {
481        measure_flexbox_cell(ctx, &flat, index, w, h, known_w, known_h)
482    };
483
484    Value::LayoutCache(i_slint_core::layout::solve_flexbox_layout_with_measure(
485        &FlexboxLayoutData {
486            width: sf32(s, "width"),
487            height: sf32(s, "height"),
488            spacing_h: sf32(s, "spacing_h"),
489            spacing_v: sf32(s, "spacing_v"),
490            padding_h: s.get_field("padding_h").map(to_padding).unwrap_or_default(),
491            padding_v: s.get_field("padding_v").map(to_padding).unwrap_or_default(),
492            alignment: s.get_field("alignment").map(to_enum).unwrap_or_default(),
493            direction: s.get_field("direction").map(to_enum).unwrap_or_default(),
494            cross_axis_line_alignment: s
495                .get_field("cross_axis_line_alignment")
496                .map(to_enum)
497                .unwrap_or_default(),
498            cross_axis_alignment: s
499                .get_field("cross_axis_alignment")
500                .map(to_enum)
501                .unwrap_or_default(),
502            flex_wrap: s.get_field("flex_wrap").map(to_enum).unwrap_or_default(),
503            cells_h: Slice::from_slice(&ch),
504            cells_v: Slice::from_slice(&cv),
505            flex_props: Slice::from_slice(&fp),
506        },
507        Slice::from_slice(&ri),
508        Some(&mut measure),
509    ))
510}
511
512/// Interpret [`Expression::FlexboxLayoutInfoCrossAxisWithMeasure`]: the
513/// `flexbox_layout_info_cross_axis` builtin plus the measure callback, so
514/// height-for-width cells are measured at the main-axis size taffy assigns
515/// them rather than at the container size the cells were pre-measured at.
516pub(crate) fn flexbox_layout_info_cross_axis_with_measure(
517    ctx: &mut EvalContext,
518    expr: &Expression,
519) -> Value {
520    let Expression::FlexboxLayoutInfoCrossAxisWithMeasure { arguments, measure_cells } = expr
521    else {
522        return Value::Void;
523    };
524    let a: Vec<Value> = arguments.iter().map(|e| eval_expression(ctx, e)).collect();
525    let (ch, cv) = (to_cells(&a[0]), to_cells(&a[1]));
526    let fp = to_flex_props(&a[2]);
527    let flat = flatten_measure_cells(ctx, measure_cells);
528    let mut measure = |index: usize, w: f32, h: f32, known_w: bool, known_h: bool| {
529        measure_flexbox_cell(ctx, &flat, index, w, h, known_w, known_h)
530    };
531    i_slint_core::layout::flexbox_layout_info_cross_axis_with_measure(
532        Slice::from_slice(&ch),
533        Slice::from_slice(&cv),
534        Slice::from_slice(&fp),
535        to_f32(&a[3]),
536        to_f32(&a[4]),
537        &to_padding(&a[5]),
538        &to_padding(&a[6]),
539        to_enum(&a[7]),
540        to_enum(&a[8]),
541        to_f32(&a[9]),
542        Some(&mut measure),
543    )
544    .into()
545}