diff --git a/android/src/toga_android/app.py b/android/src/toga_android/app.py index 1ca3257897..4fe60f83c0 100644 --- a/android/src/toga_android/app.py +++ b/android/src/toga_android/app.py @@ -257,6 +257,14 @@ def get_screens(self): screen_list = display_manager.getDisplays() return [ScreenImpl(self, screen) for screen in screen_list] + ###################################################################### + # App state + ###################################################################### + + def get_dark_mode_state(self): + self.interface.factory.not_implemented("dark mode state") + return None + ###################################################################### # App capabilities ###################################################################### diff --git a/changes/2841.feature.txt b/changes/2841.feature.txt new file mode 100644 index 0000000000..129aa8a383 --- /dev/null +++ b/changes/2841.feature.txt @@ -0,0 +1 @@ +Apps can now interrogate whether they are in dark mode on some platforms. diff --git a/cocoa/src/toga_cocoa/app.py b/cocoa/src/toga_cocoa/app.py index d2cff2172f..f7a018d63f 100644 --- a/cocoa/src/toga_cocoa/app.py +++ b/cocoa/src/toga_cocoa/app.py @@ -312,6 +312,17 @@ def set_main_window(self, window): def get_screens(self): return [ScreenImpl(native=screen) for screen in NSScreen.screens] + ###################################################################### + # App state + ###################################################################### + + def get_dark_mode_state(self): + appearance = self.native.effectiveAppearance + # Standard theme names in MacOS + # https://developer.apple.com/documentation/appkit/nsappearance/name-swift.struct?language=objc + in_dark_mode = "Dark" in str(appearance.name) + return in_dark_mode + ###################################################################### # App capabilities ###################################################################### diff --git a/core/src/toga/app.py b/core/src/toga/app.py index 7b54c9d4d4..eda024ed47 100644 --- a/core/src/toga/app.py +++ b/core/src/toga/app.py @@ -391,6 +391,15 @@ def home_page(self) -> str | None: menu items.""" return self._home_page + @property + def dark_mode(self) -> bool | None: + """Whether the user has dark mode enabled in their environment (read-only). + + :returns: A Boolean describing if the app is in dark mode; ``None`` if Toga + cannot determine if the app is in dark mode. + """ + return self._impl.get_dark_mode_state() + @property def icon(self) -> Icon: """The Icon for the app. diff --git a/core/tests/app/test_app.py b/core/tests/app/test_app.py index 6ad1e23aac..22524eb5c1 100644 --- a/core/tests/app/test_app.py +++ b/core/tests/app/test_app.py @@ -796,6 +796,12 @@ async def on_running(self): assert running["called"] +def test_dark_mode_state(app): + """Dark mode settings can be read through the dark_mode property.""" + # The dummy backend is currently set to always be True + assert app.dark_mode + + def test_deprecated_id(event_loop): """The deprecated `id` constructor argument is ignored, and the property of the same name is redirected to `app_id`""" diff --git a/dummy/src/toga_dummy/app.py b/dummy/src/toga_dummy/app.py index 4eb5172016..1136acda09 100644 --- a/dummy/src/toga_dummy/app.py +++ b/dummy/src/toga_dummy/app.py @@ -93,6 +93,13 @@ def get_screens(self): def set_icon(self, icon): self._action("set_icon", icon=icon) + ###################################################################### + # App state + ###################################################################### + + def get_dark_mode_state(self): + return True + ###################################################################### # App capabilities ###################################################################### diff --git a/gtk/src/toga_gtk/app.py b/gtk/src/toga_gtk/app.py index 467614e6d8..f30004dd83 100644 --- a/gtk/src/toga_gtk/app.py +++ b/gtk/src/toga_gtk/app.py @@ -188,6 +188,14 @@ def get_screens(self): ] return screen_list + ###################################################################### + # App state + ###################################################################### + + def get_dark_mode_state(self): + self.interface.factory.not_implemented("dark mode state") + return None + ###################################################################### # App capabilities ###################################################################### diff --git a/iOS/src/toga_iOS/app.py b/iOS/src/toga_iOS/app.py index 3a24161845..a627326675 100644 --- a/iOS/src/toga_iOS/app.py +++ b/iOS/src/toga_iOS/app.py @@ -112,6 +112,14 @@ def set_main_window(self, window): def get_screens(self): return [ScreenImpl(UIScreen.mainScreen)] + ###################################################################### + # App state + ###################################################################### + + def get_dark_mode_state(self): + self.interface.factory.not_implemented("dark mode state") + return None + ###################################################################### # App capabilities ###################################################################### diff --git a/iOS/src/toga_iOS/screens.py b/iOS/src/toga_iOS/screens.py index 00570e1ebc..a992e01c62 100644 --- a/iOS/src/toga_iOS/screens.py +++ b/iOS/src/toga_iOS/screens.py @@ -9,6 +9,7 @@ class Screen: _instances = {} def __new__(cls, native): + # native is an instance of UIScreen if native in cls._instances: return cls._instances[native] else: diff --git a/testbed/tests/app/test_app.py b/testbed/tests/app/test_app.py index 8573cd34af..301affb568 100644 --- a/testbed/tests/app/test_app.py +++ b/testbed/tests/app/test_app.py @@ -1,3 +1,4 @@ +from types import NoneType from unittest.mock import Mock import toga @@ -253,3 +254,7 @@ async def test_app_icon(app, app_probe): app.icon = toga.Icon.APP_ICON await app_probe.redraw("Revert app icon to default") app_probe.assert_app_icon(None) + + +async def test_dark_mode_state_read(app): + assert isinstance(app.dark_mode, (NoneType, bool)) diff --git a/textual/src/toga_textual/app.py b/textual/src/toga_textual/app.py index 079abc9592..9ee6eab600 100644 --- a/textual/src/toga_textual/app.py +++ b/textual/src/toga_textual/app.py @@ -74,6 +74,14 @@ def set_main_window(self, window): def get_screens(self): return [ScreenImpl(window._impl.native) for window in self.interface.windows] + ###################################################################### + # App state + ###################################################################### + + def get_dark_mode_state(self): + self.interface.factory.not_implemented("dark mode state") + return None + ###################################################################### # App capabilities ###################################################################### diff --git a/web/src/toga_web/app.py b/web/src/toga_web/app.py index 5ca159a80f..5f9e2b8e80 100644 --- a/web/src/toga_web/app.py +++ b/web/src/toga_web/app.py @@ -68,6 +68,14 @@ def set_main_window(self, window): def get_screens(self): return [ScreenImpl(js.document.documentElement)] + ###################################################################### + # App state + ###################################################################### + + def get_dark_mode_state(self): + self.interface.factory.not_implemented("dark mode state") + return None + ###################################################################### # App capabilities ###################################################################### diff --git a/winforms/src/toga_winforms/app.py b/winforms/src/toga_winforms/app.py index f01610667c..d40e6e1036 100644 --- a/winforms/src/toga_winforms/app.py +++ b/winforms/src/toga_winforms/app.py @@ -206,6 +206,14 @@ def get_screens(self): ] return screen_list + ###################################################################### + # App state + ###################################################################### + + def get_dark_mode_state(self): + self.interface.factory.not_implemented("dark mode state") + return None + ###################################################################### # App capabilities ######################################################################