|
|
|
#pragma region License
|
|
|
|
/*
|
|
|
|
License (OLC-3)
|
|
|
|
~~~~~~~~~~~~~~~
|
|
|
|
|
|
|
|
Copyright 2018 - 2023 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.
|
|
|
|
|
|
|
|
Portions of this software are copyright © 2023 The FreeType
|
|
|
|
Project (www.freetype.org). Please see LICENSE_FT.txt for more information.
|
|
|
|
All rights reserved.
|
|
|
|
*/
|
|
|
|
#pragma endregion
|
|
|
|
#pragma once
|
|
|
|
#include "Menu.h"
|
|
|
|
#include "MenuComponent.h"
|
|
|
|
#include "MenuItemButton.h"
|
|
|
|
#include "Crawler.h"
|
|
|
|
|
|
|
|
using A=Attribute;
|
|
|
|
|
|
|
|
class ScrollableWindowComponent:public MenuComponent{
|
|
|
|
protected:
|
|
|
|
Renderable r;
|
|
|
|
std::vector<MenuComponent*>components;
|
|
|
|
MenuComponent*upButton=nullptr;
|
|
|
|
MenuComponent*downButton=nullptr;
|
|
|
|
geom2d::rect<float>bounds; //It's for the scrollbar.
|
|
|
|
float scrollBarHeight=0;
|
|
|
|
float scrollBarTop=0;
|
|
|
|
bool scrollBarSelected=false;
|
|
|
|
float scrollBarHoverTime=0;
|
|
|
|
protected:
|
|
|
|
inline bool OnScreen(MenuComponent*component){
|
|
|
|
return geom2d::overlaps(geom2d::rect<float>{{},rect.size},geom2d::rect<float>{component->rect.pos+V(A::SCROLL_OFFSET)+vf2d{2,2},component->rect.size-vf2d{2,2}});
|
|
|
|
}
|
|
|
|
public:
|
|
|
|
inline ScrollableWindowComponent(geom2d::rect<float>rect,ComponentAttr attributes=ComponentAttr::BACKGROUND|ComponentAttr::OUTLINE)
|
|
|
|
:MenuComponent(rect,"",[](MenuFuncData data){return true;},ButtonAttr::UNSELECTABLE|ButtonAttr::UNSELECTABLE_VIA_KEYBOARD){
|
|
|
|
background=attributes&ComponentAttr::BACKGROUND;
|
|
|
|
border=attributes&ComponentAttr::OUTLINE;
|
|
|
|
r.Create(uint32_t(rect.size.x),uint32_t(rect.size.y));
|
|
|
|
}
|
|
|
|
virtual inline void RemoveAllComponents(){
|
|
|
|
while(components.size()>0){
|
|
|
|
RemoveButton(components.back());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
virtual inline void RemoveButton(MenuComponent*button){
|
|
|
|
std::vector<MenuComponent*>&buttonList=Menu::menus[button->parentMenu]->buttons.at(int(button->GetPos().y));
|
|
|
|
std::vector<MenuComponent*>&keyboardButtonList=Menu::menus[button->parentMenu]->keyboardButtons.at(int(button->GetPos().y));
|
|
|
|
size_t removedCount=0;
|
|
|
|
removedCount+=std::erase(buttonList,button);
|
|
|
|
removedCount+=std::erase(keyboardButtonList,button);
|
|
|
|
if(removedCount!=2){
|
|
|
|
std::cout<<"WARNING! Attempted to remove buttons from button listing, but not found!";
|
|
|
|
}
|
|
|
|
if(buttonList.size()==0){
|
|
|
|
if(!Menu::menus[button->parentMenu]->buttons.erase(int(button->GetPos().y))){
|
|
|
|
ERR("WARNING! Attempted to erase key "<<button->GetPos().y<<" from button map, but the list still exists!")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if(keyboardButtonList.size()==0){
|
|
|
|
if(!Menu::menus[button->parentMenu]->keyboardButtons.erase(int(button->GetPos().y))){
|
|
|
|
ERR("WARNING! Attempted to erase key "<<button->GetPos().y<<" from button map, but the list still exists!")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Menu::menus[button->parentMenu]->components.erase(button->GetName());
|
|
|
|
components.erase(std::find(components.begin(),components.end(),button));
|
|
|
|
Menu::menus[button->parentMenu]->RecalculateComponentCount();
|
|
|
|
delete button;
|
|
|
|
}
|
|
|
|
protected:
|
|
|
|
virtual inline void AfterCreate()override{
|
|
|
|
//Let's use the internal name of this component to add unique names for sub-components.
|
|
|
|
upButton=Menu::menus[parentMenu]->ADD(name+vf2d(rect.pos+vf2d{rect.size.x-12,0}).str()+"_"+vf2d(12,12).str(),MenuComponent)({rect.pos+vf2d{rect.size.x-12,0},{12,12}},"^",[&](MenuFuncData dat){V(A::SCROLL_OFFSET).y+="ThemeGlobal.MenuButtonScrollSpeed"_I;return true;},ButtonAttr::UNSELECTABLE_VIA_KEYBOARD)DEPTH -1 END;
|
|
|
|
downButton=Menu::menus[parentMenu]->ADD(name+vf2d(rect.pos+rect.size-vf2d{12,12}).str()+"_"+vf2d(12,12).str(),MenuComponent)({rect.pos+rect.size-vf2d{12,12},{12,12}},"v",[&](MenuFuncData dat){V(A::SCROLL_OFFSET).y-="ThemeGlobal.MenuButtonScrollSpeed"_I;return true;},ButtonAttr::UNSELECTABLE_VIA_KEYBOARD)DEPTH -1 END;
|
|
|
|
}
|
|
|
|
virtual inline void BeforeUpdate(Crawler*game)override{
|
|
|
|
for(MenuComponent*component:components){
|
|
|
|
component->BeforeUpdate(game);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
virtual inline void Update(Crawler*game)override{
|
|
|
|
MenuComponent::Update(game);
|
|
|
|
|
|
|
|
vf2d windowAbsPos=Menu::menus[parentMenu]->pos+rect.pos;
|
|
|
|
|
|
|
|
bool mouseOverScrollbar=geom2d::overlaps(geom2d::rect<float>(windowAbsPos+vf2d{rect.size.x-12,scrollBarTop+12},{12,scrollBarHeight}),game->GetMousePos());
|
|
|
|
|
|
|
|
if(mouseOverScrollbar||scrollBarSelected){
|
|
|
|
scrollBarHoverTime=std::min(scrollBarHoverTime+game->GetElapsedTime(),"ThemeGlobal.HighlightTime"_F);
|
|
|
|
if(game->GetMouse(0).bPressed){
|
|
|
|
scrollBarSelected=true;
|
|
|
|
}
|
|
|
|
if(game->GetMouse(0).bReleased){
|
|
|
|
scrollBarSelected=false;
|
|
|
|
}
|
|
|
|
if(scrollBarSelected){
|
|
|
|
float spaceBetweenTopAndBottomArrows=rect.size.y-24;
|
|
|
|
float viewHeight=rect.size.y;
|
|
|
|
|
|
|
|
|
|
|
|
float totalContentHeight=bounds.size.y;
|
|
|
|
if(totalContentHeight==0)totalContentHeight=1;
|
|
|
|
float scrollBarScale=(spaceBetweenTopAndBottomArrows/totalContentHeight);
|
|
|
|
//The scroll amount moves centered on the position the mouse is at.
|
|
|
|
float newScrollbarTop=(game->GetMousePos().y-windowAbsPos.y-12)-scrollBarHeight/2;
|
|
|
|
V(A::SCROLL_OFFSET).y=(-newScrollbarTop+1)/scrollBarScale; //Derived formula from the draw code.
|
|
|
|
}
|
|
|
|
}else{
|
|
|
|
scrollBarHoverTime=std::max(scrollBarHoverTime-game->GetElapsedTime(),0.f);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(game->GetMouseWheel()!=0){
|
|
|
|
if(game->GetMouseWheel()>0){
|
|
|
|
V(A::SCROLL_OFFSET).y+="ThemeGlobal.MenuScrollWheelSpeed"_I;
|
|
|
|
}else{
|
|
|
|
V(A::SCROLL_OFFSET).y-="ThemeGlobal.MenuScrollWheelSpeed"_I;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(bounds.size.y-rect.size.y>0){
|
|
|
|
V(A::SCROLL_OFFSET).y=std::clamp(V(A::SCROLL_OFFSET).y,-(bounds.size.y-rect.size.y),0.f);
|
|
|
|
}else{
|
|
|
|
V(A::SCROLL_OFFSET).y=0;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::sort(components.begin(),components.end(),[](MenuComponent*c1,MenuComponent*c2){return c1->depth>c2->depth;});
|
|
|
|
for(MenuComponent*component:components){
|
|
|
|
component->disabled=!OnScreen(component);
|
|
|
|
component->_Update(game);
|
|
|
|
}
|
|
|
|
|
|
|
|
upButton->disabled=false;
|
|
|
|
downButton->disabled=false;
|
|
|
|
if(geom2d::contains(rect,bounds)){//This means we have no reason to show a scrollbar.
|
|
|
|
upButton->disabled=true;
|
|
|
|
downButton->disabled=true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
virtual inline void Draw(Crawler*game,vf2d parentPos)override{
|
|
|
|
MenuComponent::Draw(game,parentPos);
|
|
|
|
Sprite*prevDrawTarget=game->GetDrawTarget();
|
|
|
|
game->SetDrawTarget(r.Sprite());
|
|
|
|
game->Clear(BLANK);
|
|
|
|
for(MenuComponent*component:components){
|
|
|
|
component->_Draw(game,V(A::SCROLL_OFFSET));
|
|
|
|
}
|
|
|
|
game->SetDrawTarget(prevDrawTarget);
|
|
|
|
game->DrawSprite(parentPos+rect.pos,r.Sprite());
|
|
|
|
}
|
|
|
|
|
|
|
|
inline void DrawScrollbar(Crawler*game,vf2d parentPos,bool focused){
|
|
|
|
float spaceBetweenTopAndBottomArrows=rect.size.y-24;
|
|
|
|
float viewHeight=rect.size.y;
|
|
|
|
float totalContentHeight=bounds.size.y;
|
|
|
|
if(totalContentHeight==0)totalContentHeight=1;
|
|
|
|
float scrollBarScale=(spaceBetweenTopAndBottomArrows/totalContentHeight);
|
|
|
|
scrollBarHeight=std::min(spaceBetweenTopAndBottomArrows,viewHeight*scrollBarScale-1);
|
|
|
|
scrollBarTop=-V(A::SCROLL_OFFSET).y*scrollBarScale+1;
|
|
|
|
|
|
|
|
float focusedWindowColorMult=(focused?1:"ThemeGlobal.MenuUnfocusedColorMult"_F);
|
|
|
|
|
|
|
|
game->FillRectDecal(rect.pos+parentPos+vf2d{rect.size.x-11.75f,scrollBarTop+12},{12,scrollBarHeight},PixelLerp(Menu::GetCurrentTheme().GetButtonCol(),Menu::GetCurrentTheme().GetHighlightCol(),scrollBarHoverTime/"ThemeGlobal.HighlightTime"_F)*focusedWindowColorMult);
|
|
|
|
game->DrawRectDecal(rect.pos+parentPos+vf2d{rect.size.x-11.75f,scrollBarTop+12},{12,scrollBarHeight},WHITE*focusedWindowColorMult);
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual inline void DrawDecal(Crawler*game,vf2d parentPos,bool focused)override{
|
|
|
|
MenuComponent::DrawDecal(game,parentPos,focused);
|
|
|
|
if(border){
|
|
|
|
game->DrawRectDecal(rect.pos+Menu::menus[parentMenu]->pos,rect.size);
|
|
|
|
}
|
|
|
|
for(MenuComponent*component:components){
|
|
|
|
component->_DrawDecal(game,rect.pos+Menu::menus[parentMenu]->pos+V(A::SCROLL_OFFSET),focused);
|
|
|
|
}
|
|
|
|
DrawScrollbar(game,Menu::menus[parentMenu]->pos,focused);
|
|
|
|
}
|
|
|
|
virtual bool GetHoverState(Crawler*game,MenuComponent*child)override{
|
|
|
|
return geom2d::overlaps(geom2d::rect<float>{Menu::menus[parentMenu]->pos+rect.pos+child->rect.pos+V(A::SCROLL_OFFSET),child->rect.size},game->GetMousePos());
|
|
|
|
}
|
|
|
|
//Calculates the bounds of all components.
|
|
|
|
geom2d::rect<float> inline CalculateBounds(){
|
|
|
|
geom2d::rect<float>bounds;
|
|
|
|
for(MenuComponent*component:components){
|
|
|
|
if(component->rect.pos.x<bounds.pos.x){
|
|
|
|
float sizeIncrease=bounds.pos.x-component->rect.pos.x;
|
|
|
|
bounds.size.x+=sizeIncrease;
|
|
|
|
bounds.pos.x=component->rect.pos.x;
|
|
|
|
}
|
|
|
|
if(component->rect.right().start.x>bounds.right().start.x){
|
|
|
|
float sizeIncrease=component->rect.right().start.x-bounds.right().start.x;
|
|
|
|
bounds.size.x+=sizeIncrease;
|
|
|
|
}
|
|
|
|
if(component->rect.pos.y<bounds.pos.y){
|
|
|
|
float sizeIncrease=bounds.pos.y-component->rect.pos.y;
|
|
|
|
bounds.size.y+=sizeIncrease;
|
|
|
|
bounds.pos.y=component->rect.pos.y;
|
|
|
|
}
|
|
|
|
if(component->rect.bottom().start.y>bounds.bottom().start.y){
|
|
|
|
float sizeIncrease=component->rect.bottom().start.y-bounds.bottom().start.y;
|
|
|
|
bounds.size.y+=sizeIncrease;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return bounds;
|
|
|
|
}
|
|
|
|
public:
|
|
|
|
template<class T>
|
|
|
|
T* AddComponent(std::string key,T*button){
|
|
|
|
components.push_back(button);
|
|
|
|
button->renderInMain=false; //Now we are in control!
|
|
|
|
button->parentComponent=this;
|
|
|
|
|
|
|
|
if(button->rect.pos.x<bounds.pos.x){
|
|
|
|
float sizeIncrease=bounds.pos.x-button->rect.pos.x;
|
|
|
|
bounds.size.x+=sizeIncrease;
|
|
|
|
bounds.pos.x=button->rect.pos.x;
|
|
|
|
}
|
|
|
|
if(button->rect.right().start.x>bounds.right().start.x){
|
|
|
|
float sizeIncrease=button->rect.right().start.x-bounds.right().start.x;
|
|
|
|
bounds.size.x+=sizeIncrease;
|
|
|
|
}
|
|
|
|
if(button->rect.pos.y<bounds.pos.y){
|
|
|
|
float sizeIncrease=bounds.pos.y-button->rect.pos.y;
|
|
|
|
bounds.size.y+=sizeIncrease;
|
|
|
|
bounds.pos.y=button->rect.pos.y;
|
|
|
|
}
|
|
|
|
if(button->rect.bottom().start.y>bounds.bottom().start.y){
|
|
|
|
float sizeIncrease=button->rect.bottom().start.y-bounds.bottom().start.y;
|
|
|
|
bounds.size.y+=sizeIncrease;
|
|
|
|
}
|
|
|
|
|
|
|
|
Menu::menus[parentMenu]->AddComponent(key,button);
|
|
|
|
return button;
|
|
|
|
}
|
|
|
|
virtual inline bool PointWithinParent(MenuComponent*child,vi2d drawPos)override{
|
|
|
|
return geom2d::overlaps(geom2d::rect<float>{Menu::menus[parentMenu]->pos+rect.pos,rect.size},drawPos);
|
|
|
|
}
|
|
|
|
virtual inline bool HandleOutsideDisabledButtonSelection(MenuComponent*disabledButton)override{
|
|
|
|
//Set the offset so the center is highlighted by this button.
|
|
|
|
V(A::SCROLL_OFFSET).y=-(disabledButton->rect.pos.y-disabledButton->rect.size.y/2);
|
|
|
|
V(A::SCROLL_OFFSET).y+=rect.size.y/2;
|
|
|
|
return true;
|
|
|
|
};
|
|
|
|
virtual void Cleanup()override{}
|
|
|
|
inline std::vector<MenuComponent*>&GetComponents(){
|
|
|
|
return components;
|
|
|
|
}
|
|
|
|
virtual inline void Enable(bool enabled)override final{
|
|
|
|
disabled=!enabled;
|
|
|
|
if(upButton){upButton->Enable(enabled);}
|
|
|
|
if(downButton){downButton->Enable(enabled);}
|
|
|
|
};
|
|
|
|
};
|