Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions crates/app/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,12 @@ fn translucent_canvas_enabled() -> bool {
}

fn main_window_background() -> WindowBackgroundAppearance {
if cfg!(target_os = "windows") || vibrancy_enabled() {
if cfg!(target_os = "windows") {
WindowBackgroundAppearance::Blurred
} else if vibrancy_enabled() {
// `run_shell` slides a stock `NSVisualEffectView` under a transparent
// window; GPUI's `Blurred` material stopped blurring on macOS 27.
WindowBackgroundAppearance::Transparent
} else {
WindowBackgroundAppearance::Opaque
}
Expand Down Expand Up @@ -467,7 +471,7 @@ fn main() {
window_decorations: cfg!(target_os = "windows")
.then_some(WindowDecorations::Client),
// Persistent windows use the platform's system material:
// macOS blur, Windows Acrylic, or an opaque fallback.
// macOS sidebar vibrancy, Windows Acrylic, or an opaque fallback.
window_background: main_window_background(),
// Throttle background redraws (spinners, streaming output) to
// ~2 FPS while the window is inactive; gpui lifts the cap the
Expand Down
11 changes: 4 additions & 7 deletions crates/ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,12 @@ native-preview = [
"dep:gpui-android",
"dep:gpui-wry",
"dep:wry",
"dep:raw-window-handle",
"dep:preview-mcp",
"dep:async-lock",
"dep:objc2-core-foundation",
"dep:objc2-image-io",
"dep:objc2-web-kit",
"dep:block2",
"dep:objc2",
"dep:objc2-app-kit",
"dep:objc2-foundation",
"dep:webview2-com",
"dep:windows-core",
Expand All @@ -51,7 +48,7 @@ voice = ["dep:tcode-voice"]
# macOS TCC permission status, grant and relaunch for a *local* attachment.
local-permissions = ["dep:computer-use-mcp"]
# Reading an image off the macOS AppKit pasteboard.
pasteboard-image = ["dep:objc2-app-kit", "dep:objc2-foundation"]
pasteboard-image = ["dep:objc2-foundation"]

[dependencies]
url = "2"
Expand Down Expand Up @@ -98,7 +95,7 @@ reqwest = { package = "gpui-pre-reqwest", version = "0.12.15", default-features
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
gpui-wry = { version = "0.6.0", optional = true }
wry = { version = "0.53.3", package = "lb-wry", features = ["mac-proxy"], optional = true }
raw-window-handle = { version = "0.6", features = ["std"], optional = true }
raw-window-handle = { version = "0.6", features = ["std"] }
# Only the Windows WebContext needs it, but Cargo has no per-feature target
# narrower than this.
async-lock = { version = "3", optional = true }
Expand All @@ -107,8 +104,8 @@ async-lock = { version = "3", optional = true }
tcode-voice = { path = "../voice", optional = true }
computer-use-mcp = { path = "../computer-use-mcp", optional = true }
block2 = { version = "0.6.2", optional = true }
objc2 = { version = "0.6.4", optional = true }
objc2-app-kit = { version = "0.3.2", features = ["NSGraphicsContext", "NSImage", "NSImageRep", "NSPasteboard", "objc2-core-graphics"], optional = true }
objc2 = "0.6.4"
objc2-app-kit = { version = "0.3.2", features = ["NSAppearance", "NSGraphics", "NSGraphicsContext", "NSImage", "NSImageRep", "NSPasteboard", "NSResponder", "NSView", "NSVisualEffectView", "NSWindow", "objc2-core-graphics"] }
objc2-foundation = { version = "0.3.2", features = ["NSDictionary", "NSError", "NSString", "NSURLError", "NSURLRequest", "block2"], optional = true }

# The macOS WKWebView snapshot backend, and the navigation delegate the
Expand Down
2 changes: 1 addition & 1 deletion crates/ui/src/chat/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2609,7 +2609,7 @@ impl Render for ChatView {
let root = v_flex().size_full().min_w_0().bg(if compact {
crate::material::content_surface(cx)
} else {
cx.theme().background
crate::material::canvas(cx)
});

if self.workspace_store.read(cx).chat_loading() {
Expand Down
2 changes: 2 additions & 0 deletions crates/ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ pub mod icon;
mod local_permissions;
pub mod markdown;
// Shared material helpers are also used by the phone shell.
#[cfg(target_os = "macos")]
mod macos_backdrop;
pub mod material;
mod orchestrate_settings;
pub mod overlay;
Expand Down
76 changes: 76 additions & 0 deletions crates/ui/src/macos_backdrop.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//! The macOS window backdrop: one semantic `NSVisualEffectView` under the GPUI
//! view, left exactly as AppKit builds it.
//!
//! GPUI's own `WindowBackgroundAppearance::Blurred` strips the material's tint
//! layers and relies on the `.selection` material for the blur; on macOS 27
//! `.selection` no longer carries a backdrop layer at all, so that path renders
//! no blur (#445). A stock semantic material blurs on every release, follows the
//! window's active state, and honours Reduce Transparency and the macOS 27
//! Liquid Glass slider without any tint of our own on top.

use gpui::{App, Global, Window};
use objc2::rc::Retained;
use objc2::{MainThreadMarker, MainThreadOnly as _};
use objc2_app_kit::{
NSAppearance, NSAppearanceCustomization as _, NSAppearanceNameAqua, NSAppearanceNameDarkAqua,
NSAutoresizingMaskOptions, NSView, NSVisualEffectBlendingMode, NSVisualEffectMaterial,
NSVisualEffectState, NSVisualEffectView, NSWindowOrderingMode,
};
use raw_window_handle::{HasWindowHandle, RawWindowHandle};

use crate::theme::ThemeMode;

struct SystemBackdrop(Retained<NSVisualEffectView>);
impl Global for SystemBackdrop {}

/// Whether the window carries a system material that already tints the
/// backdrop, so the theme canvas must not tint it a second time.
pub(crate) fn installed(cx: &App) -> bool {
cx.has_global::<SystemBackdrop>()
}

/// Slide the material under GPUI's view. A no-op if the native window cannot be
/// reached, in which case the theme canvas keeps tinting the transparent window.
pub(crate) fn install(window: &Window, cx: &mut App) {
let Ok(handle) = HasWindowHandle::window_handle(window) else {
return;
};
let RawWindowHandle::AppKit(handle) = handle.as_raw() else {
return;
};
let Some(mtm) = MainThreadMarker::new() else {
return;
};
// SAFETY: GPUI hands out its live NSView, which outlives this call.
let gpui_view: &NSView = unsafe { handle.ns_view.cast().as_ref() };
let Some(content) = gpui_view.window().and_then(|window| window.contentView()) else {
return;
};
let effect =
NSVisualEffectView::initWithFrame(NSVisualEffectView::alloc(mtm), content.bounds());
effect.setMaterial(NSVisualEffectMaterial::Sidebar);
effect.setBlendingMode(NSVisualEffectBlendingMode::BehindWindow);
effect.setState(NSVisualEffectState::FollowsWindowActiveState);
effect.setAutoresizingMask(
NSAutoresizingMaskOptions::ViewWidthSizable | NSAutoresizingMaskOptions::ViewHeightSizable,
);
content.addSubview_positioned_relativeTo(&effect, NSWindowOrderingMode::Below, None);
cx.set_global(SystemBackdrop(effect));
}

/// The material follows the window's appearance, but the theme mode can be
/// forced from Settings; pin the material to the theme so a dark palette never
/// sits on a light material.
pub(crate) fn sync_appearance(mode: ThemeMode, cx: &App) {
let Some(backdrop) = cx.try_global::<SystemBackdrop>() else {
return;
};
let name = if mode.is_dark() {
unsafe { NSAppearanceNameDarkAqua }
} else {
unsafe { NSAppearanceNameAqua }
};
backdrop
.0
.setAppearance(NSAppearance::appearanceNamed(name).as_deref());
}
11 changes: 11 additions & 0 deletions crates/ui/src/material.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ fn rgba(r: u8, g: u8, b: u8, a: u8) -> Hsla {
.into()
}

/// T0 canvas: the theme's translucent tint over the native window material.
/// Transparent when the macOS system material already tints the backdrop
/// (`macos_backdrop`), so the two tints do not stack.
pub fn canvas(cx: &App) -> Hsla {
#[cfg(target_os = "macos")]
if crate::macos_backdrop::installed(cx) {
return gpui::transparent_black();
}
cx.theme().background
}

/// Flatten the canvas over fullscreen vibrancy, where its translucent color
/// would otherwise composite against black.
pub fn opaque_canvas(cx: &App) -> Hsla {
Expand Down
2 changes: 1 addition & 1 deletion crates/ui/src/overlay/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ impl Render for OverlayHost {
cx.stop_propagation();
}
})
.bg(cx.theme().background)
.bg(crate::material::canvas(cx))
.text_color(cx.theme().foreground)
.font_family(cx.theme().font_family.clone())
.child(self.view.clone())
Expand Down
12 changes: 12 additions & 0 deletions crates/ui/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ pub fn run_shell(
},
));
let captured = mounted.clone();
#[cfg(target_os = "macos")]
let window_background = options.window.window_background;
let window = cx
.open_window(options.window, move |window, cx| {
window.set_window_title(&title);
Expand All @@ -176,6 +178,16 @@ pub fn run_shell(
cx.new(|cx| OverlayHost::new(shell, window, cx))
})
.expect("failed to open the tcode window");
// A transparent macOS window gets its blur from a stock semantic material
// rather than GPUI's `Blurred` path; see `macos_backdrop`.
#[cfg(target_os = "macos")]
if window_background == gpui::WindowBackgroundAppearance::Transparent {
use crate::theme::ActiveTheme as _;
let _ = window.update(cx, |_, window, cx| {
crate::macos_backdrop::install(window, cx);
theme::change_mode(cx.theme().mode, Some(window), cx);
});
}
let shell = mounted
.borrow_mut()
.take()
Expand Down
2 changes: 2 additions & 0 deletions crates/ui/src/theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,8 @@ pub fn change_mode(mode: ThemeMode, window: Option<&mut Window>, cx: &mut App) {
};
cx.set_global(base_theme);
cx.set_global(theme);
#[cfg(target_os = "macos")]
crate::macos_backdrop::sync_appearance(mode, cx);
if let Some(window) = window {
window.refresh();
}
Expand Down
27 changes: 19 additions & 8 deletions docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ The material layers are:

| Layer | Use | Treatment |
| --- | --- | --- |
| T0 | Sidebar and window edges | Translucent theme canvas over the native window material |
| T0 | Sidebar and window edges | Native window material; the theme canvas tints it only where the material carries no tint of its own (Windows) |
| T1 | Chat, right panel and Settings reading surfaces | Near-opaque warm paper in light mode, blue carbon in dark mode |
| T2 | Inline fields, hover and selection | Theme-derived tints |
| T3 | Composer, popovers, dialogs, menus and toasts | Opaque popover fill, hairline border and soft shadow |
Expand All @@ -128,13 +128,24 @@ from the command palette and sheets. They darken the page in both themes.

## Window material

The persistent main window uses native backdrop material: macOS keeps its
existing blurred vibrancy, while Windows deliberately uses GPUI's
`WindowBackgroundAppearance::Blurred`, which the locked Windows backend maps to
Acrylic Accent state 4. Mica was rejected because it did not provide the
perceptible live background-through blur required in the exposed T0 sidebar and
window-edge regions. Both native materials retain the embedded theme's
translucent canvas so the system backdrop can show through.
The persistent main window uses native backdrop material. macOS opens a
`WindowBackgroundAppearance::Transparent` window and slides a stock
`NSVisualEffectView` (`.sidebar` material, behind-window blending, following
the window's active state) under the GPUI view
([macos_backdrop.rs](../crates/ui/src/macos_backdrop.rs)). The material is
left exactly as AppKit builds it, so it follows Reduce Transparency and the
macOS 27 Liquid Glass slider, and it is pinned to the theme mode so a forced
dark palette never sits on a light material. Because the system tint already
carries the T0 tone, the theme canvas ([`material::canvas`](../crates/ui/src/material.rs))
is fully transparent on macOS. GPUI's own `Blurred` path is not used on macOS:
it strips the material's tint layers and depends on the `.selection` material
for blur, which carries no backdrop layer on macOS 27.

Windows deliberately uses GPUI's `WindowBackgroundAppearance::Blurred`, which
the locked Windows backend maps to Acrylic Accent state 4. Mica was rejected
because it did not provide the perceptible live background-through blur
required in the exposed T0 sidebar and window-edge regions. Acrylic carries no
tint of its own, so Windows keeps the embedded theme's translucent canvas over it.
`TCODE_NO_VIBRANCY=1` keeps its macOS-only
diagnostic behavior: an opaque window with a flattened canvas. Linux and other
platforms remain opaque and flatten that canvas to its solid RGB base. In-app
Expand Down