add touch support to all objects

This commit is contained in:
ITotalJustice
2025-01-04 20:31:16 +00:00
parent f48f9a527f
commit 705947fefb
38 changed files with 797 additions and 874 deletions

View File

@@ -51,13 +51,12 @@ add_executable(sphaira
source/ui/notification.cpp
source/ui/nvg_util.cpp
source/ui/option_box.cpp
source/ui/option_list.cpp
source/ui/popup_list.cpp
source/ui/progress_box.cpp
source/ui/scrollable_text.cpp
source/ui/scrollbar.cpp
source/ui/sidebar.cpp
source/ui/widget.cpp
source/ui/list.cpp
source/app.cpp
source/download.cpp

View File

@@ -33,7 +33,9 @@ enum class LaunchType {
Forwader_Sphaira,
};
// todo: why is this global???
void DrawElement(float x, float y, float w, float h, ThemeEntryID id);
void DrawElement(const Vec4&, ThemeEntryID id);
class App {
public:

View File

@@ -10,7 +10,6 @@ public:
ErrorBox(Result code, const std::string& message);
auto Update(Controller* controller, TouchInfo* touch) -> void override;
auto OnLayoutChange() -> void override;
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
private:

View File

@@ -0,0 +1,26 @@
#pragma once
#include "ui/object.hpp"
namespace sphaira::ui {
struct List final : Object {
using Callback = std::function<bool(NVGcontext* vg, Theme* theme, Vec4 v, u64 index)>;
List(const Vec4& pos, const Vec4& v, const Vec2& pad = {});
void Do(u64 index, u64 count, Callback callback) const {
Do(nullptr, nullptr, index, count, callback);
}
void Do(NVGcontext* vg, Theme* theme, u64 index, u64 count, Callback callback, float y_off = 0) const;
private:
auto Draw(NVGcontext* vg, Theme* theme) -> void override {}
private:
Vec4 m_v;
Vec2 m_pad;
};
} // namespace sphaira::ui

View File

@@ -2,6 +2,7 @@
#include "ui/menus/menu_base.hpp"
#include "ui/scrollable_text.hpp"
#include "ui/list.hpp"
#include "nro.hpp"
#include "fs.hpp"
#include <span>
@@ -100,7 +101,7 @@ private:
std::size_t m_index{}; // where i am in the array
std::vector<Option> m_options;
LazyImage m_banner;
std::vector<LazyImage> m_screens;
std::unique_ptr<List> m_list;
std::shared_ptr<ScrollableText> m_details;
std::shared_ptr<ScrollableText> m_changelog;
@@ -209,6 +210,7 @@ private:
LazyImage m_local;
LazyImage m_installed;
ImageDownloadState m_repo_download_state{ImageDownloadState::None};
std::unique_ptr<List> m_list;
std::string m_search_term;
std::string m_author_term;

View File

@@ -1,6 +1,7 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "ui/list.hpp"
#include "nro.hpp"
#include "fs.hpp"
#include "option.hpp"
@@ -239,6 +240,7 @@ private:
std::vector<u32> m_entries_index_search; // files found via search
std::span<u32> m_entries_current;
std::unique_ptr<List> m_list;
std::optional<fs::FsPath> m_daybreak_path;
// search options

View File

@@ -1,6 +1,7 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "ui/list.hpp"
#include "fs.hpp"
#include "option.hpp"
#include <vector>
@@ -68,6 +69,7 @@ private:
std::vector<Entry> m_entries;
std::size_t m_index{};
std::size_t m_index_offset{};
std::unique_ptr<List> m_list;
};
} // namespace sphaira::ui::menu::gh

View File

@@ -1,6 +1,7 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "ui/list.hpp"
#include "nro.hpp"
#include "fs.hpp"
#include "option.hpp"
@@ -52,6 +53,10 @@ private:
std::vector<NroEntry> m_entries;
std::size_t m_start{};
std::size_t m_index{}; // where i am in the array
std::unique_ptr<List> m_list;
// todo: needed for scroll
float y_off = 0;
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_AlphabeticalStar};
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Decending};

View File

@@ -21,9 +21,6 @@ struct MenuBase : Widget {
void SetTitleSubHeading(std::string sub_heading);
void SetSubHeading(std::string sub_heading);
static auto ScrollHelperDown(u64& index, u64& start, u64 step, s64 row, s64 page, u64 size) -> bool;
static auto ScrollHelperUp(u64& index, u64& start, s64 step, s64 row, s64 page, s64 size) -> bool;
private:
void UpdateVars();

View File

@@ -2,6 +2,7 @@
#include "ui/menus/menu_base.hpp"
#include "ui/scrollable_text.hpp"
#include "ui/list.hpp"
#include "option.hpp"
#include <span>
@@ -166,6 +167,10 @@ struct Menu final : MenuBase {
void SetIndex(std::size_t index) {
m_index = index;
if (m_index > m_start && m_index - m_start >= 6) {
m_start = m_index/3*3 - 3;
}
}
// void SetSearch(const std::string& term);
@@ -187,6 +192,7 @@ private:
std::size_t m_start{};
std::size_t m_index{}; // where i am in the array
std::unique_ptr<List> m_list;
// options
option::OptionLong m_sort{INI_SECTION, "sort", 0};

View File

@@ -19,7 +19,6 @@ public:
auto IsDone() const noexcept { return m_count == 0; }
private:
void OnLayoutChange() override;
void Draw(NVGcontext* vg, Theme* theme) override;
private:
@@ -34,7 +33,6 @@ public:
NotifMananger() = default;
~NotifMananger() = default;
void OnLayoutChange() override;
void Draw(NVGcontext* vg, Theme* theme) override;
void Push(const NotifEntry& entry);

View File

@@ -1,7 +1,7 @@
#pragma once
#include "nanovg.h"
#include "ui/widget.hpp"
#include "ui/types.hpp"
namespace sphaira::ui::gfx {
@@ -81,10 +81,8 @@ void textBounds(NVGcontext*, float x, float y, float *bounds, const char* str, .
// void textBounds(NVGcontext*, float *bounds, const char* str);
auto getButton(Button button) -> const char*;
void drawButton(NVGcontext* vg, float x, float y, float size, Button button);
void drawButtons(NVGcontext* vg, const Widget::Actions& actions, const NVGcolor& c, float start_x = 1220.f);
void drawDimBackground(NVGcontext* vg);
void drawScrollbar(NVGcontext* vg, Theme* theme, u32 index_off, u32 count, u32 max_per_page);
void drawScrollbar(NVGcontext* vg, Theme* theme, float x, float y, float h, u32 index_off, u32 count, u32 max_per_page);
void updateHighlightAnimation();
void getHighlightAnimation(float* gradientX, float* gradientY, float* color);

View File

@@ -9,8 +9,6 @@ public:
Object() = default;
virtual ~Object() = default;
// virtual auto OnLayoutChange() -> void = 0;
virtual auto OnLayoutChange() -> void {};
virtual auto Draw(NVGcontext* vg, Theme* theme) -> void = 0;
auto GetPos() const noexcept {

View File

@@ -12,7 +12,6 @@ public:
OptionBoxEntry(const std::string& text, Vec4 pos);
auto Update(Controller* controller, TouchInfo* touch) -> void override {}
auto OnLayoutChange() -> void override {}
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
auto Selected(bool enable) -> void;
@@ -40,13 +39,13 @@ public:
OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, std::size_t index, Callback cb); // tri
auto Update(Controller* controller, TouchInfo* touch) -> void override;
auto OnLayoutChange() -> void override;
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
auto OnFocusGained() noexcept -> void override;
auto OnFocusLost() noexcept -> void override;
private:
auto Setup(std::size_t index) -> void; // common setup values
void SetIndex(std::size_t index);
private:
std::string m_message;

View File

@@ -1,27 +0,0 @@
#pragma once
#include "ui/widget.hpp"
#include <optional>
namespace sphaira::ui {
class OptionList final : public Widget {
public:
using Options = std::vector<std::pair<std::string, std::function<void()>>>;
public:
OptionList(Options _options);
auto Update(Controller* controller, TouchInfo* touch) -> void override;
auto OnLayoutChange() -> void override;
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
protected:
Options m_options;
std::size_t m_index{};
private:
};
} // namespace sphaira::ui

View File

@@ -1,7 +1,7 @@
#pragma once
#include "ui/widget.hpp"
#include "ui/scrollbar.hpp"
#include "ui/list.hpp"
#include <optional>
namespace sphaira::ui {
@@ -19,11 +19,13 @@ public:
PopupList(std::string title, Items items, std::size_t& index_ref);
auto Update(Controller* controller, TouchInfo* touch) -> void override;
auto OnLayoutChange() -> void override;
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
auto OnFocusGained() noexcept -> void override;
auto OnFocusLost() noexcept -> void override;
private:
void SetIndex(std::size_t index);
private:
static constexpr Vec2 m_title_pos{70.f, 28.f};
static constexpr Vec4 m_block{280.f, 110.f, 720.f, 60.f};
@@ -36,14 +38,11 @@ private:
std::size_t m_index; // index in list array
std::size_t m_index_offset{}; // drawing from array start
// std::size_t& index_ref;
// std::string& index_str_ref;
std::unique_ptr<List> m_list;
float m_selected_y{};
float m_yoff{};
float m_line_top{};
float m_line_bottom{};
ScrollBar m_scrollbar;
};
} // namespace sphaira::ui

View File

@@ -1,34 +0,0 @@
#pragma once
#include "ui/widget.hpp"
namespace sphaira::ui {
class ScrollBar final : public Widget {
public:
enum class Direction { DOWN, UP };
public:
ScrollBar() = default;
ScrollBar(Vec4 bounds, float entry_height, std::size_t entries);
auto Update(Controller* controller, TouchInfo* touch) -> void override {}
auto OnLayoutChange() -> void override;
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
auto Setup(Vec4 bounds, float entry_height, std::size_t entries) -> void;
auto Move(Direction direction) -> void;
private:
auto Setup() -> void;
private:
Vec4 m_bounds{};
std::size_t m_entries{};
std::size_t m_index{};
float m_entry_height{};
float m_step_size{};
bool m_should_draw{false};
};
} // namespace sphaira::ui

View File

@@ -1,6 +1,7 @@
#pragma once
#include "ui/widget.hpp"
#include "ui/list.hpp"
#include <memory>
namespace sphaira::ui {
@@ -9,7 +10,6 @@ class SidebarEntryBase : public Widget {
public:
SidebarEntryBase(std::string&& title);
virtual auto Draw(NVGcontext* vg, Theme* theme) -> void override;
virtual auto OnLayoutChange() -> void override {}
protected:
std::string m_title;
@@ -24,9 +24,9 @@ public:
SidebarEntryBool(std::string title, bool option, Callback cb, std::string true_str = "On", std::string false_str = "Off");
SidebarEntryBool(std::string title, bool& option, std::string true_str = "On", std::string false_str = "Off");
private:
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
private:
bool m_option;
Callback m_callback;
std::string m_true_str;
@@ -101,14 +101,11 @@ public:
Sidebar(std::string title, std::string sub, Side side);
auto Update(Controller* controller, TouchInfo* touch) -> void override;
auto OnLayoutChange() -> void override {}
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
auto OnFocusGained() noexcept -> void override;
auto OnFocusLost() noexcept -> void override;
void Add(std::shared_ptr<SidebarEntryBase> entry);
void AddSpacer();
void AddHeader(std::string name);
private:
void SetIndex(std::size_t index);
@@ -121,13 +118,13 @@ private:
std::size_t m_index{};
std::size_t m_index_offset{};
std::unique_ptr<List> m_list;
Vec4 m_top_bar{};
Vec4 m_bottom_bar{};
Vec2 m_title_pos{};
Vec4 m_base_pos{};
float m_selected_y{};
static constexpr float m_title_size{28.f};
// static constexpr Vec2 box_size{380.f, 70.f};
static constexpr Vec2 m_box_size{400.f, 70.f};

View File

@@ -198,16 +198,27 @@ struct Theme {
ElementEntry elements[ThemeEntryID_MAX];
};
// enum class TouchGesture {
// None,
// Tap,
// Scroll,
// };
struct TouchInfo {
HidTouchState initial;
HidTouchState cur;
auto in_range(const Vec4& v) const -> bool {
return cur.x >= v.x && cur.x <= v.x + v.w && cur.y >= v.y && cur.y <= v.y + v.h;
}
auto in_range(s32 x, s32 y, s32 w, s32 h) const -> bool {
return cur.x >= x && cur.x <= x + w && cur.y >= y && cur.y <= y + h;
return in_range(Vec4(x, y, w, h));
}
bool is_touching;
bool is_tap;
bool is_scroll;
bool is_clicked;
};

View File

@@ -8,7 +8,20 @@
namespace sphaira::ui {
struct uiButton final : Object {
uiButton(Button button, Action action) : m_button{button}, m_action{action} {}
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
Button m_button;
Action m_action;
Vec4 m_button_pos{};
Vec4 m_hint_pos{};
};
struct Widget : public Object {
using Actions = std::map<Button, Action>;
using uiButtons = std::vector<uiButton>;
virtual ~Widget() = default;
virtual void Update(Controller* controller, TouchInfo* touch);
@@ -59,8 +72,12 @@ struct Widget : public Object {
return m_pop;
}
using Actions = std::map<Button, Action>;
// using Actions = std::unordered_map<Button, Action>;
auto GetUiButtons() const -> uiButtons;
static auto GetUiButtons(const Actions& actions, float x = 1220, float y = 675) -> uiButtons;
static auto ScrollHelperDown(u64& index, u64& start, u64 step, s64 row, s64 page, u64 size) -> bool;
static auto ScrollHelperUp(u64& index, u64& start, s64 step, s64 row, s64 page, s64 size) -> bool;
Actions m_actions;
bool m_focus{false};
bool m_pop{false};

View File

@@ -701,12 +701,6 @@ void App::ExitRestart() {
void App::Poll() {
m_controller.Reset();
padUpdate(&m_pad);
m_controller.m_kdown = padGetButtonsDown(&m_pad);
m_controller.m_kheld = padGetButtons(&m_pad);
m_controller.m_kup = padGetButtonsUp(&m_pad);
m_controller.UpdateButtonHeld(static_cast<u64>(Button::ANY_DIRECTION));
HidTouchScreenState state{};
hidGetTouchScreenStates(&state, 1);
m_touch_info.is_clicked = false;
@@ -715,7 +709,6 @@ void App::Poll() {
m_touch_info.initial = m_touch_info.cur = state.touches[0];
m_touch_info.is_touching = true;
m_touch_info.is_tap = true;
// PlaySoundEffect(SoundEffect_Limit);
} else if (state.count >= 1 && m_touch_info.is_touching) {
m_touch_info.cur = state.touches[0];
@@ -723,13 +716,24 @@ void App::Poll() {
(std::abs((s32)m_touch_info.initial.x - (s32)m_touch_info.cur.x) > 20 ||
std::abs((s32)m_touch_info.initial.y - (s32)m_touch_info.cur.y) > 20)) {
m_touch_info.is_tap = false;
m_touch_info.is_scroll = true;
}
} else if (m_touch_info.is_touching) {
m_touch_info.is_touching = false;
m_touch_info.is_scroll = false;
if (m_touch_info.is_tap) {
m_touch_info.is_clicked = true;
}
}
// todo: better implement this to match hos
if (!m_touch_info.is_touching && !m_touch_info.is_clicked) {
padUpdate(&m_pad);
m_controller.m_kdown = padGetButtonsDown(&m_pad);
m_controller.m_kheld = padGetButtons(&m_pad);
m_controller.m_kup = padGetButtonsUp(&m_pad);
m_controller.UpdateButtonHeld(static_cast<u64>(Button::ANY_DIRECTION));
}
}
void App::Update() {
@@ -802,17 +806,21 @@ auto App::GetVg() -> NVGcontext* {
}
void DrawElement(float x, float y, float w, float h, ThemeEntryID id) {
DrawElement({x, y, w, h}, id);
}
void DrawElement(const Vec4& v, ThemeEntryID id) {
const auto& e = g_app->m_theme.elements[id];
switch (e.type) {
case ElementType::None: {
} break;
case ElementType::Texture: {
const auto paint = nvgImagePattern(g_app->vg, x, y, w, h, 0, e.texture, 1.f);
ui::gfx::drawRect(g_app->vg, x, y, w, h, paint);
const auto paint = nvgImagePattern(g_app->vg, v.x, v.y, v.w, v.h, 0, e.texture, 1.f);
ui::gfx::drawRect(g_app->vg, v, paint);
} break;
case ElementType::Colour: {
ui::gfx::drawRect(g_app->vg, x, y, w, h, e.colour);
ui::gfx::drawRect(g_app->vg, v, e.colour);
} break;
}
}

View File

@@ -1134,10 +1134,6 @@ auto ErrorBox::Update(Controller* controller, TouchInfo* touch) -> void {
Widget::Update(controller, touch);
}
auto ErrorBox::OnLayoutChange() -> void {
}
auto ErrorBox::Draw(NVGcontext* vg, Theme* theme) -> void {
gfx::dimBackground(vg);
gfx::drawRect(vg, m_pos, theme->elements[ThemeEntryID_SELECTED].colour);

View File

@@ -0,0 +1,55 @@
#include "ui/list.hpp"
namespace sphaira::ui {
List::List(const Vec4& pos, const Vec4& v, const Vec2& pad)
: m_v{v}
, m_pad{pad} {
m_pos = pos;
}
void List::Do(NVGcontext* vg, Theme* theme, u64 index, u64 count, Callback callback, float y_off) const {
auto v = m_v;
if (vg) {
nvgSave(vg);
nvgScissor(vg, GetX(), GetY(), GetW(), GetH());
}
// index = (y_off / (v.h + m_pad.y)) / (GetW() / (v.w + m_pad.x));
// v.y -= y_off;
for (u64 i = index; i < count; v.y += v.h + m_pad.y) {
if (v.y > GetY() + GetH()) {
break;
}
const auto x = v.x;
for (; i < count; i++, v.x += v.w + m_pad.x) {
// only draw if full x is in bounds
if (v.x + v.w > GetX() + GetW()) {
break;
}
Vec4 vv = v;
// if not drawing, only return clipped v as its used for touch
if (!vg) {
vv.w = std::min(v.x + v.w, m_pos.x + m_pos.w) - v.x;
vv.h = std::min(v.y + v.h, m_pos.y + m_pos.h) - v.y;
}
if (!callback(vg, theme, vv, i)) {
return;
}
}
v.x = x;
}
if (vg) {
nvgRestore(vg);
}
}
} // namespace sphaira::ui

View File

@@ -271,19 +271,11 @@ void DrawIcon(NVGcontext* vg, const LazyImage& l, const LazyImage& d, float x, f
rounded_image = false;
gfx::drawRect(vg, x, y, w, h, nvgRGB(i.first_pixel[0], i.first_pixel[1], i.first_pixel[2]), rounded);
}
if (iw > w || ih > h) {
crop = true;
nvgSave(vg);
nvgScissor(vg, x, y, w, h);
}
if (rounded_image) {
gfx::drawImageRounded(vg, ix, iy, iw, ih, i.image);
} else {
gfx::drawImage(vg, ix, iy, iw, ih, i.image);
}
if (crop) {
nvgRestore(vg);
}
}
void DrawIcon(NVGcontext* vg, const LazyImage& l, const LazyImage& d, Vec4 vec, bool rounded = true, float scale = 1.0) {
@@ -711,6 +703,11 @@ EntryMenu::EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu)
SetSubHeading(m_entry.binary);
SetSubHeading(m_entry.description);
UpdateOptions();
// todo: see Draw()
// const Vec4 v{75, 110, 370, 155};
// const Vec2 pad{10, 10};
// m_list = std::make_unique<List>(3, 3, v, pad);
}
EntryMenu::~EntryMenu() {
@@ -755,6 +752,7 @@ void EntryMenu::Draw(NVGcontext* vg, Theme* theme) {
// for (const auto& option : m_options) {
const auto& text_col = theme->elements[ThemeEntryID_TEXT].colour;
// todo: rewrite this mess and use list
constexpr float mm = 0;//20;
constexpr Vec4 block{968.f + mm, 110.f, 256.f - mm*2, 60.f};
constexpr float text_xoffset{15.f};
@@ -886,8 +884,6 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/banners");
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/screens");
// m_span = m_entries;
this->SetActions(
std::make_pair(Button::RIGHT, Action{[this](){
if (m_entries_current.empty()) {
@@ -1005,6 +1001,9 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
m_sort = (SortType)ini_getl(INI_SECTION, "sort", m_sort, App::CONFIG_PATH);
m_order = (OrderType)ini_getl(INI_SECTION, "order", m_order, App::CONFIG_PATH);
const Vec4 v{75, 110, 370, 155};
const Vec2 pad{10, 10};
m_list = std::make_unique<List>(m_pos, v, pad);
Sort();
}
@@ -1015,26 +1014,19 @@ Menu::~Menu() {
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
const u64 SCROLL = m_start;
const u64 max_entry_display = 9;
const u64 nro_total = m_entries.size();
const u64 cursor_pos = m_index;
if (touch->is_clicked) {
for (u64 i = 0, pos = SCROLL, y = 110, w = 370, h = 155; pos < nro_total && i < max_entry_display; y += h + 10) {
for (u64 j = 0, x = 75; j < 3 && pos < nro_total && i < max_entry_display; j++, i++, pos++, x += w + 10) {
if (touch->in_range(x, y, w, h)) {
if (pos == m_index) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
SetIndex(pos);
}
break;
}
m_list->Do(m_start, m_entries_current.size(), [this, &touch](auto* vg, auto* theme, auto v, auto pos) {
if (touch->is_clicked && touch->in_range(v)) {
if (pos == m_index) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
SetIndex(pos);
}
return false;
}
}
return true;
});
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
@@ -1050,123 +1042,114 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
return;
}
const u64 SCROLL = m_start;
const u64 max_entry_display = 9;
const u64 nro_total = m_entries_current.size();
const u64 cursor_pos = m_index;
// only draw scrollbar if needed
if (nro_total > max_entry_display) {
const auto scrollbar_size = 500.f;
const auto sb_h = 3.f / (float)nro_total * scrollbar_size;
const auto sb_y = SCROLL / 3.f;
gfx::drawRect(vg, SCREEN_WIDTH - 50, 100, 10, scrollbar_size, theme->elements[ThemeEntryID_GRID].colour);
gfx::drawRect(vg, SCREEN_WIDTH - 50+2, 102 + sb_h * sb_y, 10-4, sb_h + (sb_h * 2) - 4, theme->elements[ThemeEntryID_TEXT_SELECTED].colour);
}
gfx::drawScrollbar(vg, theme, m_start, m_entries_current.size(), 9);
// max images per frame, in order to not hit io / gpu too hard.
const int image_load_max = 2;
int image_load_count = 0;
for (u64 i = 0, pos = SCROLL, y = 110, w = 370, h = 155; pos < nro_total && i < max_entry_display; y += h + 10) {
for (u64 j = 0, x = 75; j < 3 && pos < nro_total && i < max_entry_display; j++, i++, pos++, x += w + 10) {
const auto index = m_entries_current[pos];
auto& e = m_entries[index];
auto& image = e.image;
m_list->Do(vg, theme, m_start, m_entries_current.size(), [this, &image_load_count](auto* vg, auto* theme, auto v, auto pos) {
const auto& [x, y, w, h] = v;
const auto index = m_entries_current[pos];
auto& e = m_entries[index];
auto& image = e.image;
// try and load cached image.
if (image_load_count < image_load_max && !image.image && !image.tried_cache) {
image.tried_cache = true;
image.cached = EntryLoadImageFile(BuildIconCachePath(e), image);
if (image.cached) {
image_load_count++;
}
}
// lazy load image
if (!image.image || image.cached) {
switch (image.state) {
case ImageDownloadState::None: {
const auto path = BuildIconCachePath(e);
const auto url = BuildIconUrl(e);
image.state = ImageDownloadState::Progress;
curl::Api().ToFileAsync(
curl::Url{url},
curl::Path{path},
curl::Flags{curl::Flag_Cache},
curl::OnComplete{[this, &image](auto& result) {
if (result.success) {
image.state = ImageDownloadState::Done;
// data hasn't changed
if (result.code == 304) {
image.cached = false;
}
} else {
image.state = ImageDownloadState::Failed;
log_write("failed to download image\n");
}
}
});
} break;
case ImageDownloadState::Progress: {
} break;
case ImageDownloadState::Done: {
if (image_load_count < image_load_max) {
image.cached = false;
if (!EntryLoadImageFile(BuildIconCachePath(e), e.image)) {
image.state = ImageDownloadState::Failed;
} else {
image_load_count++;
}
}
} break;
case ImageDownloadState::Failed: {
} break;
}
}
auto text_id = ThemeEntryID_TEXT;
if (pos == cursor_pos) {
text_id = ThemeEntryID_TEXT_SELECTED;
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour);
} else {
DrawElement(x, y, w, h, ThemeEntryID_GRID);
}
constexpr double image_scale = 256.0 / 115.0;
// const float image_size = 256 / image_scale;
// const float image_size_h = 150 / image_scale;
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);
nvgSave(vg);
nvgScissor(vg, x, y, w - 30.f, h); // clip
{
const float font_size = 18;
gfx::drawTextArgs(vg, x + 148, y + 45, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.title.c_str());
gfx::drawTextArgs(vg, x + 148, y + 80, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.author.c_str());
gfx::drawTextArgs(vg, x + 148, y + 115, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.version.c_str());
}
nvgRestore(vg);
float i_size = 22;
switch (e.status) {
case EntryStatus::Get:
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_get.image);
break;
case EntryStatus::Installed:
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_installed.image);
break;
case EntryStatus::Local:
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_local.image);
break;
case EntryStatus::Update:
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_update.image);
break;
// try and load cached image.
if (image_load_count < image_load_max && !image.image && !image.tried_cache) {
image.tried_cache = true;
image.cached = EntryLoadImageFile(BuildIconCachePath(e), image);
if (image.cached) {
image_load_count++;
}
}
}
// lazy load image
if (!image.image || image.cached) {
switch (image.state) {
case ImageDownloadState::None: {
const auto path = BuildIconCachePath(e);
const auto url = BuildIconUrl(e);
image.state = ImageDownloadState::Progress;
curl::Api().ToFileAsync(
curl::Url{url},
curl::Path{path},
curl::Flags{curl::Flag_Cache},
curl::OnComplete{[this, &image](auto& result) {
if (result.success) {
image.state = ImageDownloadState::Done;
// data hasn't changed
if (result.code == 304) {
image.cached = false;
}
} else {
image.state = ImageDownloadState::Failed;
log_write("failed to download image\n");
}
}
});
} break;
case ImageDownloadState::Progress: {
} break;
case ImageDownloadState::Done: {
if (image_load_count < image_load_max) {
image.cached = false;
if (!EntryLoadImageFile(BuildIconCachePath(e), e.image)) {
image.state = ImageDownloadState::Failed;
} else {
image_load_count++;
}
}
} break;
case ImageDownloadState::Failed: {
} break;
}
}
auto text_id = ThemeEntryID_TEXT;
if (pos == m_index) {
text_id = ThemeEntryID_TEXT_SELECTED;
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour);
} else {
DrawElement(x, y, w, h, ThemeEntryID_GRID);
}
constexpr double image_scale = 256.0 / 115.0;
// const float image_size = 256 / image_scale;
// const float image_size_h = 150 / image_scale;
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 clip_y = std::min(GetY() + GetH(), y + h) - y;
nvgSave(vg);
nvgScissor(vg, v.x, v.y, w - 30.f, clip_y); // clip
{
const float font_size = 18;
gfx::drawTextArgs(vg, x + 148, y + 45, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.title.c_str());
gfx::drawTextArgs(vg, x + 148, y + 80, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.author.c_str());
gfx::drawTextArgs(vg, x + 148, y + 115, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.version.c_str());
}
nvgRestore(vg);
float i_size = 22;
switch (e.status) {
case EntryStatus::Get:
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_get.image);
break;
case EntryStatus::Installed:
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_installed.image);
break;
case EntryStatus::Local:
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_local.image);
break;
case EntryStatus::Update:
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_update.image);
break;
}
return true;
});
}
void Menu::OnFocusGained() {
@@ -1223,6 +1206,10 @@ void Menu::SetIndex(std::size_t index) {
m_start = 0;
}
if (m_index > m_start && m_index - m_start >= 9) {
m_start = m_index/3*3 - 6;
}
this->SetSubHeading(std::to_string(m_index + 1) + " / " + std::to_string(m_entries_current.size()));
}

View File

@@ -551,6 +551,9 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
}})
);
const Vec4 v{75, GetY() + 1.f + 42.f, 1220.f-45.f*2, 60};
m_list = std::make_unique<List>(m_pos, v);
fs::FsPath buf;
ini_gets("paths", "last_path", "/", buf, sizeof(buf), App::CONFIG_PATH);
SetFs(buf, m_mount.Get());
@@ -562,6 +565,19 @@ Menu::~Menu() {
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
m_list->Do(m_index_offset, m_entries_current.size(), [this, touch](auto* vg, auto* theme, auto v, auto i) {
if (touch->is_clicked && touch->in_range(v)) {
if (m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
SetIndex(i);
}
return false;
}
return true;
});
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
@@ -574,35 +590,14 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
return;
}
const u64 SCROLL = m_index_offset;
constexpr u64 max_entry_display = 8;
const u64 entry_total = m_entries_current.size();
// only draw scrollbar if needed
if (entry_total > max_entry_display) {
const auto scrollbar_size = 500.f;
const auto sb_h = 1.f / (float)entry_total * scrollbar_size;
const auto sb_y = SCROLL;
gfx::drawRect(vg, SCREEN_WIDTH - 50, 100, 10, scrollbar_size, gfx::getColour(gfx::Colour::BLACK));
gfx::drawRect(vg, SCREEN_WIDTH - 50+2, 102 + sb_h * sb_y, 10-4, sb_h + (sb_h * (max_entry_display - 1)) - 4, gfx::getColour(gfx::Colour::SILVER));
}
// constexpr Vec4 line_top{30.f, 86.f, 1220.f, 1.f};
// constexpr Vec4 line_bottom{30.f, 646.f, 1220.f, 1.f};
// constexpr Vec4 block{280.f, 110.f, 720.f, 60.f};
constexpr Vec4 block{75.f, 110.f, 1220.f-45.f*2, 60.f};
constexpr float text_xoffset{15.f};
// todo: cleanup
const float x = block.x;
float y = GetY() + 1.f + 42.f;
const float h = block.h;
const float w = block.w;
gfx::drawScrollbar(vg, theme, m_index_offset, m_entries_current.size(), 8);
nvgSave(vg);
nvgScissor(vg, GetX(), GetY(), GetW(), GetH());
for (std::size_t i = m_index_offset; i < m_entries_current.size(); i++) {
m_list->Do(vg, theme, m_index_offset, m_entries_current.size(), [this, text_col](auto* vg, auto* theme, auto v, auto i) {
const auto& [x, y, w, h] = v;
auto& e = GetEntry(i);
if (e.IsDir()) {
@@ -679,11 +674,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
}
}
y += h;
if (!InYBounds(y)) {
break;
}
}
return true;
});
nvgRestore(vg);
}
@@ -711,6 +703,10 @@ void Menu::SetIndex(std::size_t index) {
m_index_offset = 0;
}
if (m_index > m_index_offset && m_index - m_index_offset >= 7) {
m_index_offset = m_index - 7;
}
if (!m_entries_current.empty() && !GetEntry().checked_internal_extension && GetEntry().extension == "zip") {
GetEntry().checked_internal_extension = true;

View File

@@ -363,6 +363,9 @@ Menu::Menu() : MenuBase{"GitHub"_i18n} {
SetPop();
}})
);
const Vec4 v{75, GetY() + 1.f + 42.f, 1220.f-45.f*2, 60};
m_list = std::make_unique<List>(m_pos, v);
}
Menu::~Menu() {
@@ -370,6 +373,19 @@ Menu::~Menu() {
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
m_list->Do(m_index_offset, m_entries.size(), [this, touch](auto* vg, auto* theme, auto v, auto i) {
if (touch->is_clicked && touch->in_range(v)) {
if (m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
SetIndex(i);
}
return false;
}
return true;
});
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
@@ -382,35 +398,11 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
return;
}
const u64 SCROLL = m_index_offset;
constexpr u64 max_entry_display = 8;
const u64 entry_total = m_entries.size();
// only draw scrollbar if needed
if (entry_total > max_entry_display) {
const auto scrollbar_size = 500.f;
const auto sb_h = 1.f / (float)entry_total * scrollbar_size;
const auto sb_y = SCROLL;
gfx::drawRect(vg, SCREEN_WIDTH - 50, 100, 10, scrollbar_size, gfx::getColour(gfx::Colour::BLACK));
gfx::drawRect(vg, SCREEN_WIDTH - 50+2, 102 + sb_h * sb_y, 10-4, sb_h + (sb_h * (max_entry_display - 1)) - 4, gfx::getColour(gfx::Colour::SILVER));
}
// constexpr Vec4 line_top{30.f, 86.f, 1220.f, 1.f};
// constexpr Vec4 line_bottom{30.f, 646.f, 1220.f, 1.f};
// constexpr Vec4 block{280.f, 110.f, 720.f, 60.f};
constexpr Vec4 block{75.f, 110.f, 1220.f-45.f*2, 60.f};
constexpr float text_xoffset{15.f};
gfx::drawScrollbar(vg, theme, m_index_offset, m_entries.size(), 8);
// todo: cleanup
const float x = block.x;
float y = GetY() + 1.f + 42.f;
const float h = block.h;
const float w = block.w;
nvgSave(vg);
nvgScissor(vg, GetX(), GetY(), GetW(), GetH());
for (std::size_t i = m_index_offset; i < m_entries.size(); i++) {
m_list->Do(vg, theme, m_index_offset, m_entries.size(), [this, text_col](auto* vg, auto* theme, auto v, auto i) {
const auto& [x, y, w, h] = v;
auto& e = m_entries[i];
auto text_id = ThemeEntryID_TEXT;
@@ -431,14 +423,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
nvgRestore(vg);
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f), 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE, theme->elements[text_id].colour, "version: %s", e.tag.c_str());
y += h;
if (!InYBounds(y)) {
break;
}
}
nvgRestore(vg);
return true;
});
}
void Menu::OnFocusGained() {
@@ -454,6 +440,10 @@ void Menu::SetIndex(std::size_t index) {
m_index_offset = 0;
}
if (m_index > m_index_offset && m_index - m_index_offset >= 7) {
m_index_offset = m_index - 7;
}
SetTitleSubHeading(m_entries[m_index].json_path);
UpdateSubheading();
}

View File

@@ -141,6 +141,10 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
}
}})
);
const Vec4 v{75, 110, 370, 155};
const Vec2 pad{10, 10};
m_list = std::make_unique<List>(m_pos, v, pad);
}
Menu::~Menu() {
@@ -154,99 +158,92 @@ Menu::~Menu() {
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
const u64 SCROLL = m_start;
const u64 max_entry_display = 9;
const u64 nro_total = m_entries.size();
const u64 cursor_pos = m_index;
if (touch->is_clicked) {
for (u64 i = 0, pos = SCROLL, y = 110, w = 370, h = 155; pos < nro_total && i < max_entry_display; y += h + 10) {
for (u64 j = 0, x = 75; j < 3 && pos < nro_total && i < max_entry_display; j++, i++, pos++, x += w + 10) {
if (touch->in_range(x, y, w, h)) {
if (pos == m_index) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
SetIndex(pos);
}
break;
if (touch->is_clicked && touch->in_range(GetPos())) {
m_list->Do(m_start, m_entries.size(), [this, touch](auto* vg, auto* theme, auto v, auto i) {
if (touch->is_clicked && touch->in_range(v)) {
if (m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
SetIndex(i);
}
return false;
}
}
return true;
});
} else if (touch->is_scroll && touch->in_range(GetPos())) {
y_off = (s32)touch->initial.y - (s32)touch->cur.y;
log_write("y off: %.2f\n", y_off);
} else {
// y_off = 0;
}
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
MenuBase::Draw(vg, theme);
const u64 SCROLL = m_start;
const u64 max_entry_display = 9;
const u64 nro_total = m_entries.size();
const u64 cursor_pos = m_index;
fs::FsNativeSd fs;
// only draw scrollbar if needed
if (nro_total > max_entry_display) {
const auto scrollbar_size = 500.f;
const auto sb_h = 3.f / (float)nro_total * scrollbar_size;
const auto sb_y = SCROLL / 3.f;
gfx::drawRect(vg, SCREEN_WIDTH - 50, 100, 10, scrollbar_size, theme->elements[ThemeEntryID_GRID].colour);
gfx::drawRect(vg, SCREEN_WIDTH - 50+2, 102 + sb_h * sb_y, 10-4, sb_h + (sb_h * 2) - 4, theme->elements[ThemeEntryID_TEXT_SELECTED].colour);
}
gfx::drawScrollbar(vg, theme, m_start, m_entries.size(), 9);
// max images per frame, in order to not hit io / gpu too hard.
const int image_load_max = 2;
int image_load_count = 0;
for (u64 i = 0, pos = SCROLL, y = 110, w = 370, h = 155; pos < nro_total && i < max_entry_display; y += h + 10) {
for (u64 j = 0, x = 75; j < 3 && pos < nro_total && i < max_entry_display; j++, i++, pos++, x += w + 10) {
auto& e = m_entries[pos];
nvgSave(vg);
nvgScissor(vg, GetX(), GetY(), GetW(), GetH());
// lazy load image
if (image_load_count < image_load_max) {
if (!e.image && e.icon_size && e.icon_offset) {
// NOTE: it seems that images can be any size. SuperTux uses a 1024x1024
// ~300Kb image, which takes a few frames to completely load.
// really, switch-tools should handle this by resizing the image before
// adding it to the nro, as well as validate its a valid jpeg.
const auto icon = nro_get_icon(e.path, e.icon_size, e.icon_offset);
if (!icon.empty()) {
e.image = nvgCreateImageMem(vg, 0, icon.data(), icon.size());
image_load_count++;
}
m_list->Do(vg, theme, m_start, m_entries.size(), [this, &image_load_count](auto* vg, auto* theme, auto v, auto pos) {
const auto& [x, y, w, h] = v;
auto& e = m_entries[pos];
// lazy load image
if (image_load_count < image_load_max) {
if (!e.image && e.icon_size && e.icon_offset) {
// NOTE: it seems that images can be any size. SuperTux uses a 1024x1024
// ~300Kb image, which takes a few frames to completely load.
// really, switch-tools should handle this by resizing the image before
// adding it to the nro, as well as validate its a valid jpeg.
const auto icon = nro_get_icon(e.path, e.icon_size, e.icon_offset);
if (!icon.empty()) {
e.image = nvgCreateImageMem(vg, 0, icon.data(), icon.size());
image_load_count++;
}
}
auto text_id = ThemeEntryID_TEXT;
if (pos == cursor_pos) {
text_id = ThemeEntryID_TEXT_SELECTED;
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour);
} else {
DrawElement(x, y, w, h, ThemeEntryID_GRID);
}
const float image_size = 115;
gfx::drawImageRounded(vg, x + 20, y + 20, image_size, image_size, e.image ? e.image : App::GetDefaultImage());
nvgSave(vg);
nvgScissor(vg, x, y, w - 30.f, h); // clip
{
bool has_star = false;
if (IsStarEnabled()) {
if (!e.has_star.has_value()) {
e.has_star = fs.FileExists(GenerateStarPath(e.path));
}
has_star = e.has_star.value();
}
const float font_size = 18;
gfx::drawTextArgs(vg, x + 148, y + 45, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, "%s%s", has_star ? "\u2605 " : "", e.GetName());
gfx::drawTextArgs(vg, x + 148, y + 80, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.GetAuthor());
gfx::drawTextArgs(vg, x + 148, y + 115, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.GetDisplayVersion());
}
nvgRestore(vg);
}
}
auto text_id = ThemeEntryID_TEXT;
if (pos == m_index) {
text_id = ThemeEntryID_TEXT_SELECTED;
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, v, theme->elements[ThemeEntryID_SELECTED].colour);
} else {
DrawElement(v, ThemeEntryID_GRID);
}
const float image_size = 115;
gfx::drawImageRounded(vg, x + 20, y + 20, image_size, image_size, e.image ? e.image : App::GetDefaultImage());
const auto clip_y = std::min(GetY() + GetH(), y + h) - y;
nvgSave(vg);
nvgScissor(vg, x, y, w - 30.f, clip_y); // clip
{
bool has_star = false;
if (IsStarEnabled()) {
if (!e.has_star.has_value()) {
e.has_star = fs::FsNativeSd().FileExists(GenerateStarPath(e.path));
}
has_star = e.has_star.value();
}
const float font_size = 18;
gfx::drawTextArgs(vg, x + 148, y + 45, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, "%s%s", has_star ? "\u2605 " : "", e.GetName());
gfx::drawTextArgs(vg, x + 148, y + 80, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.GetAuthor());
gfx::drawTextArgs(vg, x + 148, y + 115, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.GetDisplayVersion());
}
nvgRestore(vg);
return true;
}, y_off);
nvgRestore(vg);
}
void Menu::OnFocusGained() {
@@ -262,6 +259,10 @@ void Menu::SetIndex(std::size_t index) {
m_start = 0;
}
if (m_index > m_start && m_index - m_start >= 9) {
m_start = m_index/3*3 - 6;
}
const auto& e = m_entries[m_index];
if (IsStarEnabled()) {

View File

@@ -229,8 +229,6 @@ MainMenu::MainMenu() {
language_items.push_back("Russian"_i18n);
language_items.push_back("Swedish"_i18n);
options->AddHeader("Header"_i18n);
options->AddSpacer();
options->Add(std::make_shared<SidebarEntryCallback>("Theme"_i18n, [this](){
SidebarEntryArray::Items theme_items{};
const auto theme_meta = App::GetThemeMetaList();

View File

@@ -103,61 +103,4 @@ void MenuBase::UpdateVars() {
m_poll_timestamp.Update();
}
auto MenuBase::ScrollHelperDown(u64& index, u64& start, u64 step, s64 row, s64 page, u64 size) -> bool {
const auto old_index = index;
if (!size) {
return false;
}
if (index + step < size) {
index += step;
} else {
index = size - 1;
}
if (index != old_index) {
App::PlaySoundEffect(SoundEffect_Scroll);
s64 delta = index - old_index;
if (index - start >= page) {
do {
start += row;
delta -= row;
} while (delta > 0 && start + page < size);
}
return true;
}
return false;
}
auto MenuBase::ScrollHelperUp(u64& index, u64& start, s64 step, s64 row, s64 page, s64 size) -> bool {
const auto old_index = index;
if (!size) {
return false;
}
if (index >= step) {
index -= step;
} else {
index = 0;
}
if (index != old_index) {
App::PlaySoundEffect(SoundEffect_Scroll);
// if ()
while (index < start) {
// log_write("moved up\n");
start -= row;
}
return true;
}
return false;
}
} // namespace sphaira::ui::menu

View File

@@ -545,6 +545,10 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
m_page_index = 0;
m_pages.resize(1);
PackListDownload();
const Vec4 v{75, 110, 350, 250};
const Vec2 pad{10, 10};
m_list = std::make_unique<List>(m_pos, v, pad);
}
Menu::~Menu() {
@@ -563,26 +567,18 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
return;
}
const u64 SCROLL = m_start;
const u64 max_entry_display = 9;
const u64 nro_total = page.m_packList.size();// m_entries_current.size();
const u64 cursor_pos = m_index;
if (touch->is_clicked) {
for (u64 i = 0, pos = SCROLL, y = 110, w = 350, h = 250; pos < nro_total && i < max_entry_display; y += h + 10) {
for (u64 j = 0, x = 75; j < 3 && pos < nro_total && i < max_entry_display; j++, i++, pos++, x += w + 10) {
if (touch->in_range(x, y, w, h)) {
if (pos == m_index) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
SetIndex(pos);
}
break;
}
m_list->Do(m_start, page.m_packList.size(), [this, touch](auto* vg, auto* theme, auto v, auto i) {
if (touch->is_clicked && touch->in_range(v)) {
if (m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
SetIndex(i);
}
return false;
}
}
return true;
});
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
@@ -609,109 +605,98 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
return;
}
const u64 SCROLL = m_start;
const u64 max_entry_display = 9;
const u64 nro_total = page.m_packList.size();// m_entries_current.size();
const u64 cursor_pos = m_index;
// only draw scrollbar if needed
if (nro_total > max_entry_display) {
const auto scrollbar_size = 500.f;
const auto sb_h = 3.f / (float)(nro_total + 3) * scrollbar_size;
const auto sb_y = SCROLL / 3.f;
gfx::drawRect(vg, SCREEN_WIDTH - 50, 100, 10, scrollbar_size, theme->elements[ThemeEntryID_GRID].colour);
gfx::drawRect(vg, SCREEN_WIDTH - 50+2, 102 + sb_h * sb_y, 10-4, sb_h + (sb_h * 2) - 4, theme->elements[ThemeEntryID_TEXT_SELECTED].colour);
}
gfx::drawScrollbar(vg, theme, m_start, page.m_packList.size(), 9);
// max images per frame, in order to not hit io / gpu too hard.
const int image_load_max = 2;
int image_load_count = 0;
nvgSave(vg);
nvgScissor(vg, 30, 87, 1220 - 30, 646 - 87); // clip
m_list->Do(vg, theme, m_start, page.m_packList.size(), [this, &page, &image_load_count](auto* vg, auto* theme, auto v, auto pos) {
const auto& [x, y, w, h] = v;
auto& e = page.m_packList[pos];
for (u64 i = 0, pos = SCROLL, y = 110, w = 350, h = 250; pos < nro_total && i < max_entry_display; y += h + 10) {
for (u64 j = 0, x = 75; j < 3 && pos < nro_total && i < max_entry_display; j++, i++, pos++, x += w + 10) {
const auto index = pos;
auto& e = page.m_packList[index];
auto text_id = ThemeEntryID_TEXT;
if (pos == m_index) {
text_id = ThemeEntryID_TEXT_SELECTED;
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour);
} else {
DrawElement(x, y, w, h, ThemeEntryID_GRID);
}
auto text_id = ThemeEntryID_TEXT;
if (pos == cursor_pos) {
text_id = ThemeEntryID_TEXT_SELECTED;
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour);
} else {
DrawElement(x, y, w, h, ThemeEntryID_GRID);
const float xoff = (350 - 320) / 2;
const float yoff = (350 - 320) / 2;
// lazy load image
if (e.themes.size()) {
auto& theme = e.themes[0];
auto& image = e.themes[0].preview.lazy_image;
// try and load cached image.
if (image_load_count < image_load_max && !image.image && !image.tried_cache) {
image.tried_cache = true;
image.cached = loadThemeImage(theme);
if (image.cached) {
image_load_count++;
}
}
const float xoff = (350 - 320) / 2;
const float yoff = (350 - 320) / 2;
if (!image.image || image.cached) {
switch (image.state) {
case ImageDownloadState::None: {
const auto path = apiBuildIconCache(theme);
log_write("downloading theme!: %s\n", path);
// lazy load image
if (e.themes.size()) {
auto& theme = e.themes[0];
auto& image = e.themes[0].preview.lazy_image;
// try and load cached image.
if (image_load_count < image_load_max && !image.image && !image.tried_cache) {
image.tried_cache = true;
image.cached = loadThemeImage(theme);
if (image.cached) {
image_load_count++;
}
}
if (!image.image || image.cached) {
switch (image.state) {
case ImageDownloadState::None: {
const auto path = apiBuildIconCache(theme);
log_write("downloading theme!: %s\n", path);
const auto url = theme.preview.thumb;
log_write("downloading url: %s\n", url.c_str());
image.state = ImageDownloadState::Progress;
curl::Api().ToFileAsync(
curl::Url{url},
curl::Path{path},
curl::Flags{curl::Flag_Cache},
curl::OnComplete{[this, &image](auto& result) {
if (result.success) {
image.state = ImageDownloadState::Done;
// data hasn't changed
if (result.code == 304) {
image.cached = false;
}
} else {
image.state = ImageDownloadState::Failed;
log_write("failed to download image\n");
const auto url = theme.preview.thumb;
log_write("downloading url: %s\n", url.c_str());
image.state = ImageDownloadState::Progress;
curl::Api().ToFileAsync(
curl::Url{url},
curl::Path{path},
curl::Flags{curl::Flag_Cache},
curl::OnComplete{[this, &image](auto& result) {
if (result.success) {
image.state = ImageDownloadState::Done;
// data hasn't changed
if (result.code == 304) {
image.cached = false;
}
} else {
image.state = ImageDownloadState::Failed;
log_write("failed to download image\n");
}
});
} break;
case ImageDownloadState::Progress: {
} break;
case ImageDownloadState::Done: {
image.cached = false;
if (!loadThemeImage(theme)) {
image.state = ImageDownloadState::Failed;
} else {
image_load_count++;
}
} break;
case ImageDownloadState::Failed: {
} break;
}
}
});
} break;
case ImageDownloadState::Progress: {
gfx::drawImageRounded(vg, x + xoff, y, 320, 180, image.image ? image.image : App::GetDefaultImage());
} break;
case ImageDownloadState::Done: {
image.cached = false;
if (!loadThemeImage(theme)) {
image.state = ImageDownloadState::Failed;
} else {
image_load_count++;
}
} break;
case ImageDownloadState::Failed: {
} break;
}
}
gfx::drawImageRounded(vg, x + xoff, y, 320, 180, image.image ? image.image : App::GetDefaultImage());
}
const auto clip_y = std::min(GetY() + GetH(), y + h) - y;
nvgSave(vg);
nvgScissor(vg, x, y, w - 30.f, clip_y); // clip
{
gfx::drawTextArgs(vg, x + xoff, y + 180 + 20, 18, NVG_ALIGN_LEFT, theme->elements[text_id].colour, "%s", e.details.name.c_str());
gfx::drawTextArgs(vg, x + xoff, y + 180 + 55, 18, NVG_ALIGN_LEFT, theme->elements[text_id].colour, "%s", e.creator.display_name.c_str());
}
}
nvgRestore(vg);
nvgRestore(vg);
return true;
});
}
void Menu::OnFocusGained() {

View File

@@ -11,10 +11,6 @@ NotifEntry::NotifEntry(std::string text, Side side)
, m_side{side} {
}
auto NotifEntry::OnLayoutChange() -> void {
}
auto NotifEntry::Draw(NVGcontext* vg, Theme* theme, float y) -> bool {
m_pos.y = y;
Draw(vg, theme);
@@ -57,11 +53,6 @@ auto NotifEntry::Draw(NVGcontext* vg, Theme* theme) -> void {
gfx::drawText(vg, Vec2{m_pos.x + (m_pos.w / 2.f), m_pos.y + (m_pos.h / 2.f)}, font_size, text_col, m_text.c_str(), NVG_ALIGN_MIDDLE | NVG_ALIGN_CENTER);
}
auto NotifMananger::OnLayoutChange() -> void {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
}
auto NotifMananger::Draw(NVGcontext* vg, Theme* theme) -> void {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));

View File

@@ -291,7 +291,7 @@ void textBounds(NVGcontext* vg, float x, float y, float *bounds, const char* str
void dimBackground(NVGcontext* vg) {
// drawRectIntenal(vg, {0.f,0.f,1280.f,720.f}, nvgRGBA(30,30,30,180));
// drawRectIntenal(vg, {0.f,0.f,1920.f,1080.f}, nvgRGBA(20, 20, 20, 225), false);
drawRectIntenal(vg, {0.f,0.f,1920.f,1080.f}, nvgRGBA(0, 0, 0, 220), false);
drawRectIntenal(vg, {0.f,0.f,1920.f,1080.f}, nvgRGBA(0, 0, 0, 230), false);
}
void drawRect(NVGcontext* vg, float x, float y, float w, float h, Colour c, bool rounded) {
@@ -453,58 +453,25 @@ void drawTextArgs(NVGcontext* vg, float x, float y, float size, int align, Colou
drawTextIntenal(vg, {x, y}, size, buffer, nullptr, align, getColour(c));
}
void drawButton(NVGcontext* vg, float x, float y, float size, Button button) {
drawText(vg, x, y, size, getButton(button), nullptr, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, getColour(Colour::WHITE));
}
void drawScrollbar(NVGcontext* vg, Theme* theme, float x, float y, float h, u32 index_off, u32 count, u32 max_per_page) {
const u64 SCROLL = index_off;
const u64 max_entry_display = max_per_page;
const u64 entry_total = count;
const float scc2 = 8.0;
const float scw = 2.0;
void drawButtons(NVGcontext* vg, const Widget::Actions& _actions, const NVGcolor& c, float start_x) {
nvgBeginPath(vg);
nvgFontSize(vg, 24.f);
nvgTextAlign(vg, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP);
nvgFillColor(vg, c);
float x = start_x;
const float y = 675.f;
float bounds[4]{};
// swaps L/R position, idc how shit this is, it's called once per frame.
std::vector<std::pair<Button, Action>> actions;
actions.reserve(_actions.size());
for (const auto a: _actions) {
// swap
if (a.first == Button::R && actions.size() && actions.back().first == Button::L) {
const auto s = actions.back();
actions.back() = a;
actions.emplace_back(s);
} else {
actions.emplace_back(a);
}
}
for (const auto& [button, action] : actions) {
if (action.IsHidden() || action.m_hint.empty()) {
continue;
}
nvgFontSize(vg, 20.f);
nvgTextBounds(vg, x, y, action.m_hint.c_str(), nullptr, bounds);
auto len = bounds[2] - bounds[0];
nvgText(vg, x, y, action.m_hint.c_str(), nullptr);
x -= len + 8.f;
nvgFontSize(vg, 26.f);
nvgTextBounds(vg, x, y - 7.f, getButton(button), nullptr, bounds);
len = bounds[2] - bounds[0];
nvgText(vg, x, y - 4.f, getButton(button), nullptr);
x -= len + 34.f;
// only draw scrollbar if needed
if (entry_total > max_entry_display) {
const float sb_h = 1.f / (float)entry_total * h;
const float sb_y = SCROLL;
gfx::drawRect(vg, x, y, scc2, h, theme->elements[ThemeEntryID_GRID].colour, false);
gfx::drawRect(vg, x + scw, y + scw + sb_h * sb_y, scc2 - scw * 2, sb_h * float(max_entry_display) - scw * 2, theme->elements[ThemeEntryID_TEXT_SELECTED].colour, false);
}
}
// from gc installer
void drawDimBackground(NVGcontext* vg) {
// drawRect(vg, 0, 0, 1920, 1080, nvgRGBA(20, 20, 20, 225));
drawRect(vg, 0, 0, 1920, 1080, nvgRGBA(0, 0, 0, 220));
void drawScrollbar(NVGcontext* vg, Theme* theme, u32 index_off, u32 count, u32 max_per_page) {
// drawScrollbar(vg, SCREEN_WIDTH - 50, 100, 500, index_off, count, max_per_page);
drawScrollbar(vg, theme, SCREEN_WIDTH - 50, 100, SCREEN_HEIGHT-200, index_off, count, max_per_page);
}
#define HIGHLIGHT_SPEED 350.0

View File

@@ -79,26 +79,16 @@ OptionBox::OptionBox(const std::string& message, const Option& a, const Option&
auto OptionBox::Update(Controller* controller, TouchInfo* touch) -> void {
Widget::Update(controller, touch);
// if (!controller->GotDown(Button::ANY_HORIZONTAL)) {
// return;
// }
// const auto old_index = m_index;
// if (controller->GotDown(Button::LEFT) && m_index) {
// m_index--;
// } else if (controller->GotDown(Button::RIGHT) && m_index < (m_entries.size() - 1)) {
// m_index++;
// }
// if (old_index != m_index) {
// m_entries[old_index].Selected(false);
// m_entries[m_index].Selected(true);
// }
}
auto OptionBox::OnLayoutChange() -> void {
if (touch->is_clicked) {
for (std::size_t i = 0; i < m_entries.size(); i++) {
auto& e = m_entries[i];
if (touch->in_range(e.GetPos())) {
SetIndex(i);
FireAction(Button::A);
break;
}
}
}
}
auto OptionBox::Draw(NVGcontext* vg, Theme* theme) -> void {
@@ -136,18 +126,12 @@ auto OptionBox::Setup(std::size_t index) -> void {
SetActions(
std::make_pair(Button::LEFT, Action{[this](){
if (m_index) {
m_entries[m_index].Selected(false);
m_index--;
m_entries[m_index].Selected(true);
App::PlaySoundEffect(SoundEffect_Focus);
SetIndex(m_index - 1);
}
}}),
std::make_pair(Button::RIGHT, Action{[this](){
if (m_index < (m_entries.size() - 1)) {
m_entries[m_index].Selected(false);
m_index++;
m_entries[m_index].Selected(true);
App::PlaySoundEffect(SoundEffect_Focus);
SetIndex(m_index + 1);
}
}}),
std::make_pair(Button::A, Action{[this](){
@@ -161,4 +145,12 @@ auto OptionBox::Setup(std::size_t index) -> void {
);
}
void OptionBox::SetIndex(std::size_t index) {
if (m_index != index) {
m_entries[m_index].Selected(false);
m_index = index;
m_entries[m_index].Selected(true);
}
}
} // namespace sphaira::ui

View File

@@ -1,33 +0,0 @@
#include "ui/option_list.hpp"
#include "app.hpp"
#include "ui/nvg_util.hpp"
#include "i18n.hpp"
namespace sphaira::ui {
OptionList::OptionList(Options options)
: m_options{std::move(options)} {
SetAction(Button::A, Action{"Select"_i18n, [this](){
const auto& [_, func] = m_options[m_index];
func();
SetPop();
}});
SetAction(Button::B, Action{"Back"_i18n, [this](){
SetPop();
}});
}
auto OptionList::Update(Controller* controller, TouchInfo* touch) -> void {
}
auto OptionList::OnLayoutChange() -> void {
}
auto OptionList::Draw(NVGcontext* vg, Theme* theme) -> void {
}
} // namespace sphaira::ui

View File

@@ -22,7 +22,9 @@ PopupList::PopupList(std::string title, Items items, std::string& index_ref)
const auto it = std::find(m_items.cbegin(), m_items.cend(), index_ref);
if (it != m_items.cend()) {
m_index = std::distance(m_items.cbegin(), it);
m_selected_y = m_line_top + 1.f + 42.f + (static_cast<float>(m_index) * m_block.h);
if (m_index >= 7) {
m_index_offset = m_index - 6;
}
}
m_callback = [&index_ref, this](auto op_idx) {
@@ -47,8 +49,10 @@ PopupList::PopupList(std::string title, Items items, Callback cb, std::string in
const auto it = std::find(m_items.cbegin(), m_items.cend(), index);
if (it != m_items.cend()) {
m_index = std::distance(m_items.cbegin(), it);
m_selected_y = m_line_top + 1.f + 42.f + (static_cast<float>(m_index) * m_block.h);
SetIndex(std::distance(m_items.cbegin(), it));
if (m_index >= 7) {
m_index_offset = m_index - 6;
}
}
}
@@ -64,11 +68,26 @@ PopupList::PopupList(std::string title, Items items, Callback cb, std::size_t in
m_pos.y = 720.f - m_pos.h;
m_line_top = m_pos.y + 70.f;
m_line_bottom = 720.f - 73.f;
m_selected_y = m_line_top + 1.f + 42.f + (static_cast<float>(m_index) * m_block.h);
if (m_index >= 7) {
m_index_offset = m_index - 6;
}
m_scrollbar.Setup(Vec4{1220.f, m_line_top, 1.f, m_line_bottom - m_line_top}, m_block.h, m_items.size());
Vec4 v{m_block};
v.y = m_line_top + 1.f + 42.f;
const Vec4 pos{0, m_line_top, 1280.f, m_line_bottom - m_line_top};
m_list = std::make_unique<List>(pos, v);
SetActions(
this->SetActions(
std::make_pair(Button::DOWN, Action{[this](){
if (ScrollHelperDown(m_index, m_index_offset, 1, 1, 7, m_items.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::UP, Action{[this](){
if (ScrollHelperUp(m_index, m_index_offset, 1, 1, 7, m_items.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::A, Action{"Select"_i18n, [this](){
if (m_callback) {
m_callback(m_index);
@@ -76,9 +95,6 @@ PopupList::PopupList(std::string title, Items items, Callback cb, std::size_t in
SetPop();
}}),
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
if (m_callback) {
m_callback(std::nullopt);
}
SetPop();
}})
);
@@ -87,37 +103,16 @@ PopupList::PopupList(std::string title, Items items, Callback cb, std::size_t in
auto PopupList::Update(Controller* controller, TouchInfo* touch) -> void {
Widget::Update(controller, touch);
if (!controller->GotDown(Button::ANY_VERTICAL)) {
return;
if (touch->is_clicked && touch->in_range(GetPos())) {
m_list->Do(m_index_offset, m_items.size(), [this, touch](auto* vg, auto* theme, auto v, auto i) {
if (touch->is_clicked && touch->in_range(v)) {
SetIndex(i);
FireAction(Button::A);
return false;
}
return true;
});
}
const auto old_index = m_index;
if (controller->GotDown(Button::DOWN) && m_index < (m_items.size() - 1)) {
m_index++;
m_selected_y += m_block.h;
} else if (controller->GotDown(Button::UP) && m_index != 0) {
m_index--;
m_selected_y -= m_block.h;
}
if (old_index != m_index) {
App::PlaySoundEffect(SoundEffect_Scroll);
OnLayoutChange();
}
}
auto PopupList::OnLayoutChange() -> void {
if ((m_selected_y + m_block.h) > m_line_bottom) {
m_selected_y -= m_block.h;
m_index_offset++;
m_scrollbar.Move(ScrollBar::Direction::DOWN);
} else if (m_selected_y <= m_line_top) {
m_selected_y += m_block.h;
m_index_offset--;
m_scrollbar.Move(ScrollBar::Direction::UP);
}
// LOG("sely: %.2f, index_off: %lu\n", m_selected_y, m_index_offset);
}
auto PopupList::Draw(NVGcontext* vg, Theme* theme) -> void {
@@ -127,16 +122,10 @@ auto PopupList::Draw(NVGcontext* vg, Theme* theme) -> void {
gfx::drawRect(vg, 30.f, m_line_top, m_line_width, 1.f, theme->elements[ThemeEntryID_TEXT].colour);
gfx::drawRect(vg, 30.f, m_line_bottom, m_line_width, 1.f, theme->elements[ThemeEntryID_TEXT].colour);
// todo: cleanup
const float x = m_block.x;
float y = m_line_top + 1.f + 42.f;
const float h = m_block.h;
const float w = m_block.w;
gfx::drawScrollbar(vg, theme, 1250, m_line_top + 20, m_line_bottom - m_line_top - 40, m_index_offset, m_items.size(), 7);
nvgSave(vg);
nvgScissor(vg, 0, m_line_top, 1280.f, m_line_bottom - m_line_top);
for (std::size_t i = m_index_offset; i < m_items.size(); ++i) {
m_list->Do(vg, theme, m_index_offset, m_items.size(), [this](auto* vg, auto* theme, auto v, auto i) {
const auto& [x, y, w, h] = v;
if (m_index == i) {
gfx::drawRect(vg, x - 4.f, y - 4.f, w + 8.f, h + 8.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour);
gfx::drawRect(vg, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour);
@@ -146,14 +135,9 @@ auto PopupList::Draw(NVGcontext* vg, Theme* theme) -> void {
gfx::drawRect(vg, x, y + h, w, 1.f, theme->elements[ThemeEntryID_TEXT].colour);
gfx::drawText(vg, x + m_text_xoffset, y + (h / 2.f), 20.f, m_items[i].c_str(), NULL, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->elements[ThemeEntryID_TEXT].colour);
}
y += h;
if (y > m_line_bottom) {
break;
}
}
nvgRestore(vg);
return true;
});
m_scrollbar.Draw(vg, theme);
Widget::Draw(vg, theme);
}
@@ -167,4 +151,12 @@ auto PopupList::OnFocusLost() noexcept -> void {
SetHidden(true);
}
void PopupList::SetIndex(std::size_t index) {
m_index = index;
if (m_index > m_index_offset && m_index - m_index_offset >= 6) {
m_index_offset = m_index - 6;
}
}
} // namespace sphaira::ui

View File

@@ -1,68 +0,0 @@
#include "ui/scrollbar.hpp"
#include "ui/nvg_util.hpp"
namespace sphaira::ui {
ScrollBar::ScrollBar(Vec4 bounds, float entry_height, std::size_t entries)
: m_bounds{bounds}
, m_entries{entries}
, m_entry_height{entry_height} {
Setup();
}
auto ScrollBar::OnLayoutChange() -> void {
}
auto ScrollBar::Draw(NVGcontext* vg, Theme* theme) -> void {
if (m_should_draw) {
gfx::drawRect(vg, m_pos, gfx::Colour::RED);
}
}
auto ScrollBar::Setup(Vec4 bounds, float entry_height, std::size_t entries) -> void {
m_bounds = bounds;
m_entry_height = entry_height;
m_entries = entries;
Setup();
}
auto ScrollBar::Setup() -> void {
m_bounds.y += 5.f;
m_bounds.h -= 10.f;
const float total_size = (m_entry_height) * static_cast<float>(m_entries);
if (total_size > m_bounds.h) {
m_step_size = total_size / m_entries;
m_pos.x = m_bounds.x;
m_pos.y = m_bounds.y;
m_pos.w = 2.f;
m_pos.h = total_size - m_bounds.h;
m_should_draw = true;
// LOG("total size: %.2f\n", total_size);
// LOG("step size: %.2f\n", m_step_size);
// LOG("pos y: %.2f\n", m_pos.y);
// LOG("pos h: %.2f\n", m_pos.h);
} else {
// LOG("not big enough for scroll total: %.2f bounds: %.2f\n", total_size, bounds.h);
}
}
auto ScrollBar::Move(Direction direction) -> void {
switch (direction) {
case Direction::DOWN:
if (m_index < (m_entries - 1)) {
m_index++;
m_pos.y += m_step_size;
}
break;
case Direction::UP:
if (m_index != 0) {
m_index--;
m_pos.y -= m_step_size;
}
break;
}
}
} // namespace sphaira::ui

View File

@@ -7,13 +7,12 @@
namespace sphaira::ui {
namespace {
struct SidebarSpacer : SidebarEntryBase {
};
struct SidebarHeader : SidebarEntryBase {
};
auto DistanceBetweenY(Vec4 va, Vec4 vb) -> Vec4 {
return Vec4{
va.x, va.y,
va.w, vb.y - va.y
};
}
} // namespace
@@ -191,24 +190,27 @@ Sidebar::Sidebar(std::string title, std::string sub, Side side, Items&& items)
m_title_pos = Vec2{m_pos.x + 30.f, m_pos.y + 40.f};
m_base_pos = Vec4{GetX() + 30.f, GetY() + 170.f, m_pos.w - (30.f * 2.f), 70.f};
// each item has it's own Action, but we take over B
SetAction(Button::B, Action{"Back"_i18n, [this](){
SetPop();
}});
const Vec4 pos = DistanceBetweenY(m_top_bar, m_bottom_bar);
m_list = std::make_unique<List>(pos, m_base_pos);
m_selected_y = m_base_pos.y;
if (!m_items.empty()) {
// setup positions
m_selected_y = m_base_pos.y;
// for (auto&p : m_items) {
// p->SetPos(m_base_pos);
// m_base_pos.y += m_base_pos.h;
// }
// // give focus to first entry.
// m_items[m_index]->OnFocusGained();
}
this->SetActions(
std::make_pair(Button::DOWN, Action{[this](){
auto index = m_index;
if (ScrollHelperDown(index, m_index_offset, 1, 1, 6, m_items.size())) {
SetIndex(index);
}
}}),
std::make_pair(Button::UP, Action{[this](){
auto index = m_index;
if (ScrollHelperUp(index, m_index_offset, 1, 1, 6, m_items.size())) {
SetIndex(index);
}
}}),
// each item has it's own Action, but we take over B
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
SetPop();
}})
);
}
Sidebar::Sidebar(std::string title, std::string sub, Side side)
@@ -218,45 +220,40 @@ Sidebar::Sidebar(std::string title, std::string sub, Side side)
auto Sidebar::Update(Controller* controller, TouchInfo* touch) -> void {
m_items[m_index]->Update(controller, touch);
const auto old_index = m_index;
Widget::Update(controller, touch);
if (touch->is_clicked && touch->in_range(GetPos())) {
m_list->Do(m_index_offset, m_items.size(), [this, touch](auto* vg, auto* theme, auto v, auto i) {
const auto& [x, y, w, h] = v;
m_items[i]->SetY(y);
if (touch->in_range(m_items[i]->GetPos())) {
SetIndex(i);
FireAction(Button::A);
m_items[m_index]->FireAction(Button::A);
return false;
}
return true;
});
} else if (touch->is_clicked && !touch->in_range(GetPos())) {
App::PopToMenu();
}
Actions draw_actions{m_actions};
const auto& actions_ref = m_items[m_index]->GetActions();
draw_actions.insert(actions_ref.cbegin(), actions_ref.cend());
const auto d = GetUiButtons(draw_actions, m_pos.x + m_pos.w - 60.f);
for (auto& e : d) {
if (touch->is_clicked && touch->in_range(e.GetPos())) {
FireAction(e.m_button);
m_items[m_index]->FireAction(e.m_button);
}
}
if (m_items[m_index]->ShouldPop()) {
SetPop();
}
const auto old_index = m_index;
if (controller->GotDown(Button::ANY_DOWN) && m_index < (m_items.size() - 1)) {
m_index++;
m_selected_y += m_box_size.y;
} else if (controller->GotDown(Button::ANY_UP) && m_index != 0) {
m_index--;
m_selected_y -= m_box_size.y;
}
// if we moved
if (m_index != old_index) {
App::PlaySoundEffect(SoundEffect_Scroll);
m_items[old_index]->OnFocusLost();
m_items[m_index]->OnFocusGained();
// move offset
if ((m_selected_y + m_box_size.y) >= m_bottom_bar.y) {
m_selected_y -= m_box_size.y;
m_index_offset++;
// LOG("move down\n");
} else if (m_selected_y <= m_top_bar.y) {
// LOG("move up sely %.2f top %.2f\n", m_selected_y, m_top_bar.y);
m_selected_y += m_box_size.y;
m_index_offset--;
}
}
}
auto DistanceBetweenY(Vec4 va, Vec4 vb) -> Vec4 {
return Vec4{
va.x, va.y,
va.w, vb.y - va.y
};
}
auto Sidebar::Draw(NVGcontext* vg, Theme* theme) -> void {
@@ -268,26 +265,28 @@ auto Sidebar::Draw(NVGcontext* vg, Theme* theme) -> void {
gfx::drawRect(vg, m_top_bar, theme->elements[ThemeEntryID_TEXT].colour);
gfx::drawRect(vg, m_bottom_bar, theme->elements[ThemeEntryID_TEXT].colour);
// only draw scrollbar if needed
const auto dist = DistanceBetweenY(m_top_bar, m_bottom_bar);
nvgSave(vg);
nvgScissor(vg, dist.x, dist.y, dist.w, dist.h);
gfx::drawScrollbar(vg, theme, GetX() + GetW() - 20, m_base_pos.y - 10, dist.h - m_base_pos.y + 48, m_index_offset, m_items.size(), 6);
// for (std::size_t i = m_index_offset; i < m_items.size(); ++i) {
// m_items[i]->Draw(vg, theme);
// }
for (auto&p : m_items) {
p->Draw(vg, theme);
}
nvgRestore(vg);
m_list->Do(vg, theme, m_index_offset, m_items.size(), [this](auto* vg, auto* theme, auto v, auto i) {
const auto& [x, y, w, h] = v;
m_items[i]->SetY(y);
m_items[i]->Draw(vg, theme);
return true;
});
// draw the buttons. fetch the actions from current item and insert into array.
Actions draw_actions{m_actions};
const auto& actions_ref = m_items[m_index]->GetActions();
draw_actions.insert(actions_ref.cbegin(), actions_ref.cend());
gfx::drawButtons(vg, draw_actions, theme->elements[ThemeEntryID_TEXT].colour, m_pos.x + m_pos.w - 60.f);
auto d = GetUiButtons(draw_actions, m_pos.x + m_pos.w - 60.f);
for (auto& e : d) {
e.Draw(vg, theme);
}
// gfx::drawButtons(vg, draw_actions, theme->elements[ThemeEntryID_TEXT].colour, m_pos.x + m_pos.w - 60.f);
}
auto Sidebar::OnFocusGained() noexcept -> void {
@@ -303,23 +302,22 @@ auto Sidebar::OnFocusLost() noexcept -> void {
void Sidebar::Add(std::shared_ptr<SidebarEntryBase> entry) {
m_items.emplace_back(entry);
m_items.back()->SetPos(m_base_pos);
m_base_pos.y += m_base_pos.h;
// for (auto&p : m_items) {
// p->SetPos(base_pos);
// m_base_pos.y += m_base_pos.h;
// }
// give focus to first entry.
m_items[m_index]->OnFocusGained();
}
void Sidebar::AddSpacer() {
}
void Sidebar::AddHeader(std::string name) {
void Sidebar::SetIndex(std::size_t index) {
// if we moved
if (m_index != index) {
m_items[m_index]->OnFocusLost();
m_index = index;
m_items[m_index]->OnFocusGained();
if (m_index > m_index_offset && m_index - m_index_offset >= 5) {
m_index_offset = m_index - 5;
}
}
}
} // namespace sphaira::ui

View File

@@ -1,9 +1,22 @@
#include "ui/widget.hpp"
#include "ui/nvg_util.hpp"
#include "app.hpp"
#include "log.hpp"
namespace sphaira::ui {
auto uiButton::Draw(NVGcontext* vg, Theme* theme) -> void {
// enable to see button region
// gfx::drawRect(vg, m_pos, gfx::Colour::RED);
nvgTextAlign(vg, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP);
nvgFillColor(vg, theme->elements[ThemeEntryID_TEXT].colour);
nvgFontSize(vg, 20);
nvgText(vg, m_hint_pos.x, m_hint_pos.y, m_action.m_hint.c_str(), nullptr);
nvgFontSize(vg, 26);
nvgText(vg, m_button_pos.x, m_button_pos.y, gfx::getButton(m_button), nullptr);
}
void Widget::Update(Controller* controller, TouchInfo* touch) {
for (const auto& [button, action] : m_actions) {
if ((action.m_type & ActionType::DOWN) && controller->GotDown(button)) {
@@ -19,18 +32,22 @@ void Widget::Update(Controller* controller, TouchInfo* touch) {
action.Invoke(true);
}
}
auto draw_actions = GetUiButtons();
for (auto& e : draw_actions) {
if (touch->is_clicked && touch->in_range(e.GetPos())) {
FireAction(e.m_button);
log_write("got click: %s\n", e.m_action.m_hint.c_str());
}
}
}
void Widget::Draw(NVGcontext* vg, Theme* theme) {
Actions draw_actions;
auto draw_actions = GetUiButtons();
for (const auto& [button, action] : m_actions) {
if (!action.IsHidden()) {
draw_actions.emplace(button, action);
}
for (auto& e : draw_actions) {
e.Draw(vg, theme);
}
gfx::drawButtons(vg, draw_actions, theme->elements[ThemeEntryID_TEXT].colour);
}
auto Widget::HasAction(Button button) const -> bool {
@@ -58,4 +75,116 @@ auto Widget::FireAction(Button b, u8 type) -> bool {
return false;
}
auto Widget::ScrollHelperDown(u64& index, u64& start, u64 step, s64 row, s64 page, u64 size) -> bool {
const auto old_index = index;
if (!size) {
return false;
}
if (index + step < size) {
index += step;
} else {
index = size - 1;
}
if (index != old_index) {
App::PlaySoundEffect(SoundEffect_Scroll);
s64 delta = index - old_index;
if (index - start >= page) {
do {
start += row;
delta -= row;
} while (delta > 0 && start + page < size);
}
return true;
}
return false;
}
auto Widget::ScrollHelperUp(u64& index, u64& start, s64 step, s64 row, s64 page, s64 size) -> bool {
const auto old_index = index;
if (!size) {
return false;
}
if (index >= step) {
index -= step;
} else {
index = 0;
}
if (index != old_index) {
App::PlaySoundEffect(SoundEffect_Scroll);
// if ()
while (index < start) {
// log_write("moved up\n");
start -= row;
}
return true;
}
return false;
}
auto Widget::GetUiButtons(const Actions& actions, float x, float y) -> uiButtons {
auto vg = App::GetVg();
uiButtons draw_actions;
draw_actions.reserve(actions.size());
// build array
for (const auto& [button, action] : actions) {
if (action.IsHidden() || action.m_hint.empty()) {
continue;
}
uiButton ui_button{button, action};
// swap
if (button == Button::R && draw_actions.size() && draw_actions.back().m_button == Button::L) {
const auto s = draw_actions.back();
draw_actions.back().m_button = button;
draw_actions.back().m_action = action;
draw_actions.emplace_back(s);
} else {
draw_actions.emplace_back(ui_button);
}
}
float bounds[4]{};
for (auto& e : draw_actions) {
nvgTextAlign(vg, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP);
nvgFontSize(vg, 20.f);
nvgTextBounds(vg, x, y, e.m_action.m_hint.c_str(), nullptr, bounds);
auto len = bounds[2] - bounds[0];
e.m_hint_pos = {x, 675, len, 20};
x -= len + 8.f;
nvgFontSize(vg, 26.f);
nvgTextBounds(vg, x, y - 7.f, gfx::getButton(e.m_button), nullptr, bounds);
len = bounds[2] - bounds[0];
e.m_button_pos = {x, 675 - 4.f, len, 26};
x -= len + 34.f;
e.SetPos(e.m_button_pos);
e.SetX(e.GetX() - 40);
e.SetW(e.m_hint_pos.x - e.m_button_pos.x + len + 25);
e.SetY(e.GetY() - 18);
e.SetH(26 + 18 * 2);
}
return draw_actions;
}
auto Widget::GetUiButtons() const -> uiButtons {
return GetUiButtons(m_actions);
}
} // namespace sphaira::ui