Explorar o código

frontend: Add new appearance options

Warchamp7 hai 8 meses
pai
achega
c0c77071b5

+ 3 - 0
frontend/OBSApp.cpp

@@ -296,6 +296,9 @@ void OBSApp::InitUserConfigDefaults()
 	config_set_default_bool(userConfig, "BasicWindow", "MultiviewDrawAreas", true);
 	config_set_default_bool(userConfig, "BasicWindow", "MultiviewDrawAreas", true);
 
 
 	config_set_default_bool(userConfig, "BasicWindow", "MediaControlsCountdownTimer", 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)
 static bool do_mkdir(const char *path)

+ 142 - 54
frontend/OBSApp_Themes.cpp

@@ -204,7 +204,7 @@ static QColor ParseColor(CFParser &cfp)
 	return res;
 	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);
 	int ret = cf_next_token_should_be(cfp, "(", ";", nullptr);
 	if (ret != PARSE_SUCCESS)
 	if (ret != PARSE_SUCCESS)
@@ -216,36 +216,44 @@ static bool ParseCalc(CFParser &cfp, QStringList &calc, vector<OBSThemeVariable>
 		if (cf_token_is(cfp, ";"))
 		if (cf_token_is(cfp, ";"))
 			break;
 			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 */
 			 * They are anonymous variables */
 			OBSThemeVariable var;
 			OBSThemeVariable var;
-			QStringList subcalc;
+			QStringList subvalues;
 
 
 			var.name = QString("__unnamed_%1").arg(QRandomGenerator::global()->generate64());
 			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;
 				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));
 			vars.push_back(std::move(var));
 		} else if (cf_token_is(cfp, "var")) {
 		} else if (cf_token_is(cfp, "var")) {
 			QString value;
 			QString value;
 			if (!ParseVarName(cfp, value))
 			if (!ParseVarName(cfp, value))
 				return false;
 				return false;
 
 
-			calc << value;
+			values << value;
 		} else {
 		} 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))
 		if (!cf_next_token(cfp))
 			return false;
 			return false;
 	}
 	}
 
 
-	return !calc.isEmpty();
+	return !values.isEmpty();
 }
 }
 
 
 static vector<OBSThemeVariable> ParseThemeVariables(const char *themeData)
 static vector<OBSThemeVariable> ParseThemeVariables(const char *themeData)
@@ -316,6 +324,11 @@ static vector<OBSThemeVariable> ParseThemeVariables(const char *themeData)
 		if (!cf_next_token(cfp))
 		if (!cf_next_token(cfp))
 			return vars;
 			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) {
 		if (cfp->cur_token->type == CFTOKEN_NUM) {
 			const char *ch = cfp->cur_token->str.array;
 			const char *ch = cfp->cur_token->str.array;
 			const char *end = ch + cfp->cur_token->str.len;
 			const char *end = ch + cfp->cur_token->str.len;
@@ -348,14 +361,20 @@ static vector<OBSThemeVariable> ParseThemeVariables(const char *themeData)
 
 
 			var.value = value;
 			var.value = value;
 			var.type = OBSThemeVariable::Alias;
 			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;
 				continue;
 
 
-			var.type = OBSThemeVariable::Calc;
-			var.value = calc;
+			var.value = values;
 		} else {
 		} else {
 			var.type = OBSThemeVariable::String;
 			var.type = OBSThemeVariable::String;
 			BPtr strVal = cf_literal_to_str(cfp->cur_token->str.array, cfp->cur_token->str.len);
 			BPtr strVal = cf_literal_to_str(cfp->cur_token->str.array, cfp->cur_token->str.len);
@@ -367,8 +386,9 @@ static vector<OBSThemeVariable> ParseThemeVariables(const char *themeData)
 
 
 		if (cf_token_is(cfp, "!") &&
 		if (cf_token_is(cfp, "!") &&
 		    cf_next_token_should_be(cfp, "editable", nullptr, nullptr) == PARSE_SUCCESS) {
 		    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));
 				     QT_TO_UTF8(var.name));
 			} else {
 			} else {
 				var.editable = true;
 				var.editable = true;
@@ -496,10 +516,10 @@ static bool ResolveVariable(const QHash<QString, OBSThemeVariable> &vars, OBSThe
 	return true;
 	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)
 					  const int recursion = 0)
 {
 {
 	OBSThemeVariable var;
 	OBSThemeVariable var;
@@ -527,15 +547,17 @@ static OBSThemeVariable ParseCalcVariable(const QHash<QString, OBSThemeVariable>
 		var.value = value;
 		var.value = value;
 		ResolveVariable(vars, var);
 		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 */
 		/* Only number or size would be valid here */
 		if (var.type != OBSThemeVariable::Number && var.type != OBSThemeVariable::Size) {
 		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");
 			throw invalid_argument("Operand not of numeric type");
 		}
 		}
 	}
 	}
@@ -543,69 +565,85 @@ static OBSThemeVariable ParseCalcVariable(const QHash<QString, OBSThemeVariable>
 	return var;
 	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) {
 	if (recursion >= 10) {
 		/* Abort after 10 levels of recursion */
 		/* Abort after 10 levels of recursion */
-		blog(LOG_ERROR, "Maximum calc() recursion levels hit!");
+		blog(LOG_ERROR, "Maximum recursion levels hit!");
 		return "'Invalid expression'";
 		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'";
 		return "'Invalid expression'";
 	}
 	}
 
 
+	QStringList args = var.value.toStringList();
 	QString &opt = args[1];
 	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));
 		blog(LOG_ERROR, "Unknown/invalid calc() operator: %s", QT_TO_UTF8(opt));
 		return "'Invalid expression'";
 		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;
 	OBSThemeVariable val1, val2;
 	try {
 	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 (...) {
 	} catch (...) {
 		return "'Invalid expression'";
 		return "'Invalid expression'";
 	}
 	}
 
 
 	/* Ensure that suffixes match (if any) */
 	/* Ensure that suffixes match (if any) */
 	if (!val1.suffix.isEmpty() && !val2.suffix.isEmpty() && val1.suffix != val2.suffix) {
 	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));
 		     QT_TO_UTF8(val1.suffix), QT_TO_UTF8(val2.suffix));
 		return "'Invalid expression'";
 		return "'Invalid expression'";
 	}
 	}
 
 
-	double val = numeric_limits<double>::quiet_NaN();
 	double d1 = val1.userValue.isValid() ? val1.userValue.toDouble() : val1.value.toDouble();
 	double d1 = val1.userValue.isValid() ? val1.userValue.toDouble() : val1.value.toDouble();
 	double d2 = val2.userValue.isValid() ? val2.userValue.toDouble() : val2.value.toDouble();
 	double d2 = val2.userValue.isValid() ? val2.userValue.toDouble() : val2.value.toDouble();
 
 
 	if (!isfinite(d1) || !isfinite(d2)) {
 	if (!isfinite(d1) || !isfinite(d2)) {
 		blog(LOG_ERROR,
 		blog(LOG_ERROR,
-		     "calc() received at least one invalid value:"
+		     "At least one invalid math value:"
 		     " op1: %f, op2: %f",
 		     " op1: %f, op2: %f",
 		     d1, d2);
 		     d1, d2);
 		return "'Invalid expression'";
 		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;
 	bool isInteger = ceill(val) == val;
@@ -661,8 +699,9 @@ static QString PrepareQSS(const QHash<QString, OBSThemeVariable> &vars, const QS
 
 
 		if (var.type == OBSThemeVariable::Color) {
 		if (var.type == OBSThemeVariable::Color) {
 			replace = value.value<QColor>().name(QColor::HexRgb);
 			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) {
 		} else if (var.type == OBSThemeVariable::Size || var.type == OBSThemeVariable::Number) {
 			double val = value.toDouble();
 			double val = value.toDouble();
 			bool isInteger = ceill(val) == val;
 			bool isInteger = ceill(val) == val;
@@ -747,6 +786,23 @@ static QPalette PreparePalette(const QHash<QString, OBSThemeVariable> &vars, con
 	return pal;
 	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)
 OBSTheme *OBSApp::GetTheme(const QString &name)
 {
 {
 	if (!themes.contains(name))
 	if (!themes.contains(name))
@@ -775,6 +831,22 @@ bool OBSApp::SetTheme(const QString &name)
 	QStringList themeIds(theme->dependencies);
 	QStringList themeIds(theme->dependencies);
 	themeIds << theme->id;
 	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 */
 	/* Find and add high contrast adjustment layer if available */
 	if (HighContrastEnabled()) {
 	if (HighContrastEnabled()) {
 		for (const OBSTheme &theme_ : themes) {
 		for (const OBSTheme &theme_ : themes) {
@@ -805,6 +877,22 @@ bool OBSApp::SetTheme(const QString &name)
 		contents.emplaceBack(content.constData());
 		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 QString stylesheet = PrepareQSS(vars, contents);
 	const QPalette palette = PreparePalette(vars, defaultPalette);
 	const QPalette palette = PreparePalette(vars, defaultPalette);
 	setPalette(palette);
 	setPalette(palette);

+ 68 - 0
frontend/components/AbsoluteSlider.cpp

@@ -1,16 +1,23 @@
 #include "AbsoluteSlider.hpp"
 #include "AbsoluteSlider.hpp"
+
+#include <QPainter>
+
 #include "moc_AbsoluteSlider.cpp"
 #include "moc_AbsoluteSlider.cpp"
 
 
 AbsoluteSlider::AbsoluteSlider(QWidget *parent) : SliderIgnoreScroll(parent)
 AbsoluteSlider::AbsoluteSlider(QWidget *parent) : SliderIgnoreScroll(parent)
 {
 {
 	installEventFilter(this);
 	installEventFilter(this);
 	setMouseTracking(true);
 	setMouseTracking(true);
+
+	tickColor.setRgb(0x5b, 0x62, 0x73);
 }
 }
 
 
 AbsoluteSlider::AbsoluteSlider(Qt::Orientation orientation, QWidget *parent) : SliderIgnoreScroll(orientation, parent)
 AbsoluteSlider::AbsoluteSlider(Qt::Orientation orientation, QWidget *parent) : SliderIgnoreScroll(orientation, parent)
 {
 {
 	installEventFilter(this);
 	installEventFilter(this);
 	setMouseTracking(true);
 	setMouseTracking(true);
+
+	tickColor.setRgb(0x5b, 0x62, 0x73);
 }
 }
 
 
 void AbsoluteSlider::mousePressEvent(QMouseEvent *event)
 void AbsoluteSlider::mousePressEvent(QMouseEvent *event)
@@ -96,3 +103,64 @@ int AbsoluteSlider::posToRangeValue(QMouseEvent *event)
 
 
 	return sliderValue;
 	return sliderValue;
 }
 }
+
+bool AbsoluteSlider::getDisplayTicks() const
+{
+	return displayTicks;
+}
+
+void AbsoluteSlider::setDisplayTicks(bool display)
+{
+	displayTicks = display;
+}
+
+QColor AbsoluteSlider::getTickColor() const
+{
+	return tickColor;
+}
+
+void AbsoluteSlider::setTickColor(QColor c)
+{
+	tickColor = std::move(c);
+}
+
+void AbsoluteSlider::paintEvent(QPaintEvent *event)
+{
+	if (!getDisplayTicks()) {
+		QSlider::paintEvent(event);
+		return;
+	}
+
+	QPainter painter(this);
+
+	QStyleOptionSlider opt;
+	initStyleOption(&opt);
+
+	QRect groove = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this);
+	QRect handle = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this);
+
+	const bool isHorizontal = orientation() == Qt::Horizontal;
+
+	const int sliderLength = isHorizontal ? groove.width() - handle.width() : groove.height() - handle.height();
+	const int handleSize = isHorizontal ? handle.width() : handle.height();
+	const int grooveSize = isHorizontal ? groove.height() : groove.width();
+	const int grooveStart = isHorizontal ? groove.left() : groove.top();
+	const int tickLinePos = isHorizontal ? groove.center().y() : groove.center().x();
+	const int tickLength = std::max((int)(grooveSize * 1.5) + grooveSize, 8 + grooveSize);
+	const int tickLineStart = tickLinePos - (tickLength / 2) + 1;
+
+	for (double offset = minimum(); offset <= maximum(); offset += singleStep()) {
+		double tickPercent = (offset - minimum()) / (maximum() - minimum());
+		const int tickLineOffset = grooveStart + std::floor(sliderLength * tickPercent) + (handleSize / 2);
+
+		const int xPos = isHorizontal ? tickLineOffset : tickLineStart;
+		const int yPos = isHorizontal ? tickLineStart : tickLineOffset;
+
+		const int tickWidth = isHorizontal ? 1 : tickLength;
+		const int tickHeight = isHorizontal ? tickLength : 1;
+
+		painter.fillRect(xPos, yPos, tickWidth, tickHeight, tickColor);
+	}
+
+	QSlider::paintEvent(event);
+}

+ 12 - 0
frontend/components/AbsoluteSlider.hpp

@@ -4,11 +4,18 @@
 
 
 class AbsoluteSlider : public SliderIgnoreScroll {
 class AbsoluteSlider : public SliderIgnoreScroll {
 	Q_OBJECT
 	Q_OBJECT
+	Q_PROPERTY(QColor tickColor READ getTickColor WRITE setTickColor DESIGNABLE true)
 
 
 public:
 public:
 	AbsoluteSlider(QWidget *parent = nullptr);
 	AbsoluteSlider(QWidget *parent = nullptr);
 	AbsoluteSlider(Qt::Orientation orientation, QWidget *parent = nullptr);
 	AbsoluteSlider(Qt::Orientation orientation, QWidget *parent = nullptr);
 
 
+	bool getDisplayTicks() const;
+	void setDisplayTicks(bool display);
+
+	QColor getTickColor() const;
+	void setTickColor(QColor c);
+
 signals:
 signals:
 	void absoluteSliderHovered(int value);
 	void absoluteSliderHovered(int value);
 
 
@@ -20,6 +27,11 @@ protected:
 
 
 	int posToRangeValue(QMouseEvent *event);
 	int posToRangeValue(QMouseEvent *event);
 
 
+	virtual void paintEvent(QPaintEvent *event) override;
+
 private:
 private:
 	bool dragging = false;
 	bool dragging = false;
+	bool displayTicks = false;
+
+	QColor tickColor;
 };
 };

+ 105 - 85
frontend/data/themes/Yami.obt

@@ -93,15 +93,9 @@
 
 
     /* Layout */
     /* Layout */
     /* Configurable Values */
     /* Configurable Values */
-
-    /* TODO: Min 8, Max 12, Step 1 */
-    --font_base_value: 10;
-
-    /* TODO: Min 2, Max 7, Step 1 */
-    --spacing_base_value: 4;
-
-    /* TODO: Min 0.25, Max 10, Step 2 */
-    --padding_base_value: 4;
+    --font_base_value: var(--obsFontScale);
+    --padding_base_value: var(--obsPadding);
+    --spacing_base_value: calc(2 + calc(var(--obsPadding) / 2));
 
 
     /* TODO: Better Accessibility focus state */
     /* TODO: Better Accessibility focus state */
     /* TODO: Move Accessibilty Colors to Theme config system */
     /* TODO: Move Accessibilty Colors to Theme config system */
@@ -111,26 +105,27 @@
     --os_mac_font_base_value: 12;
     --os_mac_font_base_value: 12;
 
 
     --font_base: calc(1pt * var(--font_base_value));
     --font_base: calc(1pt * var(--font_base_value));
-    --font_small: calc(0.9pt * var(--font_base_value));
-    --font_xsmall: calc(0.85pt * var(--font_base_value));
+    --font_small: max(7pt, calc(0.8pt * var(--font_base_value)));
+    --font_xsmall: max(6.25pt, calc(0.85pt * var(--font_base_value)));
     --font_large: calc(1.1pt * var(--font_base_value));
     --font_large: calc(1.1pt * var(--font_base_value));
     --font_xlarge: calc(1.5pt * var(--font_base_value));
     --font_xlarge: calc(1.5pt * var(--font_base_value));
 
 
     --font_heading: calc(2.5pt * var(--font_base_value));
     --font_heading: calc(2.5pt * var(--font_base_value));
 
 
-    --icon_base: calc(6px + var(--font_base_value));
+    --icon_base: calc(calc(max(2, var(--obsPadding)) * 1px) + 12px);
 
 
-    --spacing_base: calc(0.5px * var(--spacing_base_value));
-    --spacing_large: calc(1px * var(--spacing_base_value));
-    --spacing_small: calc(0.25px * var(--spacing_base_value));
+    --spacing_base: min(max(1px, calc(0.4 * var(--spacing_base_value))), 2px);
+    --spacing_large: min(max(2px, calc(1px * var(--spacing_base_value))), 4px);
+    --spacing_small: max(1px, calc(0.25px * var(--spacing_base_value)));
     --spacing_title: 4px;
     --spacing_title: 4px;
 
 
     --padding_base: calc(0.5px * var(--padding_base_value));
     --padding_base: calc(0.5px * var(--padding_base_value));
-    --padding_large: calc(1px * var(--padding_base_value));
-    --padding_xlarge: calc(1.75px * var(--padding_base_value));
-    --padding_small: calc(0.25px * var(--padding_base_value));
+    --padding_large: min(max(1px, calc(1px * var(--padding_base_value))), 5px);
+    --padding_xlarge: min(max(2px, calc(1.75px * var(--padding_base_value))), 10px);
+    --padding_small: max(0px, calc(0.25px * var(--padding_base_value)));
 
 
-    --padding_wide: calc(8px + calc(2 * var(--padding_base_value)));
+    --padding_container: max(4px, var(--padding_base));
+    --padding_wide: min(calc(12px + max(var(--padding_base_value), 4)), 24px);
     --padding_menu: calc(4px + calc(2 * var(--padding_base_value)));
     --padding_menu: calc(4px + calc(2 * var(--padding_base_value)));
 
 
     --padding_base_border: calc(var(--padding_base) + 1px);
     --padding_base_border: calc(var(--padding_base) + 1px);
@@ -154,9 +149,10 @@
     --input_font_scale: calc(var(--font_base_value) * 2.2);
     --input_font_scale: calc(var(--font_base_value) * 2.2);
     --input_font_padding: calc(var(--padding_base_value) * 2);
     --input_font_padding: calc(var(--padding_base_value) * 2);
 
 
-    --input_height_base: calc(var(--input_font_scale) + var(--input_font_padding));
-    --input_padding: var(--padding_large);
-    --input_height: calc(var(--input_height_base) - calc(var(--input_padding) * 2));
+    --input_height_base: max(calc(var(--input_font_scale) + var(--input_font_padding)), 24);
+    --input_padding: calc(2px + var(--padding_base));
+    --input_text_padding: max(calc(6px + var(--padding_base)), 8px);
+    --input_height: calc(var(--input_height_base) - calc(var(--input_padding) * 2px));
     --input_height_half: calc(var(--input_height_base) / 2);
     --input_height_half: calc(var(--input_height_base) / 2);
 
 
     --input_bg: var(--grey4);
     --input_bg: var(--grey4);
@@ -196,6 +192,8 @@
     --scrollbar_down: var(--grey8);
     --scrollbar_down: var(--grey8);
     --scrollbar_border: var(--grey2);
     --scrollbar_border: var(--grey2);
 
 
+    --preview_scale_width: calc(calc(var(--input_text_padding) * 3.5) * calc(var(--font_base_value) / 10));
+
     --separator_hover: var(--white1);
     --separator_hover: var(--white1);
 
 
     --highlight: rgb(42, 130, 218);
     --highlight: rgb(42, 130, 218);
@@ -449,6 +447,9 @@ QListWidget QWidget {
     border: 1px solid var(--bg_base);
     border: 1px solid var(--bg_base);
 }
 }
 
 
+* {
+    spacing: var(--spacing_small);
+}
 
 
 /* Misc */
 /* Misc */
 
 
@@ -612,7 +613,7 @@ OBSDock > QWidget {
 }
 }
 
 
 #transitionsFrame {
 #transitionsFrame {
-    padding: var(--padding_large);
+    padding: var(--padding_container);
 }
 }
 
 
 OBSDock QLabel {
 OBSDock QLabel {
@@ -661,16 +662,15 @@ QScrollArea {
  * oversize it and use margin to crunch it back down
  * oversize it and use margin to crunch it back down
  */
  */
 OBSBasicStatusBar {
 OBSBasicStatusBar {
-    margin-top: 4px;
+    margin-top: var(--spacing_large);
     border-top: 1px solid var(--border_color);
     border-top: 1px solid var(--border_color);
     background: var(--bg_base);
     background: var(--bg_base);
 }
 }
 
 
 StatusBarWidget > QFrame {
 StatusBarWidget > QFrame {
-    margin-top: 1px;
     border: 0px solid var(--border_color);
     border: 0px solid var(--border_color);
     border-left-width: 1px;
     border-left-width: 1px;
-    padding: 0px 8px 2px;
+    padding: 0px var(--padding_xlarge) var(--padding_small);
 }
 }
 
 
 /* Group Box */
 /* Group Box */
@@ -803,6 +803,7 @@ QToolBar {
     background-color: transparent;
     background-color: transparent;
     border: none;
     border: none;
     margin: var(--spacing_base) 0px;
     margin: var(--spacing_base) 0px;
+    spacing: var(--spacing_base);
 }
 }
 
 
 QToolBarExtension {
 QToolBarExtension {
@@ -893,11 +894,10 @@ QTabBar QToolButton {
 QComboBox,
 QComboBox,
 QDateTimeEdit {
 QDateTimeEdit {
     background-color: var(--input_bg);
     background-color: var(--input_bg);
-    border-style: solid;
     border: 1px solid var(--input_bg);
     border: 1px solid var(--input_bg);
     border-radius: var(--border_radius);
     border-radius: var(--border_radius);
-    padding: var(--padding_large) var(--padding_large);
-    padding-left: 10px;
+    padding: var(--input_padding) var(--input_text_padding);
+    height: var(--input_height);
 }
 }
 
 
 QComboBox QAbstractItemView {
 QComboBox QAbstractItemView {
@@ -974,8 +974,7 @@ QPlainTextEdit {
     background-color: var(--input_bg);
     background-color: var(--input_bg);
     border: none;
     border: none;
     border-radius: var(--border_radius);
     border-radius: var(--border_radius);
-    padding: var(--input_padding) var(--padding_small) var(--input_padding) var(--input_padding);
-    padding-left: 8px;
+    padding: var(--input_padding) var(--input_text_padding);
     border: 1px solid var(--input_bg);
     border: 1px solid var(--input_bg);
     height: var(--input_height);
     height: var(--input_height);
 }
 }
@@ -994,6 +993,13 @@ QPlainTextEdit:focus {
     border-color: var(--input_border_focus);
     border-color: var(--input_border_focus);
 }
 }
 
 
+QLineEdit:read-only,
+QLineEdit:read-only:hover,
+QLineEdit:read-only:focus {
+    background-color: transparent;
+    border-color: var(--input_bg);
+}
+
 QTextEdit:!editable,
 QTextEdit:!editable,
 QTextEdit:!editable:hover,
 QTextEdit:!editable:hover,
 QTextEdit:!editable:focus {
 QTextEdit:!editable:focus {
@@ -1007,8 +1013,8 @@ QDoubleSpinBox {
     background-color: var(--input_bg);
     background-color: var(--input_bg);
     border: 1px solid var(--input_bg);
     border: 1px solid var(--input_bg);
     border-radius: var(--border_radius);
     border-radius: var(--border_radius);
-    padding: var(--input_padding) 0px var(--input_padding) var(--input_padding);
-    padding-left: 8px;
+    padding: var(--input_padding) var(--input_text_padding);
+    height: var(--input_height);
     max-height: var(--input_height);
     max-height: var(--input_height);
 }
 }
 
 
@@ -1096,7 +1102,7 @@ QDoubleSpinBox::down-arrow {
 
 
 /* Controls Dock */
 /* Controls Dock */
 #controlsFrame {
 #controlsFrame {
-    padding: var(--padding_large);
+    padding: var(--padding_container);
 }
 }
 
 
 #controlsFrame QPushButton {
 #controlsFrame QPushButton {
@@ -1143,17 +1149,58 @@ QDoubleSpinBox::down-arrow {
 /* Buttons */
 /* Buttons */
 
 
 QPushButton {
 QPushButton {
-    color: var(--text);
     background-color: var(--button_bg);
     background-color: var(--button_bg);
+    color: var(--text);
+    border: 1px solid var(--button_border);
     border-radius: var(--border_radius);
     border-radius: var(--border_radius);
     height: var(--input_height);
     height: var(--input_height);
     max-height: var(--input_height);
     max-height: var(--input_height);
+    margin-top: var(--spacing_input);
+    margin-bottom: var(--spacing_input);
     padding: var(--input_padding) var(--padding_wide);
     padding: var(--input_padding) var(--padding_wide);
     icon-size: var(--icon_base);
     icon-size: var(--icon_base);
+    outline: none;
 }
 }
 
 
-QPushButton {
-    border: 1px solid var(--button_border);
+QPushButton:hover {
+    background-color: var(--button_bg_hover);
+}
+
+QPushButton:hover,
+QPushButton:focus {
+    border-color: var(--button_border_hover);
+}
+
+QPushButton::flat {
+    background-color: var(--button_bg);
+}
+
+QPushButton:checked {
+    background-color: var(--primary);
+    border-color: var(--primary_light);
+}
+
+QPushButton:checked:hover,
+QPushButton:checked:focus {
+    border-color: var(--primary_lighter);
+}
+
+QPushButton:pressed,
+QPushButton:pressed:hover {
+    background-color: var(--button_bg_down);
+    border-color: var(--button_border);
+}
+
+QPushButton:disabled {
+    background-color: var(--button_bg_disabled);
+    border-color: var(--button_border);
+}
+
+QPushButton::menu-indicator {
+    image: url(theme:Dark/down.svg);
+    subcontrol-position: right;
+    subcontrol-origin: padding;
+    width: 25px;
 }
 }
 
 
 QToolButton {
 QToolButton {
@@ -1164,7 +1211,7 @@ QToolButton,
 .btn-tool {
 .btn-tool {
     background-color: var(--button_bg);
     background-color: var(--button_bg);
     padding: var(--padding_base) var(--padding_base);
     padding: var(--padding_base) var(--padding_base);
-    margin: 0px var(--spacing_base);
+    margin: 0px 0px;
     border: 1px solid var(--button_border);
     border: 1px solid var(--button_border);
     border-radius: var(--border_radius);
     border-radius: var(--border_radius);
     icon-size: var(--icon_base);
     icon-size: var(--icon_base);
@@ -1175,15 +1222,6 @@ QToolButton:last-child,
     margin-right: 0px;
     margin-right: 0px;
 }
 }
 
 
-QPushButton:hover,
-QPushButton:focus {
-    border-color: var(--button_border_hover);
-}
-
-QPushButton:hover {
-    background-color: var(--button_bg_hover);
-}
-
 QToolButton:hover,
 QToolButton:hover,
 QToolButton:focus,
 QToolButton:focus,
 .btn-tool:hover,
 .btn-tool:hover,
@@ -1194,25 +1232,6 @@ QToolButton:focus,
     background-color: var(--button_bg_hover);
     background-color: var(--button_bg_hover);
 }
 }
 
 
-QPushButton::flat {
-    background-color: var(--button_bg);
-}
-
-QPushButton:checked {
-    background-color: var(--primary);
-}
-
-QPushButton:checked:hover,
-QPushButton:checked:focus {
-    border-color: var(--primary_lighter);
-}
-
-QPushButton:pressed,
-QPushButton:pressed:hover {
-    background-color: var(--button_bg_down);
-    border-color: var(--button_border);
-}
-
 QToolButton:pressed,
 QToolButton:pressed,
 QToolButton:pressed:hover,
 QToolButton:pressed:hover,
 .btn-tool:pressed,
 .btn-tool:pressed,
@@ -1221,24 +1240,12 @@ QToolButton:pressed:hover,
     border-color: var(--button_border);
     border-color: var(--button_border);
 }
 }
 
 
-QPushButton:disabled {
-    background-color: var(--button_bg_disabled);
-    border-color: var(--button_border);
-}
-
 QToolButton:disabled,
 QToolButton:disabled,
 .btn-tool:disabled {
 .btn-tool:disabled {
     background-color: var(--button_bg_disabled);
     background-color: var(--button_bg_disabled);
     border-color: transparent;
     border-color: transparent;
 }
 }
 
 
-QPushButton::menu-indicator {
-    image: url(theme:Dark/down.svg);
-    subcontrol-position: right;
-    subcontrol-origin: padding;
-    width: 25px;
-}
-
 /* Sliders */
 /* Sliders */
 
 
 QSlider::groove {
 QSlider::groove {
@@ -1309,7 +1316,7 @@ QSlider::handle:hover {
 }
 }
 
 
 QSlider::handle:pressed {
 QSlider::handle:pressed {
-    background-color: var(--white5);
+    background-color: var(--white3);
 }
 }
 
 
 QSlider::handle:disabled {
 QSlider::handle:disabled {
@@ -1349,6 +1356,15 @@ QSlider::handle:disabled {
     border-bottom: 1px solid #3c404b;
     border-bottom: 1px solid #3c404b;
 }
 }
 
 
+VolControl {
+    background: var(--bg_base);
+}
+
+VolControl QLabel {
+    font-size: var(--font_small);
+    margin: var(--spacing_small) 0px;
+}
+
 VolControl #volLabel {
 VolControl #volLabel {
     padding: var(--padding_base) 0px var(--padding_base);
     padding: var(--padding_base) 0px var(--padding_base);
     text-align: center;
     text-align: center;
@@ -1377,7 +1393,7 @@ VolControl #volLabel {
 }
 }
 
 
 #vMixerScrollArea VolControl {
 #vMixerScrollArea VolControl {
-    padding: var(--padding_large) 0px var(--padding_base);
+    padding: var(--padding_container) 0px var(--padding_container);
     border-right: 1px solid var(--border_color);
     border-right: 1px solid var(--border_color);
 }
 }
 
 
@@ -1407,6 +1423,7 @@ VolControl #volLabel {
 }
 }
 
 
 #vMixerScrollArea VolControl QPushButton {
 #vMixerScrollArea VolControl QPushButton {
+    margin-left: var(--spacing_base);
     margin-right: var(--padding_xlarge);
     margin-right: var(--padding_xlarge);
 }
 }
 
 
@@ -1414,10 +1431,6 @@ VolControl #volLabel {
     margin-left: var(--padding_xlarge);
     margin-left: var(--padding_xlarge);
 }
 }
 
 
-VolControl {
-    background: var(--bg_base);
-}
-
 VolumeMeter {
 VolumeMeter {
     background: transparent;
     background: transparent;
 }
 }
@@ -1527,6 +1540,7 @@ QGroupBox::indicator,
 QTableView::indicator {
 QTableView::indicator {
     width: var(--icon_base);
     width: var(--icon_base);
     height: var(--icon_base);
     height: var(--icon_base);
+    margin-right: var(--spacing_large);
 }
 }
 
 
 QGroupBox::indicator {
 QGroupBox::indicator {
@@ -1952,7 +1966,7 @@ OBSBasicAdvAudio #scrollAreaWidgetContents {
     font-size: var(--font_xsmall);
     font-size: var(--font_xsmall);
     height: 14px;
     height: 14px;
     max-height: 14px;
     max-height: 14px;
-    padding: 0px var(--padding_xlarge);
+    padding: 0px;
     margin: 0;
     margin: 0;
     border: none;
     border: none;
     border-radius: 0;
     border-radius: 0;
@@ -1962,7 +1976,13 @@ OBSBasicAdvAudio #scrollAreaWidgetContents {
     border: 1px solid var(--grey6);
     border: 1px solid var(--grey6);
 }
 }
 
 
+#previewScalePercent {
+    padding: 0px var(--input_text_padding);
+    min-width: var(--preview_scale_width);
+}
+
 #previewScalingMode {
 #previewScalingMode {
+    padding: 0px var(--input_text_padding);
     border: 1px solid var(--grey6);
     border: 1px solid var(--grey6);
 }
 }
 
 

+ 8 - 15
frontend/data/themes/Yami_Classic.ovt

@@ -24,23 +24,13 @@
     --primary_light: rgb(33,71,109);
     --primary_light: rgb(33,71,109);
 
 
     /* Layout */
     /* Layout */
-    --font_base_value: 9;
-    --spacing_base_value: 2;
-    --padding_base_value: 0.25;
+    --font_small: max(7pt, calc(0.5pt * var(--font_base_value)));
 
 
-    /* OS Fixes */
-    --os_mac_font_base_value: 11;
+    --padding_large: min(max(0px, calc(1px * var(--padding_base_value))), 5px);
 
 
-    --font_small: calc(0.75pt * var(--font_base_value));
+    --padding_container: max(2px, var(--padding_base));
 
 
-    --icon_base: calc(6px + var(--font_base_value));
-
-    --padding_xlarge: calc(2px + calc(0.5px * var(--padding_base_value)));
-
-    --padding_wide: calc(18px + calc(0.25 * var(--padding_base_value)));
-    --padding_menu: calc(8px + calc(1 * var(--padding_base_value)));
-
-    --input_height_base: calc(1px + calc(var(--input_font_scale) + var(--input_font_padding)));
+    /* Inputs / Controls */
 
 
     --border_color: var(--grey6);
     --border_color: var(--grey6);
 
 
@@ -48,6 +38,10 @@
     --border_radius_small: 1px;
     --border_radius_small: 1px;
     --border_radius_large: 2px;
     --border_radius_large: 2px;
 
 
+    --input_height_base: max(calc(var(--input_font_scale) + var(--input_font_padding)), 20);
+    --input_padding: calc(0px + var(--padding_base));
+    --input_text_padding: max(calc(6px + var(--padding_base)), 8px);
+
     --input_bg: var(--grey4);
     --input_bg: var(--grey4);
     --input_bg_hover: var(--grey1);
     --input_bg_hover: var(--grey1);
     --input_bg_focus: var(--grey6);
     --input_bg_focus: var(--grey6);
@@ -263,7 +257,6 @@ QPushButton[toolButton="true"] {
 
 
 #vMixerScrollArea QLabel {
 #vMixerScrollArea QLabel {
   font-size: var(--font_small);
   font-size: var(--font_small);
-  margin: var(--padding_xlarge) 0px;
 }
 }
 
 
 #vMixerScrollArea #volLabel {
 #vMixerScrollArea #volLabel {

+ 237 - 10
frontend/forms/OBSBasicSettings.ui

@@ -979,7 +979,7 @@
                 <item>
                 <item>
                  <widget class="QGroupBox" name="appearanceGeneral">
                  <widget class="QGroupBox" name="appearanceGeneral">
                   <property name="title">
                   <property name="title">
-                   <string>Basic.Settings.Appearance.General</string>
+                   <string>Basic.Settings.Appearance</string>
                   </property>
                   </property>
                   <property name="checkable">
                   <property name="checkable">
                    <bool>false</bool>
                    <bool>false</bool>
@@ -1021,6 +1021,200 @@
                     <widget class="QComboBox" name="themeVariant"/>
                     <widget class="QComboBox" name="themeVariant"/>
                    </item>
                    </item>
                    <item row="2" column="0">
                    <item row="2" column="0">
+                    <widget class="QLabel" name="appearanceSettingLabelFontScale">
+                     <property name="text">
+                      <string>Font Size</string>
+                     </property>
+                     <property name="buddy">
+                      <cstring>appearanceFontScale</cstring>
+                     </property>
+                    </widget>
+                   </item>
+                   <item row="2" column="1">
+                    <widget class="QFrame" name="frame_2">
+                     <property name="frameShape">
+                      <enum>QFrame::NoFrame</enum>
+                     </property>
+                     <property name="frameShadow">
+                      <enum>QFrame::Plain</enum>
+                     </property>
+                     <property name="lineWidth">
+                      <number>0</number>
+                     </property>
+                     <layout class="QHBoxLayout" name="horizontalLayout_23" stretch="0,5">
+                      <property name="leftMargin">
+                       <number>0</number>
+                      </property>
+                      <property name="topMargin">
+                       <number>0</number>
+                      </property>
+                      <property name="rightMargin">
+                       <number>0</number>
+                      </property>
+                      <property name="bottomMargin">
+                       <number>0</number>
+                      </property>
+                      <item>
+                       <widget class="QLineEdit" name="appearanceFontScaleText">
+                        <property name="sizePolicy">
+                         <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+                          <horstretch>0</horstretch>
+                          <verstretch>0</verstretch>
+                         </sizepolicy>
+                        </property>
+                        <property name="focusPolicy">
+                         <enum>Qt::NoFocus</enum>
+                        </property>
+                        <property name="text">
+                         <string>10</string>
+                        </property>
+                        <property name="alignment">
+                         <set>Qt::AlignCenter</set>
+                        </property>
+                        <property name="readOnly">
+                         <bool>true</bool>
+                        </property>
+                        <property name="clearButtonEnabled">
+                         <bool>false</bool>
+                        </property>
+                       </widget>
+                      </item>
+                      <item>
+                       <widget class="AbsoluteSlider" name="appearanceFontScale">
+                        <property name="sizePolicy">
+                         <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+                          <horstretch>0</horstretch>
+                          <verstretch>0</verstretch>
+                         </sizepolicy>
+                        </property>
+                        <property name="minimum">
+                         <number>8</number>
+                        </property>
+                        <property name="maximum">
+                         <number>12</number>
+                        </property>
+                        <property name="pageStep">
+                         <number>2</number>
+                        </property>
+                        <property name="value">
+                         <number>10</number>
+                        </property>
+                        <property name="orientation">
+                         <enum>Qt::Horizontal</enum>
+                        </property>
+                        <property name="tickPosition">
+                         <enum>QSlider::TicksBothSides</enum>
+                        </property>
+                        <property name="tickInterval">
+                         <number>1</number>
+                        </property>
+                       </widget>
+                      </item>
+                     </layout>
+                    </widget>
+                   </item>
+                   <item row="3" column="0">
+                    <widget class="QLabel" name="label_20">
+                     <property name="text">
+                      <string>Density</string>
+                     </property>
+                    </widget>
+                   </item>
+                   <item row="3" column="1">
+                    <widget class="QFrame" name="frame_5">
+                     <property name="frameShape">
+                      <enum>QFrame::NoFrame</enum>
+                     </property>
+                     <property name="frameShadow">
+                      <enum>QFrame::Plain</enum>
+                     </property>
+                     <property name="lineWidth">
+                      <number>0</number>
+                     </property>
+                     <layout class="QHBoxLayout" name="horizontalLayout_34">
+                      <property name="leftMargin">
+                       <number>0</number>
+                      </property>
+                      <property name="topMargin">
+                       <number>0</number>
+                      </property>
+                      <property name="rightMargin">
+                       <number>0</number>
+                      </property>
+                      <property name="bottomMargin">
+                       <number>0</number>
+                      </property>
+                      <item>
+                       <widget class="QPushButton" name="appearanceDensity1">
+                        <property name="text">
+                         <string>Classic</string>
+                        </property>
+                        <property name="checkable">
+                         <bool>true</bool>
+                        </property>
+                        <property name="autoExclusive">
+                         <bool>true</bool>
+                        </property>
+                        <attribute name="buttonGroup">
+                         <string notr="true">appearanceDensityButtonGroup</string>
+                        </attribute>
+                       </widget>
+                      </item>
+                      <item>
+                       <widget class="QPushButton" name="appearanceDensity2">
+                        <property name="text">
+                         <string>Compact</string>
+                        </property>
+                        <property name="checkable">
+                         <bool>true</bool>
+                        </property>
+                        <property name="autoExclusive">
+                         <bool>true</bool>
+                        </property>
+                        <attribute name="buttonGroup">
+                         <string notr="true">appearanceDensityButtonGroup</string>
+                        </attribute>
+                       </widget>
+                      </item>
+                      <item>
+                       <widget class="QPushButton" name="appearanceDensity3">
+                        <property name="text">
+                         <string>Normal</string>
+                        </property>
+                        <property name="checkable">
+                         <bool>true</bool>
+                        </property>
+                        <property name="checked">
+                         <bool>true</bool>
+                        </property>
+                        <property name="autoExclusive">
+                         <bool>true</bool>
+                        </property>
+                        <attribute name="buttonGroup">
+                         <string notr="true">appearanceDensityButtonGroup</string>
+                        </attribute>
+                       </widget>
+                      </item>
+                      <item>
+                       <widget class="QPushButton" name="appearanceDensity4">
+                        <property name="text">
+                         <string>Comfortable</string>
+                        </property>
+                        <property name="checkable">
+                         <bool>true</bool>
+                        </property>
+                        <property name="autoExclusive">
+                         <bool>true</bool>
+                        </property>
+                        <attribute name="buttonGroup">
+                         <string notr="true">appearanceDensityButtonGroup</string>
+                        </attribute>
+                       </widget>
+                      </item>
+                     </layout>
+                    </widget>
+                   </item>
+                   <item row="4" column="0">
                     <spacer name="horizontalSpacer_17">
                     <spacer name="horizontalSpacer_17">
                      <property name="orientation">
                      <property name="orientation">
                       <enum>Qt::Horizontal</enum>
                       <enum>Qt::Horizontal</enum>
@@ -1028,7 +1222,7 @@
                      <property name="sizeHint" stdset="0">
                      <property name="sizeHint" stdset="0">
                       <size>
                       <size>
                        <width>170</width>
                        <width>170</width>
-                       <height>0</height>
+                       <height>10</height>
                       </size>
                       </size>
                      </property>
                      </property>
                     </spacer>
                     </spacer>
@@ -1049,6 +1243,31 @@
                   </property>
                   </property>
                  </spacer>
                  </spacer>
                 </item>
                 </item>
+                <item>
+                 <widget class="QFrame" name="appearanceOptionsWarning">
+                  <property name="frameShape">
+                   <enum>QFrame::NoFrame</enum>
+                  </property>
+                  <property name="frameShadow">
+                   <enum>QFrame::Plain</enum>
+                  </property>
+                  <property name="lineWidth">
+                   <number>0</number>
+                  </property>
+                  <layout class="QVBoxLayout" name="verticalLayout_35">
+                   <item>
+                    <widget class="QLabel" name="appearanceOptionsWarningLabel">
+                     <property name="text">
+                      <string>Some appearance options are not available for this style.</string>
+                     </property>
+                     <property name="class" stdset="0">
+                      <string>text-warning</string>
+                     </property>
+                    </widget>
+                   </item>
+                  </layout>
+                 </widget>
+                </item>
                </layout>
                </layout>
               </widget>
               </widget>
              </item>
              </item>
@@ -2898,8 +3117,8 @@
                         <rect>
                         <rect>
                          <x>0</x>
                          <x>0</x>
                          <y>0</y>
                          <y>0</y>
-                         <width>766</width>
-                         <height>592</height>
+                         <width>424</width>
+                         <height>175</height>
                         </rect>
                         </rect>
                        </property>
                        </property>
                        <layout class="QVBoxLayout" name="verticalLayout_14">
                        <layout class="QVBoxLayout" name="verticalLayout_14">
@@ -3304,8 +3523,8 @@
                             <rect>
                             <rect>
                              <x>0</x>
                              <x>0</x>
                              <y>0</y>
                              <y>0</y>
-                             <width>766</width>
-                             <height>558</height>
+                             <width>509</width>
+                             <height>371</height>
                             </rect>
                             </rect>
                            </property>
                            </property>
                            <property name="sizePolicy">
                            <property name="sizePolicy">
@@ -3945,8 +4164,8 @@
                             <rect>
                             <rect>
                              <x>0</x>
                              <x>0</x>
                              <y>0</y>
                              <y>0</y>
-                             <width>766</width>
-                             <height>558</height>
+                             <width>625</width>
+                             <height>489</height>
                             </rect>
                             </rect>
                            </property>
                            </property>
                            <layout class="QVBoxLayout" name="verticalLayout_27">
                            <layout class="QVBoxLayout" name="verticalLayout_27">
@@ -4495,8 +4714,8 @@
                            <rect>
                            <rect>
                             <x>0</x>
                             <x>0</x>
                             <y>0</y>
                             <y>0</y>
-                            <width>766</width>
-                            <height>592</height>
+                            <width>258</width>
+                            <height>510</height>
                            </rect>
                            </rect>
                           </property>
                           </property>
                           <layout class="QVBoxLayout" name="verticalLayout_28">
                           <layout class="QVBoxLayout" name="verticalLayout_28">
@@ -8479,6 +8698,11 @@
    <extends>QLineEdit</extends>
    <extends>QLineEdit</extends>
    <header>settings/OBSHotkeyEdit.hpp</header>
    <header>settings/OBSHotkeyEdit.hpp</header>
   </customwidget>
   </customwidget>
+  <customwidget>
+   <class>AbsoluteSlider</class>
+   <extends>QSlider</extends>
+   <header>components/AbsoluteSlider.hpp</header>
+  </customwidget>
  </customwidgets>
  </customwidgets>
  <tabstops>
  <tabstops>
   <tabstop>listWidget</tabstop>
   <tabstop>listWidget</tabstop>
@@ -9058,4 +9282,7 @@
    </hints>
    </hints>
   </connection>
   </connection>
  </connections>
  </connections>
+ <buttongroups>
+  <buttongroup name="appearanceDensityButtonGroup"/>
+ </buttongroups>
 </ui>
 </ui>

+ 6 - 0
frontend/settings/OBSBasicSettings.cpp

@@ -297,6 +297,7 @@ void RestrictResetBitrates(initializer_list<QComboBox *> boxes, int maxbitrate);
 #define SCROLL_CHANGED  &QSpinBox::valueChanged
 #define SCROLL_CHANGED  &QSpinBox::valueChanged
 #define DSCROLL_CHANGED &QDoubleSpinBox::valueChanged
 #define DSCROLL_CHANGED &QDoubleSpinBox::valueChanged
 #define TEXT_CHANGED    &QPlainTextEdit::textChanged
 #define TEXT_CHANGED    &QPlainTextEdit::textChanged
+#define SLIDER_CHANGED  &QSlider::valueChanged
 
 
 #define GENERAL_CHANGED &OBSBasicSettings::GeneralChanged
 #define GENERAL_CHANGED &OBSBasicSettings::GeneralChanged
 #define STREAM1_CHANGED &OBSBasicSettings::Stream1Changed
 #define STREAM1_CHANGED &OBSBasicSettings::Stream1Changed
@@ -368,6 +369,11 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent)
 	HookWidget(ui->multiviewLayout,      COMBO_CHANGED,  GENERAL_CHANGED);
 	HookWidget(ui->multiviewLayout,      COMBO_CHANGED,  GENERAL_CHANGED);
 	HookWidget(ui->theme, 		     COMBO_CHANGED,  APPEAR_CHANGED);
 	HookWidget(ui->theme, 		     COMBO_CHANGED,  APPEAR_CHANGED);
 	HookWidget(ui->themeVariant,	     COMBO_CHANGED,  APPEAR_CHANGED);
 	HookWidget(ui->themeVariant,	     COMBO_CHANGED,  APPEAR_CHANGED);
+	HookWidget(ui->appearanceFontScale,  SLIDER_CHANGED, APPEAR_CHANGED);
+	HookWidget(ui->appearanceDensity1,   CHECK_CHANGED,  APPEAR_CHANGED);
+	HookWidget(ui->appearanceDensity2,   CHECK_CHANGED,  APPEAR_CHANGED);
+	HookWidget(ui->appearanceDensity3,   CHECK_CHANGED,  APPEAR_CHANGED);
+	HookWidget(ui->appearanceDensity4,   CHECK_CHANGED,  APPEAR_CHANGED);
 	HookWidget(ui->service,              COMBO_CHANGED,  STREAM1_CHANGED);
 	HookWidget(ui->service,              COMBO_CHANGED,  STREAM1_CHANGED);
 	HookWidget(ui->server,               COMBO_CHANGED,  STREAM1_CHANGED);
 	HookWidget(ui->server,               COMBO_CHANGED,  STREAM1_CHANGED);
 	HookWidget(ui->customServer,         EDIT_CHANGED,   STREAM1_CHANGED);
 	HookWidget(ui->customServer,         EDIT_CHANGED,   STREAM1_CHANGED);

+ 3 - 0
frontend/settings/OBSBasicSettings.hpp

@@ -225,6 +225,8 @@ private:
 
 
 	/* Appearance */
 	/* Appearance */
 	void InitAppearancePage();
 	void InitAppearancePage();
+	void enableAppearanceFontControls(bool enable);
+	void enableAppearanceDensityControls(bool enable);
 
 
 	bool IsCustomServer();
 	bool IsCustomServer();
 
 
@@ -346,6 +348,7 @@ private:
 private slots:
 private slots:
 	void on_theme_activated(int idx);
 	void on_theme_activated(int idx);
 	void on_themeVariant_activated(int idx);
 	void on_themeVariant_activated(int idx);
+	void updateAppearanceControls();
 
 
 	void on_listWidget_itemSelectionChanged();
 	void on_listWidget_itemSelectionChanged();
 	void on_buttonBox_clicked(QAbstractButton *button);
 	void on_buttonBox_clicked(QAbstractButton *button);

+ 53 - 0
frontend/settings/OBSBasicSettings_Appearance.cpp

@@ -21,6 +21,15 @@ void OBSBasicSettings::InitAppearancePage()
 		ui->theme->setCurrentIndex(idx);
 		ui->theme->setCurrentIndex(idx);
 
 
 	ui->themeVariant->setPlaceholderText(QTStr("Basic.Settings.Appearance.General.NoVariant"));
 	ui->themeVariant->setPlaceholderText(QTStr("Basic.Settings.Appearance.General.NoVariant"));
+
+	ui->appearanceFontScale->setDisplayTicks(true);
+
+	connect(ui->appearanceFontScale, &QSlider::valueChanged, ui->appearanceFontScaleText,
+		[this](int value) { ui->appearanceFontScaleText->setText(QString::number(value)); });
+	ui->appearanceFontScaleText->setText(QString::number(ui->appearanceFontScale->value()));
+
+	connect(App(), &OBSApp::StyleChanged, this, &OBSBasicSettings::updateAppearanceControls);
+	updateAppearanceControls();
 }
 }
 
 
 void OBSBasicSettings::LoadThemeList(bool reload)
 void OBSBasicSettings::LoadThemeList(bool reload)
@@ -83,6 +92,16 @@ void OBSBasicSettings::LoadAppearanceSettings(bool reload)
 
 
 		App()->SetTheme(themeId);
 		App()->SetTheme(themeId);
 	}
 	}
+
+	int fontScale = config_get_int(App()->GetUserConfig(), "Appearance", "FontScale");
+	ui->appearanceFontScale->setValue(fontScale);
+
+	int densityId = config_get_int(App()->GetUserConfig(), "Appearance", "Density");
+	QAbstractButton *densityButton = ui->appearanceDensityButtonGroup->button(densityId);
+	if (densityButton) {
+		densityButton->setChecked(true);
+	}
+	updateAppearanceControls();
 }
 }
 
 
 void OBSBasicSettings::SaveAppearanceSettings()
 void OBSBasicSettings::SaveAppearanceSettings()
@@ -93,6 +112,13 @@ void OBSBasicSettings::SaveAppearanceSettings()
 	if (savedTheme != currentTheme) {
 	if (savedTheme != currentTheme) {
 		config_set_string(config, "Appearance", "Theme", QT_TO_UTF8(currentTheme->id));
 		config_set_string(config, "Appearance", "Theme", QT_TO_UTF8(currentTheme->id));
 	}
 	}
+
+	config_set_int(config, "Appearance", "FontScale", ui->appearanceFontScale->value());
+
+	int densityId = ui->appearanceDensityButtonGroup->checkedId();
+	config_set_int(config, "Appearance", "Density", densityId);
+
+	App()->SetTheme(currentTheme->id);
 }
 }
 
 
 void OBSBasicSettings::on_theme_activated(int)
 void OBSBasicSettings::on_theme_activated(int)
@@ -104,3 +130,30 @@ void OBSBasicSettings::on_themeVariant_activated(int)
 {
 {
 	LoadAppearanceSettings(true);
 	LoadAppearanceSettings(true);
 }
 }
+
+void OBSBasicSettings::updateAppearanceControls()
+{
+	OBSTheme *theme = App()->GetTheme();
+	enableAppearanceFontControls(theme->usesFontScale);
+	enableAppearanceDensityControls(theme->usesDensity);
+	if (!theme->usesFontScale || !theme->usesDensity) {
+		ui->appearanceOptionsWarning->setVisible(true);
+	} else {
+		ui->appearanceOptionsWarning->setVisible(false);
+	}
+	style()->polish(ui->appearanceOptionsWarningLabel);
+}
+
+void OBSBasicSettings::enableAppearanceFontControls(bool enable)
+{
+	ui->appearanceFontScale->setEnabled(enable);
+	ui->appearanceFontScaleText->setEnabled(enable);
+}
+
+void OBSBasicSettings::enableAppearanceDensityControls(bool enable)
+{
+	const QList<QAbstractButton *> buttons = ui->appearanceDensityButtonGroup->buttons();
+	for (QAbstractButton *button : buttons) {
+		button->setEnabled(enable);
+	}
+}

+ 3 - 0
frontend/utility/OBSTheme.hpp

@@ -41,4 +41,7 @@ struct OBSTheme {
 	bool isVisible;      /* Whether it should be shown to the user */
 	bool isVisible;      /* Whether it should be shown to the user */
 	bool isBaseTheme;    /* Whether it is a "style" or variant */
 	bool isBaseTheme;    /* Whether it is a "style" or variant */
 	bool isHighContrast; /* Whether it is a high-contrast adjustment layer */
 	bool isHighContrast; /* Whether it is a high-contrast adjustment layer */
+
+	bool usesFontScale = false; /* Whether the generated QSS uses the font scale option */
+	bool usesDensity = false;   /* Whether the generated QSS uses the density option */
 };
 };

+ 2 - 0
frontend/utility/OBSThemeVariable.hpp

@@ -28,6 +28,8 @@ struct OBSThemeVariable {
 		String, /* Raw string (e.g. color name, border style, etc.) */
 		String, /* Raw string (e.g. color name, border style, etc.) */
 		Alias,  /* Points at another variable, value will be the key */
 		Alias,  /* Points at another variable, value will be the key */
 		Calc,   /* Simple calculation with two operands */
 		Calc,   /* Simple calculation with two operands */
+		Min,    /* Get the smallest of two Size or Number */
+		Max,    /* Get the largest of two Size or Number */
 	};
 	};
 
 
 	/* Whether the variable should be editable in the UI */
 	/* Whether the variable should be editable in the UI */