Index: ps/trunk/source/gui/CDropDown.cpp =================================================================== --- ps/trunk/source/gui/CDropDown.cpp (revision 22756) +++ ps/trunk/source/gui/CDropDown.cpp (revision 22757) @@ -1,531 +1,531 @@ /* 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 . */ #include "precompiled.h" #include "CDropDown.h" #include "gui/CGUIColor.h" #include "lib/external_libraries/libsdl.h" #include "lib/ogl.h" #include "lib/timer.h" #include "ps/CLogger.h" CDropDown::CDropDown(CGUI& pGUI) : CList(pGUI), IGUIObject(pGUI), m_Open(false), m_HideScrollBar(false), m_ElementHighlight(-1) { AddSetting("button_width"); AddSetting("dropdown_size"); AddSetting("dropdown_buffer"); AddSetting("minimum_visible_items"); // AddSetting("sound_closed"); AddSetting("sound_disabled"); AddSetting("sound_enter"); AddSetting("sound_leave"); AddSetting("sound_opened"); AddSetting("sprite"); // Background that sits around the size AddSetting("sprite_disabled"); AddSetting("sprite_list"); // Background of the drop down list AddSetting("sprite2"); // Button that sits to the right AddSetting("sprite2_over"); AddSetting("sprite2_pressed"); AddSetting("sprite2_disabled"); AddSetting("text_valign"); // Add these in CList! And implement TODO //AddSetting("textcolor_over"); //AddSetting("textcolor_pressed"); AddSetting("textcolor_selected"); AddSetting("textcolor_disabled"); // Scrollbar is forced to be true. GUI::SetSetting(this, "scrollbar", 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; bool scrollbar; const CGUIList& pList = GUI::GetSetting(this, "list"); GUI::GetSetting(this, "scrollbar", scrollbar); float scroll = 0.f; if (scrollbar) scroll = GetScrollBar(0).GetPos(); CRect rect = GetListRect(); mouse.y += scroll; int set = -1; for (int i = 0; i < static_cast(pList.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: { bool enabled; GUI::GetSetting(this, "enabled", enabled); if (enabled) PlaySound("sound_enter"); break; } case GUIM_MOUSE_LEAVE: { GUI::GetSetting(this, "selected", m_ElementHighlight); bool enabled; GUI::GetSetting(this, "enabled", enabled); if (enabled) PlaySound("sound_leave"); 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: { bool enabled; GUI::GetSetting(this, "enabled", enabled); if (!enabled) { PlaySound("sound_disabled"); break; } if (!m_Open) { const CGUIList& pList = GUI::GetSetting(this, "list"); if (pList.m_Items.empty()) return; m_Open = true; GetScrollBar(0).SetZ(GetBufferedZ()); GUI::GetSetting(this, "selected", m_ElementHighlight); // Start at the position of the selected item, if possible. GetScrollBar(0).SetPos(m_ItemsYPositions.empty() ? 0 : m_ItemsYPositions[m_ElementHighlight] - 60); PlaySound("sound_opened"); 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("sound_closed"); 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: { bool enabled; GUI::GetSetting(this, "enabled", enabled); // Don't switch elements by scrolling when open, causes a confusing interaction between this and the scrollbar. if (m_Open || !enabled) break; GUI::GetSetting(this, "selected", m_ElementHighlight); if (m_ElementHighlight + 1 >= (int)m_ItemsYPositions.size() - 1) break; ++m_ElementHighlight; GUI::SetSetting(this, "selected", m_ElementHighlight); break; } case GUIM_MOUSE_WHEEL_UP: { bool enabled; GUI::GetSetting(this, "enabled", enabled); // Don't switch elements by scrolling when open, causes a confusing interaction between this and the scrollbar. if (m_Open || !enabled) break; GUI::GetSetting(this, "selected", m_ElementHighlight); if (m_ElementHighlight - 1 < 0) break; m_ElementHighlight--; GUI::SetSetting(this, "selected", m_ElementHighlight); break; } case GUIM_LOST_FOCUS: { if (m_Open) PlaySound("sound_closed"); 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() GUI::SetSetting(this, "selected", m_ElementHighlight); 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(); const CGUIList& pList = GUI::GetSetting(this, "list"); // 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(pList.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(pList.m_Items[i].GetRawString().LowerCase()[j]) - static_cast(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) { GUI::SetSetting(this, "selected", closest); 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) GUI::GetSetting(this, "selected", m_ElementHighlight); return result; } void CDropDown::SetupListRect() { extern int g_yres; extern float g_GuiScale; float size, buffer, yres; yres = g_yres / g_GuiScale; u32 minimumVisibleItems; GUI::GetSetting(this, "dropdown_size", size); GUI::GetSetting(this, "dropdown_buffer", buffer); GUI::GetSetting(this, "minimum_visible_items", minimumVisibleItems); if (m_ItemsYPositions.empty()) { m_CachedListRect = CRect(m_CachedActualSize.left, m_CachedActualSize.bottom + buffer, m_CachedActualSize.right, m_CachedActualSize.bottom + buffer + size); m_HideScrollBar = false; } // Too many items so use a scrollbar else if (m_ItemsYPositions.back() > size) { // Place items below if at least some items can be placed below if (m_CachedActualSize.bottom + buffer + size <= yres) m_CachedListRect = CRect(m_CachedActualSize.left, m_CachedActualSize.bottom + buffer, m_CachedActualSize.right, m_CachedActualSize.bottom + buffer + size); else if ((m_ItemsYPositions.size() > minimumVisibleItems && yres - m_CachedActualSize.bottom - buffer >= m_ItemsYPositions[minimumVisibleItems]) || m_CachedActualSize.top < yres - m_CachedActualSize.bottom) m_CachedListRect = CRect(m_CachedActualSize.left, m_CachedActualSize.bottom + buffer, 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 - buffer - size), m_CachedActualSize.right, m_CachedActualSize.top - buffer); m_HideScrollBar = false; } else { // Enough space below, no scrollbar needed if (m_CachedActualSize.bottom + buffer + m_ItemsYPositions.back() <= yres) { m_CachedListRect = CRect(m_CachedActualSize.left, m_CachedActualSize.bottom + buffer, m_CachedActualSize.right, m_CachedActualSize.bottom + buffer + 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() > minimumVisibleItems && yres - m_CachedActualSize.bottom - buffer >= m_ItemsYPositions[minimumVisibleItems]) || m_CachedActualSize.top < yres - m_CachedActualSize.bottom) { m_CachedListRect = CRect(m_CachedActualSize.left, m_CachedActualSize.bottom + buffer, 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 - buffer - m_ItemsYPositions.back()), m_CachedActualSize.right, m_CachedActualSize.top - buffer); m_HideScrollBar = m_CachedActualSize.top > m_ItemsYPositions.back() + buffer; } } } CRect CDropDown::GetListRect() const { return m_CachedListRect; } -bool CDropDown::MouseOver() +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() { float bz = GetBufferedZ(); float dropdown_size, button_width; GUI::GetSetting(this, "dropdown_size", dropdown_size); GUI::GetSetting(this, "button_width", button_width); int cell_id, selected = 0; bool enabled; GUI::GetSetting(this, "enabled", enabled); CGUISpriteInstance& sprite = GUI::GetSetting(this, enabled ? "sprite" : "sprite_disabled"); CGUISpriteInstance& sprite2 = GUI::GetSetting(this, "sprite2"); GUI::GetSetting(this, "cell_id", cell_id); GUI::GetSetting(this, "selected", selected); m_pGUI.DrawSprite(sprite, cell_id, bz, m_CachedActualSize); if (button_width > 0.f) { CRect rect(m_CachedActualSize.right-button_width, m_CachedActualSize.top, m_CachedActualSize.right, m_CachedActualSize.bottom); if (!enabled) { CGUISpriteInstance& sprite2_second = GUI::GetSetting(this, "sprite2_disabled"); m_pGUI.DrawSprite(sprite2_second || sprite2, cell_id, bz + 0.05f, rect); } else if (m_Open) { CGUISpriteInstance& sprite2_second = GUI::GetSetting(this, "sprite2_pressed"); m_pGUI.DrawSprite(sprite2_second || sprite2, cell_id, bz + 0.05f, rect); } else if (m_MouseHovering) { CGUISpriteInstance& sprite2_second = GUI::GetSetting(this, "sprite2_over"); m_pGUI.DrawSprite(sprite2_second || sprite2, cell_id, bz + 0.05f, rect); } else m_pGUI.DrawSprite(sprite2, cell_id, bz + 0.05f, rect); } if (selected != -1) // TODO: Maybe check validity completely? { CRect cliparea(m_CachedActualSize.left, m_CachedActualSize.top, m_CachedActualSize.right-button_width, m_CachedActualSize.bottom); const CGUIColor& color = GUI::GetSetting(this, enabled ? "textcolor_selected" : "textcolor_disabled"); CPos pos(m_CachedActualSize.left, m_CachedActualSize.top); DrawText(selected, color, pos, bz+0.1f, cliparea); } // Disable scrollbar during drawing without sending a setting-changed message bool& scrollbar = GUI::GetSetting(this, "scrollbar"); bool old = scrollbar; if (m_Open) { if (m_HideScrollBar) scrollbar = false; DrawList(m_ElementHighlight, "sprite_list", "sprite_selectarea", "textcolor"); if (m_HideScrollBar) 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; } Index: ps/trunk/source/gui/CDropDown.h =================================================================== --- ps/trunk/source/gui/CDropDown.h (revision 22756) +++ ps/trunk/source/gui/CDropDown.h (revision 22757) @@ -1,130 +1,130 @@ /* 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 . */ /* GUI Object - Drop Down (list) --Overview-- Works just like a list-box, but it hides all the elements that aren't selected. They can be brought up by pressing the control. --More info-- Check GUI.h */ #ifndef INCLUDED_CDROPDOWN #define INCLUDED_CDROPDOWN #include "GUI.h" #include "CList.h" /** * Drop Down * * The control can be pressed, but we will not inherent * this behavior from IGUIButtonBehavior, because when * you press this control, the list with elements will * immediately appear, and not first after release * (which is the whole gist of the IGUIButtonBehavior). */ class CDropDown : public CList { GUI_OBJECT(CDropDown) public: CDropDown(CGUI& pGUI); virtual ~CDropDown(); // virtual void ResetStates() { IGUIButtonBehavior::ResetStates(); } /** * @see IGUIObject#HandleMessage() */ virtual void HandleMessage(SGUIMessage& Message); /** * Handle events manually to catch keyboard inputting. */ virtual InReaction ManuallyHandleEvent(const SDL_Event_* ev); /** * Draws the Button */ virtual void Draw(); // This is one of the few classes we actually need to redefine this function // this is because the size of the control changes whether it is open // or closed. - virtual bool MouseOver(); + virtual bool IsMouseOver() const; virtual float GetBufferedZ() const; protected: /** * If the size changed, the texts have to be updated as * the word wrapping depends on the size. */ virtual void UpdateCachedSize(); /** * Sets up text, should be called every time changes has been * made that can change the visual. */ void SetupText(); // Sets up the cached GetListRect. Decided whether it should // have a scrollbar, and so on. virtual void SetupListRect(); // Specify a new List rectangle. virtual CRect GetListRect() const; /** * Placement of text. */ CPos m_TextPos; // Is the dropdown opened? bool m_Open; // I didn't cache this at first, but it's just as easy as caching // m_CachedActualSize, so I thought, what the heck it's used a lot. CRect m_CachedListRect; // Hide scrollbar when it's not needed bool m_HideScrollBar; // Not necessarily the element that is selected, this is just // which element should be highlighted. When opening the dropdown // it is set to "selected", but then when moving the mouse it will // change. int m_ElementHighlight; // Stores any text entered by the user for quick access to an element // (ie if you type "acro" it will take you to acropolis). std::string m_InputBuffer; // used to know if we want to restart anew or add to m_inputbuffer. double m_TimeOfLastInput; }; #endif // INCLUDED_CDROPDOWN Index: ps/trunk/source/gui/CSlider.cpp =================================================================== --- ps/trunk/source/gui/CSlider.cpp (revision 22756) +++ ps/trunk/source/gui/CSlider.cpp (revision 22757) @@ -1,142 +1,142 @@ /* 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 . */ #include "precompiled.h" #include "CSlider.h" #include "GUI.h" #include "lib/ogl.h" #include "ps/CLogger.h" CSlider::CSlider(CGUI& pGUI) : IGUIObject(pGUI), m_IsPressed(false), m_ButtonSide(0) { AddSetting("value"); AddSetting("min_value"); AddSetting("max_value"); AddSetting("cell_id"); AddSetting("sprite"); AddSetting("sprite_bar"); AddSetting("button_width"); GUI::GetSetting(this, "value", m_Value); GUI::GetSetting(this, "min_value", m_MinValue); GUI::GetSetting(this, "max_value", m_MaxValue); GUI::GetSetting(this, "button_width", m_ButtonSide); m_Value = Clamp(m_Value, m_MinValue, m_MaxValue); } CSlider::~CSlider() { } float CSlider::GetSliderRatio() const { return (m_MaxValue - m_MinValue) / (m_CachedActualSize.GetWidth() - m_ButtonSide); } void CSlider::IncrementallyChangeValue(const float difference) { m_Value = Clamp(m_Value + difference, m_MinValue, m_MaxValue); UpdateValue(); } void CSlider::HandleMessage(SGUIMessage& Message) { switch (Message.type) { case GUIM_SETTINGS_UPDATED: { GUI::GetSetting(this, "value", m_Value); GUI::GetSetting(this, "min_value", m_MinValue); GUI::GetSetting(this, "max_value", m_MaxValue); GUI::GetSetting(this, "button_width", m_ButtonSide); m_Value = Clamp(m_Value, m_MinValue, m_MaxValue); break; } case GUIM_MOUSE_WHEEL_DOWN: { if (m_IsPressed) break; IncrementallyChangeValue(-0.01f); break; } case GUIM_MOUSE_WHEEL_UP: { if (m_IsPressed) break; IncrementallyChangeValue(0.01f); break; } case GUIM_MOUSE_PRESS_LEFT: { m_Mouse = m_pGUI.GetMousePos(); m_IsPressed = true; IncrementallyChangeValue((m_Mouse.x - GetButtonRect().CenterPoint().x) * GetSliderRatio()); break; } case GUIM_MOUSE_RELEASE_LEFT: { m_IsPressed = false; break; } case GUIM_MOUSE_MOTION: { - if (!MouseOver()) + if (!IsMouseOver()) m_IsPressed = false; if (m_IsPressed) { float difference = float(m_pGUI.GetMousePos().x - m_Mouse.x) * GetSliderRatio(); m_Mouse = m_pGUI.GetMousePos(); IncrementallyChangeValue(difference); } break; } default: break; } } void CSlider::Draw() { CGUISpriteInstance& sprite = GUI::GetSetting(this, "sprite_bar"); CGUISpriteInstance& sprite_button = GUI::GetSetting(this, "sprite"); int cell_id; GUI::GetSetting(this, "cell_id", cell_id); CRect slider_line(m_CachedActualSize); slider_line.left += m_ButtonSide / 2.0f; slider_line.right -= m_ButtonSide / 2.0f; float bz = GetBufferedZ(); m_pGUI.DrawSprite(sprite, cell_id, bz, slider_line); m_pGUI.DrawSprite(sprite_button, cell_id, bz, GetButtonRect()); } void CSlider::UpdateValue() { GUI::SetSetting(this, "value", m_Value); ScriptEvent("valuechange"); } -CRect CSlider::GetButtonRect() +CRect CSlider::GetButtonRect() const { float ratio = m_MaxValue > m_MinValue ? (m_Value - m_MinValue) / (m_MaxValue - m_MinValue) : 0.0f; float x = m_CachedActualSize.left + ratio * (m_CachedActualSize.GetWidth() - m_ButtonSide); float y = m_CachedActualSize.top + (m_CachedActualSize.GetHeight() - m_ButtonSide) / 2.0; return CRect(x, y, x + m_ButtonSide, y + m_ButtonSide); } Index: ps/trunk/source/gui/CSlider.h =================================================================== --- ps/trunk/source/gui/CSlider.h (revision 22756) +++ ps/trunk/source/gui/CSlider.h (revision 22757) @@ -1,65 +1,65 @@ /* 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 . */ #ifndef INCLUDED_CSLIDER #define INCLUDED_CSLIDER #include "GUI.h" class CSlider : public IGUIObject { GUI_OBJECT(CSlider) public: CSlider(CGUI& pGUI); virtual ~CSlider(); protected: /** * @see IGUIObject#HandleMessage() */ virtual void HandleMessage(SGUIMessage& Message); virtual void Draw(); /** * Change settings and send the script event */ void UpdateValue(); - CRect GetButtonRect(); + CRect GetButtonRect() const; /** * @return ratio between the value of the slider and its actual size in the GUI */ float GetSliderRatio() const; void IncrementallyChangeValue(const float value); float m_MinValue, m_MaxValue, m_Value; private: bool m_IsPressed; CPos m_Mouse; float m_ButtonSide; }; #endif // INCLUDED_CSLIDER Index: ps/trunk/source/gui/GUIManager.cpp =================================================================== --- ps/trunk/source/gui/GUIManager.cpp (revision 22756) +++ ps/trunk/source/gui/GUIManager.cpp (revision 22757) @@ -1,423 +1,423 @@ /* 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 . */ #include "precompiled.h" #include "GUIManager.h" #include "gui/CGUI.h" #include "lib/timer.h" #include "ps/Filesystem.h" #include "ps/CLogger.h" #include "ps/Profile.h" #include "ps/XML/Xeromyces.h" #include "ps/GameSetup/Config.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptRuntime.h" CGUIManager* g_GUI = NULL; // General TODOs: // // A lot of the CGUI data could (and should) be shared between // multiple pages, instead of treating them as completely independent, to save // memory and loading time. // called from main loop when (input) events are received. // event is passed to other handlers if false is returned. // trampoline: we don't want to make the HandleEvent implementation static InReaction gui_handler(const SDL_Event_* ev) { PROFILE("GUI event handler"); return g_GUI->HandleEvent(ev); } static Status ReloadChangedFileCB(void* param, const VfsPath& path) { return static_cast(param)->ReloadChangedFile(path); } CGUIManager::CGUIManager() { m_ScriptRuntime = g_ScriptRuntime; m_ScriptInterface.reset(new ScriptInterface("Engine", "GUIManager", m_ScriptRuntime)); m_ScriptInterface->SetCallbackData(this); m_ScriptInterface->LoadGlobalScripts(); if (!CXeromyces::AddValidator(g_VFS, "gui_page", "gui/gui_page.rng")) LOGERROR("CGUIManager: failed to load GUI page grammar file 'gui/gui_page.rng'"); if (!CXeromyces::AddValidator(g_VFS, "gui", "gui/gui.rng")) LOGERROR("CGUIManager: failed to load GUI XML grammar file 'gui/gui.rng'"); RegisterFileReloadFunc(ReloadChangedFileCB, this); } CGUIManager::~CGUIManager() { UnregisterFileReloadFunc(ReloadChangedFileCB, this); } bool CGUIManager::HasPages() { return !m_PageStack.empty(); } void CGUIManager::SwitchPage(const CStrW& pageName, ScriptInterface* srcScriptInterface, JS::HandleValue initData) { // The page stack is cleared (including the script context where initData came from), // therefore we have to clone initData. shared_ptr initDataClone; if (!initData.isUndefined()) initDataClone = srcScriptInterface->WriteStructuredClone(initData); m_PageStack.clear(); PushPage(pageName, initDataClone, JS::UndefinedHandleValue); } void CGUIManager::PushPage(const CStrW& pageName, shared_ptr initData, JS::HandleValue callbackFunction) { // Store the callback handler in the current GUI page before opening the new one if (!m_PageStack.empty() && !callbackFunction.isUndefined()) m_PageStack.back().SetCallbackFunction(*m_ScriptInterface, callbackFunction); // Push the page prior to loading its contents, because that may push // another GUI page on init which should be pushed on top of this new page. m_PageStack.emplace_back(pageName, initData); m_PageStack.back().LoadPage(m_ScriptRuntime); ResetCursor(); } void CGUIManager::PopPage(shared_ptr args) { if (m_PageStack.size() < 2) { debug_warn(L"Tried to pop GUI page when there's < 2 in the stack"); return; } m_PageStack.pop_back(); m_PageStack.back().PerformCallbackFunction(args); } CGUIManager::SGUIPage::SGUIPage(const CStrW& pageName, const shared_ptr initData) : name(pageName), initData(initData), inputs(), gui(), callbackFunction() { } void CGUIManager::SGUIPage::LoadPage(shared_ptr scriptRuntime) { // If we're hotloading then try to grab some data from the previous page shared_ptr hotloadData; if (gui) { shared_ptr scriptInterface = gui->GetScriptInterface(); JSContext* cx = scriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue global(cx, scriptInterface->GetGlobalObject()); JS::RootedValue hotloadDataVal(cx); scriptInterface->CallFunction(global, "getHotloadData", &hotloadDataVal); hotloadData = scriptInterface->WriteStructuredClone(hotloadDataVal); } inputs.clear(); gui.reset(new CGUI(scriptRuntime)); gui->Initialize(); VfsPath path = VfsPath("gui") / name; inputs.insert(path); CXeromyces xero; if (xero.Load(g_VFS, path, "gui_page") != PSRETURN_OK) // Fail silently (Xeromyces reported the error) return; int elmt_page = xero.GetElementID("page"); int elmt_include = xero.GetElementID("include"); XMBElement root = xero.GetRoot(); if (root.GetNodeName() != elmt_page) { LOGERROR("GUI page '%s' must have root element ", utf8_from_wstring(name)); return; } XERO_ITER_EL(root, node) { if (node.GetNodeName() != elmt_include) { LOGERROR("GUI page '%s' must only have elements inside ", utf8_from_wstring(name)); continue; } std::string name = node.GetText(); CStrW nameW (node.GetText().FromUTF8()); PROFILE2("load gui xml"); PROFILE2_ATTR("name: %s", name.c_str()); TIMER(nameW.c_str()); if (name.back() == '/') { VfsPath directory = VfsPath("gui") / nameW; VfsPaths pathnames; vfs::GetPathnames(g_VFS, directory, L"*.xml", pathnames); for (const VfsPath& path : pathnames) gui->LoadXmlFile(path, inputs); } else { VfsPath path = VfsPath("gui") / nameW; gui->LoadXmlFile(path, inputs); } } gui->SendEventToAll("load"); shared_ptr scriptInterface = gui->GetScriptInterface(); JSContext* cx = scriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue initDataVal(cx); JS::RootedValue hotloadDataVal(cx); JS::RootedValue global(cx, scriptInterface->GetGlobalObject()); if (initData) scriptInterface->ReadStructuredClone(initData, &initDataVal); if (hotloadData) scriptInterface->ReadStructuredClone(hotloadData, &hotloadDataVal); if (scriptInterface->HasProperty(global, "init") && !scriptInterface->CallFunctionVoid(global, "init", initDataVal, hotloadDataVal)) LOGERROR("GUI page '%s': Failed to call init() function", utf8_from_wstring(name)); } void CGUIManager::SGUIPage::SetCallbackFunction(ScriptInterface& scriptInterface, JS::HandleValue callbackFunc) { if (!callbackFunc.isObject()) { LOGERROR("Given callback handler is not an object!"); return; } // Does not require JSAutoRequest if (!JS_ObjectIsFunction(scriptInterface.GetContext(), &callbackFunc.toObject())) { LOGERROR("Given callback handler is not a function!"); return; } callbackFunction = std::make_shared(scriptInterface.GetJSRuntime(), callbackFunc); } void CGUIManager::SGUIPage::PerformCallbackFunction(shared_ptr args) { if (!callbackFunction) return; shared_ptr scriptInterface = gui->GetScriptInterface(); JSContext* cx = scriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedObject globalObj(cx, &scriptInterface->GetGlobalObject().toObject()); JS::RootedValue funcVal(cx, *callbackFunction); // Delete the callback function, so that it is not called again callbackFunction.reset(); JS::RootedValue argVal(cx); if (args) scriptInterface->ReadStructuredClone(args, &argVal); JS::AutoValueVector paramData(cx); paramData.append(argVal); JS::RootedValue result(cx); JS_CallFunctionValue(cx, globalObj, funcVal, paramData, &result); } Status CGUIManager::ReloadChangedFile(const VfsPath& path) { for (SGUIPage& p : m_PageStack) if (p.inputs.count(path)) { LOGMESSAGE("GUI file '%s' changed - reloading page '%s'", path.string8(), utf8_from_wstring(p.name)); p.LoadPage(m_ScriptRuntime); // TODO: this can crash if LoadPage runs an init script which modifies the page stack and breaks our iterators } return INFO::OK; } Status CGUIManager::ReloadAllPages() { // TODO: this can crash if LoadPage runs an init script which modifies the page stack and breaks our iterators for (SGUIPage& p : m_PageStack) p.LoadPage(m_ScriptRuntime); return INFO::OK; } void CGUIManager::ResetCursor() { g_CursorName = g_DefaultCursor; } std::string CGUIManager::GetSavedGameData() { shared_ptr scriptInterface = top()->GetScriptInterface(); JSContext* cx = scriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue data(cx); JS::RootedValue global(cx, top()->GetGlobalObject()); scriptInterface->CallFunction(global, "getSavedGameData", &data); return scriptInterface->StringifyJSON(&data, false); } void CGUIManager::RestoreSavedGameData(const std::string& jsonData) { shared_ptr scriptInterface = top()->GetScriptInterface(); JSContext* cx = scriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue global(cx, top()->GetGlobalObject()); JS::RootedValue dataVal(cx); scriptInterface->ParseJSON(jsonData, &dataVal); scriptInterface->CallFunctionVoid(global, "restoreSavedGameData", dataVal); } InReaction CGUIManager::HandleEvent(const SDL_Event_* ev) { // We want scripts to have access to the raw input events, so they can do complex // processing when necessary (e.g. for unit selection and camera movement). // Sometimes they'll want to be layered behind the GUI widgets (e.g. to detect mousedowns on the // visible game area), sometimes they'll want to intercepts events before the GUI (e.g. // to capture all mouse events until a mouseup after dragging). // So we call two separate handler functions: bool handled; { PROFILE("handleInputBeforeGui"); JSContext* cx = top()->GetScriptInterface()->GetContext(); JSAutoRequest rq(cx); JS::RootedValue global(cx, top()->GetGlobalObject()); if (top()->GetScriptInterface()->CallFunction(global, "handleInputBeforeGui", handled, *ev, top()->FindObjectUnderMouse())) if (handled) return IN_HANDLED; } { PROFILE("handle event in native GUI"); InReaction r = top()->HandleEvent(ev); if (r != IN_PASS) return r; } { // We can't take the following lines out of this scope because top() may be another gui page than it was when calling handleInputBeforeGui! JSContext* cx = top()->GetScriptInterface()->GetContext(); JSAutoRequest rq(cx); JS::RootedValue global(cx, top()->GetGlobalObject()); PROFILE("handleInputAfterGui"); if (top()->GetScriptInterface()->CallFunction(global, "handleInputAfterGui", handled, *ev)) if (handled) return IN_HANDLED; } return IN_PASS; } void CGUIManager::SendEventToAll(const CStr& eventName) const { top()->SendEventToAll(eventName); } void CGUIManager::SendEventToAll(const CStr& eventName, JS::HandleValueArray paramData) const { top()->SendEventToAll(eventName, paramData); } void CGUIManager::TickObjects() { PROFILE3("gui tick"); // We share the script runtime with everything else that runs in the same thread. // This call makes sure we trigger GC regularly even if the simulation is not running. m_ScriptInterface->GetRuntime()->MaybeIncrementalGC(1.0f); // Save an immutable copy so iterators aren't invalidated by tick handlers PageStackType pageStack = m_PageStack; for (const SGUIPage& p : pageStack) p.gui->TickObjects(); } -void CGUIManager::Draw() +void CGUIManager::Draw() const { PROFILE3_GPU("gui"); for (const SGUIPage& p : m_PageStack) p.gui->Draw(); } void CGUIManager::UpdateResolution() { // Save an immutable copy so iterators aren't invalidated by event handlers PageStackType pageStack = m_PageStack; for (const SGUIPage& p : pageStack) { p.gui->UpdateResolution(); p.gui->SendEventToAll("WindowResized"); } } bool CGUIManager::TemplateExists(const std::string& templateName) const { return m_TemplateLoader.TemplateExists(templateName); } const CParamNode& CGUIManager::GetTemplate(const std::string& templateName) { const CParamNode& templateRoot = m_TemplateLoader.GetTemplateFileData(templateName).GetChild("Entity"); if (!templateRoot.IsOk()) LOGERROR("Invalid template found for '%s'", templateName.c_str()); return templateRoot; } // This returns a shared_ptr to make sure the CGUI doesn't get deallocated // while we're in the middle of calling a function on it (e.g. if a GUI script // calls SwitchPage) shared_ptr CGUIManager::top() const { ENSURE(m_PageStack.size()); return m_PageStack.back().gui; } Index: ps/trunk/source/gui/GUIManager.h =================================================================== --- ps/trunk/source/gui/GUIManager.h (revision 22756) +++ ps/trunk/source/gui/GUIManager.h (revision 22757) @@ -1,196 +1,196 @@ /* 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 . */ #ifndef INCLUDED_GUIMANAGER #define INCLUDED_GUIMANAGER #include #include #include "lib/input.h" #include "lib/file/vfs/vfs_path.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/TemplateLoader.h" #include "scriptinterface/ScriptVal.h" #include "scriptinterface/ScriptInterface.h" class CGUI; class JSObject; class IGUIObject; struct CGUIColor; struct SGUIIcon; /** * External interface to the GUI system. * * The GUI consists of a set of pages. Each page is constructed from a * series of XML files, and is independent from any other page. * Only one page is active at a time. All events and render requests etc * will go to the active page. This lets the GUI switch between pre-game menu * and in-game UI. */ class CGUIManager { NONCOPYABLE(CGUIManager); public: CGUIManager(); ~CGUIManager(); shared_ptr GetScriptInterface() { return m_ScriptInterface; } shared_ptr GetRuntime() { return m_ScriptRuntime; } shared_ptr GetActiveGUI() { return top(); } /** * Returns whether there are any current pages. */ bool HasPages(); /** * Load a new GUI page and make it active. All current pages will be destroyed. */ void SwitchPage(const CStrW& name, ScriptInterface* srcScriptInterface, JS::HandleValue initData); /** * Load a new GUI page and make it active. All current pages will be retained, * and will still be drawn and receive tick events, but will not receive * user inputs. * If given, the callbackHandler function will be executed once this page is closed. */ void PushPage(const CStrW& pageName, shared_ptr initData, JS::HandleValue callbackFunc); /** * Unload the currently active GUI page, and make the previous page active. * (There must be at least two pages when you call this.) */ void PopPage(shared_ptr args); /** * Called when a file has been modified, to hotload changes. */ Status ReloadChangedFile(const VfsPath& path); /** * Sets the default mouse pointer. */ void ResetCursor(); /** * Called when we should reload all pages (e.g. translation hotloading update). */ Status ReloadAllPages(); /** * Pass input events to the currently active GUI page. */ InReaction HandleEvent(const SDL_Event_* ev); /** * See CGUI::SendEventToAll; applies to the currently active page. */ void SendEventToAll(const CStr& eventName) const; void SendEventToAll(const CStr& eventName, JS::HandleValueArray paramData) const; /** * See CGUI::TickObjects; applies to @em all loaded pages. */ void TickObjects(); /** * See CGUI::Draw; applies to @em all loaded pages. */ - void Draw(); + void Draw() const; /** * See CGUI::UpdateResolution; applies to @em all loaded pages. */ void UpdateResolution(); /** * Calls the current page's script function getSavedGameData() and returns the result. */ std::string GetSavedGameData(); void RestoreSavedGameData(const std::string& jsonData); /** * Check if a template with this name exists */ bool TemplateExists(const std::string& templateName) const; /** * Retrieve the requested template, used for displaying faction specificities. */ const CParamNode& GetTemplate(const std::string& templateName); private: struct SGUIPage { // COPYABLE, because event handlers may invalidate page stack iterators by open or close pages, // and event handlers need to be called for the entire stack. /** * Initializes the data that will be used to create the CGUI page one or multiple times (hotloading). */ SGUIPage(const CStrW& pageName, const shared_ptr initData); /** * Create the CGUI with it's own ScriptInterface. Deletes the previous CGUI if it existed. */ void LoadPage(shared_ptr scriptRuntime); /** * Sets the callback handler when a new page is opened that will be performed when the page is closed. */ void SetCallbackFunction(ScriptInterface& scriptInterface, JS::HandleValue callbackFunc); /** * Execute the stored callback function with the given arguments. */ void PerformCallbackFunction(shared_ptr args); CStrW name; boost::unordered_set inputs; // for hotloading shared_ptr initData; // data to be passed to the init() function shared_ptr gui; // the actual GUI page /** * Function executed by this parent GUI page when the child GUI page it pushed is popped. * Notice that storing it in the SGUIPage instead of CGUI means that it will survive the hotloading CGUI reset. */ shared_ptr callbackFunction; }; shared_ptr top() const; shared_ptr m_ScriptRuntime; shared_ptr m_ScriptInterface; typedef std::vector PageStackType; PageStackType m_PageStack; CTemplateLoader m_TemplateLoader; }; extern CGUIManager* g_GUI; extern InReaction gui_handler(const SDL_Event_* ev); #endif // INCLUDED_GUIMANAGER Index: ps/trunk/source/gui/IGUIObject.cpp =================================================================== --- ps/trunk/source/gui/IGUIObject.cpp (revision 22756) +++ ps/trunk/source/gui/IGUIObject.cpp (revision 22757) @@ -1,492 +1,492 @@ /* 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 . */ #include "precompiled.h" #include "GUI.h" #include "gui/scripting/JSInterface_GUITypes.h" #include "gui/scripting/JSInterface_IGUIObject.h" #include "ps/GameSetup/Config.h" #include "ps/CLogger.h" #include "ps/Profile.h" #include "scriptinterface/ScriptInterface.h" #include "soundmanager/ISoundManager.h" IGUIObject::IGUIObject(CGUI& pGUI) : m_pGUI(pGUI), m_pParent(NULL), m_MouseHovering(false), m_LastClickTime() { AddSetting("enabled"); AddSetting("hidden"); AddSetting("size"); AddSetting("style"); AddSetting("hotkey"); AddSetting("z"); AddSetting("absolute"); AddSetting("ghost"); AddSetting("aspectratio"); AddSetting("tooltip"); AddSetting("tooltip_style"); // Setup important defaults GUI::SetSetting(this, "hidden", false); GUI::SetSetting(this, "ghost", false); GUI::SetSetting(this, "enabled", true); GUI::SetSetting(this, "absolute", true); } IGUIObject::~IGUIObject() { for (const std::pair& p : m_Settings) delete p.second; if (!m_ScriptHandlers.empty()) JS_RemoveExtraGCRootsTracer(m_pGUI.GetScriptInterface()->GetJSRuntime(), Trace, this); } //------------------------------------------------------------------- // Functions //------------------------------------------------------------------- void IGUIObject::AddChild(IGUIObject* pChild) { // ENSURE(pChild); pChild->SetParent(this); m_Children.push_back(pChild); { try { // Atomic function, if it fails it won't // have changed anything //UpdateObjects(); pChild->GetGUI().UpdateObjects(); } catch (PSERROR_GUI&) { // If anything went wrong, reverse what we did and throw // an exception telling it never added a child m_Children.erase(m_Children.end()-1); throw; } } // else do nothing } void IGUIObject::AddToPointersMap(map_pObjects& ObjectMap) { // Just don't do anything about the top node if (m_pParent == NULL) return; // Now actually add this one // notice we won't add it if it's doesn't have any parent // (i.e. being the base object) if (m_Name.empty()) { throw PSERROR_GUI_ObjectNeedsName(); } if (ObjectMap.count(m_Name) > 0) { throw PSERROR_GUI_NameAmbiguity(m_Name.c_str()); } else { ObjectMap[m_Name] = this; } } void IGUIObject::Destroy() { // Is there anything besides the children to destroy? } template void IGUIObject::AddSetting(const CStr& Name) { // This can happen due to inheritance if (SettingExists(Name)) return; m_Settings[Name] = new CGUISetting(*this, Name); } -bool IGUIObject::MouseOver() +bool IGUIObject::IsMouseOver() const { return m_CachedActualSize.PointInside(m_pGUI.GetMousePos()); } bool IGUIObject::MouseOverIcon() { return false; } void IGUIObject::UpdateMouseOver(IGUIObject* const& pMouseOver) { if (pMouseOver == this) { if (!m_MouseHovering) SendEvent(GUIM_MOUSE_ENTER, "mouseenter"); m_MouseHovering = true; SendEvent(GUIM_MOUSE_OVER, "mousemove"); } else { if (m_MouseHovering) { m_MouseHovering = false; SendEvent(GUIM_MOUSE_LEAVE, "mouseleave"); } } } bool IGUIObject::SettingExists(const CStr& Setting) const { return m_Settings.count(Setting) == 1; } PSRETURN IGUIObject::SetSetting(const CStr& Setting, const CStrW& Value, const bool& SkipMessage) { if (!SettingExists(Setting)) return PSRETURN_GUI_InvalidSetting; if (!m_Settings[Setting]->FromString(Value, SkipMessage)) return PSRETURN_GUI_UnableToParse; return PSRETURN_OK; } void IGUIObject::ChooseMouseOverAndClosest(IGUIObject*& pObject) { - if (!MouseOver()) + if (!IsMouseOver()) return; // Check if we've got competition at all if (pObject == NULL) { pObject = this; return; } // Or if it's closer if (GetBufferedZ() >= pObject->GetBufferedZ()) { pObject = this; return; } } IGUIObject* IGUIObject::GetParent() const { // Important, we're not using GetParent() for these // checks, that could screw it up if (m_pParent) { if (m_pParent->m_pParent == NULL) return NULL; } return m_pParent; } void IGUIObject::ResetStates() { // Notify the gui that we aren't hovered anymore UpdateMouseOver(nullptr); } void IGUIObject::UpdateCachedSize() { bool absolute; GUI::GetSetting(this, "absolute", absolute); float aspectratio = 0.f; GUI::GetSetting(this, "aspectratio", aspectratio); const CClientArea& ca = GUI::GetSetting(this, "size"); // If absolute="false" and the object has got a parent, // use its cached size instead of the screen. Notice // it must have just been cached for it to work. if (absolute == false && m_pParent && !IsRootObject()) m_CachedActualSize = ca.GetClientArea(m_pParent->m_CachedActualSize); else m_CachedActualSize = ca.GetClientArea(CRect(0.f, 0.f, g_xres / g_GuiScale, g_yres / g_GuiScale)); // In a few cases, GUI objects have to resize to fill the screen // but maintain a constant aspect ratio. // Adjust the size to be the max possible, centered in the original size: if (aspectratio) { if (m_CachedActualSize.GetWidth() > m_CachedActualSize.GetHeight()*aspectratio) { float delta = m_CachedActualSize.GetWidth() - m_CachedActualSize.GetHeight()*aspectratio; m_CachedActualSize.left += delta/2.f; m_CachedActualSize.right -= delta/2.f; } else { float delta = m_CachedActualSize.GetHeight() - m_CachedActualSize.GetWidth()/aspectratio; m_CachedActualSize.bottom -= delta/2.f; m_CachedActualSize.top += delta/2.f; } } } void IGUIObject::LoadStyle(CGUI& pGUI, const CStr& StyleName) { if (pGUI.HasStyle(StyleName)) LoadStyle(pGUI.GetStyle(StyleName)); else debug_warn(L"IGUIObject::LoadStyle failed"); } void IGUIObject::LoadStyle(const SGUIStyle& Style) { // Iterate settings, it won't be able to set them all probably, but that doesn't matter for (const std::pair& p : Style.m_SettingsDefaults) { // Try set setting in object SetSetting(p.first, p.second); // It doesn't matter if it fail, it's not suppose to be able to set every setting. // since it's generic. // The beauty with styles is that it can contain more settings // than exists for the objects using it. So if the SetSetting // fails, don't care. } } float IGUIObject::GetBufferedZ() const { bool absolute; GUI::GetSetting(this, "absolute", absolute); float Z; GUI::GetSetting(this, "z", Z); if (absolute) return Z; else { if (GetParent()) return GetParent()->GetBufferedZ() + Z; else { // In philosophy, a parentless object shouldn't be able to have a relative sizing, // but we'll accept it so that absolute can be used as default without a complaint. // Also, you could consider those objects children to the screen resolution. return Z; } } } void IGUIObject::RegisterScriptHandler(const CStr& Action, const CStr& Code, CGUI& pGUI) { JSContext* cx = pGUI.GetScriptInterface()->GetContext(); JSAutoRequest rq(cx); JS::RootedValue globalVal(cx, pGUI.GetGlobalObject()); JS::RootedObject globalObj(cx, &globalVal.toObject()); const int paramCount = 1; const char* paramNames[paramCount] = { "mouse" }; // Location to report errors from CStr CodeName = GetName()+" "+Action; // Generate a unique name static int x = 0; char buf[64]; sprintf_s(buf, ARRAY_SIZE(buf), "__eventhandler%d (%s)", x++, Action.c_str()); JS::CompileOptions options(cx); options.setFileAndLine(CodeName.c_str(), 0); options.setIsRunOnce(false); JS::RootedFunction func(cx); JS::AutoObjectVector emptyScopeChain(cx); if (!JS::CompileFunction(cx, emptyScopeChain, options, buf, paramCount, paramNames, Code.c_str(), Code.length(), &func)) { LOGERROR("RegisterScriptHandler: Failed to compile the script for %s", Action.c_str()); return; } JS::RootedObject funcObj(cx, JS_GetFunctionObject(func)); SetScriptHandler(Action, funcObj); } void IGUIObject::SetScriptHandler(const CStr& Action, JS::HandleObject Function) { if (m_ScriptHandlers.empty()) JS_AddExtraGCRootsTracer(m_pGUI.GetScriptInterface()->GetJSRuntime(), Trace, this); m_ScriptHandlers[Action] = JS::Heap(Function); } InReaction IGUIObject::SendEvent(EGUIMessageType type, const CStr& EventName) { PROFILE2_EVENT("gui event"); PROFILE2_ATTR("type: %s", EventName.c_str()); PROFILE2_ATTR("object: %s", m_Name.c_str()); SGUIMessage msg(type); HandleMessage(msg); ScriptEvent(EventName); return (msg.skipped ? IN_PASS : IN_HANDLED); } void IGUIObject::ScriptEvent(const CStr& Action) { std::map >::iterator it = m_ScriptHandlers.find(Action); if (it == m_ScriptHandlers.end()) return; JSContext* cx = m_pGUI.GetScriptInterface()->GetContext(); JSAutoRequest rq(cx); // Set up the 'mouse' parameter JS::RootedValue mouse(cx); const CPos& mousePos = m_pGUI.GetMousePos(); m_pGUI.GetScriptInterface()->CreateObject( &mouse, "x", mousePos.x, "y", mousePos.y, "buttons", m_pGUI.GetMouseButtons()); JS::AutoValueVector paramData(cx); paramData.append(mouse); JS::RootedObject obj(cx, GetJSObject()); JS::RootedValue handlerVal(cx, JS::ObjectValue(*it->second)); JS::RootedValue result(cx); bool ok = JS_CallFunctionValue(cx, obj, handlerVal, paramData, &result); if (!ok) { // We have no way to propagate the script exception, so just ignore it // and hope the caller checks JS_IsExceptionPending } } void IGUIObject::ScriptEvent(const CStr& Action, const JS::HandleValueArray& paramData) { std::map >::iterator it = m_ScriptHandlers.find(Action); if (it == m_ScriptHandlers.end()) return; JSContext* cx = m_pGUI.GetScriptInterface()->GetContext(); JSAutoRequest rq(cx); JS::RootedObject obj(cx, GetJSObject()); JS::RootedValue handlerVal(cx, JS::ObjectValue(*it->second)); JS::RootedValue result(cx); if (!JS_CallFunctionValue(cx, obj, handlerVal, paramData, &result)) JS_ReportError(cx, "Errors executing script action \"%s\"", Action.c_str()); } void IGUIObject::CreateJSObject() { JSContext* cx = m_pGUI.GetScriptInterface()->GetContext(); JSAutoRequest rq(cx); m_JSObject.init(cx, m_pGUI.GetScriptInterface()->CreateCustomObject("GUIObject")); JS_SetPrivate(m_JSObject.get(), this); } JSObject* IGUIObject::GetJSObject() { // Cache the object when somebody first asks for it, because otherwise // we end up doing far too much object allocation. if (!m_JSObject.initialized()) CreateJSObject(); return m_JSObject.get(); } -bool IGUIObject::IsHidden() +bool IGUIObject::IsHidden() const { // Statically initialise some strings, so we don't have to do // lots of allocation every time this function is called static const CStr strHidden("hidden"); return GUI::GetSetting(this, strHidden); } -bool IGUIObject::IsHiddenOrGhost() +bool IGUIObject::IsHiddenOrGhost() const { static const CStr strGhost("ghost"); return IsHidden() || GUI::GetSetting(this, strGhost); } void IGUIObject::PlaySound(const CStr& settingName) const { if (!g_SoundManager) return; const CStrW& soundPath = GUI::GetSetting(this, settingName); if (!soundPath.empty()) g_SoundManager->PlayAsUI(soundPath.c_str(), false); } CStr IGUIObject::GetPresentableName() const { // __internal(), must be at least 13 letters to be able to be // an internal name if (m_Name.length() <= 12) return m_Name; if (m_Name.substr(0, 10) == "__internal") return CStr("[unnamed object]"); else return m_Name; } void IGUIObject::SetFocus() { m_pGUI.SetFocusedObject(this); } bool IGUIObject::IsFocused() const { return m_pGUI.GetFocusedObject() == this; } bool IGUIObject::IsRootObject() const { return m_pParent == m_pGUI.GetBaseObject(); } void IGUIObject::TraceMember(JSTracer* trc) { // Please ensure to adapt the Tracer enabling and disabling in accordance with the GC things traced! for (std::pair>& handler : m_ScriptHandlers) JS_CallObjectTracer(trc, &handler.second, "IGUIObject::m_ScriptHandlers"); } // Instantiate templated functions: #define TYPE(T) template void IGUIObject::AddSetting(const CStr& Name); #include "GUItypes.h" #undef TYPE Index: ps/trunk/source/gui/IGUIObject.h =================================================================== --- ps/trunk/source/gui/IGUIObject.h (revision 22756) +++ ps/trunk/source/gui/IGUIObject.h (revision 22757) @@ -1,516 +1,516 @@ /* 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 . */ /* * The base class of an object. * All objects are derived from this class. * It's an abstract data type, so it can't be used per se. * Also contains a Dummy object which is used for completely blank objects. */ #ifndef INCLUDED_IGUIOBJECT #define INCLUDED_IGUIOBJECT #include "IGUIObject.h" #include "gui/CGUI.h" #include "gui/GUIbase.h" #include "gui/scripting/JSInterface_IGUIObject.h" #include "lib/input.h" // just for IN_PASS #include "ps/XML/Xeromyces.h" #include #include #include struct SGUIStyle; class JSObject; class IGUISetting; template class GUI; ERROR_TYPE(GUI, UnableToParse); /** * GUI object such as a button or an input-box. * Abstract data type ! */ class IGUIObject { friend class CGUI; friend class IGUIScrollBar; friend class GUITooltip; // Allow getProperty to access things like GetParent() friend bool JSI_IGUIObject::getProperty(JSContext* cx, JS::HandleObject obj, JS::HandleId id, JS::MutableHandleValue vp); friend bool JSI_IGUIObject::setProperty(JSContext* cx, JS::HandleObject obj, JS::HandleId id, JS::MutableHandleValue vp, JS::ObjectOpResult& result); friend bool JSI_IGUIObject::getComputedSize(JSContext* cx, uint argc, JS::Value* vp); public: NONCOPYABLE(IGUIObject); IGUIObject(CGUI& pGUI); virtual ~IGUIObject(); /** * Checks if mouse is hovering this object. * The mouse position is cached in CGUI. * * This function checks if the mouse is hovering the * rectangle that the base setting "size" makes. * Although it is virtual, so one could derive * an object from CButton, which changes only this * to checking the circle that "size" makes. * * @return true if mouse is hovering */ - virtual bool MouseOver(); + virtual bool IsMouseOver() const; /** * Test if mouse position is over an icon */ virtual bool MouseOverIcon(); //-------------------------------------------------------- /** @name Leaf Functions */ //-------------------------------------------------------- //@{ /// Get object name, name is unique const CStr& GetName() const { return m_Name; } /// Get object name void SetName(const CStr& Name) { m_Name = Name; } // Get Presentable name. // Will change all internally set names to something like "" CStr GetPresentableName() const; /** * Adds object and its children to the map, it's name being the * first part, and the second being itself. * * @param ObjectMap Adds this to the map_pObjects. * * @throws PSERROR_GUI_ObjectNeedsName Name is missing * @throws PSERROR_GUI_NameAmbiguity Name is already taken */ void AddToPointersMap(map_pObjects& ObjectMap); /** * Notice nothing will be returned or thrown if the child hasn't * been inputted into the GUI yet. This is because that's were * all is checked. Now we're just linking two objects, but * it's when we're inputting them into the GUI we'll check * validity! Notice also when adding it to the GUI this function * will inevitably have been called by CGUI::AddObject which * will catch the throw and return the error code. * i.e. The user will never put in the situation wherein a throw * must be caught, the GUI's internal error handling will be * completely transparent to the interfacially sequential model. * * @param pChild Child to add * * @throws PSERROR_GUI from CGUI::UpdateObjects(). */ void AddChild(IGUIObject* pChild); /** * Return all child objects of the current object. */ - std::vector& GetChildren() { return m_Children; } + const std::vector& GetChildren() const { return m_Children; } //@} //-------------------------------------------------------- /** @name Settings Management */ //-------------------------------------------------------- //@{ /** * Returns whether there is a setting with the given name registered. * * @param Setting setting name * @return True if settings exist. */ bool SettingExists(const CStr& Setting) const; /** * Returns whether this is object is set to be hidden. */ - bool IsHidden(); + bool IsHidden() const; /** * Returns whether this object is set to be hidden or ghost. */ - bool IsHiddenOrGhost(); + bool IsHiddenOrGhost() const; /** * All sizes are relative to resolution, and the calculation * is not wanted in real time, therefore it is cached, update * the cached size with this function. */ virtual void UpdateCachedSize(); /** * Reset internal state of this object. */ virtual void ResetStates(); /** * Set a setting by string, regardless of what type it is. * * example a CRect(10,10,20,20) would be "10 10 20 20" * * @param Setting Setting by name * @param Value Value to set to * @param SkipMessage Does not send a GUIM_SETTINGS_UPDATED if true * * @return PSRETURN (PSRETURN_OK if successful) */ PSRETURN SetSetting(const CStr& Setting, const CStrW& Value, const bool& SkipMessage = false); /** * Set the script handler for a particular object-specific action * * @param Action Name of action * @param Code Javascript code to execute when the action occurs * @param pGUI GUI instance to associate the script with */ void RegisterScriptHandler(const CStr& Action, const CStr& Code, CGUI& pGUI); /** * Creates the JS Object representing this page upon first use. * Can be overridden by derived classes to extend it. */ virtual void CreateJSObject(); /** * Retrieves the JSObject representing this GUI object. */ JSObject* GetJSObject(); //@} protected: //-------------------------------------------------------- /** @name Called by CGUI and friends * * Methods that the CGUI will call using * its friendship, these should not * be called by user. * These functions' security are a lot * what constitutes the GUI's */ //-------------------------------------------------------- //@{ /** * Add a setting to m_Settings * * @param Type Setting type * @param Name Setting reference name */ template void AddSetting(const CStr& Name); /** * Calls Destroy on all children, and deallocates all memory. * MEGA TODO Should it destroy it's children? */ virtual void Destroy(); public: /** * This function is called with different messages * for instance when the mouse enters the object. * * @param Message GUI Message */ virtual void HandleMessage(SGUIMessage& UNUSED(Message)) {} /** * Calls an IGUIObject member function recursively on this object and its children. * Aborts recursion at IGUIObjects that have the isRestricted function return true. * The arguments of the callback function must be references. */ template - void RecurseObject(bool(IGUIObject::*isRestricted)(), void(IGUIObject::*callbackFunction)(Args... args), Args&&... args) + void RecurseObject(bool(IGUIObject::*isRestricted)() const, void(IGUIObject::*callbackFunction)(Args... args), Args&&... args) { if (this != m_pGUI.GetBaseObject()) { if (isRestricted && (this->*isRestricted)()) return; (this->*callbackFunction)(args...); } for (IGUIObject* const& obj : m_Children) obj->RecurseObject(isRestricted, callbackFunction, args...); } protected: /** * Draws the object. * * @throws PSERROR if any. But this will mostlikely be * very rare since if an object is drawn unsuccessfully * it'll probably only output in the Error log, and not * disrupt the whole GUI drawing. */ virtual void Draw() = 0; /** * Some objects need to handle the SDL_Event_ manually. * For instance the input box. * * Only the object with focus will have this function called. * * Returns either IN_PASS or IN_HANDLED. If IN_HANDLED, then * the key won't be passed on and processed by other handlers. * This is used for keys that the GUI uses. */ virtual InReaction ManuallyHandleEvent(const SDL_Event_* UNUSED(ev)) { return IN_PASS; } /** * Loads a style. * * @param GUIinstance Reference to the GUI * @param StyleName Style by name */ void LoadStyle(CGUI& pGUI, const CStr& StyleName); /** * Loads a style. * * @param Style The style object. */ void LoadStyle(const SGUIStyle& Style); /** * Returns not the Z value, but the actual buffered Z value, i.e. if it's * defined relative, then it will check its parent's Z value and add * the relativity. * * @return Actual Z value on the screen. */ virtual float GetBufferedZ() const; /** * Set parent of this object */ void SetParent(IGUIObject* pParent) { m_pParent = pParent; } public: CGUI& GetGUI() { return m_pGUI; } const CGUI& GetGUI() const { return m_pGUI; } /** * Take focus! */ void SetFocus(); /** * Workaround to avoid a dynamic_cast which can be 80 times slower than this. */ virtual void* GetTextOwner() { return nullptr; } protected: /** * Check if object is focused. */ bool IsFocused() const; /** * NOTE! This will not just return m_pParent, when that is * need use it! There is one exception to it, when the parent is * the top-node (the object that isn't a real object), this * will return NULL, so that the top-node's children are * seemingly parentless. * * @return Pointer to parent */ IGUIObject* GetParent() const; /** * Handle additional children to the \-tag. In IGUIObject, this function does * nothing. In CList and CDropDown, it handles the \, used to build the data. * * Returning false means the object doesn't recognize the child. Should be reported. * Notice 'false' is default, because an object not using this function, should not * have any additional children (and this function should never be called). */ virtual bool HandleAdditionalChildren(const XMBElement& UNUSED(child), CXeromyces* UNUSED(pFile)) { return false; } /** * Cached size, real size m_Size is actually dependent on resolution * and can have different *real* outcomes, this is the real outcome * cached to avoid slow calculations in real time. */ CRect m_CachedActualSize; /** * Send event to this GUI object (HandleMessage and ScriptEvent) * * @param type Type of GUI message to be handled * @param EventName String representation of event name * @return IN_HANDLED if event was handled, or IN_PASS if skipped */ InReaction SendEvent(EGUIMessageType type, const CStr& EventName); /** * Execute the script for a particular action. * Does nothing if no script has been registered for that action. * The mouse coordinates will be passed as the first argument. * * @param Action Name of action */ void ScriptEvent(const CStr& Action); /** * Execute the script for a particular action. * Does nothing if no script has been registered for that action. * * @param Action Name of action * @param paramData JS::HandleValueArray arguments to pass to the event. */ void ScriptEvent(const CStr& Action, const JS::HandleValueArray& paramData); void SetScriptHandler(const CStr& Action, JS::HandleObject Function); /** * Inputes the object that is currently hovered, this function * updates this object accordingly (i.e. if it's the object * being inputted one thing happens, and not, another). * * @param pMouseOver Object that is currently hovered, * can OF COURSE be NULL too! */ void UpdateMouseOver(IGUIObject* const& pMouseOver); /** * Retrieves the configured sound filename from the given setting name and plays that once. */ void PlaySound(const CStr& settingName) const; //@} private: //-------------------------------------------------------- /** @name Internal functions */ //-------------------------------------------------------- //@{ /** * Inputs a reference pointer, checks if the new inputted object * if hovered, if so, then check if this's Z value is greater * than the inputted object... If so then the object is closer * and we'll replace the pointer with this. * Also Notice input can be NULL, which means the Z value demand * is out. NOTICE you can't input NULL as const so you'll have * to set an object to NULL. * * @param pObject Object pointer, can be either the old one, or * the new one. */ void ChooseMouseOverAndClosest(IGUIObject*& pObject); // Is the object a Root object, in philosophy, this means it // has got no parent, and technically, it's got the m_BaseObject // as parent. bool IsRootObject() const; static void Trace(JSTracer* trc, void* data) { reinterpret_cast(data)->TraceMember(trc); } void TraceMember(JSTracer* trc); // Variables protected: // Name of object CStr m_Name; // Constructed on the heap, will be destroyed along with the the object // TODO Gee: really the above? vector_pObjects m_Children; // Pointer to parent IGUIObject *m_pParent; //This represents the last click time for each mouse button double m_LastClickTime[6]; /** * This is an array of true or false, each element is associated with * a string representing a setting. Number of elements is equal to * number of settings. * * A true means the setting has been manually set in the file when * read. This is important to know because I don't want to force * the user to include its \-XML-files first, so somehow * the GUI needs to know which settings were set, and which is meant * to. */ // More variables - // Is mouse hovering the object? used with the function MouseOver() + // Is mouse hovering the object? used with the function IsMouseOver() bool m_MouseHovering; /** * Settings pool, all an object's settings are located here * If a derived object has got more settings that the base * settings, it's because they have a new version of the * function SetupSettings(). * * @see SetupSettings() */ public: std::map m_Settings; protected: // An object can't function stand alone CGUI& m_pGUI; // Internal storage for registered script handlers. std::map > m_ScriptHandlers; // Cached JSObject representing this GUI object JS::PersistentRootedObject m_JSObject; }; /** * Dummy object used primarily for the root object * or objects of type 'empty' */ class CGUIDummyObject : public IGUIObject { GUI_OBJECT(CGUIDummyObject) public: CGUIDummyObject(CGUI& pGUI) : IGUIObject(pGUI) {} virtual void Draw() {} // Empty can never be hovered. It is only a category. - virtual bool MouseOver() { return false; } + virtual bool IsMouseOver() const { return false; } }; #endif // INCLUDED_IGUIOBJECT Index: ps/trunk/source/gui/MiniMap.cpp =================================================================== --- ps/trunk/source/gui/MiniMap.cpp (revision 22756) +++ ps/trunk/source/gui/MiniMap.cpp (revision 22757) @@ -1,712 +1,712 @@ /* 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 . */ #include "precompiled.h" #include #include "MiniMap.h" #include "graphics/GameView.h" #include "graphics/LOSTexture.h" #include "graphics/MiniPatch.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureEntry.h" #include "graphics/TerrainTextureManager.h" #include "graphics/TerritoryTexture.h" #include "gui/GUI.h" #include "gui/GUIManager.h" #include "gui/GUIMatrix.h" #include "lib/bits.h" #include "lib/external_libraries/libsdl.h" #include "lib/ogl.h" #include "lib/timer.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/GameSetup/Config.h" #include "ps/Profile.h" #include "ps/World.h" #include "ps/XML/Xeromyces.h" #include "renderer/Renderer.h" #include "renderer/RenderingOptions.h" #include "renderer/WaterManager.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpMinimap.h" #include "simulation2/system/ParamNode.h" extern bool g_GameRestarted; // Set max drawn entities to UINT16_MAX for now, which is more than enough // TODO: we should be cleverer about drawing them to reduce clutter const u16 MAX_ENTITIES_DRAWN = 65535; static unsigned int ScaleColor(unsigned int color, float x) { unsigned int r = unsigned(float(color & 0xff) * x); unsigned int g = unsigned(float((color>>8) & 0xff) * x); unsigned int b = unsigned(float((color>>16) & 0xff) * x); return (0xff000000 | b | g<<8 | r<<16); } CMiniMap::CMiniMap(CGUI& pGUI) : IGUIObject(pGUI), m_TerrainTexture(0), m_TerrainData(0), m_MapSize(0), m_Terrain(0), m_TerrainDirty(true), m_MapScale(1.f), m_EntitiesDrawn(0), m_IndexArray(GL_STATIC_DRAW), m_VertexArray(GL_DYNAMIC_DRAW), m_NextBlinkTime(0.0), m_PingDuration(25.0), m_BlinkState(false), m_WaterHeight(0.0) { AddSetting("tooltip"); AddSetting("tooltip_style"); m_Clicking = false; m_MouseHovering = false; // Register Relax NG validator CXeromyces::AddValidator(g_VFS, "pathfinder", "simulation/data/pathfinder.rng"); // Get the maximum height for unit passage in water. CParamNode externalParamNode; CParamNode::LoadXML(externalParamNode, L"simulation/data/pathfinder.xml", "pathfinder"); const CParamNode pathingSettings = externalParamNode.GetChild("Pathfinder").GetChild("PassabilityClasses"); if (pathingSettings.GetChild("default").IsOk() && pathingSettings.GetChild("default").GetChild("MaxWaterDepth").IsOk()) m_ShallowPassageHeight = pathingSettings.GetChild("default").GetChild("MaxWaterDepth").ToFloat(); else m_ShallowPassageHeight = 0.0f; m_AttributePos.type = GL_FLOAT; m_AttributePos.elems = 2; m_VertexArray.AddAttribute(&m_AttributePos); m_AttributeColor.type = GL_UNSIGNED_BYTE; m_AttributeColor.elems = 4; m_VertexArray.AddAttribute(&m_AttributeColor); m_VertexArray.SetNumVertices(MAX_ENTITIES_DRAWN); m_VertexArray.Layout(); m_IndexArray.SetNumVertices(MAX_ENTITIES_DRAWN); m_IndexArray.Layout(); VertexArrayIterator index = m_IndexArray.GetIterator(); for (u16 i = 0; i < MAX_ENTITIES_DRAWN; ++i) *index++ = i; m_IndexArray.Upload(); m_IndexArray.FreeBackingStore(); VertexArrayIterator attrPos = m_AttributePos.GetIterator(); VertexArrayIterator attrColor = m_AttributeColor.GetIterator(); for (u16 i = 0; i < MAX_ENTITIES_DRAWN; ++i) { (*attrColor)[0] = 0; (*attrColor)[1] = 0; (*attrColor)[2] = 0; (*attrColor)[3] = 0; ++attrColor; (*attrPos)[0] = -10000.0f; (*attrPos)[1] = -10000.0f; ++attrPos; } m_VertexArray.Upload(); double blinkDuration = 1.0; // Tests won't have config initialised if (CConfigDB::IsInitialised()) { CFG_GET_VAL("gui.session.minimap.pingduration", m_PingDuration); CFG_GET_VAL("gui.session.minimap.blinkduration", blinkDuration); } m_HalfBlinkDuration = blinkDuration/2; } CMiniMap::~CMiniMap() { Destroy(); } void CMiniMap::HandleMessage(SGUIMessage& Message) { switch (Message.type) { case GUIM_MOUSE_PRESS_LEFT: if (m_MouseHovering) { SetCameraPos(); m_Clicking = true; } break; case GUIM_MOUSE_RELEASE_LEFT: if (m_MouseHovering && m_Clicking) SetCameraPos(); m_Clicking = false; break; case GUIM_MOUSE_DBLCLICK_LEFT: if (m_MouseHovering && m_Clicking) SetCameraPos(); m_Clicking = false; break; case GUIM_MOUSE_ENTER: m_MouseHovering = true; break; case GUIM_MOUSE_LEAVE: m_Clicking = false; m_MouseHovering = false; break; case GUIM_MOUSE_RELEASE_RIGHT: CMiniMap::FireWorldClickEvent(SDL_BUTTON_RIGHT, 1); break; case GUIM_MOUSE_DBLCLICK_RIGHT: CMiniMap::FireWorldClickEvent(SDL_BUTTON_RIGHT, 2); break; case GUIM_MOUSE_MOTION: if (m_MouseHovering && m_Clicking) SetCameraPos(); break; case GUIM_MOUSE_WHEEL_DOWN: case GUIM_MOUSE_WHEEL_UP: Message.Skip(); break; default: break; } } -bool CMiniMap::MouseOver() +bool CMiniMap::IsMouseOver() const { // Get the mouse position. const CPos& mousePos = m_pGUI.GetMousePos(); // Get the position of the center of the minimap. CPos minimapCenter = CPos(m_CachedActualSize.left + m_CachedActualSize.GetWidth() / 2.0, m_CachedActualSize.bottom - m_CachedActualSize.GetHeight() / 2.0); // Take the magnitude of the difference of the mouse position and minimap center. double distFromCenter = sqrt(pow((mousePos.x - minimapCenter.x), 2) + pow((mousePos.y - minimapCenter.y), 2)); // If the distance is less then the radius of the minimap (half the width) the mouse is over the minimap. if (distFromCenter < m_CachedActualSize.GetWidth() / 2.0) return true; else return false; } -void CMiniMap::GetMouseWorldCoordinates(float& x, float& z) +void CMiniMap::GetMouseWorldCoordinates(float& x, float& z) const { // Determine X and Z according to proportion of mouse position and minimap const CPos& mousePos = m_pGUI.GetMousePos(); float px = (mousePos.x - m_CachedActualSize.left) / m_CachedActualSize.GetWidth(); float py = (m_CachedActualSize.bottom - mousePos.y) / m_CachedActualSize.GetHeight(); float angle = GetAngle(); // Scale world coordinates for shrunken square map x = TERRAIN_TILE_SIZE * m_MapSize * (m_MapScale * (cos(angle)*(px-0.5) - sin(angle)*(py-0.5)) + 0.5); z = TERRAIN_TILE_SIZE * m_MapSize * (m_MapScale * (cos(angle)*(py-0.5) + sin(angle)*(px-0.5)) + 0.5); } void CMiniMap::SetCameraPos() { CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); CVector3D target; GetMouseWorldCoordinates(target.X, target.Z); target.Y = terrain->GetExactGroundLevel(target.X, target.Z); g_Game->GetView()->MoveCameraTarget(target); } -float CMiniMap::GetAngle() +float CMiniMap::GetAngle() const { CVector3D cameraIn = m_Camera->m_Orientation.GetIn(); return -atan2(cameraIn.X, cameraIn.Z); } void CMiniMap::FireWorldClickEvent(int UNUSED(button), int UNUSED(clicks)) { JSContext* cx = g_GUI->GetActiveGUI()->GetScriptInterface()->GetContext(); JSAutoRequest rq(cx); float x, z; GetMouseWorldCoordinates(x, z); JS::RootedValue coords(cx); g_GUI->GetActiveGUI()->GetScriptInterface()->CreateObject(&coords, "x", x, "z", z); JS::AutoValueVector paramData(cx); paramData.append(coords); ScriptEvent("worldclick", paramData); } // This sets up and draws the rectangle on the minimap // which represents the view of the camera in the world. -void CMiniMap::DrawViewRect(CMatrix3D transform) +void CMiniMap::DrawViewRect(CMatrix3D transform) const { // Compute the camera frustum intersected with a fixed-height plane. // Use the water height as a fixed base height, which should be the lowest we can go float h = g_Renderer.GetWaterManager()->m_WaterHeight; const float width = m_CachedActualSize.GetWidth(); const float height = m_CachedActualSize.GetHeight(); const float invTileMapSize = 1.0f / float(TERRAIN_TILE_SIZE * m_MapSize); CVector3D hitPt[4]; hitPt[0] = m_Camera->GetWorldCoordinates(0, g_Renderer.GetHeight(), h); hitPt[1] = m_Camera->GetWorldCoordinates(g_Renderer.GetWidth(), g_Renderer.GetHeight(), h); hitPt[2] = m_Camera->GetWorldCoordinates(g_Renderer.GetWidth(), 0, h); hitPt[3] = m_Camera->GetWorldCoordinates(0, 0, h); float ViewRect[4][2]; for (int i = 0; i < 4; ++i) { // convert to minimap space ViewRect[i][0] = (width * hitPt[i].X * invTileMapSize); ViewRect[i][1] = (height * hitPt[i].Z * invTileMapSize); } float viewVerts[] = { ViewRect[0][0], -ViewRect[0][1], ViewRect[1][0], -ViewRect[1][1], ViewRect[2][0], -ViewRect[2][1], ViewRect[3][0], -ViewRect[3][1] }; // Enable Scissoring to restrict the rectangle to only the minimap. glScissor( m_CachedActualSize.left * g_GuiScale, g_Renderer.GetHeight() - m_CachedActualSize.bottom * g_GuiScale, width * g_GuiScale, height * g_GuiScale); glEnable(GL_SCISSOR_TEST); glLineWidth(2.0f); CShaderDefines lineDefines; lineDefines.Add(str_MINIMAP_LINE, str_1); CShaderTechniquePtr tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, g_Renderer.GetSystemShaderDefines(), lineDefines); tech->BeginPass(); CShaderProgramPtr shader = tech->GetShader(); shader->Uniform(str_transform, transform); shader->Uniform(str_color, 1.0f, 0.3f, 0.3f, 1.0f); shader->VertexPointer(2, GL_FLOAT, 0, viewVerts); shader->AssertPointersBound(); if (!g_Renderer.m_SkipSubmit) glDrawArrays(GL_LINE_LOOP, 0, 4); tech->EndPass(); glLineWidth(1.0f); glDisable(GL_SCISSOR_TEST); } struct MinimapUnitVertex { // This struct is copyable for convenience and because to move is to copy for primitives. u8 r, g, b, a; float x, y; }; // Adds a vertex to the passed VertexArray static void inline addVertex(const MinimapUnitVertex& v, VertexArrayIterator& attrColor, VertexArrayIterator& attrPos) { (*attrColor)[0] = v.r; (*attrColor)[1] = v.g; (*attrColor)[2] = v.b; (*attrColor)[3] = v.a; ++attrColor; (*attrPos)[0] = v.x; (*attrPos)[1] = v.y; ++attrPos; } -void CMiniMap::DrawTexture(CShaderProgramPtr shader, float coordMax, float angle, float x, float y, float x2, float y2, float z) +void CMiniMap::DrawTexture(CShaderProgramPtr shader, float coordMax, float angle, float x, float y, float x2, float y2, float z) const { // Rotate the texture coordinates (0,0)-(coordMax,coordMax) around their center point (m,m) // Scale square maps to fit in circular minimap area const float s = sin(angle) * m_MapScale; const float c = cos(angle) * m_MapScale; const float m = coordMax / 2.f; float quadTex[] = { m*(-c + s + 1.f), m*(-c + -s + 1.f), m*(c + s + 1.f), m*(-c + s + 1.f), m*(c + -s + 1.f), m*(c + s + 1.f), m*(c + -s + 1.f), m*(c + s + 1.f), m*(-c + -s + 1.f), m*(c + -s + 1.f), m*(-c + s + 1.f), m*(-c + -s + 1.f) }; float quadVerts[] = { x, y, z, x2, y, z, x2, y2, z, x2, y2, z, x, y2, z, x, y, z }; shader->TexCoordPointer(GL_TEXTURE0, 2, GL_FLOAT, 0, quadTex); shader->VertexPointer(3, GL_FLOAT, 0, quadVerts); shader->AssertPointersBound(); if (!g_Renderer.m_SkipSubmit) glDrawArrays(GL_TRIANGLES, 0, 6); } // TODO: render the minimap in a framebuffer and just draw the frambuffer texture // most of the time, updating the framebuffer twice a frame. // Here it updates as ping-pong either texture or vertex array each sec to lower gpu stalling // (those operations cause a gpu sync, which slows down the way gpu works) void CMiniMap::Draw() { PROFILE3("render minimap"); // The terrain isn't actually initialized until the map is loaded, which // happens when the game is started, so abort until then. if (!g_Game || !g_Game->IsGameStarted()) return; CSimulation2* sim = g_Game->GetSimulation2(); CmpPtr cmpRangeManager(*sim, SYSTEM_ENTITY); ENSURE(cmpRangeManager); // Set our globals in case they hadn't been set before m_Camera = g_Game->GetView()->GetCamera(); m_Terrain = g_Game->GetWorld()->GetTerrain(); m_Width = (u32)(m_CachedActualSize.right - m_CachedActualSize.left); m_Height = (u32)(m_CachedActualSize.bottom - m_CachedActualSize.top); m_MapSize = m_Terrain->GetVerticesPerSide(); m_TextureSize = (GLsizei)round_up_to_pow2((size_t)m_MapSize); m_MapScale = (cmpRangeManager->GetLosCircular() ? 1.f : 1.414f); if (!m_TerrainTexture || g_GameRestarted) CreateTextures(); // only update 2x / second // (note: since units only move a few pixels per second on the minimap, // we can get away with infrequent updates; this is slow) // TODO: Update all but camera at same speed as simulation static double last_time; const double cur_time = timer_Time(); const bool doUpdate = cur_time - last_time > 0.5; if (doUpdate) { last_time = cur_time; if (m_TerrainDirty || m_WaterHeight != g_Renderer.GetWaterManager()->m_WaterHeight) RebuildTerrainTexture(); } const float x = m_CachedActualSize.left, y = m_CachedActualSize.bottom; const float x2 = m_CachedActualSize.right, y2 = m_CachedActualSize.top; const float z = GetBufferedZ(); const float texCoordMax = (float)(m_MapSize - 1) / (float)m_TextureSize; const float angle = GetAngle(); const float unitScale = (cmpRangeManager->GetLosCircular() ? 1.f : m_MapScale/2.f); // Disable depth updates to prevent apparent z-fighting-related issues // with some drivers causing units to get drawn behind the texture. glDepthMask(0); CShaderProgramPtr shader; CShaderTechniquePtr tech; CShaderDefines baseDefines; baseDefines.Add(str_MINIMAP_BASE, str_1); tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, g_Renderer.GetSystemShaderDefines(), baseDefines); tech->BeginPass(); shader = tech->GetShader(); // Draw the main textured quad shader->BindTexture(str_baseTex, m_TerrainTexture); const CMatrix3D baseTransform = GetDefaultGuiMatrix(); CMatrix3D baseTextureTransform; baseTextureTransform.SetIdentity(); shader->Uniform(str_transform, baseTransform); shader->Uniform(str_textureTransform, baseTextureTransform); DrawTexture(shader, texCoordMax, angle, x, y, x2, y2, z); // Draw territory boundaries glEnable(GL_BLEND); CTerritoryTexture& territoryTexture = g_Game->GetView()->GetTerritoryTexture(); shader->BindTexture(str_baseTex, territoryTexture.GetTexture()); const CMatrix3D* territoryTransform = territoryTexture.GetMinimapTextureMatrix(); shader->Uniform(str_transform, baseTransform); shader->Uniform(str_textureTransform, *territoryTransform); DrawTexture(shader, 1.0f, angle, x, y, x2, y2, z); tech->EndPass(); // Draw the LOS quad in black, using alpha values from the LOS texture CLOSTexture& losTexture = g_Game->GetView()->GetLOSTexture(); CShaderDefines losDefines; losDefines.Add(str_MINIMAP_LOS, str_1); tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, g_Renderer.GetSystemShaderDefines(), losDefines); tech->BeginPass(); shader = tech->GetShader(); shader->BindTexture(str_baseTex, losTexture.GetTexture()); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); const CMatrix3D* losTransform = losTexture.GetMinimapTextureMatrix(); shader->Uniform(str_transform, baseTransform); shader->Uniform(str_textureTransform, *losTransform); DrawTexture(shader, 1.0f, angle, x, y, x2, y2, z); tech->EndPass(); glDisable(GL_BLEND); PROFILE_START("minimap units"); CShaderDefines pointDefines; pointDefines.Add(str_MINIMAP_POINT, str_1); tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, g_Renderer.GetSystemShaderDefines(), pointDefines); tech->BeginPass(); shader = tech->GetShader(); shader->Uniform(str_transform, baseTransform); shader->Uniform(str_pointSize, 3.f); CMatrix3D unitMatrix; unitMatrix.SetIdentity(); // Center the minimap on the origin of the axis of rotation. unitMatrix.Translate(-(x2 - x) / 2.f, -(y2 - y) / 2.f, 0.f); // Rotate the map. unitMatrix.RotateZ(angle); // Scale square maps to fit. unitMatrix.Scale(unitScale, unitScale, 1.f); // Move the minimap back to it's starting position. unitMatrix.Translate((x2 - x) / 2.f, (y2 - y) / 2.f, 0.f); // Move the minimap to it's final location. unitMatrix.Translate(x, y, z); // Apply the gui matrix. unitMatrix *= GetDefaultGuiMatrix(); // Load the transform into the shader. shader->Uniform(str_transform, unitMatrix); const float sx = (float)m_Width / ((m_MapSize - 1) * TERRAIN_TILE_SIZE); const float sy = (float)m_Height / ((m_MapSize - 1) * TERRAIN_TILE_SIZE); CSimulation2::InterfaceList ents = sim->GetEntitiesWithInterface(IID_Minimap); if (doUpdate) { VertexArrayIterator attrPos = m_AttributePos.GetIterator(); VertexArrayIterator attrColor = m_AttributeColor.GetIterator(); m_EntitiesDrawn = 0; MinimapUnitVertex v; std::vector pingingVertices; pingingVertices.reserve(MAX_ENTITIES_DRAWN / 2); if (cur_time > m_NextBlinkTime) { m_BlinkState = !m_BlinkState; m_NextBlinkTime = cur_time + m_HalfBlinkDuration; } entity_pos_t posX, posZ; for (CSimulation2::InterfaceList::const_iterator it = ents.begin(); it != ents.end(); ++it) { ICmpMinimap* cmpMinimap = static_cast(it->second); if (cmpMinimap->GetRenderData(v.r, v.g, v.b, posX, posZ)) { ICmpRangeManager::ELosVisibility vis = cmpRangeManager->GetLosVisibility(it->first, g_Game->GetSimulation2()->GetSimContext().GetCurrentDisplayedPlayer()); if (vis != ICmpRangeManager::VIS_HIDDEN) { v.a = 255; v.x = posX.ToFloat() * sx; v.y = -posZ.ToFloat() * sy; // Check minimap pinging to indicate something if (m_BlinkState && cmpMinimap->CheckPing(cur_time, m_PingDuration)) { v.r = 255; // ping color is white v.g = 255; v.b = 255; pingingVertices.push_back(v); } else { addVertex(v, attrColor, attrPos); ++m_EntitiesDrawn; } } } } // Add the pinged vertices at the end, so they are drawn on top for (size_t v = 0; v < pingingVertices.size(); ++v) { addVertex(pingingVertices[v], attrColor, attrPos); ++m_EntitiesDrawn; } ENSURE(m_EntitiesDrawn < MAX_ENTITIES_DRAWN); m_VertexArray.Upload(); } m_VertexArray.PrepareForRendering(); if (m_EntitiesDrawn > 0) { #if !CONFIG2_GLES if (g_RenderingOptions.GetRenderPath() == RenderPath::SHADER) glEnable(GL_VERTEX_PROGRAM_POINT_SIZE); #endif u8* indexBase = m_IndexArray.Bind(); u8* base = m_VertexArray.Bind(); const GLsizei stride = (GLsizei)m_VertexArray.GetStride(); shader->VertexPointer(2, GL_FLOAT, stride, base + m_AttributePos.offset); shader->ColorPointer(4, GL_UNSIGNED_BYTE, stride, base + m_AttributeColor.offset); shader->AssertPointersBound(); if (!g_Renderer.m_SkipSubmit) glDrawElements(GL_POINTS, (GLsizei)(m_EntitiesDrawn), GL_UNSIGNED_SHORT, indexBase); g_Renderer.GetStats().m_DrawCalls++; CVertexBuffer::Unbind(); #if !CONFIG2_GLES if (g_RenderingOptions.GetRenderPath() == RenderPath::SHADER) glDisable(GL_VERTEX_PROGRAM_POINT_SIZE); #endif } tech->EndPass(); DrawViewRect(unitMatrix); PROFILE_END("minimap units"); // Reset depth mask glDepthMask(1); } void CMiniMap::CreateTextures() { Destroy(); // Create terrain texture glGenTextures(1, &m_TerrainTexture); g_Renderer.BindTexture(0, m_TerrainTexture); // Initialise texture with solid black, for the areas we don't // overwrite with glTexSubImage2D later u32* texData = new u32[m_TextureSize * m_TextureSize]; for (ssize_t i = 0; i < m_TextureSize * m_TextureSize; ++i) texData[i] = 0xFF000000; glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_TextureSize, m_TextureSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, texData); delete[] texData; m_TerrainData = new u32[(m_MapSize - 1) * (m_MapSize - 1)]; glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // Rebuild and upload both of them RebuildTerrainTexture(); } void CMiniMap::RebuildTerrainTexture() { u32 x = 0; u32 y = 0; u32 w = m_MapSize - 1; u32 h = m_MapSize - 1; m_WaterHeight = g_Renderer.GetWaterManager()->m_WaterHeight; m_TerrainDirty = false; for (u32 j = 0; j < h; ++j) { u32* dataPtr = m_TerrainData + ((y + j) * (m_MapSize - 1)) + x; for (u32 i = 0; i < w; ++i) { float avgHeight = ( m_Terrain->GetVertexGroundLevel((int)i, (int)j) + m_Terrain->GetVertexGroundLevel((int)i+1, (int)j) + m_Terrain->GetVertexGroundLevel((int)i, (int)j+1) + m_Terrain->GetVertexGroundLevel((int)i+1, (int)j+1) ) / 4.0f; if (avgHeight < m_WaterHeight && avgHeight > m_WaterHeight - m_ShallowPassageHeight) { // shallow water *dataPtr++ = 0xffc09870; } else if (avgHeight < m_WaterHeight) { // Set water as constant color for consistency on different maps *dataPtr++ = 0xffa07850; } else { int hmap = ((int)m_Terrain->GetHeightMap()[(y + j) * m_MapSize + x + i]) >> 8; int val = (hmap / 3) + 170; u32 color = 0xFFFFFFFF; CMiniPatch* mp = m_Terrain->GetTile(x + i, y + j); if (mp) { CTerrainTextureEntry* tex = mp->GetTextureEntry(); if (tex) { // If the texture can't be loaded yet, set the dirty flags // so we'll try regenerating the terrain texture again soon if(!tex->GetTexture()->TryLoad()) m_TerrainDirty = true; color = tex->GetBaseColor(); } } *dataPtr++ = ScaleColor(color, float(val) / 255.0f); } } } // Upload the texture g_Renderer.BindTexture(0, m_TerrainTexture); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m_MapSize - 1, m_MapSize - 1, GL_RGBA, GL_UNSIGNED_BYTE, m_TerrainData); } void CMiniMap::Destroy() { if (m_TerrainTexture) { glDeleteTextures(1, &m_TerrainTexture); m_TerrainTexture = 0; } SAFE_ARRAY_DELETE(m_TerrainData); } Index: ps/trunk/source/gui/MiniMap.h =================================================================== --- ps/trunk/source/gui/MiniMap.h (revision 22756) +++ ps/trunk/source/gui/MiniMap.h (revision 22757) @@ -1,114 +1,114 @@ /* 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 . */ #ifndef INCLUDED_MINIMAP #define INCLUDED_MINIMAP #include "gui/GUI.h" #include "renderer/VertexArray.h" class CCamera; class CMatrix3D; class CTerrain; class CMiniMap : public IGUIObject { GUI_OBJECT(CMiniMap) public: CMiniMap(CGUI& pGUI); virtual ~CMiniMap(); protected: virtual void Draw(); /** * @see IGUIObject#HandleMessage() */ virtual void HandleMessage(SGUIMessage& Message); /** - * @see IGUIObject#MouseOver() + * @see IGUIObject#IsMouseOver() */ - virtual bool MouseOver(); + virtual bool IsMouseOver() const; // create the minimap textures void CreateTextures(); // rebuild the terrain texture map void RebuildTerrainTexture(); // destroy and free any memory and textures void Destroy(); void SetCameraPos(); void FireWorldClickEvent(int button, int clicks); // the terrain we are mini-mapping const CTerrain* m_Terrain; const CCamera* m_Camera; //Whether or not the mouse is currently down bool m_Clicking; // minimap texture handles GLuint m_TerrainTexture; // texture data u32* m_TerrainData; // whether we need to regenerate the terrain texture bool m_TerrainDirty; ssize_t m_Width, m_Height; // map size ssize_t m_MapSize; // texture size GLsizei m_TextureSize; // 1.f if map is circular or 1.414f if square (to shrink it inside the circle) float m_MapScale; // maximal water height to allow the passage of a unit (for underwater shallows). float m_ShallowPassageHeight; float m_WaterHeight; - void DrawTexture(CShaderProgramPtr shader, float coordMax, float angle, float x, float y, float x2, float y2, float z); + void DrawTexture(CShaderProgramPtr shader, float coordMax, float angle, float x, float y, float x2, float y2, float z) const; - void DrawViewRect(CMatrix3D transform); + void DrawViewRect(CMatrix3D transform) const; - void GetMouseWorldCoordinates(float& x, float& z); + void GetMouseWorldCoordinates(float& x, float& z) const; - float GetAngle(); + float GetAngle() const; VertexIndexArray m_IndexArray; VertexArray m_VertexArray; VertexArray::Attribute m_AttributePos; VertexArray::Attribute m_AttributeColor; size_t m_EntitiesDrawn; double m_PingDuration; double m_HalfBlinkDuration; double m_NextBlinkTime; bool m_BlinkState; }; #endif // INCLUDED_MINIMAP