1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "AppearanceOverride.h"
7 #include "mozilla/widget/ThemeChangeKind.h"
8 #include "nsLookAndFeel.h"
9 #include "nsCocoaFeatures.h"
10 #include "nsNativeThemeColors.h"
11 #include "nsStyleConsts.h"
12 #include "nsIContent.h"
14 #include "gfxFontConstants.h"
15 #include "gfxPlatformMac.h"
16 #include "nsCSSColorUtils.h"
17 #include "mozilla/FontPropertyTypes.h"
18 #include "mozilla/gfx/2D.h"
19 #include "mozilla/StaticPrefs_widget.h"
20 #include "mozilla/Telemetry.h"
21 #include "mozilla/widget/WidgetMessageUtils.h"
23 #import <Cocoa/Cocoa.h>
24 #import <AppKit/NSColor.h>
26 // This must be included last:
27 #include "nsObjCExceptions.h"
29 using namespace mozilla;
31 @interface MOZLookAndFeelDynamicChangeObserver : NSObject
32 + (void)startObserving;
35 nsLookAndFeel::nsLookAndFeel() {
36 [MOZLookAndFeelDynamicChangeObserver startObserving];
39 nsLookAndFeel::~nsLookAndFeel() = default;
41 void nsLookAndFeel::EnsureInit() {
46 NS_OBJC_BEGIN_TRY_ABORT_BLOCK
50 [[NSWindow alloc] initWithContentRect:NSZeroRect
51 styleMask:NSWindowStyleMaskTitled
52 backing:NSBackingStoreBuffered
54 auto release = MakeScopeExit([&] { [window release]; });
56 mRtl = window.windowTitlebarLayoutDirection ==
57 NSUserInterfaceLayoutDirectionRightToLeft;
58 mTitlebarHeight = std::ceil(window.frame.size.height);
62 NS_OBJC_END_TRY_ABORT_BLOCK
65 void nsLookAndFeel::RefreshImpl() {
67 nsXPLookAndFeel::RefreshImpl();
70 static nscolor GetColorFromNSColor(NSColor* aColor) {
71 NSColor* deviceColor =
72 [aColor colorUsingColorSpace:NSColorSpace.deviceRGBColorSpace];
73 return NS_RGBA((unsigned int)(deviceColor.redComponent * 255.0),
74 (unsigned int)(deviceColor.greenComponent * 255.0),
75 (unsigned int)(deviceColor.blueComponent * 255.0),
76 (unsigned int)(deviceColor.alphaComponent * 255.0));
79 static nscolor GetColorFromNSColorWithCustomAlpha(NSColor* aColor,
81 NSColor* deviceColor =
82 [aColor colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]];
83 return NS_RGBA((unsigned int)(deviceColor.redComponent * 255.0),
84 (unsigned int)(deviceColor.greenComponent * 255.0),
85 (unsigned int)(deviceColor.blueComponent * 255.0),
86 (unsigned int)(alpha * 255.0));
89 // Turns an opaque selection color into a partially transparent selection color,
90 // which usually leads to better contrast with the text color and which should
91 // look more visually appealing in most contexts.
92 // The idea is that the text and its regular, non-selected background are
93 // usually chosen in such a way that they contrast well. Making the selection
94 // color partially transparent causes the selection color to mix with the text's
95 // regular background, so the end result will often have better contrast with
96 // the text than an arbitrary opaque selection color.
97 // The motivating example for this is the light selection color on dark web
98 // pages: White text on a light blue selection color has very bad contrast,
99 // whereas white text on dark blue (which what you get if you mix
100 // partially-transparent light blue with the black textbox background) has much
102 static nscolor ProcessSelectionBackground(nscolor aColor, ColorScheme aScheme) {
103 if (aScheme == ColorScheme::Dark) {
104 // When we use a dark selection color, we do not change alpha because we do
105 // not use dark selection in content. The dark system color is appropriate
106 // for Firefox UI without needing to adjust its alpha.
109 uint16_t hue, sat, value;
111 nscolor resultColor = aColor;
112 NS_RGB2HSV(resultColor, hue, sat, value, alpha);
114 alpha = alpha / factor;
116 // The color is not a shade of grey, restore the saturation taken away by
118 sat = mozilla::clamped(sat * factor, 0, 255);
120 // The color is a shade of grey, find the value that looks equivalent
121 // on a white background with the given opacity.
122 value = mozilla::clamped(255 - (255 - value) * factor, 0, 255);
124 NS_HSV2RGB(resultColor, hue, sat, value, alpha);
128 nsresult nsLookAndFeel::NativeGetColor(ColorID aID, ColorScheme aScheme,
130 NS_OBJC_BEGIN_TRY_ABORT_BLOCK
132 NSAppearance.currentAppearance = NSAppearanceForColorScheme(aScheme);
136 case ColorID::Infobackground:
137 color = aScheme == ColorScheme::Light
138 ? NS_RGB(0xdd, 0xdd, 0xdd)
139 : GetColorFromNSColor(NSColor.windowBackgroundColor);
141 case ColorID::Highlight:
142 color = ProcessSelectionBackground(
143 GetColorFromNSColor(NSColor.selectedTextBackgroundColor), aScheme);
145 // This is used to gray out the selection when it's not focused. Used with
146 // nsISelectionController::SELECTION_DISABLED.
147 case ColorID::TextSelectDisabledBackground:
148 color = ProcessSelectionBackground(
149 GetColorFromNSColor(NSColor.secondarySelectedControlColor), aScheme);
151 case ColorID::MozMenuhoverdisabled:
152 aColor = NS_TRANSPARENT;
154 case ColorID::Accentcolor:
155 color = GetColorFromNSColor(NSColor.controlAccentColor);
157 case ColorID::MozMenuhover:
158 case ColorID::Selecteditem:
159 color = GetColorFromNSColor(NSColor.selectedContentBackgroundColor);
160 if (aID == ColorID::MozMenuhover &&
161 !LookAndFeel::GetInt(IntID::PrefersReducedTransparency)) {
162 // Wash the color a little bit with semi-transparent white to match a
163 // bit closer the native NSVisualEffectSelection on menus.
164 color = NS_ComposeColors(
166 NS_RGBA(255, 255, 255, aScheme == ColorScheme::Light ? 51 : 25));
169 case ColorID::Accentcolortext:
170 case ColorID::MozMenuhovertext:
171 case ColorID::Selecteditemtext:
172 color = GetColorFromNSColor(NSColor.selectedMenuItemTextColor);
174 case ColorID::IMESelectedRawTextBackground:
175 case ColorID::IMESelectedConvertedTextBackground:
176 case ColorID::IMERawInputBackground:
177 case ColorID::IMEConvertedTextBackground:
178 color = NS_TRANSPARENT;
180 case ColorID::IMESelectedRawTextForeground:
181 case ColorID::IMESelectedConvertedTextForeground:
182 case ColorID::IMERawInputForeground:
183 case ColorID::IMEConvertedTextForeground:
184 case ColorID::Highlighttext:
185 color = NS_SAME_AS_FOREGROUND_COLOR;
187 case ColorID::IMERawInputUnderline:
188 case ColorID::IMEConvertedTextUnderline:
189 color = NS_40PERCENT_FOREGROUND_COLOR;
191 case ColorID::IMESelectedRawTextUnderline:
192 case ColorID::IMESelectedConvertedTextUnderline:
193 color = NS_SAME_AS_FOREGROUND_COLOR;
197 // css2 system colors http://www.w3.org/TR/REC-CSS2/ui.html#system-colors
199 // It's really hard to effectively map these to the Appearance Manager
200 // properly, since they are modeled word for word after the win32 system
201 // colors and don't have any real counterparts in the Mac world. I'm sure
202 // we'll be tweaking these for years to come.
204 // Thanks to mpt26@student.canterbury.ac.nz for the hardcoded values that
206 // if querying the Appearance Manager fails ;)
208 case ColorID::MozMacDefaultbuttontext:
209 color = NS_RGB(0xFF, 0xFF, 0xFF);
211 case ColorID::MozSidebar:
212 color = aScheme == ColorScheme::Light ? NS_RGB(0xf6, 0xf6, 0xf6)
213 : NS_RGB(0x2d, 0x2d, 0x2d);
215 case ColorID::MozSidebarborder:
216 // hsla(240, 5%, 5%, .1)
217 color = NS_RGBA(12, 12, 13, 26);
219 case ColorID::MozButtonactivetext:
220 // Pre-macOS 12, pressed buttons were filled with the highlight color and
221 // the text was white. Starting with macOS 12, pressed (non-default)
222 // buttons are filled with medium gray and the text color is the same as
223 // in the non-pressed state.
224 color = nsCocoaFeatures::OnMontereyOrLater()
225 ? GetColorFromNSColor(NSColor.controlTextColor)
226 : NS_RGB(0xFF, 0xFF, 0xFF);
228 case ColorID::Windowtext:
229 case ColorID::MozDialogtext:
230 color = GetColorFromNSColor(NSColor.windowFrameTextColor);
232 case ColorID::Appworkspace:
233 color = NS_RGB(0xFF, 0xFF, 0xFF);
235 case ColorID::Background:
236 color = NS_RGB(0x63, 0x63, 0xCE);
238 case ColorID::Buttonface:
239 case ColorID::MozButtonhoverface:
240 case ColorID::MozButtonactiveface:
241 case ColorID::MozButtondisabledface:
242 case ColorID::MozColheader:
243 case ColorID::MozColheaderhover:
244 case ColorID::MozColheaderactive:
245 color = GetColorFromNSColor(NSColor.controlColor);
246 if (!NS_GET_A(color)) {
247 color = GetColorFromNSColor(NSColor.controlBackgroundColor);
250 case ColorID::Buttonhighlight:
251 color = GetColorFromNSColor(NSColor.selectedControlColor);
253 case ColorID::Scrollbar:
254 color = GetColorFromNSColor(NSColor.scrollBarColor);
256 case ColorID::Threedhighlight:
257 color = GetColorFromNSColor(NSColor.highlightColor);
259 case ColorID::Buttonshadow:
260 case ColorID::Threeddarkshadow:
261 color = aScheme == ColorScheme::Dark ? *GenericDarkColor(aID)
262 : NS_RGB(0xDC, 0xDC, 0xDC);
264 case ColorID::Threedshadow:
265 color = aScheme == ColorScheme::Dark ? *GenericDarkColor(aID)
266 : NS_RGB(0xE0, 0xE0, 0xE0);
268 case ColorID::Threedface:
269 color = aScheme == ColorScheme::Dark ? *GenericDarkColor(aID)
270 : NS_RGB(0xF0, 0xF0, 0xF0);
272 case ColorID::Threedlightshadow:
273 case ColorID::Buttonborder:
274 case ColorID::MozDisabledfield:
275 color = aScheme == ColorScheme::Dark ? *GenericDarkColor(aID)
276 : NS_RGB(0xDA, 0xDA, 0xDA);
279 // Hand-picked from Sonoma because there doesn't seem to be any
280 // appropriate menu system color.
281 color = aScheme == ColorScheme::Dark ? NS_RGB(0x36, 0x36, 0x39)
282 : NS_RGB(0xeb, 0xeb, 0xeb);
284 case ColorID::Windowframe:
285 color = GetColorFromNSColor(NSColor.windowFrameColor);
287 case ColorID::MozDialog:
288 case ColorID::Window:
289 color = GetColorFromNSColor(aScheme == ColorScheme::Light
290 ? NSColor.windowBackgroundColor
291 : NSColor.underPageBackgroundColor);
294 case ColorID::MozCombobox:
295 color = GetColorFromNSColor(NSColor.controlBackgroundColor);
297 case ColorID::Fieldtext:
298 case ColorID::MozComboboxtext:
299 case ColorID::Buttontext:
300 case ColorID::MozButtonhovertext:
301 case ColorID::Menutext:
302 case ColorID::Infotext:
303 case ColorID::MozCellhighlighttext:
304 case ColorID::MozColheadertext:
305 case ColorID::MozColheaderhovertext:
306 case ColorID::MozColheaderactivetext:
307 case ColorID::MozSidebartext:
308 color = GetColorFromNSColor(NSColor.controlTextColor);
310 case ColorID::MozMacFocusring:
311 color = GetColorFromNSColorWithCustomAlpha(
312 NSColor.keyboardFocusIndicatorColor, 0.48);
314 case ColorID::MozMacDisabledtoolbartext:
315 case ColorID::Graytext:
316 color = GetColorFromNSColor(NSColor.disabledControlTextColor);
318 case ColorID::MozCellhighlight:
319 // For inactive list selection
320 color = GetColorFromNSColor(NSColor.secondarySelectedControlColor);
322 case ColorID::MozEventreerow:
323 // Background color of even list rows.
325 GetColorFromNSColor(NSColor.controlAlternatingRowBackgroundColors[0]);
327 case ColorID::MozOddtreerow:
328 // Background color of odd list rows.
330 GetColorFromNSColor(NSColor.controlAlternatingRowBackgroundColors[1]);
332 case ColorID::MozNativehyperlinktext:
333 color = GetColorFromNSColor(NSColor.linkColor);
335 case ColorID::MozNativevisitedhyperlinktext:
336 color = GetColorFromNSColor(NSColor.systemPurpleColor);
338 case ColorID::MozHeaderbartext:
339 case ColorID::MozHeaderbarinactivetext:
340 case ColorID::Inactivecaptiontext:
341 case ColorID::Captiontext:
342 aColor = GetColorFromNSColor(NSColor.textColor);
344 case ColorID::MozHeaderbar:
345 case ColorID::MozHeaderbarinactive:
346 case ColorID::Inactivecaption:
347 case ColorID::Activecaption:
348 // This has better contrast than the stand-in colors.
349 aColor = GetColorFromNSColor(NSColor.windowBackgroundColor);
351 case ColorID::Marktext:
353 case ColorID::SpellCheckerUnderline:
354 case ColorID::Activeborder:
355 case ColorID::Inactiveborder:
356 aColor = GetStandinForNativeColor(aID, aScheme);
359 aColor = NS_RGB(0xff, 0xff, 0xff);
360 return NS_ERROR_FAILURE;
366 NS_OBJC_END_TRY_ABORT_BLOCK
369 static bool SystemWantsDarkTheme() {
370 // This returns true if the macOS system appearance is set to dark mode,
372 NSAppearanceName aquaOrDarkAqua =
373 [NSApp.effectiveAppearance bestMatchFromAppearancesWithNames:@[
374 NSAppearanceNameAqua, NSAppearanceNameDarkAqua
376 return [aquaOrDarkAqua isEqualToString:NSAppearanceNameDarkAqua];
379 nsresult nsLookAndFeel::NativeGetInt(IntID aID, int32_t& aResult) {
380 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
382 nsresult res = NS_OK;
385 case IntID::ScrollButtonLeftMouseButtonAction:
388 case IntID::ScrollButtonMiddleMouseButtonAction:
389 case IntID::ScrollButtonRightMouseButtonAction:
392 case IntID::CaretBlinkTime:
395 case IntID::CaretWidth:
398 case IntID::SelectTextfieldsOnKeyFocus:
399 // Select textfield content when focused by kbd
400 // used by EventStateManager::sTextfieldSelectModel
403 case IntID::SubmenuDelay:
406 case IntID::MenusCanOverlapOSBar:
407 // xul popups are not allowed to overlap the menubar.
410 case IntID::SkipNavigatingDisabledMenuItem:
413 case IntID::DragThresholdX:
414 case IntID::DragThresholdY:
417 case IntID::ScrollArrowStyle:
418 aResult = eScrollArrow_None;
420 case IntID::UseOverlayScrollbars:
421 case IntID::AllowOverlayScrollbarsOverlap:
422 aResult = NSScroller.preferredScrollerStyle == NSScrollerStyleOverlay;
424 case IntID::ScrollbarDisplayOnMouseMove:
427 case IntID::ScrollbarFadeBeginDelay:
430 case IntID::ScrollbarFadeDuration:
433 case IntID::TreeOpenDelay:
436 case IntID::TreeCloseDelay:
439 case IntID::TreeLazyScrollDelay:
442 case IntID::TreeScrollDelay:
445 case IntID::TreeScrollLinesMax:
448 case IntID::MacBigSurTheme:
449 aResult = nsCocoaFeatures::OnBigSurOrLater();
455 case IntID::MacTitlebarHeight:
457 aResult = mTitlebarHeight;
459 case IntID::AlertNotificationOrigin:
460 aResult = NS_ALERT_TOP;
462 case IntID::TabFocusModel:
463 aResult = [NSApp isFullKeyboardAccessEnabled]
464 ? nsIContent::eTabFocus_any
465 : nsIContent::eTabFocus_textControlsMask;
467 case IntID::ScrollToClick: {
468 aResult = [[NSUserDefaults standardUserDefaults]
469 boolForKey:@"AppleScrollerPagingBehavior"];
471 case IntID::ChosenMenuItemsShouldBlink:
474 case IntID::IMERawInputUnderlineStyle:
475 case IntID::IMEConvertedTextUnderlineStyle:
476 case IntID::IMESelectedRawTextUnderlineStyle:
477 case IntID::IMESelectedConvertedTextUnderline:
478 aResult = static_cast<int32_t>(StyleTextDecorationStyle::Solid);
480 case IntID::SpellCheckerUnderlineStyle:
481 aResult = static_cast<int32_t>(StyleTextDecorationStyle::Dotted);
483 case IntID::ScrollbarButtonAutoRepeatBehavior:
486 case IntID::SwipeAnimationEnabled:
487 aResult = NSEvent.isSwipeTrackingFromScrollEventsEnabled;
489 case IntID::ContextMenuOffsetVertical:
492 case IntID::ContextMenuOffsetHorizontal:
495 case IntID::SystemUsesDarkTheme:
496 aResult = SystemWantsDarkTheme();
498 case IntID::PrefersReducedMotion:
500 NSWorkspace.sharedWorkspace.accessibilityDisplayShouldReduceMotion;
502 case IntID::PrefersReducedTransparency:
503 aResult = NSWorkspace.sharedWorkspace
504 .accessibilityDisplayShouldReduceTransparency;
506 case IntID::InvertedColors:
508 NSWorkspace.sharedWorkspace.accessibilityDisplayShouldInvertColors;
510 case IntID::UseAccessibilityTheme:
511 aResult = NSWorkspace.sharedWorkspace
512 .accessibilityDisplayShouldIncreaseContrast;
514 case IntID::PanelAnimations:
519 res = NS_ERROR_FAILURE;
523 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
526 nsresult nsLookAndFeel::NativeGetFloat(FloatID aID, float& aResult) {
527 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
529 nsresult res = NS_OK;
532 case FloatID::IMEUnderlineRelativeSize:
535 case FloatID::SpellCheckerUnderlineRelativeSize:
538 case FloatID::CursorScale: {
539 id uaDefaults = [[NSUserDefaults alloc]
540 initWithSuiteName:@"com.apple.universalaccess"];
541 float f = [uaDefaults floatForKey:@"mouseDriverCursorSize"];
542 [uaDefaults release];
543 aResult = f > 0.0 ? f : 1.0; // default to 1.0 if value not available
548 res = NS_ERROR_FAILURE;
553 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
556 bool nsLookAndFeel::NativeGetFont(FontID aID, nsString& aFontName,
557 gfxFontStyle& aFontStyle) {
558 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
561 gfxPlatformMac::LookupSystemFont(aID, name, aFontStyle);
562 aFontName.Append(NS_ConvertUTF8toUTF16(name));
566 NS_OBJC_END_TRY_BLOCK_RETURN(false);
569 void nsLookAndFeel::RecordAccessibilityTelemetry() {
570 if ([[NSWorkspace sharedWorkspace]
571 respondsToSelector:@selector
572 (accessibilityDisplayShouldInvertColors)]) {
574 [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldInvertColors];
575 Telemetry::ScalarSet(Telemetry::ScalarID::A11Y_INVERT_COLORS, val);
579 @implementation MOZLookAndFeelDynamicChangeObserver
581 + (void)startObserving {
582 static MOZLookAndFeelDynamicChangeObserver* gInstance = nil;
584 gInstance = [[MOZLookAndFeelDynamicChangeObserver alloc] init]; // leaked
588 - (instancetype)init {
591 [NSNotificationCenter.defaultCenter
593 selector:@selector(colorsChanged)
594 name:NSControlTintDidChangeNotification
596 [NSNotificationCenter.defaultCenter
598 selector:@selector(colorsChanged)
599 name:NSSystemColorsDidChangeNotification
602 [NSWorkspace.sharedWorkspace.notificationCenter
604 selector:@selector(mediaQueriesChanged)
605 name:NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification
608 [NSNotificationCenter.defaultCenter
610 selector:@selector(scrollbarsChanged)
611 name:NSPreferredScrollerStyleDidChangeNotification
613 [NSDistributedNotificationCenter.defaultCenter
615 selector:@selector(scrollbarsChanged)
616 name:@"AppleAquaScrollBarVariantChanged"
618 suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately];
619 [NSDistributedNotificationCenter.defaultCenter
621 selector:@selector(cachedValuesChanged)
622 name:@"AppleNoRedisplayAppearancePreferenceChanged"
624 suspensionBehavior:NSNotificationSuspensionBehaviorCoalesce];
625 [NSDistributedNotificationCenter.defaultCenter
627 selector:@selector(cachedValuesChanged)
628 name:@"com.apple.KeyboardUIModeDidChange"
630 suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately];
632 [MOZGlobalAppearance.sharedInstance addObserver:self
633 forKeyPath:@"effectiveAppearance"
636 [NSApp addObserver:self
637 forKeyPath:@"effectiveAppearance"
644 - (void)observeValueForKeyPath:(NSString*)keyPath
646 change:(NSDictionary<NSKeyValueChangeKey, id>*)change
647 context:(void*)context {
648 if ([keyPath isEqualToString:@"effectiveAppearance"]) {
649 [self entireThemeChanged];
651 [super observeValueForKeyPath:keyPath
658 - (void)entireThemeChanged {
659 LookAndFeel::NotifyChangedAllWindows(widget::ThemeChangeKind::StyleAndLayout);
662 - (void)scrollbarsChanged {
663 LookAndFeel::NotifyChangedAllWindows(widget::ThemeChangeKind::StyleAndLayout);
666 - (void)mediaQueriesChanged {
667 // Changing`Invert Colors` sends
668 // AccessibilityDisplayOptionsDidChangeNotifications. We monitor that setting
669 // via telemetry, so call into that recording method here.
670 nsLookAndFeel::RecordAccessibilityTelemetry();
671 LookAndFeel::NotifyChangedAllWindows(
672 widget::ThemeChangeKind::MediaQueriesOnly);
675 - (void)colorsChanged {
676 LookAndFeel::NotifyChangedAllWindows(widget::ThemeChangeKind::Style);
679 - (void)cachedValuesChanged {
680 // We only need to re-cache (and broadcast) updated LookAndFeel values, so
681 // that they're up-to-date the next time they're queried. No further change
682 // handling is needed.
683 // TODO: Add a change hint for this which avoids the unnecessary media query
685 LookAndFeel::NotifyChangedAllWindows(
686 widget::ThemeChangeKind::MediaQueriesOnly);