From cfcba5e9e7a9078ccfe043889d26b13492f91ba4 Mon Sep 17 00:00:00 2001
From: nastys <>
Date: Wed, 8 Apr 2020 17:03:37 +0200
Subject: [PATCH] Update libtesla; Hide PV_MARK
---
Makefile | 1 +
README.md | 5 +-
libs/libtesla/.vscode/c_cpp_properties.json | 4 +-
libs/libtesla/.vscode/settings.json | 4 +-
libs/libtesla/README.md | 6 +-
libs/libtesla/example/Makefile | 4 +-
libs/libtesla/example/source/main.cpp | 62 +-
libs/libtesla/include/tesla.hpp | 2004 +++++++++++++++----
source/constants.h | 4 +
source/main.cpp | 26 +-
10 files changed, 1679 insertions(+), 441 deletions(-)
diff --git a/Makefile b/Makefile
index 4d3fc9d..8dc2ff8 100644
--- a/Makefile
+++ b/Makefile
@@ -38,6 +38,7 @@ include $(DEVKITPRO)/libnx/switch_rules
# NACP building is skipped as well.
#---------------------------------------------------------------------------------
APP_TITLE := MEGAHAKUS
+APP_VERSION := 2.4.0
TARGET := $(notdir $(CURDIR))
BUILD := build
diff --git a/README.md b/README.md
index 2078ef2..db491df 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# MEGAHAKUS 2.2.0
+# MEGAHAKUS 2.4.0
A Tesla overlay for 初音ミク Project DIVA MEGA39's.
![screenshot](https://github.com/nastys/MEGAHAKUS/raw/master/preview.jpg)
@@ -48,7 +48,8 @@ R: +1.0%
Thanks to SwigS-27 for the resolution scale address.
## Debug mode
-Currently works in handheld mode only unless ``D-pad cursor`` is enabled, and button releases are a bit buggy.
+NOTE: Not compatible with DLC pack 2nd+.
+Currently, button releases are a bit buggy.
```
Joystick (default)/D-pad: move cursor
L/R/Joystick: hold/push to move faster
diff --git a/libs/libtesla/.vscode/c_cpp_properties.json b/libs/libtesla/.vscode/c_cpp_properties.json
index 49ee1d2..5c18c1b 100644
--- a/libs/libtesla/.vscode/c_cpp_properties.json
+++ b/libs/libtesla/.vscode/c_cpp_properties.json
@@ -4,7 +4,7 @@
"name": "DKP Aarch64 Windows",
"includePath": [
"C:/devkitPro/devkitA64/aarch64-none-elf/include/**",
- "C:/devkitPro/devkitA64/lib/gcc/aarch64-none-elf/8.3.0/include/**",
+ "C:/devkitPro/devkitA64/lib/gcc/aarch64-none-elf/9.2.0/include/**",
"C:/devkitPro/libnx/include/**",
"C:/devkitPro/portlibs/switch/include/**",
"${workspaceFolder}/include/**"
@@ -24,7 +24,7 @@
"name": "DKP Aarch64 Linux",
"includePath": [
"/opt/devkitpro/devkitA64/aarch64-none-elf/include/**",
- "/opt/devkitpro/devkitA64/lib/gcc/aarch64-none-elf/8.3.0/include/**",
+ "/opt/devkitpro/devkitA64/lib/gcc/aarch64-none-elf/9.2.0/include/**",
"/opt/devkitpro/libnx/include/**",
"/opt/devkitpro/portlibs/switch/include/**",
"${workspaceFolder}/include/**"
diff --git a/libs/libtesla/.vscode/settings.json b/libs/libtesla/.vscode/settings.json
index 2da0be0..7e8dd02 100644
--- a/libs/libtesla/.vscode/settings.json
+++ b/libs/libtesla/.vscode/settings.json
@@ -51,6 +51,8 @@
"thread": "cpp",
"cinttypes": "cpp",
"typeinfo": "cpp",
- "map": "cpp"
+ "map": "cpp",
+ "stack": "cpp",
+ "list": "cpp"
}
}
\ No newline at end of file
diff --git a/libs/libtesla/README.md b/libs/libtesla/README.md
index ca70674..5782d16 100644
--- a/libs/libtesla/README.md
+++ b/libs/libtesla/README.md
@@ -10,11 +10,11 @@ It's main goal is to make sure all overlays look and feel similar and don't diff
## Screenshots
-`Or rather screen pictures. Overlays do NOT show up on Screenshots.`
+`Overlays do NOT show up on Screenshots. These pictures were taken using a capture card`
## Example
diff --git a/libs/libtesla/example/Makefile b/libs/libtesla/example/Makefile
index edb1e40..7f5b880 100644
--- a/libs/libtesla/example/Makefile
+++ b/libs/libtesla/example/Makefile
@@ -37,7 +37,9 @@ include $(DEVKITPRO)/libnx/switch_rules
# of a homebrew executable (.nro). This is intended to be used for sysmodules.
# NACP building is skipped as well.
#---------------------------------------------------------------------------------
-APP_TITLE := Tesla Overlay Template
+
+APP_TITLE := Tesla Example
+VERSION_VERSION := 1.3.0
TARGET := $(notdir $(CURDIR))
BUILD := build
diff --git a/libs/libtesla/example/source/main.cpp b/libs/libtesla/example/source/main.cpp
index b7978ef..ca9aff3 100644
--- a/libs/libtesla/example/source/main.cpp
+++ b/libs/libtesla/example/source/main.cpp
@@ -1,22 +1,72 @@
#define TESLA_INIT_IMPL // If you have more than one file using the tesla header, only define this in the main one
#include // The Tesla Header
+
+class GuiSecondary : public tsl::Gui {
+public:
+ GuiSecondary() {}
+
+ virtual tsl::elm::Element* createUI() override {
+ auto *rootFrame = new tsl::elm::OverlayFrame("Tesla Example", "v1.3.1 - Secondary Gui");
+
+ rootFrame->setContent(new tsl::elm::DebugRectangle(tsl::gfx::Color{ 0x8, 0x3, 0x8, 0xF }));
+
+ return rootFrame;
+ }
+};
+
class GuiTest : public tsl::Gui {
public:
GuiTest(u8 arg1, u8 arg2, bool arg3) { }
// Called when this Gui gets loaded to create the UI
- // Allocate all your elements on the heap. libtesla will make sure to clean them up when not needed anymore
+ // Allocate all elements on the heap. libtesla will make sure to clean them up when not needed anymore
virtual tsl::elm::Element* createUI() override {
- auto rootFrame = new tsl::elm::OverlayFrame("Tesla Template", "v1.1.0");
+ // A OverlayFrame is the base element every overlay consists of. This will draw the default Title and Subtitle.
+ // If you need more information in the header or want to change it's look, use a HeaderOverlayFrame.
+ auto frame = new tsl::elm::OverlayFrame("Tesla Example", "v1.3.0");
+
+ // A list that can contain sub elements and handles scrolling
auto list = new tsl::elm::List();
- list->addItem(new tsl::elm::ListItem("Test List Item"));
- list->addItem(new tsl::elm::ToggleListItem("Test Toggle List Item", true));
+ // List Items
+ list->addItem(new tsl::elm::CategoryHeader("List items"));
- rootFrame->setContent(list);
+ auto *clickableListItem = new tsl::elm::ListItem("Clickable List Item", "...");
+ clickableListItem->setClickListener([](u64 keys) {
+ if (keys & KEY_A) {
+ tsl::changeTo();
+ return true;
+ }
- return rootFrame;
+ return false;
+ });
+
+ list->addItem(clickableListItem);
+ list->addItem(new tsl::elm::ListItem("Default List Item"));
+ list->addItem(new tsl::elm::ToggleListItem("Toggle List Item", true));
+
+ // Custom Drawer, a element that gives direct access to the renderer
+ list->addItem(new tsl::elm::CategoryHeader("Custom Drawer", true));
+ list->addItem(new tsl::elm::CustomDrawer([](tsl::gfx::Renderer *renderer, s32 x, s32 y, s32 w, s32 h) {
+ renderer->drawCircle(x + 40, y + 40, 20, true, renderer->a(0xF00F));
+ renderer->drawCircle(x + 50, y + 50, 20, true, renderer->a(0xF0F0));
+ renderer->drawRect(x + 130, y + 30, 60, 40, renderer->a(0xFF00));
+ renderer->drawString("Hello :)", false, x + 250, y + 70, 20, renderer->a(0xFF0F));
+ renderer->drawRect(x + 40, y + 90, 300, 10, renderer->a(0xF0FF));
+ }), 100);
+
+ // Track bars
+ list->addItem(new tsl::elm::CategoryHeader("Track bars"));
+ list->addItem(new tsl::elm::TrackBar("\u2600"));
+ list->addItem(new tsl::elm::StepTrackBar("\uE13C", 20));
+ list->addItem(new tsl::elm::NamedStepTrackBar("\uE132", { "Selection 1", "Selection 2", "Selection 3" }));
+
+ // Add the list to the frame for it to be drawn
+ frame->setContent(list);
+
+ // Return the frame to have it become the top level element of this Gui
+ return frame;
}
// Called once every frame to update values
diff --git a/libs/libtesla/include/tesla.hpp b/libs/libtesla/include/tesla.hpp
index b760796..0c85d47 100644
--- a/libs/libtesla/include/tesla.hpp
+++ b/libs/libtesla/include/tesla.hpp
@@ -53,10 +53,19 @@
#pragma GCC diagnostic pop
+#define ELEMENT_BOUNDS(elem) elem->getX(), elem->getY(), elem->getWidth(), elem->getHeight()
+
+#define ELEMENT_TOP_BOUND(elem) (elem->getY())
+#define ELEMENT_LEFT_BOUND(elem) (elem->getX())
+#define ELEMENT_RIGHT_BOUND(elem) (elem->getX() + elem->getWidth())
+#define ELEMENT_BOTTOM_BOUND(elem) (elem->getY() + elem->getHeight())
#define ASSERT_EXIT(x) if (R_FAILED(x)) std::exit(1)
#define ASSERT_FATAL(x) if (Result res = x; R_FAILED(res)) fatalThrow(res)
+#define PACKED __attribute__((packed))
+#define ALWAYS_INLINE __attribute__((always_inline))
+
/// Evaluates an expression that returns a result, and returns the result if it would fail.
#define R_TRY(resultExpr) \
({ \
@@ -66,6 +75,7 @@
} \
})
+using namespace std::literals::string_literals;
using namespace std::literals::chrono_literals;
namespace tsl {
@@ -83,14 +93,26 @@ namespace tsl {
extern u16 LayerPosY; ///< Y position of the Tesla layer
extern u16 FramebufferWidth; ///< Width of the framebuffer
extern u16 FramebufferHeight; ///< Height of the framebuffer
+ extern u64 launchCombo; ///< Overlay activation key combo
}
namespace style {
- constexpr u32 ListItemDefaultHeight = 72; ///< Height of a standard ListItem
+ constexpr u32 ListItemDefaultHeight = 70; ///< Standard list item height
+ constexpr u32 TrackBarDefaultHeight = 90; ///< Standard track bar height
+ constexpr u8 ListItemHighlightSaturation = 0x6; ///< Maximum saturation of Listitem highlights
+ constexpr u8 ListItemHighlightLength = 22; ///< Maximum length of Listitem highlights
namespace color {
- constexpr u16 ColorTransparent = 0x0000; ///< Transparent color
+ constexpr u16 ColorFrameBackground = 0xD000; ///< Overlay frame background color
+ constexpr u16 ColorTransparent = 0x0000; ///< Transparent color
+ constexpr u16 ColorHighlight = 0xFDF0; ///< Greenish highlight color
+ constexpr u16 ColorFrame = 0xF777; ///< Outer boarder color
+ constexpr u16 ColorHandle = 0xF555; ///< Track bar handle color
+ constexpr u16 ColorText = 0xFFFF; ///< Standard text color
+ constexpr u16 ColorDescription = 0xFAAA; ///< Description text color
+ constexpr u16 ColorHeaderBar = 0xFCCC; ///< Category header rectangle color
+ constexpr u16 ColorClickAnimation = 0xF220;
}
}
@@ -107,8 +129,19 @@ namespace tsl {
Left, ///< Focus moved from left to rigth
Right ///< Focus moved from right to left
};
+
+ /**
+ * @brief Current input controll mode
+ *
+ */
+ enum class InputMode {
+ Controller, ///< Input from controller
+ Touch, ///< Touch input
+ TouchScroll ///< Moving/scrolling touch input
+ };
class Overlay;
+ namespace elm { class Element; }
namespace impl {
@@ -124,11 +157,35 @@ namespace tsl {
return static_cast(u8(lhs) | u8(rhs));
}
+ /**
+ * @brief Combo key mapping
+ */
+ struct KeyInfo {
+ u64 key;
+ const char* name;
+ const char* glyph;
+ };
+
+ /**
+ * @brief Combo key mappings
+ *
+ * Ordered as they should be displayed
+ */
+ static const std::list KEYS_INFO = {
+ { KEY_L, "L", "\uE0A4" }, { KEY_R, "R", "\uE0A5" },
+ { KEY_ZL, "ZL", "\uE0A6" }, { KEY_ZR, "ZR", "\uE0A7" },
+ { KEY_SL, "SL", "\uE0A8" }, { KEY_SR, "SR", "\uE0A9" },
+ { KEY_DLEFT, "DLEFT", "\uE07B" }, { KEY_DUP, "DUP", "\uE079" }, { KEY_DRIGHT, "DRIGHT", "\uE07C" }, { KEY_DDOWN, "DDOWN", "\uE07A" },
+ { KEY_A, "A", "\uE0A0" }, { KEY_B, "B", "\uE0A1" }, { KEY_X, "X", "\uE0A2" }, { KEY_Y, "Y", "\uE0A3" },
+ { KEY_LSTICK, "LS", "\uE08A" }, { KEY_RSTICK, "RS", "\uE08B" },
+ { KEY_MINUS, "MINUS", "\uE0B6" }, { KEY_PLUS, "PLUS", "\uE0B5" }
+ };
+
}
[[maybe_unused]] static void goBack();
- [[maybe_unused]] static void setNextOverlay(std::string ovlPath, std::string args = "");
+ [[maybe_unused]] static void setNextOverlay(const std::string& ovlPath, std::string args = "");
template
int loop(int argc, char** argv);
@@ -148,6 +205,18 @@ namespace tsl {
smExit();
}
+ /**
+ * @brief Wrapper for sd card access using stdio
+ * @note Consider using raw fs calls instead as they are faster and need less space
+ *
+ * @param f wrapped function
+ */
+ static inline void doWithSDCardHandle(std::function f) {
+ fsdevMountSdmc();
+ f();
+ fsdevUnmountDevice("sdmc");
+ }
+
/**
* @brief Guard that will execute a passed function at the end of the current scope
*
@@ -159,8 +228,8 @@ namespace tsl {
private:
std::function f;
public:
- __attribute__((always_inline)) ScopeGuard(std::function f) : f(std::move(f)) { }
- __attribute__((always_inline)) ~ScopeGuard() { if (f) { f(); } }
+ ALWAYS_INLINE ScopeGuard(std::function f) : f(std::move(f)) { }
+ ALWAYS_INLINE ~ScopeGuard() { if (f) { f(); } }
void dismiss() { f = nullptr; }
};
@@ -188,7 +257,7 @@ namespace tsl {
static void requestForeground(bool enabled) {
u64 applicationAruid = 0, appletAruid = 0;
- for (u64 programId = 0x0100000000001000ul; programId < 0x0100000000001020ul; programId++) {
+ for (u64 programId = 0x0100000000001000UL; programId < 0x0100000000001020UL; programId++) {
pmdmntGetProcessId(&appletAruid, programId);
if (appletAruid != 0)
@@ -223,6 +292,21 @@ namespace tsl {
return out;
}
+ /**
+ * @brief Limit a strings length and end it with "…"
+ *
+ * @param string String to truncate
+ * @param maxLength Maximum length of string
+ */
+ static std::string limitStringLength(std::string string, size_t maxLength) {
+ if (string.length() <= maxLength)
+ return string;
+
+ std::strcpy(&string[maxLength - 2], "…");
+
+ return string;
+ }
+
namespace ini {
/**
@@ -230,13 +314,18 @@ namespace tsl {
*/
using IniData = std::map>;
+ /**
+ * @brief Tesla config file
+ */
+ static const char* CONFIG_FILE = "/config/tesla/config.ini";
+
/**
* @brief Parses a ini string
*
* @param str String to parse
* @return Parsed data
*/
- static IniData parseIni(std::string &str) {
+ static IniData parseIni(const std::string &str) {
IniData iniData;
auto lines = split(str, '\n');
@@ -257,6 +346,97 @@ namespace tsl {
return iniData;
}
+ /**
+ * @brief Unparses ini data into a string
+ *
+ * @param iniData Ini data
+ * @return Ini string
+ */
+ static std::string unparseIni(IniData const &iniData) {
+ std::string string;
+ bool addSectionGap = false;
+ for (auto §ion : iniData) {
+ if (addSectionGap)
+ string += "\n";
+ string += "["s + section.first + "]\n"s;
+ for (auto &keyValue : section.second) {
+ string += keyValue.first + "="s + keyValue.second + "\n"s;
+ }
+ }
+ return string;
+ }
+
+ /**
+ * @brief Read Tesla settings file
+ *
+ * @return Settings data
+ */
+ static IniData readOverlaySettings() {
+ /* Open Sd card filesystem. */
+ FsFileSystem fsSdmc;
+ if (R_FAILED(fsOpenSdCardFileSystem(&fsSdmc)))
+ return {};
+ hlp::ScopeGuard fsGuard([&] { fsFsClose(&fsSdmc); });
+
+ /* Open config file. */
+ FsFile fileConfig;
+ if (R_FAILED(fsFsOpenFile(&fsSdmc, CONFIG_FILE, FsOpenMode_Read, &fileConfig)))
+ return {};
+ hlp::ScopeGuard fileGuard([&] { fsFileClose(&fileConfig); });
+
+ /* Get config file size. */
+ s64 configFileSize;
+ if (R_FAILED(fsFileGetSize(&fileConfig, &configFileSize)))
+ return {};
+
+ /* Read and parse config file. */
+ std::string configFileData(configFileSize, '\0');
+ u64 readSize;
+ Result rc = fsFileRead(&fileConfig, 0, configFileData.data(), configFileSize, FsReadOption_None, &readSize);
+ if (R_FAILED(rc) || readSize != static_cast(configFileSize))
+ return {};
+
+ return parseIni(configFileData);
+ }
+
+ /**
+ * @brief Replace Tesla settings file with new data
+ *
+ * @param iniData new data
+ */
+ static void writeOverlaySettings(IniData const &iniData) {
+ /* Open Sd card filesystem. */
+ FsFileSystem fsSdmc;
+ if (R_FAILED(fsOpenSdCardFileSystem(&fsSdmc)))
+ return;
+ hlp::ScopeGuard fsGuard([&] { fsFsClose(&fsSdmc); });
+
+ /* Open config file. */
+ FsFile fileConfig;
+ if (R_FAILED(fsFsOpenFile(&fsSdmc, CONFIG_FILE, FsOpenMode_Write, &fileConfig)))
+ return;
+ hlp::ScopeGuard fileGuard([&] { fsFileClose(&fileConfig); });
+
+ std::string iniString = unparseIni(iniData);
+
+ fsFileWrite(&fileConfig, 0, iniString.c_str(), iniString.length(), FsWriteOption_Flush);
+ }
+
+ /**
+ * @brief Merge and save changes into Tesla settings file
+ *
+ * @param changes setting values to add or update
+ */
+ static void updateOverlaySettings(IniData const &changes) {
+ hlp::ini::IniData iniData = hlp::ini::readOverlaySettings();
+ for (auto §ion : changes) {
+ for (auto &keyValue : section.second) {
+ iniData[section.first][keyValue.first] = keyValue.second;
+ }
+ }
+ writeOverlaySettings(iniData);
+ }
+
}
/**
@@ -265,44 +445,44 @@ namespace tsl {
* @param value Key string
* @return Key code
*/
- static u64 stringToKeyCode(std::string &value) {
- if (strcasecmp(value.c_str(), "A") == 0)
- return KEY_A;
- else if (strcasecmp(value.c_str(), "B") == 0)
- return KEY_B;
- else if (strcasecmp(value.c_str(), "X") == 0)
- return KEY_X;
- else if (strcasecmp(value.c_str(), "Y") == 0)
- return KEY_Y;
- else if (strcasecmp(value.c_str(), "LS") == 0)
- return KEY_LSTICK;
- else if (strcasecmp(value.c_str(), "RS") == 0)
- return KEY_RSTICK;
- else if (strcasecmp(value.c_str(), "L") == 0)
- return KEY_L;
- else if (strcasecmp(value.c_str(), "R") == 0)
- return KEY_R;
- else if (strcasecmp(value.c_str(), "ZL") == 0)
- return KEY_ZL;
- else if (strcasecmp(value.c_str(), "ZR") == 0)
- return KEY_ZR;
- else if (strcasecmp(value.c_str(), "PLUS") == 0)
- return KEY_PLUS;
- else if (strcasecmp(value.c_str(), "MINUS") == 0)
- return KEY_MINUS;
- else if (strcasecmp(value.c_str(), "DLEFT") == 0)
- return KEY_DLEFT;
- else if (strcasecmp(value.c_str(), "DUP") == 0)
- return KEY_DUP;
- else if (strcasecmp(value.c_str(), "DRIGHT") == 0)
- return KEY_DRIGHT;
- else if (strcasecmp(value.c_str(), "DDOWN") == 0)
- return KEY_DDOWN;
- else if (strcasecmp(value.c_str(), "SL") == 0)
- return KEY_SL;
- else if (strcasecmp(value.c_str(), "SR") == 0)
- return KEY_SR;
- else return 0;
+ static u64 stringToKeyCode(const std::string &value) {
+ for (auto &keyInfo : impl::KEYS_INFO) {
+ if (strcasecmp(value.c_str(), keyInfo.name) == 0)
+ return keyInfo.key;
+ }
+ return 0;
+ }
+
+ /**
+ * @brief Decodes a combo string into key codes
+ *
+ * @param value Combo string
+ * @return Key codes
+ */
+ static u64 comboStringToKeys(const std::string &value) {
+ u64 keyCombo = 0x00;
+ for (std::string key : hlp::split(value, '+')) {
+ keyCombo |= hlp::stringToKeyCode(key);
+ }
+ return keyCombo;
+ }
+
+ /**
+ * @brief Encodes key codes into a combo string
+ *
+ * @param keys Key codes
+ * @return Combo string
+ */
+ static std::string keysToComboString(u64 keys) {
+ std::string str;
+ for (auto &keyInfo : impl::KEYS_INFO) {
+ if (keys & keyInfo.key) {
+ if (!str.empty())
+ str.append("+");
+ str.append(keyInfo.name);
+ }
+ }
+ return str;
}
}
@@ -329,6 +509,10 @@ namespace tsl {
inline Color(u8 r, u8 g, u8 b, u8 a): r(r), g(g), b(b), a(a) {}
};
+ struct ScissoringConfig {
+ s32 x, y, w, h;
+ };
+
/**
* @brief Manages the Tesla layer and draws raw data to the screen
*/
@@ -356,20 +540,27 @@ namespace tsl {
* @param w Width
* @param h Height
*/
- inline void enableScissoring(u16 x, u16 y, u16 w, u16 h) {
- this->m_scissoring = true;
+ inline void enableScissoring(s32 x, s32 y, s32 w, s32 h) {
+ if (this->m_scissoring)
+ this->m_scissoringStack.push_back(this->m_currScissorConfig);
+ else
+ this->m_scissoring = true;
- this->m_scissorBounds[0] = x;
- this->m_scissorBounds[1] = y;
- this->m_scissorBounds[2] = w;
- this->m_scissorBounds[3] = h;
+ this->m_currScissorConfig = { x, y, w, h };
}
/**
* @brief Disables scissoring
*/
inline void disableScissoring() {
- this->m_scissoring = false;
+ if (this->m_scissoringStack.size() > 0) {
+ this->m_currScissorConfig = this->m_scissoringStack.back();
+ this->m_scissoringStack.pop_back();
+ }
+ else {
+ this->m_scissoring = false;
+ this->m_currScissorConfig = { 0 };
+ }
}
@@ -382,11 +573,14 @@ namespace tsl {
* @param y Y pos
* @param color Color
*/
- inline void setPixel(s16 x, s16 y, Color color) {
+ inline void setPixel(s32 x, s32 y, Color color) {
if (x < 0 || y < 0 || x >= cfg::FramebufferWidth || y >= cfg::FramebufferHeight)
return;
- static_cast(this->getCurrentFramebuffer())[this->getPixelOffset(x, y)] = color;
+ u32 offset = this->getPixelOffset(x, y);
+
+ if (offset != UINT32_MAX)
+ static_cast(this->getCurrentFramebuffer())[offset] = color;
}
/**
@@ -410,11 +604,16 @@ namespace tsl {
* @param y Y pos
* @param color Color
*/
- inline void setPixelBlendSrc(s16 x, s16 y, Color color) {
+ inline void setPixelBlendSrc(s32 x, s32 y, Color color) {
if (x < 0 || y < 0 || x >= cfg::FramebufferWidth || y >= cfg::FramebufferHeight)
return;
- Color src((static_cast(this->getCurrentFramebuffer()))[this->getPixelOffset(x, y)]);
+ u32 offset = this->getPixelOffset(x, y);
+
+ if (offset == UINT32_MAX)
+ return;
+
+ Color src((static_cast(this->getCurrentFramebuffer()))[offset]);
Color dst(color);
Color end(0);
@@ -433,18 +632,23 @@ namespace tsl {
* @param y Y pos
* @param color Color
*/
- inline void setPixelBlendDst(s16 x, s16 y, Color color) {
+ inline void setPixelBlendDst(s32 x, s32 y, Color color) {
if (x < 0 || y < 0 || x >= cfg::FramebufferWidth || y >= cfg::FramebufferHeight)
return;
- Color src((static_cast(this->getCurrentFramebuffer()))[this->getPixelOffset(x, y)]);
+ u32 offset = this->getPixelOffset(x, y);
+
+ if (offset == UINT32_MAX)
+ return;
+
+ Color src((static_cast(this->getCurrentFramebuffer()))[offset]);
Color dst(color);
Color end(0);
end.r = this->blendColor(src.r, dst.r, dst.a);
end.g = this->blendColor(src.g, dst.g, dst.a);
end.b = this->blendColor(src.b, dst.b, dst.a);
- end.a = dst.a;
+ end.a = std::min(dst.a + src.a, 0xF);
this->setPixel(x, y, end);
}
@@ -458,12 +662,68 @@ namespace tsl {
* @param h Height
* @param color Color
*/
- inline void drawRect(s16 x, s16 y, s16 w, s16 h, Color color) {
- for (s16 x1 = x; x1 < (x + w); x1++)
- for (s16 y1 = y; y1 < (y + h); y1++)
+ inline void drawRect(s32 x, s32 y, s32 w, s32 h, Color color) {
+ for (s32 x1 = x; x1 < (x + w); x1++)
+ for (s32 y1 = y; y1 < (y + h); y1++)
this->setPixelBlendDst(x1, y1, color);
}
+ void drawCircle(s32 centerX, s32 centerY, u16 radius, bool filled, Color color) {
+ s32 x = radius;
+ s32 y = 0;
+ s32 radiusError = 0;
+ s32 xChange = 1 - (radius << 1);
+ s32 yChange = 0;
+
+ while (x >= y) {
+ if(filled) {
+ for (s32 i = centerX - x; i <= centerX + x; i++) {
+ s32 y0 = centerY + y;
+ s32 y1 = centerY - y;
+ s32 x0 = i;
+
+ this->setPixelBlendDst(x0, y0, color);
+ this->setPixelBlendDst(x0, y1, color);
+ }
+
+ for (s32 i = centerX - y; i <= centerX + y; i++) {
+ s32 y0 = centerY + x;
+ s32 y1 = centerY - x;
+ s32 x0 = i;
+
+ this->setPixelBlendDst(x0, y0, color);
+ this->setPixelBlendDst(x0, y1, color);
+ }
+
+ y++;
+ radiusError += yChange;
+ yChange += 2;
+ if (((radiusError << 1) + xChange) > 0) {
+ x--;
+ radiusError += xChange;
+ xChange += 2;
+ }
+ } else {
+ this->setPixelBlendDst(centerX + x, centerY + y, color);
+ this->setPixelBlendDst(centerX + y, centerY + x, color);
+ this->setPixelBlendDst(centerX - y, centerY + x, color);
+ this->setPixelBlendDst(centerX - x, centerY + y, color);
+ this->setPixelBlendDst(centerX - x, centerY - y, color);
+ this->setPixelBlendDst(centerX - y, centerY - x, color);
+ this->setPixelBlendDst(centerX + y, centerY - x, color);
+ this->setPixelBlendDst(centerX + x, centerY - y, color);
+
+ if(radiusError <= 0) {
+ y++;
+ radiusError += 2 * y + 1;
+ } else {
+ x--;
+ radiusError -= 2 * x + 1;
+ }
+ }
+ }
+ }
+
/**
* @brief Draws a RGBA8888 bitmap from memory
*
@@ -511,17 +771,22 @@ namespace tsl {
* @param color Text color. Use transparent color to skip drawing and only get the string's dimensions
* @return Dimensions of drawn string
*/
- std::pair drawString(const char* string, bool monospace, u32 x, u32 y, float fontSize, Color color) {
+ std::pair drawString(const char* string, bool monospace, s32 x, s32 y, float fontSize, Color color, ssize_t maxWidth = 0, size_t* written = nullptr) {
const size_t stringLength = strlen(string);
- u32 maxX = x;
- u32 currX = x;
- u32 currY = y;
- u32 prevCharacter = 0;
+ s32 maxX = x;
+ s32 currX = x;
+ s32 currY = y;
u32 i = 0;
do {
+ if (maxWidth > 0 && maxWidth < (currX - x))
+ break;
+
+ if (written != nullptr)
+ *written += 1;
+
u32 currCharacter;
ssize_t codepointWidth = decode_utf8(&currCharacter, reinterpret_cast(string + i));
@@ -538,7 +803,6 @@ namespace tsl {
currFont = &this->m_stdFont;
float currFontSize = stbtt_ScaleForPixelHeight(currFont, fontSize);
- currX += currFontSize * stbtt_GetCodepointKernAdvance(currFont, prevCharacter, currCharacter);
int bounds[4] = { 0 };
stbtt_GetCodepointBitmapBoxSubpixel(currFont, currCharacter, currFontSize, currFontSize,
@@ -560,7 +824,7 @@ namespace tsl {
this->drawGlyph(currCharacter, currX + bounds[0], currY + bounds[1], color, currFont, currFontSize);
currX += xAdvance * currFontSize;
-
+
} while (i < stringLength);
maxX = std::max(currX, maxX);
@@ -603,7 +867,8 @@ namespace tsl {
void *m_currentFramebuffer = nullptr;
bool m_scissoring = false;
- u16 m_scissorBounds[4];
+ ScissoringConfig m_currScissorConfig;
+ std::vector m_scissoringStack;
stbtt_fontinfo m_stdFont, m_extFont;
@@ -678,13 +943,13 @@ namespace tsl {
* @param y Y Pos
* @return Offset
*/
- const u32 getPixelOffset(u32 x, u32 y) {
+ const u32 getPixelOffset(s32 x, s32 y) {
if (this->m_scissoring) {
- if (x < this->m_scissorBounds[0] ||
- y < this->m_scissorBounds[1] ||
- x > this->m_scissorBounds[0] + this->m_scissorBounds[2] ||
- y > this->m_scissorBounds[1] + this->m_scissorBounds[3])
- return cfg::FramebufferWidth * cfg::FramebufferHeight * 2 + 1;
+ if (x < this->m_currScissorConfig.x ||
+ y < this->m_currScissorConfig.y ||
+ x > this->m_currScissorConfig.x + this->m_currScissorConfig.w ||
+ y > this->m_currScissorConfig.y + this->m_currScissorConfig.h)
+ return UINT32_MAX;
}
u32 tmpPos = ((y & 127) / 16) + (x / 32 * 8) + ((y / 16 / 8) * (((cfg::FramebufferWidth / 2) / 16 * 8)));
@@ -755,25 +1020,21 @@ namespace tsl {
* @return Result
*/
Result initFonts() {
- Result res;
-
static PlFontData stdFontData, extFontData;
// Nintendo's default font
- if(R_FAILED(res = plGetSharedFontByType(&stdFontData, PlSharedFontType_Standard)))
- return res;
+ R_TRY(plGetSharedFontByType(&stdFontData, PlSharedFontType_Standard));
u8 *fontBuffer = reinterpret_cast(stdFontData.address);
stbtt_InitFont(&this->m_stdFont, fontBuffer, stbtt_GetFontOffsetForIndex(fontBuffer, 0));
// Nintendo's extended font containing a bunch of icons
- if(R_FAILED(res = plGetSharedFontByType(&extFontData, PlSharedFontType_NintendoExt)))
- return res;
+ R_TRY(plGetSharedFontByType(&extFontData, PlSharedFontType_NintendoExt));
fontBuffer = reinterpret_cast(extFontData.address);
stbtt_InitFont(&this->m_extFont, fontBuffer, stbtt_GetFontOffsetForIndex(fontBuffer, 0));
- return res;
+ return 0;
}
/**
@@ -789,7 +1050,6 @@ namespace tsl {
* @warning Don't call this before calling \ref startFrame once
*/
inline void endFrame() {
- std::memcpy(this->getNextFramebuffer(), this->getCurrentFramebuffer(), this->getFramebufferSize());
this->waitForVSync();
framebufferEnd(&this->m_framebuffer);
@@ -814,11 +1074,11 @@ namespace tsl {
if (glyphBmp == nullptr)
return;
- for (s16 bmpY = 0; bmpY < height; bmpY++) {
- for (s16 bmpX = 0; bmpX < width; bmpX++) {
+ for (s32 bmpY = 0; bmpY < height; bmpY++) {
+ for (s32 bmpX = 0; bmpX < width; bmpX++) {
Color tmpColor = color;
tmpColor.a = (glyphBmp[width * bmpY + bmpX] >> 4) * (float(tmpColor.a) / 0xF);
- this->setPixelBlendSrc(x + bmpX, y + bmpY, tmpColor);
+ this->setPixelBlendDst(x + bmpX, y + bmpY, tmpColor);
}
}
@@ -833,6 +1093,13 @@ namespace tsl {
namespace elm {
+ enum class TouchEvent {
+ Touch,
+ Hold,
+ Scroll,
+ Release
+ };
+
/**
* @brief The top level Element of the libtesla UI library
* @note When creating your own elements, extend from this or one of it's sub classes
@@ -840,7 +1107,7 @@ namespace tsl {
class Element {
public:
Element() {}
- virtual ~Element() {}
+ virtual ~Element() { }
/**
* @brief Handles focus requesting
@@ -868,6 +1135,20 @@ namespace tsl {
return m_clickListener(keys);
}
+ /**
+ * @brief Called once per frame with the latest HID inputs
+ *
+ * @param keysDown Buttons pressed in the last frame
+ * @param keysHeld Buttons held down longer than one frame
+ * @param touchInput Last touch position
+ * @param leftJoyStick Left joystick position
+ * @param rightJoyStick Right joystick position
+ * @return Weather or not the input has been consumed
+ */
+ virtual bool handleInput(u64 keysDown, u64 keysHeld, touchPosition touchPos, JoystickPosition joyStickPosLeft, JoystickPosition joyStickPosRight) {
+ return false;
+ }
+
/**
* @brief Function called when the element got touched
* @todo Not yet implemented
@@ -877,7 +1158,7 @@ namespace tsl {
* @return true when touch input has been consumed
* @return false when touch input should be passed on to the parent
*/
- virtual bool onTouch(u32 x, u32 y) {
+ virtual bool onTouch(TouchEvent event, s32 currX, s32 currY, s32 prevX, s32 prevY, s32 initialX, s32 initialY) {
return false;
}
@@ -907,10 +1188,21 @@ namespace tsl {
* @param renderer
*/
virtual void frame(gfx::Renderer *renderer) final {
+ renderer->enableScissoring(0, 0, tsl::cfg::FramebufferWidth, tsl::cfg::FramebufferHeight);
+
if (this->m_focused)
- this->drawHighlight(renderer);
+ this->drawFocusBackground(renderer);
+
+ renderer->disableScissoring();
this->draw(renderer);
+
+ renderer->enableScissoring(0, 0, tsl::cfg::FramebufferWidth, tsl::cfg::FramebufferHeight);
+
+ if (this->m_focused)
+ this->drawHighlight(renderer);
+
+ renderer->disableScissoring();
}
/**
@@ -923,7 +1215,7 @@ namespace tsl {
if (parent == nullptr)
this->layout(0, 0, cfg::FramebufferWidth, cfg::FramebufferHeight);
else
- this->layout(parent->getX(), parent->getY(), parent->getWidth(), parent->getHeight());
+ this->layout(ELEMENT_BOUNDS(parent));
}
/**
@@ -937,6 +1229,45 @@ namespace tsl {
this->m_highlightShakingStartTime = std::chrono::system_clock::now();
}
+ /**
+ * @brief Triggers the blue click animation to signal a element has been clicked on
+ *
+ */
+ virtual void triggerClickAnimation() final {
+ this->m_clickAnimationProgress = tsl::style::ListItemHighlightLength;
+ }
+
+ /**
+ * @brief Draws the blue highlight animation when clicking on a button
+ * @note Override this if you have a element that e.g requires a non-rectangular animation or a different color
+ *
+ * @param renderer Renderer
+ */
+ virtual void drawClickAnimation(gfx::Renderer *renderer) {
+ gfx::Color animColor = tsl::style::color::ColorClickAnimation;
+ u8 saturation = tsl::style::ListItemHighlightSaturation * (float(this->m_clickAnimationProgress) / float(tsl::style::ListItemHighlightLength));
+
+ animColor.g = saturation;
+ animColor.b = saturation;
+
+ renderer->drawRect(ELEMENT_BOUNDS(this), a(animColor));
+ }
+
+ /**
+ * @brief Draws the back background when a element is highlighted
+ * @note Override this if you have a element that e.g requires a non-rectangular focus
+ *
+ * @param renderer Renderer
+ */
+ virtual void drawFocusBackground(gfx::Renderer *renderer) {
+ renderer->drawRect(ELEMENT_BOUNDS(this), a(0xF000));
+
+ if (this->m_clickAnimationProgress > 0) {
+ this->drawClickAnimation(renderer);
+ this->m_clickAnimationProgress--;
+ }
+ }
+
/**
* @brief Draws the blue boarder when a element is highlighted
* @note Override this if you have a element that e.g requires a non-rectangular focus
@@ -984,12 +1315,11 @@ namespace tsl {
}
}
- renderer->drawRect(this->m_x, this->m_y, this->m_width, this->m_height, a(0xF000));
+ renderer->drawRect(this->getX() + x - 4, this->getY() + y - 4, this->getWidth() + 8, 4, a(highlightColor));
+ renderer->drawRect(this->getX() + x - 4, this->getY() + y + this->getHeight(), this->getWidth() + 8, 4, a(highlightColor));
+ renderer->drawRect(this->getX() + x - 4, this->getY() + y, 4, this->getHeight(), a(highlightColor));
+ renderer->drawRect(this->getX() + x + this->getWidth(), this->getY() + y, 4, this->getHeight(), a(highlightColor));
- renderer->drawRect(this->m_x + x - 4, this->m_y + y - 4, this->m_width + 8, 4, a(highlightColor));
- renderer->drawRect(this->m_x + x - 4, this->m_y + y + this->m_height, this->m_width + 8, 4, a(highlightColor));
- renderer->drawRect(this->m_x + x - 4, this->m_y + y, 4, this->m_height, a(highlightColor));
- renderer->drawRect(this->m_x + x + this->m_width, this->m_y + y, 4, this->m_height, a(highlightColor));
}
/**
@@ -1000,7 +1330,7 @@ namespace tsl {
* @param width Width
* @param height Height
*/
- virtual void setBoundaries(u16 x, u16 y, u16 width, u16 height) final {
+ virtual void setBoundaries(s32 x, s32 y, s32 width, s32 height) final {
this->m_x = x;
this->m_y = y;
this->m_width = width;
@@ -1021,25 +1351,25 @@ namespace tsl {
*
* @return X position
*/
- virtual inline u16 getX() final { return this->m_x; }
+ virtual inline s32 getX() final { return this->m_x; }
/**
* @brief Gets the element's Y position
*
* @return Y position
*/
- virtual inline u16 getY() final { return this->m_y; }
+ virtual inline s32 getY() final { return this->m_y; }
/**
* @brief Gets the element's Width
*
* @return Width
*/
- virtual inline u16 getWidth() final { return this->m_width; }
+ virtual inline s32 getWidth() final { return this->m_width; }
/**
* @brief Gets the element's Height
*
* @return Height
*/
- virtual inline u16 getHeight() final { return this->m_height; }
+ virtual inline s32 getHeight() final { return this->m_height; }
/**
* @brief Sets the element's parent
@@ -1063,23 +1393,23 @@ namespace tsl {
*/
virtual inline void setFocused(bool focused) { this->m_focused = focused; }
- protected:
- constexpr static inline auto a = &gfx::Renderer::a;
- private:
- friend class Gui;
+ static InputMode getInputMode() { return Element::s_inputMode; }
- u16 m_x = 0, m_y = 0, m_width = 0, m_height = 0;
- Element *m_parent = nullptr;
- bool m_focused = false;
+ static void setInputMode(InputMode mode) { Element::s_inputMode = mode; }
- std::function m_clickListener = [](u64) { return false; };
+ protected:
+ constexpr static inline auto a = &gfx::Renderer::a;
+ bool m_focused = false;
+ u8 m_clickAnimationProgress = 0;
// Highlight shake animation
bool m_highlightShaking = false;
std::chrono::system_clock::time_point m_highlightShakingStartTime;
FocusDirection m_highlightShakingDirection;
+ static inline InputMode s_inputMode;
+
/**
* @brief Shake animation callculation based on a damped sine wave
*
@@ -1095,6 +1425,43 @@ namespace tsl {
return roundf(a * exp(-(tau * t_) * sin(w * t_)));
}
+
+ private:
+ friend class Gui;
+
+ s32 m_x = 0, m_y = 0, m_width = 0, m_height = 0;
+ Element *m_parent = nullptr;
+
+ std::function m_clickListener = [](u64) { return false; };
+
+ };
+
+ /**
+ * @brief A Element that exposes the renderer directly to draw custom views easily
+ */
+ class CustomDrawer : public Element {
+ public:
+ /**
+ * @brief Constructor
+ * @note This element should only be used to draw static things the user cannot interact with e.g info text, images, etc.
+ *
+ * @param renderFunc Callback that will be called once every frame to draw this view
+ */
+ CustomDrawer(std::function renderFunc) : Element(), m_renderFunc(renderFunc) {}
+ virtual ~CustomDrawer() {}
+
+ virtual void draw(gfx::Renderer* renderer) override {
+ renderer->enableScissoring(ELEMENT_BOUNDS(this));
+ this->m_renderFunc(renderer, ELEMENT_BOUNDS(this));
+ renderer->disableScissoring();
+ }
+
+ virtual void layout(u16 parentX, u16 parentY, u16 parentWidth, u16 parentHeight) override {
+
+ }
+
+ private:
+ std::function m_renderFunc;
};
@@ -1110,20 +1477,22 @@ namespace tsl {
* @param title Name of the Overlay drawn bolt at the top
* @param subtitle Subtitle drawn bellow the title e.g version number
*/
- OverlayFrame(std::string title, std::string subtitle) : Element(), m_title(title), m_subtitle(subtitle) {}
+ OverlayFrame(const std::string& title, const std::string& subtitle) : Element(), m_title(title), m_subtitle(subtitle) {}
virtual ~OverlayFrame() {
if (this->m_contentElement != nullptr)
delete this->m_contentElement;
}
virtual void draw(gfx::Renderer *renderer) override {
- renderer->fillScreen(a({ 0x0, 0x0, 0x0, 0xD }));
+ renderer->fillScreen(a(tsl::style::color::ColorFrameBackground));
+ renderer->drawRect(tsl::cfg::FramebufferWidth - 1, 0, 1, tsl::cfg::FramebufferHeight, a(0xF222));
+
+ renderer->drawString(this->m_title.c_str(), false, 20, 50, 30, a(tsl::style::color::ColorText));
+ renderer->drawString(this->m_subtitle.c_str(), false, 20, 70, 15, a(tsl::style::color::ColorDescription));
- renderer->drawString(this->m_title.c_str(), false, 20, 50, 30, a(0xFFFF));
- renderer->drawString(this->m_subtitle.c_str(), false, 20, 70, 15, a(0xFFFF));
+ renderer->drawRect(15, tsl::cfg::FramebufferHeight - 73, tsl::cfg::FramebufferWidth - 30, 1, a(tsl::style::color::ColorText));
- renderer->drawRect(15, 720 - 73, tsl::cfg::FramebufferWidth - 30, 1, a(0xFFFF));
- renderer->drawString("\uE0E1 Back \uE0E0 OK", false, 30, 693, 23, a(0xFFFF));
+ renderer->drawString("\uE0E1 Back \uE0E0 OK", false, 30, 693, 23, a(tsl::style::color::ColorText));
if (this->m_contentElement != nullptr)
this->m_contentElement->frame(renderer);
@@ -1133,7 +1502,7 @@ namespace tsl {
this->setBoundaries(parentX, parentY, parentWidth, parentHeight);
if (this->m_contentElement != nullptr) {
- this->m_contentElement->setBoundaries(parentX + 35, parentY + 175, parentWidth - 85, parentHeight - 90 - 100);
+ this->m_contentElement->setBoundaries(parentX + 35, parentY + 125, parentWidth - 85, parentHeight - 73 - 125);
this->m_contentElement->invalidate();
}
}
@@ -1145,6 +1514,18 @@ namespace tsl {
return nullptr;
}
+ virtual bool onTouch(TouchEvent event, s32 currX, s32 currY, s32 prevX, s32 prevY, s32 initialX, s32 initialY) {
+ // Discard touches outside bounds
+ if (currX < ELEMENT_LEFT_BOUND(this->m_contentElement) || currX > ELEMENT_RIGHT_BOUND(this->m_contentElement))
+ return false;
+ if (currY < ELEMENT_TOP_BOUND(this->m_contentElement) || currY > ELEMENT_BOTTOM_BOUND(this->m_contentElement))
+ return false;
+
+ if (this->m_contentElement != nullptr)
+ return this->m_contentElement->onTouch(event, currX, currY, prevX, prevY, initialX, initialY);
+ else return false;
+ }
+
/**
* @brief Sets the content of the frame
*
@@ -1162,143 +1543,834 @@ namespace tsl {
}
}
- protected:
- Element *m_contentElement = nullptr;
-
- std::string m_title, m_subtitle;
- };
-
- /**
- * @brief Single color rectangle element mainly used for debugging to visualize boundaries
- *
- */
- class DebugRectangle : public Element {
- public:
/**
- * @brief Constructor
+ * @brief Changes the title of the menu
*
- * @param color Color of the rectangle
+ * @param title Title to change to
*/
- DebugRectangle(gfx::Color color) : Element(), m_color(color) {}
- virtual ~DebugRectangle() {}
+ virtual void setTitle(const std::string &title) final {
+ this->m_title = title;
+ }
- virtual void draw(gfx::Renderer *renderer) override {
- renderer->drawRect(this->getX(), this->getY(), this->getWidth(), this->getHeight(), a(this->m_color));
+ /**
+ * @brief Changes the subtitle of the menu
+ *
+ * @param title Subtitle to change to
+ */
+ virtual void setSubtitle(const std::string &subtitle) final {
+ this->m_subtitle = subtitle;
}
- virtual void layout(u16 parentX, u16 parentY, u16 parentWidth, u16 parentHeight) override {}
+ protected:
+ Element *m_contentElement = nullptr;
- private:
- gfx::Color m_color;
+ std::string m_title, m_subtitle;
};
/**
- * @brief A item that goes into a list
+ * @brief The base frame which can contain another view with a customizable header
*
*/
- class ListItem : public Element {
+ class HeaderOverlayFrame : public Element {
public:
- /**
- * @brief Constructor
- *
- * @param text Initial description text
- */
- ListItem(std::string text) : Element(), m_text(text) {}
- virtual ~ListItem() {}
+ HeaderOverlayFrame(u16 headerHeight = 175) : Element(), m_headerHeight(headerHeight) {}
+ virtual ~HeaderOverlayFrame() {
+ if (this->m_contentElement != nullptr)
+ delete this->m_contentElement;
+
+ if (this->m_header != nullptr)
+ delete this->m_header;
+ }
virtual void draw(gfx::Renderer *renderer) override {
- if (this->m_valueWidth == 0) {
- auto [width, height] = renderer->drawString(this->m_value.c_str(), false, 0, 0, 20, tsl::style::color::ColorTransparent);
- this->m_valueWidth = width;
- }
+ renderer->fillScreen(a(tsl::style::color::ColorFrameBackground));
+ renderer->drawRect(tsl::cfg::FramebufferWidth - 1, 0, 1, tsl::cfg::FramebufferHeight, a(0xF222));
- renderer->drawRect(this->getX(), this->getY(), this->getWidth(), 1, a({ 0x5, 0x5, 0x5, 0xF }));
- renderer->drawRect(this->getX(), this->getY() + this->getHeight(), this->getWidth(), 1, a({ 0x5, 0x5, 0x5, 0xF }));
+ renderer->drawRect(15, tsl::cfg::FramebufferHeight - 73, tsl::cfg::FramebufferWidth - 30, 1, a(tsl::style::color::ColorText));
- renderer->drawString(this->m_text.c_str(), false, this->getX() + 20, this->getY() + 45, 23, a({ 0xF, 0xF, 0xF, 0xF }));
+ renderer->drawString("\uE0E1 Back \uE0E0 OK", false, 30, 693, 23, a(tsl::style::color::ColorText));
- renderer->drawString(this->m_value.c_str(), false, this->getX() + this->getWidth() - this->m_valueWidth - 20, this->getY() + 45, 20, this->m_faint ? a({ 0x6, 0x6, 0x6, 0xF }) : a({ 0x5, 0xC, 0xA, 0xF }));
+ if (this->m_header != nullptr)
+ this->m_header->frame(renderer);
+
+ if (this->m_contentElement != nullptr)
+ this->m_contentElement->frame(renderer);
}
virtual void layout(u16 parentX, u16 parentY, u16 parentWidth, u16 parentHeight) override {
+ this->setBoundaries(parentX, parentY, parentWidth, parentHeight);
+
+ if (this->m_contentElement != nullptr) {
+ this->m_contentElement->setBoundaries(parentX + 35, parentY + this->m_headerHeight, parentWidth - 85, parentHeight - 73 - this->m_headerHeight);
+ this->m_contentElement->invalidate();
+ }
+
+ if (this->m_header != nullptr) {
+ this->m_header->setBoundaries(parentX, parentY, parentWidth, this->m_headerHeight);
+ this->m_header->invalidate();
+ }
+ }
+
+ virtual bool onTouch(TouchEvent event, s32 currX, s32 currY, s32 prevX, s32 prevY, s32 initialX, s32 initialY) {
+ // Discard touches outside bounds
+ if (currX < ELEMENT_LEFT_BOUND(this->m_contentElement) || currX > ELEMENT_RIGHT_BOUND(this->m_contentElement))
+ return false;
+ if (currY < ELEMENT_TOP_BOUND(this->m_contentElement) || currY > ELEMENT_BOTTOM_BOUND(this->m_contentElement))
+ return false;
+ if (this->m_contentElement != nullptr)
+ return this->m_contentElement->onTouch(event, currX, currY, prevX, prevY, initialX, initialY);
+ else return false;
}
virtual Element* requestFocus(Element *oldFocus, FocusDirection direction) override {
- return this;
+ if (this->m_contentElement != nullptr)
+ return this->m_contentElement->requestFocus(oldFocus, direction);
+ else
+ return nullptr;
}
/**
- * @brief Sets the left hand description text of the list item
+ * @brief Sets the content of the frame
*
- * @param text Text
+ * @param content Element
*/
- virtual inline void setText(std::string text) final {
- this->m_text = text;
- }
+ virtual void setContent(Element *content) final {
+ if (this->m_contentElement != nullptr)
+ delete this->m_contentElement;
- /**
- * @brief Sets the right hand value text of the list item
- *
- * @param value Text
- * @param faint Should the text be drawn in a glowing green or a faint gray
+ this->m_contentElement = content;
+
+ if (content != nullptr) {
+ this->m_contentElement->setParent(this);
+ this->invalidate();
+ }
+ }
+
+ /**
+ * @brief Sets the header of the frame
+ *
+ * @param header Header custom drawer
+ */
+ virtual void setHeader(CustomDrawer *header) final {
+ if (this->m_header != nullptr)
+ delete this->m_header;
+
+ this->m_header = header;
+
+ if (header != nullptr) {
+ this->m_header->setParent(this);
+ this->invalidate();
+ }
+ }
+
+ protected:
+ Element *m_contentElement = nullptr;
+ CustomDrawer *m_header = nullptr;
+
+ u16 m_headerHeight;
+ };
+
+ /**
+ * @brief Single color rectangle element mainly used for debugging to visualize boundaries
+ *
+ */
+ class DebugRectangle : public Element {
+ public:
+ /**
+ * @brief Constructor
+ *
+ * @param color Color of the rectangle
+ */
+ DebugRectangle(gfx::Color color) : Element(), m_color(color) {}
+ virtual ~DebugRectangle() {}
+
+ virtual void draw(gfx::Renderer *renderer) override {
+ renderer->drawRect(ELEMENT_BOUNDS(this), a(this->m_color));
+ }
+
+ virtual void layout(u16 parentX, u16 parentY, u16 parentWidth, u16 parentHeight) override {}
+
+ private:
+ gfx::Color m_color;
+ };
+
+
+ /**
+ * @brief A List containing list items
+ *
+ */
+ class List : public Element {
+ public:
+ /**
+ * @brief Constructor
+ *
+ */
+ List() : Element() {}
+ virtual ~List() {
+ for (auto& item : this->m_items)
+ delete item;
+ }
+
+ virtual void draw(gfx::Renderer *renderer) override {
+ if (this->m_clearList) {
+ for (auto& item : this->m_items)
+ delete item;
+
+ this->m_items.clear();
+ this->m_offset = 0;
+ this->m_focusedIndex = 0;
+ this->invalidate();
+ this->m_clearList = false;
+ }
+
+ for (auto &element : this->m_itemsToAdd) {
+ element->invalidate();
+ this->m_items.push_back(element);
+ this->invalidate();
+ this->updateScrollOffset();
+ }
+ this->m_itemsToAdd.clear();
+
+ renderer->enableScissoring(ELEMENT_LEFT_BOUND(this), ELEMENT_TOP_BOUND(this) - 5, this->getWidth(), this->getHeight() + 4);
+
+ for (auto &entry : this->m_items) {
+ if (ELEMENT_BOTTOM_BOUND(entry) > ELEMENT_TOP_BOUND(this) && ELEMENT_TOP_BOUND(entry) < ELEMENT_BOTTOM_BOUND(this)) {
+ entry->frame(renderer);
+ }
+ }
+
+ renderer->disableScissoring();
+
+ if (this->m_listHeight > this->getHeight()) {
+ float scrollbarHeight = static_cast(this->getHeight() * this->getHeight()) / this->m_listHeight;
+ float scrollbarOffset = (static_cast(this->m_offset)) / static_cast(this->m_listHeight - this->getHeight()) * (this->getHeight() - std::ceil(scrollbarHeight));
+
+ renderer->drawRect(ELEMENT_RIGHT_BOUND(this) + 10, this->getY() + scrollbarOffset, 5, scrollbarHeight - 50, a(tsl::style::color::ColorHandle));
+ renderer->drawCircle(ELEMENT_RIGHT_BOUND(this) + 12, this->getY() + scrollbarOffset, 2, true, a(tsl::style::color::ColorHandle));
+ renderer->drawCircle(ELEMENT_RIGHT_BOUND(this) + 12, this->getY() + scrollbarOffset + scrollbarHeight - 50, 2, true, a(tsl::style::color::ColorHandle));
+
+ float prevOffset = this->m_offset;
+
+ if (Element::getInputMode() == InputMode::Controller)
+ this->m_offset += ((this->m_nextOffset) - this->m_offset) * 0.1F;
+ else if (Element::getInputMode() == InputMode::TouchScroll)
+ this->m_offset += ((this->m_nextOffset) - this->m_offset);
+
+ if (static_cast(prevOffset) != static_cast(this->m_offset))
+ this->invalidate();
+ }
+
+ }
+
+ virtual void layout(u16 parentX, u16 parentY, u16 parentWidth, u16 parentHeight) override {
+ s32 y = this->getY() - this->m_offset;
+
+ this->m_listHeight = 0;
+ for (auto &entry : this->m_items)
+ this->m_listHeight += entry->getHeight();
+
+ for (auto &entry : this->m_items) {
+ entry->setBoundaries(this->getX(), y, this->getWidth(), entry->getHeight());
+ entry->invalidate();
+ y += entry->getHeight();
+ }
+ }
+
+ virtual bool onTouch(TouchEvent event, s32 currX, s32 currY, s32 prevX, s32 prevY, s32 initialX, s32 initialY) {
+ bool handled = false;
+
+ // Discard touches out of bounds
+ if (currX < ELEMENT_LEFT_BOUND(this) || currX > ELEMENT_RIGHT_BOUND(this))
+ return false;
+ if (currY < ELEMENT_TOP_BOUND(this) || currY > ELEMENT_BOTTOM_BOUND(this))
+ return false;
+
+ // Direct touches to all children
+ for (auto &item : this->m_items)
+ handled |= item->onTouch(event, currX, currY, prevX, prevY, initialX, initialY);
+
+ if (handled)
+ return true;
+
+ // Handle scrolling
+ if (event != TouchEvent::Release && Element::getInputMode() == InputMode::TouchScroll) {
+ if (prevX != 0 && prevY != 0)
+ this->m_nextOffset += (prevY - currY);
+
+ if (this->m_nextOffset < 0)
+ this->m_nextOffset = 0;
+
+ if (this->m_nextOffset > (this->m_listHeight - this->getHeight()) + 50)
+ this->m_nextOffset = (this->m_listHeight - this->getHeight() + 50);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * @brief Adds a new item to the list before the next frame starts
+ *
+ * @param element Element to add
+ * @param height Height of the element. Don't set this parameter for libtesla to try and figure out the size based on the type
+ */
+ virtual void addItem(Element *element, u16 height = 0) final {
+ if (element != nullptr) {
+ if (height != 0)
+ element->setBoundaries(this->getX(), this->getY(), this->getWidth(), height);
+
+ element->setParent(this);
+ element->invalidate();
+
+ this->m_itemsToAdd.push_back(element);
+ }
+
+ }
+
+ /**
+ * @brief Removes all children from the list later on
+ * @warning When clearing a list, make sure none of the its children are focused. Call \ref Gui::removeFocus before.
+ */
+ virtual void clear() final {
+ this->m_clearList = true;
+ }
+
+ virtual Element* requestFocus(Element *oldFocus, FocusDirection direction) override {
+ Element *newFocus = nullptr;
+
+ if (this->m_clearList || this->m_itemsToAdd.size() > 0)
+ return nullptr;
+
+ if (direction == FocusDirection::None) {
+ u16 i = 0;
+
+ if (oldFocus == nullptr) {
+ s32 elementHeight = 0;
+ while (elementHeight < this->m_offset && i < this->m_items.size() - 1) {
+ i++;
+ elementHeight += this->m_items[i]->getHeight();
+ }
+ }
+
+ for (; i < this->m_items.size(); i++) {
+ newFocus = this->m_items[i]->requestFocus(oldFocus, direction);
+
+ if (newFocus != nullptr) {
+ this->m_focusedIndex = i;
+
+ this->updateScrollOffset();
+ return newFocus;
+ }
+ }
+ } else {
+ if (direction == FocusDirection::Down) {
+
+ for (u16 i = this->m_focusedIndex + 1; i < this->m_items.size(); i++) {
+ newFocus = this->m_items[i]->requestFocus(oldFocus, direction);
+
+ if (newFocus != nullptr && newFocus != oldFocus) {
+ this->m_focusedIndex = i;
+
+ this->updateScrollOffset();
+ return newFocus;
+ }
+ }
+
+ return oldFocus;
+ } else if (direction == FocusDirection::Up) {
+ if (this->m_focusedIndex > 0) {
+
+ for (u16 i = this->m_focusedIndex - 1; i >= 0; i--) {
+ if (i > this->m_items.size() || this->m_items[i] == nullptr)
+ return oldFocus;
+ else
+ newFocus = this->m_items[i]->requestFocus(oldFocus, direction);
+
+ if (newFocus != nullptr && newFocus != oldFocus) {
+ this->m_focusedIndex = i;
+
+ this->updateScrollOffset();
+ return newFocus;
+ }
+ }
+ }
+
+ return oldFocus;
+ }
+ }
+
+ return oldFocus;
+ }
+
+ /**
+ * @brief Gets the index in the list of the element passed in
+ *
+ * @param element Element to check
+ * @return Index in list. -1 for if the element isn't a member of the list
+ */
+ virtual s32 getIndexInList(Element *element) {
+ auto it = std::find(this->m_items.begin(), this->m_items.end(), element);
+
+ if (it == this->m_items.end())
+ return -1;
+
+ return it - this->m_items.begin();
+ }
+
+ protected:
+ std::vector m_items;
+ u16 m_focusedIndex = 0;
+
+ float m_offset = 0, m_nextOffset = 0;
+ s32 m_listHeight = 0;
+
+ bool m_clearList = false;
+ std::vector m_itemsToAdd;
+
+ private:
+
+ virtual void updateScrollOffset() {
+ if (this->getInputMode() != InputMode::Controller)
+ return;
+
+ if (this->m_listHeight <= this->getHeight()) {
+ this->m_nextOffset = 0;
+ this->m_offset = 0;
+
+ return;
+ }
+
+ this->m_nextOffset = 0;
+ for (u16 i = 0; i < this->m_focusedIndex; i++)
+ this->m_nextOffset += this->m_items[i]->getHeight();
+
+ this->m_nextOffset -= this->getHeight() / 3;
+
+ if (this->m_nextOffset < 0)
+ this->m_nextOffset = 0;
+
+ if (this->m_nextOffset > (this->m_listHeight - this->getHeight()) + 50)
+ this->m_nextOffset = (this->m_listHeight - this->getHeight() + 50);
+ }
+ };
+
+ /**
+ * @brief A item that goes into a list
+ *
+ */
+ class ListItem : public Element {
+ public:
+ /**
+ * @brief Constructor
+ *
+ * @param text Initial description text
+ */
+ ListItem(const std::string& text, const std::string& value = "")
+ : Element(), m_text(text), m_value(value) {
+ }
+ virtual ~ListItem() {}
+
+ virtual void draw(gfx::Renderer *renderer) override {
+ if (this->m_touched && Element::getInputMode() == InputMode::Touch) {
+ renderer->drawRect(ELEMENT_BOUNDS(this), a(tsl::style::color::ColorClickAnimation));
+ }
+
+ if (this->m_maxWidth == 0) {
+ if (this->m_value.length() > 0) {
+ auto [valueWidth, valueHeight] = renderer->drawString(this->m_value.c_str(), false, 0, 0, 20, tsl::style::color::ColorTransparent);
+ this->m_maxWidth = this->getWidth() - valueWidth - 70;
+ } else {
+ this->m_maxWidth = this->getWidth() - 40;
+ }
+
+ size_t written = 0;
+ renderer->drawString(this->m_text.c_str(), false, 0, 0, 23, tsl::style::color::ColorTransparent, this->m_maxWidth, &written);
+ this->m_trunctuated = written < this->m_text.length();
+
+ if (this->m_trunctuated) {
+ this->m_maxScroll = this->m_text.length() + 8;
+ this->m_scrollText = this->m_text + " " + this->m_text;
+ this->m_ellipsisText = hlp::limitStringLength(this->m_text, written);
+ }
+ }
+
+ renderer->drawRect(this->getX(), this->getY(), this->getWidth(), 1, a(tsl::style::color::ColorFrame));
+ renderer->drawRect(this->getX(), ELEMENT_BOTTOM_BOUND(this), this->getWidth(), 1, a(tsl::style::color::ColorFrame));
+
+ const char *text = m_text.c_str();
+ if (this->m_trunctuated) {
+ if (this->m_focused) {
+ if (this->m_scroll) {
+ if ((this->m_scrollAnimationCounter % 20) == 0) {
+ this->m_scrollOffset++;
+ if (this->m_scrollOffset >= this->m_maxScroll) {
+ this->m_scrollOffset = 0;
+ this->m_scroll = false;
+ this->m_scrollAnimationCounter = 0;
+ }
+ }
+ text = this->m_scrollText.c_str() + this->m_scrollOffset;
+ } else {
+ if (this->m_scrollAnimationCounter > 60) {
+ this->m_scroll = true;
+ this->m_scrollAnimationCounter = 0;
+ }
+ }
+ this->m_scrollAnimationCounter++;
+ } else {
+ text = this->m_ellipsisText.c_str();
+ }
+ }
+
+ renderer->drawString(text, false, this->getX() + 20, this->getY() + 45, 23, a(tsl::style::color::ColorText), this->m_maxWidth);
+
+ renderer->drawString(this->m_value.c_str(), false, this->getX() + this->m_maxWidth + 45, this->getY() + 45, 20, this->m_faint ? a(tsl::style::color::ColorDescription) : a(tsl::style::color::ColorHighlight));
+ }
+
+ virtual void layout(u16 parentX, u16 parentY, u16 parentWidth, u16 parentHeight) override {
+ this->setBoundaries(this->getX(), this->getY(), this->getWidth(), tsl::style::ListItemDefaultHeight);
+ }
+
+ virtual bool onClick(u64 keys) override {
+ if (keys & KEY_A)
+ this->triggerClickAnimation();
+ else if (keys & (KEY_UP | KEY_DOWN | KEY_LEFT | KEY_RIGHT))
+ this->m_clickAnimationProgress = 0;
+
+ return Element::onClick(keys);
+ }
+
+
+ virtual bool onTouch(TouchEvent event, s32 currX, s32 currY, s32 prevX, s32 prevY, s32 initialX, s32 initialY) override {
+ if (event == TouchEvent::Touch)
+ this->m_touched = currX > ELEMENT_LEFT_BOUND(this) && currX < (ELEMENT_RIGHT_BOUND(this)) && currY > ELEMENT_TOP_BOUND(this) && currY < (ELEMENT_BOTTOM_BOUND(this));
+
+ if (event == TouchEvent::Release && this->m_touched) {
+ this->m_touched = false;
+
+ if (Element::getInputMode() == InputMode::Touch) {
+ bool handled = this->onClick(KEY_A);
+
+ this->m_clickAnimationProgress = 0;
+ return handled;
+ }
+ }
+
+
+ return false;
+ }
+
+
+ virtual void setFocused(bool state) override {
+ this->m_scroll = false;
+ this->m_scrollOffset = 0;
+ this->m_scrollAnimationCounter = 0;
+ this->m_focused = state;
+ }
+
+ virtual Element* requestFocus(Element *oldFocus, FocusDirection direction) override {
+ return this;
+ }
+
+ /**
+ * @brief Sets the left hand description text of the list item
+ *
+ * @param text Text
*/
- virtual inline void setValue(std::string value, bool faint = false) {
+ virtual inline void setText(const std::string& text) {
+ this->m_text = text;
+ this->m_scrollText = "";
+ this->m_ellipsisText = "";
+ this->m_maxWidth = 0;
+ }
+
+ /**
+ * @brief Sets the right hand value text of the list item
+ *
+ * @param value Text
+ * @param faint Should the text be drawn in a glowing green or a faint gray
+ */
+ virtual inline void setValue(const std::string& value, bool faint = false) {
this->m_value = value;
this->m_faint = faint;
- this->m_valueWidth = 0;
+ this->m_maxWidth = 0;
}
protected:
std::string m_text;
std::string m_value = "";
+ std::string m_scrollText = "";
+ std::string m_ellipsisText = "";
+
+ bool m_scroll = false;
+ bool m_trunctuated = false;
bool m_faint = false;
- u16 m_valueWidth = 0;
+ bool m_touched = false;
+
+ u16 m_maxScroll = 0;
+ u16 m_scrollOffset = 0;
+ u32 m_maxWidth = 0;
+ u16 m_scrollAnimationCounter = 0;
};
- /**
- * @brief A toggleable list item that changes the state from On to Off when the A button gets pressed
- *
- */
- class ToggleListItem : public ListItem {
- public:
- /**
- * @brief Constructor
- *
- * @param text Initial description text
- * @param initialState Is the toggle set to On or Off initially
- * @param onValue Value drawn if the toggle is on
- * @param offValue Value drawn if the toggle is off
- */
- ToggleListItem(std::string text, bool initialState, std::string onValue = "On", std::string offValue = "Off")
- : ListItem(text), m_state(initialState), m_onValue(onValue), m_offValue(offValue) {
-
- this->setState(this->m_state);
+ /**
+ * @brief A toggleable list item that changes the state from On to Off when the A button gets pressed
+ *
+ */
+ class ToggleListItem : public ListItem {
+ public:
+ /**
+ * @brief Constructor
+ *
+ * @param text Initial description text
+ * @param initialState Is the toggle set to On or Off initially
+ * @param onValue Value drawn if the toggle is on
+ * @param offValue Value drawn if the toggle is off
+ */
+ ToggleListItem(const std::string& text, bool initialState, const std::string& onValue = "On", const std::string& offValue = "Off")
+ : ListItem(text), m_state(initialState), m_onValue(onValue), m_offValue(offValue) {
+
+ this->setState(this->m_state);
+ }
+
+ virtual ~ToggleListItem() {}
+
+ virtual bool onClick(u64 keys) override {
+ if (keys & KEY_A) {
+ this->m_state = !this->m_state;
+
+ this->setState(this->m_state);
+ this->m_stateChangedListener(this->m_state);
+
+ return ListItem::onClick(keys);
+ }
+
+ return false;
+ }
+
+ /**
+ * @brief Gets the current state of the toggle
+ *
+ * @return State
+ */
+ virtual inline bool getState() {
+ return this->m_state;
+ }
+
+ /**
+ * @brief Sets the current state of the toggle. Updates the Value
+ *
+ * @param state State
+ */
+ virtual void setState(bool state) {
+ this->m_state = state;
+
+ if (state)
+ this->setValue(this->m_onValue, false);
+ else
+ this->setValue(this->m_offValue, true);
+ }
+
+ /**
+ * @brief Adds a listener that gets called whenever the state of the toggle changes
+ *
+ * @param stateChangedListener Listener with the current state passed in as parameter
+ */
+ void setStateChangedListener(std::function stateChangedListener) {
+ this->m_stateChangedListener = stateChangedListener;
+ }
+
+ protected:
+ bool m_state = true;
+ std::string m_onValue, m_offValue;
+
+ std::function m_stateChangedListener = [](bool){};
+ };
+
+ class CategoryHeader : public ListItem {
+ public:
+ CategoryHeader(const std::string &title, bool hasSeparator = false) : ListItem(title), m_hasSeparator(hasSeparator) {}
+ virtual ~CategoryHeader() {}
+
+ virtual void draw(gfx::Renderer *renderer) override {
+ renderer->drawRect(this->getX() - 2, ELEMENT_BOTTOM_BOUND(this) - 30, 5, 23, a(tsl::style::color::ColorHeaderBar));
+ renderer->drawString(this->m_text.c_str(), false, this->getX() + 13, ELEMENT_BOTTOM_BOUND(this) - 12, 15, a(tsl::style::color::ColorText));
+
+ if (this->m_hasSeparator)
+ renderer->drawRect(this->getX(), ELEMENT_BOTTOM_BOUND(this), this->getWidth(), 1, a(tsl::style::color::ColorFrame));
+ }
+
+ virtual void layout(u16 parentX, u16 parentY, u16 parentWidth, u16 parentHeight) override {
+ // Check if the CategoryHeader is part of a list and if it's the first entry in it, half it's height
+ if (List *list = dynamic_cast(this->getParent()); list != nullptr) {
+ if (list->getIndexInList(this) == 0) {
+ this->setBoundaries(this->getX(), this->getY(), this->getWidth(), tsl::style::ListItemDefaultHeight / 2);
+ return;
+ }
+ }
+
+ this->setBoundaries(this->getX(), this->getY(), this->getWidth(), tsl::style::ListItemDefaultHeight);
+ }
+
+ virtual bool onClick(u64 keys) {
+ return false;
+ }
+
+ virtual Element* requestFocus(Element *oldFocus, FocusDirection direction) override {
+ return nullptr;
+ }
+
+ private:
+ bool m_hasSeparator;
+ };
+
+ /**
+ * @brief A customizable analog trackbar going from 0% to 100% (like the brightness slider)
+ *
+ */
+ class TrackBar : public ListItem {
+ public:
+ /**
+ * @brief Constructor
+ *
+ * @param icon Icon shown next to the track bar
+ */
+ TrackBar(const char icon[3]) : ListItem(icon), m_icon(icon) { }
+
+ virtual ~TrackBar() {}
+
+ virtual bool handleInput(u64 keysDown, u64 keysHeld, touchPosition touchInput, JoystickPosition leftJoyStick, JoystickPosition rightJoyStick) override {
+ if (keysHeld & KEY_LEFT && keysHeld & KEY_RIGHT)
+ return true;
+
+ if (keysHeld & KEY_LEFT) {
+ if (this->m_value > 0) {
+ this->m_value--;
+ this->m_valueChangedListener(this->m_value);
+ return true;
+ }
+ }
+
+ if (keysHeld & KEY_RIGHT) {
+ if (this->m_value < 100) {
+ this->m_value++;
+ this->m_valueChangedListener(this->m_value);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ virtual bool onTouch(TouchEvent event, s32 currX, s32 currY, s32 prevX, s32 prevY, s32 initialX, s32 initialY) override {
+ if (event == TouchEvent::Release) {
+ this->m_interactionLocked = false;
+ return false;
+ }
+
+
+ if (!this->m_interactionLocked && initialX > ELEMENT_LEFT_BOUND(this) && initialX < (ELEMENT_RIGHT_BOUND(this)) && initialY > ELEMENT_TOP_BOUND(this) && initialY < ELEMENT_BOTTOM_BOUND(this)) {
+ if (currX > ELEMENT_LEFT_BOUND(this) + 50 && currX < ELEMENT_RIGHT_BOUND(this) && currY > ELEMENT_TOP_BOUND(this) && currY < ELEMENT_BOTTOM_BOUND(this)) {
+ this->m_value = (static_cast(currX - (this->getX() + 60)) / static_cast(this->getWidth() - 95)) * 100;
+
+ if (this->m_value < 0)
+ this->m_value = 0;
+
+ if (this->m_value > 100)
+ this->m_value = 100;
+
+ return true;
+ }
+ }
+ else
+ this->m_interactionLocked = true;
+
+ return false;
+ }
+
+ virtual void draw(gfx::Renderer *renderer) override {
+ renderer->drawRect(this->getX(), this->getY(), this->getWidth(), 1, a(tsl::style::color::ColorFrame));
+ renderer->drawRect(this->getX(), ELEMENT_BOTTOM_BOUND(this), this->getWidth(), 1, a(tsl::style::color::ColorFrame));
+
+ renderer->drawString(this->m_icon, false, this->getX() + 15, this->getY() + 50, 23, a(tsl::style::color::ColorText), this->m_maxWidth);
+
+ u16 handlePos = (this->getWidth() - 95) * static_cast(this->m_value) / 100;
+ renderer->drawCircle(this->getX() + 60, this->getY() + 42, 2, true, a(tsl::style::color::ColorHighlight));
+ renderer->drawCircle(this->getX() + 60 + this->getWidth() - 95, this->getY() + 42, 2, true, a(tsl::style::color::ColorFrame));
+ renderer->drawRect(this->getX() + 60 + handlePos, this->getY() + 40, this->getWidth() - 95 - handlePos, 5, a(tsl::style::color::ColorFrame));
+ renderer->drawRect(this->getX() + 60, this->getY() + 40, handlePos, 5, a(tsl::style::color::ColorHighlight));
+
+ renderer->drawCircle(this->getX() + 62 + handlePos, this->getY() + 42, 18, true, a(tsl::style::color::ColorHandle));
+ renderer->drawCircle(this->getX() + 62 + handlePos, this->getY() + 42, 18, false, a(tsl::style::color::ColorFrame));
+ }
+
+ virtual void layout(u16 parentX, u16 parentY, u16 parentWidth, u16 parentHeight) override {
+ this->setBoundaries(this->getX(), this->getY(), this->getWidth(), tsl::style::TrackBarDefaultHeight);
+ }
+
+ virtual void drawFocusBackground(gfx::Renderer *renderer) {
+ // No background drawn here in HOS
}
- virtual ~ToggleListItem() {}
+ virtual void drawHighlight(gfx::Renderer *renderer) override {
+ static float counter = 0;
+ const float progress = (std::sin(counter) + 1) / 2;
+ gfx::Color highlightColor = { static_cast((0x2 - 0x8) * progress + 0x8),
+ static_cast((0x8 - 0xF) * progress + 0xF),
+ static_cast((0xC - 0xF) * progress + 0xF),
+ static_cast((0x6 - 0xD) * progress + 0xD) };
- virtual bool onClick(u64 keys) {
- if (keys & KEY_A) {
- this->m_state = !this->m_state;
+ counter += 0.1F;
- this->setState(this->m_state);
- this->m_stateChangedListener(this->m_state);
+ u16 handlePos = (this->getWidth() - 95) * static_cast(this->m_value) / 100;
- return true;
+ s32 x = 0;
+ s32 y = 0;
+
+ if (Element::m_highlightShaking) {
+ auto t = (std::chrono::system_clock::now() - Element::m_highlightShakingStartTime);
+ if (t >= 100ms)
+ Element::m_highlightShaking = false;
+ else {
+ s32 amplitude = std::rand() % 5 + 5;
+
+ switch (Element::m_highlightShakingDirection) {
+ case FocusDirection::Up:
+ y -= shakeAnimation(t, amplitude);
+ break;
+ case FocusDirection::Down:
+ y += shakeAnimation(t, amplitude);
+ break;
+ case FocusDirection::Left:
+ x -= shakeAnimation(t, amplitude);
+ break;
+ case FocusDirection::Right:
+ x += shakeAnimation(t, amplitude);
+ break;
+ default:
+ break;
+ }
+
+ x = std::clamp(x, -amplitude, amplitude);
+ y = std::clamp(y, -amplitude, amplitude);
+ }
}
- return false;
+ for (u8 i = 16; i <= 19; i++) {
+ renderer->drawCircle(this->getX() + 62 + x + handlePos, this->getY() + 42 + y, i, false, a(highlightColor));
+ }
}
/**
- * @brief Gets the current state of the toggle
+ * @brief Gets the current value of the trackbar
*
* @return State
*/
- virtual inline bool getState() {
- return this->m_state;
+ virtual inline u8 getProgress() {
+ return this->m_value;
}
/**
@@ -1306,13 +2378,8 @@ namespace tsl {
*
* @param state State
*/
- virtual void setState(bool state) {
- this->m_state = state;
-
- if (state)
- this->setValue(this->m_onValue, false);
- else
- this->setValue(this->m_offValue, true);
+ virtual void setProgress(u8 value) {
+ this->m_value = value;
}
/**
@@ -1320,175 +2387,149 @@ namespace tsl {
*
* @param stateChangedListener Listener with the current state passed in as parameter
*/
- void setStateChangedListener(std::function stateChangedListener) {
- this->m_stateChangedListener = stateChangedListener;
+ void setValueChangedListener(std::function valueChangedListener) {
+ this->m_valueChangedListener = valueChangedListener;
}
protected:
- bool m_state = true;
- std::string m_onValue, m_offValue;
+ const char *m_icon = nullptr;
+ s16 m_value = 0;
+ bool m_interactionLocked = false;
- std::function m_stateChangedListener = [](bool){};
+ std::function m_valueChangedListener = [](u8){};
+
+ private:
+ virtual inline void setText(const std::string& text) {}
+ virtual inline void setValue(const std::string& value, bool faint = false) {}
};
/**
- * @brief A List containing list items
+ * @brief A customizable analog trackbar going from 0% to 100% but using discrete steps (Like the volume slider)
*
*/
- class List : public Element {
+ class StepTrackBar : public TrackBar {
public:
/**
* @brief Constructor
*
- * @param entriesShown Amount of items displayed in the list at once before scrolling starts
+ * @param icon Icon shown next to the track bar
+ * @param numSteps Number of steps the track bar has
*/
- List(u16 entriesShown = 5) : Element(), m_entriesShown(entriesShown) {}
- virtual ~List() {
- for (auto& item : this->m_items)
- delete item.element;
- }
+ StepTrackBar(const char icon[3], size_t numSteps)
+ : TrackBar(icon), m_numSteps(numSteps) { }
- virtual void draw(gfx::Renderer *renderer) override {
- u16 i = 0;
- for (auto &entry : this->m_items) {
- if (i >= this->m_offset && i < this->m_offset + this->m_entriesShown) {
- entry.element->frame(renderer);
+ virtual ~StepTrackBar() {}
+
+ virtual bool handleInput(u64 keysDown, u64 keysHeld, touchPosition touchInput, JoystickPosition leftJoyStick, JoystickPosition rightJoyStick) override {
+ static u32 tick = 0;
+
+ if (keysHeld & KEY_LEFT && keysHeld & KEY_RIGHT) {
+ tick = 0;
+ return true;
+ }
+
+ if (keysHeld & (KEY_LEFT | KEY_RIGHT)) {
+ if ((tick == 0 || tick > 20) && (tick % 3) == 0) {
+ if (keysHeld & KEY_LEFT && this->m_value > 0) {
+ this->m_value = std::max(this->m_value - (100 / (this->m_numSteps - 1)), 0);
+ } else if (keysHeld & KEY_RIGHT && this->m_value < 100) {
+ this->m_value = std::min(this->m_value + (100 / (this->m_numSteps - 1)), 100);
+ } else {
+ return false;
+ }
+ this->m_valueChangedListener(this->getProgress());
}
- i++;
+ tick++;
+ return true;
+ } else {
+ tick = 0;
}
+
+ return false;
}
- virtual void layout(u16 parentX, u16 parentY, u16 parentWidth, u16 parentHeight) override {
- u16 y = this->getY();
- u16 i = 0;
- for (auto &entry : this->m_items) {
- if (i >= this->m_offset && i < this->m_offset + this->m_entriesShown) {
- entry.element->setBoundaries(this->getX(), y, this->getWidth(), entry.height);
- entry.element->invalidate();
- y += entry.height;
+ virtual bool onTouch(TouchEvent event, s32 currX, s32 currY, s32 prevX, s32 prevY, s32 initialX, s32 initialY) override {
+ if (initialX > ELEMENT_LEFT_BOUND(this) && initialX < ELEMENT_RIGHT_BOUND(this) && initialY > ELEMENT_TOP_BOUND(this) && initialY < ELEMENT_BOTTOM_BOUND(this)) {
+ if (currY > ELEMENT_TOP_BOUND(this) && currY < ELEMENT_BOTTOM_BOUND(this)) {
+ this->m_value = (static_cast(currX - (this->getX() + 60)) / static_cast(this->getWidth() - 95)) * 100;
+
+ if (this->m_value < 0)
+ this->m_value = 0;
+
+ if (this->m_value > 100)
+ this->m_value = 100;
+
+ this->m_value = std::round(this->m_value / (100.0F / (this->m_numSteps - 1))) * (100.0F / (this->m_numSteps - 1));
+
+ return true;
}
- i++;
}
+
+ return false;
}
/**
- * @brief Adds a new item to the list
+ * @brief Gets the current value of the trackbar
*
- * @param element Element to add
- * @param height Height of the element. Don't set this parameter for libtesla to try and figure out the size based on the type
+ * @return State
*/
- virtual void addItem(Element *element, u16 height = 0) final {
- if (height == 0) {
- if (dynamic_cast(element) != nullptr)
- height = tsl::style::ListItemDefaultHeight;
- }
-
- if (element != nullptr && height > 0) {
- element->setParent(this);
- this->m_items.push_back({ element, height });
- this->invalidate();
- }
-
- if (this->m_items.size() == 1)
- this->requestFocus(nullptr, FocusDirection::None);
- }
+ virtual inline u8 getProgress() override {
+ return this->m_value / (100 / (this->m_numSteps - 1));
+ }
/**
- * @brief Removes all children from the list
+ * @brief Sets the current state of the toggle. Updates the Value
+ *
+ * @param state State
*/
- virtual void clear() final {
- for (auto& item : this->m_items)
- delete item.element;
-
- this->m_items.clear();
- this->m_offset = 0;
- this->m_focusedElement = 0;
+ virtual void setProgress(u8 value) override {
+ value = std::min(value, u8(this->m_numSteps - 1));
+ this->m_value = value * (100 / (this->m_numSteps - 1));
}
- virtual Element* requestFocus(Element *oldFocus, FocusDirection direction) override {
- if (this->m_items.size() == 0)
- return nullptr;
+ protected:
+ u8 m_numSteps = 1;
+ };
- auto it = std::find(this->m_items.begin(), this->m_items.end(), oldFocus);
- if (it == this->m_items.end() || direction == FocusDirection::None)
- return this->m_items[0].element;
+ /**
+ * @brief A customizable trackbar with multiple discrete steps with specific names. Name gets displayed above the bar
+ *
+ */
+ class NamedStepTrackBar : public StepTrackBar {
+ public:
+ /**
+ * @brief Constructor
+ *
+ * @param icon Icon shown next to the track bar
+ * @param stepDescriptions Step names displayed above the track bar
+ */
+ NamedStepTrackBar(const char icon[3], std::initializer_list stepDescriptions)
+ : StepTrackBar(icon, stepDescriptions.size()), m_stepDescriptions(stepDescriptions.begin(), stepDescriptions.end()) { }
- if (direction == FocusDirection::Up) {
- if (it == this->m_items.begin())
- return this->m_items[0].element;
- else {
- // old focus on the second item, and has offset
- if (oldFocus == (this->m_items.begin() + this->m_offset + 1)->element) {
- if (this->m_offset > 0) {
- this->m_offset--;
- this->invalidate();
- }
- }
- return (it - 1)->element;
- }
- } else if (direction == FocusDirection::Down) {
- if (it == (this->m_items.end() - 1)) {
- return this->m_items[this->m_items.size() - 1].element;
- }
- else {
- // old focus on second to last item, and has more items hidden
- if (oldFocus == (this->m_items.begin() + this->m_offset + this->m_entriesShown - 2)->element) {
- if (this->m_items.size() > this->m_offset + this->m_entriesShown) {
- this->m_offset++;
- this->invalidate();
- }
- }
- return (it + 1)->element;
- }
- }
-
- return it->element;
- }
+ virtual ~NamedStepTrackBar() {}
- protected:
- struct ListEntry {
- Element *element;
- u16 height;
+ virtual void draw(gfx::Renderer *renderer) override {
- bool operator==(Element *other) {
- return this->element == other;
- }
- };
+ u16 trackBarWidth = this->getWidth() - 95;
+ u16 stepWidth = trackBarWidth / (this->m_numSteps - 1);
- std::vector m_items;
- u16 m_focusedElement = 0;
+ for (u8 i = 0; i < this->m_numSteps; i++) {
+ renderer->drawRect(this->getX() + 60 + stepWidth * i, this->getY() + 50, 1, 10, a(tsl::style::color::ColorFrame));
+ }
- u16 m_offset = 0;
- u16 m_entriesShown = 5;
- };
+ u8 currentDescIndex = std::clamp(this->m_value / (100 / (this->m_numSteps - 1)), 0, this->m_numSteps - 1);
- /**
- * @brief A Element that exposes the renderer directly to draw custom views easily
- */
- class CustomDrawer : public Element {
- public:
- /**
- * @brief Constructor
- * @note This element should only be used to draw static things the user cannot interact with e.g info text, images, etc.
- *
- * @param renderFunc Callback that will be called once every frame to draw this view
- */
- CustomDrawer(std::function renderFunc) : Element(), m_renderFunc(renderFunc) {}
- virtual ~CustomDrawer() {}
-
- virtual void draw(gfx::Renderer* renderer) override {
- this->m_renderFunc(renderer, this->getX(), this->getY(), this->getWidth(), this->getHeight());
- }
+ auto [descWidth, descHeight] = renderer->drawString(this->m_stepDescriptions[currentDescIndex].c_str(), false, 0, 0, 15, tsl::style::color::ColorTransparent);
+ renderer->drawString(this->m_stepDescriptions[currentDescIndex].c_str(), false, ((this->getX() + 60) + (this->getWidth() - 95) / 2) - (descWidth / 2), this->getY() + 20, 15, a(tsl::style::color::ColorDescription));
- virtual void layout(u16 parentX, u16 parentY, u16 parentWidth, u16 parentHeight) override {
- this->setBoundaries(parentX, parentY, parentWidth, parentHeight);
- }
+ StepTrackBar::draw(renderer);
+ }
- private:
- std::function m_renderFunc;
- };
+ protected:
+ std::vector m_stepDescriptions;
+ };
}
@@ -1561,7 +2602,7 @@ namespace tsl {
* @param element Element to focus
* @param direction Focus direction
*/
- virtual void requestFocus(elm::Element *element, FocusDirection direction) final {
+ virtual void requestFocus(elm::Element *element, FocusDirection direction, bool shake = true) final {
elm::Element *oldFocus = this->m_focusedElement;
if (element != nullptr) {
@@ -1575,7 +2616,7 @@ namespace tsl {
}
}
- if (oldFocus == this->m_focusedElement && this->m_focusedElement != nullptr)
+ if (shake && oldFocus == this->m_focusedElement && this->m_focusedElement != nullptr)
this->m_focusedElement->shakeHighlight(direction);
}
@@ -1585,8 +2626,16 @@ namespace tsl {
* @param element Element to remove focus from. Pass nullptr to remove the focus unconditionally
*/
virtual void removeFocus(elm::Element* element = nullptr) final {
- if (element == nullptr || element == this->m_focusedElement)
- this->m_focusedElement = nullptr;
+ if (element == nullptr || element == this->m_focusedElement) {
+ if (this->m_focusedElement != nullptr) {
+ this->m_focusedElement->setFocused(false);
+ this->m_focusedElement = nullptr;
+ }
+ }
+ }
+
+ virtual void restoreFocus() final {
+ this->m_initialFocusSet = false;
}
protected:
@@ -1596,6 +2645,8 @@ namespace tsl {
elm::Element *m_focusedElement = nullptr;
elm::Element *m_topElement = nullptr;
+ bool m_initialFocusSet = false;
+
friend class Overlay;
friend class gfx::Renderer;
@@ -1608,6 +2659,15 @@ namespace tsl {
if (this->m_topElement != nullptr)
this->m_topElement->draw(renderer);
}
+
+ virtual bool initialFocusSet() final {
+ return this->m_initialFocusSet;
+ }
+
+ virtual void markInitialFocusSet() final {
+ this->m_initialFocusSet = true;
+ }
+
};
@@ -1689,6 +2749,9 @@ namespace tsl {
}
this->onShow();
+
+ if (auto& currGui = this->getCurrentGui(); currGui != nullptr)
+ currGui->restoreFocus();
}
/**
@@ -1830,7 +2893,6 @@ namespace tsl {
renderer.startFrame();
-
this->animationLoop();
this->getCurrentGui()->update();
this->getCurrentGui()->draw(&renderer);
@@ -1848,45 +2910,137 @@ namespace tsl {
* @param rightJoyStick Right joystick position
* @return Weather or not the input has been consumed
*/
- virtual void handleInput(u64 keysDown, u64 keysHeld, touchPosition touchPos, JoystickPosition joyStickPosLeft, JoystickPosition joyStickPosRight) final {
+ virtual void handleInput(u64 keysDown, u64 keysHeld, bool touchDetected, touchPosition touchPos, JoystickPosition joyStickPosLeft, JoystickPosition joyStickPosRight) final {
+ static touchPosition initialTouchPos = { 0 };
+ static touchPosition oldTouchPos = { 0 };
+ static bool oldTouchDetected = false;
+ static elm::TouchEvent touchEvent;
+ static u32 repeatTick = 0;
+
auto& currentGui = this->getCurrentGui();
+
+ if (currentGui == nullptr)
+ return;
+
auto currentFocus = currentGui->getFocusedElement();
+ auto topElement = currentGui->getTopElement();
if (currentFocus == nullptr) {
- if (elm::Element* topElement = currentGui->getTopElement(); topElement == nullptr) {
- if (keysDown & KEY_B)
- this->goBack();
+ if (keysDown & KEY_B) {
+ this->goBack();
+ return;
+ }
+ if (topElement == nullptr)
return;
+ else if (currentGui != nullptr) {
+ if (!currentGui->initialFocusSet() || keysDown & (KEY_UP | KEY_DOWN | KEY_LEFT | KEY_RIGHT)) {
+ currentGui->requestFocus(topElement, FocusDirection::None);
+ currentGui->markInitialFocusSet();
+ repeatTick = 1;
+ }
}
- else
- currentFocus = topElement;
}
bool handled = false;
elm::Element *parentElement = currentFocus;
- do {
+
+ while (!handled && parentElement != nullptr) {
handled = parentElement->onClick(keysDown);
parentElement = parentElement->getParent();
- } while (!handled && parentElement != nullptr);
+ }
+
+ parentElement = currentFocus;
+ while (!handled && parentElement != nullptr) {
+ handled = parentElement->handleInput(keysDown, keysHeld, touchPos, joyStickPosLeft, joyStickPosRight);
+ parentElement = parentElement->getParent();
+ }
if (currentGui != this->getCurrentGui())
return;
handled = handled | currentGui->handleInput(keysDown, keysHeld, touchPos, joyStickPosLeft, joyStickPosRight);
- if (!handled) {
- if (keysDown & KEY_UP)
- currentGui->requestFocus(currentFocus->getParent(), FocusDirection::Up);
- else if (keysDown & KEY_DOWN)
- currentGui->requestFocus(currentFocus->getParent(), FocusDirection::Down);
- else if (keysDown & KEY_LEFT)
- currentGui->requestFocus(currentFocus->getParent(), FocusDirection::Left);
- else if (keysDown & KEY_RIGHT)
- currentGui->requestFocus(currentFocus->getParent(), FocusDirection::Right);
- else if (keysDown & KEY_B)
- this->goBack();
+ if (!handled && currentFocus != nullptr) {
+ static bool shouldShake = true;
+
+ if ((((keysHeld & KEY_UP) != 0) + ((keysHeld & KEY_DOWN) != 0) + ((keysHeld & KEY_LEFT) != 0) + ((keysHeld & KEY_RIGHT) != 0)) == 1) {
+ if ((repeatTick == 0 || repeatTick > 20) && (repeatTick % 4) == 0) {
+ if (keysHeld & KEY_UP)
+ currentGui->requestFocus(currentFocus->getParent(), FocusDirection::Up, shouldShake);
+ else if (keysHeld & KEY_DOWN)
+ currentGui->requestFocus(currentFocus->getParent(), FocusDirection::Down, shouldShake);
+ else if (keysHeld & KEY_LEFT)
+ currentGui->requestFocus(currentFocus->getParent(), FocusDirection::Left, shouldShake);
+ else if (keysHeld & KEY_RIGHT)
+ currentGui->requestFocus(currentFocus->getParent(), FocusDirection::Right, shouldShake);
+
+ shouldShake = currentGui->getFocusedElement() != currentFocus;
+ }
+ repeatTick++;
+ } else {
+ if (keysDown & KEY_B)
+ this->goBack();
+ repeatTick = 0;
+ shouldShake = true;
+ }
+ }
+
+ if (!touchDetected && oldTouchDetected) {
+ if (currentGui != nullptr && topElement != nullptr)
+ topElement->onTouch(elm::TouchEvent::Release, oldTouchPos.px, oldTouchPos.py, oldTouchPos.px, oldTouchPos.py, initialTouchPos.px, initialTouchPos.py);
+ }
+
+ if (touchDetected) {
+
+ u32 xDistance = std::abs(static_cast(initialTouchPos.px) - static_cast(touchPos.px));
+ u32 yDistance = std::abs(static_cast(initialTouchPos.py) - static_cast(touchPos.py));
+
+ xDistance *= xDistance;
+ yDistance *= yDistance;
+
+ if ((xDistance + yDistance) > 1000) {
+ elm::Element::setInputMode(InputMode::TouchScroll);
+ touchEvent = elm::TouchEvent::Scroll;
+ } else {
+ if (touchEvent != elm::TouchEvent::Scroll)
+ touchEvent = elm::TouchEvent::Hold;
+ }
+
+ if (!oldTouchDetected) {
+ initialTouchPos = touchPos;
+ elm::Element::setInputMode(InputMode::Touch);
+ currentGui->removeFocus();
+ touchEvent = elm::TouchEvent::Touch;
+ }
+
+
+ if (currentGui != nullptr && topElement != nullptr)
+ topElement->onTouch(touchEvent, touchPos.px, touchPos.py, oldTouchPos.px, oldTouchPos.py, initialTouchPos.px, initialTouchPos.py);
+
+ oldTouchPos = touchPos;
+
+ // Hide overlay when touching out of bounds
+ if (touchPos.px >= cfg::FramebufferWidth) {
+ if (tsl::elm::Element::getInputMode() == tsl::InputMode::Touch) {
+ oldTouchPos = { 0 };
+ initialTouchPos = { 0 };
+
+ this->hide();
+ }
+ }
+ } else {
+ if (oldTouchPos.px < 150U && oldTouchPos.py > cfg::FramebufferHeight - 73U)
+ if (initialTouchPos.px < 150U && initialTouchPos.py > cfg::FramebufferHeight - 73U)
+ this->goBack();
+
+ elm::Element::setInputMode(InputMode::Controller);
+
+ oldTouchPos = { 0 };
+ initialTouchPos = { 0 };
}
+
+ oldTouchDetected = touchDetected;
}
/**
@@ -1918,25 +3072,6 @@ namespace tsl {
this->m_disableNextAnimation = true;
}
- /**
- * @brief Creates a new Gui and changes to it
- *
- * @tparam G Gui to create
- * @tparam Args Arguments to pass to the Gui
- * @param args Arguments to pass to the Gui
- * @return Reference to the newly created Gui
- */
- template
- std::unique_ptr& changeTo(Args&&... args) {
- auto newGui = std::make_unique(std::forward(args)...);
- newGui->m_topElement = newGui->createUI();
- newGui->requestFocus(newGui->m_topElement, FocusDirection::None);
-
- this->m_guiStack.push(std::move(newGui));
-
- return this->m_guiStack.top();
- }
-
/**
* @brief Changes to a different Gui
*
@@ -1945,13 +3080,25 @@ namespace tsl {
*/
std::unique_ptr& changeTo(std::unique_ptr&& gui) {
gui->m_topElement = gui->createUI();
- gui->requestFocus(gui->m_topElement, FocusDirection::None);
this->m_guiStack.push(std::move(gui));
return this->m_guiStack.top();
}
+ /**
+ * @brief Creates a new Gui and changes to it
+ *
+ * @tparam G Gui to create
+ * @tparam Args Arguments to pass to the Gui
+ * @param args Arguments to pass to the Gui
+ * @return Reference to the newly created Gui
+ */
+ template
+ std::unique_ptr& changeTo(Args&&... args) {
+ return this->changeTo(std::make_unique(std::forward(args)...));
+ }
+
/**
* @brief Pops the top Gui from the stack and goes back to the last one
* @note The Overlay gets closes once there are no more Guis on the stack
@@ -1992,7 +3139,6 @@ namespace tsl {
Event comboEvent = { 0 }, homeButtonPressEvent = { 0 }, powerButtonPressEvent = { 0 };
- u64 launchCombo = KEY_L | KEY_DDOWN | KEY_RSTICK;
bool overlayOpen = false;
std::mutex dataMutex;
@@ -2000,45 +3146,36 @@ namespace tsl {
u64 keysDownPending = 0;
u64 keysHeld = 0;
touchPosition touchPos = { 0 };
+ u32 touchCount = 0;
JoystickPosition joyStickPosLeft = { 0 }, joyStickPosRight = { 0 };
};
/**
- * @brief Parses the Tesla settings
+ * @brief Extract values from Tesla settings file
*
- * @param[out] launchCombo Overlay launch button combo
+ * @param[out] keyCombo Overlay launch button combo
*/
- static void parseOverlaySettings(u64 &launchCombo) {
- /* Open Sd card filesystem. */
- FsFileSystem fsSdmc;
- if(R_FAILED(fsOpenSdCardFileSystem(&fsSdmc)))
- return;
- hlp::ScopeGuard fsGuard([&] { fsFsClose(&fsSdmc); });
-
- /* Open config file. */
- FsFile fileConfig;
- if (R_FAILED(fsFsOpenFile(&fsSdmc, "/config/tesla/config.ini", FsOpenMode_Read, &fileConfig)))
- return;
- hlp::ScopeGuard fileGuard([&] { fsFileClose(&fileConfig); });
-
- /* Get config file size. */
- s64 configFileSize;
- if (R_FAILED(fsFileGetSize(&fileConfig, &configFileSize)))
- return;
-
- /* Read and parse config file. */
- std::string configFileData(configFileSize, '\0');
- u64 readSize;
- Result rc = fsFileRead(&fileConfig, 0, configFileData.data(), configFileSize, FsReadOption_None, &readSize);
- if (R_FAILED(rc) || readSize != static_cast(configFileSize))
- return;
+ static void parseOverlaySettings(u64 &keyCombo) {
+ hlp::ini::IniData parsedConfig = hlp::ini::readOverlaySettings();
- hlp::ini::IniData parsedConfig = hlp::ini::parseIni(configFileData);
+ u64 decodedKeys = hlp::comboStringToKeys(parsedConfig["tesla"]["key_combo"]);
+ if (decodedKeys)
+ keyCombo = decodedKeys;
+ }
- launchCombo = 0x00;
- for (std::string key : hlp::split(parsedConfig["tesla"]["key_combo"], '+'))
- launchCombo |= hlp::stringToKeyCode(key);
+ /**
+ * @brief Update and save launch combo keys
+ *
+ * @param keys the new combo keys
+ */
+ [[maybe_unused]] static void updateCombo(u64 keys) {
+ tsl::cfg::launchCombo = keys;
+ hlp::ini::updateOverlaySettings({
+ { "tesla", {
+ { "key_combo", tsl::hlp::keysToComboString(keys) }
+ }}
+ });
}
/**
@@ -2052,7 +3189,7 @@ namespace tsl {
SharedThreadData *shData = static_cast(args);
// Parse Tesla settings
- impl::parseOverlaySettings(shData->launchCombo);
+ impl::parseOverlaySettings(tsl::cfg::launchCombo);
// Drop all inputs from the previous overlay
hidScanInput();
@@ -2068,31 +3205,54 @@ namespace tsl {
shData->keysDown = 0;
shData->keysHeld = 0;
+ shData->joyStickPosLeft = { 0 };
+ shData->joyStickPosRight = { 0 };
// Combine input from all controllers
for (u8 controller = 0; controller < 8; controller++) {
if (hidIsControllerConnected(static_cast(controller))) {
shData->keysDown |= hidKeysDown(static_cast(controller));
shData->keysHeld |= hidKeysHeld(static_cast(controller));
+
+ JoystickPosition joyStickPosLeft, joyStickPosRight;
+ hidJoystickRead(&joyStickPosLeft, static_cast(controller), HidControllerJoystick::JOYSTICK_LEFT);
+ hidJoystickRead(&joyStickPosRight, static_cast(controller), HidControllerJoystick::JOYSTICK_RIGHT);
+
+ if (joyStickPosLeft.dx > 0 && joyStickPosLeft.dx > shData->joyStickPosLeft.dx)
+ shData->joyStickPosLeft.dx = joyStickPosLeft.dx;
+ if (joyStickPosLeft.dx < 0 && joyStickPosLeft.dx < shData->joyStickPosLeft.dx)
+ shData->joyStickPosLeft.dx = joyStickPosLeft.dx;
+ if (joyStickPosLeft.dy > 0 && joyStickPosLeft.dy > shData->joyStickPosLeft.dy)
+ shData->joyStickPosLeft.dy = joyStickPosLeft.dy;
+ if (joyStickPosLeft.dy < 0 && joyStickPosLeft.dy < shData->joyStickPosLeft.dy)
+ shData->joyStickPosLeft.dy = joyStickPosLeft.dy;
+ if (joyStickPosRight.dx > 0 && joyStickPosRight.dx > shData->joyStickPosRight.dx)
+ shData->joyStickPosRight.dx = joyStickPosRight.dx;
+ if (joyStickPosRight.dx < 0 && joyStickPosRight.dx < shData->joyStickPosRight.dx)
+ shData->joyStickPosRight.dx = joyStickPosRight.dx;
+ if (joyStickPosRight.dy > 0 && joyStickPosRight.dy > shData->joyStickPosRight.dy)
+ shData->joyStickPosRight.dy = joyStickPosRight.dy;
+ if (joyStickPosRight.dy < 0 && joyStickPosRight.dy < shData->joyStickPosRight.dy)
+ shData->joyStickPosRight.dy = joyStickPosRight.dy;
}
}
if (hidIsControllerConnected(CONTROLLER_HANDHELD)) {
shData->keysDown |= hidKeysDown(CONTROLLER_HANDHELD);
shData->keysHeld |= hidKeysHeld(CONTROLLER_HANDHELD);
+
+ hidJoystickRead(&shData->joyStickPosLeft, CONTROLLER_HANDHELD, HidControllerJoystick::JOYSTICK_LEFT);
+ hidJoystickRead(&shData->joyStickPosRight, CONTROLLER_HANDHELD, HidControllerJoystick::JOYSTICK_RIGHT);
}
// Read in touch positions
- if (hidTouchCount() > 0)
+ shData->touchCount = hidTouchCount();
+ if (shData->touchCount > 0)
hidTouchRead(&shData->touchPos, 0);
else
shData->touchPos = { 0 };
- // Read in joystick values
- hidJoystickRead(&shData->joyStickPosLeft, CONTROLLER_HANDHELD, HidControllerJoystick::JOYSTICK_LEFT);
- hidJoystickRead(&shData->joyStickPosRight, CONTROLLER_HANDHELD, HidControllerJoystick::JOYSTICK_RIGHT);
-
- if (((shData->keysHeld & shData->launchCombo) == shData->launchCombo) && shData->keysDown & shData->launchCombo) {
+ if (((shData->keysHeld & tsl::cfg::launchCombo) == tsl::cfg::launchCombo) && shData->keysDown & tsl::cfg::launchCombo) {
if (shData->overlayOpen) {
tsl::Overlay::get()->hide();
shData->overlayOpen = false;
@@ -2101,18 +3261,11 @@ namespace tsl {
eventFire(&shData->comboEvent);
}
- if (shData->touchPos.px >= cfg::FramebufferWidth && shData->overlayOpen) {
- if (shData->overlayOpen) {
- tsl::Overlay::get()->hide();
- shData->overlayOpen = false;
- }
- }
-
shData->keysDownPending |= shData->keysDown;
}
//20 ms
- svcSleepThread(20E6);
+ svcSleepThread(20'000'000ul);
}
}
@@ -2191,7 +3344,7 @@ namespace tsl {
Overlay::get()->goBack();
}
- static void setNextOverlay(std::string ovlPath, std::string args) {
+ static void setNextOverlay(const std::string& ovlPath, std::string args) {
args += " --skipCombo";
@@ -2219,7 +3372,7 @@ namespace tsl {
shData.running = true;
Thread hidPollerThread, homeButtonDetectorThread, powerButtonDetectorThread;
- threadCreate(&hidPollerThread, impl::hidInputPoller, &shData, nullptr, 0x1000, 0x2C, -2);
+ threadCreate(&hidPollerThread, impl::hidInputPoller, &shData, nullptr, 0x1000, 0x10, -2);
threadCreate(&homeButtonDetectorThread, impl::homeButtonDetector, &shData, nullptr, 0x1000, 0x2C, -2);
threadCreate(&powerButtonDetectorThread, impl::powerButtonDetector, &shData, nullptr, 0x1000, 0x2C, -2);
threadStart(&hidPollerThread);
@@ -2259,6 +3412,7 @@ namespace tsl {
overlay->show();
overlay->clearScreen();
+
while (shData.running) {
overlay->loop();
@@ -2266,7 +3420,7 @@ namespace tsl {
{
std::scoped_lock lock(shData.dataMutex);
if (!overlay->fadeAnimationPlaying()) {
- overlay->handleInput(shData.keysDownPending, shData.keysHeld, shData.touchPos, shData.joyStickPosLeft, shData.joyStickPosRight);
+ overlay->handleInput(shData.keysDownPending, shData.keysHeld, shData.touchCount > 0, shData.touchPos, shData.joyStickPosLeft, shData.joyStickPosRight);
}
shData.keysDownPending = 0;
}
@@ -2319,7 +3473,7 @@ namespace tsl::cfg {
u16 LayerPosY = 0;
u16 FramebufferWidth = 0;
u16 FramebufferHeight = 0;
-
+ u64 launchCombo = KEY_L | KEY_DDOWN | KEY_RSTICK;
}
extern "C" {
diff --git a/source/constants.h b/source/constants.h
index 354ccd8..bebb277 100644
--- a/source/constants.h
+++ b/source/constants.h
@@ -51,4 +51,8 @@ const unsigned char mouse_map_disabled[]={0x04, 0x00, 0x00, 0x14};*/
const unsigned char recording_enabled[]={0x1F, 0x20, 0x03, 0xD5};
const unsigned char recording_disabled[]={0x41, 0x00, 0x00, 0x36};
+// Hide PV watermark
+// Offset found by M&M
+#define PVMARK_OFFSET 0x97981D
+
#endif // CONSTANTS_H
diff --git a/source/main.cpp b/source/main.cpp
index befaf95..be8f549 100644
--- a/source/main.cpp
+++ b/source/main.cpp
@@ -15,6 +15,15 @@ static bool debug_mode_enabled=false;
static bool dpad_cursor=false;
static float opacity=1.0f;
+void hidepvmark_toggle(bool state)
+{
+ unsigned char buf;
+ if(state) buf='\0';
+ else buf='p';
+ dmntchtWriteCheatProcessMemory(metadata.main_nso_extents.base + PVMARK_OFFSET, &buf, sizeof(buf));
+ return;
+}
+
void force_test_mode(bool state)
{
if(state) MINITLAC_injectGameSubState(metadata.main_nso_extents.base, 4, 29);
@@ -234,7 +243,7 @@ class M39OLGUI : public tsl::Gui {
// Called when this Gui gets loaded to create the UI
// Allocate all your elements on the heap. libtesla will make sure to clean them up when not needed anymore
virtual tsl::elm::Element* createUI() override {
- auto rootFrame = new tsl::elm::OverlayFrame("MEGAHAKUS 2.3.0", "For MEGA39's 1.0.3");
+ auto rootFrame = new tsl::elm::OverlayFrame("MEGAHAKUS", "For MEGA39's 1.0.3");
auto list = new tsl::elm::List();
if (initialized&&debugService_isRunning()&&metadata.title_id==GAME_TITLE_ID&&bid_match())
{
@@ -363,6 +372,21 @@ class M39OLGUI : public tsl::Gui {
// Add item
list->addItem(enable_rec_itm);
}
+
+ // "Hide PV watermark"
+ // Get current state
+ unsigned char pvmbuffer;
+ if(!dmntchtReadCheatProcessMemory(metadata.main_nso_extents.base + PVMARK_OFFSET, &pvmbuffer, 1))
+ {
+ // Create item
+ auto *pvmark_itm = new tsl::elm::ToggleListItem("Hide PV watermark", pvmbuffer=='\0');
+
+ // Set listener function
+ pvmark_itm->setStateChangedListener(hidepvmark_toggle);
+
+ // Add item
+ list->addItem(pvmark_itm);
+ }
}
else
{