Skip to content

Commit

Permalink
frontend: Add new appearance options
Browse files Browse the repository at this point in the history
  • Loading branch information
Warchamp7 committed Feb 7, 2025
1 parent 79f5ba0 commit fea4822
Show file tree
Hide file tree
Showing 12 changed files with 660 additions and 164 deletions.
3 changes: 3 additions & 0 deletions frontend/OBSApp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,9 @@ void OBSApp::InitUserConfigDefaults()
config_set_default_bool(userConfig, "BasicWindow", "MultiviewDrawAreas", true);

config_set_default_bool(userConfig, "BasicWindow", "MediaControlsCountdownTimer", true);

config_set_default_int(userConfig, "Appearance", "FontScale", 10);
config_set_default_int(userConfig, "Appearance", "Density", 1);
}

static bool do_mkdir(const char *path)
Expand Down
198 changes: 144 additions & 54 deletions frontend/OBSApp_Themes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ static QColor ParseColor(CFParser &cfp)
return res;
}

static bool ParseCalc(CFParser &cfp, QStringList &calc, vector<OBSThemeVariable> &vars)
static bool ParseMath(CFParser &cfp, QStringList &values, vector<OBSThemeVariable> &vars)
{
int ret = cf_next_token_should_be(cfp, "(", ";", nullptr);
if (ret != PARSE_SUCCESS)
Expand All @@ -216,36 +216,44 @@ static bool ParseCalc(CFParser &cfp, QStringList &calc, vector<OBSThemeVariable>
if (cf_token_is(cfp, ";"))
break;

if (cf_token_is(cfp, "calc")) {
/* Internal calc's do not have proper names.
if (cf_token_is(cfp, "calc") || cf_token_is(cfp, "max") || cf_token_is(cfp, "min")) {
/* Internal math operations do not have proper names.
* They are anonymous variables */
OBSThemeVariable var;
QStringList subcalc;
QStringList subvalues;

var.name = QString("__unnamed_%1").arg(QRandomGenerator::global()->generate64());

if (!ParseCalc(cfp, subcalc, vars))
OBSThemeVariable::VariableType varType;
if (cf_token_is(cfp, "calc"))
varType = OBSThemeVariable::Calc;
else if (cf_token_is(cfp, "max"))
varType = OBSThemeVariable::Max;
else if (cf_token_is(cfp, "min"))
varType = OBSThemeVariable::Min;

if (!ParseMath(cfp, subvalues, vars))
return false;

var.type = OBSThemeVariable::Calc;
var.value = subcalc;
calc << var.name;
var.type = varType;
var.value = subvalues;
values << var.name;
vars.push_back(std::move(var));
} else if (cf_token_is(cfp, "var")) {
QString value;
if (!ParseVarName(cfp, value))
return false;

calc << value;
values << value;
} else {
calc << QString::fromUtf8(cfp->cur_token->str.array, cfp->cur_token->str.len);
values << QString::fromUtf8(cfp->cur_token->str.array, cfp->cur_token->str.len);
}

if (!cf_next_token(cfp))
return false;
}

return !calc.isEmpty();
return !values.isEmpty();
}

static vector<OBSThemeVariable> ParseThemeVariables(const char *themeData)
Expand Down Expand Up @@ -316,6 +324,11 @@ static vector<OBSThemeVariable> ParseThemeVariables(const char *themeData)
if (!cf_next_token(cfp))
return vars;

/* Special values passed to the theme by OBS are prefixed with 'obs' so we
* prevent theme variables from using it as a prefix */
if (key.startsWith("obs"))
continue;

if (cfp->cur_token->type == CFTOKEN_NUM) {
const char *ch = cfp->cur_token->str.array;
const char *end = ch + cfp->cur_token->str.len;
Expand Down Expand Up @@ -348,14 +361,20 @@ static vector<OBSThemeVariable> ParseThemeVariables(const char *themeData)

var.value = value;
var.type = OBSThemeVariable::Alias;
} else if (cf_token_is(cfp, "calc")) {
QStringList calc;
} else if (cf_token_is(cfp, "calc") || cf_token_is(cfp, "max") || cf_token_is(cfp, "min")) {
QStringList values;

if (!ParseCalc(cfp, calc, vars))
if (cf_token_is(cfp, "calc"))
var.type = OBSThemeVariable::Calc;
else if (cf_token_is(cfp, "max"))
var.type = OBSThemeVariable::Max;
else if (cf_token_is(cfp, "min"))
var.type = OBSThemeVariable::Min;

if (!ParseMath(cfp, values, vars))
continue;

var.type = OBSThemeVariable::Calc;
var.value = calc;
var.value = values;
} else {
var.type = OBSThemeVariable::String;
BPtr strVal = cf_literal_to_str(cfp->cur_token->str.array, cfp->cur_token->str.len);
Expand All @@ -367,8 +386,9 @@ static vector<OBSThemeVariable> ParseThemeVariables(const char *themeData)

if (cf_token_is(cfp, "!") &&
cf_next_token_should_be(cfp, "editable", nullptr, nullptr) == PARSE_SUCCESS) {
if (var.type == OBSThemeVariable::Calc || var.type == OBSThemeVariable::Alias) {
blog(LOG_WARNING, "Variable of calc/alias type cannot be editable: %s",
if (var.type == OBSThemeVariable::Calc || var.type == OBSThemeVariable::Max ||
var.type == OBSThemeVariable::Min || var.type == OBSThemeVariable::Alias) {
blog(LOG_WARNING, "Math or alias variable type cannot be editable: %s",
QT_TO_UTF8(var.name));
} else {
var.editable = true;
Expand Down Expand Up @@ -496,10 +516,10 @@ static bool ResolveVariable(const QHash<QString, OBSThemeVariable> &vars, OBSThe
return true;
}

static QString EvalCalc(const QHash<QString, OBSThemeVariable> &vars, const OBSThemeVariable &var,
const int recursion = 0);
static QString EvalMath(const QHash<QString, OBSThemeVariable> &vars, const OBSThemeVariable &var,
const OBSThemeVariable::VariableType type, const int recursion = 0);

static OBSThemeVariable ParseCalcVariable(const QHash<QString, OBSThemeVariable> &vars, const QString &value,
static OBSThemeVariable ParseMathVariable(const QHash<QString, OBSThemeVariable> &vars, const QString &value,
const int recursion = 0)
{
OBSThemeVariable var;
Expand Down Expand Up @@ -527,85 +547,105 @@ static OBSThemeVariable ParseCalcVariable(const QHash<QString, OBSThemeVariable>
var.value = value;
ResolveVariable(vars, var);

/* Handle nested calc()s */
if (var.type == OBSThemeVariable::Calc) {
QString val = EvalCalc(vars, var, recursion + 1);
var = ParseCalcVariable(vars, val);
/* Handle nested math calculations */
if (var.type == OBSThemeVariable::Calc || var.type == OBSThemeVariable::Max ||
var.type == OBSThemeVariable::Min) {
QString val = EvalMath(vars, var, var.type, recursion + 1);
var = ParseMathVariable(vars, val);
}

/* Only number or size would be valid here */
if (var.type != OBSThemeVariable::Number && var.type != OBSThemeVariable::Size) {
blog(LOG_ERROR, "calc() operand is not a size or number: %s", QT_TO_UTF8(var.value.toString()));
blog(LOG_ERROR, "Math operand is not a size or number: %s %s %d", QT_TO_UTF8(var.name),
QT_TO_UTF8(var.value.toString()), var.type);
throw invalid_argument("Operand not of numeric type");
}
}

return var;
}

static QString EvalCalc(const QHash<QString, OBSThemeVariable> &vars, const OBSThemeVariable &var, const int recursion)
static QString EvalMath(const QHash<QString, OBSThemeVariable> &vars, const OBSThemeVariable &var,
const OBSThemeVariable::VariableType type, const int recursion)
{
if (recursion >= 10) {
/* Abort after 10 levels of recursion */
blog(LOG_ERROR, "Maximum calc() recursion levels hit!");
blog(LOG_ERROR, "Maximum recursion levels hit!");
return "'Invalid expression'";
}

QStringList args = var.value.toStringList();
if (args.length() != 3) {
blog(LOG_ERROR, "calc() had invalid number of arguments: %lld (%s)", args.length(),
QT_TO_UTF8(args.join(", ")));
if (type != OBSThemeVariable::Calc && type != OBSThemeVariable::Max && type != OBSThemeVariable::Min) {
blog(LOG_ERROR, "Invalid type for math operation!");
return "'Invalid expression'";
}

QStringList args = var.value.toStringList();
QString &opt = args[1];
if (opt != '*' && opt != '+' && opt != '-' && opt != '/') {
if (type == OBSThemeVariable::Calc && (opt != '*' && opt != '+' && opt != '-' && opt != '/')) {
blog(LOG_ERROR, "Unknown/invalid calc() operator: %s", QT_TO_UTF8(opt));
return "'Invalid expression'";
}

if ((type == OBSThemeVariable::Max || type == OBSThemeVariable::Min) && opt != ',') {
blog(LOG_ERROR, "Invalid math separator: %s", QT_TO_UTF8(opt));
return "'Invalid expression'";
}

if (args.length() != 3) {
blog(LOG_ERROR, "Math parse had invalid number of arguments: %lld (%s)", args.length(),
QT_TO_UTF8(args.join(", ")));
return "'Invalid expression'";
}

OBSThemeVariable val1, val2;
try {
val1 = ParseCalcVariable(vars, args[0], recursion);
val2 = ParseCalcVariable(vars, args[2], recursion);
val1 = ParseMathVariable(vars, args[0], 0);
val2 = ParseMathVariable(vars, args[2], 0);
} catch (...) {
return "'Invalid expression'";
}

/* Ensure that suffixes match (if any) */
if (!val1.suffix.isEmpty() && !val2.suffix.isEmpty() && val1.suffix != val2.suffix) {
blog(LOG_ERROR, "calc() requires suffixes to match or only one to be present! %s != %s",
blog(LOG_ERROR, "Math operation requires suffixes to match or only one to be present! %s != %s",
QT_TO_UTF8(val1.suffix), QT_TO_UTF8(val2.suffix));
return "'Invalid expression'";
}

double val = numeric_limits<double>::quiet_NaN();
double d1 = val1.userValue.isValid() ? val1.userValue.toDouble() : val1.value.toDouble();
double d2 = val2.userValue.isValid() ? val2.userValue.toDouble() : val2.value.toDouble();

if (!isfinite(d1) || !isfinite(d2)) {
blog(LOG_ERROR,
"calc() received at least one invalid value:"
"At least one invalid math value:"
" op1: %f, op2: %f",
d1, d2);
return "'Invalid expression'";
}

if (opt == "+")
val = d1 + d2;
else if (opt == "-")
val = d1 - d2;
else if (opt == "*")
val = d1 * d2;
else if (opt == "/")
val = d1 / d2;
double val = numeric_limits<double>::quiet_NaN();

if (!isnormal(val)) {
blog(LOG_ERROR,
"Invalid calc() math resulted in non-normal number:"
" %f %s %f = %f",
d1, QT_TO_UTF8(opt), d2, val);
return "'Invalid expression'";
if (type == OBSThemeVariable::Calc) {
if (opt == "+")
val = d1 + d2;
else if (opt == "-")
val = d1 - d2;
else if (opt == "*")
val = d1 * d2;
else if (opt == "/")
val = d1 / d2;

if (!isnormal(val)) {
blog(LOG_ERROR,
"Invalid calc() resulted in non-normal number:"
" %f %s %f = %f",
d1, QT_TO_UTF8(opt), d2, val);
return "'Invalid expression'";
}
} else if (type == OBSThemeVariable::Max) {
val = d1 > d2 ? d1 : d2;
} else if (type == OBSThemeVariable::Min) {
val = d1 < d2 ? d1 : d2;
}

bool isInteger = ceill(val) == val;
Expand Down Expand Up @@ -661,8 +701,9 @@ static QString PrepareQSS(const QHash<QString, OBSThemeVariable> &vars, const QS

if (var.type == OBSThemeVariable::Color) {
replace = value.value<QColor>().name(QColor::HexRgb);
} else if (var.type == OBSThemeVariable::Calc) {
replace = EvalCalc(vars, var);
} else if (var.type == OBSThemeVariable::Calc || var.type == OBSThemeVariable::Max ||
var.type == OBSThemeVariable::Min) {
replace = EvalMath(vars, var, var.type);
} else if (var.type == OBSThemeVariable::Size || var.type == OBSThemeVariable::Number) {
double val = value.toDouble();
bool isInteger = ceill(val) == val;
Expand Down Expand Up @@ -747,6 +788,23 @@ static QPalette PreparePalette(const QHash<QString, OBSThemeVariable> &vars, con
return pal;
}

static double getPaddingForDensityId(int id)
{
double paddingValue = 4;

if (id == -2) {
paddingValue = 0.25;
} else if (id == -3) {
paddingValue = 2;
} else if (id == -4) {
paddingValue = 4;
} else if (id == -5) {
paddingValue = 6;
}

return paddingValue;
}

OBSTheme *OBSApp::GetTheme(const QString &name)
{
if (!themes.contains(name))
Expand Down Expand Up @@ -775,6 +833,22 @@ bool OBSApp::SetTheme(const QString &name)
QStringList themeIds(theme->dependencies);
themeIds << theme->id;

/* Inject Appearance settings into theme vars */
OBSThemeVariable fontScale;
fontScale.name = "obsFontScale";
fontScale.type = OBSThemeVariable::Number;
fontScale.value = QVariant::fromValue(config_get_int(App()->GetUserConfig(), "Appearance", "FontScale"));

const int density = config_get_int(App()->GetUserConfig(), "Appearance", "Density");

OBSThemeVariable padding;
padding.name = "obsPadding";
padding.type = OBSThemeVariable::Number;
padding.value = QVariant::fromValue(getPaddingForDensityId(density));

vars[fontScale.name] = std::move(fontScale);
vars[padding.name] = std::move(padding);

/* Find and add high contrast adjustment layer if available */
if (HighContrastEnabled()) {
for (const OBSTheme &theme_ : themes) {
Expand Down Expand Up @@ -805,6 +879,22 @@ bool OBSApp::SetTheme(const QString &name)
contents.emplaceBack(content.constData());
}

/* Check if OBS appearance settings are used in the theme */
currentTheme->usesFontScale = false;
currentTheme->usesDensity = false;
for (const OBSThemeVariable &var_ : vars) {
if (var_.type != OBSThemeVariable::Alias)
continue;

if (var_.value.toString() == "obsFontScale") {
currentTheme->usesFontScale = true;
}

if (var_.value.toString() == "obsPadding") {
currentTheme->usesDensity = true;
}
}

const QString stylesheet = PrepareQSS(vars, contents);
const QPalette palette = PreparePalette(vars, defaultPalette);
setPalette(palette);
Expand Down
Loading

0 comments on commit fea4822

Please sign in to comment.