mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 08:02:24 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user