mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-17 13:53:57 -07:00
Improves performance for Draw calls by 3-5% according to a shady benchmark. Improves memory layout, since the values are not on the heap anymore but in the using class. Reduces complexity of the implementation and increases type safety. Allows specifying default values at setting value construction time, refs D2242. Inspired by Vladislav introducing members that cached GetSetting values in c016a74309/D325, refs #4039, ee38f0db37/D763, refs 4225, a1c4c23ce4/D474, D406, which were formerly proposed to be removed in D2241. Differential Revision: https://code.wildfiregames.com/D2313 Tested on: clang 8.0.1, gcc 9.1.0, Jenkins vs2015 Comments By: Vladislav This was SVN commit r23005.
506 lines
14 KiB
C++
506 lines
14 KiB
C++
/* Copyright (C) 2019 Wildfire Games.
|
|
* This file is part of 0 A.D.
|
|
*
|
|
* 0 A.D. is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* 0 A.D. is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "precompiled.h"
|
|
|
|
#include "CDropDown.h"
|
|
|
|
#include "gui/CGUI.h"
|
|
#include "gui/CGUIColor.h"
|
|
#include "gui/CGUIList.h"
|
|
#include "gui/IGUIScrollBar.h"
|
|
#include "lib/external_libraries/libsdl.h"
|
|
#include "lib/timer.h"
|
|
#include "ps/Profile.h"
|
|
|
|
CDropDown::CDropDown(CGUI& pGUI)
|
|
: CList(pGUI),
|
|
IGUIObject(pGUI),
|
|
m_Open(),
|
|
m_HideScrollBar(),
|
|
m_ElementHighlight(-1),
|
|
m_ButtonWidth(),
|
|
m_DropDownSize(),
|
|
m_DropDownBuffer(),
|
|
m_MinimumVisibleItems(),
|
|
m_SoundClosed(),
|
|
m_SoundEnter(),
|
|
m_SoundLeave(),
|
|
m_SoundOpened(),
|
|
m_SpriteDisabled(),
|
|
m_SpriteList(),
|
|
m_Sprite2(),
|
|
m_Sprite2Over(),
|
|
m_Sprite2Pressed(),
|
|
m_Sprite2Disabled(),
|
|
m_TextColorDisabled(),
|
|
m_TextVAlign()
|
|
{
|
|
RegisterSetting("button_width", m_ButtonWidth);
|
|
RegisterSetting("dropdown_size", m_DropDownSize);
|
|
RegisterSetting("dropdown_buffer", m_DropDownBuffer);
|
|
RegisterSetting("minimum_visible_items", m_MinimumVisibleItems);
|
|
RegisterSetting("sound_closed", m_SoundClosed);
|
|
RegisterSetting("sound_enter", m_SoundEnter);
|
|
RegisterSetting("sound_leave", m_SoundLeave);
|
|
RegisterSetting("sound_opened", m_SoundOpened);
|
|
// Setting "sprite" is registered by CList and used as the background
|
|
RegisterSetting("sprite_disabled", m_SpriteDisabled);
|
|
RegisterSetting("sprite_list", m_SpriteList); // Background of the drop down list
|
|
RegisterSetting("sprite2", m_Sprite2); // Button that sits to the right
|
|
RegisterSetting("sprite2_over", m_Sprite2Over);
|
|
RegisterSetting("sprite2_pressed", m_Sprite2Pressed);
|
|
RegisterSetting("sprite2_disabled", m_Sprite2Disabled);
|
|
RegisterSetting("textcolor_disabled", m_TextColorDisabled);
|
|
RegisterSetting("text_valign", m_TextVAlign);
|
|
// Add these in CList! And implement TODO
|
|
//RegisterSetting("textcolor_over");
|
|
//RegisterSetting("textcolor_pressed");
|
|
|
|
// Scrollbar is forced to be true.
|
|
SetSetting<bool>("scrollbar", true, true);
|
|
}
|
|
|
|
CDropDown::~CDropDown()
|
|
{
|
|
}
|
|
|
|
void CDropDown::SetupText()
|
|
{
|
|
SetupListRect();
|
|
CList::SetupText();
|
|
}
|
|
|
|
void CDropDown::UpdateCachedSize()
|
|
{
|
|
CList::UpdateCachedSize();
|
|
SetupText();
|
|
}
|
|
|
|
void CDropDown::HandleMessage(SGUIMessage& Message)
|
|
{
|
|
// Important
|
|
|
|
switch (Message.type)
|
|
{
|
|
case GUIM_SETTINGS_UPDATED:
|
|
{
|
|
// Update cached list rect
|
|
if (Message.value == "size" ||
|
|
Message.value == "absolute" ||
|
|
Message.value == "dropdown_size" ||
|
|
Message.value == "dropdown_buffer" ||
|
|
Message.value == "minimum_visible_items" ||
|
|
Message.value == "scrollbar_style" ||
|
|
Message.value == "button_width")
|
|
{
|
|
SetupListRect();
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case GUIM_MOUSE_MOTION:
|
|
{
|
|
if (!m_Open)
|
|
break;
|
|
|
|
CPos mouse = m_pGUI.GetMousePos();
|
|
|
|
if (!GetListRect().PointInside(mouse))
|
|
break;
|
|
|
|
const float scroll = m_ScrollBar ? GetScrollBar(0).GetPos() : 0.f;
|
|
|
|
CRect rect = GetListRect();
|
|
mouse.y += scroll;
|
|
int set = -1;
|
|
for (int i = 0; i < static_cast<int>(m_List.m_Items.size()); ++i)
|
|
{
|
|
if (mouse.y >= rect.top + m_ItemsYPositions[i] &&
|
|
mouse.y < rect.top + m_ItemsYPositions[i+1] &&
|
|
// mouse is not over scroll-bar
|
|
(m_HideScrollBar ||
|
|
mouse.x < GetScrollBar(0).GetOuterRect().left ||
|
|
mouse.x > GetScrollBar(0).GetOuterRect().right))
|
|
{
|
|
set = i;
|
|
}
|
|
}
|
|
|
|
if (set != -1)
|
|
{
|
|
m_ElementHighlight = set;
|
|
//UpdateAutoScroll();
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case GUIM_MOUSE_ENTER:
|
|
{
|
|
if (m_Enabled)
|
|
PlaySound(m_SoundEnter);
|
|
break;
|
|
}
|
|
|
|
case GUIM_MOUSE_LEAVE:
|
|
{
|
|
m_ElementHighlight = m_Selected;
|
|
|
|
if (m_Enabled)
|
|
PlaySound(m_SoundLeave);
|
|
break;
|
|
}
|
|
|
|
// We can't inherent this routine from CList, because we need to include
|
|
// a mouse click to open the dropdown, also the coordinates are changed.
|
|
case GUIM_MOUSE_PRESS_LEFT:
|
|
{
|
|
if (!m_Enabled)
|
|
{
|
|
PlaySound(m_SoundDisabled);
|
|
break;
|
|
}
|
|
|
|
if (!m_Open)
|
|
{
|
|
if (m_List.m_Items.empty())
|
|
return;
|
|
|
|
m_Open = true;
|
|
GetScrollBar(0).SetZ(GetBufferedZ());
|
|
m_ElementHighlight = m_Selected;
|
|
|
|
// Start at the position of the selected item, if possible.
|
|
GetScrollBar(0).SetPos(m_ItemsYPositions.empty() ? 0 : m_ItemsYPositions[m_ElementHighlight] - 60);
|
|
|
|
PlaySound(m_SoundOpened);
|
|
return; // overshadow
|
|
}
|
|
else
|
|
{
|
|
const CPos& mouse = m_pGUI.GetMousePos();
|
|
|
|
// If the regular area is pressed, then abort, and close.
|
|
if (m_CachedActualSize.PointInside(mouse))
|
|
{
|
|
m_Open = false;
|
|
GetScrollBar(0).SetZ(GetBufferedZ());
|
|
PlaySound(m_SoundClosed);
|
|
return; // overshadow
|
|
}
|
|
|
|
if (m_HideScrollBar ||
|
|
mouse.x < GetScrollBar(0).GetOuterRect().left ||
|
|
mouse.x > GetScrollBar(0).GetOuterRect().right ||
|
|
mouse.y < GetListRect().top)
|
|
{
|
|
m_Open = false;
|
|
GetScrollBar(0).SetZ(GetBufferedZ());
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case GUIM_MOUSE_WHEEL_DOWN:
|
|
{
|
|
// Don't switch elements by scrolling when open, causes a confusing interaction between this and the scrollbar.
|
|
if (m_Open || !m_Enabled)
|
|
break;
|
|
|
|
m_ElementHighlight = m_Selected;
|
|
|
|
if (m_ElementHighlight + 1 >= (int)m_ItemsYPositions.size() - 1)
|
|
break;
|
|
|
|
++m_ElementHighlight;
|
|
SetSetting<i32>("selected", m_ElementHighlight, true);
|
|
break;
|
|
}
|
|
|
|
case GUIM_MOUSE_WHEEL_UP:
|
|
{
|
|
// Don't switch elements by scrolling when open, causes a confusing interaction between this and the scrollbar.
|
|
if (m_Open || !m_Enabled)
|
|
break;
|
|
|
|
m_ElementHighlight = m_Selected;
|
|
if (m_ElementHighlight - 1 < 0)
|
|
break;
|
|
|
|
--m_ElementHighlight;
|
|
SetSetting<i32>("selected", m_ElementHighlight, true);
|
|
break;
|
|
}
|
|
|
|
case GUIM_LOST_FOCUS:
|
|
{
|
|
if (m_Open)
|
|
PlaySound(m_SoundClosed);
|
|
|
|
m_Open = false;
|
|
break;
|
|
}
|
|
|
|
case GUIM_LOAD:
|
|
SetupListRect();
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Important that this is after, so that overshadowed implementations aren't processed
|
|
CList::HandleMessage(Message);
|
|
|
|
// As HandleMessage functions return void, we need to manually verify
|
|
// whether the child list's items were modified.
|
|
if (CList::GetModified())
|
|
SetupText();
|
|
}
|
|
|
|
InReaction CDropDown::ManuallyHandleEvent(const SDL_Event_* ev)
|
|
{
|
|
InReaction result = IN_PASS;
|
|
bool update_highlight = false;
|
|
|
|
if (ev->ev.type == SDL_KEYDOWN)
|
|
{
|
|
int szChar = ev->ev.key.keysym.sym;
|
|
|
|
switch (szChar)
|
|
{
|
|
case '\r':
|
|
m_Open = false;
|
|
result = IN_HANDLED;
|
|
break;
|
|
|
|
case SDLK_HOME:
|
|
case SDLK_END:
|
|
case SDLK_UP:
|
|
case SDLK_DOWN:
|
|
case SDLK_PAGEUP:
|
|
case SDLK_PAGEDOWN:
|
|
if (!m_Open)
|
|
return IN_PASS;
|
|
// Set current selected item to highlighted, before
|
|
// then really processing these in CList::ManuallyHandleEvent()
|
|
SetSetting<i32>("selected", m_ElementHighlight, true);
|
|
update_highlight = true;
|
|
break;
|
|
|
|
default:
|
|
// If we have inputed a character try to get the closest element to it.
|
|
// TODO: not too nice and doesn't deal with dashes.
|
|
if (m_Open && ((szChar >= SDLK_a && szChar <= SDLK_z) || szChar == SDLK_SPACE
|
|
|| (szChar >= SDLK_0 && szChar <= SDLK_9)
|
|
|| (szChar >= SDLK_KP_0 && szChar <= SDLK_KP_9)))
|
|
{
|
|
// arbitrary 1 second limit to add to string or start fresh.
|
|
// maximal amount of characters is 100, which imo is far more than enough.
|
|
if (timer_Time() - m_TimeOfLastInput > 1.0 || m_InputBuffer.length() >= 100)
|
|
m_InputBuffer = szChar;
|
|
else
|
|
m_InputBuffer += szChar;
|
|
|
|
m_TimeOfLastInput = timer_Time();
|
|
|
|
// let's look for the closest element
|
|
// basically it's alphabetic order and "as many letters as we can get".
|
|
int closest = -1;
|
|
int bestIndex = -1;
|
|
int difference = 1250;
|
|
for (int i = 0; i < static_cast<int>(m_List.m_Items.size()); ++i)
|
|
{
|
|
int indexOfDifference = 0;
|
|
int diff = 0;
|
|
for (size_t j = 0; j < m_InputBuffer.length(); ++j)
|
|
{
|
|
diff = std::abs(static_cast<int>(m_List.m_Items[i].GetRawString().LowerCase()[j]) - static_cast<int>(m_InputBuffer[j]));
|
|
if (diff == 0)
|
|
indexOfDifference = j+1;
|
|
else
|
|
break;
|
|
}
|
|
if (indexOfDifference > bestIndex || (indexOfDifference >= bestIndex && diff < difference))
|
|
{
|
|
bestIndex = indexOfDifference;
|
|
closest = i;
|
|
difference = diff;
|
|
}
|
|
}
|
|
// let's select the closest element. There should basically always be one.
|
|
if (closest != -1)
|
|
{
|
|
SetSetting<i32>("selected", closest, true);
|
|
update_highlight = true;
|
|
GetScrollBar(0).SetPos(m_ItemsYPositions[closest] - 60);
|
|
}
|
|
result = IN_HANDLED;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (CList::ManuallyHandleEvent(ev) == IN_HANDLED)
|
|
result = IN_HANDLED;
|
|
|
|
if (update_highlight)
|
|
m_ElementHighlight = m_Selected;
|
|
|
|
return result;
|
|
}
|
|
|
|
void CDropDown::SetupListRect()
|
|
{
|
|
extern int g_yres;
|
|
extern float g_GuiScale;
|
|
const float yres = g_yres / g_GuiScale;
|
|
|
|
if (m_ItemsYPositions.empty())
|
|
{
|
|
m_CachedListRect = CRect(m_CachedActualSize.left, m_CachedActualSize.bottom + m_DropDownBuffer,
|
|
m_CachedActualSize.right, m_CachedActualSize.bottom + m_DropDownBuffer + m_DropDownSize);
|
|
m_HideScrollBar = false;
|
|
}
|
|
// Too many items so use a scrollbar
|
|
else if (m_ItemsYPositions.back() > m_DropDownSize)
|
|
{
|
|
// Place items below if at least some items can be placed below
|
|
if (m_CachedActualSize.bottom + m_DropDownBuffer + m_DropDownSize <= yres)
|
|
m_CachedListRect = CRect(m_CachedActualSize.left, m_CachedActualSize.bottom + m_DropDownBuffer,
|
|
m_CachedActualSize.right, m_CachedActualSize.bottom + m_DropDownBuffer + m_DropDownSize);
|
|
else if ((m_ItemsYPositions.size() > m_MinimumVisibleItems && yres - m_CachedActualSize.bottom - m_DropDownBuffer >= m_ItemsYPositions[m_MinimumVisibleItems]) ||
|
|
m_CachedActualSize.top < yres - m_CachedActualSize.bottom)
|
|
m_CachedListRect = CRect(m_CachedActualSize.left, m_CachedActualSize.bottom + m_DropDownBuffer,
|
|
m_CachedActualSize.right, yres);
|
|
// Not enough space below, thus place items above
|
|
else
|
|
m_CachedListRect = CRect(m_CachedActualSize.left, std::max(0.f, m_CachedActualSize.top - m_DropDownBuffer - m_DropDownSize),
|
|
m_CachedActualSize.right, m_CachedActualSize.top - m_DropDownBuffer);
|
|
|
|
m_HideScrollBar = false;
|
|
}
|
|
else
|
|
{
|
|
// Enough space below, no scrollbar needed
|
|
if (m_CachedActualSize.bottom + m_DropDownBuffer + m_ItemsYPositions.back() <= yres)
|
|
{
|
|
m_CachedListRect = CRect(m_CachedActualSize.left, m_CachedActualSize.bottom + m_DropDownBuffer,
|
|
m_CachedActualSize.right, m_CachedActualSize.bottom + m_DropDownBuffer + m_ItemsYPositions.back());
|
|
m_HideScrollBar = true;
|
|
}
|
|
// Enough space below for some items, but not all, so place items below and use a scrollbar
|
|
else if ((m_ItemsYPositions.size() > m_MinimumVisibleItems && yres - m_CachedActualSize.bottom - m_DropDownBuffer >= m_ItemsYPositions[m_MinimumVisibleItems]) ||
|
|
m_CachedActualSize.top < yres - m_CachedActualSize.bottom)
|
|
{
|
|
m_CachedListRect = CRect(m_CachedActualSize.left, m_CachedActualSize.bottom + m_DropDownBuffer,
|
|
m_CachedActualSize.right, yres);
|
|
m_HideScrollBar = false;
|
|
}
|
|
// Not enough space below, thus place items above. Hide the scrollbar accordingly
|
|
else
|
|
{
|
|
m_CachedListRect = CRect(m_CachedActualSize.left, std::max(0.f, m_CachedActualSize.top - m_DropDownBuffer - m_ItemsYPositions.back()),
|
|
m_CachedActualSize.right, m_CachedActualSize.top - m_DropDownBuffer);
|
|
m_HideScrollBar = m_CachedActualSize.top > m_ItemsYPositions.back() + m_DropDownBuffer;
|
|
}
|
|
}
|
|
}
|
|
|
|
CRect CDropDown::GetListRect() const
|
|
{
|
|
return m_CachedListRect;
|
|
}
|
|
|
|
bool CDropDown::IsMouseOver() const
|
|
{
|
|
if (m_Open)
|
|
{
|
|
CRect rect(m_CachedActualSize.left, std::min(m_CachedActualSize.top, GetListRect().top),
|
|
m_CachedActualSize.right, std::max(m_CachedActualSize.bottom, GetListRect().bottom));
|
|
return rect.PointInside(m_pGUI.GetMousePos());
|
|
}
|
|
else
|
|
return m_CachedActualSize.PointInside(m_pGUI.GetMousePos());
|
|
}
|
|
|
|
void CDropDown::Draw()
|
|
{
|
|
const float bz = GetBufferedZ();
|
|
const CGUISpriteInstance& sprite = m_Enabled ? m_Sprite : m_SpriteDisabled;
|
|
|
|
m_pGUI.DrawSprite(sprite, m_CellID, bz, m_CachedActualSize);
|
|
|
|
if (m_ButtonWidth > 0.f)
|
|
{
|
|
CRect rect(m_CachedActualSize.right - m_ButtonWidth, m_CachedActualSize.top,
|
|
m_CachedActualSize.right, m_CachedActualSize.bottom);
|
|
|
|
if (!m_Enabled)
|
|
{
|
|
m_pGUI.DrawSprite(m_Sprite2Disabled || m_Sprite2, m_CellID, bz + 0.05f, rect);
|
|
}
|
|
else if (m_Open)
|
|
{
|
|
m_pGUI.DrawSprite(m_Sprite2Pressed || m_Sprite2, m_CellID, bz + 0.05f, rect);
|
|
}
|
|
else if (m_MouseHovering)
|
|
{
|
|
m_pGUI.DrawSprite(m_Sprite2Over || m_Sprite2, m_CellID, bz + 0.05f, rect);
|
|
}
|
|
else
|
|
m_pGUI.DrawSprite(m_Sprite2, m_CellID, bz + 0.05f, rect);
|
|
}
|
|
|
|
if (m_Selected != -1) // TODO: Maybe check validity completely?
|
|
{
|
|
CRect cliparea(m_CachedActualSize.left, m_CachedActualSize.top,
|
|
m_CachedActualSize.right - m_ButtonWidth, m_CachedActualSize.bottom);
|
|
|
|
CPos pos(m_CachedActualSize.left, m_CachedActualSize.top);
|
|
DrawText(m_Selected, m_Enabled ? m_TextColorSelected : m_TextColorDisabled, pos, bz + 0.1f, cliparea);
|
|
}
|
|
|
|
// Disable scrollbar during drawing without sending a setting-changed message
|
|
bool old = m_ScrollBar;
|
|
|
|
if (m_Open)
|
|
{
|
|
// TODO: drawScrollbar as an argument of DrawList?
|
|
if (m_HideScrollBar)
|
|
m_ScrollBar = false;
|
|
|
|
DrawList(m_ElementHighlight, m_SpriteList, m_SpriteSelectArea, m_TextColor);
|
|
|
|
if (m_HideScrollBar)
|
|
m_ScrollBar = old;
|
|
}
|
|
}
|
|
|
|
// When a dropdown list is opened, it needs to be visible above all the other
|
|
// controls on the page. The only way I can think of to do this is to increase
|
|
// its z value when opened, so that it's probably on top.
|
|
float CDropDown::GetBufferedZ() const
|
|
{
|
|
float bz = CList::GetBufferedZ();
|
|
if (m_Open)
|
|
return std::min(bz + 500.f, 1000.f); // TODO - don't use magic number for max z value
|
|
else
|
|
return bz;
|
|
}
|