diff --git a/src/app/config.cpp b/src/app/config.cpp index 0bf147d..41777cc 100644 --- a/src/app/config.cpp +++ b/src/app/config.cpp @@ -135,7 +135,12 @@ void Config::load(std::unique_lock &lock) { exclude_processes_ = j.value("exclude_processes", std::vector{}); ignore_injected_ = j.value("ignore_injected", true); audio_backend_ = j.value("audio_backend", std::string("miniaudio")); - badge_spawn_strategy_ = j.value("badge_spawn_strategy", std::string("random_screen")); + auto strategy_in = j.value("badge_spawn_strategy", std::string("random_screen")); + if (strategy_in != "random_screen" && strategy_in != "near_caret") { + spdlog::warn("Unknown badge_spawn_strategy ({}); defaulting to random_screen", strategy_in); + strategy_in = "random_screen"; + } + badge_spawn_strategy_ = std::move(strategy_in); fps_mode_ = j.value("fps_mode", std::string("auto")); fps_fixed_ = clamp_nonneg(j.value("fps_fixed", 60), "fps_fixed"); if (fps_fixed_ <= 0) { diff --git a/src/app/main.cpp b/src/app/main.cpp index 12ad65a..1e0b473 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -252,14 +252,7 @@ int main(int argc, char **argv) { if (!muted.load()) { engine.play(); } - float bx = 0.0f; - float by = 0.0f; - if (cfg.badge_spawn_strategy() == "cursor_follow") { - auto [cx, cy] = lizard::platform::cursor_pos(); - bx = cx; - by = cy; - } - overlay.enqueue_spawn(bx, by); + overlay.enqueue_spawn(0.0f, 0.0f); } } }, diff --git a/src/overlay/overlay.cpp b/src/overlay/overlay.cpp index bdc7a37..65c8d71 100644 --- a/src/overlay/overlay.cpp +++ b/src/overlay/overlay.cpp @@ -43,6 +43,7 @@ void stbi_image_free(void *); #include #elif defined(__APPLE__) #include +#include #endif #ifndef LIZARD_TEST @@ -87,9 +88,244 @@ struct Badge { int sprite; }; +struct MonitorBounds { + float left; + float top; + float right; + float bottom; +}; + +#ifdef LIZARD_TEST +namespace test { + static std::optional> g_monitors_override; + static std::optional g_foreground_monitor_override; + + void set_monitors(std::vector monitors) { + g_monitors_override = std::move(monitors); + } + + void clear_monitors() { g_monitors_override.reset(); } + + void set_foreground_monitor(std::optional index) { + g_foreground_monitor_override = index; + } + + void clear_foreground_monitor() { g_foreground_monitor_override.reset(); } + + void reset_spawn_overrides() { + g_monitors_override.reset(); + g_foreground_monitor_override.reset(); + } +} // namespace test +#endif + +namespace { + +std::vector query_system_monitors() { + std::vector monitors; +#ifdef _WIN32 + EnumDisplayMonitors( + nullptr, nullptr, + [](HMONITOR, HDC, LPRECT rect, LPARAM param) -> BOOL { + auto *out = reinterpret_cast *>(param); + if (rect) { + out->push_back(MonitorBounds{static_cast(rect->left), static_cast(rect->top), + static_cast(rect->right), + static_cast(rect->bottom)}); + } + return TRUE; + }, + reinterpret_cast(&monitors)); +#elif defined(__APPLE__) + uint32_t count = 0; + if (CGGetActiveDisplayList(0, nullptr, &count) == kCGErrorSuccess && count > 0) { + std::vector displays(count); + if (CGGetActiveDisplayList(count, displays.data(), &count) == kCGErrorSuccess) { + for (uint32_t i = 0; i < count; ++i) { + CGRect bounds = CGDisplayBounds(displays[i]); + monitors.push_back(MonitorBounds{static_cast(bounds.origin.x), + static_cast(bounds.origin.y), + static_cast(bounds.origin.x + bounds.size.width), + static_cast(bounds.origin.y + bounds.size.height)}); + } + } + } +#elif defined(__linux__) + platform::init_xlib_threads(); + if (Display *dpy = XOpenDisplay(nullptr)) { + ::Window root = DefaultRootWindow(dpy); + int nmon = 0; + if (XRRMonitorInfo *info = XRRGetMonitors(dpy, root, True, &nmon)) { + for (int i = 0; i < nmon; ++i) { + monitors.push_back(MonitorBounds{static_cast(info[i].x), static_cast(info[i].y), + static_cast(info[i].x + info[i].width), + static_cast(info[i].y + info[i].height)}); + } + XRRFreeMonitors(info); + } else { + int screen = DefaultScreen(dpy); + monitors.push_back(MonitorBounds{0.0f, 0.0f, static_cast(DisplayWidth(dpy, screen)), + static_cast(DisplayHeight(dpy, screen))}); + } + XCloseDisplay(dpy); + } +#endif + return monitors; +} + +std::optional query_system_foreground_monitor() { +#ifdef _WIN32 + HWND foreground = GetForegroundWindow(); + if (!foreground) { + return std::nullopt; + } + HMONITOR monitor = MonitorFromWindow(foreground, MONITOR_DEFAULTTONULL); + if (!monitor) { + monitor = MonitorFromWindow(foreground, MONITOR_DEFAULTTOPRIMARY); + } + if (!monitor) { + return std::nullopt; + } + MONITORINFO info{}; + info.cbSize = sizeof(info); + if (!GetMonitorInfo(monitor, &info)) { + return std::nullopt; + } + RECT rc = info.rcMonitor; + return MonitorBounds{static_cast(rc.left), static_cast(rc.top), + static_cast(rc.right), static_cast(rc.bottom)}; +#elif defined(__APPLE__) + CFArrayRef list = CGWindowListCopyWindowInfo( + kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, kCGNullWindowID); + if (!list) { + return std::nullopt; + } + CGRect bounds{}; + bool found = false; + CFIndex count = CFArrayGetCount(list); + for (CFIndex i = 0; i < count && !found; ++i) { + auto dict = static_cast(CFArrayGetValueAtIndex(list, i)); + if (!dict) { + continue; + } + CFNumberRef layer = static_cast(CFDictionaryGetValue(dict, kCGWindowLayer)); + int layer_value = 0; + if (!layer || !CFNumberGetValue(layer, kCFNumberIntType, &layer_value) || layer_value != 0) { + continue; + } + auto bounds_dict = static_cast(CFDictionaryGetValue(dict, kCGWindowBounds)); + if (bounds_dict && CGRectMakeWithDictionaryRepresentation(bounds_dict, &bounds)) { + found = true; + } + } + CFRelease(list); + if (!found) { + return std::nullopt; + } + auto monitors = query_system_monitors(); + double center_x = bounds.origin.x + bounds.size.width * 0.5; + double center_y = bounds.origin.y + bounds.size.height * 0.5; + for (const auto &m : monitors) { + if (center_x >= m.left && center_x <= m.right && center_y >= m.top && center_y <= m.bottom) { + return m; + } + } + if (!monitors.empty()) { + return monitors.front(); + } + return std::nullopt; +#elif defined(__linux__) + platform::init_xlib_threads(); + Display *dpy = XOpenDisplay(nullptr); + if (!dpy) { + return std::nullopt; + } + ::Window root = DefaultRootWindow(dpy); + Atom active_atom = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False); + if (active_atom == None) { + XCloseDisplay(dpy); + return std::nullopt; + } + Atom actual_type = None; + int actual_format = 0; + unsigned long nitems = 0; + unsigned long bytes = 0; + unsigned char *data = nullptr; + if (XGetWindowProperty(dpy, root, active_atom, 0, ~0L, False, XA_WINDOW, &actual_type, &actual_format, + &nitems, &bytes, &data) != Success || !data || nitems == 0) { + if (data) { + XFree(data); + } + XCloseDisplay(dpy); + return std::nullopt; + } + ::Window active = reinterpret_cast<::Window *>(data)[0]; + XFree(data); + if (!active) { + XCloseDisplay(dpy); + return std::nullopt; + } + XWindowAttributes attrs{}; + if (!XGetWindowAttributes(dpy, active, &attrs)) { + XCloseDisplay(dpy); + return std::nullopt; + } + int abs_x = attrs.x; + int abs_y = attrs.y; + ::Window child; + if (XTranslateCoordinates(dpy, active, root, 0, 0, &abs_x, &abs_y, &child) == False) { + abs_x = attrs.x; + abs_y = attrs.y; + } + double center_x = abs_x + attrs.width * 0.5; + double center_y = abs_y + attrs.height * 0.5; + XCloseDisplay(dpy); + auto monitors = query_system_monitors(); + for (const auto &m : monitors) { + if (center_x >= m.left && center_x <= m.right && center_y >= m.top && center_y <= m.bottom) { + return m; + } + } + if (!monitors.empty()) { + return monitors.front(); + } + return std::nullopt; +#else + return std::nullopt; +#endif +} + +} // namespace + +static std::vector active_monitors() { +#ifdef LIZARD_TEST + if (test::g_monitors_override) { + return *test::g_monitors_override; + } +#endif + return query_system_monitors(); +} + +static std::optional foreground_monitor(const std::vector &monitors) { +#ifdef LIZARD_TEST + if (test::g_foreground_monitor_override) { + std::size_t idx = *test::g_foreground_monitor_override; + if (idx < monitors.size()) { + return monitors[idx]; + } + return std::nullopt; + } +#endif + auto monitor = query_system_foreground_monitor(); + if (monitor) { + return monitor; + } + return std::nullopt; +} + enum class BadgeSpawnStrategy { RandomScreen, - CursorFollow, + NearCaret, }; class Overlay { @@ -160,6 +396,8 @@ class Overlay { int m_badge_max_px = 108; float m_view_width = 1.0f; float m_view_height = 1.0f; + float m_virtual_origin_x = 0.0f; + float m_virtual_origin_y = 0.0f; std::vector m_instanceData; std::vector m_sprites; std::unordered_map m_sprite_lookup; @@ -234,8 +472,8 @@ void Overlay::update_frame_interval() { bool Overlay::init(const app::Config &cfg, std::optional emoji_path) { auto strategy = cfg.badge_spawn_strategy(); - if (strategy == "cursor_follow") { - m_spawn_strategy = BadgeSpawnStrategy::CursorFollow; + if (strategy == "near_caret") { + m_spawn_strategy = BadgeSpawnStrategy::NearCaret; } else { m_spawn_strategy = BadgeSpawnStrategy::RandomScreen; } @@ -318,6 +556,8 @@ bool Overlay::init(const app::Config &cfg, std::optional #endif m_view_width = static_cast(desc.width); m_view_height = static_cast(desc.height); + m_virtual_origin_x = static_cast(desc.x); + m_virtual_origin_y = static_cast(desc.y); m_window = platform::create_overlay_window(desc); if (!m_window.native) { return false; @@ -702,8 +942,8 @@ void Overlay::apply_pending_config() { m_sprites = std::move(atlas->sprites); m_current_emoji_path = std::move(atlas->normalized_path); } - if (pending.spawn_strategy == "cursor_follow") { - m_spawn_strategy = BadgeSpawnStrategy::CursorFollow; + if (pending.spawn_strategy == "near_caret") { + m_spawn_strategy = BadgeSpawnStrategy::NearCaret; } else { m_spawn_strategy = BadgeSpawnStrategy::RandomScreen; } @@ -793,12 +1033,91 @@ void Overlay::spawn_badge_locked(int sprite, float x, float y) { return; } - float px = x; - float py = y; + auto monitors = active_monitors(); + if (monitors.empty()) { + monitors.push_back(MonitorBounds{m_virtual_origin_x, m_virtual_origin_y, + m_virtual_origin_x + std::max(m_view_width, 1.0f), + m_virtual_origin_y + std::max(m_view_height, 1.0f)}); + } + + auto normalized_from_absolute = [&](float abs_x, float abs_y) { + float width = m_view_width > 0.0f ? m_view_width : 1.0f; + float height = m_view_height > 0.0f ? m_view_height : 1.0f; + float nx = (abs_x - m_virtual_origin_x) / width; + float ny = (abs_y - m_virtual_origin_y) / height; + nx = std::clamp(nx, 0.0f, 1.0f); + ny = std::clamp(ny, 0.0f, 1.0f); + return std::pair{nx, ny}; + }; + + auto sample_point_in_monitor = [&](const MonitorBounds &bounds) { + constexpr float inset = 24.0f; + float left = bounds.left + inset; + float right = bounds.right - inset; + float top = bounds.top + inset; + float bottom = bounds.bottom - inset; + if (right <= left) { + float mid = (bounds.left + bounds.right) * 0.5f; + left = right = mid; + } + if (bottom <= top) { + float mid = (bounds.top + bounds.bottom) * 0.5f; + top = bottom = mid; + } + float abs_x = left; + float abs_y = top; + if (right > left) { + std::uniform_real_distribution dist_x(left, right); + abs_x = dist_x(m_rng); + } + if (bottom > top) { + std::uniform_real_distribution dist_y(top, bottom); + abs_y = dist_y(m_rng); + } + return normalized_from_absolute(abs_x, abs_y); + }; + + float px = std::clamp(x, 0.0f, 1.0f); + float py = std::clamp(y, 0.0f, 1.0f); if (m_spawn_strategy == BadgeSpawnStrategy::RandomScreen) { - std::uniform_real_distribution dist(0.0f, 1.0f); - px = dist(m_rng); - py = dist(m_rng); + std::vector weights; + weights.reserve(monitors.size()); + constexpr float inset = 24.0f; + for (const auto &m : monitors) { + float width = std::max(0.0f, m.right - m.left); + float height = std::max(0.0f, m.bottom - m.top); + float usable_width = std::max(0.0f, width - inset * 2.0f); + float usable_height = std::max(0.0f, height - inset * 2.0f); + double area = static_cast(usable_width) * static_cast(usable_height); + if (area <= 0.0 && width > 0.0f && height > 0.0f) { + area = static_cast(width) * static_cast(height); + } + if (area <= 0.0) { + area = 1.0; + } + weights.push_back(area); + } + std::discrete_distribution monitor_dist(weights.begin(), weights.end()); + std::size_t idx = monitor_dist(m_rng); + auto sampled = sample_point_in_monitor(monitors[idx]); + px = sampled.first; + py = sampled.second; + } else if (m_spawn_strategy == BadgeSpawnStrategy::NearCaret) { + if (auto caret = lizard::platform::caret_pos()) { + auto norm = normalized_from_absolute(caret->first, caret->second); + px = norm.first; + py = norm.second; + } else { + auto fg = foreground_monitor(monitors); + if (!fg && !monitors.empty()) { + fg = monitors.front(); + } + if (fg) { + auto sampled = sample_point_in_monitor(*fg); + px = sampled.first; + py = sampled.second; + } + } } std::uniform_real_distribution angleDist(-0.3f, 0.3f); diff --git a/src/platform/linux/window.cpp b/src/platform/linux/window.cpp index b3ccff1..43549a3 100644 --- a/src/platform/linux/window.cpp +++ b/src/platform/linux/window.cpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace lizard::platform { @@ -178,6 +179,48 @@ std::pair cursor_pos() { return {x, y}; } +std::optional> caret_pos() { + std::lock_guard lock(g_display_mutex); + if (!g_display) { + return std::nullopt; + } + Atom active_atom = XInternAtom(g_display, "_NET_ACTIVE_WINDOW", False); + if (active_atom == None) { + return std::nullopt; + } + Atom actual_type = None; + int actual_format = 0; + unsigned long nitems = 0; + unsigned long bytes = 0; + unsigned char *data = nullptr; + if (XGetWindowProperty(g_display, g_root, active_atom, 0, ~0L, False, XA_WINDOW, &actual_type, + &actual_format, &nitems, &bytes, &data) != Success || !data || nitems == 0) { + if (data) { + XFree(data); + } + return std::nullopt; + } + ::Window active = reinterpret_cast<::Window *>(data)[0]; + XFree(data); + if (!active || active == g_overlay) { + return std::nullopt; + } + XWindowAttributes attrs{}; + if (!XGetWindowAttributes(g_display, active, &attrs)) { + return std::nullopt; + } + int abs_x = attrs.x; + int abs_y = attrs.y; + ::Window child; + if (XTranslateCoordinates(g_display, active, g_root, 0, 0, &abs_x, &abs_y, &child) == False) { + abs_x = attrs.x; + abs_y = attrs.y; + } + float center_x = abs_x + attrs.width * 0.5f; + float center_y = abs_y + attrs.height * 0.5f; + return std::pair{center_x, center_y}; +} + bool fullscreen_window_present() { std::lock_guard lock(g_display_mutex); if (!g_display) { diff --git a/src/platform/mac/window.mm b/src/platform/mac/window.mm index 41f91fb..6c8911c 100644 --- a/src/platform/mac/window.mm +++ b/src/platform/mac/window.mm @@ -5,6 +5,8 @@ #include #include #include +#include +#include namespace lizard::platform { @@ -104,6 +106,47 @@ void poll_events(Window &window) { } } +std::optional> caret_pos() { + @autoreleasepool { + AXUIElementRef systemWide = AXUIElementCreateSystemWide(); + if (!systemWide) { + return std::nullopt; + } + AXUIElementRef focused = nullptr; + if (AXUIElementCopyAttributeValue(systemWide, kAXFocusedUIElementAttribute, + reinterpret_cast(&focused)) != kAXErrorSuccess || + !focused) { + CFRelease(systemWide); + return std::nullopt; + } + CGPoint position{0, 0}; + CGSize size{0, 0}; + AXValueRef value = nullptr; + if (AXUIElementCopyAttributeValue(focused, kAXPositionAttribute, + reinterpret_cast(&value)) == kAXErrorSuccess && value && + AXValueGetType(value) == kAXValueCGPointType) { + AXValueGetValue(value, kAXValueCGPointType, &position); + } + if (value) { + CFRelease(value); + value = nullptr; + } + if (AXUIElementCopyAttributeValue(focused, kAXSizeAttribute, + reinterpret_cast(&value)) == kAXErrorSuccess && value && + AXValueGetType(value) == kAXValueCGSizeType) { + AXValueGetValue(value, kAXValueCGSizeType, &size); + } + if (value) { + CFRelease(value); + } + CFRelease(focused); + CFRelease(systemWide); + float x = position.x + static_cast(size.width) * 0.5f; + float y = position.y + static_cast(size.height) * 0.5f; + return std::pair{x, y}; + } +} + bool fullscreen_window_present() { @autoreleasepool { CFArrayRef list = CGWindowListCopyWindowInfo( diff --git a/src/platform/win/window.cpp b/src/platform/win/window.cpp index 8c767bf..55e9f52 100644 --- a/src/platform/win/window.cpp +++ b/src/platform/win/window.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #pragma comment(lib, "dwmapi.lib") namespace lizard::platform { @@ -119,6 +120,28 @@ std::pair cursor_pos() { return {x, y}; } +std::optional> caret_pos() { + GUITHREADINFO info{}; + info.cbSize = sizeof(info); + HWND foreground = GetForegroundWindow(); + DWORD thread_id = 0; + if (foreground) { + thread_id = GetWindowThreadProcessId(foreground, nullptr); + } + if (!GetGUIThreadInfo(thread_id, &info)) { + return std::nullopt; + } + HWND caret_hwnd = info.hwndCaret ? info.hwndCaret : info.hwndFocus; + if (!caret_hwnd) { + caret_hwnd = foreground; + } + POINT caret_pos{info.rcCaret.left, info.rcCaret.bottom}; + if (!caret_hwnd || !ClientToScreen(caret_hwnd, &caret_pos)) { + return std::nullopt; + } + return std::pair{static_cast(caret_pos.x), static_cast(caret_pos.y)}; +} + bool fullscreen_window_present() { struct EnumData { bool full = false; diff --git a/src/platform/window.hpp b/src/platform/window.hpp index 530f238..c682379 100644 --- a/src/platform/window.hpp +++ b/src/platform/window.hpp @@ -2,6 +2,7 @@ #include #include +#include #ifdef _WIN32 #include #elif defined(__linux__) @@ -38,6 +39,7 @@ void destroy_window(Window &window); void poll_events(Window &window); bool fullscreen_window_present(); std::pair cursor_pos(); +std::optional> caret_pos(); void make_context_current(Window &window); void clear_current_context(Window &window); void swap_buffers(Window &window); diff --git a/src/tests/overlay_tests.cpp b/src/tests/overlay_tests.cpp index 6927e5d..387ccfd 100644 --- a/src/tests/overlay_tests.cpp +++ b/src/tests/overlay_tests.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #define private public #include "app/config.h" @@ -36,6 +38,22 @@ std::pair cursor_pos() { return {0.0f, 0.0f}; } } // namespace lizard::platform #endif +namespace lizard::overlay::test { +void set_monitors(std::vector monitors); +void clear_monitors(); +void set_foreground_monitor(std::optional index); +void clear_foreground_monitor(); +void reset_spawn_overrides(); +} // namespace lizard::overlay::test + +namespace { +std::optional> g_test_caret; +} + +namespace lizard::platform { +std::optional> caret_pos() { return g_test_caret; } +} + struct OverlayTestAccess { static std::vector &sprites(lizard::overlay::Overlay &o) { return o.m_sprites; @@ -53,6 +71,26 @@ struct OverlayTestAccess { static std::vector &badges(lizard::overlay::Overlay &o) { return o.m_badges; } + static void set_view(lizard::overlay::Overlay &o, float width, float height, float origin_x, + float origin_y) { + o.m_view_width = width; + o.m_view_height = height; + o.m_virtual_origin_x = origin_x; + o.m_virtual_origin_y = origin_y; + } + static void set_monitors(std::vector monitors) { + lizard::overlay::test::set_monitors(std::move(monitors)); + } + static void clear_monitors() { lizard::overlay::test::clear_monitors(); } + static void set_foreground(std::optional index) { + lizard::overlay::test::set_foreground_monitor(index); + } + static void clear_foreground() { lizard::overlay::test::clear_foreground_monitor(); } + static void reset_overrides() { + lizard::overlay::test::reset_spawn_overrides(); + g_test_caret.reset(); + } + static void set_caret(std::optional> caret) { g_test_caret = caret; } }; bool g_overlay_log_called = false; @@ -182,29 +220,57 @@ TEST_CASE("GL resources released when Overlay is destroyed", "[overlay]") { } TEST_CASE("random_screen strategy randomizes badge position", "[overlay]") { + OverlayTestAccess::reset_overrides(); Config cfg(std::filesystem::temp_directory_path()); cfg.badge_spawn_strategy_ = "random_screen"; Overlay ov; ov.init(cfg); + OverlayTestAccess::set_view(ov, 1920.0f, 1080.0f, 0.0f, 0.0f); + OverlayTestAccess::set_monitors({lizard::overlay::MonitorBounds{0.0f, 0.0f, 1920.0f, 1080.0f}}); OverlayTestAccess::rng(ov).seed(1337); ov.spawn_badge(0, 0.0f, 0.0f); auto &b = OverlayTestAccess::badges(ov).back(); - REQUIRE(b.x == Approx(0.262025f)); - REQUIRE(b.y == Approx(0.56053f)); + REQUIRE(b.x == Approx(0.267974f)); + REQUIRE(b.y == Approx(0.55784f)); + OverlayTestAccess::reset_overrides(); +} + +TEST_CASE("near_caret strategy uses caret coordinates when available", "[overlay]") { + OverlayTestAccess::reset_overrides(); + Config cfg(std::filesystem::temp_directory_path()); + cfg.badge_spawn_strategy_ = "near_caret"; + Overlay ov; + ov.init(cfg); + OverlayTestAccess::set_view(ov, 1920.0f, 1080.0f, 0.0f, 0.0f); + OverlayTestAccess::set_monitors({lizard::overlay::MonitorBounds{0.0f, 0.0f, 1920.0f, 1080.0f}}); + OverlayTestAccess::set_caret(std::make_optional(std::pair{960.0f, 540.0f})); + ov.spawn_badge(0, 0.0f, 0.0f); + auto &b = OverlayTestAccess::badges(ov).back(); + REQUIRE(b.x == Approx(0.5f)); + REQUIRE(b.y == Approx(0.5f)); + OverlayTestAccess::reset_overrides(); } -TEST_CASE("cursor_follow strategy uses provided coordinates", "[overlay]") { +TEST_CASE("near_caret strategy falls back to foreground monitor", "[overlay]") { + OverlayTestAccess::reset_overrides(); Config cfg(std::filesystem::temp_directory_path()); - cfg.badge_spawn_strategy_ = "cursor_follow"; + cfg.badge_spawn_strategy_ = "near_caret"; Overlay ov; ov.init(cfg); - ov.spawn_badge(0, 0.25f, 0.75f); + OverlayTestAccess::set_view(ov, 3840.0f, 1080.0f, 0.0f, 0.0f); + OverlayTestAccess::set_monitors({lizard::overlay::MonitorBounds{0.0f, 0.0f, 1920.0f, 1080.0f}, + lizard::overlay::MonitorBounds{1920.0f, 0.0f, 3840.0f, 1080.0f}}); + OverlayTestAccess::set_foreground(1); + OverlayTestAccess::rng(ov).seed(1337); + ov.spawn_badge(0, 0.0f, 0.0f); auto &b = OverlayTestAccess::badges(ov).back(); - REQUIRE(b.x == Approx(0.25f)); - REQUIRE(b.y == Approx(0.75f)); + REQUIRE(b.x == Approx(0.633987f)); + REQUIRE(b.y == Approx(0.55784f)); + OverlayTestAccess::reset_overrides(); } TEST_CASE("badge spawns respect per-second limit", "[overlay]") { + OverlayTestAccess::reset_overrides(); Config cfg(std::filesystem::temp_directory_path()); cfg.badges_per_second_max_ = 2; Overlay ov; @@ -213,4 +279,5 @@ TEST_CASE("badge spawns respect per-second limit", "[overlay]") { ov.spawn_badge(0, 0.0f, 0.0f); ov.spawn_badge(0, 0.0f, 0.0f); REQUIRE(OverlayTestAccess::badges(ov).size() == 2); + OverlayTestAccess::reset_overrides(); }