feat(windows): add native backdrop material presets (Mica / Acrylic /… (#412)

* feat(windows): add native backdrop material presets (Mica / Acrylic / Blur)

Adds a Background material dropdown (Auto / Blur / Mica / Mica Alt /
Acrylic / Off) that maps onto the native Windows backdrop APIs already
provided by the gpui fork — Mica and Mica Alt via
DwmSetWindowAttribute(DWMWA_SYSTEMBACKDROP_TYPE), Acrylic via the new
DWMSBT_TRANSIENTWINDOW material, and Blur via the classic
ACCENT_ENABLE_ACRYLICBLURBEHIND path — with no fork changes required.
* config: introduce WindowBackdrop in tty7-core with lenient kebab-case
  deserialization, defaulting to Auto for existing configs
* theme: resolve the backdrop through a build-number fallback chain
  (Mica/Mica Alt need Windows 11 22H2, Acrylic needs 22H2 natively and
  1809 via classic acrylic, Blur needs 1809; older builds fall back to
  plain translucency) and default the background alpha to
  SYSTEM_MATERIAL_OPACITY (0.82) while a material is active
* settings: replace the blur toggle with a localized backdrop dropdown
  that only lists the presets the current Windows build actually
  supports, and keep the settings panel fully opaque so workspace
  translucency never shows through it
* theme: make the file sidebar and right detail panel follow the window
  opacity so the backdrop material shows through the whole workspace,
  keeping row-level accents opaque for readability
* i18n: add backdrop keys for en, zh-CN and ja-JP, covered by the
  translation completeness test

* feat(theme): let the sidebar and right panel follow the window opacity

* update GPUI

* fix(windows): gate the sidebar translucency to translucent windows and sync the opacity slider

fix(windows): gate the sidebar translucency compensation to active materials

* fix(windows): derive the material opacity default from the resolved appearance

* fix(theme): keep WindowBackdrop semantics consistent on non-Windows

f

* fix(theme): stop Windows-only materials from pinning the blur on other platforms

* docs(changelog): document the Windows backdrop material settings

* refactor(theme): share the default window-opacity derivation

* fix(ui): keep gradient presets behind the settings panel and scope its fallbacks

* fix(ui): keep the settings theme picker legible and the backdrop label honest

f

* fix(theme): let every backdrop variant defer to the local blur toggle on non-Windows

* fix(settings): restore the backdrop dropdown selection on locale refresh

* fix(ui): keep the opened-file editor surface opaque under window translucency

* fix(settings): rebuild backdrop options after selection

* fix(settings): ignore synced windows backdrop overrides on other platforms

* fix(settings): preserve synced windows backdrop on non-windows reset

* fix(diff): keep the full-window overlay background opaque

* fix(windows): keep Auto opaque and stop the backdrop from misreporting itself

Ten findings from a review of the backdrop-material work, all in the
Windows-only paths.

The root one: `material_active` treated `Auto` as a material whenever the
legacy blur toggle happened to be on. `Auto` is the default in every config
written before this setting existed, and plenty of them carry
`window_blur: true` from the switch that no longer renders on Windows, so an
untouched install would drop from opaque to 0.82 alpha - with its file
sidebar and right panel at 0.15 - on first launch after the update, with no
visible control to undo it. Only an explicit pick in the dropdown now buys
the translucent defaults. The switch comes back on Windows while the
backdrop is `Auto`, since that is exactly when the legacy flag still decides
something.

The rest:

- Mica and Mica Alt fell back to `Blurred` with no lower bound, asking for a
  blur that does not exist below 1809 - and build 0, which is what a failed
  `RtlGetVersion` reports. They now degrade to plain translucency like
  `Blur` and `Acrylic` already did.
- Acrylic is no longer offered below 22H2, where it resolves to the very
  same classic WCA blur as `Blur`. A test now asserts that no two offered
  presets render identically on any build.
- `reload_from_config` re-applied the theme and the opacity slider but not
  the backdrop dropdown, so an external config change switched the window's
  material while the control kept naming the old one.
- The settings, opened-file and diff overlays were made opaque so the OS
  backdrop cannot show through their text; that also hid the theme
  background image, which used to show through them. They paint their own
  copy of it now, and the fill they share moved into
  `theme::overlay_background`.
- The SFTP transfers tray painted `workspace_surface_color` inside the right
  panel, which already paints it, stacking the same translucent surface
  twice into a darker band with a hard seam.
- `apply_theme` re-issued `set_background_appearance` on every `Config`
  mutation in every window. With a DWM material that now costs a
  `SetWindowPos(SWP_FRAMECHANGED)` frame recalc, so dragging the opacity
  slider recalculated the frame once per mouse sample; it is skipped when
  the appearance is unchanged.

* fix(ui): dim the overlay background image, and stop telling Windows it is macOS

Two defects found while driving the previous commit's changes in the app.

The overlays repaint the theme background image over their own opaque fill,
so it survives them being made opaque - but nothing dimmed it. Before those
overlays were opaque the image reached the eye through their translucent
fill; painting it at full strength put the settings text straight on top of
the wallpaper and made the panel unreadable at any image opacity above about
half. They now paint the image and then the workspace's own fill over it,
which is exactly the strength the image had through these overlays before,
and which needs no new constant to say so. Shared as
`app::overlay_surface_layers`, empty when the theme has no image so a
themeless window paints no second pass of anything.

The Windows-only blur row reused `SettingsBlurDesc`, whose text ends in
"(macOS)". It gets its own key in all three locales, describing the job the
flag actually still has on Windows: feeding the `Auto` material.

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
This commit is contained in:
ARNO
2026-08-09 15:14:54 +08:00
committed by GitHub
co-authored by l0ng-ai
parent e46bcbcf56
commit 61efe27f2d
16 changed files with 1839 additions and 671 deletions
+11
View File
@@ -9,6 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- **Native Windows backdrop materials** — Settings → Appearance now offers a
**Background material** picker on Windows (**Auto / Blur / Mica / Mica Alt /
Acrylic / Off**) that maps onto the OS backdrop APIs: Mica and Mica Alt via
`DWMWA_SYSTEMBACKDROP_TYPE`, Acrylic via the native
`DWMSBT_TRANSIENTWINDOW` material on Windows 11 22H2+ (classic acrylic
before that), and Blur via `ACCENT_ENABLE_ACRYLICBLURBEHIND`. The dropdown
only lists the presets the current Windows build supports, the file
sidebar and right detail panel follow the window opacity so the material
shows through the whole workspace, and the settings panel stays opaque.
macOS and Linux keep the existing blur toggle.
- **Update tty7 without leaving the app** — the launch check and
**Settings → About → Check Now** now offer **Update and Relaunch** instead of
sending the user to GitHub Releases. A dedicated `tty7-updater` helper
Generated
+701 -596
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -142,6 +142,8 @@ windows-sys = { version = "0.61", features = [
"Win32_Foundation",
"Win32_Storage_FileSystem",
"Win32_System_Registry",
"Wdk_System_SystemServices",
"Win32_System_SystemInformation",
# `MessageBeep` (the terminal bell in `terminal::view`) is a user32 export,
# but the Win32 metadata files it under Diagnostics::Debug, so that is the
# module the binding needs — nothing here pulls in dbghelp.
+44
View File
@@ -129,6 +129,8 @@ pub struct Config {
pub theme_legible_palette: bool,
pub window_opacity: Option<f32>,
pub window_blur: Option<bool>,
#[serde(default, deserialize_with = "de_lenient")]
pub window_backdrop: WindowBackdrop,
#[serde(default = "default_true")]
pub dim_inactive_panes: bool,
pub keybindings: HashMap<String, String>,
@@ -334,6 +336,24 @@ pub enum TabBarPosition {
Left,
}
/// Native window backdrop material for the Windows GUI. Other platforms retain
/// the value for config synchronization but do not use it for rendering.
/// `Auto` keeps the legacy behavior where theme blur decides between blurred
/// and plain translucent, `Blur` explicitly requests classic WCA acrylic, the
/// material variants fall back to acrylic or plain translucency on older builds
/// inside `src/ui/theme.rs`, and `Off` never requests a material.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum WindowBackdrop {
#[default]
Auto,
Blur,
Mica,
MicaAlt,
Acrylic,
Off,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum SidebarGrouping {
@@ -439,6 +459,7 @@ impl Default for Config {
theme_legible_palette: true,
window_opacity: None,
window_blur: None,
window_backdrop: WindowBackdrop::default(),
dim_inactive_panes: true,
keybindings: HashMap::new(),
keybinding_preset: default_preset(),
@@ -1103,6 +1124,29 @@ mod tests {
assert_eq!(clamp(Some(f32::NAN)), None);
}
#[test]
fn window_backdrop_defaults_and_round_trips_leniently() {
let cfg = Config::default();
assert_eq!(cfg.window_backdrop, WindowBackdrop::Auto);
let text = serde_json::to_string(&Config {
window_backdrop: WindowBackdrop::MicaAlt,
..Config::default()
})
.unwrap();
assert!(text.contains("\"window_backdrop\":\"mica-alt\""));
let restored: Config = serde_json::from_str(&text).unwrap();
assert_eq!(restored.window_backdrop, WindowBackdrop::MicaAlt);
let blur: Config = serde_json::from_str(r#"{"window_backdrop":"blur"}"#).unwrap();
assert_eq!(blur.window_backdrop, WindowBackdrop::Blur);
// Unknown values fall back to Auto instead of rejecting the whole config.
let lenient: Config = serde_json::from_str(r#"{"window_backdrop":"nope"}"#).unwrap();
assert_eq!(lenient.window_backdrop, WindowBackdrop::Auto);
}
#[test]
fn sanitize_clamps_scrollback_into_band() {
let clamp = |n: usize| {
+259 -27
View File
@@ -16,7 +16,7 @@ use std::sync::Arc;
use crate::core::actions::*;
use crate::core::config::{
Config, CursorStyle as ConfigCursorStyle, NewTabPosition, RightPanelTab, ShellConfig,
TabBarPosition,
TabBarPosition, WindowBackdrop,
};
use crate::core::session::{
Session, SessionAxis, SessionPane, SessionTab, WorkspaceId, WorkspaceStore,
@@ -37,7 +37,7 @@ use crate::ui::presets::Fill;
use crate::ui::settings::{
Recording, SettingsSection, SettingsState, ThemeEditor, humanize_action,
};
use crate::ui::theme::{apply_theme, set_menus, window_background};
use crate::ui::theme::{apply_theme, set_menus};
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum ThemeEdit {
@@ -448,6 +448,103 @@ impl TabAgentSession {
}
}
/// Maps a backdrop onto the settings dropdown. The dropdown lists the
/// presets the current Windows build supports, plus the stored value even
/// when unsupported here (see `theme::backdrop_options`), so the label
/// always matches what the window actually resolves to.
#[cfg(target_os = "windows")]
fn window_backdrop_index(backdrop: WindowBackdrop) -> usize {
crate::ui::theme::backdrop_options(backdrop)
.iter()
.position(|candidate| *candidate == backdrop)
.unwrap_or(0)
}
#[cfg(target_os = "windows")]
fn window_backdrop_from_index(idx: usize, current: WindowBackdrop) -> WindowBackdrop {
crate::ui::theme::backdrop_options(current)
.get(idx)
.copied()
.unwrap_or(WindowBackdrop::Auto)
}
#[cfg(target_os = "windows")]
fn window_backdrop_label_key(backdrop: WindowBackdrop) -> L10nKey {
match backdrop {
WindowBackdrop::Auto => L10nKey::SettingsBackdropAuto,
WindowBackdrop::Blur => L10nKey::SettingsBackdropBlur,
WindowBackdrop::Mica => L10nKey::SettingsBackdropMica,
WindowBackdrop::MicaAlt => L10nKey::SettingsBackdropMicaAlt,
WindowBackdrop::Acrylic => L10nKey::SettingsBackdropAcrylic,
WindowBackdrop::Off => L10nKey::SettingsBackdropOff,
}
}
#[cfg(target_os = "windows")]
fn window_backdrop_labels(backdrop: WindowBackdrop) -> Vec<String> {
crate::ui::theme::backdrop_options(backdrop)
.iter()
.map(|backdrop| t(window_backdrop_label_key(*backdrop)).to_string())
.collect()
}
/// What a full-window overlay (settings, the opened file, the diff view)
/// paints between its own fill and its content.
///
/// Those overlays fill opaquely on purpose, so the OS backdrop cannot show
/// through their text — but that fill also sits on top of the background
/// image the workspace root paints, and would erase it for as long as an
/// overlay is open. So each one repaints the image, then the workspace's own
/// translucent fill over it. That second layer is what keeps the overlay
/// readable: it dims the image to exactly the strength it had when these
/// overlays were themselves translucent, before they were made opaque.
///
/// Empty when the theme has no image — then the opaque fill alone is already
/// what the overlay wants, and a second pass of the same paint buys nothing.
pub(crate) fn overlay_surface_layers(cx: &App) -> Vec<gpui::Div> {
match window_background_image_layer(cx) {
Some(image) => vec![
image,
div()
.absolute()
.inset_0()
.bg(crate::ui::theme::workspace_background(cx)),
],
None => Vec::new(),
}
}
/// The theme's background image as a full-bleed layer.
pub(crate) fn window_background_image_layer(cx: &App) -> Option<gpui::Div> {
let image = cx
.try_global::<crate::ui::presets::ActiveBackground>()?
.image
.clone()?;
Some(
div()
.absolute()
.inset_0()
.overflow_hidden()
.opacity(image.opacity)
.child(
img(image.path)
.size_full()
.object_fit(gpui::ObjectFit::Cover),
),
)
}
/// Clears the window overrides that are effective on the current platform.
/// `backdrop_is_local` models whether the Windows-only backdrop participates
/// in this platform's rendering and therefore belongs to its reset operation.
fn clear_window_override_values(config: &mut Config, backdrop_is_local: bool) {
config.window_opacity = None;
config.window_blur = None;
if backdrop_is_local {
config.window_backdrop = WindowBackdrop::Auto;
}
}
impl Tty7App {
pub fn for_workspace(
id: Option<WorkspaceId>,
@@ -1454,7 +1551,10 @@ impl Tty7App {
pub(crate) fn effective_window_opacity(cx: &App) -> f32 {
let config = cx.global::<Config>();
let theme = crate::ui::presets::by_id(cx, &crate::ui::theme::effective_preset_id(cx));
config.window_opacity.or(theme.opacity).unwrap_or(1.0)
let blur = config.window_blur.unwrap_or(theme.blur);
config.window_opacity.or(theme.opacity).unwrap_or_else(|| {
crate::ui::theme::default_window_opacity(config.window_backdrop, blur)
})
}
pub(crate) fn set_window_opacity(
@@ -1481,15 +1581,37 @@ impl Tty7App {
cx.notify();
}
#[cfg(target_os = "windows")]
pub(crate) fn set_window_backdrop(
&mut self,
backdrop: WindowBackdrop,
window: &mut Window,
cx: &mut Context<Self>,
) {
cx.global_mut::<Config>().window_backdrop = backdrop;
apply_theme(Some(window), cx);
cx.global::<Config>().save();
// A material changes the default opacity (SYSTEM_MATERIAL_OPACITY
// vs 1.0), so the slider must track the new effective value.
self.sync_window_opacity_slider(window, cx);
// Rebuild the rows as well as the selected index. The previous value
// may have been an unsupported preset retained only for cross-machine
// config sync, and must disappear after the user selects a supported
// preset on this machine.
self.sync_window_backdrop_select(window, cx);
cx.notify();
}
pub(crate) fn reset_window_overrides(&mut self, window: &mut Window, cx: &mut Context<Self>) {
{
let config = cx.global_mut::<Config>();
config.window_opacity = None;
config.window_blur = None;
clear_window_override_values(config, cfg!(target_os = "windows"));
}
apply_theme(Some(window), cx);
cx.global::<Config>().save();
self.sync_window_opacity_slider(window, cx);
#[cfg(target_os = "windows")]
self.sync_window_backdrop_select(window, cx);
cx.notify();
}
@@ -1507,6 +1629,28 @@ impl Tty7App {
}
}
#[cfg(target_os = "windows")]
pub(crate) fn sync_window_backdrop_select(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(select) = self
.active_settings()
.map(|s| s.window_backdrop_select.clone())
{
let current = cx.global::<Config>().window_backdrop;
let rows = window_backdrop_labels(current);
let selected = window_backdrop_index(current);
select.update(cx, |state, cx| {
state.set_items(SearchableVec::new(rows), window, cx);
// Replacing the delegate clears its selection snapshot, so
// restore the stored value after installing the new rows.
state.set_selected_index(Some(IndexPath::default().row(selected)), window, cx);
});
}
}
pub(crate) fn pick_theme_image(&mut self, cx: &mut Context<Self>) {
let rx = cx.prompt_for_paths(gpui::PathPromptOptions {
files: true,
@@ -3635,6 +3779,8 @@ impl Tty7App {
let (font_select, font_bold_select, font_italic_select) =
self.build_font_selects(&mut subs, window, cx);
let language_select = self.build_language_select(&mut subs, window, cx);
#[cfg(target_os = "windows")]
let window_backdrop_select = self.build_window_backdrop_select(&mut subs, window, cx);
let (shell_program_input, shell_args_input, wd_path_input) =
self.build_shell_inputs(&mut subs, window, cx);
let link_file_command_input = self.build_link_file_command_input(&mut subs, window, cx);
@@ -3693,6 +3839,8 @@ impl Tty7App {
font_bold_select,
font_italic_select,
language_select,
#[cfg(target_os = "windows")]
window_backdrop_select,
shell_program_input,
shell_args_input,
wd_path_input,
@@ -3865,6 +4013,46 @@ impl Tty7App {
.unwrap_or_else(crate::ui::i18n::default_language_code)
}
/// The backdrop dropdown only lists the presets this Windows build
/// supports, in the order of `theme::supported_backdrops`; the select
/// resolves the picked label back through that same list.
#[cfg(target_os = "windows")]
fn build_window_backdrop_select(
&mut self,
subs: &mut Vec<Subscription>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Entity<SelectState<SearchableVec<String>>> {
let rows = window_backdrop_labels(cx.global::<Config>().window_backdrop);
let selected = window_backdrop_index(cx.global::<Config>().window_backdrop);
let select = cx.new(|cx| {
SelectState::new(
SearchableVec::new(rows),
Some(IndexPath::default().row(selected)),
window,
cx,
)
});
subs.push(cx.subscribe_in(
&select,
window,
move |this, _select, ev: &SelectEvent<SearchableVec<String>>, window, cx| {
if let SelectEvent::Confirm(Some(label)) = ev {
let current = cx.global::<Config>().window_backdrop;
let rows = window_backdrop_labels(current);
if let Some(idx) = rows.iter().position(|row| row == label) {
this.set_window_backdrop(
window_backdrop_from_index(idx, current),
window,
cx,
);
}
}
},
));
select
}
pub(crate) fn set_gui_language(
&mut self,
code: &'static str,
@@ -3904,6 +4092,21 @@ impl Tty7App {
.unwrap_or(0);
state.set_selected_index(Some(IndexPath::default().row(selected)), window, cx);
});
#[cfg(target_os = "windows")]
s.window_backdrop_select.update(cx, |state, cx| {
let current = cx.global::<Config>().window_backdrop;
let rows = window_backdrop_labels(current);
state.set_items(SearchableVec::new(rows), window, cx);
// `set_items` does not preserve the selection; restore the
// index of the stored value so a locale refresh (which
// re-translates the labels) cannot leave the dropdown
// showing no — or the wrong — selection.
state.set_selected_index(
Some(IndexPath::default().row(window_backdrop_index(current))),
window,
cx,
);
});
s.search.update(cx, |state, cx| {
state.set_placeholder(t(L10nKey::SearchSettings), window, cx)
});
@@ -4240,6 +4443,13 @@ impl Tty7App {
fn reload_from_config(&mut self, window: &mut Window, cx: &mut Context<Self>) {
apply_theme(Some(window), cx);
self.sync_window_opacity_slider(window, cx);
// Another window — or a hand edit / config sync picked up by the
// `Config` watcher — can change the backdrop while this window's
// settings panel is open. The window itself already switched
// material above, so the dropdown has to follow or it contradicts
// what it describes.
#[cfg(target_os = "windows")]
self.sync_window_backdrop_select(window, cx);
let config = cx.global::<Config>().clone();
if config.cursor_style != self.terminal_cursor_style
|| config.scrollback_limit != self.terminal_scrollback_limit
@@ -5310,7 +5520,7 @@ impl Render for Tty7App {
.bottom_0()
.right_0()
.w(px(self.right_panel_px(window, cx)))
.bg(cx.theme().sidebar)
.bg(crate::ui::theme::workspace_surface_color(cx))
.border_l_1()
.border_color(cx.theme().sidebar_border),
)
@@ -5331,17 +5541,21 @@ impl Render for Tty7App {
})
.into_any_element();
let (window_bg, bg_image) = match cx.try_global::<crate::ui::presets::ActiveBackground>() {
Some(bg) => (window_background(bg), bg.image.clone()),
None => (cx.theme().background.into(), None),
};
let window_bg = crate::ui::theme::workspace_background(cx);
let bg_image = window_background_image_layer(cx);
let settings_bg = crate::ui::theme::overlay_background(cx);
let settings_overlay = self.settings.is_some().then(|| {
div()
.absolute()
.inset_0()
.occlude()
.bg(window_bg)
// Opaque on purpose: the settings panel must never let the
// workspace translucency (window opacity / backdrop material)
// show through, even at window edges during a resize. The
// preset's gradient fill is preserved, just with alpha 1;
// `render_settings` repaints the theme image over it.
.bg(settings_bg)
.child(self.render_settings(window, cx))
});
@@ -5606,20 +5820,7 @@ impl Render for Tty7App {
)
.on_action(cx.listener(|_, _: &OpenDiscord, _window, cx| cx.open_url(DISCORD_URL)))
.on_action(cx.listener(|_, _: &ReportIssue, _window, cx| cx.open_url(ISSUES_URL)))
.when_some(bg_image, |this, image| {
this.child(
div()
.absolute()
.inset_0()
.overflow_hidden()
.opacity(image.opacity)
.child(
img(image.path)
.size_full()
.object_fit(gpui::ObjectFit::Cover),
),
)
})
.children(bg_image)
.child(main_layout)
.when_some(settings_overlay, |this, overlay| this.child(overlay))
.children(self.render_switcher(cx))
@@ -6466,10 +6667,41 @@ mod window_drag_tests {
#[cfg(test)]
mod tests {
use super::{
TabAgentSession, leaf_shares_the_window_daemon, mru_order, pane_attachable,
parse_ssh_connect_input, parse_ssh_option_words,
TabAgentSession, clear_window_override_values, leaf_shares_the_window_daemon, mru_order,
pane_attachable, parse_ssh_connect_input, parse_ssh_option_words,
};
#[test]
fn non_windows_reset_preserves_the_synced_windows_backdrop() {
let mut config = crate::core::config::Config::default();
config.window_opacity = Some(0.8);
config.window_blur = Some(true);
config.window_backdrop = crate::core::config::WindowBackdrop::Mica;
clear_window_override_values(&mut config, false);
assert_eq!(config.window_opacity, None);
assert_eq!(config.window_blur, None);
assert_eq!(
config.window_backdrop,
crate::core::config::WindowBackdrop::Mica,
"an inert synchronized backdrop is not a local override to reset"
);
}
#[test]
fn windows_reset_clears_the_local_backdrop_override() {
let mut config = crate::core::config::Config::default();
config.window_backdrop = crate::core::config::WindowBackdrop::Acrylic;
clear_window_override_values(&mut config, true);
assert_eq!(
config.window_backdrop,
crate::core::config::WindowBackdrop::Auto
);
}
#[test]
fn mru_puts_the_active_tab_first_and_the_last_one_used_behind_it() {
// Tab 2 is active; 0 was used most recently before it, then 1.
+9 -1
View File
@@ -918,12 +918,20 @@ impl Tty7App {
.absolute()
.inset_0()
.occlude()
.bg(cx.theme().background)
// Opaque on purpose: this overlay covers the whole workspace
// (everything but the detail panel) and an open file must never
// let the window translucency / backdrop material show through
// it. The preset's gradient fill is preserved, just with
// alpha 1 — the same paint the settings overlay uses. The
// theme background image is repainted on top of it, since the
// root's copy now sits below this fill.
.bg(crate::ui::theme::overlay_background(cx))
.on_key_down(cx.listener(|this, ev: &gpui::KeyDownEvent, window, cx| {
if ev.keystroke.key == "escape" {
this.toggle_code_panel(window, cx);
}
}))
.children(crate::ui::app::overlay_surface_layers(cx))
.child(h_flex().flex_1().min_h_0().w_full().child(editor_col))
.child(self.render_code_status_bar(window, cx))
.into_any_element(),
+50 -7
View File
@@ -3,7 +3,8 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;
use gpui::{
AnyElement, FocusHandle, FontWeight, KeyDownEvent, Pixels, Window, div, prelude::*, px,
AnyElement, Background, FocusHandle, FontWeight, Hsla, KeyDownEvent, Pixels, Window, div,
prelude::*, px,
};
use gpui_component::button::Button;
use gpui_component::{ActiveTheme as _, Icon, IconName, Sizable as _, h_flex, v_flex};
@@ -33,6 +34,19 @@ pub(crate) struct DiffOverlayState {
pub(crate) focus: Option<String>,
}
/// Paints the full-window diff surface without inheriting workspace opacity.
/// The preset's solid or gradient design remains intact, but neither it nor
/// the plain theme fallback may reveal the OS backdrop through diff text.
fn diff_overlay_background(
active: Option<&crate::ui::presets::ActiveBackground>,
fallback: Hsla,
) -> Background {
match active {
Some(bg) => crate::ui::theme::window_background_opaque(bg),
None => fallback.alpha(1.0).into(),
}
}
impl Tty7App {
pub(crate) fn toggle_diff_overlay(
&mut self,
@@ -262,12 +276,12 @@ impl Tty7App {
.absolute()
.inset_0()
.occlude()
.bg(
match cx.try_global::<crate::ui::presets::ActiveBackground>() {
Some(bg) => crate::ui::theme::window_background(bg),
None => cx.theme().background.into(),
},
)
// Opaque on purpose: this overlay covers the entire workspace,
// so window translucency and backdrop material must stop here.
.bg(diff_overlay_background(
cx.try_global::<crate::ui::presets::ActiveBackground>(),
cx.theme().background,
))
.text_color(cx.theme().foreground)
.track_focus(&overlay.focus_handle)
.on_key_down(cx.listener(|this, ev: &KeyDownEvent, window, cx| {
@@ -275,6 +289,11 @@ impl Tty7App {
this.close_diff_overlay(window, cx);
}
}))
// The opaque fill above covers the theme background image the
// workspace root paints, so the overlay carries its own copy,
// dimmed back to the strength it had when this overlay was
// itself translucent.
.children(crate::ui::app::overlay_surface_layers(cx))
.child(header)
.child(content)
.into_any_element(),
@@ -941,6 +960,30 @@ mod tests {
use crate::terminal::git_diff::{DiffLine, LineKind};
use crate::ui::i18n::set_locale;
#[test]
fn full_window_diff_background_is_opaque_with_or_without_a_preset() {
let active = crate::ui::presets::ActiveBackground {
fill: crate::ui::presets::Fill::Solid(0x12_34_56),
opacity: Some(0.2),
image: None,
};
let mut fallback: Hsla = gpui::rgb(0x65_43_21).into();
fallback.a = 0.3;
let mut opaque_fallback = fallback;
opaque_fallback.a = 1.0;
assert_eq!(
diff_overlay_background(Some(&active), fallback),
crate::ui::theme::window_background_opaque(&active),
"the active preset must keep its fill while discarding workspace translucency"
);
assert_eq!(
diff_overlay_background(None, fallback),
opaque_fallback.into(),
"the theme fallback must also block the window material"
);
}
fn line(kind: LineKind, old: Option<u32>, new: Option<u32>, text: &str) -> DiffLine {
DiffLine {
kind,
+16
View File
@@ -114,6 +114,22 @@ pub fn translate_en(key: L10nKey) -> &'static str {
}
L10nKey::SettingsBlur => "Blur",
L10nKey::SettingsBlurDesc => "Blur whatever is behind a translucent window (macOS).",
L10nKey::SettingsBlurAutoDesc => {
"Blur whatever is behind a translucent window. Only applies while Background material is Auto."
}
L10nKey::SettingsBackdrop => "Background material",
L10nKey::SettingsBackdropDesc => {
"Native Windows backdrop behind a translucent window. Mica needs Windows 11 22H2, Acrylic needs 1809; older builds fall back automatically."
}
L10nKey::SettingsSearchBackdropKeywords => {
"material backdrop mica acrylic blur frosted window background"
}
L10nKey::SettingsBackdropAuto => "Auto",
L10nKey::SettingsBackdropBlur => "Blur",
L10nKey::SettingsBackdropMica => "Mica",
L10nKey::SettingsBackdropMicaAlt => "Mica Alt",
L10nKey::SettingsBackdropAcrylic => "Acrylic",
L10nKey::SettingsBackdropOff => "Off",
L10nKey::FollowTheme => "Follow theme",
L10nKey::SettingsDimInactivePanes => "Dim inactive panes",
L10nKey::SettingsDimInactivePanesDesc => {
+16
View File
@@ -118,6 +118,22 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
}
L10nKey::SettingsBlur => "背景のぼかし",
L10nKey::SettingsBlurDesc => "半透明ウィンドウの背後にあるものをぼかす(macOS)",
L10nKey::SettingsBlurAutoDesc => {
"半透明ウィンドウの背後にあるものをぼかす。背景マテリアルが「自動」のときのみ有効です"
}
L10nKey::SettingsBackdrop => "背景マテリアル",
L10nKey::SettingsBackdropDesc => {
"半透明ウィンドウの背後にあるネイティブ Windows 背景マテリアル。Mica には Windows 11 22H2、Acrylic には 1809 が必要です。古いビルドでは自動的にフォールバックします"
}
L10nKey::SettingsSearchBackdropKeywords => {
"背景 マテリアル ぼかし すりガラス material backdrop mica acrylic blur frosted window background"
}
L10nKey::SettingsBackdropAuto => "自動",
L10nKey::SettingsBackdropBlur => "ぼかし",
L10nKey::SettingsBackdropMica => "Mica",
L10nKey::SettingsBackdropMicaAlt => "Mica Alt",
L10nKey::SettingsBackdropAcrylic => "Acrylic",
L10nKey::SettingsBackdropOff => "オフ",
L10nKey::FollowTheme => "テーマに従う",
L10nKey::SettingsDimInactivePanes => "非アクティブなペインを暗くする",
L10nKey::SettingsDimInactivePanesDesc => {
+20
View File
@@ -149,6 +149,16 @@ pub enum L10nKey {
SettingsOpacityDesc,
SettingsBlur,
SettingsBlurDesc,
SettingsBlurAutoDesc,
SettingsBackdrop,
SettingsBackdropDesc,
SettingsSearchBackdropKeywords,
SettingsBackdropAuto,
SettingsBackdropBlur,
SettingsBackdropMica,
SettingsBackdropMicaAlt,
SettingsBackdropAcrylic,
SettingsBackdropOff,
FollowTheme,
SettingsDimInactivePanes,
SettingsDimInactivePanesDesc,
@@ -1195,6 +1205,16 @@ mod tests {
L10nKey::SettingsOpacityDesc,
L10nKey::SettingsBlur,
L10nKey::SettingsBlurDesc,
L10nKey::SettingsBlurAutoDesc,
L10nKey::SettingsBackdrop,
L10nKey::SettingsBackdropDesc,
L10nKey::SettingsSearchBackdropKeywords,
L10nKey::SettingsBackdropAuto,
L10nKey::SettingsBackdropBlur,
L10nKey::SettingsBackdropMica,
L10nKey::SettingsBackdropMicaAlt,
L10nKey::SettingsBackdropAcrylic,
L10nKey::SettingsBackdropOff,
L10nKey::FollowTheme,
L10nKey::SettingsDimInactivePanes,
L10nKey::SettingsDimInactivePanesDesc,
+16
View File
@@ -104,6 +104,22 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
}
L10nKey::SettingsBlur => "模糊",
L10nKey::SettingsBlurDesc => "模糊半透明窗口背后的内容(macOS)。",
L10nKey::SettingsBlurAutoDesc => {
"模糊半透明窗口背后的内容。仅在「背景材质」为「自动」时生效。"
}
L10nKey::SettingsBackdrop => "背景材质",
L10nKey::SettingsBackdropDesc => {
"半透明窗口背后的原生 Windows 背景材质。云母需要 Windows 11 22H2,亚克力需要 1809;更旧的系统会自动回退。"
}
L10nKey::SettingsSearchBackdropKeywords => {
"材质 背景 云母 亚克力 模糊 毛玻璃 窗口 material backdrop mica acrylic blur frosted window background"
}
L10nKey::SettingsBackdropAuto => "自动",
L10nKey::SettingsBackdropBlur => "模糊",
L10nKey::SettingsBackdropMica => "云母",
L10nKey::SettingsBackdropMicaAlt => "云母 Alt",
L10nKey::SettingsBackdropAcrylic => "亚克力",
L10nKey::SettingsBackdropOff => "关闭",
L10nKey::FollowTheme => "跟随主题",
L10nKey::SettingsDimInactivePanes => "调暗非活动窗格",
L10nKey::SettingsDimInactivePanesDesc => "在分屏中淡化未聚焦的窗格,让活动窗格更突出。",
+1 -1
View File
@@ -97,7 +97,7 @@ impl Tty7App {
.w(px(width))
.h_full()
.child(backing)
.bg(cx.theme().sidebar)
.bg(crate::ui::theme::workspace_surface_color(cx))
.border_l_1()
.border_color(cx.theme().sidebar_border)
.children(cfg!(target_os = "macos").then(|| {
+141 -19
View File
@@ -1,7 +1,7 @@
use gpui::{
AnyElement, App, Context, Div, Entity, FontWeight, Image, ImageFormat, KeyDownEvent,
MouseButton, SharedString, Stateful, Subscription, Window, div, img, prelude::*, px, relative,
rgb,
AnyElement, App, Background, Context, Div, Entity, FontWeight, Image, ImageFormat,
KeyDownEvent, MouseButton, SharedString, Stateful, Subscription, Window, div, img, prelude::*,
px, relative, rgb,
};
use gpui_component::InteractiveElementExt as _;
use gpui_component::button::{Button, ButtonVariants as _};
@@ -22,6 +22,7 @@ use uuid::Uuid;
use crate::core::config::{
BellMode, Config, CursorStyle, NewTabPosition, NotifyMode, TabBarPosition, UpdateChannel,
WindowBackdrop,
};
use crate::core::keychain::CredentialRef;
use crate::core::ssh_profile::{
@@ -41,6 +42,15 @@ fn settings_row_id(label: &str, _desc: &str) -> SharedString {
SharedString::from(format!("settings-row-{label}"))
}
/// Whether the reset control has any effective override to clear on this
/// platform. A synchronized Windows backdrop remains stored elsewhere but is
/// inert here, so only platforms that expose it locally may count it.
fn window_overrides_active(config: &Config, backdrop_is_local: bool) -> bool {
config.window_opacity.is_some()
|| config.window_blur.is_some()
|| (backdrop_is_local && config.window_backdrop != WindowBackdrop::Auto)
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum SettingsSection {
Appearance,
@@ -126,6 +136,12 @@ fn settings_search_entries() -> &'static [SearchEntry] {
title: SettingsBlur,
keywords: SettingsSearchBlurKeywords,
},
#[cfg(target_os = "windows")]
SearchEntry {
section: Appearance,
title: SettingsBackdrop,
keywords: SettingsSearchBackdropKeywords,
},
SearchEntry {
section: Appearance,
title: SettingsDimInactivePanes,
@@ -437,6 +453,8 @@ pub(crate) struct SettingsState {
pub(crate) font_bold_select: Entity<SelectState<SearchableVec<String>>>,
pub(crate) font_italic_select: Entity<SelectState<SearchableVec<String>>>,
pub(crate) language_select: Entity<SelectState<SearchableVec<String>>>,
#[cfg(target_os = "windows")]
pub(crate) window_backdrop_select: Entity<SelectState<SearchableVec<String>>>,
pub(crate) shell_program_input: Entity<InputState>,
pub(crate) shell_args_input: Entity<InputState>,
pub(crate) wd_path_input: Entity<InputState>,
@@ -737,8 +755,18 @@ impl Tty7App {
cx: &mut Context<Self>,
) -> impl IntoElement + use<> {
let theme = cx.theme();
let (background, foreground, header_muted) =
(theme.background, theme.foreground, theme.muted_foreground);
// The settings panel covers the whole window. Paint it on an opaque
// surface so the workspace translucency (window opacity / backdrop
// material) never shows through the settings UI — while keeping the
// preset's gradient fill instead of collapsing to a flat color.
let background: Background = crate::ui::theme::overlay_background(cx);
// That opaque fill also covers the theme background image the
// workspace root paints, so the panel carries its own copy (dimmed by
// the workspace fill, or the settings text would sit straight on the
// wallpaper); without it the image would blink out for as long as
// settings is open.
let background_layers = crate::ui::app::overlay_surface_layers(cx);
let (foreground, header_muted) = (theme.foreground, theme.muted_foreground);
let (focus_handle, section, theme_panel_open, search) = match self.active_settings() {
Some(s) => (
@@ -870,13 +898,15 @@ impl Tty7App {
SettingsSection::About => self.render_settings_about(cx),
};
// No fill of its own: the root already paints the opaque surface and
// the background image behind it, and repainting here would hide the
// image again in the one pane that fills most of the panel.
let content_pane = if section == SettingsSection::Ssh {
v_flex()
.id("settings-content")
.flex_1()
.min_w_0()
.h_full()
.bg(background)
.child(content)
} else {
v_flex()
@@ -884,7 +914,6 @@ impl Tty7App {
.flex_1()
.min_w_0()
.h_full()
.bg(background)
.overflow_y_scroll()
.child(
div()
@@ -907,6 +936,7 @@ impl Tty7App {
this.close_settings(window, cx);
}
}))
.children(background_layers)
.child(sidebar)
.child(content_pane)
.child(
@@ -1332,11 +1362,9 @@ impl Tty7App {
return div().into_any_element();
};
let config = cx.global::<Config>();
let overridden = config.window_opacity.is_some() || config.window_blur.is_some();
let overridden = window_overrides_active(config, cfg!(target_os = "windows"));
let dim_inactive_panes = config.dim_inactive_panes;
let theme = presets::by_id(cx, &crate::ui::theme::effective_preset_id(cx));
let opacity = Tty7App::effective_window_opacity(cx);
let blur = cx.global::<Config>().window_blur.unwrap_or(theme.blur);
let opacity_control = h_flex()
.items_center()
@@ -1354,12 +1382,69 @@ impl Tty7App {
.child(format!("{:.0}%", opacity * 100.)),
)
.into_any_element();
let blur_switch = crate::ui::theme::switch("window-blur", cx)
.checked(blur)
.on_click(
cx.listener(|this, on: &bool, window, cx| this.set_window_blur(*on, window, cx)),
// Windows exposes the native backdrop materials directly; macOS keeps
// the simple blur toggle, which drives its vibrancy.
#[cfg(target_os = "windows")]
let blur_control = {
// Both selects come from the same SettingsState resolved at the
// top of this function (window_opacity_slider), so the None arm
// is unreachable today; fall back to an empty control rather
// than returning from the whole section — a missing select must
// never silently drop the opacity slider and the rest.
match self
.active_settings()
.map(|s| s.window_backdrop_select.clone())
{
Some(select) => Select::new(&select)
.small()
.w(px(180.))
.h(px(24.))
.menu_max_h(px(224.))
.into_any_element(),
None => div().into_any_element(),
}
};
#[cfg(not(target_os = "windows"))]
let blur_control =
{
let theme = presets::by_id(cx, &crate::ui::theme::effective_preset_id(cx));
let blur = config.window_blur.unwrap_or(theme.blur);
crate::ui::theme::switch("window-blur", cx)
.checked(blur)
.on_click(cx.listener(|this, on: &bool, window, cx| {
this.set_window_blur(*on, window, cx)
}))
.into_any_element()
};
// `Auto` is the one backdrop that still defers to the legacy blur
// flag, which is shared with the other platforms' vibrancy switch and
// travels with a synced config. Offer that switch here exactly when it
// has an effect — otherwise a stored `window_blur: true` would blur
// the window with no visible control to clear it, short of the reset
// button, which also discards the user's opacity.
#[cfg(target_os = "windows")]
let auto_blur_row = (config.window_backdrop == WindowBackdrop::Auto).then(|| {
let theme = presets::by_id(cx, &crate::ui::theme::effective_preset_id(cx));
let blur = config.window_blur.unwrap_or(theme.blur);
let control =
crate::ui::theme::switch("window-blur", cx)
.checked(blur)
.on_click(cx.listener(|this, on: &bool, window, cx| {
this.set_window_blur(*on, window, cx)
}))
.into_any_element();
self.settings_row(
t(L10nKey::SettingsBlur),
// Not `SettingsBlurDesc` — that one says "(macOS)", which is
// exactly wrong here. This row explains the flag's one
// remaining job on Windows: feeding the `Auto` material.
t(L10nKey::SettingsBlurAutoDesc),
control,
cx,
)
.into_any_element();
});
#[cfg(not(target_os = "windows"))]
let auto_blur_row: Option<Stateful<Div>> = None;
let dim_switch = crate::ui::theme::switch("dim-inactive-panes", cx)
.checked(dim_inactive_panes)
.on_click(cx.listener(|this, on: &bool, _w, cx| this.set_dim_inactive_panes(*on, cx)))
@@ -1374,11 +1459,20 @@ impl Tty7App {
cx,
))
.child(self.settings_row(
t(L10nKey::SettingsBlur),
t(L10nKey::SettingsBlurDesc),
blur_switch,
t(if cfg!(target_os = "windows") {
L10nKey::SettingsBackdrop
} else {
L10nKey::SettingsBlur
}),
t(if cfg!(target_os = "windows") {
L10nKey::SettingsBackdropDesc
} else {
L10nKey::SettingsBlurDesc
}),
blur_control,
cx,
))
.children(auto_blur_row)
.when(overridden, |this| {
this.child(
h_flex().mt_2().child(
@@ -5129,6 +5223,28 @@ mod tests {
);
}
#[test]
fn synced_windows_backdrop_is_only_a_local_override_on_windows() {
let mut config = Config::default();
config.window_backdrop = WindowBackdrop::MicaAlt;
assert!(window_overrides_active(&config, true));
assert!(!window_overrides_active(&config, false));
}
#[test]
fn opacity_and_blur_are_local_overrides_on_every_platform() {
let mut opacity = Config::default();
opacity.window_opacity = Some(0.8);
opacity.window_backdrop = WindowBackdrop::Mica;
let mut blur = Config::default();
blur.window_blur = Some(true);
blur.window_backdrop = WindowBackdrop::Acrylic;
assert!(window_overrides_active(&opacity, false));
assert!(window_overrides_active(&blur, false));
}
#[test]
fn every_section_has_search_entries() {
for section in SettingsSection::ALL {
@@ -5164,7 +5280,7 @@ mod tests {
#[test]
fn previously_unsearchable_settings_are_findable() {
use SettingsSection::*;
let cases: &[(&str, SettingsSection)] = &[
let mut cases: Vec<(&str, SettingsSection)> = vec![
("opacity", Appearance),
("blur", Appearance),
("completion", Input),
@@ -5179,6 +5295,12 @@ mod tests {
("claude", Agents),
("symlink", Agents),
];
#[cfg(target_os = "windows")]
cases.extend([
("material", Appearance),
("mica", Appearance),
("acrylic", Appearance),
]);
for (query, expected) in cases {
assert_eq!(
best_matching_section(query).map(|s| s.profile_label()),
+4 -2
View File
@@ -1333,7 +1333,6 @@ impl Tty7App {
let danger = cx.theme().danger;
let accent = cx.theme().accent;
let border = cx.theme().border;
let sidebar = cx.theme().sidebar;
let hover = gpui::rgb(cx.global::<crate::ui::presets::Surfaces>().sidebar.hover);
let expanded = self.sftp_panel.tray_expanded || history;
@@ -1452,11 +1451,14 @@ impl Tty7App {
});
Some(
// No fill: the tray is a child of the right panel, which already
// paints `workspace_surface_color`. Painting it again stacked a
// second src-over pass of the same translucent surface and left a
// visibly darker band with a hard seam under a backdrop material.
v_flex()
.flex_none()
.border_t_1()
.border_color(border)
.bg(sidebar)
.child(head)
.when(running > 0 && !expanded, |this| this.child(underline))
.children(body)
+1 -1
View File
@@ -679,7 +679,7 @@ impl Tty7App {
.flex_shrink_0()
.w(px(width))
.h_full()
.bg(cx.theme().sidebar)
.bg(crate::ui::theme::workspace_surface_color(cx))
.border_r_1()
.border_color(cx.theme().sidebar_border)
.child(backing)
+548 -17
View File
@@ -3,10 +3,10 @@ use gpui::{
WindowBackgroundAppearance, linear_color_stop, linear_gradient, point, px, rgb,
};
use gpui_component::scroll::ScrollbarShow;
use gpui_component::{Theme, ThemeMode};
use gpui_component::{ActiveTheme, Theme, ThemeMode};
use crate::core::actions::*;
use crate::core::config::Config;
use crate::core::config::{Config, WindowBackdrop};
use crate::terminal::view::{
ClearScrollback, CopyText, CutText, FindInTerminal, FindNext, FindPrevious, PasteText,
RedoEdit, SelectAll, UndoEdit,
@@ -14,6 +14,8 @@ use crate::terminal::view::{
use crate::ui::i18n::{L10nKey, t};
use crate::ui::presets;
use crate::ui::presets::Fill;
#[cfg(target_os = "windows")]
use std::sync::OnceLock;
pub(crate) fn traffic_light_position() -> Point<Pixels> {
point(px(9.), px(13.))
@@ -164,7 +166,40 @@ fn window_menu_items(cx: &App) -> Vec<MenuItem> {
}
pub(crate) fn window_background(bg: &presets::ActiveBackground) -> Background {
let alpha = bg.opacity.unwrap_or(1.0);
window_background_with_alpha(bg, bg.opacity.unwrap_or(1.0))
}
/// The same preset fill with the alpha channel forced to 1 — used by the
/// settings overlay, which must stay opaque (workspace translucency must
/// never show through it) while still rendering the preset's gradient
/// design instead of collapsing to a flat solid color.
pub(crate) fn window_background_opaque(bg: &presets::ActiveBackground) -> Background {
window_background_with_alpha(bg, 1.0)
}
/// The workspace's own fill: the preset at whatever alpha the window opacity
/// and backdrop material asked for.
pub(crate) fn workspace_background(cx: &App) -> Background {
match cx.try_global::<presets::ActiveBackground>() {
Some(bg) => window_background(bg),
None => cx.theme().background.into(),
}
}
/// The fill for a full-window overlay (settings, the opened file, the diff
/// view). Always opaque: the overlay covers the whole workspace, so window
/// translucency and the backdrop material must stop at it instead of showing
/// desktop through its text. Overlays pair this with
/// `app::overlay_surface_layers`, because their own opaque fill hides the
/// theme background image the workspace root paints beneath them.
pub(crate) fn overlay_background(cx: &App) -> Background {
match cx.try_global::<presets::ActiveBackground>() {
Some(bg) => window_background_opaque(bg),
None => cx.theme().background.alpha(1.0).into(),
}
}
fn window_background_with_alpha(bg: &presets::ActiveBackground, alpha: f32) -> Background {
let stop = |c: u32| -> Hsla {
let mut h: Hsla = rgb(c).into();
h.a = alpha;
@@ -225,14 +260,266 @@ pub(crate) fn effective_preset_id(cx: &App) -> String {
}
}
/// Default background alpha used while a backdrop material is active and
/// neither the theme nor the config overrides the opacity — a fully opaque
/// fill would hide the material behind it.
pub(crate) const SYSTEM_MATERIAL_OPACITY: f32 = 0.82;
/// Maps the requested backdrop to the gpui window appearance, falling back
/// down the chain when the OS build predates a material: Mica needs Windows
/// 11 22H2 (build 22621), acrylic needs 1809 (build 17763); below that plain
/// translucency is the best Windows can do. Pure function so the fallback
/// table is unit-testable on every platform.
pub(crate) fn windows_background_appearance(
backdrop: WindowBackdrop,
blur: bool,
build: u32,
) -> WindowBackgroundAppearance {
match backdrop {
WindowBackdrop::Auto => {
if blur {
WindowBackgroundAppearance::Blurred
} else {
WindowBackgroundAppearance::Transparent
}
}
WindowBackdrop::Blur => {
if build >= 17_763 {
WindowBackgroundAppearance::Blurred
} else {
WindowBackgroundAppearance::Transparent
}
}
WindowBackdrop::Mica => {
if build >= 22_621 {
WindowBackgroundAppearance::MicaBackdrop
} else if build >= 17_763 {
WindowBackgroundAppearance::Blurred
} else {
WindowBackgroundAppearance::Transparent
}
}
WindowBackdrop::MicaAlt => {
if build >= 22_621 {
WindowBackgroundAppearance::MicaAltBackdrop
} else if build >= 17_763 {
WindowBackgroundAppearance::Blurred
} else {
WindowBackgroundAppearance::Transparent
}
}
WindowBackdrop::Acrylic => {
if build >= 22_621 {
// Native acrylic (DWMSBT_TRANSIENTWINDOW) on Windows 11 22H2+.
WindowBackgroundAppearance::AcrylicBackdrop
} else if build >= 17_763 {
// Classic WCA acrylic (ACCENT_ENABLE_ACRYLICBLURBEHIND).
WindowBackgroundAppearance::Blurred
} else {
WindowBackgroundAppearance::Transparent
}
}
WindowBackdrop::Off => WindowBackgroundAppearance::Transparent,
}
}
/// The backdrop presets actually worth offering on a given Windows build.
/// Mica, Mica Alt and native acrylic all need Windows 11 22H2 (22621); below
/// that every one of them collapses onto the same classic WCA blur that
/// `Blur` already offers, so listing them would present several choices that
/// render identically — hiding them beats misrepresenting them. Below 1809
/// (17763) there is no blur API at all, so only plain translucency is left.
/// Pure function so the table is unit-testable on every platform.
pub(crate) fn supported_backdrops_for(build: u32) -> &'static [WindowBackdrop] {
if build >= 22_621 {
&[
WindowBackdrop::Auto,
WindowBackdrop::Blur,
WindowBackdrop::Mica,
WindowBackdrop::MicaAlt,
WindowBackdrop::Acrylic,
WindowBackdrop::Off,
]
} else if build >= 17_763 {
&[
WindowBackdrop::Auto,
WindowBackdrop::Blur,
WindowBackdrop::Off,
]
} else {
&[WindowBackdrop::Auto, WindowBackdrop::Off]
}
}
#[cfg(target_os = "windows")]
pub(crate) fn supported_backdrops() -> &'static [WindowBackdrop] {
supported_backdrops_for(windows_build_number())
}
#[cfg(target_os = "windows")]
pub(crate) fn windows_build_number() -> u32 {
static BUILD: OnceLock<u32> = OnceLock::new();
*BUILD.get_or_init(|| {
use windows_sys::Wdk::System::SystemServices::RtlGetVersion;
use windows_sys::Win32::System::SystemInformation::OSVERSIONINFOW;
let mut info: OSVERSIONINFOW = unsafe { std::mem::zeroed() };
// RtlGetVersion is not subject to the app-compat version lying that
// GetVersionEx is, and unlike GetVersionEx it does not require
// dwOSVersionInfoSize to be pre-filled; set it anyway so the call
// matches the documented contract.
info.dwOSVersionInfoSize = std::mem::size_of_val(&info) as u32;
let status = unsafe { RtlGetVersion(&mut info) };
if status == 0 {
info.dwBuildNumber
} else {
// Unknown build: treat it as too old, so the fallback chain picks
// the most conservative material instead of a silent no-op.
0
}
})
}
pub(crate) fn resolved_background_appearance(
backdrop: WindowBackdrop,
blur: bool,
) -> WindowBackgroundAppearance {
#[cfg(target_os = "windows")]
{
windows_background_appearance(backdrop, blur, windows_build_number())
}
#[cfg(not(target_os = "windows"))]
{
// `window_backdrop` is a Windows-only setting and this platform has
// no UI to change or clear it (only "Reset window overrides", which
// also clobbers opacity). If any variant pinned the appearance
// here, the local blur switch would silently show a checked state
// that isn't rendered — so every variant, including Off and Blur,
// defers to the legacy toggle. (A Windows-synced "off" therefore
// does not force transparency on macOS/Linux; the local blur switch
// stays authoritative.)
if blur {
WindowBackgroundAppearance::Blurred
} else {
WindowBackgroundAppearance::Transparent
}
}
}
/// Whether the resolved backdrop on this platform is actually a material —
/// i.e. the window content sits over a blur/mica layer instead of plain
/// transparency. Derived from `resolved_background_appearance` so the
/// opacity defaulting and the appearance resolution can never disagree: on
/// very old builds `Blur`/`Acrylic` fall back to `Transparent`, and a
/// see-through window with no material behind it gets no special opacity.
///
/// `Auto` never counts, even when the legacy blur toggle resolves it to
/// `Blurred`. It is the default for every config written before this
/// setting existed, and those windows were opaque with opaque sidebars —
/// treating them as materials would silently turn an untouched install
/// see-through on first launch after an update. Only an explicit pick in
/// the backdrop dropdown opts a window into the translucent defaults.
pub(crate) fn material_active(backdrop: WindowBackdrop, blur: bool) -> bool {
cfg!(target_os = "windows")
&& backdrop != WindowBackdrop::Auto
&& matches!(
resolved_background_appearance(backdrop, blur),
WindowBackgroundAppearance::Blurred
| WindowBackgroundAppearance::MicaBackdrop
| WindowBackgroundAppearance::MicaAltBackdrop
| WindowBackgroundAppearance::AcrylicBackdrop
)
}
/// The window's background alpha when neither the config nor the theme sets
/// an explicit opacity: translucent enough to show an active material,
/// fully opaque otherwise. Single source of truth for `apply_theme` and the
/// settings slider, so the two can never drift apart.
pub(crate) fn default_window_opacity(backdrop: WindowBackdrop, blur: bool) -> f32 {
if material_active(backdrop, blur) {
SYSTEM_MATERIAL_OPACITY
} else {
1.0
}
}
/// The fill for the workspace's large translucent surfaces (file sidebar,
/// right panel, SFTP panel). While a material is active and the window is
/// translucent, the surface paints on top of the already-alpha window
/// background, so its own alpha stacks (src-over) and the material would
/// show through far less than behind the terminal; a constant 0.15 keeps
/// the backdrop ratio at ~85% of the terminal's at every opacity setting.
/// `theme.sidebar` itself stays opaque — the settings theme picker paints
/// with it on top of the opaque settings overlay and must stay legible.
pub(crate) fn workspace_surface_color(cx: &App) -> Hsla {
let base: Hsla = cx.theme().sidebar;
let translucent = cx
.try_global::<presets::ActiveBackground>()
.and_then(|bg| bg.opacity)
.is_some_and(|o| o < 1.0);
if !translucent {
return base;
}
let config = cx.global::<Config>();
let theme = presets::by_id(cx, &effective_preset_id(cx));
let blur = config.window_blur.unwrap_or(theme.blur);
if material_active(config.window_backdrop, blur) {
base.alpha(0.15)
} else {
base
}
}
/// The backdrop presets offered in the settings dropdown: everything the
/// current build supports, plus the stored value even if it is not
/// supported here (e.g. Mica synced from a newer machine) — so the label
/// always matches what the window actually resolves to instead of quietly
/// showing "Auto" while a fallback appearance is applied.
#[cfg(target_os = "windows")]
pub(crate) fn backdrop_options(current: WindowBackdrop) -> Vec<WindowBackdrop> {
let mut list = supported_backdrops().to_vec();
if !list.contains(&current) {
list.push(current);
}
list
}
pub(crate) fn background_appearance(cx: &App) -> WindowBackgroundAppearance {
let config = cx.global::<Config>();
let theme = presets::by_id(cx, &effective_preset_id(cx));
if config.window_blur.unwrap_or(theme.blur) {
WindowBackgroundAppearance::Blurred
} else {
WindowBackgroundAppearance::Transparent
}
let blur = config.window_blur.unwrap_or(theme.blur);
resolved_background_appearance(config.window_backdrop, blur)
}
/// The appearance last handed to each live window, so `apply_theme` can skip
/// a `set_background_appearance` that would change nothing.
///
/// Worth the bookkeeping because the call is no longer cheap: with a DWM
/// material selected, gpui answers it by re-setting
/// `DWMWA_SYSTEMBACKDROP_TYPE` and forcing a non-client frame recalculation
/// (`SetWindowPos` with `SWP_FRAMECHANGED`). `apply_theme` runs on *every*
/// `Config` mutation in *every* window, so dragging the opacity slider —
/// which never changes the appearance — would otherwise recalc the frame
/// once per mouse-move sample and visibly stutter the drag.
#[derive(Default)]
struct AppliedAppearance(std::collections::HashMap<gpui::WindowId, WindowBackgroundAppearance>);
impl gpui::Global for AppliedAppearance {}
/// Records `appearance` for `window` and reports whether it differs from
/// what that window was last given. Entries for closed windows are dropped
/// first: window ids come from a slotmap and are reused, so a stale entry
/// could otherwise suppress the very first call for a brand-new window.
fn take_appearance_change(
window: &Window,
appearance: WindowBackgroundAppearance,
cx: &mut App,
) -> bool {
let id = window.window_handle().window_id();
let live: std::collections::HashSet<gpui::WindowId> =
cx.windows().iter().map(|w| w.window_id()).collect();
let applied = cx.default_global::<AppliedAppearance>();
applied.0.retain(|known, _| live.contains(known));
applied.0.insert(id, appearance) != Some(appearance)
}
pub(crate) fn apply_theme(mut window: Option<&mut Window>, cx: &mut App) {
@@ -254,8 +541,17 @@ pub(crate) fn apply_theme(mut window: Option<&mut Window>, cx: &mut App) {
} else {
ThemeMode::Light
};
let opacity = config.window_opacity.or(theme.opacity).filter(|o| *o < 1.0);
let blur = config.window_blur.unwrap_or(theme.blur);
// A material needs some translucency to be visible; without an explicit
// opacity override it defaults to SYSTEM_MATERIAL_OPACITY instead of
// 1.0. Derived from the *resolved* appearance so old builds where
// Blur/Acrylic fall back to plain transparency stay opaque by default.
let default_opacity = default_window_opacity(config.window_backdrop, blur);
let opacity = config
.window_opacity
.or(theme.opacity)
.unwrap_or(default_opacity);
let opacity = (opacity < 1.0).then_some(opacity);
if !follow {
sync_native_appearance(Some(theme.dark));
}
@@ -265,13 +561,13 @@ pub(crate) fn apply_theme(mut window: Option<&mut Window>, cx: &mut App) {
let active = theme.active_palette(config.theme_legible_palette);
let auto_hide_scrollbars = cx.should_auto_hide_scrollbars();
let backdrop = config.window_backdrop;
if let Some(window) = window.as_deref_mut() {
let bg_appearance = if blur {
WindowBackgroundAppearance::Blurred
} else {
WindowBackgroundAppearance::Transparent
};
window.set_background_appearance(bg_appearance);
let appearance = resolved_background_appearance(backdrop, blur);
if take_appearance_change(window, appearance, cx) {
window.set_background_appearance(appearance);
}
}
Theme::change(mode, window.as_deref_mut(), cx);
@@ -427,10 +723,15 @@ pub(crate) fn apply_theme(mut window: Option<&mut Window>, cx: &mut App) {
t.radius = px(8.);
let sidebar_bg = rgb(m.sidebar);
let sidebar_bg = Hsla::from(rgb(m.sidebar));
let sidebar_sel = rgb(surfaces.sidebar.selected);
// `t.sidebar` stays the opaque theme token: the settings theme picker
// paints with it on top of the (opaque) settings overlay, so diluting
// it would wash out that panel. The workspace sidebar/right-panel
// surfaces get their translucent variant at render time instead —
// see `workspace_surface_color`.
t.sidebar = sidebar_bg.into();
t.tokens.sidebar = Hsla::from(sidebar_bg).into();
t.tokens.sidebar = sidebar_bg.into();
t.sidebar_border = rgb(m.border).into();
t.sidebar_foreground = rgb(surfaces.sidebar.text_resting).into();
t.sidebar_accent = sidebar_sel.into();
@@ -529,4 +830,234 @@ mod tests {
assert_eq!(effective_preset_id(cx), Config::default().theme_preset);
});
}
#[test]
fn windows_background_appearance_falls_back_by_build() {
use crate::core::config::WindowBackdrop;
let a = |b: WindowBackdrop, blur: bool, build: u32| {
windows_background_appearance(b, blur, build)
};
// Auto keeps the legacy blur/translucent choice.
assert_eq!(
a(WindowBackdrop::Auto, false, 22_621),
WindowBackgroundAppearance::Transparent
);
assert_eq!(
a(WindowBackdrop::Auto, true, 17_763),
WindowBackgroundAppearance::Blurred
);
// Blur explicitly requests the classic blurred appearance from 1809
// on; older builds get plain translucency.
assert_eq!(
a(WindowBackdrop::Blur, false, 17_763),
WindowBackgroundAppearance::Blurred
);
assert_eq!(
a(WindowBackdrop::Blur, true, 17_762),
WindowBackgroundAppearance::Transparent
);
// Mica needs Windows 11 22H2; 1809..22H2 falls back to classic
// acrylic, and below 1809 there is no blur API to fall back to —
// asking for one there would leave an unbacked see-through window.
assert_eq!(
a(WindowBackdrop::Mica, false, 22_621),
WindowBackgroundAppearance::MicaBackdrop
);
assert_eq!(
a(WindowBackdrop::Mica, false, 22_620),
WindowBackgroundAppearance::Blurred
);
assert_eq!(
a(WindowBackdrop::Mica, false, 17_762),
WindowBackgroundAppearance::Transparent
);
assert_eq!(
a(WindowBackdrop::MicaAlt, false, 26_000),
WindowBackgroundAppearance::MicaAltBackdrop
);
assert_eq!(
a(WindowBackdrop::MicaAlt, false, 17_763),
WindowBackgroundAppearance::Blurred
);
assert_eq!(
a(WindowBackdrop::MicaAlt, false, 17_762),
WindowBackgroundAppearance::Transparent
);
// A failed RtlGetVersion reports build 0; every material must degrade
// to plain translucency there rather than request an unsupported one.
for backdrop in [
WindowBackdrop::Blur,
WindowBackdrop::Mica,
WindowBackdrop::MicaAlt,
WindowBackdrop::Acrylic,
] {
assert_eq!(
a(backdrop, false, 0),
WindowBackgroundAppearance::Transparent,
"{backdrop:?} on an unknown build must not request a material"
);
}
// Acrylic uses the native DWMSBT_TRANSIENTWINDOW material on 22H2+,
// classic WCA acrylic from 1809 on, plain translucency below that.
assert_eq!(
a(WindowBackdrop::Acrylic, false, 22_621),
WindowBackgroundAppearance::AcrylicBackdrop
);
assert_eq!(
a(WindowBackdrop::Acrylic, false, 22_620),
WindowBackgroundAppearance::Blurred
);
assert_eq!(
a(WindowBackdrop::Acrylic, false, 17_763),
WindowBackgroundAppearance::Blurred
);
assert_eq!(
a(WindowBackdrop::Acrylic, false, 17_762),
WindowBackgroundAppearance::Transparent
);
// Off never requests a material.
assert_eq!(
a(WindowBackdrop::Off, false, 26_000),
WindowBackgroundAppearance::Transparent
);
assert_eq!(
a(WindowBackdrop::Off, true, 26_000),
WindowBackgroundAppearance::Transparent
);
}
#[test]
fn auto_never_counts_as_a_material() {
use crate::core::config::WindowBackdrop;
// Every config written before this setting existed carries `Auto`,
// and many of them carry `window_blur: true` from the old switch.
// Those windows were opaque, with opaque sidebars — an update must
// not silently turn them see-through, so `Auto` gets the plain 1.0
// default no matter which way the legacy toggle points.
for blur in [false, true] {
assert!(!material_active(WindowBackdrop::Auto, blur));
assert_eq!(default_window_opacity(WindowBackdrop::Auto, blur), 1.0);
}
// `Off` is an explicit "no material" and must behave the same.
assert!(!material_active(WindowBackdrop::Off, true));
assert_eq!(default_window_opacity(WindowBackdrop::Off, true), 1.0);
}
#[cfg(target_os = "windows")]
#[test]
fn an_explicitly_picked_material_lowers_the_default_opacity() {
use crate::core::config::WindowBackdrop;
// The counterpart to the check above: opting in through the dropdown
// is what buys the translucent default, on a build that supports it.
if windows_build_number() < 22_621 {
return;
}
for backdrop in [
WindowBackdrop::Blur,
WindowBackdrop::Mica,
WindowBackdrop::MicaAlt,
WindowBackdrop::Acrylic,
] {
assert!(material_active(backdrop, false), "{backdrop:?}");
assert_eq!(
default_window_opacity(backdrop, false),
SYSTEM_MATERIAL_OPACITY,
"{backdrop:?}"
);
}
}
#[cfg(not(target_os = "windows"))]
#[test]
fn non_windows_backdrop_defers_to_the_local_blur_toggle() {
use crate::core::config::WindowBackdrop;
// On non-Windows, `window_backdrop` is a Windows-only setting with
// no local UI to clear it; every variant must defer to the local
// blur toggle so the switch's checked state always matches what is
// actually rendered.
for backdrop in [
WindowBackdrop::Auto,
WindowBackdrop::Blur,
WindowBackdrop::Mica,
WindowBackdrop::MicaAlt,
WindowBackdrop::Acrylic,
WindowBackdrop::Off,
] {
assert_eq!(
resolved_background_appearance(backdrop, true),
WindowBackgroundAppearance::Blurred,
"{backdrop:?} with blur on should resolve to Blurred"
);
assert_eq!(
resolved_background_appearance(backdrop, false),
WindowBackgroundAppearance::Transparent,
"{backdrop:?} with blur off should resolve to Transparent"
);
}
}
#[test]
fn supported_backdrops_follow_the_build() {
use crate::core::config::WindowBackdrop;
assert_eq!(
supported_backdrops_for(22_621),
&[
WindowBackdrop::Auto,
WindowBackdrop::Blur,
WindowBackdrop::Mica,
WindowBackdrop::MicaAlt,
WindowBackdrop::Acrylic,
WindowBackdrop::Off,
]
);
// Pre-22H2: Mica, Mica Alt and Acrylic all collapse onto the very
// same classic WCA blur that `Blur` already offers, so the dropdown
// hides them rather than listing choices that render identically.
assert_eq!(
supported_backdrops_for(22_620),
&[
WindowBackdrop::Auto,
WindowBackdrop::Blur,
WindowBackdrop::Off,
]
);
assert_eq!(
supported_backdrops_for(17_763),
&[
WindowBackdrop::Auto,
WindowBackdrop::Blur,
WindowBackdrop::Off,
]
);
// Every offered preset must resolve to a distinct appearance, or the
// dropdown is promising a difference the window cannot deliver.
// `Auto` is exempt: it has no fixed appearance of its own, it mirrors
// whichever way the blur toggle happens to be set.
for build in [22_621, 22_620, 17_763, 17_762] {
let offered: Vec<_> = supported_backdrops_for(build)
.iter()
.copied()
.filter(|b| *b != WindowBackdrop::Auto)
.collect();
for (i, a) in offered.iter().enumerate() {
for b in &offered[i + 1..] {
assert_ne!(
windows_background_appearance(*a, false, build),
windows_background_appearance(*b, false, build),
"build {build}: {a:?} and {b:?} render identically"
);
}
}
}
// Below 1809 there is no blur API at all; only plain translucency.
assert_eq!(
supported_backdrops_for(17_762),
&[WindowBackdrop::Auto, WindowBackdrop::Off]
);
}
}