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”:

  1. Subclassing NimbusStyle / injecting Nimbus painters via UIDefaults does not work (NimbusStyle is final; painter keys are ignored).
  2. Capturing Nimbus’s style factory after super.initialize(), then installing AeroStyleFactory, does.
  3. Claimed regions get an AeroSynthStyle whose painter is AeroSynthPainter → GlassSurface. Everything else stays Nimbus.
  4. getGraphicsUtils must 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.

Aqua Glass theme gallery

Midnight Aero

Dark preset — same glass language, night sky palette.

Midnight Aero theme gallery

Grass Field

Green-accented variant over a meadow horizon.

Grass Field theme gallery

Coral Reef

Warm-accented variant over turquoise water.

Coral Reef theme gallery

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.

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=true or -Daero.animate=false there.
  • 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