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