gecko-dev/widget/cocoa/VibrancyManager.mm
Markus Stange a0e9560488 Bug 1370040 - Use the public API -[NSVisualEffectView setMaskImage:] instead of the private API -[NSWindow _cornerMask]. r=spohl
The idea is the following:
Behind-window vibrancy is mostly rendered by the window server. For a given
vibrant region of a window, the window server renders a vibrancy "backdrop",
which is a blurred version of everything that's behind that region, modified
with a color tint and blended in some way. Then it puts our actual window
contents on top of that background.
The backdrop's shape is usually a rectangle. If we don't want it to be a
rectangle, we need to tell the window server about the shape that we want it to
be. We can't just "draw" a different shape in our own rendering, because our
own rendering is merely placed on top of the backdrop - but here we want to
modify the shape of the backdrop itself.

NSVisualEffectView lets us set a mask image on the view. If this view is the
content view of a window, then the view will automatically communicate the mask
image to the window server.

Traditionally, our popup windows have had a ChildView as their content view. If
we now want an NSVisualEffectView to be the content view of the window, then we
need to nest the ChildView inside that NSVisualEffectView.
But this NSVisualEffectView is only needed when the window is vibrant and the
vibrancy backdrop needs to have a certain shape. This is the case for our menus
which need to have rounded corners. If the window transitions to being
non-vibrant, or not needing a special shape, then we can go back to the way our
window's NSView hierarchy has worked traditionally. So we need to reparent
NSViews during those transitions.

MozReview-Commit-ID: Bo2VzjhhR0A

--HG--
extra : rebase_source : 9eb463cc68c16c3b9281b57455330969c5e2642c
2017-11-19 18:38:35 -05:00

260 lines
8.5 KiB
Plaintext

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "VibrancyManager.h"
#include "nsChildView.h"
#import <objc/message.h>
using namespace mozilla;
void
VibrancyManager::UpdateVibrantRegion(VibrancyType aType,
const LayoutDeviceIntRegion& aRegion)
{
if (aRegion.IsEmpty()) {
mVibrantRegions.Remove(uint32_t(aType));
return;
}
auto& vr = *mVibrantRegions.LookupOrAdd(uint32_t(aType));
vr.UpdateRegion(aRegion, mCoordinateConverter, mContainerView, ^() {
return this->CreateEffectView(aType);
});
}
void
VibrancyManager::ClearVibrantAreas() const
{
for (auto iter = mVibrantRegions.ConstIter(); !iter.Done(); iter.Next()) {
ClearVibrantRegion(iter.UserData()->Region());
}
}
void
VibrancyManager::ClearVibrantRegion(const LayoutDeviceIntRegion& aVibrantRegion) const
{
[[NSColor clearColor] set];
for (auto iter = aVibrantRegion.RectIter(); !iter.Done(); iter.Next()) {
NSRectFill(mCoordinateConverter.DevPixelsToCocoaPoints(iter.Get()));
}
}
@interface NSView(CurrentFillColor)
- (NSColor*)_currentFillColor;
@end
static NSColor*
AdjustedColor(NSColor* aFillColor, VibrancyType aType)
{
if (aType == VibrancyType::MENU && [aFillColor alphaComponent] == 1.0) {
// The opaque fill color that's used for the menu background when "Reduce
// vibrancy" is checked in the system accessibility prefs is too dark.
// This is probably because we're not using the right material for menus,
// see VibrancyManager::CreateEffectView.
return [NSColor colorWithDeviceWhite:0.96 alpha:1.0];
}
return aFillColor;
}
NSColor*
VibrancyManager::VibrancyFillColorForType(VibrancyType aType)
{
NSView* view = mVibrantRegions.LookupOrAdd(uint32_t(aType))->GetAnyView();
if (view && [view respondsToSelector:@selector(_currentFillColor)]) {
// -[NSVisualEffectView _currentFillColor] is the color that our view
// would draw during its drawRect implementation, if we hadn't
// disabled that.
return AdjustedColor([view _currentFillColor], aType);
}
return [NSColor whiteColor];
}
static void
DrawRectNothing(id self, SEL _cmd, NSRect aRect)
{
// The super implementation would clear the background.
// That's fine for views that are placed below their content, but our
// setup is different: Our drawn content is drawn to mContainerView, which
// sits below this EffectView. So we must not clear the background here,
// because we'd erase that drawn content.
// Of course the regular content drawing still needs to clear the background
// behind vibrant areas. This is taken care of by having nsNativeThemeCocoa
// return true from NeedToClearBackgroundBehindWidget for vibrant widgets.
}
static NSView*
HitTestNil(id self, SEL _cmd, NSPoint aPoint)
{
// This view must be transparent to mouse events.
return nil;
}
static BOOL
AllowsVibrancyYes(id self, SEL _cmd)
{
// Means that the foreground is blended using a vibrant blend mode.
return YES;
}
static Class
CreateEffectViewClass(BOOL aForegroundVibrancy, BOOL aIsContainer)
{
// Create a class called EffectView that inherits from NSVisualEffectView
// and overrides the methods -[NSVisualEffectView drawRect:] and
// -[NSView hitTest:].
Class NSVisualEffectViewClass = NSClassFromString(@"NSVisualEffectView");
const char* className = aForegroundVibrancy
? "EffectViewWithForegroundVibrancy" : "EffectViewWithoutForegroundVibrancy";
Class EffectViewClass = objc_allocateClassPair(NSVisualEffectViewClass, className, 0);
class_addMethod(EffectViewClass, @selector(drawRect:), (IMP)DrawRectNothing,
"v@:{CGRect={CGPoint=dd}{CGSize=dd}}");
if (!aIsContainer) {
class_addMethod(EffectViewClass, @selector(hitTest:), (IMP)HitTestNil,
"@@:{CGPoint=dd}");
}
if (aForegroundVibrancy) {
// Also override the -[NSView allowsVibrancy] method to return YES.
class_addMethod(EffectViewClass, @selector(allowsVibrancy), (IMP)AllowsVibrancyYes, "I@:");
}
return EffectViewClass;
}
static id
AppearanceForVibrancyType(VibrancyType aType)
{
Class NSAppearanceClass = NSClassFromString(@"NSAppearance");
switch (aType) {
case VibrancyType::LIGHT:
case VibrancyType::TOOLTIP:
case VibrancyType::MENU:
case VibrancyType::HIGHLIGHTED_MENUITEM:
case VibrancyType::SHEET:
case VibrancyType::SOURCE_LIST:
case VibrancyType::SOURCE_LIST_SELECTION:
case VibrancyType::ACTIVE_SOURCE_LIST_SELECTION:
return [NSAppearanceClass performSelector:@selector(appearanceNamed:)
withObject:@"NSAppearanceNameVibrantLight"];
case VibrancyType::DARK:
return [NSAppearanceClass performSelector:@selector(appearanceNamed:)
withObject:@"NSAppearanceNameVibrantDark"];
}
}
#if !defined(MAC_OS_X_VERSION_10_10) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_10
enum {
NSVisualEffectStateFollowsWindowActiveState,
NSVisualEffectStateActive,
NSVisualEffectStateInactive
};
enum {
NSVisualEffectMaterialTitlebar = 3
};
#endif
#if !defined(MAC_OS_X_VERSION_10_11) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_11
enum {
NSVisualEffectMaterialMenu = 5,
NSVisualEffectMaterialSidebar = 7
};
#endif
static NSUInteger
VisualEffectStateForVibrancyType(VibrancyType aType)
{
switch (aType) {
case VibrancyType::TOOLTIP:
case VibrancyType::MENU:
case VibrancyType::HIGHLIGHTED_MENUITEM:
case VibrancyType::SHEET:
// Tooltip and menu windows are never "key" and sheets always looks
// active, so we need to tell the vibrancy effect to look active
// regardless of window state.
return NSVisualEffectStateActive;
default:
return NSVisualEffectStateFollowsWindowActiveState;
}
}
static BOOL
HasVibrantForeground(VibrancyType aType)
{
switch (aType) {
case VibrancyType::MENU:
return YES;
default:
return NO;
}
}
#if !defined(MAC_OS_X_VERSION_10_12) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_12
enum {
NSVisualEffectMaterialSelection = 4
};
#endif
@interface NSView(NSVisualEffectViewMethods)
- (void)setState:(NSUInteger)state;
- (void)setMaterial:(NSUInteger)material;
- (void)setEmphasized:(BOOL)emphasized;
@end
/* static */ NSView*
VibrancyManager::CreateEffectView(VibrancyType aType, BOOL aIsContainer)
{
static Class EffectViewClasses[2][2] = {
{ CreateEffectViewClass(NO, NO), CreateEffectViewClass(NO, YES) },
{ CreateEffectViewClass(YES, NO), CreateEffectViewClass(YES, YES) }
};
Class EffectViewClass = EffectViewClasses[HasVibrantForeground(aType)][aIsContainer];
NSView* effectView = [[EffectViewClass alloc] initWithFrame:NSZeroRect];
[effectView performSelector:@selector(setAppearance:)
withObject:AppearanceForVibrancyType(aType)];
[effectView setState:VisualEffectStateForVibrancyType(aType)];
BOOL canUseElCapitanMaterials = nsCocoaFeatures::OnElCapitanOrLater();
if (aType == VibrancyType::MENU) {
// Before 10.11 there is no material that perfectly matches the menu
// look. Of all available material types, NSVisualEffectMaterialTitlebar
// is the one that comes closest.
[effectView setMaterial:canUseElCapitanMaterials ? NSVisualEffectMaterialMenu
: NSVisualEffectMaterialTitlebar];
} else if (aType == VibrancyType::SOURCE_LIST && canUseElCapitanMaterials) {
[effectView setMaterial:NSVisualEffectMaterialSidebar];
} else if (aType == VibrancyType::HIGHLIGHTED_MENUITEM ||
aType == VibrancyType::SOURCE_LIST_SELECTION ||
aType == VibrancyType::ACTIVE_SOURCE_LIST_SELECTION) {
[effectView setMaterial:NSVisualEffectMaterialSelection];
if ([effectView respondsToSelector:@selector(setEmphasized:)] &&
aType != VibrancyType::SOURCE_LIST_SELECTION) {
[effectView setEmphasized:YES];
}
}
return effectView;
}
static bool
ComputeSystemSupportsVibrancy()
{
#ifdef __x86_64__
return NSClassFromString(@"NSAppearance") &&
NSClassFromString(@"NSVisualEffectView");
#else
// objc_allocateClassPair doesn't work in 32 bit mode, so turn off vibrancy.
return false;
#endif
}
/* static */ bool
VibrancyManager::SystemSupportsVibrancy()
{
static bool supportsVibrancy = ComputeSystemSupportsVibrancy();
return supportsVibrancy;
}