// zlib open source license
//
// Copyright (c) 2018 to 2023 David Forsgren Piuva
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would be
// appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such, and must not be
// misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source
// distribution.
#include "Menu.h"
using namespace dsr;
PERSISTENT_DEFINITION(Menu)
static AlignedImageU8 arrowImage = image_fromAscii(
"< .xX>"
"<.x. >"
"< XX. >"
"< xXX.>"
"< XX. >"
"<.x. >"
);
void Menu::declareAttributes(StructureDefinition &target) const {
VisualComponent::declareAttributes(target);
target.declareAttribute(U"BackColor");
target.declareAttribute(U"ForeColor");
target.declareAttribute(U"Text");
target.declareAttribute(U"Padding");
target.declareAttribute(U"Spacing");
target.declareAttribute(U"HeadClass");
target.declareAttribute(U"ListClass");
}
Persistent* Menu::findAttribute(const ReadableString &name) {
if (string_caseInsensitiveMatch(name, U"Color") || string_caseInsensitiveMatch(name, U"BackColor")) {
// The short Color alias refers to the back color in Buttons, because most buttons use black text.
return &(this->backColor);
} else if (string_caseInsensitiveMatch(name, U"ForeColor")) {
return &(this->foreColor);
} else if (string_caseInsensitiveMatch(name, U"Text")) {
return &(this->text);
} else if (string_caseInsensitiveMatch(name, U"Padding")) {
return &(this->padding);
} else if (string_caseInsensitiveMatch(name, U"Spacing")) {
return &(this->spacing);
} else if (string_caseInsensitiveMatch(name, U"HeadClass") || string_caseInsensitiveMatch(name, U"Class")) {
// Class is an alias for HeadClass.
return &(this->headClass);
} else if (string_caseInsensitiveMatch(name, U"ListClass")) {
return &(this->listClass);
} else {
return VisualComponent::findAttribute(name);
}
}
Menu::Menu() {}
bool Menu::isContainer() const {
return true;
}
bool Menu::hasArrow() {
return this->subMenu && this->getChildCount() > 0;
}
static OrderedImageRgbaU8 generateHeadImage(Menu &menu, MediaMethod imageGenerator, int pressed, int focused, int hover, int width, int height, ColorRgbI32 backColor, ColorRgbI32 foreColor, const ReadableString &text, RasterFont font) {
// Create a scaled image
OrderedImageRgbaU8 result;
component_generateImage(menu.getTheme(), imageGenerator, width, height, backColor.red, backColor.green, backColor.blue, pressed, focused, hover)(result);
if (string_length(text) > 0) {
int backWidth = image_getWidth(result);
int backHeight = image_getHeight(result);
int left = menu.padding.value;
int top = (backHeight - font_getSize(font)) / 2;
if (pressed) {
top += 1;
}
// Print the text
font_printLine(result, font, text, IVector2D(left, top), ColorRgbaI32(foreColor, 255));
// Draw the arrow
if (menu.hasArrow()) {
int arrowWidth = image_getWidth(arrowImage);
int arrowHeight = image_getHeight(arrowImage);
int arrowLeft = backWidth - arrowWidth - 4;
int arrowTop = (backHeight - arrowHeight) / 2;
draw_silhouette(result, arrowImage, ColorRgbaI32(foreColor, 255), arrowLeft, arrowTop);
}
}
return result;
}
void Menu::generateGraphics() {
int headWidth = this->location.width();
int headHeight = this->location.height();
if (headWidth < 1) { headWidth = 1; }
if (headHeight < 1) { headHeight = 1; }
// headImage is set to an empty handle when something used as input changes.
if (!image_exists(this->headImage)) {
completeAssets();
bool focus = this->isFocused();
bool hover = this->isHovered();
bool press = this->pressed && hover;
this->headImage = generateHeadImage(*this, this->headImageMethod, press, focus, hover, headWidth, headHeight, this->backColor.value, this->foreColor.value, this->text.value, this->font);
}
}
// Fill the listBackgroundImageMethod with a solid color
void Menu::drawSelf(ImageRgbaU8& targetImage, const IRect &relativeLocation) {
this->generateGraphics();
if (this->menuHead_filter == 1) {
draw_alphaFilter(targetImage, this->headImage, relativeLocation.left(), relativeLocation.top());
} else {
draw_copy(targetImage, this->headImage, relativeLocation.left(), relativeLocation.top());
}
}
void Menu::generateBackground() {
if (!image_exists(this->listBackgroundImage)) {
int listWidth = this->overlayLocation.width();
int listHeight = this->overlayLocation.height();
if (listWidth < 1) { listWidth = 1; }
if (listHeight < 1) { listHeight = 1; }
component_generateImage(this->theme, this->listBackgroundImageMethod, listWidth, listHeight, this->backColor.value.red, this->backColor.value.green, this->backColor.value.blue)(this->listBackgroundImage);
}
}
void Menu::createOverlay() {
if (!this->showingOverlay()) {
this->showOverlay();
this->makeFocused(); // Focus on the current menu path to make others lose focus.
IRect memberBound = this->children[0]->location;
for (int i = 1; i < this->getChildCount(); i++) {
memberBound = IRect::merge(memberBound, this->children[i]->location);
}
// Calculate the new list bound.
this->overlayLocation = memberBound.expanded(this->padding.value) + this->location.upperLeft();
}
}
bool Menu::managesChildren() {
return true;
}
bool Menu::pointIsInsideOfOverlay(const IVector2D& pixelPosition) {
return pixelPosition.x > this->overlayLocation.left() && pixelPosition.x < this->overlayLocation.right() && pixelPosition.y > this->overlayLocation.top() && pixelPosition.y < this->overlayLocation.bottom();
}
void Menu::drawOverlay(ImageRgbaU8& targetImage, const IVector2D &absoluteOffset) {
this->generateBackground();
IVector2D overlayOffset = absoluteOffset + this->overlayLocation.upperLeft();
if (this->menuList_filter == 1) {
draw_alphaFilter(targetImage, this->listBackgroundImage, overlayOffset.x, overlayOffset.y);
} else {
draw_copy(targetImage, this->listBackgroundImage, overlayOffset.x, overlayOffset.y);
}
for (int i = 0; i < this->getChildCount(); i++) {
this->children[i]->draw(targetImage, absoluteOffset + this->location.upperLeft());
}
}
void Menu::loadTheme(const VisualTheme &theme) {
// Is it a sub-menu or top menu?
this->subMenu = this->parent != nullptr && dynamic_cast