Merge pull request 'fix(gamescope): Rearchitecture Tabs & Overlays for Gamescope' (#86) from fix/deck-gamescope into main

Reviewed-on: https://git.citron-emu.org/Citron/Emulator/pulls/86
This commit is contained in:
Collecting
2026-01-05 11:48:15 +00:00
20 changed files with 881 additions and 756 deletions

View File

@@ -2,25 +2,56 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QDialogButtonBox>
#include <QLabel>
#include <QPushButton>
#include <QVBoxLayout>
#include <QIcon>
#include <fmt/format.h>
#include "common/scm_rev.h"
#include "ui_aboutdialog.h"
#include "citron/about_dialog.h"
#include "citron/uisettings.h"
AboutDialog::AboutDialog(QWidget* parent)
: QDialog(parent), ui{std::make_unique<Ui::AboutDialog>()} {
: QDialog(parent) {
const bool is_gamescope = UISettings::IsGamescope();
if (is_gamescope) {
setWindowFlags(Qt::Window | Qt::CustomizeWindowHint | Qt::WindowTitleHint);
setWindowModality(Qt::NonModal);
}
ui = std::make_unique<Ui::AboutDialog>();
ui->setupUi(this);
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
std::string citron_build_version = "citron | 0.12.25";
#ifdef CITRON_ENABLE_PGO_USE
citron_build_version += " | PGO";
#endif
ui->setupUi(this);
// Try and request the icon from Qt theme (Linux?)
const QIcon citron_logo = QIcon::fromTheme(QStringLiteral("org.citron_emu.citron"));
if (!citron_logo.isNull()) {
ui->labelLogo->setPixmap(citron_logo.pixmap(200));
if (is_gamescope) {
resize(700, 450);
// Scale fonts up slightly so they aren't "too small"
QFont font = this->font();
font.setPointSize(font.pointSize() + 1);
this->setFont(font);
// Keep the Citron header large
ui->labelCitron->setStyleSheet(QStringLiteral("font-size: 24pt; font-weight: bold;"));
}
QPixmap logo_pixmap(QStringLiteral(":/icons/default/256x256/citron.png"));
if (!logo_pixmap.isNull()) {
int logo_size = is_gamescope ? 150 : 200;
ui->labelLogo->setPixmap(logo_pixmap);
ui->labelLogo->setFixedSize(logo_size, logo_size);
ui->labelLogo->setScaledContents(true);
}
ui->labelBuildInfo->setText(
ui->labelBuildInfo->text().arg(QString::fromStdString(citron_build_version),
QString::fromUtf8(Common::g_build_date).left(10)));

View File

@@ -3,147 +3,69 @@
<class>AboutDialog</class>
<widget class="QDialog" name="AboutDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>622</width>
<height>294</height>
</rect>
<rect><x>0</x><y>0</y><width>620</width><height>300</height></rect>
</property>
<property name="windowTitle">
<string>About citron</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<layout class="QVBoxLayout" name="mainVerticalLayout">
<property name="spacing"><number>12</number></property>
<property name="leftMargin"><number>12</number></property>
<property name="topMargin"><number>12</number></property>
<property name="rightMargin"><number>12</number></property>
<property name="bottomMargin"><number>12</number></property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1">
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<layout class="QVBoxLayout" name="logoColumn">
<item>
<widget class="QLabel" name="labelLogo">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>200</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="../../dist/qt_themes/default/default.qrc">:/icons/default/256x256/citron.png</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
<property name="minimumSize"><size><width>160</width><height>160</height></size></property>
<property name="text"><string/></property>
<property name="scaledContents"><bool>true</bool></property>
<property name="alignment"><set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set></property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item><spacer name="vS1"><property name="orientation"><enum>Qt::Vertical</enum></property></spacer></item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<layout class="QVBoxLayout" name="textColumn">
<property name="spacing"><number>6</number></property>
<item>
<widget class="QLabel" name="labelCitron">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:28pt;&quot;&gt;citron&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;html&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:28pt; font-weight:600;&quot;&gt;citron&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="labelBuildInfo">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;%1 (%2)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<property name="text"><string>&lt;html&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:10pt; color:#888888;&quot;&gt;%1 (%2)&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="labelAbout">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:12pt;&quot;&gt;citron is an experimental open-source emulator for the Nintendo Switch licensed under GPLv3.0+.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'MS Shell Dlg 2'; font-size:8pt;&quot;&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:12pt;&quot;&gt;This software should not be used to play games you have not legally obtained.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
<string>citron is an experimental open-source emulator for the Nintendo Switch licensed under GPLv3.0+. This software should not be used to play games you have not legally obtained.</string>
</property>
<property name="wordWrap"><bool>true</bool></property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item><spacer name="vS2"><property name="orientation"><enum>Qt::Vertical</enum></property></spacer></item>
<item>
<widget class="QLabel" name="labelLinks">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;a href=&quot;https://citron-emu.org/&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#039be5;&quot;&gt;Website&lt;/span&gt;&lt;/a&gt; | &lt;a href=&quot;https://git.citron-emu.org/citron/emulator&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#039be5;&quot;&gt;Source Code&lt;/span&gt;&lt;/a&gt; | &lt;a href=&quot;https://git.citron-emu.org/Citron/Emulator/commits/branch/main&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#039be5;&quot;&gt;Recent Commits&lt;/span&gt;&lt;/a&gt; | &lt;a href=&quot;https://git.citron-emu.org/Citron/Emulator/src/branch/main/LICENSE&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#039be5;&quot;&gt;License&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="openExternalLinks">
<bool>true</bool>
<string>&lt;a href=&quot;https://citron-emu.org/&quot;&gt;Website&lt;/a&gt; | &lt;a href=&quot;https://git.citron-emu.org/citron/emulator&quot;&gt;Source&lt;/a&gt; | &lt;a href=&quot;https://git.citron-emu.org/&quot;&gt;Commits&lt;/a&gt;</string>
</property>
<property name="openExternalLinks"><bool>true</bool></property>
</widget>
</item>
<item>
<widget class="QLabel" name="labelLiability">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:7pt;&quot;&gt;&amp;quot;Nintendo Switch&amp;quot; is a trademark of Nintendo. citron is not affiliated with Nintendo in any way.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;span style=&quot; font-size:8pt; color:#777777;&quot;&gt;Nintendo Switch is a trademark of Nintendo. citron is not affiliated with Nintendo.&lt;/span&gt;</string>
</property>
</widget>
</item>
@@ -153,52 +75,17 @@ p, li { white-space: pre-wrap; }
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Ok</set>
</property>
<property name="standardButtons"><set>QDialogButtonBox::Ok</set></property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../../dist/qt_themes_default/default/default.qrc"/>
<include location="../../dist/qt_themes/default/default.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>AboutDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>AboutDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -108,8 +108,17 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_,
}
Settings::SetConfiguringGlobal(true);
setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowSystemMenuHint |
Qt::WindowMinMaxButtonsHint | Qt::WindowCloseButtonHint);
const bool is_gamescope = UISettings::IsGamescope();
if (is_gamescope) {
// GameScope: Use Window flags instead of Dialog to ensure mouse focus
setWindowFlags(Qt::Window | Qt::CustomizeWindowHint | Qt::WindowTitleHint);
setWindowModality(Qt::NonModal);
} else {
setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint);
setWindowModality(Qt::WindowModal);
}
ui->setupUi(this);
auto* animation_filter = new StyleAnimationEventFilter(this);
@@ -128,9 +137,13 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_,
ui->topButtonWidget->setLayout(nav_layout);
last_palette_text_color = qApp->palette().color(QPalette::WindowText);
if (!UISettings::values.configure_dialog_geometry.isEmpty()) {
if (is_gamescope) {
resize(1100, 700);
} else if (!UISettings::values.configure_dialog_geometry.isEmpty()) {
restoreGeometry(UISettings::values.configure_dialog_geometry);
}
UpdateTheme();
tab_button_group = std::make_unique<QButtonGroup>(this);
@@ -238,8 +251,6 @@ void ConfigureDialog::UpdateTheme() {
if (!rainbow_timer) {
rainbow_timer = new QTimer(this);
connect(rainbow_timer, &QTimer::timeout, this, [this] {
// MODAL GUARD: If a color dialog or popup is open, pause updates.
// This makes the Color Picker buttons static and responsive.
if (m_is_tab_animating || !this->isVisible() || !this->isActiveWindow()) return;
const int current_index = ui->stackedWidget->currentIndex();
@@ -261,12 +272,18 @@ void ConfigureDialog::UpdateTheme() {
if (ui->horizontalNavWidget) ui->horizontalNavWidget->setStyleSheet(sidebar_css);
// 2. Action Buttons (OK/Apply/Cancel)
if (ui->buttonBox && !ui->buttonBox->underMouse()) {
ui->buttonBox->setStyleSheet(QStringLiteral(
if (ui->buttonBox) {
const QString button_css = QStringLiteral(
"QPushButton { background-color: %1; color: #ffffff; border-radius: 4px; font-weight: bold; padding: 5px 15px; }"
"QPushButton:hover { background-color: %2; }"
"QPushButton:pressed { background-color: %3; }"
).arg(hue_hex).arg(hue_light).arg(hue_dark));
).arg(hue_hex).arg(hue_light).arg(hue_dark);
for (auto* button : ui->buttonBox->findChildren<QPushButton*>()) {
if (!button->isDown()) {
button->setStyleSheet(button_css);
}
}
}
// 3. Tab Content Area
@@ -302,7 +319,17 @@ void ConfigureDialog::UpdateTheme() {
});
}
rainbow_timer->start(33);
} else if (rainbow_timer) {
}
if (ui->buttonBox) {
ui->buttonBox->setStyleSheet(QStringLiteral(
"QPushButton { background-color: %1; color: #ffffff; border-radius: 4px; font-weight: bold; padding: 5px 15px; }"
"QPushButton:hover { background-color: %2; }"
"QPushButton:pressed { background-color: %3; }"
).arg(accent).arg(Theme::GetAccentColorHover()).arg(Theme::GetAccentColorPressed()));
}
if (UISettings::values.enable_rainbow_mode.GetValue() == false && rainbow_timer) {
rainbow_timer->stop();
if (ui->topButtonWidget) ui->topButtonWidget->setStyleSheet({});
if (ui->horizontalNavWidget) ui->horizontalNavWidget->setStyleSheet({});

View File

@@ -76,14 +76,11 @@
static bool IsDarkMode() {
const std::string& theme_name = UISettings::values.theme;
// Priority 1: Check for explicitly chosen dark themes.
if (theme_name == "qdarkstyle" || theme_name == "colorful_dark" ||
theme_name == "qdarkstyle_midnight_blue" || theme_name == "colorful_midnight_blue") {
return true; // These themes are always dark.
return true;
}
// Priority 2: Check for adaptive themes ("default" and "colorful").
// For these, we fall back to checking the OS palette.
if (theme_name == "default" || theme_name == "colorful") {
const QPalette palette = qApp->palette();
const QColor text_color = palette.color(QPalette::WindowText);
@@ -91,7 +88,6 @@ static bool IsDarkMode() {
return text_color.value() > base_color.value();
}
// Fallback for any other unknown theme (assumed light).
return false;
}
@@ -112,7 +108,6 @@ ConfigurePerGame::ConfigurePerGame(QWidget* parent, u64 title_id_, const std::st
: fmt::format("{:016X}", title_id);
game_config = std::make_unique<QtConfig>(config_file_name, Config::ConfigType::PerGameConfig);
// Create tab instances
addons_tab = std::make_unique<ConfigurePerGameAddons>(system_, this);
cheats_tab = std::make_unique<ConfigurePerGameCheats>(system_, this);
audio_tab = std::make_unique<ConfigureAudio>(system_, tab_group, *builder, this);
@@ -126,8 +121,18 @@ ConfigurePerGame::ConfigurePerGame(QWidget* parent, u64 title_id_, const std::st
linux_tab = std::make_unique<ConfigureLinuxTab>(system_, tab_group, *builder, this);
system_tab = std::make_unique<ConfigureSystem>(system_, tab_group, *builder, this);
if (!UISettings::values.per_game_configure_geometry.isEmpty()) {
restoreGeometry(UISettings::values.per_game_configure_geometry);
const bool is_gamescope = UISettings::IsGamescope();
if (is_gamescope) {
setWindowFlags(Qt::Window | Qt::CustomizeWindowHint | Qt::WindowTitleHint);
setWindowModality(Qt::NonModal);
resize(1100, 700);
} else {
setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint);
setWindowModality(Qt::WindowModal);
if (!UISettings::values.per_game_configure_geometry.isEmpty()) {
restoreGeometry(UISettings::values.per_game_configure_geometry);
}
}
UpdateTheme();
@@ -140,10 +145,8 @@ ConfigurePerGame::ConfigurePerGame(QWidget* parent, u64 title_id_, const std::st
const auto add_tab = [&](QWidget* widget, const QString& title, int id) {
auto button = new QPushButton(title, this);
button->setCheckable(true);
// This object name matches the stylesheet ID selector `QPushButton#aestheticTabButton`
button->setObjectName(QStringLiteral("aestheticTabButton"));
// This custom property is used by the event filter for the animated style
button->setProperty("class", QStringLiteral("tabButton")); // Keep class for animation
button->setProperty("class", QStringLiteral("tabButton"));
button->installEventFilter(animation_filter);
ui->tabButtonsLayout->addWidget(button);
@@ -177,7 +180,6 @@ ConfigurePerGame::ConfigurePerGame(QWidget* parent, u64 title_id_, const std::st
ui->stackedWidget->setCurrentIndex(0);
}
setFocusPolicy(Qt::ClickFocus);
setWindowTitle(tr("Properties"));
addons_tab->SetTitleId(title_id);
@@ -322,26 +324,25 @@ void ConfigurePerGame::UpdateTheme() {
if (ui->tabButtonsScrollArea) {
ui->tabButtonsScrollArea->setStyleSheet(QStringLiteral(
"QScrollBar:horizontal { height: 14px; background: transparent; border-radius: 7px; }"
"QScrollBar::handle:horizontal { background-color: %1; border-radius: 6px; min-width: 30px; margin: 1px; }"
"QScrollBar::handle:horizontal { background-color: %1; border-radius: 64px; min-width: 30px; margin: 1px; }"
"QScrollBar::add-line, QScrollBar::sub-line { background: none; width: 0px; }"
).arg(hue_hex));
}
// 3. Action Buttons (OK/Cancel) and Trim button
if (ui->buttonBox && !ui->buttonBox->underMouse()) {
ui->buttonBox->setStyleSheet(QStringLiteral(
"QPushButton { background-color: %1; color: #ffffff; border-radius: 4px; font-weight: bold; padding: 5px 15px; }"
"QPushButton:hover { background-color: %2; }"
"QPushButton:pressed { background-color: %3; }"
).arg(hue_hex).arg(hue_light).arg(hue_dark));
}
// 3. Action Buttons
const QString button_css = QStringLiteral(
"QPushButton { background-color: %1; color: #ffffff; border-radius: 4px; font-weight: bold; padding: 5px 15px; }"
"QPushButton:hover { background-color: %2; }"
"QPushButton:pressed { background-color: %3; }"
).arg(hue_hex).arg(hue_light).arg(hue_dark);
if (ui->trim_xci_button && !ui->trim_xci_button->underMouse()) {
ui->trim_xci_button->setStyleSheet(QStringLiteral(
"QPushButton { background-color: %1; color: #ffffff; border: none; border-radius: 4px; padding: 10px; }"
"QPushButton:hover { background-color: %2; }"
"QPushButton:pressed { background-color: %3; }"
).arg(hue_hex).arg(hue_light).arg(hue_dark));
if (ui->buttonBox) {
for (auto* button : ui->buttonBox->findChildren<QPushButton*>()) {
if (!button->isDown()) button->setStyleSheet(button_css);
}
}
if (ui->trim_xci_button && !ui->trim_xci_button->isDown()) {
ui->trim_xci_button->setStyleSheet(button_css);
}
// 4. Tab Content Area
@@ -374,7 +375,25 @@ void ConfigurePerGame::UpdateTheme() {
});
}
rainbow_timer->start(33);
} else if (rainbow_timer) {
}
// Fix for Gamescope: Style buttons once outside the timer loop
if (ui->buttonBox) {
ui->buttonBox->setStyleSheet(QStringLiteral(
"QPushButton { background-color: %1; color: #ffffff; border-radius: 4px; font-weight: bold; padding: 5px 15px; }"
"QPushButton:hover { background-color: %2; }"
"QPushButton:pressed { background-color: %3; }"
).arg(accent).arg(Theme::GetAccentColorHover()).arg(Theme::GetAccentColorPressed()));
}
if (ui->trim_xci_button) {
ui->trim_xci_button->setStyleSheet(QStringLiteral(
"QPushButton { background-color: %1; color: #ffffff; border: none; border-radius: 4px; padding: 10px; }"
"QPushButton:hover { background-color: %2; }"
"QPushButton:pressed { background-color: %3; }"
).arg(accent).arg(Theme::GetAccentColorHover()).arg(Theme::GetAccentColorPressed()));
}
if (UISettings::values.enable_rainbow_mode.GetValue() == false && rainbow_timer) {
rainbow_timer->stop();
if (ui->tabButtonsContainer) ui->tabButtonsContainer->setStyleSheet({});
if (ui->tabButtonsScrollArea) ui->tabButtonsScrollArea->setStyleSheet({});
@@ -551,8 +570,7 @@ void ConfigurePerGame::LoadConfiguration() {
}
}
}
} catch (...) {
}
} catch (...) {}
}
try {
@@ -610,17 +628,18 @@ void ConfigurePerGame::LoadConfiguration() {
}
}
}
} catch (...) {
}
} catch (...) {}
const auto& system_build_id = system.GetApplicationProcessBuildID();
const auto system_build_id_hex = Common::HexToString(system_build_id, false);
if (system.IsPoweredOn()) {
const auto& system_build_id = system.GetApplicationProcessBuildID();
const auto system_build_id_hex = Common::HexToString(system_build_id, false);
if (!system_build_id_hex.empty() && system_build_id_hex != std::string(64, '0')) {
if (!base_build_id_hex.empty() && system_build_id_hex != base_build_id_hex) {
update_build_id_hex = system_build_id_hex;
} else if (base_build_id_hex.empty()) {
base_build_id_hex = system_build_id_hex;
if (!system_build_id_hex.empty() && system_build_id_hex != std::string(64, '0')) {
if (!base_build_id_hex.empty() && system_build_id_hex != base_build_id_hex) {
update_build_id_hex = system_build_id_hex;
} else if (base_build_id_hex.empty()) {
base_build_id_hex = system_build_id_hex;
}
}
}
@@ -863,15 +882,15 @@ void ConfigurePerGame::AnimateTabSwitch(int id) {
current_widget->hide();
current_widget->move(0, 0);
m_is_tab_animating = false; // Reset the flag
m_is_tab_animating = false;
for (auto button : button_group->buttons()) {
button->setEnabled(true);
}
});
m_is_tab_animating = true; // Set the flag
m_is_tab_animating = true;
for (auto button : button_group->buttons()) {
button->setEnabled(false);
}
animation_group->start(QAbstractAnimation::DeleteWhenStopped);
}
}

View File

@@ -2,17 +2,19 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "citron/controller_overlay.h"
#include "citron/uisettings.h"
#include "citron/configuration/configure_input_player_widget.h"
#include "citron/main.h"
#include "core/core.h"
#include "hid_core/hid_core.h"
#include <QApplication>
#include <QGridLayout>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QSizeGrip>
#include <QWindow> // Required for Wayland dragging
#include <QWindow>
#include <QResizeEvent>
namespace {
@@ -26,27 +28,34 @@ Core::HID::EmulatedController* GetPlayer1Controller(Core::System* system) {
}
return hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1);
}
}
ControllerOverlay::ControllerOverlay(GMainWindow* parent)
: QWidget(parent, Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint),
main_window(parent) {
: QWidget(parent), main_window(parent) {
// Gamescope requires ToolTip to stay visible over the game surface,
// but Desktop Wayland/Windows needs Tool to behave correctly in the taskbar/stack.
if (UISettings::IsGamescope()) {
setWindowFlags(Qt::ToolTip | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::WindowDoesNotAcceptFocus);
setAttribute(Qt::WA_ShowWithoutActivating);
setMinimumSize(112, 87); // Use the smaller Gamescope-optimized scale
} else {
setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
setMinimumSize(225, 175); // Desktop standard scale
}
setAttribute(Qt::WA_TranslucentBackground);
setAttribute(Qt::WA_NoSystemBackground);
auto* layout = new QGridLayout(this);
setLayout(layout);
// Set margins to 0 so the controller can go right to the edge of the resizable window
layout->setContentsMargins(0, 0, 0, 0);
// Create the widget that draws the controller and make it transparent
// Create the widget that draws the controller
controller_widget = new PlayerControlPreview(this);
controller_widget->setAttribute(Qt::WA_TranslucentBackground);
// Disable the raw joystick (deadzone) visualization
controller_widget->SetRawJoystickVisible(false);
// Allow the widget to expand and shrink with the window
controller_widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
layout->addWidget(controller_widget, 0, 0);
@@ -54,18 +63,46 @@ ControllerOverlay::ControllerOverlay(GMainWindow* parent)
size_grip = new QSizeGrip(this);
layout->addWidget(size_grip, 0, 0, Qt::AlignBottom | Qt::AlignRight);
// Start the timer for continuous updates
// Timer for updates
connect(&update_timer, &QTimer::timeout, this, &ControllerOverlay::UpdateControllerState);
update_timer.start(16); // ~60 FPS
// Set a minimum size and a default starting size
setMinimumSize(225, 175);
resize(450, 350);
// Initial Resize
if (UISettings::IsGamescope()) {
resize(225, 175);
} else {
resize(450, 350);
}
}
ControllerOverlay::~ControllerOverlay() = default;
void ControllerOverlay::UpdateControllerState() {
if (!main_window || !is_enabled) return;
if (UISettings::IsGamescope()) {
bool ui_active = false;
for (QWidget* w : QApplication::topLevelWidgets()) {
if (w->isWindow() && w->isVisible() && w != main_window && w != this &&
!w->inherits("GRenderWindow") &&
!w->inherits("PerformanceOverlay") &&
!w->inherits("VramOverlay") &&
!w->inherits("ControllerOverlay")) {
ui_active = true;
break;
}
}
if (ui_active) {
if (!this->isHidden()) this->hide();
return;
}
}
if (is_enabled && this->isHidden()) {
this->show();
}
Core::System* system = main_window->GetSystem();
Core::HID::EmulatedController* controller = GetPlayer1Controller(system);
if (controller_widget && controller) {
@@ -75,22 +112,23 @@ void ControllerOverlay::UpdateControllerState() {
}
}
// The paint event is now empty, which makes the background fully transparent.
void ControllerOverlay::paintEvent(QPaintEvent* event) {
Q_UNUSED(event);
// Intentionally left blank to achieve a fully transparent window background.
}
// These functions handle dragging the frameless window
void ControllerOverlay::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton && !size_grip->geometry().contains(event->pos())) {
// LOGIC BRANCH: Desktop Linux (Wayland) requires system move.
// Gamescope and Windows require manual dragging.
#if defined(Q_OS_LINUX)
// Use system move on Wayland/Linux for proper dragging
if (windowHandle()) {
if (!UISettings::IsGamescope() && windowHandle()) {
windowHandle()->startSystemMove();
} else {
is_dragging = true;
drag_start_pos = event->globalPosition().toPoint() - this->pos();
}
#else
// Original dragging implementation for other platforms (Windows, etc.)
is_dragging = true;
drag_start_pos = event->globalPosition().toPoint() - this->pos();
#endif
@@ -99,15 +137,11 @@ void ControllerOverlay::mousePressEvent(QMouseEvent* event) {
}
void ControllerOverlay::mouseMoveEvent(QMouseEvent* event) {
#if !defined(Q_OS_LINUX)
// Only handle manual dragging if we aren't using startSystemMove (which handles its own move)
if (is_dragging) {
move(event->globalPosition().toPoint() - drag_start_pos);
event->accept();
}
#else
// On Linux, the window manager handles the move, so we do nothing here.
Q_UNUSED(event);
#endif
}
void ControllerOverlay::mouseReleaseEvent(QMouseEvent* event) {
@@ -119,6 +153,14 @@ void ControllerOverlay::mouseReleaseEvent(QMouseEvent* event) {
void ControllerOverlay::resizeEvent(QResizeEvent* event) {
QWidget::resizeEvent(event);
// This ensures the layout and its widgets (like the size grip) are correctly repositioned on resize.
layout()->update();
}
void ControllerOverlay::SetVisible(bool visible) {
is_enabled = visible;
if (visible) {
this->show();
} else {
this->hide();
}
}

View File

@@ -13,6 +13,8 @@ class ControllerOverlay : public QWidget {
Q_OBJECT
public:
void SetVisible(bool visible);
bool is_enabled = false;
explicit ControllerOverlay(GMainWindow* parent);
~ControllerOverlay() override;
@@ -35,4 +37,4 @@ private:
bool is_dragging = false;
QPoint drag_start_pos;
};
};

View File

@@ -84,6 +84,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#include <QDir>
#include <QFile>
#include <QFileDialog>
#include <QGuiApplication>
#include <QInputDialog>
#include <QMessageBox>
#include <QProgressBar>
@@ -94,6 +95,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#include <QStandardPaths>
#include <QStatusBar>
#include <QString>
#include <QStyleFactory>
#include <QSysInfo>
#include <QUrl>
#include <QtConcurrent/QtConcurrent>
@@ -495,6 +497,12 @@ GMainWindow::GMainWindow(std::unique_ptr<QtConfig> config_, bool has_broken_vulk
// Create a non-modal QMessageBox instance with a nullptr parent to make it a top-level window.
// This prevents it from blocking the main application window.
auto* confirmation_dialog = new QMessageBox(nullptr);
const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope";
if (is_gamescope) {
confirmation_dialog->setWindowFlags(Qt::Window | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowStaysOnTopHint);
confirmation_dialog->resize(650, 300);
confirmation_dialog->setStyleSheet(QStringLiteral("font-size: 11pt;"));
}
confirmation_dialog->setAttribute(Qt::WA_DeleteOnClose); // This ensures it is deleted automatically on close.
confirmation_dialog->setWindowModality(Qt::NonModal); // Explicitly set modality.
confirmation_dialog->setWindowTitle(tr("First-Time Setup"));
@@ -854,15 +862,13 @@ void GMainWindow::SoftwareKeyboardShowNormal() {
}
const auto& layout = render_window->GetFramebufferLayout();
const auto x = layout.screen.left;
const auto y = layout.screen.top;
const auto w = layout.screen.GetWidth();
const auto h = layout.screen.GetHeight();
const auto scale_ratio = devicePixelRatioF();
software_keyboard->ShowNormalKeyboard(render_window->mapToGlobal(QPoint(x, y) / scale_ratio),
QSize(w, h) / scale_ratio);
software_keyboard->ShowNormalKeyboard(render_window->mapToGlobal(QPoint(x, y)),
QSize(w, h));
}
void GMainWindow::SoftwareKeyboardShowTextCheck(
@@ -895,11 +901,10 @@ void GMainWindow::SoftwareKeyboardShowInline(
(1.0f - appear_parameters.key_top_scale_y))));
const auto w = static_cast<int>(layout.screen.GetWidth() * appear_parameters.key_top_scale_x);
const auto h = static_cast<int>(layout.screen.GetHeight() * appear_parameters.key_top_scale_y);
const auto scale_ratio = devicePixelRatioF();
software_keyboard->ShowInlineKeyboard(std::move(appear_parameters),
render_window->mapToGlobal(QPoint(x, y) / scale_ratio),
QSize(w, h) / scale_ratio);
render_window->mapToGlobal(QPoint(x, y)),
QSize(w, h));
}
void GMainWindow::SoftwareKeyboardHideInline() {
@@ -979,13 +984,11 @@ void GMainWindow::WebBrowserOpenWebPage(const std::string& main_url,
}
const auto& layout = render_window->GetFramebufferLayout();
const auto scale_ratio = devicePixelRatioF();
web_applet->resize(layout.screen.GetWidth() / scale_ratio,
layout.screen.GetHeight() / scale_ratio);
web_applet->move(layout.screen.left / scale_ratio,
(layout.screen.top / scale_ratio) + menuBar()->height());
web_applet->setZoomFactor(static_cast<qreal>(layout.screen.GetWidth() / scale_ratio) /
static_cast<qreal>(Layout::ScreenUndocked::Width));
web_applet->resize(layout.screen.GetWidth(), layout.screen.GetHeight());
web_applet->move(layout.screen.left,
(layout.screen.top) + menuBar()->height());
web_applet->setZoomFactor(static_cast<qreal>(layout.screen.GetWidth()) /
static_cast<qreal>(Layout::ScreenUndocked::Width));
web_applet->setFocus();
web_applet->show();
@@ -1159,9 +1162,6 @@ void GMainWindow::InitializeWidgets() {
multiplayer_room_overlay = new MultiplayerRoomOverlay(this);
multiplayer_room_overlay->hide();
connect(this, &GMainWindow::EmulationStarting, multiplayer_room_overlay, &MultiplayerRoomOverlay::OnEmulationStarting);
connect(this, &GMainWindow::EmulationStopping, multiplayer_room_overlay, &MultiplayerRoomOverlay::OnEmulationStopping);
vram_overlay = new VramOverlay(this);
vram_overlay->hide();
@@ -1353,6 +1353,26 @@ void GMainWindow::InitializeWidgets() {
statusBar()->setVisible(true);
setStyleSheet(QStringLiteral("QStatusBar::item{border: none;}"));
const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope";
if (is_gamescope) {
statusBar()->setSizeGripEnabled(true);
this->menuBar()->setNativeMenuBar(false);
QString gamescope_style = qApp->styleSheet();
gamescope_style.append(QStringLiteral("QMenu { background-color: #2b2b2b; border: 1px solid #3d3d3d; padding: 2px; } "
"QMenu::item { padding: 5px 25px 5px 20px; } "
"QMenu::item:selected { background-color: #3d3d3d; }"));
qApp->setStyleSheet(gamescope_style);
multiplayer_room_overlay->resize(360, 240);
this->setContentsMargins(0, 0, 0, 0);
this->layout()->setContentsMargins(0, 0, 0, 0);
this->layout()->setSpacing(0);
ui->horizontalLayout->setContentsMargins(0, 0, 0, 0);
ui->horizontalLayout->setSpacing(0);
}
}
void GMainWindow::InitializeDebugWidgets() {
@@ -1483,6 +1503,12 @@ void GMainWindow::InitializeHotkeys() {
}
void GMainWindow::SetDefaultUIGeometry() {
const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope";
if (is_gamescope) {
this->resize(1280, 800);
return;
}
// geometry: 53% of the window contents are in the upper screen half, 47% in the lower half
const QRect screenRect = QGuiApplication::primaryScreen()->geometry();
@@ -1495,15 +1521,25 @@ void GMainWindow::SetDefaultUIGeometry() {
}
void GMainWindow::RestoreUIState() {
const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope";
setWindowFlags(windowFlags() & ~Qt::FramelessWindowHint);
restoreGeometry(UISettings::values.geometry);
if (!is_gamescope) {
restoreGeometry(UISettings::values.geometry);
}
// Work-around because the games list isn't supposed to be full screen
if (isFullScreen()) {
showNormal();
}
restoreState(UISettings::values.state);
render_window->setWindowFlags(render_window->windowFlags() & ~Qt::FramelessWindowHint);
render_window->restoreGeometry(UISettings::values.renderwindow_geometry);
if (!is_gamescope) {
render_window->setWindowFlags(render_window->windowFlags() & ~Qt::FramelessWindowHint);
render_window->restoreGeometry(UISettings::values.renderwindow_geometry);
}
#if MICROPROFILE_ENABLED
microProfileDialog->restoreGeometry(UISettings::values.microprofile_geometry);
microProfileDialog->setVisible(UISettings::values.microprofile_visible.GetValue());
@@ -1528,13 +1564,12 @@ void GMainWindow::RestoreUIState() {
ui->action_Show_Status_Bar->setChecked(UISettings::values.show_status_bar.GetValue());
statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked());
// Force the performance overlay to be off on startup
// Force overlays off on startup
ui->action_Show_Performance_Overlay->setChecked(false);
if (performance_overlay) {
performance_overlay->SetVisible(false);
}
// Force the VRAM overlay to be off on startup
ui->action_Show_Vram_Overlay->setChecked(false);
if (vram_overlay) {
vram_overlay->SetVisible(false);
@@ -2937,6 +2972,12 @@ void GMainWindow::OnGameListVerifyIntegrity(const std::string& game_path) {
};
QProgressDialog progress(tr("Verifying integrity..."), tr("Cancel"), 0, 100, this);
const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope";
if (is_gamescope) {
progress.setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::WindowStaysOnTopHint);
}
progress.setWindowModality(Qt::WindowModal);
progress.setMinimumDuration(100);
progress.setAutoClose(false);
@@ -4015,14 +4056,16 @@ void GMainWindow::ShowFullscreen() {
}
void GMainWindow::HideFullscreen() {
const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope";
if (ui->action_Single_Window_Mode->isChecked()) {
if (UsingExclusiveFullscreen()) {
showNormal();
restoreGeometry(UISettings::values.geometry);
if (!is_gamescope) restoreGeometry(UISettings::values.geometry);
} else {
hide();
setWindowFlags(windowFlags() & ~Qt::FramelessWindowHint);
restoreGeometry(UISettings::values.geometry);
if (!is_gamescope) restoreGeometry(UISettings::values.geometry);
raise();
show();
}
@@ -4032,15 +4075,18 @@ void GMainWindow::HideFullscreen() {
} else {
if (UsingExclusiveFullscreen()) {
render_window->showNormal();
render_window->restoreGeometry(UISettings::values.renderwindow_geometry);
if (!is_gamescope) render_window->restoreGeometry(UISettings::values.renderwindow_geometry);
} else {
render_window->hide();
render_window->setWindowFlags(windowFlags() & ~Qt::FramelessWindowHint);
render_window->restoreGeometry(UISettings::values.renderwindow_geometry);
if (!is_gamescope) render_window->restoreGeometry(UISettings::values.renderwindow_geometry);
render_window->raise();
render_window->show();
}
}
if (is_gamescope) {
}
}
void GMainWindow::ToggleWindowMode() {
@@ -4069,9 +4115,14 @@ void GMainWindow::ToggleWindowMode() {
}
void GMainWindow::ResetWindowSize(u32 width, u32 height) {
const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope";
if (is_gamescope) {
return;
}
const auto aspect_ratio = Layout::EmulationAspectRatio(
static_cast<Layout::AspectRatio>(Settings::values.aspect_ratio.GetValue()),
static_cast<float>(height) / width);
static_cast<float>(height) / width);
if (!ui->action_Single_Window_Mode->isChecked()) {
render_window->resize(height / aspect_ratio, height);
} else {
@@ -4441,6 +4492,15 @@ bool GMainWindow::question(QWidget* parent, const QString& title, const QString&
QMessageBox::StandardButtons buttons,
QMessageBox::StandardButton defaultButton) {
QMessageBox* box_dialog = new QMessageBox(parent);
const bool is_gamescope = UISettings::IsGamescope();
if (is_gamescope) {
box_dialog->setWindowFlags(Qt::Window | Qt::CustomizeWindowHint | Qt::WindowTitleHint);
box_dialog->setWindowModality(Qt::NonModal);
box_dialog->setFixedSize(600, 250);
box_dialog->setStyleSheet(QStringLiteral("font-size: 11pt;"));
}
box_dialog->setWindowTitle(title);
box_dialog->setText(text);
box_dialog->setStandardButtons(buttons);
@@ -5026,7 +5086,10 @@ void GMainWindow::OnToggleControllerOverlay() {
controller_overlay = new ControllerOverlay(this);
}
if (controller_overlay) {
controller_overlay->setVisible(visible);
controller_overlay->SetVisible(visible);
this->update();
QCoreApplication::processEvents();
}
}
@@ -5047,7 +5110,6 @@ void GMainWindow::OnTogglePerformanceOverlay() {
if (performance_overlay) {
const bool is_checked = ui->action_Show_Performance_Overlay->isChecked();
performance_overlay->SetVisible(is_checked);
UISettings::values.show_performance_overlay = is_checked;
}
}
@@ -5066,7 +5128,6 @@ void GMainWindow::OnToggleVramOverlay() {
if (vram_overlay) {
const bool is_checked = ui->action_Show_Vram_Overlay->isChecked();
vram_overlay->SetVisible(is_checked);
UISettings::values.show_vram_overlay = is_checked;
}
}
@@ -5564,10 +5625,14 @@ void GMainWindow::UpdateStatusButtons() {
}
void GMainWindow::UpdateUISettings() {
if (!ui->action_Fullscreen->isChecked()) {
const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope";
// Only save/restore geometry if we are NOT in gamescope to prevent resolution bugs
if (!ui->action_Fullscreen->isChecked() && !is_gamescope) {
UISettings::values.geometry = saveGeometry();
UISettings::values.renderwindow_geometry = render_window->saveGeometry();
}
UISettings::values.state = saveState();
#if MICROPROFILE_ENABLED
UISettings::values.microprofile_geometry = microProfileDialog->saveGeometry();
@@ -6029,80 +6094,69 @@ void VolumeButton::ResetMultiplier() {
#endif
static void SetHighDPIAttributes() {
[[maybe_unused]] const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() ||
qgetenv("XDG_CURRENT_DESKTOP") == "gamescope" ||
!qgetenv("STEAM_DECK").isEmpty();
#ifdef _WIN32
// For Windows, we want to avoid scaling artifacts on fractional scaling ratios.
// This is done by setting the optimal scaling policy for the primary screen.
// Windows logic: Set policy globally.
// removed the 'temp QApplication' here because in Qt 6 it locks the DPI logic
// before our environment overrides in main() can take effect.
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(
Qt::HighDpiScaleFactorRoundingPolicy::Round);
// Create a temporary QApplication.
int temp_argc = 0;
char** temp_argv = nullptr;
QApplication temp{temp_argc, temp_argv};
// Get the current screen geometry.
const QScreen* primary_screen = QGuiApplication::primaryScreen();
if (primary_screen == nullptr) {
return;
}
const QRect screen_rect = primary_screen->geometry();
const int real_width = screen_rect.width();
const int real_height = screen_rect.height();
const float real_ratio = primary_screen->logicalDotsPerInch() / 96.0f;
// Recommended minimum width and height for proper window fit.
// Any screen with a lower resolution than this will still have a scale of 1.
constexpr float minimum_width = 1350.0f;
constexpr float minimum_height = 900.0f;
const float width_ratio = std::max(1.0f, real_width / minimum_width);
const float height_ratio = std::max(1.0f, real_height / minimum_height);
// Get the lower of the 2 ratios and truncate, this is the maximum integer scale.
const float max_ratio = std::trunc(std::min(width_ratio, height_ratio));
if (max_ratio > real_ratio) {
QApplication::setHighDpiScaleFactorRoundingPolicy(
Qt::HighDpiScaleFactorRoundingPolicy::Round);
} else {
QApplication::setHighDpiScaleFactorRoundingPolicy(
Qt::HighDpiScaleFactorRoundingPolicy::Floor);
}
#else
// Other OSes should be better than Windows at fractional scaling.
QApplication::setHighDpiScaleFactorRoundingPolicy(
Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
#endif
// Set the DPI awareness for better scaling on Windows
#ifdef _WIN32
// Enable Per Monitor DPI Awareness for Windows 8.1+
SetProcessDPIAware();
// For Windows 10+, use Per Monitor v2 DPI Awareness
// This provides better scaling for multi-monitor setups
HMODULE shcore = LoadLibrary(L"shcore.dll");
if (shcore) {
typedef HRESULT(WINAPI* SetProcessDpiAwarenessFunc)(int);
SetProcessDpiAwarenessFunc setProcessDpiAwareness =
(SetProcessDpiAwarenessFunc)GetProcAddress(shcore, "SetProcessDpiAwareness");
if (setProcessDpiAwareness) {
// PROCESS_PER_MONITOR_DPI_AWARE_V2 = 2
setProcessDpiAwareness(2);
setProcessDpiAwareness(2); // PROCESS_PER_MONITOR_DPI_AWARE_V2
}
FreeLibrary(shcore);
}
#else
if (is_gamescope) {
// PassThrough prevents Qt6 from recursively expanding layouts to fit rounded DPIs
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(
Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
}
#endif
}
int main(int argc, char* argv[]) {
// Set environment variables for AppImage compatibility
// This must be done before the QApplication is created.
// 1. Detect Gamescope/Steam Deck hardware
const bool is_gamescope = UISettings::IsGamescope();
if (is_gamescope) {
// Kill the scaling system entirely
qputenv("QT_ENABLE_HIGHDPI_SCALING", "0");
qputenv("QT_SCALE_FACTOR", "1");
qputenv("QT_AUTO_SCREEN_SCALE_FACTOR", "0");
#ifdef __linux__
qputenv("QT_QPA_PLATFORM", "xcb");
qputenv("QT_FONT_DPI", "96");
#endif
// Stop Qt from querying physical hardware DPI for text/widgets
qputenv("QT_USE_PHYSICAL_DPI", "0");
// Force the legacy coordinate system for X11/XCB
qputenv("QT_SCREEN_SCALE_FACTORS", "1");
// Ensure Gamescope compositor handles Citron menus correctly
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar);
QCoreApplication::setAttribute(Qt::AA_DontUseNativeDialogs);
qputenv("QT_WAYLAND_SHELL_INTEGRATION", "xdg-shell");
}
// 2. Setup AppImage environment
const bool is_appimage = !qgetenv("APPIMAGE").isEmpty();
if (is_appimage) {
// Fixes Wayland crash with NVIDIA drivers by disabling explicit sync.
qputenv("QT_WAYLAND_DISABLE_EXPLICIT_SYNC", "1");
// Tell the bundled OpenSSL where to find the bundled certificates.
const QDir app_dir(QCoreApplication::applicationDirPath());
const QString certs_path = app_dir.filePath(QString::fromLatin1("../etc/ssl/certs"));
qputenv("SSL_CERT_DIR", certs_path.toUtf8());
@@ -6133,73 +6187,73 @@ int main(int argc, char* argv[]) {
Common::ConfigureNvidiaEnvironmentFlags();
// Init settings params
QCoreApplication::setOrganizationName(QStringLiteral("citron team"));
QCoreApplication::setApplicationName(QStringLiteral("citron"));
#ifdef _WIN32
// Increases the maximum open file limit to 8192
_setmaxstdio(8192);
#endif
#ifdef __APPLE__
// If you start a bundle (binary) on OSX without the Terminal, the working directory is "/".
// But since we require the working directory to be the executable path for the location of
// the user folder in the Qt Frontend, we need to cd into that working directory
const auto bin_path = Common::FS::GetBundleDirectory() / "..";
chdir(Common::FS::PathToUTF8String(bin_path).c_str());
#endif
#ifdef __linux__
// Set the DISPLAY variable in order to open web browsers
// TODO (lat9nq): Find a better solution for AppImages to start external applications
if (QString::fromLocal8Bit(qgetenv("DISPLAY")).isEmpty()) {
qputenv("DISPLAY", ":0");
}
// Fix the Wayland appId. This needs to match the name of the .desktop file without the .desktop
// suffix.
QGuiApplication::setDesktopFileName(QStringLiteral("org.citron_emu.citron"));
#endif
// Call policy attributes BEFORE creating the real QApplication instance
SetHighDPIAttributes();
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
// Disables the "?" button on all dialogs. Disabled by default on Qt6.
QCoreApplication::setAttribute(Qt::AA_DisableWindowContextHelpButton);
#endif
// Enables the core to make the qt created contexts current on std::threads
QCoreApplication::setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity);
QApplication app(argc, argv);
#ifdef _WIN32
OverrideWindowsFont();
#endif
if (is_gamescope) {
app.setStyleSheet(app.styleSheet().append(QStringLiteral(
"QDialog { "
" font-size: 11pt; "
" margin: 0px; "
" padding: 0px; "
"}"
"QLabel { font-size: 10pt; }"
)));
app.setStyle(QStyleFactory::create(QStringLiteral("Fusion")));
}
#ifdef __linux__
if (QGuiApplication::platformName().startsWith(QStringLiteral("wayland"))) {
Settings::values.is_wayland_platform.SetValue(true);
}
#endif
#ifdef CITRON_USE_AUTO_UPDATER
// Check for and apply staged updates before starting the main application
#ifdef CITRON_USE_AUTO_UPDATER
std::filesystem::path app_dir = std::filesystem::path(QCoreApplication::applicationDirPath().toStdString());
#ifdef _WIN32
// On Windows, updates are applied by the helper script after the app exits.
// If we find a staging directory here, it means the helper script failed.
// Clean it up to avoid confusion.
std::filesystem::path staging_path = app_dir / "update_staging";
if (std::filesystem::exists(staging_path)) {
try {
std::filesystem::remove_all(staging_path);
} catch (...) {
// Ignore cleanup errors
}
} catch (...) {}
}
#else
// On Linux, apply staged updates at startup as before
if (Updater::UpdaterService::HasStagedUpdate(app_dir)) {
if (Updater::UpdaterService::ApplyStagedUpdate(app_dir)) {
// Show a simple message that update was applied
QMessageBox::information(nullptr, QObject::tr("Update Applied"),
QObject::tr("Citron has been updated successfully!"));
}
@@ -6207,37 +6261,29 @@ int main(int argc, char* argv[]) {
#endif
#endif
#ifdef _WIN32
OverrideWindowsFont();
#endif
// Workaround for QTBUG-85409, for Suzhou numerals the number 1 is actually \u3021
// so we can see if we get \u3008 instead
// TL;DR all other number formats are consecutive in unicode code points
// This bug is fixed in Qt6, specifically 6.0.0-alpha1
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
const QLocale locale = QLocale::system();
if (QStringLiteral("\u3008") == locale.toString(1)) {
QLocale::setDefault(QLocale::system().name());
}
#endif
// Qt changes the locale and causes issues in float conversion using std::to_string() when
// generating shaders
setlocale(LC_ALL, "C");
GMainWindow main_window{std::move(config), has_broken_vulkan};
app.setStyle(new RainbowStyle(app.style()));
main_window.show();
if (is_gamescope) {
QTimer::singleShot(200, &main_window, [&main_window]() {
main_window.showMaximized();
if (main_window.layout()) {
main_window.layout()->activate();
}
main_window.update();
main_window.raise();
main_window.activateWindow();
});
}
QObject::connect(&app, &QGuiApplication::applicationStateChanged, &main_window,
&GMainWindow::OnAppFocusStateChanged);
int result = app.exec();
detached_tasks.WaitForAllTasks();
return result;
return app.exec();
}
void GMainWindow::OnCheckForUpdates() {

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QComboBox>
@@ -24,10 +25,22 @@
enum class ConnectionType : u8 { TraversalServer, IP };
DirectConnectWindow::DirectConnectWindow(Core::System& system_, QWidget* parent)
: QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
: QDialog(parent),
ui(std::make_unique<Ui::DirectConnect>()), system{system_}, room_network{
system.GetRoomNetwork()} {
const bool is_gamescope = UISettings::IsGamescope();
if (is_gamescope) {
setWindowFlags(Qt::Window | Qt::CustomizeWindowHint | Qt::WindowTitleHint);
setWindowModality(Qt::NonModal);
int w = 800;
int h = 500;
setFixedSize(w, h);
} else {
setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint);
}
ui->setupUi(this);
// setup the watcher for background connections

View File

@@ -32,10 +32,23 @@
HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list,
std::shared_ptr<Core::AnnounceMultiplayerSession> session,
Core::System& system_)
: QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
: QDialog(parent),
ui(std::make_unique<Ui::HostRoom>()),
announce_multiplayer_session(session), system{system_}, room_network{
system.GetRoomNetwork()} {
const bool is_gamescope = UISettings::IsGamescope();
if (is_gamescope) {
setWindowFlags(Qt::Window | Qt::CustomizeWindowHint | Qt::WindowTitleHint);
setWindowModality(Qt::NonModal);
int w = 800;
int h = 500;
setFixedSize(w, h);
} else {
setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint);
}
ui->setupUi(this);
// set up validation for all of the fields

View File

@@ -27,10 +27,23 @@
Lobby::Lobby(QWidget* parent, QStandardItemModel* list,
std::shared_ptr<Core::AnnounceMultiplayerSession> session, Core::System& system_)
: QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
: QDialog(parent),
ui(std::make_unique<Ui::Lobby>()),
announce_multiplayer_session(session), system{system_}, room_network{
system.GetRoomNetwork()} {
const bool is_gamescope = UISettings::IsGamescope();
if (is_gamescope) {
setWindowFlags(Qt::Window | Qt::CustomizeWindowHint | Qt::WindowTitleHint);
setWindowModality(Qt::NonModal);
int w = 800;
int h = 500;
setFixedSize(w, h);
} else {
setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint);
}
ui->setupUi(this);
// setup the watcher for background connections

View File

@@ -136,4 +136,15 @@ namespace UISettings {
config.value(QStringLiteral("microProfileDialogGeometry")).toByteArray();
}
bool IsGamescope() {
#ifdef __linux__
static const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() ||
qgetenv("XDG_CURRENT_DESKTOP") == "gamescope" ||
!qgetenv("STEAM_DECK").isEmpty();
return is_gamescope;
#else
return false;
#endif
}
} // namespace UISettings

View File

@@ -37,6 +37,7 @@ namespace Settings {
namespace UISettings {
bool IsDarkTheme();
bool IsGamescope();
struct ContextualShortcut {
std::string keyseq;

View File

@@ -220,9 +220,18 @@ void UpdaterDialog::OnRestartButtonClicked() {
}
void UpdaterDialog::SetupUI() {
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
const bool is_gamescope = UISettings::IsGamescope();
setMinimumSize(size());
if (is_gamescope) {
// Match the behavior of ConfigureDialog to ensure focus and visibility on Steam Deck
setWindowFlags(Qt::Window | Qt::CustomizeWindowHint | Qt::WindowTitleHint);
setWindowModality(Qt::NonModal);
resize(1100, 700);
} else {
// Desktop remains untouched
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
setMinimumSize(size());
}
ui->currentVersionValue->setText(QString::fromStdString(updater_service->GetCurrentVersion()));
ui->appImageSelectorLabel->setVisible(false);

View File

@@ -121,13 +121,7 @@
</property>
<layout class="QVBoxLayout" name="changelogLayout">
<item>
<widget class="QTextBrowser" name="changelogText">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>150</height>
</size>
</property>
<widget class="QTextBrowser" name="changelogText">
<property name="html">
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
@@ -192,7 +186,6 @@ p, li { white-space: pre-wrap; }
</property>
</spacer>
</item>
<!-- Start of added widgets -->
<item>
<layout class="QHBoxLayout" name="appImageSelectorLayout">
<item>
@@ -207,7 +200,6 @@ p, li { white-space: pre-wrap; }
</item>
</layout>
</item>
<!-- End of added widgets -->
<item>
<layout class="QHBoxLayout" name="buttonLayout">
<item>

View File

@@ -18,12 +18,18 @@
#include "network/room.h"
#include "citron/uisettings.h"
MultiplayerRoomOverlay::MultiplayerRoomOverlay(GMainWindow* parent)
: QWidget(parent), main_window(parent) {
MultiplayerRoomOverlay::MultiplayerRoomOverlay(QWidget* parent)
: QWidget(parent) {
setAttribute(Qt::WA_TranslucentBackground, true);
setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint);
setFocusPolicy(Qt::ClickFocus);
main_window = qobject_cast<GMainWindow*>(parent->window());
// Switched to Qt::Tool to allow keyboard focus for typing in chat
setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
setAttribute(Qt::WA_TranslucentBackground);
// Set smaller sizes for Steam Deck
setMinimumSize(240, 180);
resize(260, 220);
main_layout = new QGridLayout(this);
main_layout->setContentsMargins(padding, padding, 0, 0);
@@ -58,11 +64,25 @@ MultiplayerRoomOverlay::MultiplayerRoomOverlay(GMainWindow* parent)
update_timer.setSingleShot(false);
connect(&update_timer, &QTimer::timeout, this, &MultiplayerRoomOverlay::UpdateRoomData);
connect(parent, &GMainWindow::themeChanged, this, &MultiplayerRoomOverlay::UpdateTheme);
if (main_window) {
connect(main_window, &GMainWindow::themeChanged, this, &MultiplayerRoomOverlay::UpdateTheme);
}
UpdateTheme();
setMinimumSize(280, 220);
resize(320, 280);
const bool is_gamescope = UISettings::IsGamescope();
if (is_gamescope) {
setMinimumSize(320, 260);
resize(600, 520);
players_online_label->setFont(QFont(QString::fromUtf8("Segoe UI"), 11, QFont::Bold));
this->padding = 12;
main_layout->setContentsMargins(padding, padding, padding, padding);
} else {
setMinimumSize(280, 220);
resize(320, 280);
}
UpdatePosition();
}
@@ -108,48 +128,35 @@ void MultiplayerRoomOverlay::resizeEvent(QResizeEvent* event) { QWidget::resizeE
bool MultiplayerRoomOverlay::eventFilter(QObject* watched, QEvent* event) { if (event->type() == QEvent::MouseButtonPress) { if (chat_room_widget->hasFocus()) { chat_room_widget->clearFocus(); } } return QObject::eventFilter(watched, event); }
#if defined(Q_OS_LINUX)
void MultiplayerRoomOverlay::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton) {
if (size_grip->geometry().contains(event->pos())) {
// Let the size grip handle the event
} else if (!childAt(event->pos()) || childAt(event->pos()) == this) {
if (windowHandle()) {
QTimer::singleShot(0, this, [this] { windowHandle()->startSystemMove(); });
}
}
}
QWidget::mousePressEvent(event);
}
void MultiplayerRoomOverlay::mouseMoveEvent(QMouseEvent* event) {
QWidget::mouseMoveEvent(event);
}
#else // Windows and other platforms
void MultiplayerRoomOverlay::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton) {
if (size_grip->geometry().contains(event->pos())) {
// Let the size grip handle the event
} else if (!childAt(event->pos()) || childAt(event->pos()) == this) {
if (event->button() == Qt::LeftButton && !size_grip->geometry().contains(event->pos())) {
const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope";
if (is_gamescope) {
is_dragging = true;
drag_start_pos = event->globalPosition().toPoint();
widget_start_pos = this->pos();
drag_start_pos = event->globalPosition().toPoint() - this->pos();
setCursor(Qt::ClosedHandCursor);
} else {
#if defined(Q_OS_LINUX)
if (windowHandle()) windowHandle()->startSystemMove();
#else
is_dragging = true;
drag_start_pos = event->globalPosition().toPoint() - this->pos();
setCursor(Qt::ClosedHandCursor);
#endif
}
}
QWidget::mousePressEvent(event);
}
void MultiplayerRoomOverlay::mouseMoveEvent(QMouseEvent* event) {
if (is_dragging) {
QPoint delta = event->globalPosition().toPoint() - drag_start_pos;
move(widget_start_pos + delta);
has_been_moved = true;
if (is_dragging && main_window) {
QPoint new_pos = event->globalPosition().toPoint() - drag_start_pos;
QPoint win_origin = main_window->mapToGlobal(QPoint(0, 0));
move(std::clamp(new_pos.x(), win_origin.x(), win_origin.x() + main_window->width() - width()),
std::clamp(new_pos.y(), win_origin.y(), win_origin.y() + main_window->height() - height()));
}
QWidget::mouseMoveEvent(event);
}
#endif
void MultiplayerRoomOverlay::mouseReleaseEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton && is_dragging) {

View File

@@ -22,7 +22,7 @@ class MultiplayerRoomOverlay : public QWidget {
Q_OBJECT
public:
explicit MultiplayerRoomOverlay(GMainWindow* parent);
explicit MultiplayerRoomOverlay(QWidget* parent);
~MultiplayerRoomOverlay() override;
void SetVisible(bool visible);

View File

@@ -5,6 +5,8 @@
#include <QPainter>
#include <QPainterPath>
#include <QScreen>
#include <QSizeGrip>
#include <QGridLayout>
#include <QTimer>
#include <QMouseEvent>
#include <QtMath>
@@ -22,7 +24,7 @@
#include <Windows.h>
#include <comdef.h>
#include <WbemIdl.h>
#pragma comment(lib, "wbemuuid.lib") // For MSVC, helps the linker find the library
#pragma comment(lib, "wbemuuid.lib")
#endif
#ifdef Q_OS_ANDROID
@@ -37,85 +39,91 @@
#include "video_core/gpu.h"
#include "video_core/renderer_base.h"
PerformanceOverlay::PerformanceOverlay(GMainWindow* parent)
: QWidget(parent), main_window(parent) {
PerformanceOverlay::PerformanceOverlay(QWidget* parent) : QWidget(UISettings::IsGamescope() ? nullptr : parent) {
if (parent) {
main_window = qobject_cast<GMainWindow*>(parent);
}
if (UISettings::IsGamescope()) {
setWindowFlags(Qt::ToolTip | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::WindowDoesNotAcceptFocus);
setAttribute(Qt::WA_ShowWithoutActivating);
} else {
setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
}
// Set up the widget properties
setAttribute(Qt::WA_TranslucentBackground, true);
setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint);
setAttribute(Qt::WA_NoSystemBackground);
setAttribute(Qt::WA_WState_ExplicitShowHide);
// Initialize fonts with better typography
title_font = QFont(QString::fromUtf8("Segoe UI"), 9, QFont::Medium);
value_font = QFont(QString::fromUtf8("Segoe UI"), 11, QFont::Bold);
small_font = QFont(QString::fromUtf8("Segoe UI"), 8, QFont::Normal);
if (UISettings::IsGamescope()) {
title_font = QFont(QString::fromUtf8("Segoe UI"), 9, QFont::Bold);
value_font = QFont(QString::fromUtf8("Segoe UI"), 10, QFont::Bold);
small_font = QFont(QString::fromUtf8("Segoe UI"), 8, QFont::Normal);
setMinimumSize(160, 130);
resize(195, 160);
} else {
title_font = QFont(QString::fromUtf8("Segoe UI"), 9, QFont::Medium);
value_font = QFont(QString::fromUtf8("Segoe UI"), 11, QFont::Bold);
small_font = QFont(QString::fromUtf8("Segoe UI"), 8, QFont::Normal);
setMinimumSize(220, 180);
resize(220, 180);
}
temperature_color = QColor(76, 175, 80, 255); // Default to green
auto* layout = new QGridLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
size_grip = new QSizeGrip(this);
layout->addWidget(size_grip, 0, 0, Qt::AlignBottom | Qt::AlignRight);
// Graph colors
temperature_color = QColor(76, 175, 80, 255);
graph_background_color = QColor(40, 40, 40, 100);
graph_line_color = QColor(76, 175, 80, 200);
graph_fill_color = QColor(76, 175, 80, 60);
// Set up timer for updates
update_timer.setSingleShot(false);
connect(&update_timer, &QTimer::timeout, this, &PerformanceOverlay::UpdatePerformanceStats);
// Connect to the main window's theme change signal
connect(parent, &GMainWindow::themeChanged, this, &PerformanceOverlay::UpdateTheme);
// Set the initial theme colors
if (main_window) {
connect(main_window, &GMainWindow::themeChanged, this, &PerformanceOverlay::UpdateTheme);
}
UpdateTheme();
// Set initial size - larger to accommodate the graph
resize(220, 180);
// Position in top-left corner
UpdatePosition();
}
PerformanceOverlay::~PerformanceOverlay() = default;
void PerformanceOverlay::SetVisible(bool visible) {
if (is_visible == visible) {
return;
}
is_visible = visible;
is_enabled = visible;
is_visible = visible; // Update the state so the check works next time
if (visible) {
show();
update_timer.start(500); // Update every 500ms for more accurate data
update_timer.start(500);
} else {
update_timer.stop(); // Stop the timer first
hide();
update_timer.stop();
}
}
void PerformanceOverlay::paintEvent(QPaintEvent* event) {
Q_UNUSED(event)
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setRenderHint(QPainter::TextAntialiasing, true);
// Draw background with rounded corners and subtle shadow effect
QPainterPath background_path;
background_path.addRoundedRect(rect(), corner_radius, corner_radius);
// Draw subtle shadow
QPainterPath shadow_path = background_path.translated(1, 1);
painter.fillPath(shadow_path, QColor(0, 0, 0, 40));
if (!UISettings::IsGamescope()) {
QPainterPath shadow_path = background_path.translated(1, 1);
painter.fillPath(shadow_path, QColor(0, 0, 0, 40));
}
// Draw main background
painter.fillPath(background_path, background_color);
// Draw subtle border
painter.setPen(QPen(border_color, border_width));
painter.drawPath(background_path);
// Draw performance information
DrawPerformanceInfo(painter);
// Draw frame graph
DrawFrameGraph(painter);
}
@@ -124,128 +132,110 @@ void PerformanceOverlay::resizeEvent(QResizeEvent* event) {
UpdatePosition();
}
void PerformanceOverlay::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton && !size_grip->geometry().contains(event->pos())) {
#if defined(Q_OS_LINUX)
// LINUX-SPECIFIC IMPLEMENTATION (Wayland Fix)
void PerformanceOverlay::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton) {
// Hand off window moving responsibility to the OS compositor.
if (windowHandle()) {
if (!UISettings::IsGamescope() && windowHandle()) {
windowHandle()->startSystemMove();
} else {
is_dragging = true;
drag_start_pos = event->globalPosition().toPoint() - this->pos();
}
}
QWidget::mousePressEvent(event);
}
void PerformanceOverlay::mouseMoveEvent(QMouseEvent* event) {
// This function is intentionally left blank for dragging, as the
// system compositor now handles the entire move operation.
QWidget::mouseMoveEvent(event);
}
#else
// ORIGINAL IMPLEMENTATION
void PerformanceOverlay::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton) {
is_dragging = true;
drag_start_pos = event->globalPosition().toPoint();
widget_start_pos = this->pos();
setCursor(Qt::ClosedHandCursor);
drag_start_pos = event->globalPosition().toPoint() - this->pos();
#endif
event->accept();
}
QWidget::mousePressEvent(event);
}
void PerformanceOverlay::mouseMoveEvent(QMouseEvent* event) {
if (is_dragging) {
QPoint delta = event->globalPosition().toPoint() - drag_start_pos;
move(widget_start_pos + delta);
move(event->globalPosition().toPoint() - drag_start_pos);
event->accept();
}
QWidget::mouseMoveEvent(event);
}
#endif
void PerformanceOverlay::mouseReleaseEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton) {
is_dragging = false;
has_been_moved = true;
setCursor(Qt::ArrowCursor);
event->accept();
}
QWidget::mouseReleaseEvent(event);
}
void PerformanceOverlay::UpdatePerformanceStats() {
if (!main_window) {
return;
if (!main_window || !is_enabled) return;
if (UISettings::IsGamescope()) {
bool ui_active = (QApplication::activePopupWidget() != nullptr);
if (!ui_active) {
for (QWidget* w : QApplication::topLevelWidgets()) {
if (w->isVisible() && w != main_window && w != this &&
!w->inherits("GRenderWindow") &&
!w->inherits("VramOverlay") &&
!w->inherits("ControllerOverlay") &&
!w->inherits("PerformanceOverlay")) {
ui_active = true;
break;
}
}
}
if (ui_active) {
if (!this->isHidden()) this->hide();
return;
}
if (this->isHidden()) {
this->show();
}
} else {
// Desktop: Only force a show if the user actually has it enabled in the menu
if (is_enabled && this->isHidden()) {
this->show();
}
}
// Get shader building info (this is safe to call)
shaders_building = main_window->GetShadersBuilding();
// Use a static counter to only call the performance methods occasionally
// This reduces the chance of conflicts with the status bar updates
static int update_counter = 0;
update_counter++;
// Try to get performance data every 2nd update (every 1 second)
if (update_counter % 2 == 0) {
try {
current_fps = main_window->GetCurrentFPS();
current_frame_time = main_window->GetCurrentFrameTime();
emulation_speed = main_window->GetEmulationSpeed();
// Validate the values
if (std::isnan(current_fps) || current_fps < 0.0 || current_fps > 1000.0) {
current_fps = 60.0;
}
if (std::isnan(current_frame_time) || current_frame_time < 0.0 || current_frame_time > 100.0) {
current_frame_time = 16.67;
}
if (std::isnan(emulation_speed) || emulation_speed < 0.0 || emulation_speed > 1000.0) {
emulation_speed = 100.0;
}
if (std::isnan(current_fps) || current_fps < 0.0 || current_fps > 1000.0) current_fps = 60.0;
if (std::isnan(current_frame_time) || current_frame_time < 0.0 || current_frame_time > 100.0) current_frame_time = 16.67;
if (std::isnan(emulation_speed) || emulation_speed < 0.0 || emulation_speed > 1000.0) emulation_speed = 100.0;
// Ensure FPS and frame time are consistent
if (current_fps > 0.0 && current_frame_time > 0.0) {
// Recalculate frame time from FPS to ensure consistency
current_frame_time = 1000.0 / current_fps;
}
} catch (...) {
// If we get an exception, use the last known good values
// Don't reset to defaults immediately
}
if (current_fps > 0.0) current_frame_time = 1000.0 / current_fps;
} catch (...) {}
}
// Update hardware temperatures every 4th update (every 2 seconds)
if (update_counter % 4 == 0) {
UpdateHardwareTemperatures();
}
// If we don't have valid data yet, use defaults
if (std::isnan(current_fps) || current_fps <= 0.0) {
current_fps = 60.0;
}
if (std::isnan(current_frame_time) || current_frame_time <= 0.0) {
current_frame_time = 16.67; // 60 FPS
}
if (std::isnan(emulation_speed) || emulation_speed <= 0.0) {
emulation_speed = 100.0;
}
if (std::isnan(current_fps) || current_fps <= 0.0) current_fps = 60.0;
if (std::isnan(current_frame_time) || current_frame_time <= 0.0) current_frame_time = 16.67;
if (std::isnan(emulation_speed) || emulation_speed <= 0.0) emulation_speed = 100.0;
// Add frame time to graph history (only if it's valid)
if (current_frame_time > 0.0) {
AddFrameTime(current_frame_time);
}
if (current_frame_time > 0.0) AddFrameTime(current_frame_time);
// Update FPS and Temperature colors based on performance
fps_color = GetFpsColor(current_fps);
temperature_color = GetTemperatureColor(std::max({cpu_temperature, gpu_temperature, battery_temperature}));
// Trigger a repaint
update();
}
void PerformanceOverlay::UpdateHardwareTemperatures() {
// Reset data
cpu_temperature = 0.0f;
gpu_temperature = 0.0f;
cpu_sensor_type.clear();
@@ -254,41 +244,79 @@ void PerformanceOverlay::UpdateHardwareTemperatures() {
battery_temperature = 0.0f;
#if defined(Q_OS_LINUX)
// --- Standard Linux Thermal Zone Reading ---
QDir thermal_dir(QString::fromUtf8("/sys/class/thermal/"));
QStringList filters{QString::fromUtf8("thermal_zone*")};
QStringList thermal_zones = thermal_dir.entryList(filters, QDir::Dirs);
// 1. Read Battery Data (Steam Deck / Laptops)
QDir bat_dir(QStringLiteral("/sys/class/power_supply/"));
QStringList bats = bat_dir.entryList({QStringLiteral("BAT*")}, QDir::Dirs);
for (const QString& node : bats) {
QFile cap_file(bat_dir.filePath(node + QStringLiteral("/capacity")));
if (cap_file.open(QIODevice::ReadOnly)) {
battery_percentage = cap_file.readAll().trimmed().toInt();
cap_file.close();
for (const QString& zone_name : thermal_zones) {
QFile type_file(thermal_dir.filePath(zone_name + QString::fromUtf8("/type")));
if (!type_file.open(QIODevice::ReadOnly | QIODevice::Text)) continue;
QString type = QString::fromUtf8(type_file.readAll()).trimmed();
type_file.close();
QFile temp_file(thermal_dir.filePath(zone_name + QString::fromUtf8("/temp")));
if (!temp_file.open(QIODevice::ReadOnly | QIODevice::Text)) continue;
float temp = temp_file.readAll().trimmed().toFloat() / 1000.0f;
temp_file.close();
if (type.contains(QString::fromUtf8("x86_pkg_temp")) || type.contains(QString::fromUtf8("cpu"))) {
if (temp > cpu_temperature) {
cpu_temperature = temp;
cpu_sensor_type = QString::fromUtf8("CPU");
QFile btemp_file(bat_dir.filePath(node + QStringLiteral("/temp")));
if (btemp_file.open(QIODevice::ReadOnly)) {
float raw_temp = btemp_file.readAll().trimmed().toFloat();
// Detect millidegrees (35000) or tenths (350)
battery_temperature = (raw_temp > 1000) ? raw_temp / 1000.0f : raw_temp / 10.0f;
btemp_file.close();
}
} else if (type.contains(QString::fromUtf8("radeon")) || type.contains(QString::fromUtf8("amdgpu")) || type.contains(QString::fromUtf8("nvidia")) || type.contains(QString::fromUtf8("nouveau"))) {
if (temp > gpu_temperature) {
gpu_temperature = temp;
gpu_sensor_type = QString::fromUtf8("GPU");
break;
}
}
// 2. Read APU/CPU Temperatures
QDir hwmon_dir(QStringLiteral("/sys/class/hwmon/"));
QStringList hwmons = hwmon_dir.entryList({QStringLiteral("hwmon*")}, QDir::Dirs);
for (const QString& h_node : hwmons) {
QFile name_file(hwmon_dir.filePath(h_node + QStringLiteral("/name")));
if (!name_file.open(QIODevice::ReadOnly)) continue;
QString hw_name = QString::fromUtf8(name_file.readAll().trimmed());
name_file.close();
// GPU Portion (Standard Steam Deck & Desktop AMD)
if (hw_name == QStringLiteral("amdgpu")) {
QFile t_file(hwmon_dir.filePath(h_node + QStringLiteral("/temp1_input")));
if (t_file.open(QIODevice::ReadOnly)) {
gpu_temperature = t_file.readAll().trimmed().toFloat() / 1000.0f;
gpu_sensor_type = QStringLiteral("GPU");
t_file.close();
}
}
// CPU Portion (k10temp = AMD Deck/Desktop, coretemp = Intel Desktop)
else if (hw_name == QStringLiteral("k10temp") || hw_name == QStringLiteral("coretemp") || hw_name == QStringLiteral("zenpower")) {
// Check for temp1_input (AMD) or temp2_input (Intel coretemp usually starts at 2 for package)
QStringList input_candidates = {QStringLiteral("temp1_input"), QStringLiteral("temp2_input")};
for (const auto& input : input_candidates) {
QFile t_file(hwmon_dir.filePath(h_node + QStringLiteral("/") + input));
if (t_file.open(QIODevice::ReadOnly)) {
cpu_temperature = t_file.readAll().trimmed().toFloat() / 1000.0f;
cpu_sensor_type = QStringLiteral("CPU");
t_file.close();
if (cpu_temperature > 0) break;
}
}
}
}
// 3. Fallback to generic thermal_zones
if (cpu_temperature <= 0.0f) {
QDir thermal_dir(QStringLiteral("/sys/class/thermal/"));
QStringList thermal_zones = thermal_dir.entryList({QStringLiteral("thermal_zone*")}, QDir::Dirs);
for (const QString& zone_name : thermal_zones) {
QFile temp_file(thermal_dir.filePath(zone_name + QStringLiteral("/temp")));
if (temp_file.open(QIODevice::ReadOnly)) {
cpu_temperature = temp_file.readAll().trimmed().toFloat() / 1000.0f;
cpu_sensor_type = QStringLiteral("CPU");
temp_file.close();
if (cpu_temperature > 0) break;
}
}
}
#endif
#if defined(Q_OS_ANDROID)
// This uses QtAndroid Extras to get battery info from the Android system.
// NOTE: This requires the QtAndroidExtras module to be linked in the build.
QJniObject battery_status = QJniObject::callStaticObjectMethod(
"android/content/CONTEXT", "registerReceiver",
"android/content/Context", "registerReceiver",
"(Landroid/content/BroadcastReceiver;Landroid/content/IntentFilter;)Landroid/content/Intent;",
nullptr, new QJniObject("android.content.IntentFilter", "(Ljava/lang/String;)V", "android.intent.action.BATTERY_CHANGED"));
@@ -300,12 +328,8 @@ void PerformanceOverlay::UpdateHardwareTemperatures() {
int temp_tenths = battery_status.callMethod<jint>("getIntExtra", "(Ljava/lang/String;I)I",
QJniObject::fromString("temperature").object<jstring>(), -1);
if (scale > 0) {
battery_percentage = (level * 100) / scale;
}
if (temp_tenths > 0) {
battery_temperature = static_cast<float>(temp_tenths) / 10.0f;
}
if (scale > 0) battery_percentage = (level * 100) / scale;
if (temp_tenths > 0) battery_temperature = static_cast<float>(temp_tenths) / 10.0f;
}
#endif
@@ -313,9 +337,7 @@ void PerformanceOverlay::UpdateHardwareTemperatures() {
HRESULT hres;
IWbemLocator* pLoc = nullptr;
IWbemServices* pSvc = nullptr;
hres = CoCreateInstance(CLSID_WbemLocator, 0, CLSCTX_INPROC_SERVER, IID_IWbemLocator, (LPVOID*)&pLoc);
if (SUCCEEDED(hres)) {
hres = pLoc->ConnectServer(_bstr_t(L"ROOT\\WMI"), NULL, NULL, 0, NULL, 0, 0, &pSvc);
if (SUCCEEDED(hres)) {
@@ -331,12 +353,11 @@ void PerformanceOverlay::UpdateHardwareTemperatures() {
while (pEnumerator) {
pEnumerator->Next(WBEM_INFINITE, 1, &pclsObj, &uReturn);
if (uReturn == 0) break;
VARIANT vtProp;
pclsObj->Get(L"CurrentTemperature", 0, &vtProp, 0, 0);
float temp_kelvin = vtProp.uintVal / 10.0f;
cpu_temperature = temp_kelvin - 273.15f;
cpu_sensor_type = QString::fromUtf8("CPU");
cpu_sensor_type = QStringLiteral("CPU");
VariantClear(&vtProp);
pclsObj->Release();
}
@@ -351,12 +372,7 @@ void PerformanceOverlay::UpdateHardwareTemperatures() {
}
void PerformanceOverlay::UpdatePosition() {
if (!main_window) {
return;
}
// Only position in top-left corner if we haven't been moved by the user
if (!has_been_moved) {
if (main_window && !has_been_moved) {
QPoint main_window_pos = main_window->mapToGlobal(QPoint(0, 0));
move(main_window_pos.x() + 10, main_window_pos.y() + 10);
}
@@ -365,92 +381,85 @@ void PerformanceOverlay::UpdatePosition() {
void PerformanceOverlay::DrawPerformanceInfo(QPainter& painter) {
painter.setRenderHint(QPainter::TextAntialiasing, true);
int y_offset = padding;
const int line_height = 20;
// Dynamic spacing based on font size to prevent squishing
const int title_step = painter.fontMetrics().height() + 2;
const int stat_step = painter.fontMetrics().height() + 2;
// Draw title
int y_left = (padding / 2) + painter.fontMetrics().ascent();
int y_right = y_left + 10;
// 1. Draw Title (Left)
painter.setFont(title_font);
painter.setPen(text_color);
painter.drawText(padding, y_offset + 12, QString::fromUtf8("CITRON"));
painter.drawText(padding, y_left, QStringLiteral("CITRON PERFORMANCE"));
int y_offset_right = padding;
const int line_height_right = 18;
// Draw Temperatures
// 2. Draw Hardware Stats (Right Column)
painter.setFont(small_font);
const int hw_step = UISettings::IsGamescope() ? 16 : 20;
float core_temp_to_display = std::max(cpu_temperature, gpu_temperature);
if (core_temp_to_display > 0.0f) {
QString core_label = gpu_temperature > cpu_temperature ? gpu_sensor_type : cpu_sensor_type;
QString core_temp_text = QString::fromUtf8("%1: %2°C").arg(core_label).arg(core_temp_to_display, 0, 'f', 0);
painter.setPen(GetTemperatureColor(core_temp_to_display));
int text_width = painter.fontMetrics().horizontalAdvance(core_temp_text);
painter.drawText(width() - padding - text_width, y_offset_right + 12, core_temp_text);
if (cpu_temperature > 0.0f) {
QString cpu_text = QStringLiteral("CPU:%1°C").arg(cpu_temperature, 0, 'f', 0);
painter.setPen(GetTemperatureColor(cpu_temperature));
int tw = painter.fontMetrics().horizontalAdvance(cpu_text);
painter.drawText(width() - padding - tw, y_right, cpu_text);
y_right += hw_step;
}
if (gpu_temperature > 0.0f) {
QString gpu_text = QStringLiteral("GPU:%1°C").arg(gpu_temperature, 0, 'f', 0);
painter.setPen(GetTemperatureColor(gpu_temperature));
int tw = painter.fontMetrics().horizontalAdvance(gpu_text);
painter.drawText(width() - padding - tw, y_right, gpu_text);
y_right += hw_step;
}
y_offset_right += line_height_right;
// Draw Battery info
if (battery_percentage > 0) {
QString batt_text = QString::fromUtf8("Batt: %1%").arg(battery_percentage);
QString batt_text = QStringLiteral("Battery %:%1%").arg(battery_percentage);
if (battery_temperature > 0.0f) {
batt_text += QString::fromUtf8(" (%1°C)").arg(battery_temperature, 0, 'f', 0);
batt_text += QStringLiteral(" (%1°C)").arg(battery_temperature, 0, 'f', 0);
}
painter.setPen(text_color);
int text_width = painter.fontMetrics().horizontalAdvance(batt_text);
painter.drawText(width() - padding - text_width, y_offset_right + 12, batt_text);
int tw = painter.fontMetrics().horizontalAdvance(batt_text);
painter.drawText(width() - padding - tw, y_right, batt_text);
}
y_offset += line_height + 4;
// Draw FPS
// 3. Draw FPS (Left Column)
y_left += title_step;
painter.setFont(value_font);
painter.setPen(fps_color);
QString fps_text = QString::fromUtf8("%1 FPS").arg(FormatFps(current_fps));
painter.drawText(padding, y_offset, fps_text);
y_offset += line_height;
painter.drawText(padding, y_left, QStringLiteral("%1 FPS").arg(FormatFps(current_fps)));
// Draw frame time
// 4. Draw Small Stats (Left Column)
y_left += title_step;
painter.setFont(small_font);
painter.setPen(text_color);
QString frame_time_text = QString::fromUtf8("Frame: %1 ms").arg(FormatFrameTime(current_frame_time));
painter.drawText(padding, y_offset, frame_time_text);
y_offset += line_height - 2;
painter.drawText(padding, y_left, QStringLiteral("Frame:%1 ms").arg(FormatFrameTime(current_frame_time)));
// Draw emulation speed
QString speed_text = QString::fromUtf8("Speed: %1%").arg(emulation_speed, 0, 'f', 0);
painter.drawText(padding, y_offset, speed_text);
y_offset += line_height - 2;
y_left += stat_step;
painter.drawText(padding, y_left, QStringLiteral("Speed:%1%").arg(emulation_speed, 0, 'f', 0));
// Draw shader building info with accent color
if (shaders_building > 0) {
painter.setPen(QColor(255, 152, 0, 255)); // Material Design orange
QString shader_text = QString::fromUtf8("Building: %1 shader(s)").arg(shaders_building);
painter.drawText(padding, y_offset, shader_text);
y_left += stat_step;
painter.setPen(QColor(255, 152, 0));
painter.drawText(padding, y_left, QStringLiteral("Building:%1").arg(shaders_building));
}
}
void PerformanceOverlay::DrawFrameGraph(QPainter& painter) {
if (frame_times.empty()) {
return;
}
if (frame_times.empty()) return;
const int graph_y = height() - graph_height - padding;
const int graph_width = width() - (padding * 2);
const QRect graph_rect(padding, graph_y, graph_width, graph_height);
// Draw graph background
painter.fillRect(graph_rect, graph_background_color);
// Calculate graph bounds
const double min_val = std::max(0.0, min_frame_time - 1.0);
const double max_val = std::max(16.67, max_frame_time + 1.0); // 16.67ms = 60 FPS
const double max_val = std::max(16.67, max_frame_time + 1.0);
const double range = max_val - min_val;
if (range <= 0.0) return;
if (range <= 0.0) {
return;
}
// Draw grid lines
// Grid lines
painter.setPen(QPen(QColor(80, 80, 80, 100), 1));
const int grid_lines = 4;
for (int i = 1; i < grid_lines; ++i) {
@@ -458,12 +467,11 @@ void PerformanceOverlay::DrawFrameGraph(QPainter& painter) {
painter.drawLine(graph_rect.left(), y, graph_rect.right(), y);
}
// Draw 60 FPS line (16.67ms)
// 60 FPS Target line
const int fps60_y = graph_y + graph_height - static_cast<int>((16.67 - min_val) / range * graph_height);
painter.setPen(QPen(QColor(255, 255, 255, 80), 1, Qt::DashLine));
painter.drawLine(graph_rect.left(), fps60_y, graph_rect.right(), fps60_y);
// Draw frame time line
painter.setPen(QPen(graph_line_color, 2));
painter.setBrush(graph_fill_color);
@@ -476,45 +484,39 @@ void PerformanceOverlay::DrawFrameGraph(QPainter& painter) {
const double normalized_y = (frame_time - min_val) / range;
const int x = graph_rect.left() + static_cast<int>(i * x_step);
const int y = graph_y + graph_height - static_cast<int>(normalized_y * graph_height);
if (i == 0) {
graph_path.moveTo(x, y);
} else {
graph_path.lineTo(x, y);
}
if (i == 0) graph_path.moveTo(x, y); else graph_path.lineTo(x, y);
}
// Close the path for filling
graph_path.lineTo(graph_rect.right(), graph_rect.bottom());
graph_path.lineTo(graph_rect.left(), graph_rect.bottom());
graph_path.closeSubpath();
painter.drawPath(graph_path);
// Draw statistics text
painter.setFont(small_font);
painter.setPen(text_color);
const QString min_text = QString::fromUtf8("Min: %1ms").arg(FormatFrameTime(min_frame_time));
const QString avg_text = QString::fromUtf8("Avg: %1ms").arg(FormatFrameTime(avg_frame_time));
const QString max_text = QString::fromUtf8("Max: %1ms").arg(FormatFrameTime(max_frame_time));
const QString min_str = QStringLiteral("Min:%1ms").arg(FormatFrameTime(min_frame_time));
const QString avg_str = QStringLiteral("Avg:%2ms").arg(FormatFrameTime(avg_frame_time));
const QString max_str = QStringLiteral("Max:%1ms").arg(FormatFrameTime(max_frame_time));
painter.drawText(graph_rect.left(), graph_y - 5, min_text);
painter.drawText(graph_rect.center().x() - painter.fontMetrics().horizontalAdvance(avg_text) / 2,
graph_y - 5, avg_text);
painter.drawText(graph_rect.right() - painter.fontMetrics().horizontalAdvance(max_text),
graph_y - 5, max_text);
// Combine into one line for measurement
const QString full_line = QStringLiteral("%1 %2 %3").arg(min_str, avg_str, max_str);
int total_width = painter.fontMetrics().horizontalAdvance(full_line);
// If there is enough room, flatten it across the top. Otherwise, stack it.
if (total_width < graph_width - 10) {
// Flat layout
painter.drawText(graph_rect.left(), graph_y - 6, full_line);
} else {
// Stacked layout (Fallback for small windows/High-DPI scaling)
painter.drawText(graph_rect.left(), graph_y - 18, QStringLiteral("%1 %2").arg(min_str, avg_str));
painter.drawText(graph_rect.left(), graph_y - 4, max_str);
}
}
void PerformanceOverlay::AddFrameTime(double frame_time_ms) {
frame_times.push_back(frame_time_ms);
// Keep only the last MAX_FRAME_HISTORY frames
if (frame_times.size() > MAX_FRAME_HISTORY) {
frame_times.pop_front();
}
// Update statistics
if (frame_times.size() > MAX_FRAME_HISTORY) frame_times.pop_front();
if (!frame_times.empty()) {
min_frame_time = *std::min_element(frame_times.begin(), frame_times.end());
max_frame_time = *std::max_element(frame_times.begin(), frame_times.end());
@@ -523,54 +525,39 @@ void PerformanceOverlay::AddFrameTime(double frame_time_ms) {
}
QColor PerformanceOverlay::GetFpsColor(double fps) const {
if (fps >= 55.0) {
return QColor(76, 175, 80, 255); // Material Design green - Good performance
} else if (fps >= 45.0) {
return QColor(255, 152, 0, 255); // Material Design orange - Moderate performance
} else if (fps >= 30.0) {
return QColor(255, 87, 34, 255); // Material Design deep orange - Poor performance
} else {
return QColor(244, 67, 54, 255); // Material Design red - Very poor performance
}
if (fps >= 55.0) return QColor(76, 175, 80, 255);
if (fps >= 45.0) return QColor(255, 152, 0, 255);
if (fps >= 30.0) return QColor(255, 87, 34, 255);
return QColor(244, 67, 54, 255);
}
QColor PerformanceOverlay::GetTemperatureColor(float temperature) const {
if (temperature > 70.0f) {
return QColor(244, 67, 54, 255); // Material Design red
} else if (temperature > 60.0f) {
return QColor(255, 152, 0, 255); // Material Design orange
} else {
return QColor(76, 175, 80, 255); // Material Design green
}
if (temperature > 85.0f) return QColor(244, 67, 54, 255);
if (temperature > 75.0f) return QColor(255, 152, 0, 255);
return QColor(76, 175, 80, 255);
}
QString PerformanceOverlay::FormatFps(double fps) const {
if (std::isnan(fps) || fps < 0.0) {
return QString::fromUtf8("0.0");
}
if (std::isnan(fps) || fps < 0.0) return QString::fromUtf8("0.0");
return QString::number(fps, 'f', 1);
}
QString PerformanceOverlay::FormatFrameTime(double frame_time_ms) const {
if (std::isnan(frame_time_ms) || frame_time_ms < 0.0) {
return QString::fromUtf8("0.00");
}
if (std::isnan(frame_time_ms) || frame_time_ms < 0.0) return QString::fromUtf8("0.00");
return QString::number(frame_time_ms, 'f', 2);
}
void PerformanceOverlay::UpdateTheme() {
if (UISettings::IsDarkTheme()) {
// Dark Theme Colors (your original values)
background_color = QColor(20, 20, 20, 200); // Slightly more opaque
background_color = QColor(20, 20, 20, 200);
border_color = QColor(60, 60, 60, 120);
text_color = QColor(220, 220, 220, 255);
graph_background_color = QColor(40, 40, 40, 100);
} else {
// Light Theme Colors
background_color = QColor(245, 245, 245, 220);
border_color = QColor(200, 200, 200, 120);
text_color = QColor(20, 20, 20, 255);
graph_background_color = QColor(220, 220, 220, 100);
}
update(); // Force a repaint with the new colors
update();
}

View File

@@ -15,16 +15,18 @@
#include "citron/uisettings.h"
class GMainWindow;
class QSizeGrip;
class PerformanceOverlay : public QWidget {
Q_OBJECT
public:
explicit PerformanceOverlay(GMainWindow* parent);
explicit PerformanceOverlay(QWidget* parent);
~PerformanceOverlay() override;
void SetVisible(bool visible);
bool IsVisible() const { return is_visible; }
void setMainWindow(GMainWindow* window) { main_window = window; }
public slots:
void UpdateTheme();
@@ -40,6 +42,9 @@ private slots:
void UpdatePerformanceStats();
private:
bool is_enabled = false;
bool is_visible = false;
void UpdatePosition();
void UpdateHardwareTemperatures();
void DrawPerformanceInfo(QPainter& painter);
@@ -51,6 +56,7 @@ private:
void AddFrameTime(double frame_time_ms);
GMainWindow* main_window;
QSizeGrip* size_grip;
QTimer update_timer;
// Performance data
@@ -66,14 +72,13 @@ private:
float battery_temperature = 0.0f;
// Frame graph data
static constexpr size_t MAX_FRAME_HISTORY = 120; // 2 seconds at 60 FPS
static constexpr size_t MAX_FRAME_HISTORY = 120;
std::deque<double> frame_times;
double min_frame_time = 0.0;
double max_frame_time = 0.0;
double avg_frame_time = 0.0;
// Display settings
bool is_visible = false;
QFont title_font;
QFont value_font;
QFont small_font;

View File

@@ -5,6 +5,8 @@
#include <QPainter>
#include <QPainterPath>
#include <QScreen>
#include <QSizeGrip>
#include <QGridLayout>
#include <QTimer>
#include <QMouseEvent>
#include <QtMath>
@@ -25,58 +27,72 @@
#include "video_core/renderer_vulkan/vk_rasterizer.h"
#include "common/settings.h"
VramOverlay::VramOverlay(GMainWindow* parent)
: QWidget(parent), main_window(parent) {
VramOverlay::VramOverlay(QWidget* parent) : QWidget(UISettings::IsGamescope() ? nullptr : parent) {
if (parent) {
main_window = qobject_cast<GMainWindow*>(parent);
}
if (UISettings::IsGamescope()) {
setWindowFlags(Qt::ToolTip | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::WindowDoesNotAcceptFocus);
setAttribute(Qt::WA_ShowWithoutActivating);
} else {
setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
}
// Set up the widget properties
setAttribute(Qt::WA_TranslucentBackground, true);
setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint);
setAttribute(Qt::WA_NoSystemBackground);
setAttribute(Qt::WA_WState_ExplicitShowHide);
// Initialize fonts with clean typography
title_font = QFont(QString::fromUtf8("Segoe UI"), 11, QFont::Bold);
value_font = QFont(QString::fromUtf8("Segoe UI"), 10, QFont::Medium);
small_font = QFont(QString::fromUtf8("Segoe UI"), 9, QFont::Normal);
warning_font = QFont(QString::fromUtf8("Segoe UI"), 10, QFont::Bold);
// Branching Typography and Sizing
if (UISettings::IsGamescope()) {
title_font = QFont(QString::fromUtf8("Segoe UI"), 7, QFont::Bold);
value_font = QFont(QString::fromUtf8("Segoe UI"), 7, QFont::Medium);
small_font = QFont(QString::fromUtf8("Segoe UI"), 6, QFont::Normal);
warning_font = QFont(QString::fromUtf8("Segoe UI"), 8, QFont::Bold);
setMinimumSize(180, 140);
resize(200, 160);
} else {
title_font = QFont(QString::fromUtf8("Segoe UI"), 11, QFont::Bold);
value_font = QFont(QString::fromUtf8("Segoe UI"), 10, QFont::Medium);
small_font = QFont(QString::fromUtf8("Segoe UI"), 9, QFont::Normal);
warning_font = QFont(QString::fromUtf8("Segoe UI"), 10, QFont::Bold);
setMinimumSize(250, 180);
resize(250, 180);
}
// VRAM usage colors - modern palette
vram_safe_color = QColor(76, 175, 80, 255);
vram_warning_color = QColor(255, 193, 7, 255);
vram_danger_color = QColor(244, 67, 54, 255);
leak_warning_color = QColor(255, 152, 0, 255);
// Graph colors - clean and modern
graph_background_color = QColor(25, 25, 25, 255);
graph_grid_color = QColor(60, 60, 60, 100);
graph_line_color = QColor(76, 175, 80, 255);
graph_fill_color = QColor(76, 175, 80, 40);
auto* layout = new QGridLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
size_grip = new QSizeGrip(this);
layout->addWidget(size_grip, 0, 0, Qt::AlignBottom | Qt::AlignRight);
// Set up timer for updates
update_timer.setSingleShot(false);
connect(&update_timer, &QTimer::timeout, this, &VramOverlay::UpdateVramStats);
connect(parent, &GMainWindow::themeChanged, this, &VramOverlay::UpdateTheme);
if (main_window) {
connect(main_window, &GMainWindow::themeChanged, this, &VramOverlay::UpdateTheme);
}
UpdateTheme();
// Set clean, compact size
resize(250, 180);
// Position in top-right corner
UpdatePosition();
}
VramOverlay::~VramOverlay() = default;
void VramOverlay::SetVisible(bool visible) {
if (is_visible == visible) {
return;
}
is_visible = visible;
is_enabled = visible;
is_visible = visible; // Properly sync the internal state
if (visible) {
show();
update_timer.start(1000); // Update every 1 second
update_timer.start(1000);
} else {
update_timer.stop(); // Ensure the background loop stops updating
hide();
update_timer.stop();
}
}
@@ -109,9 +125,9 @@ void VramOverlay::paintEvent(QPaintEvent* event) {
}
void VramOverlay::DrawVramInfo(QPainter& painter) {
const int section_padding = 12;
const int line_height = 14;
const int section_spacing = 6;
const int section_padding = UISettings::IsGamescope() ? 5 : 12;
const int line_height = UISettings::IsGamescope() ? 11 : 14;
const int section_spacing = UISettings::IsGamescope() ? 2 : 6;
int y_offset = section_padding + 4;
painter.setFont(title_font);
@@ -123,22 +139,19 @@ void VramOverlay::DrawVramInfo(QPainter& painter) {
QColor vram_color = GetVramColor(current_vram_data.vram_percentage);
painter.setPen(vram_color);
QString vram_text = QString::fromUtf8("%1 / %2 (%3%)")
.arg(FormatMemorySize(current_vram_data.used_vram))
.arg(FormatMemorySize(current_vram_data.total_vram))
.arg(FormatPercentage(current_vram_data.vram_percentage));
.arg(FormatMemorySize(current_vram_data.used_vram))
.arg(FormatMemorySize(current_vram_data.total_vram))
.arg(FormatPercentage(current_vram_data.vram_percentage));
painter.drawText(section_padding, y_offset, vram_text);
y_offset += line_height + section_spacing;
painter.setFont(small_font);
painter.setPen(secondary_text_color);
QString buffer_text = QString::fromUtf8("Buffers: %1").arg(FormatMemorySize(current_vram_data.buffer_memory));
painter.drawText(section_padding, y_offset, buffer_text);
y_offset += line_height - 1;
QString texture_text = QString::fromUtf8("Textures: %1").arg(FormatMemorySize(current_vram_data.texture_memory));
painter.drawText(section_padding, y_offset, texture_text);
y_offset += line_height - 1;
QString staging_text = QString::fromUtf8("Staging: %1").arg(FormatMemorySize(current_vram_data.staging_memory));
painter.drawText(section_padding, y_offset, staging_text);
painter.drawText(section_padding, y_offset, QString::fromUtf8("Buffers: %1").arg(FormatMemorySize(current_vram_data.buffer_memory)));
y_offset += line_height - (UISettings::IsGamescope() ? 0 : 1);
painter.drawText(section_padding, y_offset, QString::fromUtf8("Textures: %1").arg(FormatMemorySize(current_vram_data.texture_memory)));
y_offset += line_height - (UISettings::IsGamescope() ? 0 : 1);
painter.drawText(section_padding, y_offset, QString::fromUtf8("Staging: %1").arg(FormatMemorySize(current_vram_data.staging_memory)));
y_offset += line_height + section_spacing;
painter.setPen(secondary_text_color);
@@ -157,9 +170,9 @@ void VramOverlay::DrawVramGraph(QPainter& painter) {
if (vram_usage_history.empty()) return;
const int graph_padding = 12;
const int graph_y = height() - 60;
const int graph_y = height() - (UISettings::IsGamescope() ? 50 : 60);
const int graph_width = width() - (graph_padding * 2);
const int local_graph_height = 40;
const int local_graph_height = UISettings::IsGamescope() ? 30 : 40;
QRect graph_rect(graph_padding, graph_y, graph_width, local_graph_height);
QPainterPath graph_path;
@@ -169,13 +182,10 @@ void VramOverlay::DrawVramGraph(QPainter& painter) {
painter.setPen(QPen(graph_grid_color, 1));
painter.drawPath(graph_path);
for (int i = 1; i < 4; ++i) {
int y = graph_y + (i * local_graph_height / 4);
painter.drawLine(graph_padding + 1, y, graph_padding + graph_width - 1, y);
}
if (vram_usage_history.size() > 1) {
painter.setPen(QPen(graph_line_color, 2));
QColor dynamic_color = current_vram_data.leak_detected ? leak_warning_color : GetVramColor(current_vram_data.vram_percentage);
painter.setPen(QPen(dynamic_color, 2));
QPainterPath line_path;
for (size_t i = 0; i < vram_usage_history.size(); ++i) {
double x = graph_padding + 2 + (static_cast<double>(i) / (vram_usage_history.size() - 1)) * (graph_width - 4);
@@ -187,7 +197,9 @@ void VramOverlay::DrawVramGraph(QPainter& painter) {
line_path.lineTo(graph_padding + graph_width - 2, graph_y + local_graph_height - 2);
line_path.lineTo(graph_padding + 2, graph_y + local_graph_height - 2);
line_path.closeSubpath();
painter.fillPath(line_path, graph_fill_color);
// Fill using the dynamic color with transparency
painter.fillPath(line_path, QColor(dynamic_color.red(), dynamic_color.green(), dynamic_color.blue(), 40));
}
}
@@ -208,54 +220,74 @@ void VramOverlay::resizeEvent(QResizeEvent* event) {
UpdatePosition();
}
void VramOverlay::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton && !size_grip->geometry().contains(event->pos())) {
#if defined(Q_OS_LINUX)
// LINUX-SPECIFIC IMPLEMENTATION (Wayland Fix)
void VramOverlay::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton) {
if (windowHandle()) {
if (!UISettings::IsGamescope() && windowHandle()) {
windowHandle()->startSystemMove();
} else {
is_dragging = true;
drag_start_pos = event->globalPosition().toPoint() - this->pos();
}
}
QWidget::mousePressEvent(event);
}
void VramOverlay::mouseMoveEvent(QMouseEvent* event) {
// Intentionally blank, the system compositor handles the move.
QWidget::mouseMoveEvent(event);
}
#else
// ORIGINAL IMPLEMENTATION (For Windows, Android, etc.)
void VramOverlay::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton) {
is_dragging = true;
drag_start_pos = event->globalPosition().toPoint();
widget_start_pos = pos();
setCursor(Qt::ClosedHandCursor);
drag_start_pos = event->globalPosition().toPoint() - this->pos();
#endif
event->accept();
}
QWidget::mousePressEvent(event);
}
void VramOverlay::mouseMoveEvent(QMouseEvent* event) {
if (is_dragging) {
QPoint delta = event->globalPosition().toPoint() - drag_start_pos;
move(widget_start_pos + delta);
move(event->globalPosition().toPoint() - drag_start_pos);
event->accept();
}
QWidget::mouseMoveEvent(event);
}
#endif
void VramOverlay::mouseReleaseEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton) {
is_dragging = false;
has_been_moved = true;
setCursor(Qt::ArrowCursor);
event->accept();
}
QWidget::mouseReleaseEvent(event);
}
void VramOverlay::UpdateVramStats() {
if (!main_window) return;
if (!main_window || !is_enabled) return;
if (UISettings::IsGamescope()) {
bool ui_active = (QApplication::activePopupWidget() != nullptr);
if (!ui_active) {
for (QWidget* w : QApplication::topLevelWidgets()) {
if (w->isVisible() && w != main_window && w != this &&
!w->inherits("GRenderWindow") &&
!w->inherits("PerformanceOverlay") &&
!w->inherits("ControllerOverlay") &&
!w->inherits("VramOverlay")) {
ui_active = true;
break;
}
}
}
if (ui_active) {
if (!this->isHidden()) this->hide();
return;
}
if (this->isHidden()) {
this->show();
}
} else {
// Desktop: Respect the menu toggle strictly
if (is_enabled && this->isHidden()) {
this->show();
}
}
try {
current_vram_data.total_vram = main_window->GetTotalVram();
current_vram_data.used_vram = main_window->GetUsedVram();
@@ -282,11 +314,12 @@ void VramOverlay::UpdateVramStats() {
}
last_vram_usage = current_vram_data.used_vram;
}
AddVramUsage(current_vram_data.vram_percentage);
vram_usage_history.push_back(current_vram_data.vram_percentage);
if (vram_usage_history.size() > MAX_VRAM_HISTORY) vram_usage_history.pop_front();
update();
} catch (...) {
// Ignore
}
} catch (...) {}
}
QColor VramOverlay::GetVramColor(double percentage) const {
@@ -306,24 +339,8 @@ QString VramOverlay::FormatPercentage(double percentage) const {
return QString::number(percentage, 'f', 1);
}
void VramOverlay::AddVramUsage(double percentage) {
vram_usage_history.push_back(percentage);
if (vram_usage_history.size() > MAX_VRAM_HISTORY) {
vram_usage_history.pop_front();
}
if (!vram_usage_history.empty()) {
min_vram_usage = *std::min_element(vram_usage_history.begin(), vram_usage_history.end());
max_vram_usage = *std::max_element(vram_usage_history.begin(), vram_usage_history.end());
double range = max_vram_usage - min_vram_usage;
if (range < 10.0) range = 10.0;
min_vram_usage = std::max(0.0, min_vram_usage - range * 0.1);
max_vram_usage = std::min(100.0, max_vram_usage + range * 0.1);
}
}
void VramOverlay::UpdateTheme() {
if (UISettings::IsDarkTheme()) {
// Dark Theme Colors (your original values)
background_color = QColor(15, 15, 15, 220);
border_color = QColor(45, 45, 45, 255);
text_color = QColor(240, 240, 240, 255);
@@ -331,7 +348,6 @@ void VramOverlay::UpdateTheme() {
graph_background_color = QColor(25, 25, 25, 255);
graph_grid_color = QColor(60, 60, 60, 100);
} else {
// Light Theme Colors
background_color = QColor(245, 245, 245, 220);
border_color = QColor(200, 200, 200, 255);
text_color = QColor(20, 20, 20, 255);
@@ -339,5 +355,5 @@ void VramOverlay::UpdateTheme() {
graph_background_color = QColor(225, 225, 225, 255);
graph_grid_color = QColor(190, 190, 190, 100);
}
update(); // Force a repaint
update();
}

View File

@@ -15,6 +15,7 @@
#include "citron/uisettings.h"
class GMainWindow;
class QSizeGrip;
struct VramUsageData {
u64 total_vram = 0;
@@ -33,7 +34,7 @@ class VramOverlay : public QWidget {
Q_OBJECT
public:
explicit VramOverlay(GMainWindow* parent);
explicit VramOverlay(QWidget* parent);
~VramOverlay() override;
void SetVisible(bool visible);
@@ -53,6 +54,9 @@ private slots:
void UpdateVramStats();
private:
bool is_enabled = false;
bool is_visible = false;
void UpdatePosition();
void DrawVramInfo(QPainter& painter);
void DrawVramGraph(QPainter& painter);
@@ -63,6 +67,7 @@ private:
void AddVramUsage(double percentage);
GMainWindow* main_window;
QSizeGrip* size_grip;
QTimer update_timer;
// VRAM data
@@ -71,13 +76,12 @@ private:
u32 frame_counter = 0;
// VRAM graph data
static constexpr size_t MAX_VRAM_HISTORY = 120; // 2 seconds at 60 FPS
static constexpr size_t MAX_VRAM_HISTORY = 120;
std::deque<double> vram_usage_history;
double min_vram_usage = 0.0;
double max_vram_usage = 100.0;
// Display settings
bool is_visible = false;
bool is_dragging = false;
bool has_been_moved = false;
QPoint drag_start_pos;