mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-05-27 15:42:42 -04:00
UI: Controller connect/disconnect toasts
This commit is contained in:
Vendored
+1
-1
Submodule extern/aurora updated: 4cd8d2f009...b78bbf3f58
+30
-1
@@ -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;
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace dusk::ui::input {
|
||||
namespace {
|
||||
|
||||
constexpr double kGamepadRepeatInitialDelay = 0.32;
|
||||
|
||||
@@ -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
@@ -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
@@ -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("{} <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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user