Cappuccino Theming Guide
Cappuccino features a powerful, programmatic theming engine that allows developers to completely customize the look and feel of user interface components.This allows for complex, state-aware visual definitions that are compiled ("baked") into binary assets for maximum performance and pixel-perfect metric calculations.
1. What Can Be Themed?
Virtually every visual element in the Cappuccino AppKit framework supports theming. You can customize:
- Controls:
CPButton,CPCheckBox,CPRadio,CPSlider,CPStepper,CPColorWell,CPSegmentedControl. - Inputs:
CPTextField,CPSearchField,CPTokenField,CPComboBox,CPPopUpButton. - Containers:
CPWindow(Standard, Modal, HUD),CPAlert,CPBox,CPTabView,CPSplitView,CPScrollView. - Data Views:
CPTableView(Headers, Rows, Grids, Drag & Drop),CPOutlineView,CPBrowser. - Indicators:
CPProgressIndicator(Bar, Spinner),CPLevelIndicator. - Menus:
CPMenu,CPMenuItem(Checkmarks, Backgrounds).
2. Core Concepts
Theme States
Components in Cappuccino are state-aware. A generic "style" is rarely applied; instead, you define how a control looks in specific situations. States can be combined to create highly specific rules.
- Core States:
CPThemeStateNormal,CPThemeStateHighlighted(Pressed),CPThemeStateDisabled,CPThemeStateSelected(Toggled on). - Contextual States:
CPThemeStateKeyWindow: The window containing the control is currently focused.CPThemeStateFirstResponder: The specific control has keyboard focus.
- Style Variants:
CPThemeStateHUD: A special state automatically applied to controls residing in a window with theCPHUDBackgroundWindowMaskstyle.CPThemeStateControlSizeSmall/CPThemeStateControlSizeMini: For sizing variations.
Theme Attributes
Attributes are key-value pairs assigned to states.
@"bezel-color": The background/border of the control.@"text-color": The color of the label.@"font": The typeface used.@"content-inset": Padding inside the control.
3. Modern Theming: CSS vs. Images
Cappuccino theming has evolved significantly. It is important to understand the difference between legacy styles (Aristo 1 & 2) and modern styles (Aristo 3+).
Image-Based (Legacy)
Older themes relied heavily on CPImage and CPNinePartImage. Every state (Normal, Highlighted, Disabled) required a separate .png asset sliced into a 3x3 grid.
- Pros: Exact pixel control for complex textures.
- Cons: Large file sizes; does not scale well to Retina/High-DPI displays without multiple assets; difficult to change colors dynamically.
CSS-Based (Modern)
Modern themes utilize CPColor instantiated with CSS Dictionaries instead of static images. This delegates rendering to the browser's CSS engine while keeping the logic in Objective-J.
- Pros: Resolution independent (Retina ready); extremely lightweight; supports CSS animations and gradients.
Example: CSS Animations & Gradients
You can achieve complex visual effects, such as an animated "Barber Pole" progress bar, purely via code using colorWithCSSDictionary:
// Inside a Theme Descriptor
var animatedBarColor = [CPColor colorWithCSSDictionary:@{
// Base Color
@"background-color": @"#5982DA",
// CSS Linear Gradient
@"background-image": @"linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent)",
// Sizing and Border
@"background-size": @"30px 30px",
@"border-radius": @"2px",
// CSS Keyframe Animation
@"animation": @"cp-progress-indicator-bar-slide 1s linear infinite",
@"-webkit-animation": @"cp-progress-indicator-bar-slide 1s linear infinite"
}];
// Register this color for the Progress Bar's "Bar" attribute
[self registerThemeValues:[
[@"bar-color", animatedBarColor]
] forView:progressIndicator];
4. Developing Custom Controls
When writing a custom CPControl subclass, you should integrate it with the theming engine. This allows your control to adapt to different environments (like HUD windows) without hard-coding colors.
Step 1: Define Theme Identity
You must tell the theme engine what "class" name your control maps to in the theme files, and what attributes it supports.
@implementation CPLevelIndicator : CPControl
// 1. Define the string key used in the Theme Descriptor
+ (CPString)defaultThemeClass
{
return "level-indicator";
}
// 2. Define the attributes this control allows, and their default values.
// Using [CPNull null] forces the developer to define them in the theme.
+ (CPDictionary)themeAttributes
{
return @{
@"bezel-color": [CPNull null],
@"color-empty": [CPNull null],
@"color-normal": [CPNull null],
@"color-warning": [CPNull null],
@"color-critical": [CPNull null],
@"spacing": 1.0
};
}
@end
Step 2: Retrieving Attributes in Layout
Efficient controls should query theme attributes during layoutSubviews.
Crucial Pattern: Propagating HUD State
If your control is placed inside a HUD window, it should likely inherit that look. You often need to manually check the window mask and append the CPThemeStateHUD state.
- (void)layoutSubviews
{
// 1. Capture the base state (Normal, Highlighted, Disabled, etc.)
var themeState = [self themeState];
// 2. Check if we are inside a HUD window.
// If so, force the HUD bit on. This allows the theme to return
// white text/dark backgrounds instead of standard system colors.
if ([[self window] styleMask] & CPHUDBackgroundWindowMask)
themeState = themeState.and(CPThemeStateHUD);
// 3. Retrieve a specific attribute for the calculated state
var bezelColor = [self valueForThemeAttribute:@"bezel-color" inState:themeState];
// 4. Apply logic using theme values
var filledColor = [self valueForThemeAttribute:@"color-normal" inState:themeState],
value = [self doubleValue];
if (value >= [self criticalValue])
filledColor = [self valueForThemeAttribute:@"color-critical" inState:themeState];
else if (value >= [self warningValue])
filledColor = [self valueForThemeAttribute:@"color-warning" inState:themeState];
// 5. Draw/Update Views
[[self layoutEphemeralSubviewNamed:@"track" ... ] setBackgroundColor:bezelColor];
[[self layoutEphemeralSubviewNamed:@"fill" ... ] setBackgroundColor:filledColor];
}
5. Creating and Baking a Theme (Advanced)
To create a theme, you write executable code by subclassing BKThemeDescriptor. This code is then "baked" into a binary file.
5.1 Basic Structure
Create a new file (e.g., Resources/Themes/MyCustomTheme.j):
@import <AppKit/CPControl.j>
@import <BlendKit/BKThemeDescriptor.j>
// Import your custom control if you are theming it
@import "CPLevelIndicator.j"
@implementation MyCustomTheme : BKThemeDescriptor
+ (CPString)themeName
{
return @"MyCustomTheme";
}
// Define specific components below...
@end
5.2 Theming a Component
To theme a component, define a class method that returns a template instance. You then register attributes for specific state combinations using registerThemeValues:forView:.
+ (CPLevelIndicator)levelIndicator
{
// 1. Create a template instance with default metrics
var levelIndicator = [[CPLevelIndicator alloc] initWithFrame:CGRectMake(0.0, 0.0, 100.0, 18.0)];
// 2. Define Colors (Using Modern CSS Dictionaries)
var green = [CPColor colorWithCSSString:@"#2ecc71"];
var yellow = [CPColor colorWithCSSString:@"#f1c40f"];
var red = [CPColor colorWithCSSString:@"#e74c3c"];
// HUD Colors (Monochrome style for Dark Mode)
var hudFill = [CPColor whiteColor];
var hudEmpty= [CPColor colorWithWhite:1.0 alpha:0.2];
// 3. Define the Attribute Map
var themeValues = [
// Attribute Value State(s)
// STANDARD STATE
[@"bezel-color", [CPColor clearColor]],
[@"color-normal", green],
[@"color-warning", yellow],
[@"color-critical", red],
[@"spacing", 1.0],
// HUD STATE (Dark Mode / Transparent Window)
// Since our control adds CPThemeStateHUD in layoutSubviews, these will apply
// automatically when the control is in a HUD window.
[@"color-empty", hudEmpty, CPThemeStateHUD],
[@"color-normal", hudFill, CPThemeStateHUD],
[@"color-warning", hudFill, CPThemeStateHUD],
[@"color-critical", hudFill, CPThemeStateHUD]
];
// 4. Important: Enable CSS Mode
// If you are using CSS colors instead of CPImages/NinePartImages,
// you must flag the view as 'css-based'. This notifies the runtime
// to bypass legacy image slicing logic.
[themeValues addObject:[@"css-based", YES]];
// 5. Register
[self registerThemeValues:themeValues forView:levelIndicator];
return levelIndicator;
}
5.3 The Build Process: "Baking"
A common misconception is that Cappuccino loads raw .j theme files at runtime. It does not. You must compile your imperative BKThemeDescriptor code into a declarative, binary-compatible .keyedtheme file.
Why Bake?
- Performance: Loading a pre-calculated binary archive is significantly faster than parsing JS/ObjJ at startup.
- Metrics: The build tool instantiates actual UI objects to calculate frame sizes and hit-test areas.
The BlendTask
The "baking" is handled by BlendTask in your build system.
- Import: Loads your descriptor class.
- Instantiation: Runs your code to create UI objects in memory.
- Extraction: Records the properties for every state.
- Archiving: Saves the result as
MyTheme.keyedtheme.
6. Build Configuration
To make your theme usable, you must configure the build system.
Configure the Jakefile
Tell the build tool where your theme source files are so BlendTask can bake them.
var blend = require("BlendKit/Build/blendtask").blend;
blend("MyApplication", function(task) {
task.setSources(["AppController.j", "main.j"]);
task.setResources(["Resources"]);
// 👇 Register your theme source file here for baking
task.setThemeDescriptors(["Resources/Themes/MyCustomTheme.j"]);
});
Running jake build will now generate MyCustomTheme.keyedtheme.
7. Loading and Switching Themes
Setting the Default Theme
The Info.plist file defines the default theme loaded at application launch. This value must match the string returned by + (CPString)themeName in your theme class.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- 👇 This triggers the loading of MyCustomTheme.keyedtheme -->
<key>CPDefaultTheme</key>
<string>MyCustomTheme</string>
<key>CPApplicationDelegateClass</key>
<string>AppController</string>
<key>CPPrincipalClass</key>
<string>CPApplication</string>
</dict>
</plist>
Dynamic Switching
Once themes are baked and available in the app bundle, you can switch them dynamically at runtime.
- (void)switchToPlayfulTheme:(id)sender
{
// Load the theme (requires PlayfulTheme.keyedtheme to be in Info.plist or resources)
var newTheme = [CPTheme themeNamed:@"PlayfulTheme"];
// Apply globally
[CPTheme setDefaultTheme:newTheme];
// Force redraw
[[[CPApplication sharedApplication] mainWindow] setNeedsDisplay:YES];
}