ddnet/src/game/client/ui_scrollregion.cpp
Robert Müller 55726e8da3 Simplify list box activation handling
Store the list box active state inside `CListBox` instead of tracking it separately with a pointer.

Allow activating list boxes by clicking the scrollbar. Previously it was only possible to activate list boxes by selecting an item.

Fix country selection list box in players settings not being deactivated properly when name/clan edit boxes are active, because wrong UI element IDs were being used.
2023-06-07 20:46:10 +02:00

256 lines
7.6 KiB
C++

/* (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 <engine/shared/config.h>
#include "ui_scrollregion.h"
CScrollRegion::CScrollRegion()
{
m_ScrollY = 0.0f;
m_ContentH = 0.0f;
m_RequestScrollY = -1.0f;
m_ScrollDirection = SCROLLRELATIVE_NONE;
m_ScrollSpeedMultiplier = 1.0f;
m_AnimTimeMax = 0.0f;
m_AnimTime = 0.0f;
m_AnimInitScrollY = 0.0f;
m_AnimTargetScrollY = 0.0f;
m_ContentScrollOff = vec2(0.0f, 0.0f);
m_Params = CScrollRegionParams();
}
void CScrollRegion::Begin(CUIRect *pClipRect, vec2 *pOutOffset, const 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;
if(m_ScrollDirection != SCROLLRELATIVE_NONE || (UI()->Enabled() && m_Params.m_Active && UI()->MouseHovered(&RegionRect)))
{
bool ProgrammaticScroll = false;
if(UI()->ConsumeHotkey(CUI::HOTKEY_SCROLL_UP))
m_ScrollDirection = SCROLLRELATIVE_UP;
else if(UI()->ConsumeHotkey(CUI::HOTKEY_SCROLL_DOWN))
m_ScrollDirection = SCROLLRELATIVE_DOWN;
else
ProgrammaticScroll = true;
if(!ProgrammaticScroll)
m_ScrollSpeedMultiplier = 1.0f;
if(m_ScrollDirection != SCROLLRELATIVE_NONE)
{
const bool IsPageScroll = Input()->AltIsPressed();
const float ScrollUnit = IsPageScroll && !ProgrammaticScroll ? m_ClipRect.h : m_Params.m_ScrollUnit;
m_AnimTimeMax = g_Config.m_UiSmoothScrollTime / 1000.0f;
m_AnimTime = m_AnimTimeMax;
m_AnimInitScrollY = m_ScrollY;
m_AnimTargetScrollY = (ProgrammaticScroll ? m_ScrollY : m_AnimTargetScrollY) + (int)m_ScrollDirection * ScrollUnit * m_ScrollSpeedMultiplier;
m_ScrollDirection = SCROLLRELATIVE_NONE;
m_ScrollSpeedMultiplier = 1.0f;
}
}
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 - std::pow(m_AnimTime / m_AnimTimeMax, 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 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;
m_Params.m_Active = true;
}
}
else if(InsideRail && UI()->MouseButtonClicked(0))
{
m_ScrollY += (UI()->MouseY() - (Slider.y + Slider.h / 2.0f)) / MaxSlider * MaxScroll;
UI()->SetHotItem(pID);
UI()->SetActiveItem(pID);
m_SliderGrabPos.y = Slider.h / 2.0f;
m_AnimTargetScrollY = m_ScrollY;
m_AnimTime = 0.0f;
m_Params.m_Active = 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, UI()->HotItem() == pID), 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(std::ceil(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;
}
}
void CScrollRegion::ScrollRelative(EScrollRelative Direction, float SpeedMultiplier)
{
m_ScrollDirection = Direction;
m_ScrollSpeedMultiplier = SpeedMultiplier;
}
void CScrollRegion::DoEdgeScrolling()
{
if(!IsScrollbarShown())
return;
const float ScrollBorderSize = 20.0f;
const float MaxScrollMultiplier = 2.0f;
const float ScrollSpeedFactor = MaxScrollMultiplier / ScrollBorderSize;
const float TopScrollPosition = m_ClipRect.y + ScrollBorderSize;
const float BottomScrollPosition = m_ClipRect.y + m_ClipRect.h - ScrollBorderSize;
if(UI()->MouseY() < TopScrollPosition)
ScrollRelative(SCROLLRELATIVE_UP, minimum(MaxScrollMultiplier, (TopScrollPosition - UI()->MouseY()) * ScrollSpeedFactor));
else if(UI()->MouseY() > BottomScrollPosition)
ScrollRelative(SCROLLRELATIVE_DOWN, minimum(MaxScrollMultiplier, (UI()->MouseY() - BottomScrollPosition) * ScrollSpeedFactor));
}
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;
}