Cleanup PopMessage states & code.

This commit is contained in:
J-D-K 2025-11-10 12:26:34 -05:00
parent 5c582bf19d
commit e9dc9c9af8
3 changed files with 120 additions and 67 deletions

View File

@ -1,29 +1,27 @@
# JKSV Rewrite
<p align="center">
<a href="https://github.com/J-D-K/JKSV">
<img style="border-radius: 10px;" alt="Language" src="https://img.shields.io/github/languages/top/J-D-K/JKSV?style=flat-square">
<img alt="Language" src="https://img.shields.io/github/languages/top/J-D-K/JKSV?style=for-the-badge">
</a>
<a href="https://github.com/J-D-K/JKSV/stargazers">
<img style="border-radius: 10px;" alt="GitHub stars" src="https://img.shields.io/github/stars/J-D-K/JKSV?style=flat-square">
<img alt="GitHub stars" src="https://img.shields.io/github/stars/J-D-K/JKSV?style=for-the-badge">
</a>
<a href="https://github.com/J-D-K/JKSV/network/members">
<img style="border-radius: 10px;" alt="GitHub forks" src="https://img.shields.io/github/forks/J-D-K/JKSV?style=flat-square">
<img alt="GitHub forks" src="https://img.shields.io/github/forks/J-D-K/JKSV?style=for-the-badge">
</a>
<a href="https://github.com/J-D-K/JKSV/releases">
<img style="border-radius: 10px;" alt="GitHub downloads" src="https://img.shields.io/github/downloads/J-D-K/JKSV/total?style=flat-square">
<img alt="GitHub downloads" src="https://img.shields.io/github/downloads/J-D-K/JKSV/total?style=for-the-badge">
</a>
<a href="https://github.com/J-D-K/JKSV/releases/latest">
<img style="border-radius: 10px;" alt="GitHub release" src="https://img.shields.io/github/v/release/J-D-K/JKSV?style=flat-square">
<img alt="GitHub release" src="https://img.shields.io/github/v/release/J-D-K/JKSV?style=for-the-badge">
</a>
<a href="https://github.com/J-D-K/JKSV/blob/master/LICENSE">
<img style="border-radius: 10px;" alt="License" src="https://img.shields.io/github/license/J-D-K/JKSV?style=flat-square">
<img alt="License" src="https://img.shields.io/github/license/J-D-K/JKSV?style=for-the-badge">
</a>
<img src="https://i.imgur.com/aUrq46c.png" />
</p>
---
<p align="center">
If you appreciate my work, or if JKSM or JKSV has gotten you out of a jam, please consider supporting development through a donation. Any contribution is appreciated, but never required! Creating and maintaining these tools takes a lot of time and effort.
</p>
@ -33,20 +31,20 @@
## Rewritten from the ground up:
JKSV's rewrite aims to accomplish the following:
JKSV's rewrite makes improvements on the following:
- **Clean up and modernize the codebase:** The original master branch code became a mess over time and is extremely difficult to navigate and maintain.
- **Zero global variables:** Everything is encapsulated via interfaces. This alone makes the rewrite easier to work with and **much** less fragile in comparison to the original.
- **Fully encapsulated design:** Everything is encapsulated via interfaces. This alone makes the rewrite easier to work with and **much** less fragile in comparison to the original.
- **Improved error logging:** The master branch did an extremely poor job at logging errors. The rewrite corrects this greatly. Each error is logged with the file, line, and column making errors and bugs easier to track down.
- **Designed with translations in mind:** Unlike the original, translations weren't a feature that were tacked on later. This also made the original difficult to maintain.
- **Use standard formats:** JKSV originally used a custom file parser for data files. This has been removed in favor of JSON using libjson-c.
- **Uses [FsLib](https://github.com/J-D-K/FsLib):**
- All file operations are handled using FsLib. FsLib is a C++ wrapper I wrote around libnx's FS API.
- FsLib also handles SD card redirection so libraries that depend on C standard I/O can read and write files to and from the SD card.
- **More convenient Google Drive login.**
- **Streamlined Google Drive login.**
- **Improve WebDav Support and code.**
- **Title Cache:** Titles found on the system are cached the same way [JKSM](https://github.com/J-D-K/JKSM) was. This improves boot time greatly on higher Switch firmware versions.
- The title cache is also **auto-invalidating**. No need to manually update it like JKSM on 3DS.
- **_Proper_ use of C++ features:**
- **Modern C++ design patterns and resource management:**
- **Polymorphism and state patterns** instead of scattered global variables and logic.
- **Smart pointers** to enure no memory leaks can occur.
- **C APIs wrapped in smart pointers and classes** to ensure proper cleanup.

View File

@ -29,6 +29,17 @@ namespace ui
std::string_view get_message() const noexcept;
private:
/// @brief States the message will be in.
enum class State : uint8_t
{
Rising,
Opening,
Displaying,
Closing,
Dropping,
Finished
};
/// @brief Transition for the pop up/pop out thing.
ui::Transition m_transition{};
@ -38,18 +49,12 @@ namespace ui
/// @brief This stores the message for safe keeping.
std::string m_message{};
/// @brief Current rendering coordinate for the text.
/// @brief Current state the message is in.
PopMessage::State m_state{};
/// @brief Current X rendering coordinate for the text.
int m_textX{};
/// @brief Whether or not the targetY coordinate was met.
bool m_yMet{};
/// @brief Whether
bool m_close{};
/// @brief Returns whether or not the message has reached the end of its life.
bool m_finished{};
/// @brief The current offset of the substr.
int m_substrOffset{};
@ -66,7 +71,7 @@ namespace ui
void initialize_static_members();
/// @brief Updates the Y Coord to match the target passed.
void update_y(double targetY) noexcept;
void update_y() noexcept;
/// @brief Updates the current end offset of the text.
void update_text_offset();
@ -74,10 +79,10 @@ namespace ui
/// @brief Updates the width of the dialog.
void update_width() noexcept;
/// @brief Updates the display timer and begins the closing process of the message.
void update_display_timer() noexcept;
/// @brief Renders the container around the message.
void render_container() noexcept;
/// @brief Signals the pop message to close.
void close() noexcept;
};
}

View File

@ -11,6 +11,7 @@ namespace
constexpr int START_X = 624;
constexpr int START_WIDTH = 20;
constexpr int PERMA_HEIGHT = 48;
constexpr int FONT_SIZE = 22;
constexpr int TRANSITION_THRESHOLD = 2; // This is used so the easing on opening doesn't look so odd.
}
@ -20,7 +21,9 @@ ui::PopMessage::PopMessage(int ticks, std::string_view message)
: m_transition(START_X, graphics::SCREEN_HEIGHT, START_WIDTH, PERMA_HEIGHT, 0, 0, 0, 0, TRANSITION_THRESHOLD)
, m_ticks(ticks)
, m_message(message)
, m_state(State::Rising)
{
// This will load the end cap graphics if they haven't been already.
PopMessage::initialize_static_members();
}
@ -28,6 +31,7 @@ ui::PopMessage::PopMessage(int ticks, std::string &message)
: m_transition(START_X, graphics::SCREEN_HEIGHT, START_WIDTH, PERMA_HEIGHT, 0, 0, 0, 0, TRANSITION_THRESHOLD)
, m_ticks(ticks)
, m_message(std::move(message))
, m_state(State::Rising)
{
PopMessage::initialize_static_members();
}
@ -36,26 +40,40 @@ ui::PopMessage::PopMessage(int ticks, std::string &message)
void ui::PopMessage::update(double targetY)
{
PopMessage::update_y(targetY);
PopMessage::update_text_offset();
PopMessage::update_width();
// Always update the targetY unless the message is dropping. This is so messages collapse properly.
if (m_state != State::Dropping) { m_transition.set_target_y(targetY); }
if (m_close && m_transition.in_place()) { m_finished = true; }
else if (m_displayTimer.is_triggered()) { PopMessage::close(); }
// Update the current state.
switch (m_state)
{
case State::Rising: PopMessage::update_y(); break;
case State::Opening:
{
PopMessage::update_text_offset();
PopMessage::update_width();
}
break;
case State::Displaying: PopMessage::update_display_timer(); break;
case State::Closing: PopMessage::update_width(); break;
case State::Dropping: PopMessage::update_y(); break;
default: break;
}
}
void ui::PopMessage::render()
{
PopMessage::render_container();
if (!m_yMet || m_close) { return; }
// Don't continue unless the message is in place or it's not in its closing state.
if (m_state != State::Opening && m_state != State::Displaying) { return; }
// This avoids allocating and returning another std::string.
const int y = m_transition.get_y();
const std::string_view message(m_message.c_str(), m_substrOffset);
sdl::text::render(sdl::Texture::Null, m_textX, y + 11, 22, sdl::text::NO_WRAP, colors::BLACK, message);
const std::string_view message{m_message.c_str(), static_cast<size_t>(m_substrOffset)};
sdl::text::render(sdl::Texture::Null, m_textX, y + 11, FONT_SIZE, sdl::text::NO_WRAP, colors::BLACK, message);
}
bool ui::PopMessage::finished() const noexcept { return m_finished; }
bool ui::PopMessage::finished() const noexcept { return m_state == State::Finished; }
std::string_view ui::PopMessage::get_message() const noexcept { return m_message; }
@ -71,67 +89,99 @@ void ui::PopMessage::initialize_static_members()
sm_endCaps = sdl::TextureManager::load(TEX_CAP_NAME, TEX_CAP_PATH);
}
void ui::PopMessage::update_y(double targetY) noexcept
void ui::PopMessage::update_y() noexcept
{
if (!m_close) { m_transition.set_target_y(targetY); };
// This is the tick count for typing out the message.
static constexpr uint64_t TYPE_TICK_COUNT = 5;
// Update the transition's Y position.
m_transition.update_xy();
if (!m_yMet && m_transition.in_place_xy())
// Update the state if needed.
const bool transitionUpFinished = m_state == State::Rising && m_transition.in_place_xy();
const bool transitionDownFinished = m_state == State::Dropping && m_transition.in_place_xy();
if (transitionUpFinished)
{
m_yMet = true;
m_typeTimer.start(5);
m_state = State::Opening;
m_typeTimer.start(TYPE_TICK_COUNT);
}
else if (transitionDownFinished) { m_state = State::Finished; }
}
void ui::PopMessage::update_text_offset()
{
static constexpr int HALF_WIDTH = 640;
// This is the center of the screen for centering.
static constexpr int SCREEN_CENTER = 640;
static constexpr int CONTAINER_PADDING = 32;
// Get the entire length of the message and do not continue unless the timer is triggered and we still have ground to
// cover.
const int messageLength = m_message.length();
if (!m_yMet || m_substrOffset >= messageLength || !m_typeTimer.is_triggered()) { return; }
if (!m_typeTimer.is_triggered() || m_substrOffset >= messageLength) { return; }
// This is slightly more technical than I originally thought, but I liked the effect so.
// This needs to be slightly more technical since JKSV supports UTF-8.
uint32_t codepoint{};
const char *message = m_message.c_str();
const uint8_t *targetPoint = reinterpret_cast<const uint8_t *>(&message[m_substrOffset]);
const uint8_t *targetPoint = reinterpret_cast<const uint8_t *>(&m_message[m_substrOffset]);
const ssize_t unitCount = decode_utf8(&codepoint, targetPoint);
if (unitCount <= 0) { return; }
// Add the unit count to the current offset.
m_substrOffset += unitCount;
const std::string_view subMessage(message, m_substrOffset);
const int stringWidth = sdl::text::get_width(22, subMessage);
m_textX = HALF_WIDTH - (stringWidth / 2);
const int dialogWidth = stringWidth + 32;
m_transition.set_target_width(dialogWidth);
if (m_substrOffset >= messageLength) { m_displayTimer.start(m_ticks); }
// Get the substring and calculate the updated width and X of the message.
const std::string_view subString{m_message.c_str(), static_cast<size_t>(m_substrOffset)};
const int subWidth = sdl::text::get_width(FONT_SIZE, subString);
const int containerWidth = subWidth + CONTAINER_PADDING;
m_textX = SCREEN_CENTER - (subWidth / 2);
// Update the target width of the transition.
m_transition.set_target_width(containerWidth);
// If we've hit the end of the message, start the time for displaying the message.
if (m_substrOffset >= messageLength)
{
m_state = State::Displaying;
m_displayTimer.start(m_ticks);
}
}
void ui::PopMessage::update_width() noexcept
{
if (!m_yMet) { return; }
// Update the width of the container.
m_transition.update_width_height();
if (m_close && m_transition.in_place_width_height()) { m_transition.set_target_y(graphics::SCREEN_HEIGHT); }
// To do: Not sure if I like this here, but if we're in the closing state and the transition is in place width-wise, set the
// target Y to the bottom of the screen.
if (m_state == State::Closing && m_transition.in_place_width_height())
{
m_state = State::Dropping;
m_transition.set_target_y(graphics::SCREEN_HEIGHT);
}
}
void ui::PopMessage::update_display_timer() noexcept
{
// If the timer is triggered, set the target width to the closing width and change the state to closing.
if (m_displayTimer.is_triggered())
{
m_state = State::Closing;
m_transition.set_target_width(START_WIDTH);
}
}
void ui::PopMessage::render_container() noexcept
{
constexpr int WIDTH = 48;
constexpr int HALF = WIDTH / 2;
// The width of the CAP graphics.
constexpr int CAP_WIDTH = 48;
constexpr int CAP_HALF = CAP_WIDTH / 2;
const int x = m_transition.get_centered_x() - (HALF / 2);
// Calc render coordinates.
const int x = m_transition.get_centered_x() - (CAP_HALF / 2);
const int y = m_transition.get_y();
const int width = m_transition.get_width() + HALF;
const int width = m_transition.get_width() + CAP_HALF;
sm_endCaps->render_part(sdl::Texture::Null, x, y, 0, 0, HALF, WIDTH);
sdl::render_rect_fill(sdl::Texture::Null, x + HALF, y, width - WIDTH, 48, colors::DIALOG_LIGHT);
sm_endCaps->render_part(sdl::Texture::Null, x + (width - HALF), y, HALF, 0, HALF, 48);
// Render the container.
sm_endCaps->render_part(sdl::Texture::Null, x, y, 0, 0, CAP_HALF, CAP_WIDTH);
sdl::render_rect_fill(sdl::Texture::Null, x + CAP_HALF, y, width - CAP_WIDTH, 48, colors::DIALOG_LIGHT);
sm_endCaps->render_part(sdl::Texture::Null, x + (width - CAP_HALF), y, CAP_HALF, 0, CAP_HALF, 48);
}
void ui::PopMessage::close() noexcept
{
m_close = true;
m_transition.set_target_width(START_WIDTH);
}