Merge pull request #232 from l0ng-ai/fix/dim-inactive-panes-followup

chore(settings): finish off the dim-inactive-panes setting
This commit is contained in:
l0ng-ai
2026-07-28 10:42:59 +08:00
committed by GitHub
5 changed files with 81 additions and 36 deletions
+13
View File
@@ -5,6 +5,19 @@ All notable changes to tty7 are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- **Inactive panes only fade if you want them to** — a split tab dims every pane
but the focused one so the active terminal reads as foreground. That is the
right default, but it is not free: at 55% opacity a dim theme's comment color
or a long-running build's output in the pane you are *watching* rather than
typing into gets harder to read, and some people track panes by cursor alone
and never needed the cue. Settings → Appearance → Transparency now carries a
"Dim inactive panes" switch. On by default, so nothing changes for anyone who
was happy; off renders every pane at full opacity. (#214)
## [26.7.5] - 2026-07-27
### Added
+19
View File
@@ -1095,6 +1095,25 @@ mod tests {
assert!(!newer.confirm_window_close);
}
/// Also opt-*out*: every config written before the switch existed predates
/// the choice, and those users have been looking at dimmed panes all along —
/// defaulting to `false` would silently change how every split tab looks on
/// upgrade. And once someone does turn it off, the `false` has to survive a
/// save/load cycle, or the effect they opted out of returns on next launch.
#[test]
fn dim_inactive_panes_defaults_on_and_round_trips() {
assert!(Config::default().dim_inactive_panes);
let old: Config = serde_json::from_str(r#"{"font_size": 15.0}"#).unwrap();
assert!(old.dim_inactive_panes);
let off: Config = serde_json::from_str(r#"{"dim_inactive_panes": false}"#).unwrap();
assert!(!off.dim_inactive_panes);
let json = serde_json::to_string(&off).unwrap();
let back: Config = serde_json::from_str(&json).unwrap();
assert!(!back.dim_inactive_panes);
}
#[test]
fn theme_follow_system_defaults_and_round_trips() {
// Old configs (no follow-system keys) must land on off + the built-in
+8 -5
View File
@@ -2506,8 +2506,8 @@ impl Tty7App {
self.update_config(cx, |cfg| cfg.check_for_updates = on);
}
/// Toggle inactive-pane dimming. Applies on the next render — the pane tree
/// reads the flag from the `Config` global each frame.
/// Toggle inactive-pane dimming. Applies on the next render — `update_config`
/// notifies, and this view's render is what hands the flag to the pane tree.
pub(crate) fn set_dim_inactive_panes(&mut self, on: bool, cx: &mut Context<Self>) {
self.update_config(cx, |cfg| cfg.dim_inactive_panes = on);
}
@@ -5214,7 +5214,7 @@ impl Render for Tty7App {
.get(self.active)
.and_then(|t| t.pane.focused_or_first(window, cx))
.and_then(|leaf| self.render_ssh_status_strip(&leaf, cx));
// Render the active tab's pane tree; show focus rings only when split.
// Render the active tab's pane tree.
let body = match self.tabs.get(self.active) {
// Zero tabs: the window's own face — the home page (see `ui::home`).
None => self.render_home(cx).into_any_element(),
@@ -5235,8 +5235,11 @@ impl Render for Tty7App {
.child(leaf.clone())
.into_any_element(),
None => {
let show_focus = active_tab.pane.leaves().len() > 1;
active_tab.pane.render(show_focus, window, cx)
// Fading the unfocused panes only says anything once the
// tab is actually split, and the user can turn it off.
let dim_inactive = active_tab.pane.leaves().len() > 1
&& cx.global::<Config>().dim_inactive_panes;
active_tab.pane.render(dim_inactive, window, cx)
}
}
}
+21 -21
View File
@@ -524,33 +524,33 @@ impl Pane<Entity<TerminalView>> {
self.close_leaf_where(&|v| v.entity_id() == target.entity_id())
}
/// Render the subtree. `show_focus` draws a focus ring on the active leaf
/// (suppressed when the tab has a single pane).
pub fn render(&self, show_focus: bool, window: &mut Window, cx: &mut App) -> gpui::AnyElement {
/// Render the subtree. `dim_inactive` fades every leaf but the focused one;
/// the caller decides it — it is off for an unsplit tab (nothing to
/// distinguish) and off when the user turned `dim_inactive_panes` off. Kept
/// a parameter rather than a `Config` global read here so the tree stays
/// renderable without one, as the rest of this module is.
pub fn render(
&self,
dim_inactive: bool,
window: &mut Window,
cx: &mut App,
) -> gpui::AnyElement {
match self {
Pane::Empty => div().into_any_element(),
Pane::Leaf(v) => {
let focused = show_focus && v.read(cx).focus_handle.contains_focused(window, cx);
let focused = v.read(cx).focus_handle.contains_focused(window, cx);
// No full border (it reads as a hard rectangle).
div()
.size_full()
.relative()
.overflow_hidden()
// Inactive panes (only when the tab is actually split) fade back
// so the focused terminal reads as foreground without a hard
// border. Element opacity multiplies through the whole subtree
// (terminal glyphs + cell fills), unlike a background-tinted
// scrim which is near-invisible on a light theme (white on
// white). Applied to the container, so a click still lands on
// the terminal and focuses it. `dim_inactive_panes` opts out.
.when(
show_focus
&& !focused
&& cx
.global::<crate::core::config::Config>()
.dim_inactive_panes,
|d| d.opacity(0.55),
)
// Inactive panes fade back so the focused terminal reads as
// foreground without a hard border. Element opacity multiplies
// through the whole subtree (terminal glyphs + cell fills),
// unlike a background-tinted scrim which is near-invisible on a
// light theme (white on white). Applied to the container, so a
// click still lands on the terminal and focuses it.
.when(dim_inactive && !focused, |d| d.opacity(0.55))
.child(v.clone())
.into_any_element()
}
@@ -681,7 +681,7 @@ impl Pane<Entity<TerminalView>> {
.flex_basis(px(0.))
.min_w_0()
.min_h_0()
.child(a.render(show_focus, window, cx)),
.child(a.render(dim_inactive, window, cx)),
)
.child(divider)
.child(
@@ -691,7 +691,7 @@ impl Pane<Entity<TerminalView>> {
.flex_basis(px(0.))
.min_w_0()
.min_h_0()
.child(b.render(show_focus, window, cx)),
.child(b.render(dim_inactive, window, cx)),
)
.into_any_element()
}
+20 -10
View File
@@ -144,6 +144,11 @@ fn settings_search_entries() -> &'static [SearchEntry] {
title: "Blur",
keywords: "transparency translucent frosted vibrancy window background",
},
SearchEntry {
section: Appearance,
title: "Dim inactive panes",
keywords: "fade unfocused inactive split pane focus opacity highlight active dimming",
},
SearchEntry {
section: Appearance,
title: "Font size",
@@ -1518,10 +1523,12 @@ impl Tty7App {
.into_any_element()
}
/// Window section (Appearance): global opacity slider + blur switch that
/// apply to every theme. Both are config *overrides* — until touched they
/// follow the active theme's own `opacity`/`blur`, and "Follow theme"
/// clears them back to that state.
/// Window section (Appearance): the global opacity slider and blur switch
/// that apply to every theme, then the inactive-pane dimming switch. The
/// first two are config *overrides* — until touched they follow the active
/// theme's own `opacity`/`blur`, and "Follow theme" clears them back to that
/// state; the dimming switch is a plain flag no theme carries a value for,
/// so it sits below that button and "Follow theme" leaves it alone.
fn render_window_section(&self, cx: &mut Context<Self>) -> AnyElement {
let Some(slider) = self
.active_settings()
@@ -1578,12 +1585,6 @@ impl Tty7App {
blur_switch,
cx,
))
.child(self.settings_row(
"Dim inactive panes",
"Fade unfocused panes in a split so the active one stands out.",
dim_switch,
cx,
))
// Only offered while an override is active; otherwise the values
// already follow the theme and the button would be a no-op.
.when(overridden, |this| {
@@ -1598,6 +1599,14 @@ impl Tty7App {
),
)
})
// Below "Follow theme", which resets the two rows above it and not
// this one — a plain setting with no theme value behind it.
.child(self.settings_row(
"Dim inactive panes",
"Fade unfocused panes in a split so the active one stands out.",
dim_switch,
cx,
))
.into_any_element()
}
@@ -4623,6 +4632,7 @@ mod tests {
"Sidebar grouping",
"Tab completion",
"History search",
"Dim inactive panes",
] {
assert!(
settings_search_entries().iter().any(|e| e.title == title),