Skip to content

feat(app): add unfocused split-pane dimming — configurable opacity for visual hierarchy - #59

Merged
griffinwork40 merged 1 commit into
mainfrom
afk/split-pane-dimming
Aug 25, 2026
Merged

feat(app): add unfocused split-pane dimming — configurable opacity for visual hierarchy#59
griffinwork40 merged 1 commit into
mainfrom
afk/split-pane-dimming

Conversation

@griffinwork40

Copy link
Copy Markdown
Owner

Unfocused split-pane dimming

When a split is active, the inactive pane dims to a configurable alpha value — making it clear at a glance which pane has focus without hiding the adjacent context.

Config

"unfocusedPaneOpacity": 0.85

0.0–1.0. Default 1.0 = no dimming (no visual change for users who don't set this). 0.85 is a good starting point. Out-of-range values are rejected with a stderr warning and the default is kept.

Hit ⌘R after editing ~/.config/umber/config.json to see the change live.

What changes

File What
Config.swift ConfigFile.unfocusedPaneOpacity (optional Double) + AppConfig.unfocusedPaneOpacity (Double, default 1.0) + fail-soft load path
SplitContainerView.swift setFocusedChild(_:opacity:) — sets alphaValue on both child views with a 150ms ease-in-out animation (skipped at 1.0 so the common case is zero-overhead). didReceiveClickInChild callback + hitChild(at:) so mouse clicks update dimming before firstResponder changes
SpaceViewController+Splits.swift updateSplitDimming(for:) resolves the focused child via the isDescendant first-responder walk (same path as focusedShellHost) and drives the container. Called from: split creation, ⌘⇧H/J/K/L keyboard navigation, config reload, and mouse clicks. Dimming + callback are cleared at every teardown path
SpaceViewController.swift One line in apply(config:) to refresh active-split dimming after ⌘R
StarterConfig.swift Documents the new field with a usage example

Behaviour

  • Single-pane terminal: always full opacity — feature is invisible unless you're in a split.
  • Split creation (⌘⇧\): primary has focus, peer dims immediately.
  • Keyboard focus (⌘⇧H/J/K/L): 150ms animated transition as focus crosses.
  • Mouse click on the unfocused pane: dimming flips before firstResponder changes, so the response feels instantaneous.
  • Config reload (⌘R): new opacity takes effect on the active split immediately.
  • Split close / peer exit / tab close: alpha is reset to 1.0 and the callback is cleared before the view is released.
  • Both engines: alphaValue is set on the documentView (the view the container owns), so it works identically with SwiftTerm and GhosttyKit panes without naming either engine.

File sizes

All files within the 350-LOC ceiling. Largest touched file: SpaceViewController.swift at 348 (was 344 before; 2 lines from the ceiling, same as before this PR).

Config.swift                  336  ⚠ within 14
SpaceViewController.swift     348  ⚠ within 2
SpaceViewController+Splits.swift  272
SplitContainerView.swift      323  ⚠ within 27
StarterConfig.swift            63

Concerns

  • SpaceViewController.swift is at 348/350. The one line added to apply(config:) was necessary (it's the only fan-out path for config reload), and it's a comment + one line of code. The file should be split before anything else lands there.
  • Mouse-click dimming depends on SplitContainerView.mouseDown, which fires only when the event reaches the container — if an engine-internal view intercepts mouseDown before it bubbles to the container, the click callback won't fire. In that case dimming still corrects on the next ⌘⇧H/J/K/L or config reload. This is acceptable for v1 given both engines let events bubble normally through NSView.
  • No gate added. Dimming is an AppKit visual effect that imports NSAnimationContext and CAMediaTimingFunction — both ungatable in the headless swiftc harness pattern. Covered by daily-drive and the file-size gate.

…r visual hierarchy

When a split is active the inactive pane dims to a configurable alpha,
making it clear at a glance which pane has focus without hiding the
context from the adjacent one.

Config: add `unfocusedPaneOpacity` (0.0–1.0, default 1.0 = no dimming).
Fail-soft: out-of-range values warn on stderr and fall back to 1.0.
0.85 is a good starting point; set in config.json and hit ⌘R.

Implementation:
- Config.swift: ConfigFile.unfocusedPaneOpacity (optional Double) +
  AppConfig.unfocusedPaneOpacity (Double, default 1.0) + load() resolver
  that rejects values outside [0,1] with a warning.
- SplitContainerView.swift: setFocusedChild(_:opacity:) — sets alphaValue
  on both child views, with a 150ms ease-in-out animation when opacity<1
  (skipped entirely at 1.0 so the common case is zero-overhead).
  Also adds didReceiveClickInChild callback and hitChild(at:) helper so
  mouse clicks that cross pane boundaries update dimming before
  firstResponder changes.
- SpaceViewController+Splits.swift: updateSplitDimming(for:) resolves
  the focused child via the firstResponder walk (same isDescendant path
  as focusedShellHost) and drives the container. Called from:
  · split creation (primary wins immediately)
  · ⌘⇧H/J/K/L keyboard focus movement
  · apply(config:) config reload via SpaceViewController.swift
  · mouse clicks (via didReceiveClickInChild callback)
  Dimming is cleared (alpha 1.0) and the callback is nil'd at every
  split-teardown path: closeSplitPane, terminateSplitPeer, teardownSplit.
- SpaceViewController.swift: one line added to apply(config:) to refresh
  active-split dimming after a ⌘R reload.
- StarterConfig.swift: documents the new field with usage guidance.

Only affects splits — a single-pane terminal is always full opacity.
Works identically with both SwiftTerm and Ghostty engine panes.
@griffinwork40
griffinwork40 force-pushed the afk/split-pane-dimming branch from 3064969 to 2919e73 Compare August 25, 2026 20:08
@griffinwork40
griffinwork40 merged commit 4f5cbed into main Aug 25, 2026
1 check failed
@griffinwork40
griffinwork40 deleted the afk/split-pane-dimming branch August 25, 2026 20:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant