summaryrefslogtreecommitdiffstats
path: root/client/SDL
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--client/SDL/CMakeLists.txt128
-rw-r--r--client/SDL/aad/CMakeLists.txt75
-rw-r--r--client/SDL/aad/dummy.cpp0
-rw-r--r--client/SDL/aad/qt/webview_impl.cpp105
-rw-r--r--client/SDL/aad/sdl_config.hpp.in3
-rw-r--r--client/SDL/aad/sdl_webview.cpp129
-rw-r--r--client/SDL/aad/sdl_webview.hpp38
-rw-r--r--client/SDL/aad/webview_impl.hpp24
-rw-r--r--client/SDL/aad/wrapper/README1
-rw-r--r--client/SDL/aad/wrapper/webview.h2781
-rw-r--r--client/SDL/aad/wrapper/webview_impl.cpp82
-rw-r--r--client/SDL/dialogs/CMakeLists.txt75
-rw-r--r--client/SDL/dialogs/font/OFL.txt93
-rw-r--r--client/SDL/dialogs/font/OpenSans-Italic-VariableFont_wdth,wght.ttfbin0 -> 580356 bytes
-rw-r--r--client/SDL/dialogs/font/OpenSans-VariableFont_wdth,wght.ttfbin0 -> 529700 bytes
-rw-r--r--client/SDL/dialogs/font/README.txt100
-rw-r--r--client/SDL/dialogs/res/CMakeLists.txt89
-rw-r--r--client/SDL/dialogs/res/convert_res_to_c.cpp184
-rw-r--r--client/SDL/dialogs/res/sdl_resource_file.cpp25
-rw-r--r--client/SDL/dialogs/res/sdl_resource_file.hpp33
-rw-r--r--client/SDL/dialogs/res/sdl_resource_manager.cpp78
-rw-r--r--client/SDL/dialogs/res/sdl_resource_manager.hpp46
-rw-r--r--client/SDL/dialogs/sdl_button.cpp71
-rw-r--r--client/SDL/dialogs/sdl_button.hpp26
-rw-r--r--client/SDL/dialogs/sdl_buttons.cpp105
-rw-r--r--client/SDL/dialogs/sdl_buttons.hpp37
-rw-r--r--client/SDL/dialogs/sdl_connection_dialog.cpp536
-rw-r--r--client/SDL/dialogs/sdl_connection_dialog.hpp129
-rw-r--r--client/SDL/dialogs/sdl_dialogs.cpp621
-rw-r--r--client/SDL/dialogs/sdl_dialogs.hpp53
-rw-r--r--client/SDL/dialogs/sdl_input.cpp177
-rw-r--r--client/SDL/dialogs/sdl_input.hpp73
-rw-r--r--client/SDL/dialogs/sdl_input_widgets.cpp299
-rw-r--r--client/SDL/dialogs/sdl_input_widgets.hpp44
-rw-r--r--client/SDL/dialogs/sdl_select.cpp74
-rw-r--r--client/SDL/dialogs/sdl_select.hpp46
-rw-r--r--client/SDL/dialogs/sdl_selectlist.cpp208
-rw-r--r--client/SDL/dialogs/sdl_selectlist.hpp42
-rw-r--r--client/SDL/dialogs/sdl_widget.cpp280
-rw-r--r--client/SDL/dialogs/sdl_widget.hpp88
-rw-r--r--client/SDL/dialogs/test/CMakeLists.txt30
-rw-r--r--client/SDL/dialogs/test/TestSDLDialogs.cpp99
-rw-r--r--client/SDL/man/CMakeLists.txt12
-rw-r--r--client/SDL/man/sdl-freerdp-config.1.xml.in81
-rw-r--r--client/SDL/man/sdl-freerdp-envvar.1.xml.in15
-rw-r--r--client/SDL/man/sdl-freerdp-examples.1.xml.in95
-rw-r--r--client/SDL/man/sdl-freerdp.1.xml.in67
-rw-r--r--client/SDL/sdl_channels.cpp83
-rw-r--r--client/SDL/sdl_channels.hpp29
-rw-r--r--client/SDL/sdl_disp.cpp471
-rw-r--r--client/SDL/sdl_disp.hpp82
-rw-r--r--client/SDL/sdl_freerdp.cpp1704
-rw-r--r--client/SDL/sdl_freerdp.hpp88
-rw-r--r--client/SDL/sdl_kbd.cpp568
-rw-r--r--client/SDL/sdl_kbd.hpp56
-rw-r--r--client/SDL/sdl_monitor.cpp331
-rw-r--r--client/SDL/sdl_monitor.hpp28
-rw-r--r--client/SDL/sdl_pointer.cpp197
-rw-r--r--client/SDL/sdl_pointer.hpp27
-rw-r--r--client/SDL/sdl_touch.cpp285
-rw-r--r--client/SDL/sdl_touch.hpp36
-rw-r--r--client/SDL/sdl_types.hpp46
-rw-r--r--client/SDL/sdl_utils.cpp465
-rw-r--r--client/SDL/sdl_utils.hpp113
-rw-r--r--client/SDL/sdl_window.cpp203
-rw-r--r--client/SDL/sdl_window.hpp62
66 files changed, 12271 insertions, 0 deletions
diff --git a/client/SDL/CMakeLists.txt b/client/SDL/CMakeLists.txt
new file mode 100644
index 0000000..6d2b778
--- /dev/null
+++ b/client/SDL/CMakeLists.txt
@@ -0,0 +1,128 @@
+# FreeRDP: A Remote Desktop Protocol Implementation
+# FreeRDP SDL Client
+#
+# Copyright 2022 Armin Novak <anovak@thincast.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+cmake_minimum_required(VERSION 3.13)
+
+if(POLICY CMP0091)
+ cmake_policy(SET CMP0091 NEW)
+endif()
+if (NOT FREERDP_DEFAULT_PROJECT_VERSION)
+ set(FREERDP_DEFAULT_PROJECT_VERSION "1.0.0.0")
+endif()
+
+project(sdl-freerdp
+ LANGUAGES CXX
+ VERSION ${FREERDP_DEFAULT_PROJECT_VERSION}
+)
+
+message("project ${PROJECT_NAME} is using version ${PROJECT_VERSION}")
+
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+set(CMAKE_CXX_EXTENSIONS ON)
+
+list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/)
+include(CommonConfigOptions)
+
+include(ConfigureFreeRDP)
+
+option(WITH_DEBUG_SDL_EVENTS "[dangerous, not for release builds!] Debug SDL events" ${DEFAULT_DEBUG_OPTION})
+option(WITH_DEBUG_SDL_KBD_EVENTS "[dangerous, not for release builds!] Debug SDL keyboard events" ${DEFAULT_DEBUG_OPTION})
+option(WITH_WIN_CONSOLE "Build ${PROJECT_NAME} with console support" ON)
+option(WITH_SDL_LINK_SHARED "link SDL dynamic or static" ON)
+
+if(WITH_WIN_CONSOLE)
+ set(WIN32_GUI_FLAG "")
+else()
+ set(WIN32_GUI_FLAG "WIN32")
+endif()
+
+
+if (WITH_DEBUG_SDL_EVENTS)
+ add_definitions(-DWITH_DEBUG_SDL_EVENTS)
+endif()
+if (WITH_DEBUG_SDL_KBD_EVENTS)
+ add_definitions(-DWITH_DEBUG_SDL_KBD_EVENTS)
+endif()
+
+find_package(SDL2 REQUIRED COMPONENTS)
+include_directories(${SDL2_INCLUDE_DIR})
+include_directories(${SDL2_INCLUDE_DIRS})
+find_package(cJSON)
+
+set(LIBS "")
+if (cJSON_FOUND)
+ include_directories(${CJSON_INCLUDE_DIRS})
+ list(APPEND LIBS ${CJSON_LIBRARIES})
+ add_compile_definitions(CJSON_FOUND)
+endif()
+
+find_package(Threads REQUIRED)
+
+add_subdirectory(dialogs)
+set(SRCS
+ sdl_types.hpp
+ sdl_utils.cpp
+ sdl_utils.hpp
+ sdl_kbd.cpp
+ sdl_kbd.hpp
+ sdl_touch.cpp
+ sdl_touch.hpp
+ sdl_pointer.cpp
+ sdl_pointer.hpp
+ sdl_disp.cpp
+ sdl_disp.hpp
+ sdl_monitor.cpp
+ sdl_monitor.hpp
+ sdl_freerdp.hpp
+ sdl_freerdp.cpp
+ sdl_channels.hpp
+ sdl_channels.cpp
+ sdl_window.hpp
+ sdl_window.cpp
+)
+
+add_subdirectory(aad)
+list(APPEND LIBS
+ winpr
+ freerdp
+ freerdp-client
+ Threads::Threads
+ sdl_client_res
+ dialogs
+ aad-view
+ )
+
+if (NOT WITH_SDL_LINK_SHARED)
+ list(APPEND LIBS ${SDL2_STATIC_LIBRARIES})
+else()
+ list(APPEND LIBS ${SDL2_LIBRARIES})
+endif()
+
+add_executable(${PROJECT_NAME}
+ ${WIN32_GUI_FLAG}
+ ${SRCS}
+ )
+
+if (WITH_BINARY_VERSIONING)
+ set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "${PROJECT_NAME}${PROJECT_VERSION_MAJOR}")
+endif()
+target_link_libraries(${PROJECT_NAME} PRIVATE ${LIBS})
+set_property(TARGET ${PROJECT_NAME} PROPERTY FOLDER "Client/SDL")
+install(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT client)
+
+add_subdirectory(man)
diff --git a/client/SDL/aad/CMakeLists.txt b/client/SDL/aad/CMakeLists.txt
new file mode 100644
index 0000000..2286542
--- /dev/null
+++ b/client/SDL/aad/CMakeLists.txt
@@ -0,0 +1,75 @@
+set(WITH_WEBVIEW_DEFAULT OFF)
+if (UNIX AND NOT APPLE)
+ set(WITH_WEBVIEW_DEFAULT ON)
+endif()
+
+option(WITH_WEBVIEW "Build with WebView support for AAD login popup browser" ${WITH_WEBVIEW_DEFAULT})
+if (WITH_WEBVIEW)
+ option(WITH_WEBVIEW_QT "Build with QtWebEngine support for AAD login broweser popup" OFF)
+
+ set(SRCS
+ sdl_webview.hpp
+ webview_impl.hpp
+ sdl_webview.cpp
+ )
+ set(LIBS
+ winpr
+ )
+
+ if (WITH_WEBVIEW_QT)
+ find_package(Qt5 COMPONENTS WebEngineWidgets REQUIRED)
+
+ list(APPEND SRCS
+ qt/webview_impl.cpp
+ )
+
+ list(APPEND LIBS
+ Qt5::WebEngineWidgets
+ )
+ else()
+ list(APPEND SRCS
+ wrapper/webview.h
+ wrapper/webview_impl.cpp
+ )
+
+ if (WIN32)
+ find_package(unofficial-webview2 CONFIG REQUIRED)
+ list(APPEND LIBS
+ unofficial::webview2::webview2
+ )
+ elseif(APPLE)
+ find_library(WEBKIT Webkit REQUIRED)
+ list(APPEND LIBS
+ ${WEBKIT}
+ )
+ else()
+ find_package(PkgConfig REQUIRED)
+ pkg_check_modules(WEBVIEW_GTK webkit2gtk-4.0 REQUIRED)
+ include_directories(${WEBVIEW_GTK_INCLUDE_DIRS})
+ list(APPEND LIBS
+ ${WEBVIEW_GTK_LIBRARIES}
+ )
+ endif()
+ endif()
+else()
+ set(SRCS
+ dummy.cpp
+ )
+endif()
+
+configure_file(sdl_config.hpp.in sdl_config.hpp @ONLY)
+
+add_library(aad-view STATIC
+ ${SRCS}
+)
+target_include_directories(aad-view PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
+target_link_libraries(aad-view
+ PRIVATE
+ ${LIBS}
+)
+target_compile_definitions(
+ aad-view
+ PUBLIC
+ ${DEFINITIONS}
+)
+
diff --git a/client/SDL/aad/dummy.cpp b/client/SDL/aad/dummy.cpp
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/client/SDL/aad/dummy.cpp
diff --git a/client/SDL/aad/qt/webview_impl.cpp b/client/SDL/aad/qt/webview_impl.cpp
new file mode 100644
index 0000000..e70cc46
--- /dev/null
+++ b/client/SDL/aad/qt/webview_impl.cpp
@@ -0,0 +1,105 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * Popup browser for AAD authentication
+ *
+ * Copyright 2023 Isaac Klein <fifthdegree@protonmail.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <QApplication>
+#include <QWebEngineView>
+#include <QWebEngineProfile>
+#include <QWebEngineUrlScheme>
+#include <QWebEngineUrlSchemeHandler>
+#include <QWebEngineUrlRequestJob>
+
+#include <string>
+#include <cstdlib>
+#include <cstdarg>
+#include <winpr/string.h>
+#include <winpr/assert.h>
+#include <freerdp/log.h>
+#include <freerdp/build-config.h>
+
+#include "../webview_impl.hpp"
+
+#define TAG CLIENT_TAG("sdl.webview")
+
+class SchemeHandler : public QWebEngineUrlSchemeHandler
+{
+ public:
+ explicit SchemeHandler(QObject* parent = nullptr) : QWebEngineUrlSchemeHandler(parent)
+ {
+ }
+
+ void requestStarted(QWebEngineUrlRequestJob* request) override
+ {
+ QUrl url = request->requestUrl();
+
+ int rc = -1;
+ for (auto& param : url.query().split('&'))
+ {
+ QStringList pair = param.split('=');
+
+ if (pair.size() != 2 || pair[0] != QLatin1String("code"))
+ continue;
+
+ auto qc = pair[1];
+ m_code = qc.toStdString();
+ rc = 0;
+ break;
+ }
+ qApp->exit(rc);
+ }
+
+ [[nodiscard]] std::string code() const
+ {
+ return m_code;
+ }
+
+ private:
+ std::string m_code{};
+};
+
+bool webview_impl_run(const std::string& title, const std::string& url, std::string& code)
+{
+ int argc = 1;
+ const auto vendor = QString::fromUtf8(FREERDP_VENDOR_STRING);
+ const auto product = QString::fromUtf8(FREERDP_PRODUCT_STRING);
+ QWebEngineUrlScheme::registerScheme(QWebEngineUrlScheme("ms-appx-web"));
+
+ std::string wtitle = title;
+ char* argv[] = { wtitle.data() };
+ QCoreApplication::setOrganizationName(vendor);
+ QCoreApplication::setApplicationName(product);
+ QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+ QApplication app(argc, argv);
+
+ SchemeHandler handler;
+ QWebEngineProfile::defaultProfile()->installUrlSchemeHandler("ms-appx-web", &handler);
+
+ QWebEngineView webview;
+ webview.load(QUrl(QString::fromStdString(url)));
+ webview.show();
+
+ if (app.exec() != 0)
+ return false;
+
+ auto val = handler.code();
+ if (val.empty())
+ return false;
+ code = val;
+
+ return !code.empty();
+}
diff --git a/client/SDL/aad/sdl_config.hpp.in b/client/SDL/aad/sdl_config.hpp.in
new file mode 100644
index 0000000..34d0751
--- /dev/null
+++ b/client/SDL/aad/sdl_config.hpp.in
@@ -0,0 +1,3 @@
+#pragma once
+
+#cmakedefine WITH_WEBVIEW
diff --git a/client/SDL/aad/sdl_webview.cpp b/client/SDL/aad/sdl_webview.cpp
new file mode 100644
index 0000000..b4df75b
--- /dev/null
+++ b/client/SDL/aad/sdl_webview.cpp
@@ -0,0 +1,129 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * Popup browser for AAD authentication
+ *
+ * Copyright 2023 Isaac Klein <fifthdegree@protonmail.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <string>
+#include <sstream>
+#include <cstdlib>
+#include <winpr/string.h>
+#include <freerdp/log.h>
+
+#include "sdl_webview.hpp"
+#include "webview_impl.hpp"
+
+#define TAG CLIENT_TAG("SDL.webview")
+
+static BOOL sdl_webview_get_rdsaad_access_token(freerdp* instance, const char* scope,
+ const char* req_cnf, char** token)
+{
+ WINPR_ASSERT(instance);
+ WINPR_ASSERT(scope);
+ WINPR_ASSERT(req_cnf);
+ WINPR_ASSERT(token);
+
+ WINPR_UNUSED(instance);
+
+ std::string client_id = "5177bc73-fd99-4c77-a90c-76844c9b6999";
+ std::string redirect_uri =
+ "ms-appx-web%3a%2f%2fMicrosoft.AAD.BrokerPlugin%2f5177bc73-fd99-4c77-a90c-76844c9b6999";
+
+ *token = nullptr;
+
+ auto url =
+ "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=" + client_id +
+ "&response_type=code&scope=" + scope + "&redirect_uri=" + redirect_uri;
+
+ const std::string title = "FreeRDP WebView - AAD access token";
+ std::string code;
+ auto rc = webview_impl_run(title, url, code);
+ if (!rc || code.empty())
+ return FALSE;
+
+ auto token_request = "grant_type=authorization_code&code=" + code + "&client_id=" + client_id +
+ "&scope=" + scope + "&redirect_uri=" + redirect_uri +
+ "&req_cnf=" + req_cnf;
+ return client_common_get_access_token(instance, token_request.c_str(), token);
+}
+
+static BOOL sdl_webview_get_avd_access_token(freerdp* instance, char** token)
+{
+ WINPR_ASSERT(token);
+
+ std::string client_id = "a85cf173-4192-42f8-81fa-777a763e6e2c";
+ std::string redirect_uri =
+ "ms-appx-web%3a%2f%2fMicrosoft.AAD.BrokerPlugin%2fa85cf173-4192-42f8-81fa-777a763e6e2c";
+ std::string scope = "https%3A%2F%2Fwww.wvd.microsoft.com%2F.default";
+
+ *token = nullptr;
+
+ auto url =
+ "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=" + client_id +
+ "&response_type=code&scope=" + scope + "&redirect_uri=" + redirect_uri;
+ const std::string title = "FreeRDP WebView - AVD access token";
+ std::string code;
+ auto rc = webview_impl_run(title, url, code);
+ if (!rc || code.empty())
+ return FALSE;
+
+ auto token_request = "grant_type=authorization_code&code=" + code + "&client_id=" + client_id +
+ "&scope=" + scope + "&redirect_uri=" + redirect_uri;
+ return client_common_get_access_token(instance, token_request.c_str(), token);
+}
+
+BOOL sdl_webview_get_access_token(freerdp* instance, AccessTokenType tokenType, char** token,
+ size_t count, ...)
+{
+ WINPR_ASSERT(instance);
+ WINPR_ASSERT(token);
+ switch (tokenType)
+ {
+ case ACCESS_TOKEN_TYPE_AAD:
+ {
+ if (count < 2)
+ {
+ WLog_ERR(TAG,
+ "ACCESS_TOKEN_TYPE_AAD expected 2 additional arguments, but got %" PRIuz
+ ", aborting",
+ count);
+ return FALSE;
+ }
+ else if (count > 2)
+ WLog_WARN(TAG,
+ "ACCESS_TOKEN_TYPE_AAD expected 2 additional arguments, but got %" PRIuz
+ ", ignoring",
+ count);
+ va_list ap;
+ va_start(ap, count);
+ const char* scope = va_arg(ap, const char*);
+ const char* req_cnf = va_arg(ap, const char*);
+ const BOOL rc = sdl_webview_get_rdsaad_access_token(instance, scope, req_cnf, token);
+ va_end(ap);
+ return rc;
+ }
+ case ACCESS_TOKEN_TYPE_AVD:
+ if (count != 0)
+ WLog_WARN(TAG,
+ "ACCESS_TOKEN_TYPE_AVD expected 0 additional arguments, but got %" PRIuz
+ ", ignoring",
+ count);
+ return sdl_webview_get_avd_access_token(instance, token);
+ default:
+ WLog_ERR(TAG, "Unexpected value for AccessTokenType [%" PRIuz "], aborting", tokenType);
+ return FALSE;
+ }
+}
diff --git a/client/SDL/aad/sdl_webview.hpp b/client/SDL/aad/sdl_webview.hpp
new file mode 100644
index 0000000..49461d6
--- /dev/null
+++ b/client/SDL/aad/sdl_webview.hpp
@@ -0,0 +1,38 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * Popup browser for AAD authentication
+ *
+ * Copyright 2023 Isaac Klein <fifthdegree@protonmail.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <freerdp/freerdp.h>
+
+#include <sdl_config.hpp>
+
+#if defined(WITH_WEBVIEW)
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+ BOOL sdl_webview_get_access_token(freerdp* instance, AccessTokenType tokenType, char** token,
+ size_t count, ...);
+
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/client/SDL/aad/webview_impl.hpp b/client/SDL/aad/webview_impl.hpp
new file mode 100644
index 0000000..25bca3c
--- /dev/null
+++ b/client/SDL/aad/webview_impl.hpp
@@ -0,0 +1,24 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * Popup browser for AAD authentication
+ *
+ * Copyright 2023 Isaac Klein <fifthdegree@protonmail.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <string>
+
+bool webview_impl_run(const std::string& title, const std::string& url, std::string& code);
diff --git a/client/SDL/aad/wrapper/README b/client/SDL/aad/wrapper/README
new file mode 100644
index 0000000..da906ba
--- /dev/null
+++ b/client/SDL/aad/wrapper/README
@@ -0,0 +1 @@
+upstream at https://github.com/webview/webview/
diff --git a/client/SDL/aad/wrapper/webview.h b/client/SDL/aad/wrapper/webview.h
new file mode 100644
index 0000000..4919265
--- /dev/null
+++ b/client/SDL/aad/wrapper/webview.h
@@ -0,0 +1,2781 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2017 Serge Zaitsev
+ * Copyright (c) 2022 Steffen André Langnes
+ *
+ * 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.
+ */
+#ifndef WEBVIEW_H
+#define WEBVIEW_H
+
+#ifndef WEBVIEW_API
+#define WEBVIEW_API extern
+#endif
+
+#ifndef WEBVIEW_VERSION_MAJOR
+// The current library major version.
+#define WEBVIEW_VERSION_MAJOR 0
+#endif
+
+#ifndef WEBVIEW_VERSION_MINOR
+// The current library minor version.
+#define WEBVIEW_VERSION_MINOR 10
+#endif
+
+#ifndef WEBVIEW_VERSION_PATCH
+// The current library patch version.
+#define WEBVIEW_VERSION_PATCH 0
+#endif
+
+#ifndef WEBVIEW_VERSION_PRE_RELEASE
+// SemVer 2.0.0 pre-release labels prefixed with "-".
+#define WEBVIEW_VERSION_PRE_RELEASE ""
+#endif
+
+#ifndef WEBVIEW_VERSION_BUILD_METADATA
+// SemVer 2.0.0 build metadata prefixed with "+".
+#define WEBVIEW_VERSION_BUILD_METADATA ""
+#endif
+
+// Utility macro for stringifying a macro argument.
+#define WEBVIEW_STRINGIFY(x) #x
+
+// Utility macro for stringifying the result of a macro argument expansion.
+#define WEBVIEW_EXPAND_AND_STRINGIFY(x) WEBVIEW_STRINGIFY(x)
+
+// SemVer 2.0.0 version number in MAJOR.MINOR.PATCH format.
+#define WEBVIEW_VERSION_NUMBER \
+ WEBVIEW_EXPAND_AND_STRINGIFY(WEBVIEW_VERSION_MAJOR) \
+ "." WEBVIEW_EXPAND_AND_STRINGIFY(WEBVIEW_VERSION_MINOR) "." WEBVIEW_EXPAND_AND_STRINGIFY( \
+ WEBVIEW_VERSION_PATCH)
+
+// Holds the elements of a MAJOR.MINOR.PATCH version number.
+typedef struct
+{
+ // Major version.
+ unsigned int major;
+ // Minor version.
+ unsigned int minor;
+ // Patch version.
+ unsigned int patch;
+} webview_version_t;
+
+// Holds the library's version information.
+typedef struct
+{
+ // The elements of the version number.
+ webview_version_t version;
+ // SemVer 2.0.0 version number in MAJOR.MINOR.PATCH format.
+ char version_number[32];
+ // SemVer 2.0.0 pre-release labels prefixed with "-" if specified, otherwise
+ // an empty string.
+ char pre_release[48];
+ // SemVer 2.0.0 build metadata prefixed with "+", otherwise an empty string.
+ char build_metadata[48];
+} webview_version_info_t;
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+ typedef void* webview_t;
+
+ // Creates a new webview instance. If debug is non-zero - developer tools will
+ // be enabled (if the platform supports them). Window parameter can be a
+ // pointer to the native window handle. If it's non-null - then child WebView
+ // is embedded into the given parent window. Otherwise a new window is created.
+ // Depending on the platform, a GtkWindow, NSWindow or HWND pointer can be
+ // passed here. Returns null on failure. Creation can fail for various reasons
+ // such as when required runtime dependencies are missing or when window creation
+ // fails.
+ WEBVIEW_API webview_t webview_create(int debug, void* window);
+
+ // Destroys a webview and closes the native window.
+ WEBVIEW_API void webview_destroy(webview_t w);
+
+ // Runs the main loop until it's terminated. After this function exits - you
+ // must destroy the webview.
+ WEBVIEW_API void webview_run(webview_t w);
+
+ // Stops the main loop. It is safe to call this function from another other
+ // background thread.
+ WEBVIEW_API void webview_terminate(webview_t w);
+
+ // Posts a function to be executed on the main thread. You normally do not need
+ // to call this function, unless you want to tweak the native window.
+ WEBVIEW_API void webview_dispatch(webview_t w, void (*fn)(webview_t w, void* arg), void* arg);
+
+ // Returns a native window handle pointer. When using GTK backend the pointer
+ // is GtkWindow pointer, when using Cocoa backend the pointer is NSWindow
+ // pointer, when using Win32 backend the pointer is HWND pointer.
+ WEBVIEW_API void* webview_get_window(webview_t w);
+
+ // Updates the title of the native window. Must be called from the UI thread.
+ WEBVIEW_API void webview_set_title(webview_t w, const char* title);
+
+// Window size hints
+#define WEBVIEW_HINT_NONE 0 // Width and height are default size
+#define WEBVIEW_HINT_MIN 1 // Width and height are minimum bounds
+#define WEBVIEW_HINT_MAX 2 // Width and height are maximum bounds
+#define WEBVIEW_HINT_FIXED 3 // Window size can not be changed by a user
+ // Updates native window size. See WEBVIEW_HINT constants.
+ WEBVIEW_API void webview_set_size(webview_t w, int width, int height, int hints);
+
+ // Navigates webview to the given URL. URL may be a properly encoded data URI.
+ // Examples:
+ // webview_navigate(w, "https://github.com/webview/webview");
+ // webview_navigate(w, "data:text/html,%3Ch1%3EHello%3C%2Fh1%3E");
+ // webview_navigate(w, "data:text/html;base64,PGgxPkhlbGxvPC9oMT4=");
+ WEBVIEW_API void webview_navigate(webview_t w, const char* url);
+
+ // Set webview HTML directly.
+ // Example: webview_set_html(w, "<h1>Hello</h1>");
+ WEBVIEW_API void webview_set_html(webview_t w, const char* html);
+
+ // Injects JavaScript code at the initialization of the new page. Every time
+ // the webview will open a the new page - this initialization code will be
+ // executed. It is guaranteed that code is executed before window.onload.
+ WEBVIEW_API void webview_init(webview_t w, const char* js);
+
+ // Evaluates arbitrary JavaScript code. Evaluation happens asynchronously, also
+ // the result of the expression is ignored. Use RPC bindings if you want to
+ // receive notifications about the results of the evaluation.
+ WEBVIEW_API void webview_eval(webview_t w, const char* js);
+
+ // Binds a native C callback so that it will appear under the given name as a
+ // global JavaScript function. Internally it uses webview_init(). Callback
+ // receives a request string and a user-provided argument pointer. Request
+ // string is a JSON array of all the arguments passed to the JavaScript
+ // function.
+ WEBVIEW_API void webview_bind(webview_t w, const char* name,
+ void (*fn)(const char* seq, const char* req, void* arg),
+ void* arg);
+
+ // Removes a native C callback that was previously set by webview_bind.
+ WEBVIEW_API void webview_unbind(webview_t w, const char* name);
+
+ // Allows to return a value from the native binding. Original request pointer
+ // must be provided to help internal RPC engine match requests with responses.
+ // If status is zero - result is expected to be a valid JSON result value.
+ // If status is not zero - result is an error JSON object.
+ WEBVIEW_API void webview_return(webview_t w, const char* seq, int status, const char* result);
+
+ // Get the library's version information.
+ // @since 0.10
+ WEBVIEW_API const webview_version_info_t* webview_version();
+
+#ifdef __cplusplus
+}
+
+#ifndef WEBVIEW_HEADER
+
+#if !defined(WEBVIEW_GTK) && !defined(WEBVIEW_COCOA) && !defined(WEBVIEW_EDGE)
+#if defined(__APPLE__)
+#define WEBVIEW_COCOA
+#elif defined(__unix__)
+#define WEBVIEW_GTK
+#elif defined(_WIN32)
+#define WEBVIEW_EDGE
+#else
+#error "please, specify webview backend"
+#endif
+#endif
+
+#ifndef WEBVIEW_DEPRECATED
+#if __cplusplus >= 201402L
+#define WEBVIEW_DEPRECATED(reason) [[deprecated(reason)]]
+#elif defined(_MSC_VER)
+#define WEBVIEW_DEPRECATED(reason) __declspec(deprecated(reason))
+#else
+#define WEBVIEW_DEPRECATED(reason) __attribute__((deprecated(reason)))
+#endif
+#endif
+
+#ifndef WEBVIEW_DEPRECATED_PRIVATE
+#define WEBVIEW_DEPRECATED_PRIVATE WEBVIEW_DEPRECATED("Private API should not be used")
+#endif
+
+#include <array>
+#include <atomic>
+#include <functional>
+#include <future>
+#include <map>
+#include <string>
+#include <utility>
+#include <vector>
+#include <locale>
+#include <codecvt>
+#include <cstring>
+
+namespace webview
+{
+
+ using dispatch_fn_t = std::function<void()>;
+
+ namespace detail
+ {
+
+ // The library's version information.
+ constexpr const webview_version_info_t library_version_info{
+ { WEBVIEW_VERSION_MAJOR, WEBVIEW_VERSION_MINOR, WEBVIEW_VERSION_PATCH },
+ WEBVIEW_VERSION_NUMBER,
+ WEBVIEW_VERSION_PRE_RELEASE,
+ WEBVIEW_VERSION_BUILD_METADATA
+ };
+
+ inline int json_parse_c(const char* s, size_t sz, const char* key, size_t keysz,
+ const char** value, size_t* valuesz)
+ {
+ enum
+ {
+ JSON_STATE_VALUE,
+ JSON_STATE_LITERAL,
+ JSON_STATE_STRING,
+ JSON_STATE_ESCAPE,
+ JSON_STATE_UTF8
+ } state = JSON_STATE_VALUE;
+ const char* k = nullptr;
+ int index = 1;
+ int depth = 0;
+ int utf8_bytes = 0;
+
+ *value = nullptr;
+ *valuesz = 0;
+
+ if (key == nullptr)
+ {
+ index = static_cast<decltype(index)>(keysz);
+ if (index < 0)
+ {
+ return -1;
+ }
+ keysz = 0;
+ }
+
+ for (; sz > 0; s++, sz--)
+ {
+ enum
+ {
+ JSON_ACTION_NONE,
+ JSON_ACTION_START,
+ JSON_ACTION_END,
+ JSON_ACTION_START_STRUCT,
+ JSON_ACTION_END_STRUCT
+ } action = JSON_ACTION_NONE;
+ auto c = static_cast<unsigned char>(*s);
+ switch (state)
+ {
+ case JSON_STATE_VALUE:
+ if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == ',' || c == ':')
+ {
+ continue;
+ }
+ else if (c == '"')
+ {
+ action = JSON_ACTION_START;
+ state = JSON_STATE_STRING;
+ }
+ else if (c == '{' || c == '[')
+ {
+ action = JSON_ACTION_START_STRUCT;
+ }
+ else if (c == '}' || c == ']')
+ {
+ action = JSON_ACTION_END_STRUCT;
+ }
+ else if (c == 't' || c == 'f' || c == 'n' || c == '-' ||
+ (c >= '0' && c <= '9'))
+ {
+ action = JSON_ACTION_START;
+ state = JSON_STATE_LITERAL;
+ }
+ else
+ {
+ return -1;
+ }
+ break;
+ case JSON_STATE_LITERAL:
+ if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == ',' ||
+ c == ']' || c == '}' || c == ':')
+ {
+ state = JSON_STATE_VALUE;
+ s--;
+ sz++;
+ action = JSON_ACTION_END;
+ }
+ else if (c < 32 || c > 126)
+ {
+ return -1;
+ } // fallthrough
+ case JSON_STATE_STRING:
+ if (c < 32 || (c > 126 && c < 192))
+ {
+ return -1;
+ }
+ else if (c == '"')
+ {
+ action = JSON_ACTION_END;
+ state = JSON_STATE_VALUE;
+ }
+ else if (c == '\\')
+ {
+ state = JSON_STATE_ESCAPE;
+ }
+ else if (c >= 192 && c < 224)
+ {
+ utf8_bytes = 1;
+ state = JSON_STATE_UTF8;
+ }
+ else if (c >= 224 && c < 240)
+ {
+ utf8_bytes = 2;
+ state = JSON_STATE_UTF8;
+ }
+ else if (c >= 240 && c < 247)
+ {
+ utf8_bytes = 3;
+ state = JSON_STATE_UTF8;
+ }
+ else if (c >= 128 && c < 192)
+ {
+ return -1;
+ }
+ break;
+ case JSON_STATE_ESCAPE:
+ if (c == '"' || c == '\\' || c == '/' || c == 'b' || c == 'f' || c == 'n' ||
+ c == 'r' || c == 't' || c == 'u')
+ {
+ state = JSON_STATE_STRING;
+ }
+ else
+ {
+ return -1;
+ }
+ break;
+ case JSON_STATE_UTF8:
+ if (c < 128 || c > 191)
+ {
+ return -1;
+ }
+ utf8_bytes--;
+ if (utf8_bytes == 0)
+ {
+ state = JSON_STATE_STRING;
+ }
+ break;
+ default:
+ return -1;
+ }
+
+ if (action == JSON_ACTION_END_STRUCT)
+ {
+ depth--;
+ }
+
+ if (depth == 1)
+ {
+ if (action == JSON_ACTION_START || action == JSON_ACTION_START_STRUCT)
+ {
+ if (index == 0)
+ {
+ *value = s;
+ }
+ else if (keysz > 0 && index == 1)
+ {
+ k = s;
+ }
+ else
+ {
+ index--;
+ }
+ }
+ else if (action == JSON_ACTION_END || action == JSON_ACTION_END_STRUCT)
+ {
+ if (*value != nullptr && index == 0)
+ {
+ *valuesz = (size_t)(s + 1 - *value);
+ return 0;
+ }
+ else if (keysz > 0 && k != nullptr)
+ {
+ if (keysz == (size_t)(s - k - 1) && memcmp(key, k + 1, keysz) == 0)
+ {
+ index = 0;
+ }
+ else
+ {
+ index = 2;
+ }
+ k = nullptr;
+ }
+ }
+ }
+
+ if (action == JSON_ACTION_START_STRUCT)
+ {
+ depth++;
+ }
+ }
+ return -1;
+ }
+
+ inline std::string json_escape(const std::string& s)
+ {
+ // TODO: implement
+ return '"' + s + '"';
+ }
+
+ inline int json_unescape(const char* s, size_t n, char* out)
+ {
+ int r = 0;
+ if (*s++ != '"')
+ {
+ return -1;
+ }
+ while (n > 2)
+ {
+ char c = *s;
+ if (c == '\\')
+ {
+ s++;
+ n--;
+ switch (*s)
+ {
+ case 'b':
+ c = '\b';
+ break;
+ case 'f':
+ c = '\f';
+ break;
+ case 'n':
+ c = '\n';
+ break;
+ case 'r':
+ c = '\r';
+ break;
+ case 't':
+ c = '\t';
+ break;
+ case '\\':
+ c = '\\';
+ break;
+ case '/':
+ c = '/';
+ break;
+ case '\"':
+ c = '\"';
+ break;
+ default: // TODO: support unicode decoding
+ return -1;
+ }
+ }
+ if (out != nullptr)
+ {
+ *out++ = c;
+ }
+ s++;
+ n--;
+ r++;
+ }
+ if (*s != '"')
+ {
+ return -1;
+ }
+ if (out != nullptr)
+ {
+ *out = '\0';
+ }
+ return r;
+ }
+
+ inline std::string json_parse(const std::string& s, const std::string& key, const int index)
+ {
+ const char* value = nullptr;
+ size_t value_sz = 0;
+ if (key.empty())
+ {
+ json_parse_c(s.c_str(), s.length(), nullptr, index, &value, &value_sz);
+ }
+ else
+ {
+ json_parse_c(s.c_str(), s.length(), key.c_str(), key.length(), &value, &value_sz);
+ }
+ if (value != nullptr)
+ {
+ if (value[0] != '"')
+ {
+ return { value, value_sz };
+ }
+ int n = json_unescape(value, value_sz, nullptr);
+ if (n > 0)
+ {
+ char* decoded = new char[n + 1];
+ json_unescape(value, value_sz, decoded);
+ std::string result(decoded, n);
+ delete[] decoded;
+ return result;
+ }
+ }
+ return "";
+ }
+
+ } // namespace detail
+
+ WEBVIEW_DEPRECATED_PRIVATE
+ inline int json_parse_c(const char* s, size_t sz, const char* key, size_t keysz,
+ const char** value, size_t* valuesz)
+ {
+ return detail::json_parse_c(s, sz, key, keysz, value, valuesz);
+ }
+
+ WEBVIEW_DEPRECATED_PRIVATE
+ inline std::string json_escape(const std::string& s)
+ {
+ return detail::json_escape(s);
+ }
+
+ WEBVIEW_DEPRECATED_PRIVATE
+ inline int json_unescape(const char* s, size_t n, char* out)
+ {
+ return detail::json_unescape(s, n, out);
+ }
+
+ WEBVIEW_DEPRECATED_PRIVATE
+ inline std::string json_parse(const std::string& s, const std::string& key, const int index)
+ {
+ return detail::json_parse(s, key, index);
+ }
+
+} // namespace webview
+
+#if defined(WEBVIEW_GTK)
+//
+// ====================================================================
+//
+// This implementation uses webkit2gtk backend. It requires gtk+3.0 and
+// webkit2gtk-4.0 libraries. Proper compiler flags can be retrieved via:
+//
+// pkg-config --cflags --libs gtk+-3.0 webkit2gtk-4.0
+//
+// ====================================================================
+//
+#include <JavaScriptCore/JavaScript.h>
+#include <gtk/gtk.h>
+#include <webkit2/webkit2.h>
+
+namespace webview
+{
+ namespace detail
+ {
+
+ class gtk_webkit_engine
+ {
+ public:
+ gtk_webkit_engine(bool debug, void* window) : m_window(static_cast<GtkWidget*>(window))
+ {
+ if (gtk_init_check(nullptr, nullptr) == FALSE)
+ {
+ return;
+ }
+ m_window = static_cast<GtkWidget*>(window);
+ if (m_window == nullptr)
+ {
+ m_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ }
+ g_signal_connect(G_OBJECT(m_window), "destroy",
+ G_CALLBACK(+[](GtkWidget*, gpointer arg) {
+ static_cast<gtk_webkit_engine*>(arg)->terminate();
+ }),
+ this);
+ // Initialize webview widget
+ m_webview = webkit_web_view_new();
+ WebKitUserContentManager* manager =
+ webkit_web_view_get_user_content_manager(WEBKIT_WEB_VIEW(m_webview));
+ g_signal_connect(manager, "script-message-received::external",
+ G_CALLBACK(+[](WebKitUserContentManager*,
+ WebKitJavascriptResult* r, gpointer arg) {
+ auto* w = static_cast<gtk_webkit_engine*>(arg);
+ char* s = get_string_from_js_result(r);
+ w->on_message(s);
+ g_free(s);
+ }),
+ this);
+ webkit_user_content_manager_register_script_message_handler(manager, "external");
+ init("window.external={invoke:function(s){window.webkit.messageHandlers."
+ "external.postMessage(s);}}");
+
+ gtk_container_add(GTK_CONTAINER(m_window), GTK_WIDGET(m_webview));
+ gtk_widget_grab_focus(GTK_WIDGET(m_webview));
+
+ WebKitSettings* settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(m_webview));
+ webkit_settings_set_javascript_can_access_clipboard(settings, true);
+ if (debug)
+ {
+ webkit_settings_set_enable_write_console_messages_to_stdout(settings, true);
+ webkit_settings_set_enable_developer_extras(settings, true);
+ }
+
+ gtk_widget_show_all(m_window);
+ }
+ virtual ~gtk_webkit_engine() = default;
+ void* window()
+ {
+ return (void*)m_window;
+ }
+ void run()
+ {
+ gtk_main();
+ }
+ void terminate()
+ {
+ gtk_main_quit();
+ }
+ void dispatch(std::function<void()> f)
+ {
+ g_idle_add_full(G_PRIORITY_HIGH_IDLE, (GSourceFunc)([](void* f) -> int {
+ (*static_cast<dispatch_fn_t*>(f))();
+ return G_SOURCE_REMOVE;
+ }),
+ new std::function<void()>(f),
+ [](void* f) { delete static_cast<dispatch_fn_t*>(f); });
+ }
+
+ void set_title(const std::string& title)
+ {
+ gtk_window_set_title(GTK_WINDOW(m_window), title.c_str());
+ }
+
+ void set_size(int width, int height, int hints)
+ {
+ gtk_window_set_resizable(GTK_WINDOW(m_window), hints != WEBVIEW_HINT_FIXED);
+ if (hints == WEBVIEW_HINT_NONE)
+ {
+ gtk_window_resize(GTK_WINDOW(m_window), width, height);
+ }
+ else if (hints == WEBVIEW_HINT_FIXED)
+ {
+ gtk_widget_set_size_request(m_window, width, height);
+ }
+ else
+ {
+ GdkGeometry g;
+ g.min_width = g.max_width = width;
+ g.min_height = g.max_height = height;
+ GdkWindowHints h =
+ (hints == WEBVIEW_HINT_MIN ? GDK_HINT_MIN_SIZE : GDK_HINT_MAX_SIZE);
+ // This defines either MIN_SIZE, or MAX_SIZE, but not both:
+ gtk_window_set_geometry_hints(GTK_WINDOW(m_window), nullptr, &g, h);
+ }
+ }
+
+ void navigate(const std::string& url)
+ {
+ webkit_web_view_load_uri(WEBKIT_WEB_VIEW(m_webview), url.c_str());
+ }
+
+ void add_navigate_listener(std::function<void(const std::string&, void*)> callback,
+ void* arg)
+ {
+ g_signal_connect(WEBKIT_WEB_VIEW(m_webview), "load-changed",
+ G_CALLBACK(on_load_changed), this);
+ navigateCallbackArg = arg;
+ navigateCallback = std::move(callback);
+ }
+
+ void add_scheme_handler(const std::string& scheme,
+ std::function<void(const std::string&, void*)> callback,
+ void* arg)
+ {
+ auto view = WEBKIT_WEB_VIEW(m_webview);
+ auto context = webkit_web_view_get_context(view);
+
+ scheme_handlers.insert({ scheme, { .arg = arg, .fkt = callback } });
+ webkit_web_context_register_uri_scheme(context, scheme.c_str(), scheme_handler,
+ static_cast<gpointer>(this), nullptr);
+ }
+
+ void set_html(const std::string& html)
+ {
+ webkit_web_view_load_html(WEBKIT_WEB_VIEW(m_webview), html.c_str(), nullptr);
+ }
+
+ void init(const std::string& js)
+ {
+ WebKitUserContentManager* manager =
+ webkit_web_view_get_user_content_manager(WEBKIT_WEB_VIEW(m_webview));
+ webkit_user_content_manager_add_script(
+ manager, webkit_user_script_new(
+ js.c_str(), WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
+ WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nullptr, nullptr));
+ }
+
+ void eval(const std::string& js)
+ {
+ webkit_web_view_run_javascript(WEBKIT_WEB_VIEW(m_webview), js.c_str(), nullptr,
+ nullptr, nullptr);
+ }
+
+ private:
+ virtual void on_message(const std::string& msg) = 0;
+
+ struct handler_t
+ {
+ void* arg;
+ std::function<void(const std::string&, void*)> fkt;
+ };
+
+ std::map<std::string, handler_t> scheme_handlers;
+
+ void scheme_handler_call(const std::string& scheme, const std::string& url)
+ {
+ auto handler = scheme_handlers.find(scheme);
+ if (handler != scheme_handlers.end())
+ {
+ const auto& arg = handler->second;
+ arg.fkt(url, arg.arg);
+ }
+ }
+
+ static void scheme_handler(WebKitURISchemeRequest* request, gpointer user_data)
+ {
+ auto _this = static_cast<gtk_webkit_engine*>(user_data);
+
+ auto scheme = webkit_uri_scheme_request_get_scheme(request);
+ auto uri = webkit_uri_scheme_request_get_uri(request);
+ _this->scheme_handler_call(scheme, uri);
+ }
+
+ static char* get_string_from_js_result(WebKitJavascriptResult* r)
+ {
+ char* s = nullptr;
+#if WEBKIT_MAJOR_VERSION >= 2 && WEBKIT_MINOR_VERSION >= 22
+ JSCValue* value = webkit_javascript_result_get_js_value(r);
+ s = jsc_value_to_string(value);
+#else
+ JSGlobalContextRef ctx = webkit_javascript_result_get_global_context(r);
+ JSValueRef value = webkit_javascript_result_get_value(r);
+ JSStringRef js = JSValueToStringCopy(ctx, value, nullptr);
+ size_t n = JSStringGetMaximumUTF8CStringSize(js);
+ s = g_new(char, n);
+ JSStringGetUTF8CString(js, s, n);
+ JSStringRelease(js);
+#endif
+ return s;
+ }
+
+ GtkWidget* m_window;
+ GtkWidget* m_webview;
+
+ void* navigateCallbackArg = nullptr;
+ std::function<void(const std::string&, void*)> navigateCallback = nullptr;
+
+ static void on_load_changed(WebKitWebView* web_view, WebKitLoadEvent load_event,
+ gpointer arg)
+ {
+ if (load_event == WEBKIT_LOAD_FINISHED)
+ {
+ auto inst = static_cast<gtk_webkit_engine*>(arg);
+ inst->navigateCallback(webkit_web_view_get_uri(web_view),
+ inst->navigateCallbackArg);
+ }
+ }
+ };
+
+ } // namespace detail
+
+ using browser_engine = detail::gtk_webkit_engine;
+
+} // namespace webview
+
+#elif defined(WEBVIEW_COCOA)
+
+//
+// ====================================================================
+//
+// This implementation uses Cocoa WKWebView backend on macOS. It is
+// written using ObjC runtime and uses WKWebView class as a browser runtime.
+// You should pass "-framework Webkit" flag to the compiler.
+//
+// ====================================================================
+//
+
+#include <CoreGraphics/CoreGraphics.h>
+#include <objc/NSObjCRuntime.h>
+#include <objc/objc-runtime.h>
+
+namespace webview
+{
+ namespace detail
+ {
+ namespace objc
+ {
+
+ // A convenient template function for unconditionally casting the specified
+ // C-like function into a function that can be called with the given return
+ // type and arguments. Caller takes full responsibility for ensuring that
+ // the function call is valid. It is assumed that the function will not
+ // throw exceptions.
+ template <typename Result, typename Callable, typename... Args>
+ Result invoke(Callable callable, Args... args) noexcept
+ {
+ return reinterpret_cast<Result (*)(Args...)>(callable)(args...);
+ }
+
+ // Calls objc_msgSend.
+ template <typename Result, typename... Args> Result msg_send(Args... args) noexcept
+ {
+ return invoke<Result>(objc_msgSend, args...);
+ }
+
+ } // namespace objc
+
+ enum NSBackingStoreType : NSUInteger
+ {
+ NSBackingStoreBuffered = 2
+ };
+
+ enum NSWindowStyleMask : NSUInteger
+ {
+ NSWindowStyleMaskTitled = 1,
+ NSWindowStyleMaskClosable = 2,
+ NSWindowStyleMaskMiniaturizable = 4,
+ NSWindowStyleMaskResizable = 8
+ };
+
+ enum NSApplicationActivationPolicy : NSInteger
+ {
+ NSApplicationActivationPolicyRegular = 0
+ };
+
+ enum WKUserScriptInjectionTime : NSInteger
+ {
+ WKUserScriptInjectionTimeAtDocumentStart = 0
+ };
+
+ enum NSModalResponse : NSInteger
+ {
+ NSModalResponseOK = 1
+ };
+
+ // Convenient conversion of string literals.
+ inline id operator"" _cls(const char* s, std::size_t)
+ {
+ return (id)objc_getClass(s);
+ }
+ inline SEL operator"" _sel(const char* s, std::size_t)
+ {
+ return sel_registerName(s);
+ }
+ inline id operator"" _str(const char* s, std::size_t)
+ {
+ return objc::msg_send<id>("NSString"_cls, "stringWithUTF8String:"_sel, s);
+ }
+
+ class cocoa_wkwebview_engine
+ {
+ public:
+ cocoa_wkwebview_engine(bool debug, void* window)
+ : m_debug{ debug }, m_parent_window{ window }
+ {
+ auto app = get_shared_application();
+ auto delegate = create_app_delegate();
+ objc_setAssociatedObject(delegate, "webview", (id)this, OBJC_ASSOCIATION_ASSIGN);
+ objc::msg_send<void>(app, "setDelegate:"_sel, delegate);
+
+ // See comments related to application lifecycle in create_app_delegate().
+ if (window)
+ {
+ on_application_did_finish_launching(delegate, app);
+ }
+ else
+ {
+ // Start the main run loop so that the app delegate gets the
+ // NSApplicationDidFinishLaunchingNotification notification after the run
+ // loop has started in order to perform further initialization.
+ // We need to return from this constructor so this run loop is only
+ // temporary.
+ objc::msg_send<void>(app, "run"_sel);
+ }
+ }
+ virtual ~cocoa_wkwebview_engine() = default;
+ void* window()
+ {
+ return (void*)m_window;
+ }
+ void terminate()
+ {
+ auto app = get_shared_application();
+ objc::msg_send<void>(app, "terminate:"_sel, nullptr);
+ }
+ void run()
+ {
+ auto app = get_shared_application();
+ objc::msg_send<void>(app, "run"_sel);
+ }
+ void dispatch(std::function<void()> f)
+ {
+ dispatch_async_f(dispatch_get_main_queue(), new dispatch_fn_t(f),
+ (dispatch_function_t)([](void* arg) {
+ auto f = static_cast<dispatch_fn_t*>(arg);
+ (*f)();
+ delete f;
+ }));
+ }
+ void set_title(const std::string& title)
+ {
+ objc::msg_send<void>(
+ m_window, "setTitle:"_sel,
+ objc::msg_send<id>("NSString"_cls, "stringWithUTF8String:"_sel, title.c_str()));
+ }
+ void set_size(int width, int height, int hints)
+ {
+ auto style = static_cast<NSWindowStyleMask>(NSWindowStyleMaskTitled |
+ NSWindowStyleMaskClosable |
+ NSWindowStyleMaskMiniaturizable);
+ if (hints != WEBVIEW_HINT_FIXED)
+ {
+ style = static_cast<NSWindowStyleMask>(style | NSWindowStyleMaskResizable);
+ }
+ objc::msg_send<void>(m_window, "setStyleMask:"_sel, style);
+
+ if (hints == WEBVIEW_HINT_MIN)
+ {
+ objc::msg_send<void>(m_window, "setContentMinSize:"_sel,
+ CGSizeMake(width, height));
+ }
+ else if (hints == WEBVIEW_HINT_MAX)
+ {
+ objc::msg_send<void>(m_window, "setContentMaxSize:"_sel,
+ CGSizeMake(width, height));
+ }
+ else
+ {
+ objc::msg_send<void>(m_window, "setFrame:display:animate:"_sel,
+ CGRectMake(0, 0, width, height), YES, NO);
+ }
+ objc::msg_send<void>(m_window, "center"_sel);
+ }
+ void navigate(const std::string& url)
+ {
+ auto nsurl = objc::msg_send<id>(
+ "NSURL"_cls, "URLWithString:"_sel,
+ objc::msg_send<id>("NSString"_cls, "stringWithUTF8String:"_sel, url.c_str()));
+
+ objc::msg_send<void>(
+ m_webview, "loadRequest:"_sel,
+ objc::msg_send<id>("NSURLRequest"_cls, "requestWithURL:"_sel, nsurl));
+ }
+
+ void add_navigate_listener(std::function<void(const std::string&, void*)> callback,
+ void* arg)
+ {
+ m_navigateCallback = callback;
+ m_navigateCallbackArg = arg;
+ }
+
+ void set_html(const std::string& html)
+ {
+ objc::msg_send<void>(
+ m_webview, "loadHTMLString:baseURL:"_sel,
+ objc::msg_send<id>("NSString"_cls, "stringWithUTF8String:"_sel, html.c_str()),
+ nullptr);
+ }
+ void init(const std::string& js)
+ {
+ // Equivalent Obj-C:
+ // [m_manager addUserScript:[[WKUserScript alloc] initWithSource:[NSString
+ // stringWithUTF8String:js.c_str()]
+ // injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]]
+ objc::msg_send<void>(
+ m_manager, "addUserScript:"_sel,
+ objc::msg_send<id>(
+ objc::msg_send<id>("WKUserScript"_cls, "alloc"_sel),
+ "initWithSource:injectionTime:forMainFrameOnly:"_sel,
+ objc::msg_send<id>("NSString"_cls, "stringWithUTF8String:"_sel, js.c_str()),
+ WKUserScriptInjectionTimeAtDocumentStart, YES));
+ }
+ void eval(const std::string& js)
+ {
+ objc::msg_send<void>(
+ m_webview, "evaluateJavaScript:completionHandler:"_sel,
+ objc::msg_send<id>("NSString"_cls, "stringWithUTF8String:"_sel, js.c_str()),
+ nullptr);
+ }
+
+ private:
+ virtual void on_message(const std::string& msg) = 0;
+ id create_app_delegate()
+ {
+ // Note: Avoid registering the class name "AppDelegate" as it is the
+ // default name in projects created with Xcode, and using the same name
+ // causes objc_registerClassPair to crash.
+ auto cls =
+ objc_allocateClassPair((Class) "NSResponder"_cls, "WebviewAppDelegate", 0);
+ class_addProtocol(cls, objc_getProtocol("NSTouchBarProvider"));
+ class_addMethod(cls, "applicationShouldTerminateAfterLastWindowClosed:"_sel,
+ (IMP)(+[](id, SEL, id) -> BOOL { return 1; }), "c@:@");
+ // If the library was not initialized with an existing window then the user
+ // is likely managing the application lifecycle and we would not get the
+ // "applicationDidFinishLaunching:" message and therefore do not need to
+ // add this method.
+ if (!m_parent_window)
+ {
+ class_addMethod(cls, "applicationDidFinishLaunching:"_sel,
+ (IMP)(+[](id self, SEL, id notification) {
+ auto app = objc::msg_send<id>(notification, "object"_sel);
+ auto w = get_associated_webview(self);
+ w->on_application_did_finish_launching(self, app);
+ }),
+ "v@:@");
+ }
+ objc_registerClassPair(cls);
+ return objc::msg_send<id>((id)cls, "new"_sel);
+ }
+ id create_script_message_handler()
+ {
+ auto cls = objc_allocateClassPair((Class) "NSResponder"_cls,
+ "WebkitScriptMessageHandler", 0);
+ class_addProtocol(cls, objc_getProtocol("WKScriptMessageHandler"));
+ class_addMethod(cls, "userContentController:didReceiveScriptMessage:"_sel,
+ (IMP)(+[](id self, SEL, id, id msg) {
+ auto w = get_associated_webview(self);
+ w->on_message(objc::msg_send<const char*>(
+ objc::msg_send<id>(msg, "body"_sel), "UTF8String"_sel));
+ }),
+ "v@:@@");
+ objc_registerClassPair(cls);
+ auto instance = objc::msg_send<id>((id)cls, "new"_sel);
+ objc_setAssociatedObject(instance, "webview", (id)this, OBJC_ASSOCIATION_ASSIGN);
+ return instance;
+ }
+ static id create_webkit_ui_delegate()
+ {
+ auto cls = objc_allocateClassPair((Class) "NSObject"_cls, "WebkitUIDelegate", 0);
+ class_addProtocol(cls, objc_getProtocol("WKUIDelegate"));
+ class_addMethod(
+ cls,
+ "webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:"_sel,
+ (IMP)(+[](id, SEL, id, id parameters, id, id completion_handler) {
+ auto allows_multiple_selection =
+ objc::msg_send<BOOL>(parameters, "allowsMultipleSelection"_sel);
+ auto allows_directories =
+ objc::msg_send<BOOL>(parameters, "allowsDirectories"_sel);
+
+ // Show a panel for selecting files.
+ auto panel = objc::msg_send<id>("NSOpenPanel"_cls, "openPanel"_sel);
+ objc::msg_send<void>(panel, "setCanChooseFiles:"_sel, YES);
+ objc::msg_send<void>(panel, "setCanChooseDirectories:"_sel,
+ allows_directories);
+ objc::msg_send<void>(panel, "setAllowsMultipleSelection:"_sel,
+ allows_multiple_selection);
+ auto modal_response =
+ objc::msg_send<NSModalResponse>(panel, "runModal"_sel);
+
+ // Get the URLs for the selected files. If the modal was canceled
+ // then we pass null to the completion handler to signify
+ // cancellation.
+ id urls = modal_response == NSModalResponseOK
+ ? objc::msg_send<id>(panel, "URLs"_sel)
+ : nullptr;
+
+ // Invoke the completion handler block.
+ auto sig = objc::msg_send<id>("NSMethodSignature"_cls,
+ "signatureWithObjCTypes:"_sel, "v@?@");
+ auto invocation = objc::msg_send<id>(
+ "NSInvocation"_cls, "invocationWithMethodSignature:"_sel, sig);
+ objc::msg_send<void>(invocation, "setTarget:"_sel, completion_handler);
+ objc::msg_send<void>(invocation, "setArgument:atIndex:"_sel, &urls, 1);
+ objc::msg_send<void>(invocation, "invoke"_sel);
+ }),
+ "v@:@@@@");
+ objc_registerClassPair(cls);
+ return objc::msg_send<id>((id)cls, "new"_sel);
+ }
+ id create_webkit_navigation_delegate()
+ {
+ auto cls =
+ objc_allocateClassPair((Class) "NSObject"_cls, "WebkitNavigationDelegate", 0);
+ class_addProtocol(cls, objc_getProtocol("WKNavigationDelegate"));
+ class_addMethod(cls, "webView:didFinishNavigation:"_sel,
+ (IMP)(+[](id delegate, SEL sel, id webview, id navigation) {
+ auto w = get_associated_webview(delegate);
+ auto url = objc::msg_send<id>(webview, "URL"_sel);
+ auto nstr = objc::msg_send<id>(url, "absoluteString"_sel);
+ auto str = objc::msg_send<char*>(nstr, "UTF8String"_sel);
+ w->m_navigateCallback(str, w->m_navigateCallbackArg);
+ }),
+ "v@:@");
+ objc_registerClassPair(cls);
+ auto instance = objc::msg_send<id>((id)cls, "new"_sel);
+ objc_setAssociatedObject(instance, "webview", (id)this, OBJC_ASSOCIATION_ASSIGN);
+ return instance;
+ }
+ static id get_shared_application()
+ {
+ return objc::msg_send<id>("NSApplication"_cls, "sharedApplication"_sel);
+ }
+ static cocoa_wkwebview_engine* get_associated_webview(id object)
+ {
+ auto w = (cocoa_wkwebview_engine*)objc_getAssociatedObject(object, "webview");
+ assert(w);
+ return w;
+ }
+ static id get_main_bundle() noexcept
+ {
+ return objc::msg_send<id>("NSBundle"_cls, "mainBundle"_sel);
+ }
+ static bool is_app_bundled() noexcept
+ {
+ auto bundle = get_main_bundle();
+ if (!bundle)
+ {
+ return false;
+ }
+ auto bundle_path = objc::msg_send<id>(bundle, "bundlePath"_sel);
+ auto bundled = objc::msg_send<BOOL>(bundle_path, "hasSuffix:"_sel, ".app"_str);
+ return !!bundled;
+ }
+ void on_application_did_finish_launching(id /*delegate*/, id app)
+ {
+ // See comments related to application lifecycle in create_app_delegate().
+ if (!m_parent_window)
+ {
+ // Stop the main run loop so that we can return
+ // from the constructor.
+ objc::msg_send<void>(app, "stop:"_sel, nullptr);
+ }
+
+ // Activate the app if it is not bundled.
+ // Bundled apps launched from Finder are activated automatically but
+ // otherwise not. Activating the app even when it has been launched from
+ // Finder does not seem to be harmful but calling this function is rarely
+ // needed as proper activation is normally taken care of for us.
+ // Bundled apps have a default activation policy of
+ // NSApplicationActivationPolicyRegular while non-bundled apps have a
+ // default activation policy of NSApplicationActivationPolicyProhibited.
+ if (!is_app_bundled())
+ {
+ // "setActivationPolicy:" must be invoked before
+ // "activateIgnoringOtherApps:" for activation to work.
+ objc::msg_send<void>(app, "setActivationPolicy:"_sel,
+ NSApplicationActivationPolicyRegular);
+ // Activate the app regardless of other active apps.
+ // This can be obtrusive so we only do it when necessary.
+ objc::msg_send<void>(app, "activateIgnoringOtherApps:"_sel, YES);
+ }
+
+ // Main window
+ if (!m_parent_window)
+ {
+ m_window = objc::msg_send<id>("NSWindow"_cls, "alloc"_sel);
+ auto style = NSWindowStyleMaskTitled;
+ m_window = objc::msg_send<id>(
+ m_window, "initWithContentRect:styleMask:backing:defer:"_sel,
+ CGRectMake(0, 0, 0, 0), style, NSBackingStoreBuffered, NO);
+ }
+ else
+ {
+ m_window = (id)m_parent_window;
+ }
+
+ // Webview
+ auto config = objc::msg_send<id>("WKWebViewConfiguration"_cls, "new"_sel);
+ m_manager = objc::msg_send<id>(config, "userContentController"_sel);
+ m_webview = objc::msg_send<id>("WKWebView"_cls, "alloc"_sel);
+
+ if (m_debug)
+ {
+ // Equivalent Obj-C:
+ // [[config preferences] setValue:@YES forKey:@"developerExtrasEnabled"];
+ objc::msg_send<id>(
+ objc::msg_send<id>(config, "preferences"_sel), "setValue:forKey:"_sel,
+ objc::msg_send<id>("NSNumber"_cls, "numberWithBool:"_sel, YES),
+ "developerExtrasEnabled"_str);
+ }
+
+ // Equivalent Obj-C:
+ // [[config preferences] setValue:@YES forKey:@"fullScreenEnabled"];
+ objc::msg_send<id>(objc::msg_send<id>(config, "preferences"_sel),
+ "setValue:forKey:"_sel,
+ objc::msg_send<id>("NSNumber"_cls, "numberWithBool:"_sel, YES),
+ "fullScreenEnabled"_str);
+
+ // Equivalent Obj-C:
+ // [[config preferences] setValue:@YES forKey:@"javaScriptCanAccessClipboard"];
+ objc::msg_send<id>(objc::msg_send<id>(config, "preferences"_sel),
+ "setValue:forKey:"_sel,
+ objc::msg_send<id>("NSNumber"_cls, "numberWithBool:"_sel, YES),
+ "javaScriptCanAccessClipboard"_str);
+
+ // Equivalent Obj-C:
+ // [[config preferences] setValue:@YES forKey:@"DOMPasteAllowed"];
+ objc::msg_send<id>(objc::msg_send<id>(config, "preferences"_sel),
+ "setValue:forKey:"_sel,
+ objc::msg_send<id>("NSNumber"_cls, "numberWithBool:"_sel, YES),
+ "DOMPasteAllowed"_str);
+
+ auto ui_delegate = create_webkit_ui_delegate();
+ objc::msg_send<void>(m_webview, "initWithFrame:configuration:"_sel,
+ CGRectMake(0, 0, 0, 0), config);
+ objc::msg_send<void>(m_webview, "setUIDelegate:"_sel, ui_delegate);
+
+ auto navigation_delegate = create_webkit_navigation_delegate();
+ objc::msg_send<void>(m_webview, "setNavigationDelegate:"_sel, navigation_delegate);
+ auto script_message_handler = create_script_message_handler();
+ objc::msg_send<void>(m_manager, "addScriptMessageHandler:name:"_sel,
+ script_message_handler, "external"_str);
+
+ init(R""(
+ window.external = {
+ invoke: function(s) {
+ window.webkit.messageHandlers.external.postMessage(s);
+ },
+ };
+ )"");
+ objc::msg_send<void>(m_window, "setContentView:"_sel, m_webview);
+ objc::msg_send<void>(m_window, "makeKeyAndOrderFront:"_sel, nullptr);
+ }
+ bool m_debug;
+ void* m_parent_window;
+ id m_window;
+ id m_webview;
+ id m_manager;
+ void* m_navigateCallbackArg = nullptr;
+ std::function<void(const std::string&, void*)> m_navigateCallback = 0;
+ };
+
+ } // namespace detail
+
+ using browser_engine = detail::cocoa_wkwebview_engine;
+
+} // namespace webview
+
+#elif defined(WEBVIEW_EDGE)
+
+//
+// ====================================================================
+//
+// This implementation uses Win32 API to create a native window. It
+// uses Edge/Chromium webview2 backend as a browser engine.
+//
+// ====================================================================
+//
+
+#define WIN32_LEAN_AND_MEAN
+#include <shlobj.h>
+#include <shlwapi.h>
+#include <stdlib.h>
+#include <windows.h>
+
+#include "WebView2.h"
+
+#ifdef _MSC_VER
+#pragma comment(lib, "advapi32.lib")
+#pragma comment(lib, "ole32.lib")
+#pragma comment(lib, "shell32.lib")
+#pragma comment(lib, "shlwapi.lib")
+#pragma comment(lib, "user32.lib")
+#pragma comment(lib, "version.lib")
+#endif
+
+namespace webview
+{
+ namespace detail
+ {
+
+ using msg_cb_t = std::function<void(const std::string)>;
+
+ // Converts a narrow (UTF-8-encoded) string into a wide (UTF-16-encoded) string.
+ inline std::wstring widen_string(const std::string& input)
+ {
+ if (input.empty())
+ {
+ return std::wstring();
+ }
+ UINT cp = CP_UTF8;
+ DWORD flags = MB_ERR_INVALID_CHARS;
+ auto input_c = input.c_str();
+ auto input_length = static_cast<int>(input.size());
+ auto required_length =
+ MultiByteToWideChar(cp, flags, input_c, input_length, nullptr, 0);
+ if (required_length > 0)
+ {
+ std::wstring output(static_cast<std::size_t>(required_length), L'\0');
+ if (MultiByteToWideChar(cp, flags, input_c, input_length, &output[0],
+ required_length) > 0)
+ {
+ return output;
+ }
+ }
+ // Failed to convert string from UTF-8 to UTF-16
+ return std::wstring();
+ }
+
+ // Converts a wide (UTF-16-encoded) string into a narrow (UTF-8-encoded) string.
+ inline std::string narrow_string(const std::wstring& input)
+ {
+ if (input.empty())
+ {
+ return std::string();
+ }
+ UINT cp = CP_UTF8;
+ DWORD flags = WC_ERR_INVALID_CHARS;
+ auto input_c = input.c_str();
+ auto input_length = static_cast<int>(input.size());
+ auto required_length =
+ WideCharToMultiByte(cp, flags, input_c, input_length, nullptr, 0, nullptr, nullptr);
+ if (required_length > 0)
+ {
+ std::string output(static_cast<std::size_t>(required_length), '\0');
+ if (WideCharToMultiByte(cp, flags, input_c, input_length, &output[0],
+ required_length, nullptr, nullptr) > 0)
+ {
+ return output;
+ }
+ }
+ // Failed to convert string from UTF-16 to UTF-8
+ return std::string();
+ }
+
+ // Parses a version string with 1-4 integral components, e.g. "1.2.3.4".
+ // Missing or invalid components default to 0, and excess components are ignored.
+ template <typename T>
+ std::array<unsigned int, 4> parse_version(const std::basic_string<T>& version) noexcept
+ {
+ auto parse_component = [](auto sb, auto se) -> unsigned int {
+ try
+ {
+ auto n = std::stol(std::basic_string<T>(sb, se));
+ return n < 0 ? 0 : n;
+ }
+ catch (std::exception&)
+ {
+ return 0;
+ }
+ };
+ auto end = version.end();
+ auto sb = version.begin(); // subrange begin
+ auto se = sb; // subrange end
+ unsigned int ci = 0; // component index
+ std::array<unsigned int, 4> components{};
+ while (sb != end && se != end && ci < components.size())
+ {
+ if (*se == static_cast<T>('.'))
+ {
+ components[ci++] = parse_component(sb, se);
+ sb = ++se;
+ continue;
+ }
+ ++se;
+ }
+ if (sb < se && ci < components.size())
+ {
+ components[ci] = parse_component(sb, se);
+ }
+ return components;
+ }
+
+ template <typename T, std::size_t Length>
+ auto parse_version(const T (&version)[Length]) noexcept
+ {
+ return parse_version(std::basic_string<T>(version, Length));
+ }
+
+ std::wstring get_file_version_string(const std::wstring& file_path) noexcept
+ {
+ DWORD dummy_handle; // Unused
+ DWORD info_buffer_length = GetFileVersionInfoSizeW(file_path.c_str(), &dummy_handle);
+ if (info_buffer_length == 0)
+ {
+ return std::wstring();
+ }
+ std::vector<char> info_buffer;
+ info_buffer.reserve(info_buffer_length);
+ if (!GetFileVersionInfoW(file_path.c_str(), 0, info_buffer_length, info_buffer.data()))
+ {
+ return std::wstring();
+ }
+ auto sub_block = L"\\StringFileInfo\\040904B0\\ProductVersion";
+ LPWSTR version = nullptr;
+ unsigned int version_length = 0;
+ if (!VerQueryValueW(info_buffer.data(), sub_block, reinterpret_cast<LPVOID*>(&version),
+ &version_length))
+ {
+ return std::wstring();
+ }
+ if (!version || version_length == 0)
+ {
+ return std::wstring();
+ }
+ return std::wstring(version, version_length);
+ }
+
+ // A wrapper around COM library initialization. Calls CoInitializeEx in the
+ // constructor and CoUninitialize in the destructor.
+ class com_init_wrapper
+ {
+ public:
+ com_init_wrapper(DWORD dwCoInit)
+ {
+ // We can safely continue as long as COM was either successfully
+ // initialized or already initialized.
+ // RPC_E_CHANGED_MODE means that CoInitializeEx was already called with
+ // a different concurrency model.
+ switch (CoInitializeEx(nullptr, dwCoInit))
+ {
+ case S_OK:
+ case S_FALSE:
+ m_initialized = true;
+ break;
+ }
+ }
+
+ ~com_init_wrapper()
+ {
+ if (m_initialized)
+ {
+ CoUninitialize();
+ m_initialized = false;
+ }
+ }
+
+ com_init_wrapper(const com_init_wrapper& other) = delete;
+ com_init_wrapper& operator=(const com_init_wrapper& other) = delete;
+ com_init_wrapper(com_init_wrapper&& other) = delete;
+ com_init_wrapper& operator=(com_init_wrapper&& other) = delete;
+
+ bool is_initialized() const
+ {
+ return m_initialized;
+ }
+
+ private:
+ bool m_initialized = false;
+ };
+
+ // Holds a symbol name and associated type for code clarity.
+ template <typename T> class library_symbol
+ {
+ public:
+ using type = T;
+
+ constexpr explicit library_symbol(const char* name) : m_name(name)
+ {
+ }
+ constexpr const char* get_name() const
+ {
+ return m_name;
+ }
+
+ private:
+ const char* m_name;
+ };
+
+ // Loads a native shared library and allows one to get addresses for those
+ // symbols.
+ class native_library
+ {
+ public:
+ explicit native_library(const wchar_t* name) : m_handle(LoadLibraryW(name))
+ {
+ }
+
+ ~native_library()
+ {
+ if (m_handle)
+ {
+ FreeLibrary(m_handle);
+ m_handle = nullptr;
+ }
+ }
+
+ native_library(const native_library& other) = delete;
+ native_library& operator=(const native_library& other) = delete;
+ native_library(native_library&& other) = default;
+ native_library& operator=(native_library&& other) = default;
+
+ // Returns true if the library is currently loaded; otherwise false.
+ operator bool() const
+ {
+ return is_loaded();
+ }
+
+ // Get the address for the specified symbol or nullptr if not found.
+ template <typename Symbol> typename Symbol::type get(const Symbol& symbol) const
+ {
+ if (is_loaded())
+ {
+ return reinterpret_cast<typename Symbol::type>(
+ GetProcAddress(m_handle, symbol.get_name()));
+ }
+ return nullptr;
+ }
+
+ // Returns true if the library is currently loaded; otherwise false.
+ bool is_loaded() const
+ {
+ return !!m_handle;
+ }
+
+ void detach()
+ {
+ m_handle = nullptr;
+ }
+
+ private:
+ HMODULE m_handle = nullptr;
+ };
+
+ struct user32_symbols
+ {
+ using DPI_AWARENESS_CONTEXT = HANDLE;
+ using SetProcessDpiAwarenessContext_t = BOOL(WINAPI*)(DPI_AWARENESS_CONTEXT);
+ using SetProcessDPIAware_t = BOOL(WINAPI*)();
+
+ static constexpr auto SetProcessDpiAwarenessContext =
+ library_symbol<SetProcessDpiAwarenessContext_t>("SetProcessDpiAwarenessContext");
+ static constexpr auto SetProcessDPIAware =
+ library_symbol<SetProcessDPIAware_t>("SetProcessDPIAware");
+ };
+
+ struct shcore_symbols
+ {
+ typedef enum
+ {
+ PROCESS_PER_MONITOR_DPI_AWARE = 2
+ } PROCESS_DPI_AWARENESS;
+ using SetProcessDpiAwareness_t = HRESULT(WINAPI*)(PROCESS_DPI_AWARENESS);
+
+ static constexpr auto SetProcessDpiAwareness =
+ library_symbol<SetProcessDpiAwareness_t>("SetProcessDpiAwareness");
+ };
+
+ class reg_key
+ {
+ public:
+ explicit reg_key(HKEY root_key, const wchar_t* sub_key, DWORD options,
+ REGSAM sam_desired)
+ {
+ HKEY handle;
+ auto status = RegOpenKeyExW(root_key, sub_key, options, sam_desired, &handle);
+ if (status == ERROR_SUCCESS)
+ {
+ m_handle = handle;
+ }
+ }
+
+ explicit reg_key(HKEY root_key, const std::wstring& sub_key, DWORD options,
+ REGSAM sam_desired)
+ : reg_key(root_key, sub_key.c_str(), options, sam_desired)
+ {
+ }
+
+ virtual ~reg_key()
+ {
+ if (m_handle)
+ {
+ RegCloseKey(m_handle);
+ m_handle = nullptr;
+ }
+ }
+
+ reg_key(const reg_key& other) = delete;
+ reg_key& operator=(const reg_key& other) = delete;
+ reg_key(reg_key&& other) = delete;
+ reg_key& operator=(reg_key&& other) = delete;
+
+ bool is_open() const
+ {
+ return !!m_handle;
+ }
+ bool get_handle() const
+ {
+ return m_handle;
+ }
+
+ std::wstring query_string(const wchar_t* name) const
+ {
+ DWORD buf_length = 0;
+ // Get the size of the data in bytes.
+ auto status =
+ RegQueryValueExW(m_handle, name, nullptr, nullptr, nullptr, &buf_length);
+ if (status != ERROR_SUCCESS && status != ERROR_MORE_DATA)
+ {
+ return std::wstring();
+ }
+ // Read the data.
+ std::wstring result(buf_length / sizeof(wchar_t), 0);
+ auto buf = reinterpret_cast<LPBYTE>(&result[0]);
+ status = RegQueryValueExW(m_handle, name, nullptr, nullptr, buf, &buf_length);
+ if (status != ERROR_SUCCESS)
+ {
+ return std::wstring();
+ }
+ // Remove trailing null-characters.
+ for (std::size_t length = result.size(); length > 0; --length)
+ {
+ if (result[length - 1] != 0)
+ {
+ result.resize(length);
+ break;
+ }
+ }
+ return result;
+ }
+
+ private:
+ HKEY m_handle = nullptr;
+ };
+
+ inline bool enable_dpi_awareness()
+ {
+ auto user32 = native_library(L"user32.dll");
+ if (auto fn = user32.get(user32_symbols::SetProcessDpiAwarenessContext))
+ {
+ if (fn(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE))
+ {
+ return true;
+ }
+ return GetLastError() == ERROR_ACCESS_DENIED;
+ }
+ if (auto shcore = native_library(L"shcore.dll"))
+ {
+ if (auto fn = shcore.get(shcore_symbols::SetProcessDpiAwareness))
+ {
+ auto result = fn(shcore_symbols::PROCESS_PER_MONITOR_DPI_AWARE);
+ return result == S_OK || result == E_ACCESSDENIED;
+ }
+ }
+ if (auto fn = user32.get(user32_symbols::SetProcessDPIAware))
+ {
+ return !!fn();
+ }
+ return true;
+ }
+
+// Enable built-in WebView2Loader implementation by default.
+#ifndef WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL
+#define WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL 1
+#endif
+
+// Link WebView2Loader.dll explicitly by default only if the built-in
+// implementation is enabled.
+#ifndef WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK
+#define WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL
+#endif
+
+// Explicit linking of WebView2Loader.dll should be used along with
+// the built-in implementation.
+#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1 && WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK != 1
+#undef WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK
+#error Please set WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK=1.
+#endif
+
+#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1
+ // Gets the last component of a Windows native file path.
+ // For example, if the path is "C:\a\b" then the result is "b".
+ template <typename T>
+ std::basic_string<T> get_last_native_path_component(const std::basic_string<T>& path)
+ {
+ if (auto pos = path.find_last_of(static_cast<T>('\\'));
+ pos != std::basic_string<T>::npos)
+ {
+ return path.substr(pos + 1);
+ }
+ return std::basic_string<T>();
+ }
+#endif /* WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL */
+
+ template <typename T> struct cast_info_t
+ {
+ using type = T;
+ IID iid;
+ };
+
+ namespace mswebview2
+ {
+ static constexpr IID IID_ICoreWebView2CreateCoreWebView2ControllerCompletedHandler{
+ 0x6C4819F3, 0xC9B7, 0x4260, 0x81, 0x27, 0xC9, 0xF5, 0xBD, 0xE7, 0xF6, 0x8C
+ };
+ static constexpr IID IID_ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler{
+ 0x4E8A3389, 0xC9D8, 0x4BD2, 0xB6, 0xB5, 0x12, 0x4F, 0xEE, 0x6C, 0xC1, 0x4D
+ };
+ static constexpr IID IID_ICoreWebView2PermissionRequestedEventHandler{
+ 0x15E1C6A3, 0xC72A, 0x4DF3, 0x91, 0xD7, 0xD0, 0x97, 0xFB, 0xEC, 0x6B, 0xFD
+ };
+ static constexpr IID IID_ICoreWebView2WebMessageReceivedEventHandler{
+ 0x57213F19, 0x00E6, 0x49FA, 0x8E, 0x07, 0x89, 0x8E, 0xA0, 0x1E, 0xCB, 0xD2
+ };
+
+#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1
+ enum class webview2_runtime_type
+ {
+ installed = 0,
+ embedded = 1
+ };
+
+ namespace webview2_symbols
+ {
+ using CreateWebViewEnvironmentWithOptionsInternal_t = HRESULT(STDMETHODCALLTYPE*)(
+ bool, webview2_runtime_type, PCWSTR, IUnknown*,
+ ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler*);
+ using DllCanUnloadNow_t = HRESULT(STDMETHODCALLTYPE*)();
+
+ static constexpr auto CreateWebViewEnvironmentWithOptionsInternal =
+ library_symbol<CreateWebViewEnvironmentWithOptionsInternal_t>(
+ "CreateWebViewEnvironmentWithOptionsInternal");
+ static constexpr auto DllCanUnloadNow =
+ library_symbol<DllCanUnloadNow_t>("DllCanUnloadNow");
+ } // namespace webview2_symbols
+#endif /* WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL */
+
+#if WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK == 1
+ namespace webview2_symbols
+ {
+ using CreateCoreWebView2EnvironmentWithOptions_t = HRESULT(STDMETHODCALLTYPE*)(
+ PCWSTR, PCWSTR, ICoreWebView2EnvironmentOptions*,
+ ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler*);
+ using GetAvailableCoreWebView2BrowserVersionString_t =
+ HRESULT(STDMETHODCALLTYPE*)(PCWSTR, LPWSTR*);
+
+ static constexpr auto CreateCoreWebView2EnvironmentWithOptions =
+ library_symbol<CreateCoreWebView2EnvironmentWithOptions_t>(
+ "CreateCoreWebView2EnvironmentWithOptions");
+ static constexpr auto GetAvailableCoreWebView2BrowserVersionString =
+ library_symbol<GetAvailableCoreWebView2BrowserVersionString_t>(
+ "GetAvailableCoreWebView2BrowserVersionString");
+ } // namespace webview2_symbols
+#endif /* WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK */
+
+ class loader
+ {
+ public:
+ HRESULT create_environment_with_options(
+ PCWSTR browser_dir, PCWSTR user_data_dir,
+ ICoreWebView2EnvironmentOptions* env_options,
+ ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler* created_handler)
+ const
+ {
+#if WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK == 1
+ if (m_lib.is_loaded())
+ {
+ if (auto fn = m_lib.get(
+ webview2_symbols::CreateCoreWebView2EnvironmentWithOptions))
+ {
+ return fn(browser_dir, user_data_dir, env_options, created_handler);
+ }
+ }
+#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1
+ return create_environment_with_options_impl(browser_dir, user_data_dir,
+ env_options, created_handler);
+#else
+ return S_FALSE;
+#endif
+#else
+ return ::CreateCoreWebView2EnvironmentWithOptions(browser_dir, user_data_dir,
+ env_options, created_handler);
+#endif /* WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK */
+ }
+
+ HRESULT
+ get_available_browser_version_string(PCWSTR browser_dir, LPWSTR* version) const
+ {
+#if WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK == 1
+ if (m_lib.is_loaded())
+ {
+ if (auto fn = m_lib.get(
+ webview2_symbols::GetAvailableCoreWebView2BrowserVersionString))
+ {
+ return fn(browser_dir, version);
+ }
+ }
+#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1
+ return get_available_browser_version_string_impl(browser_dir, version);
+#else
+ return S_FALSE;
+#endif
+#else
+ return ::GetAvailableCoreWebView2BrowserVersionString(browser_dir, version);
+#endif /* WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK */
+ }
+
+ private:
+#if WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL == 1
+ struct client_info_t
+ {
+ bool found = false;
+ std::wstring dll_path;
+ std::wstring version;
+ webview2_runtime_type runtime_type;
+ };
+
+ HRESULT create_environment_with_options_impl(
+ PCWSTR browser_dir, PCWSTR user_data_dir,
+ ICoreWebView2EnvironmentOptions* env_options,
+ ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler* created_handler)
+ const
+ {
+ auto found_client = find_available_client(browser_dir);
+ if (!found_client.found)
+ {
+ return -1;
+ }
+ auto client_dll = native_library(found_client.dll_path.c_str());
+ if (auto fn = client_dll.get(
+ webview2_symbols::CreateWebViewEnvironmentWithOptionsInternal))
+ {
+ return fn(true, found_client.runtime_type, user_data_dir, env_options,
+ created_handler);
+ }
+ if (auto fn = client_dll.get(webview2_symbols::DllCanUnloadNow))
+ {
+ if (!fn())
+ {
+ client_dll.detach();
+ }
+ }
+ return ERROR_SUCCESS;
+ }
+
+ HRESULT
+ get_available_browser_version_string_impl(PCWSTR browser_dir, LPWSTR* version) const
+ {
+ if (!version)
+ {
+ return -1;
+ }
+ auto found_client = find_available_client(browser_dir);
+ if (!found_client.found)
+ {
+ return -1;
+ }
+ auto info_length_bytes =
+ found_client.version.size() * sizeof(found_client.version[0]);
+ auto info = static_cast<LPWSTR>(CoTaskMemAlloc(info_length_bytes));
+ if (!info)
+ {
+ return -1;
+ }
+ CopyMemory(info, found_client.version.c_str(), info_length_bytes);
+ *version = info;
+ return 0;
+ }
+
+ client_info_t find_available_client(PCWSTR browser_dir) const
+ {
+ if (browser_dir)
+ {
+ return find_embedded_client(api_version, browser_dir);
+ }
+ auto found_client =
+ find_installed_client(api_version, true, default_release_channel_guid);
+ if (!found_client.found)
+ {
+ found_client =
+ find_installed_client(api_version, false, default_release_channel_guid);
+ }
+ return found_client;
+ }
+
+ std::wstring make_client_dll_path(const std::wstring& dir) const
+ {
+ auto dll_path = dir;
+ if (!dll_path.empty())
+ {
+ auto last_char = dir[dir.size() - 1];
+ if (last_char != L'\\' && last_char != L'/')
+ {
+ dll_path += L'\\';
+ }
+ }
+ dll_path += L"EBWebView\\";
+#if defined(_M_X64) || defined(__x86_64__)
+ dll_path += L"x64";
+#elif defined(_M_IX86) || defined(__i386__)
+ dll_path += L"x86";
+#elif defined(_M_ARM64) || defined(__aarch64__)
+ dll_path += L"arm64";
+#else
+#error WebView2 integration for this platform is not yet supported.
+#endif
+ dll_path += L"\\EmbeddedBrowserWebView.dll";
+ return dll_path;
+ }
+
+ client_info_t find_installed_client(unsigned int min_api_version, bool system,
+ const std::wstring& release_channel) const
+ {
+ std::wstring sub_key = client_state_reg_sub_key;
+ sub_key += release_channel;
+ auto root_key = system ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER;
+ reg_key key(root_key, sub_key, 0, KEY_READ | KEY_WOW64_32KEY);
+ if (!key.is_open())
+ {
+ return {};
+ }
+ auto ebwebview_value = key.query_string(L"EBWebView");
+
+ auto client_version_string = get_last_native_path_component(ebwebview_value);
+ auto client_version = parse_version(client_version_string);
+ if (client_version[2] < min_api_version)
+ {
+ // Our API version is greater than the runtime API version.
+ return {};
+ }
+
+ auto client_dll_path = make_client_dll_path(ebwebview_value);
+ return { true, client_dll_path, client_version_string,
+ webview2_runtime_type::installed };
+ }
+
+ client_info_t find_embedded_client(unsigned int min_api_version,
+ const std::wstring& dir) const
+ {
+ auto client_dll_path = make_client_dll_path(dir);
+
+ auto client_version_string = get_file_version_string(client_dll_path);
+ auto client_version = parse_version(client_version_string);
+ if (client_version[2] < min_api_version)
+ {
+ // Our API version is greater than the runtime API version.
+ return {};
+ }
+
+ return { true, client_dll_path, client_version_string,
+ webview2_runtime_type::embedded };
+ }
+
+ // The minimum WebView2 API version we need regardless of the SDK release
+ // actually used. The number comes from the SDK release version,
+ // e.g. 1.0.1150.38. To be safe the SDK should have a number that is greater
+ // than or equal to this number. The Edge browser webview client must
+ // have a number greater than or equal to this number.
+ static constexpr unsigned int api_version = 1150;
+
+ static constexpr auto client_state_reg_sub_key =
+ L"SOFTWARE\\Microsoft\\EdgeUpdate\\ClientState\\";
+
+ // GUID for the stable release channel.
+ static constexpr auto stable_release_guid =
+ L"{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}";
+
+ static constexpr auto default_release_channel_guid = stable_release_guid;
+#endif /* WEBVIEW_MSWEBVIEW2_BUILTIN_IMPL */
+
+#if WEBVIEW_MSWEBVIEW2_EXPLICIT_LINK == 1
+ native_library m_lib{ L"WebView2Loader.dll" };
+#endif
+ };
+
+ namespace cast_info
+ {
+ static constexpr auto controller_completed =
+ cast_info_t<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>{
+ IID_ICoreWebView2CreateCoreWebView2ControllerCompletedHandler
+ };
+
+ static constexpr auto environment_completed =
+ cast_info_t<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>{
+ IID_ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler
+ };
+
+ static constexpr auto message_received =
+ cast_info_t<ICoreWebView2WebMessageReceivedEventHandler>{
+ IID_ICoreWebView2WebMessageReceivedEventHandler
+ };
+
+ static constexpr auto permission_requested =
+ cast_info_t<ICoreWebView2PermissionRequestedEventHandler>{
+ IID_ICoreWebView2PermissionRequestedEventHandler
+ };
+ } // namespace cast_info
+ } // namespace mswebview2
+
+ class webview2_com_handler
+ : public ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler,
+ public ICoreWebView2CreateCoreWebView2ControllerCompletedHandler,
+ public ICoreWebView2WebMessageReceivedEventHandler,
+ public ICoreWebView2PermissionRequestedEventHandler,
+ public ICoreWebView2NavigationCompletedEventHandler
+ {
+ using webview2_com_handler_cb_t =
+ std::function<void(ICoreWebView2Controller*, ICoreWebView2* webview)>;
+
+ public:
+ webview2_com_handler(HWND hwnd, msg_cb_t msgCb, webview2_com_handler_cb_t cb)
+ : m_window(hwnd), m_msgCb(msgCb), m_cb(cb)
+ {
+ }
+
+ virtual ~webview2_com_handler() = default;
+ webview2_com_handler(const webview2_com_handler& other) = delete;
+ webview2_com_handler& operator=(const webview2_com_handler& other) = delete;
+ webview2_com_handler(webview2_com_handler&& other) = delete;
+ webview2_com_handler& operator=(webview2_com_handler&& other) = delete;
+
+ ULONG STDMETHODCALLTYPE AddRef()
+ {
+ return ++m_ref_count;
+ }
+ ULONG STDMETHODCALLTYPE Release()
+ {
+ if (m_ref_count > 1)
+ {
+ return --m_ref_count;
+ }
+ delete this;
+ return 0;
+ }
+ HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID* ppv)
+ {
+ using namespace mswebview2::cast_info;
+
+ if (!ppv)
+ {
+ return E_POINTER;
+ }
+
+ // All of the COM interfaces we implement should be added here regardless
+ // of whether they are required.
+ // This is just to be on the safe side in case the WebView2 Runtime ever
+ // requests a pointer to an interface we implement.
+ // The WebView2 Runtime must at the very least be able to get a pointer to
+ // ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler when we use
+ // our custom WebView2 loader implementation, and observations have shown
+ // that it is the only interface requested in this case. None have been
+ // observed to be requested when using the official WebView2 loader.
+
+ if (cast_if_equal_iid(riid, controller_completed, ppv) ||
+ cast_if_equal_iid(riid, environment_completed, ppv) ||
+ cast_if_equal_iid(riid, message_received, ppv) ||
+ cast_if_equal_iid(riid, permission_requested, ppv))
+ {
+ return S_OK;
+ }
+
+ return E_NOINTERFACE;
+ }
+ HRESULT STDMETHODCALLTYPE Invoke(HRESULT res, ICoreWebView2Environment* env)
+ {
+ if (SUCCEEDED(res))
+ {
+ res = env->CreateCoreWebView2Controller(m_window, this);
+ if (SUCCEEDED(res))
+ {
+ return S_OK;
+ }
+ }
+ try_create_environment();
+ return S_OK;
+ }
+ HRESULT STDMETHODCALLTYPE Invoke(HRESULT res, ICoreWebView2Controller* controller)
+ {
+ if (FAILED(res))
+ {
+ // See try_create_environment() regarding
+ // HRESULT_FROM_WIN32(ERROR_INVALID_STATE).
+ // The result is E_ABORT if the parent window has been destroyed already.
+ switch (res)
+ {
+ case HRESULT_FROM_WIN32(ERROR_INVALID_STATE):
+ case E_ABORT:
+ return S_OK;
+ }
+ try_create_environment();
+ return S_OK;
+ }
+
+ ICoreWebView2* webview;
+ ::EventRegistrationToken token;
+ controller->get_CoreWebView2(&webview);
+ webview->add_WebMessageReceived(this, &token);
+ webview->add_PermissionRequested(this, &token);
+ webview->add_NavigationCompleted(this, &token);
+
+ m_cb(controller, webview);
+ return S_OK;
+ }
+ HRESULT STDMETHODCALLTYPE Invoke(ICoreWebView2* sender,
+ ICoreWebView2WebMessageReceivedEventArgs* args)
+ {
+ LPWSTR message;
+ args->TryGetWebMessageAsString(&message);
+ m_msgCb(narrow_string(message));
+ sender->PostWebMessageAsString(message);
+
+ CoTaskMemFree(message);
+ return S_OK;
+ }
+ HRESULT STDMETHODCALLTYPE Invoke(ICoreWebView2* sender,
+ ICoreWebView2PermissionRequestedEventArgs* args)
+ {
+ COREWEBVIEW2_PERMISSION_KIND kind;
+ args->get_PermissionKind(&kind);
+ if (kind == COREWEBVIEW2_PERMISSION_KIND_CLIPBOARD_READ)
+ {
+ args->put_State(COREWEBVIEW2_PERMISSION_STATE_ALLOW);
+ }
+ return S_OK;
+ }
+ HRESULT STDMETHODCALLTYPE Invoke(ICoreWebView2* sender,
+ ICoreWebView2NavigationCompletedEventArgs* args)
+ {
+ PWSTR uri = nullptr;
+ auto hr = sender->get_Source(&uri);
+ if (SUCCEEDED(hr))
+ {
+ auto curi = std::wstring_convert<std::codecvt_utf8<wchar_t> >().to_bytes(uri);
+ if (navigateCallback)
+ navigateCallback(curi, navigateCallbackArg);
+ }
+ CoTaskMemFree(uri);
+ return hr;
+ }
+
+ // Checks whether the specified IID equals the IID of the specified type and
+ // if so casts the "this" pointer to T and returns it. Returns nullptr on
+ // mismatching IIDs.
+ // If ppv is specified then the pointer will also be assigned to *ppv.
+ template <typename T>
+ T* cast_if_equal_iid(REFIID riid, const cast_info_t<T>& info,
+ LPVOID* ppv = nullptr) noexcept
+ {
+ T* ptr = nullptr;
+ if (IsEqualIID(riid, info.iid))
+ {
+ ptr = static_cast<T*>(this);
+ ptr->AddRef();
+ }
+ if (ppv)
+ {
+ *ppv = ptr;
+ }
+ return ptr;
+ }
+
+ // Set the function that will perform the initiating logic for creating
+ // the WebView2 environment.
+ void set_attempt_handler(std::function<HRESULT()> attempt_handler) noexcept
+ {
+ m_attempt_handler = attempt_handler;
+ }
+
+ // Retry creating a WebView2 environment.
+ // The initiating logic for creating the environment is defined by the
+ // caller of set_attempt_handler().
+ void try_create_environment() noexcept
+ {
+ // WebView creation fails with HRESULT_FROM_WIN32(ERROR_INVALID_STATE) if
+ // a running instance using the same user data folder exists, and the
+ // Environment objects have different EnvironmentOptions.
+ // Source:
+ // https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2environment?view=webview2-1.0.1150.38
+ if (m_attempts < m_max_attempts)
+ {
+ ++m_attempts;
+ auto res = m_attempt_handler();
+ if (SUCCEEDED(res))
+ {
+ return;
+ }
+ // Not entirely sure if this error code only applies to
+ // CreateCoreWebView2Controller so we check here as well.
+ if (res == HRESULT_FROM_WIN32(ERROR_INVALID_STATE))
+ {
+ return;
+ }
+ try_create_environment();
+ return;
+ }
+ // Give up.
+ m_cb(nullptr, nullptr);
+ }
+
+ void STDMETHODCALLTYPE add_navigate_listener(
+ std::function<void(const std::string&, void*)> callback, void* arg)
+ {
+ navigateCallback = std::move(callback);
+ navigateCallbackArg = arg;
+ }
+
+ private:
+ HWND m_window;
+ msg_cb_t m_msgCb;
+ webview2_com_handler_cb_t m_cb;
+ std::atomic<ULONG> m_ref_count{ 1 };
+ std::function<HRESULT()> m_attempt_handler;
+ unsigned int m_max_attempts = 5;
+ unsigned int m_attempts = 0;
+ void* navigateCallbackArg = nullptr;
+ std::function<void(const std::string&, void*)> navigateCallback = 0;
+ };
+
+ class win32_edge_engine
+ {
+ public:
+ win32_edge_engine(bool debug, void* window)
+ {
+ if (!is_webview2_available())
+ {
+ return;
+ }
+ if (!m_com_init.is_initialized())
+ {
+ return;
+ }
+ enable_dpi_awareness();
+ if (window == nullptr)
+ {
+ HINSTANCE hInstance = GetModuleHandle(nullptr);
+ HICON icon = (HICON)LoadImage(hInstance, IDI_APPLICATION, IMAGE_ICON,
+ GetSystemMetrics(SM_CXICON),
+ GetSystemMetrics(SM_CYICON), LR_DEFAULTCOLOR);
+
+ WNDCLASSEXW wc;
+ ZeroMemory(&wc, sizeof(WNDCLASSEX));
+ wc.cbSize = sizeof(WNDCLASSEX);
+ wc.hInstance = hInstance;
+ wc.lpszClassName = L"webview";
+ wc.hIcon = icon;
+ wc.lpfnWndProc =
+ (WNDPROC)(+[](HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) -> LRESULT {
+ auto w = (win32_edge_engine*)GetWindowLongPtr(hwnd, GWLP_USERDATA);
+ switch (msg)
+ {
+ case WM_SIZE:
+ w->resize(hwnd);
+ break;
+ case WM_CLOSE:
+ DestroyWindow(hwnd);
+ break;
+ case WM_DESTROY:
+ w->terminate();
+ break;
+ case WM_GETMINMAXINFO:
+ {
+ auto lpmmi = (LPMINMAXINFO)lp;
+ if (w == nullptr)
+ {
+ return 0;
+ }
+ if (w->m_maxsz.x > 0 && w->m_maxsz.y > 0)
+ {
+ lpmmi->ptMaxSize = w->m_maxsz;
+ lpmmi->ptMaxTrackSize = w->m_maxsz;
+ }
+ if (w->m_minsz.x > 0 && w->m_minsz.y > 0)
+ {
+ lpmmi->ptMinTrackSize = w->m_minsz;
+ }
+ }
+ break;
+ default:
+ return DefWindowProcW(hwnd, msg, wp, lp);
+ }
+ return 0;
+ });
+ RegisterClassExW(&wc);
+ m_window = CreateWindowW(L"webview", L"", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,
+ CW_USEDEFAULT, 640, 480, nullptr, nullptr, hInstance,
+ nullptr);
+ if (m_window == nullptr)
+ {
+ return;
+ }
+ SetWindowLongPtr(m_window, GWLP_USERDATA, (LONG_PTR)this);
+ }
+ else
+ {
+ m_window = *(static_cast<HWND*>(window));
+ }
+
+ ShowWindow(m_window, SW_SHOW);
+ UpdateWindow(m_window);
+ SetFocus(m_window);
+
+ auto cb = std::bind(&win32_edge_engine::on_message, this, std::placeholders::_1);
+
+ embed(m_window, debug, cb);
+ resize(m_window);
+ m_controller->MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC);
+ }
+
+ virtual ~win32_edge_engine()
+ {
+ if (m_com_handler)
+ {
+ m_com_handler->Release();
+ m_com_handler = nullptr;
+ }
+ if (m_webview)
+ {
+ m_webview->Release();
+ m_webview = nullptr;
+ }
+ if (m_controller)
+ {
+ m_controller->Release();
+ m_controller = nullptr;
+ }
+ }
+
+ win32_edge_engine(const win32_edge_engine& other) = delete;
+ win32_edge_engine& operator=(const win32_edge_engine& other) = delete;
+ win32_edge_engine(win32_edge_engine&& other) = delete;
+ win32_edge_engine& operator=(win32_edge_engine&& other) = delete;
+
+ void run()
+ {
+ MSG msg;
+ BOOL res;
+ while ((res = GetMessage(&msg, nullptr, 0, 0)) != -1)
+ {
+ if (msg.hwnd)
+ {
+ TranslateMessage(&msg);
+ DispatchMessage(&msg);
+ continue;
+ }
+ if (msg.message == WM_APP)
+ {
+ auto f = (dispatch_fn_t*)(msg.lParam);
+ (*f)();
+ delete f;
+ }
+ else if (msg.message == WM_QUIT)
+ {
+ return;
+ }
+ }
+ }
+ void* window()
+ {
+ return (void*)m_window;
+ }
+ void terminate()
+ {
+ PostQuitMessage(0);
+ }
+ void dispatch(dispatch_fn_t f)
+ {
+ PostThreadMessage(m_main_thread, WM_APP, 0, (LPARAM) new dispatch_fn_t(f));
+ }
+
+ void set_title(const std::string& title)
+ {
+ SetWindowTextW(m_window, widen_string(title).c_str());
+ }
+
+ void set_size(int width, int height, int hints)
+ {
+ auto style = GetWindowLong(m_window, GWL_STYLE);
+ if (hints == WEBVIEW_HINT_FIXED)
+ {
+ style &= ~(WS_THICKFRAME | WS_MAXIMIZEBOX);
+ }
+ else
+ {
+ style |= (WS_THICKFRAME | WS_MAXIMIZEBOX);
+ }
+ SetWindowLong(m_window, GWL_STYLE, style);
+
+ if (hints == WEBVIEW_HINT_MAX)
+ {
+ m_maxsz.x = width;
+ m_maxsz.y = height;
+ }
+ else if (hints == WEBVIEW_HINT_MIN)
+ {
+ m_minsz.x = width;
+ m_minsz.y = height;
+ }
+ else
+ {
+ RECT r;
+ r.left = r.top = 0;
+ r.right = width;
+ r.bottom = height;
+ AdjustWindowRect(&r, WS_OVERLAPPEDWINDOW, 0);
+ SetWindowPos(m_window, nullptr, r.left, r.top, r.right - r.left,
+ r.bottom - r.top,
+ SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOMOVE | SWP_FRAMECHANGED);
+ resize(m_window);
+ }
+ }
+
+ void navigate(const std::string& url)
+ {
+ auto wurl = widen_string(url);
+ m_webview->Navigate(wurl.c_str());
+ }
+
+ void init(const std::string& js)
+ {
+ auto wjs = widen_string(js);
+ m_webview->AddScriptToExecuteOnDocumentCreated(wjs.c_str(), nullptr);
+ }
+
+ void eval(const std::string& js)
+ {
+ auto wjs = widen_string(js);
+ m_webview->ExecuteScript(wjs.c_str(), nullptr);
+ }
+
+ void add_navigate_listener(std::function<void(const std::string&, void*)> callback,
+ void* arg)
+ {
+ m_com_handler->add_navigate_listener(callback, arg);
+ }
+
+ void set_html(const std::string& html)
+ {
+ m_webview->NavigateToString(widen_string(html).c_str());
+ }
+
+ private:
+ bool embed(HWND wnd, bool debug, msg_cb_t cb)
+ {
+ std::atomic_flag flag = ATOMIC_FLAG_INIT;
+ flag.test_and_set();
+
+ wchar_t currentExePath[MAX_PATH];
+ GetModuleFileNameW(nullptr, currentExePath, MAX_PATH);
+ wchar_t* currentExeName = PathFindFileNameW(currentExePath);
+
+ wchar_t dataPath[MAX_PATH];
+ if (!SUCCEEDED(SHGetFolderPathW(nullptr, CSIDL_APPDATA, nullptr, 0, dataPath)))
+ {
+ return false;
+ }
+ wchar_t userDataFolder[MAX_PATH];
+ PathCombineW(userDataFolder, dataPath, currentExeName);
+
+ m_com_handler = new webview2_com_handler(
+ wnd, cb, [&](ICoreWebView2Controller* controller, ICoreWebView2* webview) {
+ if (!controller || !webview)
+ {
+ flag.clear();
+ return;
+ }
+ controller->AddRef();
+ webview->AddRef();
+ m_controller = controller;
+ m_webview = webview;
+ flag.clear();
+ });
+
+ m_com_handler->set_attempt_handler([&] {
+ return m_webview2_loader.create_environment_with_options(
+ nullptr, userDataFolder, nullptr, m_com_handler);
+ });
+ m_com_handler->try_create_environment();
+
+ MSG msg = {};
+ while (flag.test_and_set() && GetMessage(&msg, nullptr, 0, 0))
+ {
+ TranslateMessage(&msg);
+ DispatchMessage(&msg);
+ }
+ if (!m_controller || !m_webview)
+ {
+ return false;
+ }
+ ICoreWebView2Settings* settings = nullptr;
+ auto res = m_webview->get_Settings(&settings);
+ if (res != S_OK)
+ {
+ return false;
+ }
+ res = settings->put_AreDevToolsEnabled(debug ? TRUE : FALSE);
+ if (res != S_OK)
+ {
+ return false;
+ }
+ init("window.external={invoke:s=>window.chrome.webview.postMessage(s)}");
+ return true;
+ }
+
+ void resize(HWND wnd)
+ {
+ if (m_controller == nullptr)
+ {
+ return;
+ }
+ RECT bounds;
+ GetClientRect(wnd, &bounds);
+ m_controller->put_Bounds(bounds);
+ }
+
+ bool is_webview2_available() const noexcept
+ {
+ LPWSTR version_info = nullptr;
+ auto res =
+ m_webview2_loader.get_available_browser_version_string(nullptr, &version_info);
+ // The result will be equal to HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)
+ // if the WebView2 runtime is not installed.
+ auto ok = SUCCEEDED(res) && version_info;
+ if (version_info)
+ {
+ CoTaskMemFree(version_info);
+ }
+ return ok;
+ }
+
+ virtual void on_message(const std::string& msg) = 0;
+
+ // The app is expected to call CoInitializeEx before
+ // CreateCoreWebView2EnvironmentWithOptions.
+ // Source:
+ // https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/webview2-idl#createcorewebview2environmentwithoptions
+ com_init_wrapper m_com_init{ COINIT_APARTMENTTHREADED };
+ HWND m_window = nullptr;
+ POINT m_minsz = POINT{ 0, 0 };
+ POINT m_maxsz = POINT{ 0, 0 };
+ DWORD m_main_thread = GetCurrentThreadId();
+ ICoreWebView2* m_webview = nullptr;
+ ICoreWebView2Controller* m_controller = nullptr;
+ webview2_com_handler* m_com_handler = nullptr;
+ mswebview2::loader m_webview2_loader;
+ };
+
+ } // namespace detail
+
+ using browser_engine = detail::win32_edge_engine;
+
+} // namespace webview
+
+#endif /* WEBVIEW_GTK, WEBVIEW_COCOA, WEBVIEW_EDGE */
+
+namespace webview
+{
+
+ class webview : public browser_engine
+ {
+ public:
+ webview(bool debug = false, void* wnd = nullptr) : browser_engine(debug, wnd)
+ {
+ }
+
+ void navigate(const std::string& url)
+ {
+ if (url.empty())
+ {
+ browser_engine::navigate("about:blank");
+ return;
+ }
+ browser_engine::navigate(url);
+ }
+
+ using binding_t = std::function<void(std::string, std::string, void*)>;
+ class binding_ctx_t
+ {
+ public:
+ binding_ctx_t(binding_t callback, void* arg) : callback(callback), arg(arg)
+ {
+ }
+ // This function is called upon execution of the bound JS function
+ binding_t callback;
+ // This user-supplied argument is passed to the callback
+ void* arg;
+ };
+
+ using sync_binding_t = std::function<std::string(std::string)>;
+
+ // Synchronous bind
+ void bind(const std::string& name, sync_binding_t fn)
+ {
+ auto wrapper = [this, fn](const std::string& seq, const std::string& req,
+ void* /*arg*/) { resolve(seq, 0, fn(req)); };
+ bind(name, wrapper, nullptr);
+ }
+
+ // Asynchronous bind
+ void bind(const std::string& name, binding_t fn, void* arg)
+ {
+ if (bindings.count(name) > 0)
+ {
+ return;
+ }
+ bindings.emplace(name, binding_ctx_t(fn, arg));
+ auto js = "(function() { var name = '" + name + "';" + R""(
+ var RPC = window._rpc = (window._rpc || {nextSeq: 1});
+ window[name] = function() {
+ var seq = RPC.nextSeq++;
+ var promise = new Promise(function(resolve, reject) {
+ RPC[seq] = {
+ resolve: resolve,
+ reject: reject,
+ };
+ });
+ window.external.invoke(JSON.stringify({
+ id: seq,
+ method: name,
+ params: Array.prototype.slice.call(arguments),
+ }));
+ return promise;
+ }
+ })())"";
+ init(js);
+ eval(js);
+ }
+
+ void unbind(const std::string& name)
+ {
+ auto found = bindings.find(name);
+ if (found != bindings.end())
+ {
+ auto js = "delete window['" + name + "'];";
+ init(js);
+ eval(js);
+ bindings.erase(found);
+ }
+ }
+
+ void resolve(const std::string& seq, int status, const std::string& result)
+ {
+ dispatch([seq, status, result, this]() {
+ if (status == 0)
+ {
+ eval("window._rpc[" + seq + "].resolve(" + result + "); delete window._rpc[" +
+ seq + "]");
+ }
+ else
+ {
+ eval("window._rpc[" + seq + "].reject(" + result + "); delete window._rpc[" +
+ seq + "]");
+ }
+ });
+ }
+
+ private:
+ void on_message(const std::string& msg) override
+ {
+ auto seq = detail::json_parse(msg, "id", 0);
+ auto name = detail::json_parse(msg, "method", 0);
+ auto args = detail::json_parse(msg, "params", 0);
+ auto found = bindings.find(name);
+ if (found == bindings.end())
+ {
+ return;
+ }
+ const auto& context = found->second;
+ context.callback(seq, args, context.arg);
+ }
+
+ std::map<std::string, binding_ctx_t> bindings;
+ };
+} // namespace webview
+
+WEBVIEW_API webview_t webview_create(int debug, void* wnd)
+{
+ auto w = new webview::webview(debug, wnd);
+ if (!w->window())
+ {
+ delete w;
+ return nullptr;
+ }
+ return w;
+}
+
+WEBVIEW_API void webview_destroy(webview_t w)
+{
+ delete static_cast<webview::webview*>(w);
+}
+
+WEBVIEW_API void webview_run(webview_t w)
+{
+ static_cast<webview::webview*>(w)->run();
+}
+
+WEBVIEW_API void webview_terminate(webview_t w)
+{
+ static_cast<webview::webview*>(w)->terminate();
+}
+
+WEBVIEW_API void webview_dispatch(webview_t w, void (*fn)(webview_t, void*), void* arg)
+{
+ static_cast<webview::webview*>(w)->dispatch([=]() { fn(w, arg); });
+}
+
+WEBVIEW_API void* webview_get_window(webview_t w)
+{
+ return static_cast<webview::webview*>(w)->window();
+}
+
+WEBVIEW_API void webview_set_title(webview_t w, const char* title)
+{
+ static_cast<webview::webview*>(w)->set_title(title);
+}
+
+WEBVIEW_API void webview_set_size(webview_t w, int width, int height, int hints)
+{
+ static_cast<webview::webview*>(w)->set_size(width, height, hints);
+}
+
+WEBVIEW_API void webview_navigate(webview_t w, const char* url)
+{
+ static_cast<webview::webview*>(w)->navigate(url);
+}
+
+WEBVIEW_API void webview_set_html(webview_t w, const char* html)
+{
+ static_cast<webview::webview*>(w)->set_html(html);
+}
+
+WEBVIEW_API void webview_init(webview_t w, const char* js)
+{
+ static_cast<webview::webview*>(w)->init(js);
+}
+
+WEBVIEW_API void webview_eval(webview_t w, const char* js)
+{
+ static_cast<webview::webview*>(w)->eval(js);
+}
+
+WEBVIEW_API void webview_bind(webview_t w, const char* name,
+ void (*fn)(const char* seq, const char* req, void* arg), void* arg)
+{
+ static_cast<webview::webview*>(w)->bind(
+ name,
+ [=](const std::string& seq, const std::string& req, void* arg) {
+ fn(seq.c_str(), req.c_str(), arg);
+ },
+ arg);
+}
+
+WEBVIEW_API void webview_unbind(webview_t w, const char* name)
+{
+ static_cast<webview::webview*>(w)->unbind(name);
+}
+
+WEBVIEW_API void webview_return(webview_t w, const char* seq, int status, const char* result)
+{
+ static_cast<webview::webview*>(w)->resolve(seq, status, result);
+}
+
+WEBVIEW_API const webview_version_info_t* webview_version()
+{
+ return &webview::detail::library_version_info;
+}
+
+#endif /* WEBVIEW_HEADER */
+#endif /* __cplusplus */
+#endif /* WEBVIEW_H */
diff --git a/client/SDL/aad/wrapper/webview_impl.cpp b/client/SDL/aad/wrapper/webview_impl.cpp
new file mode 100644
index 0000000..5f4d3d5
--- /dev/null
+++ b/client/SDL/aad/wrapper/webview_impl.cpp
@@ -0,0 +1,82 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * Popup browser for AAD authentication
+ *
+ * Copyright 2023 Isaac Klein <fifthdegree@protonmail.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "webview.h"
+
+#include <assert.h>
+#include <string>
+#include <vector>
+#include <map>
+#include <regex>
+#include <sstream>
+#include "../webview_impl.hpp"
+
+static std::vector<std::string> split(const std::string& input, const std::string& regex)
+{
+ // passing -1 as the submatch index parameter performs splitting
+ std::regex re(regex);
+ std::sregex_token_iterator first{ input.begin(), input.end(), re, -1 };
+ std::sregex_token_iterator last;
+ return { first, last };
+}
+
+static std::map<std::string, std::string> urlsplit(const std::string& url)
+{
+ auto pos = url.find("?");
+ if (pos == std::string::npos)
+ return {};
+ auto surl = url.substr(pos);
+ auto args = split(surl, "&");
+
+ std::map<std::string, std::string> argmap;
+ for (const auto& arg : args)
+ {
+ auto kv = split(arg, "=");
+ if (kv.size() == 2)
+ argmap.insert({ kv[0], kv[1] });
+ }
+ return argmap;
+}
+
+static void fkt(const std::string& url, void* arg)
+{
+ auto args = urlsplit(url);
+ auto val = args.find("code");
+ if (val == args.end())
+ return;
+
+ assert(arg);
+ auto rcode = static_cast<std::string*>(arg);
+ *rcode = val->second;
+}
+
+bool webview_impl_run(const std::string& title, const std::string& url, std::string& code)
+{
+ webview::webview w(false, nullptr);
+
+ w.set_title(title);
+ w.set_size(640, 480, WEBVIEW_HINT_NONE);
+
+ std::string scheme;
+ w.add_scheme_handler("ms-appx-web", fkt, &scheme);
+ w.add_navigate_listener(fkt, &code);
+ w.navigate(url);
+ w.run();
+ return !code.empty();
+}
diff --git a/client/SDL/dialogs/CMakeLists.txt b/client/SDL/dialogs/CMakeLists.txt
new file mode 100644
index 0000000..4cf2a16
--- /dev/null
+++ b/client/SDL/dialogs/CMakeLists.txt
@@ -0,0 +1,75 @@
+set(SRCS
+ sdl_button.hpp
+ sdl_button.cpp
+ sdl_buttons.hpp
+ sdl_buttons.cpp
+ sdl_dialogs.cpp
+ sdl_dialogs.hpp
+ sdl_widget.hpp
+ sdl_widget.cpp
+ sdl_input.hpp
+ sdl_input.cpp
+ sdl_input_widgets.cpp
+ sdl_input_widgets.hpp
+ sdl_select.hpp
+ sdl_select.cpp
+ sdl_selectlist.hpp
+ sdl_selectlist.cpp
+ sdl_connection_dialog.cpp
+ sdl_connection_dialog.hpp
+)
+
+list(APPEND LIBS
+ sdl_client_res
+ winpr
+)
+
+if (NOT WITH_SDL_LINK_SHARED)
+ list(APPEND LIBS ${SDL2_STATIC_LIBRARIES})
+else()
+ list(APPEND LIBS ${SDL2_LIBRARIES})
+endif()
+
+macro(find_sdl_component name)
+ find_package(${name})
+ if (NOT ${name}_FOUND)
+ find_package(PkgConfig REQUIRED)
+ pkg_check_modules(${name} REQUIRED ${name})
+
+ if (BUILD_SHARED_LIBS)
+ list(APPEND LIBS ${${name}_LIBRARIES})
+ link_directories(${${name}_LIBRARY_DIRS})
+ include_directories(${${name}_INCLUDE_DIRS})
+ else()
+ list(APPEND LIBS ${${name}_STATIC_LIBRARIES})
+ link_directories(${${name}_STATIC_LIBRARY_DIRS})
+ include_directories(${${name}_STATIC_INCLUDE_DIRS})
+ endif()
+ else()
+ if (WITH_SDL_LINK_SHARED)
+ list(APPEND LIBS ${name}::${name})
+ else()
+ list(APPEND LIBS ${name}::${name}-static)
+ endif()
+ endif()
+endmacro()
+
+find_sdl_component(SDL2_ttf)
+
+option(WITH_SDL_IMAGE_DIALOGS "Build with SDL_image support (recommended)" OFF)
+if (WITH_SDL_IMAGE_DIALOGS)
+ find_sdl_component(SDL2_image)
+ add_definitions(-DWITH_SDL_IMAGE_DIALOGS)
+endif()
+
+add_subdirectory(res)
+
+add_library(dialogs STATIC
+ ${SRCS}
+)
+
+target_link_libraries(dialogs PRIVATE ${LIBS})
+
+if(BUILD_TESTING)
+# add_subdirectory(test)
+endif()
diff --git a/client/SDL/dialogs/font/OFL.txt b/client/SDL/dialogs/font/OFL.txt
new file mode 100644
index 0000000..9b448d4
--- /dev/null
+++ b/client/SDL/dialogs/font/OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/client/SDL/dialogs/font/OpenSans-Italic-VariableFont_wdth,wght.ttf b/client/SDL/dialogs/font/OpenSans-Italic-VariableFont_wdth,wght.ttf
new file mode 100644
index 0000000..5bda9cc
--- /dev/null
+++ b/client/SDL/dialogs/font/OpenSans-Italic-VariableFont_wdth,wght.ttf
Binary files differ
diff --git a/client/SDL/dialogs/font/OpenSans-VariableFont_wdth,wght.ttf b/client/SDL/dialogs/font/OpenSans-VariableFont_wdth,wght.ttf
new file mode 100644
index 0000000..e4142bf
--- /dev/null
+++ b/client/SDL/dialogs/font/OpenSans-VariableFont_wdth,wght.ttf
Binary files differ
diff --git a/client/SDL/dialogs/font/README.txt b/client/SDL/dialogs/font/README.txt
new file mode 100644
index 0000000..2548322
--- /dev/null
+++ b/client/SDL/dialogs/font/README.txt
@@ -0,0 +1,100 @@
+Open Sans Variable Font
+=======================
+
+This download contains Open Sans as both variable fonts and static fonts.
+
+Open Sans is a variable font with these axes:
+ wdth
+ wght
+
+This means all the styles are contained in these files:
+ OpenSans-VariableFont_wdth,wght.ttf
+ OpenSans-Italic-VariableFont_wdth,wght.ttf
+
+If your app fully supports variable fonts, you can now pick intermediate styles
+that aren’t available as static fonts. Not all apps support variable fonts, and
+in those cases you can use the static font files for Open Sans:
+ static/OpenSans_Condensed-Light.ttf
+ static/OpenSans_Condensed-Regular.ttf
+ static/OpenSans_Condensed-Medium.ttf
+ static/OpenSans_Condensed-SemiBold.ttf
+ static/OpenSans_Condensed-Bold.ttf
+ static/OpenSans_Condensed-ExtraBold.ttf
+ static/OpenSans_SemiCondensed-Light.ttf
+ static/OpenSans_SemiCondensed-Regular.ttf
+ static/OpenSans_SemiCondensed-Medium.ttf
+ static/OpenSans_SemiCondensed-SemiBold.ttf
+ static/OpenSans_SemiCondensed-Bold.ttf
+ static/OpenSans_SemiCondensed-ExtraBold.ttf
+ static/OpenSans-Light.ttf
+ static/OpenSans-Regular.ttf
+ static/OpenSans-Medium.ttf
+ static/OpenSans-SemiBold.ttf
+ static/OpenSans-Bold.ttf
+ static/OpenSans-ExtraBold.ttf
+ static/OpenSans_Condensed-LightItalic.ttf
+ static/OpenSans_Condensed-Italic.ttf
+ static/OpenSans_Condensed-MediumItalic.ttf
+ static/OpenSans_Condensed-SemiBoldItalic.ttf
+ static/OpenSans_Condensed-BoldItalic.ttf
+ static/OpenSans_Condensed-ExtraBoldItalic.ttf
+ static/OpenSans_SemiCondensed-LightItalic.ttf
+ static/OpenSans_SemiCondensed-Italic.ttf
+ static/OpenSans_SemiCondensed-MediumItalic.ttf
+ static/OpenSans_SemiCondensed-SemiBoldItalic.ttf
+ static/OpenSans_SemiCondensed-BoldItalic.ttf
+ static/OpenSans_SemiCondensed-ExtraBoldItalic.ttf
+ static/OpenSans-LightItalic.ttf
+ static/OpenSans-Italic.ttf
+ static/OpenSans-MediumItalic.ttf
+ static/OpenSans-SemiBoldItalic.ttf
+ static/OpenSans-BoldItalic.ttf
+ static/OpenSans-ExtraBoldItalic.ttf
+
+Get started
+-----------
+
+1. Install the font files you want to use
+
+2. Use your app's font picker to view the font family and all the
+available styles
+
+Learn more about variable fonts
+-------------------------------
+
+ https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts
+ https://variablefonts.typenetwork.com
+ https://medium.com/variable-fonts
+
+In desktop apps
+
+ https://theblog.adobe.com/can-variable-fonts-illustrator-cc
+ https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts
+
+Online
+
+ https://developers.google.com/fonts/docs/getting_started
+ https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
+ https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts
+
+Installing fonts
+
+ MacOS: https://support.apple.com/en-us/HT201749
+ Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux
+ Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows
+
+Android Apps
+
+ https://developers.google.com/fonts/docs/android
+ https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts
+
+License
+-------
+Please read the full license text (OFL.txt) to understand the permissions,
+restrictions and requirements for usage, redistribution, and modification.
+
+You can use them in your products & projects – print or digital,
+commercial or otherwise.
+
+This isn't legal advice, please consider consulting a lawyer and see the full
+license for all details.
diff --git a/client/SDL/dialogs/res/CMakeLists.txt b/client/SDL/dialogs/res/CMakeLists.txt
new file mode 100644
index 0000000..5591e4a
--- /dev/null
+++ b/client/SDL/dialogs/res/CMakeLists.txt
@@ -0,0 +1,89 @@
+
+add_executable(freerdp-res2bin
+ convert_res_to_c.cpp
+)
+
+set(SRCS
+ sdl_resource_manager.cpp
+ sdl_resource_manager.hpp
+)
+
+set(RES_SVG_FILES
+ ${CMAKE_SOURCE_DIR}/resources/FreeRDP_Icon.svg
+ ${CMAKE_SOURCE_DIR}/resources/icon_info.svg
+ ${CMAKE_SOURCE_DIR}/resources/icon_warning.svg
+ ${CMAKE_SOURCE_DIR}/resources/icon_error.svg
+)
+
+set(RES_FONT_FILES
+ ${CMAKE_SOURCE_DIR}/client/SDL/dialogs/font/OpenSans-VariableFont_wdth,wght.ttf
+)
+
+macro(convert_to_bin FILE FILE_TYPE)
+ get_filename_component(FILE_NAME ${FILE} NAME)
+ string(REGEX REPLACE "[^a-zA-Z0-9]" "_" TARGET_NAME ${FILE_NAME})
+
+ set(FILE_BIN_DIR ${CMAKE_CURRENT_BINARY_DIR}/bin)
+ set(FILE_BYPRODUCTS ${FILE_BIN_DIR}/${TARGET_NAME}.hpp ${FILE_BIN_DIR}/${TARGET_NAME}.cpp)
+
+ list(APPEND FACTORY_SRCS
+ ${FILE_BYPRODUCTS}
+ )
+
+ add_custom_command(
+ OUTPUT ${FILE_BYPRODUCTS}
+ COMMAND ${CMAKE_COMMAND} -E make_directory ${FILE_BIN_DIR}
+ COMMAND $<TARGET_FILE:freerdp-res2bin> ${FILE} ${FILE_TYPE} ${TARGET_NAME} ${FILE_BIN_DIR}
+ COMMENT "create image resources"
+ DEPENDS freerdp-res2bin
+ DEPENDS ${FILE}
+ )
+endmacro()
+
+option(SDL_USE_COMPILED_RESOURCES "Compile in images/fonts" ON)
+
+if (SDL_USE_COMPILED_RESOURCES)
+ list(APPEND SRCS
+ sdl_resource_file.cpp
+ sdl_resource_file.hpp
+ )
+
+ include_directories(${CMAKE_CURRENT_SOURCE_DIR})
+
+ if (WITH_SDL_IMAGE_DIALOGS)
+ foreach(FILE ${RES_SVG_FILES})
+ convert_to_bin("${FILE}" "images")
+ endforeach()
+ endif()
+
+ foreach(FILE ${RES_FONT_FILES})
+ convert_to_bin("${FILE}" "fonts")
+ endforeach()
+ add_definitions(-DSDL_USE_COMPILED_RESOURCES)
+else()
+ set(SDL_RESOURCE_ROOT ${CMAKE_INSTALL_FULL_DATAROOTDIR}/FreeRDP)
+ if (WITH_BINARY_VERSIONING)
+ string(APPEND SDL_RESOURCE_ROOT "${PROJECT_VERSION_MAJOR}")
+ endif()
+
+ add_definitions(-DSDL_RESOURCE_ROOT="${SDL_RESOURCE_ROOT}")
+
+ if (WITH_SDL_IMAGE_DIALOGS)
+ install(
+ FILES ${RES_SVG_FILES}
+ DESTINATION ${SDL_RESOURCE_ROOT}/images
+ )
+ endif()
+
+ install(
+ FILES ${RES_FONT_FILES}
+ DESTINATION ${SDL_RESOURCE_ROOT}/fonts
+ )
+endif()
+
+add_library(sdl_client_res OBJECT
+ ${RES_FILES}
+ ${SRCS}
+ ${FACTORY_SRCS}
+)
+set_property(TARGET sdl_client_res PROPERTY POSITION_INDEPENDENT_CODE ON)
diff --git a/client/SDL/dialogs/res/convert_res_to_c.cpp b/client/SDL/dialogs/res/convert_res_to_c.cpp
new file mode 100644
index 0000000..07309d5
--- /dev/null
+++ b/client/SDL/dialogs/res/convert_res_to_c.cpp
@@ -0,0 +1,184 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ *
+ * Copyright 2023 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <string>
+#include <fstream>
+#include <algorithm>
+#include <iomanip>
+#include <iostream>
+#if __has_include(<filesystem>)
+#include <filesystem>
+namespace fs = std::filesystem;
+#elif __has_include(<experimental/filesystem>)
+#include <experimental/filesystem>
+namespace fs = std::experimental::filesystem;
+#else
+#error Could not find system header "<filesystem>" or "<experimental/filesystem>"
+#endif
+
+static void usage(const char* prg)
+{
+ std::cerr << prg << " <file> <type> <class name> <dst path>" << std::endl;
+}
+
+static int write_comment_header(std::ostream& out, const fs::path& prg, const std::string& fname)
+{
+ out << "/* AUTOGENERATED file, do not edit" << std::endl
+ << " *" << std::endl
+ << " * generated by '" << prg.filename() << "'" << std::endl
+ << " *" << std::endl
+ << " * contains the converted file '" << fname << "'" << std::endl
+ << " */" << std::endl
+ << std::endl;
+ return 0;
+}
+
+static int write_cpp_header(std::ostream& out, const fs::path& prg, const fs::path& file,
+ const std::string& name, const std::string& type)
+{
+ auto fname = file.filename().string();
+ auto rc = write_comment_header(out, prg, fname);
+ if (rc != 0)
+ return rc;
+
+ out << "#include <vector>" << std::endl
+ << "#include \"" << name << ".hpp\"" << std::endl
+ << std::endl
+ << "std::string " << name << "::name() {" << std::endl
+ << "return \"" << fname << "\";" << std::endl
+ << "}" << std::endl
+ << "std::string " << name << "::type() {" << std::endl
+ << "return \"" << type << "\";" << std::endl
+ << "}" << std::endl
+ << std::endl
+ << "const SDLResourceFile " << name << "::_initializer(type(), name(), init());"
+ << std::endl
+ << std::endl
+ << "std::vector<unsigned char> " << name << "::init() {" << std::endl
+ << "static const unsigned char data[] = {" << std::endl;
+
+ return 0;
+}
+
+static int readwrite(std::ofstream& out, std::ifstream& ifs)
+{
+ size_t pos = 0;
+ char c = 0;
+ while (ifs.read(&c, 1) && ifs.good())
+ {
+ unsigned val = c & 0xff;
+ out << "0x" << std::hex << std::setfill('0') << std::setw(2) << val;
+ if (ifs.peek() != EOF)
+ out << ",";
+ if ((pos++ % 16) == 15)
+ out << std::endl;
+ }
+
+ return 0;
+}
+
+static int write_cpp_trailer(std::ostream& out)
+{
+ out << std::endl;
+ out << "};" << std::endl;
+ out << std::endl;
+ out << "return std::vector<unsigned char>(data, data + sizeof(data));" << std::endl;
+ out << "}" << std::endl;
+ return 0;
+}
+
+static int write_hpp_header(const fs::path& prg, const fs::path& file, const std::string& name,
+ const std::string& fname)
+{
+ std::ofstream out(file, std::ios::out);
+ if (!out.is_open())
+ {
+ std::cerr << "Failed to open output file '" << file << "'" << std::endl;
+ return -1;
+ }
+ auto rc = write_comment_header(out, prg, fname);
+ if (rc != 0)
+ return rc;
+
+ out << "#pragma once" << std::endl
+ << std::endl
+ << "#include <vector>" << std::endl
+ << "#include <string>" << std::endl
+ << "#include \"sdl_resource_file.hpp\"" << std::endl
+ << std::endl
+ << "class " << name << " {" << std::endl
+ << "public:" << std::endl
+ << name << "() = delete;" << std::endl
+ << std::endl
+ << "static std::string name();" << std::endl
+ << "static std::string type();" << std::endl
+ << std::endl
+ << "private:" << std::endl
+ << "static std::vector<unsigned char> init();" << std::endl
+ << "static const SDLResourceFile _initializer;" << std::endl
+ << std::endl
+ << "};" << std::endl;
+ return 0;
+}
+
+int main(int argc, char* argv[])
+{
+ fs::path prg(argv[0]);
+ if (argc != 5)
+ {
+ usage(argv[0]);
+ return -1;
+ }
+
+ fs::path file(argv[1]);
+ std::string etype = argv[2];
+ std::string cname = argv[3];
+ fs::path dst(argv[4]);
+ fs::path hdr(argv[4]);
+
+ dst /= cname + ".cpp";
+ hdr /= cname + ".hpp";
+
+ std::ofstream out;
+ out.open(dst);
+ if (!out.is_open())
+ {
+ std::cerr << "Failed to open output file '" << dst << "'" << std::endl;
+ return -2;
+ }
+
+ std::ifstream ifs(file, std::ios::in | std::ios::binary);
+ if (!ifs.is_open())
+ {
+ std::cerr << "Failed to open input file '" << file << "'" << std::endl;
+ return -3;
+ }
+
+ auto rc = write_cpp_header(out, prg, file, cname, etype);
+ if (rc != 0)
+ return -1;
+
+ rc = readwrite(out, ifs);
+ if (rc != 0)
+ return rc;
+
+ rc = write_cpp_trailer(out);
+ if (rc != 0)
+ return rc;
+ return write_hpp_header(prg, hdr, cname, file.filename().string());
+}
diff --git a/client/SDL/dialogs/res/sdl_resource_file.cpp b/client/SDL/dialogs/res/sdl_resource_file.cpp
new file mode 100644
index 0000000..c48612d
--- /dev/null
+++ b/client/SDL/dialogs/res/sdl_resource_file.cpp
@@ -0,0 +1,25 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ *
+ * Copyright 2023 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "sdl_resource_file.hpp"
+#include "sdl_resource_manager.hpp"
+
+SDLResourceFile::SDLResourceFile(const std::string& type, const std::string& id,
+ const std::vector<unsigned char>& data)
+{
+ SDLResourceManager::insert(type, id, data);
+}
diff --git a/client/SDL/dialogs/res/sdl_resource_file.hpp b/client/SDL/dialogs/res/sdl_resource_file.hpp
new file mode 100644
index 0000000..5846921
--- /dev/null
+++ b/client/SDL/dialogs/res/sdl_resource_file.hpp
@@ -0,0 +1,33 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ *
+ * Copyright 2023 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#pragma once
+
+#include <string>
+#include <vector>
+
+class SDLResourceFile
+{
+ public:
+ SDLResourceFile(const std::string& type, const std::string& id,
+ const std::vector<unsigned char>& data);
+ virtual ~SDLResourceFile() = default;
+
+ private:
+ SDLResourceFile(const SDLResourceFile& other) = delete;
+ SDLResourceFile(const SDLResourceFile&& other) = delete;
+};
diff --git a/client/SDL/dialogs/res/sdl_resource_manager.cpp b/client/SDL/dialogs/res/sdl_resource_manager.cpp
new file mode 100644
index 0000000..90ccf31
--- /dev/null
+++ b/client/SDL/dialogs/res/sdl_resource_manager.cpp
@@ -0,0 +1,78 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ *
+ * Copyright 2023 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "sdl_resource_manager.hpp"
+#include <iostream>
+#if __has_include(<filesystem>)
+#include <filesystem>
+namespace fs = std::filesystem;
+#elif __has_include(<experimental/filesystem>)
+#include <experimental/filesystem>
+namespace fs = std::experimental::filesystem;
+#else
+#error Could not find system header "<filesystem>" or "<experimental/filesystem>"
+#endif
+
+SDL_RWops* SDLResourceManager::get(const std::string& type, const std::string& id)
+{
+ std::string uuid = type + "/" + id;
+
+#if defined(SDL_USE_COMPILED_RESOURCES)
+ auto val = resources().find(uuid);
+ if (val == resources().end())
+ return nullptr;
+
+ const auto& v = val->second;
+ return SDL_RWFromConstMem(v.data(), v.size());
+#else
+ fs::path path(SDL_RESOURCE_ROOT);
+ path /= type;
+ path /= id;
+
+ if (!fs::exists(path))
+ {
+ std::cerr << "sdl-freerdp expects resource '" << uuid << "' at location "
+ << fs::absolute(path) << std::endl;
+ std::cerr << "file not found, application will fail" << std::endl;
+ }
+ return SDL_RWFromFile(path.native().c_str(), "rb");
+#endif
+}
+
+const std::string SDLResourceManager::typeFonts()
+{
+ return "fonts";
+}
+
+const std::string SDLResourceManager::typeImages()
+{
+ return "images";
+}
+
+void SDLResourceManager::insert(const std::string& type, const std::string& id,
+ const std::vector<unsigned char>& data)
+{
+ std::string uuid = type + "/" + id;
+ resources().emplace(uuid, data);
+}
+
+std::map<std::string, std::vector<unsigned char>>& SDLResourceManager::resources()
+{
+
+ static std::map<std::string, std::vector<unsigned char>> resources = {};
+ return resources;
+}
diff --git a/client/SDL/dialogs/res/sdl_resource_manager.hpp b/client/SDL/dialogs/res/sdl_resource_manager.hpp
new file mode 100644
index 0000000..b4f463c
--- /dev/null
+++ b/client/SDL/dialogs/res/sdl_resource_manager.hpp
@@ -0,0 +1,46 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ *
+ * Copyright 2023 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#pragma once
+
+#include <string>
+#include <map>
+#include <vector>
+#include <SDL.h>
+
+class SDLResourceManager
+{
+ friend class SDLResourceFile;
+
+ public:
+ static SDL_RWops* get(const std::string& type, const std::string& id);
+
+ static const std::string typeFonts();
+ static const std::string typeImages();
+
+ protected:
+ static void insert(const std::string& type, const std::string& id,
+ const std::vector<unsigned char>& data);
+
+ private:
+ SDLResourceManager() = delete;
+ SDLResourceManager(const SDLResourceManager& other) = delete;
+ SDLResourceManager(const SDLResourceManager&& other) = delete;
+ ~SDLResourceManager() = delete;
+
+ static std::map<std::string, std::vector<unsigned char>>& resources();
+};
diff --git a/client/SDL/dialogs/sdl_button.cpp b/client/SDL/dialogs/sdl_button.cpp
new file mode 100644
index 0000000..cfa2107
--- /dev/null
+++ b/client/SDL/dialogs/sdl_button.cpp
@@ -0,0 +1,71 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * SDL Client Channels
+ *
+ * Copyright 2023 Armin Novak <armin.novak@thincast.com>
+ * Copyright 2023 Thincast Technologies GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <cassert>
+
+#include "sdl_button.hpp"
+
+static const SDL_Color buttonbackgroundcolor = { 0x69, 0x66, 0x63, 0xff };
+static const SDL_Color buttonhighlightcolor = { 0xcd, 0xca, 0x35, 0x60 };
+static const SDL_Color buttonmouseovercolor = { 0x66, 0xff, 0x66, 0x60 };
+static const SDL_Color buttonfontcolor = { 0xd1, 0xcf, 0xcd, 0xff };
+
+SdlButton::SdlButton(SDL_Renderer* renderer, const std::string& label, int id, const SDL_Rect& rect)
+ : SdlWidget(renderer, rect, false), _name(label), _id(id)
+{
+ assert(renderer);
+
+ update_text(renderer, _name, buttonfontcolor, buttonbackgroundcolor);
+}
+
+SdlButton::SdlButton(SdlButton&& other) noexcept
+ : SdlWidget(std::move(other)), _name(std::move(other._name)), _id(std::move(other._id))
+{
+}
+
+bool SdlButton::highlight(SDL_Renderer* renderer)
+{
+ assert(renderer);
+
+ std::vector<SDL_Color> colors = { buttonbackgroundcolor, buttonhighlightcolor };
+ if (!fill(renderer, colors))
+ return false;
+ return update_text(renderer, _name, buttonfontcolor);
+}
+
+bool SdlButton::mouseover(SDL_Renderer* renderer)
+{
+ std::vector<SDL_Color> colors = { buttonbackgroundcolor, buttonmouseovercolor };
+ if (!fill(renderer, colors))
+ return false;
+ return update_text(renderer, _name, buttonfontcolor);
+}
+
+bool SdlButton::update(SDL_Renderer* renderer)
+{
+ assert(renderer);
+
+ return update_text(renderer, _name, buttonfontcolor, buttonbackgroundcolor);
+}
+
+int SdlButton::id() const
+{
+ return _id;
+}
diff --git a/client/SDL/dialogs/sdl_button.hpp b/client/SDL/dialogs/sdl_button.hpp
new file mode 100644
index 0000000..350e7db
--- /dev/null
+++ b/client/SDL/dialogs/sdl_button.hpp
@@ -0,0 +1,26 @@
+#pragma once
+
+#include <string>
+
+#include "sdl_widget.hpp"
+
+class SdlButton : public SdlWidget
+{
+ public:
+ SdlButton(SDL_Renderer* renderer, const std::string& label, int id, const SDL_Rect& rect);
+ SdlButton(SdlButton&& other) noexcept;
+ ~SdlButton() override = default;
+
+ bool highlight(SDL_Renderer* renderer);
+ bool mouseover(SDL_Renderer* renderer);
+ bool update(SDL_Renderer* renderer);
+
+ [[nodiscard]] int id() const;
+
+ private:
+ SdlButton(const SdlButton& other) = delete;
+
+ private:
+ std::string _name;
+ int _id;
+};
diff --git a/client/SDL/dialogs/sdl_buttons.cpp b/client/SDL/dialogs/sdl_buttons.cpp
new file mode 100644
index 0000000..8190cbe
--- /dev/null
+++ b/client/SDL/dialogs/sdl_buttons.cpp
@@ -0,0 +1,105 @@
+#include <cassert>
+#include <algorithm>
+
+#include "sdl_buttons.hpp"
+
+static const Uint32 hpadding = 10;
+
+bool SdlButtonList::populate(SDL_Renderer* renderer, const std::vector<std::string>& labels,
+ const std::vector<int>& ids, Sint32 total_width, Sint32 offsetY,
+ Sint32 width, Sint32 height)
+{
+ assert(renderer);
+ assert(width >= 0);
+ assert(height >= 0);
+ assert(labels.size() == ids.size());
+
+ _list.clear();
+ size_t button_width = ids.size() * (width + hpadding) + hpadding;
+ size_t offsetX = total_width - std::min<size_t>(total_width, button_width);
+ for (size_t x = 0; x < ids.size(); x++)
+ {
+ const size_t curOffsetX = offsetX + x * (static_cast<size_t>(width) + hpadding);
+ const SDL_Rect rect = { static_cast<int>(curOffsetX), offsetY, width, height };
+ _list.emplace_back(renderer, labels[x], ids[x], rect);
+ }
+ return true;
+}
+
+SdlButton* SdlButtonList::get_selected(const SDL_MouseButtonEvent& button)
+{
+ const Sint32 x = button.x;
+ const Sint32 y = button.y;
+
+ return get_selected(x, y);
+}
+
+SdlButton* SdlButtonList::get_selected(Sint32 x, Sint32 y)
+{
+ for (auto& btn : _list)
+ {
+ auto r = btn.rect();
+ if ((x >= r.x) && (x <= r.x + r.w) && (y >= r.y) && (y <= r.y + r.h))
+ return &btn;
+ }
+ return nullptr;
+}
+
+bool SdlButtonList::set_highlight_next(bool reset)
+{
+ if (reset)
+ _highlighted = nullptr;
+ else
+ {
+ auto next = _highlight_index++;
+ _highlight_index %= _list.size();
+ auto& element = _list[next];
+ _highlighted = &element;
+ }
+ return true;
+}
+
+bool SdlButtonList::set_highlight(size_t index)
+{
+ if (index >= _list.size())
+ {
+ _highlighted = nullptr;
+ return false;
+ }
+ auto& element = _list[index];
+ _highlighted = &element;
+ _highlight_index = ++index % _list.size();
+ return true;
+}
+
+bool SdlButtonList::set_mouseover(Sint32 x, Sint32 y)
+{
+ _mouseover = get_selected(x, y);
+ return _mouseover != nullptr;
+}
+
+void SdlButtonList::clear()
+{
+ _list.clear();
+ _mouseover = nullptr;
+ _highlighted = nullptr;
+ _highlight_index = 0;
+}
+
+bool SdlButtonList::update(SDL_Renderer* renderer)
+{
+ assert(renderer);
+
+ for (auto& btn : _list)
+ {
+ if (!btn.update(renderer))
+ return false;
+ }
+
+ if (_highlighted)
+ _highlighted->highlight(renderer);
+
+ if (_mouseover)
+ _mouseover->mouseover(renderer);
+ return true;
+}
diff --git a/client/SDL/dialogs/sdl_buttons.hpp b/client/SDL/dialogs/sdl_buttons.hpp
new file mode 100644
index 0000000..7f82903
--- /dev/null
+++ b/client/SDL/dialogs/sdl_buttons.hpp
@@ -0,0 +1,37 @@
+#pragma once
+
+#include <vector>
+#include <cstdint>
+
+#include "sdl_button.hpp"
+
+class SdlButtonList
+{
+ public:
+ SdlButtonList() = default;
+ virtual ~SdlButtonList() = default;
+
+ bool populate(SDL_Renderer* renderer, const std::vector<std::string>& labels,
+ const std::vector<int>& ids, Sint32 total_width, Sint32 offsetY, Sint32 width,
+ Sint32 heigth);
+
+ bool update(SDL_Renderer* renderer);
+ SdlButton* get_selected(const SDL_MouseButtonEvent& button);
+ SdlButton* get_selected(Sint32 x, Sint32 y);
+
+ bool set_highlight_next(bool reset = false);
+ bool set_highlight(size_t index);
+ bool set_mouseover(Sint32 x, Sint32 y);
+
+ void clear();
+
+ private:
+ SdlButtonList(const SdlButtonList& other) = delete;
+ SdlButtonList(SdlButtonList&& other) = delete;
+
+ private:
+ std::vector<SdlButton> _list;
+ SdlButton* _highlighted = nullptr;
+ size_t _highlight_index = 0;
+ SdlButton* _mouseover = nullptr;
+};
diff --git a/client/SDL/dialogs/sdl_connection_dialog.cpp b/client/SDL/dialogs/sdl_connection_dialog.cpp
new file mode 100644
index 0000000..cbb6349
--- /dev/null
+++ b/client/SDL/dialogs/sdl_connection_dialog.cpp
@@ -0,0 +1,536 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * SDL Client helper dialogs
+ *
+ * Copyright 2023 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include <cassert>
+#include <thread>
+
+#include "sdl_connection_dialog.hpp"
+#include "../sdl_utils.hpp"
+#include "../sdl_freerdp.hpp"
+#include "res/sdl_resource_manager.hpp"
+
+static const SDL_Color backgroundcolor = { 0x38, 0x36, 0x35, 0xff };
+static const SDL_Color textcolor = { 0xd1, 0xcf, 0xcd, 0xff };
+static const SDL_Color infocolor = { 0x43, 0xe0, 0x0f, 0x60 };
+static const SDL_Color warncolor = { 0xcd, 0xca, 0x35, 0x60 };
+static const SDL_Color errorcolor = { 0xf7, 0x22, 0x30, 0x60 };
+
+static const Uint32 vpadding = 5;
+static const Uint32 hpadding = 5;
+
+SDLConnectionDialog::SDLConnectionDialog(rdpContext* context)
+ : _context(context), _window(nullptr), _renderer(nullptr)
+{
+ SDL_Init(SDL_INIT_TIMER | SDL_INIT_VIDEO);
+ hide();
+}
+
+SDLConnectionDialog::~SDLConnectionDialog()
+{
+ resetTimer();
+ destroyWindow();
+ SDL_Quit();
+}
+
+bool SDLConnectionDialog::visible() const
+{
+ return _window && _renderer;
+}
+
+bool SDLConnectionDialog::setTitle(const char* fmt, ...)
+{
+ std::lock_guard lock(_mux);
+ va_list ap;
+ va_start(ap, fmt);
+ _title = print(fmt, ap);
+ va_end(ap);
+
+ return show(MSG_NONE);
+}
+
+bool SDLConnectionDialog::showInfo(const char* fmt, ...)
+{
+ va_list ap;
+ va_start(ap, fmt);
+ auto rc = show(MSG_INFO, fmt, ap);
+ va_end(ap);
+ return rc;
+}
+
+bool SDLConnectionDialog::showWarn(const char* fmt, ...)
+{
+ va_list ap;
+ va_start(ap, fmt);
+ auto rc = show(MSG_WARN, fmt, ap);
+ va_end(ap);
+ return rc;
+}
+
+bool SDLConnectionDialog::showError(const char* fmt, ...)
+{
+ va_list ap;
+ va_start(ap, fmt);
+ auto rc = show(MSG_ERROR, fmt, ap);
+ va_end(ap);
+ return setTimer();
+}
+
+bool SDLConnectionDialog::show()
+{
+ std::lock_guard lock(_mux);
+ return show(_type_active);
+}
+
+bool SDLConnectionDialog::hide()
+{
+ std::lock_guard lock(_mux);
+ return show(MSG_DISCARD);
+}
+
+bool SDLConnectionDialog::running() const
+{
+ std::lock_guard lock(_mux);
+ return _running;
+}
+
+bool SDLConnectionDialog::update()
+{
+ std::lock_guard lock(_mux);
+ switch (_type)
+ {
+ case MSG_INFO:
+ case MSG_WARN:
+ case MSG_ERROR:
+ _type_active = _type;
+ createWindow();
+ break;
+ case MSG_DISCARD:
+ resetTimer();
+ destroyWindow();
+ break;
+ default:
+ if (_window)
+ {
+ SDL_SetWindowTitle(_window, _title.c_str());
+ }
+ break;
+ }
+ _type = MSG_NONE;
+ return true;
+}
+
+bool SDLConnectionDialog::setModal()
+{
+ if (_window)
+ {
+ auto sdl = get_context(_context);
+ if (sdl->windows.empty())
+ return true;
+
+ auto parent = sdl->windows.begin()->second.window();
+ SDL_SetWindowModalFor(_window, parent);
+ SDL_RaiseWindow(_window);
+ }
+ return true;
+}
+
+bool SDLConnectionDialog::clearWindow(SDL_Renderer* renderer)
+{
+ assert(renderer);
+
+ const int drc = SDL_SetRenderDrawColor(renderer, backgroundcolor.r, backgroundcolor.g,
+ backgroundcolor.b, backgroundcolor.a);
+ if (widget_log_error(drc, "SDL_SetRenderDrawColor"))
+ return false;
+
+ const int rcls = SDL_RenderClear(renderer);
+ return !widget_log_error(rcls, "SDL_RenderClear");
+}
+
+bool SDLConnectionDialog::update(SDL_Renderer* renderer)
+{
+ if (!renderer)
+ return false;
+
+ if (!clearWindow(renderer))
+ return false;
+
+ for (auto& btn : _list)
+ {
+ if (!btn.widget.update_text(renderer, _msg, btn.fgcolor, btn.bgcolor))
+ return false;
+ }
+
+ if (!_buttons.update(renderer))
+ return false;
+
+ SDL_RenderPresent(renderer);
+ return true;
+}
+
+bool SDLConnectionDialog::wait(bool ignoreRdpContext)
+{
+ while (running())
+ {
+ if (!ignoreRdpContext)
+ {
+ if (freerdp_shall_disconnect_context(_context))
+ return false;
+ }
+ std::this_thread::yield();
+ }
+ return true;
+}
+
+bool SDLConnectionDialog::handle(const SDL_Event& event)
+{
+ Uint32 windowID = 0;
+ if (_window)
+ {
+ windowID = SDL_GetWindowID(_window);
+ }
+
+ switch (event.type)
+ {
+ case SDL_USEREVENT_RETRY_DIALOG:
+ return update();
+ case SDL_QUIT:
+ resetTimer();
+ destroyWindow();
+ return false;
+ case SDL_KEYDOWN:
+ case SDL_KEYUP:
+ if (visible())
+ {
+ auto ev = reinterpret_cast<const SDL_KeyboardEvent&>(event);
+ update(_renderer);
+ switch (event.key.keysym.sym)
+ {
+ case SDLK_RETURN:
+ case SDLK_RETURN2:
+ case SDLK_ESCAPE:
+ case SDLK_KP_ENTER:
+ if (event.type == SDL_KEYUP)
+ {
+ freerdp_abort_event(_context);
+ sdl_push_quit();
+ }
+ break;
+ case SDLK_TAB:
+ _buttons.set_highlight_next();
+ break;
+ default:
+ break;
+ }
+
+ return windowID == ev.windowID;
+ }
+ return false;
+ case SDL_MOUSEMOTION:
+ if (visible())
+ {
+ auto ev = reinterpret_cast<const SDL_MouseMotionEvent&>(event);
+
+ _buttons.set_mouseover(event.button.x, event.button.y);
+ update(_renderer);
+ return windowID == ev.windowID;
+ }
+ return false;
+ case SDL_MOUSEBUTTONDOWN:
+ case SDL_MOUSEBUTTONUP:
+ if (visible())
+ {
+ auto ev = reinterpret_cast<const SDL_MouseButtonEvent&>(event);
+ update(_renderer);
+
+ auto button = _buttons.get_selected(event.button);
+ if (button)
+ {
+ if (event.type == SDL_MOUSEBUTTONUP)
+ {
+ freerdp_abort_event(_context);
+ sdl_push_quit();
+ }
+ }
+
+ return windowID == ev.windowID;
+ }
+ return false;
+ case SDL_MOUSEWHEEL:
+ if (visible())
+ {
+ auto ev = reinterpret_cast<const SDL_MouseWheelEvent&>(event);
+ update(_renderer);
+ return windowID == ev.windowID;
+ }
+ return false;
+ case SDL_FINGERUP:
+ case SDL_FINGERDOWN:
+ if (visible())
+ {
+ auto ev = reinterpret_cast<const SDL_TouchFingerEvent&>(event);
+ update(_renderer);
+#if SDL_VERSION_ATLEAST(2, 0, 18)
+ return windowID == ev.windowID;
+#else
+ return false;
+#endif
+ }
+ return false;
+ case SDL_WINDOWEVENT:
+ {
+ auto ev = reinterpret_cast<const SDL_WindowEvent&>(event);
+ switch (ev.event)
+ {
+ case SDL_WINDOWEVENT_CLOSE:
+ if (windowID == ev.windowID)
+ {
+ freerdp_abort_event(_context);
+ sdl_push_quit();
+ }
+ break;
+ default:
+ update(_renderer);
+ setModal();
+ break;
+ }
+
+ return windowID == ev.windowID;
+ }
+ default:
+ return false;
+ }
+}
+
+bool SDLConnectionDialog::createWindow()
+{
+ destroyWindow();
+
+ const size_t widget_height = 50;
+ const size_t widget_width = 600;
+ const size_t total_height = 300;
+
+ _window = SDL_CreateWindow(
+ _title.c_str(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, widget_width,
+ total_height, SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_MOUSE_FOCUS | SDL_WINDOW_INPUT_FOCUS);
+ if (_window == nullptr)
+ {
+ widget_log_error(-1, "SDL_CreateWindow");
+ return false;
+ }
+ setModal();
+
+ _renderer = SDL_CreateRenderer(_window, -1, SDL_RENDERER_ACCELERATED);
+ if (_renderer == nullptr)
+ {
+ widget_log_error(-1, "SDL_CreateRenderer");
+ return false;
+ }
+
+ SDL_Color res_bgcolor;
+ switch (_type_active)
+ {
+ case MSG_INFO:
+ res_bgcolor = infocolor;
+ break;
+ case MSG_WARN:
+ res_bgcolor = warncolor;
+ break;
+ case MSG_ERROR:
+ res_bgcolor = errorcolor;
+ break;
+ case MSG_DISCARD:
+ default:
+ res_bgcolor = backgroundcolor;
+ break;
+ }
+
+#if defined(WITH_SDL_IMAGE_DIALOGS)
+ std::string res_name;
+ switch (_type_active)
+ {
+ case MSG_INFO:
+ res_name = "icon_info.svg";
+ break;
+ case MSG_WARN:
+ res_name = "icon_warning.svg";
+ break;
+ case MSG_ERROR:
+ res_name = "icon_error.svg";
+ break;
+ case MSG_DISCARD:
+ default:
+ res_name = "";
+ break;
+ }
+
+ int height = (total_height - 3ul * vpadding) / 2ul;
+ SDL_Rect iconRect{ hpadding, vpadding, widget_width / 4ul - 2ul * hpadding, height };
+ widget_cfg_t icon{ textcolor,
+ res_bgcolor,
+ { _renderer, iconRect,
+ SDLResourceManager::get(SDLResourceManager::typeImages(), res_name) } };
+ _list.emplace_back(std::move(icon));
+
+ iconRect.y += height;
+
+ widget_cfg_t logo{ textcolor,
+ backgroundcolor,
+ { _renderer, iconRect,
+ SDLResourceManager::get(SDLResourceManager::typeImages(),
+ "FreeRDP_Icon.svg") } };
+ _list.emplace_back(std::move(logo));
+
+ SDL_Rect rect = { widget_width / 4ul, vpadding, widget_width * 3ul / 4ul,
+ total_height - 3ul * vpadding - widget_height };
+#else
+ SDL_Rect rect = { hpadding, vpadding, widget_width - 2ul * hpadding,
+ total_height - 2ul * vpadding };
+#endif
+
+ widget_cfg_t w{ textcolor, backgroundcolor, { _renderer, rect, false } };
+ w.widget.set_wrap(true, widget_width);
+ _list.emplace_back(std::move(w));
+ rect.y += widget_height + vpadding;
+
+ const std::vector<int> buttonids = { 1 };
+ const std::vector<std::string> buttonlabels = { "cancel" };
+ _buttons.populate(_renderer, buttonlabels, buttonids, widget_width,
+ total_height - widget_height - vpadding,
+ static_cast<Sint32>(widget_width / 2), static_cast<Sint32>(widget_height));
+ _buttons.set_highlight(0);
+
+ SDL_ShowWindow(_window);
+ SDL_RaiseWindow(_window);
+
+ return true;
+}
+
+void SDLConnectionDialog::destroyWindow()
+{
+ _buttons.clear();
+ _list.clear();
+ SDL_DestroyRenderer(_renderer);
+ SDL_DestroyWindow(_window);
+ _renderer = nullptr;
+ _window = nullptr;
+}
+
+bool SDLConnectionDialog::show(MsgType type, const char* fmt, va_list ap)
+{
+ std::lock_guard lock(_mux);
+ _msg = print(fmt, ap);
+ return show(type);
+}
+
+bool SDLConnectionDialog::show(MsgType type)
+{
+ _type = type;
+ return sdl_push_user_event(SDL_USEREVENT_RETRY_DIALOG);
+}
+
+std::string SDLConnectionDialog::print(const char* fmt, va_list ap)
+{
+ int size = -1;
+ std::string res;
+
+ do
+ {
+ res.resize(128);
+ if (size > 0)
+ res.resize(size);
+
+ va_list copy;
+ va_copy(copy, ap);
+ size = vsnprintf(res.data(), res.size(), fmt, copy);
+ va_end(copy);
+
+ } while ((size > 0) && (size > res.size()));
+
+ return res;
+}
+
+bool SDLConnectionDialog::setTimer(Uint32 timeoutMS)
+{
+ std::lock_guard lock(_mux);
+ resetTimer();
+
+ _timer = SDL_AddTimer(timeoutMS, &SDLConnectionDialog::timeout, this);
+ _running = true;
+ return true;
+}
+
+void SDLConnectionDialog::resetTimer()
+{
+ if (_running)
+ SDL_RemoveTimer(_timer);
+ _running = false;
+}
+
+Uint32 SDLConnectionDialog::timeout(Uint32 intervalMS, void* pvthis)
+{
+ auto ths = static_cast<SDLConnectionDialog*>(pvthis);
+ ths->hide();
+ ths->_running = false;
+ return 0;
+}
+
+SDLConnectionDialogHider::SDLConnectionDialogHider(freerdp* instance)
+ : SDLConnectionDialogHider(get(instance))
+{
+}
+
+SDLConnectionDialogHider::SDLConnectionDialogHider(rdpContext* context)
+ : SDLConnectionDialogHider(get(context))
+{
+}
+
+SDLConnectionDialogHider::SDLConnectionDialogHider(SDLConnectionDialog* dialog) : _dialog(dialog)
+{
+ if (_dialog)
+ {
+ _visible = _dialog->visible();
+ if (_visible)
+ {
+ _dialog->hide();
+ }
+ }
+}
+
+SDLConnectionDialogHider::~SDLConnectionDialogHider()
+{
+ if (_dialog && _visible)
+ {
+ _dialog->show();
+ }
+}
+
+SDLConnectionDialog* SDLConnectionDialogHider::get(freerdp* instance)
+{
+ if (!instance)
+ return nullptr;
+ return get(instance->context);
+}
+
+SDLConnectionDialog* SDLConnectionDialogHider::get(rdpContext* context)
+{
+ auto sdl = get_context(context);
+ if (!sdl)
+ return nullptr;
+ return sdl->connection_dialog.get();
+}
diff --git a/client/SDL/dialogs/sdl_connection_dialog.hpp b/client/SDL/dialogs/sdl_connection_dialog.hpp
new file mode 100644
index 0000000..f21f538
--- /dev/null
+++ b/client/SDL/dialogs/sdl_connection_dialog.hpp
@@ -0,0 +1,129 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * SDL Client helper dialogs
+ *
+ * Copyright 2023 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <memory>
+#include <mutex>
+#include <string>
+#include <vector>
+
+#include <SDL.h>
+
+#include <freerdp/freerdp.h>
+
+#include "sdl_widget.hpp"
+#include "sdl_buttons.hpp"
+
+class SDLConnectionDialog
+{
+ public:
+ explicit SDLConnectionDialog(rdpContext* context);
+ SDLConnectionDialog(const SDLConnectionDialog& other) = delete;
+ SDLConnectionDialog(const SDLConnectionDialog&& other) = delete;
+ virtual ~SDLConnectionDialog();
+
+ bool visible() const;
+
+ bool setTitle(const char* fmt, ...);
+ bool showInfo(const char* fmt, ...);
+ bool showWarn(const char* fmt, ...);
+ bool showError(const char* fmt, ...);
+
+ bool show();
+ bool hide();
+
+ bool running() const;
+ bool wait(bool ignoreRdpContextQuit = false);
+
+ bool handle(const SDL_Event& event);
+
+ private:
+ enum MsgType
+ {
+ MSG_NONE,
+ MSG_INFO,
+ MSG_WARN,
+ MSG_ERROR,
+ MSG_DISCARD
+ };
+
+ private:
+ bool createWindow();
+ void destroyWindow();
+
+ bool update();
+
+ bool setModal();
+
+ bool clearWindow(SDL_Renderer* renderer);
+
+ bool update(SDL_Renderer* renderer);
+
+ bool show(MsgType type, const char* fmt, va_list ap);
+ bool show(MsgType type);
+
+ std::string print(const char* fmt, va_list ap);
+ bool setTimer(Uint32 timeoutMS = 15000);
+ void resetTimer();
+
+ private:
+ static Uint32 timeout(Uint32 intervalMS, void* _this);
+
+ private:
+ struct widget_cfg_t
+ {
+ SDL_Color fgcolor;
+ SDL_Color bgcolor;
+ SdlWidget widget;
+ };
+
+ private:
+ rdpContext* _context;
+ SDL_Window* _window;
+ SDL_Renderer* _renderer;
+ mutable std::mutex _mux;
+ std::string _title;
+ std::string _msg;
+ MsgType _type = MSG_NONE;
+ MsgType _type_active = MSG_NONE;
+ SDL_TimerID _timer = -1;
+ bool _running = false;
+ std::vector<widget_cfg_t> _list;
+ SdlButtonList _buttons;
+};
+
+class SDLConnectionDialogHider
+{
+ public:
+ explicit SDLConnectionDialogHider(freerdp* instance);
+ explicit SDLConnectionDialogHider(rdpContext* context);
+
+ explicit SDLConnectionDialogHider(SDLConnectionDialog* dialog);
+
+ ~SDLConnectionDialogHider();
+
+ private:
+ SDLConnectionDialog* get(freerdp* instance);
+ SDLConnectionDialog* get(rdpContext* context);
+
+ private:
+ SDLConnectionDialog* _dialog;
+ bool _visible;
+};
diff --git a/client/SDL/dialogs/sdl_dialogs.cpp b/client/SDL/dialogs/sdl_dialogs.cpp
new file mode 100644
index 0000000..32f8457
--- /dev/null
+++ b/client/SDL/dialogs/sdl_dialogs.cpp
@@ -0,0 +1,621 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * SDL Client helper dialogs
+ *
+ * Copyright 2023 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <vector>
+#include <string>
+#include <cassert>
+
+#include <freerdp/log.h>
+#include <freerdp/utils/smartcardlogon.h>
+
+#include <SDL.h>
+
+#include "../sdl_freerdp.hpp"
+#include "sdl_dialogs.hpp"
+#include "sdl_input.hpp"
+#include "sdl_input_widgets.hpp"
+#include "sdl_select.hpp"
+#include "sdl_selectlist.hpp"
+
+#define TAG CLIENT_TAG("SDL.dialogs")
+
+enum
+{
+ SHOW_DIALOG_ACCEPT_REJECT = 1,
+ SHOW_DIALOG_TIMED_ACCEPT = 2
+};
+
+static const char* type_str_for_flags(UINT32 flags)
+{
+ const char* type = "RDP-Server";
+
+ if (flags & VERIFY_CERT_FLAG_GATEWAY)
+ type = "RDP-Gateway";
+
+ if (flags & VERIFY_CERT_FLAG_REDIRECT)
+ type = "RDP-Redirect";
+ return type;
+}
+
+static BOOL sdl_wait_for_result(rdpContext* context, Uint32 type, SDL_Event* result)
+{
+ const SDL_Event empty = { 0 };
+
+ WINPR_ASSERT(context);
+ WINPR_ASSERT(result);
+
+ while (!freerdp_shall_disconnect_context(context))
+ {
+ *result = empty;
+ const int rc = SDL_PeepEvents(result, 1, SDL_GETEVENT, type, type);
+ if (rc > 0)
+ return TRUE;
+ Sleep(1);
+ }
+ return FALSE;
+}
+
+static int sdl_show_dialog(rdpContext* context, const char* title, const char* message,
+ Sint32 flags)
+{
+ SDL_Event event = { 0 };
+
+ if (!sdl_push_user_event(SDL_USEREVENT_SHOW_DIALOG, title, message, flags))
+ return 0;
+
+ if (!sdl_wait_for_result(context, SDL_USEREVENT_SHOW_RESULT, &event))
+ return 0;
+
+ return event.user.code;
+}
+
+BOOL sdl_authenticate_ex(freerdp* instance, char** username, char** password, char** domain,
+ rdp_auth_reason reason)
+{
+ SDL_Event event = { 0 };
+ BOOL res = FALSE;
+
+ SDLConnectionDialogHider hider(instance);
+
+ const char* target = freerdp_settings_get_server_name(instance->context->settings);
+ switch (reason)
+ {
+ case AUTH_NLA:
+ break;
+
+ case AUTH_TLS:
+ case AUTH_RDP:
+ case AUTH_SMARTCARD_PIN: /* in this case password is pin code */
+ if ((*username) && (*password))
+ return TRUE;
+ break;
+ case GW_AUTH_HTTP:
+ case GW_AUTH_RDG:
+ case GW_AUTH_RPC:
+ target =
+ freerdp_settings_get_string(instance->context->settings, FreeRDP_GatewayHostname);
+ break;
+ default:
+ break;
+ }
+
+ char* title = nullptr;
+ size_t titlesize = 0;
+ winpr_asprintf(&title, &titlesize, "Credentials required for %s", target);
+
+ char* u = nullptr;
+ char* d = nullptr;
+ char* p = nullptr;
+
+ assert(username);
+ assert(domain);
+ assert(password);
+
+ u = *username;
+ d = *domain;
+ p = *password;
+
+ if (!sdl_push_user_event(SDL_USEREVENT_AUTH_DIALOG, title, u, d, p, reason))
+ goto fail;
+
+ if (!sdl_wait_for_result(instance->context, SDL_USEREVENT_AUTH_RESULT, &event))
+ goto fail;
+ else
+ {
+ auto arg = reinterpret_cast<SDL_UserAuthArg*>(event.padding);
+
+ res = arg->result > 0 ? TRUE : FALSE;
+
+ free(*username);
+ free(*domain);
+ free(*password);
+ *username = arg->user;
+ *domain = arg->domain;
+ *password = arg->password;
+ }
+
+fail:
+ free(title);
+ return res;
+}
+
+BOOL sdl_choose_smartcard(freerdp* instance, SmartcardCertInfo** cert_list, DWORD count,
+ DWORD* choice, BOOL gateway)
+{
+ BOOL res = FALSE;
+
+ WINPR_ASSERT(instance);
+ WINPR_ASSERT(cert_list);
+ WINPR_ASSERT(choice);
+
+ SDLConnectionDialogHider hider(instance);
+ std::vector<std::string> strlist;
+ std::vector<const char*> list;
+ for (DWORD i = 0; i < count; i++)
+ {
+ const SmartcardCertInfo* cert = cert_list[i];
+ char* reader = ConvertWCharToUtf8Alloc(cert->reader, nullptr);
+ char* container_name = ConvertWCharToUtf8Alloc(cert->containerName, nullptr);
+
+ char* msg = nullptr;
+ size_t len = 0;
+
+ winpr_asprintf(&msg, &len,
+ "%s\n\tReader: %s\n\tUser: %s@%s\n\tSubject: %s\n\tIssuer: %s\n\tUPN: %s",
+ container_name, reader, cert->userHint, cert->domainHint, cert->subject,
+ cert->issuer, cert->upn);
+
+ strlist.emplace_back(msg);
+ free(msg);
+ free(reader);
+ free(container_name);
+
+ auto& m = strlist.back();
+ list.push_back(m.c_str());
+ }
+
+ SDL_Event event = { 0 };
+ const char* title = "Select a logon smartcard certificate";
+ if (gateway)
+ title = "Select a gateway logon smartcard certificate";
+ if (!sdl_push_user_event(SDL_USEREVENT_SCARD_DIALOG, title, list.data(), count))
+ goto fail;
+
+ if (!sdl_wait_for_result(instance->context, SDL_USEREVENT_SCARD_RESULT, &event))
+ goto fail;
+
+ res = (event.user.code >= 0) ? TRUE : FALSE;
+ *choice = static_cast<DWORD>(event.user.code);
+
+fail:
+ return res;
+}
+
+SSIZE_T sdl_retry_dialog(freerdp* instance, const char* what, size_t current, void* userarg)
+{
+ WINPR_ASSERT(instance);
+ WINPR_ASSERT(instance->context);
+ WINPR_ASSERT(what);
+
+ auto sdl = get_context(instance->context);
+ std::lock_guard<CriticalSection> lock(sdl->critical);
+ WINPR_ASSERT(sdl->connection_dialog);
+
+ sdl->connection_dialog->setTitle("Retry connection to %s",
+ freerdp_settings_get_server_name(instance->context->settings));
+
+ if ((strcmp(what, "arm-transport") != 0) && (strcmp(what, "connection") != 0))
+ {
+ sdl->connection_dialog->showError("Unknown module %s, aborting", what);
+ return -1;
+ }
+
+ if (current == 0)
+ {
+ if (strcmp(what, "arm-transport") == 0)
+ sdl->connection_dialog->showWarn("[%s] Starting your VM. It may take up to 5 minutes",
+ what);
+ }
+
+ auto settings = instance->context->settings;
+ const BOOL enabled = freerdp_settings_get_bool(settings, FreeRDP_AutoReconnectionEnabled);
+
+ if (!enabled)
+ {
+ sdl->connection_dialog->showError(
+ "Automatic reconnection disabled, terminating. Try to connect again later");
+ return -1;
+ }
+
+ const size_t max = freerdp_settings_get_uint32(settings, FreeRDP_AutoReconnectMaxRetries);
+ const size_t delay = freerdp_settings_get_uint32(settings, FreeRDP_TcpConnectTimeout);
+ if (current >= max)
+ {
+ sdl->connection_dialog->showError(
+ "[%s] retries exceeded. Your VM failed to start. Try again later or contact your "
+ "tech support for help if this keeps happening.",
+ what);
+ return -1;
+ }
+
+ sdl->connection_dialog->showInfo("[%s] retry %" PRIuz "/%" PRIuz ", delaying %" PRIuz
+ "ms before next attempt",
+ what, current, max, delay);
+ return delay;
+}
+
+BOOL sdl_present_gateway_message(freerdp* instance, UINT32 type, BOOL isDisplayMandatory,
+ BOOL isConsentMandatory, size_t length, const WCHAR* wmessage)
+{
+ if (!isDisplayMandatory)
+ return TRUE;
+
+ char* title = nullptr;
+ size_t len = 0;
+ winpr_asprintf(&title, &len, "[gateway]");
+
+ Sint32 flags = 0;
+ if (isConsentMandatory)
+ flags = SHOW_DIALOG_ACCEPT_REJECT;
+ else if (isDisplayMandatory)
+ flags = SHOW_DIALOG_TIMED_ACCEPT;
+ char* message = ConvertWCharNToUtf8Alloc(wmessage, length, nullptr);
+
+ SDLConnectionDialogHider hider(instance);
+ const int rc = sdl_show_dialog(instance->context, title, message, flags);
+ free(title);
+ free(message);
+ return rc > 0 ? TRUE : FALSE;
+}
+
+int sdl_logon_error_info(freerdp* instance, UINT32 data, UINT32 type)
+{
+ int rc = -1;
+ const char* str_data = freerdp_get_logon_error_info_data(data);
+ const char* str_type = freerdp_get_logon_error_info_type(type);
+
+ if (!instance || !instance->context)
+ return -1;
+
+ /* ignore LOGON_MSG_SESSION_CONTINUE message */
+ if (type == LOGON_MSG_SESSION_CONTINUE)
+ return 0;
+
+ SDLConnectionDialogHider hider(instance);
+
+ char* title = nullptr;
+ size_t tlen = 0;
+ winpr_asprintf(&title, &tlen, "[%s] info",
+ freerdp_settings_get_server_name(instance->context->settings));
+
+ char* message = nullptr;
+ size_t mlen = 0;
+ winpr_asprintf(&message, &mlen, "Logon Error Info %s [%s]", str_data, str_type);
+
+ rc = sdl_show_dialog(instance->context, title, message, SHOW_DIALOG_ACCEPT_REJECT);
+ free(title);
+ free(message);
+ return rc;
+}
+
+static DWORD sdl_show_ceritifcate_dialog(rdpContext* context, const char* title,
+ const char* message)
+{
+ SDLConnectionDialogHider hider(context);
+ if (!sdl_push_user_event(SDL_USEREVENT_CERT_DIALOG, title, message))
+ return 0;
+
+ SDL_Event event = { 0 };
+ if (!sdl_wait_for_result(context, SDL_USEREVENT_CERT_RESULT, &event))
+ return 0;
+ return static_cast<DWORD>(event.user.code);
+}
+
+DWORD sdl_verify_changed_certificate_ex(freerdp* instance, const char* host, UINT16 port,
+ const char* common_name, const char* subject,
+ const char* issuer, const char* new_fingerprint,
+ const char* old_subject, const char* old_issuer,
+ const char* old_fingerprint, DWORD flags)
+{
+ const char* type = type_str_for_flags(flags);
+
+ WINPR_ASSERT(instance);
+ WINPR_ASSERT(instance->context);
+ WINPR_ASSERT(instance->context->settings);
+
+ SDLConnectionDialogHider hider(instance);
+ /* Newer versions of FreeRDP allow exposing the whole PEM by setting
+ * FreeRDP_CertificateCallbackPreferPEM to TRUE
+ */
+ char* new_fp_str = nullptr;
+ size_t len = 0;
+ if (flags & VERIFY_CERT_FLAG_FP_IS_PEM)
+ {
+ winpr_asprintf(&new_fp_str, &len,
+ "----------- Certificate --------------\n"
+ "%s\n"
+ "--------------------------------------\n",
+ new_fingerprint);
+ }
+ else
+ winpr_asprintf(&new_fp_str, &len, "Thumbprint: %s\n", new_fingerprint);
+
+ /* Newer versions of FreeRDP allow exposing the whole PEM by setting
+ * FreeRDP_CertificateCallbackPreferPEM to TRUE
+ */
+ char* old_fp_str = nullptr;
+ size_t olen = 0;
+ if (flags & VERIFY_CERT_FLAG_FP_IS_PEM)
+ {
+ winpr_asprintf(&old_fp_str, &olen,
+ "----------- Certificate --------------\n"
+ "%s\n"
+ "--------------------------------------\n",
+ old_fingerprint);
+ }
+ else
+ winpr_asprintf(&old_fp_str, &olen, "Thumbprint: %s\n", old_fingerprint);
+
+ const char* collission_str = "";
+ if (flags & VERIFY_CERT_FLAG_MATCH_LEGACY_SHA1)
+ {
+ collission_str =
+ "A matching entry with legacy SHA1 was found in local known_hosts2 store.\n"
+ "If you just upgraded from a FreeRDP version before 2.0 this is expected.\n"
+ "The hashing algorithm has been upgraded from SHA1 to SHA256.\n"
+ "All manually accepted certificates must be reconfirmed!\n"
+ "\n";
+ }
+
+ char* title = nullptr;
+ size_t tlen = 0;
+ winpr_asprintf(&title, &tlen, "Certificate for %s:%" PRIu16 " (%s) has changed", host, port,
+ type);
+
+ char* message = nullptr;
+ size_t mlen = 0;
+ winpr_asprintf(&message, &mlen,
+ "New Certificate details:\n"
+ "Common Name: %s\n"
+ "Subject: %s\n"
+ "Issuer: %s\n"
+ "%s\n"
+ "Old Certificate details:\n"
+ "Subject: %s\n"
+ "Issuer: %s\n"
+ "%s\n"
+ "%s\n"
+ "The above X.509 certificate does not match the certificate used for previous "
+ "connections.\n"
+ "This may indicate that the certificate has been tampered with.\n"
+ "Please contact the administrator of the RDP server and clarify.\n",
+ common_name, subject, issuer, new_fp_str, old_subject, old_issuer, old_fp_str,
+ collission_str);
+
+ const DWORD rc = sdl_show_ceritifcate_dialog(instance->context, title, message);
+ free(title);
+ free(message);
+ free(new_fp_str);
+ free(old_fp_str);
+
+ return rc;
+}
+
+DWORD sdl_verify_certificate_ex(freerdp* instance, const char* host, UINT16 port,
+ const char* common_name, const char* subject, const char* issuer,
+ const char* fingerprint, DWORD flags)
+{
+ const char* type = type_str_for_flags(flags);
+
+ /* Newer versions of FreeRDP allow exposing the whole PEM by setting
+ * FreeRDP_CertificateCallbackPreferPEM to TRUE
+ */
+ char* fp_str = nullptr;
+ size_t len = 0;
+ if (flags & VERIFY_CERT_FLAG_FP_IS_PEM)
+ {
+ winpr_asprintf(&fp_str, &len,
+ "----------- Certificate --------------\n"
+ "%s\n"
+ "--------------------------------------\n",
+ fingerprint);
+ }
+ else
+ winpr_asprintf(&fp_str, &len, "Thumbprint: %s\n", fingerprint);
+
+ char* title = nullptr;
+ size_t tlen = 0;
+ winpr_asprintf(&title, &tlen, "New certificate for %s:%" PRIu16 " (%s)", host, port, type);
+
+ char* message = nullptr;
+ size_t mlen = 0;
+ winpr_asprintf(
+ &message, &mlen,
+ "Common Name: %s\n"
+ "Subject: %s\n"
+ "Issuer: %s\n"
+ "%s\n"
+ "The above X.509 certificate could not be verified, possibly because you do not have\n"
+ "the CA certificate in your certificate store, or the certificate has expired.\n"
+ "Please look at the OpenSSL documentation on how to add a private CA to the store.\n",
+ common_name, subject, issuer, fp_str);
+
+ SDLConnectionDialogHider hider(instance);
+ const DWORD rc = sdl_show_ceritifcate_dialog(instance->context, title, message);
+ free(fp_str);
+ free(title);
+ free(message);
+ return rc;
+}
+
+BOOL sdl_cert_dialog_show(const char* title, const char* message)
+{
+ int buttonid = -1;
+ enum
+ {
+ BUTTONID_CERT_ACCEPT_PERMANENT = 23,
+ BUTTONID_CERT_ACCEPT_TEMPORARY = 24,
+ BUTTONID_CERT_DENY = 25
+ };
+ const SDL_MessageBoxButtonData buttons[] = {
+ { 0, BUTTONID_CERT_ACCEPT_PERMANENT, "permanent" },
+ { SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, BUTTONID_CERT_ACCEPT_TEMPORARY, "temporary" },
+ { SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, BUTTONID_CERT_DENY, "cancel" }
+ };
+
+ const SDL_MessageBoxData data = { SDL_MESSAGEBOX_WARNING, nullptr, title, message,
+ ARRAYSIZE(buttons), buttons, nullptr };
+ const int rc = SDL_ShowMessageBox(&data, &buttonid);
+
+ Sint32 value = -1;
+ if (rc < 0)
+ value = 0;
+ else
+ {
+ switch (buttonid)
+ {
+ case BUTTONID_CERT_ACCEPT_PERMANENT:
+ value = 1;
+ break;
+ case BUTTONID_CERT_ACCEPT_TEMPORARY:
+ value = 2;
+ break;
+ default:
+ value = 0;
+ break;
+ }
+ }
+
+ return sdl_push_user_event(SDL_USEREVENT_CERT_RESULT, value);
+}
+
+BOOL sdl_message_dialog_show(const char* title, const char* message, Sint32 flags)
+{
+ int buttonid = -1;
+ enum
+ {
+ BUTTONID_SHOW_ACCEPT = 24,
+ BUTTONID_SHOW_DENY = 25
+ };
+ const SDL_MessageBoxButtonData buttons[] = {
+ { SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, BUTTONID_SHOW_ACCEPT, "accept" },
+ { SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, BUTTONID_SHOW_DENY, "cancel" }
+ };
+
+ const int button_cnt = (flags & SHOW_DIALOG_ACCEPT_REJECT) ? 2 : 1;
+ const SDL_MessageBoxData data = {
+ SDL_MESSAGEBOX_WARNING, nullptr, title, message, button_cnt, buttons, nullptr
+ };
+ const int rc = SDL_ShowMessageBox(&data, &buttonid);
+
+ Sint32 value = -1;
+ if (rc < 0)
+ value = 0;
+ else
+ {
+ switch (buttonid)
+ {
+ case BUTTONID_SHOW_ACCEPT:
+ value = 1;
+ break;
+ default:
+ value = 0;
+ break;
+ }
+ }
+
+ return sdl_push_user_event(SDL_USEREVENT_SHOW_RESULT, value);
+}
+
+BOOL sdl_auth_dialog_show(const SDL_UserAuthArg* args)
+{
+ const std::vector<std::string> auth = { "Username: ", "Domain: ",
+ "Password: " };
+ const std::vector<std::string> authPin = { "Device: ", "PIN: " };
+ const std::vector<std::string> gw = { "GatewayUsername: ", "GatewayDomain: ",
+ "GatewayPassword: " };
+ std::vector<std::string> prompt;
+ Sint32 rc = -1;
+
+ switch (args->result)
+ {
+ case AUTH_SMARTCARD_PIN:
+ prompt = authPin;
+ break;
+ case AUTH_TLS:
+ case AUTH_RDP:
+ case AUTH_NLA:
+ prompt = auth;
+ break;
+ case GW_AUTH_HTTP:
+ case GW_AUTH_RDG:
+ case GW_AUTH_RPC:
+ prompt = gw;
+ break;
+ default:
+ break;
+ }
+
+ std::vector<std::string> result;
+
+ if (!prompt.empty())
+ {
+ std::vector<std::string> initial{ args->user ? args->user : "Smartcard", "" };
+ std::vector<Uint32> flags = { SdlInputWidget::SDL_INPUT_READONLY,
+ SdlInputWidget::SDL_INPUT_MASK };
+ if (args->result != AUTH_SMARTCARD_PIN)
+ {
+ initial = { args->user ? args->user : "", args->domain ? args->domain : "",
+ args->password ? args->password : "" };
+ flags = { 0, 0, SdlInputWidget::SDL_INPUT_MASK };
+ }
+ SdlInputWidgetList ilist(args->title, prompt, initial, flags);
+ rc = ilist.run(result);
+ }
+
+ if ((result.size() < prompt.size()))
+ rc = -1;
+
+ char* user = nullptr;
+ char* domain = nullptr;
+ char* pwd = nullptr;
+ if (rc > 0)
+ {
+ user = _strdup(result[0].c_str());
+ if (args->result == AUTH_SMARTCARD_PIN)
+ pwd = _strdup(result[1].c_str());
+ else
+ {
+ domain = _strdup(result[1].c_str());
+ pwd = _strdup(result[2].c_str());
+ }
+ }
+ return sdl_push_user_event(SDL_USEREVENT_AUTH_RESULT, user, domain, pwd, rc);
+}
+
+BOOL sdl_scard_dialog_show(const char* title, Sint32 count, const char** list)
+{
+ std::vector<std::string> vlist;
+ vlist.reserve(count);
+ for (Sint32 x = 0; x < count; x++)
+ vlist.emplace_back(list[x]);
+ SdlSelectList slist(title, vlist);
+ Sint32 value = slist.run();
+ return sdl_push_user_event(SDL_USEREVENT_SCARD_RESULT, value);
+}
diff --git a/client/SDL/dialogs/sdl_dialogs.hpp b/client/SDL/dialogs/sdl_dialogs.hpp
new file mode 100644
index 0000000..ae9bbe6
--- /dev/null
+++ b/client/SDL/dialogs/sdl_dialogs.hpp
@@ -0,0 +1,53 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * SDL Client helper dialogs
+ *
+ * Copyright 2023 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <winpr/wtypes.h>
+#include <freerdp/freerdp.h>
+
+#include "../sdl_types.hpp"
+#include "../sdl_utils.hpp"
+
+BOOL sdl_authenticate_ex(freerdp* instance, char** username, char** password, char** domain,
+ rdp_auth_reason reason);
+BOOL sdl_choose_smartcard(freerdp* instance, SmartcardCertInfo** cert_list, DWORD count,
+ DWORD* choice, BOOL gateway);
+
+SSIZE_T sdl_retry_dialog(freerdp* instance, const char* what, size_t current, void* userarg);
+
+DWORD sdl_verify_certificate_ex(freerdp* instance, const char* host, UINT16 port,
+ const char* common_name, const char* subject, const char* issuer,
+ const char* fingerprint, DWORD flags);
+
+DWORD sdl_verify_changed_certificate_ex(freerdp* instance, const char* host, UINT16 port,
+ const char* common_name, const char* subject,
+ const char* issuer, const char* new_fingerprint,
+ const char* old_subject, const char* old_issuer,
+ const char* old_fingerprint, DWORD flags);
+
+int sdl_logon_error_info(freerdp* instance, UINT32 data, UINT32 type);
+
+BOOL sdl_present_gateway_message(freerdp* instance, UINT32 type, BOOL isDisplayMandatory,
+ BOOL isConsentMandatory, size_t length, const WCHAR* message);
+
+BOOL sdl_message_dialog_show(const char* title, const char* message, Sint32 flags);
+BOOL sdl_cert_dialog_show(const char* title, const char* message);
+BOOL sdl_scard_dialog_show(const char* title, Sint32 count, const char** list);
+BOOL sdl_auth_dialog_show(const SDL_UserAuthArg* args);
diff --git a/client/SDL/dialogs/sdl_input.cpp b/client/SDL/dialogs/sdl_input.cpp
new file mode 100644
index 0000000..6e7bf12
--- /dev/null
+++ b/client/SDL/dialogs/sdl_input.cpp
@@ -0,0 +1,177 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * SDL Client helper dialogs
+ *
+ * Copyright 2023 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "sdl_input.hpp"
+
+#include <cassert>
+
+#include <string>
+
+#include <SDL.h>
+#include <SDL_ttf.h>
+
+#include "sdl_widget.hpp"
+#include "sdl_button.hpp"
+#include "sdl_buttons.hpp"
+
+static const SDL_Color inputbackgroundcolor = { 0x56, 0x56, 0x56, 0xff };
+static const SDL_Color inputhighlightcolor = { 0x80, 0, 0, 0x60 };
+static const SDL_Color inputmouseovercolor = { 0, 0x80, 0, 0x60 };
+static const SDL_Color inputfontcolor = { 0xd1, 0xcf, 0xcd, 0xff };
+static const SDL_Color labelbackgroundcolor = { 0x56, 0x56, 0x56, 0xff };
+static const SDL_Color labelfontcolor = { 0xd1, 0xcf, 0xcd, 0xff };
+static const Uint32 vpadding = 5;
+static const Uint32 hpadding = 10;
+
+SdlInputWidget::SdlInputWidget(SDL_Renderer* renderer, const std::string& label,
+ const std::string& initial, Uint32 flags, size_t offset,
+ size_t width, size_t height)
+ : _flags(flags), _text(initial), _text_label(label),
+ _label(renderer,
+ { 0, static_cast<int>(offset * (height + vpadding)), static_cast<int>(width),
+ static_cast<int>(height) },
+ false),
+ _input(renderer,
+ { static_cast<int>(width + hpadding), static_cast<int>(offset * (height + vpadding)),
+ static_cast<int>(width), static_cast<int>(height) },
+ true),
+ _highlight(false), _mouseover(false)
+{
+}
+
+SdlInputWidget::SdlInputWidget(SdlInputWidget&& other) noexcept
+ : _flags(std::move(other._flags)), _text(std::move(other._text)),
+ _text_label(std::move(other._text_label)), _label(std::move(other._label)),
+ _input(std::move(other._input)), _highlight(other._highlight), _mouseover(other._mouseover)
+{
+}
+
+bool SdlInputWidget::fill_label(SDL_Renderer* renderer, SDL_Color color)
+{
+ if (!_label.fill(renderer, color))
+ return false;
+ return _label.update_text(renderer, _text_label, labelfontcolor);
+}
+
+bool SdlInputWidget::update_label(SDL_Renderer* renderer)
+{
+ return _label.update_text(renderer, _text_label, labelfontcolor, labelbackgroundcolor);
+}
+
+bool SdlInputWidget::set_mouseover(SDL_Renderer* renderer, bool mouseOver)
+{
+ if (readonly())
+ return true;
+ _mouseover = mouseOver;
+ return update_input(renderer);
+}
+
+bool SdlInputWidget::set_highlight(SDL_Renderer* renderer, bool highlight)
+{
+ if (readonly())
+ return true;
+ _highlight = highlight;
+ return update_input(renderer);
+}
+
+bool SdlInputWidget::update_input(SDL_Renderer* renderer)
+{
+ std::vector<SDL_Color> colors = { inputbackgroundcolor };
+ if (_highlight)
+ colors.push_back(inputhighlightcolor);
+ if (_mouseover)
+ colors.push_back(inputmouseovercolor);
+
+ if (!_input.fill(renderer, colors))
+ return false;
+ return update_input(renderer, inputfontcolor);
+}
+
+bool SdlInputWidget::resize_input(size_t size)
+{
+ _text.resize(size);
+
+ return true;
+}
+
+bool SdlInputWidget::set_str(SDL_Renderer* renderer, const std::string& text)
+{
+ if (readonly())
+ return true;
+ _text = text;
+ if (!resize_input(_text.size()))
+ return false;
+ return update_input(renderer);
+}
+
+bool SdlInputWidget::remove_str(SDL_Renderer* renderer, size_t count)
+{
+ if (readonly())
+ return true;
+
+ assert(renderer);
+ if (_text.empty())
+ return true;
+
+ if (!resize_input(_text.size() - count))
+ return false;
+ return update_input(renderer);
+}
+
+bool SdlInputWidget::append_str(SDL_Renderer* renderer, const std::string& str)
+{
+ assert(renderer);
+ if (readonly())
+ return true;
+
+ _text.append(str);
+ if (!resize_input(_text.size()))
+ return false;
+ return update_input(renderer);
+}
+
+const SDL_Rect& SdlInputWidget::input_rect() const
+{
+ return _input.rect();
+}
+
+std::string SdlInputWidget::value() const
+{
+ return _text;
+}
+
+bool SdlInputWidget::readonly() const
+{
+ return (_flags & SDL_INPUT_READONLY) != 0;
+}
+
+bool SdlInputWidget::update_input(SDL_Renderer* renderer, SDL_Color fgcolor)
+{
+ std::string text = _text;
+ if (!text.empty())
+ {
+ if (_flags & SDL_INPUT_MASK)
+ {
+ for (char& x : text)
+ x = '*';
+ }
+ }
+
+ return _input.update_text(renderer, text, fgcolor);
+}
diff --git a/client/SDL/dialogs/sdl_input.hpp b/client/SDL/dialogs/sdl_input.hpp
new file mode 100644
index 0000000..11492d1
--- /dev/null
+++ b/client/SDL/dialogs/sdl_input.hpp
@@ -0,0 +1,73 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * SDL Client helper dialogs
+ *
+ * Copyright 2023 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <vector>
+#include <string>
+
+#include <SDL.h>
+#include "sdl_widget.hpp"
+
+class SdlInputWidget
+{
+ public:
+ enum
+ {
+ SDL_INPUT_MASK = 1,
+ SDL_INPUT_READONLY = 2
+ };
+
+ public:
+ SdlInputWidget(SDL_Renderer* renderer, const std::string& label, const std::string& initial,
+ Uint32 flags, size_t offset, size_t width, size_t height);
+ SdlInputWidget(SdlInputWidget&& other) noexcept;
+
+ bool fill_label(SDL_Renderer* renderer, SDL_Color color);
+ bool update_label(SDL_Renderer* renderer);
+
+ bool set_mouseover(SDL_Renderer* renderer, bool mouseOver);
+ bool set_highlight(SDL_Renderer* renderer, bool hightlight);
+ bool update_input(SDL_Renderer* renderer);
+ bool resize_input(size_t size);
+
+ bool set_str(SDL_Renderer* renderer, const std::string& text);
+ bool remove_str(SDL_Renderer* renderer, size_t count);
+ bool append_str(SDL_Renderer* renderer, const std::string& text);
+
+ [[nodiscard]] const SDL_Rect& input_rect() const;
+ [[nodiscard]] std::string value() const;
+
+ [[nodiscard]] bool readonly() const;
+
+ protected:
+ bool update_input(SDL_Renderer* renderer, SDL_Color fgclor);
+
+ private:
+ SdlInputWidget(const SdlInputWidget& other) = delete;
+
+ private:
+ Uint32 _flags;
+ std::string _text;
+ std::string _text_label;
+ SdlWidget _label;
+ SdlWidget _input;
+ bool _highlight;
+ bool _mouseover;
+};
diff --git a/client/SDL/dialogs/sdl_input_widgets.cpp b/client/SDL/dialogs/sdl_input_widgets.cpp
new file mode 100644
index 0000000..5846308
--- /dev/null
+++ b/client/SDL/dialogs/sdl_input_widgets.cpp
@@ -0,0 +1,299 @@
+#include <cassert>
+#include <algorithm>
+
+#include "sdl_input_widgets.hpp"
+
+static const Uint32 vpadding = 5;
+
+SdlInputWidgetList::SdlInputWidgetList(const std::string& title,
+ const std::vector<std::string>& labels,
+ const std::vector<std::string>& initial,
+ const std::vector<Uint32>& flags)
+ : _window(nullptr), _renderer(nullptr)
+{
+ assert(labels.size() == initial.size());
+ assert(labels.size() == flags.size());
+ const std::vector<int> buttonids = { INPUT_BUTTON_ACCEPT, INPUT_BUTTON_CANCEL };
+ const std::vector<std::string> buttonlabels = { "accept", "cancel" };
+
+ const size_t widget_width = 300;
+ const size_t widget_heigth = 50;
+
+ const size_t total_width = widget_width + widget_width;
+ const size_t input_height = labels.size() * (widget_heigth + vpadding) + vpadding;
+ const size_t total_height = input_height + widget_heigth;
+ _window = SDL_CreateWindow(
+ title.c_str(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, total_width, total_height,
+ SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_MOUSE_FOCUS | SDL_WINDOW_INPUT_FOCUS);
+ if (_window == nullptr)
+ {
+ widget_log_error(-1, "SDL_CreateWindow");
+ }
+ else
+ {
+
+ _renderer = SDL_CreateRenderer(_window, -1, SDL_RENDERER_ACCELERATED);
+ if (_renderer == nullptr)
+ {
+ widget_log_error(-1, "SDL_CreateRenderer");
+ }
+ else
+ {
+ for (size_t x = 0; x < labels.size(); x++)
+ _list.emplace_back(_renderer, labels[x], initial[x], flags[x], x, widget_width,
+ widget_heigth);
+
+ _buttons.populate(_renderer, buttonlabels, buttonids, total_width,
+ static_cast<Sint32>(input_height), static_cast<Sint32>(widget_width),
+ static_cast<Sint32>(widget_heigth));
+ _buttons.set_highlight(0);
+ }
+ }
+}
+
+ssize_t SdlInputWidgetList::next(ssize_t current)
+{
+ size_t iteration = 0;
+ auto val = static_cast<size_t>(current);
+
+ do
+ {
+ if (iteration >= _list.size())
+ return -1;
+
+ if (iteration == 0)
+ {
+ if (current < 0)
+ val = 0;
+ else
+ val++;
+ }
+ else
+ val++;
+ iteration++;
+ val %= _list.size();
+ } while (!valid(static_cast<ssize_t>(val)));
+ return static_cast<ssize_t>(val);
+}
+
+bool SdlInputWidgetList::valid(ssize_t current) const
+{
+ if (current < 0)
+ return false;
+ auto s = static_cast<size_t>(current);
+ if (s >= _list.size())
+ return false;
+ return !_list[s].readonly();
+}
+
+SdlInputWidget* SdlInputWidgetList::get(ssize_t index)
+{
+ if (index < 0)
+ return nullptr;
+ auto s = static_cast<size_t>(index);
+ if (s >= _list.size())
+ return nullptr;
+ return &_list[s];
+}
+
+SdlInputWidgetList::~SdlInputWidgetList()
+{
+ _list.clear();
+ _buttons.clear();
+ SDL_DestroyRenderer(_renderer);
+ SDL_DestroyWindow(_window);
+}
+
+bool SdlInputWidgetList::update(SDL_Renderer* renderer)
+{
+ for (auto& btn : _list)
+ {
+ if (!btn.update_label(renderer))
+ return false;
+ if (!btn.update_input(renderer))
+ return false;
+ }
+
+ return _buttons.update(renderer);
+}
+
+ssize_t SdlInputWidgetList::get_index(const SDL_MouseButtonEvent& button)
+{
+ const Sint32 x = button.x;
+ const Sint32 y = button.y;
+ for (size_t i = 0; i < _list.size(); i++)
+ {
+ auto& cur = _list[i];
+ auto r = cur.input_rect();
+
+ if ((x >= r.x) && (x <= r.x + r.w) && (y >= r.y) && (y <= r.y + r.h))
+ return i;
+ }
+ return -1;
+}
+
+int SdlInputWidgetList::run(std::vector<std::string>& result)
+{
+ int res = -1;
+ ssize_t LastActiveTextInput = -1;
+ ssize_t CurrentActiveTextInput = next(-1);
+
+ if (!_window || !_renderer)
+ return -2;
+
+ try
+ {
+ bool running = true;
+ std::vector<SDL_Keycode> pressed;
+ while (running)
+ {
+ if (!clear_window(_renderer))
+ throw;
+
+ if (!update(_renderer))
+ throw;
+
+ if (!_buttons.update(_renderer))
+ throw;
+
+ SDL_Event event = {};
+ SDL_WaitEvent(&event);
+ switch (event.type)
+ {
+ case SDL_KEYUP:
+ {
+ auto it = std::remove(pressed.begin(), pressed.end(), event.key.keysym.sym);
+ pressed.erase(it, pressed.end());
+ }
+ break;
+ case SDL_KEYDOWN:
+ pressed.push_back(event.key.keysym.sym);
+ switch (event.key.keysym.sym)
+ {
+ case SDLK_BACKSPACE:
+ {
+ auto cur = get(CurrentActiveTextInput);
+ if (cur)
+ {
+ if (!cur->remove_str(_renderer, 1))
+ throw;
+ }
+ }
+ break;
+ case SDLK_TAB:
+ CurrentActiveTextInput = next(CurrentActiveTextInput);
+ break;
+ case SDLK_RETURN:
+ case SDLK_RETURN2:
+ case SDLK_KP_ENTER:
+ running = false;
+ res = INPUT_BUTTON_ACCEPT;
+ break;
+ case SDLK_ESCAPE:
+ running = false;
+ res = INPUT_BUTTON_CANCEL;
+ break;
+ case SDLK_v:
+ if (pressed.size() == 2)
+ {
+ if ((pressed[0] == SDLK_LCTRL) || (pressed[0] == SDLK_RCTRL))
+ {
+ auto cur = get(CurrentActiveTextInput);
+ if (cur)
+ {
+ auto text = SDL_GetClipboardText();
+ cur->set_str(_renderer, text);
+ }
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ break;
+ case SDL_TEXTINPUT:
+ {
+ auto cur = get(CurrentActiveTextInput);
+ if (cur)
+ {
+ if (!cur->append_str(_renderer, event.text.text))
+ throw;
+ }
+ }
+ break;
+ case SDL_MOUSEMOTION:
+ {
+ auto TextInputIndex = get_index(event.button);
+ for (auto& cur : _list)
+ {
+ if (!cur.set_mouseover(_renderer, false))
+ throw;
+ }
+ if (TextInputIndex >= 0)
+ {
+ auto& cur = _list[static_cast<size_t>(TextInputIndex)];
+ if (!cur.set_mouseover(_renderer, true))
+ throw;
+ }
+
+ _buttons.set_mouseover(event.button.x, event.button.y);
+ }
+ break;
+ case SDL_MOUSEBUTTONDOWN:
+ {
+ auto val = get_index(event.button);
+ if (valid(val))
+ CurrentActiveTextInput = val;
+
+ auto button = _buttons.get_selected(event.button);
+ if (button)
+ {
+ running = false;
+ if (button->id() == INPUT_BUTTON_CANCEL)
+ res = INPUT_BUTTON_CANCEL;
+ else
+ res = INPUT_BUTTON_ACCEPT;
+ }
+ }
+ break;
+ case SDL_QUIT:
+ res = INPUT_BUTTON_CANCEL;
+ running = false;
+ break;
+ default:
+ break;
+ }
+
+ if (LastActiveTextInput != CurrentActiveTextInput)
+ {
+ if (CurrentActiveTextInput < 0)
+ SDL_StopTextInput();
+ else
+ SDL_StartTextInput();
+ LastActiveTextInput = CurrentActiveTextInput;
+ }
+
+ for (auto& cur : _list)
+ {
+ if (!cur.set_highlight(_renderer, false))
+ throw;
+ }
+ auto cur = get(CurrentActiveTextInput);
+ if (cur)
+ {
+ if (!cur->set_highlight(_renderer, true))
+ throw;
+ }
+
+ SDL_RenderPresent(_renderer);
+ }
+
+ for (auto& cur : _list)
+ result.push_back(cur.value());
+ }
+ catch (...)
+ {
+ }
+
+ return res;
+}
diff --git a/client/SDL/dialogs/sdl_input_widgets.hpp b/client/SDL/dialogs/sdl_input_widgets.hpp
new file mode 100644
index 0000000..83568ba
--- /dev/null
+++ b/client/SDL/dialogs/sdl_input_widgets.hpp
@@ -0,0 +1,44 @@
+#pragma once
+
+#include <string>
+#include <vector>
+#include <SDL.h>
+
+#include "sdl_input.hpp"
+#include "sdl_buttons.hpp"
+
+class SdlInputWidgetList
+{
+ public:
+ SdlInputWidgetList(const std::string& title, const std::vector<std::string>& labels,
+ const std::vector<std::string>& initial, const std::vector<Uint32>& flags);
+ virtual ~SdlInputWidgetList();
+
+ int run(std::vector<std::string>& result);
+
+ protected:
+ bool update(SDL_Renderer* renderer);
+ ssize_t get_index(const SDL_MouseButtonEvent& button);
+
+ private:
+ SdlInputWidgetList(const SdlInputWidgetList& other) = delete;
+ SdlInputWidgetList(SdlInputWidgetList&& other) = delete;
+
+ private:
+ enum
+ {
+ INPUT_BUTTON_ACCEPT = 1,
+ INPUT_BUTTON_CANCEL = -2
+ };
+
+ private:
+ ssize_t next(ssize_t current);
+ [[nodiscard]] bool valid(ssize_t current) const;
+ SdlInputWidget* get(ssize_t index);
+
+ private:
+ SDL_Window* _window;
+ SDL_Renderer* _renderer;
+ std::vector<SdlInputWidget> _list;
+ SdlButtonList _buttons;
+};
diff --git a/client/SDL/dialogs/sdl_select.cpp b/client/SDL/dialogs/sdl_select.cpp
new file mode 100644
index 0000000..f0e0327
--- /dev/null
+++ b/client/SDL/dialogs/sdl_select.cpp
@@ -0,0 +1,74 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * SDL Client helper dialogs
+ *
+ * Copyright 2023 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <cassert>
+
+#include <string>
+
+#include <SDL.h>
+#include <SDL_ttf.h>
+
+#include "sdl_select.hpp"
+#include "sdl_widget.hpp"
+#include "sdl_button.hpp"
+#include "sdl_buttons.hpp"
+#include "sdl_input_widgets.hpp"
+
+static const SDL_Color labelmouseovercolor = { 0, 0x80, 0, 0x60 };
+static const SDL_Color labelbackgroundcolor = { 0x69, 0x66, 0x63, 0xff };
+static const SDL_Color labelhighlightcolor = { 0xcd, 0xca, 0x35, 0x60 };
+static const SDL_Color labelfontcolor = { 0xd1, 0xcf, 0xcd, 0xff };
+
+SdlSelectWidget::SdlSelectWidget(SDL_Renderer* renderer, const std::string& label,
+ const SDL_Rect& rect)
+ : SdlWidget(renderer, rect, true), _text(label), _mouseover(false), _highlight(false)
+{
+ update_text(renderer);
+}
+
+SdlSelectWidget::SdlSelectWidget(SdlSelectWidget&& other) noexcept
+ : SdlWidget(std::move(other)), _text(std::move(other._text)), _mouseover(other._mouseover),
+ _highlight(other._highlight)
+{
+}
+
+bool SdlSelectWidget::set_mouseover(SDL_Renderer* renderer, bool mouseOver)
+{
+ _mouseover = mouseOver;
+ return update_text(renderer);
+}
+
+bool SdlSelectWidget::set_highlight(SDL_Renderer* renderer, bool highlight)
+{
+ _highlight = highlight;
+ return update_text(renderer);
+}
+
+bool SdlSelectWidget::update_text(SDL_Renderer* renderer)
+{
+ assert(renderer);
+ std::vector<SDL_Color> colors = { labelbackgroundcolor };
+ if (_highlight)
+ colors.push_back(labelhighlightcolor);
+ if (_mouseover)
+ colors.push_back(labelmouseovercolor);
+ if (!fill(renderer, colors))
+ return false;
+ return SdlWidget::update_text(renderer, _text, labelfontcolor);
+}
diff --git a/client/SDL/dialogs/sdl_select.hpp b/client/SDL/dialogs/sdl_select.hpp
new file mode 100644
index 0000000..af67b74
--- /dev/null
+++ b/client/SDL/dialogs/sdl_select.hpp
@@ -0,0 +1,46 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * SDL Client helper dialogs
+ *
+ * Copyright 2023 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <string>
+#include <vector>
+
+#include <SDL.h>
+#include "sdl_widget.hpp"
+
+class SdlSelectWidget : public SdlWidget
+{
+ public:
+ SdlSelectWidget(SDL_Renderer* renderer, const std::string& label, const SDL_Rect& rect);
+ SdlSelectWidget(SdlSelectWidget&& other) noexcept;
+ ~SdlSelectWidget() override = default;
+
+ bool set_mouseover(SDL_Renderer* renderer, bool mouseOver);
+ bool set_highlight(SDL_Renderer* renderer, bool highlight);
+ bool update_text(SDL_Renderer* renderer);
+
+ private:
+ SdlSelectWidget(const SdlSelectWidget& other) = delete;
+
+ private:
+ std::string _text;
+ bool _mouseover;
+ bool _highlight;
+};
diff --git a/client/SDL/dialogs/sdl_selectlist.cpp b/client/SDL/dialogs/sdl_selectlist.cpp
new file mode 100644
index 0000000..20437cc
--- /dev/null
+++ b/client/SDL/dialogs/sdl_selectlist.cpp
@@ -0,0 +1,208 @@
+#include "sdl_selectlist.hpp"
+
+static const Uint32 vpadding = 5;
+
+SdlSelectList::SdlSelectList(const std::string& title, const std::vector<std::string>& labels)
+ : _window(nullptr), _renderer(nullptr)
+{
+ const size_t widget_height = 50;
+ const size_t widget_width = 600;
+
+ const size_t total_height = labels.size() * (widget_height + vpadding) + vpadding;
+ _window = SDL_CreateWindow(title.c_str(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
+ widget_width, total_height + widget_height,
+ SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_MOUSE_FOCUS |
+ SDL_WINDOW_INPUT_FOCUS);
+ if (_window == nullptr)
+ {
+ widget_log_error(-1, "SDL_CreateWindow");
+ }
+ else
+ {
+ _renderer = SDL_CreateRenderer(_window, -1, SDL_RENDERER_ACCELERATED);
+ if (_renderer == nullptr)
+ {
+ widget_log_error(-1, "SDL_CreateRenderer");
+ }
+ else
+ {
+ SDL_Rect rect = { 0, 0, widget_width, widget_height };
+ for (auto& label : labels)
+ {
+ _list.emplace_back(_renderer, label, rect);
+ rect.y += widget_height + vpadding;
+ }
+
+ const std::vector<int> buttonids = { INPUT_BUTTON_ACCEPT, INPUT_BUTTON_CANCEL };
+ const std::vector<std::string> buttonlabels = { "accept", "cancel" };
+ _buttons.populate(
+ _renderer, buttonlabels, buttonids, widget_width, static_cast<Sint32>(total_height),
+ static_cast<Sint32>(widget_width / 2), static_cast<Sint32>(widget_height));
+ _buttons.set_highlight(0);
+ }
+ }
+}
+
+SdlSelectList::~SdlSelectList()
+{
+ _list.clear();
+ _buttons.clear();
+ SDL_DestroyRenderer(_renderer);
+ SDL_DestroyWindow(_window);
+}
+
+int SdlSelectList::run()
+{
+ int res = -2;
+ ssize_t CurrentActiveTextInput = 0;
+ bool running = true;
+
+ if (!_window || !_renderer)
+ return -2;
+ try
+ {
+ while (running)
+ {
+ if (!clear_window(_renderer))
+ throw;
+
+ if (!update_text())
+ throw;
+
+ if (!_buttons.update(_renderer))
+ throw;
+
+ SDL_Event event = { 0 };
+ SDL_WaitEvent(&event);
+ switch (event.type)
+ {
+ case SDL_KEYDOWN:
+ switch (event.key.keysym.sym)
+ {
+ case SDLK_UP:
+ case SDLK_BACKSPACE:
+ if (CurrentActiveTextInput > 0)
+ CurrentActiveTextInput--;
+ else
+ CurrentActiveTextInput = _list.size() - 1;
+ break;
+ case SDLK_DOWN:
+ case SDLK_TAB:
+ if (CurrentActiveTextInput < 0)
+ CurrentActiveTextInput = 0;
+ else
+ CurrentActiveTextInput++;
+ CurrentActiveTextInput = CurrentActiveTextInput % _list.size();
+ break;
+ case SDLK_RETURN:
+ case SDLK_RETURN2:
+ case SDLK_KP_ENTER:
+ running = false;
+ res = CurrentActiveTextInput;
+ break;
+ case SDLK_ESCAPE:
+ running = false;
+ res = INPUT_BUTTON_CANCEL;
+ break;
+ default:
+ break;
+ }
+ break;
+ case SDL_MOUSEMOTION:
+ {
+ ssize_t TextInputIndex = get_index(event.button);
+ reset_mouseover();
+ if (TextInputIndex >= 0)
+ {
+ auto& cur = _list[TextInputIndex];
+ if (!cur.set_mouseover(_renderer, true))
+ throw;
+ }
+
+ _buttons.set_mouseover(event.button.x, event.button.y);
+ }
+ break;
+ case SDL_MOUSEBUTTONDOWN:
+ {
+ auto button = _buttons.get_selected(event.button);
+ if (button)
+ {
+ running = false;
+ if (button->id() == INPUT_BUTTON_CANCEL)
+ res = INPUT_BUTTON_CANCEL;
+ else
+ res = CurrentActiveTextInput;
+ }
+ else
+ {
+ CurrentActiveTextInput = get_index(event.button);
+ }
+ }
+ break;
+ case SDL_QUIT:
+ res = INPUT_BUTTON_CANCEL;
+ running = false;
+ break;
+ default:
+ break;
+ }
+
+ reset_highlight();
+ if (CurrentActiveTextInput >= 0)
+ {
+ auto& cur = _list[CurrentActiveTextInput];
+ if (!cur.set_highlight(_renderer, true))
+ throw;
+ }
+
+ SDL_RenderPresent(_renderer);
+ }
+ }
+ catch (...)
+ {
+ return -1;
+ }
+ return res;
+}
+
+ssize_t SdlSelectList::get_index(const SDL_MouseButtonEvent& button)
+{
+ const Sint32 x = button.x;
+ const Sint32 y = button.y;
+ for (size_t i = 0; i < _list.size(); i++)
+ {
+ auto& cur = _list[i];
+ auto r = cur.rect();
+
+ if ((x >= r.x) && (x <= r.x + r.w) && (y >= r.y) && (y <= r.y + r.h))
+ return i;
+ }
+ return -1;
+}
+
+bool SdlSelectList::update_text()
+{
+ for (auto& cur : _list)
+ {
+ if (!cur.update_text(_renderer))
+ return false;
+ }
+
+ return true;
+}
+
+void SdlSelectList::reset_mouseover()
+{
+ for (auto& cur : _list)
+ {
+ cur.set_mouseover(_renderer, false);
+ }
+}
+
+void SdlSelectList::reset_highlight()
+{
+ for (auto& cur : _list)
+ {
+ cur.set_highlight(_renderer, false);
+ }
+}
diff --git a/client/SDL/dialogs/sdl_selectlist.hpp b/client/SDL/dialogs/sdl_selectlist.hpp
new file mode 100644
index 0000000..3da0e14
--- /dev/null
+++ b/client/SDL/dialogs/sdl_selectlist.hpp
@@ -0,0 +1,42 @@
+#pragma once
+
+#include <string>
+#include <vector>
+
+#include <SDL.h>
+
+#include "sdl_select.hpp"
+#include "sdl_button.hpp"
+#include "sdl_buttons.hpp"
+
+class SdlSelectList
+{
+ public:
+ SdlSelectList(const std::string& title, const std::vector<std::string>& labels);
+ virtual ~SdlSelectList();
+
+ int run();
+
+ private:
+ SdlSelectList(const SdlSelectList& other) = delete;
+ SdlSelectList(SdlSelectList&& other) = delete;
+
+ private:
+ enum
+ {
+ INPUT_BUTTON_ACCEPT = 0,
+ INPUT_BUTTON_CANCEL = -2
+ };
+
+ private:
+ ssize_t get_index(const SDL_MouseButtonEvent& button);
+ bool update_text();
+ void reset_mouseover();
+ void reset_highlight();
+
+ private:
+ SDL_Window* _window;
+ SDL_Renderer* _renderer;
+ std::vector<SdlSelectWidget> _list;
+ SdlButtonList _buttons;
+};
diff --git a/client/SDL/dialogs/sdl_widget.cpp b/client/SDL/dialogs/sdl_widget.cpp
new file mode 100644
index 0000000..6e11b5a
--- /dev/null
+++ b/client/SDL/dialogs/sdl_widget.cpp
@@ -0,0 +1,280 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * SDL Client helper dialogs
+ *
+ * Copyright 2023 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <cassert>
+#include <cstdio>
+#include <cstdlib>
+
+#include <SDL.h>
+#include <SDL_ttf.h>
+
+#include "sdl_widget.hpp"
+#include "../sdl_utils.hpp"
+
+#include "res/sdl_resource_manager.hpp"
+
+#include <freerdp/log.h>
+
+#if defined(WITH_SDL_IMAGE_DIALOGS)
+#include <SDL_image.h>
+#endif
+
+#define TAG CLIENT_TAG("SDL.widget")
+
+static const SDL_Color backgroundcolor = { 0x38, 0x36, 0x35, 0xff };
+
+static const Uint32 hpadding = 10;
+
+SdlWidget::SdlWidget(SDL_Renderer* renderer, const SDL_Rect& rect, bool input)
+ : _rect(rect), _input(input)
+{
+ assert(renderer);
+
+ auto ops = SDLResourceManager::get(SDLResourceManager::typeFonts(),
+ "OpenSans-VariableFont_wdth,wght.ttf");
+ if (!ops)
+ widget_log_error(-1, "SDLResourceManager::get");
+ else
+ {
+ _font = TTF_OpenFontRW(ops, 1, 64);
+ if (!_font)
+ widget_log_error(-1, "TTF_OpenFontRW");
+ }
+}
+
+#if defined(WITH_SDL_IMAGE_DIALOGS)
+SdlWidget::SdlWidget(SDL_Renderer* renderer, const SDL_Rect& rect, SDL_RWops* ops) : _rect(rect)
+{
+ if (ops)
+ {
+ _image = IMG_LoadTexture_RW(renderer, ops, 1);
+ if (!_image)
+ widget_log_error(-1, "IMG_LoadTextureTyped_RW");
+ }
+}
+#endif
+
+SdlWidget::SdlWidget(SdlWidget&& other) noexcept
+ : _font(std::move(other._font)), _image(other._image), _rect(std::move(other._rect)),
+ _input(other._input), _wrap(other._wrap), _text_width(other._text_width)
+{
+ other._font = nullptr;
+ other._image = nullptr;
+}
+
+SDL_Texture* SdlWidget::render_text(SDL_Renderer* renderer, const std::string& text,
+ SDL_Color fgcolor, SDL_Rect& src, SDL_Rect& dst)
+{
+ auto surface = TTF_RenderUTF8_Blended(_font, text.c_str(), fgcolor);
+ if (!surface)
+ {
+ widget_log_error(-1, "TTF_RenderText_Blended");
+ return nullptr;
+ }
+
+ auto texture = SDL_CreateTextureFromSurface(renderer, surface);
+ SDL_FreeSurface(surface);
+ if (!texture)
+ {
+ widget_log_error(-1, "SDL_CreateTextureFromSurface");
+ return nullptr;
+ }
+
+ TTF_SizeUTF8(_font, text.c_str(), &src.w, &src.h);
+
+ /* Do some magic:
+ * - Add padding before and after text
+ * - if text is too long only show the last elements
+ * - if text is too short only update used space
+ */
+ dst = _rect;
+ dst.x += hpadding;
+ dst.w -= 2 * hpadding;
+ const float scale = static_cast<float>(dst.h) / static_cast<float>(src.h);
+ const float sws = static_cast<float>(src.w) * scale;
+ const float dws = static_cast<float>(dst.w) / scale;
+ if (static_cast<float>(dst.w) > sws)
+ dst.w = static_cast<int>(sws);
+ if (static_cast<float>(src.w) > dws)
+ {
+ src.x = src.w - static_cast<int>(dws);
+ src.w = static_cast<int>(dws);
+ }
+ return texture;
+}
+
+SDL_Texture* SdlWidget::render_text_wrapped(SDL_Renderer* renderer, const std::string& text,
+ SDL_Color fgcolor, SDL_Rect& src, SDL_Rect& dst)
+{
+ Sint32 w = 0;
+ Sint32 h = 0;
+ TTF_SizeUTF8(_font, " ", &w, &h);
+ auto surface = TTF_RenderUTF8_Blended_Wrapped(_font, text.c_str(), fgcolor, _text_width);
+ if (!surface)
+ {
+ widget_log_error(-1, "TTF_RenderText_Blended");
+ return nullptr;
+ }
+
+ src.w = surface->w;
+ src.h = surface->h;
+
+ auto texture = SDL_CreateTextureFromSurface(renderer, surface);
+ SDL_FreeSurface(surface);
+ if (!texture)
+ {
+ widget_log_error(-1, "SDL_CreateTextureFromSurface");
+ return nullptr;
+ }
+
+ /* Do some magic:
+ * - Add padding before and after text
+ * - if text is too long only show the last elements
+ * - if text is too short only update used space
+ */
+ dst = _rect;
+ dst.x += hpadding;
+ dst.w -= 2 * hpadding;
+ const float scale = static_cast<float>(src.h) / static_cast<float>(src.w);
+ auto dh = src.h * scale;
+ if (dh < dst.h)
+ dst.h = dh;
+
+ return texture;
+}
+
+SdlWidget::~SdlWidget()
+{
+ TTF_CloseFont(_font);
+ if (_image)
+ SDL_DestroyTexture(_image);
+}
+
+bool SdlWidget::error_ex(Uint32 res, const char* what, const char* file, size_t line,
+ const char* fkt)
+{
+ static wLog* log = nullptr;
+ if (!log)
+ log = WLog_Get(TAG);
+ return sdl_log_error_ex(res, log, what, file, line, fkt);
+}
+
+static bool draw_rect(SDL_Renderer* renderer, const SDL_Rect* rect, SDL_Color color)
+{
+ const int drc = SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
+ if (widget_log_error(drc, "SDL_SetRenderDrawColor"))
+ return false;
+
+ const int rc = SDL_RenderFillRect(renderer, rect);
+ return !widget_log_error(rc, "SDL_RenderFillRect");
+}
+
+bool SdlWidget::fill(SDL_Renderer* renderer, SDL_Color color)
+{
+ std::vector<SDL_Color> colors = { color };
+ return fill(renderer, colors);
+}
+
+bool SdlWidget::fill(SDL_Renderer* renderer, const std::vector<SDL_Color>& colors)
+{
+ assert(renderer);
+ SDL_BlendMode mode = SDL_BLENDMODE_INVALID;
+ SDL_GetRenderDrawBlendMode(renderer, &mode);
+ SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE);
+ for (auto color : colors)
+ {
+ draw_rect(renderer, &_rect, color);
+ SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_ADD);
+ }
+ SDL_SetRenderDrawBlendMode(renderer, mode);
+ return true;
+}
+
+bool SdlWidget::update_text(SDL_Renderer* renderer, const std::string& text, SDL_Color fgcolor,
+ SDL_Color bgcolor)
+{
+ assert(renderer);
+
+ if (!fill(renderer, bgcolor))
+ return false;
+ return update_text(renderer, text, fgcolor);
+}
+
+bool SdlWidget::wrap() const
+{
+ return _wrap;
+}
+
+bool SdlWidget::set_wrap(bool wrap, size_t width)
+{
+ _wrap = wrap;
+ _text_width = width;
+ return _wrap;
+}
+
+const SDL_Rect& SdlWidget::rect() const
+{
+ return _rect;
+}
+
+bool SdlWidget::update_text(SDL_Renderer* renderer, const std::string& text, SDL_Color fgcolor)
+{
+
+ if (text.empty())
+ return true;
+
+ SDL_Rect src{};
+ SDL_Rect dst{};
+
+ SDL_Texture* texture = nullptr;
+ if (_image)
+ {
+ texture = _image;
+ dst = _rect;
+ auto rc = SDL_QueryTexture(_image, nullptr, nullptr, &src.w, &src.h);
+ if (rc < 0)
+ widget_log_error(rc, "SDL_QueryTexture");
+ }
+ else if (_wrap)
+ texture = render_text_wrapped(renderer, text, fgcolor, src, dst);
+ else
+ texture = render_text(renderer, text, fgcolor, src, dst);
+ if (!texture)
+ return false;
+
+ const int rc = SDL_RenderCopy(renderer, texture, &src, &dst);
+ if (!_image)
+ SDL_DestroyTexture(texture);
+ if (rc < 0)
+ return !widget_log_error(rc, "SDL_RenderCopy");
+ return true;
+}
+
+bool clear_window(SDL_Renderer* renderer)
+{
+ assert(renderer);
+
+ const int drc = SDL_SetRenderDrawColor(renderer, backgroundcolor.r, backgroundcolor.g,
+ backgroundcolor.b, backgroundcolor.a);
+ if (widget_log_error(drc, "SDL_SetRenderDrawColor"))
+ return false;
+
+ const int rcls = SDL_RenderClear(renderer);
+ return !widget_log_error(rcls, "SDL_RenderClear");
+}
diff --git a/client/SDL/dialogs/sdl_widget.hpp b/client/SDL/dialogs/sdl_widget.hpp
new file mode 100644
index 0000000..ebc7dbd
--- /dev/null
+++ b/client/SDL/dialogs/sdl_widget.hpp
@@ -0,0 +1,88 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * SDL Client helper dialogs
+ *
+ * Copyright 2023 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <string>
+
+#include <vector>
+#include <SDL.h>
+#include <SDL_ttf.h>
+
+#if defined(_MSC_VER)
+#include <BaseTsd.h>
+typedef SSIZE_T ssize_t;
+#endif
+
+#if !defined(HAS_NOEXCEPT)
+#if defined(__clang__)
+#if __has_feature(cxx_noexcept)
+#define HAS_NOEXCEPT
+#endif
+#elif defined(__GXX_EXPERIMENTAL_CXX0X__) && __GNUC__ * 10 + __GNUC_MINOR__ >= 46 || \
+ defined(_MSC_FULL_VER) && _MSC_FULL_VER >= 190023026
+#define HAS_NOEXCEPT
+#endif
+#endif
+
+#ifndef HAS_NOEXCEPT
+#define noexcept
+#endif
+
+class SdlWidget
+{
+ public:
+ SdlWidget(SDL_Renderer* renderer, const SDL_Rect& rect, bool input);
+ SdlWidget(SDL_Renderer* renderer, const SDL_Rect& rect, SDL_RWops* ops);
+ SdlWidget(SdlWidget&& other) noexcept;
+ virtual ~SdlWidget();
+
+ bool fill(SDL_Renderer* renderer, SDL_Color color);
+ bool fill(SDL_Renderer* renderer, const std::vector<SDL_Color>& colors);
+ bool update_text(SDL_Renderer* renderer, const std::string& text, SDL_Color fgcolor);
+ bool update_text(SDL_Renderer* renderer, const std::string& text, SDL_Color fgcolor,
+ SDL_Color bgcolor);
+
+ bool wrap() const;
+ bool set_wrap(bool wrap = true, size_t width = 0);
+ const SDL_Rect& rect() const;
+
+ public:
+#define widget_log_error(res, what) SdlWidget::error_ex(res, what, __FILE__, __LINE__, __func__)
+ static bool error_ex(Uint32 res, const char* what, const char* file, size_t line,
+ const char* fkt);
+
+ private:
+ SdlWidget(const SdlWidget& other) = delete;
+
+ SDL_Texture* render_text(SDL_Renderer* renderer, const std::string& text, SDL_Color fgcolor,
+ SDL_Rect& src, SDL_Rect& dst);
+ SDL_Texture* render_text_wrapped(SDL_Renderer* renderer, const std::string& text,
+ SDL_Color fgcolor, SDL_Rect& src, SDL_Rect& dst);
+
+ private:
+ TTF_Font* _font = nullptr;
+ SDL_Texture* _image = nullptr;
+ SDL_Rect _rect;
+ bool _input = false;
+ bool _wrap = false;
+ size_t _text_width = 0;
+};
+
+bool clear_window(SDL_Renderer* renderer);
diff --git a/client/SDL/dialogs/test/CMakeLists.txt b/client/SDL/dialogs/test/CMakeLists.txt
new file mode 100644
index 0000000..c1003d4
--- /dev/null
+++ b/client/SDL/dialogs/test/CMakeLists.txt
@@ -0,0 +1,30 @@
+set(MODULE_NAME "TestSDL")
+set(MODULE_PREFIX "TEST_SDL")
+
+set(DRIVER ${MODULE_NAME}.cpp)
+
+set(TEST_SRCS
+ TestSDLDialogs.cpp
+)
+
+create_test_sourcelist(SRCS
+ ${DRIVER}
+ ${TEST_SRCS})
+
+add_library(${MODULE_NAME} ${SRCS})
+
+list(APPEND LIBS
+ dialogs
+)
+
+target_link_libraries(${MODULE_NAME} ${LIBS})
+
+set_target_properties(${MODULE_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${TESTING_OUTPUT_DIRECTORY}")
+
+foreach(test ${TESTS})
+ get_filename_component(TestName ${test} NAME_WE)
+ add_test(${TestName} ${TESTING_OUTPUT_DIRECTORY}/${MODULE_NAME} ${TestName})
+endforeach()
+
+set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "FreeRDP/Client/Test")
+
diff --git a/client/SDL/dialogs/test/TestSDLDialogs.cpp b/client/SDL/dialogs/test/TestSDLDialogs.cpp
new file mode 100644
index 0000000..558fb4c
--- /dev/null
+++ b/client/SDL/dialogs/test/TestSDLDialogs.cpp
@@ -0,0 +1,99 @@
+#include "../sdl_selectlist.hpp"
+#include "../sdl_input_widgets.hpp"
+
+#include <freerdp/api.h>
+#include <winpr/wlog.h>
+
+BOOL sdl_log_error_ex(Uint32 res, wLog* log, const char* what, const char* file, size_t line,
+ const char* fkt)
+{
+ return FALSE;
+}
+
+static bool test_input_dialog()
+{
+ const auto title = "sometitle";
+ std::vector<std::string> labels;
+ std::vector<std::string> initial;
+ std::vector<Uint32> flags;
+ for (size_t x = 0; x < 12; x++)
+ {
+ labels.push_back("label" + std::to_string(x));
+ initial.push_back(std::to_string(x));
+
+ Uint32 flag = 0;
+ if ((x % 2) != 0)
+ flag |= SdlInputWidget::SDL_INPUT_MASK;
+ if ((x % 3) == 0)
+ flag |= SdlInputWidget::SDL_INPUT_READONLY;
+
+ flags.push_back(flag);
+ }
+ SdlInputWidgetList list{ title, labels, initial, flags };
+ std::vector<std::string> result;
+ auto rc = list.run(result);
+ if (rc < 0)
+ {
+ return false;
+ }
+ if (result.size() != labels.size())
+ {
+ return false;
+ }
+ return true;
+}
+
+static bool test_select_dialog()
+{
+ const auto title = "sometitle";
+ std::vector<std::string> labels;
+ for (size_t x = 0; x < 12; x++)
+ {
+ labels.push_back("label" + std::to_string(x));
+ }
+ SdlSelectList list{ title, labels };
+ auto rc = list.run();
+ if (rc < 0)
+ {
+ return false;
+ }
+ if (static_cast<size_t>(rc) >= labels.size())
+ return false;
+
+ return true;
+}
+
+extern "C"
+{
+ FREERDP_API int TestSDLDialogs(int argc, char* argv[]);
+}
+
+int TestSDLDialogs(int argc, char* argv[])
+{
+ int rc = 0;
+
+ (void)argc;
+ (void)argv;
+
+#if 0
+ SDL_Init(SDL_INIT_VIDEO);
+ try
+ {
+#if 1
+ if (!test_input_dialog())
+ throw -1;
+#endif
+#if 1
+ if (!test_select_dialog())
+ throw -2;
+#endif
+ }
+ catch (int e)
+ {
+ rc = e;
+ }
+ SDL_Quit();
+
+#endif
+ return rc;
+}
diff --git a/client/SDL/man/CMakeLists.txt b/client/SDL/man/CMakeLists.txt
new file mode 100644
index 0000000..1fb2adc
--- /dev/null
+++ b/client/SDL/man/CMakeLists.txt
@@ -0,0 +1,12 @@
+set(DEPS
+ sdl-freerdp-channels.1.xml
+ sdl-freerdp-config.1.xml
+ sdl-freerdp-examples.1.xml
+ sdl-freerdp-envvar.1.xml
+ )
+
+set(MANPAGE_NAME ${PROJECT_NAME})
+if (WITH_BINARY_VERSIONING)
+ set(MANPAGE_NAME ${PROJECT_NAME}${PROJECT_VERSION_MAJOR})
+endif()
+generate_and_install_freerdp_man_from_xml(${PROJECT_NAME}.1 ${MANPAGE_NAME}.1 "${DEPS}")
diff --git a/client/SDL/man/sdl-freerdp-config.1.xml.in b/client/SDL/man/sdl-freerdp-config.1.xml.in
new file mode 100644
index 0000000..3bace73
--- /dev/null
+++ b/client/SDL/man/sdl-freerdp-config.1.xml.in
@@ -0,0 +1,81 @@
+<refsect1>
+ <title>Configuration file</title>
+
+ <variablelist>
+ <varlistentry>
+ <term>Format and Location:</term>
+ <listitem>
+ <para>The configuration file is stored per user.<sbr/>
+ The <replaceable>XDG_CONFIG_HOME</replaceable> environment variable can be used to override the base directory.<sbr/>
+ This defaults to <replaceable>~/.config</replaceable>
+ The location relative to <replaceable>XDG_CONFIG_HOME</replaceable> is <replaceable>$XDG_CONFIG_HOME/@VENDOR@/@PRODUCT@/@PROJECT_NAME@.json</replaceable><sbr/>
+ The configuration is stored in JSON format</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>Supported options:</term>
+ <listitem>
+ <varlistentry>
+ <term><replaceable>SDL_KeyModMask</replaceable></term>
+ <listitem>
+ <varlistentry>
+ <listitem>
+ <para>Defines the key combination required for SDL client shortcuts.<sbr/>
+ Default <replaceable>KMOD_RSHIFT</replaceable><sbr/>
+ An array of <replaceable>SDL_Keymod</replaceable> strings as defined at <replaceable>https://wiki.libsdl.org/SDL2/SDL_Keymod</replaceable></para>
+ </listitem>
+ </varlistentry>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><replaceable>SDL_Fullscreen</replaceable></term>
+ <listitem>
+ <varlistentry>
+ <listitem>
+ <para>Toggles client fullscreen state.<sbr/>
+ Default <replaceable>SDL_SCANCODE_RETURN</replaceable>.<sbr/>
+ A string as defined at <replaceable>https://wiki.libsdl.org/SDL2/SDLScancodeLookup</replaceable></para>
+ </listitem>
+ </varlistentry>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><replaceable>SDL_Resizeable</replaceable></term>
+ <listitem>
+ <varlistentry>
+ <listitem>
+ <para>Toggles local window resizeable state.<sbr/>
+ Default <replaceable>SDL_SCANCODE_R</replaceable>.<sbr/>
+ A string as defined at <replaceable>https://wiki.libsdl.org/SDL2/SDLScancodeLookup</replaceable></para>
+ </listitem>
+ </varlistentry>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><replaceable>SDL_Grab</replaceable></term>
+ <listitem>
+ <varlistentry>
+ <listitem>
+ <para>Toggles keyboard and mouse grab state.<sbr/>
+ Default <replaceable>SDL_SCANCODE_G</replaceable>.<sbr/>
+ A string as defined at <replaceable>https://wiki.libsdl.org/SDL2/SDLScancodeLookup</replaceable></para>
+ </listitem>
+ </varlistentry>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><replaceable>SDL_Disconnect</replaceable></term>
+ <listitem>
+ <varlistentry>
+ <listitem>
+ <para>Disconnects from the RDP session.<sbr/>
+ Default <replaceable>SDL_SCANCODE_D</replaceable>.<sbr/>
+ A string as defined at <replaceable>https://wiki.libsdl.org/SDL2/SDLScancodeLookup</replaceable></para>
+ </listitem>
+ </varlistentry>
+ </listitem>
+ </varlistentry>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+</refsect1>
diff --git a/client/SDL/man/sdl-freerdp-envvar.1.xml.in b/client/SDL/man/sdl-freerdp-envvar.1.xml.in
new file mode 100644
index 0000000..ab6c8c5
--- /dev/null
+++ b/client/SDL/man/sdl-freerdp-envvar.1.xml.in
@@ -0,0 +1,15 @@
+<refsect1>
+ <title>Environment variables</title>
+
+ <variablelist>
+ <varlistentry>
+ <term>wlog environment variable</term>
+ <listitem>
+ <para>sdl-freerdp uses wLog as its log facility, you can refer to the
+ corresponding man page (wlog(7)) for more informations. Arguments passed
+ via the <replaceable>/log-level</replaceable> or <replaceable>/log-filters</replaceable>
+ have precedence over the environment variables.</para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+</refsect1>
diff --git a/client/SDL/man/sdl-freerdp-examples.1.xml.in b/client/SDL/man/sdl-freerdp-examples.1.xml.in
new file mode 100644
index 0000000..7b0f873
--- /dev/null
+++ b/client/SDL/man/sdl-freerdp-examples.1.xml.in
@@ -0,0 +1,95 @@
+<refsect1>
+ <title>Examples</title>
+ <variablelist>
+ <varlistentry>
+ <term><command>sdl-freerdp connection.rdp /p:Pwd123! /f</command></term>
+ <listitem>
+ <para>Connect in fullscreen mode using a stored configuration <replaceable>connection.rdp</replaceable> and the password <replaceable>Pwd123!</replaceable></para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><command>sdl-freerdp /u:USER /size:50%h /v:rdp.contoso.com</command></term>
+ <listitem>
+ <para>Connect to host <replaceable>rdp.contoso.com</replaceable> with user <replaceable>USER</replaceable> and a size of <replaceable>50 percent of the height</replaceable>. If width (w) is set instead of height (h) like /size:50%w. 50 percent of the width is used.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><command>sdl-freerdp /u:CONTOSO\\JohnDoe /p:Pwd123! /v:rdp.contoso.com</command></term>
+ <listitem>
+ <para>Connect to host <replaceable>rdp.contoso.com</replaceable> with user <replaceable>CONTOSO\\JohnDoe</replaceable> and password <replaceable>Pwd123!</replaceable></para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><command>sdl-freerdp /u:JohnDoe /p:Pwd123! /w:1366 /h:768 /v:192.168.1.100:4489</command></term>
+ <listitem>
+ <para>Connect to host <replaceable>192.168.1.100</replaceable> on port <replaceable>4489</replaceable> with user <replaceable>JohnDoe</replaceable>, password <replaceable>Pwd123!</replaceable>. The screen width is set to <replaceable>1366</replaceable> and the height to <replaceable>768</replaceable></para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><command>sdl-freerdp /u:JohnDoe /p:Pwd123! /vmconnect:C824F53E-95D2-46C6-9A18-23A5BB403532 /v:192.168.1.100</command></term>
+ <listitem>
+ <para>Establish a connection to host <replaceable>192.168.1.100</replaceable> with user <replaceable>JohnDoe</replaceable>, password <replaceable>Pwd123!</replaceable> and connect to Hyper-V console (use port 2179, disable negotiation) with VMID <replaceable>C824F53E-95D2-46C6-9A18-23A5BB403532</replaceable></para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><command>+clipboard</command></term>
+ <listitem>
+ <para>Activate clipboard redirection</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><command>/drive:home,/home/user</command></term>
+ <listitem>
+ <para>Activate drive redirection of <replaceable>/home/user</replaceable> as home drive</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><command>/smartcard:&lt;device&gt;</command></term>
+ <listitem>
+ <para>Activate smartcard redirection for device <replaceable>device</replaceable></para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><command>/printer:&lt;device&gt;,&lt;driver&gt;</command></term>
+ <listitem>
+ <para>Activate printer redirection for printer <replaceable>device</replaceable> using driver <replaceable>driver</replaceable></para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><command>/serial:&lt;device&gt;</command></term>
+ <listitem>
+ <para>Activate serial port redirection for port <replaceable>device</replaceable></para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><command>/parallel:&lt;device&gt;</command></term>
+ <listitem>
+ <para>Activate parallel port redirection for port <replaceable>device</replaceable></para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><command>/sound:sys:alsa</command></term>
+ <listitem>
+ <para>Activate audio output redirection using device <replaceable>sys:alsa</replaceable></para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><command>/microphone:sys:alsa</command></term>
+ <listitem>
+ <para>Activate audio input redirection using device <replaceable>sys:alsa</replaceable></para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><command>/multimedia:sys:alsa</command></term>
+ <listitem>
+ <para>Activate multimedia redirection using device <replaceable>sys:alsa</replaceable></para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><command>/usb:id,dev:054c:0268</command></term>
+ <listitem>
+ <para>Activate USB device redirection for the device identified by <replaceable>054c:0268</replaceable></para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+</refsect1>
diff --git a/client/SDL/man/sdl-freerdp.1.xml.in b/client/SDL/man/sdl-freerdp.1.xml.in
new file mode 100644
index 0000000..c4b9918
--- /dev/null
+++ b/client/SDL/man/sdl-freerdp.1.xml.in
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE refentry
+PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
+"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd" [
+ <!ENTITY syntax SYSTEM "freerdp-argument.1.xml">
+ <!ENTITY channels SYSTEM "sdl-freerdp-channels.1.xml">
+ <!ENTITY config SYSTEM "sdl-freerdp-config.1.xml">
+ <!ENTITY envvar SYSTEM "sdl-freerdp-envvar.1.xml">
+ <!ENTITY examples SYSTEM "sdl-freerdp-examples.1.xml">
+ ]
+>
+
+<refentry>
+ <refentryinfo>
+ <date>@MAN_TODAY@</date>
+ <author>
+ <authorblurb><para>The FreeRDP Team</para></authorblurb>
+ </author>
+ </refentryinfo>
+ <refmeta>
+ <refentrytitle>@MANPAGE_NAME@</refentrytitle>
+ <manvolnum>1</manvolnum>
+ <refmiscinfo class="source">freerdp</refmiscinfo>
+ <refmiscinfo class="manual">@MANPAGE_NAME@</refmiscinfo>
+ </refmeta>
+ <refnamediv>
+ <refname><application>@MANPAGE_NAME@</application></refname>
+ <refpurpose>FreeRDP SDL client</refpurpose>
+ </refnamediv>
+ <refsynopsisdiv>
+ <refsynopsisdivinfo>
+ <date>@MAN_TODAY@</date>
+ </refsynopsisdivinfo>
+ <para>
+ <command>@MANPAGE_NAME@</command> [file] [options] [/v:server[:port]]
+ </para>
+ </refsynopsisdiv>
+ <refsect1>
+ <refsect1info>
+ <date>@MAN_TODAY@</date>
+ </refsect1info>
+ <title>DESCRIPTION</title>
+ <para>
+ <command>@MANPAGE_NAME@</command> is an SDL Remote Desktop Protocol (RDP)
+ client which is part of the FreeRDP project. An RDP server is built-in
+ to many editions of Windows. Alternative servers included ogon, gnome-remote-desktop,
+ xrdp and VRDP (VirtualBox).
+ </para>
+ </refsect1>
+
+ &syntax;
+
+ &channels;
+
+ &config;
+
+ &envvar;
+
+ &examples;
+
+ <refsect1>
+ <title>LINKS</title>
+ <para>
+ <ulink url="http://www.freerdp.com/">http://www.freerdp.com/</ulink>
+ </para>
+ </refsect1>
+</refentry>
diff --git a/client/SDL/sdl_channels.cpp b/client/SDL/sdl_channels.cpp
new file mode 100644
index 0000000..958c5e7
--- /dev/null
+++ b/client/SDL/sdl_channels.cpp
@@ -0,0 +1,83 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * SDL Client Channels
+ *
+ * Copyright 2022 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <freerdp/config.h>
+
+#include <winpr/assert.h>
+
+#include <freerdp/client/rail.h>
+#include <freerdp/client/cliprdr.h>
+#include <freerdp/client/disp.h>
+
+#include "sdl_channels.hpp"
+#include "sdl_freerdp.hpp"
+#include "sdl_disp.hpp"
+
+void sdl_OnChannelConnectedEventHandler(void* context, const ChannelConnectedEventArgs* e)
+{
+ auto sdl = get_context(context);
+
+ WINPR_ASSERT(sdl);
+ WINPR_ASSERT(e);
+
+ if (strcmp(e->name, RAIL_SVC_CHANNEL_NAME) == 0)
+ {
+ }
+ else if (strcmp(e->name, CLIPRDR_SVC_CHANNEL_NAME) == 0)
+ {
+ auto clip = reinterpret_cast<CliprdrClientContext*>(e->pInterface);
+ WINPR_ASSERT(clip);
+ clip->custom = context;
+ }
+ else if (strcmp(e->name, DISP_DVC_CHANNEL_NAME) == 0)
+ {
+ auto disp = reinterpret_cast<DispClientContext*>(e->pInterface);
+ WINPR_ASSERT(disp);
+ sdl->disp.init(disp);
+ }
+ else
+ freerdp_client_OnChannelConnectedEventHandler(context, e);
+}
+
+void sdl_OnChannelDisconnectedEventHandler(void* context, const ChannelDisconnectedEventArgs* e)
+{
+ auto sdl = get_context(context);
+
+ WINPR_ASSERT(sdl);
+ WINPR_ASSERT(e);
+
+ // TODO: Set resizeable depending on disp channel and /dynamic-resolution
+ if (strcmp(e->name, RAIL_SVC_CHANNEL_NAME) == 0)
+ {
+ }
+ else if (strcmp(e->name, CLIPRDR_SVC_CHANNEL_NAME) == 0)
+ {
+ auto clip = reinterpret_cast<CliprdrClientContext*>(e->pInterface);
+ WINPR_ASSERT(clip);
+ clip->custom = nullptr;
+ }
+ else if (strcmp(e->name, DISP_DVC_CHANNEL_NAME) == 0)
+ {
+ auto disp = reinterpret_cast<DispClientContext*>(e->pInterface);
+ WINPR_ASSERT(disp);
+ sdl->disp.uninit(disp);
+ }
+ else
+ freerdp_client_OnChannelDisconnectedEventHandler(context, e);
+}
diff --git a/client/SDL/sdl_channels.hpp b/client/SDL/sdl_channels.hpp
new file mode 100644
index 0000000..a5c9f7d
--- /dev/null
+++ b/client/SDL/sdl_channels.hpp
@@ -0,0 +1,29 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * SDL Client Channels
+ *
+ * Copyright 2022 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <freerdp/freerdp.h>
+#include <freerdp/client/channels.h>
+
+int sdl_on_channel_connected(freerdp* instance, const char* name, void* pInterface);
+int sdl_on_channel_disconnected(freerdp* instance, const char* name, void* pInterface);
+
+void sdl_OnChannelConnectedEventHandler(void* context, const ChannelConnectedEventArgs* e);
+void sdl_OnChannelDisconnectedEventHandler(void* context, const ChannelDisconnectedEventArgs* e);
diff --git a/client/SDL/sdl_disp.cpp b/client/SDL/sdl_disp.cpp
new file mode 100644
index 0000000..ffd13c8
--- /dev/null
+++ b/client/SDL/sdl_disp.cpp
@@ -0,0 +1,471 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * SDL Display Control Channel
+ *
+ * Copyright 2023 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <vector>
+#include <winpr/sysinfo.h>
+#include <winpr/assert.h>
+
+#include <freerdp/gdi/gdi.h>
+
+#include <SDL.h>
+
+#include "sdl_disp.hpp"
+#include "sdl_kbd.hpp"
+#include "sdl_utils.hpp"
+#include "sdl_freerdp.hpp"
+
+#include <freerdp/log.h>
+#define TAG CLIENT_TAG("sdl.disp")
+
+#define RESIZE_MIN_DELAY 200 /* minimum delay in ms between two resizes */
+#define MAX_RETRIES 5
+
+BOOL sdlDispContext::settings_changed()
+{
+ auto settings = _sdl->context()->settings;
+ WINPR_ASSERT(settings);
+
+ if (_lastSentWidth != _targetWidth)
+ return TRUE;
+
+ if (_lastSentHeight != _targetHeight)
+ return TRUE;
+
+ if (_lastSentDesktopOrientation !=
+ freerdp_settings_get_uint16(settings, FreeRDP_DesktopOrientation))
+ return TRUE;
+
+ if (_lastSentDesktopScaleFactor !=
+ freerdp_settings_get_uint32(settings, FreeRDP_DesktopScaleFactor))
+ return TRUE;
+
+ if (_lastSentDeviceScaleFactor !=
+ freerdp_settings_get_uint32(settings, FreeRDP_DeviceScaleFactor))
+ return TRUE;
+ /* TODO
+ if (_fullscreen != _sdl->fullscreen)
+ return TRUE;
+ */
+ return FALSE;
+}
+
+BOOL sdlDispContext::update_last_sent()
+{
+ WINPR_ASSERT(_sdl);
+
+ auto settings = _sdl->context()->settings;
+ WINPR_ASSERT(settings);
+
+ _lastSentWidth = _targetWidth;
+ _lastSentHeight = _targetHeight;
+ _lastSentDesktopOrientation = freerdp_settings_get_uint16(settings, FreeRDP_DesktopOrientation);
+ _lastSentDesktopScaleFactor = freerdp_settings_get_uint32(settings, FreeRDP_DesktopScaleFactor);
+ _lastSentDeviceScaleFactor = freerdp_settings_get_uint32(settings, FreeRDP_DeviceScaleFactor);
+ // TODO _fullscreen = _sdl->fullscreen;
+ return TRUE;
+}
+
+BOOL sdlDispContext::sendResize()
+{
+ DISPLAY_CONTROL_MONITOR_LAYOUT layout = {};
+ auto settings = _sdl->context()->settings;
+
+ if (!settings)
+ return FALSE;
+
+ if (!_activated || !_disp)
+ return TRUE;
+
+ if (GetTickCount64() - _lastSentDate < RESIZE_MIN_DELAY)
+ return TRUE;
+
+ _lastSentDate = GetTickCount64();
+
+ if (!settings_changed())
+ return TRUE;
+
+ const UINT32 mcount = freerdp_settings_get_uint32(settings, FreeRDP_MonitorCount);
+ if (_sdl->fullscreen && (mcount > 0))
+ {
+ auto monitors = static_cast<const rdpMonitor*>(
+ freerdp_settings_get_pointer(settings, FreeRDP_MonitorDefArray));
+ if (sendLayout(monitors, mcount) != CHANNEL_RC_OK)
+ return FALSE;
+ }
+ else
+ {
+ _waitingResize = TRUE;
+ layout.Flags = DISPLAY_CONTROL_MONITOR_PRIMARY;
+ layout.Top = layout.Left = 0;
+ layout.Width = _targetWidth;
+ layout.Height = _targetHeight;
+ layout.Orientation = freerdp_settings_get_uint16(settings, FreeRDP_DesktopOrientation);
+ layout.DesktopScaleFactor =
+ freerdp_settings_get_uint32(settings, FreeRDP_DesktopScaleFactor);
+ layout.DeviceScaleFactor = freerdp_settings_get_uint32(settings, FreeRDP_DeviceScaleFactor);
+ layout.PhysicalWidth = _targetWidth;
+ layout.PhysicalHeight = _targetHeight;
+
+ if (IFCALLRESULT(CHANNEL_RC_OK, _disp->SendMonitorLayout, _disp, 1, &layout) !=
+ CHANNEL_RC_OK)
+ return FALSE;
+ }
+ return update_last_sent();
+}
+
+BOOL sdlDispContext::set_window_resizable()
+{
+ _sdl->update_resizeable(TRUE);
+ return TRUE;
+}
+
+static BOOL sdl_disp_check_context(void* context, SdlContext** ppsdl, sdlDispContext** ppsdlDisp,
+ rdpSettings** ppSettings)
+{
+ if (!context)
+ return FALSE;
+
+ auto sdl = get_context(context);
+
+ if (!sdl->context()->settings)
+ return FALSE;
+
+ *ppsdl = sdl;
+ *ppsdlDisp = &sdl->disp;
+ *ppSettings = sdl->context()->settings;
+ return TRUE;
+}
+
+void sdlDispContext::OnActivated(void* context, const ActivatedEventArgs* e)
+{
+ SdlContext* sdl = nullptr;
+ sdlDispContext* sdlDisp = nullptr;
+ rdpSettings* settings = nullptr;
+
+ if (!sdl_disp_check_context(context, &sdl, &sdlDisp, &settings))
+ return;
+
+ sdlDisp->_waitingResize = FALSE;
+
+ if (sdlDisp->_activated && !freerdp_settings_get_bool(settings, FreeRDP_Fullscreen))
+ {
+ sdlDisp->set_window_resizable();
+
+ if (e->firstActivation)
+ return;
+
+ sdlDisp->addTimer();
+ }
+}
+
+void sdlDispContext::OnGraphicsReset(void* context, const GraphicsResetEventArgs* e)
+{
+ SdlContext* sdl = nullptr;
+ sdlDispContext* sdlDisp = nullptr;
+ rdpSettings* settings = nullptr;
+
+ WINPR_UNUSED(e);
+ if (!sdl_disp_check_context(context, &sdl, &sdlDisp, &settings))
+ return;
+
+ sdlDisp->_waitingResize = FALSE;
+
+ if (sdlDisp->_activated && !freerdp_settings_get_bool(settings, FreeRDP_Fullscreen))
+ {
+ sdlDisp->set_window_resizable();
+ sdlDisp->addTimer();
+ }
+}
+
+Uint32 sdlDispContext::OnTimer(Uint32 interval, void* param)
+{
+ auto ctx = static_cast<sdlDispContext*>(param);
+ if (!ctx)
+ return 0;
+
+ SdlContext* sdl = ctx->_sdl;
+ sdlDispContext* sdlDisp = nullptr;
+ rdpSettings* settings = nullptr;
+
+ if (!sdl_disp_check_context(sdl->context(), &sdl, &sdlDisp, &settings))
+ return 0;
+
+ WLog_Print(sdl->log, WLOG_TRACE, "checking for display changes...");
+ if (!sdlDisp->_activated || freerdp_settings_get_bool(settings, FreeRDP_Fullscreen))
+ return 0;
+ else
+ {
+ auto rc = sdlDisp->sendResize();
+ if (!rc)
+ WLog_Print(sdl->log, WLOG_TRACE, "sent new display layout, result %d", rc);
+ }
+ if (sdlDisp->_timer_retries++ >= MAX_RETRIES)
+ {
+ WLog_Print(sdl->log, WLOG_TRACE, "deactivate timer, retries exceeded");
+ return 0;
+ }
+
+ WLog_Print(sdl->log, WLOG_TRACE, "fire timer one more time");
+ return interval;
+}
+
+UINT sdlDispContext::sendLayout(const rdpMonitor* monitors, size_t nmonitors)
+{
+ UINT ret = CHANNEL_RC_OK;
+
+ WINPR_ASSERT(monitors);
+ WINPR_ASSERT(nmonitors > 0);
+
+ auto settings = _sdl->context()->settings;
+ WINPR_ASSERT(settings);
+
+ std::vector<DISPLAY_CONTROL_MONITOR_LAYOUT> layouts;
+ layouts.resize(nmonitors);
+
+ for (size_t i = 0; i < nmonitors; i++)
+ {
+ auto monitor = &monitors[i];
+ auto layout = &layouts[i];
+
+ layout->Flags = (monitor->is_primary ? DISPLAY_CONTROL_MONITOR_PRIMARY : 0);
+ layout->Left = monitor->x;
+ layout->Top = monitor->y;
+ layout->Width = monitor->width;
+ layout->Height = monitor->height;
+ layout->Orientation = ORIENTATION_LANDSCAPE;
+ layout->PhysicalWidth = monitor->attributes.physicalWidth;
+ layout->PhysicalHeight = monitor->attributes.physicalHeight;
+
+ switch (monitor->attributes.orientation)
+ {
+ case 90:
+ layout->Orientation = ORIENTATION_PORTRAIT;
+ break;
+
+ case 180:
+ layout->Orientation = ORIENTATION_LANDSCAPE_FLIPPED;
+ break;
+
+ case 270:
+ layout->Orientation = ORIENTATION_PORTRAIT_FLIPPED;
+ break;
+
+ case 0:
+ default:
+ /* MS-RDPEDISP - 2.2.2.2.1:
+ * Orientation (4 bytes): A 32-bit unsigned integer that specifies the
+ * orientation of the monitor in degrees. Valid values are 0, 90, 180
+ * or 270
+ *
+ * So we default to ORIENTATION_LANDSCAPE
+ */
+ layout->Orientation = ORIENTATION_LANDSCAPE;
+ break;
+ }
+
+ layout->DesktopScaleFactor =
+ freerdp_settings_get_uint32(settings, FreeRDP_DesktopScaleFactor);
+ layout->DeviceScaleFactor =
+ freerdp_settings_get_uint32(settings, FreeRDP_DeviceScaleFactor);
+ }
+
+ WINPR_ASSERT(_disp);
+ ret = IFCALLRESULT(CHANNEL_RC_OK, _disp->SendMonitorLayout, _disp, layouts.size(),
+ layouts.data());
+ return ret;
+}
+
+BOOL sdlDispContext::addTimer()
+{
+ if (SDL_WasInit(SDL_INIT_TIMER) == 0)
+ return FALSE;
+
+ SDL_RemoveTimer(_timer);
+ WLog_Print(_sdl->log, WLOG_TRACE, "adding new display check timer");
+
+ _timer_retries = 0;
+ sendResize();
+ _timer = SDL_AddTimer(1000, sdlDispContext::OnTimer, this);
+ return TRUE;
+}
+
+#if SDL_VERSION_ATLEAST(2, 0, 10)
+BOOL sdlDispContext::handle_display_event(const SDL_DisplayEvent* ev)
+{
+ WINPR_ASSERT(ev);
+
+ switch (ev->event)
+ {
+#if SDL_VERSION_ATLEAST(2, 0, 14)
+ case SDL_DISPLAYEVENT_CONNECTED:
+ SDL_Log("A new display with id %d was connected", ev->display);
+ return TRUE;
+ case SDL_DISPLAYEVENT_DISCONNECTED:
+ SDL_Log("The display with id %d was disconnected", ev->display);
+ return TRUE;
+#endif
+ case SDL_DISPLAYEVENT_ORIENTATION:
+ SDL_Log("The orientation of display with id %d was changed", ev->display);
+ return TRUE;
+ default:
+ return TRUE;
+ }
+}
+#endif
+
+BOOL sdlDispContext::handle_window_event(const SDL_WindowEvent* ev)
+{
+ WINPR_ASSERT(ev);
+
+ auto bordered = freerdp_settings_get_bool(_sdl->context()->settings, FreeRDP_Decorations)
+ ? SDL_TRUE
+ : SDL_FALSE;
+
+ auto it = _sdl->windows.find(ev->windowID);
+ if (it != _sdl->windows.end())
+ it->second.setBordered(bordered);
+
+ switch (ev->event)
+ {
+ case SDL_WINDOWEVENT_HIDDEN:
+ case SDL_WINDOWEVENT_MINIMIZED:
+ gdi_send_suppress_output(_sdl->context()->gdi, TRUE);
+
+ return TRUE;
+
+ case SDL_WINDOWEVENT_EXPOSED:
+ case SDL_WINDOWEVENT_SHOWN:
+ case SDL_WINDOWEVENT_MAXIMIZED:
+ case SDL_WINDOWEVENT_RESTORED:
+ gdi_send_suppress_output(_sdl->context()->gdi, FALSE);
+ return TRUE;
+
+ case SDL_WINDOWEVENT_RESIZED:
+ case SDL_WINDOWEVENT_SIZE_CHANGED:
+ _targetWidth = ev->data1;
+ _targetHeight = ev->data2;
+ return addTimer();
+
+ case SDL_WINDOWEVENT_LEAVE:
+ WINPR_ASSERT(_sdl);
+ _sdl->input.keyboard_grab(ev->windowID, SDL_FALSE);
+ return TRUE;
+ case SDL_WINDOWEVENT_ENTER:
+ WINPR_ASSERT(_sdl);
+ _sdl->input.keyboard_grab(ev->windowID, SDL_TRUE);
+ return _sdl->input.keyboard_focus_in();
+ case SDL_WINDOWEVENT_FOCUS_GAINED:
+ case SDL_WINDOWEVENT_TAKE_FOCUS:
+ return _sdl->input.keyboard_focus_in();
+
+ default:
+ return TRUE;
+ }
+}
+
+UINT sdlDispContext::DisplayControlCaps(DispClientContext* disp, UINT32 maxNumMonitors,
+ UINT32 maxMonitorAreaFactorA, UINT32 maxMonitorAreaFactorB)
+{
+ /* we're called only if dynamic resolution update is activated */
+ WINPR_ASSERT(disp);
+
+ auto sdlDisp = reinterpret_cast<sdlDispContext*>(disp->custom);
+ return sdlDisp->DisplayControlCaps(maxNumMonitors, maxMonitorAreaFactorA,
+ maxMonitorAreaFactorB);
+}
+
+UINT sdlDispContext::DisplayControlCaps(UINT32 maxNumMonitors, UINT32 maxMonitorAreaFactorA,
+ UINT32 maxMonitorAreaFactorB)
+{
+ auto settings = _sdl->context()->settings;
+ WINPR_ASSERT(settings);
+
+ WLog_DBG(TAG,
+ "DisplayControlCapsPdu: MaxNumMonitors: %" PRIu32 " MaxMonitorAreaFactorA: %" PRIu32
+ " MaxMonitorAreaFactorB: %" PRIu32 "",
+ maxNumMonitors, maxMonitorAreaFactorA, maxMonitorAreaFactorB);
+ _activated = TRUE;
+
+ if (freerdp_settings_get_bool(settings, FreeRDP_Fullscreen))
+ return CHANNEL_RC_OK;
+
+ WLog_DBG(TAG, "DisplayControlCapsPdu: setting the window as resizable");
+ return set_window_resizable() ? CHANNEL_RC_OK : CHANNEL_RC_NO_MEMORY;
+}
+
+BOOL sdlDispContext::init(DispClientContext* disp)
+{
+ if (!disp)
+ return FALSE;
+
+ auto settings = _sdl->context()->settings;
+
+ if (!settings)
+ return FALSE;
+
+ _disp = disp;
+ disp->custom = this;
+
+ if (freerdp_settings_get_bool(settings, FreeRDP_DynamicResolutionUpdate))
+ {
+ disp->DisplayControlCaps = sdlDispContext::DisplayControlCaps;
+ }
+
+ _sdl->update_resizeable(TRUE);
+ return TRUE;
+}
+
+BOOL sdlDispContext::uninit(DispClientContext* disp)
+{
+ if (!disp)
+ return FALSE;
+
+ _disp = nullptr;
+ _sdl->update_resizeable(FALSE);
+ return TRUE;
+}
+
+sdlDispContext::sdlDispContext(SdlContext* sdl) : _sdl(sdl), _timer(0)
+{
+ SDL_Init(SDL_INIT_TIMER | SDL_INIT_VIDEO);
+
+ WINPR_ASSERT(_sdl);
+ WINPR_ASSERT(_sdl->context()->settings);
+ WINPR_ASSERT(_sdl->context()->pubSub);
+
+ auto settings = _sdl->context()->settings;
+ auto pubSub = _sdl->context()->pubSub;
+
+ _lastSentWidth = _targetWidth = freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth);
+ _lastSentHeight = _targetHeight = freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight);
+ PubSub_SubscribeActivated(pubSub, sdlDispContext::OnActivated);
+ PubSub_SubscribeGraphicsReset(pubSub, sdlDispContext::OnGraphicsReset);
+ addTimer();
+}
+
+sdlDispContext::~sdlDispContext()
+{
+ wPubSub* pubSub = _sdl->context()->pubSub;
+ WINPR_ASSERT(pubSub);
+
+ PubSub_UnsubscribeActivated(pubSub, sdlDispContext::OnActivated);
+ PubSub_UnsubscribeGraphicsReset(pubSub, sdlDispContext::OnGraphicsReset);
+ SDL_RemoveTimer(_timer);
+ SDL_Quit();
+}
diff --git a/client/SDL/sdl_disp.hpp b/client/SDL/sdl_disp.hpp
new file mode 100644
index 0000000..fbe6362
--- /dev/null
+++ b/client/SDL/sdl_disp.hpp
@@ -0,0 +1,82 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * SDL Display Control Channel
+ *
+ * Copyright 2023 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#pragma once
+
+#include <freerdp/types.h>
+#include <freerdp/event.h>
+#include <freerdp/client/disp.h>
+
+#include "sdl_types.hpp"
+
+#include <SDL.h>
+
+class sdlDispContext
+{
+
+ public:
+ explicit sdlDispContext(SdlContext* sdl);
+ ~sdlDispContext();
+
+ BOOL init(DispClientContext* disp);
+ BOOL uninit(DispClientContext* disp);
+
+#if SDL_VERSION_ATLEAST(2, 0, 10)
+ BOOL handle_display_event(const SDL_DisplayEvent* ev);
+#endif
+
+ BOOL handle_window_event(const SDL_WindowEvent* ev);
+
+ private:
+ UINT DisplayControlCaps(UINT32 maxNumMonitors, UINT32 maxMonitorAreaFactorA,
+ UINT32 maxMonitorAreaFactorB);
+ BOOL set_window_resizable();
+
+ BOOL sendResize();
+ BOOL settings_changed();
+ BOOL update_last_sent();
+ UINT sendLayout(const rdpMonitor* monitors, size_t nmonitors);
+
+ BOOL addTimer();
+
+ private:
+ static UINT DisplayControlCaps(DispClientContext* disp, UINT32 maxNumMonitors,
+ UINT32 maxMonitorAreaFactorA, UINT32 maxMonitorAreaFactorB);
+ static void OnActivated(void* context, const ActivatedEventArgs* e);
+ static void OnGraphicsReset(void* context, const GraphicsResetEventArgs* e);
+ static Uint32 SDLCALL OnTimer(Uint32 interval, void* param);
+
+ private:
+ SdlContext* _sdl = nullptr;
+ DispClientContext* _disp = nullptr;
+ int _eventBase = -1;
+ int _errorBase = -1;
+ int _lastSentWidth = -1;
+ int _lastSentHeight = -1;
+ UINT64 _lastSentDate = 0;
+ int _targetWidth = -1;
+ int _targetHeight = -1;
+ BOOL _activated = FALSE;
+ BOOL _waitingResize = FALSE;
+ BOOL _fullscreen = FALSE;
+ UINT16 _lastSentDesktopOrientation = 0;
+ UINT32 _lastSentDesktopScaleFactor = 0;
+ UINT32 _lastSentDeviceScaleFactor = 0;
+ SDL_TimerID _timer = 0;
+ unsigned _timer_retries = 0;
+};
diff --git a/client/SDL/sdl_freerdp.cpp b/client/SDL/sdl_freerdp.cpp
new file mode 100644
index 0000000..890bf77
--- /dev/null
+++ b/client/SDL/sdl_freerdp.cpp
@@ -0,0 +1,1704 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * FreeRDP SDL UI
+ *
+ * Copyright 2022 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <memory>
+#include <mutex>
+#include <iostream>
+
+#include <freerdp/config.h>
+
+#include <cerrno>
+#include <cstdio>
+#include <cstring>
+
+#include <freerdp/freerdp.h>
+#include <freerdp/constants.h>
+#include <freerdp/gdi/gdi.h>
+#include <freerdp/streamdump.h>
+#include <freerdp/utils/signal.h>
+
+#include <freerdp/client/file.h>
+#include <freerdp/client/cmdline.h>
+#include <freerdp/client/cliprdr.h>
+#include <freerdp/client/channels.h>
+#include <freerdp/channels/channels.h>
+
+#include <winpr/crt.h>
+#include <winpr/assert.h>
+#include <winpr/synch.h>
+#include <freerdp/log.h>
+
+#include <SDL.h>
+#include <SDL_hints.h>
+#include <SDL_video.h>
+
+#include "sdl_channels.hpp"
+#include "sdl_freerdp.hpp"
+#include "sdl_utils.hpp"
+#include "sdl_disp.hpp"
+#include "sdl_monitor.hpp"
+#include "sdl_kbd.hpp"
+#include "sdl_touch.hpp"
+#include "sdl_pointer.hpp"
+#include "dialogs/sdl_dialogs.hpp"
+
+#include "aad/sdl_webview.hpp"
+
+#define SDL_TAG CLIENT_TAG("SDL")
+
+enum SDL_EXIT_CODE
+{
+ /* section 0-15: protocol-independent codes */
+ SDL_EXIT_SUCCESS = 0,
+ SDL_EXIT_DISCONNECT = 1,
+ SDL_EXIT_LOGOFF = 2,
+ SDL_EXIT_IDLE_TIMEOUT = 3,
+ SDL_EXIT_LOGON_TIMEOUT = 4,
+ SDL_EXIT_CONN_REPLACED = 5,
+ SDL_EXIT_OUT_OF_MEMORY = 6,
+ SDL_EXIT_CONN_DENIED = 7,
+ SDL_EXIT_CONN_DENIED_FIPS = 8,
+ SDL_EXIT_USER_PRIVILEGES = 9,
+ SDL_EXIT_FRESH_CREDENTIALS_REQUIRED = 10,
+ SDL_EXIT_DISCONNECT_BY_USER = 11,
+
+ /* section 16-31: license error set */
+ SDL_EXIT_LICENSE_INTERNAL = 16,
+ SDL_EXIT_LICENSE_NO_LICENSE_SERVER = 17,
+ SDL_EXIT_LICENSE_NO_LICENSE = 18,
+ SDL_EXIT_LICENSE_BAD_CLIENT_MSG = 19,
+ SDL_EXIT_LICENSE_HWID_DOESNT_MATCH = 20,
+ SDL_EXIT_LICENSE_BAD_CLIENT = 21,
+ SDL_EXIT_LICENSE_CANT_FINISH_PROTOCOL = 22,
+ SDL_EXIT_LICENSE_CLIENT_ENDED_PROTOCOL = 23,
+ SDL_EXIT_LICENSE_BAD_CLIENT_ENCRYPTION = 24,
+ SDL_EXIT_LICENSE_CANT_UPGRADE = 25,
+ SDL_EXIT_LICENSE_NO_REMOTE_CONNECTIONS = 26,
+
+ /* section 32-127: RDP protocol error set */
+ SDL_EXIT_RDP = 32,
+
+ /* section 128-254: xfreerdp specific exit codes */
+ SDL_EXIT_PARSE_ARGUMENTS = 128,
+ SDL_EXIT_MEMORY = 129,
+ SDL_EXIT_PROTOCOL = 130,
+ SDL_EXIT_CONN_FAILED = 131,
+ SDL_EXIT_AUTH_FAILURE = 132,
+ SDL_EXIT_NEGO_FAILURE = 133,
+ SDL_EXIT_LOGON_FAILURE = 134,
+ SDL_EXIT_ACCOUNT_LOCKED_OUT = 135,
+ SDL_EXIT_PRE_CONNECT_FAILED = 136,
+ SDL_EXIT_CONNECT_UNDEFINED = 137,
+ SDL_EXIT_POST_CONNECT_FAILED = 138,
+ SDL_EXIT_DNS_ERROR = 139,
+ SDL_EXIT_DNS_NAME_NOT_FOUND = 140,
+ SDL_EXIT_CONNECT_FAILED = 141,
+ SDL_EXIT_MCS_CONNECT_INITIAL_ERROR = 142,
+ SDL_EXIT_TLS_CONNECT_FAILED = 143,
+ SDL_EXIT_INSUFFICIENT_PRIVILEGES = 144,
+ SDL_EXIT_CONNECT_CANCELLED = 145,
+
+ SDL_EXIT_CONNECT_TRANSPORT_FAILED = 147,
+ SDL_EXIT_CONNECT_PASSWORD_EXPIRED = 148,
+ SDL_EXIT_CONNECT_PASSWORD_MUST_CHANGE = 149,
+ SDL_EXIT_CONNECT_KDC_UNREACHABLE = 150,
+ SDL_EXIT_CONNECT_ACCOUNT_DISABLED = 151,
+ SDL_EXIT_CONNECT_PASSWORD_CERTAINLY_EXPIRED = 152,
+ SDL_EXIT_CONNECT_CLIENT_REVOKED = 153,
+ SDL_EXIT_CONNECT_WRONG_PASSWORD = 154,
+ SDL_EXIT_CONNECT_ACCESS_DENIED = 155,
+ SDL_EXIT_CONNECT_ACCOUNT_RESTRICTION = 156,
+ SDL_EXIT_CONNECT_ACCOUNT_EXPIRED = 157,
+ SDL_EXIT_CONNECT_LOGON_TYPE_NOT_GRANTED = 158,
+ SDL_EXIT_CONNECT_NO_OR_MISSING_CREDENTIALS = 159,
+
+ SDL_EXIT_UNKNOWN = 255,
+};
+
+struct sdl_exit_code_map_t
+{
+ DWORD error;
+ int code;
+ const char* code_tag;
+};
+
+#define ENTRY(x, y) \
+ { \
+ x, y, #y \
+ }
+static const struct sdl_exit_code_map_t sdl_exit_code_map[] = {
+ ENTRY(FREERDP_ERROR_SUCCESS, SDL_EXIT_SUCCESS), ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_DISCONNECT),
+ ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_LOGOFF), ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_IDLE_TIMEOUT),
+ ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_LOGON_TIMEOUT),
+ ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_CONN_REPLACED),
+ ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_OUT_OF_MEMORY),
+ ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_CONN_DENIED),
+ ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_CONN_DENIED_FIPS),
+ ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_USER_PRIVILEGES),
+ ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_FRESH_CREDENTIALS_REQUIRED),
+ ENTRY(ERRINFO_LOGOFF_BY_USER, SDL_EXIT_DISCONNECT_BY_USER),
+ ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_UNKNOWN),
+
+ /* section 16-31: license error set */
+ ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_LICENSE_INTERNAL),
+ ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_LICENSE_NO_LICENSE_SERVER),
+ ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_LICENSE_NO_LICENSE),
+ ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_LICENSE_BAD_CLIENT_MSG),
+ ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_LICENSE_HWID_DOESNT_MATCH),
+ ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_LICENSE_BAD_CLIENT),
+ ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_LICENSE_CANT_FINISH_PROTOCOL),
+ ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_LICENSE_CLIENT_ENDED_PROTOCOL),
+ ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_LICENSE_BAD_CLIENT_ENCRYPTION),
+ ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_LICENSE_CANT_UPGRADE),
+ ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_LICENSE_NO_REMOTE_CONNECTIONS),
+ ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_LICENSE_CANT_UPGRADE),
+
+ /* section 32-127: RDP protocol error set */
+ ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_RDP),
+
+ /* section 128-254: xfreerdp specific exit codes */
+ ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_PARSE_ARGUMENTS), ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_MEMORY),
+ ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_PROTOCOL), ENTRY(FREERDP_ERROR_NONE, SDL_EXIT_CONN_FAILED),
+
+ ENTRY(FREERDP_ERROR_AUTHENTICATION_FAILED, SDL_EXIT_AUTH_FAILURE),
+ ENTRY(FREERDP_ERROR_SECURITY_NEGO_CONNECT_FAILED, SDL_EXIT_NEGO_FAILURE),
+ ENTRY(FREERDP_ERROR_CONNECT_LOGON_FAILURE, SDL_EXIT_LOGON_FAILURE),
+ ENTRY(FREERDP_ERROR_CONNECT_ACCOUNT_LOCKED_OUT, SDL_EXIT_ACCOUNT_LOCKED_OUT),
+ ENTRY(FREERDP_ERROR_PRE_CONNECT_FAILED, SDL_EXIT_PRE_CONNECT_FAILED),
+ ENTRY(FREERDP_ERROR_CONNECT_UNDEFINED, SDL_EXIT_CONNECT_UNDEFINED),
+ ENTRY(FREERDP_ERROR_POST_CONNECT_FAILED, SDL_EXIT_POST_CONNECT_FAILED),
+ ENTRY(FREERDP_ERROR_DNS_ERROR, SDL_EXIT_DNS_ERROR),
+ ENTRY(FREERDP_ERROR_DNS_NAME_NOT_FOUND, SDL_EXIT_DNS_NAME_NOT_FOUND),
+ ENTRY(FREERDP_ERROR_CONNECT_FAILED, SDL_EXIT_CONNECT_FAILED),
+ ENTRY(FREERDP_ERROR_MCS_CONNECT_INITIAL_ERROR, SDL_EXIT_MCS_CONNECT_INITIAL_ERROR),
+ ENTRY(FREERDP_ERROR_TLS_CONNECT_FAILED, SDL_EXIT_TLS_CONNECT_FAILED),
+ ENTRY(FREERDP_ERROR_INSUFFICIENT_PRIVILEGES, SDL_EXIT_INSUFFICIENT_PRIVILEGES),
+ ENTRY(FREERDP_ERROR_CONNECT_CANCELLED, SDL_EXIT_CONNECT_CANCELLED),
+ ENTRY(FREERDP_ERROR_CONNECT_TRANSPORT_FAILED, SDL_EXIT_CONNECT_TRANSPORT_FAILED),
+ ENTRY(FREERDP_ERROR_CONNECT_PASSWORD_EXPIRED, SDL_EXIT_CONNECT_PASSWORD_EXPIRED),
+ ENTRY(FREERDP_ERROR_CONNECT_PASSWORD_MUST_CHANGE, SDL_EXIT_CONNECT_PASSWORD_MUST_CHANGE),
+ ENTRY(FREERDP_ERROR_CONNECT_KDC_UNREACHABLE, SDL_EXIT_CONNECT_KDC_UNREACHABLE),
+ ENTRY(FREERDP_ERROR_CONNECT_ACCOUNT_DISABLED, SDL_EXIT_CONNECT_ACCOUNT_DISABLED),
+ ENTRY(FREERDP_ERROR_CONNECT_PASSWORD_CERTAINLY_EXPIRED,
+ SDL_EXIT_CONNECT_PASSWORD_CERTAINLY_EXPIRED),
+ ENTRY(FREERDP_ERROR_CONNECT_CLIENT_REVOKED, SDL_EXIT_CONNECT_CLIENT_REVOKED),
+ ENTRY(FREERDP_ERROR_CONNECT_WRONG_PASSWORD, SDL_EXIT_CONNECT_WRONG_PASSWORD),
+ ENTRY(FREERDP_ERROR_CONNECT_ACCESS_DENIED, SDL_EXIT_CONNECT_ACCESS_DENIED),
+ ENTRY(FREERDP_ERROR_CONNECT_ACCOUNT_RESTRICTION, SDL_EXIT_CONNECT_ACCOUNT_RESTRICTION),
+ ENTRY(FREERDP_ERROR_CONNECT_ACCOUNT_EXPIRED, SDL_EXIT_CONNECT_ACCOUNT_EXPIRED),
+ ENTRY(FREERDP_ERROR_CONNECT_LOGON_TYPE_NOT_GRANTED, SDL_EXIT_CONNECT_LOGON_TYPE_NOT_GRANTED),
+ ENTRY(FREERDP_ERROR_CONNECT_NO_OR_MISSING_CREDENTIALS,
+ SDL_EXIT_CONNECT_NO_OR_MISSING_CREDENTIALS)
+};
+
+static const struct sdl_exit_code_map_t* sdl_map_entry_by_code(int exit_code)
+{
+ for (size_t x = 0; x < ARRAYSIZE(sdl_exit_code_map); x++)
+ {
+ const struct sdl_exit_code_map_t* cur = &sdl_exit_code_map[x];
+ if (cur->code == exit_code)
+ return cur;
+ }
+ return nullptr;
+}
+
+static const struct sdl_exit_code_map_t* sdl_map_entry_by_error(DWORD error)
+{
+ for (size_t x = 0; x < ARRAYSIZE(sdl_exit_code_map); x++)
+ {
+ const struct sdl_exit_code_map_t* cur = &sdl_exit_code_map[x];
+ if (cur->error == error)
+ return cur;
+ }
+ return nullptr;
+}
+
+static int sdl_map_error_to_exit_code(DWORD error)
+{
+ const struct sdl_exit_code_map_t* entry = sdl_map_entry_by_error(error);
+ if (entry)
+ return entry->code;
+
+ return SDL_EXIT_CONN_FAILED;
+}
+
+static const char* sdl_map_error_to_code_tag(DWORD error)
+{
+ const struct sdl_exit_code_map_t* entry = sdl_map_entry_by_error(error);
+ if (entry)
+ return entry->code_tag;
+ return nullptr;
+}
+
+static const char* sdl_map_to_code_tag(int code)
+{
+ const struct sdl_exit_code_map_t* entry = sdl_map_entry_by_code(code);
+ if (entry)
+ return entry->code_tag;
+ return nullptr;
+}
+
+static int error_info_to_error(freerdp* instance, DWORD* pcode, char** msg, size_t* len)
+{
+ const DWORD code = freerdp_error_info(instance);
+ const char* name = freerdp_get_error_info_name(code);
+ const char* str = freerdp_get_error_info_string(code);
+ const int exit_code = sdl_map_error_to_exit_code(code);
+
+ winpr_asprintf(msg, len, "Terminate with %s due to ERROR_INFO %s [0x%08" PRIx32 "]: %s",
+ sdl_map_error_to_code_tag(exit_code), name, code, str);
+ WLog_DBG(SDL_TAG, "%s", *msg);
+ if (pcode)
+ *pcode = code;
+ return exit_code;
+}
+
+/* This function is called whenever a new frame starts.
+ * It can be used to reset invalidated areas. */
+static BOOL sdl_begin_paint(rdpContext* context)
+{
+ rdpGdi* gdi = nullptr;
+ auto sdl = get_context(context);
+
+ WINPR_ASSERT(sdl);
+
+ HANDLE handles[] = { sdl->update_complete.handle(), freerdp_abort_event(context) };
+ const DWORD status = WaitForMultipleObjects(ARRAYSIZE(handles), handles, FALSE, INFINITE);
+ switch (status)
+ {
+ case WAIT_OBJECT_0:
+ break;
+ default:
+ return FALSE;
+ }
+ sdl->update_complete.clear();
+
+ gdi = context->gdi;
+ WINPR_ASSERT(gdi);
+ WINPR_ASSERT(gdi->primary);
+ WINPR_ASSERT(gdi->primary->hdc);
+ WINPR_ASSERT(gdi->primary->hdc->hwnd);
+ WINPR_ASSERT(gdi->primary->hdc->hwnd->invalid);
+ gdi->primary->hdc->hwnd->invalid->null = TRUE;
+ gdi->primary->hdc->hwnd->ninvalid = 0;
+
+ return TRUE;
+}
+
+static BOOL sdl_redraw(SdlContext* sdl)
+{
+ WINPR_ASSERT(sdl);
+
+ auto gdi = sdl->context()->gdi;
+ return gdi_send_suppress_output(gdi, FALSE);
+}
+
+class SdlEventUpdateTriggerGuard
+{
+ private:
+ SdlContext* _sdl;
+
+ public:
+ explicit SdlEventUpdateTriggerGuard(SdlContext* sdl) : _sdl(sdl)
+ {
+ }
+ ~SdlEventUpdateTriggerGuard()
+ {
+ _sdl->update_complete.set();
+ }
+};
+
+static bool sdl_draw_to_window_rect(SdlContext* sdl, SdlWindow& window, SDL_Surface* surface,
+ SDL_Point offset, const SDL_Rect& srcRect)
+{
+ SDL_Rect dstRect = { offset.x + srcRect.x, offset.y + srcRect.y, srcRect.w, srcRect.h };
+ return window.blit(surface, srcRect, dstRect);
+}
+
+static bool sdl_draw_to_window_rect(SdlContext* sdl, SdlWindow& window, SDL_Surface* surface,
+ SDL_Point offset, const std::vector<SDL_Rect>& rects = {})
+{
+ if (rects.empty())
+ {
+ return sdl_draw_to_window_rect(sdl, window, surface, offset,
+ { 0, 0, surface->w, surface->h });
+ }
+ for (auto& srcRect : rects)
+ {
+ if (!sdl_draw_to_window_rect(sdl, window, surface, offset, srcRect))
+ return false;
+ }
+ return true;
+}
+
+static bool sdl_draw_to_window_scaled_rect(SdlContext* sdl, SdlWindow& window, SDL_Surface* surface,
+ const SDL_Rect& srcRect)
+{
+ SDL_Rect dstRect = srcRect;
+ sdl_scale_coordinates(sdl, window.id(), &dstRect.x, &dstRect.y, FALSE, TRUE);
+ sdl_scale_coordinates(sdl, window.id(), &dstRect.w, &dstRect.h, FALSE, TRUE);
+ return window.blit(surface, srcRect, dstRect);
+}
+
+static BOOL sdl_draw_to_window_scaled_rect(SdlContext* sdl, SdlWindow& window, SDL_Surface* surface,
+ const std::vector<SDL_Rect>& rects = {})
+{
+ if (rects.empty())
+ {
+ return sdl_draw_to_window_scaled_rect(sdl, window, surface,
+ { 0, 0, surface->w, surface->h });
+ }
+ for (const auto& srcRect : rects)
+ {
+ if (!sdl_draw_to_window_scaled_rect(sdl, window, surface, srcRect))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static BOOL sdl_draw_to_window(SdlContext* sdl, SdlWindow& window,
+ const std::vector<SDL_Rect>& rects = {})
+{
+ WINPR_ASSERT(sdl);
+
+ auto context = sdl->context();
+ auto gdi = context->gdi;
+
+ auto size = window.rect();
+
+ if (!freerdp_settings_get_bool(context->settings, FreeRDP_SmartSizing))
+ {
+ if (gdi->width < size.w)
+ {
+ window.setOffsetX((size.w - gdi->width) / 2);
+ }
+ if (gdi->height < size.h)
+ {
+ window.setOffsetY((size.h - gdi->height) / 2);
+ }
+
+ auto surface = sdl->primary.get();
+ if (!sdl_draw_to_window_rect(sdl, window, surface, { window.offsetX(), window.offsetY() },
+ rects))
+ return FALSE;
+ }
+ else
+ {
+ if (!sdl_draw_to_window_scaled_rect(sdl, window, sdl->primary.get(), rects))
+ return FALSE;
+ }
+ window.updateSurface();
+ return TRUE;
+}
+
+static BOOL sdl_draw_to_window(SdlContext* sdl, std::map<Uint32, SdlWindow>& windows,
+ const std::vector<SDL_Rect>& rects = {})
+{
+ for (auto& window : windows)
+ {
+ if (!sdl_draw_to_window(sdl, window.second, rects))
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static BOOL sdl_end_paint_process(rdpContext* context)
+{
+ rdpGdi* gdi = nullptr;
+ auto sdl = get_context(context);
+
+ WINPR_ASSERT(context);
+
+ SdlEventUpdateTriggerGuard guard(sdl);
+
+ gdi = context->gdi;
+ WINPR_ASSERT(gdi);
+ WINPR_ASSERT(gdi->primary);
+ WINPR_ASSERT(gdi->primary->hdc);
+ WINPR_ASSERT(gdi->primary->hdc->hwnd);
+ WINPR_ASSERT(gdi->primary->hdc->hwnd->invalid);
+ if (gdi->suppressOutput || gdi->primary->hdc->hwnd->invalid->null)
+ return TRUE;
+
+ const INT32 ninvalid = gdi->primary->hdc->hwnd->ninvalid;
+ const GDI_RGN* cinvalid = gdi->primary->hdc->hwnd->cinvalid;
+
+ if (ninvalid < 1)
+ return TRUE;
+
+ std::vector<SDL_Rect> rects;
+ for (INT32 x = 0; x < ninvalid; x++)
+ {
+ auto& rgn = cinvalid[x];
+ rects.push_back({ rgn.x, rgn.y, rgn.w, rgn.h });
+ }
+
+ return sdl_draw_to_window(sdl, sdl->windows, rects);
+}
+
+/* This function is called when the library completed composing a new
+ * frame. Read out the changed areas and blit them to your output device.
+ * The image buffer will have the format specified by gdi_init
+ */
+static BOOL sdl_end_paint(rdpContext* context)
+{
+ auto sdl = get_context(context);
+ WINPR_ASSERT(sdl);
+
+ std::lock_guard<CriticalSection> lock(sdl->critical);
+ const BOOL rc = sdl_push_user_event(SDL_USEREVENT_UPDATE, context);
+
+ return rc;
+}
+
+static void sdl_destroy_primary(SdlContext* sdl)
+{
+ if (!sdl)
+ return;
+ sdl->primary.reset();
+ sdl->primary_format.reset();
+}
+
+/* Create a SDL surface from the GDI buffer */
+static BOOL sdl_create_primary(SdlContext* sdl)
+{
+ rdpGdi* gdi = nullptr;
+
+ WINPR_ASSERT(sdl);
+
+ gdi = sdl->context()->gdi;
+ WINPR_ASSERT(gdi);
+
+ sdl_destroy_primary(sdl);
+ sdl->primary = SDLSurfacePtr(
+ SDL_CreateRGBSurfaceWithFormatFrom(gdi->primary_buffer, static_cast<int>(gdi->width),
+ static_cast<int>(gdi->height),
+ static_cast<int>(FreeRDPGetBitsPerPixel(gdi->dstFormat)),
+ static_cast<int>(gdi->stride), sdl->sdl_pixel_format),
+ SDL_FreeSurface);
+ sdl->primary_format = SDLPixelFormatPtr(SDL_AllocFormat(sdl->sdl_pixel_format), SDL_FreeFormat);
+
+ if (!sdl->primary || !sdl->primary_format)
+ return FALSE;
+
+ SDL_SetSurfaceBlendMode(sdl->primary.get(), SDL_BLENDMODE_NONE);
+ SDL_FillRect(sdl->primary.get(), nullptr,
+ SDL_MapRGBA(sdl->primary_format.get(), 0, 0, 0, 0xff));
+
+ return TRUE;
+}
+
+static BOOL sdl_desktop_resize(rdpContext* context)
+{
+ rdpGdi* gdi = nullptr;
+ rdpSettings* settings = nullptr;
+ auto sdl = get_context(context);
+
+ WINPR_ASSERT(sdl);
+ WINPR_ASSERT(context);
+
+ settings = context->settings;
+ WINPR_ASSERT(settings);
+
+ std::lock_guard<CriticalSection> lock(sdl->critical);
+ gdi = context->gdi;
+ if (!gdi_resize(gdi, freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth),
+ freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight)))
+ return FALSE;
+ return sdl_create_primary(sdl);
+}
+
+/* This function is called to output a System BEEP */
+static BOOL sdl_play_sound(rdpContext* context, const PLAY_SOUND_UPDATE* play_sound)
+{
+ /* TODO: Implement */
+ WINPR_UNUSED(context);
+ WINPR_UNUSED(play_sound);
+ return TRUE;
+}
+
+static BOOL sdl_wait_for_init(SdlContext* sdl)
+{
+ WINPR_ASSERT(sdl);
+ sdl->initialize.set();
+
+ HANDLE handles[] = { sdl->initialized.handle(), freerdp_abort_event(sdl->context()) };
+
+ const DWORD rc = WaitForMultipleObjects(ARRAYSIZE(handles), handles, FALSE, INFINITE);
+ switch (rc)
+ {
+ case WAIT_OBJECT_0:
+ return TRUE;
+ default:
+ return FALSE;
+ }
+}
+
+/* Called before a connection is established.
+ * Set all configuration options to support and load channels here. */
+static BOOL sdl_pre_connect(freerdp* instance)
+{
+ WINPR_ASSERT(instance);
+ WINPR_ASSERT(instance->context);
+
+ auto sdl = get_context(instance->context);
+
+ auto settings = instance->context->settings;
+ WINPR_ASSERT(settings);
+
+ /* Optional OS identifier sent to server */
+ if (!freerdp_settings_set_uint32(settings, FreeRDP_OsMajorType, OSMAJORTYPE_UNIX))
+ return FALSE;
+ if (!freerdp_settings_set_uint32(settings, FreeRDP_OsMinorType, OSMINORTYPE_NATIVE_SDL))
+ return FALSE;
+ /* OrderSupport is initialized at this point.
+ * Only override it if you plan to implement custom order
+ * callbacks or deactiveate certain features. */
+ /* Register the channel listeners.
+ * They are required to set up / tear down channels if they are loaded. */
+ PubSub_SubscribeChannelConnected(instance->context->pubSub, sdl_OnChannelConnectedEventHandler);
+ PubSub_SubscribeChannelDisconnected(instance->context->pubSub,
+ sdl_OnChannelDisconnectedEventHandler);
+
+ if (!freerdp_settings_get_bool(settings, FreeRDP_AuthenticationOnly))
+ {
+ UINT32 maxWidth = 0;
+ UINT32 maxHeight = 0;
+
+ if (!sdl_wait_for_init(sdl))
+ return FALSE;
+
+ std::lock_guard<CriticalSection> lock(sdl->critical);
+ if (!freerdp_settings_get_bool(settings, FreeRDP_UseCommonStdioCallbacks))
+ sdl->connection_dialog = std::make_unique<SDLConnectionDialog>(instance->context);
+ if (sdl->connection_dialog)
+ {
+ sdl->connection_dialog->setTitle("Connecting to '%s'",
+ freerdp_settings_get_server_name(settings));
+ sdl->connection_dialog->showInfo(
+ "The connection is being established\n\nPlease wait...");
+ }
+ if (!sdl_detect_monitors(sdl, &maxWidth, &maxHeight))
+ return FALSE;
+
+ if ((maxWidth != 0) && (maxHeight != 0) &&
+ !freerdp_settings_get_bool(settings, FreeRDP_SmartSizing))
+ {
+ WLog_Print(sdl->log, WLOG_INFO, "Update size to %ux%u", maxWidth, maxHeight);
+ if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopWidth, maxWidth))
+ return FALSE;
+ if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight, maxHeight))
+ return FALSE;
+ }
+ }
+ else
+ {
+ /* Check +auth-only has a username and password. */
+ if (!freerdp_settings_get_string(settings, FreeRDP_Password))
+ {
+ WLog_Print(sdl->log, WLOG_INFO, "auth-only, but no password set. Please provide one.");
+ return FALSE;
+ }
+
+ if (!freerdp_settings_set_bool(settings, FreeRDP_DeactivateClientDecoding, TRUE))
+ return FALSE;
+
+ WLog_Print(sdl->log, WLOG_INFO, "Authentication only. Don't connect SDL.");
+ }
+
+ /* TODO: Any code your client requires */
+ return TRUE;
+}
+
+static const char* sdl_window_get_title(rdpSettings* settings)
+{
+ const char* windowTitle = nullptr;
+ UINT32 port = 0;
+ BOOL addPort = 0;
+ const char* name = nullptr;
+ const char* prefix = "FreeRDP:";
+
+ if (!settings)
+ return nullptr;
+
+ windowTitle = freerdp_settings_get_string(settings, FreeRDP_WindowTitle);
+ if (windowTitle)
+ return windowTitle;
+
+ name = freerdp_settings_get_server_name(settings);
+ port = freerdp_settings_get_uint32(settings, FreeRDP_ServerPort);
+
+ addPort = (port != 3389);
+
+ char buffer[MAX_PATH + 64] = { 0 };
+
+ if (!addPort)
+ sprintf_s(buffer, sizeof(buffer), "%s %s", prefix, name);
+ else
+ sprintf_s(buffer, sizeof(buffer), "%s %s:%" PRIu32, prefix, name, port);
+
+ if (!freerdp_settings_set_string(settings, FreeRDP_WindowTitle, buffer))
+ return nullptr;
+ return freerdp_settings_get_string(settings, FreeRDP_WindowTitle);
+}
+
+static void sdl_term_handler(int signum, const char* signame, void* context)
+{
+ sdl_push_quit();
+}
+
+static void sdl_cleanup_sdl(SdlContext* sdl)
+{
+ if (!sdl)
+ return;
+
+ std::lock_guard<CriticalSection> lock(sdl->critical);
+ sdl->windows.clear();
+ sdl->connection_dialog.reset();
+
+ sdl_destroy_primary(sdl);
+
+ freerdp_del_signal_cleanup_handler(sdl->context(), sdl_term_handler);
+ TTF_Quit();
+ SDL_Quit();
+}
+
+static BOOL sdl_create_windows(SdlContext* sdl)
+{
+ WINPR_ASSERT(sdl);
+
+ auto settings = sdl->context()->settings;
+ auto title = sdl_window_get_title(settings);
+ BOOL rc = FALSE;
+
+ UINT32 windowCount = freerdp_settings_get_uint32(settings, FreeRDP_MonitorCount);
+
+ for (UINT32 x = 0; x < windowCount; x++)
+ {
+ auto monitor = static_cast<rdpMonitor*>(
+ freerdp_settings_get_pointer_array_writable(settings, FreeRDP_MonitorDefArray, x));
+
+ Uint32 w = monitor->width;
+ Uint32 h = monitor->height;
+ if (!(freerdp_settings_get_bool(settings, FreeRDP_UseMultimon) ||
+ freerdp_settings_get_bool(settings, FreeRDP_Fullscreen)))
+ {
+ w = freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth);
+ h = freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight);
+ }
+
+ Uint32 flags = SDL_WINDOW_SHOWN;
+ Uint32 startupX = SDL_WINDOWPOS_CENTERED_DISPLAY(x);
+ Uint32 startupY = SDL_WINDOWPOS_CENTERED_DISPLAY(x);
+
+ if (monitor->attributes.desktopScaleFactor > 100)
+ {
+#if SDL_VERSION_ATLEAST(2, 0, 1)
+ flags |= SDL_WINDOW_ALLOW_HIGHDPI;
+#endif
+ }
+
+ if (freerdp_settings_get_bool(settings, FreeRDP_Fullscreen) &&
+ !freerdp_settings_get_bool(settings, FreeRDP_UseMultimon))
+ {
+ flags |= SDL_WINDOW_FULLSCREEN;
+ }
+
+ if (freerdp_settings_get_bool(settings, FreeRDP_UseMultimon))
+ {
+ flags |= SDL_WINDOW_BORDERLESS;
+ }
+
+ if (!freerdp_settings_get_bool(settings, FreeRDP_Decorations))
+ flags |= SDL_WINDOW_BORDERLESS;
+
+ SdlWindow window{ title,
+ static_cast<int>(startupX),
+ static_cast<int>(startupY),
+ static_cast<int>(w),
+ static_cast<int>(h),
+ flags };
+ if (!window.window())
+ goto fail;
+
+ if (freerdp_settings_get_bool(settings, FreeRDP_UseMultimon))
+ {
+ auto r = window.rect();
+ window.setOffsetX(0 - r.x);
+ window.setOffsetY(0 - r.y);
+ }
+
+ sdl->windows.insert({ window.id(), std::move(window) });
+ }
+
+ rc = TRUE;
+fail:
+
+ sdl->windows_created.set();
+ return rc;
+}
+
+static BOOL sdl_wait_create_windows(SdlContext* sdl)
+{
+ std::lock_guard<CriticalSection> lock(sdl->critical);
+ sdl->windows_created.clear();
+ if (!sdl_push_user_event(SDL_USEREVENT_CREATE_WINDOWS, sdl))
+ return FALSE;
+
+ HANDLE handles[] = { sdl->initialized.handle(), freerdp_abort_event(sdl->context()) };
+
+ const DWORD rc = WaitForMultipleObjects(ARRAYSIZE(handles), handles, FALSE, INFINITE);
+ switch (rc)
+ {
+ case WAIT_OBJECT_0:
+ return TRUE;
+ default:
+ return FALSE;
+ }
+}
+
+static bool shall_abort(SdlContext* sdl)
+{
+ std::lock_guard<CriticalSection> lock(sdl->critical);
+ if (freerdp_shall_disconnect_context(sdl->context()))
+ {
+ if (!sdl->connection_dialog)
+ return true;
+ return !sdl->connection_dialog->running();
+ }
+ return false;
+}
+
+static int sdl_run(SdlContext* sdl)
+{
+ int rc = -1;
+ WINPR_ASSERT(sdl);
+
+ HANDLE handles[] = { sdl->initialize.handle(), freerdp_abort_event(sdl->context()) };
+ const DWORD status = WaitForMultipleObjects(ARRAYSIZE(handles), handles, FALSE, INFINITE);
+ switch (status)
+ {
+ case WAIT_OBJECT_0:
+ break;
+ default:
+ return -1;
+ }
+
+ SDL_Init(SDL_INIT_VIDEO);
+ TTF_Init();
+#if SDL_VERSION_ATLEAST(2, 0, 16)
+ SDL_SetHint(SDL_HINT_ALLOW_ALT_TAB_WHILE_GRABBED, "0");
+#endif
+#if SDL_VERSION_ATLEAST(2, 0, 8)
+ SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0");
+#endif
+
+ freerdp_add_signal_cleanup_handler(sdl->context(), sdl_term_handler);
+
+ sdl->initialized.set();
+
+ while (!shall_abort(sdl))
+ {
+ SDL_Event windowEvent = { 0 };
+ while (!shall_abort(sdl) && SDL_WaitEventTimeout(nullptr, 1000))
+ {
+ /* Only poll standard SDL events and SDL_USEREVENTS meant to create dialogs.
+ * do not process the dialog return value events here.
+ */
+ const int prc = SDL_PeepEvents(&windowEvent, 1, SDL_GETEVENT, SDL_FIRSTEVENT,
+ SDL_USEREVENT_RETRY_DIALOG);
+ if (prc < 0)
+ {
+ if (sdl_log_error(prc, sdl->log, "SDL_PeepEvents"))
+ continue;
+ }
+
+#if defined(WITH_DEBUG_SDL_EVENTS)
+ SDL_Log("got event %s [0x%08" PRIx32 "]", sdl_event_type_str(windowEvent.type),
+ windowEvent.type);
+#endif
+ std::lock_guard<CriticalSection> lock(sdl->critical);
+ /* The session might have been disconnected while we were waiting for a new SDL event.
+ * In that case ignore the SDL event and terminate. */
+ if (freerdp_shall_disconnect_context(sdl->context()))
+ continue;
+
+ if (sdl->connection_dialog)
+ {
+ if (sdl->connection_dialog->handle(windowEvent))
+ {
+ continue;
+ }
+ }
+
+ switch (windowEvent.type)
+ {
+ case SDL_QUIT:
+ freerdp_abort_connect_context(sdl->context());
+ break;
+ case SDL_KEYDOWN:
+ case SDL_KEYUP:
+ {
+ const SDL_KeyboardEvent* ev = &windowEvent.key;
+ sdl->input.keyboard_handle_event(ev);
+ }
+ break;
+ case SDL_KEYMAPCHANGED:
+ {
+ }
+ break; // TODO: Switch keyboard layout
+ case SDL_MOUSEMOTION:
+ {
+ const SDL_MouseMotionEvent* ev = &windowEvent.motion;
+ sdl_handle_mouse_motion(sdl, ev);
+ }
+ break;
+ case SDL_MOUSEBUTTONDOWN:
+ case SDL_MOUSEBUTTONUP:
+ {
+ const SDL_MouseButtonEvent* ev = &windowEvent.button;
+ sdl_handle_mouse_button(sdl, ev);
+ }
+ break;
+ case SDL_MOUSEWHEEL:
+ {
+ const SDL_MouseWheelEvent* ev = &windowEvent.wheel;
+ sdl_handle_mouse_wheel(sdl, ev);
+ }
+ break;
+ case SDL_FINGERDOWN:
+ {
+ const SDL_TouchFingerEvent* ev = &windowEvent.tfinger;
+ sdl_handle_touch_down(sdl, ev);
+ }
+ break;
+ case SDL_FINGERUP:
+ {
+ const SDL_TouchFingerEvent* ev = &windowEvent.tfinger;
+ sdl_handle_touch_up(sdl, ev);
+ }
+ break;
+ case SDL_FINGERMOTION:
+ {
+ const SDL_TouchFingerEvent* ev = &windowEvent.tfinger;
+ sdl_handle_touch_motion(sdl, ev);
+ }
+ break;
+#if SDL_VERSION_ATLEAST(2, 0, 10)
+ case SDL_DISPLAYEVENT:
+ {
+ const SDL_DisplayEvent* ev = &windowEvent.display;
+ sdl->disp.handle_display_event(ev);
+ }
+ break;
+#endif
+ case SDL_WINDOWEVENT:
+ {
+ const SDL_WindowEvent* ev = &windowEvent.window;
+ sdl->disp.handle_window_event(ev);
+
+ switch (ev->event)
+ {
+ case SDL_WINDOWEVENT_RESIZED:
+ case SDL_WINDOWEVENT_SIZE_CHANGED:
+ {
+ auto window = sdl->windows.find(ev->windowID);
+ if (window != sdl->windows.end())
+ {
+ window->second.fill();
+ window->second.updateSurface();
+ }
+ }
+ break;
+ case SDL_WINDOWEVENT_MOVED:
+ {
+ auto window = sdl->windows.find(ev->windowID);
+ if (window != sdl->windows.end())
+ {
+ auto r = window->second.rect();
+ auto id = window->second.id();
+ WLog_DBG(SDL_TAG, "%lu: %dx%d-%dx%d", id, r.x, r.y, r.w, r.h);
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ break;
+
+ case SDL_RENDER_TARGETS_RESET:
+ sdl_redraw(sdl);
+ break;
+ case SDL_RENDER_DEVICE_RESET:
+ sdl_redraw(sdl);
+ break;
+ case SDL_APP_WILLENTERFOREGROUND:
+ sdl_redraw(sdl);
+ break;
+ case SDL_USEREVENT_CERT_DIALOG:
+ {
+ auto title = static_cast<const char*>(windowEvent.user.data1);
+ auto msg = static_cast<const char*>(windowEvent.user.data2);
+ sdl_cert_dialog_show(title, msg);
+ }
+ break;
+ case SDL_USEREVENT_SHOW_DIALOG:
+ {
+ auto title = static_cast<const char*>(windowEvent.user.data1);
+ auto msg = static_cast<const char*>(windowEvent.user.data2);
+ sdl_message_dialog_show(title, msg, windowEvent.user.code);
+ }
+ break;
+ case SDL_USEREVENT_SCARD_DIALOG:
+ {
+ auto title = static_cast<const char*>(windowEvent.user.data1);
+ auto msg = static_cast<const char**>(windowEvent.user.data2);
+ sdl_scard_dialog_show(title, windowEvent.user.code, msg);
+ }
+ break;
+ case SDL_USEREVENT_AUTH_DIALOG:
+ sdl_auth_dialog_show(
+ reinterpret_cast<const SDL_UserAuthArg*>(windowEvent.padding));
+ break;
+ case SDL_USEREVENT_UPDATE:
+ {
+ auto context = static_cast<rdpContext*>(windowEvent.user.data1);
+ sdl_end_paint_process(context);
+ }
+ break;
+ case SDL_USEREVENT_CREATE_WINDOWS:
+ {
+ auto ctx = static_cast<SdlContext*>(windowEvent.user.data1);
+ sdl_create_windows(ctx);
+ }
+ break;
+ case SDL_USEREVENT_WINDOW_RESIZEABLE:
+ {
+ auto window = static_cast<SdlWindow*>(windowEvent.user.data1);
+ const SDL_bool use = windowEvent.user.code != 0 ? SDL_TRUE : SDL_FALSE;
+ if (window)
+ window->resizeable(use);
+ }
+ break;
+ case SDL_USEREVENT_WINDOW_FULLSCREEN:
+ {
+ auto window = static_cast<SdlWindow*>(windowEvent.user.data1);
+ const SDL_bool enter = windowEvent.user.code != 0 ? SDL_TRUE : SDL_FALSE;
+ if (window)
+ window->fullscreen(enter);
+ }
+ break;
+ case SDL_USEREVENT_POINTER_NULL:
+ SDL_ShowCursor(SDL_DISABLE);
+ break;
+ case SDL_USEREVENT_POINTER_DEFAULT:
+ {
+ SDL_Cursor* def = SDL_GetDefaultCursor();
+ SDL_SetCursor(def);
+ SDL_ShowCursor(SDL_ENABLE);
+ }
+ break;
+ case SDL_USEREVENT_POINTER_POSITION:
+ {
+ const auto x =
+ static_cast<INT32>(reinterpret_cast<uintptr_t>(windowEvent.user.data1));
+ const auto y =
+ static_cast<INT32>(reinterpret_cast<uintptr_t>(windowEvent.user.data2));
+
+ SDL_Window* window = SDL_GetMouseFocus();
+ if (window)
+ {
+ const Uint32 id = SDL_GetWindowID(window);
+
+ INT32 sx = x;
+ INT32 sy = y;
+ if (sdl_scale_coordinates(sdl, id, &sx, &sy, FALSE, FALSE))
+ SDL_WarpMouseInWindow(window, sx, sy);
+ }
+ }
+ break;
+ case SDL_USEREVENT_POINTER_SET:
+ sdl_Pointer_Set_Process(&windowEvent.user);
+ break;
+ case SDL_USEREVENT_QUIT:
+ default:
+ break;
+ }
+ }
+ }
+
+ rc = 1;
+
+ sdl_cleanup_sdl(sdl);
+ return rc;
+}
+
+/* Called after a RDP connection was successfully established.
+ * Settings might have changed during negociation of client / server feature
+ * support.
+ *
+ * Set up local framebuffers and paing callbacks.
+ * If required, register pointer callbacks to change the local mouse cursor
+ * when hovering over the RDP window
+ */
+static BOOL sdl_post_connect(freerdp* instance)
+{
+ WINPR_ASSERT(instance);
+
+ auto context = instance->context;
+ WINPR_ASSERT(context);
+
+ auto sdl = get_context(context);
+
+ // Retry was successful, discard dialog
+ {
+ std::lock_guard<CriticalSection> lock(sdl->critical);
+ if (sdl->connection_dialog)
+ sdl->connection_dialog->hide();
+ }
+
+ if (freerdp_settings_get_bool(context->settings, FreeRDP_AuthenticationOnly))
+ {
+ /* Check +auth-only has a username and password. */
+ if (!freerdp_settings_get_string(context->settings, FreeRDP_Password))
+ {
+ WLog_Print(sdl->log, WLOG_INFO, "auth-only, but no password set. Please provide one.");
+ return FALSE;
+ }
+
+ WLog_Print(sdl->log, WLOG_INFO, "Authentication only. Don't connect to X.");
+ return TRUE;
+ }
+
+ if (!sdl_wait_create_windows(sdl))
+ return FALSE;
+
+ sdl->sdl_pixel_format = SDL_PIXELFORMAT_BGRA32;
+ if (!gdi_init(instance, PIXEL_FORMAT_BGRA32))
+ return FALSE;
+
+ if (!sdl_create_primary(sdl))
+ return FALSE;
+
+ if (!sdl_register_pointer(instance->context->graphics))
+ return FALSE;
+
+ WINPR_ASSERT(context->update);
+
+ context->update->BeginPaint = sdl_begin_paint;
+ context->update->EndPaint = sdl_end_paint;
+ context->update->PlaySound = sdl_play_sound;
+ context->update->DesktopResize = sdl_desktop_resize;
+ context->update->SetKeyboardIndicators = sdlInput::keyboard_set_indicators;
+ context->update->SetKeyboardImeStatus = sdlInput::keyboard_set_ime_status;
+
+ sdl->update_resizeable(FALSE);
+ sdl->update_fullscreen(freerdp_settings_get_bool(context->settings, FreeRDP_Fullscreen) ||
+ freerdp_settings_get_bool(context->settings, FreeRDP_UseMultimon));
+ return TRUE;
+}
+
+/* This function is called whether a session ends by failure or success.
+ * Clean up everything allocated by pre_connect and post_connect.
+ */
+static void sdl_post_disconnect(freerdp* instance)
+{
+ if (!instance)
+ return;
+
+ if (!instance->context)
+ return;
+
+ PubSub_UnsubscribeChannelConnected(instance->context->pubSub,
+ sdl_OnChannelConnectedEventHandler);
+ PubSub_UnsubscribeChannelDisconnected(instance->context->pubSub,
+ sdl_OnChannelDisconnectedEventHandler);
+ gdi_free(instance);
+}
+
+static void sdl_post_final_disconnect(freerdp* instance)
+{
+ if (!instance)
+ return;
+
+ if (!instance->context)
+ return;
+
+ auto context = get_context(instance->context);
+}
+
+/* RDP main loop.
+ * Connects RDP, loops while running and handles event and dispatch, cleans up
+ * after the connection ends. */
+static DWORD WINAPI sdl_client_thread_proc(SdlContext* sdl)
+{
+ DWORD nCount = 0;
+ DWORD status = 0;
+ int exit_code = SDL_EXIT_SUCCESS;
+ char* error_msg = nullptr;
+ size_t error_msg_len = 0;
+
+ HANDLE handles[MAXIMUM_WAIT_OBJECTS] = {};
+
+ WINPR_ASSERT(sdl);
+
+ auto instance = sdl->context()->instance;
+ WINPR_ASSERT(instance);
+
+ BOOL rc = freerdp_connect(instance);
+
+ rdpContext* context = sdl->context();
+ rdpSettings* settings = context->settings;
+ WINPR_ASSERT(settings);
+
+ if (!rc)
+ {
+ UINT32 error = freerdp_get_last_error(context);
+ exit_code = sdl_map_error_to_exit_code(error);
+ }
+
+ if (freerdp_settings_get_bool(settings, FreeRDP_AuthenticationOnly))
+ {
+ DWORD code = freerdp_get_last_error(context);
+ freerdp_abort_connect_context(context);
+ WLog_Print(sdl->log, WLOG_ERROR, "Authentication only, %s [0x%08" PRIx32 "] %s",
+ freerdp_get_last_error_name(code), code, freerdp_get_last_error_string(code));
+ goto terminate;
+ }
+
+ if (!rc)
+ {
+ DWORD code = freerdp_error_info(instance);
+ if (exit_code == SDL_EXIT_SUCCESS)
+ exit_code = error_info_to_error(instance, &code, &error_msg, &error_msg_len);
+
+ auto last = freerdp_get_last_error(context);
+ if (!error_msg)
+ {
+ winpr_asprintf(&error_msg, &error_msg_len, "%s [0x%08" PRIx32 "]\n%s",
+ freerdp_get_last_error_name(last), last,
+ freerdp_get_last_error_string(last));
+ }
+
+ if (last == FREERDP_ERROR_AUTHENTICATION_FAILED)
+ exit_code = SDL_EXIT_AUTH_FAILURE;
+ else if (code == ERRINFO_SUCCESS)
+ exit_code = SDL_EXIT_CONN_FAILED;
+
+ goto terminate;
+ }
+
+ while (!freerdp_shall_disconnect_context(context))
+ {
+ /*
+ * win8 and server 2k12 seem to have some timing issue/race condition
+ * when a initial sync request is send to sync the keyboard indicators
+ * sending the sync event twice fixed this problem
+ */
+ if (freerdp_focus_required(instance))
+ {
+ auto ctx = get_context(context);
+ WINPR_ASSERT(ctx);
+ if (!ctx->input.keyboard_focus_in())
+ break;
+ if (!ctx->input.keyboard_focus_in())
+ break;
+ }
+
+ nCount = freerdp_get_event_handles(context, handles, ARRAYSIZE(handles));
+
+ if (nCount == 0)
+ {
+ WLog_Print(sdl->log, WLOG_ERROR, "freerdp_get_event_handles failed");
+ break;
+ }
+
+ status = WaitForMultipleObjects(nCount, handles, FALSE, 100);
+
+ if (status == WAIT_FAILED)
+ {
+ if (client_auto_reconnect(instance))
+ continue;
+ else
+ {
+ /*
+ * Indicate an unsuccessful connection attempt if reconnect
+ * did not succeed and no other error was specified.
+ */
+ if (freerdp_error_info(instance) == 0)
+ exit_code = SDL_EXIT_CONN_FAILED;
+ }
+
+ if (freerdp_get_last_error(context) == FREERDP_ERROR_SUCCESS)
+ WLog_Print(sdl->log, WLOG_ERROR, "WaitForMultipleObjects failed with %" PRIu32 "",
+ status);
+ break;
+ }
+
+ if (!freerdp_check_event_handles(context))
+ {
+ if (freerdp_get_last_error(context) == FREERDP_ERROR_SUCCESS)
+ WLog_Print(sdl->log, WLOG_ERROR, "Failed to check FreeRDP event handles");
+
+ break;
+ }
+ }
+
+ if (exit_code == SDL_EXIT_SUCCESS)
+ {
+ DWORD code = 0;
+ exit_code = error_info_to_error(instance, &code, &error_msg, &error_msg_len);
+
+ if ((code == ERRINFO_LOGOFF_BY_USER) &&
+ (freerdp_get_disconnect_ultimatum(context) == Disconnect_Ultimatum_user_requested))
+ {
+ const char* msg = "Error info says user did not initiate but disconnect ultimatum says "
+ "they did; treat this as a user logoff";
+ free(error_msg);
+ error_msg = nullptr;
+ error_msg_len = 0;
+ winpr_asprintf(&error_msg, &error_msg_len, "%s", msg);
+
+ /* This situation might be limited to Windows XP. */
+ WLog_Print(sdl->log, WLOG_INFO, "%s", msg);
+ exit_code = SDL_EXIT_LOGOFF;
+ }
+ }
+
+ freerdp_disconnect(instance);
+
+terminate:
+ if (freerdp_settings_get_bool(settings, FreeRDP_AuthenticationOnly))
+ WLog_Print(sdl->log, WLOG_INFO, "Authentication only, exit status %s [%" PRId32 "]",
+ sdl_map_to_code_tag(exit_code), exit_code);
+ else
+ {
+ switch (exit_code)
+ {
+ case SDL_EXIT_SUCCESS:
+ case SDL_EXIT_DISCONNECT:
+ case SDL_EXIT_LOGOFF:
+ case SDL_EXIT_DISCONNECT_BY_USER:
+ break;
+ default:
+ {
+ std::lock_guard<CriticalSection> lock(sdl->critical);
+ if (sdl->connection_dialog)
+ sdl->connection_dialog->showError(error_msg);
+ }
+ break;
+ }
+ }
+ free(error_msg);
+ sdl->exit_code = exit_code;
+ sdl_push_user_event(SDL_USEREVENT_QUIT);
+#if SDL_VERSION_ATLEAST(2, 0, 16)
+ SDL_TLSCleanup();
+#endif
+ return 0;
+}
+
+/* Optional global initializer.
+ * Here we just register a signal handler to print out stack traces
+ * if available. */
+static BOOL sdl_client_global_init(void)
+{
+#if defined(_WIN32)
+ WSADATA wsaData = { 0 };
+ const DWORD wVersionRequested = MAKEWORD(1, 1);
+ const int rc = WSAStartup(wVersionRequested, &wsaData);
+ if (rc != 0)
+ {
+ WLog_ERR(SDL_TAG, "WSAStartup failed with %s [%d]", gai_strerrorA(rc), rc);
+ return FALSE;
+ }
+#endif
+
+ if (freerdp_handle_signals() != 0)
+ return FALSE;
+
+ return TRUE;
+}
+
+/* Optional global tear down */
+static void sdl_client_global_uninit(void)
+{
+#if defined(_WIN32)
+ WSACleanup();
+#endif
+}
+
+static BOOL sdl_client_new(freerdp* instance, rdpContext* context)
+{
+ auto sdl = reinterpret_cast<sdl_rdp_context*>(context);
+
+ if (!instance || !context)
+ return FALSE;
+
+ sdl->sdl = new SdlContext(context);
+ if (!sdl->sdl)
+ return FALSE;
+
+ instance->PreConnect = sdl_pre_connect;
+ instance->PostConnect = sdl_post_connect;
+ instance->PostDisconnect = sdl_post_disconnect;
+ instance->PostFinalDisconnect = sdl_post_final_disconnect;
+ instance->AuthenticateEx = sdl_authenticate_ex;
+ instance->VerifyCertificateEx = sdl_verify_certificate_ex;
+ instance->VerifyChangedCertificateEx = sdl_verify_changed_certificate_ex;
+ instance->LogonErrorInfo = sdl_logon_error_info;
+ instance->PresentGatewayMessage = sdl_present_gateway_message;
+ instance->ChooseSmartcard = sdl_choose_smartcard;
+ instance->RetryDialog = sdl_retry_dialog;
+
+#ifdef WITH_WEBVIEW
+ instance->GetAccessToken = sdl_webview_get_access_token;
+#else
+ instance->GetAccessToken = client_cli_get_access_token;
+#endif
+ /* TODO: Client display set up */
+
+ return TRUE;
+}
+
+static void sdl_client_free(freerdp* instance, rdpContext* context)
+{
+ auto sdl = reinterpret_cast<sdl_rdp_context*>(context);
+
+ if (!context)
+ return;
+
+ delete sdl->sdl;
+}
+
+static int sdl_client_start(rdpContext* context)
+{
+ auto sdl = get_context(context);
+ WINPR_ASSERT(sdl);
+
+ sdl->thread = std::thread(sdl_client_thread_proc, sdl);
+ return 0;
+}
+
+static int sdl_client_stop(rdpContext* context)
+{
+ auto sdl = get_context(context);
+ WINPR_ASSERT(sdl);
+
+ /* We do not want to use freerdp_abort_connect_context here.
+ * It would change the exit code and we do not want that. */
+ HANDLE event = freerdp_abort_event(context);
+ if (!SetEvent(event))
+ return -1;
+
+ sdl->thread.join();
+ return 0;
+}
+
+static int RdpClientEntry(RDP_CLIENT_ENTRY_POINTS* pEntryPoints)
+{
+ WINPR_ASSERT(pEntryPoints);
+
+ ZeroMemory(pEntryPoints, sizeof(RDP_CLIENT_ENTRY_POINTS));
+ pEntryPoints->Version = RDP_CLIENT_INTERFACE_VERSION;
+ pEntryPoints->Size = sizeof(RDP_CLIENT_ENTRY_POINTS_V1);
+ pEntryPoints->GlobalInit = sdl_client_global_init;
+ pEntryPoints->GlobalUninit = sdl_client_global_uninit;
+ pEntryPoints->ContextSize = sizeof(sdl_rdp_context);
+ pEntryPoints->ClientNew = sdl_client_new;
+ pEntryPoints->ClientFree = sdl_client_free;
+ pEntryPoints->ClientStart = sdl_client_start;
+ pEntryPoints->ClientStop = sdl_client_stop;
+ return 0;
+}
+
+static void context_free(sdl_rdp_context* sdl)
+{
+ if (sdl)
+ freerdp_client_context_free(&sdl->common.context);
+}
+
+static const char* category2str(int category)
+{
+ switch (category)
+ {
+ case SDL_LOG_CATEGORY_APPLICATION:
+ return "SDL_LOG_CATEGORY_APPLICATION";
+ case SDL_LOG_CATEGORY_ERROR:
+ return "SDL_LOG_CATEGORY_ERROR";
+ case SDL_LOG_CATEGORY_ASSERT:
+ return "SDL_LOG_CATEGORY_ASSERT";
+ case SDL_LOG_CATEGORY_SYSTEM:
+ return "SDL_LOG_CATEGORY_SYSTEM";
+ case SDL_LOG_CATEGORY_AUDIO:
+ return "SDL_LOG_CATEGORY_AUDIO";
+ case SDL_LOG_CATEGORY_VIDEO:
+ return "SDL_LOG_CATEGORY_VIDEO";
+ case SDL_LOG_CATEGORY_RENDER:
+ return "SDL_LOG_CATEGORY_RENDER";
+ case SDL_LOG_CATEGORY_INPUT:
+ return "SDL_LOG_CATEGORY_INPUT";
+ case SDL_LOG_CATEGORY_TEST:
+ return "SDL_LOG_CATEGORY_TEST";
+ case SDL_LOG_CATEGORY_RESERVED1:
+ return "SDL_LOG_CATEGORY_RESERVED1";
+ case SDL_LOG_CATEGORY_RESERVED2:
+ return "SDL_LOG_CATEGORY_RESERVED2";
+ case SDL_LOG_CATEGORY_RESERVED3:
+ return "SDL_LOG_CATEGORY_RESERVED3";
+ case SDL_LOG_CATEGORY_RESERVED4:
+ return "SDL_LOG_CATEGORY_RESERVED4";
+ case SDL_LOG_CATEGORY_RESERVED5:
+ return "SDL_LOG_CATEGORY_RESERVED5";
+ case SDL_LOG_CATEGORY_RESERVED6:
+ return "SDL_LOG_CATEGORY_RESERVED6";
+ case SDL_LOG_CATEGORY_RESERVED7:
+ return "SDL_LOG_CATEGORY_RESERVED7";
+ case SDL_LOG_CATEGORY_RESERVED8:
+ return "SDL_LOG_CATEGORY_RESERVED8";
+ case SDL_LOG_CATEGORY_RESERVED9:
+ return "SDL_LOG_CATEGORY_RESERVED9";
+ case SDL_LOG_CATEGORY_RESERVED10:
+ return "SDL_LOG_CATEGORY_RESERVED10";
+ case SDL_LOG_CATEGORY_CUSTOM:
+ default:
+ return "SDL_LOG_CATEGORY_CUSTOM";
+ }
+}
+
+static SDL_LogPriority wloglevel2dl(DWORD level)
+{
+ switch (level)
+ {
+ case WLOG_TRACE:
+ return SDL_LOG_PRIORITY_VERBOSE;
+ case WLOG_DEBUG:
+ return SDL_LOG_PRIORITY_DEBUG;
+ case WLOG_INFO:
+ return SDL_LOG_PRIORITY_INFO;
+ case WLOG_WARN:
+ return SDL_LOG_PRIORITY_WARN;
+ case WLOG_ERROR:
+ return SDL_LOG_PRIORITY_ERROR;
+ case WLOG_FATAL:
+ return SDL_LOG_PRIORITY_CRITICAL;
+ case WLOG_OFF:
+ default:
+ return SDL_LOG_PRIORITY_VERBOSE;
+ }
+}
+
+static DWORD sdlpriority2wlog(SDL_LogPriority priority)
+{
+ DWORD level = WLOG_OFF;
+ switch (priority)
+ {
+ case SDL_LOG_PRIORITY_VERBOSE:
+ level = WLOG_TRACE;
+ break;
+ case SDL_LOG_PRIORITY_DEBUG:
+ level = WLOG_DEBUG;
+ break;
+ case SDL_LOG_PRIORITY_INFO:
+ level = WLOG_INFO;
+ break;
+ case SDL_LOG_PRIORITY_WARN:
+ level = WLOG_WARN;
+ break;
+ case SDL_LOG_PRIORITY_ERROR:
+ level = WLOG_ERROR;
+ break;
+ case SDL_LOG_PRIORITY_CRITICAL:
+ level = WLOG_FATAL;
+ break;
+ default:
+ break;
+ }
+
+ return level;
+}
+
+static void SDLCALL winpr_LogOutputFunction(void* userdata, int category, SDL_LogPriority priority,
+ const char* message)
+{
+ auto sdl = static_cast<SdlContext*>(userdata);
+ WINPR_ASSERT(sdl);
+
+ const DWORD level = sdlpriority2wlog(priority);
+ auto log = sdl->log;
+ if (!WLog_IsLevelActive(log, level))
+ return;
+
+ WLog_PrintMessage(log, WLOG_MESSAGE_TEXT, level, __LINE__, __FILE__, __func__, "[%s] %s",
+ category2str(category), message);
+}
+
+static void print_config_file_help()
+{
+#if defined(CJSON_FOUND)
+ std::cout << "CONFIGURATION FILE" << std::endl;
+ std::cout << std::endl;
+ std::cout << " The SDL client supports some user defined configuration options." << std::endl;
+ std::cout << " Settings are stored in JSON format" << std::endl;
+ std::cout << " The location is a per user file. Location for current user is "
+ << sdl_get_pref_file() << std::endl;
+ std::cout
+ << " The XDG_CONFIG_HOME environment variable can be used to override the base directory."
+ << std::endl;
+ std::cout << std::endl;
+ std::cout << " The following configuration options are supported:" << std::endl;
+ std::cout << std::endl;
+ std::cout << " SDL_KeyModMask" << std::endl;
+ std::cout << " Defines the key combination required for SDL client shortcuts."
+ << std::endl;
+ std::cout << " Default KMOD_RSHIFT" << std::endl;
+ std::cout << " An array of SDL_Keymod strings as defined at "
+ "https://wiki.libsdl.org/SDL2/SDL_Keymod"
+ << std::endl;
+ std::cout << std::endl;
+ std::cout << " SDL_Fullscreen" << std::endl;
+ std::cout << " Toggles client fullscreen state." << std::endl;
+ std::cout << " Default SDL_SCANCODE_RETURN." << std::endl;
+ std::cout << " A string as "
+ "defined at https://wiki.libsdl.org/SDL2/SDLScancodeLookup"
+ << std::endl;
+ std::cout << std::endl;
+ std::cout << " SDL_Resizeable" << std::endl;
+ std::cout << " Toggles local window resizeable state." << std::endl;
+ std::cout << " Default SDL_SCANCODE_R." << std::endl;
+ std::cout << " A string as "
+ "defined at https://wiki.libsdl.org/SDL2/SDLScancodeLookup"
+ << std::endl;
+ std::cout << std::endl;
+ std::cout << " SDL_Grab" << std::endl;
+ std::cout << " Toggles keyboard and mouse grab state." << std::endl;
+ std::cout << " Default SDL_SCANCODE_G." << std::endl;
+ std::cout << " A string as "
+ "defined at https://wiki.libsdl.org/SDL2/SDLScancodeLookup"
+ << std::endl;
+ std::cout << std::endl;
+ std::cout << " SDL_Disconnect" << std::endl;
+ std::cout << " Disconnects from the RDP session." << std::endl;
+ std::cout << " Default SDL_SCANCODE_D." << std::endl;
+ std::cout << " A string as defined at https://wiki.libsdl.org/SDL2/SDLScancodeLookup"
+ << std::endl;
+#endif
+}
+
+int main(int argc, char* argv[])
+{
+ int rc = -1;
+ int status = 0;
+ RDP_CLIENT_ENTRY_POINTS clientEntryPoints = {};
+
+ freerdp_client_warn_experimental(argc, argv);
+
+ RdpClientEntry(&clientEntryPoints);
+ std::unique_ptr<sdl_rdp_context, void (*)(sdl_rdp_context*)> sdl_rdp(
+ reinterpret_cast<sdl_rdp_context*>(freerdp_client_context_new(&clientEntryPoints)),
+ context_free);
+
+ if (!sdl_rdp)
+ return -1;
+ auto sdl = sdl_rdp->sdl;
+
+ auto settings = sdl->context()->settings;
+ WINPR_ASSERT(settings);
+
+ status = freerdp_client_settings_parse_command_line(settings, argc, argv, FALSE);
+ if (status)
+ {
+ rc = freerdp_client_settings_command_line_status_print(settings, status, argc, argv);
+ print_config_file_help();
+ if (freerdp_settings_get_bool(settings, FreeRDP_ListMonitors))
+ sdl_list_monitors(sdl);
+ return rc;
+ }
+
+ SDL_LogSetOutputFunction(winpr_LogOutputFunction, sdl);
+ auto level = WLog_GetLogLevel(sdl->log);
+ SDL_LogSetAllPriority(wloglevel2dl(level));
+
+ auto context = sdl->context();
+ WINPR_ASSERT(context);
+
+ if (!stream_dump_register_handlers(context, CONNECTION_STATE_MCS_CREATE_REQUEST, FALSE))
+ return -1;
+
+ if (freerdp_client_start(context) != 0)
+ return -1;
+
+ rc = sdl_run(sdl);
+
+ if (freerdp_client_stop(context) != 0)
+ return -1;
+
+ rc = sdl->exit_code;
+
+ return rc;
+}
+
+BOOL SdlContext::update_fullscreen(BOOL enter)
+{
+ std::lock_guard<CriticalSection> lock(critical);
+ for (const auto& window : windows)
+ {
+ if (!sdl_push_user_event(SDL_USEREVENT_WINDOW_FULLSCREEN, &window.second, enter))
+ return FALSE;
+ }
+ fullscreen = enter;
+ return TRUE;
+}
+
+BOOL SdlContext::update_resizeable(BOOL enable)
+{
+ std::lock_guard<CriticalSection> lock(critical);
+
+ const auto settings = context()->settings;
+ const BOOL dyn = freerdp_settings_get_bool(settings, FreeRDP_DynamicResolutionUpdate);
+ const BOOL smart = freerdp_settings_get_bool(settings, FreeRDP_SmartSizing);
+ BOOL use = (dyn && enable) || smart;
+
+ for (const auto& window : windows)
+ {
+ if (!sdl_push_user_event(SDL_USEREVENT_WINDOW_RESIZEABLE, &window.second, use))
+ return FALSE;
+ }
+ resizeable = use;
+
+ return TRUE;
+}
+
+SdlContext::SdlContext(rdpContext* context)
+ : _context(context), log(WLog_Get(SDL_TAG)), update_complete(true), disp(this), input(this),
+ primary(nullptr, SDL_FreeSurface), primary_format(nullptr, SDL_FreeFormat)
+{
+}
+
+rdpContext* SdlContext::context() const
+{
+ return _context;
+}
+
+rdpClientContext* SdlContext::common() const
+{
+ return reinterpret_cast<rdpClientContext*>(_context);
+}
diff --git a/client/SDL/sdl_freerdp.hpp b/client/SDL/sdl_freerdp.hpp
new file mode 100644
index 0000000..79ed890
--- /dev/null
+++ b/client/SDL/sdl_freerdp.hpp
@@ -0,0 +1,88 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * SDL Client
+ *
+ * Copyright 2022 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <memory>
+#include <thread>
+#include <map>
+
+#include <freerdp/freerdp.h>
+#include <freerdp/client/rdpei.h>
+#include <freerdp/client/rail.h>
+#include <freerdp/client/cliprdr.h>
+#include <freerdp/client/rdpgfx.h>
+
+#include <SDL.h>
+#include <SDL_video.h>
+
+#include "sdl_types.hpp"
+#include "sdl_disp.hpp"
+#include "sdl_kbd.hpp"
+#include "sdl_utils.hpp"
+#include "sdl_window.hpp"
+#include "dialogs/sdl_connection_dialog.hpp"
+
+using SDLSurfacePtr = std::unique_ptr<SDL_Surface, decltype(&SDL_FreeSurface)>;
+using SDLPixelFormatPtr = std::unique_ptr<SDL_PixelFormat, decltype(&SDL_FreeFormat)>;
+
+class SdlContext
+{
+ public:
+ explicit SdlContext(rdpContext* context);
+
+ private:
+ rdpContext* _context;
+
+ public:
+ wLog* log;
+
+ /* SDL */
+ bool fullscreen = false;
+ bool resizeable = false;
+ bool grab_mouse = false;
+ bool grab_kbd = false;
+
+ std::map<Uint32, SdlWindow> windows;
+
+ CriticalSection critical;
+ std::thread thread;
+ WinPREvent initialize;
+ WinPREvent initialized;
+ WinPREvent update_complete;
+ WinPREvent windows_created;
+ int exit_code = -1;
+
+ sdlDispContext disp;
+ sdlInput input;
+
+ SDLSurfacePtr primary;
+ SDLPixelFormatPtr primary_format;
+
+ Uint32 sdl_pixel_format = 0;
+
+ std::unique_ptr<SDLConnectionDialog> connection_dialog;
+
+ public:
+ BOOL update_resizeable(BOOL enable);
+ BOOL update_fullscreen(BOOL enter);
+
+ [[nodiscard]] rdpContext* context() const;
+ [[nodiscard]] rdpClientContext* common() const;
+};
diff --git a/client/SDL/sdl_kbd.cpp b/client/SDL/sdl_kbd.cpp
new file mode 100644
index 0000000..4d62389
--- /dev/null
+++ b/client/SDL/sdl_kbd.cpp
@@ -0,0 +1,568 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * FreeRDP SDL keyboard helper
+ *
+ * Copyright 2022 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "sdl_kbd.hpp"
+#include "sdl_disp.hpp"
+#include "sdl_freerdp.hpp"
+#include "sdl_utils.hpp"
+
+#include <map>
+
+#include <freerdp/scancode.h>
+
+#include <freerdp/log.h>
+#define TAG CLIENT_TAG("SDL.kbd")
+
+typedef struct
+{
+ Uint32 sdl;
+ const char* sdl_name;
+ UINT32 rdp;
+ const char* rdp_name;
+} scancode_entry_t;
+
+#define STR(x) #x
+#define ENTRY(x, y) \
+ { \
+ x, STR(x), y, #y \
+ }
+static const scancode_entry_t map[] = {
+ ENTRY(SDL_SCANCODE_UNKNOWN, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_A, RDP_SCANCODE_KEY_A),
+ ENTRY(SDL_SCANCODE_B, RDP_SCANCODE_KEY_B),
+ ENTRY(SDL_SCANCODE_C, RDP_SCANCODE_KEY_C),
+ ENTRY(SDL_SCANCODE_D, RDP_SCANCODE_KEY_D),
+ ENTRY(SDL_SCANCODE_E, RDP_SCANCODE_KEY_E),
+ ENTRY(SDL_SCANCODE_F, RDP_SCANCODE_KEY_F),
+ ENTRY(SDL_SCANCODE_G, RDP_SCANCODE_KEY_G),
+ ENTRY(SDL_SCANCODE_H, RDP_SCANCODE_KEY_H),
+ ENTRY(SDL_SCANCODE_I, RDP_SCANCODE_KEY_I),
+ ENTRY(SDL_SCANCODE_J, RDP_SCANCODE_KEY_J),
+ ENTRY(SDL_SCANCODE_K, RDP_SCANCODE_KEY_K),
+ ENTRY(SDL_SCANCODE_L, RDP_SCANCODE_KEY_L),
+ ENTRY(SDL_SCANCODE_M, RDP_SCANCODE_KEY_M),
+ ENTRY(SDL_SCANCODE_N, RDP_SCANCODE_KEY_N),
+ ENTRY(SDL_SCANCODE_O, RDP_SCANCODE_KEY_O),
+ ENTRY(SDL_SCANCODE_P, RDP_SCANCODE_KEY_P),
+ ENTRY(SDL_SCANCODE_Q, RDP_SCANCODE_KEY_Q),
+ ENTRY(SDL_SCANCODE_R, RDP_SCANCODE_KEY_R),
+ ENTRY(SDL_SCANCODE_S, RDP_SCANCODE_KEY_S),
+ ENTRY(SDL_SCANCODE_T, RDP_SCANCODE_KEY_T),
+ ENTRY(SDL_SCANCODE_U, RDP_SCANCODE_KEY_U),
+ ENTRY(SDL_SCANCODE_V, RDP_SCANCODE_KEY_V),
+ ENTRY(SDL_SCANCODE_W, RDP_SCANCODE_KEY_W),
+ ENTRY(SDL_SCANCODE_X, RDP_SCANCODE_KEY_X),
+ ENTRY(SDL_SCANCODE_Y, RDP_SCANCODE_KEY_Y),
+ ENTRY(SDL_SCANCODE_Z, RDP_SCANCODE_KEY_Z),
+ ENTRY(SDL_SCANCODE_1, RDP_SCANCODE_KEY_1),
+ ENTRY(SDL_SCANCODE_2, RDP_SCANCODE_KEY_2),
+ ENTRY(SDL_SCANCODE_3, RDP_SCANCODE_KEY_3),
+ ENTRY(SDL_SCANCODE_4, RDP_SCANCODE_KEY_4),
+ ENTRY(SDL_SCANCODE_5, RDP_SCANCODE_KEY_5),
+ ENTRY(SDL_SCANCODE_6, RDP_SCANCODE_KEY_6),
+ ENTRY(SDL_SCANCODE_7, RDP_SCANCODE_KEY_7),
+ ENTRY(SDL_SCANCODE_8, RDP_SCANCODE_KEY_8),
+ ENTRY(SDL_SCANCODE_9, RDP_SCANCODE_KEY_9),
+ ENTRY(SDL_SCANCODE_0, RDP_SCANCODE_KEY_0),
+ ENTRY(SDL_SCANCODE_RETURN, RDP_SCANCODE_RETURN),
+ ENTRY(SDL_SCANCODE_ESCAPE, RDP_SCANCODE_ESCAPE),
+ ENTRY(SDL_SCANCODE_BACKSPACE, RDP_SCANCODE_BACKSPACE),
+ ENTRY(SDL_SCANCODE_TAB, RDP_SCANCODE_TAB),
+ ENTRY(SDL_SCANCODE_SPACE, RDP_SCANCODE_SPACE),
+ ENTRY(SDL_SCANCODE_MINUS, RDP_SCANCODE_OEM_MINUS),
+ ENTRY(SDL_SCANCODE_CAPSLOCK, RDP_SCANCODE_CAPSLOCK),
+ ENTRY(SDL_SCANCODE_F1, RDP_SCANCODE_F1),
+ ENTRY(SDL_SCANCODE_F2, RDP_SCANCODE_F2),
+ ENTRY(SDL_SCANCODE_F3, RDP_SCANCODE_F3),
+ ENTRY(SDL_SCANCODE_F4, RDP_SCANCODE_F4),
+ ENTRY(SDL_SCANCODE_F5, RDP_SCANCODE_F5),
+ ENTRY(SDL_SCANCODE_F6, RDP_SCANCODE_F6),
+ ENTRY(SDL_SCANCODE_F7, RDP_SCANCODE_F7),
+ ENTRY(SDL_SCANCODE_F8, RDP_SCANCODE_F8),
+ ENTRY(SDL_SCANCODE_F9, RDP_SCANCODE_F9),
+ ENTRY(SDL_SCANCODE_F10, RDP_SCANCODE_F10),
+ ENTRY(SDL_SCANCODE_F11, RDP_SCANCODE_F11),
+ ENTRY(SDL_SCANCODE_F12, RDP_SCANCODE_F12),
+ ENTRY(SDL_SCANCODE_F13, RDP_SCANCODE_F13),
+ ENTRY(SDL_SCANCODE_F14, RDP_SCANCODE_F14),
+ ENTRY(SDL_SCANCODE_F15, RDP_SCANCODE_F15),
+ ENTRY(SDL_SCANCODE_F16, RDP_SCANCODE_F16),
+ ENTRY(SDL_SCANCODE_F17, RDP_SCANCODE_F17),
+ ENTRY(SDL_SCANCODE_F18, RDP_SCANCODE_F18),
+ ENTRY(SDL_SCANCODE_F19, RDP_SCANCODE_F19),
+ ENTRY(SDL_SCANCODE_F20, RDP_SCANCODE_F20),
+ ENTRY(SDL_SCANCODE_F21, RDP_SCANCODE_F21),
+ ENTRY(SDL_SCANCODE_F22, RDP_SCANCODE_F22),
+ ENTRY(SDL_SCANCODE_F23, RDP_SCANCODE_F23),
+ ENTRY(SDL_SCANCODE_F24, RDP_SCANCODE_F24),
+ ENTRY(SDL_SCANCODE_NUMLOCKCLEAR, RDP_SCANCODE_NUMLOCK),
+ ENTRY(SDL_SCANCODE_KP_DIVIDE, RDP_SCANCODE_DIVIDE),
+ ENTRY(SDL_SCANCODE_KP_MULTIPLY, RDP_SCANCODE_MULTIPLY),
+ ENTRY(SDL_SCANCODE_KP_MINUS, RDP_SCANCODE_SUBTRACT),
+ ENTRY(SDL_SCANCODE_KP_PLUS, RDP_SCANCODE_ADD),
+ ENTRY(SDL_SCANCODE_KP_ENTER, RDP_SCANCODE_RETURN_KP),
+ ENTRY(SDL_SCANCODE_KP_1, RDP_SCANCODE_NUMPAD1),
+ ENTRY(SDL_SCANCODE_KP_2, RDP_SCANCODE_NUMPAD2),
+ ENTRY(SDL_SCANCODE_KP_3, RDP_SCANCODE_NUMPAD3),
+ ENTRY(SDL_SCANCODE_KP_4, RDP_SCANCODE_NUMPAD4),
+ ENTRY(SDL_SCANCODE_KP_5, RDP_SCANCODE_NUMPAD5),
+ ENTRY(SDL_SCANCODE_KP_6, RDP_SCANCODE_NUMPAD6),
+ ENTRY(SDL_SCANCODE_KP_7, RDP_SCANCODE_NUMPAD7),
+ ENTRY(SDL_SCANCODE_KP_8, RDP_SCANCODE_NUMPAD8),
+ ENTRY(SDL_SCANCODE_KP_9, RDP_SCANCODE_NUMPAD9),
+ ENTRY(SDL_SCANCODE_KP_0, RDP_SCANCODE_NUMPAD0),
+ ENTRY(SDL_SCANCODE_KP_PERIOD, RDP_SCANCODE_OEM_PERIOD),
+ ENTRY(SDL_SCANCODE_LCTRL, RDP_SCANCODE_LCONTROL),
+ ENTRY(SDL_SCANCODE_LSHIFT, RDP_SCANCODE_LSHIFT),
+ ENTRY(SDL_SCANCODE_LALT, RDP_SCANCODE_LMENU),
+ ENTRY(SDL_SCANCODE_LGUI, RDP_SCANCODE_LWIN),
+ ENTRY(SDL_SCANCODE_RCTRL, RDP_SCANCODE_RCONTROL),
+ ENTRY(SDL_SCANCODE_RSHIFT, RDP_SCANCODE_RSHIFT),
+ ENTRY(SDL_SCANCODE_RALT, RDP_SCANCODE_RMENU),
+ ENTRY(SDL_SCANCODE_RGUI, RDP_SCANCODE_RWIN),
+ ENTRY(SDL_SCANCODE_MODE, RDP_SCANCODE_APPS),
+ ENTRY(SDL_SCANCODE_MUTE, RDP_SCANCODE_VOLUME_MUTE),
+ ENTRY(SDL_SCANCODE_VOLUMEUP, RDP_SCANCODE_VOLUME_UP),
+ ENTRY(SDL_SCANCODE_VOLUMEDOWN, RDP_SCANCODE_VOLUME_DOWN),
+ ENTRY(SDL_SCANCODE_GRAVE, RDP_SCANCODE_OEM_3),
+ ENTRY(SDL_SCANCODE_COMMA, RDP_SCANCODE_OEM_COMMA),
+ ENTRY(SDL_SCANCODE_PERIOD, RDP_SCANCODE_OEM_PERIOD),
+ ENTRY(SDL_SCANCODE_SLASH, RDP_SCANCODE_OEM_2),
+ ENTRY(SDL_SCANCODE_BACKSLASH, RDP_SCANCODE_OEM_5),
+ ENTRY(SDL_SCANCODE_SCROLLLOCK, RDP_SCANCODE_SCROLLLOCK),
+ ENTRY(SDL_SCANCODE_INSERT, RDP_SCANCODE_INSERT),
+ ENTRY(SDL_SCANCODE_PRINTSCREEN, RDP_SCANCODE_PRINTSCREEN),
+ ENTRY(SDL_SCANCODE_HOME, RDP_SCANCODE_HOME),
+ ENTRY(SDL_SCANCODE_DELETE, RDP_SCANCODE_DELETE),
+ ENTRY(SDL_SCANCODE_RIGHT, RDP_SCANCODE_RIGHT),
+ ENTRY(SDL_SCANCODE_LEFT, RDP_SCANCODE_LEFT),
+ ENTRY(SDL_SCANCODE_DOWN, RDP_SCANCODE_DOWN),
+ ENTRY(SDL_SCANCODE_UP, RDP_SCANCODE_UP),
+ ENTRY(SDL_SCANCODE_SEMICOLON, RDP_SCANCODE_OEM_1),
+ ENTRY(SDL_SCANCODE_PAUSE, RDP_SCANCODE_PAUSE),
+ ENTRY(SDL_SCANCODE_PAGEUP, RDP_SCANCODE_PRIOR),
+ ENTRY(SDL_SCANCODE_END, RDP_SCANCODE_END),
+ ENTRY(SDL_SCANCODE_PAGEDOWN, RDP_SCANCODE_NEXT),
+ ENTRY(SDL_SCANCODE_AUDIONEXT, RDP_SCANCODE_MEDIA_NEXT_TRACK),
+ ENTRY(SDL_SCANCODE_AUDIOPREV, RDP_SCANCODE_MEDIA_PREV_TRACK),
+ ENTRY(SDL_SCANCODE_AUDIOSTOP, RDP_SCANCODE_MEDIA_STOP),
+ ENTRY(SDL_SCANCODE_AUDIOPLAY, RDP_SCANCODE_MEDIA_PLAY_PAUSE),
+ ENTRY(SDL_SCANCODE_AUDIOMUTE, RDP_SCANCODE_VOLUME_MUTE),
+ ENTRY(SDL_SCANCODE_MEDIASELECT, RDP_SCANCODE_LAUNCH_MEDIA_SELECT),
+ ENTRY(SDL_SCANCODE_MAIL, RDP_SCANCODE_LAUNCH_MAIL),
+ ENTRY(SDL_SCANCODE_APP1, RDP_SCANCODE_LAUNCH_APP1),
+ ENTRY(SDL_SCANCODE_APP2, RDP_SCANCODE_LAUNCH_APP2),
+ ENTRY(SDL_SCANCODE_SYSREQ, RDP_SCANCODE_SYSREQ),
+ ENTRY(SDL_SCANCODE_WWW, RDP_SCANCODE_BROWSER_HOME),
+ ENTRY(SDL_SCANCODE_LEFTBRACKET, RDP_SCANCODE_OEM_4),
+ ENTRY(SDL_SCANCODE_RIGHTBRACKET, RDP_SCANCODE_OEM_6),
+ ENTRY(SDL_SCANCODE_APOSTROPHE, RDP_SCANCODE_OEM_7),
+ ENTRY(SDL_SCANCODE_NONUSBACKSLASH, RDP_SCANCODE_OEM_102),
+ ENTRY(SDL_SCANCODE_SLEEP, RDP_SCANCODE_SLEEP),
+ ENTRY(SDL_SCANCODE_EQUALS, RDP_SCANCODE_OEM_PLUS),
+ ENTRY(SDL_SCANCODE_KP_COMMA, RDP_SCANCODE_DECIMAL),
+ ENTRY(SDL_SCANCODE_FIND, RDP_SCANCODE_BROWSER_SEARCH),
+ ENTRY(SDL_SCANCODE_RETURN2, RDP_SCANCODE_RETURN_KP),
+ ENTRY(SDL_SCANCODE_AC_SEARCH, RDP_SCANCODE_BROWSER_SEARCH),
+ ENTRY(SDL_SCANCODE_AC_HOME, RDP_SCANCODE_BROWSER_HOME),
+ ENTRY(SDL_SCANCODE_AC_BACK, RDP_SCANCODE_BROWSER_BACK),
+ ENTRY(SDL_SCANCODE_AC_FORWARD, RDP_SCANCODE_BROWSER_FORWARD),
+ ENTRY(SDL_SCANCODE_AC_STOP, RDP_SCANCODE_BROWSER_STOP),
+
+#if 1 // TODO: unmapped
+ ENTRY(SDL_SCANCODE_NONUSHASH, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_APPLICATION, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_POWER, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_EQUALS, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_EXECUTE, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_HELP, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_MENU, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_SELECT, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_STOP, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_AGAIN, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_UNDO, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_CUT, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_COPY, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_PASTE, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_EQUALSAS400, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_INTERNATIONAL1, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_INTERNATIONAL2, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_INTERNATIONAL3, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_INTERNATIONAL4, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_INTERNATIONAL5, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_INTERNATIONAL6, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_INTERNATIONAL7, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_INTERNATIONAL8, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_INTERNATIONAL9, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_LANG1, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_LANG2, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_LANG3, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_LANG4, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_LANG5, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_LANG6, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_LANG7, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_LANG8, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_LANG9, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_ALTERASE, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_CANCEL, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_CLEAR, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_PRIOR, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_SEPARATOR, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_OUT, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_OPER, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_CLEARAGAIN, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_CRSEL, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_EXSEL, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_00, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_000, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_THOUSANDSSEPARATOR, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_DECIMALSEPARATOR, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_CURRENCYUNIT, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_CURRENCYSUBUNIT, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_LEFTPAREN, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_RIGHTPAREN, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_LEFTBRACE, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_RIGHTBRACE, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_TAB, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_BACKSPACE, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_A, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_B, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_C, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_D, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_E, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_F, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_XOR, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_POWER, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_PERCENT, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_LESS, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_GREATER, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_AMPERSAND, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_DBLAMPERSAND, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_VERTICALBAR, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_DBLVERTICALBAR, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_COLON, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_HASH, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_SPACE, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_AT, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_EXCLAM, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_MEMSTORE, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_MEMRECALL, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_MEMCLEAR, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_MEMADD, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_MEMSUBTRACT, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_MEMMULTIPLY, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_MEMDIVIDE, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_PLUSMINUS, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_CLEAR, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_CLEARENTRY, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_BINARY, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_OCTAL, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_DECIMAL, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KP_HEXADECIMAL, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_CALCULATOR, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_COMPUTER, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_AC_REFRESH, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_AC_BOOKMARKS, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_BRIGHTNESSDOWN, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_BRIGHTNESSUP, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_DISPLAYSWITCH, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KBDILLUMTOGGLE, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KBDILLUMDOWN, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_KBDILLUMUP, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_EJECT, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_AUDIOREWIND, RDP_SCANCODE_UNKNOWN),
+ ENTRY(SDL_SCANCODE_AUDIOFASTFORWARD, RDP_SCANCODE_UNKNOWN)
+#endif
+};
+
+static UINT32 sdl_get_kbd_flags(void)
+{
+ UINT32 flags = 0;
+
+ SDL_Keymod mod = SDL_GetModState();
+ if ((mod & KMOD_NUM) != 0)
+ flags |= KBD_SYNC_NUM_LOCK;
+ if ((mod & KMOD_CAPS) != 0)
+ flags |= KBD_SYNC_CAPS_LOCK;
+#if SDL_VERSION_ATLEAST(2, 0, 18)
+ if ((mod & KMOD_SCROLL) != 0)
+ flags |= KBD_SYNC_SCROLL_LOCK;
+#endif
+
+ // TODO: KBD_SYNC_KANA_LOCK
+
+ return flags;
+}
+
+BOOL sdlInput::keyboard_sync_state()
+{
+ const UINT32 syncFlags = sdl_get_kbd_flags();
+ return freerdp_input_send_synchronize_event(_sdl->context()->input, syncFlags);
+}
+
+BOOL sdlInput::keyboard_focus_in()
+{
+ auto input = _sdl->context()->input;
+ WINPR_ASSERT(input);
+
+ auto syncFlags = sdl_get_kbd_flags();
+ freerdp_input_send_focus_in_event(input, syncFlags);
+
+ /* finish with a mouse pointer position like mstsc.exe if required */
+#if 0
+ if (xfc->remote_app)
+ return;
+
+ if (XQueryPointer(xfc->display, xfc->window->handle, &w, &w, &d, &d, &x, &y, &state))
+ {
+ if ((x >= 0) && (x < xfc->window->width) && (y >= 0) && (y < xfc->window->height))
+ {
+ xf_event_adjust_coordinates(xfc, &x, &y);
+ freerdp_client_send_button_event(&xfc->common, FALSE, PTR_FLAGS_MOVE, x, y);
+ }
+ }
+#endif
+ return TRUE;
+}
+
+/* This function is called to update the keyboard indicator LED */
+BOOL sdlInput::keyboard_set_indicators(rdpContext* context, UINT16 led_flags)
+{
+ WINPR_UNUSED(context);
+
+ int state = KMOD_NONE;
+
+ if ((led_flags & KBD_SYNC_NUM_LOCK) != 0)
+ state |= KMOD_NUM;
+ if ((led_flags & KBD_SYNC_CAPS_LOCK) != 0)
+ state |= KMOD_CAPS;
+#if SDL_VERSION_ATLEAST(2, 0, 18)
+ if ((led_flags & KBD_SYNC_SCROLL_LOCK) != 0)
+ state |= KMOD_SCROLL;
+#endif
+
+ // TODO: KBD_SYNC_KANA_LOCK
+
+ SDL_SetModState(static_cast<SDL_Keymod>(state));
+
+ return TRUE;
+}
+
+/* This function is called to set the IME state */
+BOOL sdlInput::keyboard_set_ime_status(rdpContext* context, UINT16 imeId, UINT32 imeState,
+ UINT32 imeConvMode)
+{
+ if (!context)
+ return FALSE;
+
+ WLog_WARN(TAG,
+ "KeyboardSetImeStatus(unitId=%04" PRIx16 ", imeState=%08" PRIx32
+ ", imeConvMode=%08" PRIx32 ") ignored",
+ imeId, imeState, imeConvMode);
+ return TRUE;
+}
+
+uint32_t sdlInput::prefToMask()
+{
+ const std::map<std::string, SDL_Keymod> mapping = {
+ { "KMOD_LSHIFT", KMOD_LSHIFT },
+ { "KMOD_RSHIFT", KMOD_RSHIFT },
+ { "KMOD_LCTRL", KMOD_LCTRL },
+ { "KMOD_RCTRL", KMOD_RCTRL },
+ { "KMOD_LALT", KMOD_LALT },
+ { "KMOD_RALT", KMOD_RALT },
+ { "KMOD_LGUI", KMOD_LGUI },
+ { "KMOD_RGUI", KMOD_RGUI },
+ { "KMOD_NUM", KMOD_NUM },
+ { "KMOD_CAPS", KMOD_CAPS },
+ { "KMOD_MODE", KMOD_MODE },
+#if SDL_VERSION_ATLEAST(2, 0, 18)
+ { "KMOD_SCROLL", KMOD_SCROLL },
+#endif
+ { "KMOD_CTRL", KMOD_CTRL },
+ { "KMOD_SHIFT", KMOD_SHIFT },
+ { "KMOD_ALT", KMOD_ALT },
+ { "KMOD_GUI", KMOD_GUI }
+ };
+ uint32_t mod = KMOD_NONE;
+ for (const auto& val : sdl_get_pref_array("SDL_KeyModMask", { "KMOD_RSHIFT" }))
+ {
+ auto it = mapping.find(val);
+ if (it != mapping.end())
+ {
+ mod |= it->second;
+ }
+ }
+ return mod;
+}
+
+static const char* sdl_scancode_name(Uint32 scancode)
+{
+ for (const auto& cur : map)
+ {
+ if (cur.sdl == scancode)
+ return cur.sdl_name;
+ }
+
+ return "SDL_SCANCODE_UNKNOWN";
+}
+
+static Uint32 sdl_scancode_val(const char* scancodeName)
+{
+ for (const auto& cur : map)
+ {
+ if (strcmp(cur.sdl_name, scancodeName) == 0)
+ return cur.sdl;
+ }
+
+ return SDL_SCANCODE_UNKNOWN;
+}
+
+static const char* sdl_rdp_scancode_name(UINT32 scancode)
+{
+ for (const auto& cur : map)
+ {
+ if (cur.rdp == scancode)
+ return cur.rdp_name;
+ }
+
+ return "RDP_SCANCODE_UNKNOWN";
+}
+
+static UINT32 sdl_rdp_scancode_val(const char* scancodeName)
+{
+ for (const auto& cur : map)
+ {
+ if (strcmp(cur.rdp_name, scancodeName) == 0)
+ return cur.rdp;
+ }
+
+ return RDP_SCANCODE_UNKNOWN;
+}
+
+static UINT32 sdl_scancode_to_rdp(Uint32 scancode)
+{
+ UINT32 rdp = RDP_SCANCODE_UNKNOWN;
+
+ for (const auto& cur : map)
+ {
+ if (cur.sdl == scancode)
+ {
+ rdp = cur.rdp;
+ break;
+ }
+ }
+
+#if defined(WITH_DEBUG_SDL_KBD_EVENTS)
+ auto code = static_cast<SDL_Scancode>(scancode);
+ WLog_DBG(TAG, "got %s [%s] -> [%s]", SDL_GetScancodeName(code), sdl_scancode_name(scancode),
+ sdl_rdp_scancode_name(rdp));
+#endif
+ return rdp;
+}
+
+uint32_t sdlInput::prefKeyValue(const std::string& key, uint32_t fallback)
+{
+ auto item = sdl_get_pref_string(key);
+ if (item.empty())
+ return fallback;
+ auto val = sdl_scancode_val(item.c_str());
+ if (val == SDL_SCANCODE_UNKNOWN)
+ return fallback;
+ return val;
+}
+
+BOOL sdlInput::keyboard_handle_event(const SDL_KeyboardEvent* ev)
+{
+ WINPR_ASSERT(ev);
+ const UINT32 rdp_scancode = sdl_scancode_to_rdp(ev->keysym.scancode);
+ const SDL_Keymod mods = SDL_GetModState();
+ const auto mask = prefToMask();
+ const auto valFullscreen = prefKeyValue("SDL_Fullscreen", SDL_SCANCODE_RETURN);
+ const auto valResizeable = prefKeyValue("SDL_Resizeable", SDL_SCANCODE_R);
+ const auto valGrab = prefKeyValue("SDL_Grab", SDL_SCANCODE_G);
+ const auto valDisconnect = prefKeyValue("SDL_Disconnect", SDL_SCANCODE_D);
+
+ if ((mods & mask) == mask)
+ {
+ if (ev->type == SDL_KEYDOWN)
+ {
+ if (ev->keysym.scancode == valFullscreen)
+ {
+ _sdl->update_fullscreen(!_sdl->fullscreen);
+ return TRUE;
+ }
+ if (ev->keysym.scancode == valResizeable)
+ {
+ _sdl->update_resizeable(!_sdl->resizeable);
+ return TRUE;
+ }
+
+ if (ev->keysym.scancode == valGrab)
+ {
+ keyboard_grab(ev->windowID, _sdl->grab_kbd ? SDL_FALSE : SDL_TRUE);
+ return TRUE;
+ }
+ if (ev->keysym.scancode == valDisconnect)
+ {
+ freerdp_abort_connect_context(_sdl->context());
+ return TRUE;
+ }
+ }
+ }
+ return freerdp_input_send_keyboard_event_ex(_sdl->context()->input, ev->type == SDL_KEYDOWN,
+ ev->repeat, rdp_scancode);
+}
+
+BOOL sdlInput::keyboard_grab(Uint32 windowID, SDL_bool enable)
+{
+ auto it = _sdl->windows.find(windowID);
+ if (it == _sdl->windows.end())
+ return FALSE;
+ _sdl->grab_kbd = enable;
+ return it->second.grabKeyboard(enable);
+}
+
+BOOL sdlInput::mouse_focus(Uint32 windowID)
+{
+ if (_lastWindowID != windowID)
+ {
+ _lastWindowID = windowID;
+ auto it = _sdl->windows.find(windowID);
+ if (it == _sdl->windows.end())
+ return FALSE;
+
+ it->second.raise();
+ }
+ return TRUE;
+}
+
+BOOL sdlInput::mouse_grab(Uint32 windowID, SDL_bool enable)
+{
+ auto it = _sdl->windows.find(windowID);
+ if (it == _sdl->windows.end())
+ return FALSE;
+ _sdl->grab_mouse = enable;
+ return it->second.grabMouse(enable);
+}
+
+sdlInput::sdlInput(SdlContext* sdl) : _sdl(sdl), _lastWindowID(UINT32_MAX)
+{
+ WINPR_ASSERT(_sdl);
+}
diff --git a/client/SDL/sdl_kbd.hpp b/client/SDL/sdl_kbd.hpp
new file mode 100644
index 0000000..2a6c7fa
--- /dev/null
+++ b/client/SDL/sdl_kbd.hpp
@@ -0,0 +1,56 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * SDL Client keyboard helper
+ *
+ * Copyright 2022 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <string>
+
+#include <winpr/wtypes.h>
+#include <freerdp/freerdp.h>
+#include <SDL.h>
+
+#include "sdl_types.hpp"
+
+class sdlInput
+{
+ public:
+ explicit sdlInput(SdlContext* sdl);
+ ~sdlInput() = default;
+
+ BOOL keyboard_sync_state();
+ BOOL keyboard_focus_in();
+
+ BOOL keyboard_handle_event(const SDL_KeyboardEvent* ev);
+
+ BOOL keyboard_grab(Uint32 windowID, SDL_bool enable);
+ BOOL mouse_focus(Uint32 windowID);
+ BOOL mouse_grab(Uint32 windowID, SDL_bool enable);
+
+ public:
+ static BOOL keyboard_set_indicators(rdpContext* context, UINT16 led_flags);
+ static BOOL keyboard_set_ime_status(rdpContext* context, UINT16 imeId, UINT32 imeState,
+ UINT32 imeConvMode);
+
+ static uint32_t prefToMask();
+ static uint32_t prefKeyValue(const std::string& key, uint32_t fallback = SDL_SCANCODE_UNKNOWN);
+
+ private:
+ SdlContext* _sdl;
+ Uint32 _lastWindowID;
+};
diff --git a/client/SDL/sdl_monitor.cpp b/client/SDL/sdl_monitor.cpp
new file mode 100644
index 0000000..e637b48
--- /dev/null
+++ b/client/SDL/sdl_monitor.cpp
@@ -0,0 +1,331 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * X11 Monitor Handling
+ *
+ * Copyright 2011 Marc-Andre Moreau <marcandre.moreau@gmail.com>
+ * Copyright 2017 David Fort <contact@hardening-consulting.com>
+ * Copyright 2018 Kai Harms <kharms@rangee.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <freerdp/config.h>
+
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+
+#include <SDL.h>
+
+#include <winpr/assert.h>
+#include <winpr/crt.h>
+
+#include <freerdp/log.h>
+
+#define TAG CLIENT_TAG("sdl")
+
+#include "sdl_monitor.hpp"
+#include "sdl_freerdp.hpp"
+
+typedef struct
+{
+ RECTANGLE_16 area;
+ RECTANGLE_16 workarea;
+ BOOL primary;
+} MONITOR_INFO;
+
+typedef struct
+{
+ int nmonitors;
+ RECTANGLE_16 area;
+ RECTANGLE_16 workarea;
+ MONITOR_INFO* monitors;
+} VIRTUAL_SCREEN;
+
+/* See MSDN Section on Multiple Display Monitors: http://msdn.microsoft.com/en-us/library/dd145071
+ */
+
+int sdl_list_monitors(SdlContext* sdl)
+{
+ SDL_Init(SDL_INIT_VIDEO);
+ const int nmonitors = SDL_GetNumVideoDisplays();
+
+ printf("listing %d monitors:\n", nmonitors);
+ for (int i = 0; i < nmonitors; i++)
+ {
+ SDL_Rect rect = {};
+ const int brc = SDL_GetDisplayBounds(i, &rect);
+ const char* name = SDL_GetDisplayName(i);
+
+ if (brc != 0)
+ continue;
+ printf(" %s [%d] [%s] %dx%d\t+%d+%d\n", (i == 0) ? "*" : " ", i, name, rect.w, rect.h,
+ rect.x, rect.y);
+ }
+
+ SDL_Quit();
+ return 0;
+}
+
+static BOOL sdl_is_monitor_id_active(SdlContext* sdl, UINT32 id)
+{
+ const rdpSettings* settings = nullptr;
+
+ WINPR_ASSERT(sdl);
+
+ settings = sdl->context()->settings;
+ WINPR_ASSERT(settings);
+
+ const UINT32 NumMonitorIds = freerdp_settings_get_uint32(settings, FreeRDP_NumMonitorIds);
+ if (!NumMonitorIds)
+ return TRUE;
+
+ for (UINT32 index = 0; index < NumMonitorIds; index++)
+ {
+ auto cur = static_cast<const UINT32*>(
+ freerdp_settings_get_pointer_array(settings, FreeRDP_MonitorIds, index));
+ if (cur && (*cur == id))
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static BOOL sdl_apply_max_size(SdlContext* sdl, UINT32* pMaxWidth, UINT32* pMaxHeight)
+{
+ WINPR_ASSERT(sdl);
+ WINPR_ASSERT(pMaxWidth);
+ WINPR_ASSERT(pMaxHeight);
+
+ auto settings = sdl->context()->settings;
+ WINPR_ASSERT(settings);
+
+ *pMaxWidth = 0;
+ *pMaxHeight = 0;
+
+ for (size_t x = 0; x < freerdp_settings_get_uint32(settings, FreeRDP_MonitorCount); x++)
+ {
+ auto monitor = static_cast<const rdpMonitor*>(
+ freerdp_settings_get_pointer_array(settings, FreeRDP_MonitorDefArray, x));
+
+ if (freerdp_settings_get_bool(settings, FreeRDP_Fullscreen))
+ {
+ *pMaxWidth = monitor->width;
+ *pMaxHeight = monitor->height;
+ }
+ else if (freerdp_settings_get_bool(settings, FreeRDP_Workarea))
+ {
+ SDL_Rect rect = {};
+ SDL_GetDisplayUsableBounds(monitor->orig_screen, &rect);
+ *pMaxWidth = rect.w;
+ *pMaxHeight = rect.h;
+ }
+ else if (freerdp_settings_get_uint32(settings, FreeRDP_PercentScreen) > 0)
+ {
+ SDL_Rect rect = {};
+ SDL_GetDisplayUsableBounds(monitor->orig_screen, &rect);
+
+ *pMaxWidth = rect.w;
+ *pMaxHeight = rect.h;
+
+ if (freerdp_settings_get_bool(settings, FreeRDP_PercentScreenUseWidth))
+ *pMaxWidth =
+ (rect.w * freerdp_settings_get_uint32(settings, FreeRDP_PercentScreen)) / 100;
+
+ if (freerdp_settings_get_bool(settings, FreeRDP_PercentScreenUseHeight))
+ *pMaxHeight =
+ (rect.h * freerdp_settings_get_uint32(settings, FreeRDP_PercentScreen)) / 100;
+ }
+ else if (freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth) &&
+ freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight))
+ {
+ *pMaxWidth = freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth);
+ *pMaxHeight = freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight);
+ }
+ }
+ return TRUE;
+}
+
+#if SDL_VERSION_ATLEAST(2, 0, 10)
+static UINT32 sdl_orientaion_to_rdp(SDL_DisplayOrientation orientation)
+{
+ switch (orientation)
+ {
+ case SDL_ORIENTATION_LANDSCAPE:
+ return ORIENTATION_LANDSCAPE;
+ case SDL_ORIENTATION_LANDSCAPE_FLIPPED:
+ return ORIENTATION_LANDSCAPE_FLIPPED;
+ case SDL_ORIENTATION_PORTRAIT_FLIPPED:
+ return ORIENTATION_PORTRAIT_FLIPPED;
+ case SDL_ORIENTATION_PORTRAIT:
+ default:
+ return ORIENTATION_PORTRAIT;
+ }
+}
+#endif
+
+static BOOL sdl_apply_display_properties(SdlContext* sdl)
+{
+ WINPR_ASSERT(sdl);
+
+ rdpSettings* settings = sdl->context()->settings;
+ WINPR_ASSERT(settings);
+
+ const UINT32 numIds = freerdp_settings_get_uint32(settings, FreeRDP_NumMonitorIds);
+ if (!freerdp_settings_set_pointer_len(settings, FreeRDP_MonitorDefArray, nullptr, numIds))
+ return FALSE;
+ if (!freerdp_settings_set_uint32(settings, FreeRDP_MonitorCount, numIds))
+ return FALSE;
+
+ for (UINT32 x = 0; x < numIds; x++)
+ {
+ auto id = static_cast<const UINT32*>(
+ freerdp_settings_get_pointer_array(settings, FreeRDP_MonitorIds, x));
+ WINPR_ASSERT(id);
+
+ float ddpi = 1.0f;
+ float hdpi = 1.0f;
+ float vdpi = 1.0f;
+ SDL_Rect rect = {};
+
+ SDL_GetDisplayBounds(*id, &rect);
+ SDL_GetDisplayDPI(*id, &ddpi, &hdpi, &vdpi);
+
+ bool highDpi = hdpi > 100;
+
+ if (highDpi)
+ {
+ // HighDPI is problematic with SDL: We can only get native resolution by creating a
+ // window. Work around this by checking the supported resolutions (and keep maximum)
+ // Also scale the DPI
+ const SDL_Rect scaleRect = rect;
+ for (int i = 0; i < SDL_GetNumDisplayModes(*id); i++)
+ {
+ SDL_DisplayMode mode = {};
+ SDL_GetDisplayMode(x, i, &mode);
+
+ if (mode.w > rect.w)
+ {
+ rect.w = mode.w;
+ rect.h = mode.h;
+ }
+ else if (mode.w == rect.w)
+ {
+ if (mode.h > rect.h)
+ {
+ rect.w = mode.w;
+ rect.h = mode.h;
+ }
+ }
+ }
+
+ const float dw = 1.0f * rect.w / scaleRect.w;
+ const float dh = 1.0f * rect.h / scaleRect.h;
+ hdpi /= dw;
+ vdpi /= dh;
+ }
+
+#if SDL_VERSION_ATLEAST(2, 0, 10)
+ const SDL_DisplayOrientation orientation = SDL_GetDisplayOrientation(*id);
+ const UINT32 rdp_orientation = sdl_orientaion_to_rdp(orientation);
+#else
+ const UINT32 rdp_orientation = ORIENTATION_LANDSCAPE;
+#endif
+
+ auto monitor = static_cast<rdpMonitor*>(
+ freerdp_settings_get_pointer_array_writable(settings, FreeRDP_MonitorDefArray, x));
+ WINPR_ASSERT(monitor);
+
+ /* windows uses 96 dpi as 'default' and the scale factors are in percent. */
+ const auto factor = ddpi / 96.0f * 100.0f;
+ monitor->orig_screen = x;
+ monitor->x = rect.x;
+ monitor->y = rect.y;
+ monitor->width = rect.w;
+ monitor->height = rect.h;
+ monitor->is_primary = x == 0;
+ monitor->attributes.desktopScaleFactor = factor;
+ monitor->attributes.deviceScaleFactor = 100;
+ monitor->attributes.orientation = rdp_orientation;
+ monitor->attributes.physicalWidth = rect.w / hdpi;
+ monitor->attributes.physicalHeight = rect.h / vdpi;
+ }
+ return TRUE;
+}
+
+static BOOL sdl_detect_single_window(SdlContext* sdl, UINT32* pMaxWidth, UINT32* pMaxHeight)
+{
+ WINPR_ASSERT(sdl);
+ WINPR_ASSERT(pMaxWidth);
+ WINPR_ASSERT(pMaxHeight);
+
+ rdpSettings* settings = sdl->context()->settings;
+ WINPR_ASSERT(settings);
+
+ if ((!freerdp_settings_get_bool(settings, FreeRDP_UseMultimon) &&
+ !freerdp_settings_get_bool(settings, FreeRDP_SpanMonitors)) ||
+ (freerdp_settings_get_bool(settings, FreeRDP_Workarea) &&
+ !freerdp_settings_get_bool(settings, FreeRDP_RemoteApplicationMode)))
+ {
+ /* If no monitors were specified on the command-line then set the current monitor as active
+ */
+ if (freerdp_settings_get_uint32(settings, FreeRDP_NumMonitorIds) == 0)
+ {
+ const size_t id =
+ (sdl->windows.size() > 0) ? sdl->windows.begin()->second.displayIndex() : 0;
+ if (!freerdp_settings_set_pointer_len(settings, FreeRDP_MonitorIds, &id, 1))
+ return FALSE;
+ }
+ else
+ {
+
+ /* Always sets number of monitors from command-line to just 1.
+ * If the monitor is invalid then we will default back to current monitor
+ * later as a fallback. So, there is no need to validate command-line entry here.
+ */
+ if (!freerdp_settings_set_uint32(settings, FreeRDP_NumMonitorIds, 1))
+ return FALSE;
+ }
+
+ // TODO: Fill monitor struct
+ if (!sdl_apply_display_properties(sdl))
+ return FALSE;
+ return sdl_apply_max_size(sdl, pMaxWidth, pMaxHeight);
+ }
+ return TRUE;
+}
+
+BOOL sdl_detect_monitors(SdlContext* sdl, UINT32* pMaxWidth, UINT32* pMaxHeight)
+{
+ WINPR_ASSERT(sdl);
+ WINPR_ASSERT(pMaxWidth);
+ WINPR_ASSERT(pMaxHeight);
+
+ rdpSettings* settings = sdl->context()->settings;
+ WINPR_ASSERT(settings);
+
+ const int numDisplays = SDL_GetNumVideoDisplays();
+ if (!freerdp_settings_set_pointer_len(settings, FreeRDP_MonitorIds, nullptr, numDisplays))
+ return FALSE;
+
+ for (size_t x = 0; x < numDisplays; x++)
+ {
+ if (!freerdp_settings_set_pointer_array(settings, FreeRDP_MonitorIds, x, &x))
+ return FALSE;
+ }
+
+ if (!sdl_apply_display_properties(sdl))
+ return FALSE;
+
+ return sdl_detect_single_window(sdl, pMaxWidth, pMaxHeight);
+}
diff --git a/client/SDL/sdl_monitor.hpp b/client/SDL/sdl_monitor.hpp
new file mode 100644
index 0000000..64f9f56
--- /dev/null
+++ b/client/SDL/sdl_monitor.hpp
@@ -0,0 +1,28 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * SDL Monitor Handling
+ *
+ * Copyright 2023 Armin Novak <anovak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <freerdp/api.h>
+#include <freerdp/freerdp.h>
+
+#include "sdl_types.hpp"
+
+int sdl_list_monitors(SdlContext* sdl);
+BOOL sdl_detect_monitors(SdlContext* sdl, UINT32* pWidth, UINT32* pHeight);
diff --git a/client/SDL/sdl_pointer.cpp b/client/SDL/sdl_pointer.cpp
new file mode 100644
index 0000000..ad8a4f3
--- /dev/null
+++ b/client/SDL/sdl_pointer.cpp
@@ -0,0 +1,197 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * Wayland Mouse Pointer
+ *
+ * Copyright 2023 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <freerdp/config.h>
+
+#include <freerdp/gdi/gdi.h>
+
+#include "sdl_pointer.hpp"
+#include "sdl_freerdp.hpp"
+#include "sdl_touch.hpp"
+#include "sdl_utils.hpp"
+
+#include <SDL_mouse.h>
+
+#define TAG CLIENT_TAG("SDL.pointer")
+
+typedef struct
+{
+ rdpPointer pointer;
+ SDL_Cursor* cursor;
+ SDL_Surface* image;
+ size_t size;
+ void* data;
+} sdlPointer;
+
+static BOOL sdl_Pointer_New(rdpContext* context, rdpPointer* pointer)
+{
+ auto ptr = reinterpret_cast<sdlPointer*>(pointer);
+
+ WINPR_ASSERT(context);
+ if (!ptr)
+ return FALSE;
+
+ rdpGdi* gdi = context->gdi;
+ WINPR_ASSERT(gdi);
+
+ ptr->size = 4ull * pointer->width * pointer->height;
+ ptr->data = winpr_aligned_malloc(ptr->size, 16);
+
+ if (!ptr->data)
+ return FALSE;
+
+ auto data = static_cast<BYTE*>(ptr->data);
+ if (!freerdp_image_copy_from_pointer_data(
+ data, gdi->dstFormat, 0, 0, 0, pointer->width, pointer->height, pointer->xorMaskData,
+ pointer->lengthXorMask, pointer->andMaskData, pointer->lengthAndMask, pointer->xorBpp,
+ &context->gdi->palette))
+ {
+ winpr_aligned_free(ptr->data);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static void sdl_Pointer_Clear(sdlPointer* ptr)
+{
+ WINPR_ASSERT(ptr);
+ SDL_FreeCursor(ptr->cursor);
+ SDL_FreeSurface(ptr->image);
+ ptr->cursor = nullptr;
+ ptr->image = nullptr;
+}
+
+static void sdl_Pointer_Free(rdpContext* context, rdpPointer* pointer)
+{
+ auto ptr = reinterpret_cast<sdlPointer*>(pointer);
+ WINPR_UNUSED(context);
+
+ if (ptr)
+ {
+ sdl_Pointer_Clear(ptr);
+ winpr_aligned_free(ptr->data);
+ ptr->data = nullptr;
+ }
+}
+
+static BOOL sdl_Pointer_SetDefault(rdpContext* context)
+{
+ WINPR_UNUSED(context);
+
+ return sdl_push_user_event(SDL_USEREVENT_POINTER_DEFAULT);
+}
+
+static BOOL sdl_Pointer_Set(rdpContext* context, rdpPointer* pointer)
+{
+ auto sdl = get_context(context);
+
+ return sdl_push_user_event(SDL_USEREVENT_POINTER_SET, pointer, sdl);
+}
+
+BOOL sdl_Pointer_Set_Process(SDL_UserEvent* uptr)
+{
+ INT32 w = 0;
+ INT32 h = 0;
+ INT32 x = 0;
+ INT32 y = 0;
+ INT32 sw = 0;
+ INT32 sh = 0;
+
+ WINPR_ASSERT(uptr);
+
+ auto sdl = static_cast<SdlContext*>(uptr->data2);
+ WINPR_ASSERT(sdl);
+
+ auto context = sdl->context();
+ auto ptr = static_cast<sdlPointer*>(uptr->data1);
+ WINPR_ASSERT(ptr);
+
+ rdpPointer* pointer = &ptr->pointer;
+
+ rdpGdi* gdi = context->gdi;
+ WINPR_ASSERT(gdi);
+
+ x = static_cast<INT32>(pointer->xPos);
+ y = static_cast<INT32>(pointer->yPos);
+ sw = w = static_cast<INT32>(pointer->width);
+ sh = h = static_cast<INT32>(pointer->height);
+
+ SDL_Window* window = SDL_GetMouseFocus();
+ if (!window)
+ return sdl_Pointer_SetDefault(context);
+
+ const Uint32 id = SDL_GetWindowID(window);
+
+ if (!sdl_scale_coordinates(sdl, id, &x, &y, FALSE, FALSE) ||
+ !sdl_scale_coordinates(sdl, id, &sw, &sh, FALSE, FALSE))
+ return FALSE;
+
+ sdl_Pointer_Clear(ptr);
+
+ const DWORD bpp = FreeRDPGetBitsPerPixel(gdi->dstFormat);
+ ptr->image =
+ SDL_CreateRGBSurfaceWithFormat(0, sw, sh, static_cast<int>(bpp), sdl->sdl_pixel_format);
+ if (!ptr->image)
+ return FALSE;
+
+ SDL_LockSurface(ptr->image);
+ auto pixels = static_cast<BYTE*>(ptr->image->pixels);
+ auto data = static_cast<const BYTE*>(ptr->data);
+ const BOOL rc = freerdp_image_scale(
+ pixels, gdi->dstFormat, static_cast<UINT32>(ptr->image->pitch), 0, 0,
+ static_cast<UINT32>(ptr->image->w), static_cast<UINT32>(ptr->image->h), data,
+ gdi->dstFormat, 0, 0, 0, static_cast<UINT32>(w), static_cast<UINT32>(h));
+ SDL_UnlockSurface(ptr->image);
+ if (!rc)
+ return FALSE;
+
+ ptr->cursor = SDL_CreateColorCursor(ptr->image, x, y);
+ if (!ptr->cursor)
+ return FALSE;
+
+ SDL_SetCursor(ptr->cursor);
+ SDL_ShowCursor(SDL_ENABLE);
+ return TRUE;
+}
+
+static BOOL sdl_Pointer_SetNull(rdpContext* context)
+{
+ WINPR_UNUSED(context);
+
+ return sdl_push_user_event(SDL_USEREVENT_POINTER_NULL);
+}
+
+static BOOL sdl_Pointer_SetPosition(rdpContext* context, UINT32 x, UINT32 y)
+{
+ auto sdl = get_context(context);
+ WINPR_ASSERT(sdl);
+
+ return sdl_push_user_event(SDL_USEREVENT_POINTER_POSITION, x, y);
+}
+
+BOOL sdl_register_pointer(rdpGraphics* graphics)
+{
+ const rdpPointer pointer = { sizeof(sdlPointer), sdl_Pointer_New,
+ sdl_Pointer_Free, sdl_Pointer_Set,
+ sdl_Pointer_SetNull, sdl_Pointer_SetDefault,
+ sdl_Pointer_SetPosition, 0 };
+ graphics_register_pointer(graphics, &pointer);
+ return TRUE;
+}
diff --git a/client/SDL/sdl_pointer.hpp b/client/SDL/sdl_pointer.hpp
new file mode 100644
index 0000000..006e962
--- /dev/null
+++ b/client/SDL/sdl_pointer.hpp
@@ -0,0 +1,27 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * SDL Mouse Pointer
+ *
+ * Copyright 2023 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <SDL.h>
+#include <freerdp/graphics.h>
+
+BOOL sdl_register_pointer(rdpGraphics* graphics);
+
+BOOL sdl_Pointer_Set_Process(SDL_UserEvent* uptr);
diff --git a/client/SDL/sdl_touch.cpp b/client/SDL/sdl_touch.cpp
new file mode 100644
index 0000000..81fcbfb
--- /dev/null
+++ b/client/SDL/sdl_touch.cpp
@@ -0,0 +1,285 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * FreeRDP SDL touch/mouse input
+ *
+ * Copyright 2022 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <freerdp/config.h>
+
+#include "sdl_touch.hpp"
+#include "sdl_freerdp.hpp"
+
+#include <winpr/wtypes.h>
+#include <winpr/assert.h>
+
+#include <freerdp/freerdp.h>
+#include <freerdp/gdi/gdi.h>
+
+#include <SDL.h>
+
+#define TAG CLIENT_TAG("SDL.touch")
+
+BOOL sdl_scale_coordinates(SdlContext* sdl, Uint32 windowId, INT32* px, INT32* py,
+ BOOL fromLocalToRDP, BOOL applyOffset)
+{
+ rdpGdi* gdi = nullptr;
+ double sx = 1.0;
+ double sy = 1.0;
+
+ if (!sdl || !px || !py || !sdl->context()->gdi)
+ return FALSE;
+
+ WINPR_ASSERT(sdl->context()->gdi);
+ WINPR_ASSERT(sdl->context()->settings);
+
+ gdi = sdl->context()->gdi;
+
+ // TODO: Make this multimonitor ready!
+ // TODO: Need to find the primary monitor, get the scale
+ // TODO: Need to find the destination monitor, get the scale
+ // TODO: All intermediate monitors, get the scale
+
+ int offset_x = 0;
+ int offset_y = 0;
+ for (const auto& it : sdl->windows)
+ {
+ auto& window = it.second;
+ const auto id = window.id();
+ if (id != windowId)
+ {
+ continue;
+ }
+
+ auto size = window.rect();
+
+ sx = size.w / static_cast<double>(gdi->width);
+ sy = size.h / static_cast<double>(gdi->height);
+ offset_x = window.offsetX();
+ offset_y = window.offsetY();
+ break;
+ }
+
+ if (freerdp_settings_get_bool(sdl->context()->settings, FreeRDP_SmartSizing))
+ {
+ if (!fromLocalToRDP)
+ {
+ *px = static_cast<INT32>(*px * sx);
+ *py = static_cast<INT32>(*py * sy);
+ }
+ else
+ {
+ *px = static_cast<INT32>(*px / sx);
+ *py = static_cast<INT32>(*py / sy);
+ }
+ }
+ else if (applyOffset)
+ {
+ *px -= offset_x;
+ *py -= offset_y;
+ }
+
+ return TRUE;
+}
+
+static BOOL sdl_get_touch_scaled(SdlContext* sdl, const SDL_TouchFingerEvent* ev, INT32* px,
+ INT32* py, BOOL local)
+{
+ Uint32 windowID = 0;
+
+ WINPR_ASSERT(sdl);
+ WINPR_ASSERT(ev);
+ WINPR_ASSERT(px);
+ WINPR_ASSERT(py);
+
+#if SDL_VERSION_ATLEAST(2, 0, 12)
+ SDL_Window* window = SDL_GetWindowFromID(ev->windowID);
+#else
+ SDL_Window* window = SDL_GetMouseFocus();
+#endif
+
+ if (!window)
+ return FALSE;
+
+ windowID = SDL_GetWindowID(window);
+ SDL_Surface* surface = SDL_GetWindowSurface(window);
+ if (!surface)
+ return FALSE;
+
+ // TODO: Add the offset of the surface in the global coordinates
+ *px = static_cast<INT32>(ev->x * static_cast<float>(surface->w));
+ *py = static_cast<INT32>(ev->y * static_cast<float>(surface->h));
+ return sdl_scale_coordinates(sdl, windowID, px, py, local, TRUE);
+}
+
+static BOOL send_mouse_wheel(SdlContext* sdl, UINT16 flags, INT32 avalue)
+{
+ WINPR_ASSERT(sdl);
+ if (avalue < 0)
+ {
+ flags |= PTR_FLAGS_WHEEL_NEGATIVE;
+ avalue = -avalue;
+ }
+
+ while (avalue > 0)
+ {
+ const UINT16 cval = (avalue > 0xFF) ? 0xFF : static_cast<UINT16>(avalue);
+ UINT16 cflags = flags | cval;
+ /* Convert negative values to 9bit twos complement */
+ if (flags & PTR_FLAGS_WHEEL_NEGATIVE)
+ cflags = (flags & 0xFF00) | (0x100 - cval);
+ if (!freerdp_client_send_wheel_event(sdl->common(), cflags))
+ return FALSE;
+
+ avalue -= cval;
+ }
+ return TRUE;
+}
+
+static UINT32 sdl_scale_pressure(const float pressure)
+{
+ const float val = pressure * 0x400; /* [MS-RDPEI] 2.2.3.3.1.1 RDPINPUT_TOUCH_CONTACT */
+ if (val < 0.0f)
+ return 0;
+ if (val > 0x400)
+ return 0x400;
+ return static_cast<UINT32>(val);
+}
+
+BOOL sdl_handle_touch_up(SdlContext* sdl, const SDL_TouchFingerEvent* ev)
+{
+ WINPR_ASSERT(sdl);
+ WINPR_ASSERT(ev);
+
+ INT32 x = 0;
+ INT32 y = 0;
+ if (!sdl_get_touch_scaled(sdl, ev, &x, &y, TRUE))
+ return FALSE;
+ return freerdp_client_handle_touch(sdl->common(), FREERDP_TOUCH_UP | FREERDP_TOUCH_HAS_PRESSURE,
+ static_cast<INT32>(ev->fingerId),
+ sdl_scale_pressure(ev->pressure), x, y);
+}
+
+BOOL sdl_handle_touch_down(SdlContext* sdl, const SDL_TouchFingerEvent* ev)
+{
+ WINPR_ASSERT(sdl);
+ WINPR_ASSERT(ev);
+
+ INT32 x = 0;
+ INT32 y = 0;
+ if (!sdl_get_touch_scaled(sdl, ev, &x, &y, TRUE))
+ return FALSE;
+ return freerdp_client_handle_touch(
+ sdl->common(), FREERDP_TOUCH_DOWN | FREERDP_TOUCH_HAS_PRESSURE,
+ static_cast<INT32>(ev->fingerId), sdl_scale_pressure(ev->pressure), x, y);
+}
+
+BOOL sdl_handle_touch_motion(SdlContext* sdl, const SDL_TouchFingerEvent* ev)
+{
+ WINPR_ASSERT(sdl);
+ WINPR_ASSERT(ev);
+
+ INT32 x = 0;
+ INT32 y = 0;
+ if (!sdl_get_touch_scaled(sdl, ev, &x, &y, TRUE))
+ return FALSE;
+ return freerdp_client_handle_touch(
+ sdl->common(), FREERDP_TOUCH_MOTION | FREERDP_TOUCH_HAS_PRESSURE,
+ static_cast<INT32>(ev->fingerId), sdl_scale_pressure(ev->pressure), x, y);
+}
+
+BOOL sdl_handle_mouse_motion(SdlContext* sdl, const SDL_MouseMotionEvent* ev)
+{
+ WINPR_ASSERT(sdl);
+ WINPR_ASSERT(ev);
+
+ sdl->input.mouse_focus(ev->windowID);
+ const BOOL relative = freerdp_client_use_relative_mouse_events(sdl->common());
+ INT32 x = relative ? ev->xrel : ev->x;
+ INT32 y = relative ? ev->yrel : ev->y;
+ sdl_scale_coordinates(sdl, ev->windowID, &x, &y, TRUE, TRUE);
+ return freerdp_client_send_button_event(sdl->common(), relative, PTR_FLAGS_MOVE, x, y);
+}
+
+BOOL sdl_handle_mouse_wheel(SdlContext* sdl, const SDL_MouseWheelEvent* ev)
+{
+ WINPR_ASSERT(sdl);
+ WINPR_ASSERT(ev);
+
+ const BOOL flipped = (ev->direction == SDL_MOUSEWHEEL_FLIPPED);
+ const INT32 x = ev->x * (flipped ? -1 : 1) * 0x78;
+ const INT32 y = ev->y * (flipped ? -1 : 1) * 0x78;
+ UINT16 flags = 0;
+
+ if (y != 0)
+ {
+ flags |= PTR_FLAGS_WHEEL;
+ send_mouse_wheel(sdl, flags, y);
+ }
+
+ if (x != 0)
+ {
+ flags |= PTR_FLAGS_HWHEEL;
+ send_mouse_wheel(sdl, flags, x);
+ }
+ return TRUE;
+}
+
+BOOL sdl_handle_mouse_button(SdlContext* sdl, const SDL_MouseButtonEvent* ev)
+{
+ UINT16 flags = 0;
+ UINT16 xflags = 0;
+
+ WINPR_ASSERT(sdl);
+ WINPR_ASSERT(ev);
+
+ if (ev->state == SDL_PRESSED)
+ {
+ flags |= PTR_FLAGS_DOWN;
+ xflags |= PTR_XFLAGS_DOWN;
+ }
+
+ switch (ev->button)
+ {
+ case 1:
+ flags |= PTR_FLAGS_BUTTON1;
+ break;
+ case 2:
+ flags |= PTR_FLAGS_BUTTON3;
+ break;
+ case 3:
+ flags |= PTR_FLAGS_BUTTON2;
+ break;
+ case 4:
+ xflags |= PTR_XFLAGS_BUTTON1;
+ break;
+ case 5:
+ xflags |= PTR_XFLAGS_BUTTON2;
+ break;
+ default:
+ break;
+ }
+
+ const BOOL relative = freerdp_client_use_relative_mouse_events(sdl->common());
+ INT32 x = relative ? 0 : ev->x;
+ INT32 y = relative ? 0 : ev->y;
+ sdl_scale_coordinates(sdl, ev->windowID, &x, &y, TRUE, TRUE);
+ if ((flags & (~PTR_FLAGS_DOWN)) != 0)
+ return freerdp_client_send_button_event(sdl->common(), relative, flags, x, y);
+ else if ((xflags & (~PTR_XFLAGS_DOWN)) != 0)
+ return freerdp_client_send_extended_button_event(sdl->common(), relative, xflags, x, y);
+ else
+ return FALSE;
+}
diff --git a/client/SDL/sdl_touch.hpp b/client/SDL/sdl_touch.hpp
new file mode 100644
index 0000000..395fddb
--- /dev/null
+++ b/client/SDL/sdl_touch.hpp
@@ -0,0 +1,36 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * FreeRDP SDL touch/mouse input
+ *
+ * Copyright 2022 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <winpr/wtypes.h>
+
+#include <SDL.h>
+#include "sdl_types.hpp"
+
+BOOL sdl_scale_coordinates(SdlContext* sdl, Uint32 windowId, INT32* px, INT32* py,
+ BOOL fromLocalToRDP, BOOL applyOffset);
+
+BOOL sdl_handle_mouse_motion(SdlContext* sdl, const SDL_MouseMotionEvent* ev);
+BOOL sdl_handle_mouse_wheel(SdlContext* sdl, const SDL_MouseWheelEvent* ev);
+BOOL sdl_handle_mouse_button(SdlContext* sdl, const SDL_MouseButtonEvent* ev);
+
+BOOL sdl_handle_touch_down(SdlContext* sdl, const SDL_TouchFingerEvent* ev);
+BOOL sdl_handle_touch_up(SdlContext* sdl, const SDL_TouchFingerEvent* ev);
+BOOL sdl_handle_touch_motion(SdlContext* sdl, const SDL_TouchFingerEvent* ev);
diff --git a/client/SDL/sdl_types.hpp b/client/SDL/sdl_types.hpp
new file mode 100644
index 0000000..831472c
--- /dev/null
+++ b/client/SDL/sdl_types.hpp
@@ -0,0 +1,46 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * SDL Client
+ *
+ * Copyright 2022 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <freerdp/freerdp.h>
+
+class SdlContext;
+
+typedef struct
+{
+ rdpClientContext common;
+ SdlContext* sdl;
+} sdl_rdp_context;
+
+static inline SdlContext* get_context(void* ctx)
+{
+ if (!ctx)
+ return nullptr;
+ auto sdl = static_cast<sdl_rdp_context*>(ctx);
+ return sdl->sdl;
+}
+
+static inline SdlContext* get_context(rdpContext* ctx)
+{
+ if (!ctx)
+ return nullptr;
+ auto sdl = reinterpret_cast<sdl_rdp_context*>(ctx);
+ return sdl->sdl;
+}
diff --git a/client/SDL/sdl_utils.cpp b/client/SDL/sdl_utils.cpp
new file mode 100644
index 0000000..c3bd2cf
--- /dev/null
+++ b/client/SDL/sdl_utils.cpp
@@ -0,0 +1,465 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * SDL Client
+ *
+ * Copyright 2022 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <fstream>
+#if __has_include(<filesystem>)
+#include <filesystem>
+namespace fs = std::filesystem;
+#elif __has_include(<experimental/filesystem>)
+#include <experimental/filesystem>
+namespace fs = std::experimental::filesystem;
+#else
+#error Could not find system header "<filesystem>" or "<experimental/filesystem>"
+#endif
+
+#include <cassert>
+#include "sdl_utils.hpp"
+
+#include "sdl_freerdp.hpp"
+
+#include <SDL.h>
+
+#include <winpr/path.h>
+#include <freerdp/version.h>
+#if defined(CJSON_FOUND)
+#include <cjson/cJSON.h>
+#endif
+
+const char* sdl_event_type_str(Uint32 type)
+{
+#define STR(x) #x
+#define EV_CASE_STR(x) \
+ case x: \
+ return STR(x)
+
+ switch (type)
+ {
+ EV_CASE_STR(SDL_FIRSTEVENT);
+ EV_CASE_STR(SDL_QUIT);
+ EV_CASE_STR(SDL_APP_TERMINATING);
+ EV_CASE_STR(SDL_APP_LOWMEMORY);
+ EV_CASE_STR(SDL_APP_WILLENTERBACKGROUND);
+ EV_CASE_STR(SDL_APP_DIDENTERBACKGROUND);
+ EV_CASE_STR(SDL_APP_WILLENTERFOREGROUND);
+ EV_CASE_STR(SDL_APP_DIDENTERFOREGROUND);
+#if SDL_VERSION_ATLEAST(2, 0, 10)
+ EV_CASE_STR(SDL_DISPLAYEVENT);
+#endif
+ EV_CASE_STR(SDL_WINDOWEVENT);
+ EV_CASE_STR(SDL_SYSWMEVENT);
+ EV_CASE_STR(SDL_KEYDOWN);
+ EV_CASE_STR(SDL_KEYUP);
+ EV_CASE_STR(SDL_TEXTEDITING);
+ EV_CASE_STR(SDL_TEXTINPUT);
+ EV_CASE_STR(SDL_KEYMAPCHANGED);
+ EV_CASE_STR(SDL_MOUSEMOTION);
+ EV_CASE_STR(SDL_MOUSEBUTTONDOWN);
+ EV_CASE_STR(SDL_MOUSEBUTTONUP);
+ EV_CASE_STR(SDL_MOUSEWHEEL);
+ EV_CASE_STR(SDL_JOYAXISMOTION);
+ EV_CASE_STR(SDL_JOYBALLMOTION);
+ EV_CASE_STR(SDL_JOYHATMOTION);
+ EV_CASE_STR(SDL_JOYBUTTONDOWN);
+ EV_CASE_STR(SDL_JOYBUTTONUP);
+ EV_CASE_STR(SDL_JOYDEVICEADDED);
+ EV_CASE_STR(SDL_JOYDEVICEREMOVED);
+ EV_CASE_STR(SDL_CONTROLLERAXISMOTION);
+ EV_CASE_STR(SDL_CONTROLLERBUTTONDOWN);
+ EV_CASE_STR(SDL_CONTROLLERBUTTONUP);
+ EV_CASE_STR(SDL_CONTROLLERDEVICEADDED);
+ EV_CASE_STR(SDL_CONTROLLERDEVICEREMOVED);
+ EV_CASE_STR(SDL_CONTROLLERDEVICEREMAPPED);
+#if SDL_VERSION_ATLEAST(2, 0, 14)
+ EV_CASE_STR(SDL_LOCALECHANGED);
+ EV_CASE_STR(SDL_CONTROLLERTOUCHPADDOWN);
+ EV_CASE_STR(SDL_CONTROLLERTOUCHPADMOTION);
+ EV_CASE_STR(SDL_CONTROLLERTOUCHPADUP);
+ EV_CASE_STR(SDL_CONTROLLERSENSORUPDATE);
+#endif
+ EV_CASE_STR(SDL_FINGERDOWN);
+ EV_CASE_STR(SDL_FINGERUP);
+ EV_CASE_STR(SDL_FINGERMOTION);
+ EV_CASE_STR(SDL_DOLLARGESTURE);
+ EV_CASE_STR(SDL_DOLLARRECORD);
+ EV_CASE_STR(SDL_MULTIGESTURE);
+ EV_CASE_STR(SDL_CLIPBOARDUPDATE);
+ EV_CASE_STR(SDL_DROPFILE);
+ EV_CASE_STR(SDL_DROPTEXT);
+ EV_CASE_STR(SDL_DROPBEGIN);
+ EV_CASE_STR(SDL_DROPCOMPLETE);
+ EV_CASE_STR(SDL_AUDIODEVICEADDED);
+ EV_CASE_STR(SDL_AUDIODEVICEREMOVED);
+#if SDL_VERSION_ATLEAST(2, 0, 9)
+ EV_CASE_STR(SDL_SENSORUPDATE);
+#endif
+ EV_CASE_STR(SDL_RENDER_TARGETS_RESET);
+ EV_CASE_STR(SDL_RENDER_DEVICE_RESET);
+ EV_CASE_STR(SDL_USEREVENT);
+
+ EV_CASE_STR(SDL_USEREVENT_CERT_DIALOG);
+ EV_CASE_STR(SDL_USEREVENT_CERT_RESULT);
+ EV_CASE_STR(SDL_USEREVENT_SHOW_DIALOG);
+ EV_CASE_STR(SDL_USEREVENT_SHOW_RESULT);
+ EV_CASE_STR(SDL_USEREVENT_AUTH_DIALOG);
+ EV_CASE_STR(SDL_USEREVENT_AUTH_RESULT);
+ EV_CASE_STR(SDL_USEREVENT_SCARD_DIALOG);
+ EV_CASE_STR(SDL_USEREVENT_RETRY_DIALOG);
+ EV_CASE_STR(SDL_USEREVENT_SCARD_RESULT);
+ EV_CASE_STR(SDL_USEREVENT_UPDATE);
+ EV_CASE_STR(SDL_USEREVENT_CREATE_WINDOWS);
+ EV_CASE_STR(SDL_USEREVENT_WINDOW_RESIZEABLE);
+ EV_CASE_STR(SDL_USEREVENT_WINDOW_FULLSCREEN);
+ EV_CASE_STR(SDL_USEREVENT_POINTER_NULL);
+ EV_CASE_STR(SDL_USEREVENT_POINTER_DEFAULT);
+ EV_CASE_STR(SDL_USEREVENT_POINTER_POSITION);
+ EV_CASE_STR(SDL_USEREVENT_POINTER_SET);
+ EV_CASE_STR(SDL_USEREVENT_QUIT);
+
+ EV_CASE_STR(SDL_LASTEVENT);
+ default:
+ return "SDL_UNKNOWNEVENT";
+ }
+#undef EV_CASE_STR
+#undef STR
+}
+
+const char* sdl_error_string(Uint32 res)
+{
+ if (res == 0)
+ return nullptr;
+
+ return SDL_GetError();
+}
+
+BOOL sdl_log_error_ex(Uint32 res, wLog* log, const char* what, const char* file, size_t line,
+ const char* fkt)
+{
+ const char* msg = sdl_error_string(res);
+
+ WINPR_UNUSED(file);
+
+ if (!msg)
+ return FALSE;
+
+ WLog_Print(log, WLOG_ERROR, "[%s:%" PRIuz "][%s]: %s", fkt, line, what, msg);
+ return TRUE;
+}
+
+BOOL sdl_push_user_event(Uint32 type, ...)
+{
+ SDL_Event ev = {};
+ SDL_UserEvent* event = &ev.user;
+
+ va_list ap;
+ va_start(ap, type);
+ event->type = type;
+ switch (type)
+ {
+ case SDL_USEREVENT_AUTH_RESULT:
+ {
+ auto arg = reinterpret_cast<SDL_UserAuthArg*>(ev.padding);
+ arg->user = va_arg(ap, char*);
+ arg->domain = va_arg(ap, char*);
+ arg->password = va_arg(ap, char*);
+ arg->result = va_arg(ap, Sint32);
+ }
+ break;
+ case SDL_USEREVENT_AUTH_DIALOG:
+ {
+ auto arg = reinterpret_cast<SDL_UserAuthArg*>(ev.padding);
+
+ arg->title = va_arg(ap, char*);
+ arg->user = va_arg(ap, char*);
+ arg->domain = va_arg(ap, char*);
+ arg->password = va_arg(ap, char*);
+ arg->result = va_arg(ap, Sint32);
+ }
+ break;
+ case SDL_USEREVENT_SCARD_DIALOG:
+ {
+ event->data1 = va_arg(ap, char*);
+ event->data2 = va_arg(ap, char**);
+ event->code = va_arg(ap, Sint32);
+ }
+ break;
+ case SDL_USEREVENT_RETRY_DIALOG:
+ break;
+ case SDL_USEREVENT_SCARD_RESULT:
+ case SDL_USEREVENT_SHOW_RESULT:
+ case SDL_USEREVENT_CERT_RESULT:
+ event->code = va_arg(ap, Sint32);
+ break;
+
+ case SDL_USEREVENT_SHOW_DIALOG:
+ event->data1 = va_arg(ap, char*);
+ event->data2 = va_arg(ap, char*);
+ event->code = va_arg(ap, Sint32);
+ break;
+ case SDL_USEREVENT_CERT_DIALOG:
+ event->data1 = va_arg(ap, char*);
+ event->data2 = va_arg(ap, char*);
+ break;
+ case SDL_USEREVENT_UPDATE:
+ event->data1 = va_arg(ap, void*);
+ break;
+ case SDL_USEREVENT_POINTER_POSITION:
+ event->data1 = reinterpret_cast<void*>(static_cast<uintptr_t>(va_arg(ap, UINT32)));
+ event->data2 = reinterpret_cast<void*>(static_cast<uintptr_t>(va_arg(ap, UINT32)));
+ break;
+ case SDL_USEREVENT_POINTER_SET:
+ event->data1 = va_arg(ap, void*);
+ event->data2 = va_arg(ap, void*);
+ break;
+ case SDL_USEREVENT_CREATE_WINDOWS:
+ event->data1 = reinterpret_cast<void*>(va_arg(ap, void*));
+ break;
+ case SDL_USEREVENT_WINDOW_FULLSCREEN:
+ case SDL_USEREVENT_WINDOW_RESIZEABLE:
+ event->data1 = va_arg(ap, void*);
+ event->code = va_arg(ap, int);
+ break;
+ case SDL_USEREVENT_QUIT:
+ case SDL_USEREVENT_POINTER_NULL:
+ case SDL_USEREVENT_POINTER_DEFAULT:
+ break;
+ default:
+ va_end(ap);
+ return FALSE;
+ }
+ va_end(ap);
+ return SDL_PushEvent(&ev) == 1;
+}
+
+CriticalSection::CriticalSection()
+{
+ InitializeCriticalSection(&_section);
+}
+
+CriticalSection::~CriticalSection()
+{
+ DeleteCriticalSection(&_section);
+}
+
+void CriticalSection::lock()
+{
+ EnterCriticalSection(&_section);
+}
+
+void CriticalSection::unlock()
+{
+ LeaveCriticalSection(&_section);
+}
+
+WinPREvent::WinPREvent(bool initial)
+ : _handle(CreateEventA(nullptr, TRUE, initial ? TRUE : FALSE, nullptr))
+{
+}
+
+WinPREvent::~WinPREvent()
+{
+ CloseHandle(_handle);
+}
+
+void WinPREvent::set()
+{
+ SetEvent(_handle);
+}
+
+void WinPREvent::clear()
+{
+ ResetEvent(_handle);
+}
+
+bool WinPREvent::isSet() const
+{
+ return WaitForSingleObject(_handle, 0) == WAIT_OBJECT_0;
+}
+
+HANDLE WinPREvent::handle() const
+{
+ return _handle;
+}
+
+bool sdl_push_quit()
+{
+ SDL_Event ev = { 0 };
+ ev.type = SDL_QUIT;
+ SDL_PushEvent(&ev);
+ return true;
+}
+
+std::string sdl_window_event_str(Uint8 ev)
+{
+ switch (ev)
+ {
+ case SDL_WINDOWEVENT_NONE:
+ return "SDL_WINDOWEVENT_NONE";
+ case SDL_WINDOWEVENT_SHOWN:
+ return "SDL_WINDOWEVENT_SHOWN";
+ case SDL_WINDOWEVENT_HIDDEN:
+ return "SDL_WINDOWEVENT_HIDDEN";
+ case SDL_WINDOWEVENT_EXPOSED:
+ return "SDL_WINDOWEVENT_EXPOSED";
+ case SDL_WINDOWEVENT_MOVED:
+ return "SDL_WINDOWEVENT_MOVED";
+ case SDL_WINDOWEVENT_RESIZED:
+ return "SDL_WINDOWEVENT_RESIZED";
+ case SDL_WINDOWEVENT_SIZE_CHANGED:
+ return "SDL_WINDOWEVENT_SIZE_CHANGED";
+ case SDL_WINDOWEVENT_MINIMIZED:
+ return "SDL_WINDOWEVENT_MINIMIZED";
+ case SDL_WINDOWEVENT_MAXIMIZED:
+ return "SDL_WINDOWEVENT_MAXIMIZED";
+ case SDL_WINDOWEVENT_RESTORED:
+ return "SDL_WINDOWEVENT_RESTORED";
+ case SDL_WINDOWEVENT_ENTER:
+ return "SDL_WINDOWEVENT_ENTER";
+ case SDL_WINDOWEVENT_LEAVE:
+ return "SDL_WINDOWEVENT_LEAVE";
+ case SDL_WINDOWEVENT_FOCUS_GAINED:
+ return "SDL_WINDOWEVENT_FOCUS_GAINED";
+ case SDL_WINDOWEVENT_FOCUS_LOST:
+ return "SDL_WINDOWEVENT_FOCUS_LOST";
+ case SDL_WINDOWEVENT_CLOSE:
+ return "SDL_WINDOWEVENT_CLOSE";
+#if SDL_VERSION_ATLEAST(2, 0, 5)
+ case SDL_WINDOWEVENT_TAKE_FOCUS:
+ return "SDL_WINDOWEVENT_TAKE_FOCUS";
+ case SDL_WINDOWEVENT_HIT_TEST:
+ return "SDL_WINDOWEVENT_HIT_TEST";
+#endif
+#if SDL_VERSION_ATLEAST(2, 0, 18)
+ case SDL_WINDOWEVENT_ICCPROF_CHANGED:
+ return "SDL_WINDOWEVENT_ICCPROF_CHANGED";
+ case SDL_WINDOWEVENT_DISPLAY_CHANGED:
+ return "SDL_WINDOWEVENT_DISPLAY_CHANGED";
+#endif
+ default:
+ return "SDL_WINDOWEVENT_UNKNOWN";
+ }
+}
+
+#if defined(CJSON_FOUND)
+using cJSONPtr = std::unique_ptr<cJSON, decltype(&cJSON_Delete)>;
+
+static cJSONPtr get()
+{
+ auto config = sdl_get_pref_file();
+
+ std::ifstream ifs(config);
+ std::string content((std::istreambuf_iterator<char>(ifs)), (std::istreambuf_iterator<char>()));
+ return { cJSON_ParseWithLength(content.c_str(), content.size()), cJSON_Delete };
+}
+
+static cJSON* get_item(const std::string& key)
+{
+ static cJSONPtr config{ nullptr, cJSON_Delete };
+ if (!config)
+ config = get();
+ if (!config)
+ return nullptr;
+ return cJSON_GetObjectItem(config.get(), key.c_str());
+}
+
+static std::string item_to_str(cJSON* item, const std::string& fallback = "")
+{
+ if (!item || !cJSON_IsString(item))
+ return fallback;
+ auto str = cJSON_GetStringValue(item);
+ if (!str)
+ return {};
+ return str;
+}
+#endif
+
+std::string sdl_get_pref_string(const std::string& key, const std::string& fallback)
+{
+#if defined(CJSON_FOUND)
+ auto item = get_item(key);
+ return item_to_str(item, fallback);
+#else
+ return fallback;
+#endif
+}
+
+bool sdl_get_pref_bool(const std::string& key, bool fallback)
+{
+#if defined(CJSON_FOUND)
+ auto item = get_item(key);
+ if (!item || !cJSON_IsBool(item))
+ return fallback;
+ return cJSON_IsTrue(item);
+#else
+ return fallback;
+#endif
+}
+
+int64_t sdl_get_pref_int(const std::string& key, int64_t fallback)
+{
+#if defined(CJSON_FOUND)
+ auto item = get_item(key);
+ if (!item || !cJSON_IsNumber(item))
+ return fallback;
+ auto val = cJSON_GetNumberValue(item);
+ return static_cast<int64_t>(val);
+#else
+ return fallback;
+#endif
+}
+
+std::vector<std::string> sdl_get_pref_array(const std::string& key,
+ const std::vector<std::string>& fallback)
+{
+#if defined(CJSON_FOUND)
+ auto item = get_item(key);
+ if (!item || !cJSON_IsArray(item))
+ return fallback;
+
+ std::vector<std::string> values;
+ for (int x = 0; x < cJSON_GetArraySize(item); x++)
+ {
+ auto cur = cJSON_GetArrayItem(item, x);
+ values.push_back(item_to_str(cur));
+ }
+
+ return values;
+#else
+ return fallback;
+#endif
+}
+
+std::string sdl_get_pref_dir()
+{
+ using CStringPtr = std::unique_ptr<char, decltype(&free)>;
+ CStringPtr path(GetKnownPath(KNOWN_PATH_XDG_CONFIG_HOME), free);
+ if (!path)
+ return {};
+
+ fs::path config{ path.get() };
+ config /= FREERDP_VENDOR;
+ config /= FREERDP_PRODUCT;
+ return config.string();
+}
+
+std::string sdl_get_pref_file()
+{
+ fs::path config{ sdl_get_pref_dir() };
+ config /= "sdl-freerdp.json";
+ return config.string();
+}
diff --git a/client/SDL/sdl_utils.hpp b/client/SDL/sdl_utils.hpp
new file mode 100644
index 0000000..75cb461
--- /dev/null
+++ b/client/SDL/sdl_utils.hpp
@@ -0,0 +1,113 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * SDL Client
+ *
+ * Copyright 2022 Armin Novak <armin.novak@thincast.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <string>
+
+#include <winpr/synch.h>
+#include <winpr/wlog.h>
+
+#include <SDL.h>
+#include <string>
+#include <vector>
+
+class CriticalSection
+{
+ public:
+ CriticalSection();
+ ~CriticalSection();
+
+ void lock();
+ void unlock();
+
+ private:
+ CRITICAL_SECTION _section;
+};
+
+class WinPREvent
+{
+ public:
+ explicit WinPREvent(bool initial = false);
+ ~WinPREvent();
+
+ void set();
+ void clear();
+ [[nodiscard]] bool isSet() const;
+
+ [[nodiscard]] HANDLE handle() const;
+
+ private:
+ HANDLE _handle;
+};
+
+enum
+{
+ SDL_USEREVENT_UPDATE = SDL_USEREVENT + 1,
+ SDL_USEREVENT_CREATE_WINDOWS,
+ SDL_USEREVENT_WINDOW_RESIZEABLE,
+ SDL_USEREVENT_WINDOW_FULLSCREEN,
+ SDL_USEREVENT_POINTER_NULL,
+ SDL_USEREVENT_POINTER_DEFAULT,
+ SDL_USEREVENT_POINTER_POSITION,
+ SDL_USEREVENT_POINTER_SET,
+ SDL_USEREVENT_QUIT,
+ SDL_USEREVENT_CERT_DIALOG,
+ SDL_USEREVENT_SHOW_DIALOG,
+ SDL_USEREVENT_AUTH_DIALOG,
+ SDL_USEREVENT_SCARD_DIALOG,
+ SDL_USEREVENT_RETRY_DIALOG,
+
+ SDL_USEREVENT_CERT_RESULT,
+ SDL_USEREVENT_SHOW_RESULT,
+ SDL_USEREVENT_AUTH_RESULT,
+ SDL_USEREVENT_SCARD_RESULT
+};
+
+typedef struct
+{
+ Uint32 type;
+ Uint32 timestamp;
+ char* title;
+ char* user;
+ char* domain;
+ char* password;
+ Sint32 result;
+} SDL_UserAuthArg;
+
+BOOL sdl_push_user_event(Uint32 type, ...);
+
+bool sdl_push_quit();
+
+std::string sdl_window_event_str(Uint8 ev);
+const char* sdl_event_type_str(Uint32 type);
+const char* sdl_error_string(Uint32 res);
+
+#define sdl_log_error(res, log, what) sdl_log_error_ex(res, log, what, __FILE__, __LINE__, __func__)
+BOOL sdl_log_error_ex(Uint32 res, wLog* log, const char* what, const char* file, size_t line,
+ const char* fkt);
+
+std::string sdl_get_pref_dir();
+std::string sdl_get_pref_file();
+
+std::string sdl_get_pref_string(const std::string& key, const std::string& fallback = "");
+int64_t sdl_get_pref_int(const std::string& key, int64_t fallback = 0);
+bool sdl_get_pref_bool(const std::string& key, bool fallback = false);
+std::vector<std::string> sdl_get_pref_array(const std::string& key,
+ const std::vector<std::string>& fallback = {});
diff --git a/client/SDL/sdl_window.cpp b/client/SDL/sdl_window.cpp
new file mode 100644
index 0000000..c5437bc
--- /dev/null
+++ b/client/SDL/sdl_window.cpp
@@ -0,0 +1,203 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * SDL Client
+ *
+ * Copyright 2023 Armin Novak <armin.novak@thincast.com>
+ * Copyright 2023 Thincast Technologies GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "sdl_window.hpp"
+#include "sdl_utils.hpp"
+
+SdlWindow::SdlWindow(const std::string& title, Sint32 startupX, Sint32 startupY, Sint32 width,
+ Sint32 height, Uint32 flags)
+ : _window(SDL_CreateWindow(title.c_str(), startupX, startupY, width, height, flags)),
+ _offset_x(0), _offset_y(0)
+{
+}
+
+SdlWindow::SdlWindow(SdlWindow&& other)
+ : _window(other._window), _offset_x(other._offset_x), _offset_y(other._offset_y)
+{
+ other._window = nullptr;
+}
+
+SdlWindow::~SdlWindow()
+{
+ SDL_DestroyWindow(_window);
+}
+
+Uint32 SdlWindow::id() const
+{
+ if (!_window)
+ return 0;
+ return SDL_GetWindowID(_window);
+}
+
+int SdlWindow::displayIndex() const
+{
+ if (!_window)
+ return 0;
+ return SDL_GetWindowDisplayIndex(_window);
+}
+
+SDL_Rect SdlWindow::rect() const
+{
+ SDL_Rect rect = {};
+ if (_window)
+ {
+ SDL_GetWindowPosition(_window, &rect.x, &rect.y);
+ SDL_GetWindowSize(_window, &rect.w, &rect.h);
+ }
+ return rect;
+}
+
+SDL_Window* SdlWindow::window() const
+{
+ return _window;
+}
+
+Sint32 SdlWindow::offsetX() const
+{
+ return _offset_x;
+}
+
+void SdlWindow::setOffsetX(Sint32 x)
+{
+ _offset_x = x;
+}
+
+void SdlWindow::setOffsetY(Sint32 y)
+{
+ _offset_y = y;
+}
+
+Sint32 SdlWindow::offsetY() const
+{
+ return _offset_y;
+}
+
+bool SdlWindow::grabKeyboard(bool enable)
+{
+ if (!_window)
+ return false;
+#if SDL_VERSION_ATLEAST(2, 0, 16)
+ SDL_SetWindowKeyboardGrab(_window, enable ? SDL_TRUE : SDL_FALSE);
+ return true;
+#else
+ SDL_LogError(SDL_LOG_CATEGORY_INPUT, "Keyboard grabbing not supported by SDL2 < 2.0.16");
+ return false;
+#endif
+}
+
+bool SdlWindow::grabMouse(bool enable)
+{
+ if (!_window)
+ return false;
+#if SDL_VERSION_ATLEAST(2, 0, 16)
+ SDL_SetWindowMouseGrab(_window, enable ? SDL_TRUE : SDL_FALSE);
+#else
+ SDL_SetWindowGrab(_window, enable ? SDL_TRUE : SDL_FALSE);
+#endif
+ return true;
+}
+
+void SdlWindow::setBordered(bool bordered)
+{
+ if (_window)
+ SDL_SetWindowBordered(_window, bordered ? SDL_TRUE : SDL_FALSE);
+}
+
+void SdlWindow::raise()
+{
+ SDL_RaiseWindow(_window);
+}
+
+void SdlWindow::resizeable(bool use)
+{
+ SDL_SetWindowResizable(_window, use ? SDL_TRUE : SDL_FALSE);
+}
+
+void SdlWindow::fullscreen(bool enter)
+{
+ auto curFlags = SDL_GetWindowFlags(_window);
+
+ if (enter)
+ {
+ if (!(curFlags & SDL_WINDOW_BORDERLESS))
+ {
+ auto idx = SDL_GetWindowDisplayIndex(_window);
+ SDL_DisplayMode mode = {};
+ SDL_GetCurrentDisplayMode(idx, &mode);
+
+ SDL_RestoreWindow(_window); // Maximize so we can see the caption and
+ // bits
+ SDL_SetWindowBordered(_window, SDL_FALSE);
+ SDL_SetWindowPosition(_window, 0, 0);
+#if SDL_VERSION_ATLEAST(2, 0, 16)
+ SDL_SetWindowAlwaysOnTop(_window, SDL_TRUE);
+#endif
+ SDL_RaiseWindow(_window);
+ SDL_SetWindowSize(_window, mode.w, mode.h);
+ }
+ }
+ else
+ {
+ if (curFlags & SDL_WINDOW_BORDERLESS)
+ {
+
+ SDL_SetWindowBordered(_window, SDL_TRUE);
+#if SDL_VERSION_ATLEAST(2, 0, 16)
+ SDL_SetWindowAlwaysOnTop(_window, SDL_FALSE);
+#endif
+ SDL_RaiseWindow(_window);
+ SDL_MinimizeWindow(_window); // Maximize so we can see the caption and bits
+ SDL_MaximizeWindow(_window); // Maximize so we can see the caption and bits
+ }
+ }
+}
+
+bool SdlWindow::fill(Uint8 r, Uint8 g, Uint8 b, Uint8 a)
+{
+ auto surface = SDL_GetWindowSurface(_window);
+ if (!surface)
+ return false;
+ SDL_Rect rect = { 0, 0, surface->w, surface->h };
+ auto color = SDL_MapRGBA(surface->format, r, g, b, a);
+
+ SDL_FillRect(surface, &rect, color);
+ return true;
+}
+
+bool SdlWindow::blit(SDL_Surface* surface, const SDL_Rect& srcRect, SDL_Rect& dstRect)
+{
+ auto screen = SDL_GetWindowSurface(_window);
+ if (!screen || !surface)
+ return false;
+ if (!SDL_SetClipRect(surface, &srcRect))
+ return false;
+ if (!SDL_SetClipRect(screen, &dstRect))
+ return false;
+ auto rc = SDL_BlitScaled(surface, &srcRect, screen, &dstRect);
+ if (rc != 0)
+ {
+ SDL_LogError(SDL_LOG_CATEGORY_RENDER, "SDL_BlitScaled: %s [%d]", sdl_error_string(rc), rc);
+ }
+ return rc == 0;
+}
+
+void SdlWindow::updateSurface()
+{
+ SDL_UpdateWindowSurface(_window);
+}
diff --git a/client/SDL/sdl_window.hpp b/client/SDL/sdl_window.hpp
new file mode 100644
index 0000000..4f84e1b
--- /dev/null
+++ b/client/SDL/sdl_window.hpp
@@ -0,0 +1,62 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * SDL Client
+ *
+ * Copyright 2023 Armin Novak <armin.novak@thincast.com>
+ * Copyright 2023 Thincast Technologies GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#pragma once
+
+#include <string>
+#include <SDL.h>
+
+class SdlWindow
+{
+ public:
+ SdlWindow(const std::string& title, Sint32 startupX, Sint32 startupY, Sint32 width,
+ Sint32 height, Uint32 flags);
+ SdlWindow(SdlWindow&& other);
+ ~SdlWindow();
+
+ [[nodiscard]] Uint32 id() const;
+ [[nodiscard]] int displayIndex() const;
+ [[nodiscard]] SDL_Rect rect() const;
+ [[nodiscard]] SDL_Window* window() const;
+
+ [[nodiscard]] Sint32 offsetX() const;
+ void setOffsetX(Sint32 x);
+
+ void setOffsetY(Sint32 y);
+ [[nodiscard]] Sint32 offsetY() const;
+
+ bool grabKeyboard(bool enable);
+ bool grabMouse(bool enable);
+ void setBordered(bool bordered);
+ void raise();
+ void resizeable(bool use);
+ void fullscreen(bool use);
+
+ bool fill(Uint8 r = 0x00, Uint8 g = 0x00, Uint8 b = 0x00, Uint8 a = 0xff);
+ bool blit(SDL_Surface* surface, const SDL_Rect& src, SDL_Rect& dst);
+ void updateSurface();
+
+ private:
+ SDL_Window* _window = nullptr;
+ Sint32 _offset_x = 0;
+ Sint32 _offset_y = 0;
+
+ private:
+ SdlWindow(const SdlWindow& other) = delete;
+};