buildCandidate method

ThemeSpec buildCandidate(
  1. int seed,
  2. ThemeFamily family,
  3. int engineVersion, {
  4. int attempt = 0,
  5. bool accessible = false,
  6. int? seedHueTenths,
  7. ThemeIntent? intent,
  8. bool intentCentred = false,
})

One unvalidated attempt (tests and diagnostics; generate adds repair, retries and the hero fallback).

Implementation

ThemeSpec buildCandidate(
  int seed,
  ThemeFamily family,
  int engineVersion, {
  int attempt = 0,
  bool accessible = false,
  int? seedHueTenths,
  ThemeIntent? intent,
  bool intentCentred = false,
}) {
  final shipped =
      familyDefinitionAt(family, engineVersion) ??
      (throw ArgumentError.value(
        family,
        'family',
        'not shipped at engine version $engineVersion',
      ));
  final steering = _steering(
    intent,
    intentCentred,
    engineVersion,
    seedHueTenths,
  );
  if (steering != null && !steerableFamilies.contains(family)) {
    // Not steerable yet: the family's own theme, with the intent's name.
    final spec = buildCandidate(
      seed,
      family,
      engineVersion,
      attempt: attempt,
      accessible: accessible,
    );
    return spec.copyWith(
      variant: ThemeVariant.steered,
      intent: steering.intent.forSpec,
      intentCentred: steering.centred,
      name: steering.nameOr(() => spec.name),
    );
  }
  final def = steering == null ? shipped : steering.revise(shipped);
  // The six families of the 2026-10 round each build their own palette,
  // background scene, shape, style, type and motion from the same named
  // streams (their own files).
  if (family == ThemeFamily.claymorphism) {
    return buildClaymorphismCandidate(
      seed,
      def,
      engineVersion,
      attempt: attempt,
      accessible: accessible,
      steering: steering,
    );
  }
  if (family == ThemeFamily.y2kChrome) {
    return buildY2kAeroCandidate(
      seed,
      def,
      engineVersion,
      attempt: attempt,
      accessible: accessible,
    );
  }
  if (family == ThemeFamily.paperInk) {
    return buildPaperInkCandidate(
      seed,
      def,
      engineVersion,
      attempt: attempt,
      accessible: accessible,
    );
  }
  if (family == ThemeFamily.cyberpunk) {
    return buildCyberpunkCandidate(
      seed,
      def,
      engineVersion,
      attempt: attempt,
      accessible: accessible,
    );
  }
  if (family == ThemeFamily.vaporwave) {
    return buildVaporwaveCandidate(
      seed,
      def,
      engineVersion,
      attempt: attempt,
      accessible: accessible,
    );
  }
  if (family == ThemeFamily.bauhaus) {
    return buildBauhausCandidate(
      seed,
      def,
      engineVersion,
      attempt: attempt,
      accessible: accessible,
    );
  }
  if (family == ThemeFamily.neoBrutalism) {
    // Neo-brutalism builds its own palette (ink outlines and hard
    // shadow, saturated flat fills, outlined display card;
    // neo_brutalism.dart) from the same named streams.
    return buildNeoBrutalCandidate(
      seed,
      def,
      engineVersion,
      attempt: attempt,
      accessible: accessible,
    );
  }
  if (family == ThemeFamily.liquidGlass) {
    // Liquid Glass builds its own palette (vivid mesh, translucent glass
    // tints solved on their composites, display backing; liquid_glass.dart)
    // from the same named streams.
    return buildGlassCandidate(
      seed,
      def,
      engineVersion,
      attempt: attempt,
      accessible: accessible,
    );
  }
  if (family == ThemeFamily.retro) {
    // Retro builds its own palette (plastic body, keycaps, LCD / VFD
    // panel and segments; retro.dart) from the same named streams.
    return buildRetroCandidate(
      seed,
      def,
      engineVersion,
      attempt: attempt,
      accessible: accessible,
    );
  }
  final paletteTag = attempt == 0
      ? RngTag.palette
      : RngTag.sub(RngTag.palette, 'retry', attempt);
  if (def.keyStyle.kind == KeyStyleKind.soft) {
    // Neumorphism builds its own palette (surface-coloured keys, soft
    // shadow pair, solved rims); other families never reach this.
    return buildNeumorphicCandidate(
      seed,
      def,
      engineVersion,
      paletteTag: paletteTag,
      accessible: accessible,
    );
  }
  final p = Rng.stream(seed, paletteTag);
  final drawnBrightness = p.pickWeighted(def.brightnessWeights) == 0
      ? ThemeBrightness.dark
      : ThemeBrightness.light;
  final brightness = steering?.brightnessFor(def) ?? drawnBrightness;
  final dark = brightness == ThemeBrightness.dark;
  final ranges = def.ranges(brightness);
  final drawnScheme = pickWeightedValue(p, def.schemes);
  final scheme = steering?.schemeFor(def.schemes) ?? drawnScheme;
  final ir = steering == null
      ? null
      : Rng.stream(seed, intentTag(paletteTag));
  final steeredH0 = ir == null ? null : steering!.baseHue(ir);
  var hues = _Hues.draw(
    scheme,
    p,
    h0Override: seedHueTenths == null
        ? steeredH0
        : _wrapTenths(seedHueTenths),
  );
  if (ir != null) {
    hues = hues.withAccents(
      steering!.accentHue(ir, 0),
      steering.accentHue(ir, 1),
    );
  }
  final lim = accessible
      ? GuardrailLimits.accessible
      : GuardrailLimits.standard;
  final bile = def.bile;
  final faces = _sampleFaces(
    p,
    ranges,
    scheme,
    hues,
    bile,
    dark: dark,
    accessible: accessible,
  );

  // Labels.
  final policy = lim.labelPolicy;
  final labelChroma = p.nextIntIn(10, ranges.labelMaxChromaMilli) / 1000;
  KeyColors key(Lch face, double edgeShade) {
    final start = face.l < 0.58 ? 0.97 : 0.17;
    var pair = repairPair(face, (
      l: start,
      c: labelChroma,
      h: face.h,
    ), policy: policy);
    if (bile.recheckFinal) {
      // Label repair may have moved the face into the disliked band:
      // check the final colour and re-solve with the hue rotated out.
      final f = argbToOklch(pair.face);
      final hue = (f.h * 10).round() % 3600;
      final rotated = bile.apply(hue, f.l, f.c);
      if (rotated != hue) {
        final moved = (l: f.l, c: f.c, h: rotated / 10);
        pair = repairPair(moved, (
          l: start,
          c: labelChroma,
          h: moved.h,
        ), policy: policy);
      }
    }
    final f = pair.faceLch;
    final edgeL = f.l + edgeShade;
    return KeyColors(
      face: pair.face,
      label: pair.label,
      edge: oklchToArgb(
        edgeL < 0 ? 0 : (edgeL > 1 ? 1 : edgeL),
        f.c * 0.85,
        f.h,
      ),
    );
  }

  final extruded = def.keyStyle.kind == KeyStyleKind.extruded;
  final edgeShade = extruded
      ? -def.keyStyle.edgeShadePermille / 1000
      : (dark ? 0.08 : -0.08);
  final digit = key(faces.digit, edgeShade);
  final function = key(faces.function, edgeShade);
  final operator = key(faces.operator, edgeShade);
  final equals = key(faces.equals, edgeShade);

  final s = faces.surface;
  final surfaceArgb = oklchToArgb(s.l, s.c, s.h);
  final background = _background(
    Rng.stream(seed, RngTag.background),
    def,
    faces.background,
    hues,
    dark: dark,
  );
  final bgArgb = background.base;

  // Display texts against every backdrop.
  final draft = Palette(
    background: bgArgb,
    surface: surfaceArgb,
    displayText: 0,
    previewText: 0,
    error: 0,
    caret: 0,
    focusRing: 0,
    accent: 0,
    digit: digit,
    operator: operator,
    equals: equals,
    function: function,
    shadow: 0,
    highlight: 0,
    seedColor: operator.face,
  );
  final backdrops = [surfaceArgb, ...backgroundSamples(background)];
  int text(double l, double c, double hDeg, double target) =>
      solveOnAll(oklchToArgb(l, c, hDeg), backdrops, target) ??
      blackOrWhite(surfaceArgb);
  final h0 = hues.h0 / 10;
  final displayText = text(dark ? 0.97 : 0.17, 0.012, h0, lim.displayRatio);
  final previewTarget = lim.previewRatio < ratioText
      ? ratioText
      : lim.previewRatio;
  final previewText = text(dark ? 0.80 : 0.42, 0.03, h0, previewTarget);
  final error = text(dark ? 0.72 : 0.50, 0.16, 25, lim.displayRatio);
  final opH = hues.operator / 10;
  final accentL = dark ? 0.80 : 0.50;
  final accentC = _chroma(p, ranges.accentChroma, accentL, opH);
  final accent = text(accentL, accentC, opH, lim.caretRatio);

  final palette = draft.copyWith(
    displayText: displayText,
    previewText: previewText,
    error: error,
    caret: accent,
    focusRing: accent,
    accent: accent,
    shadow: extruded ? 0xB3000000 : (dark ? 0x52000000 : 0x29000000),
    highlight: dark ? 0x14FFFFFF : 0x66FFFFFF,
  );

  // Motion (shared with the extruded style's depth).
  final motion = def.motionRanges.vary(
    def.motion,
    Rng.stream(seed, RngTag.motion),
  );
  final (keyShape, cue) = _shape(Rng.stream(seed, RngTag.shape), def);
  final keyStyle = _style(Rng.stream(seed, RngTag.style), def, motion);
  final type = _type(Rng.stream(seed, RngTag.type), def);
  String generatedName() => generateThemeName(
    rng: Rng.stream(seed, RngTag.name),
    accentArgb: operator.face,
    family: family,
    dark: dark,
  );
  final name = steering?.nameOr(generatedName) ?? generatedName();

  return ThemeSpec(
    seed: seed,
    family: family,
    engineVersion: engineVersion,
    variant: seedHueTenths != null
        ? ThemeVariant.wallpaper
        : (steering != null ? ThemeVariant.steered : ThemeVariant.generated),
    seedHueTenths: seedHueTenths == null ? null : _wrapTenths(seedHueTenths),
    intent: steering?.intent.forSpec,
    intentCentred: steering?.centred ?? false,
    brightness: brightness,
    accessible: accessible,
    palette: palette,
    keyShape: keyShape,
    keyStyle: keyStyle,
    type: type,
    background: background,
    motion: motion,
    sound: def.sound,
    name: name,
    nonColourOperatorCue: cue,
  );
}