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