431 lines
13 KiB
C++
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
|