Lay the ratio chips out by hand, because Slint will not lay them out

The ratio chips shipped inside a `GridLayout` with `row` and `col`
computed from the repeater's index. It compiles. On screen every chip
piles into a single row that runs off the panel, and the terminal fills
with

    Internal error in Slint: RepeatedItemTree::grid_layout_input_data()
    not implemented

once per frame. A `for` inside a `GridLayout` is not supported, and
nothing says so until it is running.

A `HorizontalLayout` is not the answer either: six chips side by side
need over 400px, and this column's width is the largest width any panel
declares — so one row would widen every other panel in the application to
fit a control that is on screen only while cropping.

So the grid is arithmetic over the index inside a plain `Rectangle`. It
costs the layout engine nothing, it wraps a seventh ratio onto a third
row by itself, and — because a bare `Rectangle` declares no preferred
width — it takes the width the column already has instead of setting it.
The Portrait chip gets a container of its own for the same reason: a
`ChoiceChip` dropped straight into a `VerticalLayout` is stretched the
full width of the panel and reads as a button for the section rather than
as one more chip.

The three conditional pieces are also now individually-conditional
children of the one layout rather than a nested layout under a single
`if`, which is the convention `app.slint` and `masks.slint` already carry
notes about: a conditional nested layout under-reports its height here
and the panels below it draw on top of one another.

All of this was invisible in the source and obvious in a screenshot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-29 13:49:20 +02:00
co-authored by Claude Opus 5
parent 52c655e6cf
commit 2c8768ac29
2 changed files with 66 additions and 26 deletions
+62 -22
View File
@@ -625,34 +625,74 @@ export component GeometryPanel inherits Rectangle {
// the offered set lives in one place: `CropAspect::CHOICES`. Their
// positions are computed from the index for the same reason — a
// seventh ratio wraps onto a third row by itself.
if root.cropping: VerticalLayout {
spacing: 4px;
padding-top: Theme.gap-sm;
// **Three individually-conditional children, not one `if` around a
// nested layout.** A nested `VerticalLayout` under a condition
// under-reports its height here, and the panels below it get drawn
// on top of one another — invisible in this file and obvious on
// screen. `app.slint` and `masks.slint` both carry the same note
// for the same reason.
if root.cropping: Label { text: "Ratio"; body: true; }
Label { text: "Ratio"; body: true; }
// **The chips are placed, not laid out, and that is not a
// shortcut.** Slint has no wrapping layout, and a `for` inside a
// `GridLayout` compiles and then fails at run time —
// `RepeatedItemTree::grid_layout_input_data() not implemented`,
// printed once per frame while every chip piles into a single row
// that runs off the panel. A `HorizontalLayout` is worse still: six
// chips side by side need over 400px, and this column's width is
// the largest any panel declares, so it would widen every other
// panel in the application to fit a control that is on screen only
// while cropping.
//
// So the grid is arithmetic over the index. It costs the layout
// engine nothing, it wraps a seventh ratio onto a third row by
// itself, and — because a bare `Rectangle` declares no preferred
// width — it takes whatever width the column already has instead
// of setting it.
if root.cropping: Rectangle {
property <int> columns: 3;
property <int> rows: ceil(root.aspects.length / self.columns);
property <length> pitch: Theme.control-height + Theme.gap-sm;
property <length> chip-width:
(self.width - (self.columns - 1) * Theme.gap-sm) / self.columns;
GridLayout {
spacing: Theme.gap-sm;
// A floor, so a narrow window shrinks the column rather than
// the chips: three chips at the width `ChoiceChip` calls
// comfortable is what this row is for.
min-width: self.columns * 64px + (self.columns - 1) * Theme.gap-sm;
height: self.rows * self.pitch - Theme.gap-sm;
for name[i] in root.aspects: ChoiceChip {
row: floor(i / 3);
col: mod(i, 3);
label: name;
selected: i == root.aspect;
enabled: root.enabled;
clicked => { root.aspect-picked(i); }
}
for name[i] in root.aspects: ChoiceChip {
x: mod(i, parent.columns) * (parent.chip-width + Theme.gap-sm);
y: floor(i / parent.columns) * parent.pitch;
width: parent.chip-width;
height: Theme.control-height;
label: name;
selected: i == root.aspect;
enabled: root.enabled;
clicked => { root.aspect-picked(i); }
}
}
// One chip, lit when the ratio is standing on its short edge,
// rather than a Landscape/Portrait pair: the second chip in such a
// pair says nothing the first does not, and this column is narrow.
//
// Disabled rather than hidden where the ratio has no second
// orientation, so the panel does not change height as the chips
// above it are tried.
//
// In a container of its own so it takes one chip's width and sits
// under the first column: a `ChoiceChip` placed straight into the
// `VerticalLayout` is stretched across the whole panel, which reads
// as a button for the section rather than as one more chip.
if root.cropping: Rectangle {
height: Theme.control-height;
// One chip, lit when the ratio is standing on its short edge,
// rather than a Landscape/Portrait pair: the second chip in
// such a pair says nothing the first does not, and this row is
// already the widest thing in the panel.
//
// Disabled rather than hidden where the ratio has no second
// orientation, so the panel does not change height as the
// chips above it are tried.
ChoiceChip {
x: 0;
width: (parent.width - 2 * Theme.gap-sm) / 3;
height: 100%;
label: "Portrait";
selected: root.portrait;
enabled: root.enabled && root.aspect-turnable;