diff --git a/Extensions/olcPGEX_PopUpMenu.h b/Extensions/olcPGEX_PopUpMenu.h new file mode 100644 index 0000000..5b38b02 --- /dev/null +++ b/Extensions/olcPGEX_PopUpMenu.h @@ -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 + +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 itemPointer; + std::vector 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 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 \ No newline at end of file