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
+1 -1
+30 -1
View File
@@ -65,13 +65,35 @@ toast heading {
color: #92875B;
}
toast message {
toast heading > span {
flex: 1 0 auto;
}
toast heading > row {
flex: 1 0 auto;
display: flex;
align-items: center;
gap: 4dp;
}
toast message {
display: flex;
flex-flow: column;
align-items: start;
justify-content: start;
gap: 8dp;
}
toast message row {
display: flex;
align-items: start;
justify-content: start;
}
toast message row.muted {
opacity: 0.5;
}
toast progress {
height: 4dp;
position: absolute;
@@ -113,6 +135,13 @@ icon.trophy {
decorator: text("" center center);
}
icon.controller {
width: 24dp;
height: 24dp;
font-size: 24dp;
decorator: text("" center center);
}
logo {
position: absolute;
width: 100dp;
+1 -1
View File
@@ -12,7 +12,7 @@
#include <algorithm>
#include <array>
namespace dusk::ui {
namespace dusk::ui::input {
namespace {
constexpr double kGamepadRepeatInitialDelay = 0.32;
+4 -1
View File
@@ -1,7 +1,10 @@
#pragma once
namespace dusk::ui {
union SDL_Event;
namespace dusk::ui::input {
void handle_event(const SDL_Event& event) noexcept;
void update_input() noexcept;
void reset_input_state() noexcept;
void sync_input_block() noexcept;
+15 -4
View File
@@ -38,18 +38,29 @@ Rml::Element* create_toast(Rml::Element* parent, const Toast& toast) {
}
{
auto* heading = append(elem, "heading");
auto* span = append(heading, "span");
span->SetInnerRML(toast.title);
if (toast.title.starts_with("<")) {
heading->SetInnerRML(toast.title);
} else {
auto* span = append(heading, "span");
span->SetInnerRML(toast.title);
}
if (toast.type == "achievement") {
auto* icon = append(heading, "icon");
icon->SetClass("trophy", true);
mDoAud_seStartMenu(kSoundAchievementUnlock);
} else if (toast.type == "controller") {
auto* icon = append(heading, "icon");
icon->SetClass("controller", true);
}
}
{
auto* message = append(elem, "message");
auto* span = append(message, "span");
span->SetInnerRML(toast.content);
if (toast.content.starts_with("<")) {
message->SetInnerRML(toast.content);
} else {
auto* span = append(message, "span");
span->SetInnerRML(toast.content);
}
}
{
auto* progress = append(elem, "progress");
+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 {
+3
View File
@@ -85,4 +85,7 @@ Insets safe_area_insets(Rml::Context* context) noexcept;
void push_toast(Toast toast) noexcept;
std::deque<Toast>& get_toasts() noexcept;
const char* battery_icon(SDL_PowerState state, int level) noexcept;
const char* connection_state_icon(SDL_JoystickConnectionState state) noexcept;
} // namespace dusk::ui