diff --git a/resources/styles/nheko-dark.qss b/resources/styles/nheko-dark.qss index 86056bb2..e81fa0b8 100644 --- a/resources/styles/nheko-dark.qss +++ b/resources/styles/nheko-dark.qss @@ -3,6 +3,10 @@ QLabel { color: #caccd1; } +TimelineItem { + qproperty-backgroundColor: #202228; +} + #chatPage, #chatPage > * { background-color: #202228; diff --git a/resources/styles/nheko.qss b/resources/styles/nheko.qss index ca5a8f0d..468ae0f1 100644 --- a/resources/styles/nheko.qss +++ b/resources/styles/nheko.qss @@ -3,6 +3,10 @@ QLabel { color: #333; } +TimelineItem { + qproperty-backgroundColor: white; +} + #chatPage, #chatPage > * { background-color: white; diff --git a/resources/styles/system.qss b/resources/styles/system.qss index 45263e96..6663ee6b 100644 --- a/resources/styles/system.qss +++ b/resources/styles/system.qss @@ -3,6 +3,10 @@ TypingDisplay { qproperty-backgroundColor: palette(window); } +TimelineItem { + qproperty-backgroundColor: palette(window); +} + TimelineView, TimelineView > * { border: none; 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("#"); - for (int i = 0; i < 3; i++) { - int value = (hash >> (i * 8)) & 0xFF; - colour.append(("00" + QString::number(value, 16)).right(2)); + + 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; + } } - return colour; + + 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++) { + 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 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 #include +#include + 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 &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