diff --git a/nebula_app/src/gpui_shell/mod.rs b/nebula_app/src/gpui_shell/mod.rs index cc6db3a..e0fdd76 100644 --- a/nebula_app/src/gpui_shell/mod.rs +++ b/nebula_app/src/gpui_shell/mod.rs @@ -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. diff --git a/nebula_app/src/gpui_shell/settings_pane.rs b/nebula_app/src/gpui_shell/settings_pane.rs index 6e444e3..0ce0f3a 100644 --- a/nebula_app/src/gpui_shell/settings_pane.rs +++ b/nebula_app/src/gpui_shell/settings_pane.rs @@ -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, diff --git a/nebula_app/src/gpui_shell/wallpaper.rs b/nebula_app/src/gpui_shell/wallpaper.rs index 24a95cc..dbf9696 100644 --- a/nebula_app/src/gpui_shell/wallpaper.rs +++ b/nebula_app/src/gpui_shell/wallpaper.rs @@ -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, +} 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::().map(|v| v.blur).unwrap_or(BlurModeName::None); - if cx.try_global::().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::>(); + let already_applied = cx + .try_global::() + .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::>(); + 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::() 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::() 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>> = const { std::cell::RefCell::new(None) }; } /// 生成(并缓存)噪点 tile。 +#[allow(dead_code)] fn noise_tile() -> Arc { NOISE_TILE.with(|slot| { if let Some(tile) = slot.borrow().as_ref() { @@ -511,24 +550,20 @@ fn noise_tile() -> Arc { }) } -/// 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, window: &mut Window, cx: &App) { let Some(effects) = cx.try_global::() else { return }; if effects.blur != BlurModeName::Mica { diff --git a/nebula_app/src/gpui_shell/windows_backdrop.rs b/nebula_app/src/gpui_shell/windows_backdrop.rs new file mode 100644 index 0000000..6c212ec --- /dev/null +++ b/nebula_app/src/gpui_shell/windows_backdrop.rs @@ -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 { + // 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) } +} diff --git a/nebula_app/src/gpui_shell/workspace.rs b/nebula_app/src/gpui_shell/workspace.rs index 8dd70dd..a6e71c3 100644 --- a/nebula_app/src/gpui_shell/workspace.rs +++ b/nebula_app/src/gpui_shell/workspace.rs @@ -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, ); diff --git a/nebula_settings/src/lib.rs b/nebula_settings/src/lib.rs index 64ece0d..990c89b 100644 --- a/nebula_settings/src/lib.rs +++ b/nebula_settings/src/lib.rs @@ -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()); }