A Frutiger Aero look and feel for Java Swing — glossy aqua glass, wet-edge rims, and sky gradients, drawn procedurally with Java2D and installed in one line. Spelled Fruitiger on purpose.
Fruitiger Aero LAF is a Swing LookAndFeel that restyles the 2008-era Frutiger Aero glass aesthetic — translucent controls, wet-edge rims, gloss bands, sky gradients — without native code, JNI, or binary art assets. Every motif is painted with Java2D from a token system.
UIManager.setLookAndFeel(new FruitigerAeroLookAndFeel());
Spelling: the aesthetic is canonically Frutiger (Adrian Frutiger + Windows Aero). The project’s own name is Fruitiger, and that spelling is what the package (
com.fruitiger.aero), Maven artifact, and class names use.
Status: pre-1.0 (0.1.0-SNAPSHOT). Phases 0–4 are in; Phase 5 is hardening toward 1.0. MIT licensed. Java 17+, module com.fruitiger.aero, runtime dependency: java.desktop only.
What it is (and isn’t)
| Is | A Nimbus-derived LAF that claims Synth regions and paints them as Aero; unclaimed regions fall through to stock Nimbus |
| How | FruitigerAeroLookAndFeel extends NimbusLookAndFeel, then wraps SynthLookAndFeel.setStyleFactory(...) with AeroStyleFactory — no sun.*, no --add-opens |
| Design system | Tokens: AeroPalette, AeroMetrics, AeroTypography, AeroMotion — not scattered magic numbers |
| Themes | Aqua Glass, Midnight Aero, Grass Field, Coral Reef; default follows the OS |
| Isn’t | A restyle of native OS window chrome. AeroFrame draws its own glass title bar on an undecorated JFrame; it does not touch DWM / the window manager |
Architecture
The ADR (docs/adr/0001-nimbus-derived-approach.md) is the short version of “why this weird path”:
- Subclassing
NimbusStyle/ injecting Nimbus painters viaUIDefaultsdoes not work (NimbusStyleisfinal; painter keys are ignored). - Capturing Nimbus’s style factory after
super.initialize(), then installingAeroStyleFactory, does. - Claimed regions get an
AeroSynthStylewhose painter isAeroSynthPainter→GlassSurface. Everything else stays Nimbus. getGraphicsUtilsmust keep delegating — otherwise button labels vanish silently (pinned by the spike suite).
flowchart LR
app[Your Swing app] --> ui[UIManager.setLookAndFeel]
ui --> laf[FruitigerAeroLookAndFeel]
laf --> nimbus[NimbusLookAndFeel layout / state / popups]
laf --> factory[AeroStyleFactory]
factory -->|claimed region| aeroStyle[AeroSynthStyle]
factory -->|else| nimbusStyle[Nimbus style]
aeroStyle --> painter[AeroSynthPainter]
painter --> glass[GlassSurface + tokens]
GlassSurface is a pure function of (Graphics2D, size, GlassState, palette, metrics) — body gradient, gloss, wet-edge rim, stacked-ring shadow — so golden/render tests can paint off-screen on the EDT without a display for most suites (CI still uses xvfb-run on Linux).
Themes and settings
Presets (ThemeId) — each shot is the demo gallery under that theme:
Aqua Glass
Light default — classic aqua glass over a sky gradient.

Midnight Aero
Dark preset — same glass language, night sky palette.

Grass Field
Green-accented variant over a meadow horizon.

Coral Reef
Warm-accented variant over turquoise water.

| Key | Display |
|---|---|
aquaGlass |
Aqua Glass (light default) |
midnightAero |
Midnight Aero (dark) |
grassField |
Grass Field (green-accent meadow) |
coralReef |
Coral Reef (warm / turquoise) |
Configure via AeroSettings or JVM properties (-Daero.theme=…, -Daero.background=skyGradient, -Daero.motifs=subtle, -Daero.animate=false, -Daero.uiScale=1.25, -Daero.accent=35C6E6, …). The demo’s theme bar re-installs the LAF live through AeroSettings.toBuilder() without a restart.
Using it
<dependency>
<groupId>com.fruitiger</groupId>
<artifactId>fruitiger-aero-laf</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>
Install once, early, on the EDT (before you build the tree — Swing snapshots colours at construction):
FruitigerAeroLookAndFeel.install();
// or
FruitigerAeroLookAndFeel.install(
AeroSettings.builder()
.theme(ThemeId.MIDNIGHT_AERO)
.backgroundStyle(BackgroundStyle.SKY_GRADIENT)
.motifs(Motifs.subtle())
.build());
Optional companions:
| Helper | Why it exists |
|---|---|
AeroFrame |
Undecorated frame + drawn glass title bar, drag, 6 px resize margin |
AeroBackgrounds.install + installTranslucentPanels |
Sky / grass / image backdrop via JLayer (opaque panels otherwise hide it) |
AeroSelection.applyTo(table/list) |
Table/List selection colours are not reachable from the key table |
GlassMotion.install(component) |
Hover/press bits Swing’s generic Synth state path never sends |
AeroIconFactory |
Procedural glyphs (FOLDER, CHECK, arrows, …) at crisp HiDPI sizes |
Escape hatch: component.putClientProperty("Aero.ui", "none") paints that component as stock Nimbus.
Demo gallery
mvn -B install
java -Djava.awt.headless=false -jar aero-demo/target/aero-demo-*.jar
Tabs for Buttons / Text / Selection / Progress / Menus / Choosers, plus a forced-state matrix, all inside an AeroFrame. --smoke paints the gallery off-screen and fails if stock Nimbus greys or the #E1E3E8 tab chrome leak back in. -Djava.awt.headless=false matters — without it you get black frames or “styling” failures that are really launch failures.
Honest limitations
Documented and verified in PLAN.md / the ADR, not vibes:
- Table/List selection and hover/press motion are opt-in helpers — Swing doesn’t give the LAF a clean hook.
- Sky backgrounds are opt-in (
JLayer+ translucent layout panels); opaque fills run before painters. - Reduced-motion auto-detect is real on Windows / GNOME / KDE desktop properties; bare X11 / Xvfb / CI often see
null— set-Daero.motionReduced=trueor-Daero.animate=falsethere. - Phase 4’s perf budget is not met yet; that’s on the Phase 5 roadmap.
- Deliberately unclaimed examples include
SplitPane(kept as the spike’s fall-through control).
Build / test
mvn -B install # library + demo
mvn -B verify # CI entry
~85 tests across palette, glass golden, frame, gallery, and phase suites. The NimbusOverrideSpikeTest is a hard gate: if a JDK upgrade breaks setStyleFactory, CI should yell. Matrix target: JDK 17 / 21 / 25 × Windows / Linux.
Where to look
| Area | Start here |
|---|---|
| Entry LAF | fruitiger-aero-laf/.../FruitigerAeroLookAndFeel.java |
| Claimed regions / factory | paint/AeroStyleFactory.java |
| Glass paint | paint/GlassSurface.java, AeroSynthPainter.java |
| Tokens | tokens/AeroPalette.java, AeroMetrics, AeroTypography, AeroMotion |
| Frame chrome | frame/AeroFrame.java, AeroTitleBar.java |
| Backdrop | background/AeroBackgrounds.java |
| Demo | aero-demo/.../DemoApp.java |
| Why Nimbus-wrapper | docs/adr/0001-nimbus-derived-approach.md |
| Gotchas / phases | PLAN.md (§19–§21 especially) |
| Full app on this LAF | Fruity Visualizer |