Style themes update

Michael Strauß michaelstrau2 at gmail.com
Sat Jan 28 20:07:21 UTC 2023


The following document describes the current state of the proposed
theming feature for JavaFX.
I've tweaked the feature stacking and APIs to accomodate some of the
insights gained by previous discussions on this mailing list. For
example, controlling the platform window apperance and potentially
using progammatically generated Stylesheet objects is now possible.
I'm now looking to gather another round of feedback to further refine
the proposed feature.

Since the feature is quite large, it will be delivered in three installments:

PR 1: Add a platform preferences API to expose UI settings of the
operating system
PR 2: Add a Stage.appearance property to make the appearance of
platform window decorations customizable
PR 3: Add StyleTheme, which promotes themes to first-class citizens in JavaFX



Summary
-------
Enhance JavaFX with style themes, which are collections of CSS
stylesheets with custom logic to dynamically change or swap
stylesheets at runtime. This allows developers to create visually
pleasing themes that, due to their dynamic nature, can integrate well
with the look&feel of modern operating systems (e.g. dark mode and
accent colors).



Goals
-----
* Expose UI settings of the operating system (color preferences, dark
mode, etc.) as "platform preferences" in JavaFX.
* Allow the light/dark appearance of window decorations be controlled by JavaFX.
* Promote CSS user-agent themes from an implementation detail to a
first-class concept.
* Implement Caspian and Modena as first-class themes.



Non-goals
---------
It is not a goal to
* Provide a rich/opinionated framework that developers can use to
create custom theme classes;
* Add any new features to Caspian or Modena (e.g. dark mode).



Motivation
----------
While JavaFX knows about themes (it ships with two of them: Caspian
and Modena), it doesn't offer a public API to create custom themes.
What's a theme? It's a dynamic collection of user-agent stylesheets
with some logic to determine when and how individual stylesheets are
included in the CSS cascade. For example, a stylesheet with
high-contrast theme colors may be dynamically included in the cascade
depending on whether the operating system was set to a high contrast
mode.

In the CSS subsystem, JavaFX already supports application-wide
user-agent stylesheet collections (that's how the built-in themes are
implemented). The public API seems like an afterthought: the
Application.userAgentStylesheet property, which specifies a stylesheet
URI, also accepts two magic constants ("CASPIAN" and "MODENA") to
select one of the built-in themes.

There are two workarounds to create a custom theme for a JavaFX application:
1) Add author stylesheets to the Scene
2) Replace the built-in theme with a single new user-agent stylesheet

The first option can be used to extend or modify the built-in theme,
but it does so by changing the semantics of the new styles: author
stylesheets override user code, while user-agent stylesheets don't. It
also makes it harder to create entirely new themes for JavaFX, since
the built-in styles are always present in the CSS cascade.

The second option retains the semantics of themes (allow user code to
override properties), but comes at the price of being quite clunky:
* Only a single stylesheet can be specified. As such, there's no way
to create a custom theme that is comprised of many individual
stylesheets (like the built-in themes are).
* Existing themes can't be modified. Once the
Application.userAgentStylesheet property is set to any stylesheet, all
stylesheets that came with the built-in theme are discarded.
* Even if developers choose to copy and modify a built-in stylesheet
in its entirety, they will lose all dynamic features (for example,
reacting to changes of the high contrast platform preference), because
the logic that conditionally adds to or removes stylesheets from the
CSS cascade is only available for the two built-in themes and cannot
be added to custom stylesheets.

Ultimately, JavaFX needs to enable developers to easily create
visually pleasing themes that keep up with the changing trends of user
interface design. This includes the ability to integrate well with the
platforms that JavaFX applications run on, for example by supporting
dark mode and accent coloring.



Platform preferences
--------------------
Platform preferences are the preferred UI settings of the operating
system. For example, on Windows this includes the color values
identified by the Windows.UI.ViewManagement.UIColorType enumeration;
on macOS this includes the system color values of the NSColor class.
Exposing these dynamic values to JavaFX application allows developers
to create themes that can integrate seamlessly with the color scheme
of the operating system.

Platform preferences are exposed as an ObservableMap of
platform-specific key-value pairs, which means that the preferences
available on Windows are different from macOS or Linux. JavaFX
provides a small, curated list of preferences that are available on
most platforms, and are therefore exposed with a platform-independent
API:

    public interface Preferences extends ObservableMap<String, Object> {
        // Platform-independent API
        ReadOnlyObjectProperty<Appearance> appearanceProperty();
        ReadOnlyObjectProperty<Color> backgroundColorProperty();
        ReadOnlyObjectProperty<Color> foregroundColorProperty();
        ReadOnlyObjectProperty<Color> accentColorProperty();

        // Convenience methods to retrieve platform-specific values from the map
        String getString(String key);
        String getString(String key, String fallbackValue);
        Boolean getBoolean(String key);
        boolean getBoolean(String key, boolean fallbackValue);
        Color getColor(String key);
        Color getColor(String key, Color fallbackValue);
    }

The platform appearance is defined by the new javafx.stage.Appearance
enumeration:

    public enum Appearance {
        LIGHT,
        DARK
    }

An instance of the Preferences interface can be retrieved by calling
Platform.getPreferences().



Stage appearance
----------------
The Preferences.appearance property indicates whether the operating
system uses a light or dark color scheme. A well-designed dark mode
integration requires a JavaFX application to not only provide a set of
suitable dark stylesheets, but also configure the platform window
decorations to reflect the dark theme. This requires a new API to
control the appearance of window decorations:

    public class Stage {
        ...
        public ObjectProperty<Appearance> appearanceProperty();
        public Appearance getAppearance();
        public void setAppearance(Appearance appearance);
        ...
    }

By default, a JavaFX stage uses a light appearance. Developers can set
the stage appearance to either light or dark independently from the
operating system's dark mode setting. This can be useful for
applications that only offer either a light or a dark theme.
Applications that support both appearances and want to reflect the OS
preference can do so by simply binding the stage appearance to the
platform appearance:

    var stage = new Stage();
    stage.appearanceProperty().bind(
        Platform.getPreferences().appearanceProperty());

    stage.setScene(...);
    stage.show();



Style themes
------------
The new javafx.css.StyleTheme interface promotes themes to first-class
citizens in JavaFX:

    public interface StyleTheme {
        List<Stylesheet> getStylesheets();
    }

Note that the getStylesheets() method returns a list of
javafx.css.Stylesheet instances, not a list of URIs. Applications can
load stylesheets from CSS files with a set of new method on the
Stylesheet class:

    public Stylesheet {
        ...
        public static Stylesheet load(String url);
        public static Stylesheet load(InputStream stream);
        ...
    }

A future enhancement of the javafx.css APIs might allow applications
to programmatically create Stylesheet instances from scratch, without
parsing external CSS files or data URIs.

If a StyleTheme implementation returns an ObservableList<Stylesheet>
from its getStylesheets() method, the CSS subsystem observes the list
for changes and automatically reapplies the stylesheets when the list
has changed. This allows themes to dynamically respond to changing
platform or user preferences by adding or removing stylesheets at
runtime.

The two built-in themes CaspianTheme and ModenaTheme are exposed as
public API in the javafx.scene.control.theme package. Both classes
extend ThemeBase, which is a simple StyleTheme implementation that
allows developers to easily extend the built-in themes by prepending
or appending additional stylesheets:

    Application.setUserAgentStyleTheme(new ModenaTheme() {
        {
            addFirst(Stylesheet.load("stylesheet1.css"));
            addLast(Stylesheet.load("stylesheet2.css"));
        }
    });

ThemeBase has no other extension points aside from addFirst and
addLast, since the built-in theme stylesheets are not designed to
support further extension. A future enhancement may refactor the
built-in themes to make color definitions swappable, and automatically
pick up preferred platform colors and accent colors.



Applying a theme
----------------
A new userAgentStyleTheme property is added to
javafx.application.Application, and userAgentStylesheet is promoted to
a JavaFX property (currently, this is just a getter/setter pair):

    public abstract class Application
        ...
        public static StringProperty userAgentStylesheetProperty();
        public static String getUserAgentStylesheet();
        public static void setUserAgentStylesheet(String url);
        ...
        public static ObjectProperty<StyleTheme> userAgentStyleThemeProperty();
        public static StyleTheme getUserAgentStyleTheme();
        public static void setUserAgentStyleTheme(StyleTheme theme);
    }

userAgentStyleTheme and userAgentStylesheet are correlated to preserve
backwards compatibility: setting userAgentStylesheet to the magic
values "CASPIAN" or "MODENA" will implicitly set userAgentStyleTheme
to a new instance of the CaspianTheme or ModenaTheme class. In the CSS
cascade, userAgentStylesheet has a higher precedence than
userAgentStyleTheme. Setting userAgentStylesheet to a value other than
"CASPIAN" or "MODENA" will override the style theme of the application
in its entirety, preserving backwards compatibility with applications
that were created before the style theme API was added.

A future enhancement may add a userAgentStyleTheme property to other
scene graph classes like Scene, Region or Parent. This would allow
applications to use different style themes across the application.


More information about the openjfx-dev mailing list