We want Darling GUI to be themable. After careful consideration, this I what I came up with.
Non-goals:
Implement an advanced, sophisticated, full-featured theming system. E.g. GTK and Qt implement complete CSS (or CSS-like language) parsers and rendering engines. We don't want that. Instead, we want to reuse those existing theming engines (and themes for them) by shipping Darling with special GTK and Qt themes that render Cocoa controls using the existing GTK/Qt drawing machinery.
Goals:
Have a simple theming system/API in place that works just fine without relying on other toolkits.
GTK and Qt (and in the future possibly Android) themes should be supported as first-class citizens, not bolted-on afterthoughts.
Those toolkits should not be required to be installed on user's host unless the user is currently using the corresponding theme.
Provide some compatibility with Mavericks+ NSAppearance API.
Third-party theming solutions for Cocoa should work as expected.
We want to ship a few (think 1 or 2) built-in Aqua-like themes with Darling, alongside the GTK and Qt themes. It's expected that most users will be using the GTK/Qt ones, because those integrate best into native Linux desktop environments.
I initially thought we could use Cocotron's existing NSGrpahicsStyle/NSInterfacePart APIs, but those turned out to be unfit for our needs. So, let's make our own.
The "theme" class is NSAppearance. The theming engine is implemented inside AppKit (and not as a separate private framework), in cocotron/AppKit/NSAppearance.subproj, and replaces NSGrpahicsStyle/NSInterfacePart.
Because in a typical case most (but not all) of the drawing will be done by GTK/Qt using their native cairo/QPainter graphics stacks, it is crucial that we don't perform a costy conversion between O2Surface and cairo_surface_t/QPaintDevice on each drawRect: call. This means the NSWindow's CG/O2 context should be implemented on top of the native graphics stack the current theme prefers.
It should look something like this:
@interface NSAppearance
+ (NSAppearance *) currentAppearance;
- (Class) preferredContextType;
// actual rendering methods
// this renders a button onto *any* kind of O2Context, not just the kind it prefers
// but it can have an optimized implementation for that one
- (void) renderButtonOfSize: (CGSize) size withContext: (O2Context *) context miscArgs: (MiscArgs) args;
@end
@implementation NSGTKAppearance
- (Class) preferredContextType {
return [O2CairoContext class];
}
- (void) renderButtonOfSize: (CGSize) size withContext: (O2Context *) context miscArgs: (MiscArgs) args {
[context performCairoOperation: ^(cairo_t *cairo_context) {
gtk_render_button_somehow(cairo_context, args);
}];
}
@end
@interface O2Context (CairoCompatibility)
- (void) performCairoOperation: (void (^)(cairo_t *)) operation;
@end
@implementation O2CairoContext (CairoCompatibility)
- (void) performCairoOperation: (void (^)(cairo_t *)) operation {
operation(_cairo_context);
}
@end
@implementation O2Context (CairoCompatibility)
- (void) performCairoOperation: (void (^)(cairo_t *)) operation {
char *rawData = [[self surface] pixelBytes];
cairo_surface_t *cairo_surface = cairo_image_surface_create_for_data(rawData, ...);
cairo_t *cairo_context = cairo_create(cairo_surface);
operation(cairo_context);
cairo_destroy(cairo_context);
cairo_surface_destroy(cairo_surface);
}
@end
Next, we should un-implement Cocotron's support for backing store types other than NSBackingStoreBuffered, thus getting rid of all the "window context" vs "backing context" distinctions and context creating difficulties. A CGWindow will then just use [[[NSAppearance currentAppearance] preferredContextType] initWithSize: _size] or something similar to create its context.
We then implement the built-in theme(s) directly on top of CoreGraphics/Onyx2D inside AppKit, but GTK and Qt themes (and their O2Surface/Context impls) go into separate bundles/frameworks. This is because we don't want AppKit to link to Qt and GTK (and preferably cairo too). This also means we do need to make themes loadable and switchable in runtime, but that should be just as easy as doing [NSAppearance setCurrentAppearance: newAppearance], which should automatically make all the windows recreate their contexts using the new O2Context subclass and redraw their contents.
There are some aspects of look'n'feel (e.g. whether to use server-side or client-side window decorations or whether to draw menu in-window, as a pop-up from a headerbar button or forward it over D-Bus to the DE) that should not be defined by the current theme and tweakable independently from it.
Maybe we should also implement NSDynamicSystemColor and somehow integrate it with NSAppearance?
We want Darling GUI to be themable. After careful consideration, this I what I came up with.
Non-goals:
Goals:
NSAppearance
API.We want to ship a few (think 1 or 2) built-in Aqua-like themes with Darling, alongside the GTK and Qt themes. It's expected that most users will be using the GTK/Qt ones, because those integrate best into native Linux desktop environments.
I initially thought we could use Cocotron's existing
NSGrpahicsStyle
/NSInterfacePart
APIs, but those turned out to be unfit for our needs. So, let's make our own.The "theme" class is
NSAppearance
. The theming engine is implemented inside AppKit (and not as a separate private framework), incocotron/AppKit/NSAppearance.subproj
, and replacesNSGrpahicsStyle
/NSInterfacePart
.Because in a typical case most (but not all) of the drawing will be done by GTK/Qt using their native cairo/QPainter graphics stacks, it is crucial that we don't perform a costy conversion between
O2Surface
andcairo_surface_t
/QPaintDevice
on eachdrawRect:
call. This means the NSWindow's CG/O2 context should be implemented on top of the native graphics stack the current theme prefers.It should look something like this:
Next, we should un-implement Cocotron's support for backing store types other than
NSBackingStoreBuffered
, thus getting rid of all the "window context" vs "backing context" distinctions and context creating difficulties. ACGWindow
will then just use[[[NSAppearance currentAppearance] preferredContextType] initWithSize: _size]
or something similar to create its context.We then implement the built-in theme(s) directly on top of CoreGraphics/Onyx2D inside AppKit, but GTK and Qt themes (and their O2Surface/Context impls) go into separate bundles/frameworks. This is because we don't want AppKit to link to Qt and GTK (and preferably cairo too). This also means we do need to make themes loadable and switchable in runtime, but that should be just as easy as doing
[NSAppearance setCurrentAppearance: newAppearance]
, which should automatically make all the windows recreate their contexts using the new O2Context subclass and redraw their contents.There are some aspects of look'n'feel (e.g. whether to use server-side or client-side window decorations or whether to draw menu in-window, as a pop-up from a headerbar button or forward it over D-Bus to the DE) that should not be defined by the current theme and tweakable independently from it.
Maybe we should also implement
NSDynamicSystemColor
and somehow integrate it withNSAppearance
?Feedback & ideas welcome.