From a82c15e6497315c624053e72cdd5524f113e0fd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20L=C3=B8nner=C3=B8d=20Madsen?= Date: Fri, 21 Aug 2026 13:59:10 +0200 Subject: [PATCH] feat(juce): theme the activation UI from ActivationConfig The docs promised a re-skinnable activation UI, but the palette lived on a private ActivationLookAndFeel inside ActivationComponent::Impl and the three font helpers were non-virtual with no typeface of their own, so the only reachable token was the accent colour (#23). Theming now travels on the config. ActivationConfig gains `palette` and `fonts`, read when the component builds its LookAndFeel, which is the only moment that works: the views cache palette-coloured icon Drawables in their constructors, so a theme handed over later would reach half the screen. ActivationPalette moves to a new ActivationTheme.h alongside ActivationFonts (a Typeface::Ptr per role, plus a makeFont hook for full control). Fifteen new tokens replace the 36 colours still hardcoded in ActivationComponent.cpp: the glow and spinner track, card and progress surfaces, scrollbars, seat pips, skeleton bars, the trial and danger ramps, the white on accent fills, and the panel shadow and overlay scrim. Every default is the exact byte value in use before, so the render is unchanged. ActivationDialog also frames its window in palette.backgroundBottom. Covered by four unit tests and eight new snapshots across three unlike themes; the 21 existing snapshots stay byte-identical. --- README.md | 2 +- docs/juce-module.md | 35 ++- modules/moonbase_licensing/README.md | 4 +- .../juce/ActivationConfig.h | 16 +- .../moonbase_licensing/juce/ActivationTheme.h | 105 ++++++++ .../juce/ui/ActivationComponent.cpp | 79 +++--- .../juce/ui/ActivationDialog.cpp | 4 +- .../juce/ui/ActivationLookAndFeel.h | 67 ++--- .../moonbase_licensing/moonbase_licensing.h | 1 + tests/juce/controller_tests.cpp | 80 ++++++ tests/visual/README.md | 22 ++ tests/visual/snapshot_main.cpp | 252 ++++++++++++++++++ 12 files changed, 580 insertions(+), 87 deletions(-) create mode 100644 modules/moonbase_licensing/juce/ActivationTheme.h diff --git a/README.md b/README.md index 0541008..e3f536e 100644 --- a/README.md +++ b/README.md @@ -430,7 +430,7 @@ available and unchanged. | | Native module | `OnlineUnlockStatus` bridge | | --- | --- | --- | | **Form** | Drop-in JUCE module | Copy-paste reference header | -| **Built-in UI** | Yes (polished, animated, themeable) | No (you build it) | +| **Built-in UI** | Yes (polished, animated, themeable: `config.palette` + `config.fonts`) | No (you build it) | | **JUCE integration** | Native Moonbase API | `juce::OnlineUnlockStatus` wrapper | | **JUCE version** | 8.0.4+ | 7+ | | **Device fingerprint** | Spec v2 (`mbd2_`), cross-SDK; scoped `mbd2s_` on mobile | Spec v2 (`mbd2_`), cross-SDK; scoped `mbd2s_` on mobile | diff --git a/docs/juce-module.md b/docs/juce-module.md index c831aa4..e35c79c 100644 --- a/docs/juce-module.md +++ b/docs/juce-module.md @@ -159,9 +159,38 @@ gating decisions (read it on the message thread). Everything in `ActivationConfig` after the connection fields is brand/UI: product + manufacturer name, `accent` colour, the Moonbase co-brand badge (`showMoonbaseBadge`), the `trialLengthDays` + `trialFeatures` list (shown on the Trial / Expired screens), -`enableOffline`, and the `activationUrl`. For deeper re-skinning, mutate `ActivationLookAndFeel::palette` -(every colour is a token) or bundle real Inter / Space Mono typefaces and point the -`heading` / `body` / `mono` font helpers at them. +`enableOffline`, and the `activationUrl`. + +For a deeper re-skin, `config.palette` holds every other colour in the UI as its own +token, and `config.fonts` takes your typefaces for the three font roles: + +```cpp +config.accent = juce::Colour(0xffe4a03c); + +config.palette.backgroundTop = juce::Colour(0xff2a1c12); // see ActivationTheme.h +config.palette.panelTop = juce::Colour(0xff1e1610); // for the full token list +config.palette.textPrimary = juce::Colour(0xfff7ecdc); +config.palette.onAccent = juce::Colour(0xff2a1a08); // text drawn on the accent + +// Bundle faces with juce_add_binary_data and hand them over per role. Do this +// rather than setting a default LookAndFeel: inside a DAW that one is shared +// with the host and every other plugin in the process. +config.fonts.heading = juce::Typeface::createSystemTypefaceFor( + BinaryData::InterBold_ttf, BinaryData::InterBold_ttfSize); +config.fonts.body = juce::Typeface::createSystemTypefaceFor( + BinaryData::InterRegular_ttf, BinaryData::InterRegular_ttfSize); +config.fonts.mono = juce::Typeface::createSystemTypefaceFor( + BinaryData::SpaceMono_ttf, BinaryData::SpaceMono_ttfSize); +``` + +Set both before you construct the `ActivationComponent` (or pass the config to +`ActivationDialog::show`, which also frames its window in `palette.backgroundBottom`). +The component reads the theme once, when it builds its LookAndFeel and pre-renders its +icons, so a palette changed afterwards would only reach half the screen. + +Leave any token alone and it keeps the built-in design's value, so a single line is a +valid theme. `config.fonts.makeFont` is the escape hatch when one typeface per role is +not enough: set it and it decides every font the UI asks for, by role and height. ## Device identity diff --git a/modules/moonbase_licensing/README.md b/modules/moonbase_licensing/README.md index c45406c..5e73867 100644 --- a/modules/moonbase_licensing/README.md +++ b/modules/moonbase_licensing/README.md @@ -125,7 +125,9 @@ richer gating, and `onActivationChanged` fires whenever it changes. - **Branding** — everything in `ActivationConfig` after the connection fields is UI: names, accent colour, logo `Drawable`, overridable copy (`config.strings`), trial length + feature list, the removable Moonbase badge, and the activation URL. - Re-skin deeper via `ActivationLookAndFeel::palette`. + Re-skin deeper with `config.palette` (every other colour, one token per role) and + `config.fonts` (your typefaces for heading / body / mono). Both are read when the + component is built, so set them on the config rather than after the fact. - **Refresh entitlements** — `controller().refreshLicense()` re-validates online so a freshly purchased sub-product/upgrade loads without a restart (async, silent, with an optional completion callback). diff --git a/modules/moonbase_licensing/juce/ActivationConfig.h b/modules/moonbase_licensing/juce/ActivationConfig.h index 1921d36..025da92 100644 --- a/modules/moonbase_licensing/juce/ActivationConfig.h +++ b/modules/moonbase_licensing/juce/ActivationConfig.h @@ -2,7 +2,8 @@ // Everything needed to wire up + brand an activation flow. The connection // fields configure the Moonbase SDK; the branding fields drive the built-in UI -// (the Solstice design's product name, accent, trial copy, co-brand badge). +// (the Solstice design's product name, accent, palette, typefaces, trial copy, +// co-brand badge). #include #include @@ -20,6 +21,7 @@ #include #include +#include "ActivationTheme.h" #include "JuceMetadata.h" // Named by resolvedDeviceIdResolver(). Included here rather than relied on from // the module umbrella so this header stays usable on its own. @@ -85,6 +87,18 @@ struct ActivationConfig juce::String manufacturerName; // defaults to JucePlugin_Manufacturer; shown under the product name juce::Colour accent = juce::Colour(0xff186cdc); // Moonbase blue + // Every other colour in the UI, one token per role, and optional typefaces + // for its three font roles (heading / body / mono). Both default to the + // built-in design, so override only what you want to change: + // + // config.palette.backgroundTop = juce::Colour(0xff1a1512); + // config.fonts.body = juce::Typeface::createSystemTypefaceFor(...); + // + // See ActivationTheme.h for the full token list. Read once, when the + // ActivationComponent is constructed. + ActivationPalette palette; + ActivationFonts fonts; + // Where the customer exchanges their machine file for a license file during // offline activation. Defaults to "{endpoint}/activate" when left unset. juce::URL activationUrl; diff --git a/modules/moonbase_licensing/juce/ActivationTheme.h b/modules/moonbase_licensing/juce/ActivationTheme.h new file mode 100644 index 0000000..22d3a97 --- /dev/null +++ b/modules/moonbase_licensing/juce/ActivationTheme.h @@ -0,0 +1,105 @@ +#pragma once + +// Colour + typeface tokens for the built-in activation UI, from the "Solstice +// Activation" design. +// +// Set them on ActivationConfig (config.palette, config.fonts) before you build +// an ActivationComponent. The component reads the theme once, when it creates +// its LookAndFeel and pre-renders its icons, so a theme handed over afterwards +// would only reach half the screen. + +#include + +#include + +namespace moonbase::juce_integration { + +// Every colour in the UI except the accent, which stays its own +// ActivationConfig field because it is the one-line way to brand the flow. +// Override the tokens you care about and leave the rest at the design's values: +// +// config.palette.backgroundTop = juce::Colour(0xff1a1512); +// config.palette.textPrimary = juce::Colour(0xfff7f2ea); +struct ActivationPalette +{ + // Backdrop + plugin window. + juce::Colour backgroundTop { 0xff0e1626 }; + juce::Colour backgroundMid { 0xff070a11 }; + juce::Colour backgroundBottom { 0xff04060b }; + juce::Colour panelTop { 0xff0d121c }; + juce::Colour panelMid { 0xff080b13 }; + juce::Colour panelBottom { 0xff06090f }; + juce::Colour panelBorder { 0x14ffffff }; + juce::Colour hairline { 0x1affffff }; + juce::Colour panelShadow { 0x73000000 }; // soft drop shadow under the panel + juce::Colour overlayDim { 0x94000000 }; // scrim when config.overlayBackdrop is set + + // Surfaces inside the panel. + juce::Colour cardFill { 0x06ffffff }; // license/seat/notes cards, drop zone, feature field + juce::Colour trackFill { 0x12ffffff }; // unfilled part of a progress bar + juce::Colour skeleton { 0x10ffffff }; // loading placeholder bars + juce::Colour scrollThumb { 0x80ffffff }; + juce::Colour scrollTrack { 0x1affffff }; + + // Text. + juce::Colour textPrimary { 0xfff5f8fb }; + juce::Colour textBody { 0xffcdd8e6 }; + juce::Colour textBright { 0xff9fb3cc }; + juce::Colour textSecondary { 0xff768aa4 }; + juce::Colour textMuted { 0xff5a6b82 }; + + // Controls. + juce::Colour ghostFill { 0x0affffff }; + juce::Colour ghostBorder { 0x21ffffff }; + juce::Colour ghostHover { 0x14ffffff }; + juce::Colour link { 0xff6aa8ff }; + juce::Colour onAccent { 0xffffffff }; // text + icons drawn on an accent fill + juce::Colour seatEmpty { 0x1affffff }; // seat pips this license has not used + + // Motion: the panel's breathing top-edge glow and the activation spinner. + juce::Colour glow { 0xff82cef1 }; + juce::Colour spinnerTrack { 0x26ffffff }; + + // Status. + juce::Colour success { 0xff34d27b }; + juce::Colour successFill { 0x2416a34a }; + juce::Colour successBorder { 0x5916a34a }; + juce::Colour trial { 0xffeab308 }; + juce::Colour trialBright { 0xfff5c542 }; // bright end of the trial progress gradient + juce::Colour onTrial { 0xff131519 }; // text drawn on top of a `trial` fill + juce::Colour error { 0xfff08a8a }; + juce::Colour errorStrong { 0xffdc5050 }; // expired bar + the pill washes derived from it + juce::Colour errorDeep { 0xffb9444c }; // dark end of the expired progress gradient + juce::Colour dangerFill { 0x14dc5050 }; + juce::Colour dangerBorder { 0x4cdc5050 }; +}; + +// Optional typefaces for the UI's three font roles. Leave a role null and it +// falls back to the platform default (sans, sans Bold, monospaced). +// +// Bundle your own with juce_add_binary_data and hand them over here rather than +// setting a process-wide default LookAndFeel: inside a DAW that default is +// shared with the host and every other plugin loaded in the same process. +// +// config.fonts.body = juce::Typeface::createSystemTypefaceFor( +// BinaryData::InterRegular_ttf, BinaryData::InterRegular_ttfSize); +struct ActivationFonts +{ + enum class Role + { + heading, // titles, buttons, pills. Supply a bold face: no style is applied on top. + body, // paragraphs, labels, links + mono // device id chip, license file names + }; + + juce::Typeface::Ptr heading; + juce::Typeface::Ptr body; + juce::Typeface::Ptr mono; + + // Last-word hook, for when a single typeface per role is not enough (a + // variable font, per-size style choices, your own juce::Font cache). When + // set it decides every font the UI asks for, and the fields above are unused. + std::function makeFont; +}; + +} // namespace moonbase::juce_integration diff --git a/modules/moonbase_licensing/juce/ui/ActivationComponent.cpp b/modules/moonbase_licensing/juce/ui/ActivationComponent.cpp index 635559e..fca1e15 100644 --- a/modules/moonbase_licensing/juce/ui/ActivationComponent.cpp +++ b/modules/moonbase_licensing/juce/ui/ActivationComponent.cpp @@ -203,7 +203,7 @@ class StyledButton : public juce::Button, if (down) col = col.darker(0.05f); g.setColour(col); g.fillRoundedRectangle(r, radius); - textColour = juce::Colours::white; + textColour = lnf.palette.onAccent; } else if (style == Style::danger) { @@ -405,7 +405,7 @@ class DropZone : public juce::Component, auto r = getLocalBounds().toFloat().reduced(1.0f); const bool has = fileName.isNotEmpty(); - g.setColour(dragOver ? lnf.palette.ghostHover : Colour(0x06ffffff)); + g.setColour(dragOver ? lnf.palette.ghostHover : lnf.palette.cardFill); g.fillRoundedRectangle(r, 10.0f); const auto borderCol = dragOver ? lnf.accent @@ -792,14 +792,14 @@ class BrowserWaitView : public ScreenView, auto ring = sb.reduced(thickness * 0.5f + 1.0f); juce::Path track; track.addEllipse(ring); - g.setColour(Colour(0x26ffffff)); + g.setColour(lnf.palette.spinnerTrack); g.strokePath(track, juce::PathStrokeType(thickness)); juce::Path arc; const float start = spinPhase * kTwoPi; arc.addCentredArc(ring.getCentreX(), ring.getCentreY(), ring.getWidth() * 0.5f, ring.getHeight() * 0.5f, 0.0f, start, start + kPi * 0.6f, true); - g.setColour(Colour(0xff82cef1)); + g.setColour(lnf.palette.glow); g.strokePath(arc, juce::PathStrokeType(thickness, juce::PathStrokeType::curved, juce::PathStrokeType::rounded)); } @@ -811,7 +811,7 @@ class BrowserWaitView : public ScreenView, const float tw = juce::GlyphArrangement::getStringWidth(chipFont, chipText); auto chip = Rectangle(0, 0, juce::jmin((float) getWidth(), tw + 52.0f), 32.0f) .withCentre({ (float) getWidth() * 0.5f, (float) row.getCentreY() }); - g.setColour(Colour(0x0affffff)); + g.setColour(lnf.palette.ghostFill); g.fillRoundedRectangle(chip, 16.0f); g.setColour(lnf.palette.ghostBorder); g.drawRoundedRectangle(chip, 16.0f, 1.0f); @@ -894,7 +894,7 @@ class SuccessView : public ScreenView // Mini card (anchored above the buttons). auto card = cardRow.withSizeKeepingCentre(juce::jmin(380, getWidth()), juce::jmin(86, cardRow.getHeight())); - g.setColour(Colour(0x06ffffff)); + g.setColour(lnf.palette.cardFill); g.fillRoundedRectangle(card.toFloat(), 12.0f); g.setColour(lnf.palette.panelBorder); g.drawRoundedRectangle(card.toFloat(), 12.0f, 1.0f); @@ -958,7 +958,7 @@ class OfflineView : public ScreenView saveMachine = std::make_unique( l, StyledButton::Style::ghost, u8("Save machine file\xe2\x80\xa6"), - icons::fromStroke(icons::fileDown, Colour(0xff82cef1), 1.6f)); + icons::fromStroke(icons::fileDown, l.palette.glow, 1.6f)); saveMachine->onClick = [this] { chooseMachineFileLocation(); }; addAndMakeVisible(*saveMachine); @@ -1073,7 +1073,7 @@ class OfflineView : public ScreenView auto badge = row.removeFromLeft(20).withSizeKeepingCentre(20, 20).toFloat(); g.setColour(lnf.accent); g.fillEllipse(badge); - g.setColour(juce::Colours::white); + g.setColour(lnf.palette.onAccent); g.setFont(lnf.heading(11.0f)); g.drawText(juce::String(number), badge, Justification::centred); row.removeFromLeft(10); @@ -1176,7 +1176,7 @@ class TrialView : public ScreenView TrialView(ActivationController& c, ActivationLookAndFeel& l) : ScreenView(c, l) { unlock = std::make_unique(l, StyledButton::Style::accent, "Unlock full version", - icons::fromStroke(icons::lock, juce::Colours::white, 1.8f)); + icons::fromStroke(icons::lock, l.palette.onAccent, 1.8f)); unlock->onClick = [this] { controller.beginOnlineActivation(); }; addAndMakeVisible(*unlock); @@ -1192,8 +1192,8 @@ class TrialView : public ScreenView // only shows when the list overflows (default autohide keeps it out of the // way for a list that fits, which renders as a plain checklist). auto& scrollbar = featuresViewport.getVerticalScrollBar(); - scrollbar.setColour(juce::ScrollBar::thumbColourId, Colour(0x80ffffff)); - scrollbar.setColour(juce::ScrollBar::trackColourId, Colour(0x1affffff)); + scrollbar.setColour(juce::ScrollBar::thumbColourId, l.palette.scrollThumb); + scrollbar.setColour(juce::ScrollBar::trackColourId, l.palette.scrollTrack); addAndMakeVisible(featuresViewport); } @@ -1221,7 +1221,7 @@ class TrialView : public ScreenView const auto pb = pill::rightAlignedIn(layout.header, headerRightInset, pw, 22.0f, layout.header.getCentreY()); pill::background(g, pb, lnf.palette.trial); - g.setColour(Colour(0xff131519)); + g.setColour(lnf.palette.onTrial); g.setFont(pf); g.drawText(pillText, pb, Justification::centred); @@ -1235,13 +1235,13 @@ class TrialView : public ScreenView layout.subtitle.getWidth(), layout.subtitle.getHeight(), Justification::topLeft, 4, 1.0f); const auto bar = layout.bar; - g.setColour(Colour(0x12ffffff)); + g.setColour(lnf.palette.trackFill); g.fillRoundedRectangle(bar.toFloat(), 3.0f); const int total = controller.config().trialLengthDays; const float frac = total > 0 ? juce::jlimit(0.0f, 1.0f, (float) days / (float) total) : 0.0f; auto fill = bar.toFloat().withWidth((float) bar.getWidth() * frac); g.setGradientFill(juce::ColourGradient(lnf.palette.trial, fill.getX(), 0, - Colour(0xfff5c542), fill.getRight(), 0, false)); + lnf.palette.trialBright, fill.getRight(), 0, false)); g.fillRoundedRectangle(fill, 3.0f); // When the list is longer than the band, frame it as a bounded, scrollable @@ -1251,7 +1251,7 @@ class TrialView : public ScreenView const auto fa = layout.features; if (featureList != nullptr && featureList->contentHeight() > fa.getHeight()) { - g.setColour(Colour(0x06ffffff)); + g.setColour(lnf.palette.cardFill); g.fillRoundedRectangle(fa.toFloat(), 10.0f); g.setColour(lnf.palette.panelBorder); g.drawRoundedRectangle(fa.toFloat().reduced(0.5f), 10.0f, 1.0f); @@ -1330,7 +1330,7 @@ class ExpiredView : public ScreenView ExpiredView(ActivationController& c, ActivationLookAndFeel& l) : ScreenView(c, l) { unlock = std::make_unique(l, StyledButton::Style::accent, "Unlock full version", - icons::fromStroke(icons::lock, juce::Colours::white, 1.8f)); + icons::fromStroke(icons::lock, l.palette.onAccent, 1.8f)); unlock->onClick = [this] { controller.beginOnlineActivation(); }; addAndMakeVisible(*unlock); @@ -1382,18 +1382,18 @@ class ExpiredView : public ScreenView // Full, red progress bar (the trial bar at 100%). r.removeFromTop(14); auto bar = r.removeFromTop(6); - g.setColour(Colour(0x12ffffff)); + g.setColour(lnf.palette.trackFill); g.fillRoundedRectangle(bar.toFloat(), 3.0f); - g.setGradientFill(juce::ColourGradient(Colour(0xffb9444c), (float) bar.getX(), 0.0f, - Colour(0xffdc5050), (float) bar.getRight(), 0.0f, false)); + g.setGradientFill(juce::ColourGradient(lnf.palette.errorDeep, (float) bar.getX(), 0.0f, + lnf.palette.errorStrong, (float) bar.getRight(), 0.0f, false)); g.fillRoundedRectangle(bar.toFloat(), 3.0f); // Red "audio is bypassed" callout. r.removeFromTop(22); auto callout = r.removeFromTop(juce::jmin(66, r.getHeight())); - g.setColour(Colour(0x12dc5050)); + g.setColour(lnf.palette.errorStrong.withAlpha(0x12 / 255.0f)); g.fillRoundedRectangle(callout.toFloat(), 10.0f); - g.setColour(Colour(0x38dc5050)); + g.setColour(lnf.palette.errorStrong.withAlpha(0x38 / 255.0f)); g.drawRoundedRectangle(callout.toFloat(), 10.0f, 1.0f); auto inner = callout.reduced(15, 12); auto iconArea = inner.removeFromLeft(18).withSizeKeepingCentre(18, 18).toFloat(); @@ -1447,7 +1447,8 @@ class ExpiredView : public ScreenView auto pf = lnf.heading(10.5f); const auto pb = pill::rightAlignedIn(slot, headerRightInset, pill::width(pf, label, pill::dotD), 22.0f, centreY); - pill::background(g, pb, Colour(0x1edc5050), Colour(0x52dc5050)); + pill::background(g, pb, lnf.palette.errorStrong.withAlpha(0x1e / 255.0f), + lnf.palette.errorStrong.withAlpha(0x52 / 255.0f)); pill::dot(g, pb, lnf.palette.error); g.setFont(pf); g.drawText(label, pill::labelArea(pb, pill::dotD), Justification::centredLeft); @@ -1550,7 +1551,7 @@ class DetailsView : public ScreenView // Info card fills the remaining middle, capped at its natural height. auto card = r.removeFromTop(juce::jmin(5 * 38, r.getHeight())); - g.setColour(Colour(0x06ffffff)); + g.setColour(lnf.palette.cardFill); g.fillRoundedRectangle(card.toFloat(), 12.0f); g.setColour(lnf.palette.panelBorder); g.drawRoundedRectangle(card.toFloat(), 12.0f, 1.0f); @@ -1626,7 +1627,7 @@ class DetailsView : public ScreenView void drawSeatBox(Graphics& g, Rectangle box) { - g.setColour(Colour(0x06ffffff)); + g.setColour(lnf.palette.cardFill); g.fillRoundedRectangle(box.toFloat(), 12.0f); g.setColour(lnf.palette.panelBorder); g.drawRoundedRectangle(box.toFloat(), 12.0f, 1.0f); @@ -1663,14 +1664,14 @@ class DetailsView : public ScreenView float x = r.getX(); for (int i = 0; i < total; ++i) { - g.setColour(i < used ? lnf.accent : Colour(0x1affffff)); + g.setColour(i < used ? lnf.accent : lnf.palette.seatEmpty); g.fillRoundedRectangle(x, r.getY(), segW, r.getHeight(), r.getHeight() * 0.5f); x += segW + gap; } } else { - g.setColour(Colour(0x1affffff)); + g.setColour(lnf.palette.seatEmpty); g.fillRoundedRectangle(r, r.getHeight() * 0.5f); const float frac = juce::jlimit(0.0f, 1.0f, (float) used / (float) total); g.setColour(lnf.accent); @@ -1822,7 +1823,7 @@ class UpdateNotesList : public juce::Component { auto bar = Rectangle(0, i * skeletonStep, (int) ((float) getWidth() * widths[i]), 11); - g.setColour(Colour(0x10ffffff)); + g.setColour(lnf.palette.skeleton); g.fillRoundedRectangle(bar.toFloat(), 5.5f); } return; @@ -1854,7 +1855,7 @@ class UpdateAvailableView : public ScreenView { download = std::make_unique(l, StyledButton::Style::accent, "Download", icons::fromStroke(icons::downloadTray, - juce::Colours::white, 1.8f)); + l.palette.onAccent, 1.8f)); download->onClick = [this] { if (controller.updateInfo().phase == Phase::Done) @@ -1867,7 +1868,7 @@ class UpdateAvailableView : public ScreenView // Shown instead of Download when this license can't fetch the installer // (e.g. a trial when the product restricts downloads to owners). unlock = std::make_unique(l, StyledButton::Style::accent, "Unlock full version", - icons::fromStroke(icons::lock, juce::Colours::white, 1.8f)); + icons::fromStroke(icons::lock, l.palette.onAccent, 1.8f)); unlock->onClick = [this] { controller.beginOnlineActivation(); }; addChildComponent(*unlock); @@ -1890,8 +1891,8 @@ class UpdateAvailableView : public ScreenView notesViewport.setScrollBarsShown(true, false); notesViewport.setScrollBarThickness(11); auto& scrollbar = notesViewport.getVerticalScrollBar(); - scrollbar.setColour(juce::ScrollBar::thumbColourId, Colour(0x80ffffff)); - scrollbar.setColour(juce::ScrollBar::trackColourId, Colour(0x1affffff)); + scrollbar.setColour(juce::ScrollBar::thumbColourId, l.palette.scrollThumb); + scrollbar.setColour(juce::ScrollBar::trackColourId, l.palette.scrollTrack); addAndMakeVisible(notesViewport); } @@ -1951,7 +1952,7 @@ class UpdateAvailableView : public ScreenView g.drawFittedText(headingText, l.heading, Justification::topLeft, 1); // "What's new" card frame + label (the notes viewport sits inside it). - g.setColour(Colour(0x06ffffff)); + g.setColour(lnf.palette.cardFill); g.fillRoundedRectangle(l.card.toFloat(), 12.0f); g.setColour(lnf.palette.panelBorder); g.drawRoundedRectangle(l.card.toFloat().reduced(0.5f), 12.0f, 1.0f); @@ -2049,7 +2050,7 @@ class UpdateAvailableView : public ScreenView auto pct = row.removeFromRight(44); row.removeFromRight(10); auto bar = row.withSizeKeepingCentre(row.getWidth(), 6); - g.setColour(Colour(0x12ffffff)); + g.setColour(lnf.palette.trackFill); g.fillRoundedRectangle(bar.toFloat(), 3.0f); auto fill = bar.toFloat().withWidth((float) bar.getWidth() * (float) juce::jlimit(0.0, 1.0, info.progress)); @@ -2098,7 +2099,8 @@ struct ActivationComponent::Impl : public juce::ChangeListener, : owner(o), ownedController(std::make_unique(std::move(cfg))), controller(*ownedController), - lnf(controller.config().accent) + lnf(controller.config().accent, controller.config().palette, + controller.config().fonts) { init(/*ownsController=*/true); } @@ -2108,7 +2110,8 @@ struct ActivationComponent::Impl : public juce::ChangeListener, : owner(o), ownedController(nullptr), controller(existing), - lnf(controller.config().accent) + lnf(controller.config().accent, controller.config().palette, + controller.config().fonts) { init(/*ownsController=*/false); } @@ -2532,7 +2535,7 @@ struct ActivationComponent::Impl : public juce::ChangeListener, { // Modal over a host (e.g. a plugin editor): dim what's behind so the // app shows through instead of an opaque takeover. - g.fillAll(juce::Colours::black.withAlpha(0.58f)); + g.fillAll(lnf.palette.overlayDim); } else { @@ -2553,7 +2556,7 @@ struct ActivationComponent::Impl : public juce::ChangeListener, g.addTransform(juce::AffineTransform::scale(s, s, panel.getCentreX(), panel.getCentreY())); // soft outer shadow - g.setColour(juce::Colours::black.withAlpha(0.45f)); + g.setColour(lnf.palette.panelShadow); g.fillRoundedRectangle(panel.translated(0, 14).expanded(2.0f), 18.0f); juce::ColourGradient pg(lnf.palette.panelTop, panel.getX(), panel.getY(), @@ -2570,7 +2573,7 @@ struct ActivationComponent::Impl : public juce::ChangeListener, panel.getWidth() * 0.64f, 2.0f); juce::ColourGradient gg(lnf.accent.withAlpha(0.0f), glowLine.getX(), 0, lnf.accent.withAlpha(0.0f), glowLine.getRight(), 0, false); - gg.addColour(0.5, Colour(0xff82cef1).withAlpha(breathe)); + gg.addColour(0.5, lnf.palette.glow.withAlpha(breathe)); g.setGradientFill(gg); g.fillRoundedRectangle(glowLine, 1.0f); // The "secured by moonbase" footer is the MoonbaseBadge child component. diff --git a/modules/moonbase_licensing/juce/ui/ActivationDialog.cpp b/modules/moonbase_licensing/juce/ui/ActivationDialog.cpp index 1b8e42d..f1be076 100644 --- a/modules/moonbase_licensing/juce/ui/ActivationDialog.cpp +++ b/modules/moonbase_licensing/juce/ui/ActivationDialog.cpp @@ -8,8 +8,10 @@ class ActivationDialogWindow : public juce::DocumentWindow { public: ActivationDialogWindow(ActivationConfig config, std::function onClosedIn) + // Read the theme before the config is moved into the component below, so + // a re-skinned flow does not sit in a stock near-black window frame. : juce::DocumentWindow("Activate " + config.resolvedProductName(), - juce::Colour(0xff04060b), juce::DocumentWindow::closeButton), + config.palette.backgroundBottom, juce::DocumentWindow::closeButton), onClosed(std::move(onClosedIn)) { auto* comp = new ActivationComponent(std::move(config)); diff --git a/modules/moonbase_licensing/juce/ui/ActivationLookAndFeel.h b/modules/moonbase_licensing/juce/ui/ActivationLookAndFeel.h index 9322fd2..44c1da0 100644 --- a/modules/moonbase_licensing/juce/ui/ActivationLookAndFeel.h +++ b/modules/moonbase_licensing/juce/ui/ActivationLookAndFeel.h @@ -1,77 +1,60 @@ #pragma once // Palette, fonts and icon helpers for the built-in activation UI, ported from -// the "Solstice Activation" design. The accent colour comes from -// ActivationConfig; everything else is a themeable token here. Subclass or -// mutate the palette to re-skin the whole flow. +// the "Solstice Activation" design. The accent colour, the palette and the +// typefaces all come from ActivationConfig; this class is the resolved view of +// them that the screens paint through. #include +#include #include #include -namespace moonbase::juce_integration { - -struct ActivationPalette -{ - // Backdrop + plugin window. - juce::Colour backgroundTop { 0xff0e1626 }; - juce::Colour backgroundMid { 0xff070a11 }; - juce::Colour backgroundBottom { 0xff04060b }; - juce::Colour panelTop { 0xff0d121c }; - juce::Colour panelMid { 0xff080b13 }; - juce::Colour panelBottom { 0xff06090f }; - juce::Colour panelBorder { 0x14ffffff }; - juce::Colour hairline { 0x1affffff }; - - // Text. - juce::Colour textPrimary { 0xfff5f8fb }; - juce::Colour textBody { 0xffcdd8e6 }; - juce::Colour textBright { 0xff9fb3cc }; - juce::Colour textSecondary { 0xff768aa4 }; - juce::Colour textMuted { 0xff5a6b82 }; +#include "../ActivationTheme.h" - // Controls. - juce::Colour ghostFill { 0x0affffff }; - juce::Colour ghostBorder { 0x21ffffff }; - juce::Colour ghostHover { 0x14ffffff }; - juce::Colour link { 0xff6aa8ff }; - - // Status. - juce::Colour success { 0xff34d27b }; - juce::Colour successFill { 0x2416a34a }; - juce::Colour successBorder { 0x5916a34a }; - juce::Colour trial { 0xffeab308 }; - juce::Colour error { 0xfff08a8a }; - juce::Colour dangerFill { 0x14dc5050 }; - juce::Colour dangerBorder { 0x4cdc5050 }; -}; +namespace moonbase::juce_integration { class ActivationLookAndFeel : public juce::LookAndFeel_V4 { public: - explicit ActivationLookAndFeel(juce::Colour accentColour = juce::Colour(0xff186cdc)) - : accent(accentColour) + explicit ActivationLookAndFeel(juce::Colour accentColour = juce::Colour(0xff186cdc), + ActivationPalette paletteIn = {}, + ActivationFonts fontsIn = {}) + : accent(accentColour), palette(std::move(paletteIn)), fonts(std::move(fontsIn)) { setColour(juce::ResizableWindow::backgroundColourId, palette.backgroundBottom); } juce::Colour accent; ActivationPalette palette; + ActivationFonts fonts; // Fonts: Inter ~ default sans, Space Mono ~ default monospaced. Bundle real - // typefaces with juce_add_binary_data and swap these if you want exact - // fidelity. + // typefaces with juce_add_binary_data and set config.fonts for exact + // fidelity; each role falls back to the platform default when left unset. [[nodiscard]] juce::Font heading(float height) const { + if (fonts.makeFont) + return fonts.makeFont(ActivationFonts::Role::heading, height); + if (fonts.heading != nullptr) + return juce::Font(juce::FontOptions().withTypeface(fonts.heading).withHeight(height)); return juce::Font(juce::FontOptions().withHeight(height).withStyle("Bold")); } [[nodiscard]] juce::Font body(float height) const { + if (fonts.makeFont) + return fonts.makeFont(ActivationFonts::Role::body, height); + if (fonts.body != nullptr) + return juce::Font(juce::FontOptions().withTypeface(fonts.body).withHeight(height)); return juce::Font(juce::FontOptions().withHeight(height)); } [[nodiscard]] juce::Font mono(float height) const { + if (fonts.makeFont) + return fonts.makeFont(ActivationFonts::Role::mono, height); + if (fonts.mono != nullptr) + return juce::Font(juce::FontOptions().withTypeface(fonts.mono).withHeight(height)); return juce::Font(juce::FontOptions(juce::Font::getDefaultMonospacedFontName(), height, juce::Font::plain)); } diff --git a/modules/moonbase_licensing/moonbase_licensing.h b/modules/moonbase_licensing/moonbase_licensing.h index 83a0078..7124908 100644 --- a/modules/moonbase_licensing/moonbase_licensing.h +++ b/modules/moonbase_licensing/moonbase_licensing.h @@ -75,6 +75,7 @@ #include "juce/legacy_juce_device_id_resolver.h" #include "juce/JuceMetadata.h" #include "juce/LicenseGate.h" +#include "juce/ActivationTheme.h" #include "juce/ActivationConfig.h" #include "juce/ActivationController.h" #include "juce/ui/ActivationLookAndFeel.h" diff --git a/tests/juce/controller_tests.cpp b/tests/juce/controller_tests.cpp index 7485034..a4c8a21 100644 --- a/tests/juce/controller_tests.cpp +++ b/tests/juce/controller_tests.cpp @@ -16,6 +16,8 @@ #include #include +#include +#include #include "test_helpers.hpp" @@ -1234,6 +1236,84 @@ TEST_CASE("describeDevice reports provenance, and nothing for an opaque resolver } } +//============================================================================== +// Theming: ActivationConfig carries the palette + typefaces the UI paints +// through, so a re-skin is in place before the component builds its icons. + +TEST_CASE("the look and feel resolves the palette it is given") +{ + ActivationPalette custom; + custom.backgroundTop = juce::Colour(0xff1a1512); + custom.cardFill = juce::Colour(0x0affe8d0); + custom.onAccent = juce::Colour(0xff2b1d10); + + ActivationLookAndFeel lnf(juce::Colour(0xffe4a03c), custom); + + CHECK(lnf.accent == juce::Colour(0xffe4a03c)); + CHECK(lnf.palette.backgroundTop == custom.backgroundTop); + CHECK(lnf.palette.cardFill == custom.cardFill); + CHECK(lnf.palette.onAccent == custom.onAccent); + // Untouched tokens keep the design's defaults. + CHECK(lnf.palette.textPrimary == ActivationPalette{}.textPrimary); + // The window background tracks the palette, not the built-in near-black. + CHECK(lnf.findColour(juce::ResizableWindow::backgroundColourId) + == custom.backgroundBottom); +} + +TEST_CASE("a config with no theme keeps the built-in design") +{ + ActivationConfig config; + ActivationLookAndFeel lnf(config.accent, config.palette, config.fonts); + + CHECK(lnf.palette.backgroundTop == ActivationPalette{}.backgroundTop); + CHECK(lnf.heading(14.0f).getHeight() == doctest::Approx(14.0f)); + CHECK(lnf.heading(14.0f).isBold()); + CHECK(!lnf.body(14.0f).isBold()); +} + +TEST_CASE("fonts.makeFont takes over every font the UI asks for") +{ + using Role = ActivationFonts::Role; + + std::vector> asked; + ActivationFonts fonts; + fonts.makeFont = [&asked](Role role, float height) + { + asked.emplace_back(role, height); + return juce::Font(juce::FontOptions().withHeight(height).withStyle("Italic")); + }; + + ActivationLookAndFeel lnf(juce::Colour(0xff186cdc), {}, fonts); + + CHECK(lnf.heading(20.0f).isItalic()); + CHECK(lnf.body(13.0f).isItalic()); + CHECK(lnf.mono(11.0f).isItalic()); + + REQUIRE(asked.size() == 3); + CHECK(asked[0] == std::make_pair(Role::heading, 20.0f)); + CHECK(asked[1] == std::make_pair(Role::body, 13.0f)); + CHECK(asked[2] == std::make_pair(Role::mono, 11.0f)); +} + +TEST_CASE("a role typeface is used for that role only") +{ + // A tiny valid TTF is more machinery than this needs: a system typeface is + // enough to prove the plumbing, since the fallback path never sets one. + auto face = juce::Font(juce::FontOptions().withHeight(12.0f)).getTypefacePtr(); + if (face == nullptr) + return; // no resolvable system font on this box; the fallbacks are covered above + + ActivationFonts fonts; + fonts.mono = face; + + ActivationLookAndFeel lnf(juce::Colour(0xff186cdc), {}, fonts); + + CHECK(lnf.mono(12.5f).getTypefacePtr() == face); + CHECK(lnf.mono(12.5f).getHeight() == doctest::Approx(12.5f)); + // The other roles are untouched, so they still resolve at paint time. + CHECK(lnf.heading(12.5f).isBold()); +} + //============================================================================== int main(int argc, char** argv) { diff --git a/tests/visual/README.md b/tests/visual/README.md index 3486912..0c7ae03 100644 --- a/tests/visual/README.md +++ b/tests/visual/README.md @@ -42,9 +42,31 @@ PNG per state: | `12-update-downloading` | Update installer downloading (progress bar) | | `13-update-error` | Update details failed to load (error) | | `14-update-gated` | Update the license can't download (Unlock CTA) | +| `15-theme-ember-welcome` | Welcome, "Ember" theme (warm near-black, amber, monospaced) | +| `16-theme-ember-activating` | Browser activation, Ember (spinner arc + track) | +| `17-theme-ember-trial` | Trial, Ember (pill, progress gradient, feature list) | +| `18-theme-daylight-success` | Just activated, "Daylight" theme (light) | +| `19-theme-daylight-details` | License details, Daylight (cards, seat pips) | +| `20-theme-daylight-offline` | Offline flow, Daylight (drop zone, links) | +| `21-theme-forest-expired` | Trial ended, "Understory" theme (deep green, danger tokens) | +| `22-theme-forest-update` | Update ready, Understory (notes card + scrollbar) | Add a state by adding a `writeSnapshot(...)` call. +## Themes + +`15` onwards render the same UI through `config.palette` + `config.fonts`, the +module's re-skin seam. They are the regression net for the colour tokens: a +colour still hardcoded in `ActivationComponent.cpp` shows up here as stock +blue-grey chrome, a cyan glow or a white wash on a panel that has none. The +three themes (`emberTheme()`, `daylightTheme()`, `forestTheme()` in +`snapshot_main.cpp`) are deliberately unalike, and between them the eight screens +touch every token in `ActivationPalette`. + +Ember also exercises the typeface seam. It routes all three font roles through +`fonts.makeFont` to the platform's monospaced face rather than bundling a +typeface, so the render stays reproducible on any machine. + ## Run locally ```bash diff --git a/tests/visual/snapshot_main.cpp b/tests/visual/snapshot_main.cpp index 7036cdd..9a4a151 100644 --- a/tests/visual/snapshot_main.cpp +++ b/tests/visual/snapshot_main.cpp @@ -63,6 +63,195 @@ ERUn++6CVMPvZo67jVbTY+GCXYfW4gGVZQIDAQAB return config; } +//============================================================================== +// Themes. +// +// config.palette + config.fonts are the module's re-skin seam, so these render +// the same screens through deliberately unlike themes: a warm near-black with a +// monospaced face, a light one, and a deep green one. They are the regression +// net for the colour tokens. Anything still hardcoded in the UI shows up here as +// stock blue-grey chrome, a cyan glow or a white wash on a panel that has none. + +// Warm near-black, amber accent, everything set in the monospaced face. This is +// the theme from issue #23: a plugin whose own UI is warm and monospaced. +ActivationConfig emberTheme() +{ + auto config = demoConfig(); + config.productName = "Ember"; + config.manufacturerName = "Foundry Audio"; + config.accent = juce::Colour(0xffe4a03c); + + auto& p = config.palette; + p.backgroundTop = juce::Colour(0xff2a1c12); + p.backgroundMid = juce::Colour(0xff150d08); + p.backgroundBottom = juce::Colour(0xff0c0704); + p.panelTop = juce::Colour(0xff1e1610); + p.panelMid = juce::Colour(0xff150f0a); + p.panelBottom = juce::Colour(0xff100b07); + p.panelBorder = juce::Colour(0x18ffe9c9); + p.hairline = juce::Colour(0x1affe9c9); + p.panelShadow = juce::Colour(0x73140a02); + p.overlayDim = juce::Colour(0x94140a02); + + p.cardFill = juce::Colour(0x08ffe9c9); + p.trackFill = juce::Colour(0x14ffe9c9); + p.skeleton = juce::Colour(0x12ffe9c9); + p.scrollThumb = juce::Colour(0x80ffe9c9); + p.scrollTrack = juce::Colour(0x1affe9c9); + + p.textPrimary = juce::Colour(0xfff7ecdc); + p.textBody = juce::Colour(0xffe3d2ba); + p.textBright = juce::Colour(0xffc8ae8d); + p.textSecondary = juce::Colour(0xffa08a6e); + p.textMuted = juce::Colour(0xff7a6752); + + p.ghostFill = juce::Colour(0x0affe9c9); + p.ghostBorder = juce::Colour(0x24ffe9c9); + p.ghostHover = juce::Colour(0x16ffe9c9); + p.link = juce::Colour(0xffe4a03c); + p.seatEmpty = juce::Colour(0x1affe9c9); + p.onAccent = juce::Colour(0xff2a1a08); + + p.glow = juce::Colour(0xffffcf8a); + p.spinnerTrack = juce::Colour(0x26ffe9c9); + + p.success = juce::Colour(0xff8fc46a); + p.successFill = juce::Colour(0x248fc46a); + p.successBorder = juce::Colour(0x598fc46a); + p.trial = juce::Colour(0xffe4a03c); + p.trialBright = juce::Colour(0xfff7cf82); + p.onTrial = juce::Colour(0xff2a1a08); + p.error = juce::Colour(0xffe8967a); + p.errorStrong = juce::Colour(0xffd2603c); + p.errorDeep = juce::Colour(0xffa8482c); + p.dangerFill = juce::Colour(0x14d2603c); + p.dangerBorder = juce::Colour(0x4cd2603c); + + // The typeface seam. A real plugin points these at bundled faces via + // juce::Typeface::createSystemTypefaceFor; the snapshot uses the platform's + // monospaced font so the render stays reproducible on any machine. + config.fonts.makeFont = [](ActivationFonts::Role role, float height) + { + return juce::Font(juce::FontOptions(juce::Font::getDefaultMonospacedFontName(), height, + role == ActivationFonts::Role::heading ? juce::Font::bold + : juce::Font::plain)); + }; + return config; +} + +// A light theme: the inversion every dark-on-light token has to survive. +ActivationConfig daylightTheme() +{ + auto config = demoConfig(); + config.productName = "Daylight"; + config.manufacturerName = "Northward Studio"; + config.accent = juce::Colour(0xff1b5fd0); + + auto& p = config.palette; + p.backgroundTop = juce::Colour(0xfff4f6fa); + p.backgroundMid = juce::Colour(0xffe9edf4); + p.backgroundBottom = juce::Colour(0xffdfe4ec); + p.panelTop = juce::Colour(0xffffffff); + p.panelMid = juce::Colour(0xfffbfcfe); + p.panelBottom = juce::Colour(0xfff4f7fb); + p.panelBorder = juce::Colour(0x1e000f28); + p.hairline = juce::Colour(0x14000f28); + p.panelShadow = juce::Colour(0x2a0a1730); + p.overlayDim = juce::Colour(0x5c0a1730); + + p.cardFill = juce::Colour(0x08000f28); + p.trackFill = juce::Colour(0x14000f28); + p.skeleton = juce::Colour(0x12000f28); + p.scrollThumb = juce::Colour(0x66000f28); + p.scrollTrack = juce::Colour(0x14000f28); + + p.textPrimary = juce::Colour(0xff11182a); + p.textBody = juce::Colour(0xff2c3648); + p.textBright = juce::Colour(0xff42506a); + p.textSecondary = juce::Colour(0xff5c6b86); + p.textMuted = juce::Colour(0xff8593a8); + + p.ghostFill = juce::Colour(0x08000f28); + p.ghostBorder = juce::Colour(0x24000f28); + p.ghostHover = juce::Colour(0x12000f28); + p.link = juce::Colour(0xff1b5fd0); + p.seatEmpty = juce::Colour(0x1a000f28); + p.onAccent = juce::Colour(0xffffffff); + + p.glow = juce::Colour(0xff5aa0f0); + p.spinnerTrack = juce::Colour(0x1e000f28); + + p.success = juce::Colour(0xff127a45); + p.successFill = juce::Colour(0x1e127a45); + p.successBorder = juce::Colour(0x59127a45); + p.trial = juce::Colour(0xffb07908); + p.trialBright = juce::Colour(0xffd9a33a); + p.onTrial = juce::Colour(0xfffdf6e6); + p.error = juce::Colour(0xffb03030); + p.errorStrong = juce::Colour(0xffc23a3a); + p.errorDeep = juce::Colour(0xff8f2626); + p.dangerFill = juce::Colour(0x14c23a3a); + p.dangerBorder = juce::Colour(0x4cc23a3a); + return config; +} + +// Deep green with a lime accent: a hue nowhere near the built-in blue, so a +// leftover accent-adjacent literal stands out immediately. +ActivationConfig forestTheme() +{ + auto config = demoConfig(); + config.productName = "Understory"; + config.manufacturerName = "Fernwood Audio"; + config.accent = juce::Colour(0xff5ed17c); + + auto& p = config.palette; + p.backgroundTop = juce::Colour(0xff0e2018); + p.backgroundMid = juce::Colour(0xff08130e); + p.backgroundBottom = juce::Colour(0xff050c08); + p.panelTop = juce::Colour(0xff0c1a13); + p.panelMid = juce::Colour(0xff08120d); + p.panelBottom = juce::Colour(0xff060e0a); + p.panelBorder = juce::Colour(0x18d8ffe8); + p.hairline = juce::Colour(0x1ad8ffe8); + p.panelShadow = juce::Colour(0x73010703); + p.overlayDim = juce::Colour(0x94010703); + + p.cardFill = juce::Colour(0x08d8ffe8); + p.trackFill = juce::Colour(0x14d8ffe8); + p.skeleton = juce::Colour(0x12d8ffe8); + p.scrollThumb = juce::Colour(0x80d8ffe8); + p.scrollTrack = juce::Colour(0x1ad8ffe8); + + p.textPrimary = juce::Colour(0xffeafff2); + p.textBody = juce::Colour(0xffc4dfd0); + p.textBright = juce::Colour(0xff9dc4ae); + p.textSecondary = juce::Colour(0xff76a189); + p.textMuted = juce::Colour(0xff58806a); + + p.ghostFill = juce::Colour(0x0ad8ffe8); + p.ghostBorder = juce::Colour(0x24d8ffe8); + p.ghostHover = juce::Colour(0x16d8ffe8); + p.link = juce::Colour(0xff7ee08a); + p.seatEmpty = juce::Colour(0x1ad8ffe8); + p.onAccent = juce::Colour(0xff062012); + + p.glow = juce::Colour(0xffa8f5b8); + p.spinnerTrack = juce::Colour(0x26d8ffe8); + + p.success = juce::Colour(0xff5ed17c); + p.successFill = juce::Colour(0x245ed17c); + p.successBorder = juce::Colour(0x595ed17c); + p.trial = juce::Colour(0xffd9c04a); + p.trialBright = juce::Colour(0xfff2e08a); + p.onTrial = juce::Colour(0xff10190a); + p.error = juce::Colour(0xffe8a08a); + p.errorStrong = juce::Colour(0xffd85a4a); + p.errorDeep = juce::Colour(0xffa8402e); + p.dangerFill = juce::Colour(0x14d85a4a); + p.dangerBorder = juce::Colour(0x4cd85a4a); + return config; +} + // A fixed wall-clock anchor for every rendered date and trial countdown, so the // snapshots are identical run to run regardless of the real date. Paired with // ActivationController::setPreviewClock so trialDaysRemaining() measures against @@ -369,6 +558,69 @@ int main(int argc, char* argv[]) cfg); } + //== Themed renders ======================================================= + // The re-skin seam (config.palette + config.fonts), rendered through three + // unlike themes across the screens that between them touch every token: the + // spinner and glow, the trial pill and its progress gradient, cards and seat + // pips, the drop zone, the success card, the danger washes, and the update + // screen's skeleton + scrollbar. + { + writeSnapshot(outDir, "15-theme-ember-welcome", + [](ActivationController& c) { c.setPreviewState(Screen::Welcome); }, + emberTheme()); + + writeSnapshot(outDir, "16-theme-ember-activating", + [](ActivationController& c) { c.setPreviewState(Screen::BrowserWait); }, + emberTheme()); + + writeSnapshot(outDir, "17-theme-ember-trial", + [](ActivationController& c) + { c.setPreviewState(Screen::Trial, makeLicense(true)); }, + emberTheme()); + + writeSnapshot(outDir, "18-theme-daylight-success", + [](ActivationController& c) + { c.setPreviewState(Screen::Success, makeLicense(false)); }, + daylightTheme()); + + writeSnapshot(outDir, "19-theme-daylight-details", + [](ActivationController& c) + { c.setPreviewState(Screen::Details, makeLicense(false)); }, + daylightTheme()); + + writeSnapshot(outDir, "20-theme-daylight-offline", + [](ActivationController& c) { c.setPreviewState(Screen::Offline); }, + daylightTheme()); + + writeSnapshot(outDir, "21-theme-forest-expired", + [](ActivationController& c) + { c.setPreviewState(Screen::Expired, makeExpiredTrial()); }, + forestTheme()); + + auto updateCfg = forestTheme(); + updateCfg.applicationVersion = "2.3.1"; // older than the license's released version + // Long enough to overflow the card, so the themed scrollbar renders too. + const juce::String themedNotes = + "Version 2.4.0\n" + "\n" + "New oversampling modes up to 16x for cleaner high-gain tones.\n" + "8 new factory presets from the Laniakea sound pack.\n" + "Reworked the modulation matrix with per-slot depth control.\n" + "Fixes Apple Silicon AU validation under Logic 11.\n" + "Fixes a rare crash when loading presets saved in the 1.x series.\n" + "Lower CPU usage in the analyzer view.\n" + "Retina-correct metering on mixed-DPI multi-monitor setups.\n" + "New A/B compare with copy-from-B and snapshot slots.\n" + "MIDI learn now supports relative encoders.\n" + "Improved oversampling latency reporting to the host.\n" + "Various accessibility and keyboard-navigation improvements."; + + writeSnapshot(outDir, "22-theme-forest-update", + [themedNotes](ActivationController& c) + { c.setPreviewUpdate(UpdatePhase::Ready, makeUpdateLicense(), themedNotes); }, + updateCfg); + } + juce::Logger::outputDebugString("UI snapshots written to " + outDir.getFullPathName()); return 0; }