fix(gpui): use native Windows backdrop materials
This commit is contained in:
1 parent
33a1aae8e4
commit
e464624a3e
6 files changed
+245
-136
No files matched your search
@@ -27,6 +27,8 @@ pub mod theme;
|
||||
pub mod toast;
|
||||
pub mod wallpaper;
|
||||
pub mod widgets;
|
||||
#[cfg(windows)]
|
||||
mod windows_backdrop;
|
||||
pub mod workspace;
|
||||
|
||||
use gpui::{App, AppContext as _, Bounds, WindowBounds, WindowOptions, px, size};
|
||||
@@ -92,6 +94,10 @@ pub fn run_shell() {
|
||||
}
|
||||
}
|
||||
let _runtime_server = runtime_server;
|
||||
// GPUI 默认给 DirectComposition 主窗加 NOREDIRECTIONBITMAP;DWM 在创建时消费
|
||||
// 该样式,事后再清已经无法给 system backdrop 补回挂载表面。
|
||||
#[cfg(windows)]
|
||||
let _window_creation_hook = windows_backdrop::install_main_window_creation_hook();
|
||||
gpui::Application::new().with_assets(Assets).run(move |cx| {
|
||||
// GPUI is the only event loop in `--gpui` mode, so it owns the same
|
||||
// per-process hook pipe before the first TerminalView spawns.
|
||||
|
||||
@@ -569,13 +569,13 @@ impl SettingsPane {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
// 模糊三档的标签带上取舍提示:三者不是质量递进,而是三套成本模型
|
||||
// (见 `nebula_settings::BlurModeName`)。用户按性能预算选,不是按
|
||||
// "越靠后越好"选。
|
||||
// 四档按 DWM 每帧成本排列,不是质量递进;用户按性能预算选择。
|
||||
// Aero 是实时玻璃,Acrylic 是实时材质模糊;两者都会采样窗口后方真实内容。
|
||||
// Mica 使用系统壁纸 backdrop,不由 Nebula 读取或重采样。
|
||||
add_select(
|
||||
"blur",
|
||||
&["无", "平衡(Mica)", "高质量(Acrylic)"],
|
||||
&["none", "mica", "acrylic"],
|
||||
&["无", "Mica(低开销)", "Aero(玻璃)", "Acrylic(高开销)"],
|
||||
&["none", "mica", "aero", "acrylic"],
|
||||
runtime.blur.settings_value(),
|
||||
window,
|
||||
cx,
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
//! 设置来源是共享层 `nebula_settings`(新增壁纸五键),解码结果按
|
||||
//! (路径, mtime, 透明度) 缓存;[`refresh`] 在启动与设置热应用时调用。
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::SystemTime;
|
||||
@@ -171,40 +172,23 @@ pub fn chrome_surface_opacity(cx: &App) -> f32 {
|
||||
|
||||
/// 开窗参数用。GPUI 通用层在窗口创建时就会把这个值下发到平台层
|
||||
/// (`gpui::Window::new` → `platform_window.set_background_appearance`),
|
||||
/// 所以启动即模糊不需要等 [`refresh`] 补第二次。
|
||||
/// Aero/Acrylic 可在创建时直接生效;Mica 的 DWM 属性需要 HWND,因此在窗口注册后
|
||||
/// 由 [`refresh`] 紧接着写入。
|
||||
pub fn initial_background_appearance() -> WindowBackgroundAppearance {
|
||||
background_appearance(nebula_settings::RuntimeSettings::load().blur)
|
||||
}
|
||||
|
||||
/// 模糊开关 → 窗口背景外观。**唯一落笔点**,启动与热应用共用。
|
||||
///
|
||||
/// # 为什么不能照抄旧壳的 DWM system backdrop
|
||||
/// # 两条 Windows 原生通道为什么要分开
|
||||
///
|
||||
/// 旧壳(winit)用 `DwmSetWindowAttribute(DWMWA_SYSTEMBACKDROP_TYPE,
|
||||
/// DWMSBT_TRANSIENTWINDOW)` 三行就拿到 Acrylic。同一段代码搬到 GPUI 壳上
|
||||
/// **比无效更糟**——2026-08-21 同窗对照实测(`opacity=0`、后方放高频视频画面):
|
||||
/// 写 `DWMSBT_TRANSIENTWINDOW` 后 `HRESULT=0`、属性能读回 3,但画面变成一块
|
||||
/// **不透明浅灰色板**,后方内容整块消失(既不透明也不模糊);改回
|
||||
/// `DWMSBT_NONE` 并只写 AccentPolicy Acrylic,同一窗口立刻恢复真模糊。
|
||||
/// Mica 按 MicaForEveryone 的公开方案使用 `DWMSBT_MAINWINDOW`,由 DWM 提供系统
|
||||
/// 壁纸 backdrop。Nebula 不读取壁纸文件,也不自行猜测多显示器排布。
|
||||
///
|
||||
/// 根因是合成结构不同:GPUI 只要没禁用 DirectComposition 就给窗口加
|
||||
/// `WS_EX_NOREDIRECTIONBITMAP`(`gpui/src/platform/windows/window.rs`),DWM
|
||||
/// 不为它创建重定向表面,system backdrop 没有正确的挂载点。旧壳是普通重定向
|
||||
/// 窗口,才没这个问题。
|
||||
///
|
||||
/// 两条已被实测否决、不要再试的"修法":
|
||||
/// 1. 运行时 `SetWindowLongPtrW` 清掉 `WS_EX_NOREDIRECTIONBITMAP` 再配
|
||||
/// `SWP_FRAMECHANGED`——该样式在 `CreateWindowEx` 时就被 DWM 消费,事后改
|
||||
/// 不回来(实测调用后读回样式位仍然置位),窗口内容也仍走 DComp visual。
|
||||
/// 2. 用 system backdrop "替代" AccentPolicy——见上,结果是不透明色板。
|
||||
///
|
||||
/// 上游 Zed 走过同一条弯路:PR #41842 第一版把 `Blurred` 改成
|
||||
/// `DWMSBT_TRANSIENTWINDOW`,第二版又改回 `SetWindowCompositionAttribute`,
|
||||
/// 只把 Mica / MicaAlt 留给 DWM backdrop。所以这里直接用 GPUI 的
|
||||
/// [`WindowBackgroundAppearance::Blurred`]——Windows 平台层落到
|
||||
/// `ACCENT_ENABLE_ACRYLICBLURBEHIND`(且已处理 tint alpha 为 0 时 DWM 跳过
|
||||
/// 模糊的坑),macOS / Linux 各自落到原生毛玻璃。一处开关,三平台生效,
|
||||
/// 不必碰 fork 也不必自写 FFI。
|
||||
/// Aero/Acrylic 继续使用 GPUI 已验证的 AccentPolicy 通道。Aero 额外沿用
|
||||
/// MicaForEveryone 的 `DwmEnableBlurBehindWindow` + 半透明深色玻璃配方;GPUI 的
|
||||
/// DirectComposition 窗口上,`DWMSBT_TRANSIENTWINDOW` 会形成不透明灰板,不能
|
||||
/// 因为 Mica 与它同属 system backdrop 就混用;切换档位时会显式清理另一条通道。
|
||||
///
|
||||
/// # 不透明度 100% 时看不到模糊是正交结果,不是失效
|
||||
///
|
||||
@@ -220,23 +204,32 @@ pub fn initial_background_appearance() -> WindowBackgroundAppearance {
|
||||
/// 出后方内容。`Transparent` 会额外挂 `ACCENT_ENABLE_TRANSPARENTGRADIENT`
|
||||
/// 的纯色渐变层,对我们没有收益。其他平台维持原有 `Transparent` 语义。
|
||||
///
|
||||
/// # Mica 为什么也要 `Blurred`
|
||||
/// # 哪些档位要窗口保持可透
|
||||
///
|
||||
/// Windows 侧这个返回值只决定 GPUI 平台层预写哪套 AccentPolicy,随后
|
||||
/// [`apply_windows_accent_policy`] 会按档位覆写。Mica 需要的是「窗口保持可
|
||||
/// 透」这件事本身——材质在窗口内容**下方**,内容不透就看不见。两档模糊在这
|
||||
/// 一点上诉求相同,所以共用 `Blurred`,差异全部落在覆写那一步。
|
||||
/// `Aero` / `Acrylic` 的材质由 DWM 画在窗口内容**下方**,内容不透就看不见,所以
|
||||
/// 要 `Blurred`——这个返回值决定 GPUI 平台层预写哪套 AccentPolicy(启动即生效,
|
||||
/// 不必等 [`refresh`] 补第二次),随后 [`apply_windows_accent_policy`] 覆写成
|
||||
/// state 3 / state 4。
|
||||
///
|
||||
/// `Mica` 使用 `Transparent`,只让 GPUI 保持预乘 alpha;真正材质由
|
||||
/// [`apply_windows_accent_policy`] 写入 `DWMSBT_MAINWINDOW`。不能使用 `Opaque`,
|
||||
/// 否则客户区像素会遮住 DWM 在窗口下方合成的系统 backdrop。
|
||||
fn background_appearance(blur: BlurModeName) -> WindowBackgroundAppearance {
|
||||
if blur.enabled() {
|
||||
return WindowBackgroundAppearance::Blurred;
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
WindowBackgroundAppearance::Opaque
|
||||
match blur {
|
||||
BlurModeName::Aero | BlurModeName::Acrylic => WindowBackgroundAppearance::Blurred,
|
||||
BlurModeName::Mica => WindowBackgroundAppearance::Transparent,
|
||||
BlurModeName::None => WindowBackgroundAppearance::Opaque,
|
||||
}
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
WindowBackgroundAppearance::Transparent
|
||||
if blur.enabled() {
|
||||
WindowBackgroundAppearance::Blurred
|
||||
} else {
|
||||
WindowBackgroundAppearance::Transparent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,7 +237,10 @@ fn background_appearance(blur: BlurModeName) -> WindowBackgroundAppearance {
|
||||
/// 窗口级材质是**跨进程**调用(`SetWindowCompositionAttribute` 两次 +
|
||||
/// `DwmSetWindowAttribute`)。不做门控就等于每帧和 DWM 往返三次,滑块直接
|
||||
/// 拖成幻灯片——2026-08-21 实测。档位没变时一次都不碰。
|
||||
struct AppliedBlur(BlurModeName);
|
||||
struct AppliedBlur {
|
||||
blur: BlurModeName,
|
||||
windows: HashSet<gpui::WindowId>,
|
||||
}
|
||||
|
||||
impl gpui::Global for AppliedBlur {}
|
||||
|
||||
@@ -270,23 +266,32 @@ impl gpui::Global for AppliedBlur {}
|
||||
/// [`App::defer`] 把应用推到本轮 effect cycle 末尾,那时窗口已归还 slot。
|
||||
/// update 失败不再静默:留 warn,避免同一个坑第三次被当成"DWM 不生效"。
|
||||
///
|
||||
/// # 模糊态没变就直接返回
|
||||
/// # 模糊态没变时只处理新窗口
|
||||
///
|
||||
/// 不透明度/壁纸改动也会走到这里,但它们只影响我们自己绘制的像素,窗口级
|
||||
/// 模糊属性一个字节都不用改。透明度是滑块,一次拖拽几十上百个事件,所以这条
|
||||
/// 短路是拖拽手感的必要条件,不是可选优化。重绘由调用链上的 `cx.notify()`
|
||||
/// 与主题重建负责。
|
||||
/// 短路是拖拽手感的必要条件,不是可选优化。多窗口下则按 `WindowId` 补应用新窗,
|
||||
/// 避免全局档位相同就让第二个窗口漏掉原生 backdrop。
|
||||
fn apply_window_effects(cx: &mut App) {
|
||||
// 全局缺失时按"关"处理而不是缺省档:这条路径只在极早期或异常态走到,
|
||||
// 宁可少一层材质,也不要凭空给窗口开上模糊再被 refresh 纠正一次。
|
||||
let blur = cx.try_global::<VisualEffects>().map(|v| v.blur).unwrap_or(BlurModeName::None);
|
||||
if cx.try_global::<AppliedBlur>().map(|applied| applied.0) == Some(blur) {
|
||||
return;
|
||||
}
|
||||
cx.set_global(AppliedBlur(blur));
|
||||
let appearance = background_appearance(blur);
|
||||
cx.defer(move |cx| {
|
||||
for handle in cx.windows() {
|
||||
// 必须在 defer 后枚举:触发设置变更的窗口此时才重新放回 App 窗口表。
|
||||
let handles = cx.windows();
|
||||
let window_ids = handles.iter().map(|handle| handle.window_id()).collect::<HashSet<_>>();
|
||||
let already_applied = cx
|
||||
.try_global::<AppliedBlur>()
|
||||
.filter(|applied| applied.blur == blur)
|
||||
.map(|applied| applied.windows.clone())
|
||||
.unwrap_or_default();
|
||||
let pending = handles
|
||||
.into_iter()
|
||||
.filter(|handle| !already_applied.contains(&handle.window_id()))
|
||||
.collect::<Vec<_>>();
|
||||
cx.set_global(AppliedBlur { blur, windows: window_ids });
|
||||
for handle in pending {
|
||||
if let Err(err) = handle.update(cx, |_, window, _| {
|
||||
window.set_background_appearance(appearance);
|
||||
#[cfg(windows)]
|
||||
@@ -299,48 +304,34 @@ fn apply_window_effects(cx: &mut App) {
|
||||
});
|
||||
}
|
||||
|
||||
/// 显式落下 Windows 材质属性:AccentPolicy 与 system backdrop 二选一。
|
||||
/// 显式落下 Windows 材质属性。
|
||||
///
|
||||
/// # 三档各自写什么
|
||||
/// # 四档各自写什么
|
||||
///
|
||||
/// | 档位 | AccentPolicy | SYSTEMBACKDROP | DWM 每帧成本 |
|
||||
/// |---|---|---|---|
|
||||
/// | `None` | 全零 | `DWMSBT_NONE` | 无 |
|
||||
/// | `Mica` | 全零 | `DWMSBT_MAINWINDOW` | 无(只在窗口移动时重采样壁纸) |
|
||||
/// | `Acrylic` | state 4 + 非零 alpha | `DWMSBT_NONE` | 整窗实时高斯模糊 |
|
||||
/// | `Aero` | state 3 + 玻璃色调 | `DWMSBT_NONE` | 整窗实时玻璃模糊 |
|
||||
/// | `Mica` | 全零 | `DWMSBT_MAINWINDOW` | 系统壁纸 backdrop |
|
||||
/// | `Acrylic` | state 4 + 非零 alpha | `DWMSBT_NONE` | 实时模糊 + tint/噪点/饱和 |
|
||||
///
|
||||
/// `Mica` 与 MicaForEveryone 使用同一公开 DWM 属性。Nebula 不读取
|
||||
/// `SPI_GETDESKWALLPAPER` 或 `TranscodedWallpaper`;显示器选择、壁纸排布、模糊和
|
||||
/// 色调全部交给系统合成器。Windows 不支持该属性时退回 Acrylic,避免透明空洞。
|
||||
///
|
||||
/// 两条通道**必须互斥**:同时开 Acrylic 与 system backdrop 时 DWM 的行为未
|
||||
/// 定义(实测表现为 backdrop 赢,Acrylic 被吞)。所以每档都要把另一条显式
|
||||
/// 写回中性值,不能只写自己那条。
|
||||
///
|
||||
/// # `DWMSBT_MAINWINDOW`(Mica) 能用,`DWMSBT_TRANSIENTWINDOW`(Acrylic 材质) 不能
|
||||
///
|
||||
/// 2026-08-21 记录过 `DWMSBT_TRANSIENTWINDOW` 在本壳上变成不透明浅灰色板,
|
||||
/// 当时归因于 GPUI 的 `WS_EX_NOREDIRECTIONBITMAP`(DComp 窗口没有重定向表面
|
||||
/// 给 system backdrop 挂载)。**2026-08-22 用户实测推翻了这个外推**:同一族的
|
||||
/// `DWMSBT_MAINWINDOW` 在本壳上工作正常,能正确显示模糊的桌面壁纸色调。
|
||||
///
|
||||
/// 也就是说那次失败是 `TRANSIENTWINDOW` 这一个值的问题,不是整个 system
|
||||
/// backdrop 家族的问题。**不要**因为看到旧注释就把 Mica 一起判死;反过来也
|
||||
/// **不要**因为 Mica 能用就再去试 `TRANSIENTWINDOW`——那一条已经实测两次。
|
||||
///
|
||||
/// # Mica 天生没有玻璃感,靠壳侧自绘补
|
||||
///
|
||||
/// Acrylic 的观感由四层构成:实时背景采样 + 高斯模糊 + 噪点纹理 + 饱和度
|
||||
/// 提升。Mica 只有前两层的廉价版(采样桌面壁纸 + 强模糊 + 色调映射),刻意
|
||||
/// 不做噪点与饱和度——它的设计目标是"低调的材质底色"而非"毛玻璃"。
|
||||
///
|
||||
/// 所以 Mica 档的玻璃感由 [`paint_glass_overlay`] 在壳侧自绘补齐:噪点纹理与
|
||||
/// tint 恰好是 Acrylic 里最便宜的两层,自己画的成本接近零,还换来完全可调。
|
||||
///
|
||||
/// `SetWindowCompositionAttribute` 未进入公开 SDK,所以和 GPUI 上游一样动态取
|
||||
/// 函数地址;backdrop 与 `DwmFlush` 则用公开 DWM API。任一步失败都留日志,
|
||||
/// 避免把 API 失败再次误判成"设置没有热应用"。
|
||||
/// 函数地址;backdrop 则用公开 DWM API。任一步失败都留日志,避免把 API 失败
|
||||
/// 再次误判成"设置没有热应用"。
|
||||
#[cfg(windows)]
|
||||
fn apply_windows_accent_policy(window: &Window, blur: BlurModeName) {
|
||||
use windows_sys::Win32::Foundation::{BOOL, HWND};
|
||||
use windows_sys::Win32::Graphics::Dwm::{
|
||||
DWMSBT_MAINWINDOW, DWMSBT_NONE, DWMWA_SYSTEMBACKDROP_TYPE, DwmSetWindowAttribute,
|
||||
DWM_BB_ENABLE, DWM_BLURBEHIND, DWMSBT_MAINWINDOW, DWMSBT_NONE, DWMWA_SYSTEMBACKDROP_TYPE,
|
||||
DwmEnableBlurBehindWindow, DwmSetWindowAttribute,
|
||||
};
|
||||
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress};
|
||||
use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
|
||||
@@ -370,10 +361,29 @@ fn apply_windows_accent_policy(window: &Window, blur: BlurModeName) {
|
||||
return;
|
||||
};
|
||||
let hwnd = handle.hwnd.get() as *mut core::ffi::c_void;
|
||||
let blur_behind = DWM_BLURBEHIND {
|
||||
dwFlags: DWM_BB_ENABLE,
|
||||
fEnable: i32::from(blur == BlurModeName::Aero),
|
||||
hRgnBlur: std::ptr::null_mut(),
|
||||
fTransitionOnMaximized: 0,
|
||||
};
|
||||
// MicaForEveryone 会先开启 DWM blur-behind,再写 AccentPolicy。这里对所有
|
||||
// 档位都落一次 enable/disable,避免从 Aero 热切换后遗留玻璃层。
|
||||
let blur_behind_result = unsafe { DwmEnableBlurBehindWindow(hwnd, &blur_behind) };
|
||||
let backdrop: i32 = match blur {
|
||||
BlurModeName::Mica => DWMSBT_MAINWINDOW,
|
||||
BlurModeName::None | BlurModeName::Acrylic => DWMSBT_NONE,
|
||||
BlurModeName::None | BlurModeName::Aero | BlurModeName::Acrylic => DWMSBT_NONE,
|
||||
};
|
||||
// SAFETY: hwnd 来自当前存活的 GPUI 窗口,属性和值均为公开 DWM ABI。
|
||||
let backdrop_result = unsafe {
|
||||
DwmSetWindowAttribute(
|
||||
hwnd,
|
||||
DWMWA_SYSTEMBACKDROP_TYPE as u32,
|
||||
&backdrop as *const _ as *const core::ffi::c_void,
|
||||
std::mem::size_of::<i32>() as u32,
|
||||
)
|
||||
};
|
||||
let system_mica_available = blur == BlurModeName::Mica && backdrop_result >= 0;
|
||||
let mut accent = match blur {
|
||||
BlurModeName::Acrylic => AccentPolicy {
|
||||
state: 4, // ACCENT_ENABLE_ACRYLICBLURBEHIND
|
||||
@@ -382,9 +392,23 @@ fn apply_windows_accent_policy(window: &Window, blur: BlurModeName) {
|
||||
gradient_color: 0x0100_0000,
|
||||
animation_id: 0,
|
||||
},
|
||||
// Mica 走 backdrop 通道,AccentPolicy 必须让位——否则 Acrylic 残留会
|
||||
// 盖住 Mica,切档时看起来像"没生效"。
|
||||
BlurModeName::None | BlurModeName::Mica => {
|
||||
// Aero = MicaForEveryone 的 Win32 玻璃配方:实时 BlurBehind + 约 60%
|
||||
// 深色玻璃色调。它与 DWMBlurGlass 注入 DWM 后构造的完整 Aero effect graph
|
||||
// 并非同一套私有实现,但使用的是普通应用能够安全调用的对应系统接口。
|
||||
BlurModeName::Aero => AccentPolicy {
|
||||
state: 3, // ACCENT_ENABLE_BLURBEHIND
|
||||
flags: 0,
|
||||
// 与 MicaForEveryone RuleService.EnableBlurBehind 一致:A=152,RGB=2B2B2B。
|
||||
// 之前的 0x01000000 几乎没有玻璃色调,导致 Aero 与 Acrylic 肉眼难区分。
|
||||
gradient_color: 0x982B_2B2B,
|
||||
animation_id: 0,
|
||||
},
|
||||
// 不支持系统 Mica 的 Windows 版本退回 Acrylic;成功时必须关闭 Accent,
|
||||
// 两套 backdrop 同开会互相覆盖。
|
||||
BlurModeName::Mica if !system_mica_available => {
|
||||
AccentPolicy { state: 4, flags: 0, gradient_color: 0x0100_0000, animation_id: 0 }
|
||||
},
|
||||
BlurModeName::Mica | BlurModeName::None => {
|
||||
AccentPolicy { state: 0, flags: 0, gradient_color: 0, animation_id: 0 }
|
||||
},
|
||||
};
|
||||
@@ -401,16 +425,9 @@ fn apply_windows_accent_policy(window: &Window, blur: BlurModeName) {
|
||||
// 约 8~16ms)。AccentPolicy 与 backdrop 的改动本来就在下一帧生效,配合
|
||||
// 调用方的 `window.refresh()` 已经即时;而 2026-08-21 实测,一旦这条路径
|
||||
// 被每帧走到(拖不透明度滑块),那次 flush 就把滑块拖成了幻灯片。
|
||||
let (backdrop_result, accent_result) = unsafe {
|
||||
let backdrop_result = DwmSetWindowAttribute(
|
||||
hwnd,
|
||||
DWMWA_SYSTEMBACKDROP_TYPE as u32,
|
||||
&backdrop as *const _ as *const core::ffi::c_void,
|
||||
std::mem::size_of::<i32>() as u32,
|
||||
);
|
||||
|
||||
let accent_result = unsafe {
|
||||
let user32 = GetModuleHandleA(c"user32.dll".as_ptr() as *const u8);
|
||||
let accent_result = if user32.is_null() {
|
||||
if user32.is_null() {
|
||||
None
|
||||
} else {
|
||||
GetProcAddress(user32, c"SetWindowCompositionAttribute".as_ptr() as *const u8).map(
|
||||
@@ -420,13 +437,26 @@ fn apply_windows_accent_policy(window: &Window, blur: BlurModeName) {
|
||||
set_attribute(hwnd, &mut data)
|
||||
},
|
||||
)
|
||||
};
|
||||
(backdrop_result, accent_result)
|
||||
}
|
||||
};
|
||||
if backdrop_result < 0 {
|
||||
if blur == BlurModeName::Mica {
|
||||
log::warn!(
|
||||
"system Mica is unavailable (HRESULT=0x{:08X}); falling back to Acrylic",
|
||||
backdrop_result as u32
|
||||
);
|
||||
} else {
|
||||
log::warn!(
|
||||
"DwmSetWindowAttribute(DWMWA_SYSTEMBACKDROP_TYPE={backdrop}) failed: HRESULT=0x{:08X}",
|
||||
backdrop_result as u32
|
||||
);
|
||||
}
|
||||
}
|
||||
if blur_behind_result < 0 {
|
||||
log::warn!(
|
||||
"DwmSetWindowAttribute(DWMWA_SYSTEMBACKDROP_TYPE={backdrop}) failed: HRESULT=0x{:08X}",
|
||||
backdrop_result as u32
|
||||
"DwmEnableBlurBehindWindow(enable={}) failed: HRESULT=0x{:08X}",
|
||||
blur == BlurModeName::Aero,
|
||||
blur_behind_result as u32
|
||||
);
|
||||
}
|
||||
match accent_result {
|
||||
@@ -464,26 +494,35 @@ fn decode(
|
||||
Some((image, source, width, height))
|
||||
}
|
||||
|
||||
// ---- 以下一组只服务已停用的 [`paint_glass_overlay`](见其文档)。按用户要求
|
||||
// ---- 保留实现,因此统一标 `dead_code`,不要因为"没人用"就删掉。
|
||||
|
||||
/// 噪点 tile 边长(物理像素)。越大平铺次数越少、内存越高:512 时 3K 屏约
|
||||
/// 28 次 `paint_image`,1MB 纹理——两头都便宜。
|
||||
#[allow(dead_code)]
|
||||
const NOISE_TILE_PX: u32 = 512;
|
||||
/// 噪点强度。Acrylic 自带的颗粒非常细微(目测 3~5%),高于 ~8% 会从"玻璃"
|
||||
/// 变成"脏"。
|
||||
#[allow(dead_code)]
|
||||
const NOISE_ALPHA: u8 = 14;
|
||||
/// 白色 tint 浓度。暗色主题要更厚——Mica 的壁纸色调在暗色下偏沉,正是它
|
||||
/// "不够透亮"的主因;亮色主题本就够亮,加太多会过曝。
|
||||
#[allow(dead_code)]
|
||||
const TINT_ALPHA_DARK: f32 = 0.08;
|
||||
#[allow(dead_code)]
|
||||
const TINT_ALPHA_LIGHT: f32 = 0.05;
|
||||
|
||||
thread_local! {
|
||||
/// 噪点 tile 只依赖上面几个常量,进程内生成一次即可。用 `thread_local`
|
||||
/// 而不是 `OnceLock`:`RenderImage` 只在渲染线程用,不必为跨线程共享去
|
||||
/// 背 `Send + Sync` 的约束。
|
||||
#[allow(dead_code)]
|
||||
static NOISE_TILE: std::cell::RefCell<Option<Arc<RenderImage>>> =
|
||||
const { std::cell::RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// 生成(并缓存)噪点 tile。
|
||||
#[allow(dead_code)]
|
||||
fn noise_tile() -> Arc<RenderImage> {
|
||||
NOISE_TILE.with(|slot| {
|
||||
if let Some(tile) = slot.borrow().as_ref() {
|
||||
@@ -511,24 +550,20 @@ fn noise_tile() -> Arc<RenderImage> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Mica 档的玻璃增强层:白 tint + 噪点颗粒,把 DWM 那层寡淡的壁纸色调补成
|
||||
/// 接近 Acrylic 的毛玻璃观感。整窗一层,画在 chrome 与壁纸**之下**。
|
||||
/// 【已停用,保留作参考】Mica 档的玻璃增强层:白 tint + 噪点颗粒。
|
||||
///
|
||||
/// # 为什么这层值得自己画
|
||||
/// # 为什么停用
|
||||
///
|
||||
/// Acrylic 的观感由四层构成:实时背景采样 + 高斯模糊 + 噪点纹理 + 亮度/
|
||||
/// 饱和度提升。前两层是 DWM 每帧的重活,也正是 2026-08-22 实测里 `dwm.exe`
|
||||
/// 均值 28% 的来源;后两层则是**纯静态叠加**——一张平铺小图加一个半透明
|
||||
/// 矩形,在我们自己的绘制层里成本接近零。
|
||||
/// 这层的前提是"系统 Mica 已经提供了壁纸模糊,我们只补噪点与 tint"。2026-08-22
|
||||
/// 实测证明前提不成立——系统 Mica 在本壳上从未生效,底下是一块纯色兜底,于是
|
||||
/// 这层只是在纯色上再刷一层雾,观感上离 Mica 更远(用户判定"明显不是 Mica")。
|
||||
///
|
||||
/// Mica 恰好只给前两层的廉价版(采样桌面壁纸 + 强模糊,且只在窗口移动时
|
||||
/// 更新),刻意省掉后两层。于是"拿 Mica 的成本 + 补 Acrylic 的观感"成立,
|
||||
/// 这就是 2026-08-22 用户裁定的甜品区。
|
||||
/// Mica 现在由 DWM 的系统 backdrop 完整合成,噪点与 tint 不应在客户区重复叠加,
|
||||
/// 所以这层不再有调用点。代码按用户要求保留,供其他材质实验复用。
|
||||
///
|
||||
/// 调玻璃感只动本文件顶部那四个常量,不要改绘制顺序:tint 必须在噪点之下,
|
||||
/// 否则颗粒会被 tint 冲淡到看不见。
|
||||
///
|
||||
/// Acrylic 档不叠这层——它自带噪点与亮度提升,再叠一遍就糊了。
|
||||
#[allow(dead_code)]
|
||||
pub fn paint_glass_overlay(bounds: Bounds<Pixels>, window: &mut Window, cx: &App) {
|
||||
let Some(effects) = cx.try_global::<VisualEffects>() else { return };
|
||||
if effects.blur != BlurModeName::Mica {
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
//! Windows system-backdrop prerequisites for GPUI top-level windows.
|
||||
//!
|
||||
//! GPUI renders through DirectComposition and creates every platform window with
|
||||
//! `WS_EX_NOREDIRECTIONBITMAP`. That is correct for transparent tool windows, but a
|
||||
//! top-level Mica window needs a DWM redirection surface. The style is consumed by
|
||||
//! DWM during `CreateWindowExW`, so changing it after the HWND exists is too late.
|
||||
|
||||
use windows_sys::Win32::Foundation::{LPARAM, LRESULT, WPARAM};
|
||||
use windows_sys::Win32::System::Threading::GetCurrentThreadId;
|
||||
use windows_sys::Win32::UI::WindowsAndMessaging::{
|
||||
CBT_CREATEWNDW, CallNextHookEx, HCBT_CREATEWND, HHOOK, SetWindowsHookExW, UnhookWindowsHookEx,
|
||||
WH_CBT, WS_EX_APPWINDOW, WS_EX_NOREDIRECTIONBITMAP,
|
||||
};
|
||||
|
||||
/// Installs a same-thread CBT hook before GPUI creates its first HWND.
|
||||
///
|
||||
/// Only regular application windows are changed. GPUI menus, prompts and other
|
||||
/// tool windows keep `WS_EX_NOREDIRECTIONBITMAP`, preserving their transparent
|
||||
/// DirectComposition behavior.
|
||||
pub(super) fn install_main_window_creation_hook() -> Option<MainWindowCreationHook> {
|
||||
// SAFETY: the callback is a process-local function and the hook is scoped to
|
||||
// the current UI thread, so no DLL module handle is required by Win32.
|
||||
let hook = unsafe {
|
||||
SetWindowsHookExW(
|
||||
WH_CBT,
|
||||
Some(main_window_creation_hook),
|
||||
std::ptr::null_mut(),
|
||||
GetCurrentThreadId(),
|
||||
)
|
||||
};
|
||||
if hook.is_null() {
|
||||
log::warn!(
|
||||
"failed to install the GPUI main-window creation hook; system Mica may be unavailable"
|
||||
);
|
||||
None
|
||||
} else {
|
||||
Some(MainWindowCreationHook(hook))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct MainWindowCreationHook(HHOOK);
|
||||
|
||||
impl Drop for MainWindowCreationHook {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: the handle came from SetWindowsHookExW and is owned by this guard.
|
||||
unsafe {
|
||||
UnhookWindowsHookEx(self.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "system" fn main_window_creation_hook(
|
||||
code: i32,
|
||||
wparam: WPARAM,
|
||||
lparam: LPARAM,
|
||||
) -> LRESULT {
|
||||
if code >= 0 && code as u32 == HCBT_CREATEWND && lparam != 0 {
|
||||
// SAFETY: for HCBT_CREATEWND, Win32 guarantees lParam points to a writable
|
||||
// CBT_CREATEWNDW for the window whose creation is still in progress.
|
||||
let create = unsafe { &mut *(lparam as *mut CBT_CREATEWNDW) };
|
||||
if !create.lpcs.is_null() {
|
||||
let create_struct = unsafe { &mut *create.lpcs };
|
||||
let is_main_window = create_struct.dwExStyle & WS_EX_APPWINDOW != 0;
|
||||
if is_main_window {
|
||||
create_struct.dwExStyle &= !WS_EX_NOREDIRECTIONBITMAP;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: every hook must forward events it does not consume. A null hook
|
||||
// handle is explicitly supported for CallNextHookEx.
|
||||
unsafe { CallNextHookEx(std::ptr::null_mut(), code, wparam, lparam) }
|
||||
}
|
||||
@@ -4144,18 +4144,12 @@ impl Render for NebulaWorkspace {
|
||||
this.open_ai_session_palette(window, cx);
|
||||
}))
|
||||
.child(
|
||||
// 最底层壁纸(chrome 之下):卡外区域由这层负责,卡内切片由
|
||||
// 终端元素在卡底色之上重画(旧壳同一层模型)。Mica 的自绘
|
||||
// tint + 噪点增强层暂时停用,保留在下面的注释调用与
|
||||
// wallpaper.rs 实现中,待实机重新调参。
|
||||
// 用户显式配置的背景图画在 chrome 之下;系统 Mica/Aero/Acrylic
|
||||
// 位于整个 GPUI 内容层下方,由 DWM 合成,不能在这里读取壁纸仿画。
|
||||
// 卡外区域由这层负责,卡内切片由终端元素在卡底色之上重画。
|
||||
gpui::canvas(
|
||||
|_, _, _| (),
|
||||
|bounds, _, window, cx| {
|
||||
// 暂停 Mica 自绘增强层:当前 tint + 噪点会盖住系统 Mica
|
||||
// 自身的壁纸材质,尤其在 opacity=0 时仍留下约 13% 遮罩,
|
||||
// 反而削弱背景层次。实现完整保留在 wallpaper.rs,等后续
|
||||
// 基于实机截图重新调参后再恢复调用。
|
||||
// crate::gpui_shell::wallpaper::paint_glass_overlay(bounds, window, cx);
|
||||
crate::gpui_shell::wallpaper::paint_wallpaper_under_chrome(
|
||||
bounds, window, cx,
|
||||
);
|
||||
|
||||
+22
-21
@@ -644,28 +644,19 @@ impl BellModeName {
|
||||
}
|
||||
}
|
||||
|
||||
/// 窗口背景模糊材质:关 / Mica(平衡)/ Acrylic(高质量)。
|
||||
///
|
||||
/// 三档对应三套完全不同的 DWM 成本模型,落点在
|
||||
/// `gpui_shell::wallpaper::apply_windows_accent_policy`:
|
||||
/// - `None`:AccentPolicy 全零 + `DWMSBT_NONE`,DWM 只贴一张不透明纹理。
|
||||
/// - `Mica`:`DWMSBT_MAINWINDOW`。DWM 采样**桌面壁纸**做色调,只在窗口移动
|
||||
/// 时更新,不逐帧模糊——这是"甜品区"的性能来源。代价是它天生没有玻璃
|
||||
/// 感(无噪点纹理、无饱和度提升),需要壳侧自绘玻璃层补齐。
|
||||
/// - `Acrylic`:`ACCENT_ENABLE_ACRYLICBLURBEHIND`。DWM 对窗口后方**实时
|
||||
/// 内容**逐帧高斯模糊 + 噪点 + 饱和度。观感最好,也是唯一能透出后方其他
|
||||
/// 窗口的档位;2026-08-22 实测这一档把 dwm.exe 顶到 28% 均值。
|
||||
///
|
||||
/// # 缺省为什么是 Mica 而不是 Acrylic
|
||||
///
|
||||
/// 2026-08-22 用户实测:3K@165Hz 下 Acrylic 档 `nebula.exe` 22.7% +
|
||||
/// `dwm.exe` 28.3%,关掉模糊后卡顿立刻好转。默认档必须是性能安全的那个,
|
||||
/// 想要实时透视的用户显式选高质量。
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
|
||||
/// 窗口背景材质。**按 DWM 每帧成本递增排列**,不是质量递进——四者是四套成本
|
||||
/// 模型,不存在"越靠后越好":
|
||||
///
|
||||
/// - `None`:无材质。窗口按不透明度直接透出后方内容,不模糊。
|
||||
/// - `Mica`:Windows 系统壁纸 backdrop;由 DWM 合成,不含后方其他窗口。
|
||||
/// - `Aero`:实时模糊窗口**后方的真实内容**,并叠加 Win32 深色玻璃色调。
|
||||
/// - `Acrylic`:实时模糊 + tint/噪点/饱和度,最贵。
|
||||
pub enum BlurModeName {
|
||||
None,
|
||||
#[default]
|
||||
Mica,
|
||||
Aero,
|
||||
Acrylic,
|
||||
}
|
||||
|
||||
@@ -674,10 +665,11 @@ impl BlurModeName {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"none" | "off" | "0" | "false" | "no" => Some(Self::None),
|
||||
"mica" => Some(Self::Mica),
|
||||
"aero" | "blurbehind" => Some(Self::Aero),
|
||||
"acrylic" => Some(Self::Acrylic),
|
||||
// 旧壳的布尔开关:`blur=1` 只表达"我要模糊",并不表达要哪种材质。
|
||||
// 迁到 Mica 而非 Acrylic——本次改动的初衷就是降 GPU 占用,把存量
|
||||
// 用户留在最贵的那一档等于没修。想要实时透视的人显式改成 acrylic。
|
||||
// 用户留在最贵的那一档等于没修。想要实时透视的人显式改成 aero。
|
||||
"1" | "true" | "yes" | "on" => Some(Self::Mica),
|
||||
_ => None,
|
||||
}
|
||||
@@ -687,11 +679,12 @@ impl BlurModeName {
|
||||
match self {
|
||||
Self::None => "none",
|
||||
Self::Mica => "mica",
|
||||
Self::Aero => "aero",
|
||||
Self::Acrylic => "acrylic",
|
||||
}
|
||||
}
|
||||
|
||||
/// 是否需要窗口内容保留透明像素(两档模糊都要,否则材质被自己盖住)。
|
||||
/// 是否需要窗口内容保留透明像素(除 `None` 外都要,否则材质被自己盖住)。
|
||||
pub fn enabled(self) -> bool {
|
||||
!matches!(self, Self::None)
|
||||
}
|
||||
@@ -1054,14 +1047,19 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blur_mode_parses_three_tiers_and_migrates_legacy_bool() {
|
||||
fn blur_mode_parses_four_tiers_and_migrates_legacy_bool() {
|
||||
assert_eq!(BlurModeName::from_settings("none"), Some(BlurModeName::None));
|
||||
assert_eq!(BlurModeName::from_settings("off"), Some(BlurModeName::None));
|
||||
assert_eq!(BlurModeName::from_settings("mica"), Some(BlurModeName::Mica));
|
||||
assert_eq!(BlurModeName::from_settings("aero"), Some(BlurModeName::Aero));
|
||||
// 未公开 API 的名字(`ACCENT_ENABLE_BLURBEHIND`)也认,手改配置的人
|
||||
// 可能照着实现名写。
|
||||
assert_eq!(BlurModeName::from_settings("blurbehind"), Some(BlurModeName::Aero));
|
||||
assert_eq!(BlurModeName::from_settings("acrylic"), Some(BlurModeName::Acrylic));
|
||||
assert_eq!(BlurModeName::from_settings("frosted"), None);
|
||||
// 大小写与空白由 from_settings 归一(设置文件是手改的)。
|
||||
assert_eq!(BlurModeName::from_settings(" Acrylic "), Some(BlurModeName::Acrylic));
|
||||
assert_eq!(BlurModeName::from_settings(" AERO "), Some(BlurModeName::Aero));
|
||||
|
||||
// 旧壳布尔值迁移:关保持关,开落到 Mica(不是最贵的 Acrylic)。
|
||||
assert_eq!(BlurModeName::from_settings("0"), Some(BlurModeName::None));
|
||||
@@ -1081,12 +1079,15 @@ mod tests {
|
||||
|
||||
// settings_value 与 from_settings 必须互为逆运算,否则设置页存盘后
|
||||
// 重开会掉档。
|
||||
for mode in [BlurModeName::None, BlurModeName::Mica, BlurModeName::Acrylic] {
|
||||
for mode in
|
||||
[BlurModeName::None, BlurModeName::Mica, BlurModeName::Aero, BlurModeName::Acrylic]
|
||||
{
|
||||
assert_eq!(BlurModeName::from_settings(mode.settings_value()), Some(mode));
|
||||
}
|
||||
|
||||
assert!(!BlurModeName::None.enabled());
|
||||
assert!(BlurModeName::Mica.enabled());
|
||||
assert!(BlurModeName::Aero.enabled());
|
||||
assert!(BlurModeName::Acrylic.enabled());
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user