re-add sys-clk manager, default to mtc auto adj (others are useless), new gpu table for erista units, custom sys-clk updates
This commit is contained in:
souldbminersmwc
2025-09-18 22:20:44 -04:00
parent f3eae72b47
commit 342f9dd116
203 changed files with 55075 additions and 189 deletions

View File

@@ -0,0 +1,933 @@
/* RetroArch - A frontend for libretro.
* Borealis, a Nintendo Switch UI Library
* Copyright (C) 2014-2018 - Jean-André Santoni
* Copyright (C) 2011-2018 - Daniel De Matteis
* Copyright (C) 2019 - natinusala
* Copyright (C) 2019 - p-sam
*
* RetroArch is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with RetroArch.
* If not, see <http://www.gnu.org/licenses/>.
*/
#include <compat/strl.h>
#include <encodings/utf.h>
#include <features/features_cpu.h>
#include <math.h>
#include <retro_assert.h>
#include <retro_math.h>
#include <retro_miscellaneous.h>
#include <stdio.h>
#include <string.h>
#include <string/stdstring.h>
#include <borealis/animations.hpp>
#include <vector>
namespace brls
{
struct tween
{
float duration;
float running_since;
float initial_value;
float target_value;
float* subject;
uintptr_t tag;
easing_cb easing;
tween_cb cb;
tween_cb tick;
void* userdata;
bool deleted;
};
struct menu_animation
{
std::vector<tween> list;
std::vector<tween> pending;
bool pending_deletes;
bool in_update;
};
typedef struct menu_animation menu_animation_t;
#define TICKER_SPEED 333
#define TICKER_SLOW_SPEED 1600
static const char ticker_spacer_default[] = " | ";
static menu_animation_t anim;
static retro_time_t cur_time = 0;
static retro_time_t old_time = 0;
static uint64_t ticker_idx = 0; /* updated every TICKER_SPEED ms */
static uint64_t ticker_slow_idx = 0; /* updated every TICKER_SLOW_SPEED ms */
static float delta_time = 0.0f;
static bool animation_is_active = false;
static bool ticker_is_active = false;
static double highlight_gradient_x = 0.0f;
static double highlight_gradient_y = 0.0f;
static double highlight_color = 0.0f;
/* from https://github.com/kikito/tween.lua/blob/master/tween.lua */
static float easing_linear(float t, float b, float c, float d)
{
return c * t / d + b;
}
static float easing_in_out_quad(float t, float b, float c, float d)
{
t = t / d * 2;
if (t < 1)
return c / 2 * pow(t, 2) + b;
return -c / 2 * ((t - 1) * (t - 3) - 1) + b;
}
static float easing_in_quad(float t, float b, float c, float d)
{
return c * pow(t / d, 2) + b;
}
static float easing_out_quad(float t, float b, float c, float d)
{
t = t / d;
return -c * t * (t - 2) + b;
}
static float easing_out_in_quad(float t, float b, float c, float d)
{
if (t < d / 2)
return easing_out_quad(t * 2, b, c / 2, d);
return easing_in_quad((t * 2) - d, b + c / 2, c / 2, d);
}
static float easing_in_cubic(float t, float b, float c, float d)
{
return c * pow(t / d, 3) + b;
}
static float easing_out_cubic(float t, float b, float c, float d)
{
return c * (pow(t / d - 1, 3) + 1) + b;
}
static float easing_in_out_cubic(float t, float b, float c, float d)
{
t = t / d * 2;
if (t < 1)
return c / 2 * t * t * t + b;
t = t - 2;
return c / 2 * (t * t * t + 2) + b;
}
static float easing_out_in_cubic(float t, float b, float c, float d)
{
if (t < d / 2)
return easing_out_cubic(t * 2, b, c / 2, d);
return easing_in_cubic((t * 2) - d, b + c / 2, c / 2, d);
}
static float easing_in_quart(float t, float b, float c, float d)
{
return c * pow(t / d, 4) + b;
}
static float easing_out_quart(float t, float b, float c, float d)
{
return -c * (pow(t / d - 1, 4) - 1) + b;
}
static float easing_in_out_quart(float t, float b, float c, float d)
{
t = t / d * 2;
if (t < 1)
return c / 2 * pow(t, 4) + b;
return -c / 2 * (pow(t - 2, 4) - 2) + b;
}
static float easing_out_in_quart(float t, float b, float c, float d)
{
if (t < d / 2)
return easing_out_quart(t * 2, b, c / 2, d);
return easing_in_quart((t * 2) - d, b + c / 2, c / 2, d);
}
static float easing_in_quint(float t, float b, float c, float d)
{
return c * pow(t / d, 5) + b;
}
static float easing_out_quint(float t, float b, float c, float d)
{
return c * (pow(t / d - 1, 5) + 1) + b;
}
static float easing_in_out_quint(float t, float b, float c, float d)
{
t = t / d * 2;
if (t < 1)
return c / 2 * pow(t, 5) + b;
return c / 2 * (pow(t - 2, 5) + 2) + b;
}
static float easing_out_in_quint(float t, float b, float c, float d)
{
if (t < d / 2)
return easing_out_quint(t * 2, b, c / 2, d);
return easing_in_quint((t * 2) - d, b + c / 2, c / 2, d);
}
static float easing_in_sine(float t, float b, float c, float d)
{
return -c * cos(t / d * (M_PI / 2)) + c + b;
}
static float easing_out_sine(float t, float b, float c, float d)
{
return c * sin(t / d * (M_PI / 2)) + b;
}
static float easing_in_out_sine(float t, float b, float c, float d)
{
return -c / 2 * (cos(M_PI * t / d) - 1) + b;
}
static float easing_out_in_sine(float t, float b, float c, float d)
{
if (t < d / 2)
return easing_out_sine(t * 2, b, c / 2, d);
return easing_in_sine((t * 2) - d, b + c / 2, c / 2, d);
}
static float easing_in_expo(float t, float b, float c, float d)
{
if (t == 0)
return b;
return c * powf(2, 10 * (t / d - 1)) + b - c * 0.001;
}
static float easing_out_expo(float t, float b, float c, float d)
{
if (t == d)
return b + c;
return c * 1.001 * (-powf(2, -10 * t / d) + 1) + b;
}
static float easing_in_out_expo(float t, float b, float c, float d)
{
if (t == 0)
return b;
if (t == d)
return b + c;
t = t / d * 2;
if (t < 1)
return c / 2 * powf(2, 10 * (t - 1)) + b - c * 0.0005;
return c / 2 * 1.0005 * (-powf(2, -10 * (t - 1)) + 2) + b;
}
static float easing_out_in_expo(float t, float b, float c, float d)
{
if (t < d / 2)
return easing_out_expo(t * 2, b, c / 2, d);
return easing_in_expo((t * 2) - d, b + c / 2, c / 2, d);
}
static float easing_in_circ(float t, float b, float c, float d)
{
return (-c * (sqrt(1 - powf(t / d, 2)) - 1) + b);
}
static float easing_out_circ(float t, float b, float c, float d)
{
return (c * sqrt(1 - powf(t / d - 1, 2)) + b);
}
static float easing_in_out_circ(float t, float b, float c, float d)
{
t = t / d * 2;
if (t < 1)
return -c / 2 * (sqrt(1 - t * t) - 1) + b;
t = t - 2;
return c / 2 * (sqrt(1 - t * t) + 1) + b;
}
static float easing_out_in_circ(float t, float b, float c, float d)
{
if (t < d / 2)
return easing_out_circ(t * 2, b, c / 2, d);
return easing_in_circ((t * 2) - d, b + c / 2, c / 2, d);
}
static float easing_out_bounce(float t, float b, float c, float d)
{
t = t / d;
if (t < 1 / 2.75)
return c * (7.5625 * t * t) + b;
if (t < 2 / 2.75)
{
t = t - (1.5 / 2.75);
return c * (7.5625 * t * t + 0.75) + b;
}
else if (t < 2.5 / 2.75)
{
t = t - (2.25 / 2.75);
return c * (7.5625 * t * t + 0.9375) + b;
}
t = t - (2.625 / 2.75);
return c * (7.5625 * t * t + 0.984375) + b;
}
static float easing_in_bounce(float t, float b, float c, float d)
{
return c - easing_out_bounce(d - t, 0, c, d) + b;
}
static float easing_in_out_bounce(float t, float b, float c, float d)
{
if (t < d / 2)
return easing_in_bounce(t * 2, 0, c, d) * 0.5 + b;
return easing_out_bounce(t * 2 - d, 0, c, d) * 0.5 + c * .5 + b;
}
static float easing_out_in_bounce(float t, float b, float c, float d)
{
if (t < d / 2)
return easing_out_bounce(t * 2, b, c / 2, d);
return easing_in_bounce((t * 2) - d, b + c / 2, c / 2, d);
}
static void menu_animation_ticker_generic(uint64_t idx,
size_t max_width, size_t* offset, size_t* width)
{
int ticker_period = (int)(2 * (*width - max_width) + 4);
int phase = idx % ticker_period;
int phase_left_stop = 2;
int phase_left_moving = (int)(phase_left_stop + (*width - max_width));
int phase_right_stop = phase_left_moving + 2;
int left_offset = phase - phase_left_stop;
int right_offset = (int)((*width - max_width) - (phase - phase_right_stop));
if (phase < phase_left_stop)
*offset = 0;
else if (phase < phase_left_moving)
*offset = left_offset;
else if (phase < phase_right_stop)
*offset = *width - max_width;
else
*offset = right_offset;
*width = max_width;
}
static void menu_animation_ticker_loop(uint64_t idx,
size_t max_width, size_t str_width, size_t spacer_width,
size_t* offset1, size_t* width1,
size_t* offset2, size_t* width2,
size_t* offset3, size_t* width3)
{
int ticker_period = (int)(str_width + spacer_width);
int phase = idx % ticker_period;
/* Output offsets/widths are unsigned size_t, but it's
* easier to perform the required calculations with ints,
* so create some temporary variables... */
int offset;
int width;
/* Looping text is composed of up to three strings,
* where std::string 1 and 2 are different regions of the
* source text and std::string 2 is a spacer:
*
* |-----max_width-----|
* [std::string 1][std::string 2][std::string 3]
*
* The following implementation could probably be optimised,
* but any performance gains would be trivial compared with
* all the std::string manipulation that has to happen afterwards...
*/
/* String 1 */
offset = (phase < (int)str_width) ? phase : 0;
width = (int)(str_width - phase);
width = (width < 0) ? 0 : width;
width = (width > (int)max_width) ? max_width : width;
*offset1 = offset;
*width1 = width;
/* String 2 */
offset = (int)(phase - str_width);
offset = offset < 0 ? 0 : offset;
width = (int)(max_width - *width1);
width = (width > (int)spacer_width) ? spacer_width : width;
width = width - offset;
*offset2 = offset;
*width2 = width;
/* String 3 */
width = max_width - (*width1 + *width2);
width = width < 0 ? 0 : width;
/* Note: offset is always zero here so offset3 is
* unnecessary - but include it anyway to preserve
* symmetry... */
*offset3 = 0;
*width3 = width;
}
void menu_animation_init(void)
{
// Nothing to do
}
void menu_animation_free(void)
{
anim.list.clear();
anim.pending.clear();
anim.in_update = false;
anim.pending_deletes = false;
}
static void menu_delayed_animation_cb(void* userdata)
{
menu_delayed_animation_t* delayed_animation = (menu_delayed_animation_t*)userdata;
menu_animation_push(&delayed_animation->entry);
free(delayed_animation);
}
void menu_animation_push_delayed(unsigned delay, menu_animation_ctx_entry_t* entry)
{
menu_timer_ctx_entry_t timer_entry;
menu_delayed_animation_t* delayed_animation = (menu_delayed_animation_t*)malloc(sizeof(menu_delayed_animation_t));
delayed_animation->entry.cb = entry->cb;
delayed_animation->entry.duration = entry->duration;
delayed_animation->entry.easing_enum = entry->easing_enum;
delayed_animation->entry.subject = entry->subject;
delayed_animation->entry.tag = entry->tag;
delayed_animation->entry.target_value = entry->target_value;
delayed_animation->entry.tick = entry->tick;
delayed_animation->entry.userdata = entry->userdata;
timer_entry.cb = menu_delayed_animation_cb;
timer_entry.tick = NULL;
timer_entry.duration = delay;
timer_entry.userdata = delayed_animation;
menu_timer_start(&delayed_animation->timer, &timer_entry);
}
bool menu_animation_push(menu_animation_ctx_entry_t* entry)
{
struct tween t;
t.duration = entry->duration;
t.running_since = 0;
t.initial_value = *entry->subject;
t.target_value = entry->target_value;
t.subject = entry->subject;
t.tag = entry->tag;
t.cb = entry->cb;
t.tick = entry->tick;
t.userdata = entry->userdata;
t.easing = NULL;
t.deleted = false;
switch (entry->easing_enum)
{
case EASING_LINEAR:
t.easing = &easing_linear;
break;
/* Quad */
case EASING_IN_QUAD:
t.easing = &easing_in_quad;
break;
case EASING_OUT_QUAD:
t.easing = &easing_out_quad;
break;
case EASING_IN_OUT_QUAD:
t.easing = &easing_in_out_quad;
break;
case EASING_OUT_IN_QUAD:
t.easing = &easing_out_in_quad;
break;
/* Cubic */
case EASING_IN_CUBIC:
t.easing = &easing_in_cubic;
break;
case EASING_OUT_CUBIC:
t.easing = &easing_out_cubic;
break;
case EASING_IN_OUT_CUBIC:
t.easing = &easing_in_out_cubic;
break;
case EASING_OUT_IN_CUBIC:
t.easing = &easing_out_in_cubic;
break;
/* Quart */
case EASING_IN_QUART:
t.easing = &easing_in_quart;
break;
case EASING_OUT_QUART:
t.easing = &easing_out_quart;
break;
case EASING_IN_OUT_QUART:
t.easing = &easing_in_out_quart;
break;
case EASING_OUT_IN_QUART:
t.easing = &easing_out_in_quart;
break;
/* Quint */
case EASING_IN_QUINT:
t.easing = &easing_in_quint;
break;
case EASING_OUT_QUINT:
t.easing = &easing_out_quint;
break;
case EASING_IN_OUT_QUINT:
t.easing = &easing_in_out_quint;
break;
case EASING_OUT_IN_QUINT:
t.easing = &easing_out_in_quint;
break;
/* Sine */
case EASING_IN_SINE:
t.easing = &easing_in_sine;
break;
case EASING_OUT_SINE:
t.easing = &easing_out_sine;
break;
case EASING_IN_OUT_SINE:
t.easing = &easing_in_out_sine;
break;
case EASING_OUT_IN_SINE:
t.easing = &easing_out_in_sine;
break;
/* Expo */
case EASING_IN_EXPO:
t.easing = &easing_in_expo;
break;
case EASING_OUT_EXPO:
t.easing = &easing_out_expo;
break;
case EASING_IN_OUT_EXPO:
t.easing = &easing_in_out_expo;
break;
case EASING_OUT_IN_EXPO:
t.easing = &easing_out_in_expo;
break;
/* Circ */
case EASING_IN_CIRC:
t.easing = &easing_in_circ;
break;
case EASING_OUT_CIRC:
t.easing = &easing_out_circ;
break;
case EASING_IN_OUT_CIRC:
t.easing = &easing_in_out_circ;
break;
case EASING_OUT_IN_CIRC:
t.easing = &easing_out_in_circ;
break;
/* Bounce */
case EASING_IN_BOUNCE:
t.easing = &easing_in_bounce;
break;
case EASING_OUT_BOUNCE:
t.easing = &easing_out_bounce;
break;
case EASING_IN_OUT_BOUNCE:
t.easing = &easing_in_out_bounce;
break;
case EASING_OUT_IN_BOUNCE:
t.easing = &easing_out_in_bounce;
break;
default:
break;
}
/* ignore born dead tweens */
if (!t.easing || t.duration == 0 || t.initial_value == t.target_value)
return false;
if (anim.in_update)
anim.pending.push_back(t);
else
anim.list.push_back(t);
return true;
}
void menu_animation_get_highlight(float* gradient_x, float* gradient_y, float* color)
{
if (gradient_x)
*gradient_x = (float)highlight_gradient_x;
if (gradient_y)
*gradient_y = (float)highlight_gradient_y;
if (color)
*color = (float)highlight_color;
}
#define HIGHLIGHT_SPEED 350.0
static void menu_animation_update_time(bool timedate_enable)
{
static retro_time_t
last_clock_update
= 0;
static retro_time_t
last_ticker_update
= 0;
static retro_time_t
last_ticker_slow_update
= 0;
/* Adjust ticker speed */
float speed_factor = 1.0f;
unsigned ticker_speed = (unsigned)(((float)TICKER_SPEED / speed_factor) + 0.5);
unsigned ticker_slow_speed = (unsigned)(((float)TICKER_SLOW_SPEED / speed_factor) + 0.5);
cur_time = cpu_features_get_time_usec() / 1000;
delta_time = old_time == 0 ? 0 : cur_time - old_time;
old_time = cur_time;
highlight_gradient_x = (cos((double)cur_time / HIGHLIGHT_SPEED / 3.0) + 1.0) / 2.0;
highlight_gradient_y = (sin((double)cur_time / HIGHLIGHT_SPEED / 3.0) + 1.0) / 2.0;
highlight_color = (sin((double)cur_time / HIGHLIGHT_SPEED * 2.0) + 1.0) / 2.0;
if (((cur_time - last_clock_update) > 1000)
&& timedate_enable)
{
animation_is_active = true;
last_clock_update = cur_time;
}
if (ticker_is_active
&& cur_time - last_ticker_update >= ticker_speed)
{
ticker_idx++;
last_ticker_update = cur_time;
}
if (ticker_is_active
&& cur_time - last_ticker_slow_update >= ticker_slow_speed)
{
ticker_slow_idx++;
last_ticker_slow_update = cur_time;
}
}
bool menu_animation_update(void)
{
unsigned i;
menu_animation_update_time(false);
anim.in_update = true;
anim.pending_deletes = false;
for (i = 0; i < anim.list.size(); i++)
{
struct tween* tween = &anim.list[i];
tween->running_since += delta_time;
*tween->subject = tween->easing(
tween->running_since,
tween->initial_value,
tween->target_value - tween->initial_value,
tween->duration);
if (tween->tick)
tween->tick(tween->userdata);
if (tween->running_since >= tween->duration)
{
*tween->subject = tween->target_value;
if (tween->cb)
tween->cb(tween->userdata);
anim.list.erase(anim.list.begin() + i);
i--;
}
}
if (anim.pending_deletes)
{
for (i = 0; i < anim.list.size(); i++)
{
struct tween* tween = &anim.list[i];
if (tween->deleted)
{
anim.list.erase(anim.list.begin() + i);
i--;
}
}
anim.pending_deletes = false;
}
if (anim.pending.size() > 0)
{
anim.list.insert(anim.list.begin(), anim.pending.begin(), anim.pending.end());
anim.pending.clear();
}
anim.in_update = false;
animation_is_active = anim.list.size() > 0;
return animation_is_active;
}
bool menu_animation_ticker(menu_animation_ctx_ticker_t* ticker)
{
size_t str_len = utf8len(ticker->str);
if (!ticker->spacer)
ticker->spacer = ticker_spacer_default;
if ((size_t)str_len <= ticker->len)
{
utf8cpy(ticker->s,
PATH_MAX_LENGTH,
ticker->str,
ticker->len);
return false;
}
if (!ticker->selected)
{
utf8cpy(ticker->s, PATH_MAX_LENGTH, ticker->str, ticker->len - 3);
strlcat(ticker->s, "...", PATH_MAX_LENGTH);
return false;
}
/* Note: If we reach this point then str_len > ticker->len
* (previously had an unecessary 'if (str_len > ticker->len)'
* check here...) */
switch (ticker->type_enum)
{
case TICKER_TYPE_LOOP:
{
size_t offset1, offset2, offset3;
size_t width1, width2, width3;
/* Horribly oversized temporary buffer
* > utf8 support makes this whole thing incredibly
* ugly/inefficient. Not much we can do about it... */
char tmp[PATH_MAX_LENGTH];
tmp[0] = '\0';
ticker->s[0] = '\0';
menu_animation_ticker_loop(
ticker->idx,
ticker->len,
str_len, utf8len(ticker->spacer),
&offset1, &width1,
&offset2, &width2,
&offset3, &width3);
if (width1 > 0)
{
utf8cpy(
ticker->s,
PATH_MAX_LENGTH,
utf8skip(ticker->str, offset1),
width1);
}
if (width2 > 0)
{
utf8cpy(
tmp,
PATH_MAX_LENGTH,
utf8skip(ticker->spacer, offset2),
width2);
strlcat(ticker->s, tmp, PATH_MAX_LENGTH);
}
if (width3 > 0)
{
utf8cpy(
tmp,
PATH_MAX_LENGTH,
utf8skip(ticker->str, offset3),
width3);
strlcat(ticker->s, tmp, PATH_MAX_LENGTH);
}
break;
}
case TICKER_TYPE_BOUNCE:
default:
{
size_t offset = 0;
menu_animation_ticker_generic(
ticker->idx,
ticker->len,
&offset,
&str_len);
utf8cpy(
ticker->s,
PATH_MAX_LENGTH,
utf8skip(ticker->str, offset),
str_len);
break;
}
}
ticker_is_active = true;
return true;
}
bool menu_animation_is_active(void)
{
return animation_is_active || ticker_is_active;
}
bool menu_animation_kill_by_tag(menu_animation_ctx_tag* tag)
{
unsigned i;
if (!tag || *tag == (uintptr_t)-1)
return false;
for (i = 0; i < anim.list.size(); ++i)
{
struct tween* t = &anim.list[i];
if (t->tag != *tag)
continue;
if (anim.in_update)
{
t->deleted = true;
anim.pending_deletes = true;
}
else
{
anim.list.erase(anim.list.begin() + i);
--i;
}
}
return true;
}
void menu_animation_kill_by_subject(menu_animation_ctx_subject_t* subject)
{
unsigned i, j, killed = 0;
float** sub = (float**)subject->data;
for (i = 0; i < anim.list.size() && killed < subject->count; ++i)
{
struct tween* t = &anim.list[i];
for (j = 0; j < subject->count; ++j)
{
if (t->subject != sub[j])
continue;
if (anim.in_update)
{
t->deleted = true;
anim.pending_deletes = true;
}
else
{
anim.list.erase(anim.list.begin() + i);
--i;
}
killed++;
break;
}
}
}
float menu_animation_get_delta_time(void)
{
return delta_time;
}
bool menu_animation_ctl(enum menu_animation_ctl_state state, void* data)
{
switch (state)
{
case MENU_ANIMATION_CTL_CLEAR_ACTIVE:
animation_is_active = false;
ticker_is_active = false;
break;
case MENU_ANIMATION_CTL_SET_ACTIVE:
animation_is_active = true;
ticker_is_active = true;
break;
case MENU_ANIMATION_CTL_NONE:
default:
break;
}
return true;
}
void menu_timer_start(menu_timer_t* timer, menu_timer_ctx_entry_t* timer_entry)
{
menu_animation_ctx_entry_t entry;
menu_animation_ctx_tag tag = (uintptr_t)timer;
menu_timer_kill(timer);
*timer = 0.0f;
entry.easing_enum = EASING_LINEAR;
entry.tag = tag;
entry.duration = timer_entry->duration;
entry.target_value = 1.0f;
entry.subject = timer;
entry.cb = timer_entry->cb;
entry.tick = timer_entry->tick;
entry.userdata = timer_entry->userdata;
menu_animation_push(&entry);
}
void menu_timer_kill(menu_timer_t* timer)
{
menu_animation_ctx_tag tag = (uintptr_t)timer;
menu_animation_kill_by_tag(&tag);
}
uint64_t menu_animation_get_ticker_idx(void)
{
return ticker_idx;
}
uint64_t menu_animation_get_ticker_slow_idx(void)
{
return ticker_slow_idx;
}
} // namespace brls

View File

@@ -0,0 +1,414 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019 natinusala
Copyright (C) 2019 p-sam
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <borealis/applet_frame.hpp>
#include <borealis/application.hpp>
namespace brls
{
AppletFrame::AppletFrame(bool padLeft, bool padRight)
{
Style* style = Application::getStyle();
if (padLeft)
this->leftPadding = style->AppletFrame.separatorSpacing;
if (padRight)
this->rightPadding = style->AppletFrame.separatorSpacing;
this->hint = new Hint();
this->hint->setParent(this);
this->registerAction("Back", Key::B, [this] { return this->onCancel(); });
}
void AppletFrame::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, Style* style, FrameContext* ctx)
{
// Text
if (this->headerStyle == HeaderStyle::REGULAR)
{
// Title
NVGcolor titleColor = a(ctx->theme->textColor);
if (this->contentView)
titleColor.a *= this->contentView->getAlpha();
nvgFillColor(vg, titleColor);
nvgFontSize(vg, style->AppletFrame.titleSize);
nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
nvgFontFaceId(vg, ctx->fontStash->regular);
nvgBeginPath(vg);
nvgText(vg, x + style->AppletFrame.titleStart, y + style->AppletFrame.headerHeightRegular / 2 + style->AppletFrame.titleOffset, this->title.c_str(), nullptr);
// Header
nvgBeginPath(vg);
nvgFillColor(vg, a(ctx->theme->textColor));
nvgRect(vg, x + style->AppletFrame.separatorSpacing, y + style->AppletFrame.headerHeightRegular - 1, width - style->AppletFrame.separatorSpacing * 2, 1);
nvgFill(vg);
}
else if (this->headerStyle == HeaderStyle::POPUP)
{
// Header Text
nvgBeginPath(vg);
nvgFillColor(vg, a(ctx->theme->textColor));
nvgFontFaceId(vg, ctx->fontStash->regular);
nvgFontSize(vg, style->PopupFrame.headerFontSize);
nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
nvgText(vg, x + style->PopupFrame.headerTextLeftPadding,
y + style->PopupFrame.headerTextTopPadding,
this->title.c_str(),
nullptr);
// Sub title text 1
nvgBeginPath(vg);
nvgFillColor(vg, a(ctx->theme->descriptionColor));
nvgFontFaceId(vg, ctx->fontStash->regular);
nvgFontSize(vg, style->PopupFrame.subTitleFontSize);
nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_TOP);
nvgText(vg, x + style->PopupFrame.subTitleLeftPadding,
y + style->PopupFrame.subTitleTopPadding,
this->subTitleLeft.c_str(),
nullptr);
float bounds[4];
nvgTextBounds(vg, x, y, this->subTitleLeft.c_str(), nullptr, bounds);
// Sub title separator
nvgFillColor(vg, a(ctx->theme->descriptionColor)); // we purposely don't apply opacity
nvgBeginPath(vg);
nvgRect(vg, x + style->PopupFrame.subTitleLeftPadding + (bounds[2] - bounds[0]) + style->PopupFrame.subTitleSpacing,
y + style->PopupFrame.subTitleSeparatorTopPadding,
1,
style->PopupFrame.subTitleSeparatorHeight);
nvgFill(vg);
// Sub title text 2
nvgBeginPath(vg);
nvgFillColor(vg, a(ctx->theme->descriptionColor));
nvgFontFaceId(vg, ctx->fontStash->regular);
nvgFontSize(vg, style->PopupFrame.subTitleFontSize);
nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_TOP);
nvgText(vg, x + style->PopupFrame.subTitleLeftPadding + (bounds[2] - bounds[0]) + (style->PopupFrame.subTitleSpacing * 2),
y + style->PopupFrame.subTitleTopPadding,
this->subTitleRight.c_str(),
nullptr);
// Header
nvgBeginPath(vg);
nvgRect(vg, x + style->AppletFrame.separatorSpacing, y + style->AppletFrame.headerHeightPopup - 1, width - style->AppletFrame.separatorSpacing * 2, 1);
nvgFill(vg);
}
// Footer
NVGcolor footerColor = a(ctx->theme->textColor);
if (this->slideIn)
footerColor.a = 0.0f;
else if (this->slideOut)
footerColor.a = 1.0f;
nvgFillColor(vg, footerColor);
std::string* text = &this->footerText;
if (*text == "")
text = Application::getCommonFooter();
nvgFontSize(vg, style->AppletFrame.footerTextSize);
nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
nvgBeginPath(vg);
nvgText(vg, x + style->AppletFrame.separatorSpacing + style->AppletFrame.footerTextSpacing, y + height - style->AppletFrame.footerHeight / 2, text->c_str(), nullptr);
// Hint
this->hint->frame(ctx);
// Icon
if (this->icon)
this->icon->frame(ctx);
// Separators
nvgFillColor(vg, a(ctx->theme->separatorColor));
// Footer
nvgBeginPath(vg);
nvgRect(vg, x + style->AppletFrame.separatorSpacing, y + height - style->AppletFrame.footerHeight, width - style->AppletFrame.separatorSpacing * 2, 1);
nvgFill(vg);
// Content view
if (contentView)
{
float slideAlpha = 1.0f - this->contentView->alpha;
if ((this->slideIn && this->animation == ViewAnimation::SLIDE_LEFT) || (this->slideOut && this->animation == ViewAnimation::SLIDE_RIGHT))
slideAlpha = 1.0f - slideAlpha;
int translation = (int)((float)style->AppletFrame.slideAnimation * slideAlpha);
if ((this->slideIn && this->animation == ViewAnimation::SLIDE_LEFT) || (this->slideOut && this->animation == ViewAnimation::SLIDE_RIGHT))
translation -= style->AppletFrame.slideAnimation;
if (this->slideOut || this->slideIn)
nvgTranslate(vg, -translation, 0);
contentView->frame(ctx);
if (this->slideOut || this->slideIn)
nvgTranslate(vg, translation, 0);
}
}
View* AppletFrame::getDefaultFocus()
{
if (this->contentView)
return this->contentView->getDefaultFocus();
return nullptr;
}
void AppletFrame::layout(NVGcontext* vg, Style* style, FontStash* stash)
{
// Icon
if (this->icon)
{
if (this->headerStyle == HeaderStyle::REGULAR)
{
this->icon->setBoundaries(style->AppletFrame.imageLeftPadding, style->AppletFrame.imageTopPadding, style->AppletFrame.imageSize, style->AppletFrame.imageSize);
this->icon->invalidate();
}
else if (this->headerStyle == HeaderStyle::POPUP)
{
this->icon->setBoundaries(style->PopupFrame.edgePadding + style->PopupFrame.imageLeftPadding, style->PopupFrame.imageTopPadding, style->PopupFrame.imageSize, style->PopupFrame.imageSize);
this->icon->invalidate();
}
}
// Content
if (this->contentView)
{
if (this->headerStyle == HeaderStyle::REGULAR)
this->contentView->setBoundaries(this->x + leftPadding, this->y + style->AppletFrame.headerHeightRegular, this->width - this->leftPadding - this->rightPadding, this->height - style->AppletFrame.footerHeight - style->AppletFrame.headerHeightRegular);
else if (this->headerStyle == HeaderStyle::POPUP)
this->contentView->setBoundaries(this->x + leftPadding, this->y + style->AppletFrame.headerHeightPopup, this->width - this->leftPadding - this->rightPadding, this->height - style->AppletFrame.footerHeight - style->AppletFrame.headerHeightPopup);
this->contentView->invalidate();
}
// Hint
// TODO: convert the bottom-left footer into a Label to get its width and avoid clipping with the hint
unsigned hintWidth = this->width - style->AppletFrame.separatorSpacing * 2 - style->AppletFrame.footerTextSpacing * 2;
this->hint->setBoundaries(
this->x + this->width - hintWidth - style->AppletFrame.separatorSpacing - style->AppletFrame.footerTextSpacing,
this->y + this->height - style->AppletFrame.footerHeight,
hintWidth,
style->AppletFrame.footerHeight);
this->hint->invalidate();
}
void AppletFrame::setContentView(View* view)
{
this->contentView = view;
if (this->contentView)
{
this->contentView->setParent(this);
this->contentView->willAppear();
}
this->invalidate();
}
bool AppletFrame::hasContentView()
{
return this->contentView;
}
void AppletFrame::setTitle(std::string title)
{
this->title = title;
}
void AppletFrame::setFooterText(std::string footerText)
{
this->footerText = footerText;
}
void AppletFrame::setSubtitle(std::string left, std::string right)
{
this->subTitleLeft = left;
this->subTitleRight = right;
}
void AppletFrame::setIcon(unsigned char* buffer, size_t bufferSize)
{
if (!this->icon)
{
Image* icon = new Image(buffer, bufferSize);
icon->setScaleType(ImageScaleType::SCALE);
icon->setParent(this);
this->icon = icon;
}
else if (Image* icon = dynamic_cast<Image*>(this->icon))
{
icon->setImage(buffer, bufferSize);
}
this->icon->invalidate();
}
void AppletFrame::setIcon(std::string imagePath)
{
if (!this->icon)
{
Image* icon = new Image(imagePath);
icon->setScaleType(ImageScaleType::SCALE);
icon->setParent(this);
this->icon = icon;
}
else if (Image* icon = dynamic_cast<Image*>(this->icon))
{
icon->setImage(imagePath);
}
this->icon->invalidate();
}
void AppletFrame::setIcon(View* view)
{
if (this->icon)
delete this->icon;
if (view != nullptr)
view->setParent(this);
this->icon = view;
}
void AppletFrame::setHeaderStyle(HeaderStyle headerStyle)
{
this->headerStyle = headerStyle;
this->invalidate();
}
AppletFrame::~AppletFrame()
{
if (this->contentView)
{
this->contentView->willDisappear(true);
delete this->contentView;
}
if (this->icon)
delete this->icon;
delete this->hint;
}
void AppletFrame::willAppear(bool resetState)
{
if (this->icon)
this->icon->willAppear(resetState);
if (this->contentView)
this->contentView->willAppear(resetState);
this->hint->willAppear(resetState);
}
void AppletFrame::willDisappear(bool resetState)
{
if (this->icon)
this->icon->willDisappear(resetState);
if (this->contentView)
this->contentView->willDisappear(resetState);
this->hint->willDisappear(resetState);
}
void AppletFrame::show(std::function<void(void)> cb, bool animated, ViewAnimation animation)
{
this->animation = animation;
if (animated && (animation == ViewAnimation::SLIDE_LEFT || animation == ViewAnimation::SLIDE_RIGHT) && this->contentView)
{
this->slideIn = true;
this->contentView->show([this]() {
this->slideIn = false;
},
true, animation);
}
else if (this->contentView && this->contentView->isHidden())
{
this->contentView->show([]() {}, animated, animation);
}
View::show(cb, animated, animation);
}
void AppletFrame::hide(std::function<void(void)> cb, bool animated, ViewAnimation animation)
{
this->animation = animation;
if (animated && (animation == ViewAnimation::SLIDE_LEFT || animation == ViewAnimation::SLIDE_RIGHT) && this->contentView)
{
this->slideOut = true;
this->contentView->hide([this, cb]() {
this->slideOut = false;
},
true, animation);
}
else if (this->contentView && !this->contentView->isHidden())
{
this->contentView->hide([]() {}, animated, animation);
}
View::hide(cb, animated, animation);
}
bool AppletFrame::onCancel()
{
/*
* TODO: this assumes AppletFrames are used as "activities" in the app, which may be wrong
* so we should instead change the view stack to an "activity" stack and have them popped when
* the user presses B on the root view of an "activity"
*/
Application::popView();
return true;
}
void AppletFrame::onWindowSizeChanged()
{
if (this->contentView)
this->contentView->onWindowSizeChanged();
if (this->icon)
this->icon->onWindowSizeChanged();
if (this->hint)
this->hint->onWindowSizeChanged();
}
} // namespace brls

View File

@@ -0,0 +1,991 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019-2020 natinusala
Copyright (C) 2019 p-sam
Copyright (C) 2020 WerWolv
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <algorithm>
#include <borealis.hpp>
#include <string>
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>
#include <glad.h>
#define GLM_FORCE_PURE
#define GLM_ENABLE_EXPERIMENTAL
#include <nanovg.h>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtx/rotate_vector.hpp>
#include <glm/mat4x4.hpp>
#include <glm/vec3.hpp>
#include <glm/vec4.hpp>
#define NANOVG_GL3_IMPLEMENTATION
#include <nanovg_gl.h>
#ifdef __SWITCH__
#include <switch.h>
#endif
#include <chrono>
#include <set>
#include <thread>
// Constants used for scaling as well as
// creating a window of the right size on PC
constexpr uint32_t WINDOW_WIDTH = 1280;
constexpr uint32_t WINDOW_HEIGHT = 720;
#define DEFAULT_FPS 60
#define BUTTON_REPEAT_DELAY 15
#define BUTTON_REPEAT_CADENCY 5
// glfw code from the glfw hybrid app by fincs
// https://github.com/fincs/hybrid_app
namespace brls
{
// TODO: Use this instead of a glViewport each frame
static void windowFramebufferSizeCallback(GLFWwindow* window, int width, int height)
{
if (!width || !height)
return;
glViewport(0, 0, width, height);
Application::windowScale = (float)width / (float)WINDOW_WIDTH;
float contentHeight = ((float)height / (Application::windowScale * (float)WINDOW_HEIGHT)) * (float)WINDOW_HEIGHT;
Application::contentWidth = WINDOW_WIDTH;
Application::contentHeight = (unsigned)roundf(contentHeight);
Application::resizeNotificationManager();
Logger::info("Window size changed to %dx%d", width, height);
Logger::info("New scale factor is %f", Application::windowScale);
}
static void joystickCallback(int jid, int event)
{
if (event == GLFW_CONNECTED)
{
Logger::info("Joystick %d connected", jid);
if (glfwJoystickIsGamepad(jid))
Logger::info("Joystick %d is gamepad: \"%s\"", jid, glfwGetGamepadName(jid));
}
else if (event == GLFW_DISCONNECTED)
Logger::info("Joystick %d disconnected", jid);
}
static void errorCallback(int errorCode, const char* description)
{
Logger::error("[GLFW:%d] %s", errorCode, description);
}
static void windowKeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods)
{
if (action == GLFW_PRESS)
{
// Check for toggle-fullscreen combo
if (key == GLFW_KEY_ENTER && mods == GLFW_MOD_ALT)
{
static int saved_x, saved_y, saved_width, saved_height;
if (!glfwGetWindowMonitor(window))
{
// Back up window position/size
glfwGetWindowPos(window, &saved_x, &saved_y);
glfwGetWindowSize(window, &saved_width, &saved_height);
// Switch to fullscreen mode
GLFWmonitor* monitor = glfwGetPrimaryMonitor();
const GLFWvidmode* mode = glfwGetVideoMode(monitor);
glfwSetWindowMonitor(window, monitor, 0, 0, mode->width, mode->height, mode->refreshRate);
}
else
{
// Switch back to windowed mode
glfwSetWindowMonitor(window, nullptr, saved_x, saved_y, saved_width, saved_height, GLFW_DONT_CARE);
}
}
}
}
bool Application::init(std::string title)
{
return Application::init(title, Style::horizon(), Theme::horizon());
}
bool Application::init(std::string title, Style style, Theme theme)
{
// Init rng
std::srand(std::time(nullptr));
// Init managers
Application::taskManager = new TaskManager();
Application::notificationManager = new NotificationManager();
// Init static variables
Application::currentStyle = style;
Application::currentFocus = nullptr;
Application::oldGamepad = {};
Application::gamepad = {};
Application::title = title;
// Init theme to defaults
Application::setTheme(theme);
// Init glfw
glfwSetErrorCallback(errorCallback);
glfwInitHint(GLFW_JOYSTICK_HAT_BUTTONS, GLFW_FALSE);
if (!glfwInit())
{
Logger::error("Failed to initialize glfw");
return false;
}
// Create window
#ifdef __APPLE__
// Explicitly ask for a 3.2 context on OS X
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// Force scaling off to keep desired framebuffer size
glfwWindowHint(GLFW_COCOA_RETINA_FRAMEBUFFER, GLFW_FALSE);
#else
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#endif
Application::window = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, title.c_str(), nullptr, nullptr);
if (!window)
{
Logger::error("glfw: failed to create window\n");
glfwTerminate();
return false;
}
// Configure window
glfwSetInputMode(window, GLFW_STICKY_KEYS, GLFW_TRUE);
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, windowFramebufferSizeCallback);
glfwSetKeyCallback(window, windowKeyCallback);
glfwSetJoystickCallback(joystickCallback);
// Load OpenGL routines using glad
gladLoadGLLoader((GLADloadproc)glfwGetProcAddress);
glfwSwapInterval(1);
Logger::info("GL Vendor: %s", glGetString(GL_VENDOR));
Logger::info("GL Renderer: %s", glGetString(GL_RENDERER));
Logger::info("GL Version: %s", glGetString(GL_VERSION));
if (glfwJoystickIsGamepad(GLFW_JOYSTICK_1))
{
GLFWgamepadstate state;
Logger::info("Gamepad detected: %s", glfwGetGamepadName(GLFW_JOYSTICK_1));
glfwGetGamepadState(GLFW_JOYSTICK_1, &state);
}
// Initialize the scene
Application::vg = nvgCreateGL3(NVG_STENCIL_STROKES | NVG_ANTIALIAS);
if (!vg)
{
Logger::error("Unable to init nanovg");
glfwTerminate();
return false;
}
windowFramebufferSizeCallback(window, WINDOW_WIDTH, WINDOW_HEIGHT);
glfwSetTime(0.0);
// Load fonts
#ifdef __SWITCH__
{
PlFontData font;
// Standard font
Result rc = plGetSharedFontByType(&font, PlSharedFontType_Standard);
if (R_SUCCEEDED(rc))
{
Logger::info("Using Switch shared font");
Application::fontStash.regular = Application::loadFontFromMemory("regular", font.address, font.size, false);
}
// Korean font
rc = plGetSharedFontByType(&font, PlSharedFontType_KO);
if (R_SUCCEEDED(rc))
{
Logger::info("Adding Switch shared Korean font");
Application::fontStash.korean = Application::loadFontFromMemory("korean", font.address, font.size, false);
nvgAddFallbackFontId(Application::vg, Application::fontStash.regular, Application::fontStash.korean);
}
// Extented font
rc = plGetSharedFontByType(&font, PlSharedFontType_NintendoExt);
if (R_SUCCEEDED(rc))
{
Logger::info("Using Switch shared symbols font");
Application::fontStash.sharedSymbols = Application::loadFontFromMemory("symbols", font.address, font.size, false);
}
}
#else
// Use illegal font if available
if (access(BOREALIS_ASSET("Illegal-Font.ttf"), F_OK) != -1)
Application::fontStash.regular = Application::loadFont("regular", BOREALIS_ASSET("Illegal-Font.ttf"));
else
Application::fontStash.regular = Application::loadFont("regular", BOREALIS_ASSET("inter/Inter-Switch.ttf"));
if (access(BOREALIS_ASSET("Wingdings.ttf"), F_OK) != -1)
Application::fontStash.sharedSymbols = Application::loadFont("sharedSymbols", BOREALIS_ASSET("Wingdings.ttf"));
#endif
// Material font
if (access(BOREALIS_ASSET("material/MaterialIcons-Regular.ttf"), F_OK) != -1)
Application::fontStash.material = Application::loadFont("material", BOREALIS_ASSET("material/MaterialIcons-Regular.ttf"));
// Set symbols font as fallback
if (Application::fontStash.sharedSymbols)
{
Logger::info("Using shared symbols font");
nvgAddFallbackFontId(Application::vg, Application::fontStash.regular, Application::fontStash.sharedSymbols);
}
else
{
Logger::error("Shared symbols font not found");
}
// Set Material as fallback
if (Application::fontStash.material)
{
Logger::info("Using Material font");
nvgAddFallbackFontId(Application::vg, Application::fontStash.regular, Application::fontStash.material);
}
else
{
Logger::error("Material font not found");
}
// Load theme
#ifdef __SWITCH__
ColorSetId nxTheme;
setsysGetColorSetId(&nxTheme);
if (nxTheme == ColorSetId_Dark)
Application::currentThemeVariant = ThemeVariant_DARK;
else
Application::currentThemeVariant = ThemeVariant_LIGHT;
#else
char* themeEnv = getenv("BOREALIS_THEME");
if (themeEnv != nullptr && !strcasecmp(themeEnv, "DARK"))
Application::currentThemeVariant = ThemeVariant_DARK;
else
Application::currentThemeVariant = ThemeVariant_LIGHT;
#endif
// Init window size
GLint viewport[4];
glGetIntegerv(GL_VIEWPORT, viewport);
Application::windowWidth = viewport[2];
Application::windowHeight = viewport[3];
// Init animations engine
menu_animation_init();
// Default FPS cap
Application::setMaximumFPS(DEFAULT_FPS);
return true;
}
bool Application::mainLoop()
{
// Frame start
retro_time_t frameStart = 0;
if (Application::frameTime > 0.0f)
frameStart = cpu_features_get_time_usec();
// glfw events
bool is_active;
do
{
is_active = !glfwGetWindowAttrib(Application::window, GLFW_ICONIFIED);
if (is_active)
glfwPollEvents();
else
glfwWaitEvents();
if (glfwWindowShouldClose(Application::window))
{
Application::exit();
return false;
}
} while (!is_active);
// libnx applet main loop
#ifdef __SWITCH__
if (!appletMainLoop())
{
Application::exit();
return false;
}
#endif
// Gamepad
if (!glfwGetGamepadState(GLFW_JOYSTICK_1, &Application::gamepad))
{
// Keyboard -> DPAD Mapping
Application::gamepad.buttons[GLFW_GAMEPAD_BUTTON_DPAD_LEFT] = glfwGetKey(window, GLFW_KEY_LEFT);
Application::gamepad.buttons[GLFW_GAMEPAD_BUTTON_DPAD_RIGHT] = glfwGetKey(window, GLFW_KEY_RIGHT);
Application::gamepad.buttons[GLFW_GAMEPAD_BUTTON_DPAD_UP] = glfwGetKey(window, GLFW_KEY_UP);
Application::gamepad.buttons[GLFW_GAMEPAD_BUTTON_DPAD_DOWN] = glfwGetKey(window, GLFW_KEY_DOWN);
Application::gamepad.buttons[GLFW_GAMEPAD_BUTTON_START] = glfwGetKey(window, GLFW_KEY_ESCAPE);
Application::gamepad.buttons[GLFW_GAMEPAD_BUTTON_BACK] = glfwGetKey(window, GLFW_KEY_F1);
Application::gamepad.buttons[GLFW_GAMEPAD_BUTTON_A] = glfwGetKey(window, GLFW_KEY_ENTER);
Application::gamepad.buttons[GLFW_GAMEPAD_BUTTON_B] = glfwGetKey(window, GLFW_KEY_BACKSPACE);
Application::gamepad.buttons[GLFW_GAMEPAD_BUTTON_LEFT_BUMPER] = glfwGetKey(window, GLFW_KEY_L);
Application::gamepad.buttons[GLFW_GAMEPAD_BUTTON_RIGHT_BUMPER] = glfwGetKey(window, GLFW_KEY_R);
}
// Trigger gamepad events
// TODO: Translate axis events to dpad events here
bool anyButtonPressed = false;
bool repeating = false;
static retro_time_t buttonPressTime = 0;
static int repeatingButtonTimer = 0;
for (int i = GLFW_GAMEPAD_BUTTON_A; i <= GLFW_GAMEPAD_BUTTON_LAST; i++)
{
if (Application::gamepad.buttons[i] == GLFW_PRESS)
{
anyButtonPressed = true;
repeating = (repeatingButtonTimer > BUTTON_REPEAT_DELAY && repeatingButtonTimer % BUTTON_REPEAT_CADENCY == 0);
if (Application::oldGamepad.buttons[i] != GLFW_PRESS || repeating)
Application::onGamepadButtonPressed(i, repeating);
}
if (Application::gamepad.buttons[i] != Application::oldGamepad.buttons[i])
buttonPressTime = repeatingButtonTimer = 0;
}
if (anyButtonPressed && cpu_features_get_time_usec() - buttonPressTime > 1000)
{
buttonPressTime = cpu_features_get_time_usec();
repeatingButtonTimer++; // Increased once every ~1ms
}
Application::oldGamepad = Application::gamepad;
// Handle window size changes
GLint viewport[4];
glGetIntegerv(GL_VIEWPORT, viewport);
unsigned newWidth = viewport[2];
unsigned newHeight = viewport[3];
if (Application::windowWidth != newWidth || Application::windowHeight != newHeight)
{
Application::windowWidth = newWidth;
Application::windowHeight = newHeight;
Application::onWindowSizeChanged();
}
// Animations
menu_animation_update();
// Tasks
Application::taskManager->frame();
// Render
Application::frame();
glfwSwapBuffers(window);
// Sleep if necessary
if (Application::frameTime > 0.0f)
{
retro_time_t currentFrameTime = cpu_features_get_time_usec() - frameStart;
retro_time_t frameTime = (retro_time_t)(Application::frameTime * 1000);
if (frameTime > currentFrameTime)
{
retro_time_t toSleep = frameTime - currentFrameTime;
std::this_thread::sleep_for(std::chrono::microseconds(toSleep));
}
}
return true;
}
void Application::quit()
{
glfwSetWindowShouldClose(window, GLFW_TRUE);
}
void Application::navigate(FocusDirection direction)
{
View* currentFocus = Application::currentFocus;
// Do nothing if there is no current focus or if it doesn't have a parent
// (in which case there is nothing to traverse)
if (!currentFocus || !currentFocus->hasParent())
return;
// Get next view to focus by traversing the views tree upwards
View* nextFocus = currentFocus->getParent()->getNextFocus(direction, currentFocus->getParentUserData());
while (!nextFocus) // stop when we find a view to focus
{
if (!currentFocus->hasParent() || !currentFocus->getParent()->hasParent()) // stop when we reach the root of the tree
break;
currentFocus = currentFocus->getParent();
nextFocus = currentFocus->getParent()->getNextFocus(direction, currentFocus->getParentUserData());
}
// No view to focus at the end of the traversal: wiggle and return
if (!nextFocus)
{
Application::currentFocus->shakeHighlight(direction);
return;
}
// Otherwise give it focus
Application::giveFocus(nextFocus);
}
void Application::onGamepadButtonPressed(char button, bool repeating)
{
if (Application::blockInputsTokens != 0)
return;
if (repeating && Application::repetitionOldFocus == Application::currentFocus)
return;
Application::repetitionOldFocus = Application::currentFocus;
// Actions
if (Application::handleAction(button))
return;
// Navigation
// Only navigate if the button hasn't been consumed by an action
// (allows overriding DPAD buttons using actions)
switch (button)
{
case GLFW_GAMEPAD_BUTTON_DPAD_DOWN:
Application::navigate(FocusDirection::DOWN);
break;
case GLFW_GAMEPAD_BUTTON_DPAD_UP:
Application::navigate(FocusDirection::UP);
break;
case GLFW_GAMEPAD_BUTTON_DPAD_LEFT:
Application::navigate(FocusDirection::LEFT);
break;
case GLFW_GAMEPAD_BUTTON_DPAD_RIGHT:
Application::navigate(FocusDirection::RIGHT);
break;
default:
break;
}
}
View* Application::getCurrentFocus()
{
return Application::currentFocus;
}
bool Application::handleAction(char button)
{
View* hintParent = Application::currentFocus;
std::set<Key> consumedKeys;
while (hintParent != nullptr)
{
for (auto& action : hintParent->getActions())
{
if (action.key != static_cast<Key>(button))
continue;
if (consumedKeys.find(action.key) != consumedKeys.end())
continue;
if (action.available)
if (action.actionListener())
consumedKeys.insert(action.key);
}
hintParent = hintParent->getParent();
}
return !consumedKeys.empty();
}
void Application::frame()
{
// Frame context
FrameContext frameContext = FrameContext();
frameContext.pixelRatio = (float)Application::windowWidth / (float)Application::windowHeight;
frameContext.vg = Application::vg;
frameContext.fontStash = &Application::fontStash;
frameContext.theme = Application::getThemeValues();
nvgBeginFrame(Application::vg, Application::windowWidth, Application::windowHeight, frameContext.pixelRatio);
nvgScale(Application::vg, Application::windowScale, Application::windowScale);
// GL Clear
glClearColor(
frameContext.theme->backgroundColor[0],
frameContext.theme->backgroundColor[1],
frameContext.theme->backgroundColor[2],
1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
std::vector<View*> viewsToDraw;
// Draw all views in the stack
// until we find one that's not translucent
for (size_t i = 0; i < Application::viewStack.size(); i++)
{
View* view = Application::viewStack[Application::viewStack.size() - 1 - i];
viewsToDraw.push_back(view);
if (!view->isTranslucent())
break;
}
for (size_t i = 0; i < viewsToDraw.size(); i++)
{
View* view = viewsToDraw[viewsToDraw.size() - 1 - i];
view->frame(&frameContext);
}
// Framerate counter
if (Application::framerateCounter)
Application::framerateCounter->frame(&frameContext);
// Notifications
Application::notificationManager->frame(&frameContext);
// End frame
nvgResetTransform(Application::vg); // scale
nvgEndFrame(Application::vg);
}
void Application::exit()
{
Application::clear();
if (Application::vg)
nvgDeleteGL3(Application::vg);
glfwTerminate();
menu_animation_free();
if (Application::framerateCounter)
delete Application::framerateCounter;
delete Application::taskManager;
delete Application::notificationManager;
}
void Application::setDisplayFramerate(bool enabled)
{
if (!Application::framerateCounter && enabled)
{
Logger::info("Enabling framerate counter");
Application::framerateCounter = new FramerateCounter();
Application::resizeFramerateCounter();
}
else if (Application::framerateCounter && !enabled)
{
Logger::info("Disabling framerate counter");
delete Application::framerateCounter;
Application::framerateCounter = nullptr;
}
}
void Application::toggleFramerateDisplay()
{
Application::setDisplayFramerate(!Application::framerateCounter);
}
void Application::resizeFramerateCounter()
{
if (!Application::framerateCounter)
return;
Style* style = Application::getStyle();
unsigned framerateCounterWidth = style->FramerateCounter.width;
unsigned width = WINDOW_WIDTH;
Application::framerateCounter->setBoundaries(
width - framerateCounterWidth,
0,
framerateCounterWidth,
style->FramerateCounter.height);
Application::framerateCounter->invalidate();
}
void Application::resizeNotificationManager()
{
Application::notificationManager->setBoundaries(0, 0, Application::contentWidth, Application::contentHeight);
Application::notificationManager->invalidate();
}
void Application::notify(std::string text)
{
Application::notificationManager->notify(text);
}
NotificationManager* Application::getNotificationManager()
{
return Application::notificationManager;
}
void Application::giveFocus(View* view)
{
View* oldFocus = Application::currentFocus;
View* newFocus = view ? view->getDefaultFocus() : nullptr;
if (oldFocus != newFocus)
{
if (oldFocus)
oldFocus->onFocusLost();
Application::currentFocus = newFocus;
Application::globalFocusChangeEvent.fire(newFocus);
if (newFocus)
{
newFocus->onFocusGained();
Logger::debug("Giving focus to %s", newFocus->describe().c_str());
}
}
}
void Application::popView(ViewAnimation animation, std::function<void(void)> cb)
{
if (Application::viewStack.size() <= 1) // never pop the root view
return;
Application::blockInputs();
View* last = Application::viewStack[Application::viewStack.size() - 1];
last->willDisappear(true);
last->setForceTranslucent(true);
bool wait = animation == ViewAnimation::FADE; // wait for the new view animation to be done before showing the old one?
// Hide animation (and show previous view, if any)
last->hide([last, animation, wait, cb]() {
last->setForceTranslucent(false);
Application::viewStack.pop_back();
delete last;
// Animate the old view once the new one
// has ended its animation
if (Application::viewStack.size() > 0 && wait)
{
View* newLast = Application::viewStack[Application::viewStack.size() - 1];
if (newLast->isHidden())
{
newLast->willAppear(false);
newLast->show(cb, true, animation);
}
else
{
cb();
}
}
Application::unblockInputs();
},
true, animation);
// Animate the old view immediately
if (!wait && Application::viewStack.size() > 1)
{
View* toShow = Application::viewStack[Application::viewStack.size() - 2];
toShow->willAppear(false);
toShow->show(cb, true, animation);
}
// Focus
if (Application::focusStack.size() > 0)
{
View* newFocus = Application::focusStack[Application::focusStack.size() - 1];
Logger::debug("Giving focus to %s, and removing it from the focus stack", newFocus->describe().c_str());
Application::giveFocus(newFocus);
Application::focusStack.pop_back();
}
}
void Application::pushView(View* view, ViewAnimation animation)
{
Application::blockInputs();
// Call hide() on the previous view in the stack if no
// views are translucent, then call show() once the animation ends
View* last = nullptr;
if (Application::viewStack.size() > 0)
last = Application::viewStack[Application::viewStack.size() - 1];
bool fadeOut = last && !last->isTranslucent() && !view->isTranslucent(); // play the fade out animation?
bool wait = animation == ViewAnimation::FADE; // wait for the old view animation to be done before showing the new one?
view->registerAction("Exit", Key::PLUS, [] { Application::quit(); return true; });
view->registerAction(
"FPS", Key::MINUS, [] { Application::toggleFramerateDisplay(); return true; }, true);
// Fade out animation
if (fadeOut)
{
view->setForceTranslucent(true); // set the new view translucent until the fade out animation is done playing
// Animate the new view directly
if (!wait)
{
view->show([]() {
Application::unblockInputs();
},
true, animation);
}
last->hide([animation, wait]() {
View* newLast = Application::viewStack[Application::viewStack.size() - 1];
newLast->setForceTranslucent(false);
// Animate the new view once the old one
// has ended its animation
if (wait)
newLast->show([]() { Application::unblockInputs(); }, true, animation);
},
true, animation);
}
view->setBoundaries(0, 0, Application::contentWidth, Application::contentHeight);
if (!fadeOut)
view->show([]() { Application::unblockInputs(); }, true, animation);
else
view->alpha = 0.0f;
// Focus
if (Application::viewStack.size() > 0)
{
Logger::debug("Pushing %s to the focus stack", Application::currentFocus->describe().c_str());
Application::focusStack.push_back(Application::currentFocus);
}
// Layout and prepare view
view->invalidate(true);
view->willAppear(true);
Application::giveFocus(view->getDefaultFocus());
// And push it
Application::viewStack.push_back(view);
}
void Application::onWindowSizeChanged()
{
Logger::debug("Layout triggered");
for (View* view : Application::viewStack)
{
view->setBoundaries(0, 0, Application::contentWidth, Application::contentHeight);
view->invalidate();
view->onWindowSizeChanged();
}
Application::resizeNotificationManager();
Application::resizeFramerateCounter();
}
void Application::clear()
{
for (View* view : Application::viewStack)
{
view->willDisappear(true);
delete view;
}
Application::viewStack.clear();
}
Style* Application::getStyle()
{
return &Application::currentStyle;
}
void Application::setTheme(Theme theme)
{
Application::currentTheme = theme;
}
ThemeValues* Application::getThemeValues()
{
return &Application::currentTheme.colors[Application::currentThemeVariant];
}
ThemeValues* Application::getThemeValuesForVariant(ThemeVariant variant)
{
return &Application::currentTheme.colors[variant];
}
ThemeVariant Application::getThemeVariant()
{
return Application::currentThemeVariant;
}
int Application::loadFont(const char* fontName, const char* filePath)
{
return nvgCreateFont(Application::vg, fontName, filePath);
}
int Application::loadFontFromMemory(const char* fontName, void* address, size_t size, bool freeData)
{
return nvgCreateFontMem(Application::vg, fontName, (unsigned char*)address, size, freeData);
}
int Application::findFont(const char* fontName)
{
return nvgFindFont(Application::vg, fontName);
}
void Application::crash(std::string text)
{
CrashFrame* crashFrame = new CrashFrame(text);
Application::pushView(crashFrame);
}
void Application::blockInputs()
{
Application::blockInputsTokens += 1;
}
void Application::unblockInputs()
{
if (Application::blockInputsTokens > 0)
Application::blockInputsTokens -= 1;
}
NVGcontext* Application::getNVGContext()
{
return Application::vg;
}
TaskManager* Application::getTaskManager()
{
return Application::taskManager;
}
void Application::setCommonFooter(std::string footer)
{
Application::commonFooter = footer;
}
std::string* Application::getCommonFooter()
{
return &Application::commonFooter;
}
FramerateCounter::FramerateCounter()
: Label(LabelStyle::LIST_ITEM, "FPS: ---")
{
this->setColor(nvgRGB(255, 255, 255));
this->setVerticalAlign(NVG_ALIGN_MIDDLE);
this->setHorizontalAlign(NVG_ALIGN_RIGHT);
this->setBackground(Background::BACKDROP);
this->lastSecond = cpu_features_get_time_usec() / 1000;
}
void FramerateCounter::frame(FrameContext* ctx)
{
// Update counter
retro_time_t current = cpu_features_get_time_usec() / 1000;
if (current - this->lastSecond >= 1000)
{
char fps[10];
snprintf(fps, sizeof(fps), "FPS: %03d", this->frames);
this->setText(std::string(fps));
this->invalidate(); // update width for background
this->frames = 0;
this->lastSecond = current;
}
this->frames++;
// Regular frame
Label::frame(ctx);
}
void Application::setMaximumFPS(unsigned fps)
{
if (fps == 0)
Application::frameTime = 0.0f;
else
{
Application::frameTime = 1000 / (float)fps;
}
Logger::info("Maximum FPS set to %d - using a frame time of %.2f ms", fps, Application::frameTime);
}
std::string Application::getTitle()
{
return Application::title;
}
GenericEvent* Application::getGlobalFocusChangeEvent()
{
return &Application::globalFocusChangeEvent;
}
VoidEvent* Application::getGlobalHintsUpdateEvent()
{
return &Application::globalHintsUpdateEvent;
}
FontStash* Application::getFontStash()
{
return &Application::fontStash;
}
} // namespace brls

View File

@@ -0,0 +1,376 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019 natinusala
Copyright (C) 2019 WerWolv
Copyright (C) 2019 p-sam
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <math.h>
#include <stdio.h>
#include <borealis/animations.hpp>
#include <borealis/application.hpp>
#include <borealis/box_layout.hpp>
#include <borealis/logger.hpp>
#include <iterator>
namespace brls
{
BoxLayout::BoxLayout(BoxLayoutOrientation orientation, size_t defaultFocus)
: orientation(orientation)
, originalDefaultFocus(defaultFocus)
, defaultFocusedIndex(defaultFocus)
{
}
void BoxLayout::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, Style* style, FrameContext* ctx)
{
// Draw children
for (BoxLayoutChild* child : this->children)
child->view->frame(ctx);
}
void BoxLayout::setGravity(BoxLayoutGravity gravity)
{
this->gravity = gravity;
this->invalidate();
}
void BoxLayout::setSpacing(unsigned spacing)
{
this->spacing = spacing;
this->invalidate();
}
unsigned BoxLayout::getSpacing()
{
return this->spacing;
}
void BoxLayout::setMargins(unsigned top, unsigned right, unsigned bottom, unsigned left)
{
this->marginBottom = bottom;
this->marginLeft = left;
this->marginRight = right;
this->marginTop = top;
this->invalidate();
}
void BoxLayout::setMarginBottom(unsigned bottom)
{
this->marginBottom = bottom;
this->invalidate();
}
size_t BoxLayout::getViewsCount()
{
return this->children.size();
}
View* BoxLayout::getDefaultFocus()
{
// Focus default focus first
if (this->defaultFocusedIndex < this->children.size())
{
View* newFocus = this->children[this->defaultFocusedIndex]->view->getDefaultFocus();
if (newFocus)
return newFocus;
}
// Fallback to finding the first focusable view
for (size_t i = 0; i < this->children.size(); i++)
{
View* newFocus = this->children[i]->view->getDefaultFocus();
if (newFocus)
return newFocus;
}
return nullptr;
}
View* BoxLayout::getNextFocus(FocusDirection direction, void* parentUserData)
{
// Return nullptr immediately if focus direction mismatches the layout direction
if ((this->orientation == BoxLayoutOrientation::HORIZONTAL && direction != FocusDirection::LEFT && direction != FocusDirection::RIGHT) || (this->orientation == BoxLayoutOrientation::VERTICAL && direction != FocusDirection::UP && direction != FocusDirection::DOWN))
{
return nullptr;
}
// Traverse the children
size_t offset = 1; // which way are we going in the children list
if ((this->orientation == BoxLayoutOrientation::HORIZONTAL && direction == FocusDirection::LEFT) || (this->orientation == BoxLayoutOrientation::VERTICAL && direction == FocusDirection::UP))
{
offset = -1;
}
size_t currentFocusIndex = *((size_t*)parentUserData) + offset;
View* currentFocus = nullptr;
while (!currentFocus && currentFocusIndex >= 0 && currentFocusIndex < this->children.size())
{
currentFocus = this->children[currentFocusIndex]->view->getDefaultFocus();
currentFocusIndex += offset;
}
return currentFocus;
}
void BoxLayout::removeView(int index, bool free)
{
BoxLayoutChild* toRemove = this->children[index];
toRemove->view->willDisappear(true);
if (free)
delete toRemove->view;
delete toRemove;
this->children.erase(this->children.begin() + index);
}
void BoxLayout::clear(bool free)
{
while (!this->children.empty())
this->removeView(0, free);
}
void BoxLayout::layout(NVGcontext* vg, Style* style, FontStash* stash)
{
// Vertical orientation
if (this->orientation == BoxLayoutOrientation::VERTICAL)
{
unsigned entriesHeight = 0;
int yAdvance = this->y + this->marginTop;
for (size_t i = 0; i < this->children.size(); i++)
{
BoxLayoutChild* child = this->children[i];
unsigned childHeight = child->view->getHeight();
if (child->fill)
child->view->setBoundaries(this->x + this->marginLeft,
yAdvance,
this->width - this->marginLeft - this->marginRight,
this->y + this->height - yAdvance - this->marginBottom);
else
child->view->setBoundaries(this->x + this->marginLeft,
yAdvance,
this->width - this->marginLeft - this->marginRight,
child->view->getHeight(false));
child->view->invalidate(true); // call layout directly in case height is updated
childHeight = child->view->getHeight();
int spacing = (int)this->spacing;
View* next = (this->children.size() > 1 && i <= this->children.size() - 2) ? this->children[i + 1]->view : nullptr;
this->customSpacing(child->view, next, &spacing);
if (child->view->isCollapsed())
spacing = 0;
if (!child->view->isHidden())
entriesHeight += spacing + childHeight;
yAdvance += spacing + childHeight;
}
// TODO: apply gravity
// Update height if needed
if (this->resize)
this->setHeight(entriesHeight - spacing + this->marginTop + this->marginBottom);
}
// Horizontal orientation
else if (this->orientation == BoxLayoutOrientation::HORIZONTAL)
{
// Layout
int xAdvance = this->x + this->marginLeft;
for (size_t i = 0; i < this->children.size(); i++)
{
BoxLayoutChild* child = this->children[i];
unsigned childWidth = child->view->getWidth();
if (child->fill)
child->view->setBoundaries(xAdvance,
this->y + this->marginTop,
this->x + this->width - xAdvance - this->marginRight,
this->height - this->marginTop - this->marginBottom);
else
child->view->setBoundaries(xAdvance,
this->y + this->marginTop,
childWidth,
this->height - this->marginTop - this->marginBottom);
child->view->invalidate(true); // call layout directly in case width is updated
childWidth = child->view->getWidth();
int spacing = (int)this->spacing;
View* next = (this->children.size() > 1 && i <= this->children.size() - 2) ? this->children[i + 1]->view : nullptr;
this->customSpacing(child->view, next, &spacing);
if (child->view->isCollapsed())
spacing = 0;
xAdvance += spacing + childWidth;
}
// Apply gravity
// TODO: more efficient gravity implementation?
if (!this->children.empty())
{
switch (this->gravity)
{
case BoxLayoutGravity::RIGHT:
{
// Take the remaining empty space between the last view's
// right boundary and ours and push all views by this amount
View* lastView = this->children[this->children.size() - 1]->view;
unsigned lastViewRight = lastView->getX() + lastView->getWidth();
unsigned ourRight = this->getX() + this->getWidth();
if (lastViewRight <= ourRight)
{
unsigned difference = ourRight - lastViewRight;
for (BoxLayoutChild* child : this->children)
{
View* view = child->view;
view->setBoundaries(
view->getX() + difference,
view->getY(),
view->getWidth(),
view->getHeight());
view->invalidate();
}
}
break;
}
default:
break;
}
}
// TODO: update width if needed (introduce entriesWidth)
}
}
void BoxLayout::setResize(bool resize)
{
this->resize = resize;
this->invalidate();
}
void BoxLayout::addView(View* view, bool fill, bool resetState)
{
BoxLayoutChild* child = new BoxLayoutChild();
child->view = view;
child->fill = fill;
this->children.push_back(child);
size_t position = this->children.size() - 1;
size_t* userdata = (size_t*)malloc(sizeof(size_t));
*userdata = position;
view->setParent(this, userdata);
view->willAppear(resetState);
this->invalidate();
}
View* BoxLayout::getChild(size_t index)
{
return this->children[index]->view;
}
bool BoxLayout::isEmpty()
{
return this->children.size() == 0;
}
bool BoxLayout::isChildFocused()
{
return this->childFocused;
}
void BoxLayout::onChildFocusGained(View* child)
{
this->childFocused = true;
// Remember focus if needed
if (this->rememberFocus)
{
size_t index = *((size_t*)child->getParentUserData());
this->defaultFocusedIndex = index;
}
View::onChildFocusGained(child);
}
void BoxLayout::onChildFocusLost(View* child)
{
this->childFocused = false;
View::onChildFocusLost(child);
}
BoxLayout::~BoxLayout()
{
for (BoxLayoutChild* child : this->children)
{
child->view->willDisappear(true);
delete child->view;
delete child;
}
this->children.clear();
}
void BoxLayout::willAppear(bool resetState)
{
for (BoxLayoutChild* child : this->children)
child->view->willAppear(resetState);
}
void BoxLayout::willDisappear(bool resetState)
{
for (BoxLayoutChild* child : this->children)
child->view->willDisappear(resetState);
// Reset default focus to original one if needed
if (this->rememberFocus)
this->defaultFocusedIndex = this->originalDefaultFocus;
}
void BoxLayout::onWindowSizeChanged()
{
for (BoxLayoutChild* child : this->children)
child->view->onWindowSizeChanged();
}
void BoxLayout::setRememberFocus(bool remember)
{
this->rememberFocus = remember;
}
} // namespace brls

View File

@@ -0,0 +1,213 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019-2020 natinusala
Copyright (C) 2019 p-sam
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <math.h>
#include <borealis/application.hpp>
#include <borealis/button.hpp>
namespace brls
{
Button::Button(ButtonStyle style)
: style(style)
{
this->registerAction("OK", Key::A, [this] { return this->onClick(); });
}
LabelStyle Button::getLabelStyle()
{
if (this->style == ButtonStyle::BORDERLESS)
return LabelStyle::BUTTON_BORDERLESS;
else if (this->style == ButtonStyle::DIALOG)
return LabelStyle::BUTTON_DIALOG;
else if (this->style == ButtonStyle::CRASH)
return LabelStyle::CRASH;
if (this->state == ButtonState::DISABLED)
return LabelStyle::BUTTON_PLAIN_DISABLED;
else
return LabelStyle::BUTTON_PLAIN;
}
Button::~Button()
{
if (this->label != nullptr)
delete this->label;
if (this->image != nullptr)
delete this->image;
}
void Button::layout(NVGcontext* vg, Style* style, FontStash* stash)
{
unsigned imageWidth = this->label ? this->getHeight() : this->getWidth();
unsigned imageHeight = this->getHeight();
if (!this->image)
imageWidth = 0;
if (this->label != nullptr)
{
this->label->setWidth(this->getWidth() - imageWidth);
this->label->invalidate(true);
this->label->setBoundaries(
this->x + imageWidth,
this->y + this->getHeight() / 2 - this->label->getHeight() / 2,
this->label->getWidth(),
this->label->getHeight());
this->label->invalidate();
}
if (this->image != nullptr)
{
this->image->setHeight(imageHeight);
this->image->setWidth(imageWidth);
this->image->invalidate(true);
this->image->setBoundaries(
this->x,
this->y + this->getHeight() / 2 - this->image->getHeight() / 2,
this->image->getWidth(),
this->image->getHeight());
}
}
Button* Button::setLabel(std::string label)
{
if (this->label != nullptr)
delete this->label;
this->label = new Label(this->getLabelStyle(), label, true);
this->label->setHorizontalAlign(NVG_ALIGN_CENTER);
this->label->setParent(this);
return this;
}
Button* Button::setImage(std::string path)
{
this->image = new Image(path);
this->image->setParent(this);
return this;
}
Button* Button::setImage(unsigned char* buffer, size_t bufferSize)
{
this->image = new Image(buffer, bufferSize);
this->image->setParent(this);
return this;
}
void Button::setState(ButtonState state)
{
this->state = state;
if (this->label != nullptr)
this->label->setStyle(this->getLabelStyle());
}
ButtonState Button::getState()
{
return this->state;
}
void Button::getHighlightInsets(unsigned* top, unsigned* right, unsigned* bottom, unsigned* left)
{
if (this->style == ButtonStyle::DIALOG)
{
View::getHighlightInsets(top, right, bottom, left);
*right -= 1;
return;
}
Style* style = Application::getStyle();
*top = style->Button.highlightInset;
*right = style->Button.highlightInset;
*bottom = style->Button.highlightInset;
*left = style->Button.highlightInset;
}
void Button::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, Style* style, FrameContext* ctx)
{
float cornerRadius = this->cornerRadiusOverride ? this->cornerRadiusOverride : (float)style->Button.cornerRadius;
// Background
switch (this->style)
{
case ButtonStyle::PLAIN:
{
nvgFillColor(vg, a(this->state == ButtonState::DISABLED ? ctx->theme->buttonPlainDisabledBackgroundColor : ctx->theme->buttonPlainEnabledBackgroundColor));
nvgBeginPath(vg);
nvgRoundedRect(vg, x, y, width, height, cornerRadius);
nvgFill(vg);
break;
}
default:
break;
}
// Shadow
if (this->state == ButtonState::ENABLED && this->style == ButtonStyle::PLAIN)
{
float shadowWidth = style->Button.shadowWidth;
float shadowFeather = style->Button.shadowFeather;
float shadowOpacity = style->Button.shadowOpacity;
float shadowOffset = style->Button.shadowOffset;
NVGpaint shadowPaint = nvgBoxGradient(vg,
x, y + shadowWidth,
width, height,
cornerRadius * 2, shadowFeather,
RGBA(0, 0, 0, shadowOpacity * alpha), transparent);
nvgBeginPath(vg);
nvgRect(vg, x - shadowOffset, y - shadowOffset,
width + shadowOffset * 2, height + shadowOffset * 3);
nvgRoundedRect(vg, x, y, width, height, cornerRadius);
nvgPathWinding(vg, NVG_HOLE);
nvgFillPaint(vg, shadowPaint);
nvgFill(vg);
}
// Label
if (this->label != nullptr)
this->label->frame(ctx);
if (this->image != nullptr)
this->image->frame(ctx);
}
bool Button::onClick()
{
if (this->state == ButtonState::DISABLED)
return false;
return this->clickEvent.fire(this);
}
GenericEvent* Button::getClickEvent()
{
return &this->clickEvent;
}
void Button::setCornerRadius(float cornerRadius)
{
this->cornerRadiusOverride = cornerRadius;
if (this->image != nullptr)
this->image->setCornerRadius(cornerRadius);
}
} // namespace brls

View File

@@ -0,0 +1,151 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019-2020 natinusala
Copyright (C) 2019 p-sam
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <math.h>
#include <borealis/animations.hpp>
#include <borealis/application.hpp>
#include <borealis/crash_frame.hpp>
namespace brls
{
CrashFrame::CrashFrame(std::string text)
{
// Label
this->label = new Label(LabelStyle::CRASH, text, true);
this->label->setHorizontalAlign(NVG_ALIGN_CENTER);
this->label->setParent(this);
// Button
this->button = (new Button(ButtonStyle::CRASH))->setLabel("OK");
this->button->setParent(this);
this->button->alpha = 0.0f;
this->button->getClickEvent()->subscribe([](View* view) { Application::quit(); });
this->button->overrideThemeVariant(Application::getThemeValuesForVariant(ThemeVariant_DARK));
// Hint
this->hint = new Hint();
this->hint->setParent(this);
}
void CrashFrame::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, Style* style, FrameContext* ctx)
{
nvgSave(vg);
// Background
nvgFillColor(vg, RGB(0, 0, 0));
nvgBeginPath(vg);
nvgRect(vg, x, y, width, height);
nvgFill(vg);
// Scale
float scale = (this->alpha + 2.0f) / 3.0f;
nvgTranslate(vg, (1.0f - scale) * width * 0.5f, (1.0f - scale) * height * 0.5f);
nvgScale(vg, scale, scale);
// Label
this->label->frame(ctx);
// [!] box
unsigned boxSize = style->CrashFrame.boxSize;
nvgStrokeColor(vg, RGB(255, 255, 255));
nvgStrokeWidth(vg, style->CrashFrame.boxStrokeWidth);
nvgBeginPath(vg);
nvgRect(vg, x + width / 2 - boxSize / 2, y + style->CrashFrame.boxSpacing, boxSize, boxSize);
nvgStroke(vg);
nvgFillColor(vg, RGB(255, 255, 255));
nvgFontSize(vg, (float)style->CrashFrame.boxSize / 1.25f);
nvgTextAlign(vg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
nvgBeginPath(vg);
nvgText(vg, x + width / 2, y + style->CrashFrame.boxSpacing + boxSize / 2, "!", nullptr);
nvgFill(vg);
// End scale
nvgResetTransform(vg);
nvgRestore(vg);
// Footer
nvgBeginPath(vg);
nvgRect(vg, x + style->AppletFrame.separatorSpacing, y + height - style->AppletFrame.footerHeight, width - style->AppletFrame.separatorSpacing * 2, 1);
nvgFill(vg);
nvgFontSize(vg, style->AppletFrame.footerTextSize);
nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
nvgBeginPath(vg);
nvgText(vg, x + style->AppletFrame.separatorSpacing + style->AppletFrame.footerTextSpacing, y + height - style->AppletFrame.footerHeight / 2, Application::getTitle().c_str(), nullptr);
// Button
this->button->frame(ctx);
// Hint
this->hint->frame(ctx);
}
void CrashFrame::onShowAnimationEnd()
{
this->button->show([]() {});
}
View* CrashFrame::getDefaultFocus()
{
return this->button->getDefaultFocus();
}
void CrashFrame::layout(NVGcontext* vg, Style* style, FontStash* stash)
{
// Label
this->label->setWidth(roundf((float)this->width * style->CrashFrame.labelWidth));
this->label->invalidate(true);
this->label->setBoundaries(
this->x + this->width / 2 - this->label->getWidth() / 2,
this->y + (this->height - style->AppletFrame.footerHeight) / 2,
this->label->getWidth(),
this->label->getHeight());
// Button
this->button->setBoundaries(
this->x + this->width / 2 - style->CrashFrame.buttonWidth / 2,
this->y + this->height - style->AppletFrame.footerHeight - style->CrashFrame.boxSpacing - style->CrashFrame.buttonHeight,
style->CrashFrame.buttonWidth,
style->CrashFrame.buttonHeight);
this->button->invalidate();
// Hint
// TODO: convert the bottom-left footer into a Label to get its width and avoid clipping with the hint
unsigned hintWidth = this->width - style->AppletFrame.separatorSpacing * 2 - style->AppletFrame.footerTextSpacing * 2;
this->hint->setBoundaries(
this->x + this->width - hintWidth - style->AppletFrame.separatorSpacing - style->AppletFrame.footerTextSpacing,
this->y + this->height - style->AppletFrame.footerHeight,
hintWidth,
style->AppletFrame.footerHeight);
this->hint->invalidate();
}
CrashFrame::~CrashFrame()
{
delete this->label;
delete this->hint;
}
} // namespace brls

View File

@@ -0,0 +1,342 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019-2020 natinusala
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <borealis/application.hpp>
#include <borealis/button.hpp>
#include <borealis/dialog.hpp>
// TODO: different open animation?
namespace brls
{
Dialog::Dialog(View* contentView)
: contentView(contentView)
{
if (contentView)
contentView->setParent(this);
this->registerAction("Back", Key::B, [this] { return this->onCancel(); });
}
Dialog::Dialog(std::string text)
: Dialog(new Label(LabelStyle::DIALOG, text, true))
{
}
void Dialog::addButton(std::string label, GenericEvent::Callback cb)
{
if (this->buttons.size() >= 3)
return;
DialogButton* button = new DialogButton();
button->label = label;
button->cb = cb;
this->buttons.push_back(button);
this->rebuildButtons();
this->invalidate();
}
void Dialog::open()
{
Application::pushView(this);
if (this->buttons.size() == 0)
Application::blockInputs();
}
// TODO: do something better in case another view was pushed in the meantime
void Dialog::close(std::function<void(void)> cb)
{
Application::popView(ViewAnimation::FADE, cb);
if (this->buttons.size() == 0)
Application::unblockInputs();
}
void Dialog::setCancelable(bool cancelable)
{
this->cancelable = cancelable;
}
void Dialog::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, Style* style, FrameContext* ctx)
{
// Backdrop
nvgFillColor(vg, a(ctx->theme->dialogBackdrop));
nvgBeginPath(vg);
nvgRect(vg, x, y, width, height);
nvgFill(vg);
// Shadow
float shadowWidth = style->Dialog.shadowWidth;
float shadowFeather = style->Dialog.shadowFeather;
float shadowOpacity = style->Dialog.shadowOpacity;
float shadowOffset = style->Dialog.shadowOffset;
NVGpaint shadowPaint = nvgBoxGradient(vg,
this->frameX, this->frameY + shadowWidth,
this->frameWidth, this->frameHeight,
style->Dialog.cornerRadius * 2, shadowFeather,
RGBA(0, 0, 0, shadowOpacity * alpha), transparent);
nvgBeginPath(vg);
nvgRect(vg, this->frameX - shadowOffset, this->frameY - shadowOffset,
this->frameWidth + shadowOffset * 2, this->frameHeight + shadowOffset * 3);
nvgRoundedRect(vg, this->frameX, this->frameY, this->frameWidth, this->frameHeight, style->Dialog.cornerRadius);
nvgPathWinding(vg, NVG_HOLE);
nvgFillPaint(vg, shadowPaint);
nvgFill(vg);
// Frame
nvgFillColor(vg, a(ctx->theme->dialogColor));
nvgBeginPath(vg);
nvgRoundedRect(vg, this->frameX, this->frameY, this->frameWidth, this->frameHeight, style->Dialog.cornerRadius);
nvgFill(vg);
// Content view
if (this->contentView)
this->contentView->frame(ctx);
// Buttons separator
if (this->buttons.size() > 0)
{
unsigned buttonsHeight = this->getButtonsHeight();
nvgFillColor(vg, a(ctx->theme->dialogButtonSeparatorColor));
// First vertical separator
nvgBeginPath(vg);
nvgRect(vg, this->frameX, this->frameY + this->frameHeight - buttonsHeight, this->frameWidth, style->Dialog.buttonSeparatorHeight);
nvgFill(vg);
// Second vertical separator
if (this->buttons.size() == 3)
{
nvgBeginPath(vg);
nvgRect(vg, this->frameX, this->frameY + this->frameHeight - style->Dialog.buttonHeight, this->frameWidth, style->Dialog.buttonSeparatorHeight);
nvgFill(vg);
}
// Horizontal separator
if (this->buttons.size() >= 2)
{
nvgBeginPath(vg);
nvgRect(
vg,
this->frameX + this->frameWidth / 2 + style->Dialog.buttonSeparatorHeight / 2,
this->frameY + this->frameHeight - style->Dialog.buttonHeight + 1, // offset by 1 to fix aliasing artifact
style->Dialog.buttonSeparatorHeight,
style->Dialog.buttonHeight - 1);
nvgFill(vg);
}
}
// Buttons
if (this->verticalButtonsLayout)
this->verticalButtonsLayout->frame(ctx);
}
View* Dialog::getDefaultFocus()
{
if (this->buttons.size() > 0 && this->verticalButtonsLayout)
return this->verticalButtonsLayout->getDefaultFocus();
return nullptr;
}
bool Dialog::onCancel()
{
if (this->cancelable)
this->close();
return this->cancelable;
}
unsigned Dialog::getButtonsHeight()
{
Style* style = Application::getStyle();
if (this->buttons.size() == 3)
return style->Dialog.buttonHeight * 2;
else if (this->buttons.size() > 0) // 1 or 2
return style->Dialog.buttonHeight;
else
return 0;
}
void Dialog::layout(NVGcontext* vg, Style* style, FontStash* stash)
{
this->frameWidth = style->Dialog.width;
this->frameHeight = style->Dialog.height;
unsigned buttonsHeight = this->getButtonsHeight();
this->frameHeight += buttonsHeight;
this->frameX = getWidth() / 2 - this->frameWidth / 2;
this->frameY = getHeight() / 2 - this->frameHeight / 2;
unsigned contentX = this->frameX + style->Dialog.paddingLeftRight;
unsigned contentY = this->frameY + style->Dialog.paddingTopBottom;
unsigned contentWidth = this->frameWidth - style->Dialog.paddingLeftRight * 2;
unsigned contentHeight = this->frameHeight - style->Dialog.paddingTopBottom * 2 - buttonsHeight;
if (this->contentView)
{
// First layout to get height
this->contentView->setBoundaries(
contentX,
contentY,
contentWidth,
contentHeight);
this->contentView->invalidate(true); // layout directly to get height
// Center the content view in the dialog
// or resize it if needed
unsigned newContentHeight = this->contentView->getHeight();
int difference = contentHeight - newContentHeight;
if (difference < 0)
{
this->frameHeight += -difference;
}
else
{
contentY += difference / 2;
this->contentView->setBoundaries(
contentX,
contentY,
contentWidth,
contentHeight);
this->contentView->invalidate();
}
}
// Buttons
if (this->verticalButtonsLayout)
{
this->verticalButtonsLayout->setBoundaries(
this->frameX,
this->frameY + this->frameHeight - buttonsHeight,
this->frameWidth,
style->Dialog.buttonHeight);
// Only one big button
if (this->buttons.size() == 1)
{
this->verticalButtonsLayout->getChild(0)->setHeight(style->Dialog.buttonHeight);
}
// Two buttons on one row
else if (this->buttons.size() == 2)
{
this->horizontalButtonsLayout->setHeight(style->Dialog.buttonHeight);
this->horizontalButtonsLayout->getChild(0)->setWidth(this->frameWidth / 2);
this->horizontalButtonsLayout->getChild(1)->setWidth(this->frameWidth / 2);
}
// Two rows: one with one button and one with two
else if (this->buttons.size() == 3)
{
this->verticalButtonsLayout->getChild(0)->setHeight(style->Dialog.buttonHeight);
this->horizontalButtonsLayout->setHeight(style->Dialog.buttonHeight);
this->horizontalButtonsLayout->getChild(0)->setWidth(this->frameWidth / 2);
this->horizontalButtonsLayout->getChild(1)->setWidth(this->frameWidth / 2);
}
this->verticalButtonsLayout->invalidate();
if (this->horizontalButtonsLayout)
this->horizontalButtonsLayout->invalidate();
}
}
void Dialog::rebuildButtons()
{
if (this->verticalButtonsLayout)
delete this->verticalButtonsLayout;
this->verticalButtonsLayout = nullptr;
// horizontal box layout will be deleted by
// the vertical layout destructor
if (this->buttons.size() > 0)
{
this->verticalButtonsLayout = new BoxLayout(BoxLayoutOrientation::VERTICAL);
this->verticalButtonsLayout->setParent(this);
// Only one big button
if (this->buttons.size() == 1)
{
Button* button = (new Button(ButtonStyle::DIALOG))->setLabel(this->buttons[0]->label);
button->getClickEvent()->subscribe(this->buttons[0]->cb);
this->verticalButtonsLayout->addView(button);
}
// Two buttons on one row
else if (this->buttons.size() == 2)
{
this->horizontalButtonsLayout = new BoxLayout(BoxLayoutOrientation::HORIZONTAL);
this->verticalButtonsLayout->addView(this->horizontalButtonsLayout);
for (DialogButton* dialogButton : this->buttons)
{
Button* button = (new Button(ButtonStyle::DIALOG))->setLabel(dialogButton->label);
button->getClickEvent()->subscribe(dialogButton->cb);
this->horizontalButtonsLayout->addView(button);
}
}
// Two rows: one with one button and one with two
else if (this->buttons.size() == 3)
{
Button* button = (new Button(ButtonStyle::DIALOG))->setLabel(this->buttons[0]->label);
button->getClickEvent()->subscribe(this->buttons[0]->cb);
this->verticalButtonsLayout->addView(button);
this->horizontalButtonsLayout = new BoxLayout(BoxLayoutOrientation::HORIZONTAL);
this->verticalButtonsLayout->addView(this->horizontalButtonsLayout);
for (size_t i = 1; i < this->buttons.size(); i++)
{
DialogButton* dialogButton = this->buttons[i];
Button* button = (new Button(ButtonStyle::DIALOG))->setLabel(dialogButton->label);
button->getClickEvent()->subscribe(dialogButton->cb);
this->horizontalButtonsLayout->addView(button);
}
}
}
}
Dialog::~Dialog()
{
if (this->contentView)
delete this->contentView;
if (this->verticalButtonsLayout)
delete this->verticalButtonsLayout;
for (DialogButton* dialogButton : this->buttons)
delete dialogButton;
// horizontal box layout will be deleted by
// the vertical layout destructor
}
} // namespace brls

View File

@@ -0,0 +1,214 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019-2020 natinusala
Copyright (C) 2019 p-sam
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <borealis/animations.hpp>
#include <borealis/application.hpp>
#include <borealis/dropdown.hpp>
#include <borealis/logger.hpp>
#define SELECT_VIEW_MAX_ITEMS 6 // for max height
#define min(a, b) ((a < b) ? a : b)
// TODO: Turns out the fade out animation is the same as the fade in (top -> bottom)
namespace brls
{
Dropdown::Dropdown(std::string title, std::vector<std::string> values, ValueSelectedEvent::Callback cb, size_t selected)
: title(title)
{
Style* style = Application::getStyle();
this->valueEvent.subscribe(cb);
this->topOffset = (float)style->Dropdown.listPadding / 8.0f;
this->valuesCount = values.size();
this->list = new List(selected);
this->list->setParent(this);
this->list->setMargins(1, 0, 1, 0);
for (size_t i = 0; i < values.size(); i++)
{
std::string value = values[i];
ListItem* item = new ListItem(value);
if (i == selected)
item->setChecked(true);
item->setHeight(style->Dropdown.listItemHeight);
item->setTextSize(style->Dropdown.listItemTextSize);
item->getClickEvent()->subscribe([this, i](View* view) {
this->valueEvent.fire(i);
Application::popView();
});
this->list->addView(item);
}
this->hint = new Hint();
this->hint->setParent(this);
this->registerAction("Back", Key::B, [this] { return this->onCancel(); });
}
void Dropdown::show(std::function<void(void)> cb, bool animate, ViewAnimation animation)
{
View::show(cb);
menu_animation_ctx_entry_t entry;
entry.duration = this->getShowAnimationDuration(animation);
entry.easing_enum = EASING_OUT_QUAD;
entry.subject = &this->topOffset;
entry.tag = (uintptr_t) nullptr;
entry.target_value = 0.0f;
entry.tick = [this](void* userdata) { this->invalidate(); };
entry.userdata = nullptr;
menu_animation_push(&entry);
}
void Dropdown::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, Style* style, FrameContext* ctx)
{
unsigned top = this->list->getY() - style->Dropdown.headerHeight - style->Dropdown.listPadding;
// Backdrop
nvgFillColor(vg, a(ctx->theme->dropdownBackgroundColor));
nvgBeginPath(vg);
nvgRect(vg, x, y, width, top);
nvgFill(vg);
// TODO: Shadow
// Background
nvgFillColor(vg, a(ctx->theme->sidebarColor));
nvgBeginPath(vg);
nvgRect(vg, x, top, width, height - top);
nvgFill(vg);
// List
this->list->frame(ctx);
// Footer
this->hint->frame(ctx);
nvgFillColor(vg, ctx->theme->separatorColor); // we purposely don't apply opacity
nvgBeginPath(vg);
nvgRect(vg, x + style->AppletFrame.separatorSpacing, y + height - style->AppletFrame.footerHeight, width - style->AppletFrame.separatorSpacing * 2, 1);
nvgFill(vg);
nvgFillColor(vg, ctx->theme->textColor); // we purposely don't apply opacity
nvgFontSize(vg, style->AppletFrame.footerTextSize);
nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
nvgBeginPath(vg);
nvgText(vg, x + style->AppletFrame.separatorSpacing + style->AppletFrame.footerTextSpacing, y + height - style->AppletFrame.footerHeight / 2, Application::getCommonFooter()->c_str(), nullptr);
// Header
nvgFillColor(vg, a(ctx->theme->separatorColor));
nvgBeginPath(vg);
nvgRect(vg, x + style->AppletFrame.separatorSpacing, top + style->Dropdown.headerHeight - 1, width - style->AppletFrame.separatorSpacing * 2, 1);
nvgFill(vg);
nvgBeginPath(vg);
nvgFillColor(vg, a(ctx->theme->textColor));
nvgFontFaceId(vg, ctx->fontStash->regular);
nvgFontSize(vg, style->Dropdown.headerFontSize);
nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
nvgText(vg, x + style->Dropdown.headerPadding, top + style->Dropdown.headerHeight / 2, this->title.c_str(), nullptr);
}
bool Dropdown::onCancel()
{
this->valueEvent.fire(-1);
Application::popView();
return true;
}
unsigned Dropdown::getShowAnimationDuration(ViewAnimation animation)
{
return View::getShowAnimationDuration(animation) / 2;
}
void Dropdown::layout(NVGcontext* vg, Style* style, FontStash* stash)
{
// Layout and move the list
unsigned listHeight = min(SELECT_VIEW_MAX_ITEMS, this->valuesCount) * style->Dropdown.listItemHeight - (unsigned)this->topOffset;
unsigned listWidth = style->Dropdown.listWidth + style->List.marginLeftRight * 2;
this->list->setBoundaries(
this->width / 2 - listWidth / 2,
this->height - style->AppletFrame.footerHeight - listHeight - style->Dropdown.listPadding + (unsigned)this->topOffset,
listWidth,
listHeight);
this->list->invalidate(true); // call layout directly to update scrolling
// Hint
// TODO: convert the bottom-left footer into a Label to get its width and avoid clipping with the hint
unsigned hintWidth = this->width - style->AppletFrame.separatorSpacing * 2 - style->AppletFrame.footerTextSpacing * 2;
this->hint->setBoundaries(
this->x + this->width - hintWidth - style->AppletFrame.separatorSpacing - style->AppletFrame.footerTextSpacing,
this->y + this->height - style->AppletFrame.footerHeight,
hintWidth,
style->AppletFrame.footerHeight);
this->hint->invalidate();
}
View* Dropdown::getDefaultFocus()
{
return this->list->getDefaultFocus();
}
void Dropdown::open(std::string title, std::vector<std::string> values, ValueSelectedEvent::Callback cb, int selected)
{
Dropdown* dropdown = new Dropdown(title, values, cb, selected);
Application::pushView(dropdown);
}
void Dropdown::willAppear(bool resetState)
{
if (this->list)
this->list->willAppear(resetState);
if (this->hint)
this->hint->willAppear(resetState);
}
void Dropdown::willDisappear(bool resetState)
{
if (this->list)
this->list->willDisappear(resetState);
if (this->hint)
this->hint->willDisappear(resetState);
}
Dropdown::~Dropdown()
{
delete this->list;
delete this->hint;
}
} // namespace brls

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
*.dll
*.exe
*.c~
*.un~
*.o

View File

@@ -0,0 +1,69 @@
/* Copyright (C) 2010-2018 The RetroArch team
*
* ---------------------------------------------------------------------------------------
* The following license statement only applies to this file (compat_strl.c).
* ---------------------------------------------------------------------------------------
*
* Permission is hereby granted, free of charge,
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include <stdlib.h>
#include <ctype.h>
#include <compat/strl.h>
/* Implementation of strlcpy()/strlcat() based on OpenBSD. */
#ifndef __MACH__
size_t strlcpy(char *dest, const char *source, size_t size)
{
size_t src_size = 0;
size_t n = size;
if (n)
while (--n && (*dest++ = *source++)) src_size++;
if (!n)
{
if (size) *dest = '\0';
while (*source++) src_size++;
}
return src_size;
}
size_t strlcat(char *dest, const char *source, size_t size)
{
size_t len = strlen(dest);
dest += len;
if (len > size)
size = 0;
else
size -= len;
return len + strlcpy(dest, source, size);
}
#endif
char *strldup(const char *s, size_t n)
{
char *dst = (char*)malloc(sizeof(char) * (n + 1));
strlcpy(dst, s, n);
return dst;
}

View File

@@ -0,0 +1,510 @@
/* Copyright (C) 2010-2018 The RetroArch team
*
* ---------------------------------------------------------------------------------------
* The following license statement only applies to this file (encoding_utf.c).
* ---------------------------------------------------------------------------------------
*
* Permission is hereby granted, free of charge,
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include <stdint.h>
#include <stdlib.h>
#include <stddef.h>
#include <string.h>
#include <boolean.h>
#include <compat/strl.h>
#include <retro_inline.h>
#include <encodings/utf.h>
#if defined(_WIN32) && !defined(_XBOX)
#include <windows.h>
#elif defined(_XBOX)
#include <xtl.h>
#endif
static unsigned leading_ones(uint8_t c)
{
unsigned ones = 0;
while (c & 0x80)
{
ones++;
c <<= 1;
}
return ones;
}
/* Simple implementation. Assumes the sequence is
* properly synchronized and terminated. */
size_t utf8_conv_utf32(uint32_t *out, size_t out_chars,
const char *in, size_t in_size)
{
unsigned i;
size_t ret = 0;
while (in_size && out_chars)
{
unsigned extra, shift;
uint32_t c;
uint8_t first = *in++;
unsigned ones = leading_ones(first);
if (ones > 6 || ones == 1) /* Invalid or desync. */
break;
extra = ones ? ones - 1 : ones;
if (1 + extra > in_size) /* Overflow. */
break;
shift = (extra - 1) * 6;
c = (first & ((1 << (7 - ones)) - 1)) << (6 * extra);
for (i = 0; i < extra; i++, in++, shift -= 6)
c |= (*in & 0x3f) << shift;
*out++ = c;
in_size -= 1 + extra;
out_chars--;
ret++;
}
return ret;
}
bool utf16_conv_utf8(uint8_t *out, size_t *out_chars,
const uint16_t *in, size_t in_size)
{
static uint8_t kUtf8Limits[5] = { 0xC0, 0xE0, 0xF0, 0xF8, 0xFC };
size_t out_pos = 0;
size_t in_pos = 0;
for (;;)
{
unsigned numAdds;
uint32_t value;
if (in_pos == in_size)
{
*out_chars = out_pos;
return true;
}
value = in[in_pos++];
if (value < 0x80)
{
if (out)
out[out_pos] = (char)value;
out_pos++;
continue;
}
if (value >= 0xD800 && value < 0xE000)
{
uint32_t c2;
if (value >= 0xDC00 || in_pos == in_size)
break;
c2 = in[in_pos++];
if (c2 < 0xDC00 || c2 >= 0xE000)
break;
value = (((value - 0xD800) << 10) | (c2 - 0xDC00)) + 0x10000;
}
for (numAdds = 1; numAdds < 5; numAdds++)
if (value < (((uint32_t)1) << (numAdds * 5 + 6)))
break;
if (out)
out[out_pos] = (char)(kUtf8Limits[numAdds - 1]
+ (value >> (6 * numAdds)));
out_pos++;
do
{
numAdds--;
if (out)
out[out_pos] = (char)(0x80
+ ((value >> (6 * numAdds)) & 0x3F));
out_pos++;
}while (numAdds != 0);
}
*out_chars = out_pos;
return false;
}
/* Acts mostly like strlcpy.
*
* Copies the given number of UTF-8 characters,
* but at most d_len bytes.
*
* Always NULL terminates.
* Does not copy half a character.
*
* Returns number of bytes. 's' is assumed valid UTF-8.
* Use only if 'chars' is considerably less than 'd_len'. */
size_t utf8cpy(char *d, size_t d_len, const char *s, size_t chars)
{
const uint8_t *sb = (const uint8_t*)s;
const uint8_t *sb_org = sb;
if (!s)
return 0;
while (*sb && chars-- > 0)
{
sb++;
while ((*sb & 0xC0) == 0x80) sb++;
}
if ((size_t)(sb - sb_org) > d_len-1 /* NUL */)
{
sb = sb_org + d_len-1;
while ((*sb & 0xC0) == 0x80) sb--;
}
memcpy(d, sb_org, sb-sb_org);
d[sb-sb_org] = '\0';
return sb-sb_org;
}
const char *utf8skip(const char *str, size_t chars)
{
const uint8_t *strb = (const uint8_t*)str;
if (!chars)
return str;
do
{
strb++;
while ((*strb & 0xC0)==0x80) strb++;
chars--;
} while(chars);
return (const char*)strb;
}
size_t utf8len(const char *string)
{
size_t ret = 0;
if (!string)
return 0;
while (*string)
{
if ((*string & 0xC0) != 0x80)
ret++;
string++;
}
return ret;
}
#define utf8_walkbyte(string) (*((*(string))++))
/* Does not validate the input, returns garbage if it's not UTF-8. */
uint32_t utf8_walk(const char **string)
{
uint8_t first = utf8_walkbyte(string);
uint32_t ret = 0;
if (first < 128)
return first;
ret = (ret << 6) | (utf8_walkbyte(string) & 0x3F);
if (first >= 0xE0)
{
ret = (ret << 6) | (utf8_walkbyte(string) & 0x3F);
if (first >= 0xF0)
{
ret = (ret << 6) | (utf8_walkbyte(string) & 0x3F);
return ret | (first & 7) << 18;
}
return ret | (first & 15) << 12;
}
return ret | (first & 31) << 6;
}
static bool utf16_to_char(uint8_t **utf_data,
size_t *dest_len, const uint16_t *in)
{
unsigned len = 0;
while (in[len] != '\0')
len++;
utf16_conv_utf8(NULL, dest_len, in, len);
*dest_len += 1;
*utf_data = (uint8_t*)malloc(*dest_len);
if (*utf_data == 0)
return false;
return utf16_conv_utf8(*utf_data, dest_len, in, len);
}
bool utf16_to_char_string(const uint16_t *in, char *s, size_t len)
{
size_t dest_len = 0;
uint8_t *utf16_data = NULL;
bool ret = utf16_to_char(&utf16_data, &dest_len, in);
if (ret)
{
utf16_data[dest_len] = 0;
strlcpy(s, (const char*)utf16_data, len);
}
free(utf16_data);
utf16_data = NULL;
return ret;
}
#if defined(_WIN32) && !defined(_XBOX) && !defined(UNICODE)
/* Returned pointer MUST be freed by the caller if non-NULL. */
static char *mb_to_mb_string_alloc(const char *str,
enum CodePage cp_in, enum CodePage cp_out)
{
char *path_buf = NULL;
wchar_t *path_buf_wide = NULL;
int path_buf_len = 0;
int path_buf_wide_len = MultiByteToWideChar(cp_in, 0, str, -1, NULL, 0);
/* Windows 95 will return 0 from these functions with
* a UTF8 codepage set without MSLU.
*
* From an unknown MSDN version (others omit this info):
* - CP_UTF8 Windows 98/Me, Windows NT 4.0 and later:
* Translate using UTF-8. When this is set, dwFlags must be zero.
* - Windows 95: Under the Microsoft Layer for Unicode,
* MultiByteToWideChar also supports CP_UTF7 and CP_UTF8.
*/
if (path_buf_wide_len)
{
path_buf_wide = (wchar_t*)
calloc(path_buf_wide_len + sizeof(wchar_t), sizeof(wchar_t));
if (path_buf_wide)
{
MultiByteToWideChar(cp_in, 0,
str, -1, path_buf_wide, path_buf_wide_len);
if (*path_buf_wide)
{
path_buf_len = WideCharToMultiByte(cp_out, 0,
path_buf_wide, -1, NULL, 0, NULL, NULL);
if (path_buf_len)
{
path_buf = (char*)
calloc(path_buf_len + sizeof(char), sizeof(char));
if (path_buf)
{
WideCharToMultiByte(cp_out, 0,
path_buf_wide, -1, path_buf,
path_buf_len, NULL, NULL);
free(path_buf_wide);
if (*path_buf)
return path_buf;
free(path_buf);
return NULL;
}
}
else
{
free(path_buf_wide);
return strdup(str);
}
}
}
}
else
return strdup(str);
if (path_buf_wide)
free(path_buf_wide);
return NULL;
}
#endif
/* Returned pointer MUST be freed by the caller if non-NULL. */
char* utf8_to_local_string_alloc(const char *str)
{
if (str && *str)
{
#if defined(_WIN32) && !defined(_XBOX) && !defined(UNICODE)
return mb_to_mb_string_alloc(str, CODEPAGE_UTF8, CODEPAGE_LOCAL);
#else
/* assume string needs no modification if not on Windows */
return strdup(str);
#endif
}
return NULL;
}
/* Returned pointer MUST be freed by the caller if non-NULL. */
char* local_to_utf8_string_alloc(const char *str)
{
if (str && *str)
{
#if defined(_WIN32) && !defined(_XBOX) && !defined(UNICODE)
return mb_to_mb_string_alloc(str, CODEPAGE_LOCAL, CODEPAGE_UTF8);
#else
/* assume string needs no modification if not on Windows */
return strdup(str);
#endif
}
return NULL;
}
/* Returned pointer MUST be freed by the caller if non-NULL. */
wchar_t* utf8_to_utf16_string_alloc(const char *str)
{
#ifdef _WIN32
int len = 0;
int out_len = 0;
#else
size_t len = 0;
size_t out_len = 0;
#endif
wchar_t *buf = NULL;
if (!str || !*str)
return NULL;
#ifdef _WIN32
len = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0);
if (len)
{
buf = (wchar_t*)calloc(len, sizeof(wchar_t));
if (!buf)
return NULL;
out_len = MultiByteToWideChar(CP_UTF8, 0, str, -1, buf, len);
}
else
{
/* fallback to ANSI codepage instead */
len = MultiByteToWideChar(CP_ACP, 0, str, -1, NULL, 0);
if (len)
{
buf = (wchar_t*)calloc(len, sizeof(wchar_t));
if (!buf)
return NULL;
out_len = MultiByteToWideChar(CP_ACP, 0, str, -1, buf, len);
}
}
if (out_len < 0)
{
free(buf);
return NULL;
}
#else
/* NOTE: For now, assume non-Windows platforms' locale is already UTF-8. */
len = mbstowcs(NULL, str, 0) + 1;
if (len)
{
buf = (wchar_t*)calloc(len, sizeof(wchar_t));
if (!buf)
return NULL;
out_len = mbstowcs(buf, str, len);
}
if (out_len == (size_t)-1)
{
free(buf);
return NULL;
}
#endif
return buf;
}
/* Returned pointer MUST be freed by the caller if non-NULL. */
char* utf16_to_utf8_string_alloc(const wchar_t *str)
{
#ifdef _WIN32
int len = 0;
#else
size_t len = 0;
#endif
char *buf = NULL;
if (!str || !*str)
return NULL;
#ifdef _WIN32
{
UINT code_page = CP_UTF8;
len = WideCharToMultiByte(code_page,
0, str, -1, NULL, 0, NULL, NULL);
/* fallback to ANSI codepage instead */
if (!len)
{
code_page = CP_ACP;
len = WideCharToMultiByte(code_page,
0, str, -1, NULL, 0, NULL, NULL);
}
buf = (char*)calloc(len, sizeof(char));
if (!buf)
return NULL;
if (WideCharToMultiByte(code_page,
0, str, -1, buf, len, NULL, NULL) < 0)
{
free(buf);
return NULL;
}
}
#else
/* NOTE: For now, assume non-Windows platforms'
* locale is already UTF-8. */
len = wcstombs(NULL, str, 0) + 1;
if (len)
{
buf = (char*)calloc(len, sizeof(char));
if (!buf)
return NULL;
if (wcstombs(buf, str, len) == (size_t)-1)
{
free(buf);
return NULL;
}
}
#endif
return buf;
}

View File

@@ -0,0 +1,889 @@
/* Copyright (C) 2010-2018 The RetroArch team
*
* ---------------------------------------------------------------------------------------
* The following license statement only applies to this file (features_cpu.c).
* ---------------------------------------------------------------------------------------
*
* Permission is hereby granted, free of charge,
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include <stdio.h>
#include <stdlib.h>
#if defined(_WIN32)
#include <direct.h>
#else
#include <unistd.h>
#endif
#include <compat/strl.h>
#include <streams/file_stream.h>
#include <libretro.h>
#include <features/features_cpu.h>
#include <retro_timers.h>
#if defined(_WIN32) && !defined(_XBOX)
#include <windows.h>
#endif
#if defined(__CELLOS_LV2__)
#ifndef _PPU_INTRINSICS_H
#include <ppu_intrinsics.h>
#endif
#elif defined(_XBOX360)
#include <PPCIntrinsics.h>
#elif defined(_POSIX_MONOTONIC_CLOCK) || defined(ANDROID) || defined(__QNX__) || defined(DJGPP)
/* POSIX_MONOTONIC_CLOCK is not being defined in Android headers despite support being present. */
#include <time.h>
#endif
#if defined(__QNX__) && !defined(CLOCK_MONOTONIC)
#define CLOCK_MONOTONIC 2
#endif
#if defined(PSP)
#include <pspkernel.h>
#include <sys/time.h>
#include <psprtc.h>
#endif
#if defined(VITA)
#include <psp2/kernel/processmgr.h>
#include <psp2/rtc.h>
#endif
#if defined(PS2)
#include <kernel.h>
#include <timer.h>
#include <time.h>
#endif
#if defined(__PSL1GHT__)
#include <sys/time.h>
#elif defined(__CELLOS_LV2__)
#include <sys/sys_time.h>
#endif
#ifdef GEKKO
#include <ogc/lwp_watchdog.h>
#endif
#ifdef WIIU
#include <wiiu/os/time.h>
#endif
#if defined(HAVE_LIBNX)
#include <switch.h>
#elif defined(SWITCH)
#include <libtransistor/types.h>
#include <libtransistor/svc.h>
#endif
#if defined(_3DS)
#include <3ds/svc.h>
#include <3ds/os.h>
#include <3ds/services/cfgu.h>
#endif
/* iOS/OSX specific. Lacks clock_gettime(), so implement it. */
#ifdef __MACH__
#include <sys/time.h>
#ifndef CLOCK_MONOTONIC
#define CLOCK_MONOTONIC 0
#endif
#ifndef CLOCK_REALTIME
#define CLOCK_REALTIME 0
#endif
/* this function is part of iOS 10 now */
static int ra_clock_gettime(int clk_ik, struct timespec *t)
{
struct timeval now;
int rv = gettimeofday(&now, NULL);
if (rv)
return rv;
t->tv_sec = now.tv_sec;
t->tv_nsec = now.tv_usec * 1000;
return 0;
}
#endif
#if defined(__MACH__) && __IPHONE_OS_VERSION_MIN_REQUIRED < 100000
#else
#define ra_clock_gettime clock_gettime
#endif
#ifdef EMSCRIPTEN
#include <emscripten.h>
#endif
#if defined(BSD) || defined(__APPLE__)
#include <sys/sysctl.h>
#endif
#include <string.h>
/**
* cpu_features_get_perf_counter:
*
* Gets performance counter.
*
* Returns: performance counter.
**/
retro_perf_tick_t cpu_features_get_perf_counter(void)
{
retro_perf_tick_t time_ticks = 0;
#if defined(_WIN32)
long tv_sec, tv_usec;
#if defined(_MSC_VER) && _MSC_VER <= 1200
static const unsigned __int64 epoch = 11644473600000000;
#else
static const unsigned __int64 epoch = 11644473600000000ULL;
#endif
FILETIME file_time;
SYSTEMTIME system_time;
ULARGE_INTEGER ularge;
GetSystemTime(&system_time);
SystemTimeToFileTime(&system_time, &file_time);
ularge.LowPart = file_time.dwLowDateTime;
ularge.HighPart = file_time.dwHighDateTime;
tv_sec = (long)((ularge.QuadPart - epoch) / 10000000L);
tv_usec = (long)(system_time.wMilliseconds * 1000);
time_ticks = (1000000 * tv_sec + tv_usec);
#elif defined(__linux__) || defined(__QNX__) || defined(__MACH__)
struct timespec tv = {0};
if (ra_clock_gettime(CLOCK_MONOTONIC, &tv) == 0)
time_ticks = (retro_perf_tick_t)tv.tv_sec * 1000000000 +
(retro_perf_tick_t)tv.tv_nsec;
#elif defined(__GNUC__) && defined(__i386__) || defined(__i486__) || defined(__i686__) || defined(_M_X64) || defined(_M_AMD64)
__asm__ volatile ("rdtsc" : "=A" (time_ticks));
#elif defined(__GNUC__) && defined(__x86_64__) || defined(_M_IX86)
unsigned a, d;
__asm__ volatile ("rdtsc" : "=a" (a), "=d" (d));
time_ticks = (retro_perf_tick_t)a | ((retro_perf_tick_t)d << 32);
#elif defined(__ARM_ARCH_6__)
__asm__ volatile( "mrc p15, 0, %0, c9, c13, 0" : "=r"(time_ticks) );
#elif defined(__CELLOS_LV2__) || defined(_XBOX360) || defined(__powerpc__) || defined(__ppc__) || defined(__POWERPC__)
time_ticks = __mftb();
#elif defined(GEKKO)
time_ticks = gettime();
#elif defined(PSP)
sceRtcGetCurrentTick((uint64_t*)&time_ticks);
#elif defined(VITA)
sceRtcGetCurrentTick((SceRtcTick*)&time_ticks);
#elif defined(PS2)
time_ticks = clock()*294912; // 294,912MHZ / 1000 msecs
#elif defined(_3DS)
time_ticks = svcGetSystemTick();
#elif defined(WIIU)
time_ticks = OSGetSystemTime();
#elif defined(__mips__)
struct timeval tv;
gettimeofday(&tv,NULL);
time_ticks = (1000000 * tv.tv_sec + tv.tv_usec);
#elif defined(HAVE_LIBNX)
time_ticks = armGetSystemTick();
#endif
return time_ticks;
}
/**
* cpu_features_get_time_usec:
*
* Gets time in microseconds.
*
* Returns: time in microseconds.
**/
retro_time_t cpu_features_get_time_usec(void)
{
#if defined(_WIN32)
static LARGE_INTEGER freq;
LARGE_INTEGER count;
/* Frequency is guaranteed to not change. */
if (!freq.QuadPart && !QueryPerformanceFrequency(&freq))
return 0;
if (!QueryPerformanceCounter(&count))
return 0;
return count.QuadPart * 1000000 / freq.QuadPart;
#elif defined(__CELLOS_LV2__)
return sys_time_get_system_time();
#elif defined(GEKKO)
return ticks_to_microsecs(gettime());
#elif defined(WIIU)
return ticks_to_us(OSGetSystemTime());
#elif defined(SWITCH) || defined(HAVE_LIBNX)
return (svcGetSystemTick() * 10) / 192;
#elif defined(_POSIX_MONOTONIC_CLOCK) || defined(__QNX__) || defined(ANDROID) || defined(__MACH__)
struct timespec tv = {0};
if (ra_clock_gettime(CLOCK_MONOTONIC, &tv) < 0)
return 0;
return tv.tv_sec * INT64_C(1000000) + (tv.tv_nsec + 500) / 1000;
#elif defined(EMSCRIPTEN)
return emscripten_get_now() * 1000;
#elif defined(PS2)
return clock()*1000;
#elif defined(__mips__) || defined(DJGPP)
struct timeval tv;
gettimeofday(&tv,NULL);
return (1000000 * tv.tv_sec + tv.tv_usec);
#elif defined(_3DS)
return osGetTime() * 1000;
#elif defined(VITA)
return sceKernelGetProcessTimeWide();
#else
#error "Your platform does not have a timer function implemented in cpu_features_get_time_usec(). Cannot continue."
#endif
}
#if defined(__x86_64__) || defined(__i386__) || defined(__i486__) || defined(__i686__) || (defined(_M_X64) && _MSC_VER > 1310) || (defined(_M_IX86) && _MSC_VER > 1310)
#define CPU_X86
#endif
#if defined(_MSC_VER) && !defined(_XBOX)
#if (_MSC_VER > 1310)
#include <intrin.h>
#endif
#endif
#if defined(CPU_X86) && !defined(__MACH__)
void x86_cpuid(int func, int flags[4])
{
/* On Android, we compile RetroArch with PIC, and we
* are not allowed to clobber the ebx register. */
#ifdef __x86_64__
#define REG_b "rbx"
#define REG_S "rsi"
#else
#define REG_b "ebx"
#define REG_S "esi"
#endif
#if defined(__GNUC__)
__asm__ volatile (
"mov %%" REG_b ", %%" REG_S "\n"
"cpuid\n"
"xchg %%" REG_b ", %%" REG_S "\n"
: "=a"(flags[0]), "=S"(flags[1]), "=c"(flags[2]), "=d"(flags[3])
: "a"(func));
#elif defined(_MSC_VER)
__cpuid(flags, func);
#else
printf("Unknown compiler. Cannot check CPUID with inline assembly.\n");
memset(flags, 0, 4 * sizeof(int));
#endif
}
/* Only runs on i686 and above. Needs to be conditionally run. */
static uint64_t xgetbv_x86(uint32_t idx)
{
#if defined(__GNUC__)
uint32_t eax, edx;
__asm__ volatile (
/* Older GCC versions (Apple's GCC for example) do
* not understand xgetbv instruction.
* Stamp out the machine code directly.
*/
".byte 0x0f, 0x01, 0xd0\n"
: "=a"(eax), "=d"(edx) : "c"(idx));
return ((uint64_t)edx << 32) | eax;
#elif _MSC_FULL_VER >= 160040219
/* Intrinsic only works on 2010 SP1 and above. */
return _xgetbv(idx);
#else
printf("Unknown compiler. Cannot check xgetbv bits.\n");
return 0;
#endif
}
#endif
#if defined(__ARM_NEON__)
static void arm_enable_runfast_mode(void)
{
/* RunFast mode. Enables flush-to-zero and some
* floating point optimizations. */
static const unsigned x = 0x04086060;
static const unsigned y = 0x03000000;
int r;
__asm__ volatile(
"fmrx %0, fpscr \n\t" /* r0 = FPSCR */
"and %0, %0, %1 \n\t" /* r0 = r0 & 0x04086060 */
"orr %0, %0, %2 \n\t" /* r0 = r0 | 0x03000000 */
"fmxr fpscr, %0 \n\t" /* FPSCR = r0 */
: "=r"(r)
: "r"(x), "r"(y)
);
}
#endif
#if defined(__linux__) && !defined(CPU_X86)
static unsigned char check_arm_cpu_feature(const char* feature)
{
char line[1024];
unsigned char status = 0;
RFILE *fp = filestream_open("/proc/cpuinfo",
RETRO_VFS_FILE_ACCESS_READ,
RETRO_VFS_FILE_ACCESS_HINT_NONE);
if (!fp)
return 0;
while (filestream_gets(fp, line, sizeof(line)) != NULL)
{
if (strncmp(line, "Features\t: ", 11))
continue;
if (strstr(line + 11, feature) != NULL)
status = 1;
break;
}
filestream_close(fp);
return status;
}
#if !defined(_SC_NPROCESSORS_ONLN)
/* Parse an decimal integer starting from 'input', but not going further
* than 'limit'. Return the value into '*result'.
*
* NOTE: Does not skip over leading spaces, or deal with sign characters.
* NOTE: Ignores overflows.
*
* The function returns NULL in case of error (bad format), or the new
* position after the decimal number in case of success (which will always
* be <= 'limit').
*/
static const char *parse_decimal(const char* input,
const char* limit, int* result)
{
const char* p = input;
int val = 0;
while (p < limit)
{
int d = (*p - '0');
if ((unsigned)d >= 10U)
break;
val = val*10 + d;
p++;
}
if (p == input)
return NULL;
*result = val;
return p;
}
/* Parse a textual list of cpus and store the result inside a CpuList object.
* Input format is the following:
* - comma-separated list of items (no spaces)
* - each item is either a single decimal number (cpu index), or a range made
* of two numbers separated by a single dash (-). Ranges are inclusive.
*
* Examples: 0
* 2,4-127,128-143
* 0-1
*/
static void cpulist_parse(CpuList* list, char **buf, ssize_t length)
{
const char* p = (const char*)buf;
const char* end = p + length;
/* NOTE: the input line coming from sysfs typically contains a
* trailing newline, so take care of it in the code below
*/
while (p < end && *p != '\n')
{
int val, start_value, end_value;
/* Find the end of current item, and put it into 'q' */
const char *q = (const char*)memchr(p, ',', end-p);
if (!q)
q = end;
/* Get first value */
p = parse_decimal(p, q, &start_value);
if (p == NULL)
return;
end_value = start_value;
/* If we're not at the end of the item, expect a dash and
* and integer; extract end value.
*/
if (p < q && *p == '-')
{
p = parse_decimal(p+1, q, &end_value);
if (p == NULL)
return;
}
/* Set bits CPU list bits */
for (val = start_value; val <= end_value; val++)
{
if ((unsigned)val < 32)
list->mask |= (uint32_t)(1U << val);
}
/* Jump to next item */
p = q;
if (p < end)
p++;
}
}
/* Read a CPU list from one sysfs file */
static void cpulist_read_from(CpuList* list, const char* filename)
{
ssize_t length;
char *buf = NULL;
list->mask = 0;
if (filestream_read_file(filename, (void**)&buf, &length) != 1)
return;
cpulist_parse(list, &buf, length);
if (buf)
free(buf);
buf = NULL;
}
#endif
#endif
/**
* cpu_features_get_core_amount:
*
* Gets the amount of available CPU cores.
*
* Returns: amount of CPU cores available.
**/
unsigned cpu_features_get_core_amount(void)
{
#if defined(_WIN32) && !defined(_XBOX)
/* Win32 */
SYSTEM_INFO sysinfo;
#if defined(__WINRT__) || defined(WINAPI_FAMILY) && WINAPI_FAMILY == WINAPI_FAMILY_PHONE_APP
GetNativeSystemInfo(&sysinfo);
#else
GetSystemInfo(&sysinfo);
#endif
return sysinfo.dwNumberOfProcessors;
#elif defined(GEKKO)
return 1;
#elif defined(PSP) || defined(PS2)
return 1;
#elif defined(VITA)
return 4;
#elif defined(HAVE_LIBNX) || defined(SWITCH)
return 4;
#elif defined(_3DS)
u8 device_model = 0xFF;
CFGU_GetSystemModel(&device_model);/*(0 = O3DS, 1 = O3DSXL, 2 = N3DS, 3 = 2DS, 4 = N3DSXL, 5 = N2DSXL)*/
switch (device_model)
{
case 0:
case 1:
case 3:
/*Old 3/2DS*/
return 2;
case 2:
case 4:
case 5:
/*New 3/2DS*/
return 4;
default:
/*Unknown Device Or Check Failed*/
break;
}
return 1;
#elif defined(WIIU)
return 3;
#elif defined(_SC_NPROCESSORS_ONLN)
/* Linux, most UNIX-likes. */
long ret = sysconf(_SC_NPROCESSORS_ONLN);
if (ret <= 0)
return (unsigned)1;
return (unsigned)ret;
#elif defined(BSD) || defined(__APPLE__)
/* BSD */
/* Copypasta from stackoverflow, dunno if it works. */
int num_cpu = 0;
int mib[4];
size_t len = sizeof(num_cpu);
mib[0] = CTL_HW;
mib[1] = HW_AVAILCPU;
sysctl(mib, 2, &num_cpu, &len, NULL, 0);
if (num_cpu < 1)
{
mib[1] = HW_NCPU;
sysctl(mib, 2, &num_cpu, &len, NULL, 0);
if (num_cpu < 1)
num_cpu = 1;
}
return num_cpu;
#elif defined(__linux__)
CpuList cpus_present[1];
CpuList cpus_possible[1];
int amount = 0;
cpulist_read_from(cpus_present, "/sys/devices/system/cpu/present");
cpulist_read_from(cpus_possible, "/sys/devices/system/cpu/possible");
/* Compute the intersection of both sets to get the actual number of
* CPU cores that can be used on this device by the kernel.
*/
cpus_present->mask &= cpus_possible->mask;
amount = __builtin_popcount(cpus_present->mask);
if (amount == 0)
return 1;
return amount;
#elif defined(_XBOX360)
return 3;
#else
/* No idea, assume single core. */
return 1;
#endif
}
/* According to http://en.wikipedia.org/wiki/CPUID */
#define VENDOR_INTEL_b 0x756e6547
#define VENDOR_INTEL_c 0x6c65746e
#define VENDOR_INTEL_d 0x49656e69
/**
* cpu_features_get:
*
* Gets CPU features..
*
* Returns: bitmask of all CPU features available.
**/
uint64_t cpu_features_get(void)
{
int flags[4];
int vendor_shuffle[3];
char vendor[13];
size_t len = 0;
uint64_t cpu_flags = 0;
uint64_t cpu = 0;
unsigned max_flag = 0;
#if defined(CPU_X86) && !defined(__MACH__)
int vendor_is_intel = 0;
const int avx_flags = (1 << 27) | (1 << 28);
#endif
char buf[sizeof(" MMX MMXEXT SSE SSE2 SSE3 SSSE3 SS4 SSE4.2 AES AVX AVX2 NEON VMX VMX128 VFPU PS")];
memset(buf, 0, sizeof(buf));
(void)len;
(void)cpu_flags;
(void)flags;
(void)max_flag;
(void)vendor;
(void)vendor_shuffle;
#if defined(__MACH__)
len = sizeof(size_t);
if (sysctlbyname("hw.optional.mmx", NULL, &len, NULL, 0) == 0)
{
cpu |= RETRO_SIMD_MMX;
cpu |= RETRO_SIMD_MMXEXT;
}
len = sizeof(size_t);
if (sysctlbyname("hw.optional.floatingpoint", NULL, &len, NULL, 0) == 0)
{
cpu |= RETRO_SIMD_CMOV;
}
len = sizeof(size_t);
if (sysctlbyname("hw.optional.sse", NULL, &len, NULL, 0) == 0)
cpu |= RETRO_SIMD_SSE;
len = sizeof(size_t);
if (sysctlbyname("hw.optional.sse2", NULL, &len, NULL, 0) == 0)
cpu |= RETRO_SIMD_SSE2;
len = sizeof(size_t);
if (sysctlbyname("hw.optional.sse3", NULL, &len, NULL, 0) == 0)
cpu |= RETRO_SIMD_SSE3;
len = sizeof(size_t);
if (sysctlbyname("hw.optional.supplementalsse3", NULL, &len, NULL, 0) == 0)
cpu |= RETRO_SIMD_SSSE3;
len = sizeof(size_t);
if (sysctlbyname("hw.optional.sse4_1", NULL, &len, NULL, 0) == 0)
cpu |= RETRO_SIMD_SSE4;
len = sizeof(size_t);
if (sysctlbyname("hw.optional.sse4_2", NULL, &len, NULL, 0) == 0)
cpu |= RETRO_SIMD_SSE42;
len = sizeof(size_t);
if (sysctlbyname("hw.optional.aes", NULL, &len, NULL, 0) == 0)
cpu |= RETRO_SIMD_AES;
len = sizeof(size_t);
if (sysctlbyname("hw.optional.avx1_0", NULL, &len, NULL, 0) == 0)
cpu |= RETRO_SIMD_AVX;
len = sizeof(size_t);
if (sysctlbyname("hw.optional.avx2_0", NULL, &len, NULL, 0) == 0)
cpu |= RETRO_SIMD_AVX2;
len = sizeof(size_t);
if (sysctlbyname("hw.optional.altivec", NULL, &len, NULL, 0) == 0)
cpu |= RETRO_SIMD_VMX;
len = sizeof(size_t);
if (sysctlbyname("hw.optional.neon", NULL, &len, NULL, 0) == 0)
cpu |= RETRO_SIMD_NEON;
#elif defined(_XBOX1)
cpu |= RETRO_SIMD_MMX;
cpu |= RETRO_SIMD_SSE;
cpu |= RETRO_SIMD_MMXEXT;
#elif defined(CPU_X86)
(void)avx_flags;
x86_cpuid(0, flags);
vendor_shuffle[0] = flags[1];
vendor_shuffle[1] = flags[3];
vendor_shuffle[2] = flags[2];
vendor[0] = '\0';
memcpy(vendor, vendor_shuffle, sizeof(vendor_shuffle));
/* printf("[CPUID]: Vendor: %s\n", vendor); */
vendor_is_intel = (
flags[1] == VENDOR_INTEL_b &&
flags[2] == VENDOR_INTEL_c &&
flags[3] == VENDOR_INTEL_d);
max_flag = flags[0];
if (max_flag < 1) /* Does CPUID not support func = 1? (unlikely ...) */
return 0;
x86_cpuid(1, flags);
if (flags[3] & (1 << 15))
cpu |= RETRO_SIMD_CMOV;
if (flags[3] & (1 << 23))
cpu |= RETRO_SIMD_MMX;
if (flags[3] & (1 << 25))
{
/* SSE also implies MMXEXT (according to FFmpeg source). */
cpu |= RETRO_SIMD_SSE;
cpu |= RETRO_SIMD_MMXEXT;
}
if (flags[3] & (1 << 26))
cpu |= RETRO_SIMD_SSE2;
if (flags[2] & (1 << 0))
cpu |= RETRO_SIMD_SSE3;
if (flags[2] & (1 << 9))
cpu |= RETRO_SIMD_SSSE3;
if (flags[2] & (1 << 19))
cpu |= RETRO_SIMD_SSE4;
if (flags[2] & (1 << 20))
cpu |= RETRO_SIMD_SSE42;
if ((flags[2] & (1 << 23)))
cpu |= RETRO_SIMD_POPCNT;
if (vendor_is_intel && (flags[2] & (1 << 22)))
cpu |= RETRO_SIMD_MOVBE;
if (flags[2] & (1 << 25))
cpu |= RETRO_SIMD_AES;
/* Must only perform xgetbv check if we have
* AVX CPU support (guaranteed to have at least i686). */
if (((flags[2] & avx_flags) == avx_flags)
&& ((xgetbv_x86(0) & 0x6) == 0x6))
cpu |= RETRO_SIMD_AVX;
if (max_flag >= 7)
{
x86_cpuid(7, flags);
if (flags[1] & (1 << 5))
cpu |= RETRO_SIMD_AVX2;
}
x86_cpuid(0x80000000, flags);
max_flag = flags[0];
if (max_flag >= 0x80000001u)
{
x86_cpuid(0x80000001, flags);
if (flags[3] & (1 << 23))
cpu |= RETRO_SIMD_MMX;
if (flags[3] & (1 << 22))
cpu |= RETRO_SIMD_MMXEXT;
}
#elif defined(__linux__)
if (check_arm_cpu_feature("neon"))
{
cpu |= RETRO_SIMD_NEON;
#ifdef __ARM_NEON__
arm_enable_runfast_mode();
#endif
}
if (check_arm_cpu_feature("vfpv3"))
cpu |= RETRO_SIMD_VFPV3;
if (check_arm_cpu_feature("vfpv4"))
cpu |= RETRO_SIMD_VFPV4;
if (check_arm_cpu_feature("asimd"))
{
cpu |= RETRO_SIMD_ASIMD;
#ifdef __ARM_NEON__
cpu |= RETRO_SIMD_NEON;
arm_enable_runfast_mode();
#endif
}
#if 0
check_arm_cpu_feature("swp");
check_arm_cpu_feature("half");
check_arm_cpu_feature("thumb");
check_arm_cpu_feature("fastmult");
check_arm_cpu_feature("vfp");
check_arm_cpu_feature("edsp");
check_arm_cpu_feature("thumbee");
check_arm_cpu_feature("tls");
check_arm_cpu_feature("idiva");
check_arm_cpu_feature("idivt");
#endif
#elif defined(__ARM_NEON__)
cpu |= RETRO_SIMD_NEON;
arm_enable_runfast_mode();
#elif defined(__ALTIVEC__)
cpu |= RETRO_SIMD_VMX;
#elif defined(XBOX360)
cpu |= RETRO_SIMD_VMX128;
#elif defined(PSP) || defined(PS2)
cpu |= RETRO_SIMD_VFPU;
#elif defined(GEKKO)
cpu |= RETRO_SIMD_PS;
#endif
if (cpu & RETRO_SIMD_MMX) strlcat(buf, " MMX", sizeof(buf));
if (cpu & RETRO_SIMD_MMXEXT) strlcat(buf, " MMXEXT", sizeof(buf));
if (cpu & RETRO_SIMD_SSE) strlcat(buf, " SSE", sizeof(buf));
if (cpu & RETRO_SIMD_SSE2) strlcat(buf, " SSE2", sizeof(buf));
if (cpu & RETRO_SIMD_SSE3) strlcat(buf, " SSE3", sizeof(buf));
if (cpu & RETRO_SIMD_SSSE3) strlcat(buf, " SSSE3", sizeof(buf));
if (cpu & RETRO_SIMD_SSE4) strlcat(buf, " SSE4", sizeof(buf));
if (cpu & RETRO_SIMD_SSE42) strlcat(buf, " SSE4.2", sizeof(buf));
if (cpu & RETRO_SIMD_AES) strlcat(buf, " AES", sizeof(buf));
if (cpu & RETRO_SIMD_AVX) strlcat(buf, " AVX", sizeof(buf));
if (cpu & RETRO_SIMD_AVX2) strlcat(buf, " AVX2", sizeof(buf));
if (cpu & RETRO_SIMD_NEON) strlcat(buf, " NEON", sizeof(buf));
if (cpu & RETRO_SIMD_VFPV3) strlcat(buf, " VFPv3", sizeof(buf));
if (cpu & RETRO_SIMD_VFPV4) strlcat(buf, " VFPv4", sizeof(buf));
if (cpu & RETRO_SIMD_VMX) strlcat(buf, " VMX", sizeof(buf));
if (cpu & RETRO_SIMD_VMX128) strlcat(buf, " VMX128", sizeof(buf));
if (cpu & RETRO_SIMD_VFPU) strlcat(buf, " VFPU", sizeof(buf));
if (cpu & RETRO_SIMD_PS) strlcat(buf, " PS", sizeof(buf));
if (cpu & RETRO_SIMD_ASIMD) strlcat(buf, " ASIMD", sizeof(buf));
return cpu;
}
void cpu_features_get_model_name(char *name, int len)
{
#if defined(CPU_X86) && !defined(__MACH__)
union {
int i[4];
unsigned char s[16];
} flags;
int i, j;
size_t pos = 0;
bool start = false;
if (!name)
return;
x86_cpuid(0x80000000, flags.i);
if (flags.i[0] < 0x80000004)
return;
for (i = 0; i < 3; i++)
{
memset(flags.i, 0, sizeof(flags.i));
x86_cpuid(0x80000002 + i, flags.i);
for (j = 0; j < sizeof(flags.s); j++)
{
if (!start && flags.s[j] == ' ')
continue;
else
start = true;
if (pos == len - 1)
{
/* truncate if we ran out of room */
name[pos] = '\0';
goto end;
}
name[pos++] = flags.s[j];
}
}
end:
/* terminate our string */
if (pos < (size_t)len)
name[pos] = '\0';
#elif defined(__MACH__)
if (!name)
return;
{
size_t len_size = len;
sysctlbyname("machdep.cpu.brand_string", name, &len_size, NULL, 0);
}
#else
if (!name)
return;
return;
#endif
}

View File

@@ -0,0 +1,18 @@
Copyright (c) 2013 Mikko Mononen memon@inside.org
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,74 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019 natinusala
Copyright (C) 2019 p-sam
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <borealis/application.hpp>
#include <borealis/header.hpp>
namespace brls
{
Header::Header(std::string label, bool separator, std::string sublabel)
: label(label)
, sublabel(sublabel)
, separator(separator)
{
Style* style = Application::getStyle();
this->setHeight(style->Header.height);
}
void Header::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, Style* style, FrameContext* ctx)
{
unsigned padding = style->Header.padding;
// Rectangle
nvgBeginPath(vg);
nvgFillColor(vg, a(ctx->theme->headerRectangleColor));
nvgRect(vg, x, y + padding, style->Header.rectangleWidth, height - padding * 2);
nvgFill(vg);
// Label
nvgBeginPath(vg);
nvgFontFaceId(vg, ctx->fontStash->regular);
nvgFontSize(vg, style->Header.fontSize);
nvgFillColor(vg, a(ctx->theme->textColor));
nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
nvgText(vg, x + style->Header.rectangleWidth + padding, y + height / 2, this->label.c_str(), nullptr);
// Sublabel
if (this->sublabel != "")
{
nvgBeginPath(vg);
nvgFontFaceId(vg, ctx->fontStash->regular);
nvgFontSize(vg, style->Header.fontSize);
nvgFillColor(vg, a(ctx->theme->descriptionColor));
nvgTextAlign(vg, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE);
nvgText(vg, x + width - style->Header.rectangleWidth - padding, y + height / 2, this->sublabel.c_str(), nullptr);
}
// Separator
if (this->separator)
{
nvgBeginPath(vg);
nvgFillColor(vg, a(ctx->theme->listItemSeparatorColor));
nvgRect(vg, x, y + height, width, 1);
nvgFill(vg);
}
}
} // namespace brls

View File

@@ -0,0 +1,224 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2020 WerWolv
Copyright (C) 2020 natinusala
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <borealis/actions.hpp>
#include <borealis/application.hpp>
#include <borealis/hint.hpp>
#include <borealis/label.hpp>
#include <set>
namespace brls
{
Hint::Hint(bool animate)
: BoxLayout(BoxLayoutOrientation::HORIZONTAL)
, animate(animate)
{
Style* style = Application::getStyle();
this->setGravity(BoxLayoutGravity::RIGHT);
this->setHeight(style->AppletFrame.footerHeight);
this->setSpacing(style->AppletFrame.footerTextSpacing);
// Subscribe to all events
this->globalFocusEventSubscriptor = Application::getGlobalFocusChangeEvent()->subscribe([this](View* newFocus) {
this->rebuildHints();
});
this->globalHintsUpdateEventSubscriptor = Application::getGlobalHintsUpdateEvent()->subscribe([this]() {
this->rebuildHints();
});
}
bool actionsSortFunc(Action a, Action b)
{
// From left to right:
// - first +
// - then all hints that are not B and A
// - finally B and A
// + is before all others
if (a.key == Key::PLUS)
return true;
// A is after all others
if (b.key == Key::A)
return true;
// B is after all others but A
if (b.key == Key::B && a.key != Key::A)
return true;
// Keep original order for the rest
return false;
}
void Hint::rebuildHints()
{
// Check if the focused element is still a child of the same parent as the hint view's
{
View* focusParent = Application::getCurrentFocus();
View* hintBaseParent = this;
while (focusParent != nullptr)
{
if (focusParent->getParent() == nullptr)
break;
focusParent = focusParent->getParent();
}
while (hintBaseParent != nullptr)
{
if (hintBaseParent->getParent() == nullptr)
break;
hintBaseParent = hintBaseParent->getParent();
}
if (focusParent != hintBaseParent)
return;
}
// Empty the layout and re-populate it with new Labels
this->clear(true);
std::set<Key> addedKeys; // we only ever want one action per key
View* focusParent = Application::getCurrentFocus();
// Iterate over the view tree to find all the actions to display
std::vector<Action> actions;
while (focusParent != nullptr)
{
for (auto& action : focusParent->getActions())
{
if (action.hidden)
continue;
if (addedKeys.find(action.key) != addedKeys.end())
continue;
addedKeys.insert(action.key);
actions.push_back(action);
}
focusParent = focusParent->getParent();
}
// Sort the actions
std::stable_sort(actions.begin(), actions.end(), actionsSortFunc);
// Populate the layout with labels
for (Action action : actions)
{
std::string hintText = Hint::getKeyIcon(action.key) + " " + action.hintText;
Label* label = new Label(LabelStyle::HINT, hintText);
this->addView(label);
}
}
Hint::~Hint()
{
// Unregister all events
Application::getGlobalFocusChangeEvent()->unsubscribe(this->globalFocusEventSubscriptor);
Application::getGlobalHintsUpdateEvent()->unsubscribe(this->globalHintsUpdateEventSubscriptor);
}
std::string Hint::getKeyIcon(Key key)
{
switch (key)
{
case Key::A:
return "\uE0E0";
case Key::B:
return "\uE0E1";
case Key::X:
return "\uE0E2";
case Key::Y:
return "\uE0E3";
case Key::LSTICK:
return "\uE104";
case Key::RSTICK:
return "\uE105";
case Key::L:
return "\uE0E4";
case Key::R:
return "\uE0E5";
case Key::PLUS:
return "\uE0EF";
case Key::MINUS:
return "\uE0F0";
case Key::DLEFT:
return "\uE0ED";
case Key::DUP:
return "\uE0EB";
case Key::DRIGHT:
return "\uE0EF";
case Key::DDOWN:
return "\uE0EC";
default:
return "\uE152";
}
}
void Hint::willAppear(bool resetState)
{
// Push ourself to hide other hints
// We assume that we are the top-most hint
Hint::pushHint(this);
}
void Hint::willDisappear(bool resetState)
{
// Pop ourself to show other hints
Hint::popHint(this);
}
void Hint::pushHint(Hint* hint)
{
// If the hint is already in the stack, remove it and put it back on top
Hint::globalHintStack.erase(std::remove(Hint::globalHintStack.begin(), Hint::globalHintStack.end(), hint), Hint::globalHintStack.end());
Hint::globalHintStack.push_back(hint);
//Trigger animation
Hint::animateHints();
}
void Hint::popHint(Hint* hint)
{
Hint::globalHintStack.erase(std::remove(Hint::globalHintStack.begin(), Hint::globalHintStack.end(), hint), Hint::globalHintStack.end());
Hint::animateHints();
// animateHints() won't call hide() on the hint since it's been removed from the stack
// but it doesn't matter since it is getting destroyed anyay
}
void Hint::animateHints()
{
for (size_t i = 0; i < Hint::globalHintStack.size(); i++)
{
// Last one = top one: show
if (i == Hint::globalHintStack.size() - 1)
Hint::globalHintStack[i]->show([]() {}, false);
else
Hint::globalHintStack[i]->hide([]() {}, false);
}
}
} // namespace brls

View File

@@ -0,0 +1,183 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019 WerWolv
Copyright (C) 2019 p-sam
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <borealis/application.hpp>
#include <borealis/image.hpp>
#include <cstring>
namespace brls
{
Image::Image(std::string imagePath)
{
this->setImage(imagePath);
this->setOpacity(1.0F);
}
Image::Image(unsigned char* buffer, size_t bufferSize)
{
this->setImage(buffer, bufferSize);
this->setOpacity(1.0F);
}
Image::~Image()
{
if (this->imageBuffer != nullptr)
delete[] this->imageBuffer;
if (this->texture != -1)
nvgDeleteImage(Application::getNVGContext(), this->texture);
}
void Image::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, Style* style, FrameContext* ctx)
{
nvgSave(vg);
if (this->texture != -1)
{
nvgBeginPath(vg);
nvgRoundedRect(vg, x + this->imageX, y + this->imageY, this->imageWidth, this->imageHeight, this->cornerRadius);
nvgFillPaint(vg, a(this->imgPaint));
nvgFill(vg);
}
nvgRestore(vg);
}
void Image::reloadTexture()
{
NVGcontext* vg = Application::getNVGContext();
if (this->texture != -1)
nvgDeleteImage(vg, this->texture);
if (!this->imagePath.empty())
this->texture = nvgCreateImage(vg, this->imagePath.c_str(), 0);
else if (this->imageBuffer != nullptr)
this->texture = nvgCreateImageMem(vg, 0, this->imageBuffer, this->imageBufferSize);
}
void Image::layout(NVGcontext* vg, Style* style, FontStash* stash)
{
if (this->origViewWidth == 0 || this->origViewHeight == 0)
{
this->origViewWidth = this->getWidth();
this->origViewHeight = this->getHeight();
}
nvgImageSize(vg, this->texture, &this->imageWidth, &this->imageHeight);
this->setWidth(this->origViewWidth);
this->setHeight(this->origViewHeight);
this->imageX = 0;
this->imageY = 0;
float viewAspectRatio = static_cast<float>(this->getWidth()) / static_cast<float>(this->getHeight());
float imageAspectRatio = static_cast<float>(this->imageWidth) / static_cast<float>(this->imageHeight);
switch (imageScaleType)
{
case ImageScaleType::NO_RESIZE:
this->imageX = (this->origViewWidth - this->imageWidth) / 2.0F;
this->imageY = (this->origViewHeight - this->imageHeight) / 2.0F;
break;
case ImageScaleType::FIT:
if (viewAspectRatio >= imageAspectRatio)
{
this->imageHeight = this->getHeight();
this->imageWidth = this->imageHeight * imageAspectRatio;
this->imageX = (this->origViewWidth - this->imageWidth) / 2.0F;
}
else
{
this->imageWidth = this->getWidth();
this->imageHeight = this->imageWidth * imageAspectRatio;
this->imageY = (this->origViewHeight - this->imageHeight) / 2.0F;
}
break;
case ImageScaleType::CROP:
if (viewAspectRatio < imageAspectRatio)
{
this->imageHeight = this->getHeight();
this->imageWidth = this->imageHeight * imageAspectRatio;
this->imageX = (this->origViewWidth - this->imageWidth) / 2.0F;
}
else
{
this->imageWidth = this->getWidth();
this->imageHeight = this->imageWidth * imageAspectRatio;
this->imageY = (this->origViewHeight - this->imageHeight) / 2.0F;
}
break;
case ImageScaleType::SCALE:
this->imageWidth = this->getWidth();
this->imageHeight = this->getHeight();
break;
case ImageScaleType::VIEW_RESIZE:
this->setWidth(this->imageWidth);
this->setHeight(this->imageHeight);
break;
}
this->imgPaint = nvgImagePattern(vg, getX() + this->imageX, getY() + this->imageY, this->imageWidth, this->imageHeight, 0, this->texture, this->alpha);
}
void Image::setImage(unsigned char* buffer, size_t bufferSize)
{
if (this->imageBuffer != nullptr)
delete[] this->imageBuffer;
this->imagePath = "";
this->imageBuffer = new unsigned char[bufferSize];
std::memcpy(this->imageBuffer, buffer, bufferSize);
this->imageBufferSize = bufferSize;
this->reloadTexture();
this->invalidate();
}
void Image::setImage(std::string imagePath)
{
this->imagePath = imagePath;
if (this->imageBuffer != nullptr)
delete[] this->imageBuffer;
this->imageBuffer = nullptr;
this->reloadTexture();
this->invalidate();
}
void Image::setOpacity(float opacity)
{
this->alpha = opacity;
this->invalidate();
}
void Image::setScaleType(ImageScaleType imageScaleType)
{
this->imageScaleType = imageScaleType;
this->invalidate();
}
} // namespace brls

View File

@@ -0,0 +1,229 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019-2020 natinusala
Copyright (C) 2019 p-sam
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <borealis/application.hpp>
#include <borealis/label.hpp>
namespace brls
{
Label::Label(LabelStyle labelStyle, std::string text, bool multiline)
: text(text)
, multiline(multiline)
, labelStyle(labelStyle)
{
Style* style = Application::getStyle();
this->lineHeight = style->Label.lineHeight;
switch (labelStyle)
{
case LabelStyle::REGULAR:
this->fontSize = style->Label.regularFontSize;
break;
case LabelStyle::MEDIUM:
this->fontSize = style->Label.mediumFontSize;
break;
case LabelStyle::SMALL:
this->fontSize = style->Label.smallFontSize;
break;
case LabelStyle::DESCRIPTION:
this->fontSize = style->Label.descriptionFontSize;
break;
case LabelStyle::CRASH:
this->fontSize = style->Label.crashFontSize;
break;
case LabelStyle::BUTTON_PLAIN_DISABLED:
case LabelStyle::BUTTON_PLAIN:
case LabelStyle::BUTTON_BORDERLESS:
case LabelStyle::BUTTON_DIALOG:
this->fontSize = style->Label.buttonFontSize;
break;
case LabelStyle::LIST_ITEM:
this->fontSize = style->Label.listItemFontSize;
break;
case LabelStyle::NOTIFICATION:
this->fontSize = style->Label.notificationFontSize;
this->lineHeight = style->Label.notificationLineHeight;
break;
case LabelStyle::DIALOG:
this->fontSize = style->Label.dialogFontSize;
break;
case LabelStyle::HINT:
this->fontSize = style->Label.hintFontSize;
}
}
void Label::setHorizontalAlign(NVGalign align)
{
this->horizontalAlign = align;
}
void Label::setVerticalAlign(NVGalign align)
{
this->verticalAlign = align;
}
void Label::setFontSize(unsigned size)
{
this->fontSize = size;
if (this->getParent())
this->getParent()->invalidate();
}
void Label::setText(std::string text)
{
this->text = text;
if (this->hasParent())
this->getParent()->invalidate();
}
void Label::setStyle(LabelStyle style)
{
this->labelStyle = style;
}
void Label::layout(NVGcontext* vg, Style* style, FontStash* stash)
{
nvgSave(vg);
nvgReset(vg);
nvgFontSize(vg, this->fontSize);
nvgTextAlign(vg, this->horizontalAlign | NVG_ALIGN_TOP);
nvgFontFaceId(vg, this->getFont(stash));
nvgTextLineHeight(vg, this->lineHeight);
float bounds[4];
// Update width or height to text bounds
if (this->multiline)
{
nvgTextBoxBounds(vg, this->x, this->y, this->width, this->text.c_str(), nullptr, bounds);
this->height = bounds[3] - bounds[1]; // ymax - ymin
}
else
{
nvgTextBounds(vg, this->x, this->y, this->text.c_str(), nullptr, bounds);
unsigned oldWidth = this->width;
this->width = bounds[2] - bounds[0]; // xmax - xmin
// offset the position to compensate the width change
// and keep right alignment
if (this->horizontalAlign == NVG_ALIGN_RIGHT)
this->x += oldWidth - this->width;
}
nvgRestore(vg);
}
void Label::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, Style* style, FrameContext* ctx)
{
nvgFillColor(vg, this->getColor(ctx->theme));
// Draw
nvgFontSize(vg, this->fontSize);
nvgFontFaceId(vg, this->getFont(ctx->fontStash));
if (this->multiline)
{
nvgTextLineHeight(vg, this->lineHeight);
nvgTextAlign(vg, this->horizontalAlign | NVG_ALIGN_TOP);
nvgBeginPath(vg);
nvgTextBox(vg, x, y, width, this->text.c_str(), nullptr);
}
else
{
nvgTextLineHeight(vg, 1.0f);
nvgTextAlign(vg, this->horizontalAlign | this->verticalAlign);
nvgBeginPath(vg);
if (this->horizontalAlign == NVG_ALIGN_RIGHT)
x += width;
else if (this->horizontalAlign == NVG_ALIGN_CENTER)
x += width / 2;
// TODO: Ticker
if (this->verticalAlign == NVG_ALIGN_BOTTOM)
nvgText(vg, x, y + height, this->text.c_str(), nullptr);
else
nvgText(vg, x, y + height / 2, this->text.c_str(), nullptr);
}
}
void Label::setColor(NVGcolor color)
{
this->customColor = color;
this->useCustomColor = true;
}
void Label::unsetColor()
{
this->useCustomColor = false;
}
NVGcolor Label::getColor(ThemeValues* theme)
{
// Use custom color if any
if (this->useCustomColor)
return a(this->customColor);
switch (this->labelStyle)
{
case LabelStyle::DESCRIPTION:
return a(theme->descriptionColor);
case LabelStyle::CRASH:
return RGB(255, 255, 255);
case LabelStyle::BUTTON_PLAIN:
return a(theme->buttonPlainEnabledTextColor);
case LabelStyle::BUTTON_PLAIN_DISABLED:
return a(theme->buttonPlainDisabledTextColor);
case LabelStyle::NOTIFICATION:
return a(theme->notificationTextColor);
case LabelStyle::BUTTON_DIALOG:
return a(theme->dialogButtonColor);
default:
return a(theme->textColor);
}
}
void Label::setFont(int font)
{
this->customFont = font;
this->useCustomFont = true;
}
void Label::unsetFont()
{
this->useCustomFont = false;
}
int Label::getFont(FontStash* stash)
{
if (this->useCustomFont)
return this->customFont;
return stash->regular;
}
} // namespace brls

View File

@@ -0,0 +1,127 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019 WerWolv
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <borealis/application.hpp>
#include <borealis/layer_view.hpp>
namespace brls
{
LayerView::LayerView()
{
}
LayerView::~LayerView()
{
for (unsigned int i = 0; i < this->layers.size(); i++)
delete this->layers[i];
this->layers.clear();
}
void LayerView::addLayer(View* view)
{
if (view)
{
view->setParent(this);
this->layers.push_back(view);
}
}
void LayerView::changeLayer(int index, bool focus)
{
if (index >= 0 && index < static_cast<int>(this->layers.size()))
{
Application::blockInputs();
if (this->selectedIndex >= 0)
{
this->layers[this->selectedIndex]->willDisappear(true);
this->layers[this->selectedIndex]->hide([]() {});
}
this->selectedIndex = index;
this->layers[this->selectedIndex]->willAppear(true);
this->layers[this->selectedIndex]->show([=]() {
if (focus)
Application::giveFocus(this->layers[this->selectedIndex]->getDefaultFocus());
Application::unblockInputs();
});
this->layers[index]->invalidate();
}
if (index == -1)
{
if (this->selectedIndex > 0)
{
this->layers[this->selectedIndex]->willDisappear(true);
this->layers[this->selectedIndex]->hide([]() {});
}
this->selectedIndex = index;
}
}
int LayerView::getLayerIndex()
{
return this->selectedIndex;
}
View* LayerView::getDefaultFocus()
{
if (this->selectedIndex >= 0 && this->selectedIndex < static_cast<int>(this->layers.size()))
{
View* newFocus = this->layers[this->selectedIndex]->getDefaultFocus();
if (newFocus)
{
return newFocus;
}
}
return nullptr;
}
void LayerView::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, Style* style, FrameContext* ctx)
{
if (this->selectedIndex >= 0 && this->selectedIndex < static_cast<int>(this->layers.size()))
this->layers[this->selectedIndex]->frame(ctx);
}
void LayerView::layout(NVGcontext* vg, Style* style, FontStash* stash)
{
if (this->selectedIndex >= 0 && this->selectedIndex < static_cast<int>(this->layers.size()))
{
this->layers[this->selectedIndex]->setBoundaries(this->getX(), this->getY(), this->getWidth(), this->getHeight());
this->layers[this->selectedIndex]->invalidate();
}
}
void LayerView::willAppear(bool resetState)
{
if (this->selectedIndex >= 0 && this->selectedIndex < static_cast<int>(this->layers.size()))
this->layers[this->selectedIndex]->willAppear(true);
}
void LayerView::willDisappear(bool resetState)
{
if (this->selectedIndex >= 0 && this->selectedIndex < static_cast<int>(this->layers.size()))
this->layers[this->selectedIndex]->willDisappear(true);
}
}

View File

@@ -0,0 +1,618 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019-2020 natinusala
Copyright (C) 2019 p-sam
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <math.h>
#include <borealis/animations.hpp>
#include <borealis/application.hpp>
#include <borealis/dropdown.hpp>
#include <borealis/header.hpp>
#include <borealis/list.hpp>
#include <borealis/logger.hpp>
#include <borealis/swkbd.hpp>
#include <borealis/table.hpp>
// TODO: Scrollbar
namespace brls
{
ListContentView::ListContentView(List* list, size_t defaultFocus)
: BoxLayout(BoxLayoutOrientation::VERTICAL, defaultFocus)
, list(list)
{
Style* style = Application::getStyle();
this->setMargins(style->List.marginTopBottom, style->List.marginLeftRight, style->List.marginTopBottom, style->List.marginLeftRight);
this->setSpacing(style->List.spacing);
this->setRememberFocus(true);
}
void ListContentView::customSpacing(View* current, View* next, int* spacing)
{
// Don't add spacing to the first list item
// if it doesn't have a description and the second one is a
// list item too
// Or if the next item is a ListItemGroupSpacing
if (ListItem* currentItem = dynamic_cast<ListItem*>(current))
{
if (currentItem->getReduceDescriptionSpacing())
{
if (next != nullptr)
*spacing /= 2;
}
else if (ListItem* nextItem = dynamic_cast<ListItem*>(next))
{
if (!currentItem->hasDescription())
{
*spacing = 2;
nextItem->setDrawTopSeparator(currentItem->isCollapsed());
}
}
else if (dynamic_cast<ListItemGroupSpacing*>(next))
{
*spacing = 0;
}
else if (dynamic_cast<Table*>(next))
{
*spacing /= 2;
}
}
// Table custom spacing
else if (dynamic_cast<Table*>(current))
{
*spacing /= 2;
}
// ListItemGroupSpacing custom spacing
else if (dynamic_cast<ListItemGroupSpacing*>(current))
{
*spacing /= 2;
}
// Header custom spacing
else if (dynamic_cast<Header*>(current) || dynamic_cast<Header*>(next))
{
if (dynamic_cast<Header*>(current) && dynamic_cast<ListItem*>(next))
{
*spacing = 1;
}
else if (dynamic_cast<Label*>(current) && dynamic_cast<Header*>(next))
{
// Keep default spacing
}
else
{
Style* style = Application::getStyle();
*spacing = style->Header.padding;
}
}
// Call list custom spacing
if (this->list)
this->list->customSpacing(current, next, spacing);
}
ListItem::ListItem(std::string label, std::string description, std::string subLabel)
: label(label)
, subLabel(subLabel)
{
Style* style = Application::getStyle();
this->setHeight(subLabel != "" ? style->List.Item.heightWithSubLabel : style->List.Item.height);
this->setTextSize(style->Label.listItemFontSize);
if (description != "")
{
this->descriptionView = new Label(LabelStyle::DESCRIPTION, description, true);
this->descriptionView->setParent(this);
}
this->registerAction("OK", Key::A, [this] { return this->onClick(); });
}
void ListItem::setThumbnail(Image* image)
{
if (this->thumbnailView)
delete this->thumbnailView;
if (image != NULL)
{
this->thumbnailView = image;
this->thumbnailView->setParent(this);
this->invalidate();
}
}
void ListItem::setThumbnail(std::string imagePath)
{
if (this->thumbnailView)
this->thumbnailView->setImage(imagePath);
else
this->thumbnailView = new Image(imagePath);
this->thumbnailView->setParent(this);
this->thumbnailView->setScaleType(ImageScaleType::FIT);
this->invalidate();
}
void ListItem::setThumbnail(unsigned char* buffer, size_t bufferSize)
{
if (this->thumbnailView)
this->thumbnailView->setImage(buffer, bufferSize);
else
this->thumbnailView = new Image(buffer, bufferSize);
this->thumbnailView->setParent(this);
this->thumbnailView->setScaleType(ImageScaleType::FIT);
this->invalidate();
}
bool ListItem::getReduceDescriptionSpacing()
{
return this->reduceDescriptionSpacing;
}
void ListItem::setReduceDescriptionSpacing(bool value)
{
this->reduceDescriptionSpacing = value;
}
void ListItem::setIndented(bool indented)
{
this->indented = indented;
}
void ListItem::setTextSize(unsigned textSize)
{
this->textSize = textSize;
}
void ListItem::setChecked(bool checked)
{
this->checked = checked;
}
bool ListItem::onClick()
{
return this->clickEvent.fire(this);
}
GenericEvent* ListItem::getClickEvent()
{
return &this->clickEvent;
}
void ListItem::layout(NVGcontext* vg, Style* style, FontStash* stash)
{
// Description
if (this->descriptionView)
{
unsigned indent = style->List.Item.descriptionIndent;
if (this->indented)
indent += style->List.Item.indent;
this->height = style->List.Item.height;
this->descriptionView->setBoundaries(this->x + indent, this->y + this->height + style->List.Item.descriptionSpacing, this->width - indent * 2, 0);
this->descriptionView->invalidate(true); // we must call layout directly
this->height += this->descriptionView->getHeight() + style->List.Item.descriptionSpacing;
}
// Thumbnail
if (this->thumbnailView)
{
Style* style = Application::getStyle();
unsigned thumbnailSize = height - style->List.Item.thumbnailPadding * 2;
this->thumbnailView->setBoundaries(
x + style->List.Item.thumbnailPadding,
y + style->List.Item.thumbnailPadding,
thumbnailSize,
thumbnailSize);
this->thumbnailView->invalidate();
}
}
void ListItem::getHighlightInsets(unsigned* top, unsigned* right, unsigned* bottom, unsigned* left)
{
Style* style = Application::getStyle();
View::getHighlightInsets(top, right, bottom, left);
if (descriptionView)
*bottom = -(descriptionView->getHeight() + style->List.Item.descriptionSpacing);
if (indented)
*left = -style->List.Item.indent;
}
void ListItem::resetValueAnimation()
{
this->valueAnimation = 0.0f;
menu_animation_ctx_tag tag = (uintptr_t) & this->valueAnimation;
menu_animation_kill_by_tag(&tag);
}
void ListItem::setValue(std::string value, bool faint, bool animate)
{
this->oldValue = this->value;
this->oldValueFaint = this->valueFaint;
this->value = value;
this->valueFaint = faint;
this->resetValueAnimation();
if (animate && this->oldValue != "")
{
Style* style = Application::getStyle();
menu_animation_ctx_tag tag = (uintptr_t) & this->valueAnimation;
menu_animation_ctx_entry_t entry;
entry.cb = [this](void* userdata) { this->resetValueAnimation(); };
entry.duration = style->AnimationDuration.highlight;
entry.easing_enum = EASING_IN_OUT_QUAD;
entry.subject = &this->valueAnimation;
entry.tag = tag;
entry.target_value = 1.0f;
entry.tick = [](void* userdata) {};
entry.userdata = nullptr;
menu_animation_push(&entry);
}
}
std::string ListItem::getValue()
{
return this->value;
}
void ListItem::setDrawTopSeparator(bool draw)
{
this->drawTopSeparator = draw;
}
View* ListItem::getDefaultFocus()
{
if (this->collapseState != 1.0f)
return nullptr;
return this;
}
void ListItem::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, Style* style, FrameContext* ctx)
{
unsigned baseHeight = this->height;
bool hasSubLabel = this->subLabel != "";
bool hasThumbnail = this->thumbnailView;
unsigned leftPadding = hasThumbnail ? this->thumbnailView->getWidth() + style->List.Item.thumbnailPadding * 2 : style->List.Item.padding;
if (this->indented)
{
x += style->List.Item.indent;
width -= style->List.Item.indent;
}
// Description
if (this->descriptionView)
{
// Don't count description as part of list item
baseHeight -= this->descriptionView->getHeight() + style->List.Item.descriptionSpacing;
this->descriptionView->frame(ctx);
}
// Value
unsigned valueX = x + width - style->List.Item.padding;
unsigned valueY = y + (hasSubLabel ? baseHeight - baseHeight / 3 : baseHeight / 2);
nvgTextAlign(vg, NVG_ALIGN_RIGHT | (hasSubLabel ? NVG_ALIGN_TOP : NVG_ALIGN_MIDDLE));
nvgFontFaceId(vg, ctx->fontStash->regular);
if (this->valueAnimation != 0.0f)
{
//Old value
NVGcolor valueColor = a(this->oldValueFaint ? ctx->theme->listItemFaintValueColor : ctx->theme->listItemValueColor);
valueColor.a *= (1.0f - this->valueAnimation);
nvgFillColor(vg, valueColor);
nvgFontSize(vg, style->List.Item.valueSize * (1.0f - this->valueAnimation));
nvgBeginPath(vg);
nvgText(vg, valueX, valueY, this->oldValue.c_str(), nullptr);
//New value
valueColor = a(this->valueFaint ? ctx->theme->listItemFaintValueColor : ctx->theme->listItemValueColor);
valueColor.a *= this->valueAnimation;
nvgFillColor(vg, valueColor);
nvgFontSize(vg, style->List.Item.valueSize * this->valueAnimation);
nvgBeginPath(vg);
nvgText(vg, valueX, valueY, this->value.c_str(), nullptr);
}
else
{
nvgFillColor(vg, a(this->valueFaint ? ctx->theme->listItemFaintValueColor : ctx->theme->listItemValueColor));
nvgFontSize(vg, style->List.Item.valueSize);
nvgFontFaceId(vg, ctx->fontStash->regular);
nvgBeginPath(vg);
nvgText(vg, valueX, valueY, this->value.c_str(), nullptr);
}
// Checked marker
if (this->checked)
{
unsigned radius = style->List.Item.selectRadius;
unsigned centerX = x + width - radius - style->List.Item.padding;
unsigned centerY = y + baseHeight / 2;
float radiusf = (float)radius;
int thickness = roundf(radiusf * 0.10f);
// Background
nvgFillColor(vg, a(ctx->theme->listItemValueColor));
nvgBeginPath(vg);
nvgCircle(vg, centerX, centerY, radiusf);
nvgFill(vg);
// Check mark
nvgFillColor(vg, a(ctx->theme->backgroundColorRGB));
// Long stroke
nvgSave(vg);
nvgTranslate(vg, centerX, centerY);
nvgRotate(vg, -NVG_PI / 4.0f);
nvgBeginPath(vg);
nvgRect(vg, -(radiusf * 0.55f), 0, radiusf * 1.3f, thickness);
nvgFill(vg);
nvgRestore(vg);
// Short stroke
nvgSave(vg);
nvgTranslate(vg, centerX - (radiusf * 0.65f), centerY);
nvgRotate(vg, NVG_PI / 4.0f);
nvgBeginPath(vg);
nvgRect(vg, 0, -(thickness / 2), radiusf * 0.53f, thickness);
nvgFill(vg);
nvgRestore(vg);
}
// Label
nvgFillColor(vg, a(ctx->theme->textColor));
nvgFontSize(vg, this->textSize);
nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
nvgFontFaceId(vg, ctx->fontStash->regular);
nvgBeginPath(vg);
nvgText(vg, x + leftPadding, y + baseHeight / (hasSubLabel ? 3 : 2), this->label.c_str(), nullptr);
// Sub Label
if (hasSubLabel)
{
nvgFillColor(vg, a(ctx->theme->descriptionColor));
nvgFontSize(vg, style->Label.descriptionFontSize);
nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_TOP);
nvgFontFaceId(vg, ctx->fontStash->regular);
nvgBeginPath(vg);
nvgText(vg, x + leftPadding, y + baseHeight - baseHeight / 3, this->subLabel.c_str(), nullptr);
}
// Thumbnail
if (hasThumbnail)
this->thumbnailView->frame(ctx);
// Separators
// Offset by one to be hidden by highlight
nvgFillColor(vg, a(ctx->theme->listItemSeparatorColor));
// Top
if (this->drawTopSeparator)
{
nvgBeginPath(vg);
nvgRect(vg, x, y - 1, width, 1);
nvgFill(vg);
}
// Bottom
nvgBeginPath(vg);
nvgRect(vg, x, y + 1 + baseHeight, width, 1);
nvgFill(vg);
}
bool ListItem::hasDescription()
{
return this->descriptionView;
}
std::string ListItem::getLabel()
{
return this->label;
}
ListItem::~ListItem()
{
if (this->descriptionView)
delete this->descriptionView;
if (this->thumbnailView)
delete this->thumbnailView;
this->resetValueAnimation();
}
ToggleListItem::ToggleListItem(std::string label, bool initialValue, std::string description, std::string onValue, std::string offValue)
: ListItem(label, description)
, toggleState(initialValue)
, onValue(onValue)
, offValue(offValue)
{
this->updateValue();
}
void ToggleListItem::updateValue()
{
if (this->toggleState)
this->setValue(this->onValue, false);
else
this->setValue(this->offValue, true);
}
bool ToggleListItem::onClick()
{
this->toggleState = !this->toggleState;
this->updateValue();
ListItem::onClick();
return true;
}
bool ToggleListItem::getToggleState()
{
return this->toggleState;
}
InputListItem::InputListItem(std::string label, std::string initialValue, std::string helpText, std::string description, int maxInputLength)
: ListItem(label, description)
, helpText(helpText)
, maxInputLength(maxInputLength)
{
this->setValue(initialValue, false);
}
bool InputListItem::onClick()
{
Swkbd::openForText([&](std::string text) {
this->setValue(text, false);
},
this->helpText, "", this->maxInputLength, this->getValue());
ListItem::onClick();
return true;
}
IntegerInputListItem::IntegerInputListItem(std::string label, int initialValue, std::string helpText, std::string description, int maxInputLength)
: InputListItem(label, std::to_string(initialValue), helpText, description, maxInputLength)
{
}
bool IntegerInputListItem::onClick()
{
Swkbd::openForNumber([&](int number) {
this->setValue(std::to_string(number), false);
},
this->helpText, "", this->maxInputLength, this->getValue());
ListItem::onClick();
return true;
}
ListItemGroupSpacing::ListItemGroupSpacing(bool separator)
: Rectangle(nvgRGBA(0, 0, 0, 0))
{
ThemeValues* theme = Application::getThemeValues();
if (separator)
this->setColor(theme->listItemSeparatorColor);
}
SelectListItem::SelectListItem(std::string label, std::vector<std::string> values, unsigned selectedValue)
: ListItem(label, "")
, values(values)
, selectedValue(selectedValue)
{
this->setValue(values[selectedValue], false, false);
this->getClickEvent()->subscribe([this](View* view) {
ValueSelectedEvent::Callback valueCallback = [this](int result) {
if (result == -1)
return;
this->setValue(this->values[result], false, false);
this->selectedValue = result;
this->valueEvent.fire(result);
};
Dropdown::open(this->getLabel(), this->values, valueCallback, this->selectedValue);
});
}
void SelectListItem::setSelectedValue(unsigned value)
{
if (value >= 0 && value < this->values.size())
{
this->selectedValue = value;
this->setValue(this->values[value], false, false);
}
}
ValueSelectedEvent* SelectListItem::getValueSelectedEvent()
{
return &this->valueEvent;
}
List::List(size_t defaultFocus)
{
this->layout = new ListContentView(this, defaultFocus);
this->layout->setResize(true);
this->layout->setParent(this);
this->setContentView(this->layout);
}
// Wrapped BoxLayout methods
void List::addView(View* view, bool fill)
{
this->layout->addView(view, fill);
}
void List::setMargins(unsigned top, unsigned right, unsigned bottom, unsigned left)
{
this->layout->setMargins(
top,
right,
bottom,
left);
}
void List::setSpacing(unsigned spacing)
{
this->layout->setSpacing(spacing);
}
unsigned List::getSpacing()
{
return this->layout->getSpacing();
}
void List::setMarginBottom(unsigned bottom)
{
this->layout->setMarginBottom(bottom);
}
void List::customSpacing(View* current, View* next, int* spacing)
{
// Nothing to do by default
}
List::~List()
{
// ScrollView already deletes the content view
}
} // namespace brls

View File

@@ -0,0 +1,73 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019 natinusala
Copyright (C) 2019 p-sam
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <stdarg.h>
#include <stdio.h>
#include <borealis/logger.hpp>
namespace brls
{
static LogLevel g_logLevel = LogLevel::INFO;
void Logger::setLogLevel(LogLevel newLogLevel)
{
g_logLevel = newLogLevel;
}
void Logger::log(LogLevel logLevel, const char* prefix, const char* color, const char* format, va_list ap)
{
if (g_logLevel < logLevel)
return;
printf("\033%s[%s]\033[0m ", color, prefix);
vprintf(format, ap);
printf("\n");
#ifdef __MINGW32__
fflush(0);
#endif
}
void Logger::error(const char* format, ...)
{
va_list ap;
va_start(ap, format);
Logger::log(LogLevel::ERROR, "ERROR", "[0;31m", format, ap);
va_end(ap);
}
void Logger::info(const char* format, ...)
{
va_list ap;
va_start(ap, format);
Logger::log(LogLevel::INFO, "INFO", "[0;34m", format, ap);
va_end(ap);
}
void Logger::debug(const char* format, ...)
{
va_list ap;
va_start(ap, format);
Logger::log(LogLevel::DEBUG, "DEBUG", "[0;32m", format, ap);
va_end(ap);
}
} // namespace brls

View File

@@ -0,0 +1,48 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019 natinusala
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <borealis/material_icon.hpp>
namespace brls
{
MaterialIcon::MaterialIcon(std::string icon)
: icon(icon)
{
}
void MaterialIcon::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, Style* style, FrameContext* ctx)
{
NVGcolor color = a(ctx->theme->textColor);
nvgTextLineHeight(vg, 1.0f);
nvgFillColor(vg, color);
nvgFontSize(vg, height);
nvgFontFaceId(vg, ctx->fontStash->material);
nvgTextAlign(vg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
nvgBeginPath(vg);
nvgText(vg, this->middleX, this->middleY, this->icon.c_str(), nullptr);
}
void MaterialIcon::layout(NVGcontext* vg, Style* style, FontStash* stash)
{
this->middleX = this->getX() + this->getWidth() / 2;
this->middleY = this->getY() + this->getHeight() / 2;
}
}; // namespace brls

View File

@@ -0,0 +1,205 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019 natinusala
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <borealis/application.hpp>
#include <borealis/logger.hpp>
#include <borealis/notification_manager.hpp>
#include <cstring>
// TODO: add a timeout duration enum for longer notifications
namespace brls
{
NotificationManager::NotificationManager()
{
std::memset(this->notifications, 0, sizeof(Notification*) * BRLS_NOTIFICATIONS_MAX);
}
void NotificationManager::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, Style* style, FrameContext* ctx)
{
for (size_t i = 0; i < BRLS_NOTIFICATIONS_MAX; i++)
{
if (this->notifications[i])
{
Notification* notification = this->notifications[i];
float alpha = notification->getAlpha();
float translation = 0.0f;
if (alpha != 1.0f)
{
translation = (1.0f - alpha) * (float)style->Notification.slideAnimation;
nvgTranslate(vg, translation, 0);
}
this->notifications[i]->frame(ctx);
if (alpha != 1.0f)
nvgTranslate(vg, -translation, 0);
}
}
}
void NotificationManager::notify(std::string text)
{
// Find the first available notification slot
bool found = false;
size_t i = 0;
for (i = 0; i < BRLS_NOTIFICATIONS_MAX; i++)
{
if (!this->notifications[i])
{
found = true;
break;
}
}
if (!found)
{
brls::Logger::info("Discarding notification \"%s\"", text.c_str());
return;
}
// Create the notification
brls::Logger::info("Showing notification \"%s\"", text.c_str());
Notification* notification = new Notification(text);
notification->setParent(this);
notification->show([]() {});
// Timeout timer
menu_timer_ctx_entry_t entry;
entry.duration = Application::getStyle()->AnimationDuration.notificationTimeout;
entry.tick = [](void*) {};
entry.userdata = nullptr;
entry.cb = [this, notification, i](void* userdata) {
notification->hide([this, notification, i]() {
delete notification;
this->notifications[i] = nullptr;
});
};
menu_timer_start(&notification->timeoutTimer, &entry);
this->notifications[i] = notification;
// Layout the notification
this->layoutNotification(i);
}
void NotificationManager::layoutNotification(size_t index)
{
Notification* notification = this->notifications[index];
if (!notification)
return;
Style* style = Application::getStyle();
// Get the position of the last notification
Notification* lastNotification = nullptr;
for (size_t i = 0; i < index; i++)
{
if (this->notifications[i])
lastNotification = this->notifications[i];
}
unsigned y = lastNotification ? (lastNotification->getY() + lastNotification->getHeight()) : 0;
// Layout the notification
unsigned width = style->Notification.width;
notification->setBoundaries(
this->getX() + this->getWidth() - width,
this->getY() + y,
width,
0 // height is dynamic
);
notification->invalidate(); // TODO: call layout directly to fix posting multiple notifications in one frame
}
void NotificationManager::layout(NVGcontext* vg, Style* style, FontStash* stash)
{
for (size_t i = 0; i < BRLS_NOTIFICATIONS_MAX; i++)
{
if (this->notifications[i])
this->layoutNotification(i);
}
}
NotificationManager::~NotificationManager()
{
for (size_t i = 0; i < BRLS_NOTIFICATIONS_MAX; i++)
{
if (this->notifications[i])
delete this->notifications[i];
}
}
Notification::Notification(std::string text)
{
this->setBackground(Background::BACKDROP);
this->label = new Label(LabelStyle::NOTIFICATION, text, true);
label->setParent(this);
}
Notification::~Notification()
{
delete this->label;
}
void Notification::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, Style* style, FrameContext* ctx)
{
this->label->frame(ctx);
}
void Notification::layout(NVGcontext* vg, Style* style, FontStash* stash)
{
unsigned padding = style->Notification.padding;
unsigned fontSize = style->Label.notificationFontSize;
float lineHeight = style->Label.notificationLineHeight;
// Layout the label
this->label->setWidth(this->getWidth() - padding * 2);
this->label->setHeight(0); // height is dynamic
this->label->invalidate(true); // layout directly to update height
unsigned minLabelHeight = (unsigned int)(lineHeight * fontSize) + fontSize; // 2 lines
unsigned labelYAdvance = padding;
if (this->label->getHeight() < minLabelHeight)
{
labelYAdvance += (minLabelHeight - this->label->getHeight()) / 2;
}
this->label->setBoundaries(
this->getX() + padding,
this->getY() + labelYAdvance,
this->label->getWidth(),
this->label->getHeight());
// Update our own height
this->setHeight(std::max(
this->label->getHeight() + padding * 2,
minLabelHeight + padding * 2));
}
}; // namespace brls

View File

@@ -0,0 +1,163 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019 natinusala
Copyright (C) 2019 WerWolv
Copyright (C) 2019 p-sam
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <borealis/animations.hpp>
#include <borealis/application.hpp>
#include <borealis/logger.hpp>
#include <borealis/popup_frame.hpp>
namespace brls
{
PopupFrame::PopupFrame(std::string title, unsigned char* imageBuffer, size_t imageBufferSize, AppletFrame* contentView, std::string subTitleLeft, std::string subTitleRight)
: contentView(contentView)
{
if (this->contentView)
{
this->contentView->setParent(this);
this->contentView->setHeaderStyle(HeaderStyle::POPUP);
this->contentView->setTitle(title);
this->contentView->setSubtitle(subTitleLeft, subTitleRight);
this->contentView->setIcon(imageBuffer, imageBufferSize);
this->contentView->invalidate();
}
contentView->setAnimateHint(true);
this->registerAction("Back", Key::B, [this] { return this->onCancel(); });
}
PopupFrame::PopupFrame(std::string title, std::string imagePath, AppletFrame* contentView, std::string subTitleLeft, std::string subTitleRight)
: contentView(contentView)
{
if (this->contentView)
{
this->contentView->setParent(this);
this->contentView->setHeaderStyle(HeaderStyle::POPUP);
this->contentView->setTitle(title);
this->contentView->setSubtitle(subTitleLeft, subTitleRight);
this->contentView->setIcon(imagePath);
this->contentView->invalidate();
}
contentView->setAnimateHint(true);
this->registerAction("Back", Key::B, [this] { return this->onCancel(); });
}
PopupFrame::PopupFrame(std::string title, AppletFrame* contentView, std::string subTitleLeft, std::string subTitleRight)
: contentView(contentView)
{
if (this->contentView)
{
this->contentView->setParent(this);
this->contentView->setHeaderStyle(HeaderStyle::POPUP);
this->contentView->setTitle(title);
this->contentView->setSubtitle(subTitleLeft, subTitleRight);
this->contentView->invalidate();
}
contentView->setAnimateHint(true);
this->registerAction("Back", Key::B, [this] { return this->onCancel(); });
}
void PopupFrame::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, Style* style, FrameContext* ctx)
{
// Backdrop
nvgFillColor(vg, a(ctx->theme->dropdownBackgroundColor));
nvgBeginPath(vg);
nvgRect(vg, 0, y, width, height);
nvgFill(vg);
// Background
nvgFillColor(vg, a(ctx->theme->backgroundColorRGB));
nvgBeginPath(vg);
nvgRect(vg, style->PopupFrame.edgePadding, y, width - style->PopupFrame.edgePadding * 2, height);
nvgFill(vg);
// TODO: Shadow
// Content view
nvgSave(vg);
nvgScissor(vg, style->PopupFrame.edgePadding, 0, style->PopupFrame.contentWidth, height);
this->contentView->frame(ctx);
nvgRestore(vg);
}
bool PopupFrame::onCancel()
{
Application::popView();
return true;
}
unsigned PopupFrame::getShowAnimationDuration(ViewAnimation animation)
{
return View::getShowAnimationDuration(animation) / 2;
}
void PopupFrame::layout(NVGcontext* vg, Style* style, FontStash* stash)
{
this->contentView->setBoundaries(style->PopupFrame.edgePadding, 0, style->PopupFrame.contentWidth, this->getHeight());
this->contentView->invalidate();
}
View* PopupFrame::getDefaultFocus()
{
if (this->contentView)
return this->contentView->getDefaultFocus();
return nullptr;
}
void PopupFrame::open(std::string title, unsigned char* imageBuffer, size_t imageBufferSize, AppletFrame* contentView, std::string subTitleLeft, std::string subTitleRight)
{
PopupFrame* popupFrame = new PopupFrame(title, imageBuffer, imageBufferSize, contentView, subTitleLeft, subTitleRight);
Application::pushView(popupFrame);
}
void PopupFrame::open(std::string title, std::string imagePath, AppletFrame* contentView, std::string subTitleLeft, std::string subTitleRight)
{
PopupFrame* popupFrame = new PopupFrame(title, imagePath, contentView, subTitleLeft, subTitleRight);
Application::pushView(popupFrame);
}
void PopupFrame::open(std::string title, AppletFrame* contentView, std::string subTitleLeft, std::string subTitleRight)
{
PopupFrame* popupFrame = new PopupFrame(title, contentView, subTitleLeft, subTitleRight);
Application::pushView(popupFrame);
}
void PopupFrame::willAppear(bool resetState)
{
this->contentView->willAppear(resetState);
}
void PopupFrame::willDisappear(bool resetState)
{
this->contentView->willDisappear(resetState);
}
PopupFrame::~PopupFrame()
{
if (this->contentView)
delete this->contentView;
}
} // namespace brls

View File

@@ -0,0 +1,134 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019 Billy Laws
Copyright (C) 2019 p-sam
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <borealis/application.hpp>
#include <borealis/progress_display.hpp>
namespace brls
{
ProgressDisplay::ProgressDisplay(ProgressDisplayFlags progressFlags)
{
if (progressFlags & ProgressDisplayFlags::PERCENTAGE)
this->label = new Label(LabelStyle::DIALOG, "0%", false);
if (progressFlags & ProgressDisplayFlags::SPINNER)
this->spinner = new ProgressSpinner();
}
void ProgressDisplay::setProgress(int current, int max)
{
if (current > max)
return;
this->progressPercentage = ((current * 100) / max);
if (!this->label)
return;
std::string labelText = std::to_string((unsigned)this->progressPercentage);
labelText += "%";
this->label->setText(labelText);
}
void ProgressDisplay::layout(NVGcontext* vg, Style* style, FontStash* stash)
{
if (this->label)
{
this->label->setWidth(style->ProgressDisplay.percentageLabelWidth);
this->label->invalidate(true);
this->label->setBoundaries(
this->x + this->width - this->label->getWidth() / 2,
this->y + this->height / 2 - this->label->getHeight() / 2,
this->label->getWidth(),
this->label->getHeight());
}
if (this->spinner)
{
this->spinner->setWidth(this->height);
this->spinner->setHeight(this->height);
this->spinner->setBoundaries(
this->x,
this->y,
this->spinner->getWidth(),
this->spinner->getHeight());
}
}
void ProgressDisplay::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, Style* style, FrameContext* ctx)
{
unsigned progressBarWidth = width;
unsigned progressBarX = x;
if (this->label)
{
progressBarWidth -= this->label->getWidth();
this->label->frame(ctx);
}
if (this->spinner)
{
// 1.25 accounts for the extra width of the curve on sides of progress bar
progressBarWidth -= this->spinner->getWidth() * 1.25f;
progressBarX += this->spinner->getWidth() * 1.25f;
this->spinner->frame(ctx);
}
nvgBeginPath(vg);
nvgMoveTo(vg, progressBarX, y + height / 2);
nvgLineTo(vg, progressBarX + progressBarWidth, y + height / 2);
nvgStrokeColor(vg, a(ctx->theme->listItemSeparatorColor));
nvgStrokeWidth(vg, height / 3);
nvgLineCap(vg, NVG_ROUND);
nvgStroke(vg);
if (this->progressPercentage > 0.0f)
{
nvgBeginPath(vg);
nvgMoveTo(vg, progressBarX, y + height / 2);
nvgLineTo(vg, progressBarX + ((float)progressBarWidth * this->progressPercentage) / 100, y + height / 2);
nvgStrokeColor(vg, a(ctx->theme->listItemValueColor));
nvgStrokeWidth(vg, height / 3);
nvgLineCap(vg, NVG_ROUND);
nvgStroke(vg);
}
}
void ProgressDisplay::willAppear(bool resetState)
{
if (this->spinner)
this->spinner->willAppear(resetState);
}
void ProgressDisplay::willDisappear(bool resetState)
{
if (this->spinner)
this->spinner->willDisappear(resetState);
}
ProgressDisplay::~ProgressDisplay()
{
if (this->spinner)
delete this->spinner;
if (this->label)
delete this->label;
}
} // namespace brls

View File

@@ -0,0 +1,96 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019 Billy Laws
Copyright (C) 2019 p-sam
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <math.h>
#include <borealis/animations.hpp>
#include <borealis/application.hpp>
#include <borealis/progress_spinner.hpp>
namespace brls
{
ProgressSpinner::ProgressSpinner() {}
void ProgressSpinner::restartAnimation()
{
Style* style = Application::getStyle();
menu_animation_ctx_tag tag = (uintptr_t) & this->animationValue;
menu_animation_ctx_entry_t entry;
this->animationValue = 0.0f;
menu_animation_kill_by_tag(&tag);
entry.cb = [this](void* userdata) { this->restartAnimation(); };
entry.duration = style->AnimationDuration.progress;
entry.easing_enum = EASING_LINEAR;
entry.subject = &this->animationValue;
entry.tag = tag;
entry.target_value = 8.0f;
entry.tick = [](void* userdata) {};
entry.userdata = nullptr;
menu_animation_push(&entry);
}
void ProgressSpinner::layout(NVGcontext* vg, Style* style, FontStash* stash)
{
this->height = std::min(this->width, this->height);
this->width = this->height;
}
void ProgressSpinner::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, Style* style, FrameContext* ctx)
{
NVGcolor barColor = a(ctx->theme->spinnerBarColor);
// Each bar of the spinner
for (int i = 0 + animationValue; i < 8 + animationValue; i++)
{
barColor.a = fmax((i - animationValue) / 8.0f, a(ctx->theme->spinnerBarColor).a);
nvgSave(vg);
nvgTranslate(vg, x + width / 2, y + height / 2);
nvgRotate(vg, nvgDegToRad(i * 45)); // Internal angle of octagon
nvgBeginPath(vg);
nvgMoveTo(vg, height * style->ProgressSpinner.centerGapMultiplier, 0);
nvgLineTo(vg, height / 2 - height * style->ProgressSpinner.centerGapMultiplier, 0);
nvgStrokeColor(vg, barColor);
nvgStrokeWidth(vg, height * style->ProgressSpinner.barWidthMultiplier);
nvgLineCap(vg, NVG_SQUARE);
nvgStroke(vg);
nvgRestore(vg);
}
}
void ProgressSpinner::willAppear(bool resetState)
{
this->restartAnimation();
}
void ProgressSpinner::willDisappear(bool resetState)
{
menu_animation_ctx_tag tag = (uintptr_t) & this->animationValue;
this->animationValue = 0.0f;
menu_animation_kill_by_tag(&tag);
}
} // namespace brls

View File

@@ -0,0 +1,55 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019 natinusala
Copyright (C) 2019 p-sam
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <borealis/logger.hpp>
#include <borealis/rectangle.hpp>
namespace brls
{
Rectangle::Rectangle(NVGcolor color)
{
this->setColor(color);
}
void Rectangle::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, Style* style, FrameContext* ctx)
{
NVGcolor color = a(this->color);
if (color.a == 0.0f)
return;
nvgFillColor(vg, color);
nvgBeginPath(vg);
nvgRect(vg, x, y, width, height);
nvgFill(vg);
}
void Rectangle::setColor(NVGcolor color)
{
this->color = color;
}
void Rectangle::layout(NVGcontext* vg, Style* style, FontStash* stash)
{
// Nothing to do
}
} // namespace brls

View File

@@ -0,0 +1,88 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019 natinusala
Copyright (C) 2019 p-sam
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <borealis/application.hpp>
#include <borealis/repeating_task.hpp>
namespace brls
{
RepeatingTask::RepeatingTask(retro_time_t interval)
: interval(interval)
{
Application::getTaskManager()->registerRepeatingTask(this);
}
void RepeatingTask::run(retro_time_t currentTime)
{
this->lastRun = currentTime;
}
void RepeatingTask::start()
{
this->onStart();
this->running = true;
}
void RepeatingTask::pause()
{
this->running = false;
}
void RepeatingTask::stop()
{
this->pause();
this->stopRequested = true;
}
void RepeatingTask::fireNow()
{
if (!this->isRunning())
return;
retro_time_t currentTime = cpu_features_get_time_usec() / 1000;
this->run(currentTime);
}
retro_time_t RepeatingTask::getInterval()
{
return this->interval;
}
retro_time_t RepeatingTask::getLastRun()
{
return this->lastRun;
}
bool RepeatingTask::isRunning()
{
return this->running;
}
bool RepeatingTask::isStopRequested()
{
return this->stopRequested;
}
RepeatingTask::~RepeatingTask()
{
// Nothing to do here, only the Task Manager is supposed to free tasks
}
} // namespace brls

View File

@@ -0,0 +1,236 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2020 natinusala
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <math.h>
#include <borealis/application.hpp>
#include <borealis/scroll_view.hpp>
namespace brls
{
void ScrollView::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, Style* style, FrameContext* ctx)
{
if (!this->contentView)
return;
// Update scrolling if needed - try until it works
if (this->updateScrollingOnNextFrame && this->updateScrolling(false))
this->updateScrollingOnNextFrame = false;
// Enable scissoring
nvgSave(vg);
nvgScissor(vg, x, y, this->width, this->height);
// Draw content view
this->contentView->frame(ctx);
//Disable scissoring
nvgRestore(vg);
}
unsigned ScrollView::getYCenter(View* view)
{
return view->getY() + view->getHeight() / 2;
}
void ScrollView::layout(NVGcontext* vg, Style* style, FontStash* stash)
{
this->prebakeScrolling();
// Update scrolling if needed
if (this->updateScrollingOnNextLayout)
{
this->updateScrollingOnNextLayout = false;
this->updateScrolling(false);
}
// Layout content view
if (this->contentView)
{
unsigned contentHeight = this->contentView->getHeight();
this->contentView->setBoundaries(
this->getX(),
this->getY() - roundf(this->scrollY * (float)contentHeight),
this->getWidth(),
contentHeight);
this->contentView->invalidate();
}
this->ready = true;
}
void ScrollView::willAppear(bool resetState)
{
this->prebakeScrolling();
// First scroll all the way to the top
// then wait for the first frame to scroll
// to the selected view if needed (only known then)
if (resetState)
{
this->startScrolling(false, 0.0f);
this->updateScrollingOnNextFrame = true; // focus may have changed since
}
if (this->contentView)
this->contentView->willAppear(resetState);
}
void ScrollView::willDisappear(bool resetState)
{
// Send event to content view
if (this->contentView)
this->contentView->willDisappear(resetState);
}
View* ScrollView::getDefaultFocus()
{
return this->contentView;
}
void ScrollView::setContentView(View* view)
{
this->contentView = view;
if (this->contentView)
{
this->contentView->setParent(this);
this->contentView->willAppear(true);
}
this->invalidate();
}
View* ScrollView::getContentView()
{
return this->contentView;
}
void ScrollView::prebakeScrolling()
{
// Prebaked values for scrolling
this->middleY = this->y + this->height / 2;
this->bottomY = this->y + this->height;
}
bool ScrollView::updateScrolling(bool animated)
{
// Don't scroll if layout hasn't been called yet
if (!this->ready || !this->contentView)
return false;
float contentHeight = (float)this->contentView->getHeight();
// Ensure content is laid out too
if (contentHeight == 0)
return false;
View* focusedView = Application::getCurrentFocus();
int currentSelectionMiddleOnScreen = focusedView->getY() + focusedView->getHeight() / 2;
float newScroll = -(this->scrollY * contentHeight) - ((float)currentSelectionMiddleOnScreen - (float)this->middleY);
// Bottom boundary
if ((float)this->y + newScroll + contentHeight < (float)this->bottomY)
newScroll = (float)this->height - contentHeight;
// Top boundary
if (newScroll > 0.0f)
newScroll = 0.0f;
// Apply 0.0f -> 1.0f scale
newScroll = abs(newScroll) / contentHeight;
//Start animation
this->startScrolling(animated, newScroll);
return true;
}
void ScrollView::startScrolling(bool animated, float newScroll)
{
if (newScroll == this->scrollY)
return;
menu_animation_ctx_tag tag = (uintptr_t) & this->scrollY;
menu_animation_kill_by_tag(&tag);
if (animated)
{
Style* style = Application::getStyle();
menu_animation_ctx_entry_t entry;
entry.cb = [](void* userdata) {};
entry.duration = style->AnimationDuration.highlight;
entry.easing_enum = EASING_OUT_QUAD;
entry.subject = &this->scrollY;
entry.tag = tag;
entry.target_value = newScroll;
entry.tick = [this](void* userdata) { this->scrollAnimationTick(); };
entry.userdata = nullptr;
menu_animation_push(&entry);
}
else
{
this->scrollY = newScroll;
}
this->invalidate(!animated); // layout immediately if not animated
}
void ScrollView::scrollAnimationTick()
{
this->invalidate();
}
void ScrollView::onChildFocusGained(View* child)
{
// layout hasn't been called yet, don't scroll
if (!this->ready)
return;
// Safety check to ensure that we don't have
// two children (setContentView called twice)
if (child != this->contentView)
return;
// Start scrolling
this->updateScrolling(true);
View::onChildFocusGained(child);
}
void ScrollView::onWindowSizeChanged()
{
this->updateScrollingOnNextLayout = true;
if (this->contentView)
this->contentView->onWindowSizeChanged();
}
ScrollView::~ScrollView()
{
if (this->contentView)
{
this->contentView->willDisappear(true);
delete this->contentView;
}
}
} // namespace brls

View File

@@ -0,0 +1,172 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019 natinusala
Copyright (C) 2019 WerWolv
Copyright (C) 2019 p-sam
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <borealis/application.hpp>
#include <borealis/sidebar.hpp>
namespace brls
{
Sidebar::Sidebar()
: BoxLayout(BoxLayoutOrientation::VERTICAL)
{
Style* style = Application::getStyle();
this->setWidth(style->Sidebar.width);
this->setSpacing(style->Sidebar.spacing);
this->setMargins(style->Sidebar.marginTop, style->Sidebar.marginRight, style->Sidebar.marginBottom, style->Sidebar.marginLeft);
this->setBackground(Background::SIDEBAR);
}
View* Sidebar::getDefaultFocus()
{
// Sanity check
if (this->lastFocus >= this->children.size())
this->lastFocus = 0;
// Try to focus last focused one
View* toFocus = this->children[this->lastFocus]->view->getDefaultFocus();
if (toFocus)
return toFocus;
// Otherwise just get the first available item
return BoxLayout::getDefaultFocus();
}
void Sidebar::onChildFocusGained(View* child)
{
size_t position = *((size_t*)child->getParentUserData());
this->lastFocus = position;
BoxLayout::onChildFocusGained(child);
}
SidebarItem* Sidebar::addItem(std::string label, View* view)
{
SidebarItem* item = new SidebarItem(label, this);
item->setAssociatedView(view);
if (this->isEmpty())
setActive(item);
this->addView(item);
return item;
}
void Sidebar::addSeparator()
{
SidebarSeparator* separator = new SidebarSeparator();
this->addView(separator);
}
void Sidebar::setActive(SidebarItem* active)
{
if (currentActive)
currentActive->setActive(false);
currentActive = active;
active->setActive(true);
}
SidebarItem::SidebarItem(std::string label, Sidebar* sidebar)
: label(label)
, sidebar(sidebar)
{
Style* style = Application::getStyle();
this->setHeight(style->Sidebar.Item.height);
this->registerAction("OK", Key::A, [this] { return this->onClick(); });
}
void SidebarItem::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, Style* style, FrameContext* ctx)
{
// Label
nvgFillColor(vg, a(this->active ? ctx->theme->activeTabColor : ctx->theme->textColor));
nvgFontSize(vg, style->Sidebar.Item.textSize);
nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
nvgFontFaceId(vg, ctx->fontStash->regular);
nvgBeginPath(vg);
nvgText(vg, x + style->Sidebar.Item.textOffsetX + style->Sidebar.Item.padding, y + height / 2, this->label.c_str(), nullptr);
// Active marker
if (this->active)
{
nvgFillColor(vg, a(ctx->theme->activeTabColor));
nvgBeginPath(vg);
nvgRect(vg, x + style->Sidebar.Item.padding, y + style->Sidebar.Item.padding, style->Sidebar.Item.activeMarkerWidth, style->Sidebar.Item.height - style->Sidebar.Item.padding * 2);
nvgFill(vg);
}
}
bool SidebarItem::onClick()
{
Application::onGamepadButtonPressed(GLFW_GAMEPAD_BUTTON_DPAD_RIGHT, false);
return true;
}
void SidebarItem::setActive(bool active)
{
this->active = active;
}
SidebarSeparator::SidebarSeparator()
{
Style* style = Application::getStyle();
this->setHeight(style->Sidebar.Separator.height);
}
void SidebarSeparator::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, Style* style, FrameContext* ctx)
{
nvgFillColor(vg, a(ctx->theme->sidebarSeparatorColor));
nvgBeginPath(vg);
nvgRect(vg, x, y + height / 2, width, 1);
nvgFill(vg);
}
void SidebarItem::setAssociatedView(View* view)
{
this->associatedView = view;
}
bool SidebarItem::isActive()
{
return this->active;
}
void SidebarItem::onFocusGained()
{
this->sidebar->setActive(this);
View::onFocusGained();
}
View* SidebarItem::getAssociatedView()
{
return this->associatedView;
}
SidebarItem::~SidebarItem()
{
if (this->associatedView)
delete this->associatedView;
}
} // namespace brls

View File

@@ -0,0 +1,136 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019 Billy Laws
Copyright (C) 2019 p-sam
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <borealis/application.hpp>
#include <borealis/logger.hpp>
#include <borealis/staged_applet_frame.hpp>
#include <borealis/style.hpp>
#include <borealis/theme.hpp>
namespace brls
{
StagedAppletFrame::StagedAppletFrame()
: AppletFrame(true, true)
{
}
void StagedAppletFrame::addStage(View* view)
{
stageViews.push_back(view);
if (!currentStage)
enterStage(0, false);
}
void StagedAppletFrame::nextStage()
{
enterStage((int)currentStage + 1, true);
}
void StagedAppletFrame::previousStage()
{
enterStage((int)currentStage - 1, true);
}
// TODO: Add animation to bullets when changing stage
// TODO: Add left/right and right/left animations for views when changing stage (use show/hide with the animation)
void StagedAppletFrame::enterStage(int index, bool requestFocus)
{
if ((size_t)index >= stageViews.size())
return;
if (this->hasContentView())
stageViews[currentStage]->willDisappear(true);
currentStage = (size_t)index;
this->setContentView(stageViews[currentStage]); // calls willAppear
if (requestFocus)
Application::giveFocus(this->getDefaultFocus());
}
void StagedAppletFrame::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, Style* style, FrameContext* ctx)
{
AppletFrame::draw(vg, x, y, width, height, style, ctx);
if (stageViews.empty())
return;
for (size_t i = 0; i < stageViews.size(); i++)
{
bool current = (i == (stageViews.size() - 1) - currentStage);
// Border color for next stages
NVGcolor borderColor = ctx->theme->nextStageBulletColor;
nvgBeginPath(vg);
nvgCircle(vg, x + width - style->AppletFrame.separatorSpacing * (style->StagedAppletFrame.progressIndicatorSpacing + i) + style->AppletFrame.separatorSpacing + style->AppletFrame.separatorSpacing / 2,
y + style->AppletFrame.headerHeightRegular / 2, current ? style->StagedAppletFrame.progressIndicatorRadiusSelected : style->StagedAppletFrame.progressIndicatorRadiusUnselected);
// Current stage
if (current)
{
nvgFillColor(vg, a(ctx->theme->listItemValueColor));
nvgFill(vg);
}
// Previous stages
else if (i > (stageViews.size() - 1) - currentStage)
{
borderColor = ctx->theme->separatorColor;
nvgFillColor(vg, a(ctx->theme->textColor));
nvgFill(vg);
}
if (!current)
{
nvgStrokeColor(vg, a(borderColor));
nvgStrokeWidth(vg, style->StagedAppletFrame.progressIndicatorBorderWidth);
nvgStroke(vg);
}
}
}
unsigned StagedAppletFrame::getStagesCount()
{
return this->stageViews.size();
}
unsigned StagedAppletFrame::getCurrentStage()
{
return this->currentStage;
}
unsigned StagedAppletFrame::isLastStage()
{
return this->currentStage == this->stageViews.size() - 1;
}
StagedAppletFrame::~StagedAppletFrame()
{
for (View* view : this->stageViews)
delete view;
this->stageViews.clear();
this->setContentView(nullptr); // so that ~AppletFrame() doesn't free it twice
}
} // namespace brls

View File

@@ -0,0 +1,270 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019 natinusala
Copyright (C) 2019 WerWolv
Copyright (C) 2019 p-sam
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <borealis/style.hpp>
namespace brls
{
Style Style::horizon()
{
Style style = Style();
style.AppletFrame = {
.headerHeightRegular = 88,
.headerHeightPopup = 129,
.footerHeight = 73,
.imageLeftPadding = 64,
.imageTopPadding = 20,
.imageSize = 52,
.separatorSpacing = 30,
.titleSize = 28,
.titleStart = 130,
.titleOffset = 5,
.footerTextSize = 22,
.footerTextSpacing = 30,
.slideAnimation = 20
};
style.Highlight = {
.strokeWidth = 5,
.cornerRadius = 0.5f,
.shadowWidth = 2,
.shadowOffset = 10,
.shadowFeather = 10,
.shadowOpacity = 128
};
style.Background = {
.sidebarBorderHeight = 16
};
style.Sidebar = {
.width = 410,
.spacing = 0,
.marginLeft = 88,
.marginRight = 30,
.marginTop = 40,
.marginBottom = 40,
.Item = {
.height = 70,
.textSize = 22,
.padding = 9,
.textOffsetX = 14,
.activeMarkerWidth = 4,
},
.Separator = { .height = 28 }
};
style.List = {
.marginLeftRight = 60,
.marginTopBottom = 42,
.spacing = 61,
.Item = {
.height = 69, // offset by 1 to have the highlight hide the separator
.heightWithSubLabel = 99,
.valueSize = 20,
.padding = 15,
.thumbnailPadding = 11,
.descriptionIndent = 20,
.descriptionSpacing = 16,
.indent = 40,
.selectRadius = 15 }
};
style.Label = {
.regularFontSize = 20,
.mediumFontSize = 18,
.smallFontSize = 16,
.descriptionFontSize = 16,
.crashFontSize = 24,
.buttonFontSize = 24,
.listItemFontSize = 24,
.notificationFontSize = 18,
.dialogFontSize = 24,
.hintFontSize = 22,
.lineHeight = 1.65f,
.notificationLineHeight = 1.35f
};
style.CrashFrame = {
.labelWidth = 0.60f,
.boxStrokeWidth = 5,
.boxSize = 64,
.boxSpacing = 90,
.buttonWidth = 356,
.buttonHeight = 60,
.buttonSpacing = 47
};
style.Button = {
.cornerRadius = 5.0f,
.highlightInset = 2,
.shadowWidth = 2.0f,
.shadowFeather = 10.0f,
.shadowOpacity = 63.75f,
.shadowOffset = 10.0f
};
style.TableRow = {
.headerHeight = 60,
.headerTextSize = 22,
.bodyHeight = 38,
.bodyIndent = 40,
.bodyTextSize = 18,
.padding = 15
};
style.Dropdown = {
.listWidth = 720,
.listPadding = 40,
.listItemHeight = 60,
.listItemTextSize = 20,
.headerHeight = 71,
.headerFontSize = 24,
.headerPadding = 70
};
style.PopupFrame = {
.edgePadding = 120,
.separatorSpacing = 30,
.footerHeight = 73,
.imageLeftPadding = 60,
.imageTopPadding = 17,
.imageSize = 100,
.contentWidth = 1040,
.contentHeight = 518,
.headerTextLeftPadding = 180,
.headerTextTopPadding = 64,
.subTitleLeftPadding = 182,
.subTitleTopPadding = 95,
.subTitleSpacing = 20,
.subTitleSeparatorLeftPadding = 280,
.subTitleSeparatorTopPadding = 92,
.subTitleSeparatorHeight = 20,
.headerFontSize = 28,
.subTitleFontSize = 16
};
style.StagedAppletFrame = {
.progressIndicatorSpacing = 4,
.progressIndicatorRadiusUnselected = 5 - 1, // minus half of border width
.progressIndicatorRadiusSelected = 8,
.progressIndicatorBorderWidth = 2
};
style.ProgressSpinner = {
.centerGapMultiplier = 0.2f,
.barWidthMultiplier = 0.06f
};
style.ProgressDisplay = {
.percentageLabelWidth = 70
};
style.Header = {
.height = 44,
.padding = 11,
.rectangleWidth = 5,
.fontSize = 18
};
style.FramerateCounter = {
.width = 125,
.height = 26
};
style.ThumbnailSidebar = {
.marginLeftRight = 109, // used for the image only = (410 - 192) / 2, image size is 192*192 with a 410px wide sidebar
.marginTopBottom = 47,
.buttonHeight = 70,
.buttonMargin = 60
};
style.AnimationDuration = {
.show = 250,
.showSlide = 125,
.highlight = 100,
.shake = 15,
.collapse = 100,
.progress = 1000,
.notificationTimeout = 4000
};
style.Notification = {
.width = 280,
.padding = 16,
.slideAnimation = 40
};
style.Dialog = {
.width = 770,
.height = 220,
.paddingTopBottom = 65,
.paddingLeftRight = 115,
.cornerRadius = 5.0f,
.buttonHeight = 72,
.buttonSeparatorHeight = 2,
.shadowWidth = 2.0f,
.shadowFeather = 10.0f,
.shadowOpacity = 63.75f,
.shadowOffset = 10.0f
};
return style;
}
} // namespace brls

View File

@@ -0,0 +1,43 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019 natinusala
Copyright (C) 2019 WerWolv
Copyright (C) 2019 p-sam
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <switch.h>
#include <unistd.h>
static int nxlink_sock = -1;
void userAppInit()
{
romfsInit();
socketInitializeDefault();
nxlink_sock = nxlinkStdio();
plInitialize(PlServiceType_User);
setsysInitialize();
}
void userAppExit()
{
if (nxlink_sock != -1)
close(nxlink_sock);
socketExit();
romfsExit();
plExit();
setsysExit();
}

View File

@@ -0,0 +1,126 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019 WerWolv
Copyright (C) 2019 p-sam
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <borealis/logger.hpp>
#include <borealis/swkbd.hpp>
#include <cstring>
#include <iostream>
#ifdef __SWITCH__
#include <switch.h>
#endif
namespace brls
{
#ifdef __SWITCH__
static SwkbdConfig createSwkbdBaseConfig(std::string headerText, std::string subText, int maxStringLength, std::string initialText)
{
SwkbdConfig config;
swkbdCreate(&config, 0);
swkbdConfigMakePresetDefault(&config);
swkbdConfigSetHeaderText(&config, headerText.c_str());
swkbdConfigSetSubText(&config, subText.c_str());
swkbdConfigSetStringLenMax(&config, maxStringLength);
swkbdConfigSetInitialText(&config, initialText.c_str());
swkbdConfigSetStringLenMin(&config, 1);
swkbdConfigSetBlurBackground(&config, true);
return config;
}
#else
static std::string terminalInput(std::string text)
{
printf("\033[0;94m[INPUT] \033[0;36m%s\033[0m: ", text.c_str());
std::string line;
std::getline(std::cin, line);
return line;
}
#endif
bool Swkbd::openForText(std::function<void(std::string)> f, std::string headerText, std::string subText, int maxStringLength, std::string initialText)
{
#ifdef __SWITCH__
SwkbdConfig config = createSwkbdBaseConfig(headerText, subText, maxStringLength, initialText);
swkbdConfigSetType(&config, SwkbdType_Normal);
swkbdConfigSetKeySetDisableBitmask(&config, SwkbdKeyDisableBitmask_At | SwkbdKeyDisableBitmask_Percent | SwkbdKeyDisableBitmask_ForwardSlash | SwkbdKeyDisableBitmask_Backslash);
char buffer[0x100];
if (R_SUCCEEDED(swkbdShow(&config, buffer, 0x100)) && strcmp(buffer, "") != 0)
{
f(buffer);
swkbdClose(&config);
return true;
}
swkbdClose(&config);
return false;
#else
std::string line = terminalInput(headerText);
f(line);
return true;
#endif
}
bool Swkbd::openForNumber(std::function<void(int)> f, std::string headerText, std::string subText, int maxStringLength, std::string initialText, std::string leftButton, std::string rightButton)
{
#ifdef __SWITCH__
SwkbdConfig config = createSwkbdBaseConfig(headerText, subText, maxStringLength, initialText);
swkbdConfigSetType(&config, SwkbdType_NumPad);
swkbdConfigSetLeftOptionalSymbolKey(&config, leftButton.c_str());
swkbdConfigSetRightOptionalSymbolKey(&config, rightButton.c_str());
swkbdConfigSetKeySetDisableBitmask(&config, SwkbdKeyDisableBitmask_At | SwkbdKeyDisableBitmask_Percent | SwkbdKeyDisableBitmask_ForwardSlash | SwkbdKeyDisableBitmask_Backslash);
char buffer[0x100];
if (R_SUCCEEDED(swkbdShow(&config, buffer, 0x100)) && strcmp(buffer, "") != 0)
{
f(std::stol(buffer));
swkbdClose(&config);
return true;
}
swkbdClose(&config);
return false;
#else
std::string line = terminalInput(headerText);
try
{
f(stol(line));
return true;
}
catch (const std::exception& e)
{
Logger::error("Could not parse input, did you enter a valid integer?");
return false;
}
#endif
}
} // namespace brls

View File

@@ -0,0 +1,117 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019-2020 natinusala
Copyright (C) 2019 WerWolv
Copyright (C) 2019 p-sam
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <borealis/application.hpp>
#include <borealis/box_layout.hpp>
#include <borealis/rectangle.hpp>
#include <borealis/sidebar.hpp>
#include <borealis/tab_frame.hpp>
namespace brls
{
TabFrame::TabFrame()
: AppletFrame(false, true)
{
//Create sidebar
this->sidebar = new Sidebar();
// Setup content view
this->layout = new BoxLayout(BoxLayoutOrientation::HORIZONTAL);
layout->addView(sidebar);
this->setContentView(layout);
}
bool TabFrame::onCancel()
{
// Go back to sidebar if not already focused
if (!this->sidebar->isChildFocused())
{
Application::onGamepadButtonPressed(GLFW_GAMEPAD_BUTTON_DPAD_LEFT, false);
return true;
}
return AppletFrame::onCancel();
}
void TabFrame::switchToView(View* view)
{
if (this->rightPane == view)
return;
if (this->layout->getViewsCount() > 1)
{
if (this->rightPane)
this->rightPane->willDisappear(true);
this->layout->removeView(1, false);
}
if (view != nullptr)
{
this->layout->addView(view, true, true); // addView() calls willAppear()
this->rightPane = view;
}
}
void TabFrame::addTab(std::string label, View* view)
{
SidebarItem* item = this->sidebar->addItem(label, view);
item->getFocusEvent()->subscribe([this](View* view) {
if (SidebarItem* item = dynamic_cast<SidebarItem*>(view))
this->switchToView(item->getAssociatedView());
});
// Switch to first one as soon as we add it
if (!this->rightPane)
{
Logger::debug("Switching to the first tab");
this->switchToView(view);
}
}
void TabFrame::addSeparator()
{
this->sidebar->addSeparator();
}
View* TabFrame::getDefaultFocus()
{
// Try to focus the right pane
if (this->layout->getViewsCount() > 1)
{
View* newFocus = this->rightPane->getDefaultFocus();
if (newFocus)
return newFocus;
}
// Otherwise focus sidebar
return this->sidebar->getDefaultFocus();
}
TabFrame::~TabFrame()
{
switchToView(nullptr);
// Content view is freed by ~AppletFrame()
}
} // namespace brls

View File

@@ -0,0 +1,147 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019 natinusala
Copyright (C) 2019 p-sam
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <borealis/table.hpp>
namespace brls
{
TableRow* Table::addRow(TableRowType type, std::string label, std::string value)
{
TableRow* row = new TableRow(type, label, value);
this->rows.push_back(row);
return row;
}
void Table::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, Style* style, FrameContext* ctx)
{
ThemeValues* theme = ctx->theme;
unsigned yAdvance = 0;
for (size_t i = 0; i < this->rows.size(); i++)
{
bool even = i % 2 == 0;
TableRow* row = this->rows[i];
// Get appearance
unsigned indent = 0;
unsigned height = 0;
unsigned fontSize = 0;
NVGcolor backgroundColor;
NVGcolor textColor;
switch (row->getType())
{
case TableRowType::HEADER:
indent = 0;
height = style->TableRow.headerHeight;
textColor = theme->textColor;
fontSize = style->TableRow.headerTextSize;
break;
case TableRowType::BODY:
indent = style->TableRow.bodyIndent;
height = style->TableRow.bodyHeight;
textColor = theme->tableBodyTextColor;
fontSize = style->TableRow.bodyTextSize;
break;
}
backgroundColor = even ? theme->tableEvenBackgroundColor : transparent;
// Background
nvgFillColor(vg, a(backgroundColor));
nvgBeginPath(vg);
nvgRect(vg, x + indent, y + yAdvance, width - indent, height);
nvgFill(vg);
// Text
nvgFillColor(vg, a(textColor));
nvgFontFaceId(vg, ctx->fontStash->regular);
nvgFontSize(vg, fontSize);
// Label
nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
nvgBeginPath(vg);
nvgText(vg, x + indent + style->TableRow.padding, y + yAdvance + height / 2, row->getLabel()->c_str(), nullptr);
// Value
nvgTextAlign(vg, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE);
nvgBeginPath(vg);
nvgText(vg, x + width - style->TableRow.padding, y + yAdvance + height / 2, row->getValue()->c_str(), nullptr);
yAdvance += height;
}
}
void Table::layout(NVGcontext* vg, Style* style, FontStash* stash)
{
unsigned height = 0;
for (TableRow* row : this->rows)
{
switch (row->getType())
{
case TableRowType::HEADER:
height += style->TableRow.headerHeight;
break;
case TableRowType::BODY:
height += style->TableRow.bodyHeight;
break;
}
}
this->setHeight(height);
}
Table::~Table()
{
for (TableRow* row : this->rows)
{
delete row;
}
}
TableRow::TableRow(TableRowType type, std::string label, std::string value)
: type(type)
, label(label)
, value(value)
{
}
std::string* TableRow::getLabel()
{
return &this->label;
}
std::string* TableRow::getValue()
{
return &this->value;
}
void TableRow::setValue(std::string value)
{
this->value = value;
}
TableRowType TableRow::getType()
{
return this->type;
}
} // namespace brls

View File

@@ -0,0 +1,69 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019 natinusala
Copyright (C) 2019 p-sam
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <features/features_cpu.h>
#include <borealis/task_manager.hpp>
namespace brls
{
void TaskManager::frame()
{
// Repeating tasks
retro_time_t currentTime = cpu_features_get_time_usec() / 1000;
for (auto i = this->repeatingTasks.begin(); i != this->repeatingTasks.end(); i++)
{
RepeatingTask* task = *i;
// Stop the task if needed
if (task->isStopRequested())
{
this->stopRepeatingTask(task);
this->repeatingTasks.erase(i--);
}
// Fire it
else if (task->isRunning() && currentTime - task->getLastRun() > task->getInterval())
{
task->run(currentTime);
}
}
}
void TaskManager::registerRepeatingTask(RepeatingTask* task)
{
this->repeatingTasks.push_back(task);
}
void TaskManager::stopRepeatingTask(RepeatingTask* task)
{
task->onStop();
delete task;
}
TaskManager::~TaskManager()
{
// Stop all repeating tasks
for (RepeatingTask* task : this->repeatingTasks)
this->stopRepeatingTask(task);
this->repeatingTasks.clear();
}
} // namespace brls

View File

@@ -0,0 +1,130 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019 natinusala
Copyright (C) 2019 p-sam
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <borealis/theme.hpp>
#define DARK theme.colors[ThemeVariant_DARK]
#define LIGHT theme.colors[ThemeVariant_LIGHT]
namespace brls
{
// Default colors
Theme Theme::horizon()
{
Theme theme = Theme();
// Light variant
LIGHT.backgroundColor[0] = 0.922f;
LIGHT.backgroundColor[1] = 0.922f;
LIGHT.backgroundColor[2] = 0.922f;
LIGHT.backgroundColorRGB = nvgRGB(235, 235, 235);
LIGHT.textColor = nvgRGB(51, 51, 51);
LIGHT.descriptionColor = nvgRGB(140, 140, 140);
LIGHT.notificationTextColor = nvgRGB(255, 255, 255);
LIGHT.backdropColor = nvgRGBA(0, 0, 0, 178);
LIGHT.separatorColor = nvgRGB(45, 45, 45);
LIGHT.sidebarColor = nvgRGB(240, 240, 240);
LIGHT.activeTabColor = nvgRGB(49, 79, 235);
LIGHT.sidebarSeparatorColor = nvgRGB(208, 208, 208);
LIGHT.highlightBackgroundColor = nvgRGB(252, 255, 248);
LIGHT.highlightColor1 = nvgRGB(13, 182, 213);
LIGHT.highlightColor2 = nvgRGB(80, 239, 217);
LIGHT.listItemSeparatorColor = nvgRGB(207, 207, 207);
LIGHT.listItemValueColor = nvgRGB(43, 81, 226);
LIGHT.listItemFaintValueColor = nvgRGB(181, 184, 191);
LIGHT.tableEvenBackgroundColor = nvgRGB(240, 240, 240);
LIGHT.tableBodyTextColor = nvgRGB(131, 131, 131);
LIGHT.dropdownBackgroundColor = nvgRGBA(0, 0, 0, 178);
LIGHT.nextStageBulletColor = nvgRGB(165, 165, 165);
LIGHT.spinnerBarColor = nvgRGBA(131, 131, 131, 102);
LIGHT.headerRectangleColor = nvgRGB(127, 127, 127);
LIGHT.buttonPlainEnabledBackgroundColor = nvgRGB(50, 79, 241);
LIGHT.buttonPlainDisabledBackgroundColor = nvgRGB(201, 201, 209);
LIGHT.buttonPlainEnabledTextColor = nvgRGB(255, 255, 255);
LIGHT.buttonPlainDisabledTextColor = nvgRGB(220, 220, 228);
LIGHT.dialogColor = nvgRGB(240, 240, 240);
LIGHT.dialogBackdrop = nvgRGBA(0, 0, 0, 100);
LIGHT.dialogButtonColor = nvgRGB(46, 78, 255);
LIGHT.dialogButtonSeparatorColor = nvgRGB(210, 210, 210);
// Dark variant
DARK.backgroundColor[0] = 0.176f;
DARK.backgroundColor[1] = 0.176f;
DARK.backgroundColor[2] = 0.176f;
DARK.backgroundColorRGB = nvgRGB(45, 45, 45);
DARK.textColor = nvgRGB(255, 255, 255);
DARK.descriptionColor = nvgRGB(163, 163, 163);
DARK.notificationTextColor = nvgRGB(255, 255, 255);
DARK.backdropColor = nvgRGBA(0, 0, 0, 178);
DARK.separatorColor = nvgRGB(255, 255, 255);
DARK.sidebarColor = nvgRGB(50, 50, 50);
DARK.activeTabColor = nvgRGB(0, 255, 204);
DARK.sidebarSeparatorColor = nvgRGB(81, 81, 81);
DARK.highlightBackgroundColor = nvgRGB(31, 34, 39);
DARK.highlightColor1 = nvgRGB(25, 138, 198);
DARK.highlightColor2 = nvgRGB(137, 241, 242);
DARK.listItemSeparatorColor = nvgRGB(78, 78, 78);
DARK.listItemValueColor = nvgRGB(88, 195, 169);
DARK.listItemFaintValueColor = nvgRGB(93, 103, 105);
DARK.tableEvenBackgroundColor = nvgRGB(57, 58, 60);
DARK.tableBodyTextColor = nvgRGB(155, 157, 156);
DARK.dropdownBackgroundColor = nvgRGBA(0, 0, 0, 178); // TODO: 178 may be too much for dark theme
DARK.nextStageBulletColor = nvgRGB(165, 165, 165);
DARK.spinnerBarColor = nvgRGBA(131, 131, 131, 102); // TODO: get this right
DARK.headerRectangleColor = nvgRGB(160, 160, 160);
DARK.buttonPlainEnabledBackgroundColor = nvgRGB(1, 255, 201);
DARK.buttonPlainDisabledBackgroundColor = nvgRGB(83, 87, 86);
DARK.buttonPlainEnabledTextColor = nvgRGB(52, 41, 55);
DARK.buttonPlainDisabledTextColor = nvgRGB(71, 75, 74);
DARK.dialogColor = nvgRGB(70, 70, 70);
DARK.dialogBackdrop = nvgRGBA(0, 0, 0, 100);
DARK.dialogButtonColor = nvgRGB(3, 251, 199);
DARK.dialogButtonSeparatorColor = nvgRGB(103, 103, 103);
return theme;
}
} // namespace brls

View File

@@ -0,0 +1,242 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019 natinusala
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <borealis/application.hpp>
#include <borealis/thumbnail_frame.hpp>
namespace brls
{
ThumbnailFrame::ThumbnailFrame()
: AppletFrame(true, false)
{
// Create the ThumbnailSidebar
this->sidebar = new ThumbnailSidebar();
// Setup content view
this->boxLayout = new BoxLayout(BoxLayoutOrientation::HORIZONTAL);
AppletFrame::setContentView(this->boxLayout);
}
void ThumbnailFrame::setContentView(View* view)
{
this->thumbnailContentView = view;
// Clear the layout
this->boxLayout->clear();
// Add the views
this->boxLayout->addView(view);
this->boxLayout->addView(this->sidebar);
// Invalidate
this->invalidate();
}
void ThumbnailFrame::layout(NVGcontext* vg, Style* style, FontStash* stash)
{
// Resize content view
if (this->thumbnailContentView)
{
unsigned sidebarWidth = this->sidebar->getWidth();
this->thumbnailContentView->setWidth(this->getWidth() - sidebarWidth - this->leftPadding - this->rightPadding);
}
// Layout the rest
AppletFrame::layout(vg, style, stash);
}
ThumbnailSidebar* ThumbnailFrame::getSidebar()
{
return this->sidebar;
}
ThumbnailFrame::~ThumbnailFrame()
{
// If content view wasn't set, free the sidebar manually
if (!this->thumbnailContentView)
delete this->sidebar;
}
ThumbnailSidebar::ThumbnailSidebar()
{
Style* style = Application::getStyle();
this->setBackground(Background::SIDEBAR);
this->setWidth(style->Sidebar.width);
this->button = (new Button(ButtonStyle::PLAIN))->setLabel("Save");
this->button->setParent(this);
}
Button* ThumbnailSidebar::getButton()
{
return this->button;
}
void ThumbnailSidebar::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, Style* style, FrameContext* ctx)
{
if (this->image)
this->image->frame(ctx);
if (this->title)
this->title->frame(ctx);
if (this->subTitle)
this->subTitle->frame(ctx);
this->button->frame(ctx);
}
void ThumbnailSidebar::layout(NVGcontext* vg, Style* style, FontStash* stash)
{
unsigned sidebarWidth = getWidth() - style->AppletFrame.separatorSpacing;
unsigned yAdvance = getY() + style->ThumbnailSidebar.marginTopBottom;
unsigned titleX = getX() + style->ThumbnailSidebar.marginLeftRight / 2;
unsigned titleWidth = sidebarWidth - style->ThumbnailSidebar.marginLeftRight;
// Image
if (this->image)
{
unsigned imageX = getX() + style->ThumbnailSidebar.marginLeftRight;
unsigned imageWidth = sidebarWidth - style->ThumbnailSidebar.marginLeftRight * 2;
this->image->setBoundaries(
imageX,
yAdvance,
imageWidth,
imageWidth);
yAdvance += this->image->getHeight() + style->ThumbnailSidebar.marginTopBottom;
}
// Title
if (this->title)
{
this->title->setBoundaries(
titleX,
yAdvance,
titleWidth,
0 // height is dynamic
);
// Call layout directly to update height
this->title->invalidate(true);
yAdvance += this->title->getHeight() + style->ThumbnailSidebar.marginTopBottom / 2;
}
// Subtitle
if (this->subTitle)
{
this->subTitle->setBoundaries(
titleX,
yAdvance,
titleWidth,
0 // height doesn't matter
);
this->subTitle->invalidate();
}
//Button
unsigned buttonWidth = sidebarWidth - style->ThumbnailSidebar.buttonMargin * 2;
unsigned buttonHeight = style->ThumbnailSidebar.buttonHeight;
this->button->setBoundaries(
getX() + style->ThumbnailSidebar.buttonMargin,
getY() + getHeight() - style->ThumbnailSidebar.marginTopBottom - buttonHeight,
buttonWidth,
buttonHeight);
}
View* ThumbnailSidebar::getDefaultFocus()
{
return this->button->getDefaultFocus();
}
void ThumbnailSidebar::setThumbnail(std::string imagePath)
{
if (this->image)
{
this->image->setImage(imagePath);
}
else
{
this->image = new Image(imagePath);
this->image->setParent(this);
this->invalidate();
}
}
void ThumbnailSidebar::setThumbnail(unsigned char* buffer, size_t bufferSize)
{
if (this->image)
{
this->image->setImage(buffer, bufferSize);
}
else
{
this->image = new Image(buffer, bufferSize);
this->image->setParent(this);
this->invalidate();
}
}
void ThumbnailSidebar::setSubtitle(std::string subTitle)
{
if (!this->subTitle)
{
this->subTitle = new Label(LabelStyle::DESCRIPTION, "");
this->subTitle->setParent(this);
}
this->subTitle->setText(subTitle);
this->invalidate();
}
// TODO: Add ellipsis if the title exceeds three lines
void ThumbnailSidebar::setTitle(std::string title)
{
if (!this->title)
{
this->title = new Label(LabelStyle::REGULAR, "", true);
this->title->setParent(this);
}
this->title->setText(title);
this->invalidate();
}
ThumbnailSidebar::~ThumbnailSidebar()
{
if (this->image)
delete this->image;
if (this->title)
delete this->title;
if (this->subTitle)
delete this->subTitle;
delete this->button;
}
} // namespace brls

View File

@@ -0,0 +1,630 @@
/*
Borealis, a Nintendo Switch UI Library
Copyright (C) 2019-2020 natinusala
Copyright (C) 2019 p-sam
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <math.h>
#include <algorithm>
#include <borealis/animations.hpp>
#include <borealis/application.hpp>
#include <borealis/view.hpp>
namespace brls
{
NVGcolor transparent = nvgRGBA(0, 0, 0, 0);
static int shakeAnimation(float t, float a) // a = amplitude
{
// Damped sine wave
float w = 0.8f; // period
float c = 0.35f; // damp factor
return roundf(a * exp(-(c * t)) * sin(w * t));
}
void View::shakeHighlight(FocusDirection direction)
{
this->highlightShaking = true;
this->highlightShakeStart = cpu_features_get_time_usec() / 1000;
this->highlightShakeDirection = direction;
this->highlightShakeAmplitude = std::rand() % 15 + 10;
}
float View::getAlpha(bool child)
{
return this->alpha * (this->parent ? this->parent->getAlpha(true) : 1.0f);
}
NVGcolor View::a(NVGcolor color)
{
NVGcolor newColor = color;
newColor.a *= this->getAlpha();
return newColor;
}
NVGpaint View::a(NVGpaint paint)
{
NVGpaint newPaint = paint;
newPaint.innerColor.a *= this->getAlpha();
newPaint.outerColor.a *= this->getAlpha();
return newPaint;
}
// TODO: Only draw views that are onscreen (w/ some margins)
void View::frame(FrameContext* ctx)
{
Style* style = Application::getStyle();
ThemeValues* oldTheme = ctx->theme;
nvgSave(ctx->vg);
// Theme override
if (this->themeOverride)
ctx->theme = themeOverride;
// Layout if needed
if (this->dirty)
{
this->invalidate(true);
this->dirty = false;
}
if (this->alpha > 0.0f && this->collapseState != 0.0f)
{
// Draw background
this->drawBackground(ctx->vg, ctx, style);
// Draw highlight background
if (this->highlightAlpha > 0.0f && this->isHighlightBackgroundEnabled())
this->drawHighlight(ctx->vg, ctx->theme, this->highlightAlpha, style, true);
// Collapse clipping
if (this->collapseState < 1.0f)
{
nvgSave(ctx->vg);
nvgIntersectScissor(ctx->vg, x, y, this->width, this->height * this->collapseState);
}
// Draw the view
this->draw(ctx->vg, this->x, this->y, this->width, this->height, style, ctx);
// Draw highlight
if (this->highlightAlpha > 0.0f)
this->drawHighlight(ctx->vg, ctx->theme, this->highlightAlpha, style, false);
//Reset clipping
if (this->collapseState < 1.0f)
nvgRestore(ctx->vg);
}
// Cleanup
if (this->themeOverride)
ctx->theme = oldTheme;
nvgRestore(ctx->vg);
}
void View::collapse(bool animated)
{
menu_animation_ctx_tag tag = (uintptr_t) & this->collapseState;
menu_animation_kill_by_tag(&tag);
if (animated)
{
Style* style = Application::getStyle();
menu_animation_ctx_entry_t entry;
entry.cb = [](void* userdata) {};
entry.duration = style->AnimationDuration.collapse;
entry.easing_enum = EASING_OUT_QUAD;
entry.subject = &this->collapseState;
entry.tag = tag;
entry.target_value = 0.0f;
entry.tick = [this](void* userdata) { if (this->hasParent()) this->getParent()->invalidate(); };
entry.userdata = nullptr;
menu_animation_push(&entry);
}
else
{
this->collapseState = 0.0f;
}
}
bool View::isCollapsed()
{
return this->collapseState < 1.0f;
}
void View::expand(bool animated)
{
menu_animation_ctx_tag tag = (uintptr_t) & this->collapseState;
menu_animation_kill_by_tag(&tag);
if (animated)
{
Style* style = Application::getStyle();
menu_animation_ctx_entry_t entry;
entry.cb = [](void* userdata) {};
entry.duration = style->AnimationDuration.collapse;
entry.easing_enum = EASING_OUT_QUAD;
entry.subject = &this->collapseState;
entry.tag = tag;
entry.target_value = 1.0f;
entry.tick = [this](void* userdata) { if (this->hasParent()) this->getParent()->invalidate(); };
entry.userdata = nullptr;
menu_animation_push(&entry);
}
else
{
this->collapseState = 1.0f;
}
}
// TODO: Slight glow all around
void View::drawHighlight(NVGcontext* vg, ThemeValues* theme, float alpha, Style* style, bool background)
{
nvgSave(vg);
nvgResetScissor(vg);
unsigned insetTop, insetRight, insetBottom, insetLeft;
this->getHighlightInsets(&insetTop, &insetRight, &insetBottom, &insetLeft);
float cornerRadius;
this->getHighlightMetrics(style, &cornerRadius);
unsigned x = this->x - insetLeft - style->Highlight.strokeWidth / 2;
unsigned y = this->y - insetTop - style->Highlight.strokeWidth / 2;
unsigned width = this->width + insetLeft + insetRight + style->Highlight.strokeWidth;
unsigned height = this->height + insetTop + insetBottom + style->Highlight.strokeWidth;
// Shake animation
if (this->highlightShaking)
{
retro_time_t curTime = cpu_features_get_time_usec() / 1000;
retro_time_t t = (curTime - highlightShakeStart) / 10;
if (t >= style->AnimationDuration.shake)
{
this->highlightShaking = false;
}
else
{
switch (this->highlightShakeDirection)
{
case FocusDirection::RIGHT:
x += shakeAnimation(t, this->highlightShakeAmplitude);
break;
case FocusDirection::LEFT:
x -= shakeAnimation(t, this->highlightShakeAmplitude);
break;
case FocusDirection::DOWN:
y += shakeAnimation(t, this->highlightShakeAmplitude);
break;
case FocusDirection::UP:
y -= shakeAnimation(t, this->highlightShakeAmplitude);
break;
default:
break;
}
}
}
// Draw
if (background)
{
// Background
nvgFillColor(vg, RGBAf(theme->highlightBackgroundColor.r, theme->highlightBackgroundColor.g, theme->highlightBackgroundColor.b, this->highlightAlpha));
nvgBeginPath(vg);
nvgRoundedRect(vg, x, y, width, height, cornerRadius);
nvgFill(vg);
}
else
{
// Shadow
NVGpaint shadowPaint = nvgBoxGradient(vg,
x, y + style->Highlight.shadowWidth,
width, height,
cornerRadius * 2, style->Highlight.shadowFeather,
RGBA(0, 0, 0, style->Highlight.shadowOpacity * alpha), transparent);
nvgBeginPath(vg);
nvgRect(vg, x - style->Highlight.shadowOffset, y - style->Highlight.shadowOffset,
width + style->Highlight.shadowOffset * 2, height + style->Highlight.shadowOffset * 3);
nvgRoundedRect(vg, x, y, width, height, cornerRadius);
nvgPathWinding(vg, NVG_HOLE);
nvgFillPaint(vg, shadowPaint);
nvgFill(vg);
// Border
float gradientX, gradientY, color;
menu_animation_get_highlight(&gradientX, &gradientY, &color);
NVGcolor pulsationColor = RGBAf((color * theme->highlightColor1.r) + (1 - color) * theme->highlightColor2.r,
(color * theme->highlightColor1.g) + (1 - color) * theme->highlightColor2.g,
(color * theme->highlightColor1.b) + (1 - color) * theme->highlightColor2.b,
alpha);
NVGcolor borderColor = theme->highlightColor2;
borderColor.a = 0.5f * alpha * this->getAlpha();
NVGpaint border1Paint = nvgRadialGradient(vg,
x + gradientX * width, y + gradientY * height,
style->Highlight.strokeWidth * 10, style->Highlight.strokeWidth * 40,
borderColor, transparent);
NVGpaint border2Paint = nvgRadialGradient(vg,
x + (1 - gradientX) * width, y + (1 - gradientY) * height,
style->Highlight.strokeWidth * 10, style->Highlight.strokeWidth * 40,
borderColor, transparent);
nvgBeginPath(vg);
nvgStrokeColor(vg, pulsationColor);
nvgStrokeWidth(vg, style->Highlight.strokeWidth);
nvgRoundedRect(vg, x, y, width, height, cornerRadius);
nvgStroke(vg);
nvgBeginPath(vg);
nvgStrokePaint(vg, border1Paint);
nvgStrokeWidth(vg, style->Highlight.strokeWidth);
nvgRoundedRect(vg, x, y, width, height, cornerRadius);
nvgStroke(vg);
nvgBeginPath(vg);
nvgStrokePaint(vg, border2Paint);
nvgStrokeWidth(vg, style->Highlight.strokeWidth);
nvgRoundedRect(vg, x, y, width, height, cornerRadius);
nvgStroke(vg);
}
nvgRestore(vg);
}
void View::setBackground(Background background)
{
this->background = background;
}
void View::drawBackground(NVGcontext* vg, FrameContext* ctx, Style* style)
{
switch (this->background)
{
case Background::SIDEBAR:
{
unsigned backdropHeight = style->Background.sidebarBorderHeight;
// Solid color
nvgBeginPath(vg);
nvgFillColor(vg, a(ctx->theme->sidebarColor));
nvgRect(vg, this->x, this->y + backdropHeight, this->width, this->height - backdropHeight * 2);
nvgFill(vg);
//Borders gradient
// Top
NVGpaint topGradient = nvgLinearGradient(vg, this->x, this->y + backdropHeight, this->x, this->y, a(ctx->theme->sidebarColor), transparent);
nvgBeginPath(vg);
nvgFillPaint(vg, topGradient);
nvgRect(vg, this->x, this->y, this->width, backdropHeight);
nvgFill(vg);
// Bottom
NVGpaint bottomGradient = nvgLinearGradient(vg, this->x, this->y + this->height - backdropHeight, this->x, this->y + this->height, a(ctx->theme->sidebarColor), transparent);
nvgBeginPath(vg);
nvgFillPaint(vg, bottomGradient);
nvgRect(vg, this->x, this->y + this->height - backdropHeight, this->width, backdropHeight);
nvgFill(vg);
break;
}
case Background::DEBUG:
{
nvgFillColor(vg, RGB(255, 0, 0));
nvgBeginPath(vg);
nvgRect(vg, this->x, this->y, this->width, this->height);
nvgFill(vg);
break;
}
case Background::BACKDROP:
{
nvgFillColor(vg, a(ctx->theme->backdropColor));
nvgBeginPath(vg);
nvgRect(vg, this->x, this->y, this->width, this->height);
nvgFill(vg);
}
case Background::NONE:
break;
}
}
void View::registerAction(std::string hintText, Key key, ActionListener actionListener, bool hidden)
{
if (auto it = std::find(this->actions.begin(), this->actions.end(), key); it != this->actions.end())
*it = { key, hintText, true, hidden, actionListener };
else
this->actions.push_back({ key, hintText, true, hidden, actionListener });
}
void View::updateActionHint(Key key, std::string hintText)
{
if (auto it = std::find(this->actions.begin(), this->actions.end(), key); it != this->actions.end())
it->hintText = hintText;
Application::getGlobalHintsUpdateEvent()->fire();
}
void View::setActionAvailable(Key key, bool available)
{
if (auto it = std::find(this->actions.begin(), this->actions.end(), key); it != this->actions.end())
it->available = available;
}
void View::setBoundaries(int x, int y, unsigned width, unsigned height)
{
this->x = x;
this->y = y;
this->width = width;
this->height = height;
}
void View::setParent(View* parent, void* parentUserdata)
{
this->parent = parent;
this->parentUserdata = parentUserdata;
}
void* View::getParentUserData()
{
return this->parentUserdata;
}
bool View::isFocused()
{
return this->focused;
}
View* View::getParent()
{
return this->parent;
}
bool View::hasParent()
{
return this->parent;
}
void View::setWidth(unsigned width)
{
this->width = width;
}
void View::setHeight(unsigned height)
{
this->height = height;
}
int View::getX()
{
return this->x;
}
int View::getY()
{
return this->y;
}
unsigned View::getHeight(bool includeCollapse)
{
return this->height * (includeCollapse ? this->collapseState : 1.0f);
}
unsigned View::getWidth()
{
return this->width;
}
void View::onFocusGained()
{
this->focused = true;
Style* style = Application::getStyle();
menu_animation_ctx_tag tag = (uintptr_t)this->highlightAlpha;
menu_animation_ctx_entry_t entry;
entry.cb = [](void* userdata) {};
entry.duration = style->AnimationDuration.highlight;
entry.easing_enum = EASING_OUT_QUAD;
entry.subject = &this->highlightAlpha;
entry.tag = tag;
entry.target_value = 1.0f;
entry.tick = [](void* userdata) {};
entry.userdata = nullptr;
menu_animation_push(&entry);
this->focusEvent.fire(this);
if (this->hasParent())
this->getParent()->onChildFocusGained(this);
}
GenericEvent* View::getFocusEvent()
{
return &this->focusEvent;
}
/**
* Fired when focus is lost
*/
void View::onFocusLost()
{
this->focused = false;
Style* style = Application::getStyle();
menu_animation_ctx_tag tag = (uintptr_t)this->highlightAlpha;
menu_animation_ctx_entry_t entry;
entry.cb = [](void* userdata) {};
entry.duration = style->AnimationDuration.highlight;
entry.easing_enum = EASING_OUT_QUAD;
entry.subject = &this->highlightAlpha;
entry.tag = tag;
entry.target_value = 0.0f;
entry.tick = [](void* userdata) {};
entry.userdata = nullptr;
menu_animation_push(&entry);
if (this->hasParent())
this->getParent()->onChildFocusLost(this);
}
void View::setForceTranslucent(bool translucent)
{
this->forceTranslucent = translucent;
}
unsigned View::getShowAnimationDuration(ViewAnimation animation)
{
Style* style = Application::getStyle();
if (animation == ViewAnimation::SLIDE_LEFT || animation == ViewAnimation::SLIDE_RIGHT)
return style->AnimationDuration.showSlide;
return style->AnimationDuration.show;
}
void View::show(std::function<void(void)> cb, bool animate, ViewAnimation animation)
{
brls::Logger::debug("Showing %s with animation %d", this->describe().c_str(), animation);
this->hidden = false;
menu_animation_ctx_tag tag = (uintptr_t) & this->alpha;
menu_animation_kill_by_tag(&tag);
this->fadeIn = true;
if (animate)
{
this->alpha = 0.0f;
menu_animation_ctx_entry_t entry;
entry.cb = [this, cb](void* userdata) {
this->fadeIn = false;
this->onShowAnimationEnd();
cb();
};
entry.duration = this->getShowAnimationDuration(animation);
entry.easing_enum = EASING_OUT_QUAD;
entry.subject = &this->alpha;
entry.tag = tag;
entry.target_value = 1.0f;
entry.tick = [](void* userdata) {};
entry.userdata = nullptr;
menu_animation_push(&entry);
}
else
{
this->alpha = 1.0f;
this->fadeIn = false;
this->onShowAnimationEnd();
cb();
}
}
void View::hide(std::function<void(void)> cb, bool animated, ViewAnimation animation)
{
brls::Logger::debug("Hiding %s with animation %d", this->describe().c_str(), animation);
this->hidden = true;
this->fadeIn = false;
menu_animation_ctx_tag tag = (uintptr_t) & this->alpha;
menu_animation_kill_by_tag(&tag);
if (animated)
{
this->alpha = 1.0f;
menu_animation_ctx_entry_t entry;
entry.cb = [cb](void* userdata) { cb(); };
entry.duration = this->getShowAnimationDuration(animation);
entry.easing_enum = EASING_OUT_QUAD;
entry.subject = &this->alpha;
entry.tag = tag;
entry.target_value = 0.0f;
entry.tick = [](void* userdata) {};
entry.userdata = nullptr;
menu_animation_push(&entry);
}
else
{
this->alpha = 0.0f;
cb();
}
}
bool View::isHidden()
{
return this->hidden;
}
void View::overrideThemeVariant(ThemeValues* theme)
{
this->themeOverride = theme;
}
View::~View()
{
menu_animation_ctx_tag alphaTag = (uintptr_t) & this->alpha;
menu_animation_kill_by_tag(&alphaTag);
menu_animation_ctx_tag highlightTag = (uintptr_t) & this->highlightAlpha;
menu_animation_kill_by_tag(&highlightTag);
menu_animation_ctx_tag collapseTag = (uintptr_t) & this->collapseState;
menu_animation_kill_by_tag(&collapseTag);
// Parent userdata
if (this->parentUserdata)
{
free(this->parentUserdata);
this->parentUserdata = nullptr;
}
// Focus sanity check
if (Application::getCurrentFocus() == this)
Application::giveFocus(nullptr);
}
void View::invalidate(bool immediate)
{
if (immediate)
this->layout(Application::getNVGContext(), Application::getStyle(), Application::getFontStash());
else
this->dirty = true;
}
} // namespace brls