diff --git a/CMakeLists.txt b/CMakeLists.txt index 95c038a30..40080509e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp index 0a93d6b0d..2a8c307fd 100644 --- a/src/game/client/gameclient.cpp +++ b/src/game/client/gameclient.cpp @@ -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); diff --git a/src/game/client/ui.cpp b/src/game/client/ui.cpp index f16741002..8775ef35d 100644 --- a/src/game/client/ui.cpp +++ b/src/game/client/ui.cpp @@ -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(); @@ -90,6 +97,7 @@ void CUI::Init(IKernel *pKernel) m_pTextRender = pKernel->RequestInterface(); InitInputs(m_pInput->GetEventsRaw(), m_pInput->GetEventCountRaw()); CUIRect::Init(m_pGraphics); + CUIElementBase::Init(this); } void CUI::InitInputs(IInput::CEvent *pInputEventsArray, int *pInputEventCount) diff --git a/src/game/client/ui.h b/src/game/client/ui.h index 2aaae2258..5dbdc6949 100644 --- a/src/game/client/ui.h +++ b/src/game/client/ui.h @@ -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 { }; diff --git a/src/game/client/ui_scrollregion.cpp b/src/game/client/ui_scrollregion.cpp new file mode 100644 index 000000000..91ceb588f --- /dev/null +++ b/src/game/client/ui_scrollregion.cpp @@ -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 +#include + +#include +#include + +#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; +} diff --git a/src/game/client/ui_scrollregion.h b/src/game/client/ui_scrollregion.h new file mode 100644 index 000000000..b63b1f7bd --- /dev/null +++ b/src/game/client/ui_scrollregion.h @@ -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 diff --git a/src/game/editor/editor.cpp b/src/game/editor/editor.cpp index 56f2b3c0a..5096ea261 100644 --- a/src/game/editor/editor.cpp +++ b/src/game/editor/editor.cpp @@ -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;