vcl/README.themes.md | 209 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+)
New commits: commit 5ea1f5d8e05e24c704a987fd86a1f63fea9c9641 Author: Sahil Gautam <sahil.gautam.ext...@allotropia.de> AuthorDate: Mon Mar 3 17:14:48 2025 +0530 Commit: Sahil Gautam <sahil.gautam.ext...@allotropia.de> CommitDate: Mon Mar 17 18:08:35 2025 +0100 Add vcl/README.themes.md for themes implementation details Change-Id: Ia1df363bc0a8d8d23ce3182504c4d0fb9164245c Reviewed-on: https://gerrit.libreoffice.org/c/core/+/182447 Reviewed-by: Sahil Gautam <sahil.gautam.ext...@allotropia.de> Tested-by: Jenkins diff --git a/vcl/README.themes.md b/vcl/README.themes.md new file mode 100644 index 000000000000..72337f8d68cf --- /dev/null +++ b/vcl/README.themes.md @@ -0,0 +1,209 @@ +# LibreOffice Themes + +## How to read this + +It is suggested that you have the code open side-by-side and first read some part here and then the code that it talks about. + +## VLC Plugins and the UpdateSettings functions + +LibreOffice VCL (a bunch of interfaces and a base implementation) has four main platform specific implementations (gtk, qt, win, +osx). Each VCL plugin has an `UpdateSettings(AllSettings& rSettings)` function somewhere. This function reads styling information +like colors from the platfrom specific widget toolkit and loads the information into the `StyleSettings` instance passed in the +argument (indirectly through `AllSettings`). + +## The StyleSettings Instance + +The `StyleSettings` (SS) class manages the colors. Various parts of the codebase call getters and setters on SS to get the default +color, or to override it. There exists a static `StyleSettings` instance in the application, and the instances which are created +here and there are merged with that static SS instance. we can access the static instance from anywhere in the application by the +following function call. + +```cpp +const StyleSettings& rStyleSettings = Application::GetSettings().GetStyleSettings(); +``` + +## How UserPreferences are Saved (registry) + +There exist two kind of files for state/configuration management, .xcu and .xcs files. These are XML files, the .xcs files are XML +schema files which define the "schema" for the configuration like a colorscheme node will have the following entries colors , and +the colors will have a light and a dark variant... The .xcu files are the configuration data files which define the default values +for the configuration nodes defined in the schema files. + +We use the term `registry` to refer to the application's configuration and we save the modifications to the default values (set in +the .xcu files) in a file named `registrymodifications.xcu` which lives in `$XDG_CONFIG_HOME/libreoffice/(somewhere here)`. + +## ColorConfig, ColorConfig_Impl, and EditableColorConfig + +From the themes/colors perspective, think of `ColorConfig_Impl` as a code representation of the colors in the registry, and +think of `ColorConfig` as a *read-only wrapper over `ColorConfig_Impl`. There exists another class in this bunch named +`EditableColorConfig`, and as the name suggests it is a read-write wrapper over `ColorConfig_Impl`. + +The "Appearance" tab on the "Options" dialog interacts with the registry thanks to an `EditableColorConfig` instance. + +## Getting System Colors into the static StyleSettings object + +So if you setup some printfdebugging statements in the `UpdateSettings` functions and in the `ColorConfig` constructor, you will +find that when the application starts, first the `UpdateSettings` functions are executed, and then the first every `ColorConfig` +instance is created. + +Also if you add and set a non-static flag to the `StyleSettings` and print it out from the `UpdateSettings` functions and the +`ColorConfig` constructor, you will find that the flag doesn't make it to the static instance (accessed from in `ColorConfig`) +immediately. We use such a flag `mbSystemColorsLoaded` to see if the static `StyleSettings` object has the system colors or not. + +## The LibreOfficeTheme registry flag + +```xml +<prop oor:name="LibreOfficeTheme" oor:type="xs:short" oor:nillable="false"> + <info> + <desc>Specifies LibreOfficeTheme state.</desc> + <label>LibreOffice Theme</label> + ... +``` + +To enable or disable theming, we have a `LibreOfficeTheme` enum in the registry which is represented by `enum class ThemeState`. +in the code. The default value is `ENABLED` and the only way for the user to disable it is by changing it to `0` in the expert +configuration. + +> It's still a dispute whether to enable or disable a theming by default, so please refer to the .xcs file and don't take the +> explaination for implementation. + +```cpp +enum class ThemeState +{ + DISABLED = 0, + ENABLED = 1, + RESET = 2, +}; +``` + +## High Level Code overview of Themes Implementation + +We load the colors from the widget toolkit into the `StyleSettings` object and set a flag `mbSystemColorsLoaded` to `true`. Then +in the `ColorConfig` constructor `ColorConfig::SetupTheme()`. We will be back to `SetupTheme` after we understand how theme colors +are stored. + +## Talking about Singleton ThemeColors class and the path Colors travel + +`themecolors.hxx` defines a singleton class named `ThemeColors`. This class has two static members. The second one is that of the +class itself, and the first one is a boolean flag which is used to check if theme colors are cached or not. + +```cpp +class VCL_DLLPUBLIC ThemeColors +{ + ThemeColors() {} + static bool m_bIsThemeCached; + static ThemeColors m_aThemeColors; + ... +``` + +All the colors are essentially registry values grouped in colorschemes and accessed using various `ColorConfig`s +(ColorConfig_Impl, EditableColorConfig, ColorConfig), we just talked about it above. So the theme colors (colors for the UI) are +loaded from the registry into this singleton `ThemeColors` instance, and we set the `m_bIsThemeCached` flag to `true`. Then the +various VCL plugins check the flag and if the theme colors are cached, these colors are sent to the widget toolkit in different +ways depending on the toolkit, like css in case of gtk, QPalette in case of Qt. + +Then when the UpdateSettings function is called again, the colors read from the widget toolkit are these custom colors. Then the +`StyleSettings` object is loaded with these colors and they make it to every corner of the application which gets its colors from +`StyleSettings` object. + +## Back to ColorConfig::SetupTheme() + +So in `ColorConfig::SetupTheme()`, we first check if `LibreOfficeTheme` enum is set to `DISABLED`, and if so then we mark +`ThemeColors` as not cached, so no custom colors are set at the toolkit level and return from the `SetupTheme()` function. Then we +check if `LibreOfficeTheme` is set to `RESET` which happens when the user presses the `Reset All` button (after which he restarts +the system). If true then we check for `mbSystemColorsLoaded` to see if the default colors from the widget toolkit have made it to +the static StyleSettings instance or not, and if that's true as well, we set `LibreOfficeTheme` enum to `ENABLED` + +Then in the last part of `SetupTheme()`, which we reach only if `LibreOfficeTheme` is set to `ENABLED`, we check if the theme +colors are cached or not (if the UI colors are loaded from the registry into the static `ThemeColors` instance or not). If cached, +we don't touch those over and over. If theme colors are not cached, then we `Load` the `CurrentScheme` which means that we load +the colors for the current scheme from the registry and store them in `ColorConfig_Impl` instance. + +```cpp + ... + if (!ThemeColors::IsThemeCached()) + { + // registry to ColorConfig::m_pImpl + m_pImpl->Load(GetCurrentSchemeName()); + m_pImpl->CommitCurrentSchemeName(); + + // ColorConfig::m_pImpl to static ThemeColors::m_aThemeColors + LoadThemeColorsFromRegistry(); + } + ... +``` + +Then the `LoadThemeColorsFromRegistry` function is called which loads colors from the registry into the `ThemeColors` instance by +calling `ColorConfig::GetColorValue` for each entry. In `ColorConfig::GetColorValue` call, if the color value in the registry is +`COL_AUTO` then we call `ColorConfig::GetDefaultColor` which returns hardcoded default colors for the document, and StyleSettings +colors for the UI (see `lcl_GetDefaultUIColor`). + +If the color value is not `COL_AUTO`, then the value from the registry is returned, this way we save the user's preferences and +get the default colors from hardcoded colors array and StyleSettings. + +```cpp +void ColorConfig::LoadThemeColorsFromRegistry() +{ + ThemeColors& rThemeColors = ThemeColors::GetThemeColors(); + + rThemeColors.SetWindowColor(GetColorValue(svtools::WINDOWCOLOR).nColor); + rThemeColors.SetWindowTextColor(GetColorValue(svtools::WINDOWTEXTCOLOR).nColor); + ... +``` + +## What happens when "Reset All" is pressed + +When the `Reset All` button is pressed, all the registry color values are set to `COL_AUTO` and `LibreOfficeTheme` is set to +`RESET`. Then after restart, the `IsThemeReset` conditional in `ColorConfig::SetupTheme()` checks if StyleSettings has the system +colors or not, and once it has, `LibreOfficeTheme` is set to `ENABLED` which then goes through the last conditional and +`LoadThemeColorsFromRegistry` is called (just explained above). Since all the registry entries were set to `COL_AUTO`, we end up +getting default values for all the colors (hardcoded ones for document and StyleSettings colors for UI). + +## ColorConfigValue now has nLightColor and nDarkColor entries + +```cpp +struct ColorConfigValue +{ + bool bIsVisible; // validity depends on the element type + ::Color nColor; // used as a cache for the current color + Color nLightColor; + Color nDarkColor; + ... +``` + +Each color entry has two color values, one for light and one for dark. Based on the `ApplicationAppearance`, either light or dark +color value is used. Since the nColor "variable name" is used in 250+ places in the codebase, I found it unreasonible to replace +all the 250+ references with a conditional like this. + +```cpp +Color nColor; +if (IsDarkMode()) + nColor = aColorConfig.GetColorValue( svtools::APPBACKGROUND ).nDarkColor; +else + nColor = aColorConfig.GetColorValue( svtools::APPBACKGROUND ).nLightColor; +``` + +This would have been very inefficient because `IsDarkMode()` is a virtual function (being called 250+ times, maybe every frame??). +So instaed of using a conditional, I use `nColor` as the cache. When the colors are loaded from the registry (see +`ColorConfig_Impl::Load`), I cache the value into `nColor` based on `ApplicationAppearance` value (whether light or dark mode). +And since we ask the user to restart the application after changing appearance or color values, caching works without any issues. + +## Automatic scheme as the fallback + +In case the scheme that you are trying to load doesn't exist because "the extension was removed?", or "someone edited the +registry".. the "Automatic" scheme is used as the fallback. + +```cpp +void ColorConfig_Impl::Load(const OUString& rScheme) + ... + if (!ThemeColors::IsAutomaticTheme(sScheme)) + { + uno::Sequence<OUString> aSchemes = GetSchemeNames(); + bool bFound = std::any_of(aSchemes.begin(), aSchemes.end(), + [&sScheme](const OUString& rSchemeName) { return sScheme == rSchemeName; }); + + if (!bFound) + sScheme = AUTOMATIC_COLOR_SCHEME; + } + ... +```