992 lines
29 KiB
C++
992 lines
29 KiB
C++
/*
|
|
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
|