Skip to content

Commit

Permalink
Refactor mouse global queries into osc::App
Browse files Browse the repository at this point in the history
  • Loading branch information
adamkewley committed Jan 3, 2025
1 parent 3069a89 commit 4e058fe
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 52 deletions.
55 changes: 55 additions & 0 deletions src/oscar/Platform/App.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
#include <oscar/Utils/SynchronizedValue.h>

#include <ankerl/unordered_dense.h>
#if defined(__APPLE__)
#include <TargetConditionals.h> // `TARGET_OS_IOS`
#endif
#include <SDL3/SDL.h>
#include <SDL3/SDL_error.h>
#include <SDL3/SDL_events.h>
Expand All @@ -44,11 +47,19 @@
#include <cstdint>
#include <ctime>
#include <exception>
#include <ranges>
#include <sstream>
#include <stdexcept>
#include <vector>

#if SDL_VERSION_ATLEAST(2,0,4) && !defined(__EMSCRIPTEN__) && !defined(__ANDROID__) && !(defined(__APPLE__) && TARGET_OS_IOS) && !defined(__amigaos4__)
#define SDL_HAS_CAPTURE_AND_GLOBAL_MOUSE 1 // NOLINT(cppcoreguidelines-macro-usage)
#else
#define SDL_HAS_CAPTURE_AND_GLOBAL_MOUSE 0 // NOLINT(cppcoreguidelines-macro-usage)
#endif

using namespace osc;
namespace rgs = std::ranges;

template<>
struct osc::Converter<SDL_Rect, Rect> final {
Expand Down Expand Up @@ -256,6 +267,21 @@ namespace
log_info("user data directory: %s", rv.string().c_str());
return rv;
}

// Returns whether global (OS-level, rather than window-level) mouse data
// can be acquired from the OS.
bool can_mouse_use_global_state()
{
// Check and store if we are on a SDL backend that supports global mouse position
// ("wayland" and "rpi" don't support it, but we chose to use a white-list instead of a black-list)
bool mouse_can_use_global_state = false;
#if SDL_HAS_CAPTURE_AND_GLOBAL_MOUSE
const auto sdl_backend = std::string_view{SDL_GetCurrentVideoDriver()};
const auto global_mouse_whitelist = std::to_array<std::string_view>({"windows", "cocoa", "x11", "DIVE", "VMAN"});
mouse_can_use_global_state = rgs::any_of(global_mouse_whitelist, [sdl_backend](std::string_view whitelisted) { return sdl_backend.starts_with(whitelisted); });
#endif
return mouse_can_use_global_state;
}
}

namespace
Expand Down Expand Up @@ -687,6 +713,22 @@ class osc::App::Impl final {
return (SDL_GetWindowFlags(main_window_.get()) & SDL_WINDOW_MINIMIZED) != 0u;
}

bool can_query_mouse_state_globally() const
{
return can_query_mouse_state_globally_;
}

bool can_query_if_mouse_is_hovering_main_window_globally() const
{
// SDL on Linux/OSX doesn't report events for unfocused windows (see https://github.com/ocornut/imgui/issues/4960)
// We will use 'MouseCanReportHoveredViewport' to set 'ImGuiBackendFlags_HasMouseHoveredViewport' dynamically each frame.
#ifndef __APPLE__
return can_query_mouse_state_globally();
#else
return false;
#endif
}

void push_cursor_override(const Cursor& cursor)
{
cursor_handler_.push_cursor_override(cursor);
Expand Down Expand Up @@ -1061,6 +1103,9 @@ class osc::App::Impl final {
// application-wide handler for the mouse cursor
CursorHandler cursor_handler_;

// flag that indicates if the mouse state can be queried at a global (OS) level.
bool can_query_mouse_state_globally_ = can_mouse_use_global_state();

// get performance counter frequency (for the delta clocks)
Uint64 perf_counter_frequency_ = SDL_GetPerformanceFrequency();

Expand Down Expand Up @@ -1253,6 +1298,16 @@ bool osc::App::is_main_window_minimized() const
return impl_->is_main_window_minimized();
}

bool osc::App::can_query_mouse_state_globally() const
{
return impl_->can_query_mouse_state_globally();
}

bool osc::App::can_query_if_mouse_is_hovering_main_window_globally() const
{
return impl_->can_query_if_mouse_is_hovering_main_window_globally();
}

void osc::App::push_cursor_override(const Cursor& cursor)
{
impl_->push_cursor_override(cursor);
Expand Down
7 changes: 7 additions & 0 deletions src/oscar/Platform/App.h
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,13 @@ namespace osc
// returns `true` if the main application window is minimized
bool is_main_window_minimized() const;

// returns `true` if mouse data can be acquired from the operating system directly
bool can_query_mouse_state_globally() const;

// returns `true` if the global hover state of the mouse can be queried to ask if it's
// currently hovering the main window (even if the window isn't focused).
bool can_query_if_mouse_is_hovering_main_window_globally() const;

// pushes the given cursor onto the application-wide cursor stack, making it
// the currently-active cursor until it is either popped, via `pop_cursor_override`,
// or another cursor is pushed.
Expand Down
70 changes: 18 additions & 52 deletions src/oscar/UI/ui_context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@
#include <imgui_internal.h>
#include <implot.h>
#include <SDL3/SDL.h>
#if defined(__APPLE__)
#include <TargetConditionals.h>
#endif
#ifdef __EMSCRIPTEN__
#include <emscripten/em_js.h>
#endif
Expand All @@ -43,12 +40,6 @@
#include <string_view>
#include <utility>

#if SDL_VERSION_ATLEAST(2,0,4) && !defined(__EMSCRIPTEN__) && !defined(__ANDROID__) && !(defined(__APPLE__) && TARGET_OS_IOS) && !defined(__amigaos4__)
#define SDL_HAS_CAPTURE_AND_GLOBAL_MOUSE 1 // NOLINT(cppcoreguidelines-macro-usage)
#else
#define SDL_HAS_CAPTURE_AND_GLOBAL_MOUSE 0 // NOLINT(cppcoreguidelines-macro-usage)
#endif

using namespace osc;
namespace rgs = std::ranges;

Expand Down Expand Up @@ -76,34 +67,6 @@ struct osc::Converter<ImGuiMouseCursor, CursorShape> final {

namespace
{
// Returns whether global (OS-level, rather than window-level) mouse data
// can be acquired from the OS.
bool can_mouse_use_global_state()
{
// Check and store if we are on a SDL backend that supports global mouse position
// ("wayland" and "rpi" don't support it, but we chose to use a white-list instead of a black-list)
bool mouse_can_use_global_state = false;
#if SDL_HAS_CAPTURE_AND_GLOBAL_MOUSE
const auto sdl_backend = std::string_view{SDL_GetCurrentVideoDriver()};
const auto global_mouse_whitelist = std::to_array<std::string_view>({"windows", "cocoa", "x11", "DIVE", "VMAN"});
mouse_can_use_global_state = rgs::any_of(global_mouse_whitelist, [sdl_backend](std::string_view whitelisted) { return sdl_backend.starts_with(whitelisted); });
#endif
return mouse_can_use_global_state;
}

// Returns whether the global hover state of the mouse can be queried to ask if it's
// currently hovering a given UI viewport.
bool can_mouse_report_hovered_viewport(bool mouse_can_use_global_state)
{
// SDL on Linux/OSX doesn't report events for unfocused windows (see https://github.com/ocornut/imgui/issues/4960)
// We will use 'MouseCanReportHoveredViewport' to set 'ImGuiBackendFlags_HasMouseHoveredViewport' dynamically each frame.
#ifndef __APPLE__
return mouse_can_use_global_state;
#else
return false;
#endif
}

#ifndef EMSCRIPTEN
// this is necessary because ImGui will take ownership and be responsible for
// freeing the memory with `ImGui::MemFree`
Expand Down Expand Up @@ -153,9 +116,6 @@ namespace
std::optional<CursorShape> CurrentCustomCursor;
int MouseButtonsDown = 0;
int MouseLastLeaveFrame = 0;
bool MouseCanUseGlobalState = can_mouse_use_global_state();
// This is hard to use/unreliable on SDL so we'll set ImGuiBackendFlags_HasMouseHoveredViewport dynamically based on state.
bool MouseCanReportHoveredViewport = can_mouse_report_hovered_viewport(MouseCanUseGlobalState);
};

// Backend data stored in io.BackendPlatformUserData to allow support for multiple Dear ImGui contexts
Expand Down Expand Up @@ -494,26 +454,31 @@ namespace
// This code is incredibly messy because some of the functions we need for full viewport support are not available in SDL < 2.0.4.
void ImGui_ImplSDL2_UpdateMouseData()
{
const App& app = App::get();
BackendData* bd = try_get_ui_backend_data();
ImGuiIO& io = ImGui::GetIO();

// We forward mouse input when hovered or captured (via SDL_MOUSEMOTION) or when focused (below)
#if SDL_HAS_CAPTURE_AND_GLOBAL_MOUSE
// SDL_CaptureMouse() let the OS know e.g. that our imgui drag outside the SDL window boundaries shouldn't e.g. trigger other operations outside
SDL_CaptureMouse(bd->MouseButtonsDown != 0);
SDL_Window* focused_window = SDL_GetKeyboardFocus();
const bool is_app_focused = (focused_window != nullptr && (bd->Window == focused_window || ImGui::FindViewportByPlatformHandle(cpp20::bit_cast<void*>(focused_window)) != nullptr));
#else
SDL_Window* focused_window = bd->Window;
const bool is_app_focused = (SDL_GetWindowFlags(bd->Window) & SDL_WINDOW_INPUT_FOCUS) != 0; // SDL 2.0.3 and non-windowed systems: single-viewport only
#endif
SDL_Window* focused_window = nullptr;
bool is_app_focused = false;
if (app.can_query_mouse_state_globally()) {
// SDL_CaptureMouse() let the OS know e.g. that our imgui drag outside the SDL window boundaries shouldn't e.g. trigger other operations outside
SDL_CaptureMouse(bd->MouseButtonsDown != 0);

focused_window = SDL_GetKeyboardFocus();
is_app_focused = (focused_window != nullptr && (bd->Window == focused_window || ImGui::FindViewportByPlatformHandle(cpp20::bit_cast<void*>(focused_window)) != nullptr));
}
else {
focused_window = bd->Window;
is_app_focused = (SDL_GetWindowFlags(bd->Window) & SDL_WINDOW_INPUT_FOCUS) != 0; // SDL 2.0.3 and non-windowed systems: single-viewport only
}

if (is_app_focused) {

// (Optional) Set OS mouse position from Dear ImGui if requested (rarely used, only when ImGuiConfigFlags_NavEnableSetMousePos is enabled by user)
if (io.WantSetMousePos) {
const float scale = App::get().main_window_device_independent_to_os_ratio();
if (SDL_HAS_CAPTURE_AND_GLOBAL_MOUSE and (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)) {
if (app.can_query_mouse_state_globally() and (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)) {
SDL_WarpMouseGlobal(scale * io.MousePos.x, scale * io.MousePos.y);
}
else {
Expand All @@ -522,7 +487,7 @@ namespace
}

// (Optional) Fallback to provide mouse position when focused (SDL_MOUSEMOTION already provides this when hovered or captured)
if (bd->MouseCanUseGlobalState && bd->MouseButtonsDown == 0) {
if (app.can_query_mouse_state_globally() && bd->MouseButtonsDown == 0) {
// Single-viewport mode: mouse position in client window coordinates (io.MousePos is (0,0) when the mouse is on the upper-left corner of the app window)
// Multi-viewport mode: mouse position in OS absolute coordinates (io.MousePos is (0,0) when the mouse is on the upper-left of the primary monitor)
float mouse_x = 0;
Expand Down Expand Up @@ -629,7 +594,8 @@ namespace

// Our io.AddMouseViewportEvent() calls will only be valid when not capturing.
// Technically speaking testing for 'bd->MouseButtonsDown == 0' would be more rigorous, but testing for payload reduces noise and potential side effects.
if (bd.MouseCanReportHoveredViewport and ImGui::GetDragDropPayload() == nullptr) {
// This is hard to use/unreliable on SDL so we'll set ImGuiBackendFlags_HasMouseHoveredViewport dynamically based on state.
if (app.can_query_if_mouse_is_hovering_main_window_globally() and ImGui::GetDragDropPayload() == nullptr) {
io.BackendFlags |= ImGuiBackendFlags_HasMouseHoveredViewport;
}
else {
Expand Down

0 comments on commit 4e058fe

Please sign in to comment.