-
Theming in Chatterino is currently hard. This is especially visible when changing theme values (see #4140). It turns out, that, in order to support multiple themes, one needs to make specific adjustments for these themes. Especially, handling light and dark themes without checking for the current theme is often not enough, as they have different needs for contrast & saturation, so one needs to branch on the theme. The Then there's the feature request to support custom themes. This isn't possible with the current system. I think one decent indicator for an extensible theming system is the usage/existence of the I'm proposing a new theme system, similar to how Telegram Desktop does it. I made an example branch here (diff). The most controversial change is probably the addition of some style & code generator (similar to The main benefits of having another generator (over a manual implementation) are
The input themes are "regular" CSS files (with two additional @-rules added: Example Dark.css@chatterino {
author: "Forsen";
icon-set: "dark";
}
:root {
--gray50: #555555;
--gray100: #6b6b6b; /* 0.6 */
--gray200: #616161; /* 0.65 */
--gray300: #575757; /* 0.7 */
--gray400: #454545; /* 0.78 */
--gray500: #424242; /* 0.79 */
--gray600: #383838; /* 0.85 */
--gray700: #2e2e2e; /* 0.9 */
--gray800: #242424; /* 0.95 */
--gray900: #212121; /* 0.96 */
--gray950: #1a1a1a; /* 1.0 */
--text-bright: #fff;
--text: #eee;
--text-dimmed: #aaa;
--highlight-color: #ee6166;
--sub-color: #c466ff;
--link-text-color: #4286f4;
--system-text-color: #8c7f7f;
--placeholder-text-color: #5d5555;
--scrollbar-thumb: #b3b3b3;
--scrollbar-thumb-selected: #a6a6a6;
--split-header-selected: hsl(210, 100%, 70%);
--window-bg: #111;
--tab-bg: #252525;
--tab-bg-selected: #555555;
--accent: #00aeef;
}
Tabs {
border: white;
divider-line: var(--tab-bg-selected);
@nest regular {
text: var(--text-dimmed);
@nest backgrounds {
regular: var(--tab-bg);
hover: var(--tab-bg);
unfocused: var(--tab-bg);
}
@nest line {
regular: var(--gray400);
hover: var(--gray400);
unfocused: var(--gray400);
}
}
@nest new-message {
text: var(--text);
@nest backgrounds {
regular: var(--tab-bg);
hover: var(--tab-bg);
unfocused: var(--tab-bg);
}
@nest line {
regular: #888;
hover: #888;
unfocused: #888;
}
}
@nest highlighted {
text: var(--text);
@nest backgrounds {
regular: var(--tab-bg);
hover: var(--tab-bg);
unfocused: var(--tab-bg);
}
@nest line {
regular: var(--highlight-color);
hover: var(--highlight-color);
unfocused: var(--highlight-color);
}
}
@nest selected {
text: var(--text-bright);
@nest backgrounds {
regular: var(--tab-bg-selected);
hover: var(--tab-bg-selected);
unfocused: var(--tab-bg-selected);
}
@nest line {
regular: var(--accent);
hover: var(--accent);
unfocused: var(--accent);
}
}
}
Messages {
disabled: hsla(0, 0%, 10%, 0.6);
selection: hsla(0, 0%, 100%, 0.251);
highlight-animation-start: hsla(0, 0%, 90%, 0.431);
highlight-animation-end: hsla(0, 0%, 90%, 0);
@nest text-colors {
regular: var(--text-bright);
caret: var(--text-bright);
link: var(--link-text-color);
system: var(--system-text-color);
chat-placeholder: var(--placeholder-text-color);
}
@nest backgrounds {
regular: var(--gray950);
alternate: var(--gray900);
}
}
Scrollbars {
background: transparent;
thumb: var(--scrollbar-thumb);
thumbselected: var(--scrollbar-thumb-selected);
@nest highlights {
highlight: var(--highlight-color);
subscription: var(--sub-color);
}
}
Tooltip {
text: var(--text-bright);
background: black;
}
Splits {
message-seperator: var(--gray600);
background: var(--gray950);
drop-preview: hsla(205, 100%, 50%, 0.18);
drop-preview-border: hsl(205, 100%, 50%);
drop-target-rect: hsla(205, 100%, 50%, 0);
drop-target-rect-border: hsla(205, 100%, 50%, 0);
resize-handle: hsla(205, 100%, 50%, 0.44);
resize-handle-background: hsla(205, 100%, 50%, 0.125);
@nest header {
border: var(--gray600);
focused-border: var(--gray500);
background: var(--gray700);
focused-background: var(--gray500);
text: var(--text-bright);
focused-text: var(--split-header-selected);
}
@nest input {
border: var(--gray950);
background: var(--gray800);
selection: transparent; /* unused? */
focused-line: var(--highlight-color);
text: var(--highlight-color);
}
}
Colors {
accent-color: var(--accent);
}
Window {
text: var(--text);
background: var(--window-bg);
border-focused: var(--accent);
border-unfocused: var(--gray100);
} This will be compiled to a Compiled Dark.c2theme
This was one part of the generator. The other part is generating a header and source file for a given theme layout (in theory this could be done manually). GeneratedTheme.hpp#include <QColor>
#include <QByteArray>
namespace chatterino::theme {
class GeneratedTheme {
public:
struct TabColors {
struct {
QColor regular;
QColor hover;
QColor unfocused;
} backgrounds;
struct {
QColor regular;
QColor hover;
QColor unfocused;
} line;
QColor text;
};
struct {
QColor accentColor;
} colors;
struct {
struct {
QColor regular;
QColor alternate;
} backgrounds;
QColor disabled;
QColor highlightAnimationEnd;
QColor highlightAnimationStart;
QColor selection;
struct {
QColor regular;
QColor caret;
QColor link;
QColor system;
QColor chatPlaceholder;
} textColors;
} messages;
struct {
QColor background;
struct {
QColor highlight;
QColor subscription;
} highlights;
QColor thumb;
QColor thumbSelected;
} scrollbars;
struct {
QColor background;
QColor dropPreview;
QColor dropPreviewBorder;
QColor dropTargetRect;
QColor dropTargetRectBorder;
struct {
QColor border;
QColor focusedBorder;
QColor background;
QColor focusedBackground;
QColor text;
QColor focusedText;
} header;
struct {
QColor border;
QColor background;
QColor selection;
QColor focusedLine;
QColor text;
} input;
QColor messageSeperator;
QColor resizeHandle;
QColor resizeHandleBackground;
} splits;
struct {
QColor border;
QColor dividerLine;
TabColors highlighted;
TabColors newMessage;
TabColors regular;
TabColors selected;
} tabs;
struct {
QColor text;
QColor background;
} tooltip;
struct {
QColor text;
QColor background;
QColor borderUnfocused;
QColor borderFocused;
} window;
GeneratedTheme();
protected:
bool setColor(const QByteArray &name, QColor color);
void reset();
void applyChanges();
private:
QColor colors_[72];
};
} // namespace chatterino::theme GeneratedTheme.cpp#include "GeneratedTheme.hpp"
#include <QColor>
#include <QString>
#include <QByteArray>
#include <QMap>
#include <cstring>
namespace {
int getDataIndex(const QByteArray &name);
} // namespace
namespace chatterino::theme {
GeneratedTheme::GeneratedTheme() {
this->reset();
this->applyChanges();
}
void GeneratedTheme::applyChanges() {
const auto d = [this](size_t i) -> const QColor& { return this->colors_[i]; };
this->colors = {
d(0),
};
this->messages = {
{
d(1),
d(2),
},
d(3),
d(4),
d(5),
d(6),
{
d(7),
d(8),
d(9),
d(10),
d(11),
},
};
this->scrollbars = {
d(12),
{
d(13),
d(14),
},
d(15),
d(16),
};
this->splits = {
d(17),
d(18),
d(19),
d(20),
d(21),
{
d(22),
d(23),
d(24),
d(25),
d(26),
d(27),
},
{
d(28),
d(29),
d(30),
d(31),
d(32),
},
d(33),
d(34),
d(35),
};
this->tabs = {
d(36),
d(37),
{
{
d(38),
d(39),
d(40),
},
{
d(41),
d(42),
d(43),
},
d(44),
},
{
{
d(45),
d(46),
d(47),
},
{
d(48),
d(49),
d(50),
},
d(51),
},
{
{
d(52),
d(53),
d(54),
},
{
d(55),
d(56),
d(57),
},
d(58),
},
{
{
d(59),
d(60),
d(61),
},
{
d(62),
d(63),
d(64),
},
d(65),
},
};
this->tooltip = {
d(66),
d(67),
};
this->window = {
d(68),
d(69),
d(70),
d(71),
};
this->reset();
}
void GeneratedTheme::reset() {
this->colors_[0] = {0, 174, 239, 255};
this->colors_[1] = {26, 26, 26, 255};
this->colors_[2] = {33, 33, 33, 255};
this->colors_[3] = {26, 26, 26, 153};
this->colors_[4] = {230, 230, 230, 0};
this->colors_[5] = {230, 230, 230, 110};
this->colors_[6] = {255, 255, 255, 64};
this->colors_[7] = {255, 255, 255, 255};
this->colors_[8] = {255, 255, 255, 255};
this->colors_[9] = {66, 134, 244, 255};
this->colors_[10] = {140, 127, 127, 255};
this->colors_[11] = {93, 85, 85, 255};
this->colors_[12] = {0, 0, 0, 0};
this->colors_[13] = {238, 97, 102, 255};
this->colors_[14] = {196, 102, 255, 255};
this->colors_[15] = {179, 179, 179, 255};
this->colors_[16] = {166, 166, 166, 255};
this->colors_[17] = {26, 26, 26, 255};
this->colors_[18] = {0, 149, 255, 46};
this->colors_[19] = {0, 149, 255, 255};
this->colors_[20] = {0, 149, 255, 0};
this->colors_[21] = {0, 149, 255, 0};
this->colors_[22] = {56, 56, 56, 255};
this->colors_[23] = {66, 66, 66, 255};
this->colors_[24] = {46, 46, 46, 255};
this->colors_[25] = {66, 66, 66, 255};
this->colors_[26] = {255, 255, 255, 255};
this->colors_[27] = {102, 179, 255, 255};
this->colors_[28] = {26, 26, 26, 255};
this->colors_[29] = {36, 36, 36, 255};
this->colors_[30] = {0, 0, 0, 0};
this->colors_[31] = {238, 97, 102, 255};
this->colors_[32] = {238, 97, 102, 255};
this->colors_[33] = {56, 56, 56, 255};
this->colors_[34] = {0, 149, 255, 112};
this->colors_[35] = {0, 149, 255, 32};
this->colors_[36] = {255, 255, 255, 255};
this->colors_[37] = {85, 85, 85, 255};
this->colors_[38] = {37, 37, 37, 255};
this->colors_[39] = {37, 37, 37, 255};
this->colors_[40] = {37, 37, 37, 255};
this->colors_[41] = {238, 97, 102, 255};
this->colors_[42] = {238, 97, 102, 255};
this->colors_[43] = {238, 97, 102, 255};
this->colors_[44] = {238, 238, 238, 255};
this->colors_[45] = {37, 37, 37, 255};
this->colors_[46] = {37, 37, 37, 255};
this->colors_[47] = {37, 37, 37, 255};
this->colors_[48] = {136, 136, 136, 255};
this->colors_[49] = {136, 136, 136, 255};
this->colors_[50] = {136, 136, 136, 255};
this->colors_[51] = {238, 238, 238, 255};
this->colors_[52] = {37, 37, 37, 255};
this->colors_[53] = {37, 37, 37, 255};
this->colors_[54] = {37, 37, 37, 255};
this->colors_[55] = {69, 69, 69, 255};
this->colors_[56] = {69, 69, 69, 255};
this->colors_[57] = {69, 69, 69, 255};
this->colors_[58] = {170, 170, 170, 255};
this->colors_[59] = {85, 85, 85, 255};
this->colors_[60] = {85, 85, 85, 255};
this->colors_[61] = {85, 85, 85, 255};
this->colors_[62] = {0, 174, 239, 255};
this->colors_[63] = {0, 174, 239, 255};
this->colors_[64] = {0, 174, 239, 255};
this->colors_[65] = {255, 255, 255, 255};
this->colors_[66] = {255, 255, 255, 255};
this->colors_[67] = {0, 0, 0, 255};
this->colors_[68] = {238, 238, 238, 255};
this->colors_[69] = {17, 17, 17, 255};
this->colors_[70] = {107, 107, 107, 255};
this->colors_[71] = {0, 174, 239, 255};
}
bool GeneratedTheme::setColor(const QByteArray &name, QColor color) {
auto idx = getDataIndex(name);
if (idx < 0) return false;
this->colors_[idx] = color;
return true;
}
} // namespace chatterino::theme
namespace {
int getDataIndex(const QByteArray &name) {
static const QMap<QByteArray, size_t> dataMap = {
{"colors.accentcolor", 0},
{"messages.backgrounds.regular", 1},
{"messages.backgrounds.alternate", 2},
{"messages.disabled", 3},
{"messages.highlightanimationend", 4},
{"messages.highlightanimationstart", 5},
{"messages.selection", 6},
{"messages.textcolors.regular", 7},
{"messages.textcolors.caret", 8},
{"messages.textcolors.link", 9},
{"messages.textcolors.system", 10},
{"messages.textcolors.chatplaceholder", 11},
{"scrollbars.background", 12},
{"scrollbars.highlights.highlight", 13},
{"scrollbars.highlights.subscription", 14},
{"scrollbars.thumb", 15},
{"scrollbars.thumbselected", 16},
{"splits.background", 17},
{"splits.droppreview", 18},
{"splits.droppreviewborder", 19},
{"splits.droptargetrect", 20},
{"splits.droptargetrectborder", 21},
{"splits.header.border", 22},
{"splits.header.focusedborder", 23},
{"splits.header.background", 24},
{"splits.header.focusedbackground", 25},
{"splits.header.text", 26},
{"splits.header.focusedtext", 27},
{"splits.input.border", 28},
{"splits.input.background", 29},
{"splits.input.selection", 30},
{"splits.input.focusedline", 31},
{"splits.input.text", 32},
{"splits.messageseperator", 33},
{"splits.resizehandle", 34},
{"splits.resizehandlebackground", 35},
{"tabs.border", 36},
{"tabs.dividerline", 37},
{"tabs.highlighted.backgrounds.regular", 38},
{"tabs.highlighted.backgrounds.hover", 39},
{"tabs.highlighted.backgrounds.unfocused", 40},
{"tabs.highlighted.line.regular", 41},
{"tabs.highlighted.line.hover", 42},
{"tabs.highlighted.line.unfocused", 43},
{"tabs.highlighted.text", 44},
{"tabs.newmessage.backgrounds.regular", 45},
{"tabs.newmessage.backgrounds.hover", 46},
{"tabs.newmessage.backgrounds.unfocused", 47},
{"tabs.newmessage.line.regular", 48},
{"tabs.newmessage.line.hover", 49},
{"tabs.newmessage.line.unfocused", 50},
{"tabs.newmessage.text", 51},
{"tabs.regular.backgrounds.regular", 52},
{"tabs.regular.backgrounds.hover", 53},
{"tabs.regular.backgrounds.unfocused", 54},
{"tabs.regular.line.regular", 55},
{"tabs.regular.line.hover", 56},
{"tabs.regular.line.unfocused", 57},
{"tabs.regular.text", 58},
{"tabs.selected.backgrounds.regular", 59},
{"tabs.selected.backgrounds.hover", 60},
{"tabs.selected.backgrounds.unfocused", 61},
{"tabs.selected.line.regular", 62},
{"tabs.selected.line.hover", 63},
{"tabs.selected.line.unfocused", 64},
{"tabs.selected.text", 65},
{"tooltip.text", 66},
{"tooltip.background", 67},
{"window.text", 68},
{"window.background", 69},
{"window.borderunfocused", 70},
{"window.borderfocused", 71},
};
return dataMap.value(name, -1);
}
} // namespace Why not do it like...? I haven't really found many applications having a theme system that could be applied here. One application I found was OBS. They're basically parsing CSS. I don't think this approach is reasonable for Chatterino. The final Chatterino executable shouldn't contain a (half compliant) CSS parser. Especially because Chatterino's theme has nested colors. Further work These themes don't apply to Qt's palette. This would be the next step, as well as using What do you think about this approach? Are there other approaches you know that would be viable here? |
Beta Was this translation helpful? Give feedback.
Replies: 4 comments 1 reply
-
I noticed Qt itself has some kind of mostly compliant Using CSS as an input into the preprocessor might be bad as CSS is a lot more expressive than |
Beta Was this translation helpful? Give feedback.
-
I just want to put forward that compiling css into a custom format sounds unnecessarily more complicated than using json |
Beta Was this translation helpful? Give feedback.
-
I like the idea and mostly agree on your observations regarding tech debt. One thing I'm not sure I agree on is this part however:
I think it would be nice if we could drop this usage too, and instead provide icons as part of the theme itself, i.e., themes being "resource sets" instead of just a CSS(-like) file. We would have to think about how to package icons with themes, especially in regards to making default icons available for user-defined themes as well. But I believe this would result in a nicer developer experience when dealing with icons across themes as well as additional customizability for users. As an extra, it also would not require users to explicitly classify their themes into "light theme" or "dark theme". |
Beta Was this translation helpful? Give feedback.
-
Implemented using JSON in #4471 |
Beta Was this translation helpful? Give feedback.
Implemented using JSON in #4471