feat(app): add unfocused split-pane dimming — configurable opacity for visual hierarchy - #59
Merged
Merged
Conversation
…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
force-pushed
the
afk/split-pane-dimming
branch
from
August 25, 2026 20:08
3064969 to
2919e73
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
0.0–1.0. Default1.0= no dimming (no visual change for users who don't set this).0.85is a good starting point. Out-of-range values are rejected with astderrwarning and the default is kept.Hit ⌘R after editing
~/.config/umber/config.jsonto see the change live.What changes
Config.swiftConfigFile.unfocusedPaneOpacity(optionalDouble) +AppConfig.unfocusedPaneOpacity(Double, default1.0) + fail-soft load pathSplitContainerView.swiftsetFocusedChild(_:opacity:)— setsalphaValueon both child views with a 150ms ease-in-out animation (skipped at 1.0 so the common case is zero-overhead).didReceiveClickInChildcallback +hitChild(at:)so mouse clicks update dimming beforefirstResponderchangesSpaceViewController+Splits.swiftupdateSplitDimming(for:)resolves the focused child via theisDescendantfirst-responder walk (same path asfocusedShellHost) 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 pathSpaceViewController.swiftapply(config:)to refresh active-split dimming after ⌘RStarterConfig.swiftBehaviour
firstResponderchanges, so the response feels instantaneous.alphaValueis set on thedocumentView(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.swiftat 348 (was 344 before; 2 lines from the ceiling, same as before this PR).Concerns
SpaceViewController.swiftis at 348/350. The one line added toapply(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.SplitContainerView.mouseDown, which fires only when the event reaches the container — if an engine-internal view interceptsmouseDownbefore 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 throughNSView.NSAnimationContextandCAMediaTimingFunction— both ungatable in the headlessswiftcharness pattern. Covered by daily-drive and the file-size gate.