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-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <QDialogButtonBox>
#include <QLabel>
#include <QPushButton>
#include <QVBoxLayout>
#include <QIcon> #include <QIcon>
#include <fmt/format.h> #include <fmt/format.h>
#include "common/scm_rev.h" #include "common/scm_rev.h"
#include "ui_aboutdialog.h" #include "ui_aboutdialog.h"
#include "citron/about_dialog.h" #include "citron/about_dialog.h"
#include "citron/uisettings.h"
AboutDialog::AboutDialog(QWidget* parent) 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"; std::string citron_build_version = "citron | 0.12.25";
#ifdef CITRON_ENABLE_PGO_USE #ifdef CITRON_ENABLE_PGO_USE
citron_build_version += " | PGO"; citron_build_version += " | PGO";
#endif #endif
ui->setupUi(this); if (is_gamescope) {
// Try and request the icon from Qt theme (Linux?) resize(700, 450);
const QIcon citron_logo = QIcon::fromTheme(QStringLiteral("org.citron_emu.citron"));
if (!citron_logo.isNull()) { // Scale fonts up slightly so they aren't "too small"
ui->labelLogo->setPixmap(citron_logo.pixmap(200)); 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->setText(
ui->labelBuildInfo->text().arg(QString::fromStdString(citron_build_version), ui->labelBuildInfo->text().arg(QString::fromStdString(citron_build_version),
QString::fromUtf8(Common::g_build_date).left(10))); QString::fromUtf8(Common::g_build_date).left(10)));

View File

@@ -3,147 +3,69 @@
<class>AboutDialog</class> <class>AboutDialog</class>
<widget class="QDialog" name="AboutDialog"> <widget class="QDialog" name="AboutDialog">
<property name="geometry"> <property name="geometry">
<rect> <rect><x>0</x><y>0</y><width>620</width><height>300</height></rect>
<x>0</x>
<y>0</y>
<width>622</width>
<height>294</height>
</rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string>About citron</string> <string>About citron</string>
</property> </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> <item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1"> <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1">
<item> <item>
<layout class="QVBoxLayout" name="verticalLayout_2"> <layout class="QVBoxLayout" name="logoColumn">
<item> <item>
<widget class="QLabel" name="labelLogo"> <widget class="QLabel" name="labelLogo">
<property name="sizePolicy"> <property name="minimumSize"><size><width>160</width><height>160</height></size></property>
<sizepolicy hsizetype="Preferred" vsizetype="Preferred"> <property name="text"><string/></property>
<horstretch>0</horstretch> <property name="scaledContents"><bool>true</bool></property>
<verstretch>0</verstretch> <property name="alignment"><set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set></property>
</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>
</widget> </widget>
</item> </item>
<item> <item><spacer name="vS1"><property name="orientation"><enum>Qt::Vertical</enum></property></spacer></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>
</layout> </layout>
</item> </item>
<item> <item>
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="textColumn">
<property name="spacing"><number>6</number></property>
<item> <item>
<widget class="QLabel" name="labelCitron"> <widget class="QLabel" name="labelCitron">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text"> <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> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QLabel" name="labelBuildInfo"> <widget class="QLabel" name="labelBuildInfo">
<property name="sizePolicy"> <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>
<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> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QLabel" name="labelAbout"> <widget class="QLabel" name="labelAbout">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text"> <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; <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>
&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>
</property> </property>
<property name="wordWrap"><bool>true</bool></property>
</widget> </widget>
</item> </item>
<item> <item><spacer name="vS2"><property name="orientation"><enum>Qt::Vertical</enum></property></spacer></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> <item>
<widget class="QLabel" name="labelLinks"> <widget class="QLabel" name="labelLinks">
<property name="text"> <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> <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> </property>
<property name="openExternalLinks"><bool>true</bool></property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QLabel" name="labelLiability"> <widget class="QLabel" name="labelLiability">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text"> <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> </property>
</widget> </widget>
</item> </item>
@@ -153,52 +75,17 @@ p, li { white-space: pre-wrap; }
</item> </item>
<item> <item>
<widget class="QDialogButtonBox" name="buttonBox"> <widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation"> <property name="standardButtons"><set>QDialogButtonBox::Ok</set></property>
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Ok</set>
</property>
</widget> </widget>
</item> </item>
</layout> </layout>
</widget> </widget>
<resources>
<include location="../../dist/qt_themes_default/default/default.qrc"/>
<include location="../../dist/qt_themes/default/default.qrc"/>
</resources>
<connections> <connections>
<connection> <connection>
<sender>buttonBox</sender> <sender>buttonBox</sender>
<signal>accepted()</signal> <signal>accepted()</signal>
<receiver>AboutDialog</receiver> <receiver>AboutDialog</receiver>
<slot>accept()</slot> <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> </connection>
</connections> </connections>
</ui> </ui>

View File

@@ -108,8 +108,17 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_,
} }
Settings::SetConfiguringGlobal(true); 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); ui->setupUi(this);
auto* animation_filter = new StyleAnimationEventFilter(this); auto* animation_filter = new StyleAnimationEventFilter(this);
@@ -128,9 +137,13 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_,
ui->topButtonWidget->setLayout(nav_layout); ui->topButtonWidget->setLayout(nav_layout);
last_palette_text_color = qApp->palette().color(QPalette::WindowText); 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); restoreGeometry(UISettings::values.configure_dialog_geometry);
} }
UpdateTheme(); UpdateTheme();
tab_button_group = std::make_unique<QButtonGroup>(this); tab_button_group = std::make_unique<QButtonGroup>(this);
@@ -238,8 +251,6 @@ void ConfigureDialog::UpdateTheme() {
if (!rainbow_timer) { if (!rainbow_timer) {
rainbow_timer = new QTimer(this); rainbow_timer = new QTimer(this);
connect(rainbow_timer, &QTimer::timeout, this, [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; if (m_is_tab_animating || !this->isVisible() || !this->isActiveWindow()) return;
const int current_index = ui->stackedWidget->currentIndex(); const int current_index = ui->stackedWidget->currentIndex();
@@ -261,12 +272,18 @@ void ConfigureDialog::UpdateTheme() {
if (ui->horizontalNavWidget) ui->horizontalNavWidget->setStyleSheet(sidebar_css); if (ui->horizontalNavWidget) ui->horizontalNavWidget->setStyleSheet(sidebar_css);
// 2. Action Buttons (OK/Apply/Cancel) // 2. Action Buttons (OK/Apply/Cancel)
if (ui->buttonBox && !ui->buttonBox->underMouse()) { if (ui->buttonBox) {
ui->buttonBox->setStyleSheet(QStringLiteral( const QString button_css = QStringLiteral(
"QPushButton { background-color: %1; color: #ffffff; border-radius: 4px; font-weight: bold; padding: 5px 15px; }" "QPushButton { background-color: %1; color: #ffffff; border-radius: 4px; font-weight: bold; padding: 5px 15px; }"
"QPushButton:hover { background-color: %2; }" "QPushButton:hover { background-color: %2; }"
"QPushButton:pressed { background-color: %3; }" "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 // 3. Tab Content Area
@@ -302,7 +319,17 @@ void ConfigureDialog::UpdateTheme() {
}); });
} }
rainbow_timer->start(33); 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(); rainbow_timer->stop();
if (ui->topButtonWidget) ui->topButtonWidget->setStyleSheet({}); if (ui->topButtonWidget) ui->topButtonWidget->setStyleSheet({});
if (ui->horizontalNavWidget) ui->horizontalNavWidget->setStyleSheet({}); if (ui->horizontalNavWidget) ui->horizontalNavWidget->setStyleSheet({});

View File

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

View File

@@ -2,17 +2,19 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include "citron/controller_overlay.h" #include "citron/controller_overlay.h"
#include "citron/uisettings.h"
#include "citron/configuration/configure_input_player_widget.h" #include "citron/configuration/configure_input_player_widget.h"
#include "citron/main.h" #include "citron/main.h"
#include "core/core.h" #include "core/core.h"
#include "hid_core/hid_core.h" #include "hid_core/hid_core.h"
#include <QApplication>
#include <QGridLayout> #include <QGridLayout>
#include <QMouseEvent> #include <QMouseEvent>
#include <QPainter> #include <QPainter>
#include <QPainterPath> #include <QPainterPath>
#include <QSizeGrip> #include <QSizeGrip>
#include <QWindow> // Required for Wayland dragging #include <QWindow>
#include <QResizeEvent> #include <QResizeEvent>
namespace { namespace {
@@ -26,27 +28,34 @@ Core::HID::EmulatedController* GetPlayer1Controller(Core::System* system) {
} }
return hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1); return hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1);
} }
} }
ControllerOverlay::ControllerOverlay(GMainWindow* parent) ControllerOverlay::ControllerOverlay(GMainWindow* parent)
: QWidget(parent, Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint), : QWidget(parent), main_window(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_TranslucentBackground);
setAttribute(Qt::WA_NoSystemBackground);
auto* layout = new QGridLayout(this); auto* layout = new QGridLayout(this);
setLayout(layout); 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); 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 = new PlayerControlPreview(this);
controller_widget->setAttribute(Qt::WA_TranslucentBackground); controller_widget->setAttribute(Qt::WA_TranslucentBackground);
// Disable the raw joystick (deadzone) visualization
controller_widget->SetRawJoystickVisible(false); controller_widget->SetRawJoystickVisible(false);
// Allow the widget to expand and shrink with the window
controller_widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); controller_widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
layout->addWidget(controller_widget, 0, 0); layout->addWidget(controller_widget, 0, 0);
@@ -54,18 +63,46 @@ ControllerOverlay::ControllerOverlay(GMainWindow* parent)
size_grip = new QSizeGrip(this); size_grip = new QSizeGrip(this);
layout->addWidget(size_grip, 0, 0, Qt::AlignBottom | Qt::AlignRight); 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); connect(&update_timer, &QTimer::timeout, this, &ControllerOverlay::UpdateControllerState);
update_timer.start(16); // ~60 FPS update_timer.start(16); // ~60 FPS
// Set a minimum size and a default starting size // Initial Resize
setMinimumSize(225, 175); if (UISettings::IsGamescope()) {
resize(450, 350); resize(225, 175);
} else {
resize(450, 350);
}
} }
ControllerOverlay::~ControllerOverlay() = default; ControllerOverlay::~ControllerOverlay() = default;
void ControllerOverlay::UpdateControllerState() { 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::System* system = main_window->GetSystem();
Core::HID::EmulatedController* controller = GetPlayer1Controller(system); Core::HID::EmulatedController* controller = GetPlayer1Controller(system);
if (controller_widget && controller) { 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) { void ControllerOverlay::paintEvent(QPaintEvent* event) {
Q_UNUSED(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) { void ControllerOverlay::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton && !size_grip->geometry().contains(event->pos())) { 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) #if defined(Q_OS_LINUX)
// Use system move on Wayland/Linux for proper dragging if (!UISettings::IsGamescope() && windowHandle()) {
if (windowHandle()) {
windowHandle()->startSystemMove(); windowHandle()->startSystemMove();
} else {
is_dragging = true;
drag_start_pos = event->globalPosition().toPoint() - this->pos();
} }
#else #else
// Original dragging implementation for other platforms (Windows, etc.)
is_dragging = true; is_dragging = true;
drag_start_pos = event->globalPosition().toPoint() - this->pos(); drag_start_pos = event->globalPosition().toPoint() - this->pos();
#endif #endif
@@ -99,15 +137,11 @@ void ControllerOverlay::mousePressEvent(QMouseEvent* event) {
} }
void ControllerOverlay::mouseMoveEvent(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) { if (is_dragging) {
move(event->globalPosition().toPoint() - drag_start_pos); move(event->globalPosition().toPoint() - drag_start_pos);
event->accept(); 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) { void ControllerOverlay::mouseReleaseEvent(QMouseEvent* event) {
@@ -119,6 +153,14 @@ void ControllerOverlay::mouseReleaseEvent(QMouseEvent* event) {
void ControllerOverlay::resizeEvent(QResizeEvent* event) { void ControllerOverlay::resizeEvent(QResizeEvent* event) {
QWidget::resizeEvent(event); QWidget::resizeEvent(event);
// This ensures the layout and its widgets (like the size grip) are correctly repositioned on resize.
layout()->update(); 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 Q_OBJECT
public: public:
void SetVisible(bool visible);
bool is_enabled = false;
explicit ControllerOverlay(GMainWindow* parent); explicit ControllerOverlay(GMainWindow* parent);
~ControllerOverlay() override; ~ControllerOverlay() override;
@@ -35,4 +37,4 @@ private:
bool is_dragging = false; bool is_dragging = false;
QPoint drag_start_pos; QPoint drag_start_pos;
}; };

View File

@@ -84,6 +84,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#include <QDir> #include <QDir>
#include <QFile> #include <QFile>
#include <QFileDialog> #include <QFileDialog>
#include <QGuiApplication>
#include <QInputDialog> #include <QInputDialog>
#include <QMessageBox> #include <QMessageBox>
#include <QProgressBar> #include <QProgressBar>
@@ -94,6 +95,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#include <QStandardPaths> #include <QStandardPaths>
#include <QStatusBar> #include <QStatusBar>
#include <QString> #include <QString>
#include <QStyleFactory>
#include <QSysInfo> #include <QSysInfo>
#include <QUrl> #include <QUrl>
#include <QtConcurrent/QtConcurrent> #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. // 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. // This prevents it from blocking the main application window.
auto* confirmation_dialog = new QMessageBox(nullptr); 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->setAttribute(Qt::WA_DeleteOnClose); // This ensures it is deleted automatically on close.
confirmation_dialog->setWindowModality(Qt::NonModal); // Explicitly set modality. confirmation_dialog->setWindowModality(Qt::NonModal); // Explicitly set modality.
confirmation_dialog->setWindowTitle(tr("First-Time Setup")); confirmation_dialog->setWindowTitle(tr("First-Time Setup"));
@@ -854,15 +862,13 @@ void GMainWindow::SoftwareKeyboardShowNormal() {
} }
const auto& layout = render_window->GetFramebufferLayout(); const auto& layout = render_window->GetFramebufferLayout();
const auto x = layout.screen.left; const auto x = layout.screen.left;
const auto y = layout.screen.top; const auto y = layout.screen.top;
const auto w = layout.screen.GetWidth(); const auto w = layout.screen.GetWidth();
const auto h = layout.screen.GetHeight(); const auto h = layout.screen.GetHeight();
const auto scale_ratio = devicePixelRatioF();
software_keyboard->ShowNormalKeyboard(render_window->mapToGlobal(QPoint(x, y) / scale_ratio), software_keyboard->ShowNormalKeyboard(render_window->mapToGlobal(QPoint(x, y)),
QSize(w, h) / scale_ratio); QSize(w, h));
} }
void GMainWindow::SoftwareKeyboardShowTextCheck( void GMainWindow::SoftwareKeyboardShowTextCheck(
@@ -895,11 +901,10 @@ void GMainWindow::SoftwareKeyboardShowInline(
(1.0f - appear_parameters.key_top_scale_y)))); (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 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 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), software_keyboard->ShowInlineKeyboard(std::move(appear_parameters),
render_window->mapToGlobal(QPoint(x, y) / scale_ratio), render_window->mapToGlobal(QPoint(x, y)),
QSize(w, h) / scale_ratio); QSize(w, h));
} }
void GMainWindow::SoftwareKeyboardHideInline() { void GMainWindow::SoftwareKeyboardHideInline() {
@@ -979,13 +984,11 @@ void GMainWindow::WebBrowserOpenWebPage(const std::string& main_url,
} }
const auto& layout = render_window->GetFramebufferLayout(); const auto& layout = render_window->GetFramebufferLayout();
const auto scale_ratio = devicePixelRatioF(); web_applet->resize(layout.screen.GetWidth(), layout.screen.GetHeight());
web_applet->resize(layout.screen.GetWidth() / scale_ratio, web_applet->move(layout.screen.left,
layout.screen.GetHeight() / scale_ratio); (layout.screen.top) + menuBar()->height());
web_applet->move(layout.screen.left / scale_ratio, web_applet->setZoomFactor(static_cast<qreal>(layout.screen.GetWidth()) /
(layout.screen.top / scale_ratio) + menuBar()->height()); static_cast<qreal>(Layout::ScreenUndocked::Width));
web_applet->setZoomFactor(static_cast<qreal>(layout.screen.GetWidth() / scale_ratio) /
static_cast<qreal>(Layout::ScreenUndocked::Width));
web_applet->setFocus(); web_applet->setFocus();
web_applet->show(); web_applet->show();
@@ -1159,9 +1162,6 @@ void GMainWindow::InitializeWidgets() {
multiplayer_room_overlay = new MultiplayerRoomOverlay(this); multiplayer_room_overlay = new MultiplayerRoomOverlay(this);
multiplayer_room_overlay->hide(); 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 = new VramOverlay(this);
vram_overlay->hide(); vram_overlay->hide();
@@ -1353,6 +1353,26 @@ void GMainWindow::InitializeWidgets() {
statusBar()->setVisible(true); statusBar()->setVisible(true);
setStyleSheet(QStringLiteral("QStatusBar::item{border: none;}")); 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() { void GMainWindow::InitializeDebugWidgets() {
@@ -1483,6 +1503,12 @@ void GMainWindow::InitializeHotkeys() {
} }
void GMainWindow::SetDefaultUIGeometry() { 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 // geometry: 53% of the window contents are in the upper screen half, 47% in the lower half
const QRect screenRect = QGuiApplication::primaryScreen()->geometry(); const QRect screenRect = QGuiApplication::primaryScreen()->geometry();
@@ -1495,15 +1521,25 @@ void GMainWindow::SetDefaultUIGeometry() {
} }
void GMainWindow::RestoreUIState() { void GMainWindow::RestoreUIState() {
const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope";
setWindowFlags(windowFlags() & ~Qt::FramelessWindowHint); 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 // Work-around because the games list isn't supposed to be full screen
if (isFullScreen()) { if (isFullScreen()) {
showNormal(); showNormal();
} }
restoreState(UISettings::values.state); 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 #if MICROPROFILE_ENABLED
microProfileDialog->restoreGeometry(UISettings::values.microprofile_geometry); microProfileDialog->restoreGeometry(UISettings::values.microprofile_geometry);
microProfileDialog->setVisible(UISettings::values.microprofile_visible.GetValue()); 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()); ui->action_Show_Status_Bar->setChecked(UISettings::values.show_status_bar.GetValue());
statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked()); 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); ui->action_Show_Performance_Overlay->setChecked(false);
if (performance_overlay) { if (performance_overlay) {
performance_overlay->SetVisible(false); performance_overlay->SetVisible(false);
} }
// Force the VRAM overlay to be off on startup
ui->action_Show_Vram_Overlay->setChecked(false); ui->action_Show_Vram_Overlay->setChecked(false);
if (vram_overlay) { if (vram_overlay) {
vram_overlay->SetVisible(false); 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); 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.setWindowModality(Qt::WindowModal);
progress.setMinimumDuration(100); progress.setMinimumDuration(100);
progress.setAutoClose(false); progress.setAutoClose(false);
@@ -4015,14 +4056,16 @@ void GMainWindow::ShowFullscreen() {
} }
void GMainWindow::HideFullscreen() { void GMainWindow::HideFullscreen() {
const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope";
if (ui->action_Single_Window_Mode->isChecked()) { if (ui->action_Single_Window_Mode->isChecked()) {
if (UsingExclusiveFullscreen()) { if (UsingExclusiveFullscreen()) {
showNormal(); showNormal();
restoreGeometry(UISettings::values.geometry); if (!is_gamescope) restoreGeometry(UISettings::values.geometry);
} else { } else {
hide(); hide();
setWindowFlags(windowFlags() & ~Qt::FramelessWindowHint); setWindowFlags(windowFlags() & ~Qt::FramelessWindowHint);
restoreGeometry(UISettings::values.geometry); if (!is_gamescope) restoreGeometry(UISettings::values.geometry);
raise(); raise();
show(); show();
} }
@@ -4032,15 +4075,18 @@ void GMainWindow::HideFullscreen() {
} else { } else {
if (UsingExclusiveFullscreen()) { if (UsingExclusiveFullscreen()) {
render_window->showNormal(); render_window->showNormal();
render_window->restoreGeometry(UISettings::values.renderwindow_geometry); if (!is_gamescope) render_window->restoreGeometry(UISettings::values.renderwindow_geometry);
} else { } else {
render_window->hide(); render_window->hide();
render_window->setWindowFlags(windowFlags() & ~Qt::FramelessWindowHint); 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->raise();
render_window->show(); render_window->show();
} }
} }
if (is_gamescope) {
}
} }
void GMainWindow::ToggleWindowMode() { void GMainWindow::ToggleWindowMode() {
@@ -4069,9 +4115,14 @@ void GMainWindow::ToggleWindowMode() {
} }
void GMainWindow::ResetWindowSize(u32 width, u32 height) { 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( const auto aspect_ratio = Layout::EmulationAspectRatio(
static_cast<Layout::AspectRatio>(Settings::values.aspect_ratio.GetValue()), 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()) { if (!ui->action_Single_Window_Mode->isChecked()) {
render_window->resize(height / aspect_ratio, height); render_window->resize(height / aspect_ratio, height);
} else { } else {
@@ -4441,6 +4492,15 @@ bool GMainWindow::question(QWidget* parent, const QString& title, const QString&
QMessageBox::StandardButtons buttons, QMessageBox::StandardButtons buttons,
QMessageBox::StandardButton defaultButton) { QMessageBox::StandardButton defaultButton) {
QMessageBox* box_dialog = new QMessageBox(parent); 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->setWindowTitle(title);
box_dialog->setText(text); box_dialog->setText(text);
box_dialog->setStandardButtons(buttons); box_dialog->setStandardButtons(buttons);
@@ -5026,7 +5086,10 @@ void GMainWindow::OnToggleControllerOverlay() {
controller_overlay = new ControllerOverlay(this); controller_overlay = new ControllerOverlay(this);
} }
if (controller_overlay) { 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) { if (performance_overlay) {
const bool is_checked = ui->action_Show_Performance_Overlay->isChecked(); const bool is_checked = ui->action_Show_Performance_Overlay->isChecked();
performance_overlay->SetVisible(is_checked); performance_overlay->SetVisible(is_checked);
UISettings::values.show_performance_overlay = is_checked; UISettings::values.show_performance_overlay = is_checked;
} }
} }
@@ -5066,7 +5128,6 @@ void GMainWindow::OnToggleVramOverlay() {
if (vram_overlay) { if (vram_overlay) {
const bool is_checked = ui->action_Show_Vram_Overlay->isChecked(); const bool is_checked = ui->action_Show_Vram_Overlay->isChecked();
vram_overlay->SetVisible(is_checked); vram_overlay->SetVisible(is_checked);
UISettings::values.show_vram_overlay = is_checked; UISettings::values.show_vram_overlay = is_checked;
} }
} }
@@ -5564,10 +5625,14 @@ void GMainWindow::UpdateStatusButtons() {
} }
void GMainWindow::UpdateUISettings() { 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.geometry = saveGeometry();
UISettings::values.renderwindow_geometry = render_window->saveGeometry(); UISettings::values.renderwindow_geometry = render_window->saveGeometry();
} }
UISettings::values.state = saveState(); UISettings::values.state = saveState();
#if MICROPROFILE_ENABLED #if MICROPROFILE_ENABLED
UISettings::values.microprofile_geometry = microProfileDialog->saveGeometry(); UISettings::values.microprofile_geometry = microProfileDialog->saveGeometry();
@@ -6029,80 +6094,69 @@ void VolumeButton::ResetMultiplier() {
#endif #endif
static void SetHighDPIAttributes() { static void SetHighDPIAttributes() {
[[maybe_unused]] const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() ||
qgetenv("XDG_CURRENT_DESKTOP") == "gamescope" ||
!qgetenv("STEAM_DECK").isEmpty();
#ifdef _WIN32 #ifdef _WIN32
// For Windows, we want to avoid scaling artifacts on fractional scaling ratios. // Windows logic: Set policy globally.
// This is done by setting the optimal scaling policy for the primary screen. // 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(); SetProcessDPIAware();
// For Windows 10+, use Per Monitor v2 DPI Awareness
// This provides better scaling for multi-monitor setups
HMODULE shcore = LoadLibrary(L"shcore.dll"); HMODULE shcore = LoadLibrary(L"shcore.dll");
if (shcore) { if (shcore) {
typedef HRESULT(WINAPI* SetProcessDpiAwarenessFunc)(int); typedef HRESULT(WINAPI* SetProcessDpiAwarenessFunc)(int);
SetProcessDpiAwarenessFunc setProcessDpiAwareness = SetProcessDpiAwarenessFunc setProcessDpiAwareness =
(SetProcessDpiAwarenessFunc)GetProcAddress(shcore, "SetProcessDpiAwareness"); (SetProcessDpiAwarenessFunc)GetProcAddress(shcore, "SetProcessDpiAwareness");
if (setProcessDpiAwareness) { if (setProcessDpiAwareness) {
// PROCESS_PER_MONITOR_DPI_AWARE_V2 = 2 setProcessDpiAwareness(2); // PROCESS_PER_MONITOR_DPI_AWARE_V2
setProcessDpiAwareness(2);
} }
FreeLibrary(shcore); FreeLibrary(shcore);
} }
#else
if (is_gamescope) {
// PassThrough prevents Qt6 from recursively expanding layouts to fit rounded DPIs
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(
Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
}
#endif #endif
} }
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
// Set environment variables for AppImage compatibility // 1. Detect Gamescope/Steam Deck hardware
// This must be done before the QApplication is created. 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(); const bool is_appimage = !qgetenv("APPIMAGE").isEmpty();
if (is_appimage) { if (is_appimage) {
// Fixes Wayland crash with NVIDIA drivers by disabling explicit sync.
qputenv("QT_WAYLAND_DISABLE_EXPLICIT_SYNC", "1"); qputenv("QT_WAYLAND_DISABLE_EXPLICIT_SYNC", "1");
// Tell the bundled OpenSSL where to find the bundled certificates.
const QDir app_dir(QCoreApplication::applicationDirPath()); const QDir app_dir(QCoreApplication::applicationDirPath());
const QString certs_path = app_dir.filePath(QString::fromLatin1("../etc/ssl/certs")); const QString certs_path = app_dir.filePath(QString::fromLatin1("../etc/ssl/certs"));
qputenv("SSL_CERT_DIR", certs_path.toUtf8()); qputenv("SSL_CERT_DIR", certs_path.toUtf8());
@@ -6133,73 +6187,73 @@ int main(int argc, char* argv[]) {
Common::ConfigureNvidiaEnvironmentFlags(); Common::ConfigureNvidiaEnvironmentFlags();
// Init settings params
QCoreApplication::setOrganizationName(QStringLiteral("citron team")); QCoreApplication::setOrganizationName(QStringLiteral("citron team"));
QCoreApplication::setApplicationName(QStringLiteral("citron")); QCoreApplication::setApplicationName(QStringLiteral("citron"));
#ifdef _WIN32 #ifdef _WIN32
// Increases the maximum open file limit to 8192
_setmaxstdio(8192); _setmaxstdio(8192);
#endif #endif
#ifdef __APPLE__ #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() / ".."; const auto bin_path = Common::FS::GetBundleDirectory() / "..";
chdir(Common::FS::PathToUTF8String(bin_path).c_str()); chdir(Common::FS::PathToUTF8String(bin_path).c_str());
#endif #endif
#ifdef __linux__ #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()) { if (QString::fromLocal8Bit(qgetenv("DISPLAY")).isEmpty()) {
qputenv("DISPLAY", ":0"); 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")); QGuiApplication::setDesktopFileName(QStringLiteral("org.citron_emu.citron"));
#endif #endif
// Call policy attributes BEFORE creating the real QApplication instance
SetHighDPIAttributes(); SetHighDPIAttributes();
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) #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); QCoreApplication::setAttribute(Qt::AA_DisableWindowContextHelpButton);
#endif #endif
// Enables the core to make the qt created contexts current on std::threads
QCoreApplication::setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity); QCoreApplication::setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity);
QApplication app(argc, argv); 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__ #ifdef __linux__
if (QGuiApplication::platformName().startsWith(QStringLiteral("wayland"))) { if (QGuiApplication::platformName().startsWith(QStringLiteral("wayland"))) {
Settings::values.is_wayland_platform.SetValue(true); Settings::values.is_wayland_platform.SetValue(true);
} }
#endif #endif
#ifdef CITRON_USE_AUTO_UPDATER #ifdef CITRON_USE_AUTO_UPDATER
// Check for and apply staged updates before starting the main application
std::filesystem::path app_dir = std::filesystem::path(QCoreApplication::applicationDirPath().toStdString()); std::filesystem::path app_dir = std::filesystem::path(QCoreApplication::applicationDirPath().toStdString());
#ifdef _WIN32 #ifdef _WIN32
// On Windows, updates are applied by the helper script after the app exits. // 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"; std::filesystem::path staging_path = app_dir / "update_staging";
if (std::filesystem::exists(staging_path)) { if (std::filesystem::exists(staging_path)) {
try { try {
std::filesystem::remove_all(staging_path); std::filesystem::remove_all(staging_path);
} catch (...) { } catch (...) {}
// Ignore cleanup errors
}
} }
#else #else
// On Linux, apply staged updates at startup as before
if (Updater::UpdaterService::HasStagedUpdate(app_dir)) { if (Updater::UpdaterService::HasStagedUpdate(app_dir)) {
if (Updater::UpdaterService::ApplyStagedUpdate(app_dir)) { if (Updater::UpdaterService::ApplyStagedUpdate(app_dir)) {
// Show a simple message that update was applied
QMessageBox::information(nullptr, QObject::tr("Update Applied"), QMessageBox::information(nullptr, QObject::tr("Update Applied"),
QObject::tr("Citron has been updated successfully!")); QObject::tr("Citron has been updated successfully!"));
} }
@@ -6207,37 +6261,29 @@ int main(int argc, char* argv[]) {
#endif #endif
#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"); setlocale(LC_ALL, "C");
GMainWindow main_window{std::move(config), has_broken_vulkan}; GMainWindow main_window{std::move(config), has_broken_vulkan};
app.setStyle(new RainbowStyle(app.style())); app.setStyle(new RainbowStyle(app.style()));
main_window.show(); 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, QObject::connect(&app, &QGuiApplication::applicationStateChanged, &main_window,
&GMainWindow::OnAppFocusStateChanged); &GMainWindow::OnAppFocusStateChanged);
int result = app.exec(); return app.exec();
detached_tasks.WaitForAllTasks();
return result;
} }
void GMainWindow::OnCheckForUpdates() { void GMainWindow::OnCheckForUpdates() {

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project // SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <QComboBox> #include <QComboBox>
@@ -24,10 +25,22 @@
enum class ConnectionType : u8 { TraversalServer, IP }; enum class ConnectionType : u8 { TraversalServer, IP };
DirectConnectWindow::DirectConnectWindow(Core::System& system_, QWidget* parent) 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{ ui(std::make_unique<Ui::DirectConnect>()), system{system_}, room_network{
system.GetRoomNetwork()} { 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); ui->setupUi(this);
// setup the watcher for background connections // setup the watcher for background connections

View File

@@ -32,10 +32,23 @@
HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list, HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list,
std::shared_ptr<Core::AnnounceMultiplayerSession> session, std::shared_ptr<Core::AnnounceMultiplayerSession> session,
Core::System& system_) Core::System& system_)
: QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), : QDialog(parent),
ui(std::make_unique<Ui::HostRoom>()), ui(std::make_unique<Ui::HostRoom>()),
announce_multiplayer_session(session), system{system_}, room_network{ announce_multiplayer_session(session), system{system_}, room_network{
system.GetRoomNetwork()} { 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); ui->setupUi(this);
// set up validation for all of the fields // set up validation for all of the fields

View File

@@ -27,10 +27,23 @@
Lobby::Lobby(QWidget* parent, QStandardItemModel* list, Lobby::Lobby(QWidget* parent, QStandardItemModel* list,
std::shared_ptr<Core::AnnounceMultiplayerSession> session, Core::System& system_) 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>()), ui(std::make_unique<Ui::Lobby>()),
announce_multiplayer_session(session), system{system_}, room_network{ announce_multiplayer_session(session), system{system_}, room_network{
system.GetRoomNetwork()} { 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); ui->setupUi(this);
// setup the watcher for background connections // setup the watcher for background connections

View File

@@ -136,4 +136,15 @@ namespace UISettings {
config.value(QStringLiteral("microProfileDialogGeometry")).toByteArray(); 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 } // namespace UISettings

View File

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

View File

@@ -220,9 +220,18 @@ void UpdaterDialog::OnRestartButtonClicked() {
} }
void UpdaterDialog::SetupUI() { 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->currentVersionValue->setText(QString::fromStdString(updater_service->GetCurrentVersion()));
ui->appImageSelectorLabel->setVisible(false); ui->appImageSelectorLabel->setVisible(false);

View File

@@ -121,13 +121,7 @@
</property> </property>
<layout class="QVBoxLayout" name="changelogLayout"> <layout class="QVBoxLayout" name="changelogLayout">
<item> <item>
<widget class="QTextBrowser" name="changelogText"> <widget class="QTextBrowser" name="changelogText">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>150</height>
</size>
</property>
<property name="html"> <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; <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; &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> </property>
</spacer> </spacer>
</item> </item>
<!-- Start of added widgets -->
<item> <item>
<layout class="QHBoxLayout" name="appImageSelectorLayout"> <layout class="QHBoxLayout" name="appImageSelectorLayout">
<item> <item>
@@ -207,7 +200,6 @@ p, li { white-space: pre-wrap; }
</item> </item>
</layout> </layout>
</item> </item>
<!-- End of added widgets -->
<item> <item>
<layout class="QHBoxLayout" name="buttonLayout"> <layout class="QHBoxLayout" name="buttonLayout">
<item> <item>

View File

@@ -18,12 +18,18 @@
#include "network/room.h" #include "network/room.h"
#include "citron/uisettings.h" #include "citron/uisettings.h"
MultiplayerRoomOverlay::MultiplayerRoomOverlay(GMainWindow* parent) MultiplayerRoomOverlay::MultiplayerRoomOverlay(QWidget* parent)
: QWidget(parent), main_window(parent) { : QWidget(parent) {
setAttribute(Qt::WA_TranslucentBackground, true); main_window = qobject_cast<GMainWindow*>(parent->window());
setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint);
setFocusPolicy(Qt::ClickFocus); // 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 = new QGridLayout(this);
main_layout->setContentsMargins(padding, padding, 0, 0); main_layout->setContentsMargins(padding, padding, 0, 0);
@@ -58,11 +64,25 @@ MultiplayerRoomOverlay::MultiplayerRoomOverlay(GMainWindow* parent)
update_timer.setSingleShot(false); update_timer.setSingleShot(false);
connect(&update_timer, &QTimer::timeout, this, &MultiplayerRoomOverlay::UpdateRoomData); 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(); UpdateTheme();
setMinimumSize(280, 220); const bool is_gamescope = UISettings::IsGamescope();
resize(320, 280); 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(); 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); } 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) { void MultiplayerRoomOverlay::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton) { if (event->button() == Qt::LeftButton && !size_grip->geometry().contains(event->pos())) {
if (size_grip->geometry().contains(event->pos())) { const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope";
// Let the size grip handle the event if (is_gamescope) {
} 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) {
is_dragging = true; is_dragging = true;
drag_start_pos = event->globalPosition().toPoint(); drag_start_pos = event->globalPosition().toPoint() - this->pos();
widget_start_pos = this->pos();
setCursor(Qt::ClosedHandCursor); 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); QWidget::mousePressEvent(event);
} }
void MultiplayerRoomOverlay::mouseMoveEvent(QMouseEvent* event) { void MultiplayerRoomOverlay::mouseMoveEvent(QMouseEvent* event) {
if (is_dragging) { if (is_dragging && main_window) {
QPoint delta = event->globalPosition().toPoint() - drag_start_pos; QPoint new_pos = event->globalPosition().toPoint() - drag_start_pos;
move(widget_start_pos + delta); QPoint win_origin = main_window->mapToGlobal(QPoint(0, 0));
has_been_moved = true; 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); QWidget::mouseMoveEvent(event);
} }
#endif
void MultiplayerRoomOverlay::mouseReleaseEvent(QMouseEvent* event) { void MultiplayerRoomOverlay::mouseReleaseEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton && is_dragging) { if (event->button() == Qt::LeftButton && is_dragging) {

View File

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

View File

@@ -5,6 +5,8 @@
#include <QPainter> #include <QPainter>
#include <QPainterPath> #include <QPainterPath>
#include <QScreen> #include <QScreen>
#include <QSizeGrip>
#include <QGridLayout>
#include <QTimer> #include <QTimer>
#include <QMouseEvent> #include <QMouseEvent>
#include <QtMath> #include <QtMath>
@@ -22,7 +24,7 @@
#include <Windows.h> #include <Windows.h>
#include <comdef.h> #include <comdef.h>
#include <WbemIdl.h> #include <WbemIdl.h>
#pragma comment(lib, "wbemuuid.lib") // For MSVC, helps the linker find the library #pragma comment(lib, "wbemuuid.lib")
#endif #endif
#ifdef Q_OS_ANDROID #ifdef Q_OS_ANDROID
@@ -37,85 +39,91 @@
#include "video_core/gpu.h" #include "video_core/gpu.h"
#include "video_core/renderer_base.h" #include "video_core/renderer_base.h"
PerformanceOverlay::PerformanceOverlay(GMainWindow* parent) PerformanceOverlay::PerformanceOverlay(QWidget* parent) : QWidget(UISettings::IsGamescope() ? nullptr : parent) {
: QWidget(parent), main_window(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); 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 if (UISettings::IsGamescope()) {
title_font = QFont(QString::fromUtf8("Segoe UI"), 9, QFont::Medium); title_font = QFont(QString::fromUtf8("Segoe UI"), 9, QFont::Bold);
value_font = QFont(QString::fromUtf8("Segoe UI"), 11, QFont::Bold); value_font = QFont(QString::fromUtf8("Segoe UI"), 10, QFont::Bold);
small_font = QFont(QString::fromUtf8("Segoe UI"), 8, QFont::Normal); 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_background_color = QColor(40, 40, 40, 100);
graph_line_color = QColor(76, 175, 80, 200); graph_line_color = QColor(76, 175, 80, 200);
graph_fill_color = QColor(76, 175, 80, 60); graph_fill_color = QColor(76, 175, 80, 60);
// Set up timer for updates
update_timer.setSingleShot(false); update_timer.setSingleShot(false);
connect(&update_timer, &QTimer::timeout, this, &PerformanceOverlay::UpdatePerformanceStats); connect(&update_timer, &QTimer::timeout, this, &PerformanceOverlay::UpdatePerformanceStats);
// Connect to the main window's theme change signal if (main_window) {
connect(parent, &GMainWindow::themeChanged, this, &PerformanceOverlay::UpdateTheme); connect(main_window, &GMainWindow::themeChanged, this, &PerformanceOverlay::UpdateTheme);
// Set the initial theme colors }
UpdateTheme(); UpdateTheme();
// Set initial size - larger to accommodate the graph
resize(220, 180);
// Position in top-left corner
UpdatePosition(); UpdatePosition();
} }
PerformanceOverlay::~PerformanceOverlay() = default; PerformanceOverlay::~PerformanceOverlay() = default;
void PerformanceOverlay::SetVisible(bool visible) { void PerformanceOverlay::SetVisible(bool visible) {
if (is_visible == visible) { is_enabled = visible;
return; is_visible = visible; // Update the state so the check works next time
}
is_visible = visible;
if (visible) { if (visible) {
show(); show();
update_timer.start(500); // Update every 500ms for more accurate data update_timer.start(500);
} else { } else {
update_timer.stop(); // Stop the timer first
hide(); hide();
update_timer.stop();
} }
} }
void PerformanceOverlay::paintEvent(QPaintEvent* event) { void PerformanceOverlay::paintEvent(QPaintEvent* event) {
Q_UNUSED(event) Q_UNUSED(event)
QPainter painter(this); QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true); painter.setRenderHint(QPainter::Antialiasing, true);
painter.setRenderHint(QPainter::TextAntialiasing, true); painter.setRenderHint(QPainter::TextAntialiasing, true);
// Draw background with rounded corners and subtle shadow effect
QPainterPath background_path; QPainterPath background_path;
background_path.addRoundedRect(rect(), corner_radius, corner_radius); background_path.addRoundedRect(rect(), corner_radius, corner_radius);
// Draw subtle shadow if (!UISettings::IsGamescope()) {
QPainterPath shadow_path = background_path.translated(1, 1); QPainterPath shadow_path = background_path.translated(1, 1);
painter.fillPath(shadow_path, QColor(0, 0, 0, 40)); painter.fillPath(shadow_path, QColor(0, 0, 0, 40));
}
// Draw main background
painter.fillPath(background_path, background_color); painter.fillPath(background_path, background_color);
// Draw subtle border
painter.setPen(QPen(border_color, border_width)); painter.setPen(QPen(border_color, border_width));
painter.drawPath(background_path); painter.drawPath(background_path);
// Draw performance information
DrawPerformanceInfo(painter); DrawPerformanceInfo(painter);
// Draw frame graph
DrawFrameGraph(painter); DrawFrameGraph(painter);
} }
@@ -124,128 +132,110 @@ void PerformanceOverlay::resizeEvent(QResizeEvent* event) {
UpdatePosition(); UpdatePosition();
} }
void PerformanceOverlay::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton && !size_grip->geometry().contains(event->pos())) {
#if defined(Q_OS_LINUX) #if defined(Q_OS_LINUX)
// LINUX-SPECIFIC IMPLEMENTATION (Wayland Fix) if (!UISettings::IsGamescope() && windowHandle()) {
void PerformanceOverlay::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton) {
// Hand off window moving responsibility to the OS compositor.
if (windowHandle()) {
windowHandle()->startSystemMove(); 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 #else
// ORIGINAL IMPLEMENTATION
void PerformanceOverlay::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton) {
is_dragging = true; is_dragging = true;
drag_start_pos = event->globalPosition().toPoint(); drag_start_pos = event->globalPosition().toPoint() - this->pos();
widget_start_pos = this->pos(); #endif
setCursor(Qt::ClosedHandCursor); event->accept();
} }
QWidget::mousePressEvent(event);
} }
void PerformanceOverlay::mouseMoveEvent(QMouseEvent* event) { void PerformanceOverlay::mouseMoveEvent(QMouseEvent* event) {
if (is_dragging) { if (is_dragging) {
QPoint delta = event->globalPosition().toPoint() - drag_start_pos; move(event->globalPosition().toPoint() - drag_start_pos);
move(widget_start_pos + delta); event->accept();
} }
QWidget::mouseMoveEvent(event);
} }
#endif
void PerformanceOverlay::mouseReleaseEvent(QMouseEvent* event) { void PerformanceOverlay::mouseReleaseEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton) { if (event->button() == Qt::LeftButton) {
is_dragging = false; is_dragging = false;
has_been_moved = true; has_been_moved = true;
setCursor(Qt::ArrowCursor); setCursor(Qt::ArrowCursor);
event->accept();
} }
QWidget::mouseReleaseEvent(event); QWidget::mouseReleaseEvent(event);
} }
void PerformanceOverlay::UpdatePerformanceStats() { void PerformanceOverlay::UpdatePerformanceStats() {
if (!main_window) { if (!main_window || !is_enabled) return;
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(); 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; static int update_counter = 0;
update_counter++; update_counter++;
// Try to get performance data every 2nd update (every 1 second)
if (update_counter % 2 == 0) { if (update_counter % 2 == 0) {
try { try {
current_fps = main_window->GetCurrentFPS(); current_fps = main_window->GetCurrentFPS();
current_frame_time = main_window->GetCurrentFrameTime(); current_frame_time = main_window->GetCurrentFrameTime();
emulation_speed = main_window->GetEmulationSpeed(); 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_fps) || current_fps < 0.0 || current_fps > 1000.0) { if (std::isnan(current_frame_time) || current_frame_time < 0.0 || current_frame_time > 100.0) current_frame_time = 16.67;
current_fps = 60.0; if (std::isnan(emulation_speed) || emulation_speed < 0.0 || emulation_speed > 1000.0) emulation_speed = 100.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 = 1000.0 / current_fps;
if (current_fps > 0.0 && current_frame_time > 0.0) { } catch (...) {}
// 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
}
} }
// Update hardware temperatures every 4th update (every 2 seconds)
if (update_counter % 4 == 0) { if (update_counter % 4 == 0) {
UpdateHardwareTemperatures(); 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_fps) || current_fps <= 0.0) { if (std::isnan(current_frame_time) || current_frame_time <= 0.0) current_frame_time = 16.67;
current_fps = 60.0; if (std::isnan(emulation_speed) || emulation_speed <= 0.0) emulation_speed = 100.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;
}
// 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); fps_color = GetFpsColor(current_fps);
temperature_color = GetTemperatureColor(std::max({cpu_temperature, gpu_temperature, battery_temperature})); temperature_color = GetTemperatureColor(std::max({cpu_temperature, gpu_temperature, battery_temperature}));
// Trigger a repaint
update(); update();
} }
void PerformanceOverlay::UpdateHardwareTemperatures() { void PerformanceOverlay::UpdateHardwareTemperatures() {
// Reset data
cpu_temperature = 0.0f; cpu_temperature = 0.0f;
gpu_temperature = 0.0f; gpu_temperature = 0.0f;
cpu_sensor_type.clear(); cpu_sensor_type.clear();
@@ -254,41 +244,79 @@ void PerformanceOverlay::UpdateHardwareTemperatures() {
battery_temperature = 0.0f; battery_temperature = 0.0f;
#if defined(Q_OS_LINUX) #if defined(Q_OS_LINUX)
// --- Standard Linux Thermal Zone Reading --- // 1. Read Battery Data (Steam Deck / Laptops)
QDir thermal_dir(QString::fromUtf8("/sys/class/thermal/")); QDir bat_dir(QStringLiteral("/sys/class/power_supply/"));
QStringList filters{QString::fromUtf8("thermal_zone*")}; QStringList bats = bat_dir.entryList({QStringLiteral("BAT*")}, QDir::Dirs);
QStringList thermal_zones = thermal_dir.entryList(filters, 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 btemp_file(bat_dir.filePath(node + QStringLiteral("/temp")));
QFile type_file(thermal_dir.filePath(zone_name + QString::fromUtf8("/type"))); if (btemp_file.open(QIODevice::ReadOnly)) {
if (!type_file.open(QIODevice::ReadOnly | QIODevice::Text)) continue; float raw_temp = btemp_file.readAll().trimmed().toFloat();
QString type = QString::fromUtf8(type_file.readAll()).trimmed(); // Detect millidegrees (35000) or tenths (350)
type_file.close(); battery_temperature = (raw_temp > 1000) ? raw_temp / 1000.0f : raw_temp / 10.0f;
btemp_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");
} }
} else if (type.contains(QString::fromUtf8("radeon")) || type.contains(QString::fromUtf8("amdgpu")) || type.contains(QString::fromUtf8("nvidia")) || type.contains(QString::fromUtf8("nouveau"))) { break;
if (temp > gpu_temperature) { }
gpu_temperature = temp; }
gpu_sensor_type = QString::fromUtf8("GPU");
// 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 #endif
#if defined(Q_OS_ANDROID) #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( QJniObject battery_status = QJniObject::callStaticObjectMethod(
"android/content/CONTEXT", "registerReceiver", "android/content/Context", "registerReceiver",
"(Landroid/content/BroadcastReceiver;Landroid/content/IntentFilter;)Landroid/content/Intent;", "(Landroid/content/BroadcastReceiver;Landroid/content/IntentFilter;)Landroid/content/Intent;",
nullptr, new QJniObject("android.content.IntentFilter", "(Ljava/lang/String;)V", "android.intent.action.BATTERY_CHANGED")); 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", int temp_tenths = battery_status.callMethod<jint>("getIntExtra", "(Ljava/lang/String;I)I",
QJniObject::fromString("temperature").object<jstring>(), -1); QJniObject::fromString("temperature").object<jstring>(), -1);
if (scale > 0) { if (scale > 0) battery_percentage = (level * 100) / scale;
battery_percentage = (level * 100) / scale; if (temp_tenths > 0) battery_temperature = static_cast<float>(temp_tenths) / 10.0f;
}
if (temp_tenths > 0) {
battery_temperature = static_cast<float>(temp_tenths) / 10.0f;
}
} }
#endif #endif
@@ -313,9 +337,7 @@ void PerformanceOverlay::UpdateHardwareTemperatures() {
HRESULT hres; HRESULT hres;
IWbemLocator* pLoc = nullptr; IWbemLocator* pLoc = nullptr;
IWbemServices* pSvc = nullptr; IWbemServices* pSvc = nullptr;
hres = CoCreateInstance(CLSID_WbemLocator, 0, CLSCTX_INPROC_SERVER, IID_IWbemLocator, (LPVOID*)&pLoc); hres = CoCreateInstance(CLSID_WbemLocator, 0, CLSCTX_INPROC_SERVER, IID_IWbemLocator, (LPVOID*)&pLoc);
if (SUCCEEDED(hres)) { if (SUCCEEDED(hres)) {
hres = pLoc->ConnectServer(_bstr_t(L"ROOT\\WMI"), NULL, NULL, 0, NULL, 0, 0, &pSvc); hres = pLoc->ConnectServer(_bstr_t(L"ROOT\\WMI"), NULL, NULL, 0, NULL, 0, 0, &pSvc);
if (SUCCEEDED(hres)) { if (SUCCEEDED(hres)) {
@@ -331,12 +353,11 @@ void PerformanceOverlay::UpdateHardwareTemperatures() {
while (pEnumerator) { while (pEnumerator) {
pEnumerator->Next(WBEM_INFINITE, 1, &pclsObj, &uReturn); pEnumerator->Next(WBEM_INFINITE, 1, &pclsObj, &uReturn);
if (uReturn == 0) break; if (uReturn == 0) break;
VARIANT vtProp; VARIANT vtProp;
pclsObj->Get(L"CurrentTemperature", 0, &vtProp, 0, 0); pclsObj->Get(L"CurrentTemperature", 0, &vtProp, 0, 0);
float temp_kelvin = vtProp.uintVal / 10.0f; float temp_kelvin = vtProp.uintVal / 10.0f;
cpu_temperature = temp_kelvin - 273.15f; cpu_temperature = temp_kelvin - 273.15f;
cpu_sensor_type = QString::fromUtf8("CPU"); cpu_sensor_type = QStringLiteral("CPU");
VariantClear(&vtProp); VariantClear(&vtProp);
pclsObj->Release(); pclsObj->Release();
} }
@@ -351,12 +372,7 @@ void PerformanceOverlay::UpdateHardwareTemperatures() {
} }
void PerformanceOverlay::UpdatePosition() { void PerformanceOverlay::UpdatePosition() {
if (!main_window) { if (main_window && !has_been_moved) {
return;
}
// Only position in top-left corner if we haven't been moved by the user
if (!has_been_moved) {
QPoint main_window_pos = main_window->mapToGlobal(QPoint(0, 0)); QPoint main_window_pos = main_window->mapToGlobal(QPoint(0, 0));
move(main_window_pos.x() + 10, main_window_pos.y() + 10); move(main_window_pos.x() + 10, main_window_pos.y() + 10);
} }
@@ -365,92 +381,85 @@ void PerformanceOverlay::UpdatePosition() {
void PerformanceOverlay::DrawPerformanceInfo(QPainter& painter) { void PerformanceOverlay::DrawPerformanceInfo(QPainter& painter) {
painter.setRenderHint(QPainter::TextAntialiasing, true); painter.setRenderHint(QPainter::TextAntialiasing, true);
int y_offset = padding; // Dynamic spacing based on font size to prevent squishing
const int line_height = 20; 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.setFont(title_font);
painter.setPen(text_color); 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; // 2. Draw Hardware Stats (Right Column)
const int line_height_right = 18;
// Draw Temperatures
painter.setFont(small_font); painter.setFont(small_font);
const int hw_step = UISettings::IsGamescope() ? 16 : 20;
float core_temp_to_display = std::max(cpu_temperature, gpu_temperature); if (cpu_temperature > 0.0f) {
if (core_temp_to_display > 0.0f) { QString cpu_text = QStringLiteral("CPU:%1°C").arg(cpu_temperature, 0, 'f', 0);
QString core_label = gpu_temperature > cpu_temperature ? gpu_sensor_type : cpu_sensor_type; painter.setPen(GetTemperatureColor(cpu_temperature));
QString core_temp_text = QString::fromUtf8("%1: %2°C").arg(core_label).arg(core_temp_to_display, 0, 'f', 0); int tw = painter.fontMetrics().horizontalAdvance(cpu_text);
painter.setPen(GetTemperatureColor(core_temp_to_display)); painter.drawText(width() - padding - tw, y_right, cpu_text);
int text_width = painter.fontMetrics().horizontalAdvance(core_temp_text); y_right += hw_step;
painter.drawText(width() - padding - text_width, y_offset_right + 12, core_temp_text); }
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) { 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) { 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); painter.setPen(text_color);
int text_width = painter.fontMetrics().horizontalAdvance(batt_text); int tw = painter.fontMetrics().horizontalAdvance(batt_text);
painter.drawText(width() - padding - text_width, y_offset_right + 12, batt_text); painter.drawText(width() - padding - tw, y_right, batt_text);
} }
y_offset += line_height + 4; // 3. Draw FPS (Left Column)
y_left += title_step;
// Draw FPS
painter.setFont(value_font); painter.setFont(value_font);
painter.setPen(fps_color); painter.setPen(fps_color);
QString fps_text = QString::fromUtf8("%1 FPS").arg(FormatFps(current_fps)); painter.drawText(padding, y_left, QStringLiteral("%1 FPS").arg(FormatFps(current_fps)));
painter.drawText(padding, y_offset, fps_text);
y_offset += line_height;
// Draw frame time // 4. Draw Small Stats (Left Column)
y_left += title_step;
painter.setFont(small_font); painter.setFont(small_font);
painter.setPen(text_color); painter.setPen(text_color);
QString frame_time_text = QString::fromUtf8("Frame: %1 ms").arg(FormatFrameTime(current_frame_time)); painter.drawText(padding, y_left, QStringLiteral("Frame:%1 ms").arg(FormatFrameTime(current_frame_time)));
painter.drawText(padding, y_offset, frame_time_text);
y_offset += line_height - 2;
// Draw emulation speed y_left += stat_step;
QString speed_text = QString::fromUtf8("Speed: %1%").arg(emulation_speed, 0, 'f', 0); painter.drawText(padding, y_left, QStringLiteral("Speed:%1%").arg(emulation_speed, 0, 'f', 0));
painter.drawText(padding, y_offset, speed_text);
y_offset += line_height - 2;
// Draw shader building info with accent color
if (shaders_building > 0) { if (shaders_building > 0) {
painter.setPen(QColor(255, 152, 0, 255)); // Material Design orange y_left += stat_step;
QString shader_text = QString::fromUtf8("Building: %1 shader(s)").arg(shaders_building); painter.setPen(QColor(255, 152, 0));
painter.drawText(padding, y_offset, shader_text); painter.drawText(padding, y_left, QStringLiteral("Building:%1").arg(shaders_building));
} }
} }
void PerformanceOverlay::DrawFrameGraph(QPainter& painter) { void PerformanceOverlay::DrawFrameGraph(QPainter& painter) {
if (frame_times.empty()) { if (frame_times.empty()) return;
return;
}
const int graph_y = height() - graph_height - padding; const int graph_y = height() - graph_height - padding;
const int graph_width = width() - (padding * 2); const int graph_width = width() - (padding * 2);
const QRect graph_rect(padding, graph_y, graph_width, graph_height); const QRect graph_rect(padding, graph_y, graph_width, graph_height);
// Draw graph background
painter.fillRect(graph_rect, graph_background_color); 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 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; const double range = max_val - min_val;
if (range <= 0.0) return;
if (range <= 0.0) { // Grid lines
return;
}
// Draw grid lines
painter.setPen(QPen(QColor(80, 80, 80, 100), 1)); painter.setPen(QPen(QColor(80, 80, 80, 100), 1));
const int grid_lines = 4; const int grid_lines = 4;
for (int i = 1; i < grid_lines; ++i) { 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); 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); 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.setPen(QPen(QColor(255, 255, 255, 80), 1, Qt::DashLine));
painter.drawLine(graph_rect.left(), fps60_y, graph_rect.right(), fps60_y); painter.drawLine(graph_rect.left(), fps60_y, graph_rect.right(), fps60_y);
// Draw frame time line
painter.setPen(QPen(graph_line_color, 2)); painter.setPen(QPen(graph_line_color, 2));
painter.setBrush(graph_fill_color); painter.setBrush(graph_fill_color);
@@ -476,45 +484,39 @@ void PerformanceOverlay::DrawFrameGraph(QPainter& painter) {
const double normalized_y = (frame_time - min_val) / range; const double normalized_y = (frame_time - min_val) / range;
const int x = graph_rect.left() + static_cast<int>(i * x_step); 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); 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.right(), graph_rect.bottom());
graph_path.lineTo(graph_rect.left(), graph_rect.bottom()); graph_path.lineTo(graph_rect.left(), graph_rect.bottom());
graph_path.closeSubpath(); graph_path.closeSubpath();
painter.drawPath(graph_path); painter.drawPath(graph_path);
// Draw statistics text
painter.setFont(small_font); painter.setFont(small_font);
painter.setPen(text_color); painter.setPen(text_color);
const QString min_text = QString::fromUtf8("Min: %1ms").arg(FormatFrameTime(min_frame_time)); const QString min_str = QStringLiteral("Min:%1ms").arg(FormatFrameTime(min_frame_time));
const QString avg_text = QString::fromUtf8("Avg: %1ms").arg(FormatFrameTime(avg_frame_time)); const QString avg_str = QStringLiteral("Avg:%2ms").arg(FormatFrameTime(avg_frame_time));
const QString max_text = QString::fromUtf8("Max: %1ms").arg(FormatFrameTime(max_frame_time)); const QString max_str = QStringLiteral("Max:%1ms").arg(FormatFrameTime(max_frame_time));
painter.drawText(graph_rect.left(), graph_y - 5, min_text); // Combine into one line for measurement
painter.drawText(graph_rect.center().x() - painter.fontMetrics().horizontalAdvance(avg_text) / 2, const QString full_line = QStringLiteral("%1 %2 %3").arg(min_str, avg_str, max_str);
graph_y - 5, avg_text); int total_width = painter.fontMetrics().horizontalAdvance(full_line);
painter.drawText(graph_rect.right() - painter.fontMetrics().horizontalAdvance(max_text),
graph_y - 5, max_text); // 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) { void PerformanceOverlay::AddFrameTime(double frame_time_ms) {
frame_times.push_back(frame_time_ms); frame_times.push_back(frame_time_ms);
if (frame_times.size() > MAX_FRAME_HISTORY) frame_times.pop_front();
// Keep only the last MAX_FRAME_HISTORY frames
if (frame_times.size() > MAX_FRAME_HISTORY) {
frame_times.pop_front();
}
// Update statistics
if (!frame_times.empty()) { if (!frame_times.empty()) {
min_frame_time = *std::min_element(frame_times.begin(), frame_times.end()); min_frame_time = *std::min_element(frame_times.begin(), frame_times.end());
max_frame_time = *std::max_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 { QColor PerformanceOverlay::GetFpsColor(double fps) const {
if (fps >= 55.0) { if (fps >= 55.0) return QColor(76, 175, 80, 255);
return QColor(76, 175, 80, 255); // Material Design green - Good performance if (fps >= 45.0) return QColor(255, 152, 0, 255);
} else if (fps >= 45.0) { if (fps >= 30.0) return QColor(255, 87, 34, 255);
return QColor(255, 152, 0, 255); // Material Design orange - Moderate performance return QColor(244, 67, 54, 255);
} 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
}
} }
QColor PerformanceOverlay::GetTemperatureColor(float temperature) const { QColor PerformanceOverlay::GetTemperatureColor(float temperature) const {
if (temperature > 70.0f) { if (temperature > 85.0f) return QColor(244, 67, 54, 255);
return QColor(244, 67, 54, 255); // Material Design red if (temperature > 75.0f) return QColor(255, 152, 0, 255);
} else if (temperature > 60.0f) { return QColor(76, 175, 80, 255);
return QColor(255, 152, 0, 255); // Material Design orange
} else {
return QColor(76, 175, 80, 255); // Material Design green
}
} }
QString PerformanceOverlay::FormatFps(double fps) const { QString PerformanceOverlay::FormatFps(double fps) const {
if (std::isnan(fps) || fps < 0.0) { if (std::isnan(fps) || fps < 0.0) return QString::fromUtf8("0.0");
return QString::fromUtf8("0.0");
}
return QString::number(fps, 'f', 1); return QString::number(fps, 'f', 1);
} }
QString PerformanceOverlay::FormatFrameTime(double frame_time_ms) const { QString PerformanceOverlay::FormatFrameTime(double frame_time_ms) const {
if (std::isnan(frame_time_ms) || frame_time_ms < 0.0) { if (std::isnan(frame_time_ms) || frame_time_ms < 0.0) return QString::fromUtf8("0.00");
return QString::fromUtf8("0.00");
}
return QString::number(frame_time_ms, 'f', 2); return QString::number(frame_time_ms, 'f', 2);
} }
void PerformanceOverlay::UpdateTheme() { void PerformanceOverlay::UpdateTheme() {
if (UISettings::IsDarkTheme()) { if (UISettings::IsDarkTheme()) {
// Dark Theme Colors (your original values) background_color = QColor(20, 20, 20, 200);
background_color = QColor(20, 20, 20, 200); // Slightly more opaque
border_color = QColor(60, 60, 60, 120); border_color = QColor(60, 60, 60, 120);
text_color = QColor(220, 220, 220, 255); text_color = QColor(220, 220, 220, 255);
graph_background_color = QColor(40, 40, 40, 100); graph_background_color = QColor(40, 40, 40, 100);
} else { } else {
// Light Theme Colors
background_color = QColor(245, 245, 245, 220); background_color = QColor(245, 245, 245, 220);
border_color = QColor(200, 200, 200, 120); border_color = QColor(200, 200, 200, 120);
text_color = QColor(20, 20, 20, 255); text_color = QColor(20, 20, 20, 255);
graph_background_color = QColor(220, 220, 220, 100); 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" #include "citron/uisettings.h"
class GMainWindow; class GMainWindow;
class QSizeGrip;
class PerformanceOverlay : public QWidget { class PerformanceOverlay : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
explicit PerformanceOverlay(GMainWindow* parent); explicit PerformanceOverlay(QWidget* parent);
~PerformanceOverlay() override; ~PerformanceOverlay() override;
void SetVisible(bool visible); void SetVisible(bool visible);
bool IsVisible() const { return is_visible; } bool IsVisible() const { return is_visible; }
void setMainWindow(GMainWindow* window) { main_window = window; }
public slots: public slots:
void UpdateTheme(); void UpdateTheme();
@@ -40,6 +42,9 @@ private slots:
void UpdatePerformanceStats(); void UpdatePerformanceStats();
private: private:
bool is_enabled = false;
bool is_visible = false;
void UpdatePosition(); void UpdatePosition();
void UpdateHardwareTemperatures(); void UpdateHardwareTemperatures();
void DrawPerformanceInfo(QPainter& painter); void DrawPerformanceInfo(QPainter& painter);
@@ -51,6 +56,7 @@ private:
void AddFrameTime(double frame_time_ms); void AddFrameTime(double frame_time_ms);
GMainWindow* main_window; GMainWindow* main_window;
QSizeGrip* size_grip;
QTimer update_timer; QTimer update_timer;
// Performance data // Performance data
@@ -66,14 +72,13 @@ private:
float battery_temperature = 0.0f; float battery_temperature = 0.0f;
// Frame graph data // 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; std::deque<double> frame_times;
double min_frame_time = 0.0; double min_frame_time = 0.0;
double max_frame_time = 0.0; double max_frame_time = 0.0;
double avg_frame_time = 0.0; double avg_frame_time = 0.0;
// Display settings // Display settings
bool is_visible = false;
QFont title_font; QFont title_font;
QFont value_font; QFont value_font;
QFont small_font; QFont small_font;

View File

@@ -5,6 +5,8 @@
#include <QPainter> #include <QPainter>
#include <QPainterPath> #include <QPainterPath>
#include <QScreen> #include <QScreen>
#include <QSizeGrip>
#include <QGridLayout>
#include <QTimer> #include <QTimer>
#include <QMouseEvent> #include <QMouseEvent>
#include <QtMath> #include <QtMath>
@@ -25,58 +27,72 @@
#include "video_core/renderer_vulkan/vk_rasterizer.h" #include "video_core/renderer_vulkan/vk_rasterizer.h"
#include "common/settings.h" #include "common/settings.h"
VramOverlay::VramOverlay(GMainWindow* parent) VramOverlay::VramOverlay(QWidget* parent) : QWidget(UISettings::IsGamescope() ? nullptr : parent) {
: QWidget(parent), main_window(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); 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 // Branching Typography and Sizing
title_font = QFont(QString::fromUtf8("Segoe UI"), 11, QFont::Bold); if (UISettings::IsGamescope()) {
value_font = QFont(QString::fromUtf8("Segoe UI"), 10, QFont::Medium); title_font = QFont(QString::fromUtf8("Segoe UI"), 7, QFont::Bold);
small_font = QFont(QString::fromUtf8("Segoe UI"), 9, QFont::Normal); value_font = QFont(QString::fromUtf8("Segoe UI"), 7, QFont::Medium);
warning_font = QFont(QString::fromUtf8("Segoe UI"), 10, QFont::Bold); 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_safe_color = QColor(76, 175, 80, 255);
vram_warning_color = QColor(255, 193, 7, 255); vram_warning_color = QColor(255, 193, 7, 255);
vram_danger_color = QColor(244, 67, 54, 255); vram_danger_color = QColor(244, 67, 54, 255);
leak_warning_color = QColor(255, 152, 0, 255); leak_warning_color = QColor(255, 152, 0, 255);
// Graph colors - clean and modern auto* layout = new QGridLayout(this);
graph_background_color = QColor(25, 25, 25, 255); layout->setContentsMargins(0, 0, 0, 0);
graph_grid_color = QColor(60, 60, 60, 100); size_grip = new QSizeGrip(this);
graph_line_color = QColor(76, 175, 80, 255); layout->addWidget(size_grip, 0, 0, Qt::AlignBottom | Qt::AlignRight);
graph_fill_color = QColor(76, 175, 80, 40);
// Set up timer for updates
update_timer.setSingleShot(false); update_timer.setSingleShot(false);
connect(&update_timer, &QTimer::timeout, this, &VramOverlay::UpdateVramStats); 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(); UpdateTheme();
// Set clean, compact size
resize(250, 180);
// Position in top-right corner
UpdatePosition(); UpdatePosition();
} }
VramOverlay::~VramOverlay() = default; VramOverlay::~VramOverlay() = default;
void VramOverlay::SetVisible(bool visible) { void VramOverlay::SetVisible(bool visible) {
if (is_visible == visible) { is_enabled = visible;
return; is_visible = visible; // Properly sync the internal state
}
is_visible = visible;
if (visible) { if (visible) {
show(); show();
update_timer.start(1000); // Update every 1 second update_timer.start(1000);
} else { } else {
update_timer.stop(); // Ensure the background loop stops updating
hide(); hide();
update_timer.stop();
} }
} }
@@ -109,9 +125,9 @@ void VramOverlay::paintEvent(QPaintEvent* event) {
} }
void VramOverlay::DrawVramInfo(QPainter& painter) { void VramOverlay::DrawVramInfo(QPainter& painter) {
const int section_padding = 12; const int section_padding = UISettings::IsGamescope() ? 5 : 12;
const int line_height = 14; const int line_height = UISettings::IsGamescope() ? 11 : 14;
const int section_spacing = 6; const int section_spacing = UISettings::IsGamescope() ? 2 : 6;
int y_offset = section_padding + 4; int y_offset = section_padding + 4;
painter.setFont(title_font); painter.setFont(title_font);
@@ -123,22 +139,19 @@ void VramOverlay::DrawVramInfo(QPainter& painter) {
QColor vram_color = GetVramColor(current_vram_data.vram_percentage); QColor vram_color = GetVramColor(current_vram_data.vram_percentage);
painter.setPen(vram_color); painter.setPen(vram_color);
QString vram_text = QString::fromUtf8("%1 / %2 (%3%)") QString vram_text = QString::fromUtf8("%1 / %2 (%3%)")
.arg(FormatMemorySize(current_vram_data.used_vram)) .arg(FormatMemorySize(current_vram_data.used_vram))
.arg(FormatMemorySize(current_vram_data.total_vram)) .arg(FormatMemorySize(current_vram_data.total_vram))
.arg(FormatPercentage(current_vram_data.vram_percentage)); .arg(FormatPercentage(current_vram_data.vram_percentage));
painter.drawText(section_padding, y_offset, vram_text); painter.drawText(section_padding, y_offset, vram_text);
y_offset += line_height + section_spacing; y_offset += line_height + section_spacing;
painter.setFont(small_font); painter.setFont(small_font);
painter.setPen(secondary_text_color); 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, QString::fromUtf8("Buffers: %1").arg(FormatMemorySize(current_vram_data.buffer_memory)));
painter.drawText(section_padding, y_offset, buffer_text); y_offset += line_height - (UISettings::IsGamescope() ? 0 : 1);
y_offset += line_height - 1; painter.drawText(section_padding, y_offset, QString::fromUtf8("Textures: %1").arg(FormatMemorySize(current_vram_data.texture_memory)));
QString texture_text = 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, texture_text); painter.drawText(section_padding, y_offset, QString::fromUtf8("Staging: %1").arg(FormatMemorySize(current_vram_data.staging_memory)));
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);
y_offset += line_height + section_spacing; y_offset += line_height + section_spacing;
painter.setPen(secondary_text_color); painter.setPen(secondary_text_color);
@@ -157,9 +170,9 @@ void VramOverlay::DrawVramGraph(QPainter& painter) {
if (vram_usage_history.empty()) return; if (vram_usage_history.empty()) return;
const int graph_padding = 12; 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 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); QRect graph_rect(graph_padding, graph_y, graph_width, local_graph_height);
QPainterPath graph_path; QPainterPath graph_path;
@@ -169,13 +182,10 @@ void VramOverlay::DrawVramGraph(QPainter& painter) {
painter.setPen(QPen(graph_grid_color, 1)); painter.setPen(QPen(graph_grid_color, 1));
painter.drawPath(graph_path); 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) { 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; QPainterPath line_path;
for (size_t i = 0; i < vram_usage_history.size(); ++i) { 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); 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 + graph_width - 2, graph_y + local_graph_height - 2);
line_path.lineTo(graph_padding + 2, graph_y + local_graph_height - 2); line_path.lineTo(graph_padding + 2, graph_y + local_graph_height - 2);
line_path.closeSubpath(); 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(); UpdatePosition();
} }
void VramOverlay::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton && !size_grip->geometry().contains(event->pos())) {
#if defined(Q_OS_LINUX) #if defined(Q_OS_LINUX)
// LINUX-SPECIFIC IMPLEMENTATION (Wayland Fix) if (!UISettings::IsGamescope() && windowHandle()) {
void VramOverlay::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton) {
if (windowHandle()) {
windowHandle()->startSystemMove(); 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 #else
// ORIGINAL IMPLEMENTATION (For Windows, Android, etc.)
void VramOverlay::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton) {
is_dragging = true; is_dragging = true;
drag_start_pos = event->globalPosition().toPoint(); drag_start_pos = event->globalPosition().toPoint() - this->pos();
widget_start_pos = pos(); #endif
setCursor(Qt::ClosedHandCursor); event->accept();
} }
QWidget::mousePressEvent(event);
} }
void VramOverlay::mouseMoveEvent(QMouseEvent* event) { void VramOverlay::mouseMoveEvent(QMouseEvent* event) {
if (is_dragging) { if (is_dragging) {
QPoint delta = event->globalPosition().toPoint() - drag_start_pos; move(event->globalPosition().toPoint() - drag_start_pos);
move(widget_start_pos + delta); event->accept();
} }
QWidget::mouseMoveEvent(event);
} }
#endif
void VramOverlay::mouseReleaseEvent(QMouseEvent* event) { void VramOverlay::mouseReleaseEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton) { if (event->button() == Qt::LeftButton) {
is_dragging = false; is_dragging = false;
has_been_moved = true; has_been_moved = true;
setCursor(Qt::ArrowCursor); setCursor(Qt::ArrowCursor);
event->accept();
} }
QWidget::mouseReleaseEvent(event); QWidget::mouseReleaseEvent(event);
} }
void VramOverlay::UpdateVramStats() { 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 { try {
current_vram_data.total_vram = main_window->GetTotalVram(); current_vram_data.total_vram = main_window->GetTotalVram();
current_vram_data.used_vram = main_window->GetUsedVram(); current_vram_data.used_vram = main_window->GetUsedVram();
@@ -282,11 +314,12 @@ void VramOverlay::UpdateVramStats() {
} }
last_vram_usage = current_vram_data.used_vram; 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(); update();
} catch (...) { } catch (...) {}
// Ignore
}
} }
QColor VramOverlay::GetVramColor(double percentage) const { QColor VramOverlay::GetVramColor(double percentage) const {
@@ -306,24 +339,8 @@ QString VramOverlay::FormatPercentage(double percentage) const {
return QString::number(percentage, 'f', 1); 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() { void VramOverlay::UpdateTheme() {
if (UISettings::IsDarkTheme()) { if (UISettings::IsDarkTheme()) {
// Dark Theme Colors (your original values)
background_color = QColor(15, 15, 15, 220); background_color = QColor(15, 15, 15, 220);
border_color = QColor(45, 45, 45, 255); border_color = QColor(45, 45, 45, 255);
text_color = QColor(240, 240, 240, 255); text_color = QColor(240, 240, 240, 255);
@@ -331,7 +348,6 @@ void VramOverlay::UpdateTheme() {
graph_background_color = QColor(25, 25, 25, 255); graph_background_color = QColor(25, 25, 25, 255);
graph_grid_color = QColor(60, 60, 60, 100); graph_grid_color = QColor(60, 60, 60, 100);
} else { } else {
// Light Theme Colors
background_color = QColor(245, 245, 245, 220); background_color = QColor(245, 245, 245, 220);
border_color = QColor(200, 200, 200, 255); border_color = QColor(200, 200, 200, 255);
text_color = QColor(20, 20, 20, 255); text_color = QColor(20, 20, 20, 255);
@@ -339,5 +355,5 @@ void VramOverlay::UpdateTheme() {
graph_background_color = QColor(225, 225, 225, 255); graph_background_color = QColor(225, 225, 225, 255);
graph_grid_color = QColor(190, 190, 190, 100); graph_grid_color = QColor(190, 190, 190, 100);
} }
update(); // Force a repaint update();
} }

View File

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