diff options
Diffstat (limited to '')
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 Binary files differnew file mode 100644 index 0000000..5bda9cc --- /dev/null +++ b/client/SDL/dialogs/font/OpenSans-Italic-VariableFont_wdth,wght.ttf diff --git a/client/SDL/dialogs/font/OpenSans-VariableFont_wdth,wght.ttf b/client/SDL/dialogs/font/OpenSans-VariableFont_wdth,wght.ttf Binary files differnew file mode 100644 index 0000000..e4142bf --- /dev/null +++ b/client/SDL/dialogs/font/OpenSans-VariableFont_wdth,wght.ttf 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:<device></command></term> + <listitem> + <para>Activate smartcard redirection for device <replaceable>device</replaceable></para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>/printer:<device>,<driver></command></term> + <listitem> + <para>Activate printer redirection for printer <replaceable>device</replaceable> using driver <replaceable>driver</replaceable></para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>/serial:<device></command></term> + <listitem> + <para>Activate serial port redirection for port <replaceable>device</replaceable></para> + </listitem> + </varlistentry> + <varlistentry> + <term><command>/parallel:<device></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; +}; |