JKSV/source/ui/PopMessage.cpp
2025-11-10 12:26:34 -05:00

188 lines
6.6 KiB
C++

#include "ui/PopMessage.hpp"
#include "config/config.hpp"
#include "graphics/colors.hpp"
#include "graphics/screen.hpp"
#include "mathutil.hpp"
#include "sdl.hpp"
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.
}
// ---- Construction ----
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();
}
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();
}
// ---- Public functions ----
void ui::PopMessage::update(double targetY)
{
// 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); }
// 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();
// 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(), 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_state == State::Finished; }
std::string_view ui::PopMessage::get_message() const noexcept { return m_message; }
// ---- Private functions ----
void ui::PopMessage::initialize_static_members()
{
static constexpr std::string_view TEX_CAP_NAME = "PopCaps";
static constexpr const char *TEX_CAP_PATH = "romfs:/Textures/PopMessage.png";
if (sm_endCaps) { return; }
sm_endCaps = sdl::TextureManager::load(TEX_CAP_NAME, TEX_CAP_PATH);
}
void ui::PopMessage::update_y() noexcept
{
// 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();
// 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_state = State::Opening;
m_typeTimer.start(TYPE_TICK_COUNT);
}
else if (transitionDownFinished) { m_state = State::Finished; }
}
void ui::PopMessage::update_text_offset()
{
// 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_typeTimer.is_triggered() || m_substrOffset >= messageLength) { return; }
// This needs to be slightly more technical since JKSV supports UTF-8.
uint32_t codepoint{};
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;
// 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
{
// Update the width of the container.
m_transition.update_width_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
{
// The width of the CAP graphics.
constexpr int CAP_WIDTH = 48;
constexpr int CAP_HALF = CAP_WIDTH / 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() + CAP_HALF;
// 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);
}