Guide to implementing Dark Mode

I know I’m very late to the party here, but until now I have avoided doing anything with Dark Mode.

The forum is riddled with Dark Mode related posts, but I see no summary anywhere of what one needs to do to implement Dark Mode support in a Xojo app. The Xojo Blog also has no guide. I don’t see anything in the docs, and the LR has almost nothing to say about it. [EDIT: actually yes it does - https://documentation.xojo.com/topics/user_interface/desktop/macos/supporting_macos_dark_mode.html ]

All I know so far is:

  1. there is a switch in the IDE for supporting Dark mode
  2. said switch is read only in code
  3. there is an App event which fires when Dark Mode goes on or off
  4. Colors have a IsDarkMode property

If I missed something please add to that list.

Question is: what as a designer do I actually need to do to support Dark Mode?

I guess:

  1. turn on the switch
  2. monitor the mode changed event
  3. call different sets of graphics and drawing colors per state?

Judging from the number of related topics on the forum, there must be more to it than that?

It’s found in the Inspector (the right side) when you have Build Settings > Shared selected.

Mostly nothing unless you do custom drawing in your app.

No, this is not the best way to handle Dark Mode. The Paint event for any drawing you’re doing should be aware of IsDarkMode and select the colors necessary. This way, the operating system can decide when is the best time to draw.

  1. You can use IsDarkMode when drawing:

    if Color.IsDarkMode then
    g.DrawingColor = LightGrey
    else
    g.DrawingColor = MidGrey
    end if

  2. There is an AppearanceChanged event in the App. I use this to send internal notifications to most windows and containers so that those can change whatever needs to be changed.

Thanks, that clears things up. I wondered why a color object should have this property. Now I see it’s a global lookup. (I had expected that to be an Application property).

I have a small library of subclassed objects I use in all my Xojo apps which do their own drawing. This is why I have avoided dealing with Dark Mode. For example, listboxes in my apps normally consist of 3 objects: a subclassed listbox which does custom drawing for background and text, a scrollbar object (the normal listbox scrollbars are not used) and a canvas for the header. So all of these will have to change their drawing in Dark Mode. Only the scrollbar should take care of itself.

Some of my apps use the NSStatusItem menubar icons. A customer complained that an icon drawn in black (trendy for non-Dark mode) doesn’t work in Dark Mode.

etc.

Hmm, well if you run into troubles along the way I’m always happy to provide input regarding visibility.

These are handled correctly when you set the NSImage isTemplate true. If you’re using MBS it’s super easy.

Thanks for the offer to help, and thanks very much for this tip! I do use MBS and was dreading having to open Adobe to make alternative icons.

My IDE was locked up compiling, here’s sample code for MBS to set the StatusItem icon:

dim toIcon as new NSImageMBS(SpecialFolder.Resources.Child("myIcon.png"))
toIcon.isTemplate = true
toIcon.setSize(16, 16)
oStatusItemMBS.Image = toIcon
1 Like

The doc page is here:

https://documentation.xojo.com/topics/user_interface/desktop/macos/supporting_macos_dark_mode.html

2 Likes

D’oh! Thank you Paul, I knew I must have missed something :slight_smile:

I notice the UserGuide page is not linked on the following LR pages:

  • Application.AllowDarkMode
  • WebSession.IsDarkMode
  • Color.IsDarkMode

Thanks. I’ve updated those pages.

@Aaron_Hunt

Don’t forget to check out Color Groups, which allow you to specify a dark and a light color (or a named color from the OS) which will automatically switch for you.

For instance, if you made a color group with a light gray and a medium gray and called it GrayColorGroup, you could just use the object in the Paint event like this:

g.DrawingColor = GrayColorGroup

and the framework will choose the right color for you without having to check IsDarkMode.

2 Likes

Thanks; I did not know about ColorGroup. In fact, I was about to embark on making my own class to manage Colors in a similar way, apparently reinventing the wheel …

I just double-checked, and ColorGroup is not mentioned anywhere on the UserGuide page, nor is it linked to on any of the DarkMode-related entries in the LR. Seems like it should at least be under “See Also” on all those pages.

EDIT: Looking into ColorGroup a bit more, seems like it should really get a paragraph on the UserGuide page.

For whatever it’s worth, trying to call a color from a ColorGroup in a project on a Mac running High Sierra (which of course doesn’t have a DarkMode) causes a hard crash:

Application Specific Backtrace 1:
0 CoreFoundation 0x00007fff346cbbbb __exceptionPreprocess + 171
1 libobjc.A.dylib 0x00007fff5b985c76 objc_exception_throw + 48
2 CoreFoundation 0x00007fff34764814 -[NSObject(NSObject) doesNotRecognizeSelector:] + 132
3 CoreFoundation 0x00007fff346414a0 forwarding + 1456
4 CoreFoundation 0x00007fff34640e68 _CF_forwarding_prep_0 + 120
5 MyApp.debug 0x000000010ca51b23 _MacOS.SetAppearanceFromOS%p% + 147
6 MyApp.debug 0x000000010c9126fd ColorGroup.Operator_Convert%c%o + 621
7 MyApp.debug 0x000000010d13c435 xListbox.Event_CellBackgroundPaint%b%ooi8i8 + 1653

The ColorGroup objects can be created, but not called. This makes them a no-go for me, because I regularly move code between machines running different OS versions.

Some, not all, predefined system colors only work from macOS 10.15 +. Otherwise you have to hardcode the colors.

https://documentation.xojo.com/api/graphics/colorgroup.html#colorgroup-constructor(defaultlightcolor_as_color-_defaultdarkcolor_as_color)

This means ColorGroups can be used with the predefined system variables only for macOS 10.15 +. So in your case you have to use the constructor linked above.

But I am using those constructors. The ColorGroup objects are created in code and assigned colors with &c literals. The crash report clearly shows there is a call to an unrecognised selector (no Dark Mode in this OS).

2 Likes

Right. Since Xojo mediates between “easy-to-use functions” and the more complex OS level functions, the Xojo function should take care of avoiding calls on systems that do not support it, and gracefully handle it (as in this case: simply always choose the bright mode colors).

2 Likes

FWIW, the public CustomEditField code has two functions that may prove helpful with Dark Mode handling.

See https://github.com/tempelmann/custom-editfield/blob/master/cef/CustomEditField/EditFieldGlobals.rbbas

  • EditFieldGlobals.IsDarkMode() works on all systems and with older IDE versions.
  • EditFieldGlobals.AdjustColorForDarkMode() attempts to turn a generic color into one that’s suitable for dark mode. It’s not perfect but the best if you cannot predict the colors that are used (like in this project, which allows the user to choose arbitrary colors).
1 Like

You can also check out GraffitiColors which predates ColorGroups but provides all system colors in whatever theme currently being used, custom theme change listener delegates, and system-wide Accessibility settings in the MacOS module, as well as the application of system effects if you’re building for macOS.

1 Like

To see what system colors are available and which OS versions they’re available on, you can download the demo of the Ohanaware App Kit from here.

Then select “System Colors” from the “Tools” menu.

1 Like