diff --git a/sphaira/CMakeLists.txt b/sphaira/CMakeLists.txt index e4ca6bc..d8617d0 100644 --- a/sphaira/CMakeLists.txt +++ b/sphaira/CMakeLists.txt @@ -62,6 +62,7 @@ add_executable(sphaira source/ui/widget.cpp source/ui/list.cpp source/ui/bubbles.cpp + source/ui/scrolling_text.cpp source/app.cpp source/download.cpp diff --git a/sphaira/include/ui/menus/appstore.hpp b/sphaira/include/ui/menus/appstore.hpp index 0ca4924..cdad5d2 100644 --- a/sphaira/include/ui/menus/appstore.hpp +++ b/sphaira/include/ui/menus/appstore.hpp @@ -2,6 +2,7 @@ #include "ui/menus/menu_base.hpp" #include "ui/scrollable_text.hpp" +#include "ui/scrolling_text.hpp" #include "ui/list.hpp" #include "nro.hpp" #include "fs.hpp" @@ -169,6 +170,10 @@ private: std::vector m_entries_index_search{}; std::span m_entries_current{}; + ScrollingText m_scroll_name{}; + ScrollingText m_scroll_author{}; + ScrollingText m_scroll_version{}; + Filter m_filter{Filter::Filter_All}; SortType m_sort{SortType::SortType_Updated}; OrderType m_order{OrderType::OrderType_Descending}; diff --git a/sphaira/include/ui/menus/game_menu.hpp b/sphaira/include/ui/menus/game_menu.hpp index e46954b..4de03aa 100644 --- a/sphaira/include/ui/menus/game_menu.hpp +++ b/sphaira/include/ui/menus/game_menu.hpp @@ -1,6 +1,7 @@ #pragma once #include "ui/menus/menu_base.hpp" +#include "ui/scrolling_text.hpp" #include "ui/list.hpp" #include "fs.hpp" #include "option.hpp" @@ -57,6 +58,10 @@ private: std::vector m_entries{}; s64 m_index{}; // where i am in the array std::unique_ptr m_list{}; + + ScrollingText m_scroll_name{}; + ScrollingText m_scroll_author{}; + ScrollingText m_scroll_version{}; }; } // namespace sphaira::ui::menu::game diff --git a/sphaira/include/ui/menus/homebrew.hpp b/sphaira/include/ui/menus/homebrew.hpp index bfc9891..4255085 100644 --- a/sphaira/include/ui/menus/homebrew.hpp +++ b/sphaira/include/ui/menus/homebrew.hpp @@ -1,6 +1,7 @@ #pragma once #include "ui/menus/menu_base.hpp" +#include "ui/scrolling_text.hpp" #include "ui/list.hpp" #include "nro.hpp" #include "fs.hpp" @@ -56,6 +57,10 @@ private: s64 m_index{}; // where i am in the array std::unique_ptr m_list{}; + ScrollingText m_scroll_name{}; + ScrollingText m_scroll_author{}; + ScrollingText m_scroll_version{}; + option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_AlphabeticalStar}; option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending}; option::OptionBool m_hide_sphaira{INI_SECTION, "hide_sphaira", false}; diff --git a/sphaira/include/ui/menus/themezer.hpp b/sphaira/include/ui/menus/themezer.hpp index a92c338..aa6a43f 100644 --- a/sphaira/include/ui/menus/themezer.hpp +++ b/sphaira/include/ui/menus/themezer.hpp @@ -2,6 +2,7 @@ #include "ui/menus/menu_base.hpp" #include "ui/scrollable_text.hpp" +#include "ui/scrolling_text.hpp" #include "ui/list.hpp" #include "option.hpp" #include @@ -160,6 +161,9 @@ private: s64 m_index{}; // where i am in the array std::unique_ptr m_list{}; + ScrollingText m_scroll_name{}; + ScrollingText m_scroll_author{}; + // options option::OptionLong m_sort{INI_SECTION, "sort", 0}; option::OptionLong m_order{INI_SECTION, "order", 0}; diff --git a/sphaira/include/ui/scrolling_text.hpp b/sphaira/include/ui/scrolling_text.hpp new file mode 100644 index 0000000..ec526da --- /dev/null +++ b/sphaira/include/ui/scrolling_text.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "ui/widget.hpp" +#include + +namespace sphaira::ui { + +struct ScrollingText final { +public: + void Draw(NVGcontext*, bool focus, float x, float y, float w, float size, int align, const NVGcolor& colour, const std::string& text_entry); + void DrawArgs(NVGcontext*, bool focus, float x, float y, float w, float size, int align, const NVGcolor& colour, const char* s, ...) __attribute__ ((format (printf, 10, 11))); + +private: + std::string m_str; + s64 m_tick; + float m_text_xoff; +}; + +} // namespace sphaira::ui diff --git a/sphaira/source/ui/menus/appstore.cpp b/sphaira/source/ui/menus/appstore.cpp index dab06f2..31ce733 100644 --- a/sphaira/source/ui/menus/appstore.cpp +++ b/sphaira/source/ui/menus/appstore.cpp @@ -1027,7 +1027,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { } auto text_id = ThemeEntryID_TEXT; - if (pos == m_index) { + const auto selected = pos == m_index; + if (selected) { text_id = ThemeEntryID_TEXT_SELECTED; gfx::drawRectOutline(vg, theme, 4.f, v); } else { @@ -1040,13 +1041,16 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { DrawIcon(vg, e.image, m_default_image, x + 20, y + 20, 115, 115, true, image_scale); // gfx::drawImage(vg, x + 20, y + 20, image_size, image_size_h, image.image ? image.image : m_default_image); + const auto text_off = 148; + const auto text_x = x + text_off; + const auto text_clip_w = w - 30.f - text_off; nvgSave(vg); - nvgIntersectScissor(vg, v.x, v.y, w - 30.f, h); // clip + nvgIntersectScissor(vg, text_x, y, text_clip_w, h); // clip { const float font_size = 18; - gfx::drawTextArgs(vg, x + 148, y + 45, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.title.c_str()); - gfx::drawTextArgs(vg, x + 148, y + 80, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.author.c_str()); - gfx::drawTextArgs(vg, x + 148, y + 115, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.version.c_str()); + m_scroll_name.Draw(vg, selected, text_x, y + 45, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.title.c_str()); + m_scroll_author.Draw(vg, selected, text_x, y + 80, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.author.c_str()); + m_scroll_version.Draw(vg, selected, text_x, y + 115, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.version.c_str()); } nvgRestore(vg); diff --git a/sphaira/source/ui/menus/filebrowser.cpp b/sphaira/source/ui/menus/filebrowser.cpp index 8a782e0..c6d4fad 100644 --- a/sphaira/source/ui/menus/filebrowser.cpp +++ b/sphaira/source/ui/menus/filebrowser.cpp @@ -734,7 +734,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { } auto text_id = ThemeEntryID_TEXT; - if (m_index == i) { + const auto selected = m_index == i; + if (selected) { text_id = ThemeEntryID_TEXT_SELECTED; gfx::drawRectOutline(vg, theme, 4.f, v); } else { diff --git a/sphaira/source/ui/menus/game_menu.cpp b/sphaira/source/ui/menus/game_menu.cpp index d56161d..7bb73df 100644 --- a/sphaira/source/ui/menus/game_menu.cpp +++ b/sphaira/source/ui/menus/game_menu.cpp @@ -133,23 +133,6 @@ Menu::Menu() : MenuBase{"Games"_i18n} { }, m_entries[m_index].image )); }, true)); - - #if 0 - options->Add(std::make_shared("Enable auto delete"_i18n, [this](){ - const auto rc = nsEnableApplicationAutoDelete(m_entries[m_index].app_id); - Notify(rc, "Failed to enable auto delete"); - })); - - options->Add(std::make_shared("Disable auto delete"_i18n, [this](){ - const auto rc = nsDisableApplicationAutoDelete(m_entries[m_index].app_id); - Notify(rc, "Failed to disable auto delete"); - })); - - options->Add(std::make_shared("Withdraw update request"_i18n, [this](){ - const auto rc = nsWithdrawApplicationUpdateRequest(m_entries[m_index].app_id); - Notify(rc, "Failed to withdraw update request"); - })); - #endif } }}) ); @@ -200,7 +183,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { } auto text_id = ThemeEntryID_TEXT; - if (pos == m_index) { + const auto selected = pos == m_index; + if (selected) { text_id = ThemeEntryID_TEXT_SELECTED; gfx::drawRectOutline(vg, theme, 4.f, v); } else { @@ -210,13 +194,16 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { const float image_size = 115; gfx::drawImage(vg, x + 20, y + 20, image_size, image_size, e.image ? e.image : App::GetDefaultImage(), 5); + const auto text_off = 148; + const auto text_x = x + text_off; + const auto text_clip_w = w - 30.f - text_off; nvgSave(vg); - nvgIntersectScissor(vg, x, y, w - 30.f, h); // clip + nvgIntersectScissor(vg, text_x, y, text_clip_w, h); // clip { const float font_size = 18; - gfx::drawTextArgs(vg, x + 148, y + 45, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetName()); - gfx::drawTextArgs(vg, x + 148, y + 80, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetAuthor()); - gfx::drawTextArgs(vg, x + 148, y + 115, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetDisplayVersion()); + m_scroll_name.Draw(vg, selected, text_x, y + 45, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetName()); + m_scroll_author.Draw(vg, selected, text_x, y + 80, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetAuthor()); + m_scroll_version.Draw(vg, selected, text_x, y + 115, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetDisplayVersion()); } nvgRestore(vg); }); @@ -235,7 +222,6 @@ void Menu::SetIndex(s64 index) { m_list->SetYoff(0); } - // todo: set subheadering. char title_id[33]; std::snprintf(title_id, sizeof(title_id), "%016lX", m_entries[m_index].app_id); SetTitleSubHeading(title_id); diff --git a/sphaira/source/ui/menus/homebrew.cpp b/sphaira/source/ui/menus/homebrew.cpp index c031970..6ec7912 100644 --- a/sphaira/source/ui/menus/homebrew.cpp +++ b/sphaira/source/ui/menus/homebrew.cpp @@ -162,7 +162,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { } auto text_id = ThemeEntryID_TEXT; - if (pos == m_index) { + const auto selected = pos == m_index; + if (selected) { text_id = ThemeEntryID_TEXT_SELECTED; gfx::drawRectOutline(vg, theme, 4.f, v); } else { @@ -172,8 +173,11 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { const float image_size = 115; gfx::drawImage(vg, x + 20, y + 20, image_size, image_size, e.image ? e.image : App::GetDefaultImage(), 15); + const auto text_off = 148; + const auto text_x = x + text_off; + const auto text_clip_w = w - 30.f - text_off; nvgSave(vg); - nvgIntersectScissor(vg, x, y, w - 30.f, h); // clip + nvgIntersectScissor(vg, text_x, y, text_clip_w, h); // clip { bool has_star = false; if (IsStarEnabled()) { @@ -184,9 +188,9 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { } const float font_size = 18; - gfx::drawTextArgs(vg, x + 148, y + 45, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), "%s%s", has_star ? "\u2605 " : "", e.GetName()); - gfx::drawTextArgs(vg, x + 148, y + 80, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetAuthor()); - gfx::drawTextArgs(vg, x + 148, y + 115, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetDisplayVersion()); + m_scroll_name.DrawArgs(vg, selected, text_x, y + 45, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), "%s%s", has_star ? "\u2605 " : "", e.GetName()); + m_scroll_author.Draw(vg, selected, text_x, y + 80, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetAuthor()); + m_scroll_version.Draw(vg, selected, text_x, y + 115, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetDisplayVersion()); } nvgRestore(vg); }); diff --git a/sphaira/source/ui/menus/themezer.cpp b/sphaira/source/ui/menus/themezer.cpp index 9594882..f5d2a14 100644 --- a/sphaira/source/ui/menus/themezer.cpp +++ b/sphaira/source/ui/menus/themezer.cpp @@ -537,7 +537,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { auto& e = page.m_packList[pos]; auto text_id = ThemeEntryID_TEXT; - if (pos == m_index) { + const auto selected = pos == m_index; + if (selected) { text_id = ThemeEntryID_TEXT_SELECTED; gfx::drawRectOutline(vg, theme, 4.f, v); } else { @@ -607,11 +608,14 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { gfx::drawImage(vg, x + xoff, y, 320, 180, image.image ? image.image : App::GetDefaultImage(), 15); } + const auto text_x = x + xoff; + const auto text_clip_w = w - 30.f - xoff; nvgSave(vg); - nvgIntersectScissor(vg, x, y, w - 30.f, h); // clip + nvgIntersectScissor(vg, text_x, y, text_clip_w, h); // clip { - gfx::drawTextArgs(vg, x + xoff, y + 180 + 20, 18, NVG_ALIGN_LEFT, theme->GetColour(text_id), "%s", e.details.name.c_str()); - gfx::drawTextArgs(vg, x + xoff, y + 180 + 55, 18, NVG_ALIGN_LEFT, theme->GetColour(text_id), "%s", e.creator.display_name.c_str()); + const float font_size = 18; + m_scroll_name.Draw(vg, selected, text_x, y + 180 + 20, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.details.name.c_str()); + m_scroll_author.Draw(vg, selected, text_x, y + 180 + 55, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.creator.display_name.c_str()); } nvgRestore(vg); }); diff --git a/sphaira/source/ui/scrolling_text.cpp b/sphaira/source/ui/scrolling_text.cpp new file mode 100644 index 0000000..380dc44 --- /dev/null +++ b/sphaira/source/ui/scrolling_text.cpp @@ -0,0 +1,72 @@ +#include "ui/scrolling_text.hpp" +#include "ui/nvg_util.hpp" +#include "app.hpp" +#include + +namespace sphaira::ui { +namespace { + +auto GetTextScrollSpeed() -> float { + switch (App::GetTextScrollSpeed()) { + case 0: return 0.5; + default: case 1: return 1.0; + case 2: return 1.5; + } +} + +} // namespace + +void ScrollingText::Draw(NVGcontext* vg, bool focus, float x, float y, float w, float size, int align, const NVGcolor& colour, const std::string& text_entry) { + if (!focus) { + gfx::drawText(vg, x, y, size, colour, text_entry.c_str(), align); + return; + } + + if (m_str != text_entry) { + m_str = text_entry; + m_tick = 0; + m_text_xoff = 0; + } + + float bounds[4]; + auto value_str = text_entry; + nvgFontSize(vg, size); + nvgTextAlign(vg, align); + nvgTextBounds(vg, 0, 0, value_str.c_str(), nullptr, bounds); + + if (focus) { + const auto scroll_amount = GetTextScrollSpeed(); + if (bounds[2] > w) { + value_str += " "; + nvgTextBounds(vg, 0, 0, value_str.c_str(), nullptr, bounds); + + if (!m_text_xoff) { + m_tick++; + if (m_tick >= 90) { + m_tick = 0; + m_text_xoff += scroll_amount; + } + } else if (bounds[2] > m_text_xoff) { + m_text_xoff += std::min(scroll_amount, bounds[2] - m_text_xoff); + } else { + m_text_xoff = 0; + } + + value_str += text_entry; + } + } + + const Vec2 pos{x - m_text_xoff, y}; + gfx::drawText(vg, pos, size, colour, value_str.c_str(), align); +} + +void ScrollingText::DrawArgs(NVGcontext* vg, bool focus, float x, float y, float w, float size, int align, const NVGcolor& colour, const char* s, ...) { + std::va_list v{}; + va_start(v, s); + char buffer[0x100]; + std::vsnprintf(buffer, sizeof(buffer), s, v); + va_end(v); + Draw(vg, focus, x, y, w, size, align, colour, buffer); +} + +} // namespace sphaira::ui