#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr uint32_t kDefaultDpi = 96; constexpr uint32_t kDefaultCursorAutoHideMilliseconds = 1000; constexpr uint32_t kCursorAutoHideEvent = SDL_EVENT_USER + 1; // Enabled on macOS: without a high-DPI backing the Metal drawable stays at // logical point size and CoreAnimation upscales the whole window to the Retina // panel, blurring everything (the settings UI most visibly). Internal 3D render // cost is governed separately by the resolution-scale cvars, so this only makes // the final composite and UI render at native panel resolution. REXCVAR_DEFINE_BOOL(sdl_high_pixel_density, true, "UI/SDL", "Request high-DPI backing pixels for SDL windows") .lifecycle(rex::cvar::Lifecycle::kRequiresRestart); std::unordered_map& WindowMap() { static std::unordered_map windows; return windows; } Uint32 SDLCALL AutoHideCursorTimerCallback(void* userdata, SDL_TimerID timer_id, Uint32 interval) { (void)interval; SDL_Event event = {}; event.type = kCursorAutoHideEvent; event.user.data1 = userdata; event.user.data2 = reinterpret_cast(uintptr_t(timer_id)); SDL_PushEvent(&event); return 0; } uint32_t ResolveWindowWidth(uint32_t requested_width) { if (REXCVAR_GET(window_width) > 0) { return uint32_t(REXCVAR_GET(window_width)); } if (!rex::cvar::HasNonDefaultValue("window_width")) { if (rex::cvar::HasNonDefaultValue("video_mode_width") && REXCVAR_GET(video_mode_width) > 0) { return uint32_t(std::clamp(REXCVAR_GET(video_mode_width), 1, 8192)); } int32_t preset_width = 0; int32_t preset_height = 0; if (rex::graphics::video_mode_util::TryGetResolutionPresetFromCVar(preset_width, preset_height)) { return uint32_t(std::clamp(preset_width, 1, 8192)); } } return requested_width; } uint32_t ResolveWindowHeight(uint32_t requested_height) { if (REXCVAR_GET(window_height) > 0) { return uint32_t(REXCVAR_GET(window_height)); } if (!rex::cvar::HasNonDefaultValue("window_height")) { if (rex::cvar::HasNonDefaultValue("video_mode_height") && REXCVAR_GET(video_mode_height) > 0) { return uint32_t(std::clamp(REXCVAR_GET(video_mode_height), 1, 8192)); } int32_t preset_width = 0; int32_t preset_height = 0; if (rex::graphics::video_mode_util::TryGetResolutionPresetFromCVar(preset_width, preset_height)) { return uint32_t(std::clamp(preset_height, 1, 8192)); } } return requested_height; } using rex::ui::VirtualKey; VirtualKey TranslateSDLScancode(SDL_Scancode scancode) { switch (scancode) { case SDL_SCANCODE_F1: return VirtualKey::kF1; case SDL_SCANCODE_F2: return VirtualKey::kF2; case SDL_SCANCODE_F3: return VirtualKey::kF3; case SDL_SCANCODE_F4: return VirtualKey::kF4; case SDL_SCANCODE_F5: return VirtualKey::kF5; case SDL_SCANCODE_F6: return VirtualKey::kF6; case SDL_SCANCODE_F7: return VirtualKey::kF7; case SDL_SCANCODE_F8: return VirtualKey::kF8; case SDL_SCANCODE_F9: return VirtualKey::kF9; case SDL_SCANCODE_F10: return VirtualKey::kF10; case SDL_SCANCODE_F11: return VirtualKey::kF11; case SDL_SCANCODE_F12: return VirtualKey::kF12; case SDL_SCANCODE_ESCAPE: return VirtualKey::kEscape; case SDL_SCANCODE_RETURN: case SDL_SCANCODE_KP_ENTER: return VirtualKey::kReturn; case SDL_SCANCODE_SPACE: return VirtualKey::kSpace; case SDL_SCANCODE_TAB: return VirtualKey::kTab; case SDL_SCANCODE_BACKSPACE: return VirtualKey::kBack; case SDL_SCANCODE_DELETE: return VirtualKey::kDelete; case SDL_SCANCODE_INSERT: return VirtualKey::kInsert; case SDL_SCANCODE_HOME: return VirtualKey::kHome; case SDL_SCANCODE_END: return VirtualKey::kEnd; case SDL_SCANCODE_PAGEUP: return VirtualKey::kPrior; case SDL_SCANCODE_PAGEDOWN: return VirtualKey::kNext; case SDL_SCANCODE_LEFT: return VirtualKey::kLeft; case SDL_SCANCODE_RIGHT: return VirtualKey::kRight; case SDL_SCANCODE_UP: return VirtualKey::kUp; case SDL_SCANCODE_DOWN: return VirtualKey::kDown; case SDL_SCANCODE_LSHIFT: return VirtualKey::kLShift; case SDL_SCANCODE_RSHIFT: return VirtualKey::kRShift; case SDL_SCANCODE_LCTRL: return VirtualKey::kLControl; case SDL_SCANCODE_RCTRL: return VirtualKey::kRControl; case SDL_SCANCODE_LALT: return VirtualKey::kLMenu; case SDL_SCANCODE_RALT: return VirtualKey::kRMenu; case SDL_SCANCODE_LGUI: return VirtualKey::kLWin; case SDL_SCANCODE_RGUI: return VirtualKey::kRWin; case SDL_SCANCODE_APPLICATION: return VirtualKey::kApps; case SDL_SCANCODE_CAPSLOCK: return VirtualKey::kCapital; case SDL_SCANCODE_NUMLOCKCLEAR: return VirtualKey::kNumLock; case SDL_SCANCODE_SCROLLLOCK: return VirtualKey::kScroll; case SDL_SCANCODE_PRINTSCREEN: return VirtualKey::kSnapshot; case SDL_SCANCODE_PAUSE: return VirtualKey::kPause; case SDL_SCANCODE_0: return VirtualKey::k0; case SDL_SCANCODE_1: return VirtualKey::k1; case SDL_SCANCODE_2: return VirtualKey::k2; case SDL_SCANCODE_3: return VirtualKey::k3; case SDL_SCANCODE_4: return VirtualKey::k4; case SDL_SCANCODE_5: return VirtualKey::k5; case SDL_SCANCODE_6: return VirtualKey::k6; case SDL_SCANCODE_7: return VirtualKey::k7; case SDL_SCANCODE_8: return VirtualKey::k8; case SDL_SCANCODE_9: return VirtualKey::k9; case SDL_SCANCODE_A: return VirtualKey::kA; case SDL_SCANCODE_B: return VirtualKey::kB; case SDL_SCANCODE_C: return VirtualKey::kC; case SDL_SCANCODE_D: return VirtualKey::kD; case SDL_SCANCODE_E: return VirtualKey::kE; case SDL_SCANCODE_F: return VirtualKey::kF; case SDL_SCANCODE_G: return VirtualKey::kG; case SDL_SCANCODE_H: return VirtualKey::kH; case SDL_SCANCODE_I: return VirtualKey::kI; case SDL_SCANCODE_J: return VirtualKey::kJ; case SDL_SCANCODE_K: return VirtualKey::kK; case SDL_SCANCODE_L: return VirtualKey::kL; case SDL_SCANCODE_M: return VirtualKey::kM; case SDL_SCANCODE_N: return VirtualKey::kN; case SDL_SCANCODE_O: return VirtualKey::kO; case SDL_SCANCODE_P: return VirtualKey::kP; case SDL_SCANCODE_Q: return VirtualKey::kQ; case SDL_SCANCODE_R: return VirtualKey::kR; case SDL_SCANCODE_S: return VirtualKey::kS; case SDL_SCANCODE_T: return VirtualKey::kT; case SDL_SCANCODE_U: return VirtualKey::kU; case SDL_SCANCODE_V: return VirtualKey::kV; case SDL_SCANCODE_W: return VirtualKey::kW; case SDL_SCANCODE_X: return VirtualKey::kX; case SDL_SCANCODE_Y: return VirtualKey::kY; case SDL_SCANCODE_Z: return VirtualKey::kZ; case SDL_SCANCODE_KP_0: return VirtualKey::kNumpad0; case SDL_SCANCODE_KP_1: return VirtualKey::kNumpad1; case SDL_SCANCODE_KP_2: return VirtualKey::kNumpad2; case SDL_SCANCODE_KP_3: return VirtualKey::kNumpad3; case SDL_SCANCODE_KP_4: return VirtualKey::kNumpad4; case SDL_SCANCODE_KP_5: return VirtualKey::kNumpad5; case SDL_SCANCODE_KP_6: return VirtualKey::kNumpad6; case SDL_SCANCODE_KP_7: return VirtualKey::kNumpad7; case SDL_SCANCODE_KP_8: return VirtualKey::kNumpad8; case SDL_SCANCODE_KP_9: return VirtualKey::kNumpad9; case SDL_SCANCODE_KP_PLUS: return VirtualKey::kAdd; case SDL_SCANCODE_KP_MINUS: return VirtualKey::kSubtract; case SDL_SCANCODE_KP_MULTIPLY: return VirtualKey::kMultiply; case SDL_SCANCODE_KP_DIVIDE: return VirtualKey::kDivide; case SDL_SCANCODE_KP_DECIMAL: return VirtualKey::kDecimal; case SDL_SCANCODE_GRAVE: return VirtualKey::kOem3; case SDL_SCANCODE_MINUS: return VirtualKey::kOemMinus; case SDL_SCANCODE_EQUALS: return VirtualKey::kOemPlus; case SDL_SCANCODE_COMMA: return VirtualKey::kOemComma; case SDL_SCANCODE_PERIOD: return VirtualKey::kOemPeriod; case SDL_SCANCODE_SEMICOLON: return VirtualKey::kOem1; case SDL_SCANCODE_SLASH: return VirtualKey::kOem2; case SDL_SCANCODE_BACKSLASH: return VirtualKey::kOem5; case SDL_SCANCODE_LEFTBRACKET: return VirtualKey::kOem4; case SDL_SCANCODE_RIGHTBRACKET: return VirtualKey::kOem6; case SDL_SCANCODE_APOSTROPHE: return VirtualKey::kOem7; default: return VirtualKey::kNone; } } rex::ui::MouseEvent::Button TranslateMouseButton(uint8_t button) { switch (button) { case SDL_BUTTON_LEFT: return rex::ui::MouseEvent::Button::kLeft; case SDL_BUTTON_RIGHT: return rex::ui::MouseEvent::Button::kRight; case SDL_BUTTON_MIDDLE: return rex::ui::MouseEvent::Button::kMiddle; case SDL_BUTTON_X1: return rex::ui::MouseEvent::Button::kX1; case SDL_BUTTON_X2: return rex::ui::MouseEvent::Button::kX2; default: return rex::ui::MouseEvent::Button::kNone; } } } // namespace namespace rex { namespace ui { std::unique_ptr Window::Create(WindowedAppContext& app_context, const std::string_view title, uint32_t desired_logical_width, uint32_t desired_logical_height) { desired_logical_width = ResolveWindowWidth(desired_logical_width); desired_logical_height = ResolveWindowHeight(desired_logical_height); return std::make_unique(app_context, title, desired_logical_width, desired_logical_height); } SDLWindow::SDLWindow(WindowedAppContext& app_context, const std::string_view title, uint32_t desired_logical_width, uint32_t desired_logical_height) : Window(app_context, title, desired_logical_width, desired_logical_height) {} SDLWindow::~SDLWindow() { EnterDestructor(); RemoveCursorAutoHideTimer(); if (window_) { WindowMap().erase(SDL_GetWindowID(window_)); } if (metal_view_) { SDL_Metal_DestroyView(metal_view_); metal_view_ = nullptr; metal_layer_ = nullptr; } if (window_) { SDL_DestroyWindow(window_); window_ = nullptr; } } void SDLWindow::HandleSDLEvent(const SDL_Event& event) { SDL_WindowID window_id = 0; switch (event.type) { case SDL_EVENT_WINDOW_SHOWN: case SDL_EVENT_WINDOW_HIDDEN: case SDL_EVENT_WINDOW_EXPOSED: case SDL_EVENT_WINDOW_MOVED: case SDL_EVENT_WINDOW_RESIZED: case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: case SDL_EVENT_WINDOW_METAL_VIEW_RESIZED: case SDL_EVENT_WINDOW_FOCUS_GAINED: case SDL_EVENT_WINDOW_FOCUS_LOST: case SDL_EVENT_WINDOW_CLOSE_REQUESTED: case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED: case SDL_EVENT_WINDOW_ENTER_FULLSCREEN: case SDL_EVENT_WINDOW_LEAVE_FULLSCREEN: window_id = event.window.windowID; break; case kCursorAutoHideEvent: window_id = SDL_WindowID(reinterpret_cast(event.user.data1)); break; case SDL_EVENT_KEY_DOWN: case SDL_EVENT_KEY_UP: window_id = event.key.windowID; break; case SDL_EVENT_TEXT_INPUT: window_id = event.text.windowID; break; case SDL_EVENT_MOUSE_BUTTON_DOWN: case SDL_EVENT_MOUSE_BUTTON_UP: window_id = event.button.windowID; break; case SDL_EVENT_MOUSE_MOTION: window_id = event.motion.windowID; break; case SDL_EVENT_MOUSE_WHEEL: window_id = event.wheel.windowID; break; default: return; } auto it = WindowMap().find(window_id); if (it != WindowMap().end()) { it->second->HandleEvent(event); } } uint32_t SDLWindow::GetLatestDpiImpl() const { return dpi_ ? dpi_ : kDefaultDpi; } bool SDLWindow::OpenImpl() { SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1"); SDL_WindowFlags flags = SDL_WINDOW_RESIZABLE; if (REXCVAR_GET(sdl_high_pixel_density)) { flags |= SDL_WINDOW_HIGH_PIXEL_DENSITY; } if (IsFullscreen()) { flags |= SDL_WINDOW_FULLSCREEN; } window_ = SDL_CreateWindow(GetTitle().c_str(), int(GetDesiredLogicalWidth()), int(GetDesiredLogicalHeight()), flags); if (!window_) { REXLOG_ERROR("SDLWindow: Failed to create window: {}", SDL_GetError()); return false; } WindowMap().emplace(SDL_GetWindowID(window_), this); metal_view_ = SDL_Metal_CreateView(window_); if (!metal_view_) { REXLOG_ERROR("SDLWindow: Failed to create Metal view: {}", SDL_GetError()); WindowMap().erase(SDL_GetWindowID(window_)); SDL_DestroyWindow(window_); window_ = nullptr; return false; } metal_layer_ = SDL_Metal_GetLayer(metal_view_); if (!metal_layer_) { REXLOG_ERROR("SDLWindow: Failed to get Metal layer: {}", SDL_GetError()); SDL_Metal_DestroyView(metal_view_); metal_view_ = nullptr; WindowMap().erase(SDL_GetWindowID(window_)); SDL_DestroyWindow(window_); window_ = nullptr; return false; } dpi_ = QueryDpi(); SDL_ShowWindow(window_); if (IsMouseCaptureRequested()) { SDL_SetWindowMouseGrab(window_, true); } ApplyNewCursorVisibility(CursorVisibility::kVisible); WindowDestructionReceiver destruction_receiver(this); HandleSizeUpdate(destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrClosed()) { return true; } OnFocusUpdate((SDL_GetWindowFlags(window_) & SDL_WINDOW_INPUT_FOCUS) != 0, destruction_receiver); return true; } void SDLWindow::RequestCloseImpl() { WindowDestructionReceiver destruction_receiver(this); OnBeforeClose(destruction_receiver); if (!destruction_receiver.IsWindowDestroyed()) { RemoveCursorAutoHideTimer(); if (metal_view_) { SDL_Metal_DestroyView(metal_view_); metal_view_ = nullptr; metal_layer_ = nullptr; } if (window_) { WindowMap().erase(SDL_GetWindowID(window_)); SDL_DestroyWindow(window_); window_ = nullptr; } OnAfterClose(); } } void SDLWindow::ApplyNewFullscreen() { if (window_) { SDL_SetWindowFullscreen(window_, IsFullscreen()); } } void SDLWindow::ApplyNewTitle() { if (window_) { SDL_SetWindowTitle(window_, GetTitle().c_str()); } } void SDLWindow::ApplyNewMouseCapture() { if (window_) { SDL_SetWindowMouseGrab(window_, true); } } void SDLWindow::ApplyNewMouseRelease() { if (window_) { SDL_SetWindowMouseGrab(window_, false); } } void SDLWindow::ApplyNewCursorVisibility(CursorVisibility old_cursor_visibility) { (void)old_cursor_visibility; RemoveCursorAutoHideTimer(); switch (GetCursorVisibility()) { case CursorVisibility::kVisible: cursor_currently_auto_hidden_ = false; SDL_ShowCursor(); break; case CursorVisibility::kAutoHidden: cursor_currently_auto_hidden_ = true; SDL_HideCursor(); break; case CursorVisibility::kHidden: cursor_currently_auto_hidden_ = true; SDL_HideCursor(); break; } } void SDLWindow::FocusImpl() { if (window_) { SDL_RaiseWindow(window_); } } std::unique_ptr SDLWindow::CreateSurfaceImpl(Surface::TypeFlags allowed_types) { if (!(allowed_types & Surface::kTypeFlag_SDLMetalView)) { return nullptr; } return std::make_unique(window_, metal_view_, metal_layer_); } void SDLWindow::RequestPaintImpl() { bool expected = false; if (!paint_event_queued_.compare_exchange_strong(expected, true, std::memory_order_acq_rel, std::memory_order_acquire)) { return; } SDL_Event event = {}; event.type = SDL_EVENT_WINDOW_EXPOSED; event.window.windowID = SDL_GetWindowID(window_); if (!SDL_PushEvent(&event)) { paint_event_queued_.store(false, std::memory_order_release); } } void SDLWindow::HandleEvent(const SDL_Event& event) { WindowDestructionReceiver destruction_receiver(this); switch (event.type) { case SDL_EVENT_WINDOW_EXPOSED: paint_event_queued_.store(false, std::memory_order_release); OnPaint(); break; case SDL_EVENT_WINDOW_RESIZED: case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: case SDL_EVENT_WINDOW_METAL_VIEW_RESIZED: case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED: HandleSizeUpdate(destruction_receiver); break; case SDL_EVENT_WINDOW_FOCUS_GAINED: OnFocusUpdate(true, destruction_receiver); break; case SDL_EVENT_WINDOW_FOCUS_LOST: OnFocusUpdate(false, destruction_receiver); break; case SDL_EVENT_WINDOW_ENTER_FULLSCREEN: OnDesiredFullscreenUpdate(true); break; case SDL_EVENT_WINDOW_LEAVE_FULLSCREEN: OnDesiredFullscreenUpdate(false); break; case SDL_EVENT_WINDOW_CLOSE_REQUESTED: #if REX_PLATFORM_MAC app_context().QuitFromUIThread(); #else RequestCloseImpl(); app_context().RequestDeferredQuit(); #endif break; case kCursorAutoHideEvent: if (GetCursorVisibility() == CursorVisibility::kAutoHidden && SDL_TimerID(reinterpret_cast(event.user.data2)) == cursor_auto_hide_timer_) { cursor_auto_hide_timer_ = 0; cursor_currently_auto_hidden_ = true; SDL_HideCursor(); } break; case SDL_EVENT_KEY_DOWN: HandleKey(event.key, true, destruction_receiver); break; case SDL_EVENT_KEY_UP: HandleKey(event.key, false, destruction_receiver); break; case SDL_EVENT_TEXT_INPUT: HandleTextInput(event.text, destruction_receiver); break; case SDL_EVENT_MOUSE_BUTTON_DOWN: HandleMouseButton(event.button, true, destruction_receiver); break; case SDL_EVENT_MOUSE_BUTTON_UP: HandleMouseButton(event.button, false, destruction_receiver); break; case SDL_EVENT_MOUSE_MOTION: HandleMouseMotion(event.motion, destruction_receiver); break; case SDL_EVENT_MOUSE_WHEEL: HandleMouseWheel(event.wheel, destruction_receiver); break; default: break; } } void SDLWindow::HandleSizeUpdate(WindowDestructionReceiver& destruction_receiver) { dpi_ = QueryDpi(); int width = 0; int height = 0; if (!SDL_GetWindowSizeInPixels(window_, &width, &height)) { return; } if (!IsFullscreen()) { int logical_width = 0; int logical_height = 0; if (SDL_GetWindowSize(window_, &logical_width, &logical_height)) { OnDesiredLogicalSizeUpdate(uint32_t(std::max(logical_width, 0)), uint32_t(std::max(logical_height, 0))); } } OnActualSizeUpdate(uint32_t(std::max(width, 0)), uint32_t(std::max(height, 0)), destruction_receiver); } void SDLWindow::HandleKey(const SDL_KeyboardEvent& event, bool down, WindowDestructionReceiver& destruction_receiver) { SDL_Keymod mod = SDL_GetModState(); KeyEvent e(this, TranslateSDLScancode(event.scancode), event.repeat ? 2 : 1, !down, (mod & SDL_KMOD_SHIFT) != 0, (mod & SDL_KMOD_CTRL) != 0, (mod & SDL_KMOD_ALT) != 0, (mod & SDL_KMOD_GUI) != 0); if (down) { OnKeyDown(e, destruction_receiver); } else { OnKeyUp(e, destruction_receiver); } } void SDLWindow::HandleMouseButton(const SDL_MouseButtonEvent& event, bool down, WindowDestructionReceiver& destruction_receiver) { RevealAutoHiddenCursor(); int32_t physical_x = 0; int32_t physical_y = 0; WindowPointToPhysical(event.x, event.y, physical_x, physical_y); MouseEvent e(this, TranslateMouseButton(event.button), physical_x, physical_y); if (down) { OnMouseDown(e, destruction_receiver); } else { OnMouseUp(e, destruction_receiver); } } void SDLWindow::HandleMouseMotion(const SDL_MouseMotionEvent& event, WindowDestructionReceiver& destruction_receiver) { RevealAutoHiddenCursor(); int32_t physical_x = 0; int32_t physical_y = 0; WindowPointToPhysical(event.x, event.y, physical_x, physical_y); MouseEvent e(this, MouseEvent::Button::kNone, physical_x, physical_y); OnMouseMove(e, destruction_receiver); } void SDLWindow::HandleTextInput(const SDL_TextInputEvent& event, WindowDestructionReceiver& destruction_receiver) { if (!event.text) { return; } SDL_Keymod mod = SDL_GetModState(); const uint8_t* s = reinterpret_cast(event.text); while (*s) { // Decode one UTF-8 codepoint. uint32_t codepoint = 0; uint32_t continuation_count = 0; uint8_t lead = *s++; if (lead < 0x80) { codepoint = lead; } else if ((lead & 0xE0) == 0xC0) { codepoint = lead & 0x1F; continuation_count = 1; } else if ((lead & 0xF0) == 0xE0) { codepoint = lead & 0x0F; continuation_count = 2; } else if ((lead & 0xF8) == 0xF0) { codepoint = lead & 0x07; continuation_count = 3; } else { // Stray continuation or invalid lead byte - skip. continue; } bool valid = true; for (uint32_t i = 0; i < continuation_count; ++i) { uint8_t continuation = *s; if ((continuation & 0xC0) != 0x80) { valid = false; break; } codepoint = (codepoint << 6) | (continuation & 0x3F); ++s; } if (!valid || !codepoint) { continue; } // Mirrors the Win32 WM_CHAR path - the character is carried in the // KeyEvent's virtual key field. KeyEvent e(this, VirtualKey(codepoint), 1, false, (mod & SDL_KMOD_SHIFT) != 0, (mod & SDL_KMOD_CTRL) != 0, (mod & SDL_KMOD_ALT) != 0, (mod & SDL_KMOD_GUI) != 0); OnKeyChar(e, destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrClosed()) { return; } } } void SDLWindow::SetTextInputActive(bool active) { if (!window_) { return; } if (active) { SDL_StartTextInput(window_); } else { SDL_StopTextInput(window_); } } void SDLWindow::HandleMouseWheel(const SDL_MouseWheelEvent& event, WindowDestructionReceiver& destruction_receiver) { RevealAutoHiddenCursor(); int32_t physical_x = 0; int32_t physical_y = 0; WindowPointToPhysical(event.mouse_x, event.mouse_y, physical_x, physical_y); MouseEvent e(this, MouseEvent::Button::kNone, physical_x, physical_y, int32_t(std::lround(event.x * double(MouseEvent::kScrollPerDetent))), int32_t(std::lround(event.y * double(MouseEvent::kScrollPerDetent)))); OnMouseWheel(e, destruction_receiver); } void SDLWindow::RevealAutoHiddenCursor() { if (GetCursorVisibility() != CursorVisibility::kAutoHidden) { return; } if (cursor_currently_auto_hidden_) { cursor_currently_auto_hidden_ = false; SDL_ShowCursor(); } SetCursorAutoHideTimer(); } void SDLWindow::SetCursorAutoHideTimer() { RemoveCursorAutoHideTimer(); if (!window_) { return; } cursor_auto_hide_timer_ = SDL_AddTimer(kDefaultCursorAutoHideMilliseconds, AutoHideCursorTimerCallback, reinterpret_cast(uintptr_t(SDL_GetWindowID(window_)))); } void SDLWindow::RemoveCursorAutoHideTimer() { if (!cursor_auto_hide_timer_) { return; } SDL_RemoveTimer(cursor_auto_hide_timer_); cursor_auto_hide_timer_ = 0; } void SDLWindow::WindowPointToPhysical(float x, float y, int32_t& physical_x, int32_t& physical_y) const { int logical_width = 0; int logical_height = 0; int physical_width = 0; int physical_height = 0; if (window_ && SDL_GetWindowSize(window_, &logical_width, &logical_height) && SDL_GetWindowSizeInPixels(window_, &physical_width, &physical_height) && logical_width > 0 && logical_height > 0 && physical_width > 0 && physical_height > 0) { physical_x = int32_t(std::lround(double(x) * double(physical_width) / double(logical_width))); physical_y = int32_t(std::lround(double(y) * double(physical_height) / double(logical_height))); return; } physical_x = int32_t(std::lround(x)); physical_y = int32_t(std::lround(y)); } uint32_t SDLWindow::QueryDpi() const { if (!window_) { return kDefaultDpi; } float scale = SDL_GetWindowDisplayScale(window_); if (scale <= 0.0f) { scale = 1.0f; } return std::clamp(uint32_t(std::lround(double(kDefaultDpi) * scale)), uint32_t(48), uint32_t(384)); } std::unique_ptr MenuItem::Create(Type type, const std::string& text, const std::string& hotkey, std::function callback) { return std::make_unique(type, text, hotkey, std::move(callback)); } } // namespace ui } // namespace rex