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/Telemetry.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 = mozilla::clamped(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 = mozilla::clamped(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);
137 case ColorID::Infobackground:
138 color = aScheme == ColorScheme::Light
139 ? NS_RGB(0xdd, 0xdd, 0xdd)
140 : GetColorFromNSColor(NSColor.windowBackgroundColor);
142 case ColorID::Highlight:
143 color = ProcessSelectionBackground(
144 GetColorFromNSColor(NSColor.selectedTextBackgroundColor), aScheme);
146 // This is used to gray out the selection when it's not focused. Used with
147 // nsISelectionController::SELECTION_DISABLED.
148 case ColorID::TextSelectDisabledBackground:
149 color = ProcessSelectionBackground(
150 GetColorFromNSColor(NSColor.secondarySelectedControlColor), aScheme);
152 case ColorID::MozMenuhoverdisabled:
153 aColor = NS_TRANSPARENT;
155 case ColorID::Accentcolor:
156 color = GetColorFromNSColor(NSColor.controlAccentColor);
158 case ColorID::MozMenuhover:
159 case ColorID::Selecteditem:
160 color = GetColorFromNSColor(NSColor.selectedContentBackgroundColor);
161 if (aID == ColorID::MozMenuhover &&
162 !LookAndFeel::GetInt(IntID::PrefersReducedTransparency)) {
163 // Wash the color a little bit with semi-transparent white to match a
164 // bit closer the native NSVisualEffectSelection on menus.
165 color = NS_ComposeColors(
167 NS_RGBA(255, 255, 255, aScheme == ColorScheme::Light ? 51 : 25));
170 case ColorID::Accentcolortext:
171 case ColorID::MozMenuhovertext:
172 case ColorID::Selecteditemtext:
173 color = GetColorFromNSColor(NSColor.selectedMenuItemTextColor);
175 case ColorID::IMESelectedRawTextBackground:
176 case ColorID::IMESelectedConvertedTextBackground:
177 case ColorID::IMERawInputBackground:
178 case ColorID::IMEConvertedTextBackground:
179 color = NS_TRANSPARENT;
181 case ColorID::IMESelectedRawTextForeground:
182 case ColorID::IMESelectedConvertedTextForeground:
183 case ColorID::IMERawInputForeground:
184 case ColorID::IMEConvertedTextForeground:
185 case ColorID::Highlighttext:
186 color = NS_SAME_AS_FOREGROUND_COLOR;
188 case ColorID::IMERawInputUnderline:
189 case ColorID::IMEConvertedTextUnderline:
190 color = NS_40PERCENT_FOREGROUND_COLOR;
192 case ColorID::IMESelectedRawTextUnderline:
193 case ColorID::IMESelectedConvertedTextUnderline:
194 color = NS_SAME_AS_FOREGROUND_COLOR;
198 // css2 system colors http://www.w3.org/TR/REC-CSS2/ui.html#system-colors
200 // It's really hard to effectively map these to the Appearance Manager
201 // properly, since they are modeled word for word after the win32 system
202 // colors and don't have any real counterparts in the Mac world. I'm sure
203 // we'll be tweaking these for years to come.
205 // Thanks to mpt26@student.canterbury.ac.nz for the hardcoded values that
207 // if querying the Appearance Manager fails ;)
209 case ColorID::MozMacDefaultbuttontext:
210 color = NS_RGB(0xFF, 0xFF, 0xFF);
212 case ColorID::MozSidebar:
213 color = aScheme == ColorScheme::Light ? NS_RGB(0xf6, 0xf6, 0xf6)
214 : NS_RGB(0x2d, 0x2d, 0x2d);
216 case ColorID::MozSidebarborder:
217 // hsla(240, 5%, 5%, .1)
218 color = NS_RGBA(12, 12, 13, 26);
220 case ColorID::MozButtonactivetext:
221 // Pre-macOS 12, pressed buttons were filled with the highlight color and
222 // the text was white. Starting with macOS 12, pressed (non-default)
223 // buttons are filled with medium gray and the text color is the same as
224 // in the non-pressed state.
225 color = nsCocoaFeatures::OnMontereyOrLater()
226 ? GetColorFromNSColor(NSColor.controlTextColor)
227 : NS_RGB(0xFF, 0xFF, 0xFF);
229 case ColorID::Windowtext:
230 case ColorID::MozDialogtext:
231 color = GetColorFromNSColor(NSColor.windowFrameTextColor);
233 case ColorID::Appworkspace:
234 color = NS_RGB(0xFF, 0xFF, 0xFF);
236 case ColorID::Background:
237 color = NS_RGB(0x63, 0x63, 0xCE);
239 case ColorID::Buttonface:
240 case ColorID::MozButtonhoverface:
241 case ColorID::MozButtonactiveface:
242 case ColorID::MozButtondisabledface:
243 color = GetColorFromNSColor(NSColor.controlColor);
244 if (!NS_GET_A(color)) {
245 color = GetColorFromNSColor(NSColor.controlBackgroundColor);
248 case ColorID::Buttonhighlight:
249 color = GetColorFromNSColor(NSColor.selectedControlColor);
251 case ColorID::Scrollbar:
252 color = GetColorFromNSColor(NSColor.scrollBarColor);
254 case ColorID::Threedhighlight:
255 color = GetColorFromNSColor(NSColor.highlightColor);
257 case ColorID::Buttonshadow:
258 case ColorID::Threeddarkshadow:
259 color = aScheme == ColorScheme::Dark ? *GenericDarkColor(aID)
260 : NS_RGB(0xDC, 0xDC, 0xDC);
262 case ColorID::Threedshadow:
263 color = aScheme == ColorScheme::Dark ? *GenericDarkColor(aID)
264 : NS_RGB(0xE0, 0xE0, 0xE0);
266 case ColorID::Threedface:
267 color = aScheme == ColorScheme::Dark ? *GenericDarkColor(aID)
268 : NS_RGB(0xF0, 0xF0, 0xF0);
270 case ColorID::Threedlightshadow:
271 case ColorID::Buttonborder:
272 case ColorID::MozDisabledfield:
273 color = aScheme == ColorScheme::Dark ? *GenericDarkColor(aID)
274 : NS_RGB(0xDA, 0xDA, 0xDA);
277 // Hand-picked from Sonoma because there doesn't seem to be any
278 // appropriate menu system color.
279 color = aScheme == ColorScheme::Dark ? NS_RGB(0x36, 0x36, 0x39)
280 : NS_RGB(0xeb, 0xeb, 0xeb);
282 case ColorID::Windowframe:
283 color = GetColorFromNSColor(NSColor.windowFrameColor);
285 case ColorID::MozDialog:
286 case ColorID::Window:
287 color = GetColorFromNSColor(aScheme == ColorScheme::Light
288 ? NSColor.windowBackgroundColor
289 : NSColor.underPageBackgroundColor);
292 case ColorID::MozCombobox:
293 color = GetColorFromNSColor(NSColor.controlBackgroundColor);
295 case ColorID::Fieldtext:
296 case ColorID::MozComboboxtext:
297 case ColorID::Buttontext:
298 case ColorID::MozButtonhovertext:
299 case ColorID::Menutext:
300 case ColorID::Infotext:
301 case ColorID::MozCellhighlighttext:
302 case ColorID::MozSidebartext:
303 color = GetColorFromNSColor(NSColor.controlTextColor);
305 case ColorID::MozMacFocusring:
306 color = GetColorFromNSColorWithCustomAlpha(
307 NSColor.keyboardFocusIndicatorColor, 0.48);
309 case ColorID::MozMacDisabledtoolbartext:
310 case ColorID::Graytext:
311 color = GetColorFromNSColor(NSColor.disabledControlTextColor);
313 case ColorID::MozCellhighlight:
314 // For inactive list selection
315 color = GetColorFromNSColor(NSColor.secondarySelectedControlColor);
317 case ColorID::MozColheadertext:
318 case ColorID::MozColheaderhovertext:
319 case ColorID::MozColheaderactivetext:
320 color = GetColorFromNSColor(NSColor.headerTextColor);
322 case ColorID::MozColheaderactive:
323 color = GetColorFromNSColor(
324 NSColor.unemphasizedSelectedContentBackgroundColor);
326 case ColorID::MozColheader:
327 case ColorID::MozColheaderhover:
328 case ColorID::MozEventreerow:
329 // Background color of even list rows.
331 GetColorFromNSColor(NSColor.controlAlternatingRowBackgroundColors[0]);
333 case ColorID::MozOddtreerow:
334 // Background color of odd list rows.
336 GetColorFromNSColor(NSColor.controlAlternatingRowBackgroundColors[1]);
338 case ColorID::MozNativehyperlinktext:
339 color = GetColorFromNSColor(NSColor.linkColor);
341 case ColorID::MozNativevisitedhyperlinktext:
342 color = GetColorFromNSColor(NSColor.systemPurpleColor);
344 case ColorID::MozHeaderbartext:
345 case ColorID::MozHeaderbarinactivetext:
346 case ColorID::Inactivecaptiontext:
347 case ColorID::Captiontext:
348 aColor = GetColorFromNSColor(NSColor.textColor);
350 case ColorID::MozHeaderbar:
351 case ColorID::MozHeaderbarinactive:
352 case ColorID::Inactivecaption:
353 case ColorID::Activecaption:
354 // This has better contrast than the stand-in colors.
355 aColor = GetColorFromNSColor(NSColor.windowBackgroundColor);
357 case ColorID::Marktext:
359 case ColorID::SpellCheckerUnderline:
360 case ColorID::Activeborder:
361 case ColorID::Inactiveborder:
362 case ColorID::MozAutofillBackground:
363 case ColorID::TargetTextBackground:
364 case ColorID::TargetTextForeground:
365 aColor = GetStandinForNativeColor(aID, aScheme);
368 aColor = NS_RGB(0xff, 0xff, 0xff);
369 return NS_ERROR_FAILURE;
375 NS_OBJC_END_TRY_ABORT_BLOCK
378 static bool SystemWantsDarkTheme() {
379 // This returns true if the macOS system appearance is set to dark mode,
381 NSAppearanceName aquaOrDarkAqua =
382 [NSApp.effectiveAppearance bestMatchFromAppearancesWithNames:@[
383 NSAppearanceNameAqua, NSAppearanceNameDarkAqua
385 return [aquaOrDarkAqua isEqualToString:NSAppearanceNameDarkAqua];
388 nsresult nsLookAndFeel::NativeGetInt(IntID aID, int32_t& aResult) {
389 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
391 nsresult res = NS_OK;
394 case IntID::ScrollButtonLeftMouseButtonAction:
397 case IntID::ScrollButtonMiddleMouseButtonAction:
398 case IntID::ScrollButtonRightMouseButtonAction:
401 case IntID::CaretBlinkTime:
404 case IntID::CaretWidth:
407 case IntID::SelectTextfieldsOnKeyFocus:
408 // Select textfield content when focused by kbd
409 // used by EventStateManager::sTextfieldSelectModel
412 case IntID::SubmenuDelay:
415 case IntID::MenusCanOverlapOSBar:
416 // xul popups are not allowed to overlap the menubar.
419 case IntID::SkipNavigatingDisabledMenuItem:
422 case IntID::DragThresholdX:
423 case IntID::DragThresholdY:
426 case IntID::ScrollArrowStyle:
427 aResult = eScrollArrow_None;
429 case IntID::UseOverlayScrollbars:
430 case IntID::AllowOverlayScrollbarsOverlap:
431 aResult = NSScroller.preferredScrollerStyle == NSScrollerStyleOverlay;
433 case IntID::ScrollbarDisplayOnMouseMove:
436 case IntID::ScrollbarFadeBeginDelay:
439 case IntID::ScrollbarFadeDuration:
442 case IntID::TreeOpenDelay:
445 case IntID::TreeCloseDelay:
448 case IntID::TreeLazyScrollDelay:
451 case IntID::TreeScrollDelay:
454 case IntID::TreeScrollLinesMax:
457 case IntID::MacBigSurTheme:
458 aResult = nsCocoaFeatures::OnBigSurOrLater();
464 case IntID::MacTitlebarHeight:
466 aResult = mTitlebarHeight;
468 case IntID::AlertNotificationOrigin:
469 aResult = NS_ALERT_TOP;
471 case IntID::ScrollToClick: {
472 aResult = [[NSUserDefaults standardUserDefaults]
473 boolForKey:@"AppleScrollerPagingBehavior"];
475 case IntID::ChosenMenuItemsShouldBlink:
478 case IntID::IMERawInputUnderlineStyle:
479 case IntID::IMEConvertedTextUnderlineStyle:
480 case IntID::IMESelectedRawTextUnderlineStyle:
481 case IntID::IMESelectedConvertedTextUnderline:
482 aResult = static_cast<int32_t>(StyleTextDecorationStyle::Solid);
484 case IntID::SpellCheckerUnderlineStyle:
485 aResult = static_cast<int32_t>(StyleTextDecorationStyle::Dotted);
487 case IntID::ScrollbarButtonAutoRepeatBehavior:
490 case IntID::SwipeAnimationEnabled:
491 aResult = NSEvent.isSwipeTrackingFromScrollEventsEnabled;
493 case IntID::ContextMenuOffsetVertical:
496 case IntID::ContextMenuOffsetHorizontal:
499 case IntID::SystemUsesDarkTheme:
500 aResult = SystemWantsDarkTheme();
502 case IntID::PrefersReducedMotion:
504 NSWorkspace.sharedWorkspace.accessibilityDisplayShouldReduceMotion;
506 case IntID::PrefersReducedTransparency:
507 aResult = NSWorkspace.sharedWorkspace
508 .accessibilityDisplayShouldReduceTransparency;
510 case IntID::InvertedColors:
512 NSWorkspace.sharedWorkspace.accessibilityDisplayShouldInvertColors;
514 case IntID::UseAccessibilityTheme:
515 aResult = NSWorkspace.sharedWorkspace
516 .accessibilityDisplayShouldIncreaseContrast;
518 case IntID::PanelAnimations:
521 case IntID::FullKeyboardAccess:
522 aResult = NSApp.isFullKeyboardAccessEnabled;
526 res = NS_ERROR_FAILURE;
530 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
533 nsresult nsLookAndFeel::NativeGetFloat(FloatID aID, float& aResult) {
534 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
536 nsresult res = NS_OK;
539 case FloatID::IMEUnderlineRelativeSize:
542 case FloatID::SpellCheckerUnderlineRelativeSize:
545 case FloatID::CursorScale: {
546 id uaDefaults = [[NSUserDefaults alloc]
547 initWithSuiteName:@"com.apple.universalaccess"];
548 float f = [uaDefaults floatForKey:@"mouseDriverCursorSize"];
549 [uaDefaults release];
550 aResult = f > 0.0 ? f : 1.0; // default to 1.0 if value not available
555 res = NS_ERROR_FAILURE;
560 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
563 bool nsLookAndFeel::NativeGetFont(FontID aID, nsString& aFontName,
564 gfxFontStyle& aFontStyle) {
565 NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
568 gfxPlatformMac::LookupSystemFont(aID, name, aFontStyle);
569 aFontName.Append(NS_ConvertUTF8toUTF16(name));
573 NS_OBJC_END_TRY_BLOCK_RETURN(false);
576 void nsLookAndFeel::RecordAccessibilityTelemetry() {
577 if ([[NSWorkspace sharedWorkspace]
578 respondsToSelector:@selector
579 (accessibilityDisplayShouldInvertColors)]) {
581 [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldInvertColors];
582 Telemetry::ScalarSet(Telemetry::ScalarID::A11Y_INVERT_COLORS, val);
586 nsresult nsLookAndFeel::GetKeyboardLayoutImpl(nsACString& aLayout) {
587 TISInputSourceRef source = ::TISCopyCurrentKeyboardInputSource();
590 CFStringRef layoutName = static_cast<CFStringRef>(
591 ::TISGetInputSourceProperty(source, kTISPropertyLocalizedName));
592 CopyNSStringToXPCOMString((const NSString*)layoutName, layout);
593 aLayout.Assign(NS_ConvertUTF16toUTF8(layout));
599 @implementation MOZLookAndFeelDynamicChangeObserver
601 + (void)startObserving {
602 static MOZLookAndFeelDynamicChangeObserver* gInstance = nil;
604 gInstance = [[MOZLookAndFeelDynamicChangeObserver alloc] init]; // leaked
608 - (instancetype)init {
611 [NSNotificationCenter.defaultCenter
613 selector:@selector(colorsChanged)
614 name:NSControlTintDidChangeNotification
616 [NSNotificationCenter.defaultCenter
618 selector:@selector(colorsChanged)
619 name:NSSystemColorsDidChangeNotification
622 [NSWorkspace.sharedWorkspace.notificationCenter
624 selector:@selector(mediaQueriesChanged)
625 name:NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification
628 [NSNotificationCenter.defaultCenter
630 selector:@selector(scrollbarsChanged)
631 name:NSPreferredScrollerStyleDidChangeNotification
633 [NSDistributedNotificationCenter.defaultCenter
635 selector:@selector(scrollbarsChanged)
636 name:@"AppleAquaScrollBarVariantChanged"
638 suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately];
639 [NSDistributedNotificationCenter.defaultCenter
641 selector:@selector(cachedValuesChanged)
642 name:@"AppleNoRedisplayAppearancePreferenceChanged"
644 suspensionBehavior:NSNotificationSuspensionBehaviorCoalesce];
645 [NSDistributedNotificationCenter.defaultCenter
647 selector:@selector(cachedValuesChanged)
648 name:@"com.apple.KeyboardUIModeDidChange"
650 suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately];
652 [NSApp addObserver:self
653 forKeyPath:@"effectiveAppearance"
660 - (void)observeValueForKeyPath:(NSString*)keyPath
662 change:(NSDictionary<NSKeyValueChangeKey, id>*)change
663 context:(void*)context {
664 if ([keyPath isEqualToString:@"effectiveAppearance"]) {
665 [self entireThemeChanged];
667 [super observeValueForKeyPath:keyPath
674 - (void)entireThemeChanged {
675 LookAndFeel::NotifyChangedAllWindows(widget::ThemeChangeKind::StyleAndLayout);
678 - (void)scrollbarsChanged {
679 LookAndFeel::NotifyChangedAllWindows(widget::ThemeChangeKind::StyleAndLayout);
682 - (void)mediaQueriesChanged {
683 // Changing`Invert Colors` sends
684 // AccessibilityDisplayOptionsDidChangeNotifications. We monitor that setting
685 // via telemetry, so call into that recording method here.
686 nsLookAndFeel::RecordAccessibilityTelemetry();
687 LookAndFeel::NotifyChangedAllWindows(
688 widget::ThemeChangeKind::MediaQueriesOnly);
691 - (void)colorsChanged {
692 LookAndFeel::NotifyChangedAllWindows(widget::ThemeChangeKind::Style);
695 - (void)cachedValuesChanged {
696 // We only need to re-cache (and broadcast) updated LookAndFeel values, so
697 // that they're up-to-date the next time they're queried. No further change
698 // handling is needed.
699 // TODO: Add a change hint for this which avoids the unnecessary media query
701 LookAndFeel::NotifyChangedAllWindows(
702 widget::ThemeChangeKind::MediaQueriesOnly);