cmux-next: keep each painting's subject clear of the sidebar and agent chat
Phase 1 of the backgrounds plan (briefs/next-backgrounds-plan.md, agreed by Leo).
Background paintings often put their subject under the sidebar and the agent chat, or cut it off in wide and short windows. The cause was that every wide painting framed the same fixed point (0.5, 0.45), the placeholder render_backdrop_art.py writes in auto mode. Now each work names its subject, and the art places itself so that subject lies where art can still be seen.
What changes
- Subjects.
art/manifest.jsongives every work asubjectbox (fractions of the 1600×1000 image, top-left origin).- The 73 wide works were marked by eye from gridded contact sheets.
- The 101 portrait works that
render_sideputs on the right get the painting's own rect, computed from its render geometry and trimmed to the head and upper body (top 70%), which fits a 21:9 window. - The generator emits the boxes into
BackdropArt+Entries.swift.BackdropArtMetadatacarries them assubjectBox.
- Placement (
BackdropArtPlacement.swift, replacingcropRect). It takes the least zoom (up to 1.4×) at which the art covers the window and its subject lies in the visible span, centered there.- If the subject is taller than the window, it keeps its top.
- If the subject fits the span but not while the art covers the window, the art slides, and shrinks if needed (down to 0.7×), off the edge under the panes. A blurred, dimmed copy of the art fills behind it (
artFillView). - If even that fails, the art covers the window around the subject's center, as before.
- Visible span. The window minus the sidebar and a docked agent chat column (
DockRole.agentChat).SidebarContainerViewreports its column, at the target width so the art moves once per slide, throughWindowMaterialView.setCover(_:for:). EachLayoutRootViewdoes the same for its agent chat, under its own key, so a parked workspace clears only its own column.- CmuxNextApp is unchanged (check-app-size).
- A gap narrower than 35% of the window falls back to the whole window.
- When a column opens, closes or resizes, the art moves on the
movespring, and snaps under Reduce Motion.
- Your own images use their whole frame as the subject, so they center as before and only slide as far as covering the window allows. Dragging to place them yourself is phase 6.
- Audit tool:
scripts/cmux-next/art/framing_sheet.pymirrors the Swift math. It prints, per window shape, how many works keep their subject, and--sheetdraws them all.
Evidence
framing_sheet.py over all 174 works. Kept or cut is measured on each placement (subject frame inside the visible span and the window). The sidebar is 240 pt, and the agent chat is 420 pt when docked:
| Window | Old fixed point keeps subject | New: keeps / slides or shrinks over fill / cut |
|---|---|---|
| 16:10, no panes | 174 | 174 / 0 / 0 |
| 16:10, sidebar + agent chat | 99 | 113 / 59 / 2 |
| 21:9, sidebar + agent chat | 2 | 117 / 57 / 0 |
| Half screen | 105 | 124 / 44 / 6 |
The 2 still cut at 16:10 are Klee's Fish Magic and The Starry Night, whose subjects span nearly the whole picture.
Tests
CmuxNextDesignTests/BackdropArtPlacementTests:- placement cases: full fit, wide-short window, moving clear of the agent column, shrink over fill, too-wide fallback
- span clamping
- focal-only art
- degenerate sizes
- every catalog work in three layouts, with sanity checks: no stretching, nothing outside the image or view
CmuxNextAppTests/BackdropArtFramingTests: a realWindowMaterialViewmoves its art and fill whencoveredColumnschanges, and moves back when they clear.
Verification
- Not built locally: this lane has no Mac (no builds on Air Blue). Swift compile and tests are left to this PR's CI.
lint-ios-package-conventions.sh: clean.verify-local.py --affectedfails only on files missing from my sparse checkout (Xcode project, iOS catalogs) and on existing catalog state. This PR adds no strings.generate_backdrop_art.py --checkwas already stale on base forLocalizable.xcstringsandlocalization-allowed-omissions.json. I reverted that unrelated churn, so this PR only regeneratesBackdropArt+Entries.swift.- Not dogfooded yet. Worth a look with a
dev-build: open an agent chat and watch the painting move.
WINDOW
None. No settings schema, catalog or Package.swift change. The on/off toggle (appearance.backgroundAvoidPanes, default on) lands in phase 5.
Changelog
Fixed: Background paintings keep their subject in view, clear of the sidebar and the agent chat, in any window shape.
🤖 Generated with Claude Code