ZaStoGram_desktop/Telegram/SourceFiles/ui/chat/chat_theme_readability.cpp

700 lines
18 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ui/chat/chat_theme_readability.h"
#include "ui/style/style_core_palette.h"
#include "ui/color_contrast.h"
#include <QtCore/QtMath>
namespace Ui {
namespace {
constexpr auto kRoles = 6;
constexpr auto kStep = 0.005;
constexpr auto kSteps = 160;
constexpr auto kOutgoingLift = 0.08;
constexpr auto kMinChroma = 0.03;
constexpr auto kOutgoingChromaScale = 1.15;
constexpr auto kOutgoingChromaMin = 0.05;
constexpr auto kOutgoingChromaMax = 0.16;
constexpr auto kGamutIterations = 30;
constexpr auto kGamutEpsilon = 1e-4;
constexpr auto kOliveFrom = 95.;
constexpr auto kOliveSplit = 110.;
constexpr auto kOliveTo = 125.;
constexpr auto kOliveWarm = 75.;
constexpr auto kOliveGreen = 140.;
constexpr auto kOliveLightness = 0.66;
constexpr auto kOliveRange = 0.18;
constexpr auto kWhiteTextContrast = 4.5;
constexpr auto kAlphaStep = 8;
using Getter = const style::color &(style::palette_data::*)() const;
struct Lab {
float64 lightness = 0.;
float64 a = 0.;
float64 b = 0.;
};
struct Floors {
float64 bubbleText = 0.;
float64 text = 0.;
float64 separation = 0.;
std::array<float64, kRoles> selected = {};
std::array<float64, kRoles> unselected = {};
float64 names = 0.;
float64 namesUnselected = 0.;
};
struct Side {
Getter bg = nullptr;
Getter bgSelected = nullptr;
Getter text = nullptr;
Getter textSelected = nullptr;
std::array<Getter, kRoles> selected = {};
std::array<Getter, kRoles> unselected = {};
std::vector<Getter> bgDependents;
std::vector<Getter> dependents;
bool names = false;
};
using P = style::palette_data;
[[nodiscard]] const Side &Incoming() {
static const auto result = Side{
.bg = &P::msgInBg,
.bgSelected = &P::msgInBgSelected,
.text = &P::historyTextInFg,
.textSelected = &P::historyTextInFgSelected,
.selected = {
&P::historyLinkInFgSelected,
&P::msgInServiceFgSelected,
&P::msgInDateFgSelected,
&P::msgInMonoFgSelected,
&P::msgInReplyBarSelColor,
nullptr,
},
.unselected = {
&P::historyLinkInFg,
&P::msgInServiceFg,
&P::msgInDateFg,
&P::msgInMonoFg,
&P::msgInReplyBarColor,
nullptr,
},
.bgDependents = {
&P::historyFileInIconFg,
&P::historyFileThumbIconFg,
&P::historyFileInRadialFg,
},
.dependents = {
&P::historyFileInIconFgSelected,
&P::historyFileThumbIconFgSelected,
&P::historyFileInRadialFgSelected,
},
.names = true,
};
return result;
}
[[nodiscard]] const Side &Outgoing() {
static const auto result = Side{
.bg = &P::msgOutBg,
.bgSelected = &P::msgOutBgSelected,
.text = &P::historyTextOutFg,
.textSelected = &P::historyTextOutFgSelected,
.selected = {
&P::historyLinkOutFgSelected,
&P::msgOutServiceFgSelected,
&P::msgOutDateFgSelected,
&P::msgOutMonoFgSelected,
&P::msgOutReplyBarSelColor,
&P::historyOutIconFgSelected,
},
.unselected = {
&P::historyLinkOutFg,
&P::msgOutServiceFg,
&P::msgOutDateFg,
&P::msgOutMonoFg,
&P::msgOutReplyBarColor,
&P::historyOutIconFg,
},
.bgDependents = {
&P::historyFileOutIconFg,
&P::historyFileOutRadialFg,
},
.dependents = {
&P::historyFileOutIconFgSelected,
&P::historyFileOutRadialFgSelected,
},
};
return result;
}
[[nodiscard]] const std::array<Getter, 8> &PeerNames() {
static const auto result = std::array<Getter, 8>{
&P::historyPeer1NameFg,
&P::historyPeer2NameFg,
&P::historyPeer3NameFg,
&P::historyPeer4NameFg,
&P::historyPeer5NameFg,
&P::historyPeer6NameFg,
&P::historyPeer7NameFg,
&P::historyPeer8NameFg,
};
return result;
}
[[nodiscard]] const std::array<Getter, 8> &PeerNamesSelected() {
static const auto result = std::array<Getter, 8>{
&P::historyPeer1NameFgSelected,
&P::historyPeer2NameFgSelected,
&P::historyPeer3NameFgSelected,
&P::historyPeer4NameFgSelected,
&P::historyPeer5NameFgSelected,
&P::historyPeer6NameFgSelected,
&P::historyPeer7NameFgSelected,
&P::historyPeer8NameFgSelected,
};
return result;
}
[[nodiscard]] Floors GlobalFloors(bool dark) {
auto result = dark
? Floors{
.bubbleText = 4.5,
.text = 4.5,
.separation = 0.10,
.selected = { 3.0, 3.0, 3.0, 3.0, 2.5, 3.0 },
.names = 3.0,
}
: Floors{
.bubbleText = 4.5,
.text = 4.5,
.separation = 0.06,
.selected = { 2.5, 2.2, 1.7, 2.4, 2.0, 1.7 },
.names = 1.8,
};
result.unselected = result.selected;
result.namesUnselected = result.names;
return result;
}
[[nodiscard]] float64 ToLinear(int channel) {
const auto value = channel / 255.;
return (value <= 0.04045)
? (value / 12.92)
: std::pow((value + 0.055) / 1.055, 2.4);
}
[[nodiscard]] float64 FromLinear(float64 value) {
value = std::clamp(value, 0., 1.);
return (value <= 0.0031308)
? (12.92 * value)
: (1.055 * std::pow(value, 1. / 2.4) - 0.055);
}
[[nodiscard]] Lab ToLab(const QColor &color) {
const auto r = ToLinear(color.red());
const auto g = ToLinear(color.green());
const auto b = ToLinear(color.blue());
const auto l = std::cbrt(
0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
const auto m = std::cbrt(
0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
const auto s = std::cbrt(
0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
return {
.lightness = 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s,
.a = 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
.b = 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s,
};
}
[[nodiscard]] float64 Chroma(const Lab &lab) {
return std::hypot(lab.a, lab.b);
}
[[nodiscard]] float64 Hue(const Lab &lab) {
const auto result = std::fmod(
std::atan2(lab.b, lab.a) * 180. / M_PI,
360.);
return (result < 0.) ? (result + 360.) : result;
}
[[nodiscard]] std::array<float64, 3> LinearFromLch(
float64 lightness,
float64 chroma,
float64 hue) {
const auto radians = hue * M_PI / 180.;
const auto a = chroma * std::cos(radians);
const auto b = chroma * std::sin(radians);
const auto l = lightness + 0.3963377774 * a + 0.2158037573 * b;
const auto m = lightness - 0.1055613458 * a - 0.0638541728 * b;
const auto s = lightness - 0.0894841775 * a - 1.2914855480 * b;
const auto l3 = l * l * l;
const auto m3 = m * m * m;
const auto s3 = s * s * s;
return {
4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3,
-1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3,
-0.0041960863 * l3 - 0.7034186147 * m3 + 1.7076147010 * s3,
};
}
[[nodiscard]] bool InGamut(float64 lightness, float64 chroma, float64 hue) {
for (const auto value : LinearFromLch(lightness, chroma, hue)) {
if (value < -kGamutEpsilon || value > 1. + kGamutEpsilon) {
return false;
}
}
return true;
}
[[nodiscard]] QColor FromLch(float64 lightness, float64 chroma, float64 hue) {
if (!InGamut(lightness, chroma, hue)) {
auto low = 0.;
auto high = chroma;
for (auto i = 0; i != kGamutIterations; ++i) {
const auto middle = (low + high) / 2.;
if (InGamut(lightness, middle, hue)) {
low = middle;
} else {
high = middle;
}
}
chroma = low;
}
const auto rgb = LinearFromLch(lightness, chroma, hue);
const auto channel = [](float64 value) {
return int(std::round(FromLinear(value) * 255.));
};
return QColor(channel(rgb[0]), channel(rgb[1]), channel(rgb[2]));
}
// WHY: darkened yellows read as olive; amber or green keeps them clean.
[[nodiscard]] float64 NudgedHue(float64 hue, float64 lightness) {
if (hue < kOliveFrom || hue > kOliveTo || lightness >= kOliveLightness) {
return hue;
}
const auto progress = std::min(
(kOliveLightness - lightness) / kOliveRange,
1.);
const auto target = (hue < kOliveSplit) ? kOliveWarm : kOliveGreen;
return hue + (target - hue) * progress;
}
[[nodiscard]] QColor Make(
float64 lightness,
float64 chroma,
float64 hue,
bool nudge) {
return FromLch(
lightness,
chroma,
nudge ? NudgedHue(hue, lightness) : hue);
}
[[nodiscard]] QColor WithAlpha(QColor color, int alpha) {
color.setAlpha(alpha);
return color;
}
[[nodiscard]] QColor Composite(const QColor &over, const QColor &under) {
if (over.alpha() == 255) {
return over;
}
const auto alpha = over.alpha() / 255.;
const auto mix = [&](int a, int b) {
return int(std::round(a * alpha + b * (1. - alpha)));
};
return QColor(
mix(over.red(), under.red()),
mix(over.green(), under.green()),
mix(over.blue(), under.blue()));
}
[[nodiscard]] float64 ContrastOver(const QColor &over, const QColor &under) {
return CountContrast(Composite(over, under), under);
}
[[nodiscard]] float64 Luminance(const QColor &color) {
const auto map = [](float64 value) {
return (value <= 0.03928)
? (value / 12.92)
: std::pow((value + 0.055) / 1.055, 2.4);
};
return map(color.redF()) * 0.2126
+ map(color.greenF()) * 0.7152
+ map(color.blueF()) * 0.0722;
}
[[nodiscard]] float64 DeltaE(const QColor &a, const QColor &b) {
const auto first = ToLab(a);
const auto second = ToLab(b);
return std::sqrt(
std::pow(first.lightness - second.lightness, 2)
+ std::pow(first.a - second.a, 2)
+ std::pow(first.b - second.b, 2));
}
[[nodiscard]] std::optional<QColor> NearestFeasible(
float64 lightness,
float64 chroma,
float64 hue,
Fn<bool(const QColor&)> ok,
bool nudge) {
if (const auto color = Make(lightness, chroma, hue, nudge); ok(color)) {
return color;
}
for (auto step = 1; step <= kSteps; ++step) {
for (const auto sign : { 1, -1 }) {
const auto now = lightness + sign * step * kStep;
if (now < 0.05 || now > 0.98) {
continue;
}
const auto color = Make(now, chroma, hue, nudge);
if (ok(color)) {
return color;
}
}
}
return std::nullopt;
}
[[nodiscard]] QColor EnsureContrast(
const QColor &color,
const std::vector<QColor> &backgrounds,
float64 floor,
bool nudge) {
const auto ok = [&](const QColor &now) {
for (const auto &background : backgrounds) {
if (ContrastOver(now, background) < floor) {
return false;
}
}
return true;
};
if (ok(color)) {
return color;
}
const auto lab = ToLab(color);
const auto chroma = Chroma(lab);
const auto hue = Hue(lab);
const auto up = Luminance(Composite(color, backgrounds.front()))
>= Luminance(backgrounds.front());
const auto scan = [&](int direction) -> std::optional<QColor> {
for (auto step = 1; step <= kSteps; ++step) {
const auto lightness = lab.lightness + direction * step * kStep;
if (lightness < 0. || lightness > 1.) {
break;
}
const auto now = WithAlpha(
Make(lightness, chroma, hue, nudge),
color.alpha());
if (ok(now)) {
return now;
}
}
return std::nullopt;
};
const auto opacify = [&](int direction) -> std::optional<QColor> {
const auto edge = Make((direction > 0) ? 1. : 0., chroma, hue, nudge);
for (auto alpha = color.alpha(); alpha < 255;) {
alpha = std::min(alpha + kAlphaStep, 255);
if (const auto now = WithAlpha(edge, alpha); ok(now)) {
return now;
}
}
return std::nullopt;
};
for (const auto direction : { up ? 1 : -1, up ? -1 : 1 }) {
if (const auto found = scan(direction)) {
return *found;
} else if (const auto opaque = opacify(direction)) {
return *opaque;
}
}
return color;
}
[[nodiscard]] QColor ChooseSelectedBg(
const std::vector<QColor> &stops,
const QColor &selected,
const QColor &text,
bool dark,
const Floors &floors,
bool derive) {
const auto ok = [&](const QColor &color) {
if (ContrastOver(text, color) < floors.text) {
return false;
}
for (const auto &stop : stops) {
if (DeltaE(color, stop) < floors.separation) {
return false;
}
}
return true;
};
if (!derive && ok(selected)) {
return selected;
}
const auto lab = ToLab(selected);
auto lightness = lab.lightness;
auto chroma = Chroma(lab);
auto hue = Hue(lab);
if (derive) {
auto mean = Lab();
auto lightest = 0.;
for (const auto &stop : stops) {
const auto value = ToLab(stop);
mean.lightness += value.lightness;
mean.a += value.a;
mean.b += value.b;
lightest = std::max(lightest, value.lightness);
}
mean.lightness /= stops.size();
mean.a /= stops.size();
mean.b /= stops.size();
if (const auto value = Chroma(mean); value >= kMinChroma) {
chroma = std::clamp(
value * kOutgoingChromaScale,
kOutgoingChromaMin,
kOutgoingChromaMax);
hue = Hue(mean);
}
lightness = lightest + kOutgoingLift;
}
const auto found = NearestFeasible(lightness, chroma, hue, ok, dark);
return found ? WithAlpha(*found, selected.alpha()) : selected;
}
[[nodiscard]] std::vector<QColor> SideStops(
const style::palette &palette,
const Side &side,
const std::vector<QColor> &gradient) {
return (gradient.size() > 1)
? gradient
: std::vector<QColor>{ (palette.*side.bg)()->c };
}
[[nodiscard]] Floors Measure(
const style::palette &palette,
const Side &side,
const std::vector<QColor> &stops) {
const auto selected = (palette.*side.bgSelected)()->c;
const auto max = std::numeric_limits<float64>::max();
auto result = Floors{
.bubbleText = max,
.text = ContrastOver((palette.*side.textSelected)()->c, selected),
.separation = max,
};
for (const auto &stop : stops) {
result.bubbleText = std::min(
result.bubbleText,
ContrastOver((palette.*side.text)()->c, stop));
result.separation = std::min(
result.separation,
DeltaE(selected, stop));
}
for (auto i = 0; i != kRoles; ++i) {
if (!side.selected[i]) {
result.selected[i] = result.unselected[i] = max;
continue;
}
result.selected[i] = ContrastOver(
(palette.*side.selected[i])()->c,
selected);
result.unselected[i] = max;
for (const auto &stop : stops) {
result.unselected[i] = std::min(
result.unselected[i],
ContrastOver((palette.*side.unselected[i])()->c, stop));
}
}
if (side.names) {
result.names = result.namesUnselected = max;
for (auto i = 0; i != int(PeerNames().size()); ++i) {
const auto &name = (palette.*PeerNames()[i])();
const auto &nameSelected = (palette.*PeerNamesSelected()[i])();
result.names = std::min(
result.names,
ContrastOver(nameSelected->c, selected));
result.namesUnselected = std::min(
result.namesUnselected,
ContrastOver(name->c, stops.front()));
}
}
return result;
}
[[nodiscard]] Floors Lowest(Floors a, const Floors &b) {
a.bubbleText = std::min(a.bubbleText, b.bubbleText);
a.text = std::min(a.text, b.text);
a.separation = std::min(a.separation, b.separation);
for (auto i = 0; i != kRoles; ++i) {
a.selected[i] = std::min(a.selected[i], b.selected[i]);
a.unselected[i] = std::min(a.unselected[i], b.unselected[i]);
}
a.names = std::min(a.names, b.names);
a.namesUnselected = std::min(a.namesUnselected, b.namesUnselected);
return a;
}
void Set(const style::color &color, const QColor &value) {
color.set(
uchar(value.red()),
uchar(value.green()),
uchar(value.blue()),
uchar(value.alpha()));
}
void Replace(
const style::palette &palette,
const style::color &color,
const QColor &now,
const std::vector<Getter> &dependents) {
const auto was = color->c;
if (now == was) {
return;
}
Set(color, now);
for (const auto getter : dependents) {
if (const auto &other = (palette.*getter)(); other->c == was) {
Set(other, now);
}
}
}
void SetContrasted(
const style::color &color,
const std::vector<QColor> &backgrounds,
float64 floor) {
const auto now = EnsureContrast(color->c, backgrounds, floor, true);
if (now != color->c) {
Set(color, now);
}
}
void EnsureSideReadable(
const style::palette &palette,
const Side &side,
const BubblesReadabilityArgs &args,
bool outgoing) {
const auto gradient = outgoing
? args.outgoingGradient
: std::vector<QColor>();
const auto global = GlobalFloors(args.dark);
const auto floors = args.reference
? Lowest(global, Measure(
*args.reference,
side,
SideStops(*args.reference, side, {})))
: global;
if (gradient.size() < 2) {
const auto &bubble = (palette.*side.bg)();
const auto now = EnsureContrast(
bubble->c,
{ (palette.*side.text)()->c },
floors.bubbleText,
true);
Replace(palette, bubble, now, side.bgDependents);
}
const auto stops = SideStops(palette, side, gradient);
const auto &selected = (palette.*side.bgSelected)();
const auto now = ChooseSelectedBg(
stops,
selected->c,
(palette.*side.textSelected)()->c,
args.dark,
floors,
outgoing && args.deriveOutgoingSelection);
Replace(palette, selected, now, side.dependents);
for (auto i = 0; i != kRoles; ++i) {
if (side.selected[i]) {
SetContrasted(
(palette.*side.selected[i])(),
{ now },
floors.selected[i]);
}
}
for (auto i = 0; i != kRoles; ++i) {
if (side.unselected[i]) {
SetContrasted(
(palette.*side.unselected[i])(),
stops,
floors.unselected[i]);
}
}
if (side.names) {
for (auto i = 0; i != int(PeerNames().size()); ++i) {
SetContrasted(
(palette.*PeerNamesSelected()[i])(),
{ now },
floors.names);
SetContrasted(
(palette.*PeerNames()[i])(),
stops,
floors.namesUnselected);
}
}
}
} // namespace
void EnsureBubblesReadable(
const style::palette &palette,
const BubblesReadabilityArgs &args) {
EnsureSideReadable(palette, Incoming(), args, false);
EnsureSideReadable(palette, Outgoing(), args, true);
}
bool IsDarkPalette(const style::palette &palette) {
const auto background = palette.windowBg()->c;
return CountContrast(background, QColor(0, 0, 0))
< CountContrast(background, QColor(255, 255, 255));
}
QColor EnsurePeerNameReadable(
const QColor &color,
const QColor &background,
bool dark) {
return EnsureContrast(
color,
{ background },
GlobalFloors(dark).names,
true);
}
std::vector<QColor> DarkenUnderWhiteText(std::vector<QColor> colors) {
const auto white = QColor(255, 255, 255);
for (auto &color : colors) {
if (ContrastOver(white, color) >= kWhiteTextContrast) {
continue;
}
const auto lab = ToLab(color);
const auto chroma = Chroma(lab);
const auto hue = Hue(lab);
for (auto step = 1; step <= kSteps; ++step) {
const auto now = Make(
lab.lightness - step * kStep,
chroma,
hue,
true);
if (ContrastOver(white, now) >= kWhiteTextContrast) {
color = now;
break;
}
}
}
return colors;
}
} // namespace Ui