rexglue-skate3/src/ui/window_sdl.cpp

816 lines
25 KiB
C++

#include <rex/ui/window_sdl.h>
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <rex/assert.h>
#include <rex/cvar.h>
#include <rex/graphics/flags.h>
#include <rex/graphics/video_mode_util.h>
#include <rex/logging.h>
#include <rex/platform.h>
#include <rex/ui/surface_sdl.h>
#include <rex/ui/virtual_key.h>
#include <SDL3/SDL_hints.h>
#include <SDL3/SDL_keyboard.h>
#include <SDL3/SDL_mouse.h>
#include <SDL3/SDL_scancode.h>
#include <SDL3/SDL_timer.h>
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<SDL_WindowID, rex::ui::SDLWindow*>& WindowMap() {
static std::unordered_map<SDL_WindowID, rex::ui::SDLWindow*> 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<void*>(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> 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<SDLWindow>(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<uintptr_t>(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<Surface> SDLWindow::CreateSurfaceImpl(Surface::TypeFlags allowed_types) {
if (!(allowed_types & Surface::kTypeFlag_SDLMetalView)) {
return nullptr;
}
return std::make_unique<SDLMetalViewSurface>(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<uintptr_t>(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<const uint8_t*>(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<void*>(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<ui::MenuItem> MenuItem::Create(Type type, const std::string& text,
const std::string& hotkey,
std::function<void()> callback) {
return std::make_unique<SDLMenuItem>(type, text, hotkey, std::move(callback));
}
} // namespace ui
} // namespace rex