# Layout and content insets

> How expanded width, chrome insets, and nookContentInsets compose into usable content width.

The expanded panel's **glass width** and the **content column** inside it are controlled
by different knobs on different layers. Stacking horizontal padding - for example
`.padding(.horizontal, 12)` on a home view when the framework already applies
`metrics.edgePadding` - is the most common cause of dead space beside answer text
and bottom command rows.

This guide maps each knob, shows how they compose, and points at
`Examples/LayoutNook/main.swift` for a working host pattern.

## Three layers

Expanded layout stacks three responsibilities:

1. **`NookSurface` (`NookView`)** - clips the notch shape, applies
   `NookStyle.expandedContentInsets` as `.safeAreaInset` strips, adds structural
   horizontal padding (`topCornerRadius`) for the panel ears, and derives the outer
   `nookContentInsets` from corner radii.
2. **`NookKit` (`NookExpandedView`)** - pins the inner VStack to
   `expandedWidth`, wraps it in `metrics.edgePadding`, and re-injects reduced
   `nookContentInsets` for descendants.
3. **Host views** - read `@Environment(\.nookContentInsets)` when content pins to an
   edge or corner; use `frame(maxWidth: .infinity, …)` when a row should span the
   full content column.

```
┌─ NookShape clip (topCornerRadius / bottomCornerRadius) ─────────────────┐
│  .padding(.horizontal, topCornerRadius)  ← structural ear clearance      │
│  ┌─ expandedContent HStack ────────────────────────────────────────────┐ │
│  │  safeAreaInset: expandedContentInsets (default 0/8/8/8)             │ │
│  │  ┌─ NookExpandedView ─────────────────────────────────────────────┐ │ │
│  │  │  .padding(edgePadding)  ← default 8 pt                         │ │ │
│  │  │  ┌─ VStack .frame(width: expandedWidth ?? 520) ──────────────┐ │ │ │
│  │  │  │  env: nookContentInsets (reduced by edgePadding)           │ │ │ │
│  │  │  │  → top bar, Settings, host home                            │ │ │ │
│  │  │  └────────────────────────────────────────────────────────────┘ │ │ │
│  │  └────────────────────────────────────────────────────────────────┘ │ │
│  └──────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────┘
```

## Knob reference

| Knob | Layer | What it does |
|------|-------|--------------|
| `NookConfiguration.expandedWidth` | NookKit | Fixed width of the inner VStack (default 520). Does not resize the glass by itself - the surface measures the wrapped chrome. |
| `NookChromeMetrics.edgePadding` | NookKit | Padding around the inner VStack inside the expanded surface. Default `8` (`NookLayout.edgePadding`). Subtracted before re-injecting `nookContentInsets`. |
| `NookStyle.expandedContentInsets` | NookSurface | Per-edge `.safeAreaInset` strip the chrome reserves around expanded content. Default: top `0`, other edges `8`. |
| `NookStyle.topCornerRadius` / `bottomCornerRadius` | NookSurface | Panel corner radii; drive clip shape and `nookContentInsets` derivation. |
| `EnvironmentValues.nookContentInsets` | Surface -> host | Residual curve clearance **relative to the host content frame**. Use for edge/corner-pinned layout - not as a substitute for centering. |
| `NookTopBarConfiguration.notchClearance` | NookKit | Whether expanded content starts below the hardware notch (`.automatic`, the default) or may run up beside it (`.manual`). See [Clearing the notch](#clearing-the-notch). |
| `EnvironmentValues.nookNotchCutout` | Surface -> host | Where the hardware notch falls in the host content frame. `.none` in the floating form. |

`expandedContentInsets` and `edgePadding` both default to **8 pt** on the sides for
historical parity, but they are **independent knobs** on different layers.

## Usable content width (worked example)

At the framework defaults - `expandedWidth = 520`, radii top `19` / bottom `24`,
`expandedContentInsets` `(0, 8, 8, 8)`, `edgePadding = 8`:

**Step 1 - outer `nookContentInsets`** (injected by `NookView`):

| Edge | Residual |
|------|----------|
| top | 19 |
| bottom | 16 |
| leading | 16 |
| trailing | 16 |

**Step 2 - inner `nookContentInsets`** (after `NookExpandedView` subtracts
`edgePadding`):

| Edge | Residual |
|------|----------|
| top | 11 |
| bottom | 8 |
| leading | 8 |
| trailing | 8 |

**Step 3 - edge-aligned text width** inside the 520 pt VStack:

```
520 − leading(8) − trailing(8) = 504 pt
```

`SettingsView` and `NookTopBar` follow this pattern - section labels and icon
clusters pad by `contentInsets.leading` / `contentInsets.trailing` so everything
shares one left margin.

### Wider panel (600 pt, trimmed bottom inset)

`LayoutNook` sets `expandedWidth = 600` and tightens only the bottom chrome strip
(`expandedContentInsets.bottom = 2`, sides unchanged):

- Inner VStack width: **600 pt**
- Edge-aligned usable width: **600 − 8 − 8 = 584 pt**
- `nookContentInsets.bottom` **rises** to 22 (24 − 2) - bottom-*corner* content must
  inset more; horizontally-centered rows are unaffected and sit ~6 pt closer to the
  rounded bottom.

## Recommended host patterns

### Set width and trim chrome insets in configuration

```swift
var configuration = NookConfiguration()
configuration.expandedWidth = 600
configuration.style = NookStyle(
    topCornerRadius: 19,
    bottomCornerRadius: 24,
    expandedContentInsets: NookEdgeInsets(top: 0, bottom: 2, leading: 8, trailing: 8)
)
configuration.setHome { MyHomeView() }
NookApp.main(configuration)
```

Leave `metrics.edgePadding` at the default unless you have a deliberate reason to
change the wrapper padding - most hosts only need `expandedWidth` and
`expandedContentInsets`.

### Read `nookContentInsets` in the home view

```swift
struct MyHomeView: View {
    @Environment(\.nookContentInsets) private var contentInsets

    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            Text("Answer text …")
                .padding(.leading, contentInsets.leading)
                .padding(.trailing, contentInsets.trailing)
            Spacer(minLength: 0)
            commandRow
        }
    }

    private var commandRow: some View {
        HStack { /* … */ }
            .frame(maxWidth: .infinity, alignment: .leading)
            .padding(.leading, contentInsets.leading)
            .padding(.trailing, contentInsets.trailing)
            .padding(.bottom, contentInsets.bottom)
    }
}
```

Do **not** add `.padding(.horizontal, 12)` (or any fixed horizontal padding) on the
home root - `NookExpandedView` already applies `metrics.edgePadding`. Extra padding
narrows the effective column and leaves visible dead space beside text and toolbars.

### Full-width bottom command rows

Bottom toolbars that should span the content column need two things:

1. **`frame(maxWidth: .infinity, alignment: .leading)`** on the row - otherwise
   SwiftUI shrink-wraps the `HStack` to its buttons and the row looks narrower than
   the panel.
2. **Edge insets from `nookContentInsets`** - not a second horizontal padding pass.

Centered command rows (icons in the middle of the panel) can ignore horizontal
`nookContentInsets`; only corner-pinned content needs the leading/trailing values.

### When you can skip `nookContentInsets`

- **Centered placeholder content** (title + subtitle in the middle) - the default
  demo home does this with vertical padding only.
- **Full-bleed custom art** that intentionally reaches toward the curves - rare; test
  on a notched display.

When content aligns with Settings rows or the top-bar leading cluster, always use
`nookContentInsets`.

## Anti-pattern: double horizontal padding

```swift
// Avoid — stacks on top of NookExpandedView's edgePadding (8) and any
// expandedContentInsets the chrome already consumed.
VStack { … }
    .padding(.horizontal, 12)
```

If the panel feels narrower than the glass, check for fixed horizontal padding on
the registered home view before tuning `expandedWidth`.

## Clearing the notch

In the notch form the panel's top edge is the top of the screen, so the camera housing
covers the middle of a band across the top of the expanded content. The top bar fills
that band: its title sits left of the notch, and the lock and gear sit right of it.
Anything else drawn in the middle of the band is hidden.

`NookTopBarConfiguration.notchClearance` decides what the content does about it:

| Value | Top bar showing | Top bar hidden |
|-------|-----------------|----------------|
| `.automatic` (default) | The bar grows to fill the band when the notch is taller than `metrics.topBarHeight`. | The content starts where it would below the bar. The band stays empty, apart from views placed there with `nookNotchAccessories(leading:trailing:)`. |
| `.manual` | The content starts right below the bar. | The content starts at the top of the panel, beside and under the notch. Lay it out around the notch yourself. |

The floating form has no notch, so both values lay out the same there.

### Views beside the notch

Apply `nookNotchAccessories(leading:trailing:)` to the outermost view of your home
view's body to put small views, such as icons and buttons, on either side of the notch:

```swift
struct PlayerHome: View {
    var body: some View {
        PlayerView()
            .nookNotchAccessories {
                Image(systemName: "music.note")
            } trailing: {
                NookKeepOpenButton()
            }
    }
}

var configuration = NookConfiguration()
configuration.topBar.showsTopBar = false
configuration.setHome { PlayerHome() }
```

With `.automatic` the accessories take the band and the rest of the view starts below
the notch. With `.manual` they fill the band at the top of the view and the content
follows them. Where there is no band to take, below the top bar or in the floating form,
they are the view's first row. The modifier goes inside a view type because
`setHome(_:)` takes a `Sendable` view, which a modified view is not.

Each side only gets the width between the content's edge and the notch: about 150 pt at
the default 520 pt width, and about 80 pt at 380 pt, beside a 185 pt notch. Wrap a label
in `ViewThatFits` to fall back to an icon when it does not fit, as
`Examples/PlaygroundNook` does.

### Laying out around the notch yourself

`@Environment(\.nookNotchCutout)` describes the notch relative to the view that reads
it: its `width`, how far it reaches down into the frame (`height`), and how far its center
sits from the frame's center (`centerOffset`). It is `.none` in the floating form.
`NookNotchRow` uses it to split a row around the notch:

```swift
NookNotchRow {
    Label("Today", systemImage: "sun.max")
} trailing: {
    Button("Refresh", systemImage: "arrow.clockwise", action: refresh)
}
```

Like `nookContentInsets`, the value is relative to the reading view's frame. A wrapper
that pads its content should re-inject an adjusted copy made with
`insetBy(top:leading:trailing:)`.

## See also

- `Examples/LayoutNook/main.swift` - working 600 pt host with trimmed bottom inset
  and a full-width command row.
- [Theming](/guides/theming/#chrome-shape-animation-and-width) - corner radii and
  `expandedContentInsets` in context.
- [Settings chrome](/guides/settings-chrome/) - top-bar layout that shares the same
  inset contract.
- `Sources/NookKit/App/Views/Settings/SettingsView.swift` - canonical edge-aligned
  list layout inside the framework.
- `Tests/NookKitTests/NookContentInsetsTests.swift` - executable spec for inset
  derivation math.
- `Tests/NookKitTests/NookNotchClearanceTests.swift` - where the content, the top bar,
  and notch accessories land around the notch.
