parent
7ea9c0a315
commit
4750278b31
@ -0,0 +1,585 @@ |
||||
/*
|
||||
olcPGEX_PopUp.h |
||||
|
||||
+-------------------------------------------------------------+ |
||||
| OneLoneCoder Pixel Game Engine Extension | |
||||
| Retro PopUp Menu 1.0 | |
||||
+-------------------------------------------------------------+ |
||||
|
||||
What is this? |
||||
~~~~~~~~~~~~~ |
||||
This is an extension to the olcPixelGameEngine, which provides |
||||
a quick and easy to use, flexible, skinnable context pop-up
|
||||
menu system. |
||||
|
||||
License (OLC-3) |
||||
~~~~~~~~~~~~~~~ |
||||
|
||||
Copyright 2018 - 2020 OneLoneCoder.com |
||||
|
||||
Redistribution and use in source and binary forms, with or without |
||||
modification, are permitted provided that the following conditions |
||||
are met: |
||||
|
||||
1. Redistributions or derivations of source code must retain the above |
||||
copyright notice, this list of conditions and the following disclaimer. |
||||
|
||||
2. Redistributions or derivative works in binary form must reproduce |
||||
the above copyright notice. This list of conditions and the following |
||||
disclaimer must be reproduced in the documentation and/or other |
||||
materials provided with the distribution. |
||||
|
||||
3. Neither the name of the copyright holder nor the names of its |
||||
contributors may be used to endorse or promote products derived |
||||
from this software without specific prior written permission. |
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
|
||||
Links |
||||
~~~~~ |
||||
YouTube: https://www.youtube.com/javidx9
|
||||
Discord: https://discord.gg/WhwHUMV
|
||||
Twitter: https://www.twitter.com/javidx9
|
||||
Twitch: https://www.twitch.tv/javidx9
|
||||
GitHub: https://www.github.com/onelonecoder
|
||||
Homepage: https://www.onelonecoder.com
|
||||
|
||||
Author |
||||
~~~~~~ |
||||
David Barr, aka javidx9, ©OneLoneCoder 2019, 2020 |
||||
*/ |
||||
|
||||
|
||||
/*
|
||||
Example |
||||
~~~~~~~ |
||||
|
||||
#define OLC_PGEX_POPUPMENU |
||||
#include "olcPGEX_PopUpMenu.h" |
||||
|
||||
NOTE: Requires a 9-patch sprite, by default each patch is |
||||
8x8 pixels, patches are as follows: |
||||
|
||||
| PANEL TL | PANEL T | PANEL TR | SCROLL UP | CURSOR TL | CURSOR TR | |
||||
| PANEL L | PANEL M | PANEL R | SUBMENU | CURSOR BL | CURSOR BR | |
||||
| PANEL BL | PANEL B | PANEL BR | SCROLL DOWN | UNUSED | UNUSED | |
||||
|
||||
You can find an example sprite here: |
||||
https://github.com/OneLoneCoder/olcPixelGameEngine/blob/master/Videos/RetroMenu.png
|
||||
|
||||
Constructing A Menu |
||||
~~~~~~~~~~~~~~~~~~~ |
||||
|
||||
// Declaration (presumably inside class)
|
||||
olc::popup::Menu m; |
||||
|
||||
// Construction (root menu is a 1x5 table)
|
||||
m.SetTable(1, 5); |
||||
|
||||
// Add first item to root menu (A 1x5 submenu)
|
||||
m["Menu1"].SetTable(1, 5); |
||||
|
||||
// Add items to first item
|
||||
m["Menu1"]["Item1"]; |
||||
m["Menu1"]["Item2"]; |
||||
|
||||
// Add a 4x3 submenu
|
||||
m["Menu1"]["Item3"].SetTable(4, 3); |
||||
m["Menu1"]["Item3"]["Option1"]; |
||||
m["Menu1"]["Item3"]["Option2"]; |
||||
|
||||
// Set properties of specific item
|
||||
m["Menu1"]["Item3"]["Option3"].Enable(false); |
||||
m["Menu1"]["Item3"]["Option4"]; |
||||
m["Menu1"]["Item3"]["Option5"]; |
||||
m["Menu1"]["Item4"]; |
||||
|
||||
// Add second item to root menu
|
||||
m["Menu2"].SetTable(3, 3); |
||||
m["Menu2"]["Item1"]; |
||||
m["Menu2"]["Item2"].SetID(1001).Enable(true); |
||||
m["Menu2"]["Item3"]; |
||||
|
||||
// Construct the menu structure
|
||||
m.Build(); |
||||
|
||||
|
||||
Displaying a Menu |
||||
~~~~~~~~~~~~~~~~~ |
||||
|
||||
// Declaration of menu manager (presumably inside class)
|
||||
olc::popup::Manager man; |
||||
|
||||
// Set the Menu object to the MenuManager (call once per pop)
|
||||
man.Open(&m); |
||||
|
||||
// Draw Menu at position (30, 30), using "patch sprite"
|
||||
man.Draw(sprGFX, { 30,30 }); |
||||
|
||||
|
||||
Interacting with menu |
||||
~~~~~~~~~~~~~~~~~~~~~ |
||||
|
||||
// Send key events to menu
|
||||
if (GetKey(olc::Key::UP).bPressed) mm.OnUp(); |
||||
if (GetKey(olc::Key::DOWN).bPressed) mm.OnDown(); |
||||
if (GetKey(olc::Key::LEFT).bPressed) mm.OnLeft(); |
||||
if (GetKey(olc::Key::RIGHT).bPressed) mm.OnRight(); |
||||
if (GetKey(olc::Key::Z).bPressed) mm.OnBack(); |
||||
|
||||
// "Confirm/Action" Key does something, if it returns non-null
|
||||
// then a menu item has been selected. The specific item will
|
||||
// be returned
|
||||
olc::popup::Menu* command = nullptr; |
||||
if (GetKey(olc::Key::SPACE).bPressed) command = mm.OnConfirm(); |
||||
if (command != nullptr) |
||||
{ |
||||
std::string sLastAction =
|
||||
"Selected: " + command->GetName() +
|
||||
" ID: " + std::to_string(command->GetID()); |
||||
|
||||
// Optionally close menu?
|
||||
mm.Close(); |
||||
} |
||||
|
||||
*/ |
||||
|
||||
#ifndef OLC_PGEX_POPUPMENU_H |
||||
#define OLC_PGEX_POPUPMENU_H |
||||
|
||||
#include <cstdint> |
||||
|
||||
namespace olc |
||||
{ |
||||
namespace popup |
||||
{ |
||||
constexpr int32_t nPatch = 8; |
||||
|
||||
class Menu |
||||
{ |
||||
public: |
||||
Menu(); |
||||
Menu(const std::string n); |
||||
|
||||
Menu& SetTable(int32_t nColumns, int32_t nRows); |
||||
Menu& SetID(int32_t id); |
||||
Menu& Enable(bool b); |
||||
|
||||
int32_t GetID(); |
||||
std::string& GetName();
|
||||
bool Enabled(); |
||||
bool HasChildren(); |
||||
olc::vi2d GetSize(); |
||||
olc::vi2d& GetCursorPosition(); |
||||
Menu& operator[](const std::string& name); |
||||
void Build(); |
||||
void DrawSelf(olc::PixelGameEngine& pge, olc::Sprite* sprGFX, olc::vi2d vScreenOffset); |
||||
void ClampCursor(); |
||||
void OnUp(); |
||||
void OnDown(); |
||||
void OnLeft(); |
||||
void OnRight(); |
||||
Menu* OnConfirm(); |
||||
Menu* GetSelectedItem(); |
||||
|
||||
protected: |
||||
int32_t nID = -1; |
||||
olc::vi2d vCellTable = { 1, 0 }; |
||||
std::unordered_map<std::string, size_t> itemPointer; |
||||
std::vector<olc::popup::Menu> items; |
||||
olc::vi2d vSizeInPatches = { 0, 0 }; |
||||
olc::vi2d vCellSize = { 0, 0 }; |
||||
olc::vi2d vCellPadding = { 2, 0 }; |
||||
olc::vi2d vCellCursor = { 0, 0 }; |
||||
int32_t nCursorItem = 0; |
||||
int32_t nTopVisibleRow = 0; |
||||
int32_t nTotalRows = 0; |
||||
const olc::vi2d vPatchSize = { nPatch, nPatch }; |
||||
std::string sName; |
||||
olc::vi2d vCursorPos = { 0, 0 }; |
||||
bool bEnabled = true; |
||||
}; |
||||
|
||||
class Manager : public olc::PGEX |
||||
{ |
||||
public: |
||||
Manager(); |
||||
void Open(Menu* mo); |
||||
void Close(); |
||||
void OnUp(); |
||||
void OnDown(); |
||||
void OnLeft(); |
||||
void OnRight(); |
||||
void OnBack(); |
||||
Menu* OnConfirm(); |
||||
void Draw(olc::Sprite* sprGFX, olc::vi2d vScreenOffset); |
||||
|
||||
private: |
||||
std::list<Menu*> panels; |
||||
}; |
||||
|
||||
} |
||||
}; |
||||
|
||||
|
||||
|
||||
|
||||
#ifdef OLC_PGEX_POPUPMENU |
||||
#undef OLC_PGEX_POPUPMENU |
||||
|
||||
namespace olc |
||||
{ |
||||
namespace popup |
||||
{ |
||||
Menu::Menu() |
||||
{ |
||||
} |
||||
|
||||
Menu::Menu(const std::string n) |
||||
{ |
||||
sName = n; |
||||
} |
||||
|
||||
|
||||
Menu& Menu::SetTable(int32_t nColumns, int32_t nRows) |
||||
{ |
||||
vCellTable = { nColumns, nRows }; |
||||
return *this; |
||||
} |
||||
|
||||
Menu& Menu::SetID(int32_t id) |
||||
{ |
||||
nID = id; |
||||
return *this; |
||||
} |
||||
|
||||
Menu& Menu::Enable(bool b) |
||||
{ |
||||
bEnabled = b; |
||||
return *this; |
||||
} |
||||
|
||||
int32_t Menu::GetID() |
||||
{ |
||||
return nID; |
||||
} |
||||
|
||||
std::string& Menu::GetName() |
||||
{ |
||||
return sName; |
||||
} |
||||
|
||||
bool Menu::Enabled() |
||||
{ |
||||
return bEnabled; |
||||
} |
||||
|
||||
bool Menu::HasChildren() |
||||
{ |
||||
return !items.empty(); |
||||
} |
||||
|
||||
olc::vi2d Menu::GetSize() |
||||
{ |
||||
return { int32_t(sName.size()), 1 }; |
||||
} |
||||
|
||||
olc::vi2d& Menu::GetCursorPosition() |
||||
{ |
||||
return vCursorPos; |
||||
} |
||||
|
||||
Menu& Menu::operator[](const std::string& name) |
||||
{ |
||||
if (itemPointer.count(name) == 0) |
||||
{ |
||||
itemPointer[name] = items.size(); |
||||
items.push_back(Menu(name)); |
||||
} |
||||
|
||||
return items[itemPointer[name]]; |
||||
} |
||||
|
||||
void Menu::Build() |
||||
{ |
||||
// Recursively build all children, so they can determine their size, use
|
||||
// that size to indicate cell sizes if this object contains more than
|
||||
// one item
|
||||
for (auto& m : items) |
||||
{ |
||||
if (m.HasChildren()) |
||||
{ |
||||
m.Build(); |
||||
} |
||||
|
||||
// Longest child name determines cell width
|
||||
vCellSize.x = std::max(m.GetSize().x, vCellSize.x); |
||||
vCellSize.y = std::max(m.GetSize().y, vCellSize.y); |
||||
} |
||||
|
||||
// Adjust size of this object (in patches) if it were rendered as a panel
|
||||
vSizeInPatches.x = vCellTable.x * vCellSize.x + (vCellTable.x - 1) * vCellPadding.x + 2; |
||||
vSizeInPatches.y = vCellTable.y * vCellSize.y + (vCellTable.y - 1) * vCellPadding.y + 2; |
||||
|
||||
// Calculate how many rows this item has to hold
|
||||
nTotalRows = (items.size() / vCellTable.x) + (((items.size() % vCellTable.x) > 0) ? 1 : 0); |
||||
} |
||||
|
||||
void Menu::DrawSelf(olc::PixelGameEngine& pge, olc::Sprite* sprGFX, olc::vi2d vScreenOffset) |
||||
{ |
||||
// === Draw Panel
|
||||
|
||||
// Record current pixel mode user is using
|
||||
olc::Pixel::Mode currentPixelMode = pge.GetPixelMode(); |
||||
pge.SetPixelMode(olc::Pixel::MASK); |
||||
|
||||
// Draw Panel & Border
|
||||
olc::vi2d vPatchPos = { 0,0 }; |
||||
for (vPatchPos.x = 0; vPatchPos.x < vSizeInPatches.x; vPatchPos.x++) |
||||
{ |
||||
for (vPatchPos.y = 0; vPatchPos.y < vSizeInPatches.y; vPatchPos.y++) |
||||
{ |
||||
// Determine position in screen space
|
||||
olc::vi2d vScreenLocation = vPatchPos * nPatch + vScreenOffset; |
||||
|
||||
// Calculate which patch is needed
|
||||
olc::vi2d vSourcePatch = { 0, 0 }; |
||||
if (vPatchPos.x > 0) vSourcePatch.x = 1; |
||||
if (vPatchPos.x == vSizeInPatches.x - 1) vSourcePatch.x = 2; |
||||
if (vPatchPos.y > 0) vSourcePatch.y = 1; |
||||
if (vPatchPos.y == vSizeInPatches.y - 1) vSourcePatch.y = 2; |
||||
|
||||
// Draw Actual Patch
|
||||
pge.DrawPartialSprite(vScreenLocation, sprGFX, vSourcePatch * nPatch, vPatchSize); |
||||
} |
||||
} |
||||
|
||||
// === Draw Panel Contents
|
||||
olc::vi2d vCell = { 0,0 }; |
||||
vPatchPos = { 1,1 }; |
||||
|
||||
// Work out visible items
|
||||
int32_t nTopLeftItem = nTopVisibleRow * vCellTable.x; |
||||
int32_t nBottomRightItem = vCellTable.y * vCellTable.x + nTopLeftItem; |
||||
|
||||
// Clamp to size of child item vector
|
||||
nBottomRightItem = std::min(int32_t(items.size()), nBottomRightItem); |
||||
int32_t nVisibleItems = nBottomRightItem - nTopLeftItem; |
||||
|
||||
// Draw Scroll Markers (if required)
|
||||
if (nTopVisibleRow > 0) |
||||
{ |
||||
vPatchPos = { vSizeInPatches.x - 2, 0 }; |
||||
olc::vi2d vScreenLocation = vPatchPos * nPatch + vScreenOffset; |
||||
olc::vi2d vSourcePatch = { 3, 0 }; |
||||
pge.DrawPartialSprite(vScreenLocation, sprGFX, vSourcePatch * nPatch, vPatchSize); |
||||
} |
||||
|
||||
if ((nTotalRows - nTopVisibleRow) > vCellTable.y) |
||||
{ |
||||
vPatchPos = { vSizeInPatches.x - 2, vSizeInPatches.y - 1 }; |
||||
olc::vi2d vScreenLocation = vPatchPos * nPatch + vScreenOffset; |
||||
olc::vi2d vSourcePatch = { 3, 2 }; |
||||
pge.DrawPartialSprite(vScreenLocation, sprGFX, vSourcePatch * nPatch, vPatchSize); |
||||
} |
||||
|
||||
// Draw Visible Items
|
||||
for (int32_t i = 0; i < nVisibleItems; i++) |
||||
{ |
||||
// Cell location
|
||||
vCell.x = i % vCellTable.x; |
||||
vCell.y = i / vCellTable.x; |
||||
|
||||
// Patch location (including border offset and padding)
|
||||
vPatchPos.x = vCell.x * (vCellSize.x + vCellPadding.x) + 1; |
||||
vPatchPos.y = vCell.y * (vCellSize.y + vCellPadding.y) + 1; |
||||
|
||||
// Actual screen location in pixels
|
||||
olc::vi2d vScreenLocation = vPatchPos * nPatch + vScreenOffset; |
||||
|
||||
// Display Item Header
|
||||
pge.DrawString(vScreenLocation, items[nTopLeftItem + i].sName, items[nTopLeftItem + i].bEnabled ? olc::WHITE : olc::DARK_GREY); |
||||
|
||||
if (items[nTopLeftItem + i].HasChildren()) |
||||
{ |
||||
// Display Indicator that panel has a sub panel
|
||||
vPatchPos.x = vCell.x * (vCellSize.x + vCellPadding.x) + 1 + vCellSize.x; |
||||
vPatchPos.y = vCell.y * (vCellSize.y + vCellPadding.y) + 1; |
||||
olc::vi2d vSourcePatch = { 3, 1 }; |
||||
vScreenLocation = vPatchPos * nPatch + vScreenOffset; |
||||
pge.DrawPartialSprite(vScreenLocation, sprGFX, vSourcePatch * nPatch, vPatchSize); |
||||
} |
||||
} |
||||
|
||||
// Calculate cursor position in screen space in case system draws it
|
||||
vCursorPos.x = (vCellCursor.x * (vCellSize.x + vCellPadding.x)) * nPatch + vScreenOffset.x - nPatch; |
||||
vCursorPos.y = ((vCellCursor.y - nTopVisibleRow) * (vCellSize.y + vCellPadding.y)) * nPatch + vScreenOffset.y + nPatch; |
||||
} |
||||
|
||||
void Menu::ClampCursor() |
||||
{ |
||||
// Find item in children
|
||||
nCursorItem = vCellCursor.y * vCellTable.x + vCellCursor.x; |
||||
|
||||
// Clamp Cursor
|
||||
if (nCursorItem >= int32_t(items.size())) |
||||
{ |
||||
vCellCursor.y = (items.size() / vCellTable.x); |
||||
vCellCursor.x = (items.size() % vCellTable.x) - 1; |
||||
nCursorItem = items.size() - 1; |
||||
} |
||||
} |
||||
|
||||
void Menu::OnUp() |
||||
{ |
||||
vCellCursor.y--; |
||||
if (vCellCursor.y < 0) vCellCursor.y = 0; |
||||
|
||||
if (vCellCursor.y < nTopVisibleRow) |
||||
{ |
||||
nTopVisibleRow--; |
||||
if (nTopVisibleRow < 0) nTopVisibleRow = 0; |
||||
} |
||||
|
||||
ClampCursor(); |
||||
} |
||||
|
||||
void Menu::OnDown() |
||||
{ |
||||
vCellCursor.y++; |
||||
if (vCellCursor.y == nTotalRows) vCellCursor.y = nTotalRows - 1; |
||||
|
||||
if (vCellCursor.y > (nTopVisibleRow + vCellTable.y - 1)) |
||||
{ |
||||
nTopVisibleRow++; |
||||
if (nTopVisibleRow > (nTotalRows - vCellTable.y)) |
||||
nTopVisibleRow = nTotalRows - vCellTable.y; |
||||
} |
||||
|
||||
ClampCursor(); |
||||
} |
||||
|
||||
void Menu::OnLeft() |
||||
{ |
||||
vCellCursor.x--; |
||||
if (vCellCursor.x < 0) vCellCursor.x = 0; |
||||
ClampCursor(); |
||||
} |
||||
|
||||
void Menu::OnRight() |
||||
{ |
||||
vCellCursor.x++; |
||||
if (vCellCursor.x == vCellTable.x) vCellCursor.x = vCellTable.x - 1; |
||||
ClampCursor(); |
||||
} |
||||
|
||||
Menu* Menu::OnConfirm() |
||||
{ |
||||
// Check if selected item has children
|
||||
if (items[nCursorItem].HasChildren()) |
||||
{ |
||||
return &items[nCursorItem]; |
||||
} |
||||
else |
||||
return this; |
||||
} |
||||
|
||||
Menu* Menu::GetSelectedItem() |
||||
{ |
||||
return &items[nCursorItem]; |
||||
} |
||||
|
||||
// =====================================================================
|
||||
|
||||
Manager::Manager() |
||||
{ |
||||
} |
||||
|
||||
void Manager::Open(Menu* mo)
|
||||
{
|
||||
Close();
|
||||
panels.push_back(mo);
|
||||
} |
||||
|
||||
void Manager::Close() |
||||
{ |
||||
panels.clear();
|
||||
} |
||||
|
||||
void Manager::OnUp()
|
||||
{
|
||||
if (!panels.empty()) panels.back()->OnUp();
|
||||
} |
||||
|
||||
void Manager::OnDown()
|
||||
{
|
||||
if (!panels.empty()) panels.back()->OnDown();
|
||||
} |
||||
|
||||
void Manager::OnLeft() |
||||
{
|
||||
if (!panels.empty()) panels.back()->OnLeft(); |
||||
} |
||||
|
||||
void Manager::OnRight() |
||||
{
|
||||
if (!panels.empty()) panels.back()->OnRight(); |
||||
} |
||||
|
||||
void Manager::OnBack() |
||||
{ |
||||
if (!panels.empty()) panels.pop_back();
|
||||
} |
||||
|
||||
Menu* Manager::OnConfirm() |
||||
{ |
||||
if (panels.empty()) return nullptr; |
||||
|
||||
Menu* next = panels.back()->OnConfirm(); |
||||
if (next == panels.back()) |
||||
{ |
||||
if (panels.back()->GetSelectedItem()->Enabled()) |
||||
return panels.back()->GetSelectedItem(); |
||||
} |
||||
else |
||||
{ |
||||
if (next->Enabled()) |
||||
panels.push_back(next); |
||||
} |
||||
|
||||
return nullptr; |
||||
} |
||||
|
||||
void Manager::Draw(olc::Sprite* sprGFX, olc::vi2d vScreenOffset) |
||||
{ |
||||
if (panels.empty()) return; |
||||
|
||||
// Draw Visible Menu System
|
||||
for (auto& p : panels) |
||||
{ |
||||
p->DrawSelf(*pge, sprGFX, vScreenOffset); |
||||
vScreenOffset += {10, 10}; |
||||
} |
||||
|
||||
// Draw Cursor
|
||||
olc::Pixel::Mode currentPixelMode = pge->GetPixelMode(); |
||||
pge->SetPixelMode(olc::Pixel::ALPHA); |
||||
pge->DrawPartialSprite(panels.back()->GetCursorPosition(), sprGFX, olc::vi2d(4, 0) * nPatch, { nPatch * 2, nPatch * 2 }); |
||||
pge->SetPixelMode(currentPixelMode); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
|
||||
#endif |
||||
#endif |
Loading…
Reference in new issue