414 lines
14 KiB
C++
414 lines
14 KiB
C++
#include "ui/nvg_util.hpp"
|
|
#include "log.hpp"
|
|
#include <cstddef>
|
|
#include <cstdio>
|
|
#include <cstdarg>
|
|
#include <array>
|
|
#include <utility>
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
namespace sphaira::ui::gfx {
|
|
namespace {
|
|
|
|
constexpr auto ALIGN_HOR = NVG_ALIGN_LEFT|NVG_ALIGN_CENTER|NVG_ALIGN_RIGHT;
|
|
constexpr auto ALIGN_VER = NVG_ALIGN_TOP|NVG_ALIGN_MIDDLE|NVG_ALIGN_BOTTOM|NVG_ALIGN_BASELINE;
|
|
|
|
constexpr std::array buttons = {
|
|
std::pair{Button::A, "\uE0E0"},
|
|
std::pair{Button::B, "\uE0E1"},
|
|
std::pair{Button::X, "\uE0E2"},
|
|
std::pair{Button::Y, "\uE0E3"},
|
|
std::pair{Button::L, "\uE0E4"},
|
|
std::pair{Button::R, "\uE0E5"},
|
|
std::pair{Button::L2, "\uE0E6"},
|
|
std::pair{Button::R2, "\uE0E7"},
|
|
std::pair{Button::UP, "\uE0EB"},
|
|
std::pair{Button::DOWN, "\uE0EC"},
|
|
std::pair{Button::LEFT, "\uE0ED"},
|
|
std::pair{Button::RIGHT, "\uE0EE"},
|
|
std::pair{Button::START, "\uE0EF"},
|
|
std::pair{Button::SELECT, "\uE0F0"},
|
|
// std::pair{Button::LS, "\uE101"},
|
|
// std::pair{Button::RS, "\uE102"},
|
|
std::pair{Button::L3, "\uE104"},
|
|
std::pair{Button::R3, "\uE105"},
|
|
};
|
|
|
|
// software based clipping, saves a few cpu cycles.
|
|
bool ClipRect(float x, float y) {
|
|
return x >= SCREEN_WIDTH || y >= SCREEN_HEIGHT;
|
|
}
|
|
|
|
bool ClipText(float x, float y, int align) {
|
|
if ((!(align & ALIGN_HOR) || (align & NVG_ALIGN_LEFT)) && x >= SCREEN_WIDTH) {
|
|
return true;
|
|
}
|
|
|
|
if ((!(align & ALIGN_VER) || (align & NVG_ALIGN_TOP)) && y >= SCREEN_HEIGHT) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// NEW ---------------------
|
|
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGcolor& c, float rounded) {
|
|
if (ClipRect(v.x, v.y)) {
|
|
return;
|
|
}
|
|
|
|
nvgBeginPath(vg);
|
|
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, rounded);
|
|
nvgFillColor(vg, c);
|
|
nvgFill(vg);
|
|
}
|
|
|
|
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGpaint& p, float rounded) {
|
|
if (ClipRect(v.x, v.y)) {
|
|
return;
|
|
}
|
|
|
|
nvgBeginPath(vg);
|
|
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, rounded);
|
|
nvgFillPaint(vg, p);
|
|
nvgFill(vg);
|
|
}
|
|
|
|
void drawRectOutlineInternal(NVGcontext* vg, const Theme* theme, float size, const Vec4& v) {
|
|
float gradientX, gradientY, color;
|
|
getHighlightAnimation(&gradientX, &gradientY, &color);
|
|
|
|
const auto strokeWidth = 5.F;
|
|
auto v2 = v;
|
|
v2.x -= strokeWidth / 2.F;
|
|
v2.y -= strokeWidth / 2.F;
|
|
v2.w += strokeWidth;
|
|
v2.h += strokeWidth;
|
|
const auto corner_radius = 0.5F;
|
|
|
|
const auto shadow_width = 2.F;
|
|
const auto shadow_offset = 10.F;
|
|
const auto shadow_feather = 10.F;
|
|
const auto shadow_opacity = 128.F;
|
|
|
|
// Shadow
|
|
NVGpaint shadowPaint = nvgBoxGradient(vg,
|
|
v2.x, v2.y + shadow_width,
|
|
v2.w, v2.h,
|
|
corner_radius * 2, shadow_feather,
|
|
nvgRGBA(0, 0, 0, shadow_opacity * 1.f), nvgRGBA(0, 0, 0, 0));
|
|
|
|
nvgBeginPath(vg);
|
|
nvgRect(vg, v2.x - shadow_offset, v2.y - shadow_offset,
|
|
v2.w + shadow_offset * 2, v2.h + shadow_offset * 3);
|
|
nvgRoundedRect(vg, v2.x, v2.y, v2.w, v2.h, corner_radius);
|
|
nvgPathWinding(vg, NVG_HOLE);
|
|
nvgFillPaint(vg, shadowPaint);
|
|
nvgFill(vg);
|
|
|
|
const auto color1 = theme->GetColour(ThemeEntryID_HIGHLIGHT_1);
|
|
const auto color2 = theme->GetColour(ThemeEntryID_HIGHLIGHT_2);
|
|
const auto borderColor = nvgRGBAf(color2.r, color2.g, color2.b, 0.5);
|
|
const auto transparent = nvgRGBA(0, 0, 0, 0);
|
|
|
|
const auto pulsationColor = nvgRGBAf((color * color1.r) + (1 - color) * color2.r,
|
|
(color * color1.g) + (1 - color) * color2.g,
|
|
(color * color1.b) + (1 - color) * color2.b,
|
|
1.f);
|
|
|
|
const auto border1Paint = nvgRadialGradient(vg,
|
|
v2.x + gradientX * v2.w, v2.y + gradientY * v2.h,
|
|
strokeWidth * 10, strokeWidth * 40,
|
|
borderColor, transparent);
|
|
|
|
const auto border2Paint = nvgRadialGradient(vg,
|
|
v2.x + (1 - gradientX) * v2.w, v2.y + (1 - gradientY) * v2.h,
|
|
strokeWidth * 10, strokeWidth * 40,
|
|
borderColor, transparent);
|
|
|
|
nvgBeginPath(vg);
|
|
nvgStrokeColor(vg, pulsationColor);
|
|
nvgStrokeWidth(vg, strokeWidth);
|
|
nvgRoundedRect(vg, v2.x, v2.y, v2.w, v2.h, corner_radius);
|
|
nvgStroke(vg);
|
|
|
|
nvgBeginPath(vg);
|
|
nvgStrokePaint(vg, border1Paint);
|
|
nvgStrokeWidth(vg, strokeWidth);
|
|
nvgRoundedRect(vg, v2.x, v2.y, v2.w, v2.h, corner_radius);
|
|
nvgStroke(vg);
|
|
|
|
nvgBeginPath(vg);
|
|
nvgStrokePaint(vg, border2Paint);
|
|
nvgStrokeWidth(vg, strokeWidth);
|
|
nvgRoundedRect(vg, v2.x, v2.y, v2.w, v2.h, corner_radius);
|
|
nvgStroke(vg);
|
|
}
|
|
|
|
void drawRectOutlineInternal(NVGcontext* vg, const Theme* theme, float size, const Vec4& v, const NVGcolor& c) {
|
|
if (ClipRect(v.x, v.y)) {
|
|
return;
|
|
}
|
|
|
|
const auto corner_radius = 0.5;
|
|
drawRectOutlineInternal(vg, theme, size, v);
|
|
nvgBeginPath(vg);
|
|
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, corner_radius);
|
|
nvgFillColor(vg, c);
|
|
nvgFill(vg);
|
|
}
|
|
|
|
void drawTextIntenal(NVGcontext* vg, const Vec2& v, float size, const char* str, const char* end, int align, const NVGcolor& c) {
|
|
if (ClipText(v.x, v.y, align)) {
|
|
return;
|
|
}
|
|
|
|
nvgBeginPath(vg);
|
|
nvgFontSize(vg, size);
|
|
nvgTextAlign(vg, align);
|
|
nvgFillColor(vg, c);
|
|
nvgText(vg, v.x, v.y, str, end);
|
|
}
|
|
|
|
void drawTriangleInternal(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGcolor& c) {
|
|
nvgBeginPath(vg);
|
|
nvgMoveTo(vg, aX, aY);
|
|
nvgLineTo(vg, bX, bY);
|
|
nvgLineTo(vg, cX, cY);
|
|
nvgFillColor(vg, c);
|
|
nvgFill(vg);
|
|
}
|
|
|
|
void drawTriangleInternal(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGpaint& p) {
|
|
nvgBeginPath(vg);
|
|
nvgMoveTo(vg, aX, aY);
|
|
nvgLineTo(vg, bX, bY);
|
|
nvgLineTo(vg, cX, cY);
|
|
nvgFillPaint(vg, p);
|
|
nvgFill(vg);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
const char* getButton(const Button want) {
|
|
for (auto& [key, val] : buttons) {
|
|
if (key == want) {
|
|
return val;
|
|
}
|
|
}
|
|
std::unreachable();
|
|
}
|
|
|
|
void drawTextArgs(NVGcontext* vg, float x, float y, float size, int align, const NVGcolor& c, const char* str, ...) {
|
|
std::va_list v;
|
|
va_start(v, str);
|
|
char buffer[0x100];
|
|
std::vsnprintf(buffer, sizeof(buffer), str, v);
|
|
va_end(v);
|
|
drawText(vg, x, y, size, buffer, nullptr, align, c);
|
|
}
|
|
|
|
void drawImage(NVGcontext* vg, const Vec4& v, int texture, float rounded) {
|
|
const auto paint = nvgImagePattern(vg, v.x, v.y, v.w, v.h, 0, texture, 1.f);
|
|
drawRect(vg, v, paint, rounded);
|
|
}
|
|
|
|
void drawImage(NVGcontext* vg, float x, float y, float w, float h, int texture, float rounded) {
|
|
drawImage(vg, Vec4(x, y, w, h), texture, rounded);
|
|
}
|
|
|
|
void drawTextBox(NVGcontext* vg, float x, float y, float size, float bound, const NVGcolor& c, const char* str, int align, const char* end) {
|
|
if (ClipText(x, y, align)) {
|
|
return;
|
|
}
|
|
|
|
nvgBeginPath(vg);
|
|
nvgFontSize(vg, size);
|
|
nvgTextAlign(vg, align);
|
|
nvgFillColor(vg, c);
|
|
nvgTextBox(vg, x, y, bound, str, end);
|
|
}
|
|
|
|
void textBounds(NVGcontext* vg, float x, float y, float *bounds, const char* str, ...) {
|
|
char buf[0x100];
|
|
va_list v;
|
|
va_start(v, str);
|
|
std::vsnprintf(buf, sizeof(buf), str, v);
|
|
va_end(v);
|
|
nvgTextBounds(vg, x, y, buf, nullptr, bounds);
|
|
}
|
|
|
|
// NEW-----------
|
|
|
|
void dimBackground(NVGcontext* vg) {
|
|
drawRectIntenal(vg, {0.f,0.f,SCREEN_WIDTH,SCREEN_HEIGHT}, nvgRGBA(0, 0, 0, 180), false);
|
|
}
|
|
|
|
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGcolor& c, float rounded) {
|
|
drawRectIntenal(vg, {x,y,w,h}, c, rounded);
|
|
}
|
|
|
|
void drawRect(NVGcontext* vg, const Vec4& v, const NVGcolor& c, float rounded) {
|
|
drawRectIntenal(vg, v, c, rounded);
|
|
}
|
|
|
|
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGpaint& p, float rounded) {
|
|
drawRectIntenal(vg, {x,y,w,h}, p, rounded);
|
|
}
|
|
|
|
void drawRect(NVGcontext* vg, const Vec4& v, const NVGpaint& p, float rounded) {
|
|
drawRectIntenal(vg, v, p, rounded);
|
|
}
|
|
|
|
void drawRectOutline(NVGcontext* vg, const Theme* theme, float size, float x, float y, float w, float h) {
|
|
drawRectOutlineInternal(vg, theme, size, {x,y,w,h}, theme->GetColour(ThemeEntryID_SELECTED_BACKGROUND));
|
|
}
|
|
|
|
void drawRectOutline(NVGcontext* vg, const Theme* theme, float size, const Vec4& v) {
|
|
drawRectOutlineInternal(vg, theme, size, v, theme->GetColour(ThemeEntryID_SELECTED_BACKGROUND));
|
|
}
|
|
|
|
void drawText(NVGcontext* vg, float x, float y, float size, const char* str, const char* end, int align, const NVGcolor& c) {
|
|
drawTextIntenal(vg, {x,y}, size, str, end, align, c);
|
|
}
|
|
|
|
void drawText(NVGcontext* vg, float x, float y, float size, const NVGcolor& c, const char* str, int align, const char* end) {
|
|
drawTextIntenal(vg, {x,y}, size, str, end, align, c);
|
|
}
|
|
|
|
void drawText(NVGcontext* vg, const Vec2& v, float size, const char* str, const char* end, int align, const NVGcolor& c) {
|
|
drawTextIntenal(vg, v, size, str, end, align, c);
|
|
}
|
|
|
|
void drawText(NVGcontext* vg, const Vec2& v, float size, const NVGcolor& c, const char* str, int align, const char* end) {
|
|
drawTextIntenal(vg, v, size, str, end, align, c);
|
|
}
|
|
|
|
void drawScrollbar(NVGcontext* vg, const Theme* theme, float x, float y, float h, u32 index_off, u32 count, u32 max_per_page) {
|
|
const s64 SCROLL = index_off;
|
|
const s64 max_entry_display = max_per_page;
|
|
const s64 entry_total = count;
|
|
const float scc2 = 8.0;
|
|
const float scw = 2.0;
|
|
|
|
// 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->GetColour(ThemeEntryID_SCROLLBAR_BACKGROUND), false);
|
|
gfx::drawRect(vg, x + scw, y + scw + sb_h * sb_y, scc2 - scw * 2, sb_h * float(max_entry_display) - scw * 2, theme->GetColour(ThemeEntryID_SCROLLBAR), false);
|
|
}
|
|
}
|
|
|
|
void drawScrollbar(NVGcontext* vg, const Theme* theme, u32 index_off, u32 count, u32 max_per_page) {
|
|
drawScrollbar(vg, theme, SCREEN_WIDTH - 50, 100, SCREEN_HEIGHT-200, index_off, count, max_per_page);
|
|
}
|
|
|
|
void drawScrollbar2(NVGcontext* vg, const Theme* theme, float x, float y, float h, s64 index_off, s64 count, s64 row, s64 page) {
|
|
// round up
|
|
if (count % row) {
|
|
count = count + (row - count % row);
|
|
}
|
|
|
|
const float scc2 = 6.0;
|
|
const float scw = 2.0;
|
|
|
|
// only draw scrollbar if needed
|
|
if (count > page) {
|
|
const float sb_h = 1.f / (float)count * h;
|
|
const float sb_y = index_off;
|
|
gfx::drawRect(vg, x, y, scc2, h, theme->GetColour(ThemeEntryID_SCROLLBAR_BACKGROUND), false);
|
|
gfx::drawRect(vg, x + scw, y + scw + sb_h * sb_y, scc2 - scw * 2, sb_h * float(page) - scw * 2, theme->GetColour(ThemeEntryID_SCROLLBAR), false);
|
|
}
|
|
}
|
|
|
|
void drawScrollbar2(NVGcontext* vg, const Theme* theme, s64 index_off, s64 count, s64 row, s64 page) {
|
|
drawScrollbar2(vg, theme, SCREEN_WIDTH - 50, 100, SCREEN_HEIGHT-200, index_off, count, row, page);
|
|
}
|
|
|
|
void drawTriangle(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGcolor& c) {
|
|
drawTriangleInternal(vg, aX, aY, bX, bY, cX, cY, c);
|
|
}
|
|
|
|
void drawTriangle(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGpaint& p) {
|
|
drawTriangleInternal(vg, aX, aY, bX, bY, cX, cY, p);
|
|
}
|
|
|
|
void drawAppLable(NVGcontext* vg, const Theme* theme, ScrollingText& st, float x, float y, float w, const char* name) {
|
|
// todo: no more 5am code
|
|
const float max_box_w = 392.f;
|
|
const float box_h = 48.f;
|
|
// used for adjusting the position of the box.
|
|
const float clip_pad = 25.f;
|
|
const float clip_left = clip_pad;
|
|
const float clip_right = 1220.f - clip_pad;
|
|
const float text_pad = 25.f;
|
|
const float font_size = 22.f;
|
|
|
|
nvgTextAlign(vg, NVG_ALIGN_LEFT);
|
|
nvgFontSize(vg, font_size);
|
|
float bounds[4]{};
|
|
nvgTextBounds(vg, 0, 0, name, NULL, bounds);
|
|
|
|
const float trinaglex = x + (w / 2.f) - 9.f;
|
|
const float trinagley = y - 14.f;
|
|
const float center_x = x + (w / 2.f);
|
|
const float y_offset = y - 62.f; // top of box
|
|
const float text_width = bounds[2];
|
|
float box_w = text_width + text_pad * 2;
|
|
if (box_w > max_box_w) {
|
|
box_w = max_box_w;
|
|
}
|
|
|
|
float box_x = center_x - (box_w / 2.f);
|
|
if (box_x < clip_left) {
|
|
box_x = clip_left;
|
|
}
|
|
if ((box_x + box_w) > clip_right) {
|
|
// box_x -= ((box_x + box_w) - clip_right) / 2;
|
|
box_x = (clip_right - box_w);
|
|
}
|
|
|
|
const float text_x = box_x + text_pad;
|
|
const float text_y = y_offset + (box_h / 2.f);
|
|
|
|
drawRect(vg, {x-4, y-4, w+8, w+8}, theme->GetColour(ThemeEntryID_GRID));
|
|
nvgBeginPath(vg);
|
|
|
|
nvgRoundedRect(vg, box_x, y_offset, box_w, box_h, 3.f);
|
|
nvgFillColor(vg, theme->GetColour(ThemeEntryID_SELECTED_BACKGROUND));
|
|
nvgFill(vg);
|
|
|
|
drawTriangle(vg, trinaglex, trinagley, trinaglex + 18.f, trinagley, trinaglex + 9.f, trinagley + 12.f, theme->GetColour(ThemeEntryID_SELECTED_BACKGROUND));
|
|
st.Draw(vg, true, text_x, text_y, box_w - text_pad * 2, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_SELECTED), name);
|
|
}
|
|
|
|
#define HIGHLIGHT_SPEED 350.0
|
|
|
|
static double highlightGradientX = 0;
|
|
static double highlightGradientY = 0;
|
|
static double highlightColor = 0;
|
|
|
|
void updateHighlightAnimation() {
|
|
const auto currentTime = svcGetSystemTick() * 10 / 192 / 1000;
|
|
|
|
// Update variables
|
|
highlightGradientX = (std::cos((double)currentTime / HIGHLIGHT_SPEED / 3.0) + 1.0) / 2.0;
|
|
highlightGradientY = (std::sin((double)currentTime / HIGHLIGHT_SPEED / 3.0) + 1.0) / 2.0;
|
|
highlightColor = (std::sin((double)currentTime / HIGHLIGHT_SPEED * 2.0) + 1.0) / 2.0;
|
|
}
|
|
|
|
void getHighlightAnimation(float* gradientX, float* gradientY, float* color) {
|
|
if (gradientX)
|
|
*gradientX = (float)highlightGradientX;
|
|
|
|
if (gradientY)
|
|
*gradientY = (float)highlightGradientY;
|
|
|
|
if (color)
|
|
*color = (float)highlightColor;
|
|
}
|
|
|
|
} // namespace sphaira::ui::gfx
|