validateTheme function

List<GuardrailViolation> validateTheme(
  1. ThemeSpec spec, {
  2. GuardrailLimits? limits,
})

Every guardrail spec fails (empty = ships).

Implementation

List<GuardrailViolation> validateTheme(
  ThemeSpec spec, {
  GuardrailLimits? limits,
}) {
  final lim = limits ?? GuardrailLimits.forSpec(spec);
  final p = spec.palette;
  final out = <GuardrailViolation>[];
  final bgs = backgroundSamples(spec.background);
  final backdrops = displayBackdrops(spec);

  void ratio(GuardrailRule rule, double measured, double required) {
    if (measured < required) {
      out.add(GuardrailViolation(rule, measured, required));
    }
  }

  void label(GuardrailRule rule, KeyColors k) => ratio(
    rule,
    _minContrast(k.label, faceSamples(k.face, bgs)),
    lim.labelRatio,
  );

  label(GuardrailRule.digitLabel, p.digit);
  label(GuardrailRule.operatorLabel, p.operator);
  label(GuardrailRule.equalsLabel, p.equals);
  label(GuardrailRule.functionLabel, p.function);
  ratio(
    GuardrailRule.displayText,
    _minContrast(p.displayText, backdrops),
    lim.displayRatio,
  );
  ratio(
    GuardrailRule.errorText,
    _minContrast(p.error, backdrops),
    lim.displayRatio,
  );
  ratio(
    GuardrailRule.previewText,
    _minContrast(p.previewText, backdrops),
    lim.previewRatio,
  );
  ratio(GuardrailRule.caret, _minContrast(p.caret, backdrops), lim.caretRatio);
  ratio(
    GuardrailRule.focusRing,
    _minContrast(p.focusRing, bgs),
    lim.focusRingRatio,
  );

  final digit = faceSamples(p.digit.face, bgs);
  final op = faceSamples(p.operator.face, bgs);
  final eq = faceSamples(p.equals.face, bgs);
  if (spec.nonColourOperatorCue == OperatorCue.none) {
    void distinct(GuardrailRule rule, List<int> face) {
      final cr = _minFaceContrast(face, digit);
      if (cr < lim.faceContrast) {
        out.add(GuardrailViolation(rule, cr, lim.faceContrast, 'contrast'));
        return;
      }
      final de = _minCvd(face, digit);
      if (de < lim.cvdDeltaE) {
        out.add(GuardrailViolation(rule, de, lim.cvdDeltaE, 'cvd'));
      }
    }

    distinct(GuardrailRule.operatorVsDigit, op);
    distinct(GuardrailRule.equalsVsDigit, eq);
  }
  final eqOp = _minCvd(eq, op);
  if (eqOp < lim.equalsVsOperatorDeltaE) {
    out.add(
      GuardrailViolation(
        GuardrailRule.equalsVsOperator,
        eqOp,
        lim.equalsVsOperatorDeltaE,
      ),
    );
  }
  final ghost = p.lcdGhost;
  if (ghost != null) {
    var worst = 0.0;
    for (final b in backdrops) {
      final r = contrastRatio(compositeOver(ghost, b), b);
      if (r > worst) worst = r;
    }
    if (worst > lcdGhostMaxRatio) {
      out.add(
        GuardrailViolation(
          GuardrailRule.lcdGhost,
          worst,
          lcdGhostMaxRatio,
          'maximum',
        ),
      );
    }
  }
  if (spec.keyStyle.kind == KeyStyleKind.soft) {
    _validateSoft(spec, lim, bgs, out);
  }
  // Scoped by family (not by `KeyStyleKind.outlined`): Paper & ink draws
  // outlined keys too, without the hard shadow. Neo-brutalism is the only
  // family whose specs are outlined at engine v1–v3, so no output changes.
  if (spec.family == ThemeFamily.neoBrutalism) {
    _validateBrutal(spec, bgs, out);
  }
  if (spec.family == ThemeFamily.liquidGlass) {
    out.addAll(validateGlass(spec, lim, bgs));
  }
  out.addAll(switch (spec.family) {
    ThemeFamily.claymorphism => validateClaymorphism(spec, lim, bgs),
    ThemeFamily.y2kChrome => validateY2kAero(spec, lim, bgs),
    ThemeFamily.paperInk => validatePaperInk(spec, lim, bgs),
    ThemeFamily.cyberpunk => validateCyberpunk(spec, lim, bgs),
    ThemeFamily.vaporwave => validateVaporwave(spec, lim, bgs),
    ThemeFamily.bauhaus => validateBauhaus(spec, lim, bgs),
    _ => const <GuardrailViolation>[],
  });
  return out;
}