UI: Controller connect/disconnect toasts

This commit is contained in:
Luke Street
2026-05-06 11:14:12 -06:00
parent cfe1f2304b
commit 4e23472ed5
7 changed files with 166 additions and 14 deletions
+112 -6
View File
@@ -2,7 +2,9 @@
#include <RmlUi/Core.h>
#include <SDL3/SDL_filesystem.h>
#include <absl/container/flat_hash_set.h>
#include <aurora/rmlui.hpp>
#include <fmt/format.h>
#include <algorithm>
#include <filesystem>
@@ -26,6 +28,12 @@ std::vector<std::unique_ptr<Document> > sDocumentStack;
std::vector<std::unique_ptr<Document> > sPassiveDocuments;
std::deque<Toast> sToasts;
// Sometimes gamepads can connect and disconnect quickly, especially during
// connection negotiation. In this case, we'll receive an _ADDED event for a
// disconnected gamepad. Storing IDs here lets use only show disconnected
// notifications for gamepads that we sent a connected notification for.
absl::flat_hash_set<SDL_JoystickID> sConnectedGamepads;
} // namespace
bool initialize() noexcept {
@@ -51,11 +59,109 @@ bool initialize() noexcept {
void shutdown() noexcept {
sDocumentStack.clear();
sPassiveDocuments.clear();
reset_input_state();
release_input_block();
sConnectedGamepads.clear();
input::reset_input_state();
input::release_input_block();
sInitialized = false;
}
const char* battery_icon(SDL_PowerState state, int level) noexcept {
if (state == SDL_POWERSTATE_UNKNOWN || state == SDL_POWERSTATE_NO_BATTERY) {
return "e1a6"; // Battery Unknown
}
if (state == SDL_POWERSTATE_ERROR) {
return "f7ea"; // Battery Error
}
if (state == SDL_POWERSTATE_CHARGED || level == 100) {
return "e1a4"; // Battery Full
}
if (state == SDL_POWERSTATE_CHARGING) {
if (level >= 90)
return "f0a7"; // Battery Charging 90
if (level >= 80)
return "f0a6"; // Battery Charging 80
if (level >= 60)
return "f0a5"; // Battery Charging 60
if (level >= 50)
return "f0a4"; // Battery Charging 50
if (level >= 30)
return "f0a3"; // Battery Charging 30
if (level >= 20)
return "f0a2"; // Battery Charging 20
return "e1a3"; // Battery Charging Full (we use it as empty)
}
if (level >= 85)
return "ebd2"; // Battery 6 Bar
if (level >= 70)
return "ebd4"; // Battery 5 Bar
if (level >= 55)
return "ebe2"; // Battery 4 Bar
if (level >= 40)
return "ebdd"; // Battery 3 Bar
if (level >= 25)
return "ebe0"; // Battery 2 Bar
if (level >= 10)
return "ebd9"; // Battery 1 Bar
return "e19c"; // Battery Alert
}
const char* connection_state_icon(SDL_JoystickConnectionState state) noexcept {
switch (state) {
case SDL_JOYSTICK_CONNECTION_WIRELESS:
return "e1a7";
case SDL_JOYSTICK_CONNECTION_WIRED:
return "e1e0";
default:
return nullptr;
}
}
void handle_event(const SDL_Event& event) noexcept {
if (event.type == SDL_EVENT_GAMEPAD_ADDED) {
auto* gamepad = SDL_GetGamepadFromID(event.gdevice.which);
if (SDL_GamepadConnected(gamepad)) {
const char* name = SDL_GetGamepadName(gamepad);
Rml::String content = fmt::format("<span>{}</span>", name ? name : "[Unknown]");
Rml::String title = "Controller connected";
if (const char* icon = connection_state_icon(SDL_GetGamepadConnectionState(gamepad))) {
title = fmt::format(
"<row><span>{}</span> <icon class=\"connection\">&#x{};</icon></row>", title,
icon);
}
int batteryLevel = -1;
const auto powerState = SDL_GetGamepadPowerInfo(gamepad, &batteryLevel);
if (powerState != SDL_POWERSTATE_UNKNOWN) {
content = fmt::format(
"<row>{}</row><row class=\"muted\"><icon class=\"battery\">&#x{};</icon>",
content, battery_icon(powerState, batteryLevel));
if (batteryLevel > -1) {
content = fmt::format("{}&nbsp;<span>{}%</span>", content, batteryLevel);
}
content += "</row>";
}
push_toast({
.type = "controller",
.title = title,
.content = content,
.duration = std::chrono::seconds(4),
});
sConnectedGamepads.insert(event.gdevice.which);
}
} else if (event.type == SDL_EVENT_GAMEPAD_REMOVED &&
sConnectedGamepads.contains(event.gdevice.which))
{
const char* name = SDL_GetGamepadNameForID(event.gdevice.which);
push_toast({
.type = "controller",
.title = "Controller disconnected",
.content = name ? name : "[Unknown]",
.duration = std::chrono::seconds(4),
});
sConnectedGamepads.erase(event.gdevice.which);
}
input::handle_event(event);
}
Document& push_document(std::unique_ptr<Document> doc, bool show, bool passive) noexcept {
Document& ret = *doc;
if (passive) {
@@ -66,7 +172,7 @@ Document& push_document(std::unique_ptr<Document> doc, bool show, bool passive)
if (show) {
ret.show();
}
sync_input_block();
input::sync_input_block();
return ret;
}
@@ -74,7 +180,7 @@ void show_top_document() noexcept {
if (auto* doc = top_document()) {
doc->show();
}
sync_input_block();
input::sync_input_block();
}
bool any_document_visible() noexcept {
@@ -99,7 +205,7 @@ Document* top_document() noexcept {
}
void update() noexcept {
update_input();
input::update_input();
for (const auto& doc : sDocumentStack) {
doc->update();
}
@@ -131,7 +237,7 @@ void update() noexcept {
}
}
sync_input_block();
input::sync_input_block();
}
std::filesystem::path resource_path(const std::filesystem::path& filename) noexcept {