diff --git a/.gitignore b/.gitignore index 54b77b4ef..c8c423006 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,9 @@ *.x86_64 *.hex +# Deko3d shaders +*.dksh + # Switch Executables *.nso *.nro diff --git a/troposphere/Makefile b/troposphere/Makefile index 2be61ea0a..25adf0da2 100644 --- a/troposphere/Makefile +++ b/troposphere/Makefile @@ -1,4 +1,4 @@ -APPLICATIONS := reboot_to_payload +APPLICATIONS := daybreak reboot_to_payload SUBFOLDERS := $(APPLICATIONS) diff --git a/troposphere/daybreak/Makefile b/troposphere/daybreak/Makefile new file mode 100644 index 000000000..8753aca8f --- /dev/null +++ b/troposphere/daybreak/Makefile @@ -0,0 +1,282 @@ +#--------------------------------------------------------------------------------- +.SUFFIXES: +#--------------------------------------------------------------------------------- + +ifeq ($(strip $(DEVKITPRO)),) +$(error "Please set DEVKITPRO in your environment. export DEVKITPRO=/devkitpro") +endif + +TOPDIR ?= $(CURDIR) +include $(DEVKITPRO)/libnx/switch_rules + +#--------------------------------------------------------------------------------- +# TARGET is the name of the output +# BUILD is the directory where object files & intermediate files will be placed +# SOURCES is a list of directories containing source code +# DATA is a list of directories containing data files +# INCLUDES is a list of directories containing header files +# ROMFS is the directory containing data to be added to RomFS, relative to the Makefile (Optional) +# +# NO_ICON: if set to anything, do not use icon. +# NO_NACP: if set to anything, no .nacp file is generated. +# APP_TITLE is the name of the app stored in the .nacp file (Optional) +# APP_AUTHOR is the author of the app stored in the .nacp file (Optional) +# APP_VERSION is the version of the app stored in the .nacp file (Optional) +# APP_TITLEID is the titleID of the app stored in the .nacp file (Optional) +# ICON is the filename of the icon (.jpg), relative to the project folder. +# If not set, it attempts to use one of the following (in this order): +# - .jpg +# - icon.jpg +# - /default_icon.jpg +# +# CONFIG_JSON is the filename of the NPDM config file (.json), relative to the project folder. +# If not set, it attempts to use one of the following (in this order): +# - .json +# - config.json +# If a JSON file is provided or autodetected, an ExeFS PFS0 (.nsp) is built instead +# of a homebrew executable (.nro). This is intended to be used for sysmodules. +# NACP building is skipped as well. +#--------------------------------------------------------------------------------- +TARGET := daybreak +BUILD := build +SOURCES := source nanovg/shaders +DATA := data +INCLUDES := include ../include +ROMFS := romfs + +# Output folders for autogenerated files in romfs +OUT_SHADERS := shaders + +APP_TITLE := Daybreak +APP_AUTHOR := Atmosphere-NX +APP_VERSION := 1.0.0 + +#--------------------------------------------------------------------------------- +# options for code generation +#--------------------------------------------------------------------------------- +ARCH := -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE + +CFLAGS := -g -Wall -O2 -ffunction-sections \ + $(ARCH) $(DEFINES) + +CFLAGS += $(INCLUDE) -D__SWITCH__ + +CXXFLAGS := $(CFLAGS) -std=gnu++17 -fno-exceptions -fno-rtti + +ASFLAGS := -g $(ARCH) +LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) + +LIBS := -lnanovg -ldeko3d -lnx + +#--------------------------------------------------------------------------------- +# list of directories containing libraries, this must be the top level containing +# include and lib +#--------------------------------------------------------------------------------- +LIBDIRS := $(PORTLIBS) $(LIBNX) $(CURDIR)/nanovg/ + +#--------------------------------------------------------------------------------- +# no real need to edit anything past this point unless you need to add additional +# rules for different file extensions +#--------------------------------------------------------------------------------- +ifneq ($(BUILD),$(notdir $(CURDIR))) +#--------------------------------------------------------------------------------- + +export OUTPUT := $(CURDIR)/$(TARGET) +export TOPDIR := $(CURDIR) + +export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \ + $(foreach dir,$(DATA),$(CURDIR)/$(dir)) + +export DEPSDIR := $(CURDIR)/$(BUILD) + +SUBFOLDERS := nanovg + +TOPTARGETS := all clean + +$(TOPTARGETS): $(SUBFOLDERS) + +$(SUBFOLDERS): + $(MAKE) -C $@ $(MAKECMDGOALS) + +CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))) +CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp))) +SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s))) +GLSLFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.glsl))) +BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*))) + +#--------------------------------------------------------------------------------- +# use CXX for linking C++ projects, CC for standard C +#--------------------------------------------------------------------------------- +ifeq ($(strip $(CPPFILES)),) +#--------------------------------------------------------------------------------- + export LD := $(CC) +#--------------------------------------------------------------------------------- +else +#--------------------------------------------------------------------------------- + export LD := $(CXX) +#--------------------------------------------------------------------------------- +endif +#--------------------------------------------------------------------------------- + +export OFILES_BIN := $(addsuffix .o,$(BINFILES)) +export OFILES_SRC := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o) +export OFILES := $(OFILES_BIN) $(OFILES_SRC) +export HFILES_BIN := $(addsuffix .h,$(subst .,_,$(BINFILES))) +export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) + +ifneq ($(strip $(ROMFS)),) + ROMFS_TARGETS := + ROMFS_FOLDERS := + ifneq ($(strip $(OUT_SHADERS)),) + ROMFS_SHADERS := $(ROMFS)/$(OUT_SHADERS) + ROMFS_TARGETS += $(patsubst %.glsl, $(ROMFS_SHADERS)/%.dksh, $(GLSLFILES)) + ROMFS_FOLDERS += $(ROMFS_SHADERS) + endif + + export ROMFS_DEPS := $(foreach file,$(ROMFS_TARGETS),$(CURDIR)/$(file)) +endif + +export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ + $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ + -I$(CURDIR)/$(BUILD) + +ifeq ($(strip $(CONFIG_JSON)),) + jsons := $(wildcard *.json) + ifneq (,$(findstring $(TARGET).json,$(jsons))) + export APP_JSON := $(TOPDIR)/$(TARGET).json + else + ifneq (,$(findstring config.json,$(jsons))) + export APP_JSON := $(TOPDIR)/config.json + endif + endif +else + export APP_JSON := $(TOPDIR)/$(CONFIG_JSON) +endif + +ifeq ($(strip $(ICON)),) + icons := $(wildcard *.jpg) + ifneq (,$(findstring $(TARGET).jpg,$(icons))) + export APP_ICON := $(TOPDIR)/$(TARGET).jpg + else + ifneq (,$(findstring icon.jpg,$(icons))) + export APP_ICON := $(TOPDIR)/icon.jpg + endif + endif +else + export APP_ICON := $(TOPDIR)/$(ICON) +endif + +ifeq ($(strip $(NO_ICON)),) + export NROFLAGS += --icon=$(APP_ICON) +endif + +ifeq ($(strip $(NO_NACP)),) + export NROFLAGS += --nacp=$(CURDIR)/$(TARGET).nacp +endif + +ifneq ($(APP_TITLEID),) + export NACPFLAGS += --titleid=$(APP_TITLEID) +endif + +ifneq ($(ROMFS),) + export NROFLAGS += --romfsdir=$(CURDIR)/$(ROMFS) +endif + +.PHONY: $(TOPTARGETS) $(SUBFOLDERS) all clean + +#--------------------------------------------------------------------------------- +all: $(ROMFS_TARGETS) | $(BUILD) + @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile + +$(BUILD): + @mkdir -p $@ + +ifneq ($(strip $(ROMFS_TARGETS)),) + +$(ROMFS_TARGETS): | $(ROMFS_FOLDERS) + +$(ROMFS_FOLDERS): + @mkdir -p $@ + +$(ROMFS_SHADERS)/%_vsh.dksh: %_vsh.glsl + @echo {vert} $(notdir $<) + @uam -s vert -o $@ $< + +$(ROMFS_SHADERS)/%_tcsh.dksh: %_tcsh.glsl + @echo {tess_ctrl} $(notdir $<) + @uam -s tess_ctrl -o $@ $< + +$(ROMFS_SHADERS)/%_tesh.dksh: %_tesh.glsl + @echo {tess_eval} $(notdir $<) + @uam -s tess_eval -o $@ $< + +$(ROMFS_SHADERS)/%_gsh.dksh: %_gsh.glsl + @echo {geom} $(notdir $<) + @uam -s geom -o $@ $< + +$(ROMFS_SHADERS)/%_fsh.dksh: %_fsh.glsl + @echo {frag} $(notdir $<) + @uam -s frag -o $@ $< + +$(ROMFS_SHADERS)/%.dksh: %.glsl + @echo {comp} $(notdir $<) + @uam -s comp -o $@ $< + +endif + +#--------------------------------------------------------------------------------- +clean: + @echo clean ... +ifeq ($(strip $(APP_JSON)),) + @rm -fr $(BUILD) $(ROMFS_FOLDERS) $(TARGET).nro $(TARGET).nacp $(TARGET).elf +else + @rm -fr $(BUILD) $(ROMFS_FOLDERS) $(TARGET).nsp $(TARGET).nso $(TARGET).npdm $(TARGET).elf +endif + + +#--------------------------------------------------------------------------------- +else +.PHONY: all + +DEPENDS := $(OFILES:.o=.d) + +#--------------------------------------------------------------------------------- +# main targets +#--------------------------------------------------------------------------------- +ifeq ($(strip $(APP_JSON)),) + +all : $(OUTPUT).nro + +ifeq ($(strip $(NO_NACP)),) +$(OUTPUT).nro : $(OUTPUT).elf $(OUTPUT).nacp $(ROMFS_DEPS) +else +$(OUTPUT).nro : $(OUTPUT).elf $(ROMFS_DEPS) +endif + +else + +all : $(OUTPUT).nsp + +$(OUTPUT).nsp : $(OUTPUT).nso $(OUTPUT).npdm + +$(OUTPUT).nso : $(OUTPUT).elf + +endif + +$(OUTPUT).elf : $(OFILES) + +$(OFILES_SRC) : $(HFILES_BIN) + +#--------------------------------------------------------------------------------- +# you need a rule like this for each extension you use as binary data +#--------------------------------------------------------------------------------- +%.bin.o %_bin.h : %.bin +#--------------------------------------------------------------------------------- + @echo $(notdir $<) + @$(bin2o) + +-include $(DEPENDS) + +#--------------------------------------------------------------------------------------- +endif +#--------------------------------------------------------------------------------------- \ No newline at end of file diff --git a/troposphere/daybreak/icon.jpg b/troposphere/daybreak/icon.jpg new file mode 100644 index 000000000..867e5d53b Binary files /dev/null and b/troposphere/daybreak/icon.jpg differ diff --git a/troposphere/daybreak/source/ams_su.c b/troposphere/daybreak/source/ams_su.c new file mode 100644 index 000000000..a5ef3282b --- /dev/null +++ b/troposphere/daybreak/source/ams_su.c @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2018-2020 Atmosphère-NX + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope 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 . + */ +#include +#include +#include "ams_su.h" +#include "service_guard.h" + +static Service g_amssuSrv; +static TransferMemory g_tmem; + +NX_GENERATE_SERVICE_GUARD(amssu); + +Result _amssuInitialize(void) { + return smGetService(&g_amssuSrv, "ams:su"); +} + +void _amssuCleanup(void) { + serviceClose(&g_amssuSrv); + tmemClose(&g_tmem); +} + +Service *amssuGetServiceSession(void) { + return &g_amssuSrv; +} + +Result amssuGetUpdateInformation(AmsSuUpdateInformation *out, const char *path) { + char send_path[FS_MAX_PATH] = {0}; + strncpy(send_path, path, FS_MAX_PATH-1); + send_path[FS_MAX_PATH-1] = 0; + + return serviceDispatchOut(&g_amssuSrv, 0, *out, + .buffer_attrs = { SfBufferAttr_In | SfBufferAttr_HipcPointer | SfBufferAttr_FixedSize }, + .buffers = { { send_path, FS_MAX_PATH } }, + ); +} + +Result amssuValidateUpdate(AmsSuUpdateValidationInfo *out, const char *path) { + char send_path[FS_MAX_PATH] = {0}; + strncpy(send_path, path, FS_MAX_PATH-1); + send_path[FS_MAX_PATH-1] = 0; + + return serviceDispatchOut(&g_amssuSrv, 1, *out, + .buffer_attrs = { SfBufferAttr_In | SfBufferAttr_HipcPointer | SfBufferAttr_FixedSize }, + .buffers = { { send_path, FS_MAX_PATH } }, + ); +} + +Result amssuSetupUpdate(void *buffer, size_t size, const char *path, bool exfat) { + Result rc = 0; + + if (buffer == NULL) { + rc = tmemCreate(&g_tmem, size, Perm_None); + } else { + rc = tmemCreateFromMemory(&g_tmem, buffer, size, Perm_None); + } + if (R_FAILED(rc)) return rc; + + char send_path[FS_MAX_PATH] = {0}; + strncpy(send_path, path, FS_MAX_PATH-1); + send_path[FS_MAX_PATH-1] = 0; + + const struct { + u8 exfat; + u64 size; + } in = { exfat, g_tmem.size }; + + rc = serviceDispatchIn(&g_amssuSrv, 2, in, + .in_num_handles = 1, + .in_handles = { g_tmem.handle }, + .buffer_attrs = { SfBufferAttr_In | SfBufferAttr_HipcPointer | SfBufferAttr_FixedSize }, + .buffers = { { send_path, FS_MAX_PATH } }, + ); + if (R_FAILED((rc))) { + tmemClose(&g_tmem); + } + + return rc; +} + +Result amssuSetupUpdateWithVariation(void *buffer, size_t size, const char *path, bool exfat, u32 variation) { + Result rc = 0; + + if (buffer == NULL) { + rc = tmemCreate(&g_tmem, size, Perm_None); + } else { + rc = tmemCreateFromMemory(&g_tmem, buffer, size, Perm_None); + } + if (R_FAILED(rc)) return rc; + + char send_path[FS_MAX_PATH] = {0}; + strncpy(send_path, path, FS_MAX_PATH-1); + send_path[FS_MAX_PATH-1] = 0; + + const struct { + u8 exfat; + u32 variation; + u64 size; + } in = { exfat, variation, g_tmem.size }; + + rc = serviceDispatchIn(&g_amssuSrv, 3, in, + .in_num_handles = 1, + .in_handles = { g_tmem.handle }, + .buffer_attrs = { SfBufferAttr_In | SfBufferAttr_HipcPointer | SfBufferAttr_FixedSize }, + .buffers = { { send_path, FS_MAX_PATH } }, + ); + if (R_FAILED((rc))) { + tmemClose(&g_tmem); + } + + return rc; +} + +Result amssuRequestPrepareUpdate(AsyncResult *a) { + memset(a, 0, sizeof(*a)); + + Handle event = INVALID_HANDLE; + Result rc = serviceDispatch(&g_amssuSrv, 4, + .out_num_objects = 1, + .out_objects = &a->s, + .out_handle_attrs = { SfOutHandleAttr_HipcCopy }, + .out_handles = &event, + ); + + if (R_SUCCEEDED(rc)) + eventLoadRemote(&a->event, event, false); + + return rc; +} + +Result amssuGetPrepareUpdateProgress(NsSystemUpdateProgress *out) { + return serviceDispatchOut(&g_amssuSrv, 5, *out); +} + +Result amssuHasPreparedUpdate(bool *out) { + u8 outval = 0; + Result rc = serviceDispatchOut(&g_amssuSrv, 6, outval); + if (R_SUCCEEDED(rc)) { + if (out) *out = outval & 1; + } + return rc; +} + +Result amssuApplyPreparedUpdate() { + return serviceDispatch(&g_amssuSrv, 7); +} \ No newline at end of file diff --git a/troposphere/daybreak/source/ams_su.h b/troposphere/daybreak/source/ams_su.h new file mode 100644 index 000000000..d38e183fe --- /dev/null +++ b/troposphere/daybreak/source/ams_su.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2018-2020 Atmosphère-NX + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope 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 . + */ +#pragma once +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + u32 version; + bool exfat_supported; + u32 num_firmware_variations; + u32 firmware_variation_ids[16]; +} AmsSuUpdateInformation; + +typedef struct { + Result result; + NcmContentMetaKey invalid_key; + NcmContentId invalid_content_id; +} AmsSuUpdateValidationInfo; + +Result amssuInitialize(); +void amssuExit(); +Service *amssuGetServiceSession(void); + +Result amssuGetUpdateInformation(AmsSuUpdateInformation *out, const char *path); +Result amssuValidateUpdate(AmsSuUpdateValidationInfo *out, const char *path); +Result amssuSetupUpdate(void *buffer, size_t size, const char *path, bool exfat); +Result amssuSetupUpdateWithVariation(void *buffer, size_t size, const char *path, bool exfat, u32 variation); +Result amssuRequestPrepareUpdate(AsyncResult *a); +Result amssuGetPrepareUpdateProgress(NsSystemUpdateProgress *out); +Result amssuHasPreparedUpdate(bool *out); +Result amssuApplyPreparedUpdate(); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/troposphere/daybreak/source/assert.hpp b/troposphere/daybreak/source/assert.hpp new file mode 100644 index 000000000..1b8ed4d5f --- /dev/null +++ b/troposphere/daybreak/source/assert.hpp @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020 Adubbz + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope 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 . + */ +#pragma once + +#include + +#define DBK_ABORT_UNLESS(expr) \ + if (!static_cast(expr)) { \ + std::abort(); \ + } diff --git a/troposphere/daybreak/source/main.cpp b/troposphere/daybreak/source/main.cpp new file mode 100644 index 000000000..ef95b363f --- /dev/null +++ b/troposphere/daybreak/source/main.cpp @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2020 Adubbz + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope 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 . + */ +#include +#include +#include +#include +#include +#include "ui.hpp" +#include "ams_su.h" + +extern "C" { + + void userAppInit(void) { + Result rc = 0; + + if (R_FAILED(rc = amssuInitialize())) { + fatalThrow(rc); + } + + if (R_FAILED(rc = romfsInit())) { + fatalThrow(rc); + } + + if (R_FAILED(rc = spsmInitialize())) { + fatalThrow(rc); + } + + if (R_FAILED(rc = plInitialize(PlServiceType_User))) { + fatalThrow(rc); + } + } + + void userAppExit(void) { + romfsExit(); + plExit(); + spsmExit(); + amssuExit(); + } + +} + +namespace { + + static constexpr u32 FramebufferWidth = 1280; + static constexpr u32 FramebufferHeight = 720; + +} + +class Daybreak : public CApplication { + private: + static constexpr unsigned NumFramebuffers = 2; + static constexpr unsigned StaticCmdSize = 0x1000; + + dk::UniqueDevice m_device; + dk::UniqueQueue m_queue; + dk::UniqueSwapchain m_swapchain; + + std::optional m_pool_images; + std::optional m_pool_code; + std::optional m_pool_data; + + dk::UniqueCmdBuf m_cmd_buf; + DkCmdList m_render_cmdlist; + + dk::Image m_depth_buffer; + CMemPool::Handle m_depth_buffer_mem; + dk::Image m_framebuffers[NumFramebuffers]; + CMemPool::Handle m_framebuffers_mem[NumFramebuffers]; + DkCmdList m_framebuffer_cmdlists[NumFramebuffers]; + + std::optional m_renderer; + NVGcontext *m_vg; + int m_standard_font; + public: + Daybreak() { + Result rc = 0; + + /* Create the deko3d device. */ + m_device = dk::DeviceMaker{}.create(); + + /* Create the main queue. */ + m_queue = dk::QueueMaker{m_device}.setFlags(DkQueueFlags_Graphics).create(); + + /* Create the memory pools. */ + m_pool_images.emplace(m_device, DkMemBlockFlags_GpuCached | DkMemBlockFlags_Image, 16*1024*1024); + m_pool_code.emplace(m_device, DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached | DkMemBlockFlags_Code, 128*1024); + m_pool_data.emplace(m_device, DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached, 1*1024*1024); + + /* Create the static command buffer and feed it freshly allocated memory. */ + m_cmd_buf = dk::CmdBufMaker{m_device}.create(); + CMemPool::Handle cmdmem = m_pool_data->allocate(StaticCmdSize); + m_cmd_buf.addMemory(cmdmem.getMemBlock(), cmdmem.getOffset(), cmdmem.getSize()); + + /* Create the framebuffer resources. */ + this->CreateFramebufferResources(); + + m_renderer.emplace(FramebufferWidth, FramebufferHeight, m_device, m_queue, *m_pool_images, *m_pool_code, *m_pool_data); + m_vg = nvgCreateDk(&*m_renderer, NVG_ANTIALIAS | NVG_STENCIL_STROKES); + + + PlFontData font; + if (R_FAILED(rc = plGetSharedFontByType(&font, PlSharedFontType_Standard))) { + fatalThrow(rc); + } + + m_standard_font = nvgCreateFontMem(m_vg, "switch-standard", static_cast(font.address), font.size, 0); + } + + ~Daybreak() { + /* Destroy the framebuffer resources. This should be done first. */ + this->DestroyFramebufferResources(); + + /* Cleanup vg. */ + nvgDeleteDk(m_vg); + + /* Destroy the renderer. */ + m_renderer.reset(); + } + private: + void CreateFramebufferResources() { + /* Create layout for the depth buffer. */ + dk::ImageLayout layout_depth_buffer; + dk::ImageLayoutMaker{m_device} + .setFlags(DkImageFlags_UsageRender | DkImageFlags_HwCompression) + .setFormat(DkImageFormat_S8) + .setDimensions(FramebufferWidth, FramebufferHeight) + .initialize(layout_depth_buffer); + + /* Create the depth buffer. */ + m_depth_buffer_mem = m_pool_images->allocate(layout_depth_buffer.getSize(), layout_depth_buffer.getAlignment()); + m_depth_buffer.initialize(layout_depth_buffer, m_depth_buffer_mem.getMemBlock(), m_depth_buffer_mem.getOffset()); + + /* Create layout for the framebuffers. */ + dk::ImageLayout layout_framebuffer; + dk::ImageLayoutMaker{m_device} + .setFlags(DkImageFlags_UsageRender | DkImageFlags_UsagePresent | DkImageFlags_HwCompression) + .setFormat(DkImageFormat_RGBA8_Unorm) + .setDimensions(FramebufferWidth, FramebufferHeight) + .initialize(layout_framebuffer); + + /* Create the framebuffers. */ + std::array fb_array; + const u64 fb_size = layout_framebuffer.getSize(); + const u32 fb_align = layout_framebuffer.getAlignment(); + + for (unsigned int i = 0; i < NumFramebuffers; i++) { + /* Allocate a framebuffer. */ + m_framebuffers_mem[i] = m_pool_images->allocate(fb_size, fb_align); + m_framebuffers[i].initialize(layout_framebuffer, m_framebuffers_mem[i].getMemBlock(), m_framebuffers_mem[i].getOffset()); + + /* Generate a command list that binds it. */ + dk::ImageView color_target{ m_framebuffers[i] }, depth_target{ m_depth_buffer }; + m_cmd_buf.bindRenderTargets(&color_target, &depth_target); + m_framebuffer_cmdlists[i] = m_cmd_buf.finishList(); + + /* Fill in the array for use later by the swapchain creation code. */ + fb_array[i] = &m_framebuffers[i]; + } + + /* Create the swapchain using the framebuffers. */ + m_swapchain = dk::SwapchainMaker{m_device, nwindowGetDefault(), fb_array}.create(); + + /* Generate the main rendering cmdlist. */ + this->RecordStaticCommands(); + } + + void DestroyFramebufferResources() { + /* Return early if we have nothing to destroy. */ + if (!m_swapchain) return; + + /* Make sure the queue is idle before destroying anything. */ + m_queue.waitIdle(); + + /* Clear the static cmdbuf, destroying the static cmdlists in the process. */ + m_cmd_buf.clear(); + + /* Destroy the swapchain. */ + m_swapchain.destroy(); + + /* Destroy the framebuffers. */ + for (unsigned int i = 0; i < NumFramebuffers; i ++) { + m_framebuffers_mem[i].destroy(); + } + + /* Destroy the depth buffer. */ + m_depth_buffer_mem.destroy(); + } + + void RecordStaticCommands() { + /* Initialize state structs with deko3d defaults. */ + dk::RasterizerState rasterizer_state; + dk::ColorState color_state; + dk::ColorWriteState color_write_state; + + /* Configure the viewport and scissor. */ + m_cmd_buf.setViewports(0, { { 0.0f, 0.0f, FramebufferWidth, FramebufferHeight, 0.0f, 1.0f } }); + m_cmd_buf.setScissors(0, { { 0, 0, FramebufferWidth, FramebufferHeight } }); + + /* Clear the color and depth buffers. */ + m_cmd_buf.clearColor(0, DkColorMask_RGBA, 0.f, 0.f, 0.f, 1.0f); + m_cmd_buf.clearDepthStencil(true, 1.0f, 0xFF, 0); + + /* Bind required state. */ + m_cmd_buf.bindRasterizerState(rasterizer_state); + m_cmd_buf.bindColorState(color_state); + m_cmd_buf.bindColorWriteState(color_write_state); + + m_render_cmdlist = m_cmd_buf.finishList(); + } + + void Render(u64 ns) { + /* Acquire a framebuffer from the swapchain (and wait for it to be available). */ + int slot = m_queue.acquireImage(m_swapchain); + + /* Run the command list that attaches said framebuffer to the queue. */ + m_queue.submitCommands(m_framebuffer_cmdlists[slot]); + + /* Run the main rendering command list. */ + m_queue.submitCommands(m_render_cmdlist); + + nvgBeginFrame(m_vg, FramebufferWidth, FramebufferHeight, 1.0f); + dbk::RenderMenu(m_vg, ns); + nvgEndFrame(m_vg); + + /* Now that we are done rendering, present it to the screen. */ + m_queue.presentImage(m_swapchain, slot); + } + + public: + bool onFrame(u64 ns) override { + dbk::UpdateMenu(ns); + this->Render(ns); + return !dbk::IsExitRequested(); + } +}; + +int main(int argc, char **argv) { + /* Initialize the menu. */ + dbk::InitializeMenu(FramebufferWidth, FramebufferHeight); + + Daybreak daybreak; + daybreak.run(); + return 0; +} diff --git a/troposphere/daybreak/source/service_guard.h b/troposphere/daybreak/source/service_guard.h new file mode 100644 index 000000000..5fbc5fca9 --- /dev/null +++ b/troposphere/daybreak/source/service_guard.h @@ -0,0 +1,56 @@ +#pragma once +#include +#include +#include +#include +#include + +typedef struct ServiceGuard { + Mutex mutex; + u32 refCount; +} ServiceGuard; + +NX_INLINE bool serviceGuardBeginInit(ServiceGuard* g) +{ + mutexLock(&g->mutex); + return (g->refCount++) == 0; +} + +NX_INLINE Result serviceGuardEndInit(ServiceGuard* g, Result rc, void (*cleanupFunc)(void)) +{ + if (R_FAILED(rc)) { + cleanupFunc(); + --g->refCount; + } + mutexUnlock(&g->mutex); + return rc; +} + +NX_INLINE void serviceGuardExit(ServiceGuard* g, void (*cleanupFunc)(void)) +{ + mutexLock(&g->mutex); + if (g->refCount && (--g->refCount) == 0) + cleanupFunc(); + mutexUnlock(&g->mutex); +} + +#define NX_GENERATE_SERVICE_GUARD_PARAMS(name, _paramdecl, _parampass) \ +\ +static ServiceGuard g_##name##Guard; \ +NX_INLINE Result _##name##Initialize _paramdecl; \ +static void _##name##Cleanup(void); \ +\ +Result name##Initialize _paramdecl \ +{ \ + Result rc = 0; \ + if (serviceGuardBeginInit(&g_##name##Guard)) \ + rc = _##name##Initialize _parampass; \ + return serviceGuardEndInit(&g_##name##Guard, rc, _##name##Cleanup); \ +} \ +\ +void name##Exit(void) \ +{ \ + serviceGuardExit(&g_##name##Guard, _##name##Cleanup); \ +} + +#define NX_GENERATE_SERVICE_GUARD(name) NX_GENERATE_SERVICE_GUARD_PARAMS(name, (void), ()) \ No newline at end of file diff --git a/troposphere/daybreak/source/ui.cpp b/troposphere/daybreak/source/ui.cpp new file mode 100644 index 000000000..4bae0d5f4 --- /dev/null +++ b/troposphere/daybreak/source/ui.cpp @@ -0,0 +1,966 @@ +/* + * Copyright (c) 2020 Adubbz + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope 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 . + */ +#include +#include +#include +#include +#include "ui.hpp" +#include "ui_util.hpp" +#include "assert.hpp" + +namespace dbk { + + namespace { + + u32 g_screen_width; + u32 g_screen_height; + + std::shared_ptr g_current_menu; + bool g_initialized = false; + bool g_exit_requested = false; + + u32 g_prev_touch_count = -1; + touchPosition g_start_touch_position; + bool g_started_touching = false; + bool g_tapping = false; + bool g_touches_moving = false; + bool g_finished_touching = false; + + /* Update install state. */ + char g_update_path[FS_MAX_PATH]; + bool g_use_exfat = false; + + constexpr u32 MaxTapMovement = 20; + + void UpdateInput() { + /* Update the previous touch count. */ + g_prev_touch_count = hidTouchCount(); + + /* Scan for input and update touch state. */ + hidScanInput(); + const u32 touch_count = hidTouchCount(); + + if (g_prev_touch_count == 0 && touch_count > 0) { + hidTouchRead(&g_start_touch_position, 0); + g_started_touching = true; + g_tapping = true; + } else { + g_started_touching = false; + } + + if (g_prev_touch_count > 0 && touch_count == 0) { + g_finished_touching = true; + g_tapping = false; + } else { + g_finished_touching = false; + } + + /* Check if currently moving. */ + if (g_prev_touch_count > 0 && touch_count > 0) { + touchPosition current_touch_position; + hidTouchRead(¤t_touch_position, 0); + + if ((abs(current_touch_position.px - g_start_touch_position.px) > MaxTapMovement || abs(current_touch_position.py - g_start_touch_position.py) > MaxTapMovement)) { + g_touches_moving = true; + g_tapping = false; + } else { + g_touches_moving = false; + } + } else { + g_touches_moving = false; + } + } + + void ChangeMenu(std::shared_ptr menu) { + g_current_menu = menu; + } + + void ReturnToPreviousMenu() { + /* Go to the previous menu if there is one. */ + if (g_current_menu->GetPrevMenu() != nullptr) { + g_current_menu = g_current_menu->GetPrevMenu(); + } + } + + Result IsPathBottomLevel(const char *path, bool *out) { + Result rc = 0; + FsFileSystem *fs; + char translated_path[FS_MAX_PATH] = {}; + DBK_ABORT_UNLESS(fsdevTranslatePath(path, &fs, translated_path) != -1); + + FsDir dir; + if (R_FAILED(rc = fsFsOpenDirectory(fs, translated_path, FsDirOpenMode_ReadDirs, &dir))) { + return rc; + } + + s64 entry_count; + if (R_FAILED(rc = fsDirGetEntryCount(&dir, &entry_count))) { + return rc; + } + + *out = entry_count == 0; + fsDirClose(&dir); + return rc; + } + + } + + void Menu::AddButton(u32 id, const char *text, float x, float y, float w, float h) { + DBK_ABORT_UNLESS(id < MaxButtons); + Button button = { + .id = id, + .selected = false, + .enabled = true, + .x = x, + .y = y, + .w = w, + .h = h, + }; + + strncpy(button.text, text, sizeof(button.text)-1); + m_buttons[id] = button; + } + + void Menu::SetButtonSelected(u32 id, bool selected) { + DBK_ABORT_UNLESS(id < MaxButtons); + auto &button = m_buttons[id]; + + if (button) { + button->selected = selected; + } + } + + void Menu::DeselectAllButtons() { + for (auto &button : m_buttons) { + /* Ensure button is present. */ + if (!button) { + continue; + } + button->selected = false; + } + } + + void Menu::SetButtonEnabled(u32 id, bool enabled) { + DBK_ABORT_UNLESS(id < MaxButtons); + auto &button = m_buttons[id]; + button->enabled = enabled; + } + + Button *Menu::GetButton(u32 id) { + DBK_ABORT_UNLESS(id < MaxButtons); + return !m_buttons[id] ? nullptr : &(*m_buttons[id]); + } + + Button *Menu::GetSelectedButton() { + for (auto &button : m_buttons) { + if (button && button->enabled && button->selected) { + return &(*button); + } + } + + return nullptr; + } + + Button *Menu::GetClosestButtonToSelection(Direction direction) { + const Button *selected_button = this->GetSelectedButton(); + + if (selected_button == nullptr || direction == Direction::Invalid) { + return nullptr; + } + + Button *closest_button = nullptr; + float closest_distance = 0.0f; + + for (auto &button : m_buttons) { + /* Skip absent button. */ + if (!button || !button->enabled) { + continue; + } + + /* Skip buttons that are in the wrong direction. */ + if ((direction == Direction::Down && button->y <= selected_button->y) || + (direction == Direction::Up && button->y >= selected_button->y) || + (direction == Direction::Right && button->x <= selected_button->x) || + (direction == Direction::Left && button->x >= selected_button->x)) { + continue; + } + + const float x_dist = button->x - selected_button->x; + const float y_dist = button->y - selected_button->y; + const float sq_dist = x_dist * x_dist + y_dist * y_dist; + + /* If we don't already have a closest button, set it. */ + if (closest_button == nullptr) { + closest_button = &(*button); + closest_distance = sq_dist; + continue; + } + + /* Update the closest button if this one is closer. */ + if (sq_dist < closest_distance) { + closest_button = &(*button); + closest_distance = sq_dist; + } + } + + return closest_button; + } + + Button *Menu::GetTouchedButton() { + touchPosition touch; + const u32 touch_count = hidTouchCount(); + + for (u32 i = 0; i < touch_count && g_started_touching; i++) { + hidTouchRead(&touch, i); + + for (auto &button : m_buttons) { + if (button && button->enabled && button->IsPositionInBounds(touch.px, touch.py)) { + return &(*button); + } + } + } + + return nullptr; + } + + Button *Menu::GetActivatedButton() { + Button *selected_button = this->GetSelectedButton(); + + if (selected_button == nullptr) { + return nullptr; + } + + const u64 k_down = hidKeysDown(CONTROLLER_P1_AUTO); + + if (k_down & KEY_A || this->GetTouchedButton() == selected_button) { + return selected_button; + } + + return nullptr; + } + + void Menu::UpdateButtons() { + const u64 k_down = hidKeysDown(CONTROLLER_P1_AUTO); + Direction direction = Direction::Invalid; + + if (k_down & KEY_DOWN) { + direction = Direction::Down; + } else if (k_down & KEY_UP) { + direction = Direction::Up; + } else if (k_down & KEY_LEFT) { + direction = Direction::Left; + } else if (k_down & KEY_RIGHT) { + direction = Direction::Right; + } + + /* Select the closest button. */ + if (const Button *closest_button = this->GetClosestButtonToSelection(direction); closest_button != nullptr) { + this->DeselectAllButtons(); + this->SetButtonSelected(closest_button->id, true); + } + + /* Select the touched button. */ + if (const Button *touched_button = this->GetTouchedButton(); touched_button != nullptr) { + this->DeselectAllButtons(); + this->SetButtonSelected(touched_button->id, true); + } + } + + void Menu::DrawButtons(NVGcontext *vg, u64 ns) { + for (auto &button : m_buttons) { + /* Ensure button is present. */ + if (!button) { + continue; + } + + /* Set the button style. */ + auto style = ButtonStyle::StandardDisabled; + if (button->enabled) { + style = button->selected ? ButtonStyle::StandardSelected : ButtonStyle::Standard; + } + + DrawButton(vg, button->text, button->x, button->y, button->w, button->h, style, ns); + } + } + + void Menu::LogText(const char *format, ...) { + /* Create a temporary string. */ + char tmp[0x100]; + va_list args; + va_start(args, format); + vsnprintf(tmp, sizeof(tmp)-1, format, args); + va_end(args); + + /* Append the text to the log buffer. */ + strncat(m_log_buffer, tmp, sizeof(m_log_buffer)-1); + } + + std::shared_ptr Menu::GetPrevMenu() { + return m_prev_menu; + } + + MainMenu::MainMenu() : Menu(nullptr) { + const float x = g_screen_width / 2.0f - WindowWidth / 2.0f; + const float y = g_screen_height / 2.0f - WindowHeight / 2.0f; + + this->AddButton(InstallButtonId, "Install", x + ButtonHorizontalPadding, y + TitleGap, WindowWidth - ButtonHorizontalPadding * 2, ButtonHeight); + this->AddButton(ExitButtonId, "Exit", x + ButtonHorizontalPadding, y + TitleGap + ButtonHeight + ButtonVerticalGap, WindowWidth - ButtonHorizontalPadding * 2, ButtonHeight); + this->SetButtonSelected(InstallButtonId, true); + } + + void MainMenu::Update(u64 ns) { + u64 k_down = hidKeysDown(CONTROLLER_P1_AUTO); + + if (k_down & KEY_B) { + g_exit_requested = true; + } + + /* Take action if a button has been activated. */ + if (const Button *activated_button = this->GetActivatedButton(); activated_button != nullptr) { + switch (activated_button->id) { + case InstallButtonId: + ChangeMenu(std::make_shared(g_current_menu, "/")); + break; + case ExitButtonId: + g_exit_requested = true; + break; + } + } + + this->UpdateButtons(); + + /* Fallback on selecting the install button. */ + if (const Button *selected_button = this->GetSelectedButton(); k_down && selected_button == nullptr) { + this->SetButtonSelected(InstallButtonId, true); + } + } + + void MainMenu::Draw(NVGcontext *vg, u64 ns) { + DrawWindow(vg, "Daybreak", g_screen_width / 2.0f - WindowWidth / 2.0f, g_screen_height / 2.0f - WindowHeight / 2.0f, WindowWidth, WindowHeight); + this->DrawButtons(vg, ns); + } + + FileMenu::FileMenu(std::shared_ptr prev_menu, const char *root) : Menu(prev_menu), m_current_index(0), m_scroll_offset(0), m_touch_start_scroll_offset(0), m_touch_finalize_selection(false) { + Result rc = 0; + + strncpy(m_root, root, sizeof(m_root)-1); + + if (R_FAILED(rc = this->PopulateFileEntries())) { + fatalThrow(rc); + } + } + + Result FileMenu::PopulateFileEntries() { + /* Open the directory. */ + DIR *dir = opendir(m_root); + if (dir == nullptr) { + return fsdevGetLastResult(); + } + + /* Add file entries to the list. */ + struct dirent *ent; + while ((ent = readdir(dir)) != nullptr) { + if (ent->d_type == DT_DIR) { + FileEntry file_entry = {}; + strncpy(file_entry.name, ent->d_name, sizeof(file_entry.name)); + m_file_entries.push_back(file_entry); + } + } + + /* Close the directory. */ + closedir(dir); + return 0; + } + + bool FileMenu::IsSelectionVisible() { + const float visible_start = m_scroll_offset; + const float visible_end = visible_start + FileListHeight; + const float entry_start = static_cast(m_current_index) * (FileRowHeight + FileRowGap); + const float entry_end = entry_start + (FileRowHeight + FileRowGap); + return entry_start >= visible_start && entry_end <= visible_end; + } + + void FileMenu::ScrollToSelection() { + const float visible_start = m_scroll_offset; + const float visible_end = visible_start + FileListHeight; + const float entry_start = static_cast(m_current_index) * (FileRowHeight + FileRowGap); + const float entry_end = entry_start + (FileRowHeight + FileRowGap); + + if (entry_end > visible_end) { + m_scroll_offset += entry_end - visible_end; + } else if (entry_end < visible_end) { + m_scroll_offset = entry_start; + } + } + + bool FileMenu::IsEntryTouched(u32 i) { + const float x = g_screen_width / 2.0f - WindowWidth / 2.0f; + const float y = g_screen_height / 2.0f - WindowHeight / 2.0f; + + touchPosition current_pos; + hidTouchRead(¤t_pos, 0); + + /* Check if the tap is within the x bounds. */ + if (current_pos.px >= x + TextBackgroundOffset + FileRowHorizontalInset && current_pos.px <= WindowWidth - (TextBackgroundOffset + FileRowHorizontalInset) * 2.0f) { + const float y_min = y + TitleGap + FileRowGap + i * (FileRowHeight + FileRowGap) - m_scroll_offset; + const float y_max = y_min + FileRowHeight; + + /* Check if the tap is within the y bounds. */ + if (current_pos.py >= y_min && current_pos.py <= y_max) { + return true; + } + } + + return false; + } + + void FileMenu::UpdateTouches() { + /* Setup values on initial touch. */ + if (g_started_touching) { + m_touch_start_scroll_offset = m_scroll_offset; + + /* We may potentially finalize the selection later if we start off touching it. */ + if (this->IsEntryTouched(m_current_index)) { + m_touch_finalize_selection = true; + } + } + + /* Scroll based on touch movement. */ + if (g_touches_moving) { + touchPosition current_pos; + hidTouchRead(¤t_pos, 0); + + const int dist_y = current_pos.py - g_start_touch_position.py; + float new_scroll_offset = m_touch_start_scroll_offset - static_cast(dist_y); + float max_scroll = (FileRowHeight + FileRowGap) * static_cast(m_file_entries.size()) - FileListHeight; + + /* Don't allow scrolling if there is not enough elements. */ + if (max_scroll < 0.0f) { + max_scroll = 0.0f; + } + + /* Don't allow scrolling before the first element. */ + if (new_scroll_offset < 0.0f) { + new_scroll_offset = 0.0f; + } + + /* Don't allow scrolling past the last element. */ + if (new_scroll_offset > max_scroll) { + new_scroll_offset = max_scroll; + } + + m_scroll_offset = new_scroll_offset; + } + + /* Select any tapped entries. */ + if (g_tapping) { + for (u32 i = 0; i < m_file_entries.size(); i++) { + if (this->IsEntryTouched(i)) { + /* The current index is checked later. */ + if (i == m_current_index) { + continue; + } + + m_current_index = i; + + /* Don't finalize selection if we touch something else. */ + m_touch_finalize_selection = false; + break; + } + } + } + + /* Don't finalize selection if we aren't finished and we've either stopped tapping or are no longer touching the selection. */ + if (!g_finished_touching && (!g_tapping || !this->IsEntryTouched(m_current_index))) { + m_touch_finalize_selection = false; + } + + /* Finalize selection if the currently selected entry is touched for the second time. */ + if (g_finished_touching && m_touch_finalize_selection) { + this->FinalizeSelection(); + m_touch_finalize_selection = false; + } + } + + void FileMenu::FinalizeSelection() { + DBK_ABORT_UNLESS(m_current_index < m_file_entries.size()); + FileEntry &entry = m_file_entries[m_current_index]; + + /* Determine the selected path. */ + char current_path[FS_MAX_PATH] = {}; + snprintf(current_path, sizeof(current_path)-1, "%s%s", m_root, entry.name); + + /* Determine if the chosen path is the bottom level. */ + Result rc = 0; + bool bottom_level; + if (R_FAILED(rc = IsPathBottomLevel(current_path, &bottom_level))) { + fatalThrow(rc); + } + + /* Show exfat settings or the next file menu. */ + if (bottom_level) { + /* Set the update path. */ + snprintf(g_update_path, sizeof(g_update_path)-1, "%s", current_path); + + /* Change the menu. */ + ChangeMenu(std::make_shared(g_current_menu)); + } else { + ChangeMenu(std::make_shared(g_current_menu, current_path)); + } + } + + void FileMenu::Update(u64 ns) { + u64 k_down = hidKeysDown(CONTROLLER_P1_AUTO); + + /* Go back if B is pressed. */ + if (k_down & KEY_B) { + ReturnToPreviousMenu(); + return; + } + + /* Finalize selection on pressing A. */ + if (k_down & KEY_A) { + this->FinalizeSelection(); + } + + /* Update touch input. */ + this->UpdateTouches(); + + const u32 prev_index = m_current_index; + + if (k_down & KEY_DOWN) { + /* Scroll down. */ + if (m_current_index >= (m_file_entries.size() - 1)) { + m_current_index = 0; + } else { + m_current_index++; + } + } else if (k_down & KEY_UP) { + /* Scroll up. */ + if (m_current_index == 0) { + m_current_index = m_file_entries.size() - 1; + } else { + m_current_index--; + } + } + + /* Scroll to the selection if it isn't visible. */ + if (prev_index != m_current_index && !this->IsSelectionVisible()) { + this->ScrollToSelection(); + } + } + + void FileMenu::Draw(NVGcontext *vg, u64 ns) { + const float x = g_screen_width / 2.0f - WindowWidth / 2.0f; + const float y = g_screen_height / 2.0f - WindowHeight / 2.0f; + + DrawWindow(vg, "Select an update directory", x, y, WindowWidth, WindowHeight); + DrawTextBackground(vg, x + TextBackgroundOffset, y + TitleGap, WindowWidth - TextBackgroundOffset * 2.0f, (FileRowHeight + FileRowGap) * MaxFileRows + FileRowGap); + + nvgSave(vg); + nvgScissor(vg, x + TextBackgroundOffset, y + TitleGap, WindowWidth - TextBackgroundOffset * 2.0f, (FileRowHeight + FileRowGap) * MaxFileRows + FileRowGap); + + for (u32 i = 0; i < m_file_entries.size(); i++) { + FileEntry &entry = m_file_entries[i]; + auto style = ButtonStyle::FileSelect; + + if (i == m_current_index) { + style = ButtonStyle::FileSelectSelected; + } + + DrawButton(vg, entry.name, x + TextBackgroundOffset + FileRowHorizontalInset, y + TitleGap + FileRowGap + i * (FileRowHeight + FileRowGap) - m_scroll_offset, WindowWidth - (TextBackgroundOffset + FileRowHorizontalInset) * 2.0f, FileRowHeight, style, ns); + } + + nvgRestore(vg); + } + + ValidateUpdateMenu::ValidateUpdateMenu(std::shared_ptr prev_menu) : Menu(prev_menu), m_has_drawn(false), m_has_info(false), m_has_validated(false) { + const float x = g_screen_width / 2.0f - WindowWidth / 2.0f; + const float y = g_screen_height / 2.0f - WindowHeight / 2.0f; + + /* Add buttons. */ + this->AddButton(BackButtonId, "Back", x + HorizontalGap, y + WindowHeight - BottomGap - ButtonHeight, ButtonWidth, ButtonHeight); + this->AddButton(ContinueButtonId, "Continue", x + HorizontalGap + ButtonWidth + ButtonHorizontalGap, y + WindowHeight - BottomGap - ButtonHeight, ButtonWidth, ButtonHeight); + this->SetButtonEnabled(BackButtonId, false); + this->SetButtonEnabled(ContinueButtonId, false); + + /* Obtain update information. */ + if (R_FAILED(this->GetUpdateInformation())) { + this->SetButtonEnabled(BackButtonId, true); + this->SetButtonSelected(BackButtonId, true); + } else { + /* Log this early so it is printed out before validation causes stalling. */ + this->LogText("Validating update, this may take a moment...\n"); + } + } + + Result ValidateUpdateMenu::GetUpdateInformation() { + Result rc = 0; + this->LogText("Directory %s\n", g_update_path); + + /* Attempt to get the update information. */ + if (R_FAILED(rc = amssuGetUpdateInformation(&m_update_info, g_update_path))) { + if (rc == 0x1a405) { + this->LogText("No update found in folder.\nEnsure your ncas are named correctly!\nResult: 0x%08x\n", rc); + } else { + this->LogText("Failed to get update information.\nResult: 0x%08x\n", rc); + } + return rc; + } + + /* Print update information. */ + this->LogText("- Version: %d.%d.%d\n", (m_update_info.version >> 26) & 0x1f, (m_update_info.version >> 20) & 0x1f, (m_update_info.version >> 16) & 0xf); + if (m_update_info.exfat_supported) { + this->LogText("- exFAT: Supported\n"); + } else { + this->LogText("- exFAT: Unsupported\n"); + } + this->LogText("- Firmware variations: %d\n", m_update_info.num_firmware_variations); + + /* Mark as having obtained update info. */ + m_has_info = true; + return rc; + } + + void ValidateUpdateMenu::ValidateUpdate() { + Result rc = 0; + + /* Validate the update. */ + if (R_FAILED(rc = amssuValidateUpdate(&m_validation_info, g_update_path))) { + this->LogText("Failed to validate update.\nResult: 0x%08x\n", rc); + return; + } + + /* Check the result. */ + if (R_SUCCEEDED(m_validation_info.result)) { + this->LogText("Update is valid!\n"); + + /* Enable the back and continue buttons and select the continue button. */ + this->SetButtonEnabled(BackButtonId, true); + this->SetButtonEnabled(ContinueButtonId, true); + this->SetButtonSelected(ContinueButtonId, true); + } else { + /* Log the missing content info. */ + const u32 version = m_validation_info.invalid_key.version; + this->LogText("Validation failed with result: 0x%08x\n", m_validation_info.result); + this->LogText("Missing content:\n- Program id: %016lx\n- Version: %d.%d.%d\n", m_validation_info.invalid_key.id, (version >> 26) & 0x1f, (version >> 20) & 0x1f, (version >> 16) & 0xf); + + /* Log the missing content id. */ + this->LogText("- Content id: "); + for (size_t i = 0; i < sizeof(NcmContentId); i++) { + this->LogText("%02x", m_validation_info.invalid_content_id.c[i]); + } + this->LogText("\n"); + + /* Enable the back button and select it. */ + this->SetButtonEnabled(BackButtonId, true); + this->SetButtonSelected(BackButtonId, true); + } + + /* Mark validation as being complete. */ + m_has_validated = true; + } + + void ValidateUpdateMenu::Update(u64 ns) { + /* Perform validation if it hasn't been done already. */ + if (m_has_info && m_has_drawn && !m_has_validated) { + this->ValidateUpdate(); + } + + u64 k_down = hidKeysDown(CONTROLLER_P1_AUTO); + + /* Go back if B is pressed. */ + if (k_down & KEY_B) { + ReturnToPreviousMenu(); + return; + } + + /* Take action if a button has been activated. */ + if (const Button *activated_button = this->GetActivatedButton(); activated_button != nullptr) { + switch (activated_button->id) { + case BackButtonId: + ReturnToPreviousMenu(); + return; + case ContinueButtonId: + /* Don't continue if validation hasn't been done or has failed. */ + if (!m_has_validated || R_FAILED(m_validation_info.result)) { + break; + } + + if (m_update_info.exfat_supported) { + ChangeMenu(std::make_shared(g_current_menu)); + } else { + g_use_exfat = false; + ChangeMenu(std::make_shared(g_current_menu)); + } + + return; + } + } + + this->UpdateButtons(); + } + + void ValidateUpdateMenu::Draw(NVGcontext *vg, u64 ns) { + const float x = g_screen_width / 2.0f - WindowWidth / 2.0f; + const float y = g_screen_height / 2.0f - WindowHeight / 2.0f; + + DrawWindow(vg, "Update information", x, y, WindowWidth, WindowHeight); + DrawTextBackground(vg, x + HorizontalGap, y + TitleGap, WindowWidth - HorizontalGap * 2.0f, TextAreaHeight); + DrawTextBlock(vg, m_log_buffer, x + HorizontalGap + TextHorizontalInset, y + TitleGap + TextVerticalInset, WindowWidth - (HorizontalGap + TextHorizontalInset) * 2.0f, TextAreaHeight - TextVerticalInset * 2.0f); + + this->DrawButtons(vg, ns); + m_has_drawn = true; + } + + ChooseExfatMenu::ChooseExfatMenu(std::shared_ptr prev_menu) : Menu(prev_menu) { + const float x = g_screen_width / 2.0f - WindowWidth / 2.0f; + const float y = g_screen_height / 2.0f - WindowHeight / 2.0f; + + this->AddButton(Fat32ButtonId, "FAT32", x + ButtonHorizontalInset, y + TitleGap, ButtonWidth, ButtonHeight); + this->AddButton(ExFatButtonId, "exFAT", x + ButtonHorizontalInset + ButtonWidth + ButtonHorizontalGap, y + TitleGap, ButtonWidth, ButtonHeight); + this->SetButtonSelected(ExFatButtonId, true); + } + + void ChooseExfatMenu::Update(u64 ns) { + u64 k_down = hidKeysDown(CONTROLLER_P1_AUTO); + + /* Go back if B is pressed. */ + if (k_down & KEY_B) { + ReturnToPreviousMenu(); + return; + } + + /* Take action if a button has been activated. */ + if (const Button *activated_button = this->GetActivatedButton(); activated_button != nullptr) { + switch (activated_button->id) { + case Fat32ButtonId: + g_use_exfat = false; + break; + case ExFatButtonId: + g_use_exfat = true; + break; + } + + ChangeMenu(std::make_shared(g_current_menu)); + } + + this->UpdateButtons(); + + /* Fallback on selecting the exfat button. */ + if (const Button *selected_button = this->GetSelectedButton(); k_down && selected_button == nullptr) { + this->SetButtonSelected(ExFatButtonId, true); + } + } + + void ChooseExfatMenu::Draw(NVGcontext *vg, u64 ns) { + const float x = g_screen_width / 2.0f - WindowWidth / 2.0f; + const float y = g_screen_height / 2.0f - WindowHeight / 2.0f; + + DrawWindow(vg, "Select driver variant", x, y, WindowWidth, WindowHeight); + this->DrawButtons(vg, ns); + } + + InstallUpdateMenu::InstallUpdateMenu(std::shared_ptr prev_menu) : Menu(prev_menu), m_install_state(InstallState::NeedsDraw), m_progress_percent(0.0f) { + const float x = g_screen_width / 2.0f - WindowWidth / 2.0f; + const float y = g_screen_height / 2.0f - WindowHeight / 2.0f; + + /* Add buttons. */ + this->AddButton(ShutdownButtonId, "Shutdown", x + HorizontalGap, y + WindowHeight - BottomGap - ButtonHeight, ButtonWidth, ButtonHeight); + this->AddButton(RebootButtonId, "Reboot", x + HorizontalGap + ButtonWidth + ButtonHorizontalGap, y + WindowHeight - BottomGap - ButtonHeight, ButtonWidth, ButtonHeight); + this->SetButtonEnabled(ShutdownButtonId, false); + this->SetButtonEnabled(RebootButtonId, false); + } + + void InstallUpdateMenu::MarkForReboot() { + this->SetButtonEnabled(ShutdownButtonId, true); + this->SetButtonEnabled(RebootButtonId, true); + this->SetButtonSelected(RebootButtonId, true); + m_install_state = InstallState::AwaitingReboot; + } + + Result InstallUpdateMenu::TransitionUpdateState() { + Result rc = 0; + if (m_install_state == InstallState::NeedsSetup) { + /* Setup the update. */ + if (R_FAILED(rc = amssuSetupUpdate(nullptr, UpdateTaskBufferSize, g_update_path, g_use_exfat))) { + this->LogText("Failed to setup update.\nResult: 0x%08x\n", rc); + this->MarkForReboot(); + return rc; + } + + /* Log setup completion. */ + this->LogText("Update setup complete.\n"); + m_install_state = InstallState::NeedsPrepare; + } else if (m_install_state == InstallState::NeedsPrepare) { + /* Request update preparation. */ + if (R_FAILED(rc = amssuRequestPrepareUpdate(&m_prepare_result))) { + this->LogText("Failed to request update preparation.\nResult: 0x%08x\n", rc); + this->MarkForReboot(); + return rc; + } + + /* Log awaiting prepare. */ + this->LogText("Preparing update...\n"); + m_install_state = InstallState::AwaitingPrepare; + } else if (m_install_state == InstallState::AwaitingPrepare) { + /* Check if preparation has a result. */ + if (R_FAILED(rc = asyncResultWait(&m_prepare_result, 0)) && rc != 0xea01) { + this->LogText("Failed to check update preparation result.\nResult: 0x%08x\n", rc); + this->MarkForReboot(); + return rc; + } else if (R_SUCCEEDED(rc)) { + if (R_FAILED(rc = asyncResultGet(&m_prepare_result))) { + this->LogText("Failed to prepare update.\nResult: 0x%08x\n", rc); + this->MarkForReboot(); + return rc; + } + } + + /* Check if the update has been prepared. */ + bool prepared; + if (R_FAILED(rc = amssuHasPreparedUpdate(&prepared))) { + this->LogText("Failed to check if update has been prepared.\nResult: 0x%08x\n", rc); + this->MarkForReboot(); + return rc; + } + + /* Mark for application if preparation complete. */ + if (prepared) { + this->LogText("Update preparation complete.\nApplying update...\n"); + m_install_state = InstallState::NeedsApply; + return rc; + } + + /* Check update progress. */ + NsSystemUpdateProgress update_progress = {}; + if (R_FAILED(rc = amssuGetPrepareUpdateProgress(&update_progress))) { + this->LogText("Failed to check update progress.\nResult: 0x%08x\n", rc); + this->MarkForReboot(); + return rc; + } + + /* Update progress percent. */ + if (update_progress.total_size > 0.0f) { + m_progress_percent = static_cast(update_progress.current_size) / static_cast(update_progress.total_size); + } else { + m_progress_percent = 0.0f; + } + } else if (m_install_state == InstallState::NeedsApply) { + /* Apply the prepared update. */ + if (R_FAILED(rc = amssuApplyPreparedUpdate())) { + this->LogText("Failed to apply update.\nResult: 0x%08x\n", rc); + } + + /* Log success. */ + this->LogText("Update applied successfully.\n"); + this->MarkForReboot(); + return rc; + } + + return rc; + } + + void InstallUpdateMenu::Update(u64 ns) { + /* Transition to the next update state. */ + if (m_install_state != InstallState::NeedsDraw && m_install_state != InstallState::AwaitingReboot) { + this->TransitionUpdateState(); + } + + /* Take action if a button has been activated. */ + if (const Button *activated_button = this->GetActivatedButton(); activated_button != nullptr) { + switch (activated_button->id) { + case ShutdownButtonId: + if (R_FAILED(appletRequestToShutdown())) { + spsmShutdown(false); + } + break; + case RebootButtonId: + if (R_FAILED(appletRequestToReboot())) { + spsmShutdown(true); + } + break; + } + } + + this->UpdateButtons(); + } + + void InstallUpdateMenu::Draw(NVGcontext *vg, u64 ns) { + const float x = g_screen_width / 2.0f - WindowWidth / 2.0f; + const float y = g_screen_height / 2.0f - WindowHeight / 2.0f; + + DrawWindow(vg, "Installing update", x, y, WindowWidth, WindowHeight); + DrawProgressText(vg, x + HorizontalGap, y + TitleGap, m_progress_percent); + DrawProgressBar(vg, x + HorizontalGap, y + TitleGap + ProgressTextHeight, WindowWidth - HorizontalGap * 2.0f, ProgressBarHeight, m_progress_percent); + DrawTextBackground(vg, x + HorizontalGap, y + TitleGap + ProgressTextHeight + ProgressBarHeight + VerticalGap, WindowWidth - HorizontalGap * 2.0f, TextAreaHeight); + DrawTextBlock(vg, m_log_buffer, x + HorizontalGap + TextHorizontalInset, y + TitleGap + ProgressTextHeight + ProgressBarHeight + VerticalGap + TextVerticalInset, WindowWidth - (HorizontalGap + TextHorizontalInset) * 2.0f, TextAreaHeight - TextVerticalInset * 2.0f); + + this->DrawButtons(vg, ns); + + /* We have drawn now, allow setup to occur. */ + if (m_install_state == InstallState::NeedsDraw) { + this->LogText("Beginning update setup...\n"); + m_install_state = InstallState::NeedsSetup; + } + } + + void InitializeMenu(u32 screen_width, u32 screen_height) { + /* Set the screen width and height. */ + g_screen_width = screen_width; + g_screen_height = screen_height; + + /* Change the current menu to the main menu. */ + g_current_menu = std::make_shared(); + + /* Mark as initialized. */ + g_initialized = true; + } + + void UpdateMenu(u64 ns) { + DBK_ABORT_UNLESS(g_initialized); + DBK_ABORT_UNLESS(g_current_menu != nullptr); + UpdateInput(); + g_current_menu->Update(ns); + } + + void RenderMenu(NVGcontext *vg, u64 ns) { + DBK_ABORT_UNLESS(g_initialized); + DBK_ABORT_UNLESS(g_current_menu != nullptr); + + /* Draw background. */ + DrawBackground(vg, g_screen_width, g_screen_height); + + /* Draw stars. */ + DrawStar(vg, 40.0f, 64.0f, 3.0f); + DrawStar(vg, 110.0f, 300.0f, 3.0f); + DrawStar(vg, 200.0f, 150.0f, 4.0f); + DrawStar(vg, 370.0f, 280.0f, 3.0f); + DrawStar(vg, 450.0f, 40.0f, 3.5f); + DrawStar(vg, 710.0f, 90.0f, 3.0f); + DrawStar(vg, 900.0f, 240.0f, 3.0f); + DrawStar(vg, 970.0f, 64.0f, 4.0f); + DrawStar(vg, 1160.0f, 160.0f, 3.5f); + DrawStar(vg, 1210.0f, 350.0f, 3.0f); + + g_current_menu->Draw(vg, ns); + } + + bool IsExitRequested() { + return g_exit_requested; + } + +} diff --git a/troposphere/daybreak/source/ui.hpp b/troposphere/daybreak/source/ui.hpp new file mode 100644 index 000000000..8afa43b16 --- /dev/null +++ b/troposphere/daybreak/source/ui.hpp @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2020 Adubbz + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope 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 . + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include "ams_su.h" + +namespace dbk { + + struct Button { + static constexpr u32 InvalidButtonId = -1; + + u32 id; + bool selected; + bool enabled; + char text[256]; + float x; + float y; + float w; + float h; + + inline bool IsPositionInBounds(float x, float y) { + return x >= this->x && y >= this->y && x < (this->x + this->w) && y < (this->y + this->h); + } + }; + + enum class Direction { + Up, + Down, + Left, + Right, + Invalid, + }; + + class Menu { + protected: + static constexpr size_t MaxButtons = 32; + static constexpr size_t LogBufferSize = 0x1000; + protected: + std::array, MaxButtons> m_buttons; + const std::shared_ptr m_prev_menu; + char m_log_buffer[LogBufferSize]; + protected: + void AddButton(u32 id, const char *text, float x, float y, float w, float h); + void SetButtonSelected(u32 id, bool selected); + void DeselectAllButtons(); + void SetButtonEnabled(u32 id, bool enabled); + + Button *GetButton(u32 id); + Button *GetSelectedButton(); + Button *GetClosestButtonToSelection(Direction direction); + Button *GetTouchedButton(); + Button *GetActivatedButton(); + + void UpdateButtons(); + void DrawButtons(NVGcontext *vg, u64 ns); + + void LogText(const char *format, ...); + public: + Menu(std::shared_ptr prev_menu) : m_buttons({}), m_prev_menu(prev_menu), m_log_buffer{} { /* ... */ } + + std::shared_ptr GetPrevMenu(); + virtual void Update(u64 ns) = 0; + virtual void Draw(NVGcontext *vg, u64 ns) = 0; + }; + + class MainMenu : public Menu { + private: + static constexpr u32 InstallButtonId = 0; + static constexpr u32 ExitButtonId = 1; + + static constexpr float WindowWidth = 400.0f; + static constexpr float WindowHeight = 240.0f; + static constexpr float TitleGap = 90.0f; + static constexpr float ButtonHorizontalPadding = 20.0f; + static constexpr float ButtonHeight = 60.0f; + static constexpr float ButtonVerticalGap = 10.0f; + public: + MainMenu(); + + virtual void Update(u64 ns) override; + virtual void Draw(NVGcontext *vg, u64 ns) override; + }; + + class FileMenu : public Menu { + private: + struct FileEntry { + char name[FS_MAX_PATH]; + }; + private: + static constexpr size_t MaxFileRows = 11; + + static constexpr float WindowWidth = 1200.0f; + static constexpr float WindowHeight = 680.0f; + static constexpr float TitleGap = 90.0f; + static constexpr float TextBackgroundOffset = 20.0f; + static constexpr float FileRowHeight = 40.0f; + static constexpr float FileRowGap = 10.0f; + static constexpr float FileRowHorizontalInset = 10.0f; + static constexpr float FileListHeight = MaxFileRows * (FileRowHeight + FileRowGap); + private: + char m_root[FS_MAX_PATH]; + std::vector m_file_entries; + u32 m_current_index; + float m_scroll_offset; + float m_touch_start_scroll_offset; + bool m_touch_finalize_selection; + + Result PopulateFileEntries(); + bool IsSelectionVisible(); + void ScrollToSelection(); + bool IsEntryTouched(u32 i); + void UpdateTouches(); + void FinalizeSelection(); + public: + FileMenu(std::shared_ptr prev_menu, const char *root); + + virtual void Update(u64 ns) override; + virtual void Draw(NVGcontext *vg, u64 ns) override; + }; + + class ValidateUpdateMenu : public Menu { + private: + static constexpr u32 BackButtonId = 0; + static constexpr u32 ContinueButtonId = 1; + + static constexpr float WindowWidth = 600.0f; + static constexpr float WindowHeight = 600.0f; + static constexpr float TitleGap = 90.0f; + static constexpr float BottomGap = 20.0f; + static constexpr float HorizontalGap = 20.0f; + static constexpr float TextAreaHeight = 410.0f; + static constexpr float TextHorizontalInset = 6.0f; + static constexpr float TextVerticalInset = 6.0f; + static constexpr float ButtonHeight = 60.0f; + static constexpr float ButtonHorizontalGap = 10.0f; + static constexpr float ButtonWidth = (WindowWidth - HorizontalGap * 2.0f) / 2.0f - ButtonHorizontalGap; + private: + AmsSuUpdateInformation m_update_info; + AmsSuUpdateValidationInfo m_validation_info; + bool m_has_drawn; + bool m_has_info; + bool m_has_validated; + + Result GetUpdateInformation(); + void ValidateUpdate(); + public: + ValidateUpdateMenu(std::shared_ptr prev_menu); + + virtual void Update(u64 ns) override; + virtual void Draw(NVGcontext *vg, u64 ns) override; + }; + + class ChooseExfatMenu : public Menu { + private: + static constexpr u32 Fat32ButtonId = 0; + static constexpr u32 ExFatButtonId = 1; + + static constexpr float WindowWidth = 600.0f; + static constexpr float WindowHeight = 180.0f; + static constexpr float TitleGap = 90.0f; + static constexpr float ButtonHeight = 60.0f; + static constexpr float ButtonHorizontalInset = 20.0f; + static constexpr float ButtonHorizontalGap = 10.0f; + static constexpr float ButtonWidth = (WindowWidth - ButtonHorizontalInset * 2.0f) / 2.0f - ButtonHorizontalGap; + public: + ChooseExfatMenu(std::shared_ptr prev_menu); + + virtual void Update(u64 ns) override; + virtual void Draw(NVGcontext *vg, u64 ns) override; + }; + + class InstallUpdateMenu : public Menu { + private: + enum class InstallState { + NeedsDraw, + NeedsSetup, + NeedsPrepare, + AwaitingPrepare, + NeedsApply, + AwaitingReboot, + }; + private: + static constexpr u32 ShutdownButtonId = 0; + static constexpr u32 RebootButtonId = 1; + + static constexpr float WindowWidth = 600.0f; + static constexpr float WindowHeight = 600.0f; + static constexpr float TitleGap = 120.0f; + static constexpr float BottomGap = 20.0f; + static constexpr float HorizontalGap = 20.0f; + static constexpr float ProgressTextHeight = 20.0f; + static constexpr float ProgressBarHeight = 30.0f; + static constexpr float VerticalGap = 10.0f; + static constexpr float TextAreaHeight = 320.0f; + static constexpr float TextHorizontalInset = 6.0f; + static constexpr float TextVerticalInset = 6.0f; + static constexpr float ButtonHeight = 60.0f; + static constexpr float ButtonHorizontalGap = 10.0f; + static constexpr float ButtonWidth = (WindowWidth - HorizontalGap * 2.0f) / 2.0f - ButtonHorizontalGap; + + static constexpr size_t UpdateTaskBufferSize = 0x100000; + private: + InstallState m_install_state; + AsyncResult m_prepare_result; + float m_progress_percent; + + void MarkForReboot(); + Result TransitionUpdateState(); + public: + InstallUpdateMenu(std::shared_ptr prev_menu); + + virtual void Update(u64 ns) override; + virtual void Draw(NVGcontext *vg, u64 ns) override; + }; + + void InitializeMenu(u32 screen_width, u32 screen_height); + void UpdateMenu(u64 ns); + void RenderMenu(NVGcontext *vg, u64 ns); + bool IsExitRequested(); + +} diff --git a/troposphere/daybreak/source/ui_util.cpp b/troposphere/daybreak/source/ui_util.cpp new file mode 100644 index 000000000..aa88c4971 --- /dev/null +++ b/troposphere/daybreak/source/ui_util.cpp @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2020 Adubbz + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope 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 . + */ +#include "ui_util.hpp" +#include +#include + +namespace dbk { + + namespace { + + constexpr const char *SwitchStandardFont = "switch-standard"; + constexpr float WindowCornerRadius = 20.0f; + constexpr float TextAreaCornerRadius = 10.0f; + constexpr float ButtonCornerRaidus = 3.0f; + + NVGcolor GetSelectionRGB2(u64 ns) { + /* Calculate the rgb values for the breathing colour effect. */ + const double t = static_cast(ns) / 1'000'000'000.0d; + const float d = -0.5 * cos(3.0f*t) + 0.5f; + const int r2 = 83 + (float)(144 - 83) * (d * 0.7f + 0.3f); + const int g2 = 71 + (float)(185 - 71) * (d * 0.7f + 0.3f); + const int b2 = 185 + (float)(217 - 185) * (d * 0.7f + 0.3f); + return nvgRGB(r2, g2, b2); + } + + } + + void DrawStar(NVGcontext *vg, float x, float y, float width) { + nvgBeginPath(vg); + nvgEllipse(vg, x, y, width, width * 3.0f); + nvgEllipse(vg, x, y, width * 3.0f, width); + nvgFillColor(vg, nvgRGB(65, 71, 115)); + nvgFill(vg); + } + + void DrawBackground(NVGcontext *vg, float w, float h) { + /* Draw the background gradient. */ + const NVGpaint bg_paint = nvgLinearGradient(vg, w / 2.0f, 0, w / 2.0f, h + 20.0f, nvgRGB(20, 24, 50), nvgRGB(46, 57, 127)); + nvgBeginPath(vg); + nvgRect(vg, 0, 0, w, h); + nvgFillPaint(vg, bg_paint); + nvgFill(vg); + } + + void DrawWindow(NVGcontext *vg, const char *title, float x, float y, float w, float h) { + /* Draw the window background. */ + const NVGpaint window_bg_paint = nvgLinearGradient(vg, x + w / 2.0f, y, x + w / 2.0f, y + h + h / 4.0f, nvgRGB(255, 255, 255), nvgRGB(188, 214, 234)); + nvgBeginPath(vg); + nvgRoundedRect(vg, x, y, w, h, WindowCornerRadius); + nvgFillPaint(vg, window_bg_paint); + nvgFill(vg); + + /* Draw the shadow surrounding the window. */ + NVGpaint shadowPaint = nvgBoxGradient(vg, x, y + 2, w, h, WindowCornerRadius * 2, 10, nvgRGBA(0, 0, 0, 128), nvgRGBA(0, 0, 0, 0)); + nvgBeginPath(vg); + nvgRect(vg, x - 10, y - 10, w + 20, h + 30); + nvgRoundedRect(vg, x, y, w, h, WindowCornerRadius); + nvgPathWinding(vg, NVG_HOLE); + nvgFillPaint(vg, shadowPaint); + nvgFill(vg); + + /* Setup the font. */ + nvgFontSize(vg, 32.0f); + nvgFontFace(vg, SwitchStandardFont); + nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE); + nvgFillColor(vg, nvgRGB(0, 0, 0)); + + /* Draw the title. */ + const float tw = nvgTextBounds(vg, 0, 0, title, nullptr, nullptr); + nvgText(vg, x + w * 0.5f - tw * 0.5f, y + 40.0f, title, nullptr); + } + + void DrawButton(NVGcontext *vg, const char *text, float x, float y, float w, float h, ButtonStyle style, u64 ns) { + /* Fill the background if selected. */ + if (style == ButtonStyle::StandardSelected || style == ButtonStyle::FileSelectSelected) { + NVGpaint bg_paint = nvgLinearGradient(vg, x, y + h / 2.0f, x + w, y + h / 2.0f, nvgRGB(83, 71, 185), GetSelectionRGB2(ns)); + nvgBeginPath(vg); + nvgRoundedRect(vg, x, y, w, h, ButtonCornerRaidus); + nvgFillPaint(vg, bg_paint); + nvgFill(vg); + } + + /* Draw the shadow surrounding the button. */ + if (style == ButtonStyle::Standard || style == ButtonStyle::StandardSelected || style == ButtonStyle::StandardDisabled || style == ButtonStyle::FileSelectSelected) { + const unsigned char shadow_color = style == ButtonStyle::Standard ? 128 : 64; + NVGpaint shadow_paint = nvgBoxGradient(vg, x, y, w, h, ButtonCornerRaidus, 5, nvgRGBA(0, 0, 0, shadow_color), nvgRGBA(0, 0, 0, 0)); + nvgBeginPath(vg); + nvgRect(vg, x - 10, y - 10, w + 20, h + 30); + nvgRoundedRect(vg, x, y, w, h, ButtonCornerRaidus); + nvgPathWinding(vg, NVG_HOLE); + nvgFillPaint(vg, shadow_paint); + nvgFill(vg); + } + + /* Setup the font. */ + nvgFontSize(vg, 20.0f); + nvgFontFace(vg, SwitchStandardFont); + nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE); + + /* Set the text colour. */ + if (style == ButtonStyle::StandardSelected || style == ButtonStyle::FileSelectSelected) { + nvgFillColor(vg, nvgRGB(255, 255, 255)); + } else { + const unsigned char alpha = style == ButtonStyle::StandardDisabled ? 64 : 255; + nvgFillColor(vg, nvgRGBA(0, 0, 0, alpha)); + } + + /* Draw the button text. */ + const float tw = nvgTextBounds(vg, 0, 0, text, nullptr, nullptr); + + if (style == ButtonStyle::Standard || style == ButtonStyle::StandardSelected || style == ButtonStyle::StandardDisabled) { + nvgText(vg, x + w * 0.5f - tw * 0.5f, y + h * 0.5f, text, nullptr); + } else { + nvgText(vg, x + 10.0f, y + h * 0.5f, text, nullptr); + } + } + + void DrawTextBackground(NVGcontext *vg, float x, float y, float w, float h) { + nvgBeginPath(vg); + nvgRoundedRect(vg, x, y, w, h, TextAreaCornerRadius); + nvgFillColor(vg, nvgRGBA(0, 0, 0, 16)); + nvgFill(vg); + } + + void DrawProgressText(NVGcontext *vg, float x, float y, float progress) { + char progress_text[32] = {}; + snprintf(progress_text, sizeof(progress_text)-1, "%d%% complete", static_cast(progress * 100.0f)); + + nvgFontSize(vg, 24.0f); + nvgFontFace(vg, SwitchStandardFont); + nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE); + nvgFillColor(vg, nvgRGB(0, 0, 0)); + nvgText(vg, x, y, progress_text, nullptr); + } + + void DrawProgressBar(NVGcontext *vg, float x, float y, float w, float h, float progress) { + /* Draw the progress bar background. */ + nvgBeginPath(vg); + nvgRoundedRect(vg, x, y, w, h, WindowCornerRadius); + nvgFillColor(vg, nvgRGBA(0, 0, 0, 128)); + nvgFill(vg); + + /* Draw the progress bar fill. */ + if (progress > 0.0f) { + NVGpaint progress_fill_paint = nvgLinearGradient(vg, x, y + 0.5f * h, x + w, y + 0.5f * h, nvgRGB(83, 71, 185), nvgRGB(144, 185, 217)); + nvgBeginPath(vg); + nvgRoundedRect(vg, x, y, WindowCornerRadius + (w - WindowCornerRadius) * progress, h, WindowCornerRadius); + nvgFillPaint(vg, progress_fill_paint); + nvgFill(vg); + } + } + + void DrawTextBlock(NVGcontext *vg, const char *text, float x, float y, float w, float h) { + /* Save state and scissor. */ + nvgSave(vg); + nvgScissor(vg, x, y, w, h); + + /* Configure the text. */ + nvgFontSize(vg, 18.0f); + nvgFontFace(vg, SwitchStandardFont); + nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_TOP); + nvgFillColor(vg, nvgRGB(0, 0, 0)); + + /* Determine the bounds of the text box. */ + float bounds[4]; + nvgTextBoxBounds(vg, 0, 0, w, text, nullptr, bounds); + + /* Adjust the y to only show the last part of the text that fits. */ + float y_adjustment = 0.0f; + if (bounds[3] > h) { + y_adjustment = bounds[3] - h; + } + + /* Draw the text box and restore state. */ + nvgTextBox(vg, x, y - y_adjustment, w, text, nullptr); + nvgRestore(vg); + } + +} diff --git a/troposphere/daybreak/source/ui_util.hpp b/troposphere/daybreak/source/ui_util.hpp new file mode 100644 index 000000000..4522760ab --- /dev/null +++ b/troposphere/daybreak/source/ui_util.hpp @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 Adubbz + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope 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 . + */ +#pragma once +#include +#include + +namespace dbk { + + enum class ButtonStyle { + Standard, + StandardSelected, + StandardDisabled, + FileSelect, + FileSelectSelected, + }; + + void DrawStar(NVGcontext *vg, float x, float y, float width); + void DrawBackground(NVGcontext *vg, float w, float h); + void DrawWindow(NVGcontext *vg, const char *title, float x, float y, float w, float h); + void DrawButton(NVGcontext *vg, const char *text, float x, float y, float w, float h, ButtonStyle style, u64 ns); + void DrawTextBackground(NVGcontext *vg, float x, float y, float w, float h); + void DrawProgressText(NVGcontext *vg, float x, float y, float progress); + void DrawProgressBar(NVGcontext *vg, float x, float y, float w, float h, float progress); + void DrawTextBlock(NVGcontext *vg, const char *text, float x, float y, float w, float h); + +}