Files
cheats-updater/lib/borealis/library/lib/applet_frame.cpp
2026-03-05 20:18:29 +01:00

431 lines
13 KiB
C++

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