/
Mr.Stalin
/
FOnline-Engine
Обзор
Документация
Войти
/
Mr.Stalin
/
FOnline-Engine
Код
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
Source/Frontend/Application.cpp
3 655 строк
129 KB
cvet
Alloc fixes (#192)
27 июл 2026, 14:30
Не верифицирован
27 июл 2026, 14:30
ba3760e
Код
Авторство
О чём код?
// __________ ___ ______ _ // / ____/ __ \____ / (_)___ ___ / ____/___ ____ _(_)___ ___ // / /_ / / / / __ \/ / / __ \/ _ \ / __/ / __ \/ __ `/ / __ \/ _ ` // / __/ / /_/ / / / / / / / / / __/ / /___/ / / / /_/ / / / / / __/ // /_/ \____/_/ /_/_/_/_/ /_/\___/ /_____/_/ /_/\__, /_/_/ /_/\___/ // /____/ // FOnline Engine // https://fonline.ru // https://github.com/cvet/fonline // // MIT License // // Copyright (c) 2006 - 2026, Anton Tsvetinskiy aka cvet <cvet@tut.by> // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // #include "Application.h" #include "ConfigFile.h" #include "ImGuiStuff.h" #include "WebRelated.h" #include "SDL3/SDL.h" #include "SDL3/SDL_audio.h" #include "SDL3/SDL_events.h" #include "SDL3/SDL_video.h" FO_BEGIN_NAMESPACE static ImGuiKey KeycodeToImGuiKey(SDL_Keycode keycode); static auto MakeInputKeyMap() -> unordered_map<SDL_Keycode, KeyCode>; static auto MakeMouseButtonMap() -> unordered_map<int32_t, MouseButton>; struct Application::Context { unique_nptr<Renderer> ActiveRenderer {}; RenderType ActiveRendererType {}; nptr<RenderTexture> RenderTargetTex {}; ucolor ClearColor {150, 150, 150, 255}; vector<unique_ptr<HeadlessWindowStub>> NullWindowStubs {}; vector<InputEvent> EventsQueue {}; vector<InputEvent> NextFrameEventsQueue {}; unique_del_nptr<SDL_AudioStream> AudioStream {}; SDL_AudioSpec AudioSpec {}; AppAudio::AudioStreamCallback AudioStreamWriter {}; vector<uint8_t> AudioStreamBuf {}; unordered_map<SDL_Keycode, KeyCode> KeysMap {MakeInputKeyMap()}; unordered_map<int32_t, MouseButton> MouseButtonsMap {MakeMouseButtonMap()}; }; int32_t AppRender::MAX_ATLAS_WIDTH {}; int32_t AppRender::MAX_ATLAS_HEIGHT {}; int32_t AppRender::MAX_BONES {}; const int32_t AppAudio::AUDIO_FORMAT_U8 {SDL_AUDIO_U8}; const int32_t AppAudio::AUDIO_FORMAT_S16 {SDL_AUDIO_S16}; static constexpr float32_t GAMEPAD_STICK_DEADZONE = 0.2f; static constexpr float32_t GAMEPAD_TRIGGER_DEADZONE = 0.15f; static auto GetActiveRenderer(auto&& ctx) { FO_STACK_TRACE_ENTRY(); FO_VERIFY_AND_THROW(ctx->ActiveRenderer, "No active renderer is selected"); return ctx->ActiveRenderer.as_ptr(); } static auto MakeInputKeyMap() -> unordered_map<SDL_Keycode, KeyCode> { FO_STACK_TRACE_ENTRY(); return { {SDL_SCANCODE_ESCAPE, KeyCode::Escape}, {SDL_SCANCODE_1, KeyCode::C1}, {SDL_SCANCODE_2, KeyCode::C2}, {SDL_SCANCODE_3, KeyCode::C3}, {SDL_SCANCODE_4, KeyCode::C4}, {SDL_SCANCODE_5, KeyCode::C5}, {SDL_SCANCODE_6, KeyCode::C6}, {SDL_SCANCODE_7, KeyCode::C7}, {SDL_SCANCODE_8, KeyCode::C8}, {SDL_SCANCODE_9, KeyCode::C9}, {SDL_SCANCODE_0, KeyCode::C0}, {SDL_SCANCODE_MINUS, KeyCode::Minus}, {SDL_SCANCODE_EQUALS, KeyCode::Equals}, {SDL_SCANCODE_BACKSPACE, KeyCode::Back}, {SDL_SCANCODE_TAB, KeyCode::Tab}, {SDL_SCANCODE_Q, KeyCode::Q}, {SDL_SCANCODE_W, KeyCode::W}, {SDL_SCANCODE_E, KeyCode::E}, {SDL_SCANCODE_R, KeyCode::R}, {SDL_SCANCODE_T, KeyCode::T}, {SDL_SCANCODE_Y, KeyCode::Y}, {SDL_SCANCODE_U, KeyCode::U}, {SDL_SCANCODE_I, KeyCode::I}, {SDL_SCANCODE_O, KeyCode::O}, {SDL_SCANCODE_P, KeyCode::P}, {SDL_SCANCODE_LEFTBRACKET, KeyCode::Lbracket}, {SDL_SCANCODE_RIGHTBRACKET, KeyCode::Rbracket}, {SDL_SCANCODE_RETURN, KeyCode::Return}, {SDL_SCANCODE_LCTRL, KeyCode::Lcontrol}, {SDL_SCANCODE_A, KeyCode::A}, {SDL_SCANCODE_S, KeyCode::S}, {SDL_SCANCODE_D, KeyCode::D}, {SDL_SCANCODE_F, KeyCode::F}, {SDL_SCANCODE_G, KeyCode::G}, {SDL_SCANCODE_H, KeyCode::H}, {SDL_SCANCODE_J, KeyCode::J}, {SDL_SCANCODE_K, KeyCode::K}, {SDL_SCANCODE_L, KeyCode::L}, {SDL_SCANCODE_SEMICOLON, KeyCode::Semicolon}, {SDL_SCANCODE_APOSTROPHE, KeyCode::Apostrophe}, {SDL_SCANCODE_GRAVE, KeyCode::Grave}, {SDL_SCANCODE_LSHIFT, KeyCode::Lshift}, {SDL_SCANCODE_BACKSLASH, KeyCode::Backslash}, {SDL_SCANCODE_Z, KeyCode::Z}, {SDL_SCANCODE_X, KeyCode::X}, {SDL_SCANCODE_C, KeyCode::C}, {SDL_SCANCODE_V, KeyCode::V}, {SDL_SCANCODE_B, KeyCode::B}, {SDL_SCANCODE_N, KeyCode::N}, {SDL_SCANCODE_M, KeyCode::M}, {SDL_SCANCODE_COMMA, KeyCode::Comma}, {SDL_SCANCODE_PERIOD, KeyCode::Period}, {SDL_SCANCODE_SLASH, KeyCode::Slash}, {SDL_SCANCODE_RSHIFT, KeyCode::Rshift}, {SDL_SCANCODE_KP_MULTIPLY, KeyCode::Multiply}, {SDL_SCANCODE_LALT, KeyCode::Lmenu}, {SDL_SCANCODE_SPACE, KeyCode::Space}, {SDL_SCANCODE_CAPSLOCK, KeyCode::Capital}, {SDL_SCANCODE_F1, KeyCode::F1}, {SDL_SCANCODE_F2, KeyCode::F2}, {SDL_SCANCODE_F3, KeyCode::F3}, {SDL_SCANCODE_F4, KeyCode::F4}, {SDL_SCANCODE_F5, KeyCode::F5}, {SDL_SCANCODE_F6, KeyCode::F6}, {SDL_SCANCODE_F7, KeyCode::F7}, {SDL_SCANCODE_F8, KeyCode::F8}, {SDL_SCANCODE_F9, KeyCode::F9}, {SDL_SCANCODE_F10, KeyCode::F10}, {SDL_SCANCODE_NUMLOCKCLEAR, KeyCode::Numlock}, {SDL_SCANCODE_SCROLLLOCK, KeyCode::Scroll}, {SDL_SCANCODE_KP_7, KeyCode::Numpad7}, {SDL_SCANCODE_KP_8, KeyCode::Numpad8}, {SDL_SCANCODE_KP_9, KeyCode::Numpad9}, {SDL_SCANCODE_KP_MINUS, KeyCode::Subtract}, {SDL_SCANCODE_KP_4, KeyCode::Numpad4}, {SDL_SCANCODE_KP_5, KeyCode::Numpad5}, {SDL_SCANCODE_KP_6, KeyCode::Numpad6}, {SDL_SCANCODE_KP_PLUS, KeyCode::Add}, {SDL_SCANCODE_KP_1, KeyCode::Numpad1}, {SDL_SCANCODE_KP_2, KeyCode::Numpad2}, {SDL_SCANCODE_KP_3, KeyCode::Numpad3}, {SDL_SCANCODE_KP_0, KeyCode::Numpad0}, {SDL_SCANCODE_KP_PERIOD, KeyCode::Decimal}, {SDL_SCANCODE_F11, KeyCode::F11}, {SDL_SCANCODE_F12, KeyCode::F12}, {SDL_SCANCODE_KP_ENTER, KeyCode::Numpadenter}, {SDL_SCANCODE_RCTRL, KeyCode::Rcontrol}, {SDL_SCANCODE_KP_DIVIDE, KeyCode::Divide}, {SDL_SCANCODE_SYSREQ, KeyCode::Sysrq}, {SDL_SCANCODE_RALT, KeyCode::Rmenu}, {SDL_SCANCODE_PAUSE, KeyCode::Pause}, {SDL_SCANCODE_HOME, KeyCode::Home}, {SDL_SCANCODE_UP, KeyCode::Up}, {SDL_SCANCODE_PAGEUP, KeyCode::Prior}, {SDL_SCANCODE_LEFT, KeyCode::Left}, {SDL_SCANCODE_RIGHT, KeyCode::Right}, {SDL_SCANCODE_END, KeyCode::End}, {SDL_SCANCODE_DOWN, KeyCode::Down}, {SDL_SCANCODE_PAGEDOWN, KeyCode::Next}, {SDL_SCANCODE_INSERT, KeyCode::Insert}, {SDL_SCANCODE_DELETE, KeyCode::Delete}, {SDL_SCANCODE_LGUI, KeyCode::Lwin}, {SDL_SCANCODE_RGUI, KeyCode::Rwin}, }; } static auto MakeMouseButtonMap() -> unordered_map<int32_t, MouseButton> { FO_STACK_TRACE_ENTRY(); return { {SDL_BUTTON_LEFT, MouseButton::Left}, {SDL_BUTTON_RIGHT, MouseButton::Right}, {SDL_BUTTON_MIDDLE, MouseButton::Middle}, {SDL_BUTTON_X1, MouseButton::Ext0}, {SDL_BUTTON_X2, MouseButton::Ext1}, {6, MouseButton::Ext2}, {7, MouseButton::Ext3}, {8, MouseButton::Ext4}, }; } static auto WindowPosToScreenPos(ptr<const Renderer> renderer, isize32 screen_size, ipos32 pos) -> ipos32 { FO_STACK_TRACE_ENTRY(); irect32 vp = renderer->GetViewPort(); int32_t screen_x = iround<int32_t>(numeric_cast<float32_t>(pos.x - vp.x) / numeric_cast<float32_t>(vp.width) * numeric_cast<float32_t>(screen_size.width)); int32_t screen_y = iround<int32_t>(numeric_cast<float32_t>(pos.y - vp.y) / numeric_cast<float32_t>(vp.height) * numeric_cast<float32_t>(screen_size.height)); return {screen_x, screen_y}; } static auto ScreenPosToWindowPos(ptr<const Renderer> renderer, isize32 screen_size, ipos32 pos) -> ipos32 { FO_STACK_TRACE_ENTRY(); irect32 vp = renderer->GetViewPort(); int32_t win_x = vp.x + iround<int32_t>(numeric_cast<float32_t>(pos.x) / numeric_cast<float32_t>(screen_size.width) * numeric_cast<float32_t>(vp.width)); int32_t win_y = vp.y + iround<int32_t>(numeric_cast<float32_t>(pos.y) / numeric_cast<float32_t>(screen_size.height) * numeric_cast<float32_t>(vp.height)); return {win_x, win_y}; } static auto MakeSdlWindowHolder(ptr<SDL_Window> window) noexcept -> unique_del_ptr<SDL_Window> { FO_NO_STACK_TRACE_ENTRY(); return make_unique_del_ptr(window, [](SDL_Window* raw_window) { if (raw_window != nullptr) { SDL_DestroyWindow(raw_window); } }); } static auto MakeSdlRendererHolder(ptr<SDL_Renderer> renderer) noexcept -> unique_del_ptr<SDL_Renderer> { FO_NO_STACK_TRACE_ENTRY(); return make_unique_del_ptr(renderer, [](SDL_Renderer* raw_renderer) { if (raw_renderer != nullptr) { SDL_DestroyRenderer(raw_renderer); } }); } struct TemporarySdlWindowRenderer { unique_del_ptr<SDL_Window> Window; unique_del_ptr<SDL_Renderer> Renderer; }; static auto TryCreateTemporarySdlWindowRenderer(ptr<const char> title, int32_t width, int32_t height, SDL_WindowFlags flags) -> optional<TemporarySdlWindowRenderer> { FO_STACK_TRACE_ENTRY(); nptr<SDL_Window> window_out {}; nptr<SDL_Renderer> renderer_out {}; bool created = SDL_CreateWindowAndRenderer(title.get(), width, height, flags, window_out.get_pp(), renderer_out.get_pp()); if (!created) { if (renderer_out) { auto renderer = MakeSdlRendererHolder(renderer_out); ignore_unused(renderer); } if (window_out) { auto window = MakeSdlWindowHolder(window_out); ignore_unused(window); } return std::nullopt; } FO_VERIFY_AND_THROW(window_out, "SDL returned a null window despite successful creation"); FO_VERIFY_AND_THROW(renderer_out, "SDL returned a null renderer despite successful creation"); return TemporarySdlWindowRenderer {MakeSdlWindowHolder(window_out), MakeSdlRendererHolder(renderer_out)}; } static auto GetSdlDisplayMode(SDL_DisplayID display_id) -> ptr<const SDL_DisplayMode> { FO_STACK_TRACE_ENTRY(); nptr<const SDL_DisplayMode> display_mode = display_id != 0 ? SDL_GetCurrentDisplayMode(display_id) : nullptr; FO_VERIFY_AND_THROW(display_mode, "SDL current display mode is unavailable"); return display_mode; } static void UpdateMonitorSettings(GlobalSettings& settings, ptr<const SDL_DisplayMode> display_mode) noexcept { FO_NO_STACK_TRACE_ENTRY(); *const_cast<std::remove_cvref_t<decltype(settings.MonitorWidth)>*>(&settings.MonitorWidth) = display_mode->w; *const_cast<std::remove_cvref_t<decltype(settings.MonitorHeight)>*>(&settings.MonitorHeight) = display_mode->h; } // Routed through the SafeAlloc raw tier rather than the bare Mem* primitives so SDL gets the same // out-of-memory handling as ImGui, AngelScript, zlib and ozz instead of silently receiving null static auto SdlMemMalloc(size_t size) noexcept -> void* { FO_NO_STACK_TRACE_ENTRY(); return SafeAlloc::MallocRaw(size).get(); } static auto SdlMemCalloc(size_t num, size_t size) noexcept -> void* { FO_NO_STACK_TRACE_ENTRY(); return SafeAlloc::CallocRaw(num, size).get(); } static auto SdlMemRealloc(void* mem, size_t size) noexcept -> void* { FO_NO_STACK_TRACE_ENTRY(); return SafeAlloc::ReallocRaw(mem, size).get(); } static void SdlMemFree(void* mem) noexcept { FO_NO_STACK_TRACE_ENTRY(); SafeAlloc::FreeRaw(mem); } Application::Application(GlobalSettings&& settings, AppInitFlags flags) : Settings {std::move(settings)}, MainWindow {make_ptr(this)}, Render {make_ptr(this)}, Input {make_ptr(this)}, Audio {make_ptr(this)}, _ctx {SafeAlloc::MakeUnique<Context>()} { FO_STACK_TRACE_ENTRY(); SDL_SetMemoryFunctions(&SdlMemMalloc, &SdlMemCalloc, &SdlMemRealloc, &SdlMemFree); SDL_SetHint(SDL_HINT_APP_ID, FO_DEV_NAME); SDL_SetHint(SDL_HINT_APP_NAME, Settings.GameName.c_str()); SDL_SetHint(SDL_HINT_ORIENTATIONS, "LandscapeLeft LandscapeRight"); if (Settings.NullRenderer) { SDL_SetHint(SDL_HINT_RENDER_DRIVER, "dummy"); SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy"); AppRender::MAX_ATLAS_WIDTH = 2048; AppRender::MAX_ATLAS_HEIGHT = 2048; } if constexpr (FO_ANDROID) { SDL_SetHint(SDL_HINT_ANDROID_BLOCK_ON_PAUSE, "0"); } SDL_SetHint(SDL_HINT_ORIENTATIONS, "LandscapeLeft LandscapeRight"); SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "1"); WebRelated::ApplyApplicationHints(); // Initialize input events if (SDL_WasInit(SDL_INIT_EVENTS) == 0 && !SDL_InitSubSystem(SDL_INIT_EVENTS)) { throw AppInitException("SDL_InitSubSystem SDL_INIT_EVENTS failed", SDL_GetError()); } if (!Settings.DisableGamepad && SDL_WasInit(SDL_INIT_GAMEPAD) == 0 && !SDL_InitSubSystem(SDL_INIT_GAMEPAD)) { WriteLog("SDL_InitSubSystem SDL_INIT_GAMEPAD failed: {}", SDL_GetError()); } if (!Settings.DisableGamepad) { RefreshGamepadConnection(); } // Initialize audio if (!Settings.DisableAudio) { if (SDL_WasInit(SDL_INIT_AUDIO) != 0 || SDL_InitSubSystem(SDL_INIT_AUDIO)) { auto stream_callback = [](void* userdata, SDL_AudioStream* stream, int32_t additional_amount, int32_t total_amount) FO_DEFERRED { auto app = cast_from_void<Application*>(userdata); FO_VERIFY_AND_THROW(app, "Audio stream callback received a null application pointer"); ignore_unused(total_amount); if (additional_amount > 0) { auto audio_stream = make_nptr(stream); FO_VERIFY_AND_THROW(audio_stream, "Audio stream callback received a null stream"); if (numeric_cast<size_t>(additional_amount) > app->_ctx->AudioStreamBuf.size()) { app->_ctx->AudioStreamBuf.resize(numeric_cast<size_t>(additional_amount) * 2); } auto silence = numeric_cast<uint8_t>(SDL_GetSilenceValueForFormat(app->_ctx->AudioSpec.format)); auto audio_stream_data = app->_ctx->AudioStreamBuf.data(); MemFill(audio_stream_data, silence, numeric_cast<size_t>(additional_amount)); if (app->_ctx->AudioStreamWriter) { span<uint8_t> audio_stream_span = {audio_stream_data, numeric_cast<size_t>(additional_amount)}; app->_ctx->AudioStreamWriter(silence, audio_stream_span); } SDL_PutAudioStreamData(audio_stream.get(), audio_stream_data, additional_amount); } }; auto opened_audio_stream = make_nptr(SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, nullptr, stream_callback, make_nptr(this).void_cast())); if (opened_audio_stream) { auto audio_stream = make_unique_del_ptr(opened_audio_stream, [](SDL_AudioStream* raw_audio_stream) { if (raw_audio_stream != nullptr) { auto audio_stream = make_ptr(raw_audio_stream); SDL_DestroyAudioStream(audio_stream.get()); } }); if (SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(audio_stream.get()), &_ctx->AudioSpec, nullptr)) { if (SDL_ResumeAudioStreamDevice(audio_stream.get())) { _ctx->AudioStream = std::move(audio_stream); } else { WriteLog("SDL resume audio device failed, error {}", SDL_GetError()); } } else { WriteLog("SDL get audio device format failed, error {}", SDL_GetError()); } } else { WriteLog("SDL open audio device stream failed, error {}", SDL_GetError()); } } else { WriteLog("SDL init audio subsystem failed, error {}", SDL_GetError()); } } // First choose render type by user preference if (Settings.NullRenderer) { _ctx->ActiveRendererType = RenderType::Null; _ctx->ActiveRenderer = SafeAlloc::MakeUnique<Null_Renderer>(); } #if FO_HAVE_OPENGL else if (Settings.ForceOpenGL) { _ctx->ActiveRendererType = RenderType::OpenGL; _ctx->ActiveRenderer = SafeAlloc::MakeUnique<OpenGL_Renderer>(); } #endif #if FO_HAVE_DIRECT_3D else if (Settings.ForceDirect3D) { _ctx->ActiveRendererType = RenderType::Direct3D; _ctx->ActiveRenderer = SafeAlloc::MakeUnique<Direct3D_Renderer>(); } #endif #if FO_HAVE_METAL else if (Settings.ForceMetal) { _ctx->ActiveRendererType = RenderType::Metal; throw NotImplementedException(FO_LINE_STR); } #endif #if FO_HAVE_VULKAN else if (Settings.ForceVulkan) { _ctx->ActiveRendererType = RenderType::Vulkan; _ctx->ActiveRenderer = SafeAlloc::MakeUnique<Vulkan_Renderer>(); } #endif #if FO_HAVE_SDL_GPU else if (Settings.ForceSDLGpu) { _ctx->ActiveRendererType = RenderType::SDLGpu; _ctx->ActiveRenderer = SafeAlloc::MakeUnique<SDLGpu_Renderer>(); } #endif // If none of selected then evaluate automatic selection #if FO_HAVE_DIRECT_3D if (!_ctx->ActiveRenderer) { _ctx->ActiveRendererType = RenderType::Direct3D; _ctx->ActiveRenderer = SafeAlloc::MakeUnique<Direct3D_Renderer>(); } #endif #if FO_HAVE_METAL if (!_ctx->ActiveRenderer) { _ctx->ActiveRendererType = RenderType::Metal; } #endif #if FO_HAVE_VULKAN if (!_ctx->ActiveRenderer) { _ctx->ActiveRendererType = RenderType::Vulkan; _ctx->ActiveRenderer = SafeAlloc::MakeUnique<Vulkan_Renderer>(); } #endif #if FO_HAVE_OPENGL if (!_ctx->ActiveRenderer) { _ctx->ActiveRendererType = RenderType::OpenGL; _ctx->ActiveRenderer = SafeAlloc::MakeUnique<OpenGL_Renderer>(); } #endif if (!_ctx->ActiveRenderer) { throw AppInitException("No renderer selected"); } auto active_renderer = GetActiveRenderer(_ctx); // Determine main window size #if FO_IOS || FO_ANDROID _isTablet = true; #endif // Initialize video system if (SDL_WasInit(SDL_INIT_VIDEO) == 0 && !SDL_InitSubSystem(SDL_INIT_VIDEO)) { throw AppInitException("SDL_InitSubSystem SDL_INIT_VIDEO failed", SDL_GetError()); } _clientMode = IsEnumSet(flags, AppInitFlags::ClientMode); if (IsEnumSet(flags, AppInitFlags::ClientMode)) { SDL_DisableScreenSaver(); } if (IsEnumSet(flags, AppInitFlags::ClientMode) && (Settings.HideNativeCursor || !Input.IsMouseAvailable())) { SDL_HideCursor(); _nativeCursorHidden = true; } if (_isTablet) { auto display_id = SDL_GetPrimaryDisplay(); auto display_mode = GetSdlDisplayMode(display_id); Settings.ScreenWidth = std::max(display_mode->w, display_mode->h); Settings.ScreenHeight = std::min(display_mode->w, display_mode->h); float32_t ratio = numeric_cast<float32_t>(Settings.ScreenWidth) / numeric_cast<float32_t>(Settings.ScreenHeight); Settings.ScreenHeight = 768; Settings.ScreenWidth = iround<int32_t>(numeric_cast<float32_t>(Settings.ScreenHeight) * ratio); Settings.Fullscreen = true; } if (IsEnumSet(flags, AppInitFlags::ClientMode)) { _ctx->ClearColor = {0, 0, 0, 255}; } WebRelated::ApplyWindowSettings(Settings); MainWindow._windowHandle = CreateInternalWindow({Settings.ScreenWidth, Settings.ScreenHeight}); MainWindow._title = Settings.GameName; MainWindow._virtualSize = {Settings.ScreenWidth, Settings.ScreenHeight}; MainWindow._virtualScreenSize = {Settings.ScreenWidth, Settings.ScreenHeight}; auto main_window = make_ptr(&MainWindow); _allWindows.emplace_back(main_window); _activeWindow = main_window; if (_ctx->ActiveRendererType != RenderType::Null && !Settings.Fullscreen) { int32_t actual_width = Settings.ScreenWidth; int32_t actual_height = Settings.ScreenHeight; auto sdl_window = MainWindow._windowHandle.reinterpret_as<SDL_Window>(); FO_VERIFY_AND_THROW(sdl_window, "Window handle does not reference a valid SDL window"); SDL_GetWindowSizeInPixels(sdl_window.get(), &actual_width, &actual_height); if (actual_width > 0 && actual_height > 0) { Settings.ScreenWidth = actual_width; Settings.ScreenHeight = actual_height; MainWindow._virtualSize = {actual_width, actual_height}; MainWindow._virtualScreenSize = {actual_width, actual_height}; } } if (_ctx->ActiveRendererType != RenderType::Null && IsEnumSet(flags, AppInitFlags::ClientMode) && !_isTablet && Settings.Fullscreen) { auto sdl_window = MainWindow._windowHandle.reinterpret_as<SDL_Window>(); FO_VERIFY_AND_THROW(sdl_window, "Window handle does not reference a valid SDL window"); if (SDL_SetWindowFullscreen(sdl_window.get(), true)) { _mainWindowFullscreenBackbufferMode = true; SDL_SyncWindow(sdl_window.get()); } } auto main_window_handle = MainWindow._windowHandle; active_renderer->Init(Settings, main_window_handle); if (_ctx->ActiveRendererType != RenderType::Null && MainWindow.IsFullscreen()) { SyncMainWindowBackbufferSize(); } if (IsEnumSet(flags, AppInitFlags::ClientMode) && Settings.AlwaysOnTop) { MainWindow.AlwaysOnTop(true); } if (_ctx->ActiveRendererType != RenderType::Null && !_isTablet) { auto sdl_window = MainWindow._windowHandle.reinterpret_as<SDL_Window>(); FO_VERIFY_AND_THROW(sdl_window, "Window handle does not reference a valid SDL window"); SDL_StartTextInput(sdl_window.get()); } // Init Dear ImGui ImGuiExt::Init(); ImGuiIO& io = ImGui::GetIO(); ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); io.BackendPlatformName = "fonline"; io.WantSaveIniSettings = false; io.IniFilename = nullptr; io.ConfigErrorRecoveryEnableAssert = false; string sdl_backend = SDL_GetCurrentVideoDriver(); vector<string> global_mouse_whitelist = {"windows", "cocoa", "x11", "DIVE", "VMAN"}; _mouseCanUseGlobalState = std::ranges::any_of(global_mouse_whitelist, [&sdl_backend](auto& entry) { return strex(sdl_backend).starts_with(entry); }); if (Settings.ImGuiColorStyle == "Dark") { ImGui::StyleColorsDark(); } else if (Settings.ImGuiColorStyle == "Classic") { ImGui::StyleColorsClassic(); } else { ImGui::StyleColorsLight(); // Default theme } io.BackendFlags |= ImGuiBackendFlags_RendererHasTextures; if (Input.IsMouseAvailable()) { io.BackendFlags |= ImGuiBackendFlags_HasSetMousePos; } platform_io.Platform_GetClipboardTextFn = [](ImGuiContext*) -> const char* FO_DEFERRED { return GetApp()->Input.GetClipboardText().c_str(); }; platform_io.Platform_SetClipboardTextFn = [](ImGuiContext*, const char* text) FO_DEFERRED { GetApp()->Input.SetClipboardText(text ? string_view {text} : string_view {}); }; platform_io.Platform_ClipboardUserData = nullptr; #if FO_WINDOWS if (_ctx->ActiveRendererType != RenderType::Null) { auto sdl_window = MainWindow._windowHandle.reinterpret_as<SDL_Window>(); FO_VERIFY_AND_THROW(sdl_window, "Window handle does not reference a valid SDL window"); auto sdl_windows_props = SDL_GetWindowProperties(sdl_window.get()); ImGui::GetMainViewport()->PlatformHandleRaw = SDL_GetPointerProperty(sdl_windows_props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr); } #endif SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1"); io.Fonts->Flags = ImFontAtlasFlags_None; io.Fonts->TexDesiredFormat = ImTextureFormat_RGBA32; io.Fonts->TexMinWidth = Settings.ImGuiFontTextureSize; io.Fonts->TexMinHeight = Settings.ImGuiFontTextureSize; io.Fonts->TexMaxWidth = AppRender::MAX_ATLAS_SIZE; io.Fonts->TexMaxHeight = AppRender::MAX_ATLAS_SIZE; // Default effect FileSystem base_fs; base_fs.AddPackSource(IsPackaged() ? Settings.ClientResources : Settings.BakeOutput, "Embedded", true); base_fs.AddPackSource(IsPackaged() ? Settings.ClientResources : Settings.BakeOutput, "Core", true); LoadImGuiEffect(base_fs); _imguiDrawBuf = active_renderer->CreateDrawBuffer(false); // Start timings _timeFrequency = SDL_GetPerformanceFrequency(); _time = SDL_GetPerformanceCounter(); } Application::~Application() { FO_STACK_TRACE_ENTRY(); _imguiTextures.clear(); _imguiEffect.reset(); _imguiDrawBuf.reset(); ImGui::DestroyContext(); if (_ctx->AudioStream) { _ctx->AudioStream.reset(); } CloseGamepad(); for (size_t i = 0; i != _childWindows.size(); ++i) { _childWindows[i]->_virtualRenderTex.reset(); } _childWindows.clear(); auto main_window = make_ptr(&MainWindow); _activeWindow = main_window; _currentRenderingWindow = nullptr; _previousRenderTarget = nullptr; _allWindows.clear(); _ctx->NullWindowStubs.clear(); _ctx->RenderTargetTex = nullptr; // Renderer backends may need the SDL window while releasing their native resources. _ctx->ActiveRenderer.reset(); if (MainWindow._windowHandle) { if (_ctx->ActiveRendererType != RenderType::Null) { auto sdl_window = MainWindow._windowHandle.reinterpret_as<SDL_Window>(); SDL_StopTextInput(sdl_window.get()); auto owned_window = MakeSdlWindowHolder(sdl_window); ignore_unused(owned_window); } MainWindow._windowHandle = nullptr; } _ctx->ClearColor = {150, 150, 150, 255}; _ctx->ActiveRendererType = RenderType::Null; } void Application::OpenLink(string_view link) { FO_STACK_TRACE_ENTRY(); string link_text = string(link); auto link_ptr = make_ptr(link_text.c_str()); SDL_OpenURL(link_ptr.get()); } void Application::LoadImGuiEffect(const FileSystem& resources) { FO_STACK_TRACE_ENTRY(); if (!_imguiEffect && resources.IsFileExists(Settings.ImGuiDefaultEffect)) { auto active_renderer = GetActiveRenderer(_ctx); _imguiEffect = active_renderer->CreateEffect(EffectUsage::ImGui, Settings.ImGuiDefaultEffect, [&](string_view path) -> string { auto file = resources.ReadFile(path); FO_VERIFY_AND_THROW(file, "ImGui_Default effect not found"); return file.GetStr(); }); } } #if FO_IOS void Application::SetMainLoopCallback(void (*callback)(void*)) { FO_STACK_TRACE_ENTRY(); auto sdl_window = MainWindow._windowHandle.reinterpret_as<SDL_Window>(); FO_VERIFY_AND_THROW(sdl_window, "Window handle does not reference a valid SDL window"); SDL_SetiOSAnimationCallback(sdl_window.get(), 1, callback, nullptr); } #endif auto Application::CreateChildWindow(isize32 size, string_view title) -> ptr<AppWindow> { FO_STACK_TRACE_ENTRY(); if (size.width <= 0 || size.height <= 0) { size = {Settings.ScreenWidth, Settings.ScreenHeight}; } auto window = SafeAlloc::MakeUnique<AppWindow>(this); window->_isVirtual = true; window->_virtualSize = size; window->_virtualScreenSize = size; window->_virtualLayoutSize = size; window->_title = title.empty() ? strex("Window {}", _childWindows.size() + 1).str() : string {title}; _allWindows.emplace_back(window); _childWindows.emplace_back(std::move(window)); _activeWindow = _allWindows.back(); return _allWindows.back(); } void Application::DestroyChildWindow(nptr<AppWindow> window) { FO_STACK_TRACE_ENTRY(); auto main_window = make_ptr(&MainWindow); if (!window || window == main_window) { return; } std::erase_if(_allWindows, [&](auto&& entry) { return entry == window; }); auto it = std::ranges::find_if(_childWindows, [&](const auto& entry) { return window == entry; }); if (it == _childWindows.end()) { return; } if (_activeWindow == window) { if (!_childWindows.empty() && _childWindows.front() != window) { _activeWindow = _childWindows.front(); } else { _activeWindow = main_window; } } if (_currentRenderingWindow == window) { EndWindowRender(); } (*it)->_virtualRenderTex.reset(); _childWindows.erase(it); if (_childWindows.empty()) { _activeWindow = main_window; } } void Application::SetActiveWindow(nptr<AppWindow> window) { FO_STACK_TRACE_ENTRY(); if (!window) { auto main_window = make_ptr(&MainWindow); _activeWindow = main_window; return; } _activeWindow = window; } void Application::EnsureVirtualRenderTexture(ptr<AppWindow> window, isize32 size) { FO_STACK_TRACE_ENTRY(); FO_VERIFY_AND_THROW(window->_isVirtual, "Window is not virtual"); ignore_unused(size); isize32 desired = window->_virtualSize.width > 0 && window->_virtualSize.height > 0 // ? window->_virtualSize : isize32 {Settings.ScreenWidth, Settings.ScreenHeight}; bool recreate_texture = true; auto existing_render_tex = window->GetRenderTexture(); if (existing_render_tex) { recreate_texture = existing_render_tex->Size != desired; } if (recreate_texture) { auto active_renderer = GetActiveRenderer(_ctx); window->_virtualRenderTex = active_renderer->CreateTexture(desired, true, true); window->_virtualRenderTex->FlippedHeight = active_renderer->IsRenderTargetFlipped(); } } auto Application::IsMainWindowActuallyFullscreen() const -> bool { FO_STACK_TRACE_ENTRY(); if (_ctx->ActiveRendererType == RenderType::Null || !MainWindow._windowHandle) { return MainWindow._windowHandle ? MainWindow.ResolveWindowStub()->Fullscreen : false; } auto sdl_window = MainWindow._windowHandle.reinterpret_as<SDL_Window>(); FO_VERIFY_AND_THROW(sdl_window, "Window handle does not reference a valid SDL window"); return (SDL_GetWindowFlags(sdl_window.get()) & SDL_WINDOW_FULLSCREEN) != 0; } auto Application::IsMainWindowDisplayModeSize(isize32 size) const -> bool { FO_STACK_TRACE_ENTRY(); if (_ctx->ActiveRendererType == RenderType::Null || !MainWindow._windowHandle || size.width <= 0 || size.height <= 0) { return false; } auto sdl_window = MainWindow._windowHandle.reinterpret_as<SDL_Window>(); FO_VERIFY_AND_THROW(sdl_window, "Window handle does not reference a valid SDL window"); SDL_DisplayID display_id = SDL_GetDisplayForWindow(sdl_window.get()); auto display_mode = (display_id != 0 ? SDL_GetCurrentDisplayMode(display_id) : nullptr); return display_mode && display_mode->w == size.width && display_mode->h == size.height; } auto Application::GetMainWindowBackbufferSize() const -> isize32 { FO_STACK_TRACE_ENTRY(); if (_ctx->ActiveRendererType == RenderType::Null || !MainWindow._windowHandle) { return {Settings.ScreenWidth, Settings.ScreenHeight}; } auto sdl_window = MainWindow._windowHandle.reinterpret_as<SDL_Window>(); FO_VERIFY_AND_THROW(sdl_window, "Window handle does not reference a valid SDL window"); if (Settings.Fullscreen || _mainWindowFullscreenBackbufferMode || IsMainWindowActuallyFullscreen()) { int32_t window_width = Settings.ScreenWidth; int32_t window_height = Settings.ScreenHeight; SDL_GetWindowSizeInPixels(sdl_window.get(), &window_width, &window_height); if (window_width > 0 && window_height > 0 && (window_width != Settings.ScreenWidth || window_height != Settings.ScreenHeight)) { return {window_width, window_height}; } SDL_DisplayID display_id = SDL_GetDisplayForWindow(sdl_window.get()); auto display_mode = (display_id != 0 ? SDL_GetCurrentDisplayMode(display_id) : nullptr); if (display_mode && display_mode->w > 0 && display_mode->h > 0) { return {display_mode->w, display_mode->h}; } } int32_t width = Settings.ScreenWidth; int32_t height = Settings.ScreenHeight; SDL_GetWindowSizeInPixels(sdl_window.get(), &width, &height); return width > 0 && height > 0 ? isize32 {width, height} : isize32 {Settings.ScreenWidth, Settings.ScreenHeight}; } void Application::SyncMainWindowBackbufferSize() { FO_STACK_TRACE_ENTRY(); isize32 backbuffer_size = GetMainWindowBackbufferSize(); if (backbuffer_size.width > 0 && backbuffer_size.height > 0) { auto active_renderer = GetActiveRenderer(_ctx); active_renderer->OnResizeWindow(backbuffer_size); } } auto Application::MakeAspectFitRect(isize32 source_size, isize32 target_size) const -> irect32 { FO_STACK_TRACE_ENTRY(); if (source_size.width <= 0 || source_size.height <= 0 || target_size.width <= 0 || target_size.height <= 0) { return {}; } float32_t source_aspect = checked_div<float32_t>(numeric_cast<float32_t>(source_size.width), numeric_cast<float32_t>(source_size.height)); float32_t target_aspect = checked_div<float32_t>(numeric_cast<float32_t>(target_size.width), numeric_cast<float32_t>(target_size.height)); int32_t width = iround<int32_t>(source_aspect <= target_aspect ? numeric_cast<float32_t>(target_size.height) * source_aspect : numeric_cast<float32_t>(target_size.width)); int32_t height = iround<int32_t>(source_aspect <= target_aspect ? numeric_cast<float32_t>(target_size.height) : numeric_cast<float32_t>(target_size.width) / source_aspect); return { (target_size.width - width) / 2, (target_size.height - height) / 2, width, height, }; } void Application::BeginWindowRender(ptr<AppWindow> window) { FO_STACK_TRACE_ENTRY(); FO_VERIFY_AND_THROW(!_currentRenderingWindow, "Current rendering window must be unset before this operation"); if (!window->_isVirtual) { _currentRenderingWindow = window; return; } EnsureVirtualRenderTexture(window, window->_virtualLayoutSize); _previousRenderTarget = _ctx->RenderTargetTex; _currentRenderingWindow = window; Render.SetRenderTarget(window->GetRenderTexture()); isize32 screen_size = window->GetScreenSize(); if (screen_size.width > 0 && screen_size.height > 0) { _hostScreenWidthSaved = Settings.ScreenWidth; _hostScreenHeightSaved = Settings.ScreenHeight; _hostScreenSizeSaved = true; Settings.ScreenWidth = screen_size.width; Settings.ScreenHeight = screen_size.height; } Render.ClearRenderTarget(ucolor {0, 0, 0, 255}, true, false); } void Application::EndWindowRender() { FO_STACK_TRACE_ENTRY(); if (!_currentRenderingWindow) { return; } bool was_virtual = _currentRenderingWindow->_isVirtual; auto prev = _previousRenderTarget; _previousRenderTarget = nullptr; _currentRenderingWindow = nullptr; if (was_virtual) { // Restore the host's screen size so engine chrome (tab bar, server panel) renders at OS pixels. if (_hostScreenSizeSaved) { Settings.ScreenWidth = _hostScreenWidthSaved; Settings.ScreenHeight = _hostScreenHeightSaved; _hostScreenSizeSaved = false; } Render.SetRenderTarget(prev); } } auto Application::TranslateHostPosToActiveWindow(ipos32 pos) const -> ipos32 { FO_STACK_TRACE_ENTRY(); auto window = _currentRenderingWindow; if (!window) { window = _activeWindow; } if (!window || !window->_isVirtual) { return pos; } const irect32& rect = window->_displayRect; if (rect.width <= 0 || rect.height <= 0 || window->_virtualSize.width <= 0 || window->_virtualSize.height <= 0) { return pos; } isize32 screen_size = window->GetScreenSize(); irect32 content_rect = MakeAspectFitRect(screen_size, window->_virtualSize); if (content_rect.width <= 0 || content_rect.height <= 0) { return pos; } int32_t virtual_x = iround<int32_t>(numeric_cast<float32_t>(pos.x - rect.x) * numeric_cast<float32_t>(window->_virtualSize.width) / numeric_cast<float32_t>(rect.width)); int32_t virtual_y = iround<int32_t>(numeric_cast<float32_t>(pos.y - rect.y) * numeric_cast<float32_t>(window->_virtualSize.height) / numeric_cast<float32_t>(rect.height)); int32_t local_x = iround<int32_t>(numeric_cast<float32_t>(virtual_x - content_rect.x) * numeric_cast<float32_t>(screen_size.width) / numeric_cast<float32_t>(content_rect.width)); int32_t local_y = iround<int32_t>(numeric_cast<float32_t>(virtual_y - content_rect.y) * numeric_cast<float32_t>(screen_size.height) / numeric_cast<float32_t>(content_rect.height)); return {local_x, local_y}; } auto Application::TranslateActiveWindowPosToHost(ipos32 pos) const -> ipos32 { FO_STACK_TRACE_ENTRY(); auto window = _currentRenderingWindow; if (!window) { window = _activeWindow; } if (!window || !window->_isVirtual) { return pos; } const irect32& rect = window->_displayRect; if (rect.width <= 0 || rect.height <= 0 || window->_virtualSize.width <= 0 || window->_virtualSize.height <= 0) { return pos; } isize32 screen_size = window->GetScreenSize(); irect32 content_rect = MakeAspectFitRect(screen_size, window->_virtualSize); if (content_rect.width <= 0 || content_rect.height <= 0) { return pos; } int32_t virtual_x = content_rect.x + iround<int32_t>(numeric_cast<float32_t>(pos.x) * numeric_cast<float32_t>(content_rect.width) / numeric_cast<float32_t>(screen_size.width)); int32_t virtual_y = content_rect.y + iround<int32_t>(numeric_cast<float32_t>(pos.y) * numeric_cast<float32_t>(content_rect.height) / numeric_cast<float32_t>(screen_size.height)); int32_t host_x = rect.x + iround<int32_t>(numeric_cast<float32_t>(virtual_x) * numeric_cast<float32_t>(rect.width) / numeric_cast<float32_t>(window->_virtualSize.width)); int32_t host_y = rect.y + iround<int32_t>(numeric_cast<float32_t>(virtual_y) * numeric_cast<float32_t>(rect.height) / numeric_cast<float32_t>(window->_virtualSize.height)); return {host_x, host_y}; } auto Application::ScaleHostDeltaToActiveWindow(ipos32 delta) const -> ipos32 { FO_STACK_TRACE_ENTRY(); if (!_activeWindow || !_activeWindow->_isVirtual) { return delta; } const irect32& rect = _activeWindow->_displayRect; if (rect.width <= 0 || rect.height <= 0 || _activeWindow->_virtualSize.width <= 0 || _activeWindow->_virtualSize.height <= 0) { return delta; } isize32 screen_size = _activeWindow->GetScreenSize(); irect32 content_rect = MakeAspectFitRect(screen_size, _activeWindow->_virtualSize); if (content_rect.width <= 0 || content_rect.height <= 0) { return delta; } int32_t local_dx = iround<int32_t>(numeric_cast<float32_t>(delta.x) * numeric_cast<float32_t>(_activeWindow->_virtualSize.width) * numeric_cast<float32_t>(screen_size.width) / (numeric_cast<float32_t>(rect.width) * numeric_cast<float32_t>(content_rect.width))); int32_t local_dy = iround<int32_t>(numeric_cast<float32_t>(delta.y) * numeric_cast<float32_t>(_activeWindow->_virtualSize.height) * numeric_cast<float32_t>(screen_size.height) / (numeric_cast<float32_t>(rect.height) * numeric_cast<float32_t>(content_rect.height))); return {local_dx, local_dy}; } auto Application::CreateInternalWindow(isize32 size) -> ptr<WindowInternalHandle> { FO_STACK_TRACE_ENTRY(); if (_ctx->ActiveRendererType == RenderType::Null) { auto handle = SafeAlloc::MakeUnique<HeadlessWindowStub>(); handle->Size = size; auto headless_window = handle.as_ptr(); _ctx->NullWindowStubs.emplace_back(std::move(handle)); return headless_window.reinterpret_as<WindowInternalHandle>(); } // Initialize window SDL_PropertiesID props = SDL_CreateProperties(); SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN, 1); if (Settings.WindowResizable) { SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN, 1); } #if FO_HAVE_OPENGL if (_ctx->ActiveRendererType == RenderType::OpenGL) { #if !FO_WEB SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_OPENGL_BOOLEAN, 1); #endif SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 0); SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 0); SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 0); SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); #if FO_OPENGL_ES SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); #else SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1); #endif } #endif #if FO_HAVE_METAL if (_ctx->ActiveRendererType == RenderType::Metal) { SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_METAL_BOOLEAN, 1); } #endif #if FO_HAVE_VULKAN if (_ctx->ActiveRendererType == RenderType::Vulkan) { SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_VULKAN_BOOLEAN, 1); } #endif if (_isTablet && !Settings.HeadlessWindow) { SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_FULLSCREEN_BOOLEAN, 1); SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_BORDERLESS_BOOLEAN, 1); } if (Settings.HeadlessWindow) { SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HIDDEN_BOOLEAN, 1); } if (Settings.WindowCentered) { SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, SDL_WINDOWPOS_CENTERED); SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, SDL_WINDOWPOS_CENTERED); } auto window_title = make_ptr(Settings.GameName.c_str()); SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, window_title.get()); SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, size.width); SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, size.height); auto sdl_window = make_nptr(SDL_CreateWindowWithProperties(props)); if (!sdl_window) { throw AppInitException("Window creation failed", SDL_GetError()); } if (!_isTablet) { SDL_SetWindowFullscreenMode(sdl_window.get(), nullptr); } auto display_id = SDL_GetDisplayForWindow(sdl_window.get()); auto display_mode = GetSdlDisplayMode(display_id); UpdateMonitorSettings(Settings, display_mode); return sdl_window.void_cast(); } auto Application::ResolveTouchPos(float32_t normalized_x, float32_t normalized_y) const -> ipos32 { FO_STACK_TRACE_ENTRY(); int32_t window_width = Settings.ScreenWidth; int32_t window_height = Settings.ScreenHeight; if (_ctx->ActiveRendererType != RenderType::Null && MainWindow._windowHandle) { auto sdl_window = MainWindow.ResolveWindowHandle().reinterpret_as<SDL_Window>(); SDL_GetWindowSizeInPixels(sdl_window.get(), &window_width, &window_height); } int32_t max_x = window_width > 0 ? window_width - 1 : 0; int32_t max_y = window_height > 0 ? window_height - 1 : 0; int32_t window_x = std::clamp(iround<int32_t>(normalized_x * numeric_cast<float32_t>(window_width)), 0, max_x); int32_t window_y = std::clamp(iround<int32_t>(normalized_y * numeric_cast<float32_t>(window_height)), 0, max_y); if (_ctx->ActiveRendererType == RenderType::Null) { return {window_x, window_y}; } auto active_renderer = GetActiveRenderer(_ctx); return WindowPosToScreenPos(active_renderer, {Settings.ScreenWidth, Settings.ScreenHeight}, {window_x, window_y}); } auto Application::GetTouchElapsedMs(uint64_t start_time, uint64_t end_time) const -> uint32_t { FO_STACK_TRACE_ENTRY(); if (end_time <= start_time || _timeFrequency == 0) { return 0; } return numeric_cast<uint32_t>((end_time - start_time) * 1000 / _timeFrequency); } auto Application::GetTouchDistance(ipos32 from, ipos32 to) const -> float32_t { FO_STACK_TRACE_ENTRY(); float32_t dx = numeric_cast<float32_t>(to.x - from.x); float32_t dy = numeric_cast<float32_t>(to.y - from.y); return std::sqrt(dx * dx + dy * dy); } auto Application::FindTouchPoint(int64_t finger_id) -> nptr<TouchPointState> { FO_STACK_TRACE_ENTRY(); if (_touchPrimary.Active && _touchPrimary.FingerId == finger_id) { return &_touchPrimary; } if (_touchSecondary.Active && _touchSecondary.FingerId == finger_id) { return &_touchSecondary; } return nullptr; } auto Application::FindOtherTouchPoint(int64_t finger_id) -> nptr<TouchPointState> { FO_STACK_TRACE_ENTRY(); if (_touchPrimary.Active && _touchPrimary.FingerId != finger_id) { return &_touchPrimary; } if (_touchSecondary.Active && _touchSecondary.FingerId != finger_id) { return &_touchSecondary; } return nullptr; } auto Application::AcquireTouchPoint(int64_t finger_id) -> nptr<TouchPointState> { FO_STACK_TRACE_ENTRY(); if (auto existing_touch = FindTouchPoint(finger_id)) { return existing_touch; } if (!_touchPrimary.Active) { return &_touchPrimary; } if (!_touchSecondary.Active) { return &_touchSecondary; } return nullptr; } void Application::ReleaseTouchPoint(int64_t finger_id) { FO_STACK_TRACE_ENTRY(); if (_touchPrimary.FingerId == finger_id) { _touchPrimary = {}; } else if (_touchSecondary.FingerId == finger_id) { _touchSecondary = {}; } } void Application::ResetTouchGestures() { FO_STACK_TRACE_ENTRY(); _touchPrimary = {}; _touchSecondary = {}; _pendingTouchTap = {}; _touchPinchActive = false; _touchTapSuppressed = false; _touchLastPinchDistance = 0.0f; } void Application::QueueTouchDown(int64_t finger_id, ipos32 pos) { FO_STACK_TRACE_ENTRY(); _ctx->EventsQueue.emplace_back(InputEvent::TouchDownEvent {finger_id, pos.x, pos.y}); } void Application::QueueTouchMove(int64_t finger_id, ipos32 pos, ipos32 delta) { FO_STACK_TRACE_ENTRY(); _ctx->EventsQueue.emplace_back(InputEvent::TouchMoveEvent {finger_id, pos.x, pos.y, delta.x, delta.y}); } void Application::QueueTouchUp(int64_t finger_id, ipos32 pos) { FO_STACK_TRACE_ENTRY(); _ctx->EventsQueue.emplace_back(InputEvent::TouchUpEvent {finger_id, pos.x, pos.y}); } void Application::QueueTouchTap(ipos32 pos) { FO_STACK_TRACE_ENTRY(); _ctx->EventsQueue.emplace_back(InputEvent::TouchTapEvent {pos.x, pos.y}); } void Application::QueueTouchDoubleTap(ipos32 pos) { FO_STACK_TRACE_ENTRY(); _ctx->EventsQueue.emplace_back(InputEvent::TouchDoubleTapEvent {pos.x, pos.y}); } void Application::QueueTouchScroll(ipos32 pos, ipos32 delta) { FO_STACK_TRACE_ENTRY(); _ctx->EventsQueue.emplace_back(InputEvent::TouchScrollEvent {pos.x, pos.y, delta.x, delta.y}); } void Application::QueueTouchZoom(ipos32 pos, float32_t factor) { FO_STACK_TRACE_ENTRY(); _ctx->EventsQueue.emplace_back(InputEvent::TouchZoomEvent {pos.x, pos.y, factor}); } void Application::FlushPendingTouchTap() { FO_STACK_TRACE_ENTRY(); if (!_pendingTouchTap.Active) { return; } auto cur_time = SDL_GetPerformanceCounter(); if (GetTouchElapsedMs(_pendingTouchTap.ReleaseTime, cur_time) < TOUCH_DOUBLE_TAP_MAX_TIME_MS) { return; } QueueTouchTap(_pendingTouchTap.Pos); _pendingTouchTap = {}; } void Application::UpdateNativeCursorVisibility(bool imguiOverlayWantsCursor) { FO_STACK_TRACE_ENTRY(); if (_ctx->ActiveRendererType == RenderType::Null || _isTablet) { return; } bool should_hide_cursor; if (!Input.IsMouseAvailable()) { should_hide_cursor = true; } else if (!_childWindows.empty()) { should_hide_cursor = false; } else { should_hide_cursor = _clientMode && Settings.HideNativeCursor && !imguiOverlayWantsCursor; } if (should_hide_cursor == _nativeCursorHidden) { return; } if (should_hide_cursor) { SDL_HideCursor(); } else { SDL_ShowCursor(); } _nativeCursorHidden = should_hide_cursor; } void Application::CloseGamepad() { FO_STACK_TRACE_ENTRY(); if (_gamepadHandle) { SDL_CloseGamepad(cast_from_void<SDL_Gamepad*>(_gamepadHandle.get()).get()); _gamepadHandle = nullptr; } _gamepadInstanceId = -1; _gamepadState = {}; } void Application::RefreshGamepadConnection() { FO_STACK_TRACE_ENTRY(); if (_gamepadHandle || SDL_WasInit(SDL_INIT_GAMEPAD) == 0) { return; } int count = 0; auto queried_gamepads = make_nptr(SDL_GetGamepads(&count)); if (!queried_gamepads) { return; } auto gamepads = make_unique_del_ptr(queried_gamepads, [](SDL_JoystickID* raw_data) { if (raw_data != nullptr) { auto data = make_ptr(raw_data); SDL_free(data.get()); } }); for (int i = 0; i < count; i++) { auto gamepad = make_nptr(SDL_OpenGamepad(gamepads[numeric_cast<size_t>(i)])); if (!gamepad) { continue; } _gamepadHandle = gamepad; _gamepadInstanceId = numeric_cast<int32_t>(SDL_GetGamepadID(gamepad.get())); _gamepadState.Available = true; UpdateGamepadAxis(SDL_GAMEPAD_AXIS_LEFTX, SDL_GetGamepadAxis(gamepad.get(), SDL_GAMEPAD_AXIS_LEFTX)); UpdateGamepadAxis(SDL_GAMEPAD_AXIS_LEFTY, SDL_GetGamepadAxis(gamepad.get(), SDL_GAMEPAD_AXIS_LEFTY)); UpdateGamepadAxis(SDL_GAMEPAD_AXIS_RIGHTX, SDL_GetGamepadAxis(gamepad.get(), SDL_GAMEPAD_AXIS_RIGHTX)); UpdateGamepadAxis(SDL_GAMEPAD_AXIS_RIGHTY, SDL_GetGamepadAxis(gamepad.get(), SDL_GAMEPAD_AXIS_RIGHTY)); UpdateGamepadAxis(SDL_GAMEPAD_AXIS_LEFT_TRIGGER, SDL_GetGamepadAxis(gamepad.get(), SDL_GAMEPAD_AXIS_LEFT_TRIGGER)); UpdateGamepadAxis(SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, SDL_GetGamepadAxis(gamepad.get(), SDL_GAMEPAD_AXIS_RIGHT_TRIGGER)); UpdateGamepadButton(SDL_GAMEPAD_BUTTON_SOUTH, SDL_GetGamepadButton(gamepad.get(), SDL_GAMEPAD_BUTTON_SOUTH)); UpdateGamepadButton(SDL_GAMEPAD_BUTTON_EAST, SDL_GetGamepadButton(gamepad.get(), SDL_GAMEPAD_BUTTON_EAST)); UpdateGamepadButton(SDL_GAMEPAD_BUTTON_WEST, SDL_GetGamepadButton(gamepad.get(), SDL_GAMEPAD_BUTTON_WEST)); UpdateGamepadButton(SDL_GAMEPAD_BUTTON_NORTH, SDL_GetGamepadButton(gamepad.get(), SDL_GAMEPAD_BUTTON_NORTH)); UpdateGamepadButton(SDL_GAMEPAD_BUTTON_BACK, SDL_GetGamepadButton(gamepad.get(), SDL_GAMEPAD_BUTTON_BACK)); UpdateGamepadButton(SDL_GAMEPAD_BUTTON_START, SDL_GetGamepadButton(gamepad.get(), SDL_GAMEPAD_BUTTON_START)); UpdateGamepadButton(SDL_GAMEPAD_BUTTON_LEFT_STICK, SDL_GetGamepadButton(gamepad.get(), SDL_GAMEPAD_BUTTON_LEFT_STICK)); UpdateGamepadButton(SDL_GAMEPAD_BUTTON_RIGHT_STICK, SDL_GetGamepadButton(gamepad.get(), SDL_GAMEPAD_BUTTON_RIGHT_STICK)); UpdateGamepadButton(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, SDL_GetGamepadButton(gamepad.get(), SDL_GAMEPAD_BUTTON_LEFT_SHOULDER)); UpdateGamepadButton(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, SDL_GetGamepadButton(gamepad.get(), SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER)); UpdateGamepadButton(SDL_GAMEPAD_BUTTON_DPAD_UP, SDL_GetGamepadButton(gamepad.get(), SDL_GAMEPAD_BUTTON_DPAD_UP)); UpdateGamepadButton(SDL_GAMEPAD_BUTTON_DPAD_DOWN, SDL_GetGamepadButton(gamepad.get(), SDL_GAMEPAD_BUTTON_DPAD_DOWN)); UpdateGamepadButton(SDL_GAMEPAD_BUTTON_DPAD_LEFT, SDL_GetGamepadButton(gamepad.get(), SDL_GAMEPAD_BUTTON_DPAD_LEFT)); UpdateGamepadButton(SDL_GAMEPAD_BUTTON_DPAD_RIGHT, SDL_GetGamepadButton(gamepad.get(), SDL_GAMEPAD_BUTTON_DPAD_RIGHT)); break; } } void Application::UpdateGamepadAxis(int32_t axis, int32_t value) { FO_STACK_TRACE_ENTRY(); auto normalize_gamepad_stick_axis = [](int32_t axis_value) -> float32_t { FO_STACK_TRACE_ENTRY(); float32_t normalized = axis_value >= 0 ? numeric_cast<float32_t>(axis_value) / 32767.0f : numeric_cast<float32_t>(axis_value) / 32768.0f; float32_t abs_value = std::abs(normalized); if (abs_value <= GAMEPAD_STICK_DEADZONE) { return 0.0f; } float32_t scaled = std::clamp((abs_value - GAMEPAD_STICK_DEADZONE) / (1.0f - GAMEPAD_STICK_DEADZONE), 0.0f, 1.0f); return normalized < 0.0f ? -scaled : scaled; }; auto normalize_gamepad_trigger_axis = [](int32_t axis_value) -> float32_t { FO_STACK_TRACE_ENTRY(); float32_t normalized = std::clamp(numeric_cast<float32_t>(axis_value) / 32767.0f, 0.0f, 1.0f); if (normalized <= GAMEPAD_TRIGGER_DEADZONE) { return 0.0f; } return std::clamp((normalized - GAMEPAD_TRIGGER_DEADZONE) / (1.0f - GAMEPAD_TRIGGER_DEADZONE), 0.0f, 1.0f); }; switch (static_cast<SDL_GamepadAxis>(axis)) { case SDL_GAMEPAD_AXIS_LEFTX: _gamepadState.LeftStickX = normalize_gamepad_stick_axis(value); break; case SDL_GAMEPAD_AXIS_LEFTY: _gamepadState.LeftStickY = normalize_gamepad_stick_axis(value); break; case SDL_GAMEPAD_AXIS_RIGHTX: _gamepadState.RightStickX = normalize_gamepad_stick_axis(value); break; case SDL_GAMEPAD_AXIS_RIGHTY: _gamepadState.RightStickY = normalize_gamepad_stick_axis(value); break; case SDL_GAMEPAD_AXIS_LEFT_TRIGGER: _gamepadState.LeftTrigger = normalize_gamepad_trigger_axis(value); break; case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER: _gamepadState.RightTrigger = normalize_gamepad_trigger_axis(value); break; default: break; } } void Application::UpdateGamepadButton(int32_t button, bool pressed) { FO_STACK_TRACE_ENTRY(); switch (static_cast<SDL_GamepadButton>(button)) { case SDL_GAMEPAD_BUTTON_SOUTH: _gamepadState.South = pressed; break; case SDL_GAMEPAD_BUTTON_EAST: _gamepadState.East = pressed; break; case SDL_GAMEPAD_BUTTON_WEST: _gamepadState.West = pressed; break; case SDL_GAMEPAD_BUTTON_NORTH: _gamepadState.North = pressed; break; case SDL_GAMEPAD_BUTTON_BACK: _gamepadState.Back = pressed; break; case SDL_GAMEPAD_BUTTON_START: _gamepadState.Start = pressed; break; case SDL_GAMEPAD_BUTTON_LEFT_STICK: _gamepadState.LeftStickButton = pressed; break; case SDL_GAMEPAD_BUTTON_RIGHT_STICK: _gamepadState.RightStickButton = pressed; break; case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER: _gamepadState.LeftShoulder = pressed; break; case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER: _gamepadState.RightShoulder = pressed; break; case SDL_GAMEPAD_BUTTON_DPAD_UP: _gamepadState.DpadUp = pressed; break; case SDL_GAMEPAD_BUTTON_DPAD_DOWN: _gamepadState.DpadDown = pressed; break; case SDL_GAMEPAD_BUTTON_DPAD_LEFT: _gamepadState.DpadLeft = pressed; break; case SDL_GAMEPAD_BUTTON_DPAD_RIGHT: _gamepadState.DpadRight = pressed; break; default: break; } } void Application::BeginFrame() { FO_STACK_TRACE_ENTRY(); if (IsQuitSignalReceived()) { RequestQuit(); } FO_VERIFY_AND_THROW(_ctx->RenderTargetTex == nullptr, "Context render target tex must be unset before this operation"); auto active_renderer = GetActiveRenderer(_ctx); active_renderer->ClearRenderTarget(_ctx->ClearColor); ImGuiIO& io = ImGui::GetIO(); bool imgui_capture_mouse = io.WantCaptureMouse; bool imgui_capture_keyboard = io.WantCaptureKeyboard || io.WantTextInput; auto host_pos_inside_active_virtual = [&](ipos32 host_pos) -> bool { if (!_activeWindow || !_activeWindow->_isVirtual) { return false; } const irect32& r = _activeWindow->_displayRect; return r.width > 0 && r.height > 0 && // host_pos.x >= r.x && host_pos.x < r.x + r.width && // host_pos.y >= r.y && host_pos.y < r.y + r.height; }; auto switch_active_to_hovered_child = [&](ipos32 host_pos) { for (size_t i = 0; i != _childWindows.size(); ++i) { const irect32& r = _childWindows[i]->_displayRect; if (r.width > 0 && r.height > 0 && host_pos.x >= r.x && host_pos.x < r.x + r.width && host_pos.y >= r.y && host_pos.y < r.y + r.height) { if (_activeWindow != _childWindows[i]) { _activeWindow = _childWindows[i]; } break; } } }; if (!_ctx->NextFrameEventsQueue.empty()) { _ctx->EventsQueue.insert(_ctx->EventsQueue.end(), _ctx->NextFrameEventsQueue.begin(), _ctx->NextFrameEventsQueue.end()); _ctx->NextFrameEventsQueue.clear(); } SDL_PumpEvents(); SDL_Event sdl_event; bool mouse_motion_event_seen = false; while (SDL_PollEvent(&sdl_event)) { switch (sdl_event.type) { case SDL_EVENT_MOUSE_MOTION: { InputEvent::MouseMoveEvent ev; ipos32 screen_pos = WindowPosToScreenPos(active_renderer, {Settings.ScreenWidth, Settings.ScreenHeight}, {iround<int32_t>(sdl_event.motion.x), iround<int32_t>(sdl_event.motion.y)}); irect32 vp = active_renderer->GetViewPort(); float32_t x_ratio = numeric_cast<float32_t>(Settings.ScreenWidth) / numeric_cast<float32_t>(vp.width); float32_t y_ratio = numeric_cast<float32_t>(Settings.ScreenHeight) / numeric_cast<float32_t>(vp.height); ipos32 host_delta = ipos32 {iround<int32_t>(sdl_event.motion.xrel * x_ratio), iround<int32_t>(sdl_event.motion.yrel * y_ratio)}; switch_active_to_hovered_child(screen_pos); // Mouse events are pushed to the active client; remap host (ImGui display) coords into // the active virtual window's local screen coords so the client sees positions inside its own viewport. ipos32 local_pos = TranslateHostPosToActiveWindow(screen_pos); ipos32 local_delta = ScaleHostDeltaToActiveWindow(host_delta); ev.MouseX = local_pos.x; ev.MouseY = local_pos.y; ev.DeltaX = local_delta.x; ev.DeltaY = local_delta.y; mouse_motion_event_seen = true; _lastMouseMoveHostPos = screen_pos; _lastMouseMoveHostPosValid = true; if (!imgui_capture_mouse || host_pos_inside_active_virtual(screen_pos)) { _ctx->EventsQueue.emplace_back(ev); } io.AddMouseSourceEvent(sdl_event.motion.which == SDL_TOUCH_MOUSEID ? ImGuiMouseSource_TouchScreen : ImGuiMouseSource_Mouse); io.AddMousePosEvent(numeric_cast<float32_t>(screen_pos.x), numeric_cast<float32_t>(screen_pos.y)); } break; case SDL_EVENT_MOUSE_BUTTON_UP: case SDL_EVENT_MOUSE_BUTTON_DOWN: { ipos32 button_screen_pos = WindowPosToScreenPos(active_renderer, {Settings.ScreenWidth, Settings.ScreenHeight}, {iround<int32_t>(sdl_event.button.x), iround<int32_t>(sdl_event.button.y)}); switch_active_to_hovered_child(button_screen_pos); bool button_to_client = !imgui_capture_mouse || host_pos_inside_active_virtual(button_screen_pos); auto button_it = _ctx->MouseButtonsMap.find(sdl_event.button.button); if (button_it != _ctx->MouseButtonsMap.end()) { if (sdl_event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) { InputEvent::MouseDownEvent ev; ev.Button = button_it->second; if (button_to_client) { _ctx->EventsQueue.emplace_back(ev); } } else { InputEvent::MouseUpEvent ev; ev.Button = button_it->second; if (button_to_client) { _ctx->EventsQueue.emplace_back(ev); } } } int32_t mouse_button = -1; if (sdl_event.button.button == SDL_BUTTON_LEFT) { mouse_button = 0; } else if (sdl_event.button.button == SDL_BUTTON_RIGHT) { mouse_button = 1; } else if (sdl_event.button.button == SDL_BUTTON_MIDDLE) { mouse_button = 2; } else if (sdl_event.button.button == SDL_BUTTON_X1) { mouse_button = 3; } else if (sdl_event.button.button == SDL_BUTTON_X2) { mouse_button = 4; } if (mouse_button != -1) { io.AddMouseSourceEvent(sdl_event.button.which == SDL_TOUCH_MOUSEID ? ImGuiMouseSource_TouchScreen : ImGuiMouseSource_Mouse); io.AddMouseButtonEvent(mouse_button, (sdl_event.type == SDL_EVENT_MOUSE_BUTTON_DOWN)); _mouseButtonsDown = sdl_event.type == SDL_EVENT_MOUSE_BUTTON_DOWN ? (_mouseButtonsDown | (1 << mouse_button)) : (_mouseButtonsDown & ~(1 << mouse_button)); } } break; case SDL_EVENT_FINGER_MOTION: case SDL_EVENT_FINGER_DOWN: case SDL_EVENT_FINGER_UP: { auto finger_id = numeric_cast<int64_t>(sdl_event.tfinger.fingerID); ipos32 touch_pos = ResolveTouchPos(sdl_event.tfinger.x, sdl_event.tfinger.y); if (sdl_event.type == SDL_EVENT_FINGER_DOWN) { auto cur_time = SDL_GetPerformanceCounter(); if (_pendingTouchTap.Active) { uint32_t tap_elapsed = GetTouchElapsedMs(_pendingTouchTap.ReleaseTime, cur_time); float32_t tap_distance = GetTouchDistance(_pendingTouchTap.Pos, touch_pos); if (tap_elapsed >= TOUCH_DOUBLE_TAP_MAX_TIME_MS || tap_distance > numeric_cast<float32_t>(TOUCH_DOUBLE_TAP_MAX_DIST)) { QueueTouchTap(_pendingTouchTap.Pos); _pendingTouchTap = {}; } } auto touch = AcquireTouchPoint(finger_id); if (!touch) { ResetTouchGestures(); touch = AcquireTouchPoint(finger_id); } FO_VERIFY_AND_THROW(touch, "Failed to acquire a touch point slot"); touch->FingerId = finger_id; touch->StartPos = touch_pos; touch->LastPos = touch_pos; touch->StartTime = cur_time; touch->Active = true; touch->ScrollActive = false; if (!imgui_capture_mouse) { QueueTouchDown(finger_id, touch_pos); } if (auto other_touch = FindOtherTouchPoint(finger_id)) { _touchPinchActive = true; _touchTapSuppressed = true; _touchLastPinchDistance = GetTouchDistance(touch_pos, other_touch->LastPos); _pendingTouchTap = {}; } } else if (sdl_event.type == SDL_EVENT_FINGER_MOTION) { auto touch = FindTouchPoint(finger_id); if (!touch) { break; } ipos32 delta = ipos32 {touch_pos.x - touch->LastPos.x, touch_pos.y - touch->LastPos.y}; touch->LastPos = touch_pos; if (!imgui_capture_mouse && (delta.x != 0 || delta.y != 0)) { QueueTouchMove(finger_id, touch->LastPos, delta); } if (_touchPinchActive) { auto other_touch = FindOtherTouchPoint(finger_id); if (!other_touch) { _touchPinchActive = false; _touchLastPinchDistance = 0.0f; break; } float32_t pinch_distance = GetTouchDistance(touch->LastPos, other_touch->LastPos); if (_touchLastPinchDistance > 1.0f && pinch_distance > 1.0f) { float32_t factor = pinch_distance / _touchLastPinchDistance; if (!imgui_capture_mouse && std::abs(factor - 1.0f) > 0.01f) { QueueTouchZoom({(touch->LastPos.x + other_touch->LastPos.x) / 2, (touch->LastPos.y + other_touch->LastPos.y) / 2}, factor); } } _touchLastPinchDistance = pinch_distance; _pendingTouchTap = {}; } else { if (GetTouchDistance(touch->StartPos, touch->LastPos) > numeric_cast<float32_t>(TOUCH_TAP_MAX_DIST)) { touch->ScrollActive = true; } if (touch->ScrollActive) { _pendingTouchTap = {}; if (!imgui_capture_mouse && (delta.x != 0 || delta.y != 0)) { QueueTouchScroll(touch->LastPos, delta); } } } } else { auto touch = FindTouchPoint(finger_id); if (!touch) { break; } touch->LastPos = touch_pos; if (!imgui_capture_mouse) { QueueTouchUp(finger_id, touch->LastPos); } if (_touchPinchActive) { ReleaseTouchPoint(finger_id); _touchPinchActive = false; _touchLastPinchDistance = 0.0f; _pendingTouchTap = {}; if (auto remaining_touch = FindOtherTouchPoint(finger_id)) { auto cur_time = SDL_GetPerformanceCounter(); remaining_touch->StartPos = remaining_touch->LastPos; remaining_touch->StartTime = cur_time; remaining_touch->ScrollActive = false; } if (!_touchPrimary.Active && !_touchSecondary.Active) { _touchTapSuppressed = false; } break; } if (_touchTapSuppressed) { ReleaseTouchPoint(finger_id); if (!_touchPrimary.Active && !_touchSecondary.Active) { _touchTapSuppressed = false; } break; } auto cur_time = SDL_GetPerformanceCounter(); float32_t tap_distance = GetTouchDistance(touch->StartPos, touch->LastPos); uint32_t tap_elapsed = GetTouchElapsedMs(touch->StartTime, cur_time); bool is_tap = !touch->ScrollActive && tap_distance <= numeric_cast<float32_t>(TOUCH_TAP_MAX_DIST) && tap_elapsed <= TOUCH_TAP_MAX_TIME_MS; ReleaseTouchPoint(finger_id); if (!is_tap) { break; } if (_pendingTouchTap.Active) { uint32_t previous_tap_elapsed = GetTouchElapsedMs(_pendingTouchTap.ReleaseTime, cur_time); float32_t previous_tap_distance = GetTouchDistance(_pendingTouchTap.Pos, touch_pos); if (previous_tap_elapsed <= TOUCH_DOUBLE_TAP_MAX_TIME_MS && previous_tap_distance <= numeric_cast<float32_t>(TOUCH_DOUBLE_TAP_MAX_DIST)) { if (!imgui_capture_mouse) { QueueTouchDoubleTap(touch_pos); } _pendingTouchTap = {}; break; } QueueTouchTap(_pendingTouchTap.Pos); _pendingTouchTap = {}; } if (!imgui_capture_mouse) { _pendingTouchTap.Pos = touch_pos; _pendingTouchTap.ReleaseTime = cur_time; _pendingTouchTap.Active = true; } } } break; case SDL_EVENT_MOUSE_WHEEL: { InputEvent::MouseWheelEvent ev; ev.Delta = iround<int32_t>(sdl_event.wheel.y); ipos32 wheel_screen_pos = WindowPosToScreenPos(active_renderer, {Settings.ScreenWidth, Settings.ScreenHeight}, {iround<int32_t>(sdl_event.wheel.mouse_x), iround<int32_t>(sdl_event.wheel.mouse_y)}); switch_active_to_hovered_child(wheel_screen_pos); if (!imgui_capture_mouse || host_pos_inside_active_virtual(wheel_screen_pos)) { _ctx->EventsQueue.emplace_back(ev); } float32_t wheel_x = sdl_event.wheel.x > 0 ? 1.0f : (sdl_event.wheel.x < 0 ? -1.0f : 0.0f); float32_t wheel_y = sdl_event.wheel.y > 0 ? 1.0f : (sdl_event.wheel.y < 0 ? -1.0f : 0.0f); io.AddMouseSourceEvent(sdl_event.wheel.which == SDL_TOUCH_MOUSEID ? ImGuiMouseSource_TouchScreen : ImGuiMouseSource_Mouse); io.AddMouseWheelEvent(wheel_x, wheel_y); } break; case SDL_EVENT_GAMEPAD_ADDED: { RefreshGamepadConnection(); } break; case SDL_EVENT_GAMEPAD_REMOVED: { if (_gamepadInstanceId == numeric_cast<int32_t>(sdl_event.gdevice.which)) { CloseGamepad(); RefreshGamepadConnection(); } } break; case SDL_EVENT_GAMEPAD_AXIS_MOTION: { if (_gamepadInstanceId == numeric_cast<int32_t>(sdl_event.gaxis.which)) { UpdateGamepadAxis(sdl_event.gaxis.axis, sdl_event.gaxis.value); } } break; case SDL_EVENT_GAMEPAD_BUTTON_DOWN: case SDL_EVENT_GAMEPAD_BUTTON_UP: { if (_gamepadInstanceId == numeric_cast<int32_t>(sdl_event.gbutton.which)) { UpdateGamepadButton(sdl_event.gbutton.button, sdl_event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN); } } break; case SDL_EVENT_KEY_UP: case SDL_EVENT_KEY_DOWN: { if (sdl_event.type == SDL_EVENT_KEY_DOWN) { InputEvent::KeyDownEvent ev; ev.Code = _ctx->KeysMap[sdl_event.key.scancode]; if (!imgui_capture_keyboard) { _ctx->EventsQueue.emplace_back(ev); } #if !FO_WEB // Desktop paste: read system clipboard and inject as KeyCode::Text // On web, paste is handled by the JS capturing handler in WebRelated.cpp if (ev.Code == KeyCode::V && (sdl_event.key.mod & SDL_KMOD_CTRL) != 0 && !imgui_capture_keyboard) { InputEvent::KeyDownEvent paste_ev; paste_ev.Code = KeyCode::Text; auto sdl_clipboard_text = make_nptr(SDL_GetClipboardText()); if (sdl_clipboard_text) { auto clipboard_text = make_unique_del_ptr(sdl_clipboard_text, [](char* raw_data) { FO_NO_STACK_TRACE_ENTRY(); if (raw_data != nullptr) { auto data = make_ptr(raw_data); SDL_free(data.get()); } }); paste_ev.Text = string {clipboard_text.get()}; } else { paste_ev.Text = {}; } _ctx->EventsQueue.emplace_back(paste_ev); InputEvent::KeyUpEvent paste_ev2; paste_ev2.Code = KeyCode::Text; _ctx->EventsQueue.emplace_back(paste_ev2); } #endif if (ev.Code == KeyCode::Escape && io.KeyShift) { RequestQuit(); } } else { InputEvent::KeyUpEvent ev; ev.Code = _ctx->KeysMap[sdl_event.key.scancode]; if (!imgui_capture_keyboard) { _ctx->EventsQueue.emplace_back(ev); } } auto sdl_key_mods = sdl_event.key.mod; Input._ctrlDown = (sdl_key_mods & SDL_KMOD_CTRL) != 0; Input._shiftDown = (sdl_key_mods & SDL_KMOD_SHIFT) != 0; Input._altDown = (sdl_key_mods & SDL_KMOD_ALT) != 0; io.AddKeyEvent(ImGuiMod_Ctrl, Input._ctrlDown); io.AddKeyEvent(ImGuiMod_Shift, Input._shiftDown); io.AddKeyEvent(ImGuiMod_Alt, Input._altDown); io.AddKeyEvent(ImGuiMod_Super, (sdl_key_mods & SDL_KMOD_GUI) != 0); ImGuiKey key = KeycodeToImGuiKey(sdl_event.key.key); io.AddKeyEvent(key, sdl_event.type == SDL_EVENT_KEY_DOWN); io.SetKeyEventNativeData(key, numeric_cast<int32_t>(sdl_event.key.key), sdl_event.key.scancode, sdl_event.key.scancode); } break; case SDL_EVENT_TEXT_INPUT: { InputEvent::KeyDownEvent ev1; ev1.Code = KeyCode::Text; ev1.Text = sdl_event.text.text; if (!imgui_capture_keyboard) { _ctx->EventsQueue.emplace_back(ev1); } InputEvent::KeyUpEvent ev2; ev2.Code = KeyCode::Text; if (!imgui_capture_keyboard) { _ctx->EventsQueue.emplace_back(ev2); } io.AddInputCharactersUTF8(sdl_event.text.text); } break; case SDL_EVENT_DROP_TEXT: { InputEvent::KeyDownEvent ev1; ev1.Code = KeyCode::Text; ev1.Text = sdl_event.drop.data; _ctx->EventsQueue.emplace_back(ev1); InputEvent::KeyUpEvent ev2; ev2.Code = KeyCode::Text; _ctx->EventsQueue.emplace_back(ev2); } break; case SDL_EVENT_DROP_FILE: { if (auto file_size = fs_file_size(sdl_event.drop.data)) { std::ifstream file {fs_open_ifstream(sdl_event.drop.data)}; if (!file) { break; } bool stripped = false; auto size = numeric_cast<size_t>(*file_size); if (size > AppInput::DROP_FILE_STRIP_LENGHT) { stripped = true; size = AppInput::DROP_FILE_STRIP_LENGHT; } char buf[AppInput::DROP_FILE_STRIP_LENGHT + 1]; if (size == 0 || (file.read(buf, static_cast<std::streamsize>(size)) && file.gcount() == static_cast<std::streamsize>(size))) { buf[size] = 0; InputEvent::KeyDownEvent ev1; ev1.Code = KeyCode::Text; ev1.Text = strex("{}\n{}{}", sdl_event.drop.data, buf, stripped ? "..." : ""); _ctx->EventsQueue.emplace_back(ev1); InputEvent::KeyUpEvent ev2; ev2.Code = KeyCode::Text; _ctx->EventsQueue.emplace_back(ev2); } } } break; case SDL_EVENT_TEXT_EDITING: { } break; case SDL_EVENT_DID_ENTER_FOREGROUND: { _onPauseDispatcher(); } break; case SDL_EVENT_DID_ENTER_BACKGROUND: { _onResumeDispatcher(); } break; case SDL_EVENT_LOW_MEMORY: { _onLowMemoryDispatcher(); } break; case SDL_EVENT_WINDOW_CLOSE_REQUESTED: { RequestQuit(); } break; case SDL_EVENT_WINDOW_MOUSE_ENTER: { _pendingMouseLeaveFrame = 0; } break; case SDL_EVENT_WINDOW_MOUSE_LEAVE: { _pendingMouseLeaveFrame = ImGui::GetFrameCount() + 1; } break; case SDL_EVENT_WINDOW_FOCUS_GAINED: { io.AddFocusEvent(true); } break; case SDL_EVENT_WINDOW_FOCUS_LOST: { io.AddFocusEvent(false); Input._shiftDown = false; Input._ctrlDown = false; Input._altDown = false; _lastMouseMoveHostPosValid = false; ResetTouchGestures(); } break; case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: { auto resized_window = make_nptr(SDL_GetWindowFromID(sdl_event.window.windowID)); FO_VERIFY_AND_THROW(resized_window, "Resize event references an unknown window id"); int32_t width = 0; int32_t height = 0; SDL_GetWindowSizeInPixels(resized_window.get(), &width, &height); auto main_sdl_window = MainWindow._windowHandle.reinterpret_as<SDL_Window>(); FO_VERIFY_AND_THROW(main_sdl_window, "Window handle does not reference a valid SDL window"); bool is_main = resized_window == main_sdl_window.get(); isize32 event_size {width, height}; bool fullscreen_backbuffer_resize = is_main && (Settings.Fullscreen || _mainWindowFullscreenBackbufferMode || MainWindow.IsFullscreen() || _mainWindowFullscreenTransition || IsMainWindowDisplayModeSize(event_size)); bool screen_size_changed = false; bool update_logical_size = is_main && !fullscreen_backbuffer_resize; if (update_logical_size && (Settings.ScreenWidth != width || Settings.ScreenHeight != height)) { isize32 old_host {Settings.ScreenWidth, Settings.ScreenHeight}; isize32 new_host = event_size; Settings.ScreenWidth = width; Settings.ScreenHeight = height; MainWindow._virtualSize = new_host; MainWindow._virtualScreenSize = new_host; screen_size_changed = true; if (old_host.width > 0 && old_host.height > 0) { auto scale = [](int32_t value, int32_t old_dim, int32_t new_dim) { // return iround<int32_t>(numeric_cast<float32_t>(value) * numeric_cast<float32_t>(new_dim) / numeric_cast<float32_t>(old_dim)); }; for (ptr<AppWindow> window : _allWindows) { if (window->_isVirtual && window->_displayRect.width > 0 && window->_displayRect.height > 0) { irect32 r = window->_displayRect; window->_displayRect = irect32 { // scale(r.x, old_host.width, new_host.width), // scale(r.y, old_host.height, new_host.height), // scale(r.width, old_host.width, new_host.width), // scale(r.height, old_host.height, new_host.height), }; } } } } isize32 backbuffer_size = event_size; if (backbuffer_size.width > 0 && backbuffer_size.height > 0) { active_renderer->OnResizeWindow(backbuffer_size); } for (ptr<AppWindow> window : copy(_allWindows)) { if ((window->_windowHandle ? window->_windowHandle.reinterpret_as<SDL_Window>() : nullptr) == resized_window) { window->_onWindowSizeChangedDispatcher(); if (screen_size_changed || !is_main) { window->_onScreenSizeChangedDispatcher(); } } } } break; case SDL_EVENT_WINDOW_DISPLAY_CHANGED: case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED: { auto window = make_nptr(SDL_GetWindowFromID(sdl_event.window.windowID)); FO_VERIFY_AND_THROW(window, "Display change event references an unknown window id"); auto display_id = SDL_GetDisplayForWindow(window.get()); auto display_mode = GetSdlDisplayMode(display_id); UpdateMonitorSettings(Settings, display_mode); } break; case SDL_EVENT_QUIT: { RequestQuit(); } break; case SDL_EVENT_TERMINATING: { ExitApp(true); } default: break; } } if (_mainWindowFullscreenTransition && IsMainWindowActuallyFullscreen() == Settings.Fullscreen) { _mainWindowFullscreenTransition = false; } FlushPendingTouchTap(); // Setup display size io.DisplaySize = ImVec2(numeric_cast<float32_t>(Settings.ScreenWidth), numeric_cast<float32_t>(Settings.ScreenHeight)); // Setup time step uint64_t cur_time = SDL_GetPerformanceCounter(); io.DeltaTime = numeric_cast<float32_t>(numeric_cast<float64_t>(cur_time - _time) / numeric_cast<float64_t>(_timeFrequency)); _time = cur_time; // Mouse state if (_ctx->ActiveRendererType != RenderType::Null) { if (_pendingMouseLeaveFrame != 0 && _pendingMouseLeaveFrame >= ImGui::GetFrameCount() && _mouseButtonsDown == 0) { io.AddMousePosEvent(-std::numeric_limits<float32_t>::max(), -std::numeric_limits<float32_t>::max()); _pendingMouseLeaveFrame = 0; } auto main_sdl_window = MainWindow._windowHandle.reinterpret_as<SDL_Window>(); FO_VERIFY_AND_THROW(main_sdl_window, "Window handle does not reference a valid SDL window"); SDL_SetWindowMouseGrab(main_sdl_window.get(), MainWindow._grabbed); #if FO_WINDOWS || FO_LINUX || FO_MAC bool is_app_focused = main_sdl_window == SDL_GetKeyboardFocus(); #else const bool is_app_focused = (SDL_GetWindowFlags(main_sdl_window.get()) & SDL_WINDOW_INPUT_FOCUS) != 0; #endif if (is_app_focused) { if (!Input.IsMouseAvailable()) { if (_mouseButtonsDown == 0) { io.AddMousePosEvent(-std::numeric_limits<float32_t>::max(), -std::numeric_limits<float32_t>::max()); } } else { if (io.WantSetMousePos) { Input.SetMousePosition({iround<int32_t>(io.MousePos.x), iround<int32_t>(io.MousePos.y)}, &MainWindow); } if (_mouseCanUseGlobalState && _mouseButtonsDown == 0) { float32_t mouse_x_global; float32_t mouse_y_global; SDL_GetGlobalMouseState(&mouse_x_global, &mouse_y_global); int32_t window_x; int32_t window_y; SDL_GetWindowPosition(main_sdl_window.get(), &window_x, &window_y); ipos32 screen_pos = WindowPosToScreenPos(active_renderer, {Settings.ScreenWidth, Settings.ScreenHeight}, {iround<int32_t>(mouse_x_global) - window_x, iround<int32_t>(mouse_y_global) - window_y}); if (!mouse_motion_event_seen && (!_lastMouseMoveHostPosValid || screen_pos != _lastMouseMoveHostPos)) { InputEvent::MouseMoveEvent ev; ipos32 host_delta = _lastMouseMoveHostPosValid ? ipos32 {screen_pos.x - _lastMouseMoveHostPos.x, screen_pos.y - _lastMouseMoveHostPos.y} : ipos32 {}; switch_active_to_hovered_child(screen_pos); ipos32 local_pos = TranslateHostPosToActiveWindow(screen_pos); ipos32 local_delta = ScaleHostDeltaToActiveWindow(host_delta); ev.MouseX = local_pos.x; ev.MouseY = local_pos.y; ev.DeltaX = local_delta.x; ev.DeltaY = local_delta.y; if (!imgui_capture_mouse || host_pos_inside_active_virtual(screen_pos)) { _ctx->EventsQueue.emplace_back(ev); } } _lastMouseMoveHostPos = screen_pos; _lastMouseMoveHostPosValid = true; io.AddMousePosEvent(numeric_cast<float32_t>(screen_pos.x), numeric_cast<float32_t>(screen_pos.y)); } } } } else { io.AddMousePosEvent(numeric_cast<float32_t>(Settings.ScreenWidth / 2), numeric_cast<float32_t>(Settings.ScreenHeight / 2)); } ImGui::NewFrame(); _onFrameBeginDispatcher(); } void Application::EndFrame() { FO_STACK_TRACE_ENTRY(); if (_currentRenderingWindow) { EndWindowRender(); } FO_VERIFY_AND_THROW(_ctx->RenderTargetTex == nullptr, "Context render target tex must be unset before this operation"); auto active_renderer = GetActiveRenderer(_ctx); // Skip unprocessed events _ctx->EventsQueue.clear(); // Render ImGui ImGui::Render(); UpdateNativeCursorVisibility(ImGui::GetIO().WantCaptureMouse || ImGui::GetIO().WantTextInput); auto draw_data = make_nptr(ImGui::GetDrawData()); FO_VERIFY_AND_THROW(draw_data, "ImGui produced no draw data after rendering"); if (draw_data->Textures != nullptr) { for (ptr<ImTextureData> im_tex : *draw_data->Textures) { if (im_tex->Status != ImTextureStatus_OK) { if (im_tex->Status == ImTextureStatus_WantCreate) { auto tex_size = isize(im_tex->Width, im_tex->Height); FO_VERIFY_AND_THROW(tex_size.square() * 4 == numeric_cast<size_t>(im_tex->GetSizeInBytes()), "ImGui texture byte size does not match square RGBA texture dimensions", tex_size.square() * 4, im_tex->GetSizeInBytes()); auto font_tex = active_renderer->CreateTexture(tex_size, true, false); auto tex_data = cast_from_void<const ucolor*>(im_tex->GetPixels()); FO_VERIFY_AND_THROW(tex_data, "ImGui texture pixel data is null"); size_t tex_pixels_count = numeric_cast<size_t>(tex_size.width) * tex_size.height; auto tex_data_ptr = tex_data.as_ptr(); auto tex_pixels = make_span(tex_data_ptr, tex_pixels_count); font_tex->UpdateTextureRegion({}, tex_size, tex_pixels); im_tex->SetTexID(make_nptr(font_tex.get()).void_cast()); im_tex->SetStatus(ImTextureStatus_OK); _imguiTextures.emplace_back(std::move(font_tex)); } else if (im_tex->Status == ImTextureStatus_WantUpdates) { ipos32 update_pos = ipos32(im_tex->UpdateRect.x, im_tex->UpdateRect.y); isize32 update_size = isize32(im_tex->UpdateRect.w, im_tex->UpdateRect.h); auto update_data = cast_from_void<const ucolor*>(im_tex->GetPixelsAt(update_pos.x, update_pos.y)); FO_VERIFY_AND_THROW(update_data, "ImGui texture pixel data is null"); size_t update_pitch = numeric_cast<size_t>(im_tex->Width); size_t update_size_in_pixels = update_size.height != 0 ? (numeric_cast<size_t>(update_size.height - 1) * update_pitch) + numeric_cast<size_t>(update_size.width) : 0; auto tex = cast_from_void<RenderTexture*>(im_tex->GetTexID()); FO_VERIFY_AND_THROW(tex, "ImGui texture id does not reference a render texture"); auto update_data_ptr = update_data.as_ptr(); auto update_pixels = make_span(update_data_ptr, update_size_in_pixels); tex->UpdateTextureRegion(update_pos, update_size, update_pixels, true); im_tex->SetStatus(ImTextureStatus_OK); } else if (im_tex->Status == ImTextureStatus_WantDestroy) { auto tex = cast_from_void<RenderTexture*>(im_tex->GetTexID()); FO_VERIFY_AND_THROW(tex, "ImGui texture id does not reference a render texture"); auto it = std::find(_imguiTextures.begin(), _imguiTextures.end(), tex.get()); FO_VERIFY_AND_THROW(it != _imguiTextures.end(), "Lookup failed in imgui textures"); _imguiTextures.erase(it); im_tex->SetTexID(ImTextureID_Invalid); im_tex->SetStatus(ImTextureStatus_Destroyed); } } } } int32_t fb_width = iround<int32_t>(draw_data->DisplaySize.x * draw_data->FramebufferScale.x); int32_t fb_height = iround<int32_t>(draw_data->DisplaySize.y * draw_data->FramebufferScale.y); if (_imguiEffect && _imguiDrawBuf && fb_width > 0 && fb_height > 0) { // Scissor/clipping auto clip_off = draw_data->DisplayPos; auto clip_scale = draw_data->FramebufferScale; // Render command lists for (int32_t cmd = 0; cmd < draw_data->CmdListsCount; cmd++) { ptr<const ImDrawList> cmd_list = draw_data->CmdLists[cmd]; _imguiDrawBuf->Vertices.resize(cmd_list->VtxBuffer.Size); _imguiDrawBuf->VertCount = _imguiDrawBuf->Vertices.size(); for (int32_t i = 0; i < cmd_list->VtxBuffer.Size; i++) { auto& v = _imguiDrawBuf->Vertices[i]; const auto& iv = cmd_list->VtxBuffer[i]; v.PosX = iv.pos.x; v.PosY = iv.pos.y; v.TexU = iv.uv.x; v.TexV = iv.uv.y; v.Color = ucolor {iv.col}; } _imguiDrawBuf->Indices.resize(cmd_list->IdxBuffer.Size); _imguiDrawBuf->IndCount = _imguiDrawBuf->Indices.size(); for (int32_t i = 0; i < cmd_list->IdxBuffer.Size; i++) { _imguiDrawBuf->Indices[i] = numeric_cast<vindex_t>(cmd_list->IdxBuffer[i]); } _imguiDrawBuf->Upload(_imguiEffect->GetUsage()); for (int32_t cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++) { auto pcmd = make_ptr(&cmd_list->CmdBuffer[cmd_i]); FO_VERIFY_AND_THROW(pcmd->UserCallback == nullptr, "Unexpected ImGui user callback in draw command"); int32_t clip_rect_l = iround<int32_t>((pcmd->ClipRect.x - clip_off.x) * clip_scale.x); int32_t clip_rect_t = iround<int32_t>((pcmd->ClipRect.y - clip_off.y) * clip_scale.y); int32_t clip_rect_r = iround<int32_t>((pcmd->ClipRect.z - clip_off.x) * clip_scale.x); int32_t clip_rect_b = iround<int32_t>((pcmd->ClipRect.w - clip_off.y) * clip_scale.y); if (clip_rect_l < fb_width && clip_rect_t < fb_height && clip_rect_r >= 0 && clip_rect_b >= 0) { active_renderer->EnableScissor({clip_rect_l, clip_rect_t, clip_rect_r - clip_rect_l, clip_rect_b - clip_rect_t}); auto texture = cast_from_void<RenderTexture*>(pcmd->TexRef.GetTexID()); FO_VERIFY_AND_THROW(texture, "ImGui texture id does not reference a render texture"); _imguiEffect->DrawBuffer(_imguiDrawBuf, pcmd->IdxOffset, pcmd->ElemCount, texture); active_renderer->DisableScissor(); } } } } // Frame complete, swap buffers active_renderer->Present(); _onFrameEndDispatcher(); #if FO_TRACY FrameMark; #endif } auto Application::IsHeadless() const noexcept -> bool { FO_NO_STACK_TRACE_ENTRY(); return false; } auto Application::IsQuitRequested() const -> bool { FO_NO_STACK_TRACE_ENTRY(); return _quit || IsQuitSignalReceived(); } void Application::RequestQuit(bool success) noexcept { FO_STACK_TRACE_ENTRY(); if (!success) { _quitSuccess.store(false); } if (bool expected = false; _quit.compare_exchange_strong(expected, true)) { WriteLog("Quit requested"); _onQuitDispatcher(); _quitEvent.notify_all(); } } void Application::WaitForRequestedQuit() { FO_STACK_TRACE_ENTRY(); unique_lock locker {_quitLocker}; while (!_quit) { // Timed wait: the signal handler only latches the quit-signal flag (async-signal-safe), so this // waiting thread is the one that converts it into a regular RequestQuit with logging/notification. _quitEvent.wait_for(locker, std::chrono::milliseconds {100}); if (IsQuitSignalReceived()) { RequestQuit(); } } } auto AppWindow::GetSize() const -> isize32 { FO_STACK_TRACE_ENTRY(); if (_isVirtual) { return _virtualSize.width > 0 && _virtualSize.height > 0 ? _virtualSize : isize32 {_app->Settings.ScreenWidth, _app->Settings.ScreenHeight}; } if (_app->_ctx->ActiveRendererType != RenderType::Null) { int32_t width = 1000; int32_t height = 1000; auto sdl_window = ResolveWindowHandle().reinterpret_as<SDL_Window>(); SDL_GetWindowSizeInPixels(sdl_window.get(), &width, &height); return {width, height}; } return ResolveWindowStub()->Size; } void AppWindow::SetSize(isize32 size) { FO_STACK_TRACE_ENTRY(); if (_isVirtual) { _virtualSize = size; _onWindowSizeChangedDispatcher(); return; } if (_app->_ctx->ActiveRendererType != RenderType::Null) { auto sdl_window = ResolveWindowHandle().reinterpret_as<SDL_Window>(); SDL_SetWindowSize(sdl_window.get(), size.width, size.height); } else { ResolveWindowStub()->Size = size; _onWindowSizeChangedDispatcher(); } } auto AppWindow::GetScreenSize() const -> isize32 { FO_STACK_TRACE_ENTRY(); if (_isVirtual) { return _virtualScreenSize.width > 0 && _virtualScreenSize.height > 0 ? _virtualScreenSize : GetSize(); } return {_app->Settings.ScreenWidth, _app->Settings.ScreenHeight}; } void AppWindow::SetScreenSize(isize32 size) { FO_STACK_TRACE_ENTRY(); if (_isVirtual) { if (size != _virtualScreenSize) { _virtualScreenSize = size; _onScreenSizeChangedDispatcher(); } } else { if (size.width != _app->Settings.ScreenWidth || size.height != _app->Settings.ScreenHeight) { _app->Settings.ScreenWidth = size.width; _app->Settings.ScreenHeight = size.height; WebRelated::ApplyCanvasLayout(_app->Settings); _onScreenSizeChangedDispatcher(); } } } auto AppWindow::GetPosition() const -> ipos32 { FO_STACK_TRACE_ENTRY(); if (_isVirtual) { return _virtualPosition; } if (_app->_ctx->ActiveRendererType != RenderType::Null) { int32_t x = 0; int32_t y = 0; auto sdl_window = ResolveWindowHandle().reinterpret_as<SDL_Window>(); SDL_GetWindowPosition(sdl_window.get(), &x, &y); return {x, y}; } return ResolveWindowStub()->Position; } void AppWindow::SetPosition(ipos32 pos) { FO_STACK_TRACE_ENTRY(); if (_isVirtual) { _virtualPosition = pos; return; } if (_app->_ctx->ActiveRendererType != RenderType::Null) { auto sdl_window = ResolveWindowHandle().reinterpret_as<SDL_Window>(); SDL_SetWindowPosition(sdl_window.get(), pos.x, pos.y); } else { ResolveWindowStub()->Position = pos; } } auto AppWindow::IsFocused() const -> bool { FO_STACK_TRACE_ENTRY(); if (_isVirtual) { return _app->_activeWindow == this; } if (_app->_ctx->ActiveRendererType != RenderType::Null) { auto sdl_window = ResolveWindowHandle().reinterpret_as<SDL_Window>(); return (SDL_GetWindowFlags(sdl_window.get()) & SDL_WINDOW_INPUT_FOCUS) != 0; } return true; } void AppWindow::Minimize() { FO_STACK_TRACE_ENTRY(); if (_isVirtual) { _app->MainWindow.Minimize(); return; } if (_app->_ctx->ActiveRendererType != RenderType::Null) { auto sdl_window = ResolveWindowHandle().reinterpret_as<SDL_Window>(); SDL_MinimizeWindow(sdl_window.get()); } else { ResolveWindowStub()->Minimized = true; } } auto AppWindow::IsFullscreen() const -> bool { FO_STACK_TRACE_ENTRY(); if (_isVirtual) { return _app->MainWindow.IsFullscreen(); } if (_app->_ctx->ActiveRendererType != RenderType::Null) { if (this == &_app->MainWindow) { return _app->_mainWindowFullscreenTransition ? _app->Settings.Fullscreen : _app->IsMainWindowActuallyFullscreen(); } auto sdl_window = ResolveWindowHandle().reinterpret_as<SDL_Window>(); return (SDL_GetWindowFlags(sdl_window.get()) & SDL_WINDOW_FULLSCREEN) != 0; } return ResolveWindowStub()->Fullscreen; } auto AppWindow::ToggleFullscreen(bool enable) -> bool { FO_STACK_TRACE_ENTRY(); if (_isVirtual) { return _app->MainWindow.ToggleFullscreen(enable); } if (_app->_ctx->ActiveRendererType == RenderType::Null) { auto window = ResolveWindowStub(); bool changed = window->Fullscreen != enable; window->Fullscreen = enable; _app->Settings.Fullscreen = enable; return changed; } if (this == &_app->MainWindow && _app->_mainWindowFullscreenTransition) { return false; } bool is_fullscreen = IsFullscreen(); auto sdl_window = ResolveWindowHandle().reinterpret_as<SDL_Window>(); if (is_fullscreen == enable) { _app->Settings.Fullscreen = is_fullscreen; if (this == &_app->MainWindow) { _app->_mainWindowFullscreenBackbufferMode = enable; } return false; } if (!is_fullscreen && enable) { bool previous_setting = _app->Settings.Fullscreen; bool previous_backbuffer_mode = _app->_mainWindowFullscreenBackbufferMode; _app->_mainWindowFullscreenTransition = true; _app->Settings.Fullscreen = true; _app->_mainWindowFullscreenBackbufferMode = true; bool result = SDL_SetWindowFullscreen(sdl_window.get(), true); if (result) { SDL_SyncWindow(sdl_window.get()); _app->SyncMainWindowBackbufferSize(); return true; } _app->Settings.Fullscreen = previous_setting; _app->_mainWindowFullscreenBackbufferMode = previous_backbuffer_mode; _app->_mainWindowFullscreenTransition = false; } else if (is_fullscreen && !enable) { bool previous_setting = _app->Settings.Fullscreen; _app->_mainWindowFullscreenTransition = true; _app->Settings.Fullscreen = false; bool result = SDL_SetWindowFullscreen(sdl_window.get(), false); if (result) { SDL_SyncWindow(sdl_window.get()); _app->_mainWindowFullscreenBackbufferMode = false; _app->SyncMainWindowBackbufferSize(); return true; } _app->Settings.Fullscreen = previous_setting; _app->_mainWindowFullscreenTransition = false; } return false; } void AppWindow::Blink() { FO_STACK_TRACE_ENTRY(); if (_isVirtual) { _app->MainWindow.Blink(); return; } if (_app->_ctx->ActiveRendererType == RenderType::Null) { return; } auto sdl_window = ResolveWindowHandle().reinterpret_as<SDL_Window>(); SDL_FlashWindow(sdl_window.get(), SDL_FLASH_UNTIL_FOCUSED); } void AppWindow::AlwaysOnTop(bool enable) { FO_STACK_TRACE_ENTRY(); if (_isVirtual) { _app->MainWindow.AlwaysOnTop(enable); return; } if (_app->_ctx->ActiveRendererType == RenderType::Null) { ResolveWindowStub()->AlwaysOnTop = enable; return; } auto sdl_window = ResolveWindowHandle().reinterpret_as<SDL_Window>(); SDL_SetWindowAlwaysOnTop(sdl_window.get(), enable); } void AppWindow::SetTitle(string_view title) { FO_STACK_TRACE_ENTRY(); _title = string {title}; // Virtual windows show the title in the engine's tab bar; only OS windows need to push it down to SDL. if (!_isVirtual && _windowHandle && _app->_ctx->ActiveRendererType != RenderType::Null) { auto title_ptr = make_ptr(_title.c_str()); auto sdl_window = _windowHandle.reinterpret_as<SDL_Window>(); FO_VERIFY_AND_THROW(sdl_window, "Window handle does not reference a valid SDL window"); SDL_SetWindowTitle(sdl_window.get(), title_ptr.get()); } } void AppWindow::GrabInput(bool enable) { FO_STACK_TRACE_ENTRY(); _grabbed = enable; } auto AppWindow::ResolveWindowHandle() const -> ptr<WindowInternalHandle> { FO_STACK_TRACE_ENTRY(); FO_VERIFY_AND_THROW(_windowHandle, "Missing native window handle"); ptr<WindowInternalHandle> window_handle {_windowHandle.get_no_const()}; return window_handle; } auto AppWindow::ResolveWindowStub() const -> ptr<HeadlessWindowStub> { FO_STACK_TRACE_ENTRY(); return ResolveWindowHandle().reinterpret_as<HeadlessWindowStub>(); } void AppWindow::Destroy() { FO_STACK_TRACE_ENTRY(); if (_isVirtual) { _app->DestroyChildWindow(this); return; } if (_app->_ctx->ActiveRendererType == RenderType::Null) { if (_windowHandle && this != &_app->MainWindow) { auto window_stub = _windowHandle.reinterpret_as<const HeadlessWindowStub>(); std::erase_if(_app->_ctx->NullWindowStubs, [window_stub](const auto& entry) { return window_stub == entry; }); _windowHandle = nullptr; } return; } if (_windowHandle && this != &_app->MainWindow) { auto sdl_window = _windowHandle.reinterpret_as<SDL_Window>(); FO_VERIFY_AND_THROW(sdl_window, "Window handle does not reference a valid SDL window"); auto owned_window = MakeSdlWindowHolder(sdl_window); ignore_unused(owned_window); _windowHandle = nullptr; } } auto AppRender::CreateTexture(isize32 size, bool linear_filtered, bool with_depth) -> unique_ptr<RenderTexture> { FO_STACK_TRACE_ENTRY(); auto active_renderer = GetActiveRenderer(_app->_ctx); return active_renderer->CreateTexture(size, linear_filtered, with_depth); } void AppRender::SetRenderTarget(nptr<RenderTexture> tex) { FO_STACK_TRACE_ENTRY(); // While a virtual window is active, redirect the implicit "back buffer" target (nullptr) // to the window's offscreen texture so the existing render-target stack walks back into it. if (!tex) { if (auto virt = _app->_currentRenderingWindow; virt) { if (virt->IsVirtual()) { if (auto virt_tex = virt->GetRenderTexture()) { tex = virt_tex; } } } } auto active_renderer = GetActiveRenderer(_app->_ctx); active_renderer->SetRenderTarget(tex); _app->_ctx->RenderTargetTex = tex; } auto AppRender::GetRenderTarget() -> nptr<RenderTexture> { FO_STACK_TRACE_ENTRY(); return _app->_ctx->RenderTargetTex; } void AppRender::ClearRenderTarget(optional<ucolor> color, bool depth, bool stencil) { FO_STACK_TRACE_ENTRY(); auto active_renderer = GetActiveRenderer(_app->_ctx); active_renderer->ClearRenderTarget(color, depth, stencil); } void AppRender::EnableScissor(irect32 rect) { FO_STACK_TRACE_ENTRY(); auto active_renderer = GetActiveRenderer(_app->_ctx); active_renderer->EnableScissor(rect); } void AppRender::DisableScissor() { FO_STACK_TRACE_ENTRY(); auto active_renderer = GetActiveRenderer(_app->_ctx); active_renderer->DisableScissor(); } auto AppRender::CreateDrawBuffer(bool is_static) -> unique_ptr<RenderDrawBuffer> { FO_STACK_TRACE_ENTRY(); auto active_renderer = GetActiveRenderer(_app->_ctx); return active_renderer->CreateDrawBuffer(is_static); } auto AppRender::CreateEffect(EffectUsage usage, string_view name, const RenderEffectLoader& loader) -> unique_ptr<RenderEffect> { FO_STACK_TRACE_ENTRY(); auto active_renderer = GetActiveRenderer(_app->_ctx); return active_renderer->CreateEffect(usage, name, loader); } auto AppRender::CreateOrthoMatrix(float32_t left, float32_t right, float32_t bottom, float32_t top, float32_t nearp, float32_t farp) const -> mat44 { FO_STACK_TRACE_ENTRY(); auto active_renderer = GetActiveRenderer(_app->_ctx); return active_renderer->CreateOrthoMatrix(left, right, bottom, top, nearp, farp); } auto AppRender::IsRenderTargetFlipped() const -> bool { FO_STACK_TRACE_ENTRY(); auto active_renderer = GetActiveRenderer(_app->_ctx); return active_renderer->IsRenderTargetFlipped(); } auto AppRender::GetProjMatrix() const -> mat44 { FO_STACK_TRACE_ENTRY(); auto active_renderer = GetActiveRenderer(_app->_ctx); return active_renderer->GetProjMatrix(); } void AppRender::SetOrthoDepthRange(float32_t nearp, float32_t farp) noexcept { FO_STACK_TRACE_ENTRY(); auto active_renderer = GetActiveRenderer(_app->_ctx); active_renderer->SetOrthoDepthRange(nearp, farp); } auto AppInput::IsMouseAvailable() const noexcept -> bool { FO_STACK_TRACE_ENTRY(); return _app->_ctx->ActiveRendererType != RenderType::Null && !_app->_isTablet; } auto AppInput::GetGamepadState() const noexcept -> GamepadState { FO_STACK_TRACE_ENTRY(); return _app->_gamepadState; } auto AppInput::GetMousePosition() const -> ipos32 { FO_STACK_TRACE_ENTRY(); if (!IsMouseAvailable()) { return _lastMousePos; } float32_t x = 100; float32_t y = 100; if (_app->_ctx->ActiveRendererType != RenderType::Null && _app->_mouseCanUseGlobalState && _app->MainWindow.IsFocused()) { float32_t mouse_x_global; float32_t mouse_y_global; SDL_GetGlobalMouseState(&mouse_x_global, &mouse_y_global); int32_t window_x; int32_t window_y; auto sdl_window = _app->MainWindow._windowHandle.reinterpret_as<SDL_Window>(); FO_VERIFY_AND_THROW(sdl_window, "Window handle does not reference a valid SDL window"); SDL_GetWindowPosition(sdl_window.get(), &window_x, &window_y); x = mouse_x_global - numeric_cast<float32_t>(window_x); y = mouse_y_global - numeric_cast<float32_t>(window_y); } else if (_app->_ctx->ActiveRendererType != RenderType::Null) { SDL_GetMouseState(&x, &y); } auto active_renderer = GetActiveRenderer(_app->_ctx); ipos32 host_pos = WindowPosToScreenPos(active_renderer, {_app->Settings.ScreenWidth, _app->Settings.ScreenHeight}, {iround<int32_t>(x), iround<int32_t>(y)}); return _app->TranslateHostPosToActiveWindow(host_pos); } void AppInput::SetMousePosition(ipos32 pos, nptr<const IAppWindow> relative_to) { FO_STACK_TRACE_ENTRY(); if (!IsMouseAvailable()) { return; } if (_app->_ctx->ActiveRendererType != RenderType::Null) { _lastMousePos = pos; SDL_SetEventEnabled(SDL_EVENT_MOUSE_MOTION, false); auto restore_mouse_motion = scope_exit([]() noexcept { SDL_SetEventEnabled(SDL_EVENT_MOUSE_MOTION, true); }); // When the active window is virtual, `pos` is in that window's local screen coords — // remap it back into host (ImGui display) coords before handing it to SDL. ipos32 host_pos = _app->TranslateActiveWindowPosToHost(pos); if (relative_to) { auto active_renderer = GetActiveRenderer(_app->_ctx); ipos32 window_pos = ScreenPosToWindowPos(active_renderer, {_app->Settings.ScreenWidth, _app->Settings.ScreenHeight}, host_pos); if (auto handle = relative_to->GetWindowHandleForInput(); handle) { auto sdl_window = handle.reinterpret_as<SDL_Window>(); FO_VERIFY_AND_THROW(sdl_window, "Window handle does not reference a valid SDL window"); SDL_WarpMouseInWindow(sdl_window.get(), numeric_cast<float32_t>(window_pos.x), numeric_cast<float32_t>(window_pos.y)); } } else { SDL_WarpMouseGlobal(numeric_cast<float32_t>(host_pos.x), numeric_cast<float32_t>(host_pos.y)); } } } auto AppInput::PollEvent(InputEvent& ev) -> bool { FO_STACK_TRACE_ENTRY(); if (!_app->_ctx->EventsQueue.empty()) { ev = _app->_ctx->EventsQueue.front(); _app->_ctx->EventsQueue.erase(_app->_ctx->EventsQueue.begin()); return true; } return false; } void AppInput::ClearEvents() { FO_STACK_TRACE_ENTRY(); _app->_ctx->EventsQueue.clear(); } void AppInput::PushEvent(const InputEvent& ev, bool push_to_this_frame) { FO_STACK_TRACE_ENTRY(); if (push_to_this_frame) { _app->_ctx->EventsQueue.emplace_back(ev); } else { _app->_ctx->NextFrameEventsQueue.emplace_back(ev); } } void AppInput::SetScreenKeyboardEnabled(bool enabled) { FO_STACK_TRACE_ENTRY(); if (_app->_ctx->ActiveRendererType == RenderType::Null || !_app->MainWindow._windowHandle || !SDL_HasScreenKeyboardSupport()) { return; } auto window = _app->MainWindow._windowHandle.reinterpret_as<SDL_Window>(); FO_VERIFY_AND_THROW(window, "Window handle does not reference a valid SDL window"); bool text_input_active = SDL_TextInputActive(window.get()); if (text_input_active == enabled) { return; } if (enabled) { SDL_StartTextInput(window.get()); } else { SDL_StopTextInput(window.get()); } } void AppInput::SetClipboardText(string_view text) { FO_STACK_TRACE_ENTRY(); string clipboard_text = string(text); auto clipboard_text_ptr = make_ptr(clipboard_text.c_str()); SDL_SetClipboardText(clipboard_text_ptr.get()); WebRelated::SyncClipboardToSystem(text); } auto AppInput::GetClipboardText() -> const string& { FO_STACK_TRACE_ENTRY(); auto sdl_clipboard_text = make_nptr(SDL_GetClipboardText()); if (sdl_clipboard_text) { auto clipboard_text = make_unique_del_ptr(sdl_clipboard_text, [](char* raw_data) { if (raw_data != nullptr) { auto data = make_ptr(raw_data); SDL_free(data.get()); } }); _clipboardTextStorage = string {clipboard_text.get()}; } else { _clipboardTextStorage = {}; } return _clipboardTextStorage; } auto AppAudio::IsEnabled() const -> bool { FO_STACK_TRACE_ENTRY(); return !!_app->_ctx->AudioStream; } void AppAudio::SetSource(AudioStreamCallback stream_callback) { FO_STACK_TRACE_ENTRY(); FO_VERIFY_AND_THROW(IsEnabled(), "Application subsystem is not enabled"); LockDevice(); _app->_ctx->AudioStreamWriter = std::move(stream_callback); UnlockDevice(); } auto AppAudio::ConvertAudio(int32_t format, int32_t channels, int32_t rate, vector<uint8_t>& buf) -> bool { FO_STACK_TRACE_ENTRY(); FO_VERIFY_AND_THROW(IsEnabled(), "Application subsystem is not enabled"); SDL_AudioSpec spec; spec.format = static_cast<SDL_AudioFormat>(format); spec.channels = numeric_cast<Uint8>(channels); spec.freq = rate; if (buf.empty()) { return true; } if (spec.format != _app->_ctx->AudioSpec.format || spec.channels != _app->_ctx->AudioSpec.channels || spec.freq != _app->_ctx->AudioSpec.freq) { nptr<uint8_t> dst_data {}; int32_t dst_len {}; if (!SDL_ConvertAudioSamples(&spec, buf.data(), numeric_cast<int32_t>(buf.size()), &_app->_ctx->AudioSpec, dst_data.get_pp(), &dst_len)) { return false; } FO_VERIFY_AND_THROW(dst_data, "SDL audio conversion returned null output data"); auto converted_data = make_unique_del_ptr(dst_data, [](uint8_t* raw_data) { FO_NO_STACK_TRACE_ENTRY(); if (raw_data != nullptr) { auto data = make_ptr(raw_data); SDL_free(data.get()); } }); buf.resize(numeric_cast<size_t>(dst_len)); if (!buf.empty()) { MemCopy(buf.data(), converted_data, buf.size()); } } return true; } void AppAudio::MixAudio(span<uint8_t> output, const_span<uint8_t> buf, int32_t volume) { FO_STACK_TRACE_ENTRY(); FO_VERIFY_AND_THROW(IsEnabled(), "Application subsystem is not enabled"); FO_VERIFY_AND_THROW(output.size() == buf.size(), "Mix audio output and buffer sizes mismatch", output.size(), buf.size()); float32_t volume_01 = numeric_cast<float32_t>(std::clamp(volume, 0, 100)) / 100.0f; SDL_MixAudio(output.data(), buf.data(), _app->_ctx->AudioSpec.format, numeric_cast<Uint32>(output.size()), volume_01); } void AppAudio::LockDevice() { FO_STACK_TRACE_ENTRY(); FO_VERIFY_AND_THROW(IsEnabled(), "Application subsystem is not enabled"); ptr<SDL_AudioStream> audio_stream = _app->_ctx->AudioStream; SDL_LockAudioStream(audio_stream.get()); } void AppAudio::UnlockDevice() { FO_STACK_TRACE_ENTRY(); FO_VERIFY_AND_THROW(IsEnabled(), "Application subsystem is not enabled"); ptr<SDL_AudioStream> audio_stream = _app->_ctx->AudioStream; SDL_UnlockAudioStream(audio_stream.get()); } void Application::ShowErrorMessage(string_view message, string_view traceback, bool fatal_error) { FO_STACK_TRACE_ENTRY(); if (IsRunInDebugger()) { return; } SDL_SetMemoryFunctions(&SdlMemMalloc, &SdlMemCalloc, &SdlMemRealloc, &SdlMemFree); string_view title = fatal_error ? "Fatal Error" : "Error"; #if FO_WEB || FO_ANDROID || FO_IOS #if FO_WEB const auto web_message = traceback.empty() ? string(message) : strex("{}\n\n{}", message, traceback); WebRelated::ShowError(title, web_message); #else const string title_text = string(title); const string message_text = string(message); auto title_ptr = make_ptr(title_text.c_str()); auto message_ptr = make_ptr(message_text.c_str()); SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, title_ptr.get(), message_ptr.get(), nullptr); #endif #else string title_text = string(title); string verb_message = string(message); if (!traceback.empty()) { verb_message += strex("\n\n{}", traceback); } static mutex ignore_entries_locker; static unordered_set<string> ignore_entries FO_TSA_GUARDED_BY(ignore_entries_locker); if (!fatal_error) { scoped_lock locker {ignore_entries_locker}; if (ignore_entries.count(verb_message) != 0) { return; } } SDL_MessageBoxButtonData copy_button; SDL_zero(copy_button); copy_button.buttonID = 0; copy_button.text = "Copy"; SDL_MessageBoxButtonData ignore_all_button; SDL_zero(ignore_all_button); ignore_all_button.buttonID = 1; ignore_all_button.text = "Ignore All"; SDL_MessageBoxButtonData ignore_button; SDL_zero(ignore_button); ignore_button.buttonID = 2; ignore_button.text = "Ignore"; ignore_button.flags |= SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT; ignore_button.flags |= SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT; SDL_MessageBoxButtonData exit_button; SDL_zero(exit_button); exit_button.buttonID = 3; exit_button.text = "Exit"; const SDL_MessageBoxButtonData buttons_with_ignore[] = {copy_button, ignore_all_button, ignore_button, exit_button}; const SDL_MessageBoxButtonData buttons_with_exit[] = {copy_button, exit_button}; SDL_MessageBoxData data; SDL_zero(data); data.flags = SDL_MESSAGEBOX_ERROR | SDL_MESSAGEBOX_BUTTONS_LEFT_TO_RIGHT; auto title_ptr = make_ptr(title_text.c_str()); auto message_ptr = make_ptr(verb_message.c_str()); data.title = title_ptr.get(); data.message = message_ptr.get(); data.numbuttons = fatal_error ? 2 : 4; data.buttons = fatal_error ? buttons_with_exit : buttons_with_ignore; int32_t buttonid = 0; while (SDL_ShowMessageBox(&data, &buttonid)) { if (buttonid == 0) { SDL_SetClipboardText(message_ptr.get()); } else if (buttonid == 1) { scoped_lock locker {ignore_entries_locker}; ignore_entries.emplace(verb_message); break; } else if (buttonid == 3) { if (IsAppInitialized()) { GetApp()->RequestQuit(false); } break; } else { break; } } #endif } void Application::ShowProgressWindow(string_view text, const ProgressWindowCallback& callback) { FO_STACK_TRACE_ENTRY(); SDL_SetMemoryFunctions(&SdlMemMalloc, &SdlMemCalloc, &SdlMemRealloc, &SdlMemFree); bool run_in_separate_thread = false; if (SDL_WasInit(SDL_INIT_VIDEO) != 0 || SDL_InitSubSystem(SDL_INIT_VIDEO)) { constexpr SDL_WindowFlags flags = SDL_WINDOW_BORDERLESS | SDL_WINDOW_ALWAYS_ON_TOP | SDL_WINDOW_UTILITY | SDL_WINDOW_NOT_FOCUSABLE; int32_t window_width = numeric_cast<int32_t>(text.length() * SDL_DEBUG_TEXT_FONT_CHARACTER_SIZE + 50); constexpr int32_t window_height = numeric_cast<int32_t>(60); float32_t text_x = numeric_cast<float32_t>(window_width / 2 - numeric_cast<int32_t>((text.length() * SDL_DEBUG_TEXT_FONT_CHARACTER_SIZE) / 2)); constexpr float32_t text_y = numeric_cast<float32_t>(window_height / 2 - SDL_DEBUG_TEXT_FONT_CHARACTER_SIZE / 2); string window_text = string(text); auto window_text_ptr = make_ptr(window_text.c_str()); SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1"); optional<TemporarySdlWindowRenderer> window_renderer = TryCreateTemporarySdlWindowRenderer(window_text_ptr, window_width, window_height, flags); if (window_renderer) { ptr<SDL_Window> window = window_renderer->Window; ptr<SDL_Renderer> renderer = window_renderer->Renderer; SDL_SetWindowPosition(window.get(), SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED); run_in_separate_thread = true; auto fut = run_async("SplashCallback", [&] { callback(); }); auto start_time = SDL_GetTicks(); while (true) { for (SDL_Event sdl_event; SDL_PollEvent(&sdl_event);) { // Skip } float32_t t = numeric_cast<float>(SDL_GetTicks() - start_time) / 1000.0f; auto intensity = static_cast<Uint8>(150.0f + (std::sin(t * 2.0f) * 0.5f + 0.5f) * 50.0f); SDL_SetRenderDrawColor(renderer.get(), intensity, intensity, intensity, 255); SDL_RenderClear(renderer.get()); SDL_SetRenderDrawColor(renderer.get(), 0, 0, 0, 255); SDL_RenderDebugText(renderer.get(), text_x, text_y, window_text_ptr.get()); SDL_RenderPresent(renderer.get()); if (fut.wait_for(std::chrono::milliseconds(16)) == std::future_status::ready) { fut.get(); break; } } } } if (!run_in_separate_thread) { callback(); } } void Application::ChooseOptionsWindow(string_view title, const vector<string>& options, set<int32_t>& selected) { FO_STACK_TRACE_ENTRY(); if (options.empty()) { return; } SDL_SetMemoryFunctions(&SdlMemMalloc, &SdlMemCalloc, &SdlMemRealloc, &SdlMemFree); if (SDL_WasInit(SDL_INIT_VIDEO) != 0 || SDL_InitSubSystem(SDL_INIT_VIDEO)) { constexpr SDL_WindowFlags flags = SDL_WINDOW_BORDERLESS | SDL_WINDOW_ALWAYS_ON_TOP | SDL_WINDOW_UTILITY; constexpr int32_t line_height = 20; constexpr int32_t char_size = SDL_DEBUG_TEXT_FONT_CHARACTER_SIZE; int32_t max_options_text = numeric_cast<int32_t>(std::ranges::max_element(options, [](auto&& a, auto&& b) { return a.size() < b.size(); })->length() * char_size); int32_t window_width = numeric_cast<int32_t>(std::max(max_options_text, numeric_cast<int32_t>(title.length() * char_size)) + 50); int32_t window_height = numeric_cast<int32_t>((options.size() + 2) * line_height); string title_text = string(title); auto title_ptr = make_ptr(title_text.c_str()); SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1"); optional<TemporarySdlWindowRenderer> window_renderer = TryCreateTemporarySdlWindowRenderer(title_ptr, window_width, window_height, flags); if (window_renderer) { ptr<SDL_Window> window = window_renderer->Window; ptr<SDL_Renderer> renderer = window_renderer->Renderer; SDL_SetWindowPosition(window.get(), SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED); for (bool running = true; running;) { auto toggle_index = [&](int32_t index) { if ((SDL_GetModState() & SDL_KMOD_CTRL) == 0) { selected.clear(); } if (!selected.contains(index)) { selected.emplace(index); } else { selected.erase(index); } }; for (SDL_Event sdl_event; SDL_PollEvent(&sdl_event);) { if (sdl_event.type == SDL_EVENT_QUIT) { ExitApp(true); } else if (sdl_event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) { int32_t mx = iround<int32_t>(sdl_event.button.x); int32_t my = iround<int32_t>(sdl_event.button.y); if (mx >= 0 && mx < window_width) { int32_t line = my / line_height; if (line > 0 && line < numeric_cast<int32_t>(options.size() + 2)) { if (line < numeric_cast<int32_t>(options.size() + 1)) { toggle_index(line - 1); } else { running = false; } } } } else if (sdl_event.type == SDL_EVENT_KEY_DOWN) { if (sdl_event.key.key == SDLK_RETURN) { running = false; } else if (sdl_event.key.key == SDLK_ESCAPE) { ExitApp(true); } else if (sdl_event.key.key >= SDLK_1 && sdl_event.key.key <= SDLK_9) { toggle_index(numeric_cast<int32_t>(sdl_event.key.key - SDLK_1)); } } } SDL_SetRenderDrawColor(renderer.get(), 255, 255, 255, 255); SDL_RenderClear(renderer.get()); for (size_t i = 0; i < options.size() + 2; i++) { SDL_FRect rect; rect.x = numeric_cast<float32_t>(1); rect.y = numeric_cast<float32_t>(line_height * i + 1); rect.w = numeric_cast<float32_t>(window_width - 2); rect.h = numeric_cast<float32_t>(line_height - 2); if (i == 0) { SDL_SetRenderDrawColor(renderer.get(), 150, 150, 150, 255); } else if (i < options.size() + 1) { if (selected.contains(numeric_cast<int32_t>(i - 1))) { SDL_SetRenderDrawColor(renderer.get(), 0, 200, 0, 255); } else { SDL_SetRenderDrawColor(renderer.get(), 150, 150, 150, 255); } } else { SDL_SetRenderDrawColor(renderer.get(), 150, 150, 150, 255); } SDL_RenderRect(renderer.get(), &rect); string text; if (i == 0) { text = title; SDL_SetRenderDrawColor(renderer.get(), 0, 0, 0, 255); } else if (i < options.size() + 1) { text = strex("{}) {}", i, options[i - 1]); if (selected.contains(numeric_cast<int32_t>(i - 1))) { SDL_SetRenderDrawColor(renderer.get(), 0, 200, 0, 255); } else { SDL_SetRenderDrawColor(renderer.get(), 0, 0, 0, 255); } } else { text = "--- ENTER ---"; SDL_SetRenderDrawColor(renderer.get(), 0, 0, 0, 255); } float32_t text_x = numeric_cast<float32_t>(window_width / 2 - numeric_cast<int32_t>((text.length() * char_size) / 2)); float32_t text_y = numeric_cast<float32_t>(i * line_height + line_height / 2 - char_size / 2); auto text_ptr = make_ptr(text.c_str()); SDL_RenderDebugText(renderer.get(), text_x, text_y, text_ptr.get()); } SDL_RenderPresent(renderer.get()); SDL_Delay(16); } } } } static ImGuiKey KeycodeToImGuiKey(SDL_Keycode keycode) { FO_STACK_TRACE_ENTRY(); switch (keycode) { case SDLK_TAB: return ImGuiKey_Tab; case SDLK_LEFT: return ImGuiKey_LeftArrow; case SDLK_RIGHT: return ImGuiKey_RightArrow; case SDLK_UP: return ImGuiKey_UpArrow; case SDLK_DOWN: return ImGuiKey_DownArrow; case SDLK_PAGEUP: return ImGuiKey_PageUp; case SDLK_PAGEDOWN: return ImGuiKey_PageDown; case SDLK_HOME: return ImGuiKey_Home; case SDLK_END: return ImGuiKey_End; case SDLK_INSERT: return ImGuiKey_Insert; case SDLK_DELETE: return ImGuiKey_Delete; case SDLK_BACKSPACE: return ImGuiKey_Backspace; case SDLK_SPACE: return ImGuiKey_Space; case SDLK_RETURN: return ImGuiKey_Enter; case SDLK_ESCAPE: return ImGuiKey_Escape; case SDLK_APOSTROPHE: return ImGuiKey_Apostrophe; case SDLK_COMMA: return ImGuiKey_Comma; case SDLK_MINUS: return ImGuiKey_Minus; case SDLK_PERIOD: return ImGuiKey_Period; case SDLK_SLASH: return ImGuiKey_Slash; case SDLK_SEMICOLON: return ImGuiKey_Semicolon; case SDLK_EQUALS: return ImGuiKey_Equal; case SDLK_LEFTBRACKET: return ImGuiKey_LeftBracket; case SDLK_BACKSLASH: return ImGuiKey_Backslash; case SDLK_RIGHTBRACKET: return ImGuiKey_RightBracket; case SDLK_GRAVE: return ImGuiKey_GraveAccent; case SDLK_CAPSLOCK: return ImGuiKey_CapsLock; case SDLK_SCROLLLOCK: return ImGuiKey_ScrollLock; case SDLK_NUMLOCKCLEAR: return ImGuiKey_NumLock; case SDLK_PRINTSCREEN: return ImGuiKey_PrintScreen; case SDLK_PAUSE: return ImGuiKey_Pause; case SDLK_KP_0: return ImGuiKey_Keypad0; case SDLK_KP_1: return ImGuiKey_Keypad1; case SDLK_KP_2: return ImGuiKey_Keypad2; case SDLK_KP_3: return ImGuiKey_Keypad3; case SDLK_KP_4: return ImGuiKey_Keypad4; case SDLK_KP_5: return ImGuiKey_Keypad5; case SDLK_KP_6: return ImGuiKey_Keypad6; case SDLK_KP_7: return ImGuiKey_Keypad7; case SDLK_KP_8: return ImGuiKey_Keypad8; case SDLK_KP_9: return ImGuiKey_Keypad9; case SDLK_KP_PERIOD: return ImGuiKey_KeypadDecimal; case SDLK_KP_DIVIDE: return ImGuiKey_KeypadDivide; case SDLK_KP_MULTIPLY: return ImGuiKey_KeypadMultiply; case SDLK_KP_MINUS: return ImGuiKey_KeypadSubtract; case SDLK_KP_PLUS: return ImGuiKey_KeypadAdd; case SDLK_KP_ENTER: return ImGuiKey_KeypadEnter; case SDLK_KP_EQUALS: return ImGuiKey_KeypadEqual; case SDLK_LCTRL: return ImGuiKey_LeftCtrl; case SDLK_LSHIFT: return ImGuiKey_LeftShift; case SDLK_LALT: return ImGuiKey_LeftAlt; case SDLK_LGUI: return ImGuiKey_LeftSuper; case SDLK_RCTRL: return ImGuiKey_RightCtrl; case SDLK_RSHIFT: return ImGuiKey_RightShift; case SDLK_RALT: return ImGuiKey_RightAlt; case SDLK_RGUI: return ImGuiKey_RightSuper; case SDLK_APPLICATION: return ImGuiKey_Menu; case SDLK_0: return ImGuiKey_0; case SDLK_1: return ImGuiKey_1; case SDLK_2: return ImGuiKey_2; case SDLK_3: return ImGuiKey_3; case SDLK_4: return ImGuiKey_4; case SDLK_5: return ImGuiKey_5; case SDLK_6: return ImGuiKey_6; case SDLK_7: return ImGuiKey_7; case SDLK_8: return ImGuiKey_8; case SDLK_9: return ImGuiKey_9; case SDLK_A: return ImGuiKey_A; case SDLK_B: return ImGuiKey_B; case SDLK_C: return ImGuiKey_C; case SDLK_D: return ImGuiKey_D; case SDLK_E: return ImGuiKey_E; case SDLK_F: return ImGuiKey_F; case SDLK_G: return ImGuiKey_G; case SDLK_H: return ImGuiKey_H; case SDLK_I: return ImGuiKey_I; case SDLK_J: return ImGuiKey_J; case SDLK_K: return ImGuiKey_K; case SDLK_L: return ImGuiKey_L; case SDLK_M: return ImGuiKey_M; case SDLK_N: return ImGuiKey_N; case SDLK_O: return ImGuiKey_O; case SDLK_P: return ImGuiKey_P; case SDLK_Q: return ImGuiKey_Q; case SDLK_R: return ImGuiKey_R; case SDLK_S: return ImGuiKey_S; case SDLK_T: return ImGuiKey_T; case SDLK_U: return ImGuiKey_U; case SDLK_V: return ImGuiKey_V; case SDLK_W: return ImGuiKey_W; case SDLK_X: return ImGuiKey_X; case SDLK_Y: return ImGuiKey_Y; case SDLK_Z: return ImGuiKey_Z; case SDLK_F1: return ImGuiKey_F1; case SDLK_F2: return ImGuiKey_F2; case SDLK_F3: return ImGuiKey_F3; case SDLK_F4: return ImGuiKey_F4; case SDLK_F5: return ImGuiKey_F5; case SDLK_F6: return ImGuiKey_F6; case SDLK_F7: return ImGuiKey_F7; case SDLK_F8: return ImGuiKey_F8; case SDLK_F9: return ImGuiKey_F9; case SDLK_F10: return ImGuiKey_F10; case SDLK_F11: return ImGuiKey_F11; case SDLK_F12: return ImGuiKey_F12; default: break; } return ImGuiKey_None; } FO_END_NAMESPACE