The open source repository for the action RPG game in development by Sig Productions titled 'Adventures in Lestoria'!
https://forums.lestoria.net
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
323 lines
16 KiB
323 lines
16 KiB
#pragma region License
|
|
/*
|
|
License (OLC-3)
|
|
~~~~~~~~~~~~~~~
|
|
|
|
Copyright 2024 Joshua Sigona <sigonasr2@gmail.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 © 2024 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 "AdventuresInLestoria.h"
|
|
|
|
using A=Attribute;
|
|
|
|
class ScrollableWindowComponent:public MenuComponent{
|
|
friend class Menu;
|
|
protected:
|
|
ViewPort subWindow;
|
|
std::vector<std::weak_ptr<MenuComponent>>components;
|
|
std::weak_ptr<MenuComponent>upButton;
|
|
std::weak_ptr<MenuComponent>downButton;
|
|
geom2d::rect<float>bounds; //It's for the scrollbar.
|
|
float scrollBarHeight=0;
|
|
float scrollBarTop=0;
|
|
bool scrollBarSelected=false;
|
|
float scrollBarHoverTime=0;
|
|
vf2d scrollOffset{};
|
|
vf2d targetScrollOffset{};
|
|
float lastScrollUpdate=0.f;
|
|
float selectionIndex=0.f;
|
|
int selectionSkipIncrement=1; //How many items are on each row. This determines the increments that we skip by while scrolling.
|
|
protected:
|
|
inline bool OnScreen(std::weak_ptr<MenuComponent>component){
|
|
return geom2d::overlaps(geom2d::rect<float>{{},rect.size},geom2d::rect<float>{component.lock()->rect.pos+vf2d{2,2},component.lock()->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;
|
|
}
|
|
virtual inline void RemoveAllComponents(){
|
|
while(components.size()>0){
|
|
RemoveButton(components.back());
|
|
}
|
|
}
|
|
virtual inline void RemoveButton(std::weak_ptr<MenuComponent>button){
|
|
auto componentSearchResults=std::find_if(components.begin(),components.end(),[&](std::weak_ptr<MenuComponent>ptr){return &*ptr.lock()==&*button.lock();});
|
|
if(componentSearchResults==components.end())ERR("Could not find Component"<<std::quoted(button.lock()->GetName())<<" inside the component list!");
|
|
components.erase(componentSearchResults);
|
|
size_t removedCount=0;
|
|
|
|
MenuType parentMenu=button.lock()->parentMenu;
|
|
|
|
removedCount+=Menu::menus[parentMenu]->components.erase(button.lock()->GetName());
|
|
if(removedCount!=1){
|
|
std::cout<<"WARNING! Attempted to remove buttons from button listing, but not found!";
|
|
}
|
|
Menu::menus[parentMenu]->RecalculateComponentCount();
|
|
CalculateBounds();
|
|
}
|
|
virtual inline void SetScrollAmount(vf2d scrollOffset){
|
|
this->targetScrollOffset=scrollOffset;
|
|
}
|
|
//Provide an amount to scroll by in units/sec.
|
|
virtual inline void Scroll(float amt){
|
|
SetScrollAmount(GetTargetScrollAmount()-vf2d{0,amt*game->GetElapsedTime()*"Interface.AnalogScrollSpeed"_F});
|
|
}
|
|
//Use this when you need to add more scrolling offset to a previous amount as GetScrollAmount() is used to get the internal scroll offset specifically.
|
|
virtual inline vf2d GetTargetScrollAmount()const{
|
|
return targetScrollOffset;
|
|
}
|
|
virtual bool GetHoverState(AiL*game,MenuComponent*child)override{
|
|
return geom2d::overlaps(geom2d::rect<float>{Menu::menus[parentMenu]->pos+rect.pos,rect.size},game->GetMousePos())&& //Make sure the mouse is inside the parent window component first....
|
|
geom2d::overlaps(geom2d::rect<float>{Menu::menus[parentMenu]->pos+rect.pos+child->rect.pos,child->rect.size},game->GetMousePos());
|
|
}
|
|
|
|
inline void SetSelectionSkipIncrement(const int selectionSkipIncrement){
|
|
this->selectionSkipIncrement=selectionSkipIncrement;
|
|
}
|
|
|
|
inline void IncreaseSelectionIndex(const float val){
|
|
float prevIndex=selectionIndex/selectionSkipIncrement;
|
|
selectionIndex=std::clamp(selectionIndex+val,0.f,float(components.size()-1));
|
|
if(size_t(prevIndex)!=size_t(selectionIndex/selectionSkipIncrement)){Menu::menus[parentMenu]->SetSelection(components[size_t(selectionIndex)],false);}
|
|
}
|
|
protected:
|
|
virtual inline vf2d GetScrollAmount()const{
|
|
return scrollOffset;
|
|
}
|
|
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)(geom2d::rect<float>{rect.pos+vf2d{rect.size.x-12,0},{12,12}},"^",[&](MenuFuncData dat){SetScrollAmount(GetScrollAmount()+vf2d{0,"ThemeGlobal.MenuButtonScrollSpeed"_F});return true;},ButtonAttr::UNSELECTABLE_VIA_KEYBOARD)DEPTH depth-1 END;
|
|
downButton=Menu::menus[parentMenu]->ADD(name+vf2d(rect.pos+rect.size-vf2d{12,12}).str()+"_"+vf2d(12,12).str(),MenuComponent)(geom2d::rect<float>{rect.pos+rect.size-vf2d{12,12},{12,12}},"v",[&](MenuFuncData dat){SetScrollAmount(GetScrollAmount()-vf2d{0,"ThemeGlobal.MenuButtonScrollSpeed"_F});return true;},ButtonAttr::UNSELECTABLE_VIA_KEYBOARD)DEPTH depth-1 END;
|
|
subWindow=ViewPort::rectViewPort({},rect.size,Menu::menus[parentMenu]->pos+rect.pos);
|
|
if(!upButton.expired()){upButton.lock()->Enable(!disabled);}
|
|
if(!downButton.expired()){downButton.lock()->Enable(!disabled);}
|
|
}
|
|
virtual inline void BeforeUpdate(AiL*game)override{
|
|
MenuComponent::BeforeUpdate(game);
|
|
for(std::weak_ptr<MenuComponent>component:components){
|
|
std::shared_ptr<MenuComponent>componentPtr=component.lock();
|
|
if(componentPtr->renderInMain)ERR(std::format("WARNING! Component {} is inside a ScrollableWindowComponent but renders in main instead! Parent Component: {}",componentPtr->GetName(),GetName()));
|
|
componentPtr->_BeforeUpdate(game);
|
|
}
|
|
}
|
|
virtual inline void Update(AiL*game)override{
|
|
MenuComponent::Update(game);
|
|
|
|
lastScrollUpdate=std::max(0.f,lastScrollUpdate-game->GetElapsedTime());
|
|
|
|
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&&!geom2d::contains(rect,bounds)){
|
|
scrollBarSelected=true;
|
|
}
|
|
if(game->GetMouse(0).bReleased){
|
|
scrollBarSelected=false;
|
|
Menu::scrolling=false;
|
|
}
|
|
if(scrollBarSelected){
|
|
Menu::scrolling=true;
|
|
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;
|
|
SetScrollAmount({GetScrollAmount().x,(-newScrollbarTop+1)/scrollBarScale});
|
|
}
|
|
}else{
|
|
scrollBarHoverTime=std::max(scrollBarHoverTime-game->GetElapsedTime(),0.f);
|
|
}
|
|
|
|
if(game->GetMouseWheel()!=0){
|
|
if(game->GetMouseWheel()>0){
|
|
SetScrollAmount(GetTargetScrollAmount()+vf2d{0,"ThemeGlobal.MenuScrollWheelSpeed"_F});
|
|
}else{
|
|
SetScrollAmount(GetTargetScrollAmount()-vf2d{0,"ThemeGlobal.MenuScrollWheelSpeed"_F});
|
|
}
|
|
}
|
|
|
|
if(bounds.size.y-rect.size.y>0){
|
|
scrollOffset.y=std::clamp(GetScrollAmount().y,-(bounds.size.y-rect.size.y),0.f);
|
|
SetScrollAmount({targetScrollOffset.x,std::clamp(targetScrollOffset.y,-(bounds.size.y-rect.size.y),0.f)});
|
|
selectionIndex=std::clamp(selectionIndex,0.f,float(components.size()-1));
|
|
}else{
|
|
scrollOffset.y=0;
|
|
SetScrollAmount({targetScrollOffset.x,0});
|
|
selectionIndex=std::clamp(selectionIndex,0.f,float(components.size()-1));
|
|
}
|
|
|
|
std::sort(components.begin(),components.end(),[](std::weak_ptr<MenuComponent>c1,std::weak_ptr<MenuComponent>c2){return c1.lock()->depth>c2.lock()->depth;});
|
|
for(std::weak_ptr<MenuComponent>component:components){
|
|
component.lock()->disabled=!OnScreen(component.lock());
|
|
component.lock()->_Update(game);
|
|
}
|
|
|
|
upButton.lock()->disabled=false;
|
|
downButton.lock()->disabled=false;
|
|
if(geom2d::contains(rect,bounds)){//This means we have no reason to show a scrollbar.
|
|
upButton.lock()->disabled=true;
|
|
downButton.lock()->disabled=true;
|
|
}
|
|
|
|
#pragma region Move scroll offset towards target offset
|
|
if(scrollOffset.y!=targetScrollOffset.y){
|
|
if(lastScrollUpdate==0.f){
|
|
float diff=fabs(targetScrollOffset.y-scrollOffset.y);
|
|
|
|
if(targetScrollOffset.y>scrollOffset.y){
|
|
scrollOffset.y+=diff/4.f;
|
|
if(targetScrollOffset.y<scrollOffset.y){
|
|
scrollOffset.y=targetScrollOffset.y;
|
|
}
|
|
}else{
|
|
scrollOffset.y-=diff/4.f;
|
|
if(targetScrollOffset.y>scrollOffset.y){
|
|
scrollOffset.y=targetScrollOffset.y;
|
|
}
|
|
}
|
|
|
|
for(std::weak_ptr<MenuComponent>component:components){
|
|
component.lock()->rect.pos=component.lock()->originalPos+scrollOffset;
|
|
}
|
|
lastScrollUpdate=1/60.f;
|
|
}
|
|
}
|
|
#pragma endregion
|
|
}
|
|
inline void DrawScrollbar(ViewPort&window,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=-GetScrollAmount().y*scrollBarScale+1;
|
|
|
|
float focusedWindowColorMult=(focused?1:"ThemeGlobal.MenuUnfocusedColorMult"_F);
|
|
|
|
window.FillRectDecal(rect.pos+parentPos+vf2d{rect.size.x-12.f,scrollBarTop+12},{12,scrollBarHeight},PixelLerp(Menu::GetCurrentTheme().GetButtonCol(),Menu::GetCurrentTheme().GetHighlightCol(),scrollBarHoverTime/"ThemeGlobal.HighlightTime"_F)*focusedWindowColorMult);
|
|
window.DrawRectDecal(rect.pos+parentPos+vf2d{rect.size.x-12.f,scrollBarTop+12},{12,scrollBarHeight},WHITE*focusedWindowColorMult);
|
|
}
|
|
|
|
inline float GetComponentIndex(std::weak_ptr<MenuComponent>comp){
|
|
return float(std::distance(GetComponents().begin(),std::find_if(GetComponents().begin(),GetComponents().end(),[&](auto&component){return &*comp.lock()==&*component.lock();})));
|
|
}
|
|
|
|
virtual inline void DrawDecal(ViewPort&window,bool focused)override{
|
|
MenuComponent::DrawDecal(window,focused);
|
|
if(border){
|
|
window.DrawRectDecal(rect.pos,rect.size);
|
|
}
|
|
for(std::weak_ptr<MenuComponent>component:components){
|
|
component.lock()->_DrawDecal(subWindow,focused);
|
|
}
|
|
if(!geom2d::contains(rect,bounds)){
|
|
DrawScrollbar(window,{},focused);
|
|
}
|
|
}
|
|
//Calculates the bounds of all components.
|
|
inline void CalculateBounds(){
|
|
bounds={};
|
|
for(std::weak_ptr<MenuComponent>component:components){
|
|
if(component.lock()->rect.pos.x<bounds.pos.x){
|
|
float sizeIncrease=bounds.pos.x-component.lock()->rect.pos.x;
|
|
bounds.size.x+=sizeIncrease;
|
|
bounds.pos.x=component.lock()->rect.pos.x;
|
|
}
|
|
if(component.lock()->rect.right().start.x>bounds.right().start.x){
|
|
float sizeIncrease=component.lock()->rect.right().start.x-bounds.right().start.x;
|
|
bounds.size.x+=sizeIncrease;
|
|
}
|
|
if(component.lock()->rect.pos.y<bounds.pos.y){
|
|
float sizeIncrease=bounds.pos.y-component.lock()->rect.pos.y;
|
|
bounds.size.y+=sizeIncrease;
|
|
bounds.pos.y=component.lock()->rect.pos.y;
|
|
}
|
|
if(component.lock()->rect.bottom().start.y>bounds.bottom().start.y){
|
|
float sizeIncrease=component.lock()->rect.bottom().start.y-bounds.bottom().start.y;
|
|
bounds.size.y+=sizeIncrease;
|
|
}
|
|
}
|
|
}
|
|
public:
|
|
template<class T>
|
|
std::shared_ptr<T> _AddComponent(std::string key,std::shared_ptr<T>button){
|
|
if(Menu::menus[parentMenu]->components.count(key)){
|
|
ERR("WARNING! Key "<<key<<" for menu"<<parentMenu<<" already exists! Key names must be unique!")
|
|
}
|
|
|
|
components.push_back(button);
|
|
button->renderInMain=false; //Now we are in control!
|
|
button->parentComponent=DYNAMIC_POINTER_CAST<ScrollableWindowComponent>(Menu::menus[parentMenu]->components[this->GetName()]);
|
|
button->disabled=disabled;
|
|
|
|
CalculateBounds();
|
|
|
|
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 PointWithinParent(MenuComponent*child,geom2d::rect<float> drawRect)override{
|
|
return geom2d::overlaps(geom2d::rect<float>{Menu::menus[parentMenu]->pos+rect.pos,rect.size},drawRect);
|
|
}
|
|
virtual inline bool HandleOutsideDisabledButtonSelection(std::weak_ptr<MenuComponent>disabledButton)override{
|
|
//Set the offset so the center is highlighted by this button.
|
|
SetScrollAmount(vf2d{GetScrollAmount().x,GetScrollAmount().y-disabledButton.lock()->rect.pos.y+disabledButton.lock()->rect.size.y});
|
|
return true;
|
|
};
|
|
virtual void Cleanup()override{}
|
|
inline std::vector<std::weak_ptr<MenuComponent>>&GetComponents(){
|
|
return components;
|
|
}
|
|
virtual inline void Enable(bool enabled)override final{
|
|
disabled=!enabled;
|
|
for(std::weak_ptr<MenuComponent>component:components){
|
|
component.lock()->Enable(enabled);
|
|
}
|
|
if(upButton.lock()){upButton.lock()->Enable(enabled);}
|
|
if(downButton.lock()){downButton.lock()->Enable(enabled);}
|
|
};
|
|
}; |