From 6eca1b485dba7355d827745284ed2f0072f9e370 Mon Sep 17 00:00:00 2001 From: Thorbjørn Lindeijer Date: Tue, 26 Mar 2024 10:47:51 +0000 Subject: Use SDL2 support for color and system mouse cursors This way the cursor is not limited by the framerate nor affected by input lag. Also, when custom cursor is disabled, a few different system cursors are now used instead. It also avoids an issue on Wayland, where hiding the cursor (as done to render our own one) would cause the cursor to get locked within the window. On macOS it fixes two cursors being visible when hovering the window while it is in the background. The cursor can unfortunately no longer gently fade away. --- src/gui/gui.cpp | 231 ++++++++++++++++++++++++-------------- src/gui/gui.h | 31 ++--- src/resources/resourcemanager.cpp | 15 ++- src/resources/resourcemanager.h | 34 ++++-- src/resources/theme.cpp | 4 +- 5 files changed, 194 insertions(+), 121 deletions(-) diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index ae74cab2..dd637170 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -32,18 +32,17 @@ #include "client.h" #include "configuration.h" -#include "eventlistener.h" #include "graphics.h" #include "log.h" -#include "resources/image.h" -#include "resources/imageset.h" #include "resources/resourcemanager.h" #include "resources/theme.h" #include #include +#include + // Guichan stuff Gui *gui = nullptr; SDLInput *guiInput = nullptr; @@ -54,30 +53,8 @@ gcn::Font *boldFont = nullptr; // Mono font gcn::Font *monoFont = nullptr; -class GuiConfigListener : public EventListener -{ - public: - GuiConfigListener(Gui *g): - mGui(g) - {} - - void event(Event::Channel channel, const Event &event) override - { - if (channel == Event::ConfigChannel) - { - if (event.getType() == Event::ConfigOptionChanged && - event.getString("option") == "customcursor") - { - bool bCustomCursor = config.getBoolValue("customcursor"); - mGui->setUseCustomCursor(bCustomCursor); - } - } - } - private: - Gui *mGui; -}; - Gui::Gui(Graphics *graphics) + : mCustomCursorScale(graphics->getScale()) { logger->log("Initializing GUI..."); // Set graphics @@ -146,20 +123,24 @@ Gui::Gui(Graphics *graphics) std::string("': ") + e.getMessage()); } + loadCustomCursors(); + loadSystemCursors(); + gcn::Widget::setGlobalFont(mGuiFont); // Initialize mouse cursor and listen for changes to the option setUseCustomCursor(config.getBoolValue("customcursor")); - mConfigListener = new GuiConfigListener(this); - mConfigListener->listen(Event::ConfigChannel); + + listen(Event::ConfigChannel); } Gui::~Gui() { - delete mConfigListener; + for (auto cursor : mSystemMouseCursors) + SDL_FreeCursor(cursor); - if (mMouseCursors) - mMouseCursors->decRef(); + for (auto cursor : mCustomMouseCursors) + SDL_FreeCursor(cursor); delete mGuiFont; delete boldFont; @@ -174,14 +155,9 @@ Gui::~Gui() void Gui::logic() { - // Fade out mouse cursor after extended inactivity - if (mMouseInactivityTimer < 100 * 15) - { - ++mMouseInactivityTimer; - mMouseCursorAlpha = std::min(1.0f, mMouseCursorAlpha + 0.05f); - } - else - mMouseCursorAlpha = std::max(0.0f, mMouseCursorAlpha - 0.005f); + // Hide mouse cursor after extended inactivity + if (get_elapsed_time(mLastMouseActivityTime) > 15000) + SDL_ShowCursor(SDL_DISABLE); Palette::advanceGradients(); @@ -194,40 +170,30 @@ void Gui::logic() } } -void Gui::draw() +void Gui::event(Event::Channel channel, const Event &event) { - mGraphics->_beginDraw(); - - mGraphics->pushClipArea(mTop->getDimension()); - mTop->draw(mGraphics); - mGraphics->popClipArea(); - - int mouseX; - int mouseY; - Uint8 button = SDL_GetMouseState(&mouseX, &mouseY); - float logicalX; - float logicalY; - graphics->windowToLogical(mouseX, mouseY, logicalX, logicalY); - - if ((Client::hasMouseFocus() || button & SDL_BUTTON(1)) - && mCustomCursor - && mMouseCursorAlpha > 0.0f) + if (channel == Event::ConfigChannel) { - Image *mouseCursor = mMouseCursors->get(static_cast(mCursorType)); - mouseCursor->setAlpha(mMouseCursorAlpha); - - static_cast(mGraphics)->drawImageF( - mouseCursor, - logicalX - 15, - logicalY - 17); + if (event.getType() == Event::ConfigOptionChanged && + event.getString("option") == "customcursor") + { + setUseCustomCursor(config.getBoolValue("customcursor")); + } } - - mGraphics->_endDraw(); } bool Gui::videoResized(int width, int height) { - TrueTypeFont::updateFontScale(static_cast(mGraphics)->getScale()); + const float scale = static_cast(mGraphics)->getScale(); + + TrueTypeFont::updateFontScale(scale); + + if (mCustomCursorScale != scale) + { + mCustomCursorScale = scale; + loadCustomCursors(); + updateCursor(); + } auto *top = static_cast(getTop()); @@ -247,36 +213,33 @@ void Gui::setUseCustomCursor(bool customCursor) return; mCustomCursor = customCursor; + updateCursor(); +} - if (mCustomCursor) - { - // Hide the SDL mouse cursor - SDL_ShowCursor(SDL_DISABLE); +void Gui::setCursorType(Cursor cursor) +{ + if (mCursorType == cursor) + return; - // Load the mouse cursor - mMouseCursors = Theme::getImageSetFromTheme("mouse.png", 40, 40); + mCursorType = cursor; + updateCursor(); +} - if (!mMouseCursors) - logger->error("Unable to load mouse cursors."); - } +void Gui::updateCursor() +{ + if (mCustomCursor && !mCustomMouseCursors.empty()) + SDL_SetCursor(mCustomMouseCursors[static_cast(mCursorType)]); else - { - // Show the SDL mouse cursor - SDL_ShowCursor(SDL_ENABLE); - - // Unload the mouse cursor - if (mMouseCursors) - { - mMouseCursors->decRef(); - mMouseCursors = nullptr; - } - } + SDL_SetCursor(mSystemMouseCursors[static_cast(mCursorType)]); } void Gui::handleMouseMoved(const gcn::MouseInput &mouseInput) { gcn::Gui::handleMouseMoved(mouseInput); - mMouseInactivityTimer = 0; + mLastMouseActivityTime = tick_time; + + // Make sure the cursor is visible + SDL_ShowCursor(SDL_ENABLE); } void Gui::handleTextInput(const TextInput &textInput) @@ -289,3 +252,97 @@ void Gui::handleTextInput(const TextInput &textInput) } } } + +static SDL_Surface *loadSurface(const std::string &path) +{ + if (SDL_RWops *file = ResourceManager::getInstance()->open(path)) + return IMG_Load_RW(file, 1); + return nullptr; +} + +void Gui::loadCustomCursors() +{ + for (auto cursor : mCustomMouseCursors) + SDL_FreeCursor(cursor); + + mCustomMouseCursors.clear(); + + const std::string cursorPath = Theme::resolveThemePath("mouse.png"); + SDL_Surface *mouseSurface = loadSurface(cursorPath); + if (!mouseSurface) + { + logger->log("Warning: Unable to load mouse cursor file (%s): %s", + cursorPath.c_str(), SDL_GetError()); + return; + } + + SDL_SetSurfaceBlendMode(mouseSurface, SDL_BLENDMODE_NONE); + +#if SDL_BYTEORDER == SDL_BIG_ENDIAN + const Uint32 rmask = 0xff000000; + const Uint32 gmask = 0x00ff0000; + const Uint32 bmask = 0x0000ff00; + const Uint32 amask = 0x000000ff; +#else + const Uint32 rmask = 0x000000ff; + const Uint32 gmask = 0x0000ff00; + const Uint32 bmask = 0x00ff0000; + const Uint32 amask = 0xff000000; +#endif + + constexpr int cursorSize = 40; + const int targetCursorSize = cursorSize * mCustomCursorScale; + const int columns = mouseSurface->w / cursorSize; + + SDL_Surface *cursorSurface = SDL_CreateRGBSurface( + 0, targetCursorSize, targetCursorSize, 32, + rmask, gmask, bmask, amask); + + for (int i = 0; i <= static_cast(Cursor::DOWN); ++i) + { + int x = i % columns * cursorSize; + int y = i / columns * cursorSize; + + SDL_Rect srcrect = { x, y, cursorSize, cursorSize }; + SDL_Rect dstrect = { 0, 0, targetCursorSize, targetCursorSize }; + SDL_BlitScaled(mouseSurface, &srcrect, cursorSurface, &dstrect); + + SDL_Cursor *cursor = SDL_CreateColorCursor(cursorSurface, + 15 * mCustomCursorScale, + 17 * mCustomCursorScale); + if (!cursor) + { + logger->log("Warning: Unable to create cursor: %s", SDL_GetError()); + } + + mCustomMouseCursors.push_back(cursor); + } + + SDL_FreeSurface(cursorSurface); + SDL_FreeSurface(mouseSurface); +} + +void Gui::loadSystemCursors() +{ + constexpr struct { + Cursor cursor; + SDL_SystemCursor systemCursor; + } cursors[] = { + { Cursor::POINTER, SDL_SYSTEM_CURSOR_ARROW }, + { Cursor::RESIZE_ACROSS, SDL_SYSTEM_CURSOR_SIZEWE }, + { Cursor::RESIZE_DOWN, SDL_SYSTEM_CURSOR_SIZENS }, + { Cursor::RESIZE_DOWN_LEFT, SDL_SYSTEM_CURSOR_SIZENESW }, + { Cursor::RESIZE_DOWN_RIGHT, SDL_SYSTEM_CURSOR_SIZENWSE }, + { Cursor::FIGHT, SDL_SYSTEM_CURSOR_HAND }, + { Cursor::PICKUP, SDL_SYSTEM_CURSOR_HAND }, + { Cursor::TALK, SDL_SYSTEM_CURSOR_HAND }, + { Cursor::ACTION, SDL_SYSTEM_CURSOR_HAND }, + { Cursor::LEFT, SDL_SYSTEM_CURSOR_ARROW }, + { Cursor::UP, SDL_SYSTEM_CURSOR_ARROW }, + { Cursor::RIGHT, SDL_SYSTEM_CURSOR_ARROW }, + { Cursor::DOWN, SDL_SYSTEM_CURSOR_ARROW } + }; + + for (auto cursor : cursors) + mSystemMouseCursors.push_back(SDL_CreateSystemCursor(cursor.systemCursor)); +} diff --git a/src/gui/gui.h b/src/gui/gui.h index 29dcdef2..e5f5149a 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -22,14 +22,17 @@ #ifndef GUI_H #define GUI_H +#include "eventlistener.h" #include "guichanfwd.h" #include +#include + +#include + class TextInput; class Graphics; -class GuiConfigListener; -class ImageSet; class SDLInput; /** @@ -65,7 +68,7 @@ enum class Cursor { * * \ingroup GUI */ -class Gui : public gcn::Gui +class Gui : public gcn::Gui, public EventListener { public: Gui(Graphics *screen); @@ -78,11 +81,7 @@ class Gui : public gcn::Gui */ void logic() override; - /** - * Draws the whole Gui by calling draw functions down in the - * Gui hierarchy. It also draws the mouse pointer. - */ - void draw() override; + void event(Event::Channel channel, const Event &event) override; /** * Called when the application window has been resized. @@ -115,21 +114,25 @@ class Gui : public gcn::Gui /** * Sets which cursor should be used. */ - void setCursorType(Cursor index) - { mCursorType = index; } + void setCursorType(Cursor cursor); protected: void handleMouseMoved(const gcn::MouseInput &mouseInput) override; void handleTextInput(const TextInput &textInput); private: - GuiConfigListener *mConfigListener; + void updateCursor(); + + void loadCustomCursors(); + void loadSystemCursors(); + gcn::Font *mGuiFont; /**< The global GUI font */ gcn::Font *mInfoParticleFont; /**< Font for Info Particles*/ bool mCustomCursor = false; /**< Show custom cursor */ - ImageSet *mMouseCursors = nullptr; /**< Mouse cursor images */ - float mMouseCursorAlpha = 1.0f; - int mMouseInactivityTimer = 0; + float mCustomCursorScale = 1.0f; + std::vector mSystemMouseCursors; + std::vector mCustomMouseCursors; + int mLastMouseActivityTime = 0; Cursor mCursorType = Cursor::POINTER; }; diff --git a/src/resources/resourcemanager.cpp b/src/resources/resourcemanager.cpp index e2979f06..f43aea41 100644 --- a/src/resources/resourcemanager.cpp +++ b/src/resources/resourcemanager.cpp @@ -230,6 +230,11 @@ std::string ResourceManager::getPath(const std::string &file) return path; } +SDL_RWops *ResourceManager::open(const std::string &path) +{ + return PHYSFSRWOPS_openRead(path.c_str()); +} + Resource *ResourceManager::get(const std::string &idPath, const std::function &generator) { @@ -265,10 +270,10 @@ Resource *ResourceManager::get(const std::string &idPath, return resource; } -Resource *ResourceManager::load(const std::string &path, loader fun) +Resource *ResourceManager::get(const std::string &path, loader fun) { return get(path, [&] () -> Resource * { - if (SDL_RWops *rw = PHYSFSRWOPS_openRead(path.c_str())) + if (SDL_RWops *rw = open(path)) return fun(rw); return nullptr; }); @@ -276,12 +281,12 @@ Resource *ResourceManager::load(const std::string &path, loader fun) Music *ResourceManager::getMusic(const std::string &idPath) { - return static_cast(load(idPath, Music::load)); + return static_cast(get(idPath, Music::load)); } SoundEffect *ResourceManager::getSoundEffect(const std::string &idPath) { - return static_cast(load(idPath, SoundEffect::load)); + return static_cast(get(idPath, SoundEffect::load)); } Image *ResourceManager::getImage(const std::string &idPath) @@ -295,7 +300,7 @@ Image *ResourceManager::getImage(const std::string &idPath) d = std::make_unique(path.substr(p + 1)); path = path.substr(0, p); } - SDL_RWops *rw = PHYSFSRWOPS_openRead(path.c_str()); + SDL_RWops *rw = open(path); if (!rw) return nullptr; diff --git a/src/resources/resourcemanager.h b/src/resources/resourcemanager.h index 7a92818f..6694321c 100644 --- a/src/resources/resourcemanager.h +++ b/src/resources/resourcemanager.h @@ -104,6 +104,16 @@ class ResourceManager */ std::string getPath(const std::string &file); + /** + * Opens a file for reading. The caller is responsible for closing the + * file. + * + * @param path The file name. + * @return A valid SDL_RWops pointer or NULL if the file + * could not be opened. + */ + SDL_RWops *open(const std::string &path); + /** * Creates a resource and adds it to the resource map. * @@ -124,18 +134,7 @@ class ResourceManager * @return A valid resource or NULL if the resource could * not be loaded. */ - Resource *load(const std::string &path, loader fun); - - /** - * Copies a file from one place to another (useful for extracting - * raw files from a zip archive, for example) - * - * @param src Source file name - * @param dst Destination file name - * @return true on success, false on failure. An error message should be - * in the log file. - */ - bool copyFile(const std::string &src, const std::string &dst); + Resource *get(const std::string &path, loader fun); /** * Convenience wrapper around ResourceManager::get for loading @@ -182,6 +181,17 @@ class ResourceManager void *loadFile(const std::string &filename, int &filesize, bool inflate = true); + /** + * Copies a file from one place to another (useful for extracting + * raw files from a zip archive, for example) + * + * @param src Source file name + * @param dst Destination file name + * @return true on success, false on failure. An error message should be + * in the log file. + */ + bool copyFile(const std::string &src, const std::string &dst); + /** * Retrieves the contents of a text file. */ diff --git a/src/resources/theme.cpp b/src/resources/theme.cpp index 1db92feb..67cd6650 100644 --- a/src/resources/theme.cpp +++ b/src/resources/theme.cpp @@ -49,9 +49,7 @@ static void initDefaultThemePath() ResourceManager *resman = ResourceManager::getInstance(); defaultThemePath = branding.getStringValue("guiThemePath"); - if (!defaultThemePath.empty() && resman->isDirectory(defaultThemePath)) - return; - else + if (defaultThemePath.empty() || !resman->isDirectory(defaultThemePath)) defaultThemePath = "graphics/gui/"; } -- cgit v1.2.3-70-g09d2