Cockatrice/cockatrice/src/interface/theme_icon_ink.h
Lukas Brübach daa1a73956 [Icons] Order the emphasis ladder by chroma, not contrast
Contrast cannot order the ladder. On a dark surface the highest-contrast
tone is the palest one, so a 7:1 primary came out near-white while the
secondary kept the family's color; the ladder read inverted, and for the
shipped semantic greens and reds the primary level was already satisfied
by the secondary, so the two levels resolved to the same hex.

Emphasis is chromatic intensity now. Quiet recedes toward the surface at
the 3:1 non-text floor, secondary keeps the family color at the 4.5:1
text floor, and primary is the most chromatic tone the same floor allows.
Every level stays legible on the window and the button surface, and the
primary is a strictly more saturated member of its family on both
schemes. The reformatted palettes move the two upper levels onto the
shared floor and leave the quiet level alone.
2026-11-08 15:42:51 +01:00

128 lines
5 KiB
C++

/**
* @file theme_icon_ink.h
* @ingroup Interface
*/
#ifndef THEME_ICON_INK_H
#define THEME_ICON_INK_H
#include "theme_icon.h"
#include <QColor>
/**
* @brief Turns a text color into the ink a glyph is painted with.
*
* A glyph is not a word. Body text is read in sentences, at a size where a 2px
* stroke would be unusually heavy; a glyph is a 16px mark of the same stroke,
* and antialiasing eats its edges, so the same color that reads as solid type
* reads as a thin gray line beside it. Painting icons with `WindowText` verbatim
* is what makes them recede.
*
* The rule here is deliberately small: give the glyph a little of the theme's
* accent chroma, then hold it to a contrast floor against the surface it sits
* on. The chroma is what makes it read as an icon rather than as leftover text;
* the floor keeps a theme with a weak text color from producing a glyph that is
* not legible. Neither step invents a color: both are derived from the palette
* the theme already ships.
*
* The rule only applies to the neutral label roles. A semantic tint
* (`AppColor::Positive`, `QPalette::Highlight`, `QPalette::Link`) is a hue the
* call site chose on purpose, so it is left alone.
*/
namespace ThemeIconInk
{
/**
* @brief WCAG relative luminance of an opaque color.
*/
double relativeLuminance(const QColor &color);
/**
* @brief WCAG contrast ratio between two opaque colors, from 1.0 to 21.0.
*/
double contrastRatio(const QColor &first, const QColor &second);
/**
* @brief Mixes two colors, @p amount = 0 returning @p color and 1 returning @p target.
*
* RGB interpolation, with no gamma or hue-space correction: the inputs are
* palette colors that already sit in sRGB, and the result has to be predictable
* enough for a theme author to reason about it.
*/
QColor blend(const QColor &color, const QColor &target, double amount);
/**
* @brief Pushes @p color away from @p background until it reaches @p minRatio.
*
* Moves @p color toward black on a light background and toward white on a dark
* one, so the hue survives the adjustment. Returns @p color unchanged when it
* already meets the floor, which is the common case for a shipped palette.
*
* @param color Color to adjust.
* @param background Surface the color is drawn on.
* @param minRatio Contrast floor, as a WCAG ratio.
* @return The adjusted color; the nearer pole when even that cannot reach it.
*/
QColor reachContrast(const QColor &color, const QColor &background, double minRatio);
/**
* @brief Accent share used by production callers of glyphInk().
*
* Exposed so the preview sheet and the tests name the value they pin instead of
* repeating the literal, and so the default argument below has exactly one
* source: the constant in theme_icon_ink.cpp.
*/
double defaultAccentMix();
/**
* @brief Icon ink for a neutral glyph, given the palette it is drawn from.
*
* @param text Neutral text color, normally `QPalette::WindowText`.
* @param surface Surface the glyph is drawn on, normally `QPalette::Window`.
* @param accent The theme accent, normally `QPalette::Highlight`.
* @param accentMix How much of the accent color to borrow. Defaults to
* defaultAccentMix(); previews and tests sweep it to compare
* strengths, production callers do not pass it.
* @return The ink to resolve `currentColor` to, as an RGB color.
*/
QColor glyphInk(const QColor &text, const QColor &surface, const QColor &accent, double accentMix = defaultAccentMix());
/**
* @brief Contrast floor a glyph that labels a control has to clear.
*
* 4.5:1, the Text-level WCAG ratio: an icon that is the only label for a button
* is carrying text, and the icon guidelines in `.opencode/docs/ui-assets.md`
* already ask for that when there is no visible label.
*/
double labelContrastFloor();
/**
* @brief Applies an emphasis level to an already-hued glyph color.
*
* Secondary keeps the color untouched, so it is the no-op the default needs.
* Primary is the most chromatic member of its family: it raises the saturation
* to the most that still clears the text floor. Contrast alone cannot order the
* ladder, because on a dark surface the highest-contrast tone is the palest one;
* a primary that reads as a washed-out version of the secondary is exactly the
* inversion this rule removes. Quiet
* blends the color toward the surface it sits on and stops at the non-text
* legibility floor, so a labelling glyph recedes without disappearing.
*
* @param color Hue-resolved glyph color.
* @param surface Surface the glyph is drawn on.
* @param emphasis How much attention the glyph claims.
* @return The emphasized color, as an RGB color.
*/
QColor forEmphasis(const QColor &color, const QColor &surface, IconEmphasis emphasis);
/** @brief Contrast floor the primary level reaches, so it stays legible. */
double primaryContrastFloor();
/** @brief How far a quiet glyph is blended into its surface. */
double quietBlend();
/** @brief Contrast floor a quiet glyph keeps, the WCAG non-text minimum. */
double quietContrastFloor();
} // namespace ThemeIconInk
#endif // THEME_ICON_INK_H