Bug 1931425 - Limit how often moz-label's #setStyles runs r=reusable-components-revie...
[gecko.git] / widget / cocoa / nsLookAndFeel.mm
bloba1d016d88a4783aa5a9bce489960560f8f0558ee
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"
12 #include "gfxFont.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;
34 @end
36 nsLookAndFeel::nsLookAndFeel() {
37   [MOZLookAndFeelDynamicChangeObserver startObserving];
40 nsLookAndFeel::~nsLookAndFeel() = default;
42 void nsLookAndFeel::EnsureInit() {
43   if (mInitialized) {
44     return;
45   }
47   NS_OBJC_BEGIN_TRY_ABORT_BLOCK
49   mInitialized = true;
50   NSWindow* window =
51       [[NSWindow alloc] initWithContentRect:NSZeroRect
52                                   styleMask:NSWindowStyleMaskTitled
53                                     backing:NSBackingStoreBuffered
54                                       defer:NO];
55   auto release = MakeScopeExit([&] { [window release]; });
57   mRtl = window.windowTitlebarLayoutDirection ==
58          NSUserInterfaceLayoutDirectionRightToLeft;
59   mTitlebarHeight = std::ceil(window.frame.size.height);
61   RecordTelemetry();
63   NS_OBJC_END_TRY_ABORT_BLOCK
66 void nsLookAndFeel::RefreshImpl() {
67   mInitialized = false;
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,
81                                                   float alpha) {
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
102 // better contrast.
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.
108     return aColor;
109   }
110   uint16_t hue, sat, value;
111   uint8_t alpha;
112   nscolor resultColor = aColor;
113   NS_RGB2HSV(resultColor, hue, sat, value, alpha);
114   int factor = 2;
115   alpha = alpha / factor;
116   if (sat > 0) {
117     // The color is not a shade of grey, restore the saturation taken away by
118     // the transparency.
119     sat = std::clamp(sat * factor, 0, 255);
120   } else {
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);
124   }
125   NS_HSV2RGB(resultColor, hue, sat, value, alpha);
126   return resultColor;
129 nsresult nsLookAndFeel::NativeGetColor(ColorID aID, ColorScheme aScheme,
130                                        nscolor& aColor) {
131   NS_OBJC_BEGIN_TRY_ABORT_BLOCK
133   NSAppearance.currentAppearance = NSAppearanceForColorScheme(aScheme);
135   switch (aID) {
136     case ColorID::Infobackground:
137       aColor = aScheme == ColorScheme::Light
138                    ? NS_RGB(0xdd, 0xdd, 0xdd)
139                    : GetColorFromNSColor(NSColor.windowBackgroundColor);
140       break;
141     case ColorID::Highlight:
142       aColor = ProcessSelectionBackground(
143           GetColorFromNSColor(NSColor.selectedTextBackgroundColor), aScheme);
144       break;
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);
150       break;
151     case ColorID::MozMenuhoverdisabled:
152       aColor = NS_TRANSPARENT;
153       break;
154     case ColorID::Accentcolor:
155       aColor = GetColorFromNSColor(NSColor.controlAccentColor);
156       break;
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(
165             aColor,
166             NS_RGBA(255, 255, 255, aScheme == ColorScheme::Light ? 51 : 25));
167       }
168       break;
169     case ColorID::Accentcolortext:
170     case ColorID::MozMenuhovertext:
171     case ColorID::Selecteditemtext:
172       aColor = GetColorFromNSColor(NSColor.selectedMenuItemTextColor);
173       break;
174     case ColorID::IMESelectedRawTextBackground:
175     case ColorID::IMESelectedConvertedTextBackground:
176     case ColorID::IMERawInputBackground:
177     case ColorID::IMEConvertedTextBackground:
178       aColor = NS_TRANSPARENT;
179       break;
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;
186       break;
187     case ColorID::IMERawInputUnderline:
188     case ColorID::IMEConvertedTextUnderline:
189       aColor = NS_40PERCENT_FOREGROUND_COLOR;
190       break;
191     case ColorID::IMESelectedRawTextUnderline:
192     case ColorID::IMESelectedConvertedTextUnderline:
193       aColor = NS_SAME_AS_FOREGROUND_COLOR;
194       break;
196       //
197       // css2 system colors http://www.w3.org/TR/REC-CSS2/ui.html#system-colors
198       //
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.
203       //
204       // Thanks to mpt26@student.canterbury.ac.nz for the hardcoded values that
205       // form the defaults
206       //  if querying the Appearance Manager fails ;)
207       //
208     case ColorID::MozMacDefaultbuttontext:
209       aColor = NS_RGB(0xFF, 0xFF, 0xFF);
210       break;
211     case ColorID::MozSidebar:
212       aColor = aScheme == ColorScheme::Light ? NS_RGB(0xff, 0xff, 0xff)
213                                              : NS_RGB(0x2d, 0x2d, 0x2d);
214       break;
215     case ColorID::MozSidebarborder:
216       // hsla(240, 5%, 5%, .1)
217       aColor = NS_RGBA(12, 12, 13, 26);
218       break;
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);
227       break;
228     case ColorID::Appworkspace:
229       aColor = NS_RGB(0xFF, 0xFF, 0xFF);
230       break;
231     case ColorID::Background:
232       aColor = NS_RGB(0x63, 0x63, 0xCE);
233       break;
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);
241       }
242       break;
243     case ColorID::Buttonhighlight:
244       aColor = GetColorFromNSColor(NSColor.selectedControlColor);
245       break;
246     case ColorID::Scrollbar:
247       aColor = GetColorFromNSColor(NSColor.scrollBarColor);
248       break;
249     case ColorID::Threedhighlight:
250       aColor = GetColorFromNSColor(NSColor.highlightColor);
251       break;
252     case ColorID::Buttonshadow:
253     case ColorID::Threeddarkshadow:
254       aColor = aScheme == ColorScheme::Dark ? *GenericDarkColor(aID)
255                                             : NS_RGB(0xDC, 0xDC, 0xDC);
256       break;
257     case ColorID::Threedshadow:
258       aColor = aScheme == ColorScheme::Dark ? *GenericDarkColor(aID)
259                                             : NS_RGB(0xE0, 0xE0, 0xE0);
260       break;
261     case ColorID::Threedface:
262       aColor = aScheme == ColorScheme::Dark ? *GenericDarkColor(aID)
263                                             : NS_RGB(0xF0, 0xF0, 0xF0);
264       break;
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);
270       break;
271     case ColorID::Menu:
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);
276       break;
277     case ColorID::Windowframe:
278       aColor = GetColorFromNSColor(NSColor.windowFrameColor);
279       break;
280     case ColorID::MozDialog:
281     case ColorID::Window:
282       aColor = GetColorFromNSColor(aScheme == ColorScheme::Light
283                                        ? NSColor.windowBackgroundColor
284                                        : NSColor.underPageBackgroundColor);
285       break;
286     case ColorID::Field:
287     case ColorID::MozCombobox:
288       aColor = GetColorFromNSColor(NSColor.controlBackgroundColor);
289       break;
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);
299       break;
300     case ColorID::MozMacFocusring:
301       aColor = GetColorFromNSColorWithCustomAlpha(
302           NSColor.keyboardFocusIndicatorColor, 0.48);
303       break;
304     case ColorID::MozMacDisabledtoolbartext:
305     case ColorID::Graytext:
306       aColor = GetColorFromNSColor(NSColor.disabledControlTextColor);
307       break;
308     case ColorID::MozCellhighlight:
309       // For inactive list selection
310       aColor = GetColorFromNSColor(NSColor.secondarySelectedControlColor);
311       break;
312     case ColorID::MozColheadertext:
313     case ColorID::MozColheaderhovertext:
314     case ColorID::MozColheaderactivetext:
315       aColor = GetColorFromNSColor(NSColor.headerTextColor);
316       break;
317     case ColorID::MozColheaderactive:
318       aColor = GetColorFromNSColor(
319           NSColor.unemphasizedSelectedContentBackgroundColor);
320       break;
321     case ColorID::MozColheader:
322     case ColorID::MozColheaderhover:
323     case ColorID::MozEventreerow:
324       // Background color of even list rows.
325       aColor =
326           GetColorFromNSColor(NSColor.controlAlternatingRowBackgroundColors[0]);
327       break;
328     case ColorID::MozOddtreerow:
329       // Background color of odd list rows.
330       aColor =
331           GetColorFromNSColor(NSColor.controlAlternatingRowBackgroundColors[1]);
332       break;
333     case ColorID::MozNativehyperlinktext:
334       aColor = GetColorFromNSColor(NSColor.linkColor);
335       break;
336     case ColorID::MozNativevisitedhyperlinktext:
337       aColor = GetColorFromNSColor(NSColor.systemPurpleColor);
338       break;
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);
346       return NS_OK;
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);
353       return NS_OK;
354     case ColorID::Marktext:
355     case ColorID::Mark:
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);
363       return NS_OK;
364     default:
365       aColor = NS_RGB(0xff, 0xff, 0xff);
366       return NS_ERROR_FAILURE;
367   }
368   return NS_OK;
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,
375   // false otherwise.
376   NSAppearanceName aquaOrDarkAqua =
377       [NSApp.effectiveAppearance bestMatchFromAppearancesWithNames:@[
378         NSAppearanceNameAqua, NSAppearanceNameDarkAqua
379       ]];
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;
388   switch (aID) {
389     case IntID::ScrollButtonLeftMouseButtonAction:
390       aResult = 0;
391       break;
392     case IntID::ScrollButtonMiddleMouseButtonAction:
393     case IntID::ScrollButtonRightMouseButtonAction:
394       aResult = 3;
395       break;
396     case IntID::CaretBlinkTime:
397       aResult = 567;
398       break;
399     case IntID::CaretWidth:
400       aResult = 1;
401       break;
402     case IntID::SelectTextfieldsOnKeyFocus:
403       // Select textfield content when focused by kbd
404       // used by EventStateManager::sTextfieldSelectModel
405       aResult = 1;
406       break;
407     case IntID::SubmenuDelay:
408       aResult = 200;
409       break;
410     case IntID::MenusCanOverlapOSBar:
411       // xul popups are not allowed to overlap the menubar.
412       aResult = 0;
413       break;
414     case IntID::SkipNavigatingDisabledMenuItem:
415       aResult = 1;
416       break;
417     case IntID::DragThresholdX:
418     case IntID::DragThresholdY:
419       aResult = 4;
420       break;
421     case IntID::ScrollArrowStyle:
422       aResult = eScrollArrow_None;
423       break;
424     case IntID::UseOverlayScrollbars:
425     case IntID::AllowOverlayScrollbarsOverlap:
426       aResult = NSScroller.preferredScrollerStyle == NSScrollerStyleOverlay;
427       break;
428     case IntID::ScrollbarDisplayOnMouseMove:
429       aResult = 0;
430       break;
431     case IntID::ScrollbarFadeBeginDelay:
432       aResult = 450;
433       break;
434     case IntID::ScrollbarFadeDuration:
435       aResult = 200;
436       break;
437     case IntID::TreeOpenDelay:
438       aResult = 1000;
439       break;
440     case IntID::TreeCloseDelay:
441       aResult = 1000;
442       break;
443     case IntID::TreeLazyScrollDelay:
444       aResult = 150;
445       break;
446     case IntID::TreeScrollDelay:
447       aResult = 100;
448       break;
449     case IntID::TreeScrollLinesMax:
450       aResult = 3;
451       break;
452     case IntID::MacBigSurTheme:
453       aResult = nsCocoaFeatures::OnBigSurOrLater();
454       break;
455     case IntID::MacRTL:
456       EnsureInit();
457       aResult = mRtl;
458       break;
459     case IntID::MacTitlebarHeight:
460       EnsureInit();
461       aResult = mTitlebarHeight;
462       break;
463     case IntID::AlertNotificationOrigin:
464       aResult = NS_ALERT_TOP;
465       break;
466     case IntID::ScrollToClick: {
467       aResult = [[NSUserDefaults standardUserDefaults]
468           boolForKey:@"AppleScrollerPagingBehavior"];
469     } break;
470     case IntID::ChosenMenuItemsShouldBlink:
471       aResult = 1;
472       break;
473     case IntID::IMERawInputUnderlineStyle:
474     case IntID::IMEConvertedTextUnderlineStyle:
475     case IntID::IMESelectedRawTextUnderlineStyle:
476     case IntID::IMESelectedConvertedTextUnderline:
477       aResult = static_cast<int32_t>(StyleTextDecorationStyle::Solid);
478       break;
479     case IntID::SpellCheckerUnderlineStyle:
480       aResult = static_cast<int32_t>(StyleTextDecorationStyle::Dotted);
481       break;
482     case IntID::ScrollbarButtonAutoRepeatBehavior:
483       aResult = 0;
484       break;
485     case IntID::SwipeAnimationEnabled:
486       aResult = NSEvent.isSwipeTrackingFromScrollEventsEnabled;
487       break;
488     case IntID::ContextMenuOffsetVertical:
489       aResult = -6;
490       break;
491     case IntID::ContextMenuOffsetHorizontal:
492       aResult = 1;
493       break;
494     case IntID::SystemUsesDarkTheme:
495       aResult = SystemWantsDarkTheme();
496       break;
497     case IntID::PrefersReducedMotion:
498       aResult =
499           NSWorkspace.sharedWorkspace.accessibilityDisplayShouldReduceMotion;
500       break;
501     case IntID::PrefersReducedTransparency:
502       aResult = NSWorkspace.sharedWorkspace
503                     .accessibilityDisplayShouldReduceTransparency;
504       break;
505     case IntID::InvertedColors:
506       aResult =
507           NSWorkspace.sharedWorkspace.accessibilityDisplayShouldInvertColors;
508       break;
509     case IntID::UseAccessibilityTheme:
510       aResult = NSWorkspace.sharedWorkspace
511                     .accessibilityDisplayShouldIncreaseContrast;
512       break;
513     case IntID::PanelAnimations:
514       aResult = 1;
515       break;
516     case IntID::FullKeyboardAccess:
517       aResult = NSApp.isFullKeyboardAccessEnabled;
518       break;
519     default:
520       aResult = 0;
521       res = NS_ERROR_FAILURE;
522   }
523   return res;
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;
533   switch (aID) {
534     case FloatID::IMEUnderlineRelativeSize:
535       aResult = 2.0f;
536       break;
537     case FloatID::SpellCheckerUnderlineRelativeSize:
538       aResult = 2.0f;
539       break;
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
546       break;
547     }
548     default:
549       aResult = -1.0;
550       res = NS_ERROR_FAILURE;
551   }
553   return res;
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;
562   nsAutoCString name;
563   gfxPlatformMac::LookupSystemFont(aID, name, aFontStyle);
564   aFontName.Append(NS_ConvertUTF8toUTF16(name));
566   return true;
568   NS_OBJC_END_TRY_BLOCK_RETURN(false);
571 void nsLookAndFeel::RecordAccessibilityTelemetry() {
572   if ([[NSWorkspace sharedWorkspace]
573           respondsToSelector:@selector
574           (accessibilityDisplayShouldInvertColors)]) {
575     bool val =
576         [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldInvertColors];
577     glean::a11y::invert_colors.Set(val);
578   }
581 nsresult nsLookAndFeel::GetKeyboardLayoutImpl(nsACString& aLayout) {
582   TISInputSourceRef source = ::TISCopyCurrentKeyboardInputSource();
583   nsAutoString layout;
585   CFStringRef layoutName = static_cast<CFStringRef>(
586       ::TISGetInputSourceProperty(source, kTISPropertyInputSourceID));
587   CopyNSStringToXPCOMString((const NSString*)layoutName, layout);
588   aLayout.Assign(NS_ConvertUTF16toUTF8(layout));
590   ::CFRelease(source);
591   return NS_OK;
594 @implementation MOZLookAndFeelDynamicChangeObserver
596 + (void)startObserving {
597   static MOZLookAndFeelDynamicChangeObserver* gInstance = nil;
598   if (!gInstance) {
599     gInstance = [[MOZLookAndFeelDynamicChangeObserver alloc] init];  // leaked
600   }
603 - (instancetype)init {
604   self = [super init];
606   [NSNotificationCenter.defaultCenter
607       addObserver:self
608          selector:@selector(colorsChanged)
609              name:NSControlTintDidChangeNotification
610            object:nil];
611   [NSNotificationCenter.defaultCenter
612       addObserver:self
613          selector:@selector(colorsChanged)
614              name:NSSystemColorsDidChangeNotification
615            object:nil];
617   [NSWorkspace.sharedWorkspace.notificationCenter
618       addObserver:self
619          selector:@selector(mediaQueriesChanged)
620              name:NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification
621            object:nil];
623   [NSNotificationCenter.defaultCenter
624       addObserver:self
625          selector:@selector(scrollbarsChanged)
626              name:NSPreferredScrollerStyleDidChangeNotification
627            object:nil];
628   [NSDistributedNotificationCenter.defaultCenter
629              addObserver:self
630                 selector:@selector(scrollbarsChanged)
631                     name:@"AppleAquaScrollBarVariantChanged"
632                   object:nil
633       suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately];
634   [NSDistributedNotificationCenter.defaultCenter
635              addObserver:self
636                 selector:@selector(cachedValuesChanged)
637                     name:@"AppleNoRedisplayAppearancePreferenceChanged"
638                   object:nil
639       suspensionBehavior:NSNotificationSuspensionBehaviorCoalesce];
640   [NSDistributedNotificationCenter.defaultCenter
641              addObserver:self
642                 selector:@selector(cachedValuesChanged)
643                     name:@"com.apple.KeyboardUIModeDidChange"
644                   object:nil
645       suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately];
647   [NSApp addObserver:self
648           forKeyPath:@"effectiveAppearance"
649              options:0
650              context:nil];
652   return self;
655 - (void)observeValueForKeyPath:(NSString*)keyPath
656                       ofObject:(id)object
657                         change:(NSDictionary<NSKeyValueChangeKey, id>*)change
658                        context:(void*)context {
659   if ([keyPath isEqualToString:@"effectiveAppearance"]) {
660     [self entireThemeChanged];
661   } else {
662     [super observeValueForKeyPath:keyPath
663                          ofObject:object
664                            change:change
665                           context:context];
666   }
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
695   // invalidation.
696   LookAndFeel::NotifyChangedAllWindows(
697       widget::ThemeChangeKind::MediaQueriesOnly);
699 @end