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 "mozilla/widget/ThemeChangeKind.h"
7 #include "nsLookAndFeel.h"
8 #include "nsCocoaFeatures.h"
9 #include "nsNativeThemeColors.h"
10 #include "nsStyleConsts.h"
11 #include "nsIContent.h"
13 #include "gfxFontConstants.h"
14 #include "gfxPlatformMac.h"
15 #include "nsCSSColorUtils.h"
16 #include "mozilla/FontPropertyTypes.h"
17 #include "mozilla/gfx/2D.h"
18 #include "mozilla/StaticPrefs_widget.h"
19 #include "mozilla/glean/AccessibleMetrics.h"
20 #include "mozilla/widget/WidgetMessageUtils.h"
21 #include "mozilla/MacStringHelpers.h"
23 #import <Cocoa/Cocoa.h>
24 #import <Carbon/Carbon.h>
25 #import <AppKit/NSColor.h>
27 // This must be included last:
28 #include "nsObjCExceptions.h"
30 using namespace mozilla;
32 @interface MOZLookAndFeelDynamicChangeObserver : NSObject
33 + (void)startObserving;
36 nsLookAndFeel::nsLookAndFeel() {
37 [MOZLookAndFeelDynamicChangeObserver startObserving];
40 nsLookAndFeel::~nsLookAndFeel() = default;
42 void nsLookAndFeel::EnsureInit() {
47 NS_OBJC_BEGIN_TRY_ABORT_BLOCK
51 [[NSWindow alloc] initWithContentRect:NSZeroRect
52 styleMask:NSWindowStyleMaskTitled
53 backing:NSBackingStoreBuffered
55 auto release = MakeScopeExit([&] { [window release]; });
57 mRtl = window.windowTitlebarLayoutDirection ==
58 NSUserInterfaceLayoutDirectionRightToLeft;
59 mTitlebarHeight = std::ceil(window.frame.size.height);
63 NS_OBJC_END_TRY_ABORT_BLOCK
66 void nsLookAndFeel::RefreshImpl() {
68 nsXPLookAndFeel::RefreshImpl();
71 static nscolor GetColorFromNSColor(NSColor* aColor) {
72 NSColor* deviceColor =
73 [aColor colorUsingColorSpace:NSColorSpace.deviceRGBColorSpace];
74 return NS_RGBA((unsigned int)(deviceColor.redComponent * 255.0),
75 (unsigned int)(deviceColor.greenComponent * 255.0),
76 (unsigned int)(deviceColor.blueComponent * 255.0),
77 (unsigned int)(deviceColor.alphaComponent * 255.0));
80 static nscolor GetColorFromNSColorWithCustomAlpha(NSColor* aColor,
82 NSColor* deviceColor =
83 [aColor colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]];
84 return NS_RGBA((unsigned int)(deviceColor.redComponent * 255.0),
85 (unsigned int)(deviceColor.greenComponent * 255.0),
86 (unsigned int)(deviceColor.blueComponent * 255.0),
87 (unsigned int)(alpha * 255.0));
90 // Turns an opaque selection color into a partially transparent selection color,
91 // which usually leads to better contrast with the text color and which should
92 // look more visually appealing in most contexts.
93 // The idea is that the text and its regular, non-selected background are
94 // usually chosen in such a way that they contrast well. Making the selection
95 // color partially transparent causes the selection color to mix with the text's
96 // regular background, so the end result will often have better contrast with
97 // the text than an arbitrary opaque selection color.
98 // The motivating example for this is the light selection color on dark web
99 // pages: White text on a light blue selection color has very bad contrast,
100 // whereas white text on dark blue (which what you get if you mix
101 // partially-transparent light blue with the black textbox background) has much
103 static nscolor ProcessSelectionBackground(nscolor aColor, ColorScheme aScheme) {
104 if (aScheme == ColorScheme::Dark) {
105 // When we use a dark selection color, we do not change alpha because we do
106 // not use dark selection in content. The dark system color is appropriate
107 // for Firefox UI without needing to adjust its alpha.
110 uint16_t hue, sat, value;
112 nscolor resultColor = aColor;
113 NS_RGB2HSV(resultColor, hue, sat, value, alpha);
115 alpha = alpha / factor;
117 // The color is not a shade of grey, restore the saturation taken away by
119 sat = std::clamp(sat * factor, 0, 255);
121 // The color is a shade of grey, find the value that looks equivalent
122 // on a white background with the given opacity.
123 value = std::clamp(255 - (255 - value) * factor, 0, 255);
125 NS_HSV2RGB(resultColor, hue, sat, value, alpha);
129 nsresult nsLookAndFeel::NativeGetColor(ColorID aID, ColorScheme aScheme,
131 NS_OBJC_BEGIN_TRY_ABORT_BLOCK
133 NSAppearance.currentAppearance = NSAppearanceForColorScheme(aScheme);
136 case ColorID::Infobackground:
137 aColor = aScheme == ColorScheme::Light
138 ? NS_RGB(0xdd, 0xdd, 0xdd)
139 : GetColorFromNSColor(NSColor.windowBackgroundColor);
141 case ColorID::Highlight:
142 aColor = 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 aColor = ProcessSelectionBackground(
149 GetColorFromNSColor(NSColor.secondarySelectedControlColor), aScheme);
151 case ColorID::MozMenuhoverdisabled:
152 aColor = NS_TRANSPARENT;
154 case ColorID::Accentcolor:
155 aColor = GetColorFromNSColor(NSColor.controlAccentColor);
157 case ColorID::MozMenuhover:
158 case ColorID::Selecteditem:
159 aColor = 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 aColor = 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 aColor = GetColorFromNSColor(NSColor.selectedMenuItemTextColor);
174 case ColorID::IMESelectedRawTextBackground:
175 case ColorID::IMESelectedConvertedTextBackground:
176 case ColorID::IMERawInputBackground:
177 case ColorID::IMEConvertedTextBackground:
178 aColor = NS_TRANSPARENT;
180 case ColorID::IMESelectedRawTextForeground:
181 case ColorID::IMESelectedConvertedTextForeground:
182 case ColorID::IMERawInputForeground:
183 case ColorID::IMEConvertedTextForeground:
184 case ColorID::Highlighttext:
185 aColor = NS_SAME_AS_FOREGROUND_COLOR;
187 case ColorID::IMERawInputUnderline:
188 case ColorID::IMEConvertedTextUnderline:
189 aColor = NS_40PERCENT_FOREGROUND_COLOR;
191 case ColorID::IMESelectedRawTextUnderline:
192 case ColorID::IMESelectedConvertedTextUnderline:
193 aColor = 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 aColor = NS_RGB(0xFF, 0xFF, 0xFF);
211 case ColorID::MozSidebar:
212 aColor = aScheme == ColorScheme::Light ? NS_RGB(0xff, 0xff, 0xff)
213 : NS_RGB(0x2d, 0x2d, 0x2d);
215 case ColorID::MozSidebarborder:
216 // hsla(240, 5%, 5%, .1)
217 aColor = 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 aColor = nsCocoaFeatures::OnMontereyOrLater()
225 ? GetColorFromNSColor(NSColor.controlTextColor)
226 : NS_RGB(0xFF, 0xFF, 0xFF);
228 case ColorID::Appworkspace:
229 aColor = NS_RGB(0xFF, 0xFF, 0xFF);
231 case ColorID::Background:
232 aColor = NS_RGB(0x63, 0x63, 0xCE);
234 case ColorID::Buttonface:
235 case ColorID::MozButtonhoverface:
236 case ColorID::MozButtonactiveface:
237 case ColorID::MozButtondisabledface:
238 aColor = GetColorFromNSColor(NSColor.controlColor);
239 if (!NS_GET_A(aColor)) {
240 aColor = GetColorFromNSColor(NSColor.controlBackgroundColor);
243 case ColorID::Buttonhighlight:
244 aColor = GetColorFromNSColor(NSColor.selectedControlColor);
246 case ColorID::Scrollbar:
247 aColor = GetColorFromNSColor(NSColor.scrollBarColor);
249 case ColorID::Threedhighlight:
250 aColor = GetColorFromNSColor(NSColor.highlightColor);
252 case ColorID::Buttonshadow:
253 case ColorID::Threeddarkshadow:
254 aColor = aScheme == ColorScheme::Dark ? *GenericDarkColor(aID)
255 : NS_RGB(0xDC, 0xDC, 0xDC);
257 case ColorID::Threedshadow:
258 aColor = aScheme == ColorScheme::Dark ? *GenericDarkColor(aID)
259 : NS_RGB(0xE0, 0xE0, 0xE0);
261 case ColorID::Threedface:
262 aColor = aScheme == ColorScheme::Dark ? *GenericDarkColor(aID)
263 : NS_RGB(0xF0, 0xF0, 0xF0);
265 case ColorID::Threedlightshadow:
266 case ColorID::Buttonborder:
267 case ColorID::MozDisabledfield:
268 aColor = aScheme == ColorScheme::Dark ? *GenericDarkColor(aID)
269 : NS_RGB(0xDA, 0xDA, 0xDA);
272 // Hand-picked from Sonoma because there doesn't seem to be any
273 // appropriate menu system color.
274 aColor = aScheme == ColorScheme::Dark ? NS_RGB(0x36, 0x36, 0x39)
275 : NS_RGB(0xeb, 0xeb, 0xeb);
277 case ColorID::Windowframe:
278 aColor = GetColorFromNSColor(NSColor.windowFrameColor);
280 case ColorID::MozDialog:
281 case ColorID::Window:
282 aColor = GetColorFromNSColor(aScheme == ColorScheme::Light
283 ? NSColor.windowBackgroundColor
284 : NSColor.underPageBackgroundColor);
287 case ColorID::MozCombobox:
288 aColor = GetColorFromNSColor(NSColor.controlBackgroundColor);
290 case ColorID::Fieldtext:
291 case ColorID::MozComboboxtext:
292 case ColorID::Buttontext:
293 case ColorID::MozButtonhovertext:
294 case ColorID::Menutext:
295 case ColorID::Infotext:
296 case ColorID::MozCellhighlighttext:
297 case ColorID::MozSidebartext:
298 aColor = GetColorFromNSColor(NSColor.controlTextColor);
300 case ColorID::MozMacFocusring:
301 aColor = GetColorFromNSColorWithCustomAlpha(
302 NSColor.keyboardFocusIndicatorColor, 0.48);
304 case ColorID::MozMacDisabledtoolbartext:
305 case ColorID::Graytext:
306 aColor = GetColorFromNSColor(NSColor.disabledControlTextColor);
308 case ColorID::MozCellhighlight:
309 // For inactive list selection
310 aColor = GetColorFromNSColor(NSColor.secondarySelectedControlColor);
312 case ColorID::MozColheadertext:
313 case ColorID::MozColheaderhovertext:
314 case ColorID::MozColheaderactivetext:
315 aColor = GetColorFromNSColor(NSColor.headerTextColor);
317 case ColorID::MozColheaderactive:
318 aColor = GetColorFromNSColor(
319 NSColor.unemphasizedSelectedContentBackgroundColor);
321 case ColorID::MozColheader:
322 case ColorID::MozColheaderhover:
323 case ColorID::MozEventreerow:
324 // Background color of even list rows.
326 GetColorFromNSColor(NSColor.controlAlternatingRowBackgroundColors[0]);
328 case ColorID::MozOddtreerow:
329 // Background color of odd list rows.
331 GetColorFromNSColor(NSColor.controlAlternatingRowBackgroundColors[1]);
333 case ColorID::MozNativehyperlinktext:
334 aColor = GetColorFromNSColor(NSColor.linkColor);
336 case ColorID::MozNativevisitedhyperlinktext:
337 aColor = GetColorFromNSColor(NSColor.systemPurpleColor);
339 case ColorID::MozHeaderbartext:
340 case ColorID::MozHeaderbarinactivetext:
341 case ColorID::Inactivecaptiontext:
342 case ColorID::Captiontext:
343 case ColorID::Windowtext:
344 case ColorID::MozDialogtext:
345 aColor = GetColorFromNSColor(NSColor.labelColor);
347 case ColorID::MozHeaderbar:
348 case ColorID::MozHeaderbarinactive:
349 case ColorID::Inactivecaption:
350 case ColorID::Activecaption:
351 // This has better contrast than the stand-in colors.
352 aColor = GetColorFromNSColor(NSColor.windowBackgroundColor);
354 case ColorID::Marktext:
356 case ColorID::SpellCheckerUnderline:
357 case ColorID::Activeborder:
358 case ColorID::Inactiveborder:
359 case ColorID::MozAutofillBackground:
360 case ColorID::TargetTextBackground:
361 case ColorID::TargetTextForeground:
362 aColor = GetStandinForNativeColor(aID, aScheme);
365 aColor = NS_RGB(0xff, 0xff, 0xff);
366 return NS_ERROR_FAILURE;
370 NS_OBJC_END_TRY_ABORT_BLOCK
373 static bool SystemWantsDarkTheme() {
374 // This returns true if the macOS system appearance is set to dark mode,
376 NSAppearanceName aquaOrDarkAqua =
377 [NSApp.effectiveAppearance bestMatchFromAppearancesWithNames:@[
378 NSAppearanceNameAqua, NSAppearanceNameDarkAqua
380 return [aquaOrDarkAqua isEqualToString:NSAppearanceNameDarkAqua];
383 nsresult nsLookAndFeel::NativeGetInt(IntID aID, int32_t& aResult) {
384 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
386 nsresult res = NS_OK;
389 case IntID::ScrollButtonLeftMouseButtonAction:
392 case IntID::ScrollButtonMiddleMouseButtonAction:
393 case IntID::ScrollButtonRightMouseButtonAction:
396 case IntID::CaretBlinkTime:
399 case IntID::CaretWidth:
402 case IntID::SelectTextfieldsOnKeyFocus:
403 // Select textfield content when focused by kbd
404 // used by EventStateManager::sTextfieldSelectModel
407 case IntID::SubmenuDelay:
410 case IntID::MenusCanOverlapOSBar:
411 // xul popups are not allowed to overlap the menubar.
414 case IntID::SkipNavigatingDisabledMenuItem:
417 case IntID::DragThresholdX:
418 case IntID::DragThresholdY:
421 case IntID::ScrollArrowStyle:
422 aResult = eScrollArrow_None;
424 case IntID::UseOverlayScrollbars:
425 case IntID::AllowOverlayScrollbarsOverlap:
426 aResult = NSScroller.preferredScrollerStyle == NSScrollerStyleOverlay;
428 case IntID::ScrollbarDisplayOnMouseMove:
431 case IntID::ScrollbarFadeBeginDelay:
434 case IntID::ScrollbarFadeDuration:
437 case IntID::TreeOpenDelay:
440 case IntID::TreeCloseDelay:
443 case IntID::TreeLazyScrollDelay:
446 case IntID::TreeScrollDelay:
449 case IntID::TreeScrollLinesMax:
452 case IntID::MacBigSurTheme:
453 aResult = nsCocoaFeatures::OnBigSurOrLater();
459 case IntID::MacTitlebarHeight:
461 aResult = mTitlebarHeight;
463 case IntID::AlertNotificationOrigin:
464 aResult = NS_ALERT_TOP;
466 case IntID::ScrollToClick: {
467 aResult = [[NSUserDefaults standardUserDefaults]
468 boolForKey:@"AppleScrollerPagingBehavior"];
470 case IntID::ChosenMenuItemsShouldBlink:
473 case IntID::IMERawInputUnderlineStyle:
474 case IntID::IMEConvertedTextUnderlineStyle:
475 case IntID::IMESelectedRawTextUnderlineStyle:
476 case IntID::IMESelectedConvertedTextUnderline:
477 aResult = static_cast<int32_t>(StyleTextDecorationStyle::Solid);
479 case IntID::SpellCheckerUnderlineStyle:
480 aResult = static_cast<int32_t>(StyleTextDecorationStyle::Dotted);
482 case IntID::ScrollbarButtonAutoRepeatBehavior:
485 case IntID::SwipeAnimationEnabled:
486 aResult = NSEvent.isSwipeTrackingFromScrollEventsEnabled;
488 case IntID::ContextMenuOffsetVertical:
491 case IntID::ContextMenuOffsetHorizontal:
494 case IntID::SystemUsesDarkTheme:
495 aResult = SystemWantsDarkTheme();
497 case IntID::PrefersReducedMotion:
499 NSWorkspace.sharedWorkspace.accessibilityDisplayShouldReduceMotion;
501 case IntID::PrefersReducedTransparency:
502 aResult = NSWorkspace.sharedWorkspace
503 .accessibilityDisplayShouldReduceTransparency;
505 case IntID::InvertedColors:
507 NSWorkspace.sharedWorkspace.accessibilityDisplayShouldInvertColors;
509 case IntID::UseAccessibilityTheme:
510 aResult = NSWorkspace.sharedWorkspace
511 .accessibilityDisplayShouldIncreaseContrast;
513 case IntID::PanelAnimations:
516 case IntID::FullKeyboardAccess:
517 aResult = NSApp.isFullKeyboardAccessEnabled;
521 res = NS_ERROR_FAILURE;
525 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
528 nsresult nsLookAndFeel::NativeGetFloat(FloatID aID, float& aResult) {
529 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
531 nsresult res = NS_OK;
534 case FloatID::IMEUnderlineRelativeSize:
537 case FloatID::SpellCheckerUnderlineRelativeSize:
540 case FloatID::CursorScale: {
541 id uaDefaults = [[NSUserDefaults alloc]
542 initWithSuiteName:@"com.apple.universalaccess"];
543 float f = [uaDefaults floatForKey:@"mouseDriverCursorSize"];
544 [uaDefaults release];
545 aResult = f > 0.0 ? f : 1.0; // default to 1.0 if value not available
550 res = NS_ERROR_FAILURE;
555 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
558 bool nsLookAndFeel::NativeGetFont(FontID aID, nsString& aFontName,
559 gfxFontStyle& aFontStyle) {
560 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
563 gfxPlatformMac::LookupSystemFont(aID, name, aFontStyle);
564 aFontName.Append(NS_ConvertUTF8toUTF16(name));
568 NS_OBJC_END_TRY_BLOCK_RETURN(false);
571 void nsLookAndFeel::RecordAccessibilityTelemetry() {
572 if ([[NSWorkspace sharedWorkspace]
573 respondsToSelector:@selector
574 (accessibilityDisplayShouldInvertColors)]) {
576 [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldInvertColors];
577 glean::a11y::invert_colors.Set(val);
581 nsresult nsLookAndFeel::GetKeyboardLayoutImpl(nsACString& aLayout) {
582 TISInputSourceRef source = ::TISCopyCurrentKeyboardInputSource();
585 CFStringRef layoutName = static_cast<CFStringRef>(
586 ::TISGetInputSourceProperty(source, kTISPropertyInputSourceID));
587 CopyNSStringToXPCOMString((const NSString*)layoutName, layout);
588 aLayout.Assign(NS_ConvertUTF16toUTF8(layout));
594 @implementation MOZLookAndFeelDynamicChangeObserver
596 + (void)startObserving {
597 static MOZLookAndFeelDynamicChangeObserver* gInstance = nil;
599 gInstance = [[MOZLookAndFeelDynamicChangeObserver alloc] init]; // leaked
603 - (instancetype)init {
606 [NSNotificationCenter.defaultCenter
608 selector:@selector(colorsChanged)
609 name:NSControlTintDidChangeNotification
611 [NSNotificationCenter.defaultCenter
613 selector:@selector(colorsChanged)
614 name:NSSystemColorsDidChangeNotification
617 [NSWorkspace.sharedWorkspace.notificationCenter
619 selector:@selector(mediaQueriesChanged)
620 name:NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification
623 [NSNotificationCenter.defaultCenter
625 selector:@selector(scrollbarsChanged)
626 name:NSPreferredScrollerStyleDidChangeNotification
628 [NSDistributedNotificationCenter.defaultCenter
630 selector:@selector(scrollbarsChanged)
631 name:@"AppleAquaScrollBarVariantChanged"
633 suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately];
634 [NSDistributedNotificationCenter.defaultCenter
636 selector:@selector(cachedValuesChanged)
637 name:@"AppleNoRedisplayAppearancePreferenceChanged"
639 suspensionBehavior:NSNotificationSuspensionBehaviorCoalesce];
640 [NSDistributedNotificationCenter.defaultCenter
642 selector:@selector(cachedValuesChanged)
643 name:@"com.apple.KeyboardUIModeDidChange"
645 suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately];
647 [NSApp addObserver:self
648 forKeyPath:@"effectiveAppearance"
655 - (void)observeValueForKeyPath:(NSString*)keyPath
657 change:(NSDictionary<NSKeyValueChangeKey, id>*)change
658 context:(void*)context {
659 if ([keyPath isEqualToString:@"effectiveAppearance"]) {
660 [self entireThemeChanged];
662 [super observeValueForKeyPath:keyPath
669 - (void)entireThemeChanged {
670 LookAndFeel::NotifyChangedAllWindows(widget::ThemeChangeKind::StyleAndLayout);
673 - (void)scrollbarsChanged {
674 LookAndFeel::NotifyChangedAllWindows(widget::ThemeChangeKind::StyleAndLayout);
677 - (void)mediaQueriesChanged {
678 // Changing`Invert Colors` sends
679 // AccessibilityDisplayOptionsDidChangeNotifications. We monitor that setting
680 // via telemetry, so call into that recording method here.
681 nsLookAndFeel::RecordAccessibilityTelemetry();
682 LookAndFeel::NotifyChangedAllWindows(
683 widget::ThemeChangeKind::MediaQueriesOnly);
686 - (void)colorsChanged {
687 LookAndFeel::NotifyChangedAllWindows(widget::ThemeChangeKind::Style);
690 - (void)cachedValuesChanged {
691 // We only need to re-cache (and broadcast) updated LookAndFeel values, so
692 // that they're up-to-date the next time they're queried. No further change
693 // handling is needed.
694 // TODO: Add a change hint for this which avoids the unnecessary media query
696 LookAndFeel::NotifyChangedAllWindows(
697 widget::ThemeChangeKind::MediaQueriesOnly);