diff options
Diffstat (limited to 'src/plugins/platforms/wasm')
56 files changed, 5219 insertions, 3359 deletions
diff --git a/src/plugins/platforms/wasm/CMakeLists.txt b/src/plugins/platforms/wasm/CMakeLists.txt index 962c289640..775946aaf9 100644 --- a/src/plugins/platforms/wasm/CMakeLists.txt +++ b/src/plugins/platforms/wasm/CMakeLists.txt @@ -1,41 +1,41 @@ # Copyright (C) 2022 The Qt Company Ltd. # SPDX-License-Identifier: BSD-3-Clause -# Generated from wasm.pro. - ##################################################################### ## QWasmIntegrationPlugin Plugin: ##################################################################### qt_internal_add_plugin(QWasmIntegrationPlugin OUTPUT_NAME qwasm - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES wasm # special case + DEFAULT_IF "wasm" IN_LIST QT_QPA_PLATFORMS PLUGIN_TYPE platforms - STATIC SOURCES main.cpp qwasmaccessibility.cpp qwasmaccessibility.h + qwasmbase64iconstore.cpp qwasmbase64iconstore.h qwasmclipboard.cpp qwasmclipboard.h qwasmcompositor.cpp qwasmcompositor.h qwasmcssstyle.cpp qwasmcssstyle.h qwasmcursor.cpp qwasmcursor.h + qwasmdom.cpp qwasmdom.h qwasmevent.cpp qwasmevent.h qwasmeventdispatcher.cpp qwasmeventdispatcher.h - qwasmeventtranslator.cpp qwasmeventtranslator.h qwasmfontdatabase.cpp qwasmfontdatabase.h qwasmintegration.cpp qwasmintegration.h + qwasmkeytranslator.cpp qwasmkeytranslator.h qwasmoffscreensurface.cpp qwasmoffscreensurface.h qwasmopenglcontext.cpp qwasmopenglcontext.h qwasmplatform.cpp qwasmplatform.h qwasmscreen.cpp qwasmscreen.h qwasmservices.cpp qwasmservices.h - qwasmstring.cpp qwasmstring.h - qwasmstylepixmaps_p.h qwasmtheme.cpp qwasmtheme.h qwasmwindow.cpp qwasmwindow.h + qwasmwindowclientarea.cpp qwasmwindowclientarea.h + qwasmwindowtreenode.cpp qwasmwindowtreenode.h + qwasmwindownonclientarea.cpp qwasmwindownonclientarea.h qwasminputcontext.cpp qwasminputcontext.h - qwasmdrag.cpp qwasmdrag.h qwasmwindowstack.cpp qwasmwindowstack.h + qwasmdrag.cpp qwasmdrag.h DEFINES QT_EGL_NO_X11 QT_NO_FOREACH @@ -48,7 +48,6 @@ qt_internal_add_plugin(QWasmIntegrationPlugin # Resources: set(wasmfonts_resource_files - "${QtBase_SOURCE_DIR}/src/3rdparty/wasm/Vera.ttf" "${QtBase_SOURCE_DIR}/src/3rdparty/wasm/DejaVuSans.ttf" "${QtBase_SOURCE_DIR}/src/3rdparty/wasm/DejaVuSansMono.ttf" ) @@ -70,7 +69,6 @@ qt_internal_extend_target(QWasmIntegrationPlugin CONDITION QT_FEATURE_opengl Qt::OpenGLPrivate ) -#### Keys ignored in scope 4:.:.:wasm.pro:NOT TARGET___equals____ss_QT_DEFAULT_QPA_PLUGIN: # PLUGIN_EXTENDS = "-" set(wasm_support_files diff --git a/src/plugins/platforms/wasm/main.cpp b/src/plugins/platforms/wasm/main.cpp index 1b430829ad..f32ef5aab8 100644 --- a/src/plugins/platforms/wasm/main.cpp +++ b/src/plugins/platforms/wasm/main.cpp @@ -6,6 +6,8 @@ QT_BEGIN_NAMESPACE +using namespace Qt::Literals::StringLiterals; + class QWasmIntegrationPlugin : public QPlatformIntegrationPlugin { Q_OBJECT @@ -17,7 +19,7 @@ public: QPlatformIntegration *QWasmIntegrationPlugin::create(const QString& system, const QStringList& paramList) { Q_UNUSED(paramList); - if (!system.compare(QStringLiteral("wasm"), Qt::CaseInsensitive)) + if (!system.compare("wasm"_L1, Qt::CaseInsensitive)) return new QWasmIntegration; return nullptr; diff --git a/src/plugins/platforms/wasm/qtloader.js b/src/plugins/platforms/wasm/qtloader.js index 75385e8111..8027dd8fa9 100644 --- a/src/plugins/platforms/wasm/qtloader.js +++ b/src/plugins/platforms/wasm/qtloader.js @@ -1,598 +1,301 @@ -// Copyright (C) 2018 The Qt Company Ltd. +// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only -// QtLoader provides javascript API for managing Qt application modules. -// -// QtLoader provides API on top of Emscripten which supports common lifecycle -// tasks such as displaying placeholder content while the module downloads, -// handing application exits, and checking for browser wasm support. -// -// There are two usage modes: -// * Managed: QtLoader owns and manages the HTML display elements like -// the loader and canvas. -// * External: The embedding HTML page owns the display elements. QtLoader -// provides event callbacks which the page reacts to. -// -// Managed mode usage: -// -// var config = { -// containerElements : [$("container-id")]; -// } -// var qtLoader = new QtLoader(config); -// qtLoader.loadEmscriptenModule("applicationName"); -// -// External mode usage: -// -// var config = { -// canvasElements : [$("canvas-id")], -// showLoader: function() { -// loader.style.display = 'block' -// canvas.style.display = 'hidden' -// }, -// showCanvas: function() { -// loader.style.display = 'hidden' -// canvas.style.display = 'block' -// return canvas; -// } -// } -// var qtLoader = new QtLoader(config); -// qtLoader.loadEmscriptenModule("applicationName"); -// -// Config keys -// -// containerElements : [container-element, ...] -// One or more HTML elements. QtLoader will display loader elements -// on these while loading the application, and replace the loader with a -// canvas on load complete. -// canvasElements : [canvas-element, ...] -// One or more canvas elements. -// showLoader : function(status, containerElement) -// Optional loading element constructor function. Implement to create -// a custom loading screen. This function may be called multiple times, -// while preparing the application binary. "status" is a string -// containing the loading sub-status, and may be either "Downloading", -// or "Compiling". The browser may be using streaming compilation, in -// which case the wasm module is compiled during downloading and the -// there is no separate compile step. -// showCanvas : function(containerElement) -// Optional canvas constructor function. Implement to create custom -// canvas elements. -// showExit : function(crashed, exitCode, containerElement) -// Optional exited element constructor function. -// showError : function(crashed, exitCode, containerElement) -// Optional error element constructor function. -// statusChanged : function(newStatus) -// Optional callback called when the status of the app has changed -// -// path : <string> -// Prefix path for wasm file, realative to the loading HMTL file. -// restartMode : "DoNotRestart", "RestartOnExit", "RestartOnCrash" -// Controls whether the application should be reloaded on exits. The default is "DoNotRestart" -// restartType : "RestartModule", "ReloadPage" -// restartLimit : <int> -// Restart attempts limit. The default is 10. -// stdoutEnabled : <bool> -// stderrEnabled : <bool> -// environment : <object> -// key-value environment variable pairs. -// -// QtLoader object API -// -// webAssemblySupported : bool -// webGLSupported : bool -// canLoadQt : bool -// Reports if WebAssembly and WebGL are supported. These are requirements for -// running Qt applications. -// loadEmscriptenModule(applicationName) -// Loads the application from the given emscripten javascript module file and wasm file -// status -// One of "Created", "Loading", "Running", "Exited". -// crashed -// Set to true if there was an unclean exit. -// exitCode -// main()/emscripten_force_exit() return code. Valid on status change to -// "Exited", iff crashed is false. -// exitText -// Abort/exit message. -// addCanvasElement -// Add canvas at run-time. Adds a corresponding QScreen, -// removeCanvasElement -// Remove canvas at run-time. Removes the corresponding QScreen. -// resizeCanvasElement -// Signals to the application that a canvas has been resized. -// setFontDpi -// Sets the logical font dpi for the application. -// module -// Returns the Emscripten module object, or undefined if the module -// has not been created yet. Note that the module object becomes available -// at the very end of the loading sequence, _after_ the transition from -// Loading to Running occurs. - - -// Forces the use of constructor on QtLoader instance. -// This passthrough makes both the old-style: -// -// const loader = QtLoader(config); -// -// and the new-style: -// -// const loader = new QtLoader(config); -// -// instantiation types work. -function QtLoader(config) +/** + * Loads the instance of a WASM module. + * + * @param config May contain any key normally accepted by emscripten and the 'qt' extra key, with + * the following sub-keys: + * - environment: { [name:string] : string } + * environment variables set on the instance + * - onExit: (exitStatus: { text: string, code?: number, crashed: bool }) => void + * called when the application has exited for any reason. There are two cases: + * aborted: crashed is true, text contains an error message. + * exited: crashed is false, code contians the exit code. + * + * Note that by default Emscripten does not exit when main() returns. This behavior + * is controlled by the EXIT_RUNTIME linker flag; set "-s EXIT_RUNTIME=1" to make + * Emscripten tear down the runtime and exit when main() returns. + * + * - containerElements: HTMLDivElement[] + * Array of host elements for Qt screens. Each of these elements is mapped to a QScreen on + * launch. + * - fontDpi: number + * Specifies font DPI for the instance + * - onLoaded: () => void + * Called when the module has loaded, at the point in time where any loading placeholder + * should be hidden and the application window should be shown. + * - entryFunction: (emscriptenConfig: object) => Promise<EmscriptenModule> + * Qt always uses emscripten's MODULARIZE option. This is the MODULARIZE entry function. + * - module: Promise<WebAssembly.Module> + * The module to create the instance from (optional). Specifying the module allows optimizing + * use cases where several instances are created from a single WebAssembly source. + * - qtdir: string + * Path to Qt installation. This path will be used for loading Qt shared libraries and plugins. + * The path is set to 'qt' by default, and is relative to the path of the web page's html file. + * This property is not in use when static linking is used, since this build mode includes all + * libraries and plugins in the wasm file. + * - preload: [string]: Array of file paths to json-encoded files which specifying which files to preload. + * The preloaded files will be downloaded at application startup and copied to the in-memory file + * system provided by Emscripten. + * + * Each json file must contain an array of source, destination objects: + * [ + * { + * "source": "path/to/source", + * "destination": "/path/to/destination" + * }, + * ... + * ] + * The source path is relative to the html file path. The destination path must be + * an absolute path. + * + * $QTDIR may be used as a placeholder for the "qtdir" configuration property (see @qtdir), for instance: + * "source": "$QTDIR/plugins/imageformats/libqjpeg.so" + * - localFonts.requestPermission: bool + * Whether Qt should request for local fonts access permission on startup (default false). + * - localFonts.familiesCollection string + * Specifies a collection of local fonts to load. Possible values are: + * "NoFontFamilies" : Don't load any font families + * "DefaultFontFamilies" : A subset of available font families; currently the "web-safe" fonts (default). + * "AllFontFamilies" : All local font families (not reccomended) + * - localFonts.extraFamilies: [string] + * Adds additional font families to be loaded at startup. + * + * @return Promise<instance: EmscriptenModule> + * The promise is resolved when the module has been instantiated and its main function has been + * called. + * + * @see https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/emscripten for + * EmscriptenModule + */ +async function qtLoad(config) { - return new _QtLoader(config); -} - -function _QtLoader(config) -{ - const self = this; - - // The Emscripten module and module configuration object. The module - // object is created in completeLoadEmscriptenModule(). - self.module = undefined; - self.moduleConfig = {}; - - // Qt properties. These are propagated to the Emscripten module after - // it has been created. - self.qtContainerElements = undefined; - self.qtFontDpi = 96; - - function webAssemblySupported() { - return typeof WebAssembly !== "undefined" - } - - function webGLSupported() { - // We expect that WebGL is supported if WebAssembly is; however - // the GPU may be blacklisted. - try { - var canvas = document.createElement("canvas"); - return !!(window.WebGLRenderingContext && (canvas.getContext("webgl") || canvas.getContext("experimental-webgl"))); - } catch (e) { - return false; + const throwIfEnvUsedButNotExported = (instance, config) => + { + const environment = config.environment; + if (!environment || Object.keys(environment).length === 0) + return; + const isEnvExported = typeof instance.ENV === 'object'; + if (!isEnvExported) + throw new Error('ENV must be exported if environment variables are passed'); + }; + + if (typeof config !== 'object') + throw new Error('config is required, expected an object'); + if (typeof config.qt !== 'object') + throw new Error('config.qt is required, expected an object'); + if (typeof config.qt.entryFunction !== 'function') + throw new Error('config.qt.entryFunction is required, expected a function'); + + config.qt.qtdir ??= 'qt'; + config.qt.preload ??= []; + + config.qtContainerElements = config.qt.containerElements; + delete config.qt.containerElements; + config.qtFontDpi = config.qt.fontDpi; + delete config.qt.fontDpi; + + // Make Emscripten not call main(); this gives us more control over + // the startup sequence. + const originalNoInitialRun = config.noInitialRun; + const originalArguments = config.arguments; + config.noInitialRun = true; + + // Used for rejecting a failed load's promise where emscripten itself does not allow it, + // like in instantiateWasm below. This allows us to throw in case of a load error instead of + // hanging on a promise to entry function, which emscripten unfortunately does. + let circuitBreakerReject; + const circuitBreaker = new Promise((_, reject) => { circuitBreakerReject = reject; }); + + // If module async getter is present, use it so that module reuse is possible. + if (config.qt.module) { + config.instantiateWasm = async (imports, successCallback) => + { + try { + const module = await config.qt.module; + successCallback( + await WebAssembly.instantiate(module, imports), module); + } catch (e) { + circuitBreakerReject(e); + } } } - - function canLoadQt() { - // The current Qt implementation requires WebAssembly (asm.js is not in use), - // and also WebGL (there is no raster fallback). - return webAssemblySupported() && webGLSupported(); - } - - function removeChildren(element) { - while (element.firstChild) element.removeChild(element.firstChild); - } - - function createCanvas() { - var canvas = document.createElement("canvas"); - canvas.className = "QtCanvas"; - canvas.style.height = "100%"; - canvas.style.width = "100%"; - - // Set contentEditable in order to enable clipboard events; hide the resulting focus frame. - canvas.contentEditable = true; - canvas.style.outline = "0px solid transparent"; - canvas.style.caretColor = "transparent"; - canvas.style.cursor = "default"; - - return canvas; - } - - // Set default state handler functions and create canvases if needed - if (config.containerElements !== undefined) { - - config.canvasElements = config.containerElements.map(createCanvas); - - config.showError = config.showError || function(errorText, container) { - removeChildren(container); - var errorTextElement = document.createElement("text"); - errorTextElement.className = "QtError" - errorTextElement.innerHTML = errorText; - return errorTextElement; + const fetchJsonHelper = async path => (await fetch(path)).json(); + const filesToPreload = (await Promise.all(config.qt.preload.map(fetchJsonHelper))).flat(); + const qtPreRun = (instance) => { + // Copy qt.environment to instance.ENV + throwIfEnvUsedButNotExported(instance, config); + for (const [name, value] of Object.entries(config.qt.environment ?? {})) + instance.ENV[name] = value; + + // Preload files from qt.preload + const makeDirs = (FS, filePath) => { + const parts = filePath.split("/"); + let path = "/"; + for (let i = 0; i < parts.length - 1; ++i) { + const part = parts[i]; + if (part == "") + continue; + path += part + "/"; + try { + FS.mkdir(path); + } catch (error) { + const EEXIST = 20; + if (error.errno != EEXIST) + throw error; + } + } } - config.showLoader = config.showLoader || function(loadingState, container) { - removeChildren(container); - var loadingText = document.createElement("text"); - loadingText.className = "QtLoading" - loadingText.innerHTML = '<p><center> ${loadingState}...</center><p>'; - return loadingText; - }; - - config.showCanvas = config.showCanvas || function(canvas, container) { - removeChildren(container); + const extractFilenameAndDir = (path) => { + const parts = path.split('/'); + const filename = parts.pop(); + const dir = parts.join('/'); + return { + filename: filename, + dir: dir + }; } - - config.showExit = config.showExit || function(crashed, exitCode, container) { - if (!crashed) - return undefined; - - removeChildren(container); - var fontSize = 54; - var crashSymbols = ["\u{1F615}", "\u{1F614}", "\u{1F644}", "\u{1F928}", "\u{1F62C}", - "\u{1F915}", "\u{2639}", "\u{1F62E}", "\u{1F61E}", "\u{1F633}"]; - var symbolIndex = Math.floor(Math.random() * crashSymbols.length); - var errorHtml = `<font size='${fontSize}'> ${crashSymbols[symbolIndex]} </font>` - var errorElement = document.createElement("text"); - errorElement.className = "QtExit" - errorElement.innerHTML = errorHtml; - return errorElement; + const preloadFile = (file) => { + makeDirs(instance.FS, file.destination); + const source = file.source.replace('$QTDIR', config.qt.qtdir); + const filenameAndDir = extractFilenameAndDir(file.destination); + instance.FS.createPreloadedFile(filenameAndDir.dir, filenameAndDir.filename, source, true, true); } + const isFsExported = typeof instance.FS === 'object'; + if (!isFsExported) + throw new Error('FS must be exported if preload is used'); + filesToPreload.forEach(preloadFile); } - config.restartMode = config.restartMode || "DoNotRestart"; - config.restartLimit = config.restartLimit || 10; - - if (config.stdoutEnabled === undefined) config.stdoutEnabled = true; - if (config.stderrEnabled === undefined) config.stderrEnabled = true; - - // Make sure config.path is defined and ends with "/" if needed - if (config.path === undefined) - config.path = ""; - if (config.path.length > 0 && !config.path.endsWith("/")) - config.path = config.path.concat("/"); - - if (config.environment === undefined) - config.environment = {}; - - var publicAPI = {}; - publicAPI.webAssemblySupported = webAssemblySupported(); - publicAPI.webGLSupported = webGLSupported(); - publicAPI.canLoadQt = canLoadQt(); - publicAPI.canLoadApplication = canLoadQt(); - publicAPI.status = undefined; - publicAPI.loadEmscriptenModule = loadEmscriptenModule; - publicAPI.addCanvasElement = addCanvasElement; - publicAPI.removeCanvasElement = removeCanvasElement; - publicAPI.resizeCanvasElement = resizeCanvasElement; - publicAPI.setFontDpi = setFontDpi; - publicAPI.fontDpi = fontDpi; - publicAPI.module = module; - - self.restartCount = 0; - - function handleError(error) { - self.error = error; - setStatus("Error"); - console.error(error); - } - - function fetchResource(filePath) { - var fullPath = config.path + filePath; - return fetch(fullPath).then(function(response) { - if (!response.ok) { - let err = response.status + " " + response.statusText + " " + response.url; - handleError(err); - return Promise.reject(err) - } else { - return response; - } - }); - } - - function fetchText(filePath) { - return fetchResource(filePath).then(function(response) { - return response.text(); - }); - } + if (!config.preRun) + config.preRun = []; + config.preRun.push(qtPreRun); - function fetchThenCompileWasm(response) { - return response.arrayBuffer().then(function(data) { - self.loaderSubState = "Compiling"; - setStatus("Loading") // trigger loaderSubState update - return WebAssembly.compile(data); - }); - } - - function fetchCompileWasm(filePath) { - return fetchResource(filePath).then(function(response) { - if (typeof WebAssembly.compileStreaming !== "undefined") { - self.loaderSubState = "Downloading/Compiling"; - setStatus("Loading"); - return WebAssembly.compileStreaming(response).catch(function(error) { - // compileStreaming may/will fail if the server does not set the correct - // mime type (application/wasm) for the wasm file. Fall back to fetch, - // then compile in this case. - return fetchThenCompileWasm(response); - }); - } else { - // Fall back to fetch, then compile if compileStreaming is not supported - return fetchThenCompileWasm(response); - } - }); + const originalOnRuntimeInitialized = config.onRuntimeInitialized; + config.onRuntimeInitialized = () => { + originalOnRuntimeInitialized?.(); + config.qt.onLoaded?.(); } - function loadEmscriptenModule(applicationName) { - - // Loading in qtloader.js goes through four steps: - // 1) Check prerequisites - // 2) Download resources - // 3) Configure the emscripten Module object - // 4) Start the emcripten runtime, after which emscripten takes over - - // Check for Wasm & WebGL support; set error and return before downloading resources if missing - if (!webAssemblySupported()) { - handleError("Error: WebAssembly is not supported"); - return; - } - if (!webGLSupported()) { - handleError("Error: WebGL is not supported"); - return; - } - - // Continue waiting if loadEmscriptenModule() is called again - if (publicAPI.status == "Loading") - return; - self.loaderSubState = "Downloading"; - setStatus("Loading"); - - // Fetch emscripten generated javascript runtime - var emscriptenModuleSource = undefined - var emscriptenModuleSourcePromise = fetchText(applicationName + ".js").then(function(source) { - emscriptenModuleSource = source - }); - - // Fetch and compile wasm module - var wasmModule = undefined; - var wasmModulePromise = fetchCompileWasm(applicationName + ".wasm").then(function (module) { - wasmModule = module; - }); - - // Wait for all resources ready - Promise.all([emscriptenModuleSourcePromise, wasmModulePromise]).then(function(){ - completeLoadEmscriptenModule(applicationName, emscriptenModuleSource, wasmModule); - }).catch(function(error) { - handleError(error); - // An error here is fatal, abort - self.moduleConfig.onAbort(error) - }); + const originalLocateFile = config.locateFile; + config.locateFile = filename => { + const originalLocatedFilename = originalLocateFile ? originalLocateFile(filename) : filename; + if (originalLocatedFilename.startsWith('libQt6')) + return `${config.qt.qtdir}/lib/${originalLocatedFilename}`; + return originalLocatedFilename; } - function completeLoadEmscriptenModule(applicationName, emscriptenModuleSource, wasmModule) { + let onExitCalled = false; + const originalOnExit = config.onExit; + config.onExit = code => { + originalOnExit?.(); - // The wasm binary has been compiled into a module during resource download, - // and is ready to be instantiated. Define the instantiateWasm callback which - // emscripten will call to create the instance. - self.moduleConfig.instantiateWasm = function(imports, successCallback) { - WebAssembly.instantiate(wasmModule, imports).then(function(instance) { - successCallback(instance, wasmModule); - }, function(error) { - handleError(error) + if (!onExitCalled) { + onExitCalled = true; + config.qt.onExit?.({ + code, + crashed: false }); - return {}; - }; - - self.moduleConfig.locateFile = self.moduleConfig.locateFile || function(filename) { - return config.path + filename; - }; - - // Attach status callbacks - self.moduleConfig.setStatus = self.moduleConfig.setStatus || function(text) { - // Currently the only usable status update from this function - // is "Running..." - if (text.startsWith("Running")) - setStatus("Running"); - }; - self.moduleConfig.monitorRunDependencies = self.moduleConfig.monitorRunDependencies || function(left) { - // console.log("monitorRunDependencies " + left) - }; - - // Attach standard out/err callbacks. - self.moduleConfig.print = self.moduleConfig.print || function(text) { - if (config.stdoutEnabled) - console.log(text) - }; - self.moduleConfig.printErr = self.moduleConfig.printErr || function(text) { - if (config.stderrEnabled) - console.warn(text) - }; - - // Error handling: set status to "Exited", update crashed and - // exitCode according to exit type. - // Emscripten will typically call printErr with the error text - // as well. Note that emscripten may also throw exceptions from - // async callbacks. These should be handled in window.onerror by user code. - self.moduleConfig.onAbort = self.moduleConfig.onAbort || function(text) { - publicAPI.crashed = true; - publicAPI.exitText = text; - setStatus("Exited"); - }; - self.moduleConfig.quit = self.moduleConfig.quit || function(code, exception) { - - // Emscripten (and Qt) supports exiting from main() while keeping the app - // running. Don't transition into the "Exited" state for clean exits. - if (code == 0) - return; - - if (exception.name == "ExitStatus") { - // Clean exit with code - publicAPI.exitText = undefined - publicAPI.exitCode = code; - } else { - publicAPI.exitText = exception.toString(); - publicAPI.crashed = true; - } - setStatus("Exited"); - }; - - self.moduleConfig.preRun = self.moduleConfig.preRun || [] - self.moduleConfig.preRun.push(function(module) { - // Set environment variables - for (var [key, value] of Object.entries(config.environment)) { - module.ENV[key.toUpperCase()] = value; - } - // Propagate Qt module properties - module.qtContainerElements = self.qtContainerElements; - module.qtFontDpi = self.qtFontDpi; - }); - - self.moduleConfig.mainScriptUrlOrBlob = new Blob([emscriptenModuleSource], {type: 'text/javascript'}); - - self.qtContainerElements = config.canvasElements; - - config.restart = function() { - - // Restart by reloading the page. This will wipe all state which means - // reload loops can't be prevented. - if (config.restartType == "ReloadPage") { - location.reload(); - } - - // Restart by readling the emscripten app module. - ++self.restartCount; - if (self.restartCount > config.restartLimit) { - handleError("Error: This application has crashed too many times and has been disabled. Reload the page to try again."); - return; - } - loadEmscriptenModule(applicationName); - }; - - publicAPI.exitCode = undefined; - publicAPI.exitText = undefined; - publicAPI.crashed = false; - - // Load the Emscripten application module. This is done by eval()'ing the - // javascript runtime generated by Emscripten, and then calling - // createQtAppInstance(), which was added to the global scope. - eval(emscriptenModuleSource); - createQtAppInstance(self.moduleConfig).then(function(module) { - self.module = module; - }); - } - - function setErrorContent() { - if (config.containerElements === undefined) { - if (config.showError !== undefined) - config.showError(self.error); - return; - } - - for (container of config.containerElements) { - var errorElement = config.showError(self.error, container); - container.appendChild(errorElement); } } - function setLoaderContent() { - if (config.containerElements === undefined) { - if (config.showLoader !== undefined) - config.showLoader(self.loaderSubState); - return; - } - - for (container of config.containerElements) { - var loaderElement = config.showLoader(self.loaderSubState, container); - container.appendChild(loaderElement); + const originalOnAbort = config.onAbort; + config.onAbort = text => + { + originalOnAbort?.(); + + if (!onExitCalled) { + onExitCalled = true; + config.qt.onExit?.({ + text, + crashed: true + }); } - } - - function setCanvasContent() { - if (config.containerElements === undefined) { - if (config.showCanvas !== undefined) - config.showCanvas(); + }; + + // Call app/emscripten module entry function. It may either come from the emscripten + // runtime script or be customized as needed. + let instance; + try { + instance = await Promise.race( + [circuitBreaker, config.qt.entryFunction(config)]); + + // Call main after creating the instance. We've opted into manually + // calling main() by setting noInitialRun in the config. Thie Works around + // issue where Emscripten suppresses all exceptions thrown during main. + if (!originalNoInitialRun) + instance.callMain(originalArguments); + } catch (e) { + // If this is the exception thrown by app.exec() then that is a normal + // case and we suppress it. + if (e == "unwind") // not much to go on return; - } - for (var i = 0; i < config.containerElements.length; ++i) { - var container = config.containerElements[i]; - var canvas = config.canvasElements[i]; - config.showCanvas(canvas, container); - container.appendChild(canvas); + if (!onExitCalled) { + onExitCalled = true; + config.qt.onExit?.({ + text: e.message, + crashed: true + }); } + throw e; } - function setExitContent() { - - // publicAPI.crashed = true; - - if (publicAPI.status != "Exited") - return; - - if (config.containerElements === undefined) { - if (config.showExit !== undefined) - config.showExit(publicAPI.crashed, publicAPI.exitCode); - return; - } - - if (!publicAPI.crashed) - return; + return instance; +} - for (container of config.containerElements) { - var loaderElement = config.showExit(publicAPI.crashed, publicAPI.exitCode, container); - if (loaderElement !== undefined) - container.appendChild(loaderElement); - } +// Compatibility API. This API is deprecated, +// and will be removed in a future version of Qt. +function QtLoader(qtConfig) { + + const warning = 'Warning: The QtLoader API is deprecated and will be removed in ' + + 'a future version of Qt. Please port to the new qtLoad() API.'; + console.warn(warning); + + let emscriptenConfig = qtConfig.moduleConfig || {} + qtConfig.moduleConfig = undefined; + const showLoader = qtConfig.showLoader; + qtConfig.showLoader = undefined; + const showError = qtConfig.showError; + qtConfig.showError = undefined; + const showExit = qtConfig.showExit; + qtConfig.showExit = undefined; + const showCanvas = qtConfig.showCanvas; + qtConfig.showCanvas = undefined; + if (qtConfig.canvasElements) { + qtConfig.containerElements = qtConfig.canvasElements + qtConfig.canvasElements = undefined; + } else { + qtConfig.containerElements = qtConfig.containerElements; + qtConfig.containerElements = undefined; } - - var committedStatus = undefined; - function handleStatusChange() { - if (publicAPI.status != "Loading" && committedStatus == publicAPI.status) - return; - committedStatus = publicAPI.status; - - if (publicAPI.status == "Error") { - setErrorContent(); - } else if (publicAPI.status == "Loading") { - setLoaderContent(); - } else if (publicAPI.status == "Running") { - setCanvasContent(); - } else if (publicAPI.status == "Exited") { - if (config.restartMode == "RestartOnExit" || - config.restartMode == "RestartOnCrash" && publicAPI.crashed) { - committedStatus = undefined; - config.restart(); - } else { - setExitContent(); + emscriptenConfig.qt = qtConfig; + + let qtloader = { + exitCode: undefined, + exitText: "", + loadEmscriptenModule: _name => { + try { + qtLoad(emscriptenConfig); + } catch (e) { + showError?.(e.message); } } - - // Send status change notification - if (config.statusChanged) - config.statusChanged(publicAPI.status); } - function setStatus(status) { - if (status != "Loading" && publicAPI.status == status) - return; - publicAPI.status = status; - - window.setTimeout(function() { handleStatusChange(); }, 0); - } - - function addCanvasElement(element) { - if (publicAPI.status == "Running") - self.module.qtAddContainerElement(element); - else - console.log("Error: addCanvasElement can only be called in the Running state"); + qtConfig.onLoaded = () => { + showCanvas?.(); } - function removeCanvasElement(element) { - if (publicAPI.status == "Running") - self.module.qtRemoveContainerElement(element); - else - console.log("Error: removeCanvasElement can only be called in the Running state"); + qtConfig.onExit = exit => { + qtloader.exitCode = exit.code + qtloader.exitText = exit.text; + showExit?.(); } - function resizeCanvasElement(element) { - if (publicAPI.status == "Running") - self.module.qtResizeContainerElement(element); - } - - function setFontDpi(dpi) { - self.qtFontDpi = dpi; - if (publicAPI.status == "Running") - self.module.qtUpdateDpi(); - } + showLoader?.("Loading"); - function fontDpi() { - return self.qtFontDpi; - } - - function module() { - return self.module; - } - - setStatus("Created"); - - return publicAPI; -} + return qtloader; +}; diff --git a/src/plugins/platforms/wasm/qwasmaccessibility.cpp b/src/plugins/platforms/wasm/qwasmaccessibility.cpp index eb3e30b140..4c3cb46ba3 100644 --- a/src/plugins/platforms/wasm/qwasmaccessibility.cpp +++ b/src/plugins/platforms/wasm/qwasmaccessibility.cpp @@ -3,9 +3,14 @@ #include "qwasmaccessibility.h" #include "qwasmscreen.h" - +#include "qwasmwindow.h" +#include "qwasmintegration.h" #include <QtGui/qwindow.h> +#if QT_CONFIG(accessibility) + +#include <QtGui/private/qaccessiblebridgeutils_p.h> + Q_LOGGING_CATEGORY(lcQpaAccessibility, "qt.qpa.accessibility") // Qt WebAssembly a11y backend @@ -18,33 +23,107 @@ Q_LOGGING_CATEGORY(lcQpaAccessibility, "qt.qpa.accessibility") // events. In addition or alternatively, we could also walk the accessibility tree // from setRootObject(). - QWasmAccessibility::QWasmAccessibility() { + s_instance = this; } QWasmAccessibility::~QWasmAccessibility() { + s_instance = nullptr; +} + +QWasmAccessibility *QWasmAccessibility::s_instance = nullptr; +QWasmAccessibility* QWasmAccessibility::get() +{ + return s_instance; +} + +void QWasmAccessibility::addAccessibilityEnableButton(QWindow *window) +{ + get()->addAccessibilityEnableButtonImpl(window); +} + +void QWasmAccessibility::removeAccessibilityEnableButton(QWindow *window) +{ + get()->removeAccessibilityEnableButtonImpl(window); +} + +void QWasmAccessibility::addAccessibilityEnableButtonImpl(QWindow *window) +{ + if (m_accessibilityEnabled) + return; + + emscripten::val container = getContainer(window); + emscripten::val document = getDocument(container); + emscripten::val button = document.call<emscripten::val>("createElement", std::string("button")); + button.set("innerText", std::string("Enable Screen Reader")); + button["classList"].call<void>("add", emscripten::val("hidden-visually-read-by-screen-reader")); + container.call<void>("appendChild", button); + + auto enableContext = std::make_tuple(button, std::make_unique<qstdweb::EventCallback> + (button, std::string("click"), [this](emscripten::val) { enableAccessibility(); })); + m_enableButtons.insert(std::make_pair(window, std::move(enableContext))); +} + +void QWasmAccessibility::removeAccessibilityEnableButtonImpl(QWindow *window) +{ + auto it = m_enableButtons.find(window); + if (it == m_enableButtons.end()) + return; + + // Remove button + auto [element, callback] = it->second; + Q_UNUSED(callback); + element["parentElement"].call<void>("removeChild", element); + m_enableButtons.erase(it); +} + +void QWasmAccessibility::enableAccessibility() +{ + // Enable accessibility globally for the applicaton. Remove all "enable" + // buttons and populate the accessibility tree, starting from the root object. + + Q_ASSERT(!m_accessibilityEnabled); + m_accessibilityEnabled = true; + for (const auto& [key, value] : m_enableButtons) { + const auto &[element, callback] = value; + Q_UNUSED(key); + Q_UNUSED(callback); + element["parentElement"].call<void>("removeChild", element); + } + m_enableButtons.clear(); + populateAccessibilityTree(QAccessible::queryAccessibleInterface(m_rootObject)); +} + +emscripten::val QWasmAccessibility::getContainer(QWindow *window) +{ + return window ? static_cast<QWasmWindow *>(window->handle())->a11yContainer() + : emscripten::val::undefined(); } emscripten::val QWasmAccessibility::getContainer(QAccessibleInterface *iface) { - // Get to QWasmScreen::container(), return undefined element if unable to - QWindow *window = iface->window(); - if (!window) + if (!iface) return emscripten::val::undefined(); - QWasmScreen *screen = QWasmScreen::get(window->screen()); - if (!screen) - return emscripten::val::undefined(); - return screen->element(); + return getContainer(getWindow(iface)); +} + +QWindow *QWasmAccessibility::getWindow(QAccessibleInterface *iface) +{ + QWindow *window = iface->window(); + // this is needed to add tabs as the window is not available + if (!window && iface->parent()) + window = iface->parent()->window(); + return window; } emscripten::val QWasmAccessibility::getDocument(const emscripten::val &container) { if (container.isUndefined()) - return emscripten::val::undefined(); + return emscripten::val::global("document"); return container["ownerDocument"]; } @@ -62,12 +141,12 @@ emscripten::val QWasmAccessibility::createHtmlElement(QAccessibleInterface *ifac // Get the correct html document for the container, or fall back // to the global document. TODO: Does using the correct document actually matter? - emscripten::val document = container.isUndefined() ? emscripten::val::global("document") : getDocument(container); + emscripten::val document = getDocument(container); // Translate the Qt a11y elemen role into html element type + ARIA role. // Here we can either create <div> elements with a spesific ARIA role, // or create e.g. <button> elements which should have built-in accessibility. - emscripten::val element = [iface, document] { + emscripten::val element = [this, iface, document] { emscripten::val element = emscripten::val::undefined(); @@ -75,17 +154,134 @@ emscripten::val QWasmAccessibility::createHtmlElement(QAccessibleInterface *ifac case QAccessible::Button: { element = document.call<emscripten::val>("createElement", std::string("button")); + element.call<void>("addEventListener", emscripten::val("click"), + emscripten::val::module_property("qtEventReceived"), true); } break; - case QAccessible::CheckBox: { element = document.call<emscripten::val>("createElement", std::string("input")); element.call<void>("setAttribute", std::string("type"), std::string("checkbox")); + if (iface->state().checked) { + element.call<void>("setAttribute", std::string("checked"), std::string("true")); + } + element.call<void>("addEventListener", emscripten::val("change"), + emscripten::val::module_property("qtEventReceived"), true); + + } break; + + case QAccessible::RadioButton: { + element = document.call<emscripten::val>("createElement", std::string("input")); + element.call<void>("setAttribute", std::string("type"), std::string("radio")); + if (iface->state().checked) { + element.call<void>("setAttribute", std::string("checked"), std::string("true")); + } + element.set(std::string("name"), std::string("buttonGroup")); + element.call<void>("addEventListener", emscripten::val("change"), + emscripten::val::module_property("qtEventReceived"), true); + } break; + + case QAccessible::SpinBox: { + element = document.call<emscripten::val>("createElement", std::string("input")); + element.call<void>("setAttribute", std::string("type"), std::string("number")); + std::string valueString = iface->valueInterface()->currentValue().toString().toStdString(); + element.call<void>("setAttribute", std::string("value"), valueString); + element.call<void>("addEventListener", emscripten::val("change"), + emscripten::val::module_property("qtEventReceived"), true); + } break; + + case QAccessible::Slider: { + element = document.call<emscripten::val>("createElement", std::string("input")); + element.call<void>("setAttribute", std::string("type"), std::string("range")); + std::string valueString = iface->valueInterface()->currentValue().toString().toStdString(); + element.call<void>("setAttribute", std::string("value"), valueString); + element.call<void>("addEventListener", emscripten::val("change"), + emscripten::val::module_property("qtEventReceived"), true); + } break; + + case QAccessible::PageTabList:{ + element = document.call<emscripten::val>("createElement", std::string("div")); + element.call<void>("setAttribute", std::string("role"), std::string("tablist")); + QString idName = iface->text(QAccessible::Name).replace(" ", "_"); + idName += "_tabList"; + element.call<void>("setAttribute", std::string("id"), idName.toStdString()); + + for (int i = 0; i < iface->childCount(); ++i) { + if (iface->child(i)->role() == QAccessible::PageTab){ + emscripten::val elementTab = emscripten::val::undefined(); + elementTab = ensureHtmlElement(iface->child(i)); + elementTab.call<void>("setAttribute", std::string("aria-owns"), idName.toStdString()); + setHtmlElementGeometry(iface->child(i)); + } + } + } break; + + case QAccessible::PageTab:{ + element = document.call<emscripten::val>("createElement", std::string("button")); + element.call<void>("setAttribute", std::string("role"), std::string("tab")); + QString text = iface->text(QAccessible::Name); + element.call<void>("setAttribute", std::string("title"), text.toStdString()); + element.call<void>("addEventListener", emscripten::val("click"), + emscripten::val::module_property("qtEventReceived"), true); + } break; + + case QAccessible::ScrollBar: { + element = document.call<emscripten::val>("createElement", std::string("div")); + element.call<void>("setAttribute", std::string("role"), std::string("scrollbar")); + std::string valueString = iface->valueInterface()->currentValue().toString().toStdString(); + element.call<void>("setAttribute", std::string("aria-valuenow"), valueString); + element.call<void>("addEventListener", emscripten::val("change"), + emscripten::val::module_property("qtEventReceived"), true); } break; + case QAccessible::StaticText: { + element = document.call<emscripten::val>("createElement", std::string("textarea")); + element.call<void>("setAttribute", std::string("readonly"), std::string("true")); + + } break; + case QAccessible::Dialog: { + element = document.call<emscripten::val>("createElement", std::string("dialog")); + }break; + case QAccessible::ToolBar:{ + element = document.call<emscripten::val>("createElement", std::string("div")); + QString text = iface->text(QAccessible::Name); + + element.call<void>("setAttribute", std::string("role"), std::string("toolbar")); + element.call<void>("setAttribute", std::string("title"), text.toStdString()); + element.call<void>("addEventListener", emscripten::val("click"), + emscripten::val::module_property("qtEventReceived"), true); + }break; + case QAccessible::MenuItem: + case QAccessible::ButtonMenu: { + element = document.call<emscripten::val>("createElement", std::string("button")); + QString text = iface->text(QAccessible::Name); + + element.call<void>("setAttribute", std::string("role"), std::string("menuitem")); + element.call<void>("setAttribute", std::string("title"), text.toStdString()); + element.call<void>("addEventListener", emscripten::val("click"), + emscripten::val::module_property("qtEventReceived"), true); + }break; + case QAccessible::MenuBar: + case QAccessible::PopupMenu: { + element = document.call<emscripten::val>("createElement",std::string("div")); + QString text = iface->text(QAccessible::Name); + element.call<void>("setAttribute", std::string("role"), std::string("menubar")); + element.call<void>("setAttribute", std::string("title"), text.toStdString()); + for (int i = 0; i < iface->childCount(); ++i) { + emscripten::val childElement = emscripten::val::undefined(); + childElement= ensureHtmlElement(iface->child(i)); + childElement.call<void>("setAttribute", std::string("aria-owns"), text.toStdString()); + setHtmlElementTextName(iface->child(i)); + setHtmlElementGeometry(iface->child(i)); + } + }break; + case QAccessible::EditableText: { + element = document.call<emscripten::val>("createElement", std::string("input")); + element.call<void>("setAttribute", std::string("type"),std::string("text")); + element.call<void>("addEventListener", emscripten::val("input"), + emscripten::val::module_property("qtEventReceived"), true); + } break; default: qCDebug(lcQpaAccessibility) << "TODO: createHtmlElement() handle" << iface->role(); element = document.call<emscripten::val>("createElement", std::string("div")); - //element.set("AriaRole", "foo"); } return element; @@ -136,14 +332,23 @@ void QWasmAccessibility::setHtmlElementVisibility(QAccessibleInterface *iface, b void QWasmAccessibility::setHtmlElementGeometry(QAccessibleInterface *iface) { emscripten::val element = ensureHtmlElement(iface); - setHtmlElementGeometry(iface, element); + + // QAccessibleInterface gives us the geometry in global (screen) coordinates. Translate that + // to window geometry in order to position elements relative to window origin. + QWindow *window = getWindow(iface); + if (!window) + qCWarning(lcQpaAccessibility) << "Unable to find window for" << iface << "setting null geometry"; + QRect screenGeometry = iface->rect(); + QPoint windowPos = window ? window->mapFromGlobal(screenGeometry.topLeft()) : QPoint(); + QRect windowGeometry(windowPos, screenGeometry.size()); + + setHtmlElementGeometry(element, windowGeometry); } -void QWasmAccessibility::setHtmlElementGeometry(QAccessibleInterface *iface, emscripten::val element) +void QWasmAccessibility::setHtmlElementGeometry(emscripten::val element, QRect geometry) { // Position the element using "position: absolute" in order to place // it under the corresponding Qt element in the screen. - QRect geometry = iface->rect(); emscripten::val style = element["style"]; style.set("position", std::string("absolute")); style.set("z-index", std::string("-1")); // FIXME: "0" should be sufficient to order beheind the @@ -161,18 +366,93 @@ void QWasmAccessibility::setHtmlElementTextName(QAccessibleInterface *iface) element.set("innerHTML", text.toStdString()); // FIXME: use something else than innerHTML } +void QWasmAccessibility::setHtmlElementTextNameLE(QAccessibleInterface *iface) { + emscripten::val element = ensureHtmlElement(iface); + QString text = iface->text(QAccessible::Name); + element.call<void>("setAttribute", std::string("name"), text.toStdString()); + QString value = iface->text(QAccessible::Value); + element.set("innerHTML", value.toStdString()); +} + +void QWasmAccessibility::setHtmlElementDescription(QAccessibleInterface *iface) { + emscripten::val element = ensureHtmlElement(iface); + QString desc = iface->text(QAccessible::Description); + element.call<void>("setAttribute", std::string("aria-description"), desc.toStdString()); +} + void QWasmAccessibility::handleStaticTextUpdate(QAccessibleEvent *event) { switch (event->type()) { case QAccessible::NameChanged: { setHtmlElementTextName(event->accessibleInterface()); } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; default: qCDebug(lcQpaAccessibility) << "TODO: implement handleStaticTextUpdate for event" << event->type(); break; } } +void QWasmAccessibility::handleLineEditUpdate(QAccessibleEvent *event) { + + switch (event->type()) { + case QAccessible::NameChanged: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::Focus: + case QAccessible::TextRemoved: + case QAccessible::TextInserted: + case QAccessible::TextCaretMoved: { + setHtmlElementTextNameLE(event->accessibleInterface()); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qCDebug(lcQpaAccessibility) << "TODO: implement handleLineEditUpdate for event" << event->type(); + break; + } +} + +void QWasmAccessibility::handleEventFromHtmlElement(const emscripten::val event) +{ + + QAccessibleInterface *iface = m_elements.key(event["target"]); + if (iface == nullptr) { + return; + } else { + QString eventType = QString::fromStdString(event["type"].as<std::string>()); + const auto& actionNames = QAccessibleBridgeUtils::effectiveActionNames(iface); + if (actionNames.contains(QAccessibleActionInterface::pressAction())) { + + iface->actionInterface()->doAction(QAccessibleActionInterface::pressAction()); + + } else if (actionNames.contains(QAccessibleActionInterface::toggleAction())) { + + iface->actionInterface()->doAction(QAccessibleActionInterface::toggleAction()); + + } else if (actionNames.contains(QAccessibleActionInterface::increaseAction()) || + actionNames.contains(QAccessibleActionInterface::decreaseAction())) { + + QString val = QString::fromStdString(event["target"]["value"].as<std::string>()); + + iface->valueInterface()->setCurrentValue(val.toInt()); + + } else if (eventType == "input") { + + // as EditableTextInterface is not implemented in qml accessibility + // so we need to check the role for text to update in the textbox during accessibility + + if (iface->editableTextInterface() || iface->role() == QAccessible::EditableText) { + std::string insertText = event["target"]["value"].as<std::string>(); + iface->setText(QAccessible::Value, QString::fromStdString(insertText)); + } + } + } +} + void QWasmAccessibility::handleButtonUpdate(QAccessibleEvent *event) { qCDebug(lcQpaAccessibility) << "TODO: implement handleButtonUpdate for event" << event->type(); @@ -181,17 +461,230 @@ void QWasmAccessibility::handleButtonUpdate(QAccessibleEvent *event) void QWasmAccessibility::handleCheckBoxUpdate(QAccessibleEvent *event) { switch (event->type()) { + case QAccessible::Focus: case QAccessible::NameChanged: { setHtmlElementTextName(event->accessibleInterface()); } break; + case QAccessible::StateChanged: { + QAccessibleInterface *accessible = event->accessibleInterface(); + emscripten::val element = ensureHtmlElement(accessible); + bool checkedString = accessible->state().checked ? true : false; + element.call<void>("setAttribute", std::string("checked"), checkedString); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; default: qCDebug(lcQpaAccessibility) << "TODO: implement handleCheckBoxUpdate for event" << event->type(); break; } } +void QWasmAccessibility::handleToolUpdate(QAccessibleEvent *event) +{ + QAccessibleInterface *iface = event->accessibleInterface(); + QString text = iface->text(QAccessible::Name); + QString desc = iface->text(QAccessible::Description); + switch (event->type()) { + case QAccessible::NameChanged: + case QAccessible::StateChanged:{ + emscripten::val element = ensureHtmlElement(iface); + element.call<void>("setAttribute", std::string("title"), text.toStdString()); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qCDebug(lcQpaAccessibility) << "TODO: implement handleToolUpdate for event" << event->type(); + break; + } +} +void QWasmAccessibility::handleMenuUpdate(QAccessibleEvent *event) +{ + QAccessibleInterface *iface = event->accessibleInterface(); + QString text = iface->text(QAccessible::Name); + QString desc = iface->text(QAccessible::Description); + switch (event->type()) { + case QAccessible::Focus: + case QAccessible::NameChanged: + case QAccessible::MenuStart ://"TODO: To implement later + case QAccessible::PopupMenuStart://"TODO: To implement later + case QAccessible::StateChanged:{ + emscripten::val element = ensureHtmlElement(iface); + element.call<void>("setAttribute", std::string("title"), text.toStdString()); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qCDebug(lcQpaAccessibility) << "TODO: implement handleMenuUpdate for event" << event->type(); + break; + } +} +void QWasmAccessibility::handleDialogUpdate(QAccessibleEvent *event) { + + switch (event->type()) { + case QAccessible::NameChanged: + case QAccessible::Focus: + case QAccessible::DialogStart: + case QAccessible::StateChanged: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qCDebug(lcQpaAccessibility) << "TODO: implement handleLineEditUpdate for event" << event->type(); + break; + } +} + +void QWasmAccessibility::populateAccessibilityTree(QAccessibleInterface *iface) +{ + if (!iface) + return; + + // Create html element for the interface, sync up properties. + ensureHtmlElement(iface); + const bool visible = !iface->state().invisible; + setHtmlElementVisibility(iface, visible); + setHtmlElementGeometry(iface); + setHtmlElementTextName(iface); + setHtmlElementDescription(iface); + + for (int i = 0; i < iface->childCount(); ++i) + populateAccessibilityTree(iface->child(i)); +} + +void QWasmAccessibility::handleRadioButtonUpdate(QAccessibleEvent *event) +{ + switch (event->type()) { + case QAccessible::Focus: + case QAccessible::NameChanged: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::StateChanged: { + QAccessibleInterface *accessible = event->accessibleInterface(); + emscripten::val element = ensureHtmlElement(accessible); + std::string checkedString = accessible->state().checked ? "true" : "false"; + element.call<void>("setAttribute", std::string("checked"), checkedString); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qDebug() << "TODO: implement handleRadioButtonUpdate for event" << event->type(); + break; + } +} + +void QWasmAccessibility::handleSpinBoxUpdate(QAccessibleEvent *event) +{ + switch (event->type()) { + case QAccessible::Focus: + case QAccessible::NameChanged: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::ValueChanged: { + QAccessibleInterface *accessible = event->accessibleInterface(); + emscripten::val element = ensureHtmlElement(accessible); + std::string valueString = accessible->valueInterface()->currentValue().toString().toStdString(); + element.call<void>("setAttribute", std::string("value"), valueString); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qDebug() << "TODO: implement handleSpinBoxUpdate for event" << event->type(); + break; + } +} + +void QWasmAccessibility::handleSliderUpdate(QAccessibleEvent *event) +{ + switch (event->type()) { + case QAccessible::Focus: + case QAccessible::NameChanged: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::ValueChanged: { + QAccessibleInterface *accessible = event->accessibleInterface(); + emscripten::val element = ensureHtmlElement(accessible); + std::string valueString = accessible->valueInterface()->currentValue().toString().toStdString(); + element.call<void>("setAttribute", std::string("value"), valueString); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qDebug() << "TODO: implement handleSliderUpdate for event" << event->type(); + break; + } +} + +void QWasmAccessibility::handleScrollBarUpdate(QAccessibleEvent *event) +{ + switch (event->type()) { + case QAccessible::Focus: + case QAccessible::NameChanged: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::ValueChanged: { + QAccessibleInterface *accessible = event->accessibleInterface(); + emscripten::val element = ensureHtmlElement(accessible); + std::string valueString = accessible->valueInterface()->currentValue().toString().toStdString(); + element.call<void>("setAttribute", std::string("aria-valuenow"), valueString); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qDebug() << "TODO: implement handleSliderUpdate for event" << event->type(); + break; + } + +} + +void QWasmAccessibility::handlePageTabUpdate(QAccessibleEvent *event) +{ + switch (event->type()) { + case QAccessible::NameChanged: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::Focus: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qDebug() << "TODO: implement handlePageTabUpdate for event" << event->type(); + break; + } +} + +void QWasmAccessibility::handlePageTabListUpdate(QAccessibleEvent *event) +{ + switch (event->type()) { + case QAccessible::NameChanged: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::Focus: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qDebug() << "TODO: implement handlePageTabUpdate for event" << event->type(); + break; + } +} void QWasmAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *event) { + if (!m_accessibilityEnabled) + return; + QAccessibleInterface *iface = event->accessibleInterface(); if (!iface) { qWarning() << "notifyAccessibilityUpdate with null a11y interface" ; @@ -202,12 +695,12 @@ void QWasmAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *event) // https://doc.qt.io/qt-5/qaccessible.html#Event-enum switch (event->type()) { case QAccessible::ObjectShow: - setHtmlElementVisibility(iface, true); // Sync up properties on show; setHtmlElementGeometry(iface); setHtmlElementTextName(iface); + setHtmlElementDescription(iface); return; break; @@ -232,16 +725,46 @@ void QWasmAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *event) case QAccessible::CheckBox: handleCheckBoxUpdate(event); break; + case QAccessible::EditableText: + handleLineEditUpdate(event); + break; + case QAccessible::Dialog: + handleDialogUpdate(event); + break; + case QAccessible::MenuItem: + case QAccessible::MenuBar: + case QAccessible::PopupMenu: + handleMenuUpdate(event); + break; + case QAccessible::ToolBar: + case QAccessible::ButtonMenu: + handleToolUpdate(event); + case QAccessible::RadioButton: + handleRadioButtonUpdate(event); + break; + case QAccessible::SpinBox: + handleSpinBoxUpdate(event); + break; + case QAccessible::Slider: + handleSliderUpdate(event); + break; + case QAccessible::PageTab: + handlePageTabUpdate(event); + break; + case QAccessible::PageTabList: + handlePageTabListUpdate(event); + break; + case QAccessible::ScrollBar: + handleScrollBarUpdate(event); + break; default: qCDebug(lcQpaAccessibility) << "TODO: implement notifyAccessibilityUpdate for role" << iface->role(); }; } -void QWasmAccessibility::setRootObject(QObject *o) +void QWasmAccessibility::setRootObject(QObject *root) { - qCDebug(lcQpaAccessibility) << "setRootObject" << o; - QAccessibleInterface *iface = QAccessible::queryAccessibleInterface(o); - Q_UNUSED(iface) + m_rootObject = root; } void QWasmAccessibility::initialize() @@ -253,3 +776,14 @@ void QWasmAccessibility::cleanup() { } + +void QWasmAccessibility::onHtmlEventReceived(emscripten::val event) +{ + static_cast<QWasmAccessibility *>(QWasmIntegration::get()->accessibility())->handleEventFromHtmlElement(event); +} + +EMSCRIPTEN_BINDINGS(qtButtonEvent) { + function("qtEventReceived", &QWasmAccessibility::onHtmlEventReceived); +} + +#endif // QT_CONFIG(accessibility) diff --git a/src/plugins/platforms/wasm/qwasmaccessibility.h b/src/plugins/platforms/wasm/qwasmaccessibility.h index 6d5330c560..c4be7f0d72 100644 --- a/src/plugins/platforms/wasm/qwasmaccessibility.h +++ b/src/plugins/platforms/wasm/qwasmaccessibility.h @@ -4,12 +4,21 @@ #ifndef QWASMACCESIBILITY_H #define QWASMACCESIBILITY_H +#include <QtCore/qtconfigmacros.h> +#include <QtGui/qtguiglobal.h> + +#if QT_CONFIG(accessibility) + #include <QtCore/qhash.h> +#include <private/qstdweb_p.h> #include <qpa/qplatformaccessibility.h> #include <emscripten/val.h> #include <QLoggingCategory> +#include <map> +#include <emscripten/bind.h> + Q_DECLARE_LOGGING_CATEGORY(lcQpaAccessibility) class QWasmAccessibility : public QPlatformAccessibility @@ -18,30 +27,66 @@ public: QWasmAccessibility(); ~QWasmAccessibility(); + static QWasmAccessibility* get(); + + static void addAccessibilityEnableButton(QWindow *window); + static void removeAccessibilityEnableButton(QWindow *window); + +private: + void addAccessibilityEnableButtonImpl(QWindow *window); + void removeAccessibilityEnableButtonImpl(QWindow *window); + void enableAccessibility(); + + static emscripten::val getContainer(QWindow *window); static emscripten::val getContainer(QAccessibleInterface *iface); static emscripten::val getDocument(const emscripten::val &container); static emscripten::val getDocument(QAccessibleInterface *iface); + static QWindow *getWindow(QAccessibleInterface *iface); emscripten::val createHtmlElement(QAccessibleInterface *iface); void destroyHtmlElement(QAccessibleInterface *iface); emscripten::val ensureHtmlElement(QAccessibleInterface *iface); void setHtmlElementVisibility(QAccessibleInterface *iface, bool visible); void setHtmlElementGeometry(QAccessibleInterface *iface); - void setHtmlElementGeometry(QAccessibleInterface *iface, emscripten::val element); + void setHtmlElementGeometry(emscripten::val element, QRect geometry); void setHtmlElementTextName(QAccessibleInterface *iface); + void setHtmlElementTextNameLE(QAccessibleInterface *iface); + void setHtmlElementDescription(QAccessibleInterface *iface); void handleStaticTextUpdate(QAccessibleEvent *event); void handleButtonUpdate(QAccessibleEvent *event); void handleCheckBoxUpdate(QAccessibleEvent *event); + void handleDialogUpdate(QAccessibleEvent *event); + void handleMenuUpdate(QAccessibleEvent *event); + void handleToolUpdate(QAccessibleEvent *event); + void handleLineEditUpdate(QAccessibleEvent *event); + void handleRadioButtonUpdate(QAccessibleEvent *event); + void handleSpinBoxUpdate(QAccessibleEvent *event); + void handlePageTabUpdate(QAccessibleEvent *event); + void handleSliderUpdate(QAccessibleEvent *event); + void handleScrollBarUpdate(QAccessibleEvent *event); + void handlePageTabListUpdate(QAccessibleEvent *event); + + void handleEventFromHtmlElement(const emscripten::val event); + void populateAccessibilityTree(QAccessibleInterface *iface); void notifyAccessibilityUpdate(QAccessibleEvent *event) override; void setRootObject(QObject *o) override; void initialize() override; void cleanup() override; +public: // public for EMSCRIPTEN_BINDINGS + static void onHtmlEventReceived(emscripten::val event); + private: + static QWasmAccessibility *s_instance; + QObject *m_rootObject = nullptr; + bool m_accessibilityEnabled = false; + std::map<QWindow *, std::tuple<emscripten::val, std::shared_ptr<qstdweb::EventCallback>>> m_enableButtons; QHash<QAccessibleInterface *, emscripten::val> m_elements; }; +#endif // QT_CONFIG(accessibility) + #endif diff --git a/src/plugins/platforms/wasm/qwasmbackingstore.cpp b/src/plugins/platforms/wasm/qwasmbackingstore.cpp index e962592862..a3c1ae8a50 100644 --- a/src/plugins/platforms/wasm/qwasmbackingstore.cpp +++ b/src/plugins/platforms/wasm/qwasmbackingstore.cpp @@ -4,6 +4,7 @@ #include "qwasmbackingstore.h" #include "qwasmwindow.h" #include "qwasmcompositor.h" +#include "qwasmdom.h" #include <QtGui/qpainter.h> #include <QtGui/qbackingstore.h> @@ -75,37 +76,8 @@ void QWasmBackingStore::updateTexture(QWasmWindow *window) clippedDpiScaledRegion |= r; } - for (const QRect &dirtyRect : clippedDpiScaledRegion) { - constexpr int BytesPerColor = 4; - if (dirtyRect.width() == imageRect.width()) { - // Copy a contiguous chunk of memory - // ............... - // OOOOOOOOOOOOOOO - // OOOOOOOOOOOOOOO -> image data - // OOOOOOOOOOOOOOO - // ............... - auto imageMemory = emscripten::typed_memory_view(dirtyRect.width() * dirtyRect.height() - * BytesPerColor, - m_image.constScanLine(dirtyRect.y())); - m_webImageDataArray["data"].call<void>("set", imageMemory); - } else { - // Go through the scanlines manually to set the individual lines in bulk. This is - // marginally less performant than the above. - // ............... - // ...OOOOOOOOO... r = 0 -> image data - // ...OOOOOOOOO... r = 1 -> image data - // ...OOOOOOOOO... r = 2 -> image data - // ............... - for (int r = 0; r < dirtyRect.height(); ++r) { - auto scanlineMemory = emscripten::typed_memory_view( - dirtyRect.width() * 4, - m_image.constScanLine(r) + BytesPerColor * dirtyRect.x()); - m_webImageDataArray["data"].call<void>("set", scanlineMemory, - (r * dirtyRect.width() + dirtyRect.x()) - * BytesPerColor); - } - } - } + for (const QRect &dirtyRect : clippedDpiScaledRegion) + dom::drawImageToWebImageDataArray(m_image, m_webImageDataArray, dirtyRect); m_dirty = QRegion(); } diff --git a/src/plugins/platforms/wasm/qwasmbase64iconstore.cpp b/src/plugins/platforms/wasm/qwasmbase64iconstore.cpp new file mode 100644 index 0000000000..8f05f082ea --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmbase64iconstore.cpp @@ -0,0 +1,40 @@ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmbase64iconstore.h" + +#include <QtCore/qfile.h> + +QT_BEGIN_NAMESPACE + +Q_GLOBAL_STATIC(Base64IconStore, globalWasmWindowIconStore); + +Base64IconStore::Base64IconStore() +{ + QString iconSources[static_cast<size_t>(IconType::Size)] = { + QStringLiteral(":/wasm-window/maximize.svg"), QStringLiteral(":/wasm-window/qtlogo.svg"), + QStringLiteral(":/wasm-window/restore.svg"), QStringLiteral(":/wasm-window/x.svg") + }; + + for (size_t iconType = static_cast<size_t>(IconType::First); + iconType < static_cast<size_t>(IconType::Size); ++iconType) { + QFile svgFile(iconSources[static_cast<size_t>(iconType)]); + if (!svgFile.open(QIODevice::ReadOnly)) + Q_ASSERT(false); // A resource should always be opened. + m_storage[static_cast<size_t>(iconType)] = svgFile.readAll().toBase64(); + } +} + +Base64IconStore::~Base64IconStore() = default; + +Base64IconStore *Base64IconStore::get() +{ + return globalWasmWindowIconStore(); +} + +std::string_view Base64IconStore::getIcon(IconType type) const +{ + return m_storage[static_cast<size_t>(type)]; +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmbase64iconstore.h b/src/plugins/platforms/wasm/qwasmbase64iconstore.h new file mode 100644 index 0000000000..89704f2d2c --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmbase64iconstore.h @@ -0,0 +1,37 @@ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMBASE64IMAGESTORE_H +#define QWASMBASE64IMAGESTORE_H + +#include <string> +#include <string_view> + +#include <QtCore/qtconfigmacros.h> + +QT_BEGIN_NAMESPACE +class Base64IconStore +{ +public: + enum class IconType { + Maximize, + First = Maximize, + QtLogo, + Restore, + X, + Size, + }; + + Base64IconStore(); + ~Base64IconStore(); + + static Base64IconStore *get(); + + std::string_view getIcon(IconType type) const; + +private: + std::string m_storage[static_cast<size_t>(IconType::Size)]; +}; + +QT_END_NAMESPACE +#endif // QWASMBASE64IMAGESTORE_H diff --git a/src/plugins/platforms/wasm/qwasmclipboard.cpp b/src/plugins/platforms/wasm/qwasmclipboard.cpp index 6959a1a4a5..1aa3ffa5b3 100644 --- a/src/plugins/platforms/wasm/qwasmclipboard.cpp +++ b/src/plugins/platforms/wasm/qwasmclipboard.cpp @@ -2,20 +2,19 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmclipboard.h" +#include "qwasmdom.h" +#include "qwasmevent.h" #include "qwasmwindow.h" -#include "qwasmstring.h" -#include <private/qstdweb_p.h> -#include <emscripten.h> -#include <emscripten/html5.h> -#include <emscripten/bind.h> -#include <emscripten/val.h> +#include <private/qstdweb_p.h> #include <QCoreApplication> #include <qpa/qwindowsysteminterface.h> #include <QBuffer> #include <QString> +#include <emscripten/val.h> + QT_BEGIN_NAMESPACE using namespace emscripten; @@ -27,12 +26,11 @@ static void commonCopyEvent(val event) // doing it this way seems to sanitize the text better that calling data() like down below if (_mimes->hasText()) { - event["clipboardData"].call<void>("setData", val("text/plain") - , QWasmString::fromQString(_mimes->text())); + event["clipboardData"].call<void>("setData", val("text/plain"), + _mimes->text().toEcmaString()); } if (_mimes->hasHtml()) { - event["clipboardData"].call<void>("setData", val("text/html") - , QWasmString::fromQString(_mimes->html())); + event["clipboardData"].call<void>("setData", val("text/html"), _mimes->html().toEcmaString()); } for (auto mimetype : _mimes->formats()) { @@ -40,8 +38,8 @@ static void commonCopyEvent(val event) continue; QByteArray ba = _mimes->data(mimetype); if (!ba.isEmpty()) - event["clipboardData"].call<void>("setData", QWasmString::fromQString(mimetype) - , val(ba.constData())); + event["clipboardData"].call<void>("setData", mimetype.toEcmaString(), + val(ba.constData())); } event.call<void>("preventDefault"); @@ -51,8 +49,8 @@ static void qClipboardCutTo(val event) { if (!QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi()) { // Send synthetic Ctrl+X to make the app cut data to Qt's clipboard - QWindowSystemInterface::handleKeyEvent<QWindowSystemInterface::SynchronousDelivery>( - 0, QEvent::KeyPress, Qt::Key_C, Qt::ControlModifier, "X"); + QWindowSystemInterface::handleKeyEvent( + 0, QEvent::KeyPress, Qt::Key_X, Qt::ControlModifier, "X"); } commonCopyEvent(event); @@ -62,85 +60,17 @@ static void qClipboardCopyTo(val event) { if (!QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi()) { // Send synthetic Ctrl+C to make the app copy data to Qt's clipboard - QWindowSystemInterface::handleKeyEvent<QWindowSystemInterface::SynchronousDelivery>( + QWindowSystemInterface::handleKeyEvent( 0, QEvent::KeyPress, Qt::Key_C, Qt::ControlModifier, "C"); } commonCopyEvent(event); } -static void qWasmClipboardPaste(QMimeData *mData) -{ - // Persist clipboard data so that the app can read it when handling the CTRL+V - QWasmIntegration::get()->clipboard()-> - QPlatformClipboard::setMimeData(mData, QClipboard::Clipboard); - - QWindowSystemInterface::handleKeyEvent<QWindowSystemInterface::SynchronousDelivery>( - 0, QEvent::KeyPress, Qt::Key_V, Qt::ControlModifier, "V"); -} - -static void qClipboardPasteTo(val dataTransfer) +static void qClipboardPasteTo(val event) { - val clipboardData = dataTransfer["clipboardData"]; - val types = clipboardData["types"]; - int typesCount = types["length"].as<int>(); - std::string stdMimeFormat; - QMimeData *mMimeData = new QMimeData; - for (int i = 0; i < typesCount; i++) { - stdMimeFormat = types[i].as<std::string>(); - QString mimeFormat = QString::fromStdString(stdMimeFormat); - if (mimeFormat.contains("STRING", Qt::CaseSensitive) || mimeFormat.contains("TEXT", Qt::CaseSensitive)) - continue; - - if (mimeFormat.contains("text")) { -// also "text/plain;charset=utf-8" -// "UTF8_STRING" "MULTIPLE" - val mimeData = clipboardData.call<val>("getData", val(stdMimeFormat)); // as DataTransfer - - const QString qstr = QWasmString::toQString(mimeData); + event.call<void>("preventDefault"); // prevent browser from handling drop event - if (qstr.length() > 0) { - if (mimeFormat.contains("text/html")) { - mMimeData->setHtml(qstr); - } else if (mimeFormat.isEmpty() || mimeFormat.contains("text/plain")) { - mMimeData->setText(qstr); // the type can be empty - } else { - mMimeData->setData(mimeFormat, qstr.toLocal8Bit());} - } - } else { - val items = clipboardData["items"]; - - int itemsCount = items["length"].as<int>(); - // handle data - for (int i = 0; i < itemsCount; i++) { - val item = items[i]; - val clipboardFile = item.call<emscripten::val>("getAsFile"); // string kind is handled above - if (clipboardFile.isUndefined() || item["kind"].as<std::string>() == "string" ) { - continue; - } - qstdweb::File file(clipboardFile); - - mimeFormat = QString::fromStdString(file.type()); - QByteArray fileContent; - fileContent.resize(file.size()); - - file.stream(fileContent.data(), [=]() { - if (!fileContent.isEmpty()) { - - if (mimeFormat.contains("image")) { - QImage image; - image.loadFromData(fileContent, nullptr); - mMimeData->setImageData(image); - } else { - mMimeData->setData(mimeFormat,fileContent.data()); - } - qWasmClipboardPaste(mMimeData); - } - }); - } // next item - } - } - if (!mMimeData->formats().isEmpty()) - qWasmClipboardPaste(mMimeData); + QWasmIntegration::get()->getWasmClipboard()->sendClipboardData(event); } EMSCRIPTEN_BINDINGS(qtClipboardModule) { @@ -182,11 +112,9 @@ void QWasmClipboard::setMimeData(QMimeData *mimeData, QClipboard::Mode mode) writeToClipboard(); } -QWasmClipboard::ProcessKeyboardResult -QWasmClipboard::processKeyboard(const QWasmEventTranslator::TranslatedEvent &event, - const QFlags<Qt::KeyboardModifier> &modifiers) +QWasmClipboard::ProcessKeyboardResult QWasmClipboard::processKeyboard(const KeyEvent &event) { - if (event.type != QEvent::KeyPress || !modifiers.testFlag(Qt::ControlModifier)) + if (event.type != EventType::KeyDown || !event.modifiers.testFlag(Qt::ControlModifier)) return ProcessKeyboardResult::Ignored; if (event.key != Qt::Key_C && event.key != Qt::Key_V && event.key != Qt::Key_X) @@ -226,15 +154,15 @@ void QWasmClipboard::initClipboardPermissions() })()); } -void QWasmClipboard::installEventHandlers(const emscripten::val &screenElement) +void QWasmClipboard::installEventHandlers(const emscripten::val &target) { emscripten::val cContext = val::undefined(); emscripten::val isChromium = val::global("window")["chrome"]; - if (!isChromium.isUndefined()) { + if (!isChromium.isUndefined()) { cContext = val::global("document"); - } else { - cContext = screenElement; - } + } else { + cContext = target; + } // Fallback path for browsers which do not support direct clipboard access cContext.call<void>("addEventListener", val("cut"), val::module_property("qtClipboardCutTo"), true); @@ -315,12 +243,12 @@ void QWasmClipboard::writeToClipboardApi() // we have a blob, now create a ClipboardItem emscripten::val type = emscripten::val::array(); - type.set("type", val(QWasmString::fromQString(mimetype))); + type.set("type", mimetype.toEcmaString()); emscripten::val contentBlob = emscripten::val::global("Blob").new_(contentArray, type); emscripten::val clipboardItemObject = emscripten::val::object(); - clipboardItemObject.set(val(QWasmString::fromQString(mimetype)), contentBlob); + clipboardItemObject.set(mimetype.toEcmaString(), contentBlob); val clipboardItemData = val::global("ClipboardItem").new_(clipboardItemObject); @@ -354,4 +282,23 @@ void QWasmClipboard::writeToClipboard() val document = val::global("document"); document.call<val>("execCommand", val("copy")); } + +void QWasmClipboard::sendClipboardData(emscripten::val event) +{ + qDebug() << "sendClipboardData"; + + dom::DataTransfer *transfer = new dom::DataTransfer(event["clipboardData"]); + const auto mimeCallback = std::function([transfer](QMimeData *data) { + + // Persist clipboard data so that the app can read it when handling the CTRL+V + QWasmIntegration::get()->clipboard()->QPlatformClipboard::setMimeData(data, QClipboard::Clipboard); + QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyPress, Qt::Key_V, + Qt::ControlModifier, "V"); + QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyRelease, Qt::Key_V, + Qt::ControlModifier, "V"); + delete transfer; + }); + + transfer->toMimeDataWithFile(mimeCallback); +} QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmclipboard.h b/src/plugins/platforms/wasm/qwasmclipboard.h index 924c3f582f..86618dd560 100644 --- a/src/plugins/platforms/wasm/qwasmclipboard.h +++ b/src/plugins/platforms/wasm/qwasmclipboard.h @@ -7,15 +7,16 @@ #include <QObject> #include <qpa/qplatformclipboard.h> +#include <private/qstdweb_p.h> #include <QMimeData> #include <emscripten/bind.h> #include <emscripten/val.h> -#include "qwasmeventtranslator.h" - QT_BEGIN_NAMESPACE +struct KeyEvent; + class QWasmClipboard : public QObject, public QPlatformClipboard { public: @@ -34,10 +35,10 @@ public: bool supportsMode(QClipboard::Mode mode) const override; bool ownsMode(QClipboard::Mode mode) const override; - ProcessKeyboardResult processKeyboard(const QWasmEventTranslator::TranslatedEvent &event, - const QFlags<Qt::KeyboardModifier> &modifiers); - void installEventHandlers(const emscripten::val &canvas); + ProcessKeyboardResult processKeyboard(const KeyEvent &event); + static void installEventHandlers(const emscripten::val &target); bool hasClipboardApi(); + void sendClipboardData(emscripten::val event); private: void initClipboardPermissions(); diff --git a/src/plugins/platforms/wasm/qwasmcompositor.cpp b/src/plugins/platforms/wasm/qwasmcompositor.cpp index b68f380fe7..ef460f666f 100644 --- a/src/plugins/platforms/wasm/qwasmcompositor.cpp +++ b/src/plugins/platforms/wasm/qwasmcompositor.cpp @@ -3,214 +3,56 @@ #include "qwasmcompositor.h" #include "qwasmwindow.h" -#include "qwasmeventtranslator.h" -#include "qwasmeventdispatcher.h" -#include "qwasmclipboard.h" -#include "qwasmevent.h" -#include <QtGui/private/qwindow_p.h> - -#include <private/qguiapplication_p.h> +#include <private/qeventdispatcher_wasm_p.h> #include <qpa/qwindowsysteminterface.h> -#include <QtCore/qcoreapplication.h> -#include <QtGui/qguiapplication.h> - -#include <emscripten/bind.h> -namespace { -QWasmWindow *asWasmWindow(QWindow *window) -{ - return static_cast<QWasmWindow*>(window->handle()); -} -} // namespace +#include <emscripten/html5.h> using namespace emscripten; -Q_GUI_EXPORT int qt_defaultDpiX(); +bool QWasmCompositor::m_requestUpdateHoldEnabled = true; -bool g_scrollingInvertedFromDevice = false; - -static void mouseWheelEvent(emscripten::val event) +QWasmCompositor::QWasmCompositor(QWasmScreen *screen) : QObject(screen) { - emscripten::val wheelInverted = event["webkitDirectionInvertedFromDevice"]; - if (wheelInverted.as<bool>()) - g_scrollingInvertedFromDevice = true; -} - -EMSCRIPTEN_BINDINGS(qtMouseModule) { - function("qtMouseWheelEvent", &mouseWheelEvent); -} - -QWasmCompositor::QWasmCompositor(QWasmScreen *screen) - : QObject(screen), - m_windowManipulation(screen), - m_windowStack(std::bind(&QWasmCompositor::onTopWindowChanged, this)), - m_eventTranslator(std::make_unique<QWasmEventTranslator>()) -{ - m_touchDevice = std::make_unique<QPointingDevice>( - "touchscreen", 1, QInputDevice::DeviceType::TouchScreen, - QPointingDevice::PointerType::Finger, - QPointingDevice::Capability::Position | QPointingDevice::Capability::Area - | QPointingDevice::Capability::NormalizedPosition, - 10, 0); - QWindowSystemInterface::registerInputDevice(m_touchDevice.get()); + QWindowSystemInterface::setSynchronousWindowSystemEvents(true); } QWasmCompositor::~QWasmCompositor() { - m_windowUnderMouse.clear(); - if (m_requestAnimationFrameId != -1) emscripten_cancel_animation_frame(m_requestAnimationFrameId); - deregisterEventHandlers(); - destroy(); -} - -void QWasmCompositor::deregisterEventHandlers() -{ - QByteArray screenElementSelector = screen()->eventTargetId().toUtf8(); - emscripten_set_keydown_callback(screenElementSelector.constData(), 0, 0, NULL); - emscripten_set_keyup_callback(screenElementSelector.constData(), 0, 0, NULL); - - emscripten_set_focus_callback(screenElementSelector.constData(), 0, 0, NULL); - - emscripten_set_wheel_callback(screenElementSelector.constData(), 0, 0, NULL); - - emscripten_set_touchstart_callback(screenElementSelector.constData(), 0, 0, NULL); - emscripten_set_touchend_callback(screenElementSelector.constData(), 0, 0, NULL); - emscripten_set_touchmove_callback(screenElementSelector.constData(), 0, 0, NULL); - emscripten_set_touchcancel_callback(screenElementSelector.constData(), 0, 0, NULL); - - screen()->element().call<void>("removeEventListener", std::string("drop"), - val::module_property("qtDrop"), val(true)); -} - -void QWasmCompositor::destroy() -{ // TODO(mikolaj.boc): Investigate if m_isEnabled is needed at all. It seems like a frame should // not be generated after this instead. m_isEnabled = false; // prevent frame() from creating a new m_context } -void QWasmCompositor::initEventHandlers() -{ - if (platform() == Platform::MacOS) { - if (!emscripten::val::global("window")["safari"].isUndefined()) { - screen()->element().call<void>("addEventListener", val("wheel"), - val::module_property("qtMouseWheelEvent")); - } - } - - constexpr EM_BOOL UseCapture = 1; - - const QByteArray screenElementSelector = screen()->eventTargetId().toUtf8(); - emscripten_set_keydown_callback(screenElementSelector.constData(), (void *)this, UseCapture, - &keyboard_cb); - emscripten_set_keyup_callback(screenElementSelector.constData(), (void *)this, UseCapture, - &keyboard_cb); - - val screenElement = screen()->element(); - const auto callback = std::function([this](emscripten::val event) { - if (processPointer(*PointerEvent::fromWeb(event))) - event.call<void>("preventDefault"); - }); - - m_pointerDownCallback = - std::make_unique<qstdweb::EventCallback>(screenElement, "pointerdown", callback); - m_pointerMoveCallback = - std::make_unique<qstdweb::EventCallback>(screenElement, "pointermove", callback); - m_pointerUpCallback = - std::make_unique<qstdweb::EventCallback>(screenElement, "pointerup", callback); - m_pointerEnterCallback = - std::make_unique<qstdweb::EventCallback>(screenElement, "pointerenter", callback); - m_pointerLeaveCallback = - std::make_unique<qstdweb::EventCallback>(screenElement, "pointerleave", callback); - - emscripten_set_focus_callback(screenElementSelector.constData(), (void *)this, UseCapture, - &focus_cb); - - emscripten_set_wheel_callback(screenElementSelector.constData(), (void *)this, UseCapture, - &wheel_cb); - - emscripten_set_touchstart_callback(screenElementSelector.constData(), (void *)this, UseCapture, - &touchCallback); - emscripten_set_touchend_callback(screenElementSelector.constData(), (void *)this, UseCapture, - &touchCallback); - emscripten_set_touchmove_callback(screenElementSelector.constData(), (void *)this, UseCapture, - &touchCallback); - emscripten_set_touchcancel_callback(screenElementSelector.constData(), (void *)this, UseCapture, - &touchCallback); - - screenElement.call<void>("addEventListener", std::string("drop"), - val::module_property("qtDrop"), val(true)); - screenElement.set("data-qtdropcontext", // ? unique - emscripten::val(quintptr(reinterpret_cast<void *>(screen())))); -} - -void QWasmCompositor::startResize(Qt::Edges edges) -{ - m_windowManipulation.startResize(edges); -} - -void QWasmCompositor::addWindow(QWasmWindow *window) -{ - m_windowStack.pushWindow(window); - m_windowStack.topWindow()->requestActivateWindow(); - - updateEnabledState(); -} - -void QWasmCompositor::removeWindow(QWasmWindow *window) -{ - m_requestUpdateWindows.remove(window); - m_windowStack.removeWindow(window); - if (m_windowStack.topWindow()) - m_windowStack.topWindow()->requestActivateWindow(); - - updateEnabledState(); -} - -void QWasmCompositor::updateEnabledState() +void QWasmCompositor::onWindowTreeChanged(QWasmWindowTreeNodeChangeType changeType, + QWasmWindow *window) { - m_isEnabled = std::any_of(m_windowStack.begin(), m_windowStack.end(), [](QWasmWindow *window) { - return !window->context2d().isUndefined(); - }); + auto allWindows = screen()->allWindows(); + setEnabled(std::any_of(allWindows.begin(), allWindows.end(), [](QWasmWindow *element) { + return !element->context2d().isUndefined(); + })); + if (changeType == QWasmWindowTreeNodeChangeType::NodeRemoval) + m_requestUpdateWindows.remove(window); } -void QWasmCompositor::raise(QWasmWindow *window) +void QWasmCompositor::setEnabled(bool enabled) { - m_windowStack.raise(window); + m_isEnabled = enabled; } -void QWasmCompositor::lower(QWasmWindow *window) +// requestUpdate delivery is initially disabled at startup, while Qt completes +// startup tasks such as font loading. This function enables requestUpdate delivery +// again. +bool QWasmCompositor::releaseRequestUpdateHold() { - m_windowStack.lower(window); -} - -QWindow *QWasmCompositor::windowAt(QPoint targetPointInScreenCoords, int padding) const -{ - const auto found = std::find_if( - m_windowStack.begin(), m_windowStack.end(), - [padding, &targetPointInScreenCoords](const QWasmWindow *window) { - const QRect geometry = window->windowFrameGeometry().adjusted(-padding, -padding, - padding, padding); - - return window->isVisible() && geometry.contains(targetPointInScreenCoords); - }); - return found != m_windowStack.end() ? (*found)->window() : nullptr; -} - -QWindow *QWasmCompositor::keyWindow() const -{ - return m_windowStack.topWindow() ? m_windowStack.topWindow()->window() : nullptr; -} - -void QWasmCompositor::requestUpdateAllWindows() -{ - m_requestUpdateAllWindows = true; - requestUpdate(); + const bool wasEnabled = m_requestUpdateHoldEnabled; + m_requestUpdateHoldEnabled = false; + return wasEnabled; } void QWasmCompositor::requestUpdateWindow(QWasmWindow *window, UpdateRequestDeliveryType updateType) @@ -234,6 +76,9 @@ void QWasmCompositor::requestUpdate() if (m_requestAnimationFrameId != -1) return; + if (m_requestUpdateHoldEnabled) + return; + static auto frame = [](double frameTime, void *context) -> int { Q_UNUSED(frameTime); @@ -254,40 +99,40 @@ void QWasmCompositor::deliverUpdateRequests() // update set. auto requestUpdateWindows = m_requestUpdateWindows; m_requestUpdateWindows.clear(); - bool requestUpdateAllWindows = m_requestUpdateAllWindows; - m_requestUpdateAllWindows = false; // Update window content, either all windows or a spesific set of windows. Use the correct // update type: QWindow subclasses expect that requested and delivered updateRequests matches // exactly. m_inDeliverUpdateRequest = true; - if (requestUpdateAllWindows) { - for (QWasmWindow *window : m_windowStack) { - auto it = requestUpdateWindows.find(window); - UpdateRequestDeliveryType updateType = - (it == m_requestUpdateWindows.end() ? ExposeEventDelivery : it.value()); - deliverUpdateRequest(window, updateType); - } - } else { - for (auto it = requestUpdateWindows.constBegin(); it != requestUpdateWindows.constEnd(); ++it) { - auto *window = it.key(); - UpdateRequestDeliveryType updateType = it.value(); - deliverUpdateRequest(window, updateType); - } + for (auto it = requestUpdateWindows.constBegin(); it != requestUpdateWindows.constEnd(); ++it) { + auto *window = it.key(); + UpdateRequestDeliveryType updateType = it.value(); + deliverUpdateRequest(window, updateType); } + m_inDeliverUpdateRequest = false; - frame(requestUpdateAllWindows, requestUpdateWindows.keys()); + frame(requestUpdateWindows.keys()); } void QWasmCompositor::deliverUpdateRequest(QWasmWindow *window, UpdateRequestDeliveryType updateType) { - // update by deliverUpdateRequest and expose event accordingly. + QWindow *qwindow = window->window(); + + // Make sure the DPR value for the window is up to date on expose/repaint. + // FIXME: listen to native DPR change events instead, if/when available. + QWindowSystemInterface::handleWindowDevicePixelRatioChanged(qwindow); + + // Update by deliverUpdateRequest and expose event according to requested update + // type. If the window has not yet been exposed then we must expose it first regardless + // of update type. The deliverUpdateRequest must still be sent in this case in order + // to maintain correct window update state. + QRect updateRect(QPoint(0, 0), qwindow->geometry().size()); if (updateType == UpdateRequestDelivery) { - window->QPlatformWindow::deliverUpdateRequest(); + if (qwindow->isExposed() == false) + QWindowSystemInterface::handleExposeEvent(qwindow, updateRect); + window->deliverUpdateRequest(); } else { - QWindow *qwindow = window->window(); - QWindowSystemInterface::handleExposeEvent<QWindowSystemInterface::SynchronousDelivery>( - qwindow, QRect(QPoint(0, 0), qwindow->geometry().size())); + QWindowSystemInterface::handleExposeEvent(qwindow, updateRect); } } @@ -296,557 +141,19 @@ void QWasmCompositor::handleBackingStoreFlush(QWindow *window) // Request update to flush the updated backing store content, unless we are currently // processing an update, in which case the new content will flushed as a part of that update. if (!m_inDeliverUpdateRequest) - requestUpdateWindow(asWasmWindow(window)); -} - -int dpiScaled(qreal value) -{ - return value * (qreal(qt_defaultDpiX()) / 96.0); + requestUpdateWindow(static_cast<QWasmWindow *>(window->handle())); } -void QWasmCompositor::frame(bool all, const QList<QWasmWindow *> &windows) +void QWasmCompositor::frame(const QList<QWasmWindow *> &windows) { - if (!m_isEnabled || m_windowStack.empty() || !screen()) + if (!m_isEnabled || !screen()) return; - if (all) { - std::for_each(m_windowStack.rbegin(), m_windowStack.rend(), - [](QWasmWindow *window) { window->paint(); }); - } else { - std::for_each(windows.begin(), windows.end(), [](QWasmWindow *window) { window->paint(); }); - } -} - -void QWasmCompositor::WindowManipulation::resizeWindow(const QPoint& amount) -{ - const auto& minShrink = std::get<ResizeState>(m_state->operationSpecific).m_minShrink; - const auto& maxGrow = std::get<ResizeState>(m_state->operationSpecific).m_maxGrow; - const auto &resizeEdges = std::get<ResizeState>(m_state->operationSpecific).m_resizeEdges; - - const QPoint cappedGrowVector( - std::min(maxGrow.x(), - std::max(minShrink.x(), - (resizeEdges & Qt::Edge::LeftEdge) ? -amount.x() - : (resizeEdges & Qt::Edge::RightEdge) ? amount.x() - : 0)), - std::min(maxGrow.y(), - std::max(minShrink.y(), - (resizeEdges & Qt::Edge::TopEdge) ? -amount.y() - : (resizeEdges & Qt::Edge::BottomEdge) ? amount.y() - : 0))); - - const auto& initialBounds = - std::get<ResizeState>(m_state->operationSpecific).m_initialWindowBounds; - m_state->window->setGeometry(initialBounds.adjusted( - (resizeEdges & Qt::Edge::LeftEdge) ? -cappedGrowVector.x() : 0, - (resizeEdges & Qt::Edge::TopEdge) ? -cappedGrowVector.y() : 0, - (resizeEdges & Qt::Edge::RightEdge) ? cappedGrowVector.x() : 0, - (resizeEdges & Qt::Edge::BottomEdge) ? cappedGrowVector.y() : 0)); -} - -void QWasmCompositor::onTopWindowChanged() -{ - constexpr int zOrderForElementInFrontOfScreen = 3; - int z = zOrderForElementInFrontOfScreen; - std::for_each(m_windowStack.rbegin(), m_windowStack.rend(), - [&z](QWasmWindow *window) { window->setZOrder(z++); }); - - auto it = m_windowStack.begin(); - if (it == m_windowStack.end()) { - return; - } - (*it)->onActivationChanged(true); - ++it; - for (; it != m_windowStack.end(); ++it) { - (*it)->onActivationChanged(false); - } + for (QWasmWindow *window : windows) + window->paint(); } QWasmScreen *QWasmCompositor::screen() { return static_cast<QWasmScreen *>(parent()); } - -int QWasmCompositor::keyboard_cb(int eventType, const EmscriptenKeyboardEvent *keyEvent, void *userData) -{ - QWasmCompositor *wasmCompositor = reinterpret_cast<QWasmCompositor *>(userData); - return static_cast<int>(wasmCompositor->processKeyboard(eventType, keyEvent)); -} - -int QWasmCompositor::focus_cb(int eventType, const EmscriptenFocusEvent *focusEvent, void *userData) -{ - Q_UNUSED(eventType) - Q_UNUSED(focusEvent) - Q_UNUSED(userData) - - return 0; -} - -int QWasmCompositor::wheel_cb(int eventType, const EmscriptenWheelEvent *wheelEvent, void *userData) -{ - QWasmCompositor *compositor = (QWasmCompositor *) userData; - return static_cast<int>(compositor->processWheel(eventType, wheelEvent)); -} - -int QWasmCompositor::touchCallback(int eventType, const EmscriptenTouchEvent *touchEvent, void *userData) -{ - auto compositor = reinterpret_cast<QWasmCompositor*>(userData); - return static_cast<int>(compositor->processTouch(eventType, touchEvent)); -} - -bool QWasmCompositor::processPointer(const PointerEvent& event) -{ - if (event.pointerType != PointerType::Mouse) - return false; - - QWindow *const targetWindow = ([this, &event]() -> QWindow * { - auto *targetWindow = m_mouseCaptureWindow != nullptr ? m_mouseCaptureWindow.get() - : m_windowManipulation.operation() == WindowManipulation::Operation::None - ? screen()->compositor()->windowAt(event.point, 5) - : nullptr; - - return targetWindow ? targetWindow : m_lastMouseTargetWindow.get(); - })(); - if (!targetWindow) - return false; - m_lastMouseTargetWindow = targetWindow; - - const QPoint pointInTargetWindowCoords = targetWindow->mapFromGlobal(event.point); - const bool pointerIsWithinTargetWindowBounds = targetWindow->geometry().contains(event.point); - const bool isTargetWindowBlocked = QGuiApplicationPrivate::instance()->isWindowBlocked(targetWindow); - - if (m_mouseInScreen && m_windowUnderMouse != targetWindow - && pointerIsWithinTargetWindowBounds) { - // delayed mouse enter - enterWindow(targetWindow, pointInTargetWindowCoords, event.point); - m_windowUnderMouse = targetWindow; - } - - QWasmWindow *wasmTargetWindow = asWasmWindow(targetWindow); - Qt::WindowStates windowState = targetWindow->windowState(); - const bool isTargetWindowResizable = !windowState.testFlag(Qt::WindowMaximized) && !windowState.testFlag(Qt::WindowFullScreen); - - switch (event.type) { - case EventType::PointerDown: - { - screen()->element().call<void>("setPointerCapture", event.pointerId); - - if (targetWindow) - targetWindow->requestActivate(); - - m_windowManipulation.onPointerDown(event, targetWindow); - break; - } - case EventType::PointerUp: - { - screen()->element().call<void>("releasePointerCapture", event.pointerId); - - m_windowManipulation.onPointerUp(event); - break; - } - case EventType::PointerMove: - { - if (wasmTargetWindow && event.mouseButtons.testFlag(Qt::NoButton)) { - const bool isOnResizeRegion = wasmTargetWindow->isPointOnResizeRegion(event.point); - - if (isTargetWindowResizable && isOnResizeRegion && !isTargetWindowBlocked) { - const QCursor resizingCursor = QWasmEventTranslator::cursorForEdges( - wasmTargetWindow->resizeEdgesAtPoint(event.point)); - - if (resizingCursor != targetWindow->cursor()) { - m_isResizeCursorDisplayed = true; - QWasmCursor::setOverrideWasmCursor(resizingCursor, targetWindow->screen()); - } - } else if (m_isResizeCursorDisplayed) { // off resizing area - m_isResizeCursorDisplayed = false; - QWasmCursor::clearOverrideWasmCursor(targetWindow->screen()); - } - } - - m_windowManipulation.onPointerMove(event); - if (m_windowManipulation.operation() != WindowManipulation::Operation::None) - requestUpdate(); - break; - } - case EventType::PointerEnter: - processMouseEnter(nullptr); - break; - case EventType::PointerLeave: - processMouseLeave(); - break; - default: - break; - }; - - if (!pointerIsWithinTargetWindowBounds && event.mouseButtons.testFlag(Qt::NoButton)) { - leaveWindow(m_lastMouseTargetWindow); - } - - const bool eventAccepted = deliverEventToTarget(event, targetWindow); - if (!eventAccepted && event.type == EventType::PointerDown) - QGuiApplicationPrivate::instance()->closeAllPopups(); - return eventAccepted; -} - -bool QWasmCompositor::deliverEventToTarget(const PointerEvent &event, QWindow *eventTarget) -{ - Q_ASSERT(!m_mouseCaptureWindow || m_mouseCaptureWindow.get() == eventTarget); - - const QPoint targetPointClippedToScreen( - std::max(screen()->geometry().left(), - std::min(screen()->geometry().right(), event.point.x())), - std::max(screen()->geometry().top(), - std::min(screen()->geometry().bottom(), event.point.y()))); - - bool deliveringToPreviouslyClickedWindow = false; - - if (!eventTarget) { - if (event.type != EventType::PointerUp || !m_lastMouseTargetWindow) - return false; - - eventTarget = m_lastMouseTargetWindow; - m_lastMouseTargetWindow = nullptr; - deliveringToPreviouslyClickedWindow = true; - } - - WindowArea windowArea = WindowArea::Client; - if (!deliveringToPreviouslyClickedWindow && !m_mouseCaptureWindow - && !eventTarget->geometry().contains(targetPointClippedToScreen)) { - if (!eventTarget->frameGeometry().contains(targetPointClippedToScreen)) - return false; - windowArea = WindowArea::NonClient; - } - - const QEvent::Type eventType = - MouseEvent::mouseEventTypeFromEventType(event.type, windowArea); - - return eventType != QEvent::None && - QWindowSystemInterface::handleMouseEvent<QWindowSystemInterface::SynchronousDelivery>( - eventTarget, QWasmIntegration::getTimestamp(), - eventTarget->mapFromGlobal(targetPointClippedToScreen), - targetPointClippedToScreen, event.mouseButtons, event.mouseButton, - eventType, event.modifiers); -} - -QWasmCompositor::WindowManipulation::WindowManipulation(QWasmScreen *screen) - : m_screen(screen) -{ - Q_ASSERT(!!screen); -} - -QWasmCompositor::WindowManipulation::Operation QWasmCompositor::WindowManipulation::operation() const -{ - if (!m_state) - return Operation::None; - - return std::holds_alternative<MoveState>(m_state->operationSpecific) - ? Operation::Move : Operation::Resize; -} - -void QWasmCompositor::WindowManipulation::onPointerDown( - const PointerEvent& event, QWindow* windowAtPoint) -{ - // Only one operation at a time. - if (operation() != Operation::None) - return; - - if (event.mouseButton != Qt::MouseButton::LeftButton) - return; - - const bool isTargetWindowResizable = - !windowAtPoint->windowStates().testFlag(Qt::WindowMaximized) && - !windowAtPoint->windowStates().testFlag(Qt::WindowFullScreen); - if (!isTargetWindowResizable) - return; - - const bool isTargetWindowBlocked = - QGuiApplicationPrivate::instance()->isWindowBlocked(windowAtPoint); - if (isTargetWindowBlocked) - return; - - std::unique_ptr<std::variant<ResizeState, MoveState>> operationSpecific; - if (asWasmWindow(windowAtPoint)->isPointOnTitle(event.point)) { - operationSpecific = std::make_unique<std::variant<ResizeState, MoveState>>( - MoveState{ .m_lastPointInScreenCoords = event.point }); - } else if (asWasmWindow(windowAtPoint)->isPointOnResizeRegion(event.point)) { - operationSpecific = std::make_unique<std::variant<ResizeState, MoveState>>(ResizeState{ - .m_resizeEdges = asWasmWindow(windowAtPoint)->resizeEdgesAtPoint(event.point), - .m_originInScreenCoords = event.point, - .m_initialWindowBounds = windowAtPoint->geometry(), - .m_minShrink = - QPoint(windowAtPoint->minimumWidth() - windowAtPoint->geometry().width(), - windowAtPoint->minimumHeight() - windowAtPoint->geometry().height()), - .m_maxGrow = - QPoint(windowAtPoint->maximumWidth() - windowAtPoint->geometry().width(), - windowAtPoint->maximumHeight() - windowAtPoint->geometry().height()), - }); - } else { - return; - } - - m_state.reset(new OperationState{ - .pointerId = event.pointerId, - .window = windowAtPoint, - .operationSpecific = std::move(*operationSpecific), - }); -} - -void QWasmCompositor::WindowManipulation::onPointerMove( - const PointerEvent& event) -{ - m_systemDragInitData = { - .lastMouseMovePoint = m_screen->clipPoint(event.point), - .lastMousePointerId = event.pointerId, - }; - - if (operation() == Operation::None || event.pointerId != m_state->pointerId) - return; - - switch (operation()) { - case Operation::Move: { - const QPoint targetPointClippedToScreen = m_screen->clipPoint(event.point); - const QPoint difference = targetPointClippedToScreen - - std::get<MoveState>(m_state->operationSpecific).m_lastPointInScreenCoords; - - std::get<MoveState>(m_state->operationSpecific).m_lastPointInScreenCoords = targetPointClippedToScreen; - - m_state->window->setPosition(m_state->window->position() + difference); - break; - } - case Operation::Resize: { - const auto pointInScreenCoords = m_screen->geometry().topLeft() + event.point; - resizeWindow(pointInScreenCoords - - std::get<ResizeState>(m_state->operationSpecific).m_originInScreenCoords); - break; - } - case Operation::None: - Q_ASSERT(0); - break; - } -} - -void QWasmCompositor::WindowManipulation::onPointerUp(const PointerEvent& event) -{ - if (operation() == Operation::None || event.mouseButtons != 0 || event.pointerId != m_state->pointerId) - return; - - m_state.reset(); -} - -void QWasmCompositor::WindowManipulation::startResize(Qt::Edges edges) -{ - Q_ASSERT_X(operation() == Operation::None, Q_FUNC_INFO, - "Resize must not start anew when one is in progress"); - - auto *window = m_screen->compositor()->windowAt(m_systemDragInitData.lastMouseMovePoint); - if (Q_UNLIKELY(!window)) - return; - - m_state.reset(new OperationState{ - .pointerId = m_systemDragInitData.lastMousePointerId, - .window = window, - .operationSpecific = - ResizeState{ - .m_resizeEdges = edges, - .m_originInScreenCoords = m_systemDragInitData.lastMouseMovePoint, - .m_initialWindowBounds = window->geometry(), - .m_minShrink = - QPoint(window->minimumWidth() - window->geometry().width(), - window->minimumHeight() - window->geometry().height()), - .m_maxGrow = - QPoint(window->maximumWidth() - window->geometry().width(), - window->maximumHeight() - window->geometry().height()), - }, - }); - m_screen->element().call<void>("setPointerCapture", m_systemDragInitData.lastMousePointerId); -} - -bool QWasmCompositor::processKeyboard(int eventType, const EmscriptenKeyboardEvent *emKeyEvent) -{ - constexpr bool ProceedToNativeEvent = false; - Q_ASSERT(eventType == EMSCRIPTEN_EVENT_KEYDOWN || eventType == EMSCRIPTEN_EVENT_KEYUP); - - auto translatedEvent = m_eventTranslator->translateKeyEvent(eventType, emKeyEvent); - - const QFlags<Qt::KeyboardModifier> modifiers = KeyboardModifier::getForEvent(*emKeyEvent); - - const auto clipboardResult = QWasmIntegration::get()->getWasmClipboard()->processKeyboard( - translatedEvent, modifiers); - - using ProcessKeyboardResult = QWasmClipboard::ProcessKeyboardResult; - if (clipboardResult == ProcessKeyboardResult::NativeClipboardEventNeeded) - return ProceedToNativeEvent; - - if (translatedEvent.text.isEmpty()) - translatedEvent.text = QString(emKeyEvent->key); - if (translatedEvent.text.size() > 1) - translatedEvent.text.clear(); - const auto result = - QWindowSystemInterface::handleKeyEvent<QWindowSystemInterface::SynchronousDelivery>( - 0, translatedEvent.type, translatedEvent.key, modifiers, translatedEvent.text); - return clipboardResult == ProcessKeyboardResult::NativeClipboardEventAndCopiedDataNeeded - ? ProceedToNativeEvent - : result; -} - -bool QWasmCompositor::processWheel(int eventType, const EmscriptenWheelEvent *wheelEvent) -{ - Q_UNUSED(eventType); - - const EmscriptenMouseEvent* mouseEvent = &wheelEvent->mouse; - - int scrollFactor = 0; - switch (wheelEvent->deltaMode) { - case DOM_DELTA_PIXEL: - scrollFactor = 1; - break; - case DOM_DELTA_LINE: - scrollFactor = 12; - break; - case DOM_DELTA_PAGE: - scrollFactor = 20; - break; - }; - - scrollFactor = -scrollFactor; // Web scroll deltas are inverted from Qt deltas. - - Qt::KeyboardModifiers modifiers = KeyboardModifier::getForEvent(*mouseEvent); - QPoint targetPointInScreenElementCoords(mouseEvent->targetX, mouseEvent->targetY); - QPoint targetPointInScreenCoords = - screen()->geometry().topLeft() + targetPointInScreenElementCoords; - - QWindow *targetWindow = screen()->compositor()->windowAt(targetPointInScreenCoords, 5); - if (!targetWindow) - return 0; - QPoint pointInTargetWindowCoords = targetWindow->mapFromGlobal(targetPointInScreenCoords); - - QPoint pixelDelta; - - if (wheelEvent->deltaY != 0) pixelDelta.setY(wheelEvent->deltaY * scrollFactor); - if (wheelEvent->deltaX != 0) pixelDelta.setX(wheelEvent->deltaX * scrollFactor); - - QPoint angleDelta = pixelDelta; // FIXME: convert from pixels? - - bool accepted = QWindowSystemInterface::handleWheelEvent( - targetWindow, QWasmIntegration::getTimestamp(), pointInTargetWindowCoords, - targetPointInScreenCoords, pixelDelta, angleDelta, modifiers, - Qt::NoScrollPhase, Qt::MouseEventNotSynthesized, - g_scrollingInvertedFromDevice); - return accepted; -} - -bool QWasmCompositor::processTouch(int eventType, const EmscriptenTouchEvent *touchEvent) -{ - QList<QWindowSystemInterface::TouchPoint> touchPointList; - touchPointList.reserve(touchEvent->numTouches); - QWindow *targetWindow = nullptr; - - for (int i = 0; i < touchEvent->numTouches; i++) { - - const EmscriptenTouchPoint *touches = &touchEvent->touches[i]; - - QPoint targetPointInScreenElementCoords(touches->targetX, touches->targetY); - QPoint targetPointInScreenCoords = - screen()->geometry().topLeft() + targetPointInScreenElementCoords; - - targetWindow = screen()->compositor()->windowAt(targetPointInScreenCoords, 5); - if (targetWindow == nullptr) - continue; - - QWindowSystemInterface::TouchPoint touchPoint; - - touchPoint.area = QRect(0, 0, 8, 8); - touchPoint.id = touches->identifier; - touchPoint.pressure = 1.0; - - touchPoint.area.moveCenter(targetPointInScreenCoords); - - const auto tp = m_pressedTouchIds.constFind(touchPoint.id); - if (tp != m_pressedTouchIds.constEnd()) - touchPoint.normalPosition = tp.value(); - - QPointF pointInTargetWindowCoords = QPointF(targetWindow->mapFromGlobal(targetPointInScreenCoords)); - QPointF normalPosition(pointInTargetWindowCoords.x() / targetWindow->width(), - pointInTargetWindowCoords.y() / targetWindow->height()); - - const bool stationaryTouchPoint = (normalPosition == touchPoint.normalPosition); - touchPoint.normalPosition = normalPosition; - - switch (eventType) { - case EMSCRIPTEN_EVENT_TOUCHSTART: - if (tp != m_pressedTouchIds.constEnd()) { - touchPoint.state = (stationaryTouchPoint - ? QEventPoint::State::Stationary - : QEventPoint::State::Updated); - } else { - touchPoint.state = QEventPoint::State::Pressed; - } - m_pressedTouchIds.insert(touchPoint.id, touchPoint.normalPosition); - - break; - case EMSCRIPTEN_EVENT_TOUCHEND: - touchPoint.state = QEventPoint::State::Released; - m_pressedTouchIds.remove(touchPoint.id); - break; - case EMSCRIPTEN_EVENT_TOUCHMOVE: - touchPoint.state = (stationaryTouchPoint - ? QEventPoint::State::Stationary - : QEventPoint::State::Updated); - - m_pressedTouchIds.insert(touchPoint.id, touchPoint.normalPosition); - break; - default: - break; - } - - touchPointList.append(touchPoint); - } - - QFlags<Qt::KeyboardModifier> keyModifier = KeyboardModifier::getForEvent(*touchEvent); - - bool accepted = false; - - if (eventType == EMSCRIPTEN_EVENT_TOUCHCANCEL) - accepted = QWindowSystemInterface::handleTouchCancelEvent(targetWindow, QWasmIntegration::getTimestamp(), m_touchDevice.get(), keyModifier); - else - accepted = QWindowSystemInterface::handleTouchEvent<QWindowSystemInterface::SynchronousDelivery>( - targetWindow, QWasmIntegration::getTimestamp(), m_touchDevice.get(), touchPointList, keyModifier); - - return static_cast<int>(accepted); -} - -void QWasmCompositor::setCapture(QWasmWindow *window) -{ - Q_ASSERT(std::find(m_windowStack.begin(), m_windowStack.end(), window) != m_windowStack.end()); - m_mouseCaptureWindow = window->window(); -} - -void QWasmCompositor::releaseCapture() -{ - m_mouseCaptureWindow = nullptr; -} - -void QWasmCompositor::leaveWindow(QWindow *window) -{ - m_windowUnderMouse = nullptr; - QWindowSystemInterface::handleLeaveEvent<QWindowSystemInterface::SynchronousDelivery>(window); -} - -void QWasmCompositor::enterWindow(QWindow *window, const QPoint &pointInTargetWindowCoords, const QPoint &targetPointInScreenCoords) -{ - QWindowSystemInterface::handleEnterEvent<QWindowSystemInterface::SynchronousDelivery>(window, pointInTargetWindowCoords, targetPointInScreenCoords); -} - -bool QWasmCompositor::processMouseEnter(const EmscriptenMouseEvent *mouseEvent) -{ - Q_UNUSED(mouseEvent) - // mouse has entered the screen area - m_mouseInScreen = true; - return true; -} - -bool QWasmCompositor::processMouseLeave() -{ - m_mouseInScreen = false; - return true; -} diff --git a/src/plugins/platforms/wasm/qwasmcompositor.h b/src/plugins/platforms/wasm/qwasmcompositor.h index c1fc62d380..4953d65233 100644 --- a/src/plugins/platforms/wasm/qwasmcompositor.h +++ b/src/plugins/platforms/wasm/qwasmcompositor.h @@ -7,26 +7,15 @@ #include "qwasmwindowstack.h" #include <qpa/qplatformwindow.h> -#include <QMap> - -#include <QtGui/qinputdevice.h> -#include <QtCore/private/qstdweb_p.h> -#include <QPointer> -#include <QPointingDevice> - -#include <emscripten/html5.h> -#include <emscripten/emscripten.h> -#include <emscripten/bind.h> +#include <QMap> QT_BEGIN_NAMESPACE -struct PointerEvent; class QWasmWindow; class QWasmScreen; -class QOpenGLContext; -class QOpenGLTexture; -class QWasmEventTranslator; + +enum class QWasmWindowTreeNodeChangeType; class QWasmCompositor final : public QObject { @@ -35,148 +24,35 @@ public: QWasmCompositor(QWasmScreen *screen); ~QWasmCompositor() final; - void initEventHandlers(); - - struct QWasmFrameOptions { - QRect rect; - int lineWidth; - QPalette palette; - }; - - void startResize(Qt::Edges edges); - - void addWindow(QWasmWindow *window); - void removeWindow(QWasmWindow *window); - void setVisible(QWasmWindow *window, bool visible); - void raise(QWasmWindow *window); - void lower(QWasmWindow *window); - QWindow *windowAt(QPoint globalPoint, int padding = 0) const; - QWindow *keyWindow() const; + void onScreenDeleting(); QWasmScreen *screen(); + void setEnabled(bool enabled); + static bool releaseRequestUpdateHold(); + + void requestUpdate(); enum UpdateRequestDeliveryType { ExposeEventDelivery, UpdateRequestDelivery }; - void requestUpdateAllWindows(); void requestUpdateWindow(QWasmWindow *window, UpdateRequestDeliveryType updateType = ExposeEventDelivery); - void setCapture(QWasmWindow *window); - void releaseCapture(); - void handleBackingStoreFlush(QWindow *window); + void onWindowTreeChanged(QWasmWindowTreeNodeChangeType changeType, QWasmWindow *window); private: - class WindowManipulation { - public: - enum class Operation { - None, - Move, - Resize, - }; - - WindowManipulation(QWasmScreen* screen); - - void onPointerDown(const PointerEvent& event, QWindow* windowAtPoint); - void onPointerMove(const PointerEvent& event); - void onPointerUp(const PointerEvent& event); - void startResize(Qt::Edges edges); - - Operation operation() const; - - private: - struct ResizeState { - Qt::Edges m_resizeEdges; - QPoint m_originInScreenCoords; - QRect m_initialWindowBounds; - const QPoint m_minShrink; - const QPoint m_maxGrow; - }; - struct MoveState { - QPoint m_lastPointInScreenCoords; - }; - struct OperationState - { - int pointerId; - QPointer<QWindow> window; - std::variant<ResizeState, MoveState> operationSpecific; - }; - struct SystemDragInitData - { - QPoint lastMouseMovePoint; - int lastMousePointerId = -1; - }; - - void resizeWindow(const QPoint& amount); - ResizeState makeResizeState(Qt::Edges edges, const QPoint &startPoint, QWindow *window); - - QWasmScreen *m_screen; - - SystemDragInitData m_systemDragInitData; - std::unique_ptr<OperationState> m_state; - }; - - void frame(bool all, const QList<QWasmWindow *> &windows); - - void onTopWindowChanged(); + void frame(const QList<QWasmWindow *> &windows); void deregisterEventHandlers(); - void destroy(); - void requestUpdate(); void deliverUpdateRequests(); void deliverUpdateRequest(QWasmWindow *window, UpdateRequestDeliveryType updateType); - static int keyboard_cb(int eventType, const EmscriptenKeyboardEvent *keyEvent, void *userData); - static int focus_cb(int eventType, const EmscriptenFocusEvent *focusEvent, void *userData); - static int wheel_cb(int eventType, const EmscriptenWheelEvent *wheelEvent, void *userData); - - bool processPointer(const PointerEvent& event); - bool deliverEventToTarget(const PointerEvent& event, QWindow *eventTarget); - - static int touchCallback(int eventType, const EmscriptenTouchEvent *ev, void *userData); - - bool processKeyboard(int eventType, const EmscriptenKeyboardEvent *keyEvent); - bool processWheel(int eventType, const EmscriptenWheelEvent *wheelEvent); - bool processMouseEnter(const EmscriptenMouseEvent *mouseEvent); - bool processMouseLeave(); - bool processTouch(int eventType, const EmscriptenTouchEvent *touchEvent); - - void enterWindow(QWindow *window, const QPoint &localPoint, const QPoint &globalPoint); - void leaveWindow(QWindow *window); - - void updateEnabledState(); - - WindowManipulation m_windowManipulation; - QWasmWindowStack m_windowStack; - bool m_isEnabled = true; - QSize m_targetSize; - qreal m_targetDevicePixelRatio = 1; QMap<QWasmWindow *, UpdateRequestDeliveryType> m_requestUpdateWindows; - bool m_requestUpdateAllWindows = false; int m_requestAnimationFrameId = -1; bool m_inDeliverUpdateRequest = false; - - QPointer<QWindow> m_lastMouseTargetWindow; - QPointer<QWindow> m_mouseCaptureWindow; - - std::unique_ptr<qstdweb::EventCallback> m_pointerDownCallback; - std::unique_ptr<qstdweb::EventCallback> m_pointerMoveCallback; - std::unique_ptr<qstdweb::EventCallback> m_pointerUpCallback; - std::unique_ptr<qstdweb::EventCallback> m_pointerLeaveCallback; - std::unique_ptr<qstdweb::EventCallback> m_pointerEnterCallback; - - std::unique_ptr<QPointingDevice> m_touchDevice; - - QMap <int, QPointF> m_pressedTouchIds; - - bool m_isResizeCursorDisplayed = false; - - std::unique_ptr<QWasmEventTranslator> m_eventTranslator; - - bool m_mouseInScreen = false; - QPointer<QWindow> m_windowUnderMouse; + static bool m_requestUpdateHoldEnabled; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmcssstyle.cpp b/src/plugins/platforms/wasm/qwasmcssstyle.cpp index 0f31d8abde..e0e1a99f48 100644 --- a/src/plugins/platforms/wasm/qwasmcssstyle.cpp +++ b/src/plugins/platforms/wasm/qwasmcssstyle.cpp @@ -3,6 +3,8 @@ #include "qwasmcssstyle.h" +#include "qwasmbase64iconstore.h" + #include <QtCore/qstring.h> #include <QtCore/qfile.h> @@ -12,6 +14,8 @@ namespace { const char *Style = R"css( .qt-screen { --border-width: 4px; + --resize-outline-width: 8px; + --resize-outline-half-width: var(--resize-outline-width) / 2; position: relative; border: none; @@ -20,21 +24,114 @@ const char *Style = R"css( width: 100%; height: 100%; overflow: hidden; - outline: none; +} + +.qt-screen div { + touch-action: none; } .qt-window { - box-shadow: rgb(0 0 0 / 20%) 0px 10px 16px 0px, rgb(0 0 0 / 19%) 0px 6px 20px 0px; - pointer-events: none; position: absolute; background-color: lightgray; } -.qt-window.has-title-bar { +.qt-window-contents { + overflow: hidden; + position: relative; +} + +.qt-window.transparent-for-input { + pointer-events: none; +} + +.qt-window.has-shadow { + box-shadow: rgb(0 0 0 / 20%) 0px 10px 16px 0px, rgb(0 0 0 / 19%) 0px 6px 20px 0px; +} + +.qt-window.has-border { border: var(--border-width) solid lightgray; caret-color: transparent; } +.qt-window.frameless { + background-color: transparent; +} + +.resize-outline { + position: absolute; + display: none; +} + +.qt-window.no-resize > .resize-outline { display: none; } + +.qt-window.has-border:not(.maximized):not(.no-resize) .resize-outline { + display: block; +} + +.resize-outline.nw { + left: calc(-1 * var(--resize-outline-half-width) - var(--border-width)); + top: calc(-1 * var(--resize-outline-half-width) - var(--border-width)); + width: var(--resize-outline-width); + height: var(--resize-outline-width); + cursor: nwse-resize; +} + +.resize-outline.n { + left: var(--resize-outline-half-width); + top: calc(-1 * var(--resize-outline-half-width) - var(--border-width)); + height: var(--resize-outline-width); + width: calc(100% + 2 * var(--border-width) - var(--resize-outline-width)); + cursor: ns-resize; +} + +.resize-outline.ne { + left: calc(100% + var(--border-width) - var(--resize-outline-half-width)); + top: calc(-1 * var(--resize-outline-half-width) - var(--border-width)); + width: var(--resize-outline-width); + height: var(--resize-outline-width); + cursor: nesw-resize; +} + +.resize-outline.w { + left: calc(-1 * var(--resize-outline-half-width) - var(--border-width)); + top: 0; + height: calc(100% + 2 * var(--border-width) - var(--resize-outline-width)); + width: var(--resize-outline-width); + cursor: ew-resize; +} + +.resize-outline.e { + left: calc(100% + var(--border-width) - var(--resize-outline-half-width)); + top: 0; + height: calc(100% + 2 * var(--border-width) - var(--resize-outline-width)); + width: var(--resize-outline-width); + cursor: ew-resize; +} + +.resize-outline.sw { + left: calc(-1 * var(--resize-outline-half-width) - var(--border-width)); + top: calc(100% + var(--border-width) - var(--resize-outline-half-width)); + width: var(--resize-outline-width); + height: var(--resize-outline-width); + cursor: nesw-resize; +} + +.resize-outline.s { + left: var(--resize-outline-half-width); + top: calc(100% + var(--border-width) - var(--resize-outline-half-width)); + height: var(--resize-outline-width); + width: calc(100% + 2 * var(--border-width) - var(--resize-outline-width)); + cursor: ns-resize; +} + +.resize-outline.se { + left: calc(100% + var(--border-width) - var(--resize-outline-half-width)); + top: calc(100% + var(--border-width) - var(--resize-outline-half-width)); + width: var(--resize-outline-width); + height: var(--resize-outline-width); + cursor: nwse-resize; +} + .title-bar { display: none; align-items: center; @@ -43,17 +140,23 @@ const char *Style = R"css( padding-bottom: 4px; } -.qt-window.has-title-bar .title-bar { +.qt-window.has-border > .title-bar { display: flex; } .title-bar .window-name { + display: none; font-family: 'Lucida Grande'; white-space: nowrap; user-select: none; overflow: hidden; } + +.qt-window.has-title .title-bar .window-name { + display: block; +} + .title-bar .spacer { flex-grow: 1 } @@ -64,6 +167,16 @@ const char *Style = R"css( .qt-window-canvas-container { display: flex; + pointer-events: none; +} + +.title-bar div { + pointer-events: none; +} + +.qt-window-a11y-container { + position: absolute; + z-index: -1; } .title-bar .image-button { @@ -75,7 +188,7 @@ const char *Style = R"css( align-items: center; } -.title-bar .image-button span { +.title-bar .image-button img { width: 10px; height: 10px; user-select: none; @@ -84,97 +197,51 @@ const char *Style = R"css( background-size: 10px 10px; } -.title-bar .image-button span[qt-builtin-image-type=x] { - background-image: url("data:image/svg+xml;base64,$close_icon"); -} - -.title-bar .image-button span[qt-builtin-image-type=qt-logo] { - background-image: url("qtlogo.svg"); -} - -.title-bar .image-button span[qt-builtin-image-type=restore] { - background-image: url("data:image/svg+xml;base64,$restore_icon"); -} - -.title-bar .image-button span[qt-builtin-image-type=maximize] { - background-image: url("data:image/svg+xml;base64,$maximize_icon"); -} .title-bar .action-button { pointer-events: all; - align-self: end; } -.qt-window.blocked .title-bar .action-button { +.qt-window.blocked div { pointer-events: none; } -.title-bar .action-button span { +.title-bar .action-button img { transition: filter 0.08s ease-out; } -.title-bar .action-button:hover span { +.title-bar .action-button:hover img { filter: invert(0.45); } -.title-bar .action-button:active span { +.title-bar .action-button:active img { filter: invert(0.6); } +/* This will clip the content within 50% frame in 1x1 pixel area, preventing it + from being rendered on the page, but it should still be read by modern + screen readers */ +.hidden-visually-read-by-screen-reader { + visibility: visible; + clip: rect(1px, 1px, 1px, 1px); + clip-path: inset(50%); + height: 1px; + width: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; +} + )css"; -class Base64IconStore -{ -public: - enum class IconType { - Maximize, - First = Maximize, - QtLogo, - Restore, - X, - Size, - }; - - Base64IconStore() - { - QString iconSources[static_cast<size_t>(IconType::Size)] = { - QStringLiteral(":/wasm-window/maximize.svg"), - QStringLiteral(":/wasm-window/qtlogo.svg"), QStringLiteral(":/wasm-window/restore.svg"), - QStringLiteral(":/wasm-window/x.svg") - }; - - for (size_t iconType = static_cast<size_t>(IconType::First); - iconType < static_cast<size_t>(IconType::Size); ++iconType) { - QFile svgFile(iconSources[static_cast<size_t>(iconType)]); - if (!svgFile.open(QIODevice::ReadOnly)) - Q_ASSERT(false); // A resource should always be opened. - m_storage[static_cast<size_t>(iconType)] = svgFile.readAll().toBase64(); - } - } - ~Base64IconStore() = default; - - std::string_view getIcon(IconType type) const { return m_storage[static_cast<size_t>(type)]; } - -private: - std::string m_storage[static_cast<size_t>(IconType::Size)]; -}; - -void replace(std::string &str, const std::string &from, const std::string_view &to) -{ - str.replace(str.find(from), from.length(), to); -} } // namespace emscripten::val QWasmCSSStyle::createStyleElement(emscripten::val parent) { - Base64IconStore store; auto document = parent["ownerDocument"]; auto screenStyle = document.call<emscripten::val>("createElement", emscripten::val("style")); - auto text = std::string(Style); - replace(text, "$close_icon", store.getIcon(Base64IconStore::IconType::X)); - replace(text, "$restore_icon", store.getIcon(Base64IconStore::IconType::Restore)); - replace(text, "$maximize_icon", store.getIcon(Base64IconStore::IconType::Maximize)); - screenStyle.set("textContent", text); + screenStyle.set("textContent", std::string(Style)); return screenStyle; } diff --git a/src/plugins/platforms/wasm/qwasmcursor.cpp b/src/plugins/platforms/wasm/qwasmcursor.cpp index e159b8fe7d..c258befa77 100644 --- a/src/plugins/platforms/wasm/qwasmcursor.cpp +++ b/src/plugins/platforms/wasm/qwasmcursor.cpp @@ -3,9 +3,11 @@ #include "qwasmcursor.h" #include "qwasmscreen.h" -#include "qwasmstring.h" +#include "qwasmwindow.h" +#include <QtCore/qbuffer.h> #include <QtCore/qdebug.h> +#include <QtCore/qstring.h> #include <QtGui/qwindow.h> #include <emscripten/emscripten.h> @@ -14,122 +16,86 @@ QT_BEGIN_NAMESPACE using namespace emscripten; -void QWasmCursor::changeCursor(QCursor *windowCursor, QWindow *window) -{ - if (!window) - return; - QScreen *screen = window->screen(); - if (!screen) - return; - - if (windowCursor) { - - // Bitmap and custom cursors are not implemented (will fall back to "auto") - if (windowCursor->shape() == Qt::BitmapCursor || windowCursor->shape() >= Qt::CustomCursor) - qWarning() << "QWasmCursor: bitmap and custom cursors are not supported"; - - - htmlCursorName = cursorShapeToHtml(windowCursor->shape()); - } - if (htmlCursorName.isEmpty()) - htmlCursorName = "default"; - - setWasmCursor(screen, htmlCursorName); -} - -QByteArray QWasmCursor::cursorShapeToHtml(Qt::CursorShape shape) +namespace { +QByteArray cursorToCss(const QCursor *cursor) { - QByteArray cursorName; - + auto shape = cursor->shape(); switch (shape) { case Qt::ArrowCursor: - cursorName = "default"; - break; + return "default"; case Qt::UpArrowCursor: - cursorName = "n-resize"; - break; + return "n-resize"; case Qt::CrossCursor: - cursorName = "crosshair"; - break; + return "crosshair"; case Qt::WaitCursor: - cursorName = "wait"; - break; + return "wait"; case Qt::IBeamCursor: - cursorName = "text"; - break; + return "text"; case Qt::SizeVerCursor: - cursorName = "ns-resize"; - break; + return "ns-resize"; case Qt::SizeHorCursor: - cursorName = "ew-resize"; - break; + return "ew-resize"; case Qt::SizeBDiagCursor: - cursorName = "nesw-resize"; - break; + return "nesw-resize"; case Qt::SizeFDiagCursor: - cursorName = "nwse-resize"; - break; + return "nwse-resize"; case Qt::SizeAllCursor: - cursorName = "move"; - break; + return "move"; case Qt::BlankCursor: - cursorName = "none"; - break; + return "none"; case Qt::SplitVCursor: - cursorName = "row-resize"; - break; + return "row-resize"; case Qt::SplitHCursor: - cursorName = "col-resize"; - break; + return "col-resize"; case Qt::PointingHandCursor: - cursorName = "pointer"; - break; + return "pointer"; case Qt::ForbiddenCursor: - cursorName = "not-allowed"; - break; + return "not-allowed"; case Qt::WhatsThisCursor: - cursorName = "help"; - break; + return "help"; case Qt::BusyCursor: - cursorName = "progress"; - break; + return "progress"; case Qt::OpenHandCursor: - cursorName = "grab"; - break; + return "grab"; case Qt::ClosedHandCursor: - cursorName = "grabbing"; - break; + return "grabbing"; case Qt::DragCopyCursor: - cursorName = "copy"; - break; + return "copy"; case Qt::DragMoveCursor: - cursorName = "default"; - break; + return "default"; case Qt::DragLinkCursor: - cursorName = "alias"; - break; + return "alias"; + case Qt::BitmapCursor: { + auto pixmap = cursor->pixmap(); + QByteArray cursorAsPng; + QBuffer buffer(&cursorAsPng); + buffer.open(QBuffer::WriteOnly); + pixmap.save(&buffer, "PNG"); + buffer.close(); + auto cursorAsBase64 = cursorAsPng.toBase64(); + auto hotSpot = cursor->hotSpot(); + auto encodedCursor = + QString("url(data:image/png;base64,%1) %2 %3, auto") + .arg(QString::fromUtf8(cursorAsBase64), + QString::number(hotSpot.x()), + QString::number(hotSpot.y())); + return encodedCursor.toUtf8(); + } default: - break; + static_assert(Qt::CustomCursor == 25, + "New cursor type added, handle it"); + qWarning() << "QWasmCursor: " << shape << " unsupported"; + return "default"; } - - return cursorName; -} - -void QWasmCursor::setWasmCursor(QScreen *screen, const QByteArray &name) -{ - QWasmScreen::get(screen)->element()["style"].set("cursor", val(name.constData())); } +} // namespace -void QWasmCursor::setOverrideWasmCursor(const QCursor &windowCursor, QScreen *screen) -{ - QWasmCursor *wCursor = static_cast<QWasmCursor *>(QWasmScreen::get(screen)->cursor()); - wCursor->setWasmCursor(screen, wCursor->cursorShapeToHtml(windowCursor.shape())); -} - -void QWasmCursor::clearOverrideWasmCursor(QScreen *screen) +void QWasmCursor::changeCursor(QCursor *windowCursor, QWindow *window) { - QWasmCursor *wCursor = static_cast<QWasmCursor *>(QWasmScreen::get(screen)->cursor()); - wCursor->setWasmCursor(screen, wCursor->htmlCursorName); + if (!window) + return; + if (QWasmWindow *wasmWindow = static_cast<QWasmWindow *>(window->handle())) + wasmWindow->setWindowCursor(windowCursor ? cursorToCss(windowCursor) : "default"); } QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmcursor.h b/src/plugins/platforms/wasm/qwasmcursor.h index 4a5cb23bf4..6873602caf 100644 --- a/src/plugins/platforms/wasm/qwasmcursor.h +++ b/src/plugins/platforms/wasm/qwasmcursor.h @@ -5,19 +5,13 @@ #define QWASMCURSOR_H #include <qpa/qplatformcursor.h> + QT_BEGIN_NAMESPACE class QWasmCursor : public QPlatformCursor { public: void changeCursor(QCursor *windowCursor, QWindow *window) override; - - QByteArray cursorShapeToHtml(Qt::CursorShape shape); - static void setOverrideWasmCursor(const QCursor &windowCursor, QScreen *screen); - static void clearOverrideWasmCursor(QScreen *screen); -private: - QByteArray htmlCursorName = "default"; - void setWasmCursor(QScreen *screen, const QByteArray &name); }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmdom.cpp b/src/plugins/platforms/wasm/qwasmdom.cpp new file mode 100644 index 0000000000..96790ca71f --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmdom.cpp @@ -0,0 +1,310 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmdom.h" + +#include <QtCore/qdir.h> +#include <QtCore/qfile.h> +#include <QtCore/qpoint.h> +#include <QtCore/qrect.h> +#include <QtGui/qimage.h> +#include <private/qstdweb_p.h> +#include <QtCore/qurl.h> + +#include <utility> +#include <emscripten/wire.h> + +QT_BEGIN_NAMESPACE + +namespace dom { +namespace { +std::string dropActionToDropEffect(Qt::DropAction action) +{ + switch (action) { + case Qt::DropAction::CopyAction: + return "copy"; + case Qt::DropAction::IgnoreAction: + return "none"; + case Qt::DropAction::LinkAction: + return "link"; + case Qt::DropAction::MoveAction: + case Qt::DropAction::TargetMoveAction: + return "move"; + case Qt::DropAction::ActionMask: + Q_ASSERT(false); + return ""; + } +} +} // namespace + +DataTransfer::DataTransfer(emscripten::val webDataTransfer) + : webDataTransfer(webDataTransfer) { +} + +DataTransfer::~DataTransfer() = default; + +DataTransfer::DataTransfer(const DataTransfer &other) = default; + +DataTransfer::DataTransfer(DataTransfer &&other) = default; + +DataTransfer &DataTransfer::operator=(const DataTransfer &other) = default; + +DataTransfer &DataTransfer::operator=(DataTransfer &&other) = default; + +void DataTransfer::setDragImage(emscripten::val element, const QPoint &hotspot) +{ + webDataTransfer.call<void>("setDragImage", element, emscripten::val(hotspot.x()), + emscripten::val(hotspot.y())); +} + +void DataTransfer::setData(std::string format, std::string data) +{ + webDataTransfer.call<void>("setData", emscripten::val(std::move(format)), + emscripten::val(std::move(data))); +} + +void DataTransfer::setDropAction(Qt::DropAction action) +{ + webDataTransfer.set("dropEffect", emscripten::val(dropActionToDropEffect(action))); +} + +void DataTransfer::setDataFromMimeData(const QMimeData &mimeData) +{ + for (const auto &format : mimeData.formats()) { + auto data = mimeData.data(format); + + auto encoded = format.startsWith("text/") + ? QString::fromLocal8Bit(data).toStdString() + : "QB64" + QString::fromLocal8Bit(data.toBase64()).toStdString(); + + setData(format.toStdString(), std::move(encoded)); + } +} + +// Converts a DataTransfer instance to a QMimeData instance. Invokes the +// given callback when the conversion is complete. The callback takes ownership +// of the QMimeData. +void DataTransfer::toMimeDataWithFile(std::function<void(QMimeData *)> callback) +{ + enum class ItemKind { + File, + String, + }; + + class MimeContext { + + public: + MimeContext(int itemCount, std::function<void(QMimeData *)> callback) + :m_remainingItemCount(itemCount), m_callback(callback) + { + + } + + void deref() { + if (--m_remainingItemCount > 0) + return; + + QList<QUrl> allUrls; + allUrls.append(mimeData->urls()); + allUrls.append(fileUrls); + mimeData->setUrls(allUrls); + + m_callback(mimeData); + + // Delete files; we expect that the user callback reads/copies + // file content before returning. + // Fixme: tie file lifetime to lifetime of the QMimeData? + for (QUrl fileUrl: fileUrls) + QFile(fileUrl.toLocalFile()).remove(); + + delete this; + } + + QMimeData *mimeData = new QMimeData(); + QList<QUrl> fileUrls; + + private: + int m_remainingItemCount; + std::function<void(QMimeData *)> m_callback; + }; + + const auto items = webDataTransfer["items"]; + const int itemCount = items["length"].as<int>(); + const int fileCount = webDataTransfer["files"]["length"].as<int>(); + MimeContext *mimeContext = new MimeContext(itemCount, callback); + + for (int i = 0; i < itemCount; ++i) { + const auto item = items[i]; + const auto itemKind = + item["kind"].as<std::string>() == "string" ? ItemKind::String : ItemKind::File; + const auto itemMimeType = QString::fromStdString(item["type"].as<std::string>()); + + switch (itemKind) { + case ItemKind::File: { + qstdweb::File webfile(item.call<emscripten::val>("getAsFile")); + + if (webfile.size() > 1e+9) { // limit file size to 1 GB + qWarning() << "File is too large (> 1GB) and will be skipped. File size is" << webfile.size(); + mimeContext->deref(); + continue; + } + + QString mimeFormat = QString::fromStdString(webfile.type()); + QString fileName = QString::fromStdString(webfile.name()); + + // there's a file, now read it + QByteArray fileContent(webfile.size(), Qt::Uninitialized); + webfile.stream(fileContent.data(), [=]() { + + // If we get a single file, and that file is an image, then + // try to decode the image data. This handles the case where + // image data (i.e. not an image file) is pasted. The browsers + // will then create a fake "image.png" file which has the image + // data. As a side effect Qt will also decode the image for + // single-image-file drops, since there is no way to differentiate + // the fake "image.png" from a real one. + if (fileCount == 1 && mimeFormat.contains("image/")) { + QImage image; + if (image.loadFromData(fileContent)) + mimeContext->mimeData->setImageData(image); + } + + QDir qtTmpDir("/qt/tmp/"); // "tmp": indicate that these files won't stay around + qtTmpDir.mkpath(qtTmpDir.path()); + + QUrl fileUrl = QUrl::fromLocalFile(qtTmpDir.filePath(QString::fromStdString(webfile.name()))); + mimeContext->fileUrls.append(fileUrl); + + QFile file(fileUrl.toLocalFile()); + if (!file.open(QFile::WriteOnly)) { + qWarning() << "File was not opened"; + mimeContext->deref(); + return; + } + if (file.write(fileContent) < 0) { + qWarning() << "Write failed"; + file.close(); + } + mimeContext->deref(); + }); + break; + } + case ItemKind::String: + if (itemMimeType.contains("STRING", Qt::CaseSensitive) + || itemMimeType.contains("TEXT", Qt::CaseSensitive)) { + mimeContext->deref(); + break; + } + QString a; + QString data = QString::fromEcmaString(webDataTransfer.call<emscripten::val>( + "getData", emscripten::val(itemMimeType.toStdString()))); + + if (!data.isEmpty()) { + if (itemMimeType == "text/html") + mimeContext->mimeData->setHtml(data); + else if (itemMimeType.isEmpty() || itemMimeType == "text/plain") + mimeContext->mimeData->setText(data); // the type can be empty + else if (itemMimeType.isEmpty() || itemMimeType == "text/uri-list") { + QList<QUrl> urls; + urls.append(data); + mimeContext->mimeData->setUrls(urls); + } else { + // TODO improve encoding + if (data.startsWith("QB64")) { + data.remove(0, 4); + mimeContext->mimeData->setData(itemMimeType, + QByteArray::fromBase64(QByteArray::fromStdString( + data.toStdString()))); + } else { + mimeContext->mimeData->setData(itemMimeType, data.toLocal8Bit()); + } + } + } + mimeContext->deref(); + break; + } + } // for items +} + +QMimeData *DataTransfer::toMimeDataPreview() +{ + auto data = new QMimeData(); + + QList<QUrl> uriList; + for (int i = 0; i < webDataTransfer["items"]["length"].as<int>(); ++i) { + const auto item = webDataTransfer["items"][i]; + if (item["kind"].as<std::string>() == "file") { + uriList.append(QUrl("blob://placeholder")); + } else { + const auto itemMimeType = QString::fromStdString(item["type"].as<std::string>()); + data->setData(itemMimeType, QByteArray()); + } + } + data->setUrls(uriList); + return data; +} + +void syncCSSClassWith(emscripten::val element, std::string cssClassName, bool flag) +{ + if (flag) { + element["classList"].call<void>("add", emscripten::val(std::move(cssClassName))); + return; + } + + element["classList"].call<void>("remove", emscripten::val(std::move(cssClassName))); +} + +QPointF mapPoint(emscripten::val source, emscripten::val target, const QPointF &point) +{ + const auto sourceBoundingRect = + QRectF::fromDOMRect(source.call<emscripten::val>("getBoundingClientRect")); + const auto targetBoundingRect = + QRectF::fromDOMRect(target.call<emscripten::val>("getBoundingClientRect")); + + const auto offset = sourceBoundingRect.topLeft() - targetBoundingRect.topLeft(); + return point + offset; +} + +void drawImageToWebImageDataArray(const QImage &sourceImage, emscripten::val destinationImageData, + const QRect &sourceRect) +{ + Q_ASSERT_X(destinationImageData["constructor"]["name"].as<std::string>() == "ImageData", + Q_FUNC_INFO, "The destination should be an ImageData instance"); + + constexpr int BytesPerColor = 4; + if (sourceRect.width() == sourceImage.width()) { + // Copy a contiguous chunk of memory + // ............... + // OOOOOOOOOOOOOOO + // OOOOOOOOOOOOOOO -> image data + // OOOOOOOOOOOOOOO + // ............... + auto imageMemory = emscripten::typed_memory_view(sourceRect.width() * sourceRect.height() + * BytesPerColor, + sourceImage.constScanLine(sourceRect.y())); + destinationImageData["data"].call<void>( + "set", imageMemory, sourceRect.y() * sourceImage.width() * BytesPerColor); + } else { + // Go through the scanlines manually to set the individual lines in bulk. This is + // marginally less performant than the above. + // ............... + // ...OOOOOOOOO... r = 0 -> image data + // ...OOOOOOOOO... r = 1 -> image data + // ...OOOOOOOOO... r = 2 -> image data + // ............... + for (int row = 0; row < sourceRect.height(); ++row) { + auto scanlineMemory = + emscripten::typed_memory_view(sourceRect.width() * BytesPerColor, + sourceImage.constScanLine(row + sourceRect.y()) + + BytesPerColor * sourceRect.x()); + destinationImageData["data"].call<void>("set", scanlineMemory, + (sourceRect.y() + row) * sourceImage.width() + * BytesPerColor + + sourceRect.x() * BytesPerColor); + } + } +} + +} // namespace dom + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmdom.h b/src/plugins/platforms/wasm/qwasmdom.h new file mode 100644 index 0000000000..0a520815a3 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmdom.h @@ -0,0 +1,63 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMDOM_H +#define QWASMDOM_H + +#include <QtCore/qtconfigmacros.h> +#include <QtCore/QPointF> +#include <private/qstdweb_p.h> +#include <QtCore/qnamespace.h> + +#include <emscripten/val.h> + +#include <functional> +#include <memory> +#include <string> + +#include <QMimeData> +QT_BEGIN_NAMESPACE + +namespace qstdweb { + struct CancellationFlag; +} + + +class QPoint; +class QRect; + +namespace dom { +struct DataTransfer +{ + explicit DataTransfer(emscripten::val webDataTransfer); + ~DataTransfer(); + DataTransfer(const DataTransfer &other); + DataTransfer(DataTransfer &&other); + DataTransfer &operator=(const DataTransfer &other); + DataTransfer &operator=(DataTransfer &&other); + + void toMimeDataWithFile(std::function<void(QMimeData *)> callback); + QMimeData *toMimeDataPreview(); + void setDragImage(emscripten::val element, const QPoint &hotspot); + void setData(std::string format, std::string data); + void setDropAction(Qt::DropAction dropAction); + void setDataFromMimeData(const QMimeData &mimeData); + + emscripten::val webDataTransfer; +}; + +inline emscripten::val document() +{ + return emscripten::val::global("document"); +} + +void syncCSSClassWith(emscripten::val element, std::string cssClassName, bool flag); + +QPointF mapPoint(emscripten::val source, emscripten::val target, const QPointF &point); + +void drawImageToWebImageDataArray(const QImage &source, emscripten::val destinationImageData, + const QRect &sourceRect); +} // namespace dom + +QT_END_NAMESPACE +#endif // QWASMDOM_H diff --git a/src/plugins/platforms/wasm/qwasmdrag.cpp b/src/plugins/platforms/wasm/qwasmdrag.cpp index c2b50e4356..d07a46618f 100644 --- a/src/plugins/platforms/wasm/qwasmdrag.cpp +++ b/src/plugins/platforms/wasm/qwasmdrag.cpp @@ -1,209 +1,291 @@ -// Copyright (C) 2022 The Qt Company Ltd. +// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmdrag.h" -#include <iostream> -#include <QMimeDatabase> +#include "qwasmbase64iconstore.h" +#include "qwasmdom.h" +#include "qwasmevent.h" +#include "qwasmintegration.h" -#include <emscripten.h> -#include <emscripten/html5.h> -#include <emscripten/val.h> -#include <emscripten/bind.h> -#include <private/qstdweb_p.h> #include <qpa/qwindowsysteminterface.h> -#include <private/qsimpledrag_p.h> -#include "qwasmcompositor.h" -#include "qwasmeventtranslator.h" -#include <QtCore/QEventLoop> -#include <QMimeData> -#include <private/qshapedpixmapdndwindow_p.h> + +#include <QtCore/private/qstdweb_p.h> +#include <QtCore/qeventloop.h> +#include <QtCore/qmimedata.h> +#include <QtCore/qtimer.h> +#include <QFile> + +#include <functional> +#include <string> +#include <utility> QT_BEGIN_NAMESPACE -using namespace emscripten; +namespace { + +QWindow *windowForDrag(QDrag *drag) +{ + QWindow *window = qobject_cast<QWindow *>(drag->source()); + if (window) + return window; + if (drag->source()->metaObject()->indexOfMethod("_q_closestWindowHandle()") == -1) + return nullptr; + + QMetaObject::invokeMethod(drag->source(), "_q_closestWindowHandle", + Q_RETURN_ARG(QWindow *, window)); + return window; +} + +} // namespace -static void getTextPlainCallback(val m_string) +struct QWasmDrag::DragState +{ + class DragImage + { + public: + DragImage(const QPixmap &pixmap, const QMimeData *mimeData, QWindow *window); + ~DragImage(); + + emscripten::val htmlElement(); + + private: + emscripten::val generateDragImage(const QPixmap &pixmap, const QMimeData *mimeData); + emscripten::val generateDragImageFromText(const QMimeData *mimeData); + emscripten::val generateDefaultDragImage(); + emscripten::val generateDragImageFromPixmap(const QPixmap &pixmap); + + emscripten::val m_imageDomElement; + emscripten::val m_temporaryImageElementParent; + }; + + DragState(QDrag *drag, QWindow *window, std::function<void()> quitEventLoopClosure); + ~DragState(); + DragState(const QWasmDrag &other) = delete; + DragState(QWasmDrag &&other) = delete; + DragState &operator=(const QWasmDrag &other) = delete; + DragState &operator=(QWasmDrag &&other) = delete; + + QDrag *drag; + QWindow *window; + std::function<void()> quitEventLoopClosure; + std::unique_ptr<DragImage> dragImage; + Qt::DropAction dropAction = Qt::DropAction::IgnoreAction; +}; + +QWasmDrag::QWasmDrag() = default; + +QWasmDrag::~QWasmDrag() = default; + +QWasmDrag *QWasmDrag::instance() { - QWasmDrag *thisDrag = static_cast<QWasmDrag*>(QWasmIntegration::get()->drag()); - thisDrag->m_mimeData->setText(QString::fromStdString(m_string.as<std::string>())); - thisDrag->qWasmDrop(); + return static_cast<QWasmDrag *>(QWasmIntegration::get()->drag()); } -static void getTextUrlCallback(val m_string) +Qt::DropAction QWasmDrag::drag(QDrag *drag) { - QWasmDrag *thisDrag = static_cast<QWasmDrag*>(QWasmIntegration::get()->drag()); - thisDrag->m_mimeData->setData(QStringLiteral("text/uri-list"), - QByteArray::fromStdString(m_string.as<std::string>())); + Q_ASSERT_X(!m_dragState, Q_FUNC_INFO, "Drag already in progress"); + + QWindow *window = windowForDrag(drag); + if (!window) + return Qt::IgnoreAction; + + Qt::DropAction dragResult = Qt::IgnoreAction; + if (qstdweb::haveJspi()) { + QEventLoop loop; + m_dragState = std::make_unique<DragState>(drag, window, [&loop]() { loop.quit(); }); + loop.exec(); + dragResult = m_dragState->dropAction; + m_dragState.reset(); + } - thisDrag->qWasmDrop(); + if (dragResult == Qt::IgnoreAction) + dragResult = QBasicDrag::drag(drag); + + return dragResult; } -static void getTextHtmlCallback(val m_string) +void QWasmDrag::onNativeDragStarted(DragEvent *event) { - QWasmDrag *thisDrag = static_cast<QWasmDrag*>(QWasmIntegration::get()->drag()); - thisDrag->m_mimeData->setHtml(QString::fromStdString(m_string.as<std::string>())); + Q_ASSERT_X(event->type == EventType::DragStart, Q_FUNC_INFO, + "The event is not a DragStart event"); + // It is possible for a drag start event to arrive from another window. + if (!m_dragState || m_dragState->window != event->targetWindow) { + event->cancelDragStart(); + return; + } - thisDrag->qWasmDrop(); + m_dragState->dragImage = std::make_unique<DragState::DragImage>( + m_dragState->drag->pixmap(), m_dragState->drag->mimeData(), event->targetWindow); + event->dataTransfer.setDragImage(m_dragState->dragImage->htmlElement(), + m_dragState->drag->hotSpot()); + event->dataTransfer.setDataFromMimeData(*m_dragState->drag->mimeData()); } -static void dropEvent(val event) -{ - // someone dropped a file into the browser window - // event is dataTransfer object - // if drop event from outside browser, we do not get any mouse release, maybe mouse move - // after the drop event - - // data-context thing was not working here :( - QWasmDrag *wasmDrag = static_cast<QWasmDrag*>(QWasmIntegration::get()->drag()); - - wasmDrag->m_wasmScreen = - reinterpret_cast<QWasmScreen*>(event["target"]["data-qtdropcontext"].as<quintptr>()); - - wasmDrag->m_mouseDropPoint = QPoint(event["x"].as<int>(), event["y"].as<int>()); - if (wasmDrag->m_mimeData) - delete wasmDrag->m_mimeData; - wasmDrag->m_mimeData = new QMimeData; - wasmDrag->m_qButton = MouseEvent::buttonFromWeb(event["button"].as<int>()); - - wasmDrag->m_keyModifiers = Qt::NoModifier; - if (event["altKey"].as<bool>()) - wasmDrag->m_keyModifiers |= Qt::AltModifier; - if (event["ctrlKey"].as<bool>()) - wasmDrag->m_keyModifiers |= Qt::ControlModifier; - if (event["metaKey"].as<bool>()) - wasmDrag->m_keyModifiers |= Qt::MetaModifier; - - event.call<void>("preventDefault"); // prevent browser from handling drop event - - std::string dEffect = event["dataTransfer"]["dropEffect"].as<std::string>(); - - wasmDrag->m_dropActions = Qt::IgnoreAction; - if (dEffect == "copy") - wasmDrag->m_dropActions = Qt::CopyAction; - if (dEffect == "move") - wasmDrag->m_dropActions = Qt::MoveAction; - if (dEffect == "link") - wasmDrag->m_dropActions = Qt::LinkAction; - - val dt = event["dataTransfer"]["items"]["length"]; - - val typesCount = event["dataTransfer"]["types"]["length"]; - - // handle mimedata - int count = dt.as<int>(); - wasmDrag->m_mimeTypesCount = count; - // kind is file type: file or string - for (int i=0; i < count; i++) { - val item = event["dataTransfer"]["items"][i]; - val kind = item["kind"]; - val fileType = item["type"]; - - if (kind.as<std::string>() == "file") { - val m_file = item.call<val>("getAsFile"); - if (m_file.isUndefined()) { - continue; - } - - qstdweb::File file(m_file); - - QString mimeFormat = QString::fromStdString(file.type()); - QByteArray fileContent; - fileContent.resize(file.size()); - - file.stream(fileContent.data(), [=]() { - if (!fileContent.isEmpty()) { - - if (mimeFormat.contains("image")) { - QImage image; - image.loadFromData(fileContent, nullptr); - wasmDrag->m_mimeData->setImageData(image); - } else { - wasmDrag->m_mimeData->setData(mimeFormat, fileContent.data()); - } - wasmDrag->qWasmDrop(); - } - }); - - } else { // string - - if (fileType.as<std::string>() == "text/uri-list" - || fileType.as<std::string>() == "text/x-moz-url") { - item.call<val>("getAsString", val::module_property("qtgetTextUrl")); - } else if (fileType.as<std::string>() == "text/html") { - item.call<val>("getAsString", val::module_property("qtgetTextHtml")); - } else { // handle everything else here as plain text - item.call<val>("getAsString", val::module_property("qtgetTextPlain")); - } - } +void QWasmDrag::onNativeDragOver(DragEvent *event) +{ + auto mimeDataPreview = event->dataTransfer.toMimeDataPreview(); + + const Qt::DropActions actions = m_dragState + ? m_dragState->drag->supportedActions() + : (Qt::DropAction::CopyAction | Qt::DropAction::MoveAction + | Qt::DropAction::LinkAction); + + const auto dragResponse = QWindowSystemInterface::handleDrag( + event->targetWindow, &*mimeDataPreview, event->pointInPage.toPoint(), actions, + event->mouseButton, event->modifiers); + event->acceptDragOver(); + if (dragResponse.isAccepted()) { + event->dataTransfer.setDropAction(dragResponse.acceptedAction()); + } else { + event->dataTransfer.setDropAction(Qt::DropAction::IgnoreAction); } } -EMSCRIPTEN_BINDINGS(drop_module) { - function("qtDrop", &dropEvent); - function("qtgetTextPlain", &getTextPlainCallback); - function("qtgetTextUrl", &getTextUrlCallback); - function("qtgetTextHtml", &getTextHtmlCallback); +void QWasmDrag::onNativeDrop(DragEvent *event) +{ + QWasmWindow *wasmWindow = QWasmWindow::fromWindow(event->targetWindow); + + const auto screenElementPos = dom::mapPoint( + event->target(), wasmWindow->platformScreen()->element(), event->localPoint); + const auto screenPos = + wasmWindow->platformScreen()->mapFromLocal(screenElementPos); + const QPoint targetWindowPos = event->targetWindow->mapFromGlobal(screenPos).toPoint(); + + const Qt::DropActions actions = m_dragState + ? m_dragState->drag->supportedActions() + : (Qt::DropAction::CopyAction | Qt::DropAction::MoveAction + | Qt::DropAction::LinkAction); + Qt::MouseButton mouseButton = event->mouseButton; + QFlags<Qt::KeyboardModifier> modifiers = event->modifiers; + + // Accept the native drop event: We are going to async read any dropped + // files, but the browser expects that accepted state is set before any + // async calls. + event->acceptDrop(); + + const auto dropCallback = [&m_dragState = m_dragState, wasmWindow, targetWindowPos, + actions, mouseButton, modifiers](QMimeData *mimeData) { + + auto dropResponse = std::make_shared<QPlatformDropQtResponse>(true, Qt::DropAction::CopyAction); + *dropResponse = QWindowSystemInterface::handleDrop(wasmWindow->window(), mimeData, + targetWindowPos, actions, + mouseButton, modifiers); + + if (dropResponse->isAccepted()) + m_dragState->dropAction = dropResponse->acceptedAction(); + + delete mimeData; + }; + + event->dataTransfer.toMimeDataWithFile(dropCallback); } +void QWasmDrag::onNativeDragFinished(DragEvent *event) +{ + m_dragState->dropAction = event->dropAction; + m_dragState->quitEventLoopClosure(); +} + +QWasmDrag::DragState::DragImage::DragImage(const QPixmap &pixmap, const QMimeData *mimeData, + QWindow *window) + : m_temporaryImageElementParent(QWasmWindow::fromWindow(window)->containerElement()) +{ + m_imageDomElement = generateDragImage(pixmap, mimeData); + + m_imageDomElement.set("className", "hidden-drag-image"); + m_temporaryImageElementParent.call<void>("appendChild", m_imageDomElement); +} + +QWasmDrag::DragState::DragImage::~DragImage() +{ + m_temporaryImageElementParent.call<void>("removeChild", m_imageDomElement); +} -QWasmDrag::QWasmDrag() +emscripten::val QWasmDrag::DragState::DragImage::generateDragImage(const QPixmap &pixmap, + const QMimeData *mimeData) { - init(); + if (!pixmap.isNull()) + return generateDragImageFromPixmap(pixmap); + if (mimeData->hasFormat("text/plain")) + return generateDragImageFromText(mimeData); + return generateDefaultDragImage(); } -QWasmDrag::~QWasmDrag() +emscripten::val +QWasmDrag::DragState::DragImage::generateDragImageFromText(const QMimeData *mimeData) { - if (m_mimeData) - delete m_mimeData; + emscripten::val dragImageElement = + emscripten::val::global("document") + .call<emscripten::val>("createElement", emscripten::val("span")); + + constexpr qsizetype MaxCharactersInDragImage = 100; + + const auto text = QString::fromUtf8(mimeData->data("text/plain")); + dragImageElement.set( + "innerText", + text.first(qMin(qsizetype(MaxCharactersInDragImage), text.length())).toStdString()); + return dragImageElement; } -void QWasmDrag::init() +emscripten::val QWasmDrag::DragState::DragImage::generateDefaultDragImage() { + emscripten::val dragImageElement = + emscripten::val::global("document") + .call<emscripten::val>("createElement", emscripten::val("div")); + + auto innerImgElement = emscripten::val::global("document") + .call<emscripten::val>("createElement", emscripten::val("img")); + innerImgElement.set("src", + "data:image/" + std::string("svg+xml") + ";base64," + + std::string(Base64IconStore::get()->getIcon( + Base64IconStore::IconType::QtLogo))); + + constexpr char DragImageSize[] = "50px"; + + dragImageElement["style"].set("width", DragImageSize); + innerImgElement["style"].set("width", DragImageSize); + dragImageElement["style"].set("display", "flex"); + + dragImageElement.call<void>("appendChild", innerImgElement); + return dragImageElement; } -void QWasmDrag::drop(const QPoint &globalPos, Qt::MouseButtons b, Qt::KeyboardModifiers mods) +emscripten::val QWasmDrag::DragState::DragImage::generateDragImageFromPixmap(const QPixmap &pixmap) { - QSimpleDrag::drop(globalPos, b, mods); + emscripten::val dragImageElement = + emscripten::val::global("document") + .call<emscripten::val>("createElement", emscripten::val("canvas")); + dragImageElement.set("width", pixmap.width()); + dragImageElement.set("height", pixmap.height()); + + dragImageElement["style"].set( + "width", std::to_string(pixmap.width() / pixmap.devicePixelRatio()) + "px"); + dragImageElement["style"].set( + "height", std::to_string(pixmap.height() / pixmap.devicePixelRatio()) + "px"); + + auto context2d = dragImageElement.call<emscripten::val>("getContext", emscripten::val("2d")); + auto imageData = context2d.call<emscripten::val>( + "createImageData", emscripten::val(pixmap.width()), emscripten::val(pixmap.height())); + + dom::drawImageToWebImageDataArray(pixmap.toImage().convertedTo(QImage::Format::Format_RGBA8888), + imageData, QRect(0, 0, pixmap.width(), pixmap.height())); + context2d.call<void>("putImageData", imageData, emscripten::val(0), emscripten::val(0)); + + return dragImageElement; } -void QWasmDrag::move(const QPoint &globalPos, Qt::MouseButtons b, Qt::KeyboardModifiers mods) +emscripten::val QWasmDrag::DragState::DragImage::htmlElement() { - QSimpleDrag::move(globalPos, b, mods); + return m_imageDomElement; } -void QWasmDrag::qWasmDrop() -{ - // collect mime - QWasmDrag *thisDrag = static_cast<QWasmDrag*>(QWasmIntegration::get()->drag()); - - if (thisDrag->m_mimeTypesCount != thisDrag->m_mimeData->formats().size()) - return; // keep collecting mimetypes - - // start drag enter - QWindowSystemInterface::handleDrag(thisDrag->m_wasmScreen->topLevelAt(thisDrag->m_mouseDropPoint), - thisDrag->m_mimeData, - thisDrag->m_mouseDropPoint, - thisDrag->m_dropActions, - thisDrag->m_qButton, - thisDrag->m_keyModifiers); - - // drag drop - QWindowSystemInterface::handleDrop(thisDrag->m_wasmScreen->topLevelAt(thisDrag->m_mouseDropPoint), - thisDrag->m_mimeData, - thisDrag->m_mouseDropPoint, - thisDrag->m_dropActions, - thisDrag->m_qButton, - thisDrag->m_keyModifiers); - - // drag leave - QWindowSystemInterface::handleDrag(thisDrag->m_wasmScreen->topLevelAt(thisDrag->m_mouseDropPoint), - nullptr, - QPoint(), - Qt::IgnoreAction, { }, { }); - - thisDrag->m_mimeData->clear(); - thisDrag->m_mimeTypesCount = 0; +QWasmDrag::DragState::DragState(QDrag *drag, QWindow *window, + std::function<void()> quitEventLoopClosure) + : drag(drag), window(window), quitEventLoopClosure(std::move(quitEventLoopClosure)) +{ } +QWasmDrag::DragState::~DragState() = default; + QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmdrag.h b/src/plugins/platforms/wasm/qwasmdrag.h index 6358b415c3..146a69ebe8 100644 --- a/src/plugins/platforms/wasm/qwasmdrag.h +++ b/src/plugins/platforms/wasm/qwasmdrag.h @@ -1,43 +1,47 @@ -// Copyright (C) 2022 The Qt Company Ltd. +// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only -#ifndef QWASMDRAG_H -#define QWASMDRAG_H +#ifndef QWINDOWSDRAG_H +#define QWINDOWSDRAG_H -#include <qpa/qplatformdrag.h> -#include <private/qsimpledrag_p.h> #include <private/qstdweb_p.h> -#include <QDrag> -#include "qwasmscreen.h" +#include <private/qsimpledrag_p.h> + +#include <qpa/qplatformdrag.h> +#include <QtGui/qdrag.h> -QT_REQUIRE_CONFIG(draganddrop); +#include <memory> QT_BEGIN_NAMESPACE -class QWasmDrag : public QSimpleDrag +struct DragEvent; + +class QWasmDrag final : public QSimpleDrag { public: - QWasmDrag(); - ~QWasmDrag(); + ~QWasmDrag() override; + QWasmDrag(const QWasmDrag &other) = delete; + QWasmDrag(QWasmDrag &&other) = delete; + QWasmDrag &operator=(const QWasmDrag &other) = delete; + QWasmDrag &operator=(QWasmDrag &&other) = delete; + + static QWasmDrag *instance(); - void drop(const QPoint &globalPos, Qt::MouseButtons b, Qt::KeyboardModifiers mods) override; - void move(const QPoint &globalPos, Qt::MouseButtons b, Qt::KeyboardModifiers mods) override; + void onNativeDragOver(DragEvent *event); + void onNativeDrop(DragEvent *event); + void onNativeDragStarted(DragEvent *event); + void onNativeDragFinished(DragEvent *event); - Qt::MouseButton m_qButton; - QPoint m_mouseDropPoint; - QFlags<Qt::KeyboardModifier> m_keyModifiers; - Qt::DropActions m_dropActions; - QWasmScreen *m_wasmScreen = nullptr; - int m_mimeTypesCount = 0; - QMimeData *m_mimeData = nullptr; - void qWasmDrop(); + // QPlatformDrag: + Qt::DropAction drag(QDrag *drag) final; private: - void init(); -}; + struct DragState; + std::unique_ptr<DragState> m_dragState; +}; QT_END_NAMESPACE -#endif // QWASMDRAG_H +#endif // QWINDOWSDRAG_H diff --git a/src/plugins/platforms/wasm/qwasmevent.cpp b/src/plugins/platforms/wasm/qwasmevent.cpp index f066d0041e..e418263655 100644 --- a/src/plugins/platforms/wasm/qwasmevent.cpp +++ b/src/plugins/platforms/wasm/qwasmevent.cpp @@ -3,8 +3,78 @@ #include "qwasmevent.h" +#include "qwasmkeytranslator.h" + +#include <QtCore/private/qmakearray_p.h> +#include <QtCore/private/qstringiterator_p.h> +#include <QtCore/qregularexpression.h> + QT_BEGIN_NAMESPACE +namespace { +constexpr std::string_view WebDeadKeyValue = "Dead"; + +bool isDeadKeyEvent(const char *key) +{ + return qstrncmp(key, WebDeadKeyValue.data(), WebDeadKeyValue.size()) == 0; +} + +Qt::Key getKeyFromCode(const std::string &code) +{ + if (auto mapping = QWasmKeyTranslator::mapWebKeyTextToQtKey(code.c_str())) + return *mapping; + + static QRegularExpression regex(QString(QStringLiteral(R"re((?:Key|Digit)(\w))re"))); + const auto codeQString = QString::fromStdString(code); + const auto match = regex.match(codeQString); + + if (!match.hasMatch()) + return Qt::Key_unknown; + + constexpr size_t CharacterIndex = 1; + return static_cast<Qt::Key>(match.capturedView(CharacterIndex).at(0).toLatin1()); +} + +Qt::Key webKeyToQtKey(const std::string &code, const std::string &key, bool isDeadKey, + QFlags<Qt::KeyboardModifier> modifiers) +{ + if (isDeadKey) { + auto mapped = getKeyFromCode(code); + switch (mapped) { + case Qt::Key_U: + return Qt::Key_Dead_Diaeresis; + case Qt::Key_E: + return Qt::Key_Dead_Acute; + case Qt::Key_I: + return Qt::Key_Dead_Circumflex; + case Qt::Key_N: + return Qt::Key_Dead_Tilde; + case Qt::Key_QuoteLeft: + return modifiers.testFlag(Qt::ShiftModifier) ? Qt::Key_Dead_Tilde : Qt::Key_Dead_Grave; + case Qt::Key_6: + return Qt::Key_Dead_Circumflex; + case Qt::Key_Apostrophe: + return modifiers.testFlag(Qt::ShiftModifier) ? Qt::Key_Dead_Diaeresis + : Qt::Key_Dead_Acute; + case Qt::Key_AsciiTilde: + return Qt::Key_Dead_Tilde; + default: + return Qt::Key_unknown; + } + } else if (auto mapping = QWasmKeyTranslator::mapWebKeyTextToQtKey(key.c_str())) { + return *mapping; + } + + // cast to unicode key + QString str = QString::fromUtf8(key.c_str()).toUpper(); + if (str.length() > 1) + return Qt::Key_unknown; + + QStringIterator i(str); + return static_cast<Qt::Key>(i.next(0)); +} +} // namespace + namespace KeyboardModifier { template <> @@ -16,10 +86,129 @@ QFlags<Qt::KeyboardModifier> getForEvent<EmscriptenKeyboardEvent>( } } // namespace KeyboardModifier -std::optional<PointerEvent> PointerEvent::fromWeb(emscripten::val event) +Event::Event(EventType type, emscripten::val webEvent) + : webEvent(webEvent), type(type) +{ +} + +Event::~Event() = default; + +Event::Event(const Event &other) = default; + +Event::Event(Event &&other) = default; + +Event &Event::operator=(const Event &other) = default; + +Event &Event::operator=(Event &&other) = default; + +KeyEvent::KeyEvent(EventType type, emscripten::val event) : Event(type, event) { - PointerEvent ret; + const auto code = event["code"].as<std::string>(); + const auto webKey = event["key"].as<std::string>(); + deadKey = isDeadKeyEvent(webKey.c_str()); + autoRepeat = event["repeat"].as<bool>(); + modifiers = KeyboardModifier::getForEvent(event); + key = webKeyToQtKey(code, webKey, deadKey, modifiers); + + text = QString::fromUtf8(webKey); + if (text.size() > 1) + text.clear(); + + if (key == Qt::Key_Tab) + text = "\t"; +} + +KeyEvent::~KeyEvent() = default; + +KeyEvent::KeyEvent(const KeyEvent &other) = default; + +KeyEvent::KeyEvent(KeyEvent &&other) = default; + +KeyEvent &KeyEvent::operator=(const KeyEvent &other) = default; + +KeyEvent &KeyEvent::operator=(KeyEvent &&other) = default; + +std::optional<KeyEvent> KeyEvent::fromWebWithDeadKeyTranslation(emscripten::val event, + QWasmDeadKeySupport *deadKeySupport) +{ + const auto eventType = ([&event]() -> std::optional<EventType> { + const auto eventTypeString = event["type"].as<std::string>(); + + if (eventTypeString == "keydown") + return EventType::KeyDown; + else if (eventTypeString == "keyup") + return EventType::KeyUp; + return std::nullopt; + })(); + if (!eventType) + return std::nullopt; + + auto result = KeyEvent(*eventType, event); + deadKeySupport->applyDeadKeyTranslations(&result); + + return result; +} + +MouseEvent::MouseEvent(EventType type, emscripten::val event) : Event(type, event) +{ + mouseButton = MouseEvent::buttonFromWeb(event["button"].as<int>()); + mouseButtons = MouseEvent::buttonsFromWeb(event["buttons"].as<unsigned short>()); + // The current button state (event.buttons) may be out of sync for some PointerDown + // events where the "down" state is very brief, for example taps on Apple trackpads. + // Qt expects that the current button state is in sync with the event, so we sync + // it up here. + if (type == EventType::PointerDown) + mouseButtons |= mouseButton; + localPoint = QPointF(event["offsetX"].as<qreal>(), event["offsetY"].as<qreal>()); + pointInPage = QPointF(event["pageX"].as<qreal>(), event["pageY"].as<qreal>()); + pointInViewport = QPointF(event["clientX"].as<qreal>(), event["clientY"].as<qreal>()); + modifiers = KeyboardModifier::getForEvent(event); +} + +MouseEvent::~MouseEvent() = default; + +MouseEvent::MouseEvent(const MouseEvent &other) = default; + +MouseEvent::MouseEvent(MouseEvent &&other) = default; + +MouseEvent &MouseEvent::operator=(const MouseEvent &other) = default; + +MouseEvent &MouseEvent::operator=(MouseEvent &&other) = default; + +PointerEvent::PointerEvent(EventType type, emscripten::val event) : MouseEvent(type, event) +{ + pointerId = event["pointerId"].as<int>(); + pointerType = ([type = event["pointerType"].as<std::string>()]() { + if (type == "mouse") + return PointerType::Mouse; + if (type == "touch") + return PointerType::Touch; + if (type == "pen") + return PointerType::Pen; + return PointerType::Other; + })(); + width = event["width"].as<qreal>(); + height = event["height"].as<qreal>(); + pressure = event["pressure"].as<qreal>(); + tiltX = event["tiltX"].as<qreal>(); + tiltY = event["tiltY"].as<qreal>(); + tangentialPressure = event["tangentialPressure"].as<qreal>(); + twist = event["twist"].as<qreal>(); + isPrimary = event["isPrimary"].as<bool>(); +} + +PointerEvent::~PointerEvent() = default; + +PointerEvent::PointerEvent(const PointerEvent &other) = default; + +PointerEvent::PointerEvent(PointerEvent &&other) = default; +PointerEvent &PointerEvent::operator=(const PointerEvent &other) = default; + +PointerEvent &PointerEvent::operator=(PointerEvent &&other) = default; + +std::optional<PointerEvent> PointerEvent::fromWeb(emscripten::val event) +{ const auto eventType = ([&event]() -> std::optional<EventType> { const auto eventTypeString = event["type"].as<std::string>(); @@ -38,16 +227,112 @@ std::optional<PointerEvent> PointerEvent::fromWeb(emscripten::val event) if (!eventType) return std::nullopt; - ret.type = *eventType; - ret.pointerType = event["pointerType"].as<std::string>() == "mouse" ? - PointerType::Mouse : PointerType::Other; - ret.mouseButton = MouseEvent::buttonFromWeb(event["button"].as<int>()); - ret.mouseButtons = MouseEvent::buttonsFromWeb(event["buttons"].as<unsigned short>()); - ret.point = QPoint(event["offsetX"].as<int>(), event["offsetY"].as<int>()); - ret.pointerId = event["pointerId"].as<int>(); - ret.modifiers = KeyboardModifier::getForEvent(event); + return PointerEvent(*eventType, event); +} + +DragEvent::DragEvent(EventType type, emscripten::val event, QWindow *window) + : MouseEvent(type, event), dataTransfer(event["dataTransfer"]), targetWindow(window) +{ + dropAction = ([event]() { + const std::string effect = event["dataTransfer"]["dropEffect"].as<std::string>(); + + if (effect == "copy") + return Qt::CopyAction; + else if (effect == "move") + return Qt::MoveAction; + else if (effect == "link") + return Qt::LinkAction; + return Qt::IgnoreAction; + })(); +} + +DragEvent::~DragEvent() = default; + +DragEvent::DragEvent(const DragEvent &other) = default; + +DragEvent::DragEvent(DragEvent &&other) = default; + +DragEvent &DragEvent::operator=(const DragEvent &other) = default; + +DragEvent &DragEvent::operator=(DragEvent &&other) = default; + +std::optional<DragEvent> DragEvent::fromWeb(emscripten::val event, QWindow *targetWindow) +{ + const auto eventType = ([&event]() -> std::optional<EventType> { + const auto eventTypeString = event["type"].as<std::string>(); + + if (eventTypeString == "dragend") + return EventType::DragEnd; + if (eventTypeString == "dragover") + return EventType::DragOver; + if (eventTypeString == "dragstart") + return EventType::DragStart; + if (eventTypeString == "drop") + return EventType::Drop; + return std::nullopt; + })(); + if (!eventType) + return std::nullopt; + return DragEvent(*eventType, event, targetWindow); +} - return ret; +void DragEvent::cancelDragStart() +{ + Q_ASSERT_X(type == EventType::DragStart, Q_FUNC_INFO, "Only supported for DragStart"); + webEvent.call<void>("preventDefault"); +} + +void DragEvent::acceptDragOver() +{ + Q_ASSERT_X(type == EventType::DragOver, Q_FUNC_INFO, "Only supported for DragOver"); + webEvent.call<void>("preventDefault"); +} + +void DragEvent::acceptDrop() +{ + Q_ASSERT_X(type == EventType::Drop, Q_FUNC_INFO, "Only supported for Drop"); + webEvent.call<void>("preventDefault"); +} + +WheelEvent::WheelEvent(EventType type, emscripten::val event) : MouseEvent(type, event) +{ + deltaMode = ([event]() { + const int deltaMode = event["deltaMode"].as<int>(); + const auto jsWheelEventType = emscripten::val::global("WheelEvent"); + if (deltaMode == jsWheelEventType["DOM_DELTA_PIXEL"].as<int>()) + return DeltaMode::Pixel; + else if (deltaMode == jsWheelEventType["DOM_DELTA_LINE"].as<int>()) + return DeltaMode::Line; + return DeltaMode::Page; + })(); + + delta = QPointF(event["deltaX"].as<qreal>(), event["deltaY"].as<qreal>()); + + webkitDirectionInvertedFromDevice = event["webkitDirectionInvertedFromDevice"].as<bool>(); +} + +WheelEvent::~WheelEvent() = default; + +WheelEvent::WheelEvent(const WheelEvent &other) = default; + +WheelEvent::WheelEvent(WheelEvent &&other) = default; + +WheelEvent &WheelEvent::operator=(const WheelEvent &other) = default; + +WheelEvent &WheelEvent::operator=(WheelEvent &&other) = default; + +std::optional<WheelEvent> WheelEvent::fromWeb(emscripten::val event) +{ + const auto eventType = ([&event]() -> std::optional<EventType> { + const auto eventTypeString = event["type"].as<std::string>(); + + if (eventTypeString == "wheel") + return EventType::Wheel; + return std::nullopt; + })(); + if (!eventType) + return std::nullopt; + return WheelEvent(*eventType, event); } QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmevent.h b/src/plugins/platforms/wasm/qwasmevent.h index df9276b746..bd0fb39f11 100644 --- a/src/plugins/platforms/wasm/qwasmevent.h +++ b/src/plugins/platforms/wasm/qwasmevent.h @@ -5,11 +5,12 @@ #define QWASMEVENT_H #include "qwasmplatform.h" +#include "qwasmdom.h" #include <QtCore/qglobal.h> #include <QtCore/qnamespace.h> #include <QtGui/qevent.h> - +#include <private/qstdweb_p.h> #include <QPoint> #include <emscripten/html5.h> @@ -17,16 +18,29 @@ QT_BEGIN_NAMESPACE +class QWasmDeadKeySupport; +class QWindow; + enum class EventType { + DragEnd, + DragOver, + DragStart, + Drop, + KeyDown, + KeyUp, PointerDown, PointerMove, PointerUp, PointerEnter, PointerLeave, + PointerCancel, + Wheel, }; enum class PointerType { Mouse, + Touch, + Pen, Other, }; @@ -35,6 +49,8 @@ enum class WindowArea { Client, }; +enum class DeltaMode { Pixel, Line, Page }; + namespace KeyboardModifier { namespace internal { @@ -107,17 +123,47 @@ QFlags<Qt::KeyboardModifier> getForEvent<EmscriptenKeyboardEvent>( } // namespace KeyboardModifier -struct Q_CORE_EXPORT Event +struct Event { + Event(EventType type, emscripten::val webEvent); + ~Event(); + Event(const Event &other); + Event(Event &&other); + Event &operator=(const Event &other); + Event &operator=(Event &&other); + + emscripten::val webEvent; EventType type; + emscripten::val target() const { return webEvent["target"]; } }; -struct Q_CORE_EXPORT MouseEvent : public Event +struct KeyEvent : public Event { - QPoint point; - Qt::MouseButton mouseButton; - Qt::MouseButtons mouseButtons; + static std::optional<KeyEvent> + fromWebWithDeadKeyTranslation(emscripten::val webEvent, QWasmDeadKeySupport *deadKeySupport); + + KeyEvent(EventType type, emscripten::val webEvent); + ~KeyEvent(); + KeyEvent(const KeyEvent &other); + KeyEvent(KeyEvent &&other); + KeyEvent &operator=(const KeyEvent &other); + KeyEvent &operator=(KeyEvent &&other); + + Qt::Key key; QFlags<Qt::KeyboardModifier> modifiers; + bool deadKey; + QString text; + bool autoRepeat; +}; + +struct MouseEvent : public Event +{ + MouseEvent(EventType type, emscripten::val webEvent); + ~MouseEvent(); + MouseEvent(const MouseEvent &other); + MouseEvent(MouseEvent &&other); + MouseEvent &operator=(const MouseEvent &other); + MouseEvent &operator=(MouseEvent &&other); static constexpr Qt::MouseButton buttonFromWeb(int webButton) { switch (webButton) { @@ -153,14 +199,72 @@ struct Q_CORE_EXPORT MouseEvent : public Event return QEvent::None; } } + + QPointF localPoint; + QPointF pointInPage; + QPointF pointInViewport; + Qt::MouseButton mouseButton; + Qt::MouseButtons mouseButtons; + QFlags<Qt::KeyboardModifier> modifiers; }; -struct Q_CORE_EXPORT PointerEvent : public MouseEvent +struct PointerEvent : public MouseEvent { static std::optional<PointerEvent> fromWeb(emscripten::val webEvent); + PointerEvent(EventType type, emscripten::val webEvent); + ~PointerEvent(); + PointerEvent(const PointerEvent &other); + PointerEvent(PointerEvent &&other); + PointerEvent &operator=(const PointerEvent &other); + PointerEvent &operator=(PointerEvent &&other); + PointerType pointerType; int pointerId; + qreal pressure; + qreal tiltX; + qreal tiltY; + qreal tangentialPressure; + qreal twist; + qreal width; + qreal height; + bool isPrimary; +}; + +struct DragEvent : public MouseEvent +{ + static std::optional<DragEvent> fromWeb(emscripten::val webEvent, QWindow *targetQWindow); + + DragEvent(EventType type, emscripten::val webEvent, QWindow *targetQWindow); + ~DragEvent(); + DragEvent(const DragEvent &other); + DragEvent(DragEvent &&other); + DragEvent &operator=(const DragEvent &other); + DragEvent &operator=(DragEvent &&other); + + void cancelDragStart(); + void acceptDragOver(); + void acceptDrop(); + + Qt::DropAction dropAction; + dom::DataTransfer dataTransfer; + QWindow *targetWindow; +}; + +struct WheelEvent : public MouseEvent +{ + static std::optional<WheelEvent> fromWeb(emscripten::val webEvent); + + WheelEvent(EventType type, emscripten::val webEvent); + ~WheelEvent(); + WheelEvent(const WheelEvent &other); + WheelEvent(WheelEvent &&other); + WheelEvent &operator=(const WheelEvent &other); + WheelEvent &operator=(WheelEvent &&other); + + DeltaMode deltaMode; + bool webkitDirectionInvertedFromDevice; + QPointF delta; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmeventdispatcher.cpp b/src/plugins/platforms/wasm/qwasmeventdispatcher.cpp index 2fd1a30401..1f2d3095d6 100644 --- a/src/plugins/platforms/wasm/qwasmeventdispatcher.cpp +++ b/src/plugins/platforms/wasm/qwasmeventdispatcher.cpp @@ -2,16 +2,34 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmeventdispatcher.h" +#include "qwasmintegration.h" #include <QtGui/qpa/qwindowsysteminterface.h> QT_BEGIN_NAMESPACE // Note: All event dispatcher functionality is implemented in QEventDispatcherWasm -// in QtCore, except for processWindowSystemEvents() below which uses API from QtGui. -void QWasmEventDispatcher::processWindowSystemEvents(QEventLoop::ProcessEventsFlags flags) +// in QtCore, except for processPostedEvents() below which uses API from QtGui. +bool QWasmEventDispatcher::processPostedEvents() { - QWindowSystemInterface::sendWindowSystemEvents(flags); + QEventDispatcherWasm::processPostedEvents(); + return QWindowSystemInterface::sendWindowSystemEvents(QEventLoop::AllEvents); +} + +void QWasmEventDispatcher::onLoaded() +{ + // This function is called when the application is ready to paint + // the first frame. Send the qtlaoder onLoaded event first (via + // the base class implementation), and then enable/call requestUpdate + // to deliver a frame. + QEventDispatcherWasm::onLoaded(); + + // Make sure all screens have a defined size; and pick + // up size changes due to onLoaded event handling. + QWasmIntegration *wasmIntegration = QWasmIntegration::get(); + wasmIntegration->resizeAllScreens(); + + wasmIntegration->releaseRequesetUpdateHold(); } QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmeventdispatcher.h b/src/plugins/platforms/wasm/qwasmeventdispatcher.h index a28fa7263b..cbf10482e3 100644 --- a/src/plugins/platforms/wasm/qwasmeventdispatcher.h +++ b/src/plugins/platforms/wasm/qwasmeventdispatcher.h @@ -11,7 +11,8 @@ QT_BEGIN_NAMESPACE class QWasmEventDispatcher : public QEventDispatcherWasm { protected: - void processWindowSystemEvents(QEventLoop::ProcessEventsFlags flags) override; + bool processPostedEvents() override; + void onLoaded() override; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmeventtranslator.cpp b/src/plugins/platforms/wasm/qwasmeventtranslator.cpp deleted file mode 100644 index 323a985454..0000000000 --- a/src/plugins/platforms/wasm/qwasmeventtranslator.cpp +++ /dev/null @@ -1,362 +0,0 @@ -// Copyright (C) 2018 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only - -#include "qwasmeventtranslator.h" -#include "qwasmeventdispatcher.h" -#include "qwasmcompositor.h" -#include "qwasmintegration.h" -#include "qwasmclipboard.h" -#include "qwasmstring.h" -#include "qwasmcursor.h" -#include <QtGui/qevent.h> -#include <qpa/qwindowsysteminterface.h> -#include <QtCore/qcoreapplication.h> -#include <QtCore/qglobal.h> -#include <QtCore/qobject.h> - -#include <QtCore/qdeadlinetimer.h> -#include <private/qmakearray_p.h> -#include <QtCore/qnamespace.h> -#include <QCursor> -#include <QtCore/private/qstringiterator_p.h> - -#include <emscripten/bind.h> - -#include <iostream> - -QT_BEGIN_NAMESPACE - -using namespace emscripten; - -namespace { -constexpr std::string_view WebDeadKeyValue = "Dead"; - -struct Emkb2QtData -{ - static constexpr char StringTerminator = '\0'; - - const char *em; - unsigned int qt; - - constexpr bool operator<=(const Emkb2QtData &that) const noexcept - { - return !(strcmp(that) > 0); - } - - bool operator<(const Emkb2QtData &that) const noexcept { return ::strcmp(em, that.em) < 0; } - - constexpr bool operator==(const Emkb2QtData &that) const noexcept { return strcmp(that) == 0; } - - constexpr int strcmp(const Emkb2QtData &that, const int i = 0) const - { - return em[i] == StringTerminator && that.em[i] == StringTerminator ? 0 - : em[i] == StringTerminator ? -1 - : that.em[i] == StringTerminator ? 1 - : em[i] < that.em[i] ? -1 - : em[i] > that.em[i] ? 1 - : strcmp(that, i + 1); - } -}; - -template<unsigned int Qt, char ... EmChar> -struct Emkb2Qt -{ - static constexpr const char storage[sizeof ... (EmChar) + 1] = {EmChar..., '\0'}; - using Type = Emkb2QtData; - static constexpr Type data() noexcept { return Type{storage, Qt}; } -}; - -template<unsigned int Qt, char ... EmChar> constexpr char Emkb2Qt<Qt, EmChar...>::storage[]; - -static constexpr const auto WebToQtKeyCodeMappings = qMakeArray( - QSortedData< - Emkb2Qt< Qt::Key_Escape, 'E','s','c','a','p','e' >, - Emkb2Qt< Qt::Key_Tab, 'T','a','b' >, - Emkb2Qt< Qt::Key_Backspace, 'B','a','c','k','s','p','a','c','e' >, - Emkb2Qt< Qt::Key_Return, 'E','n','t','e','r' >, - Emkb2Qt< Qt::Key_Insert, 'I','n','s','e','r','t' >, - Emkb2Qt< Qt::Key_Delete, 'D','e','l','e','t','e' >, - Emkb2Qt< Qt::Key_Pause, 'P','a','u','s','e' >, - Emkb2Qt< Qt::Key_Pause, 'C','l','e','a','r' >, - Emkb2Qt< Qt::Key_Home, 'H','o','m','e' >, - Emkb2Qt< Qt::Key_End, 'E','n','d' >, - Emkb2Qt< Qt::Key_Left, 'A','r','r','o','w','L','e','f','t' >, - Emkb2Qt< Qt::Key_Up, 'A','r','r','o','w','U','p' >, - Emkb2Qt< Qt::Key_Right, 'A','r','r','o','w','R','i','g','h','t' >, - Emkb2Qt< Qt::Key_Down, 'A','r','r','o','w','D','o','w','n' >, - Emkb2Qt< Qt::Key_PageUp, 'P','a','g','e','U','p' >, - Emkb2Qt< Qt::Key_PageDown, 'P','a','g','e','D','o','w','n' >, - Emkb2Qt< Qt::Key_Shift, 'S','h','i','f','t' >, - Emkb2Qt< Qt::Key_Control, 'C','o','n','t','r','o','l' >, - Emkb2Qt< Qt::Key_Meta, 'M','e','t','a'>, - Emkb2Qt< Qt::Key_Meta, 'O','S'>, - Emkb2Qt< Qt::Key_Alt, 'A','l','t','L','e','f','t' >, - Emkb2Qt< Qt::Key_Alt, 'A','l','t' >, - Emkb2Qt< Qt::Key_CapsLock, 'C','a','p','s','L','o','c','k' >, - Emkb2Qt< Qt::Key_NumLock, 'N','u','m','L','o','c','k' >, - Emkb2Qt< Qt::Key_ScrollLock, 'S','c','r','o','l','l','L','o','c','k' >, - Emkb2Qt< Qt::Key_F1, 'F','1' >, - Emkb2Qt< Qt::Key_F2, 'F','2' >, - Emkb2Qt< Qt::Key_F3, 'F','3' >, - Emkb2Qt< Qt::Key_F4, 'F','4' >, - Emkb2Qt< Qt::Key_F5, 'F','5' >, - Emkb2Qt< Qt::Key_F6, 'F','6' >, - Emkb2Qt< Qt::Key_F7, 'F','7' >, - Emkb2Qt< Qt::Key_F8, 'F','8' >, - Emkb2Qt< Qt::Key_F9, 'F','9' >, - Emkb2Qt< Qt::Key_F10, 'F','1','0' >, - Emkb2Qt< Qt::Key_F11, 'F','1','1' >, - Emkb2Qt< Qt::Key_F12, 'F','1','2' >, - Emkb2Qt< Qt::Key_F13, 'F','1','3' >, - Emkb2Qt< Qt::Key_F14, 'F','1','4' >, - Emkb2Qt< Qt::Key_F15, 'F','1','5' >, - Emkb2Qt< Qt::Key_F16, 'F','1','6' >, - Emkb2Qt< Qt::Key_F17, 'F','1','7' >, - Emkb2Qt< Qt::Key_F18, 'F','1','8' >, - Emkb2Qt< Qt::Key_F19, 'F','1','9' >, - Emkb2Qt< Qt::Key_F20, 'F','2','0' >, - Emkb2Qt< Qt::Key_F21, 'F','2','1' >, - Emkb2Qt< Qt::Key_F22, 'F','2','2' >, - Emkb2Qt< Qt::Key_F23, 'F','2','3' >, - Emkb2Qt< Qt::Key_Paste, 'P','a','s','t','e' >, - Emkb2Qt< Qt::Key_AltGr, 'A','l','t','R','i','g','h','t' >, - Emkb2Qt< Qt::Key_Help, 'H','e','l','p' >, - Emkb2Qt< Qt::Key_yen, 'I','n','t','l','Y','e','n' >, - Emkb2Qt< Qt::Key_Menu, 'C','o','n','t','e','x','t','M','e','n','u' > - >::Data{} - ); - -static constexpr const auto WebToQtKeyCodeMappingsWithShift = qMakeArray( - QSortedData< - // shifted - Emkb2Qt< Qt::Key_Agrave, '\xc3','\x80' >, - Emkb2Qt< Qt::Key_Aacute, '\xc3','\x81' >, - Emkb2Qt< Qt::Key_Acircumflex, '\xc3','\x82' >, - Emkb2Qt< Qt::Key_Adiaeresis, '\xc3','\x84' >, - Emkb2Qt< Qt::Key_AE, '\xc3','\x86' >, - Emkb2Qt< Qt::Key_Atilde, '\xc3','\x83' >, - Emkb2Qt< Qt::Key_Aring, '\xc3','\x85' >, - Emkb2Qt< Qt::Key_Egrave, '\xc3','\x88' >, - Emkb2Qt< Qt::Key_Eacute, '\xc3','\x89' >, - Emkb2Qt< Qt::Key_Ecircumflex, '\xc3','\x8a' >, - Emkb2Qt< Qt::Key_Ediaeresis, '\xc3','\x8b' >, - Emkb2Qt< Qt::Key_Iacute, '\xc3','\x8d' >, - Emkb2Qt< Qt::Key_Icircumflex, '\xc3','\x8e' >, - Emkb2Qt< Qt::Key_Idiaeresis, '\xc3','\x8f' >, - Emkb2Qt< Qt::Key_Igrave, '\xc3','\x8c' >, - Emkb2Qt< Qt::Key_Ocircumflex, '\xc3','\x94' >, - Emkb2Qt< Qt::Key_Odiaeresis, '\xc3','\x96' >, - Emkb2Qt< Qt::Key_Ograve, '\xc3','\x92' >, - Emkb2Qt< Qt::Key_Oacute, '\xc3','\x93' >, - Emkb2Qt< Qt::Key_Ooblique, '\xc3','\x98' >, - Emkb2Qt< Qt::Key_Otilde, '\xc3','\x95' >, - Emkb2Qt< Qt::Key_Ucircumflex, '\xc3','\x9b' >, - Emkb2Qt< Qt::Key_Udiaeresis, '\xc3','\x9c' >, - Emkb2Qt< Qt::Key_Ugrave, '\xc3','\x99' >, - Emkb2Qt< Qt::Key_Uacute, '\xc3','\x9a' >, - Emkb2Qt< Qt::Key_Ntilde, '\xc3','\x91' >, - Emkb2Qt< Qt::Key_Ccedilla, '\xc3','\x87' >, - Emkb2Qt< Qt::Key_ydiaeresis, '\xc3','\x8f' >, - Emkb2Qt< Qt::Key_Yacute, '\xc3','\x9d' > - >::Data{} -); - -std::optional<Qt::Key> findMappingByBisection(const char *toFind) -{ - const Emkb2QtData searchKey{ toFind, 0 }; - const auto it = std::lower_bound(WebToQtKeyCodeMappings.cbegin(), WebToQtKeyCodeMappings.cend(), - searchKey); - return it != WebToQtKeyCodeMappings.cend() && searchKey == *it ? static_cast<Qt::Key>(it->qt) - : std::optional<Qt::Key>(); -} - -bool isDeadKeyEvent(const EmscriptenKeyboardEvent *emKeyEvent) -{ - return qstrncmp(emKeyEvent->key, WebDeadKeyValue.data(), WebDeadKeyValue.size()) == 0; -} - -Qt::Key translateEmscriptKey(const EmscriptenKeyboardEvent *emscriptKey) -{ - if (isDeadKeyEvent(emscriptKey)) { - if (auto mapping = findMappingByBisection(emscriptKey->code)) - return *mapping; - } - if (auto mapping = findMappingByBisection(emscriptKey->key)) - return *mapping; - - // cast to unicode key - QString str = QString::fromUtf8(emscriptKey->key).toUpper(); - QStringIterator i(str); - return static_cast<Qt::Key>(i.next(0)); -} - -struct KeyMapping { Qt::Key from, to; }; - -constexpr KeyMapping tildeKeyTable[] = { // ~ - { Qt::Key_A, Qt::Key_Atilde }, - { Qt::Key_N, Qt::Key_Ntilde }, - { Qt::Key_O, Qt::Key_Otilde }, -}; -constexpr KeyMapping graveKeyTable[] = { // ` - { Qt::Key_A, Qt::Key_Agrave }, - { Qt::Key_E, Qt::Key_Egrave }, - { Qt::Key_I, Qt::Key_Igrave }, - { Qt::Key_O, Qt::Key_Ograve }, - { Qt::Key_U, Qt::Key_Ugrave }, -}; -constexpr KeyMapping acuteKeyTable[] = { // ' - { Qt::Key_A, Qt::Key_Aacute }, - { Qt::Key_E, Qt::Key_Eacute }, - { Qt::Key_I, Qt::Key_Iacute }, - { Qt::Key_O, Qt::Key_Oacute }, - { Qt::Key_U, Qt::Key_Uacute }, - { Qt::Key_Y, Qt::Key_Yacute }, -}; -constexpr KeyMapping diaeresisKeyTable[] = { // umlaut ¨ - { Qt::Key_A, Qt::Key_Adiaeresis }, - { Qt::Key_E, Qt::Key_Ediaeresis }, - { Qt::Key_I, Qt::Key_Idiaeresis }, - { Qt::Key_O, Qt::Key_Odiaeresis }, - { Qt::Key_U, Qt::Key_Udiaeresis }, - { Qt::Key_Y, Qt::Key_ydiaeresis }, -}; -constexpr KeyMapping circumflexKeyTable[] = { // ^ - { Qt::Key_A, Qt::Key_Acircumflex }, - { Qt::Key_E, Qt::Key_Ecircumflex }, - { Qt::Key_I, Qt::Key_Icircumflex }, - { Qt::Key_O, Qt::Key_Ocircumflex }, - { Qt::Key_U, Qt::Key_Ucircumflex }, -}; - -static Qt::Key find_impl(const KeyMapping *first, const KeyMapping *last, Qt::Key key) noexcept -{ - while (first != last) { - if (first->from == key) - return first->to; - ++first; - } - return Qt::Key_unknown; -} - -template <size_t N> -static Qt::Key find(const KeyMapping (&map)[N], Qt::Key key) noexcept -{ - return find_impl(map, map + N, key); -} - -Qt::Key translateBaseKeyUsingDeadKey(Qt::Key accentBaseKey, Qt::Key deadKey) -{ - switch (deadKey) { - case Qt::Key_QuoteLeft: { - // ` macOS: Key_Dead_Grave - return platform() == Platform::MacOS ? find(graveKeyTable, accentBaseKey) - : find(diaeresisKeyTable, accentBaseKey); - } - case Qt::Key_O: // ´ Key_Dead_Grave - return find(graveKeyTable, accentBaseKey); - case Qt::Key_E: // ´ Key_Dead_Acute - return find(acuteKeyTable, accentBaseKey); - case Qt::Key_AsciiTilde: - case Qt::Key_N: // Key_Dead_Tilde - return find(tildeKeyTable, accentBaseKey); - case Qt::Key_U: // ¨ Key_Dead_Diaeresis - return find(diaeresisKeyTable, accentBaseKey); - case Qt::Key_I: // macOS Key_Dead_Circumflex - case Qt::Key_6: // linux - case Qt::Key_Apostrophe: // linux - return find(circumflexKeyTable, accentBaseKey); - default: - return Qt::Key_unknown; - }; -} - -template<class T> -std::optional<QString> findKeyTextByKeyId(const T &mappingArray, Qt::Key qtKey) -{ - const auto it = std::find_if(mappingArray.cbegin(), mappingArray.cend(), - [qtKey](const Emkb2QtData &data) { return data.qt == qtKey; }); - return it != mappingArray.cend() ? it->em : std::optional<QString>(); -} -} // namespace - -QWasmEventTranslator::QWasmEventTranslator() = default; - -QWasmEventTranslator::~QWasmEventTranslator() = default; - -QCursor QWasmEventTranslator::cursorForEdges(Qt::Edges edges) -{ - switch (edges) { - case Qt::Edge::LeftEdge | Qt::Edge::TopEdge: - case Qt::Edge::RightEdge | Qt::Edge::BottomEdge: - return Qt::SizeFDiagCursor; - case Qt::Edge::LeftEdge | Qt::Edge::BottomEdge: - case Qt::Edge::RightEdge | Qt::Edge::TopEdge: - return Qt::SizeBDiagCursor; - case Qt::Edge::TopEdge: - case Qt::Edge::BottomEdge: - return Qt::SizeVerCursor; - case Qt::Edge::LeftEdge: - case Qt::Edge::RightEdge: - return Qt::SizeHorCursor; - case Qt::Edge(0): - return Qt::ArrowCursor; - default: - Q_ASSERT(false); // Bad edges - } - return Qt::ArrowCursor; -} - -QWasmEventTranslator::TranslatedEvent -QWasmEventTranslator::translateKeyEvent(int emEventType, const EmscriptenKeyboardEvent *keyEvent) -{ - TranslatedEvent ret; - switch (emEventType) { - case EMSCRIPTEN_EVENT_KEYDOWN: - ret.type = QEvent::KeyPress; - break; - case EMSCRIPTEN_EVENT_KEYUP: - ret.type = QEvent::KeyRelease; - break; - default: - // Should not be reached - do not call with this event type. - Q_ASSERT(false); - break; - }; - - ret.key = translateEmscriptKey(keyEvent); - - if (isDeadKeyEvent(keyEvent) || ret.key == Qt::Key_AltGr) { - if (keyEvent->shiftKey && ret.key == Qt::Key_QuoteLeft) - ret.key = Qt::Key_AsciiTilde; - m_emDeadKey = ret.key; - } - - if (m_emDeadKey != Qt::Key_unknown - && (m_keyModifiedByDeadKeyOnPress == Qt::Key_unknown - || ret.key == m_keyModifiedByDeadKeyOnPress)) { - const Qt::Key baseKey = ret.key; - const Qt::Key translatedKey = translateBaseKeyUsingDeadKey(baseKey, m_emDeadKey); - if (translatedKey != Qt::Key_unknown) - ret.key = translatedKey; - - if (auto text = keyEvent->shiftKey - ? findKeyTextByKeyId(WebToQtKeyCodeMappingsWithShift, ret.key) - : findKeyTextByKeyId(WebToQtKeyCodeMappings, ret.key)) { - if (ret.type == QEvent::KeyPress) { - Q_ASSERT(m_keyModifiedByDeadKeyOnPress == Qt::Key_unknown); - m_keyModifiedByDeadKeyOnPress = baseKey; - } else { - Q_ASSERT(ret.type == QEvent::KeyRelease); - Q_ASSERT(m_keyModifiedByDeadKeyOnPress == baseKey); - m_keyModifiedByDeadKeyOnPress = Qt::Key_unknown; - m_emDeadKey = Qt::Key_unknown; - } - ret.text = *text; - return ret; - } - } - ret.text = QString::fromUtf8(keyEvent->key); - return ret; -} - -QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmeventtranslator.h b/src/plugins/platforms/wasm/qwasmeventtranslator.h deleted file mode 100644 index d89b543386..0000000000 --- a/src/plugins/platforms/wasm/qwasmeventtranslator.h +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (C) 2018 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only - -#ifndef QWASMEVENTTRANSLATOR_H -#define QWASMEVENTTRANSLATOR_H - -#include <QtCore/qobject.h> -#include <QtCore/qrect.h> -#include <QtCore/qpoint.h> -#include <emscripten/html5.h> -#include "qwasmwindow.h" -#include <QtGui/qinputdevice.h> -#include <QHash> -#include <QCursor> -#include "qwasmevent.h" -#include "qwasmplatform.h" - -QT_BEGIN_NAMESPACE - -class QWindow; - -class QWasmEventTranslator : public QObject -{ - Q_OBJECT - -public: - struct TranslatedEvent - { - QEvent::Type type; - Qt::Key key; - QString text; - }; - explicit QWasmEventTranslator(); - ~QWasmEventTranslator(); - - static QCursor cursorForEdges(Qt::Edges edges); - - TranslatedEvent translateKeyEvent(int emEventType, const EmscriptenKeyboardEvent *keyEvent); - -private: - static quint64 getTimestamp(); - - Qt::Key m_emDeadKey = Qt::Key_unknown; - Qt::Key m_keyModifiedByDeadKeyOnPress = Qt::Key_unknown; -}; - -QT_END_NAMESPACE -#endif // QWASMEVENTTRANSLATOR_H diff --git a/src/plugins/platforms/wasm/qwasmfontdatabase.cpp b/src/plugins/platforms/wasm/qwasmfontdatabase.cpp index 37f80279e0..3f3dc10f71 100644 --- a/src/plugins/platforms/wasm/qwasmfontdatabase.cpp +++ b/src/plugins/platforms/wasm/qwasmfontdatabase.cpp @@ -6,152 +6,315 @@ #include <QtCore/qfile.h> #include <QtCore/private/qstdweb_p.h> +#include <QtCore/private/qeventdispatcher_wasm_p.h> #include <QtGui/private/qguiapplication_p.h> #include <emscripten.h> #include <emscripten/val.h> #include <emscripten/bind.h> -// FIXME: replace with shared implementation from qstdweb -QByteArray fromUint8Array(emscripten::val uint8array) -{ - qstdweb::ArrayBuffer arrayBuffer(uint8array); - - using qstdweb::Uint8Array; - Uint8Array sourceArray(arrayBuffer); - if (sourceArray.length() > std::numeric_limits<qsizetype>::max()) - return QByteArray(); - QByteArray destinationArray; - destinationArray.resize(sourceArray.length()); - sourceArray.copyTo(destinationArray.data()); - return destinationArray; -} +#include <map> +#include <array> QT_BEGIN_NAMESPACE using namespace emscripten; using namespace Qt::StringLiterals; -void QWasmFontDatabase::populateFontDatabase() + +namespace { + +class FontData { - // Load font file from resources. Currently - // all fonts needs to be bundled with the nexe - // as Qt resources. +public: + FontData(val fontData) + :m_fontData(fontData) {} - const QString fontFileNames[] = { - QStringLiteral(":/fonts/DejaVuSansMono.ttf"), - QStringLiteral(":/fonts/Vera.ttf"), - QStringLiteral(":/fonts/DejaVuSans.ttf"), - }; - for (const QString &fontFileName : fontFileNames) { - QFile theFont(fontFileName); - if (!theFont.open(QIODevice::ReadOnly)) - break; + QString family() const + { + return QString::fromStdString(m_fontData["family"].as<std::string>()); + } - QFreeTypeFontDatabase::addTTFile(theFont.readAll(), fontFileName.toLatin1()); + QString fullName() const + { + return QString::fromStdString(m_fontData["fullName"].as<std::string>()); } - // check if local-fonts API is available in the browser - val window = val::global("window"); - val fonts = window["queryLocalFonts"]; + QString postscriptName() const + { + return QString::fromStdString(m_fontData["postscriptName"].as<std::string>()); + } - if (fonts.isUndefined()) - return; + QString style() const + { + return QString::fromStdString(m_fontData["style"].as<std::string>()); + } - val permissions = val::global("navigator")["permissions"]; - if (permissions["request"].isUndefined()) + val value() const + { + return m_fontData; + } + +private: + val m_fontData; +}; + +val makeObject(const char *key, const char *value) +{ + val obj = val::object(); + obj.set(key, std::string(value)); + return obj; +} + +void printError(val err) { + qCWarning(lcQpaFonts) + << QString::fromStdString(err["name"].as<std::string>()) + << QString::fromStdString(err["message"].as<std::string>()); + QWasmFontDatabase::endAllFontFileLoading(); +} + +void checkFontAccessPermitted(std::function<void(bool)> callback) +{ + const val permissions = val::global("navigator")["permissions"]; + if (permissions.isUndefined()) { + callback(false); return; + } - val requestLocalFontsPermission = val::object(); - requestLocalFontsPermission.set("name", std::string("local-fonts")); - - qstdweb::PromiseCallbacks permissionRequestCallbacks { - .thenFunc = [window](val status) { - qCDebug(lcQpaFonts) << "onFontPermissionSuccess:" - << QString::fromStdString(status["state"].as<std::string>()); - - // query all available local fonts and call registerFontFamily for each of them - qstdweb::Promise::make(window, "queryLocalFonts", { - .thenFunc = [](val status) { - const int count = status["length"].as<int>(); - for (int i = 0; i < count; ++i) { - val font = status.call<val>("at", i); - const std::string family = font["family"].as<std::string>(); - QFreeTypeFontDatabase::registerFontFamily(QString::fromStdString(family)); - } - QWasmFontDatabase::notifyFontsChanged(); - }, - .catchFunc = [](val) { - qCWarning(lcQpaFonts) - << "Error while trying to query local-fonts API"; - } - }); + qstdweb::Promise::make(permissions, "query", { + .thenFunc = [callback](val status) { + callback(status["state"].as<std::string>() == "granted"); }, - .catchFunc = [](val error) { - qCWarning(lcQpaFonts) - << "Error while requesting local-fonts API permission: " - << QString::fromStdString(error["name"].as<std::string>()); - } - }; + }, makeObject("name", "local-fonts")); +} - // request local fonts permission (currently supported only by Chrome 103+) - qstdweb::Promise::make(permissions, "request", std::move(permissionRequestCallbacks), std::move(requestLocalFontsPermission)); +void queryLocalFonts(std::function<void(const QList<FontData> &)> callback) +{ + emscripten::val window = emscripten::val::global("window"); + qstdweb::Promise::make(window, "queryLocalFonts", { + .thenFunc = [callback](emscripten::val fontArray) { + QList<FontData> fonts; + const int count = fontArray["length"].as<int>(); + fonts.reserve(count); + for (int i = 0; i < count; ++i) + fonts.append(FontData(fontArray.call<emscripten::val>("at", i))); + callback(fonts); + }, + .catchFunc = printError + }); } -void QWasmFontDatabase::populateFamily(const QString &familyName) +void readBlob(val blob, std::function<void(const QByteArray &)> callback) { - val window = val::global("window"); + qstdweb::Promise::make(blob, "arrayBuffer", { + .thenFunc = [callback](emscripten::val fontArrayBuffer) { + QByteArray fontData = qstdweb::Uint8Array(qstdweb::ArrayBuffer(fontArrayBuffer)).copyToQByteArray(); + callback(fontData); + }, + .catchFunc = printError + }); +} - auto queryFontsArgument = val::array(std::vector<val>({ val(familyName.toStdString()) })); - val queryFont = val::object(); - queryFont.set("postscriptNames", std::move(queryFontsArgument)); +void readFont(FontData font, std::function<void(const QByteArray &)> callback) +{ + qstdweb::Promise::make(font.value(), "blob", { + .thenFunc = [callback](val blob) { + readBlob(blob, [callback](const QByteArray &data) { + callback(data); + }); + }, + .catchFunc = printError + }); +} - qstdweb::PromiseCallbacks localFontsQueryCallback { - .thenFunc = [](val status) { - val font = status.call<val>("at", 0); +emscripten::val getLocalFontsConfigProperty(const char *name) { + emscripten::val qt = val::module_property("qt"); + if (qt.isUndefined()) + return emscripten::val(); + emscripten::val localFonts = qt["localFonts"]; + if (localFonts.isUndefined()) + return emscripten::val(); + return localFonts[name]; +}; + +bool getLocalFontsBoolConfigPropertyWithDefault(const char *name, bool defaultValue) { + emscripten::val prop = getLocalFontsConfigProperty(name); + if (prop.isUndefined()) + return defaultValue; + return prop.as<bool>(); +}; + +QString getLocalFontsStringConfigPropertyWithDefault(const char *name, QString defaultValue) { + emscripten::val prop = getLocalFontsConfigProperty(name); + if (prop.isUndefined()) + return defaultValue; + return QString::fromStdString(prop.as<std::string>()); +}; + +QStringList getLocalFontsStringListConfigPropertyWithDefault(const char *name, QStringList defaultValue) { + emscripten::val array = getLocalFontsConfigProperty(name); + if (array.isUndefined()) + return defaultValue; + + QStringList list; + int size = array["length"].as<int>(); + for (int i = 0; i < size; ++i) { + emscripten::val element = array.call<emscripten::val>("at", i); + QString string = QString::fromStdString(element.as<std::string>()); + if (!string.isEmpty()) + list.append(string); + } + return list; +}; - if (font.isUndefined()) - return; +} // namespace - qstdweb::PromiseCallbacks blobQueryCallback { - .thenFunc = [](val status) { - qCDebug(lcQpaFonts) << "onBlobQuerySuccess"; +QWasmFontDatabase::QWasmFontDatabase() +:QFreeTypeFontDatabase() +{ + m_localFontsApiSupported = val::global("window")["queryLocalFonts"].isUndefined() == false; + if (m_localFontsApiSupported) + beginFontDatabaseStartupTask(); +} - qstdweb::PromiseCallbacks arrayBufferCallback { - .thenFunc = [](val status) { - qCDebug(lcQpaFonts) << "onArrayBuffer" ; +QWasmFontDatabase *QWasmFontDatabase::get() +{ + return static_cast<QWasmFontDatabase *>(QWasmIntegration::get()->fontDatabase()); +} - QByteArray fontByteArray = fromUint8Array(status); +// Populates the font database with local fonts. Will make the browser ask +// the user for permission if needed. Does nothing if the Local Font Access API +// is not supported. +void QWasmFontDatabase::populateLocalfonts() +{ + // Decide which font families to populate based on user preferences + QStringList selectedLocalFontFamilies; + bool allFamilies = false; + + switch (m_localFontFamilyLoadSet) { + case NoFontFamilies: + default: + // keep empty selectedLocalFontFamilies + break; + case DefaultFontFamilies: { + const QStringList webSafeFontFamilies = + {"Arial", "Verdana", "Tahoma", "Trebuchet", "Times New Roman", + "Georgia", "Garamond", "Courier New"}; + selectedLocalFontFamilies = webSafeFontFamilies; + } break; + case AllFontFamilies: + allFamilies = true; + break; + } - QFreeTypeFontDatabase::addTTFile(fontByteArray, QByteArray()); + selectedLocalFontFamilies += m_extraLocalFontFamilies; - QWasmFontDatabase::notifyFontsChanged(); - }, - .catchFunc = [](val) { - qCWarning(lcQpaFonts) << "onArrayBufferError"; - } - }; + if (selectedLocalFontFamilies.isEmpty() && !allFamilies) { + endAllFontFileLoading(); + return; + } - qstdweb::Promise::make(status, "arrayBuffer", std::move(arrayBufferCallback)); - }, - .catchFunc = [](val) { - qCWarning(lcQpaFonts) << "onBlobQueryError"; - } - }; + populateLocalFontFamilies(selectedLocalFontFamilies, allFamilies); +} - qstdweb::Promise::make(font, "blob", std::move(blobQueryCallback)); - }, - .catchFunc = [](val) { - qCWarning(lcQpaFonts) << "onLocalFontsQueryError"; +namespace { + QStringList toStringList(emscripten::val array) + { + QStringList list; + int size = array["length"].as<int>(); + for (int i = 0; i < size; ++i) { + emscripten::val element = array.call<emscripten::val>("at", i); + QString string = QString::fromStdString(element.as<std::string>()); + if (!string.isEmpty()) + list.append(string); } + return list; + } +} + +void QWasmFontDatabase::populateLocalFontFamilies(emscripten::val families) +{ + if (!m_localFontsApiSupported) + return; + populateLocalFontFamilies(toStringList(families), false); +} + +void QWasmFontDatabase::populateLocalFontFamilies(const QStringList &fontFamilies, bool allFamilies) +{ + queryLocalFonts([fontFamilies, allFamilies](const QList<FontData> &fonts) { + refFontFileLoading(); + QList<FontData> filteredFonts; + std::copy_if(fonts.begin(), fonts.end(), std::back_inserter(filteredFonts), + [fontFamilies, allFamilies](FontData fontData) { + return allFamilies || fontFamilies.contains(fontData.family()); + }); + + for (const FontData &font: filteredFonts) { + refFontFileLoading(); + readFont(font, [font](const QByteArray &fontData){ + QFreeTypeFontDatabase::registerFontFamily(font.family()); + QFreeTypeFontDatabase::addTTFile(fontData, QByteArray()); + derefFontFileLoading(); + }); + } + derefFontFileLoading(); + }); + +} + +void QWasmFontDatabase::populateFontDatabase() +{ + // Load bundled font file from resources. + const QString fontFileNames[] = { + QStringLiteral(":/fonts/DejaVuSansMono.ttf"), + QStringLiteral(":/fonts/DejaVuSans.ttf"), }; + for (const QString &fontFileName : fontFileNames) { + QFile theFont(fontFileName); + if (!theFont.open(QIODevice::ReadOnly)) + break; + + QFreeTypeFontDatabase::addTTFile(theFont.readAll(), fontFileName.toLatin1()); + } + + // Get config options for controlling local fonts usage + m_queryLocalFontsPermission = getLocalFontsBoolConfigPropertyWithDefault("requestPermission", false); + QString fontFamilyLoadSet = getLocalFontsStringConfigPropertyWithDefault("familiesCollection", "DefaultFontFamilies"); + m_extraLocalFontFamilies = getLocalFontsStringListConfigPropertyWithDefault("extraFamilies", QStringList()); + + if (fontFamilyLoadSet == "NoFontFamilies") { + m_localFontFamilyLoadSet = NoFontFamilies; + } else if (fontFamilyLoadSet == "DefaultFontFamilies") { + m_localFontFamilyLoadSet = DefaultFontFamilies; + } else if (fontFamilyLoadSet == "AllFontFamilies") { + m_localFontFamilyLoadSet = AllFontFamilies; + } else { + m_localFontFamilyLoadSet = NoFontFamilies; + qWarning() << "Unknown fontFamilyLoadSet value" << fontFamilyLoadSet; + } - qstdweb::Promise::make(window, "queryLocalFonts", std::move(localFontsQueryCallback), std::move(queryFont)); + if (!m_localFontsApiSupported) + return; + + // Populate the font database with local fonts. Either try unconditianlly + // if displyaing a fonts permissions dialog at startup is allowed, or else + // only if we already have permission. + if (m_queryLocalFontsPermission) { + populateLocalfonts(); + } else { + checkFontAccessPermitted([this](bool granted) { + if (granted) + populateLocalfonts(); + else + endAllFontFileLoading(); + }); + } } QFontEngine *QWasmFontDatabase::fontEngine(const QFontDef &fontDef, void *handle) { - return QFreeTypeFontDatabase::fontEngine(fontDef, handle); + QFontEngine *fontEngine = QFreeTypeFontDatabase::fontEngine(fontDef, handle); + return fontEngine; } QStringList QWasmFontDatabase::fallbacksForFamily(const QString &family, QFont::Style style, @@ -161,9 +324,9 @@ QStringList QWasmFontDatabase::fallbacksForFamily(const QString &family, QFont:: QStringList fallbacks = QFreeTypeFontDatabase::fallbacksForFamily(family, style, styleHint, script); - // Add the vera.ttf and DejaVuSans.ttf fonts (loaded in populateFontDatabase above) as falback fonts + // Add the DejaVuSans.ttf font (loaded in populateFontDatabase above) as a falback font // to all other fonts (except itself). - static const QString wasmFallbackFonts[] = { "Bitstream Vera Sans", "DejaVu Sans" }; + static const QString wasmFallbackFonts[] = { "DejaVu Sans" }; for (auto wasmFallbackFont : wasmFallbackFonts) { if (family != wasmFallbackFont && !fallbacks.contains(wasmFallbackFont)) fallbacks.append(wasmFallbackFont); @@ -179,13 +342,63 @@ void QWasmFontDatabase::releaseHandle(void *handle) QFont QWasmFontDatabase::defaultFont() const { - return QFont("Bitstream Vera Sans"_L1); + return QFont("DejaVu Sans"_L1); } -void QWasmFontDatabase::notifyFontsChanged() +namespace { + int g_pendingFonts = 0; + bool g_fontStartupTaskCompleted = false; +} + +// Registers font loading as a startup task, which makes Qt delay +// sending onLoaded event until font loading has completed. +void QWasmFontDatabase::beginFontDatabaseStartupTask() +{ + g_fontStartupTaskCompleted = false; + QEventDispatcherWasm::registerStartupTask(); +} + +// Ends the font loading startup task. +void QWasmFontDatabase::endFontDatabaseStartupTask() { - QFontCache::instance()->clear(); - emit qGuiApp->fontDatabaseChanged(); + if (!g_fontStartupTaskCompleted) { + g_fontStartupTaskCompleted = true; + QEventDispatcherWasm::completeStarupTask(); + } } +// Registers that a font file will be loaded. +void QWasmFontDatabase::refFontFileLoading() +{ + g_pendingFonts += 1; +} + +// Registers that one font file has been loaded, and sends notifactions +// when all pending font files have been loaded. +void QWasmFontDatabase::derefFontFileLoading() +{ + if (--g_pendingFonts <= 0) { + QFontCache::instance()->clear(); + emit qGuiApp->fontDatabaseChanged(); + endFontDatabaseStartupTask(); + } +} + +// Unconditionally ends local font loading, for instance if there +// are no fonts to load or if there was an unexpected error. +void QWasmFontDatabase::endAllFontFileLoading() +{ + bool hadPandingfonts = g_pendingFonts > 0; + if (hadPandingfonts) { + // The hadPandingfonts counter might no longer be correct; disable counting + // and send notifications unconditionally. + g_pendingFonts = 0; + QFontCache::instance()->clear(); + emit qGuiApp->fontDatabaseChanged(); + } + + endFontDatabaseStartupTask(); +} + + QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmfontdatabase.h b/src/plugins/platforms/wasm/qwasmfontdatabase.h index 22c550f244..a1c8f1ff48 100644 --- a/src/plugins/platforms/wasm/qwasmfontdatabase.h +++ b/src/plugins/platforms/wasm/qwasmfontdatabase.h @@ -6,13 +6,17 @@ #include <QtGui/private/qfreetypefontdatabase_p.h> +#include <emscripten/val.h> + QT_BEGIN_NAMESPACE class QWasmFontDatabase : public QFreeTypeFontDatabase { public: + QWasmFontDatabase(); + static QWasmFontDatabase *get(); + void populateFontDatabase() override; - void populateFamily(const QString &familyName) override; QFontEngine *fontEngine(const QFontDef &fontDef, void *handle) override; QStringList fallbacksForFamily(const QString &family, QFont::Style style, QFont::StyleHint styleHint, @@ -20,7 +24,26 @@ public: void releaseHandle(void *handle) override; QFont defaultFont() const override; - static void notifyFontsChanged(); + void populateLocalfonts(); + void populateLocalFontFamilies(emscripten::val families); + void populateLocalFontFamilies(const QStringList &famliies, bool allFamilies); + + static void beginFontDatabaseStartupTask(); + static void endFontDatabaseStartupTask(); + static void refFontFileLoading(); + static void derefFontFileLoading(); + static void endAllFontFileLoading(); + +private: + bool m_localFontsApiSupported = false; + bool m_queryLocalFontsPermission = false; + enum FontFamilyLoadSet { + NoFontFamilies, + DefaultFontFamilies, + AllFontFamilies, + }; + FontFamilyLoadSet m_localFontFamilyLoadSet; + QStringList m_extraLocalFontFamilies; }; QT_END_NAMESPACE #endif diff --git a/src/plugins/platforms/wasm/qwasminputcontext.cpp b/src/plugins/platforms/wasm/qwasminputcontext.cpp index 1bf8b5f168..ae72e7b7f9 100644 --- a/src/plugins/platforms/wasm/qwasminputcontext.cpp +++ b/src/plugins/platforms/wasm/qwasminputcontext.cpp @@ -5,9 +5,9 @@ #include "qwasminputcontext.h" #include "qwasmintegration.h" +#include "qwasmplatform.h" #include <QRectF> #include <qpa/qplatforminputcontext.h> -#include "qwasmeventtranslator.h" #include "qwasmscreen.h" #include <qguiapplication.h> #include <qwindow.h> @@ -24,14 +24,13 @@ static void inputCallback(emscripten::val event) if (length <= 0) return; - // use only last character - emscripten::val _incomingCharVal = event["target"]["value"][length - 1]; + emscripten::val _incomingCharVal = event["data"]; if (_incomingCharVal != emscripten::val::undefined() && _incomingCharVal != emscripten::val::null()) { QString str = QString::fromStdString(_incomingCharVal.as<std::string>()); QWasmInputContext *wasmInput = - reinterpret_cast<QWasmInputContext*>(event["target"]["data-context"].as<quintptr>()); - wasmInput->inputStringChanged(str, wasmInput); + reinterpret_cast<QWasmInputContext*>(event["target"]["data-qinputcontext"].as<quintptr>()); + wasmInput->inputStringChanged(str, EMSCRIPTEN_EVENT_KEYDOWN, wasmInput); } // this clears the input string, so backspaces do not send a character // but stops suggestions @@ -39,7 +38,7 @@ static void inputCallback(emscripten::val event) } EMSCRIPTEN_BINDINGS(clipboard_module) { - function("qt_InputContextCallback", &inputCallback); + function("qtInputContextCallback", &inputCallback); } QWasmInputContext::QWasmInputContext() @@ -50,28 +49,24 @@ QWasmInputContext::QWasmInputContext() m_inputElement.set("style", "position:absolute;left:-1000px;top:-1000px"); // offscreen m_inputElement.set("contenteditable","true"); - if (platform() == Platform::Android) { - emscripten::val body = document["body"]; - body.call<void>("appendChild", m_inputElement); - - m_inputElement.call<void>("addEventListener", std::string("input"), - emscripten::val::module_property("qt_InputContextCallback"), - emscripten::val(false)); - m_inputElement.set("data-context", - emscripten::val(quintptr(reinterpret_cast<void *>(this)))); - - // android sends Enter through target window, let's just handle this here - emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, (void *)this, 1, - &androidKeyboardCallback); - - } - if (platform() == Platform::MacOS || platform() == Platform::iPhone) - { + if (platform() == Platform::MacOS || platform() == Platform::iOS) { auto callback = [=](emscripten::val) { m_inputElement["parentElement"].call<void>("removeChild", m_inputElement); inputPanelIsOpen = false; }; m_blurEventHandler.reset(new EventCallback(m_inputElement, "blur", callback)); + + } else { + + const std::string inputType = platform() == Platform::Windows ? "textInput" : "input"; + + document.call<void>("addEventListener", inputType, + emscripten::val::module_property("qtInputContextCallback"), + emscripten::val(false)); + m_inputElement.set("data-qinputcontext", + emscripten::val(quintptr(reinterpret_cast<void *>(this)))); + emscripten::val body = document["body"]; + body.call<void>("appendChild", m_inputElement); } QObject::connect(qGuiApp, &QGuiApplication::focusWindowChanged, this, @@ -80,7 +75,7 @@ QWasmInputContext::QWasmInputContext() QWasmInputContext::~QWasmInputContext() { - if (platform() == Platform::Android) + if (platform() == Platform::Android || platform() == Platform::Windows) emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, 0, NULL); } @@ -89,14 +84,11 @@ void QWasmInputContext::focusWindowChanged(QWindow *focusWindow) m_focusWindow = focusWindow; } -emscripten::val QWasmInputContext::focusScreenElement() +emscripten::val QWasmInputContext::inputHandlerElementForFocusedWindow() { if (!m_focusWindow) return emscripten::val::undefined(); - QScreen *screen = m_focusWindow->screen(); - if (!screen) - return emscripten::val::undefined(); - return QWasmScreen::get(screen)->element(); + return static_cast<QWasmWindow *>(m_focusWindow->handle())->inputHandlerElement(); } void QWasmInputContext::update(Qt::InputMethodQueries queries) @@ -107,8 +99,10 @@ void QWasmInputContext::update(Qt::InputMethodQueries queries) void QWasmInputContext::showInputPanel() { if (platform() == Platform::Windows - && inputPanelIsOpen) // call this only once for win32 - return; + && !inputPanelIsOpen) { // call this only once for win32 + m_inputElement.call<void>("focus"); + return; + } // this is called each time the keyboard is touched // Add the input element as a child of the screen for the @@ -119,12 +113,12 @@ void QWasmInputContext::showInputPanel() // screen element. if (platform() == Platform::MacOS // keep for compatibility - || platform() == Platform::iPhone + || platform() == Platform::iOS || platform() == Platform::Windows) { - emscripten::val screenElement = focusScreenElement(); - if (screenElement.isUndefined()) + emscripten::val inputWrapper = inputHandlerElementForFocusedWindow(); + if (inputWrapper.isUndefined()) return; - screenElement.call<void>("appendChild", m_inputElement); + inputWrapper.call<void>("appendChild", m_inputElement); } m_inputElement.call<void>("focus"); @@ -139,29 +133,29 @@ void QWasmInputContext::hideInputPanel() inputPanelIsOpen = false; } -void QWasmInputContext::inputStringChanged(QString &inputString, QWasmInputContext *context) +void QWasmInputContext::inputStringChanged(QString &inputString, int eventType, QWasmInputContext *context) { Q_UNUSED(context) QKeySequence keys = QKeySequence::fromString(inputString); - // synthesize this keyevent as android is not normal - QWindowSystemInterface::handleKeyEvent<QWindowSystemInterface::SynchronousDelivery>( - 0, QEvent::KeyPress,keys[0].key(), keys[0].keyboardModifiers(), inputString); -} + Qt::Key thisKey = keys[0].key(); -int QWasmInputContext::androidKeyboardCallback(int eventType, - const EmscriptenKeyboardEvent *keyEvent, - void *userData) -{ - // we get Enter, Backspace and function keys via emscripten on target window - Q_UNUSED(eventType) - QString strKey(keyEvent->key); - if (strKey == "Unidentified" || strKey == "Process") - return false; - - QWasmInputContext *wasmInput = reinterpret_cast<QWasmInputContext*>(userData); - wasmInput->inputStringChanged(strKey, wasmInput); + // synthesize this keyevent as android is not normal + if (inputString.size() > 2 && (thisKey < Qt::Key_F35 + || thisKey > Qt::Key_Back)) { + inputString.clear(); + } + if (inputString == QStringLiteral("Escape")) { + thisKey = Qt::Key_Escape; + inputString.clear(); + } else if (thisKey == Qt::Key(0)) { + thisKey = Qt::Key_Return; + } - return true; + QWindowSystemInterface::handleKeyEvent( + 0, eventType == EMSCRIPTEN_EVENT_KEYDOWN ? QEvent::KeyPress : QEvent::KeyRelease, + thisKey, keys[0].keyboardModifiers(), + eventType == EMSCRIPTEN_EVENT_KEYDOWN ? inputString : QStringLiteral("")); } + QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasminputcontext.h b/src/plugins/platforms/wasm/qwasminputcontext.h index 0886ae8d84..10dd1a0950 100644 --- a/src/plugins/platforms/wasm/qwasminputcontext.h +++ b/src/plugins/platforms/wasm/qwasminputcontext.h @@ -29,19 +29,17 @@ public: bool isValid() const override { return true; } void focusWindowChanged(QWindow *focusWindow); - void inputStringChanged(QString &, QWasmInputContext *context); + void inputStringChanged(QString &, int eventType, QWasmInputContext *context); + emscripten::val m_inputElement = emscripten::val::null(); private: - emscripten::val focusScreenElement(); + emscripten::val inputHandlerElementForFocusedWindow(); bool m_inputPanelVisible = false; QPointer<QWindow> m_focusWindow; - emscripten::val m_inputElement = emscripten::val::null(); std::unique_ptr<qstdweb::EventCallback> m_blurEventHandler; std::unique_ptr<qstdweb::EventCallback> m_inputEventHandler; - static int androidKeyboardCallback(int eventType, - const EmscriptenKeyboardEvent *keyEvent, void *userData); bool inputPanelIsOpen = false; }; diff --git a/src/plugins/platforms/wasm/qwasmintegration.cpp b/src/plugins/platforms/wasm/qwasmintegration.cpp index 4c6f1b14c8..f5cc3e2eee 100644 --- a/src/plugins/platforms/wasm/qwasmintegration.cpp +++ b/src/plugins/platforms/wasm/qwasmintegration.cpp @@ -2,7 +2,6 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmintegration.h" -#include "qwasmeventtranslator.h" #include "qwasmeventdispatcher.h" #include "qwasmcompositor.h" #include "qwasmopenglcontext.h" @@ -11,19 +10,18 @@ #include "qwasmaccessibility.h" #include "qwasmservices.h" #include "qwasmoffscreensurface.h" -#include "qwasmstring.h" - +#include "qwasmplatform.h" #include "qwasmwindow.h" #include "qwasmbackingstore.h" #include "qwasmfontdatabase.h" -#if defined(Q_OS_UNIX) -#include <QtGui/private/qgenericunixeventdispatcher_p.h> -#endif +#include "qwasmdrag.h" + #include <qpa/qplatformwindow.h> #include <QtGui/qscreen.h> #include <qpa/qwindowsysteminterface.h> #include <QtCore/qcoreapplication.h> #include <qpa/qplatforminputcontextfactory_p.h> +#include <qpa/qwindowsysteminterface_p.h> #include <emscripten/bind.h> #include <emscripten/val.h> @@ -34,18 +32,25 @@ QT_BEGIN_NAMESPACE +extern void qt_set_sequence_auto_mnemonic(bool); + using namespace emscripten; using namespace Qt::StringLiterals; +static void setContainerElements(emscripten::val elementArray) +{ + QWasmIntegration::get()->setContainerElements(elementArray); +} + static void addContainerElement(emscripten::val element) { - QWasmIntegration::get()->addScreen(element); + QWasmIntegration::get()->addContainerElement(element); } static void removeContainerElement(emscripten::val element) { - QWasmIntegration::get()->removeScreen(element); + QWasmIntegration::get()->removeContainerElement(element); } static void resizeContainerElement(emscripten::val element) @@ -64,31 +69,45 @@ static void resizeAllScreens(emscripten::val event) QWasmIntegration::get()->resizeAllScreens(); } +static void loadLocalFontFamilies(emscripten::val event) +{ + QWasmIntegration::get()->loadLocalFontFamilies(event); +} + EMSCRIPTEN_BINDINGS(qtQWasmIntegraton) { + function("qtSetContainerElements", &setContainerElements); function("qtAddContainerElement", &addContainerElement); function("qtRemoveContainerElement", &removeContainerElement); function("qtResizeContainerElement", &resizeContainerElement); function("qtUpdateDpi", &qtUpdateDpi); function("qtResizeAllScreens", &resizeAllScreens); + function("qtLoadLocalFontFamilies", &loadLocalFontFamilies); } QWasmIntegration *QWasmIntegration::s_instance; QWasmIntegration::QWasmIntegration() - : m_fontDb(nullptr), - m_desktopServices(nullptr), - m_clipboard(new QWasmClipboard), - m_accessibility(new QWasmAccessibility) + : m_fontDb(nullptr) + , m_desktopServices(nullptr) + , m_clipboard(new QWasmClipboard) +#if QT_CONFIG(accessibility) + , m_accessibility(new QWasmAccessibility) +#endif { s_instance = this; + if (platform() == Platform::MacOS) + qt_set_sequence_auto_mnemonic(false); + touchPoints = emscripten::val::global("navigator")["maxTouchPoints"].as<int>(); + QWindowSystemInterfacePrivate::TabletEvent::setPlatformSynthesizesMouse(false); // Create screens for container elements. Each container element will ultimately become a // div element. Qt historically supported supplying canvas for screen elements - these elements // will be transformed into divs and warnings about deprecation will be printed. See // QWasmScreen ctor. + emscripten::val filtered = emscripten::val::array(); emscripten::val qtContainerElements = val::module_property("qtContainerElements"); if (qtContainerElements.isArray()) { for (int i = 0; i < qtContainerElements["length"].as<int>(); ++i) { @@ -96,13 +115,14 @@ QWasmIntegration::QWasmIntegration() if (element.isNull() || element.isUndefined()) qWarning() << "Skipping null or undefined element in qtContainerElements"; else - addScreen(element); + filtered.call<void>("push", element); } } else { // No screens, which may or may not be intended qWarning() << "The qtContainerElements module property was not set or is invalid. " "Proceeding with no screens."; } + setContainerElements(filtered); // install browser window resize handler emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_TRUE, @@ -122,7 +142,7 @@ QWasmIntegration::QWasmIntegration() visualViewport.call<void>("addEventListener", val("resize"), val::module_property("qtResizeAllScreens")); } - m_drag = new QWasmDrag(); + m_drag = std::make_unique<QWasmDrag>(); } QWasmIntegration::~QWasmIntegration() @@ -139,11 +159,12 @@ QWasmIntegration::~QWasmIntegration() delete m_desktopServices; if (m_platformInputContext) delete m_platformInputContext; - delete m_drag; +#if QT_CONFIG(accessibility) delete m_accessibility; +#endif for (const auto &elementAndScreen : m_screens) - elementAndScreen.second->deleteScreen(); + elementAndScreen.wasmScreen->deleteScreen(); m_screens.clear(); @@ -166,8 +187,10 @@ bool QWasmIntegration::hasCapability(QPlatformIntegration::Capability cap) const QPlatformWindow *QWasmIntegration::createPlatformWindow(QWindow *window) const { - QWasmCompositor *compositor = QWasmScreen::get(window->screen())->compositor(); - return new QWasmWindow(window, compositor, m_backingStores.value(window)); + auto *wasmScreen = QWasmScreen::get(window->screen()); + QWasmCompositor *compositor = wasmScreen->compositor(); + return new QWasmWindow(window, wasmScreen->deadKeySupport(), compositor, + m_backingStores.value(window)); } QPlatformBackingStore *QWasmIntegration::createPlatformBackingStore(QWindow *window) const @@ -183,21 +206,31 @@ void QWasmIntegration::removeBackingStore(QWindow* window) m_backingStores.remove(window); } +void QWasmIntegration::releaseRequesetUpdateHold() +{ + if (QWasmCompositor::releaseRequestUpdateHold()) + { + for (const auto &elementAndScreen : m_screens) { + elementAndScreen.wasmScreen->compositor()->requestUpdate(); + } + } +} + #ifndef QT_NO_OPENGL QPlatformOpenGLContext *QWasmIntegration::createPlatformOpenGLContext(QOpenGLContext *context) const { - return new QWasmOpenGLContext(context->format()); + return new QWasmOpenGLContext(context); } #endif void QWasmIntegration::initialize() { - if (qgetenv("QT_IM_MODULE").isEmpty() && touchPoints < 1) + auto icStrs = QPlatformInputContextFactory::requested(); + if (icStrs.isEmpty() && touchPoints < 1) return; - QString icStr = QPlatformInputContextFactory::requested(); - if (!icStr.isNull()) - m_inputContext.reset(QPlatformInputContextFactory::create(icStr)); + if (!icStrs.isEmpty()) + m_inputContext.reset(QPlatformInputContextFactory::create(icStrs)); else m_inputContext.reset(new QWasmInputContext()); } @@ -227,10 +260,14 @@ QAbstractEventDispatcher *QWasmIntegration::createEventDispatcher() const QVariant QWasmIntegration::styleHint(QPlatformIntegration::StyleHint hint) const { - if (hint == ShowIsFullScreen) + switch (hint) { + case ShowIsFullScreen: return true; - - return QPlatformIntegration::styleHint(hint); + case UnderlineShortcut: + return platform() != Platform::MacOS; + default: + return QPlatformIntegration::styleHint(hint); + } } Qt::WindowState QWasmIntegration::defaultWindowState(Qt::WindowFlags flags) const @@ -273,36 +310,93 @@ QPlatformAccessibility *QWasmIntegration::accessibility() const } #endif +void QWasmIntegration::setContainerElements(emscripten::val elementArray) +{ + const auto *primaryScreenBefore = m_screens.isEmpty() ? nullptr : m_screens[0].wasmScreen; + QList<ScreenMapping> newScreens; + + QList<QWasmScreen *> screensToDelete; + std::transform(m_screens.begin(), m_screens.end(), std::back_inserter(screensToDelete), + [](const ScreenMapping &mapping) { return mapping.wasmScreen; }); + + for (int i = 0; i < elementArray["length"].as<int>(); ++i) { + const auto element = elementArray[i]; + const auto it = std::find_if( + m_screens.begin(), m_screens.end(), + [&element](const ScreenMapping &screen) { return screen.emscriptenVal == element; }); + QWasmScreen *screen; + if (it != m_screens.end()) { + screen = it->wasmScreen; + screensToDelete.erase(std::remove_if(screensToDelete.begin(), screensToDelete.end(), + [screen](const QWasmScreen *removedScreen) { + return removedScreen == screen; + }), + screensToDelete.end()); + } else { + screen = new QWasmScreen(element); + QWindowSystemInterface::handleScreenAdded(screen); + } + newScreens.push_back({element, screen}); + } + + std::for_each(screensToDelete.begin(), screensToDelete.end(), + [](QWasmScreen *removed) { removed->deleteScreen(); }); + + m_screens = newScreens; + auto *primaryScreenAfter = m_screens.isEmpty() ? nullptr : m_screens[0].wasmScreen; + if (primaryScreenAfter && primaryScreenAfter != primaryScreenBefore) + QWindowSystemInterface::handlePrimaryScreenChanged(primaryScreenAfter); +} -void QWasmIntegration::addScreen(const emscripten::val &element) +void QWasmIntegration::addContainerElement(emscripten::val element) { + Q_ASSERT_X(m_screens.end() + == std::find_if(m_screens.begin(), m_screens.end(), + [&element](const ScreenMapping &screen) { + return screen.emscriptenVal == element; + }), + Q_FUNC_INFO, "Double-add of an element"); + QWasmScreen *screen = new QWasmScreen(element); - m_screens.append(qMakePair(element, screen)); - m_clipboard->installEventHandlers(element); QWindowSystemInterface::handleScreenAdded(screen); + m_screens.push_back({element, screen}); } -void QWasmIntegration::removeScreen(const emscripten::val &element) +void QWasmIntegration::removeContainerElement(emscripten::val element) { - auto it = std::find_if(m_screens.begin(), m_screens.end(), - [&] (const QPair<emscripten::val, QWasmScreen *> &candidate) { return candidate.first.equals(element); }); + const auto *primaryScreenBefore = m_screens.isEmpty() ? nullptr : m_screens[0].wasmScreen; + + const auto it = + std::find_if(m_screens.begin(), m_screens.end(), + [&element](const ScreenMapping &screen) { return screen.emscriptenVal == element; }); if (it == m_screens.end()) { - qWarning() << "Attempting to remove non-existing screen for element" << QWasmString::toQString(element["id"]);; + qWarning() << "Attempt to remove a nonexistent screen."; return; } - it->second->deleteScreen(); - m_screens.erase(it); + + QWasmScreen *removedScreen = it->wasmScreen; + removedScreen->deleteScreen(); + + m_screens.erase(std::remove_if(m_screens.begin(), m_screens.end(), + [removedScreen](const ScreenMapping &mapping) { + return removedScreen == mapping.wasmScreen; + }), + m_screens.end()); + auto *primaryScreenAfter = m_screens.isEmpty() ? nullptr : m_screens[0].wasmScreen; + if (primaryScreenAfter && primaryScreenAfter != primaryScreenBefore) + QWindowSystemInterface::handlePrimaryScreenChanged(primaryScreenAfter); } void QWasmIntegration::resizeScreen(const emscripten::val &element) { auto it = std::find_if(m_screens.begin(), m_screens.end(), - [&] (const QPair<emscripten::val, QWasmScreen *> &candidate) { return candidate.first.equals(element); }); + [&] (const ScreenMapping &candidate) { return candidate.emscriptenVal.equals(element); }); if (it == m_screens.end()) { - qWarning() << "Attempting to resize non-existing screen for element" << QWasmString::toQString(element["id"]);; + qWarning() << "Attempting to resize non-existing screen for element" + << QString::fromEcmaString(element["id"]); return; } - it->second->updateQScreenAndCanvasRenderSize(); + it->wasmScreen->updateQScreenAndCanvasRenderSize(); } void QWasmIntegration::updateDpi() @@ -312,13 +406,18 @@ void QWasmIntegration::updateDpi() return; qreal dpiValue = dpi.as<qreal>(); for (const auto &elementAndScreen : m_screens) - QWindowSystemInterface::handleScreenLogicalDotsPerInchChange(elementAndScreen.second->screen(), dpiValue, dpiValue); + QWindowSystemInterface::handleScreenLogicalDotsPerInchChange(elementAndScreen.wasmScreen->screen(), dpiValue, dpiValue); } void QWasmIntegration::resizeAllScreens() { for (const auto &elementAndScreen : m_screens) - elementAndScreen.second->updateQScreenAndCanvasRenderSize(); + elementAndScreen.wasmScreen->updateQScreenAndCanvasRenderSize(); +} + +void QWasmIntegration::loadLocalFontFamilies(emscripten::val families) +{ + m_fontDb->populateLocalFontFamilies(families); } quint64 QWasmIntegration::getTimestamp() @@ -329,7 +428,7 @@ quint64 QWasmIntegration::getTimestamp() #if QT_CONFIG(draganddrop) QPlatformDrag *QWasmIntegration::drag() const { - return m_drag; + return m_drag.get(); } #endif // QT_CONFIG(draganddrop) diff --git a/src/plugins/platforms/wasm/qwasmintegration.h b/src/plugins/platforms/wasm/qwasmintegration.h index 76296ff1a7..870bd0d16b 100644 --- a/src/plugins/platforms/wasm/qwasmintegration.h +++ b/src/plugins/platforms/wasm/qwasmintegration.h @@ -6,23 +6,20 @@ #include "qwasmwindow.h" +#include "qwasminputcontext.h" + #include <qpa/qplatformintegration.h> #include <qpa/qplatformscreen.h> #include <qpa/qplatforminputcontext.h> #include <QtCore/qhash.h> +#include <private/qstdweb_p.h> + #include <emscripten.h> #include <emscripten/html5.h> #include <emscripten/val.h> -#include "qwasminputcontext.h" -#include <private/qstdweb_p.h> - -#if QT_CONFIG(draganddrop) -#include "qwasmdrag.h" -#endif - QT_BEGIN_NAMESPACE class QWasmEventTranslator; @@ -35,6 +32,7 @@ class QWasmBackingStore; class QWasmClipboard; class QWasmAccessibility; class QWasmServices; +class QWasmDrag; class QWasmIntegration : public QObject, public QPlatformIntegration { @@ -72,21 +70,29 @@ public: QWasmInputContext *getWasmInputContext() { return m_platformInputContext; } static QWasmIntegration *get() { return s_instance; } - void addScreen(const emscripten::val &canvas); - void removeScreen(const emscripten::val &canvas); + void setContainerElements(emscripten::val elementArray); + void addContainerElement(emscripten::val elementArray); + void removeContainerElement(emscripten::val elementArray); void resizeScreen(const emscripten::val &canvas); - void resizeAllScreens(); void updateDpi(); + void resizeAllScreens(); + void loadLocalFontFamilies(emscripten::val families); void removeBackingStore(QWindow* window); + void releaseRequesetUpdateHold(); static quint64 getTimestamp(); int touchPoints; private: + struct ScreenMapping { + emscripten::val emscriptenVal; + QWasmScreen *wasmScreen; + }; + mutable QWasmFontDatabase *m_fontDb; mutable QWasmServices *m_desktopServices; mutable QHash<QWindow *, QWasmBackingStore *> m_backingStores; - QList<QPair<emscripten::val, QWasmScreen *>> m_screens; + QList<ScreenMapping> m_screens; mutable QWasmClipboard *m_clipboard; mutable QWasmAccessibility *m_accessibility; @@ -97,7 +103,7 @@ private: mutable QWasmInputContext *m_platformInputContext = nullptr; #if QT_CONFIG(draganddrop) - QWasmDrag *m_drag; + std::unique_ptr<QWasmDrag> m_drag; #endif }; diff --git a/src/plugins/platforms/wasm/qwasmkeytranslator.cpp b/src/plugins/platforms/wasm/qwasmkeytranslator.cpp new file mode 100644 index 0000000000..8f5240d2d0 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmkeytranslator.cpp @@ -0,0 +1,295 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmkeytranslator.h" +#include "qwasmevent.h" + +#include <QtCore/private/qmakearray_p.h> +#include <QtCore/qglobal.h> +#include <QtCore/qobject.h> + +#include <algorithm> + +QT_BEGIN_NAMESPACE + +namespace { +struct WebKb2QtData +{ + static constexpr char StringTerminator = '\0'; + + const char *web; + unsigned int qt; + + constexpr bool operator<=(const WebKb2QtData &that) const noexcept + { + return !(strcmp(that) > 0); + } + + bool operator<(const WebKb2QtData &that) const noexcept { return ::strcmp(web, that.web) < 0; } + + constexpr bool operator==(const WebKb2QtData &that) const noexcept { return strcmp(that) == 0; } + + constexpr int strcmp(const WebKb2QtData &that, const int i = 0) const + { + return web[i] == StringTerminator && that.web[i] == StringTerminator ? 0 + : web[i] == StringTerminator ? -1 + : that.web[i] == StringTerminator ? 1 + : web[i] < that.web[i] ? -1 + : web[i] > that.web[i] ? 1 + : strcmp(that, i + 1); + } +}; + +template<unsigned int Qt, char... WebChar> +struct Web2Qt +{ + static constexpr const char storage[sizeof...(WebChar) + 1] = { WebChar..., '\0' }; + using Type = WebKb2QtData; + static constexpr Type data() noexcept { return Type{ storage, Qt }; } +}; + +template<unsigned int Qt, char... WebChar> +constexpr char Web2Qt<Qt, WebChar...>::storage[]; + +static constexpr const auto WebToQtKeyCodeMappings = qMakeArray( + QSortedData<Web2Qt<Qt::Key_Alt, 'A', 'l', 't', 'L', 'e', 'f', 't'>, + Web2Qt<Qt::Key_Alt, 'A', 'l', 't'>, + Web2Qt<Qt::Key_AltGr, 'A', 'l', 't', 'R', 'i', 'g', 'h', 't'>, + Web2Qt<Qt::Key_Apostrophe, 'Q', 'u', 'o', 't', 'e'>, + Web2Qt<Qt::Key_Backspace, 'B', 'a', 'c', 'k', 's', 'p', 'a', 'c', 'e'>, + Web2Qt<Qt::Key_CapsLock, 'C', 'a', 'p', 's', 'L', 'o', 'c', 'k'>, + Web2Qt<Qt::Key_Control, 'C', 'o', 'n', 't', 'r', 'o', 'l'>, + Web2Qt<Qt::Key_Delete, 'D', 'e', 'l', 'e', 't', 'e'>, + Web2Qt<Qt::Key_Down, 'A', 'r', 'r', 'o', 'w', 'D', 'o', 'w', 'n'>, + Web2Qt<Qt::Key_Escape, 'E', 's', 'c', 'a', 'p', 'e'>, + Web2Qt<Qt::Key_F1, 'F', '1'>, Web2Qt<Qt::Key_F2, 'F', '2'>, + Web2Qt<Qt::Key_F11, 'F', '1', '1'>, Web2Qt<Qt::Key_F12, 'F', '1', '2'>, + Web2Qt<Qt::Key_F13, 'F', '1', '3'>, Web2Qt<Qt::Key_F14, 'F', '1', '4'>, + Web2Qt<Qt::Key_F15, 'F', '1', '5'>, Web2Qt<Qt::Key_F16, 'F', '1', '6'>, + Web2Qt<Qt::Key_F17, 'F', '1', '7'>, Web2Qt<Qt::Key_F18, 'F', '1', '8'>, + Web2Qt<Qt::Key_F19, 'F', '1', '9'>, Web2Qt<Qt::Key_F20, 'F', '2', '0'>, + Web2Qt<Qt::Key_F21, 'F', '2', '1'>, Web2Qt<Qt::Key_F22, 'F', '2', '2'>, + Web2Qt<Qt::Key_F23, 'F', '2', '3'>, + Web2Qt<Qt::Key_F3, 'F', '3'>, Web2Qt<Qt::Key_F4, 'F', '4'>, + Web2Qt<Qt::Key_F5, 'F', '5'>, Web2Qt<Qt::Key_F6, 'F', '6'>, + Web2Qt<Qt::Key_F7, 'F', '7'>, Web2Qt<Qt::Key_F8, 'F', '8'>, + Web2Qt<Qt::Key_F9, 'F', '9'>, Web2Qt<Qt::Key_F10, 'F', '1', '0'>, + Web2Qt<Qt::Key_Help, 'H', 'e', 'l', 'p'>, + Web2Qt<Qt::Key_Home, 'H', 'o', 'm', 'e'>, Web2Qt<Qt::Key_End, 'E', 'n', 'd'>, + Web2Qt<Qt::Key_Insert, 'I', 'n', 's', 'e', 'r', 't'>, + Web2Qt<Qt::Key_Left, 'A', 'r', 'r', 'o', 'w', 'L', 'e', 'f', 't'>, + Web2Qt<Qt::Key_Meta, 'M', 'e', 't', 'a'>, Web2Qt<Qt::Key_Meta, 'O', 'S'>, + Web2Qt<Qt::Key_Menu, 'C', 'o', 'n', 't', 'e', 'x', 't', 'M', 'e', 'n', 'u'>, + Web2Qt<Qt::Key_NumLock, 'N', 'u', 'm', 'L', 'o', 'c', 'k'>, + Web2Qt<Qt::Key_PageDown, 'P', 'a', 'g', 'e', 'D', 'o', 'w', 'n'>, + Web2Qt<Qt::Key_PageUp, 'P', 'a', 'g', 'e', 'U', 'p'>, + Web2Qt<Qt::Key_Paste, 'P', 'a', 's', 't', 'e'>, + Web2Qt<Qt::Key_Pause, 'C', 'l', 'e', 'a', 'r'>, + Web2Qt<Qt::Key_Pause, 'P', 'a', 'u', 's', 'e'>, + Web2Qt<Qt::Key_QuoteLeft, 'B', 'a', 'c', 'k', 'q', 'u', 'o', 't', 'e'>, + Web2Qt<Qt::Key_QuoteLeft, 'I', 'n', 't', 'l', 'B', 'a', 'c', 'k', 's', 'l', 'a', 's', 'h'>, + Web2Qt<Qt::Key_Return, 'E', 'n', 't', 'e', 'r'>, + Web2Qt<Qt::Key_Right, 'A', 'r', 'r', 'o', 'w', 'R', 'i', 'g', 'h', 't'>, + Web2Qt<Qt::Key_ScrollLock, 'S', 'c', 'r', 'o', 'l', 'l', 'L', 'o', 'c', 'k'>, + Web2Qt<Qt::Key_Shift, 'S', 'h', 'i', 'f', 't'>, + Web2Qt<Qt::Key_Tab, 'T', 'a', 'b'>, + Web2Qt<Qt::Key_Up, 'A', 'r', 'r', 'o', 'w', 'U', 'p'>, + Web2Qt<Qt::Key_yen, 'I', 'n', 't', 'l', 'Y', 'e', 'n'>>::Data{}); + +static constexpr const auto DiacriticalCharsKeyToTextLowercase = qMakeArray( + QSortedData< + Web2Qt<Qt::Key_Aacute, '\xc3', '\xa1'>, + Web2Qt<Qt::Key_Acircumflex, '\xc3', '\xa2'>, + Web2Qt<Qt::Key_Adiaeresis, '\xc3', '\xa4'>, + Web2Qt<Qt::Key_AE, '\xc3', '\xa6'>, + Web2Qt<Qt::Key_Agrave, '\xc3', '\xa0'>, + Web2Qt<Qt::Key_Aring, '\xc3', '\xa5'>, + Web2Qt<Qt::Key_Atilde, '\xc3', '\xa3'>, + Web2Qt<Qt::Key_Ccedilla, '\xc3', '\xa7'>, + Web2Qt<Qt::Key_Eacute, '\xc3', '\xa9'>, + Web2Qt<Qt::Key_Ecircumflex, '\xc3', '\xaa'>, + Web2Qt<Qt::Key_Ediaeresis, '\xc3', '\xab'>, + Web2Qt<Qt::Key_Egrave, '\xc3', '\xa8'>, + Web2Qt<Qt::Key_Iacute, '\xc3', '\xad'>, + Web2Qt<Qt::Key_Icircumflex, '\xc3', '\xae'>, + Web2Qt<Qt::Key_Idiaeresis, '\xc3', '\xaf'>, + Web2Qt<Qt::Key_Igrave, '\xc3', '\xac'>, + Web2Qt<Qt::Key_Ntilde, '\xc3', '\xb1'>, + Web2Qt<Qt::Key_Oacute, '\xc3', '\xb3'>, + Web2Qt<Qt::Key_Ocircumflex, '\xc3', '\xb4'>, + Web2Qt<Qt::Key_Odiaeresis, '\xc3', '\xb6'>, + Web2Qt<Qt::Key_Ograve, '\xc3', '\xb2'>, + Web2Qt<Qt::Key_Ooblique, '\xc3', '\xb8'>, + Web2Qt<Qt::Key_Otilde, '\xc3', '\xb5'>, + Web2Qt<Qt::Key_Uacute, '\xc3', '\xba'>, + Web2Qt<Qt::Key_Ucircumflex, '\xc3', '\xbb'>, + Web2Qt<Qt::Key_Udiaeresis, '\xc3', '\xbc'>, + Web2Qt<Qt::Key_Ugrave, '\xc3', '\xb9'>, + Web2Qt<Qt::Key_Yacute, '\xc3', '\xbd'>, + Web2Qt<Qt::Key_ydiaeresis, '\xc3', '\xbf'>>::Data{}); + +static constexpr const auto DiacriticalCharsKeyToTextUppercase = qMakeArray( + QSortedData< + Web2Qt<Qt::Key_Aacute, '\xc3', '\x81'>, + Web2Qt<Qt::Key_Acircumflex, '\xc3', '\x82'>, + Web2Qt<Qt::Key_Adiaeresis, '\xc3', '\x84'>, + Web2Qt<Qt::Key_AE, '\xc3', '\x86'>, + Web2Qt<Qt::Key_Agrave, '\xc3', '\x80'>, + Web2Qt<Qt::Key_Aring, '\xc3', '\x85'>, + Web2Qt<Qt::Key_Atilde, '\xc3', '\x83'>, + Web2Qt<Qt::Key_Ccedilla, '\xc3', '\x87'>, + Web2Qt<Qt::Key_Eacute, '\xc3', '\x89'>, + Web2Qt<Qt::Key_Ecircumflex, '\xc3', '\x8a'>, + Web2Qt<Qt::Key_Ediaeresis, '\xc3', '\x8b'>, + Web2Qt<Qt::Key_Egrave, '\xc3', '\x88'>, + Web2Qt<Qt::Key_Iacute, '\xc3', '\x8d'>, + Web2Qt<Qt::Key_Icircumflex, '\xc3', '\x8e'>, + Web2Qt<Qt::Key_Idiaeresis, '\xc3', '\x8f'>, + Web2Qt<Qt::Key_Igrave, '\xc3', '\x8c'>, + Web2Qt<Qt::Key_Ntilde, '\xc3', '\x91'>, + Web2Qt<Qt::Key_Oacute, '\xc3', '\x93'>, + Web2Qt<Qt::Key_Ocircumflex, '\xc3', '\x94'>, + Web2Qt<Qt::Key_Odiaeresis, '\xc3', '\x96'>, + Web2Qt<Qt::Key_Ograve, '\xc3', '\x92'>, + Web2Qt<Qt::Key_Ooblique, '\xc3', '\x98'>, + Web2Qt<Qt::Key_Otilde, '\xc3', '\x95'>, + Web2Qt<Qt::Key_Uacute, '\xc3', '\x9a'>, + Web2Qt<Qt::Key_Ucircumflex, '\xc3', '\x9b'>, + Web2Qt<Qt::Key_Udiaeresis, '\xc3', '\x9c'>, + Web2Qt<Qt::Key_Ugrave, '\xc3', '\x99'>, + Web2Qt<Qt::Key_Yacute, '\xc3', '\x9d'>, + Web2Qt<Qt::Key_ydiaeresis, '\xc5', '\xb8'>>::Data{}); + +static_assert(DiacriticalCharsKeyToTextLowercase.size() + == DiacriticalCharsKeyToTextUppercase.size(), + "Add the new key to both arrays"); + +struct KeyMapping +{ + Qt::Key from, to; +}; + +constexpr KeyMapping tildeKeyTable[] = { + // ~ + { Qt::Key_A, Qt::Key_Atilde }, + { Qt::Key_N, Qt::Key_Ntilde }, + { Qt::Key_O, Qt::Key_Otilde }, +}; +constexpr KeyMapping graveKeyTable[] = { + // ` + { Qt::Key_A, Qt::Key_Agrave }, { Qt::Key_E, Qt::Key_Egrave }, { Qt::Key_I, Qt::Key_Igrave }, + { Qt::Key_O, Qt::Key_Ograve }, { Qt::Key_U, Qt::Key_Ugrave }, +}; +constexpr KeyMapping acuteKeyTable[] = { + // ' + { Qt::Key_A, Qt::Key_Aacute }, { Qt::Key_E, Qt::Key_Eacute }, { Qt::Key_I, Qt::Key_Iacute }, + { Qt::Key_O, Qt::Key_Oacute }, { Qt::Key_U, Qt::Key_Uacute }, { Qt::Key_Y, Qt::Key_Yacute }, +}; +constexpr KeyMapping diaeresisKeyTable[] = { + // umlaut ¨ + { Qt::Key_A, Qt::Key_Adiaeresis }, { Qt::Key_E, Qt::Key_Ediaeresis }, + { Qt::Key_I, Qt::Key_Idiaeresis }, { Qt::Key_O, Qt::Key_Odiaeresis }, + { Qt::Key_U, Qt::Key_Udiaeresis }, { Qt::Key_Y, Qt::Key_ydiaeresis }, +}; +constexpr KeyMapping circumflexKeyTable[] = { + // ^ + { Qt::Key_A, Qt::Key_Acircumflex }, { Qt::Key_E, Qt::Key_Ecircumflex }, + { Qt::Key_I, Qt::Key_Icircumflex }, { Qt::Key_O, Qt::Key_Ocircumflex }, + { Qt::Key_U, Qt::Key_Ucircumflex }, +}; + +static Qt::Key find_impl(const KeyMapping *first, const KeyMapping *last, Qt::Key key) noexcept +{ + while (first != last) { + if (first->from == key) + return first->to; + ++first; + } + return Qt::Key_unknown; +} + +template<size_t N> +static Qt::Key find(const KeyMapping (&map)[N], Qt::Key key) noexcept +{ + return find_impl(map, map + N, key); +} + +Qt::Key translateBaseKeyUsingDeadKey(Qt::Key accentBaseKey, Qt::Key deadKey) +{ + switch (deadKey) { + case Qt::Key_Dead_Grave: + return find(graveKeyTable, accentBaseKey); + case Qt::Key_Dead_Acute: + return find(acuteKeyTable, accentBaseKey); + case Qt::Key_Dead_Tilde: + return find(tildeKeyTable, accentBaseKey); + case Qt::Key_Dead_Diaeresis: + return find(diaeresisKeyTable, accentBaseKey); + case Qt::Key_Dead_Circumflex: + return find(circumflexKeyTable, accentBaseKey); + default: + return Qt::Key_unknown; + }; +} + +template<class T> +std::optional<QString> findKeyTextByKeyId(const T &mappingArray, Qt::Key qtKey) +{ + const auto it = std::find_if(mappingArray.cbegin(), mappingArray.cend(), + [qtKey](const WebKb2QtData &data) { return data.qt == qtKey; }); + return it != mappingArray.cend() ? it->web : std::optional<QString>(); +} +} // namespace + +std::optional<Qt::Key> QWasmKeyTranslator::mapWebKeyTextToQtKey(const char *toFind) +{ + const WebKb2QtData searchKey{ toFind, 0 }; + const auto it = std::lower_bound(WebToQtKeyCodeMappings.cbegin(), WebToQtKeyCodeMappings.cend(), + searchKey); + return it != WebToQtKeyCodeMappings.cend() && searchKey == *it ? static_cast<Qt::Key>(it->qt) + : std::optional<Qt::Key>(); +} + +QWasmDeadKeySupport::QWasmDeadKeySupport() = default; + +QWasmDeadKeySupport::~QWasmDeadKeySupport() = default; + +void QWasmDeadKeySupport::applyDeadKeyTranslations(KeyEvent *event) +{ + if (event->deadKey) { + m_activeDeadKey = event->key; + } else if (m_activeDeadKey != Qt::Key_unknown + && (((m_keyModifiedByDeadKeyOnPress == Qt::Key_unknown + && event->type == EventType::KeyDown)) + || (m_keyModifiedByDeadKeyOnPress == event->key + && event->type == EventType::KeyUp))) { + const Qt::Key baseKey = event->key; + const Qt::Key translatedKey = translateBaseKeyUsingDeadKey(baseKey, m_activeDeadKey); + if (translatedKey != Qt::Key_unknown) { + event->key = translatedKey; + + auto foundText = event->modifiers.testFlag(Qt::ShiftModifier) + ? findKeyTextByKeyId(DiacriticalCharsKeyToTextUppercase, event->key) + : findKeyTextByKeyId(DiacriticalCharsKeyToTextLowercase, event->key); + Q_ASSERT(foundText.has_value()); + event->text = foundText->size() == 1 ? *foundText : QString(); + } + + if (!event->text.isEmpty()) { + if (event->type == EventType::KeyDown) { + // Assume the first keypress with an active dead key is treated as modified, + // regardless of whether it has actually been modified or not. Take into account + // only events that produce actual key text. + if (!event->text.isEmpty()) + m_keyModifiedByDeadKeyOnPress = baseKey; + } else { + Q_ASSERT(event->type == EventType::KeyUp); + Q_ASSERT(m_keyModifiedByDeadKeyOnPress == baseKey); + m_keyModifiedByDeadKeyOnPress = Qt::Key_unknown; + m_activeDeadKey = Qt::Key_unknown; + } + } + } +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmkeytranslator.h b/src/plugins/platforms/wasm/qwasmkeytranslator.h new file mode 100644 index 0000000000..11a89e6193 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmkeytranslator.h @@ -0,0 +1,34 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMKEYTRANSLATOR_H +#define QWASMKEYTRANSLATOR_H + +#include <QtCore/qnamespace.h> +#include <QtCore/qtypes.h> + +#include <optional> + +QT_BEGIN_NAMESPACE + +struct KeyEvent; + +namespace QWasmKeyTranslator { +std::optional<Qt::Key> mapWebKeyTextToQtKey(const char *toFind); +} + +class QWasmDeadKeySupport +{ +public: + explicit QWasmDeadKeySupport(); + ~QWasmDeadKeySupport(); + + void applyDeadKeyTranslations(KeyEvent *event); + +private: + Qt::Key m_activeDeadKey = Qt::Key_unknown; + Qt::Key m_keyModifiedByDeadKeyOnPress = Qt::Key_unknown; +}; + +QT_END_NAMESPACE +#endif // QWASMKEYTRANSLATOR_H diff --git a/src/plugins/platforms/wasm/qwasmoffscreensurface.cpp b/src/plugins/platforms/wasm/qwasmoffscreensurface.cpp index 31adc73cf5..dcfc4433e6 100644 --- a/src/plugins/platforms/wasm/qwasmoffscreensurface.cpp +++ b/src/plugins/platforms/wasm/qwasmoffscreensurface.cpp @@ -6,10 +6,30 @@ QT_BEGIN_NAMESPACE QWasmOffscreenSurface::QWasmOffscreenSurface(QOffscreenSurface *offscreenSurface) - :QPlatformOffscreenSurface(offscreenSurface) + : QPlatformOffscreenSurface(offscreenSurface), m_offscreenCanvas(emscripten::val::undefined()) { + const auto offscreenCanvasClass = emscripten::val::global("OffscreenCanvas"); + // The OffscreenCanvas is not supported on some browsers, most notably on Safari. + if (!offscreenCanvasClass) + return; + + m_offscreenCanvas = offscreenCanvasClass.new_(offscreenSurface->size().width(), + offscreenSurface->size().height()); + + m_specialTargetId = std::string("!qtoffscreen_") + std::to_string(uintptr_t(this)); + + emscripten::val::module_property("specialHTMLTargets") + .set(m_specialTargetId, m_offscreenCanvas); +} + +QWasmOffscreenSurface::~QWasmOffscreenSurface() +{ + emscripten::val::module_property("specialHTMLTargets").delete_(m_specialTargetId); } -QWasmOffscreenSurface::~QWasmOffscreenSurface() = default; +bool QWasmOffscreenSurface::isValid() const +{ + return !m_offscreenCanvas.isNull() && !m_offscreenCanvas.isUndefined(); +} QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmoffscreensurface.h b/src/plugins/platforms/wasm/qwasmoffscreensurface.h index 88a64b775a..1c71310448 100644 --- a/src/plugins/platforms/wasm/qwasmoffscreensurface.h +++ b/src/plugins/platforms/wasm/qwasmoffscreensurface.h @@ -6,6 +6,10 @@ #include <qpa/qplatformoffscreensurface.h> +#include <emscripten/val.h> + +#include <string> + QT_BEGIN_NAMESPACE class QOffscreenSurface; @@ -14,8 +18,13 @@ class QWasmOffscreenSurface final : public QPlatformOffscreenSurface public: explicit QWasmOffscreenSurface(QOffscreenSurface *offscreenSurface); ~QWasmOffscreenSurface() final; -private: + const std::string &id() const { return m_specialTargetId; } + bool isValid() const override; + +private: + std::string m_specialTargetId; + emscripten::val m_offscreenCanvas; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmopenglcontext.cpp b/src/plugins/platforms/wasm/qwasmopenglcontext.cpp index 3dfb201367..8a4664ec8c 100644 --- a/src/plugins/platforms/wasm/qwasmopenglcontext.cpp +++ b/src/plugins/platforms/wasm/qwasmopenglcontext.cpp @@ -2,6 +2,8 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmopenglcontext.h" + +#include "qwasmoffscreensurface.h" #include "qwasmintegration.h" #include <EGL/egl.h> #include <emscripten/bind.h> @@ -18,35 +20,25 @@ EMSCRIPTEN_BINDINGS(qwasmopenglcontext) QT_BEGIN_NAMESPACE -QWasmOpenGLContext::QWasmOpenGLContext(const QSurfaceFormat &format) - : m_requestedFormat(format) +QWasmOpenGLContext::QWasmOpenGLContext(QOpenGLContext *context) + : m_actualFormat(context->format()), m_qGlContext(context) { - m_requestedFormat.setRenderableType(QSurfaceFormat::OpenGLES); + m_actualFormat.setRenderableType(QSurfaceFormat::OpenGLES); // if we set one, we need to set the other as well since in webgl, these are tied together - if (format.depthBufferSize() < 0 && format.stencilBufferSize() > 0) - m_requestedFormat.setDepthBufferSize(16); - - if (format.stencilBufferSize() < 0 && format.depthBufferSize() > 0) - m_requestedFormat.setStencilBufferSize(8); + if (m_actualFormat.depthBufferSize() < 0 && m_actualFormat.stencilBufferSize() > 0) + m_actualFormat.setDepthBufferSize(16); + if (m_actualFormat.stencilBufferSize() < 0 && m_actualFormat.depthBufferSize() > 0) + m_actualFormat.setStencilBufferSize(8); } QWasmOpenGLContext::~QWasmOpenGLContext() { - if (!m_context) - return; - // Destroy GL context. Work around bug in emscripten_webgl_destroy_context // which removes all event handlers on the canvas by temporarily replacing the function // that does the removal with a function that does nothing. - emscripten::val jsEvents = emscripten::val::module_property("JSEvents"); - emscripten::val savedRemoveAllHandlersOnTargetFunction = - jsEvents["removeAllHandlersOnTarget"]; - jsEvents.set("removeAllHandlersOnTarget", emscripten::val::module_property("qtDoNothing")); - emscripten_webgl_destroy_context(m_context); - jsEvents.set("removeAllHandlersOnTarget", savedRemoveAllHandlersOnTargetFunction); - m_context = 0; + destroyWebGLContext(m_ownedWebGLContext.handle); } bool QWasmOpenGLContext::isOpenGLVersionSupported(QSurfaceFormat format) @@ -59,20 +51,61 @@ bool QWasmOpenGLContext::isOpenGLVersionSupported(QSurfaceFormat format) (format.majorVersion() == 3 && format.minorVersion() == 0)); } -bool QWasmOpenGLContext::maybeCreateEmscriptenContext(QPlatformSurface *surface) +EMSCRIPTEN_WEBGL_CONTEXT_HANDLE +QWasmOpenGLContext::obtainEmscriptenContext(QPlatformSurface *surface) { - if (m_context && m_surface == surface) - return true; - - // TODO(mikolajboc): Use OffscreenCanvas if available. - if (surface->surface()->surfaceClass() == QSurface::Offscreen) - return false; - - m_surface = surface; + if (m_ownedWebGLContext.surface == surface) + return m_ownedWebGLContext.handle; + + if (surface->surface()->surfaceClass() == QSurface::Offscreen) { + // Reuse the existing context for offscreen drawing, even if it happens to be a canvas + // context. This is because it is impossible to re-home an existing context to the + // new surface and works as an emulation measure. + if (m_ownedWebGLContext.handle) + return m_ownedWebGLContext.handle; + + // The non-shared offscreen context is heavily limited on WASM, but we provide it + // anyway for potential pixel readbacks. + m_ownedWebGLContext = + QOpenGLContextData{ .surface = surface, + .handle = createEmscriptenContext( + static_cast<QWasmOffscreenSurface *>(surface)->id(), + m_actualFormat) }; + } else { + destroyWebGLContext(m_ownedWebGLContext.handle); + + // Create a full on-screen context for the window canvas. + m_ownedWebGLContext = QOpenGLContextData{ + .surface = surface, + .handle = createEmscriptenContext(static_cast<QWasmWindow *>(surface)->canvasSelector(), + m_actualFormat) + }; + } + + EmscriptenWebGLContextAttributes actualAttributes; + + EMSCRIPTEN_RESULT attributesResult = emscripten_webgl_get_context_attributes(m_ownedWebGLContext.handle, &actualAttributes); + if (attributesResult == EMSCRIPTEN_RESULT_SUCCESS) { + if (actualAttributes.majorVersion == 1) { + m_actualFormat.setMajorVersion(2); + } else if (actualAttributes.majorVersion == 2) { + m_actualFormat.setMajorVersion(3); + } + m_actualFormat.setMinorVersion(0); + } + + return m_ownedWebGLContext.handle; +} - auto *window = static_cast<QWasmWindow *>(surface); - m_context = createEmscriptenContext(window->canvasSelector(), m_requestedFormat); - return true; +void QWasmOpenGLContext::destroyWebGLContext(EMSCRIPTEN_WEBGL_CONTEXT_HANDLE contextHandle) +{ + if (!contextHandle) + return; + emscripten::val jsEvents = emscripten::val::module_property("JSEvents"); + emscripten::val savedRemoveAllHandlersOnTargetFunction = jsEvents["removeAllHandlersOnTarget"]; + jsEvents.set("removeAllHandlersOnTarget", emscripten::val::module_property("qtDoNothing")); + emscripten_webgl_destroy_context(contextHandle); + jsEvents.set("removeAllHandlersOnTarget", savedRemoveAllHandlersOnTargetFunction); } EMSCRIPTEN_WEBGL_CONTEXT_HANDLE @@ -86,9 +119,8 @@ QWasmOpenGLContext::createEmscriptenContext(const std::string &canvasSelector, attributes.failIfMajorPerformanceCaveat = false; attributes.antialias = true; attributes.enableExtensionsByDefault = true; - attributes.majorVersion = format.majorVersion() - 1; - attributes.minorVersion = format.minorVersion(); - + attributes.majorVersion = 2; // try highest supported version ES3.0 / WebGL 2.0 + attributes.minorVersion = 0; // emscripten only supports minor version 0 // WebGL doesn't allow separate attach buffers to STENCIL_ATTACHMENT and DEPTH_ATTACHMENT // we need both or none const bool useDepthStencil = (format.depthBufferSize() > 0 || format.stencilBufferSize() > 0); @@ -97,13 +129,20 @@ QWasmOpenGLContext::createEmscriptenContext(const std::string &canvasSelector, attributes.alpha = format.alphaBufferSize() > 0; attributes.depth = useDepthStencil; attributes.stencil = useDepthStencil; - - return emscripten_webgl_create_context(canvasSelector.c_str(), &attributes); + EMSCRIPTEN_RESULT contextResult = emscripten_webgl_create_context(canvasSelector.c_str(), &attributes); + + if (contextResult <= 0) { + // fallback to opengles2/webgl1 + // for devices that do not support opengles3/webgl2 + attributes.majorVersion = 1; + contextResult = emscripten_webgl_create_context(canvasSelector.c_str(), &attributes); + } + return contextResult; } QSurfaceFormat QWasmOpenGLContext::format() const { - return m_requestedFormat; + return m_actualFormat; } GLuint QWasmOpenGLContext::defaultFramebufferObject(QPlatformSurface *surface) const @@ -113,10 +152,22 @@ GLuint QWasmOpenGLContext::defaultFramebufferObject(QPlatformSurface *surface) c bool QWasmOpenGLContext::makeCurrent(QPlatformSurface *surface) { - if (!maybeCreateEmscriptenContext(surface)) + static bool sentSharingWarning = false; + if (!sentSharingWarning && isSharing()) { + qWarning() << "The functionality for sharing OpenGL contexts is limited, see documentation"; + sentSharingWarning = true; + } + + if (auto *shareContext = m_qGlContext->shareContext()) + return shareContext->makeCurrent(surface->surface()); + + const auto context = obtainEmscriptenContext(surface); + if (!context) return false; - return emscripten_webgl_make_context_current(m_context) == EMSCRIPTEN_RESULT_SUCCESS; + m_usedWebGLContextHandle = context; + + return emscripten_webgl_make_context_current(context) == EMSCRIPTEN_RESULT_SUCCESS; } void QWasmOpenGLContext::swapBuffers(QPlatformSurface *surface) @@ -132,17 +183,17 @@ void QWasmOpenGLContext::doneCurrent() bool QWasmOpenGLContext::isSharing() const { - return false; + return m_qGlContext->shareContext(); } bool QWasmOpenGLContext::isValid() const { - if (!isOpenGLVersionSupported(m_requestedFormat)) + if (!isOpenGLVersionSupported(m_actualFormat)) return false; // Note: we get isValid() calls before we see the surface and can // create a native context, so no context is also a valid state. - return !m_context || !emscripten_is_webgl_context_lost(m_context); + return !m_usedWebGLContextHandle || !emscripten_is_webgl_context_lost(m_usedWebGLContextHandle); } QFunctionPointer QWasmOpenGLContext::getProcAddress(const char *procName) diff --git a/src/plugins/platforms/wasm/qwasmopenglcontext.h b/src/plugins/platforms/wasm/qwasmopenglcontext.h index ac456d90e4..2a8bcc5d9b 100644 --- a/src/plugins/platforms/wasm/qwasmopenglcontext.h +++ b/src/plugins/platforms/wasm/qwasmopenglcontext.h @@ -1,6 +1,9 @@ // Copyright (C) 2018 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only +#ifndef QWASMOPENGLCONTEXT_H +#define QWASMOPENGLCONTEXT_H + #include <qpa/qplatformopenglcontext.h> #include <emscripten.h> @@ -8,11 +11,13 @@ QT_BEGIN_NAMESPACE +class QOpenGLContext; class QPlatformScreen; +class QPlatformSurface; class QWasmOpenGLContext : public QPlatformOpenGLContext { public: - QWasmOpenGLContext(const QSurfaceFormat &format); + explicit QWasmOpenGLContext(QOpenGLContext *context); ~QWasmOpenGLContext(); QSurfaceFormat format() const override; @@ -25,15 +30,25 @@ public: QFunctionPointer getProcAddress(const char *procName) override; private: + struct QOpenGLContextData + { + QPlatformSurface *surface = nullptr; + EMSCRIPTEN_WEBGL_CONTEXT_HANDLE handle = 0; + }; + static bool isOpenGLVersionSupported(QSurfaceFormat format); - bool maybeCreateEmscriptenContext(QPlatformSurface *surface); + EMSCRIPTEN_WEBGL_CONTEXT_HANDLE obtainEmscriptenContext(QPlatformSurface *surface); static EMSCRIPTEN_WEBGL_CONTEXT_HANDLE createEmscriptenContext(const std::string &canvasSelector, QSurfaceFormat format); - QSurfaceFormat m_requestedFormat; - QPlatformSurface *m_surface = nullptr; - EMSCRIPTEN_WEBGL_CONTEXT_HANDLE m_context = 0; + static void destroyWebGLContext(EMSCRIPTEN_WEBGL_CONTEXT_HANDLE contextHandle); + + QSurfaceFormat m_actualFormat; + QOpenGLContext *m_qGlContext; + QOpenGLContextData m_ownedWebGLContext; + EMSCRIPTEN_WEBGL_CONTEXT_HANDLE m_usedWebGLContextHandle = 0; }; QT_END_NAMESPACE +#endif // QWASMOPENGLCONTEXT_H diff --git a/src/plugins/platforms/wasm/qwasmplatform.cpp b/src/plugins/platforms/wasm/qwasmplatform.cpp index c641e345e4..e54992be1d 100644 --- a/src/plugins/platforms/wasm/qwasmplatform.cpp +++ b/src/plugins/platforms/wasm/qwasmplatform.cpp @@ -13,8 +13,9 @@ Platform platform() if (rawPlatform.call<bool>("includes", emscripten::val("Mac"))) return Platform::MacOS; - if (rawPlatform.call<bool>("includes", emscripten::val("iPhone"))) - return Platform::iPhone; + if (rawPlatform.call<bool>("includes", emscripten::val("iPhone")) + || rawPlatform.call<bool>("includes", emscripten::val("iPad"))) + return Platform::iOS; if (rawPlatform.call<bool>("includes", emscripten::val("Win32"))) return Platform::Windows; if (rawPlatform.call<bool>("includes", emscripten::val("Linux"))) { diff --git a/src/plugins/platforms/wasm/qwasmplatform.h b/src/plugins/platforms/wasm/qwasmplatform.h index 239efdeae9..5b32e43633 100644 --- a/src/plugins/platforms/wasm/qwasmplatform.h +++ b/src/plugins/platforms/wasm/qwasmplatform.h @@ -19,7 +19,7 @@ enum class Platform { Windows, Linux, Android, - iPhone, + iOS }; Platform platform(); diff --git a/src/plugins/platforms/wasm/qwasmscreen.cpp b/src/plugins/platforms/wasm/qwasmscreen.cpp index 578afb75cf..0490b2bfe0 100644 --- a/src/plugins/platforms/wasm/qwasmscreen.cpp +++ b/src/plugins/platforms/wasm/qwasmscreen.cpp @@ -2,12 +2,12 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmscreen.h" -#include "qwasmwindow.h" -#include "qwasmeventtranslator.h" + #include "qwasmcompositor.h" -#include "qwasmintegration.h" -#include "qwasmstring.h" #include "qwasmcssstyle.h" +#include "qwasmintegration.h" +#include "qwasmkeytranslator.h" +#include "qwasmwindow.h" #include <emscripten/bind.h> #include <emscripten/val.h> @@ -28,9 +28,10 @@ const char *QWasmScreen::m_canvasResizeObserverCallbackContextPropertyName = QWasmScreen::QWasmScreen(const emscripten::val &containerOrCanvas) : m_container(containerOrCanvas), + m_intermediateContainer(emscripten::val::undefined()), m_shadowContainer(emscripten::val::undefined()), m_compositor(new QWasmCompositor(this)), - m_eventTranslator(new QWasmEventTranslator()) + m_deadKeySupport(std::make_unique<QWasmDeadKeySupport>()) { auto document = m_container["ownerDocument"]; // Each screen is represented by a div container. All of the windows exist therein as @@ -40,12 +41,23 @@ QWasmScreen::QWasmScreen(const emscripten::val &containerOrCanvas) qWarning() << "Support for canvas elements as an element backing screen is deprecated. The " "canvas provided for the screen will be transformed into a div."; auto container = document.call<emscripten::val>("createElement", emscripten::val("div")); - m_container["parentNode"].call<void>("replaceChild", m_container, container); + m_container["parentNode"].call<void>("replaceChild", container, m_container); m_container = container; } + + // Create an intermediate container which we can remove during cleanup in ~QWasmScreen(). + // This is required due to the attachShadow() call below; there is no corresponding + // "detachShadow()" API to return the container to its previous state. + m_intermediateContainer = document.call<emscripten::val>("createElement", emscripten::val("div")); + m_intermediateContainer.set("id", std::string("qt-shadow-container")); + emscripten::val intermediateContainerStyle = m_intermediateContainer["style"]; + intermediateContainerStyle.set("width", std::string("100%")); + intermediateContainerStyle.set("height", std::string("100%")); + m_container.call<void>("appendChild", m_intermediateContainer); + auto shadowOptions = emscripten::val::object(); shadowOptions.set("mode", "open"); - auto shadow = m_container.call<emscripten::val>("attachShadow", shadowOptions); + auto shadow = m_intermediateContainer.call<emscripten::val>("attachShadow", shadowOptions); m_shadowContainer = document.call<emscripten::val>("createElement", emscripten::val("div")); @@ -57,22 +69,11 @@ QWasmScreen::QWasmScreen(const emscripten::val &containerOrCanvas) m_shadowContainer["classList"].call<void>("add", std::string("qt-screen")); - // Set contenteditable so that the canvas gets clipboard events, - // then hide the resulting focus frame, and reset the cursor. - m_shadowContainer.set("contentEditable", std::string("true")); - // set inputmode to none to stop mobile keyboard opening - // when user clicks anywhere on the canvas. - m_shadowContainer.set("inputmode", std::string("none")); - - // Hide the canvas from screen readers. - m_shadowContainer.call<void>("setAttribute", std::string("aria-hidden"), std::string("true")); - // Disable the default context menu; Qt applications typically // provide custom right-click behavior. m_onContextMenu = std::make_unique<qstdweb::EventCallback>( m_shadowContainer, "contextmenu", [](emscripten::val event) { event.call<void>("preventDefault"); }); - // Create "specialHTMLTargets" mapping for the canvas - the element might be unreachable based // on its id only under some conditions, like the target being embedded in a shadow DOM or a // subframe. @@ -82,17 +83,32 @@ QWasmScreen::QWasmScreen(const emscripten::val &containerOrCanvas) emscripten::val::module_property("specialHTMLTargets") .set(outerScreenId().toStdString(), m_container); - // Install event handlers on the container/canvas. This must be - // done after the canvas has been created above. - m_compositor->initEventHandlers(); - updateQScreenAndCanvasRenderSize(); m_shadowContainer.call<void>("focus"); + + m_touchDevice = std::make_unique<QPointingDevice>( + "touchscreen", 1, QInputDevice::DeviceType::TouchScreen, + QPointingDevice::PointerType::Finger, + QPointingDevice::Capability::Position | QPointingDevice::Capability::Area + | QPointingDevice::Capability::NormalizedPosition, + 10, 0); + m_tabletDevice = std::make_unique<QPointingDevice>( + "stylus", 2, QInputDevice::DeviceType::Stylus, + QPointingDevice::PointerType::Pen, + QPointingDevice::Capability::Position | QPointingDevice::Capability::Pressure + | QPointingDevice::Capability::NormalizedPosition + | QInputDevice::Capability::MouseEmulation + | QInputDevice::Capability::Hover | QInputDevice::Capability::Rotation + | QInputDevice::Capability::XTilt | QInputDevice::Capability::YTilt + | QInputDevice::Capability::TangentialPressure, + 0, 0); + + QWindowSystemInterface::registerInputDevice(m_touchDevice.get()); } QWasmScreen::~QWasmScreen() { - Q_ASSERT(!m_compositor); // deleteScreen should have been called to remove this screen + m_intermediateContainer.call<void>("remove"); emscripten::val::module_property("specialHTMLTargets") .set(eventTargetId().toStdString(), emscripten::val::undefined()); @@ -103,9 +119,6 @@ QWasmScreen::~QWasmScreen() void QWasmScreen::deleteScreen() { - // Delete the compositor before removing the screen, since its destruction routine needs to use - // the fully operational screen. - m_compositor.reset(); // Deletes |this|! QWindowSystemInterface::handleScreenRemoved(this); } @@ -127,11 +140,6 @@ QWasmCompositor *QWasmScreen::compositor() return m_compositor.get(); } -QWasmEventTranslator *QWasmScreen::eventTranslator() -{ - return m_eventTranslator.get(); -} - emscripten::val QWasmScreen::element() const { return m_shadowContainer; @@ -198,7 +206,7 @@ qreal QWasmScreen::devicePixelRatio() const QString QWasmScreen::name() const { - return QWasmString::toQString(m_shadowContainer["id"]); + return QString::fromEcmaString(m_shadowContainer["id"]); } QPlatformCursor *QWasmScreen::cursor() const @@ -215,19 +223,30 @@ void QWasmScreen::resizeMaximizedWindows() QWindow *QWasmScreen::topWindow() const { - return m_compositor->keyWindow(); + return activeChild() ? activeChild()->window() : nullptr; } QWindow *QWasmScreen::topLevelAt(const QPoint &p) const { - return m_compositor->windowAt(p); + const auto found = + std::find_if(childStack().begin(), childStack().end(), [&p](const QWasmWindow *window) { + const QRect geometry = window->windowFrameGeometry(); + + return window->isVisible() && geometry.contains(p); + }); + return found != childStack().end() ? (*found)->window() : nullptr; +} + +QPointF QWasmScreen::mapFromLocal(const QPointF &p) const +{ + return geometry().topLeft() + p; } -QPoint QWasmScreen::clipPoint(const QPoint &p) const +QPointF QWasmScreen::clipPoint(const QPointF &p) const { - return QPoint( - std::max(screen()->geometry().left(), std::min(screen()->geometry().right(), p.x())), - std::max(screen()->geometry().top(), std::min(screen()->geometry().bottom(), p.y()))); + const auto geometryF = screen()->geometry().toRectF(); + return QPointF(qBound(geometryF.left(), p.x(), geometryF.right()), + qBound(geometryF.top(), p.y(), geometryF.bottom())); } void QWasmScreen::invalidateSize() @@ -243,6 +262,18 @@ void QWasmScreen::setGeometry(const QRect &rect) resizeMaximizedWindows(); } +void QWasmScreen::onSubtreeChanged(QWasmWindowTreeNodeChangeType changeType, + QWasmWindowTreeNode *parent, QWasmWindow *child) +{ + Q_UNUSED(parent); + if (changeType == QWasmWindowTreeNodeChangeType::NodeInsertion && parent == this + && childStack().size() == 1) { + child->window()->setFlag(Qt::WindowStaysOnBottomHint); + } + QWasmWindowTreeNode::onSubtreeChanged(changeType, parent, child); + m_compositor->onWindowTreeChanged(changeType, child); +} + void QWasmScreen::updateQScreenAndCanvasRenderSize() { // The HTML canvas has two sizes: the CSS size and the canvas render size. @@ -271,7 +302,6 @@ void QWasmScreen::updateQScreenAndCanvasRenderSize() }; setGeometry(QRect(getElementBodyPosition(m_shadowContainer), cssSize.toSize())); - m_compositor->requestUpdateAllWindows(); } void QWasmScreen::canvasResizeObserverCallback(emscripten::val entries, emscripten::val) @@ -317,4 +347,31 @@ void QWasmScreen::installCanvasResizeObserver() resizeObserver.call<void>("observe", m_shadowContainer); } +emscripten::val QWasmScreen::containerElement() +{ + return m_shadowContainer; +} + +QWasmWindowTreeNode *QWasmScreen::parentNode() +{ + return nullptr; +} + +QList<QWasmWindow *> QWasmScreen::allWindows() +{ + QList<QWasmWindow *> windows; + for (auto *child : childStack()) { + const QWindowList list = child->window()->findChildren<QWindow *>(Qt::FindChildrenRecursively); + for (auto child : list) { + auto handle = child->handle(); + if (handle) { + auto wnd = static_cast<QWasmWindow *>(handle); + windows.push_back(wnd); + } + } + windows.push_back(child); + } + return windows; +} + QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmscreen.h b/src/plugins/platforms/wasm/qwasmscreen.h index fd573059e6..da171d3f50 100644 --- a/src/plugins/platforms/wasm/qwasmscreen.h +++ b/src/plugins/platforms/wasm/qwasmscreen.h @@ -6,6 +6,8 @@ #include "qwasmcursor.h" +#include "qwasmwindowtreenode.h" + #include <qpa/qplatformscreen.h> #include <QtCore/qscopedpointer.h> @@ -20,10 +22,10 @@ class QPlatformOpenGLContext; class QWasmWindow; class QWasmBackingStore; class QWasmCompositor; -class QWasmEventTranslator; +class QWasmDeadKeySupport; class QOpenGLContext; -class QWasmScreen : public QObject, public QPlatformScreen +class QWasmScreen : public QObject, public QPlatformScreen, public QWasmWindowTreeNode { Q_OBJECT public: @@ -36,9 +38,13 @@ public: emscripten::val element() const; QString eventTargetId() const; QString outerScreenId() const; + QPointingDevice *touchDevice() { return m_touchDevice.get(); } + QPointingDevice *tabletDevice() { return m_tabletDevice.get(); } QWasmCompositor *compositor(); - QWasmEventTranslator *eventTranslator(); + QWasmDeadKeySupport *deadKeySupport() { return m_deadKeySupport.get(); } + + QList<QWasmWindow *> allWindows(); QRect geometry() const override; int depth() const override; @@ -52,7 +58,12 @@ public: QWindow *topWindow() const; QWindow *topLevelAt(const QPoint &p) const override; - QPoint clipPoint(const QPoint &p) const; + // QWasmWindowTreeNode: + emscripten::val containerElement() final; + QWasmWindowTreeNode *parentNode() final; + + QPointF mapFromLocal(const QPointF &p) const; + QPointF clipPoint(const QPointF &p) const; void invalidateSize(); void updateQScreenAndCanvasRenderSize(); @@ -63,10 +74,17 @@ public slots: void setGeometry(const QRect &rect); private: + // QWasmWindowTreeNode: + void onSubtreeChanged(QWasmWindowTreeNodeChangeType changeType, QWasmWindowTreeNode *parent, + QWasmWindow *child) final; + emscripten::val m_container; + emscripten::val m_intermediateContainer; emscripten::val m_shadowContainer; std::unique_ptr<QWasmCompositor> m_compositor; - std::unique_ptr<QWasmEventTranslator> m_eventTranslator; + std::unique_ptr<QPointingDevice> m_touchDevice; + std::unique_ptr<QPointingDevice> m_tabletDevice; + std::unique_ptr<QWasmDeadKeySupport> m_deadKeySupport; QRect m_geometry = QRect(0, 0, 100, 100); int m_depth = 32; QImage::Format m_format = QImage::Format_RGB32; diff --git a/src/plugins/platforms/wasm/qwasmservices.cpp b/src/plugins/platforms/wasm/qwasmservices.cpp index b9f48090e1..e767295e41 100644 --- a/src/plugins/platforms/wasm/qwasmservices.cpp +++ b/src/plugins/platforms/wasm/qwasmservices.cpp @@ -2,7 +2,6 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmservices.h" -#include "qwasmstring.h" #include <QtCore/QUrl> #include <QtCore/QDebug> @@ -13,8 +12,8 @@ QT_BEGIN_NAMESPACE bool QWasmServices::openUrl(const QUrl &url) { - emscripten::val jsUrl = QWasmString::fromQString(url.toString()); - emscripten::val::global("window").call<void>("open", jsUrl, emscripten::val("_blank")); + emscripten::val::global("window").call<void>("open", url.toString().toEcmaString(), + emscripten::val("_blank")); return true; } diff --git a/src/plugins/platforms/wasm/qwasmstring.cpp b/src/plugins/platforms/wasm/qwasmstring.cpp deleted file mode 100644 index 3de84afef3..0000000000 --- a/src/plugins/platforms/wasm/qwasmstring.cpp +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (C) 2020 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only - -#include "qwasmstring.h" - -QT_BEGIN_NAMESPACE - -using namespace emscripten; - -val QWasmString::fromQString(const QString &str) -{ - static const val UTF16ToString( - val::module_property("UTF16ToString")); - - auto ptr = quintptr(str.utf16()); - return UTF16ToString(val(ptr)); -} - -QString QWasmString::toQString(const val &v) -{ - QString result; - if (!v.isString()) - return result; - - static const val stringToUTF16( - val::module_property("stringToUTF16")); - static const val length("length"); - - int len = v[length].as<int>(); - result.resize(len); - auto ptr = quintptr(result.utf16()); - stringToUTF16(v, val(ptr), val((len + 1) * 2)); - return result; -} - -QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmstring.h b/src/plugins/platforms/wasm/qwasmstring.h deleted file mode 100644 index 62927ee93c..0000000000 --- a/src/plugins/platforms/wasm/qwasmstring.h +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (C) 2020 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only - -#pragma once - -#include <qstring.h> - -#include <emscripten/val.h> - -QT_BEGIN_NAMESPACE - -class QWasmString -{ -public: - static emscripten::val fromQString(const QString &str); - static QString toQString(const emscripten::val &v); -}; -QT_END_NAMESPACE - diff --git a/src/plugins/platforms/wasm/qwasmstylepixmaps_p.h b/src/plugins/platforms/wasm/qwasmstylepixmaps_p.h deleted file mode 100644 index 0f061a38b3..0000000000 --- a/src/plugins/platforms/wasm/qwasmstylepixmaps_p.h +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (C) 2018 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only - -#ifndef QWASMSTYLEPIXMAPS_P_H -#define QWASMSTYLEPIXMAPS_P_H - -// -// W A R N I N G -// ------------- -// -// This file is not part of the Qt API. It exists purely as an -// implementation detail. This header file may change from version to -// version without notice, or even be removed. -// -// We mean it. -// - -QT_BEGIN_NAMESPACE - -/* XPM */ -static const char * const qt_menu_xpm[] = { -"16 16 72 1", -" c None", -". c #65AF36", -"+ c #66B036", -"@ c #77B94C", -"# c #A7D28C", -"$ c #BADBA4", -"% c #A4D088", -"& c #72B646", -"* c #9ACB7A", -"= c #7FBD56", -"- c #85C05F", -"; c #F4F9F0", -"> c #FFFFFF", -", c #E5F1DC", -"' c #ECF5E7", -") c #7ABA50", -"! c #83BF5C", -"~ c #AED595", -"{ c #D7EACA", -"] c #A9D28D", -"^ c #BCDDA8", -"/ c #C4E0B1", -"( c #81BE59", -"_ c #D0E7C2", -": c #D4E9C6", -"< c #6FB542", -"[ c #6EB440", -"} c #88C162", -"| c #98CA78", -"1 c #F4F9F1", -"2 c #8FC56C", -"3 c #F1F8EC", -"4 c #E8F3E1", -"5 c #D4E9C7", -"6 c #74B748", -"7 c #80BE59", -"8 c #73B747", -"9 c #6DB43F", -"0 c #CBE4BA", -"a c #80BD58", -"b c #6DB33F", -"c c #FEFFFE", -"d c #68B138", -"e c #F9FCF7", -"f c #91C66F", -"g c #E8F3E0", -"h c #DCEDD0", -"i c #91C66E", -"j c #A3CF86", -"k c #C9E3B8", -"l c #B0D697", -"m c #E3F0DA", -"n c #95C873", -"o c #E6F2DE", -"p c #9ECD80", -"q c #BEDEAA", -"r c #C7E2B6", -"s c #79BA4F", -"t c #6EB441", -"u c #BCDCA7", -"v c #FAFCF8", -"w c #F6FAF3", -"x c #84BF5D", -"y c #EDF6E7", -"z c #FAFDF9", -"A c #88C263", -"B c #98CA77", -"C c #CDE5BE", -"D c #67B037", -"E c #D9EBCD", -"F c #6AB23C", -"G c #77B94D", -" .++++++++++++++", -".+++++++++++++++", -"+++@#$%&+++*=+++", -"++-;>,>')+!>~+++", -"++{>]+^>/(_>:~<+", -"+[>>}+|>123>456+", -"+7>>8+->>90>~+++", -"+a>>b+a>c[0>~+++", -"+de>=+f>g+0>~+++", -"++h>i+j>k+0>~+++", -"++l>mno>p+q>rst+", -"++duv>wl++xy>zA+", -"++++B>Cb++++&D++", -"+++++0zE++++++++", -"++++++FG+++++++.", -"++++++++++++++. "}; - -static const char * const qt_close_xpm[] = { -"10 10 2 1", -"# c #000000", -". c None", -"..........", -".##....##.", -"..##..##..", -"...####...", -"....##....", -"...####...", -"..##..##..", -".##....##.", -"..........", -".........."}; - -static const char * const qt_maximize_xpm[]={ -"10 10 2 1", -"# c #000000", -". c None", -"#########.", -"#########.", -"#.......#.", -"#.......#.", -"#.......#.", -"#.......#.", -"#.......#.", -"#.......#.", -"#########.", -".........."}; - - -static const char * const qt_normalizeup_xpm[] = { -"10 10 2 1", -"# c #000000", -". c None", -"...######.", -"...######.", -"...#....#.", -".######.#.", -".######.#.", -".#....###.", -".#....#...", -".#....#...", -".######...", -".........."}; - -QT_END_NAMESPACE -#endif // QWASMSTYLEPIXMAPS_P_H diff --git a/src/plugins/platforms/wasm/qwasmwindow.cpp b/src/plugins/platforms/wasm/qwasmwindow.cpp index 842aaccc6e..0513f46e5b 100644 --- a/src/plugins/platforms/wasm/qwasmwindow.cpp +++ b/src/plugins/platforms/wasm/qwasmwindow.cpp @@ -5,232 +5,95 @@ #include <private/qguiapplication_p.h> #include <QtCore/qfile.h> #include <QtGui/private/qwindow_p.h> +#include <QtGui/private/qhighdpiscaling_p.h> #include <private/qpixmapcache_p.h> #include <QtGui/qopenglfunctions.h> #include <QBuffer> +#include "qwasmbase64iconstore.h" +#include "qwasmdom.h" +#include "qwasmclipboard.h" +#include "qwasmintegration.h" +#include "qwasmkeytranslator.h" #include "qwasmwindow.h" +#include "qwasmwindowclientarea.h" #include "qwasmscreen.h" -#include "qwasmstylepixmaps_p.h" #include "qwasmcompositor.h" #include "qwasmevent.h" #include "qwasmeventdispatcher.h" -#include "qwasmstring.h" +#include "qwasmaccessibility.h" +#include "qwasmclipboard.h" #include <iostream> +#include <sstream> + #include <emscripten/val.h> -#include <GL/gl.h> +#include <QtCore/private/qstdweb_p.h> QT_BEGIN_NAMESPACE -Q_GUI_EXPORT int qt_defaultDpiX(); - namespace { -enum class IconType { - Maximize, - First = Maximize, - QtLogo, - Restore, - X, - Size, -}; - -void syncCSSClassWith(emscripten::val element, std::string cssClassName, bool flag) -{ - if (flag) { - element["classList"].call<void>("add", emscripten::val(std::move(cssClassName))); - return; - } - - element["classList"].call<void>("remove", emscripten::val(std::move(cssClassName))); +QWasmWindowStack::PositionPreference positionPreferenceFromWindowFlags(Qt::WindowFlags flags) +{ + if (flags.testFlag(Qt::WindowStaysOnTopHint)) + return QWasmWindowStack::PositionPreference::StayOnTop; + if (flags.testFlag(Qt::WindowStaysOnBottomHint)) + return QWasmWindowStack::PositionPreference::StayOnBottom; + return QWasmWindowStack::PositionPreference::Regular; } } // namespace -class QWasmWindow::WebImageButton -{ -public: - class Callbacks - { - public: - Callbacks() = default; - Callbacks(std::function<void()> onInteraction, std::function<void()> onClick) - : m_onInteraction(std::move(onInteraction)), m_onClick(std::move(onClick)) - { - Q_ASSERT_X(!!m_onInteraction == !!m_onClick, Q_FUNC_INFO, - "Both callbacks need to be either null or non-null"); - } - ~Callbacks() = default; - - Callbacks(const Callbacks &) = delete; - Callbacks(Callbacks &&) = default; - Callbacks &operator=(const Callbacks &) = delete; - Callbacks &operator=(Callbacks &&) = default; - - operator bool() const { return !!m_onInteraction; } - - void onInteraction() { return m_onInteraction(); } - void onClick() { return m_onClick(); } - - private: - std::function<void()> m_onInteraction; - std::function<void()> m_onClick; - }; - - WebImageButton() - : m_containerElement( - emscripten::val::global("document") - .call<emscripten::val>("createElement", emscripten::val("div"))), - m_imageHolderElement( - emscripten::val::global("document") - .call<emscripten::val>("createElement", emscripten::val("span"))) - { - m_imageHolderElement.set("draggable", false); - - m_containerElement["classList"].call<void>("add", emscripten::val("image-button")); - m_containerElement.call<void>("appendChild", m_imageHolderElement); - } - - ~WebImageButton() = default; - - void setCallbacks(Callbacks callbacks) - { - if (callbacks) { - if (!m_webClickEventCallback) { - m_webMouseDownEventCallback = std::make_unique<qstdweb::EventCallback>( - m_containerElement, "mousedown", [this](emscripten::val event) { - event.call<void>("preventDefault"); - event.call<void>("stopPropagation"); - m_callbacks.onInteraction(); - }); - m_webClickEventCallback = std::make_unique<qstdweb::EventCallback>( - m_containerElement, "click", - [this](emscripten::val) { m_callbacks.onClick(); }); - } - } else { - m_webMouseDownEventCallback.reset(); - m_webClickEventCallback.reset(); - } - syncCSSClassWith(m_containerElement, "action-button", !!callbacks); - m_callbacks = std::move(callbacks); - } - - void setImage(std::string_view imageData, std::string_view format) - { - m_imageHolderElement.call<void>("removeAttribute", - emscripten::val("qt-builtin-image-type")); - m_imageHolderElement["style"].set("backgroundImage", - "url('data:image/" + std::string(format) + ";base64," - + std::string(imageData) + "')"); - } - - void setImage(IconType type) - { - m_imageHolderElement["style"].set("backgroundImage", emscripten::val::undefined()); - const auto imageType = ([type]() { - switch (type) { - case IconType::QtLogo: - return "qt-logo"; - case IconType::X: - return "x"; - case IconType::Restore: - return "restore"; - case IconType::Maximize: - return "maximize"; - default: - return "err"; - } - })(); - m_imageHolderElement.call<void>("setAttribute", emscripten::val("qt-builtin-image-type"), - emscripten::val(imageType)); - } - - void setVisible(bool visible) - { - m_containerElement["style"].set("display", visible ? "flex" : "none"); - } - - emscripten::val htmlElement() const { return m_containerElement; } - emscripten::val imageElement() const { return m_imageHolderElement; } - -private: - emscripten::val m_containerElement; - emscripten::val m_imageHolderElement; - std::unique_ptr<qstdweb::EventCallback> m_webMouseMoveEventCallback; - std::unique_ptr<qstdweb::EventCallback> m_webMouseDownEventCallback; - std::unique_ptr<qstdweb::EventCallback> m_webClickEventCallback; - - Callbacks m_callbacks; -}; +Q_GUI_EXPORT int qt_defaultDpiX(); -QWasmWindow::QWasmWindow(QWindow *w, QWasmCompositor *compositor, QWasmBackingStore *backingStore) +QWasmWindow::QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport, + QWasmCompositor *compositor, QWasmBackingStore *backingStore) : QPlatformWindow(w), m_window(w), m_compositor(compositor), m_backingStore(backingStore), - m_document(emscripten::val::global("document")), + m_deadKeySupport(deadKeySupport), + m_document(dom::document()), m_qtWindow(m_document.call<emscripten::val>("createElement", emscripten::val("div"))), m_windowContents(m_document.call<emscripten::val>("createElement", emscripten::val("div"))), - m_titleBar(m_document.call<emscripten::val>("createElement", emscripten::val("div"))), - m_label(m_document.call<emscripten::val>("createElement", emscripten::val("div"))), m_canvasContainer(m_document.call<emscripten::val>("createElement", emscripten::val("div"))), + m_a11yContainer(m_document.call<emscripten::val>("createElement", emscripten::val("div"))), m_canvas(m_document.call<emscripten::val>("createElement", emscripten::val("canvas"))) { m_qtWindow.set("className", "qt-window"); m_qtWindow["style"].set("display", std::string("none")); - m_qtWindow.call<void>("appendChild", m_windowContents); - - m_icon = std::make_unique<WebImageButton>(); - m_icon->setImage(IconType::QtLogo); + m_nonClientArea = std::make_unique<NonClientArea>(this, m_qtWindow); + m_nonClientArea->titleBar()->setTitle(window()->title()); - m_titleBar.call<void>("appendChild", m_icon->htmlElement()); - m_titleBar.set("className", "title-bar"); + m_clientArea = std::make_unique<ClientArea>(this, compositor->screen(), m_windowContents); - auto spacer = m_document.call<emscripten::val>("createElement", emscripten::val("div")); - spacer["style"].set("width", "4px"); - m_titleBar.call<void>("appendChild", spacer); - - m_label.set("innerText", emscripten::val(window()->title().toStdString())); - m_label.set("className", "window-name"); - - m_titleBar.call<void>("appendChild", m_label); - - spacer = m_document.call<emscripten::val>("createElement", emscripten::val("div")); - spacer.set("className", "spacer"); - m_titleBar.call<void>("appendChild", spacer); - - m_restore = std::make_unique<WebImageButton>(); - m_restore->setImage(IconType::Restore); - m_restore->setCallbacks(WebImageButton::Callbacks([this]() { onInteraction(); }, - [this]() { onRestoreClicked(); })); - - m_titleBar.call<void>("appendChild", m_restore->htmlElement()); - - m_maximize = std::make_unique<WebImageButton>(); - m_maximize->setImage(IconType::Maximize); - m_maximize->setCallbacks(WebImageButton::Callbacks([this]() { onInteraction(); }, - [this]() { onMaximizeClicked(); })); + m_windowContents.set("className", "qt-window-contents"); + m_qtWindow.call<void>("appendChild", m_windowContents); - m_titleBar.call<void>("appendChild", m_maximize->htmlElement()); + m_canvas["classList"].call<void>("add", emscripten::val("qt-window-content")); - m_close = std::make_unique<WebImageButton>(); - m_close->setImage(IconType::X); - m_close->setCallbacks(WebImageButton::Callbacks([this]() { onInteraction(); }, - [this]() { onCloseClicked(); })); + // Set contenteditable so that the canvas gets clipboard events, + // then hide the resulting focus frame. + m_canvas.set("contentEditable", std::string("true")); + m_canvas["style"].set("outline", std::string("none")); - m_titleBar.call<void>("appendChild", m_close->htmlElement()); + QWasmClipboard::installEventHandlers(m_canvas); - m_windowContents.call<void>("appendChild", m_titleBar); + // set inputMode to none to stop mobile keyboard opening + // when user clicks anywhere on the canvas. + m_canvas.set("inputMode", std::string("none")); - m_canvas["classList"].call<void>("add", emscripten::val("qt-window-content")); + // Hide the canvas from screen readers. + m_canvas.call<void>("setAttribute", std::string("aria-hidden"), std::string("true")); m_windowContents.call<void>("appendChild", m_canvasContainer); m_canvasContainer["classList"].call<void>("add", emscripten::val("qt-window-canvas-container")); m_canvasContainer.call<void>("appendChild", m_canvas); - compositor->screen()->element().call<void>("appendChild", m_qtWindow); + m_canvasContainer.call<void>("appendChild", m_a11yContainer); + m_a11yContainer["classList"].call<void>("add", emscripten::val("qt-window-a11y-container")); const bool rendersTo2dContext = w->surfaceType() != QSurface::OpenGLSurface; if (rendersTo2dContext) @@ -240,51 +103,130 @@ QWasmWindow::QWasmWindow(QWindow *w, QWasmCompositor *compositor, QWasmBackingSt m_qtWindow.set("id", "qt-window-" + std::to_string(m_winId)); emscripten::val::module_property("specialHTMLTargets").set(canvasSelector(), m_canvas); - m_compositor->addWindow(this); + m_flags = window()->flags(); + + const auto pointerCallback = std::function([this](emscripten::val event) { + if (processPointer(*PointerEvent::fromWeb(event))) + event.call<void>("preventDefault"); + }); + + m_pointerEnterCallback = + std::make_unique<qstdweb::EventCallback>(m_qtWindow, "pointerenter", pointerCallback); + m_pointerLeaveCallback = + std::make_unique<qstdweb::EventCallback>(m_qtWindow, "pointerleave", pointerCallback); + + m_wheelEventCallback = std::make_unique<qstdweb::EventCallback>( + m_qtWindow, "wheel", [this](emscripten::val event) { + if (processWheel(*WheelEvent::fromWeb(event))) + event.call<void>("preventDefault"); + }); + + const auto keyCallback = std::function([this](emscripten::val event) { + if (processKey(*KeyEvent::fromWebWithDeadKeyTranslation(event, m_deadKeySupport))) + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + }); + + emscripten::val keyFocusWindow; + if (QWasmIntegration::get()->inputContext()) { + QWasmInputContext *wasmContext = + static_cast<QWasmInputContext *>(QWasmIntegration::get()->inputContext()); + // if there is an touchscreen input context, + // use that window for key input + keyFocusWindow = wasmContext->m_inputElement; + } else { + keyFocusWindow = m_qtWindow; + } + + m_keyDownCallback = + std::make_unique<qstdweb::EventCallback>(keyFocusWindow, "keydown", keyCallback); + m_keyUpCallback = std::make_unique<qstdweb::EventCallback>(keyFocusWindow, "keyup", keyCallback); + + setParent(parent()); } QWasmWindow::~QWasmWindow() { emscripten::val::module_property("specialHTMLTargets").delete_(canvasSelector()); - destroy(); - m_compositor->removeWindow(this); + m_canvasContainer.call<void>("removeChild", m_canvas); + m_context2d = emscripten::val::undefined(); + commitParent(nullptr); if (m_requestAnimationFrameId > -1) emscripten_cancel_animation_frame(m_requestAnimationFrameId); +#if QT_CONFIG(accessibility) + QWasmAccessibility::removeAccessibilityEnableButton(window()); +#endif } -void QWasmWindow::destroy() +QSurfaceFormat QWasmWindow::format() const { - m_qtWindow["parentElement"].call<emscripten::val>("removeChild", m_qtWindow); + return window()->requestedFormat(); +} - m_canvasContainer.call<void>("removeChild", m_canvas); - m_context2d = emscripten::val::undefined(); +QWasmWindow *QWasmWindow::fromWindow(QWindow *window) +{ + return static_cast<QWasmWindow *>(window->handle()); } -void QWasmWindow::initialize() +void QWasmWindow::onRestoreClicked() +{ + window()->setWindowState(Qt::WindowNoState); +} + +void QWasmWindow::onMaximizeClicked() +{ + window()->setWindowState(Qt::WindowMaximized); +} + +void QWasmWindow::onToggleMaximized() +{ + window()->setWindowState(m_state.testFlag(Qt::WindowMaximized) ? Qt::WindowNoState + : Qt::WindowMaximized); +} + +void QWasmWindow::onCloseClicked() { - QRect rect = windowGeometry(); + window()->close(); +} - constexpr int minSizeBoundForDialogsAndRegularWindows = 100; - const int windowType = window()->flags() & Qt::WindowType_Mask; - const int systemMinSizeLowerBound = windowType == Qt::Window || windowType == Qt::Dialog - ? minSizeBoundForDialogsAndRegularWindows - : 0; +void QWasmWindow::onNonClientAreaInteraction() +{ + requestActivateWindow(); + QGuiApplicationPrivate::instance()->closeAllPopups(); +} - const QSize minimumSize(std::max(windowMinimumSize().width(), systemMinSizeLowerBound), - std::max(windowMinimumSize().height(), systemMinSizeLowerBound)); - const QSize maximumSize = windowMaximumSize(); - const QSize targetSize = !rect.isEmpty() ? rect.size() : minimumSize; +bool QWasmWindow::onNonClientEvent(const PointerEvent &event) +{ + QPointF pointInScreen = platformScreen()->mapFromLocal( + dom::mapPoint(event.target(), platformScreen()->element(), event.localPoint)); + return QWindowSystemInterface::handleMouseEvent( + window(), QWasmIntegration::getTimestamp(), window()->mapFromGlobal(pointInScreen), + pointInScreen, event.mouseButtons, event.mouseButton, + MouseEvent::mouseEventTypeFromEventType(event.type, WindowArea::NonClient), + event.modifiers); +} - rect.setWidth(qBound(minimumSize.width(), targetSize.width(), maximumSize.width())); - rect.setHeight(qBound(minimumSize.width(), targetSize.height(), maximumSize.height())); +void QWasmWindow::initialize() +{ + auto initialGeometry = QPlatformWindow::initialGeometry(window(), + windowGeometry(), defaultWindowSize, defaultWindowSize); + m_normalGeometry = initialGeometry; setWindowState(window()->windowStates()); setWindowFlags(window()->flags()); setWindowTitle(window()->title()); + setMask(QHighDpi::toNativeLocalRegion(window()->mask(), window())); + if (window()->isTopLevel()) setWindowIcon(window()->icon()); - m_normalGeometry = rect; QPlatformWindow::setGeometry(m_normalGeometry); + +#if QT_CONFIG(accessibility) + // Add accessibility-enable button. The user can activate this + // button to opt-in to accessibility. + if (window()->isTopLevel()) + QWasmAccessibility::addAccessibilityEnableButton(window()); +#endif } QWasmScreen *QWasmWindow::platformScreen() const @@ -308,6 +250,11 @@ void QWasmWindow::setZOrder(int z) m_qtWindow["style"].set("zIndex", std::to_string(z)); } +void QWasmWindow::setWindowCursor(QByteArray cssCursorName) +{ + m_windowContents["style"].set("cursor", emscripten::val(cssCursorName.constData())); +} + void QWasmWindow::setGeometry(const QRect &rect) { const auto margins = frameMargins(); @@ -318,22 +265,41 @@ void QWasmWindow::setGeometry(const QRect &rect) if (m_state.testFlag(Qt::WindowMaximized)) return platformScreen()->availableGeometry().marginsRemoved(frameMargins()); - const auto screenGeometry = screen()->geometry(); + auto offset = rect.topLeft() - (!parent() ? screen()->geometry().topLeft() : QPoint()); + + // In viewport + auto containerGeometryInViewport = + QRectF::fromDOMRect(parentNode()->containerElement().call<emscripten::val>( + "getBoundingClientRect")) + .toRect(); + + auto rectInViewport = QRect(containerGeometryInViewport.topLeft() + offset, rect.size()); - QRect result(rect); - result.moveTop(std::max(std::min(rect.y(), screenGeometry.bottom()), - screenGeometry.y() + margins.top())); - return result; + QRect cappedGeometry(rectInViewport); + if (!parent()) { + // Clamp top level windows top position to the screen bounds + cappedGeometry.moveTop( + std::max(std::min(rectInViewport.y(), containerGeometryInViewport.bottom()), + containerGeometryInViewport.y() + margins.top())); + } + cappedGeometry.setSize( + cappedGeometry.size().expandedTo(windowMinimumSize()).boundedTo(windowMaximumSize())); + return QRect(QPoint(rect.x(), rect.y() + cappedGeometry.y() - rectInViewport.y()), + rect.size()); })(); + m_nonClientArea->onClientAreaWidthChange(clientAreaRect.width()); + const auto frameRect = clientAreaRect .adjusted(-margins.left(), -margins.top(), margins.right(), margins.bottom()) - .translated(-screen()->geometry().topLeft()); + .translated(!parent() ? -screen()->geometry().topLeft() : QPoint()); m_qtWindow["style"].set("left", std::to_string(frameRect.left()) + "px"); m_qtWindow["style"].set("top", std::to_string(frameRect.top()) + "px"); m_canvasContainer["style"].set("width", std::to_string(clientAreaRect.width()) + "px"); m_canvasContainer["style"].set("height", std::to_string(clientAreaRect.height()) + "px"); + m_a11yContainer["style"].set("width", std::to_string(clientAreaRect.width()) + "px"); + m_a11yContainer["style"].set("height", std::to_string(clientAreaRect.height()) + "px"); // Important for the title flexbox to shrink correctly m_windowContents["style"].set("width", std::to_string(clientAreaRect.width()) + "px"); @@ -364,6 +330,8 @@ void QWasmWindow::setVisible(bool visible) m_compositor->requestUpdateWindow(this, QWasmCompositor::ExposeEventDelivery); m_qtWindow["style"].set("display", visible ? "block" : "none"); + if (window()->isActive()) + m_canvas.call<void>("focus"); if (visible) applyWindowState(); } @@ -375,24 +343,27 @@ bool QWasmWindow::isVisible() const QMargins QWasmWindow::frameMargins() const { - const auto border = borderMargins(); - const auto titleBarBounds = - QRectF::fromDOMRect(m_titleBar.call<emscripten::val>("getBoundingClientRect")); - - return QMarginsF(border.left(), border.top() + titleBarBounds.height(), border.right(), - border.bottom()) + const auto frameRect = + QRectF::fromDOMRect(m_qtWindow.call<emscripten::val>("getBoundingClientRect")); + const auto canvasRect = + QRectF::fromDOMRect(m_windowContents.call<emscripten::val>("getBoundingClientRect")); + return QMarginsF(canvasRect.left() - frameRect.left(), canvasRect.top() - frameRect.top(), + frameRect.right() - canvasRect.right(), + frameRect.bottom() - canvasRect.bottom()) .toMargins(); } void QWasmWindow::raise() { - m_compositor->raise(this); + bringToTop(); invalidate(); + if (QWasmIntegration::get()->inputContext()) + m_canvas.call<void>("focus"); } void QWasmWindow::lower() { - m_compositor->lower(this); + sendToBottom(); invalidate(); } @@ -403,96 +374,14 @@ WId QWasmWindow::winId() const void QWasmWindow::propagateSizeHints() { - QRect rect = windowGeometry(); - if (rect.size().width() < windowMinimumSize().width() - && rect.size().height() < windowMinimumSize().height()) { - rect.setSize(windowMinimumSize()); - setGeometry(rect); - } -} - -bool QWasmWindow::startSystemResize(Qt::Edges edges) -{ - m_compositor->startResize(edges); - - return true; -} - -void QWasmWindow::onRestoreClicked() -{ - window()->setWindowState(Qt::WindowNoState); -} - -void QWasmWindow::onMaximizeClicked() -{ - window()->setWindowState(Qt::WindowMaximized); -} - -void QWasmWindow::onCloseClicked() -{ - window()->close(); -} - -void QWasmWindow::onInteraction() -{ - if (!isActive()) - requestActivateWindow(); -} - -QMarginsF QWasmWindow::borderMargins() const -{ - const auto frameRect = - QRectF::fromDOMRect(m_qtWindow.call<emscripten::val>("getBoundingClientRect")); - const auto canvasRect = - QRectF::fromDOMRect(m_windowContents.call<emscripten::val>("getBoundingClientRect")); - return QMarginsF(canvasRect.left() - frameRect.left(), canvasRect.top() - frameRect.top(), - frameRect.right() - canvasRect.right(), - frameRect.bottom() - canvasRect.bottom()); -} - -QRegion QWasmWindow::resizeRegion() const -{ - QMargins margins = borderMargins().toMargins(); - QRegion result(window()->frameGeometry().marginsAdded(margins)); - result -= window()->frameGeometry().marginsRemoved(margins); - - return result; -} - -bool QWasmWindow::isPointOnTitle(QPoint globalPoint) const -{ - return QRectF::fromDOMRect(m_titleBar.call<emscripten::val>("getBoundingClientRect")) - .contains(globalPoint); + // setGeometry() will take care of minimum and maximum size constraints + setGeometry(windowGeometry()); + m_nonClientArea->propagateSizeHints(); } -bool QWasmWindow::isPointOnResizeRegion(QPoint point) const +void QWasmWindow::setOpacity(qreal level) { - // Certain windows, like undocked dock widgets, are both popups and dialogs. Those should be - // resizable. - if (windowIsPopupType(window()->flags())) - return false; - return (window()->maximumSize().isEmpty() || window()->minimumSize() != window()->maximumSize()) - && resizeRegion().contains(point); -} - -Qt::Edges QWasmWindow::resizeEdgesAtPoint(QPoint point) const -{ - const QPoint topLeft = window()->frameGeometry().topLeft() - QPoint(5, 5); - const QPoint bottomRight = window()->frameGeometry().bottomRight() + QPoint(5, 5); - const int gripAreaWidth = std::min(20, (bottomRight.y() - topLeft.y()) / 2); - - const QRect top(topLeft, QPoint(bottomRight.x(), topLeft.y() + gripAreaWidth)); - const QRect bottom(QPoint(topLeft.x(), bottomRight.y() - gripAreaWidth), bottomRight); - const QRect left(topLeft, QPoint(topLeft.x() + gripAreaWidth, bottomRight.y())); - const QRect right(QPoint(bottomRight.x() - gripAreaWidth, topLeft.y()), bottomRight); - - Q_ASSERT(!top.intersects(bottom)); - Q_ASSERT(!left.intersects(right)); - - Qt::Edges edges(top.contains(point) ? Qt::Edge::TopEdge : Qt::Edge(0)); - edges |= bottom.contains(point) ? Qt::Edge::BottomEdge : Qt::Edge(0); - edges |= left.contains(point) ? Qt::Edge::LeftEdge : Qt::Edge(0); - return edges | (right.contains(point) ? Qt::Edge::RightEdge : Qt::Edge(0)); + m_qtWindow["style"].set("opacity", qBound(0.0, level, 1.0)); } void QWasmWindow::invalidate() @@ -502,17 +391,34 @@ void QWasmWindow::invalidate() void QWasmWindow::onActivationChanged(bool active) { - syncCSSClassWith(m_qtWindow, "inactive", !active); + dom::syncCSSClassWith(m_qtWindow, "inactive", !active); } void QWasmWindow::setWindowFlags(Qt::WindowFlags flags) { + if (flags.testFlag(Qt::WindowStaysOnTopHint) != m_flags.testFlag(Qt::WindowStaysOnTopHint) + || flags.testFlag(Qt::WindowStaysOnBottomHint) + != m_flags.testFlag(Qt::WindowStaysOnBottomHint)) { + onPositionPreferenceChanged(positionPreferenceFromWindowFlags(flags)); + } m_flags = flags; - syncCSSClassWith(m_qtWindow, "has-title-bar", hasTitleBar()); + dom::syncCSSClassWith(m_qtWindow, "frameless", !hasFrame()); + dom::syncCSSClassWith(m_qtWindow, "has-border", hasBorder()); + dom::syncCSSClassWith(m_qtWindow, "has-shadow", hasShadow()); + dom::syncCSSClassWith(m_qtWindow, "has-title", hasTitleBar()); + dom::syncCSSClassWith(m_qtWindow, "transparent-for-input", + flags.testFlag(Qt::WindowTransparentForInput)); + + m_nonClientArea->titleBar()->setMaximizeVisible(hasMaximizeButton()); + m_nonClientArea->titleBar()->setCloseVisible(m_flags.testFlag(Qt::WindowCloseButtonHint)); } void QWasmWindow::setWindowState(Qt::WindowStates newState) { + // Child windows can not have window states other than Qt::WindowActive + if (parent()) + newState &= Qt::WindowActive; + const Qt::WindowStates oldState = m_state; if (newState.testFlag(Qt::WindowMinimized)) { @@ -533,7 +439,7 @@ void QWasmWindow::setWindowState(Qt::WindowStates newState) void QWasmWindow::setWindowTitle(const QString &title) { - m_label.set("innerText", emscripten::val(title.toStdString())); + m_nonClientArea->titleBar()->setTitle(title); } void QWasmWindow::setWindowIcon(const QIcon &icon) @@ -541,14 +447,15 @@ void QWasmWindow::setWindowIcon(const QIcon &icon) const auto dpi = screen()->devicePixelRatio(); auto pixmap = icon.pixmap(10 * dpi, 10 * dpi); if (pixmap.isNull()) { - m_icon->setImage(IconType::QtLogo); + m_nonClientArea->titleBar()->setIcon( + Base64IconStore::get()->getIcon(Base64IconStore::IconType::QtLogo), "svg+xml"); return; } QByteArray bytes; QBuffer buffer(&bytes); pixmap.save(&buffer, "png"); - m_icon->setImage(bytes.toBase64().toStdString(), "png"); + m_nonClientArea->titleBar()->setIcon(bytes.toBase64().toStdString(), "png"); } void QWasmWindow::applyWindowState() @@ -564,16 +471,90 @@ void QWasmWindow::applyWindowState() else newGeom = normalGeometry(); - syncCSSClassWith(m_qtWindow, "has-title-bar", hasTitleBar()); + dom::syncCSSClassWith(m_qtWindow, "has-border", hasBorder()); + dom::syncCSSClassWith(m_qtWindow, "maximized", isMaximized); - m_restore->setVisible(isMaximized); - m_maximize->setVisible(!isMaximized); + m_nonClientArea->titleBar()->setRestoreVisible(isMaximized); + m_nonClientArea->titleBar()->setMaximizeVisible(hasMaximizeButton()); if (isVisible()) QWindowSystemInterface::handleWindowStateChanged(window(), m_state, m_previousWindowState); setGeometry(newGeom); } +void QWasmWindow::commitParent(QWasmWindowTreeNode *parent) +{ + onParentChanged(m_commitedParent, parent, positionPreferenceFromWindowFlags(window()->flags())); + m_commitedParent = parent; +} + +bool QWasmWindow::processKey(const KeyEvent &event) +{ + constexpr bool ProceedToNativeEvent = false; + Q_ASSERT(event.type == EventType::KeyDown || event.type == EventType::KeyUp); + + const auto clipboardResult = + QWasmIntegration::get()->getWasmClipboard()->processKeyboard(event); + + using ProcessKeyboardResult = QWasmClipboard::ProcessKeyboardResult; + if (clipboardResult == ProcessKeyboardResult::NativeClipboardEventNeeded) + return ProceedToNativeEvent; + + const auto result = QWindowSystemInterface::handleKeyEvent( + 0, event.type == EventType::KeyDown ? QEvent::KeyPress : QEvent::KeyRelease, event.key, + event.modifiers, event.text, event.autoRepeat); + return clipboardResult == ProcessKeyboardResult::NativeClipboardEventAndCopiedDataNeeded + ? ProceedToNativeEvent + : result; +} + +bool QWasmWindow::processPointer(const PointerEvent &event) +{ + if (event.pointerType != PointerType::Mouse && event.pointerType != PointerType::Pen) + return false; + + switch (event.type) { + case EventType::PointerEnter: { + const auto pointInScreen = platformScreen()->mapFromLocal( + dom::mapPoint(event.target(), platformScreen()->element(), event.localPoint)); + QWindowSystemInterface::handleEnterEvent( + window(), m_window->mapFromGlobal(pointInScreen), pointInScreen); + break; + } + case EventType::PointerLeave: + QWindowSystemInterface::handleLeaveEvent(window()); + break; + default: + break; + } + + return false; +} + +bool QWasmWindow::processWheel(const WheelEvent &event) +{ + // Web scroll deltas are inverted from Qt deltas - negate. + const int scrollFactor = -([&event]() { + switch (event.deltaMode) { + case DeltaMode::Pixel: + return 1; + case DeltaMode::Line: + return 12; + case DeltaMode::Page: + return 20; + }; + })(); + + const auto pointInScreen = platformScreen()->mapFromLocal( + dom::mapPoint(event.target(), platformScreen()->element(), event.localPoint)); + + return QWindowSystemInterface::handleWheelEvent( + window(), QWasmIntegration::getTimestamp(), window()->mapFromGlobal(pointInScreen), + pointInScreen, (event.delta * scrollFactor).toPoint(), + (event.delta * scrollFactor).toPoint(), event.modifiers, Qt::NoScrollPhase, + Qt::MouseEventNotSynthesized, event.webkitDirectionInvertedFromDevice); +} + QRect QWasmWindow::normalGeometry() const { return m_normalGeometry; @@ -589,10 +570,30 @@ void QWasmWindow::requestUpdate() m_compositor->requestUpdateWindow(this, QWasmCompositor::UpdateRequestDelivery); } +bool QWasmWindow::hasFrame() const +{ + return !m_flags.testFlag(Qt::FramelessWindowHint); +} + +bool QWasmWindow::hasBorder() const +{ + return hasFrame() && !m_state.testFlag(Qt::WindowFullScreen) && !m_flags.testFlag(Qt::SubWindow) + && !windowIsPopupType(m_flags) && !parent(); +} + bool QWasmWindow::hasTitleBar() const { - return !m_state.testFlag(Qt::WindowFullScreen) && m_flags.testFlag(Qt::WindowTitleHint) - && !windowIsPopupType(m_flags); + return hasBorder() && m_flags.testFlag(Qt::WindowTitleHint); +} + +bool QWasmWindow::hasShadow() const +{ + return hasBorder() && !m_flags.testFlag(Qt::NoDropShadowWindowHint); +} + +bool QWasmWindow::hasMaximizeButton() const +{ + return !m_state.testFlag(Qt::WindowMaximized) && m_flags.testFlag(Qt::WindowMaximizeButtonHint); } bool QWasmWindow::windowIsPopupType(Qt::WindowFlags flags) const @@ -611,18 +612,19 @@ void QWasmWindow::requestActivateWindow() return; } - if (window()->isTopLevel()) - raise(); + raise(); + setAsActiveNode(); + + if (!QWasmIntegration::get()->inputContext()) + m_canvas.call<void>("focus"); + QPlatformWindow::requestActivateWindow(); } bool QWasmWindow::setMouseGrabEnabled(bool grab) { - if (grab) - m_compositor->setCapture(this); - else - m_compositor->releaseCapture(); - return true; + Q_UNUSED(grab); + return false; } bool QWasmWindow::windowEvent(QEvent *event) @@ -639,9 +641,61 @@ bool QWasmWindow::windowEvent(QEvent *event) } } +void QWasmWindow::setMask(const QRegion ®ion) +{ + if (region.isEmpty()) { + m_qtWindow["style"].set("clipPath", emscripten::val("")); + return; + } + + std::ostringstream cssClipPath; + cssClipPath << "path('"; + for (const auto &rect : region) { + const auto cssRect = rect.adjusted(0, 0, 1, 1); + cssClipPath << "M " << cssRect.left() << " " << cssRect.top() << " "; + cssClipPath << "L " << cssRect.right() << " " << cssRect.top() << " "; + cssClipPath << "L " << cssRect.right() << " " << cssRect.bottom() << " "; + cssClipPath << "L " << cssRect.left() << " " << cssRect.bottom() << " z "; + } + cssClipPath << "')"; + m_qtWindow["style"].set("clipPath", emscripten::val(cssClipPath.str())); +} + +void QWasmWindow::setParent(const QPlatformWindow *) +{ + commitParent(parentNode()); +} + std::string QWasmWindow::canvasSelector() const { return "!qtwindow" + std::to_string(m_winId); } +emscripten::val QWasmWindow::containerElement() +{ + return m_windowContents; +} + +QWasmWindowTreeNode *QWasmWindow::parentNode() +{ + if (parent()) + return static_cast<QWasmWindow *>(parent()); + return platformScreen(); +} + +QWasmWindow *QWasmWindow::asWasmWindow() +{ + return this; +} + +void QWasmWindow::onParentChanged(QWasmWindowTreeNode *previous, QWasmWindowTreeNode *current, + QWasmWindowStack::PositionPreference positionPreference) +{ + if (previous) + previous->containerElement().call<void>("removeChild", m_qtWindow); + if (current) + current->containerElement().call<void>("appendChild", m_qtWindow); + QWasmWindowTreeNode::onParentChanged(previous, current, positionPreference); +} + QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmwindow.h b/src/plugins/platforms/wasm/qwasmwindow.h index 183f68a311..ab0dc68e83 100644 --- a/src/plugins/platforms/wasm/qwasmwindow.h +++ b/src/plugins/platforms/wasm/qwasmwindow.h @@ -6,10 +6,14 @@ #include "qwasmintegration.h" #include <qpa/qplatformwindow.h> +#include <qpa/qplatformwindow_p.h> #include <emscripten/html5.h> #include "qwasmbackingstore.h" #include "qwasmscreen.h" #include "qwasmcompositor.h" +#include "qwasmwindownonclientarea.h" +#include "qwasmwindowstack.h" +#include "qwasmwindowtreenode.h" #include <QtCore/private/qstdweb_p.h> #include "QtGui/qopenglcontext.h" @@ -17,101 +21,148 @@ #include <emscripten/val.h> +#include <memory> + QT_BEGIN_NAMESPACE -class QWasmWindow final : public QPlatformWindow +namespace qstdweb { +class EventCallback; +} + +class ClientArea; +struct KeyEvent; +struct PointerEvent; +class QWasmDeadKeySupport; +struct WheelEvent; + +class QWasmWindow final : public QPlatformWindow, + public QWasmWindowTreeNode, + public QNativeInterface::Private::QWasmWindow { public: - QWasmWindow(QWindow *w, QWasmCompositor *compositor, QWasmBackingStore *backingStore); + QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport, QWasmCompositor *compositor, + QWasmBackingStore *backingStore); ~QWasmWindow() final; - void destroy(); - void initialize() override; + static QWasmWindow *fromWindow(QWindow *window); + QSurfaceFormat format() const override; void paint(); void setZOrder(int order); + void setWindowCursor(QByteArray cssCursorName); void onActivationChanged(bool active); + bool isVisible() const; + void onNonClientAreaInteraction(); + void onRestoreClicked(); + void onMaximizeClicked(); + void onToggleMaximized(); + void onCloseClicked(); + bool onNonClientEvent(const PointerEvent &event); + + // QPlatformWindow: + void initialize() override; void setGeometry(const QRect &) override; void setVisible(bool visible) override; - bool isVisible() const; QMargins frameMargins() const override; - WId winId() const override; - void propagateSizeHints() override; + void setOpacity(qreal level) override; void raise() override; void lower() override; QRect normalGeometry() const override; qreal devicePixelRatio() const override; void requestUpdate() override; void requestActivateWindow() override; - - QWasmScreen *platformScreen() const; - void setBackingStore(QWasmBackingStore *store) { m_backingStore = store; } - QWasmBackingStore *backingStore() const { return m_backingStore; } - QWindow *window() const { return m_window; } - - bool startSystemResize(Qt::Edges edges) final; - - bool isPointOnTitle(QPoint point) const; - bool isPointOnResizeRegion(QPoint point) const; - - Qt::Edges resizeEdgesAtPoint(QPoint point) const; - void setWindowFlags(Qt::WindowFlags flags) override; void setWindowState(Qt::WindowStates state) override; void setWindowTitle(const QString &title) override; void setWindowIcon(const QIcon &icon) override; - void applyWindowState(); bool setKeyboardGrabEnabled(bool) override { return false; } bool setMouseGrabEnabled(bool grab) final; bool windowEvent(QEvent *event) final; + void setMask(const QRegion ®ion) final; + void setParent(const QPlatformWindow *window) final; + + QWasmScreen *platformScreen() const; + void setBackingStore(QWasmBackingStore *store) { m_backingStore = store; } + QWasmBackingStore *backingStore() const { return m_backingStore; } + QWindow *window() const { return m_window; } std::string canvasSelector() const; - emscripten::val context2d() { return m_context2d; } -private: - friend class QWasmScreen; + emscripten::val context2d() const { return m_context2d; } + emscripten::val a11yContainer() const { return m_a11yContainer; } + emscripten::val inputHandlerElement() const { return m_windowContents; } - class WebImageButton; + // QNativeInterface::Private::QWasmWindow + emscripten::val document() const override { return m_document; } + emscripten::val clientArea() const override { return m_qtWindow; } - QMarginsF borderMargins() const; - QRegion resizeRegion() const; + // QWasmWindowTreeNode: + emscripten::val containerElement() final; + QWasmWindowTreeNode *parentNode() final; - void onRestoreClicked(); - void onMaximizeClicked(); - void onCloseClicked(); - void onInteraction(); +private: + friend class QWasmScreen; + static constexpr auto defaultWindowSize = 160; + + // QWasmWindowTreeNode: + QWasmWindow *asWasmWindow() final; + void onParentChanged(QWasmWindowTreeNode *previous, QWasmWindowTreeNode *current, + QWasmWindowStack::PositionPreference positionPreference) final; void invalidate(); + bool hasFrame() const; bool hasTitleBar() const; + bool hasBorder() const; + bool hasShadow() const; + bool hasMaximizeButton() const; + void applyWindowState(); + void commitParent(QWasmWindowTreeNode *parent); + + bool processKey(const KeyEvent &event); + bool processPointer(const PointerEvent &event); + bool processWheel(const WheelEvent &event); QWindow *m_window = nullptr; QWasmCompositor *m_compositor = nullptr; QWasmBackingStore *m_backingStore = nullptr; + QWasmDeadKeySupport *m_deadKeySupport; QRect m_normalGeometry {0, 0, 0 ,0}; emscripten::val m_document; emscripten::val m_qtWindow; emscripten::val m_windowContents; - emscripten::val m_titleBar; - emscripten::val m_label; emscripten::val m_canvasContainer; + emscripten::val m_a11yContainer; emscripten::val m_canvas; emscripten::val m_context2d = emscripten::val::undefined(); - std::unique_ptr<WebImageButton> m_close; - std::unique_ptr<WebImageButton> m_maximize; - std::unique_ptr<WebImageButton> m_restore; - std::unique_ptr<WebImageButton> m_icon; + std::unique_ptr<NonClientArea> m_nonClientArea; + std::unique_ptr<ClientArea> m_clientArea; + + QWasmWindowTreeNode *m_commitedParent = nullptr; + + std::unique_ptr<qstdweb::EventCallback> m_keyDownCallback; + std::unique_ptr<qstdweb::EventCallback> m_keyUpCallback; + + std::unique_ptr<qstdweb::EventCallback> m_pointerLeaveCallback; + std::unique_ptr<qstdweb::EventCallback> m_pointerEnterCallback; + + std::unique_ptr<qstdweb::EventCallback> m_dropCallback; + + std::unique_ptr<qstdweb::EventCallback> m_wheelEventCallback; Qt::WindowStates m_state = Qt::WindowNoState; Qt::WindowStates m_previousWindowState = Qt::WindowNoState; Qt::WindowFlags m_flags = Qt::Widget; + QPoint m_lastPointerMovePoint; + WId m_winId = 0; + bool m_wantCapture = false; bool m_hasTitle = false; bool m_needsCompositor = false; long m_requestAnimationFrameId = -1; diff --git a/src/plugins/platforms/wasm/qwasmwindowclientarea.cpp b/src/plugins/platforms/wasm/qwasmwindowclientarea.cpp new file mode 100644 index 0000000000..6da3e24c05 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmwindowclientarea.cpp @@ -0,0 +1,195 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmwindowclientarea.h" + +#include "qwasmdom.h" +#include "qwasmevent.h" +#include "qwasmscreen.h" +#include "qwasmwindow.h" +#include "qwasmdrag.h" + +#include <QtGui/private/qguiapplication_p.h> +#include <QtGui/qpointingdevice.h> + +#include <QtCore/qassert.h> + +QT_BEGIN_NAMESPACE + +ClientArea::ClientArea(QWasmWindow *window, QWasmScreen *screen, emscripten::val element) + : m_screen(screen), m_window(window), m_element(element) +{ + const auto callback = std::function([this](emscripten::val event) { + processPointer(*PointerEvent::fromWeb(event)); + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + }); + + m_pointerDownCallback = + std::make_unique<qstdweb::EventCallback>(element, "pointerdown", callback); + m_pointerMoveCallback = + std::make_unique<qstdweb::EventCallback>(element, "pointermove", callback); + m_pointerUpCallback = std::make_unique<qstdweb::EventCallback>(element, "pointerup", callback); + m_pointerCancelCallback = + std::make_unique<qstdweb::EventCallback>(element, "pointercancel", callback); + + element.call<void>("setAttribute", emscripten::val("draggable"), emscripten::val("true")); + + m_dragStartCallback = std::make_unique<qstdweb::EventCallback>( + element, "dragstart", [this](emscripten::val webEvent) { + webEvent.call<void>("preventDefault"); + auto event = *DragEvent::fromWeb(webEvent, m_window->window()); + QWasmDrag::instance()->onNativeDragStarted(&event); + }); + m_dragOverCallback = std::make_unique<qstdweb::EventCallback>( + element, "dragover", [this](emscripten::val webEvent) { + webEvent.call<void>("preventDefault"); + auto event = *DragEvent::fromWeb(webEvent, m_window->window()); + QWasmDrag::instance()->onNativeDragOver(&event); + }); + m_dropCallback = std::make_unique<qstdweb::EventCallback>( + element, "drop", [this](emscripten::val webEvent) { + webEvent.call<void>("preventDefault"); + auto event = *DragEvent::fromWeb(webEvent, m_window->window()); + QWasmDrag::instance()->onNativeDrop(&event); + }); + m_dragEndCallback = std::make_unique<qstdweb::EventCallback>( + element, "dragend", [this](emscripten::val webEvent) { + webEvent.call<void>("preventDefault"); + auto event = *DragEvent::fromWeb(webEvent, m_window->window()); + QWasmDrag::instance()->onNativeDragFinished(&event); + }); + +} + +bool ClientArea::processPointer(const PointerEvent &event) +{ + + switch (event.type) { + case EventType::PointerDown: + m_element.call<void>("setPointerCapture", event.pointerId); + if ((m_window->window()->flags() & Qt::WindowDoesNotAcceptFocus) + != Qt::WindowDoesNotAcceptFocus + && m_window->window()->isTopLevel()) + m_window->window()->requestActivate(); + break; + case EventType::PointerUp: + m_element.call<void>("releasePointerCapture", event.pointerId); + break; + default: + break; + }; + + const bool eventAccepted = deliverEvent(event); + if (!eventAccepted && event.type == EventType::PointerDown) + QGuiApplicationPrivate::instance()->closeAllPopups(); + return eventAccepted; +} + +bool ClientArea::deliverEvent(const PointerEvent &event) +{ + const auto pointInScreen = m_screen->mapFromLocal( + dom::mapPoint(event.target(), m_screen->element(), event.localPoint)); + + const auto geometryF = m_screen->geometry().toRectF(); + const QPointF targetPointClippedToScreen( + qBound(geometryF.left(), pointInScreen.x(), geometryF.right()), + qBound(geometryF.top(), pointInScreen.y(), geometryF.bottom())); + + if (event.pointerType == PointerType::Mouse) { + const QEvent::Type eventType = + MouseEvent::mouseEventTypeFromEventType(event.type, WindowArea::Client); + + return eventType != QEvent::None + && QWindowSystemInterface::handleMouseEvent( + m_window->window(), QWasmIntegration::getTimestamp(), + m_window->window()->mapFromGlobal(targetPointClippedToScreen), + targetPointClippedToScreen, event.mouseButtons, event.mouseButton, + eventType, event.modifiers); + } + + if (event.pointerType == PointerType::Pen) { + qreal pressure; + switch (event.type) { + case EventType::PointerDown : + case EventType::PointerMove : + pressure = event.pressure; + break; + case EventType::PointerUp : + pressure = 0.0; + break; + default: + return false; + } + // Tilt in the browser is in the range +-90, but QTabletEvent only goes to +-60. + qreal xTilt = qBound(-60.0, event.tiltX, 60.0); + qreal yTilt = qBound(-60.0, event.tiltY, 60.0); + // Barrel rotation is reported as 0 to 359, but QTabletEvent wants a signed value. + qreal rotation = event.twist > 180.0 ? 360.0 - event.twist : event.twist; + return QWindowSystemInterface::handleTabletEvent( + m_window->window(), QWasmIntegration::getTimestamp(), m_screen->tabletDevice(), + m_window->window()->mapFromGlobal(targetPointClippedToScreen), + targetPointClippedToScreen, event.mouseButtons, pressure, xTilt, yTilt, + event.tangentialPressure, rotation, event.modifiers); + } + + QWindowSystemInterface::TouchPoint *touchPoint; + + QPointF pointInTargetWindowCoords = + QPointF(m_window->window()->mapFromGlobal(targetPointClippedToScreen)); + QPointF normalPosition(pointInTargetWindowCoords.x() / m_window->window()->width(), + pointInTargetWindowCoords.y() / m_window->window()->height()); + + const auto tp = m_pointerIdToTouchPoints.find(event.pointerId); + if (event.pointerType != PointerType::Pen && tp != m_pointerIdToTouchPoints.end()) { + touchPoint = &tp.value(); + } else { + touchPoint = &m_pointerIdToTouchPoints + .insert(event.pointerId, QWindowSystemInterface::TouchPoint()) + .value(); + + // Assign touch point id. TouchPoint::id is int, but QGuiApplicationPrivate::processTouchEvent() + // will not synthesize mouse events for touch points with negative id; use the absolute value for + // the touch point id. + touchPoint->id = qAbs(event.pointerId); + + touchPoint->state = QEventPoint::State::Pressed; + } + + const bool stationaryTouchPoint = (normalPosition == touchPoint->normalPosition); + touchPoint->normalPosition = normalPosition; + touchPoint->area = QRectF(targetPointClippedToScreen, QSizeF(event.width, event.height)) + .translated(-event.width / 2, -event.height / 2); + touchPoint->pressure = event.pressure; + + switch (event.type) { + case EventType::PointerUp: + touchPoint->state = QEventPoint::State::Released; + break; + case EventType::PointerMove: + touchPoint->state = (stationaryTouchPoint ? QEventPoint::State::Stationary + : QEventPoint::State::Updated); + break; + default: + break; + } + + QList<QWindowSystemInterface::TouchPoint> touchPointList; + touchPointList.reserve(m_pointerIdToTouchPoints.size()); + std::transform(m_pointerIdToTouchPoints.begin(), m_pointerIdToTouchPoints.end(), + std::back_inserter(touchPointList), + [](const QWindowSystemInterface::TouchPoint &val) { return val; }); + + if (event.type == EventType::PointerUp) + m_pointerIdToTouchPoints.remove(event.pointerId); + + return event.type == EventType::PointerCancel + ? QWindowSystemInterface::handleTouchCancelEvent( + m_window->window(), QWasmIntegration::getTimestamp(), m_screen->touchDevice(), + event.modifiers) + : QWindowSystemInterface::handleTouchEvent( + m_window->window(), QWasmIntegration::getTimestamp(), m_screen->touchDevice(), + touchPointList, event.modifiers); +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmwindowclientarea.h b/src/plugins/platforms/wasm/qwasmwindowclientarea.h new file mode 100644 index 0000000000..ba745a59a8 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmwindowclientarea.h @@ -0,0 +1,52 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMWINDOWCLIENTAREA_H +#define QWASMWINDOWCLIENTAREA_H + +#include <QtCore/qnamespace.h> +#include <qpa/qwindowsysteminterface.h> +#include <QtCore/QMap> + +#include <emscripten/val.h> + +#include <memory> + +QT_BEGIN_NAMESPACE + +namespace qstdweb { +class EventCallback; +} + +struct PointerEvent; +class QWasmScreen; +class QWasmWindow; + +class ClientArea +{ +public: + ClientArea(QWasmWindow *window, QWasmScreen *screen, emscripten::val element); + +private: + bool processPointer(const PointerEvent &event); + bool deliverEvent(const PointerEvent &event); + + std::unique_ptr<qstdweb::EventCallback> m_pointerDownCallback; + std::unique_ptr<qstdweb::EventCallback> m_pointerMoveCallback; + std::unique_ptr<qstdweb::EventCallback> m_pointerUpCallback; + std::unique_ptr<qstdweb::EventCallback> m_pointerCancelCallback; + + std::unique_ptr<qstdweb::EventCallback> m_dragOverCallback; + std::unique_ptr<qstdweb::EventCallback> m_dragStartCallback; + std::unique_ptr<qstdweb::EventCallback> m_dragEndCallback; + std::unique_ptr<qstdweb::EventCallback> m_dropCallback; + + QMap<int, QWindowSystemInterface::TouchPoint> m_pointerIdToTouchPoints; + + QWasmScreen *m_screen; + QWasmWindow *m_window; + emscripten::val m_element; +}; + +QT_END_NAMESPACE +#endif // QWASMWINDOWNONCLIENTAREA_H diff --git a/src/plugins/platforms/wasm/qwasmwindownonclientarea.cpp b/src/plugins/platforms/wasm/qwasmwindownonclientarea.cpp new file mode 100644 index 0000000000..00fa8fb236 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmwindownonclientarea.cpp @@ -0,0 +1,460 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmwindownonclientarea.h" + +#include "qwasmbase64iconstore.h" +#include "qwasmdom.h" +#include "qwasmevent.h" +#include "qwasmintegration.h" + +#include <qpa/qwindowsysteminterface.h> + +#include <QtCore/qassert.h> + +QT_BEGIN_NAMESPACE + +WebImageButton::Callbacks::Callbacks() = default; +WebImageButton::Callbacks::Callbacks(std::function<void()> onInteraction, + std::function<void()> onClick) + : m_onInteraction(std::move(onInteraction)), m_onClick(std::move(onClick)) +{ + Q_ASSERT_X(!!m_onInteraction == !!m_onClick, Q_FUNC_INFO, + "Both callbacks need to be either null or non-null"); +} +WebImageButton::Callbacks::~Callbacks() = default; + +WebImageButton::Callbacks::Callbacks(Callbacks &&) = default; +WebImageButton::Callbacks &WebImageButton::Callbacks::operator=(Callbacks &&) = default; + +void WebImageButton::Callbacks::onInteraction() +{ + return m_onInteraction(); +} + +void WebImageButton::Callbacks::onClick() +{ + return m_onClick(); +} + +WebImageButton::WebImageButton() + : m_containerElement( + dom::document().call<emscripten::val>("createElement", emscripten::val("div"))), + m_imgElement(dom::document().call<emscripten::val>("createElement", emscripten::val("img"))) +{ + m_imgElement.set("draggable", false); + + m_containerElement["classList"].call<void>("add", emscripten::val("image-button")); + m_containerElement.call<void>("appendChild", m_imgElement); +} + +WebImageButton::~WebImageButton() = default; + +void WebImageButton::setCallbacks(Callbacks callbacks) +{ + if (callbacks) { + if (!m_webClickEventCallback) { + m_webMouseDownEventCallback = std::make_unique<qstdweb::EventCallback>( + m_containerElement, "pointerdown", [this](emscripten::val event) { + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + m_callbacks.onInteraction(); + }); + m_webClickEventCallback = std::make_unique<qstdweb::EventCallback>( + m_containerElement, "click", [this](emscripten::val event) { + m_callbacks.onClick(); + event.call<void>("stopPropagation"); + }); + } + } else { + m_webMouseDownEventCallback.reset(); + m_webClickEventCallback.reset(); + } + dom::syncCSSClassWith(m_containerElement, "action-button", !!callbacks); + m_callbacks = std::move(callbacks); +} + +void WebImageButton::setImage(std::string_view imageData, std::string_view format) +{ + m_imgElement.set("src", + "data:image/" + std::string(format) + ";base64," + std::string(imageData)); +} + +void WebImageButton::setVisible(bool visible) +{ + m_containerElement["style"].set("display", visible ? "flex" : "none"); +} + +Resizer::ResizerElement::ResizerElement(emscripten::val parentElement, Qt::Edges edges, + Resizer *resizer) + : m_element(dom::document().call<emscripten::val>("createElement", emscripten::val("div"))), + m_edges(edges), + m_resizer(resizer) +{ + Q_ASSERT_X(m_resizer, Q_FUNC_INFO, "Resizer cannot be null"); + + m_element["classList"].call<void>("add", emscripten::val("resize-outline")); + m_element["classList"].call<void>("add", emscripten::val(cssClassNameForEdges(edges))); + + parentElement.call<void>("appendChild", m_element); + + m_mouseDownEvent = std::make_unique<qstdweb::EventCallback>( + m_element, "pointerdown", [this](emscripten::val event) { + if (!onPointerDown(*PointerEvent::fromWeb(event))) + return; + m_resizer->onInteraction(); + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + }); + m_mouseMoveEvent = std::make_unique<qstdweb::EventCallback>( + m_element, "pointermove", [this](emscripten::val event) { + if (onPointerMove(*PointerEvent::fromWeb(event))) + event.call<void>("preventDefault"); + }); + m_mouseUpEvent = std::make_unique<qstdweb::EventCallback>( + m_element, "pointerup", [this](emscripten::val event) { + if (onPointerUp(*PointerEvent::fromWeb(event))) { + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + } + }); +} + +Resizer::ResizerElement::~ResizerElement() +{ + m_element["parentElement"].call<emscripten::val>("removeChild", m_element); +} + +Resizer::ResizerElement::ResizerElement(ResizerElement &&other) = default; + +bool Resizer::ResizerElement::onPointerDown(const PointerEvent &event) +{ + m_element.call<void>("setPointerCapture", event.pointerId); + m_capturedPointerId = event.pointerId; + + m_resizer->startResize(m_edges, event); + return true; +} + +bool Resizer::ResizerElement::onPointerMove(const PointerEvent &event) +{ + if (m_capturedPointerId != event.pointerId) + return false; + + m_resizer->continueResize(event); + return true; +} + +bool Resizer::ResizerElement::onPointerUp(const PointerEvent &event) +{ + if (m_capturedPointerId != event.pointerId) + return false; + + m_resizer->finishResize(); + m_element.call<void>("releasePointerCapture", event.pointerId); + m_capturedPointerId = -1; + return true; +} + +Resizer::Resizer(QWasmWindow *window, emscripten::val parentElement) + : m_window(window), m_windowElement(parentElement) +{ + Q_ASSERT_X(m_window, Q_FUNC_INFO, "Window must not be null"); + + constexpr std::array<int, 8> ResizeEdges = { Qt::TopEdge | Qt::LeftEdge, + Qt::TopEdge, + Qt::TopEdge | Qt::RightEdge, + Qt::LeftEdge, + Qt::RightEdge, + Qt::BottomEdge | Qt::LeftEdge, + Qt::BottomEdge, + Qt::BottomEdge | Qt::RightEdge }; + std::transform(std::begin(ResizeEdges), std::end(ResizeEdges), std::back_inserter(m_elements), + [parentElement, this](int edges) { + return std::make_unique<ResizerElement>(parentElement, + Qt::Edges::fromInt(edges), this); + }); +} + +Resizer::~Resizer() = default; + +ResizeConstraints Resizer::getResizeConstraints() { + const auto *window = m_window->window(); + const auto minShrink = QPoint(window->minimumWidth() - window->geometry().width(), + window->minimumHeight() - window->geometry().height()); + const auto maxGrow = QPoint(window->maximumWidth() - window->geometry().width(), + window->maximumHeight() - window->geometry().height()); + + const auto frameRect = + QRectF::fromDOMRect(m_windowElement.call<emscripten::val>("getBoundingClientRect")); + auto containerGeometry = + QRectF::fromDOMRect(m_window->parentNode()->containerElement().call<emscripten::val>( + "getBoundingClientRect")); + + const int maxGrowTop = frameRect.top() - containerGeometry.top(); + + return ResizeConstraints{minShrink, maxGrow, maxGrowTop}; +} + +void Resizer::onInteraction() +{ + m_window->onNonClientAreaInteraction(); +} + +void Resizer::startResize(Qt::Edges resizeEdges, const PointerEvent &event) +{ + Q_ASSERT_X(!m_currentResizeData, Q_FUNC_INFO, "Another resize in progress"); + + m_currentResizeData.reset(new ResizeData{ + .edges = resizeEdges, + .originInScreenCoords = dom::mapPoint( + event.target(), m_window->platformScreen()->element(), event.localPoint), + }); + + const auto resizeConstraints = getResizeConstraints(); + m_currentResizeData->minShrink = resizeConstraints.minShrink; + + m_currentResizeData->maxGrow = + QPoint(resizeConstraints.maxGrow.x(), + std::min(resizeEdges & Qt::Edge::TopEdge ? resizeConstraints.maxGrowTop : INT_MAX, + resizeConstraints.maxGrow.y())); + + m_currentResizeData->initialBounds = m_window->window()->geometry(); +} + +void Resizer::continueResize(const PointerEvent &event) +{ + const auto pointInScreen = + dom::mapPoint(event.target(), m_window->platformScreen()->element(), event.localPoint); + const auto amount = (pointInScreen - m_currentResizeData->originInScreenCoords).toPoint(); + const QPoint cappedGrowVector( + std::min(m_currentResizeData->maxGrow.x(), + std::max(m_currentResizeData->minShrink.x(), + (m_currentResizeData->edges & Qt::Edge::LeftEdge) ? -amount.x() + : (m_currentResizeData->edges & Qt::Edge::RightEdge) + ? amount.x() + : 0)), + std::min(m_currentResizeData->maxGrow.y(), + std::max(m_currentResizeData->minShrink.y(), + (m_currentResizeData->edges & Qt::Edge::TopEdge) ? -amount.y() + : (m_currentResizeData->edges & Qt::Edge::BottomEdge) + ? amount.y() + : 0))); + + auto bounds = m_currentResizeData->initialBounds.adjusted( + (m_currentResizeData->edges & Qt::Edge::LeftEdge) ? -cappedGrowVector.x() : 0, + (m_currentResizeData->edges & Qt::Edge::TopEdge) ? -cappedGrowVector.y() : 0, + (m_currentResizeData->edges & Qt::Edge::RightEdge) ? cappedGrowVector.x() : 0, + (m_currentResizeData->edges & Qt::Edge::BottomEdge) ? cappedGrowVector.y() : 0); + + m_window->window()->setGeometry(bounds); +} + +void Resizer::finishResize() +{ + Q_ASSERT_X(m_currentResizeData, Q_FUNC_INFO, "No resize in progress"); + m_currentResizeData.reset(); +} + +TitleBar::TitleBar(QWasmWindow *window, emscripten::val parentElement) + : m_window(window), + m_element(dom::document().call<emscripten::val>("createElement", emscripten::val("div"))), + m_label(dom::document().call<emscripten::val>("createElement", emscripten::val("div"))) +{ + m_icon = std::make_unique<WebImageButton>(); + m_icon->setImage(Base64IconStore::get()->getIcon(Base64IconStore::IconType::QtLogo), "svg+xml"); + m_element.call<void>("appendChild", m_icon->htmlElement()); + m_element.set("className", "title-bar"); + + auto spacer = dom::document().call<emscripten::val>("createElement", emscripten::val("div")); + spacer["style"].set("width", "4px"); + m_element.call<void>("appendChild", spacer); + + m_label.set("className", "window-name"); + + m_element.call<void>("appendChild", m_label); + + spacer = dom::document().call<emscripten::val>("createElement", emscripten::val("div")); + spacer.set("className", "spacer"); + m_element.call<void>("appendChild", spacer); + + m_restore = std::make_unique<WebImageButton>(); + m_restore->setImage(Base64IconStore::get()->getIcon(Base64IconStore::IconType::Restore), + "svg+xml"); + m_restore->setCallbacks( + WebImageButton::Callbacks([this]() { m_window->onNonClientAreaInteraction(); }, + [this]() { m_window->onRestoreClicked(); })); + + m_element.call<void>("appendChild", m_restore->htmlElement()); + + m_maximize = std::make_unique<WebImageButton>(); + m_maximize->setImage(Base64IconStore::get()->getIcon(Base64IconStore::IconType::Maximize), + "svg+xml"); + m_maximize->setCallbacks( + WebImageButton::Callbacks([this]() { m_window->onNonClientAreaInteraction(); }, + [this]() { m_window->onMaximizeClicked(); })); + + m_element.call<void>("appendChild", m_maximize->htmlElement()); + + m_close = std::make_unique<WebImageButton>(); + m_close->setImage(Base64IconStore::get()->getIcon(Base64IconStore::IconType::X), "svg+xml"); + m_close->setCallbacks( + WebImageButton::Callbacks([this]() { m_window->onNonClientAreaInteraction(); }, + [this]() { m_window->onCloseClicked(); })); + + m_element.call<void>("appendChild", m_close->htmlElement()); + + parentElement.call<void>("appendChild", m_element); + + m_mouseDownEvent = std::make_unique<qstdweb::EventCallback>( + m_element, "pointerdown", [this](emscripten::val event) { + if (!onPointerDown(*PointerEvent::fromWeb(event))) + return; + m_window->onNonClientAreaInteraction(); + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + }); + m_mouseMoveEvent = std::make_unique<qstdweb::EventCallback>( + m_element, "pointermove", [this](emscripten::val event) { + if (onPointerMove(*PointerEvent::fromWeb(event))) { + event.call<void>("preventDefault"); + } + }); + m_mouseUpEvent = std::make_unique<qstdweb::EventCallback>( + m_element, "pointerup", [this](emscripten::val event) { + if (onPointerUp(*PointerEvent::fromWeb(event))) { + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + } + }); + m_doubleClickEvent = std::make_unique<qstdweb::EventCallback>( + m_element, "dblclick", [this](emscripten::val event) { + if (onDoubleClick()) { + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + } + }); +} + +TitleBar::~TitleBar() +{ + m_element["parentElement"].call<emscripten::val>("removeChild", m_element); +} + +void TitleBar::setTitle(const QString &title) +{ + m_label.set("innerText", emscripten::val(title.toStdString())); +} + +void TitleBar::setRestoreVisible(bool visible) +{ + m_restore->setVisible(visible); +} + +void TitleBar::setMaximizeVisible(bool visible) +{ + m_maximize->setVisible(visible); +} + +void TitleBar::setCloseVisible(bool visible) +{ + m_close->setVisible(visible); +} + +void TitleBar::setIcon(std::string_view imageData, std::string_view format) +{ + m_icon->setImage(imageData, format); +} + +void TitleBar::setWidth(int width) +{ + m_element["style"].set("width", std::to_string(width) + "px"); +} + +QRectF TitleBar::geometry() const +{ + return QRectF::fromDOMRect(m_element.call<emscripten::val>("getBoundingClientRect")); +} + +bool TitleBar::onPointerDown(const PointerEvent &event) +{ + m_element.call<void>("setPointerCapture", event.pointerId); + m_capturedPointerId = event.pointerId; + + m_moveStartWindowPosition = m_window->window()->position(); + m_moveStartPoint = clipPointWithScreen(event.localPoint); + m_window->onNonClientEvent(event); + return true; +} + +bool TitleBar::onPointerMove(const PointerEvent &event) +{ + if (m_capturedPointerId != event.pointerId) + return false; + + const QPoint delta = (clipPointWithScreen(event.localPoint) - m_moveStartPoint).toPoint(); + + m_window->window()->setPosition(m_moveStartWindowPosition + delta); + m_window->onNonClientEvent(event); + return true; +} + +bool TitleBar::onPointerUp(const PointerEvent &event) +{ + if (m_capturedPointerId != event.pointerId) + return false; + + m_element.call<void>("releasePointerCapture", event.pointerId); + m_capturedPointerId = -1; + m_window->onNonClientEvent(event); + return true; +} + +bool TitleBar::onDoubleClick() +{ + m_window->onToggleMaximized(); + return true; +} + +QPointF TitleBar::clipPointWithScreen(const QPointF &pointInTitleBarCoords) const +{ + auto containerRect = + QRectF::fromDOMRect(m_window->parentNode()->containerElement().call<emscripten::val>( + "getBoundingClientRect")); + const auto p = dom::mapPoint(m_element, m_window->parentNode()->containerElement(), + pointInTitleBarCoords); + + auto result = QPointF(qBound(0., qreal(p.x()), containerRect.width()), + qBound(0., qreal(p.y()), containerRect.height())); + return m_window->parent() ? result : m_window->platformScreen()->mapFromLocal(result).toPoint(); +} + +NonClientArea::NonClientArea(QWasmWindow *window, emscripten::val qtWindowElement) + : m_qtWindowElement(qtWindowElement), + m_resizer(std::make_unique<Resizer>(window, m_qtWindowElement)), + m_titleBar(std::make_unique<TitleBar>(window, m_qtWindowElement)) +{ + updateResizability(); +} + +NonClientArea::~NonClientArea() = default; + +void NonClientArea::onClientAreaWidthChange(int width) +{ + m_titleBar->setWidth(width); +} + +void NonClientArea::propagateSizeHints() +{ + updateResizability(); +} + +void NonClientArea::updateResizability() +{ + const auto resizeConstraints = m_resizer->getResizeConstraints(); + const bool nonResizable = resizeConstraints.minShrink.isNull() + && resizeConstraints.maxGrow.isNull() && resizeConstraints.maxGrowTop == 0; + dom::syncCSSClassWith(m_qtWindowElement, "no-resize", nonResizable); +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmwindownonclientarea.h b/src/plugins/platforms/wasm/qwasmwindownonclientarea.h new file mode 100644 index 0000000000..78c77585a0 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmwindownonclientarea.h @@ -0,0 +1,227 @@ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMWINDOWNONCLIENTAREA_H +#define QWASMWINDOWNONCLIENTAREA_H + +#include <QtCore/qrect.h> +#include <QtCore/qtconfigmacros.h> +#include <QtCore/qnamespace.h> + +#include <emscripten/val.h> + +#include <functional> +#include <memory> +#include <string_view> +#include <vector> + +QT_BEGIN_NAMESPACE + +namespace qstdweb { +class EventCallback; +} + +struct PointerEvent; +class QWindow; +class Resizer; +class TitleBar; +class QWasmWindow; + +class NonClientArea +{ +public: + NonClientArea(QWasmWindow *window, emscripten::val containerElement); + ~NonClientArea(); + + void onClientAreaWidthChange(int width); + void propagateSizeHints(); + TitleBar *titleBar() const { return m_titleBar.get(); } + +private: + void updateResizability(); + + emscripten::val m_qtWindowElement; + std::unique_ptr<Resizer> m_resizer; + std::unique_ptr<TitleBar> m_titleBar; +}; + +class WebImageButton +{ +public: + class Callbacks + { + public: + Callbacks(); + Callbacks(std::function<void()> onInteraction, std::function<void()> onClick); + ~Callbacks(); + + Callbacks(const Callbacks &) = delete; + Callbacks(Callbacks &&); + Callbacks &operator=(const Callbacks &) = delete; + Callbacks &operator=(Callbacks &&); + + operator bool() const { return !!m_onInteraction; } + + void onInteraction(); + void onClick(); + + private: + std::function<void()> m_onInteraction; + std::function<void()> m_onClick; + }; + + WebImageButton(); + ~WebImageButton(); + + void setCallbacks(Callbacks callbacks); + void setImage(std::string_view imageData, std::string_view format); + void setVisible(bool visible); + + emscripten::val htmlElement() const { return m_containerElement; } + emscripten::val imageElement() const { return m_imgElement; } + +private: + emscripten::val m_containerElement; + emscripten::val m_imgElement; + + std::unique_ptr<qstdweb::EventCallback> m_webMouseMoveEventCallback; + std::unique_ptr<qstdweb::EventCallback> m_webMouseDownEventCallback; + std::unique_ptr<qstdweb::EventCallback> m_webClickEventCallback; + + Callbacks m_callbacks; +}; + +struct ResizeConstraints { + QPoint minShrink; + QPoint maxGrow; + int maxGrowTop; +}; + +class Resizer +{ +public: + class ResizerElement + { + public: + static constexpr const char *cssClassNameForEdges(Qt::Edges edges) + { + switch (edges) { + case Qt::TopEdge | Qt::LeftEdge:; + return "nw"; + case Qt::TopEdge: + return "n"; + case Qt::TopEdge | Qt::RightEdge: + return "ne"; + case Qt::LeftEdge: + return "w"; + case Qt::RightEdge: + return "e"; + case Qt::BottomEdge | Qt::LeftEdge: + return "sw"; + case Qt::BottomEdge: + return "s"; + case Qt::BottomEdge | Qt::RightEdge: + return "se"; + default: + return ""; + } + } + + ResizerElement(emscripten::val parentElement, Qt::Edges edges, Resizer *resizer); + ~ResizerElement(); + ResizerElement(const ResizerElement &other) = delete; + ResizerElement(ResizerElement &&other); + ResizerElement &operator=(const ResizerElement &other) = delete; + ResizerElement &operator=(ResizerElement &&other) = delete; + + bool onPointerDown(const PointerEvent &event); + bool onPointerMove(const PointerEvent &event); + bool onPointerUp(const PointerEvent &event); + + private: + emscripten::val m_element; + + int m_capturedPointerId = -1; + + const Qt::Edges m_edges; + + Resizer *m_resizer; + + std::unique_ptr<qstdweb::EventCallback> m_mouseDownEvent; + std::unique_ptr<qstdweb::EventCallback> m_mouseMoveEvent; + std::unique_ptr<qstdweb::EventCallback> m_mouseUpEvent; + }; + + using ClickCallback = std::function<void()>; + + Resizer(QWasmWindow *window, emscripten::val parentElement); + ~Resizer(); + + ResizeConstraints getResizeConstraints(); + +private: + void onInteraction(); + void startResize(Qt::Edges resizeEdges, const PointerEvent &event); + void continueResize(const PointerEvent &event); + void finishResize(); + + struct ResizeData + { + Qt::Edges edges = Qt::Edges::fromInt(0); + QPointF originInScreenCoords; + QPoint minShrink; + QPoint maxGrow; + QRect initialBounds; + }; + std::unique_ptr<ResizeData> m_currentResizeData; + + QWasmWindow *m_window; + emscripten::val m_windowElement; + std::vector<std::unique_ptr<ResizerElement>> m_elements; +}; + +class TitleBar +{ +public: + TitleBar(QWasmWindow *window, emscripten::val parentElement); + ~TitleBar(); + + void setTitle(const QString &title); + void setRestoreVisible(bool visible); + void setMaximizeVisible(bool visible); + void setCloseVisible(bool visible); + void setIcon(std::string_view imageData, std::string_view format); + void setWidth(int width); + + QRectF geometry() const; + +private: + bool onPointerDown(const PointerEvent &event); + bool onPointerMove(const PointerEvent &event); + bool onPointerUp(const PointerEvent &event); + bool onDoubleClick(); + + QPointF clipPointWithScreen(const QPointF &pointInTitleBarCoords) const; + + QWasmWindow *m_window; + + emscripten::val m_element; + emscripten::val m_label; + + std::unique_ptr<WebImageButton> m_close; + std::unique_ptr<WebImageButton> m_maximize; + std::unique_ptr<WebImageButton> m_restore; + std::unique_ptr<WebImageButton> m_icon; + + int m_capturedPointerId = -1; + QPointF m_moveStartPoint; + QPoint m_moveStartWindowPosition; + + std::unique_ptr<qstdweb::EventCallback> m_mouseDownEvent; + std::unique_ptr<qstdweb::EventCallback> m_mouseMoveEvent; + std::unique_ptr<qstdweb::EventCallback> m_mouseUpEvent; + std::unique_ptr<qstdweb::EventCallback> m_doubleClickEvent; +}; + +QT_END_NAMESPACE +#endif // QWASMWINDOWNONCLIENTAREA_H diff --git a/src/plugins/platforms/wasm/qwasmwindowstack.cpp b/src/plugins/platforms/wasm/qwasmwindowstack.cpp index 098f1c1ff2..d3769c7a1b 100644 --- a/src/plugins/platforms/wasm/qwasmwindowstack.cpp +++ b/src/plugins/platforms/wasm/qwasmwindowstack.cpp @@ -5,20 +5,38 @@ QT_BEGIN_NAMESPACE -QWasmWindowStack::QWasmWindowStack(TopWindowChangedCallbackType topWindowChangedCallback) - : m_topWindowChangedCallback(std::move(topWindowChangedCallback)) +QWasmWindowStack::QWasmWindowStack(WindowOrderChangedCallbackType windowOrderChangedCallback) + : m_windowOrderChangedCallback(std::move(windowOrderChangedCallback)), + m_regularWindowsBegin(m_windowStack.begin()), + m_alwaysOnTopWindowsBegin(m_windowStack.begin()) { } QWasmWindowStack::~QWasmWindowStack() = default; -void QWasmWindowStack::pushWindow(QWasmWindow *window) +void QWasmWindowStack::pushWindow(QWasmWindow *window, PositionPreference position) { Q_ASSERT(m_windowStack.count(window) == 0); - m_windowStack.push_back(window); - - m_topWindowChangedCallback(); + if (position == PositionPreference::StayOnTop) { + const auto stayOnTopDistance = + std::distance(m_windowStack.begin(), m_alwaysOnTopWindowsBegin); + const auto regularDistance = std::distance(m_windowStack.begin(), m_regularWindowsBegin); + m_windowStack.push_back(window); + m_alwaysOnTopWindowsBegin = m_windowStack.begin() + stayOnTopDistance; + m_regularWindowsBegin = m_windowStack.begin() + regularDistance; + } else if (position == PositionPreference::Regular) { + const auto regularDistance = std::distance(m_windowStack.begin(), m_regularWindowsBegin); + m_alwaysOnTopWindowsBegin = m_windowStack.insert(m_alwaysOnTopWindowsBegin, window) + 1; + m_regularWindowsBegin = m_windowStack.begin() + regularDistance; + } else { + const auto stayOnTopDistance = + std::distance(m_windowStack.begin(), m_alwaysOnTopWindowsBegin); + m_regularWindowsBegin = m_windowStack.insert(m_regularWindowsBegin, window) + 1; + m_alwaysOnTopWindowsBegin = m_windowStack.begin() + stayOnTopDistance + 1; + } + + m_windowOrderChangedCallback(); } void QWasmWindowStack::removeWindow(QWasmWindow *window) @@ -26,41 +44,105 @@ void QWasmWindowStack::removeWindow(QWasmWindow *window) Q_ASSERT(m_windowStack.count(window) == 1); auto it = std::find(m_windowStack.begin(), m_windowStack.end(), window); - const bool removingBottom = m_windowStack.begin() == it; - const bool removingTop = m_windowStack.end() - 1 == it; - if (removingBottom) - m_firstWindowTreatment = FirstWindowTreatment::Regular; + const auto position = getWindowPositionPreference(it); + const auto stayOnTopDistance = std::distance(m_windowStack.begin(), m_alwaysOnTopWindowsBegin); + const auto regularDistance = std::distance(m_windowStack.begin(), m_regularWindowsBegin); m_windowStack.erase(it); - if (removingTop) - m_topWindowChangedCallback(); + m_alwaysOnTopWindowsBegin = m_windowStack.begin() + stayOnTopDistance + - (position != PositionPreference::StayOnTop ? 1 : 0); + m_regularWindowsBegin = m_windowStack.begin() + regularDistance + - (position == PositionPreference::StayOnBottom ? 1 : 0); + + m_windowOrderChangedCallback(); } void QWasmWindowStack::raise(QWasmWindow *window) { Q_ASSERT(m_windowStack.count(window) == 1); - if (window == rootWindow() || window == topWindow()) + if (window == topWindow()) return; - auto it = std::find(regularWindowsBegin(), m_windowStack.end(), window); - std::rotate(it, it + 1, m_windowStack.end()); - m_topWindowChangedCallback(); + auto it = std::find(m_windowStack.begin(), m_windowStack.end(), window); + auto itEnd = ([this, position = getWindowPositionPreference(it)]() { + switch (position) { + case PositionPreference::StayOnTop: + return m_windowStack.end(); + case PositionPreference::Regular: + return m_alwaysOnTopWindowsBegin; + case PositionPreference::StayOnBottom: + return m_regularWindowsBegin; + } + })(); + std::rotate(it, it + 1, itEnd); + m_windowOrderChangedCallback(); } void QWasmWindowStack::lower(QWasmWindow *window) { Q_ASSERT(m_windowStack.count(window) == 1); - if (window == rootWindow()) + if (window == *m_windowStack.begin()) return; - const bool loweringTopWindow = topWindow() == window; - auto it = std::find(regularWindowsBegin(), m_windowStack.end(), window); - std::rotate(regularWindowsBegin(), it, it + 1); - if (loweringTopWindow && topWindow() != window) - m_topWindowChangedCallback(); + auto it = std::find(m_windowStack.begin(), m_windowStack.end(), window); + auto itBegin = ([this, position = getWindowPositionPreference(it)]() { + switch (position) { + case PositionPreference::StayOnTop: + return m_alwaysOnTopWindowsBegin; + case PositionPreference::Regular: + return m_regularWindowsBegin; + case PositionPreference::StayOnBottom: + return m_windowStack.begin(); + } + })(); + + std::rotate(itBegin, it, it + 1); + m_windowOrderChangedCallback(); +} + +void QWasmWindowStack::windowPositionPreferenceChanged(QWasmWindow *window, + PositionPreference position) +{ + auto it = std::find(m_windowStack.begin(), m_windowStack.end(), window); + const auto currentPosition = getWindowPositionPreference(it); + + const auto zones = static_cast<int>(position) - static_cast<int>(currentPosition); + Q_ASSERT(zones != 0); + + if (zones < 0) { + // Perform right rotation so that the window lands on top of regular windows + const auto begin = std::make_reverse_iterator(it + 1); + const auto end = position == PositionPreference::Regular + ? std::make_reverse_iterator(m_alwaysOnTopWindowsBegin) + : std::make_reverse_iterator(m_regularWindowsBegin); + std::rotate(begin, begin + 1, end); + if (zones == -2) { + ++m_alwaysOnTopWindowsBegin; + ++m_regularWindowsBegin; + } else if (position == PositionPreference::Regular) { + ++m_alwaysOnTopWindowsBegin; + } else { + ++m_regularWindowsBegin; + } + } else { + // Perform left rotation so that the window lands at the bottom of always on top windows + const auto begin = it; + const auto end = position == PositionPreference::Regular ? m_regularWindowsBegin + : m_alwaysOnTopWindowsBegin; + std::rotate(begin, begin + 1, end); + if (zones == 2) { + --m_alwaysOnTopWindowsBegin; + --m_regularWindowsBegin; + } else if (position == PositionPreference::Regular) { + --m_regularWindowsBegin; + } else { + --m_alwaysOnTopWindowsBegin; + } + } + m_windowOrderChangedCallback(); } QWasmWindowStack::iterator QWasmWindowStack::begin() @@ -103,21 +185,19 @@ size_t QWasmWindowStack::size() const return m_windowStack.size(); } -QWasmWindow *QWasmWindowStack::rootWindow() const -{ - return m_firstWindowTreatment == FirstWindowTreatment::AlwaysAtBottom ? m_windowStack.first() - : nullptr; -} - QWasmWindow *QWasmWindowStack::topWindow() const { return m_windowStack.empty() ? nullptr : m_windowStack.last(); } -QWasmWindowStack::StorageType::iterator QWasmWindowStack::regularWindowsBegin() +QWasmWindowStack::PositionPreference +QWasmWindowStack::getWindowPositionPreference(StorageType::iterator windowIt) const { - return m_windowStack.begin() - + (m_firstWindowTreatment == FirstWindowTreatment::AlwaysAtBottom ? 1 : 0); + if (windowIt >= m_alwaysOnTopWindowsBegin) + return PositionPreference::StayOnTop; + if (windowIt >= m_regularWindowsBegin) + return PositionPreference::Regular; + return PositionPreference::StayOnBottom; } QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmwindowstack.h b/src/plugins/platforms/wasm/qwasmwindowstack.h index e98ebf904c..c75001157a 100644 --- a/src/plugins/platforms/wasm/qwasmwindowstack.h +++ b/src/plugins/platforms/wasm/qwasmwindowstack.h @@ -21,10 +21,10 @@ class QWasmWindow; // Access to the top element is facilitated by |topWindow|. // Changes to the top element are signaled via the |topWindowChangedCallback| supplied at // construction. -Q_AUTOTEST_EXPORT class QWasmWindowStack +class Q_AUTOTEST_EXPORT QWasmWindowStack { public: - using TopWindowChangedCallbackType = std::function<void()>; + using WindowOrderChangedCallbackType = std::function<void()>; using StorageType = QList<QWasmWindow *>; @@ -32,13 +32,20 @@ public: using const_iterator = StorageType::const_reverse_iterator; using const_reverse_iterator = StorageType::const_iterator; - explicit QWasmWindowStack(TopWindowChangedCallbackType topWindowChangedCallback); + enum class PositionPreference { + StayOnBottom, + Regular, + StayOnTop, + }; + + explicit QWasmWindowStack(WindowOrderChangedCallbackType topWindowChangedCallback); ~QWasmWindowStack(); - void pushWindow(QWasmWindow *window); + void pushWindow(QWasmWindow *window, PositionPreference position); void removeWindow(QWasmWindow *window); void raise(QWasmWindow *window); void lower(QWasmWindow *window); + void windowPositionPreferenceChanged(QWasmWindow *window, PositionPreference position); // Iterates top-to-bottom iterator begin(); @@ -55,14 +62,12 @@ public: QWasmWindow *topWindow() const; private: - enum class FirstWindowTreatment { AlwaysAtBottom, Regular }; - - QWasmWindow *rootWindow() const; - StorageType::iterator regularWindowsBegin(); + PositionPreference getWindowPositionPreference(StorageType::iterator windowIt) const; - TopWindowChangedCallbackType m_topWindowChangedCallback; + WindowOrderChangedCallbackType m_windowOrderChangedCallback; QList<QWasmWindow *> m_windowStack; - FirstWindowTreatment m_firstWindowTreatment = FirstWindowTreatment::AlwaysAtBottom; + StorageType::iterator m_regularWindowsBegin; + StorageType::iterator m_alwaysOnTopWindowsBegin; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmwindowtreenode.cpp b/src/plugins/platforms/wasm/qwasmwindowtreenode.cpp new file mode 100644 index 0000000000..ea8d8dbcfa --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmwindowtreenode.cpp @@ -0,0 +1,108 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmwindowtreenode.h" + +#include "qwasmwindow.h" + +QWasmWindowTreeNode::QWasmWindowTreeNode() + : m_childStack(std::bind(&QWasmWindowTreeNode::onTopWindowChanged, this)) +{ +} + +QWasmWindowTreeNode::~QWasmWindowTreeNode() = default; + +void QWasmWindowTreeNode::onParentChanged(QWasmWindowTreeNode *previousParent, + QWasmWindowTreeNode *currentParent, + QWasmWindowStack::PositionPreference positionPreference) +{ + auto *window = asWasmWindow(); + if (previousParent) { + previousParent->m_childStack.removeWindow(window); + previousParent->onSubtreeChanged(QWasmWindowTreeNodeChangeType::NodeRemoval, previousParent, + window); + } + + if (currentParent) { + currentParent->m_childStack.pushWindow(window, positionPreference); + currentParent->onSubtreeChanged(QWasmWindowTreeNodeChangeType::NodeInsertion, currentParent, + window); + } +} + +QWasmWindow *QWasmWindowTreeNode::asWasmWindow() +{ + return nullptr; +} + +void QWasmWindowTreeNode::onSubtreeChanged(QWasmWindowTreeNodeChangeType changeType, + QWasmWindowTreeNode *parent, QWasmWindow *child) +{ + if (changeType == QWasmWindowTreeNodeChangeType::NodeInsertion && parent == this + && m_childStack.topWindow() + && m_childStack.topWindow()->window()) { + + const QVariant showWithoutActivating = m_childStack.topWindow()->window()->property("_q_showWithoutActivating"); + if (!showWithoutActivating.isValid() || !showWithoutActivating.toBool()) + m_childStack.topWindow()->requestActivateWindow(); + } + + if (parentNode()) + parentNode()->onSubtreeChanged(changeType, parent, child); +} + +void QWasmWindowTreeNode::setWindowZOrder(QWasmWindow *window, int z) +{ + window->setZOrder(z); +} + +void QWasmWindowTreeNode::onPositionPreferenceChanged( + QWasmWindowStack::PositionPreference positionPreference) +{ + if (parentNode()) { + parentNode()->m_childStack.windowPositionPreferenceChanged(asWasmWindow(), + positionPreference); + } +} + +void QWasmWindowTreeNode::setAsActiveNode() +{ + if (parentNode()) + parentNode()->setActiveChildNode(asWasmWindow()); +} + +void QWasmWindowTreeNode::bringToTop() +{ + if (!parentNode()) + return; + parentNode()->m_childStack.raise(asWasmWindow()); + parentNode()->bringToTop(); +} + +void QWasmWindowTreeNode::sendToBottom() +{ + if (!parentNode()) + return; + m_childStack.lower(asWasmWindow()); +} + +void QWasmWindowTreeNode::onTopWindowChanged() +{ + constexpr int zOrderForElementInFrontOfScreen = 3; + int z = zOrderForElementInFrontOfScreen; + std::for_each(m_childStack.rbegin(), m_childStack.rend(), + [this, &z](QWasmWindow *window) { setWindowZOrder(window, z++); }); +} + +void QWasmWindowTreeNode::setActiveChildNode(QWasmWindow *activeChild) +{ + m_activeChild = activeChild; + + auto it = m_childStack.begin(); + if (it == m_childStack.end()) + return; + for (; it != m_childStack.end(); ++it) + (*it)->onActivationChanged(*it == m_activeChild); + + setAsActiveNode(); +} diff --git a/src/plugins/platforms/wasm/qwasmwindowtreenode.h b/src/plugins/platforms/wasm/qwasmwindowtreenode.h new file mode 100644 index 0000000000..344fdb43cb --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmwindowtreenode.h @@ -0,0 +1,53 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMWINDOWTREENODE_H +#define QWASMWINDOWTREENODE_H + +#include "qwasmwindowstack.h" + +namespace emscripten { +class val; +} + +class QWasmWindow; + +enum class QWasmWindowTreeNodeChangeType { + NodeInsertion, + NodeRemoval, +}; + +class QWasmWindowTreeNode +{ +public: + QWasmWindowTreeNode(); + virtual ~QWasmWindowTreeNode(); + + virtual emscripten::val containerElement() = 0; + virtual QWasmWindowTreeNode *parentNode() = 0; + +protected: + virtual void onParentChanged(QWasmWindowTreeNode *previous, QWasmWindowTreeNode *current, + QWasmWindowStack::PositionPreference positionPreference); + virtual QWasmWindow *asWasmWindow(); + virtual void onSubtreeChanged(QWasmWindowTreeNodeChangeType changeType, + QWasmWindowTreeNode *parent, QWasmWindow *child); + virtual void setWindowZOrder(QWasmWindow *window, int z); + + void onPositionPreferenceChanged(QWasmWindowStack::PositionPreference positionPreference); + void setAsActiveNode(); + void bringToTop(); + void sendToBottom(); + + const QWasmWindowStack &childStack() const { return m_childStack; } + QWasmWindow *activeChild() const { return m_activeChild; } + +private: + void onTopWindowChanged(); + void setActiveChildNode(QWasmWindow *activeChild); + + QWasmWindowStack m_childStack; + QWasmWindow *m_activeChild = nullptr; +}; + +#endif // QWASMWINDOWTREENODE_H diff --git a/src/plugins/platforms/wasm/wasm_shell.html b/src/plugins/platforms/wasm/wasm_shell.html index aaa121981d..6e93955552 100644 --- a/src/plugins/platforms/wasm/wasm_shell.html +++ b/src/plugins/platforms/wasm/wasm_shell.html @@ -1,3 +1,8 @@ +<!-- +Copyright (C) 2024 The Qt Company Ltd. +SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only +--> + <!doctype html> <html lang="en-us"> <head> @@ -27,42 +32,48 @@ </figure> <div id="screen"></div> - <script type='text/javascript'> - let qtLoader = undefined; - function init() { - var spinner = document.querySelector('#qtspinner'); - var canvas = document.querySelector('#screen'); - var status = document.querySelector('#qtstatus') + <script type="text/javascript"> + async function init() + { + const spinner = document.querySelector('#qtspinner'); + const screen = document.querySelector('#screen'); + const status = document.querySelector('#qtstatus'); + + const showUi = (ui) => { + [spinner, screen].forEach(element => element.style.display = 'none'); + if (screen === ui) + screen.style.position = 'default'; + ui.style.display = 'block'; + } + + try { + showUi(spinner); + status.innerHTML = 'Loading...'; - qtLoader = new QtLoader({ - canvasElements : [canvas], - showLoader: function(loaderStatus) { - spinner.style.display = 'block'; - canvas.style.display = 'none'; - status.innerHTML = loaderStatus + "..."; - }, - showError: function(errorText) { - status.innerHTML = errorText; - spinner.style.display = 'block'; - canvas.style.display = 'none'; - }, - showExit: function() { - status.innerHTML = "Application exit"; - if (qtLoader.exitCode !== undefined) - status.innerHTML += " with code " + qtLoader.exitCode; - if (qtLoader.exitText !== undefined) - status.innerHTML += " (" + qtLoader.exitText + ")"; - spinner.style.display = 'block'; - canvas.style.display = 'none'; - }, - showCanvas: function() { - spinner.style.display = 'none'; - canvas.style.display = 'block'; - }, - }); - qtLoader.loadEmscriptenModule("@APPNAME@"); - } + const instance = await qtLoad({ + qt: { + onLoaded: () => showUi(screen), + onExit: exitData => + { + status.innerHTML = 'Application exit'; + status.innerHTML += + exitData.code !== undefined ? ` with code ${exitData.code}` : ''; + status.innerHTML += + exitData.text !== undefined ? ` (${exitData.text})` : ''; + showUi(spinner); + }, + entryFunction: window.@APPEXPORTNAME@, + containerElements: [screen], + @PRELOAD@ + } + }); + } catch (e) { + console.error(e); + console.error(e.stack); + } + } </script> + <script src="@APPNAME@.js"></script> <script type="text/javascript" src="qtloader.js"></script> </body> </html> |