Skip to content
Open
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
50 changes: 35 additions & 15 deletions src/bro_mac.mm
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ @interface BroWindow : NSWindow {
@property (nonatomic, strong) BroToolbar* navToolbar;
@property (nonatomic, strong) BroTabBar* tabBar;
@property (nonatomic, strong) NSView* shellSurface;
@property (nonatomic, strong) NSView* chromeSurface;
@property (nonatomic, strong) NSView* borderOverlay;
- (BOOL)glassBackdropVisible;
- (void)setGlassBackdropVisible:(BOOL)visible;
Expand Down Expand Up @@ -346,22 +347,40 @@ - (instancetype)init {
self.contentView = content;
g_shell_corner_radius = BroResolveShellCornerRadius(self);

// A single stable shell-wide blur removes the material boundary at the
// page edge without stretching Liquid Glass lensing across the content
// area. Opaque CEF pages are composited above it in the browser area.
// A single stable, non-lensing shell blur removes the material boundary
// at the page edge. Window-sized Liquid Glass corrupts its refractive
// backing texture on large Retina windows, so native lensing is reserved
// for the constant-height chrome and bounded controls above this surface.
// Opaque CEF pages are composited above it in the browser area.
NSView* shellSurface = [[NSView alloc] initWithFrame:frame];
shellSurface.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
NSView* shellContent =
BroInstallShellSurface(shellSurface, BroShellCornerRadius());
BroInstallShellSurface(shellSurface, BroShellCornerRadius());
[content addSubview:shellSurface];
_shellSurface = shellSurface;

// The native refractive surface is restricted to the constant-height
// chrome band. Its texture budget therefore stays stable as the window
// grows vertically, while AppKit can still render the same Liquid Glass
// treatment across the full toolbar width. Older systems use the shell
// backdrop directly and avoid stacking two visual-effect materials.
CGFloat toolbarY = frame.size.height - kToolbarHeight;
NSView* chromeSurface = [[NSView alloc]
initWithFrame:NSMakeRect(0, toolbarY, frame.size.width, kToolbarHeight)];
chromeSurface.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin;
NSView* chromeContent = chromeSurface;
if (@available(macOS 26.0, *)) {
if (BroShouldUseNativeGlass()) {
chromeContent = BroInstallGlassSurface(chromeSurface, 0.0);
}
}
[content addSubview:chromeSurface];
_chromeSurface = chromeSurface;

// Single chrome row at the very top; its content is inset past the
// traffic lights.
CGFloat toolbarY = frame.size.height - kToolbarHeight;
_navToolbar = [[BroToolbar alloc]
initWithFrame:NSMakeRect(0, toolbarY, frame.size.width, kToolbarHeight)];
[shellContent addSubview:_navToolbar];
initWithFrame:NSMakeRect(0, 0, frame.size.width, kToolbarHeight)];
[chromeContent addSubview:_navToolbar];
g_toolbar = _navToolbar;

// Tab strip inline in the toolbar row: after the nav buttons, before the
Expand Down Expand Up @@ -1072,9 +1091,8 @@ void UpdateWindowForViewportMode(BOOL mobile, BOOL animate,
// Cmd+S (View menu) hides the toolbar -- and the tab strip nested inside it,
// since BroTabBar is one of its subviews -- and the traffic lights, leaving
// an Arc-style frame: the page sits as a rounded card inset by
// kScreenshotModeGutter on all four sides, with the window's existing glass
// (the same material the chrome row already wears -- setGlassBackdropVisible:
// below) showing through the gutter, and the existing hairline
// kScreenshotModeGutter on all four sides, with the window's stable shell
// material showing through the gutter, and the existing hairline
// (BroWindowBorderView) still tracing the true window edge outside all of
// it. The active tab's container(s) already fill 100% of browserContainer's
// bounds in every mode -- desktop, mobile, split -- so animating just
Expand Down Expand Up @@ -1161,10 +1179,11 @@ static void ToggleScreenshotMode(void) {
contentBounds.size.height - kToolbarHeight);

if (!active) {
// Make the toolbar visible again before fading it in -- a hidden view's
// alpha animation never draws.
// Make the whole glass band visible again before fading it in -- a hidden
// view's alpha animation never draws.
g_main_window.chromeSurface.hidden = NO;
g_main_window.chromeSurface.alphaValue = 0.0;
g_toolbar.hidden = NO;
g_toolbar.alphaValue = 0.0;
}

// The card's own corner radius derives from the window's (bro_geometry's
Expand All @@ -1182,10 +1201,11 @@ static void ToggleScreenshotMode(void) {
: 0.0;

BroRunLayoutSpring(^{
g_toolbar.animator.alphaValue = active ? 0.0 : 1.0;
g_main_window.chromeSurface.animator.alphaValue = active ? 0.0 : 1.0;
g_main_window.browserContainer.animator.frame = containerTarget;
containerLayer.cornerRadius = cardRadius;
}, ^{
g_main_window.chromeSurface.hidden = active;
g_toolbar.hidden = active;
UpdateChromeLayout(); // final snap: exact per-tab/divider frames
BroHandler* handler = BroHandler::GetInstance();
Expand Down
18 changes: 8 additions & 10 deletions src/bro_motion.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,21 +52,23 @@ extern void BroRunLayoutSpring(void (^changes)(void),

extern void BroApplyElevation(NSView* view, BroElevation elevation);

// Shared material tokens for every glass surface. macOS 26 uses untinted
// Shared material tokens for bounded glass surfaces. macOS 26 uses untinted
// Regular glass; older systems retain an appearance-adaptive HUD fallback.
extern const CGFloat kBroGlassTintAlpha;
extern const CGFloat kBroGlassBorderWidth;
extern NSColor* BroGlassTintColor(void);
extern NSColor* BroGlassBorderColor(void);
extern NSGlassEffectViewStyle BroGlassEffectStyle(void)
API_AVAILABLE(macos(26.0));
// Runtime gate shared by every native surface. BRO_FORCE_LEGACY_GLASS=1 is a
// test seam for exercising the macOS 12–25 fallback on a macOS 26 machine.
// Runtime gate shared by native control and overlay surfaces.
// BRO_FORCE_LEGACY_GLASS=1 is a test seam for exercising the macOS 12–25
// fallback on a macOS 26 machine.
extern BOOL BroShouldUseNativeGlass(void);

// Installs the shell-sized native Liquid Glass backdrop on macOS 26. The
// returned host is NSGlassEffectView.contentView there; older systems receive
// the same content over the existing visual-effect fallback.
// Installs the stable window-sized AppKit backdrop. The shell deliberately
// stays non-lensing at every OS version because a full-window
// NSGlassEffectView corrupts its backing texture on large Retina windows;
// bounded controls and overlays still use native Liquid Glass on macOS 26.
extern NSView* BroInstallShellSurface(NSView* panel, CGFloat cornerRadius);

// Replaces the elevation's flat fill with the shared glass surface and
Expand All @@ -75,10 +77,6 @@ extern NSView* BroInstallShellSurface(NSView* panel, CGFloat cornerRadius);
// the existing HUD blur + tint.
// Call after BroApplyElevation and add all visible content to the result.
extern NSView* BroInstallGlassSurface(NSView* panel, CGFloat cornerRadius);
// Wraps related glass descendants in AppKit's batching/merging container on
// macOS 26 and returns |contentView| unchanged on the legacy path.
extern NSView* BroGlassContainerForContentView(NSView* contentView,
CGFloat spacing);
extern void BroOverlayShow(NSView* view);
extern void BroOverlayHide(NSView* view);

Expand Down
47 changes: 14 additions & 33 deletions src/bro_motion.mm
Original file line number Diff line number Diff line change
Expand Up @@ -329,23 +329,22 @@ - (void)viewDidChangeEffectiveAppearance {
CGFloat backdropRadius = BroCornerRadiusForSize(
BroNestedCornerRadius(cornerRadius, 0.0), panel.bounds.size);

if (@available(macOS 26.0, *)) {
if (BroShouldUseNativeGlass()) {
NSGlassEffectView* glass =
[[NSGlassEffectView alloc] initWithFrame:panel.bounds];
glass.cornerRadius = backdropRadius;
glass.style = BroGlassEffectStyle();
glass.tintColor = nil;
NSView* contentHost = [[NSView alloc] initWithFrame:glass.bounds];
contentHost.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
glass.contentView = contentHost;
BroPinView(glass, panel);
return contentHost;
}
}
// Keep the window-sized shell on AppKit's non-lensing material. A regular
// NSGlassEffectView allocates a refractive backdrop for its whole bounds;
// once a Retina window grows large enough, that surface starts dropping or
// stretching texture tiles during live move/resize. Liquid Glass remains on
// the bounded controls and overlays above this backdrop, where AppKit can
// render and batch it reliably.
//
// Under-window background is the native macOS 26 shell material. Older
// systems retain the established HUD blur.
NSVisualEffectView* backdrop =
[[NSVisualEffectView alloc] initWithFrame:panel.bounds];
backdrop.material = NSVisualEffectMaterialHUDWindow;
if (@available(macOS 26.0, *)) {
backdrop.material = NSVisualEffectMaterialUnderWindowBackground;
} else {
backdrop.material = NSVisualEffectMaterialHUDWindow;
}
backdrop.blendingMode = NSVisualEffectBlendingModeBehindWindow;
backdrop.state = NSVisualEffectStateActive;
backdrop.wantsLayer = YES;
Expand Down Expand Up @@ -417,24 +416,6 @@ - (void)viewDidChangeEffectiveAppearance {
return contentHost;
}

NSView* BroGlassContainerForContentView(NSView* contentView,
CGFloat spacing) {
if (!contentView) {
return nil;
}
if (@available(macOS 26.0, *)) {
if (BroShouldUseNativeGlass()) {
NSGlassEffectContainerView* container =
[[NSGlassEffectContainerView alloc]
initWithFrame:contentView.frame];
container.spacing = MAX(0.0, spacing);
container.contentView = contentView;
return container;
}
}
return contentView;
}

void BroOverlayShow(NSView* view) {
if (!view) {
return;
Expand Down
19 changes: 7 additions & 12 deletions src/bro_tabstrip.mm
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
static const CGFloat kHoveredTabBorderAlpha = 0.18;
// Resting pills keep the strip's 8pt gap. As a dragged pill comes within 6pt
// of a neighbor, AppKit's glass container performs the native merge/split.
static const CGFloat kTabGlassMergeSpacing = 6.0;
static const CFTimeInterval kTabColorTransitionDuration = 0.18;

static NSDictionary* BroTabLayerTransitionActions(void) {
Expand Down Expand Up @@ -419,9 +418,10 @@ void BroFetchFaviconGuarded(NSString* urlString,
}

@implementation BroTabView {
// Every macOS 26 pill owns a native glass descendant so the tab strip's
// NSGlassEffectContainerView can batch it and merge neighboring pills
// during drag. Older systems retain the established HUD fallback.
// Every macOS 26 pill owns a bounded native glass descendant. Keeping each
// effect independent prevents AppKit's rectangular batch texture from
// leaking behind the strip on large Retina windows. Older systems retain
// the established HUD fallback.
NSView* glassBackdrop_;
NSView* pillContentHost_;
BOOL nativeGlassBackdrop_;
Expand Down Expand Up @@ -461,8 +461,7 @@ - (instancetype)initWithFrame:(NSRect)frame browserId:(int)browserId {
self.focusRingType = NSFocusRingTypeNone;

// Native pills stay neutral; selection is layered through adaptive
// content and a restrained fill so all descendants remain sufficiently
// similar for NSGlassEffectContainerView to merge them.
// content and a restrained fill.
if (@available(macOS 26.0, *)) {
if (BroShouldUseNativeGlass()) {
NSGlassEffectView* glass =
Expand Down Expand Up @@ -1411,7 +1410,6 @@ - (void)scrollWheel:(NSEvent*)event {

@implementation BroTabBar {
BroHorizontalTabScrollView* tabScrollView_;
NSView* tabDocumentView_;
NSView* tabContentView_;
BroTabView* draggingTab_;
BOOL dragging_;
Expand Down Expand Up @@ -1464,9 +1462,7 @@ - (instancetype)initWithFrame:(NSRect)frame {
tabContentView_ =
[[NSView alloc] initWithFrame:NSMakeRect(0, 0, 0, frame.size.height)];
tabContentView_.wantsLayer = YES;
tabDocumentView_ = BroGlassContainerForContentView(
tabContentView_, kTabGlassMergeSpacing);
tabScrollView_.documentView = tabDocumentView_;
tabScrollView_.documentView = tabContentView_;
[self addSubview:tabScrollView_];

// Borderless "+" control, pinned between the scroll viewport and Search.
Expand Down Expand Up @@ -2113,8 +2109,7 @@ - (void)layOutTabsThroughAnimator {
}

CGFloat documentWidth = MAX(viewportWidth, x);
tabDocumentView_.frame = NSMakeRect(0, 0, documentWidth, height);
tabContentView_.frame = tabDocumentView_.bounds;
tabContentView_.frame = NSMakeRect(0, 0, documentWidth, height);
[self clampScrollOffset];

// The "+" trails the last pill by one gap (`x` already carries it) so it
Expand Down
51 changes: 22 additions & 29 deletions tests/bro_glass_surface_tests.mm
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,29 @@ int main(void) {
if (@available(macOS 26.0, *)) {
unsetenv("BRO_FORCE_LEGACY_GLASS");
NSView* shell = [[NSView alloc]
initWithFrame:NSMakeRect(0, 0, 640, 480)];
initWithFrame:NSMakeRect(0, 0, 2560, 1440)];
NSView* shellContent = BroInstallShellSurface(shell, 12.0);
NSGlassEffectView* shellGlass = (NSGlassEffectView*)
FindSubviewOfClass(shell, [NSGlassEffectView class]);
Require(shellGlass != nil, @"shell must use NSGlassEffectView");
Require(shellGlass.contentView == shellContent,
@"shell controls must live in native contentView");
Require(shellGlass.style == NSGlassEffectViewStyleRegular,
@"shell must use Regular glass");
Require(shellGlass.tintColor == nil,
@"shell glass must remain neutral");
Require(fabs(shellGlass.cornerRadius - 12.0) < 0.01,
@"shell radius must be handed to AppKit");
NSVisualEffectView* shellBackdrop = (NSVisualEffectView*)
FindSubviewOfClass(shell, [NSVisualEffectView class]);
Require(shellBackdrop != nil,
@"large shell must use the stable visual-effect backdrop");
Require(FindSubviewOfClass(shell, [NSGlassEffectView class]) == nil,
@"large shell must not allocate a window-sized glass texture");
Require(shellBackdrop.material ==
NSVisualEffectMaterialUnderWindowBackground,
@"macOS 26 shell must use the under-window material");
Require(shellContent.superview == shell,
@"shell content host must remain above the stable backdrop");

NSView* chrome = [[NSView alloc]
initWithFrame:NSMakeRect(0, 0, 2560, 52)];
NSView* chromeContent = BroInstallGlassSurface(chrome, 0.0);
NSGlassEffectView* chromeGlass = (NSGlassEffectView*)
FindSubviewOfClass(chrome, [NSGlassEffectView class]);
Require(chromeGlass != nil,
@"wide constant-height chrome must retain native glass");
Require(chromeGlass.contentView == chromeContent,
@"chrome controls must live in native contentView");

NSView* panel = [[NSView alloc]
initWithFrame:NSMakeRect(0, 0, 320, 180)];
Expand All @@ -58,18 +68,6 @@ int main(void) {
Require(fabs(panelGlass.cornerRadius - 10.0) < 0.01,
@"overlay radius must be handed to AppKit");

NSView* tabs = [[NSView alloc]
initWithFrame:NSMakeRect(0, 0, 600, 52)];
NSView* tabDocument = BroGlassContainerForContentView(tabs, 6.0);
Require([tabDocument isKindOfClass:[NSGlassEffectContainerView class]],
@"tab content must use NSGlassEffectContainerView");
NSGlassEffectContainerView* tabContainer =
(NSGlassEffectContainerView*)tabDocument;
Require(tabContainer.contentView == tabs,
@"tab content must be owned by the native container");
Require(fabs(tabContainer.spacing - 6.0) < 0.01,
@"tab merge proximity must be handed to AppKit");

setenv("BRO_FORCE_LEGACY_GLASS", "1", 1);
NSView* fallback = [[NSView alloc]
initWithFrame:NSMakeRect(0, 0, 320, 180)];
Expand All @@ -80,11 +78,6 @@ int main(void) {
@"forced fallback must retain NSVisualEffectView");
Require(fallbackContent.superview == fallback,
@"fallback content host must remain usable");
NSView* fallbackTabs = [[NSView alloc]
initWithFrame:NSMakeRect(0, 0, 600, 52)];
Require(BroGlassContainerForContentView(fallbackTabs, 6.0) ==
fallbackTabs,
@"legacy tab content must bypass the unavailable container");
}
}
return 0;
Expand Down
Loading