From c393db419e606b30c8f257b45ce2a1a7fd796a24 Mon Sep 17 00:00:00 2001
From: Stenzek <stenzek@gmail.com>
Date: Sun, 15 Jan 2023 16:40:35 +1000
Subject: [PATCH] Qt: Add Controller LED options (where supported)

---
 src/duckstation-qt/CMakeLists.txt             |  3 +
 src/duckstation-qt/colorpickerbutton.cpp      | 50 +++++++++++
 src/duckstation-qt/colorpickerbutton.h        | 29 +++++++
 .../controllerglobalsettingswidget.cpp        | 51 +++++++++++-
 .../controllerglobalsettingswidget.h          | 21 +++++
 .../controllerglobalsettingswidget.ui         | 26 ++++--
 .../controllerledsettingsdialog.ui            | 83 +++++++++++++++++++
 src/duckstation-qt/duckstation-qt.vcxproj     |  6 ++
 .../duckstation-qt.vcxproj.filters            |  7 ++
 .../icons/black/svg/lightbulb-line.svg        |  1 +
 .../icons/white/svg/lightbulb-line.svg        |  1 +
 src/duckstation-qt/resources/resources.qrc    |  2 +
 src/frontend-common/CMakeLists.txt            |  4 +-
 src/frontend-common/sdl_input_source.cpp      | 51 ++++++++++++
 src/frontend-common/sdl_input_source.h        |  6 ++
 15 files changed, 331 insertions(+), 10 deletions(-)
 create mode 100644 src/duckstation-qt/colorpickerbutton.cpp
 create mode 100644 src/duckstation-qt/colorpickerbutton.h
 create mode 100644 src/duckstation-qt/controllerledsettingsdialog.ui
 create mode 100644 src/duckstation-qt/resources/icons/black/svg/lightbulb-line.svg
 create mode 100644 src/duckstation-qt/resources/icons/white/svg/lightbulb-line.svg

diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt
index 367f1dd93..90c18ee36 100644
--- a/src/duckstation-qt/CMakeLists.txt
+++ b/src/duckstation-qt/CMakeLists.txt
@@ -27,6 +27,8 @@ set(SRCS
   cheatmanagerdialog.ui
   collapsiblewidget.cpp
   collapsiblewidget.h
+  colorpickerbutton.cpp
+  colorpickerbutton.h
   consolesettingswidget.cpp
   consolesettingswidget.h
   consolesettingswidget.ui
@@ -42,6 +44,7 @@ set(SRCS
   controllerglobalsettingswidget.cpp
   controllerglobalsettingswidget.h
   controllerglobalsettingswidget.ui
+  controllerledsettingsdialog.ui
   controllermacroeditwidget.ui
   controllermacrowidget.ui
   controllersettingsdialog.cpp
diff --git a/src/duckstation-qt/colorpickerbutton.cpp b/src/duckstation-qt/colorpickerbutton.cpp
new file mode 100644
index 000000000..52ed76fee
--- /dev/null
+++ b/src/duckstation-qt/colorpickerbutton.cpp
@@ -0,0 +1,50 @@
+// SPDX-FileCopyrightText: 2023 Connor McLaughlin <stenzek@gmail.com>
+// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
+
+#include "colorpickerbutton.h"
+#include "qtutils.h"
+
+#include <QtWidgets/QColorDialog>
+
+ColorPickerButton::ColorPickerButton(QWidget* parent) : QPushButton(parent)
+{
+  connect(this, &QPushButton::clicked, this, &ColorPickerButton::onClicked);
+  updateBackgroundColor();
+}
+
+u32 ColorPickerButton::color()
+{
+  return m_color;
+}
+
+void ColorPickerButton::setColor(u32 rgb)
+{
+  if (m_color == rgb)
+    return;
+
+  m_color = rgb;
+  updateBackgroundColor();
+}
+
+void ColorPickerButton::updateBackgroundColor()
+{
+  setStyleSheet(QStringLiteral("background-color: #%1;").arg(static_cast<uint>(m_color), 8, 16, QChar('0')));
+}
+
+void ColorPickerButton::onClicked()
+{
+  const u32 red = (m_color >> 16) & 0xff;
+  const u32 green = (m_color >> 8) & 0xff;
+  const u32 blue = m_color & 0xff;
+
+  const QColor initial(QColor::fromRgb(red, green, blue));
+  const QColor selected(QColorDialog::getColor(initial, QtUtils::GetRootWidget(this), tr("Select LED Color")));
+  if (initial == selected)
+    return;
+
+  const u32 new_rgb = (static_cast<u32>(selected.red()) << 16) | (static_cast<u32>(selected.green()) << 8) |
+                      static_cast<u32>(selected.blue());
+  m_color = new_rgb;
+  updateBackgroundColor();
+  emit colorChanged(new_rgb);
+}
diff --git a/src/duckstation-qt/colorpickerbutton.h b/src/duckstation-qt/colorpickerbutton.h
new file mode 100644
index 000000000..c15908301
--- /dev/null
+++ b/src/duckstation-qt/colorpickerbutton.h
@@ -0,0 +1,29 @@
+// SPDX-FileCopyrightText: 2023 Connor McLaughlin <stenzek@gmail.com>
+// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
+
+#pragma once
+#include "common/types.h"
+#include <QtWidgets/QPushButton>
+
+class ColorPickerButton : public QPushButton
+{
+  Q_OBJECT
+
+public:
+  ColorPickerButton(QWidget* parent);
+
+Q_SIGNALS:
+  void colorChanged(quint32 new_color);
+
+public Q_SLOTS:
+  quint32 color();
+  void setColor(quint32 rgb);
+
+private Q_SLOTS:
+  void onClicked();
+
+private:
+  void updateBackgroundColor();
+
+  u32 m_color = 0;
+};
diff --git a/src/duckstation-qt/controllerglobalsettingswidget.cpp b/src/duckstation-qt/controllerglobalsettingswidget.cpp
index 17d3788f5..beeed27e5 100644
--- a/src/duckstation-qt/controllerglobalsettingswidget.cpp
+++ b/src/duckstation-qt/controllerglobalsettingswidget.cpp
@@ -7,6 +7,8 @@
 #include "qtutils.h"
 #include "settingwidgetbinder.h"
 
+#include "frontend-common/sdl_input_source.h"
+
 ControllerGlobalSettingsWidget::ControllerGlobalSettingsWidget(QWidget* parent, ControllerSettingsDialog* dialog)
   : QWidget(parent), m_dialog(dialog)
 {
@@ -14,10 +16,18 @@ ControllerGlobalSettingsWidget::ControllerGlobalSettingsWidget(QWidget* parent,
 
   SettingsInterface* sif = dialog->getProfileSettingsInterface();
 
+#ifdef WITH_SDL2
   SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.enableSDLSource, "InputSources", "SDL", true);
   SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.enableSDLEnhancedMode, "InputSources",
                                                "SDLControllerEnhancedMode", false);
-  SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.enableMouseMapping, "UI", "EnableMouseMapping", false);
+  connect(m_ui.enableSDLSource, &QCheckBox::stateChanged, this,
+          &ControllerGlobalSettingsWidget::updateSDLOptionsEnabled);
+  connect(m_ui.ledSettings, &QToolButton::clicked, this, &ControllerGlobalSettingsWidget::ledSettingsClicked);
+#else
+  m_ui.enableSDLSource->setEnabled(false);
+  m_ui.ledSettings->setEnabled(false);
+#endif
+
 #ifdef _WIN32
   SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.enableDInputSource, "InputSources", "DInput", false);
   SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.enableXInputSource, "InputSources", "XInput", false);
@@ -27,6 +37,7 @@ ControllerGlobalSettingsWidget::ControllerGlobalSettingsWidget(QWidget* parent,
   m_ui.enableXInputSource->setEnabled(false);
   m_ui.enableRawInput->setEnabled(false);
 #endif
+  SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.enableMouseMapping, "UI", "EnableMouseMapping", false);
   SettingWidgetBinder::BindWidgetToEnumSetting(sif, m_ui.multitapMode, "ControllerPorts", "MultitapMode",
                                                &Settings::ParseMultitapModeName, &Settings::GetMultitapModeName,
                                                Settings::DEFAULT_MULTITAP_MODE);
@@ -52,8 +63,6 @@ ControllerGlobalSettingsWidget::ControllerGlobalSettingsWidget(QWidget* parent,
     m_ui.profileSettings = nullptr;
   }
 
-  connect(m_ui.enableSDLSource, &QCheckBox::stateChanged, this,
-          &ControllerGlobalSettingsWidget::updateSDLOptionsEnabled);
   connect(m_ui.multitapMode, &QComboBox::currentIndexChanged, this, [this]() { emit bindingSetupChanged(); });
 
   connect(m_ui.pointerXScale, &QSlider::valueChanged, this,
@@ -90,8 +99,44 @@ void ControllerGlobalSettingsWidget::removeDeviceFromList(const QString& identif
   }
 }
 
+void ControllerGlobalSettingsWidget::ledSettingsClicked()
+{
+  ControllerLEDSettingsDialog dialog(this, m_dialog);
+  dialog.exec();
+}
+
 void ControllerGlobalSettingsWidget::updateSDLOptionsEnabled()
 {
   const bool enabled = m_ui.enableSDLSource->isChecked();
   m_ui.enableSDLEnhancedMode->setEnabled(enabled);
+  m_ui.ledSettings->setEnabled(enabled);
+}
+
+ControllerLEDSettingsDialog::ControllerLEDSettingsDialog(QWidget* parent, ControllerSettingsDialog* dialog)
+  : QDialog(parent), m_dialog(dialog)
+{
+  m_ui.setupUi(this);
+
+  linkButton(m_ui.SDL0LED, 0);
+  linkButton(m_ui.SDL1LED, 1);
+  linkButton(m_ui.SDL2LED, 2);
+  linkButton(m_ui.SDL3LED, 3);
+
+  connect(m_ui.buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &QDialog::accept);
+}
+
+ControllerLEDSettingsDialog::~ControllerLEDSettingsDialog() = default;
+
+void ControllerLEDSettingsDialog::linkButton(ColorPickerButton* button, u32 player_id)
+{
+#ifdef WITH_SDL2
+  std::string key(fmt::format("Player{}LED", player_id));
+  const u32 current_value =
+    SDLInputSource::ParseRGBForPlayerId(m_dialog->getStringValue("SDLExtra", key.c_str(), ""), player_id);
+  button->setColor(current_value);
+
+  connect(button, &ColorPickerButton::colorChanged, this, [this, player_id, key = std::move(key)](u32 new_rgb) {
+    m_dialog->setStringValue("SDLExtra", key.c_str(), fmt::format("{:06X}", new_rgb).c_str());
+  });
+#endif
 }
diff --git a/src/duckstation-qt/controllerglobalsettingswidget.h b/src/duckstation-qt/controllerglobalsettingswidget.h
index d6c0ce1d0..cb34fed51 100644
--- a/src/duckstation-qt/controllerglobalsettingswidget.h
+++ b/src/duckstation-qt/controllerglobalsettingswidget.h
@@ -3,12 +3,17 @@
 
 #pragma once
 #include "common/types.h"
+
 #include <QtCore/QMap>
+#include <QtWidgets/QDialog>
 #include <QtWidgets/QWidget>
 #include <array>
 #include <vector>
 
+#include "colorpickerbutton.h"
+
 #include "ui_controllerglobalsettingswidget.h"
+#include "ui_controllerledsettingsdialog.h"
 
 class ControllerSettingsDialog;
 
@@ -28,7 +33,23 @@ Q_SIGNALS:
 
 private:
   void updateSDLOptionsEnabled();
+  void ledSettingsClicked();
 
   Ui::ControllerGlobalSettingsWidget m_ui;
   ControllerSettingsDialog* m_dialog;
 };
+
+class ControllerLEDSettingsDialog : public QDialog
+{
+  Q_OBJECT
+
+public:
+  ControllerLEDSettingsDialog(QWidget* parent, ControllerSettingsDialog* dialog);
+  ~ControllerLEDSettingsDialog();
+
+private:
+  void linkButton(ColorPickerButton* button, u32 player_id);
+
+  Ui::ControllerLEDSettingsDialog m_ui;
+  ControllerSettingsDialog* m_dialog;
+};
diff --git a/src/duckstation-qt/controllerglobalsettingswidget.ui b/src/duckstation-qt/controllerglobalsettingswidget.ui
index fd3ed2b38..bcb411796 100644
--- a/src/duckstation-qt/controllerglobalsettingswidget.ui
+++ b/src/duckstation-qt/controllerglobalsettingswidget.ui
@@ -151,11 +151,27 @@
        </widget>
       </item>
       <item row="1" column="1">
-       <widget class="QCheckBox" name="enableSDLEnhancedMode">
-        <property name="text">
-         <string>DualShock 4 / DualSense Enhanced Mode</string>
-        </property>
-       </widget>
+       <layout class="QHBoxLayout" name="horizontalLayout_4">
+        <item>
+         <widget class="QCheckBox" name="enableSDLEnhancedMode">
+          <property name="text">
+           <string>DualShock 4 / DualSense Enhanced Mode</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QToolButton" name="ledSettings">
+          <property name="toolTip">
+           <string>Controller LED Settings</string>
+          </property>
+          <property name="icon">
+           <iconset theme="lightbulb-line">
+            <normaloff>.</normaloff>.
+           </iconset>
+          </property>
+         </widget>
+        </item>
+       </layout>
       </item>
      </layout>
     </widget>
diff --git a/src/duckstation-qt/controllerledsettingsdialog.ui b/src/duckstation-qt/controllerledsettingsdialog.ui
new file mode 100644
index 000000000..87f9aa9c2
--- /dev/null
+++ b/src/duckstation-qt/controllerledsettingsdialog.ui
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ControllerLEDSettingsDialog</class>
+ <widget class="QDialog" name="ControllerLEDSettingsDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>501</width>
+    <height>108</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Controller LED Settings</string>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <item row="0" column="0">
+    <widget class="QGroupBox" name="groupBox">
+     <property name="title">
+      <string>SDL-0 LED</string>
+     </property>
+     <layout class="QVBoxLayout" name="verticalLayout_4">
+      <item>
+       <widget class="ColorPickerButton" name="SDL0LED"/>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item row="0" column="1">
+    <widget class="QGroupBox" name="groupBox_2">
+     <property name="title">
+      <string>SDL-1 LED</string>
+     </property>
+     <layout class="QVBoxLayout" name="verticalLayout_3">
+      <item>
+       <widget class="ColorPickerButton" name="SDL1LED"/>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item row="0" column="2">
+    <widget class="QGroupBox" name="groupBox_3">
+     <property name="title">
+      <string>SDL-2 LED</string>
+     </property>
+     <layout class="QVBoxLayout" name="verticalLayout_2">
+      <item>
+       <widget class="ColorPickerButton" name="SDL2LED"/>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item row="0" column="3">
+    <widget class="QGroupBox" name="groupBox_4">
+     <property name="title">
+      <string>SDL-3 LED</string>
+     </property>
+     <layout class="QVBoxLayout" name="verticalLayout">
+      <item>
+       <widget class="ColorPickerButton" name="SDL3LED"/>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item row="1" column="0" colspan="4">
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Close</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>ColorPickerButton</class>
+   <extends>QPushButton</extends>
+   <header>colorpickerbutton.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/duckstation-qt/duckstation-qt.vcxproj b/src/duckstation-qt/duckstation-qt.vcxproj
index 465ab924e..f01010de3 100644
--- a/src/duckstation-qt/duckstation-qt.vcxproj
+++ b/src/duckstation-qt/duckstation-qt.vcxproj
@@ -12,6 +12,7 @@
     <ClCompile Include="cheatmanagerdialog.cpp" />
     <ClCompile Include="cheatcodeeditordialog.cpp" />
     <ClCompile Include="collapsiblewidget.cpp" />
+    <ClCompile Include="colorpickerbutton.cpp" />
     <ClCompile Include="consolesettingswidget.cpp" />
     <ClCompile Include="controllerbindingwidgets.cpp" />
     <ClCompile Include="controllerglobalsettingswidget.cpp" />
@@ -79,6 +80,7 @@
     <QtMoc Include="controllerbindingwidgets.h" />
     <QtMoc Include="controllerglobalsettingswidget.h" />
     <QtMoc Include="controllersettingsdialog.h" />
+    <QtMoc Include="colorpickerbutton.h" />
     <ClInclude Include="controllersettingwidgetbinder.h" />
     <QtMoc Include="memoryviewwidget.h" />
     <ClInclude Include="resource.h" />
@@ -232,6 +234,7 @@
     <ClCompile Include="$(IntDir)moc_cheatmanagerdialog.cpp" />
     <ClCompile Include="$(IntDir)moc_cheatcodeeditordialog.cpp" />
     <ClCompile Include="$(IntDir)moc_collapsiblewidget.cpp" />
+    <ClCompile Include="$(IntDir)moc_colorpickerbutton.cpp" />
     <ClCompile Include="$(IntDir)moc_consolesettingswidget.cpp" />
     <ClCompile Include="$(IntDir)moc_controllerbindingwidgets.cpp" />
     <ClCompile Include="$(IntDir)moc_controllerglobalsettingswidget.cpp" />
@@ -319,6 +322,9 @@
     </QtTs>
   </ItemGroup>
   <ItemGroup>
+    <QtUi Include="controllerledsettingsdialog.ui">
+      <FileType>Document</FileType>
+    </QtUi>
     <None Include="translations\duckstation-qt_es-es.ts" />
     <None Include="translations\duckstation-qt_tr.ts" />
   </ItemGroup>
diff --git a/src/duckstation-qt/duckstation-qt.vcxproj.filters b/src/duckstation-qt/duckstation-qt.vcxproj.filters
index a484f2ef8..e98ec568c 100644
--- a/src/duckstation-qt/duckstation-qt.vcxproj.filters
+++ b/src/duckstation-qt/duckstation-qt.vcxproj.filters
@@ -91,6 +91,9 @@
     <ClCompile Include="$(IntDir)moc_foldersettingswidget.cpp" />
     <ClCompile Include="$(IntDir)moc_gamesummarywidget.cpp" />
     <ClCompile Include="qttranslations.cpp" />
+    <ClCompile Include="coverdownloaddialog.cpp" />
+    <ClCompile Include="$(IntDir)moc_coverdownloaddialog.cpp" />
+    <ClCompile Include="colorpickerbutton.cpp" />
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="qtutils.h" />
@@ -150,6 +153,8 @@
     <QtMoc Include="gamelistrefreshthread.h" />
     <QtMoc Include="gamesummarywidget.h" />
     <QtMoc Include="foldersettingswidget.h" />
+    <QtMoc Include="coverdownloaddialog.h" />
+    <QtMoc Include="colorpickerbutton.h" />
   </ItemGroup>
   <ItemGroup>
     <QtUi Include="consolesettingswidget.ui" />
@@ -189,6 +194,8 @@
     <QtUi Include="controllermacrowidget.ui" />
     <QtUi Include="controllermacroeditwidget.ui" />
     <QtUi Include="controllerbindingwidget_mouse.ui" />
+    <QtUi Include="coverdownloaddialog.ui" />
+    <QtUi Include="controllerledsettingsdialog.ui" />
   </ItemGroup>
   <ItemGroup>
     <Natvis Include="qt5.natvis" />
diff --git a/src/duckstation-qt/resources/icons/black/svg/lightbulb-line.svg b/src/duckstation-qt/resources/icons/black/svg/lightbulb-line.svg
new file mode 100644
index 000000000..9370abc1a
--- /dev/null
+++ b/src/duckstation-qt/resources/icons/black/svg/lightbulb-line.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M9.973 18H11v-5h2v5h1.027c.132-1.202.745-2.194 1.74-3.277.113-.122.832-.867.917-.973a6 6 0 1 0-9.37-.002c.086.107.807.853.918.974.996 1.084 1.609 2.076 1.741 3.278zM10 20v1h4v-1h-4zm-4.246-5a8 8 0 1 1 12.49.002C17.624 15.774 16 17 16 18.5V21a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-2.5C8 17 6.375 15.774 5.754 15z" fill="#000000"/></svg>
\ No newline at end of file
diff --git a/src/duckstation-qt/resources/icons/white/svg/lightbulb-line.svg b/src/duckstation-qt/resources/icons/white/svg/lightbulb-line.svg
new file mode 100644
index 000000000..454dc0fe7
--- /dev/null
+++ b/src/duckstation-qt/resources/icons/white/svg/lightbulb-line.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M9.973 18H11v-5h2v5h1.027c.132-1.202.745-2.194 1.74-3.277.113-.122.832-.867.917-.973a6 6 0 1 0-9.37-.002c.086.107.807.853.918.974.996 1.084 1.609 2.076 1.741 3.278zM10 20v1h4v-1h-4zm-4.246-5a8 8 0 1 1 12.49.002C17.624 15.774 16 17 16 18.5V21a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-2.5C8 17 6.375 15.774 5.754 15z" fill="#ffffff"/></svg>
\ No newline at end of file
diff --git a/src/duckstation-qt/resources/resources.qrc b/src/duckstation-qt/resources/resources.qrc
index 7fad1efe8..5c833fe9b 100644
--- a/src/duckstation-qt/resources/resources.qrc
+++ b/src/duckstation-qt/resources/resources.qrc
@@ -181,6 +181,7 @@
 		<file>icons/black/svg/image-fill.svg</file>
 		<file>icons/black/svg/keyboard-line.svg</file>
 		<file>icons/black/svg/layout-grid-line.svg</file>
+		<file>icons/black/svg/lightbulb-line.svg</file>
 		<file>icons/black/svg/list-check.svg</file>
 		<file>icons/black/svg/mouse-line.svg</file>
 		<file>icons/black/svg/paint-brush-line.svg</file>
@@ -512,6 +513,7 @@
 		<file>icons/white/svg/image-fill.svg</file>
 		<file>icons/white/svg/keyboard-line.svg</file>
 		<file>icons/white/svg/layout-grid-line.svg</file>
+		<file>icons/white/svg/lightbulb-line.svg</file>
 		<file>icons/white/svg/list-check.svg</file>
 		<file>icons/white/svg/mouse-line.svg</file>
 		<file>icons/white/svg/paint-brush-line.svg</file>
diff --git a/src/frontend-common/CMakeLists.txt b/src/frontend-common/CMakeLists.txt
index 2dd5d869e..6787bf87c 100644
--- a/src/frontend-common/CMakeLists.txt
+++ b/src/frontend-common/CMakeLists.txt
@@ -101,8 +101,8 @@ if(SDL2_FOUND)
     sdl_input_source.h
   )
   target_compile_definitions(frontend-common PUBLIC "WITH_SDL2=1")
-  target_include_directories(frontend-common PRIVATE ${SDL2_INCLUDE_DIRS})
-  target_link_libraries(frontend-common PRIVATE ${SDL2_LIBRARIES})
+  target_include_directories(frontend-common PUBLIC ${SDL2_INCLUDE_DIRS})
+  target_link_libraries(frontend-common PUBLIC ${SDL2_LIBRARIES})
 
   # Copy bundled SDL2 to output on Windows.
   if(WIN32)
diff --git a/src/frontend-common/sdl_input_source.cpp b/src/frontend-common/sdl_input_source.cpp
index 4ce09d84d..d8d7eeb0f 100644
--- a/src/frontend-common/sdl_input_source.cpp
+++ b/src/frontend-common/sdl_input_source.cpp
@@ -88,6 +88,18 @@ static constexpr const char* s_sdl_hat_direction_names[] = {
   // clang-format on
 };
 
+static constexpr const char* s_sdl_default_led_colors[] = {
+  "0000ff", // SDL-0
+  "ff0000", // SDL-1
+  "00ff00", // SDL-2
+  "ffff00", // SDL-3
+};
+
+static void SetControllerRGBLED(SDL_GameController* gc, u32 color)
+{
+  SDL_GameControllerSetLED(gc, (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff);
+}
+
 SDLInputSource::SDLInputSource() = default;
 
 SDLInputSource::~SDLInputSource()
@@ -149,6 +161,39 @@ void SDLInputSource::LoadSettings(SettingsInterface& si)
 {
   m_controller_enhanced_mode = si.GetBoolValue("InputSources", "SDLControllerEnhancedMode", false);
   m_sdl_hints = si.GetKeyValueList("SDLHints");
+
+  for (u32 i = 0; i < MAX_LED_COLORS; i++)
+  {
+    const u32 color = GetRGBForPlayerId(si, i);
+    if (m_led_colors[i] == color)
+      continue;
+
+    m_led_colors[i] = color;
+
+    const auto it = GetControllerDataForPlayerId(i);
+    if (it == m_controllers.end() || !it->game_controller || !SDL_GameControllerHasLED(it->game_controller))
+      continue;
+
+    SetControllerRGBLED(it->game_controller, color);
+  }
+}
+
+u32 SDLInputSource::GetRGBForPlayerId(SettingsInterface& si, u32 player_id)
+{
+  return ParseRGBForPlayerId(
+    si.GetStringValue("SDLExtra", fmt::format("Player{}LED", player_id).c_str(), s_sdl_default_led_colors[player_id]),
+    player_id);
+}
+
+u32 SDLInputSource::ParseRGBForPlayerId(const std::string_view& str, u32 player_id)
+{
+  if (player_id >= MAX_LED_COLORS)
+    return 0;
+
+  const u32 default_color = StringUtil::FromChars<u32>(s_sdl_default_led_colors[player_id], 16).value_or(0);
+  const u32 color = StringUtil::FromChars<u32>(str, 16).value_or(default_color);
+
+  return color;
 }
 
 void SDLInputSource::SetHints()
@@ -624,6 +669,12 @@ bool SDLInputSource::OpenDevice(int index, bool is_gamecontroller)
   if (!cd.haptic && !cd.use_game_controller_rumble)
     Log_VerbosePrintf("(SDLInputSource) Rumble is not supported on '%s'", name);
 
+  if (player_id >= 0 && static_cast<u32>(player_id) < MAX_LED_COLORS && gcontroller &&
+      SDL_GameControllerHasLED(gcontroller))
+  {
+    SetControllerRGBLED(gcontroller, m_led_colors[player_id]);
+  }
+
   m_controllers.push_back(std::move(cd));
 
   InputManager::OnInputDeviceConnected(StringUtil::StdStringFromFormat("SDL-%d", player_id), name);
diff --git a/src/frontend-common/sdl_input_source.h b/src/frontend-common/sdl_input_source.h
index 5daf1d341..5cd1db13f 100644
--- a/src/frontend-common/sdl_input_source.h
+++ b/src/frontend-common/sdl_input_source.h
@@ -14,6 +14,8 @@ class SettingsInterface;
 class SDLInputSource final : public InputSource
 {
 public:
+  static constexpr u32 MAX_LED_COLORS = 4;
+
   SDLInputSource();
   ~SDLInputSource();
 
@@ -38,6 +40,9 @@ public:
 
   SDL_Joystick* GetJoystickForDevice(const std::string_view& device);
 
+  static u32 GetRGBForPlayerId(SettingsInterface& si, u32 player_id);
+  static u32 ParseRGBForPlayerId(const std::string_view& str, u32 player_id);
+
 private:
   struct ControllerData
   {
@@ -82,5 +87,6 @@ private:
 
   bool m_sdl_subsystem_initialized = false;
   bool m_controller_enhanced_mode = false;
+  std::array<u32, MAX_LED_COLORS> m_led_colors{};
   std::vector<std::pair<std::string, std::string>> m_sdl_hints;
 };