Port CScrollRegion from upstream with some minor refactorings

Co-authored-by: LordSk <lordskelethom@gmail.com>
This commit is contained in:
Robert Müller 2022-08-13 13:58:11 +02:00
parent 9cbca642ea
commit f451a33361
7 changed files with 372 additions and 0 deletions

View file

@ -2103,6 +2103,8 @@ if(CLIENT)
ui.h
ui_rect.cpp
ui_rect.h
ui_scrollregion.cpp
ui_scrollregion.h
)
set_src(GAME_EDITOR GLOB src/game/editor
auto_map.cpp

View file

@ -374,6 +374,8 @@ void CGameClient::OnInit()
void CGameClient::OnUpdate()
{
CUIElementBase::Init(UI()); // update static pointer because game and editor use separate UI
// handle mouse movement
float x = 0.0f, y = 0.0f;
IInput::ECursorType CursorType = Input()->CursorRelative(&x, &y);

View file

@ -82,6 +82,13 @@ const CLinearScrollbarScale CUI::ms_LinearScrollbarScale;
const CLogarithmicScrollbarScale CUI::ms_LogarithmicScrollbarScale(25);
float CUI::ms_FontmodHeight = 0.8f;
CUI *CUIElementBase::s_pUI = nullptr;
IClient *CUIElementBase::Client() const { return s_pUI->Client(); }
IGraphics *CUIElementBase::Graphics() const { return s_pUI->Graphics(); }
IInput *CUIElementBase::Input() const { return s_pUI->Input(); }
ITextRender *CUIElementBase::TextRender() const { return s_pUI->TextRender(); }
void CUI::Init(IKernel *pKernel)
{
m_pClient = pKernel->RequestInterface<IClient>();
@ -90,6 +97,7 @@ void CUI::Init(IKernel *pKernel)
m_pTextRender = pKernel->RequestInterface<ITextRender>();
InitInputs(m_pInput->GetEventsRaw(), m_pInput->GetEventCountRaw());
CUIRect::Init(m_pGraphics);
CUIElementBase::Init(this);
}
void CUI::InitInputs(IInput::CEvent *pInputEventsArray, int *pInputEventCount)

View file

@ -158,6 +158,21 @@ struct SLabelProperties
bool m_EnableWidthCheck = true;
};
class CUIElementBase
{
private:
static CUI *s_pUI;
public:
static void Init(CUI *pUI) { s_pUI = pUI; }
IClient *Client() const;
IGraphics *Graphics() const;
IInput *Input() const;
ITextRender *TextRender() const;
CUI *UI() const { return s_pUI; }
};
class CButtonContainer
{
};

View file

@ -0,0 +1,220 @@
/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
/* If you are missing that file, acquire a complete release at teeworlds.com. */
#include <base/system.h>
#include <base/vmath.h>
#include <engine/client.h>
#include <engine/keys.h>
#include "ui_scrollregion.h"
CScrollRegion::CScrollRegion()
{
m_ScrollY = 0.0f;
m_ContentH = 0.0f;
m_AnimTime = 0.0f;
m_AnimInitScrollY = 0.0f;
m_AnimTargetScrollY = 0.0f;
m_RequestScrollY = -1.0f;
m_ContentScrollOff = vec2(0.0f, 0.0f);
m_Params = CScrollRegionParams();
}
void CScrollRegion::Begin(CUIRect *pClipRect, vec2 *pOutOffset, CScrollRegionParams *pParams)
{
if(pParams)
m_Params = *pParams;
const bool ContentOverflows = m_ContentH > pClipRect->h;
const bool ForceShowScrollbar = m_Params.m_Flags & CScrollRegionParams::FLAG_CONTENT_STATIC_WIDTH;
CUIRect ScrollBarBg;
bool HasScrollBar = ContentOverflows || ForceShowScrollbar;
CUIRect *pModifyRect = HasScrollBar ? pClipRect : nullptr;
pClipRect->VSplitRight(m_Params.m_ScrollbarWidth, pModifyRect, &ScrollBarBg);
ScrollBarBg.Margin(m_Params.m_ScrollbarMargin, &m_RailRect);
// only show scrollbar if required
if(HasScrollBar)
{
if(m_Params.m_ScrollbarBgColor.a > 0.0f)
ScrollBarBg.Draw(m_Params.m_ScrollbarBgColor, IGraphics::CORNER_R, 4.0f);
if(m_Params.m_RailBgColor.a > 0.0f)
m_RailRect.Draw(m_Params.m_RailBgColor, IGraphics::CORNER_ALL, m_RailRect.w / 2.0f);
}
if(!ContentOverflows)
m_ContentScrollOff.y = 0.0f;
if(m_Params.m_ClipBgColor.a > 0.0f)
pClipRect->Draw(m_Params.m_ClipBgColor, HasScrollBar ? IGraphics::CORNER_L : IGraphics::CORNER_ALL, 4.0f);
UI()->ClipEnable(pClipRect);
m_ClipRect = *pClipRect;
m_ContentH = 0.0f;
*pOutOffset = m_ContentScrollOff;
}
void CScrollRegion::End()
{
UI()->ClipDisable();
// only show scrollbar if content overflows
if(m_ContentH <= m_ClipRect.h)
return;
// scroll wheel
CUIRect RegionRect = m_ClipRect;
RegionRect.w += m_Params.m_ScrollbarWidth;
const float AnimationDuration = 0.5f;
if(UI()->Enabled() && UI()->MouseHovered(&RegionRect))
{
const bool IsPageScroll = Input()->KeyIsPressed(KEY_LALT) || Input()->KeyIsPressed(KEY_RALT);
const float ScrollUnit = IsPageScroll ? m_ClipRect.h : m_Params.m_ScrollUnit;
if(Input()->KeyPress(KEY_MOUSE_WHEEL_UP))
{
m_AnimTime = AnimationDuration;
m_AnimInitScrollY = m_ScrollY;
m_AnimTargetScrollY -= ScrollUnit;
}
else if(Input()->KeyPress(KEY_MOUSE_WHEEL_DOWN))
{
m_AnimTime = AnimationDuration;
m_AnimInitScrollY = m_ScrollY;
m_AnimTargetScrollY += ScrollUnit;
}
}
const float SliderHeight = maximum(m_Params.m_SliderMinHeight, m_ClipRect.h / m_ContentH * m_RailRect.h);
CUIRect Slider = m_RailRect;
Slider.h = SliderHeight;
const float MaxSlider = m_RailRect.h - SliderHeight;
const float MaxScroll = m_ContentH - m_ClipRect.h;
if(m_RequestScrollY >= 0.0f)
{
m_AnimTargetScrollY = m_RequestScrollY;
m_AnimTime = 0.0f;
m_RequestScrollY = -1.0f;
}
m_AnimTargetScrollY = clamp(m_AnimTargetScrollY, 0.0f, MaxScroll);
if(absolute(m_AnimInitScrollY - m_AnimTargetScrollY) < 0.5f)
m_AnimTime = 0.0f;
if(m_AnimTime > 0.0f)
{
m_AnimTime -= Client()->RenderFrameTime();
float AnimProgress = (1.0f - powf(m_AnimTime / AnimationDuration, 3.0f)); // cubic ease out
m_ScrollY = m_AnimInitScrollY + (m_AnimTargetScrollY - m_AnimInitScrollY) * AnimProgress;
}
else
{
m_ScrollY = m_AnimTargetScrollY;
}
Slider.y += m_ScrollY / MaxScroll * MaxSlider;
bool Hovered = false;
bool Grabbed = false;
const void *pID = &m_ScrollY;
const bool InsideSlider = UI()->MouseHovered(&Slider);
const bool InsideRail = UI()->MouseHovered(&m_RailRect);
if(UI()->CheckActiveItem(pID) && UI()->MouseButton(0))
{
float MouseY = UI()->MouseY();
m_ScrollY += (MouseY - (Slider.y + m_SliderGrabPos.y)) / MaxSlider * MaxScroll;
m_SliderGrabPos.y = clamp(m_SliderGrabPos.y, 0.0f, SliderHeight);
m_AnimTargetScrollY = m_ScrollY;
m_AnimTime = 0.0f;
Grabbed = true;
}
else if(InsideSlider)
{
UI()->SetHotItem(pID);
if(!UI()->CheckActiveItem(pID) && UI()->MouseButtonClicked(0))
{
UI()->SetActiveItem(pID);
m_SliderGrabPos.y = UI()->MouseY() - Slider.y;
m_AnimTargetScrollY = m_ScrollY;
m_AnimTime = 0.0f;
}
Hovered = true;
}
else if(InsideRail && UI()->MouseButtonClicked(0))
{
m_ScrollY += (UI()->MouseY() - (Slider.y + Slider.h / 2.0f)) / MaxSlider * MaxScroll;
UI()->SetActiveItem(pID);
m_SliderGrabPos.y = Slider.h / 2.0f;
m_AnimTargetScrollY = m_ScrollY;
m_AnimTime = 0.0f;
Hovered = true;
}
else if(UI()->CheckActiveItem(pID) && !UI()->MouseButton(0))
{
UI()->SetActiveItem(nullptr);
}
m_ScrollY = clamp(m_ScrollY, 0.0f, MaxScroll);
m_ContentScrollOff.y = -m_ScrollY;
Slider.Draw(m_Params.SliderColor(Grabbed, Hovered), IGraphics::CORNER_ALL, Slider.w / 2.0f);
}
bool CScrollRegion::AddRect(const CUIRect &Rect, bool ShouldScrollHere)
{
m_LastAddedRect = Rect;
// Round up and add 1 to fix pixel clipping at the end of the scrolling area
m_ContentH = maximum(ceilf(Rect.y + Rect.h - (m_ClipRect.y + m_ContentScrollOff.y)) + 1.0f, m_ContentH);
if(ShouldScrollHere)
ScrollHere();
return !IsRectClipped(Rect);
}
void CScrollRegion::ScrollHere(EScrollOption Option)
{
const float MinHeight = minimum(m_ClipRect.h, m_LastAddedRect.h);
const float TopScroll = m_LastAddedRect.y - (m_ClipRect.y + m_ContentScrollOff.y);
switch(Option)
{
case SCROLLHERE_TOP:
m_RequestScrollY = TopScroll;
break;
case SCROLLHERE_BOTTOM:
m_RequestScrollY = TopScroll - (m_ClipRect.h - MinHeight);
break;
case SCROLLHERE_KEEP_IN_VIEW:
default:
const float DeltaY = m_LastAddedRect.y - m_ClipRect.y;
if(DeltaY < 0)
m_RequestScrollY = TopScroll;
else if(DeltaY > (m_ClipRect.h - MinHeight))
m_RequestScrollY = TopScroll - (m_ClipRect.h - MinHeight);
break;
}
}
bool CScrollRegion::IsRectClipped(const CUIRect &Rect) const
{
return (m_ClipRect.x > (Rect.x + Rect.w) || (m_ClipRect.x + m_ClipRect.w) < Rect.x || m_ClipRect.y > (Rect.y + Rect.h) || (m_ClipRect.y + m_ClipRect.h) < Rect.y);
}
bool CScrollRegion::IsScrollbarShown() const
{
return m_ContentH > m_ClipRect.h;
}
bool CScrollRegion::IsAnimating() const
{
return m_AnimTime > 0.0f;
}

View file

@ -0,0 +1,123 @@
/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
/* If you are missing that file, acquire a complete release at teeworlds.com. */
#ifndef GAME_CLIENT_UI_SCROLLREGION_H
#define GAME_CLIENT_UI_SCROLLREGION_H
#include "ui.h"
struct CScrollRegionParams
{
float m_ScrollbarWidth;
float m_ScrollbarMargin;
float m_SliderMinHeight;
float m_ScrollUnit;
ColorRGBA m_ClipBgColor;
ColorRGBA m_ScrollbarBgColor;
ColorRGBA m_RailBgColor;
ColorRGBA m_SliderColor;
ColorRGBA m_SliderColorHover;
ColorRGBA m_SliderColorGrabbed;
unsigned m_Flags;
enum
{
FLAG_CONTENT_STATIC_WIDTH = 1 << 0,
};
CScrollRegionParams()
{
m_ScrollbarWidth = 20.0f;
m_ScrollbarMargin = 5.0f;
m_SliderMinHeight = 25.0f;
m_ScrollUnit = 10.0f;
m_ClipBgColor = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
m_ScrollbarBgColor = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
m_RailBgColor = ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f);
m_SliderColor = ColorRGBA(0.8f, 0.8f, 0.8f, 1.0f);
m_SliderColorHover = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
m_SliderColorGrabbed = ColorRGBA(0.9f, 0.9f, 0.9f, 1.0f);
m_Flags = 0;
}
ColorRGBA SliderColor(bool Active, bool Hovered) const
{
if(Active)
return m_SliderColorGrabbed;
else if(Hovered)
return m_SliderColorHover;
return m_SliderColor;
}
};
/*
Usage:
-- Initialization --
static CScrollRegion s_ScrollRegion;
vec2 ScrollOffset(0, 0);
s_ScrollRegion.Begin(&ScrollRegionRect, &ScrollOffset);
Content = ScrollRegionRect;
Content.y += ScrollOffset.y;
-- "Register" your content rects --
CUIRect Rect;
Content.HSplitTop(SomeValue, &Rect, &Content);
s_ScrollRegion.AddRect(Rect);
-- [Optional] Knowing if a rect is clipped --
s_ScrollRegion.IsRectClipped(Rect);
-- [Optional] Scroll to a rect (to the last added rect)--
...
s_ScrollRegion.AddRect(Rect);
s_ScrollRegion.ScrollHere(Option);
-- [Convenience] Add rect and check for visibility at the same time
if(s_ScrollRegion.AddRect(Rect))
// The rect is visible (not clipped)
-- [Convenience] Add rect and scroll to it if it's selected
if(s_ScrollRegion.AddRect(Rect, ScrollToSelection && IsSelected))
// The rect is visible (not clipped)
-- End --
s_ScrollRegion.End();
*/
// Instances of CScrollRegion must be static, as member addresses are used as UI item IDs
class CScrollRegion : private CUIElementBase
{
private:
float m_ScrollY;
float m_ContentH;
float m_RequestScrollY; // [0, ContentHeight]
float m_AnimTime;
float m_AnimInitScrollY;
float m_AnimTargetScrollY;
CUIRect m_ClipRect;
CUIRect m_RailRect;
CUIRect m_LastAddedRect; // saved for ScrollHere()
vec2 m_SliderGrabPos; // where did user grab the slider
vec2 m_ContentScrollOff;
CScrollRegionParams m_Params;
public:
enum EScrollOption
{
SCROLLHERE_KEEP_IN_VIEW = 0,
SCROLLHERE_TOP,
SCROLLHERE_BOTTOM,
};
CScrollRegion();
void Begin(CUIRect *pClipRect, vec2 *pOutOffset, CScrollRegionParams *pParams = nullptr);
void End();
bool AddRect(const CUIRect &Rect, bool ShouldScrollHere = false); // returns true if the added rect is visible (not clipped)
void ScrollHere(EScrollOption Option = SCROLLHERE_KEEP_IN_VIEW);
bool IsRectClipped(const CUIRect &Rect) const;
bool IsScrollbarShown() const;
bool IsAnimating() const;
};
#endif

View file

@ -6392,6 +6392,8 @@ void CEditor::PlaceBorderTiles()
void CEditor::OnUpdate()
{
CUIElementBase::Init(UI()); // update static pointer because game and editor use separate UI
if(!m_EditorWasUsedBefore)
{
m_EditorWasUsedBefore = true;