From 0f6049c8883fe3bb0f4b75a05b12b4ffcd5c480d Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 5 Jan 2026 10:08:02 +0000 Subject: [PATCH 01/21] fix(gamescope): Restructure About Dialog UI for Gamescope Signed-off-by: Collecting --- src/citron/about_dialog.cpp | 43 +++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/src/citron/about_dialog.cpp b/src/citron/about_dialog.cpp index d21b901bd..2512a2719 100644 --- a/src/citron/about_dialog.cpp +++ b/src/citron/about_dialog.cpp @@ -2,25 +2,56 @@ // SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include +#include +#include +#include #include #include #include "common/scm_rev.h" #include "ui_aboutdialog.h" #include "citron/about_dialog.h" +#include "citron/uisettings.h" AboutDialog::AboutDialog(QWidget* parent) - : QDialog(parent), ui{std::make_unique()} { + : 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->setupUi(this); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + std::string citron_build_version = "citron | 0.12.25"; #ifdef CITRON_ENABLE_PGO_USE citron_build_version += " | PGO"; #endif - ui->setupUi(this); - // Try and request the icon from Qt theme (Linux?) - const QIcon citron_logo = QIcon::fromTheme(QStringLiteral("org.citron_emu.citron")); - if (!citron_logo.isNull()) { - ui->labelLogo->setPixmap(citron_logo.pixmap(200)); + if (is_gamescope) { + resize(700, 450); + + // Scale fonts up slightly so they aren't "too small" + QFont font = this->font(); + font.setPointSize(font.pointSize() + 1); + this->setFont(font); + + // Keep the Citron header large + ui->labelCitron->setStyleSheet(QStringLiteral("font-size: 24pt; font-weight: bold;")); } + + QPixmap logo_pixmap(QStringLiteral(":/icons/default/256x256/citron.png")); + if (!logo_pixmap.isNull()) { + int logo_size = is_gamescope ? 150 : 200; + ui->labelLogo->setPixmap(logo_pixmap); + ui->labelLogo->setFixedSize(logo_size, logo_size); + ui->labelLogo->setScaledContents(true); + } + ui->labelBuildInfo->setText( ui->labelBuildInfo->text().arg(QString::fromStdString(citron_build_version), QString::fromUtf8(Common::g_build_date).left(10))); From d3f2ce7e7fa247fa631fdc526230b9497219f8ed Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 5 Jan 2026 10:08:34 +0000 Subject: [PATCH 02/21] fix(gamescope): Restructure About Dialog UI for Gamescope Signed-off-by: Collecting --- src/citron/aboutdialog.ui | 161 ++++++-------------------------------- 1 file changed, 24 insertions(+), 137 deletions(-) diff --git a/src/citron/aboutdialog.ui b/src/citron/aboutdialog.ui index 3e4857b93..3c8796e6c 100644 --- a/src/citron/aboutdialog.ui +++ b/src/citron/aboutdialog.ui @@ -3,147 +3,69 @@ AboutDialog - - 0 - 0 - 622 - 294 - + 00620300 About citron - + + 12 + 12 + 12 + 12 + 12 - + - - - 0 - 0 - - - - - 200 - 200 - - - - - - - :/icons/default/256x256/citron.png - - - true - + 160160 + + true + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - - Qt::Vertical - - - - 20 - 40 - - - - + Qt::Vertical - + + 6 - - - 0 - 0 - - - <html><head/><body><p><span style=" font-size:28pt;">citron</span></p></body></html> + <html><body><p><span style=" font-size:28pt; font-weight:600;">citron</span></p></body></html> - - - 0 - 0 - - - - <html><head/><body><p>%1 (%2)</p></body></html> + <html><body><p><span style=" font-size:10pt; color:#888888;">%1 (%2)</span></p></body></html> - - - 0 - 0 - - - <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> -p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:12pt;">citron is an experimental open-source emulator for the Nintendo Switch licensed under GPLv3.0+.</span></p> -<p style="-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;"><br /></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:12pt;">This software should not be used to play games you have not legally obtained.</span></p></body></html> - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true + 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. + true - - - - Qt::Vertical - - - - 20 - 40 - - - - + Qt::Vertical - <html><head/><body><p><a href="https://citron-emu.org/"><span style=" text-decoration: underline; color:#039be5;">Website</span></a> | <a href="https://git.citron-emu.org/citron/emulator"><span style=" text-decoration: underline; color:#039be5;">Source Code</span></a> | <a href="https://git.citron-emu.org/Citron/Emulator/commits/branch/main"><span style=" text-decoration: underline; color:#039be5;">Recent Commits</span></a> | <a href="https://git.citron-emu.org/Citron/Emulator/src/branch/main/LICENSE"><span style=" text-decoration: underline; color:#039be5;">License</span></a></p></body></html> - - - true + <a href="https://citron-emu.org/">Website</a> | <a href="https://git.citron-emu.org/citron/emulator">Source</a> | <a href="https://git.citron-emu.org/">Commits</a> + true - - - 0 - 0 - - - <html><head/><body><p><span style=" font-size:7pt;">&quot;Nintendo Switch&quot; is a trademark of Nintendo. citron is not affiliated with Nintendo in any way.</span></p></body></html> + <span style=" font-size:8pt; color:#777777;">Nintendo Switch is a trademark of Nintendo. citron is not affiliated with Nintendo.</span> @@ -153,52 +75,17 @@ p, li { white-space: pre-wrap; } - - Qt::Horizontal - - - QDialogButtonBox::Ok - + QDialogButtonBox::Ok - - - - buttonBox accepted() AboutDialog accept() - - - 20 - 20 - - - 20 - 20 - - - - - buttonBox - rejected() - AboutDialog - reject() - - - 20 - 20 - - - 20 - 20 - - From 7b3ad22711fe80ecec7729cdef98a3fb0b02cca4 Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 5 Jan 2026 10:09:18 +0000 Subject: [PATCH 03/21] fix(gamescope): Restructure Configure Dialog UI for Gamescope Signed-off-by: Collecting --- src/citron/configuration/configure_dialog.cpp | 45 +++++++++++++++---- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/src/citron/configuration/configure_dialog.cpp b/src/citron/configuration/configure_dialog.cpp index 9abb6798f..ebe40ca9c 100644 --- a/src/citron/configuration/configure_dialog.cpp +++ b/src/citron/configuration/configure_dialog.cpp @@ -108,8 +108,17 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, } Settings::SetConfiguringGlobal(true); - setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | - Qt::WindowMinMaxButtonsHint | Qt::WindowCloseButtonHint); + + const bool is_gamescope = UISettings::IsGamescope(); + if (is_gamescope) { + // GameScope: Use Window flags instead of Dialog to ensure mouse focus + setWindowFlags(Qt::Window | Qt::CustomizeWindowHint | Qt::WindowTitleHint); + setWindowModality(Qt::NonModal); + } else { + setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint); + setWindowModality(Qt::WindowModal); + } + ui->setupUi(this); auto* animation_filter = new StyleAnimationEventFilter(this); @@ -128,9 +137,13 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, ui->topButtonWidget->setLayout(nav_layout); last_palette_text_color = qApp->palette().color(QPalette::WindowText); - if (!UISettings::values.configure_dialog_geometry.isEmpty()) { + + if (is_gamescope) { + resize(1100, 700); + } else if (!UISettings::values.configure_dialog_geometry.isEmpty()) { restoreGeometry(UISettings::values.configure_dialog_geometry); } + UpdateTheme(); tab_button_group = std::make_unique(this); @@ -238,8 +251,6 @@ void ConfigureDialog::UpdateTheme() { if (!rainbow_timer) { rainbow_timer = new QTimer(this); connect(rainbow_timer, &QTimer::timeout, this, [this] { - // MODAL GUARD: If a color dialog or popup is open, pause updates. - // This makes the Color Picker buttons static and responsive. if (m_is_tab_animating || !this->isVisible() || !this->isActiveWindow()) return; const int current_index = ui->stackedWidget->currentIndex(); @@ -261,12 +272,18 @@ void ConfigureDialog::UpdateTheme() { if (ui->horizontalNavWidget) ui->horizontalNavWidget->setStyleSheet(sidebar_css); // 2. Action Buttons (OK/Apply/Cancel) - if (ui->buttonBox && !ui->buttonBox->underMouse()) { - ui->buttonBox->setStyleSheet(QStringLiteral( + if (ui->buttonBox) { + const QString button_css = QStringLiteral( "QPushButton { background-color: %1; color: #ffffff; border-radius: 4px; font-weight: bold; padding: 5px 15px; }" "QPushButton:hover { background-color: %2; }" "QPushButton:pressed { background-color: %3; }" - ).arg(hue_hex).arg(hue_light).arg(hue_dark)); + ).arg(hue_hex).arg(hue_light).arg(hue_dark); + + for (auto* button : ui->buttonBox->findChildren()) { + if (!button->isDown()) { + button->setStyleSheet(button_css); + } + } } // 3. Tab Content Area @@ -302,7 +319,17 @@ void ConfigureDialog::UpdateTheme() { }); } rainbow_timer->start(33); - } else if (rainbow_timer) { + } + + if (ui->buttonBox) { + ui->buttonBox->setStyleSheet(QStringLiteral( + "QPushButton { background-color: %1; color: #ffffff; border-radius: 4px; font-weight: bold; padding: 5px 15px; }" + "QPushButton:hover { background-color: %2; }" + "QPushButton:pressed { background-color: %3; }" + ).arg(accent).arg(Theme::GetAccentColorHover()).arg(Theme::GetAccentColorPressed())); + } + + if (UISettings::values.enable_rainbow_mode.GetValue() == false && rainbow_timer) { rainbow_timer->stop(); if (ui->topButtonWidget) ui->topButtonWidget->setStyleSheet({}); if (ui->horizontalNavWidget) ui->horizontalNavWidget->setStyleSheet({}); From 85e24df186a55cb5cdcd283edb77bfcf01d57adc Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 5 Jan 2026 10:25:05 +0000 Subject: [PATCH 04/21] fix(gamescope): Restructure Configure Dialog UI for Gamescope Signed-off-by: Collecting --- .../configuration/configure_per_game.cpp | 103 +++++++++++------- 1 file changed, 61 insertions(+), 42 deletions(-) diff --git a/src/citron/configuration/configure_per_game.cpp b/src/citron/configuration/configure_per_game.cpp index a8ce0311d..90db7e7c1 100644 --- a/src/citron/configuration/configure_per_game.cpp +++ b/src/citron/configuration/configure_per_game.cpp @@ -76,14 +76,11 @@ static bool IsDarkMode() { const std::string& theme_name = UISettings::values.theme; - // Priority 1: Check for explicitly chosen dark themes. if (theme_name == "qdarkstyle" || theme_name == "colorful_dark" || theme_name == "qdarkstyle_midnight_blue" || theme_name == "colorful_midnight_blue") { - return true; // These themes are always dark. + return true; } - // Priority 2: Check for adaptive themes ("default" and "colorful"). - // For these, we fall back to checking the OS palette. if (theme_name == "default" || theme_name == "colorful") { const QPalette palette = qApp->palette(); const QColor text_color = palette.color(QPalette::WindowText); @@ -91,7 +88,6 @@ static bool IsDarkMode() { return text_color.value() > base_color.value(); } - // Fallback for any other unknown theme (assumed light). return false; } @@ -112,7 +108,6 @@ ConfigurePerGame::ConfigurePerGame(QWidget* parent, u64 title_id_, const std::st : fmt::format("{:016X}", title_id); game_config = std::make_unique(config_file_name, Config::ConfigType::PerGameConfig); - // Create tab instances addons_tab = std::make_unique(system_, this); cheats_tab = std::make_unique(system_, this); audio_tab = std::make_unique(system_, tab_group, *builder, this); @@ -126,8 +121,18 @@ ConfigurePerGame::ConfigurePerGame(QWidget* parent, u64 title_id_, const std::st linux_tab = std::make_unique(system_, tab_group, *builder, this); system_tab = std::make_unique(system_, tab_group, *builder, this); - if (!UISettings::values.per_game_configure_geometry.isEmpty()) { - restoreGeometry(UISettings::values.per_game_configure_geometry); + const bool is_gamescope = UISettings::IsGamescope(); + + if (is_gamescope) { + setWindowFlags(Qt::Window | Qt::CustomizeWindowHint | Qt::WindowTitleHint); + setWindowModality(Qt::NonModal); + resize(1100, 700); + } else { + setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint); + setWindowModality(Qt::WindowModal); + if (!UISettings::values.per_game_configure_geometry.isEmpty()) { + restoreGeometry(UISettings::values.per_game_configure_geometry); + } } UpdateTheme(); @@ -140,10 +145,8 @@ ConfigurePerGame::ConfigurePerGame(QWidget* parent, u64 title_id_, const std::st const auto add_tab = [&](QWidget* widget, const QString& title, int id) { auto button = new QPushButton(title, this); button->setCheckable(true); - // This object name matches the stylesheet ID selector `QPushButton#aestheticTabButton` button->setObjectName(QStringLiteral("aestheticTabButton")); - // This custom property is used by the event filter for the animated style - button->setProperty("class", QStringLiteral("tabButton")); // Keep class for animation + button->setProperty("class", QStringLiteral("tabButton")); button->installEventFilter(animation_filter); ui->tabButtonsLayout->addWidget(button); @@ -177,7 +180,6 @@ ConfigurePerGame::ConfigurePerGame(QWidget* parent, u64 title_id_, const std::st ui->stackedWidget->setCurrentIndex(0); } - setFocusPolicy(Qt::ClickFocus); setWindowTitle(tr("Properties")); addons_tab->SetTitleId(title_id); @@ -322,26 +324,25 @@ void ConfigurePerGame::UpdateTheme() { if (ui->tabButtonsScrollArea) { ui->tabButtonsScrollArea->setStyleSheet(QStringLiteral( "QScrollBar:horizontal { height: 14px; background: transparent; border-radius: 7px; }" - "QScrollBar::handle:horizontal { background-color: %1; border-radius: 6px; min-width: 30px; margin: 1px; }" + "QScrollBar::handle:horizontal { background-color: %1; border-radius: 64px; min-width: 30px; margin: 1px; }" "QScrollBar::add-line, QScrollBar::sub-line { background: none; width: 0px; }" ).arg(hue_hex)); } - // 3. Action Buttons (OK/Cancel) and Trim button - if (ui->buttonBox && !ui->buttonBox->underMouse()) { - ui->buttonBox->setStyleSheet(QStringLiteral( - "QPushButton { background-color: %1; color: #ffffff; border-radius: 4px; font-weight: bold; padding: 5px 15px; }" - "QPushButton:hover { background-color: %2; }" - "QPushButton:pressed { background-color: %3; }" - ).arg(hue_hex).arg(hue_light).arg(hue_dark)); - } + // 3. Action Buttons + const QString button_css = QStringLiteral( + "QPushButton { background-color: %1; color: #ffffff; border-radius: 4px; font-weight: bold; padding: 5px 15px; }" + "QPushButton:hover { background-color: %2; }" + "QPushButton:pressed { background-color: %3; }" + ).arg(hue_hex).arg(hue_light).arg(hue_dark); - if (ui->trim_xci_button && !ui->trim_xci_button->underMouse()) { - ui->trim_xci_button->setStyleSheet(QStringLiteral( - "QPushButton { background-color: %1; color: #ffffff; border: none; border-radius: 4px; padding: 10px; }" - "QPushButton:hover { background-color: %2; }" - "QPushButton:pressed { background-color: %3; }" - ).arg(hue_hex).arg(hue_light).arg(hue_dark)); + if (ui->buttonBox) { + for (auto* button : ui->buttonBox->findChildren()) { + if (!button->isDown()) button->setStyleSheet(button_css); + } + } + if (ui->trim_xci_button && !ui->trim_xci_button->isDown()) { + ui->trim_xci_button->setStyleSheet(button_css); } // 4. Tab Content Area @@ -374,7 +375,25 @@ void ConfigurePerGame::UpdateTheme() { }); } rainbow_timer->start(33); - } else if (rainbow_timer) { + } + + // Fix for Gamescope: Style buttons once outside the timer loop + if (ui->buttonBox) { + ui->buttonBox->setStyleSheet(QStringLiteral( + "QPushButton { background-color: %1; color: #ffffff; border-radius: 4px; font-weight: bold; padding: 5px 15px; }" + "QPushButton:hover { background-color: %2; }" + "QPushButton:pressed { background-color: %3; }" + ).arg(accent).arg(Theme::GetAccentColorHover()).arg(Theme::GetAccentColorPressed())); + } + if (ui->trim_xci_button) { + ui->trim_xci_button->setStyleSheet(QStringLiteral( + "QPushButton { background-color: %1; color: #ffffff; border: none; border-radius: 4px; padding: 10px; }" + "QPushButton:hover { background-color: %2; }" + "QPushButton:pressed { background-color: %3; }" + ).arg(accent).arg(Theme::GetAccentColorHover()).arg(Theme::GetAccentColorPressed())); + } + + if (UISettings::values.enable_rainbow_mode.GetValue() == false && rainbow_timer) { rainbow_timer->stop(); if (ui->tabButtonsContainer) ui->tabButtonsContainer->setStyleSheet({}); if (ui->tabButtonsScrollArea) ui->tabButtonsScrollArea->setStyleSheet({}); @@ -551,8 +570,7 @@ void ConfigurePerGame::LoadConfiguration() { } } } - } catch (...) { - } + } catch (...) {} } try { @@ -610,17 +628,18 @@ void ConfigurePerGame::LoadConfiguration() { } } } - } catch (...) { - } + } catch (...) {} - const auto& system_build_id = system.GetApplicationProcessBuildID(); - const auto system_build_id_hex = Common::HexToString(system_build_id, false); + if (system.IsPoweredOn()) { + const auto& system_build_id = system.GetApplicationProcessBuildID(); + const auto system_build_id_hex = Common::HexToString(system_build_id, false); - if (!system_build_id_hex.empty() && system_build_id_hex != std::string(64, '0')) { - if (!base_build_id_hex.empty() && system_build_id_hex != base_build_id_hex) { - update_build_id_hex = system_build_id_hex; - } else if (base_build_id_hex.empty()) { - base_build_id_hex = system_build_id_hex; + if (!system_build_id_hex.empty() && system_build_id_hex != std::string(64, '0')) { + if (!base_build_id_hex.empty() && system_build_id_hex != base_build_id_hex) { + update_build_id_hex = system_build_id_hex; + } else if (base_build_id_hex.empty()) { + base_build_id_hex = system_build_id_hex; + } } } @@ -863,15 +882,15 @@ void ConfigurePerGame::AnimateTabSwitch(int id) { current_widget->hide(); current_widget->move(0, 0); - m_is_tab_animating = false; // Reset the flag + m_is_tab_animating = false; for (auto button : button_group->buttons()) { button->setEnabled(true); } }); - m_is_tab_animating = true; // Set the flag + m_is_tab_animating = true; for (auto button : button_group->buttons()) { button->setEnabled(false); } animation_group->start(QAbstractAnimation::DeleteWhenStopped); -} +} \ No newline at end of file From 3ed38c872f5f8a0321de4fdeaff5d0b068d0002d Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 5 Jan 2026 10:26:02 +0000 Subject: [PATCH 05/21] fix(gamescope): Restructure Controller Overlay for Gamescope Signed-off-by: Collecting --- src/citron/controller_overlay.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/citron/controller_overlay.h b/src/citron/controller_overlay.h index 69ab4427a..8a0e1dd53 100644 --- a/src/citron/controller_overlay.h +++ b/src/citron/controller_overlay.h @@ -13,6 +13,8 @@ class ControllerOverlay : public QWidget { Q_OBJECT public: + void SetVisible(bool visible); + bool is_enabled = false; explicit ControllerOverlay(GMainWindow* parent); ~ControllerOverlay() override; @@ -35,4 +37,4 @@ private: bool is_dragging = false; QPoint drag_start_pos; -}; \ No newline at end of file +}; From 21a19e79e7a01540b457656a28954a6dccdffe98 Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 5 Jan 2026 10:26:33 +0000 Subject: [PATCH 06/21] fix(gamescope): Restructure Controller Overlay for Gamescope Signed-off-by: Collecting --- src/citron/controller_overlay.cpp | 92 ++++++++++++++++++++++--------- 1 file changed, 67 insertions(+), 25 deletions(-) diff --git a/src/citron/controller_overlay.cpp b/src/citron/controller_overlay.cpp index c2066f43e..d17003f2a 100644 --- a/src/citron/controller_overlay.cpp +++ b/src/citron/controller_overlay.cpp @@ -2,17 +2,19 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "citron/controller_overlay.h" +#include "citron/uisettings.h" #include "citron/configuration/configure_input_player_widget.h" #include "citron/main.h" #include "core/core.h" #include "hid_core/hid_core.h" +#include #include #include #include #include #include -#include // Required for Wayland dragging +#include #include namespace { @@ -26,27 +28,34 @@ Core::HID::EmulatedController* GetPlayer1Controller(Core::System* system) { } return hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1); } + } ControllerOverlay::ControllerOverlay(GMainWindow* parent) - : QWidget(parent, Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint), - main_window(parent) { + : QWidget(parent), main_window(parent) { + + // Gamescope requires ToolTip to stay visible over the game surface, + // but Desktop Wayland/Windows needs Tool to behave correctly in the taskbar/stack. + if (UISettings::IsGamescope()) { + setWindowFlags(Qt::ToolTip | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::WindowDoesNotAcceptFocus); + setAttribute(Qt::WA_ShowWithoutActivating); + setMinimumSize(112, 87); // Use the smaller Gamescope-optimized scale + } else { + setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); + setMinimumSize(225, 175); // Desktop standard scale + } setAttribute(Qt::WA_TranslucentBackground); + setAttribute(Qt::WA_NoSystemBackground); auto* layout = new QGridLayout(this); setLayout(layout); - // Set margins to 0 so the controller can go right to the edge of the resizable window layout->setContentsMargins(0, 0, 0, 0); - // Create the widget that draws the controller and make it transparent + // Create the widget that draws the controller controller_widget = new PlayerControlPreview(this); controller_widget->setAttribute(Qt::WA_TranslucentBackground); - - // Disable the raw joystick (deadzone) visualization controller_widget->SetRawJoystickVisible(false); - - // Allow the widget to expand and shrink with the window controller_widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); layout->addWidget(controller_widget, 0, 0); @@ -54,18 +63,46 @@ ControllerOverlay::ControllerOverlay(GMainWindow* parent) size_grip = new QSizeGrip(this); layout->addWidget(size_grip, 0, 0, Qt::AlignBottom | Qt::AlignRight); - // Start the timer for continuous updates + // Timer for updates connect(&update_timer, &QTimer::timeout, this, &ControllerOverlay::UpdateControllerState); update_timer.start(16); // ~60 FPS - // Set a minimum size and a default starting size - setMinimumSize(225, 175); - resize(450, 350); + // Initial Resize + if (UISettings::IsGamescope()) { + resize(225, 175); + } else { + resize(450, 350); + } } ControllerOverlay::~ControllerOverlay() = default; void ControllerOverlay::UpdateControllerState() { + if (!main_window || !is_enabled) return; + + if (UISettings::IsGamescope()) { + bool ui_active = false; + for (QWidget* w : QApplication::topLevelWidgets()) { + if (w->isWindow() && w->isVisible() && w != main_window && w != this && + !w->inherits("GRenderWindow") && + !w->inherits("PerformanceOverlay") && + !w->inherits("VramOverlay") && + !w->inherits("ControllerOverlay")) { + ui_active = true; + break; + } + } + + if (ui_active) { + if (!this->isHidden()) this->hide(); + return; + } + } + + if (is_enabled && this->isHidden()) { + this->show(); + } + Core::System* system = main_window->GetSystem(); Core::HID::EmulatedController* controller = GetPlayer1Controller(system); if (controller_widget && controller) { @@ -75,22 +112,23 @@ void ControllerOverlay::UpdateControllerState() { } } -// The paint event is now empty, which makes the background fully transparent. void ControllerOverlay::paintEvent(QPaintEvent* event) { Q_UNUSED(event); - // Intentionally left blank to achieve a fully transparent window background. } -// These functions handle dragging the frameless window void ControllerOverlay::mousePressEvent(QMouseEvent* event) { if (event->button() == Qt::LeftButton && !size_grip->geometry().contains(event->pos())) { + + // LOGIC BRANCH: Desktop Linux (Wayland) requires system move. + // Gamescope and Windows require manual dragging. #if defined(Q_OS_LINUX) - // Use system move on Wayland/Linux for proper dragging - if (windowHandle()) { + if (!UISettings::IsGamescope() && windowHandle()) { windowHandle()->startSystemMove(); + } else { + is_dragging = true; + drag_start_pos = event->globalPosition().toPoint() - this->pos(); } #else - // Original dragging implementation for other platforms (Windows, etc.) is_dragging = true; drag_start_pos = event->globalPosition().toPoint() - this->pos(); #endif @@ -99,15 +137,11 @@ void ControllerOverlay::mousePressEvent(QMouseEvent* event) { } void ControllerOverlay::mouseMoveEvent(QMouseEvent* event) { -#if !defined(Q_OS_LINUX) + // Only handle manual dragging if we aren't using startSystemMove (which handles its own move) if (is_dragging) { move(event->globalPosition().toPoint() - drag_start_pos); event->accept(); } -#else - // On Linux, the window manager handles the move, so we do nothing here. - Q_UNUSED(event); -#endif } void ControllerOverlay::mouseReleaseEvent(QMouseEvent* event) { @@ -119,6 +153,14 @@ void ControllerOverlay::mouseReleaseEvent(QMouseEvent* event) { void ControllerOverlay::resizeEvent(QResizeEvent* event) { QWidget::resizeEvent(event); - // This ensures the layout and its widgets (like the size grip) are correctly repositioned on resize. layout()->update(); } + +void ControllerOverlay::SetVisible(bool visible) { + is_enabled = visible; + if (visible) { + this->show(); + } else { + this->hide(); + } +} From b48d726a1143e85d8d18a7b5dab6deedda1a3f1e Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 5 Jan 2026 10:27:55 +0000 Subject: [PATCH 07/21] fix(gamescope): Add Gamescope Logic Signed-off-by: Collecting --- src/citron/main.cpp | 300 +++++++++++++++++++++++++------------------- 1 file changed, 173 insertions(+), 127 deletions(-) diff --git a/src/citron/main.cpp b/src/citron/main.cpp index b96b1e109..c2cf2d8b0 100644 --- a/src/citron/main.cpp +++ b/src/citron/main.cpp @@ -84,6 +84,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include #include #include +#include #include #include #include @@ -94,6 +95,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include #include #include +#include #include #include #include @@ -495,6 +497,12 @@ GMainWindow::GMainWindow(std::unique_ptr config_, bool has_broken_vulk // Create a non-modal QMessageBox instance with a nullptr parent to make it a top-level window. // This prevents it from blocking the main application window. auto* confirmation_dialog = new QMessageBox(nullptr); + const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope"; + if (is_gamescope) { + confirmation_dialog->setWindowFlags(Qt::Window | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowStaysOnTopHint); + confirmation_dialog->resize(650, 300); + confirmation_dialog->setStyleSheet(QStringLiteral("font-size: 11pt;")); + } confirmation_dialog->setAttribute(Qt::WA_DeleteOnClose); // This ensures it is deleted automatically on close. confirmation_dialog->setWindowModality(Qt::NonModal); // Explicitly set modality. confirmation_dialog->setWindowTitle(tr("First-Time Setup")); @@ -854,15 +862,13 @@ void GMainWindow::SoftwareKeyboardShowNormal() { } const auto& layout = render_window->GetFramebufferLayout(); - const auto x = layout.screen.left; const auto y = layout.screen.top; const auto w = layout.screen.GetWidth(); const auto h = layout.screen.GetHeight(); - const auto scale_ratio = devicePixelRatioF(); - software_keyboard->ShowNormalKeyboard(render_window->mapToGlobal(QPoint(x, y) / scale_ratio), - QSize(w, h) / scale_ratio); + software_keyboard->ShowNormalKeyboard(render_window->mapToGlobal(QPoint(x, y)), + QSize(w, h)); } void GMainWindow::SoftwareKeyboardShowTextCheck( @@ -895,11 +901,10 @@ void GMainWindow::SoftwareKeyboardShowInline( (1.0f - appear_parameters.key_top_scale_y)))); const auto w = static_cast(layout.screen.GetWidth() * appear_parameters.key_top_scale_x); const auto h = static_cast(layout.screen.GetHeight() * appear_parameters.key_top_scale_y); - const auto scale_ratio = devicePixelRatioF(); software_keyboard->ShowInlineKeyboard(std::move(appear_parameters), - render_window->mapToGlobal(QPoint(x, y) / scale_ratio), - QSize(w, h) / scale_ratio); + render_window->mapToGlobal(QPoint(x, y)), + QSize(w, h)); } void GMainWindow::SoftwareKeyboardHideInline() { @@ -979,13 +984,11 @@ void GMainWindow::WebBrowserOpenWebPage(const std::string& main_url, } const auto& layout = render_window->GetFramebufferLayout(); - const auto scale_ratio = devicePixelRatioF(); - web_applet->resize(layout.screen.GetWidth() / scale_ratio, - layout.screen.GetHeight() / scale_ratio); - web_applet->move(layout.screen.left / scale_ratio, - (layout.screen.top / scale_ratio) + menuBar()->height()); - web_applet->setZoomFactor(static_cast(layout.screen.GetWidth() / scale_ratio) / - static_cast(Layout::ScreenUndocked::Width)); + web_applet->resize(layout.screen.GetWidth(), layout.screen.GetHeight()); + web_applet->move(layout.screen.left, + (layout.screen.top) + menuBar()->height()); + web_applet->setZoomFactor(static_cast(layout.screen.GetWidth()) / + static_cast(Layout::ScreenUndocked::Width)); web_applet->setFocus(); web_applet->show(); @@ -1159,9 +1162,6 @@ void GMainWindow::InitializeWidgets() { multiplayer_room_overlay = new MultiplayerRoomOverlay(this); multiplayer_room_overlay->hide(); - connect(this, &GMainWindow::EmulationStarting, multiplayer_room_overlay, &MultiplayerRoomOverlay::OnEmulationStarting); - connect(this, &GMainWindow::EmulationStopping, multiplayer_room_overlay, &MultiplayerRoomOverlay::OnEmulationStopping); - vram_overlay = new VramOverlay(this); vram_overlay->hide(); @@ -1353,6 +1353,26 @@ void GMainWindow::InitializeWidgets() { statusBar()->setVisible(true); setStyleSheet(QStringLiteral("QStatusBar::item{border: none;}")); + + const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope"; + if (is_gamescope) { + statusBar()->setSizeGripEnabled(true); + this->menuBar()->setNativeMenuBar(false); + + QString gamescope_style = qApp->styleSheet(); + gamescope_style.append(QStringLiteral("QMenu { background-color: #2b2b2b; border: 1px solid #3d3d3d; padding: 2px; } " + "QMenu::item { padding: 5px 25px 5px 20px; } " + "QMenu::item:selected { background-color: #3d3d3d; }")); + qApp->setStyleSheet(gamescope_style); + + multiplayer_room_overlay->resize(360, 240); + + this->setContentsMargins(0, 0, 0, 0); + this->layout()->setContentsMargins(0, 0, 0, 0); + this->layout()->setSpacing(0); + ui->horizontalLayout->setContentsMargins(0, 0, 0, 0); + ui->horizontalLayout->setSpacing(0); + } } void GMainWindow::InitializeDebugWidgets() { @@ -1483,6 +1503,12 @@ void GMainWindow::InitializeHotkeys() { } void GMainWindow::SetDefaultUIGeometry() { + const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope"; + + if (is_gamescope) { + this->resize(1280, 800); + return; + } // geometry: 53% of the window contents are in the upper screen half, 47% in the lower half const QRect screenRect = QGuiApplication::primaryScreen()->geometry(); @@ -1495,15 +1521,25 @@ void GMainWindow::SetDefaultUIGeometry() { } void GMainWindow::RestoreUIState() { + const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope"; + setWindowFlags(windowFlags() & ~Qt::FramelessWindowHint); - restoreGeometry(UISettings::values.geometry); + + if (!is_gamescope) { + restoreGeometry(UISettings::values.geometry); + } + // Work-around because the games list isn't supposed to be full screen if (isFullScreen()) { showNormal(); } restoreState(UISettings::values.state); - render_window->setWindowFlags(render_window->windowFlags() & ~Qt::FramelessWindowHint); - render_window->restoreGeometry(UISettings::values.renderwindow_geometry); + + if (!is_gamescope) { + render_window->setWindowFlags(render_window->windowFlags() & ~Qt::FramelessWindowHint); + render_window->restoreGeometry(UISettings::values.renderwindow_geometry); + } + #if MICROPROFILE_ENABLED microProfileDialog->restoreGeometry(UISettings::values.microprofile_geometry); microProfileDialog->setVisible(UISettings::values.microprofile_visible.GetValue()); @@ -1528,13 +1564,12 @@ void GMainWindow::RestoreUIState() { ui->action_Show_Status_Bar->setChecked(UISettings::values.show_status_bar.GetValue()); statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked()); - // Force the performance overlay to be off on startup + // Force overlays off on startup ui->action_Show_Performance_Overlay->setChecked(false); if (performance_overlay) { performance_overlay->SetVisible(false); } - // Force the VRAM overlay to be off on startup ui->action_Show_Vram_Overlay->setChecked(false); if (vram_overlay) { vram_overlay->SetVisible(false); @@ -2937,6 +2972,12 @@ void GMainWindow::OnGameListVerifyIntegrity(const std::string& game_path) { }; QProgressDialog progress(tr("Verifying integrity..."), tr("Cancel"), 0, 100, this); + + const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope"; + if (is_gamescope) { + progress.setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::WindowStaysOnTopHint); + } + progress.setWindowModality(Qt::WindowModal); progress.setMinimumDuration(100); progress.setAutoClose(false); @@ -4015,14 +4056,16 @@ void GMainWindow::ShowFullscreen() { } void GMainWindow::HideFullscreen() { + const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope"; + if (ui->action_Single_Window_Mode->isChecked()) { if (UsingExclusiveFullscreen()) { showNormal(); - restoreGeometry(UISettings::values.geometry); + if (!is_gamescope) restoreGeometry(UISettings::values.geometry); } else { hide(); setWindowFlags(windowFlags() & ~Qt::FramelessWindowHint); - restoreGeometry(UISettings::values.geometry); + if (!is_gamescope) restoreGeometry(UISettings::values.geometry); raise(); show(); } @@ -4032,15 +4075,18 @@ void GMainWindow::HideFullscreen() { } else { if (UsingExclusiveFullscreen()) { render_window->showNormal(); - render_window->restoreGeometry(UISettings::values.renderwindow_geometry); + if (!is_gamescope) render_window->restoreGeometry(UISettings::values.renderwindow_geometry); } else { render_window->hide(); render_window->setWindowFlags(windowFlags() & ~Qt::FramelessWindowHint); - render_window->restoreGeometry(UISettings::values.renderwindow_geometry); + if (!is_gamescope) render_window->restoreGeometry(UISettings::values.renderwindow_geometry); render_window->raise(); render_window->show(); } } + + if (is_gamescope) { + } } void GMainWindow::ToggleWindowMode() { @@ -4069,9 +4115,14 @@ void GMainWindow::ToggleWindowMode() { } void GMainWindow::ResetWindowSize(u32 width, u32 height) { + const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope"; + if (is_gamescope) { + return; + } + const auto aspect_ratio = Layout::EmulationAspectRatio( static_cast(Settings::values.aspect_ratio.GetValue()), - static_cast(height) / width); + static_cast(height) / width); if (!ui->action_Single_Window_Mode->isChecked()) { render_window->resize(height / aspect_ratio, height); } else { @@ -4441,6 +4492,15 @@ bool GMainWindow::question(QWidget* parent, const QString& title, const QString& QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { QMessageBox* box_dialog = new QMessageBox(parent); + + const bool is_gamescope = UISettings::IsGamescope(); + if (is_gamescope) { + box_dialog->setWindowFlags(Qt::Window | Qt::CustomizeWindowHint | Qt::WindowTitleHint); + box_dialog->setWindowModality(Qt::NonModal); + box_dialog->setFixedSize(600, 250); + box_dialog->setStyleSheet(QStringLiteral("font-size: 11pt;")); + } + box_dialog->setWindowTitle(title); box_dialog->setText(text); box_dialog->setStandardButtons(buttons); @@ -5026,7 +5086,10 @@ void GMainWindow::OnToggleControllerOverlay() { controller_overlay = new ControllerOverlay(this); } if (controller_overlay) { - controller_overlay->setVisible(visible); + + controller_overlay->SetVisible(visible); + this->update(); + QCoreApplication::processEvents(); } } @@ -5047,7 +5110,6 @@ void GMainWindow::OnTogglePerformanceOverlay() { if (performance_overlay) { const bool is_checked = ui->action_Show_Performance_Overlay->isChecked(); performance_overlay->SetVisible(is_checked); - UISettings::values.show_performance_overlay = is_checked; } } @@ -5066,7 +5128,6 @@ void GMainWindow::OnToggleVramOverlay() { if (vram_overlay) { const bool is_checked = ui->action_Show_Vram_Overlay->isChecked(); vram_overlay->SetVisible(is_checked); - UISettings::values.show_vram_overlay = is_checked; } } @@ -5564,10 +5625,14 @@ void GMainWindow::UpdateStatusButtons() { } void GMainWindow::UpdateUISettings() { - if (!ui->action_Fullscreen->isChecked()) { + const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope"; + + // Only save/restore geometry if we are NOT in gamescope to prevent resolution bugs + if (!ui->action_Fullscreen->isChecked() && !is_gamescope) { UISettings::values.geometry = saveGeometry(); UISettings::values.renderwindow_geometry = render_window->saveGeometry(); } + UISettings::values.state = saveState(); #if MICROPROFILE_ENABLED UISettings::values.microprofile_geometry = microProfileDialog->saveGeometry(); @@ -6029,80 +6094,69 @@ void VolumeButton::ResetMultiplier() { #endif static void SetHighDPIAttributes() { + [[maybe_unused]] const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || + qgetenv("XDG_CURRENT_DESKTOP") == "gamescope" || + !qgetenv("STEAM_DECK").isEmpty(); + #ifdef _WIN32 - // For Windows, we want to avoid scaling artifacts on fractional scaling ratios. - // This is done by setting the optimal scaling policy for the primary screen. + // Windows logic: Set policy globally. + // removed the 'temp QApplication' here because in Qt 6 it locks the DPI logic + // before our environment overrides in main() can take effect. + QGuiApplication::setHighDpiScaleFactorRoundingPolicy( + Qt::HighDpiScaleFactorRoundingPolicy::Round); - // Create a temporary QApplication. - int temp_argc = 0; - char** temp_argv = nullptr; - QApplication temp{temp_argc, temp_argv}; - - // Get the current screen geometry. - const QScreen* primary_screen = QGuiApplication::primaryScreen(); - if (primary_screen == nullptr) { - return; - } - - const QRect screen_rect = primary_screen->geometry(); - const int real_width = screen_rect.width(); - const int real_height = screen_rect.height(); - const float real_ratio = primary_screen->logicalDotsPerInch() / 96.0f; - - // Recommended minimum width and height for proper window fit. - // Any screen with a lower resolution than this will still have a scale of 1. - constexpr float minimum_width = 1350.0f; - constexpr float minimum_height = 900.0f; - - const float width_ratio = std::max(1.0f, real_width / minimum_width); - const float height_ratio = std::max(1.0f, real_height / minimum_height); - - // Get the lower of the 2 ratios and truncate, this is the maximum integer scale. - const float max_ratio = std::trunc(std::min(width_ratio, height_ratio)); - - if (max_ratio > real_ratio) { - QApplication::setHighDpiScaleFactorRoundingPolicy( - Qt::HighDpiScaleFactorRoundingPolicy::Round); - } else { - QApplication::setHighDpiScaleFactorRoundingPolicy( - Qt::HighDpiScaleFactorRoundingPolicy::Floor); - } -#else - // Other OSes should be better than Windows at fractional scaling. - QApplication::setHighDpiScaleFactorRoundingPolicy( - Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); -#endif - - // Set the DPI awareness for better scaling on Windows -#ifdef _WIN32 - // Enable Per Monitor DPI Awareness for Windows 8.1+ SetProcessDPIAware(); - // For Windows 10+, use Per Monitor v2 DPI Awareness - // This provides better scaling for multi-monitor setups HMODULE shcore = LoadLibrary(L"shcore.dll"); if (shcore) { typedef HRESULT(WINAPI* SetProcessDpiAwarenessFunc)(int); SetProcessDpiAwarenessFunc setProcessDpiAwareness = (SetProcessDpiAwarenessFunc)GetProcAddress(shcore, "SetProcessDpiAwareness"); if (setProcessDpiAwareness) { - // PROCESS_PER_MONITOR_DPI_AWARE_V2 = 2 - setProcessDpiAwareness(2); + setProcessDpiAwareness(2); // PROCESS_PER_MONITOR_DPI_AWARE_V2 } FreeLibrary(shcore); } +#else +if (is_gamescope) { + // PassThrough prevents Qt6 from recursively expanding layouts to fit rounded DPIs + QGuiApplication::setHighDpiScaleFactorRoundingPolicy( + Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); +} #endif } int main(int argc, char* argv[]) { - // Set environment variables for AppImage compatibility - // This must be done before the QApplication is created. + // 1. Detect Gamescope/Steam Deck hardware + const bool is_gamescope = UISettings::IsGamescope(); + + if (is_gamescope) { + // Kill the scaling system entirely + qputenv("QT_ENABLE_HIGHDPI_SCALING", "0"); + qputenv("QT_SCALE_FACTOR", "1"); + qputenv("QT_AUTO_SCREEN_SCALE_FACTOR", "0"); + +#ifdef __linux__ + qputenv("QT_QPA_PLATFORM", "xcb"); + qputenv("QT_FONT_DPI", "96"); +#endif + + // Stop Qt from querying physical hardware DPI for text/widgets + qputenv("QT_USE_PHYSICAL_DPI", "0"); + + // Force the legacy coordinate system for X11/XCB + qputenv("QT_SCREEN_SCALE_FACTORS", "1"); + + // Ensure Gamescope compositor handles Citron menus correctly + QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar); + QCoreApplication::setAttribute(Qt::AA_DontUseNativeDialogs); + qputenv("QT_WAYLAND_SHELL_INTEGRATION", "xdg-shell"); + } + + // 2. Setup AppImage environment const bool is_appimage = !qgetenv("APPIMAGE").isEmpty(); if (is_appimage) { - // Fixes Wayland crash with NVIDIA drivers by disabling explicit sync. qputenv("QT_WAYLAND_DISABLE_EXPLICIT_SYNC", "1"); - - // Tell the bundled OpenSSL where to find the bundled certificates. const QDir app_dir(QCoreApplication::applicationDirPath()); const QString certs_path = app_dir.filePath(QString::fromLatin1("../etc/ssl/certs")); qputenv("SSL_CERT_DIR", certs_path.toUtf8()); @@ -6133,73 +6187,73 @@ int main(int argc, char* argv[]) { Common::ConfigureNvidiaEnvironmentFlags(); - // Init settings params QCoreApplication::setOrganizationName(QStringLiteral("citron team")); QCoreApplication::setApplicationName(QStringLiteral("citron")); #ifdef _WIN32 - // Increases the maximum open file limit to 8192 _setmaxstdio(8192); #endif #ifdef __APPLE__ - // If you start a bundle (binary) on OSX without the Terminal, the working directory is "/". - // But since we require the working directory to be the executable path for the location of - // the user folder in the Qt Frontend, we need to cd into that working directory const auto bin_path = Common::FS::GetBundleDirectory() / ".."; chdir(Common::FS::PathToUTF8String(bin_path).c_str()); #endif #ifdef __linux__ - // Set the DISPLAY variable in order to open web browsers - // TODO (lat9nq): Find a better solution for AppImages to start external applications if (QString::fromLocal8Bit(qgetenv("DISPLAY")).isEmpty()) { qputenv("DISPLAY", ":0"); } - - // Fix the Wayland appId. This needs to match the name of the .desktop file without the .desktop - // suffix. QGuiApplication::setDesktopFileName(QStringLiteral("org.citron_emu.citron")); #endif + // Call policy attributes BEFORE creating the real QApplication instance SetHighDPIAttributes(); #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - // Disables the "?" button on all dialogs. Disabled by default on Qt6. QCoreApplication::setAttribute(Qt::AA_DisableWindowContextHelpButton); #endif - // Enables the core to make the qt created contexts current on std::threads QCoreApplication::setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity); QApplication app(argc, argv); + +#ifdef _WIN32 + OverrideWindowsFont(); +#endif + + if (is_gamescope) { + app.setStyleSheet(app.styleSheet().append(QStringLiteral( + "QDialog { " + " font-size: 11pt; " + " margin: 0px; " + " padding: 0px; " + "}" + "QLabel { font-size: 10pt; }" + ))); + + app.setStyle(QStyleFactory::create(QStringLiteral("Fusion"))); + } + #ifdef __linux__ if (QGuiApplication::platformName().startsWith(QStringLiteral("wayland"))) { Settings::values.is_wayland_platform.SetValue(true); } #endif - #ifdef CITRON_USE_AUTO_UPDATER - // Check for and apply staged updates before starting the main application +#ifdef CITRON_USE_AUTO_UPDATER std::filesystem::path app_dir = std::filesystem::path(QCoreApplication::applicationDirPath().toStdString()); #ifdef _WIN32 // On Windows, updates are applied by the helper script after the app exits. - // If we find a staging directory here, it means the helper script failed. - // Clean it up to avoid confusion. std::filesystem::path staging_path = app_dir / "update_staging"; if (std::filesystem::exists(staging_path)) { try { std::filesystem::remove_all(staging_path); - } catch (...) { - // Ignore cleanup errors - } + } catch (...) {} } #else - // On Linux, apply staged updates at startup as before if (Updater::UpdaterService::HasStagedUpdate(app_dir)) { if (Updater::UpdaterService::ApplyStagedUpdate(app_dir)) { - // Show a simple message that update was applied QMessageBox::information(nullptr, QObject::tr("Update Applied"), QObject::tr("Citron has been updated successfully!")); } @@ -6207,37 +6261,29 @@ int main(int argc, char* argv[]) { #endif #endif -#ifdef _WIN32 - OverrideWindowsFont(); -#endif - - // Workaround for QTBUG-85409, for Suzhou numerals the number 1 is actually \u3021 - // so we can see if we get \u3008 instead - // TL;DR all other number formats are consecutive in unicode code points - // This bug is fixed in Qt6, specifically 6.0.0-alpha1 -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - const QLocale locale = QLocale::system(); - if (QStringLiteral("\u3008") == locale.toString(1)) { - QLocale::setDefault(QLocale::system().name()); - } -#endif - - // Qt changes the locale and causes issues in float conversion using std::to_string() when - // generating shaders setlocale(LC_ALL, "C"); GMainWindow main_window{std::move(config), has_broken_vulkan}; - app.setStyle(new RainbowStyle(app.style())); main_window.show(); + if (is_gamescope) { + QTimer::singleShot(200, &main_window, [&main_window]() { + main_window.showMaximized(); + if (main_window.layout()) { + main_window.layout()->activate(); + } + main_window.update(); + main_window.raise(); + main_window.activateWindow(); + }); + } + QObject::connect(&app, &QGuiApplication::applicationStateChanged, &main_window, &GMainWindow::OnAppFocusStateChanged); - int result = app.exec(); - detached_tasks.WaitForAllTasks(); - return result; + return app.exec(); } void GMainWindow::OnCheckForUpdates() { From 28000c203d47b89759dc9f58dadb270607f0d3f4 Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 5 Jan 2026 10:28:56 +0000 Subject: [PATCH 08/21] fix(gamescope): Restructure Multiplayer Dialogs for Gamescope Signed-off-by: Collecting --- src/citron/multiplayer/direct_connect.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/citron/multiplayer/direct_connect.cpp b/src/citron/multiplayer/direct_connect.cpp index eafd964b4..909d14836 100644 --- a/src/citron/multiplayer/direct_connect.cpp +++ b/src/citron/multiplayer/direct_connect.cpp @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include @@ -24,10 +25,22 @@ enum class ConnectionType : u8 { TraversalServer, IP }; DirectConnectWindow::DirectConnectWindow(Core::System& system_, QWidget* parent) - : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), + : QDialog(parent), ui(std::make_unique()), system{system_}, room_network{ system.GetRoomNetwork()} { + const bool is_gamescope = UISettings::IsGamescope(); + if (is_gamescope) { + setWindowFlags(Qt::Window | Qt::CustomizeWindowHint | Qt::WindowTitleHint); + setWindowModality(Qt::NonModal); + + int w = 800; + int h = 500; + setFixedSize(w, h); + } else { + setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint); + } + ui->setupUi(this); // setup the watcher for background connections From 9c49ec0e97b21bd025edd4f54f84fef149a50a61 Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 5 Jan 2026 10:29:41 +0000 Subject: [PATCH 09/21] fix(gamescope): Restructure Multiplayer Dialogs for Gamescope Signed-off-by: Collecting --- src/citron/multiplayer/host_room.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/citron/multiplayer/host_room.cpp b/src/citron/multiplayer/host_room.cpp index 29f7e2896..9d74be9e0 100644 --- a/src/citron/multiplayer/host_room.cpp +++ b/src/citron/multiplayer/host_room.cpp @@ -32,10 +32,23 @@ HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list, std::shared_ptr session, Core::System& system_) - : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), + : QDialog(parent), ui(std::make_unique()), announce_multiplayer_session(session), system{system_}, room_network{ system.GetRoomNetwork()} { + + const bool is_gamescope = UISettings::IsGamescope(); + if (is_gamescope) { + setWindowFlags(Qt::Window | Qt::CustomizeWindowHint | Qt::WindowTitleHint); + setWindowModality(Qt::NonModal); + + int w = 800; + int h = 500; + setFixedSize(w, h); + } else { + setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint); + } + ui->setupUi(this); // set up validation for all of the fields From 88c8b4cdbb6472005a09325ed8c53d55ccb8d9bd Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 5 Jan 2026 10:30:07 +0000 Subject: [PATCH 10/21] fix(gamescope): Restructure Multiplayer Dialogs for Gamescope Signed-off-by: Collecting --- src/citron/multiplayer/lobby.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/citron/multiplayer/lobby.cpp b/src/citron/multiplayer/lobby.cpp index 658e5e1c5..bd085ce26 100644 --- a/src/citron/multiplayer/lobby.cpp +++ b/src/citron/multiplayer/lobby.cpp @@ -27,10 +27,23 @@ Lobby::Lobby(QWidget* parent, QStandardItemModel* list, std::shared_ptr session, Core::System& system_) - : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), + : QDialog(parent), ui(std::make_unique()), announce_multiplayer_session(session), system{system_}, room_network{ system.GetRoomNetwork()} { + + const bool is_gamescope = UISettings::IsGamescope(); + if (is_gamescope) { + setWindowFlags(Qt::Window | Qt::CustomizeWindowHint | Qt::WindowTitleHint); + setWindowModality(Qt::NonModal); + + int w = 800; + int h = 500; + setFixedSize(w, h); + } else { + setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint); + } + ui->setupUi(this); // setup the watcher for background connections From 9185b5ed5b6a75bc5fb0218a4f6caef7b2a113a7 Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 5 Jan 2026 10:30:47 +0000 Subject: [PATCH 11/21] fix(gamescope): Restructure Update Dialog for Gamescope Signed-off-by: Collecting --- src/citron/updater/updater_dialog.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/citron/updater/updater_dialog.cpp b/src/citron/updater/updater_dialog.cpp index 60218add3..1d1769378 100644 --- a/src/citron/updater/updater_dialog.cpp +++ b/src/citron/updater/updater_dialog.cpp @@ -220,9 +220,18 @@ void UpdaterDialog::OnRestartButtonClicked() { } void UpdaterDialog::SetupUI() { - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + const bool is_gamescope = UISettings::IsGamescope(); - setMinimumSize(size()); + if (is_gamescope) { + // Match the behavior of ConfigureDialog to ensure focus and visibility on Steam Deck + setWindowFlags(Qt::Window | Qt::CustomizeWindowHint | Qt::WindowTitleHint); + setWindowModality(Qt::NonModal); + resize(1100, 700); + } else { + // Desktop remains untouched + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + setMinimumSize(size()); + } ui->currentVersionValue->setText(QString::fromStdString(updater_service->GetCurrentVersion())); ui->appImageSelectorLabel->setVisible(false); From 6046f2c9133b309af5323dc3c56e65c713e261ba Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 5 Jan 2026 10:31:13 +0000 Subject: [PATCH 12/21] fix(gamescope): Restructure Update Dialog for Gamescope Signed-off-by: Collecting --- src/citron/updater/updater_dialog.ui | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/citron/updater/updater_dialog.ui b/src/citron/updater/updater_dialog.ui index 1f750ca39..b8fc27074 100644 --- a/src/citron/updater/updater_dialog.ui +++ b/src/citron/updater/updater_dialog.ui @@ -121,13 +121,7 @@ - - - - 16777215 - 150 - - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> @@ -192,7 +186,6 @@ p, li { white-space: pre-wrap; } - @@ -207,7 +200,6 @@ p, li { white-space: pre-wrap; } - From 4b5fbfd5c6dc88af85e18c1ac11bb20499bd165e Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 5 Jan 2026 10:31:53 +0000 Subject: [PATCH 13/21] fix(gamescope): Fix MP Overlay for Gamescope Signed-off-by: Collecting --- src/citron/util/multiplayer_room_overlay.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/citron/util/multiplayer_room_overlay.h b/src/citron/util/multiplayer_room_overlay.h index 91844d37b..7b5b5fe81 100644 --- a/src/citron/util/multiplayer_room_overlay.h +++ b/src/citron/util/multiplayer_room_overlay.h @@ -22,7 +22,7 @@ class MultiplayerRoomOverlay : public QWidget { Q_OBJECT public: - explicit MultiplayerRoomOverlay(GMainWindow* parent); + explicit MultiplayerRoomOverlay(QWidget* parent); ~MultiplayerRoomOverlay() override; void SetVisible(bool visible); From 1c7ca1d5ef70c5cb888a1ec4d0ebc85db60a439b Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 5 Jan 2026 10:32:36 +0000 Subject: [PATCH 14/21] fix(gamescope): Fix MP Overlay for Gamescope Signed-off-by: Collecting --- src/citron/util/multiplayer_room_overlay.cpp | 83 +++++++++++--------- 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/src/citron/util/multiplayer_room_overlay.cpp b/src/citron/util/multiplayer_room_overlay.cpp index f5666b322..c7699aaec 100644 --- a/src/citron/util/multiplayer_room_overlay.cpp +++ b/src/citron/util/multiplayer_room_overlay.cpp @@ -18,12 +18,18 @@ #include "network/room.h" #include "citron/uisettings.h" -MultiplayerRoomOverlay::MultiplayerRoomOverlay(GMainWindow* parent) - : QWidget(parent), main_window(parent) { +MultiplayerRoomOverlay::MultiplayerRoomOverlay(QWidget* parent) +: QWidget(parent) { - setAttribute(Qt::WA_TranslucentBackground, true); - setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint); - setFocusPolicy(Qt::ClickFocus); + main_window = qobject_cast(parent->window()); + + // Switched to Qt::Tool to allow keyboard focus for typing in chat + setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); + setAttribute(Qt::WA_TranslucentBackground); + + // Set smaller sizes for Steam Deck + setMinimumSize(240, 180); + resize(260, 220); main_layout = new QGridLayout(this); main_layout->setContentsMargins(padding, padding, 0, 0); @@ -58,11 +64,25 @@ MultiplayerRoomOverlay::MultiplayerRoomOverlay(GMainWindow* parent) update_timer.setSingleShot(false); connect(&update_timer, &QTimer::timeout, this, &MultiplayerRoomOverlay::UpdateRoomData); - connect(parent, &GMainWindow::themeChanged, this, &MultiplayerRoomOverlay::UpdateTheme); + if (main_window) { + connect(main_window, &GMainWindow::themeChanged, this, &MultiplayerRoomOverlay::UpdateTheme); + } UpdateTheme(); - setMinimumSize(280, 220); - resize(320, 280); + const bool is_gamescope = UISettings::IsGamescope(); + if (is_gamescope) { + setMinimumSize(320, 260); + resize(600, 520); + + players_online_label->setFont(QFont(QString::fromUtf8("Segoe UI"), 11, QFont::Bold)); + + this->padding = 12; + main_layout->setContentsMargins(padding, padding, padding, padding); + } else { + setMinimumSize(280, 220); + resize(320, 280); + } + UpdatePosition(); } @@ -108,48 +128,35 @@ void MultiplayerRoomOverlay::resizeEvent(QResizeEvent* event) { QWidget::resizeE bool MultiplayerRoomOverlay::eventFilter(QObject* watched, QEvent* event) { if (event->type() == QEvent::MouseButtonPress) { if (chat_room_widget->hasFocus()) { chat_room_widget->clearFocus(); } } return QObject::eventFilter(watched, event); } -#if defined(Q_OS_LINUX) void MultiplayerRoomOverlay::mousePressEvent(QMouseEvent* event) { - if (event->button() == Qt::LeftButton) { - if (size_grip->geometry().contains(event->pos())) { - // Let the size grip handle the event - } else if (!childAt(event->pos()) || childAt(event->pos()) == this) { - if (windowHandle()) { - QTimer::singleShot(0, this, [this] { windowHandle()->startSystemMove(); }); - } - } - } - QWidget::mousePressEvent(event); -} - -void MultiplayerRoomOverlay::mouseMoveEvent(QMouseEvent* event) { - QWidget::mouseMoveEvent(event); -} - -#else // Windows and other platforms -void MultiplayerRoomOverlay::mousePressEvent(QMouseEvent* event) { - if (event->button() == Qt::LeftButton) { - if (size_grip->geometry().contains(event->pos())) { - // Let the size grip handle the event - } else if (!childAt(event->pos()) || childAt(event->pos()) == this) { + if (event->button() == Qt::LeftButton && !size_grip->geometry().contains(event->pos())) { + const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope"; + if (is_gamescope) { is_dragging = true; - drag_start_pos = event->globalPosition().toPoint(); - widget_start_pos = this->pos(); + drag_start_pos = event->globalPosition().toPoint() - this->pos(); setCursor(Qt::ClosedHandCursor); + } else { + #if defined(Q_OS_LINUX) + if (windowHandle()) windowHandle()->startSystemMove(); + #else + is_dragging = true; + drag_start_pos = event->globalPosition().toPoint() - this->pos(); + setCursor(Qt::ClosedHandCursor); + #endif } } QWidget::mousePressEvent(event); } void MultiplayerRoomOverlay::mouseMoveEvent(QMouseEvent* event) { - if (is_dragging) { - QPoint delta = event->globalPosition().toPoint() - drag_start_pos; - move(widget_start_pos + delta); - has_been_moved = true; + if (is_dragging && main_window) { + QPoint new_pos = event->globalPosition().toPoint() - drag_start_pos; + QPoint win_origin = main_window->mapToGlobal(QPoint(0, 0)); + move(std::clamp(new_pos.x(), win_origin.x(), win_origin.x() + main_window->width() - width()), + std::clamp(new_pos.y(), win_origin.y(), win_origin.y() + main_window->height() - height())); } QWidget::mouseMoveEvent(event); } -#endif void MultiplayerRoomOverlay::mouseReleaseEvent(QMouseEvent* event) { if (event->button() == Qt::LeftButton && is_dragging) { From 847c648dfe38123cb5f61a49482476efb177337e Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 5 Jan 2026 10:33:16 +0000 Subject: [PATCH 15/21] fix(gamescope): Fix Perf Overlay for Gamescope Signed-off-by: Collecting --- src/citron/util/performance_overlay.h | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/citron/util/performance_overlay.h b/src/citron/util/performance_overlay.h index e17bd94c0..9716289eb 100644 --- a/src/citron/util/performance_overlay.h +++ b/src/citron/util/performance_overlay.h @@ -15,16 +15,18 @@ #include "citron/uisettings.h" class GMainWindow; +class QSizeGrip; class PerformanceOverlay : public QWidget { Q_OBJECT public: - explicit PerformanceOverlay(GMainWindow* parent); + explicit PerformanceOverlay(QWidget* parent); ~PerformanceOverlay() override; void SetVisible(bool visible); bool IsVisible() const { return is_visible; } + void setMainWindow(GMainWindow* window) { main_window = window; } public slots: void UpdateTheme(); @@ -40,6 +42,9 @@ private slots: void UpdatePerformanceStats(); private: + bool is_enabled = false; + bool is_visible = false; + void UpdatePosition(); void UpdateHardwareTemperatures(); void DrawPerformanceInfo(QPainter& painter); @@ -51,6 +56,7 @@ private: void AddFrameTime(double frame_time_ms); GMainWindow* main_window; + QSizeGrip* size_grip; QTimer update_timer; // Performance data @@ -66,14 +72,13 @@ private: float battery_temperature = 0.0f; // Frame graph data - static constexpr size_t MAX_FRAME_HISTORY = 120; // 2 seconds at 60 FPS + static constexpr size_t MAX_FRAME_HISTORY = 120; std::deque frame_times; double min_frame_time = 0.0; double max_frame_time = 0.0; double avg_frame_time = 0.0; // Display settings - bool is_visible = false; QFont title_font; QFont value_font; QFont small_font; From 2d6669ade2d424d561bf9a250a2e0bb5dd8a98c8 Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 5 Jan 2026 10:34:02 +0000 Subject: [PATCH 16/21] fix(gamescope): Fix Perf Overlay for Gamescope Signed-off-by: Collecting --- src/citron/util/performance_overlay.cpp | 493 ++++++++++++------------ 1 file changed, 240 insertions(+), 253 deletions(-) diff --git a/src/citron/util/performance_overlay.cpp b/src/citron/util/performance_overlay.cpp index 90f04038d..0503babc5 100644 --- a/src/citron/util/performance_overlay.cpp +++ b/src/citron/util/performance_overlay.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include #include #include @@ -22,7 +24,7 @@ #include #include #include -#pragma comment(lib, "wbemuuid.lib") // For MSVC, helps the linker find the library +#pragma comment(lib, "wbemuuid.lib") #endif #ifdef Q_OS_ANDROID @@ -37,85 +39,91 @@ #include "video_core/gpu.h" #include "video_core/renderer_base.h" -PerformanceOverlay::PerformanceOverlay(GMainWindow* parent) - : QWidget(parent), main_window(parent) { +PerformanceOverlay::PerformanceOverlay(QWidget* parent) : QWidget(UISettings::IsGamescope() ? nullptr : parent) { + if (parent) { + main_window = qobject_cast(parent); + } + + if (UISettings::IsGamescope()) { + setWindowFlags(Qt::ToolTip | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::WindowDoesNotAcceptFocus); + setAttribute(Qt::WA_ShowWithoutActivating); + } else { + setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); + } - // Set up the widget properties setAttribute(Qt::WA_TranslucentBackground, true); - setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint); + setAttribute(Qt::WA_NoSystemBackground); + setAttribute(Qt::WA_WState_ExplicitShowHide); - // Initialize fonts with better typography - title_font = QFont(QString::fromUtf8("Segoe UI"), 9, QFont::Medium); - value_font = QFont(QString::fromUtf8("Segoe UI"), 11, QFont::Bold); - small_font = QFont(QString::fromUtf8("Segoe UI"), 8, QFont::Normal); + if (UISettings::IsGamescope()) { + title_font = QFont(QString::fromUtf8("Segoe UI"), 9, QFont::Bold); + value_font = QFont(QString::fromUtf8("Segoe UI"), 10, QFont::Bold); + small_font = QFont(QString::fromUtf8("Segoe UI"), 8, QFont::Normal); + setMinimumSize(160, 130); + resize(195, 160); + } else { + title_font = QFont(QString::fromUtf8("Segoe UI"), 9, QFont::Medium); + value_font = QFont(QString::fromUtf8("Segoe UI"), 11, QFont::Bold); + small_font = QFont(QString::fromUtf8("Segoe UI"), 8, QFont::Normal); + setMinimumSize(220, 180); + resize(220, 180); + } - temperature_color = QColor(76, 175, 80, 255); // Default to green + auto* layout = new QGridLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + size_grip = new QSizeGrip(this); + layout->addWidget(size_grip, 0, 0, Qt::AlignBottom | Qt::AlignRight); - // Graph colors + temperature_color = QColor(76, 175, 80, 255); graph_background_color = QColor(40, 40, 40, 100); graph_line_color = QColor(76, 175, 80, 200); graph_fill_color = QColor(76, 175, 80, 60); - // Set up timer for updates update_timer.setSingleShot(false); connect(&update_timer, &QTimer::timeout, this, &PerformanceOverlay::UpdatePerformanceStats); - // Connect to the main window's theme change signal - connect(parent, &GMainWindow::themeChanged, this, &PerformanceOverlay::UpdateTheme); - // Set the initial theme colors + if (main_window) { + connect(main_window, &GMainWindow::themeChanged, this, &PerformanceOverlay::UpdateTheme); + } + UpdateTheme(); - - // Set initial size - larger to accommodate the graph - resize(220, 180); - - // Position in top-left corner UpdatePosition(); } PerformanceOverlay::~PerformanceOverlay() = default; void PerformanceOverlay::SetVisible(bool visible) { - if (is_visible == visible) { - return; - } - - is_visible = visible; + is_enabled = visible; + is_visible = visible; // Update the state so the check works next time if (visible) { show(); - update_timer.start(500); // Update every 500ms for more accurate data + update_timer.start(500); } else { + update_timer.stop(); // Stop the timer first hide(); - update_timer.stop(); } } void PerformanceOverlay::paintEvent(QPaintEvent* event) { Q_UNUSED(event) - QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); painter.setRenderHint(QPainter::TextAntialiasing, true); - // Draw background with rounded corners and subtle shadow effect QPainterPath background_path; background_path.addRoundedRect(rect(), corner_radius, corner_radius); - // Draw subtle shadow - QPainterPath shadow_path = background_path.translated(1, 1); - painter.fillPath(shadow_path, QColor(0, 0, 0, 40)); + if (!UISettings::IsGamescope()) { + QPainterPath shadow_path = background_path.translated(1, 1); + painter.fillPath(shadow_path, QColor(0, 0, 0, 40)); + } - // Draw main background painter.fillPath(background_path, background_color); - - // Draw subtle border painter.setPen(QPen(border_color, border_width)); painter.drawPath(background_path); - // Draw performance information DrawPerformanceInfo(painter); - - // Draw frame graph DrawFrameGraph(painter); } @@ -124,128 +132,110 @@ void PerformanceOverlay::resizeEvent(QResizeEvent* event) { UpdatePosition(); } +void PerformanceOverlay::mousePressEvent(QMouseEvent* event) { + if (event->button() == Qt::LeftButton && !size_grip->geometry().contains(event->pos())) { #if defined(Q_OS_LINUX) -// LINUX-SPECIFIC IMPLEMENTATION (Wayland Fix) -void PerformanceOverlay::mousePressEvent(QMouseEvent* event) { - if (event->button() == Qt::LeftButton) { - // Hand off window moving responsibility to the OS compositor. - if (windowHandle()) { + if (!UISettings::IsGamescope() && windowHandle()) { windowHandle()->startSystemMove(); + } else { + is_dragging = true; + drag_start_pos = event->globalPosition().toPoint() - this->pos(); } - } - QWidget::mousePressEvent(event); -} - -void PerformanceOverlay::mouseMoveEvent(QMouseEvent* event) { - // This function is intentionally left blank for dragging, as the - // system compositor now handles the entire move operation. - QWidget::mouseMoveEvent(event); -} - #else -// ORIGINAL IMPLEMENTATION -void PerformanceOverlay::mousePressEvent(QMouseEvent* event) { - if (event->button() == Qt::LeftButton) { is_dragging = true; - drag_start_pos = event->globalPosition().toPoint(); - widget_start_pos = this->pos(); - setCursor(Qt::ClosedHandCursor); + drag_start_pos = event->globalPosition().toPoint() - this->pos(); +#endif + event->accept(); } - QWidget::mousePressEvent(event); } void PerformanceOverlay::mouseMoveEvent(QMouseEvent* event) { if (is_dragging) { - QPoint delta = event->globalPosition().toPoint() - drag_start_pos; - move(widget_start_pos + delta); + move(event->globalPosition().toPoint() - drag_start_pos); + event->accept(); } - QWidget::mouseMoveEvent(event); } -#endif void PerformanceOverlay::mouseReleaseEvent(QMouseEvent* event) { if (event->button() == Qt::LeftButton) { is_dragging = false; has_been_moved = true; setCursor(Qt::ArrowCursor); + event->accept(); } QWidget::mouseReleaseEvent(event); } - void PerformanceOverlay::UpdatePerformanceStats() { - if (!main_window) { - return; + if (!main_window || !is_enabled) return; + + if (UISettings::IsGamescope()) { + bool ui_active = (QApplication::activePopupWidget() != nullptr); + + if (!ui_active) { + for (QWidget* w : QApplication::topLevelWidgets()) { + if (w->isVisible() && w != main_window && w != this && + !w->inherits("GRenderWindow") && + !w->inherits("VramOverlay") && + !w->inherits("ControllerOverlay") && + !w->inherits("PerformanceOverlay")) { + ui_active = true; + break; + } + } + } + + if (ui_active) { + if (!this->isHidden()) this->hide(); + return; + } + + if (this->isHidden()) { + this->show(); + } + } else { + // Desktop: Only force a show if the user actually has it enabled in the menu + if (is_enabled && this->isHidden()) { + this->show(); + } } - // Get shader building info (this is safe to call) shaders_building = main_window->GetShadersBuilding(); - // Use a static counter to only call the performance methods occasionally - // This reduces the chance of conflicts with the status bar updates static int update_counter = 0; update_counter++; - // Try to get performance data every 2nd update (every 1 second) if (update_counter % 2 == 0) { try { current_fps = main_window->GetCurrentFPS(); current_frame_time = main_window->GetCurrentFrameTime(); emulation_speed = main_window->GetEmulationSpeed(); - // Validate the values - if (std::isnan(current_fps) || current_fps < 0.0 || current_fps > 1000.0) { - current_fps = 60.0; - } - if (std::isnan(current_frame_time) || current_frame_time < 0.0 || current_frame_time > 100.0) { - current_frame_time = 16.67; - } - if (std::isnan(emulation_speed) || emulation_speed < 0.0 || emulation_speed > 1000.0) { - emulation_speed = 100.0; - } + if (std::isnan(current_fps) || current_fps < 0.0 || current_fps > 1000.0) current_fps = 60.0; + if (std::isnan(current_frame_time) || current_frame_time < 0.0 || current_frame_time > 100.0) current_frame_time = 16.67; + if (std::isnan(emulation_speed) || emulation_speed < 0.0 || emulation_speed > 1000.0) emulation_speed = 100.0; - // Ensure FPS and frame time are consistent - if (current_fps > 0.0 && current_frame_time > 0.0) { - // Recalculate frame time from FPS to ensure consistency - current_frame_time = 1000.0 / current_fps; - } - } catch (...) { - // If we get an exception, use the last known good values - // Don't reset to defaults immediately - } + if (current_fps > 0.0) current_frame_time = 1000.0 / current_fps; + } catch (...) {} } - // Update hardware temperatures every 4th update (every 2 seconds) if (update_counter % 4 == 0) { UpdateHardwareTemperatures(); } - // If we don't have valid data yet, use defaults - if (std::isnan(current_fps) || current_fps <= 0.0) { - current_fps = 60.0; - } - if (std::isnan(current_frame_time) || current_frame_time <= 0.0) { - current_frame_time = 16.67; // 60 FPS - } - if (std::isnan(emulation_speed) || emulation_speed <= 0.0) { - emulation_speed = 100.0; - } + if (std::isnan(current_fps) || current_fps <= 0.0) current_fps = 60.0; + if (std::isnan(current_frame_time) || current_frame_time <= 0.0) current_frame_time = 16.67; + if (std::isnan(emulation_speed) || emulation_speed <= 0.0) emulation_speed = 100.0; - // Add frame time to graph history (only if it's valid) - if (current_frame_time > 0.0) { - AddFrameTime(current_frame_time); - } + if (current_frame_time > 0.0) AddFrameTime(current_frame_time); - // Update FPS and Temperature colors based on performance fps_color = GetFpsColor(current_fps); temperature_color = GetTemperatureColor(std::max({cpu_temperature, gpu_temperature, battery_temperature})); - // Trigger a repaint update(); } void PerformanceOverlay::UpdateHardwareTemperatures() { - // Reset data cpu_temperature = 0.0f; gpu_temperature = 0.0f; cpu_sensor_type.clear(); @@ -254,41 +244,80 @@ void PerformanceOverlay::UpdateHardwareTemperatures() { battery_temperature = 0.0f; #if defined(Q_OS_LINUX) - // --- Standard Linux Thermal Zone Reading --- - QDir thermal_dir(QString::fromUtf8("/sys/class/thermal/")); - QStringList filters{QString::fromUtf8("thermal_zone*")}; - QStringList thermal_zones = thermal_dir.entryList(filters, QDir::Dirs); + // 1. Read Battery Data (Steam Deck / Laptops) + QDir bat_dir(QStringLiteral("/sys/class/power_supply/")); + QStringList bats = bat_dir.entryList({QStringLiteral("BAT*")}, QDir::Dirs); + for (const QString& node : bats) { + QFile cap_file(bat_dir.filePath(node + QStringLiteral("/capacity"))); + if (cap_file.open(QIODevice::ReadOnly)) { + battery_percentage = cap_file.readAll().trimmed().toInt(); + cap_file.close(); - for (const QString& zone_name : thermal_zones) { - QFile type_file(thermal_dir.filePath(zone_name + QString::fromUtf8("/type"))); - if (!type_file.open(QIODevice::ReadOnly | QIODevice::Text)) continue; - QString type = QString::fromUtf8(type_file.readAll()).trimmed(); - type_file.close(); - - QFile temp_file(thermal_dir.filePath(zone_name + QString::fromUtf8("/temp"))); - if (!temp_file.open(QIODevice::ReadOnly | QIODevice::Text)) continue; - float temp = temp_file.readAll().trimmed().toFloat() / 1000.0f; - temp_file.close(); - - if (type.contains(QString::fromUtf8("x86_pkg_temp")) || type.contains(QString::fromUtf8("cpu"))) { - if (temp > cpu_temperature) { - cpu_temperature = temp; - cpu_sensor_type = QString::fromUtf8("CPU"); + QFile btemp_file(bat_dir.filePath(node + QStringLiteral("/temp"))); + if (btemp_file.open(QIODevice::ReadOnly)) { + float raw_temp = btemp_file.readAll().trimmed().toFloat(); + // Detect millidegrees (35000) or tenths (350) + battery_temperature = (raw_temp > 1000) ? raw_temp / 1000.0f : raw_temp / 10.0f; + btemp_file.close(); } - } else if (type.contains(QString::fromUtf8("radeon")) || type.contains(QString::fromUtf8("amdgpu")) || type.contains(QString::fromUtf8("nvidia")) || type.contains(QString::fromUtf8("nouveau"))) { - if (temp > gpu_temperature) { - gpu_temperature = temp; - gpu_sensor_type = QString::fromUtf8("GPU"); + break; + } + } + + // 2. Read APU/CPU Temperatures + QDir hwmon_dir(QStringLiteral("/sys/class/hwmon/")); + QStringList hwmons = hwmon_dir.entryList({QStringLiteral("hwmon*")}, QDir::Dirs); + for (const QString& h_node : hwmons) { + QFile name_file(hwmon_dir.filePath(h_node + QStringLiteral("/name"))); + if (!name_file.open(QIODevice::ReadOnly)) continue; + QString hw_name = QString::fromUtf8(name_file.readAll().trimmed()); + name_file.close(); + + // GPU Portion (Standard Steam Deck & Desktop AMD) + if (hw_name == QStringLiteral("amdgpu")) { + QFile t_file(hwmon_dir.filePath(h_node + QStringLiteral("/temp1_input"))); + if (t_file.open(QIODevice::ReadOnly)) { + gpu_temperature = t_file.readAll().trimmed().toFloat() / 1000.0f; + gpu_sensor_type = QStringLiteral("GPU"); + t_file.close(); + } + } + // CPU Portion (k10temp = AMD Deck/Desktop, coretemp = Intel Desktop) + else if (hw_name == QStringLiteral("k10temp") || hw_name == QStringLiteral("coretemp") || hw_name == QStringLiteral("zenpower")) { + // Check for temp1_input (AMD) or temp2_input (Intel coretemp usually starts at 2 for package) + QStringList input_candidates = {QStringLiteral("temp1_input"), QStringLiteral("temp2_input")}; + for (const auto& input : input_candidates) { + QFile t_file(hwmon_dir.filePath(h_node + QStringLiteral("/") + input)); + if (t_file.open(QIODevice::ReadOnly)) { + cpu_temperature = t_file.readAll().trimmed().toFloat() / 1000.0f; + cpu_sensor_type = QStringLiteral("CPU"); + t_file.close(); + if (cpu_temperature > 0) break; + } + } + } + } + + // 3. Fallback to generic thermal_zones + if (cpu_temperature <= 0.0f) { + QDir thermal_dir(QStringLiteral("/sys/class/thermal/")); + QStringList thermal_zones = thermal_dir.entryList({QStringLiteral("thermal_zone*")}, QDir::Dirs); + for (const QString& zone_name : thermal_zones) { + QFile temp_file(thermal_dir.filePath(zone_name + QStringLiteral("/temp"))); + if (temp_file.open(QIODevice::ReadOnly)) { + cpu_temperature = temp_file.readAll().trimmed().toFloat() / 1000.0f; + cpu_sensor_type = QStringLiteral("CPU"); + temp_file.close(); + if (cpu_temperature > 0) break; } } } #endif +} #if defined(Q_OS_ANDROID) - // This uses QtAndroid Extras to get battery info from the Android system. - // NOTE: This requires the QtAndroidExtras module to be linked in the build. QJniObject battery_status = QJniObject::callStaticObjectMethod( - "android/content/CONTEXT", "registerReceiver", + "android/content/Context", "registerReceiver", "(Landroid/content/BroadcastReceiver;Landroid/content/IntentFilter;)Landroid/content/Intent;", nullptr, new QJniObject("android.content.IntentFilter", "(Ljava/lang/String;)V", "android.intent.action.BATTERY_CHANGED")); @@ -300,12 +329,8 @@ void PerformanceOverlay::UpdateHardwareTemperatures() { int temp_tenths = battery_status.callMethod("getIntExtra", "(Ljava/lang/String;I)I", QJniObject::fromString("temperature").object(), -1); - if (scale > 0) { - battery_percentage = (level * 100) / scale; - } - if (temp_tenths > 0) { - battery_temperature = static_cast(temp_tenths) / 10.0f; - } + if (scale > 0) battery_percentage = (level * 100) / scale; + if (temp_tenths > 0) battery_temperature = static_cast(temp_tenths) / 10.0f; } #endif @@ -313,9 +338,7 @@ void PerformanceOverlay::UpdateHardwareTemperatures() { HRESULT hres; IWbemLocator* pLoc = nullptr; IWbemServices* pSvc = nullptr; - hres = CoCreateInstance(CLSID_WbemLocator, 0, CLSCTX_INPROC_SERVER, IID_IWbemLocator, (LPVOID*)&pLoc); - if (SUCCEEDED(hres)) { hres = pLoc->ConnectServer(_bstr_t(L"ROOT\\WMI"), NULL, NULL, 0, NULL, 0, 0, &pSvc); if (SUCCEEDED(hres)) { @@ -331,12 +354,11 @@ void PerformanceOverlay::UpdateHardwareTemperatures() { while (pEnumerator) { pEnumerator->Next(WBEM_INFINITE, 1, &pclsObj, &uReturn); if (uReturn == 0) break; - VARIANT vtProp; pclsObj->Get(L"CurrentTemperature", 0, &vtProp, 0, 0); float temp_kelvin = vtProp.uintVal / 10.0f; cpu_temperature = temp_kelvin - 273.15f; - cpu_sensor_type = QString::fromUtf8("CPU"); + cpu_sensor_type = QStringLiteral("CPU"); VariantClear(&vtProp); pclsObj->Release(); } @@ -348,15 +370,9 @@ void PerformanceOverlay::UpdateHardwareTemperatures() { if(pSvc) pSvc->Release(); if(pLoc) pLoc->Release(); #endif -} void PerformanceOverlay::UpdatePosition() { - if (!main_window) { - return; - } - - // Only position in top-left corner if we haven't been moved by the user - if (!has_been_moved) { + if (main_window && !has_been_moved) { QPoint main_window_pos = main_window->mapToGlobal(QPoint(0, 0)); move(main_window_pos.x() + 10, main_window_pos.y() + 10); } @@ -365,92 +381,85 @@ void PerformanceOverlay::UpdatePosition() { void PerformanceOverlay::DrawPerformanceInfo(QPainter& painter) { painter.setRenderHint(QPainter::TextAntialiasing, true); - int y_offset = padding; - const int line_height = 20; + // Dynamic spacing based on font size to prevent squishing + const int title_step = painter.fontMetrics().height() + 2; + const int stat_step = painter.fontMetrics().height() + 2; - // Draw title + int y_left = (padding / 2) + painter.fontMetrics().ascent(); + int y_right = y_left + 10; + + // 1. Draw Title (Left) painter.setFont(title_font); painter.setPen(text_color); - painter.drawText(padding, y_offset + 12, QString::fromUtf8("CITRON")); + painter.drawText(padding, y_left, QStringLiteral("CITRON PERFORMANCE")); - int y_offset_right = padding; - const int line_height_right = 18; - - // Draw Temperatures + // 2. Draw Hardware Stats (Right Column) painter.setFont(small_font); + const int hw_step = UISettings::IsGamescope() ? 16 : 20; - float core_temp_to_display = std::max(cpu_temperature, gpu_temperature); - if (core_temp_to_display > 0.0f) { - QString core_label = gpu_temperature > cpu_temperature ? gpu_sensor_type : cpu_sensor_type; - QString core_temp_text = QString::fromUtf8("%1: %2°C").arg(core_label).arg(core_temp_to_display, 0, 'f', 0); - painter.setPen(GetTemperatureColor(core_temp_to_display)); - int text_width = painter.fontMetrics().horizontalAdvance(core_temp_text); - painter.drawText(width() - padding - text_width, y_offset_right + 12, core_temp_text); + if (cpu_temperature > 0.0f) { + QString cpu_text = QStringLiteral("CPU:%1°C").arg(cpu_temperature, 0, 'f', 0); + painter.setPen(GetTemperatureColor(cpu_temperature)); + int tw = painter.fontMetrics().horizontalAdvance(cpu_text); + painter.drawText(width() - padding - tw, y_right, cpu_text); + y_right += hw_step; + } + + if (gpu_temperature > 0.0f) { + QString gpu_text = QStringLiteral("GPU:%1°C").arg(gpu_temperature, 0, 'f', 0); + painter.setPen(GetTemperatureColor(gpu_temperature)); + int tw = painter.fontMetrics().horizontalAdvance(gpu_text); + painter.drawText(width() - padding - tw, y_right, gpu_text); + y_right += hw_step; } - y_offset_right += line_height_right; - // Draw Battery info if (battery_percentage > 0) { - QString batt_text = QString::fromUtf8("Batt: %1%").arg(battery_percentage); + QString batt_text = QStringLiteral("Battery %:%1%").arg(battery_percentage); if (battery_temperature > 0.0f) { - batt_text += QString::fromUtf8(" (%1°C)").arg(battery_temperature, 0, 'f', 0); + batt_text += QStringLiteral(" (%1°C)").arg(battery_temperature, 0, 'f', 0); } painter.setPen(text_color); - int text_width = painter.fontMetrics().horizontalAdvance(batt_text); - painter.drawText(width() - padding - text_width, y_offset_right + 12, batt_text); + int tw = painter.fontMetrics().horizontalAdvance(batt_text); + painter.drawText(width() - padding - tw, y_right, batt_text); } - y_offset += line_height + 4; - - // Draw FPS + // 3. Draw FPS (Left Column) + y_left += title_step; painter.setFont(value_font); painter.setPen(fps_color); - QString fps_text = QString::fromUtf8("%1 FPS").arg(FormatFps(current_fps)); - painter.drawText(padding, y_offset, fps_text); - y_offset += line_height; + painter.drawText(padding, y_left, QStringLiteral("%1 FPS").arg(FormatFps(current_fps))); - // Draw frame time + // 4. Draw Small Stats (Left Column) + y_left += title_step; painter.setFont(small_font); painter.setPen(text_color); - QString frame_time_text = QString::fromUtf8("Frame: %1 ms").arg(FormatFrameTime(current_frame_time)); - painter.drawText(padding, y_offset, frame_time_text); - y_offset += line_height - 2; + painter.drawText(padding, y_left, QStringLiteral("Frame:%1 ms").arg(FormatFrameTime(current_frame_time))); - // Draw emulation speed - QString speed_text = QString::fromUtf8("Speed: %1%").arg(emulation_speed, 0, 'f', 0); - painter.drawText(padding, y_offset, speed_text); - y_offset += line_height - 2; + y_left += stat_step; + painter.drawText(padding, y_left, QStringLiteral("Speed:%1%").arg(emulation_speed, 0, 'f', 0)); - // Draw shader building info with accent color if (shaders_building > 0) { - painter.setPen(QColor(255, 152, 0, 255)); // Material Design orange - QString shader_text = QString::fromUtf8("Building: %1 shader(s)").arg(shaders_building); - painter.drawText(padding, y_offset, shader_text); + y_left += stat_step; + painter.setPen(QColor(255, 152, 0)); + painter.drawText(padding, y_left, QStringLiteral("Building:%1").arg(shaders_building)); } } void PerformanceOverlay::DrawFrameGraph(QPainter& painter) { - if (frame_times.empty()) { - return; - } + if (frame_times.empty()) return; const int graph_y = height() - graph_height - padding; const int graph_width = width() - (padding * 2); const QRect graph_rect(padding, graph_y, graph_width, graph_height); - // Draw graph background painter.fillRect(graph_rect, graph_background_color); - // Calculate graph bounds const double min_val = std::max(0.0, min_frame_time - 1.0); - const double max_val = std::max(16.67, max_frame_time + 1.0); // 16.67ms = 60 FPS + const double max_val = std::max(16.67, max_frame_time + 1.0); const double range = max_val - min_val; + if (range <= 0.0) return; - if (range <= 0.0) { - return; - } - - // Draw grid lines + // Grid lines painter.setPen(QPen(QColor(80, 80, 80, 100), 1)); const int grid_lines = 4; for (int i = 1; i < grid_lines; ++i) { @@ -458,12 +467,11 @@ void PerformanceOverlay::DrawFrameGraph(QPainter& painter) { painter.drawLine(graph_rect.left(), y, graph_rect.right(), y); } - // Draw 60 FPS line (16.67ms) + // 60 FPS Target line const int fps60_y = graph_y + graph_height - static_cast((16.67 - min_val) / range * graph_height); painter.setPen(QPen(QColor(255, 255, 255, 80), 1, Qt::DashLine)); painter.drawLine(graph_rect.left(), fps60_y, graph_rect.right(), fps60_y); - // Draw frame time line painter.setPen(QPen(graph_line_color, 2)); painter.setBrush(graph_fill_color); @@ -476,45 +484,39 @@ void PerformanceOverlay::DrawFrameGraph(QPainter& painter) { const double normalized_y = (frame_time - min_val) / range; const int x = graph_rect.left() + static_cast(i * x_step); const int y = graph_y + graph_height - static_cast(normalized_y * graph_height); - - if (i == 0) { - graph_path.moveTo(x, y); - } else { - graph_path.lineTo(x, y); - } + if (i == 0) graph_path.moveTo(x, y); else graph_path.lineTo(x, y); } - // Close the path for filling graph_path.lineTo(graph_rect.right(), graph_rect.bottom()); graph_path.lineTo(graph_rect.left(), graph_rect.bottom()); graph_path.closeSubpath(); - painter.drawPath(graph_path); - // Draw statistics text painter.setFont(small_font); painter.setPen(text_color); - const QString min_text = QString::fromUtf8("Min: %1ms").arg(FormatFrameTime(min_frame_time)); - const QString avg_text = QString::fromUtf8("Avg: %1ms").arg(FormatFrameTime(avg_frame_time)); - const QString max_text = QString::fromUtf8("Max: %1ms").arg(FormatFrameTime(max_frame_time)); + const QString min_str = QStringLiteral("Min:%1ms").arg(FormatFrameTime(min_frame_time)); + const QString avg_str = QStringLiteral("Avg:%2ms").arg(FormatFrameTime(avg_frame_time)); + const QString max_str = QStringLiteral("Max:%1ms").arg(FormatFrameTime(max_frame_time)); - painter.drawText(graph_rect.left(), graph_y - 5, min_text); - painter.drawText(graph_rect.center().x() - painter.fontMetrics().horizontalAdvance(avg_text) / 2, - graph_y - 5, avg_text); - painter.drawText(graph_rect.right() - painter.fontMetrics().horizontalAdvance(max_text), - graph_y - 5, max_text); + // Combine into one line for measurement + const QString full_line = QStringLiteral("%1 %2 %3").arg(min_str, avg_str, max_str); + int total_width = painter.fontMetrics().horizontalAdvance(full_line); + + // If there is enough room, flatten it across the top. Otherwise, stack it. + if (total_width < graph_width - 10) { + // Flat layout + painter.drawText(graph_rect.left(), graph_y - 6, full_line); + } else { + // Stacked layout (Fallback for small windows/High-DPI scaling) + painter.drawText(graph_rect.left(), graph_y - 18, QStringLiteral("%1 %2").arg(min_str, avg_str)); + painter.drawText(graph_rect.left(), graph_y - 4, max_str); + } } void PerformanceOverlay::AddFrameTime(double frame_time_ms) { frame_times.push_back(frame_time_ms); - - // Keep only the last MAX_FRAME_HISTORY frames - if (frame_times.size() > MAX_FRAME_HISTORY) { - frame_times.pop_front(); - } - - // Update statistics + if (frame_times.size() > MAX_FRAME_HISTORY) frame_times.pop_front(); if (!frame_times.empty()) { min_frame_time = *std::min_element(frame_times.begin(), frame_times.end()); max_frame_time = *std::max_element(frame_times.begin(), frame_times.end()); @@ -523,54 +525,39 @@ void PerformanceOverlay::AddFrameTime(double frame_time_ms) { } QColor PerformanceOverlay::GetFpsColor(double fps) const { - if (fps >= 55.0) { - return QColor(76, 175, 80, 255); // Material Design green - Good performance - } else if (fps >= 45.0) { - return QColor(255, 152, 0, 255); // Material Design orange - Moderate performance - } else if (fps >= 30.0) { - return QColor(255, 87, 34, 255); // Material Design deep orange - Poor performance - } else { - return QColor(244, 67, 54, 255); // Material Design red - Very poor performance - } + if (fps >= 55.0) return QColor(76, 175, 80, 255); + if (fps >= 45.0) return QColor(255, 152, 0, 255); + if (fps >= 30.0) return QColor(255, 87, 34, 255); + return QColor(244, 67, 54, 255); } QColor PerformanceOverlay::GetTemperatureColor(float temperature) const { - if (temperature > 70.0f) { - return QColor(244, 67, 54, 255); // Material Design red - } else if (temperature > 60.0f) { - return QColor(255, 152, 0, 255); // Material Design orange - } else { - return QColor(76, 175, 80, 255); // Material Design green - } + if (temperature > 85.0f) return QColor(244, 67, 54, 255); + if (temperature > 75.0f) return QColor(255, 152, 0, 255); + return QColor(76, 175, 80, 255); } QString PerformanceOverlay::FormatFps(double fps) const { - if (std::isnan(fps) || fps < 0.0) { - return QString::fromUtf8("0.0"); - } + if (std::isnan(fps) || fps < 0.0) return QString::fromUtf8("0.0"); return QString::number(fps, 'f', 1); } QString PerformanceOverlay::FormatFrameTime(double frame_time_ms) const { - if (std::isnan(frame_time_ms) || frame_time_ms < 0.0) { - return QString::fromUtf8("0.00"); - } + if (std::isnan(frame_time_ms) || frame_time_ms < 0.0) return QString::fromUtf8("0.00"); return QString::number(frame_time_ms, 'f', 2); } void PerformanceOverlay::UpdateTheme() { if (UISettings::IsDarkTheme()) { - // Dark Theme Colors (your original values) - background_color = QColor(20, 20, 20, 200); // Slightly more opaque + background_color = QColor(20, 20, 20, 200); border_color = QColor(60, 60, 60, 120); text_color = QColor(220, 220, 220, 255); graph_background_color = QColor(40, 40, 40, 100); } else { - // Light Theme Colors background_color = QColor(245, 245, 245, 220); border_color = QColor(200, 200, 200, 120); text_color = QColor(20, 20, 20, 255); graph_background_color = QColor(220, 220, 220, 100); } - update(); // Force a repaint with the new colors + update(); } From 4f9017258a7832b7834e7339e13e5f74075073ee Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 5 Jan 2026 10:34:33 +0000 Subject: [PATCH 17/21] fix(gamescope): Fix VRAM Monitor for Gamescope Signed-off-by: Collecting --- src/citron/util/vram_overlay.h | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/citron/util/vram_overlay.h b/src/citron/util/vram_overlay.h index c53ce8ac4..4d6480316 100644 --- a/src/citron/util/vram_overlay.h +++ b/src/citron/util/vram_overlay.h @@ -15,6 +15,7 @@ #include "citron/uisettings.h" class GMainWindow; +class QSizeGrip; struct VramUsageData { u64 total_vram = 0; @@ -33,7 +34,7 @@ class VramOverlay : public QWidget { Q_OBJECT public: - explicit VramOverlay(GMainWindow* parent); + explicit VramOverlay(QWidget* parent); ~VramOverlay() override; void SetVisible(bool visible); @@ -53,6 +54,9 @@ private slots: void UpdateVramStats(); private: + bool is_enabled = false; + bool is_visible = false; + void UpdatePosition(); void DrawVramInfo(QPainter& painter); void DrawVramGraph(QPainter& painter); @@ -63,6 +67,7 @@ private: void AddVramUsage(double percentage); GMainWindow* main_window; + QSizeGrip* size_grip; QTimer update_timer; // VRAM data @@ -71,13 +76,12 @@ private: u32 frame_counter = 0; // VRAM graph data - static constexpr size_t MAX_VRAM_HISTORY = 120; // 2 seconds at 60 FPS + static constexpr size_t MAX_VRAM_HISTORY = 120; std::deque vram_usage_history; double min_vram_usage = 0.0; double max_vram_usage = 100.0; // Display settings - bool is_visible = false; bool is_dragging = false; bool has_been_moved = false; QPoint drag_start_pos; From 1d80ba5da156837eeace776cf2182601933ccc80 Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 5 Jan 2026 10:35:15 +0000 Subject: [PATCH 18/21] fix(gamescope): Fix VRAM Monitor for Gamescope Signed-off-by: Collecting --- src/citron/util/vram_overlay.cpp | 212 +++++++++++++++++-------------- 1 file changed, 114 insertions(+), 98 deletions(-) diff --git a/src/citron/util/vram_overlay.cpp b/src/citron/util/vram_overlay.cpp index 608f66b07..f66480481 100644 --- a/src/citron/util/vram_overlay.cpp +++ b/src/citron/util/vram_overlay.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include #include #include @@ -25,58 +27,72 @@ #include "video_core/renderer_vulkan/vk_rasterizer.h" #include "common/settings.h" -VramOverlay::VramOverlay(GMainWindow* parent) -: QWidget(parent), main_window(parent) { +VramOverlay::VramOverlay(QWidget* parent) : QWidget(UISettings::IsGamescope() ? nullptr : parent) { + if (parent) { + main_window = qobject_cast(parent); + } + + if (UISettings::IsGamescope()) { + setWindowFlags(Qt::ToolTip | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::WindowDoesNotAcceptFocus); + setAttribute(Qt::WA_ShowWithoutActivating); + } else { + setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); + } - // Set up the widget properties setAttribute(Qt::WA_TranslucentBackground, true); - setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint); + setAttribute(Qt::WA_NoSystemBackground); + setAttribute(Qt::WA_WState_ExplicitShowHide); - // Initialize fonts with clean typography - title_font = QFont(QString::fromUtf8("Segoe UI"), 11, QFont::Bold); - value_font = QFont(QString::fromUtf8("Segoe UI"), 10, QFont::Medium); - small_font = QFont(QString::fromUtf8("Segoe UI"), 9, QFont::Normal); - warning_font = QFont(QString::fromUtf8("Segoe UI"), 10, QFont::Bold); + // Branching Typography and Sizing + if (UISettings::IsGamescope()) { + title_font = QFont(QString::fromUtf8("Segoe UI"), 7, QFont::Bold); + value_font = QFont(QString::fromUtf8("Segoe UI"), 7, QFont::Medium); + small_font = QFont(QString::fromUtf8("Segoe UI"), 6, QFont::Normal); + warning_font = QFont(QString::fromUtf8("Segoe UI"), 8, QFont::Bold); + setMinimumSize(180, 140); + resize(200, 160); + } else { + title_font = QFont(QString::fromUtf8("Segoe UI"), 11, QFont::Bold); + value_font = QFont(QString::fromUtf8("Segoe UI"), 10, QFont::Medium); + small_font = QFont(QString::fromUtf8("Segoe UI"), 9, QFont::Normal); + warning_font = QFont(QString::fromUtf8("Segoe UI"), 10, QFont::Bold); + setMinimumSize(250, 180); + resize(250, 180); + } - // VRAM usage colors - modern palette vram_safe_color = QColor(76, 175, 80, 255); vram_warning_color = QColor(255, 193, 7, 255); vram_danger_color = QColor(244, 67, 54, 255); leak_warning_color = QColor(255, 152, 0, 255); - // Graph colors - clean and modern - graph_background_color = QColor(25, 25, 25, 255); - graph_grid_color = QColor(60, 60, 60, 100); - graph_line_color = QColor(76, 175, 80, 255); - graph_fill_color = QColor(76, 175, 80, 40); + auto* layout = new QGridLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + size_grip = new QSizeGrip(this); + layout->addWidget(size_grip, 0, 0, Qt::AlignBottom | Qt::AlignRight); - // Set up timer for updates update_timer.setSingleShot(false); connect(&update_timer, &QTimer::timeout, this, &VramOverlay::UpdateVramStats); - connect(parent, &GMainWindow::themeChanged, this, &VramOverlay::UpdateTheme); + if (main_window) { + connect(main_window, &GMainWindow::themeChanged, this, &VramOverlay::UpdateTheme); + } + UpdateTheme(); - - // Set clean, compact size - resize(250, 180); - - // Position in top-right corner UpdatePosition(); } VramOverlay::~VramOverlay() = default; void VramOverlay::SetVisible(bool visible) { - if (is_visible == visible) { - return; - } - is_visible = visible; + is_enabled = visible; + is_visible = visible; // Properly sync the internal state + if (visible) { show(); - update_timer.start(1000); // Update every 1 second + update_timer.start(1000); } else { + update_timer.stop(); // Ensure the background loop stops updating hide(); - update_timer.stop(); } } @@ -109,9 +125,9 @@ void VramOverlay::paintEvent(QPaintEvent* event) { } void VramOverlay::DrawVramInfo(QPainter& painter) { - const int section_padding = 12; - const int line_height = 14; - const int section_spacing = 6; + const int section_padding = UISettings::IsGamescope() ? 5 : 12; + const int line_height = UISettings::IsGamescope() ? 11 : 14; + const int section_spacing = UISettings::IsGamescope() ? 2 : 6; int y_offset = section_padding + 4; painter.setFont(title_font); @@ -123,22 +139,19 @@ void VramOverlay::DrawVramInfo(QPainter& painter) { QColor vram_color = GetVramColor(current_vram_data.vram_percentage); painter.setPen(vram_color); QString vram_text = QString::fromUtf8("%1 / %2 (%3%)") - .arg(FormatMemorySize(current_vram_data.used_vram)) - .arg(FormatMemorySize(current_vram_data.total_vram)) - .arg(FormatPercentage(current_vram_data.vram_percentage)); + .arg(FormatMemorySize(current_vram_data.used_vram)) + .arg(FormatMemorySize(current_vram_data.total_vram)) + .arg(FormatPercentage(current_vram_data.vram_percentage)); painter.drawText(section_padding, y_offset, vram_text); y_offset += line_height + section_spacing; painter.setFont(small_font); painter.setPen(secondary_text_color); - QString buffer_text = QString::fromUtf8("Buffers: %1").arg(FormatMemorySize(current_vram_data.buffer_memory)); - painter.drawText(section_padding, y_offset, buffer_text); - y_offset += line_height - 1; - QString texture_text = QString::fromUtf8("Textures: %1").arg(FormatMemorySize(current_vram_data.texture_memory)); - painter.drawText(section_padding, y_offset, texture_text); - y_offset += line_height - 1; - QString staging_text = QString::fromUtf8("Staging: %1").arg(FormatMemorySize(current_vram_data.staging_memory)); - painter.drawText(section_padding, y_offset, staging_text); + painter.drawText(section_padding, y_offset, QString::fromUtf8("Buffers: %1").arg(FormatMemorySize(current_vram_data.buffer_memory))); + y_offset += line_height - (UISettings::IsGamescope() ? 0 : 1); + painter.drawText(section_padding, y_offset, QString::fromUtf8("Textures: %1").arg(FormatMemorySize(current_vram_data.texture_memory))); + y_offset += line_height - (UISettings::IsGamescope() ? 0 : 1); + painter.drawText(section_padding, y_offset, QString::fromUtf8("Staging: %1").arg(FormatMemorySize(current_vram_data.staging_memory))); y_offset += line_height + section_spacing; painter.setPen(secondary_text_color); @@ -157,9 +170,9 @@ void VramOverlay::DrawVramGraph(QPainter& painter) { if (vram_usage_history.empty()) return; const int graph_padding = 12; - const int graph_y = height() - 60; + const int graph_y = height() - (UISettings::IsGamescope() ? 50 : 60); const int graph_width = width() - (graph_padding * 2); - const int local_graph_height = 40; + const int local_graph_height = UISettings::IsGamescope() ? 30 : 40; QRect graph_rect(graph_padding, graph_y, graph_width, local_graph_height); QPainterPath graph_path; @@ -169,13 +182,10 @@ void VramOverlay::DrawVramGraph(QPainter& painter) { painter.setPen(QPen(graph_grid_color, 1)); painter.drawPath(graph_path); - for (int i = 1; i < 4; ++i) { - int y = graph_y + (i * local_graph_height / 4); - painter.drawLine(graph_padding + 1, y, graph_padding + graph_width - 1, y); - } - if (vram_usage_history.size() > 1) { - painter.setPen(QPen(graph_line_color, 2)); + QColor dynamic_color = current_vram_data.leak_detected ? leak_warning_color : GetVramColor(current_vram_data.vram_percentage); + + painter.setPen(QPen(dynamic_color, 2)); QPainterPath line_path; for (size_t i = 0; i < vram_usage_history.size(); ++i) { double x = graph_padding + 2 + (static_cast(i) / (vram_usage_history.size() - 1)) * (graph_width - 4); @@ -187,7 +197,9 @@ void VramOverlay::DrawVramGraph(QPainter& painter) { line_path.lineTo(graph_padding + graph_width - 2, graph_y + local_graph_height - 2); line_path.lineTo(graph_padding + 2, graph_y + local_graph_height - 2); line_path.closeSubpath(); - painter.fillPath(line_path, graph_fill_color); + + // Fill using the dynamic color with transparency + painter.fillPath(line_path, QColor(dynamic_color.red(), dynamic_color.green(), dynamic_color.blue(), 40)); } } @@ -208,54 +220,74 @@ void VramOverlay::resizeEvent(QResizeEvent* event) { UpdatePosition(); } +void VramOverlay::mousePressEvent(QMouseEvent* event) { + if (event->button() == Qt::LeftButton && !size_grip->geometry().contains(event->pos())) { #if defined(Q_OS_LINUX) -// LINUX-SPECIFIC IMPLEMENTATION (Wayland Fix) -void VramOverlay::mousePressEvent(QMouseEvent* event) { - if (event->button() == Qt::LeftButton) { - if (windowHandle()) { + if (!UISettings::IsGamescope() && windowHandle()) { windowHandle()->startSystemMove(); + } else { + is_dragging = true; + drag_start_pos = event->globalPosition().toPoint() - this->pos(); } - } - QWidget::mousePressEvent(event); -} - -void VramOverlay::mouseMoveEvent(QMouseEvent* event) { - // Intentionally blank, the system compositor handles the move. - QWidget::mouseMoveEvent(event); -} - #else -// ORIGINAL IMPLEMENTATION (For Windows, Android, etc.) -void VramOverlay::mousePressEvent(QMouseEvent* event) { - if (event->button() == Qt::LeftButton) { is_dragging = true; - drag_start_pos = event->globalPosition().toPoint(); - widget_start_pos = pos(); - setCursor(Qt::ClosedHandCursor); + drag_start_pos = event->globalPosition().toPoint() - this->pos(); +#endif + event->accept(); } - QWidget::mousePressEvent(event); } void VramOverlay::mouseMoveEvent(QMouseEvent* event) { if (is_dragging) { - QPoint delta = event->globalPosition().toPoint() - drag_start_pos; - move(widget_start_pos + delta); + move(event->globalPosition().toPoint() - drag_start_pos); + event->accept(); } - QWidget::mouseMoveEvent(event); } -#endif void VramOverlay::mouseReleaseEvent(QMouseEvent* event) { if (event->button() == Qt::LeftButton) { is_dragging = false; has_been_moved = true; setCursor(Qt::ArrowCursor); + event->accept(); } QWidget::mouseReleaseEvent(event); } void VramOverlay::UpdateVramStats() { - if (!main_window) return; + if (!main_window || !is_enabled) return; + + if (UISettings::IsGamescope()) { + bool ui_active = (QApplication::activePopupWidget() != nullptr); + + if (!ui_active) { + for (QWidget* w : QApplication::topLevelWidgets()) { + if (w->isVisible() && w != main_window && w != this && + !w->inherits("GRenderWindow") && + !w->inherits("PerformanceOverlay") && + !w->inherits("ControllerOverlay") && + !w->inherits("VramOverlay")) { + ui_active = true; + break; + } + } + } + + if (ui_active) { + if (!this->isHidden()) this->hide(); + return; + } + + if (this->isHidden()) { + this->show(); + } + } else { + // Desktop: Respect the menu toggle strictly + if (is_enabled && this->isHidden()) { + this->show(); + } + } + try { current_vram_data.total_vram = main_window->GetTotalVram(); current_vram_data.used_vram = main_window->GetUsedVram(); @@ -282,11 +314,12 @@ void VramOverlay::UpdateVramStats() { } last_vram_usage = current_vram_data.used_vram; } - AddVramUsage(current_vram_data.vram_percentage); + + vram_usage_history.push_back(current_vram_data.vram_percentage); + if (vram_usage_history.size() > MAX_VRAM_HISTORY) vram_usage_history.pop_front(); + update(); - } catch (...) { - // Ignore - } + } catch (...) {} } QColor VramOverlay::GetVramColor(double percentage) const { @@ -306,24 +339,8 @@ QString VramOverlay::FormatPercentage(double percentage) const { return QString::number(percentage, 'f', 1); } -void VramOverlay::AddVramUsage(double percentage) { - vram_usage_history.push_back(percentage); - if (vram_usage_history.size() > MAX_VRAM_HISTORY) { - vram_usage_history.pop_front(); - } - if (!vram_usage_history.empty()) { - min_vram_usage = *std::min_element(vram_usage_history.begin(), vram_usage_history.end()); - max_vram_usage = *std::max_element(vram_usage_history.begin(), vram_usage_history.end()); - double range = max_vram_usage - min_vram_usage; - if (range < 10.0) range = 10.0; - min_vram_usage = std::max(0.0, min_vram_usage - range * 0.1); - max_vram_usage = std::min(100.0, max_vram_usage + range * 0.1); - } -} - void VramOverlay::UpdateTheme() { if (UISettings::IsDarkTheme()) { - // Dark Theme Colors (your original values) background_color = QColor(15, 15, 15, 220); border_color = QColor(45, 45, 45, 255); text_color = QColor(240, 240, 240, 255); @@ -331,7 +348,6 @@ void VramOverlay::UpdateTheme() { graph_background_color = QColor(25, 25, 25, 255); graph_grid_color = QColor(60, 60, 60, 100); } else { - // Light Theme Colors background_color = QColor(245, 245, 245, 220); border_color = QColor(200, 200, 200, 255); text_color = QColor(20, 20, 20, 255); @@ -339,5 +355,5 @@ void VramOverlay::UpdateTheme() { graph_background_color = QColor(225, 225, 225, 255); graph_grid_color = QColor(190, 190, 190, 100); } - update(); // Force a repaint + update(); } From b5a472ced169522dddf2544eefef9fafdf896cd0 Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 5 Jan 2026 10:36:08 +0000 Subject: [PATCH 19/21] fix(gamescope): Add Boolean for Gamescope Signed-off-by: Collecting --- src/citron/uisettings.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/citron/uisettings.h b/src/citron/uisettings.h index 6aece0855..7dcd66ab8 100644 --- a/src/citron/uisettings.h +++ b/src/citron/uisettings.h @@ -37,6 +37,7 @@ namespace Settings { namespace UISettings { bool IsDarkTheme(); + bool IsGamescope(); struct ContextualShortcut { std::string keyseq; From 136e04de5b8734332c58e93a84e5a96422a06f53 Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 5 Jan 2026 10:36:34 +0000 Subject: [PATCH 20/21] fix(gamescope): Add Boolean for Gamescope Signed-off-by: Collecting --- src/citron/uisettings.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/citron/uisettings.cpp b/src/citron/uisettings.cpp index c1b5a0d2e..1c064fbb4 100644 --- a/src/citron/uisettings.cpp +++ b/src/citron/uisettings.cpp @@ -136,4 +136,15 @@ namespace UISettings { config.value(QStringLiteral("microProfileDialogGeometry")).toByteArray(); } + bool IsGamescope() { +#ifdef __linux__ + static const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || + qgetenv("XDG_CURRENT_DESKTOP") == "gamescope" || + !qgetenv("STEAM_DECK").isEmpty(); + return is_gamescope; +#else + return false; +#endif +} + } // namespace UISettings From 2fc9ab97be6a383d6e67dbf06f5733dfb8b5abfd Mon Sep 17 00:00:00 2001 From: Collecting Date: Mon, 5 Jan 2026 11:08:25 +0000 Subject: [PATCH 21/21] fix(gamescope): MSVC Compilation Error Signed-off-by: Collecting --- src/citron/util/performance_overlay.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/citron/util/performance_overlay.cpp b/src/citron/util/performance_overlay.cpp index 0503babc5..d41459ef3 100644 --- a/src/citron/util/performance_overlay.cpp +++ b/src/citron/util/performance_overlay.cpp @@ -313,7 +313,6 @@ void PerformanceOverlay::UpdateHardwareTemperatures() { } } #endif -} #if defined(Q_OS_ANDROID) QJniObject battery_status = QJniObject::callStaticObjectMethod( @@ -370,6 +369,7 @@ void PerformanceOverlay::UpdateHardwareTemperatures() { if(pSvc) pSvc->Release(); if(pLoc) pLoc->Release(); #endif +} void PerformanceOverlay::UpdatePosition() { if (main_window && !has_been_moved) {