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; }