buildCandidate method

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

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,
}) {
  final def =
      familyDefinitionAt(family, engineVersion) ??
      (throw ArgumentError.value(
        family,
        'family',
        'not shipped at engine version $engineVersion',
      ));
  // 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,
    );
  }
  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 brightness = p.pickWeighted(def.brightnessWeights) == 0
      ? ThemeBrightness.dark
      : ThemeBrightness.light;
  final dark = brightness == ThemeBrightness.dark;
  final ranges = def.ranges(brightness);
  final scheme = pickWeightedValue(p, def.schemes);
  final hues = _Hues.draw(
    scheme,
    p,
    h0Override: seedHueTenths == null ? null : _wrapTenths(seedHueTenths),
  );
  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);
  final name = generateThemeName(
    rng: Rng.stream(seed, RngTag.name),
    accentArgb: operator.face,
    family: family,
    dark: dark,
  );

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