Who's Afraid of the Dark? - Making Dark Mode Work. Revised 10/28/2022

Dark Mode was a huge change for UI design, and unlike many previous UI incremental changes, this one is not 'the' new UI, but is a user selection, or a system generated change that switches with the rising and setting 'sun'.  I first experienced something like Dark Mode with my old Garmin auto GPS unit.  But in 2018, with macOS 10.14 (Mojave), Apple thrust it onto our desktops, as an option .  Adored by many and reviled by some, the Dark Mode option quickly spread to the mobile world, in September of 2019 both iOS 13 and Android 10 added the option.

The nifty part of Dark Mode is that it is up to the user to choose whether they want to use it or not, and that it is almost entirely provided by the system.  Now you will remember that with the rollout of Dark Mode, the support varied by program, some programs still stand-out with big sections of 'white' blazing despite many years, which is understandable with really old programs, but some commercial and regularly updated programs still fail to adopt truly Dark Mode appearance.  Is your program like that?  Some of mine are.

I'm in the process of major revisions to some of my programs and revising and refactoring my Xojo development templates, tools & utility classes.  So I thought that it might be a good time to share my solutions to certain development problems, and share the reasoning.

Implementing Dark Mode is really pretty easy, start with selecting 'Supports Dark Mode' in the Xojo project's Shared Build Settings.  Almost every UI element just automatically supports Dark Mode.  However there are a number of things I've found that must be addressed differently than in the past.

The complexity of Dark Mode springs from the fact that the system may change from Light Mode to Dark Mode, or back, at any moment, even while your program is up and running.  The system's stuff and Xojo framework provided stuff tend to take care of themselves, but how does your stuff know to change?  

Well, once you are using ColorGroups (with dual colors or named colors) everywhere you set colors, and using color.isDarkMode in your paint methods to determine drawing colors, you are a mostly there, as the Xojo framework and application framework will handle most of the change.

To handle everything else that needs to change, DesktopApplication provides the 'AppearanceChanged' event.  So... how does this event handler then notify every class & which needs to know Dark Mode changed?

I have considered the problem, searched the Xojo forum and other online sources.  I posted a question and got the feedback and suggestions of a number of individuals.  Then I actually designed and implemented a few solutions to assess the fitness of each.  I will here describe 3 approaches, and provide two different solutions in code, the first is more flexible and capable and computationally efficient, the second is far simpler for implementation.

Approaches

Approach 1 would be to implement the DesktopApplication.AppearanceChanged event handler, and in this handler to go through the list of open Windows and 'tell' each of them that the appearance changed.  The mechanism might be implementing an interface which specifies a method, added to each window.  The window's method would need to know every item (i.e. control) in the window which needs to somehow be notified of the appearance change or in some way updated to account for the appearance change.

Approach 2 would be to implement the DesktopApplication.AppearanceChanged event handler, and in this handler to go through a list of items (windows or controls) which have 'registered' that they need to know about appearance changes.  This is computationally efficient as the list would include only items which 'need to know'.  An interface would be a good way to assure that each of these items implements a consistemt method for appearance change notification.  Registration (when created) and unregistration (when destroyed) could be implemented in the constructor and destructor of each item that 'needs to know'.

Approach 3 would be to implement the DesktopApplication.AppearanceChanged event handler, and in this handler to walk through the list of open Windows and controls in each window and 'tell' each of them that the appearance changed.  Implement an interface, which specifies a method, added to each item which 'needs to know', to handle appearance change notification.  Polymorphism allows us to know which of the numerous Windows and Controls implement the interface (ones that 'need to know') and allows us to notify them. 

Approach #1 was advocated in the forum, however I feel that this is poorer than the other two approaches for a number of reasons.  I'm going to use #1, #2, & #3 to indicate the three listed approaches as I compare the weaknesses of each below.

For the reasons listed above, I will NOT use approach #1.  Approach #2 is efficient and flexible AND is a pattern you will see & use for other things, so I'll implement it here and provide sample code.  Approach #3 is the least entangled and relies upon polymorphism and a list/tree of visual elements automatically maintained by the framework and OS to find and notify classes which 'need to know' at run-time.  Though run-time inefficient, the frequency of appearance changes is relatively low (not multiple times per second) and already requires a few seconds to occur, so I don't see the time needed to walk the entire tree, even with very sophisticated programs, to be an issue.  The simplified implementation requirements at individual controls and at the application-level make this path most desirable to me.  Approach #3 is my preferred solution and is implemented and provided here too.  

LDS_ApperanceChangeManager - Solution #2

So, just in case you are looking for a relatively simple solution to driving your stuff to respond to Dark Mode changes.

So what does this code look like?  Well let's start with the 'LDS_DarkModeResponsive' class interface.

Now the Singleton Class LDS_AppearanceChangeManager class:

With this code: 

Private Sub Constructor()
 
End Sub

Public Shared Sub AppearanceChanged()
 if(ResponsiveItems<>nil) then
  for each dmr as LDS_DarkModeResponsive in ResponsiveItems
   dmr.AppearanceChanged()
  next
 end
End Sub

Public Shared Sub RegisterResponsiveItem(item as LDS_DarkModeResponsive)
 if(ResponsiveItems=nil) then
  redim ResponsiveItems(-1)
 end
 ResponsiveItems.add(item)
End Sub

Public Shared Sub UnregisterResponsiveItem(item as LDS_DarkModeResponsive)
 if(ResponsiveItems<>nil) then
  var index as integer = ResponsiveItems.LastIndex
  while index>=0
   if(ResponsiveItems(index) = item) then
    ResponsiveItems.RemoveAt(index)
   end
   index=index-1
  wend
 end
End Sub

Private Shared Property ResponsiveItems() As LDS_DarkModeResponsive
 

Now the DesktopApplication's AppearanceChanged event handler: 

Sub AppearanceChanged() Handles AppearanceChanged
 LDS_AppearanceChangeManager.AppearanceChanged()
End Sub

Now that is the machinery to add to your application.  Not so much I think, 1) one class interface, 2) one singleton class and 3) one line in the DesktopApplication's AppearanceChanged event handler.

To actually handle the things which need to adapt to an appearance change.  For each window, control or class, which needs to be responsive to DarkMode there are 4 steps.

Most of my classes & controls either include an 'UpdateAppearance' method, which also happens to deal with all the 'isDarkMode' stuff, or implements an 'AppearanceChanged' method.  Of course you might choose to define and implement an 'AppearanceChanged' event definition and event handler or similar method of handling the updates.

This implementation is quite light weight and easy to code.  The reason that this is the FIRST implementation I'm SHOWING is that I think it is much clearer for a less-experienced developer exactly what is occurring, it is maximally flexible, it is computationally efficient, and it is a pattern you will likely use in other contexts.  Your class registers itself when constructed, and is added to a list, it is removed from the list (by unregistering) when your class is destructed, and while it exists, any change to DarkMode will notify all registered classes of the change by a call to your class' AppearanceChanged method.  Now for a second approach... 

LDS_AppearanceChangeAutoManager - Solution #3

A little less flexible, but very generally completely sufficient solution is implemented in LDS_AppearanceChangeAutoManager.  This solution requires no registration/unregistration, but will not be able to update any classes not in the Window & Controls tree.  This is all that is required, 2 steps + 1 step in your application.

AppearanceChanged Event Handler code: 

Sub AppearanceChanged() Handles AppearanceChanged
 LDS_AppearanceChangeAutoManager.AppearanceChanged()
End Sub

So here the magic is in the LDS_AppearanceChangeAutoManager, which is a singleton class, with one function, 'AppearanceChanged' with the following code which just checks each application window and each control in each window, for each, if they are (i.e. implement) LDS_DarkModeResponsive, then the object's 'AppearanceChanged' method will be called.

Here is the code for LDS_AppearanceChangeAutoManager.AppearanceChanged. 

for each aWin as DesktopWindow in app.Windows
 if(aWin isa LDS_DarkModeResponsive) then
  LDS_DarkModeResponsive(aWin).AppearanceChanged()
 end
 var aCtrl as Object
 for index as integer = 0 to aWin.ControlCount-1
  aCtrl = aWin.Control(index)
  if(aCtrl isa LDS_DarkModeResponsive) then
   LDS_DarkModeResponsive(aCtrl).AppearanceChanged()
  end
 next
next

Either of these solutions works well for Dark Mode responsiveness.  I am using Solution #3 in my programs, until and unless something pops up which requires DarkMode responsiveness which is outside of the Window/Control tree of objects.  I don't anticipate that.

Feel free to download the file with these classes (see the top of this article or the Table of Contents downloads section) and feel free to implement them in your programs.  If you find a better way to do this, please share.  We should all be learning and can all be teaching.