// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/theme_helper_mac.h"
#import <Carbon/Carbon.h>
#import <Cocoa/Cocoa.h>
#include "base/command_line.h"
#include "base/containers/span.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/mac/mac_util.h"
#include "base/strings/sys_string_conversions.h"
#include "content/browser/renderer_host/render_process_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/renderer.mojom.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_switches.h"
#include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
using content::RenderProcessHost;
using content::RenderProcessHostImpl;
using content::ThemeHelperMac;
namespace {
void FillScrollbarThemeParams(
content::mojom::UpdateScrollbarThemeParams* params) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
NSUserDefaults* defaults = NSUserDefaults.standardUserDefaults;
[defaults synchronize];
// NSScrollerButtonDelay and NSScrollerButtonPeriod are no longer initialized
// in +[NSApplication _initializeRegisteredDefaults] as of 10.15. Their values
// still seem to affect behavior, but their use is logged as an "unusual app
// config", so it's not clear how much longer they'll be implemented.
params->has_initial_button_delay =
[defaults objectForKey:@"NSScrollerButtonDelay"] != nil;
params->initial_button_delay =
[defaults floatForKey:@"NSScrollerButtonDelay"];
params->has_autoscroll_button_delay =
[defaults objectForKey:@"NSScrollerButtonPeriod"] != nil;
params->autoscroll_button_delay =
[defaults floatForKey:@"NSScrollerButtonPeriod"];
params->jump_on_track_click =
[defaults boolForKey:@"AppleScrollerPagingBehavior"];
params->preferred_scroller_style =
static_cast<blink::ScrollerStyle>([NSScroller preferredScrollerStyle]);
id rubber_band_value = [defaults objectForKey:@"NSScrollViewRubberbanding"];
params->scroll_view_rubber_banding =
rubber_band_value ? [rubber_band_value boolValue] : YES;
}
void SendSystemColorsChangedMessage(content::mojom::Renderer* renderer) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
NSUserDefaults* defaults = NSUserDefaults.standardUserDefaults;
[defaults synchronize];
renderer->OnSystemColorsChanged(
[[defaults stringForKey:@"AppleAquaColorVariant"] intValue]);
}
SkColor NSColorToSkColor(NSColor* color) {
NSColor* color_in_color_space =
[color colorUsingColorSpace:NSColorSpace.sRGBColorSpace];
if (color_in_color_space) {
// Use nextafter() to avoid rounding colors in a way that could be off-by-
// one. See https://bugs.webkit.org/show_bug.cgi?id=6129.
static const double kScaleFactor = nextafter(256.0, 0.0);
return SkColorSetARGB(
static_cast<int>(kScaleFactor * [color_in_color_space alphaComponent]),
static_cast<int>(kScaleFactor * [color_in_color_space redComponent]),
static_cast<int>(kScaleFactor * [color_in_color_space greenComponent]),
static_cast<int>(kScaleFactor * [color_in_color_space blueComponent]));
}
// This conversion above can fail if the NSColor in question is an
// NSPatternColor (as many system colors are). These colors are actually a
// repeating pattern not just a solid color. To work around this we simply
// draw a 1x1 image of the color and use that pixel's color. It might be
// better to use an average of the colors in the pattern instead.
NSBitmapImageRep* offscreen_rep =
[[NSBitmapImageRep alloc] initWithBitmapDataPlanes:nil
pixelsWide:1
pixelsHigh:1
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSDeviceRGBColorSpace
bytesPerRow:4
bitsPerPixel:32];
{
gfx::ScopedNSGraphicsContextSaveGState gstate;
NSGraphicsContext.currentContext =
[NSGraphicsContext graphicsContextWithBitmapImageRep:offscreen_rep];
[color set];
NSRectFill(NSMakeRect(0, 0, 1, 1));
}
NSUInteger pixel[4];
[offscreen_rep getPixel:pixel atX:0 y:0];
// This recursive call will not recurse again, because the color space
// the second time around is NSDeviceRGBColorSpace.
return NSColorToSkColor([NSColor colorWithDeviceRed:pixel[0] / 255.
green:pixel[1] / 255.
blue:pixel[2] / 255.
alpha:1.]);
}
} // namespace
@interface SystemThemeObserver : NSObject {
base::RepeatingClosure _colorsChangedCallback;
}
- (instancetype)initWithColorsChangedCallback:
(base::RepeatingClosure)colorsChangedCallback;
- (void)appearancePrefsChanged:(NSNotification*)notification;
- (void)behaviorPrefsChanged:(NSNotification*)notification;
- (void)notifyPrefsChangedWithRedraw:(BOOL)redraw;
@end
@implementation SystemThemeObserver
- (instancetype)initWithColorsChangedCallback:
(base::RepeatingClosure)colorsChangedCallback {
if (!(self = [super init])) {
return nil;
}
_colorsChangedCallback = std::move(colorsChangedCallback);
NSDistributedNotificationCenter* distributedCenter =
NSDistributedNotificationCenter.defaultCenter;
[distributedCenter addObserver:self
selector:@selector(appearancePrefsChanged:)
name:@"AppleAquaScrollBarVariantChanged"
object:nil
suspensionBehavior:
NSNotificationSuspensionBehaviorDeliverImmediately];
[distributedCenter addObserver:self
selector:@selector(behaviorPrefsChanged:)
name:@"AppleNoRedisplayAppearancePreferenceChanged"
object:nil
suspensionBehavior:NSNotificationSuspensionBehaviorCoalesce];
[distributedCenter addObserver:self
selector:@selector(behaviorPrefsChanged:)
name:@"NSScrollAnimationEnabled"
object:nil
suspensionBehavior:NSNotificationSuspensionBehaviorCoalesce];
[distributedCenter addObserver:self
selector:@selector(appearancePrefsChanged:)
name:@"AppleScrollBarVariant"
object:nil
suspensionBehavior:
NSNotificationSuspensionBehaviorDeliverImmediately];
[distributedCenter
addObserver:self
selector:@selector(behaviorPrefsChanged:)
name:@"NSScrollViewRubberbanding"
object:nil
suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately];
// In single-process mode, renderers will catch these notifications
// themselves and listening for them here may trigger the DCHECK in Observe().
if (!base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kSingleProcess)) {
NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
[center addObserver:self
selector:@selector(behaviorPrefsChanged:)
name:NSPreferredScrollerStyleDidChangeNotification
object:nil];
[center addObserver:self
selector:@selector(systemColorsChanged:)
name:NSSystemColorsDidChangeNotification
object:nil];
}
return self;
}
- (void)dealloc {
[NSDistributedNotificationCenter.defaultCenter removeObserver:self];
}
- (void)appearancePrefsChanged:(NSNotification*)notification {
[self notifyPrefsChangedWithRedraw:YES];
}
- (void)behaviorPrefsChanged:(NSNotification*)notification {
[self notifyPrefsChangedWithRedraw:NO];
}
- (void)systemColorsChanged:(NSNotification*)notification {
_colorsChangedCallback.Run();
for (RenderProcessHost::iterator it(RenderProcessHost::AllHostsIterator());
!it.IsAtEnd();
it.Advance()) {
SendSystemColorsChangedMessage(
it.GetCurrentValue()->GetRendererInterface());
}
}
- (void)notifyPrefsChangedWithRedraw:(BOOL)redraw {
for (RenderProcessHost::iterator it(RenderProcessHost::AllHostsIterator());
!it.IsAtEnd();
it.Advance()) {
content::mojom::UpdateScrollbarThemeParamsPtr params =
content::mojom::UpdateScrollbarThemeParams::New();
FillScrollbarThemeParams(params.get());
params->redraw = redraw;
RenderProcessHostImpl* process_host =
static_cast<RenderProcessHostImpl*>(it.GetCurrentValue());
process_host->GetRendererInterface()->UpdateScrollbarTheme(
std::move(params));
}
for (content::WebContentsImpl* web_contents :
content::WebContentsImpl::GetAllWebContents()) {
web_contents->OnWebPreferencesChanged();
}
}
@end
namespace content {
struct ThemeHelperMac::ObjCStorage {
// ObjC object that observes notifications from the system.
SystemThemeObserver* __strong theme_observer;
};
// static
ThemeHelperMac* ThemeHelperMac::GetInstance() {
static ThemeHelperMac* instance = new ThemeHelperMac();
return instance;
}
base::ReadOnlySharedMemoryRegion
ThemeHelperMac::DuplicateReadOnlyColorMapRegion() {
return read_only_color_map_.Duplicate();
}
ThemeHelperMac::ThemeHelperMac()
: objc_storage_(std::make_unique<ObjCStorage>()) {
// Allocate a region for the SkColor value table and map it.
auto writable_region = base::WritableSharedMemoryRegion::Create(
sizeof(SkColor) * blink::kMacSystemColorIDCount *
blink::kMacSystemColorSchemeCount);
writable_color_map_ = writable_region.Map();
// Downgrade the region to read-only after it has been mapped.
read_only_color_map_ = base::WritableSharedMemoryRegion::ConvertToReadOnly(
std::move(writable_region));
// Store the current color scheme into the table.
LoadSystemColors();
// Start observing for changes.
objc_storage_->theme_observer = [[SystemThemeObserver alloc]
initWithColorsChangedCallback:base::BindRepeating(
&ThemeHelperMac::LoadSystemColors,
base::Unretained(this))];
}
ThemeHelperMac::~ThemeHelperMac() = default;
void ThemeHelperMac::LoadSystemColorsForCurrentAppearance(
base::span<SkColor> values) {
for (size_t i = 0; i < blink::kMacSystemColorIDCount; ++i) {
blink::MacSystemColorID color_id = static_cast<blink::MacSystemColorID>(i);
switch (color_id) {
case blink::MacSystemColorID::kControlAccentBlueColor: {
NSColor* color =
[NSColor colorWithCatalogName:@"System"
colorName:@"controlAccentBlueColor"];
if (color) {
values[i] = NSColorToSkColor(color);
} else {
// If the controlAccentBlueColor isn't available just set a black
// value.
values[i] = SK_ColorBLACK;
}
break;
}
case blink::MacSystemColorID::kControlAccentColor:
values[i] = NSColorToSkColor(NSColor.controlAccentColor);
break;
case blink::MacSystemColorID::kKeyboardFocusIndicator:
values[i] = NSColorToSkColor(NSColor.keyboardFocusIndicatorColor);
break;
case blink::MacSystemColorID::kSecondarySelectedControl:
values[i] = NSColorToSkColor(
NSColor.unemphasizedSelectedContentBackgroundColor);
break;
case blink::MacSystemColorID::kSelectedTextBackground:
values[i] = NSColorToSkColor(NSColor.selectedTextBackgroundColor);
break;
case blink::MacSystemColorID::kCount:
NOTREACHED_IN_MIGRATION();
break;
}
}
}
void ThemeHelperMac::LoadSystemColors() {
static_assert(blink::kMacSystemColorSchemeCount == 2,
"Light and dark color scheme system colors loaded.");
base::span<SkColor> values = writable_color_map_.GetMemoryAsSpan<SkColor>(
blink::kMacSystemColorIDCount * blink::kMacSystemColorSchemeCount);
[[NSAppearance appearanceNamed:NSAppearanceNameAqua]
performAsCurrentDrawingAppearance:^{
LoadSystemColorsForCurrentAppearance(values.subspan(
0, static_cast<size_t>(blink::MacSystemColorID::kCount)));
}];
[[NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]
performAsCurrentDrawingAppearance:^{
LoadSystemColorsForCurrentAppearance(values.subspan(
static_cast<size_t>(blink::MacSystemColorID::kCount),
static_cast<size_t>(blink::MacSystemColorID::kCount)));
}];
}
void ThemeHelperMac::OnRenderProcessHostCreated(
content::RenderProcessHost* host) {
// When a new RenderProcess is created, send it the initial preference
// parameters.
content::mojom::UpdateScrollbarThemeParamsPtr params =
content::mojom::UpdateScrollbarThemeParams::New();
FillScrollbarThemeParams(params.get());
params->redraw = false;
RenderProcessHostImpl* process_host =
static_cast<content::RenderProcessHostImpl*>(host);
content::mojom::Renderer* renderer = process_host->GetRendererInterface();
renderer->UpdateScrollbarTheme(std::move(params));
SendSystemColorsChangedMessage(renderer);
}
} // namespace content