cmux-next: keep each painting's subject clear of the sidebar and agent chat

Merged
TE
18d ago
+2120-373

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.json gives every work a subject box (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_side puts 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. BackdropArtMetadata carries them as subjectBox.
  • Placement (BackdropArtPlacement.swift, replacing cropRect). 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).
    • SidebarContainerView reports its column, at the target width so the art moves once per slide, through WindowMaterialView.setCover(_:for:). Each LayoutRootView does 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 move spring, 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.py mirrors the Swift math. It prints, per window shape, how many works keep their subject, and --sheet draws 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:

WindowOld fixed point keeps subjectNew: keeps / slides or shrinks over fill / cut
16:10, no panes174174 / 0 / 0
16:10, sidebar + agent chat99113 / 59 / 2
21:9, sidebar + agent chat2117 / 57 / 0
Half screen105124 / 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 real WindowMaterialView moves its art and fill when coveredColumns changes, 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 --affected fails 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 --check was already stale on base for Localizable.xcstrings and localization-allowed-omissions.json. I reverted that unrelated churn, so this PR only regenerates BackdropArt+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