repairClaymorphismExtras function

Palette? repairClaymorphismExtras(
  1. Palette p,
  2. ThemeSpec spec,
  3. GuardrailLimits lim,
  4. List<int> bgs,
)

Family repairs after labels and display texts (called from repairTheme for ThemeFamily.claymorphism specs only): lifts light digits / darkens light function keys off the body, raises the highlight, re-solves the function label on its ring, recomputes every drop shadow from the final faces and re-solves the focus ring over them. Null when unrepairable.

Implementation

Palette? repairClaymorphismExtras(
  Palette p,
  ThemeSpec spec,
  GuardrailLimits lim,
  List<int> bgs,
) {
  final bg = spec.background;
  if (bg.colors.length != 9) return null;
  final base = bg.colors[0] | 0xFF000000;
  final baseL = _lightness(base);
  final dark = spec.isDark;
  final policy = lim.labelPolicy;
  final shade = spec.keyStyle.edgeShadePermille;
  var out = p;

  KeyColors? moved(KeyColors k, double dl) {
    final f = argbToOklch(k.face);
    final face = oklchToArgb(_clamp(f.l + dl, 0, 1), f.c, f.h);
    return repairKeyLabel(k.copyWith(face: face), policy, [face]);
  }

  // 1. Keys off the body (light themes; night ranges make it unreachable).
  if (dark) {
    for (final (_, g) in _groups) {
      if (_lightness(_keyOf(out, g).face) - baseL <
          clayNightMinKeyAboveBodyMilli / 1000) {
        return null;
      }
    }
  } else {
    final c = bg.colors;
    bool digitOk(int f) =>
        deltaEOKArgb(f, c[0]) >= 0.045 &&
        deltaEOKArgb(f, c[3]) >= 0.045 &&
        deltaEOKArgb(f, c[6]) >= 0.045 &&
        deltaEOKArgb(f, c[1]) >= 0.025;
    for (var i = 0; i < 4 && !digitOk(out.digit.face); i++) {
      if (_lightness(out.digit.face) + 0.015 >
          clayDigitMaxLMilli / 1000 + 1e-9) {
        break;
      }
      final k = moved(out.digit, 0.015);
      if (k == null) return null;
      out = out.copyWith(digit: k);
    }
    if (!digitOk(out.digit.face)) return null;
    bool fnOk(int f) =>
        deltaEOKArgb(f, base) >= 0.045 &&
        baseL - _lightness(f) >= 0.03 &&
        minCvdDeltaE(f, base) >= clayFunctionMinCvdDeltaE;
    for (var i = 0; i < 6 && !fnOk(out.function.face); i++) {
      final k = moved(out.function, -0.015);
      if (k == null) return null;
      out = out.copyWith(function: k);
    }
    if (!fnOk(out.function.face)) return null;
  }

  // 2. The highlight.
  bool lifted(int face, int highlight) =>
      _lightness(clayHighlightComposite(face, highlight)) - _lightness(face) >=
      clayMinHighlightLift;
  bool allLifted(Palette x) =>
      lifted(x.digit.face, x.highlight) &&
      lifted(x.operator.face, x.highlight) &&
      lifted(x.equals.face, x.highlight) &&
      lifted(x.function.face, x.highlight) &&
      lifted(x.surface, x.highlight);
  while (!allLifted(out)) {
    final a = (out.highlight >>> 24) & 0xFF;
    if (a >= clayHighlightAlpha.hi) break;
    final next = a + 8 > clayHighlightAlpha.hi ? clayHighlightAlpha.hi : a + 8;
    out = out.copyWith(highlight: withAlpha(0xFFFFFFFF, next));
  }
  if (!lifted(out.digit.face, out.highlight)) {
    final floor = _max(baseL + clayDigitAboveBodyMilli / 1000, 0.915);
    for (var i = 0; i < 6 && !lifted(out.digit.face, out.highlight); i++) {
      if (_lightness(out.digit.face) - 0.01 < floor - 1e-9) break;
      final k = moved(out.digit, -0.01);
      if (k == null) return null;
      out = out.copyWith(digit: k);
    }
  }
  if (!allLifted(out)) return null;

  // 3. The function label on its ring.
  final ring = clayLabelRingColor(out.function, shade, out.highlight);
  if (contrastRatio(out.function.label, ring) < lim.labelRatio) {
    final label = solveOnAll(out.function.label, [
      out.function.face,
      ring,
    ], lim.labelRatio);
    if (label == null) return null;
    out = out.copyWith(function: out.function.copyWith(label: label));
  }

  // 4. Drop shadows from the final faces, then the focus ring where it
  // crosses them.
  out = _withEdges(out, base, dark: dark);
  final ringOn = [...bgs, ...clayFocusShadowSamples(out, bg.colors)];
  final ringFix = solveOnAll(out.focusRing, ringOn, lim.focusRingRatio);
  if (ringFix == null) return null;
  return out.copyWith(focusRing: ringFix);
}