Index: binaries/data/mods/mod/gui/gui.rng =================================================================== --- binaries/data/mods/mod/gui/gui.rng +++ binaries/data/mods/mod/gui/gui.rng @@ -262,6 +262,11 @@ + + + + + Index: binaries/data/mods/public/gui/session/minimap/MiniMap.xml =================================================================== --- binaries/data/mods/public/gui/session/minimap/MiniMap.xml +++ binaries/data/mods/public/gui/session/minimap/MiniMap.xml @@ -22,6 +22,7 @@ sprite="stretched:session/minimap-idle.png" sprite_over="stretched:session/minimap-idle-highlight.png" sprite_disabled="stretched:session/minimap-idle-disabled.png" + mouse_event_mask="texture:session/minimap-idle.png" /> Index: source/gui/ObjectBases/IGUIObject.h =================================================================== --- source/gui/ObjectBases/IGUIObject.h +++ source/gui/ObjectBases/IGUIObject.h @@ -132,6 +132,11 @@ template void RegisterSetting(const CStr& Name, T& Value); + /** + * Like 'RegisterSetting' but with a custom object. + */ + void RegisterCustomSetting(const CStr& Name, IGUISetting* setting); + /** * Returns whether there is a setting with the given name registered. * Index: source/gui/ObjectBases/IGUIObject.cpp =================================================================== --- source/gui/ObjectBases/IGUIObject.cpp +++ source/gui/ObjectBases/IGUIObject.cpp @@ -94,11 +94,16 @@ template void IGUIObject::RegisterSetting(const CStr& Name, T& Value) +{ + RegisterCustomSetting(Name, new CGUISetting(*this, Name, Value)); +} + +void IGUIObject::RegisterCustomSetting(const CStr& Name, IGUISetting* setting) { if (SettingExists(Name)) LOGERROR("The setting '%s' already exists on the object '%s'!", Name.c_str(), GetPresentableName().c_str()); else - m_Settings.emplace(Name, new CGUISetting(*this, Name, Value)); + m_Settings.emplace(Name, setting); } bool IGUIObject::SettingExists(const CStr& Setting) const Index: source/gui/ObjectTypes/CButton.h =================================================================== --- source/gui/ObjectTypes/CButton.h +++ source/gui/ObjectTypes/CButton.h @@ -23,6 +23,7 @@ #include "gui/ObjectBases/IGUIObject.h" #include "gui/ObjectBases/IGUITextOwner.h" #include "gui/SettingTypes/CGUIString.h" +#include "gui/SettingTypes/MouseEventMask.h" #include "maths/Vector2D.h" class CButton : public IGUIObject, public IGUITextOwner, public IGUIButtonBehavior @@ -57,6 +58,11 @@ */ virtual void Draw(); + /** + * @see IGUIObject#IsMouseOver() + */ + virtual bool IsMouseOver() const; + protected: /** * Sets up text, should be called every time changes has been @@ -90,6 +96,7 @@ CGUIColor m_TextColorOver; CGUIColor m_TextColorPressed; CGUIColor m_TextColorDisabled; + CGUIMouseEventMask m_MouseEventMask; }; #endif // INCLUDED_CBUTTON Index: source/gui/ObjectTypes/CButton.cpp =================================================================== --- source/gui/ObjectTypes/CButton.cpp +++ source/gui/ObjectTypes/CButton.cpp @@ -39,7 +39,8 @@ m_TextColor(), m_TextColorOver(), m_TextColorPressed(), - m_TextColorDisabled() + m_TextColorDisabled(), + m_MouseEventMask(this) { RegisterSetting("buffer_zone", m_BufferZone); RegisterSetting("caption", m_Caption); @@ -107,6 +108,15 @@ DrawText(0, ChooseColor(), m_TextPos, bz + 0.1f); } +bool CButton::IsMouseOver() const +{ + if (!IGUIObject::IsMouseOver()) + return false; + if (!m_MouseEventMask) + return true; + return m_MouseEventMask.IsMouseOver(m_pGUI.GetMousePos(), m_CachedActualSize); +} + const CGUIColor& CButton::ChooseColor() { if (!m_Enabled) Index: source/gui/SettingTypes/MouseEventMask.h =================================================================== --- /dev/null +++ source/gui/SettingTypes/MouseEventMask.h @@ -0,0 +1,60 @@ +/* Copyright (C) 2021 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_GUI_MOUSE_EVENT_MASK +#define INCLUDED_GUI_MOUSE_EVENT_MASK + +#include +#include + +class CRect; +class CVector2D; +class IGUIObject; + +/** + * A custom shape that changes the object's "over-ability", and thus where one can click on it. + * Supported: + * - "texture:[path]" loads a texture and uses either the alpha or the red channel. Any non-0 is clickable. + * The texture is always 'stretched' in sprite terminology. + * + * TODO: + * - the minimap circular shape should be moved here. + */ +class CGUIMouseEventMask +{ +public: + CGUIMouseEventMask(IGUIObject* owner); + ~CGUIMouseEventMask(); + + /** + * @return true if the mask is initialised <=> its spec is not "" + */ + explicit operator bool() const { return !!m_Impl; } + + /** + * @return true if the mouse pointer is over the mask. False if the mask is not initialised. + */ + bool IsMouseOver(const CVector2D& mousePos, const CRect& objectSize) const; + + class Impl; +protected: + class Spec; + std::string m_Spec; + std::unique_ptr m_Impl; +}; + +#endif // INCLUDED_GUI_MOUSE_EVENT_MASK Index: source/gui/SettingTypes/MouseEventMask.cpp =================================================================== --- /dev/null +++ source/gui/SettingTypes/MouseEventMask.cpp @@ -0,0 +1,169 @@ +/* Copyright (C) 2021 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 "MouseEventMask.h" + +#include "gui/CGUISetting.h" +#include "lib/tex/tex.h" +#include "ps/Filesystem.h" +#include "ps/CLogger.h" +#include "ps/CStr.h" + + +class CGUIMouseEventMask::Spec final : public IGUISetting +{ +public: + Spec(CGUIMouseEventMask& o) : m_Owner(o) {} + + virtual bool FromString(const CStrW& Value, const bool SendMessage) override; + virtual bool FromJSVal(const ScriptRequest& rq, JS::HandleValue Value, const bool SendMessage) override; + virtual void ToJSVal(const ScriptRequest& rq, JS::MutableHandleValue Value) override; +protected: + CGUIMouseEventMask& m_Owner; +}; + +class CGUIMouseEventMask::Impl +{ +public: + virtual ~Impl() = default; + virtual bool IsMouseOver(const CVector2D& mousePos, const CRect& objectSize) const = 0; +}; + + +CGUIMouseEventMask::CGUIMouseEventMask(IGUIObject* owner) +{ + owner->RegisterCustomSetting("mouse_event_mask", new Spec(*this)); +} + +CGUIMouseEventMask::~CGUIMouseEventMask() +{ +} + +bool CGUIMouseEventMask::IsMouseOver(const CVector2D& mousePos, const CRect& objectSize) const +{ + if (m_Impl) + return m_Impl->IsMouseOver(mousePos, objectSize); + return false; +} + +class CGUIMouseEventMaskTexture final : public CGUIMouseEventMask::Impl +{ +public: + static constexpr const char* identifier = "texture:"; + static constexpr size_t specOffset = sizeof(identifier); + + static std::unique_ptr Create(const std::string& spec) + { + std::shared_ptr shapeFile; + size_t size; + if (g_VFS->LoadFile(VfsPath("art") / L"textures" / L"ui" / spec.substr(specOffset), shapeFile, size) != INFO::OK) + { + LOGWARNING("Mouse event mask texture not found ('%s')", spec); + return nullptr; + } + Tex tex; + if (tex.decode(shapeFile, size) != INFO::OK) + { + LOGERROR("Could not decode mouse event mask texture '%s'", spec); + return nullptr; + } + // TODO > would be nice to downscale, maybe. + if (tex.m_Width == 0 || tex.m_Height == 0) + { + LOGERROR("Mouse event mask texture must have a non-null size ('%s')", spec); + return nullptr; + } + + auto mask = std::make_unique(); + mask->m_Width = tex.m_Width; + mask->m_Height = tex.m_Height; + mask->m_Data.reserve(mask->m_Width * mask->m_Height); + for (u8* ptr = tex.get_data(); ptr < tex.get_data() + tex.m_DataSize; ptr += tex.m_Bpp/8) + { + if (tex.m_Bpp == 32) + mask->m_Data.push_back(*(ptr + 3) > 0); + else + mask->m_Data.push_back(*ptr > 0); + } + return mask; + } + + virtual bool IsMouseOver(const CVector2D& mousePos, const CRect& objectSize) const override + { + if (m_Data.empty()) + return false; + + CVector2D delta = mousePos - objectSize.TopLeft(); + int x = floor(delta.X * m_Width / objectSize.GetWidth()); + int y = floor(delta.Y * m_Height / objectSize.GetHeight()); + if (x < 0 || y < 0 || x >= m_Width || y >= m_Height) + return false; + + return m_Data[x + y * m_Width] > 0; + } + +private: + // This uses the bool specialization on purpose for the 'compression' effect. + std::vector m_Data; + u16 m_Width; + u16 m_Height; +}; + +bool CGUIMouseEventMask::Spec::FromString(const CStrW& Value, const bool UNUSED(SendMessage)) +{ + std::string spec = Value.ToUTF8(); + if (spec == m_Owner.m_Spec) + return true; + + // Empty spec - reset the mask and return. + if (spec.empty()) + { + m_Owner.m_Impl.reset(); + return true; + } + + if (spec.find(CGUIMouseEventMaskTexture::identifier) != std::string::npos) + { + std::unique_ptr newImpl = CGUIMouseEventMaskTexture::Create(spec); + if (newImpl) + { + m_Owner.m_Impl = std::move(newImpl); + m_Owner.m_Spec = spec; + return true; + } + else + LOGERROR("Could not create shape for: %s", spec); + } + else + LOGWARNING("Unknown clickable shape: %s", spec); + return false; +} + +bool CGUIMouseEventMask::Spec::FromJSVal(const ScriptRequest& rq, JS::HandleValue value, const bool sendMessage) +{ + CStrW spec; + if (!ScriptInterface::FromJSVal(rq, value, spec)) + return false; + return FromString(spec, sendMessage); +} + +void CGUIMouseEventMask::Spec::ToJSVal(const ScriptRequest& rq, JS::MutableHandleValue Value) +{ + ScriptInterface::ToJSVal(rq, Value, m_Owner.m_Spec); +}