# 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 :
ViewPort subWindow ;
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 ;
vf2d scrollOffset ;
protected :
inline bool OnScreen ( MenuComponent * component ) {
return geom2d : : overlaps ( geom2d : : rect < float > { { } , rect . size } , geom2d : : rect < float > { component - > rect . pos + 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 ;
}
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 - > originalPos . y ) ) ;
std : : vector < MenuComponent * > & keyboardButtonList = Menu : : menus [ button - > parentMenu ] - > keyboardButtons . at ( int ( button - > originalPos . y ) ) ;
size_t removedCount = 0 ;
removedCount + = std : : erase ( buttonList , button ) ;
removedCount + = std : : erase ( keyboardButtonList , button ) ;
removedCount + = Menu : : menus [ button - > parentMenu ] - > components . erase ( button - > GetName ( ) ) ;
if ( removedCount ! = 3 ) {
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 - > originalPos . y ) ) ) {
ERR ( " WARNING! Attempted to erase key " < < button - > originalPos . y < < " from button map, but the list still exists! " )
}
}
if ( keyboardButtonList . size ( ) = = 0 ) {
if ( ! Menu : : menus [ button - > parentMenu ] - > keyboardButtons . erase ( int ( button - > originalPos . y ) ) ) {
ERR ( " WARNING! Attempted to erase key " < < button - > originalPos . y < < " from button map, but the list still exists! " )
}
}
auto componentSearchResults = std : : find ( components . begin ( ) , components . end ( ) , button ) ;
if ( componentSearchResults = = components . end ( ) ) ERR ( " Could not find Component " < < std : : quoted ( button - > GetName ( ) ) < < " inside the component list! " ) ;
components . erase ( componentSearchResults ) ;
Menu : : menus [ button - > parentMenu ] - > RecalculateComponentCount ( ) ;
delete button ;
CalculateBounds ( ) ;
}
virtual inline void SetScrollAmount ( vf2d scrollOffset ) {
this - > scrollOffset = scrollOffset ;
for ( MenuComponent * component : components ) {
component - > rect . pos = component - > originalPos + scrollOffset ;
}
}
virtual inline vf2d GetScrollAmount ( ) {
return scrollOffset ;
}
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 ) { 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 ) ( { 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 ) { upButton - > Enable ( ! disabled ) ; }
if ( downButton ) { downButton - > Enable ( ! disabled ) ; }
}
virtual inline void BeforeUpdate ( Crawler * game ) override {
MenuComponent : : BeforeUpdate ( game ) ;
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 ;
SetScrollAmount ( { GetScrollAmount ( ) . x , ( - newScrollbarTop + 1 ) / scrollBarScale } ) ;
}
} else {
scrollBarHoverTime = std : : max ( scrollBarHoverTime - game - > GetElapsedTime ( ) , 0.f ) ;
}
if ( game - > GetMouseWheel ( ) ! = 0 ) {
if ( game - > GetMouseWheel ( ) > 0 ) {
SetScrollAmount ( GetScrollAmount ( ) + vf2d { 0 , " ThemeGlobal.MenuScrollWheelSpeed " _F } ) ;
} else {
SetScrollAmount ( GetScrollAmount ( ) - vf2d { 0 , " ThemeGlobal.MenuScrollWheelSpeed " _F } ) ;
}
}
if ( bounds . size . y - rect . size . y > 0 ) {
SetScrollAmount ( { GetScrollAmount ( ) . x , std : : clamp ( GetScrollAmount ( ) . y , - ( bounds . size . y - rect . size . y ) , 0.f ) } ) ;
} else {
SetScrollAmount ( { GetScrollAmount ( ) . x , 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 ;
}
}
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 - 11.75f , 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 - 11.75f , scrollBarTop + 12 } , { 12 , scrollBarHeight } , WHITE * focusedWindowColorMult ) ;
}
virtual inline void DrawDecal ( ViewPort & window , bool focused ) override {
MenuComponent : : DrawDecal ( window , focused ) ;
if ( border ) {
window . DrawRectDecal ( rect . pos , rect . size ) ;
}
for ( MenuComponent * component : components ) {
component - > _DrawDecal ( subWindow , focused ) ;
}
DrawScrollbar ( window , { } , focused ) ;
}
virtual bool GetHoverState ( Crawler * 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 ( ) ) ;
}
//Calculates the bounds of all components.
inline void CalculateBounds ( ) {
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 ;
}
}
}
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 ;
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 ( MenuComponent * disabledButton ) override {
//Set the offset so the center is highlighted by this button.
SetScrollAmount ( vf2d { GetScrollAmount ( ) . x , - disabledButton - > rect . pos . y + disabledButton - > rect . size . y } ) ;
return true ;
} ;
virtual void Cleanup ( ) override { }
inline std : : vector < MenuComponent * > & GetComponents ( ) {
return components ;
}
virtual inline void Enable ( bool enabled ) override final {
disabled = ! enabled ;
for ( MenuComponent * component : components ) {
component - > Enable ( enabled ) ;
}
if ( upButton ) { upButton - > Enable ( enabled ) ; }
if ( downButton ) { downButton - > Enable ( enabled ) ; }
} ;
} ;