# Slint Tips

Gotchas encountered in this codebase. Review before writing or modifying Slint UI code.

## VerticalLayout inside Flickable always bottom-aligns content

**Problem:** A `VerticalLayout` placed directly inside a `Flickable` stretches to fill the viewport height. When the content is shorter than the viewport, items render at the BOTTOM — regardless of `alignment: start`, `y: 0px`, wrapper Rectangles, or explicit `viewport-height` bindings. None of these fix the issue.

**Root cause:** The Flickable's viewport determines the VerticalLayout's available height. The layout stretches to fill it, and Slint's layout algorithm places items at the bottom when there's excess space, ignoring `alignment: start`.

**Solution:** Don't use a Flickable for scrollable lists. Instead, wrap content in a clipped Rectangle with absolute y-positioning:

```slint
// WRONG — items land at the bottom
Flickable {
    vertical-stretch: 1;
    VerticalLayout {
        alignment: start;  // DOES NOT WORK
        for item in model : Rectangle { height: 40px; }
    }
}

// CORRECT — items are always at the top
Rectangle {
    vertical-stretch: 1;
    clip: true;
    for item[idx] in model : Rectangle {
        x: 12px;
        y: idx * 44px + 4px;
        width: parent.width - 24px;
        height: 40px;
    }
}
```

**Also important:** The parent VerticalLayout must have `alignment: stretch` (explicit), and the header sibling must have `vertical-stretch: 0` to prevent it from expanding into the unit list's space.

## HorizontalLayout alignment:center prevents horizontal stretching

`alignment: center` on a HorizontalLayout centers children horizontally and prevents them from stretching to fill available width — even if a child has `horizontal-stretch: 1`. This caused the serial search input box to collapse to zero width.

If you need one child to stretch AND another to be vertically centered, remove `alignment` from the HorizontalLayout and wrap the non-stretching child in a `VerticalLayout { alignment: center; }` instead.

## TouchArea overlays block TextInput click-drag selection

Placing a `TouchArea` after a `TextInput` (to add cursor styling or click-to-focus) intercepts mouse-down events, preventing native click-drag text selection. The TextInput handles focus and cursor natively — remove the TouchArea overlay. Arrow+Shift selection still works because those are keyboard events.

## ListView requires a single `for` child

`ListView` from `std-widgets.slint` can only contain a single `for` loop as its child — no conditionals (`if`), no mixed elements. If you need conditional content mixed with a list, use the clipped Rectangle approach from the Flickable tip above.

## Window background defaults to white

Set `background: Colors.background` on the Window component to avoid a bright white flash during startup while the app initializes.

## Font embedding uses bare import syntax

Slint 1.15 embeds fonts via bare imports in the .slint file, NOT via `slint_build` config:

```slint
import "fonts/Inter-Regular.ttf";
```

Set `default-font-family: "Inter"` on the Window component.

## Hover-conditional element flicker (TouchArea feedback loop)

**Problem:** An element wrapped in `if some-touch.has-hover : ...` contains its own inner `TouchArea`. When the cursor moves from the card body onto the inner TouchArea, the inner element briefly "captures" the cursor, setting `some-touch.has-hover = false`. This hides the outer wrapper, returning the cursor to `some-touch` which sets `has-hover = true` again -- causing rapid visible flickering.

**Root cause:** Slint's hit-testing routes cursor to the deepest TouchArea. When the conditional element is visible, its inner TouchArea wins cursor hits, temporarily removing it from the outer passive TouchArea, toggling the condition.

**Solution:** Add a persistent passive `TouchArea` (no `clicked` handler) at the button's position, declared BEFORE the conditional content. Condition the element on `outer-touch.has-hover || passive-zone.has-hover`. The passive zone always exists -- no feedback loop.

```slint
// WRONG -- flickers when cursor moves onto add-btn-touch
if card-hover-zone.has-hover : Rectangle {
    add-btn-touch := TouchArea { clicked => { root.add-item-clicked(); } }
}

// CORRECT -- btn-zone always present; hover state never drops
btn-zone := TouchArea { x: btn-x; y: 0px; width: 44px; height: 80px; }
if card-hover-zone.has-hover || btn-zone.has-hover : Rectangle {
    add-btn-touch := TouchArea { clicked => { root.add-item-clicked(); } }
}
```

**Child order rule:** Declare the passive zone FIRST. Slint's last-declared-child-wins rule ensures the interactive `add-btn-touch` (declared later, inside the conditional) still receives clicks.
