#include "Crawler.h" #include "MenuComponent.h" #include "DEFINES.h" #include "safemap.h" #include "Item.h" #include "MenuItemButton.h" #include "ScrollableWindowComponent.h" bool Menu::MOUSE_NAVIGATION=true; std::vectorMenu::stack; std::mapMenu::menus; std::string Menu::themeSelection="BlueDefault"; safeunorderedmapMenu::themes; safemap>Menu::inventoryListeners; const vf2d Menu::CENTERED = {-456,-456}; std::vectorMenu::unhandledComponents; ////////////////////////////////////////////////////////////////////////////////////////////////// ///__///////////////////////////////////////////////////////////////////////////////////////////// //| |//////////////////////////////////////////////////////////////////////////////////////////// //| |/////WARNING! If you are adding something here you likely are adding another container with MenuComponent pointers in it right now. //| |/////Because we are handling raw pointers, you must also add this container to the list of iterating search removal containers that occur in the //| |/////DESTRUCTOR of MenuComponents!!!!! (Go to MenuComponent::~MenuComponent()) THIS IS NOT A DRILL! //|__|//////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////// ///__///////////////////////////////////////////////////////////////////////////////////////////// //|__///////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////// MenuType Menu::lastMenuTypeCreated; std::string Menu::lastRegisteredComponent; bool Menu::cover; INCLUDE_GFX extern vi2d WINDOW_SIZE; typedef Attribute A; Menu::Menu(vf2d pos,vf2d size) :pos(pos==CENTERED?WINDOW_SIZE/2-size/2:vi2d{pos}),size(size){ r.Create(size.x,size.y); overlay.Create(WINDOW_SIZE.x,WINDOW_SIZE.y); } Menu::~Menu(){ for(auto key:components){ delete key.second; } } void Menu::InitializeMenus(){ stack.reserve(32); InitializeTestMenu(); InitializeTestSubMenu(); InitializeInventoryWindow(); InitializeClassSelectionWindow(); InitializeClassInfoWindow(); InitializeMainMenuWindow(); InitializeOverworldMapLevelWindow(); for(MenuType type=TEST;typebuttons.SetInitialized(); menus[type]->keyboardButtons.SetInitialized(); for(auto&key:menus[type]->components){ MenuComponent*component=key.second; component->AfterCreate(); } } if(Menu::unhandledComponents.size()>0){ std::cout<<"WARNING! There are "<parentMenu<<" Label: "<label<memoryLeakInfo.first<<" // Last Component: "<memoryLeakInfo.second<selectable){ buttons.Unlock(); if(buttons.count(button->rect.pos.y)){ buttons.at(button->rect.pos.y).push_back(button); }else{ buttons[button->rect.pos.y].push_back(button); } if(button->selectableViaKeyboard){ keyboardButtons.Unlock(); if(keyboardButtons.count(button->rect.pos.y)){ keyboardButtons.at(button->rect.pos.y).push_back(button); }else{ keyboardButtons[button->rect.pos.y].push_back(button); } } //We must lock the values before calling sort. Sort seems to try and create new accesses. buttons.SetInitialized(); keyboardButtons.SetInitialized(); //We make an assumption that menu components are supposed to be in left-to-right order. Sometimes we may add things out-of-order, so this fixes the problem by sorting the items afterwards. std::sort(buttons[button->rect.pos.y].begin(),buttons[button->rect.pos.y].end(),[](MenuComponent*c1,MenuComponent*c2){ return c1->GetPos().xGetPos().x; }); if(keyboardButtons.count(button->rect.pos.y)){ //Keyboard buttons may not necessarily contain this key...Let's be sure. std::sort(keyboardButtons[button->rect.pos.y].begin(),keyboardButtons[button->rect.pos.y].end(),[](MenuComponent*c1,MenuComponent*c2){ return c1->GetPos().xGetPos().x; }); } }else{ displayComponents.push_back(button); } if(components.count(key)){ ERR("WARNING! Key "<name=key; components.Unlock(); //It's possible we can add a component later on, so we will make sure we remove the lock first. components[key]=button; components.SetInitialized(); lastRegisteredComponent=key; std::erase_if(Menu::unhandledComponents,[&](MenuComponent*b1){return b1==button;}); } void Menu::CheckClickAndPerformMenuSelect(Crawler*game){ if(game->GetMouse(Mouse::LEFT).bReleased||game->GetKey(SPACE).bReleased||game->GetKey(ENTER).bReleased){ MenuSelect(game); } } void Menu::HoverMenuSelect(Crawler*game){ if(selection==vi2d{-1,-1}||buttons[selection.y][selection.x]->disabled)return; if(buttons[selection.y][selection.x]->draggable){ if(buttonHoldTime<"ThemeGlobal.MenuHoldTime"_F){ CheckClickAndPerformMenuSelect(game); }else{ draggingComponent=buttons[selection.y][selection.x]->PickUpDraggableItem(); buttonHoldTime=0; } }else{ CheckClickAndPerformMenuSelect(game); } } void Menu::MenuSelect(Crawler*game){ if(selection==vi2d{-1,-1}||buttons[selection.y][selection.x]->disabled)return; bool buttonStillValid=buttons[selection.y][selection.x]->onClick(MenuFuncData{*this,game,buttons[selection.y][selection.x]}); if(buttonStillValid){ if(buttons[selection.y][selection.x]->menuDest!=MenuType::ENUM_END){ if(stack.size()<32){ stack.push_back(menus[buttons[selection.y][selection.x]->menuDest]);//Navigate to the next menu. }else{ ERR("WARNING! Exceeded menu stack size limit!") } } } } void Menu::Update(Crawler*game){ if(draggingComponent==nullptr){ HoverMenuSelect(game); } if(!UsingMouseNavigation()&&geom2d::line(lastActiveMousePos,game->GetMousePos()).length()>="ThemeGlobal.MouseActivationDistance"_F||UsingMouseNavigation()){ SetMouseNavigation(true); } for(auto&key:buttons){ for(auto&button:key.second){ if(!button->disabled){ button->hovered=false; } } } bool itemHovered=false; if(!UsingMouseNavigation()){ if(selection!=vi2d{-1,-1}){ buttons[selection.y][selection.x]->hovered=true; itemHovered=true; } }else{ selection={-1,-1}; for(auto&key:buttons){ int index=0; for(auto&button:key.second){ if(!button->disabled){ if(button->GetHoverState(game)){ button->hovered=true; itemHovered=true; selection.y=key.first; selection.x=index; } } index++; } } } if(itemHovered&&draggingComponent==nullptr&&selection!=vi2d{-1,-1}&&(((!UsingMouseNavigation()&&(game->GetKey(ENTER).bHeld)||game->GetKey(SPACE).bHeld))||game->GetMouse(Mouse::LEFT).bHeld)){ buttonHoldTime+=game->GetElapsedTime(); }else{ buttonHoldTime=0; } if(draggingComponent!=nullptr){ MenuComponent*selectedComponent=nullptr; if(selection!=vi2d{-1,-1}){ selectedComponent=buttons[selection.y][selection.x]; } auto ClearDraggingComponent=[&](){ delete draggingComponent; //We know we allocated a new instance of this, so we will now free it. draggingComponent=nullptr; }; if(!UsingMouseNavigation()){ if(game->GetKey(ENTER).bReleased||game->GetKey(SPACE).bReleased){ if(selectedComponent==nullptr){//Dropping over an empty area. ClearDraggingComponent(); }else if(selectedComponent->DropDraggableItem(draggingComponent)){ ClearDraggingComponent(); } } }else{ if(game->GetMouse(Mouse::LEFT).bReleased){ if(selectedComponent==nullptr){//Dropping over an empty area. ClearDraggingComponent(); }else if(selectedComponent->DropDraggableItem(draggingComponent)){ ClearDraggingComponent(); } } } } KeyboardButtonNavigation(game,pos); for(auto&key:buttons){ for(auto&button:key.second){ if(button->renderInMain){ button->_Update(game); } } } for(auto&component:displayComponents){ if(component->renderInMain){ component->_Update(game); } } }; void Menu::Draw(Crawler*game){ if(GetCurrentTheme().IsScaled()){ DrawScaledWindowBackground(game,pos); }else{ DrawTiledWindowBackground(game,pos); } game->SetDrawTarget(r.Sprite()); Pixel::Mode prevMode=game->GetPixelMode(); game->SetPixelMode(Pixel::MASK); game->Clear(BLANK); for(auto&component:displayComponents){ if(component->renderInMain){ component->_Draw(game); } } for(auto&key:buttons){ for(auto&button:key.second){ if(button->renderInMain){ button->_Draw(game); } } } game->SetPixelMode(prevMode); game->SetDrawTarget(nullptr); r.Decal()->Update(); game->DrawDecal(pos,r.Decal()); for(auto&component:displayComponents){ if(component->renderInMain){ component->_DrawDecal(game,this==Menu::stack.back()); } } for(auto&key:buttons){ for(auto&button:key.second){ if(button->renderInMain){ button->_DrawDecal(game,this==Menu::stack.back()); } } } if(GetCurrentTheme().IsScaled()){ DrawScaledWindowBorder(game,pos); }else{ DrawTiledWindowBorder(game,pos); } if(draggingComponent!=nullptr){ game->SetDrawTarget(overlay.Sprite()); Pixel::Mode prevMode=game->GetPixelMode(); game->SetPixelMode(Pixel::MASK); game->Clear(BLANK); vf2d offsetPos=draggingComponent->rect.pos; if(!UsingMouseNavigation()){ MenuComponent*selectedComponent=buttons[selection.y][selection.x]; vf2d drawOffset{}; if(selectedComponent->parentComponent!=nullptr){ drawOffset+=selectedComponent->parentComponent->V(A::SCROLL_OFFSET); } draggingComponent->Draw(game,drawOffset+pos-offsetPos+selectedComponent->rect.pos+vi2d{1,-4}); }else{ draggingComponent->Draw(game,-offsetPos+game->GetMousePos()); } game->SetPixelMode(prevMode); game->SetDrawTarget(nullptr); overlay.Decal()->Update(); game->DrawDecal({0,0},overlay.Decal(),{1,1},this==Menu::stack.back()?WHITE:WHITE*"ThemeGlobal.MenuUnfocusedColorMult"_F); if(!UsingMouseNavigation()){ MenuComponent*selectedComponent=buttons[selection.y][selection.x]; vf2d drawOffset{}; if(selectedComponent->parentComponent!=nullptr){ drawOffset+=selectedComponent->parentComponent->V(A::SCROLL_OFFSET); } draggingComponent->DrawDecal(game,drawOffset+pos-offsetPos+selectedComponent->rect.pos+vi2d{1,-4},this==Menu::stack.back()); }else{ draggingComponent->DrawDecal(game,-offsetPos+game->GetMousePos(),this==Menu::stack.back()); } } }; void Menu::OpenMenu(MenuType menu,bool cover){ Menu::cover=cover; stack.clear(); stack.push_back(menus[menu]); } void Menu::KeyboardButtonNavigation(Crawler*game,vf2d menuPos){ vi2d prevSelection=selection; if(game->GetKey(RIGHT).bPressed){ if(selection==vi2d{-1,-1})return; SetMouseNavigation(false); selection.x=(size_t(selection.x)+1)%keyboardButtons[selection.y].size(); } if(game->GetKey(LEFT).bPressed){ if(selection==vi2d{-1,-1})return; selection.x--; SetMouseNavigation(false); if(selection.x<0)selection.x+=keyboardButtons[selection.y].size(); } if(game->GetKey(DOWN).bPressed||game->GetKey(UP).bPressed){ if(game->GetKey(DOWN).bPressed){ SetMouseNavigation(false); bool found=false; bool selectedItem=false; if(selection==vi2d{-1,-1}){ //Highlight first item. for(auto&key:keyboardButtons){ selection.y=key.first; break; } }else{ for(auto&key:keyboardButtons){ if(found){ //Once we discover the previous element, the next element becomes our next selection. int previousButtonX=keyboardButtons[selection.y][selection.x]->rect.pos.x; selection.y=key.first; int index=0; for(auto&button:key.second){ //Try to match a button in the same column as this button first. if(previousButtonX==button->rect.pos.x){ selection.x=index; break; } index++; } selectedItem=true; break; } if(key.first==selection.y //It's entirely possible this button was selected from the button selection list and may be out-of-bounds here. &&selection.x>=0&&selection.xGetKey(UP).bPressed){ SetMouseNavigation(false); if(selection==vi2d{-1,-1}){ //Highlight last item. for(auto&key:keyboardButtons){ selection.y=key.first; } }else{ int prevInd=-1; for(auto&key:keyboardButtons){ if(key.first==selection.y&& //It's entirely possible this button was selected from the button selection list and may be out-of-bounds here. selection.x>=0&&selection.xrect.pos.x; selection.y=prevInd; int index=0; for(auto&button:keyboardButtons[prevInd]){ //Try to match a button in the same column as this button first. if(previousButtonX==button->rect.pos.x){ selection.x=index; break; } index++; } }else{ //Since we didn't find it, it means we're at the top of the list or the list is empty. Go to the last element and use that one. int lastInd=-1; for(auto&key:keyboardButtons){ lastInd=key.first; } selection.y=lastInd; } } } //In both cases, we should clamp the X index to make sure it's still valid. if(selection.y!=-1){ selection.x=std::clamp(selection.x,0,int(keyboardButtons[selection.y].size())-1); }else{ selection.x=-1; } } if(game->GetMouse(0).bPressed||game->GetKey(ENTER).bPressed||game->GetKey(SPACE).bPressed){ SetMouseNavigation(game->GetMouse(0).bPressed); //If a click occurs we use mouse controls. if(!UsingMouseNavigation()){ buttonHoldTime=0; //Key presses automatically highlight the first button if it's not highlighted. if(selection==vi2d{-1,-1}&&buttons.size()>0){ //Find the first possible button entry in the map... int firstInd=-1; for(auto&key:buttons){ if(buttons[key.first].size()>0){ firstInd=key.first; break; } } if(firstInd!=-1){ //This means we found a valid menu item. If we didn't find one don't highlight any menu item... selection={0,firstInd}; } } }else{//Mouse click. selection={-1,-1}; for(auto&key:buttons){ int index=0; for(auto&button:key.second){ if(!button->disabled){ if(geom2d::overlaps(geom2d::rect{button->rect.pos+menuPos,button->rect.size},game->GetMousePos())){ selection={index,key.first}; break; } } index++; } } buttonHoldTime=0; } } if(prevSelection!=selection){ if(selection!=vi2d{-1,-1}&&buttons[selection.y][selection.x]->disabled){ bool handled=false; if(!UsingMouseNavigation()){ //Let's transfer some information about our selection being off the screen. Our intention with keyboard controls is that the screen will scroll to the correct location instead. //If we return false, then we handled it ourselves, no need to go back to the previous selection. if(HandleOutsideDisabledButtonSelection(buttons[selection.y][selection.x])){ handled=true; } } if(!handled){ // If the new selection of a button on this frame is disabled for some reason and we didn't handle it, we need to go back to what we had selected before. selection=prevSelection; } } } } void Menu::DrawScaledWindowBorder(Crawler*game,vf2d menuPos){ vf2d patchSize={"Interface.9PatchSize"_f[0],"Interface.9PatchSize"_f[1]}; //Upper-Left game->DrawPartialDecal(menuPos-patchSize,patchSize,GetPatchPart(0,0).Decal(),{patchSize.x*0,patchSize.y*0},patchSize,GetRenderColor()); //Upper-Right game->DrawPartialDecal(menuPos+vf2d{size.x,-patchSize.y},patchSize,GetPatchPart(2,0).Decal(),{patchSize.x*2,patchSize.y*0},patchSize,GetRenderColor()); //Bottom-Left game->DrawPartialDecal(menuPos+vf2d{-patchSize.x,size.y},patchSize,GetPatchPart(0,2).Decal(),{patchSize.x*0,patchSize.y*2},patchSize,GetRenderColor()); //Bottom-Right game->DrawPartialDecal(menuPos+vf2d{size.x,size.y},patchSize,GetPatchPart(2,2).Decal(),{patchSize.x*2,patchSize.y*2},patchSize,GetRenderColor()); //Top game->DrawPartialDecal(menuPos+vf2d{0,-patchSize.y},vf2d{size.x,patchSize.y},GetPatchPart(1,0).Decal(),{patchSize.x*1,patchSize.y*0},patchSize,GetRenderColor()); //Left game->DrawPartialDecal(menuPos+vf2d{-patchSize.x,0},vf2d{patchSize.x,size.y},GetPatchPart(0,1).Decal(),{patchSize.x*0,patchSize.y*1},patchSize,GetRenderColor()); //Right game->DrawPartialDecal(menuPos+vf2d{size.x,0},vf2d{patchSize.x,size.y},GetPatchPart(2,1).Decal(),{patchSize.x*2,patchSize.y*1},patchSize,GetRenderColor()); //Bottom game->DrawPartialDecal(menuPos+vf2d{0,size.y},vf2d{size.x,patchSize.y},GetPatchPart(1,2).Decal(),{patchSize.x*1,patchSize.y*2},patchSize,GetRenderColor()); } void Menu::DrawTiledWindowBorder(Crawler*game,vf2d menuPos){ vf2d patchSize={"Interface.9PatchSize"_f[0],"Interface.9PatchSize"_f[1]}; //Upper-Left game->DrawPartialDecal(menuPos-patchSize,patchSize,GetPatchPart(0,0).Decal(),{0,0},patchSize,GetRenderColor()); //Upper-Right game->DrawPartialDecal(menuPos+vf2d{size.x,-patchSize.y},patchSize,GetPatchPart(2,0).Decal(),{0,0},patchSize,GetRenderColor()); //Bottom-Left game->DrawPartialDecal(menuPos+vf2d{-patchSize.x,size.y},patchSize,GetPatchPart(0,2).Decal(),{0,0},patchSize,GetRenderColor()); //Bottom-Right game->DrawPartialDecal(menuPos+vf2d{size.x,size.y},patchSize,GetPatchPart(2,2).Decal(),{0,0},patchSize,GetRenderColor()); //Top game->DrawPartialDecal(menuPos+vf2d{0,-patchSize.y},vf2d{size.x,patchSize.y},GetPatchPart(1,0).Decal(),{0,0},vf2d{size.x,patchSize.y},GetRenderColor()); //Left game->DrawPartialDecal(menuPos+vf2d{-patchSize.x,0},vf2d{patchSize.x,size.y},GetPatchPart(0,1).Decal(),{0,0},vf2d{patchSize.x,size.y},GetRenderColor()); //Right game->DrawPartialDecal(menuPos+vf2d{size.x,0},vf2d{patchSize.x,size.y},GetPatchPart(2,1).Decal(),{0,0},vf2d{patchSize.x,size.y},GetRenderColor()); //Bottom game->DrawPartialDecal(menuPos+vf2d{0,size.y},vf2d{size.x,patchSize.y},GetPatchPart(1,2).Decal(),{0,0},vf2d{size.x,patchSize.y},GetRenderColor()); } void Menu::DrawScaledWindowBackground(Crawler*game,vf2d menuPos){ vf2d patchSize={"Interface.9PatchSize"_f[0],"Interface.9PatchSize"_f[1]}; //Center if(GetCurrentTheme().HasBackground()){ Decal*back=GetCurrentTheme().GetBackground(); game->DrawPartialDecal(menuPos,size,back,{0,0},back->sprite->Size(),GetRenderColor()); }else{ game->DrawPartialDecal(menuPos,size,GetPatchPart(1,1).Decal(),{patchSize.x*1,patchSize.y*1},patchSize,GetRenderColor()); } } void Menu::DrawTiledWindowBackground(Crawler*game,vf2d menuPos){ vf2d patchSize={"Interface.9PatchSize"_f[0],"Interface.9PatchSize"_f[1]}; //Center if(GetCurrentTheme().HasBackground()){ Decal*back=GetCurrentTheme().GetBackground(); game->DrawPartialDecal(menuPos,size,back,{0,0},size,GetRenderColor()); }else{ game->DrawPartialDecal(menuPos,size,GetPatchPart(1,1).Decal(),{0,0},patchSize,GetRenderColor()); } } Renderable&Menu::GetPatchPart(int x,int y){ return GFX[themeSelection+"_"+std::to_string(x)+std::to_string(y)+".png"]; } Theme&Menu::GetCurrentTheme(){ return themes[themeSelection]; } Pixel Menu::GetRenderColor(){ bool focused=Menu::stack.back()==this; Pixel col=WHITE; if(!focused){ col=WHITE*"ThemeGlobal.MenuUnfocusedColorMult"_F; } return col; } bool Menu::HandleOutsideDisabledButtonSelection(MenuComponent*disabledButton){ if(disabledButton->parentComponent!=nullptr){ return disabledButton->parentComponent->HandleOutsideDisabledButtonSelection(disabledButton); }else{ return false; } } bool Menu::UsingMouseNavigation(){ return MOUSE_NAVIGATION; }; void Menu::SetMouseNavigation(bool mouseNavigation){ if(MOUSE_NAVIGATION&&!mouseNavigation){ //When mouse navigation was enabled and now needs to be disabled, we store the mouse position. INCLUDE_game lastActiveMousePos=game->GetMousePos(); } MOUSE_NAVIGATION=mouseNavigation; }; void Menu::InventorySlotsUpdated(ITCategory cat){ //Update the inventory with a new inventory slot, since there's one additional item to interact with now. std::vector&inv=Inventory::get(cat); for(MenuComponent*component:inventoryListeners.at(cat)){ component->OnInventorySlotsUpdate(cat); } } void Menu::AddInventoryListener(MenuComponent*component,ITCategory category){ if(inventoryListeners.count(category)){ std::vector&listenerList=inventoryListeners.at(category); if(std::find(listenerList.begin(),listenerList.end(),component)!=listenerList.end()){ ERR("WARNING! Component "<name<<" has already been added to the "<0){ stack.pop_back(); }else{ ERR("WARNING! Trying to close out no menu?? Why are we doing this?") } } std::pairMenu::GetMemoryLeakReportInfo(){ return {lastMenuTypeCreated,lastRegisteredComponent}; } void Menu::CloseAllMenus(){ if(stack.size()>0){ stack.clear(); }else{ ERR("WARNING! Trying to close out no menu?? Why are we doing this?") } } bool Menu::IsMenuOpen(){ return stack.size()>0; } void Menu::CleanupAllMenus(){ for(auto key:Menu::menus){ Menu*menu=key.second; for(auto componentKey:menu->components){ MenuComponent*component=componentKey.second; component->Cleanup(); delete component; } menu->components.Reset(); menu->Cleanup(); delete menu; } Menu::menus.clear(); } void Menu::Cleanup(){}