diff --git a/src/Utils.cpp b/src/Utils.cpp
index 6229d42a..6a5c3491 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -383,20 +383,120 @@ utils::linkColor()
}
QString
-utils::generateHexColor(const QString &input)
+utils::generateHexColor(const int hash)
+{
+ QString colour("#");
+ for (int i = 0; i < 3; i++) {
+ int value = (hash >> (i * 8)) & 0xFF;
+ colour.append(("00" + QString::number(value, 16)).right(2));
+ }
+ // nhlog::ui()->debug("Hex Generated {} -> {}", QString::number(hash).toStdString(),
+ // colour.toStdString());
+ return colour.toUpper();
+}
+
+int
+utils::hashQString(const QString &input)
{
auto hash = 0;
for (int i = 0; i < input.length(); i++) {
hash = input.at(i).digitValue() + ((hash << 5) - hash);
}
+
hash *= 13;
- QString colour("#");
+
+ return hash;
+}
+
+QString
+utils::generateContrastingHexColor(const QString &input, const QString &background)
+{
+ nhlog::ui()->debug("Background hex {}", background.toStdString());
+ const QColor backgroundCol(background);
+ const qreal backgroundLum = luminance(background);
+
+ // Create a color for the input
+ auto hash = hashQString(input);
+ auto colorHex = generateHexColor(hash);
+
+ // converting to a QColor makes the luminance calc easier.
+ QColor inputColor = QColor(colorHex);
+
+ // attempt to score both the luminance and the contrast.
+ // contrast should have a higher precedence, but luminance
+ // helps dictate how exciting the colors are.
+ auto colorLum = luminance(inputColor);
+ auto contrast = computeContrast(colorLum, backgroundLum);
+
+ // If the contrast or luminance don't meet our criteria,
+ // try again and again until they do. After 10 tries,
+ // the best-scoring color will be chosen.
+ int att = 0;
+ while ((contrast < 5 || (colorLum < 0.05 || colorLum > 0.95)) && ++att < 10) {
+ hash = hashQString(input) + ((hash << 2) * 13);
+ auto newHex = generateHexColor(hash);
+ inputColor.setNamedColor(newHex);
+ auto tmpLum = luminance(inputColor);
+ auto tmpContrast = computeContrast(tmpLum, backgroundLum);
+
+ // Prioritize contrast over luminance
+ // If both values are better, it's a no brainer.
+ if (tmpContrast > contrast && (tmpLum > 0.05 && tmpLum < 0.95)) {
+ contrast = tmpContrast;
+ colorHex = newHex;
+ colorLum = tmpLum;
+ }
+ // Otherwise, if we still can get a more
+ // vibrant color and have met our contrast
+ // threshold, pick the more vibrant color,
+ // even if contrast will drop somewhat.
+ // choosing 50% luminance as ideal.
+ else if ((qAbs(tmpLum - 0.50) < qAbs(colorLum - 0.50)) && tmpContrast >= 5) {
+ contrast = tmpContrast;
+ colorHex = newHex;
+ colorLum = tmpLum;
+ }
+ // Otherwise, just take the better contrast.
+ else if (tmpContrast > contrast) {
+ contrast = tmpContrast;
+ colorHex = newHex;
+ colorLum = tmpLum;
+ }
+ }
+
+ nhlog::ui()->debug("Hex Generated for {}: [hex: {}, contrast: {}, luminance: {}]",
+ input.toStdString(),
+ colorHex.toStdString(),
+ QString::number(contrast).toStdString(),
+ QString::number(colorLum).toStdString());
+ return colorHex;
+}
+
+qreal
+utils::computeContrast(const qreal &one, const qreal &two)
+{
+ auto ratio = (one + 0.05) / (two + 0.05);
+
+ if (two > one) {
+ ratio = 1 / ratio;
+ }
+
+ return ratio;
+}
+
+qreal
+utils::luminance(const QColor &col)
+{
+ int colRgb[3] = {col.red(), col.green(), col.blue()};
+ qreal lumRgb[3];
+
for (int i = 0; i < 3; i++) {
- int value = (hash >> (i * 8)) & 0xFF;
- colour.append(("00" + QString::number(value, 16)).right(2));
+ qreal v = colRgb[i] / 255.0;
+ v <= 0.03928 ? lumRgb[i] = v / 12.92 : lumRgb[i] = qPow((v + 0.055) / 1.055, 2.4);
}
- return colour;
+
+ return lumRgb[0] * 0.2126 + lumRgb[1] * 0.7152 + lumRgb[2] * 0.0722;
}
void
diff --git a/src/Utils.h b/src/Utils.h
index 3ce2d758..8b3392da 100644
--- a/src/Utils.h
+++ b/src/Utils.h
@@ -14,6 +14,8 @@
#include <mtx/events/collections.hpp>
#include <mtx/events/common.hpp>
+#include <qmath.h>
+
class QComboBox;
namespace utils {
@@ -227,9 +229,26 @@ markdownToHtml(const QString &text);
QString
linkColor();
-//! Given an input string, create a color string
+//! Given an input integer, create a color string in #RRGGBB format
QString
-generateHexColor(const QString &string);
+generateHexColor(const int hash);
+
+//! Returns the hash code of the input QString
+int
+hashQString(const QString &input);
+
+//! Generate a color (matching #RRGGBB) that has an acceptable contrast to background that is based
+//! on the input string.
+QString
+generateContrastingHexColor(const QString &input, const QString &background);
+
+//! Given two luminance values, compute the contrast ratio between them.
+qreal
+computeContrast(const qreal &one, const qreal &two);
+
+//! Compute the luminance of a single color. Based on https://stackoverflow.com/a/9733420
+qreal
+luminance(const QColor &col);
//! Center a widget in relation to another widget.
void
diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp
index 3df78ff6..bd3d73bc 100644
--- a/src/timeline/TimelineItem.cpp
+++ b/src/timeline/TimelineItem.cpp
@@ -622,8 +622,6 @@ TimelineItem::generateUserName(const QString &user_id, const QString &displaynam
sender = displayname.split(":")[0].split("@")[1];
}
- auto userColor = utils::generateHexColor(user_id);
-
QFont usernameFont;
usernameFont.setPointSizeF(usernameFont.pointSizeF() * 1.1);
usernameFont.setWeight(QFont::Medium);
@@ -639,6 +637,12 @@ TimelineItem::generateUserName(const QString &user_id, const QString &displaynam
userName_->setAlignment(Qt::AlignLeft | Qt::AlignTop);
userName_->setFixedWidth(QFontMetrics(userName_->font()).width(userName_->text()));
+ // TimelineItem isn't displayed. This forces the QSS to get
+ // loaded.
+ qApp->style()->polish(this);
+ // generate user's unique color.
+ auto backCol = backgroundColor().name();
+ auto userColor = utils::generateContrastingHexColor(user_id, backCol);
userName_->setStyleSheet("QLabel { color : " + userColor + "; }");
auto filter = new UserProfileFilter(user_id, userName_);
diff --git a/src/timeline/TimelineItem.h b/src/timeline/TimelineItem.h
index 8159e370..c7f320d5 100644
--- a/src/timeline/TimelineItem.h
+++ b/src/timeline/TimelineItem.h
@@ -132,6 +132,8 @@ private:
class TimelineItem : public QWidget
{
Q_OBJECT
+ Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor)
+
public:
TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice> &e,
bool with_sender,
@@ -202,6 +204,9 @@ public:
const QString &room_id,
QWidget *parent);
+ void setBackgroundColor(const QColor &color) { backgroundColor_ = color; }
+ QColor backgroundColor() const { return backgroundColor_; }
+
void setUserAvatar(const QImage &pixmap);
DescInfo descriptionMessage() const { return descriptionMsg_; }
QString eventId() const { return event_id_; }
@@ -282,6 +287,8 @@ private:
QLabel *timestamp_;
QLabel *userName_;
TextLabel *body_;
+
+ QColor backgroundColor_;
};
template<class Widget>
|